blob: d7f704aa471b222655dd48a19707b98f02ff2a05 [file] [edit]
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#include "formats/pdf/embedder.h"
#include <cstdint>
#include <iterator>
#include <optional>
#include <string>
#include <tuple>
#include <utility>
#include <variant>
#include <vector>
#include "absl/algorithm/container.h"
#include "absl/container/btree_set.h"
#include "absl/status/status.h"
#include "absl/status/status_macros.h"
#include "absl/status/statusor.h"
#include "absl/strings/cord.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_format.h"
#include "absl/strings/string_view.h"
#include "absl/time/clock.h"
#include "absl/time/time.h"
#include "absl/types/span.h"
#include "formats/pdf/constants.h"
#include "formats/pdf/extractor.h"
#include "formats/pdf/object_writer.h"
#include "formats/pdf/objects.h"
#include "formats/pdf/reader.h"
#include "riegeli/bytes/reader.h"
#include "riegeli/bytes/writer.h"
namespace credentio {
namespace {
using Dictionary = Object::Dictionary;
constexpr absl::string_view kPdfHeader = "%PDF-1.7\n%\x90\x91\xC2\x9A\n";
struct XrefTableEntry {
uint32_t object_number;
uint32_t generation_number;
int64_t offset;
bool in_use;
friend bool operator<(const XrefTableEntry& lhs, const XrefTableEntry& rhs) {
return std::tie(lhs.object_number, lhs.generation_number, lhs.offset,
lhs.in_use) < std::tie(rhs.object_number,
rhs.generation_number, rhs.offset,
rhs.in_use);
}
};
template <typename T>
absl::StatusOr<T> DereferenceObject(const Object::IndirectReference& ref,
PdfReader& reader) {
ABSL_ASSIGN_OR_RETURN(
const auto& indirect_obj,
reader.GetObject(ref.object_number, ref.generation_number));
return Object::As<T>(indirect_obj.object);
}
// Using the basic mechanism to form a linked list from the free entries.
// See ISO 32000-2 Section 7.5.4 for more details.
std::vector<XrefTableEntry> ReconstructXrefTableFreeEntries(
const absl::btree_set<Object::IndirectReference>& free_entries) {
if (free_entries.empty()) {
return {};
}
std::vector<XrefTableEntry> xref_table_entries;
xref_table_entries.reserve(free_entries.size());
for (const auto& free_entry : free_entries) {
xref_table_entries.push_back(
XrefTableEntry{.object_number = free_entry.object_number,
.generation_number = free_entry.generation_number,
.offset = 0,
.in_use = false});
}
// Set the offset of each free entry to the object number of the next free
// entry.
for (int i = 0; i < xref_table_entries.size() - 1; ++i) {
xref_table_entries[i].offset = xref_table_entries[i + 1].object_number;
}
// Signify the end of the list by having an offset value of 0.
xref_table_entries.back().offset = 0;
return xref_table_entries;
}
// Returns true if the object numbers in the given vector are consecutive,
// starting from 0. Assumes the vector is already sorted by object number.
bool AreObjectNumbersConsecutive(
absl::Span<const XrefTableEntry> xref_table_entries) {
for (auto i = 0; i < xref_table_entries.size(); i++) {
if (xref_table_entries[i].object_number != i) {
return false;
}
}
return true;
}
absl::Status WritePdfHeader(riegeli::Writer& output) {
if (!output.Write(kPdfHeader)) {
return output.status();
}
return absl::OkStatus();
}
// See ISO 32000-2 Section 7.5.4 for the cross-reference table structure.
absl::Status WriteXrefTable(std::vector<XrefTableEntry>& xref_table_entries,
riegeli::Writer& output) {
// Write the xref header.
if (!output.Write("xref\n")) {
return output.status();
}
absl::c_sort(xref_table_entries);
if (!AreObjectNumbersConsecutive(xref_table_entries)) {
return absl::UnimplementedError(
"Non-consecutive object numbers in the xref table are not supported.");
}
// Write the object count section.
if (!output.Write(absl::StrCat(0, " ", xref_table_entries.size(), "\n"))) {
return output.status();
}
// Write the xref table entries.
for (const auto& entry : xref_table_entries) {
if (!output.Write(absl::StrFormat("%010d %05d %c \n", entry.offset,
entry.generation_number,
entry.in_use ? 'n' : 'f'))) {
return output.status();
}
}
return absl::OkStatus();
}
// See ISO 32000-2 Section 7.5.5 for the trailer structure.
absl::Status WriteTrailer(const Dictionary& trailer_dict,
int64_t xref_table_offset, riegeli::Writer& output) {
// Write the trailer header.
if (!output.Write("trailer\n")) {
return output.status();
}
// Write the trailer dictionary object.
Object obj = Object{.value = trailer_dict};
ABSL_RETURN_IF_ERROR(WriteObject(obj, output));
// Write the trailer footer and the offset to the last cross-reference table.
if (!output.Write(
absl::StrCat("\nstartxref\n", xref_table_offset, "\n%%EOF"))) {
return output.status();
}
return absl::OkStatus();
}
absl::Status CopyObjects(
PdfReader& reader,
absl::Span<const std::optional<Object::IndirectReference>> objects_to_skip,
int64_t start_offset, std::vector<XrefTableEntry>& xref_table_entries,
riegeli::Writer& output) {
while (reader.HasNext()) {
ABSL_ASSIGN_OR_RETURN(auto obj, reader.Next());
bool should_skip = absl::c_any_of(
objects_to_skip,
[&](const std::optional<Object::IndirectReference>& skip_ref) {
return skip_ref.has_value() &&
obj.object_number == skip_ref->object_number &&
obj.generation_number == skip_ref->generation_number;
});
if (should_skip) continue;
int64_t obj_offset = output.pos();
ABSL_RETURN_IF_ERROR(WriteIndirectObject(obj, output));
xref_table_entries.push_back(
XrefTableEntry{.object_number = obj.object_number,
.generation_number = obj.generation_number,
.offset = obj_offset - start_offset,
.in_use = true});
}
return absl::OkStatus();
}
// Creates an indirect stream Object with a zero-filled manifest store.
IndirectObject BuildC2paManifestTemplate(uint32_t object_number,
uint32_t generation_number,
int64_t manifest_store_size) {
IndirectObject store_template;
store_template.object_number = object_number;
store_template.generation_number = generation_number;
std::string formatted_date = absl::FormatTime(
"D:%Y%m%d%H%M%S+00'00'", absl::Now(), absl::UTCTimeZone());
Dictionary params_dict{
.entries = {
{std::string(kPdfSizeKey),
Object{.value = Object::Integer{.value = manifest_store_size}}},
{std::string(kPdfModDateKey),
Object{.value = Object::LiteralString{.raw_value = formatted_date}}},
}};
store_template.stream_dictionary = Dictionary{
.entries = {
{"Length",
Object{.value = Object::Integer{.value = manifest_store_size}}},
{std::string(kPdfSubtypeKey),
Object{.value = Object::Name{.value = std::string(
kPdfC2paManifestMimeType)}}},
{std::string(kPdfParamsKey), Object{.value = std::move(params_dict)}},
}};
store_template.object =
Object{.value = Object::Stream{
.raw_value = std::string(manifest_store_size, '\0')}};
return store_template;
}
IndirectObject BuildC2paManifestFileSpec(
uint32_t object_number, uint32_t generation_number,
const IndirectObject& c2pa_manifest_store_obj) {
IndirectObject file_spec;
file_spec.object_number = object_number;
file_spec.generation_number = generation_number;
file_spec.object = Object{
.value = Dictionary{
.entries = {
{std::string(kPdfTypeKey),
Object{.value = Object::Name{.value = std::string(
kPdfFileSpecType)}}},
{std::string(kPdfAfRelationshipKey),
Object{.value =
Object::Name{.value = std::string(
kPdfC2paManifestAfRelationship)}}},
{std::string(kPdfFKey),
Object{.value =
Object::LiteralString{
.raw_value =
std::string(kPdfC2paEmbeddedFileName)}}},
{std::string(kPdfUfKey),
Object{.value =
Object::LiteralString{
.raw_value =
std::string(kPdfC2paEmbeddedFileName)}}},
{std::string(kPdfEfKey),
Object{
.value = Dictionary{
.entries = {
{std::string(kPdfFKey),
Object{.value = Object::IndirectReference{
.object_number = c2pa_manifest_store_obj
.object_number,
.generation_number =
c2pa_manifest_store_obj
.generation_number}}}}}}}}}};
return file_spec;
}
absl::Status InsertNameTreePair(absl::string_view key,
const Object::IndirectReference& value,
Object::Array& name_tree_array) {
if (name_tree_array.objects.size() % 2 != 0) {
return absl::InternalError(
"Name tree array must contain an even number of objects.");
}
// Find the correct position to insert the new key-value pair to maintain
// lexicographical order of keys. The `name_tree_array` stores keys and
// values interleaved, e.g., [key1, value1, key2, value2, ...].
auto insertion_it = name_tree_array.objects.begin();
while (insertion_it != name_tree_array.objects.end()) {
// We only need to check the keys, which are at even indices.
ABSL_ASSIGN_OR_RETURN(const auto& current_key,
Object::As<Object::LiteralString>(*insertion_it));
if (key < current_key.raw_value) {
// Found the position. The new key should be inserted before this one.
break;
}
// Advance the iterator to the next key.
insertion_it += 2;
}
name_tree_array.objects.insert(
insertion_it,
{Object{.value = Object::LiteralString{.raw_value = std::string(key)}},
Object{.value = value}});
return absl::OkStatus();
}
} // namespace
absl::StatusOr<int64_t> PdfEmbedder::RewriteForEmbedding(
riegeli::Reader& input, int64_t estimated_manifest_store_size,
riegeli::Writer& output) const {
ABSL_ASSIGN_OR_RETURN(auto reader, PdfReader::Create(&input));
if (auto manifest_store = PdfExtractor().ExtractManifestStore(input);
manifest_store.ok()) {
return absl::AlreadyExistsError(
"Rewriting failed because a C2PA manifest store already exists ");
}
// The start offset of the output stream.
int64_t start_offset = output.pos();
// Step 1:
// Locate and identify objects that need to be updated/created for C2PA
// embedding.
auto file_trailer_dict = reader->file_trailer_dictionary();
ABSL_ASSIGN_OR_RETURN(
auto object_count_obj,
FindObjectInDictionary<Object::Integer>(file_trailer_dict, kPdfSizeKey));
uint32_t next_available_object_number = object_count_obj.value;
// /Root
ABSL_ASSIGN_OR_RETURN(auto doc_catalog_ref,
FindObjectInDictionary<Object::IndirectReference>(
file_trailer_dict, kPdfRootKey));
ABSL_ASSIGN_OR_RETURN(auto doc_catalog_dict, DereferenceObject<Dictionary>(
doc_catalog_ref, *reader));
// /Root/Names
std::optional<Dictionary> catalog_names = std::nullopt;
std::optional<Object::IndirectReference> catalog_names_ref = std::nullopt;
if (auto it = doc_catalog_dict.entries.find(kPdfNamesKey);
it != doc_catalog_dict.entries.end()) {
if (std::holds_alternative<Dictionary>(it->second.value)) {
// If we find the catalog names dictionary to be a direct object, move it
// into an indirect object then update the document catalog dictionary to
// point to it.
catalog_names = std::move(std::get<Dictionary>(it->second.value));
catalog_names_ref = Object::IndirectReference{
.object_number = next_available_object_number++,
.generation_number = 0};
doc_catalog_dict.entries[kPdfNamesKey] =
Object{.value = *catalog_names_ref};
} else {
ABSL_ASSIGN_OR_RETURN(catalog_names_ref,
Object::As<Object::IndirectReference>(it->second));
ABSL_ASSIGN_OR_RETURN(
catalog_names,
DereferenceObject<Dictionary>(catalog_names_ref.value(), *reader));
}
}
// /Root/Names/EmbeddedFiles
std::optional<Dictionary> name_tree = std::nullopt;
std::optional<Object::IndirectReference> name_tree_ref = std::nullopt;
if (catalog_names.has_value()) {
if (auto it = catalog_names->entries.find(kPdfEmbeddedFilesKey);
it != catalog_names->entries.end()) {
ABSL_ASSIGN_OR_RETURN(name_tree_ref,
Object::As<Object::IndirectReference>(it->second));
ABSL_ASSIGN_OR_RETURN(name_tree, DereferenceObject<Dictionary>(
name_tree_ref.value(), *reader));
}
}
// Step 2: Write the header.
ABSL_RETURN_IF_ERROR(WritePdfHeader(output));
// Step 3: Copy the objects that can be written as-is from the input stream to
// the output stream.
// A list of xref table entries that will be written to the output stream,
// including both in-use and free entries. In-use entry's offset is relative
// to `start_offset`.
std::vector<XrefTableEntry> xref_table_entries;
ABSL_RETURN_IF_ERROR(CopyObjects(
*reader,
/*objects_to_skip=*/{doc_catalog_ref, catalog_names_ref, name_tree_ref},
start_offset, xref_table_entries, output));
// Step 4.1: Create the embedded file stream object that holds the C2PA
// manifest store.
IndirectObject c2pa_manifest_store_obj = BuildC2paManifestTemplate(
/*object_number=*/next_available_object_number++,
/*generation_number=*/0, estimated_manifest_store_size);
int64_t c2pa_manifest_store_offset = output.pos();
ABSL_RETURN_IF_ERROR(WriteIndirectObject(c2pa_manifest_store_obj, output));
xref_table_entries.push_back(XrefTableEntry{
.object_number = c2pa_manifest_store_obj.object_number,
.generation_number = c2pa_manifest_store_obj.generation_number,
.offset = c2pa_manifest_store_offset - start_offset,
.in_use = true});
// Step 4.2: Create a file specification object for the C2PA manifest
// store.
IndirectObject c2pa_manifest_store_file_spec = BuildC2paManifestFileSpec(
/*object_number=*/next_available_object_number++,
/*generation_number=*/0, c2pa_manifest_store_obj);
int64_t c2pa_manifest_store_file_spec_offset = output.pos();
ABSL_RETURN_IF_ERROR(
WriteIndirectObject(c2pa_manifest_store_file_spec, output));
xref_table_entries.push_back(XrefTableEntry{
.object_number = c2pa_manifest_store_file_spec.object_number,
.generation_number = c2pa_manifest_store_file_spec.generation_number,
.offset = c2pa_manifest_store_file_spec_offset - start_offset,
.in_use = true});
// Step 4.3: Add/Update the embedded files name tree.
if (!name_tree.has_value()) {
// Since we don't find an existing name tree in the input PDF, we can safely
// create a new one.
name_tree = Dictionary{};
name_tree_ref = Object::IndirectReference{
.object_number = next_available_object_number++,
.generation_number = 0};
}
auto name_tree_it = name_tree->entries.find(kPdfNamesKey);
if (name_tree_it == name_tree->entries.end() ||
!std::holds_alternative<Object::Array>(name_tree_it->second.value)) {
name_tree_it =
name_tree->entries
.insert_or_assign(kPdfNamesKey, Object{.value = Object::Array{}})
.first;
}
Object::Array& name_tree_array =
std::get<Object::Array>(name_tree_it->second.value);
ABSL_RETURN_IF_ERROR(InsertNameTreePair(
kPdfC2paEmbeddedFileName,
Object::IndirectReference{
.object_number = c2pa_manifest_store_file_spec.object_number,
.generation_number = c2pa_manifest_store_file_spec.generation_number},
name_tree_array));
int64_t name_tree_offset = output.pos();
ABSL_RETURN_IF_ERROR(WriteIndirectObject(
IndirectObject{.object_number = name_tree_ref->object_number,
.generation_number = name_tree_ref->generation_number,
.object = Object{.value = *name_tree}},
output));
xref_table_entries.push_back(
XrefTableEntry{.object_number = name_tree_ref->object_number,
.generation_number = name_tree_ref->generation_number,
.offset = name_tree_offset - start_offset,
.in_use = true});
// Step 4.4: Add/Update the catalog names dictionary.
if (!catalog_names.has_value()) {
// Since we don't find an existing catalog names dictionary in the input
// PDF, we can safely create a new one.
catalog_names = Dictionary{};
catalog_names_ref = Object::IndirectReference{
.object_number = next_available_object_number++,
.generation_number = 0};
doc_catalog_dict.entries[kPdfNamesKey] =
Object{.value = *catalog_names_ref};
}
if (!catalog_names->entries.contains(kPdfEmbeddedFilesKey)) {
catalog_names->entries[kPdfEmbeddedFilesKey] =
Object{.value = *name_tree_ref};
}
int64_t catalog_names_offset = output.pos();
ABSL_RETURN_IF_ERROR(WriteIndirectObject(
IndirectObject{.object_number = catalog_names_ref->object_number,
.generation_number = catalog_names_ref->generation_number,
.object = Object{.value = *catalog_names}},
output));
xref_table_entries.push_back(
XrefTableEntry{.object_number = catalog_names_ref->object_number,
.generation_number = catalog_names_ref->generation_number,
.offset = catalog_names_offset - start_offset,
.in_use = true});
// Step 4.5: Update the document catalog dictionary.
auto af_it = doc_catalog_dict.entries.find(kPdfAfKey);
if (af_it == doc_catalog_dict.entries.end() ||
!std::holds_alternative<Object::Array>(af_it->second.value)) {
af_it = doc_catalog_dict.entries
.insert_or_assign(kPdfAfKey, Object{.value = Object::Array{}})
.first;
}
std::get<Object::Array>(af_it->second.value)
.objects.push_back(Object{
.value = Object::IndirectReference{
.object_number = c2pa_manifest_store_file_spec.object_number,
.generation_number =
c2pa_manifest_store_file_spec.generation_number}});
int64_t doc_catalog_dict_offset = output.pos();
ABSL_RETURN_IF_ERROR(WriteIndirectObject(
IndirectObject{.object_number = doc_catalog_ref.object_number,
.generation_number = doc_catalog_ref.generation_number,
.object = Object{.value = doc_catalog_dict}},
output));
xref_table_entries.push_back(
XrefTableEntry{.object_number = doc_catalog_ref.object_number,
.generation_number = doc_catalog_ref.generation_number,
.offset = doc_catalog_dict_offset - start_offset,
.in_use = true});
// Step 5: Compute and write the xref table.
int64_t xref_table_offset = output.pos() - start_offset;
// Add all existing free entries to the xref table.
auto xref_table_free_entries =
ReconstructXrefTableFreeEntries(reader->free_entries());
xref_table_entries.insert(
xref_table_entries.end(),
std::make_move_iterator(xref_table_free_entries.begin()),
std::make_move_iterator(xref_table_free_entries.end()));
ABSL_RETURN_IF_ERROR(WriteXrefTable(xref_table_entries, output));
// Step 6: Write the trailer section, including the trailer dictionary and
// startxref pointer.
// Update the object count in the trailer dictionary.
file_trailer_dict.entries[kPdfSizeKey] =
Object{.value = Object::Integer{
.value = static_cast<int64_t>(xref_table_entries.size())}};
ABSL_RETURN_IF_ERROR(
WriteTrailer(file_trailer_dict, xref_table_offset, output));
ABSL_ASSIGN_OR_RETURN(int64_t header_len, ComputeIndirectStreamHeaderByteSize(
c2pa_manifest_store_obj));
return c2pa_manifest_store_offset + header_len;
}
absl::Status PdfEmbedder::Embed(absl::string_view manifest_store,
int64_t insert_at_offset,
absl::Cord& embeddable_asset) const {
absl::Cord result = embeddable_asset.Subcord(0, insert_at_offset);
result.Append(manifest_store);
result.Append(embeddable_asset.Subcord(
insert_at_offset + manifest_store.size(), embeddable_asset.size()));
embeddable_asset = std::move(result);
return absl::OkStatus();
}
} // namespace credentio