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 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
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require "rubocop/rake_task"
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+ RuboCop::RakeTask.new
9
+
10
+ task default: %i[spec rubocop]
@@ -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