Coverage Report

Created: 2026-08-14 08:18

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libjxl/lib/extras/dec/gif.cc
Line
Count
Source
1
// Copyright (c) the JPEG XL Project Authors. All rights reserved.
2
//
3
// Use of this source code is governed by a BSD-style
4
// license that can be found in the LICENSE file.
5
6
#include "lib/extras/dec/gif.h"
7
8
#include <cstdint>
9
10
#include "lib/extras/dec/color_hints.h"
11
#include "lib/extras/packed_image.h"
12
#include "lib/extras/size_constraints.h"
13
#include "lib/jxl/base/span.h"
14
#include "lib/jxl/base/status.h"
15
16
#if !JPEGXL_ENABLE_GIF
17
18
namespace jxl {
19
namespace extras {
20
0
bool CanDecodeGIF() { return false; }
21
Status DecodeImageGIF(Span<const uint8_t> bytes, const ColorHints& color_hints,
22
                      PackedPixelFile* ppf,
23
2
                      const SizeConstraints* constraints) {
24
2
  return false;
25
2
}
26
}  // namespace extras
27
}  // namespace jxl
28
29
#else  // JPEGXL_ENABLE_GIF
30
31
#include <gif_lib.h>
32
#include <jxl/codestream_header.h>
33
#include <jxl/types.h>
34
35
#include <algorithm>
36
#include <cstring>
37
#include <memory>
38
#include <utility>
39
#include <vector>
40
41
#include "lib/jxl/base/rect.h"
42
#include "lib/jxl/base/sanitizers.h"
43
44
namespace jxl {
45
namespace extras {
46
47
namespace {
48
49
struct ReadState {
50
  Span<const uint8_t> bytes;
51
};
52
53
struct DGifCloser {
54
  void operator()(GifFileType* const ptr) const { DGifCloseFile(ptr, nullptr); }
55
};
56
using GifUniquePtr = std::unique_ptr<GifFileType, DGifCloser>;
57
58
struct PackedRgba {
59
  uint8_t r, g, b, a;
60
};
61
62
struct PackedRgb {
63
  uint8_t r, g, b;
64
};
65
66
Status ensure_have_alpha(PackedFrame* frame) {
67
  if (!frame->extra_channels.empty()) return true;
68
  const JxlPixelFormat alpha_format{
69
      /*num_channels=*/1u,
70
      /*data_type=*/JXL_TYPE_UINT8,
71
      /*endianness=*/JXL_NATIVE_ENDIAN,
72
      /*align=*/0,
73
  };
74
  JXL_ASSIGN_OR_RETURN(PackedImage image,
75
                       PackedImage::Create(frame->color.xsize,
76
                                           frame->color.ysize, alpha_format));
77
  frame->extra_channels.emplace_back(std::move(image));
78
  // We need to set opaque-by-default.
79
  std::fill_n(static_cast<uint8_t*>(frame->extra_channels[0].pixels()),
80
              frame->color.xsize * frame->color.ysize, 255u);
81
  return true;
82
}
83
}  // namespace
84
85
bool CanDecodeGIF() { return true; }
86
87
Status DecodeImageGIF(Span<const uint8_t> bytes, const ColorHints& color_hints,
88
                      PackedPixelFile* ppf,
89
                      const SizeConstraints* constraints) {
90
  int error = GIF_OK;
91
  ReadState state = {bytes};
92
  const auto ReadFromSpan = [](GifFileType* const gif, GifByteType* const bytes,
93
                               int n) {
94
    ReadState* const state = reinterpret_cast<ReadState*>(gif->UserData);
95
    // giflib API requires the input size `n` to be signed int.
96
    if (static_cast<size_t>(n) > state->bytes.size()) {
97
      n = state->bytes.size();
98
    }
99
    memcpy(bytes, state->bytes.data(), n);
100
    if (!state->bytes.remove_prefix(n)) return 0;
101
    return n;
102
  };
103
  GifUniquePtr gif(DGifOpen(&state, ReadFromSpan, &error));
104
  if (gif == nullptr) {
105
    if (error == D_GIF_ERR_NOT_GIF_FILE) {
106
      // Not an error.
107
      return false;
108
    } else {
109
      return JXL_FAILURE("Failed to read GIF: %s", GifErrorString(error));
110
    }
111
  }
112
  error = DGifSlurp(gif.get());
113
  if (error != GIF_OK) {
114
    return JXL_FAILURE("Failed to read GIF: %s", GifErrorString(gif->Error));
115
  }
116
117
  msan::UnpoisonMemory(gif.get(), sizeof(*gif));
118
  if (gif->SColorMap) {
119
    msan::UnpoisonMemory(gif->SColorMap, sizeof(*gif->SColorMap));
120
    msan::UnpoisonMemory(
121
        gif->SColorMap->Colors,
122
        sizeof(*gif->SColorMap->Colors) * gif->SColorMap->ColorCount);
123
  }
124
  msan::UnpoisonMemory(gif->SavedImages,
125
                       sizeof(*gif->SavedImages) * gif->ImageCount);
126
127
  JXL_RETURN_IF_ERROR(
128
      VerifyDimensions<uint32_t>(constraints, gif->SWidth, gif->SHeight));
129
  uint64_t total_pixel_count =
130
      static_cast<uint64_t>(gif->SWidth) * gif->SHeight;
131
  for (int i = 0; i < gif->ImageCount; ++i) {
132
    const SavedImage& image = gif->SavedImages[i];
133
    uint32_t w = image.ImageDesc.Width;
134
    uint32_t h = image.ImageDesc.Height;
135
    JXL_RETURN_IF_ERROR(VerifyDimensions<uint32_t>(constraints, w, h));
136
    uint64_t pixel_count = static_cast<uint64_t>(w) * h;
137
    if (total_pixel_count + pixel_count < total_pixel_count) {
138
      return JXL_FAILURE("Image too big");
139
    }
140
    total_pixel_count += pixel_count;
141
    if (constraints && (total_pixel_count > constraints->dec_max_pixels)) {
142
      return JXL_FAILURE("Image too big");
143
    }
144
  }
145
146
  if (!gif->SColorMap) {
147
    for (int i = 0; i < gif->ImageCount; ++i) {
148
      if (!gif->SavedImages[i].ImageDesc.ColorMap) {
149
        return JXL_FAILURE("Missing GIF color map");
150
      }
151
    }
152
  }
153
154
  if (gif->ImageCount > 1) {
155
    ppf->info.have_animation = JXL_TRUE;
156
    // Delays in GIF are specified in censiseconds.
157
    ppf->info.animation.tps_numerator = 100;
158
    ppf->info.animation.tps_denominator = 1;
159
  }
160
161
  ppf->frames.clear();
162
  ppf->frames.reserve(gif->ImageCount);
163
164
  ppf->info.xsize = gif->SWidth;
165
  ppf->info.ysize = gif->SHeight;
166
  ppf->info.bits_per_sample = 8;
167
  ppf->info.exponent_bits_per_sample = 0;
168
  // alpha_bits is later set to 8 if we find a frame with transparent pixels.
169
  ppf->info.alpha_bits = 0;
170
  ppf->info.alpha_exponent_bits = 0;
171
  JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, /*color_already_set=*/false,
172
                                      /*is_gray=*/false, ppf));
173
174
  ppf->info.num_color_channels = 3;
175
176
  // Pixel format for the 'canvas' onto which we paint
177
  // the (potentially individually cropped) GIF frames
178
  // of an animation.
179
  const JxlPixelFormat canvas_format{
180
      /*num_channels=*/4u,
181
      /*data_type=*/JXL_TYPE_UINT8,
182
      /*endianness=*/JXL_NATIVE_ENDIAN,
183
      /*align=*/0,
184
  };
185
186
  // Pixel format for the JXL PackedFrame that goes into the
187
  // PackedPixelFile. Here, we use 3 color channels, and provide
188
  // the alpha channel as an extra_channel wherever it is used.
189
  const JxlPixelFormat packed_frame_format{
190
      /*num_channels=*/3u,
191
      /*data_type=*/JXL_TYPE_UINT8,
192
      /*endianness=*/JXL_NATIVE_ENDIAN,
193
      /*align=*/0,
194
  };
195
196
  GifColorType background_color;
197
  if (gif->SColorMap == nullptr ||
198
      gif->SBackGroundColor >= gif->SColorMap->ColorCount) {
199
    background_color = {0, 0, 0};
200
  } else {
201
    background_color = gif->SColorMap->Colors[gif->SBackGroundColor];
202
  }
203
  const PackedRgba background_rgba{background_color.Red, background_color.Green,
204
                                   background_color.Blue, 0};
205
  JXL_ASSIGN_OR_RETURN(
206
      PackedFrame canvas,
207
      PackedFrame::Create(gif->SWidth, gif->SHeight, canvas_format));
208
  std::fill_n(static_cast<PackedRgba*>(canvas.color.pixels()),
209
              canvas.color.xsize * canvas.color.ysize, background_rgba);
210
  Rect canvas_rect{0, 0, canvas.color.xsize, canvas.color.ysize};
211
212
  Rect previous_rect_if_restore_to_background;
213
214
  bool replace = true;
215
  bool last_base_was_none = true;
216
  for (int i = 0; i < gif->ImageCount; ++i) {
217
    const SavedImage& image = gif->SavedImages[i];
218
    msan::UnpoisonMemory(image.RasterBits, sizeof(*image.RasterBits) *
219
                                               image.ImageDesc.Width *
220
                                               image.ImageDesc.Height);
221
    const Rect image_rect(image.ImageDesc.Left, image.ImageDesc.Top,
222
                          image.ImageDesc.Width, image.ImageDesc.Height);
223
224
    Rect total_rect;
225
    if (previous_rect_if_restore_to_background.xsize() != 0 ||
226
        previous_rect_if_restore_to_background.ysize() != 0) {
227
      const size_t xbegin = std::min(
228
          image_rect.x0(), previous_rect_if_restore_to_background.x0());
229
      const size_t ybegin = std::min(
230
          image_rect.y0(), previous_rect_if_restore_to_background.y0());
231
      const size_t xend =
232
          std::max(image_rect.x0() + image_rect.xsize(),
233
                   previous_rect_if_restore_to_background.x0() +
234
                       previous_rect_if_restore_to_background.xsize());
235
      const size_t yend =
236
          std::max(image_rect.y0() + image_rect.ysize(),
237
                   previous_rect_if_restore_to_background.y0() +
238
                       previous_rect_if_restore_to_background.ysize());
239
      total_rect = Rect(xbegin, ybegin, xend - xbegin, yend - ybegin);
240
      previous_rect_if_restore_to_background = Rect();
241
      replace = true;
242
    } else {
243
      total_rect = image_rect;
244
      replace = false;
245
    }
246
    if (!image_rect.IsInside(canvas_rect)) {
247
      return JXL_FAILURE("GIF frame extends outside of the canvas");
248
    }
249
250
    // Allocates the frame buffer.
251
    {
252
      JXL_ASSIGN_OR_RETURN(
253
          PackedFrame frame,
254
          PackedFrame::Create(total_rect.xsize(), total_rect.ysize(),
255
                              packed_frame_format));
256
      ppf->frames.emplace_back(std::move(frame));
257
    }
258
259
    PackedFrame* frame = &ppf->frames.back();
260
261
    // We cannot tell right from the start whether there will be a
262
    // need for an alpha channel. This is discovered only as soon as
263
    // we see a transparent pixel. We hence initialize alpha lazily.
264
    auto set_pixel_alpha = [&frame](size_t x, size_t y, uint8_t a) -> Status {
265
      // If we do not have an alpha-channel and a==255 (fully opaque),
266
      // we can skip setting this pixel-value and rely on
267
      // "no alpha channel = no transparency".
268
      if (a == 255 && frame->extra_channels.empty()) return true;
269
      JXL_RETURN_IF_ERROR(ensure_have_alpha(frame));
270
      static_cast<uint8_t*>(
271
          frame->extra_channels[0].pixels())[y * frame->color.xsize + x] = a;
272
      return true;
273
    };
274
275
    const ColorMapObject* const color_map =
276
        image.ImageDesc.ColorMap ? image.ImageDesc.ColorMap : gif->SColorMap;
277
    JXL_ENSURE(color_map);
278
    msan::UnpoisonMemory(color_map, sizeof(*color_map));
279
    msan::UnpoisonMemory(color_map->Colors,
280
                         sizeof(*color_map->Colors) * color_map->ColorCount);
281
    GraphicsControlBlock gcb;
282
    DGifSavedExtensionToGCB(gif.get(), i, &gcb);
283
    msan::UnpoisonMemory(&gcb, sizeof(gcb));
284
    bool is_full_size = total_rect.x0() == 0 && total_rect.y0() == 0 &&
285
                        total_rect.xsize() == canvas.color.xsize &&
286
                        total_rect.ysize() == canvas.color.ysize;
287
    if (ppf->info.have_animation) {
288
      frame->frame_info.duration = gcb.DelayTime;
289
      frame->frame_info.layer_info.have_crop = static_cast<int>(!is_full_size);
290
      frame->frame_info.layer_info.crop_x0 = total_rect.x0();
291
      frame->frame_info.layer_info.crop_y0 = total_rect.y0();
292
      frame->frame_info.layer_info.xsize = frame->color.xsize;
293
      frame->frame_info.layer_info.ysize = frame->color.ysize;
294
      if (last_base_was_none) {
295
        replace = true;
296
      }
297
      frame->frame_info.layer_info.blend_info.blendmode =
298
          replace ? JXL_BLEND_REPLACE : JXL_BLEND_BLEND;
299
      // We always only reference at most the last frame
300
      frame->frame_info.layer_info.blend_info.source =
301
          last_base_was_none ? 0u : 1u;
302
      frame->frame_info.layer_info.blend_info.clamp = 1;
303
      frame->frame_info.layer_info.blend_info.alpha = 0;
304
      // TODO(veluca): this could in principle be implemented.
305
      if (last_base_was_none &&
306
          (total_rect.x0() != 0 || total_rect.y0() != 0 ||
307
           total_rect.xsize() != canvas.color.xsize ||
308
           total_rect.ysize() != canvas.color.ysize || !replace)) {
309
        if (!JXL_IS_DEBUG_BUILD) {
310
          fprintf(stderr,
311
              "GIF with dispose-to-0 is not supported for non-full or blended "
312
              "frames\n");
313
        }
314
        return JXL_FAILURE(
315
            "GIF with dispose-to-0 is not supported"
316
            "for non-full or blended frames");
317
      }
318
      switch (gcb.DisposalMode) {
319
        case DISPOSE_DO_NOT:
320
        case DISPOSE_BACKGROUND:
321
          frame->frame_info.layer_info.save_as_reference = 1u;
322
          last_base_was_none = false;
323
          break;
324
        case DISPOSE_PREVIOUS:
325
          frame->frame_info.layer_info.save_as_reference = 0u;
326
          break;
327
        default:
328
          frame->frame_info.layer_info.save_as_reference = 0u;
329
          last_base_was_none = true;
330
      }
331
    }
332
333
    // Update the canvas by creating a copy first.
334
    JXL_ASSIGN_OR_RETURN(
335
        PackedImage new_canvas_image,
336
        PackedImage::Create(canvas.color.xsize, canvas.color.ysize,
337
                            canvas.color.format));
338
    memcpy(new_canvas_image.pixels(), canvas.color.pixels(),
339
           new_canvas_image.pixels_size);
340
    for (size_t y = 0, byte_index = 0; y < image_rect.ysize(); ++y) {
341
      // Assumes format.align == 0. row points to the beginning of the y row in
342
      // the image_rect.
343
      PackedRgba* row = static_cast<PackedRgba*>(new_canvas_image.pixels()) +
344
                        (y + image_rect.y0()) * new_canvas_image.xsize +
345
                        image_rect.x0();
346
      for (size_t x = 0; x < image_rect.xsize(); ++x, ++byte_index) {
347
        const GifByteType byte = image.RasterBits[byte_index];
348
        if (byte >= color_map->ColorCount) {
349
          return JXL_FAILURE("GIF color is out of bounds");
350
        }
351
352
        if (byte == gcb.TransparentColor) continue;
353
        GifColorType color = color_map->Colors[byte];
354
        row[x].r = color.Red;
355
        row[x].g = color.Green;
356
        row[x].b = color.Blue;
357
        row[x].a = 255;
358
      }
359
    }
360
    const PackedImage& sub_frame_image = frame->color;
361
    if (replace) {
362
      // Copy from the new canvas image to the subframe
363
      for (size_t y = 0; y < total_rect.ysize(); ++y) {
364
        const PackedRgba* row_in =
365
            static_cast<const PackedRgba*>(new_canvas_image.pixels()) +
366
            (y + total_rect.y0()) * new_canvas_image.xsize + total_rect.x0();
367
        PackedRgb* row_out = static_cast<PackedRgb*>(sub_frame_image.pixels()) +
368
                             y * sub_frame_image.xsize;
369
        for (size_t x = 0; x < sub_frame_image.xsize; ++x) {
370
          row_out[x].r = row_in[x].r;
371
          row_out[x].g = row_in[x].g;
372
          row_out[x].b = row_in[x].b;
373
          JXL_RETURN_IF_ERROR(set_pixel_alpha(x, y, row_in[x].a));
374
        }
375
      }
376
    } else {
377
      for (size_t y = 0, byte_index = 0; y < image_rect.ysize(); ++y) {
378
        // Assumes format.align == 0
379
        PackedRgb* row = static_cast<PackedRgb*>(sub_frame_image.pixels()) +
380
                         y * sub_frame_image.xsize;
381
        for (size_t x = 0; x < image_rect.xsize(); ++x, ++byte_index) {
382
          const GifByteType byte = image.RasterBits[byte_index];
383
          if (byte >= color_map->ColorCount) {
384
            return JXL_FAILURE("GIF color is out of bounds");
385
          }
386
          if (byte == gcb.TransparentColor) {
387
            row[x].r = 0;
388
            row[x].g = 0;
389
            row[x].b = 0;
390
            JXL_RETURN_IF_ERROR(set_pixel_alpha(x, y, 0));
391
            continue;
392
          }
393
          GifColorType color = color_map->Colors[byte];
394
          row[x].r = color.Red;
395
          row[x].g = color.Green;
396
          row[x].b = color.Blue;
397
          JXL_RETURN_IF_ERROR(set_pixel_alpha(x, y, 255));
398
        }
399
      }
400
    }
401
402
    if (!frame->extra_channels.empty()) {
403
      ppf->info.alpha_bits = 8;
404
    }
405
406
    switch (gcb.DisposalMode) {
407
      case DISPOSE_DO_NOT:
408
        canvas.color = std::move(new_canvas_image);
409
        break;
410
411
      case DISPOSE_BACKGROUND:
412
        std::fill_n(static_cast<PackedRgba*>(canvas.color.pixels()),
413
                    canvas.color.xsize * canvas.color.ysize, background_rgba);
414
        previous_rect_if_restore_to_background = image_rect;
415
        break;
416
417
      case DISPOSE_PREVIOUS:
418
        break;
419
420
      case DISPOSAL_UNSPECIFIED:
421
      default:
422
        std::fill_n(static_cast<PackedRgba*>(canvas.color.pixels()),
423
                    canvas.color.xsize * canvas.color.ysize, background_rgba);
424
    }
425
  }
426
  // Finally, if any frame has an alpha-channel, every frame will need
427
  // to have an alpha-channel.
428
  bool seen_alpha = false;
429
  for (const PackedFrame& frame : ppf->frames) {
430
    if (!frame.extra_channels.empty()) {
431
      seen_alpha = true;
432
      break;
433
    }
434
  }
435
  if (seen_alpha) {
436
    for (PackedFrame& frame : ppf->frames) {
437
      JXL_RETURN_IF_ERROR(ensure_have_alpha(&frame));
438
    }
439
  }
440
  return true;
441
}
442
443
}  // namespace extras
444
}  // namespace jxl
445
446
#endif  // JPEGXL_ENABLE_GIF