diff --git a/core/fpdfdoc/cpdf_appearancegenerator_common.cpp b/core/fpdfdoc/cpdf_appearancegenerator_common.cpp index a61f19161..6de4bfad6 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator_common.cpp +++ b/core/fpdfdoc/cpdf_appearancegenerator_common.cpp @@ -66,6 +66,16 @@ std::vector DashValues(const CPDF_Array* dash_array) { return values; } +// A dashed /BS's pattern (ISO 32000 table 168): its /D, or the default [3] +// when it names none. An empty /D is a solid line (8.4.3.6): no pattern. +std::vector BorderStyleDash(const CPDF_Dictionary* border_style_dict) { + RetainPtr dash_array = border_style_dict->GetArrayFor("D"); + if (!dash_array) { + return {kDefaultDash}; + } + return DashValues(dash_array.Get()); +} + // The dash operator for |values|; empty when there are none. ByteString DashOperator(const std::vector& values) { if (values.empty()) { @@ -302,10 +312,12 @@ BorderStyleInfo GetBorderStyleInfo(const CPDF_Dictionary* border_style_dict) { } } - std::vector dash = - DashValues(border_style_dict->GetArrayFor("D").Get()); + // A dashed border with an empty pattern draws a solid line. + std::vector dash = BorderStyleDash(border_style_dict); if (!dash.empty()) { border_style_info.dash_pattern = std::move(dash); + } else if (border_style_info.style == BorderStyle::kDash) { + border_style_info.style = BorderStyle::kSolid; } return border_style_info; @@ -551,20 +563,16 @@ float GetBorderWidth(const CPDF_Dictionary* dict) { return 1.0f; } -// The dash operator a border draws with: a dashed /BS draws its /D, or the -// default dash when it names none; a /Border draws its own dash array. +// The dash operator a border draws with, as ISO 32000 reads it: a dashed /BS +// draws its /D, or the default dash when it names none, and nothing (a solid +// line) for an empty one; a /Border draws its own dash array. ByteString GetDashPatternString(const CPDF_Dictionary* dict) { RetainPtr border_style_dict = dict->GetDictFor("BS"); if (border_style_dict) { if (border_style_dict->GetByteStringFor("S") != "D") { return ByteString(); } - std::vector dash = - DashValues(border_style_dict->GetArrayFor("D").Get()); - if (dash.empty()) { - dash.push_back(kDefaultDash); - } - return DashOperator(dash); + return DashOperator(BorderStyleDash(border_style_dict.Get())); } RetainPtr border_array = diff --git a/fpdfsdk/epdf_annot.cpp b/fpdfsdk/epdf_annot.cpp index 216e22595..0e51d4e16 100644 --- a/fpdfsdk/epdf_annot.cpp +++ b/fpdfsdk/epdf_annot.cpp @@ -1523,6 +1523,13 @@ EPDFAnnot_GetBorderStyle(FPDF_ANNOTATION annot, float* width) { if (nStyle == CPDF_Annot::BorderStyle::kUnknown) { nStyle = CPDF_Annot::BorderStyle::kSolid; } + // A dashed border with an empty pattern draws a solid line (ISO 32000 + // 8.4.3.6), so it reads solid, as a /Border with an empty dash array does. + RetainPtr pDashArray = pBSDict->GetArrayFor("D"); + if (nStyle == CPDF_Annot::BorderStyle::kDashed && pDashArray && + pDashArray->IsEmpty()) { + nStyle = CPDF_Annot::BorderStyle::kSolid; + } return static_cast(nStyle); } diff --git a/fpdfsdk/epdf_annot_embeddertest.cpp b/fpdfsdk/epdf_annot_embeddertest.cpp index 1b1b5309d..f413e3887 100644 --- a/fpdfsdk/epdf_annot_embeddertest.cpp +++ b/fpdfsdk/epdf_annot_embeddertest.cpp @@ -4353,6 +4353,13 @@ TEST_F(EPDFAnnotEmbedderTest, TextFieldDrawsItsBorderStyle) { EXPECT_THAT(look, HasSubstr(L"] 0 d")); EXPECT_THAT(look, Not(HasSubstr(L" re s"))); + // An empty pattern is a solid line (ISO 32000 8.4.3.6). + bs->SetNewFor("D"); + ASSERT_TRUE(EPDFAnnot_GenerateFormFieldAP(annot.get())); + look = GetNormalAppearance(annot.get()); + EXPECT_THAT(look, HasSubstr(L" re s")); + EXPECT_THAT(look, Not(HasSubstr(L"] 0 d"))); + bs->SetNewFor("S", "U"); ASSERT_TRUE(EPDFAnnot_GenerateFormFieldAP(annot.get())); look = GetNormalAppearance(annot.get()); @@ -7601,6 +7608,73 @@ TEST_F(EPDFAnnotEmbedderTest, DashedBorderDrawsItsDashOrTheDefault) { .has_value()); } +// An empty dash array is a solid line (ISO 32000 8.4.3.6), in /BS /D as in +// /Border: the border draws no dash and reads solid. A /D left out is the +// default dash, [3]. +TEST_F(EPDFAnnotEmbedderTest, AnEmptyDashPatternIsASolidLine) { + embedpdf_test::ScopedTestDocument doc(NewTestDocument()); + ASSERT_TRUE(doc); + ScopedFPDFPage page(FPDFPage_New(doc.get(), 0, 200, 200)); + ASSERT_TRUE(page); + + for (int subtype : {FPDF_ANNOT_SQUARE, FPDF_ANNOT_CIRCLE, FPDF_ANNOT_LINE, + FPDF_ANNOT_INK}) { + SCOPED_TRACE(subtype); + ScopedFPDFAnnotation annot(FPDFPage_CreateAnnot(page.get(), subtype)); + ASSERT_TRUE(annot); + const FS_RECTF rect{/*left=*/10.0f, /*top=*/90.0f, /*right=*/90.0f, + /*bottom=*/10.0f}; + ASSERT_TRUE(FPDFAnnot_SetRect(annot.get(), &rect)); + ASSERT_TRUE( + EPDFAnnot_SetColor(annot.get(), FPDFANNOT_COLORTYPE_Color, 0, 0, 0)); + if (subtype == FPDF_ANNOT_LINE) { + const FS_POINTF start{20, 20}; + const FS_POINTF end{80, 80}; + ASSERT_TRUE(EPDFAnnot_SetLine(annot.get(), &start, &end)); + } else if (subtype == FPDF_ANNOT_INK) { + static constexpr FS_POINTF kStroke[] = {{20, 20}, {50, 80}, {80, 30}}; + ASSERT_EQ( + 0, FPDFAnnot_AddInkStroke(annot.get(), kStroke, std::size(kStroke))); + } + ASSERT_TRUE(EPDFAnnot_SetBorderStyle(annot.get(), FPDF_ANNOT_BS_DASHED, 2)); + RetainPtr annot_dict = + CPDFAnnotContextFromFPDFAnnotation(annot.get())->GetMutableAnnotDict(); + RetainPtr bs = annot_dict->GetMutableDictFor("BS"); + ASSERT_TRUE(bs); + + // No /D: the default dash. + EXPECT_EQ(FPDF_ANNOT_BS_DASHED, + EPDFAnnot_GetBorderStyle(annot.get(), nullptr)); + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); + EXPECT_TRUE(GetNormalAppearanceStreamBytes(annot.get()) + .Find("[3 ] 0 d") + .has_value()); + + // An empty /D: solid. + bs->SetNewFor("D"); + EXPECT_EQ(FPDF_ANNOT_BS_SOLID, + EPDFAnnot_GetBorderStyle(annot.get(), nullptr)); + EXPECT_EQ(0u, EPDFAnnot_GetBorderDashPatternCount(annot.get())); + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); + EXPECT_FALSE( + GetNormalAppearanceStreamBytes(annot.get()).Find("] 0 d").has_value()); + + // A /Border whose dash array is empty, without /BS: solid too. + annot_dict->RemoveFor("BS"); + RetainPtr border = + annot_dict->SetNewFor(pdfium::annotation::kBorder); + border->AppendNew(0); + border->AppendNew(0); + border->AppendNew(2); + border->AppendNew(); + EXPECT_EQ(FPDF_ANNOT_BS_SOLID, + EPDFAnnot_GetBorderStyle(annot.get(), nullptr)); + ASSERT_TRUE(EPDFAnnot_GenerateAppearance(annot.get())); + EXPECT_FALSE( + GetNormalAppearanceStreamBytes(annot.get()).Find("] 0 d").has_value()); + } +} + TEST_F(EPDFAnnotEmbedderTest, SetNameAcceptsAnyNameAndKeepsAppearance) { embedpdf_test::ScopedTestDocument doc(NewTestDocument()); ASSERT_TRUE(doc); diff --git a/public/epdf_annot.h b/public/epdf_annot.h index c254904e3..8ffdc98ba 100644 --- a/public/epdf_annot.h +++ b/public/epdf_annot.h @@ -306,7 +306,9 @@ 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. +// the modern /BS dictionary and the legacy /Border array. A border is dashed +// when it draws dashes: a dashed /BS whose /D is an empty array, like a +// /Border whose dash array is empty, draws a solid line and reads solid. // // annot - handle to an annotation. // width - receives the border width; can be NULL. diff --git a/testing/embedpdf_layer_mode_writers.txt b/testing/embedpdf_layer_mode_writers.txt index b815409fa..fee34750e 100644 --- a/testing/embedpdf_layer_mode_writers.txt +++ b/testing/embedpdf_layer_mode_writers.txt @@ -49,6 +49,7 @@ EPDFActionEmbedderTest.NodeFilePathAndNamePayloadsFromSyntheticDicts 2 EPDFActionEmbedderTest.OpenActionDestinationForms 3 EPDFActionEmbedderTest.ResetFormThreeStates 1 EPDFActionEmbedderTest.SetNextChainsActions 3 +EPDFAnnotEmbedderTest.AnEmptyDashPatternIsASolidLine 15 EPDFAnnotEmbedderTest.AnnotFontMapStagesEveryNamedFont 16 EPDFAnnotEmbedderTest.ApplyPageRedactionsCountsRemovedAnnotations 6 EPDFAnnotEmbedderTest.ApplyPageRedactionsTwoMarksOnOneTextObject 3