skrift 0.1.0 → 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,56 +1,53 @@
1
- def midpoint(a, b); 0.5*(a+b); end
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 {|pt| pt[0],pt[1] = *(trf * Vector[*pt,1]) }
6
- end
5
+ Segment = Struct.new(:beg, :end, :ctrl)
7
6
 
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
7
+ attr_reader :points, :segments
16
8
 
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)
9
+ def initialize
10
+ @points, @segments = [], []
21
11
  end
22
- end
23
12
 
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])
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
30
18
  end
31
- image.pixels = buf.post_process
32
- return image
33
- end
34
19
 
35
- def tesselate_curve(curve)
36
- if is_flat(curve)
37
- @segments << Segment.new(curve.beg, curve.end)
38
- 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])
39
44
  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
45
 
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
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
55
52
  end
56
53
  end
data/lib/skrift/raster.rb CHANGED
@@ -1,80 +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
- 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
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
9
11
 
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
12
+ def initialize width, height
13
+ @width = width
14
+ @height = height
15
+ @cells = (0..(width*height-1)).map { Cell[0.0,0.0] }
19
16
  end
20
- end
21
17
 
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
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
28
+ end
29
+
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
32
40
 
33
- crossing_incr_x = dir_x != 0 ? (1.0 / delta[0]).abs : 1.0
34
- 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
35
43
 
36
- if dir_x == 0
37
- pixel[0] = origin[0].floor
38
- next_crossing[0] = 100.0
39
- else
40
- if dir_x > 0
44
+ if dir_x == 0
41
45
  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
46
+ next_crossing[0] = 100.0
44
47
  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
+ 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
48
57
  end
49
- end
50
58
 
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
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
60
68
 
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
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)
77
87
  end
78
- setcell.call(1.0)
79
88
  end
80
89
  end
data/lib/skrift/sft.rb CHANGED
@@ -1,83 +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
- raise "out of bounds" if glyph < 0
34
- xs = @x_scale.to_f / @font.units_per_em
35
- 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)
36
46
 
37
- return nil if adv.nil?
38
- 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)
39
49
 
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
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
49
59
 
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
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
59
69
 
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] ]])
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
75
91
  end
76
- outl = @font.decode_outline(outline)
77
- outl.render(transform, image)
78
- end
79
92
 
80
- def kerning(left_glyph, right_glyph) # 176
81
- @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
82
96
  end
83
97
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Skrift
4
- VERSION = "0.1.0"
4
+ VERSION = "0.4.0"
5
5
  end