Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions core/fpdfdoc/cpdf_appearancegenerator_common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<const CPDF_Dictionary> 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<const CPDF_Array> border_array =
annot_dict->GetArrayFor(pdfium::annotation::kBorder);
if (border_array && border_array->size() == 4) {
std::vector<float> 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 =
Expand Down
12 changes: 10 additions & 2 deletions core/fpdfdoc/cpdf_appearancegenerator_freetext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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};
}

Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions core/fpdfdoc/cpdf_appearancegenerator_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
158 changes: 158 additions & 0 deletions core/fpdfdoc/cpdf_appearancegenerator_unittest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include "core/fpdfdoc/cpdf_appearancegenerator.h"

#include <optional>
#include <vector>

#include "constants/annotation_common.h"
Expand Down Expand Up @@ -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<CPDF_Dictionary> MakeFreeTextDict(const CFX_FloatRect& rect) {
auto annot_dict = MakeAnnotDict("FreeText", rect);
annot_dict->SetNewFor<CPDF_String>("DA", "/Helv 12 Tf 0 g");
annot_dict->SetNewFor<CPDF_String>(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<CPDF_Dictionary> MakeCalloutDict() {
auto annot_dict = MakeFreeTextDict(CFX_FloatRect(0, 0, 200, 100));
annot_dict->SetNewFor<CPDF_Name>("IT", "FreeTextCallout");
annot_dict->SetFor("CL", MakeNumberArray({10, 10, 40, 40, 60, 40}));
annot_dict->SetNewFor<CPDF_Name>("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<std::vector<float>> dash = std::nullopt) {
RetainPtr<CPDF_Array> 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<std::vector<float>> dash = std::nullopt) {
auto border_style = annot_dict->SetNewFor<CPDF_Dictionary>("BS");
border_style->SetNewFor<CPDF_Number>("W", width);
if (dash.has_value()) {
border_style->SetNewFor<CPDF_Name>("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<CPDF_AppearanceGenerator::GeneratedAP> 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<const uint8_t> 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<float> border_width,
std::optional<float> 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<std::vector<float>> 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<std::vector<float>> 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<float>{4, 2});
ASSERT_FALSE(dashed.IsEmpty());
EXPECT_TRUE(dashed.Contains("[4 2 ] 0 d\n"));
EXPECT_EQ(with_border_style(std::vector<float>{4, 2}), dashed);

const ByteString empty_dash = with_border(std::vector<float>{});
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<float>{3, 1});
const ByteString dashed = GenerateFreeTextAPContent(callout.Get());
ASSERT_FALSE(dashed.IsEmpty());

const std::optional<size_t> dash = dashed.Find("[3 1 ] 0 d\n");
const std::optional<size_t> leader = dashed.Find(" l S\n");
const std::optional<size_t> ending = dashed.Find(" cm\n");
const std::optional<size_t> solid = dashed.Find("[] 0 d\n");
const std::optional<size_t> 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<float>{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;
Expand Down