webp-ffi 1.0.0-aarch64-linux-gnu

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.
@@ -0,0 +1,110 @@
1
+ #ifndef _WEBP_FFI_H_
2
+ #define _WEBP_FFI_H_
3
+
4
+ #if defined(__cplusplus) || defined(c_plusplus)
5
+ extern "C" {
6
+ #endif
7
+
8
+ typedef struct {
9
+ int lossless; // Lossless encoding (0=lossy(default), 1=lossless).
10
+ int near_lossless; // use near-lossless image preprocessing (0..100=off)
11
+ float quality; // between 0 (smallest file) and 100 (biggest)
12
+ int method; // quality/speed trade-off (0=fast, 6=slower-better)
13
+
14
+ // Parameters related to lossy compression only:
15
+ int target_size; // if non-zero, set the desired target size in bytes.
16
+ // Takes precedence over the 'compression' parameter.
17
+ float target_PSNR; // if non-zero, specifies the minimal distortion to
18
+ // try to achieve. Takes precedence over target_size.
19
+ int segments; // maximum number of segments to use, in [1..4]
20
+ int sns_strength; // Spatial Noise Shaping. 0=off, 100=maximum.
21
+ int filter_strength; // range: [0 = off .. 100 = strongest]
22
+ int filter_sharpness; // range: [0 = off .. 7 = least sharp]
23
+ int filter_type; // filtering type: 0 = simple, 1 = strong (only used
24
+ // if filter_strength > 0 or autofilter > 0)
25
+ int autofilter; // Auto adjust filter's strength [0 = off, 1 = on]
26
+ int alpha_compression; // Algorithm for encoding the alpha plane (0 = none,
27
+ // 1 = compressed with WebP lossless). Default is 1.
28
+ int alpha_filtering; // Predictive filtering method for alpha plane.
29
+ // 0: none, 1: fast, 2: best. Default if 1.
30
+ int alpha_quality; // Between 0 (smallest size) and 100 (lossless).
31
+ // Default is 100.
32
+ int pass; // number of entropy-analysis passes (in [1..10]).
33
+
34
+ int show_compressed; // if true, export the compressed picture back.
35
+ // In-loop filtering is not applied.
36
+ int preprocessing; // preprocessing filter (0=none, 1=segment-smooth)
37
+ int partitions; // log2(number of token partitions) in [0..3]
38
+ // Default is set to 0 for easier progressive decoding.
39
+ int partition_limit; // quality degradation allowed to fit the 512k limit on
40
+ // prediction modes coding (0: no degradation,
41
+ // 100: maximum possible degradation).
42
+ int width, height;
43
+ int crop_x, crop_y, crop_w, crop_h;
44
+ int resize_w, resize_h;
45
+ // -1 keeps the libwebp default for the options below
46
+ int preset; // WebPPreset, applied before the other options
47
+ int image_hint; // WebPImageHint
48
+ int exact; // keep RGB values under transparent areas
49
+ int use_sharp_yuv; // sharper (and slower) RGB to YUV conversion
50
+ int thread_level; // multi-threaded encoding
51
+ int low_memory; // less memory, more CPU
52
+ int emulate_jpeg_size; // quality comparable to JPEG at the same size
53
+ int lossless_level; // 0..9, WebPConfigLosslessPreset (turns on lossless)
54
+ int metadata; // METADATA_* flags: what to copy from the input
55
+ } FfiWebpEncodeConfig;
56
+
57
+ typedef struct {
58
+ OutputFileFormat output_format;
59
+ int bypass_filtering; // if true, skip the in-loop filtering
60
+ int no_fancy_upsampling; // if true, use faster pointwise upsampler
61
+ int crop_x, crop_y, crop_w, crop_h;
62
+ int resize_w, resize_h;
63
+ int use_threads; // if true, use multi-threaded decoding
64
+ } FfiWebpDecodeConfig;
65
+
66
+
67
+ typedef struct {
68
+ int width, height;
69
+ int has_alpha, has_animation;
70
+ int format; // 0 = mixed or unknown, 1 = lossy, 2 = lossless
71
+ } FfiWebpFeatures;
72
+
73
+ // Versions are packed as 0xMMmmpp (major, minor, patch).
74
+ int decoder_version(void);
75
+ int encoder_version(void);
76
+ int webp_get_info(const uint8_t* data, size_t data_size, int* width, int* height);
77
+ // Returns a VP8StatusCode, 0 when 'features' was filled.
78
+ int webp_get_features(const uint8_t* data, size_t data_size, FfiWebpFeatures* features);
79
+
80
+ // All functions below return 0 on success or an error code (see
81
+ // lib/webp/error.rb). On failure '*detail' holds libwebp's own reason when
82
+ // there is one: a WebPEncodingError when encoding, a VP8StatusCode when
83
+ // decoding, otherwise 0.
84
+ int webp_encode(const char *in_file, const char *out_file,
85
+ const FfiWebpEncodeConfig *encode_config, int* detail);
86
+ int webp_decode(const char *in_file, const char *out_file,
87
+ const FfiWebpDecodeConfig *decode_config, int* detail);
88
+
89
+ // In-memory versions: on success '*output' holds 'output_size' bytes that the
90
+ // caller frees with webp_free_data. On failure '*output' is NULL.
91
+ int webp_encode_data(const uint8_t* data, size_t data_size,
92
+ uint8_t** output, size_t* output_size,
93
+ const FfiWebpEncodeConfig* encode_config, int* detail);
94
+ int webp_decode_data(const uint8_t* data, size_t data_size,
95
+ uint8_t** output, size_t* output_size,
96
+ const FfiWebpDecodeConfig* decode_config, int* detail);
97
+ // Raw pixels: 'rgba' holds width * height * 4 bytes, row after row.
98
+ int webp_encode_rgba(const uint8_t* rgba, int width, int height,
99
+ uint8_t** output, size_t* output_size,
100
+ const FfiWebpEncodeConfig* encode_config, int* detail);
101
+ int webp_decode_rgba(const uint8_t* data, size_t data_size,
102
+ uint8_t** output, size_t* output_size, int* width, int* height,
103
+ const FfiWebpDecodeConfig* decode_config, int* detail);
104
+ void webp_free_data(uint8_t* data);
105
+
106
+ #if defined(__cplusplus) || defined(c_plusplus)
107
+ } // extern "C"
108
+ #endif
109
+
110
+ #endif /* _WEBP_FFI_H_ */
data/lib/webp/c.rb ADDED
@@ -0,0 +1,101 @@
1
+ module WebP
2
+ module C
3
+ # enum
4
+ OutputFileFormat = enum( :png, 0,
5
+ :pam,
6
+ :ppm,
7
+ :pgm,
8
+ :bmp,
9
+ :tiff,
10
+ :yuv,
11
+ :alpha_plane_only )
12
+ # struct
13
+ class FfiWebpEncodeConfig < FFI::Struct
14
+ layout :lossless, :int,
15
+ :near_lossless, :int,
16
+ :quality, :float,
17
+ :method, :int,
18
+ :target_size, :int,
19
+ :target_PSNR, :float,
20
+ :segments, :int,
21
+ :sns_strength, :int,
22
+ :filter_strength, :int,
23
+ :filter_sharpness, :int,
24
+ :filter_type, :int,
25
+ :autofilter, :int,
26
+ :alpha_compression, :int,
27
+ :alpha_filtering, :int,
28
+ :alpha_quality, :int,
29
+ :pass, :int,
30
+ :show_compressed, :int,
31
+ :preprocessing, :int,
32
+ :partitions, :int,
33
+ :partition_limit, :int,
34
+ # image
35
+ :width, :int,
36
+ :height, :int,
37
+ :crop_x, :int,
38
+ :crop_y, :int,
39
+ :crop_w, :int,
40
+ :crop_h, :int,
41
+ :resize_w, :int,
42
+ :resize_h, :int,
43
+ # -1 keeps the libwebp default
44
+ :preset, :int,
45
+ :image_hint, :int,
46
+ :exact, :int,
47
+ :use_sharp_yuv, :int,
48
+ :thread_level, :int,
49
+ :low_memory, :int,
50
+ :emulate_jpeg_size, :int,
51
+ :lossless_level, :int,
52
+ :metadata, :int
53
+ end
54
+
55
+ class FfiWebpFeatures < FFI::Struct
56
+ layout :width, :int,
57
+ :height, :int,
58
+ :has_alpha, :int,
59
+ :has_animation, :int,
60
+ :format, :int
61
+ end
62
+
63
+ class FfiWebpDecodeConfig < FFI::Struct
64
+ layout :output_format, OutputFileFormat,
65
+ :bypass_filtering, :int,
66
+ :no_fancy_upsampling, :int,
67
+ :crop_x, :int,
68
+ :crop_y, :int,
69
+ :crop_w, :int,
70
+ :crop_h, :int,
71
+ :resize_w, :int,
72
+ :resize_h, :int,
73
+ :use_threads, :int
74
+ end
75
+
76
+ # webp lib functions
77
+ # libwebp versions packed as 0xMMmmpp
78
+ attach_function :decoder_version, [], :int
79
+ attach_function :encoder_version, [], :int
80
+ attach_function :webp_get_info, [:buffer_in, :size_t, :pointer, :pointer], :int
81
+ attach_function :webp_get_features, [:buffer_in, :size_t, :pointer], :int
82
+ # blocking: true releases Ruby's Global VM Lock while C works, so other
83
+ # threads keep running. The C code never calls back into Ruby.
84
+ # The last argument receives libwebp's reason when a call fails.
85
+ attach_function :webp_encode, [:string, :string, :pointer, :pointer], :int, blocking: true
86
+ attach_function :webp_decode, [:string, :string, :pointer, :pointer], :int, blocking: true
87
+ # in-memory versions: (data, data size, output pointer, output size, options, reason)
88
+ attach_function :webp_encode_data, [:pointer, :size_t, :pointer, :pointer, :pointer, :pointer], :int, blocking: true
89
+ attach_function :webp_decode_data, [:pointer, :size_t, :pointer, :pointer, :pointer, :pointer], :int, blocking: true
90
+ # raw RGBA pixels: (pixels, width, height, output pointer, output size, options, reason)
91
+ attach_function :webp_encode_rgba, [:pointer, :int, :int, :pointer, :pointer, :pointer, :pointer], :int, blocking: true
92
+ # (data, data size, output pointer, output size, width, height, options, reason)
93
+ attach_function :webp_decode_rgba, [:pointer, :size_t, :pointer, :pointer, :pointer, :pointer, :pointer, :pointer], :int, blocking: true
94
+ attach_function :webp_free_data, [:pointer], :void
95
+
96
+ # internal: image size check shared by the PNG, JPEG and TIFF readers
97
+ attach_function :check_image_size, :UtilCheckImageSize, [:uint64, :uint64, :uint64, :pointer, :pointer], :int
98
+ # internal: version string of the libtiff in use
99
+ attach_function :tiff_version, :UtilTIFFVersion, [], :string
100
+ end
101
+ end
data/lib/webp/error.rb ADDED
@@ -0,0 +1,71 @@
1
+ module WebP
2
+
3
+ ENCODER_ERRORS = [
4
+ "Version mismatch",
5
+ "Invalid configuration",
6
+ "Cannot read input picture file",
7
+ "Cannot open output file",
8
+ "Cannot crop picture",
9
+ "Cannot resize picture",
10
+ "Cannot encode picture as WebP",
11
+ "Cannot allocate memory"]
12
+
13
+ DECODER_ERRORS = [
14
+ "Version mismatch",
15
+ "Invalid webp image",
16
+ "Invalid output format",
17
+ "Decoding failed",
18
+ "Cannot read input file",
19
+ "Cannot write output file",
20
+ "Cannot allocate memory"]
21
+
22
+ # encode_data and encode_rgba have no input file, so the read error says "data"
23
+ ENCODER_DATA_ERRORS = ENCODER_ERRORS.dup.tap { |messages| messages[2] = "Cannot read input picture data" }.freeze
24
+
25
+ # error codes that point at the input or the output file
26
+ ENCODER_FILE_ERRORS = { 3 => :input, 4 => :output }.freeze
27
+ DECODER_FILE_ERRORS = { 2 => :input, 4 => :input, 5 => :input, 6 => :output }.freeze
28
+
29
+ # libwebp's own reasons: WebPEncodingError values when encoding
30
+ ENCODER_REASONS = {
31
+ 1 => [:out_of_memory, "out of memory"],
32
+ 2 => [:bitstream_out_of_memory, "out of memory while writing the bitstream"],
33
+ 3 => [:null_parameter, "a required parameter is missing"],
34
+ 4 => [:invalid_configuration, "the configuration is invalid"],
35
+ 5 => [:bad_dimension, "the picture size is not supported, WebP allows up to 16383 x 16383 pixels"],
36
+ 6 => [:partition0_overflow, "partition 0 is bigger than 512 KiB, try fewer segments or a lower partition_limit"],
37
+ 7 => [:partition_overflow, "a partition is bigger than 16 MiB"],
38
+ 8 => [:bad_write, "cannot write the output"],
39
+ 9 => [:file_too_big, "the file is bigger than 4 GiB"],
40
+ 10 => [:user_abort, "aborted"]
41
+ }.freeze
42
+
43
+ # ...and VP8StatusCode values when decoding
44
+ DECODER_REASONS = {
45
+ 1 => [:out_of_memory, "out of memory"],
46
+ 2 => [:invalid_param, "invalid parameter, for example a crop outside the picture"],
47
+ 3 => [:bitstream_error, "the data is not a valid WebP bitstream"],
48
+ 4 => [:unsupported_feature, "unsupported feature, for example an animated image"],
49
+ 5 => [:suspended, "decoding was suspended"],
50
+ 6 => [:user_abort, "aborted"],
51
+ 7 => [:not_enough_data, "not enough data, the image may be truncated"]
52
+ }.freeze
53
+
54
+ # Base class for every error this gem raises.
55
+ class Error < StandardError
56
+ # the gem's error code from C, or nil
57
+ attr_reader :code
58
+ # libwebp's reason as a Symbol (for example :bad_dimension), or nil
59
+ attr_reader :reason
60
+
61
+ def initialize(message = nil, code: nil, reason: nil)
62
+ super(message)
63
+ @code = code
64
+ @reason = reason
65
+ end
66
+ end
67
+
68
+ class InvalidImageFormatError < Error; end
69
+ class EncoderError < Error; end
70
+ class DecoderError < Error; end
71
+ end
data/lib/webp/info.rb ADDED
@@ -0,0 +1,13 @@
1
+ module WebP
2
+ # Returned by WebP.info.
3
+ Info = Data.define(:width, :height, :format, :alpha, :animated, :metadata) do
4
+ # :lossy, :lossless or :mixed (animations can mix both)
5
+ def lossless? = format == :lossless
6
+ def alpha? = alpha
7
+ def animated? = animated
8
+ end
9
+
10
+ # Returned by WebP.decode_rgba: 'rgba' holds width * height * 4 bytes, row
11
+ # after row, as a binary String.
12
+ RGBAImage = Data.define(:rgba, :width, :height)
13
+ end
@@ -0,0 +1,171 @@
1
+ module WebP
2
+ class Options
3
+
4
+ # integer options; -1 tells C to keep the libwebp default
5
+ ENCODE_INT_OPTIONS = [:lossless, :near_lossless, :method, :target_size, :segments,
6
+ :sns_strength, :filter_strength, :filter_sharpness,
7
+ :filter_type, :autofilter, :alpha_compression, :alpha_filtering,
8
+ :alpha_quality, :pass, :show_compressed, :preprocessing, :partitions,
9
+ :partition_limit, :width, :height,
10
+ :exact, :use_sharp_yuv, :thread_level, :low_memory, :emulate_jpeg_size].freeze
11
+ ENCODE_FLOAT_OPTIONS = [:quality, :target_PSNR].freeze
12
+ CROP_RESIZE_OPTIONS = [:crop_x, :crop_y, :crop_w, :crop_h, :resize_w, :resize_h].freeze
13
+ # options that take a name or a list of names
14
+ ENCODE_NAMED_OPTIONS = [:preset, :image_hint, :level, :metadata].freeze
15
+ ENCODE_OPTIONS = (ENCODE_INT_OPTIONS + ENCODE_FLOAT_OPTIONS + CROP_RESIZE_OPTIONS + ENCODE_NAMED_OPTIONS).freeze
16
+ # raw pixels come with their own size and have no metadata
17
+ ENCODE_RGBA_OPTIONS = (ENCODE_OPTIONS - [:width, :height, :metadata]).freeze
18
+
19
+ DECODE_FLAGS = [:bypass_filtering, :no_fancy_upsampling, :use_threads].freeze
20
+ DECODE_OPTIONS = ([:output_format] + DECODE_FLAGS + CROP_RESIZE_OPTIONS).freeze
21
+ DECODE_RGBA_OPTIONS = (DECODE_OPTIONS - [:output_format]).freeze
22
+ OUTPUT_FORMATS = [:png, :pam, :ppm, :pgm, :bmp, :tiff, :yuv, :alpha_plane_only].freeze
23
+
24
+ # libwebp's WebPPreset, WebPImageHint and the METADATA_* flags in metadata.h
25
+ PRESETS = { default: 0, picture: 1, photo: 2, drawing: 3, icon: 4, text: 5 }.freeze
26
+ IMAGE_HINTS = { default: 0, picture: 1, photo: 2, graph: 3 }.freeze
27
+ METADATA = { exif: 1, icc: 2, xmp: 4 }.freeze
28
+
29
+ def initialize(options)
30
+ @user_options = options
31
+ end
32
+
33
+ def encode_pointer(known = ENCODE_OPTIONS)
34
+ check_unknown_options(known, "encode")
35
+ options_pointer = FFI::MemoryPointer.new :char, C::FfiWebpEncodeConfig.size, true
36
+ options_struct = C::FfiWebpEncodeConfig.new options_pointer
37
+ (ENCODE_INT_OPTIONS + [:target_PSNR]).each do |key|
38
+ value = number_option(key)
39
+ options_struct[key] = value.nil? ? -1 : value
40
+ end
41
+ encode_default(options_struct)
42
+ CROP_RESIZE_OPTIONS.each do |key|
43
+ value = number_option(key)
44
+ options_struct[key] = value unless value.nil?
45
+ end
46
+ level = level_option
47
+ quality = number_option(:quality)
48
+ # a lossless level picks its own quality, so only override it when asked
49
+ options_struct[:quality] = quality.nil? ? (level >= 0 ? -1 : 100) : quality
50
+ options_struct[:lossless_level] = level
51
+ options_struct[:preset] = named_option(:preset, PRESETS)
52
+ options_struct[:image_hint] = named_option(:image_hint, IMAGE_HINTS)
53
+ options_struct[:metadata] = metadata_option
54
+ options_pointer
55
+ end
56
+
57
+ def decode_pointer(known = DECODE_OPTIONS)
58
+ check_unknown_options(known, "decode")
59
+ options_pointer = FFI::MemoryPointer.new :char, C::FfiWebpDecodeConfig.size, true
60
+ options_struct = C::FfiWebpDecodeConfig.new options_pointer
61
+ decode_default(options_struct)
62
+ options_struct[:output_format] = C::OutputFileFormat[output_format] if @user_options[:output_format]
63
+ # set every flag explicitly, so C never reads a field we did not write
64
+ DECODE_FLAGS.each do |key|
65
+ options_struct[key] = flag_option(key)
66
+ end
67
+ CROP_RESIZE_OPTIONS.each do |key|
68
+ value = number_option(key)
69
+ options_struct[key] = value unless value.nil?
70
+ end
71
+ options_pointer
72
+ end
73
+
74
+ private
75
+
76
+ # A typo like :cropt_y used to be ignored silently, so unknown options raise.
77
+ def check_unknown_options(known, operation)
78
+ unknown = @user_options.keys - known
79
+ return if unknown.empty?
80
+ details = unknown.map { |key| "#{key.inspect}#{suggestion(key, known)}" }
81
+ raise ArgumentError, "webp-ffi: unknown #{operation} option#{'s' if unknown.size > 1} #{details.join(', ')}"
82
+ end
83
+
84
+ def suggestion(key, known)
85
+ return "" unless defined?(DidYouMean::SpellChecker)
86
+ match = DidYouMean::SpellChecker.new(dictionary: known).correct(key).first
87
+ match ? " (did you mean #{match.inspect}?)" : ""
88
+ end
89
+
90
+ # numbers pass through, true and false become 1 and 0, nil keeps the default
91
+ def number_option(key)
92
+ value = @user_options[key]
93
+ case value
94
+ when nil then nil
95
+ when true then 1
96
+ when false then 0
97
+ when Numeric then value
98
+ else
99
+ raise ArgumentError, "webp-ffi: option #{key.inspect} must be a number or true/false, got #{value.inspect}"
100
+ end
101
+ end
102
+
103
+ def flag_option(key)
104
+ value = @user_options[key]
105
+ case value
106
+ when nil, false, 0 then 0
107
+ when true, 1 then 1
108
+ else
109
+ raise ArgumentError, "webp-ffi: option #{key.inspect} must be true or false, got #{value.inspect}"
110
+ end
111
+ end
112
+
113
+ def output_format
114
+ value = @user_options[:output_format]
115
+ format = value.respond_to?(:to_sym) ? value.to_sym : value
116
+ return format if OUTPUT_FORMATS.include?(format)
117
+ raise ArgumentError, "webp-ffi: unknown output_format #{value.inspect}, use one of #{OUTPUT_FORMATS.map(&:inspect).join(', ')}"
118
+ end
119
+
120
+ # a Symbol or String from 'names', or -1 for the default
121
+ def named_option(key, names)
122
+ value = @user_options[key]
123
+ return -1 if value.nil?
124
+ name = value.respond_to?(:to_sym) ? value.to_sym : value
125
+ names.fetch(name) do
126
+ raise ArgumentError, "webp-ffi: unknown #{key} #{value.inspect}, use one of #{names.keys.map(&:inspect).join(', ')}"
127
+ end
128
+ end
129
+
130
+ # lossless compression effort from 0 (fast) to 9 (small), or -1
131
+ def level_option
132
+ value = @user_options[:level]
133
+ return -1 if value.nil?
134
+ return value if value.is_a?(Integer) && value.between?(0, 9)
135
+ raise ArgumentError, "webp-ffi: option :level must be an integer from 0 to 9, got #{value.inspect}"
136
+ end
137
+
138
+ # :all, :none, or one or more of :exif, :icc and :xmp
139
+ def metadata_option
140
+ value = @user_options[:metadata]
141
+ case value
142
+ when nil, false, :none, "none" then 0
143
+ when true, :all, "all" then METADATA.values.sum
144
+ else
145
+ Array(value).sum do |name|
146
+ METADATA.fetch(name.respond_to?(:to_sym) ? name.to_sym : name) do
147
+ raise ArgumentError, "webp-ffi: unknown metadata #{name.inspect}, use :all, :none or some of :exif, :icc, :xmp"
148
+ end
149
+ end
150
+ end
151
+ end
152
+
153
+ def encode_default(options_struct)
154
+ options_struct[:quality] = 100
155
+ similar_default(options_struct)
156
+ end
157
+
158
+ def decode_default(options_struct)
159
+ # default format is png
160
+ options_struct[:output_format] = C::OutputFileFormat[:png]
161
+ similar_default(options_struct)
162
+ end
163
+
164
+ def similar_default(options_struct)
165
+ options_struct[:crop_x] = options_struct[:crop_y] = 0
166
+ options_struct[:crop_w] = options_struct[:crop_h] = 0
167
+ options_struct[:resize_w] = options_struct[:resize_h] = 0
168
+ end
169
+
170
+ end
171
+ end
@@ -0,0 +1,3 @@
1
+ module WebP
2
+ VERSION = "1.0.0"
3
+ end
data/lib/webp/webp.rb ADDED
@@ -0,0 +1,179 @@
1
+ require "webp/options"
2
+ require "webp/c"
3
+
4
+ module WebP
5
+ # WebPBitstreamFeatures format values
6
+ FORMATS = { 0 => :mixed, 1 => :lossy, 2 => :lossless }.freeze
7
+ # flags in the VP8X header of extended WebP files
8
+ METADATA_FLAGS = { icc: 0x20, exif: 0x08, xmp: 0x04 }.freeze
9
+
10
+ class << self
11
+
12
+ def decoder_version
13
+ format_version(C.decoder_version)
14
+ end
15
+
16
+ def encoder_version
17
+ format_version(C.encoder_version)
18
+ end
19
+
20
+ # Width and height of WebP image data (a String or an IO), or nil for nil.
21
+ def webp_size(data)
22
+ return nil if data.nil?
23
+ data = data_string(data)
24
+ width, height = FFI::MemoryPointer.new(:int), FFI::MemoryPointer.new(:int)
25
+ unless 0 == C.webp_get_info(data, data.bytesize, width, height)
26
+ raise InvalidImageFormatError, "invalid WebP image data"
27
+ end
28
+ [width.read_int, height.read_int]
29
+ end
30
+
31
+ # Details about WebP image data (a String or an IO) as a WebP::Info, or nil
32
+ # for nil: size, format (:lossy, :lossless or :mixed), alpha, animation and
33
+ # the metadata it carries (:icc, :exif, :xmp).
34
+ def info(data)
35
+ return nil if data.nil?
36
+ data = data_string(data)
37
+ features = C::FfiWebpFeatures.new
38
+ status = C.webp_get_features(data, data.bytesize, features)
39
+ unless status.zero?
40
+ reason, text = DECODER_REASONS[status]
41
+ raise InvalidImageFormatError.new(["invalid WebP image data", text].compact.join(": "), reason: reason)
42
+ end
43
+ Info.new(width: features[:width], height: features[:height],
44
+ format: FORMATS.fetch(features[:format], :mixed),
45
+ alpha: features[:has_alpha] == 1, animated: features[:has_animation] == 1,
46
+ metadata: metadata_in(data))
47
+ end
48
+
49
+ # encode
50
+ def encode(input_file, output_file, options = {})
51
+ input_file, output_file = file_path(input_file), file_path(output_file)
52
+ options_pointer = init_options_object(options).encode_pointer
53
+ detail = FFI::MemoryPointer.new(:int)
54
+ code = C.webp_encode(input_file, output_file, options_pointer, detail)
55
+ unless code.zero?
56
+ raise build_error(EncoderError, ENCODER_ERRORS, code, detail.read_int, ENCODER_FILE_ERRORS, input_file, output_file)
57
+ end
58
+ true
59
+ end
60
+
61
+ # decode
62
+ def decode(input_file, output_file, options = {})
63
+ input_file, output_file = file_path(input_file), file_path(output_file)
64
+ options_pointer = init_options_object(options).decode_pointer
65
+ detail = FFI::MemoryPointer.new(:int)
66
+ code = C.webp_decode(input_file, output_file, options_pointer, detail)
67
+ unless code.zero?
68
+ raise build_error(DecoderError, DECODER_ERRORS, code, detail.read_int, DECODER_FILE_ERRORS, input_file, output_file)
69
+ end
70
+ true
71
+ end
72
+
73
+ # Encodes PNG, JPEG or TIFF image data (or raw YUV data, with the :width and
74
+ # :height options) to WebP. 'data' is a String or an IO. Returns the WebP
75
+ # image as a binary String.
76
+ def encode_data(data, options = {})
77
+ options_pointer = init_options_object(options).encode_pointer
78
+ with_output(EncoderError, ENCODER_DATA_ERRORS, data) do |input, size, output, output_size, detail|
79
+ C.webp_encode_data(input, size, output, output_size, options_pointer, detail)
80
+ end
81
+ end
82
+
83
+ # Decodes WebP image data (a String or an IO). Returns the image in the
84
+ # :output_format (PNG by default) as a binary String.
85
+ def decode_data(data, options = {})
86
+ options_pointer = init_options_object(options).decode_pointer
87
+ with_output(DecoderError, DECODER_ERRORS, data) do |input, size, output, output_size, detail|
88
+ C.webp_decode_data(input, size, output, output_size, options_pointer, detail)
89
+ end
90
+ end
91
+
92
+ # Encodes raw RGBA pixels: 'pixels' holds width * height * 4 bytes, row
93
+ # after row. Returns the WebP image as a binary String.
94
+ def encode_rgba(pixels, width, height, options = {})
95
+ width, height = Integer(width), Integer(height)
96
+ pixels = data_string(pixels)
97
+ unless width.positive? && height.positive? && pixels.bytesize == width * height * 4
98
+ raise ArgumentError, "webp-ffi: expected #{width} x #{height} x 4 bytes of RGBA pixels, got #{pixels.bytesize}"
99
+ end
100
+ options_pointer = init_options_object(options).encode_pointer(Options::ENCODE_RGBA_OPTIONS)
101
+ with_output(EncoderError, ENCODER_DATA_ERRORS, pixels) do |input, _size, output, output_size, detail|
102
+ C.webp_encode_rgba(input, width, height, output, output_size, options_pointer, detail)
103
+ end
104
+ end
105
+
106
+ # Decodes WebP image data (a String or an IO) to raw RGBA pixels. Returns a
107
+ # WebP::RGBAImage with the pixels, width and height.
108
+ def decode_rgba(data, options = {})
109
+ options_pointer = init_options_object(options).decode_pointer(Options::DECODE_RGBA_OPTIONS)
110
+ width, height = FFI::MemoryPointer.new(:int), FFI::MemoryPointer.new(:int)
111
+ rgba = with_output(DecoderError, DECODER_ERRORS, data) do |input, size, output, output_size, detail|
112
+ C.webp_decode_rgba(input, size, output, output_size, width, height, options_pointer, detail)
113
+ end
114
+ RGBAImage.new(rgba: rgba, width: width.read_int, height: height.read_int)
115
+ end
116
+
117
+ private
118
+
119
+ def format_version(version)
120
+ [version >> 16, version >> 8, version].map { |part| part & 0xff }.join(".")
121
+ end
122
+
123
+ # Accepts a String, a Pathname or anything with #to_path. Returns a new
124
+ # string, because C reads it while other Ruby threads keep running.
125
+ def file_path(file)
126
+ String.new(File.path(file))
127
+ end
128
+
129
+ # Accepts a String (or anything with #to_str) or an IO to read from.
130
+ def data_string(data)
131
+ data = data.read if data.respond_to?(:read) && !data.is_a?(String)
132
+ String.try_convert(data) or raise TypeError, "webp-ffi: expected image data as a String or an IO, got #{data.class}"
133
+ end
134
+
135
+ def metadata_in(data)
136
+ return [] unless data.bytesize >= 21 && data.byteslice(12, 4) == "VP8X"
137
+ flags = data.getbyte(20)
138
+ METADATA_FLAGS.select { |_name, flag| flags & flag != 0 }.keys
139
+ end
140
+
141
+ # Copies the input to native memory (C reads it while other Ruby threads
142
+ # keep running), yields the arguments for the C call, and returns the output
143
+ # bytes. Raises error_class when C reports an error.
144
+ def with_output(error_class, messages, data)
145
+ data = data_string(data)
146
+ input = FFI::MemoryPointer.new(:uint8, [data.bytesize, 1].max)
147
+ input.put_bytes(0, data)
148
+ output = FFI::MemoryPointer.new(:pointer)
149
+ output_size = FFI::MemoryPointer.new(:size_t)
150
+ detail = FFI::MemoryPointer.new(:int)
151
+ code = yield(input, data.bytesize, output, output_size, detail)
152
+ raise build_error(error_class, messages, code, detail.read_int) unless code.zero?
153
+
154
+ buffer = output.read_pointer
155
+ begin
156
+ buffer.read_bytes(output_size.read(:size_t))
157
+ ensure
158
+ C.webp_free_data(buffer)
159
+ end
160
+ end
161
+
162
+ def build_error(error_class, messages, code, detail, file_errors = {}, input_file = nil, output_file = nil)
163
+ message = code.between?(1, messages.size) ? messages[code - 1] : "Unknown error (code #{code})"
164
+ case file_errors[code]
165
+ when :input then message = "#{message}: #{input_file}"
166
+ when :output then message = "#{message}: #{output_file}"
167
+ end
168
+ reasons = error_class == EncoderError ? ENCODER_REASONS : DECODER_REASONS
169
+ reason, text = reasons[detail]
170
+ message = "#{message} (#{text})" if text
171
+ error_class.new(message, code: code, reason: reason)
172
+ end
173
+
174
+ def init_options_object(options)
175
+ Options.new options
176
+ end
177
+
178
+ end
179
+ end
data/lib/webp-ffi.rb ADDED
@@ -0,0 +1 @@
1
+ require "webp_ffi"
data/lib/webp_ffi.rb ADDED
@@ -0,0 +1,16 @@
1
+ require 'ffi'
2
+ require 'ffi-compiler/loader'
3
+
4
+ module WebP
5
+ module C
6
+ extend FFI::Library
7
+ ffi_lib FFI::Compiler::Loader.find('webp_ffi')
8
+ end
9
+ end
10
+
11
+ require "webp/options"
12
+ require "webp/c"
13
+ require "webp/error"
14
+ require "webp/info"
15
+ require "webp/webp"
16
+ require "webp/version"