| // 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/extractor.h" |
| |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <variant> |
| |
| #include "absl/algorithm/container.h" |
| #include "absl/status/status.h" |
| #include "absl/status/statusor.h" |
| #include "absl/strings/str_cat.h" |
| #include "absl/strings/string_view.h" |
| #include "constants/labels.h" |
| #include "formats/byte_range.h" |
| #include "formats/pdf/constants.h" |
| #include "formats/pdf/objects.h" |
| #include "formats/pdf/reader.h" |
| #include "jumbf/utils.h" |
| #if defined(__GNUC__) || defined(__clang__) |
| #include <cxxabi.h> // IWYU pragma: keep |
| |
| #include <cstdlib> |
| #include <memory> |
| #endif |
| #include "absl/status/status_macros.h" |
| #include "riegeli/bytes/reader.h" |
| |
| namespace credentio { |
| namespace { |
| |
| std::string Demangle(const char* mangled) { |
| #if defined(__GNUC__) || defined(__clang__) |
| int status = 0; |
| std::unique_ptr<char, void (*)(void*)> res{ |
| abi::__cxa_demangle(mangled, nullptr, nullptr, &status), std::free}; |
| return (status == 0) ? res.get() : mangled; |
| #else |
| return mangled; |
| #endif |
| } |
| |
| struct C2paManifestStore { |
| ByteRange location; |
| std::string content; |
| }; |
| |
| // Checks if `file_spec_dict` is a file spec dictionary for an embedded C2PA |
| // file stream. |
| absl::StatusOr<bool> IsC2paManifestFileSpec( |
| const Object::Dictionary& file_spec_dict) { |
| const auto af_relationship = FindObjectInDictionary<Object::Name>( |
| file_spec_dict, kPdfAfRelationshipKey); |
| if (!af_relationship.ok()) { |
| if (absl::IsNotFound(af_relationship.status())) { |
| return false; |
| } |
| return af_relationship.status(); |
| } |
| return af_relationship->value == kPdfC2paManifestAfRelationship; |
| } |
| |
| // Checks that the embedded file stream subtype is as expected. |
| absl::StatusOr<bool> IsC2paManifestSubtype( |
| const IndirectObject& embedded_file_obj) { |
| if (!embedded_file_obj.stream_dictionary.has_value()) { |
| return false; |
| } |
| const auto subtype = FindObjectInDictionary<Object::Name>( |
| *embedded_file_obj.stream_dictionary, kPdfSubtypeKey); |
| if (!subtype.ok()) { |
| // Optionally check the subtype if it exists as it is only required in C2PA |
| // v2.2 onward. |
| if (absl::IsNotFound(subtype.status())) { |
| return true; |
| } |
| return subtype.status(); |
| } |
| return subtype->value == kPdfC2paManifestMimeType; |
| } |
| |
| // Gets the indirect reference to the embedded file stream from the file spec |
| // dictionary under the '/EF/F' key. |
| absl::StatusOr<Object::IndirectReference> GetEmbeddedFileRef( |
| const Object::Dictionary& file_spec_dict) { |
| ABSL_ASSIGN_OR_RETURN( |
| const auto ef_dict, |
| FindObjectInDictionary<Object::Dictionary>(file_spec_dict, kPdfEfKey)); |
| |
| return FindObjectInDictionary<Object::IndirectReference>(ef_dict, kPdfFKey); |
| } |
| |
| // Gets the array of embedded file name and indirect reference pairs from the |
| // catalog dictionary under the '/Names/EmbeddedFiles/Names' key. |
| absl::StatusOr<Object::Array> GetEmbeddedFilesNameTreeArray( |
| const Object::Dictionary& catalog_dict, PdfReader& reader) { |
| if (auto it = catalog_dict.entries.find(kPdfNamesKey); |
| it != catalog_dict.entries.end()) { |
| // /Catalog/Names |
| Object::Dictionary names_dict; |
| if (std::holds_alternative<Object::IndirectReference>(it->second.value)) { |
| const auto& names_ref = |
| std::get<Object::IndirectReference>(it->second.value); |
| ABSL_ASSIGN_OR_RETURN(const auto names_obj, |
| reader.GetObject(names_ref.object_number, |
| names_ref.generation_number)); |
| ABSL_ASSIGN_OR_RETURN(names_dict, |
| Object::As<Object::Dictionary>(names_obj.object)); |
| } else if (std::holds_alternative<Object::Dictionary>(it->second.value)) { |
| names_dict = std::get<Object::Dictionary>(it->second.value); |
| } else { |
| return absl::InvalidArgumentError( |
| absl::StrCat("Unexpected object type found. Expected " |
| "IndirectReference or Dictionary, but got ", |
| Demangle(typeid(it->second.value).name()))); |
| } |
| // /Catalog/Names/EmbeddedFiles |
| auto name_tree_it = names_dict.entries.find(kPdfEmbeddedFilesKey); |
| if (name_tree_it == names_dict.entries.end()) { |
| return absl::NotFoundError("No '/EmbeddedFiles' dictionary found."); |
| } |
| Object::Dictionary name_tree_dict; |
| if (std::holds_alternative<Object::IndirectReference>( |
| name_tree_it->second.value)) { |
| const auto& name_tree_ref = |
| std::get<Object::IndirectReference>(name_tree_it->second.value); |
| ABSL_ASSIGN_OR_RETURN(const auto name_tree_obj, |
| reader.GetObject(name_tree_ref.object_number, |
| name_tree_ref.generation_number)); |
| ABSL_ASSIGN_OR_RETURN( |
| name_tree_dict, Object::As<Object::Dictionary>(name_tree_obj.object)); |
| } else if (std::holds_alternative<Object::Dictionary>( |
| name_tree_it->second.value)) { |
| name_tree_dict = std::get<Object::Dictionary>(name_tree_it->second.value); |
| } else { |
| return absl::InvalidArgumentError( |
| absl::StrCat("Unexpected object type found. Expected " |
| "IndirectReference or Dictionary, but got ", |
| Demangle(typeid(name_tree_it->second.value).name()))); |
| } |
| // /Catalog/Names/EmbeddedFiles/Names |
| return FindObjectInDictionary<Object::Array>(name_tree_dict, kPdfNamesKey); |
| } else { |
| return absl::NotFoundError( |
| "No '/Names' dictionary found in the document catalog."); |
| } |
| } |
| |
| absl::StatusOr<C2paManifestStore> ExtractManifestStoreInternal( |
| riegeli::Reader& input) { |
| ABSL_ASSIGN_OR_RETURN(auto reader, PdfReader::Create(&input)); |
| |
| // Locate the active manifest store in the file trailer dictionary. |
| const Object::Dictionary& file_trailer_dict = |
| reader->file_trailer_dictionary(); |
| |
| ABSL_ASSIGN_OR_RETURN(const auto root_ref, |
| FindObjectInDictionary<Object::IndirectReference>( |
| file_trailer_dict, kPdfRootKey)); |
| |
| ABSL_ASSIGN_OR_RETURN( |
| auto root_obj, |
| reader->GetObject(root_ref.object_number, root_ref.generation_number)); |
| ABSL_ASSIGN_OR_RETURN(auto catalog_dict, |
| Object::As<Object::Dictionary>(root_obj.object)); |
| |
| const auto& file_spec_array = |
| FindObjectInDictionary<Object::Array>(catalog_dict, kPdfAfKey); |
| if (absl::IsNotFound(file_spec_array.status())) { |
| // Early return if the document catalog dictionary does not contain the |
| // required '/AF' entry for C2PA manifest extraction. |
| return absl::NotFoundError("No manifest store found."); |
| } |
| ABSL_RETURN_IF_ERROR(file_spec_array.status()); |
| |
| ABSL_ASSIGN_OR_RETURN(auto name_tree_array, |
| GetEmbeddedFilesNameTreeArray(catalog_dict, *reader)); |
| |
| for (const auto& file_spec_obj : file_spec_array->objects) { |
| Object::Dictionary file_spec_dict; |
| Object::IndirectReference file_spec_ref; |
| if (std::holds_alternative<Object::IndirectReference>( |
| file_spec_obj.value)) { |
| // C2PA manifest file spec is expected to be referenced indirectly from |
| // the '/AF' array. |
| file_spec_ref = std::get<Object::IndirectReference>(file_spec_obj.value); |
| ABSL_ASSIGN_OR_RETURN(auto file_spec_dict_obj, |
| reader->GetObject(file_spec_ref.object_number, |
| file_spec_ref.generation_number)); |
| ABSL_ASSIGN_OR_RETURN(file_spec_dict, Object::As<Object::Dictionary>( |
| file_spec_dict_obj.object)); |
| } else { |
| // Not an indirect reference, continue searching. |
| continue; |
| } |
| |
| ABSL_ASSIGN_OR_RETURN(auto is_c2pa_manifest_file_spec, |
| IsC2paManifestFileSpec(file_spec_dict)); |
| if (!is_c2pa_manifest_file_spec) { |
| continue; |
| } |
| ABSL_ASSIGN_OR_RETURN(auto embedded_file_ref, |
| GetEmbeddedFileRef(file_spec_dict)); |
| |
| // The C2PA manifest file spec object should also be referenced from the |
| // '/EmbeddedFiles' NameTree. |
| if (absl::c_any_of(name_tree_array.objects, [&](const auto& name_tree_obj) { |
| return std::holds_alternative<Object::IndirectReference>( |
| name_tree_obj.value) && |
| std::get<Object::IndirectReference>(name_tree_obj.value) |
| .object_number == file_spec_ref.object_number && |
| std::get<Object::IndirectReference>(name_tree_obj.value) |
| .generation_number == file_spec_ref.generation_number; |
| })) { |
| // The embedded file reference was found in the NameTree. |
| // Proceed with extraction. |
| } else { |
| // The embedded file reference was NOT found in the NameTree. |
| continue; |
| } |
| |
| ABSL_ASSIGN_OR_RETURN( |
| auto embedded_file_obj, |
| reader->GetObject(embedded_file_ref.object_number, |
| embedded_file_ref.generation_number)); |
| ABSL_ASSIGN_OR_RETURN(auto is_c2pa_manifest_subtype, |
| IsC2paManifestSubtype(embedded_file_obj)); |
| if (!is_c2pa_manifest_subtype) { |
| continue; |
| } |
| ABSL_ASSIGN_OR_RETURN(auto embedded_file_stream, |
| Object::As<Object::Stream>(embedded_file_obj.object)); |
| ABSL_ASSIGN_OR_RETURN( |
| auto embedded_file_offset, |
| reader->GetObjectOffset(embedded_file_ref.object_number, |
| embedded_file_ref.generation_number)); |
| return C2paManifestStore{ |
| .location = {.offset = embedded_file_offset, |
| .length = input.pos() - embedded_file_offset}, |
| .content = embedded_file_stream.raw_value}; |
| } |
| return absl::NotFoundError("No manifest store found."); |
| } |
| } // namespace |
| |
| absl::StatusOr<std::string> PdfExtractor::ExtractManifestStore( |
| riegeli::Reader& input) const { |
| ABSL_ASSIGN_OR_RETURN(auto c2pa_manifest_store, |
| ExtractManifestStoreInternal(input)); |
| return c2pa_manifest_store.content; |
| } |
| |
| absl::StatusOr<std::optional<ByteRange>> |
| PdfExtractor::ExtractManifestStoreLocation(riegeli::Reader& input, |
| ExtractOptions options) const { |
| auto c2pa_manifest_store = ExtractManifestStoreInternal(input); |
| |
| // Extraction failed but C2PA is required, return an error. |
| if (!c2pa_manifest_store.ok() && options.requires_c2pa) { |
| return absl::NotFoundError("No manifest store found"); |
| } |
| // Extraction failed but C2PA is not required, return nullopt. |
| if (!c2pa_manifest_store.ok() && !options.requires_c2pa) { |
| return std::nullopt; |
| } |
| // Extraction succeeded, return the location. |
| return c2pa_manifest_store->location; |
| } |
| |
| bool PdfExtractor::MightBeC2paManifestStore(absl::string_view payload) const { |
| return jumbf::HasDescriptionBoxMatching(payload, kManifestStoreUuid, |
| kMinimumJumbfDescriptionToggles, |
| kManifestStoreLabel) |
| .value_or(false); |
| } |
| } // namespace credentio |