webp-ffi 0.3.1 → 1.0.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.
@@ -42,6 +42,16 @@ extern "C" {
42
42
  int width, height;
43
43
  int crop_x, crop_y, crop_w, crop_h;
44
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
45
55
  } FfiWebpEncodeConfig;
46
56
 
47
57
  typedef struct {
@@ -54,12 +64,44 @@ extern "C" {
54
64
  } FfiWebpDecodeConfig;
55
65
 
56
66
 
57
- void decoder_version(char *version);
58
- void encoder_version(char *version);
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);
59
76
  int webp_get_info(const uint8_t* data, size_t data_size, int* width, int* height);
60
- int webp_encode(const char *in_file, const char *out_file, const FfiWebpEncodeConfig *encode_config);
61
- int webp_decode(const char *in_file, const char *out_file, const FfiWebpDecodeConfig *decode_config);
62
- int test_c(int n);
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);
63
105
 
64
106
  #if defined(__cplusplus) || defined(c_plusplus)
65
107
  } // extern "C"
data/lib/webp/c.rb CHANGED
@@ -39,7 +39,25 @@ module WebP
39
39
  :crop_w, :int,
40
40
  :crop_h, :int,
41
41
  :resize_w, :int,
42
- :resize_h, :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
43
61
  end
44
62
 
45
63
  class FfiWebpDecodeConfig < FFI::Struct
@@ -56,12 +74,28 @@ module WebP
56
74
  end
57
75
 
58
76
  # webp lib functions
59
- attach_function :decoder_version, [:pointer], :void
60
- attach_function :encoder_version, [:pointer], :void
61
- attach_function :webp_get_info, [:pointer, :size_t, :pointer, :pointer], :int
62
- attach_function :webp_encode, [:string, :string, :pointer], :int
63
- attach_function :webp_decode, [:string, :string, :pointer], :int
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
64
95
 
65
- attach_function :test_c, [:int], :int
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
66
100
  end
67
101
  end
data/lib/webp/error.rb CHANGED
@@ -1,21 +1,71 @@
1
1
  module WebP
2
-
2
+
3
3
  ENCODER_ERRORS = [
4
- "Version mismatch",
5
- "Invalid configuration",
6
- "Cannot read input picture file",
7
- "Cannot open output file",
8
- "Cannot crop picture",
4
+ "Version mismatch",
5
+ "Invalid configuration",
6
+ "Cannot read input picture file",
7
+ "Cannot open output file",
8
+ "Cannot crop picture",
9
9
  "Cannot resize picture",
10
- "Cannot encode picture as WebP"]
11
-
10
+ "Cannot encode picture as WebP",
11
+ "Cannot allocate memory"]
12
+
12
13
  DECODER_ERRORS = [
13
- "Version mismatch",
14
- "Invalid webp image",
14
+ "Version mismatch",
15
+ "Invalid webp image",
15
16
  "Invalid output format",
16
- "Decoding failed"]
17
-
18
- class InvalidImageFormatError < StandardError; end
19
- class EncoderError < StandardError; end
20
- class DecoderError < StandardError; end
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
21
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
data/lib/webp/options.rb CHANGED
@@ -1,48 +1,155 @@
1
1
  module WebP
2
2
  class Options
3
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
+
4
29
  def initialize(options)
5
30
  @user_options = options
6
31
  end
7
32
 
8
- def encode_pointer
9
- options_pointer = FFI::MemoryPointer.new :char, C::FfiWebpEncodeConfig.size, false
33
+ def encode_pointer(known = ENCODE_OPTIONS)
34
+ check_unknown_options(known, "encode")
35
+ options_pointer = FFI::MemoryPointer.new :char, C::FfiWebpEncodeConfig.size, true
10
36
  options_struct = C::FfiWebpEncodeConfig.new options_pointer
11
- [:lossless, :near_lossless, :method, :target_size, :target_PSNR, :segments,
12
- :sns_strength, :filter_strength, :filter_sharpness,
13
- :filter_type, :autofilter, :alpha_compression, :alpha_filtering,
14
- :alpha_quality, :pass, :show_compressed, :preprocessing, :partitions,
15
- :partition_limit, :width, :height].each do |key|
16
- options_struct[key] = @user_options[key] ? @user_options[key] : -1
37
+ (ENCODE_INT_OPTIONS + [:target_PSNR]).each do |key|
38
+ value = number_option(key)
39
+ options_struct[key] = value.nil? ? -1 : value
17
40
  end
18
41
  encode_default(options_struct)
19
- [:quality, :crop_x, :crop_y, :crop_w,
20
- :crop_h, :resize_w, :resize_h].each do |key|
21
- options_struct[key] = @user_options[key] if @user_options[key]
42
+ CROP_RESIZE_OPTIONS.each do |key|
43
+ value = number_option(key)
44
+ options_struct[key] = value unless value.nil?
22
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
23
54
  options_pointer
24
55
  end
