webp-ffi 0.4.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ecda2fb2748d3fb12bed751cfca88e986390e60788a10f18d8802d14b563d255
4
- data.tar.gz: 66acf9c6fa79655106507da29b63d2362ca273e49738b56cbc6cadf905fc075d
3
+ metadata.gz: 0a2aa7c7aa8f8f2f0330ae03632f0ca237bbdca9a0e05be3733fff5cdb203b49
4
+ data.tar.gz: c4efe77551a0e5843026155f15417b842b3b163841e9ce31796d88ea82ee6437
5
5
  SHA512:
6
- metadata.gz: 518b57f8c6d8c03f14ffeb2088f49db71152309fe7d2519f88c26a78b0ab34fedcb35d0ad95ce7f0b5c5f530b3b2b566385b36b0d30ba663ca56fe1899b55b1e
7
- data.tar.gz: f716a1436fdef874ba6459f25549777e041e3013f8aa9f62aa4679fbdc27639dcaec2d9e614ab22de04d517b143370c2d04265719a90ee01154c770ba4fefecf
6
+ metadata.gz: 6ce44baf28402d1f1a74a524b1d1b23fc23f2e0201c8991bbdbefb0352dad619b7c93ea3344cf20ce33de8a8841ec5de02bc6ebe713bc03191816c86d1276c42
7
+ data.tar.gz: dca8030d6b4fb1d7d8411e489933fff6836bb4961f6b757104d3e08502d9137eda4e7db021b52de11c6e3bb87bfc475f7fea697fbd641c3ae97d05439b0aa15d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,68 @@
1
+ ## v1.0.0
2
+
3
+ Breaking changes:
4
+
5
+ * Require Ruby 3.2 or newer
6
+ * The source gem requires libwebp 1.0.0 or newer
7
+ * Raise `ArgumentError` for unknown options (they were ignored), with a suggestion for typos
8
+ * Raise `ArgumentError` for an unknown `output_format` instead of silently writing PNG
9
+ * Raise `ArgumentError` for option values that are not numbers
10
+ * `false` now means 0 for integer options, instead of the libwebp default
11
+ * `near_lossless` now works, so it produces lossless (larger) files; it was ignored before
12
+ * Decode raises `WebP::DecoderError` when the output file cannot be written; it reported success before
13
+ * Multi-page TIFF files use the first page instead of the last
14
+ * Remove the unused `WebP::LibC` module and `WebP::C.test_c`
15
+ * The functions in `WebP::C` changed; use the `WebP` methods instead
16
+
17
+ New:
18
+
19
+ * `WebP.encode_data` and `WebP.decode_data` encode and decode binary strings in memory
20
+ * `WebP.info` returns the size, format (lossy, lossless or mixed), alpha, animation and metadata of WebP data
21
+ * `WebP.encode_rgba` and `WebP.decode_rgba` work with raw RGBA pixels
22
+ * Copy ICC, EXIF and XMP metadata from PNG, JPEG and TIFF input with the `metadata` option, byte for byte like `cwebp -metadata`
23
+ * New encode options: `preset`, `level` (lossless preset), `image_hint`, `exact`, `use_sharp_yuv`, `thread_level`, `low_memory` and `emulate_jpeg_size`
24
+ * The in-memory, raw pixel and info methods also accept an IO
25
+ * `WebP::Error` is the base class of every error; encoder and decoder errors carry the error `code` and libwebp's `reason`, which is also in the message
26
+ * RBS type signatures in `sig/webp.rbs`
27
+ * Prebuilt gems for Linux (glibc and musl, x86_64 and aarch64) and macOS 11+ (arm64 and x86_64) with libwebp 1.6.0, libpng, libjpeg-turbo, libtiff and zlib included
28
+ * Release the Global VM Lock during encode and decode, so other Ruby threads keep running
29
+ * Accept `Pathname` and other objects with `#to_path` as file names
30
+ * Accept `true` and `false` for integer options, and the decode `output_format` as a string
31
+ * Include the file path in error messages about input and output files
32
+
33
+ Fixes:
34
+
35
+ * `target_size` and `target_PSNR` had no effect, because libwebp needs more than one pass; like cwebp, 6 passes are used now
36
+ * A failed encode left an empty output file, which the README's Rails task would then keep forever; the output is now written only after encoding succeeds
37
+ * `output_format: :yuv` wrote the same PGM file as `:pgm`; it now writes the raw YUV planes
38
+ * TIFF input with alpha lost color, because libtiff returns premultiplied pixels; they are now converted back
39
+ * Reading a PNG relied on undefined behavior (a signed left shift)
40
+ * Only regular files are read as input; on Linux a folder made the reader ask for a huge allocation
41
+ * JPEG and TIFF input was converted to YUV before lossless or sharp YUV encoding, so lossless output was not exact
42
+ * Check image sizes before allocating memory in the PNG, JPEG and TIFF readers
43
+ * Fix memory leaks when reading broken PNG and JPEG files
44
+ * Fix wrong error message when the decode input file cannot be read
45
+ * Detect write errors when closing output files
46
+ * Stop printing errors and warnings to stderr (libtiff older than 4.5.0 still prints its own)
47
+ * Always initialize every decode option passed to C
48
+
49
+ Build and release:
50
+
51
+ * Find libraries from Homebrew on Apple Silicon and from MacPorts without extra flags
52
+ * `C_FLAGS` and `LD_FLAGS` now take priority over the default library folders
53
+ * Stop the build early with install instructions when libwebp, libpng, libjpeg or libtiff is missing
54
+ * Ship a smaller gem without specs and test images
55
+ * Add rake as a runtime dependency, because gem install uses it to build the extension
56
+ * Release from a `v*` tag through GitHub Actions and RubyGems.org trusted publishing
57
+ * Pin GitHub Actions and Docker images to digests; Dependabot keeps the actions updated
58
+ * CI runs the specs with AddressSanitizer and UndefinedBehaviorSanitizer, and compares the output with libwebp's cwebp
59
+
60
+ ## v0.4.0
61
+
62
+ * Update CI
63
+ * Fix C code deprecations warnings
64
+ * Update test Ruby versions
65
+
1
66
  ## v0.2.7
