skrift-boxdrawing 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3c7c06fda622d08432995d4b07d25eefcb5b0ad3afc418e43c90db3cfae4b5d5
4
+ data.tar.gz: ff2ba884bb2d66d7f6b4b687651516e53c255a9c5b8cb78964f967e1175820d4
5
+ SHA512:
6
+ metadata.gz: c24368fed3cf853540ff2fd951f9b4e7e334ef0a8da2aca8fc5bf1e7b55bec0d10024cd358e0fd3c950a10fa3c3df2b1f875c1dbc2c23f1e9108276ff2bdb05a
7
+ data.tar.gz: 86c8f031e4fc1122abb238c70784d01a798f492672aa7133746231548565f97f0a31b870c4ea1f8596859c7dee615764e72b2e4d4c843021c77c95e09e02ff13
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
6
+
7
+ # skrift is developed alongside this plugin. To work against a local checkout,
8
+ # set a per-machine override (stored in ~/.bundle/config, never committed):
9
+ #
10
+ # bundle config set --local local.skrift /path/to/skrift
11
+ gem "skrift", git: "https://github.com/vidarh/skrift.git", branch: "master"
12
+
13
+ gem "rake", "~> 13.0"
14
+ gem "rspec", "~> 3.0"
data/LICENSE.txt ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ © 2026 Vidar Hokstad
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # skrift-boxdrawing
2
+
3
+ A [Skrift](https://github.com/vidarh/skrift) plugin that renders Unicode
4
+ box-drawing characters (U+2500–U+257F) as crisp alpha buffers for a given cell
5
+ size, **independent of any font**. Fonts vary wildly in how (and whether) they
6
+ draw the box-drawing block; rendering it geometrically gives pixel-accurate,
7
+ seam-free boxes at any size — useful for terminals, TUIs and bars.
8
+
9
+ It has no X11 (or any platform) dependency: it produces `Skrift::Image` alpha
10
+ buffers, which you composite however you like.
11
+
12
+ ## Usage
13
+
14
+ Wire it into a `Skrift::GlyphCache` through the `special:` hook, so box-drawing
15
+ codepoints are drawn geometrically and everything else falls through to the
16
+ font:
17
+
18
+ ```ruby
19
+ require "skrift"
20
+ require "skrift/boxdrawing"
21
+
22
+ boxes = nil
23
+ cache = Skrift::GlyphCache.new("DejaVuSansMono.ttf", x_scale: 16, y_scale: 16,
24
+ special: ->(cp) { boxes.glyph(cp) })
25
+ boxes = Skrift::BoxDrawing.new(cache.cell_width, cache.cell_height)
26
+
27
+ cache.glyph(0x2500) # => a horizontal-line RenderedGlyph, not the font's glyph
28
+ ```
29
+
30
+ Or use it directly:
31
+
32
+ ```ruby
33
+ boxes = Skrift::BoxDrawing.new(cell_width, cell_height)
34
+ img = boxes.glyph(0x250C) # => Skrift::Image (alpha), or nil if cp isn't a box char
35
+ ```
36
+
37
+ ## License
38
+
39
+ ISC. See `LICENSE.txt`.
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/core/rake_task"
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task default: :spec
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Skrift
4
+ class BoxDrawing
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,237 @@
1
+ require "skrift"
2
+ require_relative "boxdrawing/version"
3
+
4
+ module Skrift
5
+ # Renders Unicode box-drawing glyphs (U+2500..U+257F) as crisp alpha Images
6
+ # for a fixed cell size, independent of any font. Wire it into a renderer via
7
+ # Skrift::GlyphCache's `special:` hook:
8
+ #
9
+ # boxes = Skrift::BoxDrawing.new(cache.cell_width, cache.cell_height)
10
+ # cache = Skrift::GlyphCache.new(font, x_scale: 16, y_scale: 16,
11
+ # special: ->(cp) { boxes.glyph(cp) })
12
+ class BoxDrawing
13
+ def initialize(boxw, boxh)
14
+ @boxw, @boxh = boxw, boxh
15
+ @cache = {}
16
+ end
17
+
18
+ def stride = (@boxw + 3) & ~3
19
+
20
+ # A Skrift::Image for a box-drawing codepoint, the full block █ (U+2588),
21
+ # or nil otherwise.
22
+ def glyph(cp)
23
+ return nil unless (0x2500..0x257f).include?(cp) || cp == 0x2588
24
+ @cache[cp] ||= cache_box(cp)
25
+ end
26
+
27
+ private
28
+
29
+ def boxw = @boxw
30
+ def boxh = @boxh
31
+
32
+ def empty_box_image
33
+ img = Image.new(stride, boxh)
34
+ img.pixels = Array.new(img.width*img.height,0x00)
35
+ img
36
+ end
37
+
38
+ # The full block █ must cover the whole cell so a run of them is solid. A
39
+ # font draws its glyph slightly larger than the cell so adjacent blocks
40
+ # overlap; fitting that into the fixed cell shrinks it (aspect preserved)
41
+ # and leaves a border, so fill the cell exactly here instead.
42
+ def full_block
43
+ img = empty_box_image
44
+ boxh.times { |y| boxw.times { |x| img.pixels[y * stride + x] = 255 } }
45
+ img
46
+ end
47
+
48
+ def cache_box(ch)
49
+ return full_block if ch.ord == 0x2588
50
+
51
+ hx = (boxw+1)/2
52
+ hy = (boxh+1)/2
53
+ yoff = hy*stride
54
+ img = nil
55
+ h = 1 # 1/3 width of "heavy" line
56
+
57
+ lh = light_h = [-255, 0,255, 0, 255]; hh = heavy_h = [-255, -h,255, h, 255]
58
+ lv = light_v = [ 0,-255, 0,255, 255]; hv = heavy_v = [ -h,-255, h,255, 255]
59
+
60
+ ll = light_l = [-255, 0, 0, 0, 255]; hl = heavy_l = [-255, -h, 0, h, 255]
61
+ lu = light_u = [ 0,-255, 0, 0, 255]; hu = heavy_u = [ -h,-255, h, 0, 255]
62
+ lr = light_r = [ 0, 0,255, 0, 255]; hr = heavy_r = [ 0, -h,255, h, 255]
63
+ ld = light_d = [ 0, 0, 0,255, 255]; hd = heavy_d = [ -h, 0, h,255, 255]
64
+
65
+ hc = [ -h, -h, h, h, 255] # Heavy centre
66
+
67
+ d = 2
68
+ dblc = [-d+1, -d+1, d-1, d-1, 0] # Gap in centre of double lines.
69
+ light_vc = [0, -d, 0, d, 255] # Light vertical crossing centre of double line.
70
+
71
+ dh = double_h = [-255,-d,255,-d, 255] + [-255, d,255, d, 255]
72
+ dv = double_v = [-d,-255,-d,255, 255] + [d,-255,d,255, 255]
73
+
74
+ mask_vbar = [-d+1,-255,d-1,255,0]
75
+ mask_hbar = [-255,-d+1,255,d-1,0]
76
+
77
+ mt= masktop = [-255,-255, 255, -1, 0]; maskdtop = [-255,-255, 255,-d-1, 0]
78
+ ml=maskleft = [-255,-255, -1, 255, 0]; maskdleft = [-255,-255,-d-1, 255, 0]
79
+ mb=maskbottom = [-255, 1, 255, 255, 0]; maskdbottom= [-255, d+1, 255, 255, 0]
80
+ mr=maskright = [ 1,-255, 255, 255, 0]; maskdright = [ d+1,-255, 255, 255, 0]
81
+
82
+ mask_lbar = [-255,-d+1,d-1,d-1, 0]; dlbar=double_lbar = [-255,-d,d,d, 255]
83
+ mask_tbar = [-d+1,-255,d-1,d-1, 0]; dtbar=double_tbar = [-d,-255,d,d, 255]
84
+ mask_rbar = [-d+1,-d+1,255,d-1, 0]; drbar=double_rbar = [-d,-d,255,d, 255]
85
+ mask_dbar = [-d+1,-d+1,d-1,255, 0]; ddbar=double_dbar = [-d,-d,d,255, 255]
86
+
87
+ mask_c = [0,0,0,0,0]
88
+
89
+ hdl = heavy_dl = [heavy_d, heavy_l, hc]
90
+ hdr = heavy_dr = [heavy_d, heavy_r, hc]
91
+ hlu = heavy_lu = [heavy_l, heavy_u, hc]
92
+ hru = heavy_ru = [heavy_r, heavy_u, hc]
93
+ ldl = light_dl = [light_d, light_l]
94
+ ldr = light_dr = [light_d, light_r]
95
+ llu = light_lu = [light_l, light_u]
96
+ lru = light_ru = [light_r, light_u]
97
+
98
+ # FIXME Makes stipples wider when tx/ty etc. are big enough
99
+ tx = hx - boxw / 3
100
+ mv2 = mask_v2 = [[-tx, -255, -tx, 255, 0], [tx, -255, tx, 255,0]]
101
+
102
+ ty = hy - boxh / 3
103
+ mh2 = mask_h2 = [[-255,-ty, 255,-ty,0], [-255,ty,255,ty,0]]
104
+
105
+ # Three-gap masks for the quadruple-dash glyphs.
106
+ qx = boxw / 4
107
+ mv3 = mask_v3 = [[-qx,-255,-qx,255,0], [0,-255,0,255,0], [qx,-255,qx,255,0]]
108
+ qy = boxh / 4
109
+ mh3 = mask_h3 = [[-255,-qy,255,-qy,0], [-255,0,255,0,0], [-255,qy,255,qy,0]]
110
+
111
+ rects = {
112
+ 0x2500 => lh, 0x2501 => hh, 0x2502 => lv, 0x2503 => hv,
113
+ # Dashed lines are cut into segments by erase masks across the line:
114
+ # horizontal masks (mh) dash a vertical line, vertical masks (mv) a
115
+ # horizontal one. Two gaps give the triple dash, three the quadruple.
116
+ 0x2504 => lh + mv2, 0x2505 => hh + mv2, 0x2506 => lv + mh2, 0x2507 => hv + mh2,
117
+ 0x2508 => lh + mv3, 0x2509 => hh + mv3, 0x250A => lv + mh3, 0x250B => hv + mh3,
118
+
119
+ 0x250C => ldr, 0x250D => hr + ld, 0x250E => hd + lr, 0x250F => hdr,
120
+ 0x2510 => ldl, 0x2511 => hl + ld, 0x2512 => [hd, ll], 0x2513 => hdl,
121
+ 0x2514 => lru, 0x2515 => lu + hr, 0x2516 => lr + hu, 0x2517 => hru,
122
+ 0x2518 => llu, 0x2519 => [hl, lu], 0x251a => [ll, hu], 0x251b => hlu,
123
+ 0x251c => lv+lr, 0x251d => [lv, hr], 0x251e => ldr + hu, 0x251f => [lru, hd],
124
+
125
+ 0x2520 => hv + lr, 0x2521 => hru + ld, 0x2522 => hdr + lu, 0x2523 => hv + hr,
126
+ 0x2524 => ll + lv, 0x2525 => hl + lv, 0x2526 => hu + ldl, 0x2527 => hd + lu,
127
+ 0x2528 => hv + ll, 0x2529 => hlu + ld, 0x252a => hdl+lu, 0x252b => hl + hv,
128
+ 0x252c => lh + ld, 0x252d => [hl, ldr], 0x252e => [hr, ldr], 0x252f => [hh, ld],
129
+
130
+ 0x2530 => [lh, hd], 0x2531 => [hdl, lr], 0x2532 => [ll, hdr], 0x2533 => [hh, hd],
131
+ 0x2534 => [lh, lu], 0x2535 => [lru, hl], 0x2536 => [llu, hr], 0x2537 => [hh, lu],
132
+ 0x2538 => [lh, hu], 0x2539 => [hlu, lr], 0x253a => [hru, ll], 0x253b => [hh, hu],
133
+ 0x253c => [lv, lh], 0x253d => lv+lr+hl, 0x253e => ll+lv+hr, 0x253f => [lv, hh],
134
+
135
+ 0x2540 => [lh, ld, hu], 0x2541 => [light_h, light_u, heavy_d], 0x2542 => [heavy_v, light_h], 0x2543 => [heavy_l, heavy_u, hc, light_d, light_r],
136
+ 0x2544 => [ll, ld, hru], 0x2545 => [heavy_dl, light_ru], 0x2546 => [light_lu, heavy_dr], 0x2547 => [heavy_h, heavy_u, light_d],
137
+ 0x2548 => [hh, hd, light_u], 0x2549 => [heavy_v, heavy_l, light_r], 0x254a => [heavy_v, light_l, heavy_r], 0x254b => [heavy_v, heavy_h],
138
+ 0x254c => [lh, dblc], 0x254d => [heavy_h, dblc], 0x254e => [light_v, dblc], 0x254f => [heavy_v, dblc],
139
+
140
+ 0x2550 => dh, 0x2551 => dv, 0x2552 => [dh, lv, ml, maskdtop], 0x2553 => [dv, lh, mt, maskdleft],
141
+ 0x2554 => [ddbar, drbar, mask_dbar, mask_rbar], 0x2555 => [dh, mr, lv, maskdtop],
142
+ 0x2556 => [dv, lh,maskdright,mt], 0x2557 => [double_dbar, double_lbar, mask_dbar, mask_lbar],
143
+ 0x2558 => [lv, dh, ml, maskdbottom], 0x2559 => [lh, dv, maskdleft, mb],
144
+ 0x255A => [dtbar, double_rbar, mask_tbar, mask_rbar], 0x255B => [dh, lv, maskdbottom, mr],
145
+ 0x255C => [dv, lh, mb, maskdright], 0x255D => [dtbar, dlbar, mask_tbar, mask_lbar],
146
+ 0x255e => [lv, dh, maskleft], 0x255f => [lh, dv, dblc, maskdleft],
147
+ 0x2560 => [dv, drbar, mask_rbar, mask_vbar], 0x2561 => [lv, dh, maskright],
148
+ 0x2562 => [dv, ll, dblc], 0x2563 => [dlbar, dv, mask_lbar, mask_vbar],
149
+ 0x2564 => [dh, ld, dblc], 0x2565 => [lh, dv, mt],
150
+ 0x2566 => [dh, ddbar,mask_hbar, mask_dbar], 0x2567 => [dh, light_u,dblc],
151
+ 0x2568 => [lh, dv, mb], 0x2569 => [dh, double_tbar, mask_hbar, mask_tbar],
152
+ 0x256A => dh+lv, 0x256b => dv+lh, 0x256c => [dh, dv, mask_hbar, mask_vbar],
153
+
154
+ # 256d, 256e, 256f, 2570 => curves
155
+ # FIXME: Current is just very slightly rounded.
156
+ 0x256d => [ldr, mask_c], 0x256e => [ldl, mask_c], 0x256f => [llu, mask_c], 0x2570 => [lru, mask_c],
157
+
158
+ # 2571, 2572. 2573 => diagonals, handled below.
159
+ 0x2571 => nil, 0x2572 => nil, 0x2573 => nil,
160
+
161
+ 0x2574 => ll, 0x2575 => lu, 0x2576 => lr, 0x2577 => ld,
162
+ 0x2578 => hl, 0x2579 => hu, 0x257a => hr, 0x257b => hd,
163
+ 0x257c => ll + hr, 0x257d => lu + hd, 0x257e => hl + lr, 0x257f => hu + ld
164
+
165
+ }
166
+
167
+ r = rects[ch.ord]
168
+ if r
169
+ img = empty_box_image
170
+ r.flatten.each_slice(5) do |rect|
171
+ x1,y1,x2,y2, col = *rect
172
+ x1 = (x1+hx).clamp(0,boxw-1)
173
+ x2 = (x2+hx).clamp(0,boxw-1)
174
+ y1 = (y1+hy).clamp(0,boxh-1)
175
+ y2 = (y2+hy).clamp(0,boxh-1)
176
+ col ||= 255
177
+
178
+ a = Array.new(x2-x1+1,col)
179
+
180
+ (y1..y2).each do |y|
181
+ img.pixels[y*stride + x1 .. y*stride + x2] = a
182
+ end
183
+ end
184
+ return img
185
+ end
186
+
187
+ if ch.ord == 0x2571
188
+ img = empty_box_image
189
+ slope = boxw.to_f / boxh
190
+ x = boxw.to_f-1
191
+ (0...boxh).each do |y|
192
+ err = x - x.to_i
193
+ img.pixels[y*stride+x.ceil] = 255 #(255*(1-err)).floor
194
+ img.pixels[y*stride+x.ceil-1] = (255*err).floor if x > 0
195
+ x-= slope
196
+ end
197
+ return img
198
+ end
199
+
200
+ # FIXME: If size is *at* certain levels level, this, and the next one fails?
201
+
202
+ if ch.ord == 0x2572
203
+ img = empty_box_image
204
+ slope = boxw.to_f / boxh
205
+ x = boxw.to_f-1
206
+ (0...boxh).each do |y|
207
+ err = x - x.to_i
208
+ img.pixels[y*stride+boxw-x.ceil-1] = 255
209
+ img.pixels[y*stride+boxw-x.ceil] = (255*err).floor if x > 0
210
+ x-= slope
211
+ end
212
+ return img
213
+ end
214
+
215
+ if ch.ord == 0x2573
216
+ img = empty_box_image
217
+ slope = boxw.to_f / boxh
218
+ x = boxw.to_f-1
219
+ (0...boxh).each do |y|
220
+ err = x - x.to_i
221
+ img.pixels[y*stride+boxw-x.ceil-1] = 255
222
+ img.pixels[y*stride+boxw-x.ceil] = (255*err).floor if x > 0
223
+ img.pixels[y*stride+x.ceil-1] = (255*err).floor if x > 0
224
+ img.pixels[y*stride+x.ceil] = 255 #(255*(1-err)).floor
225
+ x-= slope
226
+ end
227
+ return img
228
+ end
229
+
230
+ if img
231
+ img = img.dup
232
+ img.pixels = img.pixels.dup
233
+ end
234
+ img || empty_box_image
235
+ end
236
+ end
237
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: skrift-boxdrawing
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Vidar Hokstad
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-06-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: skrift
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.3.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 0.3.0
27
+ description: A Skrift plugin that renders Unicode box-drawing characters (U+2500..U+257F)
28
+ as alpha buffers for a given cell size, independent of any font. Wire it in via
29
+ Skrift::GlyphCache's `special:` hook.
30
+ email:
31
+ - vidar@hokstad.com
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - ".rspec"
37
+ - Gemfile
38
+ - LICENSE.txt
39
+ - README.md
40
+ - Rakefile
41
+ - lib/skrift/boxdrawing.rb
42
+ - lib/skrift/boxdrawing/version.rb
43
+ homepage: https://github.com/vidarh/skrift-boxdrawing
44
+ licenses:
45
+ - ISC
46
+ metadata:
47
+ homepage_uri: https://github.com/vidarh/skrift-boxdrawing
48
+ source_code_uri: https://github.com/vidarh/skrift-boxdrawing
49
+ post_install_message:
50
+ rdoc_options: []
51
+ require_paths:
52
+ - lib
53
+ required_ruby_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: 3.0.0
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ requirements: []
64
+ rubygems_version: 3.4.10
65
+ signing_key:
66
+ specification_version: 4
67
+ summary: Crisp Unicode box-drawing glyphs for the Skrift font renderer
68
+ test_files: []