Create new stream objects when changing annotation appearance streams.
Avoid editing existing streams, as they may be shared.
Update affected JS tests as a result. The new behavior better matches
other PDF JS implementations.
Bug: chromium:1302455
Change-Id: Ib0f56b6f9eda29a94a2b98e758c77a84b2ef36ec
Reviewed-on: https://pdfium-review.googlesource.com/c/pdfium/+/91290
Reviewed-by: Tom Sepez <tsepez@chromium.org>
Commit-Queue: Lei Zhang <thestig@chromium.org>
diff --git a/core/fpdfapi/parser/cpdf_document.cpp b/core/fpdfapi/parser/cpdf_document.cpp
index 4a2ec62..4e71482 100644
--- a/core/fpdfapi/parser/cpdf_document.cpp
+++ b/core/fpdfapi/parser/cpdf_document.cpp
@@ -14,6 +14,7 @@
#include "core/fpdfapi/parser/cpdf_parser.h"
#include "core/fpdfapi/parser/cpdf_read_validator.h"
#include "core/fpdfapi/parser/cpdf_reference.h"
+#include "core/fpdfapi/parser/cpdf_stream.h"
#include "core/fpdfapi/parser/fpdf_parser_utility.h"
#include "core/fxcodec/jbig2/JBig2_DocumentContext.h"
#include "core/fxcrt/fx_codepage.h"
@@ -312,6 +313,16 @@
return m_pCodecContext.get();
}
+CPDF_Stream* CPDF_Document::CreateModifiedAPStream() {
+ auto* stream = NewIndirect<CPDF_Stream>();
+ m_ModifiedAPStreamIDs.insert(stream->GetObjNum());
+ return stream;
+}
+
+bool CPDF_Document::IsModifiedAPStream(const CPDF_Stream* stream) const {
+ return stream && pdfium::Contains(m_ModifiedAPStreamIDs, stream->GetObjNum());
+}
+
int CPDF_Document::GetPageIndex(uint32_t objnum) {
uint32_t skip_count = 0;
bool bSkipped = false;
diff --git a/core/fpdfapi/parser/cpdf_document.h b/core/fpdfapi/parser/cpdf_document.h
index 9eaa5ca..d5b59aa 100644
--- a/core/fpdfapi/parser/cpdf_document.h
+++ b/core/fpdfapi/parser/cpdf_document.h
@@ -112,6 +112,12 @@
m_pLinksContext = std::move(pContext);
}
+ // Behaves like NewIndirect<CPDF_Stream>(), but keeps track of the new stream.
+ CPDF_Stream* CreateModifiedAPStream();
+
+ // Returns whether CreateModifiedAPStream() created `stream`.
+ bool IsModifiedAPStream(const CPDF_Stream* stream) const;
+
// CPDF_Parser::ParsedObjectsHolder:
bool TryInit() override;
RetainPtr<CPDF_Object> ParseIndirectObject(uint32_t objnum) override;
@@ -191,6 +197,7 @@
std::unique_ptr<PageDataIface> m_pDocPage; // Must be after |m_pDocRender|.
std::unique_ptr<JBig2_DocumentContext> m_pCodecContext;
std::unique_ptr<LinkListIface> m_pLinksContext;
+ std::set<uint32_t> m_ModifiedAPStreamIDs;
std::vector<uint32_t> m_PageList; // Page number to page's dict objnum.
// Must be second to last.
diff --git a/fpdfsdk/cpdfsdk_appstream.cpp b/fpdfsdk/cpdfsdk_appstream.cpp
index 41f2330..027cb1f 100644
--- a/fpdfsdk/cpdfsdk_appstream.cpp
+++ b/fpdfsdk/cpdfsdk_appstream.cpp
@@ -1844,24 +1844,27 @@
void CPDFSDK_AppStream::Write(const ByteString& sAPType,
const ByteString& sContents,
const ByteString& sAPState) {
- CPDF_Stream* pStream = nullptr;
- CPDF_Dictionary* pParentDict = nullptr;
+ CPDF_Dictionary* pParentDict;
+ ByteString key;
if (sAPState.IsEmpty()) {
pParentDict = dict_.Get();
- pStream = dict_->GetStreamFor(sAPType);
+ key = sAPType;
} else {
CPDF_Dictionary* pAPTypeDict = dict_->GetDictFor(sAPType);
if (!pAPTypeDict)
pAPTypeDict = dict_->SetNewFor<CPDF_Dictionary>(sAPType);
pParentDict = pAPTypeDict;
- pStream = pAPTypeDict->GetStreamFor(sAPState);
+ key = sAPState;
}
- if (!pStream) {
- CPDF_Document* doc = widget_->GetPageView()->GetPDFDocument();
- pStream = doc->NewIndirect<CPDF_Stream>();
- pParentDict->SetNewFor<CPDF_Reference>(sAPType, doc, pStream->GetObjNum());
+ // If `pStream` is created by CreateModifiedAPStream(), then it is safe to
+ // edit, as it is not shared.
+ CPDF_Stream* pStream = pParentDict->GetStreamFor(key);
+ CPDF_Document* doc = widget_->GetPageView()->GetPDFDocument();
+ if (!doc->IsModifiedAPStream(pStream)) {
+ pStream = doc->CreateModifiedAPStream();
+ pParentDict->SetNewFor<CPDF_Reference>(key, doc, pStream->GetObjNum());
}
CPDF_Dictionary* pStreamDict = pStream->GetDict();
diff --git a/fpdfsdk/fpdf_formfill_embeddertest.cpp b/fpdfsdk/fpdf_formfill_embeddertest.cpp
index 2dcfcd7..269187f 100644
--- a/fpdfsdk/fpdf_formfill_embeddertest.cpp
+++ b/fpdfsdk/fpdf_formfill_embeddertest.cpp
@@ -1405,6 +1405,127 @@
UnloadPage(page);
}
+TEST_F(FPDFFormFillEmbedderTest, Bug1302455RenderOnly) {
+#if defined(_SKIA_SUPPORT_) || defined(_SKIA_SUPPORT_PATHS_)
+ const char kChecksum[] = "520c4415c9977f40d6b4af5a0a94d764";
+#else
+ const char kChecksum[] = "bbee92af1daec2340c81f482878744d8";
+#endif
+ {
+ ASSERT_TRUE(OpenDocument("bug_1302455.pdf"));
+ FPDF_PAGE page = LoadPage(0);
+ ASSERT_TRUE(page);
+
+ ScopedFPDFBitmap bitmap = RenderLoadedPage(page);
+ CompareBitmap(bitmap.get(), 300, 300, kChecksum);
+
+ EXPECT_TRUE(FPDF_SaveAsCopy(document(), this, 0));
+
+ UnloadPage(page);
+ }
+ VerifySavedDocument(300, 300, kChecksum);
+}
+
+TEST_F(FPDFFormFillEmbedderTest, Bug1302455EditFirstForm) {
+#if defined(_SKIA_SUPPORT_) || defined(_SKIA_SUPPORT_PATHS_)
+ const char kChecksum[] = "29a06da3e47f67535e266b090a5ac82d";
+#elif BUILDFLAG(IS_APPLE)
+ const char kChecksum[] = "bf5423874f188427d2500a2bc4abebbe";
+#else
+ const char kChecksum[] = "6a4ac9a15d2c34589616c8f2b05fbedd";
+#endif
+ {
+ ASSERT_TRUE(OpenDocument("bug_1302455.pdf"));
+ FPDF_PAGE page = LoadPage(0);
+ ASSERT_TRUE(page);
+
+ EXPECT_EQ(FPDF_FORMFIELD_TEXTFIELD,
+ FPDFPage_HasFormFieldAtPoint(form_handle(), page, 110, 110));
+ FORM_OnMouseMove(form_handle(), page, 0, 110, 110);
+ FORM_OnLButtonDown(form_handle(), page, 0, 110, 110);
+ FORM_OnLButtonUp(form_handle(), page, 0, 110, 110);
+ FORM_OnChar(form_handle(), page, 'A', 0);
+
+ FORM_ForceToKillFocus(form_handle());
+ ScopedFPDFBitmap bitmap = RenderLoadedPage(page);
+ CompareBitmap(bitmap.get(), 300, 300, kChecksum);
+
+ EXPECT_TRUE(FPDF_SaveAsCopy(document(), this, 0));
+
+ UnloadPage(page);
+ }
+ VerifySavedDocument(300, 300, kChecksum);
+}
+
+TEST_F(FPDFFormFillEmbedderTest, Bug1302455EditSecondForm) {
+#if defined(_SKIA_SUPPORT_) || defined(_SKIA_SUPPORT_PATHS_)
+ const char kChecksum[] = "19f8574d6378ee36e349376d88b7a2c4";
+#elif BUILDFLAG(IS_APPLE)
+ const char kChecksum[] = "8a0fd8772dba6e1e952e49d159cc64b5";
+#else
+ const char kChecksum[] = "45a7694933c2ba3c5dc8f6cc18b79175";
+#endif
+ {
+ ASSERT_TRUE(OpenDocument("bug_1302455.pdf"));
+ FPDF_PAGE page = LoadPage(0);
+ ASSERT_TRUE(page);
+
+ EXPECT_EQ(FPDF_FORMFIELD_TEXTFIELD,
+ FPDFPage_HasFormFieldAtPoint(form_handle(), page, 110, 170));
+ FORM_OnMouseMove(form_handle(), page, 0, 110, 170);
+ FORM_OnLButtonDown(form_handle(), page, 0, 110, 170);
+ FORM_OnLButtonUp(form_handle(), page, 0, 110, 170);
+ FORM_OnChar(form_handle(), page, 'B', 0);
+
+ FORM_ForceToKillFocus(form_handle());
+ ScopedFPDFBitmap bitmap = RenderLoadedPage(page);
+ CompareBitmap(bitmap.get(), 300, 300, kChecksum);
+
+ EXPECT_TRUE(FPDF_SaveAsCopy(document(), this, 0));
+
+ UnloadPage(page);
+ }
+ VerifySavedDocument(300, 300, kChecksum);
+}
+
+TEST_F(FPDFFormFillEmbedderTest, Bug1302455EditBothForms) {
+#if defined(_SKIA_SUPPORT_) || defined(_SKIA_SUPPORT_PATHS_)
+ const char kChecksum[] = "edbc9b0e190118a9039fffc11e494081";
+#elif BUILDFLAG(IS_APPLE)
+ const char kChecksum[] = "1f422ee1c520ad74b1a993b64bd4dc4a";
+#else
+ const char kChecksum[] = "13984969b1e141079ab5f4aa80185463";
+#endif
+ {
+ ASSERT_TRUE(OpenDocument("bug_1302455.pdf"));
+ FPDF_PAGE page = LoadPage(0);
+ ASSERT_TRUE(page);
+
+ EXPECT_EQ(FPDF_FORMFIELD_TEXTFIELD,
+ FPDFPage_HasFormFieldAtPoint(form_handle(), page, 110, 110));
+ FORM_OnMouseMove(form_handle(), page, 0, 110, 110);
+ FORM_OnLButtonDown(form_handle(), page, 0, 110, 110);
+ FORM_OnLButtonUp(form_handle(), page, 0, 110, 110);
+ FORM_OnChar(form_handle(), page, 'A', 0);
+
+ EXPECT_EQ(FPDF_FORMFIELD_TEXTFIELD,
+ FPDFPage_HasFormFieldAtPoint(form_handle(), page, 110, 170));
+ FORM_OnMouseMove(form_handle(), page, 0, 110, 170);
+ FORM_OnLButtonDown(form_handle(), page, 0, 110, 170);
+ FORM_OnLButtonUp(form_handle(), page, 0, 110, 170);
+ FORM_OnChar(form_handle(), page, 'B', 0);
+
+ FORM_ForceToKillFocus(form_handle());
+ ScopedFPDFBitmap bitmap = RenderLoadedPage(page);
+ CompareBitmap(bitmap.get(), 300, 300, kChecksum);
+
+ EXPECT_TRUE(FPDF_SaveAsCopy(document(), this, 0));
+
+ UnloadPage(page);
+ }
+ VerifySavedDocument(300, 300, kChecksum);
+}
+
TEST_F(FPDFFormFillEmbedderTest, RemoveFormFieldHighlight) {
#if defined(_SKIA_SUPPORT_) || defined(_SKIA_SUPPORT_PATHS_)
const char kMd5NoHighlight[] = "013aa241c39c02505d9525550be04e48";
diff --git a/testing/resources/bug_1302455.in b/testing/resources/bug_1302455.in
new file mode 100644
index 0000000..1e18f12
--- /dev/null
+++ b/testing/resources/bug_1302455.in
@@ -0,0 +1,75 @@
+{{header}}
+{{object 1 0}} <<
+ /Type /Catalog
+ /Pages 2 0 R
+ /AcroForm <<
+ /DR 4 0 R
+ /Fields [6 0 R 7 0 R]
+ >>
+>>
+endobj
+{{object 2 0}} <<
+ /Type /Pages
+ /Kids [3 0 R]
+ /Count 1
+>>
+endobj
+{{object 3 0}} <<
+ /Type /Page
+ /Parent 2 0 R
+ /Annots [6 0 R 7 0 R]
+ /MediaBox [0 0 300 300]
+ /Resources 4 0 R
+>>
+endobj
+{{object 4 0}} <<
+ /Font <<
+ /F1 5 0 R
+ >>
+>>
+endobj
+{{object 5 0}} <<
+ /Type /Font
+ /Subtype /Type1
+ /BaseFont /Helvetica
+>>
+endobj
+{{object 6 0}} <<
+ /Type /Annot
+ /Subtype /Widget
+ /FT /Tx
+ /AP <<
+ /N 8 0 R
+ >>
+ /F 4
+ /Rect [100 100 200 130]
+ /T (Text Box 1)
+>>
+endobj
+{{object 7 0}} <<
+ /Type /Annot
+ /Subtype /Widget
+ /FT /Tx
+ /AP <<
+ /N 8 0 R
+ >>
+ /F 4
+ /Rect [100 160 200 190]
+ /T (Text Box 2)
+>>
+endobj
+{{object 8 0}} <<
+ /Type /XObject
+ /Subtype /Form
+ /BBox [0 0 100 30]
+ {{streamlen}}
+>>
+stream
+1 0 0 rg
+0 0 100 30 re f
+endstream
+endobj
+{{xref}}
+{{trailer}}
+{{startxref}}
+%%EOF
diff --git a/testing/resources/bug_1302455.pdf b/testing/resources/bug_1302455.pdf
new file mode 100644
index 0000000..a9782c4
--- /dev/null
+++ b/testing/resources/bug_1302455.pdf
@@ -0,0 +1,90 @@
+%PDF-1.7
+% ò¤ô
+1 0 obj <<
+ /Type /Catalog
+ /Pages 2 0 R
+ /AcroForm <<
+ /DR 4 0 R
+ /Fields [6 0 R 7 0 R]
+ >>
+>>
+endobj
+2 0 obj <<
+ /Type /Pages
+ /Kids [3 0 R]
+ /Count 1
+>>
+endobj
+3 0 obj <<
+ /Type /Page
+ /Parent 2 0 R
+ /Annots [6 0 R 7 0 R]
+ /MediaBox [0 0 300 300]
+ /Resources 4 0 R
+>>
+endobj
+4 0 obj <<
+ /Font <<
+ /F1 5 0 R
+ >>
+>>
+endobj
+5 0 obj <<
+ /Type /Font
+ /Subtype /Type1
+ /BaseFont /Helvetica
+>>
+endobj
+6 0 obj <<
+ /Type /Annot
+ /Subtype /Widget
+ /FT /Tx
+ /AP <<
+ /N 8 0 R
+ >>
+ /F 4
+ /Rect [100 100 200 130]
+ /T (Text Box 1)
+>>
+endobj
+7 0 obj <<
+ /Type /Annot
+ /Subtype /Widget
+ /FT /Tx
+ /AP <<
+ /N 8 0 R
+ >>
+ /F 4
+ /Rect [100 160 200 190]
+ /T (Text Box 2)
+>>
+endobj
+8 0 obj <<
+ /Type /XObject
+ /Subtype /Form
+ /BBox [0 0 100 30]
+ /Length 25
+>>
+stream
+1 0 0 rg
+0 0 100 30 re f
+endstream
+endobj
+xref
+0 9
+0000000000 65535 f
+0000000015 00000 n
+0000000128 00000 n
+0000000191 00000 n
+0000000311 00000 n
+0000000362 00000 n
+0000000438 00000 n
+0000000581 00000 n
+0000000724 00000 n
+trailer <<
+ /Root 1 0 R
+ /Size 9
+>>
+startxref
+855
+%%EOF
diff --git a/testing/resources/javascript/field_properties.in b/testing/resources/javascript/field_properties.in
index 1961f6a..0a84430d 100644
--- a/testing/resources/javascript/field_properties.in
+++ b/testing/resources/javascript/field_properties.in
@@ -116,7 +116,7 @@
function testRadioButtonPropertiesCase(field) {
try {
- testROProperty(field, "exportValues", "N");
+ testROProperty(field, "exportValues", "Yes");
testRIProperty(field, "radiosInUnison", false);
testRIProperty(field, "style", "check");
testROProperty(field, "type", "radiobutton");
@@ -129,7 +129,7 @@
function testCheckBoxPropertiesCase(field) {
try {
- testROProperty(field, "exportValues", "N");
+ testROProperty(field, "exportValues", "Yes");
testRIProperty(field, "style", "check");
testROProperty(field, "type", "checkbox");
testRIProperty(field, "value", "Off");
diff --git a/testing/resources/javascript/field_properties_expected.txt b/testing/resources/javascript/field_properties_expected.txt
index 8670196..b3d8195 100644
--- a/testing/resources/javascript/field_properties_expected.txt
+++ b/testing/resources/javascript/field_properties_expected.txt
@@ -96,7 +96,7 @@
Alert: PASS: type threw Field.type: Operation not supported.
Alert: PASS: buttonPosition = 7
Alert: PASS: buttonPosition = 7
-Alert: PASS: exportValues = N
+Alert: PASS: exportValues = Yes
Alert: PASS: exportValues threw Field.exportValues: Object no longer exists.
Alert: PASS: radiosInUnison = false
Alert: PASS: radiosInUnison = false
@@ -108,7 +108,7 @@
Alert: PASS: value = Off
Alert: PASS: valueAsString = Off
Alert: PASS: valueAsString threw Field.valueAsString: Operation not supported.
-Alert: PASS: exportValues = N
+Alert: PASS: exportValues = Yes
Alert: PASS: exportValues threw Field.exportValues: Object no longer exists.
Alert: PASS: style = check
Alert: PASS: style = check