| // 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/gif/reader.h" | |
| #include <cstdint> | |
| #include <string> | |
| #include <utility> | |
| #include "absl/status/status.h" | |
| #include "absl/status/status_macros.h" | |
| #include "absl/status/statusor.h" | |
| #include "absl/strings/str_format.h" | |
| #include "absl/strings/string_view.h" | |
| #include "formats/gif/constants.h" | |
| #include "riegeli/bytes/reader.h" | |
| namespace credentio { | |
| namespace { | |
| struct ColorTable { | |
| bool has_table; | |
| uint8_t size; | |
| static ColorTable FromPacked(uint8_t packed) { | |
| return { | |
| .has_table = (packed & 0x80) != 0, | |
| .size = static_cast<uint8_t>(packed & 0x07), | |
| }; | |
| } | |
| }; | |
| absl::Status SeekToEndOfData(riegeli::Reader& input) { | |
| uint8_t size = 0x00; | |
| do { | |
| if (!input.ReadByte(size)) { | |
| return input.StatusOrAnnotate(absl::DataLossError("Failed to read byte")); | |
| } | |
| if (!input.ok()) { | |
| return input.status(); | |
| } | |
| if (size > 0) { | |
| if (!input.Skip(size)) { | |
| return absl::InternalError("Failed to seek to offset"); | |
| } | |
| if (!input.ok()) { | |
| return input.status(); | |
| } | |
| } | |
| } while (size > 0); | |
| return absl::OkStatus(); | |
| } | |
| absl::StatusOr<GifBlock> ReadHeader(riegeli::Reader& input) { | |
| GifBlock block = {.offset = input.pos()}; | |
| std::string data; | |
| if (!input.Read(kGifHeaderBlock.size(), data)) { | |
| return input.StatusOrAnnotate( | |
| absl::DataLossError("Failed to read GIF header block")); | |
| } | |
| if (data != kGifHeaderBlock) { | |
| return absl::InvalidArgumentError("Invalid GIF header block"); | |
| } | |
| block.length = data.size(); | |
| block.type = data; | |
| return std::move(block); | |
| } | |
| absl::Status PopulateDescriptor(riegeli::Reader& input, | |
| int64_t packed_byte_offset, GifBlock& block) { | |
| std::string data; | |
| if (!input.Read(block.length, data)) { | |
| return input.StatusOrAnnotate( | |
| absl::DataLossError("Failed to read descriptor")); | |
| } | |
| ColorTable packed = ColorTable::FromPacked(data[packed_byte_offset]); | |
| if (packed.has_table) { | |
| int64_t ct_length = 3 * (1 << (packed.size + 1)); | |
| block.length += ct_length; | |
| } | |
| return absl::OkStatus(); | |
| } | |
| absl::Status PopulateLogicalDescriptor(riegeli::Reader& input, | |
| GifBlock& block) { | |
| block.length = 7; | |
| block.type = std::string(kGifLogicalScreenDescriptorLabel); | |
| return PopulateDescriptor(input, 4, block); | |
| } | |
| absl::Status PopulateImageDescriptor(riegeli::Reader& input, GifBlock& block) { | |
| // Backup one byte so we can read the introducer byte. | |
| if (!input.Seek(input.pos() - 1)) { | |
| return absl::InternalError("Failed to seek to offset"); | |
| } | |
| block.length = 10; | |
| block.type = std::string(kGifImageDescriptorLabel); | |
| return PopulateDescriptor(input, 9, block); | |
| } | |
| absl::Status PopulateBlock(riegeli::Reader& input, GifBlock& block) { | |
| ABSL_RETURN_IF_ERROR(SeekToEndOfData(input)); | |
| block.length = input.pos() - block.offset; | |
| return absl::OkStatus(); | |
| } | |
| absl::Status PopulateImageData(riegeli::Reader& input, GifBlock& block) { | |
| block.type = std::string(kGifImageDataLabel); | |
| block.offset = input.pos(); | |
| if (!input.Skip(1)) { | |
| return input.StatusOrAnnotate( | |
| absl::DataLossError("Failed to skip LZW minimum code size")); | |
| } | |
| return PopulateBlock(input, block); | |
| } | |
| absl::Status PopulateApplicationExtension(riegeli::Reader& input, | |
| GifBlock& block) { | |
| int64_t next_size_offset = input.pos(); | |
| uint8_t extension_info_size = 0x00; | |
| if (!input.ReadByte(extension_info_size)) { | |
| return input.StatusOrAnnotate( | |
| absl::DataLossError("Failed to read extension info size")); | |
| } | |
| next_size_offset += 1 + extension_info_size; | |
| if (extension_info_size == kGifC2paExtensionInfoSize) { | |
| std::string data; | |
| if (!input.Read(kGifC2paIdentifier.size(), data)) { | |
| return input.StatusOrAnnotate( | |
| absl::DataLossError("Failed to read c2pa identifier")); | |
| } | |
| if (data == kGifC2paIdentifier) { | |
| block.type = kGifC2paLabel; | |
| } | |
| } | |
| // Seek past the remaining Application Extension Information | |
| if (!input.Seek(next_size_offset)) { | |
| return absl::InternalError("Failed to seek to offset"); | |
| } | |
| return PopulateBlock(input, block); | |
| } | |
| } // namespace | |
| absl::Status IterateOverGifBlocks(riegeli::Reader& input, | |
| GifBlockProcessor processor, | |
| int64_t end_offset) { | |
| if (!input.SupportsSize() || !input.Size().has_value()) { | |
| return absl::InvalidArgumentError( | |
| "Input does not support size or size is unknown"); | |
| } | |
| uint64_t local_end_offset = | |
| end_offset < 0 ? input.Size().value() : end_offset; | |
| ABSL_ASSIGN_OR_RETURN(auto header, ReadHeader(input)); | |
| ABSL_ASSIGN_OR_RETURN(bool should_continue, processor(header)); | |
| ABSL_RETURN_IF_ERROR(input.status()); | |
| if (!should_continue) { | |
| return absl::OkStatus(); | |
| } | |
| if (input.pos() >= local_end_offset) { | |
| return absl::OkStatus(); | |
| } | |
| GifBlock logical_descriptor = {.offset = input.pos()}; | |
| ABSL_RETURN_IF_ERROR(PopulateLogicalDescriptor(input, logical_descriptor)); | |
| ABSL_ASSIGN_OR_RETURN(should_continue, processor(logical_descriptor)); | |
| ABSL_RETURN_IF_ERROR(input.status()); | |
| if (!should_continue) { | |
| return absl::OkStatus(); | |
| } | |
| if (!input.Seek(logical_descriptor.offset + logical_descriptor.length)) { | |
| return input.StatusOrAnnotate(absl::InternalError( | |
| "Failed to seek to offset after logical descriptor")); | |
| } | |
| while (input.pos() < local_end_offset) { | |
| GifBlock block = {.offset = input.pos()}; | |
| uint8_t introducer = 0x00; | |
| if (!input.ReadByte(introducer)) { | |
| return input.StatusOrAnnotate( | |
| absl::DataLossError("Failed to read introducer")); | |
| } | |
| switch (introducer) { | |
| case kGifExtensionIntroducer: { | |
| // is extension | |
| uint8_t extension_type = 0x00; | |
| if (!input.ReadByte(extension_type)) { | |
| return input.StatusOrAnnotate( | |
| absl::DataLossError("Failed to read extension type")); | |
| } | |
| switch (extension_type) { | |
| case kGifExtensionPlainText: | |
| block.type = std::string(kGifExtensionPlainTextLabel); | |
| ABSL_RETURN_IF_ERROR(PopulateBlock(input, block)); | |
| break; | |
| case kGifExtensionGraphicsControl: | |
| block.type = std::string(kGifExtensionGraphicsControlLabel); | |
| ABSL_RETURN_IF_ERROR(PopulateBlock(input, block)); | |
| break; | |
| case kGifExtensionComment: | |
| block.type = std::string(kGifExtensionCommentLabel); | |
| ABSL_RETURN_IF_ERROR(PopulateBlock(input, block)); | |
| break; | |
| case kGifExtensionApplication: | |
| block.type = std::string(kGifExtensionApplicationLabel); | |
| ABSL_RETURN_IF_ERROR(PopulateApplicationExtension(input, block)); | |
| break; | |
| default: | |
| return absl::UnimplementedError(absl::StrFormat( | |
| "Extension Type not supported: %#04x", extension_type)); | |
| } | |
| break; | |
| } | |
| case kGifImageDescriptorIntroducer: { | |
| // Process the image descriptor. | |
| ABSL_RETURN_IF_ERROR(PopulateImageDescriptor(input, block)); | |
| ABSL_ASSIGN_OR_RETURN(should_continue, processor(block)); | |
| ABSL_RETURN_IF_ERROR(input.status()); | |
| if (!should_continue) { | |
| return absl::OkStatus(); | |
| } | |
| if (!input.Seek(block.offset + block.length)) { | |
| return input.StatusOrAnnotate( | |
| absl::InternalError("Failed to seek to offset")); | |
| } | |
| // Process the image data directly following the image descriptor. | |
| ABSL_RETURN_IF_ERROR(PopulateImageData(input, block)); | |
| break; | |
| } | |
| case kGifTrailerIntroducer: { | |
| block.length = 1; | |
| block.type = std::string(kGifTrailerLabel); | |
| break; | |
| } | |
| default: | |
| return absl::UnimplementedError( | |
| absl::StrFormat("Introducer not supported: %#04x", introducer)); | |
| } | |
| ABSL_ASSIGN_OR_RETURN(should_continue, processor(block)); | |
| ABSL_RETURN_IF_ERROR(input.status()); | |
| if (!should_continue) { | |
| return absl::OkStatus(); | |
| } | |
| if (!input.Seek(block.offset + block.length)) { | |
| return input.StatusOrAnnotate( | |
| absl::InternalError("Failed to seek to offset")); | |
| } | |
| if (block.type == kGifTrailerLabel) { | |
| break; | |
| } | |
| } | |
| if (input.pos() < local_end_offset) { | |
| GifBlock after_label = {.offset = input.pos(), | |
| .length = local_end_offset - input.pos(), | |
| .type = "c2pa.after"}; | |
| return processor(after_label).status(); | |
| } | |
| return absl::OkStatus(); | |
| } | |
| } // namespace credentio |