| // 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/reader.h" |
| |
| #include <algorithm> |
| #include <cstdint> |
| #include <optional> |
| #include <ostream> |
| |
| #include "absl/status/status.h" |
| #include "absl/status/status_macros.h" |
| #include "absl/status/statusor.h" |
| #include "absl/strings/match.h" |
| #include "absl/strings/str_cat.h" |
| #include "formats/jpeg/box_header.h" |
| #include "riegeli/bytes/reader.h" |
| #include "riegeli/endian/endian_reading.h" |
| |
| namespace credentio { |
| namespace { |
| |
| constexpr uint16_t kApp11ExtensionType = 0x4a50; |
| |
| // Returns the start of the next chunk following the scan data that starts at |
| // `offset`. |
| absl::StatusOr<int64_t> FindEndOfScan(riegeli::Reader& input, int64_t offset) { |
| if (!input.Seek(offset)) { |
| return input.StatusOrAnnotate( |
| absl::DataLossError("Failed to seek to offset")); |
| } |
| |
| uint8_t first = 0; |
| uint8_t second = 0; |
| while (true) { |
| if (first == 0xff && second != 0) { |
| break; |
| } |
| first = second; |
| if (!input.ReadByte(second)) { |
| return input.StatusOrAnnotate(absl::DataLossError("Failed to read byte")); |
| } |
| } |
| return input.pos() - 2; // Back up to start of chunk header. |
| } |
| |
| absl::Status PopulateJumbfData(riegeli::Reader& reader, JpegSegment& segment) { |
| if (!segment.app11_info.has_value()) { |
| // Will never happen as it's always called from PopulateApp11Info |
| return absl::InvalidArgumentError( |
| "APP11 info not populated, cannot populate JUMBf data"); |
| } |
| |
| uint64_t header_size = 8; // lbox + tbox |
| if (segment.app11_info->payload_length < header_size) { |
| // Not enough bytes for JUMBf header. |
| return absl::OkStatus(); |
| } |
| |
| segment.app11_info->jumbf_data = JpegJumbfData{}; |
| if (!riegeli::ReadBigEndian<uint32_t>(reader, |
| segment.app11_info->jumbf_data->lbox)) { |
| return reader.StatusOrAnnotate(absl::DataLossError("Failed to read lbox")); |
| } |
| if (!riegeli::ReadBigEndian<uint32_t>(reader, |
| segment.app11_info->jumbf_data->tbox)) { |
| return reader.StatusOrAnnotate(absl::DataLossError("Failed to read tbox")); |
| } |
| if (segment.app11_info->jumbf_data->lbox == 1) { |
| header_size += sizeof(uint64_t); |
| |
| if (segment.app11_info->payload_length < header_size) { |
| return absl::InvalidArgumentError( |
| "JUMBf box payload too short to contain xlbox"); |
| } |
| if (!riegeli::ReadBigEndian<uint64_t>( |
| reader, segment.app11_info->jumbf_data->xlbox)) { |
| return reader.StatusOrAnnotate( |
| absl::DataLossError("Failed to read xlbox")); |
| } |
| } |
| |
| uint64_t jumbf_size = segment.app11_info->jumbf_data->lbox == 1 |
| ? segment.app11_info->jumbf_data->xlbox |
| : segment.app11_info->jumbf_data->lbox; |
| if (jumbf_size < header_size) { |
| return absl::InvalidArgumentError("JUMBf box payload too short"); |
| } |
| |
| segment.app11_info->jumbf_data->payload_offset = |
| segment.app11_info->payload_offset + header_size; |
| segment.app11_info->jumbf_data->payload_length = |
| segment.app11_info->payload_length - header_size; |
| return absl::OkStatus(); |
| } |
| |
| absl::Status PopulateApp11Info(riegeli::Reader& reader, JpegSegment& segment) { |
| // APP11 segment payload must be at least 8 bytes long |
| if (segment.payload_length < 8) { |
| // Return OK without populating app11_info, treating it as non-C2PA. |
| return absl::OkStatus(); |
| } |
| |
| // Seek to the start of the APP11 header. |
| if (!reader.Seek(segment.payload_offset)) { |
| return reader.StatusOrAnnotate( |
| absl::DataLossError("Failed to seek to offset")); |
| } |
| |
| segment.app11_info = JpegApp11Segment{}; |
| if (!riegeli::ReadBigEndian<uint16_t>(reader, |
| segment.app11_info->extension_type)) { |
| return reader.StatusOrAnnotate( |
| absl::DataLossError("Failed to read extension type")); |
| } |
| if (!riegeli::ReadBigEndian<uint16_t>(reader, |
| segment.app11_info->segment_id)) { |
| return reader.StatusOrAnnotate( |
| absl::DataLossError("Failed to read segment ID")); |
| } |
| if (!riegeli::ReadBigEndian<uint32_t>(reader, |
| segment.app11_info->sequence_number)) { |
| return reader.StatusOrAnnotate( |
| absl::DataLossError("Failed to read sequence number")); |
| } |
| |
| segment.app11_info->payload_offset = segment.payload_offset + 8; |
| segment.app11_info->payload_length = segment.payload_length - 8; |
| |
| if (segment.app11_info->extension_type == kApp11ExtensionType) { |
| return PopulateJumbfData(reader, segment); |
| } |
| return absl::OkStatus(); |
| } |
| |
| } // namespace |
| |
| absl::Status IterateOverJpegSegments(riegeli::Reader& reader, |
| JpegSegmentProcessor processor, |
| int64_t end_offset) { |
| uint64_t current_offset = reader.pos(); |
| uint64_t actual_end_offset = |
| end_offset == -1 |
| ? reader.Size().value_or(0) |
| : std::min(static_cast<uint64_t>(reader.Size().value_or(0)), |
| static_cast<uint64_t>(end_offset)); |
| |
| while (current_offset < actual_end_offset) { |
| if (!reader.Seek(current_offset)) { |
| return reader.StatusOrAnnotate( |
| absl::DataLossError("Failed to seek to offset")); |
| } |
| |
| JpegBoxHeader box_header; |
| if (auto result = ConsumeJpegBoxHeader(reader); result.ok()) { |
| box_header = *result; |
| } else { |
| return result.status(); |
| } |
| |
| JpegSegment segment = { |
| .offset = box_header.offset, |
| .marker = box_header.type, |
| .marker_label = box_header.label(), |
| .length = box_header.size, |
| .payload_offset = box_header.size < 4 |
| ? box_header.offset + box_header.size |
| : box_header.offset + 4, |
| .payload_length = box_header.size < 4 ? 0 : box_header.size - 4, |
| }; |
| |
| if (segment.marker_label == "SOS" || |
| absl::StartsWith(segment.marker_label, "RST")) { |
| // Chunk is followed by indefinite-length image data, which is |
| // included in the box content for hashing purposes. |
| uint64_t next_chunk_start = 0; |
| if (auto result = FindEndOfScan(reader, segment.offset + segment.length); |
| result.ok()) { |
| next_chunk_start = *result; |
| } else { |
| return result.status(); |
| } |
| segment.length = next_chunk_start - segment.offset; |
| segment.payload_length = next_chunk_start - segment.payload_offset; |
| // Return to the start of the payload. |
| if (!reader.Seek(segment.payload_offset)) { |
| return reader.StatusOrAnnotate( |
| absl::DataLossError("Failed to seek to offset")); |
| } |
| } |
| |
| if (segment.marker_label == "APP11") { |
| ABSL_RETURN_IF_ERROR(PopulateApp11Info(reader, segment)); |
| } |
| |
| if (segment.length > actual_end_offset - segment.offset && |
| segment.payload_length > 0) { |
| // This box is truncated. |
| return absl::InvalidArgumentError("truncated JPEG box"); |
| } |
| current_offset = segment.offset + segment.length; |
| |
| bool continue_processing = true; |
| if (auto result = processor(segment); result.ok()) { |
| continue_processing = *result; |
| } else { |
| return result.status(); |
| } |
| if (!continue_processing) { |
| return absl::OkStatus(); |
| } |
| |
| if (segment.marker_label == "EOI") { |
| break; |
| } |
| } |
| |
| if (current_offset < actual_end_offset) { |
| // End of first image, any additional data is just a `c2pa.after` segment. |
| JpegSegment after_segment = |
| JpegSegment{.offset = current_offset, |
| .marker = 0x0000, |
| .marker_label = "c2pa.after", |
| .length = actual_end_offset - current_offset, |
| .payload_offset = 0, |
| .payload_length = 0}; |
| return processor(after_segment).status(); |
| } |
| |
| return absl::OkStatus(); |
| } |
| |
| void PrintTo(const JpegJumbfData& x, ::std::ostream* os) { |
| *os << absl::StrCat("{lbox: ", x.lbox, ", tbox: ", x.tbox, |
| ", xlbox: ", x.xlbox, |
| ", payload_offset: ", x.payload_offset, |
| ", payload_length: ", x.payload_length, "}"); |
| } |
| |
| void PrintTo(const JpegApp11Segment& x, ::std::ostream* os) { |
| *os << "{extension_type: " << x.extension_type |
| << ", segment_id: " << x.segment_id |
| << ", sequence_number: " << x.sequence_number |
| << ", payload_offset: " << x.payload_offset |
| << ", payload_length: " << x.payload_length << ", jumbf_data: "; |
| if (x.jumbf_data.has_value()) { |
| PrintTo(*x.jumbf_data, os); |
| } else { |
| *os << "nullopt"; |
| } |
| *os << "}"; |
| } |
| |
| void PrintTo(const JpegSegment& x, ::std::ostream* os) { |
| *os << "{offset: " << x.offset << ", marker: " << x.marker |
| << ", marker_label: \"" << x.marker_label << "\"" |
| << ", length: " << x.length << ", payload_offset: " << x.payload_offset |
| << ", payload_length: " << x.payload_length << ", app11_info: "; |
| if (x.app11_info.has_value()) { |
| PrintTo(*x.app11_info, os); |
| } else { |
| *os << "nullopt"; |
| } |
| *os << "}"; |
| } |
| |
| } // namespace credentio |