skrift-x11 0.2.1 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 92507a6b036b8ab2eb0145017a938019111e2ea17c4e001a693529d2518876f3
4
- data.tar.gz: 2da4b5247c4e4b085a4f4591594d09f69f2f35a93c7fe43210b68f7efbfe3798
3
+ metadata.gz: ed8b7aa32d9e1eb6984384f6f22488d3d6f5be7f5eef1c671c0226cf7ff431c5
4
+ data.tar.gz: cee3cb6866a431e8f8c2d0e4543a1eed56f85883098b1561902dd52e0641386e
5
5
  SHA512:
6
- metadata.gz: 8bac35a07d623a9f27539ec68d2a2cc4eb2cc906e788660a7cbea25323d55ff30ada7ac1474f92c7caa4b8c92a50a509bf6eaf25550c5e04d6e273a13aebe15f
7
- data.tar.gz: c35c1e39c5b5ef7daf70257dc14e0930ac70607e78b6bc8397563964377d8e090b4472162fbbb061c05c584553ae6d0b7e937e7d3ecc156f534b6eed518c41e5
6
+ metadata.gz: be22707412fa343bb25f8a9ddbd754801090ccf1458041aeb91c18513d8a963d2121eeae293a65d34d6c782d9d7d166f57b8448bb30dd755ae6452a0a3c9bc27
7
+ data.tar.gz: 84b80afb3b159bfbdc5733a397c670305b7c6dfc4d37a8e9cfac57bdb12b775703bf65ddbf62ef5a0eeab32ccad3a8ce270b9eec7cd15b99708f196b2454f2ea
data/example.rb CHANGED
@@ -52,7 +52,7 @@ f = Font.load("resources/FiraGO-Regular_extended_with_NotoSansEgyptianHieroglyph
52
52
  $skrift = Skrift::X11::Glyphs.new(dpy, f, x_scale: 40, y_scale: 40)
53
53
 
54
54
  def redraw(dpy, wid, gc)
55
- dpy.poly_fill_rectangle(wid, gc, [X11::Form::Rectangle.new(20,45, 400, 400)])
55
+ dpy.poly_fill_rectangle(wid, gc, [20,45, 400, 400])
56
56
  $skrift.render_str($pic, 0xffffff, 50,90, 'Pure Ruby w/Skrift!')
57
57
  $skrift.render_str($pic, 0xffffff, 50,140, "And unicode:")
58
58
  $skrift.render_str($pic, 0xff00ff, 50,200, "Μπορώ να φάω σπασμένα γυαλιά χωρίς να πάθω τίποτα.")
@@ -1,40 +1,67 @@
1
+ require 'skrift/boxdrawing'
1
2
 
2
- require 'skrift'
3
- module Skrift
4
- module X11
5
- # XRender constants currently not defined in pure-x11.
6
- # FIXME.
3
+ # Colour-glyph support is optional: only needed when a `color:` font is given.
4
+ begin
5
+ require 'skrift/color'
6
+ rescue LoadError
7
+ # skrift-color not installed; colour rendering will be unavailable.
8
+ end
9
+
10
+ module X11
11
+ # XRender constants currently not defined in pure-x11.
12
+ # FIXME: these are generic XRender enumerants and belong in pure-x11.
13
+ module Form
7
14
  PictOpSrc=1
8
15
  PictOpOver=3
9
16
  CPRepeat = 1
17
+ end
18
+ end
10
19
 
20
+ module Skrift
21
+ module X11
22
+ # Thin XRender adapter over Skrift::GlyphCache. Monochrome glyphs go into an
23
+ # A8 glyph set (coloured at composite time by a solid fill); colour glyphs
24
+ # (emoji, via the skrift-color delegate) go into an ARGB32 glyph set and are
25
+ # composited with a neutral white source so their own colours show. A string
26
+ # is drawn as runs of like-typed glyphs, one composite per run. All the
27
+ # platform-independent work (fallback, rasterisation, layout, caching, box
28
+ # drawing, colour delegation) lives in skrift core + the plugins.
11
29
  class Glyphs
30
+ attr_reader :fixed_width
31
+ attr_accessor :maxheight, :lm
12
32
 
13
- def initialize dpy, font, x_scale:, y_scale:, pic: nil, fixed: nil
33
+ # +color+ (optional) is a font (path, name or Skrift::Font) providing
34
+ # colour glyphs, e.g. an emoji font; requires the skrift-color gem.
35
+ def initialize(dpy, font=nil, fontset: nil, x_scale:, y_scale:, pic: nil, fixed: nil, maxheight: nil, fit: false, color: nil)
14
36
  @dpy = dpy
15
- @sft = SFT.new(font)
16
- @sft.x_scale = x_scale
17
- @sft.y_scale = y_scale
18
37
  @pic = pic
19
- @fixed = fixed
38
+ @maxheight = maxheight
20
39
 
21
- @glyphcache = {}
22
- @colcache = {}
23
- @chcache = {}
40
+ color_renderer = build_color_renderer(color, x_scale, y_scale)
41
+ @cache = Skrift::GlyphCache.new(fontset || font,
42
+ x_scale: x_scale, y_scale: y_scale,
43
+ fixed: fixed, maxheight: maxheight, fit: fit,
44
+ special: ->(cp) { @boxes.glyph(cp) },
45
+ color: color_renderer)
46
+ @boxes = Skrift::BoxDrawing.new(@cache.cell_width, @cache.cell_height)
47
+ @fixed_width = @cache.cell_width
48
+ @lm = Skrift::LMetrics.new(@cache.ascent, @cache.descent, @cache.line_gap)
24
49
 
25
- @lm = @sft.lmetrics
50
+ @colcache = {}
51
+ @gids = {} # codepoint -> [:mono|:color, glyph-set id]
52
+ @nextgid = 1
26
53
 
27
- # The glyph set
28
54
  @gfmt = @dpy.render_find_standard_format(:a8).id
29
55
  @glyphset = @dpy.render_create_glyph_set(@gfmt)
30
- end
31
56
 
32
- attr_accessor :lm
33
- def gm(ch)
34
- gid = @sft.lookup(ch.ord)
35
- @sft.gmetrics(gid)
57
+ if color_renderer
58
+ @cfmt = @dpy.render_find_standard_format(:argb24).id
59
+ @cglyphset = @dpy.render_create_glyph_set(@cfmt)
60
+ @white = @dpy.render_create_solid_fill(0xffff, 0xffff, 0xffff, 0xffff)
61
+ end
36
62
  end
37
-
63
+
64
+ def inspect = "<Glyphs #{object_id}>"
38
65
 
39
66
  def fill_for_col(col)
40
67
  return @colcache[col] if @colcache[col]
@@ -44,89 +71,109 @@ module Skrift
44
71
  g |= g << 8
45
72
  b = col & 0xff
46
73
  b |= b << 8
47
-
48
- @colcache[col] ||= @dpy.render_create_solid_fill(r,g,b,0xffff)
74
+ @colcache[col] = @dpy.render_create_solid_fill(r, g, b, 0xffff)
49
75
  end
50
76
 
51
- def fixed_width
52
- return @fixed_width if @fixed_width
53
- if @fixed
54
- # *Ensure* that the glyphs are equal width.
55
- g = gm("M")
56
- @fixed_width = g.advance_width.ceil
57
- else
58
- nil
77
+ def text_width(str) = @cache.text_width(str)
78
+
79
+ # Draw +str+ as one composite per run of same-typed glyphs (mono vs
80
+ # colour). Each rendered glyph advances the pen by one cell, so a pure-text
81
+ # string is a single A8 composite exactly as before; emoji break the run.
82
+ def render_str(pic, col, x, y, str)
83
+ glyphs = str.to_s.each_char.map { |ch| gsgid_for(ch.ord) }
84
+ rendered = 0
85
+ i = 0
86
+ while i < glyphs.length
87
+ (i += 1; next) if glyphs[i].nil? # unmapped: skip, no advance
88
+ type = glyphs[i].first
89
+ run_x = x + rendered * @fixed_width
90
+ ids = []
91
+ while i < glyphs.length && glyphs[i] && glyphs[i].first == type
92
+ ids << glyphs[i].last
93
+ rendered += 1
94
+ i += 1
95
+ end
96
+ composite_run(type, pic, col, run_x, y, ids)
59
97
  end
60
98
  end
61
-
62
-
63
- def cache_glyph(gid, baseline)
64
- return if gid.nil?
65
- mtx = @sft.gmetrics(gid)
66
-
67
- # FIXME: Not sure what to do if mtx.nil? here.
68
- # Maybe use x/y scale?
69
- w = fixed_width || mtx.min_width || 0
70
- h = mtx.min_height || 1
71
- #p [w,h]
72
- img = Image.new((w+3)&~3, h)
73
- if !@sft.render(gid, img)
74
- #STDERR.puts "Unable to render #{gid}\n"
75
- data = "\0"*(w*h)
99
+
100
+ private
101
+
102
+ def build_color_renderer(color, x_scale, y_scale)
103
+ return nil unless color
104
+ # A caller can pass its own delegate (e.g. one that gates which
105
+ # codepoints render in colour); we use it as-is. Otherwise treat the
106
+ # argument as a font and build an ungated Color::Renderer from it.
107
+ return color if color.respond_to?(:render)
108
+ unless defined?(Skrift::Color::Renderer)
109
+ raise "colour glyphs require the skrift-color gem"
110
+ end
111
+ font = Skrift::FontSet.new(color)[0]
112
+ Skrift::Color::Renderer.new(font, x_scale: x_scale, y_scale: y_scale)
113
+ end
114
+
115
+ def composite_run(type, pic, col, x, y, ids)
116
+ if type == :color
117
+ @dpy.render_composite_glyphs32(:PictOpOver, @white, pic, @cfmt, @cglyphset, 0, 0, [x, y, ids])
76
118
  else
77
- data = img.pixels.pack("C*")
119
+ @dpy.render_composite_glyphs32(:PictOpOver, fill_for_col(col), pic, @gfmt, @glyphset, 0, 0, [x, y, ids])
78
120
  end
79
-
80
- yoff = mtx.y_offset || baseline
81
-
82
- info = ::X11::Form::GlyphInfo.new(
83
- img.width, # w
84
- img.height, # h
85
- -mtx.left_side_bearing, # x
86
- yoff-baseline, # y
87
- fixed_width || mtx.advance_width, # || mtx.advance_width, # x_off
88
- 0
89
- )
90
-
91
- @dpy.render_add_glyphs(@glyphset, gid, info, data)
92
- @glyphcache[gid] = mtx.advance_width#-mtx.left_side_bearing
93
121
  end
94
122
 
95
- def text_width(str)
96
- gl = map_glyphs(str)
97
- # We *presume* that if you call text_width, you intend
98
- # to render the string. Maybe we shouldn't?
99
- cache_glyphs(gl)
100
- gl.inject(0) {|sum,gl|
101
- @glyphcache[gl].to_i + sum
102
- }
123
+ # [:mono|:color, glyph-set id] for a codepoint, uploading on first use, or
124
+ # nil for a codepoint no font maps (and which isn't box drawing/colour).
125
+ def gsgid_for(cp)
126
+ return @gids[cp] if @gids.key?(cp)
127
+ @gids[cp] = upload(cp)
103
128
  end
104
-
105
- def map_glyphs(str)
106
- # FIXME: Should probably cache by character rather than
107
- # glyph
108
- str.to_s.each_char.map do |ch|
109
- @chcache[ch] ||= @sft.lookup(ch.ord).to_i
129
+
130
+ def upload(cp)
131
+ # NUL doubles as the double-width spacer (the trailing cell of a wide
132
+ # glyph): a blank cell that only advances, letting the wide glyph in the
133
+ # previous cell overflow into it.
134
+ return upload_blank if cp.zero?
135
+
136
+ g = @cache.glyph(cp)
137
+ return nil unless g
138
+
139
+ gsgid = @nextgid
140
+ @nextgid += 1
141
+ if g.color?
142
+ @dpy.render_add_glyphs(@cglyphset, gsgid, glyph_info(g), argb_data(g.rgba))
143
+ [:color, gsgid]
144
+ elsif g.alpha
145
+ @dpy.render_add_glyphs(@glyphset, gsgid, glyph_info(g), g.alpha.pack("C*"))
146
+ [:mono, gsgid]
147
+ else
148
+ upload_blank # no outline (e.g. space)
110
149
  end
111
150
  end
112
-
113
- def cache_glyphs(gl)
114
- gl.each do |gid|
115
- data = @glyphcache[gid]
116
- if !data
117
- data = cache_glyph(gid, @lm.ascender)
118
- end
119
- end
151
+
152
+ # A blank glyph that only advances one cell.
153
+ def upload_blank
154
+ gsgid = @nextgid
155
+ @nextgid += 1
156
+ info = ::X11::Form::GlyphInfo.new(0, 0, 0, 0, @fixed_width, 0)
157
+ @dpy.render_add_glyphs(@glyphset, gsgid, info, "".b)
158
+ [:mono, gsgid]
120
159
  end
121
-
122
- def render_str(pic, col, x,y, str)
123
- fill = fill_for_col(col)
124
- gl = map_glyphs(str)
125
- cache_glyphs(gl)
126
- @dpy.render_composite_glyphs32(
127
- PictOpOver, fill, pic, @gfmt,
128
- @glyphset, 0,0, [x, y, gl]
129
- )
160
+
161
+ def glyph_info(g)
162
+ ::X11::Form::GlyphInfo.new(g.width, g.height,
163
+ -g.left_side_bearing, g.y_offset - @cache.baseline,
164
+ @fixed_width, 0)
165
+ end
166
+
167
+ # Pack straight-alpha 0xRRGGBBAA pixels as premultiplied ARGB32 in the
168
+ # little-endian byte order XRender's argb format expects (B, G, R, A).
169
+ def argb_data(rgba)
170
+ rgba.map do |px|
171
+ a = px & 0xff
172
+ r = (((px >> 24) & 0xff) * a) / 255
173
+ g = (((px >> 16) & 0xff) * a) / 255
174
+ b = (((px >> 8) & 0xff) * a) / 255
175
+ (a << 24) | (r << 16) | (g << 8) | b
176
+ end.pack("L<*")
130
177
  end
131
178
  end
132
179
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Skrift
4
4
  module X11
5
- VERSION = "0.2.1"
5
+ VERSION = "0.3.0"
6
6
  end
7
7
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/skrift/x11/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "skrift-x11"
7
+ spec.version = Skrift::X11::VERSION
8
+ spec.authors = ["Vidar Hokstad"]
9
+ spec.email = ["vidar@hokstad.com"]
10
+
11
+ spec.summary = "Helpers to use the pure-Ruvy TrueType engine Skrift with pure Pure-X11 X bindings"
12
+ #spec.description = "TODO: Write a longer description or delete this line."
13
+ #spec.homepage = "TODO: Put your gem's website or public repo URL here."
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 2.6.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"] = "TODO: Put your gem's public repo URL here."
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
+ (File.expand_path(f) == __FILE__) ||
28
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
29
+ end
30
+ end
31
+ spec.bindir = "exe"
32
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
33
+ spec.require_paths = ["lib"]
34
+
35
+ # Uncomment to register a new dependency of your gem
36
+ # spec.add_dependency "example-gem", "~> 1.0"
37
+ spec.add_dependency "skrift", ">= 0.3.0"
38
+ spec.add_dependency "skrift-boxdrawing", ">= 0.2.0"
39
+ spec.add_dependency "pure-x11"
40
+
41
+ # For more information and examples about making a new gem, check out our
42
+ # guide at: https://bundler.io/guides/creating_gem.html
43
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: skrift-x11
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vidar Hokstad
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-11-12 00:00:00.000000000 Z
11
+ date: 2026-06-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: skrift
@@ -16,14 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: 0.3.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: 0.3.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: skrift-boxdrawing
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.2.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 0.2.0
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: pure-x11
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -55,6 +69,7 @@ files:
55
69
  - lib/skrift/x11/version.rb
56
70
  - resources/FiraGO-Regular_extended_with_NotoSansEgyptianHieroglyphs-Regular.ttf
57
71
  - sig/skrift/x11.rbs
72
+ - skrift-x11.gemspec
58
73
  homepage:
59
74
  licenses:
60
75
  - MIT