/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 |