#!/usr/bin/env python3 """Build materials/.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"([^<]*)", 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)