diff --git a/core/fpdfdoc/cpdf_appearancegenerator_common.cpp b/core/fpdfdoc/cpdf_appearancegenerator_common.cpp index 05dfbb544..a61f19161 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator_common.cpp +++ b/core/fpdfdoc/cpdf_appearancegenerator_common.cpp @@ -519,6 +519,22 @@ ByteString GetColorStringWithDefault(const CPDF_Array* color_array, return GenerateColorAP(default_color, operation); } +// The stroke colour operator for /C, or none when /C names no colour: left +// out, or the empty array ISO 32000 defines as "no colour; transparent". +// Acrobat draws neither with a stroke, so the shape keeps only its fill. +std::optional GetStrokeColorString(const CPDF_Dictionary* dict) { + RetainPtr color_array = + dict->GetArrayFor(pdfium::annotation::kC); + if (!color_array) { + return std::nullopt; + } + const CFX_Color color = fpdfdoc::CFXColorFromArray(*color_array); + if (color.nColorType == CFX_Color::Type::kTransparent) { + return std::nullopt; + } + return GenerateColorAP(color, PaintOperation::kStroke); +} + float GetBorderWidth(const CPDF_Dictionary* dict) { RetainPtr border_style_dict = dict->GetDictFor("BS"); if (border_style_dict) { diff --git a/core/fpdfdoc/cpdf_appearancegenerator_internal.h b/core/fpdfdoc/cpdf_appearancegenerator_internal.h index 70f4e0cbf..602ee8a49 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator_internal.h +++ b/core/fpdfdoc/cpdf_appearancegenerator_internal.h @@ -157,6 +157,7 @@ ByteString GenerateBorderAP(const CFX_FloatRect& rect, ByteString GetColorStringWithDefault(const CPDF_Array* color_array, const CFX_Color& default_color, PaintOperation operation); +std::optional GetStrokeColorString(const CPDF_Dictionary* dict); float GetBorderWidth(const CPDF_Dictionary* dict); ByteString GetDashPatternString(const CPDF_Dictionary* dict); ByteString GetPaintOperatorString(bool is_stroke_rect, bool is_fill_rect); @@ -260,13 +261,25 @@ bool GenerateStrikeOutAP(APGenerationTarget* target, // cpdf_appearancegenerator_shapes.cpp ByteString ReadLineEndingToken(const CPDF_Array* le, size_t idx); +// `stroked` false fills a closed ending without outlining it: a line with no +// stroke colour keeps only its filled endings. void EmitArrowPath(fxcrt::ostringstream& out, float stroke_w, ArrowStyle style, - bool do_fill); -void EmitCirclePath(fxcrt::ostringstream& out, float stroke_w, bool filled); -void EmitSquarePath(fxcrt::ostringstream& out, float stroke_w, bool filled); -void EmitDiamondPath(fxcrt::ostringstream& out, float stroke_w, bool filled); + bool do_fill, + bool stroked = true); +void EmitCirclePath(fxcrt::ostringstream& out, + float stroke_w, + bool filled, + bool stroked = true); +void EmitSquarePath(fxcrt::ostringstream& out, + float stroke_w, + bool filled, + bool stroked = true); +void EmitDiamondPath(fxcrt::ostringstream& out, + float stroke_w, + bool filled, + bool stroked = true); void EmitButtOrSlashPath(fxcrt::ostringstream& out, float stroke_w, float len_factor); diff --git a/core/fpdfdoc/cpdf_appearancegenerator_shapes.cpp b/core/fpdfdoc/cpdf_appearancegenerator_shapes.cpp index 25c21dbb2..71d100eaa 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator_shapes.cpp +++ b/core/fpdfdoc/cpdf_appearancegenerator_shapes.cpp @@ -55,6 +55,7 @@ using appearance_generator::GetColorStringWithDefault; using appearance_generator::GetDashPatternString; using appearance_generator::GetPaintOperatorString; using appearance_generator::GetShapeRotationInfo; +using appearance_generator::GetStrokeColorString; using appearance_generator::IsFinitePoint; using appearance_generator::kButtLenFactor; using appearance_generator::kGSDictName; @@ -71,6 +72,14 @@ constexpr float kArrowAngle = FXSYS_PI / 6.0f; constexpr float kArrowLenFactor = 9.0f; +// The operator that paints a closed circle, square or diamond ending. +const char* ClosedEndingOperator(bool filled, bool stroked) { + if (!stroked) { + return "f\n"; + } + return filled ? "B\n" : "S\n"; +} + struct CloudyBorderInfo { bool is_cloudy = false; float intensity = 0; @@ -116,8 +125,11 @@ void GenerateLineEndings(fxcrt::ostringstream& ap, return; } - // Get styles needed for drawing the endings + // Get styles needed for drawing the endings. A hairline (/W 0) still gets + // the endings a 1 pt line has, as Acrobat draws them. const float border_w = GetBorderWidth(annot_dict); + const float ending_w = border_w > 0 ? border_w : 1.0f; + const bool has_stroke = GetStrokeColorString(annot_dict).has_value(); RetainPtr interior_color = annot_dict->GetArrayFor("IC"); const bool has_fill = interior_color && !interior_color->IsEmpty(); @@ -128,6 +140,15 @@ void GenerateLineEndings(fxcrt::ostringstream& ap, ending == CPDF_Annot::LineEnding::kUnknown) { return; } + // Without a stroke colour only a filled closed ending paints. + const bool fillable = ending == CPDF_Annot::LineEnding::kClosedArrow || + ending == CPDF_Annot::LineEnding::kRClosedArrow || + ending == CPDF_Annot::LineEnding::kCircle || + ending == CPDF_Annot::LineEnding::kSquare || + ending == CPDF_Annot::LineEnding::kDiamond; + if (!has_stroke && !(fillable && has_fill)) { + return; + } const float line_angle = atan2(unit_dir.y, unit_dir.x); float final_angle = line_angle; @@ -158,26 +179,27 @@ void GenerateLineEndings(fxcrt::ostringstream& ap, switch (ending) { case CPDF_Annot::LineEnding::kClosedArrow: case CPDF_Annot::LineEnding::kRClosedArrow: - EmitArrowPath(ap, border_w, ArrowStyle::kClosed, has_fill); + EmitArrowPath(ap, ending_w, ArrowStyle::kClosed, has_fill, + has_stroke); break; case CPDF_Annot::LineEnding::kOpenArrow: case CPDF_Annot::LineEnding::kROpenArrow: - EmitArrowPath(ap, border_w, ArrowStyle::kOpen, /*do_fill=*/false); + EmitArrowPath(ap, ending_w, ArrowStyle::kOpen, /*do_fill=*/false); break; case CPDF_Annot::LineEnding::kCircle: - EmitCirclePath(ap, border_w, has_fill); + EmitCirclePath(ap, ending_w, has_fill, has_stroke); break; case CPDF_Annot::LineEnding::kSquare: - EmitSquarePath(ap, border_w, has_fill); + EmitSquarePath(ap, ending_w, has_fill, has_stroke); break; case CPDF_Annot::LineEnding::kDiamond: - EmitDiamondPath(ap, border_w, has_fill); + EmitDiamondPath(ap, ending_w, has_fill, has_stroke); break; case CPDF_Annot::LineEnding::kButt: - EmitButtOrSlashPath(ap, border_w, kButtLenFactor); + EmitButtOrSlashPath(ap, ending_w, kButtLenFactor); break; case CPDF_Annot::LineEnding::kSlash: - EmitButtOrSlashPath(ap, border_w, kSlashLenFactor); + EmitButtOrSlashPath(ap, ending_w, kSlashLenFactor); break; default: break; @@ -361,10 +383,11 @@ bool GenerateDimensionLineAP(APGenerationTarget* target, } fxcrt::ostringstream ap; ap << "/" << kGSDictName << " gs\n"; - auto color = annot->GetArrayFor("C"); + const std::optional stroke_color = GetStrokeColorString(annot); auto interior = annot->GetArrayFor("IC"); - ap << GetColorStringWithDefault(color.Get(), CFX_Color::MakeRGB(0, 0, 0), - PaintOperation::kStroke); + if (stroke_color) { + ap << *stroke_color; + } if (interior && !interior->IsEmpty()) { ap << GetColorStringWithDefault(interior.Get(), {}, PaintOperation::kFill); } @@ -372,7 +395,8 @@ bool GenerateDimensionLineAP(APGenerationTarget* target, WritePoint(ap, from) << " m "; WritePoint(ap, to) << " l S\n"; }; - if (border > 0) { + // Without a stroke colour the lines go and the filled endings stay. + if (border > 0 && stroke_color) { WriteFloat(ap, border) << " w " << GetDashPatternString(annot); if (label.outside_arrows) { const float stub = 20 * border; @@ -394,6 +418,8 @@ bool GenerateDimensionLineAP(APGenerationTarget* target, for (const auto& connector : label.connector) { segment(connector.from, connector.to); } + } + if (border > 0) { GenerateLineEndings(ap, {line.start, line.end}, annot, label.outside_arrows); } @@ -435,7 +461,8 @@ ByteString ReadLineEndingToken(const CPDF_Array* le, size_t idx) { void EmitArrowPath(fxcrt::ostringstream& out, float stroke_w, ArrowStyle style, - bool do_fill) { + bool do_fill, + bool stroked) { const float len = kArrowLenFactor * stroke_w; const float a = kArrowAngle; // 30° const float x = -len * std::cos(a); @@ -449,11 +476,15 @@ void EmitArrowPath(fxcrt::ostringstream& out, // ClosedArrow / RClosedArrow out << "0 0 m " << x << " " << y << " l " << x << " " << -y << " l " - << (do_fill ? "b\n" // fill + stroke - : "h S\n"); // just close and stroke (no fill) + << (!stroked ? "f\n" // fill only + : do_fill ? "b\n" // fill + stroke + : "h S\n"); // just close and stroke (no fill) } -void EmitCirclePath(fxcrt::ostringstream& out, float stroke_w, bool filled) { +void EmitCirclePath(fxcrt::ostringstream& out, + float stroke_w, + bool filled, + bool stroked) { const float r = (stroke_w * 5.0f) / 2.0f; // This is a standard approximation for drawing a circle with Bezier curves. constexpr float kL = 0.5523f; @@ -463,19 +494,27 @@ void EmitCirclePath(fxcrt::ostringstream& out, float stroke_w, bool filled) { << " c " << -d << " " << r << " " << -r << " " << d << " " << -r << " 0 c " << -r << " " << -d << " " << -d << " " << -r << " 0 " << -r << " c " << d << " " << -r << " " << r << " " << -d << " " << r << " 0 c " - << (filled ? "B\n" : "S\n"); + << ClosedEndingOperator(filled, stroked); } -void EmitSquarePath(fxcrt::ostringstream& out, float stroke_w, bool filled) { +void EmitSquarePath(fxcrt::ostringstream& out, + float stroke_w, + bool filled, + bool stroked) { const float h = (stroke_w * 6.0f) / 2.0f; out << -h << " " << -h << " m " << h << " " << -h << " l " << h << " " << h - << " l " << -h << " " << h << " l h " << (filled ? "B\n" : "S\n"); + << " l " << -h << " " << h << " l h " + << ClosedEndingOperator(filled, stroked); } -void EmitDiamondPath(fxcrt::ostringstream& out, float stroke_w, bool filled) { +void EmitDiamondPath(fxcrt::ostringstream& out, + float stroke_w, + bool filled, + bool stroked) { const float h = (stroke_w * 6.0f) / 2.0f; out << "0 " << -h << " m " << h << " 0 l " - << "0 " << h << " l " << -h << " 0 l h " << (filled ? "B\n" : "S\n"); + << "0 " << h << " l " << -h << " 0 l h " + << ClosedEndingOperator(filled, stroked); } void EmitButtOrSlashPath(fxcrt::ostringstream& out, @@ -496,12 +535,14 @@ bool GenerateCircleAP(APGenerationTarget* target, CFX_Color::MakeTransparent(), PaintOperation::kFill); - app_stream << GetColorStringWithDefault( - annot_dict->GetArrayFor(pdfium::annotation::kC).Get(), - CFX_Color::MakeRGB(0, 0, 0), PaintOperation::kStroke); + const std::optional stroke_color = + GetStrokeColorString(annot_dict); + if (stroke_color) { + app_stream << *stroke_color; + } float border_width = GetBorderWidth(annot_dict); - bool is_stroke_rect = border_width > 0; + bool is_stroke_rect = stroke_color && border_width > 0; CloudyBorderInfo cloudy_info = GetCloudyBorderInfo(annot_dict); if (is_stroke_rect) { @@ -521,7 +562,9 @@ bool GenerateCircleAP(APGenerationTarget* target, GenerateCloudyEllipsePath(app_stream, draw_rect, cloudy_info.intensity, border_width); } else { - if (is_stroke_rect) { + // The outline's middle sits half its width inside /Rect, painted or not: + // a fill without its stroke keeps the stroke's place, as Acrobat draws it. + if (border_width > 0) { draw_rect.Deflate(border_width / 2, border_width / 2); } @@ -576,12 +619,14 @@ bool GeneratePolygonAP(APGenerationTarget* target, CFX_Color::MakeTransparent(), PaintOperation::kFill); - app << GetColorStringWithDefault( - annot_dict->GetArrayFor(pdfium::annotation::kC).Get(), - CFX_Color::MakeRGB(0, 0, 0), PaintOperation::kStroke); + const std::optional stroke_color = + GetStrokeColorString(annot_dict); + if (stroke_color) { + app << *stroke_color; + } const float border_w = GetBorderWidth(annot_dict); - const bool do_stroke = border_w > 0; + const bool do_stroke = stroke_color && border_w > 0; CloudyBorderInfo cloudy_info = GetCloudyBorderInfo(annot_dict); if (do_stroke) { @@ -661,19 +706,18 @@ bool GenerateLineAP(APGenerationTarget* target, ap << GetColorStringWithDefault(interior_color.Get(), {}, PaintOperation::kFill); } - ap << GetColorStringWithDefault( - annot_dict->GetArrayFor(pdfium::annotation::kC).Get(), - CFX_Color::MakeRGB(0, 0, 0), PaintOperation::kStroke); - const float border_w = GetBorderWidth(annot_dict); - if (border_w > 0) { - ap << border_w << " w " << GetDashPatternString(annot_dict); + // Draw the main line segment. Width 0 is the thinnest line the device + // draws (ISO 32000 8.4.3.2), the hairline Acrobat gives a line at /W 0. + const std::optional stroke_color = + GetStrokeColorString(annot_dict); + if (stroke_color) { + ap << *stroke_color << std::max(GetBorderWidth(annot_dict), 0.0f) << " w " + << GetDashPatternString(annot_dict); + ap << points[0].x << " " << points[0].y << " m " << points[1].x << " " + << points[1].y << " l S\n"; } - // Draw the main line segment. - ap << points[0].x << " " << points[0].y << " m " << points[1].x << " " - << points[1].y << " l S\n"; - // Draw the endings. GenerateLineEndings(ap, points, annot_dict); @@ -708,21 +752,19 @@ bool GeneratePolyLineAP(APGenerationTarget* target, ap << GetColorStringWithDefault(interior_color.Get(), {}, PaintOperation::kFill); } - ap << GetColorStringWithDefault( - annot_dict->GetArrayFor(pdfium::annotation::kC).Get(), - CFX_Color::MakeRGB(0, 0, 0), PaintOperation::kStroke); - const float border_w = GetBorderWidth(annot_dict); - if (border_w > 0) { - ap << border_w << " w " << GetDashPatternString(annot_dict); - } - - // Draw the main polyline path. - ap << points[0].x << " " << points[0].y << " m "; - for (size_t i = 1; i < points.size(); ++i) { - ap << points[i].x << " " << points[i].y << " l "; + // Draw the main polyline path, a hairline at /W 0 as a line's is. + const std::optional stroke_color = + GetStrokeColorString(annot_dict); + if (stroke_color) { + ap << *stroke_color << std::max(GetBorderWidth(annot_dict), 0.0f) << " w " + << GetDashPatternString(annot_dict); + ap << points[0].x << " " << points[0].y << " m "; + for (size_t i = 1; i < points.size(); ++i) { + ap << points[i].x << " " << points[i].y << " l "; + } + ap << "S\n"; } - ap << "S\n"; // Draw the endings. GenerateLineEndings(ap, points, annot_dict); @@ -753,24 +795,23 @@ bool GenerateInkAP(APGenerationTarget* target, return false; } - float border_width = GetBorderWidth(annot_dict); - const bool is_stroke = border_width > 0; - if (!is_stroke) { - return false; - } + // Ink is all stroke: with no stroke colour it paints nothing but keeps its + // points. Width 0 is a hairline, as Acrobat draws it. + const std::optional stroke_color = + GetStrokeColorString(annot_dict); fxcrt::ostringstream app_stream; app_stream << "/" << kGSDictName << " gs "; - app_stream << GetColorStringWithDefault( - annot_dict->GetArrayFor(pdfium::annotation::kC).Get(), - CFX_Color::MakeRGB(0, 0, 0), PaintOperation::kStroke); + if (stroke_color) { + app_stream << *stroke_color; - app_stream << border_width << " w "; + 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) + app_stream << "1 J "; // Rounded Line Caps + app_stream << "1 j "; // Rounded Line Joins (for the corners in the middle) - app_stream << GetDashPatternString(annot_dict); + app_stream << GetDashPatternString(annot_dict); + } std::vector points; for (size_t i = 0; i < ink_list->size(); i++) { @@ -780,20 +821,21 @@ bool GenerateInkAP(APGenerationTarget* target, continue; } - const float x0 = coordinates_array->GetFloatAt(0); - const float y0 = coordinates_array->GetFloatAt(1); - app_stream << x0 << " " << y0 << " m "; - points.push_back({x0, y0}); - - // Start loop at the second point (index 2) --- - // The 'm' command already moves to the first point. + 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) { - const float x = coordinates_array->GetFloatAt(j); - const float y = coordinates_array->GetFloatAt(j + 1); - app_stream << x << " " << y << " l "; - points.push_back({x, y}); + 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 "; } - app_stream << "S\n"; } @@ -820,12 +862,14 @@ bool GenerateSquareAP(APGenerationTarget* target, CFX_Color::MakeTransparent(), PaintOperation::kFill); - app_stream << GetColorStringWithDefault( - annot_dict->GetArrayFor(pdfium::annotation::kC).Get(), - CFX_Color::MakeRGB(0, 0, 0), PaintOperation::kStroke); + const std::optional stroke_color = + GetStrokeColorString(annot_dict); + if (stroke_color) { + app_stream << *stroke_color; + } float border_width = GetBorderWidth(annot_dict); - const bool is_stroke_rect = border_width > 0; + const bool is_stroke_rect = stroke_color && border_width > 0; CloudyBorderInfo cloudy_info = GetCloudyBorderInfo(annot_dict); if (is_stroke_rect) { @@ -845,7 +889,9 @@ bool GenerateSquareAP(APGenerationTarget* target, GenerateCloudyRectanglePath(app_stream, draw_rect, cloudy_info.intensity, border_width); } else { - if (is_stroke_rect) { + // The outline's middle sits half its width inside /Rect, painted or not: + // a fill without its stroke keeps the stroke's place, as Acrobat draws it. + if (border_width > 0) { draw_rect.Deflate(border_width / 2, border_width / 2); } app_stream << draw_rect.left << " " << draw_rect.bottom << " " diff --git a/fpdfsdk/epdf_annot.cpp b/fpdfsdk/epdf_annot.cpp index cbbe66308..f2305e3f5 100644 --- a/fpdfsdk/epdf_annot.cpp +++ b/fpdfsdk/epdf_annot.cpp @@ -1208,6 +1208,25 @@ EPDFAnnot_ClearColor(FPDF_ANNOTATION annot, FPDFANNOT_COLORTYPE type) { return true; } +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetNoColor(FPDF_ANNOTATION annot, FPDFANNOT_COLORTYPE type) { + ScopedFPDFAnnotationView annot_view(annot); + RetainPtr dict = + GetMutableAnnotDictFromFPDFAnnotation(annot); + if (!dict) { + return false; + } + + // OverlayColor (OC) is only valid for Redact annotations. + if (type == FPDFANNOT_COLORTYPE_OverlayColor && + FPDFAnnot_GetSubtype(annot) != FPDF_ANNOT_REDACT) { + return false; + } + + dict->SetNewFor(GetColorKeyForType(type)); + return true; +} + FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_SetOpacity(FPDF_ANNOTATION annot, float opacity) { ScopedFPDFAnnotationView annot_view(annot); diff --git a/fpdfsdk/epdf_annot_embeddertest.cpp b/fpdfsdk/epdf_annot_embeddertest.cpp index 8a345188e..27bbfffa1 100644 --- a/fpdfsdk/epdf_annot_embeddertest.cpp +++ b/fpdfsdk/epdf_annot_embeddertest.cpp @@ -849,6 +849,202 @@ TEST_F(EPDFAnnotEmbedderTest, GenerateInkAppearanceSetsRectToWhatItPaints) { EXPECT_FLOAT_EQ(49.0f, thinner.bottom); } +TEST_F(EPDFAnnotEmbedderTest, SetNoColorWritesAnEmptyColorArray) { + embedpdf_test::ScopedTestDocument doc(NewTestDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 100, 100)); + ASSERT_TRUE(page); + ScopedFPDFAnnotation annot( + FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_SQUARE)); + ASSERT_TRUE(annot); + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot.get()); + ASSERT_TRUE(context); + + ASSERT_TRUE(FPDFAnnot_SetColor(annot.get(), FPDFANNOT_COLORTYPE_Color, 255, 0, + 0, 255)); + ASSERT_TRUE(EPDFAnnot_SetNoColor(annot.get(), FPDFANNOT_COLORTYPE_Color)); + RetainPtr color = context->GetAnnotDict()->GetArrayFor("C"); + ASSERT_TRUE(color); + EXPECT_TRUE(color->IsEmpty()); + + // No colour reads as none, like a /C left out. + unsigned int r = 0; + unsigned int g = 0; + unsigned int b = 0; + EXPECT_FALSE( + EPDFAnnot_GetColor(annot.get(), FPDFANNOT_COLORTYPE_Color, &r, &g, &b)); + + // An overlay colour belongs to a redaction alone. + EXPECT_FALSE( + EPDFAnnot_SetNoColor(annot.get(), FPDFANNOT_COLORTYPE_OverlayColor)); +} + +// Acrobat draws no stroke for a /C left out or empty (ISO 32000: "no colour; +// transparent"). The fill stays where the stroke's middle would be. +TEST_F(EPDFAnnotEmbedderTest, NoStrokeColorKeepsTheFillInTheStrokesPlace) { + embedpdf_test::ScopedTestDocument doc(NewTestDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 200, 200)); + ASSERT_TRUE(page); + + auto make = [&](FPDF_ANNOTATION_SUBTYPE subtype) { + ScopedFPDFAnnotation annot(FPDFPage_CreateAnnot(page.get(), subtype)); + const FS_RECTF rect{/*left=*/10, /*top=*/60, /*right=*/60, /*bottom=*/10}; + EXPECT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + EXPECT_TRUE(FPDFAnnot_SetColor( + annot.get(), FPDFANNOT_COLORTYPE_InteriorColor, 0, 0, 255, 255)); + EXPECT_TRUE(FPDFAnnot_SetBorder(annot.get(), 0, 0, /*border_width=*/4)); + return annot; + }; + + ScopedFPDFAnnotation square = make(FPDF_ANNOT_SQUARE); + ScopedFPDFAnnotation circle = make(FPDF_ANNOT_CIRCLE); + ScopedFPDFAnnotation polygon = make(FPDF_ANNOT_POLYGON); + static constexpr FS_POINTF kVertices[] = {{10, 10}, {60, 10}, {35, 60}}; + ASSERT_TRUE( + EPDFAnnot_SetVertices(polygon.get(), kVertices, std::size(kVertices))); + + for (FPDF_ANNOTATION annot : {square.get(), circle.get(), polygon.get()}) { + // Left out, then empty. + for (bool empty : {false, true}) { + if (empty) { + ASSERT_TRUE(EPDFAnnot_SetNoColor(annot, FPDFANNOT_COLORTYPE_Color)); + } + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot)); + const std::wstring look = GetNormalAppearance(annot); + EXPECT_THAT(look, Not(HasSubstr(L"RG"))) << "empty: " << empty; + EXPECT_THAT(look, Not(HasSubstr(L" w "))) << "empty: " << empty; + EXPECT_THAT(look, HasSubstr(L"f\n")) << "empty: " << empty; + } + } + + // The square's fill is the box inset by half the 4 pt width. + EXPECT_THAT(GetNormalAppearance(square.get()), + HasSubstr(L"12 12 46 46 re f\n")); + + // A stroke colour brings the outline back, over the same fill. + ASSERT_TRUE( + EPDFAnnot_SetColor(square.get(), FPDFANNOT_COLORTYPE_Color, 255, 0, 0)); + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(square.get())); + const std::wstring outlined = GetNormalAppearance(square.get()); + EXPECT_THAT(outlined, HasSubstr(L"1 0 0 RG")); + EXPECT_THAT(outlined, HasSubstr(L"12 12 46 46 re b\n")); + + // At width 0 the outline goes and the fill takes the whole box. + ASSERT_TRUE(FPDFAnnot_SetBorder(square.get(), 0, 0, 0)); + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(square.get())); + EXPECT_THAT(GetNormalAppearance(square.get()), + HasSubstr(L"10 10 50 50 re f\n")); +} + +// A line with no stroke colour keeps only its filled endings, sized by its +// width; with no fill either, it paints nothing. +TEST_F(EPDFAnnotEmbedderTest, NoStrokeColorLineKeepsItsFilledEndings) { + embedpdf_test::ScopedTestDocument doc(NewTestDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 200, 200)); + ASSERT_TRUE(page); + ScopedFPDFAnnotation line(FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_LINE)); + ASSERT_TRUE(line); + const FS_POINTF start{20, 20}; + const FS_POINTF end{150, 120}; + ASSERT_TRUE(EPDFAnnot_SetLine(line.get(), &start, &end)); + ASSERT_TRUE(EPDFAnnot_SetLineEndings(line.get(), FPDF_ANNOT_LE_OpenArrow, + FPDF_ANNOT_LE_ClosedArrow)); + ASSERT_TRUE(FPDFAnnot_SetBorder(line.get(), 0, 0, 2)); + ASSERT_TRUE(FPDFAnnot_SetColor(line.get(), FPDFANNOT_COLORTYPE_InteriorColor, + 0, 0, 255, 255)); + ASSERT_TRUE(EPDFAnnot_SetNoColor(line.get(), FPDFANNOT_COLORTYPE_Color)); + + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(line.get())); + std::wstring look = GetNormalAppearance(line.get()); + // No segment, no open arrow: nothing is stroked. + EXPECT_THAT(look, Not(HasSubstr(L"S\n"))); + EXPECT_THAT(look, Not(HasSubstr(L"RG"))); + // The closed arrow is filled, 9 times the 2 pt width long. + EXPECT_THAT(look, HasSubstr(L" l f\n")); + EXPECT_THAT(look, HasSubstr(L"-15.5885 9 l -15.5885 -9 l f\n")); + + CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(line.get()); + ASSERT_TRUE(context); + context->GetMutableAnnotDict()->RemoveFor("IC"); + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(line.get())); + look = GetNormalAppearance(line.get()); + EXPECT_THAT(look, Not(HasSubstr(L" f\n"))); + EXPECT_THAT(look, Not(HasSubstr(L"S\n"))); +} + +// Width 0 is the thinnest line the device draws (ISO 32000 8.4.3.2): Acrobat +// gives a line, a polyline and ink a hairline there, with endings the size a +// 1 pt line has. +TEST_F(EPDFAnnotEmbedderTest, WidthZeroDrawsAHairline) { + embedpdf_test::ScopedTestDocument doc(NewTestDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 200, 200)); + ASSERT_TRUE(page); + + ScopedFPDFAnnotation line(FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_LINE)); + ASSERT_TRUE(line); + const FS_POINTF start{20, 20}; + const FS_POINTF end{150, 120}; + ASSERT_TRUE(EPDFAnnot_SetLine(line.get(), &start, &end)); + ASSERT_TRUE(EPDFAnnot_SetLineEndings(line.get(), FPDF_ANNOT_LE_None, + FPDF_ANNOT_LE_OpenArrow)); + + ScopedFPDFAnnotation polyline( + FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_POLYLINE)); + ASSERT_TRUE(polyline); + static constexpr FS_POINTF kVertices[] = {{20, 20}, {80, 120}, {150, 40}}; + ASSERT_TRUE( + EPDFAnnot_SetVertices(polyline.get(), kVertices, std::size(kVertices))); + + ScopedFPDFAnnotation ink(FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_INK)); + ASSERT_TRUE(ink); + ASSERT_EQ(0, + FPDFAnnot_AddInkStroke(ink.get(), kVertices, std::size(kVertices))); + + for (FPDF_ANNOTATION annot : {line.get(), polyline.get(), ink.get()}) { + ASSERT_TRUE( + FPDFAnnot_SetColor(annot, FPDFANNOT_COLORTYPE_Color, 255, 0, 0, 255)); + ASSERT_TRUE(FPDFAnnot_SetBorder(annot, 0, 0, 0)); + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot)); + 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")); + } + // 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")); +} + +// Ink is all stroke: with no stroke colour it paints nothing, and its /Rect +// stays on its points. +TEST_F(EPDFAnnotEmbedderTest, NoStrokeColorInkPaintsNothing) { + embedpdf_test::ScopedTestDocument doc(NewTestDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 200, 200)); + ASSERT_TRUE(page); + ScopedFPDFAnnotation ink(FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_INK)); + ASSERT_TRUE(ink); + static constexpr FS_POINTF kStroke[] = { + {50.0f, 50.0f}, {80.0f, 90.0f}, {120.0f, 60.0f}}; + ASSERT_EQ(0, FPDFAnnot_AddInkStroke(ink.get(), kStroke, std::size(kStroke))); + ASSERT_TRUE(FPDFAnnot_SetBorder(ink.get(), 0, 0, 6)); + ASSERT_TRUE(EPDFAnnot_SetNoColor(ink.get(), FPDFANNOT_COLORTYPE_Color)); + + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(ink.get())); + const std::wstring look = GetNormalAppearance(ink.get()); + EXPECT_THAT(look, Not(HasSubstr(L"S\n"))); + EXPECT_THAT(look, Not(HasSubstr(L" m "))); + FS_RECTF rect; + ASSERT_TRUE(FPDFAnnot_GetRect(ink.get(), &rect)); + EXPECT_FLOAT_EQ(50.0f, rect.left); + EXPECT_FLOAT_EQ(90.0f, rect.top); + EXPECT_FLOAT_EQ(120.0f, rect.right); + EXPECT_FLOAT_EQ(50.0f, rect.bottom); +} + TEST_F(EPDFAnnotEmbedderTest, LineRectHoldsItsArrowhead) { embedpdf_test::ScopedTestDocument doc(NewTestDocument()); ASSERT_TRUE(doc); @@ -862,6 +1058,8 @@ TEST_F(EPDFAnnotEmbedderTest, LineRectHoldsItsArrowhead) { ASSERT_TRUE(EPDFAnnot_SetLineEndings(annot.get(), FPDF_ANNOT_LE_None, FPDF_ANNOT_LE_ClosedArrow)); ASSERT_TRUE(FPDFAnnot_SetBorder(annot.get(), 0.0f, 0.0f, 2.0f)); + ASSERT_TRUE( + FPDFAnnot_SetColor(annot.get(), FPDFANNOT_COLORTYPE_Color, 0, 0, 0, 255)); // A /Rect at the two points would clip the arrowhead and the line's width. const FS_RECTF points{/*left=*/100, /*top=*/500, /*right=*/320, /*bottom=*/400}; @@ -893,6 +1091,8 @@ TEST_F(EPDFAnnotEmbedderTest, LineRectIsWhatItsStrokePaints) { const FS_POINTF level_end{300, 400}; ASSERT_TRUE(EPDFAnnot_SetLine(level.get(), &level_start, &level_end)); ASSERT_TRUE(FPDFAnnot_SetBorder(level.get(), 0.0f, 0.0f, 4.0f)); + ASSERT_TRUE( + EPDFAnnot_SetColor(level.get(), FPDFANNOT_COLORTYPE_Color, 0, 0, 0)); ASSERT_TRUE(EPDFAnnot_GenerateAppearance(level.get())); FS_RECTF rect; ASSERT_TRUE(FPDFAnnot_GetRect(level.get(), &rect)); @@ -910,6 +1110,8 @@ TEST_F(EPDFAnnotEmbedderTest, LineRectIsWhatItsStrokePaints) { const FS_POINTF slanted_end{300, 550}; ASSERT_TRUE(EPDFAnnot_SetLine(slanted.get(), &slanted_start, &slanted_end)); ASSERT_TRUE(FPDFAnnot_SetBorder(slanted.get(), 0.0f, 0.0f, 4.0f)); + ASSERT_TRUE( + EPDFAnnot_SetColor(slanted.get(), FPDFANNOT_COLORTYPE_Color, 0, 0, 0)); ASSERT_TRUE(EPDFAnnot_GenerateAppearance(slanted.get())); ASSERT_TRUE(FPDFAnnot_GetRect(slanted.get(), &rect)); EXPECT_NEAR(98.8f, rect.left, 1e-4); @@ -932,6 +1134,8 @@ TEST_F(EPDFAnnotEmbedderTest, PolylineKneePastTheMiterLimitIsBeveled) { ASSERT_TRUE( EPDFAnnot_SetVertices(annot.get(), kVertices, std::size(kVertices))); ASSERT_TRUE(FPDFAnnot_SetBorder(annot.get(), 0.0f, 0.0f, 4.0f)); + ASSERT_TRUE( + EPDFAnnot_SetColor(annot.get(), FPDFANNOT_COLORTYPE_Color, 0, 0, 0)); ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); FS_RECTF rect; ASSERT_TRUE(FPDFAnnot_GetRect(annot.get(), &rect)); @@ -1042,6 +1246,8 @@ TEST_F(EPDFAnnotEmbedderTest, DrawingRectIsWhereTheAppearanceIsDrawn) { ASSERT_TRUE(EPDFAnnot_SetLineEndings(line.get(), FPDF_ANNOT_LE_None, FPDF_ANNOT_LE_ClosedArrow)); ASSERT_TRUE(FPDFAnnot_SetBorder(line.get(), 0.0f, 0.0f, 2.0f)); + ASSERT_TRUE( + EPDFAnnot_SetColor(line.get(), FPDFANNOT_COLORTYPE_Color, 0, 0, 0)); const FS_RECTF tight{/*left=*/100, /*top=*/400, /*right=*/300, /*bottom=*/400}; ASSERT_TRUE(FPDFAnnot_SetRect(line.get(), &tight)); @@ -1100,6 +1306,8 @@ TEST_F(EPDFAnnotEmbedderTest, CloudyPolygonRectHoldsItsBumps) { EPDFAnnot_SetVertices(annot.get(), kVertices, std::size(kVertices))); ASSERT_TRUE(FPDFAnnot_SetBorder(annot.get(), 0.0f, 0.0f, 2.0f)); ASSERT_TRUE(EPDFAnnot_SetBorderEffect(annot.get(), 1.0f)); + ASSERT_TRUE( + EPDFAnnot_SetColor(annot.get(), FPDFANNOT_COLORTYPE_Color, 0, 0, 0)); const FS_RECTF points{/*left=*/100, /*top=*/250, /*right=*/300, /*bottom=*/100}; ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &points)); @@ -5855,6 +6063,8 @@ TEST_F(EPDFAnnotEmbedderTest, CloudyBumpsReachPastTheBoxIntoRect) { ASSERT_TRUE( EPDFAnnot_SetBorderStyle(annot.get(), FPDF_ANNOT_BS_SOLID, 2.0f)); ASSERT_TRUE(EPDFAnnot_SetBorderEffect(annot.get(), 1.0f)); + ASSERT_TRUE( + EPDFAnnot_SetColor(annot.get(), FPDFANNOT_COLORTYPE_Color, 0, 0, 0)); ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); // /Rect takes in the bumps, and /RD, in rectangle order, leads back to @@ -7142,6 +7352,8 @@ TEST_F(EPDFAnnotEmbedderTest, BorderStyleResolutionUsesISOPrecedence) { ScopedFPDFAnnotation annot( FPDFPage_CreateAnnot(page.get(), FPDF_ANNOT_CIRCLE)); ASSERT_TRUE(annot); + ASSERT_TRUE( + EPDFAnnot_SetColor(annot.get(), FPDFANNOT_COLORTYPE_Color, 0, 0, 0)); CPDF_AnnotContext* context = CPDFAnnotContextFromFPDFAnnotation(annot.get()); ASSERT_TRUE(context); @@ -7236,6 +7448,8 @@ TEST_F(EPDFAnnotEmbedderTest, DashedBorderDrawsItsDashOrTheDefault) { /*bottom=*/10.0f}; EXPECT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); EXPECT_TRUE(EPDFAnnot_SetBorderStyle(annot.get(), style, 2.0f)); + EXPECT_TRUE( + EPDFAnnot_SetColor(annot.get(), FPDFANNOT_COLORTYPE_Color, 0, 0, 0)); if (!dash.empty()) { EXPECT_TRUE(EPDFAnnot_SetBorderDashPattern(annot.get(), dash.data(), dash.size())); diff --git a/fpdfsdk/epdf_c_api_test.c b/fpdfsdk/epdf_c_api_test.c index 9e5a57c14..799c630a8 100644 --- a/fpdfsdk/epdf_c_api_test.c +++ b/fpdfsdk/epdf_c_api_test.c @@ -172,6 +172,7 @@ int CheckEmbedPDFCApi(void) { CHK(EPDFAnnot_SetMKText); CHK(EPDFAnnot_SetMKTextPosition); CHK(EPDFAnnot_SetName); + CHK(EPDFAnnot_SetNoColor); CHK(EPDFAnnot_SetNumberValue); CHK(EPDFAnnot_SetOpacity); CHK(EPDFAnnot_SetOverlayText); diff --git a/fpdfsdk/epdf_measure_embeddertest.cpp b/fpdfsdk/epdf_measure_embeddertest.cpp index 5ceb11cdc..c925dac5f 100644 --- a/fpdfsdk/epdf_measure_embeddertest.cpp +++ b/fpdfsdk/epdf_measure_embeddertest.cpp @@ -80,6 +80,7 @@ void MakeLine(FPDF_ANNOTATION annot) { FS_POINTF a{100, 100}, b{300, 100}; ASSERT_TRUE(FPDFAnnot_SetRect(annot, &rect)); ASSERT_TRUE(EPDFAnnot_SetLine(annot, &a, &b)); + ASSERT_TRUE(EPDFAnnot_SetColor(annot, FPDFANNOT_COLORTYPE_Color, 0, 0, 0)); SetText(annot, "Contents", L"3.45 m"); } struct Delta { diff --git a/public/epdf_annot.h b/public/epdf_annot.h index 26a5e44aa..1e4b87b5e 100644 --- a/public/epdf_annot.h +++ b/public/epdf_annot.h @@ -291,6 +291,19 @@ EPDFAnnot_GetOpacity(FPDF_ANNOTATION annot, float* opacity); FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV EPDFAnnot_ClearColor(FPDF_ANNOTATION annot, FPDFANNOT_COLORTYPE type); +// Experimental EmbedPDF Extension API. +// Write the color of an annotation as an empty array, which ISO 32000 defines +// as no color (transparent). Unlike EPDFAnnot_ClearColor, which removes the +// entry, this states "none" in a way every reader agrees on: a viewer that +// redraws a shape with no /C at all may give it a default color. +// +// annot - handle to an annotation. +// type - type of the color to be set to none. +// +// Returns true if successful. +FPDF_EXPORT FPDF_BOOL FPDF_CALLCONV +EPDFAnnot_SetNoColor(FPDF_ANNOTATION annot, FPDFANNOT_COLORTYPE type); + // Experimental EmbedPDF Extension API. // Get the border style and width of an annotation. This function handles both // the modern /BS dictionary and the legacy /Border array. diff --git a/testing/embedpdf_layer_mode_writers.txt b/testing/embedpdf_layer_mode_writers.txt index 097e782ce..4186cfc22 100644 --- a/testing/embedpdf_layer_mode_writers.txt +++ b/testing/embedpdf_layer_mode_writers.txt @@ -114,6 +114,9 @@ EPDFAnnotEmbedderTest.GenerateTextAppearancePerIcon 12 EPDFAnnotEmbedderTest.LineRectHoldsItsArrowhead 4 EPDFAnnotEmbedderTest.LineRectIsWhatItsStrokePaints 5 EPDFAnnotEmbedderTest.ListBoxKoreanUsesRegisteredDroidFallbackFont 20 +EPDFAnnotEmbedderTest.NoStrokeColorInkPaintsNothing 4 +EPDFAnnotEmbedderTest.NoStrokeColorKeepsTheFillInTheStrokesPlace 11 +EPDFAnnotEmbedderTest.NoStrokeColorLineKeepsItsFilledEndings 5 EPDFAnnotEmbedderTest.NoSubsettingFontEmbedsWholeProgram 12 EPDFAnnotEmbedderTest.OpacityIsStoredAsGiven 3 EPDFAnnotEmbedderTest.PolylineKneePastTheMiterLimitIsBeveled 4 @@ -149,6 +152,7 @@ EPDFAnnotEmbedderTest.RichTextWriterFailureLeavesDocumentUntouched 18 EPDFAnnotEmbedderTest.RotatedCaretAppearanceCarriesTransform 3 EPDFAnnotEmbedderTest.SetDefaultAppearanceKeepsFontAtAutoSize 5 EPDFAnnotEmbedderTest.SetNameAcceptsAnyNameAndKeepsAppearance 4 +EPDFAnnotEmbedderTest.SetNoColorWritesAnEmptyColorArray 3 EPDFAnnotEmbedderTest.SetNumberValueRefusesWithoutWriting 3 EPDFAnnotEmbedderTest.SetNumberValueStoresTheNumberAsGiven 3 EPDFAnnotEmbedderTest.SetRichTextJSONRejectsInvalidInputUnchanged 5 @@ -164,6 +168,7 @@ EPDFAnnotEmbedderTest.TurnedBoxRectIsTheUprightBoxAroundIt 2 EPDFAnnotEmbedderTest.VariableFontIsInstancedAtRegistration 12 EPDFAnnotEmbedderTest.WidgetRegisteredFontInstallsRealDrEntry 25 EPDFAnnotEmbedderTest.WidgetTextAlignment 2 +EPDFAnnotEmbedderTest.WidthZeroDrawsAHairline 6 EPDFAnnotRawEmbedderTest.FindsANameOnAnAnnotationWrittenInPlace 3 EPDFAnnotRawEmbedderTest.FindsAnAnnotationByNameOnItsPage 5 EPDFAnnotRawEmbedderTest.FindsAnAnnotationByObjectNumber 5