@rin-ts/sticker 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1882 @@
1
+ #include "sticker_pipeline.hpp"
2
+
3
+ #include <algorithm>
4
+ #include <array>
5
+ #include <cerrno>
6
+ #include <cmath>
7
+ #include <csetjmp>
8
+ #include <cstddef>
9
+ #include <cstdint>
10
+ #include <cstdio>
11
+ #include <cstdlib>
12
+ #include <cstring>
13
+ #include <limits>
14
+ #include <memory>
15
+ #include <new>
16
+ #include <span>
17
+ #include <string>
18
+ #include <utility>
19
+ #include <vector>
20
+
21
+ extern "C" {
22
+ #include <jpeglib.h>
23
+
24
+ #include <libavcodec/avcodec.h>
25
+ #include <libavformat/avformat.h>
26
+ #include <libavutil/error.h>
27
+ #include <libavutil/imgutils.h>
28
+ #include <libavutil/mem.h>
29
+ #include <libavutil/rational.h>
30
+ #include <libswscale/swscale.h>
31
+ }
32
+
33
+ #include <png.h>
34
+ #include <webp/decode.h>
35
+ #include <webp/demux.h>
36
+ #include <webp/encode.h>
37
+ #include <webp/mux.h>
38
+
39
+ namespace sticker {
40
+
41
+ StickerError::StickerError(const ErrorCode code, std::string message)
42
+ : std::runtime_error(std::move(message)), code_(code) {}
43
+
44
+ ErrorCode StickerError::code() const noexcept { return code_; }
45
+
46
+ namespace {
47
+
48
+ using Byte = std::uint8_t;
49
+
50
+ constexpr std::uint32_t kCanvasSize = 512U;
51
+ constexpr std::size_t kMaxInputBytes =
52
+ static_cast<std::size_t>(16U) * 1024U * 1024U;
53
+ constexpr std::uint32_t kMaxSourceDimension = 4096U;
54
+ constexpr std::uint64_t kMaxSourcePixels =
55
+ static_cast<std::uint64_t>(16U) * 1024U * 1024U;
56
+ constexpr std::uint32_t kMaxInputFrames = 600U;
57
+ constexpr std::uint64_t kMaxAnimationSourcePixels = 128ULL * 1024ULL * 1024ULL;
58
+ constexpr std::uint32_t kMaxAnimationDurationMs = 10'000U;
59
+ constexpr std::uint32_t kMinimumOutputFrameDurationMs = 50U;
60
+ constexpr std::size_t kMaximumStaticStickerBytes =
61
+ static_cast<std::size_t>(100U) * 1024U;
62
+ constexpr std::size_t kMaximumAnimatedStickerBytes =
63
+ static_cast<std::size_t>(500U) * 1024U;
64
+ constexpr std::uint8_t kMaximumOutlineWidth = 64U;
65
+ constexpr int kDistanceInfinity = 1 << 26;
66
+
67
+ struct Image {
68
+ std::uint32_t width = 0U;
69
+ std::uint32_t height = 0U;
70
+ std::vector<Byte> rgba;
71
+ };
72
+
73
+ struct FrameDimensions {
74
+ std::uint32_t width = 0U;
75
+ std::uint32_t height = 0U;
76
+ };
77
+
78
+ struct Bounds {
79
+ bool found = false;
80
+ std::uint32_t left = 0U;
81
+ std::uint32_t top = 0U;
82
+ std::uint32_t right = 0U;
83
+ std::uint32_t bottom = 0U;
84
+ };
85
+
86
+ struct GeometryPlan {
87
+ Bounds bounds;
88
+ std::uint32_t output_width = 0U;
89
+ std::uint32_t output_height = 0U;
90
+ std::uint32_t output_left = 0U;
91
+ std::uint32_t output_top = 0U;
92
+ };
93
+
94
+ struct WebpInfo {
95
+ std::uint32_t width = 0U;
96
+ std::uint32_t height = 0U;
97
+ std::vector<std::uint32_t> frame_durations_ms;
98
+
99
+ [[nodiscard]] bool animated() const { return frame_durations_ms.size() > 1U; }
100
+ };
101
+
102
+ struct VideoInfo {
103
+ std::uint32_t width = 0U;
104
+ std::uint32_t height = 0U;
105
+ std::vector<std::uint32_t> frame_durations_ms;
106
+ };
107
+
108
+ struct VideoTiming {
109
+ std::int64_t stream_duration_ms = AV_NOPTS_VALUE;
110
+ std::uint32_t fallback_frame_duration_ms = kMinimumOutputFrameDurationMs;
111
+ };
112
+
113
+ struct FrameSelection {
114
+ std::uint32_t source_index = 0U;
115
+ std::uint32_t duration_ms = 0U;
116
+ };
117
+
118
+ struct AnimationPlan {
119
+ GeometryPlan geometry;
120
+ std::vector<FrameSelection> selections;
121
+ std::uint32_t duration_ms = 0U;
122
+ };
123
+
124
+ [[noreturn]] void fail(const ErrorCode code, const std::string &message) {
125
+ throw StickerError(code, message);
126
+ }
127
+
128
+ [[nodiscard]] std::size_t checked_pixel_bytes(const std::uint32_t width,
129
+ const std::uint32_t height) {
130
+ if (width == 0U || height == 0U || width > kMaxSourceDimension ||
131
+ height > kMaxSourceDimension) {
132
+ fail(ErrorCode::kResourceLimit,
133
+ "source dimensions exceed the native worker limit");
134
+ }
135
+
136
+ const std::uint64_t pixels =
137
+ static_cast<std::uint64_t>(width) * static_cast<std::uint64_t>(height);
138
+ if (pixels > kMaxSourcePixels ||
139
+ pixels > std::numeric_limits<std::size_t>::max() / 4U) {
140
+ fail(ErrorCode::kResourceLimit,
141
+ "source pixel count exceeds the native worker limit");
142
+ }
143
+
144
+ return static_cast<std::size_t>(pixels) * 4U;
145
+ }
146
+
147
+ [[nodiscard]] std::string ffmpeg_error_message(const int error) {
148
+ std::array<char, AV_ERROR_MAX_STRING_SIZE> text{};
149
+ if (av_strerror(error, text.data(), text.size()) < 0) {
150
+ return "an unknown FFmpeg error";
151
+ }
152
+ return std::string(text.data());
153
+ }
154
+
155
+ struct VideoInput {
156
+ std::span<const Byte> bytes;
157
+ std::size_t offset = 0U;
158
+ };
159
+
160
+ [[nodiscard]] int read_video_input(void *const opaque,
161
+ std::uint8_t *const buffer,
162
+ const int buffer_size) noexcept {
163
+ auto *const input = static_cast<VideoInput *>(opaque);
164
+ if (input == nullptr || buffer == nullptr || buffer_size <= 0 ||
165
+ input->offset >= input->bytes.size()) {
166
+ return AVERROR_EOF;
167
+ }
168
+
169
+ const std::size_t requested = static_cast<std::size_t>(buffer_size);
170
+ const std::size_t available = input->bytes.size() - input->offset;
171
+ const std::size_t copied = std::min(requested, available);
172
+ std::memcpy(buffer, input->bytes.data() + input->offset, copied);
173
+ input->offset += copied;
174
+ return static_cast<int>(copied);
175
+ }
176
+
177
+ // NOLINTNEXTLINE(bugprone-easily-swappable-parameters): FFmpeg AVIO callback
178
+ // ABI.
179
+ [[nodiscard]] std::int64_t seek_video_input(void *const opaque,
180
+ const std::int64_t offset,
181
+ const int whence) noexcept {
182
+ auto *const input = static_cast<VideoInput *>(opaque);
183
+ if (input == nullptr) {
184
+ return AVERROR(EINVAL);
185
+ }
186
+
187
+ const int origin = whence & ~AVSEEK_FORCE;
188
+ if (origin == AVSEEK_SIZE) {
189
+ return static_cast<std::int64_t>(input->bytes.size());
190
+ }
191
+
192
+ std::int64_t base = 0;
193
+ switch (origin) {
194
+ case SEEK_SET:
195
+ base = 0;
196
+ break;
197
+ case SEEK_CUR:
198
+ base = static_cast<std::int64_t>(input->offset);
199
+ break;
200
+ case SEEK_END:
201
+ base = static_cast<std::int64_t>(input->bytes.size());
202
+ break;
203
+ default:
204
+ return AVERROR(EINVAL);
205
+ }
206
+
207
+ if (offset > 0 && offset > std::numeric_limits<std::int64_t>::max() - base) {
208
+ return AVERROR(EINVAL);
209
+ }
210
+ if (offset < 0 && offset < -base) {
211
+ return AVERROR(EINVAL);
212
+ }
213
+ const std::int64_t target = base + offset;
214
+ if (target < 0 || target > static_cast<std::int64_t>(input->bytes.size())) {
215
+ return AVERROR(EINVAL);
216
+ }
217
+ input->offset = static_cast<std::size_t>(target);
218
+ return target;
219
+ }
220
+
221
+ struct AvioContextDeleter {
222
+ void operator()(AVIOContext *const context) const noexcept {
223
+ AVIOContext *owned = context;
224
+ if (owned != nullptr) {
225
+ av_freep(static_cast<void *>(&owned->buffer));
226
+ }
227
+ avio_context_free(&owned);
228
+ }
229
+ };
230
+
231
+ struct FormatContextDeleter {
232
+ void operator()(AVFormatContext *const context) const noexcept {
233
+ AVFormatContext *owned = context;
234
+ avformat_close_input(&owned);
235
+ }
236
+ };
237
+
238
+ struct CodecContextDeleter {
239
+ void operator()(AVCodecContext *const context) const noexcept {
240
+ AVCodecContext *owned = context;
241
+ avcodec_free_context(&owned);
242
+ }
243
+ };
244
+
245
+ struct PacketDeleter {
246
+ void operator()(AVPacket *const packet) const noexcept {
247
+ AVPacket *owned = packet;
248
+ av_packet_free(&owned);
249
+ }
250
+ };
251
+
252
+ struct FrameDeleter {
253
+ void operator()(AVFrame *const frame) const noexcept {
254
+ AVFrame *owned = frame;
255
+ av_frame_free(&owned);
256
+ }
257
+ };
258
+
259
+ struct ScalerDeleter {
260
+ void operator()(SwsContext *const context) const noexcept {
261
+ sws_freeContext(context);
262
+ }
263
+ };
264
+
265
+ class VideoDecoder final {
266
+ public:
267
+ explicit VideoDecoder(const std::span<const Byte> source)
268
+ : input_{.bytes = source} {
269
+ constexpr int kAvioBufferBytes = 4096;
270
+ Byte *const avio_buffer = static_cast<Byte *>(av_malloc(kAvioBufferBytes));
271
+ if (avio_buffer == nullptr) {
272
+ fail(ErrorCode::kResourceLimit,
273
+ "unable to allocate a video input buffer");
274
+ }
275
+ AVIOContext *const avio =
276
+ avio_alloc_context(avio_buffer, kAvioBufferBytes, 0, &input_,
277
+ read_video_input, nullptr, seek_video_input);
278
+ if (avio == nullptr) {
279
+ av_free(avio_buffer);
280
+ fail(ErrorCode::kResourceLimit,
281
+ "unable to initialize video input buffering");
282
+ }
283
+ avio_.reset(avio);
284
+
285
+ AVFormatContext *format = avformat_alloc_context();
286
+ if (format == nullptr) {
287
+ fail(ErrorCode::kResourceLimit,
288
+ "unable to allocate a video format context");
289
+ }
290
+ format->pb = avio_.get();
291
+ format->flags |= AVFMT_FLAG_CUSTOM_IO;
292
+ const int open_status =
293
+ avformat_open_input(&format, nullptr, nullptr, nullptr);
294
+ if (open_status < 0) {
295
+ if (format != nullptr) {
296
+ avformat_free_context(format);
297
+ }
298
+ fail(ErrorCode::kUnsupportedInput,
299
+ "unsupported video input: " + ffmpeg_error_message(open_status));
300
+ }
301
+ format_.reset(format);
302
+
303
+ const int stream_info_status =
304
+ avformat_find_stream_info(format_.get(), nullptr);
305
+ if (stream_info_status < 0) {
306
+ fail(ErrorCode::kInvalidInput,
307
+ "unable to read video stream information: " +
308
+ ffmpeg_error_message(stream_info_status));
309
+ }
310
+ video_stream_index_ = av_find_best_stream(format_.get(), AVMEDIA_TYPE_VIDEO,
311
+ -1, -1, nullptr, 0);
312
+ if (video_stream_index_ < 0) {
313
+ fail(ErrorCode::kUnsupportedInput,
314
+ "input does not contain a decodable video stream");
315
+ }
316
+
317
+ stream_ = format_->streams[static_cast<std::size_t>(video_stream_index_)];
318
+ const AVCodec *const codec =
319
+ avcodec_find_decoder(stream_->codecpar->codec_id);
320
+ if (codec == nullptr) {
321
+ fail(ErrorCode::kUnsupportedInput,
322
+ "video stream uses an unsupported codec");
323
+ }
324
+
325
+ codec_.reset(avcodec_alloc_context3(codec));
326
+ if (codec_ == nullptr) {
327
+ fail(ErrorCode::kResourceLimit,
328
+ "unable to allocate a video decoder context");
329
+ }
330
+ const int parameters_status =
331
+ avcodec_parameters_to_context(codec_.get(), stream_->codecpar);
332
+ if (parameters_status < 0) {
333
+ fail(ErrorCode::kInvalidInput,
334
+ "unable to read video codec parameters: " +
335
+ ffmpeg_error_message(parameters_status));
336
+ }
337
+ const int codec_status = avcodec_open2(codec_.get(), codec, nullptr);
338
+ if (codec_status < 0) {
339
+ fail(ErrorCode::kUnsupportedInput,
340
+ "unable to initialize the video decoder: " +
341
+ ffmpeg_error_message(codec_status));
342
+ }
343
+
344
+ if (codec_->width <= 0 || codec_->height <= 0) {
345
+ fail(ErrorCode::kInvalidInput,
346
+ "video stream does not declare valid dimensions");
347
+ }
348
+ dimensions_ = FrameDimensions{
349
+ .width = static_cast<std::uint32_t>(codec_->width),
350
+ .height = static_cast<std::uint32_t>(codec_->height),
351
+ };
352
+ static_cast<void>(
353
+ checked_pixel_bytes(dimensions_.width, dimensions_.height));
354
+
355
+ packet_.reset(av_packet_alloc());
356
+ frame_.reset(av_frame_alloc());
357
+ if (packet_ == nullptr || frame_ == nullptr) {
358
+ fail(ErrorCode::kResourceLimit,
359
+ "unable to allocate video decoder buffers");
360
+ }
361
+
362
+ time_base_ = stream_->time_base;
363
+ frame_rate_ = av_guess_frame_rate(format_.get(), stream_, nullptr);
364
+ stream_duration_ = stream_->duration;
365
+ }
366
+
367
+ VideoDecoder(const VideoDecoder &) = delete;
368
+ VideoDecoder &operator=(const VideoDecoder &) = delete;
369
+ VideoDecoder(VideoDecoder &&) = delete;
370
+ VideoDecoder &operator=(VideoDecoder &&) = delete;
371
+
372
+ [[nodiscard]] FrameDimensions dimensions() const noexcept {
373
+ return dimensions_;
374
+ }
375
+
376
+ [[nodiscard]] std::uint32_t default_frame_duration_ms() const {
377
+ if (frame_rate_.num <= 0 || frame_rate_.den <= 0) {
378
+ return kMinimumOutputFrameDurationMs;
379
+ }
380
+ const AVRational milliseconds = {.num = 1, .den = 1000};
381
+ const std::int64_t duration =
382
+ av_rescale_q(1, av_inv_q(frame_rate_), milliseconds);
383
+ if (duration <= 0) {
384
+ return 1U;
385
+ }
386
+ return static_cast<std::uint32_t>(
387
+ std::min(duration, static_cast<std::int64_t>(1000)));
388
+ }
389
+
390
+ [[nodiscard]] std::int64_t stream_duration_ms() const {
391
+ if (stream_duration_ == AV_NOPTS_VALUE) {
392
+ return AV_NOPTS_VALUE;
393
+ }
394
+ const AVRational milliseconds = {.num = 1, .den = 1000};
395
+ return av_rescale_q(stream_duration_, time_base_, milliseconds);
396
+ }
397
+
398
+ template <typename Callback> void visit_decoded_frames(Callback &&callback) {
399
+ while (true) {
400
+ const int read_status = av_read_frame(format_.get(), packet_.get());
401
+ if (read_status == AVERROR_EOF) {
402
+ break;
403
+ }
404
+ if (read_status < 0) {
405
+ fail(ErrorCode::kInvalidInput, "unable to read a video packet: " +
406
+ ffmpeg_error_message(read_status));
407
+ }
408
+
409
+ if (packet_->stream_index == video_stream_index_) {
410
+ send_packet(packet_.get(), callback);
411
+ }
412
+ av_packet_unref(packet_.get());
413
+ }
414
+ send_packet(nullptr, callback);
415
+ }
416
+
417
+ [[nodiscard]] Image convert_to_rgba(const AVFrame &frame) {
418
+ if (frame.width != static_cast<int>(dimensions_.width) ||
419
+ frame.height != static_cast<int>(dimensions_.height) ||
420
+ frame.format < 0) {
421
+ fail(ErrorCode::kInvalidInput,
422
+ "video frame dimensions or pixel format changed during decoding");
423
+ }
424
+ const AVPixelFormat source_format =
425
+ static_cast<AVPixelFormat>(frame.format);
426
+ if (scaler_ == nullptr || source_format != scaler_source_format_) {
427
+ scaler_.reset(sws_getContext(frame.width, frame.height, source_format,
428
+ frame.width, frame.height, AV_PIX_FMT_RGBA,
429
+ SWS_BILINEAR, nullptr, nullptr, nullptr));
430
+ if (scaler_ == nullptr) {
431
+ fail(ErrorCode::kUnsupportedInput,
432
+ "unable to convert the video pixel format to RGBA");
433
+ }
434
+ scaler_source_format_ = source_format;
435
+ }
436
+
437
+ Image image{
438
+ .width = dimensions_.width,
439
+ .height = dimensions_.height,
440
+ .rgba = std::vector<Byte>(
441
+ checked_pixel_bytes(dimensions_.width, dimensions_.height)),
442
+ };
443
+ std::array<Byte *, 4U> destination = {nullptr, nullptr, nullptr, nullptr};
444
+ std::array<int, 4U> destination_stride = {0, 0, 0, 0};
445
+ const int destination_size = av_image_alloc(
446
+ destination.data(), destination_stride.data(),
447
+ static_cast<int>(dimensions_.width),
448
+ static_cast<int>(dimensions_.height), AV_PIX_FMT_RGBA, 32);
449
+ if (destination_size < 0 || destination[0] == nullptr) {
450
+ fail(ErrorCode::kResourceLimit,
451
+ "unable to allocate an aligned video RGBA frame");
452
+ }
453
+ std::unique_ptr<Byte, decltype(&av_free)> owned_destination(destination[0],
454
+ av_free);
455
+ const int converted =
456
+ sws_scale(scaler_.get(), frame.data, frame.linesize, 0, frame.height,
457
+ destination.data(), destination_stride.data());
458
+ if (converted != frame.height) {
459
+ fail(ErrorCode::kInvalidInput,
460
+ "unable to convert a decoded video frame to RGBA");
461
+ }
462
+ for (std::uint32_t row = 0U; row < dimensions_.height; ++row) {
463
+ std::memcpy(image.rgba.data() +
464
+ static_cast<std::size_t>(row) * dimensions_.width * 4U,
465
+ destination[0] +
466
+ static_cast<std::size_t>(row) *
467
+ static_cast<std::size_t>(destination_stride[0]),
468
+ static_cast<std::size_t>(dimensions_.width) * 4U);
469
+ }
470
+ return image;
471
+ }
472
+
473
+ private:
474
+ template <typename Callback>
475
+ void send_packet(const AVPacket *const packet, Callback &&callback) {
476
+ const int send_status = avcodec_send_packet(codec_.get(), packet);
477
+ if (send_status < 0) {
478
+ fail(ErrorCode::kInvalidInput,
479
+ "unable to send a video packet to the decoder: " +
480
+ ffmpeg_error_message(send_status));
481
+ }
482
+
483
+ while (true) {
484
+ const int receive_status =
485
+ avcodec_receive_frame(codec_.get(), frame_.get());
486
+ if (receive_status == AVERROR(EAGAIN) || receive_status == AVERROR_EOF) {
487
+ return;
488
+ }
489
+ if (receive_status < 0) {
490
+ fail(ErrorCode::kInvalidInput,
491
+ "unable to decode a video frame: " +
492
+ ffmpeg_error_message(receive_status));
493
+ }
494
+
495
+ std::int64_t timestamp = frame_->best_effort_timestamp;
496
+ if (timestamp != AV_NOPTS_VALUE) {
497
+ const AVRational milliseconds = {.num = 1, .den = 1000};
498
+ timestamp = av_rescale_q(timestamp, time_base_, milliseconds);
499
+ }
500
+ callback(*frame_, timestamp);
501
+ av_frame_unref(frame_.get());
502
+ }
503
+ }
504
+
505
+ VideoInput input_;
506
+ std::unique_ptr<AVIOContext, AvioContextDeleter> avio_;
507
+ std::unique_ptr<AVFormatContext, FormatContextDeleter> format_;
508
+ std::unique_ptr<AVCodecContext, CodecContextDeleter> codec_;
509
+ std::unique_ptr<AVPacket, PacketDeleter> packet_;
510
+ std::unique_ptr<AVFrame, FrameDeleter> frame_;
511
+ std::unique_ptr<SwsContext, ScalerDeleter> scaler_;
512
+ AVStream *stream_ = nullptr;
513
+ FrameDimensions dimensions_{};
514
+ AVRational time_base_{};
515
+ AVRational frame_rate_{};
516
+ std::int64_t stream_duration_ = AV_NOPTS_VALUE;
517
+ int video_stream_index_ = -1;
518
+ AVPixelFormat scaler_source_format_ = AV_PIX_FMT_NONE;
519
+ };
520
+
521
+ [[nodiscard]] std::size_t pixel_offset(const std::uint32_t x,
522
+ const std::uint32_t y,
523
+ const std::uint32_t width) {
524
+ return (static_cast<std::size_t>(y) * width + x) * 4U;
525
+ }
526
+
527
+ template <std::size_t N>
528
+ [[nodiscard]] bool starts_with(const std::span<const Byte> bytes,
529
+ const std::array<Byte, N> &prefix) {
530
+ return bytes.size() >= N && std::memcmp(bytes.data(), prefix.data(), N) == 0;
531
+ }
532
+
533
+ [[nodiscard]] bool is_png(const std::span<const Byte> bytes) {
534
+ constexpr std::array<Byte, 8U> kPngSignature = {0x89U, 0x50U, 0x4eU, 0x47U,
535
+ 0x0dU, 0x0aU, 0x1aU, 0x0aU};
536
+ return starts_with(bytes, kPngSignature);
537
+ }
538
+
539
+ [[nodiscard]] std::uint32_t
540
+ read_big_endian_u32(const std::span<const Byte> bytes,
541
+ const std::size_t offset) {
542
+ return (static_cast<std::uint32_t>(bytes[offset]) << 24U) |
543
+ (static_cast<std::uint32_t>(bytes[offset + 1U]) << 16U) |
544
+ (static_cast<std::uint32_t>(bytes[offset + 2U]) << 8U) |
545
+ static_cast<std::uint32_t>(bytes[offset + 3U]);
546
+ }
547
+
548
+ [[nodiscard]] bool is_apng(const std::span<const Byte> bytes) {
549
+ constexpr std::array<Byte, 4U> kAnimationControl = {'a', 'c', 'T', 'L'};
550
+ constexpr std::size_t kPngSignatureBytes = 8U;
551
+ constexpr std::size_t kPngChunkOverheadBytes = 12U;
552
+
553
+ if (!is_png(bytes)) {
554
+ return false;
555
+ }
556
+
557
+ std::size_t offset = kPngSignatureBytes;
558
+ while (bytes.size() - offset >= kPngChunkOverheadBytes) {
559
+ const std::size_t bytes_remaining = bytes.size() - offset;
560
+ const std::size_t data_size =
561
+ static_cast<std::size_t>(read_big_endian_u32(bytes, offset));
562
+ if (data_size > bytes_remaining - kPngChunkOverheadBytes) {
563
+ return false;
564
+ }
565
+ if (std::memcmp(bytes.data() + offset + 4U, kAnimationControl.data(),
566
+ kAnimationControl.size()) == 0) {
567
+ return true;
568
+ }
569
+ offset += kPngChunkOverheadBytes + data_size;
570
+ }
571
+ return false;
572
+ }
573
+
574
+ [[nodiscard]] bool is_jpeg(const std::span<const Byte> bytes) {
575
+ return bytes.size() >= 3U && bytes[0] == 0xffU && bytes[1] == 0xd8U &&
576
+ bytes[2] == 0xffU;
577
+ }
578
+
579
+ [[nodiscard]] bool is_gif(const std::span<const Byte> bytes) {
580
+ constexpr std::array<Byte, 6U> kGif87a = {'G', 'I', 'F', '8', '7', 'a'};
581
+ constexpr std::array<Byte, 6U> kGif89a = {'G', 'I', 'F', '8', '9', 'a'};
582
+ return starts_with(bytes, kGif87a) || starts_with(bytes, kGif89a);
583
+ }
584
+
585
+ [[nodiscard]] bool is_webp(const std::span<const Byte> bytes) {
586
+ constexpr std::array<Byte, 4U> kRiff = {0x52U, 0x49U, 0x46U, 0x46U};
587
+ constexpr std::array<Byte, 4U> kWebp = {0x57U, 0x45U, 0x42U, 0x50U};
588
+ return bytes.size() >= 12U && starts_with(bytes, kRiff) &&
589
+ std::memcmp(bytes.data() + 8U, kWebp.data(), kWebp.size()) == 0;
590
+ }
591
+
592
+ class PngImage final {
593
+ public:
594
+ PngImage() { image_.version = PNG_IMAGE_VERSION; }
595
+
596
+ ~PngImage() { png_image_free(&image_); }
597
+
598
+ PngImage(const PngImage &) = delete;
599
+ PngImage &operator=(const PngImage &) = delete;
600
+ PngImage(PngImage &&) = delete;
601
+ PngImage &operator=(PngImage &&) = delete;
602
+
603
+ [[nodiscard]] png_image *get() { return &image_; }
604
+
605
+ private:
606
+ png_image image_{};
607
+ };
608
+
609
+ [[nodiscard]] Image decode_png(const std::span<const Byte> bytes) {
610
+ PngImage image;
611
+ if (png_image_begin_read_from_memory(image.get(), bytes.data(),
612
+ bytes.size()) == 0) {
613
+ fail(ErrorCode::kInvalidInput, "unable to decode PNG input");
614
+ }
615
+
616
+ const std::size_t byte_count =
617
+ checked_pixel_bytes(image.get()->width, image.get()->height);
618
+ Image decoded{
619
+ .width = image.get()->width,
620
+ .height = image.get()->height,
621
+ .rgba = std::vector<Byte>(byte_count),
622
+ };
623
+ image.get()->format = PNG_FORMAT_RGBA;
624
+ const int succeeded = png_image_finish_read(image.get(), nullptr,
625
+ decoded.rgba.data(), 0, nullptr);
626
+ if (succeeded == 0) {
627
+ fail(ErrorCode::kInvalidInput, "unable to finish PNG decode");
628
+ }
629
+ return decoded;
630
+ }
631
+
632
+ struct JpegErrorManager {
633
+ jpeg_error_mgr base{};
634
+ std::jmp_buf jump_buffer{};
635
+ char message[JMSG_LENGTH_MAX]{};
636
+ };
637
+
638
+ struct FreeByteBuffer {
639
+ void operator()(Byte *const buffer) const noexcept { std::free(buffer); }
640
+ };
641
+
642
+ struct JpegDecodeState {
643
+ jpeg_decompress_struct decoder{};
644
+ JpegErrorManager error{};
645
+ bool decoder_created = false;
646
+ std::unique_ptr<Image> decoded;
647
+ std::unique_ptr<Byte, FreeByteBuffer> scanline;
648
+ };
649
+
650
+ extern "C" void sticker_jpeg_error_exit(j_common_ptr cinfo) {
651
+ auto *const error = reinterpret_cast<JpegErrorManager *>(cinfo->err);
652
+ (*cinfo->err->format_message)(cinfo, error->message);
653
+ std::longjmp(error->jump_buffer, 1);
654
+ }
655
+
656
+ void destroy_jpeg_decode_state(JpegDecodeState *const state) noexcept {
657
+ if (state == nullptr) {
658
+ return;
659
+ }
660
+ if (state->decoder_created) {
661
+ jpeg_destroy_decompress(&state->decoder);
662
+ }
663
+ delete state;
664
+ }
665
+
666
+ [[nodiscard]] Image decode_jpeg(const std::span<const Byte> bytes) {
667
+ // libjpeg reports malformed input through longjmp. State modified during
668
+ // decoding lives on the heap, while the unmodified owner exists before
669
+ // setjmp; no mutated C++ automatic object is observed after a longjmp.
670
+ using JpegStatePtr =
671
+ std::unique_ptr<JpegDecodeState, decltype(&destroy_jpeg_decode_state)>;
672
+ JpegStatePtr state(new (std::nothrow) JpegDecodeState{},
673
+ destroy_jpeg_decode_state);
674
+ if (!state) {
675
+ fail(ErrorCode::kResourceLimit, "unable to allocate JPEG decoder state");
676
+ }
677
+
678
+ state->decoder.err = jpeg_std_error(&state->error.base);
679
+ state->error.base.error_exit = sticker_jpeg_error_exit;
680
+ if (setjmp(state->error.jump_buffer) != 0) {
681
+ fail(ErrorCode::kInvalidInput,
682
+ std::string("unable to decode JPEG input: ") + state->error.message);
683
+ }
684
+
685
+ jpeg_create_decompress(&state->decoder);
686
+ state->decoder_created = true;
687
+ jpeg_mem_src(&state->decoder,
688
+ reinterpret_cast<const unsigned char *>(bytes.data()),
689
+ static_cast<unsigned long>(bytes.size()));
690
+ if (jpeg_read_header(&state->decoder, TRUE) != JPEG_HEADER_OK) {
691
+ fail(ErrorCode::kInvalidInput, "unable to read JPEG header");
692
+ }
693
+
694
+ if (state->decoder.image_width == 0U || state->decoder.image_height == 0U ||
695
+ state->decoder.image_width > kMaxSourceDimension ||
696
+ state->decoder.image_height > kMaxSourceDimension) {
697
+ fail(ErrorCode::kResourceLimit,
698
+ "JPEG dimensions exceed the native worker limit");
699
+ }
700
+
701
+ state->decoder.out_color_space = JCS_RGB;
702
+ if (jpeg_start_decompress(&state->decoder) == FALSE ||
703
+ state->decoder.output_components != 3) {
704
+ fail(ErrorCode::kInvalidInput, "unable to start JPEG RGB decode");
705
+ }
706
+
707
+ const std::uint32_t width = state->decoder.output_width;
708
+ const std::uint32_t height = state->decoder.output_height;
709
+ const std::size_t output_size = checked_pixel_bytes(width, height);
710
+ state->decoded = std::make_unique<Image>(Image{
711
+ .width = width,
712
+ .height = height,
713
+ .rgba = std::vector<Byte>(output_size),
714
+ });
715
+ state->scanline.reset(
716
+ static_cast<Byte *>(std::malloc(static_cast<std::size_t>(width) * 3U)));
717
+ if (!state->scanline) {
718
+ fail(ErrorCode::kResourceLimit, "unable to allocate JPEG scanline");
719
+ }
720
+
721
+ for (std::uint32_t y = 0U; y < height; ++y) {
722
+ JSAMPROW row = state->scanline.get();
723
+ if (jpeg_read_scanlines(&state->decoder, &row, 1U) != 1U) {
724
+ fail(ErrorCode::kInvalidInput, "unable to read a JPEG scanline");
725
+ }
726
+
727
+ for (std::uint32_t x = 0U; x < width; ++x) {
728
+ const std::size_t source_offset = static_cast<std::size_t>(x) * 3U;
729
+ const std::size_t destination_offset = pixel_offset(x, y, width);
730
+ state->decoded->rgba[destination_offset] =
731
+ state->scanline.get()[source_offset];
732
+ state->decoded->rgba[destination_offset + 1U] =
733
+ state->scanline.get()[source_offset + 1U];
734
+ state->decoded->rgba[destination_offset + 2U] =
735
+ state->scanline.get()[source_offset + 2U];
736
+ state->decoded->rgba[destination_offset + 3U] = 255U;
737
+ }
738
+ }
739
+
740
+ if (jpeg_finish_decompress(&state->decoder) == FALSE) {
741
+ fail(ErrorCode::kInvalidInput, "unable to finish JPEG decode");
742
+ }
743
+ jpeg_destroy_decompress(&state->decoder);
744
+ state->decoder_created = false;
745
+ Image decoded = std::move(*state->decoded);
746
+ state->decoded.reset();
747
+ return decoded;
748
+ }
749
+
750
+ [[nodiscard]] WebpInfo inspect_webp(const std::span<const Byte> bytes) {
751
+ WebPData data{};
752
+ data.bytes = bytes.data();
753
+ data.size = bytes.size();
754
+ using DemuxerPtr = std::unique_ptr<WebPDemuxer, decltype(&WebPDemuxDelete)>;
755
+ DemuxerPtr demuxer(WebPDemux(&data), WebPDemuxDelete);
756
+ if (!demuxer) {
757
+ fail(ErrorCode::kInvalidInput, "unable to parse WebP input");
758
+ }
759
+
760
+ const std::uint32_t width =
761
+ WebPDemuxGetI(demuxer.get(), WEBP_FF_CANVAS_WIDTH);
762
+ const std::uint32_t height =
763
+ WebPDemuxGetI(demuxer.get(), WEBP_FF_CANVAS_HEIGHT);
764
+ static_cast<void>(checked_pixel_bytes(width, height));
765
+
766
+ const std::uint32_t frame_count =
767
+ WebPDemuxGetI(demuxer.get(), WEBP_FF_FRAME_COUNT);
768
+ if (frame_count == 0U || frame_count > kMaxInputFrames) {
769
+ fail(ErrorCode::kResourceLimit,
770
+ "WebP frame count exceeds the native worker limit");
771
+ }
772
+ const std::uint64_t source_pixels =
773
+ static_cast<std::uint64_t>(width) * static_cast<std::uint64_t>(height);
774
+ if (static_cast<std::uint64_t>(frame_count) >
775
+ kMaxAnimationSourcePixels / source_pixels) {
776
+ fail(ErrorCode::kResourceLimit,
777
+ "WebP animation exceeds the native worker aggregate pixel limit");
778
+ }
779
+
780
+ WebpInfo info{
781
+ .width = width,
782
+ .height = height,
783
+ .frame_durations_ms = {},
784
+ };
785
+ info.frame_durations_ms.reserve(frame_count);
786
+ for (std::uint32_t index = 1U; index <= frame_count; ++index) {
787
+ WebPIterator iterator{};
788
+ if (WebPDemuxGetFrame(demuxer.get(), static_cast<int>(index), &iterator) ==
789
+ 0) {
790
+ fail(ErrorCode::kInvalidInput, "unable to inspect a WebP frame");
791
+ }
792
+ info.frame_durations_ms.push_back(
793
+ static_cast<std::uint32_t>(std::max(iterator.duration, 0)));
794
+ WebPDemuxReleaseIterator(&iterator);
795
+ }
796
+ return info;
797
+ }
798
+
799
+ [[nodiscard]] Image decode_static_webp(const std::span<const Byte> bytes,
800
+ const WebpInfo &info) {
801
+ WebPBitstreamFeatures features{};
802
+ if (WebPGetFeatures(bytes.data(), bytes.size(), &features) != VP8_STATUS_OK) {
803
+ fail(ErrorCode::kInvalidInput, "unable to read WebP features");
804
+ }
805
+ if (features.width != static_cast<int>(info.width) ||
806
+ features.height != static_cast<int>(info.height)) {
807
+ fail(ErrorCode::kInvalidInput, "WebP canvas dimensions are inconsistent");
808
+ }
809
+
810
+ Image decoded{
811
+ .width = info.width,
812
+ .height = info.height,
813
+ .rgba = std::vector<Byte>(checked_pixel_bytes(info.width, info.height)),
814
+ };
815
+ if (WebPDecodeRGBAInto(bytes.data(), bytes.size(), decoded.rgba.data(),
816
+ decoded.rgba.size(),
817
+ static_cast<int>(info.width * 4U)) == nullptr) {
818
+ fail(ErrorCode::kInvalidInput, "unable to decode WebP pixels");
819
+ }
820
+ return decoded;
821
+ }
822
+
823
+ template <typename Callback>
824
+ void visit_animation_frames(const std::span<const Byte> bytes,
825
+ const WebpInfo &info, Callback &&callback) {
826
+ WebPData data{};
827
+ data.bytes = bytes.data();
828
+ data.size = bytes.size();
829
+
830
+ WebPAnimDecoderOptions options{};
831
+ if (WebPAnimDecoderOptionsInit(&options) == 0) {
832
+ fail(ErrorCode::kInternal,
833
+ "unable to initialize the WebP animation decoder");
834
+ }
835
+ options.color_mode = MODE_RGBA;
836
+ options.use_threads = 1;
837
+
838
+ using DecoderPtr =
839
+ std::unique_ptr<WebPAnimDecoder, decltype(&WebPAnimDecoderDelete)>;
840
+ DecoderPtr decoder(WebPAnimDecoderNew(&data, &options),
841
+ WebPAnimDecoderDelete);
842
+ if (!decoder) {
843
+ fail(ErrorCode::kInvalidInput,
844
+ "unable to create the WebP animation decoder");
845
+ }
846
+
847
+ WebPAnimInfo animation_info{};
848
+ if (WebPAnimDecoderGetInfo(decoder.get(), &animation_info) == 0 ||
849
+ animation_info.canvas_width != info.width ||
850
+ animation_info.canvas_height != info.height ||
851
+ static_cast<std::size_t>(animation_info.frame_count) !=
852
+ info.frame_durations_ms.size()) {
853
+ fail(ErrorCode::kInvalidInput, "WebP animation metadata is inconsistent");
854
+ }
855
+
856
+ const std::size_t frame_bytes = checked_pixel_bytes(info.width, info.height);
857
+ std::uint32_t index = 0U;
858
+ while (WebPAnimDecoderHasMoreFrames(decoder.get()) != 0) {
859
+ Byte *rgba = nullptr;
860
+ int timestamp = 0;
861
+ if (WebPAnimDecoderGetNext(decoder.get(), &rgba, &timestamp) == 0 ||
862
+ rgba == nullptr) {
863
+ fail(ErrorCode::kInvalidInput, "unable to decode a WebP animation frame");
864
+ }
865
+ static_cast<void>(timestamp);
866
+ callback(index, std::span<const Byte>(rgba, frame_bytes));
867
+ ++index;
868
+ }
869
+
870
+ if (index != info.frame_durations_ms.size()) {
871
+ fail(ErrorCode::kInvalidInput,
872
+ "WebP animation frame count changed during decode");
873
+ }
874
+ }
875
+
876
+ [[nodiscard]] std::vector<std::uint32_t>
877
+ video_frame_durations(const std::vector<std::int64_t> &timestamps,
878
+ const VideoTiming timing) {
879
+ if (timestamps.empty()) {
880
+ fail(ErrorCode::kInvalidInput, "video stream has no decoded frames");
881
+ }
882
+
883
+ std::vector<std::uint32_t> durations(timestamps.size(),
884
+ timing.fallback_frame_duration_ms);
885
+ std::uint64_t total_duration_ms = 0U;
886
+ for (std::size_t index = 0U; index < timestamps.size(); ++index) {
887
+ std::int64_t duration =
888
+ static_cast<std::int64_t>(timing.fallback_frame_duration_ms);
889
+ if (index + 1U < timestamps.size()) {
890
+ duration = timestamps[index + 1U] - timestamps[index];
891
+ } else if (timing.stream_duration_ms > timestamps[index]) {
892
+ duration = timing.stream_duration_ms - timestamps[index];
893
+ }
894
+ if (duration <= 0) {
895
+ duration = static_cast<std::int64_t>(timing.fallback_frame_duration_ms);
896
+ }
897
+ if (duration > static_cast<std::int64_t>(kMaxAnimationDurationMs)) {
898
+ fail(ErrorCode::kResourceLimit,
899
+ "video frame duration exceeds the native worker limit");
900
+ }
901
+
902
+ total_duration_ms += static_cast<std::uint64_t>(duration);
903
+ if (total_duration_ms > kMaxAnimationDurationMs) {
904
+ fail(ErrorCode::kResourceLimit,
905
+ "video duration exceeds the WhatsApp duration limit");
906
+ }
907
+ durations[index] = static_cast<std::uint32_t>(duration);
908
+ }
909
+ return durations;
910
+ }
911
+
912
+ [[nodiscard]] VideoInfo inspect_video(const std::span<const Byte> source) {
913
+ VideoDecoder decoder(source);
914
+ const FrameDimensions dimensions = decoder.dimensions();
915
+ const std::uint64_t source_pixels =
916
+ static_cast<std::uint64_t>(dimensions.width) * dimensions.height;
917
+ const std::uint32_t fallback_duration_ms =
918
+ decoder.default_frame_duration_ms();
919
+
920
+ std::vector<std::int64_t> timestamps;
921
+ timestamps.reserve(kMaxInputFrames);
922
+ bool has_timestamp_origin = false;
923
+ std::int64_t timestamp_origin = 0;
924
+ std::int64_t previous_timestamp = 0;
925
+
926
+ decoder.visit_decoded_frames([&](const AVFrame &frame,
927
+ const std::int64_t timestamp) {
928
+ if (frame.width != static_cast<int>(dimensions.width) ||
929
+ frame.height != static_cast<int>(dimensions.height)) {
930
+ fail(ErrorCode::kInvalidInput,
931
+ "video frame dimensions changed during decoding");
932
+ }
933
+ if (timestamps.size() >= kMaxInputFrames ||
934
+ static_cast<std::uint64_t>(timestamps.size() + 1U) >
935
+ kMaxAnimationSourcePixels / source_pixels) {
936
+ fail(ErrorCode::kResourceLimit,
937
+ "video animation exceeds the native worker frame or pixel limit");
938
+ }
939
+
940
+ std::int64_t normalized = 0;
941
+ if (timestamp != AV_NOPTS_VALUE) {
942
+ if (!has_timestamp_origin) {
943
+ timestamp_origin = timestamp;
944
+ has_timestamp_origin = true;
945
+ }
946
+ if (timestamp >= timestamp_origin) {
947
+ if (timestamp_origin < 0 &&
948
+ timestamp >
949
+ std::numeric_limits<std::int64_t>::max() + timestamp_origin) {
950
+ fail(ErrorCode::kInvalidInput,
951
+ "video frame timestamps exceed the native worker range");
952
+ }
953
+ normalized = timestamp - timestamp_origin;
954
+ }
955
+ }
956
+ if (!timestamps.empty()) {
957
+ if (timestamp == AV_NOPTS_VALUE || normalized <= previous_timestamp) {
958
+ if (previous_timestamp >
959
+ std::numeric_limits<std::int64_t>::max() -
960
+ static_cast<std::int64_t>(fallback_duration_ms)) {
961
+ fail(ErrorCode::kInvalidInput,
962
+ "video frame timestamps overflow the native worker range");
963
+ }
964
+ normalized = previous_timestamp +
965
+ static_cast<std::int64_t>(fallback_duration_ms);
966
+ }
967
+ }
968
+ timestamps.push_back(normalized);
969
+ previous_timestamp = normalized;
970
+ });
971
+
972
+ return VideoInfo{
973
+ .width = dimensions.width,
974
+ .height = dimensions.height,
975
+ .frame_durations_ms = video_frame_durations(
976
+ timestamps,
977
+ VideoTiming{
978
+ .stream_duration_ms = decoder.stream_duration_ms(),
979
+ .fallback_frame_duration_ms = fallback_duration_ms,
980
+ }),
981
+ };
982
+ }
983
+
984
+ template <typename Callback>
985
+ void visit_video_frames(const std::span<const Byte> source,
986
+ const VideoInfo &info, Callback &&callback) {
987
+ VideoDecoder decoder(source);
988
+ const FrameDimensions dimensions = decoder.dimensions();
989
+ if (dimensions.width != info.width || dimensions.height != info.height) {
990
+ fail(ErrorCode::kInvalidInput,
991
+ "video dimensions changed between decoding passes");
992
+ }
993
+
994
+ std::uint32_t index = 0U;
995
+ decoder.visit_decoded_frames(
996
+ [&](const AVFrame &frame, const std::int64_t timestamp) {
997
+ static_cast<void>(timestamp);
998
+ if (index >= info.frame_durations_ms.size()) {
999
+ fail(ErrorCode::kInvalidInput,
1000
+ "video frame count changed between decoding passes");
1001
+ }
1002
+ const Image rgba = decoder.convert_to_rgba(frame);
1003
+ callback(index, std::span<const Byte>(rgba.rgba));
1004
+ ++index;
1005
+ });
1006
+ if (index != info.frame_durations_ms.size()) {
1007
+ fail(ErrorCode::kInvalidInput,
1008
+ "video frame count changed between decoding passes");
1009
+ }
1010
+ }
1011
+
1012
+ void extend_bounds(Bounds &bounds, const std::span<const Byte> rgba,
1013
+ const FrameDimensions dimensions,
1014
+ const std::uint8_t alpha_threshold) {
1015
+ for (std::uint32_t y = 0U; y < dimensions.height; ++y) {
1016
+ for (std::uint32_t x = 0U; x < dimensions.width; ++x) {
1017
+ if (rgba[pixel_offset(x, y, dimensions.width) + 3U] <= alpha_threshold) {
1018
+ continue;
1019
+ }
1020
+ if (!bounds.found) {
1021
+ bounds = Bounds{
1022
+ .found = true,
1023
+ .left = x,
1024
+ .top = y,
1025
+ .right = x,
1026
+ .bottom = y,
1027
+ };
1028
+ continue;
1029
+ }
1030
+ bounds.left = std::min(bounds.left, x);
1031
+ bounds.top = std::min(bounds.top, y);
1032
+ bounds.right = std::max(bounds.right, x);
1033
+ bounds.bottom = std::max(bounds.bottom, y);
1034
+ }
1035
+ }
1036
+ }
1037
+
1038
+ [[nodiscard]] GeometryPlan make_geometry_plan(Bounds bounds,
1039
+ const std::uint32_t source_width,
1040
+ const std::uint32_t source_height,
1041
+ const TranscodeOptions &options) {
1042
+ if (!options.trim) {
1043
+ bounds = Bounds{
1044
+ .found = true,
1045
+ .left = 0U,
1046
+ .top = 0U,
1047
+ .right = source_width - 1U,
1048
+ .bottom = source_height - 1U,
1049
+ };
1050
+ }
1051
+ if (!bounds.found) {
1052
+ fail(ErrorCode::kInvalidInput,
1053
+ "the sticker source has no visible pixels after alpha trimming");
1054
+ }
1055
+ if (options.outline_width > kMaximumOutlineWidth ||
1056
+ options.outline_width * 2U >= kCanvasSize) {
1057
+ fail(ErrorCode::kInvalidInput,
1058
+ "outline width is outside the supported range");
1059
+ }
1060
+
1061
+ const std::uint32_t bounds_width = bounds.right - bounds.left + 1U;
1062
+ const std::uint32_t bounds_height = bounds.bottom - bounds.top + 1U;
1063
+ const std::uint32_t usable_size =
1064
+ kCanvasSize - static_cast<std::uint32_t>(options.outline_width) * 2U;
1065
+ const double scale = std::min(
1066
+ static_cast<double>(usable_size) / static_cast<double>(bounds_width),
1067
+ static_cast<double>(usable_size) / static_cast<double>(bounds_height));
1068
+ const auto scaled_width = static_cast<std::uint32_t>(std::clamp(
1069
+ std::lround(bounds_width * scale), 1L, static_cast<long>(usable_size)));
1070
+ const auto scaled_height = static_cast<std::uint32_t>(std::clamp(
1071
+ std::lround(bounds_height * scale), 1L, static_cast<long>(usable_size)));
1072
+ return GeometryPlan{
1073
+ .bounds = bounds,
1074
+ .output_width = scaled_width,
1075
+ .output_height = scaled_height,
1076
+ .output_left = (kCanvasSize - scaled_width) / 2U,
1077
+ .output_top = (kCanvasSize - scaled_height) / 2U,
1078
+ };
1079
+ }
1080
+
1081
+ [[nodiscard]] Byte rounded_byte(const double value) {
1082
+ return static_cast<Byte>(
1083
+ std::clamp(std::lround(value), 0L, static_cast<long>(255U)));
1084
+ }
1085
+
1086
+ [[nodiscard]] Image render_frame(const std::span<const Byte> source,
1087
+ const std::uint32_t source_width,
1088
+ const std::uint32_t source_height,
1089
+ const GeometryPlan &plan) {
1090
+ const std::size_t expected_source_bytes =
1091
+ checked_pixel_bytes(source_width, source_height);
1092
+ if (source.size() != expected_source_bytes) {
1093
+ fail(ErrorCode::kInternal,
1094
+ "renderer source buffer does not match its dimensions");
1095
+ }
1096
+ if (!plan.bounds.found || plan.bounds.left > plan.bounds.right ||
1097
+ plan.bounds.top > plan.bounds.bottom ||
1098
+ plan.bounds.right >= source_width ||
1099
+ plan.bounds.bottom >= source_height || plan.output_width == 0U ||
1100
+ plan.output_height == 0U || plan.output_width > kCanvasSize ||
1101
+ plan.output_height > kCanvasSize ||
1102
+ plan.output_left > kCanvasSize - plan.output_width ||
1103
+ plan.output_top > kCanvasSize - plan.output_height) {
1104
+ fail(ErrorCode::kInternal, "renderer received an invalid geometry plan");
1105
+ }
1106
+
1107
+ Image output{
1108
+ .width = kCanvasSize,
1109
+ .height = kCanvasSize,
1110
+ .rgba =
1111
+ std::vector<Byte>(checked_pixel_bytes(kCanvasSize, kCanvasSize), 0U),
1112
+ };
1113
+
1114
+ const std::uint32_t crop_width = plan.bounds.right - plan.bounds.left + 1U;
1115
+ const std::uint32_t crop_height = plan.bounds.bottom - plan.bounds.top + 1U;
1116
+ for (std::uint32_t destination_y = 0U; destination_y < plan.output_height;
1117
+ ++destination_y) {
1118
+ const double source_y = static_cast<double>(plan.bounds.top) +
1119
+ ((static_cast<double>(destination_y) + 0.5) *
1120
+ crop_height / plan.output_height -
1121
+ 0.5);
1122
+ const double clamped_source_y =
1123
+ std::clamp(source_y, static_cast<double>(plan.bounds.top),
1124
+ static_cast<double>(plan.bounds.bottom));
1125
+ const double source_y_floor = std::floor(clamped_source_y);
1126
+ const auto source_y0 = static_cast<std::uint32_t>(source_y_floor);
1127
+ const auto source_y1 = std::min(source_y0 + 1U, plan.bounds.bottom);
1128
+ const double y_weight = clamped_source_y - source_y_floor;
1129
+
1130
+ for (std::uint32_t destination_x = 0U; destination_x < plan.output_width;
1131
+ ++destination_x) {
1132
+ const double source_x = static_cast<double>(plan.bounds.left) +
1133
+ ((static_cast<double>(destination_x) + 0.5) *
1134
+ crop_width / plan.output_width -
1135
+ 0.5);
1136
+ const double clamped_source_x =
1137
+ std::clamp(source_x, static_cast<double>(plan.bounds.left),
1138
+ static_cast<double>(plan.bounds.right));
1139
+ const double source_x_floor = std::floor(clamped_source_x);
1140
+ const auto source_x0 = static_cast<std::uint32_t>(source_x_floor);
1141
+ const auto source_x1 = std::min(source_x0 + 1U, plan.bounds.right);
1142
+ const double x_weight = clamped_source_x - source_x_floor;
1143
+
1144
+ const std::array<std::size_t, 4U> offsets = {
1145
+ pixel_offset(source_x0, source_y0, source_width),
1146
+ pixel_offset(source_x1, source_y0, source_width),
1147
+ pixel_offset(source_x0, source_y1, source_width),
1148
+ pixel_offset(source_x1, source_y1, source_width),
1149
+ };
1150
+ const std::array<double, 4U> weights = {
1151
+ (1.0 - x_weight) * (1.0 - y_weight),
1152
+ x_weight * (1.0 - y_weight),
1153
+ (1.0 - x_weight) * y_weight,
1154
+ x_weight * y_weight,
1155
+ };
1156
+
1157
+ double alpha = 0.0;
1158
+ double premultiplied_red = 0.0;
1159
+ double premultiplied_green = 0.0;
1160
+ double premultiplied_blue = 0.0;
1161
+ for (std::size_t sample = 0U; sample < offsets.size(); ++sample) {
1162
+ const std::size_t offset = offsets[sample];
1163
+ const double sample_alpha =
1164
+ static_cast<double>(source[offset + 3U]) * weights[sample];
1165
+ alpha += sample_alpha;
1166
+ premultiplied_red += static_cast<double>(source[offset]) * sample_alpha;
1167
+ premultiplied_green +=
1168
+ static_cast<double>(source[offset + 1U]) * sample_alpha;
1169
+ premultiplied_blue +=
1170
+ static_cast<double>(source[offset + 2U]) * sample_alpha;
1171
+ }
1172
+
1173
+ const std::uint32_t output_x = plan.output_left + destination_x;
1174
+ const std::uint32_t output_y = plan.output_top + destination_y;
1175
+ const std::size_t output_offset =
1176
+ pixel_offset(output_x, output_y, kCanvasSize);
1177
+ if (alpha <= 0.0) {
1178
+ continue;
1179
+ }
1180
+ output.rgba[output_offset] = rounded_byte(premultiplied_red / alpha);
1181
+ output.rgba[output_offset + 1U] =
1182
+ rounded_byte(premultiplied_green / alpha);
1183
+ output.rgba[output_offset + 2U] =
1184
+ rounded_byte(premultiplied_blue / alpha);
1185
+ output.rgba[output_offset + 3U] = rounded_byte(alpha);
1186
+ }
1187
+ }
1188
+
1189
+ return output;
1190
+ }
1191
+
1192
+ void distance_transform_1d(const std::vector<int> &source,
1193
+ std::vector<int> &destination,
1194
+ std::vector<int> &vertices,
1195
+ std::vector<double> &intersections) {
1196
+ const std::size_t length = source.size();
1197
+ std::size_t parabola = 0U;
1198
+ vertices[0] = 0;
1199
+ intersections[0] = -std::numeric_limits<double>::infinity();
1200
+ intersections[1] = std::numeric_limits<double>::infinity();
1201
+
1202
+ for (std::size_t query = 1U; query < length; ++query) {
1203
+ double intersection = 0.0;
1204
+ while (true) {
1205
+ const int previous = vertices[parabola];
1206
+ const double query_position = static_cast<double>(query);
1207
+ const double previous_position = static_cast<double>(previous);
1208
+ intersection = ((static_cast<double>(source[query]) +
1209
+ query_position * query_position) -
1210
+ (static_cast<double>(source[previous]) +
1211
+ previous_position * previous_position)) /
1212
+ (2.0 * (query_position - previous_position));
1213
+ if (intersection > intersections[parabola] || parabola == 0U) {
1214
+ break;
1215
+ }
1216
+ --parabola;
1217
+ }
1218
+
1219
+ ++parabola;
1220
+ vertices[parabola] = static_cast<int>(query);
1221
+ intersections[parabola] = intersection;
1222
+ intersections[parabola + 1U] = std::numeric_limits<double>::infinity();
1223
+ }
1224
+
1225
+ std::size_t active = 0U;
1226
+ for (std::size_t query = 0U; query < length; ++query) {
1227
+ while (intersections[active + 1U] < static_cast<double>(query)) {
1228
+ ++active;
1229
+ }
1230
+ const int delta = static_cast<int>(query) - vertices[active];
1231
+ destination[query] = delta * delta + source[vertices[active]];
1232
+ }
1233
+ }
1234
+
1235
+ void apply_outline(Image &image, const TranscodeOptions &options) {
1236
+ if (options.outline_width == 0U || options.outline_color.alpha == 0U) {
1237
+ return;
1238
+ }
1239
+
1240
+ const std::size_t pixel_count =
1241
+ static_cast<std::size_t>(image.width) * image.height;
1242
+ std::vector<int> distances(pixel_count, kDistanceInfinity);
1243
+ std::vector<int> source(std::max(image.width, image.height));
1244
+ std::vector<int> transformed(source.size());
1245
+ std::vector<int> vertices(source.size());
1246
+ std::vector<double> intersections(source.size() + 1U);
1247
+
1248
+ for (std::uint32_t y = 0U; y < image.height; ++y) {
1249
+ for (std::uint32_t x = 0U; x < image.width; ++x) {
1250
+ source[x] = image.rgba[pixel_offset(x, y, image.width) + 3U] >
1251
+ options.alpha_threshold
1252
+ ? 0
1253
+ : kDistanceInfinity;
1254
+ }
1255
+ distance_transform_1d(source, transformed, vertices, intersections);
1256
+ for (std::uint32_t x = 0U; x < image.width; ++x) {
1257
+ distances[static_cast<std::size_t>(y) * image.width + x] = transformed[x];
1258
+ }
1259
+ }
1260
+
1261
+ for (std::uint32_t x = 0U; x < image.width; ++x) {
1262
+ for (std::uint32_t y = 0U; y < image.height; ++y) {
1263
+ source[y] = distances[static_cast<std::size_t>(y) * image.width + x];
1264
+ }
1265
+ distance_transform_1d(source, transformed, vertices, intersections);
1266
+ for (std::uint32_t y = 0U; y < image.height; ++y) {
1267
+ distances[static_cast<std::size_t>(y) * image.width + x] = transformed[y];
1268
+ }
1269
+ }
1270
+
1271
+ const int radius_squared = static_cast<int>(options.outline_width) *
1272
+ static_cast<int>(options.outline_width);
1273
+ for (std::uint32_t y = 0U; y < image.height; ++y) {
1274
+ for (std::uint32_t x = 0U; x < image.width; ++x) {
1275
+ const std::size_t image_offset = pixel_offset(x, y, image.width);
1276
+ const std::size_t distance_offset =
1277
+ static_cast<std::size_t>(y) * image.width + x;
1278
+ if (image.rgba[image_offset + 3U] != 0U ||
1279
+ distances[distance_offset] > radius_squared) {
1280
+ continue;
1281
+ }
1282
+ image.rgba[image_offset] = options.outline_color.red;
1283
+ image.rgba[image_offset + 1U] = options.outline_color.green;
1284
+ image.rgba[image_offset + 2U] = options.outline_color.blue;
1285
+ image.rgba[image_offset + 3U] = options.outline_color.alpha;
1286
+ }
1287
+ }
1288
+ }
1289
+
1290
+ class Picture final {
1291
+ public:
1292
+ Picture() {
1293
+ if (WebPPictureInit(&picture_) == 0) {
1294
+ fail(ErrorCode::kInternal, "unable to initialize the WebP picture");
1295
+ }
1296
+ }
1297
+
1298
+ ~Picture() { WebPPictureFree(&picture_); }
1299
+
1300
+ Picture(const Picture &) = delete;
1301
+ Picture &operator=(const Picture &) = delete;
1302
+ Picture(Picture &&) = delete;
1303
+ Picture &operator=(Picture &&) = delete;
1304
+
1305
+ [[nodiscard]] WebPPicture *get() { return &picture_; }
1306
+
1307
+ void import_rgba(const Image &image) {
1308
+ picture_.width = static_cast<int>(image.width);
1309
+ picture_.height = static_cast<int>(image.height);
1310
+ if (WebPPictureImportRGBA(&picture_, image.rgba.data(),
1311
+ static_cast<int>(image.width * 4U)) == 0) {
1312
+ fail(ErrorCode::kEncodingFailed, "unable to import pixels into WebP");
1313
+ }
1314
+ }
1315
+
1316
+ private:
1317
+ WebPPicture picture_{};
1318
+ };
1319
+
1320
+ class MemoryWriter final {
1321
+ public:
1322
+ MemoryWriter() { WebPMemoryWriterInit(&writer_); }
1323
+
1324
+ ~MemoryWriter() { WebPFree(writer_.mem); }
1325
+
1326
+ MemoryWriter(const MemoryWriter &) = delete;
1327
+ MemoryWriter &operator=(const MemoryWriter &) = delete;
1328
+ MemoryWriter(MemoryWriter &&) = delete;
1329
+ MemoryWriter &operator=(MemoryWriter &&) = delete;
1330
+
1331
+ [[nodiscard]] WebPMemoryWriter *get() { return &writer_; }
1332
+
1333
+ [[nodiscard]] std::vector<Byte> copy() const {
1334
+ if (writer_.mem == nullptr || writer_.size == 0U) {
1335
+ return {};
1336
+ }
1337
+ return {writer_.mem, writer_.mem + writer_.size};
1338
+ }
1339
+
1340
+ private:
1341
+ WebPMemoryWriter writer_{};
1342
+ };
1343
+
1344
+ class EncodedData final {
1345
+ public:
1346
+ EncodedData() { WebPDataInit(&data_); }
1347
+
1348
+ ~EncodedData() { WebPDataClear(&data_); }
1349
+
1350
+ EncodedData(const EncodedData &) = delete;
1351
+ EncodedData &operator=(const EncodedData &) = delete;
1352
+ EncodedData(EncodedData &&) = delete;
1353
+ EncodedData &operator=(EncodedData &&) = delete;
1354
+
1355
+ [[nodiscard]] WebPData *get() { return &data_; }
1356
+
1357
+ [[nodiscard]] std::vector<Byte> copy() const {
1358
+ if (data_.bytes == nullptr || data_.size == 0U) {
1359
+ return {};
1360
+ }
1361
+ return {data_.bytes, data_.bytes + data_.size};
1362
+ }
1363
+
1364
+ private:
1365
+ WebPData data_{};
1366
+ };
1367
+
1368
+ [[nodiscard]] WebPConfig make_webp_config(const int quality,
1369
+ const bool lossless) {
1370
+ WebPConfig config{};
1371
+ if (WebPConfigPreset(&config, WEBP_PRESET_DRAWING,
1372
+ static_cast<float>(quality)) == 0) {
1373
+ fail(ErrorCode::kInternal, "unable to initialize the WebP encoder config");
1374
+ }
1375
+ if (lossless) {
1376
+ if (WebPConfigLosslessPreset(&config, 6) == 0) {
1377
+ fail(ErrorCode::kInternal,
1378
+ "unable to initialize the lossless WebP encoder config");
1379
+ }
1380
+ } else {
1381
+ config.lossless = 0;
1382
+ config.method = 6;
1383
+ config.alpha_quality = 100;
1384
+ config.thread_level = 1;
1385
+ config.exact = 0;
1386
+ config.use_sharp_yuv = 1;
1387
+ }
1388
+ if (WebPValidateConfig(&config) == 0) {
1389
+ fail(ErrorCode::kInternal, "invalid WebP encoder configuration");
1390
+ }
1391
+ return config;
1392
+ }
1393
+
1394
+ [[nodiscard]] std::vector<Byte>
1395
+ encode_static(const Image &image, const int quality, const bool lossless) {
1396
+ WebPConfig config = make_webp_config(quality, lossless);
1397
+ Picture picture;
1398
+ picture.import_rgba(image);
1399
+ MemoryWriter writer;
1400
+ picture.get()->writer = WebPMemoryWrite;
1401
+ picture.get()->custom_ptr = writer.get();
1402
+ if (WebPEncode(&config, picture.get()) == 0) {
1403
+ fail(ErrorCode::kEncodingFailed, "unable to encode static WebP output");
1404
+ }
1405
+ return writer.copy();
1406
+ }
1407
+
1408
+ [[nodiscard]] StickerResult validate_output(std::vector<Byte> bytes,
1409
+ const bool expect_animation) {
1410
+ if (bytes.empty()) {
1411
+ fail(ErrorCode::kEncodingFailed, "WebP encoder returned no output");
1412
+ }
1413
+ const WebpInfo info = inspect_webp(bytes);
1414
+ if (info.width != kCanvasSize || info.height != kCanvasSize) {
1415
+ fail(ErrorCode::kValidationFailed,
1416
+ "encoded WebP does not have a 512 by 512 canvas");
1417
+ }
1418
+
1419
+ const bool animated = info.animated();
1420
+ if (animated != expect_animation) {
1421
+ fail(ErrorCode::kValidationFailed,
1422
+ "encoded WebP animation state differs from the requested output");
1423
+ }
1424
+
1425
+ std::uint64_t duration = 0U;
1426
+ for (const std::uint32_t frame_duration : info.frame_durations_ms) {
1427
+ if (animated && frame_duration < 8U) {
1428
+ fail(ErrorCode::kValidationFailed,
1429
+ "encoded WebP contains a frame shorter than 8 milliseconds");
1430
+ }
1431
+ duration += frame_duration;
1432
+ }
1433
+ if (animated && duration > kMaxAnimationDurationMs) {
1434
+ fail(ErrorCode::kValidationFailed,
1435
+ "encoded WebP animation exceeds 10 seconds");
1436
+ }
1437
+
1438
+ const std::size_t limit =
1439
+ animated ? kMaximumAnimatedStickerBytes : kMaximumStaticStickerBytes;
1440
+ if (bytes.size() > limit) {
1441
+ fail(ErrorCode::kValidationFailed,
1442
+ "encoded WebP exceeds the WhatsApp size limit");
1443
+ }
1444
+
1445
+ return StickerResult{
1446
+ .bytes = std::move(bytes),
1447
+ .width = kCanvasSize,
1448
+ .height = kCanvasSize,
1449
+ .frame_count = static_cast<std::uint32_t>(info.frame_durations_ms.size()),
1450
+ .duration_ms = static_cast<std::uint32_t>(duration),
1451
+ .animated = animated,
1452
+ };
1453
+ }
1454
+
1455
+ [[nodiscard]] StickerResult encode_static_limited(const Image &image) {
1456
+ std::vector<Byte> encoded = encode_static(image, 100, true);
1457
+ if (encoded.size() <= kMaximumStaticStickerBytes) {
1458
+ return validate_output(std::move(encoded), false);
1459
+ }
1460
+
1461
+ constexpr std::array<int, 9U> kQualityCandidates = {90, 80, 70, 60, 50,
1462
+ 40, 30, 20, 10};
1463
+ for (const int quality : kQualityCandidates) {
1464
+ encoded = encode_static(image, quality, false);
1465
+ if (encoded.size() <= kMaximumStaticStickerBytes) {
1466
+ return validate_output(std::move(encoded), false);
1467
+ }
1468
+ }
1469
+ fail(ErrorCode::kEncodingFailed,
1470
+ "unable to fit the static sticker inside the WhatsApp size limit");
1471
+ }
1472
+
1473
+ [[nodiscard]] AnimationPlan plan_animation(const std::span<const Byte> source,
1474
+ const WebpInfo &info,
1475
+ const TranscodeOptions &options) {
1476
+ Bounds bounds{};
1477
+ std::vector<FrameSelection> selections;
1478
+ selections.reserve(info.frame_durations_ms.size());
1479
+ std::uint64_t duration_ms = 0U;
1480
+
1481
+ visit_animation_frames(
1482
+ source, info,
1483
+ [&](const std::uint32_t index, const std::span<const Byte> frame) {
1484
+ extend_bounds(bounds, frame,
1485
+ FrameDimensions{
1486
+ .width = info.width,
1487
+ .height = info.height,
1488
+ },
1489
+ options.alpha_threshold);
1490
+ std::uint32_t frame_duration = info.frame_durations_ms[index];
1491
+ if (frame_duration == 0U) {
1492
+ frame_duration = kMinimumOutputFrameDurationMs;
1493
+ }
1494
+ duration_ms += frame_duration;
1495
+ if (duration_ms > kMaxAnimationDurationMs) {
1496
+ fail(ErrorCode::kResourceLimit,
1497
+ "source animation exceeds the WhatsApp duration limit");
1498
+ }
1499
+
1500
+ if (selections.empty() ||
1501
+ selections.back().duration_ms >= kMinimumOutputFrameDurationMs) {
1502
+ selections.push_back(FrameSelection{
1503
+ .source_index = index,
1504
+ .duration_ms = frame_duration,
1505
+ });
1506
+ } else {
1507
+ selections.back().duration_ms += frame_duration;
1508
+ }
1509
+ });
1510
+
1511
+ if (selections.size() > 1U &&
1512
+ selections.back().duration_ms < kMinimumOutputFrameDurationMs) {
1513
+ selections[selections.size() - 2U].duration_ms +=
1514
+ selections.back().duration_ms;
1515
+ selections.pop_back();
1516
+ }
1517
+ if (selections.empty()) {
1518
+ fail(ErrorCode::kInvalidInput, "WebP animation has no frames");
1519
+ }
1520
+
1521
+ return AnimationPlan{
1522
+ .geometry = make_geometry_plan(bounds, info.width, info.height, options),
1523
+ .selections = std::move(selections),
1524
+ .duration_ms = static_cast<std::uint32_t>(duration_ms),
1525
+ };
1526
+ }
1527
+
1528
+ [[nodiscard]] Image render_animation_frame(const std::span<const Byte> source,
1529
+ const WebpInfo &info,
1530
+ const AnimationPlan &plan,
1531
+ const std::uint32_t wanted_index,
1532
+ const TranscodeOptions &options) {
1533
+ Image rendered{};
1534
+ bool found = false;
1535
+ visit_animation_frames(
1536
+ source, info,
1537
+ [&](const std::uint32_t index, const std::span<const Byte> frame) {
1538
+ if (index != wanted_index) {
1539
+ return;
1540
+ }
1541
+ rendered = render_frame(frame, info.width, info.height, plan.geometry);
1542
+ apply_outline(rendered, options);
1543
+ found = true;
1544
+ });
1545
+ if (!found) {
1546
+ fail(ErrorCode::kInvalidInput, "requested animation frame is unavailable");
1547
+ }
1548
+ return rendered;
1549
+ }
1550
+
1551
+ [[nodiscard]] std::vector<Byte>
1552
+ encode_animation(const std::span<const Byte> source, const WebpInfo &info,
1553
+ const AnimationPlan &plan, const TranscodeOptions &options,
1554
+ const int quality) {
1555
+ WebPAnimEncoderOptions encoder_options{};
1556
+ if (WebPAnimEncoderOptionsInit(&encoder_options) == 0) {
1557
+ fail(ErrorCode::kInternal, "unable to initialize the animation encoder");
1558
+ }
1559
+ encoder_options.anim_params.bgcolor = 0U;
1560
+ encoder_options.anim_params.loop_count = 0;
1561
+ encoder_options.minimize_size = 0;
1562
+ encoder_options.allow_mixed = 1;
1563
+
1564
+ using EncoderPtr =
1565
+ std::unique_ptr<WebPAnimEncoder, decltype(&WebPAnimEncoderDelete)>;
1566
+ EncoderPtr encoder(WebPAnimEncoderNew(static_cast<int>(kCanvasSize),
1567
+ static_cast<int>(kCanvasSize),
1568
+ &encoder_options),
1569
+ WebPAnimEncoderDelete);
1570
+ if (!encoder) {
1571
+ fail(ErrorCode::kEncodingFailed, "unable to create the animation encoder");
1572
+ }
1573
+
1574
+ WebPConfig config = make_webp_config(quality, false);
1575
+ std::size_t selection_index = 0U;
1576
+ std::uint32_t timestamp_ms = 0U;
1577
+ visit_animation_frames(
1578
+ source, info,
1579
+ [&](const std::uint32_t index, const std::span<const Byte> frame) {
1580
+ if (selection_index >= plan.selections.size() ||
1581
+ plan.selections[selection_index].source_index != index) {
1582
+ return;
1583
+ }
1584
+
1585
+ Image rendered =
1586
+ render_frame(frame, info.width, info.height, plan.geometry);
1587
+ apply_outline(rendered, options);
1588
+ Picture picture;
1589
+ picture.import_rgba(rendered);
1590
+ if (WebPAnimEncoderAdd(encoder.get(), picture.get(),
1591
+ static_cast<int>(timestamp_ms), &config) == 0) {
1592
+ fail(ErrorCode::kEncodingFailed,
1593
+ "unable to encode an animation frame");
1594
+ }
1595
+ timestamp_ms += plan.selections[selection_index].duration_ms;
1596
+ ++selection_index;
1597
+ });
1598
+
1599
+ if (selection_index != plan.selections.size()) {
1600
+ fail(ErrorCode::kInvalidInput,
1601
+ "animation frame selection could not be reconstructed");
1602
+ }
1603
+ if (WebPAnimEncoderAdd(encoder.get(), nullptr, static_cast<int>(timestamp_ms),
1604
+ nullptr) == 0) {
1605
+ fail(ErrorCode::kEncodingFailed, "unable to finalize the animation timing");
1606
+ }
1607
+
1608
+ EncodedData encoded;
1609
+ if (WebPAnimEncoderAssemble(encoder.get(), encoded.get()) == 0) {
1610
+ fail(ErrorCode::kEncodingFailed, "unable to assemble animated WebP output");
1611
+ }
1612
+ return encoded.copy();
1613
+ }
1614
+
1615
+ [[nodiscard]] StickerResult
1616
+ encode_animation_limited(const std::span<const Byte> source,
1617
+ const WebpInfo &info, const AnimationPlan &plan,
1618
+ const TranscodeOptions &options) {
1619
+ constexpr std::array<int, 9U> kQualityCandidates = {90, 80, 70, 60, 50,
1620
+ 40, 30, 20, 10};
1621
+ for (const int quality : kQualityCandidates) {
1622
+ std::vector<Byte> encoded =
1623
+ encode_animation(source, info, plan, options, quality);
1624
+ if (encoded.size() <= kMaximumAnimatedStickerBytes) {
1625
+ return validate_output(std::move(encoded), true);
1626
+ }
1627
+ }
1628
+ fail(ErrorCode::kEncodingFailed,
1629
+ "unable to fit the animated sticker inside the WhatsApp size limit");
1630
+ }
1631
+
1632
+ [[nodiscard]] AnimationPlan
1633
+ plan_video_animation(const std::span<const Byte> source, const VideoInfo &info,
1634
+ const TranscodeOptions &options) {
1635
+ Bounds bounds{};
1636
+ std::vector<FrameSelection> selections;
1637
+ selections.reserve(info.frame_durations_ms.size());
1638
+ std::uint64_t duration_ms = 0U;
1639
+
1640
+ visit_video_frames(
1641
+ source, info,
1642
+ [&](const std::uint32_t index, const std::span<const Byte> frame) {
1643
+ extend_bounds(bounds, frame,
1644
+ FrameDimensions{
1645
+ .width = info.width,
1646
+ .height = info.height,
1647
+ },
1648
+ options.alpha_threshold);
1649
+ std::uint32_t frame_duration = info.frame_durations_ms[index];
1650
+ if (frame_duration == 0U) {
1651
+ frame_duration = kMinimumOutputFrameDurationMs;
1652
+ }
1653
+ duration_ms += frame_duration;
1654
+ if (duration_ms > kMaxAnimationDurationMs) {
1655
+ fail(ErrorCode::kResourceLimit,
1656
+ "video duration exceeds the WhatsApp duration limit");
1657
+ }
1658
+
1659
+ if (selections.empty() ||
1660
+ selections.back().duration_ms >= kMinimumOutputFrameDurationMs) {
1661
+ selections.push_back(FrameSelection{
1662
+ .source_index = index,
1663
+ .duration_ms = frame_duration,
1664
+ });
1665
+ } else {
1666
+ selections.back().duration_ms += frame_duration;
1667
+ }
1668
+ });
1669
+
1670
+ if (selections.size() > 1U &&
1671
+ selections.back().duration_ms < kMinimumOutputFrameDurationMs) {
1672
+ selections[selections.size() - 2U].duration_ms +=
1673
+ selections.back().duration_ms;
1674
+ selections.pop_back();
1675
+ }
1676
+ if (selections.empty()) {
1677
+ fail(ErrorCode::kInvalidInput, "video animation has no frames");
1678
+ }
1679
+
1680
+ return AnimationPlan{
1681
+ .geometry = make_geometry_plan(bounds, info.width, info.height, options),
1682
+ .selections = std::move(selections),
1683
+ .duration_ms = static_cast<std::uint32_t>(duration_ms),
1684
+ };
1685
+ }
1686
+
1687
+ [[nodiscard]] Image
1688
+ render_video_animation_frame(const std::span<const Byte> source,
1689
+ const VideoInfo &info, const AnimationPlan &plan,
1690
+ const std::uint32_t wanted_index,
1691
+ const TranscodeOptions &options) {
1692
+ Image rendered{};
1693
+ bool found = false;
1694
+ visit_video_frames(
1695
+ source, info,
1696
+ [&](const std::uint32_t index, const std::span<const Byte> frame) {
1697
+ if (index != wanted_index) {
1698
+ return;
1699
+ }
1700
+ rendered = render_frame(frame, info.width, info.height, plan.geometry);
1701
+ apply_outline(rendered, options);
1702
+ found = true;
1703
+ });
1704
+ if (!found) {
1705
+ fail(ErrorCode::kInvalidInput, "requested video frame is unavailable");
1706
+ }
1707
+ return rendered;
1708
+ }
1709
+
1710
+ [[nodiscard]] std::vector<Byte>
1711
+ encode_video_animation(const std::span<const Byte> source,
1712
+ const VideoInfo &info, const AnimationPlan &plan,
1713
+ const TranscodeOptions &options, const int quality) {
1714
+ WebPAnimEncoderOptions encoder_options{};
1715
+ if (WebPAnimEncoderOptionsInit(&encoder_options) == 0) {
1716
+ fail(ErrorCode::kInternal, "unable to initialize the animation encoder");
1717
+ }
1718
+ encoder_options.anim_params.bgcolor = 0U;
1719
+ encoder_options.anim_params.loop_count = 0;
1720
+ encoder_options.minimize_size = 0;
1721
+ encoder_options.allow_mixed = 1;
1722
+
1723
+ using EncoderPtr =
1724
+ std::unique_ptr<WebPAnimEncoder, decltype(&WebPAnimEncoderDelete)>;
1725
+ EncoderPtr encoder(WebPAnimEncoderNew(static_cast<int>(kCanvasSize),
1726
+ static_cast<int>(kCanvasSize),
1727
+ &encoder_options),
1728
+ WebPAnimEncoderDelete);
1729
+ if (!encoder) {
1730
+ fail(ErrorCode::kEncodingFailed, "unable to create the animation encoder");
1731
+ }
1732
+
1733
+ WebPConfig config = make_webp_config(quality, false);
1734
+ std::size_t selection_index = 0U;
1735
+ std::uint32_t timestamp_ms = 0U;
1736
+ visit_video_frames(
1737
+ source, info,
1738
+ [&](const std::uint32_t index, const std::span<const Byte> frame) {
1739
+ if (selection_index >= plan.selections.size() ||
1740
+ plan.selections[selection_index].source_index != index) {
1741
+ return;
1742
+ }
1743
+
1744
+ Image rendered =
1745
+ render_frame(frame, info.width, info.height, plan.geometry);
1746
+ apply_outline(rendered, options);
1747
+ Picture picture;
1748
+ picture.import_rgba(rendered);
1749
+ if (WebPAnimEncoderAdd(encoder.get(), picture.get(),
1750
+ static_cast<int>(timestamp_ms), &config) == 0) {
1751
+ fail(ErrorCode::kEncodingFailed,
1752
+ "unable to encode a video animation frame");
1753
+ }
1754
+ timestamp_ms += plan.selections[selection_index].duration_ms;
1755
+ ++selection_index;
1756
+ });
1757
+
1758
+ if (selection_index != plan.selections.size()) {
1759
+ fail(ErrorCode::kInvalidInput,
1760
+ "video frame selection could not be reconstructed");
1761
+ }
1762
+ if (WebPAnimEncoderAdd(encoder.get(), nullptr, static_cast<int>(timestamp_ms),
1763
+ nullptr) == 0) {
1764
+ fail(ErrorCode::kEncodingFailed,
1765
+ "unable to finalize the video animation timing");
1766
+ }
1767
+
1768
+ EncodedData encoded;
1769
+ if (WebPAnimEncoderAssemble(encoder.get(), encoded.get()) == 0) {
1770
+ fail(ErrorCode::kEncodingFailed,
1771
+ "unable to assemble animated WebP video output");
1772
+ }
1773
+ return encoded.copy();
1774
+ }
1775
+
1776
+ [[nodiscard]] StickerResult
1777
+ encode_video_animation_limited(const std::span<const Byte> source,
1778
+ const VideoInfo &info, const AnimationPlan &plan,
1779
+ const TranscodeOptions &options) {
1780
+ constexpr std::array<int, 9U> kQualityCandidates = {90, 80, 70, 60, 50,
1781
+ 40, 30, 20, 10};
1782
+ for (const int quality : kQualityCandidates) {
1783
+ std::vector<Byte> encoded =
1784
+ encode_video_animation(source, info, plan, options, quality);
1785
+ if (encoded.size() <= kMaximumAnimatedStickerBytes) {
1786
+ return validate_output(std::move(encoded), true);
1787
+ }
1788
+ }
1789
+ fail(ErrorCode::kEncodingFailed,
1790
+ "unable to fit the video sticker inside the WhatsApp size limit");
1791
+ }
1792
+
1793
+ [[nodiscard]] StickerResult transcode_static(Image decoded,
1794
+ const TranscodeOptions &options) {
1795
+ Bounds bounds{};
1796
+ extend_bounds(bounds, decoded.rgba,
1797
+ FrameDimensions{
1798
+ .width = decoded.width,
1799
+ .height = decoded.height,
1800
+ },
1801
+ options.alpha_threshold);
1802
+ const GeometryPlan geometry =
1803
+ make_geometry_plan(bounds, decoded.width, decoded.height, options);
1804
+ Image rendered =
1805
+ render_frame(decoded.rgba, decoded.width, decoded.height, geometry);
1806
+ apply_outline(rendered, options);
1807
+ return encode_static_limited(rendered);
1808
+ }
1809
+
1810
+ [[nodiscard]] StickerResult
1811
+ transcode_animated_webp(const std::span<const Byte> source,
1812
+ const WebpInfo &info, const TranscodeOptions &options) {
1813
+ const AnimationPlan plan = plan_animation(source, info, options);
1814
+ if (plan.selections.size() == 1U) {
1815
+ return encode_static_limited(render_animation_frame(
1816
+ source, info, plan, plan.selections.front().source_index, options));
1817
+ }
1818
+ return encode_animation_limited(source, info, plan, options);
1819
+ }
1820
+
1821
+ [[nodiscard]] StickerResult
1822
+ transcode_animated_video(const std::span<const Byte> source,
1823
+ const VideoInfo &info,
1824
+ const TranscodeOptions &options) {
1825
+ const AnimationPlan plan = plan_video_animation(source, info, options);
1826
+ if (plan.selections.size() == 1U) {
1827
+ return encode_static_limited(render_video_animation_frame(
1828
+ source, info, plan, plan.selections.front().source_index, options));
1829
+ }
1830
+ return encode_video_animation_limited(source, info, plan, options);
1831
+ }
1832
+
1833
+ } // namespace
1834
+
1835
+ StickerResult transcode(const std::span<const std::uint8_t> source,
1836
+ const TranscodeOptions &options) {
1837
+ try {
1838
+ if (source.empty()) {
1839
+ fail(ErrorCode::kInvalidInput, "sticker input is empty");
1840
+ }
1841
+ if (source.size() > kMaxInputBytes) {
1842
+ fail(ErrorCode::kResourceLimit,
1843
+ "sticker input exceeds the native worker byte limit");
1844
+ }
1845
+ if (options.outline_width > kMaximumOutlineWidth) {
1846
+ fail(ErrorCode::kInvalidInput,
1847
+ "outline width is outside the supported range");
1848
+ }
1849
+
1850
+ if (is_webp(source)) {
1851
+ const WebpInfo info = inspect_webp(source);
1852
+ if (info.animated()) {
1853
+ return transcode_animated_webp(source, info, options);
1854
+ }
1855
+ return transcode_static(decode_static_webp(source, info), options);
1856
+ }
1857
+ if (is_png(source)) {
1858
+ if (is_apng(source)) {
1859
+ fail(ErrorCode::kUnsupportedInput,
1860
+ "APNG input is not supported; convert it to WebP first");
1861
+ }
1862
+ return transcode_static(decode_png(source), options);
1863
+ }
1864
+ if (is_jpeg(source)) {
1865
+ return transcode_static(decode_jpeg(source), options);
1866
+ }
1867
+ if (is_gif(source)) {
1868
+ fail(ErrorCode::kUnsupportedInput,
1869
+ "GIF input is not supported; use a video or animated WebP instead");
1870
+ }
1871
+ return transcode_animated_video(source, inspect_video(source), options);
1872
+ } catch (const StickerError &) {
1873
+ throw;
1874
+ } catch (const std::bad_alloc &) {
1875
+ fail(ErrorCode::kResourceLimit, "native worker ran out of memory");
1876
+ } catch (const std::exception &error) {
1877
+ fail(ErrorCode::kInternal,
1878
+ std::string("native sticker processing failed: ") + error.what());
1879
+ }
1880
+ }
1881
+
1882
+ } // namespace sticker