What it is
Two Script Editor applets that carried 75 photographed pages inside their own bundles and made Preview go full screen through System Events. Rebuilt as one dependency-free reader: all 67 distinct page geometries honoured exactly, a real page turn, a 75-page thumbnail drawer, shareable #v1p12 links, resume, and a 100% mode that draws each page at its original pixel size. 41.0 MB of source became 9.7 MB, with no page resampled — and the dead black/white photo framing was measured off 65 of the 75 pages without losing more than 0.5% of a page's ink.
Version history
Straight from the project’s own card data — what each version added.
75 pages, two volumes, rebuilt 2026-10-21 — graded at 390 / 768 / 1440
Files on the site
What broke, and what I’d change
Not written yet. This is the part only I can write — what actually went wrong, and what I’d do differently. It lives in builds/reflections/kinjitsu.md (see _TEMPLATE.md); as soon as that file has words in it, this section fills in on the next build.
Everything else on this page — the spec, the version history, the file counts — is generated from the project’s own build notes and files, so nothing here is guesswork.
Build spec
The project’s own build notes, rendered. Open the raw .md ↗
Show the spec
Build notes — 「禁日」 flipbook, rebuilt
Ask: "its a old flipbook that is very trash i made before with photos, can you make it better maybe with same size and actually put it into my website as a portfolio card and also one for my homepage"
Two deliverables came out of that: demos/kinjitsu/ (the reader) and demos/kinjitsu-teardown.html (the rebuild log), plus a card on the portfolio and a card on the homepage.
1. What the old thing actually was
Not a compiled app — two Script Editor applets:
/Applications/Installed Applications/「禁日」.app → 36 pages, JPEG, 378–465 × 640
/Applications/Installed Applications/「禁日」 后续.app → 39 pages, PNG, 413–1204 × 640–1734
Both carry the same 15,094-byte main.scpt (osadecompile diff is empty) and the same bundle id com.apple.ScriptEditor.id.test-flipbook, displayed as "test flipbook". The pages live inside each bundle at Contents/Resources/FlipbookPhotos/page 1.jpg … page 36.jpg (unpadded numbers).
The script, decompiled:
- If
FlipbookPhotos/is empty, copy user-picked images into the app bundle, then ask for a re-launch. defaults write com.apple.Preview PVImageOpeningMode 1— a machine-wide Preview preference.- Open every page at once → 36+ full images decode before first paint.
- Drive System Events:
⌘2,⌥⌘1, View ▸ Enter Full Screen, all insidetry … end trywith a
"just zoom the window" fallback. When Accessibility is denied you get error -25211 and the app still appears to open — it just never goes full screen.
Reading order: filename order, unpadded. Finder's numeric sort gets it right (verified 1,2,…,36); a plain lexicographic sort would have put page 10 before page 2.
2. What "with same size" was taken to mean
Both readings are honoured, because they turned out to be different problems:
- Same page dimensions — pages are never resampled.
build-kinjitsu.pyasserts every output file's - Same viewing size — the reader has a 100% mode that draws the page at exactly `naturalWidth ×
pixel width/height against its source and aborts if they differ; the only change is PNG → JPEG q84.
naturalHeight` (page 1 → 451×563 on every viewport, asserted in the tests). Default is fit-to-stage.
3. How it is built
~/scripts/build-kinjitsu.py (idempotent):
- reads the two bundles read-only, numeric-sorts pages, normalises names to
v1-01.jpg/v2-01.jpg - copies small JPEGs byte-for-byte; re-encodes the 38.5 MB PNG volume at native size, q84 → 8.65 MB
- writes 220 px thumbnails (75 files, 1.62 MB)
- writes
pages/manifest.jsonandpages/manifest.js(window.KJ=…) — the reader loads it as a classic - records provenance per page:
"src":"page 7.jpg"next to the web filename
<script> so the book also works off file://, where fetch() of local JSON is blocked
demos/kinjitsu/index.html — one file, no dependencies:
-104° about its left edge in 2400 px perspective; commit resets with transition:none + forced reflow so the new page doesn't swing back |#v1p12, #v2p20, kept in sync with replaceState; a hashchange handler covers pasting a link while already inside |localStorage["kinjitsu:progress"] = {v,p}; cover offers "Continue page N", hidden on a fresh visit |aria-current tracking |←/→/Space/PageUp/PageDown, Home/End, T, F, 0, Esc |prefers-reduced-motion → 130 ms fade |4. Verification (numbers, not intentions)
~/scripts/verify-kinjitsu.py (headless Chromium, 3 viewports) ends with FAILURES: none: no horizontal overflow at 390/768/1440, top bar fits (wraps to two rows on phones), page 1 decodes 451×563, →/← move 1→2→1, resting transform is literally none, 75 thumbnail buttons, end-of-volume roll-over to 1/39, 100% mode = 451×563, 0 console errors, 0 failed requests.
~/scripts/verify-kinjitsu-states.py: fresh visit hides Continue; read to page 3 + reload → Continue page 3 lands on 3/36; aborting pages/v1-05.jpg raises the error panel with that filename; reduced-motion mode still navigates.
~/scripts/verify-kinjitsu-page.py: the write-up page at 390/768/1440 → 0 overflow, 4 tables, thumbnails decode, CTA lands in the reader.
Two bugs were caught by this pass and fixed before deploy:
Continue page 1on a fresh visit —.cta{display:inline-flex}outranked thehiddenattribute.- Top bar scrolled sideways at 390 px — now wraps into two rows (
flex-wrap, tabs on row 2). - Pasting
#v1p12while already in the book did nothing — same-document fragment navigation never
Fixed with [hidden]{display:none!important}.
re-runs the script; added a hashchange handler.
5. Honest limits
- Source pages are small (page 1 is 451 px wide; drawn 543 px at 1440 = ~1.2× upscale and it shows).
- The pages are photographs: no selectable text, no search, not indexed. This Mac has only English
- No audio, bookmarks or per-page notes: none existed in the original.
- The two volumes are chained as one continuous book — my reading of the material, not a documented fact.
tessdata, so no CJK OCR was possible — the book's text was never machine-read.
6. Rebuild
python3 ~/scripts/build-kinjitsu.py # stage pages → demos/kinjitsu/pages/
python3 ~/scripts/verify-kinjitsu.py # FAILURES: none
python3 ~/scripts/verify-kinjitsu-states.py
bash ~/scripts/deploy-isaac-site.sh # → https://isaac1804.com/portfolio/demos/kinjitsu/
Original .app bundles were never modified — the build script only reads them.
Framing pass (white/black edges)
The photos came framed: black caption bar across the top (13% of the height), black surround at the sides, and a bright white strip along the sheet edge. ~/scripts/trim-kinjitsu-pages.py takes that off:
- walk each side inward while the line is still dark (mean ≤ 120); accept the band only if the band itself is
- veto the crop if > 1.5% of the page's ink would be lost, and skip dark pages entirely (title cards / night
- 65 of 75 pages trimmed, median 13.9% of the frame, max ink loss 0.52%.
black (mean ≤ 45); then remove the pure-white rim;
scenes — their black is the artwork);
Order matters: build-kinjitsu.py (stages untouched originals) → trim-kinjitsu-pages.py --apply → reindex-kinjitsu.py (rewrites manifest + thumbs at the new sizes). Re-running only the first step restores the original framing.