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 +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: []
|