| // 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/jpeg/extractor.h" | |
| #include <cstdint> | |
| #include <optional> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_macros.h" | |
| #include "absl/status/statusor.h" | |
| #include "absl/strings/string_view.h" | |
| #include "constants/labels.h" | |
| #include "formats/asset_box.h" | |
| #include "formats/byte_range.h" | |
| #include "formats/jpeg/c2pa_segment_handler.h" | |
| #include "formats/jpeg/reader.h" | |
| #include "jumbf/utils.h" | |
| #include "riegeli/bytes/reader.h" | |
| #include "utils/byte_readers.h" | |
| namespace credentio { | |
| namespace { | |
| // APP11 Marker and APP11 JPEG Extension Type values are sourced from | |
| // Section A.4 JPEG XT boxes of the ISO/IEC 18477-3:2023 | |
| const uint16_t kApp11ExtensionType = 0x4a50; | |
| } // namespace | |
| absl::StatusOr<std::string> JpegExtractor::ExtractManifestStore( | |
| riegeli::Reader& input) const { | |
| C2paSegmentHandler segment_handler; | |
| ABSL_RETURN_IF_ERROR( | |
| IterateOverJpegSegments(input, [&segment_handler](JpegSegment segment) { | |
| segment_handler.RecordSegment(std::move(segment)); | |
| return true; | |
| })); | |
| ABSL_ASSIGN_OR_RETURN(auto c2pa_segment, | |
| segment_handler.GetC2paSegment(input)); | |
| return std::move(c2pa_segment.manifest_store); | |
| } | |
| absl::StatusOr<std::optional<ByteRange>> | |
| JpegExtractor::ExtractManifestStoreLocation(riegeli::Reader& input, | |
| ExtractOptions options) const { | |
| C2paSegmentHandler c2pa_handler; | |
| ABSL_RETURN_IF_ERROR(IterateOverJpegSegments( | |
| input, | |
| [&c2pa_handler, &options, &input](JpegSegment segment) { | |
| if (options.end_offset >= 0 && | |
| (segment.offset + segment.length) > options.end_offset) { | |
| // This segment starts or ends beyond the window, ignore, stop reading | |
| // and move the input's position to the declared end. | |
| if (!input.Seek(options.end_offset)) { | |
| return absl::StatusOr<bool>(input.StatusOrAnnotate( | |
| absl::DataLossError("Failed to seek to end offset"))); | |
| } | |
| return absl::StatusOr<bool>(false); // stop iteration | |
| } | |
| c2pa_handler.RecordSegment(std::move(segment)); | |
| return absl::StatusOr<bool>(true); | |
| }, | |
| options.end_offset)); | |
| absl::StatusOr<C2paSegmentHandler::Segment> c2pa_segment = | |
| c2pa_handler.GetC2paSegment(input); | |
| if (!c2pa_segment.ok()) { | |
| if (options.requires_c2pa) { | |
| return c2pa_segment.status(); | |
| } | |
| return std::nullopt; | |
| } | |
| return ByteRange{ | |
| .offset = c2pa_segment->segment.offset, | |
| .length = c2pa_segment->segment.length, | |
| }; | |
| } | |
| absl::StatusOr<std::vector<AssetBox>> JpegExtractor::ExtractBoxes( | |
| riegeli::Reader& input, ExtractOptions options) const { | |
| C2paSegmentHandler c2pa_handler; | |
| std::vector<AssetBox> boxes; | |
| ABSL_RETURN_IF_ERROR(IterateOverJpegSegments( | |
| input, | |
| [&boxes, &c2pa_handler](JpegSegment segment) { | |
| boxes.push_back(AssetBox{ | |
| .identifier = segment.marker_label, | |
| .byte_range = {.offset = segment.offset, .length = segment.length}, | |
| }); | |
| c2pa_handler.RecordSegment(std::move(segment)); | |
| return true; | |
| }, | |
| options.end_offset)); | |
| absl::StatusOr<C2paSegmentHandler::Segment> c2pa_segment = | |
| c2pa_handler.GetC2paSegment(input); | |
| if (!c2pa_segment.ok()) { | |
| if (options.requires_c2pa) { | |
| return c2pa_segment.status(); | |
| } | |
| return boxes; | |
| } | |
| uint64_t c2pa_start = c2pa_segment->segment.offset; | |
| uint64_t c2pa_end = c2pa_start + c2pa_segment->segment.length; | |
| std::vector<AssetBox> merged_boxes; | |
| merged_boxes.reserve(boxes.size()); | |
| for (const auto& box : boxes) { | |
| uint64_t box_start = box.byte_range.offset; | |
| uint64_t box_end = box_start + box.byte_range.length; | |
| if (box_end <= c2pa_start || box_start >= c2pa_end) { | |
| // Box is before or after the C2PA segment, add it. | |
| merged_boxes.push_back(std::move(box)); | |
| continue; | |
| } | |
| if (box_end == c2pa_end) { | |
| // Box is the last C2PA Segment, add the box | |
| merged_boxes.push_back(AssetBox{ | |
| .identifier = "C2PA", | |
| .byte_range = {.offset = c2pa_start, | |
| .length = c2pa_segment->segment.length}, | |
| }); | |
| continue; | |
| } | |
| // Box is inside the C2PA segment, skip it. | |
| } | |
| return merged_boxes; | |
| } | |
| bool JpegExtractor::MightBeC2paManifestStore(absl::string_view payload) const { | |
| absl::StatusOr<uint16_t> extension_type = ConsumeUint<uint16_t>(&payload); | |
| if (!extension_type.ok() || *extension_type != kApp11ExtensionType) { | |
| return false; | |
| } | |
| // Segment ID | |
| auto iter_status = SkipBytes<uint16_t>(&payload); | |
| if (!iter_status.ok()) { | |
| return false; | |
| } | |
| // Sequence Number | |
| iter_status = SkipBytes<uint32_t>(&payload); | |
| if (!iter_status.ok()) { | |
| return false; | |
| } | |
| return jumbf::HasDescriptionBoxMatching(payload, kManifestStoreUuid, | |
| kMinimumJumbfDescriptionToggles, | |
| kManifestStoreLabel) | |
| .value_or(false); | |
| } | |
| } // namespace credentio |