Skip to content

Draw ink with the curves Acrobat fits to its points - #105

Merged
bobsingor merged 1 commit into
embedpdf/mainfrom
embedpdf/feature/ink-curves
Nov 3, 2026
Merged

bobsingor merged 1 commit into
embedpdf/mainfrom
embedpdf/feature/ink-curves

Conversation

@bobsingor

Copy link
Copy Markdown
Contributor

An ink annotation's appearance now draws each stroke as Acrobat does when it writes one, instead of straight segments between the points. A mark no longer changes look when Acrobat, or we, draw it again.

The rule is Schneider's curve fitting ("An Algorithm for Automatically Fitting Digitized Curves", Graphics Gems, 1990), without his Newton step:

  • Repeated points in a row count once; each stroke is fitted on its own.
  • A stroke leaves and arrives along its first and last segment. Where two pieces meet, the curve runs along the line from the point before to the point after (no direction where the stroke turns straight back, so the handles there have no length).
  • A piece of two points is one cubic with handles a third of its length.
  • A longer piece gets the least-squares cubic over its points, placed by chord length. It is kept when every point is within the tolerance of the curve and the curve is within the tolerance of the lines between the points; otherwise it splits at its worst point and both halves are fitted again.
  • The tolerance is sqrt(2) pt, at any width. Acrobat's lies above 1.2747 and at most 1.5062: the dense zigzag below is the case that tells, and a unit test pins both ends.
  • A stroke of one point is Acrobat's dot: a circle of radius 0.5, filled and stroked (B).

The stream is set as Acrobat sets it: RG, w only when the width isn't 1, 1 J and no join, one operator a line, numbers to four decimals. Ink with no stroke colour still paints nothing, width 0 is still a hairline, and /Rect is still measured from what's painted, now the curves.

One deliberate difference: Acrobat fills the dot without setting a fill colour, so its default black shows inside a stroke thinner than 1 pt. Ours fills it in the stroke's colour.

Checked against Acrobat

Acrobat drew annotations that had no appearance stream and saved them. Every number we write matches Acrobat's to within 0.001 pt, segment for segment (EPDFInkAppearanceEmbedderTest), on macOS and on Linux:

  • The eight cases of ink-curves.pdf: uneven segments, two points, one point (the dot, byte for byte), a repeated point, a stroke straight back on itself, two strokes, a tight turn between long runs, and a dense zigzag like a real pen (37 cubics).
  • ink-widths.pdf: the dense and the uneven stroke at /W 0.5, 1, 4 and 8, with the same curves at every width and w left out at 1.
  • The zigzag of the no-stroke grid (cell 1E), starting 526 654 m 527.6667 663.3333 521.519 682 531 682 c.

The fit lives in core/fpdfdoc/cpdf_ink_curves.{h,cpp}, so GenerateInkAP only writes it. It works from a stack, not recursion, so a long stroke that splits at every point can't exhaust the stack.

Tests

  • cpdf_ink_curves_unittest.cpp: the tolerance band (37 cubics from just above 1.2747 to 1.5062, 39 below, 33 above), two points, one point, a repeated point, and a stroke straight back on itself.
  • epdf_ink_appearance_embeddertest.cpp: the Acrobat cases above.
  • Updated: the ink /Rect test (the curve rises to 90.1728 past the middle point), the hairline test, annotation_ink_multiple.dump.txt and ink_with_color_scheme_agg.png (written on Linux in Docker, and it matches on macOS too).
  • The two ephemeral ink unit tests now give their ink a stroke colour. Without one they had failed since Draw no stroke for an empty or missing /C, as Acrobat does #104, because ink with no colour paints nothing.
  • The four new tests are recorded as layer-mode writers; the existing ink writers keep their object counts.

On macOS, compared with this branch's base (#104's head): the unit suite now passes in full (1,320), and the embedder suite (12 failures) and pixel tests (51 failures, 2 surprises) fail the same tests as before. Fork main fails the same pixel tests and the same embedder tests, plus EPDFSlicedLoadEmbedderTest.SlicesKeepToTheirBudget, which fails there and not here. Linux CI hasn't run yet.

