diff --git a/core/fpdfdoc/cpdf_appearancegenerator_common.cpp b/core/fpdfdoc/cpdf_appearancegenerator_common.cpp index 6de4bfad6..b78910cd3 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator_common.cpp +++ b/core/fpdfdoc/cpdf_appearancegenerator_common.cpp @@ -323,6 +323,30 @@ BorderStyleInfo GetBorderStyleInfo(const CPDF_Dictionary* border_style_dict) { return border_style_info; } +// An annotation's border as ISO 32000-1 12.5.2 reads it: its /BS, or else +// its /Border array, whose width (default 1) and dash array draw as +// GetBorderWidth and GetDashPatternString take them; an empty dash array +// draws a solid line. +BorderStyleInfo GetAnnotBorderStyleInfo(const CPDF_Dictionary* annot_dict) { + RetainPtr border_style_dict = + annot_dict->GetDictFor("BS"); + if (border_style_dict) { + return GetBorderStyleInfo(border_style_dict.Get()); + } + BorderStyleInfo border_style_info; + border_style_info.width = GetBorderWidth(annot_dict); + RetainPtr border_array = + annot_dict->GetArrayFor(pdfium::annotation::kBorder); + if (border_array && border_array->size() == 4) { + std::vector dash = DashValues(border_array->GetArrayAt(3).Get()); + if (!dash.empty()) { + border_style_info.style = BorderStyle::kDash; + border_style_info.dash_pattern = std::move(dash); + } + } + return border_style_info; +} + AnnotationDimensionsAndColor GetAnnotationDimensionsAndColor( const CPDF_Dictionary* annot_dict) { const AppearanceCharacteristics appearance_characteristics = diff --git a/core/fpdfdoc/cpdf_appearancegenerator_freetext.cpp b/core/fpdfdoc/cpdf_appearancegenerator_freetext.cpp index 3c65f94bf..0d14f13ab 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator_freetext.cpp +++ b/core/fpdfdoc/cpdf_appearancegenerator_freetext.cpp @@ -66,9 +66,10 @@ using appearance_generator::GenerateEphemeralDefaultAcroFormDict; using appearance_generator::GenerateExtGStateDict; using appearance_generator::GenerateResourcesDict; using appearance_generator::GenerateSquareAP; -using appearance_generator::GetBorderStyleInfo; +using appearance_generator::GetAnnotBorderStyleInfo; using appearance_generator::GetBorderWidth; using appearance_generator::GetColorStringWithDefault; +using appearance_generator::GetDashPatternString; using appearance_generator::GetDefaultAppearanceInfo; using appearance_generator::GetFontFromDrFontDictOrGenerateFallback; using appearance_generator::GetPaintOperatorString; @@ -144,6 +145,10 @@ CalloutEnvelope AppendCalloutEnvelope(fxcrt::ostringstream& appearance_stream, if (border_w > 0) { appearance_stream << border_w << " w\n"; } + // The border's dash (/BS, or else /Border) draws the leader, its ending and + // the box alike, as a line annotation's dash draws its line and endings. + const ByteString dash = GetDashPatternString(annot_dict); + appearance_stream << dash; // (e) Draw callout polyline. // Extend conn along the incoming segment direction by half the border @@ -236,6 +241,9 @@ CalloutEnvelope AppendCalloutEnvelope(fxcrt::ostringstream& appearance_stream, text_box_stroke.Deflate(half_bw, half_bw); WriteRect(appearance_stream, text_box_stroke) << " re " << GetPaintOperatorString(is_stroke_rect, is_fill_rect) << "\n"; + if (!dash.IsEmpty()) { + appearance_stream << "[] 0 d\n"; // the text's underlines draw solid + } return {text_box, border_w, box_rot}; } @@ -414,7 +422,7 @@ PrepareRichFreeTextAPInternal(CPDF_Document* doc, inline_rotation ? envelope.box_rot.matrix.TransformRect(box) : box; } else { const BorderStyleInfo border_style_info = - GetBorderStyleInfo(annot_dict->GetDictFor("BS")); + GetAnnotBorderStyleInfo(annot_dict); const ShapeRotationInfo rot_info = GetShapeRotationInfo(annot_dict); const CFX_FloatRect rect = rot_info.bbox; const float half_border_width = border_style_info.width / 2.0f; diff --git a/core/fpdfdoc/cpdf_appearancegenerator_internal.h b/core/fpdfdoc/cpdf_appearancegenerator_internal.h index 602ee8a49..53c38817f 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator_internal.h +++ b/core/fpdfdoc/cpdf_appearancegenerator_internal.h @@ -147,6 +147,7 @@ CFX_PointF Midpoint(const CFX_PointF& first, const CFX_PointF& second); ByteString BlendModeToPDFName(BlendMode bm); BlendMode DefaultBlendModeFor(CPDF_Annot::Subtype subtype); BorderStyleInfo GetBorderStyleInfo(const CPDF_Dictionary* border_style_dict); +BorderStyleInfo GetAnnotBorderStyleInfo(const CPDF_Dictionary* annot_dict); AnnotationDimensionsAndColor GetAnnotationDimensionsAndColor( const CPDF_Dictionary* annot_dict); ShapeRotationInfo GetShapeRotationInfo(const CPDF_Dictionary* annot_dict); diff --git a/core/fpdfdoc/cpdf_appearancegenerator_unittest.cpp b/core/fpdfdoc/cpdf_appearancegenerator_unittest.cpp index 953e7558f..fd91f3047 100644 --- a/core/fpdfdoc/cpdf_appearancegenerator_unittest.cpp +++ b/core/fpdfdoc/cpdf_appearancegenerator_unittest.cpp @@ -3,6 +3,7 @@ #include "core/fpdfdoc/cpdf_appearancegenerator.h" +#include #include #include "constants/annotation_common.h" @@ -207,6 +208,163 @@ TEST_F(CPDFGenerateAPTest, GenerateEphemeralFreeTextAPDoesNotCreateAcroForm) { EXPECT_FALSE(annot_dict->KeyExist(pdfium::annotation::kAP)); } +namespace { + +// A free text box at |rect| reading "hello world" in 12 pt Helvetica. +RetainPtr MakeFreeTextDict(const CFX_FloatRect& rect) { + auto annot_dict = MakeAnnotDict("FreeText", rect); + annot_dict->SetNewFor("DA", "/Helv 12 Tf 0 g"); + annot_dict->SetNewFor(pdfium::annotation::kContents, + "hello world"); + return annot_dict; +} + +// A callout's free text box, its leader running from (10, 10) through a knee +// at (40, 40) to the box, ending in an open arrow. +RetainPtr MakeCalloutDict() { + auto annot_dict = MakeFreeTextDict(CFX_FloatRect(0, 0, 200, 100)); + annot_dict->SetNewFor("IT", "FreeTextCallout"); + annot_dict->SetFor("CL", MakeNumberArray({10, 10, 40, 40, 60, 40})); + annot_dict->SetNewFor("LE", "OpenArrow"); + return annot_dict; +} + +// /Border [0 0 |width|], with |dash| as its dash array when given. +void SetBorderArray(CPDF_Dictionary* annot_dict, + float width, + std::optional> dash = std::nullopt) { + RetainPtr border = MakeNumberArray({0, 0, width}); + if (dash.has_value()) { + border->Append(MakeNumberArray(dash.value())); + } + annot_dict->SetFor(pdfium::annotation::kBorder, std::move(border)); +} + +// /BS << /W |width| >>, dashed by |dash| when given. +void SetBorderStyle(CPDF_Dictionary* annot_dict, + float width, + std::optional> dash = std::nullopt) { + auto border_style = annot_dict->SetNewFor("BS"); + border_style->SetNewFor("W", width); + if (dash.has_value()) { + border_style->SetNewFor("S", "D"); + border_style->SetFor("D", MakeNumberArray(dash.value())); + } +} + +// The content an in-memory free text appearance draws, each in a document +// of its own; empty when none is drawn. +ByteString GenerateFreeTextAPContent(CPDF_Dictionary* annot_dict) { + CPDF_TestDocument doc; + doc.CreateNewDoc(); + std::optional generated = + CPDF_AppearanceGenerator::GenerateEphemeralAnnotAP( + &doc, annot_dict, CPDF_Annot::Subtype::FREETEXT); + if (!generated || !generated->normal_stream || + !generated->normal_stream->IsMemoryBased()) { + return ByteString(); + } + const pdfium::span data = + generated->normal_stream->GetInMemoryRawData(); + return ByteString(data.data(), data.size()); +} + +} // namespace + +// Without /BS, a free text box takes its border width from /Border (ISO +// 32000-1 12.5.2), as a callout does: it draws exactly as the same width in +// /BS does, frame and text plate alike. With neither, the border is the +// default 1 pt. +TEST_F(CPDFGenerateAPTest, FreeTextBorderWidthFromBorderArray) { + auto generate = [](std::optional border_width, + std::optional bs_width) { + auto annot_dict = MakeFreeTextDict(CFX_FloatRect(0, 0, 100, 30)); + if (border_width.has_value()) { + SetBorderArray(annot_dict.Get(), border_width.value()); + } + if (bs_width.has_value()) { + SetBorderStyle(annot_dict.Get(), bs_width.value()); + } + return GenerateFreeTextAPContent(annot_dict.Get()); + }; + + // No border: no frame, and the text plate is the whole box. + const ByteString no_border = generate(0.0f, std::nullopt); + ASSERT_FALSE(no_border.IsEmpty()); + EXPECT_FALSE(no_border.Contains("re f*")); + EXPECT_TRUE(no_border.Contains("0 0 100 30 re W n\n")); + EXPECT_EQ(generate(std::nullopt, 0.0f), no_border); + + EXPECT_EQ(generate(3.0f, std::nullopt), generate(std::nullopt, 3.0f)); + + // Neither key: the 1 pt default, framed, the plate inset 2 pt. + const ByteString default_border = generate(std::nullopt, std::nullopt); + EXPECT_TRUE(default_border.Contains("re f*")); + EXPECT_TRUE(default_border.Contains("2 2 96 26 re W n\n")); + EXPECT_EQ(generate(std::nullopt, 1.0f), default_border); + + // /BS wins over /Border. + EXPECT_EQ(generate(0.0f, 3.0f), generate(std::nullopt, 3.0f)); +} + +// Without /BS, a free text box's /Border dash array dashes its frame, as the +// same pattern in /BS does (ISO 32000-1 12.5.2); an empty one draws solid. +TEST_F(CPDFGenerateAPTest, FreeTextBorderDashFromBorderArray) { + auto with_border = [](std::optional> dash) { + auto annot_dict = MakeFreeTextDict(CFX_FloatRect(0, 0, 100, 30)); + SetBorderArray(annot_dict.Get(), 2, dash); + return GenerateFreeTextAPContent(annot_dict.Get()); + }; + auto with_border_style = [](std::optional> dash) { + auto annot_dict = MakeFreeTextDict(CFX_FloatRect(0, 0, 100, 30)); + SetBorderStyle(annot_dict.Get(), 2, dash); + return GenerateFreeTextAPContent(annot_dict.Get()); + }; + + const ByteString dashed = with_border(std::vector{4, 2}); + ASSERT_FALSE(dashed.IsEmpty()); + EXPECT_TRUE(dashed.Contains("[4 2 ] 0 d\n")); + EXPECT_EQ(with_border_style(std::vector{4, 2}), dashed); + + const ByteString empty_dash = with_border(std::vector{}); + EXPECT_FALSE(empty_dash.Contains(" 0 d\n")); + EXPECT_EQ(with_border_style(std::nullopt), empty_dash); +} + +// A callout's border dash draws its leader, the leader's ending and its box +// alike, from /BS or else /Border, as a line annotation's dash draws its line +// and endings. The text's underlines then draw solid. +TEST_F(CPDFGenerateAPTest, FreeTextCalloutBorderDash) { + auto callout = MakeCalloutDict(); + SetBorderStyle(callout.Get(), 1, std::vector{3, 1}); + const ByteString dashed = GenerateFreeTextAPContent(callout.Get()); + ASSERT_FALSE(dashed.IsEmpty()); + + const std::optional dash = dashed.Find("[3 1 ] 0 d\n"); + const std::optional leader = dashed.Find(" l S\n"); + const std::optional ending = dashed.Find(" cm\n"); + const std::optional solid = dashed.Find("[] 0 d\n"); + const std::optional text = dashed.Find("BT\n"); + ASSERT_TRUE(dash.has_value()); + ASSERT_TRUE(leader.has_value()); + ASSERT_TRUE(ending.has_value()); + ASSERT_TRUE(solid.has_value()); + ASSERT_TRUE(text.has_value()); + EXPECT_LT(dash.value(), leader.value()); + EXPECT_LT(leader.value(), ending.value()); + EXPECT_LT(ending.value(), solid.value()); + EXPECT_LT(solid.value(), text.value()); + + auto from_border = MakeCalloutDict(); + SetBorderArray(from_border.Get(), 1, std::vector{3, 1}); + EXPECT_EQ(GenerateFreeTextAPContent(from_border.Get()), dashed); + + auto solid_callout = MakeCalloutDict(); + SetBorderStyle(solid_callout.Get(), 1); + EXPECT_FALSE( + GenerateFreeTextAPContent(solid_callout.Get()).Contains(" 0 d\n")); +} + TEST_F(CPDFGenerateAPTest, GeneratePersistentFreeTextAPAfterDefaultAppearanceColorUpdate) { CPDF_TestDocument doc;