Turn a screenshot of a website into working HTML and CSS. It measures the image. It does not ask a model to look at it and guess.
Part of Openware — one repo per tool, each a real, self-hostable replacement for something overpriced.
A calque is a word borrowed by translating it piece by piece rather than taking it whole — and it is also what French calls tracing paper. Both meanings are the job.
There is no model to bring, because there is no model call. Not a local one, not an API key, not a vision model. Every colour, size, weight, position, radius and gap in the output was measured off the pixels by code you can read.
That is not a limitation being made into a virtue. Screenshot-to-code normally
means posting the image to a frontier vision model, which is why the popular
tools need a key and cost money per page and are unreliable at exactly the thing
you would check first — a vision model will confidently tell you a #0f172a
background is #111827, that 15px type is 16px, and that a 12px radius is 8px.
Each miss is invisible on its own. Together they are the reason a rebuilt page
looks nearly right and feels wrong.
None of that is a language problem. It is a measurement problem, and measurement is what computers were good at before any of this.
Against a page whose real values are known — the fixture in tests/fixtures,
whose stylesheet is checked in next to it:
| measured | actual | |
|---|---|---|
| Accent colour | #2563eb |
#2563eb |
| Page background | #fefefe |
#ffffff |
| Body size | 16px | 16px |
| Headline | 48px / 700 | 48px / 700 |
| Nav links | 15px / 400 | 15px / 400 |
| Button radius | 8px | 8px |
| Card fill | #f4f4f5 |
#f4f4f5 |
| Card padding | 28px | 28px |
| Content column | 1045px | 1040px |
Mean font-size error across every text run on that page: 3.4%.
Left: the screenshot. Middle: every pixel the rebuild got wrong, in red.
Right: the rebuild. calque check produces this for any page.
- Reads a PNG or JPEG of a page and writes one self-contained HTML file
- Or writes a spec — a JSON file of everything it measured, for you or a model to build from
- Extracts a full set of design tokens: colour, type scale, weights, spacing rhythm, container width, gutters, radii
- Finds sections, columns, cards, buttons, alignment and reading order
- Cuts the photographs out of the screenshot and references them, so a photographic page rebuilds as itself rather than as a grey box
- Reconstructs vertical gradients rather than flattening them to a mean
- Ships as a Claude skill — one command, and Claude has the numbers instead of its impression of them
- Scores its own work: renders the result in a browser you already have and compares it to the original, pixel by pixel
- Runs in about a second a page
- It cannot identify a typeface. Nothing can, from a screenshot, and the
ones that claim to are pattern-matching on popularity.
type.familyis a stack whose metrics match what was measured. The character advance is measured, which is enough to tell a monospaced face from a proportional one and nothing more. - It cannot recover a photograph. With
--assetsit crops the pictures out of the screenshot: right size, right position, screenshot resolution, and any text that was sitting on top is baked in. Swap in the real files. - Font weight resolves to about one step. True 600 and true 700 measure the same stroke ratio. It emits 300/400/500/700 and refuses to invent 600, because a threshold placed between overlapping measurements is a coin toss with a number written on it.
- Dark pages with a large glow or gradient behind the type are its worst case — the measured score below says how much worse.
- Icons become placeholders. A glyph reads as a picture. Right size, right place, no icon.
- It reads one screenshot, not a site. No scrolling, no hover, no second breakpoint. What is in the image is what exists.
- It has no opinion about your framework. The output is plain HTML and CSS custom properties. As a Claude skill that is the point — Claude reads the measurements and writes the code in whatever the project already uses.
git clone https://github.com/openwarehq/calque && cd calque
npm install && npm run build
node dist/src/cli.js build screenshot.png -o page.html --assets ./assets
open page.htmlOr in a container, which needs nothing installed and works offline after the build:
docker build -t calque .
docker run --rm -v "$PWD:/work" calque build /work/screenshot.png -o /work/page.htmlThe first run downloads about 15 MB of OCR data and caches it. The Docker build does that ahead of time. Nothing else opens a socket.
calque build shot.png -o page.html --assets ./assets # image -> page
calque extract shot.png -o spec.json # image -> measurements
calque render spec.json -o page.html # measurements -> page
calque check shot.png page.html --diff diff.png # score the rebuildcheck renders your page in whatever Chrome-family browser is already on the
machine and compares it to the original in Lab. --diff writes
original | differences | rebuild side by side, which tells you what is wrong
rather than only how much.
This is the shape it was built for. Claude can describe a screenshot well and cannot read a hex value off one; calque can do nothing but read hex values off one.
cp -r skill ~/.claude/skills/calqueThen Claude runs extract, reads the measurements, and writes the component in
your project's own idiom against your project's own tokens — instead of
eyeballing #2563eb as "blue-600" and 15px as "text-sm".
The spec keeps the two kinds of claim apart on purpose. Every box, colour and
size is measured and can be trusted. Every kind, role and align is
inferred and can be overruled — and a model looking at the same image is
better at those than the heuristics are.
- Trim the window frame. A macOS window screenshot carries a border on all four sides. Left in, no row of the page is ever empty and no section boundary is ever found.
- Find the ink by subtracting a blurred copy of the image from itself, per channel. Light-on-dark and dark-on-light both come out; a gradient comes out as nothing, because it is its own neighbourhood.
- Group glyphs into lines geometrically.
- Read each line on its own with Tesseract.
- Measure colours, type sizes, stroke weights, radii, gaps and alignment.
- Assemble into sections and columns, and render.
Step 4 is the one that matters. Tesseract reads a cropped line of that fixture's
headline perfectly at every page-segmentation mode, and mangles the same
headline in the full-page image at every one of them — B u |® |. Its layout
model expects a scanned document: one type size, one column, dark on light. A
web page is none of those. So the layout analysis happens here, on geometry, and
the OCR engine only ever sees one line at a time.
Everything below was found by building the thing and looking at the output.
- Tesseract's page layout is the problem, not its recognition. Cropped, it read the headline correctly at every mode tried. Whole-page, it failed at every mode tried.
- OCR confidence doubles as a text-versus-picture classifier, for free. On a real page every text run came back at 76–96 and every icon at 0–42, with nothing in between.
- Luminance-only ink detection loses coloured text. Mint display type over a
green glow measured almost identical luma; the headline came apart into
Unlo⬚⬚able, the two letters over the brightest part of the glow simply gone. Comparing per channel recovers them. - A blur radius that finds an element does not bound it. Background subtraction haloes, so every box is a couple of pixels large on every side — which read 14px type as 20px. Correcting for one pixel per side took mean font-size error from 13.2% to 3.4%. It was one pixel at radius 2, 3 and 4 alike, so it is not a function of radius, and pretending otherwise would be fitting a curve to three identical points.
- Frame detection eats page margins if you let it. A centred layout on white has 200px of pure white down each side, which satisfies every "is this a uniform border" test there is. Cropping it destroyed exactly the measurement the gutter and container come from: a 1040px column reported as 862px with no gutter at all.
- A section boundary must never cut through an element. One comparison, and it is the strongest constraint available — a glow behind a hero shifts the measured background enough to look like a step change, and the cut lands in the middle of the headline.
- Font weight is not measurable to nine steps. Stroke width over font size separates regular (0.096–0.117) from bold (0.149–0.183) cleanly and does not separate 600 from 700 at all.
- A
url()inside astyleattribute must use single quotes. Double ones close the attribute early. The markup stays valid, the browser reports nothing, and every background image on the page silently fails to load. - Pinning each section to its measured height makes the result worse — 60.4% to 58.8% — because a rebuild whose type wraps at a different measure needs a different height. It was tried and reverted; the comment in the code says so.
- A test file can pass without running. Importing
parseArgsfrom the CLI ran the CLI, which calledprocess.exit(0)mid-load. Eighteen tests did not execute and the runner reported the file green.
node tools/batch.mjs builds and scores every fixture. The score is the share
of pixels within ΔE 8 of the original after rendering the rebuild in a browser.
| screenshot | what it is | match | in the repo |
|---|---|---|---|
| groundtruth | flat background, known values | 97.1% | yes |
| undeck | dark UI, flat panels | 97.1% | yes |
| prmpt | mixed, some imagery | 69.6% | no |
| saltline | full-bleed photograph | 47.7% | no |
| kaldvik | photographic, sparse type | 35.4% | no |
| unlovable | dark, heavy radial glow | 15.3% | yes |
Three of those are screenshots from a separate client project and are not redistributed here, so the table is more than you can reproduce from a clean clone. They are listed anyway because leaving out the three worst results would make the tool look better than it is.
Mean 60.4%, and the spread is the honest headline, not the mean. Pages built out of type and panels come back very close. Pages that are mostly a photograph come back as the photograph with the type on it — better than a blank page, nothing like the original. The dark glow page is the worst thing you can hand it, and the number says so.
If your screenshot looks like the top two rows, this will save you an afternoon. If it looks like the bottom row, read the output before you trust it.
npm test86 tests, no network. The end-to-end ones assert against the fixture's own checked-in stylesheet, so they fail when the pipeline drifts rather than when a snapshot goes stale.
MIT. Take it, host it, fork it, sell it.
