safe_image 0.3.0 → 0.5.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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +61 -12
  3. data/README.md +140 -287
  4. data/SECURITY.md +22 -11
  5. data/docs/architecture.md +63 -0
  6. data/ext/safe_image_vips_helper/extconf.rb +43 -0
  7. data/ext/safe_image_vips_helper/safe_image_vips_helper.c +1007 -0
  8. data/lib/safe_image/api/metadata.rb +85 -0
  9. data/lib/safe_image/api/transform.rb +152 -0
  10. data/lib/safe_image/backend_label.rb +24 -0
  11. data/lib/safe_image/formats.rb +96 -0
  12. data/lib/safe_image/ico.rb +42 -40
  13. data/lib/safe_image/image_magick_backend.rb +219 -162
  14. data/lib/safe_image/jpegli_backend.rb +64 -44
  15. data/lib/safe_image/metadata_operations.rb +155 -0
  16. data/lib/safe_image/native.rb +96 -290
  17. data/lib/safe_image/native_helper.rb +281 -0
  18. data/lib/safe_image/operation_backends/base.rb +83 -0
  19. data/lib/safe_image/operation_backends/image_magick.rb +123 -0
  20. data/lib/safe_image/operation_backends/vips.rb +251 -0
  21. data/lib/safe_image/operation_backends.rb +27 -0
  22. data/lib/safe_image/operation_set.rb +22 -0
  23. data/lib/safe_image/optimizer.rb +225 -98
  24. data/lib/safe_image/path_safety.rb +11 -0
  25. data/lib/safe_image/processor.rb +122 -85
  26. data/lib/safe_image/quality_defaults.rb +23 -0
  27. data/lib/safe_image/remote.rb +248 -144
  28. data/lib/safe_image/result.rb +71 -23
  29. data/lib/safe_image/runner.rb +60 -23
  30. data/lib/safe_image/sandbox.rb +44 -218
  31. data/lib/safe_image/staged_output.rb +44 -0
  32. data/lib/safe_image/svg_metadata.rb +74 -69
  33. data/lib/safe_image/transform_operations.rb +139 -0
  34. data/lib/safe_image/version.rb +1 -1
  35. data/lib/safe_image/vips_backend.rb +13 -12
  36. data/lib/safe_image.rb +62 -306
  37. metadata +43 -37
  38. data/lib/safe_image/discourse_compat.rb +0 -441
  39. data/lib/safe_image/svg_css.rb +0 -314
  40. data/lib/safe_image/svg_sanitizer.rb +0 -583
  41. data/lib/safe_image/vips_glue.rb +0 -361
  42. data/lib/safe_image/zygote.rb +0 -619
