wand-decks-kit 0.8.2 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wand-decks-kit",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "Rebuilds or authors a client-ready .pptx on the Wand design system: measured fit, embedded Geist, verbatim content.",
5
5
  "license": "SEE LICENSE IN PROMPT.md",
6
6
  "bin": {
package/tools/sheet.py ADDED
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ sheet.py — stitch rendered slides into one contact sheet.
4
+
5
+ python3 tools/sheet.py <dir-of-pngs> <out.png> [cols]
6
+
7
+ Why: the visual gate on a 24-slide deck used to mean viewing 24 images — the
8
+ single largest token cost of a rebuild, bigger than the instructions and the
9
+ spec combined. One sheet gives the whole-deck pass for the price of one image;
10
+ the full-size renders stay on disk for zooming into anything that looks off.
11
+
12
+ Pure standard library, like everything else in this kit.
13
+ """
14
+
15
+ import glob, os, struct, sys, zlib
16
+
17
+
18
+ def read_png(path):
19
+ d = open(path, "rb").read()
20
+ pos, idat = 8, b""
21
+ w = h = ct = None
22
+ while pos < len(d):
23
+ ln = struct.unpack(">I", d[pos:pos + 4])[0]
24
+ typ = d[pos + 4:pos + 8]
25
+ if typ == b"IHDR":
26
+ w, h, bd, ct = struct.unpack(">IIBB", d[pos + 8:pos + 18])
27
+ elif typ == b"IDAT":
28
+ idat += d[pos + 8:pos + 8 + ln]
29
+ pos += 12 + ln
30
+ ch = {0: 1, 2: 3, 6: 4}[ct]
31
+ raw = zlib.decompress(idat)
32
+ stride = w * ch
33
+ out = bytearray(h * stride)
34
+ prev = bytearray(stride)
35
+ i = 0
36
+ for y in range(h):
37
+ f = raw[i]; i += 1
38
+ line = bytearray(raw[i:i + stride]); i += stride
39
+ for x in range(stride):
40
+ a = line[x - ch] if x >= ch else 0
41
+ b = prev[x]
42
+ c = prev[x - ch] if x >= ch else 0
43
+ v = line[x]
44
+ if f == 1: v += a
45
+ elif f == 2: v += b
46
+ elif f == 3: v += (a + b) // 2
47
+ elif f == 4:
48
+ p = a + b - c
49
+ pa, pb, pc = abs(p - a), abs(p - b), abs(p - c)
50
+ v += a if (pa <= pb and pa <= pc) else (b if pb <= pc else c)
51
+ line[x] = v & 255
52
+ out[y * stride:(y + 1) * stride] = line
53
+ prev = line
54
+ # normalise to RGB
55
+ if ch == 3:
56
+ return w, h, bytes(out)
57
+ rgb = bytearray(w * h * 3)
58
+ for px in range(w * h):
59
+ if ch == 1:
60
+ rgb[px * 3:px * 3 + 3] = bytes([out[px]] * 3)
61
+ else:
62
+ rgb[px * 3:px * 3 + 3] = out[px * 4:px * 4 + 3]
63
+ return w, h, bytes(rgb)
64
+
65
+
66
+ def write_png(path, w, h, rgb):
67
+ raw = bytearray()
68
+ for y in range(h):
69
+ raw.append(0)
70
+ raw += rgb[y * w * 3:(y + 1) * w * 3]
71
+ def chunk(t, d):
72
+ return struct.pack(">I", len(d)) + t + d + struct.pack(">I", zlib.crc32(t + d) & 0xFFFFFFFF)
73
+ open(path, "wb").write(
74
+ b"\x89PNG\r\n\x1a\n"
75
+ + chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 2, 0, 0, 0))
76
+ + chunk(b"IDAT", zlib.compress(bytes(raw), 9))
77
+ + chunk(b"IEND", b""))
78
+
79
+
80
+ def main():
81
+ src, out = sys.argv[1], sys.argv[2]
82
+ cols = int(sys.argv[3]) if len(sys.argv) > 3 else 4
83
+ files = sorted(glob.glob(os.path.join(src, "*.png")))
84
+ files = [f for f in files if os.path.abspath(f) != os.path.abspath(out)]
85
+ if not files:
86
+ raise SystemExit(f"no PNGs in {src}")
87
+ tiles = [read_png(f) for f in files]
88
+ tw = max(t[0] for t in tiles)
89
+ th = max(t[1] for t in tiles)
90
+ gap = 6
91
+ rows = (len(tiles) + cols - 1) // cols
92
+ W = cols * tw + (cols + 1) * gap
93
+ H = rows * th + (rows + 1) * gap
94
+ sheet = bytearray(b"\x1a\x16\x38" * (W * H)) # dark ground between tiles
95
+ for i, (w, h, rgb) in enumerate(tiles):
96
+ ox = gap + (i % cols) * (tw + gap)
97
+ oy = gap + (i // cols) * (th + gap)
98
+ for y in range(h):
99
+ dst = ((oy + y) * W + ox) * 3
100
+ sheet[dst:dst + w * 3] = rgb[y * w * 3:(y + 1) * w * 3]
101
+ write_png(out, W, H, bytes(sheet))
102
+ print(f"{out} ({len(tiles)} slides, {W}x{H})")
103
+
104
+
105
+ if __name__ == "__main__":
106
+ main()
package/wand.js CHANGED
@@ -210,8 +210,18 @@ function ship(specPath, outArg) {
210
210
  const dir = path.join(work, "slides");
211
211
  fs.mkdirSync(dir, { recursive: true });
212
212
  spawnSync("pdftoppm", ["-r", "96", "-png", pdfOut, path.join(dir, "slide")], { stdio: "inherit" });
213
- shots = fs.readdirSync(dir).filter((f) => f.endsWith(".png")).length;
214
- console.log(`rendered : ${shots} slide(s) to ${rel(dir)}/ - look at every one`);
213
+ shots = fs.readdirSync(dir).filter((f) => f.startsWith("slide")).length;
214
+ // One contact sheet is the whole-deck pass for the price of one image.
215
+ // Viewing 24 renders one by one was the biggest token line of a rebuild —
216
+ // more than the instructions and the spec combined.
217
+ const small = fs.mkdtempSync(path.join(require("os").tmpdir(), "wand-sheet-"));
218
+ spawnSync("pdftoppm", ["-r", "26", "-png", pdfOut, path.join(small, "s")], { stdio: "ignore" });
219
+ const sheet = path.join(dir, "sheet.png");
220
+ runPython([path.join(KIT, "tools", "sheet.py"), small, sheet]);
221
+ fs.rmSync(small, { recursive: true, force: true });
222
+ console.log(`rendered : ${shots} slide(s) to ${rel(dir)}/`);
223
+ console.log(`visual pass : open ${rel(sheet)} — one image, every slide. Open an`);
224
+ console.log(` individual slide-NN.png only where something looks off.`);
215
225
  } else {
216
226
  console.log(`open ${rel(path.join(KIT, "preview", "preview.html"))} and look at every slide.`);
217
227
  }
package/wand_kit.js CHANGED
@@ -999,7 +999,7 @@ function productMap(pres, { tag, titleText, subtitleText, bandRow, cards, split,
999
999
 
1000
1000
 
1001
1001
  module.exports = {
1002
- VERSION: "0.8.2",
1002
+ VERSION: "0.8.3",
1003
1003
  T, F, COLS, ASSETS, radius, theme, autoSize, measure: M,
1004
1004
  background, chrome, footer, title, subtitle, kicker, contentTop,
1005
1005
  card, iconTile, pill,