summaryrefslogtreecommitdiff
path: root/media/libjxl/src/lib/extras/dec/gif.cc
diff options
context:
space:
mode:
Diffstat (limited to 'media/libjxl/src/lib/extras/dec/gif.cc')
-rw-r--r--media/libjxl/src/lib/extras/dec/gif.cc414
1 files changed, 414 insertions, 0 deletions
diff --git a/media/libjxl/src/lib/extras/dec/gif.cc b/media/libjxl/src/lib/extras/dec/gif.cc
new file mode 100644
index 0000000000..5167bf5fa0
--- /dev/null
+++ b/media/libjxl/src/lib/extras/dec/gif.cc
@@ -0,0 +1,414 @@
+// 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 "lib/extras/dec/gif.h"
+
+#include <gif_lib.h>
+#include <string.h>
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "jxl/codestream_header.h"
+#include "lib/jxl/base/compiler_specific.h"
+#include "lib/jxl/sanitizers.h"
+
+namespace jxl {
+namespace extras {
+
+namespace {
+
+struct ReadState {
+ Span<const uint8_t> bytes;
+};
+
+struct DGifCloser {
+ void operator()(GifFileType* const ptr) const { DGifCloseFile(ptr, nullptr); }
+};
+using GifUniquePtr = std::unique_ptr<GifFileType, DGifCloser>;
+
+struct PackedRgba {
+ uint8_t r, g, b, a;
+};
+
+struct PackedRgb {
+ uint8_t r, g, b;
+};
+
+// Gif does not support partial transparency, so this considers any nonzero
+// alpha channel value as opaque.
+bool AllOpaque(const PackedImage& color) {
+ for (size_t y = 0; y < color.ysize; ++y) {
+ const PackedRgba* const JXL_RESTRICT row =
+ static_cast<const PackedRgba*>(color.pixels()) + y * color.xsize;
+ for (size_t x = 0; x < color.xsize; ++x) {
+ if (row[x].a == 0) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+void ensure_have_alpha(PackedFrame* frame) {
+ if (!frame->extra_channels.empty()) return;
+ const JxlPixelFormat alpha_format{
+ /*num_channels=*/1u,
+ /*data_type=*/JXL_TYPE_UINT8,
+ /*endianness=*/JXL_NATIVE_ENDIAN,
+ /*align=*/0,
+ };
+ frame->extra_channels.emplace_back(frame->color.xsize, frame->color.ysize,
+ alpha_format);
+ // We need to set opaque-by-default.
+ std::fill_n(static_cast<uint8_t*>(frame->extra_channels[0].pixels()),
+ frame->color.xsize * frame->color.ysize, 255u);
+}
+
+} // namespace
+
+Status DecodeImageGIF(Span<const uint8_t> bytes, const ColorHints& color_hints,
+ const SizeConstraints& constraints,
+ PackedPixelFile* ppf) {
+ int error = GIF_OK;
+ ReadState state = {bytes};
+ const auto ReadFromSpan = [](GifFileType* const gif, GifByteType* const bytes,
+ int n) {
+ ReadState* const state = reinterpret_cast<ReadState*>(gif->UserData);
+ // giflib API requires the input size `n` to be signed int.
+ if (static_cast<size_t>(n) > state->bytes.size()) {
+ n = state->bytes.size();
+ }
+ memcpy(bytes, state->bytes.data(), n);
+ state->bytes.remove_prefix(n);
+ return n;
+ };
+ GifUniquePtr gif(DGifOpen(&state, ReadFromSpan, &error));
+ if (gif == nullptr) {
+ if (error == D_GIF_ERR_NOT_GIF_FILE) {
+ // Not an error.
+ return false;
+ } else {
+ return JXL_FAILURE("Failed to read GIF: %s", GifErrorString(error));
+ }
+ }
+ error = DGifSlurp(gif.get());
+ if (error != GIF_OK) {
+ return JXL_FAILURE("Failed to read GIF: %s", GifErrorString(gif->Error));
+ }
+
+ msan::UnpoisonMemory(gif.get(), sizeof(*gif));
+ if (gif->SColorMap) {
+ msan::UnpoisonMemory(gif->SColorMap, sizeof(*gif->SColorMap));
+ msan::UnpoisonMemory(
+ gif->SColorMap->Colors,
+ sizeof(*gif->SColorMap->Colors) * gif->SColorMap->ColorCount);
+ }
+ msan::UnpoisonMemory(gif->SavedImages,
+ sizeof(*gif->SavedImages) * gif->ImageCount);
+
+ JXL_RETURN_IF_ERROR(
+ VerifyDimensions<uint32_t>(&constraints, gif->SWidth, gif->SHeight));
+ uint64_t total_pixel_count =
+ static_cast<uint64_t>(gif->SWidth) * gif->SHeight;
+ for (int i = 0; i < gif->ImageCount; ++i) {
+ const SavedImage& image = gif->SavedImages[i];
+ uint32_t w = image.ImageDesc.Width;
+ uint32_t h = image.ImageDesc.Height;
+ JXL_RETURN_IF_ERROR(VerifyDimensions<uint32_t>(&constraints, w, h));
+ uint64_t pixel_count = static_cast<uint64_t>(w) * h;
+ if (total_pixel_count + pixel_count < total_pixel_count) {
+ return JXL_FAILURE("Image too big");
+ }
+ total_pixel_count += pixel_count;
+ if (total_pixel_count > constraints.dec_max_pixels) {
+ return JXL_FAILURE("Image too big");
+ }
+ }
+
+ if (!gif->SColorMap) {
+ for (int i = 0; i < gif->ImageCount; ++i) {
+ if (!gif->SavedImages[i].ImageDesc.ColorMap) {
+ return JXL_FAILURE("Missing GIF color map");
+ }
+ }
+ }
+
+ if (gif->ImageCount > 1) {
+ ppf->info.have_animation = true;
+ // Delays in GIF are specified in 100ths of a second.
+ ppf->info.animation.tps_numerator = 100;
+ ppf->info.animation.tps_denominator = 1;
+ }
+
+ ppf->frames.clear();
+ ppf->frames.reserve(gif->ImageCount);
+
+ ppf->info.xsize = gif->SWidth;
+ ppf->info.ysize = gif->SHeight;
+ ppf->info.bits_per_sample = 8;
+ ppf->info.exponent_bits_per_sample = 0;
+ // alpha_bits is later set to 8 if we find a frame with transparent pixels.
+ ppf->info.alpha_bits = 0;
+ ppf->info.alpha_exponent_bits = 0;
+ JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, /*color_already_set=*/false,
+ /*is_gray=*/false, ppf));
+
+ ppf->info.num_color_channels = 3;
+
+ // Pixel format for the 'canvas' onto which we paint
+ // the (potentially individually cropped) GIF frames
+ // of an animation.
+ const JxlPixelFormat canvas_format{
+ /*num_channels=*/4u,
+ /*data_type=*/JXL_TYPE_UINT8,
+ /*endianness=*/JXL_NATIVE_ENDIAN,
+ /*align=*/0,
+ };
+
+ // Pixel format for the JXL PackedFrame that goes into the
+ // PackedPixelFile. Here, we use 3 color channels, and provide
+ // the alpha channel as an extra_channel wherever it is used.
+ const JxlPixelFormat packed_frame_format{
+ /*num_channels=*/3u,
+ /*data_type=*/JXL_TYPE_UINT8,
+ /*endianness=*/JXL_NATIVE_ENDIAN,
+ /*align=*/0,
+ };
+
+ GifColorType background_color;
+ if (gif->SColorMap == nullptr ||
+ gif->SBackGroundColor >= gif->SColorMap->ColorCount) {
+ background_color = {0, 0, 0};
+ } else {
+ background_color = gif->SColorMap->Colors[gif->SBackGroundColor];
+ }
+ const PackedRgba background_rgba{background_color.Red, background_color.Green,
+ background_color.Blue, 0};
+ PackedFrame canvas(gif->SWidth, gif->SHeight, canvas_format);
+ std::fill_n(static_cast<PackedRgba*>(canvas.color.pixels()),
+ canvas.color.xsize * canvas.color.ysize, background_rgba);
+ Rect canvas_rect{0, 0, canvas.color.xsize, canvas.color.ysize};
+
+ Rect previous_rect_if_restore_to_background;
+
+ bool replace = true;
+ bool last_base_was_none = true;
+ for (int i = 0; i < gif->ImageCount; ++i) {
+ const SavedImage& image = gif->SavedImages[i];
+ msan::UnpoisonMemory(image.RasterBits, sizeof(*image.RasterBits) *
+ image.ImageDesc.Width *
+ image.ImageDesc.Height);
+ const Rect image_rect(image.ImageDesc.Left, image.ImageDesc.Top,
+ image.ImageDesc.Width, image.ImageDesc.Height);
+
+ Rect total_rect;
+ if (previous_rect_if_restore_to_background.xsize() != 0 ||
+ previous_rect_if_restore_to_background.ysize() != 0) {
+ const size_t xbegin = std::min(
+ image_rect.x0(), previous_rect_if_restore_to_background.x0());
+ const size_t ybegin = std::min(
+ image_rect.y0(), previous_rect_if_restore_to_background.y0());
+ const size_t xend =
+ std::max(image_rect.x0() + image_rect.xsize(),
+ previous_rect_if_restore_to_background.x0() +
+ previous_rect_if_restore_to_background.xsize());
+ const size_t yend =
+ std::max(image_rect.y0() + image_rect.ysize(),
+ previous_rect_if_restore_to_background.y0() +
+ previous_rect_if_restore_to_background.ysize());
+ total_rect = Rect(xbegin, ybegin, xend - xbegin, yend - ybegin);
+ previous_rect_if_restore_to_background = Rect();
+ replace = true;
+ } else {
+ total_rect = image_rect;
+ replace = false;
+ }
+ if (!image_rect.IsInside(canvas_rect)) {
+ return JXL_FAILURE("GIF frame extends outside of the canvas");
+ }
+
+ // Allocates the frame buffer.
+ ppf->frames.emplace_back(total_rect.xsize(), total_rect.ysize(),
+ packed_frame_format);
+ PackedFrame* frame = &ppf->frames.back();
+
+ // We cannot tell right from the start whether there will be a
+ // need for an alpha channel. This is discovered only as soon as
+ // we see a transparent pixel. We hence initialize alpha lazily.
+ auto set_pixel_alpha = [&frame](size_t x, size_t y, uint8_t a) {
+ // If we do not have an alpha-channel and a==255 (fully opaque),
+ // we can skip setting this pixel-value and rely on
+ // "no alpha channel = no transparency".
+ if (a == 255 && !frame->extra_channels.empty()) return;
+ ensure_have_alpha(frame);
+ static_cast<uint8_t*>(
+ frame->extra_channels[0].pixels())[y * frame->color.xsize + x] = a;
+ };
+
+ const ColorMapObject* const color_map =
+ image.ImageDesc.ColorMap ? image.ImageDesc.ColorMap : gif->SColorMap;
+ JXL_CHECK(color_map);
+ msan::UnpoisonMemory(color_map, sizeof(*color_map));
+ msan::UnpoisonMemory(color_map->Colors,
+ sizeof(*color_map->Colors) * color_map->ColorCount);
+ GraphicsControlBlock gcb;
+ DGifSavedExtensionToGCB(gif.get(), i, &gcb);
+ msan::UnpoisonMemory(&gcb, sizeof(gcb));
+ bool is_full_size = total_rect.x0() == 0 && total_rect.y0() == 0 &&
+ total_rect.xsize() == canvas.color.xsize &&
+ total_rect.ysize() == canvas.color.ysize;
+ if (ppf->info.have_animation) {
+ frame->frame_info.duration = gcb.DelayTime;
+ frame->frame_info.layer_info.have_crop = static_cast<int>(!is_full_size);
+ frame->frame_info.layer_info.crop_x0 = total_rect.x0();
+ frame->frame_info.layer_info.crop_y0 = total_rect.y0();
+ frame->frame_info.layer_info.xsize = frame->color.xsize;
+ frame->frame_info.layer_info.ysize = frame->color.ysize;
+ if (last_base_was_none) {
+ replace = true;
+ }
+ frame->frame_info.layer_info.blend_info.blendmode =
+ replace ? JXL_BLEND_REPLACE : JXL_BLEND_BLEND;
+ // We always only reference at most the last frame
+ frame->frame_info.layer_info.blend_info.source =
+ last_base_was_none ? 0u : 1u;
+ frame->frame_info.layer_info.blend_info.clamp = 1;
+ frame->frame_info.layer_info.blend_info.alpha = 0;
+ // TODO(veluca): this could in principle be implemented.
+ if (last_base_was_none &&
+ (total_rect.x0() != 0 || total_rect.y0() != 0 ||
+ total_rect.xsize() != canvas.color.xsize ||
+ total_rect.ysize() != canvas.color.ysize || !replace)) {
+ return JXL_FAILURE(
+ "GIF with dispose-to-0 is not supported for non-full or "
+ "blended frames");
+ }
+ switch (gcb.DisposalMode) {
+ case DISPOSE_DO_NOT:
+ case DISPOSE_BACKGROUND:
+ frame->frame_info.layer_info.save_as_reference = 1u;
+ last_base_was_none = false;
+ break;
+ case DISPOSE_PREVIOUS:
+ frame->frame_info.layer_info.save_as_reference = 0u;
+ break;
+ default:
+ frame->frame_info.layer_info.save_as_reference = 0u;
+ last_base_was_none = true;
+ }
+ }
+
+ // Update the canvas by creating a copy first.
+ PackedImage new_canvas_image(canvas.color.xsize, canvas.color.ysize,
+ canvas.color.format);
+ memcpy(new_canvas_image.pixels(), canvas.color.pixels(),
+ new_canvas_image.pixels_size);
+ for (size_t y = 0, byte_index = 0; y < image_rect.ysize(); ++y) {
+ // Assumes format.align == 0. row points to the beginning of the y row in
+ // the image_rect.
+ PackedRgba* row = static_cast<PackedRgba*>(new_canvas_image.pixels()) +
+ (y + image_rect.y0()) * new_canvas_image.xsize +
+ image_rect.x0();
+ for (size_t x = 0; x < image_rect.xsize(); ++x, ++byte_index) {
+ const GifByteType byte = image.RasterBits[byte_index];
+ if (byte >= color_map->ColorCount) {
+ return JXL_FAILURE("GIF color is out of bounds");
+ }
+
+ if (byte == gcb.TransparentColor) continue;
+ GifColorType color = color_map->Colors[byte];
+ row[x].r = color.Red;
+ row[x].g = color.Green;
+ row[x].b = color.Blue;
+ row[x].a = 255;
+ }
+ }
+ const PackedImage& sub_frame_image = frame->color;
+ if (replace) {
+ // Copy from the new canvas image to the subframe
+ for (size_t y = 0; y < total_rect.ysize(); ++y) {
+ const PackedRgba* row_in =
+ static_cast<const PackedRgba*>(new_canvas_image.pixels()) +
+ (y + total_rect.y0()) * new_canvas_image.xsize + total_rect.x0();
+ PackedRgb* row_out = static_cast<PackedRgb*>(sub_frame_image.pixels()) +
+ y * sub_frame_image.xsize;
+ for (size_t x = 0; x < sub_frame_image.xsize; ++x) {
+ row_out[x].r = row_in[x].r;
+ row_out[x].g = row_in[x].g;
+ row_out[x].b = row_in[x].b;
+ set_pixel_alpha(x, y, row_in[x].a);
+ }
+ }
+ } else {
+ for (size_t y = 0, byte_index = 0; y < image_rect.ysize(); ++y) {
+ // Assumes format.align == 0
+ PackedRgb* row = static_cast<PackedRgb*>(sub_frame_image.pixels()) +
+ y * sub_frame_image.xsize;
+ for (size_t x = 0; x < image_rect.xsize(); ++x, ++byte_index) {
+ const GifByteType byte = image.RasterBits[byte_index];
+ if (byte > color_map->ColorCount) {
+ return JXL_FAILURE("GIF color is out of bounds");
+ }
+ if (byte == gcb.TransparentColor) {
+ row[x].r = 0;
+ row[x].g = 0;
+ row[x].b = 0;
+ set_pixel_alpha(x, y, 0);
+ continue;
+ }
+ GifColorType color = color_map->Colors[byte];
+ row[x].r = color.Red;
+ row[x].g = color.Green;
+ row[x].b = color.Blue;
+ set_pixel_alpha(x, y, 255);
+ }
+ }
+ }
+
+ if (!frame->extra_channels.empty()) {
+ ppf->info.alpha_bits = 8;
+ }
+
+ switch (gcb.DisposalMode) {
+ case DISPOSE_DO_NOT:
+ canvas.color = std::move(new_canvas_image);
+ break;
+
+ case DISPOSE_BACKGROUND:
+ std::fill_n(static_cast<PackedRgba*>(canvas.color.pixels()),
+ canvas.color.xsize * canvas.color.ysize, background_rgba);
+ previous_rect_if_restore_to_background = image_rect;
+ break;
+
+ case DISPOSE_PREVIOUS:
+ break;
+
+ case DISPOSAL_UNSPECIFIED:
+ default:
+ std::fill_n(static_cast<PackedRgba*>(canvas.color.pixels()),
+ canvas.color.xsize * canvas.color.ysize, background_rgba);
+ }
+ }
+ // Finally, if any frame has an alpha-channel, every frame will need
+ // to have an alpha-channel.
+ bool seen_alpha = false;
+ for (const PackedFrame& frame : ppf->frames) {
+ if (!frame.extra_channels.empty()) {
+ seen_alpha = true;
+ break;
+ }
+ }
+ if (seen_alpha) {
+ for (PackedFrame& frame : ppf->frames) {
+ ensure_have_alpha(&frame);
+ }
+ }
+ return true;
+}
+
+} // namespace extras
+} // namespace jxl