#!/usr/bin/env python3 """Spore prints: what a morel cap might leave on dark paper overnight. Pure standard library. Every picture comes from one seed (random.Random(seed)). How it works, briefly: * A cap is laid on its side on the paper. It is modelled as a solid of revolution (a pointed egg) covered in rows of roughly hexagonal pits. * Each spore starts at a random point inside a pit and is shot straight out from the surface for a short distance (morels shoot their spores; they do not drop them from gills). Then it sinks to the paper, pushed sideways by a faint air current and jostled a little, more the further it has to fall. * Pits on the underside print almost directly onto the paper, which leaves the ghost of the honeycomb. Pits on the sides and top make the halo and the downwind plume. Spores that would land on the cap itself are dropped, because the cap was lifted away afterwards. * Landings are counted per pixel, blurred slightly, and the count is turned into a tone on a single ramp: blue-black paper, through honey, to cream. * Optional small details: a second cap, a bee's footprints, a fallen hair. Usage: python3 spore_prints.py render the six prints + thumbnails + spore-prints.json python3 spore_prints.py --seed 12 --out some.png render one seed python3 spore_prints.py --params 1 60 list parameters for a range of seeds python3 spore_prints.py --verify re-read every PNG, print stats and ASCII python3 spore_prints.py --fragment print caption data as HTML figures """ import json, math, os, random, struct, sys, time, zlib W = H = 900 THUMB = 300 HERE = os.path.dirname(os.path.abspath(__file__)) SEEDS = [2, 8, 12, 15, 26, 53] # chosen by hand from --params; see NOTES.md SQ3 = math.sqrt(3.0) HEX = ((1.0, 0.0, -0.5, SQ3 / 2), (-0.5, SQ3 / 2, -0.5, -SQ3 / 2), (-0.5, -SQ3 / 2, 1.0, 0.0)) # ---------------------------------------------------------------- parameters def make_params(rng): """Draw every parameter of a print from the seeded generator.""" p = {} second = rng.random() < 0.30 L = rng.uniform(400, 600) * (0.82 if second else 1.0) ratio = rng.uniform(0.20, 0.32) drift_deg = rng.uniform(0, 360) drift = rng.uniform(0.03, 0.85) p["cap"] = { "L": L, "W": L * ratio, "shape_a": rng.uniform(0.38, 0.60), "shape_b": rng.uniform(0.70, 1.2), "lump_a": rng.uniform(0.02, 0.07), "lump_f": rng.uniform(1.5, 3.5), "lump_ph": rng.uniform(0, 6.283), "bend": rng.uniform(-0.06, 0.06) * L, "axis_deg": rng.uniform(0, 360), "pitch": rng.uniform(17, 30), "elong": rng.uniform(1.0, 1.7), } p["drift_deg"] = drift_deg p["drift"] = drift p["grains"] = int(math.exp(rng.uniform(math.log(5e6), math.log(2e7)))) p["shot"] = rng.uniform(10, 40) p["ghost"] = rng.uniform(0.15, 1.0) p["under"] = rng.uniform(0.55, 1.0) p["jostle"] = rng.uniform(2.2, 4.5) p["settle"] = rng.uniform(0.12, 0.32) p["warmth"] = rng.uniform(0, 1) p["bee"] = rng.random() < 0.35 p["hair"] = rng.random() < 0.30 a = math.radians(drift_deg) p["cap"]["cx"] = W / 2 - math.cos(a) * drift * 80 + rng.uniform(-30, 30) p["cap"]["cy"] = H / 2 - math.sin(a) * drift * 80 + rng.uniform(-30, 30) p["second"] = None if second: c1 = p["cap"] L2 = L * rng.uniform(0.42, 0.66) c2 = { "L": L2, "W": L2 * rng.uniform(0.22, 0.33), "shape_a": rng.uniform(0.38, 0.60), "shape_b": rng.uniform(0.70, 1.2), "lump_a": rng.uniform(0.02, 0.07), "lump_f": rng.uniform(1.5, 3.5), "lump_ph": rng.uniform(0, 6.283), "bend": rng.uniform(-0.06, 0.06) * L2, "axis_deg": rng.uniform(0, 360), "pitch": c1["pitch"] * rng.uniform(0.75, 1.0), "elong": rng.uniform(1.0, 1.7), } side = 90 if rng.random() < 0.5 else -90 d = math.radians(c1["axis_deg"] + side + rng.uniform(-35, 35)) dist = c1["W"] + c2["W"] + rng.uniform(60, 140) c2["cx"] = min(730.0, max(170.0, c1["cx"] + math.cos(d) * dist)) c2["cy"] = min(730.0, max(170.0, c1["cy"] + math.sin(d) * dist)) p["second"] = c2 return p def direction_word(deg): """Screen direction for an angle measured clockwise from 'right' (y grows downward).""" words = ["right", "lower right", "down", "lower left", "left", "upper left", "up", "upper right"] return words[int(((deg % 360) + 22.5) // 45) % 8] # ---------------------------------------------------------------- the cap def cap_tables(c): """Per-pixel tables along the cap's axis: half-width, bend, and surface-normal factors.""" L = c["L"] n = int(L) + 2 a, b = c["shape_a"], c["shape_b"] tp = a / (a + b) peak = tp ** a * (1 - tp) ** b hw = [0.0] * n bend = [0.0] * n for i in range(n): t = min(1.0, max(0.0, (i + 0.5) / L)) base = t ** a * (1 - t) ** b / peak # blunt at the stem end, pointed at the tip lump = 1 + c["lump_a"] * math.sin(6.283 * c["lump_f"] * t + c["lump_ph"]) hw[i] = c["W"] * base * lump bend[i] = c["bend"] * math.sin(math.pi * t) nc = [1.0] * n na = [0.0] * n for i in range(n): slope = hw[min(n - 1, i + 1)] - hw[max(0, i - 1)] slope = max(-4.0, min(4.0, slope / 2.0)) k = 1.0 / math.sqrt(1 + slope * slope) nc[i] = k na[i] = -slope * k return hw, bend, nc, na def make_pits(c, hw, rng): """Rows of pits around the cap. Each pit: (s, phi, axial radius, angular radius, weight).""" L, pitch = c["L"], c["pitch"] rowstep = pitch * (SQ3 / 2) * c["elong"] pits = [] s = rowstep * 0.45 row = 0 while s < L - rowstep * 0.3: w = hw[int(s)] if w > 3: n = max(3, int(round(2 * math.pi * w / pitch))) arc = 2 * math.pi * w / n phase = rng.uniform(-0.12, 0.12) + (0.5 if row % 2 else 0.0) for j in range(n): fill = rng.uniform(0.76, 0.95) ps = s + rng.uniform(-0.10, 0.10) * rowstep phi = (j + phase + rng.uniform(-0.10, 0.10)) * 2 * math.pi / n rs = fill * rowstep / 1.5 rarc = fill * arc / SQ3 weight = rs * rarc * rng.uniform(0.55, 1.3) pits.append((ps, phi, rs, rarc / w, weight)) s += rowstep row += 1 return pits def simulate(c, n_launch, p, rng, dens, clumps): """Launch n_launch spores from one cap; count landings in dens, collect clump positions.""" hw, bend, nc, na = cap_tables(c) pits = make_pits(c, hw, rng) total = sum(pt[4] for pt in pits) L = c["L"] halfL = L / 2 ang = math.radians(c["axis_deg"]) ux, uy = math.cos(ang), math.sin(ang) cx, cy = c["cx"], c["cy"] da = math.radians(p["drift_deg"]) dx, dy = math.cos(da) * p["drift"], math.sin(da) * p["drift"] dr_ax = dx * ux + dy * uy dr_lat = -dx * uy + dy * ux R = p["shot"] under = p["under"] settle = p["settle"] jost = p["jostle"] sig_u = 0.8 + (1 - p["ghost"]) * 0.22 * c["pitch"] p_clump = 0.0005 rnd, gauss = rng.random, rng.gauss sin, cos, log, sqrt = math.sin, math.cos, math.log, math.sqrt xmax, ymax = W - 5, H - 5 landed = 0 for ps, pphi, rs, rphi, weight in pits: cnt = int(n_launch * weight / total + rnd()) for _ in range(cnt): e = HEX[int(rnd() * 3)] u = rnd(); v = rnd() s = ps + rs * (u * e[0] + v * e[2]) if s <= 0.0 or s >= L: continue i = int(s) w = hw[i] phi = pphi + rphi * (u * e[1] + v * e[3]) sn = sin(phi); cs = cos(phi) if rnd() > (under if cs > 0.0 else settle): continue # underside pits partly blocked; upper spores mostly carried off k = nc[i] z = w * (1.0 - cs) r = R * (0.25 - log(1.0 - rnd())) down = cs * k clump = False if down > 0.0 and r * down >= z: # shot straight onto the paper beneath the cap t = z / down sg = sig_u + 0.04 * t lat = bend[i] + w * sn + sn * k * t + gauss(0.0, sg) ax = s + na[i] * t + gauss(0.0, sg) else: hgt = z - down * r f = 0.4 + 1.2 * rnd() sg = 1.2 + jost * sqrt(hgt) if down <= 0.0 else sig_u + 0.25 * jost * sqrt(hgt) if rnd() < p_clump: clump = True sg *= 5.0 f *= 1.8 if down > 0.0: f *= 0.3 # sheltered under the overhang lat = bend[i] + w * sn + sn * k * r + hgt * f * dr_lat + gauss(0.0, sg) ax = s + na[i] * r + hgt * f * dr_ax + gauss(0.0, sg) if down <= 0.0 and 0.0 < ax < L: i2 = int(ax) if abs(lat - bend[i2]) < hw[i2]: continue # came down on the cap, not the paper a = ax - halfL x = cx + a * ux - lat * uy y = cy + a * uy + lat * ux if 5.0 <= x < xmax and 5.0 <= y < ymax: idx = int(y) * W + int(x) if clump: clumps.append(idx) else: dens[idx] += 1.0 landed += 1 return landed, len(pits) # ---------------------------------------------------------------- image helpers def box3(d, off): """Sum of three neighbours along one axis (off=1 horizontal, off=W vertical).""" pad = [0.0] * off return pad + [a + b + c for a, b, c in zip(d, d[off:], d[2 * off:])] + pad def blur(d): for _ in range(2): d = box3(d, 1) d = box3(d, W) return [v * (1 / 81.0) for v in d] def paper(rng): """Paper tone per pixel, as a ramp index around 12: mottle, vignette, a few fibres.""" G = 10 cell = W / G lat = [[rng.uniform(-1.6, 1.6) for _ in range(G + 2)] for _ in range(G + 2)] xs = [] for x in range(W): g = x / cell k = int(g) f = g - k xs.append((k, f * f * (3 - 2 * f), ((x - W / 2) / (W * 0.7)) ** 2)) out = [] for y in range(H): g = y / cell j = int(g) f = g - j f = f * f * (3 - 2 * f) row = [a + (b - a) * f for a, b in zip(lat[j], lat[j + 1])] vy = ((y - H / 2) / (H * 0.7)) ** 2 out.extend([12.0 + row[k] + (row[k + 1] - row[k]) * fx - 9.0 * (vx + vy) ** 1.3 for k, fx, vx in xs]) for _ in range(420): # paper fibres: short faint curved strokes x = rng.uniform(0, W); y = rng.uniform(0, H) a = rng.uniform(0, 6.283) curve = rng.uniform(-0.03, 0.03) amt = rng.choice((2.0, 2.0, 3.0, 3.0, 4.0, 5.0, -3.0)) for _ in range(int(rng.uniform(12, 70))): ix, iy = int(x), int(y) if 0 <= ix < W and 0 <= iy < H: out[iy * W + ix] += amt x += math.cos(a); y += math.sin(a) a += curve return [0.0 if v < 0.0 else v for v in out] def bee_trail(p, rng, d, ref): """A bee walks out of the dust: its feet lift dust where it is thick and set a little down after.""" c = p["cap"] B = 70.0 # body length in pixels (about 12 mm at this scale) a0 = rng.uniform(0, 6.283) rad = rng.uniform(0.2, 0.9) * c["W"] x = c["cx"] + math.cos(a0) * rad y = c["cy"] + math.sin(a0) * rad head = a0 + rng.uniform(-0.9, 0.9) feet = ((0.36, 0.30), (0.0, -0.42), (-0.40, 0.33)) carry = 0.0 steps = 0 side = 1 while 20 < x < W - 20 and 20 < y < H - 20 and steps < 40: ch, sh = math.cos(head), math.sin(head) for fwd, lat in feet: fx = x + (fwd * ch - side * lat * sh) * B + rng.uniform(-1.5, 1.5) fy = y + (fwd * sh + side * lat * ch) * B + rng.uniform(-1.5, 1.5) ix, iy = int(fx), int(fy) if not (4 <= ix < W - 4 and 4 <= iy < H - 4): continue here = d[iy * W + ix] put = 0.45 * carry carry = 0.72 * carry + 0.30 * min(here, ref) for yy in range(iy - 4, iy + 5): for xx in range(ix - 4, ix + 5): q = ((xx + 0.5 - fx) ** 2 + (yy + 0.5 - fy) ** 2) / 9.0 if q < 1.0: fall = 1.0 - q k = yy * W + xx d[k] = d[k] * (1.0 - 0.85 * fall) + put * fall x += ch * B * 0.40; y += sh * B * 0.40 head += rng.uniform(-0.16, 0.16) side = -side steps += 1 return steps def hair_cover(rng): """A fallen hair: one long thin curve. Returns {pixel index: coverage 0..1}.""" x = rng.uniform(150, W - 150); y = rng.uniform(150, H - 150) a = rng.uniform(0, 6.283) curve = rng.uniform(-0.006, 0.006) length = rng.uniform(320, 620) cov = {} n = int(length * 2) for i in range(n): x += math.cos(a) * 0.5; y += math.sin(a) * 0.5 curve += rng.uniform(-0.0004, 0.0004) curve = max(-0.012, min(0.012, curve)) a += curve * 0.5 fx, fy = x - 0.5, y - 0.5 ix, iy = int(math.floor(fx)), int(math.floor(fy)) tx, ty = fx - ix, fy - iy for xx, yy, wgt in ((ix, iy, (1 - tx) * (1 - ty)), (ix + 1, iy, tx * (1 - ty)), (ix, iy + 1, (1 - tx) * ty), (ix + 1, iy + 1, tx * ty)): if 0 <= xx < W and 0 <= yy < H: k = yy * W + xx cov[k] = min(1.0, cov.get(k, 0.0) + wgt * 0.5) return cov, length def palette(warmth): """256 colours on one ramp: dark paper, lighter paper, grey-brown dust, honey, cream.""" def lerp(a, b, t): return tuple(a[i] + (b[i] - a[i]) * t for i in range(3)) pale = {105: (120, 108, 88), 160: (190, 172, 130), 210: (226, 208, 160), 255: (244, 240, 226)} gold = {105: (120, 96, 58), 160: (196, 154, 84), 210: (232, 192, 110), 255: (246, 230, 190)} stops = [(0, (5, 9, 20)), (12, (11, 18, 36)), (26, (22, 33, 54)), (60, (58, 60, 66))] stops += [(k, lerp(pale[k], gold[k], warmth)) for k in (105, 160, 210, 255)] out = bytearray() for i in range(256): for (i0, c0), (i1, c1) in zip(stops, stops[1:]): if i0 <= i <= i1: col = lerp(c0, c1, (i - i0) / (i1 - i0)) break out += bytes(int(round(v)) for v in col) return bytes(out) def write_png(path, w, h, idx, pal): """Indexed-colour PNG written by hand. Tries three row filters and keeps the smallest.""" def chunk(tag, data): return struct.pack(">I", len(data)) + tag + data + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF) rows = [bytes(idx[y * w:(y + 1) * w]) for y in range(h)] best = None for ftype in (0, 1, 2): raw = bytearray() prev = bytes(w) for row in rows: raw.append(ftype) if ftype == 0: raw += row elif ftype == 1: raw += bytes([(a - b) & 255 for a, b in zip(row, b"\0" + row)]) else: raw += bytes([(a - b) & 255 for a, b in zip(row, prev)]) prev = row z = zlib.compress(bytes(raw), 9) if best is None or len(z) < len(best): best = z data = (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 3, 0, 0, 0)) + chunk(b"PLTE", pal) + chunk(b"IDAT", best) + chunk(b"IEND", b"")) with open(path, "wb") as f: f.write(data) return len(data) def thumbnail(idx): """300x300 from 900x900 by averaging 3x3 blocks of ramp indices.""" out = bytearray() for ty in range(THUMB): r0 = idx[(3 * ty) * W:(3 * ty + 1) * W] r1 = idx[(3 * ty + 1) * W:(3 * ty + 2) * W] r2 = idx[(3 * ty + 2) * W:(3 * ty + 3) * W] s = [a + b + c for a, b, c in zip(r0, r1, r2)] out += bytes([(s[i] + s[i + 1] + s[i + 2] + 4) // 9 for i in range(0, W, 3)]) return out # ---------------------------------------------------------------- one print def render(seed, path, thumb_path=None): t0 = time.time() rng = random.Random(seed) p = make_params(rng) dens = [0.0] * (W * H) clumps = [] landed, npits = simulate(p["cap"], p["grains"], p, rng, dens, clumps) launched = p["grains"] if p["second"]: c2 = p["second"] n2 = int(p["grains"] * (c2["L"] * c2["W"]) / (p["cap"]["L"] * p["cap"]["W"])) l2, np2 = simulate(c2, n2, p, rng, dens, clumps) landed += l2; npits += np2; launched += n2 d = blur(dens) sample = sorted(v for v in d[::7] if v > 0.02) ref = sample[int(len(sample) * 0.995)] if sample else 1.0 # thin dust is blurred a little more than thick dust: the far haze stays smooth # (and the file small) while the honeycomb keeps its edges soft = blur(d) q = 1.0 / (0.15 * ref) ** 2 d = [b + (a - b) / (1.0 + b * b * q) for a, b in zip(soft, d)] for k in clumps: # clumps of spores: small crisp grains, added after the blur amt = ref * (0.05 + 0.8 * rng.random() ** 3) d[k] += amt for o in (-1, 1, -W, W): d[k + o] += amt * 0.28 steps = bee_trail(p, rng, d, ref) if p["bee"] else 0 pp = paper(rng) kexp = -2.2 / ref ** 0.85 exp = math.exp idx = bytearray(int(a + (255.0 - a) * (1.0 - exp(kexp * v ** 0.85)) + 0.5) if v > 0.0 else int(a + 0.5) for a, v in zip(pp, d)) hair_len = 0.0 if p["hair"]: cov, hair_len = hair_cover(rng) for k, cv in cov.items(): idx[k] = int(idx[k] + (52 - idx[k]) * cv + 0.5) pal = palette(p["warmth"]) size = write_png(path, W, H, idx, pal) tsize = write_png(thumb_path, THUMB, THUMB, thumbnail(idx), pal) if thumb_path else 0 secs = time.time() - t0 return {"seed": seed, "file": os.path.basename(path), "bytes": size, "thumb_bytes": tsize, "seconds": round(secs, 1), "launched": launched, "landed": landed, "pits": npits, "clumps": len(clumps), "bee_steps": steps, "hair_px": round(hair_len), "params": p} # ---------------------------------------------------------------- verification def read_png(path): """Read back an 8-bit indexed PNG: check signature and every CRC, inflate, undo row filters.""" data = open(path, "rb").read() assert data[:8] == b"\x89PNG\r\n\x1a\n", "bad signature" pos = 8 chunks = {} order = [] while pos < len(data): n, tag = struct.unpack(">I4s", data[pos:pos + 8]) body = data[pos + 8:pos + 8 + n] crc = struct.unpack(">I", data[pos + 8 + n:pos + 12 + n])[0] assert crc == zlib.crc32(tag + body) & 0xFFFFFFFF, "bad CRC in %s" % tag chunks[tag] = chunks.get(tag, b"") + body order.append(tag) pos += 12 + n assert order[0] == b"IHDR" and order[-1] == b"IEND" and pos == len(data) w, h, depth, ctype, comp, filt, inter = struct.unpack(">IIBBBBB", chunks[b"IHDR"]) assert (depth, ctype, comp, filt, inter) == (8, 3, 0, 0, 0) pal = chunks[b"PLTE"] assert len(pal) == 768 raw = zlib.decompress(chunks[b"IDAT"]) assert len(raw) == h * (w + 1), "wrong amount of pixel data" out = bytearray() prev = bytes(w) for y in range(h): ft = raw[y * (w + 1)] row = bytearray(raw[y * (w + 1) + 1:(y + 1) * (w + 1)]) if ft == 1: for i in range(1, w): row[i] = (row[i] + row[i - 1]) & 255 elif ft == 2: for i in range(w): row[i] = (row[i] + prev[i]) & 255 elif ft == 3: for i in range(w): row[i] = (row[i] + (((row[i - 1] if i else 0) + prev[i]) >> 1)) & 255 elif ft == 4: for i in range(w): a = row[i - 1] if i else 0 b = prev[i] c = prev[i - 1] if i else 0 pa, pb, pc = abs(b - c), abs(a - c), abs(a + b - 2 * c) row[i] = (row[i] + (a if pa <= pb and pa <= pc else b if pb <= pc else c)) & 255 else: assert ft == 0, "unknown filter" out += row prev = bytes(row) return w, h, bytes(out), pal def stats(path, ascii_art=False): w, h, idx, pal = read_png(path) luma = [(pal[3 * i] * 299 + pal[3 * i + 1] * 587 + pal[3 * i + 2] * 114) / 1000.0 for i in range(256)] hist = [0] * 256 for v in idx: hist[v] += 1 n = w * h res = { "file": os.path.basename(path), "width": w, "height": h, "bytes": os.path.getsize(path), "mean_luma": round(sum(hist[i] * luma[i] for i in range(256)) / n, 2), "max_luma": round(max(luma[i] for i in range(256) if hist[i]), 1), "frac_nonbackground": round(sum(hist[i] for i in range(256) if luma[i] >= 45) / n, 4), "frac_bright": round(sum(hist[i] for i in range(256) if luma[i] >= 225) / n, 5), "levels_used": sum(1 for v in hist if v), } if ascii_art: cols, rows = 60, 30 ramp = " .:-=+*#%@" lines = [] for r in range(rows): y0, y1 = r * h // rows, (r + 1) * h // rows line = "" for cc in range(cols): x0, x1 = cc * w // cols, (cc + 1) * w // cols tot = 0.0 for y in range(y0, y1): tot += sum(luma[v] for v in idx[y * w + x0:y * w + x1]) m = tot / ((y1 - y0) * (x1 - x0)) t = max(0.0, min(0.999, (m - 22.0) / 190.0)) ** 0.6 line += ramp[int(t * len(ramp))] lines.append(line) res["ascii"] = lines return res # ---------------------------------------------------------------- captions def caption(rec): p = rec["params"]; c = p["cap"] bits = ["seed %d" % rec["seed"], "cap %d by %d px, tip pointing %s" % (round(c["L"]), round(2 * c["W"]), direction_word(c["axis_deg"])), "pits about %d px apart" % round(c["pitch"]), "air drift %.2f toward %s" % (p["drift"], direction_word(p["drift_deg"])), "%.2f million spores launched, %.2f million landed on the paper" % (rec["launched"] / 1e6, rec["landed"] / 1e6), "shot range %d px" % round(p["shot"]), "honeycomb sharpness %.2f" % p["ghost"], "colour %.2f (0 cream, 1 honey)" % p["warmth"]] if p["second"]: bits.append("second cap %d by %d px" % (round(p["second"]["L"]), round(2 * p["second"]["W"]))) if p["bee"]: bits.append("bee footprints, %d steps" % rec["bee_steps"]) if p["hair"]: bits.append("one hair, %d px long" % rec["hair_px"]) return "; ".join(bits) + "." def main(argv): if "--params" in argv: i = argv.index("--params") for seed in range(int(argv[i + 1]), int(argv[i + 2]) + 1): p = make_params(random.Random(seed)); c = p["cap"] print("%3d L%3d W%3d ax%3d pit%2d drift %.2f@%3d N%.1fM shot%2d ghost%.2f settle%.2f warm%.2f %s%s%s" % ( seed, c["L"], 2 * c["W"], c["axis_deg"], c["pitch"], p["drift"], p["drift_deg"], p["grains"] / 1e6, p["shot"], p["ghost"], p["settle"], p["warmth"], "2CAP " if p["second"] else "", "BEE " if p["bee"] else "", "HAIR" if p["hair"] else "")) return if "--seed" in argv: seed = int(argv[argv.index("--seed") + 1]) out = argv[argv.index("--out") + 1] rec = render(seed, out) print(caption(rec)); print(rec["seconds"], "s,", rec["bytes"], "bytes") s = stats(out, True) print({k: v for k, v in s.items() if k != "ascii"}); print("\n".join(s["ascii"])) return names = [("spore-%02d.png" % (i + 1), "spore-%02d-thumb.png" % (i + 1)) for i in range(len(SEEDS))] if "--verify" in argv: for full, th in names: for nm in (full, th): s = stats(os.path.join(HERE, nm), nm == full) print({k: v for k, v in s.items() if k != "ascii"}) if "ascii" in s: print("\n".join(" |" + ln + "|" for ln in s["ascii"])) return if "--fragment" in argv: recs = json.load(open(os.path.join(HERE, "spore-prints.json"))) for rec in recs: print(rec["file"], "::", caption(rec)) return recs = [] for seed, (full, th) in zip(SEEDS, names): rec = render(seed, os.path.join(HERE, full), os.path.join(HERE, th)) rec["thumb"] = th recs.append(rec) print("%s seed %d: %.1f s, %d bytes (thumb %d)" % (full, seed, rec["seconds"], rec["bytes"], rec["thumb_bytes"]), flush=True) with open(os.path.join(HERE, "spore-prints.json"), "w") as f: json.dump(recs, f, indent=1) if __name__ == "__main__": main(sys.argv[1:])