skrift 0.2.1 → 0.4.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 +4 -4
- data/Rakefile +5 -0
- data/{example.rb → examples/banner.rb} +1 -1
- data/{example2.rb → examples/banner_boxdrawing.rb} +1 -1
- data/{example3.rb → examples/banner_shadow.rb} +1 -1
- data/{test.rb → examples/glyph_dump.rb} +1 -1
- data/lib/skrift/font.rb +381 -291
- data/lib/skrift/font_set.rb +58 -0
- data/lib/skrift/glyph_cache.rb +142 -0
- data/lib/skrift/outline.rb +42 -48
- data/lib/skrift/raster.rb +74 -72
- data/lib/skrift/sft.rb +84 -71
- data/lib/skrift/version.rb +1 -1
- data/lib/skrift.rb +45 -10
- metadata +10 -8
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require "shellwords"
|
|
2
|
+
|
|
3
|
+
module Skrift
|
|
4
|
+
# An ordered set of fonts with per-codepoint fallback. Entries may be Font
|
|
5
|
+
# objects, or font file paths / fontconfig names that are loaded lazily and
|
|
6
|
+
# resolved via (in order) an explicit path, ~/.local/share/fonts, then
|
|
7
|
+
# `fc-match`. A single Font is just a one-element set.
|
|
8
|
+
class FontSet
|
|
9
|
+
def initialize(fonts)
|
|
10
|
+
@entries = Array(fonts)
|
|
11
|
+
@loaded = {}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def size = @entries.length
|
|
15
|
+
|
|
16
|
+
# The i-th font, lazily loaded/resolved. Returns nil if unresolvable.
|
|
17
|
+
def [](index)
|
|
18
|
+
return @loaded[index] if @loaded.key?(index)
|
|
19
|
+
@loaded[index] = resolve(@entries[index])
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Yields each resolvable font in order.
|
|
23
|
+
def each
|
|
24
|
+
return enum_for(:each) unless block_given?
|
|
25
|
+
@entries.each_index { |i| (f = self[i]) && yield(f) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The first [font, glyph_id] whose font maps +codepoint+, or nil if no font
|
|
29
|
+
# in the set has a cmap entry for it. (A font that maps the codepoint to the
|
|
30
|
+
# missing glyph, gid 0, still counts as a match — matching the prior
|
|
31
|
+
# behaviour; skipping .notdef to keep searching is a deliberate non-goal
|
|
32
|
+
# here.)
|
|
33
|
+
def lookup(codepoint)
|
|
34
|
+
@entries.each_index do |i|
|
|
35
|
+
font = self[i]
|
|
36
|
+
next unless font
|
|
37
|
+
gid = font.glyph_id(codepoint)
|
|
38
|
+
return [font, gid] unless gid.nil?
|
|
39
|
+
end
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def resolve(entry)
|
|
46
|
+
return entry if entry.is_a?(Font)
|
|
47
|
+
return Font.load(entry) if File.exist?(entry)
|
|
48
|
+
|
|
49
|
+
local = File.expand_path("~/.local/share/fonts/#{entry}")
|
|
50
|
+
return Font.load(local) if File.exist?(local)
|
|
51
|
+
|
|
52
|
+
matched = `fc-match --format='%{file}' #{Shellwords.escape(entry)}`.strip
|
|
53
|
+
return Font.load(matched) if !matched.empty? && File.exist?(matched)
|
|
54
|
+
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
module Skrift
|
|
2
|
+
# A rasterised, cached glyph. A monochrome glyph carries +alpha+ (a row-major
|
|
3
|
+
# 0..255 coverage array, stride padded to a multiple of 4), or nil for a
|
|
4
|
+
# glyph with no outline (e.g. space). A colour glyph (from a colour delegate,
|
|
5
|
+
# see GlyphCache) instead carries +rgba+, a row-major array of packed
|
|
6
|
+
# 0xRRGGBBAA pixels; for those +alpha+ is nil. Placement fields are shared.
|
|
7
|
+
RenderedGlyph = Struct.new(:alpha, :width, :height, :left_side_bearing, :y_offset, :advance_width, :rgba) do
|
|
8
|
+
def color? = !rgba.nil?
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# The platform-independent glyph pipeline shared by every backend: resolve a
|
|
12
|
+
# codepoint through a FontSet, rasterise it (with optional fit-to-cell
|
|
13
|
+
# scaling), and cache the result. Backends (XRender glyphsets, software RGB
|
|
14
|
+
# blending, ...) consume the RenderedGlyph and place it however they like.
|
|
15
|
+
class GlyphCache
|
|
16
|
+
attr_reader :fontset, :ascent, :descent, :line_gap, :cell_width
|
|
17
|
+
|
|
18
|
+
# +special+ is an optional callable codepoint -> Skrift::Image (alpha) used
|
|
19
|
+
# to override font rasterisation (e.g. a box-drawing plugin); return nil to
|
|
20
|
+
# fall through to the font.
|
|
21
|
+
#
|
|
22
|
+
# +color+ is an optional colour delegate: any object responding to
|
|
23
|
+
# #render(codepoint) and returning an RGBA colour image (with #width,
|
|
24
|
+
# #height and #pixels of packed 0xRRGGBBAA), or nil to fall through to
|
|
25
|
+
# monochrome. skrift core knows nothing about colour formats — all of that
|
|
26
|
+
# lives in the delegate (e.g. the skrift-color gem).
|
|
27
|
+
def initialize(fontset, x_scale:, y_scale:, fixed: nil, maxheight: nil, fit: false, special: nil, color: nil)
|
|
28
|
+
@fontset = fontset.is_a?(FontSet) ? fontset : FontSet.new(fontset)
|
|
29
|
+
@x_scale = x_scale
|
|
30
|
+
@y_scale = y_scale
|
|
31
|
+
@fixed = fixed
|
|
32
|
+
@maxheight = maxheight
|
|
33
|
+
@fit = fit
|
|
34
|
+
@special = special
|
|
35
|
+
@color = color
|
|
36
|
+
@cache = {}
|
|
37
|
+
|
|
38
|
+
@sft = Renderer.new(@fontset[0])
|
|
39
|
+
@sft.x_scale = x_scale
|
|
40
|
+
@sft.y_scale = y_scale
|
|
41
|
+
lm = @sft.lmetrics
|
|
42
|
+
@ascent, @descent, @line_gap = lm.ascender, lm.descender, lm.line_gap
|
|
43
|
+
|
|
44
|
+
# +fixed+ enables monospace cells: pass a Numeric cell width, or true to
|
|
45
|
+
# derive it from the 'M' advance. Either way @fixed stays truthy so glyphs
|
|
46
|
+
# render at the cell width.
|
|
47
|
+
g = @sft.gmetrics(@sft.lookup("M".ord)) || @sft.gmetrics(0)
|
|
48
|
+
@cell_width = (fixed if fixed.is_a?(Numeric)) || (g ? g.advance_width.ceil : x_scale)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def baseline = @ascent.round
|
|
52
|
+
|
|
53
|
+
# Cell height unified on ascent - descent (line gap excluded), clamped to
|
|
54
|
+
# maxheight when given.
|
|
55
|
+
def cell_height
|
|
56
|
+
@cell_height ||= [@maxheight, @ascent.ceil - @descent.ceil].compact.min
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# RenderedGlyph for +codepoint+, or nil if no font in the set maps it.
|
|
60
|
+
def glyph(codepoint)
|
|
61
|
+
return @cache[codepoint] if @cache.key?(codepoint)
|
|
62
|
+
@cache[codepoint] = build(codepoint)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Total advance for +str+: the fixed cell width per character in fixed mode,
|
|
66
|
+
# otherwise the sum of per-glyph advances.
|
|
67
|
+
def text_width(str)
|
|
68
|
+
str.to_s.each_char.sum do |ch|
|
|
69
|
+
next @cell_width if @fixed
|
|
70
|
+
(g = glyph(ch.ord)) ? g.advance_width : 0
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def build(codepoint)
|
|
77
|
+
if @special && (img = @special.call(codepoint))
|
|
78
|
+
# y_offset == baseline places the bitmap at the cell top under the
|
|
79
|
+
# standard `baseline - y_offset` formula (box drawing fills the cell).
|
|
80
|
+
return RenderedGlyph.new(img.pixels, img.width, img.height, 0, baseline, @cell_width)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Delegate to the colour renderer if it has this codepoint. We only place
|
|
84
|
+
# its RGBA image in the cell (generic geometry) — the colour rendering
|
|
85
|
+
# itself lives entirely in the delegate.
|
|
86
|
+
if @color && (cimg = @color.render(codepoint))
|
|
87
|
+
return color_glyph(cimg)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
font, gid = @fontset.lookup(codepoint)
|
|
91
|
+
return nil unless font
|
|
92
|
+
@sft.font = font
|
|
93
|
+
mtx = @sft.gmetrics(gid)
|
|
94
|
+
return nil unless mtx
|
|
95
|
+
|
|
96
|
+
# No outline (e.g. space): advance only, no alpha.
|
|
97
|
+
if mtx.min_width.nil? || mtx.min_height.nil?
|
|
98
|
+
return RenderedGlyph.new(nil, 0, 0, (mtx.left_side_bearing || 0).round, mtx.y_offset, mtx.advance_width)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
saved = nil
|
|
102
|
+
# Oversized glyph in a fixed cell: re-render smaller so it fits rather
|
|
103
|
+
# than being clamped (and distorted) at the cell edge. Only clear
|
|
104
|
+
# overshoots are scaled; marginal ones are left to the clamp.
|
|
105
|
+
if @fit && @fixed
|
|
106
|
+
sx = mtx.min_width > @cell_width * 1.1 ? @cell_width.to_f / mtx.min_width : 1.0
|
|
107
|
+
sy = mtx.min_height > cell_height * 1.1 ? cell_height.to_f / mtx.min_height : 1.0
|
|
108
|
+
s = [sx, sy].min
|
|
109
|
+
if s < 1.0
|
|
110
|
+
saved = [@sft.x_scale, @sft.y_scale]
|
|
111
|
+
@sft.x_scale *= s
|
|
112
|
+
@sft.y_scale *= s
|
|
113
|
+
mtx = @sft.gmetrics(gid)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
w = @fixed ? @cell_width : mtx.min_width
|
|
118
|
+
h = [mtx.min_height, @maxheight].compact.min
|
|
119
|
+
img = Image.new((w + 3) & ~3, h)
|
|
120
|
+
alpha = @sft.render(gid, img) ? img.pixels : Array.new(img.width * img.height, 0)
|
|
121
|
+
RenderedGlyph.new(alpha, img.width, img.height,
|
|
122
|
+
mtx.left_side_bearing.round, mtx.y_offset || baseline, mtx.advance_width)
|
|
123
|
+
ensure
|
|
124
|
+
@sft.x_scale, @sft.y_scale = saved if saved
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Wrap a colour delegate's RGBA image as a RenderedGlyph, centred in its
|
|
128
|
+
# cell span. The span (in cells) is inferred from the image width — a colour
|
|
129
|
+
# emoji rendered at the cell size is naturally about two cells wide — so the
|
|
130
|
+
# advance and centring span two cells for emoji without the cache needing a
|
|
131
|
+
# Unicode width table.
|
|
132
|
+
def color_glyph(cimg)
|
|
133
|
+
w, h = cimg.width, cimg.height
|
|
134
|
+
span = [(w.to_f / @cell_width).round, 1].max
|
|
135
|
+
advance = span * @cell_width
|
|
136
|
+
RenderedGlyph.new(nil, w, h,
|
|
137
|
+
((advance - w) / 2.0).round,
|
|
138
|
+
(baseline - (cell_height - h) / 2.0).round,
|
|
139
|
+
advance, cimg.pixels)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
data/lib/skrift/outline.rb
CHANGED
|
@@ -1,59 +1,53 @@
|
|
|
1
|
-
|
|
1
|
+
module Skrift
|
|
2
|
+
class Outline
|
|
3
|
+
include Geometry
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
def transform_points(trf, pts)
|
|
5
|
-
pts.each do |pt|
|
|
6
|
-
pt[0] = trf[0][0] * pt[0] + trf[0][1] * pt[1] + trf[0][2]
|
|
7
|
-
pt[1] = trf[1][0] * pt[0] + trf[1][1] * pt[1] + trf[1][2]
|
|
8
|
-
end
|
|
9
|
-
end
|
|
5
|
+
Segment = Struct.new(:beg, :end, :ctrl)
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
Segment = Struct.new(:beg, :end, :ctrl)
|
|
13
|
-
|
|
14
|
-
attr_reader :points, :segments
|
|
15
|
-
|
|
16
|
-
def initialize
|
|
17
|
-
@points, @segments = [], []
|
|
18
|
-
end
|
|
7
|
+
attr_reader :points, :segments
|
|
19
8
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
pt[0] = pt[0].clamp(0, width.pred)
|
|
23
|
-
pt[1] = pt[1].clamp(0, height.pred)
|
|
9
|
+
def initialize
|
|
10
|
+
@points, @segments = [], []
|
|
24
11
|
end
|
|
25
|
-
end
|
|
26
12
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
seg.ctrl ? tesselate_curve(seg) : buf.draw_line(@points[seg.beg], @points[seg.end])
|
|
13
|
+
def clip_points(width, height)
|
|
14
|
+
@points.each do |pt|
|
|
15
|
+
pt[0] = pt[0].clamp(0, width.pred)
|
|
16
|
+
pt[1] = pt[1].clamp(0, height.pred)
|
|
17
|
+
end
|
|
33
18
|
end
|
|
34
|
-
image.pixels = buf.post_process
|
|
35
|
-
return image
|
|
36
|
-
end
|
|
37
19
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
20
|
+
def render(transform, image)
|
|
21
|
+
transform_points(transform, @points)
|
|
22
|
+
clip_points(image.width, image.height)
|
|
23
|
+
buf = Raster.new(image.width, image.height)
|
|
24
|
+
@segments.each do |seg|
|
|
25
|
+
seg.ctrl ? tesselate_curve(seg) : buf.draw_line(@points[seg.beg], @points[seg.end])
|
|
26
|
+
end
|
|
27
|
+
image.pixels = buf.post_process
|
|
28
|
+
return image
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def tesselate_curve(curve)
|
|
32
|
+
if is_flat(curve)
|
|
33
|
+
@segments << Segment[curve.beg, curve.end]
|
|
34
|
+
return
|
|
35
|
+
end
|
|
36
|
+
ctrl0 = @points.length
|
|
37
|
+
@points << midpoint(@points[curve.beg], @points[curve.ctrl])
|
|
38
|
+
ctrl1 = @points.length
|
|
39
|
+
@points << midpoint(@points[curve.ctrl], @points[curve.end])
|
|
40
|
+
pivot = @points.length
|
|
41
|
+
@points << midpoint(@points[ctrl0], @points[ctrl1])
|
|
42
|
+
tesselate_curve(Segment[curve.beg, pivot, ctrl0])
|
|
43
|
+
tesselate_curve(Segment[pivot, curve.end, ctrl1])
|
|
42
44
|
end
|
|
43
|
-
ctrl0 = @points.length
|
|
44
|
-
@points << midpoint(@points[curve.beg], @points[curve.ctrl])
|
|
45
|
-
ctrl1 = @points.length
|
|
46
|
-
@points << midpoint(@points[curve.ctrl], @points[curve.end])
|
|
47
|
-
pivot = @points.length
|
|
48
|
-
@points << midpoint(@points[ctrl0], @points[ctrl1])
|
|
49
|
-
tesselate_curve(Segment[curve.beg, pivot, ctrl0])
|
|
50
|
-
tesselate_curve(Segment[pivot, curve.end, ctrl1])
|
|
51
|
-
end
|
|
52
45
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
46
|
+
# A heuristic to tell whether a given curve can be approximated closely enough by a line.
|
|
47
|
+
def is_flat(curve)
|
|
48
|
+
g = @points[curve.ctrl] - @points[curve.beg]
|
|
49
|
+
h = @points[curve.end] - @points[curve.beg]
|
|
50
|
+
(g[0]*h[1]-g[1]*h[0]).abs <= 2.0
|
|
51
|
+
end
|
|
58
52
|
end
|
|
59
53
|
end
|
data/lib/skrift/raster.rb
CHANGED
|
@@ -1,87 +1,89 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
module Skrift
|
|
2
|
+
class Raster
|
|
3
|
+
Cell = Struct.new(:area, :cover)
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def initialize width, height
|
|
12
|
-
@width = width
|
|
13
|
-
@height = height
|
|
14
|
-
@cells = (0..(width*height-1)).map { Cell[0.0,0.0] }
|
|
15
|
-
end
|
|
5
|
+
Vector = Struct.new(:x,:y) do
|
|
6
|
+
def*(f) = Vector[x*f,y*f]
|
|
7
|
+
def+(o) = Vector[x+o[0],y+o[1]]
|
|
8
|
+
def-(o) = Vector[x-o[0],y-o[1]]
|
|
9
|
+
def min = (x < y ? x : y)
|
|
10
|
+
end
|
|
16
11
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
12
|
+
def initialize width, height
|
|
13
|
+
@width = width
|
|
14
|
+
@height = height
|
|
15
|
+
@cells = (0..(width*height-1)).map { Cell[0.0,0.0] }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Integrate the values in the buffer to arrive at the final grayscale image.
|
|
19
|
+
def post_process
|
|
20
|
+
accum = 0.0
|
|
21
|
+
(@width*@height).times.collect do |i|
|
|
22
|
+
cell = @cells[i]
|
|
23
|
+
value = (accum + cell.area).abs
|
|
24
|
+
value = [value, 1.0].min * 255.0 + 0.5
|
|
25
|
+
accum += cell.cover
|
|
26
|
+
value.to_i & 0xff
|
|
27
|
+
end
|
|
26
28
|
end
|
|
27
|
-
end
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
# Draws a line into the buffer. Uses a custom 2D raycasting algorithm to do so.
|
|
31
|
+
def draw_line(origin, goal)
|
|
32
|
+
prev_distance = 0.0
|
|
33
|
+
num_steps = 0
|
|
34
|
+
delta = goal-origin
|
|
35
|
+
dir_x = delta[0] <=> 0
|
|
36
|
+
dir_y = delta[1] <=> 0
|
|
37
|
+
next_crossing = Vector[0.0,0.0]
|
|
38
|
+
pixel = Vector[0,0]
|
|
39
|
+
return if dir_y == 0
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
crossing_incr_x = dir_x != 0 ? (1.0 / delta[0]).abs : 1.0
|
|
42
|
+
crossing_incr_y = (1.0 / delta[1]).abs
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
pixel[0] = origin[0].floor
|
|
45
|
-
next_crossing[0] = 100.0
|
|
46
|
-
else
|
|
47
|
-
if dir_x > 0
|
|
44
|
+
if dir_x == 0
|
|
48
45
|
pixel[0] = origin[0].floor
|
|
49
|
-
next_crossing[0] =
|
|
50
|
-
num_steps += goal[0].ceil - origin[0].floor - 1
|
|
46
|
+
next_crossing[0] = 100.0
|
|
51
47
|
else
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
48
|
+
if dir_x > 0
|
|
49
|
+
pixel[0] = origin[0].floor
|
|
50
|
+
next_crossing[0] = crossing_incr_x - (origin[0] - pixel[0]) * crossing_incr_x
|
|
51
|
+
num_steps += goal[0].ceil - origin[0].floor - 1
|
|
52
|
+
else
|
|
53
|
+
pixel[0] = origin[0].ceil - 1
|
|
54
|
+
next_crossing[0] = (origin[0] - pixel[0]) * crossing_incr_x
|
|
55
|
+
num_steps += origin[0].ceil - goal[0].floor - 1
|
|
56
|
+
end
|
|
55
57
|
end
|
|
56
|
-
end
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
59
|
+
if dir_y > 0
|
|
60
|
+
pixel[1] = origin[1].floor
|
|
61
|
+
next_crossing[1] = crossing_incr_y - (origin[1] - pixel[1]) * crossing_incr_y
|
|
62
|
+
num_steps += goal[1].ceil - origin[1].floor - 1
|
|
63
|
+
else
|
|
64
|
+
pixel[1] = origin[1].ceil - 1
|
|
65
|
+
next_crossing[1] = (origin[1] - pixel[1]) * crossing_incr_y
|
|
66
|
+
num_steps += origin[1].ceil - goal[1].floor - 1
|
|
67
|
+
end
|
|
67
68
|
|
|
68
|
-
next_distance = next_crossing.min
|
|
69
|
-
half_delta_x = 0.5 * delta[0]
|
|
70
|
-
setcell = ->(nd) do
|
|
71
|
-
x_average = origin[0] + (prev_distance + nd) * half_delta_x - pixel[0]
|
|
72
|
-
y_difference = (nd - prev_distance).to_f * delta[1]
|
|
73
|
-
cell = @cells[pixel[1] * @width + pixel[0]]
|
|
74
|
-
cell.cover += y_difference
|
|
75
|
-
cell.area += (1.0 - x_average) * y_difference
|
|
76
|
-
end
|
|
77
|
-
num_steps.times do
|
|
78
|
-
setcell.call(next_distance)
|
|
79
|
-
prev_distance = next_distance
|
|
80
|
-
along_x = next_crossing[0] < next_crossing[1]
|
|
81
|
-
pixel += along_x ? Vector[dir_x,0] : Vector[0,dir_y]
|
|
82
|
-
next_crossing += along_x ? Vector[crossing_incr_x, 0.0] : [0.0, crossing_incr_y]
|
|
83
69
|
next_distance = next_crossing.min
|
|
70
|
+
half_delta_x = 0.5 * delta[0]
|
|
71
|
+
setcell = ->(nd) do
|
|
72
|
+
x_average = origin[0] + (prev_distance + nd) * half_delta_x - pixel[0]
|
|
73
|
+
y_difference = (nd - prev_distance).to_f * delta[1]
|
|
74
|
+
cell = @cells[pixel[1] * @width + pixel[0]]
|
|
75
|
+
cell.cover += y_difference
|
|
76
|
+
cell.area += (1.0 - x_average) * y_difference
|
|
77
|
+
end
|
|
78
|
+
num_steps.times do
|
|
79
|
+
setcell.call(next_distance)
|
|
80
|
+
prev_distance = next_distance
|
|
81
|
+
along_x = next_crossing[0] < next_crossing[1]
|
|
82
|
+
pixel += along_x ? Vector[dir_x,0] : Vector[0,dir_y]
|
|
83
|
+
next_crossing += along_x ? Vector[crossing_incr_x, 0.0] : [0.0, crossing_incr_y]
|
|
84
|
+
next_distance = next_crossing.min
|
|
85
|
+
end
|
|
86
|
+
setcell.call(1.0)
|
|
84
87
|
end
|
|
85
|
-
setcell.call(1.0)
|
|
86
88
|
end
|
|
87
89
|
end
|
data/lib/skrift/sft.rb
CHANGED
|
@@ -1,84 +1,97 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
module Skrift
|
|
2
|
+
# The scaling/rendering façade: set a scale, look up glyphs, measure and
|
|
3
|
+
# render them. (Formerly named SFT, after libschrift's handle struct.)
|
|
4
|
+
class Renderer
|
|
5
|
+
DOWNWARD_Y = 0x01
|
|
3
6
|
|
|
4
|
-
|
|
7
|
+
attr_accessor :font, :x_scale, :y_scale, :x_offset, :y_offset, :flags
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
def initialize(font)
|
|
10
|
+
@font = font
|
|
11
|
+
@x_scale = 32
|
|
12
|
+
@y_scale = 32
|
|
13
|
+
@x_offset = 0
|
|
14
|
+
@y_offset = 0
|
|
15
|
+
@flags = DOWNWARD_Y
|
|
16
|
+
end
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
# Glyph id for +codepoint+. With +variation+ (a variation selector such as
|
|
19
|
+
# Font::VS_EMOJI), resolve the <codepoint, selector> sequence via cmap-14
|
|
20
|
+
# first, falling back to the plain glyph when the font omits the sequence.
|
|
21
|
+
def lookup(codepoint, variation: nil)
|
|
22
|
+
if variation && (g = font.variation_glyph_id(codepoint, variation))
|
|
23
|
+
return g
|
|
24
|
+
end
|
|
25
|
+
font.glyph_id(codepoint)
|
|
26
|
+
end
|
|
18
27
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
def glyph_bbox(outline)
|
|
29
|
+
box = @font.glyph_bbox(outline)
|
|
30
|
+
raise if !box
|
|
31
|
+
# Transform the bounding box into Renderer coordinate space
|
|
32
|
+
xs = @x_scale.to_f / @font.units_per_em
|
|
33
|
+
ys = @y_scale.to_f / @font.units_per_em
|
|
34
|
+
box[0] = (box[0] * xs + @x_offset).floor
|
|
35
|
+
box[1] = (box[1] * ys + @y_offset).floor
|
|
36
|
+
box[2] = (box[2] * xs + @x_offset).ceil
|
|
37
|
+
box[3] = (box[3] * ys + @y_offset).ceil
|
|
38
|
+
return box
|
|
39
|
+
end
|
|
31
40
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
def gmetrics(glyph) # 149
|
|
42
|
+
return nil if glyph.nil?
|
|
43
|
+
raise "out of bounds" if glyph < 0
|
|
44
|
+
xs = @x_scale.to_f / @font.units_per_em
|
|
45
|
+
adv, lsb = @font.hor_metrics(glyph)
|
|
37
46
|
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
return nil if adv.nil?
|
|
48
|
+
metrics = GMetrics.new(adv * xs, lsb * xs + @x_offset)
|
|
40
49
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
outline = @font.outline_offset(glyph)
|
|
51
|
+
return metrics if outline.nil?
|
|
52
|
+
bbox = glyph_bbox(outline)
|
|
53
|
+
return nil if !bbox
|
|
54
|
+
metrics.min_width = bbox[2] - bbox[0] + 1
|
|
55
|
+
metrics.min_height = bbox[3] - bbox[1] + 1
|
|
56
|
+
metrics.y_offset = @flags & DOWNWARD_Y != 0 ? bbox[3] : bbox[1]
|
|
57
|
+
return metrics
|
|
58
|
+
end
|
|
50
59
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
def lmetrics
|
|
61
|
+
hhea = font.reqtable("hhea")
|
|
62
|
+
factor = @y_scale.to_f / @font.units_per_em
|
|
63
|
+
LMetrics.new(
|
|
64
|
+
font.geti16(hhea + 4) * factor, # ascender
|
|
65
|
+
font.geti16(hhea + 6) * factor, # descender
|
|
66
|
+
font.geti16(hhea + 8) * factor # line_gap
|
|
67
|
+
)
|
|
68
|
+
end
|
|
60
69
|
|
|
61
|
-
|
|
62
|
-
outline
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
70
|
+
# Rasterise +glyph+ into +image+ (mutated in place). Returns true if the
|
|
71
|
+
# glyph had a renderable outline, false otherwise (e.g. a space).
|
|
72
|
+
def render(glyph, image) # 239
|
|
73
|
+
outline = @font.outline_offset(glyph)
|
|
74
|
+
return false if outline.nil?
|
|
75
|
+
bbox = glyph_bbox(outline)
|
|
76
|
+
return false if !bbox
|
|
77
|
+
# Set up the transformation matrix such that
|
|
78
|
+
# the transformed bounding boxes min corner lines
|
|
79
|
+
# up with the (0, 0) point.
|
|
80
|
+
xr = [@x_scale.to_f / @font.units_per_em, 0.0, @x_offset - bbox[0]]
|
|
81
|
+
ys = @y_scale.to_f / @font.units_per_em
|
|
82
|
+
if @flags.allbits?(DOWNWARD_Y)
|
|
83
|
+
transform = [xr, [0.0, -ys, bbox[3] - @y_offset]]
|
|
84
|
+
else
|
|
85
|
+
transform = [xr, [0.0, +ys, @y_offset - bbox[1] ]]
|
|
86
|
+
end
|
|
87
|
+
outl = @font.decode_outline(outline)
|
|
88
|
+
return false unless outl
|
|
89
|
+
outl.render(transform, image)
|
|
90
|
+
true
|
|
76
91
|
end
|
|
77
|
-
outl = @font.decode_outline(outline)
|
|
78
|
-
outl.render(transform, image) if outl
|
|
79
|
-
end
|
|
80
92
|
|
|
81
|
-
|
|
82
|
-
|
|
93
|
+
def kerning(left_glyph, right_glyph) # 176
|
|
94
|
+
@font.kerning[[left_glyph,right_glyph].pack("n*")]
|
|
95
|
+
end
|
|
83
96
|
end
|
|
84
97
|
end
|
data/lib/skrift/version.rb
CHANGED