| // 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/app11_io.h" | |
| #include <cstddef> | |
| #include <cstdint> | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_macros.h" | |
| #include "absl/status/statusor.h" | |
| #include "absl/strings/string_view.h" | |
| #include "absl/strings/substitute.h" | |
| #include "riegeli/bytes/reader.h" | |
| #include "riegeli/bytes/string_reader.h" | |
| #include "riegeli/bytes/writer.h" | |
| #include "riegeli/endian/endian_reading.h" | |
| #include "riegeli/endian/endian_writing.h" | |
| namespace credentio { | |
| namespace { | |
| constexpr uint16_t kMaxPayloadSegmentByteSize = 65000; | |
| struct JumbfHeader { | |
| uint32_t lbox; | |
| uint32_t tbox; | |
| uint64_t xlbox; | |
| uint64_t byte_size() const { | |
| return sizeof(lbox) + sizeof(tbox) + (lbox == 1 ? sizeof(xlbox) : 0); | |
| } | |
| uint64_t box_size() const { return lbox == 1 ? xlbox : lbox; } | |
| }; | |
| absl::StatusOr<JumbfHeader> ConsumePayloadJumbfHeader(riegeli::Reader& reader) { | |
| JumbfHeader header; | |
| if (!riegeli::ReadBigEndian<uint32_t>(reader, header.lbox)) { | |
| return reader.StatusOrAnnotate(absl::DataLossError("Failed to read lbox")); | |
| } | |
| if (!riegeli::ReadBigEndian<uint32_t>(reader, header.tbox)) { | |
| return reader.StatusOrAnnotate(absl::DataLossError("Failed to read tbox")); | |
| } | |
| if (header.lbox == 1) { | |
| if (!riegeli::ReadBigEndian<uint64_t>(reader, header.xlbox)) { | |
| return reader.StatusOrAnnotate( | |
| absl::DataLossError("Failed to read xlbox")); | |
| } | |
| } | |
| return header; | |
| } | |
| absl::Status WritePayloadChunk(const JumbfHeader& header, | |
| absl::string_view payload_chunk, | |
| uint16_t segment_id, uint32_t sequence_num, | |
| riegeli::Writer& output) { | |
| if (!riegeli::WriteBigEndian<uint16_t>(0xFFEB, output)) { | |
| return output.status(); | |
| } | |
| uint64_t total_seg_length = | |
| sizeof(uint16_t) /*seg_length*/ + sizeof(uint16_t) /*ext_type*/ + | |
| sizeof(uint16_t) /*segment_id*/ + sizeof(uint32_t) /*sequence_num*/ + | |
| header.byte_size() + payload_chunk.size(); | |
| if (total_seg_length > UINT16_MAX) { | |
| return absl::OutOfRangeError("maximum segment size too large"); | |
| } | |
| uint16_t seg_length = static_cast<uint16_t>(total_seg_length); | |
| if (!riegeli::WriteBigEndian<uint16_t>(seg_length, output)) { | |
| return output.status(); | |
| } | |
| // JPEG extension type. | |
| if (!riegeli::WriteBigEndian<uint16_t>(0x4A50, output)) { | |
| return output.status(); | |
| } | |
| if (!riegeli::WriteBigEndian<uint16_t>(segment_id, output)) { | |
| return output.status(); | |
| } | |
| if (!riegeli::WriteBigEndian<uint32_t>(sequence_num, output)) { | |
| return output.status(); | |
| } | |
| if (!riegeli::WriteBigEndian<uint32_t>(header.lbox, output)) { | |
| return output.status(); | |
| } | |
| if (!riegeli::WriteBigEndian<uint32_t>(header.tbox, output)) { | |
| return output.status(); | |
| } | |
| if (header.lbox == 1) { | |
| if (!riegeli::WriteBigEndian<uint64_t>(header.xlbox, output)) { | |
| return output.status(); | |
| } | |
| } | |
| if (!output.Write(payload_chunk)) { | |
| return output.status(); | |
| } | |
| return absl::OkStatus(); | |
| } | |
| } // namespace | |
| uint64_t CalculateTotalJpegApp11SegmentsSize(uint64_t jumbf_payload_size) { | |
| uint64_t jpeg_header_size = sizeof(uint16_t) + // marker | |
| sizeof(uint16_t) + // size | |
| sizeof(uint16_t) + // ext_type | |
| sizeof(uint16_t) + // segment_id | |
| sizeof(uint32_t); // sequence_num | |
| uint64_t jumbf_header_size = | |
| JumbfHeader{.lbox = static_cast<uint32_t>( | |
| jumbf_payload_size > UINT32_MAX ? 1 : 0)} | |
| .byte_size(); | |
| uint64_t payload_size_without_header = jumbf_payload_size - jumbf_header_size; | |
| uint64_t number_of_chunks = | |
| (payload_size_without_header / kMaxPayloadSegmentByteSize) + | |
| (payload_size_without_header % kMaxPayloadSegmentByteSize != 0); | |
| return ((jpeg_header_size + jumbf_header_size) * number_of_chunks) + | |
| payload_size_without_header; | |
| } | |
| absl::Status WriteJpegApp11Payload(uint16_t segment_id, | |
| absl::string_view payload, | |
| riegeli::Writer& output) { | |
| riegeli::StringReader<> payload_header_reader(payload.substr(0, 16)); | |
| ABSL_ASSIGN_OR_RETURN(auto header, | |
| ConsumePayloadJumbfHeader(payload_header_reader)); | |
| if (header.box_size() != payload.size()) { | |
| return absl::InvalidArgumentError(absl::Substitute( | |
| "invalid jumbf box -- box size does not match payload size: $0 vs $1", | |
| header.box_size(), payload.size())); | |
| } | |
| uint32_t seq_num = 1; // Starts at 1 per ISO/IEC 19566-5. | |
| size_t cursor = header.byte_size(); | |
| while (cursor < payload.size()) { | |
| uint16_t chunk_size = kMaxPayloadSegmentByteSize; | |
| ABSL_RETURN_IF_ERROR(WritePayloadChunk(header, | |
| payload.substr(cursor, chunk_size), | |
| segment_id, seq_num, output)); | |
| cursor += chunk_size; | |
| if (++seq_num == 0) { | |
| return absl::OutOfRangeError(absl::Substitute( | |
| "too many segments required for payload of size $0", payload.size())); | |
| } | |
| } | |
| return absl::OkStatus(); | |
| } | |
| } // namespace credentio |