blob: 1c17dd53ae408d1a11ed88efacdb7668ad54f53d [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/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