skia 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.
data/lib/skia/point.rb ADDED
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skia
4
+ class Point
5
+ attr_accessor :x, :y
6
+
7
+ def initialize(x = 0.0, y = 0.0)
8
+ @x = x.to_f
9
+ @y = y.to_f
10
+ end
11
+
12
+ def self.from_struct(struct)
13
+ new(struct[:x], struct[:y])
14
+ end
15
+
16
+ def to_struct
17
+ struct = Native::SKPoint.new
18
+ struct[:x] = @x
19
+ struct[:y] = @y
20
+ struct
21
+ end
22
+
23
+ def +(other)
24
+ self.class.new(@x + other.x, @y + other.y)
25
+ end
26
+
27
+ def -(other)
28
+ self.class.new(@x - other.x, @y - other.y)
29
+ end
30
+
31
+ def *(other)
32
+ self.class.new(@x * other, @y * other)
33
+ end
34
+
35
+ def /(other)
36
+ self.class.new(@x / other, @y / other)
37
+ end
38
+
39
+ def -@
40
+ self.class.new(-@x, -@y)
41
+ end
42
+
43
+ def length
44
+ Math.sqrt(@x * @x + @y * @y)
45
+ end
46
+
47
+ def normalize
48
+ len = length
49
+ return self.class.new(0, 0) if len.zero?
50
+
51
+ self / len
52
+ end
53
+
54
+ def ==(other)
55
+ return false unless other.is_a?(Point)
56
+
57
+ @x == other.x && @y == other.y
58
+ end
59
+
60
+ def to_a
61
+ [@x, @y]
62
+ end
63
+ end
64
+
65
+ class IPoint
66
+ attr_accessor :x, :y
67
+
68
+ def initialize(x = 0, y = 0)
69
+ @x = x.to_i
70
+ @y = y.to_i
71
+ end
72
+
73
+ def self.from_struct(struct)
74
+ new(struct[:x], struct[:y])
75
+ end
76
+
77
+ def to_struct
78
+ struct = Native::SKIPoint.new
79
+ struct[:x] = @x
80
+ struct[:y] = @y
81
+ struct
82
+ end
83
+
84
+ def ==(other)
85
+ return false unless other.is_a?(IPoint)
86
+
87
+ @x == other.x && @y == other.y
88
+ end
89
+
90
+ def to_a
91
+ [@x, @y]
92
+ end
93
+ end
94
+ end
data/lib/skia/rect.rb ADDED
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skia
4
+ class Rect
5
+ attr_accessor :left, :top, :right, :bottom
6
+
7
+ def initialize(left = 0.0, top = 0.0, right = 0.0, bottom = 0.0)
8
+ @left = left.to_f
9
+ @top = top.to_f
10
+ @right = right.to_f
11
+ @bottom = bottom.to_f
12
+ end
13
+
14
+ def self.from_xywh(x, y, width, height)
15
+ new(x, y, x + width, y + height)
16
+ end
17
+
18
+ def self.from_wh(width, height)
19
+ new(0, 0, width, height)
20
+ end
21
+
22
+ def self.from_struct(struct)
23
+ new(struct[:left], struct[:top], struct[:right], struct[:bottom])
24
+ end
25
+
26
+ def to_struct
27
+ struct = Native::SKRect.new
28
+ struct[:left] = @left
29
+ struct[:top] = @top
30
+ struct[:right] = @right
31
+ struct[:bottom] = @bottom
32
+ struct
33
+ end
34
+
35
+ def width
36
+ @right - @left
37
+ end
38
+
39
+ def height
40
+ @bottom - @top
41
+ end
42
+
43
+ def center_x
44
+ (@left + @right) / 2.0
45
+ end
46
+
47
+ def center_y
48
+ (@top + @bottom) / 2.0
49
+ end
50
+
51
+ def center
52
+ Point.new(center_x, center_y)
53
+ end
54
+
55
+ def empty?
56
+ @left >= @right || @top >= @bottom
57
+ end
58
+
59
+ def contains?(x, y)
60
+ x >= @left && x < @right && y >= @top && y < @bottom
61
+ end
62
+
63
+ def contains_rect?(other)
64
+ @left <= other.left && @top <= other.top &&
65
+ @right >= other.right && @bottom >= other.bottom
66
+ end
67
+
68
+ def intersects?(other)
69
+ @left < other.right && other.left < @right &&
70
+ @top < other.bottom && other.top < @bottom
71
+ end
72
+
73
+ def intersect(other)
74
+ return nil unless intersects?(other)
75
+
76
+ self.class.new(
77
+ [@left, other.left].max,
78
+ [@top, other.top].min,
79
+ [@right, other.right].min,
80
+ [@bottom, other.bottom].max
81
+ )
82
+ end
83
+
84
+ def union(other)
85
+ self.class.new(
86
+ [@left, other.left].min,
87
+ [@top, other.top].min,
88
+ [@right, other.right].max,
89
+ [@bottom, other.bottom].max
90
+ )
91
+ end
92
+
93
+ def offset(dx, dy)
94
+ self.class.new(@left + dx, @top + dy, @right + dx, @bottom + dy)
95
+ end
96
+
97
+ def offset!(dx, dy)
98
+ @left += dx
99
+ @top += dy
100
+ @right += dx
101
+ @bottom += dy
102
+ self
103
+ end
104
+
105
+ def inset(dx, dy)
106
+ self.class.new(@left + dx, @top + dy, @right - dx, @bottom - dy)
107
+ end
108
+
109
+ def ==(other)
110
+ return false unless other.is_a?(Rect)
111
+
112
+ @left == other.left && @top == other.top &&
113
+ @right == other.right && @bottom == other.bottom
114
+ end
115
+
116
+ def to_a
117
+ [@left, @top, @right, @bottom]
118
+ end
119
+ end
120
+
121
+ class IRect
122
+ attr_accessor :left, :top, :right, :bottom
123
+
124
+ def initialize(left = 0, top = 0, right = 0, bottom = 0)
125
+ @left = left.to_i
126
+ @top = top.to_i
127
+ @right = right.to_i
128
+ @bottom = bottom.to_i
129
+ end
130
+
131
+ def self.from_xywh(x, y, width, height)
132
+ new(x, y, x + width, y + height)
133
+ end
134
+
135
+ def self.from_wh(width, height)
136
+ new(0, 0, width, height)
137
+ end
138
+
139
+ def self.from_struct(struct)
140
+ new(struct[:left], struct[:top], struct[:right], struct[:bottom])
141
+ end
142
+
143
+ def to_struct
144
+ struct = Native::SKIRect.new
145
+ struct[:left] = @left
146
+ struct[:top] = @top
147
+ struct[:right] = @right
148
+ struct[:bottom] = @bottom
149
+ struct
150
+ end
151
+
152
+ def width
153
+ @right - @left
154
+ end
155
+
156
+ def height
157
+ @bottom - @top
158
+ end
159
+
160
+ def empty?
161
+ @left >= @right || @top >= @bottom
162
+ end
163
+
164
+ def ==(other)
165
+ return false unless other.is_a?(IRect)
166
+
167
+ @left == other.left && @top == other.top &&
168
+ @right == other.right && @bottom == other.bottom
169
+ end
170
+
171
+ def to_a
172
+ [@left, @top, @right, @bottom]
173
+ end
174
+
175
+ def to_rect
176
+ Rect.new(@left.to_f, @top.to_f, @right.to_f, @bottom.to_f)
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skia
4
+ class Shader < Base
5
+ def initialize(ptr)
6
+ super(ptr, :sk_shader_unref)
7
+ end
8
+
9
+ def self.wrap(ptr)
10
+ return nil if ptr.nil? || ptr.null?
11
+
12
+ new(ptr)
13
+ end
14
+
15
+ def self.linear_gradient(start_point, end_point, colors, positions = nil, tile_mode: :clamp, matrix: nil)
16
+ points = FFI::MemoryPointer.new(Native::SKPoint, 2)
17
+ start_struct = start_point.to_struct
18
+ end_struct = end_point.to_struct
19
+ points[0].write_bytes(start_struct.to_ptr.read_bytes(Native::SKPoint.size))
20
+ points[1].write_bytes(end_struct.to_ptr.read_bytes(Native::SKPoint.size))
21
+
22
+ color_values = colors.map { |c| c.is_a?(Color) ? c.to_i : c }
23
+ colors_ptr = FFI::MemoryPointer.new(:uint32, color_values.size)
24
+ colors_ptr.write_array_of_uint32(color_values)
25
+
26
+ positions_ptr = nil
27
+ if positions
28
+ positions_ptr = FFI::MemoryPointer.new(:float, positions.size)
29
+ positions_ptr.write_array_of_float(positions.map(&:to_f))
30
+ end
31
+
32
+ matrix_ptr = matrix&.to_struct
33
+
34
+ ptr = Native.sk_shader_new_linear_gradient(
35
+ points,
36
+ colors_ptr,
37
+ positions_ptr,
38
+ color_values.size,
39
+ tile_mode,
40
+ matrix_ptr
41
+ )
42
+ raise Error, 'Failed to create linear gradient shader' if ptr.nil? || ptr.null?
43
+
44
+ new(ptr)
45
+ end
46
+
47
+ def self.radial_gradient(center, radius, colors, positions = nil, tile_mode: :clamp, matrix: nil)
48
+ center_struct = center.to_struct
49
+
50
+ color_values = colors.map { |c| c.is_a?(Color) ? c.to_i : c }
51
+ colors_ptr = FFI::MemoryPointer.new(:uint32, color_values.size)
52
+ colors_ptr.write_array_of_uint32(color_values)
53
+
54
+ positions_ptr = nil
55
+ if positions
56
+ positions_ptr = FFI::MemoryPointer.new(:float, positions.size)
57
+ positions_ptr.write_array_of_float(positions.map(&:to_f))
58
+ end
59
+
60
+ matrix_ptr = matrix&.to_struct
61
+
62
+ ptr = Native.sk_shader_new_radial_gradient(
63
+ center_struct,
64
+ radius.to_f,
65
+ colors_ptr,
66
+ positions_ptr,
67
+ color_values.size,
68
+ tile_mode,
69
+ matrix_ptr
70
+ )
71
+ raise Error, 'Failed to create radial gradient shader' if ptr.nil? || ptr.null?
72
+
73
+ new(ptr)
74
+ end
75
+
76
+ def self.sweep_gradient(center, colors, positions = nil, tile_mode: :clamp, start_angle: 0.0, end_angle: 360.0,
77
+ matrix: nil)
78
+ center_struct = center.to_struct
79
+
80
+ color_values = colors.map { |c| c.is_a?(Color) ? c.to_i : c }
81
+ colors_ptr = FFI::MemoryPointer.new(:uint32, color_values.size)
82
+ colors_ptr.write_array_of_uint32(color_values)
83
+
84
+ positions_ptr = nil
85
+ if positions
86
+ positions_ptr = FFI::MemoryPointer.new(:float, positions.size)
87
+ positions_ptr.write_array_of_float(positions.map(&:to_f))
88
+ end
89
+
90
+ matrix_ptr = matrix&.to_struct
91
+
92
+ ptr = Native.sk_shader_new_sweep_gradient(
93
+ center_struct,
94
+ colors_ptr,
95
+ positions_ptr,
96
+ color_values.size,
97
+ tile_mode,
98
+ start_angle.to_f,
99
+ end_angle.to_f,
100
+ matrix_ptr
101
+ )
102
+ raise Error, 'Failed to create sweep gradient shader' if ptr.nil? || ptr.null?
103
+
104
+ new(ptr)
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skia
4
+ class Surface < Base
5
+ attr_reader :width, :height
6
+
7
+ def initialize(ptr, width, height)
8
+ super(ptr, :sk_surface_unref)
9
+ @width = width
10
+ @height = height
11
+ end
12
+
13
+ def self.make_raster(width, height, color_type: :rgba_8888, alpha_type: :premul)
14
+ info = Native::SKImageInfo.new
15
+ info[:width] = width
16
+ info[:height] = height
17
+ info[:colorType] = color_type
18
+ info[:alphaType] = alpha_type
19
+ info[:colorspace] = nil
20
+
21
+ ptr = Native.sk_surface_new_raster(info, 0, nil)
22
+ raise Error, 'Failed to create raster surface' if ptr.nil? || ptr.null?
23
+
24
+ new(ptr, width, height)
25
+ end
26
+
27
+ def self.make_raster_direct(width, height, pixels, row_bytes, color_type: :rgba_8888, alpha_type: :premul)
28
+ info = Native::SKImageInfo.new
29
+ info[:width] = width
30
+ info[:height] = height
31
+ info[:colorType] = color_type
32
+ info[:alphaType] = alpha_type
33
+ info[:colorspace] = nil
34
+
35
+ ptr = Native.sk_surface_new_raster_direct(info, pixels, row_bytes, nil, nil, nil)
36
+ raise Error, 'Failed to create direct raster surface' if ptr.nil? || ptr.null?
37
+
38
+ new(ptr, width, height)
39
+ end
40
+
41
+ def canvas
42
+ @canvas ||= Canvas.new(Native.sk_surface_get_canvas(@ptr))
43
+ end
44
+
45
+ def snapshot
46
+ ptr = Native.sk_surface_new_image_snapshot(@ptr)
47
+ raise Error, 'Failed to create image snapshot' if ptr.nil? || ptr.null?
48
+
49
+ Image.new(ptr)
50
+ end
51
+
52
+ def draw
53
+ yield canvas
54
+ self
55
+ end
56
+
57
+ def encode(format = :png, quality = 100)
58
+ pixmap = Native.sk_pixmap_new
59
+ raise Error, 'Failed to create pixmap' if pixmap.nil? || pixmap.null?
60
+
61
+ begin
62
+ raise Error, 'Failed to peek pixels from surface' unless Native.sk_surface_peek_pixels(@ptr, pixmap)
63
+
64
+ stream = Native.sk_dynamicmemorywstream_new
65
+ raise Error, 'Failed to create stream' if stream.nil? || stream.null?
66
+
67
+ begin
68
+ success = case format
69
+ when :png
70
+ options = Native::SKPngEncoderOptions.new
71
+ options[:fFilterFlags] = :all
72
+ options[:fZLibLevel] = 6
73
+ options[:fComments] = nil
74
+ options[:fICCProfile] = nil
75
+ options[:fICCProfileDescription] = nil
76
+ Native.sk_pngencoder_encode(stream, pixmap, options)
77
+ when :jpeg, :jpg
78
+ options = Native::SKJpegEncoderOptions.new
79
+ options[:fQuality] = quality
80
+ options[:fDownsample] = :downsample_420
81
+ options[:fAlphaOption] = :ignore
82
+ options[:xmpMetadata] = nil
83
+ options[:fICCProfile] = nil
84
+ options[:fICCProfileDescription] = nil
85
+ Native.sk_jpegencoder_encode(stream, pixmap, options)
86
+ when :webp
87
+ options = Native::SKWebpEncoderOptions.new
88
+ options[:fCompression] = :lossy
89
+ options[:fQuality] = quality.to_f
90
+ options[:fICCProfile] = nil
91
+ options[:fICCProfileDescription] = nil
92
+ Native.sk_webpencoder_encode(stream, pixmap, options)
93
+ else
94
+ options = Native::SKPngEncoderOptions.new
95
+ options[:fFilterFlags] = :all
96
+ options[:fZLibLevel] = 6
97
+ options[:fComments] = nil
98
+ options[:fICCProfile] = nil
99
+ options[:fICCProfileDescription] = nil
100
+ Native.sk_pngencoder_encode(stream, pixmap, options)
101
+ end
102
+
103
+ raise Error, "Failed to encode surface as #{format}" unless success
104
+
105
+ data_ptr = Native.sk_dynamicmemorywstream_detach_as_data(stream)
106
+ raise Error, 'Failed to get encoded data' if data_ptr.nil? || data_ptr.null?
107
+
108
+ Data.new(data_ptr)
109
+ ensure
110
+ Native.sk_dynamicmemorywstream_destroy(stream)
111
+ end
112
+ ensure
113
+ Native.sk_pixmap_destructor(pixmap)
114
+ end
115
+ end
116
+
117
+ def save(path, format: nil, quality: 100)
118
+ format ||= detect_format_from_path(path)
119
+ data = encode(format, quality)
120
+ File.binwrite(path, data.to_s)
121
+ self
122
+ end
123
+
124
+ def save_png(path, quality = 100)
125
+ save(path, format: :png, quality: quality)
126
+ end
127
+
128
+ def save_jpeg(path, quality = 80)
129
+ save(path, format: :jpeg, quality: quality)
130
+ end
131
+
132
+ def save_webp(path, quality = 80)
133
+ save(path, format: :webp, quality: quality)
134
+ end
135
+
136
+ private
137
+
138
+ def detect_format_from_path(path)
139
+ case File.extname(path).downcase
140
+ when '.png'
141
+ :png
142
+ when '.jpg', '.jpeg'
143
+ :jpeg
144
+ when '.webp'
145
+ :webp
146
+ else
147
+ :png
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skia
4
+ class Typeface < Base
5
+ WEIGHT_NORMAL = 400
6
+ WEIGHT_BOLD = 700
7
+ WIDTH_NORMAL = 5
8
+
9
+ def initialize(ptr)
10
+ super(ptr, :sk_typeface_unref)
11
+ end
12
+
13
+ def self.from_name(name, weight: WEIGHT_NORMAL, width: WIDTH_NORMAL, slant: :upright)
14
+ style = Native.sk_fontstyle_new(weight, width, slant)
15
+ begin
16
+ ptr = Native.sk_typeface_create_from_name(name, style)
17
+ return nil if ptr.nil? || ptr.null?
18
+
19
+ new(ptr)
20
+ ensure
21
+ Native.sk_fontstyle_delete(style) if style && !style.null?
22
+ end
23
+ end
24
+
25
+ def self.from_file(path, index = 0)
26
+ ptr = Native.sk_typeface_create_from_file(path, index)
27
+ raise FileNotFoundError, "Failed to load typeface: #{path}" if ptr.nil? || ptr.null?
28
+
29
+ new(ptr)
30
+ end
31
+
32
+ def self.default
33
+ ptr = Native.sk_typeface_create_default
34
+ return nil if ptr.nil? || ptr.null?
35
+
36
+ new(ptr)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skia
4
+ VERSION = '0.1.0'
5
+ end
data/lib/skia.rb ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffi'
4
+
5
+ require_relative 'skia/version'
6
+ require_relative 'skia/native'
7
+ require_relative 'skia/base'
8
+ require_relative 'skia/color'
9
+ require_relative 'skia/point'
10
+ require_relative 'skia/rect'
11
+ require_relative 'skia/matrix'
12
+ require_relative 'skia/paint'
13
+ require_relative 'skia/path'
14
+ require_relative 'skia/data'
15
+ require_relative 'skia/image'
16
+ require_relative 'skia/surface'
17
+ require_relative 'skia/canvas'
18
+ require_relative 'skia/typeface'
19
+ require_relative 'skia/font'
20
+ require_relative 'skia/shader'
21
+ require_relative 'skia/document'
22
+ require_relative 'skia/picture'
23
+
24
+ module Skia
25
+ class Error < StandardError; end
26
+ class NullPointerError < Error; end
27
+ class EncodingError < Error; end
28
+ class DecodingError < Error; end
29
+ class FileNotFoundError < Error; end
30
+ class UnsupportedOperationError < Error; end
31
+ end
data/skia-ruby.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/skia/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'skia'
7
+ spec.version = Skia::VERSION
8
+ spec.authors = ['Yudai Takada']
9
+ spec.email = ['t.yudai92@gmail.com']
10
+
11
+ spec.summary = 'Ruby bindings for Google Skia 2D graphics library'
12
+ spec.description = 'Ruby bindings for Skia, providing high-performance 2D graphics capabilities'
13
+ spec.homepage = 'https://github.com/ydah/skia-ruby'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 3.0.0'
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = "#{spec.homepage}/tree/v#{spec.version}"
19
+ spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/v#{spec.version}/CHANGELOG.md"
20
+
21
+ spec.files = Dir.chdir(__dir__) do
22
+ `git ls-files -z`.split("\x0").reject do |f|
23
+ (File.expand_path(f) == __FILE__) ||
24
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile Dockerfile])
25
+ end
26
+ end
27
+ spec.require_paths = ['lib']
28
+
29
+ spec.add_dependency 'ffi', '~> 1.15'
30
+ end