image_util 0.1.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +96 -0
- data/AGENTS.md +16 -0
- data/CHANGELOG.md +18 -0
- data/LICENSE.txt +21 -0
- data/README.md +157 -0
- data/Rakefile +10 -0
- data/lib/image_util/codec/_guard.rb +35 -0
- data/lib/image_util/codec/image_magick.rb +49 -0
- data/lib/image_util/codec/libpng.rb +138 -0
- data/lib/image_util/codec/libsixel.rb +122 -0
- data/lib/image_util/codec/libturbojpeg.rb +141 -0
- data/lib/image_util/codec/pam.rb +85 -0
- data/lib/image_util/codec/ruby_sixel.rb +116 -0
- data/lib/image_util/codec.rb +117 -0
- data/lib/image_util/color.rb +149 -0
- data/lib/image_util/filter/_mixin.rb +15 -0
- data/lib/image_util/filter/background.rb +23 -0
- data/lib/image_util/filter/dither.rb +96 -0
- data/lib/image_util/filter/draw.rb +77 -0
- data/lib/image_util/filter/paste.rb +54 -0
- data/lib/image_util/filter/resize.rb +20 -0
- data/lib/image_util/filter.rb +13 -0
- data/lib/image_util/image/buffer.rb +133 -0
- data/lib/image_util/image.rb +242 -0
- data/lib/image_util/magic.rb +45 -0
- data/lib/image_util/statistic/color.rb +9 -0
- data/lib/image_util/statistic.rb +7 -0
- data/lib/image_util/util.rb +15 -0
- data/lib/image_util/version.rb +5 -0
- data/lib/image_util/view/interpolated.rb +45 -0
- data/lib/image_util/view/rounded.rb +16 -0
- data/lib/image_util/view.rb +8 -0
- data/lib/image_util.rb +17 -0
- data/sig/image_util.rbs +4 -0
- metadata +96 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 27dbed58dd9646ef50f6ae022f3d2599629670f0d430f61c76416cb28d49bbd6
|
4
|
+
data.tar.gz: 55d78376ffbbf3f85b60db158ca65c46b96164c9e6c52b0d7324b70e1bd11dc4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2c14b302a030e83cc6f398f8560138c31f53e93d9059c7f2b18ce5b81aeb41e4922932a64e8221b5e2e52fc4e5c876ac4dfd74f0eb7630e907e5a74a50a52335
|
7
|
+
data.tar.gz: 6f3834f813239cc80418d54a01224ae5afbd28870a8beff19f1a2c32fabfe7a800d2cc59a857d98faed713c49fff2cc0519332927004c900a72709b08162bcc3
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 3.2
|
3
|
+
NewCops: disable
|
4
|
+
|
5
|
+
Style/StringLiterals:
|
6
|
+
EnforcedStyle: double_quotes
|
7
|
+
Exclude:
|
8
|
+
- 'spec/**/*'
|
9
|
+
- 'image_util.gemspec'
|
10
|
+
|
11
|
+
Style/StringLiteralsInInterpolation:
|
12
|
+
EnforcedStyle: double_quotes
|
13
|
+
|
14
|
+
Style/FrozenStringLiteralComment:
|
15
|
+
Enabled: false
|
16
|
+
|
17
|
+
Style/Documentation:
|
18
|
+
Enabled: false
|
19
|
+
|
20
|
+
Layout/LineLength:
|
21
|
+
Max: 170
|
22
|
+
|
23
|
+
Metrics/BlockLength:
|
24
|
+
Enabled: false
|
25
|
+
Metrics/ClassLength:
|
26
|
+
Enabled: false
|
27
|
+
Metrics/MethodLength:
|
28
|
+
Enabled: false
|
29
|
+
Metrics/AbcSize:
|
30
|
+
Enabled: false
|
31
|
+
Metrics/CyclomaticComplexity:
|
32
|
+
Enabled: false
|
33
|
+
Metrics/PerceivedComplexity:
|
34
|
+
Enabled: false
|
35
|
+
|
36
|
+
Naming/MethodParameterName:
|
37
|
+
Enabled: false
|
38
|
+
|
39
|
+
Lint/SuppressedException:
|
40
|
+
Enabled: false
|
41
|
+
|
42
|
+
Style/MultilineBlockChain:
|
43
|
+
Enabled: false
|
44
|
+
|
45
|
+
Style/PerlBackrefs:
|
46
|
+
Enabled: false
|
47
|
+
|
48
|
+
Style/FormatStringToken:
|
49
|
+
Enabled: false
|
50
|
+
Style/FormatString:
|
51
|
+
Enabled: false
|
52
|
+
|
53
|
+
Layout/SpaceAfterComma:
|
54
|
+
Enabled: false
|
55
|
+
Layout/SpaceAroundOperators:
|
56
|
+
Enabled: false
|
57
|
+
Layout/TrailingWhitespace:
|
58
|
+
Enabled: false
|
59
|
+
|
60
|
+
Style/IfUnlessModifier:
|
61
|
+
Enabled: false
|
62
|
+
Style/NilComparison:
|
63
|
+
Enabled: false
|
64
|
+
Style/NumericPredicate:
|
65
|
+
Enabled: false
|
66
|
+
|
67
|
+
Lint/DuplicateMethods:
|
68
|
+
Enabled: false
|
69
|
+
|
70
|
+
Lint/Void:
|
71
|
+
Enabled: false
|
72
|
+
|
73
|
+
Layout/SpaceBeforeBlockBraces:
|
74
|
+
Enabled: false
|
75
|
+
|
76
|
+
Lint/UnusedBlockArgument:
|
77
|
+
Enabled: false
|
78
|
+
|
79
|
+
Naming/AccessorMethodName:
|
80
|
+
Enabled: false
|
81
|
+
|
82
|
+
Style/SingleLineMethods:
|
83
|
+
Enabled: false
|
84
|
+
|
85
|
+
Layout/EmptyLineAfterGuardClause:
|
86
|
+
Enabled: false
|
87
|
+
|
88
|
+
Style/IfInsideElse:
|
89
|
+
Enabled: false
|
90
|
+
|
91
|
+
Style/RedundantBegin:
|
92
|
+
Enabled: false
|
93
|
+
|
94
|
+
Style/SymbolProc:
|
95
|
+
Enabled: false
|
96
|
+
|
data/AGENTS.md
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# Contributor Guidelines
|
2
|
+
|
3
|
+
- Specs mirror file structure under `lib`. For example, `lib/image_util/image/buffer.rb` has a spec at `spec/image/buffer_spec.rb`.
|
4
|
+
- Use `autoload` for loading internal files. Avoid `require` and `require_relative`.
|
5
|
+
- Start every Ruby file with `# frozen_string_literal: true`.
|
6
|
+
- Prefer double-quoted strings except in specs and the gemspec.
|
7
|
+
- Use RSpec's `should` syntax instead of `expect`.
|
8
|
+
- For one-line methods, use the `def name = expression` style.
|
9
|
+
|
10
|
+
Additional notes from the existing code:
|
11
|
+
- Image data is stored in `Image::Buffer` backed by `IO::Buffer`.
|
12
|
+
- Use `Filter::Mixin#define_immutable_version` to add non-bang versions of mutating filters.
|
13
|
+
- Views such as `View::Interpolated` and `View::Rounded` are built with `Data.define`.
|
14
|
+
- Pure Ruby algorithms are provided with optional FFI wrappers for libpng, libturbojpeg and libsixel.
|
15
|
+
- Specs target at least 80% coverage as enforced by SimpleCov.
|
16
|
+
- The library aims to remain lightweight and portable.
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
## [Unreleased]
|
2
|
+
### Added
|
3
|
+
- Drop support for Ruby 3.1
|
4
|
+
- Native SIXEL encoder
|
5
|
+
- Background filter
|
6
|
+
- Format auto-detection using magic numbers
|
7
|
+
- Faster 1D paste using direct buffer copy
|
8
|
+
- Libsixel encoder with default palette
|
9
|
+
- JPEG support via libturbojpeg
|
10
|
+
- PNG support via libpng
|
11
|
+
- Dither filter for palette reduction
|
12
|
+
- Paste and Draw filters for compositing and drawing
|
13
|
+
- Rounded and interpolated pixel views
|
14
|
+
- `Image#view` helper for custom access
|
15
|
+
|
16
|
+
## [0.1.0] - 2025-07-19
|
17
|
+
|
18
|
+
- Initial release
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 hmdne
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
# ImageUtil
|
2
|
+
|
3
|
+
ImageUtil provides a minimal in-memory image container and a set of utilities for handling raw pixel data in Ruby. It is aimed at small scripts and tools that need to manipulate images without relying on heavy external dependencies.
|
4
|
+
|
5
|
+
Features include:
|
6
|
+
|
7
|
+
* Representation of images with arbitrary dimensions.
|
8
|
+
* Support for 8, 16 or 32 bit components and RGB or RGBA color values.
|
9
|
+
* A `Color` helper class capable of parsing numbers, arrays and HTML style strings.
|
10
|
+
* Conversion of an image to PAM or SIXEL for quick previews in compatible terminals.
|
11
|
+
* Built-in SIXEL encoder that works without ImageMagick.
|
12
|
+
* Convenience methods for iterating over pixel locations and setting values.
|
13
|
+
* Overlaying colors with the `+` operator which blends using the alpha channel.
|
14
|
+
* Automatic format detection when reading images from strings or files.
|
15
|
+
* Alternate pixel views for interpolated or rounded coordinates.
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
ImageUtil is available on RubyGems:
|
20
|
+
|
21
|
+
```bash
|
22
|
+
gem install image_util
|
23
|
+
```
|
24
|
+
|
25
|
+
Alternatively add it to your `Gemfile`:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
gem "image_util"
|
29
|
+
```
|
30
|
+
|
31
|
+
Run `bundle install` afterwards.
|
32
|
+
|
33
|
+
You can also build and install the gem manually:
|
34
|
+
|
35
|
+
```bash
|
36
|
+
git clone https://github.com/rbutils/image_util.git
|
37
|
+
cd image_util
|
38
|
+
bundle exec rake install
|
39
|
+
```
|
40
|
+
|
41
|
+
## Usage
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
require 'image_util'
|
45
|
+
|
46
|
+
# create a 4×4 image using 8‑bit RGBA colors
|
47
|
+
i = ImageUtil::Image.new(4, 4)
|
48
|
+
|
49
|
+
# set the top‑left pixel to red
|
50
|
+
i[0, 0] = ImageUtil::Color[255, 0, 0]
|
51
|
+
|
52
|
+
# display the image in a SIXEL-capable terminal
|
53
|
+
puts i.to_sixel
|
54
|
+
```
|
55
|
+
|
56
|
+
Images can also be iterated over or modified using ranges:
|
57
|
+
|
58
|
+
```ruby
|
59
|
+
# fill an area with blue
|
60
|
+
i[0..1, 0..1] = ImageUtil::Color['#0000ff']
|
61
|
+
|
62
|
+
# iterate over every pixel
|
63
|
+
i.each_pixel do |pixel|
|
64
|
+
# pixel is an ImageUtil::Color instance
|
65
|
+
end
|
66
|
+
|
67
|
+
# paste one image into another
|
68
|
+
target = ImageUtil::Image.new(8, 8) { ImageUtil::Color[0] }
|
69
|
+
source = ImageUtil::Image.new(2, 2) { ImageUtil::Color[255, 0, 0, 128] }
|
70
|
+
target.paste!(source, 3, 3, respect_alpha: true)
|
71
|
+
|
72
|
+
# draw a diagonal line
|
73
|
+
i.draw_line!([0, 0], [3, 3], ImageUtil::Color['red'], view: ImageUtil::View::Rounded)
|
74
|
+
```
|
75
|
+
|
76
|
+
`View::Interpolated` provides subpixel access while `View::Rounded` snaps
|
77
|
+
coordinates to the nearest pixel. These views are useful for drawing
|
78
|
+
operations like the example above.
|
79
|
+
|
80
|
+
### Reading and Writing Images
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
# detect format automatically when loading from a file
|
84
|
+
img = ImageUtil::Image.from_file("photo.png")
|
85
|
+
|
86
|
+
# save using a specific codec
|
87
|
+
img.to_file("out.jpg", :jpeg)
|
88
|
+
|
89
|
+
# convert directly to a string
|
90
|
+
data = img.to_string(:png)
|
91
|
+
```
|
92
|
+
|
93
|
+
### Filters
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
# reduce palette to 32 colors
|
97
|
+
dithered = img.dither(32)
|
98
|
+
|
99
|
+
# composite two images without altering the originals
|
100
|
+
result = base.paste(other, 10, 10)
|
101
|
+
|
102
|
+
# apply a background color to an RGBA image
|
103
|
+
flattened = img.background(ImageUtil::Color[255, 255, 255])
|
104
|
+
```
|
105
|
+
|
106
|
+
### Working with Views
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
# access using fractional coordinates
|
110
|
+
interp = img.view(ImageUtil::View::Interpolated)
|
111
|
+
interp[1.2, 2.8] = ImageUtil::Color[0, 0, 255]
|
112
|
+
|
113
|
+
# round coordinates instead
|
114
|
+
rounded = img.view(ImageUtil::View::Rounded)
|
115
|
+
color = rounded[1.6, 0.3]
|
116
|
+
```
|
117
|
+
|
118
|
+
### Codecs
|
119
|
+
|
120
|
+
ImageUtil includes a small registry of codecs for converting images to and from
|
121
|
+
common formats such as PNG, JPEG and SIXEL. The library ships with pure Ruby
|
122
|
+
encoders and FFI wrappers around `libpng`, `libturbojpeg` and `libsixel` when
|
123
|
+
available.
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
png = ImageUtil::Codec.encode(:png, i)
|
127
|
+
back = ImageUtil::Codec.decode(:png, png)
|
128
|
+
|
129
|
+
File.open("img.pam", "wb") do |f|
|
130
|
+
ImageUtil::Codec.encode_io(:pam, i, f)
|
131
|
+
end
|
132
|
+
```
|
133
|
+
|
134
|
+
You can read images from files without specifying the format:
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
image = ImageUtil::Image.from_file("picture.jpg")
|
138
|
+
```
|
139
|
+
|
140
|
+
Use `ImageUtil::Codec.supported?(format)` to check if a particular format is
|
141
|
+
available. Unsupported formats raise `ImageUtil::Codec::UnsupportedFormatError`.
|
142
|
+
|
143
|
+
## Development
|
144
|
+
|
145
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then run
|
146
|
+
`rake spec` to execute the tests. You can also run `bin/console` for an
|
147
|
+
interactive prompt for experimenting with the library.
|
148
|
+
|
149
|
+
## Contributing
|
150
|
+
|
151
|
+
Bug reports and pull requests are welcome on GitHub at
|
152
|
+
<https://github.com/rbutils/image_util>.
|
153
|
+
|
154
|
+
## License
|
155
|
+
|
156
|
+
The gem is available as open source under the terms of the
|
157
|
+
[MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageUtil
|
4
|
+
module Codec
|
5
|
+
module Guard
|
6
|
+
def guard_supported_format!(format, supported)
|
7
|
+
return if supported.map { |f| f.to_s.downcase.to_sym }.include?(format.to_s.downcase.to_sym)
|
8
|
+
|
9
|
+
raise UnsupportedFormatError, "unsupported format #{format}"
|
10
|
+
end
|
11
|
+
|
12
|
+
def guard_image_class!(image)
|
13
|
+
return if image.is_a?(Image)
|
14
|
+
|
15
|
+
raise ArgumentError, "image must be an ImageUtil::Image"
|
16
|
+
end
|
17
|
+
|
18
|
+
def guard_2d_image!(image)
|
19
|
+
guard_image_class!(image)
|
20
|
+
return if image.dimensions.length == 2
|
21
|
+
|
22
|
+
raise ArgumentError, "only 2D images supported"
|
23
|
+
end
|
24
|
+
|
25
|
+
def guard_8bit_colors!(image)
|
26
|
+
guard_image_class!(image)
|
27
|
+
return if image.color_bits == 8
|
28
|
+
|
29
|
+
raise ArgumentError, "only 8-bit colors supported"
|
30
|
+
end
|
31
|
+
|
32
|
+
module_function :guard_supported_format!, :guard_image_class!, :guard_2d_image!, :guard_8bit_colors!
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageUtil
|
4
|
+
module Codec
|
5
|
+
module ImageMagick
|
6
|
+
SUPPORTED_FORMATS = [:sixel].freeze
|
7
|
+
|
8
|
+
extend Guard
|
9
|
+
|
10
|
+
module_function
|
11
|
+
|
12
|
+
def magick_available?
|
13
|
+
return @magick_available unless @magick_available.nil?
|
14
|
+
|
15
|
+
@magick_available = system("magick", "-version", out: File::NULL, err: File::NULL)
|
16
|
+
end
|
17
|
+
|
18
|
+
def supported?(format = nil)
|
19
|
+
return false unless magick_available?
|
20
|
+
|
21
|
+
return true if format.nil?
|
22
|
+
|
23
|
+
SUPPORTED_FORMATS.include?(format.to_s.downcase.to_sym)
|
24
|
+
end
|
25
|
+
|
26
|
+
def encode(format, image)
|
27
|
+
guard_supported_format!(format, SUPPORTED_FORMATS)
|
28
|
+
|
29
|
+
IO.popen("magick pam:- sixel:-", "r+") do |io|
|
30
|
+
io << Codec::Pam.encode(:pam, image, fill_to: 6)
|
31
|
+
io.close_write
|
32
|
+
io.read
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def encode_io(format, image, io)
|
37
|
+
io << encode(format, image)
|
38
|
+
end
|
39
|
+
|
40
|
+
def decode(*)
|
41
|
+
raise UnsupportedFormatError, "decode not supported for sixel"
|
42
|
+
end
|
43
|
+
|
44
|
+
def decode_io(*)
|
45
|
+
raise UnsupportedFormatError, "decode not supported for sixel"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageUtil
|
4
|
+
module Codec
|
5
|
+
module Libpng
|
6
|
+
SUPPORTED_FORMATS = [:png].freeze
|
7
|
+
|
8
|
+
extend Guard
|
9
|
+
|
10
|
+
begin
|
11
|
+
require "ffi"
|
12
|
+
|
13
|
+
extend FFI::Library
|
14
|
+
ffi_lib [
|
15
|
+
"libpng16.so.16", # Linux
|
16
|
+
"libpng16-16.dll", "libpng16.dll", # Windows
|
17
|
+
"libpng16.16.dylib", "libpng16.dylib", # macOS
|
18
|
+
"libpng16.so", "libpng.so", "libpng.dll", "libpng.dylib", # generic
|
19
|
+
"libpng16", "libpng", "png16", "png"
|
20
|
+
]
|
21
|
+
|
22
|
+
AVAILABLE = true
|
23
|
+
rescue LoadError
|
24
|
+
AVAILABLE = false
|
25
|
+
end
|
26
|
+
|
27
|
+
PNG_IMAGE_VERSION = 1
|
28
|
+
|
29
|
+
PNG_FORMAT_FLAG_ALPHA = 0x01
|
30
|
+
PNG_FORMAT_FLAG_COLOR = 0x02
|
31
|
+
PNG_FORMAT_FLAG_LINEAR = 0x04
|
32
|
+
|
33
|
+
PNG_FORMAT_RGB = PNG_FORMAT_FLAG_COLOR
|
34
|
+
PNG_FORMAT_RGBA = PNG_FORMAT_FLAG_COLOR | PNG_FORMAT_FLAG_ALPHA
|
35
|
+
|
36
|
+
if AVAILABLE
|
37
|
+
class PngImage < FFI::Struct
|
38
|
+
layout :opaque, :pointer,
|
39
|
+
:version, :uint32,
|
40
|
+
:width, :uint32,
|
41
|
+
:height, :uint32,
|
42
|
+
:format, :uint32,
|
43
|
+
:flags, :uint32,
|
44
|
+
:colormap_entries, :uint32,
|
45
|
+
:warning_or_error, :uint32,
|
46
|
+
:message, [:char, 64]
|
47
|
+
end
|
48
|
+
|
49
|
+
attach_function :png_image_write_to_memory, %i[pointer pointer pointer int pointer int pointer], :int
|
50
|
+
attach_function :png_image_begin_read_from_memory, %i[pointer pointer size_t], :int
|
51
|
+
attach_function :png_image_finish_read, %i[pointer pointer pointer int pointer], :int
|
52
|
+
attach_function :png_image_free, [:pointer], :void
|
53
|
+
end
|
54
|
+
|
55
|
+
module_function
|
56
|
+
|
57
|
+
def supported?(format = nil)
|
58
|
+
return false unless AVAILABLE
|
59
|
+
|
60
|
+
return true if format.nil?
|
61
|
+
|
62
|
+
SUPPORTED_FORMATS.include?(format.to_s.downcase.to_sym)
|
63
|
+
end
|
64
|
+
|
65
|
+
def encode(format, image)
|
66
|
+
guard_supported_format!(format, SUPPORTED_FORMATS)
|
67
|
+
raise UnsupportedFormatError, "libpng not available" unless AVAILABLE
|
68
|
+
|
69
|
+
guard_2d_image!(image)
|
70
|
+
guard_8bit_colors!(image)
|
71
|
+
|
72
|
+
fmt = if image.color_length == 4
|
73
|
+
PNG_FORMAT_RGBA
|
74
|
+
else
|
75
|
+
PNG_FORMAT_RGB
|
76
|
+
end
|
77
|
+
|
78
|
+
img = PngImage.new
|
79
|
+
img[:version] = PNG_IMAGE_VERSION
|
80
|
+
img[:width] = image.width
|
81
|
+
img[:height] = image.height
|
82
|
+
img[:format] = fmt
|
83
|
+
img[:flags] = 0
|
84
|
+
img[:colormap_entries] = 0
|
85
|
+
|
86
|
+
row_stride = image.width * image.color_length
|
87
|
+
buffer_ptr = FFI::MemoryPointer.from_string(image.buffer.get_string)
|
88
|
+
size_ptr = FFI::MemoryPointer.new(:size_t)
|
89
|
+
|
90
|
+
ok = png_image_write_to_memory(img, nil, size_ptr, 0, buffer_ptr, row_stride, nil)
|
91
|
+
raise StandardError, img[:message].to_s if ok.zero?
|
92
|
+
|
93
|
+
size = size_ptr.read_ulong
|
94
|
+
out_ptr = FFI::MemoryPointer.new(:uchar, size)
|
95
|
+
ok = png_image_write_to_memory(img, out_ptr, size_ptr, 0, buffer_ptr, row_stride, nil)
|
96
|
+
raise StandardError, img[:message].to_s if ok.zero?
|
97
|
+
|
98
|
+
out_ptr.read_string(size_ptr.read_ulong)
|
99
|
+
ensure
|
100
|
+
png_image_free(img) if img
|
101
|
+
end
|
102
|
+
|
103
|
+
def encode_io(format, image, io)
|
104
|
+
io << encode(format, image)
|
105
|
+
end
|
106
|
+
|
107
|
+
def decode(format, data)
|
108
|
+
guard_supported_format!(format, SUPPORTED_FORMATS)
|
109
|
+
raise UnsupportedFormatError, "libpng not available" unless AVAILABLE
|
110
|
+
|
111
|
+
img = PngImage.new
|
112
|
+
img[:version] = PNG_IMAGE_VERSION
|
113
|
+
|
114
|
+
data_ptr = FFI::MemoryPointer.from_string(data)
|
115
|
+
ok = png_image_begin_read_from_memory(img, data_ptr, data.bytesize)
|
116
|
+
raise StandardError, img[:message].to_s if ok.zero?
|
117
|
+
|
118
|
+
img[:format] = PNG_FORMAT_RGBA
|
119
|
+
row_stride = img[:width] * 4
|
120
|
+
buffer_ptr = FFI::MemoryPointer.new(:uchar, row_stride * img[:height])
|
121
|
+
|
122
|
+
ok = png_image_finish_read(img, nil, buffer_ptr, row_stride, nil)
|
123
|
+
raise StandardError, img[:message].to_s if ok.zero?
|
124
|
+
|
125
|
+
raw = buffer_ptr.read_string(row_stride * img[:height])
|
126
|
+
io_buf = IO::Buffer.for(raw)
|
127
|
+
buf = Image::Buffer.new([img[:width], img[:height]], 8, 4, io_buf)
|
128
|
+
Image.from_buffer(buf)
|
129
|
+
ensure
|
130
|
+
png_image_free(img) if img
|
131
|
+
end
|
132
|
+
|
133
|
+
def decode_io(format, io)
|
134
|
+
decode(format, io.read)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ImageUtil
|
4
|
+
module Codec
|
5
|
+
module Libsixel
|
6
|
+
SUPPORTED_FORMATS = [:sixel].freeze
|
7
|
+
|
8
|
+
# https://github.com/libsixel/libsixel
|
9
|
+
SIXEL_PALETTE_MAX = 256
|
10
|
+
SIXEL_LARGE_AUTO = 0
|
11
|
+
SIXEL_REP_AUTO = 0
|
12
|
+
SIXEL_QUALITY_AUTO = 0
|
13
|
+
SIXEL_DIFFUSE_AUTO = 0
|
14
|
+
SIXEL_PIXELFORMAT_RGB888 = 3
|
15
|
+
SIXEL_PIXELFORMAT_RGBA8888 = 0x11
|
16
|
+
|
17
|
+
extend Guard
|
18
|
+
|
19
|
+
begin
|
20
|
+
require "ffi"
|
21
|
+
|
22
|
+
extend FFI::Library
|
23
|
+
ffi_lib [
|
24
|
+
"libsixel.so.1", # Linux
|
25
|
+
"libsixel-1.dll", "libsixel.dll", # Windows
|
26
|
+
"libsixel.1.dylib", "libsixel.dylib", # macOS
|
27
|
+
"libsixel.so", "libsixel"
|
28
|
+
]
|
29
|
+
|
30
|
+
callback :write_function, %i[pointer int pointer], :int
|
31
|
+
|
32
|
+
attach_function :sixel_output_new, %i[pointer write_function pointer pointer], :int
|
33
|
+
attach_function :sixel_output_unref, [:pointer], :void
|
34
|
+
attach_function :sixel_dither_new, %i[pointer int pointer], :int
|
35
|
+
attach_function :sixel_dither_initialize, %i[pointer pointer int int int int int int], :int
|
36
|
+
attach_function :sixel_dither_set_diffusion_type, %i[pointer int], :void
|
37
|
+
attach_function :sixel_dither_unref, [:pointer], :void
|
38
|
+
attach_function :sixel_encode, %i[pointer int int int pointer pointer], :int
|
39
|
+
|
40
|
+
AVAILABLE = true
|
41
|
+
rescue LoadError
|
42
|
+
AVAILABLE = false
|
43
|
+
end
|
44
|
+
|
45
|
+
module_function
|
46
|
+
|
47
|
+
def supported?(format = nil)
|
48
|
+
return false unless AVAILABLE
|
49
|
+
|
50
|
+
return true if format.nil?
|
51
|
+
|
52
|
+
SUPPORTED_FORMATS.include?(format.to_s.downcase.to_sym)
|
53
|
+
end
|
54
|
+
|
55
|
+
def encode(format, image)
|
56
|
+
guard_supported_format!(format, SUPPORTED_FORMATS)
|
57
|
+
raise UnsupportedFormatError, "libsixel not available" unless AVAILABLE
|
58
|
+
|
59
|
+
guard_2d_image!(image)
|
60
|
+
guard_8bit_colors!(image)
|
61
|
+
|
62
|
+
fmt = image.color_length == 4 ? SIXEL_PIXELFORMAT_RGBA8888 : SIXEL_PIXELFORMAT_RGB888
|
63
|
+
|
64
|
+
data = "".b
|
65
|
+
writer = FFI::Function.new(:int, %i[pointer int pointer]) do |ptr, size, _|
|
66
|
+
data << ptr.read_string(size)
|
67
|
+
size
|
68
|
+
end
|
69
|
+
|
70
|
+
out_ptr = FFI::MemoryPointer.new(:pointer)
|
71
|
+
res = sixel_output_new(out_ptr, writer, nil, nil)
|
72
|
+
raise StandardError, "sixel_output_new failed" if res != 0
|
73
|
+
|
74
|
+
output = out_ptr.read_pointer
|
75
|
+
|
76
|
+
dither_ptr = FFI::MemoryPointer.new(:pointer)
|
77
|
+
res = sixel_dither_new(dither_ptr, -1, nil)
|
78
|
+
raise StandardError, "sixel_dither_new failed" if res != 0
|
79
|
+
|
80
|
+
dither = dither_ptr.read_pointer
|
81
|
+
|
82
|
+
pixels = image.buffer.get_string
|
83
|
+
buf_ptr = FFI::MemoryPointer.new(:uchar, pixels.bytesize)
|
84
|
+
buf_ptr.put_bytes(0, pixels)
|
85
|
+
|
86
|
+
res = sixel_dither_initialize(
|
87
|
+
dither,
|
88
|
+
buf_ptr,
|
89
|
+
image.width,
|
90
|
+
image.height,
|
91
|
+
fmt,
|
92
|
+
SIXEL_LARGE_AUTO,
|
93
|
+
SIXEL_REP_AUTO,
|
94
|
+
SIXEL_QUALITY_AUTO
|
95
|
+
)
|
96
|
+
raise StandardError, "sixel_dither_initialize failed" if res != 0
|
97
|
+
|
98
|
+
sixel_dither_set_diffusion_type(dither, SIXEL_DIFFUSE_AUTO)
|
99
|
+
|
100
|
+
res = sixel_encode(buf_ptr, image.width, image.height, fmt, dither, output)
|
101
|
+
raise StandardError, "sixel_encode failed" if res != 0
|
102
|
+
|
103
|
+
data
|
104
|
+
ensure
|
105
|
+
sixel_dither_unref(dither) if defined?(dither) && dither && !dither.null?
|
106
|
+
sixel_output_unref(output) if defined?(output) && output && !output.null?
|
107
|
+
end
|
108
|
+
|
109
|
+
def encode_io(format, image, io)
|
110
|
+
io << encode(format, image)
|
111
|
+
end
|
112
|
+
|
113
|
+
def decode(*)
|
114
|
+
raise UnsupportedFormatError, "decode not supported for sixel"
|
115
|
+
end
|
116
|
+
|
117
|
+
def decode_io(*)
|
118
|
+
raise UnsupportedFormatError, "decode not supported for sixel"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|