skrift 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: fd0c67b57871e673fb91b56bd67172fc1f588b69d37dd777cd89ac65fc04a056
4
+ data.tar.gz: b31c46b7d93b1794e72bb894deb73ca54d63bfc641a7c0e3b9f72d390f97a30b
5
+ SHA512:
6
+ metadata.gz: 87c484791a46b5e82819d12e6c2633bdd12704f92f78e282f1b97cd89931e52827ec4e97e2b2bed52518cda038adad0a77a49105976f6154fa9e85801e04b529
7
+ data.tar.gz: a21dd2bd6a20d90639da4264b186d8bae76675e2d9ef4d44a1438d40a4df912396dba0bb3c03aadd58c8518f408e99e3d27bb3a5dae4ff1afc8ad38f238ec308
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in skrift.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
data/LICENSE.txt ADDED
@@ -0,0 +1,18 @@
1
+ ISC License
2
+
3
+ © 2023 Vidar Hokstad
4
+
5
+ Based on libschrift, which is
6
+ © 2019-2022 Thomas Oltmann and contributors
7
+
8
+ Permission to use, copy, modify, and/or distribute this software for any
9
+ purpose with or without fee is hereby granted, provided that the above
10
+ copyright notice and this permission notice appear in all copies.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
13
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
14
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
15
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
16
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
17
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
18
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,150 @@
1
+
2
+ # Skrift
3
+
4
+ Vidar Hokstad <vidar@hokstad.com>
5
+
6
+ This started out as a Ruby port of `libschrift`. If you need
7
+ performance, and don't mind a C dependency, consider that over this.
8
+
9
+ If you're fine with slower rendering (*cache* glyphs after rendering)
10
+ and want *pure Ruby*, consider this gem, with the caveat that the
11
+ rewrite is *new* and likely buggy.
12
+
13
+ ## Name
14
+
15
+ "Skrift" is Norwegian for "text", "writing", or "scripture", and so a
16
+ close cognate of "Schrift". Since I'm Norwegian, it seemed like an
17
+ appropriate way to set this apart from `libschrift` and ensure that if
18
+ anyone want to do a gem directly wrapping the C librarly the name remains
19
+ available.
20
+
21
+ ## License
22
+
23
+ While it's rewritten in Ruby and changed reasonably since, given it owes
24
+ so much to `libschrift`, I decided to stick with the same license (the
25
+ ISC License), since it's very permissive. See `LICENSE.md`
26
+
27
+ ## This code is opinionated
28
+
29
+ Firstly, the choice to start by rewriting `libschrift` is because that
30
+ library is an excellent demonstration of a minimalist feature set and
31
+ compact code that I wanted.
32
+
33
+ However, on top of the structure inherited largely from `libschrift`,
34
+ while working on this code, I've formed my own opinions on it which
35
+ applies to *this library*, and which the author of `libschrift` may or
36
+ may not agree with. These are *my responsibility*:
37
+
38
+ * Small code size is a virtue as long as it improves rather than hinders
39
+ understanding. Any feature will be weighed against complexity cost.
40
+
41
+ * Features that add a lot of complexity may be better written as a
42
+ separate library (I will happily work with you to ensure it's easy for
43
+ users to combine your library with `Skrift`)
44
+
45
+ * Hinting is predominantly important with low resolution. With the trend
46
+ firmly being towards 4K or 8K displays, putting a lot of effort into
47
+ hinting is pointless. I will *consider* hinting if someone wants to
48
+ contribute hinting code, but not at the cost of a lot of complexity (
49
+ unless you build it as a standalone extension)
50
+
51
+ * The current algo uses anti-aliasing. I will *consider* adding support
52
+ for monochrome rendering for the same reason as above, but currently, AA
53
+ is still *necessary* for best possible results at small sizes, at least
54
+ on FHD displays, so since it's already here, I'll *keep* the AA
55
+ support as long as lower resolution displays are still around.
56
+
57
+ * Feature "completeness" for the sake of completeness are not of interest.
58
+ E.g. I have no interest in parsing the parts of the TTF or OTF formats
59
+ this library won't use (but if you write a *compact*, well written
60
+ TTF/ OTF parser in pure Ruby, I *might* consider tearing out the
61
+ font parsing from this gem if using yours simplifies the `Skrift` code)
62
+
63
+ * Idiomatic Ruby is favoured over maximising efficiency (but
64
+ pathologically low performance is not good - I'm open to changes)
65
+
66
+ * Lowering coupling is favoured (be it for testing, or ease of improving
67
+ the code), but architecture acrobatics should be avoided. That is,
68
+ make it *possible* to test or use individual stages of the rendering
69
+ pipeline, but don't force library users to care - setup should be
70
+ minimal, and defaults sane. Factories and abstract interfaces should
71
+ stay in Java or be used to scare small children, not be found in
72
+ Ruby code.
73
+
74
+
75
+ ## Contributions and Potential Improvements
76
+
77
+ Contributions are welcome, keeping the above in mind. If your
78
+ contributions are potentially unrelated to the specific purpose of this
79
+ library, I might propose you put them in a separate gem instead, and
80
+ might then offer to help create easy integration points so your code can
81
+ safely extent this library if a user chooses to use both.
82
+
83
+ Some possible areas for extension, and my current thoughts on them
84
+ (*talk to me* if you want to work on something)
85
+
86
+ ### X11 or Wayland (or Windows, or Mac) integration
87
+
88
+ No.
89
+
90
+ I will happily ensure there necessary APIs are there so that you can
91
+ *wrap* or integrate with `Skrift`, or so that you can render to something
92
+ that makes it convenient. E.g. the current rendering to 8 bit
93
+ greyscale already makes `Skrift` (thanks to `libschrift` doing this from
94
+ the start) integrate easily with `XRender` for X11 support.
95
+
96
+ I will not, however, put platform specific code in `Skrift` itself (I
97
+ *will* accept code well-written code to make use of "platform specific"
98
+ data *from the font files*, however, because they can be useful on other
99
+ platforms)
100
+
101
+ I use this code for rendering to `X11` myself. It does not require
102
+ pushing platform specific code into this library.
103
+
104
+
105
+ ### Performance
106
+
107
+ Performance improvements are welcome *but not if they add a lot of
108
+ complexity*. C-extensions or similar will not be accepted - if you
109
+ want a C dependency, just use `libschrift`.
110
+
111
+ If you have a suggestion that involves using C in a limited way to speed
112
+ up specifics, I'd suggest providing a *separate* gem to replace/extend
113
+ the appropriate code the same way `oily_png` provides code to speed up
114
+ `chunky_png`. I'm happy to discuss specifics.
115
+
116
+ ### Grid snapping
117
+
118
+ Where I'm *most likely* to consider a hinter is a *limited* hinter
119
+ to dynamically rescale glyphs to allow using a variable spaced font
120
+ snapped to a grid for e.g. terminal use in a somewhat intelligent way (I
121
+ don't expect this is likely to look good, but I'm open to be convinced
122
+ even if it only works on some fonts, though it might well be better as a
123
+ standalone conversion tool unless it works well on a broad range of
124
+ fonts)
125
+
126
+ ### Transformations
127
+
128
+ The current code already applies linear affine transformations to the
129
+ glyphs. I'd be supportive of *compact* contributions to make it easier to
130
+ apply a broader set of transforms during rendering to allow for more
131
+ flexible layout. Please discuss API first if you want anything merged, or
132
+ if you just want stable extension points to do this in a separate
133
+ library.
134
+
135
+ ### Outlines
136
+
137
+ You can currently extract outlines, and so can of course replace the
138
+ rendering stage and render unfilled outlines on your own. That said, I'd
139
+ be supportive both of low complexity changes to render the fonts as
140
+ outlines *and* of low complexity changes to "grow" outlines (e.g. to
141
+ be able to render an outline in one colour and a filled version in the
142
+ desired size in another colour - you can't do this by just scaling). The
143
+ code that'd need to change to replace the rasteriser is tiny.
144
+
145
+ ### Text Layout
146
+
147
+ Specifically, *text layout* is perhaps out of scope of this library, but
148
+ I'm happy to discuss it - if nothing else I'd be supportive of an
149
+ extension in a separate gem and/or ensuring the basics is well
150
+ supported.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,345 @@
1
+ class Font
2
+ # TrueType, TrueType, OpenType
3
+ FILE_MAGIC = ["\0\1\0\0", "true", "OTTO"]
4
+
5
+ attr_reader :memory, :units_per_em
6
+
7
+ def initialize(memory)
8
+ @memory = memory
9
+ raise "Unsupported format (magic value: #{at(0,4).inspect})" if !FILE_MAGIC.member?(at(0,4))
10
+ head = reqtable("head")
11
+ @units_per_em = getu16(head + 18)
12
+ @loca_format = geti16(head + 50)
13
+ hhea = reqtable("hhea")
14
+ @num_long_hmtx = getu16(hhea + 34)
15
+ end
16
+
17
+ def Font.load(filename) # loadfile, 103
18
+ memory = File.read(filename).force_encoding("ASCII-8BIT")
19
+ Font.new(memory)
20
+ end
21
+
22
+ def at(offset, len=1)
23
+ raise "Out of bounds #{offset} / len #{len} (max: #{@memory.size})" if offset.to_i + len.to_i >= @memory.size
24
+ @memory[offset..(offset+len-1)]
25
+ end
26
+
27
+ def getu8(offset); at(offset).ord; end
28
+ def geti8(offset); at(offset).unpack1("c"); end
29
+ def getu16(offset); at(offset,2).unpack1("S>"); end
30
+ def geti16(offset); at(offset,2).unpack1("s>"); end
31
+ def getu32(offset); at(offset,4).unpack1("N"); end
32
+
33
+ def tables
34
+ @tables ||= Hash[*
35
+ getu16(4).times.map {|t| [at(t*16 + 12,4),getu32(t*16 + 20)] }.flatten
36
+ ]
37
+ end
38
+
39
+ def reqtable(tag); tables[tag] or raise "Unable to get table '#{tag}'"; end
40
+ def gettable(tag); tables[tag]; end
41
+
42
+ def glyph_bbox(outline)
43
+ box = at(outline+2, 8).unpack("s>*")
44
+ raise "Broken bbox #{box.inspect}" if box[2] < box[0] || box[3] < box[1]
45
+ return box
46
+ end
47
+
48
+ # Returns the offset into the font that the glyph's outline is stored at
49
+ def outline_offset(glyph) # 806
50
+ loca = reqtable("loca")
51
+ glyf = reqtable("glyf")
52
+ if @loca_format == 0
53
+ base = loca + 2 * glyph
54
+ this = 2 * getu16(base)
55
+ next_ = 2 * getu16(base + 2)
56
+ else
57
+ this, next_ = at(loca + 4 * glyph,8).unpack("NN")
58
+ end
59
+ return this == next_ ? nil : glyf + this
60
+ end
61
+
62
+ def each_cmap_entry
63
+ cmap = reqtable("cmap")
64
+ getu16(cmap + 2).times do |idx|
65
+ entry = cmap + 4 + idx * 8
66
+ type = getu16(entry) * 0100 + getu16(entry + 2)
67
+ table = cmap + getu32(entry + 4)
68
+ format = getu16(table)
69
+ yield(type, table, format)
70
+ end
71
+ end
72
+
73
+ # Maps unicode code points to glyph indices
74
+ def glyph_id(char_code)
75
+ each_cmap_entry do |type, table, format|
76
+ if (type == 0004 || type == 0312)
77
+ return cmap_fmt12_13(table, char_code, 12) if format == 12
78
+ return nil
79
+ end
80
+ end
81
+
82
+ # If no full repertoire cmap was found, try looking for a Unicode BMP map
83
+ each_cmap_entry do |type, table, format|
84
+ if type == 0003 || type == 0301
85
+ return cmap_fmt4(table + 6, char_code) if format == 4
86
+ return cmap_fmt6(table + 6, char_code) if format == 6
87
+ return nil
88
+ end
89
+ end
90
+ return nil
91
+ end
92
+
93
+ def hor_metrics(glyph)
94
+ hmtx = reqtable("hmtx")
95
+ return nil if hmtx.nil?
96
+ if glyph < @num_long_hmtx # In long metrics segment?
97
+ offset = hmtx + 4 * glyph
98
+ return getu16(offset), geti16(offset + 2)
99
+ end
100
+ # Glyph is inside short metrics segment
101
+ boundary = hmtx + 4 * @num_long_hmtx
102
+ return nil if boundary < 4
103
+ offset = boundary - 4
104
+ advance_width = getu16(offset)
105
+ offset = boundary + 2 * (glyph - @num_long_hmtx)
106
+ return advance_width, geti16(offset)
107
+ end
108
+
109
+
110
+ def cmap_fmt4(table, char_code) # 572
111
+ # cmap format 4 only supports the Unicode BMP
112
+ return nil if char_code > 0xffff
113
+ seg_count_x2 = getu16(table)
114
+ raise "Error" if (seg_count_x2 & 1) != 0 or seg_count_x2 == 0
115
+ # Find starting positions of the relevant arrays
116
+ end_codes = table + 8
117
+ start_codes = end_codes + seg_count_x2 + 2
118
+ id_deltas = start_codes + seg_count_x2
119
+ id_range_offsets = id_deltas + seg_count_x2
120
+
121
+ @ecodes ||= at(end_codes,seg_count_x2 -1).unpack("n*")
122
+ seg_id_x_x2 = @ecodes.bsearch_index {|i| i > char_code } * 2
123
+
124
+ # Look up segment info from the arrays & short circuit if the spec requires
125
+ start_code = getu16(start_codes + seg_id_x_x2)
126
+ return 0 if start_code > char_code
127
+ id_delta = getu16(id_deltas + seg_id_x_x2)
128
+ if (id_range_offset = getu16(id_range_offsets + seg_id_x_x2)) == 0
129
+ # Intentional integer under- and overflow
130
+ return (char_code + id_delta) & 0xffff
131
+ end
132
+ # Calculate offset into glyph array and determine ultimate value
133
+ id = getu16(id_range_offsets + seg_id_x_x2 + id_range_offset + 2 * (char_code - start_code))
134
+ return id ? (id + id_delta) & 0xffff : 0
135
+ end
136
+
137
+ def decode_outline(offset, rec_depth = 0, outl = Outline.new)
138
+ num_contours = geti16(offset)
139
+ return nil if num_contours == 0
140
+ return simple_outline(offset + 10, num_contours, outl) if num_contours > 0
141
+ return compound_outline(offset + 10, rec_depth, outl)
142
+ end
143
+
144
+ def cmap_fmt6(table, char_code) # 621
145
+ first_code, entry_count = at(table,4).unpack("S>*")
146
+ return nil if !char_code.between?(first_code, 0xffff)
147
+ char_code -= first_code
148
+ return nil if (char_code >= entry_count)
149
+ return getu16(table + 4 + 2 * char_code)
150
+ end
151
+
152
+ def cmap_fmt12_13(table, char_code, which)
153
+ getu32(table + 12).times do |i|
154
+ first_code, last_code, glyph_offset = at(table + (i*12) + 16, 12).unpack("N*")
155
+ next if char_code < first_code || char_code > last_code
156
+ glyph_offset += char_code-first_code if which == 12
157
+ return glyph_offset
158
+ end
159
+ return nil
160
+ end
161
+
162
+ REPEAT_FLAG = 0x08
163
+
164
+ # For a simple outline, determines each point of the outline with a set of flags
165
+ def simple_flags(off, num_pts, flags)
166
+ value = 0
167
+ repeat = 0
168
+ num_pts.times do |i|
169
+ if repeat > 0
170
+ repeat -= 1
171
+ else
172
+ value = getu8(off)
173
+ off += 1
174
+ if value.allbits?(REPEAT_FLAG)
175
+ repeat = getu8(off)
176
+ off += 1
177
+ end
178
+ end
179
+ flags[i] = value
180
+ end
181
+ return off
182
+ end
183
+
184
+ X_CHANGE_IS_SMALL = 0x02 # x2 for Y
185
+ X_CHANGE_IS_ZERO = 0x10 # x2 for Y
186
+ X_CHANGE_IS_POSITIVE = 0x10 # x2 for Y
187
+
188
+ def simple_points(offset, num_pts, points, base_point)
189
+ [].tap do |flags|
190
+ offset = simple_flags(offset, num_pts, flags)
191
+
192
+ accum = 0.0
193
+ accumulate = ->(i, factor) do
194
+ if flags[i].allbits?(X_CHANGE_IS_SMALL * factor)
195
+ offset += 1
196
+ bit = flags[i].allbits?(X_CHANGE_IS_POSITIVE * factor) ? 1 : 0
197
+ accum -= (getu8(offset-1) ^ -bit) + bit
198
+ elsif flags[i].nobits?(X_CHANGE_IS_ZERO * factor)
199
+ offset += 2
200
+ accum += geti16(offset-2)
201
+ end
202
+ accum
203
+ end
204
+
205
+ num_pts.times {|i| points << Vector[accumulate.call(i,1), 0.0] }
206
+ accum = 0.0
207
+ num_pts.times {|i| points[base_point+i][1] = accumulate.call(i,2) }
208
+ end
209
+ end
210
+
211
+ def simple_outline(offset, num_contours, outl = Outline.new)
212
+ base_points = outl.points.length
213
+ num_pts = getu16(offset + (num_contours - 1) *2) + 1
214
+ end_pts = at(offset, num_contours*2).unpack("S>*")
215
+ offset += 2*num_contours
216
+ # Falling end_pts have no sensible interpretation, so treat as error
217
+ end_pts.each_cons(2) { |a, b| raise if b < a + 1 }
218
+ offset += 2 + getu16(offset)
219
+
220
+ flags = simple_points(offset, num_pts, outl.points, base_points)
221
+
222
+ beg = 0
223
+ num_contours.times do |i|
224
+ decode_contour(outl, flags, beg, base_points+beg, end_pts[i] - beg + 1)
225
+ beg = end_pts[i] + 1
226
+ end
227
+ outl
228
+ end
229
+
230
+ POINT_IS_ON_CURVE = 0x01
231
+
232
+ def decode_contour(outl, flags, off, base_point, count)
233
+ return true if count < 2 # Invisible (no area)
234
+
235
+ if flags[off].allbits?(POINT_IS_ON_CURVE)
236
+ loose_end = base_point
237
+ base_point+= 1
238
+ off += 1
239
+ count -= 1
240
+ elsif flags[off + count - 1].allbits?(POINT_IS_ON_CURVE)
241
+ count -= 1
242
+ loose_end = base_point + count
243
+ else
244
+ loose_end = outl.points.length
245
+ outl.points << midpoint(outl.points[base_point], outl.points[base_point + count - 1])
246
+ end
247
+ beg = loose_end
248
+ ctrl = nil
249
+ count.times do |i|
250
+ cur = base_point + i
251
+ if flags[off+i].allbits?(POINT_IS_ON_CURVE)
252
+ outl.segments << Outline::Segment.new(beg, cur, ctrl)
253
+ beg = cur
254
+ ctrl = nil
255
+ else
256
+ if ctrl # 2x control points in a row -> insert midpoint
257
+ center = outl.points.length
258
+ outl.points << midpoint(outl.points[ctrl], outl.points[cur])
259
+ outl.segments << Outline::Segment.new(beg, center, ctrl)
260
+ beg = center
261
+ end
262
+ ctrl = cur
263
+ end
264
+ end
265
+ outl.segments << Outline::Segment.new(beg, loose_end, ctrl)
266
+ return true
267
+ end
268
+
269
+ OFFSETS_ARE_LARGE = 0x001
270
+ ACTUAL_XY_OFFSETS = 0x002
271
+ GOT_A_SINGLE_SCALE = 0x008
272
+ THERE_ARE_MORE_COMPONENTS = 0x020
273
+ GOT_AN_X_AND_Y_SCALE = 0x040
274
+ GOT_A_SCALE_MATRIX = 0x080
275
+
276
+ def compound_outline(offset, rec_depth, outl) # 1057
277
+ # Guard against infinite recursion (compound glyphs that have themselves as component).
278
+ return nil if rec_depth >= 4
279
+ flags = THERE_ARE_MORE_COMPONENTS
280
+ while flags.allbits?(THERE_ARE_MORE_COMPONENTS)
281
+ flags, glyph = at(offset,4).unpack("S>*")
282
+ p [flags,glyph]
283
+ offset += 4
284
+ # We don't implement point matching, and neither does stb truetype
285
+ return nil if (flags & ACTUAL_XY_OFFSETS) == 0
286
+ # Read additional X and Y offsets (in FUnits) of this component.
287
+ if (flags & OFFSETS_ARE_LARGE) != 0
288
+ local = Matrix[[1.0, 0.0, geti16(offset)], [0.0,1.0, geti16(offset+2)]]
289
+ offset += 4
290
+ else
291
+ local = Matrix[[1.0, 0.0, geti8(offset)], [0.0, 1.0, geti8(offset)+1]]
292
+ offset += 2
293
+ end
294
+
295
+ if flags.allbits?(GOT_A_SINGLE_SCALE)
296
+ local[0][0] = local[1][0] = geti16(offset) / 16384.0
297
+ offset += 2
298
+ elsif flags.allbits?(GOT_AN_X_AND_Y_SCALE)
299
+ local[0][0] = geti16(offset + 0) / 16384.0
300
+ local[1][0] = geti16(offset + 2) / 16384.0
301
+ offset += 4
302
+ elsif flags.allbits?(GOT_A_SCALE_MATRIX)
303
+ local[0][0] = geti16(offset + 0) / 16384.0
304
+ local[0][1] = geti16(offset + 2) / 16384.0
305
+ local[1][0] = geti16(offset + 4) / 16384.0
306
+ local[1][1] = geti16(offset + 6) / 16384.0
307
+ offset += 8
308
+ end
309
+
310
+ outline = outline_offset(glyph)
311
+ return nil if outline.nil?
312
+ base_point = outl.points.length
313
+ return nil if decode_outline(outline, rec_depth + 1, outl).nil?
314
+ transform_points(local,outl.points[base_point..-1])
315
+ end
316
+ return outl
317
+ end
318
+
319
+ HORIZONTAL_KERNING = 0x01
320
+ MINIMUM_KERNING = 0x02
321
+ CROSS_STREAM_KERNING = 0x04
322
+ OVERRIDE_KERNING = 0x08
323
+
324
+ def kerning
325
+ return @kerning if @kerning
326
+ offset = gettable("kern")
327
+ return nil if offset.nil? || getu16(offset) != 0
328
+ offset += 4
329
+ @kerning = {}
330
+ getu16(offset - 2).times do
331
+ length,format,flags = at(offset+2,6).unpack("S>CC")
332
+ offset += 6
333
+ if format == 0 && flags.allbits?(HORIZONTAL_KERNING) && flags.nobits?(MINIMUM_KERNING)
334
+ offset += 8
335
+ getu16(offset-8).times do |i|
336
+ v = geti16(offset+i*6+4)
337
+ @kerning[at(offset+i*6,4)] =
338
+ Kerning.new(* flags.allbits?(CROSS_STREAM_KERNING) ? [0,v] : [v,0])
339
+ end
340
+ end
341
+ offset += length
342
+ end
343
+ @kerning
344
+ end
345
+ end
@@ -0,0 +1,56 @@
1
+ def midpoint(a, b); 0.5*(a+b); end
2
+
3
+ # Applies an affine linear transformation matrix to a set of points
4
+ def transform_points(trf, pts)
5
+ pts.each {|pt| pt[0],pt[1] = *(trf * Vector[*pt,1]) }
6
+ end
7
+
8
+ class Outline
9
+ Segment = Struct.new(:beg, :end, :ctrl)
10
+
11
+ attr_reader :points, :segments
12
+
13
+ def initialize
14
+ @points, @segments = [], []
15
+ end
16
+
17
+ def clip_points(width, height)
18
+ @points.each do |pt|
19
+ pt[0] = pt[0].clamp(0, width.pred)
20
+ pt[1] = pt[1].clamp(0, height.pred)
21
+ end
22
+ end
23
+
24
+ def render(transform, image)
25
+ transform_points(transform, @points)
26
+ clip_points(image.width, image.height)
27
+ buf = Raster.new(image.width, image.height)
28
+ @segments.each do |seg|
29
+ seg.ctrl ? tesselate_curve(seg) : buf.draw_line(@points[seg.beg], @points[seg.end])
30
+ end
31
+ image.pixels = buf.post_process
32
+ return image
33
+ end
34
+
35
+ def tesselate_curve(curve)
36
+ if is_flat(curve)
37
+ @segments << Segment.new(curve.beg, curve.end)
38
+ return
39
+ end
40
+ ctrl0 = @points.length
41
+ @points << midpoint(@points[curve.beg], @points[curve.ctrl])
42
+ ctrl1 = @points.length
43
+ @points << midpoint(@points[curve.ctrl], @points[curve.end])
44
+ pivot = @points.length
45
+ @points << midpoint(@points[ctrl0], @points[ctrl1])
46
+ tesselate_curve(Segment.new(curve.beg, pivot, ctrl0))
47
+ tesselate_curve(Segment.new(pivot, curve.end, ctrl1))
48
+ end
49
+
50
+ # A heuristic to tell whether a given curve can be approximated closely enough by a line. */
51
+ def is_flat(curve)
52
+ g = @points[curve.ctrl] - @points[curve.beg]
53
+ h = @points[curve.end] - @points[curve.beg]
54
+ (g[0]*h[1]-g[1]*h[0]).abs <= 2.0
55
+ end
56
+ end
@@ -0,0 +1,80 @@
1
+ class Raster
2
+ Cell = Struct.new(:area, :cover)
3
+
4
+ def initialize width, height
5
+ @width = width
6
+ @height = height
7
+ @cells = (0..(width*height-1)).map { Cell.new(0.0,0.0) }
8
+ end
9
+
10
+ # Integrate the values in the buffer to arrive at the final grayscale image.
11
+ def post_process
12
+ accum = 0.0
13
+ (@width*@height).times.collect do |i|
14
+ cell = @cells[i]
15
+ value = (accum + cell.area).abs
16
+ value = [value, 1.0].min * 255.0 + 0.5
17
+ accum += cell.cover
18
+ value.to_i & 0xff
19
+ end
20
+ end
21
+
22
+ # Draws a line into the buffer. Uses a custom 2D raycasting algorithm to do so.
23
+ def draw_line(origin, goal)
24
+ prev_distance = 0.0
25
+ num_steps = 0
26
+ delta = goal-origin
27
+ dir_x = delta[0] <=> 0
28
+ dir_y = delta[1] <=> 0
29
+ next_crossing = Vector[0.0,0.0]
30
+ pixel = Vector[0,0]
31
+ return if dir_y == 0
32
+
33
+ crossing_incr_x = dir_x != 0 ? (1.0 / delta[0]).abs : 1.0
34
+ crossing_incr_y = (1.0 / delta[1]).abs
35
+
36
+ if dir_x == 0
37
+ pixel[0] = origin[0].floor
38
+ next_crossing[0] = 100.0
39
+ else
40
+ if dir_x > 0
41
+ pixel[0] = origin[0].floor
42
+ next_crossing[0] = crossing_incr_x - (origin[0] - pixel[0]) * crossing_incr_x
43
+ num_steps += goal[0].ceil - origin[0].floor - 1
44
+ else
45
+ pixel[0] = origin[0].ceil - 1
46
+ next_crossing[0] = (origin[0] - pixel[0]) * crossing_incr_x
47
+ num_steps += origin[0].ceil - goal[0].floor - 1
48
+ end
49
+ end
50
+
51
+ if dir_y > 0
52
+ pixel[1] = origin[1].floor
53
+ next_crossing[1] = crossing_incr_y - (origin[1] - pixel[1]) * crossing_incr_y
54
+ num_steps += goal[1].ceil - origin[1].floor - 1
55
+ else
56
+ pixel[1] = origin[1].ceil - 1
57
+ next_crossing[1] = (origin[1] - pixel[1]) * crossing_incr_y
58
+ num_steps += origin[1].ceil - goal[1].floor - 1
59
+ end
60
+
61
+ next_distance = next_crossing.min
62
+ half_delta_x = 0.5 * delta[0]
63
+ setcell = ->(nd) do
64
+ x_average = origin[0] + (prev_distance + nd) * half_delta_x - pixel[0]
65
+ y_difference = (nd - prev_distance).to_f * delta[1]
66
+ cell = @cells[pixel[1] * @width + pixel[0]]
67
+ cell.cover += y_difference
68
+ cell.area += (1.0 - x_average) * y_difference
69
+ end
70
+ num_steps.times do
71
+ setcell.call(next_distance)
72
+ prev_distance = next_distance
73
+ along_x = next_crossing[0] < next_crossing[1]
74
+ pixel += along_x ? Vector[dir_x,0] : Vector[0,dir_y]
75
+ next_crossing += along_x ? Vector[crossing_incr_x, 0.0] : Vector[0.0, crossing_incr_y]
76
+ next_distance = next_crossing.min
77
+ end
78
+ setcell.call(1.0)
79
+ end
80
+ end
data/lib/skrift/sft.rb ADDED
@@ -0,0 +1,83 @@
1
+ class SFT
2
+ DOWNWARD_Y = 0x01
3
+
4
+ attr_accessor :font, :x_scale, :y_scale, :x_offset, :y_offset, :flags
5
+
6
+ def initialize(font)
7
+ @font = font
8
+ @x_scale = 32
9
+ @y_scale = 32
10
+ @x_offset = 0
11
+ @y_offset = 0
12
+ @flags = SFT::DOWNWARD_Y
13
+ end
14
+
15
+ def lookup(codepoint)
16
+ return font.glyph_id(codepoint)
17
+ end
18
+
19
+ def glyph_bbox(outline)
20
+ box = @font.glyph_bbox(outline)
21
+ raise if !box
22
+ # Transform the bounding box into SFT coordinate space
23
+ xs = @x_scale.to_f / @font.units_per_em
24
+ ys = @y_scale.to_f / @font.units_per_em
25
+ box[0] = (box[0] * xs + @x_offset).floor
26
+ box[1] = (box[1] * ys + @y_offset).floor
27
+ box[2] = (box[2] * xs + @x_offset).ceil
28
+ box[3] = (box[3] * ys + @y_offset).ceil
29
+ return box
30
+ end
31
+
32
+ def gmetrics(glyph) # 149
33
+ raise "out of bounds" if glyph < 0
34
+ xs = @x_scale.to_f / @font.units_per_em
35
+ adv, lsb = @font.hor_metrics(glyph)
36
+
37
+ return nil if adv.nil?
38
+ metrics = GMetrics.new(adv * xs, lsb * xs + @x_offset)
39
+
40
+ outline = @font.outline_offset(glyph)
41
+ return metrics if outline.nil?
42
+ bbox = glyph_bbox(outline)
43
+ return nil if !bbox
44
+ metrics.min_width = bbox[2] - bbox[0] + 1
45
+ metrics.min_height= bbox[3] - bbox[1] + 1
46
+ metrics.y_offset = @flags & SFT::DOWNWARD_Y != 0 ? bbox[3] : bbox[1]
47
+ return metrics
48
+ end
49
+
50
+ def lmetrics
51
+ hhea= font.reqtable("hhea")
52
+ factor = @y_scale.to_f / @font.units_per_em
53
+ LMetrics.new(
54
+ font.geti16(hhea + 4) * factor, # ascender
55
+ font.geti16(hhea + 6) * factor, # descender
56
+ font.geti16(hhea + 8) * factor # line_gap
57
+ )
58
+ end
59
+
60
+ def render(glyph, image) # 239
61
+ outline = @font.outline_offset(glyph)
62
+ return false if outline.nil?
63
+ return true if outline.nil?
64
+ bbox = glyph_bbox(outline)
65
+ return false if !bbox
66
+ # Set up the transformation matrix such that
67
+ # the transformed bounding boxes min corner lines
68
+ # up with the (0, 0) point.
69
+ xr = [@x_scale.to_f / @font.units_per_em, 0.0, @x_offset - bbox[0]]
70
+ ys = @y_scale.to_f / @font.units_per_em
71
+ if @flags.allbits?(SFT::DOWNWARD_Y)
72
+ transform = Matrix.rows([xr, [0.0, -ys, bbox[3] - @y_offset]])
73
+ else
74
+ transform = Matrix.rows([xr, [0.0, +ys, @y_offset - bbox[1] ]])
75
+ end
76
+ outl = @font.decode_outline(outline)
77
+ outl.render(transform, image)
78
+ end
79
+
80
+ def kerning(left_glyph, right_glyph) # 176
81
+ @font.kerning[[left_glyph,right_glyph].pack("n*")]
82
+ end
83
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skrift
4
+ VERSION = "0.1.0"
5
+ end
data/lib/skrift.rb ADDED
@@ -0,0 +1,11 @@
1
+ GMetrics = Struct.new(:advance_width, :left_side_bearing, :y_offset, :min_width, :min_height)
2
+ LMetrics = Struct.new(:ascender, :descender, :line_gap)
3
+ Image = Struct.new(:width, :height, :pixels)
4
+ Kerning = Struct.new(:x_shift, :y_shift)
5
+
6
+ require_relative './skrift/version'
7
+ require_relative './skrift/sft'
8
+ require_relative './skrift/outline'
9
+ require_relative './skrift/raster'
10
+ require_relative './skrift/font'
11
+ require 'matrix'
Binary file
data/skrift.gemspec ADDED
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/skrift/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "skrift"
7
+ spec.version = Skrift::VERSION
8
+ spec.authors = ["Vidar Hokstad"]
9
+ spec.email = ["vidar@hokstad.com"]
10
+
11
+ spec.summary = "A pure Ruby TruteType font renderer"
12
+ #spec.description = "TODO: Write a longer description or delete this line."
13
+ spec.homepage = "https://github.com/vidarh/skrift"
14
+ spec.license = "ISC"
15
+ spec.required_ruby_version = ">= 2.7.0"
16
+
17
+ #spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
18
+
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = spec.homepage
21
+ #spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
22
+
23
+ # Specify which files should be added to the gem when it is released.
24
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
+ spec.files = Dir.chdir(__dir__) do
26
+ `git ls-files -z`.split("\x0").reject do |f|
27
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|circleci)|appveyor)})
28
+ end
29
+ end
30
+ spec.bindir = "exe"
31
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ["lib"]
33
+
34
+ # Uncomment to register a new dependency of your gem
35
+ # spec.add_dependency "example-gem", "~> 1.0"
36
+
37
+ # For more information and examples about making a new gem, check out our
38
+ # guide at: https://bundler.io/guides/creating_gem.html
39
+ end
data/test.rb ADDED
@@ -0,0 +1,77 @@
1
+
2
+ require 'pp'
3
+ require_relative './lib/skrift'
4
+
5
+ f = Font.load("resources/Ubuntu-Regular.ttf")
6
+ #f = Font.load("resources/FiraGO-Regular_extended_with_NotoSansEgyptianHieroglyphs-Regular.ttf")
7
+ #f = Font.load("/usr/share/fonts/truetype/tlwg/Umpush-BoldOblique.ttf")
8
+ #f = Font.load("/usr/share/fonts/truetype/tlwg/Garuda.ttf")
9
+ #f = Font.load("resources/FiraGO-Regular.ttf")
10
+ #f = Font.load("/usr/share/fonts/opentype/cantarell/Cantarell-Regular.otf")
11
+ p f.tables
12
+ #p f.kerning
13
+
14
+ sft = SFT.new(f)
15
+ sft.x_scale = 20
16
+ sft.y_scale = 20
17
+ sft.x_offset = 0
18
+ sft.y_offset = 0
19
+
20
+ PP.pp sft.lmetrics
21
+
22
+ if ARGV[0] == "gid"
23
+ gid = ARGV[1].to_i
24
+ elsif ARGV[0]
25
+ gid = sft.lookup(ARGV[0][0].ord)
26
+ p gid
27
+ if ARGV[0][1]
28
+ f.reqtable("kern")
29
+ #PP.pp f.kerning.map {|k,v|
30
+ # [k.unpack("n*"), v]
31
+ #}
32
+
33
+ gid2 = sft.lookup(ARGV[0][1].ord)
34
+ PP.pp sft.kerning(gid,gid2)
35
+ exit
36
+ end
37
+ end
38
+
39
+ #gid = 12
40
+
41
+ def test_glyph(sft, gid)
42
+ puts "TESTING GLYPH #{gid}"
43
+ mtx = sft.gmetrics(gid)
44
+ p mtx
45
+
46
+ #p sft.gmetrics(sft.lookup(0x43))
47
+
48
+
49
+ img = Image.new(mtx.min_width, mtx.min_height)
50
+ if sft.render(gid, img)
51
+ #r = Raster.new(img.width, img.height)
52
+ #
53
+ #r.draw_line(Point.new(0,0), Point.new(img.width-1, 0))
54
+ #r.draw_line(Point.new(10,10), Point.new(20, 10))
55
+ #r.draw_line(Point.new(10,10), Point.new(20, 20))
56
+ #r.draw_line(Point.new(20,10), Point.new(30, 20))
57
+ #r.draw_line(Point.new(0,0), Point.new(img.width-1, img.height-1))
58
+ #p r
59
+ #img.pixels = r.post_process
60
+
61
+ img.height.times do |row|
62
+ img.pixels[row*img.width .. (row+1)*img.width-1].map do |s|
63
+ print "\033[32;48;2;#{s};#{s};#{s}m%02x " % s
64
+ end
65
+ puts "\033[39;49m"
66
+ end
67
+ end
68
+ end
69
+
70
+ if gid
71
+ test_glyph(sft,gid)
72
+ else
73
+ (0..10000).each do |gid|
74
+ test_glyph(sft,gid)
75
+ end
76
+ end
77
+
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: skrift
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Vidar Hokstad
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-08-18 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - vidar@hokstad.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - ".rspec"
21
+ - Gemfile
22
+ - LICENSE.txt
23
+ - README.md
24
+ - Rakefile
25
+ - lib/skrift.rb
26
+ - lib/skrift/font.rb
27
+ - lib/skrift/outline.rb
28
+ - lib/skrift/raster.rb
29
+ - lib/skrift/sft.rb
30
+ - lib/skrift/version.rb
31
+ - resources/Ubuntu-Regular.ttf
32
+ - skrift.gemspec
33
+ - test.rb
34
+ homepage: https://github.com/vidarh/skrift
35
+ licenses:
36
+ - ISC
37
+ metadata:
38
+ homepage_uri: https://github.com/vidarh/skrift
39
+ source_code_uri: https://github.com/vidarh/skrift
40
+ post_install_message:
41
+ rdoc_options: []
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: 2.7.0
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ requirements: []
55
+ rubygems_version: 3.1.4
56
+ signing_key:
57
+ specification_version: 4
58
+ summary: A pure Ruby TruteType font renderer
59
+ test_files: []