Skip to content

feat: measurement tools (distance, perimeter, area) - #654

Open
MathiasWP wants to merge 6 commits into
embedpdf:mainfrom
MathiasWP:measure-tool-support
Open

feat: measurement tools (distance, perimeter, area)#654
MathiasWP wants to merge 6 commits into
embedpdf:mainfrom
MathiasWP:measure-tool-support

Conversation

@MathiasWP

@MathiasWP MathiasWP commented Jun 2, 2026

Copy link
Copy Markdown

Adds calibrated measurement tools — distance (line), perimeter (polyline), and area (polygon / rectangle / ellipse) — with live, zoom-stable labels, scale calibration (including draw-to-calibrate), units (mm/cm/m/in/ft/yd/pt), decimal/fractional precision, and an optional secondary unit.

Implementation: measurements are ordinary Line/Polyline/Polygon/Square/Circle annotations carrying typed measurement metadata; the unit/geometry math is a pure module in @embedpdf/models. Calibration is persisted via the /EPDFCustom data channel plus the spec /IT intent (a fully spec-compliant native /Measure dictionary for cross-viewer interop is deferred — it needs a pdfium WASM rebuild). Labels render across React/Preact/Vue/Svelte, and the snippet viewer gets a Measure mode (toolbar, draw-to-calibrate dialog, property panel) plus hold-Shift angle constraint.

Resolves #223

Add calibrated measurement tools (distance, perimeter, polygon/rectangle/
ellipse area) with live, zoom-stable labels, scale calibration (incl.
draw-to-calibrate), units (mm/cm/m/in/ft/yd/pt), decimal/fractional precision,
and an optional secondary unit.

Implementation: measurements are ordinary Line/Polyline/Polygon/Square/Circle
annotations carrying typed `measurement` metadata; pure math lives in
@embedpdf/models; persisted via the /EPDFCustom channel + spec /IT intent
(native /Measure dictionary deferred — needs a pdfium WASM rebuild). Rendered
across React/Preact/Vue/Svelte, with snippet-viewer UI (Measure mode, toolbar,
calibrate dialog, property panel) and hold-Shift angle constraint.
@vercel

vercel Bot commented Jun 2, 2026

Copy link
Copy Markdown

@MathiasWP is attempting to deploy a commit to the OpenBook Team on Vercel.

A member of the Team first needs to authorize it.

Fill rectangle/ellipse/polygon area measurements with a subtle 45° diagonal
hatch (mode === 'area' only) so the enclosed region reads as the measured area.
Sized in px/scale so density stays constant across zoom; shared AreaHatch
helper with Vue/Svelte mirrors.
@MathiasWP

Copy link
Copy Markdown
Author

@bobsingor thoughts on adding this? This is the missing piece for us to start using EmbedPDF

@MathiasWP

Copy link
Copy Markdown
Author

Also, i am not sure if this is something that should be deeper integrated into PDFIUM engine or if it's correct to solve it at this layer. LKM and i'll make any needed changes

@MathiasWP

Copy link
Copy Markdown
Author

Hi @bobsingor, i do not want to disturb you if you're busy with other things, but our license for the current PDF editor we're using is expiring in the next month, and this is the only feature we're missing to be able to use EmbedPDF.

Do you plan on looking at this implementation? And is EmbedPDF going to keep being maintained? I've seen that there's been a while since the last update, but i'm assuming you're probably on summer vacation 😄

Would love to know what your plan is! It's okay if it cannot be added within the next month or two - then we'll just buy another year with the license we have.

Also one thing i want to add is that i would love with helping reporting bugs from real usage if we manage to get EmbedPDF into our app. We have a lot of PDF usage with weird PDF's, so i would definitely help with reporting any bugs we get from Sentry to help make this library as robust as can be!

main reorganized the repo into the v3 package layout and rewrote the
annotation plugin, so every file the measure work touched was deleted
upstream (42 modify/delete conflicts). Resolved by taking main's side
throughout and dropping the v2 measure implementation; it is re-applied
against the v3 architecture in the follow-up commit.
Re-applies the measure tool against v3, where the shape of the codebase
made most of the v2 implementation unnecessary rather than portable.

What a measurement IS changed. In v2 it was five bespoke tools with their
own handlers, patch entries and per-framework label/hatch components
(React, Preact, Vue, Svelte — four copies of the same drawing). In v3 it
is a `measurement` calibration on an ordinary geometry annotation:

  - `measurement` joins the flat props vocabulary, so it is set, edited
    and cleared through the same setDefaults/updateSelection paths as
    colour or opacity. Setting it promotes a shape into a dimension;
    `null` demotes it. No new gesture, handler or subtype.
  - The five tools are `extends` presets of line/polyline/polygon/
    square/circle carrying that default, plus `clickCreate: false` —
    a fixed-size shape from a bare click measures nothing the user chose.
  - The read-out is painted by the shared `scene()`, so every framework
    renderer gets it for free. The four per-framework components are gone.

The read-out is DERIVED from the geometry on every render rather than
cached on the annotation, so a vertex drag or resize can never leave a
stale number behind. Zoom-stable sizing rides the ViewEnv `pageItems`
already threads through.

Layout:
  - engine-core `annotation/measurement.ts` — units, scale, rounding,
    formatting, and a zod-free `parseMeasurementInfo` guard (the engine
    read path ships to the browser, so it must not pull in zod).
  - engine-services — persists `/EMBD_Metadata/Measurement` plus the spec
    `/IT` intent, derived from subtype+mode so the two cannot drift.
  - core-annotation `measure.ts` — the geometry half: which formula a
    shape uses, where the label sits, and a scanline-clipped hatch (the
    SceneNode vocabulary has no patterns or clip paths and needs none).
  - viewer-chrome — a Measure mode and a Measurement section in the style
    panel that calibrates the armed tool or the selected annotation.

A committed annotation renders as the engine's baked /AP raster, which
never runs `scene()` — and the engine cannot bake a read-out. So
`measureScene()` is exposed separately and the painter draws it over the
raster; without this the label vanished the moment you finished drawing.

Tests: 34 in core-annotation (geometry, hatch clipping, scene layering,
overlay parity), 21 in engine-core (units, formatting, intent mapping),
14 in plugin-annotation (tool registry, repository round-trip).

The PDFium round-trip is NOT verified here: the engine test suite needs
@embedpdf/engine-runtime-wasm32, whose compiled lib/ is absent in this
workspace — 32 engine test files already fail on main for that reason.
`caret.geom` widened to `Geom` through the `Annot` annotation, so reading
`.rect` off it for the upright variant did not compile. Bind the geom
separately and narrowly instead.

Pre-existing on main, fixed here because it fails plugin-annotation's
typecheck, which blocks every downstream package's typecheck in the turbo
graph — including the ones this branch touches.
ISO 32000 defines measurement intents for Line, PolyLine and Polygon only.
Stamping `PolygonDimension` on a Square or Circle invents spec vocabulary,
and a foreign viewer that trusted it would go looking for `/Vertices` that
are not there. Those two are an EmbedPDF extension and are recognised the
same way every measurement is — by the calibration itself.

A demote no longer clears `/IT` on those shapes either: we never wrote it,
so it may belong to whoever authored the annotation.

This restores the deliberate spec-honesty call from the original v2 work,
which the port had flattened into "every area is a PolygonDimension".
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support for measurement tools

1 participant