safe_image 0.3.0 → 0.4.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 +4 -4
- data/CHANGELOG.md +21 -12
- data/README.md +140 -287
- data/SECURITY.md +22 -11
- data/docs/architecture.md +63 -0
- data/ext/safe_image_vips_helper/extconf.rb +43 -0
- data/ext/safe_image_vips_helper/safe_image_vips_helper.c +1007 -0
- data/lib/safe_image/api/metadata.rb +85 -0
- data/lib/safe_image/api/transform.rb +152 -0
- data/lib/safe_image/backend_label.rb +24 -0
- data/lib/safe_image/formats.rb +96 -0
- data/lib/safe_image/ico.rb +42 -40
- data/lib/safe_image/image_magick_backend.rb +219 -162
- data/lib/safe_image/jpegli_backend.rb +64 -44
- data/lib/safe_image/metadata_operations.rb +155 -0
- data/lib/safe_image/native.rb +96 -290
- data/lib/safe_image/native_helper.rb +281 -0
- data/lib/safe_image/operation_backends/base.rb +83 -0
- data/lib/safe_image/operation_backends/image_magick.rb +123 -0
- data/lib/safe_image/operation_backends/vips.rb +251 -0
- data/lib/safe_image/operation_backends.rb +27 -0
- data/lib/safe_image/operation_set.rb +22 -0
- data/lib/safe_image/optimizer.rb +225 -98
- data/lib/safe_image/path_safety.rb +11 -0
- data/lib/safe_image/processor.rb +122 -85
- data/lib/safe_image/quality_defaults.rb +23 -0
- data/lib/safe_image/remote.rb +248 -144
- data/lib/safe_image/result.rb +71 -23
- data/lib/safe_image/runner.rb +60 -23
- data/lib/safe_image/sandbox.rb +44 -218
- data/lib/safe_image/staged_output.rb +44 -0
- data/lib/safe_image/svg_metadata.rb +74 -69
- data/lib/safe_image/transform_operations.rb +139 -0
- data/lib/safe_image/version.rb +1 -1
- data/lib/safe_image/vips_backend.rb +13 -12
- data/lib/safe_image.rb +62 -306
- metadata +43 -37
- data/lib/safe_image/discourse_compat.rb +0 -441
- data/lib/safe_image/svg_css.rb +0 -314
- data/lib/safe_image/svg_sanitizer.rb +0 -583
- data/lib/safe_image/vips_glue.rb +0 -361
- data/lib/safe_image/zygote.rb +0 -619
data/lib/safe_image/native.rb
CHANGED
|
@@ -1,194 +1,118 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require "tempfile"
|
|
4
4
|
|
|
5
5
|
module SafeImage
|
|
6
|
-
# The libvips
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# all images are released deterministically.
|
|
6
|
+
# The libvips backend boundary. All libvips work is executed by the bundled
|
|
7
|
+
# safe_image_vips_helper process; the Ruby process never dlopens libvips.
|
|
8
|
+
# This module keeps the old Native call shape for the higher-level backend
|
|
9
|
+
# code while doing only argument normalization and response shaping in Ruby.
|
|
11
10
|
module Native
|
|
12
|
-
LOADERS = {
|
|
13
|
-
"jpg" => "jpegload",
|
|
14
|
-
"png" => "pngload",
|
|
15
|
-
"webp" => "webpload",
|
|
16
|
-
"gif" => "gifload",
|
|
17
|
-
"heic" => "heifload",
|
|
18
|
-
"avif" => "heifload",
|
|
19
|
-
"jxl" => "jxlload"
|
|
20
|
-
}.freeze
|
|
21
|
-
|
|
22
11
|
class << self
|
|
23
|
-
def
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
12
|
+
def available? = NativeHelper.available?
|
|
13
|
+
|
|
14
|
+
def probe(path, max_pixels = nil)
|
|
15
|
+
path = String(path)
|
|
16
|
+
input_format!(path)
|
|
17
|
+
info = NativeHelper.probe(path, checked_max_pixels(max_pixels))
|
|
18
|
+
{
|
|
19
|
+
format: info.fetch(:input_format),
|
|
20
|
+
width: info.fetch(:width),
|
|
21
|
+
height: info.fetch(:height),
|
|
22
|
+
duration_ms: info.fetch(:duration_ms)
|
|
23
|
+
}
|
|
34
24
|
end
|
|
35
25
|
|
|
36
26
|
def thumbnail(input, output, width, height, format, quality, max_pixels)
|
|
37
|
-
started = monotime
|
|
38
|
-
input = String(input)
|
|
39
|
-
output = String(output)
|
|
40
27
|
width = Integer(width)
|
|
41
28
|
height = Integer(height)
|
|
42
29
|
quality = Integer(quality)
|
|
43
30
|
raise ArgumentError, "width and height must be positive" if width <= 0 || height <= 0
|
|
44
31
|
validate_quality!(quality)
|
|
45
|
-
out_format = output_format!(format)
|
|
46
32
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
"thumbnail_image",
|
|
59
|
-
{ in: image, width: width, height: height,
|
|
60
|
-
size: "both", crop: "centre", fail_on: "error" }
|
|
61
|
-
)
|
|
62
|
-
)
|
|
63
|
-
save_image(thumb, output, out_format, quality)
|
|
64
|
-
info(input_format, out_format, thumb, started)
|
|
65
|
-
end
|
|
33
|
+
input = String(input)
|
|
34
|
+
input_format!(input)
|
|
35
|
+
NativeHelper.thumbnail(
|
|
36
|
+
input,
|
|
37
|
+
String(output),
|
|
38
|
+
width,
|
|
39
|
+
height,
|
|
40
|
+
output_format!(format),
|
|
41
|
+
quality,
|
|
42
|
+
checked_max_pixels(max_pixels)
|
|
43
|
+
)
|
|
66
44
|
end
|
|
67
45
|
|
|
68
46
|
def resize(input, output, scale, format, quality, max_pixels)
|
|
69
|
-
started = monotime
|
|
70
47
|
scale = Float(scale)
|
|
71
48
|
quality = Integer(quality)
|
|
72
49
|
unless scale.finite? && scale.positive? && scale <= 100.0
|
|
73
50
|
raise ArgumentError, "scale must be finite and in 0..100"
|
|
74
51
|
end
|
|
75
52
|
validate_quality!(quality)
|
|
76
|
-
out_format = output_format!(format)
|
|
77
53
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
54
|
+
input = String(input)
|
|
55
|
+
input_format!(input)
|
|
56
|
+
NativeHelper.resize(
|
|
57
|
+
input,
|
|
58
|
+
String(output),
|
|
59
|
+
scale,
|
|
60
|
+
output_format!(format),
|
|
61
|
+
quality,
|
|
62
|
+
checked_max_pixels(max_pixels)
|
|
63
|
+
)
|
|
86
64
|
end
|
|
87
65
|
|
|
88
66
|
def crop_north(input, output, width, height, format, quality, max_pixels)
|
|
89
|
-
started = monotime
|
|
90
67
|
width = Integer(width)
|
|
91
68
|
height = Integer(height)
|
|
92
69
|
quality = Integer(quality)
|
|
93
70
|
raise ArgumentError, "width and height must be positive" if width <= 0 || height <= 0
|
|
94
71
|
validate_quality!(quality)
|
|
95
|
-
out_format = output_format!(format)
|
|
96
|
-
|
|
97
|
-
VipsGlue.with_images do |track|
|
|
98
|
-
image, input_format = load_image(track, String(input), autorotate: true)
|
|
99
|
-
check_pixels!(image, max_pixels)
|
|
100
|
-
rotated = track.call(VipsGlue.operation("autorot", { in: image }))
|
|
101
72
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
73
|
+
input = String(input)
|
|
74
|
+
input_format!(input)
|
|
75
|
+
NativeHelper.crop_north(
|
|
76
|
+
input,
|
|
77
|
+
String(output),
|
|
78
|
+
width,
|
|
79
|
+
height,
|
|
80
|
+
output_format!(format),
|
|
81
|
+
quality,
|
|
82
|
+
checked_max_pixels(max_pixels)
|
|
83
|
+
)
|
|
111
84
|
end
|
|
112
85
|
|
|
113
86
|
def convert(input, output, format, quality, max_pixels)
|
|
114
|
-
started = monotime
|
|
115
87
|
quality = Integer(quality)
|
|
116
88
|
validate_quality!(quality)
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
image, input_format = load_image(track, String(input), autorotate: true)
|
|
121
|
-
check_pixels!(image, max_pixels)
|
|
122
|
-
rotated = track.call(VipsGlue.operation("autorot", { in: image }))
|
|
123
|
-
|
|
124
|
-
# JPEG has no alpha; flatten onto white to match the ImageMagick
|
|
125
|
-
# convert path (libvips composites onto black otherwise).
|
|
126
|
-
final =
|
|
127
|
-
if out_format == "jpg" && VipsGlue.alpha?(rotated)
|
|
128
|
-
track.call(VipsGlue.operation("flatten", { in: rotated, background: [255.0, 255.0, 255.0] }))
|
|
129
|
-
else
|
|
130
|
-
rotated
|
|
131
|
-
end
|
|
132
|
-
save_image(final, String(output), out_format, quality)
|
|
133
|
-
info(input_format, out_format, final, started)
|
|
134
|
-
end
|
|
89
|
+
input = String(input)
|
|
90
|
+
input_format!(input)
|
|
91
|
+
NativeHelper.convert(input, String(output), output_format!(format), quality, checked_max_pixels(max_pixels))
|
|
135
92
|
end
|
|
136
93
|
|
|
137
|
-
# Alpha-weighted average colour as [r, g, b] integers. Premultiplying
|
|
138
|
-
# keeps parity with ImageMagick's resize-based average; per-band means
|
|
139
|
-
# come from vips_stats (row b+1, column 4 of the stats matrix).
|
|
140
94
|
def dominant_color(path, max_pixels)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
srgb =
|
|
146
|
-
if VipsGlue.colourspace_supported?(image)
|
|
147
|
-
track.call(VipsGlue.operation("colourspace", { in: image, space: "srgb" }))
|
|
148
|
-
else
|
|
149
|
-
image
|
|
150
|
-
end
|
|
151
|
-
has_alpha = VipsGlue.alpha?(srgb)
|
|
152
|
-
work = has_alpha ? track.call(VipsGlue.operation("premultiply", { in: srgb })) : srgb
|
|
153
|
-
|
|
154
|
-
stats = track.call(VipsGlue.operation("stats", { in: work }))
|
|
155
|
-
columns = VipsGlue.width(stats)
|
|
156
|
-
matrix = VipsGlue.image_bytes(stats).unpack("d*")
|
|
157
|
-
mean = ->(band) { matrix[(band + 1) * columns + 4] || 0.0 }
|
|
158
|
-
|
|
159
|
-
bands = VipsGlue.bands(work)
|
|
160
|
-
colour_bands = has_alpha ? bands - 1 : bands
|
|
161
|
-
colour_bands = colour_bands.clamp(1, 3)
|
|
162
|
-
raise InvalidImageError, "image has no colour bands" if colour_bands < 1
|
|
163
|
-
|
|
164
|
-
alpha_mean = has_alpha ? mean.call(bands - 1) : 255.0
|
|
165
|
-
(0...3).map do |band|
|
|
166
|
-
value = mean.call([band, colour_bands - 1].min)
|
|
167
|
-
value = alpha_mean.positive? ? value * 255.0 / alpha_mean : 0.0 if has_alpha
|
|
168
|
-
value.round.clamp(0, 255)
|
|
169
|
-
end
|
|
170
|
-
end
|
|
95
|
+
path = String(path)
|
|
96
|
+
input_format!(path)
|
|
97
|
+
hex = NativeHelper.dominant_color(path, checked_max_pixels(max_pixels))
|
|
98
|
+
hex.scan(/../).map { |component| component.to_i(16) }
|
|
171
99
|
end
|
|
172
100
|
|
|
173
101
|
def pages(path, max_pixels)
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
VipsGlue.pages(image)
|
|
178
|
-
end
|
|
102
|
+
path = String(path)
|
|
103
|
+
input_format!(path)
|
|
104
|
+
NativeHelper.pages(path, checked_max_pixels(max_pixels))
|
|
179
105
|
end
|
|
180
106
|
|
|
181
107
|
def orientation(path, max_pixels)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
value = VipsGlue.orientation(image)
|
|
186
|
-
(1..8).cover?(value) ? value : 1
|
|
187
|
-
end
|
|
108
|
+
path = String(path)
|
|
109
|
+
input_format!(path)
|
|
110
|
+
NativeHelper.orientation(path, checked_max_pixels(max_pixels))
|
|
188
111
|
end
|
|
189
112
|
|
|
190
|
-
# Encodes a raw RGBA buffer (top-down rows) as PNG. Used by the
|
|
191
|
-
#
|
|
113
|
+
# Encodes a raw RGBA buffer (top-down rows) as PNG. Used by the pure-Ruby
|
|
114
|
+
# ICO decoder; the raw bytes are staged to a tempfile and consumed by the
|
|
115
|
+
# helper so libvips remains out of the Ruby process.
|
|
192
116
|
def png_from_rgba(bytes, width, height, output)
|
|
193
117
|
bytes = String(bytes)
|
|
194
118
|
width = Integer(width)
|
|
@@ -197,185 +121,67 @@ module SafeImage
|
|
|
197
121
|
raise LimitError, "rgba buffer dimensions exceed 4096x4096" if width > 4096 || height > 4096
|
|
198
122
|
raise ArgumentError, "rgba buffer must be width*height*4 bytes" if bytes.bytesize != width * height * 4
|
|
199
123
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
124
|
+
Tempfile.create(%w[safe-image-rgba .rgba], binmode: true) do |raw|
|
|
125
|
+
raw.write(bytes)
|
|
126
|
+
raw.close
|
|
127
|
+
NativeHelper.png_from_rgba(raw.path, width, height, String(output))
|
|
204
128
|
end
|
|
205
129
|
true
|
|
206
130
|
end
|
|
207
131
|
|
|
208
|
-
# Renders a letter avatar
|
|
209
|
-
#
|
|
210
|
-
# markup string is escaped by the Ruby caller; font and fontfile come
|
|
211
|
-
# from an allowlist.
|
|
132
|
+
# Renders a letter avatar through the helper. Markup has already been
|
|
133
|
+
# escaped by VipsBackend; font tokens and font files come from its allowlist.
|
|
212
134
|
def letter_avatar(output, size, red, green, blue, markup, font, fontfile)
|
|
213
135
|
size = Integer(size)
|
|
214
|
-
markup = String(markup)
|
|
215
|
-
font = String(font)
|
|
216
|
-
fontfile = String(fontfile)
|
|
217
136
|
channels = [Integer(red), Integer(green), Integer(blue)]
|
|
218
137
|
raise ArgumentError, "size must be 1..4096" unless (1..4096).cover?(size)
|
|
219
138
|
unless channels.all? { |value| (0..255).cover?(value) }
|
|
220
139
|
raise ArgumentError, "background channels must be 0..255"
|
|
221
140
|
end
|
|
222
|
-
unless VipsGlue.type_find?("text")
|
|
223
|
-
raise UnsupportedFormatError, "this libvips build has no text renderer (Pango support missing)"
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
VipsGlue.with_images do |track|
|
|
227
|
-
mask =
|
|
228
|
-
if markup.empty?
|
|
229
|
-
# Blank letter: solid background only.
|
|
230
|
-
track.call(VipsGlue.operation("black", { width: size, height: size }))
|
|
231
|
-
else
|
|
232
|
-
text_inputs = { text: markup, font: font, dpi: 72 }
|
|
233
|
-
text_inputs[:fontfile] = fontfile unless fontfile.empty?
|
|
234
|
-
text = track.call(VipsGlue.operation("text", text_inputs))
|
|
235
141
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
{ input: text, left: (text_w - crop_w) / 2, top: (text_h - crop_h) / 2,
|
|
247
|
-
width: crop_w, height: crop_h }
|
|
248
|
-
)
|
|
249
|
-
)
|
|
250
|
-
text_w = crop_w
|
|
251
|
-
text_h = crop_h
|
|
252
|
-
end
|
|
253
|
-
track.call(
|
|
254
|
-
VipsGlue.operation(
|
|
255
|
-
"embed",
|
|
256
|
-
{ in: text, x: (size - text_w) / 2, y: (size - text_h) / 2, width: size, height: size }
|
|
257
|
-
)
|
|
258
|
-
)
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
# blend = bg + (white - bg) * 0.8 * mask/255, one linear op.
|
|
262
|
-
opacity = 204.0 / 255.0 # FFFFFFCC
|
|
263
|
-
a = channels.map { |value| (255.0 - value) * opacity / 255.0 }
|
|
264
|
-
blended = track.call(VipsGlue.operation("linear", { in: mask, a: a, b: channels.map(&:to_f) }))
|
|
265
|
-
cast = track.call(VipsGlue.operation("cast", { in: blended, format: "uchar" }))
|
|
266
|
-
srgb = track.call(VipsGlue.operation("copy", { in: cast, interpretation: "srgb" }))
|
|
267
|
-
save_image(srgb, String(output), "png", 100)
|
|
268
|
-
end
|
|
142
|
+
NativeHelper.letter_avatar(
|
|
143
|
+
String(output),
|
|
144
|
+
size,
|
|
145
|
+
channels[0],
|
|
146
|
+
channels[1],
|
|
147
|
+
channels[2],
|
|
148
|
+
String(markup),
|
|
149
|
+
String(font),
|
|
150
|
+
String(fontfile)
|
|
151
|
+
)
|
|
269
152
|
true
|
|
270
153
|
end
|
|
271
154
|
|
|
272
155
|
private
|
|
273
156
|
|
|
274
|
-
def
|
|
275
|
-
|
|
276
|
-
|
|
157
|
+
def input_format!(path)
|
|
158
|
+
format = Formats.extension(path)
|
|
159
|
+
raise UnsupportedFormatError, "unsupported input format" unless Formats.native_input?(format)
|
|
277
160
|
|
|
278
|
-
|
|
279
|
-
{
|
|
280
|
-
input_format: input_format,
|
|
281
|
-
output_format: output_format,
|
|
282
|
-
width: VipsGlue.width(image),
|
|
283
|
-
height: VipsGlue.height(image),
|
|
284
|
-
duration_ms: monotime - started
|
|
285
|
-
}
|
|
161
|
+
format
|
|
286
162
|
end
|
|
287
163
|
|
|
288
|
-
def
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
when "gif" then "gif"
|
|
294
|
-
when "heic", "heif" then "heic"
|
|
295
|
-
when "avif" then "avif"
|
|
296
|
-
when "jxl" then "jxl"
|
|
164
|
+
def output_format!(format)
|
|
165
|
+
normalized = Formats.normalize(String(format))
|
|
166
|
+
canonical = Formats.native_canonical(normalized)
|
|
167
|
+
unless Formats.native_output?(canonical) && canonical != "heic"
|
|
168
|
+
raise UnsupportedFormatError, "unsupported output format"
|
|
297
169
|
end
|
|
298
|
-
end
|
|
299
170
|
|
|
300
|
-
|
|
301
|
-
normalized = normalized_format(String(format))
|
|
302
|
-
raise UnsupportedFormatError, "unsupported output format" if normalized.nil? || normalized == "heic"
|
|
303
|
-
normalized
|
|
171
|
+
canonical
|
|
304
172
|
end
|
|
305
173
|
|
|
306
174
|
def validate_quality!(quality)
|
|
307
175
|
raise ArgumentError, "quality must be 1..100" unless (1..100).cover?(quality)
|
|
308
176
|
end
|
|
309
177
|
|
|
310
|
-
def
|
|
311
|
-
|
|
312
|
-
raise UnsupportedFormatError, "unsupported input format" unless format
|
|
178
|
+
def checked_max_pixels(max_pixels)
|
|
179
|
+
return nil if max_pixels.nil?
|
|
313
180
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
# libnsgif loader: first frame only (the n=1 default), matching the
|
|
317
|
-
# [0] semantics of the ImageMagick compatibility backend.
|
|
318
|
-
raise UnsupportedFormatError, "this libvips build has no GIF loader" unless VipsGlue.type_find?("gifload")
|
|
319
|
-
when "jxl"
|
|
320
|
-
raise UnsupportedFormatError, "this libvips build has no JPEG XL loader" unless VipsGlue.type_find?("jxlload")
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
options = { filename: path, access: "sequential", fail_on: "error" }
|
|
324
|
-
image = track.call(VipsGlue.operation(LOADERS.fetch(format), options))
|
|
325
|
-
|
|
326
|
-
# autorot flips/rotates pull rows out of input order, which a
|
|
327
|
-
# sequential source can only serve while the image fits its readahead
|
|
328
|
-
# window (~512px); larger oriented images fail with "out of order
|
|
329
|
-
# read". Reload those with random access — the open itself is
|
|
330
|
-
# header-only, so the caller's pixel cap still runs before any decode.
|
|
331
|
-
if autorotate && VipsGlue.orientation(image) > 1
|
|
332
|
-
image = track.call(VipsGlue.operation(LOADERS.fetch(format), options.merge(access: "random")))
|
|
333
|
-
end
|
|
334
|
-
[image, format]
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
def check_pixels!(image, max_pixels)
|
|
338
|
-
if max_pixels.nil?
|
|
339
|
-
limit = DEFAULT_MAX_PIXELS
|
|
340
|
-
else
|
|
341
|
-
limit = Integer(max_pixels)
|
|
342
|
-
raise ArgumentError, "max_pixels must be positive" if limit <= 0
|
|
343
|
-
end
|
|
344
|
-
width = VipsGlue.width(image)
|
|
345
|
-
height = VipsGlue.height(image)
|
|
346
|
-
raise InvalidImageError, "image dimensions are invalid" if width <= 0 || height <= 0
|
|
347
|
-
pixels = width * height
|
|
348
|
-
raise LimitError, "image has #{pixels} pixels, exceeds #{limit}" if pixels > limit
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
# libvips renamed the strip-metadata save option from "strip" to "keep"
|
|
352
|
-
# (VIPS_FOREIGN_KEEP_NONE = 0) in 8.15; pick the spelling at runtime so
|
|
353
|
-
# one gem build serves distro packages from 8.13 up.
|
|
354
|
-
def strip_args
|
|
355
|
-
@strip_args ||= (VipsGlue.version <=> [8, 15]) >= 0 ? { keep: 0 } : { strip: true }
|
|
356
|
-
end
|
|
181
|
+
max_pixels = Integer(max_pixels)
|
|
182
|
+
raise ArgumentError, "max_pixels must be positive" if max_pixels <= 0
|
|
357
183
|
|
|
358
|
-
|
|
359
|
-
case format
|
|
360
|
-
when "jpg"
|
|
361
|
-
VipsGlue.operation("jpegsave", { in: image, filename: path, Q: quality, interlace: false, **strip_args }, output: nil)
|
|
362
|
-
when "png"
|
|
363
|
-
VipsGlue.operation("pngsave", { in: image, filename: path, compression: 6, **strip_args }, output: nil)
|
|
364
|
-
when "webp"
|
|
365
|
-
VipsGlue.operation("webpsave", { in: image, filename: path, Q: quality, **strip_args }, output: nil)
|
|
366
|
-
when "avif"
|
|
367
|
-
VipsGlue.operation("heifsave", { in: image, filename: path, Q: quality, compression: "av1", **strip_args }, output: nil)
|
|
368
|
-
when "gif"
|
|
369
|
-
# cgif-backed saver; optional at libvips build time. GIF output is
|
|
370
|
-
# palette-quantised and has no quality parameter.
|
|
371
|
-
raise UnsupportedFormatError, "this libvips build cannot save GIF (cgif support missing)" unless VipsGlue.type_find?("gifsave")
|
|
372
|
-
VipsGlue.operation("gifsave", { in: image, filename: path, **strip_args }, output: nil)
|
|
373
|
-
when "jxl"
|
|
374
|
-
raise UnsupportedFormatError, "this libvips build cannot save JPEG XL" unless VipsGlue.type_find?("jxlsave")
|
|
375
|
-
VipsGlue.operation("jxlsave", { in: image, filename: path, Q: quality, **strip_args }, output: nil)
|
|
376
|
-
else
|
|
377
|
-
raise UnsupportedFormatError, "unsupported output format"
|
|
378
|
-
end
|
|
184
|
+
max_pixels
|
|
379
185
|
end
|
|
380
186
|
end
|
|
381
187
|
end
|