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.
@@ -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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "harfbuzz"
5
+ rescue LoadError => error
6
+ raise LoadError, "freetype/harfbuzz requires the optional harfbuzz-ruby gem (#{error.message})"
7
+ end
8
+
9
+ require_relative "../freetype"
10
+ require_relative "text_run"
@@ -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