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.
@@ -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
@@ -1,59 +1,53 @@
1
- def midpoint(a, b) = (a+b)*0.5
1
+ module Skrift
2
+ class Outline
3
+ include Geometry
2
4
 
3
- # Applies an affine linear transformation matrix to a set of points
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
- class Outline
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
- def clip_points(width, height)
21
- @points.each do |pt|
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
- def render(transform, image)
28
- transform_points(transform, @points)
29
- clip_points(image.width, image.height)
30
- buf = Raster.new(image.width, image.height)
31
- @segments.each do |seg|
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
- def tesselate_curve(curve)
39
- if is_flat(curve)
40
- @segments << Segment[curve.beg, curve.end]
41
- return
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
- # A heuristic to tell whether a given curve can be approximated closely enough by a line. */
54
- def is_flat(curve)
55
- g = @points[curve.ctrl] - @points[curve.beg]
56
- h = @points[curve.end] - @points[curve.beg]
57
- (g[0]*h[1]-g[1]*h[0]).abs <= 2.0
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
- class Raster
2
- Cell = Struct.new(:area, :cover)
1
+ module Skrift
2
+ class Raster
3
+ Cell = Struct.new(:area, :cover)
3
4
 
4
- Vector = Struct.new(:x,:y) do
5
- def*(f) = Vector[x*f,y*f]
6
- def+(o) = Vector[x+o[0],y+o[1]]
7
- def-(o) = Vector[x-o[0],y-o[1]]
8
- def min = (x < y ? x : y)
9
- end
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
- # Integrate the values in the buffer to arrive at the final grayscale image.
18
- def post_process
19
- accum = 0.0
20
- (@width*@height).times.collect do |i|
21
- cell = @cells[i]
22
- value = (accum + cell.area).abs
23
- value = [value, 1.0].min * 255.0 + 0.5
24
- accum += cell.cover
25
- value.to_i & 0xff
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
- # Draws a line into the buffer. Uses a custom 2D raycasting algorithm to do so.
30
- def draw_line(origin, goal)
31
- prev_distance = 0.0
32
- num_steps = 0
33
- delta = goal-origin
34
- dir_x = delta[0] <=> 0
35
- dir_y = delta[1] <=> 0
36
- next_crossing = Vector[0.0,0.0]
37
- pixel = Vector[0,0]
38
- return if dir_y == 0
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
- crossing_incr_x = dir_x != 0 ? (1.0 / delta[0]).abs : 1.0
41
- crossing_incr_y = (1.0 / delta[1]).abs
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
- if dir_x == 0
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] = crossing_incr_x - (origin[0] - pixel[0]) * crossing_incr_x
50
- num_steps += goal[0].ceil - origin[0].floor - 1
46
+ next_crossing[0] = 100.0
51
47
  else
52
- pixel[0] = origin[0].ceil - 1
53
- next_crossing[0] = (origin[0] - pixel[0]) * crossing_incr_x
54
- num_steps += origin[0].ceil - goal[0].floor - 1
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
- if dir_y > 0
59
- pixel[1] = origin[1].floor
60
- next_crossing[1] = crossing_incr_y - (origin[1] - pixel[1]) * crossing_incr_y
61
- num_steps += goal[1].ceil - origin[1].floor - 1
62
- else
63
- pixel[1] = origin[1].ceil - 1
64
- next_crossing[1] = (origin[1] - pixel[1]) * crossing_incr_y
65
- num_steps += origin[1].ceil - goal[1].floor - 1
66
- end
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
- class SFT
2
- DOWNWARD_Y = 0x01
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
- attr_accessor :font, :x_scale, :y_scale, :x_offset, :y_offset, :flags
7
+ attr_accessor :font, :x_scale, :y_scale, :x_offset, :y_offset, :flags
5
8
 
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
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
- def lookup(codepoint)
16
- return font.glyph_id(codepoint)
17
- end
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
- 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
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
- def gmetrics(glyph) # 149
33
- return nil if glyph.nil?
34
- raise "out of bounds" if glyph < 0
35
- xs = @x_scale.to_f / @font.units_per_em
36
- adv, lsb = @font.hor_metrics(glyph)
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
- return nil if adv.nil?
39
- metrics = GMetrics.new(adv * xs, lsb * xs + @x_offset)
47
+ return nil if adv.nil?
48
+ metrics = GMetrics.new(adv * xs, lsb * xs + @x_offset)
40
49
 
41
- outline = @font.outline_offset(glyph)
42
- return metrics if outline.nil?
43
- bbox = glyph_bbox(outline)
44
- return nil if !bbox
45
- metrics.min_width = bbox[2] - bbox[0] + 1
46
- metrics.min_height = bbox[3] - bbox[1] + 1
47
- metrics.y_offset = @flags & SFT::DOWNWARD_Y != 0 ? bbox[3] : bbox[1]
48
- return metrics
49
- end
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
- def lmetrics
52
- hhea = font.reqtable("hhea")
53
- factor = @y_scale.to_f / @font.units_per_em
54
- LMetrics.new(
55
- font.geti16(hhea + 4) * factor, # ascender
56
- font.geti16(hhea + 6) * factor, # descender
57
- font.geti16(hhea + 8) * factor # line_gap
58
- )
59
- end
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
- def render(glyph, image) # 239
62
- outline = @font.outline_offset(glyph)
63
- return false if outline.nil?
64
- return true if outline.nil?
65
- bbox = glyph_bbox(outline)
66
- return false if !bbox
67
- # Set up the transformation matrix such that
68
- # the transformed bounding boxes min corner lines
69
- # up with the (0, 0) point.
70
- xr = [@x_scale.to_f / @font.units_per_em, 0.0, @x_offset - bbox[0]]
71
- ys = @y_scale.to_f / @font.units_per_em
72
- if @flags.allbits?(SFT::DOWNWARD_Y)
73
- transform = [xr, [0.0, -ys, bbox[3] - @y_offset]]
74
- else
75
- transform = [xr, [0.0, +ys, @y_offset - bbox[1] ]]
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
- def kerning(left_glyph, right_glyph) # 176
82
- @font.kerning[[left_glyph,right_glyph].pack("n*")]
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Skrift
4
- VERSION = "0.2.1"
4
+ VERSION = "0.4.0"
5
5
  end