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 +1 -1
- package/tools/sheet.py +106 -0
- package/wand.js +12 -2
- package/wand_kit.js +1 -1
package/package.json
CHANGED
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.
|
|
214
|
-
|
|
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.
|
|
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,
|