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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +65 -0
- data/LICENSE.txt +1 -1
- data/README.md +138 -27
- data/ext/webp_ffi/Rakefile +2 -18
- data/ext/webp_ffi/build_config.rb +119 -0
- data/ext/webp_ffi/jpegdec.c +104 -10
- data/ext/webp_ffi/jpegdec.h +5 -2
- data/ext/webp_ffi/metadata.c +138 -0
- data/ext/webp_ffi/metadata.h +49 -0
- data/ext/webp_ffi/pngdec.c +197 -20
- data/ext/webp_ffi/pngdec.h +7 -2
- data/ext/webp_ffi/tiffdec.c +187 -23
- data/ext/webp_ffi/tiffdec.h +12 -4
- data/ext/webp_ffi/util.c +82 -35
- data/ext/webp_ffi/util.h +28 -3
- data/ext/webp_ffi/webp_ffi.c +484 -165
- data/ext/webp_ffi/webp_ffi.h +47 -5
- data/lib/webp/c.rb +41 -7
- data/lib/webp/error.rb +65 -15
- data/lib/webp/info.rb +13 -0
- data/lib/webp/options.rb +129 -22
- data/lib/webp/version.rb +1 -1
- data/lib/webp/webp.rb +150 -32
- data/lib/webp_ffi.rb +1 -1
- data/sig/webp.rbs +74 -0
- metadata +31 -48
- data/.gitignore +0 -22
- data/.ruby-version +0 -1
- data/.travis.yml +0 -38
- data/Gemfile +0 -4
- data/Rakefile +0 -38
- data/lib/webp/libc.rb +0 -20
- data/spec/factories/1.png +0 -0
- data/spec/factories/1.webp +0 -0
- data/spec/factories/2.png +0 -0
- data/spec/factories/2.webp +0 -0
- data/spec/factories/3.jpg +0 -0
- data/spec/factories/3.webp +0 -0
- data/spec/factories/4.jpg +0 -0
- data/spec/factories/4.webp +0 -0
- data/spec/factories/5.tif +0 -0
- data/spec/factories/5.webp +0 -0
- data/spec/spec_helper.rb +0 -9
- data/spec/travis_build.sh +0 -6
- data/spec/webp_ffi_spec.rb +0 -191
- data/webp-ffi.gemspec +0 -28
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0a2aa7c7aa8f8f2f0330ae03632f0ca237bbdca9a0e05be3733fff5cdb203b49
|
|
4
|
+
data.tar.gz: c4efe77551a0e5843026155f15417b842b3b163841e9ce31796d88ea82ee6437
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
data/README.md
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# Webp-ffi
|
|
2
2
|
|
|
3
|
-
[](https://codeclimate.com/github/le0pard/webp-ffi)
|
|
3
|
+
[](https://github.com/le0pard/webp-ffi/actions/workflows/build.yml)
|
|
5
4
|
|
|
6
5
|
Ruby wrapper for libwebp. What is WebP?
|
|
7
6
|
|
|
@@ -11,6 +10,30 @@ WebP is a new image format that provides lossless and lossy compression for imag
|
|
|
11
10
|
|
|
12
11
|
## Installation
|
|
13
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
|
+
|
|
14
37
|
### Requirements
|
|
15
38
|
|
|
16
39
|
First of all you should have install libraries: libpng, libjpeg and libtiff.
|
|
@@ -25,13 +48,17 @@ For Fedora, CentOS:
|
|
|
25
48
|
|
|
26
49
|
For Mac OS:
|
|
27
50
|
|
|
28
|
-
brew install
|
|
51
|
+
brew install webp libpng jpeg-turbo libtiff
|
|
29
52
|
|
|
30
53
|
or (for MacPorts):
|
|
31
54
|
|
|
32
|
-
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`:
|
|
33
58
|
|
|
34
|
-
|
|
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.
|
|
35
62
|
|
|
36
63
|
### Final part
|
|
37
64
|
|
|
@@ -49,31 +76,37 @@ Or install it yourself as:
|
|
|
49
76
|
|
|
50
77
|
## Usage
|
|
51
78
|
|
|
52
|
-
### Encoder
|
|
79
|
+
### Encoder and Decoder versions
|
|
53
80
|
|
|
54
81
|
Basic info about libwebp (encoder and decoder versions):
|
|
55
82
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
2.0.0p0 :002 > WebP.decoder_version
|
|
60
|
-
=> "0.3.1"
|
|
61
|
-
2.0.0p0 :003 > WebP.encoder_version
|
|
62
|
-
=> "0.3.1"
|
|
83
|
+
require 'webp_ffi'
|
|
84
|
+
WebP.decoder_version # => "1.6.0"
|
|
85
|
+
WebP.encoder_version # => "1.6.0"
|
|
63
86
|
|
|
64
87
|
|
|
65
|
-
### WebP size
|
|
88
|
+
### WebP size and details
|
|
66
89
|
|
|
67
|
-
Get size (width and height)
|
|
90
|
+
Get the size (width and height) of a WebP image:
|
|
68
91
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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.
|
|
72
105
|
|
|
73
106
|
|
|
74
107
|
### Encode WebP image
|
|
75
108
|
|
|
76
|
-
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):
|
|
77
110
|
|
|
78
111
|
filename = File.expand_path(File.join(File.dirname(__FILE__), "spec/factories/4.png"))
|
|
79
112
|
out_filename = File.expand_path(File.join(File.dirname(__FILE__), "tmp/4.webp"))
|
|
@@ -83,16 +116,22 @@ Encode png, jpg or tiff image to webp:
|
|
|
83
116
|
Encode png, jpg or tiff image to webp with options:
|
|
84
117
|
|
|
85
118
|
WebP.encode(filename, out_filename, quality: 50, resize_w: 100, resize_h: 200)
|
|
86
|
-
WebP.encode(filename, out_filename, quality: 75, crop_x: 0,
|
|
119
|
+
WebP.encode(filename, out_filename, quality: 75, crop_x: 0, crop_y: 0, crop_w: 100, crop_h: 100)
|
|
120
|
+
|
|
87
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.
|
|
88
125
|
|
|
89
126
|
Possible encode options:
|
|
90
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
|
|
91
129
|
* **lossless** (int) - Lossless encoding (0=lossy(default), 1=lossless)
|
|
92
|
-
* **
|
|
93
|
-
* **
|
|
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
|
|
94
133
|
* **method** (int) - quality/speed trade-off (0=fast, 6=slower-better)
|
|
95
|
-
* **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
|
|
96
135
|
* **target\_PSNR** (float) - if non-zero, specifies the minimal distortion to try to achieve. Takes precedence over target\_size
|
|
97
136
|
* **segments** (int) - maximum number of segments to use, in [1..4]
|
|
98
137
|
* **sns_strength** (int) - Spatial Noise Shaping. 0=off, 100=maximum
|
|
@@ -106,12 +145,28 @@ Possible encode options:
|
|
|
106
145
|
* **pass** (int) - number of entropy-analysis passes (in [1..10])
|
|
107
146
|
* **show\_compressed** (int) - if true, export the compressed picture back. In-loop filtering is not applied
|
|
108
147
|
* **preprocessing** (int) - preprocessing filter (0=none, 1=segment-smooth)
|
|
109
|
-
* **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**
|
|
110
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)
|
|
111
157
|
* **width** (int), **height** (int) - Input size (width x height) for YUV
|
|
112
158
|
* **crop\_x** (int), **crop\_y** (int), **crop\_w** (int), **crop\_h** (int) - crop picture with the given rectangle
|
|
113
159
|
* **resize\_w** (int), **resize\_h** (int) - resize picture (after any cropping)
|
|
114
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
|
+
|
|
115
170
|
### Decode WebP image
|
|
116
171
|
|
|
117
172
|
Decode webp image (default format is png):
|
|
@@ -132,14 +187,18 @@ Decode webp image to pam, ppm, pgm, bmp, tiff or yuv format of image:
|
|
|
132
187
|
WebP.decode(filename, out_filename, output_format: :tiff)
|
|
133
188
|
WebP.decode(filename, out_filename, output_format: :yuv)
|
|
134
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
|
+
|
|
135
194
|
|
|
136
195
|
Decode webp image with options:
|
|
137
196
|
|
|
138
|
-
WebP.
|
|
139
|
-
WebP.
|
|
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)
|
|
140
199
|
|
|
141
200
|
|
|
142
|
-
Possible decode options:
|
|
201
|
+
Possible decode options (unknown options and an unknown `output_format` raise `ArgumentError`):
|
|
143
202
|
|
|
144
203
|
* **bypass\_filtering** (bool) - disable in-loop filtering
|
|
145
204
|
* **no\_fancy\_upsampling** (bool) - don't use the fancy YUV420 upscaler
|
|
@@ -147,6 +206,51 @@ Possible decode options:
|
|
|
147
206
|
* **crop\_x** (int), **crop\_y** (int), **crop\_w** (int), **crop\_h** (int) - crop picture with the given rectangle
|
|
148
207
|
* **resize\_w** (int), **resize\_h** (int) - resize picture (after any cropping)
|
|
149
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
|
+
|
|
150
254
|
## Rails assets pipeline integration
|
|
151
255
|
|
|
152
256
|
For integration with Rails 3+ you can use very simple rake task:
|
|
@@ -188,6 +292,13 @@ namespace :assets do
|
|
|
188
292
|
end
|
|
189
293
|
```
|
|
190
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
|
+
|
|
191
302
|
## Contributing
|
|
192
303
|
|
|
193
304
|
1. Fork it
|
data/ext/webp_ffi/Rakefile
CHANGED
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
|
|
1
|
+
require_relative 'build_config'
|
|
2
2
|
|
|
3
|
-
|
|
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
|
data/ext/webp_ffi/jpegdec.c
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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(
|
|
149
|
+
rgb = (uint8_t*)malloc(rgb_size);
|
|
55
150
|
if (rgb == NULL) {
|
|
56
|
-
goto
|
|
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
|
|
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);
|