safe_image 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +703 -0
- data/SECURITY.md +48 -0
- data/ext/safe_image_native/extconf.rb +8 -0
- data/ext/safe_image_native/safe_image_native.c +392 -0
- data/lib/safe_image/RT_sRGB.icm +0 -0
- data/lib/safe_image/discourse_compat.rb +283 -0
- data/lib/safe_image/image_magick_backend.rb +263 -0
- data/lib/safe_image/imagemagick_policy/policy.xml +22 -0
- data/lib/safe_image/jpegli_backend.rb +109 -0
- data/lib/safe_image/native.rb +3 -0
- data/lib/safe_image/optimizer.rb +78 -0
- data/lib/safe_image/path_safety.rb +63 -0
- data/lib/safe_image/processor.rb +196 -0
- data/lib/safe_image/remote.rb +309 -0
- data/lib/safe_image/result.rb +28 -0
- data/lib/safe_image/runner.rb +174 -0
- data/lib/safe_image/sandbox.rb +236 -0
- data/lib/safe_image/svg_metadata.rb +132 -0
- data/lib/safe_image/svg_sanitizer.rb +102 -0
- data/lib/safe_image/version.rb +5 -0
- data/lib/safe_image/vips_backend.rb +50 -0
- data/lib/safe_image.rb +272 -0
- metadata +140 -0
data/SECURITY.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
Safe Image is a hardened image-processing boundary for untrusted uploads, not a proof that hostile image bytes are harmless.
|
|
4
|
+
|
|
5
|
+
## Supported versions
|
|
6
|
+
|
|
7
|
+
Security fixes are expected to land on `main` until the gem has tagged releases. Once releases exist, report against the latest released version unless you can reproduce on `main` as well.
|
|
8
|
+
|
|
9
|
+
## Threat model
|
|
10
|
+
|
|
11
|
+
Safe Image assumes image input may be attacker-controlled. The library is designed to reduce the number of places an application touches those bytes and to remove common image-processing foot-guns:
|
|
12
|
+
|
|
13
|
+
- shell-free external command execution using argv arrays
|
|
14
|
+
- allowlisted command environment
|
|
15
|
+
- bounded command output and process-group timeout cleanup
|
|
16
|
+
- explicit libvips loader selection for supported raster formats
|
|
17
|
+
- no silent fallback from libvips to generic ImageMagick decoding
|
|
18
|
+
- restrictive ImageMagick policy disabling delegates, filters, `@file`, remote URL coders, Ghostscript-backed formats, and dangerous pseudo-formats
|
|
19
|
+
- symlink rejection for untrusted local input/output paths
|
|
20
|
+
- remote fetch SSRF hardening: scheme/port restrictions, special-use IP blocking, DNS pinning, redirect limits, HTTPS-to-HTTP rejection, header allowlists, content-type/extension agreement, and probe-before-yield
|
|
21
|
+
- bounded SVG metadata parsing and conservative SVG sanitising without handing SVG to ImageMagick for probing
|
|
22
|
+
- optional Linux Landlock/seccomp subprocess sandboxing
|
|
23
|
+
|
|
24
|
+
## Non-goals
|
|
25
|
+
|
|
26
|
+
Safe Image does not claim that parsing hostile images in-process is memory-safe. Raster decoders such as libjpeg, libpng, libwebp, libheif, libvips loaders, and ImageMagick coders still parse attacker-controlled bytes. A decoder memory-corruption bug or pathological resource-consumption bug is still possible.
|
|
27
|
+
|
|
28
|
+
The honest claim is defense-in-depth:
|
|
29
|
+
|
|
30
|
+
- without Landlock: centralized and hardened image processing with major delegate/protocol/policy foot-guns removed
|
|
31
|
+
- with Landlock: the same hardening plus a kernel containment boundary around subprocess-based public operations
|
|
32
|
+
|
|
33
|
+
If your deployment needs a hard isolation boundary, enable sandbox execution and run image processing away from your main web worker process.
|
|
34
|
+
|
|
35
|
+
## Reporting vulnerabilities
|
|
36
|
+
|
|
37
|
+
Please report suspected security issues privately to `sam@discourse.org`.
|
|
38
|
+
|
|
39
|
+
Include:
|
|
40
|
+
|
|
41
|
+
- affected version or commit
|
|
42
|
+
- input file or minimized reproducer, if shareable
|
|
43
|
+
- operation/API called
|
|
44
|
+
- expected vs actual result
|
|
45
|
+
- whether Landlock sandboxing was enabled
|
|
46
|
+
- host OS, kernel, libvips, ImageMagick, and optimizer tool versions
|
|
47
|
+
|
|
48
|
+
Do not open a public issue for an exploitable crash, sandbox escape, SSRF bypass, arbitrary file read/write, command execution bug, or denial-of-service vector until there has been time to patch.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mkmf"
|
|
4
|
+
|
|
5
|
+
pkg_config("vips") or abort "libvips development files are required (pkg-config vips failed)"
|
|
6
|
+
have_header("vips/vips.h") or abort "missing vips/vips.h"
|
|
7
|
+
have_library("vips") or abort "missing libvips"
|
|
8
|
+
create_makefile("safe_image_native")
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
#include <ruby.h>
|
|
2
|
+
#include <vips/vips.h>
|
|
3
|
+
#include <sys/stat.h>
|
|
4
|
+
#include <string.h>
|
|
5
|
+
#include <time.h>
|
|
6
|
+
#include <math.h>
|
|
7
|
+
|
|
8
|
+
static VALUE mSafeImage;
|
|
9
|
+
static VALUE mNative;
|
|
10
|
+
static VALUE eError;
|
|
11
|
+
static VALUE eUnsupported;
|
|
12
|
+
static VALUE eInvalid;
|
|
13
|
+
static VALUE eLimit;
|
|
14
|
+
|
|
15
|
+
/* Default decompression-bomb ceiling applied when the caller does not pass an
|
|
16
|
+
* explicit max_pixels. Mirrors SafeImage::DEFAULT_MAX_PIXELS and the 128MP area
|
|
17
|
+
* limit used on the ImageMagick path, so the libvips fast path is not unbounded
|
|
18
|
+
* by default. Callers that legitimately need larger images pass max_pixels. */
|
|
19
|
+
#define SAFE_IMAGE_DEFAULT_MAX_PIXELS (128LL * 1024 * 1024)
|
|
20
|
+
|
|
21
|
+
static double now_ms(void) {
|
|
22
|
+
struct timespec ts;
|
|
23
|
+
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
24
|
+
return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1000000.0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static const char *extname(const char *path) {
|
|
28
|
+
const char *dot = strrchr(path, '.');
|
|
29
|
+
return dot ? dot + 1 : "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static int streq_ci(const char *a, const char *b) {
|
|
33
|
+
#ifdef _WIN32
|
|
34
|
+
return _stricmp(a, b) == 0;
|
|
35
|
+
#else
|
|
36
|
+
return strcasecmp(a, b) == 0;
|
|
37
|
+
#endif
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static const char *normalized_format(const char *fmt) {
|
|
41
|
+
if (streq_ci(fmt, "jpg") || streq_ci(fmt, "jpeg")) return "jpg";
|
|
42
|
+
if (streq_ci(fmt, "png")) return "png";
|
|
43
|
+
if (streq_ci(fmt, "webp")) return "webp";
|
|
44
|
+
if (streq_ci(fmt, "heic") || streq_ci(fmt, "heif")) return "heic";
|
|
45
|
+
if (streq_ci(fmt, "avif")) return "avif";
|
|
46
|
+
return NULL;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static void raise_vips(void) {
|
|
50
|
+
const char *msg = vips_error_buffer();
|
|
51
|
+
VALUE rb_msg = rb_str_new_cstr(msg && *msg ? msg : "libvips error");
|
|
52
|
+
vips_error_clear();
|
|
53
|
+
rb_exc_raise(rb_exc_new3(eInvalid, rb_msg));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static void init_vips_once(void) {
|
|
57
|
+
static int initialized = 0;
|
|
58
|
+
if (initialized) return;
|
|
59
|
+
if (VIPS_INIT("safe_image") != 0) raise_vips();
|
|
60
|
+
|
|
61
|
+
/* Avoid libvips operations that are explicitly tagged as unsafe for
|
|
62
|
+
* untrusted input. Also block ImageMagick-backed loaders by class name;
|
|
63
|
+
* this gem uses explicit native libvips loaders and should never fall back
|
|
64
|
+
* to ImageMagick delegates. */
|
|
65
|
+
vips_block_untrusted_set(TRUE);
|
|
66
|
+
vips_operation_block_set("VipsForeignLoadMagick", TRUE);
|
|
67
|
+
vips_operation_block_set("VipsForeignLoadMagick6", TRUE);
|
|
68
|
+
vips_operation_block_set("VipsForeignLoadMagick7", TRUE);
|
|
69
|
+
|
|
70
|
+
/* Keep the embedded path predictable and bounded. Callers that want harder
|
|
71
|
+
* isolation should run this gem inside a sandboxed worker process. */
|
|
72
|
+
vips_concurrency_set(1);
|
|
73
|
+
vips_cache_set_max(0);
|
|
74
|
+
vips_cache_set_max_mem(0);
|
|
75
|
+
vips_cache_set_max_files(0);
|
|
76
|
+
initialized = 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static VipsImage *load_explicit(const char *path, const char **fmt_out) {
|
|
80
|
+
init_vips_once();
|
|
81
|
+
const char *fmt = normalized_format(extname(path));
|
|
82
|
+
if (!fmt) rb_raise(eUnsupported, "unsupported input format");
|
|
83
|
+
|
|
84
|
+
VipsImage *image = NULL;
|
|
85
|
+
int rc = -1;
|
|
86
|
+
if (strcmp(fmt, "jpg") == 0) {
|
|
87
|
+
rc = vips_jpegload(path, &image,
|
|
88
|
+
"access", VIPS_ACCESS_SEQUENTIAL,
|
|
89
|
+
"fail_on", VIPS_FAIL_ON_ERROR,
|
|
90
|
+
NULL);
|
|
91
|
+
} else if (strcmp(fmt, "png") == 0) {
|
|
92
|
+
rc = vips_pngload(path, &image,
|
|
93
|
+
"access", VIPS_ACCESS_SEQUENTIAL,
|
|
94
|
+
"fail_on", VIPS_FAIL_ON_ERROR,
|
|
95
|
+
NULL);
|
|
96
|
+
} else if (strcmp(fmt, "webp") == 0) {
|
|
97
|
+
rc = vips_webpload(path, &image,
|
|
98
|
+
"access", VIPS_ACCESS_SEQUENTIAL,
|
|
99
|
+
"fail_on", VIPS_FAIL_ON_ERROR,
|
|
100
|
+
NULL);
|
|
101
|
+
} else if (strcmp(fmt, "heic") == 0 || strcmp(fmt, "avif") == 0) {
|
|
102
|
+
rc = vips_heifload(path, &image,
|
|
103
|
+
"access", VIPS_ACCESS_SEQUENTIAL,
|
|
104
|
+
"fail_on", VIPS_FAIL_ON_ERROR,
|
|
105
|
+
NULL);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (rc != 0 || image == NULL) raise_vips();
|
|
109
|
+
*fmt_out = fmt;
|
|
110
|
+
return image;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static void validate_quality_or_raise(int quality) {
|
|
114
|
+
if (quality < 1 || quality > 100) rb_raise(rb_eArgError, "quality must be 1..100");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
static void validate_dimensions_or_raise(int width, int height) {
|
|
118
|
+
if (width <= 0 || height <= 0) rb_raise(rb_eArgError, "width and height must be positive");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
static void validate_scale_or_raise(double scale) {
|
|
122
|
+
if (!isfinite(scale) || scale <= 0.0 || scale > 100.0) rb_raise(rb_eArgError, "scale must be finite and in 0..100");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
static int pixels_exceed_limit(VipsImage *image, VALUE max_pixels_val, long long *pixels_out, long long *max_out) {
|
|
126
|
+
long long max_pixels;
|
|
127
|
+
if (NIL_P(max_pixels_val)) {
|
|
128
|
+
max_pixels = SAFE_IMAGE_DEFAULT_MAX_PIXELS;
|
|
129
|
+
} else {
|
|
130
|
+
max_pixels = NUM2LL(max_pixels_val);
|
|
131
|
+
if (max_pixels <= 0) rb_raise(rb_eArgError, "max_pixels must be positive");
|
|
132
|
+
}
|
|
133
|
+
if (image->Xsize <= 0 || image->Ysize <= 0) rb_raise(eInvalid, "image dimensions are invalid");
|
|
134
|
+
long long pixels = (long long)image->Xsize * (long long)image->Ysize;
|
|
135
|
+
if (pixels_out) *pixels_out = pixels;
|
|
136
|
+
if (max_out) *max_out = max_pixels;
|
|
137
|
+
return pixels > max_pixels;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
static void raise_pixels_limit(long long pixels, long long max_pixels) {
|
|
141
|
+
rb_raise(eLimit, "image has %lld pixels, exceeds %lld", pixels, max_pixels);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
static int save_explicit(VipsImage *image, const char *path, const char *fmt, int quality) {
|
|
145
|
+
if (strcmp(fmt, "jpg") == 0) {
|
|
146
|
+
return vips_jpegsave(image, path,
|
|
147
|
+
"Q", quality,
|
|
148
|
+
"interlace", FALSE,
|
|
149
|
+
"keep", VIPS_FOREIGN_KEEP_NONE,
|
|
150
|
+
NULL);
|
|
151
|
+
} else if (strcmp(fmt, "png") == 0) {
|
|
152
|
+
return vips_pngsave(image, path,
|
|
153
|
+
"compression", 6,
|
|
154
|
+
"keep", VIPS_FOREIGN_KEEP_NONE,
|
|
155
|
+
NULL);
|
|
156
|
+
} else if (strcmp(fmt, "webp") == 0) {
|
|
157
|
+
return vips_webpsave(image, path,
|
|
158
|
+
"Q", quality,
|
|
159
|
+
"keep", VIPS_FOREIGN_KEEP_NONE,
|
|
160
|
+
NULL);
|
|
161
|
+
} else if (strcmp(fmt, "avif") == 0) {
|
|
162
|
+
return vips_heifsave(image, path,
|
|
163
|
+
"Q", quality,
|
|
164
|
+
"compression", VIPS_FOREIGN_HEIF_COMPRESSION_AV1,
|
|
165
|
+
"keep", VIPS_FOREIGN_KEEP_NONE,
|
|
166
|
+
NULL);
|
|
167
|
+
}
|
|
168
|
+
rb_raise(eUnsupported, "unsupported output format");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
static VALUE rb_probe(VALUE self, VALUE path_val) {
|
|
172
|
+
Check_Type(path_val, T_STRING);
|
|
173
|
+
double start = now_ms();
|
|
174
|
+
const char *fmt = NULL;
|
|
175
|
+
VipsImage *image = load_explicit(StringValueCStr(path_val), &fmt);
|
|
176
|
+
int width = image->Xsize;
|
|
177
|
+
int height = image->Ysize;
|
|
178
|
+
double duration_ms = now_ms() - start;
|
|
179
|
+
g_object_unref(image);
|
|
180
|
+
|
|
181
|
+
VALUE hash = rb_hash_new();
|
|
182
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("format")), rb_str_new_cstr(fmt));
|
|
183
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("width")), INT2NUM(width));
|
|
184
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("height")), INT2NUM(height));
|
|
185
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("duration_ms")), DBL2NUM(duration_ms));
|
|
186
|
+
return hash;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
static VALUE rb_thumbnail(VALUE self, VALUE input_val, VALUE output_val, VALUE width_val, VALUE height_val, VALUE format_val, VALUE quality_val, VALUE max_pixels_val) {
|
|
190
|
+
Check_Type(input_val, T_STRING);
|
|
191
|
+
Check_Type(output_val, T_STRING);
|
|
192
|
+
Check_Type(format_val, T_STRING);
|
|
193
|
+
int width = NUM2INT(width_val);
|
|
194
|
+
int height = NUM2INT(height_val);
|
|
195
|
+
int quality = NUM2INT(quality_val);
|
|
196
|
+
validate_dimensions_or_raise(width, height);
|
|
197
|
+
validate_quality_or_raise(quality);
|
|
198
|
+
const char *out_fmt = normalized_format(StringValueCStr(format_val));
|
|
199
|
+
if (!out_fmt || strcmp(out_fmt, "heic") == 0) rb_raise(eUnsupported, "unsupported output format");
|
|
200
|
+
|
|
201
|
+
const char *input_path = StringValueCStr(input_val);
|
|
202
|
+
double start = now_ms();
|
|
203
|
+
|
|
204
|
+
/* Read the header through an explicit allowlisted loader. This validates the
|
|
205
|
+
* input format (the loader fails on mismatched bytes) and lets us enforce the
|
|
206
|
+
* pixel-count limit before any full decode happens. */
|
|
207
|
+
const char *input_fmt = NULL;
|
|
208
|
+
VipsImage *header = load_explicit(input_path, &input_fmt);
|
|
209
|
+
long long pixels = 0, max_pixels = 0;
|
|
210
|
+
if (pixels_exceed_limit(header, max_pixels_val, &pixels, &max_pixels)) {
|
|
211
|
+
g_object_unref(header);
|
|
212
|
+
raise_pixels_limit(pixels, max_pixels);
|
|
213
|
+
}
|
|
214
|
+
g_object_unref(header);
|
|
215
|
+
|
|
216
|
+
/* Thumbnail straight from the file so libvips can shrink on load (e.g.
|
|
217
|
+
* libjpeg DCT downscaling) instead of decoding the source at full
|
|
218
|
+
* resolution. vips_thumbnail auto-rotates from the orientation tag by
|
|
219
|
+
* default. ImageMagick loader classes are blocked globally in
|
|
220
|
+
* init_vips_once, so this still cannot reach an ImageMagick delegate. */
|
|
221
|
+
VipsImage *thumb = NULL;
|
|
222
|
+
if (vips_thumbnail(input_path, &thumb, width,
|
|
223
|
+
"height", height,
|
|
224
|
+
"size", VIPS_SIZE_BOTH,
|
|
225
|
+
"crop", VIPS_INTERESTING_CENTRE,
|
|
226
|
+
"fail_on", VIPS_FAIL_ON_ERROR,
|
|
227
|
+
NULL) != 0) {
|
|
228
|
+
raise_vips();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (save_explicit(thumb, StringValueCStr(output_val), out_fmt, quality) != 0) {
|
|
232
|
+
g_object_unref(thumb);
|
|
233
|
+
raise_vips();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
int out_width = thumb->Xsize;
|
|
237
|
+
int out_height = thumb->Ysize;
|
|
238
|
+
double duration_ms = now_ms() - start;
|
|
239
|
+
g_object_unref(thumb);
|
|
240
|
+
|
|
241
|
+
VALUE hash = rb_hash_new();
|
|
242
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("input_format")), rb_str_new_cstr(input_fmt));
|
|
243
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("output_format")), rb_str_new_cstr(out_fmt));
|
|
244
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("width")), INT2NUM(out_width));
|
|
245
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("height")), INT2NUM(out_height));
|
|
246
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("duration_ms")), DBL2NUM(duration_ms));
|
|
247
|
+
return hash;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
static VALUE rb_resize(VALUE self, VALUE input_val, VALUE output_val, VALUE scale_val, VALUE format_val, VALUE quality_val, VALUE max_pixels_val) {
|
|
251
|
+
Check_Type(input_val, T_STRING);
|
|
252
|
+
Check_Type(output_val, T_STRING);
|
|
253
|
+
Check_Type(format_val, T_STRING);
|
|
254
|
+
double scale = NUM2DBL(scale_val);
|
|
255
|
+
int quality = NUM2INT(quality_val);
|
|
256
|
+
validate_scale_or_raise(scale);
|
|
257
|
+
validate_quality_or_raise(quality);
|
|
258
|
+
const char *out_fmt = normalized_format(StringValueCStr(format_val));
|
|
259
|
+
if (!out_fmt || strcmp(out_fmt, "heic") == 0) rb_raise(eUnsupported, "unsupported output format");
|
|
260
|
+
|
|
261
|
+
double start = now_ms();
|
|
262
|
+
const char *input_fmt = NULL;
|
|
263
|
+
VipsImage *in = load_explicit(StringValueCStr(input_val), &input_fmt);
|
|
264
|
+
long long pixels = 0, max_pixels = 0;
|
|
265
|
+
if (pixels_exceed_limit(in, max_pixels_val, &pixels, &max_pixels)) {
|
|
266
|
+
g_object_unref(in);
|
|
267
|
+
raise_pixels_limit(pixels, max_pixels);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
VipsImage *rot = NULL;
|
|
271
|
+
if (vips_autorot(in, &rot, NULL) != 0) {
|
|
272
|
+
g_object_unref(in);
|
|
273
|
+
raise_vips();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
VipsImage *out = NULL;
|
|
277
|
+
if (vips_resize(rot, &out, scale, NULL) != 0) {
|
|
278
|
+
g_object_unref(rot);
|
|
279
|
+
g_object_unref(in);
|
|
280
|
+
raise_vips();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (save_explicit(out, StringValueCStr(output_val), out_fmt, quality) != 0) {
|
|
284
|
+
g_object_unref(out);
|
|
285
|
+
g_object_unref(rot);
|
|
286
|
+
g_object_unref(in);
|
|
287
|
+
raise_vips();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
int out_width = out->Xsize;
|
|
291
|
+
int out_height = out->Ysize;
|
|
292
|
+
double duration_ms = now_ms() - start;
|
|
293
|
+
g_object_unref(out);
|
|
294
|
+
g_object_unref(rot);
|
|
295
|
+
g_object_unref(in);
|
|
296
|
+
|
|
297
|
+
VALUE hash = rb_hash_new();
|
|
298
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("input_format")), rb_str_new_cstr(input_fmt));
|
|
299
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("output_format")), rb_str_new_cstr(out_fmt));
|
|
300
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("width")), INT2NUM(out_width));
|
|
301
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("height")), INT2NUM(out_height));
|
|
302
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("duration_ms")), DBL2NUM(duration_ms));
|
|
303
|
+
return hash;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
static VALUE rb_crop_north(VALUE self, VALUE input_val, VALUE output_val, VALUE width_val, VALUE height_val, VALUE format_val, VALUE quality_val, VALUE max_pixels_val) {
|
|
307
|
+
Check_Type(input_val, T_STRING);
|
|
308
|
+
Check_Type(output_val, T_STRING);
|
|
309
|
+
Check_Type(format_val, T_STRING);
|
|
310
|
+
int width = NUM2INT(width_val);
|
|
311
|
+
int height = NUM2INT(height_val);
|
|
312
|
+
int quality = NUM2INT(quality_val);
|
|
313
|
+
validate_dimensions_or_raise(width, height);
|
|
314
|
+
validate_quality_or_raise(quality);
|
|
315
|
+
const char *out_fmt = normalized_format(StringValueCStr(format_val));
|
|
316
|
+
if (!out_fmt || strcmp(out_fmt, "heic") == 0) rb_raise(eUnsupported, "unsupported output format");
|
|
317
|
+
|
|
318
|
+
double start = now_ms();
|
|
319
|
+
const char *input_fmt = NULL;
|
|
320
|
+
VipsImage *in = load_explicit(StringValueCStr(input_val), &input_fmt);
|
|
321
|
+
long long pixels = 0, max_pixels = 0;
|
|
322
|
+
if (pixels_exceed_limit(in, max_pixels_val, &pixels, &max_pixels)) {
|
|
323
|
+
g_object_unref(in);
|
|
324
|
+
raise_pixels_limit(pixels, max_pixels);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
VipsImage *rot = NULL;
|
|
328
|
+
if (vips_autorot(in, &rot, NULL) != 0) {
|
|
329
|
+
g_object_unref(in);
|
|
330
|
+
raise_vips();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
double sx = (double)width / (double)rot->Xsize;
|
|
334
|
+
double sy = (double)height / (double)rot->Ysize;
|
|
335
|
+
double scale = sx > sy ? sx : sy;
|
|
336
|
+
scale *= 1.0000001;
|
|
337
|
+
|
|
338
|
+
VipsImage *resized = NULL;
|
|
339
|
+
if (vips_resize(rot, &resized, scale, NULL) != 0) {
|
|
340
|
+
g_object_unref(rot);
|
|
341
|
+
g_object_unref(in);
|
|
342
|
+
raise_vips();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
int left = (resized->Xsize - width) / 2;
|
|
346
|
+
if (left < 0) left = 0;
|
|
347
|
+
|
|
348
|
+
VipsImage *crop = NULL;
|
|
349
|
+
if (vips_extract_area(resized, &crop, left, 0, width, height, NULL) != 0) {
|
|
350
|
+
g_object_unref(resized);
|
|
351
|
+
g_object_unref(rot);
|
|
352
|
+
g_object_unref(in);
|
|
353
|
+
raise_vips();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (save_explicit(crop, StringValueCStr(output_val), out_fmt, quality) != 0) {
|
|
357
|
+
g_object_unref(crop);
|
|
358
|
+
g_object_unref(resized);
|
|
359
|
+
g_object_unref(rot);
|
|
360
|
+
g_object_unref(in);
|
|
361
|
+
raise_vips();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
int out_width = crop->Xsize;
|
|
365
|
+
int out_height = crop->Ysize;
|
|
366
|
+
double duration_ms = now_ms() - start;
|
|
367
|
+
g_object_unref(crop);
|
|
368
|
+
g_object_unref(resized);
|
|
369
|
+
g_object_unref(rot);
|
|
370
|
+
g_object_unref(in);
|
|
371
|
+
|
|
372
|
+
VALUE hash = rb_hash_new();
|
|
373
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("input_format")), rb_str_new_cstr(input_fmt));
|
|
374
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("output_format")), rb_str_new_cstr(out_fmt));
|
|
375
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("width")), INT2NUM(out_width));
|
|
376
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("height")), INT2NUM(out_height));
|
|
377
|
+
rb_hash_aset(hash, ID2SYM(rb_intern("duration_ms")), DBL2NUM(duration_ms));
|
|
378
|
+
return hash;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
void Init_safe_image_native(void) {
|
|
382
|
+
mSafeImage = rb_define_module("SafeImage");
|
|
383
|
+
eError = rb_const_get(mSafeImage, rb_intern("Error"));
|
|
384
|
+
eUnsupported = rb_const_get(mSafeImage, rb_intern("UnsupportedFormatError"));
|
|
385
|
+
eInvalid = rb_const_get(mSafeImage, rb_intern("InvalidImageError"));
|
|
386
|
+
eLimit = rb_const_get(mSafeImage, rb_intern("LimitError"));
|
|
387
|
+
mNative = rb_define_module_under(mSafeImage, "Native");
|
|
388
|
+
rb_define_singleton_method(mNative, "probe", rb_probe, 1);
|
|
389
|
+
rb_define_singleton_method(mNative, "thumbnail", rb_thumbnail, 7);
|
|
390
|
+
rb_define_singleton_method(mNative, "resize", rb_resize, 6);
|
|
391
|
+
rb_define_singleton_method(mNative, "crop_north", rb_crop_north, 7);
|
|
392
|
+
}
|
|
Binary file
|