@rin-ts/sticker 0.0.0-stage → 0.2.1
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.
- package/LICENSE +674 -0
- package/README.md +187 -2
- package/package.json +40 -4
- package/src/index.ts +20 -0
- package/src/native/sticker_pipeline.cpp +1893 -0
- package/src/native/sticker_pipeline.hpp +66 -0
- package/src/native/sticker_worker.cpp +305 -0
- package/src/native.ts +570 -0
- package/src/types.ts +12 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <cstdint>
|
|
4
|
+
#include <span>
|
|
5
|
+
#include <stdexcept>
|
|
6
|
+
#include <string>
|
|
7
|
+
#include <vector>
|
|
8
|
+
|
|
9
|
+
namespace sticker {
|
|
10
|
+
|
|
11
|
+
enum class ErrorCode : std::uint8_t {
|
|
12
|
+
kNone = 0,
|
|
13
|
+
kInvalidInput = 1,
|
|
14
|
+
kUnsupportedInput = 2,
|
|
15
|
+
kResourceLimit = 3,
|
|
16
|
+
kEncodingFailed = 4,
|
|
17
|
+
kValidationFailed = 5,
|
|
18
|
+
kInternal = 6,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
class StickerError final : public std::runtime_error {
|
|
22
|
+
public:
|
|
23
|
+
StickerError(ErrorCode code, std::string message);
|
|
24
|
+
|
|
25
|
+
[[nodiscard]] ErrorCode code() const noexcept;
|
|
26
|
+
|
|
27
|
+
private:
|
|
28
|
+
ErrorCode code_;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
struct RgbaColor {
|
|
32
|
+
std::uint8_t red = 255U;
|
|
33
|
+
std::uint8_t green = 255U;
|
|
34
|
+
std::uint8_t blue = 255U;
|
|
35
|
+
std::uint8_t alpha = 255U;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
struct TranscodeOptions {
|
|
39
|
+
bool trim = true;
|
|
40
|
+
std::uint8_t alpha_threshold = 0U;
|
|
41
|
+
std::uint8_t outline_width = 0U;
|
|
42
|
+
RgbaColor outline_color{};
|
|
43
|
+
bool prefer_lossless = false;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
struct StickerResult {
|
|
47
|
+
std::vector<std::uint8_t> bytes;
|
|
48
|
+
std::uint32_t width = 0U;
|
|
49
|
+
std::uint32_t height = 0U;
|
|
50
|
+
std::uint32_t frame_count = 0U;
|
|
51
|
+
std::uint32_t duration_ms = 0U;
|
|
52
|
+
bool animated = false;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Converts a supported source asset into a WhatsApp-compatible WebP sticker.
|
|
57
|
+
*
|
|
58
|
+
* Supported sources are PNG, JPEG, static WebP, animated WebP, and video
|
|
59
|
+
* formats with an FFmpeg decoder. The caller owns the source bytes; the
|
|
60
|
+
* returned value owns the encoded output bytes.
|
|
61
|
+
*/
|
|
62
|
+
[[nodiscard]] StickerResult transcode(
|
|
63
|
+
std::span<const std::uint8_t> source,
|
|
64
|
+
const TranscodeOptions& options);
|
|
65
|
+
|
|
66
|
+
} // namespace sticker
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
#include "sticker_pipeline.hpp"
|
|
2
|
+
|
|
3
|
+
#include <array>
|
|
4
|
+
#include <cstddef>
|
|
5
|
+
#include <cstdint>
|
|
6
|
+
#include <cstring>
|
|
7
|
+
#include <exception>
|
|
8
|
+
#include <iostream>
|
|
9
|
+
#include <limits>
|
|
10
|
+
#include <span>
|
|
11
|
+
#include <stdexcept>
|
|
12
|
+
#include <string>
|
|
13
|
+
#include <utility>
|
|
14
|
+
#include <vector>
|
|
15
|
+
|
|
16
|
+
namespace {
|
|
17
|
+
|
|
18
|
+
using Byte = std::uint8_t;
|
|
19
|
+
|
|
20
|
+
constexpr std::array<Byte, 4U> kRequestMagic = {'S', 'T', 'K', '1'};
|
|
21
|
+
constexpr std::array<Byte, 4U> kResponseMagic = {'S', 'T', 'R', '1'};
|
|
22
|
+
constexpr Byte kProtocolVersion = 1U;
|
|
23
|
+
constexpr Byte kCommandTranscode = 1U;
|
|
24
|
+
constexpr Byte kTrimFlag = 1U << 0U;
|
|
25
|
+
constexpr Byte kPreferLosslessFlag = 1U << 1U;
|
|
26
|
+
constexpr Byte kSupportedRequestFlags = kTrimFlag | kPreferLosslessFlag;
|
|
27
|
+
constexpr Byte kUnsupportedRequestFlags =
|
|
28
|
+
static_cast<Byte>(~kSupportedRequestFlags);
|
|
29
|
+
constexpr std::size_t kRequestHeaderSize = 18U;
|
|
30
|
+
constexpr std::size_t kResponseHeaderSize = 26U;
|
|
31
|
+
constexpr std::size_t kMaximumFrameBytes =
|
|
32
|
+
static_cast<std::size_t>(17U) * 1024U * 1024U;
|
|
33
|
+
|
|
34
|
+
enum class ResponseStatus : Byte {
|
|
35
|
+
kOk = 0U,
|
|
36
|
+
kInvalidRequest = 1U,
|
|
37
|
+
kProcessingError = 2U,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
struct WorkerRequest {
|
|
41
|
+
sticker::TranscodeOptions options;
|
|
42
|
+
std::vector<Byte> input;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
struct WorkerResponse {
|
|
46
|
+
ResponseStatus status = ResponseStatus::kProcessingError;
|
|
47
|
+
sticker::ErrorCode error_code = sticker::ErrorCode::kInternal;
|
|
48
|
+
sticker::StickerResult result;
|
|
49
|
+
std::string message;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
[[nodiscard]] std::uint32_t read_u32(
|
|
53
|
+
const std::span<const Byte> bytes,
|
|
54
|
+
const std::size_t offset) {
|
|
55
|
+
return static_cast<std::uint32_t>(bytes[offset]) |
|
|
56
|
+
(static_cast<std::uint32_t>(bytes[offset + 1U]) << 8U) |
|
|
57
|
+
(static_cast<std::uint32_t>(bytes[offset + 2U]) << 16U) |
|
|
58
|
+
(static_cast<std::uint32_t>(bytes[offset + 3U]) << 24U);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
void write_u16(
|
|
62
|
+
std::vector<Byte>& bytes,
|
|
63
|
+
const std::size_t offset,
|
|
64
|
+
const std::uint16_t value) {
|
|
65
|
+
bytes[offset] = static_cast<Byte>(value & 0xffU);
|
|
66
|
+
bytes[offset + 1U] = static_cast<Byte>((value >> 8U) & 0xffU);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
void write_u32(
|
|
70
|
+
std::vector<Byte>& bytes,
|
|
71
|
+
const std::size_t offset,
|
|
72
|
+
const std::uint32_t value) {
|
|
73
|
+
bytes[offset] = static_cast<Byte>(value & 0xffU);
|
|
74
|
+
bytes[offset + 1U] = static_cast<Byte>((value >> 8U) & 0xffU);
|
|
75
|
+
bytes[offset + 2U] = static_cast<Byte>((value >> 16U) & 0xffU);
|
|
76
|
+
bytes[offset + 3U] = static_cast<Byte>((value >> 24U) & 0xffU);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
[[nodiscard]] bool read_exact(
|
|
80
|
+
std::istream& input,
|
|
81
|
+
Byte* const destination,
|
|
82
|
+
const std::size_t byte_count) {
|
|
83
|
+
input.read(
|
|
84
|
+
reinterpret_cast<char*>(destination),
|
|
85
|
+
static_cast<std::streamsize>(byte_count));
|
|
86
|
+
return input.good() ||
|
|
87
|
+
static_cast<std::size_t>(input.gcount()) == byte_count;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
[[nodiscard]] bool read_frame(
|
|
91
|
+
std::istream& input,
|
|
92
|
+
std::vector<Byte>& frame,
|
|
93
|
+
bool& clean_end_of_file) {
|
|
94
|
+
std::array<Byte, 4U> length_bytes{};
|
|
95
|
+
input.read(
|
|
96
|
+
reinterpret_cast<char*>(length_bytes.data()),
|
|
97
|
+
static_cast<std::streamsize>(length_bytes.size()));
|
|
98
|
+
const std::streamsize read_count = input.gcount();
|
|
99
|
+
if (read_count == 0 && input.eof()) {
|
|
100
|
+
clean_end_of_file = true;
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
if (read_count != static_cast<std::streamsize>(length_bytes.size())) {
|
|
104
|
+
clean_end_of_file = false;
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const std::uint32_t length = read_u32(length_bytes, 0U);
|
|
109
|
+
if (length == 0U || length > kMaximumFrameBytes) {
|
|
110
|
+
clean_end_of_file = false;
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
frame.resize(length);
|
|
114
|
+
if (!read_exact(input, frame.data(), frame.size())) {
|
|
115
|
+
clean_end_of_file = false;
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
clean_end_of_file = false;
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
[[nodiscard]] WorkerRequest parse_request(const std::span<const Byte> frame) {
|
|
123
|
+
if (frame.size() < kRequestHeaderSize ||
|
|
124
|
+
std::memcmp(frame.data(), kRequestMagic.data(), kRequestMagic.size()) !=
|
|
125
|
+
0) {
|
|
126
|
+
throw sticker::StickerError(
|
|
127
|
+
sticker::ErrorCode::kInvalidInput, "invalid sticker worker request");
|
|
128
|
+
}
|
|
129
|
+
if (frame[4U] != kProtocolVersion || frame[5U] != kCommandTranscode ||
|
|
130
|
+
frame[7U] != 0U) {
|
|
131
|
+
throw sticker::StickerError(
|
|
132
|
+
sticker::ErrorCode::kInvalidInput,
|
|
133
|
+
"unsupported sticker worker request version or command");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const Byte flags = frame[6U];
|
|
137
|
+
if ((flags & kUnsupportedRequestFlags) != 0) {
|
|
138
|
+
throw sticker::StickerError(
|
|
139
|
+
sticker::ErrorCode::kInvalidInput,
|
|
140
|
+
"sticker worker request contains unsupported flags");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const std::uint32_t input_length = read_u32(frame, 14U);
|
|
144
|
+
const std::size_t actual_input_length = frame.size() - kRequestHeaderSize;
|
|
145
|
+
if (actual_input_length > std::numeric_limits<std::uint32_t>::max() ||
|
|
146
|
+
input_length != static_cast<std::uint32_t>(actual_input_length)) {
|
|
147
|
+
throw sticker::StickerError(
|
|
148
|
+
sticker::ErrorCode::kInvalidInput,
|
|
149
|
+
"sticker worker request length does not match its payload");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const std::uint8_t outline_width = frame[9U];
|
|
153
|
+
if (outline_width > 64U) {
|
|
154
|
+
throw sticker::StickerError(
|
|
155
|
+
sticker::ErrorCode::kInvalidInput,
|
|
156
|
+
"sticker worker outline width is outside the supported range");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
WorkerRequest request{
|
|
160
|
+
.options =
|
|
161
|
+
sticker::TranscodeOptions{
|
|
162
|
+
.trim = (flags & kTrimFlag) != 0,
|
|
163
|
+
.alpha_threshold = frame[8U],
|
|
164
|
+
.outline_width = outline_width,
|
|
165
|
+
.outline_color =
|
|
166
|
+
sticker::RgbaColor{
|
|
167
|
+
.red = frame[10U],
|
|
168
|
+
.green = frame[11U],
|
|
169
|
+
.blue = frame[12U],
|
|
170
|
+
.alpha = frame[13U],
|
|
171
|
+
},
|
|
172
|
+
.prefer_lossless = (flags & kPreferLosslessFlag) != 0,
|
|
173
|
+
},
|
|
174
|
+
.input = std::vector<Byte>(
|
|
175
|
+
frame.begin() + static_cast<std::ptrdiff_t>(kRequestHeaderSize),
|
|
176
|
+
frame.end()),
|
|
177
|
+
};
|
|
178
|
+
return request;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
[[nodiscard]] WorkerResponse process_request(const WorkerRequest& request) {
|
|
182
|
+
try {
|
|
183
|
+
return WorkerResponse{
|
|
184
|
+
.status = ResponseStatus::kOk,
|
|
185
|
+
.error_code = sticker::ErrorCode::kNone,
|
|
186
|
+
.result = sticker::transcode(request.input, request.options),
|
|
187
|
+
.message = {},
|
|
188
|
+
};
|
|
189
|
+
} catch (const sticker::StickerError& error) {
|
|
190
|
+
return WorkerResponse{
|
|
191
|
+
.status = ResponseStatus::kProcessingError,
|
|
192
|
+
.error_code = error.code(),
|
|
193
|
+
.result = {},
|
|
194
|
+
.message = error.what(),
|
|
195
|
+
};
|
|
196
|
+
} catch (const std::exception& error) {
|
|
197
|
+
return WorkerResponse{
|
|
198
|
+
.status = ResponseStatus::kProcessingError,
|
|
199
|
+
.error_code = sticker::ErrorCode::kInternal,
|
|
200
|
+
.result = {},
|
|
201
|
+
.message = error.what(),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
[[nodiscard]] std::vector<Byte> encode_response(const WorkerResponse& response) {
|
|
207
|
+
const std::size_t output_size = response.result.bytes.size();
|
|
208
|
+
const std::size_t message_size = response.message.size();
|
|
209
|
+
if (output_size > std::numeric_limits<std::uint32_t>::max() ||
|
|
210
|
+
message_size > std::numeric_limits<std::uint16_t>::max()) {
|
|
211
|
+
throw std::runtime_error("sticker worker response exceeds protocol limits");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
std::vector<Byte> frame(kResponseHeaderSize + output_size + message_size);
|
|
215
|
+
std::memcpy(frame.data(), kResponseMagic.data(), kResponseMagic.size());
|
|
216
|
+
frame[4U] = kProtocolVersion;
|
|
217
|
+
frame[5U] = static_cast<Byte>(response.status);
|
|
218
|
+
frame[6U] = static_cast<Byte>(response.error_code);
|
|
219
|
+
frame[7U] = response.result.animated ? 1U : 0U;
|
|
220
|
+
write_u16(
|
|
221
|
+
frame, 8U, static_cast<std::uint16_t>(response.result.width));
|
|
222
|
+
write_u16(
|
|
223
|
+
frame, 10U, static_cast<std::uint16_t>(response.result.height));
|
|
224
|
+
write_u32(frame, 12U, response.result.frame_count);
|
|
225
|
+
write_u32(frame, 16U, response.result.duration_ms);
|
|
226
|
+
write_u32(frame, 20U, static_cast<std::uint32_t>(output_size));
|
|
227
|
+
write_u16(frame, 24U, static_cast<std::uint16_t>(message_size));
|
|
228
|
+
|
|
229
|
+
if (output_size != 0U) {
|
|
230
|
+
std::memcpy(
|
|
231
|
+
frame.data() + kResponseHeaderSize,
|
|
232
|
+
response.result.bytes.data(),
|
|
233
|
+
output_size);
|
|
234
|
+
}
|
|
235
|
+
if (message_size != 0U) {
|
|
236
|
+
std::memcpy(
|
|
237
|
+
frame.data() + kResponseHeaderSize + output_size,
|
|
238
|
+
response.message.data(),
|
|
239
|
+
message_size);
|
|
240
|
+
}
|
|
241
|
+
return frame;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
void write_frame(std::ostream& output, const std::span<const Byte> frame) {
|
|
245
|
+
if (frame.size() > std::numeric_limits<std::uint32_t>::max()) {
|
|
246
|
+
throw std::runtime_error("sticker worker response frame is too large");
|
|
247
|
+
}
|
|
248
|
+
const std::uint32_t length = static_cast<std::uint32_t>(frame.size());
|
|
249
|
+
const std::array<Byte, 4U> length_bytes = {
|
|
250
|
+
static_cast<Byte>(length & 0xffU),
|
|
251
|
+
static_cast<Byte>((length >> 8U) & 0xffU),
|
|
252
|
+
static_cast<Byte>((length >> 16U) & 0xffU),
|
|
253
|
+
static_cast<Byte>((length >> 24U) & 0xffU),
|
|
254
|
+
};
|
|
255
|
+
output.write(
|
|
256
|
+
reinterpret_cast<const char*>(length_bytes.data()),
|
|
257
|
+
static_cast<std::streamsize>(length_bytes.size()));
|
|
258
|
+
output.write(
|
|
259
|
+
reinterpret_cast<const char*>(frame.data()),
|
|
260
|
+
static_cast<std::streamsize>(frame.size()));
|
|
261
|
+
output.flush();
|
|
262
|
+
if (!output) {
|
|
263
|
+
throw std::runtime_error("unable to write sticker worker response");
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
[[nodiscard]] WorkerResponse invalid_request_response(const std::string& message) {
|
|
268
|
+
return WorkerResponse{
|
|
269
|
+
.status = ResponseStatus::kInvalidRequest,
|
|
270
|
+
.error_code = sticker::ErrorCode::kInvalidInput,
|
|
271
|
+
.result = {},
|
|
272
|
+
.message = message,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
} // namespace
|
|
277
|
+
|
|
278
|
+
int main() {
|
|
279
|
+
std::ios::sync_with_stdio(false);
|
|
280
|
+
std::cin.tie(nullptr);
|
|
281
|
+
|
|
282
|
+
while (true) {
|
|
283
|
+
std::vector<Byte> frame;
|
|
284
|
+
bool clean_end_of_file = false;
|
|
285
|
+
if (!read_frame(std::cin, frame, clean_end_of_file)) {
|
|
286
|
+
return clean_end_of_file ? 0 : 1;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
WorkerResponse response{};
|
|
290
|
+
try {
|
|
291
|
+
response = process_request(parse_request(frame));
|
|
292
|
+
} catch (const sticker::StickerError& error) {
|
|
293
|
+
response = invalid_request_response(error.what());
|
|
294
|
+
} catch (const std::exception& error) {
|
|
295
|
+
response = invalid_request_response(error.what());
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
const std::vector<Byte> encoded = encode_response(response);
|
|
300
|
+
write_frame(std::cout, encoded);
|
|
301
|
+
} catch (const std::exception&) {
|
|
302
|
+
return 1;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|