diff options
Diffstat (limited to 'media/libjxl/src/tools/box/box.cc')
-rw-r--r-- | media/libjxl/src/tools/box/box.cc | 334 |
1 files changed, 334 insertions, 0 deletions
diff --git a/media/libjxl/src/tools/box/box.cc b/media/libjxl/src/tools/box/box.cc new file mode 100644 index 0000000000..a60af5b14b --- /dev/null +++ b/media/libjxl/src/tools/box/box.cc @@ -0,0 +1,334 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "tools/box/box.h" + +#include "lib/jxl/base/byte_order.h" // for GetMaximumBrunsliEncodedSize +#include "lib/jxl/jpeg/dec_jpeg_data.h" +#include "lib/jxl/jpeg/jpeg_data.h" + +namespace jpegxl { +namespace tools { + +namespace { +// Checks if a + b > size, taking possible integer overflow into account. +bool OutOfBounds(size_t a, size_t b, size_t size) { + size_t pos = a + b; + if (pos > size) return true; + if (pos < a) return true; // overflow happened + return false; +} +} // namespace + +// Parses the header of a BMFF box. Returns the result in a Box struct. +// Sets the position to the end of the box header after parsing. The data size +// is output if known, or must be handled by the caller and runs until the end +// of the container file if not known. +jxl::Status ParseBoxHeader(const uint8_t** next_in, size_t* available_in, + Box* box) { + size_t pos = 0; + size_t size = *available_in; + const uint8_t* in = *next_in; + + if (OutOfBounds(pos, 8, size)) return JXL_FAILURE("out of bounds"); + + const size_t initial_pos = pos; + + // Total box_size including this header itself. + uint64_t box_size = LoadBE32(in + pos); + memcpy(box->type, in + pos + 4, 4); + + pos += 8; + + if (box_size == 1) { + // If the size is 1, it indicates extended size read from 64-bit integer. + if (OutOfBounds(pos, 8, size)) return JXL_FAILURE("out of bounds"); + box_size = LoadBE64(in + pos); + pos += 8; + } + + if (!memcmp("uuid", box->type, 4)) { + if (OutOfBounds(pos, 16, size)) return JXL_FAILURE("out of bounds"); + memcpy(box->extended_type, in + pos, 16); + pos += 16; + } + + // This is the end of the box header, the box data begins here. Handle + // the data size now. + const size_t data_pos = pos; + const size_t header_size = data_pos - initial_pos; + + if (box_size != 0) { + if (box_size < header_size) { + return JXL_FAILURE("invalid box size"); + } + box->data_size_given = true; + box->data_size = box_size - header_size; + } else { + // The size extends to the end of the file. We don't necessarily know the + // end of the file here, since the input size may be only part of the full + // container file. Indicate the size is not given, the caller must handle + // this. + box->data_size_given = false; + box->data_size = 0; + } + + // The remaining bytes are the data. If the box is a full box, the first + // bytes of the data have a certain structure but this is to be handled by + // the caller for the appropriate box type. + *next_in += pos; + *available_in -= pos; + + return true; +} + +jxl::Status AppendBoxHeader(const Box& box, jxl::PaddedBytes* out) { + bool use_extended = !memcmp("uuid", box.type, 4); + + uint64_t box_size = 0; + bool large_size = false; + if (box.data_size_given) { + box_size = box.data_size + 8 + (use_extended ? 16 : 0); + if (box_size >= 0x100000000ull) { + large_size = true; + } + } + + out->resize(out->size() + 4); + StoreBE32(large_size ? 1 : box_size, &out->back() - 4 + 1); + + out->resize(out->size() + 4); + memcpy(&out->back() - 4 + 1, box.type, 4); + + if (large_size) { + out->resize(out->size() + 8); + StoreBE64(box_size, &out->back() - 8 + 1); + } + + if (use_extended) { + out->resize(out->size() + 16); + memcpy(&out->back() - 16 + 1, box.extended_type, 16); + } + + return true; +} + +bool IsContainerHeader(const uint8_t* data, size_t size) { + const uint8_t box_header[] = {0, 0, 0, 0xc, 'J', 'X', + 'L', ' ', 0xd, 0xa, 0x87, 0xa}; + if (size < sizeof(box_header)) return false; + return memcmp(box_header, data, sizeof(box_header)) == 0; +} + +jxl::Status DecodeJpegXlContainerOneShot(const uint8_t* data, size_t size, + JpegXlContainer* container) { + const uint8_t* in = data; + size_t available_in = size; + + container->exif = nullptr; + container->exif_size = 0; + container->exfc = nullptr; + container->exfc_size = 0; + container->xml.clear(); + container->xmlc.clear(); + container->jumb = nullptr; + container->jumb_size = 0; + container->codestream.clear(); + container->jpeg_reconstruction = nullptr; + container->jpeg_reconstruction_size = 0; + + size_t box_index = 0; + + while (available_in != 0) { + Box box; + if (!ParseBoxHeader(&in, &available_in, &box)) { + return JXL_FAILURE("Invalid box header"); + } + + size_t data_size = box.data_size_given ? box.data_size : available_in; + + if (box.data_size > available_in) { + return JXL_FAILURE("Unexpected end of file"); + } + + if (box_index == 0) { + // TODO(lode): leave out magic signature box? + // Must be magic signature box. + if (memcmp("JXL ", box.type, 4) != 0) { + return JXL_FAILURE("Invalid magic signature"); + } + if (box.data_size != 4) return JXL_FAILURE("Invalid magic signature"); + if (in[0] != 0xd || in[1] != 0xa || in[2] != 0x87 || in[3] != 0xa) { + return JXL_FAILURE("Invalid magic signature"); + } + } else if (box_index == 1) { + // Must be ftyp box. + if (memcmp("ftyp", box.type, 4) != 0) { + return JXL_FAILURE("Invalid ftyp"); + } + if (box.data_size != 12) return JXL_FAILURE("Invalid ftyp"); + const char* expected = "jxl \0\0\0\0jxl "; + if (memcmp(expected, in, 12) != 0) return JXL_FAILURE("Invalid ftyp"); + } else if (!memcmp("jxli", box.type, 4)) { + // TODO(lode): parse JXL frame index box + if (!container->codestream.empty()) { + return JXL_FAILURE("frame index must come before codestream"); + } + } else if (!memcmp("jxlc", box.type, 4)) { + container->codestream.append(in, in + data_size); + } else if (!memcmp("jxlp", box.type, 4)) { + if (data_size < 4) return JXL_FAILURE("Invalid jxlp"); + // TODO(jon): don't just ignore the counter + container->codestream.append(in + 4, in + data_size); + } else if (!memcmp("Exif", box.type, 4)) { + if (data_size < 4) return JXL_FAILURE("Invalid Exif"); + uint32_t tiff_header_offset = LoadBE32(in); + if (tiff_header_offset > data_size - 4) + return JXL_FAILURE("Invalid Exif tiff header offset"); + container->exif = in + 4 + tiff_header_offset; + container->exif_size = data_size - 4 - tiff_header_offset; + } else if (!memcmp("Exfc", box.type, 4)) { + container->exfc = in; + container->exfc_size = data_size; + } else if (!memcmp("xml ", box.type, 4)) { + container->xml.emplace_back(in, data_size); + } else if (!memcmp("xmlc", box.type, 4)) { + container->xmlc.emplace_back(in, data_size); + } else if (!memcmp("jumb", box.type, 4)) { + container->jumb = in; + container->jumb_size = data_size; + } else if (!memcmp("jbrd", box.type, 4)) { + container->jpeg_reconstruction = in; + container->jpeg_reconstruction_size = data_size; + } else { + // Do nothing: box not recognized here but may be recognizable by + // other software. + } + + in += data_size; + available_in -= data_size; + box_index++; + } + + return true; +} + +static jxl::Status AppendBoxAndData(const char type[4], const uint8_t* data, + size_t data_size, jxl::PaddedBytes* out, + bool exif = false) { + Box box; + memcpy(box.type, type, 4); + box.data_size = data_size + (exif ? 4 : 0); + box.data_size_given = true; + JXL_RETURN_IF_ERROR(AppendBoxHeader(box, out)); + // for Exif: always use tiff header offset 0 + if (exif) + for (int i = 0; i < 4; i++) out->push_back(0); + out->append(data, data + data_size); + return true; +} + +jxl::Status EncodeJpegXlContainerOneShot(const JpegXlContainer& container, + jxl::PaddedBytes* out) { + const unsigned char header[] = {0, 0, 0, 0xc, 'J', 'X', 'L', ' ', + 0xd, 0xa, 0x87, 0xa, 0, 0, 0, 0x14, + 'f', 't', 'y', 'p', 'j', 'x', 'l', ' ', + 0, 0, 0, 0, 'j', 'x', 'l', ' '}; + size_t header_size = sizeof(header); + out->append(header, header + header_size); + + if (container.exif) { + JXL_RETURN_IF_ERROR(AppendBoxAndData("Exif", container.exif, + container.exif_size, out, true)); + } + + if (container.exfc) { + JXL_RETURN_IF_ERROR( + AppendBoxAndData("Exfc", container.exfc, container.exfc_size, out)); + } + + for (size_t i = 0; i < container.xml.size(); i++) { + JXL_RETURN_IF_ERROR(AppendBoxAndData("xml ", container.xml[i].first, + container.xml[i].second, out)); + } + + for (size_t i = 0; i < container.xmlc.size(); i++) { + JXL_RETURN_IF_ERROR(AppendBoxAndData("xmlc", container.xmlc[i].first, + container.xmlc[i].second, out)); + } + + if (container.jpeg_reconstruction) { + JXL_RETURN_IF_ERROR(AppendBoxAndData("jbrd", container.jpeg_reconstruction, + container.jpeg_reconstruction_size, + out)); + } + + if (!container.codestream.empty()) { + JXL_RETURN_IF_ERROR(AppendBoxAndData("jxlc", container.codestream.data(), + container.codestream.size(), out)); + } else { + return JXL_FAILURE("must have primary image frame"); + } + + if (container.jumb) { + JXL_RETURN_IF_ERROR( + AppendBoxAndData("jumb", container.jumb, container.jumb_size, out)); + } + + return true; +} + +// TODO(veluca): the format defined here encode some things multiple times. Fix +// that. +jxl::Status DecodeJpegXlToJpeg(jxl::DecompressParams params, + const JpegXlContainer& container, + jxl::CodecInOut* io, jxl::ThreadPool* pool) { + params.keep_dct = true; + if (container.jpeg_reconstruction == nullptr) { + return JXL_FAILURE( + "Cannot decode to JPEG without a JPEG reconstruction box"); + } + + io->Main().jpeg_data = jxl::make_unique<jxl::jpeg::JPEGData>(); + + JXL_RETURN_IF_ERROR(DecodeJPEGData( + jxl::Span<const uint8_t>(container.jpeg_reconstruction, + container.jpeg_reconstruction_size), + io->Main().jpeg_data.get())); + + auto& jpeg_data = io->Main().jpeg_data; + bool have_exif = false, have_xmp = false; + for (size_t i = 0; i < jpeg_data->app_data.size(); i++) { + if (jpeg_data->app_marker_type[i] == jxl::jpeg::AppMarkerType::kExif) { + if (have_exif) + return JXL_FAILURE("Unexpected: more than one Exif box required?"); + if (jpeg_data->app_data[i].size() != container.exif_size + 9) { + return JXL_FAILURE( + "Exif box size does not match JPEG reconstruction data"); + } + have_exif = true; + memcpy(&jpeg_data->app_data[i][3 + 6], container.exif, + container.exif_size); + } + if (jpeg_data->app_marker_type[i] == jxl::jpeg::AppMarkerType::kXMP) { + if (have_xmp) + return JXL_FAILURE("Unexpected: more than one XMP box required?"); + if (jpeg_data->app_data[i].size() != container.xml[0].second + 32) { + return JXL_FAILURE( + "XMP box size does not match JPEG reconstruction data"); + } + have_xmp = true; + memcpy(&jpeg_data->app_data[i][3 + 29], container.xml[0].first, + container.xml[0].second); + } + } + + JXL_RETURN_IF_ERROR(DecodeFile( + params, jxl::Span<const uint8_t>(container.codestream), io, pool)); + return true; +} + +} // namespace tools +} // namespace jpegxl |