The viewer side (the live drawing, its box and its hit test, from one TypeScript port of the same fit, tested against the same Acrobat numbers) is on embedpdf/embed-pdf-viewer feature/ink-curves, which needs this released and pinned.

An ink annotation's appearance now draws each stroke as Acrobat does when it writes one: cubic Béziers fitted to the points as Schneider fits digitized curves ("An Algorithm for Automatically Fitting Digitized Curves", Graphics Gems, 1990), without his Newton step. Repeated points count once. A stroke leaves and arrives along its first and last segment, and two pieces meet along the line from the point before to the point after (no direction where the stroke turns straight back). A piece of two points gets handles a third of its length. A longer piece gets the least-squares cubic over its points, kept when every point is within the tolerance of it and it is within the tolerance of the lines between the points; otherwise it splits at its worst point. The tolerance is sqrt(2) pt at any width. A stroke of one point is Acrobat's dot: a circle of radius 0.5, filled and stroked.

The stream is set as Acrobat sets it: the colour, `w` only when the width isn't 1, `1 J` and no join, one operator a line, numbers to four decimals. No stroke colour still paints nothing, width 0 is still a hairline, and /Rect follows the curves. One difference: the dot is filled in the stroke's colour, where Acrobat leaves the fill at its black default (hidden under a stroke of width 1 or more).

Checked against the appearances Acrobat wrote on save: the eight cases of ink-curves.pdf, the dense and the uneven stroke at widths 0.5, 1, 4 and 8, and the zigzag of the no-stroke grid (cell 1E), every number within 0.001 pt, on macOS and Linux (EPDFInkAppearanceEmbedderTest). The fit is cpdf_ink_curves.{h,cpp}; its unit test pins the tolerance with the dense zigzag, which Acrobat draws with 37 cubics: 39 below 1.2747, 33 above 1.5062.

Tests: annotation_ink_multiple's dump and ink_with_color_scheme_agg.png redrawn (the PNG written on Linux, and matching on macOS); the ink /Rect and hairline tests updated; the two ephemeral ink unit tests give their ink a stroke colour, without which it paints nothing since the no-stroke change. New layer-mode writers recorded. On macOS the full unit suite passes, and the embedder and pixel suites fail the same tests as before the change.
@bobsingor
bobsingor merged commit 8074892 into embedpdf/main Nov 3, 2026
1 check passed
@bobsingor
bobsingor deleted the embedpdf/feature/ink-curves branch November 3, 2026 20:53
roanutil pushed a commit to roanutil/embed-pdf-viewer that referenced this pull request Nov 4, 2026
The viewer draws an ink stroke as the runtime now writes it (embedpdf/runtime, cpdf_ink_curves.h), which is how Acrobat draws it: cubic Béziers fitted to the points as Schneider fits digitized curves, without his Newton step, never more than sqrt(2) pt from any point, and a stroke of one point as a dot of radius 0.5. The fit is ink-curves.ts, one implementation for the scene, the box and the hit test:

- Scene: each stroke is a `path` of `C` segments with round caps and no join (the curves meet smoothly); a dot is an `ellipse` filled in the stroke's colour.
- Box: the points and what the curves paint, as the runtime measures /Rect: the curves' ends and turns, half the width across each curve's ends, round caps, past each turn, and a miter where two curves meet in a corner.
- Hit test and marquee: the curves flattened to within 0.05 pt; a dot is a filled oval.

Tested against the same Acrobat numbers as the runtime: the eight cases of ink-curves.pdf to 0.001 pt, the tolerance pinned by the dense zigzag (37 cubics; 39 below 1.2747, 33 above 1.5062), and the box of the runtime's /Rect test (top at 93.172844). The fit gives the same curves in page space as in the file's.

The cloudy border's Bézier helpers moved to bezier.ts, which ink shares. Docs: the ink section of engine/annotations/shapes, synced to both sites. API snapshot: `scene`'s body.

Runtime: embedpdf/runtime#105, in the pinned f2dd40897.
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.

1 participant