Repository navigation
Draw ink with the curves Acrobat fits to its points - #105
Merged
Merged
Conversation
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
B).The stream is set as Acrobat sets it:
RG,wonly when the width isn't 1,1 Jand 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/Rectis 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: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/W0.5, 1, 4 and 8, with the same curves at every width andwleft out at 1.526 654 m 527.6667 663.3333 521.519 682 531 682 c.The fit lives in
core/fpdfdoc/cpdf_ink_curves.{h,cpp}, soGenerateInkAPonly 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./Recttest (the curve rises to 90.1728 past the middle point), the hairline test,annotation_ink_multiple.dump.txtandink_with_color_scheme_agg.png(written on Linux in Docker, and it matches on macOS too).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.