25
56
 
26
- def decode_pointer
27
- options_pointer = FFI::MemoryPointer.new :char, C::FfiWebpDecodeConfig.size, false
57
+ def decode_pointer(known = DECODE_OPTIONS)
58
+ check_unknown_options(known, "decode")
59
+ options_pointer = FFI::MemoryPointer.new :char, C::FfiWebpDecodeConfig.size, true
28
60
  options_struct = C::FfiWebpDecodeConfig.new options_pointer
29
61
  decode_default(options_struct)
30
- # options
31
- if @user_options[:output_format] && [:png, :pam, :ppm, :pgm, :bmp, :tiff, :yuv, :alpha_plane_only].include?(@user_options[:output_format])
32
- options_struct[:output_format] = C::OutputFileFormat[@user_options[:output_format]]
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)
33
66
  end
34
- [:bypass_filtering, :no_fancy_upsampling, :use_threads].each do |key|
35
- options_struct[key] = 1 if @user_options[key] && true == @user_options[key]
36
- end
37
- [:crop_x, :crop_y, :crop_w,
38
- :crop_h, :resize_w, :resize_h].each do |key|
39
- options_struct[key] = @user_options[key] if @user_options[key]
67
+ CROP_RESIZE_OPTIONS.each do |key|
68
+ value = number_option(key)
69
+ options_struct[key] = value unless value.nil?
40
70
  end
41
71
  options_pointer
42
72
  end
43
73
 
44
74
  private
45
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
+
46
153
  def encode_default(options_struct)
47
154
  options_struct[:quality] = 100
48
155
  similar_default(options_struct)
data/lib/webp/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module WebP
2
- VERSION = "0.3.1"
2
+ VERSION = "1.0.0"
3
3
  end
data/lib/webp/webp.rb CHANGED
@@ -2,60 +2,178 @@ require "webp/options"
2
2
  require "webp/c"
3
3
 
4
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
+
5
10
  class << self
6
-
11
+
7
12
  def decoder_version
8
- pointer = FFI::MemoryPointer.new(:char, 10)
9
- C.decoder_version(pointer)
10
- pointer.null? ? nil : pointer.read_string()
13
+ format_version(C.decoder_version)
11
14
  end
12
-
15
+
13
16
  def encoder_version
14
- pointer = FFI::MemoryPointer.new(:char, 10)
15
- C.encoder_version(pointer)
16
- pointer.null? ? nil : pointer.read_string()
17
+ format_version(C.encoder_version)
17
18
  end
18
-
19
- # get webp image size
19
+
20
+ # Width and height of WebP image data (a String or an IO), or nil for nil.
20
21
  def webp_size(data)
21
22
  return nil if data.nil?
22
- pointers = get_pointers_for_webp_size(data)
23
- if 0 == C.webp_get_info(pointers[:data], pointers[:data_size], pointers[:width], pointers[:height])
24
- [(pointers[:width].null? ? nil : pointers[:width].read_int), (pointers[:height].null? ? nil : pointers[:height].read_int)]
25
- else
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
26
  raise InvalidImageFormatError, "invalid WebP image data"
27
27
  end
28
+ [width.read_int, height.read_int]
28
29
  end
29
-
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
+
30
49
  # encode
31
50
  def encode(input_file, output_file, options = {})
32
- unless 0 == (encode_res = C.webp_encode(input_file, output_file, init_options_object(options).encode_pointer))
33
- raise EncoderError, ENCODER_ERRORS[encode_res - 1]
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)
34
57
  end
35
58
  true
36
59
  end
37
-
60
+
38
61
  # decode
39
62
  def decode(input_file, output_file, options = {})
40
- unless 0 == (decode_res = C.webp_decode(input_file, output_file, init_options_object(options).decode_pointer))
41
- raise DecoderError, DECODER_ERRORS[decode_res - 1]
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)
42
69
  end
43
70
  true
44
71
  end
45
-
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
+
46
117
  private
47
-
48
- def get_pointers_for_webp_size(data)
49
- pointers = Hash.new
50
- pointers[:data_size] = data.respond_to?(:bytesize) ? data.bytesize : data.size
51
- pointers[:width], pointers[:height] = FFI::MemoryPointer.new(:int, 2), FFI::MemoryPointer.new(:int, 2)
52
- pointers[:data] = FFI::MemoryPointer.new(:char, pointers[:data_size]).put_bytes(0, data)
53
- pointers
54
- end
55
-
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
+
56
174
  def init_options_object(options)
57
175
  Options.new options
58
176
  end
59
-
177
+
60
178
  end
61
- end
179
+ end
data/lib/webp_ffi.rb CHANGED
@@ -11,6 +11,6 @@ end
11
11
  require "webp/options"
12
12
  require "webp/c"
13
13
  require "webp/error"
14
- require "webp/libc" # not used right now
14
+ require "webp/info"
15
15
  require "webp/webp"
16
16
  require "webp/version"