From a305cb7331857b165548912189fac57d1edefc03 Mon Sep 17 00:00:00 2001 From: Bob Singor Date: Tue, 3 Nov 2026 22:04:46 +0200 Subject: [PATCH] Draw ink with the curves Acrobat fits to its points MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- core/fpdfdoc/BUILD.gn | 3 + core/fpdfdoc/cpdf_annot_unittest.cpp | 3 + .../cpdf_appearancegenerator_shapes.cpp | 99 ++++- .../cpdf_appearancegenerator_unittest.cpp | 2 + core/fpdfdoc/cpdf_ink_curves.cpp | 235 ++++++++++++ core/fpdfdoc/cpdf_ink_curves.h | 54 +++ core/fpdfdoc/cpdf_ink_curves_unittest.cpp | 93 +++++ fpdfsdk/BUILD.gn | 2 + fpdfsdk/epdf_annot_embeddertest.cpp | 15 +- fpdfsdk/epdf_ink_appearance_embeddertest.cpp | 348 ++++++++++++++++++ fpdfsdk/fpdf_annot_embeddertest.cpp | 5 +- testing/embedpdf_layer_mode_writers.txt | 4 + .../annotation_ink_multiple.dump.txt | 12 +- .../ink_with_color_scheme_agg.png | Bin 13580 -> 9632 bytes 14 files changed, 849 insertions(+), 26 deletions(-) create mode 100644 core/fpdfdoc/cpdf_ink_curves.cpp create mode 100644 core/fpdfdoc/cpdf_ink_curves.h create mode 100644 core/fpdfdoc/cpdf_ink_curves_unittest.cpp create mode 100644 fpdfsdk/epdf_ink_appearance_embeddertest.cpp diff --git a/core/fpdfdoc/BUILD.gn b/core/fpdfdoc/BUILD.gn index 4f4bd25da6..3d57cf9096 100644 --- a/core/fpdfdoc/BUILD.gn +++ b/core/fpdfdoc/BUILD.gn @@ -89,6 +89,8 @@ source_set("fpdfdoc") { "cpdf_icon.h", "cpdf_iconfit.cpp", "cpdf_iconfit.h", + "cpdf_ink_curves.cpp", + "cpdf_ink_curves.h", "cpdf_interactiveform.cpp", "cpdf_interactiveform.h", "cpdf_link.cpp", @@ -162,6 +164,7 @@ pdfium_unittest_source_set("unittests") { "cpdf_filespec_unittest.cpp", "cpdf_formfield_unittest.cpp", "cpdf_appearancegenerator_unittest.cpp", + "cpdf_ink_curves_unittest.cpp", "cpdf_interactiveform_unittest.cpp", "cpdf_metadata_unittest.cpp", "cpdf_measure_unittest.cpp", diff --git a/core/fpdfdoc/cpdf_annot_unittest.cpp b/core/fpdfdoc/cpdf_annot_unittest.cpp index f6e3785fc1..6a823edbd2 100644 --- a/core/fpdfdoc/cpdf_annot_unittest.cpp +++ b/core/fpdfdoc/cpdf_annot_unittest.cpp @@ -180,6 +180,9 @@ TEST_F(CPDFAnnotWithPageModuleTest, auto ink_list = pdfium::MakeRetain(); ink_list->Append(CreateQuadPointArrayFromVector({1, 1, 9, 9})); annot_dict->SetFor("InkList", std::move(ink_list)); + // Black: ink with no /C has no stroke and paints nothing. + annot_dict->SetFor(pdfium::annotation::kC, + CreateQuadPointArrayFromVector({0, 0, 0})); CPDF_Annot annot(annot_dict, &doc); EXPECT_EQ(CFX_FloatRect(0, 0, 10, 10), diff --git a/core/fpdfdoc/cpdf_appearancegenerator_shapes.cpp b/core/fpdfdoc/cpdf_appearancegenerator_shapes.cpp index 71d100eaad..e5069c78ff 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator_shapes.cpp +++ b/core/fpdfdoc/cpdf_appearancegenerator_shapes.cpp @@ -31,6 +31,7 @@ #include "core/fpdfdoc/cpdf_appearancegenerator_internal.h" #include "core/fpdfdoc/cpdf_cloudy_border.h" #include "core/fpdfdoc/cpdf_defaultappearance.h" +#include "core/fpdfdoc/cpdf_ink_curves.h" #include "core/fpdfdoc/cpdf_richtext.h" #include "core/fpdfdoc/cpdf_richtextlayout.h" #include "core/fxcrt/fx_string_wrappers.h" @@ -431,6 +432,51 @@ bool GenerateDimensionLineAP(APGenerationTarget* target, points); } +// An ink curve's number as Acrobat writes it: to four decimals, without +// trailing zeros (0.5, 527.6667). +void WriteInkNumber(fxcrt::ostringstream& stream, double value) { + if (!std::isfinite(value)) { + stream << "0"; + return; + } + ByteString text = ByteString::Format("%.4f", value); + text.TrimBack('0'); + text.TrimBack('.'); + stream << (text == "-0" ? ByteString("0") : text); +} + +void WriteInkPoint(fxcrt::ostringstream& stream, double x, double y) { + WriteInkNumber(stream, x); + stream << " "; + WriteInkNumber(stream, y); +} + +// One point of ink is Acrobat's dot: a circle of radius 0.5 about it, four +// quarter arcs from its top, clockwise, closed for the fill. +void WriteInkDot(fxcrt::ostringstream& stream, const CFX_PointF& center) { + constexpr double kRadius = 0.5; + // How far each arc's handles reach along its tangents, as Acrobat writes. + constexpr double kHandle = 0.2778; + const double x = center.x; + const double y = center.y; + WriteInkPoint(stream, x, y + kRadius); + stream << " m\n"; + const double arcs[4][6] = { + {x + kHandle, y + kRadius, x + kRadius, y + kHandle, x + kRadius, y}, + {x + kRadius, y - kHandle, x + kHandle, y - kRadius, x, y - kRadius}, + {x - kHandle, y - kRadius, x - kRadius, y - kHandle, x - kRadius, y}, + {x - kRadius, y + kHandle, x - kHandle, y + kRadius, x, y + kRadius}}; + for (const auto& arc : arcs) { + WriteInkPoint(stream, arc[0], arc[1]); + stream << " "; + WriteInkPoint(stream, arc[2], arc[3]); + stream << " "; + WriteInkPoint(stream, arc[4], arc[5]); + stream << " c\n"; + } + stream << "h\n"; +} + } // namespace namespace appearance_generator { @@ -800,20 +846,25 @@ bool GenerateInkAP(APGenerationTarget* target, const std::optional stroke_color = GetStrokeColorString(annot_dict); + // The stroke as Acrobat sets it: the colour, the width unless it is the + // default 1, and round caps. No join: the curves meet smoothly. fxcrt::ostringstream app_stream; - app_stream << "/" << kGSDictName << " gs "; + app_stream << "/" << kGSDictName << " gs\n"; if (stroke_color) { app_stream << *stroke_color; - - app_stream << std::max(GetBorderWidth(annot_dict), 0.0f) << " w "; - - app_stream << "1 J "; // Rounded Line Caps - app_stream << "1 j "; // Rounded Line Joins (for the corners in the middle) - + const float width = std::max(GetBorderWidth(annot_dict), 0.0f); + if (width != 1.0f) { + WriteInkNumber(app_stream, width); + app_stream << " w\n"; + } + app_stream << "1 J\n"; app_stream << GetDashPatternString(annot_dict); } + // Each stroke is drawn as Acrobat draws it (cpdf_ink_curves.h): fitted + // cubics, or a dot for a stroke of one point. std::vector points; + bool dot_fill_set = false; for (size_t i = 0; i < ink_list->size(); i++) { RetainPtr coordinates_array = ink_list->GetArrayAt(i); // An ink stroke needs at least one point to start. @@ -822,19 +873,39 @@ bool GenerateInkAP(APGenerationTarget* target, } const size_t first = points.size(); - points.push_back( - {coordinates_array->GetFloatAt(0), coordinates_array->GetFloatAt(1)}); - for (size_t j = 2; j < coordinates_array->size(); j += 2) { + for (size_t j = 0; j + 1 < coordinates_array->size(); j += 2) { points.push_back({coordinates_array->GetFloatAt(j), coordinates_array->GetFloatAt(j + 1)}); } if (!stroke_color) { continue; } - // The 'm' moves to the stroke's first point; every later one is an 'l'. - app_stream << points[first].x << " " << points[first].y << " m "; - for (size_t j = first + 1; j < points.size(); ++j) { - app_stream << points[j].x << " " << points[j].y << " l "; + const pdfium::span stroke = + pdfium::span(points).subspan(first); + const std::vector curves = FitInkStroke(stroke); + if (curves.empty()) { + // Acrobat fills the dot without setting a fill colour (black under a + // stroke of width 1 or more covers it); ours is the stroke's colour, so + // a thinner dot has no black middle. + if (!dot_fill_set) { + app_stream << GetColorStringWithDefault( + annot_dict->GetArrayFor(pdfium::annotation::kC).Get(), {}, + PaintOperation::kFill); + dot_fill_set = true; + } + WriteInkDot(app_stream, stroke.front()); + app_stream << "B\n"; + continue; + } + WriteInkPoint(app_stream, stroke.front().x, stroke.front().y); + app_stream << " m\n"; + for (const InkCubic& curve : curves) { + WriteInkPoint(app_stream, curve.control1.x, curve.control1.y); + app_stream << " "; + WriteInkPoint(app_stream, curve.control2.x, curve.control2.y); + app_stream << " "; + WriteInkPoint(app_stream, curve.end.x, curve.end.y); + app_stream << " c\n"; } app_stream << "S\n"; } diff --git a/core/fpdfdoc/cpdf_appearancegenerator_unittest.cpp b/core/fpdfdoc/cpdf_appearancegenerator_unittest.cpp index acadae8587..953e7558f4 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator_unittest.cpp +++ b/core/fpdfdoc/cpdf_appearancegenerator_unittest.cpp @@ -311,6 +311,8 @@ TEST_F(CPDFGenerateAPTest, GenerateEphemeralInkAPDoesNotInflateAnnotRect) { auto ink_list = pdfium::MakeRetain(); ink_list->Append(MakeNumberArray({1, 1, 9, 9})); annot_dict->SetFor("InkList", std::move(ink_list)); + // Black: ink with no /C has no stroke and paints nothing. + annot_dict->SetFor(pdfium::annotation::kC, MakeNumberArray({0, 0, 0})); const CFX_FloatRect original_rect = annot_dict->GetRectFor(pdfium::annotation::kRect); diff --git a/core/fpdfdoc/cpdf_ink_curves.cpp b/core/fpdfdoc/cpdf_ink_curves.cpp new file mode 100644 index 0000000000..0c0a459ec9 --- /dev/null +++ b/core/fpdfdoc/cpdf_ink_curves.cpp @@ -0,0 +1,235 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#include "core/fpdfdoc/cpdf_ink_curves.h" + +#include +#include + +namespace { + +using Point = InkCurvePoint; + +Point operator+(const Point& a, const Point& b) { + return {a.x + b.x, a.y + b.y}; +} + +Point operator-(const Point& a, const Point& b) { + return {a.x - b.x, a.y - b.y}; +} + +Point operator*(const Point& a, double s) { + return {a.x * s, a.y * s}; +} + +double Dot(const Point& a, const Point& b) { + return a.x * b.x + a.y * b.y; +} + +double Distance(const Point& a, const Point& b) { + return std::hypot(a.x - b.x, a.y - b.y); +} + +// `v` at length 1; no direction (0, 0) when it has no length. +Point Unit(const Point& v) { + const double length = std::hypot(v.x, v.y); + if (length == 0) { + return {0, 0}; + } + return {v.x / length, v.y / length}; +} + +// The Bernstein weights of a cubic at `u`. +double B0(double u) { + return (1 - u) * (1 - u) * (1 - u); +} +double B1(double u) { + return 3 * u * (1 - u) * (1 - u); +} +double B2(double u) { + return 3 * u * u * (1 - u); +} +double B3(double u) { + return u * u * u; +} + +struct Cubic { + Point p0; + Point p1; + Point p2; + Point p3; +}; + +Point CubicAt(const Cubic& c, double u) { + return (c.p0 * B0(u) + c.p1 * B1(u)) + (c.p2 * B2(u) + c.p3 * B3(u)); +} + +// A piece of the stroke to fit: its first and last point, and the directions +// the curve leaves the first along and arrives at the last against (pointing +// back into the curve), both unit or none. +struct Piece { + size_t first; + size_t last; + Point leaving; + Point arriving; +}; + +// Where each point of the piece sits along it, 0 to 1, by the length of the +// lines between them. +std::vector ChordLengthPlaces(const std::vector& points, + const Piece& piece) { + std::vector places = {0}; + for (size_t i = piece.first + 1; i <= piece.last; ++i) { + places.push_back(places.back() + Distance(points[i], points[i - 1])); + } + const double length = places.back(); + for (double& place : places) { + place /= length; + } + return places; +} + +// The cubic from the piece's first point to its last, along its two +// directions, whose handle lengths put it closest to the points at their +// places (least squares). Handles that come out shorter than a millionth of +// the piece's span, or backwards, are a third of the span instead. +Cubic LeastSquaresCubic(const std::vector& points, + const Piece& piece, + const std::vector& places) { + const Point& start = points[piece.first]; + const Point& end = points[piece.last]; + double c00 = 0; + double c01 = 0; + double c11 = 0; + double x0 = 0; + double x1 = 0; + for (size_t i = 0; i < places.size(); ++i) { + const double u = places[i]; + const Point a0 = piece.leaving * B1(u); + const Point a1 = piece.arriving * B2(u); + c00 += Dot(a0, a0); + c01 += Dot(a0, a1); + c11 += Dot(a1, a1); + const Point rest = + points[piece.first + i] - + ((start * B0(u) + start * B1(u)) + (end * B2(u) + end * B3(u))); + x0 += Dot(a0, rest); + x1 += Dot(a1, rest); + } + const double det = c00 * c11 - c01 * c01; + double leaving_length = det == 0 ? 0 : (x0 * c11 - x1 * c01) / det; + double arriving_length = det == 0 ? 0 : (c00 * x1 - c01 * x0) / det; + const double span = Distance(end, start); + const double epsilon = 1e-6 * span; + if (leaving_length < epsilon || arriving_length < epsilon) { + leaving_length = span / 3; + arriving_length = span / 3; + } + return {start, start + piece.leaving * leaving_length, + end + piece.arriving * arriving_length, end}; +} + +// The piece's point farthest from the curve at its place: the square of that +// distance, and which point (the last of equals). +struct WorstPoint { + double squared_distance; + size_t index; +}; + +WorstPoint FindWorstPoint(const std::vector& points, + const Piece& piece, + const std::vector& places, + const Cubic& cubic) { + WorstPoint worst = {0, (piece.last - piece.first + 1) / 2 + piece.first}; + for (size_t i = piece.first + 1; i < piece.last; ++i) { + const Point off = CubicAt(cubic, places[i - piece.first]) - points[i]; + const double squared = Dot(off, off); + if (squared >= worst.squared_distance) { + worst = {squared, i}; + } + } + return worst; +} + +double DistanceToSegment(const Point& p, const Point& a, const Point& b) { + const Point ab = b - a; + const double length_squared = Dot(ab, ab); + double t = length_squared == 0 ? 0 : Dot(p - a, ab) / length_squared; + t = t < 0 ? 0 : (t > 1 ? 1 : t); + return Distance(p, a + ab * t); +} + +// How far the curve strays from the lines between the piece's points: the +// farthest of 65 places along it, each from its nearest line. +double DistanceFromLines(const std::vector& points, + const Piece& piece, + const Cubic& cubic) { + constexpr int kSteps = 64; + double farthest = 0; + for (int k = 0; k <= kSteps; ++k) { + const Point at = CubicAt(cubic, static_cast(k) / kSteps); + double nearest = INFINITY; + for (size_t i = piece.first; i < piece.last; ++i) { + nearest = + std::fmin(nearest, DistanceToSegment(at, points[i], points[i + 1])); + } + farthest = std::fmax(farthest, nearest); + } + return farthest; +} + +InkCubic ToInkCubic(const Cubic& cubic) { + return {cubic.p1, cubic.p2, cubic.p3}; +} + +} // namespace + +std::vector FitInkStroke(pdfium::span stroke, + double tolerance) { + std::vector points; + for (const CFX_PointF& point : stroke) { + if (!points.empty() && points.back().x == point.x && + points.back().y == point.y) { + continue; + } + points.push_back({point.x, point.y}); + } + std::vector curves; + if (points.size() < 2) { + return curves; + } + + // The pieces still to fit, the next on top: a split piece's first half is + // fitted (and split) before its second, so the cubics come out in order. + // A stack, not recursion: a long stroke can split once per point. + std::vector pending = { + {0, points.size() - 1, Unit(points[1] - points[0]), + Unit(points[points.size() - 2] - points.back())}}; + while (!pending.empty()) { + const Piece piece = pending.back(); + pending.pop_back(); + const Point& start = points[piece.first]; + const Point& end = points[piece.last]; + if (piece.last - piece.first == 1) { + const double handle = Distance(end, start) / 3; + curves.push_back(ToInkCubic({start, start + piece.leaving * handle, + end + piece.arriving * handle, end})); + continue; + } + const std::vector places = ChordLengthPlaces(points, piece); + const Cubic cubic = LeastSquaresCubic(points, piece, places); + const WorstPoint worst = FindWorstPoint(points, piece, places, cubic); + if (worst.squared_distance < tolerance * tolerance && + DistanceFromLines(points, piece, cubic) < tolerance) { + curves.push_back(ToInkCubic(cubic)); + continue; + } + // The halves meet at the worst point, along the line from the point + // before it to the point after. + const size_t split = worst.index; + const Point back = Unit(points[split - 1] - points[split + 1]); + pending.push_back({split, piece.last, back * -1, piece.arriving}); + pending.push_back({piece.first, split, piece.leaving, back}); + } + return curves; +} diff --git a/core/fpdfdoc/cpdf_ink_curves.h b/core/fpdfdoc/cpdf_ink_curves.h new file mode 100644 index 0000000000..83c1a60e79 --- /dev/null +++ b/core/fpdfdoc/cpdf_ink_curves.h @@ -0,0 +1,54 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#ifndef CORE_FPDFDOC_CPDF_INK_CURVES_H_ +#define CORE_FPDFDOC_CPDF_INK_CURVES_H_ + +#include + +#include "core/fxcrt/fx_coordinates.h" +#include "core/fxcrt/span.h" + +// How Acrobat draws an ink stroke (/InkList) when it writes the appearance: +// 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. Checked against the appearances Acrobat +// wrote, to 0.001 pt, segment for segment: +// +// - Repeated points in a row count once. +// - A piece of the stroke is one cubic. At the stroke's ends it leaves and +// arrives along the first and last segment; where two pieces meet, along +// the line from the point before to the point after (no direction when +// those are the same point: 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, placed by +// chord length. It stays one piece when every point is within the tolerance +// of the curve, at its place, 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. + +// How far the curves may stray, in points. Acrobat's lies above 1.2747 and at +// most 1.5062 (the dense zigzag in the unit test tells), at any width. +inline constexpr double kInkCurveTolerance = 1.4142135623730951; // sqrt(2) + +// A point of a fitted curve. Doubles, so the fit decides on distances as +// Acrobat's does, not on floats rounded on the way. +struct InkCurvePoint { + double x = 0; + double y = 0; +}; + +// One cubic of a stroke, from the end of the one before (or the stroke's +// first point). +struct InkCubic { + InkCurvePoint control1; + InkCurvePoint control2; + InkCurvePoint end; +}; + +// The cubics that draw `stroke`, from its first point. None when it has fewer +// than two different points: Acrobat draws one point as a dot. +std::vector FitInkStroke(pdfium::span stroke, + double tolerance = kInkCurveTolerance); + +#endif // CORE_FPDFDOC_CPDF_INK_CURVES_H_ diff --git a/core/fpdfdoc/cpdf_ink_curves_unittest.cpp b/core/fpdfdoc/cpdf_ink_curves_unittest.cpp new file mode 100644 index 0000000000..1b3a9d5c38 --- /dev/null +++ b/core/fpdfdoc/cpdf_ink_curves_unittest.cpp @@ -0,0 +1,93 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +#include "core/fpdfdoc/cpdf_ink_curves.h" + +#include + +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +// Acrobat's case H: dense like a real pen, a zigzag of 40 points 3 pt apart. +std::vector DenseZigzag() { + std::vector points; + for (int i = 0; i < 40; ++i) { + points.push_back({260.0f + i * 3, 300.0f + (i % 2) * 2 + i * 0.5f}); + } + return points; +} + +void ExpectPoint(const InkCurvePoint& point, double x, double y) { + EXPECT_NEAR(x, point.x, 1e-4); + EXPECT_NEAR(y, point.y, 1e-4); +} + +} // namespace + +// Acrobat draws the dense zigzag with 37 cubics. That pins its tolerance +// between 1.2747 (below it the curve of the first three points strays too far +// from their lines: 39 cubics) and 1.5062 (above it a piece whose worst point +// is that far stays whole: 33). +TEST(CPDFInkCurvesTest, TheDenseZigzagPinsTheTolerance) { + const std::vector points = DenseZigzag(); + EXPECT_EQ(37u, FitInkStroke(points).size()); + EXPECT_EQ(37u, FitInkStroke(points, 1.28).size()); + EXPECT_EQ(37u, FitInkStroke(points, 1.5).size()); + EXPECT_EQ(39u, FitInkStroke(points, 1.27).size()); + EXPECT_EQ(33u, FitInkStroke(points, 1.51).size()); +} + +TEST(CPDFInkCurvesTest, TwoPointsGetHandlesAThirdOfTheirLength) { + static constexpr CFX_PointF kStroke[] = {{60, 520}, {200, 580}}; + const std::vector curves = FitInkStroke(kStroke); + ASSERT_EQ(1u, curves.size()); + // Acrobat: 60 520 m 106.6667 540 153.3333 560 200 580 c + ExpectPoint(curves[0].control1, 106.6667, 540); + ExpectPoint(curves[0].control2, 153.3333, 560); + ExpectPoint(curves[0].end, 200, 580); +} + +TEST(CPDFInkCurvesTest, OnePointHasNoCurves) { + static constexpr CFX_PointF kDot[] = {{260, 550}}; + EXPECT_TRUE(FitInkStroke(kDot).empty()); + static constexpr CFX_PointF kRepeated[] = {{260, 550}, {260, 550}}; + EXPECT_TRUE(FitInkStroke(kRepeated).empty()); + EXPECT_TRUE(FitInkStroke({}).empty()); +} + +// A repeated point counts once: Acrobat's case D draws as if it were there +// once. +TEST(CPDFInkCurvesTest, ARepeatedPointCountsOnce) { + static constexpr CFX_PointF kRepeated[] = { + {330, 520}, {330, 520}, {370, 580}, {410, 520}}; + static constexpr CFX_PointF kOnce[] = {{330, 520}, {370, 580}, {410, 520}}; + const std::vector repeated = FitInkStroke(kRepeated); + const std::vector once = FitInkStroke(kOnce); + ASSERT_EQ(2u, repeated.size()); + ASSERT_EQ(once.size(), repeated.size()); + for (size_t i = 0; i < once.size(); ++i) { + ExpectPoint(repeated[i].control1, once[i].control1.x, once[i].control1.y); + ExpectPoint(repeated[i].control2, once[i].control2.x, once[i].control2.y); + ExpectPoint(repeated[i].end, once[i].end.x, once[i].end.y); + } + // Acrobat: 330 520 m 343.3333 540 345.963 580 370 580 c + ExpectPoint(repeated[0].control1, 343.3333, 540); + ExpectPoint(repeated[0].control2, 345.963, 580); +} + +// A stroke that turns straight back splits where it turns, with no direction +// there: the handles at the turn have no length (Acrobat's case E). +TEST(CPDFInkCurvesTest, TurningStraightBackHasNoHandlesAtTheTurn) { + static constexpr CFX_PointF kStroke[] = {{60, 420}, {140, 470}, {60, 420}}; + const std::vector curves = FitInkStroke(kStroke); + ASSERT_EQ(2u, curves.size()); + // Acrobat: 60 420 m 86.6667 436.6667 140 470 140 470 c + // 140 470 86.6667 436.6667 60 420 c + ExpectPoint(curves[0].control1, 86.6667, 436.6667); + ExpectPoint(curves[0].control2, 140, 470); + ExpectPoint(curves[0].end, 140, 470); + ExpectPoint(curves[1].control1, 140, 470); + ExpectPoint(curves[1].control2, 86.6667, 436.6667); + ExpectPoint(curves[1].end, 60, 420); +} diff --git a/fpdfsdk/BUILD.gn b/fpdfsdk/BUILD.gn index 37cf77bac8..5c4403acd5 100644 --- a/fpdfsdk/BUILD.gn +++ b/fpdfsdk/BUILD.gn @@ -236,6 +236,8 @@ pdfium_embeddertest_source_set("embeddertests") { "epdf_form_embeddertest.cpp", # EmbedPDF: covers the annotation API (public/epdf_annot.h). "epdf_annot_embeddertest.cpp", + # EmbedPDF: covers ink appearances against Acrobat's (cpdf_ink_curves.h). + "epdf_ink_appearance_embeddertest.cpp", # EmbedPDF: covers attachments (public/epdf_attachment.h). "epdf_attachment_embeddertest.cpp", # EmbedPDF: covers a document's fonts, the resolver read and the diff --git a/fpdfsdk/epdf_annot_embeddertest.cpp b/fpdfsdk/epdf_annot_embeddertest.cpp index 27bbfffa1b..8a9ae26aac 100644 --- a/fpdfsdk/epdf_annot_embeddertest.cpp +++ b/fpdfsdk/epdf_annot_embeddertest.cpp @@ -819,8 +819,9 @@ TEST_F(EPDFAnnotEmbedderTest, GenerateInkAppearanceSetsRectToWhatItPaints) { /*border_width=*/6.0f)); // Whatever /Rect was, too wide or too tight, it becomes what the strokes - // paint: the points 50..120 x 50..90 and half the width (round caps and - // joins), the same on every redraw. + // paint: the curve through the points 50..120 x 50..90, which rises to + // 90.1728 past the middle one, and half the width (round caps), the same on + // every redraw. const FS_RECTF wide{/*left=*/0.0f, /*top=*/200.0f, /*right=*/200.0f, /*bottom=*/0.0f}; const FS_RECTF tight{/*left=*/50.0f, /*top=*/90.0f, /*right=*/120.0f, @@ -832,7 +833,7 @@ TEST_F(EPDFAnnotEmbedderTest, GenerateInkAppearanceSetsRectToWhatItPaints) { FS_RECTF rect; ASSERT_TRUE(FPDFAnnot_GetRect(annot.get(), &rect)); EXPECT_FLOAT_EQ(47.0f, rect.left) << "iteration " << i; - EXPECT_FLOAT_EQ(93.0f, rect.top) << "iteration " << i; + EXPECT_FLOAT_EQ(93.172844f, rect.top) << "iteration " << i; EXPECT_FLOAT_EQ(123.0f, rect.right) << "iteration " << i; EXPECT_FLOAT_EQ(47.0f, rect.bottom) << "iteration " << i; } @@ -844,7 +845,7 @@ TEST_F(EPDFAnnotEmbedderTest, GenerateInkAppearanceSetsRectToWhatItPaints) { FS_RECTF thinner; ASSERT_TRUE(FPDFAnnot_GetRect(annot.get(), &thinner)); EXPECT_FLOAT_EQ(49.0f, thinner.left); - EXPECT_FLOAT_EQ(91.0f, thinner.top); + EXPECT_FLOAT_EQ(91.172844f, thinner.top); EXPECT_FLOAT_EQ(121.0f, thinner.right); EXPECT_FLOAT_EQ(49.0f, thinner.bottom); } @@ -1008,11 +1009,17 @@ TEST_F(EPDFAnnotEmbedderTest, WidthZeroDrawsAHairline) { FPDFAnnot_SetColor(annot, FPDFANNOT_COLORTYPE_Color, 255, 0, 0, 255)); ASSERT_TRUE(FPDFAnnot_SetBorder(annot, 0, 0, 0)); ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot)); + } + for (FPDF_ANNOTATION annot : {line.get(), polyline.get()}) { const std::wstring look = GetNormalAppearance(annot); EXPECT_THAT(look, HasSubstr(L"1 0 0 RG")); EXPECT_THAT(look, HasSubstr(L"0 w ")); EXPECT_THAT(look, HasSubstr(L" l S\n")); } + // Ink writes as Acrobat does, an operator a line, its curves stroked. + const std::wstring ink_look = GetNormalAppearance(ink.get()); + EXPECT_THAT(ink_look, HasSubstr(L"1 0 0 RG\n0 w\n1 J\n")); + EXPECT_THAT(ink_look, HasSubstr(L" c\nS\n")); // The open arrow is as long as a 1 pt line's: 9 pt. EXPECT_THAT(GetNormalAppearance(line.get()), HasSubstr(L"-7.79423 4.5 m 0 0 l -7.79423 -4.5 l S\n")); diff --git a/fpdfsdk/epdf_ink_appearance_embeddertest.cpp b/fpdfsdk/epdf_ink_appearance_embeddertest.cpp new file mode 100644 index 0000000000..f8ffab1364 --- /dev/null +++ b/fpdfsdk/epdf_ink_appearance_embeddertest.cpp @@ -0,0 +1,348 @@ +// Copyright 2026 CloudPDF LTD +// SPDX-License-Identifier: Apache-2.0 + +// Ink appearances against the ones Acrobat writes (cpdf_ink_curves.h). Each +// case is an ink annotation Acrobat drew from its /InkList alone, on save, +// with the appearance it wrote; ours must draw the same cubics, every number +// within 0.001 pt. + +#include +#include +#include +#include +#include + +#include "constants/annotation_common.h" +#include "core/fpdfapi/page/cpdf_annotcontext.h" +#include "core/fpdfapi/parser/cpdf_dictionary.h" +#include "core/fpdfapi/parser/cpdf_stream.h" +#include "core/fpdfapi/parser/cpdf_stream_acc.h" +#include "fpdfsdk/cpdfsdk_helpers.h" +#include "fpdfsdk/epdf_edit_test_util.h" +#include "public/cpp/fpdf_scopers.h" +#include "public/epdf_annot.h" +#include "public/fpdf_annot.h" +#include "public/fpdf_edit.h" +#include "testing/embedder_test.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +using testing::HasSubstr; +using testing::Not; + +namespace { + +using Stroke = std::vector; + +// A path as its numbers, one list per subpath: the point its `m` moves to, +// then each `c`'s six. +using PathNumbers = std::vector>; + +// An ink annotation as Acrobat drew it: its strokes, and the path of the +// appearance Acrobat wrote for them (the `m` and `c` lines). +struct AcrobatInk { + const char* name; + std::vector strokes; + const char* path; +}; + +// The cases of ink-curves.pdf, all at /W 2 in red. One point (case C) is a +// dot, below. +AcrobatInk UnevenSegments() { + return {"A: uneven segments", + {{{60, 620}, {75, 680}, {160, 700}, {175, 630}, {300, 640}}}, + "60 620 m\n" + "65 640 58.902 667.1216 75 680 c\n" + "97.7288 698.183 133.9659 713.0171 160 700 c\n" + "181.3438 689.3281 153.0664 639.4001 175 630 c\n" + "213.4201 613.5342 258.3333 636.6667 300 640 c\n"}; +} + +AcrobatInk Dense() { + return { + "H: dense, like a real pen", + {{{260, 300}, {263, 302.5}, {266, 301}, {269, 303.5}, {272, 302}, + {275, 304.5}, {278, 303}, {281, 305.5}, {284, 304}, {287, 306.5}, + {290, 305}, {293, 307.5}, {296, 306}, {299, 308.5}, {302, 307}, + {305, 309.5}, {308, 308}, {311, 310.5}, {314, 309}, {317, 311.5}, + {320, 310}, {323, 312.5}, {326, 311}, {329, 313.5}, {332, 312}, + {335, 314.5}, {338, 313}, {341, 315.5}, {344, 314}, {347, 316.5}, + {350, 315}, {353, 317.5}, {356, 316}, {359, 318.5}, {362, 317}, + {365, 319.5}, {368, 318}, {371, 320.5}, {374, 319}, {377, 321.5}}}, + "260 300 m\n" + "268.7073 307.2561 257.6714 299.6119 266 301 c\n" + "267.284 301.214 267.716 303.286 269 303.5 c\n" + "270.1028 303.6838 270.8972 301.8162 272 302 c\n" + "273.284 302.214 273.716 304.286 275 304.5 c\n" + "276.1028 304.6838 276.8972 302.8162 278 303 c\n" + "279.284 303.214 279.716 305.286 281 305.5 c\n" + "282.1028 305.6838 282.8972 303.8162 284 304 c\n" + "285.284 304.214 285.716 306.286 287 306.5 c\n" + "288.1028 306.6838 288.8972 304.8162 290 305 c\n" + "291.284 305.214 291.716 307.286 293 307.5 c\n" + "294.1028 307.6838 294.8972 305.8162 296 306 c\n" + "297.284 306.214 297.716 308.286 299 308.5 c\n" + "300.1028 308.6838 300.8972 306.8162 302 307 c\n" + "303.284 307.214 303.716 309.286 305 309.5 c\n" + "306.1028 309.6838 306.8972 307.8162 308 308 c\n" + "309.284 308.214 309.716 310.286 311 310.5 c\n" + "312.1028 310.6838 312.8972 308.8162 314 309 c\n" + "315.284 309.214 315.716 311.286 317 311.5 c\n" + "318.1028 311.6838 318.8972 309.8162 320 310 c\n" + "321.284 310.214 321.716 312.286 323 312.5 c\n" + "324.1028 312.6838 324.8972 310.8162 326 311 c\n" + "327.284 311.214 327.716 313.286 329 313.5 c\n" + "330.1028 313.6838 330.8972 311.8162 332 312 c\n" + "333.284 312.214 333.716 314.286 335 314.5 c\n" + "336.1028 314.6838 336.8972 312.8162 338 313 c\n" + "339.284 313.214 339.716 315.286 341 315.5 c\n" + "342.1028 315.6838 342.8972 313.8162 344 314 c\n" + "345.284 314.214 345.716 316.286 347 316.5 c\n" + "348.1028 316.6838 348.8972 314.8162 350 315 c\n" + "351.284 315.214 351.716 317.286 353 317.5 c\n" + "354.1028 317.6838 354.8972 315.8162 356 316 c\n" + "357.284 316.214 357.716 318.286 359 318.5 c\n" + "360.1028 318.6838 360.8972 316.8162 362 317 c\n" + "363.284 317.214 363.716 319.286 365 319.5 c\n" + "366.1028 319.6838 366.8972 317.8162 368 318 c\n" + "369.284 318.214 369.716 320.286 371 320.5 c\n" + "379.3286 321.8881 368.2927 314.2439 377 321.5 c\n"}; +} + +std::vector AcrobatInks() { + return { + UnevenSegments(), + {"B: two points", + {{{60, 520}, {200, 580}}}, + "60 520 m\n" + "106.6667 540 153.3333 560 200 580 c\n"}, + {"D: a point repeated", + {{{330, 520}, {330, 520}, {370, 580}, {410, 520}}}, + "330 520 m\n" + "343.3333 540 345.963 580 370 580 c\n" + "394.037 580 396.6667 540 410 520 c\n"}, + {"E: straight back on itself", + {{{60, 420}, {140, 470}, {60, 420}}}, + "60 420 m\n" + "86.6667 436.6667 140 470 140 470 c\n" + "140 470 86.6667 436.6667 60 420 c\n"}, + {"F: two strokes", + {{{220, 420}, {260, 470}, {300, 420}}, + {{330, 420}, {370, 470}, {410, 440}}}, + "220 420 m\n" + "233.3333 436.6667 238.6563 470 260 470 c\n" + "281.3438 470 286.6667 436.6667 300 420 c\n" + "330 420 m\n" + "343.3333 436.6667 349.2935 464.8234 370 470 c\n" + "386.169 474.0423 396.6667 450 410 440 c\n"}, + {"G: a tight turn between long runs", + {{{60, 320}, {200, 330}, {205, 300}, {60, 290}}}, + "60 320 m\n" + "106.6667 323.3333 153.6532 336.3927 200 330 c\n" + "210.0429 328.6148 214.7479 302.7851 205 300 c\n" + "158.416 286.6903 108.3333 293.3333 60 290 c\n"}, + Dense(), + }; +} + +PathNumbers PathOf(const std::string& content) { + PathNumbers path; + std::vector operands; + std::istringstream tokens(content); + std::string token; + while (tokens >> token) { + char* end = nullptr; + const float value = std::strtof(token.c_str(), &end); + if (end != token.c_str() && *end == '\0') { + operands.push_back(value); + continue; + } + if (token == "m" && operands.size() >= 2) { + path.push_back({operands.end()[-2], operands.end()[-1]}); + } else if (token == "c" && operands.size() >= 6 && !path.empty()) { + path.back().insert(path.back().end(), operands.end() - 6, operands.end()); + } + operands.clear(); + } + return path; +} + +// `path` moved by (dx, dy). +PathNumbers Moved(PathNumbers path, float dx, float dy) { + for (std::vector& subpath : path) { + for (size_t i = 0; i < subpath.size(); ++i) { + subpath[i] += i % 2 == 0 ? dx : dy; + } + } + return path; +} + +std::vector Moved(std::vector strokes, float dx, float dy) { + for (Stroke& stroke : strokes) { + for (FS_POINTF& point : stroke) { + point.x += dx; + point.y += dy; + } + } + return strokes; +} + +void ExpectSamePath(const PathNumbers& expected, const PathNumbers& actual) { + ASSERT_EQ(expected.size(), actual.size()); + for (size_t i = 0; i < expected.size(); ++i) { + ASSERT_EQ(expected[i].size(), actual[i].size()) << "subpath " << i; + for (size_t j = 0; j < expected[i].size(); ++j) { + EXPECT_NEAR(expected[i][j], actual[i][j], 0.001f) + << "subpath " << i << ", number " << j; + } + } +} + +// The content of `annot`'s normal appearance. +std::string AppearanceOf(FPDF_ANNOTATION annot) { + const CPDF_Dictionary* annot_dict = + CPDFAnnotContextFromFPDFAnnotation(annot)->GetAnnotDict(); + RetainPtr ap_dict = + annot_dict->GetDictFor(pdfium::annotation::kAP); + RetainPtr normal = + ap_dict ? ap_dict->GetStreamFor("N") : nullptr; + if (!normal) { + ADD_FAILURE() << "no normal appearance"; + return std::string(); + } + auto stream_acc = pdfium::MakeRetain(std::move(normal)); + stream_acc->LoadAllDataFiltered(); + const pdfium::span data = stream_acc->GetSpan(); + return std::string(data.begin(), data.end()); +} + +// A red ink annotation of `strokes` at `width`, with the appearance we draw +// for it. +ScopedFPDFAnnotation NewRedInk(FPDF_PAGE page, + const std::vector& strokes, + float width) { + ScopedFPDFAnnotation ink(FPDFPage_CreateAnnot(page, FPDF_ANNOT_INK)); + EXPECT_TRUE(ink); + for (const Stroke& stroke : strokes) { + EXPECT_GE(FPDFAnnot_AddInkStroke(ink.get(), stroke.data(), stroke.size()), + 0); + } + EXPECT_TRUE( + FPDFAnnot_SetColor(ink.get(), FPDFANNOT_COLORTYPE_Color, 255, 0, 0, 255)); + EXPECT_TRUE(FPDFAnnot_SetBorder(ink.get(), 0, 0, width)); + EXPECT_TRUE(EPDFAnnot_GenerateAppearance(ink.get())); + return ink; +} + +} // namespace + +class EPDFInkAppearanceEmbedderTest : public EmbedderTest {}; + +TEST_F(EPDFInkAppearanceEmbedderTest, DrawsAcrobatsCurves) { + embedpdf_test::ScopedTestDocument doc(NewTestDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 612, 792)); + ASSERT_TRUE(page); + for (const AcrobatInk& acrobat : AcrobatInks()) { + SCOPED_TRACE(acrobat.name); + ScopedFPDFAnnotation ink = NewRedInk(page.get(), acrobat.strokes, 2); + const std::string content = AppearanceOf(ink.get()); + // Set as Acrobat sets it: colour, width, round caps, and no join. + EXPECT_THAT(content, HasSubstr("1 0 0 RG\n2 w\n1 J\n")); + EXPECT_THAT(content, Not(HasSubstr(" j\n"))); + ExpectSamePath(PathOf(acrobat.path), PathOf(content)); + } +} + +// One point is Acrobat's dot: a circle of radius 0.5, filled and stroked. +// Acrobat leaves the fill colour at its black default, which a stroke of +// width 1 or more covers; ours fills in the stroke's colour. +TEST_F(EPDFInkAppearanceEmbedderTest, OnePointIsAcrobatsDot) { + embedpdf_test::ScopedTestDocument doc(NewTestDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 612, 792)); + ASSERT_TRUE(page); + ScopedFPDFAnnotation ink = NewRedInk(page.get(), {{{260, 550}}}, 2); + EXPECT_THAT(AppearanceOf(ink.get()), + HasSubstr("1 0 0 RG\n2 w\n1 J\n1 0 0 rg\n" + "260 550.5 m\n" + "260.2778 550.5 260.5 550.2778 260.5 550 c\n" + "260.5 549.7222 260.2778 549.5 260 549.5 c\n" + "259.7222 549.5 259.5 549.7222 259.5 550 c\n" + "259.5 550.2778 259.7222 550.5 260 550.5 c\n" + "h\n" + "B\n")); +} + +// ink-widths.pdf: Acrobat drew the dense stroke and the uneven one at /W 0.5, +// 1, 4 and 8, each 170 pt below the one before, with the same curves (cases H +// and A moved). The width doesn't change the fit; 1, the default, isn't +// written. +TEST_F(EPDFInkAppearanceEmbedderTest, TheCurvesDoNotDependOnTheWidth) { + embedpdf_test::ScopedTestDocument doc(NewTestDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 612, 792)); + ASSERT_TRUE(page); + struct Width { + float width; + const char* written; + }; + static constexpr Width kWidths[] = {{0.5f, "1 0 0 RG\n0.5 w\n1 J\n"}, + {1, "1 0 0 RG\n1 J\n"}, + {4, "1 0 0 RG\n4 w\n1 J\n"}, + {8, "1 0 0 RG\n8 w\n1 J\n"}}; + const AcrobatInk dense_ink = Dense(); + const AcrobatInk uneven_ink = UnevenSegments(); + for (size_t i = 0; i < std::size(kWidths); ++i) { + SCOPED_TRACE(kWidths[i].width); + const float down = 170.0f * i; + ScopedFPDFAnnotation dense = + NewRedInk(page.get(), Moved(dense_ink.strokes, -220, 420 - down), + kWidths[i].width); + ScopedFPDFAnnotation uneven = + NewRedInk(page.get(), Moved(uneven_ink.strokes, 240, 60 - down), + kWidths[i].width); + for (FPDF_ANNOTATION ink : {dense.get(), uneven.get()}) { + EXPECT_THAT(AppearanceOf(ink), HasSubstr(kWidths[i].written)); + } + ExpectSamePath(Moved(PathOf(dense_ink.path), -220, 420 - down), + PathOf(AppearanceOf(dense.get()))); + ExpectSamePath(Moved(PathOf(uneven_ink.path), 240, 60 - down), + PathOf(AppearanceOf(uneven.get()))); + } +} + +// Cell 1E of no-stroke-check.pdf, a zigzag of nine points: every point is +// a turn, so every pair is its own cubic, its handles a third of its length +// and level at each tip. The first cubic is Acrobat's own. +TEST_F(EPDFInkAppearanceEmbedderTest, AZigzagIsOneCubicPerPair) { + embedpdf_test::ScopedTestDocument doc(NewTestDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 612, 792)); + ASSERT_TRUE(page); + ScopedFPDFAnnotation ink = NewRedInk(page.get(), + {{{526, 654}, + {531, 682}, + {536, 654}, + {541, 682}, + {546, 654}, + {551, 682}, + {556, 654}, + {561, 682}, + {566, 654}}}, + 2); + const std::string content = AppearanceOf(ink.get()); + EXPECT_THAT( + content, + HasSubstr("526 654 m\n527.6667 663.3333 521.519 682 531 682 c\n")); + ExpectSamePath(PathOf("526 654 m\n" + "527.6667 663.3333 521.519 682 531 682 c\n" + "540.481 682 526.519 654 536 654 c\n" + "545.481 654 531.519 682 541 682 c\n" + "550.481 682 536.519 654 546 654 c\n" + "555.481 654 541.519 682 551 682 c\n" + "560.481 682 546.519 654 556 654 c\n" + "565.481 654 551.519 682 561 682 c\n" + "570.481 682 564.3333 663.3333 566 654 c\n"), + PathOf(content)); +} diff --git a/fpdfsdk/fpdf_annot_embeddertest.cpp b/fpdfsdk/fpdf_annot_embeddertest.cpp index e5a7dec345..2f99c0f44e 100644 --- a/fpdfsdk/fpdf_annot_embeddertest.cpp +++ b/fpdfsdk/fpdf_annot_embeddertest.cpp @@ -580,8 +580,9 @@ TEST_F(FPDFAnnotEmbedderTest, ExtractInkMultiple) { EXPECT_EQ(475.336121f, rect.right); EXPECT_EQ(681.535034f, rect.top); } - // EmbedPDF(own-appearance-generator): our ink appearance strokes with round - // caps and joins and fits its box to the stroke; the dump is ours. + // EmbedPDF(own-appearance-generator): our ink appearance draws Acrobat's + // fitted curves with round caps and fits its box to the stroke; the dump is + // ours. CompareDisplayList(RenderLoadedPageWithFlagsToDisplayList(page, FPDF_ANNOT), "annotation_ink_multiple"); UnloadPageNoEvents(page); diff --git a/testing/embedpdf_layer_mode_writers.txt b/testing/embedpdf_layer_mode_writers.txt index 4186cfc22f..bd729339bc 100644 --- a/testing/embedpdf_layer_mode_writers.txt +++ b/testing/embedpdf_layer_mode_writers.txt @@ -242,6 +242,10 @@ EPDFFormEmbedderTest.ToggleWidgetsTurnOnTheirOwnState 3 EPDFFormEmbedderTest.TwoPlaneTwinWidgetsFillTogether 9 EPDFFormEmbedderTest.TypedValueSnapshotPreservesPdfShapes 3 EPDFFormEmbedderTest.WidgetRotationTurnsItsAppearance 2 +EPDFInkAppearanceEmbedderTest.AZigzagIsOneCubicPerPair 4 +EPDFInkAppearanceEmbedderTest.DrawsAcrobatsCurves 10 +EPDFInkAppearanceEmbedderTest.OnePointIsAcrobatsDot 4 +EPDFInkAppearanceEmbedderTest.TheCurvesDoNotDependOnTheWidth 11 EPDFMeasureEmbedderTest.BadArgumentsAndRejectedWrites 5 EPDFMeasureEmbedderTest.CaptionAppearanceGolden 19 EPDFMeasureEmbedderTest.DirectMeasureWithSharedNestedFormatDetachesOnlyItsOwner 5 diff --git a/testing/resources/embedder_tests/annotation_ink_multiple.dump.txt b/testing/resources/embedder_tests/annotation_ink_multiple.dump.txt index 0fbf77ecde..e789c7232a 100644 --- a/testing/resources/embedder_tests/annotation_ink_multiple.dump.txt +++ b/testing/resources/embedder_tests/annotation_ink_multiple.dump.txt @@ -36,17 +36,17 @@ DrawDeviceText font="Arial" size=14.6667 color=0xff000000 matrix=[0.7500,0.0000, RestoreState SaveState RestoreState -SetClip_PathFill rule=winding matrix=[1.0000,0.0000,0.0000,-1.0000,0.0000,792.0000] points=[move(85.8128,668.3480) line(85.8128,702.3980) line(135.8370,702.3980) line(135.8370,668.3480) line(85.8128,668.3480,close)] -DrawPath fill=0x00000000 stroke=0xff00ff00 rule=no-fill stroke_state=[width=2.0000 cap=round join=round miter=10.0000] matrix=[1.0000,0.0000,0.0000,-1.0000,0.0000,792.0000] points=[move(86.8128,676.1270) line(87.4285,676.1270) line(88.6598,676.7440) line(88.6598,677.3600) line(89.8912,677.9770) line(89.8912,678.5930) line(90.5068,679.2100) line(91.1225,679.2100) line(91.1225,679.8260) line(91.7382,679.8260) line(91.7382,680.4420) line(92.3539,680.4420) line(92.3539,681.0580) line(92.9695,681.0580) line(93.5858,681.6740) line(94.2015,681.6740) line(95.4328,682.2910) line(95.4328,682.9080) line(96.0485,682.9080) line(96.6642,683.5240) line(97.2798,683.5240) line(97.8955,683.5240) line(98.5112,683.5240) line(99.1269,683.5240) line(99.7425,683.5240) line(100.3580,683.5240) line(100.9740,683.5240) line(101.5900,683.5240) line(102.2050,682.9080) line(102.8210,682.9080) line(103.4370,682.2910) line(104.0520,681.6740) line(105.2840,681.0580) line(106.5150,679.8260) line(107.1310,679.2100) line(107.7460,677.9770) line(108.3620,677.3600) line(108.9780,677.3600) line(108.9780,676.1270) line(109.5930,675.5110) line(109.5930,674.8950) line(109.5930,674.2790) line(109.5930,673.0460) line(109.5930,672.4290) line(109.5930,671.8130) line(109.5930,671.1970) line(109.5930,670.5810) line(109.5930,669.9640) line(108.9780,669.3480) line(108.3620,669.3480) line(107.1310,669.3480) line(106.5150,669.3480) line(105.8990,669.3480) line(105.8990,669.9640) line(105.2840,669.9640) line(104.6680,669.9640) line(104.6680,670.5810) line(104.0520,670.5810) line(103.4370,671.1970) line(102.2050,671.8130) line(102.2050,672.4290) line(101.5900,672.4290) line(101.5900,673.6630) line(100.9740,674.2790) line(100.3580,674.8950) line(100.3580,675.5110) line(100.3580,676.1270) line(99.7425,677.3600) line(99.7425,677.9770) line(99.7425,679.2100) line(99.7425,679.8260) line(99.7425,680.4420) line(100.3580,682.2910) line(100.3580,682.9080) line(100.3580,684.1400) line(100.9740,685.3730) line(101.5900,685.9890) line(102.2050,686.6050) line(102.8210,687.2220) line(102.8210,687.8380) line(103.4370,687.8380) line(104.0520,688.4550) line(104.6680,688.4550) line(105.2840,688.4550) line(106.5150,689.0710) line(107.1310,689.0710) line(107.7460,689.0710) line(108.3620,689.0710) line(108.3620,689.6870) line(108.9780,689.6870) line(109.5930,689.6870) line(110.2090,689.6870) line(110.8250,689.6870) line(111.4410,689.6870) line(111.4410,689.0710) line(112.0570,689.0710) line(112.6720,689.0710) line(113.2880,688.4550) line(113.9040,688.4550) line(114.5190,687.8380) line(115.7510,687.2220) line(116.3660,686.6050) line(116.9820,685.9890) line(117.5980,684.7560) line(118.2130,684.7560) line(118.2130,684.1400) line(118.8290,683.5240) line(119.4450,682.9080) line(119.4450,682.2910) line(120.0600,681.6740) line(120.0600,681.0580) line(120.0600,680.4420) line(120.6760,679.8260) line(120.6760,679.2100) line(121.2920,678.5930) line(121.2920,677.9770) line(121.2920,677.3600) line(121.2920,676.7440) line(121.2920,676.1270) line(121.2920,675.5110) line(121.9070,675.5110) line(121.9070,674.8950) line(121.9070,674.2790) line(121.9070,673.6630) line(121.9070,673.0460) line(121.2920,673.0460) line(120.6760,673.0460) line(120.0600,673.0460) line(119.4450,673.0460) line(118.8290,673.0460) line(118.2130,673.6630) line(117.5980,674.2790) line(116.9820,674.8950) line(116.9820,675.5110) line(116.3660,676.1270) line(116.3660,676.7440) line(115.7510,677.3600) line(115.7510,677.9770) line(115.7510,678.5930) line(115.1350,678.5930) line(115.1350,679.8260) line(115.1350,680.4420) line(115.1350,681.0580) line(115.1350,681.6740) line(115.1350,682.9080) line(115.1350,683.5240) line(115.1350,684.1400) line(115.1350,684.7560) line(115.1350,685.9890) line(115.1350,686.6050) line(115.7510,687.2220) line(115.7510,688.4550) line(116.3660,689.0710) line(116.3660,689.6870) line(116.9820,690.3030) line(116.9820,690.9200) line(117.5980,690.9200) line(117.5980,691.5360) line(117.5980,692.1530) line(118.8290,692.1530) line(119.4450,692.7690) line(120.6760,693.3850) line(121.2920,694.0020) line(121.9070,694.6180) line(122.5230,695.2340) line(123.1390,695.2340) line(123.7540,695.2340) line(123.7540,695.8500) line(124.3700,695.8500) line(124.9860,695.8500) line(125.6010,695.8500) line(126.2170,695.8500) line(126.8330,695.8500) line(127.4480,695.8500) line(128.6800,695.2340) line(129.2960,695.2340) line(130.5270,694.6180) line(130.5270,694.0020) line(131.1430,694.0020) line(131.7590,693.3850) line(132.3740,692.7690) line(132.9900,692.7690) line(132.9900,692.1530) line(133.6060,691.5360) line(133.6060,690.9200) line(134.2210,690.3030) line(134.2210,689.6870) line(134.2210,689.0710) line(134.2210,688.4550) line(134.2210,687.8380) line(134.2210,687.2220) line(134.2210,686.6050) line(134.8370,686.6050) line(134.8370,685.9890) line(134.8370,685.3730) line(134.8370,684.7560) line(134.2210,684.7560) line(134.2210,684.1400) line(134.2210,683.5240) line(133.6060,683.5240) line(133.6060,682.9080) line(132.9900,682.9080) line(132.3740,682.9080) line(131.7590,682.9080) line(131.7590,683.5240) line(131.1430,684.1400) line(130.5270,684.1400) line(130.5270,684.7560) line(129.9120,684.7560) line(129.9120,685.3730) line(128.6800,685.3730) line(128.6800,685.9890) line(128.0650,686.6050) line(127.4480,687.2220) line(127.4480,687.8380) line(126.8330,689.0710) line(126.2170,689.0710) line(126.2170,689.6870) line(126.2170,690.3030) line(126.2170,690.9200) line(125.6010,691.5360) line(125.6010,692.1530) line(125.6010,693.3850) line(125.6010,694.0020) line(125.6010,695.2340) line(125.6010,696.4670) line(125.6010,697.0840) line(126.2170,697.7000) line(126.2170,698.3160) line(126.8330,698.3160) line(126.8330,698.9320) line(126.8330,699.5480) line(127.4480,699.5480) line(128.0650,699.5480) line(128.0650,700.1650) line(128.6800,700.1650) line(128.6800,700.7810) line(129.2960,700.7810) line(129.9120,700.7810) line(130.5270,700.7810) line(131.1430,700.7810) line(131.7590,700.7810) line(131.7590,701.3980) line(132.3740,701.3980) line(132.9900,701.3980) line(133.6060,701.3980) line(134.2210,701.3980)] +SetClip_PathFill rule=winding matrix=[1.0000,0.0000,0.0000,-1.0000,0.0000,792.0000] points=[move(85.8128,667.7352) line(85.8128,702.3976) line(135.8107,702.3976) line(135.8107,667.7352) line(85.8128,667.7352,close)] +DrawPath fill=0x00000000 stroke=0xff00ff00 rule=no-fill stroke_state=[width=2.0000 cap=round join=miter miter=10.0000] matrix=[1.0000,0.0000,0.0000,-1.0000,0.0000,792.0000] points=[move(86.8128,676.1273) bezier(91.4113,676.1273) bezier(94.4391,687.1021) bezier(101.5896,683.5237) bezier(105.8004,681.4165) bezier(109.5933,677.6286) bezier(109.5933,673.0455) bezier(109.5933,672.0406) bezier(110.0509,669.8848) bezier(108.9776,669.3477) bezier(100.8691,665.2903) bezier(97.5077,682.5200) bezier(102.8209,687.8378) bezier(109.3378,694.3603) bezier(117.3941,685.7789) bezier(120.0603,680.4421) bezier(120.6332,679.2954) bezier(122.9470,674.0874) bezier(121.9073,673.0455) bezier(120.7124,671.8480) bezier(117.5035,673.8509) bezier(116.9820,674.8949) bezier(115.7752,677.3105) bezier(113.4472,683.2270) bezier(115.1349,686.6054) bezier(117.6036,691.5467) bezier(120.0672,692.1603) bezier(123.7544,695.8504) bezier(124.7388,696.8356) bezier(129.8655,695.2804) bezier(130.5274,694.6180) bezier(133.2588,691.8843) bezier(136.6629,685.9675) bezier(133.6057,682.9075) bezier(132.5842,681.8851) bezier(127.8942,686.3300) bezier(127.4484,687.2216) bezier(123.5521,695.0128) bezier(125.8928,701.3976) bezier(134.2214,701.3976)] RestoreState SaveState RestoreState -SetClip_PathFill rule=winding matrix=[1.0000,0.0000,0.0000,-1.0000,0.0000,792.0000] points=[move(147.6100,557.3020) line(147.6100,635.4830) line(290.2730,635.4830) line(290.2730,557.3020) line(147.6100,557.3020,close)] -DrawPath fill=0x00000000 stroke=0xffff0000 rule=no-fill stroke_state=[width=6.0000 cap=round join=round miter=10.0000] matrix=[1.0000,0.0000,0.0000,-1.0000,0.0000,792.0000] points=[move(166.5470,560.3020) line(166.1480,561.1000) line(164.9530,562.6950) line(163.7580,564.6890) line(161.3670,568.2780) line(159.3750,571.0690) line(156.9840,576.2540) line(154.9920,579.4440) line(153.3980,582.6350) line(151.4060,589.0150) line(150.6100,593.4020) line(150.6100,596.5920) line(150.6100,601.3780) line(150.6100,604.1690) line(152.6020,608.5560) line(154.5940,610.1510) line(156.1870,611.7460) line(158.5780,613.3410) line(161.7660,614.9370) line(167.3440,616.5320) line(171.3280,617.3290) line(175.3120,617.3290) line(182.0860,617.3290) line(187.2660,617.3290) line(194.8360,617.3290) line(199.2190,616.1330) line(203.6010,615.3360) line(209.5780,613.3410) line(212.3670,612.5440) line(215.9530,609.7520) line(217.9450,608.9550) line(218.3440,608.1570) line(219.1410,606.5620) line(219.9380,604.9670) line(220.7340,602.5740) line(221.1330,600.9790) line(221.1330,599.3840) line(221.1330,595.3960) line(221.1330,593.0030) line(220.7340,591.0090) line(219.1410,586.6220) line(217.5470,583.8310) line(214.3590,579.8430) line(211.9690,577.8490) line(209.5780,575.8550) line(205.9920,573.8610) line(203.6010,573.0630) line(200.8120,572.2660) line(199.2190,572.2660) line(198.8200,572.2660) line(197.6250,571.8670) line(196.8280,571.8670) line(196.0310,571.8670) line(195.6330,571.8670) line(195.2340,571.8670) line(194.8360,572.6640) line(194.4370,573.4620) line(193.6400,575.8550) line(192.8440,578.2480) line(192.0470,583.4320) line(191.2500,585.8250) line(191.2500,587.8180) line(191.6480,591.8070) line(192.4450,594.9970) line(194.8360,598.9850) line(196.8280,601.7770) line(198.4220,604.1690) line(201.6090,607.3590) line(205.1950,608.9550) line(207.5860,610.9490) line(212.3670,613.3410) line(215.5550,614.1390) line(221.1330,615.3360) line(224.7190,615.3360) line(229.5000,615.3360) line(232.2890,615.3360) line(233.4840,614.5380) line(235.8750,614.1390) line(236.2740,612.9420) line(237.0700,612.5440) line(238.2660,610.5500) line(239.4610,608.5560) line(241.0550,604.5680) line(241.8510,602.1750) line(241.8510,599.7820) line(241.8510,594.9970) line(241.8510,591.8070) line(241.8510,588.6160) line(240.6560,586.2230) line(240.2580,584.6280) line(238.6640,582.2360) line(237.0700,581.4380) line(235.4770,579.8430) line(234.2810,579.8430) line(233.4840,579.4440) line(232.6870,579.0450) line(232.2890,579.0450) line(231.8900,579.0450) line(231.0940,579.8430) line(229.8980,583.8310) line(229.1020,587.0210) line(227.1100,592.2050) line(227.1100,595.7950) line(226.3130,599.3840) line(226.3130,604.9670) line(226.3130,608.1570) line(226.3130,611.7460) line(229.1020,616.5320) line(231.4920,618.9240) line(235.8750,622.5140) line(238.6640,624.5080) line(241.0550,626.1030) line(246.6330,629.6910) line(250.2190,630.4900) line(257.7890,632.4830) line(262.1720,632.4830) line(265.3600,632.4830) line(271.3360,632.4830) line(274.1250,631.6860) line(277.7110,630.4900) line(279.3050,629.6910) line(281.2970,628.4960) line(282.4920,626.9000) line(283.2890,626.5010) line(284.4850,624.9060) line(285.2810,623.7100) line(285.2810,623.3110) line(286.0780,622.1140) line(286.0780,621.7160) line(286.4770,621.3170) line(286.4770,620.1210) line(286.8750,619.7220) line(286.8750,618.9240) line(287.2730,618.1270) line(287.2730,617.7280) line(287.2730,617.3290) line(287.2730,616.9310) line(287.2730,616.5320) line(287.2730,616.1330) line(287.2730,615.7340) line(287.2730,615.3360)] +SetClip_PathFill rule=winding matrix=[1.0000,0.0000,0.0000,-1.0000,0.0000,792.0000] points=[move(146.6938,557.3020) line(146.6938,635.9964) line(290.2734,635.9964) line(290.2734,557.3020) line(146.6938,557.3020,close)] +DrawPath fill=0x00000000 stroke=0xffff0000 rule=no-fill stroke_state=[width=6.0000 cap=round join=miter miter=10.0000] matrix=[1.0000,0.0000,0.0000,-1.0000,0.0000,792.0000] points=[move(166.5466,560.3020) bezier(159.9053,573.5969) bezier(146.0085,587.5890) bezier(150.6095,604.1693) bezier(152.6516,611.5280) bezier(160.6363,614.8533) bezier(167.3441,616.5316) bezier(183.0008,620.4491) bezier(204.5477,617.8945) bezier(217.9454,608.9545) bezier(220.0870,607.5255) bezier(221.1327,601.7145) bezier(221.1327,599.3840) bezier(221.1327,589.5718) bezier(218.1850,581.5984) bezier(209.5782,575.8553) bezier(208.2582,574.9745) bezier(197.4461,569.6528) bezier(195.2341,571.8668) bezier(192.8162,574.2868) bezier(192.6351,580.6356) bezier(192.0468,583.4324) bezier(188.1972,601.7355) bezier(208.2405,615.3357) bezier(224.7191,615.3357) bezier(228.0560,615.3357) bezier(232.8178,615.8881) bezier(235.8752,614.1390) bezier(243.2536,609.9178) bezier(243.2649,592.7511) bezier(240.6561,586.2233) bezier(239.9791,584.5293) bezier(234.8666,577.0596) bezier(231.8905,579.0455) bezier(228.6614,581.2001) bezier(227.4781,592.4746) bezier(227.1095,595.7947) bezier(226.6156,600.2442) bezier(224.8181,607.2584) bezier(226.3127,611.7463) bezier(230.0790,623.0561) bezier(247.3134,630.7362) bezier(257.7891,632.4833) bezier(270.9365,634.6759) bezier(287.2734,630.0032) bezier(287.2734,615.3357)] RestoreState SaveState RestoreState -SetClip_PathFill rule=winding matrix=[1.0000,0.0000,0.0000,-1.0000,0.0000,792.0000] points=[move(351.1170,583.1280) line(351.1170,682.2370) line(476.0390,682.2370) line(476.0390,583.1280) line(351.1170,583.1280,close)] -DrawPath fill=0x00000000 stroke=0x4c0000ff rule=no-fill stroke_state=[width=3.0000 cap=round join=round miter=10.0000] matrix=[1.0000,0.0000,0.0000,-1.0000,0.0000,792.0000] points=[move(414.3750,679.9400) line(410.7890,676.7490) line(407.2030,673.1600) line(400.4300,667.1780) line(396.0470,662.7920) line(392.4610,659.2020) line(384.0940,652.4230) line(379.7110,648.0370) line(375.3280,643.6500) line(367.7580,636.8700) line(364.1720,634.0780) line(358.9920,628.8940) line(356.2030,626.1030) line(353.4140,623.3110) line(352.6170,622.5140) line(352.6170,622.1140) line(352.6170,621.7160) line(353.4140,620.9190) line(355.0080,620.1210) line(363.7730,617.3290) line(370.5470,614.5380) line(377.3200,611.7460) line(394.4530,608.1570) line(403.6170,607.3590) line(422.3440,605.3650) line(432.3050,604.5680) line(441.4690,603.3720) line(457.0080,602.1750) line(462.9840,602.1750) line(471.3510,601.3780) line(474.1410,601.3780) line(474.5390,601.3780) line(474.5390,601.7770) line(472.5470,603.7700) line(468.5620,607.7590) line(464.1800,611.3470) line(459.7970,614.9370) line(449.0390,623.3110) line(442.2650,628.4960) line(427.5240,639.2630) line(419.9530,645.2450) line(411.5860,651.2270) line(396.0470,661.9940) line(389.2740,666.3810) line(377.7190,673.1600) line(372.5390,676.7490) line(368.9530,678.7430) line(364.5700,680.7370) line(363.7730,680.7370) line(363.3750,680.7370) line(363.3750,679.9400) line(364.1720,675.1540) line(365.3670,670.7670) line(368.1560,660.0000) line(370.1490,653.2200) line(372.1410,646.4410) line(376.5230,633.2810) line(378.5160,626.5010) line(382.1020,614.9370) line(384.8900,608.9550) line(387.6800,599.7820) line(389.6720,595.3960) line(390.4690,591.0090) line(391.6640,586.6220) line(392.4610,585.8250) line(392.8590,585.0270) line(393.2580,585.0270) line(393.2580,584.6280) line(393.2580,585.8250) line(394.0550,590.2120) line(394.8510,593.4020) line(396.0470,600.9790) line(397.2420,606.1640) line(398.4380,611.3470) line(401.2270,622.1140) line(403.2190,628.0960) line(406.0080,639.6620) line(408.0000,645.6430) line(409.9920,651.6250) line(411.9840,660.0000) line(412.7810,664.3870) line(413.5780,668.7730) line(413.5780,669.5710) line(413.5780,669.9690) line(413.5780,670.3690) line(413.9760,670.7670) line(413.9760,671.1660) line(413.9760,671.9640) line(414.3750,672.3630) line(414.3750,673.1600) line(414.7730,674.3560) line(415.1720,674.7550) line(415.1720,675.9510) line(415.5700,676.3510) line(415.5700,676.7490)] +SetClip_PathFill rule=winding matrix=[1.0000,0.0000,0.0000,-1.0000,0.0000,792.0000] points=[move(350.9152,582.9253) line(350.9152,682.4658) line(476.1988,682.4658) line(476.1988,582.9253) line(350.9152,582.9253,close)] +DrawPath fill=0x00000000 stroke=0x4c0000ff rule=no-fill stroke_state=[width=3.0000 cap=round join=miter miter=10.0000] matrix=[1.0000,0.0000,0.0000,-1.0000,0.0000,792.0000] points=[move(414.3748,679.9399) bezier(398.5663,665.8717) bezier(383.8155,650.6469) bezier(367.7575,636.8702) bezier(363.7363,633.4202) bezier(359.9591,629.8619) bezier(356.2030,626.1029) bezier(354.8681,624.7669) bezier(351.5701,623.2878) bezier(352.6173,621.7161) bezier(355.3675,617.5883) bezier(365.9719,616.4236) bezier(370.5470,614.5381) bezier(392.4009,605.5317) bezier(418.1520,605.6300) bezier(441.4687,603.3718) bezier(451.4414,602.4059) bezier(461.3800,602.0903) bezier(471.3514,601.3775) bezier(472.4113,601.3017) bezier(473.7886,600.6254) bezier(474.5393,601.3775) bezier(475.2726,602.1121) bezier(473.2802,603.0359) bezier(472.5466,603.7701) bezier(468.5174,607.8029) bezier(464.2716,611.4006) bezier(459.7968,614.9365) bezier(437.1517,632.8302) bezier(413.9472,651.3481) bezier(389.2736,666.3809) bezier(385.4601,668.7043) bezier(381.5151,670.8085) bezier(377.7191,673.1604) bezier(375.9335,674.2667) bezier(374.3108,675.6206) bezier(372.5391,676.7490) bezier(371.6932,677.2877) bezier(364.0315,682.0516) bezier(363.3750,680.7375) bezier(361.9759,677.9368) bezier(367.9632,660.7072) bezier(368.1559,659.9998) bezier(372.2567,644.9514) bezier(376.7772,629.5925) bezier(382.1016,614.9365) bezier(384.4704,608.4162) bezier(387.5682,602.0136) bezier(389.6720,595.3955) bezier(392.3201,587.0653) bezier(388.4663,594.9449) bezier(391.6641,586.6225) bezier(391.9693,585.8282) bezier(393.2577,583.7773) bezier(393.2577,584.6282) bezier(393.2577,602.5329) bezier(401.3709,622.6469) bezier(406.0075,639.6620) bezier(408.9836,650.5833) bezier(415.5700,665.6674) bezier(415.5700,676.7490)] RestoreState RestoreState diff --git a/testing/resources/embedder_tests/ink_with_color_scheme_agg.png b/testing/resources/embedder_tests/ink_with_color_scheme_agg.png index 570c2ae5575b9946d17641d2c46091b6e3cf0883..aba14f384239d80cdb88f530831f9c4e797fe0f5 100644 GIT binary patch literal 9632 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 literal 13580 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