freetype-ruby 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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +175 -0
- data/Rakefile +8 -0
- data/examples/rugl_text.rb +127 -0
- data/examples/stagecraft_text.rb +71 -0
- data/lib/freetype/atlas.rb +127 -0
- data/lib/freetype/bitmap.rb +46 -0
- data/lib/freetype/charset.rb +13 -0
- data/lib/freetype/data/joyo_kanji.txt +1 -0
- data/lib/freetype/errors.rb +20 -0
- data/lib/freetype/face/rasterization.rb +78 -0
- data/lib/freetype/face.rb +186 -0
- data/lib/freetype/glyph.rb +20 -0
- data/lib/freetype/harfbuzz.rb +10 -0
- data/lib/freetype/image.rb +29 -0
- data/lib/freetype/library.rb +95 -0
- data/lib/freetype/metrics.rb +17 -0
- data/lib/freetype/native/structs.rb +73 -0
- data/lib/freetype/native.rb +102 -0
- data/lib/freetype/pool.rb +79 -0
- data/lib/freetype/text_run.rb +120 -0
- data/lib/freetype/version.rb +5 -0
- data/lib/freetype.rb +25 -0
- metadata +89 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FreeType
|
|
4
|
+
module FaceRasterization
|
|
5
|
+
def char_index(character)
|
|
6
|
+
ensure_open!
|
|
7
|
+
Native.FT_Get_Char_Index(pointer, codepoint_for(character))
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def glyph(character, mode: :gray, spread: 8, load_flags: Native::FT_LOAD_DEFAULT)
|
|
11
|
+
glyph_by_id(char_index(character), mode: mode, spread: spread, load_flags: load_flags)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def glyph_by_id(glyph_id, mode: :gray, spread: 8, load_flags: Native::FT_LOAD_DEFAULT)
|
|
15
|
+
ensure_open!
|
|
16
|
+
id = Integer(glyph_id)
|
|
17
|
+
raise RangeError, "glyph id #{id} is outside this face" unless id.between?(0, glyph_count - 1)
|
|
18
|
+
|
|
19
|
+
render_mode = self.class::RENDER_MODES.fetch(mode) do
|
|
20
|
+
raise ArgumentError, "unknown render mode #{mode.inspect}"
|
|
21
|
+
end
|
|
22
|
+
prepare_sdf!(spread) if mode == :sdf
|
|
23
|
+
flags = Integer(load_flags) & ~Native::FT_LOAD_RENDER
|
|
24
|
+
flags |= Native::FT_LOAD_TARGET_MONO if mode == :mono
|
|
25
|
+
Native.check!(Native.FT_Load_Glyph(pointer, id, flags), "FT_Load_Glyph")
|
|
26
|
+
slot_pointer = raw[:glyph]
|
|
27
|
+
Native.check!(Native.FT_Render_Glyph(slot_pointer, render_mode), "FT_Render_Glyph")
|
|
28
|
+
Glyph.new(id: id, slot: Native::GlyphSlotRec.new(slot_pointer))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def kerning(left_glyph_id, right_glyph_id)
|
|
32
|
+
ensure_open!
|
|
33
|
+
vector = Native::Vector.new
|
|
34
|
+
Native.check!(
|
|
35
|
+
Native.FT_Get_Kerning(
|
|
36
|
+
pointer, Integer(left_glyph_id), Integer(right_glyph_id),
|
|
37
|
+
Native::FT_KERNING_DEFAULT, vector.pointer
|
|
38
|
+
),
|
|
39
|
+
"FT_Get_Kerning"
|
|
40
|
+
)
|
|
41
|
+
FixedPoint.from_26_6(vector[:x])
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def line_advance(text)
|
|
45
|
+
previous = nil
|
|
46
|
+
String(text).each_codepoint.sum(0.0) do |codepoint|
|
|
47
|
+
id = char_index(codepoint)
|
|
48
|
+
adjustment = previous ? kerning(previous, id) : 0.0
|
|
49
|
+
previous = id
|
|
50
|
+
adjustment + glyph_by_id(id).advance
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def prepare_sdf!(spread)
|
|
57
|
+
unless (library.version <=> [2, 11, 0]) >= 0
|
|
58
|
+
raise UnsupportedError, "SDF rendering requires FreeType 2.11 or newer (found #{library.version.join('.')})"
|
|
59
|
+
end
|
|
60
|
+
unless spread.is_a?(Integer) && spread.between?(2, 32)
|
|
61
|
+
raise ArgumentError, "spread must be an integer between 2 and 32"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
value = FFI::MemoryPointer.new(:uint)
|
|
65
|
+
value.write_uint(spread)
|
|
66
|
+
Native.check!(Native.FT_Property_Set(library.pointer, "sdf", "spread", value), "FT_Property_Set")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def codepoint_for(character)
|
|
70
|
+
return character if character.is_a?(Integer) && character.between?(0, 0x10FFFF)
|
|
71
|
+
|
|
72
|
+
codepoints = String(character).codepoints
|
|
73
|
+
raise ArgumentError, "expected exactly one Unicode codepoint" unless codepoints.length == 1
|
|
74
|
+
|
|
75
|
+
codepoints.first
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "face/rasterization"
|
|
4
|
+
|
|
5
|
+
module FreeType
|
|
6
|
+
class Face
|
|
7
|
+
include FaceRasterization
|
|
8
|
+
|
|
9
|
+
RENDER_MODES = {
|
|
10
|
+
gray: Native::FT_RENDER_MODE_NORMAL,
|
|
11
|
+
normal: Native::FT_RENDER_MODE_NORMAL,
|
|
12
|
+
mono: Native::FT_RENDER_MODE_MONO,
|
|
13
|
+
sdf: Native::FT_RENDER_MODE_SDF
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
attr_reader :library, :pointer, :source_bytes
|
|
17
|
+
|
|
18
|
+
def initialize(library, source, face_index: 0)
|
|
19
|
+
@library = library
|
|
20
|
+
@state = { closed: false }
|
|
21
|
+
@pointer = create_face(source, face_index)
|
|
22
|
+
@state[:pointer] = @pointer
|
|
23
|
+
ObjectSpace.define_finalizer(self, self.class.finalizer(@state))
|
|
24
|
+
rescue StandardError
|
|
25
|
+
@memory = nil
|
|
26
|
+
@source_bytes = nil
|
|
27
|
+
raise
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.finalizer(state)
|
|
31
|
+
proc do
|
|
32
|
+
next if state[:closed] || !state[:pointer]
|
|
33
|
+
|
|
34
|
+
Native.FT_Done_Face(state[:pointer])
|
|
35
|
+
state[:closed] = true
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def close
|
|
40
|
+
return if closed?
|
|
41
|
+
|
|
42
|
+
Native.check!(Native.FT_Done_Face(pointer), "FT_Done_Face")
|
|
43
|
+
@state[:closed] = true
|
|
44
|
+
library.__send__(:unregister, self)
|
|
45
|
+
@memory = nil
|
|
46
|
+
@source_bytes = nil
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def closed?
|
|
51
|
+
@state[:closed]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def raw
|
|
55
|
+
ensure_open!
|
|
56
|
+
Native::FaceRec.new(pointer)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def family_name
|
|
60
|
+
read_string(raw[:family_name])
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def style_name
|
|
64
|
+
read_string(raw[:style_name])
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def units_per_em
|
|
68
|
+
raw[:units_per_em]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def glyph_count
|
|
72
|
+
raw[:num_glyphs]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def scalable?
|
|
76
|
+
(raw[:face_flags] & Native::FT_FACE_FLAG_SCALABLE) != 0
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def has_kerning?
|
|
80
|
+
(raw[:face_flags] & Native::FT_FACE_FLAG_KERNING) != 0
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def set_pixel_size(height, width: 0)
|
|
84
|
+
ensure_open!
|
|
85
|
+
validate_positive_integer!(height, "height")
|
|
86
|
+
raise ArgumentError, "width must be a non-negative integer" unless width.is_a?(Integer) && width >= 0
|
|
87
|
+
|
|
88
|
+
Native.check!(Native.FT_Set_Pixel_Sizes(pointer, width, height), "FT_Set_Pixel_Sizes")
|
|
89
|
+
self
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def set_char_size(pt:, dpi: 96, width: 0, horizontal_dpi: dpi)
|
|
93
|
+
ensure_open!
|
|
94
|
+
validate_positive_number!(pt, "pt")
|
|
95
|
+
validate_positive_integer!(dpi, "dpi")
|
|
96
|
+
raise ArgumentError, "width must be non-negative" unless width.is_a?(Numeric) && width >= 0
|
|
97
|
+
validate_positive_integer!(horizontal_dpi, "horizontal_dpi")
|
|
98
|
+
|
|
99
|
+
Native.check!(
|
|
100
|
+
Native.FT_Set_Char_Size(pointer, (width * 64).round, (pt * 64).round, horizontal_dpi, dpi),
|
|
101
|
+
"FT_Set_Char_Size"
|
|
102
|
+
)
|
|
103
|
+
self
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def select_charmap(encoding = :unicode)
|
|
107
|
+
ensure_open!
|
|
108
|
+
native_encoding = encoding == :unicode ? Native::FT_ENCODING_UNICODE : Integer(encoding)
|
|
109
|
+
Native.check!(Native.FT_Select_Charmap(pointer, native_encoding), "FT_Select_Charmap")
|
|
110
|
+
self
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def metrics
|
|
114
|
+
size_pointer = raw[:size]
|
|
115
|
+
raise StateError, "set a face size before reading metrics" if size_pointer.null?
|
|
116
|
+
|
|
117
|
+
native = Native::SizeRec.new(size_pointer)[:metrics]
|
|
118
|
+
Metrics.new(
|
|
119
|
+
ascender: FixedPoint.from_26_6(native[:ascender]),
|
|
120
|
+
descender: FixedPoint.from_26_6(native[:descender]),
|
|
121
|
+
line_height: FixedPoint.from_26_6(native[:height]),
|
|
122
|
+
max_advance: FixedPoint.from_26_6(native[:max_advance])
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def native_state
|
|
129
|
+
@state
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def create_face(source, face_index)
|
|
133
|
+
output = FFI::MemoryPointer.new(:pointer)
|
|
134
|
+
index = Integer(face_index)
|
|
135
|
+
if path_source?(source)
|
|
136
|
+
path = source.respond_to?(:to_path) ? source.to_path : source
|
|
137
|
+
Native.check!(Native.FT_New_Face(library.pointer, path.to_s, index, output), "FT_New_Face")
|
|
138
|
+
else
|
|
139
|
+
retain_memory_source(source)
|
|
140
|
+
Native.check!(
|
|
141
|
+
Native.FT_New_Memory_Face(library.pointer, @memory, @source_bytes.bytesize, index, output),
|
|
142
|
+
"FT_New_Memory_Face"
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
output.read_pointer
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def path_source?(source)
|
|
149
|
+
return true if source.respond_to?(:to_path)
|
|
150
|
+
return false unless source.is_a?(String)
|
|
151
|
+
return false if source.encoding == Encoding::BINARY || source.include?("\0")
|
|
152
|
+
return true if File.file?(source)
|
|
153
|
+
|
|
154
|
+
source.match?(/\.(?:ttf|ttc|otf|otc|woff2?)\z/i) || source.include?(File::SEPARATOR)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def retain_memory_source(source)
|
|
158
|
+
@source_bytes = String(source).dup.force_encoding(Encoding::BINARY).freeze
|
|
159
|
+
raise ArgumentError, "font bytes cannot be empty" if @source_bytes.empty?
|
|
160
|
+
|
|
161
|
+
@memory = FFI::MemoryPointer.new(:uchar, @source_bytes.bytesize)
|
|
162
|
+
@memory.put_bytes(0, @source_bytes)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def read_string(native_pointer)
|
|
166
|
+
native_pointer.null? ? nil : native_pointer.read_string.force_encoding(Encoding::UTF_8)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def validate_positive_number!(value, name)
|
|
170
|
+
return if value.is_a?(Numeric) && value.positive?
|
|
171
|
+
|
|
172
|
+
raise ArgumentError, "#{name} must be positive"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def validate_positive_integer!(value, name)
|
|
176
|
+
return if value.is_a?(Integer) && value.positive?
|
|
177
|
+
|
|
178
|
+
raise ArgumentError, "#{name} must be a positive integer"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def ensure_open!
|
|
182
|
+
raise ClosedError, "face is closed" if closed?
|
|
183
|
+
raise ClosedError, "library is closed" if library.closed?
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FreeType
|
|
4
|
+
class Glyph
|
|
5
|
+
attr_reader :id, :bitmap, :bearing_x, :bearing_y, :advance, :width, :height, :raw
|
|
6
|
+
|
|
7
|
+
def initialize(id:, slot:)
|
|
8
|
+
native_bitmap = slot[:bitmap]
|
|
9
|
+
@id = id
|
|
10
|
+
@width = native_bitmap[:width]
|
|
11
|
+
@height = native_bitmap[:rows]
|
|
12
|
+
@bearing_x = slot[:bitmap_left].to_f
|
|
13
|
+
@bearing_y = slot[:bitmap_top].to_f
|
|
14
|
+
@advance = FixedPoint.from_26_6(slot[:advance][:x])
|
|
15
|
+
@raw = slot
|
|
16
|
+
@bitmap = Image.build(width: width, height: height, data: BitmapConverter.copy(native_bitmap))
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "texel"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
# Texel is an optional integration.
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module FreeType
|
|
10
|
+
module Image
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def build(width:, height:, data:)
|
|
14
|
+
bytes = String(data).dup.force_encoding(Encoding::BINARY).freeze
|
|
15
|
+
if defined?(Texel::Image) && width.positive? && height.positive?
|
|
16
|
+
return Texel::Image.new(
|
|
17
|
+
width: width, height: height, channels: 1, dtype: :u8,
|
|
18
|
+
color_space: :linear, data: bytes
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
{ width: width, height: height, data: bytes }.freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def data(image)
|
|
26
|
+
image.respond_to?(:data) ? image.data : image.fetch(:data)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module FreeType
|
|
6
|
+
class Library
|
|
7
|
+
include Enumerable
|
|
8
|
+
|
|
9
|
+
attr_reader :pointer
|
|
10
|
+
|
|
11
|
+
def initialize
|
|
12
|
+
output = FFI::MemoryPointer.new(:pointer)
|
|
13
|
+
Native.check!(Native.FT_Init_FreeType(output), "FT_Init_FreeType")
|
|
14
|
+
@pointer = output.read_pointer
|
|
15
|
+
@faces = Set.new
|
|
16
|
+
@state = { pointer: @pointer, faces: [], closed: false }
|
|
17
|
+
ObjectSpace.define_finalizer(self, self.class.finalizer(@state))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.open
|
|
21
|
+
library = new
|
|
22
|
+
return library unless block_given?
|
|
23
|
+
|
|
24
|
+
begin
|
|
25
|
+
yield library
|
|
26
|
+
ensure
|
|
27
|
+
library.close
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.finalizer(state)
|
|
32
|
+
proc do
|
|
33
|
+
next if state[:closed]
|
|
34
|
+
|
|
35
|
+
state[:faces].each do |face_state|
|
|
36
|
+
next if face_state[:closed]
|
|
37
|
+
|
|
38
|
+
Native.FT_Done_Face(face_state[:pointer])
|
|
39
|
+
face_state[:closed] = true
|
|
40
|
+
end
|
|
41
|
+
Native.FT_Done_FreeType(state[:pointer])
|
|
42
|
+
state[:closed] = true
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def face(source, face_index: 0)
|
|
47
|
+
ensure_open!
|
|
48
|
+
created = Face.new(self, source, face_index: face_index)
|
|
49
|
+
@faces.add(created)
|
|
50
|
+
@state[:faces] << created.__send__(:native_state)
|
|
51
|
+
return created unless block_given?
|
|
52
|
+
|
|
53
|
+
begin
|
|
54
|
+
yield created
|
|
55
|
+
ensure
|
|
56
|
+
created.close
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def each(&block)
|
|
61
|
+
@faces.each(&block)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def version
|
|
65
|
+
ensure_open!
|
|
66
|
+
values = Array.new(3) { FFI::MemoryPointer.new(:int) }
|
|
67
|
+
Native.FT_Library_Version(pointer, *values)
|
|
68
|
+
values.map(&:read_int).freeze
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def close
|
|
72
|
+
return if closed?
|
|
73
|
+
|
|
74
|
+
@faces.to_a.each(&:close)
|
|
75
|
+
Native.check!(Native.FT_Done_FreeType(pointer), "FT_Done_FreeType")
|
|
76
|
+
@state[:closed] = true
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def closed?
|
|
81
|
+
@state[:closed]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def unregister(face)
|
|
87
|
+
@faces.delete(face)
|
|
88
|
+
@state[:faces].delete(face.__send__(:native_state))
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def ensure_open!
|
|
92
|
+
raise ClosedError, "library is closed" if closed?
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FreeType
|
|
4
|
+
Metrics = Data.define(:ascender, :descender, :line_height, :max_advance)
|
|
5
|
+
|
|
6
|
+
module FixedPoint
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def from_26_6(value)
|
|
10
|
+
Integer(value) / 64.0
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def from_16_16(value)
|
|
14
|
+
Integer(value) / 65_536.0
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FreeType
|
|
4
|
+
module Native
|
|
5
|
+
module Structs
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def define(name, fields)
|
|
9
|
+
klass = Class.new(FFI::Struct)
|
|
10
|
+
klass.layout(*fields.flatten)
|
|
11
|
+
Native.const_set(name, klass)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def define_all
|
|
15
|
+
define(:Generic, data: :pointer, finalizer: :pointer)
|
|
16
|
+
define(:Vector, x: :long, y: :long)
|
|
17
|
+
define(:BBox, x_min: :long, y_min: :long, x_max: :long, y_max: :long)
|
|
18
|
+
define(:ListRec, head: :pointer, tail: :pointer)
|
|
19
|
+
define(:BitmapSize, height: :short, width: :short, size: :long, x_ppem: :long, y_ppem: :long)
|
|
20
|
+
define(:GlyphMetrics,
|
|
21
|
+
width: :long, height: :long,
|
|
22
|
+
hori_bearing_x: :long, hori_bearing_y: :long, hori_advance: :long,
|
|
23
|
+
vert_bearing_x: :long, vert_bearing_y: :long, vert_advance: :long)
|
|
24
|
+
define(:Bitmap,
|
|
25
|
+
rows: :uint, width: :uint, pitch: :int, buffer: :pointer,
|
|
26
|
+
num_grays: :ushort, pixel_mode: :uchar, palette_mode: :uchar, palette: :pointer)
|
|
27
|
+
define(:Outline,
|
|
28
|
+
n_contours: :ushort, n_points: :ushort, points: :pointer,
|
|
29
|
+
tags: :pointer, contours: :pointer, flags: :int)
|
|
30
|
+
define(:SizeMetrics,
|
|
31
|
+
x_ppem: :ushort, y_ppem: :ushort, x_scale: :long, y_scale: :long,
|
|
32
|
+
ascender: :long, descender: :long, height: :long, max_advance: :long)
|
|
33
|
+
define(:SizeRec,
|
|
34
|
+
face: :pointer, generic: Generic.by_value, metrics: SizeMetrics.by_value, internal: :pointer)
|
|
35
|
+
define(:FaceRec,
|
|
36
|
+
num_faces: :long, face_index: :long, face_flags: :long, style_flags: :long,
|
|
37
|
+
num_glyphs: :long, family_name: :pointer, style_name: :pointer,
|
|
38
|
+
num_fixed_sizes: :int, available_sizes: :pointer, num_charmaps: :int, charmaps: :pointer,
|
|
39
|
+
generic: Generic.by_value, bbox: BBox.by_value, units_per_em: :ushort,
|
|
40
|
+
ascender: :short, descender: :short, height: :short,
|
|
41
|
+
max_advance_width: :short, max_advance_height: :short,
|
|
42
|
+
underline_position: :short, underline_thickness: :short,
|
|
43
|
+
glyph: :pointer, size: :pointer, charmap: :pointer,
|
|
44
|
+
driver: :pointer, memory: :pointer, stream: :pointer,
|
|
45
|
+
sizes_list: ListRec.by_value, autohint: Generic.by_value,
|
|
46
|
+
extensions: :pointer, internal: :pointer)
|
|
47
|
+
define(:GlyphSlotRec,
|
|
48
|
+
library: :pointer, face: :pointer, next: :pointer, glyph_index: :uint,
|
|
49
|
+
generic: Generic.by_value, metrics: GlyphMetrics.by_value,
|
|
50
|
+
linear_hori_advance: :long, linear_vert_advance: :long, advance: Vector.by_value,
|
|
51
|
+
format: :uint, bitmap: Bitmap.by_value, bitmap_left: :int, bitmap_top: :int,
|
|
52
|
+
outline: Outline.by_value, num_subglyphs: :uint, subglyphs: :pointer,
|
|
53
|
+
control_data: :pointer, control_len: :long, lsb_delta: :long, rsb_delta: :long,
|
|
54
|
+
other: :pointer, internal: :pointer)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
Structs.define_all
|
|
59
|
+
|
|
60
|
+
FT_Generic = Generic
|
|
61
|
+
FT_Vector = Vector
|
|
62
|
+
FT_BBox = BBox
|
|
63
|
+
FT_ListRec = ListRec
|
|
64
|
+
FT_Bitmap_Size = BitmapSize
|
|
65
|
+
FT_Glyph_Metrics = GlyphMetrics
|
|
66
|
+
FT_Bitmap = Bitmap
|
|
67
|
+
FT_Outline = Outline
|
|
68
|
+
FT_Size_Metrics = SizeMetrics
|
|
69
|
+
FT_SizeRec = SizeRec
|
|
70
|
+
FT_FaceRec = FaceRec
|
|
71
|
+
FT_GlyphSlotRec = GlyphSlotRec
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
|
|
5
|
+
module FreeType
|
|
6
|
+
module Native
|
|
7
|
+
extend FFI::Library
|
|
8
|
+
|
|
9
|
+
library_override = ENV["FREETYPE_LIBRARY"]
|
|
10
|
+
library_override = nil if library_override&.empty?
|
|
11
|
+
candidates = [library_override, "freetype", "libfreetype.so.6", "libfreetype.6.dylib"].compact
|
|
12
|
+
ffi_lib candidates
|
|
13
|
+
|
|
14
|
+
FT_LOAD_DEFAULT = 0
|
|
15
|
+
FT_LOAD_NO_HINTING = 1 << 1
|
|
16
|
+
FT_LOAD_RENDER = 1 << 2
|
|
17
|
+
FT_LOAD_TARGET_LIGHT = 1 << 16
|
|
18
|
+
FT_LOAD_TARGET_MONO = 2 << 16
|
|
19
|
+
|
|
20
|
+
FT_RENDER_MODE_NORMAL = 0
|
|
21
|
+
FT_RENDER_MODE_LIGHT = 1
|
|
22
|
+
FT_RENDER_MODE_MONO = 2
|
|
23
|
+
FT_RENDER_MODE_SDF = 5
|
|
24
|
+
|
|
25
|
+
FT_PIXEL_MODE_NONE = 0
|
|
26
|
+
FT_PIXEL_MODE_MONO = 1
|
|
27
|
+
FT_PIXEL_MODE_GRAY = 2
|
|
28
|
+
FT_KERNING_DEFAULT = 0
|
|
29
|
+
FT_FACE_FLAG_SCALABLE = 1 << 0
|
|
30
|
+
FT_FACE_FLAG_KERNING = 1 << 6
|
|
31
|
+
FT_ENCODING_UNICODE = 0x756E6963
|
|
32
|
+
|
|
33
|
+
ERROR_NAMES = {
|
|
34
|
+
0x00 => :Ok, 0x01 => :Cannot_Open_Resource, 0x02 => :Unknown_File_Format,
|
|
35
|
+
0x03 => :Invalid_File_Format, 0x04 => :Invalid_Version, 0x05 => :Lower_Module_Version,
|
|
36
|
+
0x06 => :Invalid_Argument, 0x07 => :Unimplemented_Feature, 0x08 => :Invalid_Table,
|
|
37
|
+
0x09 => :Invalid_Offset, 0x0A => :Array_Too_Large, 0x0B => :Missing_Module,
|
|
38
|
+
0x0C => :Missing_Property, 0x10 => :Invalid_Glyph_Index, 0x11 => :Invalid_Character_Code,
|
|
39
|
+
0x12 => :Invalid_Glyph_Format, 0x13 => :Cannot_Render_Glyph, 0x14 => :Invalid_Outline,
|
|
40
|
+
0x15 => :Invalid_Composite, 0x16 => :Too_Many_Hints, 0x17 => :Invalid_Pixel_Size,
|
|
41
|
+
0x18 => :Invalid_SVG_Document, 0x20 => :Invalid_Handle, 0x21 => :Invalid_Library_Handle,
|
|
42
|
+
0x22 => :Invalid_Driver_Handle, 0x23 => :Invalid_Face_Handle, 0x24 => :Invalid_Size_Handle,
|
|
43
|
+
0x25 => :Invalid_Slot_Handle, 0x26 => :Invalid_CharMap_Handle, 0x27 => :Invalid_Cache_Handle,
|
|
44
|
+
0x28 => :Invalid_Stream_Handle, 0x30 => :Too_Many_Drivers, 0x31 => :Too_Many_Extensions,
|
|
45
|
+
0x40 => :Out_Of_Memory, 0x41 => :Unlisted_Object, 0x51 => :Cannot_Open_Stream,
|
|
46
|
+
0x52 => :Invalid_Stream_Seek, 0x53 => :Invalid_Stream_Skip, 0x54 => :Invalid_Stream_Read,
|
|
47
|
+
0x55 => :Invalid_Stream_Operation, 0x56 => :Invalid_Frame_Operation,
|
|
48
|
+
0x57 => :Nested_Frame_Access, 0x58 => :Invalid_Frame_Read, 0x60 => :Raster_Uninitialized,
|
|
49
|
+
0x61 => :Raster_Corrupted, 0x62 => :Raster_Overflow, 0x63 => :Raster_Negative_Height,
|
|
50
|
+
0x70 => :Too_Many_Caches, 0x80 => :Invalid_Opcode, 0x81 => :Too_Few_Arguments,
|
|
51
|
+
0x82 => :Stack_Overflow, 0x83 => :Code_Overflow, 0x84 => :Bad_Argument,
|
|
52
|
+
0x85 => :Divide_By_Zero, 0x86 => :Invalid_Reference, 0x87 => :Debug_OpCode,
|
|
53
|
+
0x88 => :ENDF_In_Exec_Stream, 0x89 => :Nested_DEFS, 0x8A => :Invalid_CodeRange,
|
|
54
|
+
0x8B => :Execution_Too_Long, 0x8C => :Too_Many_Function_Defs,
|
|
55
|
+
0x8D => :Too_Many_Instruction_Defs, 0x8E => :Table_Missing, 0x8F => :Horiz_Header_Missing,
|
|
56
|
+
0x90 => :Locations_Missing, 0x91 => :Name_Table_Missing, 0x92 => :CMap_Table_Missing,
|
|
57
|
+
0x93 => :Hmtx_Table_Missing, 0x94 => :Post_Table_Missing, 0x95 => :Invalid_Horiz_Metrics,
|
|
58
|
+
0x96 => :Invalid_CharMap_Format, 0x97 => :Invalid_PPem, 0x98 => :Invalid_Vert_Metrics,
|
|
59
|
+
0x99 => :Could_Not_Find_Context, 0x9A => :Invalid_Post_Table_Format,
|
|
60
|
+
0x9B => :Invalid_Post_Table, 0x9C => :DEF_In_Glyf_Bytecode, 0x9D => :Missing_Bitmap,
|
|
61
|
+
0x9E => :Missing_SVG_Hooks, 0xA0 => :Syntax_Error, 0xA1 => :Stack_Underflow,
|
|
62
|
+
0xA2 => :Ignore, 0xA3 => :No_Unicode_Glyph_Name, 0xA4 => :Glyph_Too_Big,
|
|
63
|
+
0xB0 => :Missing_Startfont_Field, 0xB1 => :Missing_Font_Field, 0xB2 => :Missing_Size_Field,
|
|
64
|
+
0xB3 => :Missing_Fontboundingbox_Field, 0xB4 => :Missing_Chars_Field,
|
|
65
|
+
0xB5 => :Missing_Startchar_Field, 0xB6 => :Missing_Encoding_Field,
|
|
66
|
+
0xB7 => :Missing_Bbx_Field, 0xB8 => :Bbx_Too_Big, 0xB9 => :Corrupted_Font_Header,
|
|
67
|
+
0xBA => :Corrupted_Font_Glyphs
|
|
68
|
+
}.freeze
|
|
69
|
+
ERROR_CODES = ERROR_NAMES.invert.freeze
|
|
70
|
+
FT_ERRORS = enum(
|
|
71
|
+
:ft_error,
|
|
72
|
+
ERROR_NAMES.flat_map { |code, name| [:"FT_Err_#{name}", code] }
|
|
73
|
+
)
|
|
74
|
+
ERROR_NAMES.each { |code, name| const_set(:"FT_Err_#{name}", code) }
|
|
75
|
+
|
|
76
|
+
require_relative "native/structs"
|
|
77
|
+
|
|
78
|
+
attach_function :FT_Init_FreeType, [:pointer], :int
|
|
79
|
+
attach_function :FT_Done_FreeType, [:pointer], :int
|
|
80
|
+
attach_function :FT_New_Face, %i[pointer string long pointer], :int
|
|
81
|
+
attach_function :FT_New_Memory_Face, %i[pointer pointer long long pointer], :int
|
|
82
|
+
attach_function :FT_Done_Face, [:pointer], :int
|
|
83
|
+
attach_function :FT_Set_Pixel_Sizes, %i[pointer uint uint], :int
|
|
84
|
+
attach_function :FT_Set_Char_Size, %i[pointer long long uint uint], :int
|
|
85
|
+
attach_function :FT_Get_Char_Index, %i[pointer ulong], :uint
|
|
86
|
+
attach_function :FT_Load_Glyph, %i[pointer uint int32], :int
|
|
87
|
+
attach_function :FT_Load_Char, %i[pointer ulong int32], :int
|
|
88
|
+
attach_function :FT_Render_Glyph, %i[pointer int], :int
|
|
89
|
+
attach_function :FT_Get_Kerning, %i[pointer uint uint uint pointer], :int
|
|
90
|
+
attach_function :FT_Select_Charmap, %i[pointer int], :int
|
|
91
|
+
attach_function :FT_Library_Version, %i[pointer pointer pointer pointer], :void
|
|
92
|
+
attach_function :FT_Property_Set, %i[pointer string string pointer], :int
|
|
93
|
+
|
|
94
|
+
module_function
|
|
95
|
+
|
|
96
|
+
def check!(code, operation = nil)
|
|
97
|
+
return if code.zero?
|
|
98
|
+
|
|
99
|
+
raise NativeError.new(code, operation: operation)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FreeType
|
|
4
|
+
class Pool
|
|
5
|
+
def initialize
|
|
6
|
+
@mutex = Mutex.new
|
|
7
|
+
@states = {}
|
|
8
|
+
@closed = false
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.open
|
|
12
|
+
pool = new
|
|
13
|
+
return pool unless block_given?
|
|
14
|
+
|
|
15
|
+
begin
|
|
16
|
+
yield pool
|
|
17
|
+
ensure
|
|
18
|
+
pool.close
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def library
|
|
23
|
+
state.fetch(:library)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def face(source, face_index: 0)
|
|
27
|
+
current = state
|
|
28
|
+
key = source_key(source, face_index)
|
|
29
|
+
current.fetch(:faces)[key] ||= current.fetch(:library).face(source, face_index: face_index)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def with_face(source, face_index: 0)
|
|
33
|
+
yield face(source, face_index: face_index)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def clear
|
|
37
|
+
thread = Thread.current
|
|
38
|
+
current = @mutex.synchronize { @states.delete(thread) }
|
|
39
|
+
current&.fetch(:library)&.close
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def close
|
|
44
|
+
states = @mutex.synchronize do
|
|
45
|
+
return if @closed
|
|
46
|
+
|
|
47
|
+
@closed = true
|
|
48
|
+
previous = @states.values
|
|
49
|
+
@states = {}
|
|
50
|
+
previous
|
|
51
|
+
end
|
|
52
|
+
states.each { |current| current.fetch(:library).close }
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def closed?
|
|
57
|
+
@mutex.synchronize { @closed }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def state
|
|
63
|
+
@mutex.synchronize do
|
|
64
|
+
raise ClosedError, "pool is closed" if @closed
|
|
65
|
+
|
|
66
|
+
@states[Thread.current] ||= { library: Library.new, faces: {} }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def source_key(source, face_index)
|
|
71
|
+
path = source.respond_to?(:to_path) ? source.to_path : source
|
|
72
|
+
if path.is_a?(String) && File.file?(path)
|
|
73
|
+
[:path, File.expand_path(path), Integer(face_index)]
|
|
74
|
+
else
|
|
75
|
+
[:memory, source.object_id, Integer(face_index)]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|