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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +61 -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
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SafeImage
|
|
4
|
+
module API
|
|
5
|
+
# Public read-only metadata operations. Methods stay exposed on SafeImage via
|
|
6
|
+
# `extend`; inline behavior lives in operation classes.
|
|
7
|
+
module Metadata
|
|
8
|
+
def probe(path, max_pixels: nil)
|
|
9
|
+
metadata_operations.probe(path, max_pixels: max_pixels)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def type(path, max_pixels: nil)
|
|
13
|
+
metadata_operations.type(path, max_pixels: max_pixels)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def size(path, max_pixels: nil)
|
|
17
|
+
metadata_operations.size(path, max_pixels: max_pixels)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def dimensions(path, max_pixels: nil)
|
|
21
|
+
metadata_operations.dimensions(path, max_pixels: max_pixels)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def info(path, max_pixels: nil, animated: false, orientation: false)
|
|
25
|
+
metadata_operations.info(path, max_pixels: max_pixels, animated: animated, orientation: orientation)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def orientation(path, max_pixels: nil)
|
|
29
|
+
metadata_operations.orientation(path, max_pixels: max_pixels)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def dominant_color(path, max_pixels: nil)
|
|
33
|
+
metadata_operations.dominant_color(path, max_pixels: max_pixels)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def remote_info(url, **kwargs)
|
|
37
|
+
config
|
|
38
|
+
Remote.info(url, **kwargs)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def remote_size(url, **kwargs)
|
|
42
|
+
config
|
|
43
|
+
Remote.size(url, **kwargs)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def remote_dimensions(url, **kwargs)
|
|
47
|
+
remote_size(url, **kwargs)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def remote_type(url, **kwargs)
|
|
51
|
+
config
|
|
52
|
+
Remote.type(url, **kwargs)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def remote_animated?(url, **kwargs)
|
|
56
|
+
config
|
|
57
|
+
Remote.animated?(url, **kwargs)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def remote_dominant_color(url, **kwargs)
|
|
61
|
+
config
|
|
62
|
+
Remote.dominant_color(url, **kwargs)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def fetch_remote(url, **kwargs, &block)
|
|
66
|
+
config
|
|
67
|
+
Remote.fetch(url, **kwargs, &block)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def frame_count(path, max_pixels: nil)
|
|
71
|
+
metadata_operations.frame_count(path, max_pixels: max_pixels)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def animated?(path, max_pixels: nil)
|
|
75
|
+
metadata_operations.animated?(path, max_pixels: max_pixels)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def metadata_operations
|
|
81
|
+
MetadataOperations.new(config: SafeImage.config)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SafeImage
|
|
4
|
+
module API
|
|
5
|
+
# Public image-producing operations. Every method requires explicit output
|
|
6
|
+
# paths; callers own any file replacement outside this API.
|
|
7
|
+
module Transform
|
|
8
|
+
def thumbnail(
|
|
9
|
+
input:,
|
|
10
|
+
output:,
|
|
11
|
+
width:,
|
|
12
|
+
height:,
|
|
13
|
+
format: nil,
|
|
14
|
+
quality: QualityDefaults::JPEG,
|
|
15
|
+
max_pixels: nil,
|
|
16
|
+
optimize: false,
|
|
17
|
+
optimize_mode: :lossless,
|
|
18
|
+
chroma_subsampling: :auto
|
|
19
|
+
)
|
|
20
|
+
transform_operations.thumbnail(
|
|
21
|
+
input: input,
|
|
22
|
+
output: output,
|
|
23
|
+
width: width,
|
|
24
|
+
height: height,
|
|
25
|
+
format: format,
|
|
26
|
+
quality: quality,
|
|
27
|
+
max_pixels: max_pixels,
|
|
28
|
+
optimize: optimize,
|
|
29
|
+
optimize_mode: optimize_mode,
|
|
30
|
+
chroma_subsampling: chroma_subsampling
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def optimize(input:, output:, mode: :lossless, strip_metadata: true, quality: nil, strict: true)
|
|
35
|
+
transform_operations.optimize(
|
|
36
|
+
input: input,
|
|
37
|
+
output: output,
|
|
38
|
+
mode: mode,
|
|
39
|
+
strip_metadata: strip_metadata,
|
|
40
|
+
quality: quality,
|
|
41
|
+
strict: strict
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def resize(
|
|
46
|
+
input:,
|
|
47
|
+
output:,
|
|
48
|
+
width:,
|
|
49
|
+
height:,
|
|
50
|
+
quality: nil,
|
|
51
|
+
optimize: true,
|
|
52
|
+
max_pixels: nil,
|
|
53
|
+
chroma_subsampling: :auto
|
|
54
|
+
)
|
|
55
|
+
transform_operations.resize(
|
|
56
|
+
input: input,
|
|
57
|
+
output: output,
|
|
58
|
+
width: width,
|
|
59
|
+
height: height,
|
|
60
|
+
quality: quality,
|
|
61
|
+
optimize: optimize,
|
|
62
|
+
max_pixels: max_pixels,
|
|
63
|
+
chroma_subsampling: chroma_subsampling
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def crop(
|
|
68
|
+
input:,
|
|
69
|
+
output:,
|
|
70
|
+
width:,
|
|
71
|
+
height:,
|
|
72
|
+
quality: nil,
|
|
73
|
+
optimize: true,
|
|
74
|
+
max_pixels: nil,
|
|
75
|
+
chroma_subsampling: :auto
|
|
76
|
+
)
|
|
77
|
+
transform_operations.crop(
|
|
78
|
+
input: input,
|
|
79
|
+
output: output,
|
|
80
|
+
width: width,
|
|
81
|
+
height: height,
|
|
82
|
+
quality: quality,
|
|
83
|
+
optimize: optimize,
|
|
84
|
+
max_pixels: max_pixels,
|
|
85
|
+
chroma_subsampling: chroma_subsampling
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def downsize(
|
|
90
|
+
input:,
|
|
91
|
+
output:,
|
|
92
|
+
dimensions:,
|
|
93
|
+
optimize: true,
|
|
94
|
+
max_pixels: nil,
|
|
95
|
+
quality: QualityDefaults::JPEG,
|
|
96
|
+
chroma_subsampling: :auto
|
|
97
|
+
)
|
|
98
|
+
transform_operations.downsize(
|
|
99
|
+
input: input,
|
|
100
|
+
output: output,
|
|
101
|
+
dimensions: dimensions,
|
|
102
|
+
optimize: optimize,
|
|
103
|
+
max_pixels: max_pixels,
|
|
104
|
+
quality: quality,
|
|
105
|
+
chroma_subsampling: chroma_subsampling
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def convert(input:, output:, format:, quality: nil, optimize: true, max_pixels: nil, chroma_subsampling: :auto)
|
|
110
|
+
transform_operations.convert(
|
|
111
|
+
input: input,
|
|
112
|
+
output: output,
|
|
113
|
+
format: format,
|
|
114
|
+
quality: quality,
|
|
115
|
+
optimize: optimize,
|
|
116
|
+
max_pixels: max_pixels,
|
|
117
|
+
chroma_subsampling: chroma_subsampling
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def fix_orientation(input:, output:, max_pixels: nil, quality: nil)
|
|
122
|
+
transform_operations.fix_orientation(input: input, output: output, max_pixels: max_pixels, quality: quality)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def convert_favicon_to_png(input:, output:, optimize: true, max_pixels: nil)
|
|
126
|
+
transform_operations.convert_favicon_to_png(
|
|
127
|
+
input: input,
|
|
128
|
+
output: output,
|
|
129
|
+
optimize: optimize,
|
|
130
|
+
max_pixels: max_pixels
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def letter_avatar(output:, size:, background_rgb:, letter:, pointsize: 280, font: "DejaVu-Sans")
|
|
135
|
+
transform_operations.letter_avatar(
|
|
136
|
+
output: output,
|
|
137
|
+
size: size,
|
|
138
|
+
background_rgb: background_rgb,
|
|
139
|
+
letter: letter,
|
|
140
|
+
pointsize: pointsize,
|
|
141
|
+
font: font
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
def transform_operations
|
|
148
|
+
TransformOperations.new(config: SafeImage.config)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SafeImage
|
|
4
|
+
module BackendLabel
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
BASE = {
|
|
8
|
+
vips: "libvips-helper",
|
|
9
|
+
imagemagick: "imagemagick",
|
|
10
|
+
vips_helper: "libvips-helper",
|
|
11
|
+
svg_metadata: "svg-metadata",
|
|
12
|
+
ico_metadata: "ico-metadata",
|
|
13
|
+
ico_vips: "ico-ruby+libvips-helper",
|
|
14
|
+
jpegtran: "jpegtran"
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def build(backend, encoder: nil)
|
|
18
|
+
base = BASE.fetch(backend.to_sym)
|
|
19
|
+
encoder.to_s == "cjpegli" ? "#{base}+cjpegli" : base
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private_constant :BackendLabel
|
|
24
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SafeImage
|
|
4
|
+
# Single source of truth for Ruby-side format normalization and allowlists.
|
|
5
|
+
# Native C helpers keep their own low-level allowlists, but Ruby orchestration
|
|
6
|
+
# should ask this module before building backend-specific commands.
|
|
7
|
+
module Formats
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
NATIVE_INPUTS = %w[jpg png gif webp heic heif avif jxl].freeze
|
|
11
|
+
NATIVE_OUTPUTS = %w[jpg png gif webp avif jxl].freeze
|
|
12
|
+
IMAGEMAGICK_INPUTS = (NATIVE_INPUTS + %w[ico]).freeze
|
|
13
|
+
IMAGEMAGICK_OUTPUTS = (NATIVE_OUTPUTS + %w[ico]).freeze
|
|
14
|
+
OPTIMIZABLE_OUTPUTS = %w[jpg png].freeze
|
|
15
|
+
CJPEGLI_DIRECT_INPUTS = %w[png].freeze
|
|
16
|
+
|
|
17
|
+
IMAGEMAGICK_DECODERS = {
|
|
18
|
+
"jpg" => "jpeg",
|
|
19
|
+
"png" => "png",
|
|
20
|
+
"gif" => "gif",
|
|
21
|
+
"webp" => "webp",
|
|
22
|
+
"heic" => "heic",
|
|
23
|
+
"heif" => "heic",
|
|
24
|
+
"avif" => "heic",
|
|
25
|
+
"ico" => "ico",
|
|
26
|
+
"jxl" => "jxl"
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
IMAGEMAGICK_OUTPUT_CODERS = {
|
|
30
|
+
"jpg" => "jpeg",
|
|
31
|
+
"png" => "png",
|
|
32
|
+
"gif" => "gif",
|
|
33
|
+
"webp" => "webp",
|
|
34
|
+
"avif" => "avif",
|
|
35
|
+
"ico" => "ico",
|
|
36
|
+
"jxl" => "jxl"
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
def normalize(format)
|
|
40
|
+
format = format.to_s.delete_prefix(".").downcase
|
|
41
|
+
format == "jpeg" ? "jpg" : format
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Canonical names used by libvips' explicit loaders/savers. HEIF-family
|
|
45
|
+
# inputs share the HEIC loader; AVIF remains distinct because saves need the
|
|
46
|
+
# AV1 compression option.
|
|
47
|
+
def native_canonical(format)
|
|
48
|
+
normalized = normalize(format)
|
|
49
|
+
normalized == "heif" ? "heic" : normalized
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def extension(path)
|
|
53
|
+
normalize(File.extname(PathSafety.local_path(path)).delete_prefix("."))
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def native_input?(format)
|
|
57
|
+
NATIVE_INPUTS.include?(normalize(format))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def native_output?(format)
|
|
61
|
+
NATIVE_OUTPUTS.include?(normalize(format))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def imagemagick_input?(format)
|
|
65
|
+
IMAGEMAGICK_INPUTS.include?(normalize(format))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def imagemagick_output?(format)
|
|
69
|
+
IMAGEMAGICK_OUTPUTS.include?(normalize(format))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def optimizable_output?(format)
|
|
73
|
+
OPTIMIZABLE_OUTPUTS.include?(normalize(format))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def cjpegli_direct_input?(format)
|
|
77
|
+
CJPEGLI_DIRECT_INPUTS.include?(normalize(format))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def imagemagick_decoder(format)
|
|
81
|
+
normalized = normalize(format)
|
|
82
|
+
IMAGEMAGICK_DECODERS.fetch(normalized) do
|
|
83
|
+
raise UnsupportedFormatError, "unsupported ImageMagick input format: #{normalized.inspect}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def imagemagick_output_coder(format)
|
|
88
|
+
normalized = normalize(format)
|
|
89
|
+
IMAGEMAGICK_OUTPUT_CODERS.fetch(normalized) do
|
|
90
|
+
raise UnsupportedFormatError, "unsupported ImageMagick output format: #{normalized.inspect}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private_constant :Formats
|
|
96
|
+
end
|
data/lib/safe_image/ico.rb
CHANGED
|
@@ -45,7 +45,7 @@ module SafeImage
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
# Extracts the largest icon and writes it as PNG. Returns an info hash in
|
|
48
|
-
# the shape
|
|
48
|
+
# the shape OperationBackends::Base#result_from_info expects.
|
|
49
49
|
def convert_to_png(input, output, max_pixels: nil)
|
|
50
50
|
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
51
51
|
data, entries = parse(input)
|
|
@@ -53,25 +53,27 @@ module SafeImage
|
|
|
53
53
|
output = PathSafety.ensure_safe_output_path!(output).to_s
|
|
54
54
|
|
|
55
55
|
width = height = nil
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
tmp
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
56
|
+
StagedOutput.replace(output, suffix: ".safe-image.png") do |tmp_path|
|
|
57
|
+
if entry.png
|
|
58
|
+
# Enforce the pixel cap from the IHDR dimensions before the payload
|
|
59
|
+
# reaches a decoder.
|
|
60
|
+
validate_pixels!(*entry_dimensions(data, entry), max_pixels)
|
|
61
|
+
payload = data.byteslice(entry.offset, entry.size)
|
|
62
|
+
Tempfile.create(%w[safe-image-ico .png]) do |tmp|
|
|
63
|
+
tmp.binmode
|
|
64
|
+
tmp.write(payload)
|
|
65
|
+
tmp.close
|
|
66
|
+
# Sanitizing no-op resize: validates the PNG bytes, enforces the
|
|
67
|
+
# pixel cap and strips metadata on the way through libvips.
|
|
68
|
+
info = Native.resize(tmp.path, tmp_path.to_s, 1.0, "png", 100, max_pixels)
|
|
69
|
+
width = info.fetch(:width)
|
|
70
|
+
height = info.fetch(:height)
|
|
71
|
+
end
|
|
72
|
+
else
|
|
73
|
+
rgba, width, height = decode_rgba(data, entry)
|
|
74
|
+
validate_pixels!(width, height, max_pixels)
|
|
75
|
+
Native.png_from_rgba(rgba, width, height, tmp_path.to_s)
|
|
70
76
|
end
|
|
71
|
-
else
|
|
72
|
-
rgba, width, height = decode_rgba(data, entry)
|
|
73
|
-
validate_pixels!(width, height, max_pixels)
|
|
74
|
-
Native.png_from_rgba(rgba, width, height, output)
|
|
75
77
|
end
|
|
76
78
|
|
|
77
79
|
{
|
|
@@ -83,14 +85,6 @@ module SafeImage
|
|
|
83
85
|
}
|
|
84
86
|
end
|
|
85
87
|
|
|
86
|
-
def dominant_color(path, max_pixels: nil)
|
|
87
|
-
Tempfile.create(["safe-image-ico", ".png"]) do |tmp|
|
|
88
|
-
tmp.close
|
|
89
|
-
convert_to_png(path, tmp.path, max_pixels: max_pixels)
|
|
90
|
-
VipsBackend.dominant_color(tmp.path, max_pixels: max_pixels)
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
88
|
# -- container parsing ---------------------------------------------------
|
|
95
89
|
|
|
96
90
|
def parse(path)
|
|
@@ -136,7 +130,7 @@ module SafeImage
|
|
|
136
130
|
# PNG payloads carry their real dimensions in the IHDR chunk; the
|
|
137
131
|
# one-byte directory fields saturate at 256.
|
|
138
132
|
def entry_dimensions(data, entry)
|
|
139
|
-
return
|
|
133
|
+
return entry.width, entry.height unless entry.png
|
|
140
134
|
raise InvalidImageError, "png payload is truncated" if entry.size < 24
|
|
141
135
|
data.byteslice(entry.offset + 16, 8).unpack("NN")
|
|
142
136
|
end
|
|
@@ -160,7 +154,7 @@ module SafeImage
|
|
|
160
154
|
payload.unpack("Vl<l<vvVVl<l<V")
|
|
161
155
|
raise InvalidImageError, "unsupported dib header (size #{header_size})" if header_size != 40
|
|
162
156
|
raise InvalidImageError, "unsupported dib compression #{compression}" unless compression == BI_RGB
|
|
163
|
-
raise InvalidImageError, "unsupported dib bit depth #{bpp}"
|
|
157
|
+
raise InvalidImageError, "unsupported dib bit depth #{bpp}" if [1, 4, 8, 24, 32].none? { |bits| bits == bpp }
|
|
164
158
|
|
|
165
159
|
top_down = height2.negative?
|
|
166
160
|
height = height2.abs / 2
|
|
@@ -174,9 +168,12 @@ module SafeImage
|
|
|
174
168
|
palette_count = colors_used.zero? ? (1 << bpp) : colors_used
|
|
175
169
|
raise InvalidImageError, "dib palette is invalid" if palette_count > 1 << bpp
|
|
176
170
|
raise InvalidImageError, "dib palette is truncated" if payload.bytesize < palette_offset + palette_count * 4
|
|
177
|
-
palette =
|
|
178
|
-
|
|
179
|
-
|
|
171
|
+
palette =
|
|
172
|
+
payload
|
|
173
|
+
.byteslice(palette_offset, palette_count * 4)
|
|
174
|
+
.unpack("C*")
|
|
175
|
+
.each_slice(4)
|
|
176
|
+
.map { |b, g, r, _x| [r, g, b] }
|
|
180
177
|
palette_offset += palette_count * 4
|
|
181
178
|
end
|
|
182
179
|
|
|
@@ -184,7 +181,9 @@ module SafeImage
|
|
|
184
181
|
and_stride = ((width + 31) / 32) * 4
|
|
185
182
|
xor_bytes = xor_stride * height
|
|
186
183
|
and_bytes = and_stride * height
|
|
187
|
-
|
|
184
|
+
if payload.bytesize < palette_offset + xor_bytes + and_bytes
|
|
185
|
+
raise InvalidImageError, "dib pixel data is truncated"
|
|
186
|
+
end
|
|
188
187
|
|
|
189
188
|
xor_data = payload.byteslice(palette_offset, xor_bytes)
|
|
190
189
|
and_data = payload.byteslice(palette_offset + xor_bytes, and_bytes)
|
|
@@ -205,9 +204,7 @@ module SafeImage
|
|
|
205
204
|
# rotate-right-by-8.
|
|
206
205
|
def decode_32bpp(xor_data, and_data, width, height, and_stride, top_down)
|
|
207
206
|
stride = width * 4
|
|
208
|
-
unless top_down
|
|
209
|
-
xor_data = (0...height).map { |y| xor_data.byteslice((height - 1 - y) * stride, stride) }.join
|
|
210
|
-
end
|
|
207
|
+
xor_data = (0...height).map { |y| xor_data.byteslice((height - 1 - y) * stride, stride) }.join unless top_down
|
|
211
208
|
pixels = xor_data.unpack("N*")
|
|
212
209
|
|
|
213
210
|
if alpha_all_zero?(xor_data)
|
|
@@ -261,10 +258,15 @@ module SafeImage
|
|
|
261
258
|
|
|
262
259
|
index =
|
|
263
260
|
case bpp
|
|
264
|
-
when 8
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
261
|
+
when 8
|
|
262
|
+
xor_row.getbyte(x)
|
|
263
|
+
when 4
|
|
264
|
+
(
|
|
265
|
+
byte = xor_row.getbyte(x >> 1)
|
|
266
|
+
x.even? ? byte >> 4 : byte & 0x0F
|
|
267
|
+
)
|
|
268
|
+
else
|
|
269
|
+
(xor_row.getbyte(x >> 3) >> (7 - (x & 7))) & 1
|
|
268
270
|
end
|
|
269
271
|
rgba << (masked ? transparent : opaque).fetch(index) do
|
|
270
272
|
raise InvalidImageError, "dib palette index #{index} is out of range"
|