@@ -0,0 +1,1007 @@
1
+ #include <errno.h>
2
+ #include <fcntl.h>
3
+ #include <glib.h>
4
+ #include <math.h>
5
+ #include <stdarg.h>
6
+ #include <stdbool.h>
7
+ #include <stdio.h>
8
+ #include <stdlib.h>
9
+ #include <string.h>
10
+ #include <sys/time.h>
11
+ #include <unistd.h>
12
+ #include <vips/vips.h>
13
+
14
+ #define DEFAULT_MAX_PIXELS (128LL * 1024LL * 1024LL)
15
+ #define DEFAULT_JPEG_QUALITY 85
16
+ #define DEFAULT_NATIVE_CONVERT_JPEG_QUALITY 92
17
+
18
+ typedef struct {
19
+ const char *key;
20
+ const char *value;
21
+ } option_t;
22
+
23
+ typedef struct {
24
+ option_t items[64];
25
+ int count;
26
+ } options_t;
27
+
28
+ static double now_ms(void) {
29
+ struct timeval tv;
30
+ gettimeofday(&tv, NULL);
31
+ return (double)tv.tv_sec * 1000.0 + (double)tv.tv_usec / 1000.0;
32
+ }
33
+
34
+ static const char *opt(options_t *opts, const char *key) {
35
+ for (int i = 0; i < opts->count; i++) {
36
+ if (strcmp(opts->items[i].key, key) == 0) {
37
+ return opts->items[i].value;
38
+ }
39
+ }
40
+ return NULL;
41
+ }
42
+
43
+ static const char *opt_required(options_t *opts, const char *key) {
44
+ const char *value = opt(opts, key);
45
+ if (!value || value[0] == '\0') {
46
+ fprintf(stderr, "missing required option --%s\n", key);
47
+ exit(2);
48
+ }
49
+ return value;
50
+ }
51
+
52
+ static int opt_int(options_t *opts, const char *key, int fallback) {
53
+ const char *value = opt(opts, key);
54
+ if (!value) {
55
+ return fallback;
56
+ }
57
+ char *end = NULL;
58
+ long parsed = strtol(value, &end, 10);
59
+ if (!end || *end != '\0') {
60
+ fprintf(stderr, "invalid integer for --%s\n", key);
61
+ exit(2);
62
+ }
63
+ return (int)parsed;
64
+ }
65
+
66
+ static long long opt_ll(options_t *opts, const char *key, long long fallback) {
67
+ const char *value = opt(opts, key);
68
+ if (!value) {
69
+ return fallback;
70
+ }
71
+ char *end = NULL;
72
+ long long parsed = strtoll(value, &end, 10);
73
+ if (!end || *end != '\0' || parsed <= 0) {
74
+ fprintf(stderr, "invalid positive integer for --%s\n", key);
75
+ exit(2);
76
+ }
77
+ return parsed;
78
+ }
79
+
80
+ static double opt_double(options_t *opts, const char *key, double fallback) {
81
+ const char *value = opt(opts, key);
82
+ if (!value) {
83
+ return fallback;
84
+ }
85
+ char *end = NULL;
86
+ double parsed = strtod(value, &end);
87
+ if (!end || *end != '\0' || !isfinite(parsed)) {
88
+ fprintf(stderr, "invalid number for --%s\n", key);
89
+ exit(2);
90
+ }
91
+ return parsed;
92
+ }
93
+
94
+ static void parse_options(int argc, char **argv, options_t *opts) {
95
+ opts->count = 0;
96
+ for (int i = 2; i < argc; i++) {
97
+ if (strncmp(argv[i], "--", 2) != 0) {
98
+ fprintf(stderr, "unexpected argument: %s\n", argv[i]);
99
+ exit(2);
100
+ }
101
+ if (i + 1 >= argc) {
102
+ fprintf(stderr, "missing value for %s\n", argv[i]);
103
+ exit(2);
104
+ }
105
+ if (opts->count >= 64) {
106
+ fprintf(stderr, "too many options\n");
107
+ exit(2);
108
+ }
109
+ opts->items[opts->count].key = argv[i] + 2;
110
+ opts->items[opts->count].value = argv[++i];
111
+ opts->count++;
112
+ }
113
+ }
114
+
115
+ static void json_string(FILE *f, const char *s) {
116
+ fputc('"', f);
117
+ for (const unsigned char *p = (const unsigned char *)s; p && *p; p++) {
118
+ switch (*p) {
119
+ case '"':
120
+ fputs("\\\"", f);
121
+ break;
122
+ case '\\':
123
+ fputs("\\\\", f);
124
+ break;
125
+ case '\b':
126
+ fputs("\\b", f);
127
+ break;
128
+ case '\f':
129
+ fputs("\\f", f);
130
+ break;
131
+ case '\n':
132
+ fputs("\\n", f);
133
+ break;
134
+ case '\r':
135
+ fputs("\\r", f);
136
+ break;
137
+ case '\t':
138
+ fputs("\\t", f);
139
+ break;
140
+ default:
141
+ if (*p < 0x20) {
142
+ fprintf(f, "\\u%04x", *p);
143
+ } else {
144
+ fputc(*p, f);
145
+ }
146
+ }
147
+ }
148
+ fputc('"', f);
149
+ }
150
+
151
+ static void write_error(const char *response, const char *klass, const char *message) {
152
+ FILE *f = fopen(response, "w");
153
+ if (!f) {
154
+ return;
155
+ }
156
+ fputs("{\"ok\":false,\"error\":", f);
157
+ json_string(f, klass);
158
+ fputs(",\"message\":", f);
159
+ json_string(f, message ? message : "error");
160
+ fputs("}\n", f);
161
+ fclose(f);
162
+ }
163
+
164
+ static void write_info(const char *response, const char *input_format, const char *output_format,
165
+ int width, int height, double duration_ms) {
166
+ FILE *f = fopen(response, "w");
167
+ if (!f) {
168
+ fprintf(stderr, "could not write response: %s\n", strerror(errno));
169
+ exit(1);
170
+ }
171
+ fputs("{\"ok\":true", f);
172
+ if (input_format) {
173
+ fputs(",\"input_format\":", f);
174
+ json_string(f, input_format);
175
+ }
176
+ if (output_format) {
177
+ fputs(",\"output_format\":", f);
178
+ json_string(f, output_format);
179
+ }
180
+ fprintf(f, ",\"width\":%d,\"height\":%d,\"duration_ms\":%.3f}\n", width, height, duration_ms);
181
+ fclose(f);
182
+ }
183
+
184
+ static void write_value_int(const char *response, int value) {
185
+ FILE *f = fopen(response, "w");
186
+ if (!f) {
187
+ exit(1);
188
+ }
189
+ fprintf(f, "{\"ok\":true,\"value\":%d}\n", value);
190
+ fclose(f);
191
+ }
192
+
193
+ static void write_value_string(const char *response, const char *value) {
194
+ FILE *f = fopen(response, "w");
195
+ if (!f) {
196
+ exit(1);
197
+ }
198
+ fputs("{\"ok\":true,\"value\":", f);
199
+ json_string(f, value);
200
+ fputs("}\n", f);
201
+ fclose(f);
202
+ }
203
+
204
+ static const char *extname(const char *path) {
205
+ const char *dot = strrchr(path, '.');
206
+ return dot ? dot + 1 : "";
207
+ }
208
+
209
+ static const char *normalized_format(const char *format) {
210
+ if (!format) {
211
+ return NULL;
212
+ }
213
+ if (g_ascii_strcasecmp(format, "jpg") == 0 || g_ascii_strcasecmp(format, "jpeg") == 0) {
214
+ return "jpg";
215
+ }
216
+ if (g_ascii_strcasecmp(format, "png") == 0) {
217
+ return "png";
218
+ }
219
+ if (g_ascii_strcasecmp(format, "webp") == 0) {
220
+ return "webp";
221
+ }
222
+ if (g_ascii_strcasecmp(format, "gif") == 0) {
223
+ return "gif";
224
+ }
225
+ if (g_ascii_strcasecmp(format, "heic") == 0 || g_ascii_strcasecmp(format, "heif") == 0) {
226
+ return "heic";
227
+ }
228
+ if (g_ascii_strcasecmp(format, "avif") == 0) {
229
+ return "avif";
230
+ }
231
+ if (g_ascii_strcasecmp(format, "jxl") == 0) {
232
+ return "jxl";
233
+ }
234
+ return NULL;
235
+ }
236
+
237
+ static int load_image_from_source(VipsSource *source, const char *format, VipsImage **out) {
238
+ if (strcmp(format, "jpg") == 0) {
239
+ return vips_jpegload_source(source, out, "access", VIPS_ACCESS_SEQUENTIAL, "fail-on",
240
+ VIPS_FAIL_ON_ERROR, NULL);
241
+ }
242
+ if (strcmp(format, "png") == 0) {
243
+ return vips_pngload_source(source, out, "access", VIPS_ACCESS_SEQUENTIAL, "fail-on",
244
+ VIPS_FAIL_ON_ERROR, NULL);
245
+ }
246
+ if (strcmp(format, "webp") == 0) {
247
+ return vips_webpload_source(source, out, "access", VIPS_ACCESS_SEQUENTIAL, "fail-on",
248
+ VIPS_FAIL_ON_ERROR, NULL);
249
+ }
250
+ if (strcmp(format, "gif") == 0) {
251
+ return vips_gifload_source(source, out, "access", VIPS_ACCESS_SEQUENTIAL, "fail-on",
252
+ VIPS_FAIL_ON_ERROR, NULL);
253
+ }
254
+ if (strcmp(format, "heic") == 0 || strcmp(format, "avif") == 0) {
255
+ return vips_heifload_source(source, out, "access", VIPS_ACCESS_SEQUENTIAL, "fail-on",
256
+ VIPS_FAIL_ON_ERROR, NULL);
257
+ }
258
+ if (strcmp(format, "jxl") == 0) {
259
+ return vips_jxlload_source(source, out, "access", VIPS_ACCESS_SEQUENTIAL, "fail-on",
260
+ VIPS_FAIL_ON_ERROR, NULL);
261
+ }
262
+ vips_error("safe_image", "%s", "unsupported input format");
263
+ return -1;
264
+ }
265
+
266
+ static int load_image(const char *path, gboolean autorotate, VipsImage **out,
267
+ const char **format_out) {
268
+ const char *format = normalized_format(extname(path));
269
+ if (!format) {
270
+ vips_error("safe_image", "%s", "unsupported input format");
271
+ return -1;
272
+ }
273
+ int rc = -1;
274
+ if (strcmp(format, "jpg") == 0) {
275
+ rc = vips_jpegload(path, out, "access", VIPS_ACCESS_SEQUENTIAL, "fail-on", VIPS_FAIL_ON_ERROR,
276
+ NULL);
277
+ } else if (strcmp(format, "png") == 0) {
278
+ rc = vips_pngload(path, out, "access", VIPS_ACCESS_SEQUENTIAL, "fail-on", VIPS_FAIL_ON_ERROR,
279
+ NULL);
280
+ } else if (strcmp(format, "webp") == 0) {
281
+ rc = vips_webpload(path, out, "access", VIPS_ACCESS_SEQUENTIAL, "fail-on", VIPS_FAIL_ON_ERROR,
282
+ NULL);
283
+ } else if (strcmp(format, "gif") == 0) {
284
+ rc = vips_gifload(path, out, "access", VIPS_ACCESS_SEQUENTIAL, "fail-on", VIPS_FAIL_ON_ERROR,
285
+ NULL);
286
+ } else if (strcmp(format, "heic") == 0 || strcmp(format, "avif") == 0) {
287
+ rc = vips_heifload(path, out, "access", VIPS_ACCESS_SEQUENTIAL, "fail-on", VIPS_FAIL_ON_ERROR,
288
+ NULL);
289
+ } else if (strcmp(format, "jxl") == 0) {
290
+ rc = vips_jxlload(path, out, "access", VIPS_ACCESS_SEQUENTIAL, "fail-on", VIPS_FAIL_ON_ERROR,
291
+ NULL);
292
+ }
293
+ if (rc != 0) {
294
+ return rc;
295
+ }
296
+ if (autorotate && vips_image_get_orientation(*out) > 1) {
297
+ VIPS_UNREF(*out);
298
+ if (strcmp(format, "jpg") == 0) {
299
+ rc = vips_jpegload(path, out, "access", VIPS_ACCESS_RANDOM, "fail-on", VIPS_FAIL_ON_ERROR,
300
+ NULL);
301
+ } else if (strcmp(format, "png") == 0) {
302
+ rc = vips_pngload(path, out, "access", VIPS_ACCESS_RANDOM, "fail-on", VIPS_FAIL_ON_ERROR,
303
+ NULL);
304
+ } else if (strcmp(format, "webp") == 0) {
305
+ rc = vips_webpload(path, out, "access", VIPS_ACCESS_RANDOM, "fail-on", VIPS_FAIL_ON_ERROR,
306
+ NULL);
307
+ } else if (strcmp(format, "gif") == 0) {
308
+ rc = vips_gifload(path, out, "access", VIPS_ACCESS_RANDOM, "fail-on", VIPS_FAIL_ON_ERROR,
309
+ NULL);
310
+ } else if (strcmp(format, "heic") == 0 || strcmp(format, "avif") == 0) {
311
+ rc = vips_heifload(path, out, "access", VIPS_ACCESS_RANDOM, "fail-on", VIPS_FAIL_ON_ERROR,
312
+ NULL);
313
+ } else if (strcmp(format, "jxl") == 0) {
314
+ rc = vips_jxlload(path, out, "access", VIPS_ACCESS_RANDOM, "fail-on", VIPS_FAIL_ON_ERROR,
315
+ NULL);
316
+ }
317
+ if (rc != 0) {
318
+ return rc;
319
+ }
320
+ }
321
+ *format_out = format;
322
+ return 0;
323
+ }
324
+
325
+ static int check_pixels(VipsImage *image, long long max_pixels) {
326
+ int width = vips_image_get_width(image);
327
+ int height = vips_image_get_height(image);
328
+ if (width <= 0 || height <= 0) {
329
+ vips_error("safe_image", "%s", "image dimensions are invalid");
330
+ return -2;
331
+ }
332
+ long long pixels = (long long)width * (long long)height;
333
+ if (pixels > max_pixels) {
334
+ vips_error("safe_image", "image has %lld pixels, exceeds %lld", pixels, max_pixels);
335
+ return -3;
336
+ }
337
+ return 0;
338
+ }
339
+
340
+ static int save_image(VipsImage *image, const char *path, const char *format, int quality) {
341
+ if (strcmp(format, "jpg") == 0) {
342
+ return vips_jpegsave(image, path, "Q", quality, "interlace", FALSE, "strip", TRUE, NULL);
343
+ }
344
+ if (strcmp(format, "png") == 0) {
345
+ return vips_pngsave(image, path, "compression", 6, "strip", TRUE, NULL);
346
+ }
347
+ if (strcmp(format, "webp") == 0) {
348
+ return vips_webpsave(image, path, "Q", quality, "strip", TRUE, NULL);
349
+ }
350
+ if (strcmp(format, "avif") == 0) {
351
+ return vips_heifsave(image, path, "Q", quality, "compression",
352
+ VIPS_FOREIGN_HEIF_COMPRESSION_AV1, "strip", TRUE, NULL);
353
+ }
354
+ if (strcmp(format, "gif") == 0) {
355
+ return vips_gifsave(image, path, "strip", TRUE, NULL);
356
+ }
357
+ if (strcmp(format, "jxl") == 0) {
358
+ return vips_jxlsave(image, path, "Q", quality, "strip", TRUE, NULL);
359
+ }
360
+ vips_error("safe_image", "%s", "unsupported output format");
361
+ return -1;
362
+ }
363
+
364
+ static int init_vips(const char *argv0) {
365
+ if (VIPS_INIT(argv0)) {
366
+ return -1;
367
+ }
368
+ if (vips_version(0) < 8 || (vips_version(0) == 8 && vips_version(1) < 13)) {
369
+ vips_error("safe_image", "libvips >= 8.13 is required (found %d.%d)", vips_version(0),
370
+ vips_version(1));
371
+ return -1;
372
+ }
373
+ vips_block_untrusted_set(TRUE);
374
+ vips_operation_block_set("VipsForeignLoadMagick", TRUE);
375
+ vips_operation_block_set("VipsForeignLoadMagick6", TRUE);
376
+ vips_operation_block_set("VipsForeignLoadMagick7", TRUE);
377
+ vips_operation_block_set("VipsForeignLoadJxl", FALSE);
378
+ vips_operation_block_set("VipsForeignSaveJxl", FALSE);
379
+ vips_concurrency_set(1);
380
+ vips_cache_set_max(0);
381
+ vips_cache_set_max_mem(0);
382
+ vips_cache_set_max_files(0);
383
+ return 0;
384
+ }
385
+
386
+ static void fail_response(const char *response, const char *klass) {
387
+ const char *msg = vips_error_buffer();
388
+ write_error(response, klass, msg && msg[0] ? msg : "libvips error");
389
+ }
390
+
391
+ static int cmd_version(options_t *opts) {
392
+ const char *response = opt_required(opts, "response");
393
+ char version[32];
394
+ snprintf(version, sizeof(version), "%d.%d.%d", vips_version(0), vips_version(1), vips_version(2));
395
+ write_value_string(response, version);
396
+ return 0;
397
+ }
398
+
399
+ static int cmd_probe(options_t *opts, double started) {
400
+ const char *response = opt_required(opts, "response");
401
+ const char *input = opt_required(opts, "input");
402
+ long long max_pixels = opt_ll(opts, "max-pixels", DEFAULT_MAX_PIXELS);
403
+ VipsImage *image = NULL;
404
+ const char *format = NULL;
405
+ if (load_image(input, FALSE, &image, &format) != 0) {
406
+ fail_response(response, "InvalidImageError");
407
+ return 1;
408
+ }
409
+ int check = check_pixels(image, max_pixels);
410
+ if (check != 0) {
411
+ fail_response(response, check == -3 ? "LimitError" : "InvalidImageError");
412
+ VIPS_UNREF(image);
413
+ return 1;
414
+ }
415
+ write_info(response, format, NULL, vips_image_get_width(image), vips_image_get_height(image),
416
+ now_ms() - started);
417
+ VIPS_UNREF(image);
418
+ return 0;
419
+ }
420
+
421
+ static int cmd_orientation(options_t *opts) {
422
+ const char *response = opt_required(opts, "response");
423
+ const char *input = opt_required(opts, "input");
424
+ long long max_pixels = opt_ll(opts, "max-pixels", DEFAULT_MAX_PIXELS);
425
+ VipsImage *image = NULL;
426
+ const char *format = NULL;
427
+ if (load_image(input, FALSE, &image, &format) != 0) {
428
+ fail_response(response, "InvalidImageError");
429
+ return 1;
430
+ }
431
+ int check = check_pixels(image, max_pixels);
432
+ if (check != 0) {
433
+ fail_response(response, check == -3 ? "LimitError" : "InvalidImageError");
434
+ VIPS_UNREF(image);
435
+ return 1;
436
+ }
437
+ int value = vips_image_get_orientation(image);
438
+ if (value < 1 || value > 8) {
439
+ value = 1;
440
+ }
441
+ write_value_int(response, value);
442
+ VIPS_UNREF(image);
443
+ return 0;
444
+ }
445
+
446
+ static int cmd_pages(options_t *opts) {
447
+ const char *response = opt_required(opts, "response");
448
+ const char *input = opt_required(opts, "input");
449
+ long long max_pixels = opt_ll(opts, "max-pixels", DEFAULT_MAX_PIXELS);
450
+ VipsImage *image = NULL;
451
+ const char *format = NULL;
452
+ if (load_image(input, FALSE, &image, &format) != 0) {
453
+ fail_response(response, "InvalidImageError");
454
+ return 1;
455
+ }
456
+ int check = check_pixels(image, max_pixels);
457
+ if (check != 0) {
458
+ fail_response(response, check == -3 ? "LimitError" : "InvalidImageError");
459
+ VIPS_UNREF(image);
460
+ return 1;
461
+ }
462
+ write_value_int(response, vips_image_get_n_pages(image));
463
+ VIPS_UNREF(image);
464
+ return 0;
465
+ }
466
+
467
+ static int cmd_thumbnail(options_t *opts, double started) {
468
+ const char *response = opt_required(opts, "response");
469
+ const char *input = opt_required(opts, "input");
470
+ const char *output = opt_required(opts, "output");
471
+ int width = opt_int(opts, "width", 0);
472
+ int height = opt_int(opts, "height", 0);
473
+ int quality = opt_int(opts, "quality", DEFAULT_JPEG_QUALITY);
474
+ const char *out_format = normalized_format(opt_required(opts, "format"));
475
+ const char *in_format = normalized_format(extname(input));
476
+ long long max_pixels = opt_ll(opts, "max-pixels", DEFAULT_MAX_PIXELS);
477
+ if (width <= 0 || height <= 0 || quality < 1 || quality > 100 || !out_format ||
478
+ strcmp(out_format, "heic") == 0 || !in_format) {
479
+ write_error(response, "ArgumentError", "invalid thumbnail arguments");
480
+ return 1;
481
+ }
482
+
483
+ int fd = open(input, O_RDONLY | O_CLOEXEC);
484
+ if (fd < 0) {
485
+ write_error(response, "InvalidImageError", strerror(errno));
486
+ return 1;
487
+ }
488
+
489
+ VipsImage *image = NULL, *thumb = NULL;
490
+ int header_fd = dup(fd);
491
+ if (header_fd < 0) {
492
+ close(fd);
493
+ write_error(response, "InvalidImageError", strerror(errno));
494
+ return 1;
495
+ }
496
+ VipsSource *header_source = vips_source_new_from_descriptor(header_fd);
497
+ if (!header_source || load_image_from_source(header_source, in_format, &image) != 0) {
498
+ if (header_source) {
499
+ g_object_unref(header_source);
500
+ } else {
501
+ close(header_fd);
502
+ }
503
+ close(fd);
504
+ fail_response(response, "InvalidImageError");
505
+ return 1;
506
+ }
507
+ g_object_unref(header_source);
508
+ int check = check_pixels(image, max_pixels);
509
+ VIPS_UNREF(image);
510
+ if (check != 0) {
511
+ close(fd);
512
+ fail_response(response, check == -3 ? "LimitError" : "InvalidImageError");
513
+ return 1;
514
+ }
515
+
516
+ if (lseek(fd, 0, SEEK_SET) < 0) {
517
+ close(fd);
518
+ write_error(response, "InvalidImageError", strerror(errno));
519
+ return 1;
520
+ }
521
+ int thumb_fd = dup(fd);
522
+ close(fd);
523
+ if (thumb_fd < 0) {
524
+ write_error(response, "InvalidImageError", strerror(errno));
525
+ return 1;
526
+ }
527
+ VipsSource *thumb_source = vips_source_new_from_descriptor(thumb_fd);
528
+ if (!thumb_source ||
529
+ vips_thumbnail_source(thumb_source, &thumb, width, "height", height, "size", VIPS_SIZE_BOTH,
530
+ "crop", VIPS_INTERESTING_CENTRE, "fail-on", VIPS_FAIL_ON_ERROR,
531
+ NULL) != 0 ||
532
+ save_image(thumb, output, out_format, quality) != 0) {
533
+ if (thumb_source) {
534
+ g_object_unref(thumb_source);
535
+ } else {
536
+ close(thumb_fd);
537
+ }
538
+ fail_response(response, "InvalidImageError");
539
+ VIPS_UNREF(thumb);
540
+ return 1;
541
+ }
542
+ g_object_unref(thumb_source);
543
+ write_info(response, in_format, out_format, vips_image_get_width(thumb),
544
+ vips_image_get_height(thumb), now_ms() - started);
545
+ VIPS_UNREF(thumb);
546
+ return 0;
547
+ }
548
+
549
+ static int cmd_resize(options_t *opts, double started) {
550
+ const char *response = opt_required(opts, "response");
551
+ const char *input = opt_required(opts, "input");
552
+ const char *output = opt_required(opts, "output");
553
+ double scale = opt_double(opts, "scale", 0.0);
554
+ int quality = opt_int(opts, "quality", DEFAULT_JPEG_QUALITY);
555
+ const char *out_format = normalized_format(opt_required(opts, "format"));
556
+ long long max_pixels = opt_ll(opts, "max-pixels", DEFAULT_MAX_PIXELS);
557
+ if (scale <= 0.0 || scale > 100.0 || quality < 1 || quality > 100 || !out_format ||
558
+ strcmp(out_format, "heic") == 0) {
559
+ write_error(response, "ArgumentError", "invalid resize arguments");
560
+ return 1;
561
+ }
562
+ VipsImage *image = NULL, *rot = NULL, *resized = NULL;
563
+ const char *in_format = NULL;
564
+ if (load_image(input, TRUE, &image, &in_format) != 0) {
565
+ fail_response(response, "InvalidImageError");
566
+ return 1;
567
+ }
568
+ int check = check_pixels(image, max_pixels);
569
+ if (check != 0) {
570
+ fail_response(response, check == -3 ? "LimitError" : "InvalidImageError");
571
+ VIPS_UNREF(image);
572
+ return 1;
573
+ }
574
+ if (vips_autorot(image, &rot, NULL) != 0 || vips_resize(rot, &resized, scale, NULL) != 0 ||
575
+ save_image(resized, output, out_format, quality) != 0) {
576
+ fail_response(response, "InvalidImageError");
577
+ VIPS_UNREF(image);
578
+ VIPS_UNREF(rot);
579
+ VIPS_UNREF(resized);
580
+ return 1;
581
+ }
582
+ write_info(response, in_format, out_format, vips_image_get_width(resized),
583
+ vips_image_get_height(resized), now_ms() - started);
584
+ VIPS_UNREF(image);
585
+ VIPS_UNREF(rot);
586
+ VIPS_UNREF(resized);
587
+ return 0;
588
+ }
589
+
590
+ static int cmd_crop_north(options_t *opts, double started) {
591
+ const char *response = opt_required(opts, "response");
592
+ const char *input = opt_required(opts, "input");
593
+ const char *output = opt_required(opts, "output");
594
+ int width = opt_int(opts, "width", 0), height = opt_int(opts, "height", 0),
595
+ quality = opt_int(opts, "quality", DEFAULT_JPEG_QUALITY);
596
+ const char *out_format = normalized_format(opt_required(opts, "format"));
597
+ long long max_pixels = opt_ll(opts, "max-pixels", DEFAULT_MAX_PIXELS);
598
+ if (width <= 0 || height <= 0 || quality < 1 || quality > 100 || !out_format ||
599
+ strcmp(out_format, "heic") == 0) {
600
+ write_error(response, "ArgumentError", "invalid crop arguments");
601
+ return 1;
602
+ }
603
+ VipsImage *image = NULL, *rot = NULL, *resized = NULL, *cropped = NULL;
604
+ const char *in_format = NULL;
605
+ if (load_image(input, TRUE, &image, &in_format) != 0) {
606
+ fail_response(response, "InvalidImageError");
607
+ return 1;
608
+ }
609
+ int check = check_pixels(image, max_pixels);
610
+ if (check != 0) {
611
+ fail_response(response, check == -3 ? "LimitError" : "InvalidImageError");
612
+ VIPS_UNREF(image);
613
+ return 1;
614
+ }
615
+ if (vips_autorot(image, &rot, NULL) != 0) {
616
+ fail_response(response, "InvalidImageError");
617
+ VIPS_UNREF(image);
618
+ return 1;
619
+ }
620
+ double scale =
621
+ fmax((double)width / vips_image_get_width(rot), (double)height / vips_image_get_height(rot)) *
622
+ 1.0000001;
623
+ int left;
624
+ if (vips_resize(rot, &resized, scale, NULL) != 0) {
625
+ fail_response(response, "InvalidImageError");
626
+ VIPS_UNREF(image);
627
+ VIPS_UNREF(rot);
628
+ return 1;
629
+ }
630
+ left = (vips_image_get_width(resized) - width) / 2;
631
+ if (left < 0) {
632
+ left = 0;
633
+ }
634
+ if (vips_extract_area(resized, &cropped, left, 0, width, height, NULL) != 0 ||
635
+ save_image(cropped, output, out_format, quality) != 0) {
636
+ fail_response(response, "InvalidImageError");
637
+ VIPS_UNREF(image);
638
+ VIPS_UNREF(rot);
639
+ VIPS_UNREF(resized);
640
+ VIPS_UNREF(cropped);
641
+ return 1;
642
+ }
643
+ write_info(response, in_format, out_format, vips_image_get_width(cropped),
644
+ vips_image_get_height(cropped), now_ms() - started);
645
+ VIPS_UNREF(image);
646
+ VIPS_UNREF(rot);
647
+ VIPS_UNREF(resized);
648
+ VIPS_UNREF(cropped);
649
+ return 0;
650
+ }
651
+
652
+ static int cmd_convert(options_t *opts, double started) {
653
+ const char *response = opt_required(opts, "response");
654
+ const char *input = opt_required(opts, "input");
655
+ const char *output = opt_required(opts, "output");
656
+ int quality = opt_int(opts, "quality", DEFAULT_NATIVE_CONVERT_JPEG_QUALITY);
657
+ const char *out_format = normalized_format(opt_required(opts, "format"));
658
+ long long max_pixels = opt_ll(opts, "max-pixels", DEFAULT_MAX_PIXELS);
659
+ if (quality < 1 || quality > 100 || !out_format || strcmp(out_format, "heic") == 0) {
660
+ write_error(response, "ArgumentError", "invalid convert arguments");
661
+ return 1;
662
+ }
663
+ VipsImage *image = NULL, *rot = NULL, *final = NULL;
664
+ const char *in_format = NULL;
665
+ if (load_image(input, TRUE, &image, &in_format) != 0) {
666
+ fail_response(response, "InvalidImageError");
667
+ return 1;
668
+ }
669
+ int check = check_pixels(image, max_pixels);
670
+ if (check != 0) {
671
+ fail_response(response, check == -3 ? "LimitError" : "InvalidImageError");
672
+ VIPS_UNREF(image);
673
+ return 1;
674
+ }
675
+ if (vips_autorot(image, &rot, NULL) != 0) {
676
+ fail_response(response, "InvalidImageError");
677
+ VIPS_UNREF(image);
678
+ return 1;
679
+ }
680
+ if (strcmp(out_format, "jpg") == 0 && vips_image_hasalpha(rot)) {
681
+ double bg[3] = {255.0, 255.0, 255.0};
682
+ VipsArrayDouble *arr = vips_array_double_new(bg, 3);
683
+ if (vips_flatten(rot, &final, "background", arr, NULL) != 0) {
684
+ vips_area_unref((VipsArea *)arr);
685
+ fail_response(response, "InvalidImageError");
686
+ VIPS_UNREF(image);
687
+ VIPS_UNREF(rot);
688
+ return 1;
689
+ }
690
+ vips_area_unref((VipsArea *)arr);
691
+ } else {
692
+ final = rot;
693
+ g_object_ref(final);
694
+ }
695
+ if (save_image(final, output, out_format, quality) != 0) {
696
+ fail_response(response, "InvalidImageError");
697
+ VIPS_UNREF(image);
698
+ VIPS_UNREF(rot);
699
+ VIPS_UNREF(final);
700
+ return 1;
701
+ }
702
+ write_info(response, in_format, out_format, vips_image_get_width(final),
703
+ vips_image_get_height(final), now_ms() - started);
704
+ VIPS_UNREF(image);
705
+ VIPS_UNREF(rot);
706
+ VIPS_UNREF(final);
707
+ return 0;
708
+ }
709
+
710
+ static int cmd_dominant_color(options_t *opts) {
711
+ const char *response = opt_required(opts, "response");
712
+ const char *input = opt_required(opts, "input");
713
+ long long max_pixels = opt_ll(opts, "max-pixels", DEFAULT_MAX_PIXELS);
714
+ VipsImage *image = NULL, *srgb = NULL, *work = NULL, *stats = NULL;
715
+ const char *format = NULL;
716
+ if (load_image(input, FALSE, &image, &format) != 0) {
717
+ fail_response(response, "InvalidImageError");
718
+ return 1;
719
+ }
720
+ int check = check_pixels(image, max_pixels);
721
+ if (check != 0) {
722
+ fail_response(response, check == -3 ? "LimitError" : "InvalidImageError");
723
+ VIPS_UNREF(image);
724
+ return 1;
725
+ }
726
+ if (vips_colourspace_issupported(image)) {
727
+ if (vips_colourspace(image, &srgb, VIPS_INTERPRETATION_sRGB, NULL) != 0) {
728
+ fail_response(response, "InvalidImageError");
729
+ VIPS_UNREF(image);
730
+ return 1;
731
+ }
732
+ } else {
733
+ srgb = image;
734
+ g_object_ref(srgb);
735
+ }
736
+ gboolean has_alpha = vips_image_hasalpha(srgb);
737
+ if (has_alpha) {
738
+ if (vips_premultiply(srgb, &work, NULL) != 0) {
739
+ fail_response(response, "InvalidImageError");
740
+ VIPS_UNREF(image);
741
+ VIPS_UNREF(srgb);
742
+ return 1;
743
+ }
744
+ } else {
745
+ work = srgb;
746
+ g_object_ref(work);
747
+ }
748
+ if (vips_stats(work, &stats, NULL) != 0) {
749
+ fail_response(response, "InvalidImageError");
750
+ VIPS_UNREF(image);
751
+ VIPS_UNREF(srgb);
752
+ VIPS_UNREF(work);
753
+ return 1;
754
+ }
755
+ size_t len = 0;
756
+ double *matrix = (double *)vips_image_write_to_memory(stats, &len);
757
+ if (!matrix) {
758
+ fail_response(response, "InvalidImageError");
759
+ VIPS_UNREF(image);
760
+ VIPS_UNREF(srgb);
761
+ VIPS_UNREF(work);
762
+ VIPS_UNREF(stats);
763
+ return 1;
764
+ }
765
+ int columns = vips_image_get_width(stats), bands = vips_image_get_bands(work);
766
+ int colour_bands = has_alpha ? bands - 1 : bands;
767
+ if (colour_bands > 3) {
768
+ colour_bands = 3;
769
+ }
770
+ if (colour_bands < 1) {
771
+ colour_bands = 1;
772
+ }
773
+ double alpha_mean = has_alpha ? matrix[(bands - 1 + 1) * columns + 4] : 255.0;
774
+ int rgb[3];
775
+ for (int b = 0; b < 3; b++) {
776
+ int src = b < colour_bands ? b : colour_bands - 1;
777
+ double value = matrix[(src + 1) * columns + 4];
778
+ if (has_alpha) {
779
+ value = alpha_mean > 0.0 ? value * 255.0 / alpha_mean : 0.0;
780
+ }
781
+ int iv = (int)llround(value);
782
+ if (iv < 0) {
783
+ iv = 0;
784
+ }
785
+ if (iv > 255) {
786
+ iv = 255;
787
+ }
788
+ rgb[b] = iv;
789
+ }
790
+ g_free(matrix);
791
+ char hex[7];
792
+ snprintf(hex, sizeof(hex), "%02X%02X%02X", rgb[0], rgb[1], rgb[2]);
793
+ write_value_string(response, hex);
794
+ VIPS_UNREF(image);
795
+ VIPS_UNREF(srgb);
796
+ VIPS_UNREF(work);
797
+ VIPS_UNREF(stats);
798
+ return 0;
799
+ }
800
+
801
+ static int cmd_png_from_rgba(options_t *opts) {
802
+ const char *response = opt_required(opts, "response");
803
+ const char *raw_input = opt_required(opts, "raw-input");
804
+ const char *output = opt_required(opts, "output");
805
+ int width = opt_int(opts, "width", 0);
806
+ int height = opt_int(opts, "height", 0);
807
+ if (width <= 0 || height <= 0) {
808
+ write_error(response, "ArgumentError", "width and height must be positive");
809
+ return 1;
810
+ }
811
+ if (width > 4096 || height > 4096) {
812
+ write_error(response, "LimitError", "rgba buffer dimensions exceed 4096x4096");
813
+ return 1;
814
+ }
815
+
816
+ gchar *bytes = NULL;
817
+ gsize length = 0;
818
+ GError *error = NULL;
819
+ if (!g_file_get_contents(raw_input, &bytes, &length, &error)) {
820
+ write_error(response, "InvalidImageError",
821
+ error ? error->message : "could not read rgba input");
822
+ if (error) {
823
+ g_error_free(error);
824
+ }
825
+ return 1;
826
+ }
827
+
828
+ gsize expected = (gsize)width * (gsize)height * 4;
829
+ if (length != expected) {
830
+ g_free(bytes);
831
+ write_error(response, "ArgumentError", "rgba buffer must be width*height*4 bytes");
832
+ return 1;
833
+ }
834
+
835
+ VipsImage *image =
836
+ vips_image_new_from_memory_copy(bytes, length, width, height, 4, VIPS_FORMAT_UCHAR);
837
+ g_free(bytes);
838
+ if (!image) {
839
+ fail_response(response, "InvalidImageError");
840
+ return 1;
841
+ }
842
+
843
+ VipsImage *srgb = NULL;
844
+ if (vips_copy(image, &srgb, "interpretation", VIPS_INTERPRETATION_sRGB, NULL) != 0 ||
845
+ save_image(srgb, output, "png", 100) != 0) {
846
+ fail_response(response, "InvalidImageError");
847
+ VIPS_UNREF(image);
848
+ VIPS_UNREF(srgb);
849
+ return 1;
850
+ }
851
+
852
+ write_info(response, "rgba", "png", width, height, 0.0);
853
+ VIPS_UNREF(image);
854
+ VIPS_UNREF(srgb);
855
+ return 0;
856
+ }
857
+
858
+ static int cmd_letter_avatar(options_t *opts, double started) {
859
+ const char *response = opt_required(opts, "response");
860
+ const char *output = opt_required(opts, "output");
861
+ int size = opt_int(opts, "size", 0);
862
+ int red = opt_int(opts, "red", -1);
863
+ int green = opt_int(opts, "green", -1);
864
+ int blue = opt_int(opts, "blue", -1);
865
+ const char *markup = opt(opts, "markup");
866
+ const char *font = opt_required(opts, "font");
867
+ if (!markup) {
868
+ markup = "";
869
+ }
870
+ const char *fontfile = opt(opts, "fontfile");
871
+ if (!fontfile) {
872
+ fontfile = "";
873
+ }
874
+
875
+ if (size < 1 || size > 4096) {
876
+ write_error(response, "ArgumentError", "size must be 1..4096");
877
+ return 1;
878
+ }
879
+ if (red < 0 || red > 255 || green < 0 || green > 255 || blue < 0 || blue > 255) {
880
+ write_error(response, "ArgumentError", "background channels must be 0..255");
881
+ return 1;
882
+ }
883
+ if (vips_type_find("VipsOperation", "text") == 0) {
884
+ write_error(response, "UnsupportedFormatError",
885
+ "this libvips build has no text renderer (Pango support missing)");
886
+ return 1;
887
+ }
888
+
889
+ VipsImage *text = NULL, *cropped = NULL, *mask = NULL;
890
+ VipsImage *blended = NULL, *cast = NULL, *srgb = NULL;
891
+ if (markup[0] == '\0') {
892
+ if (vips_black(&mask, size, size, NULL) != 0) {
893
+ fail_response(response, "InvalidImageError");
894
+ return 1;
895
+ }
896
+ } else {
897
+ int text_rc;
898
+ if (fontfile[0] != '\0') {
899
+ text_rc = vips_text(&text, markup, "font", font, "dpi", 72, "fontfile", fontfile, NULL);
900
+ } else {
901
+ text_rc = vips_text(&text, markup, "font", font, "dpi", 72, NULL);
902
+ }
903
+ if (text_rc != 0) {
904
+ fail_response(response, "InvalidImageError");
905
+ return 1;
906
+ }
907
+
908
+ int text_w = vips_image_get_width(text);
909
+ int text_h = vips_image_get_height(text);
910
+ VipsImage *ink = text;
911
+ if (text_w > size || text_h > size) {
912
+ int crop_w = text_w < size ? text_w : size;
913
+ int crop_h = text_h < size ? text_h : size;
914
+ if (vips_extract_area(text, &cropped, (text_w - crop_w) / 2, (text_h - crop_h) / 2, crop_w,
915
+ crop_h, NULL) != 0) {
916
+ fail_response(response, "InvalidImageError");
917
+ VIPS_UNREF(text);
918
+ return 1;
919
+ }
920
+ ink = cropped;
921
+ text_w = crop_w;
922
+ text_h = crop_h;
923
+ }
924
+
925
+ if (vips_embed(ink, &mask, (size - text_w) / 2, (size - text_h) / 2, size, size, NULL) != 0) {
926
+ fail_response(response, "InvalidImageError");
927
+ VIPS_UNREF(text);
928
+ VIPS_UNREF(cropped);
929
+ return 1;
930
+ }
931
+ }
932
+
933
+ double opacity = 204.0 / 255.0;
934
+ double a[3] = {
935
+ (255.0 - (double)red) * opacity / 255.0,
936
+ (255.0 - (double)green) * opacity / 255.0,
937
+ (255.0 - (double)blue) * opacity / 255.0,
938
+ };
939
+ double b[3] = {(double)red, (double)green, (double)blue};
940
+ if (vips_linear(mask, &blended, a, b, 3, NULL) != 0 ||
941
+ vips_cast(blended, &cast, VIPS_FORMAT_UCHAR, NULL) != 0 ||
942
+ vips_copy(cast, &srgb, "interpretation", VIPS_INTERPRETATION_sRGB, NULL) != 0 ||
943
+ save_image(srgb, output, "png", 100) != 0) {
944
+ fail_response(response, "InvalidImageError");
945
+ VIPS_UNREF(text);
946
+ VIPS_UNREF(cropped);
947
+ VIPS_UNREF(mask);
948
+ VIPS_UNREF(blended);
949
+ VIPS_UNREF(cast);
950
+ VIPS_UNREF(srgb);
951
+ return 1;
952
+ }
953
+
954
+ write_info(response, "generated", "png", size, size, now_ms() - started);
955
+ VIPS_UNREF(text);
956
+ VIPS_UNREF(cropped);
957
+ VIPS_UNREF(mask);
958
+ VIPS_UNREF(blended);
959
+ VIPS_UNREF(cast);
960
+ VIPS_UNREF(srgb);
961
+ return 0;
962
+ }
963
+
964
+ int main(int argc, char **argv) {
965
+ if (argc < 3) {
966
+ fprintf(stderr, "usage: %s COMMAND --response PATH ...\n", argv[0]);
967
+ return 2;
968
+ }
969
+ double started = now_ms();
970
+ options_t opts;
971
+ parse_options(argc, argv, &opts);
972
+ const char *response = opt_required(&opts, "response");
973
+ if (init_vips(argv[0]) != 0) {
974
+ fail_response(response, "VipsUnavailableError");
975
+ return 1;
976
+ }
977
+ const char *cmd = argv[1];
978
+ int rc;
979
+ if (strcmp(cmd, "version") == 0) {
980
+ rc = cmd_version(&opts);
981
+ } else if (strcmp(cmd, "probe") == 0) {
982
+ rc = cmd_probe(&opts, started);
983
+ } else if (strcmp(cmd, "orientation") == 0) {
984
+ rc = cmd_orientation(&opts);
985
+ } else if (strcmp(cmd, "pages") == 0) {
986
+ rc = cmd_pages(&opts);
987
+ } else if (strcmp(cmd, "thumbnail") == 0) {
988
+ rc = cmd_thumbnail(&opts, started);
989
+ } else if (strcmp(cmd, "resize") == 0) {
990
+ rc = cmd_resize(&opts, started);
991
+ } else if (strcmp(cmd, "crop-north") == 0) {
992
+ rc = cmd_crop_north(&opts, started);
993
+ } else if (strcmp(cmd, "convert") == 0) {
994
+ rc = cmd_convert(&opts, started);
995
+ } else if (strcmp(cmd, "dominant-color") == 0) {
996
+ rc = cmd_dominant_color(&opts);
997
+ } else if (strcmp(cmd, "png-from-rgba") == 0) {
998
+ rc = cmd_png_from_rgba(&opts);
999
+ } else if (strcmp(cmd, "letter-avatar") == 0) {
1000
+ rc = cmd_letter_avatar(&opts, started);
1001
+ } else {
1002
+ write_error(response, "ArgumentError", "unsupported helper command");
1003
+ rc = 2;
1004
+ }
1005
+ vips_shutdown();
1006
+ return rc;
1007
+ }