| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- #!/usr/bin/env python3
- """Build materials/<encoded>.png thumbnails: curated Wikimedia Commons photos where a
- freely licensed photo actually depicts the material, generated swatches otherwise."""
- import io, os, re, sys, time, math, random, json
- import requests
- from PIL import Image, ImageDraw, ImageFilter
- ROOT = "/home/zoadian/projects/visicut_profiles"
- SIZE = 160
- API = "https://commons.wikimedia.org/w/api.php"
- UA = {"User-Agent": "visicut-settings-thumbnailer/1.0 (https://wiki.fablab-rothenburg.de/; claudeai@zoadian.de)"}
- ALLOWED = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-")
- def tp(n): return "".join(c if c in ALLOWED else "_%d_" % ord(c) for c in n)
- # material -> (Commons file title, optional crop hint "cx,cy" in 0..1 of the source)
- PHOTO = {
- "Sperrholz Birke": "Birch plywood.jpg",
- "Sperrholz Pappel": "Poplar plywood.png",
- "Sperrholz Buche": "Buche gedämpft.JPG",
- "Sperrholz Gabun": "Okumé.JPG",
- "Sperrholz Linde": "Plywood texture.JPG",
- "Acorn Sperrholz": "Wood 044 plywood.jpg",
- "Kiefer Massivholz": "4 conifer wood samples.jpg",
- "MDF": "MDF Sample.jpg",
- "Hartfaserplatte": "Isorel, masonite, hardboard..jpg",
- "Wellpappe": "Corrugated Cardboard.JPG",
- "Finnpappe": "CardboardSet001 16K-PNG Color.png",
- "Kraftplex": "Kraft tileable 1024x1024.png",
- "Dickes Bastelpapier": "Construction Paper.JPG",
- "Moosgummi": "EVA sheet.png",
- "PP-Folie": "9 x Plakene Oasis Dina4 - size A4 - 0.5 mm - A.jpg",
- "Leder (chromfrei)": "Last Drip Designs Keycase.jpg",
- }
- # generated swatches: (base colour, texture style, extra)
- SWATCH = {
- "Acrylglas (PMMA)": ("#cfe3ef", "gloss"),
- "Glas (nur Gravieren)":("#d8e6e4", "glass"),
- "PET-Folie": ("#dfe7ea", "film"),
- "Silikon Gummimatte": ("#b9bdc0", "matte"),
- "Stempelgummi": ("#8a5a3a", "matte"),
- "Tesa 6930 Laserfolie":("#e8e6df", "film"),
- "Eloxiertes Blech": ("#b6bcc2", "brushed"),
- "Judopokal-Platte": ("#d8ae3c", "brushed"),
- "Papier": ("#f5f3ee", "fibre"),
- "Karton 160g": ("#ece2cd", "fibre"),
- "Karton 200g": ("#e4d8bf", "fibre"),
- "Karton 300g": ("#dccbab", "fibre"),
- "Siebdruckkarton": ("#cbb894", "fibre"),
- "Kromapappe": ("#8e8d88", "fibre"),
- "Baumwollstoff": ("#e6e0d4", "weave"),
- "SnapPap": ("#b58a5e", "crumple"),
- "Depron Schaum (EPP)": ("#eceff1", "foam"),
- "Airplak Kartonschaum":("#efe9dc", "foam"),
- "Sandwich Karton (Graphicboard)": ("#e3d6bb", "foam"),
- "Schichtstoff (HPL)": ("#6a6a68", "gloss"),
- "HDF (Hochdichte Faserplatte)": ("#6e5c46", "matte"),
- "Graue Pappe": ("#9c9c96", "fibre"),
- "Büttenpapier 300g": ("#f2ece0", "fibre"),
- "POM Delrin": ("#eeeae2", "matte"),
- "Filz Polyester": ("#8f9296", "felt"),
- "Platane": ("#d9c3a3", "grain"),
- }
- # --------------------------------------------------------------- Commons fetch
- def imageinfo(titles):
- out = {}
- titles = list(titles)
- for i in range(0, len(titles), 25):
- chunk = titles[i:i + 25]
- for attempt in range(6):
- r = requests.get(API, headers=UA, params={
- "action": "query", "format": "json",
- "titles": "|".join("File:" + t for t in chunk),
- "prop": "imageinfo", "iiprop": "url|extmetadata|size|mime",
- "iiurlwidth": str(SIZE * 4)}, timeout=60)
- if r.status_code in (429, 503):
- time.sleep(3 * (attempt + 1)); continue
- r.raise_for_status(); break
- for p in r.json().get("query", {}).get("pages", {}).values():
- if "missing" in p:
- print(" MISSING: " + p["title"]); continue
- ii = p["imageinfo"][0]; md = ii.get("extmetadata", {})
- strip = lambda s: re.sub(r"<[^>]+>", "", s or "").strip()
- out[p["title"][5:]] = {
- "thumb": ii.get("thumburl") or ii["url"],
- "page": ii["descriptionurl"],
- "license": strip(md.get("LicenseShortName", {}).get("value")),
- "author": strip(md.get("Artist", {}).get("value"))[:80],
- }
- time.sleep(1.0)
- return out
- def square(im):
- im = im.convert("RGB")
- w, h = im.size
- s = min(w, h)
- # centre crop, but for very wide sample-strip images take the middle band
- im = im.crop(((w - s) // 2, (h - s) // 2, (w - s) // 2 + s, (h - s) // 2 + s))
- return im.resize((SIZE, SIZE), Image.LANCZOS)
- # --------------------------------------------------------------- swatches
- def hexrgb(h): return tuple(int(h[i:i+2], 16) for i in (1, 3, 5))
- def swatch(base, style, seed):
- rnd = random.Random(seed)
- r, g, b = hexrgb(base)
- im = Image.new("RGB", (SIZE, SIZE), (r, g, b))
- d = ImageDraw.Draw(im, "RGBA")
- def jitter(px, amp):
- pix = im.load()
- for y in range(SIZE):
- for x in range(SIZE):
- n = rnd.randint(-amp, amp)
- c = pix[x, y]
- pix[x, y] = tuple(max(0, min(255, v + n)) for v in c)
- if style == "fibre":
- jitter(im, 9)
- for _ in range(SIZE * 5):
- x, y = rnd.randrange(SIZE), rnd.randrange(SIZE)
- l = rnd.randint(3, 11); a = rnd.random() * math.pi
- d.line([x, y, x + l * math.cos(a), y + l * math.sin(a)],
- fill=(255, 255, 255, 40) if rnd.random() < .5 else (0, 0, 0, 22))
- im = im.filter(ImageFilter.SMOOTH)
- elif style == "weave":
- p = 3
- for y in range(0, SIZE, p):
- for x in range(0, SIZE, p):
- up = ((x // p) + (y // p)) % 2 == 0
- d.rectangle([x, y, x + p - 1, y + p - 1],
- fill=(255, 255, 255, 22) if up else (0, 0, 0, 20))
- jitter(im, 5)
- im = im.filter(ImageFilter.GaussianBlur(0.4))
- elif style == "foam":
- for _ in range(900):
- x, y = rnd.randrange(SIZE), rnd.randrange(SIZE); rr = rnd.randint(1, 3)
- d.ellipse([x - rr, y - rr, x + rr, y + rr], fill=(0, 0, 0, 14))
- d.ellipse([x - rr, y - rr, x + rr - 1, y + rr - 1], outline=(255, 255, 255, 40))
- im = im.filter(ImageFilter.SMOOTH)
- elif style == "brushed":
- for _ in range(SIZE * 14):
- y = rnd.randrange(SIZE); x = rnd.randrange(SIZE); l = rnd.randint(8, 46)
- a = rnd.randint(-20, 20)
- d.line([x, y, x + l, y], fill=(255, 255, 255, a) if a > 0 else (0, 0, 0, -a))
- im = im.filter(ImageFilter.GaussianBlur(0.3))
- elif style == "gloss":
- for y in range(SIZE):
- d.line([0, y, SIZE, y], fill=(255, 255, 255, int(46 * (1 - y / SIZE))))
- d.polygon([(0, SIZE), (SIZE * .75, 0), (SIZE, 0), (0, SIZE * .95)], fill=(255, 255, 255, 52))
- d.rectangle([0, 0, SIZE - 1, SIZE - 1], outline=(255, 255, 255, 120))
- elif style == "glass":
- for y in range(SIZE):
- d.line([0, y, SIZE, y], fill=(255, 255, 255, int(30 * (1 - y / SIZE))))
- jitter(im, 6)
- im = im.filter(ImageFilter.GaussianBlur(1.1))
- d = ImageDraw.Draw(im, "RGBA")
- d.polygon([(0, SIZE), (SIZE * .6, 0), (SIZE * .85, 0), (0, SIZE * .75)], fill=(255, 255, 255, 40))
- elif style == "film":
- for y in range(SIZE):
- d.line([0, y, SIZE, y], fill=(255, 255, 255, int(34 * abs(math.sin(y / 22.0)))))
- d.rectangle([0, 0, SIZE - 1, SIZE - 1], outline=(180, 190, 195, 160))
- elif style == "crumple":
- jitter(im, 12)
- for _ in range(60):
- x1, y1 = rnd.randrange(SIZE), rnd.randrange(SIZE)
- d.line([x1, y1, x1 + rnd.randint(-40, 40), y1 + rnd.randint(-40, 40)],
- fill=(255, 255, 255, 30), width=1)
- im = im.filter(ImageFilter.GaussianBlur(0.7))
- elif style == "felt":
- jitter(im, 10)
- for _ in range(SIZE * 22):
- x, y = rnd.randrange(SIZE), rnd.randrange(SIZE)
- l = rnd.randint(2, 6); a = rnd.random() * math.pi * 2
- d.line([x, y, x + l * math.cos(a), y + l * math.sin(a)],
- fill=(255, 255, 255, 26) if rnd.random() < .5 else (0, 0, 0, 24))
- im = im.filter(ImageFilter.GaussianBlur(0.5))
- elif style == "grain":
- pix = im.load()
- for x in range(SIZE):
- for y in range(SIZE):
- v = math.sin((x + 9 * math.sin(y / 34.0)) / 2.6) * 10 + rnd.randint(-6, 6)
- pix[x, y] = tuple(max(0, min(255, int(c + v))) for c in pix[x, y])
- d = ImageDraw.Draw(im, "RGBA")
- for _ in range(7):
- x = rnd.randrange(SIZE)
- d.line([x, 0, x + rnd.randint(-12, 12), SIZE], fill=(0, 0, 0, 26), width=rnd.randint(1, 2))
- im = im.filter(ImageFilter.SMOOTH)
- else: # matte
- jitter(im, 7)
- im = im.filter(ImageFilter.SMOOTH)
- return im
- # --------------------------------------------------------------- main
- if __name__ == "__main__":
- mats = []
- for fn in sorted(os.listdir(os.path.join(ROOT, "materials"))):
- if fn.endswith(".xml"):
- txt = open(os.path.join(ROOT, "materials", fn), encoding="utf-8").read()
- mats.append(re.search(r"<name>([^<]*)</name>", txt).group(1))
- info = imageinfo(set(PHOTO.values()))
- credits = []
- for m in mats:
- out = os.path.join(ROOT, "materials", tp(m) + ".png")
- title = PHOTO.get(m)
- if title and title in info:
- i = info[title]
- try:
- raw = requests.get(i["thumb"], headers=UA, timeout=60)
- raw.raise_for_status()
- square(Image.open(io.BytesIO(raw.content))).save(out, optimize=True)
- credits.append((m, "photo", title, i["license"], i["author"], i["page"]))
- print(" photo %-32s %s" % (m, title[:50]))
- time.sleep(0.5)
- continue
- except Exception as e:
- print(" FAILED %-32s %s (%s)" % (m, title[:40], e))
- base, style = SWATCH.get(m, ("#c9c4ba", "matte"))
- swatch(base, style, m).save(out, optimize=True)
- credits.append((m, "swatch", style, "generated", "", ""))
- print(" swatch %-32s %s" % (m, style))
- json.dump(credits, open("/tmp/claude-1000/-home-zoadian-projects-visicut-profiles/c951df34-51ea-44d2-be54-fcc330e0ab58/scratchpad/credits.json", "w"), indent=1, ensure_ascii=False)
|