Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
94383fe
Read-only walks to const accessors across content parsing
bobsingor May 8, 2026
7bfcd90
AP becomes ephemeral
bobsingor May 8, 2026
71516c3
CPDF_AnnotList preserves direct annotations
bobsingor May 8, 2026
2dd7caa
CPDF_InteractiveForm const walk + explicit normalize
bobsingor May 8, 2026
b50c87f
CPDF_Image and CPDF_DocPageData cache hardening
bobsingor May 8, 2026
3cdfb37
Install macros
bobsingor May 8, 2026
87aec2e
Widen all internal seams
bobsingor May 9, 2026
4e5781e
CPDF_BaseDocument + eager parse + freeze
bobsingor May 9, 2026
ef257ba
CPDF_LayerDocument (read fall-through)
bobsingor May 9, 2026
b345d99
COW on actual mutable handle USE
bobsingor May 9, 2026
a591805
CloneForHolder + PromoteFromBase
bobsingor May 9, 2026
502f817
EPDFLayer_SaveDeltaToBuffer + creator append-only mode
bobsingor May 9, 2026
2cde580
Introspection + benchmarks + soak
bobsingor May 9, 2026
986cd2e
Finish opening file with delta
bobsingor May 10, 2026
7d468af
ReadOnlyLayerWorkflowsProduceEmptyDelta
bobsingor May 10, 2026
8191aeb
Keep layer read paths from promoting page and name-tree state
bobsingor May 10, 2026
efe597b
Add some more tests
bobsingor May 12, 2026
a3f16c2
Prune unreachable new PDF objects during save
bobsingor May 12, 2026
f769de7
Add EPDF_LoadMemBaseDocument API
bobsingor May 15, 2026
df00447
Add EPDF_LoadMemBaseDocument64 and helpers
bobsingor May 15, 2026
f66d083
Add GetPageObjectNumberByIndex API and tests
bobsingor May 15, 2026
1bdc674
Const-correct annot APIs; add annot index
bobsingor May 17, 2026
ac54d56
Fix some readonly paths
bobsingor May 17, 2026
19fbb6d
Honor layer overlays when collecting objects
bobsingor May 17, 2026
b57a70c
Add redaction API and form XObject helper
bobsingor May 18, 2026
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
1 change: 1 addition & 0 deletions core/fpdfapi/edit/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ source_set("contentstream_write_utils") {

pdfium_unittest_source_set("unittests") {
sources = [
"cpdf_creator_unittest.cpp",
"cpdf_npagetooneexporter_unittest.cpp",
"cpdf_pagecontentgenerator_unittest.cpp",
]
Expand Down
116 changes: 106 additions & 10 deletions core/fpdfapi/edit/cpdf_creator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@

#include <stdint.h>

#include <inttypes.h>
#include <algorithm>
#include <array>
#include <set>
#include <utility>
#include <vector>

#include "core/fpdfapi/parser/cpdf_array.h"
#include "core/fpdfapi/parser/cpdf_crypto_handler.h"
Expand Down Expand Up @@ -44,10 +46,12 @@ constexpr Mask<CPDF_Creator::CreateFlags> kAllValidFlags{
CPDF_Creator::CreateFlags::kIncremental,
CPDF_Creator::CreateFlags::kNoOriginal,
CPDF_Creator::CreateFlags::kRemoveSecurity,
CPDF_Creator::CreateFlags::kSubsetNewFonts};
CPDF_Creator::CreateFlags::kSubsetNewFonts,
CPDF_Creator::CreateFlags::kIncrementalAppendOnly};
constexpr Mask<CPDF_Creator::CreateFlags> kConflictingFlags{
CPDF_Creator::CreateFlags::kIncremental,
CPDF_Creator::CreateFlags::kNoOriginal};
constexpr FX_FILESIZE kMaxFourByteXrefOffset = 0xffffffff;

class CFX_FileBufferArchive final : public IFX_ArchiveStream {
public:
Expand All @@ -56,6 +60,7 @@ class CFX_FileBufferArchive final : public IFX_ArchiveStream {

bool WriteBlock(pdfium::span<const uint8_t> buffer) override;
FX_FILESIZE CurrentOffset() const override { return offset_; }
void SetNotionalStartOffset(FX_FILESIZE offset) { offset_ = offset; }

private:
bool Flush();
Expand Down Expand Up @@ -128,6 +133,40 @@ bool OutputIndex(IFX_ArchiveStream* archive, FX_FILESIZE offset) {
archive->WriteByte(0);
}

ByteString FormatXrefOffset10(FX_FILESIZE offset) {
return ByteString::Format("%010" PRId64, static_cast<int64_t>(offset));
}

std::set<uint32_t> CollectSaveReachableObjects(
CPDF_Document* document,
const CPDF_Dictionary* encrypt_dict) {
// CPDF_LayerDocument overlays new/promoted objects on a frozen base.
// References inherited from the base graph can still point through base
// holders, so resolving through the holder would skip overlay replacements.
// Walk through the layer document instead so the effective graph is what gets
// saved.
std::set<uint32_t> objects = GetObjectsWithReferences(
document, document->IsLayerDocument()
? ObjectTreeReferenceResolveMode::kEffectiveDocument
: ObjectTreeReferenceResolveMode::kReferenceHolder);

// `GetObjectsWithReferences()` covers the normal document graph rooted at
// /Root. The save trailer may also reference dictionaries outside that graph.
// Keep those roots in sync with the trailer entries emitted in
// WriteDoc_Stage4().
RetainPtr<CPDF_Dictionary> info = document->GetInfo();
if (info && info->GetObjNum() != 0) {
objects.insert(info->GetObjNum());
}

if (encrypt_dict && !encrypt_dict->IsInline() &&
encrypt_dict->GetObjNum() != 0) {
objects.insert(encrypt_dict->GetObjNum());
}

return objects;
}

} // namespace

CPDF_Creator::CPDF_Creator(CPDF_Document* doc,
Expand All @@ -141,6 +180,11 @@ CPDF_Creator::CPDF_Creator(CPDF_Document* doc,

CPDF_Creator::~CPDF_Creator() = default;

// static
ByteString CPDF_Creator::FormatXrefOffset10ForTesting(FX_FILESIZE offset) {
return FormatXrefOffset10(offset);
}

bool CPDF_Creator::WriteIndirectObj(uint32_t objnum, const CPDF_Object* pObj) {
if (!archive_->WriteDWord(objnum) || !archive_->WriteString(" 0 obj\r\n")) {
return false;
Expand Down Expand Up @@ -189,6 +233,8 @@ bool CPDF_Creator::WriteOldObjs() {
return true;
}

// WriteOldObjs() only runs for full saves. Layer saves are incremental and
// use CollectSaveReachableObjects() when writing their overlay objects.
const std::set<uint32_t> objects_with_refs =
GetObjectsWithReferences(document_);
uint32_t last_object_number_written = 0;
Expand All @@ -210,8 +256,15 @@ bool CPDF_Creator::WriteOldObjs() {
}

bool CPDF_Creator::WriteNewObjs() {
const std::set<uint32_t> objects_with_refs =
CollectSaveReachableObjects(document_, encrypt_dict_.Get());
std::vector<uint32_t> written_new_obj_nums;
for (size_t i = cur_obj_num_; i < new_obj_num_array_.size(); ++i) {
uint32_t objnum = new_obj_num_array_[i];
if (!pdfium::Contains(objects_with_refs, objnum)) {
continue;
}

RetainPtr<const CPDF_Object> pObj = document_->GetIndirectObject(objnum);
if (!pObj) {
continue;
Expand All @@ -221,10 +274,20 @@ bool CPDF_Creator::WriteNewObjs() {
if (!WriteIndirectObj(pObj->GetObjNum(), pObj.Get())) {
return false;
}
written_new_obj_nums.push_back(objnum);
}
new_obj_num_array_ = std::move(written_new_obj_nums);
return true;
}

bool CPDF_Creator::CheckEmittedOffset(FX_FILESIZE offset) {
if (offset <= kMaxFourByteXrefOffset) {
return true;
}
failure_reason_ = FailureReason::kAppendOnlyOffsetTooLarge;
return false;
}

void CPDF_Creator::InitNewObjNumOffsets() {
for (const auto& pair : *document_) {
const uint32_t objnum = pair.first;
Expand Down Expand Up @@ -269,13 +332,16 @@ CPDF_Creator::Stage CPDF_Creator::WriteDoc_Stage1() {
}
stage_ = Stage::kInitWriteObjs20;
} else {
saved_offset_ = parser_->GetDocumentSize();
saved_offset_ = is_incremental_append_only_
? document_->GetLayerAppendBaseOffset()
: parser_->GetDocumentSize();
stage_ = Stage::kWriteIncremental15;
}
}
if (stage_ == Stage::kWriteIncremental15) {
if (is_original_ && saved_offset_ > 0) {
if (is_original_ && !is_incremental_append_only_ && saved_offset_ > 0) {
if (!parser_->WriteToArchive(archive_.get(), saved_offset_)) {
failure_reason_ = FailureReason::kArchiveError;
return Stage::kInvalid;
}
}
Expand Down Expand Up @@ -348,7 +414,8 @@ CPDF_Creator::Stage CPDF_Creator::WriteDoc_Stage3() {
uint32_t dwLastObjNum = last_obj_num_;
if (stage_ == Stage::kInitWriteXRefs80) {
xref_start_ = archive_->CurrentOffset();
if (!is_incremental_ || !parser_->IsXRefStream()) {
if (!is_incremental_ || is_incremental_append_only_ ||
!parser_->IsXRefStream()) {
if (!is_incremental_ || parser_->GetLastXRefOffset() == 0) {
ByteString str;
str = pdfium::Contains(object_offsets_, 1)
Expand Down Expand Up @@ -401,7 +468,11 @@ CPDF_Creator::Stage CPDF_Creator::WriteDoc_Stage3() {
}

while (i < j) {
str = ByteString::Format("%010d 00000 n\r\n", object_offsets_[i++]);
const FX_FILESIZE offset = object_offsets_[i++];
if (!CheckEmittedOffset(offset)) {
return Stage::kInvalid;
}
str = FormatXrefOffset10(offset) + " 00000 n\r\n";
if (!archive_->WriteString(str.AsStringView())) {
return Stage::kInvalid;
}
Expand Down Expand Up @@ -442,7 +513,11 @@ CPDF_Creator::Stage CPDF_Creator::WriteDoc_Stage3() {

while (i < j) {
objnum = new_obj_num_array_[i++];
str = ByteString::Format("%010d 00000 n\r\n", object_offsets_[objnum]);
const FX_FILESIZE offset = object_offsets_[objnum];
if (!CheckEmittedOffset(offset)) {
return Stage::kInvalid;
}
str = FormatXrefOffset10(offset) + " 00000 n\r\n";
if (!archive_->WriteString(str.AsStringView())) {
return Stage::kInvalid;
}
Expand All @@ -456,7 +531,8 @@ CPDF_Creator::Stage CPDF_Creator::WriteDoc_Stage3() {
CPDF_Creator::Stage CPDF_Creator::WriteDoc_Stage4() {
DCHECK(stage_ >= Stage::kWriteTrailerAndFinish90);

bool bXRefStream = is_incremental_ && parser_->IsXRefStream();
bool bXRefStream = is_incremental_ && !is_incremental_append_only_ &&
parser_->IsXRefStream();
if (!bXRefStream) {
if (!archive_->WriteString("trailer\r\n<<")) {
return Stage::kInvalid;
Expand Down Expand Up @@ -562,6 +638,9 @@ CPDF_Creator::Stage CPDF_Creator::WriteDoc_Stage4() {
if (it == object_offsets_.end()) {
continue;
}
if (!CheckEmittedOffset(it->second)) {
return Stage::kInvalid;
}
if (!OutputIndex(archive_.get(), it->second)) {
return Stage::kInvalid;
}
Expand All @@ -581,8 +660,11 @@ CPDF_Creator::Stage CPDF_Creator::WriteDoc_Stage4() {
return Stage::kInvalid;
}
for (i = 0; i < count; ++i) {
if (!OutputIndex(archive_.get(),
object_offsets_[new_obj_num_array_[i]])) {
const FX_FILESIZE offset = object_offsets_[new_obj_num_array_[i]];
if (!CheckEmittedOffset(offset)) {
return Stage::kInvalid;
}
if (!OutputIndex(archive_.get(), offset)) {
return Stage::kInvalid;
}
}
Expand All @@ -603,6 +685,7 @@ CPDF_Creator::Stage CPDF_Creator::WriteDoc_Stage4() {
}

bool CPDF_Creator::Create(Mask<CreateFlags> flags, int32_t file_version) {
failure_reason_ = FailureReason::kNone;
if (flags & ~kAllValidFlags) {
flags = CreateFlags::kNone;
}
Expand All @@ -617,7 +700,16 @@ bool CPDF_Creator::Create(Mask<CreateFlags> flags, int32_t file_version) {
}

is_incremental_ = !!(flags & CreateFlags::kIncremental);
is_incremental_append_only_ = !!(flags & CreateFlags::kIncrementalAppendOnly);
if (is_incremental_append_only_ && !is_incremental_) {
failure_reason_ = FailureReason::kOther;
return false;
}
is_original_ = !(flags & CreateFlags::kNoOriginal);
if (is_incremental_append_only_ && parser_) {
static_cast<CFX_FileBufferArchive*>(archive_.get())
->SetNotionalStartOffset(document_->GetLayerAppendBaseOffset());
}

if (file_version >= 10 && file_version <= 17) {
file_version_ = file_version;
Expand All @@ -629,7 +721,11 @@ bool CPDF_Creator::Create(Mask<CreateFlags> flags, int32_t file_version) {
new_obj_num_array_.clear();

InitID();
return Continue();
const bool result = Continue();
if (!result && failure_reason_ == FailureReason::kNone) {
failure_reason_ = FailureReason::kOther;
}
return result;
}

void CPDF_Creator::InitID() {
Expand Down
15 changes: 15 additions & 0 deletions core/fpdfapi/edit/cpdf_creator.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#include <memory>
#include <vector>

#include "core/fxcrt/fx_string.h"
#include "core/fxcrt/fx_stream.h"
#include "core/fxcrt/mask.h"
#include "core/fxcrt/retain_ptr.h"
Expand All @@ -36,13 +37,24 @@ class CPDF_Creator {
kRemoveSecurity = (1 << 2),
// TODO(crbug.com/42270430): Implement font subsetting.
kSubsetNewFonts = (1 << 3),
kIncrementalAppendOnly = (1 << 4),
};

enum class FailureReason {
kNone,
kAppendOnlyOffsetTooLarge,
kArchiveError,
kOther,
};

CPDF_Creator(CPDF_Document* doc,
RetainPtr<IFX_RetainableWriteStream> archive);
~CPDF_Creator();

bool Create(Mask<CreateFlags> flags, int32_t file_version);
FailureReason GetFailureReason() const { return failure_reason_; }

static ByteString FormatXrefOffset10ForTesting(FX_FILESIZE offset);

// Experimental EmbedPDF Extension: Set encryption for documents that weren't
// originally encrypted. This sets both encrypt_dict_ (for trailer writing)
Expand Down Expand Up @@ -83,6 +95,7 @@ class CPDF_Creator {
bool WriteOldObjs();
bool WriteNewObjs();
bool WriteIndirectObj(uint32_t objnum, const CPDF_Object* pObj);
bool CheckEmittedOffset(FX_FILESIZE offset);

void RemoveSecurity();

Expand All @@ -106,6 +119,8 @@ class CPDF_Creator {
bool security_changed_ = false;
bool is_incremental_ = false;
bool is_original_ = false;
bool is_incremental_append_only_ = false;
FailureReason failure_reason_ = FailureReason::kNone;
};

#endif // CORE_FPDFAPI_EDIT_CPDF_CREATOR_H_
23 changes: 23 additions & 0 deletions core/fpdfapi/edit/cpdf_creator_unittest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright 2026 The PDFium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "core/fpdfapi/edit/cpdf_creator.h"

#include "testing/gtest/include/gtest/gtest.h"

TEST(CPDFCreatorTest, FormatXrefOffset10Is64BitClean) {
ByteString small = CPDF_Creator::FormatXrefOffset10ForTesting(42);
EXPECT_EQ("0000000042", small);
EXPECT_EQ(10u, small.GetLength());

ByteString mid =
CPDF_Creator::FormatXrefOffset10ForTesting(2500000000LL);
EXPECT_EQ("2500000000", mid);
EXPECT_EQ(10u, mid.GetLength());

ByteString max =
CPDF_Creator::FormatXrefOffset10ForTesting(0xffffffff);
EXPECT_EQ("4294967295", max);
EXPECT_EQ(10u, max.GetLength());
}
14 changes: 11 additions & 3 deletions core/fpdfapi/font/cpdf_type3font.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ void CPDF_Type3Font::WillBeDestroyed() {
}

bool CPDF_Type3Font::Load() {
font_resources_ = font_dict_->GetMutableDictFor("Resources");
RetainPtr<const CPDF_Dictionary> font_resources =
font_dict_->GetDictFor("Resources");
font_resources_ =
pdfium::WrapRetain(const_cast<CPDF_Dictionary*>(font_resources.Get()));
RetainPtr<const CPDF_Array> pMatrix = font_dict_->GetArrayFor("FontMatrix");
float xscale = 1.0f;
float yscale = 1.0f;
Expand Down Expand Up @@ -93,7 +96,10 @@ bool CPDF_Type3Font::Load() {
}
}
}
char_procs_ = font_dict_->GetMutableDictFor("CharProcs");
RetainPtr<const CPDF_Dictionary> char_procs =
font_dict_->GetDictFor("CharProcs");
char_procs_ =
pdfium::WrapRetain(const_cast<CPDF_Dictionary*>(char_procs.Get()));
if (font_dict_->GetDirectObjectFor("Encoding")) {
LoadPDFEncoding(false, false);
}
Expand Down Expand Up @@ -125,8 +131,10 @@ CPDF_Type3Char* CPDF_Type3Font::LoadChar(uint32_t charcode) {
return nullptr;
}

RetainPtr<const CPDF_Stream> const_stream =
ToStream(char_procs_->GetDirectObjectFor(name));
RetainPtr<CPDF_Stream> pStream =
ToStream(char_procs_->GetMutableDirectObjectFor(name));
pdfium::WrapRetain(const_cast<CPDF_Stream*>(const_stream.Get()));
if (!pStream) {
return nullptr;
}
Expand Down
Loading