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/codec_gif.h"
7
8 #include <gif_lib.h>
9 #include <string.h>
10
11 #include <algorithm>
12 #include <string>
13 #include <utility>
14 #include <vector>
15
16 #include "lib/jxl/base/compiler_specific.h"
17 #include "lib/jxl/color_encoding_internal.h"
18 #include "lib/jxl/frame_header.h"
19 #include "lib/jxl/headers.h"
20 #include "lib/jxl/image.h"
21 #include "lib/jxl/image_bundle.h"
22 #include "lib/jxl/image_ops.h"
23 #include "lib/jxl/luminance.h"
24 #include "lib/jxl/sanitizers.h"
25
26 namespace jxl {
27 namespace extras {
28
29 namespace {
30
31 struct ReadState {
32 Span<const uint8_t> bytes;
33 };
34
35 struct DGifCloser {
operator ()jxl::extras::__anon378969c50111::DGifCloser36 void operator()(GifFileType* const ptr) const { DGifCloseFile(ptr, nullptr); }
37 };
38 using GifUniquePtr = std::unique_ptr<GifFileType, DGifCloser>;
39
40 // Gif does not support partial transparency, so this considers anything non-0
41 // as opaque.
AllOpaque(const ImageF & alpha)42 bool AllOpaque(const ImageF& alpha) {
43 for (size_t y = 0; y < alpha.ysize(); ++y) {
44 const float* const JXL_RESTRICT row = alpha.ConstRow(y);
45 for (size_t x = 0; x < alpha.xsize(); ++x) {
46 if (row[x] == 0.f) {
47 return false;
48 }
49 }
50 }
51 return true;
52 }
53
54 } // namespace
55
DecodeImageGIF(Span<const uint8_t> bytes,const ColorHints & color_hints,ThreadPool * pool,CodecInOut * io)56 Status DecodeImageGIF(Span<const uint8_t> bytes, const ColorHints& color_hints,
57 ThreadPool* pool, CodecInOut* io) {
58 int error = GIF_OK;
59 ReadState state = {bytes};
60 const auto ReadFromSpan = [](GifFileType* const gif, GifByteType* const bytes,
61 int n) {
62 ReadState* const state = reinterpret_cast<ReadState*>(gif->UserData);
63 // giflib API requires the input size `n` to be signed int.
64 if (static_cast<size_t>(n) > state->bytes.size()) {
65 n = state->bytes.size();
66 }
67 memcpy(bytes, state->bytes.data(), n);
68 state->bytes.remove_prefix(n);
69 return n;
70 };
71 GifUniquePtr gif(DGifOpen(&state, ReadFromSpan, &error));
72 if (gif == nullptr) {
73 if (error == D_GIF_ERR_NOT_GIF_FILE) {
74 // Not an error.
75 return false;
76 } else {
77 return JXL_FAILURE("Failed to read GIF: %s", GifErrorString(error));
78 }
79 }
80 error = DGifSlurp(gif.get());
81 if (error != GIF_OK) {
82 return JXL_FAILURE("Failed to read GIF: %s", GifErrorString(gif->Error));
83 }
84
85 msan::UnpoisonMemory(gif.get(), sizeof(*gif));
86 if (gif->SColorMap) {
87 msan::UnpoisonMemory(gif->SColorMap, sizeof(*gif->SColorMap));
88 msan::UnpoisonMemory(
89 gif->SColorMap->Colors,
90 sizeof(*gif->SColorMap->Colors) * gif->SColorMap->ColorCount);
91 }
92 msan::UnpoisonMemory(gif->SavedImages,
93 sizeof(*gif->SavedImages) * gif->ImageCount);
94
95 const SizeConstraints* constraints = &io->constraints;
96
97 JXL_RETURN_IF_ERROR(
98 VerifyDimensions<uint32_t>(constraints, gif->SWidth, gif->SHeight));
99 uint64_t total_pixel_count =
100 static_cast<uint64_t>(gif->SWidth) * gif->SHeight;
101 for (int i = 0; i < gif->ImageCount; ++i) {
102 const SavedImage& image = gif->SavedImages[i];
103 uint32_t w = image.ImageDesc.Width;
104 uint32_t h = image.ImageDesc.Height;
105 JXL_RETURN_IF_ERROR(VerifyDimensions<uint32_t>(constraints, w, h));
106 uint64_t pixel_count = static_cast<uint64_t>(w) * h;
107 if (total_pixel_count + pixel_count < total_pixel_count) {
108 return JXL_FAILURE("Image too big");
109 }
110 total_pixel_count += pixel_count;
111 if (total_pixel_count > constraints->dec_max_pixels) {
112 return JXL_FAILURE("Image too big");
113 }
114 }
115
116 if (!gif->SColorMap) {
117 for (int i = 0; i < gif->ImageCount; ++i) {
118 if (!gif->SavedImages[i].ImageDesc.ColorMap) {
119 return JXL_FAILURE("Missing GIF color map");
120 }
121 }
122 }
123
124 if (gif->ImageCount > 1) {
125 io->metadata.m.have_animation = true;
126 // Delays in GIF are specified in 100ths of a second.
127 io->metadata.m.animation.tps_numerator = 100;
128 }
129
130 io->frames.clear();
131 io->frames.reserve(gif->ImageCount);
132 io->dec_pixels = 0;
133
134 io->metadata.m.SetUintSamples(8);
135 io->metadata.m.SetAlphaBits(0);
136 JXL_RETURN_IF_ERROR(ApplyColorHints(color_hints, /*color_already_set=*/false,
137 /*is_gray=*/false, io));
138
139 Image3F canvas(gif->SWidth, gif->SHeight);
140 io->SetSize(gif->SWidth, gif->SHeight);
141 ImageF alpha(gif->SWidth, gif->SHeight);
142 GifColorType background_color;
143 if (gif->SColorMap == nullptr) {
144 background_color = {0, 0, 0};
145 } else {
146 if (gif->SBackGroundColor >= gif->SColorMap->ColorCount) {
147 return JXL_FAILURE("GIF specifies out-of-bounds background color");
148 }
149 background_color = gif->SColorMap->Colors[gif->SBackGroundColor];
150 }
151 FillPlane<float>(background_color.Red, &canvas.Plane(0));
152 FillPlane<float>(background_color.Green, &canvas.Plane(1));
153 FillPlane<float>(background_color.Blue, &canvas.Plane(2));
154 ZeroFillImage(&alpha);
155
156 Rect previous_rect_if_restore_to_background;
157
158 bool has_alpha = false;
159 bool replace = true;
160 bool last_base_was_none = true;
161 for (int i = 0; i < gif->ImageCount; ++i) {
162 const SavedImage& image = gif->SavedImages[i];
163 msan::UnpoisonMemory(image.RasterBits, sizeof(*image.RasterBits) *
164 image.ImageDesc.Width *
165 image.ImageDesc.Height);
166 const Rect image_rect(image.ImageDesc.Left, image.ImageDesc.Top,
167 image.ImageDesc.Width, image.ImageDesc.Height);
168 io->dec_pixels += image_rect.xsize() * image_rect.ysize();
169 Rect total_rect;
170 if (previous_rect_if_restore_to_background.xsize() != 0 ||
171 previous_rect_if_restore_to_background.ysize() != 0) {
172 const size_t xbegin = std::min(
173 image_rect.x0(), previous_rect_if_restore_to_background.x0());
174 const size_t ybegin = std::min(
175 image_rect.y0(), previous_rect_if_restore_to_background.y0());
176 const size_t xend =
177 std::max(image_rect.x0() + image_rect.xsize(),
178 previous_rect_if_restore_to_background.x0() +
179 previous_rect_if_restore_to_background.xsize());
180 const size_t yend =
181 std::max(image_rect.y0() + image_rect.ysize(),
182 previous_rect_if_restore_to_background.y0() +
183 previous_rect_if_restore_to_background.ysize());
184 total_rect = Rect(xbegin, ybegin, xend - xbegin, yend - ybegin);
185 previous_rect_if_restore_to_background = Rect();
186 replace = true;
187 } else {
188 total_rect = image_rect;
189 replace = false;
190 }
191 if (!image_rect.IsInside(canvas)) {
192 return JXL_FAILURE("GIF frame extends outside of the canvas");
193 }
194 const ColorMapObject* const color_map =
195 image.ImageDesc.ColorMap ? image.ImageDesc.ColorMap : gif->SColorMap;
196 JXL_CHECK(color_map);
197 msan::UnpoisonMemory(color_map, sizeof(*color_map));
198 msan::UnpoisonMemory(color_map->Colors,
199 sizeof(*color_map->Colors) * color_map->ColorCount);
200 GraphicsControlBlock gcb;
201 DGifSavedExtensionToGCB(gif.get(), i, &gcb);
202 msan::UnpoisonMemory(&gcb, sizeof(gcb));
203
204 ImageBundle bundle(&io->metadata.m);
205 if (io->metadata.m.have_animation) {
206 bundle.duration = gcb.DelayTime;
207 bundle.origin.x0 = total_rect.x0();
208 bundle.origin.y0 = total_rect.y0();
209 if (last_base_was_none) {
210 replace = true;
211 }
212 bundle.blend = !replace;
213 // TODO(veluca): this could in principle be implemented.
214 if (last_base_was_none &&
215 (total_rect.x0() != 0 || total_rect.y0() != 0 ||
216 total_rect.xsize() != canvas.xsize() ||
217 total_rect.ysize() != canvas.ysize() || !replace)) {
218 return JXL_FAILURE(
219 "GIF with dispose-to-0 is not supported for non-full or "
220 "blended frames");
221 }
222 switch (gcb.DisposalMode) {
223 case DISPOSE_DO_NOT:
224 case DISPOSE_BACKGROUND:
225 bundle.use_for_next_frame = true;
226 last_base_was_none = false;
227 break;
228 case DISPOSE_PREVIOUS:
229 bundle.use_for_next_frame = false;
230 break;
231 default:
232 bundle.use_for_next_frame = false;
233 last_base_was_none = true;
234 }
235 }
236 Image3F frame = CopyImage(canvas);
237 ImageF frame_alpha = CopyImage(alpha);
238 for (size_t y = 0, byte_index = 0; y < image_rect.ysize(); ++y) {
239 float* const JXL_RESTRICT row_r = image_rect.Row(&frame.Plane(0), y);
240 float* const JXL_RESTRICT row_g = image_rect.Row(&frame.Plane(1), y);
241 float* const JXL_RESTRICT row_b = image_rect.Row(&frame.Plane(2), y);
242 float* const JXL_RESTRICT row_alpha = image_rect.Row(&frame_alpha, y);
243 for (size_t x = 0; x < image_rect.xsize(); ++x, ++byte_index) {
244 const GifByteType byte = image.RasterBits[byte_index];
245 if (byte >= color_map->ColorCount) {
246 return JXL_FAILURE("GIF color is out of bounds");
247 }
248 if (byte == gcb.TransparentColor) continue;
249 GifColorType color = color_map->Colors[byte];
250 row_alpha[x] = 1.f;
251 row_r[x] = (1.f / 255) * color.Red;
252 row_g[x] = (1.f / 255) * color.Green;
253 row_b[x] = (1.f / 255) * color.Blue;
254 }
255 }
256 Image3F sub_frame(total_rect.xsize(), total_rect.ysize());
257 ImageF sub_frame_alpha(total_rect.xsize(), total_rect.ysize());
258 bool blend_alpha = false;
259 if (replace) {
260 CopyImageTo(total_rect, frame, &sub_frame);
261 CopyImageTo(total_rect, frame_alpha, &sub_frame_alpha);
262 } else {
263 for (size_t y = 0, byte_index = 0; y < image_rect.ysize(); ++y) {
264 float* const JXL_RESTRICT row_r = sub_frame.PlaneRow(0, y);
265 float* const JXL_RESTRICT row_g = sub_frame.PlaneRow(1, y);
266 float* const JXL_RESTRICT row_b = sub_frame.PlaneRow(2, y);
267 float* const JXL_RESTRICT row_alpha = sub_frame_alpha.Row(y);
268 for (size_t x = 0; x < image_rect.xsize(); ++x, ++byte_index) {
269 const GifByteType byte = image.RasterBits[byte_index];
270 if (byte > color_map->ColorCount) {
271 return JXL_FAILURE("GIF color is out of bounds");
272 }
273 if (byte == gcb.TransparentColor) {
274 row_alpha[x] = 0;
275 row_r[x] = 0;
276 row_g[x] = 0;
277 row_b[x] = 0;
278 blend_alpha =
279 true; // need to use alpha channel if BlendMode blend is used
280 continue;
281 }
282 GifColorType color = color_map->Colors[byte];
283 row_alpha[x] = 1.f;
284 row_r[x] = (1.f / 255) * color.Red;
285 row_g[x] = (1.f / 255) * color.Green;
286 row_b[x] = (1.f / 255) * color.Blue;
287 }
288 }
289 }
290 bundle.SetFromImage(std::move(sub_frame), ColorEncoding::SRGB());
291 if (has_alpha || !AllOpaque(frame_alpha) || blend_alpha) {
292 if (!has_alpha) {
293 has_alpha = true;
294 io->metadata.m.SetAlphaBits(8);
295 for (ImageBundle& previous_frame : io->frames) {
296 ImageF previous_alpha(previous_frame.xsize(), previous_frame.ysize());
297 FillImage(1.f, &previous_alpha);
298 previous_frame.SetAlpha(std::move(previous_alpha),
299 /*alpha_is_premultiplied=*/false);
300 }
301 }
302 bundle.SetAlpha(std::move(sub_frame_alpha),
303 /*alpha_is_premultiplied=*/false);
304 }
305 io->frames.push_back(std::move(bundle));
306 switch (gcb.DisposalMode) {
307 case DISPOSE_DO_NOT:
308 canvas = std::move(frame);
309 alpha = std::move(frame_alpha);
310 break;
311
312 case DISPOSE_BACKGROUND:
313 FillPlane<float>((1.f / 255) * background_color.Red, &canvas.Plane(0),
314 image_rect);
315 FillPlane<float>((1.f / 255) * background_color.Green, &canvas.Plane(1),
316 image_rect);
317 FillPlane<float>((1.f / 255) * background_color.Blue, &canvas.Plane(2),
318 image_rect);
319 FillPlane(0.f, &alpha, image_rect);
320 previous_rect_if_restore_to_background = image_rect;
321 break;
322
323 case DISPOSE_PREVIOUS:
324 break;
325
326 case DISPOSAL_UNSPECIFIED:
327 default:
328 FillPlane<float>((1.f / 255) * background_color.Red, &canvas.Plane(0));
329 FillPlane<float>((1.f / 255) * background_color.Green,
330 &canvas.Plane(1));
331 FillPlane<float>((1.f / 255) * background_color.Blue, &canvas.Plane(2));
332 ZeroFillImage(&alpha);
333 }
334 }
335
336 SetIntensityTarget(io);
337 return true;
338 }
339
340 } // namespace extras
341 } // namespace jxl
342