2
67
 
3
68
  * Fix build for old libwebp versions
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013 Alexey Vasyliev
1
+ Copyright (c) 2013 Oleksii Vasyliev
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -10,6 +10,30 @@ WebP is a new image format that provides lossless and lossy compression for imag
10
10
 
11
11
  ## Installation
12
12
 
13
+ ### Prebuilt gems
14
+
15
+ On these platforms `gem install webp-ffi` (or Bundler) picks a prebuilt gem, so nothing is compiled and no system libraries are needed:
16
+
17
+ * Linux with glibc 2.28 or newer: `x86_64-linux-gnu`, `aarch64-linux-gnu`
18
+ * Linux with musl (Alpine): `x86_64-linux-musl`, `aarch64-linux-musl`
19
+ * macOS 11 or newer: `arm64-darwin` (Apple Silicon), `x86_64-darwin` (Intel)
20
+
21
+ The prebuilt gems include libwebp 1.6.0 together with libpng, libjpeg-turbo, libtiff and zlib. Their versions and licenses are in the gem, next to the library (`ext/webp_ffi/<platform>/versions.txt` and `licenses/`).
22
+
23
+ If your `Gemfile.lock` lists only the `ruby` platform, add yours, for example:
24
+
25
+ bundle lock --add-platform x86_64-linux-gnu
26
+
27
+ To build against the system libraries instead, install the source gem:
28
+
29
+ gem install webp-ffi --platform ruby
30
+
31
+ or in a `Gemfile`:
32
+
33
+ gem "webp-ffi", force_ruby_platform: true
34
+
35
+ The rest of this section applies to the source gem.
36
+
13
37
  ### Requirements
14
38
 
15
39
  First of all you should have install libraries: libpng, libjpeg and libtiff.
@@ -24,13 +48,17 @@ For Fedora, CentOS:
24
48
 
25
49
  For Mac OS:
26
50
 
27
- brew install libjpg libpng libtiff webp
51
+ brew install webp libpng jpeg-turbo libtiff
28
52
 
29
53
  or (for MacPorts):
30
54
 
31
- sudo port install jpeg libpng tiff
55
+ sudo port install webp jpeg libpng tiff
56
+
57
+ The build finds libraries installed by Homebrew (`/opt/homebrew` or `/usr/local`) and MacPorts (`/opt/local`). For any other location, pass the paths with `C_FLAGS` and `LD_FLAGS`:
32
58
 
