tessella 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.
Files changed (188) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +900 -0
  4. data/doc/README.md +74 -0
  5. data/doc/bitfield.md +123 -0
  6. data/doc/charts.md +1370 -0
  7. data/doc/check.rb +75 -0
  8. data/doc/cli.md +483 -0
  9. data/doc/composition.md +599 -0
  10. data/doc/getting-started.md +121 -0
  11. data/doc/glyphs.md +134 -0
  12. data/doc/images.md +325 -0
  13. data/doc/library.md +239 -0
  14. data/doc/lint.md +270 -0
  15. data/doc/meter.md +210 -0
  16. data/doc/rule.md +51 -0
  17. data/doc/seq.md +169 -0
  18. data/doc/sparkline.md +63 -0
  19. data/doc/table.md +347 -0
  20. data/doc/targets.md +222 -0
  21. data/doc/text.md +84 -0
  22. data/doc/tree.md +108 -0
  23. data/doc/wave.md +122 -0
  24. data/examples/README.md +103 -0
  25. data/examples/ages.txt +9 -0
  26. data/examples/bad.csv +3 -0
  27. data/examples/boxes.txt +3 -0
  28. data/examples/broken.txt +4 -0
  29. data/examples/calibration.rb +12 -0
  30. data/examples/cities.csv +4 -0
  31. data/examples/cjk.rb +13 -0
  32. data/examples/comment.rb +9 -0
  33. data/examples/comment.sh +13 -0
  34. data/examples/cpu.txt +60 -0
  35. data/examples/dashboard.sh +25 -0
  36. data/examples/datasheet.rb +16 -0
  37. data/examples/disc.pgm +0 -0
  38. data/examples/disks.sh +18 -0
  39. data/examples/disks.txt +5 -0
  40. data/examples/dose.txt +7 -0
  41. data/examples/drift.txt +4 -0
  42. data/examples/ethernet.txt +3 -0
  43. data/examples/figure.c +5 -0
  44. data/examples/fit.rb +16 -0
  45. data/examples/floors.rb +21 -0
  46. data/examples/gallery.sh +20 -0
  47. data/examples/gauges.sh +31 -0
  48. data/examples/gradient.rb +16 -0
  49. data/examples/growth.txt +6 -0
  50. data/examples/hosts.csv +4 -0
  51. data/examples/icmp.txt +3 -0
  52. data/examples/ipv4.txt +12 -0
  53. data/examples/kinds.csv +4 -0
  54. data/examples/langs.csv +4 -0
  55. data/examples/lint.rb +16 -0
  56. data/examples/load.rb +14 -0
  57. data/examples/login.txt +10 -0
  58. data/examples/neg.csv +4 -0
  59. data/examples/normal.txt +200 -0
  60. data/examples/note.txt +2 -0
  61. data/examples/notes-fixed.md +17 -0
  62. data/examples/notes.md +17 -0
  63. data/examples/ohlc.txt +7 -0
  64. data/examples/os.csv +10 -0
  65. data/examples/outline.txt +8 -0
  66. data/examples/plot.txt +5 -0
  67. data/examples/refusals.rb +17 -0
  68. data/examples/report.sh +21 -0
  69. data/examples/rfc.sh +22 -0
  70. data/examples/samples.txt +7 -0
  71. data/examples/shapes.pgm +0 -0
  72. data/examples/sizes.tsv +3 -0
  73. data/examples/spi.txt +6 -0
  74. data/examples/status.sh +23 -0
  75. data/examples/stem.txt +12 -0
  76. data/examples/table-drift.txt +4 -0
  77. data/examples/temps.txt +31 -0
  78. data/examples/trig.sh +13 -0
  79. data/examples/trig.txt +102 -0
  80. data/examples/week.txt +8 -0
  81. data/examples/wide.txt +2 -0
  82. data/exe/tessella +6 -0
  83. data/lib/tessella/axis.rb +71 -0
  84. data/lib/tessella/bitfield.rb +194 -0
  85. data/lib/tessella/block.rb +56 -0
  86. data/lib/tessella/borders.rb +85 -0
  87. data/lib/tessella/canvas/area.rb +155 -0
  88. data/lib/tessella/canvas/ascii.rb +120 -0
  89. data/lib/tessella/canvas/block.rb +40 -0
  90. data/lib/tessella/canvas/braille.rb +30 -0
  91. data/lib/tessella/canvas/clip.rb +49 -0
  92. data/lib/tessella/canvas/density.rb +51 -0
  93. data/lib/tessella/canvas/dot.rb +27 -0
  94. data/lib/tessella/canvas/eighths.rb +43 -0
  95. data/lib/tessella/canvas/half.rb +51 -0
  96. data/lib/tessella/canvas/sextant.rb +36 -0
  97. data/lib/tessella/canvas.rb +168 -0
  98. data/lib/tessella/cell.rb +10 -0
  99. data/lib/tessella/chart.rb +211 -0
  100. data/lib/tessella/check.rb +52 -0
  101. data/lib/tessella/cli/bitfield.rb +121 -0
  102. data/lib/tessella/cli/chart.rb +615 -0
  103. data/lib/tessella/cli/chart_candle.rb +36 -0
  104. data/lib/tessella/cli/chart_heatmap.rb +21 -0
  105. data/lib/tessella/cli/chart_help.rb +85 -0
  106. data/lib/tessella/cli/chart_pie.rb +38 -0
  107. data/lib/tessella/cli/compose.rb +156 -0
  108. data/lib/tessella/cli/compose_options.rb +259 -0
  109. data/lib/tessella/cli/glyphs.rb +97 -0
  110. data/lib/tessella/cli/image.rb +250 -0
  111. data/lib/tessella/cli/input.rb +44 -0
  112. data/lib/tessella/cli/iso_time.rb +28 -0
  113. data/lib/tessella/cli/lint.rb +102 -0
  114. data/lib/tessella/cli/lint_options.rb +99 -0
  115. data/lib/tessella/cli/main.rb +121 -0
  116. data/lib/tessella/cli/meter.rb +80 -0
  117. data/lib/tessella/cli/rule.rb +57 -0
  118. data/lib/tessella/cli/seq.rb +117 -0
  119. data/lib/tessella/cli/sparkline.rb +72 -0
  120. data/lib/tessella/cli/table.rb +167 -0
  121. data/lib/tessella/cli/text.rb +78 -0
  122. data/lib/tessella/cli/tree.rb +100 -0
  123. data/lib/tessella/cli/wave.rb +148 -0
  124. data/lib/tessella/cli.rb +36 -0
  125. data/lib/tessella/color.rb +106 -0
  126. data/lib/tessella/compose.rb +243 -0
  127. data/lib/tessella/compose_fill.rb +176 -0
  128. data/lib/tessella/compose_grid.rb +36 -0
  129. data/lib/tessella/compose_shadow.rb +41 -0
  130. data/lib/tessella/error.rb +13 -0
  131. data/lib/tessella/frame.rb +170 -0
  132. data/lib/tessella/glyphs.rb +106 -0
  133. data/lib/tessella/image.rb +454 -0
  134. data/lib/tessella/image_octant.rb +46 -0
  135. data/lib/tessella/image_petscii.rb +116 -0
  136. data/lib/tessella/kinds/bar.rb +344 -0
  137. data/lib/tessella/kinds/box.rb +213 -0
  138. data/lib/tessella/kinds/box_upright.rb +110 -0
  139. data/lib/tessella/kinds/candlestick.rb +164 -0
  140. data/lib/tessella/kinds/columns.rb +259 -0
  141. data/lib/tessella/kinds/date_axis.rb +102 -0
  142. data/lib/tessella/kinds/error_bars.rb +71 -0
  143. data/lib/tessella/kinds/heatmap.rb +130 -0
  144. data/lib/tessella/kinds/line.rb +173 -0
  145. data/lib/tessella/kinds/multibar.rb +98 -0
  146. data/lib/tessella/kinds/multibar_columns.rb +130 -0
  147. data/lib/tessella/kinds/multibar_fine.rb +45 -0
  148. data/lib/tessella/kinds/multibar_rows.rb +93 -0
  149. data/lib/tessella/kinds/pie.rb +123 -0
  150. data/lib/tessella/kinds/pie_rim.rb +78 -0
  151. data/lib/tessella/kinds/sideways.rb +62 -0
  152. data/lib/tessella/kinds/stem.rb +134 -0
  153. data/lib/tessella/kinds/threshold.rb +73 -0
  154. data/lib/tessella/legend.rb +31 -0
  155. data/lib/tessella/lint/alphabet.rb +56 -0
  156. data/lib/tessella/lint/drift.rb +82 -0
  157. data/lib/tessella/lint/fences.rb +51 -0
  158. data/lib/tessella/lint/geometry.rb +120 -0
  159. data/lib/tessella/lint/geometry_checks.rb +224 -0
  160. data/lib/tessella/lint/lines.rb +115 -0
  161. data/lib/tessella/lint/mixing.rb +61 -0
  162. data/lib/tessella/lint/runner.rb +73 -0
  163. data/lib/tessella/lint/shadows.rb +120 -0
  164. data/lib/tessella/lint/target_rows.rb +66 -0
  165. data/lib/tessella/lint.rb +158 -0
  166. data/lib/tessella/meter.rb +133 -0
  167. data/lib/tessella/meter_upright.rb +43 -0
  168. data/lib/tessella/png.rb +456 -0
  169. data/lib/tessella/raster.rb +92 -0
  170. data/lib/tessella/rmagick_reader.rb +52 -0
  171. data/lib/tessella/rule.rb +59 -0
  172. data/lib/tessella/seq.rb +522 -0
  173. data/lib/tessella/sparkline.rb +57 -0
  174. data/lib/tessella/stats.rb +199 -0
  175. data/lib/tessella/table.rb +547 -0
  176. data/lib/tessella/table_cells.rb +225 -0
  177. data/lib/tessella/target.rb +105 -0
  178. data/lib/tessella/text.rb +76 -0
  179. data/lib/tessella/text_forms.rb +150 -0
  180. data/lib/tessella/tree.rb +221 -0
  181. data/lib/tessella/version.rb +5 -0
  182. data/lib/tessella/wave.rb +353 -0
  183. data/lib/tessella/width.rb +122 -0
  184. data/lib/tessella/width_class.rb +77 -0
  185. data/lib/tessella/width_invisible.rb +66 -0
  186. data/lib/tessella/width_invisible_high.rb +60 -0
  187. data/lib/tessella.rb +94 -0
  188. metadata +226 -0
