skrift 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +18 -0
- data/README.md +150 -0
- data/Rakefile +8 -0
- data/lib/skrift/font.rb +345 -0
- data/lib/skrift/outline.rb +56 -0
- data/lib/skrift/raster.rb +80 -0
- data/lib/skrift/sft.rb +83 -0
- data/lib/skrift/version.rb +5 -0
- data/lib/skrift.rb +11 -0
- data/resources/Ubuntu-Regular.ttf +0 -0
- data/skrift.gemspec +39 -0
- data/test.rb +77 -0
- metadata +59 -0
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
data/Gemfile
ADDED
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
data/lib/skrift/font.rb
ADDED
@@ -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
|
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: []
|