33
- Next, you should [install libwebp](https://developers.google.com/speed/webp/docs/compiling) (if you didn't install it by `brew` in Mac OS or by `apt-get` in Ubuntu or Debian). Webp library version should be >= 0.3.0. This gem is not support Windows systems.
59
+ C_FLAGS="-I/path/to/include" LD_FLAGS="-L/path/to/lib" gem install webp-ffi
60
+
61
+ Next, you should [install libwebp](https://developers.google.com/speed/webp/docs/compiling) (if you didn't install it by `brew` in Mac OS or by `apt-get` in Ubuntu or Debian). The source gem needs libwebp 1.0.0 or newer. This gem does not support Windows.
34
62
 
35
63
  ### Final part
36
64
 
@@ -48,31 +76,37 @@ Or install it yourself as:
48
76
 
49
77
  ## Usage
50
78
 
51
- ### Encoder end Decoder versions
79
+ ### Encoder and Decoder versions
52
80
 
53
81
  Basic info about libwebp (encoder and decoder versions):
54
82
 
55
- $ irb
56
- 2.0.0p0 :001 > require 'webp_ffi'
57
- => true
58
- 2.0.0p0 :002 > WebP.decoder_version
59
- => "0.3.1"
60
- 2.0.0p0 :003 > WebP.encoder_version
61
- => "0.3.1"
83
+ require 'webp_ffi'
84
+ WebP.decoder_version # => "1.6.0"
85
+ WebP.encoder_version # => "1.6.0"
62
86
 
63
87
 
64
- ### WebP size (width and height)
88
+ ### WebP size and details
65
89
 
66
- Get size (width and height) from webp image:
90
+ Get the size (width and height) of a WebP image:
67
91
 
68
- filename = File.expand_path(File.join(File.dirname(__FILE__), "spec/factories/4.webp"))
69
- WebP.webp_size(File.open(filename, "rb").read)
70
- => [2000, 2353]
92
+ WebP.webp_size(File.binread("photo.webp")) # => [1024, 772]
93
+
94
+ `WebP.info` returns more details as a `WebP::Info`:
95
+
96
+ info = WebP.info(File.binread("photo.webp"))
97
+ info.width # => 1024
98
+ info.height # => 772
99
+ info.format # => :lossy, :lossless or :mixed (animations)
100
+ info.alpha? # => false
101
+ info.animated? # => false
102
+ info.metadata # => [:icc, :exif] (metadata chunks in the file)
103
+
104
+ Both accept a binary String or an IO, and raise `WebP::InvalidImageFormatError` for data that is not WebP.
71
105
 
72
106
 
73
107
  ### Encode WebP image
74
108
 
75
- Encode png, jpg or tiff image to webp:
109
+ Encode png, jpg or tiff image to webp (only the first page of a multi-page tiff is used):
76
110
 
77
111
  filename = File.expand_path(File.join(File.dirname(__FILE__), "spec/factories/4.png"))
78
112
  out_filename = File.expand_path(File.join(File.dirname(__FILE__), "tmp/4.webp"))
@@ -82,16 +116,22 @@ Encode png, jpg or tiff image to webp:
82
116
  Encode png, jpg or tiff image to webp with options:
83
117
 
84
118
  WebP.encode(filename, out_filename, quality: 50, resize_w: 100, resize_h: 200)
85
- WebP.encode(filename, out_filename, quality: 75, crop_x: 0, cropt_y: 0, crop_w: 100, crop_h: 100)
119
+ WebP.encode(filename, out_filename, quality: 75, crop_x: 0, crop_y: 0, crop_w: 100, crop_h: 100)
120
+
86
121
 
122
+ File names can be strings or `Pathname` objects.
123
+
124
+ Integer options also accept `true` and `false` (as 1 and 0). Unknown option names and values that are not numbers raise `ArgumentError`, so typos are easy to spot.
87
125
 
88
126
  Possible encode options:
89
127
 
128
+ * **preset** (symbol) - start from libwebp's settings for a kind of image: `:default`, `:picture`, `:photo`, `:drawing`, `:icon` or `:text`. Other options still apply on top
90
129
  * **lossless** (int) - Lossless encoding (0=lossy(default), 1=lossless)
91
- * **near_lossless** (int) - Use near-lossless image preprocessing (0=maximum preprocessing, 100=no preprocessing(default))
92
- * **quality** (float) - between 0 (smallest file) and 100 (biggest)
130
+ * **level** (int) - lossless preset from 0 (fastest) to 9 (smallest), like `cwebp -z`. Turns on lossless encoding and picks its own quality and method unless you set them
131
+ * **near_lossless** (int) - Use near-lossless image preprocessing (0=maximum preprocessing, 100=no preprocessing(default)). Turns on lossless encoding
132
+ * **quality** (float) - between 0 (smallest file) and 100 (biggest). Default is 100
93
133
  * **method** (int) - quality/speed trade-off (0=fast, 6=slower-better)
94
- * **target\_size** (int) - if non-zero, set the desired target size in bytes. Takes precedence over the 'compression' parameter
134
+ * **target\_size** (int) - if non-zero, set the desired target size in bytes. Takes precedence over the 'compression' parameter. Like cwebp, 6 passes are used unless **pass** is set
95
135
  * **target\_PSNR** (float) - if non-zero, specifies the minimal distortion to try to achieve. Takes precedence over target\_size
96
136
  * **segments** (int) - maximum number of segments to use, in [1..4]
97
137
  * **sns_strength** (int) - Spatial Noise Shaping. 0=off, 100=maximum
@@ -105,12 +145,28 @@ Possible encode options:
105
145
  * **pass** (int) - number of entropy-analysis passes (in [1..10])
106
146
  * **show\_compressed** (int) - if true, export the compressed picture back. In-loop filtering is not applied
107
147
  * **preprocessing** (int) - preprocessing filter (0=none, 1=segment-smooth)
108
- * **partitions** (int) - log2(number of token partitions) in [0..3]. Default is set to 0 for easier progressive decoding
148
+ * **partitions** (int) - log2(number of token partitions) in [0..3]. Default is set to 0 for easier progressive decoding. libwebp only uses it together with **low\_memory**
109
149
  * **partition\_limit** (int) - quality degradation allowed to fit the 512k limit on prediction modes coding (0: no degradation, 100: maximum possible degradation)
150
+ * **image\_hint** (symbol) - kind of image for lossless encoding: `:default`, `:picture`, `:photo` or `:graph`
151
+ * **exact** (bool) - keep the RGB values under transparent areas, which libwebp otherwise changes to compress better
152
+ * **use\_sharp\_yuv** (bool) - sharper (and slower) RGB to YUV conversion for lossy encoding
153
+ * **thread\_level** (bool) - use more than one thread where libwebp can
154
+ * **low\_memory** (bool) - use less memory and more CPU
155
+ * **emulate\_jpeg\_size** (bool) - pick settings that give about the size of a JPEG of the same quality
156
+ * **metadata** - copy metadata from the input: `:all`, `:none` (default) or some of `:icc`, `:exif` and `:xmp` (see below)
110
157
  * **width** (int), **height** (int) - Input size (width x height) for YUV
111
158
  * **crop\_x** (int), **crop\_y** (int), **crop\_w** (int), **crop\_h** (int) - crop picture with the given rectangle
112
159
  * **resize\_w** (int), **resize\_h** (int) - resize picture (after any cropping)
113
160
 
161
+ ### Metadata
162
+
163
+ By default, like `cwebp`, no metadata is copied. The **metadata** option copies the ICC color profile, EXIF and XMP data from the input into the WebP file:
164
+
165
+ WebP.encode("photo.jpg", "photo.webp", metadata: :all)
166
+ WebP.encode_data(File.binread("photo.png"), metadata: [:icc])
167
+
168
+ PNG and JPEG files can carry all three. In PNG files the gem reads the iCCP and eXIf chunks, XMP text and the "raw profile" text chunks that ImageMagick and ExifTool write. Like `cwebp`, only the ICC profile and XMP data are read from TIFF files, and broken metadata fails the encode only when it was requested.
169
+
114
170
  ### Decode WebP image
115
171
 
116
172
  Decode webp image (default format is png):
@@ -131,14 +187,18 @@ Decode webp image to pam, ppm, pgm, bmp, tiff or yuv format of image:
131
187
  WebP.decode(filename, out_filename, output_format: :tiff)
132
188
  WebP.decode(filename, out_filename, output_format: :yuv)
133
189
 
190
+ `:yuv` writes the raw planes (Y, then U and V at half size, then alpha when the image has it), which `WebP.encode_data` reads back with the `width` and `height` options. `:pgm` writes the same planes as a grayscale PGM image.
191
+
192
+ The output file is written only after decoding succeeds, and encoding reads the whole input first, so the input and output can be the same file. When writing fails, a file the gem created is removed again.
193
+
134
194
 
135
195
  Decode webp image with options:
136
196
 
137
- WebP.encode(filename, out_filename, resize_w: 100, resize_h: 200)
138
- WebP.encode(filename, out_filename, crop_x: 0, cropt_y: 0, crop_w: 100, crop_h: 100)
197
+ WebP.decode(filename, out_filename, resize_w: 100, resize_h: 200)
198
+ WebP.decode(filename, out_filename, crop_x: 0, crop_y: 0, crop_w: 100, crop_h: 100)
139
199
 
140
200
 
141
- Possible decode options:
201
+ Possible decode options (unknown options and an unknown `output_format` raise `ArgumentError`):
142
202
 
143
203
  * **bypass\_filtering** (bool) - disable in-loop filtering
144
204
  * **no\_fancy\_upsampling** (bool) - don't use the fancy YUV420 upscaler
@@ -146,6 +206,51 @@ Possible decode options:
146
206
  * **crop\_x** (int), **crop\_y** (int), **crop\_w** (int), **crop\_h** (int) - crop picture with the given rectangle
147
207
  * **resize\_w** (int), **resize\_h** (int) - resize picture (after any cropping)
148
208
 
209
+ ### Encode and decode in memory
210
+
211
+ `WebP.encode_data` and `WebP.decode_data` work with binary strings instead of files, for example for uploads or images stored in S3. They take the same options as `WebP.encode` and `WebP.decode`.
212
+
213
+ webp = WebP.encode_data(File.binread("photo.jpg"), quality: 80)
214
+ WebP.webp_size(webp) # => [1024, 772]
215
+
216
+ png = WebP.decode_data(webp)
217
+ tiff = WebP.decode_data(webp, output_format: :tiff, resize_w: 200, resize_h: 150)
218
+
219
+ `encode_data` reads PNG, JPEG and TIFF data, or raw YUV 4:2:0 data when `width` and `height` are given. Both methods also accept an IO, such as an open file or a `StringIO`, and read it from its current position.
220
+
221
+ ### Raw pixels
222
+
223
+ `WebP.encode_rgba` encodes raw RGBA pixels (4 bytes per pixel, row after row), and `WebP.decode_rgba` returns them, for example to work with ChunkyPNG, libvips or OpenCV:
224
+
225
+ webp = WebP.encode_rgba(pixels, width, height, lossless: true)
226
+ image = WebP.decode_rgba(webp, resize_w: 200, resize_h: 150)
227
+ image.rgba # => binary String of 200 * 150 * 4 bytes
228
+ image.width # => 200
229
+ image.height # => 150
230
+
231
+ They take the same options as `encode_data` and `decode_data`, except the ones for files: **width**, **height**, **metadata** and **output\_format**.
232
+
233
+ ### Errors
234
+
235
+ Every error the gem raises inherits from `WebP::Error`. `WebP::EncoderError` and `WebP::DecoderError` also carry the gem's error `code` and libwebp's `reason`, when it gives one:
236
+
237
+ begin
238
+ WebP.encode("huge.png", "huge.webp")
239
+ rescue WebP::EncoderError => e
240
+ e.message # => "Cannot encode picture as WebP (the picture size is not supported, WebP allows up to 16383 x 16383 pixels)"
241
+ e.reason # => :bad_dimension
242
+ end
243
+
244
+ Invalid arguments, like unknown options, raise `ArgumentError` or `TypeError`.
245
+
246
+ ### Threads
247
+
248
+ `WebP.encode`, `WebP.decode` and the in-memory and raw pixel methods release Ruby's Global VM Lock while libwebp works, so other threads (for example in Puma or Sidekiq) keep running during long conversions.
249
+
250
+ ### Type signatures
251
+
252
+ The gem ships RBS type signatures in `sig/webp.rbs` for Steep and editors with RBS support.
253
+
149
254
  ## Rails assets pipeline integration
150
255
 
151
256
  For integration with Rails 3+ you can use very simple rake task:
@@ -187,6 +292,13 @@ namespace :assets do
187
292
  end
188
293
  ```
189
294
 
295
+ ## Releasing
296
+
297
+ 1. Update `WebP::VERSION` in `lib/webp/version.rb` and the CHANGELOG.
298
+ 2. Commit, then push a tag that matches the version: `git tag v1.0.0 && git push origin v1.0.0`.
299
+
300
+ The Release workflow builds the source gem and every prebuilt gem, tests each one after installing it, and publishes them to RubyGems.org with trusted publishing. To try the builds without publishing, run the workflow by hand from the Actions tab.
301
+
190
302
  ## Contributing
191
303
 
192
304
  1. Fork it
@@ -1,19 +1,3 @@
1
- require 'ffi-compiler/compile_task'
1
+ require_relative 'build_config'
2
2
 
3
- FFI::Compiler::CompileTask.new('webp_ffi') do |c|
4
- c.have_header?('stdio.h', '/usr/local/include')
5
- c.have_func?('puts')
6
- c.have_library?('z')
7
- # webp
8
- c.have_header?('decode.h', '/usr/local/include')
9
- c.have_header?('encode.h', '/usr/local/include')
10
- c.have_func?('WebPDecoderConfig')
11
- c.have_func?('WebPGetInfo')
12
- c.have_library?('webp')
13
- # libs
14
- c.have_library?('png')
15
- c.have_library?('jpeg')
16
- c.have_library?('tiff')
17
- c.ldflags << ENV['LD_FLAGS'] if ENV['LD_FLAGS']
18
- c.cflags << ENV['C_FLAGS'] if ENV['C_FLAGS']
19
- end
3
+ WebPFFI::BuildConfig.define_tasks('webp_ffi')
@@ -0,0 +1,119 @@
1
+ require 'open3'
2
+ require 'rbconfig'
3
+ require 'shellwords'
4
+ require 'tmpdir'
5
+
6
+ module WebPFFI
7
+ # Build settings shared by the gem install Rakefile (ext/webp_ffi/Rakefile)
8
+ # and the development Rakefile in the project root.
9
+ class BuildConfig
10
+ class MissingDependencyError < StandardError; end
11
+
12
+ # Homebrew uses /opt/homebrew on Apple Silicon and /usr/local on Intel Macs.
13
+ # MacPorts uses /opt/local. Libraries anywhere else need C_FLAGS and LD_FLAGS.
14
+ LIBRARY_PREFIXES = %w[/opt/homebrew /usr/local /opt/local].freeze
15
+
16
+ # 'requirement' is an optional preprocessor condition, such as a version check
17
+ Dependency = Struct.new(:name, :header, :library, :code, :requirement, keyword_init: true)
18
+
19
+ DEPENDENCIES = [
20
+ Dependency.new(name: "libwebp 1.0.0 or newer", header: "webp/encode.h", library: "webp", code: "WebPGetEncoderVersion();",
21
+ requirement: "WEBP_ENCODER_ABI_VERSION >= 0x020e"),
22
+ Dependency.new(name: "libpng", header: "png.h", library: "png", code: "png_access_version_number();"),
23
+ Dependency.new(name: "libjpeg", header: "jpeglib.h", library: "jpeg", code: "struct jpeg_error_mgr err; jpeg_std_error(&err);"),
24
+ Dependency.new(name: "libtiff", header: "tiffio.h", library: "tiff", code: "TIFFGetVersion();"),
25
+ Dependency.new(name: "zlib", header: "zlib.h", library: "z", code: "zlibVersion();")
26
+ ].freeze
27
+
28
+ INSTALL_HINT = <<~TEXT.freeze
29
+ Install the development packages first, for example:
30
+ macOS (Homebrew): brew install webp libpng jpeg-turbo libtiff
31
+ macOS (MacPorts): sudo port install webp libpng jpeg libtiff
32
+ Debian, Ubuntu: sudo apt-get install libwebp-dev libpng-dev libjpeg-dev libtiff-dev
33
+ Fedora: sudo dnf install libwebp-devel libpng-devel libjpeg-devel libtiff-devel
34
+ If they are installed somewhere else, point the build at them:
35
+ C_FLAGS="-I/path/to/include" LD_FLAGS="-L/path/to/lib"
36
+ TEXT
37
+
38
+ attr_reader :cflags, :ldflags, :libs
39
+
40
+ def initialize(env: ENV, prefixes: LIBRARY_PREFIXES, dependencies: DEPENDENCIES)
41
+ @env = env
42
+ @dependencies = dependencies
43
+ # user flags go first, so they win over the default prefixes
44
+ @cflags = split(env['C_FLAGS'])
45
+ @ldflags = split(env['LD_FLAGS'])
46
+ prefixes.each do |prefix|
47
+ include_dir = File.join(prefix, 'include')
48
+ lib_dir = File.join(prefix, 'lib')
49
+ @cflags << "-I#{include_dir}" if File.directory?(include_dir)
50
+ @ldflags << "-L#{lib_dir}" if File.directory?(lib_dir)
51
+ end
52
+ @libs = dependencies.map { |dependency| "-l#{dependency.library}" }
53
+ end
54
+
55
+ # Defines the compile task and a check that runs before it. Call it inside
56
+ # a namespace to keep the task names apart.
57
+ def self.define_tasks(library_name, env: ENV)
58
+ require 'rake'
59
+ require 'ffi-compiler/compile_task'
60
+ config = new(env: env)
61
+
62
+ Rake.application.last_description = "Check that libwebp and the image libraries can be found"
63
+ Rake::Task.define_task(:check_dependencies) { config.check_dependencies! }
64
+ # defined before CompileTask, so the check runs before compiling
65
+ Rake::Task.define_task(default: :check_dependencies)
66
+
67
+ FFI::Compiler::CompileTask.new(library_name) { |task| config.apply(task) }
68
+ end
69
+
70
+ def apply(task)
71
+ @cflags.each { |flag| task.cflags << Shellwords.escape(flag) }
72
+ @ldflags.each { |flag| task.ldflags << Shellwords.escape(flag) }
73
+ task.libs.concat(@libs)
74
+ task
75
+ end
76
+
77
+ def missing_dependencies
78
+ @dependencies.reject { |dependency| links?(dependency) }
79
+ end
80
+
81
+ def check_dependencies!
82
+ missing = missing_dependencies
83
+ return true if missing.empty?
84
+
85
+ names = missing.map { |dependency| "#{dependency.name} (#{dependency.header}, -l#{dependency.library})" }
86
+ raise MissingDependencyError, "webp-ffi cannot find: #{names.join(', ')}.\n#{INSTALL_HINT}"
87
+ end
88
+
89
+ private
90
+
91
+ # compiles and links a tiny program the same way the extension is built
92
+ def links?(dependency)
93
+ Dir.mktmpdir("webp-ffi-check") do |dir|
94
+ source = File.join(dir, "check.c")
95
+ requirement = dependency.requirement ? "#if !(#{dependency.requirement})\n#error too old\n#endif\n" : ""
96
+ File.write(source, <<~C)
97
+ #include <stdio.h>
98
+ #include <#{dependency.header}>
99
+ #{requirement}int main(void) { #{dependency.code} return 0; }
100
+ C
101
+ command = compiler + split(@env['CFLAGS']) + @cflags +
102
+ [source, "-o", File.join(dir, "check")] +
103
+ split(@env['LDFLAGS']) + @ldflags + ["-l#{dependency.library}"]
104
+ _output, status = Open3.capture2e(*command)
105
+ status.success?
106
+ end
107
+ rescue SystemCallError
108
+ false
109
+ end
110
+
111
+ def compiler
112
+ split(@env['CC'] || RbConfig::CONFIG['CC'] || 'cc')
113
+ end
114
+
115
+ def split(flags)
116
+ flags ? Shellwords.split(flags) : []
117
+ end
118
+ end
119
+ end
@@ -1,4 +1,5 @@
1
1
  #include "./jpegdec.h"
2
+ #include "./util.h"
2
3
 
3
4
  #include <stdio.h>
4
5
  #include <jpeglib.h>
@@ -13,20 +14,104 @@ struct my_error_mgr {
13
14
 
14
15
  static void my_error_exit(j_common_ptr dinfo) {
15
16
  struct my_error_mgr* myerr = (struct my_error_mgr*) dinfo->err;
16
- (*dinfo->err->output_message) (dinfo);
17
17
  longjmp(myerr->setjmp_buffer, 1);
18
18
  }
19
19
 
20
- int UtilReadJPEG(FILE* in_file, WebPPicture* const pic) {
20
+ // libjpeg prints errors and warnings to stderr by default. A library should
21
+ // stay quiet.
22
+ static void my_output_message(j_common_ptr dinfo) {
23
+ (void)dinfo;
24
+ }
25
+
26
+ // Based on the metadata code in libwebp's imageio/jpegdec.c.
27
+
28
+ // An ICC profile can be split over several APP2 segments:
29
+ // "ICC_PROFILE\0", the 1-based sequence number, the segment count, then data.
30
+ static int StoreICCP(j_decompress_ptr dinfo, MetadataPayload* const iccp) {
31
+ static const char kSignature[] = "ICC_PROFILE"; // 12 bytes with the '\0'
32
+ const size_t signature_length = sizeof(kSignature);
33
+ const size_t header_length = signature_length + 2;
34
+ const JOCTET* segments[256];
35
+ size_t lengths[256];
36
+ int expected_count = 0;
37
+ int found = 0;
38
+ size_t total_size = 0;
39
+ size_t offset = 0;
40
+ jpeg_saved_marker_ptr marker;
41
+ int i;
42
+
43
+ memset(lengths, 0, sizeof(lengths));
44
+ for (marker = dinfo->marker_list; marker != NULL; marker = marker->next) {
45
+ if (marker->marker == JPEG_APP0 + 2 && marker->data_length > header_length &&
46
+ !memcmp(marker->data, kSignature, signature_length)) {
47
+ const int seq = marker->data[signature_length];
48
+ const int count = marker->data[signature_length + 1];
49
+ if (seq == 0 || count == 0 || seq > count) return 0;
50
+ if (expected_count == 0) expected_count = count;
51
+ if (count != expected_count || lengths[seq] != 0) return 0;
52
+ segments[seq] = marker->data + header_length;
53
+ lengths[seq] = marker->data_length - header_length;
54
+ total_size += lengths[seq];
55
+ ++found;
56
+ }
57
+ }
58
+ if (found == 0) return 1; // no profile
59
+ if (found != expected_count) return 0; // missing segments
60
+
61
+ iccp->bytes = (uint8_t*)malloc(total_size);
62
+ if (iccp->bytes == NULL) return 0;
63
+ for (i = 1; i <= expected_count; ++i) { // in sequence order
64
+ memcpy(iccp->bytes + offset, segments[i], lengths[i]);
65
+ offset += lengths[i];
66
+ }
67
+ iccp->size = total_size;
68
+ return 1;
69
+ }
70
+
71
+ // EXIF and XMP live in APP1 segments, after a signature that is not copied.
72
+ static int ExtractMetadataFromJPEG(j_decompress_ptr dinfo,
73
+ Metadata* const metadata) {
74
+ static const char kExifSignature[] = "Exif\0"; // "Exif\0\0", 6 bytes
75
+ static const char kXMPSignature[] = "http://ns.adobe.com/xap/1.0/"; // 29 bytes
76
+ jpeg_saved_marker_ptr marker;
77
+
78
+ if (!StoreICCP(dinfo, &metadata->iccp)) return 0;
79
+ for (marker = dinfo->marker_list; marker != NULL; marker = marker->next) {
80
+ if (marker->marker != JPEG_APP0 + 1) continue;
81
+ if (marker->data_length > sizeof(kExifSignature) &&
82
+ !memcmp(marker->data, kExifSignature, sizeof(kExifSignature))) {
83
+ if (!MetadataCopy(marker->data + sizeof(kExifSignature),
84
+ marker->data_length - sizeof(kExifSignature),
85
+ &metadata->exif)) {
86
+ return 0;
87
+ }
88
+ } else if (marker->data_length > sizeof(kXMPSignature) &&
89
+ !memcmp(marker->data, kXMPSignature, sizeof(kXMPSignature))) {
90
+ if (!MetadataCopy(marker->data + sizeof(kXMPSignature),
91
+ marker->data_length - sizeof(kXMPSignature),
92
+ &metadata->xmp)) {
93
+ return 0;
94
+ }
95
+ }
96
+ }
97
+ return 1;
98
+ }
99
+
100
+ int UtilReadJPEG(FILE* in_file, WebPPicture* const pic,
101
+ Metadata* const metadata) {
21
102
  int ok = 0;
22
103
  int stride, width, height;
23
- uint8_t* rgb = NULL;
104
+ size_t rgb_size;
105
+ // 'rgb' changes after setjmp(), so it must be volatile to keep its value
106
+ // when libjpeg jumps back to the error handler.
107
+ uint8_t* volatile rgb = NULL;
24
108
  struct jpeg_decompress_struct dinfo;
25
109
  struct my_error_mgr jerr;
26
110
  JSAMPROW buffer[1];
27
111
 
28
112
  dinfo.err = jpeg_std_error(&jerr.pub);
29
113
  jerr.pub.error_exit = my_error_exit;
114
+ jerr.pub.output_message = my_output_message;
30
115
 
31
116
  if (setjmp(jerr.setjmp_buffer)) {
32
117
  Error:
@@ -36,7 +121,14 @@ int UtilReadJPEG(FILE* in_file, WebPPicture* const pic) {
36
121
 
37
122
  jpeg_create_decompress(&dinfo);
38
123
  jpeg_stdio_src(&dinfo, in_file);
124
+ if (metadata != NULL) {
125
+ jpeg_save_markers(&dinfo, JPEG_APP0 + 1, 0xffff); // EXIF and XMP
126
+ jpeg_save_markers(&dinfo, JPEG_APP0 + 2, 0xffff); // ICC profile
127
+ }
39
128
  jpeg_read_header(&dinfo, TRUE);
129
+ if (metadata != NULL && !ExtractMetadataFromJPEG(&dinfo, metadata)) {
130
+ goto Error;
131
+ }
40
132
 
41
133
  dinfo.out_color_space = JCS_RGB;
42
134
  dinfo.do_fancy_upsampling = TRUE;
@@ -47,19 +139,22 @@ int UtilReadJPEG(FILE* in_file, WebPPicture* const pic) {
47
139
  goto Error;
48
140
  }
49
141
 
50
- width = dinfo.output_width;
51
- height = dinfo.output_height;
52
- stride = dinfo.output_width * dinfo.output_components * sizeof(*rgb);
142
+ if (!UtilCheckImageSize(dinfo.output_width, dinfo.output_height,
143
+ dinfo.output_components, &stride, &rgb_size)) {
144
+ goto Error;
145
+ }
146
+ width = (int)dinfo.output_width;
147
+ height = (int)dinfo.output_height;
53
148
 
54
- rgb = (uint8_t*)malloc(stride * height);
149
+ rgb = (uint8_t*)malloc(rgb_size);
55
150
  if (rgb == NULL) {
56
- goto End;
151
+ goto Error;
57
152
  }
58
153
  buffer[0] = (JSAMPLE*)rgb;
59
154
 
60
155
  while (dinfo.output_scanline < dinfo.output_height) {
61
156
  if (jpeg_read_scanlines(&dinfo, buffer, 1) != 1) {
62
- goto End;
157
+ goto Error;
63
158
  }
64
159
  buffer[0] += stride;
65
160
  }
@@ -71,7 +166,6 @@ int UtilReadJPEG(FILE* in_file, WebPPicture* const pic) {
71
166
  pic->width = width;
72
167
  pic->height = height;
73
168
  ok = WebPPictureImportRGB(pic, rgb, stride);
74
- if (!ok) goto Error;
75
169
 
76
170
  End:
77
171
  free(rgb);
@@ -2,15 +2,18 @@
2
2
 
3
3
  #include "webp/encode.h"
4
4
  #include "webp/decode.h"
5
+ #include "./metadata.h"
5
6
 
6
7
  #ifdef __cplusplus
7
8
  extern "C" {
8
9
  #endif
9
10
 
10
11
  // Reads a JPEG from 'in_file', returning the decoded output in 'pic'.
11
- // The output is RGB.
12
+ // The output is RGB. When 'metadata' is not NULL, the ICC profile, EXIF and
13
+ // XMP data are copied into it.
12
14
  // Returns true on success.
13
- int UtilReadJPEG(FILE* in_file, struct WebPPicture* const pic);
15
+ int UtilReadJPEG(FILE* in_file, struct WebPPicture* const pic,
16
+ Metadata* const metadata);
14
17
 
15
18
  #ifdef __cplusplus
16
19
  } // extern "C"