@@ -0,0 +1,121 @@
1
+ # Getting started
2
+
3
+ ## Install
4
+
5
+ tessella is not on rubygems.org. Install it from a checkout:
6
+
7
+ ```sh
8
+ cd tessella
9
+ make install # builds the gem, installs it for this user
10
+ ```
11
+
12
+ `make uninstall` removes it. Without installing, `exe/tessella` runs
13
+ straight out of the checkout, and `ruby -I lib` finds the library.
14
+ It needs Ruby 3.4 and nothing else.
15
+
16
+ ## A first chart
17
+
18
+ Any file of numbers will do. [examples/langs.csv](../examples/langs.csv)
19
+ holds a header and three rows:
20
+
21
+ ```text
22
+ $ cat langs.csv
23
+ lang,loc
24
+ ruby,12
25
+ c,30
26
+ rust,7.5
27
+ ```
28
+
29
+ ```text
30
+ $ tessella chart --kind bar --width 30 langs.csv
31
+ ┌ ┐
32
+ ruby ┤██████████▍··············· 12
33
+ c ┤██████████████████████████ 30
34
+ rust ┤██████▌··················· 7.5
35
+ └ ┘
36
+ ```
37
+
38
+ The same, for a document kept to ASCII:
39
+
40
+ ```text
41
+ $ tessella chart --kind bar --width 30 --ascii langs.csv
42
+ + +
43
+ ruby |##########................ 12
44
+ c |########################## 30
45
+ rust |#######................... 7.5
46
+ + +
47
+ ```
48
+
49
+ ## A first diagram
50
+
51
+ Each diagram reads a small text format from a file or stdin:
52
+
53
+ <!-- tessella: allow-mixed -->
54
+ ```text
55
+ $ printf 'Client -> Server: GET /\nServer --> Client: 200\n' | tessella seq
56
+ ┌─────────┐ ┌─────────┐
57
+ │ Client │ │ Server │
58
+ └────┬────┘ └────┬────┘
59
+ │ │
60
+ │ GET / │
61
+ │───────────►│
62
+ │ │
63
+ │ 200 │
64
+ │◄┄┄┄┄┄┄┄┄┄┄┄│
65
+ │ │
66
+ ```
67
+
68
+ ## From Ruby
69
+
70
+ ```ruby
71
+ require "tessella"
72
+
73
+ puts Tessella.bar(%w[ruby c rust], [12, 30, 7.5], width: 30)
74
+ ```
75
+
76
+ Every factory returns an object that prints as the figure, and every
77
+ figure composes with every other: frame it, set it beside another,
78
+ stack, indent or caption it.
79
+
80
+ ```ruby
81
+ # frozen_string_literal: true
82
+
83
+ # examples/load.rb: a bar chart and a table side by side, framed and
84
+ # captioned -- composition of any two figures.
85
+ require "tessella"
86
+
87
+ rows = Tessella::Table.parse(File.read("langs.csv"))
88
+ labels = rows.drop(1).map(&:first)
89
+ values = rows.drop(1).map { Integer(it[1], exception: false) || it[1].to_f }
90
+ plot = Tessella.bar(labels, values, width: 20)
91
+ table = Tessella.table(rows, style: :rounded)
92
+ fig = Tessella.beside(plot, table, gap: 2, align: :middle)
93
+ puts Tessella.caption(Tessella.frame(fig, title: "Load"),
94
+ text: "Figure 1: lines of code, in thousands")
95
+ ```
96
+
97
+ ```text
98
+ $ ruby load.rb
99
+ ┌───────────────────── Load ─────────────────────┐
100
+ │ ╭──────┬─────╮ │
101
+ │ ┌ ┐ │ lang │ loc │ │
102
+ │ ruby ┤██████▍········· 12 ├──────┼─────┤ │
103
+ │ c ┤████████████████ 30 │ ruby │ 12 │ │
104
+ │ rust ┤████············ 7.5 │ c │ 30 │ │
105
+ │ └ ┘ │ rust │ 7.5 │ │
106
+ │ ╰──────┴─────╯ │
107
+ └────────────────────────────────────────────────┘
108
+
109
+ Figure 1: lines of code, in thousands
110
+ ```
111
+
112
+ ## Where next
113
+
114
+ - [Charts](charts.md): the eight kinds, their input, options and
115
+ canvases.
116
+ - [Composition](composition.md): assembling figures.
117
+ - [Trees](tree.md), [tables](table.md), [bit fields](bitfield.md),
118
+ [timing diagrams](wave.md), [sequence diagrams](seq.md),
119
+ [pictures](images.md).
120
+ - [Linting](lint.md): checking figures before they ship.
121
+ - [The command](cli.md) and [the library](library.md), for reference.
data/doc/glyphs.md ADDED
@@ -0,0 +1,134 @@
1
+ # Glyph sets and width classes
2
+
3
+ A figure lines up only if every glyph in it takes the columns the
4
+ author counted. Most do, everywhere. Some do in a Western terminal
5
+ and take two columns in one set up for Chinese, Japanese or Korean:
6
+ the box-drawing lines and the shade blocks among them. tessella names
7
+ the glyph sets it draws with, and says for each which of these
8
+ classes its glyphs fall in.
9
+
10
+ ```ruby
11
+ Tessella::Width.class_of("▒") # => :A
12
+ Tessella::Glyphs.fetch(:ramp, :blocks) # => a GlyphSet
13
+ Tessella::Glyphs.fetch(:ramp, :blocks).mixed? # => true
14
+ ```
15
+
16
+ ```text
17
+ tessella glyphs [KIND [NAME]]
18
+ tessella glyphs --check GLYPHS
19
+ ```
20
+
21
+ ## Width classes
22
+
23
+ Every character has one class, from Unicode 15.1's East Asian Width
24
+ property:
25
+
26
+ | Class | Name | Columns |
27
+ |-------|-----------|--------------------------------------------------|
28
+ | `N` | narrow | one, everywhere |
29
+ | `A` | ambiguous | one in Western terminals, two in CJK-configured ones |
30
+ | `W` | wide | two, everywhere (CJK ideographs, most emoji) |
31
+
32
+ `Tessella::Width.class_of(char)` returns `:N`, `:A` or `:W`. A set
33
+ whose glyphs all share one class keeps its shape in any terminal; a
34
+ set that spans two is *mixed*: right in a Western terminal, but its
35
+ glyphs change width against each other in a CJK one, so a figure
36
+ drawn with it stops lining up there.
37
+
38
+ ## The registry
39
+
40
+ `tessella glyphs` lists every named set the library draws with: its
41
+ kind, its name, its classes, the [width targets](targets.md) it is
42
+ safe for, and its glyphs. Every set suits `term`; a set suits `cjk`
43
+ when it holds no ambiguous glyph, and `web` when it holds no wide or
44
+ emoji-capable one. A kind narrows the list to one kind:
45
+
46
+ <!-- tessella: allow-mixed -->
47
+ ```text
48
+ $ tessella glyphs border
49
+ border light A term web ┌┐└┘─│
50
+ border heavy A term web ┏┓┗┛━┃
51
+ border double A term web ╔╗╚╝═║
52
+ border rounded A term web ╭╮╰╯─│
53
+ border dashed A term web ┌┐└┘┄┆
54
+ border dashed_fine A term web ┌┐└┘┈┊
55
+ border heavy_dashed A term web ┏┓┗┛┅┇
56
+ border heavy_dashed_fine A term web ┏┓┗┛┉┋
57
+ border double_h A term web ╒╕╘╛═│
58
+ border double_v A term web ╓╖╙╜─║
59
+ border ascii N term cjk web ++++-|
60
+ ```
61
+
62
+ A kind and a name list one set glyph by glyph, with code points:
63
+
64
+ ```text
65
+ $ tessella glyphs ramp blocks
66
+ ░ U+2591 N
67
+ ▒ U+2592 A
68
+ ▓ U+2593 A
69
+ █ U+2588 A
70
+ ```
71
+
72
+ The `blocks` ramp is mixed: `░` is narrow, the three others are
73
+ ambiguous. In a CJK terminal its lightest tone is half as wide as the
74
+ others.
75
+
76
+ From Ruby, `Tessella::Glyphs.fetch(kind, name)` returns a `GlyphSet`
77
+ (`kind`, `name`, `glyphs`, `note`, `classes`, `mixed?`, `targets`),
78
+ `Tessella::Glyphs.targets(glyphs)` the targets any glyphs suit, and
79
+ `Tessella::Glyphs.all` every set. An unknown kind or name raises
80
+ `ArgumentError`, listing the names there are. A name may say `-` for
81
+ `_`.
82
+
83
+ `--target T` lists only the sets safe for a target, here the meter
84
+ styles a CJK terminal draws as counted:
85
+
86
+ ```text
87
+ $ tessella glyphs --target cjk meter
88
+ meter arrow N term cjk web ▸▹
89
+ meter chevron N term cjk web ›
90
+ meter classic N term cjk web =
91
+ meter rectangle N term cjk web ▮▯
92
+ meter wave N term cjk web ~_
93
+ meter blade N term cjk web ▰▱ (patchy font coverage)
94
+ meter button N term cjk web ⦿⦾ (patchy font coverage)
95
+ ```
96
+
97
+ ### Arrow heads
98
+
99
+ The registry also holds the arrow heads, as kind `head`, with the rule
100
+ for choosing one in their targets and notes:
101
+
102
+ ```text
103
+ $ tessella glyphs head
104
+ head full N+A term web ►◄▲▼ mixed (▴▾ under cjk)
105
+ head small N term cjk web ▸◂▴▾
106
+ head emoji A term ▶◀ (+U+FE0E on web)
107
+ ```
108
+
109
+ `full` (`►◄▲▼`) is mixed: `► ◄` are narrow, `▲ ▼` ambiguous, so in a
110
+ CJK terminal use `▴ ▾` for up and down; `tessella lint --target cjk`
111
+ says so when it finds `▲` or `▼` on a row that must line up. `small`
112
+ (`▸◂▴▾`) suits every target. `emoji` (`▶◀`) suits the terminal only,
113
+ and needs U+FE0E after it on the web ([width targets](targets.md#web)).
114
+
115
+ ## Checking a set of one's own
116
+
117
+ `--check GLYPHS` classifies any glyphs: it prints their class and the
118
+ targets they are safe for, and exits 1 when they are mixed, so a
119
+ build can refuse a glyph set before a figure uses it:
120
+
121
+ ```text
122
+ $ tessella glyphs --check '┌┐└┘─│'
123
+ A
124
+ safe for: term web
125
+ ```
126
+
127
+ ```text
128
+ $ tessella glyphs --check '░▒▓█'
129
+ mixed: N A
130
+ safe for: term web
131
+ ```
132
+
133
+ The [lint](lint.md) measures widths with the same tables, so what
134
+ `glyphs` calls narrow, the lint counts as one column.
data/doc/images.md ADDED
@@ -0,0 +1,325 @@
1
+ # Pictures
2
+
3
+ `image` draws a picture as text: a logo for a banner, a plot or a
4
+ screenshot for a terminal, a photograph as a curiosity.
5
+
6
+ ```ruby
7
+ Tessella.image(source, style: :ramp, width: 78, ramp: nil, gamma: 1.0,
8
+ threshold: 0.5, dither: :fs, invert: false,
9
+ edge_threshold: 0.3, ascii: false) # => Block
10
+ ```
11
+
12
+ ```text
13
+ tessella image [--style S] [--width N] [--dither M] [--ramp STR|NAME]
14
+ [--gamma F] [--threshold F] [--edge-threshold F]
15
+ [--invert] [--ascii] [--max-width N] [file]
16
+ ```
17
+
18
+ ## Sources
19
+
20
+ `source` is a `Tessella::Raster`, an IO, or a String of image bytes.
21
+ The format is told by the bytes' own signature, not a file name:
22
+
23
+ - **PGM and PPM** (P2, P3, P5, P6) and **PNG** decode with nothing
24
+ installed, in plain Ruby.
25
+ - **Anything else** (JPEG, GIF, TIFF, WebP, ...) needs the `rmagick`
26
+ gem. tessella uses it when it is installed and never requires it;
27
+ without it the image is refused, naming the way round: convert to
28
+ PGM first.
29
+
30
+ ```text
31
+ $ printf 'GIF89a' | tessella image 2>&1 | fold -s -w 72 | sed 's/ $//'
32
+ tessella image: not a PNM or PNG image, and the rmagick gem is not
33
+ installed: install it, or convert the image first (magick IMAGE pgm:- |
34
+ tessella image)
35
+ ```
36
+
37
+ ```sh
38
+ magick photo.jpg pgm:- | tessella image --width 60
39
+ ```
40
+
41
+ Colour becomes grey by Rec. 601 luma, and transparency is laid over
42
+ black, so a transparent pixel is empty.
43
+
44
+ A `Raster` can also be built in Ruby, from any function: width,
45
+ height, and `width * height` grey values in 0..1, row by row from the
46
+ top, 1.0 the brightest.
47
+
48
+ ```ruby
49
+ # frozen_string_literal: true
50
+
51
+ # examples/gradient.rb: a Raster built in Ruby -- no image file --
52
+ # drawn in three styles, side by side.
53
+ require "tessella"
54
+
55
+ w = h = 48
56
+ luma = (0...h).flat_map do |y|
57
+ (0...w).map { |x| Math.hypot(x - 24, y - 24) < 22 ? x / 47.0 : 0 }
58
+ end
59
+ raster = Tessella::Raster.new(width: w, height: h, luma: luma)
60
+ figs = %i[ramp blocks quadrant].map do |style|
61
+ Tessella.caption(Tessella.image(raster, style: style, width: 16),
62
+ text: style.to_s)
63
+ end
64
+ puts Tessella.beside(*figs, gap: 3)
65
+ ```
66
+
67
+ ```text
68
+ $ ruby gradient.rb
69
+ .:::-:. ░░░░░░ ▖▖▖▖▖▖
70
+ .:-==++*#*- ░░░▒▒▒▒▓▓▓░ ▝▖▚▐▐▞▛▟▙▄
71
+ .::-==++*##%* ░░░░▒▒▒▒▓▓▓▓▓ ▘▚▝▞▞▞▞▛▟▟▜▙
72
+ ..::-==++*##%%- ░░░░▒▒▒▒▓▓▓▓█░ ▘▖▚▝▞▞▞▛▟▜▛█▌
73
+ ..::-==++*##%%- ░░░░▒▒▒▒▓▓▓▓█░ ▘▘▝▖▚▚▚▜▞▛▙██▌
74
+ ..::-==++*##%# ░░░░▒▒▒▒▓▓▓▓▓ ▗▝▖▞▐▗▚▚▜▜▙▛▛
75
+ .::-==++*##+ ░░░▒▒▒▒▓▓▓▒ ▗▗▝▖▌▙▜▚▙▙▀
76
+ .::--=-- ░░░░▒░░ ▘▘▘▝▝▘▘
77
+
78
+ ramp blocks quadrant
79
+ ```
80
+
81
+ ## Styles
82
+
83
+ The style trades tone for resolution. `ramp` and `blocks` give one
84
+ tone per character cell; the others pack several on/off pixels into a
85
+ cell, using dithering for tone; `edges` draws outlines.
86
+
87
+ | Style | Pixels per cell | Glyphs |
88
+ |------------|-----------------|---------------------------------|
89
+ | `ramp` | 1 | `` .:-=+*#%@`` (or `--ramp`) |
90
+ | `blocks` | 1 | ` ░▒▓█` |
91
+ | `half` | 1 × 2 | `▀ ▄ █` |
92
+ | `quadrant` | 2 × 2 | `▘▝▀▖▌▞▛▗▚▐▜▄▙▟█` |
93
+ | `sextant` | 2 × 3 | Unicode 13 sextants `🬀`…`🬻` |
94
+ | `braille` | 2 × 4 | braille patterns |
95
+ | `octant` | 2 × 4 | Unicode 16 octants (see below) |
96
+ | `petscii` | 4 × 8 tones | about forty shapes (see below) |
97
+ | `edges` | 1 | `─ │ ╱ ╲`, Sobel contours |
98
+
99
+ `disc.pgm` is a 64 × 64 grey disc, brightest at its centre:
100
+
101
+ ```text
102
+ $ tessella image --width 24 disc.pgm
103
+
104
+ ...::...
105
+ .::--====---:.
106
+ :--=+++**+++==-:.
107
+ :-==+**####**++=-:.
108
+ .:-=+**#%%%%##*+==:.
109
+ .:-=+*##%%%%##*+==:.
110
+ .:-=++**#####*++=-:.
111
+ .:-==+++***++==-:.
112
+ .:--======--:.
113
+ ..::::::..
114
+ ```
115
+
116
+ ```text
117
+ $ tessella image --style blocks --width 24 disc.pgm
118
+
119
+ ░░░░░
120
+ ░░░▒▒▒▒▒▒░░░
121
+ ░░▒▒▒▒▒▒▒▒▒▒▒▒░░
122
+ ░░▒▒▒▓▓▓▓▓▓▓▓▒▒▒░░
123
+ ░░▒▒▓▓▓▓██▓▓▓▓▒▒▒░░
124
+ ░░▒▒▓▓▓▓███▓▓▓▒▒▒░░
125
+ ░░▒▒▒▓▓▓▓▓▓▓▓▒▒▒░░
126
+ ░░▒▒▒▒▒▓▓▒▒▒▒▒░░
127
+ ░░░▒▒▒▒▒▒░░░░
128
+ ░░░░░░░░
129
+ ```
130
+
131
+ The braille renders on this page line up only in a terminal: on
132
+ GitHub braille is drawn about 1.2 columns wide
133
+ ([width targets](targets.md#web)).
134
+
135
+ ```text
136
+ $ tessella image --style braille --width 24 disc.pgm
137
+
138
+ ⢀ ⢄⠠⠠⠠⢀⢀
139
+ ⠄⢌⢢⢡⢱⢸⢘⢜⠔⡔⡨⠠⡀
140
+ ⠠⠡⡣⡱⡱⡱⣕⢵⣣⡳⣕⢕⡜⡌⡢⢁
141
+ ⠠⡑⢕⢱⡱⣝⢮⣗⣯⢾⣺⢮⡳⣕⢕⢜⠄⢅
142
+ ⢅⠪⡪⣪⢺⣺⢽⣾⣻⣿⣽⢯⣞⢮⡣⡣⡃⢅⠐
143
+ ⠢⡑⢕⢕⣝⢾⢽⡾⣿⢿⣽⣻⣺⢵⡹⡸⡨⢂⠂
144
+ ⡁⠪⡸⡸⡸⣕⢯⢯⣻⢽⣳⢽⡪⡇⡇⡇⡪⠐
145
+ ⠡⢂⢕⢱⠱⡍⣗⢵⢫⢎⢧⢣⢣⠣⢊⠐⠈
146
+ ⠂⠐⠌⡊⡪⠢⠣⡣⢃⠇⢕⠐⠅⠂
147
+ ⠈ ⠂⠂⠡⠑⠨⠠⠑ ⠁
148
+ ```
149
+
150
+ ```text
151
+ $ tessella image --style edges --width 24 disc.pgm
152
+
153
+ ────────
154
+ ╱────────────╲
155
+ ╱╱╱╱────────╲╲╲╲
156
+ ╲
157
+
158
+
159
+ ╱
160
+ ╲╲╲╲────────╱╱╱╱
161
+ ╲────────────╱
162
+ ──────────
163
+ ```
164
+
165
+ [examples/gallery.sh](../examples/gallery.sh) draws one picture in
166
+ four styles side by side ([output](cli.md#several-figures-one-result)).
167
+
168
+ The sextant glyphs are recent (Unicode 13, 2020): check that the
169
+ reader's font has them before using `sextant`.
170
+
171
+ `octant` packs 2 × 4 pixels into a cell, as braille does, but as
172
+ solid blocks rather than dots, so a picture keeps its tone where
173
+ braille turns grey. Its glyphs are Unicode 16 (2024), and only the
174
+ newest terminals draw them (Ghostty and its like); elsewhere each
175
+ cell shows as a box or a blank. That is why this page shows no
176
+ octant render: try `tessella image --style octant disc.pgm` in your
177
+ own terminal.
178
+
179
+ `petscii` draws as the Commodore character set did: each cell's 4 × 8
180
+ tones are matched to the glyph of least error among about forty
181
+ shapes (halves, quadrants, `▏▕▔▁`, three-quarter blocks `▊ ▆` and
182
+ their kin `▂ 🮊 🮂 🮅`, the triangles `◢◣◤◥`, `╱╲╳`, the shades `░▒▓`,
183
+ lines, corners and tees, `● ○`). `🮊 🮂 🮅` are from Unicode 13's
184
+ Legacy Computing block, so the font caveat on sextants holds here
185
+ too. A soft picture comes out in shades:
186
+
187
+ ```text
188
+ $ tessella image --style petscii --width 24 disc.pgm
189
+
190
+ ░░░░░░
191
+ ░░░▒▒▒▒▒▒░░░
192
+ ░░▒▒▒▒▒▒▒▒▒▒▒▒░░
193
+ ░░▒▒▒▓▓▓▓▓▓▓▓▒▒▒░░
194
+ ░░▒▒▓▓▓▓██▓▓▓▓▒▒▒░░
195
+ ░░▒▒▓▓▓▓██▓▓▓▓▒▒▒░░
196
+ ░░▒▒▒▓▓▓▓▓▓▓▓▒▒▒░░
197
+ ░░▒▒▒▒▒▒▒▒▒▒▒▒░░
198
+ ░░░▒▒▒▒▒▒░░░░
199
+ ░░░░░░
200
+ ```
201
+
202
+ Hard edges are where it shows its shapes. `shapes.pgm` is a ring and
203
+ a wedge in black and white:
204
+
205
+ ```text
206
+ $ tessella image --style petscii --width 24 shapes.pgm
207
+ ▁▁▁▁ 🮊███████◤
208
+ ▗▄██████▆▖ 🮊██████▘
209
+ ◢███▀▀🮂▀▜██▙ 🮊█████▘
210
+ ▕██▊ ▐██▌🮊████▘
211
+ ▕██▊ ▐██▌🮊███▘
212
+ ▜██▙▄▂▂▂▟██▛ 🮊█▛
213
+ ▝🮅██████🮅▀ 🮊▛
214
+ 🮂🮂🮂🮂 ╱
215
+ ```
216
+
217
+ `petscii` takes no dithering, so `--threshold` and `--dither` do not
218
+ apply to it.
219
+
220
+ ## Tone
221
+
222
+ - `--width N`: the picture's width in cells, 78 by default; the
223
+ height follows from the image's aspect, a cell being about twice as
224
+ tall as wide.
225
+ - `--gamma F`: every tone t in 0..1 is drawn as t to the power F;
226
+ above 1 pushes the mid-tones toward empty, below 1 toward full.
227
+ - `--invert`: tones are drawn as 1 − t. By default a bright pixel
228
+ gets a dense glyph, which suits light text on a dark terminal;
229
+ invert for dark text on a light page.
230
+ - `--ramp STR`: the tone glyphs for `ramp`, from empty to full, or
231
+ the name of a ramp in the [registry](glyphs.md#the-registry):
232
+ `ramp` (the default), `blocks`, `long` (70 ASCII glyphs, for finer
233
+ tone) or `hatch`. A name wins over the same letters taken as
234
+ glyphs.
235
+ - `--threshold F`: the tone at which a pixel is on, in the pixel
236
+ styles (0.5 by default).
237
+ - `--dither M`: how the pixel styles render tone: `fs`
238
+ (Floyd-Steinberg, the default), `atkinson`, `bayer` (ordered), or
239
+ `none` (a plain threshold).
240
+ - `--edge-threshold F`: the gradient strength an `edges` outline
241
+ needs (0.3 by default).
242
+
243
+ ```text
244
+ $ tessella image --style braille --dither none --width 24 disc.pgm
245
+
246
+
247
+
248
+ ⢀⣤⣾⣿⣿⣿⣿⣷⣦⡀
249
+ ⣠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦
250
+ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡆
251
+ ⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠇
252
+ ⠹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟
253
+ ⠈⠻⣿⣿⣿⣿⣿⣿⠿⠋
254
+ ⠈⠉⠉⠁
255
+ ```
256
+
257
+ ```text
258
+ $ tessella image --ramp ' .oO@' --width 20 disc.pgm
259
+
260
+ .......
261
+ ..oooooooo..
262
+ ..oooOOOOOooo.
263
+ ..ooOOO@@OOOoo..
264
+ ..ooOOO@@OOOoo..
265
+ .oooOOOOOOooo..
266
+ ..oooooooo..
267
+ ........
268
+ ```
269
+
270
+ ```text
271
+ $ tessella image --ramp long --width 30 disc.pgm
272
+
273
+ .`^",:,"`'
274
+ ',!<_?][[]?-~i;`
275
+ ':<]{(/fjrrjft\)}_l^
276
+ ^i-1/rucYUJJJYXvxf(]~I.
277
+ ^i?1tnzJQOmqqwZ0CYvj([~;.
278
+ .I+}\xzJ0wdhooakpZLYut)?>^
279
+ '!-1/nYQmdh#8%WokqOJcj|[~:
280
+ .l_{\xXCOqba##*hdmQUvf(]<"
281
+ ">])fuXC0mqddpwZQUcr|}_l.
282
+ :<](fxcXJCLQLCUzur\}_!`
283
+ ^l_}(/frxnnnxjt|1?>:.
284
+ ^l<_]}{11{}[-+i,.
285
+ .`,;Il!l;:^'
286
+ ```
287
+
288
+ ```text
289
+ $ tessella image --ramp hatch --width 20 disc.pgm
290
+
291
+ ·······
292
+ ··∘∘∘∘∘∘∘∘··
293
+ ··∘∘∘╱╱╱╱╱∘∘∘·
294
+ ··∘∘╱╱╱╳╳╱╱╱∘∘··
295
+ ··∘∘╱╱╱╳╳╱╱╱∘∘··
296
+ ·∘∘∘╱╱╱╱╱╱∘∘∘··
297
+ ··∘∘∘∘∘∘∘∘··
298
+ ········
299
+ ```
300
+
301
+ ## ASCII
302
+
303
+ `--ascii` allows only the `ramp` style, with an ASCII ramp:
304
+
305
+ ```text
306
+ $ tessella image --style blocks --ascii disc.pgm
307
+ tessella image: ascii: true allows only style: :ramp, not :blocks
308
+ ```
309
+
310
+ ## Errors
311
+
312
+ A picture that would render blank — `edges` on a flat image, a
313
+ threshold above every tone — is refused, naming what to change,
314
+ rather than printed as nothing (exit 1):
315
+
316
+ ```text
317
+ $ tessella image --style braille --threshold 1 --dither none disc.pgm 2>&1 | \
318
+ fold -s -w 72 | sed 's/ $//'
319
+ tessella image: the picture renders blank at style braille: lower
320
+ threshold, or try invert or another style
321
+ ```
322
+
323
+ A malformed or truncated image is bad input (exit 1); an unknown
324
+ style or dither, or a style that `--ascii` does not allow, is a usage
325
+ error (exit 2).