CRISPR
Lossless SVG to PNG, rasterized by a real browser engine. Detects and repairs font substitution.
git clone https://github.com/corderro-artz/crispr.git仕様Specification
構成Composition
開発Project
目次Contents
文書Document
Lossless SVG to PNG, rasterized by a real browser engine.
crispr converts SVG files to PNG at any resolution by handing them to headless Chromium and capturing the result. Most converters reimplement the SVG specification and diverge from it in the details; crispr does not implement it at all, so text shaping, font hinting, filters, gradients, clip paths and blend modes come out exactly as a browser draws them. The part a screenshot cannot solve is fonts: an SVG naming a font the machine lacks still renders, silently substituted, and the output looks correct while being the wrong design. crispr detects that per text node over the DevTools Protocol and fetches the font the file actually asked for.
Access
Delivery
Runtime
Overview
crispr takes one file, one directory, or several of both, converts every .svg it finds, and writes PNGs to an output folder. Everything else is ignored. The rendering is Chromium's; the parts that are crispr's own are the sizing arithmetic, the font verification, and the batching.
Capabilities
| Capability | Details |
|---|---|
| Input | One file, a directory, or several of both; only .svg is converted |
| Output | PNG at exact integer dimensions, transparent by default |
| Sizing | The SVG's own size, or --scale, --dpi, --width, --height |
| Font verification | Per text node, over the DevTools Protocol |
| Font repair | Missing families fetched from Google Fonts and cached per machine |
| Batching | One browser, one context, a pool of reused pages |
| Isolation | One malformed file never aborts a batch |
| Exit codes | 0 success, 1 a file failed or a font was substituted under --strict-fonts, 2 usage |
| Packaging | A Windows executable with no prerequisites |
Requirements
| Requirement | Version | Notes |
|---|---|---|
| Node.js | 22+ | Only for the source and the Node-dependent build |
| Chromium | Any recent build | Found automatically; see Installation |
| Network | First run only | To fetch a missing font, once per family per machine |
The portable build carries its own Chromium and needs neither Node nor a network connection.
Dependencies
| Category | Packages |
|---|---|
| Runtime | playwright-core |
| Build | typescript, esbuild, postject |
| Tests | node:test (built in) |
Installation
Prebuilt Windows builds are on the releases page.
| Shape | Size | Node needed | For |
|---|---|---|---|
| Portable | 147 MB | No | Unzip anywhere and run. Chromium included, works offline. Start here if you are unsure. |
| Single file, thin | 36 MB | No | The same crispr.exe without Chromium. Downloads it once on first run and caches it. |
| Node-dependent | 3 MB | Yes | A folder and a crispr.cmd shim, using a Node and a Chromium you already have. |
Every shape carries node_modules/playwright-core as real files. It cannot be folded into the executable: it lazily requires chromium-bidi submodules by path and resolves its own driver relative to its location on disk, so a bundled copy builds cleanly and then cannot launch anything.
crispr keeps its font cache in %LOCALAPPDATA%\crispr, so a family is downloaded once per machine rather than once per run. Delete the folder and nothing is left behind.
To build from source:
git clone https://github.com/corderro-artz/crispr.git
cd crispr
npm install
npx playwright install chromium --only-shell
npm run buildQuick Start
crispr logo.svgThat writes logo.png at the SVG's own dimensions into crispr-out beside the executable. The workflows worth knowing:
crispr assets/ -r -o dist/png --scale 2 # a whole tree at 2x, mirrored
crispr icon.svg --width 512 # pin a width, height follows the aspect ratio
crispr icon.svg --background "#07080b" # opaque instead of transparent
crispr brand/ --strict-fonts # fail the build on a wrong typeface
crispr brand/ --list-fonts # report fonts, write nothingConcepts
Why a browser
Three decisions define the renderer, each chosen over a more obvious alternative.
The file is loaded by navigating to it. file:///…/logo.svg makes Chromium render the SVG as a standalone document, so <image href="./texture.png">, an @font-face pointing at a local file, and XML entity declarations all resolve against the file's real location. Reading the file and injecting it into a wrapper page silently breaks every one of those.
Scaling rewrites the SVG rather than setting deviceScaleFactor. crispr guarantees the element has a viewBox, then sets width and height to the exact target pixels and lets Chromium re-rasterize the vectors. Dimensions come out as exact integers, where a fractional device scale such as 800/331 rounds unpredictably. It also keeps the device scale factor at 1 for every file, so an entire run shares one browser context instead of one per file.
networkidle is not used. It costs 500 ms of enforced silence per file whether or not the file references anything external. crispr waits for load, then document.fonts.ready, then for every <image> to decode — the same guarantee without the fixed penalty.
Sizing
The default is the SVG's own size: one SVG pixel becomes one PNG pixel.
| Flags | Output |
|---|---|
| (none) | The intrinsic size |
--scale 2 |
Twice the intrinsic size |
--dpi 192 |
The same, expressed as a resolution |
--width 800 |
800 wide, height from the aspect ratio |
--height 400 |
400 tall, width from the aspect ratio |
--width 800 --height 100 |
Exactly 800×100; the SVG fits per its own preserveAspectRatio |
Intrinsic size is resolved from the width and height attributes, then the viewBox, then the content's bounding box. That order carries more weight than it appears to: a standalone SVG with only a viewBox defaults to 100%×100%, so measuring its client rectangle returns the viewport rather than the artwork.
Rounding happens once, at the end. Combining --scale with --width is a usage error rather than one silently winning. Anything over 16384 px per side is refused, because past Chromium's texture limit it returns a truncated image instead of failing.
Fonts
document.fonts.check() cannot verify a font — it returns true for families that do not exist, including invented ones. crispr uses the DevTools Protocol's CSS.getPlatformFontsForNode, which reports the font Chromium actually rasterized with, per text node.
What counts as correct is the first family in the stack, not any of them. Given font-family="Noto Sans JP, Yu Gothic, Meiryo, sans-serif", landing on Yu Gothic is CSS behaving exactly as specified and still not the design. Since crispr can fetch Noto Sans JP, correct-per-CSS is too low a bar. Per-glyph fallback stays legitimate: a node whose preferred family lacks CJK coverage reports both that family and the CJK one, and seeing the preferred family anywhere is enough.
A substituted family is looked up in google/fonts over raw.githubusercontent.com, trying the ofl, apache and ufl licence directories in turn, registered as a FontFace from a base64 data URI, and the file is re-captured. Only the preferred family is fetched — pulling the rest of the stack would download the very fallbacks the author listed to avoid downloads.
crispr brand/ --font "google:Noto Sans JP" # fetch a family up front
crispr brand/ --font "Noto Sans=./NotoSans.ttf" # supply one from disk
crispr brand/ --font-fallback "Noto Sans JP=Yu Gothic"
crispr brand/ --no-font-fetch --strict-fonts # offline, and fail on anything missing--font <path> reads the family name out of the font's own name table. WOFF and WOFF2 are Brotli compressed, so their name is unreadable without a decompressor that is not worth the binary size; those need the explicit "Family=path" form, and crispr says so rather than guessing.
Note: A populated cache plus
--no-font-fetchis the reproducibility boundary — a build in that state cannot silently change. Pipelines that care should restore the cache and pass the flag.
Note: Fonts are downloaded, not redistributed. Google Fonts families carry their own licences (OFL, Apache, UFL); rendering with them is unrestricted, but check before shipping a font file.
Architecture
Source: docs/diagrams/architecture.mmd
| Module | Responsibility |
|---|---|
src/size.ts |
Sizing arithmetic. No browser dependency |
src/discover.ts |
Paths to jobs; recursion and tree mirroring |
src/fonts/name-table.ts |
Family name from an sfnt name table |
src/fonts/google.ts |
Family resolution and download |
src/fonts/cache.ts |
On-disk font cache |
src/fonts/registry.ts |
Fonts from flags, cache and network |
src/browser.ts |
Browser candidates, launch, page pool |
src/page.ts |
Functions that run inside the page |
src/render.ts |
One SVG to one PNG, including font repair |
src/report.ts |
Summaries and exit codes |
src/cli.ts |
Argument parsing and help |
src/playwright.ts |
Locating playwright-core on disk at runtime |
Design Principles
- Keep the heavy machinery out of the pure logic. Sizing, discovery and font parsing carry no browser dependency, so most of the behaviour is testable without launching anything.
- Detect rather than assume. Whether a font resolved and whether a browser will launch are both answered by trying, not by inspecting.
- Report what actually happened. A substituted font is reported by default, because a PNG in the wrong typeface looks correct and nothing else will ever mention it.
- Fail one file, not the batch.
Command Line
crispr --help| Flag | What it does |
|---|---|
-o, --out <dir> |
Output directory. Defaults to crispr-out beside the executable |
-r, --recursive |
Recurse into subdirectories, mirroring the tree into the output |
-s, --scale <n> |
Multiply the intrinsic size |
-w, --width <px> |
Pin the width; height follows the aspect ratio |
-H, --height <px> |
Pin the height; width follows the aspect ratio |
--dpi <n> |
Equivalent to --scale n/96 |
-b, --background <css> |
Solid background colour. Default is transparent |
--font <spec> |
A font file, "Family=path", or "google:Family". Repeatable |
--font-dir <dir> |
Register every font in a directory |
--font-fallback <spec> |
"Requested=Available" family remap. Repeatable |
--no-font-fetch |
Never download a missing font |
--strict-fonts |
Treat a font substitution as an error |
--list-fonts |
Report requested versus actual fonts, write no PNGs |
-j, --jobs <n> |
Concurrent pages. Default min(cpus, 8) |
--browser-path <p> |
An explicit Chromium executable |
--quiet / --verbose |
Errors only / per-file detail |
-H carries --height because -h is help. Usage errors surface before any browser starts.
Files that are not .svg are skipped without comment, since pointing the tool at a mixed asset folder is the normal case rather than a mistake. A path that does not exist is a mistake, and says so.
Browser Resolution
crispr looks for a browser in this order, and the first that actually launches wins:
| Order | Source | Pinned |
|---|---|---|
| 1 | --browser-path |
Yes |
| 2 | CRISPR_BROWSER |
Yes |
| 3 | A browsers/ folder beside the executable — the portable build |
Yes |
| 4 | %LOCALAPPDATA%\crispr\browsers — the thin build's cache |
Yes |
| 5 | Playwright's own install | Yes |
| 6 | System Microsoft Edge, then Google Chrome | No |
Only the last is unpinned, and crispr says so when it uses one: branded browsers update themselves and their headless mode differs from the headless shell, so output can drift between machines.
Development
npm run typecheck
npm test
npm run build
npm run packageTesting
122 tests, split by what they need.
npm test # unit, no browser, no network
npm run test:integration # integration, launches Chromium
npm run test:allThe unit suite covers sizing, discovery, name-table parsing, Google Fonts resolution against a stubbed fetcher, the registry, argument parsing and reporting. The integration suite renders real fixtures and spawns the CLI as a child process.
Several of the sharper defects were reachable only from integration, which is why it exists:
| Defect | Only visible when |
|---|---|
| A client rect returns the viewport for a sizeless SVG | A file has no width, height or viewBox |
| CDP reports a font's own name, not its registered alias | A font is supplied under a different family |
document.fonts is cleared by navigation |
A second file reuses a page |
| A font lookup raced itself | More than one page renders at once |
Probing executablePath() misses an --only-shell install |
Chromium was installed without full Chrome |
Workflow Notes
npm run typecheckrunstsc --noEmitunderstrictwithnoUncheckedIndexedAccess. It is expected to be silent.npm run buildbundles todist/crispr.jswith esbuild.npm run packageadditionally produces a CommonJS bundle, injects it into a copy of the Node binary withpostjectto makecrispr.exe, and assembles the three release shapes intobuild/release.playwright-corestays external in both bundles. See Installation.
Deployment
Releases are cut from a tag and carry all three shapes.
npm run test:all
npm run package
git tag -a v0.1.0 -m "crispr 0.1.0"
git push origin v0.1.0
gh release create v0.1.0 build/release/*.zip- Targets: GitHub Releases
- Artefacts: portable, single file thin, and node-dependent zips, plus
MANIFEST.md - Platform: Windows x64. The core is platform-neutral; only the builds are Windows.
Troubleshooting
Note: "no usable browser found" lists every candidate that was tried and why each failed. Install one with
npx playwright install chromium --only-shell, or pass--browser-path.
Note: A font that is not on Google Fonts cannot be fetched. Supply it with
--font "Family=path", map it to something installed with--font-fallback, or accept the substitution — it is reported either way.
Note: Output differing between two machines usually means one of them fell through to a system browser.
--verboseprints which candidate won, and crispr warns when the winner is unpinned.
Note: A PNG larger than 16384 px on a side is refused rather than written. Chromium returns a truncated image past that limit, so the check has to happen before the capture.
Links
| Resource | URL |
|---|---|
| Repository | github.com/corderro-artz/crispr |
| Releases | github.com/corderro-artz/crispr/releases |
| Tags | Git tags |
| Design specification | docs/superpowers/specs |
| Implementation plan | docs/superpowers/plans |
| Issues | github.com/corderro-artz/crispr/issues |
| Pull requests | github.com/corderro-artz/crispr/pulls |
| Actions | github.com/corderro-artz/crispr/actions |
| Security | GitHub security overview |
| License | LICENSE |
| Vaporsoft | vaporsoft.dev |
Contributing
- Create a branch from
mainfor the change. - Write tests first — pure logic in
tests/unit, anything touching a browser intests/integration. - Run
npm run typecheckandnpm run test:allbefore opening a pull request. - Run
npm run packagewhen the change affects packaging or browser resolution. - Open a pull request with enough context to review behaviour, architecture and output fidelity.
License
MIT. See LICENSE.
Copyright © 2026 Corderro Artz / Vaporsoft.
版Published
v0.1.0
16 Sep 2026First release.
crispr hands an SVG to headless Chromium and screenshots the result, so what lands in the PNG is what a browser draws — the same text shaping, hinting, filters, gradients, clip paths and blend modes — rather than a reimplementation of the spec.
Downloads
| Shape | Size | Node needed | For |
|---|---|---|---|
| Portable | 147 MB | No | Unzip anywhere and run. Chromium included, works offline. Start here if you are unsure. |
| Single file, thin | 36 MB | No | The same crispr.exe without Chromium. Downloads it once on first run and caches it. |
| Node-dependent | 3 MB | Yes | A folder plus a crispr.cmd shim, using a Node and a Chromium you already have. |
Every shape carries node_modules/playwright-core as real files — it lazily requires chromium-bidi by path and resolves its driver relative to its location on disk, so a bundled copy builds cleanly and then cannot launch anything.
What it does
Exact dimensions. Sizing rewrites the SVG's viewBox and width/height rather than setting deviceScaleFactor, so output is exact integers where a fractional device scale such as 800/331 rounds unpredictably. It also keeps the device scale factor at 1, so an entire run shares one browser context.
Font substitution is detected and repaired. An SVG naming a font the machine lacks still renders — Chromium substitutes silently and the PNG looks fine while being the wrong design. document.fonts.check() cannot catch this; it returns true for families that do not exist. crispr uses the DevTools Protocol's CSS.getPlatformFontsForNode, which reports the font actually rasterized with, then fetches the preferred family from Google Fonts and re-captures.
Correctness is judged against the first family in the stack, not any of it. Given Noto Sans JP, Yu Gothic, Meiryo, sans-serif, landing on Yu Gothic is CSS behaving exactly as specified and still not the design.
Batch-safe. One browser, one context, a pool of reused pages. One malformed file never aborts a batch. --strict-fonts turns a substitution into a failed build.
Quick start
```
crispr logo.svg
crispr assets/ -r -o dist/png --scale 2
crispr brand/ --strict-fonts
```
Notes
- Windows x64. The core is platform-neutral; only these builds are Windows.
- 122 tests.
--verboseprints which browser won the resolution chain, and crispr warns when it falls through to a system browser, whose version is outside its control.
履歴Recent commits
e0354b5docs: render the diagrams on the brand palette, and give them a generator7901854docs: render the architecture as SVG, and widen the layoutf127c54docs: add docs/diagrams/architecture.mmd7d3bc6ddocs: add docs/diagrams/architecture-dark.svg49dcedfdocs: add docs/diagrams/architecture-light.svge880aa6docs: render the architecture diagram81f9e09chore: drop the samples copy of the brand PNGs27b8ae7docs: align README with the house format