Skip to content

Repository files navigation

calque

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.


Read this before you run it

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.

What that buys you

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%.

Original, the pixels that differ, and the rebuild

Left: the screenshot. Middle: every pixel the rebuild got wrong, in red. Right: the rebuild. calque check produces this for any page.

What it does

  • 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

What it doesn't do

  • It cannot identify a typeface. Nothing can, from a screenshot, and the ones that claim to are pattern-matching on popularity. type.family is 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 --assets it 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.

Quickstart

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.html

Or 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.html

The 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.

Commands

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 rebuild

check 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.

As a Claude skill

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/calque

Then 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.

How it works

  1. 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.
  2. 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.
  3. Group glyphs into lines geometrically.
  4. Read each line on its own with Tesseract.
  5. Measure colours, type sizes, stroke weights, radii, gaps and alignment.
  6. 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.

Measured, not assumed

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 a style attribute 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 parseArgs from the CLI ran the CLI, which called process.exit(0) mid-load. Eighteen tests did not execute and the runner reported the file green.

How well it actually works

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.

Tests

npm test

86 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.

About

Turn a screenshot of a website into working HTML and CSS. Measures the pixels — no vision model, no API key, no model call at all. Ships as a Claude skill.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages