skrift 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []