fontico 0.2.0 → 1.2.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 +4 -4
- data/README.md +128 -31
- data/lib/fontico/builder.rb +61 -9
- data/lib/fontico/emitters/gfx_font.rb +190 -0
- data/lib/fontico/emitters/header.rb +165 -0
- data/lib/fontico/emitters/stylesheet.rb +5 -0
- data/lib/fontico/helper.rb +45 -23
- data/lib/fontico/lockfile.rb +36 -3
- data/lib/fontico/manifest.rb +53 -3
- data/lib/fontico/node/rasterize.mjs +192 -0
- data/lib/fontico/version.rb +1 -1
- data/lib/fontico.rb +26 -1
- data/lib/tasks/fontico.rake +4 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1ee86cae31c0f7f4acb9b303bfd2c06638ea383d86e27ff603452537ec58cfed
|
|
4
|
+
data.tar.gz: cf9b3485369dd8601822dad9b1e367bccb41c440620071412b595b755afa97c9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e9e776c9fb99a18085b38a4069f23b72c808ba48c45b321d7948a166c99dd0bf9e93f413dd13a729a5a085148ffdf8e4a1feef544c4babec168e37d037e3b316
|
|
7
|
+
data.tar.gz: c5e3727514c2ebaff8c1b879ca9c2468bd2b14df0bef16797f983580b5830ac52b75fd93b9b3f639d433b7e11bac28aedc4c6cacf1d448d66bb2a715cdeccbc5
|
data/README.md
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
|
|
3
1
|
```
|
|
4
2
|
╔═╗╔═╗╔╗╔╔╦╗╦╔═╗╔═╗
|
|
5
3
|
╠╣ ║ ║║║║ ║ ║║ ║ ║
|
|
@@ -12,8 +10,6 @@
|
|
|
12
10
|
[](https://github.com/fireho/fontico/actions/workflows/ci.yml)
|
|
13
11
|
[](LICENSE.txt)
|
|
14
12
|
|
|
15
|
-
</div>
|
|
16
|
-
|
|
17
13
|
---
|
|
18
14
|
|
|
19
15
|
```erb
|
|
@@ -25,13 +21,15 @@
|
|
|
25
21
|
Three providers, one call. Templates never name a vendor, so re-skinning the
|
|
26
22
|
app — or surviving an upstream rename — is a diff in one file.
|
|
27
23
|
|
|
28
|
-
|
|
|
29
|
-
|
|
|
30
|
-
| **353,000 icons**
|
|
31
|
-
| **One request**
|
|
32
|
-
| **361ms cold, 2ms warm** | measured on 35 icons across two remote providers and five local files
|
|
33
|
-
| **No Node, no npm**
|
|
34
|
-
| **Also PDFs**
|
|
24
|
+
| | |
|
|
25
|
+
| ------------------------ | ----------------------------------------------------------------------------------- |
|
|
26
|
+
| **353,000 icons** | any of[Iconify's 238 sets](https://icon-sets.iconify.design/), plus your own folder |
|
|
27
|
+
| **One request** | a single sprite, 2.3KB brotli, not render-blocking |
|
|
28
|
+
| **361ms cold, 2ms warm** | measured on 35 icons across two remote providers and five local files |
|
|
29
|
+
| **No Node, no npm** | the sprite target is pure Ruby; deploys rebuild offline from the lockfile |
|
|
30
|
+
| **Also PDFs** | a real TTF for Prawn, with pinned codepoints |
|
|
31
|
+
| **Also firmware** | `icons.h` for C/C++, same names, 7KB of font instead of 357KB |
|
|
32
|
+
| **Also bare metal** | a `GFXfont` compiled in — 11KB for 221 icons, no renderer, no filesystem |
|
|
35
33
|
|
|
36
34
|
## Install
|
|
37
35
|
|
|
@@ -134,13 +132,13 @@ and no Node. Commit it.
|
|
|
134
132
|
First-party exports are not uniform the way vendor icons are, so everything
|
|
135
133
|
entering `app/assets/icons/` is normalised first:
|
|
136
134
|
|
|
137
|
-
|
|
|
138
|
-
|
|
|
139
|
-
| Editor chrome | `sodipodi:`, `inkscape:`, `<metadata>`, RDF, empty `<defs>` stripped
|
|
140
|
-
| Ids
|
|
141
|
-
| viewBox
|
|
142
|
-
| Colour
|
|
143
|
-
| Safety
|
|
135
|
+
| | |
|
|
136
|
+
| ------------- | ------------------------------------------------------------------------------ |
|
|
137
|
+
| Editor chrome | `sodipodi:`, `inkscape:`, `<metadata>`, RDF, empty `<defs>` stripped |
|
|
138
|
+
| Ids | rewritten to`slug__id`, with `url(#…)`, `href`, `clip-path`, `mask` following |
|
|
139
|
+
| viewBox | any source box refitted into the target, centred, aspect preserved |
|
|
140
|
+
| Colour | folded to`currentColor`, unless the icon is detected as multicolour |
|
|
141
|
+
| Safety | `<script>`, `on*` handlers, `<foreignObject>`, external refs removed |
|
|
144
142
|
|
|
145
143
|
The id rewriting is not optional hygiene. Across 40 SVGs sampled from a real
|
|
146
144
|
machine, **34 shared `id="layer1"`** and 11 shared `id="path1"` — merging any
|
|
@@ -223,11 +221,11 @@ codepoint, so nothing gets hardcoded.
|
|
|
223
221
|
A font glyph is filled contours; it has no strokes and no colour. Each icon
|
|
224
222
|
takes one of three routes:
|
|
225
223
|
|
|
226
|
-
|
|
|
227
|
-
|
|
|
228
|
-
| **filled**
|
|
224
|
+
| | |
|
|
225
|
+
| ------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
226
|
+
| **filled** | used as-is — Material Symbols, most Iconify sets, flat first-party exports |
|
|
229
227
|
| **extracted** | stroke-based, but the provider ships a font whose glyphs are already expanded — lifted from there, losslessly (Lucide) |
|
|
230
|
-
| **refused**
|
|
228
|
+
| **refused** | stroke-based with no provider font — the build stops and names the icon |
|
|
231
229
|
|
|
232
230
|
There is deliberately no raster-trace fallback. Both published JS expanders
|
|
233
231
|
(`svg-outline-stroke`, `oslllo-svg-fixer`) run artwork through potrace and hand
|
|
@@ -243,22 +241,121 @@ build names each one it dropped.
|
|
|
243
241
|
Font targets need Node, installed on demand into `~/.cache/fontico`. A manifest
|
|
244
242
|
with `targets: [sprite]` never touches it and stays pure Ruby.
|
|
245
243
|
|
|
244
|
+
## Firmware, and C
|
|
245
|
+
|
|
246
|
+
The `c` target emits `icons.h`, so a device names icons by intent like every
|
|
247
|
+
other consumer. It draws them out of the same TTF, which is why `c` implies
|
|
248
|
+
`ttf` the way `sprite` implies `css`.
|
|
249
|
+
|
|
250
|
+
`targets:` takes a mapping when an artifact belongs somewhere specific — a
|
|
251
|
+
firmware header goes in the device's `include/`, not in `app/assets/builds`
|
|
252
|
+
next to the web files:
|
|
253
|
+
|
|
254
|
+
```yaml
|
|
255
|
+
targets:
|
|
256
|
+
sprite: # default location
|
|
257
|
+
ttf: devs/esplay/data/icons.ttf
|
|
258
|
+
c: devs/esplay/include/icons.h
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
```c
|
|
262
|
+
#include "icons.h"
|
|
263
|
+
|
|
264
|
+
display.drawIcon(ICON_WIFI, 86, 5, COLOR_WHITE, 20); /* "\xEE\x80\x81" */
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
The UTF-8 bytes are literals decided at build time, so nothing parses
|
|
268
|
+
`"U+e63e"` or builds a `String` on the heap inside a draw loop. For an icon
|
|
269
|
+
named at runtime rather than compiled in — arriving in an MQTT payload, say —
|
|
270
|
+
the header also carries a name-sorted table:
|
|
271
|
+
|
|
272
|
+
```c
|
|
273
|
+
#define FONTICO_ICONS_IMPLEMENTATION /* in exactly one translation unit */
|
|
274
|
+
#include "icons.h"
|
|
275
|
+
|
|
276
|
+
const char *glyph = fontico_icon(payload["icon"]); /* NULL if unknown */
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Two reasons this is a better deal than a vendor icon font on the device:
|
|
280
|
+
|
|
281
|
+
- **Size.** The font carries only the icons your manifest names. 35 icons
|
|
282
|
+
build a 7KB TTF, against the 357KB an unsubsetted Material Symbols drop-in
|
|
283
|
+
costs — and flash is the budget that actually binds.
|
|
284
|
+
- **Codepoints that cannot move.** They are pinned append-only in
|
|
285
|
+
`icons.lock`, which matters more here than anywhere else: a codepoint
|
|
286
|
+
compiled into a flashed image cannot be hotfixed. If adding an icon
|
|
287
|
+
renumbered the others, every board in the field would draw the wrong
|
|
288
|
+
picture after an OTA.
|
|
289
|
+
|
|
290
|
+
Names become macros — `nav.menu` is `ICON_NAV_MENU`. Two names that fold to
|
|
291
|
+
the same macro are refused rather than silently redefined, and non-ASCII
|
|
292
|
+
names are refused too: the table is sorted here and `bsearch`ed there, and a
|
|
293
|
+
byte above `0x7F` orders the other way under a signed `char`.
|
|
294
|
+
|
|
295
|
+
Multicolour icons are left out, the same as they are left out of the font —
|
|
296
|
+
there would be no glyph behind the codepoint. The build names each one.
|
|
297
|
+
|
|
298
|
+
### `gfxfont`: no font renderer on the device at all
|
|
299
|
+
|
|
300
|
+
The `c` target still needs something on the device that can read a TTF. The
|
|
301
|
+
`gfxfont` target removes that: it emits an Adafruit `GFXfont` — 1-bit bitmaps
|
|
302
|
+
and a glyph table, compiled straight into the binary. No FreeType, no
|
|
303
|
+
OpenFontRender, no filesystem partition to flash. `Adafruit_GFX` and
|
|
304
|
+
`Arduino_GFX` both take it as-is:
|
|
305
|
+
|
|
306
|
+
```yaml
|
|
307
|
+
targets:
|
|
308
|
+
sprite:
|
|
309
|
+
gfxfont:
|
|
310
|
+
path: devs/include/icons_font.h
|
|
311
|
+
size: 24
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
```c
|
|
315
|
+
#include <Arduino_GFX_Library.h>
|
|
316
|
+
#include "icons_font.h"
|
|
317
|
+
|
|
318
|
+
gfx->setFont(&fontico_icons24);
|
|
319
|
+
gfx->drawChar(x, y, ICON_WIFI, fg, bg, 1);
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
221 icons at 24px come to **9,961 bytes of bitmap** plus a 1,547-byte glyph
|
|
323
|
+
table — about 11KB for the set, against 357KB for an unsubsetted vendor font
|
|
324
|
+
on a filesystem. Bitmaps are rasterised out of the TTF fontico already
|
|
325
|
+
builds, not from the SVGs, so they inherit the same normalised geometry the
|
|
326
|
+
PDF uses instead of drifting from it through a second pipeline.
|
|
327
|
+
|
|
328
|
+
Two things follow from the format. A `GFXfont` is **fixed size**, so ask for
|
|
329
|
+
the size you will draw at rather than scaling one up. And it is indexed off a
|
|
330
|
+
byte, so it holds 224 icons; past that the build says so and points you at the
|
|
331
|
+
`c` target, which addresses codepoints directly. The char codes are positional
|
|
332
|
+
for that reason, which is why they and the bitmaps are emitted into one file —
|
|
333
|
+
they cannot disagree. Their order follows the append-only codepoints in
|
|
334
|
+
`icons.lock`, so adding an icon appends instead of renumbering what is already
|
|
335
|
+
flashed.
|
|
336
|
+
|
|
337
|
+
At one bit deep, thin strokes fall through the threshold at small sizes. An
|
|
338
|
+
icon that rasterises to nothing is refused rather than shipped as an icon that
|
|
339
|
+
draws as nothing.
|
|
340
|
+
|
|
246
341
|
## How it compares
|
|
247
342
|
|
|
248
|
-
|
|
|
249
|
-
|
|
|
250
|
-
| Icon sets
|
|
251
|
-
| Where the SVG comes from
|
|
252
|
-
| Requests
|
|
253
|
-
| JS required
|
|
254
|
-
| Rename-proof templates
|
|
255
|
-
| Offline / reproducible build | yes
|
|
256
|
-
| PDF
|
|
343
|
+
| | vendor gems (`lucide-rails`, `heroicon`) | `iconify-icon` web component | fontico |
|
|
344
|
+
| ---------------------------- | ---------------------------------------- | ------------------------------ | --------------------- |
|
|
345
|
+
| Icon sets | one, per gem | 238 | 238 + your own |
|
|
346
|
+
| Where the SVG comes from | inlined per render | fetched at runtime, per client | one build-time sprite |
|
|
347
|
+
| Requests | 0, but repeated in every response body | 1+ per icon, per visitor | 1, cached |
|
|
348
|
+
| JS required | no | yes | no |
|
|
349
|
+
| Rename-proof templates | no —`lucide_icon "pencil"` | no —`icon="lucide:pencil"` | yes —`icon "edit"` |
|
|
350
|
+
| Offline / reproducible build | yes | no | yes, via`icons.lock` |
|
|
351
|
+
| PDF | no | no | yes, TTF for Prawn |
|
|
257
352
|
|
|
258
353
|
## Status
|
|
259
354
|
|
|
260
355
|
- ✅ Manifest, resolver, preprocessor, lockfile, sprite emitter, Rails helper
|
|
261
356
|
- ✅ TTF emitter with glyph extraction, codepoint API, Prawn helpers
|
|
357
|
+
- ✅ `c` target: `icons.h` for firmware, with per-target output paths
|
|
358
|
+
- ✅ `gfxfont` target: 1-bit bitmaps compiled in, for Adafruit_GFX / Arduino_GFX
|
|
262
359
|
- ⏳ `woff2` — declared targets are skipped with a notice
|
|
263
360
|
- ⏳ `variant:` — accepted and ignored by the helper; see [TODO.md](TODO.md)
|
|
264
361
|
|
data/lib/fontico/builder.rb
CHANGED
|
@@ -31,7 +31,15 @@ module Fontico
|
|
|
31
31
|
# are already on disk: treating them as fresh meant editing logo.svg
|
|
32
32
|
# rebuilt nothing. Force is `rake fontico:update` — re-fetch remotes,
|
|
33
33
|
# keep the append-only codepoints.
|
|
34
|
-
|
|
34
|
+
# A lock from before sizes were recorded is taken at the manifest's word
|
|
35
|
+
# once, here, so an upgrade does not re-fetch everything. From then on a
|
|
36
|
+
# change to `defaults: size:` is caught like any other input change.
|
|
37
|
+
@lock.assume_size(@manifest.size)
|
|
38
|
+
|
|
39
|
+
stale = @manifest.icons.select do |icon|
|
|
40
|
+
icon.local? || @force ||
|
|
41
|
+
!@lock.fresh?(icon.name, icon.source, size: @manifest.size, multicolor: icon.multicolor)
|
|
42
|
+
end
|
|
35
43
|
cached = @manifest.icons.map(&:name) - stale.map(&:name)
|
|
36
44
|
|
|
37
45
|
unless stale.empty?
|
|
@@ -40,10 +48,7 @@ module Fontico
|
|
|
40
48
|
# when a remote body has to come off the wire — which under force is
|
|
41
49
|
# every remote, lock or no lock. Saying "missing" there would be a
|
|
42
50
|
# lie: the bodies are present, force is what made them stale.
|
|
43
|
-
if @offline && remote_stale.any?
|
|
44
|
-
reason = @force ? "re-fetch #{remote_stale.size} icon(s)" : "resolve #{remote_stale.size} icon(s) missing from icons.lock"
|
|
45
|
-
raise Error, "cannot #{reason} and --offline was given"
|
|
46
|
-
end
|
|
51
|
+
raise Error, "cannot #{offline_blocker(remote_stale)} and --offline was given" if @offline && remote_stale.any?
|
|
47
52
|
|
|
48
53
|
resolver = Resolver.new(@manifest, root: @root)
|
|
49
54
|
sources = resolver.call(only: stale.map(&:name))
|
|
@@ -63,7 +68,7 @@ module Fontico
|
|
|
63
68
|
# remote is judged by the wire, not the digest: under force it was
|
|
64
69
|
# genuinely re-fetched even when it came back byte-identical.
|
|
65
70
|
before = @lock.entry(icon.name)&.fetch("digest", nil)
|
|
66
|
-
@lock.store(icon.name, source: icon.source, body: pre.body,
|
|
71
|
+
@lock.store(icon.name, source: icon.source, body: pre.body, size: @manifest.size,
|
|
67
72
|
multicolor: pre.multicolor, warnings: pre.warnings)
|
|
68
73
|
unchanged = icon.local? && @lock.entry(icon.name)["digest"] == before
|
|
69
74
|
(unchanged ? cached : fetched) << icon.name
|
|
@@ -89,6 +94,25 @@ module Fontico
|
|
|
89
94
|
|
|
90
95
|
private
|
|
91
96
|
|
|
97
|
+
# "missing from icons.lock" is a lie when every body is present and
|
|
98
|
+
# something else invalidated them, so name the thing that actually did.
|
|
99
|
+
# A body carries its refit baked in, so a new size needs the original
|
|
100
|
+
# geometry back off the wire — there is nothing on disk to redo it from.
|
|
101
|
+
def offline_blocker(remote_stale)
|
|
102
|
+
count = remote_stale.size
|
|
103
|
+
return "re-fetch #{count} icon(s)" if @force
|
|
104
|
+
return "resolve #{count} icon(s) missing from icons.lock" unless remote_stale.all? { locked?(_1) }
|
|
105
|
+
|
|
106
|
+
"re-preprocess #{count} icon(s) at size: #{@manifest.size}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Present in the lock, with a body, for this source — so not "missing",
|
|
110
|
+
# only invalidated by one of the inputs #fresh? checks alongside it.
|
|
111
|
+
def locked?(icon)
|
|
112
|
+
e = @lock.entry(icon.name)
|
|
113
|
+
!e.nil? && !e["body"].nil? && e["source"] == icon.source
|
|
114
|
+
end
|
|
115
|
+
|
|
92
116
|
def emit(icons)
|
|
93
117
|
FileUtils.mkdir_p(@output)
|
|
94
118
|
written = []
|
|
@@ -96,9 +120,18 @@ module Fontico
|
|
|
96
120
|
|
|
97
121
|
targets = @manifest.targets - PENDING
|
|
98
122
|
targets += ["css"] if targets.include?("sprite") && !targets.include?("css")
|
|
123
|
+
# Both C targets lean on the font: the header is a map into it, and the
|
|
124
|
+
# gfxfont is rasterised out of it. Same implication as sprite -> css.
|
|
125
|
+
needs_font = targets.include?("c") || targets.include?("gfxfont")
|
|
126
|
+
targets += ["ttf"] if needs_font && !targets.include?("ttf")
|
|
127
|
+
# And the gfxfont reads the .ttf off disk, so it has to run after it.
|
|
128
|
+
targets = targets.partition { _1 != "gfxfont" }.flatten
|
|
99
129
|
|
|
100
130
|
targets.each do |target|
|
|
101
|
-
path =
|
|
131
|
+
path = path_for(target)
|
|
132
|
+
# Before the emitter runs, not after it returns: a font target writes
|
|
133
|
+
# its own file through Node and needs the directory already there.
|
|
134
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
102
135
|
emitter = emitter_for(target, [], path: path)
|
|
103
136
|
|
|
104
137
|
accepted = icons.select { emitter.accepts?(_1) }
|
|
@@ -115,12 +148,29 @@ module Fontico
|
|
|
115
148
|
[written, skipped]
|
|
116
149
|
end
|
|
117
150
|
|
|
151
|
+
# An explicit path in the manifest wins, so a firmware header can land in
|
|
152
|
+
# the device's include/ directory instead of among the web artifacts.
|
|
153
|
+
#
|
|
154
|
+
# Always absolute: a font target hands its path to Node, which runs from
|
|
155
|
+
# its own toolchain directory and cannot resolve a relative one.
|
|
156
|
+
def path_for(target)
|
|
157
|
+
override = @manifest.target_path(target)
|
|
158
|
+
return File.expand_path(override, @root) if override
|
|
159
|
+
|
|
160
|
+
File.expand_path(filename_for(target), @output)
|
|
161
|
+
end
|
|
162
|
+
|
|
118
163
|
def emitter_for(target, build = [], path: nil)
|
|
119
164
|
case target
|
|
120
165
|
when "sprite" then Emitters::Sprite.new(@manifest, build)
|
|
121
166
|
when "css" then Emitters::Stylesheet.new(@manifest, build)
|
|
122
167
|
when "ttf" then Emitters::Font.new(@manifest, build, lock: @lock, output: path)
|
|
123
|
-
|
|
168
|
+
when "c" then Emitters::Header.new(@manifest, build, lock: @lock)
|
|
169
|
+
when "gfxfont"
|
|
170
|
+
opts = @manifest.target_options(target)
|
|
171
|
+
Emitters::GfxFont.new(@manifest, build, lock: @lock, ttf: path_for("ttf"),
|
|
172
|
+
size: (opts["size"] || 24).to_i)
|
|
173
|
+
else raise Error, "unknown target #{target.inspect} (have: sprite, ttf, c, gfxfont)"
|
|
124
174
|
end
|
|
125
175
|
end
|
|
126
176
|
|
|
@@ -128,7 +178,9 @@ module Fontico
|
|
|
128
178
|
case target
|
|
129
179
|
when "sprite" then "icons.svg"
|
|
130
180
|
when "css" then "icons.css"
|
|
131
|
-
when "ttf"
|
|
181
|
+
when "ttf" then "icons.ttf"
|
|
182
|
+
when "c" then "icons.h"
|
|
183
|
+
when "gfxfont" then "icons_font.h"
|
|
132
184
|
else raise Error, "unknown target #{target.inspect}"
|
|
133
185
|
end
|
|
134
186
|
end
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontico
|
|
4
|
+
module Emitters
|
|
5
|
+
# An Adafruit GFXfont, for a display driven by Adafruit_GFX or Arduino_GFX.
|
|
6
|
+
# It compiles into the binary, so unlike the TTF target there is no
|
|
7
|
+
# FreeType on the device, no font renderer, and no filesystem to flash.
|
|
8
|
+
#
|
|
9
|
+
# The trade is that a GFXfont is a fixed size and one bit deep. Ask for the
|
|
10
|
+
# size you will draw at; scaling one up is what the built-in 6x8 font
|
|
11
|
+
# already does badly.
|
|
12
|
+
#
|
|
13
|
+
# Char codes rather than codepoints, because that is what the draw path
|
|
14
|
+
# takes: Adafruit_GFX and Arduino_GFX both index glyphs as `c - first` off
|
|
15
|
+
# a byte. They are therefore positional, which is exactly why the codes and
|
|
16
|
+
# the bitmaps are emitted into one file — they cannot fall out of step with
|
|
17
|
+
# each other. What keeps them stable across builds is the order, and the
|
|
18
|
+
# order comes from the append-only codepoints in icons.lock.
|
|
19
|
+
class GfxFont
|
|
20
|
+
# 0x20..0xFF. Below 0x20 is control characters, which some print paths
|
|
21
|
+
# intercept before the font ever sees them.
|
|
22
|
+
FIRST = 0x20
|
|
23
|
+
LAST = 0xFF
|
|
24
|
+
CAPACITY = LAST - FIRST + 1
|
|
25
|
+
|
|
26
|
+
def initialize(manifest, build, lock: nil, ttf: nil, size: 24, runner: nil)
|
|
27
|
+
@manifest = manifest
|
|
28
|
+
@build = build
|
|
29
|
+
@lock = lock
|
|
30
|
+
@ttf = ttf
|
|
31
|
+
@size = size
|
|
32
|
+
@runner = runner || NodeRunner.new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Same rule as the font it is rasterised from: one bit cannot hold
|
|
36
|
+
# colour, so a multicolour icon has nothing to become.
|
|
37
|
+
def accepts?(icon) = !(@lock&.multicolor?(icon.name) || icon.multicolor?)
|
|
38
|
+
def rules_only? = false
|
|
39
|
+
|
|
40
|
+
def call
|
|
41
|
+
icons = @build.map(&:first).sort_by { codepoint!(_1) }
|
|
42
|
+
# A GFXfont with no glyphs is not a font. This is reachable when every
|
|
43
|
+
# icon in the manifest was dropped as multicolour.
|
|
44
|
+
raise Fontico::Error, "the gfxfont target has no icons left to rasterise" if icons.empty?
|
|
45
|
+
|
|
46
|
+
guard_capacity!(icons)
|
|
47
|
+
|
|
48
|
+
result = @runner.run("rasterize.mjs", {
|
|
49
|
+
ttf: @ttf, size: @size,
|
|
50
|
+
glyphs: icons.map { { name: _1.name, codepoint: codepoint!(_1) } }
|
|
51
|
+
})
|
|
52
|
+
rows = rows_for(result["glyphs"], icons)
|
|
53
|
+
guard_blank!(rows)
|
|
54
|
+
|
|
55
|
+
render(rows, result["yAdvance"])
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def codepoint!(icon)
|
|
61
|
+
@lock&.codepoint_for(icon.name) ||
|
|
62
|
+
raise(Fontico::Error, "#{icon.name} has no codepoint in icons.lock")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def rows_for(glyphs, icons)
|
|
66
|
+
by_name = glyphs.to_h { [_1["name"], _1] }
|
|
67
|
+
offset = 0
|
|
68
|
+
icons.each_with_index.map do |icon, i|
|
|
69
|
+
g = by_name.fetch(icon.name)
|
|
70
|
+
row = { name: icon.name, source: icon.source, char: FIRST + i,
|
|
71
|
+
ident: Header.identifier(icon.name), offset: offset, glyph: g }
|
|
72
|
+
offset += g["bytes"].size
|
|
73
|
+
row
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# A GFXfont has room for one byte's worth of glyphs and no more. Failing
|
|
78
|
+
# here beats emitting a font that silently stops at icon 224.
|
|
79
|
+
def guard_capacity!(icons)
|
|
80
|
+
return if icons.size <= CAPACITY
|
|
81
|
+
|
|
82
|
+
raise Fontico::Error,
|
|
83
|
+
"the gfxfont target holds #{CAPACITY} icons (a GFXfont is indexed off a byte) " \
|
|
84
|
+
"and this manifest has #{icons.size}. Split the icons across two fonts, or " \
|
|
85
|
+
"build the c target instead, which addresses codepoints directly."
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# An icon that rasterised to nothing would draw as empty space with no
|
|
89
|
+
# complaint, on a screen nobody is watching.
|
|
90
|
+
def guard_blank!(rows)
|
|
91
|
+
blank = rows.select { _1[:glyph]["blank"] }
|
|
92
|
+
return if blank.empty?
|
|
93
|
+
|
|
94
|
+
raise Fontico::Error, <<~MSG
|
|
95
|
+
#{blank.size} icon(s) rasterised blank at #{@size}px and would draw as nothing:
|
|
96
|
+
|
|
97
|
+
#{blank.map { _1[:name] }.join("\n ")}
|
|
98
|
+
|
|
99
|
+
Thin strokes fall through a one-bit threshold at small sizes. Either
|
|
100
|
+
raise the gfxfont size, or drop these from the target.
|
|
101
|
+
MSG
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def render(rows, y_advance)
|
|
105
|
+
bytes = rows.flat_map { _1[:glyph]["bytes"] }
|
|
106
|
+
tag = "fontico_icons#{@size}"
|
|
107
|
+
|
|
108
|
+
<<~HEADER
|
|
109
|
+
/* generated by fontico — do not edit.
|
|
110
|
+
*
|
|
111
|
+
* #{rows.size} icon(s) as an Adafruit GFXfont, rasterised at #{@size}px, one bit
|
|
112
|
+
* deep. #{bytes.size} bytes of bitmap. Include this after your GFX library
|
|
113
|
+
* header; it defines the GFXglyph/GFXfont structs only if that has
|
|
114
|
+
* not already done so.
|
|
115
|
+
*
|
|
116
|
+
* gfx->setFont(&#{tag});
|
|
117
|
+
* gfx->drawChar(x, y, #{rows.first[:ident]}, fg, bg, 1);
|
|
118
|
+
*
|
|
119
|
+
* The char codes below are positional, so they and the bitmaps are
|
|
120
|
+
* one artifact and are regenerated together. Their order follows the
|
|
121
|
+
* append-only codepoints in icons.lock, so adding an icon appends
|
|
122
|
+
* rather than renumbering what is already flashed.
|
|
123
|
+
*/
|
|
124
|
+
#ifndef FONTICO_GFXFONT_H
|
|
125
|
+
#define FONTICO_GFXFONT_H
|
|
126
|
+
|
|
127
|
+
#include <stdint.h>
|
|
128
|
+
|
|
129
|
+
/* Adafruit_GFX's own guard, so the library wins when it is present. */
|
|
130
|
+
#ifndef _GFXFONT_H_
|
|
131
|
+
#define _GFXFONT_H_
|
|
132
|
+
typedef struct {
|
|
133
|
+
uint16_t bitmapOffset;
|
|
134
|
+
uint8_t width, height, xAdvance;
|
|
135
|
+
int8_t xOffset, yOffset;
|
|
136
|
+
} GFXglyph;
|
|
137
|
+
typedef struct {
|
|
138
|
+
uint8_t *bitmap;
|
|
139
|
+
GFXglyph *glyph;
|
|
140
|
+
uint16_t first, last;
|
|
141
|
+
uint8_t yAdvance;
|
|
142
|
+
} GFXfont;
|
|
143
|
+
#endif
|
|
144
|
+
|
|
145
|
+
#ifndef PROGMEM
|
|
146
|
+
#define PROGMEM
|
|
147
|
+
#endif
|
|
148
|
+
|
|
149
|
+
#{defines(rows).join("\n")}
|
|
150
|
+
|
|
151
|
+
/* One continuous MSB-first bitstream, no padding between rows. */
|
|
152
|
+
static const uint8_t #{tag}_bitmaps[] PROGMEM = {
|
|
153
|
+
#{bitmap_lines(bytes).join("\n")}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
static const GFXglyph #{tag}_glyphs[] PROGMEM = {
|
|
157
|
+
#{rows.map { glyph_line(_1) }.join("\n")}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
static const GFXfont #{tag} PROGMEM = {
|
|
161
|
+
(uint8_t *)#{tag}_bitmaps,
|
|
162
|
+
(GFXglyph *)#{tag}_glyphs,
|
|
163
|
+
0x#{format("%02X", FIRST)}, 0x#{format("%02X", rows.last[:char])}, #{y_advance}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
#endif /* FONTICO_GFXFONT_H */
|
|
167
|
+
HEADER
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def defines(rows)
|
|
171
|
+
width = rows.map { _1[:ident].length }.max.to_i
|
|
172
|
+
rows.map do |r|
|
|
173
|
+
format("#define %-#{width}s 0x%02X /* %s %s */",
|
|
174
|
+
r[:ident], r[:char], r[:name], r[:source])
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def bitmap_lines(bytes)
|
|
179
|
+
bytes.each_slice(16).map { " #{_1.map { |b| format("0x%02X", b) }.join(", ")}," }
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def glyph_line(row)
|
|
183
|
+
g = row[:glyph]
|
|
184
|
+
format(" { %5d, %3d, %3d, %3d, %4d, %4d }, /* %s */",
|
|
185
|
+
row[:offset], g["width"], g["height"], g["xAdvance"],
|
|
186
|
+
g["xOffset"], g["yOffset"], row[:name])
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fontico
|
|
4
|
+
module Emitters
|
|
5
|
+
# A C header, so firmware names icons by intent like every other consumer.
|
|
6
|
+
# The device draws them out of the TTF this is emitted alongside, and the
|
|
7
|
+
# two agree because both read codepoints from icons.lock.
|
|
8
|
+
#
|
|
9
|
+
# That pinning matters more here than anywhere else fontico uses it. A
|
|
10
|
+
# codepoint compiled into a flashed .bin cannot be hotfixed: if adding an
|
|
11
|
+
# icon renumbered the others, every board in the field would start drawing
|
|
12
|
+
# the wrong picture after an OTA. The lock is append-only precisely so that
|
|
13
|
+
# cannot happen.
|
|
14
|
+
#
|
|
15
|
+
# What the firmware gets out of this, beyond the naming: the UTF-8 bytes
|
|
16
|
+
# are literals decided at build time, so nothing parses "U+e63e" or
|
|
17
|
+
# assembles a String on the heap inside a draw loop.
|
|
18
|
+
class Header
|
|
19
|
+
PREFIX = "ICON_"
|
|
20
|
+
|
|
21
|
+
# Every byte of a private-use codepoint's UTF-8 is emitted as a \x
|
|
22
|
+
# escape and nothing else follows it inside the quotes, so C's greedy
|
|
23
|
+
# hex escapes have nothing extra to swallow.
|
|
24
|
+
def initialize(manifest, build, lock: nil)
|
|
25
|
+
@manifest = manifest
|
|
26
|
+
@build = build
|
|
27
|
+
@lock = lock
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# A glyph carries no colour, so the same icons the font refuses are the
|
|
31
|
+
# ones this cannot name — there would be no glyph at the codepoint.
|
|
32
|
+
def accepts?(icon) = !(@lock&.multicolor?(icon.name) || icon.multicolor?)
|
|
33
|
+
def rules_only? = false
|
|
34
|
+
|
|
35
|
+
def self.identifier(name) = "#{PREFIX}#{name.upcase.gsub(/[^A-Z0-9]+/, "_")}"
|
|
36
|
+
|
|
37
|
+
# U+E001 -> "\xEE\x80\x81"
|
|
38
|
+
def self.utf8_literal(codepoint)
|
|
39
|
+
[codepoint].pack("U").bytes.map { format("\\x%02X", _1) }.join
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def call
|
|
43
|
+
rows = @build.map { |icon, _| row(icon) }.sort_by { _1[:name] }
|
|
44
|
+
guard_ascii!(rows)
|
|
45
|
+
guard_collisions!(rows)
|
|
46
|
+
|
|
47
|
+
<<~HEADER
|
|
48
|
+
/* generated by fontico — do not edit.
|
|
49
|
+
*
|
|
50
|
+
* #{rows.size} icon(s) from #{File.basename(@manifest.path || "icons.yml")}, drawn out of
|
|
51
|
+
* the icons.ttf built beside it. Codepoints are pinned in icons.lock
|
|
52
|
+
* and never reissued, so a name compiled into a flashed image keeps
|
|
53
|
+
* meaning the same picture across every later build.
|
|
54
|
+
*/
|
|
55
|
+
#ifndef FONTICO_ICONS_H
|
|
56
|
+
#define FONTICO_ICONS_H
|
|
57
|
+
|
|
58
|
+
#include <stdint.h>
|
|
59
|
+
|
|
60
|
+
#ifdef __cplusplus
|
|
61
|
+
extern "C" {
|
|
62
|
+
#endif
|
|
63
|
+
|
|
64
|
+
#define FONTICO_ICON_COUNT #{rows.size}
|
|
65
|
+
|
|
66
|
+
/* UTF-8 literals, ready to hand to OpenFontRender / FreeType. */
|
|
67
|
+
#{defines(rows).join("\n")}
|
|
68
|
+
|
|
69
|
+
typedef struct {
|
|
70
|
+
const char *name;
|
|
71
|
+
const char *utf8;
|
|
72
|
+
uint16_t codepoint;
|
|
73
|
+
} FonticoIcon;
|
|
74
|
+
|
|
75
|
+
/* Sorted by name, for an icon chosen at runtime rather than compiled
|
|
76
|
+
* in — a name arriving in an MQTT payload, say. Returns NULL when the
|
|
77
|
+
* manifest has no such icon, which is the one honest answer: drawing
|
|
78
|
+
* a codepoint the font has no glyph for is an invisible box.
|
|
79
|
+
*
|
|
80
|
+
* The table lives in whichever translation unit defines
|
|
81
|
+
* FONTICO_ICONS_IMPLEMENTATION before including this file. Exactly
|
|
82
|
+
* one must, or the linker will say so. Nothing else needs to change,
|
|
83
|
+
* and a firmware that only ever uses the #{PREFIX}* defines can skip it
|
|
84
|
+
* entirely and carry no table at all.
|
|
85
|
+
*/
|
|
86
|
+
const char *fontico_icon(const char *name);
|
|
87
|
+
extern const FonticoIcon fontico_icons[FONTICO_ICON_COUNT];
|
|
88
|
+
|
|
89
|
+
#ifdef FONTICO_ICONS_IMPLEMENTATION
|
|
90
|
+
#include <stdlib.h>
|
|
91
|
+
#include <string.h>
|
|
92
|
+
|
|
93
|
+
const FonticoIcon fontico_icons[FONTICO_ICON_COUNT] = {
|
|
94
|
+
#{rows.map { %( { "#{_1[:name]}", #{_1[:ident]}, 0x#{format("%04X", _1[:codepoint])} },) }.join("\n")}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
static int fontico_icon_cmp(const void *key, const void *el) {
|
|
98
|
+
return strcmp((const char *)key, ((const FonticoIcon *)el)->name);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const char *fontico_icon(const char *name) {
|
|
102
|
+
const FonticoIcon *hit;
|
|
103
|
+
if (!name) return NULL;
|
|
104
|
+
hit = (const FonticoIcon *)bsearch(name, fontico_icons, FONTICO_ICON_COUNT,
|
|
105
|
+
sizeof(FonticoIcon), fontico_icon_cmp);
|
|
106
|
+
return hit ? hit->utf8 : NULL;
|
|
107
|
+
}
|
|
108
|
+
#endif /* FONTICO_ICONS_IMPLEMENTATION */
|
|
109
|
+
|
|
110
|
+
#ifdef __cplusplus
|
|
111
|
+
}
|
|
112
|
+
#endif
|
|
113
|
+
|
|
114
|
+
#endif /* FONTICO_ICONS_H */
|
|
115
|
+
HEADER
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def row(icon)
|
|
121
|
+
# Never mint one here. The builder has already save!d the lock by the
|
|
122
|
+
# time it emits, so a codepoint invented now would reach the firmware
|
|
123
|
+
# without ever reaching disk — and the font would have no glyph there.
|
|
124
|
+
cp = @lock&.codepoint_for(icon.name)
|
|
125
|
+
raise Fontico::Error, "#{icon.name} has no codepoint in icons.lock" if cp.nil?
|
|
126
|
+
|
|
127
|
+
{ name: icon.name, ident: self.class.identifier(icon.name),
|
|
128
|
+
codepoint: cp, source: icon.source }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# The table is sorted here with Ruby's byte-order <=> and searched on the
|
|
132
|
+
# device with strcmp, where `char` is signed on most toolchains. A byte
|
|
133
|
+
# above 0x7F orders one way here and the opposite there, so bsearch would
|
|
134
|
+
# quietly miss the icon rather than fail — an invisible box again.
|
|
135
|
+
def guard_ascii!(rows)
|
|
136
|
+
wide = rows.reject { _1[:name].ascii_only? }
|
|
137
|
+
return if wide.empty?
|
|
138
|
+
|
|
139
|
+
raise Fontico::Error,
|
|
140
|
+
"the c target needs ASCII icon names; these are not: " \
|
|
141
|
+
"#{wide.map { _1[:name] }.join(", ")}"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# ICON_NAV_MENU can be reached from nav.menu and from nav-menu, and C
|
|
145
|
+
# would take the second #define silently. Name both instead.
|
|
146
|
+
def guard_collisions!(rows)
|
|
147
|
+
clashes = rows.group_by { _1[:ident] }.select { |_, v| v.size > 1 }
|
|
148
|
+
return if clashes.empty?
|
|
149
|
+
|
|
150
|
+
detail = clashes.map { |ident, group| "#{ident} <- #{group.map { _1[:name] }.join(", ")}" }
|
|
151
|
+
raise Fontico::Error,
|
|
152
|
+
"icon names collide as C identifiers:\n #{detail.join("\n ")}\n" \
|
|
153
|
+
"Rename one side in icons.yml; dots, dashes and case all fold to the same macro."
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def defines(rows)
|
|
157
|
+
width = rows.map { _1[:ident].length }.max.to_i
|
|
158
|
+
rows.map do |r|
|
|
159
|
+
format(%(#define %-#{width}s "%s" /* %s U+%04X */),
|
|
160
|
+
r[:ident], self.class.utf8_literal(r[:codepoint]), r[:source], r[:codepoint])
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -18,9 +18,13 @@ module Fontico
|
|
|
18
18
|
def accepts?(_icon) = false
|
|
19
19
|
def rules_only? = true
|
|
20
20
|
|
|
21
|
+
# In `@layer base`: an unlayered rule outranks every layered one, so
|
|
22
|
+
# Tailwind 4's `size-6` (in `@layer utilities`) used to lose to the
|
|
23
|
+
# 1em default here. Layered, the defaults hold and a class still wins.
|
|
21
24
|
def call
|
|
22
25
|
<<~CSS
|
|
23
26
|
/* generated by fontico — do not edit */
|
|
27
|
+
@layer base {
|
|
24
28
|
.#{Fontico.css_class} {
|
|
25
29
|
display: inline-block;
|
|
26
30
|
width: 1em;
|
|
@@ -38,6 +42,7 @@ module Fontico
|
|
|
38
42
|
/* Optical sizing for icons set beside larger text. */
|
|
39
43
|
.#{Fontico.css_class}-lg { width: 1.25em; height: 1.25em; }
|
|
40
44
|
.#{Fontico.css_class}-xl { width: 1.5em; height: 1.5em; }
|
|
45
|
+
}
|
|
41
46
|
CSS
|
|
42
47
|
end
|
|
43
48
|
end
|
data/lib/fontico/helper.rb
CHANGED
|
@@ -10,27 +10,22 @@ module Fontico
|
|
|
10
10
|
# it on the baseline. Pass size: to override, or a CSS class — classes win
|
|
11
11
|
# over the attributes, so `class: "size-6"` works untouched.
|
|
12
12
|
def icon(name, size: nil, variant: nil, **options)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return missing(name) if entry.nil?
|
|
13
|
+
key = name.to_s
|
|
14
|
+
Fontico.check!(key)
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
body = +""
|
|
29
|
-
body << "<title>#{ERB::Util.html_escape(title)}</title>" if title
|
|
30
|
-
body << %(<use href="#{sprite_path(variant)}##{symbol}"></use>)
|
|
16
|
+
# A bare icon(name) is deterministic apart from one thing. The attrs
|
|
17
|
+
# hash, the html_escape of constants like "1em" and "true", the string
|
|
18
|
+
# assembly — 22 objects a call, every call, for markup that never
|
|
19
|
+
# changes. The exception is the sprite path, because asset_host is
|
|
20
|
+
# allowed to be a proc that reads the request, so what gets cached is
|
|
21
|
+
# the markup either side of the href and the path is still resolved per
|
|
22
|
+
# call. Fontico.reset! drops the cache, which the dev watcher already
|
|
23
|
+
# calls after every rebuild.
|
|
24
|
+
bare = size.nil? && variant.nil? && options.empty?
|
|
25
|
+
pair = bare ? Fontico.icon_cache[key] ||= halves(key) : halves(key, size: size, **options)
|
|
26
|
+
return missing(name) if pair.nil?
|
|
31
27
|
|
|
32
|
-
|
|
33
|
-
tag.respond_to?(:html_safe) ? tag.html_safe : tag
|
|
28
|
+
safe("#{pair[0]}#{sprite_path(variant)}#{pair[1]}")
|
|
34
29
|
end
|
|
35
30
|
|
|
36
31
|
# The `<use href>` for one icon, for markup this helper doesn't build —
|
|
@@ -45,8 +40,9 @@ module Fontico
|
|
|
45
40
|
# icons_sprite for the inline case, where the path half is empty and the
|
|
46
41
|
# fragment resolves against the current document.
|
|
47
42
|
def icon_href(name)
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
key = name.to_s
|
|
44
|
+
Fontico.check!(key)
|
|
45
|
+
entry = Fontico.manifest[key]
|
|
50
46
|
raise Fontico::Error, "no icon named #{name.inspect} in #{Fontico.manifest_path}" if entry.nil?
|
|
51
47
|
|
|
52
48
|
"#{sprite_path}##{entry.key}"
|
|
@@ -56,12 +52,38 @@ module Fontico
|
|
|
56
52
|
# where a cross-origin <use href> would silently render nothing.
|
|
57
53
|
def icons_sprite
|
|
58
54
|
Fontico.check!
|
|
59
|
-
|
|
60
|
-
svg.respond_to?(:html_safe) ? svg.html_safe : svg
|
|
55
|
+
safe(Fontico.sprite_markup)
|
|
61
56
|
end
|
|
62
57
|
|
|
63
58
|
private
|
|
64
59
|
|
|
60
|
+
# The markup either side of the `href` value, so a caller can drop a
|
|
61
|
+
# freshly resolved sprite path between them. Nil when the manifest has no
|
|
62
|
+
# such name. Everything in here is a pure function of the entry and the
|
|
63
|
+
# options, which is exactly what makes the bare case worth caching.
|
|
64
|
+
def halves(key, size: nil, **options)
|
|
65
|
+
entry = Fontico.manifest[key]
|
|
66
|
+
return nil if entry.nil?
|
|
67
|
+
|
|
68
|
+
attrs = {
|
|
69
|
+
class: [Fontico.css_class, options.delete(:class)].compact.join(" "),
|
|
70
|
+
width: size || "1em",
|
|
71
|
+
height: size || "1em",
|
|
72
|
+
style: inline_size(size, options.delete(:style)),
|
|
73
|
+
"aria-hidden": options.key?(:title) ? nil : "true",
|
|
74
|
+
role: options.key?(:title) ? "img" : nil
|
|
75
|
+
}.merge(options).compact
|
|
76
|
+
|
|
77
|
+
title = attrs.delete(:title)
|
|
78
|
+
open = +"<svg #{attrs.map { |k, v| %(#{k}="#{ERB::Util.html_escape(v)}") }.join(" ")}>"
|
|
79
|
+
open << "<title>#{ERB::Util.html_escape(title)}</title>" if title
|
|
80
|
+
open << %(<use href=")
|
|
81
|
+
|
|
82
|
+
[open.freeze, %(##{entry.key}"></use></svg>).freeze]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def safe(str) = str.respond_to?(:html_safe) ? str.html_safe : str
|
|
86
|
+
|
|
65
87
|
# width/height on an <svg> are presentation attributes, and *any* CSS
|
|
66
88
|
# declaration outranks those — including icons.css's own `.ico { width:
|
|
67
89
|
# 1em }`. So an explicitly requested size has to be inline or the
|
data/lib/fontico/lockfile.rb
CHANGED
|
@@ -41,9 +41,12 @@ module Fontico
|
|
|
41
41
|
|
|
42
42
|
def entry(name) = @entries[name]
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
# +size+ is the grid the body was refit to. It is recorded because it is
|
|
45
|
+
# an input to the body, not a property of the request for it — see #fresh?.
|
|
46
|
+
def store(name, source:, body:, size:, multicolor: false, warnings: [])
|
|
45
47
|
@entries[name] = {
|
|
46
48
|
"source" => source,
|
|
49
|
+
"size" => size,
|
|
47
50
|
"digest" => Digest::SHA256.hexdigest(body)[0, 16],
|
|
48
51
|
"multicolor" => multicolor,
|
|
49
52
|
"warnings" => warnings,
|
|
@@ -52,6 +55,16 @@ module Fontico
|
|
|
52
55
|
allocate(name)
|
|
53
56
|
end
|
|
54
57
|
|
|
58
|
+
# A lock written before sizes were recorded cannot say what grid its
|
|
59
|
+
# bodies were refit to. Taking them at the size now in the manifest costs
|
|
60
|
+
# nothing when it is right, and `rake fontico:update` fixes it when it is
|
|
61
|
+
# not. The alternative — calling every body stale — would re-fetch a whole
|
|
62
|
+
# manifest on a gem upgrade, and put a network round trip in the first
|
|
63
|
+
# deploy after it.
|
|
64
|
+
def assume_size(size)
|
|
65
|
+
@entries.each_value { _1["size"] ||= size }
|
|
66
|
+
end
|
|
67
|
+
|
|
55
68
|
# Names present in the lock but absent from the manifest keep their
|
|
56
69
|
# codepoint reserved so it is never handed to a different icon.
|
|
57
70
|
def retire_missing!(names)
|
|
@@ -61,8 +74,28 @@ module Fontico
|
|
|
61
74
|
end
|
|
62
75
|
end
|
|
63
76
|
|
|
64
|
-
|
|
65
|
-
|
|
77
|
+
# A cached body is only reusable if every input that produced it still
|
|
78
|
+
# holds. The source is the obvious one; the other two were the bug.
|
|
79
|
+
#
|
|
80
|
+
# size the body carries its refit baked in as a <g transform>,
|
|
81
|
+
# while the sprite emitter reads the target size live. Keyed
|
|
82
|
+
# on the source alone, changing `defaults: size:` moved every
|
|
83
|
+
# <symbol> to the new viewBox and left the geometry inside
|
|
84
|
+
# fit to the old grid — every icon a fraction of its size in
|
|
85
|
+
# the corner of its box, on a page that 200s. Local files are
|
|
86
|
+
# re-read every build and self-heal, so this only ever showed
|
|
87
|
+
# on cached remotes: most of a real manifest.
|
|
88
|
+
#
|
|
89
|
+
# multicolor the *override* from the manifest, not the detected result.
|
|
90
|
+
# When one is set the result always equals it, so the stored
|
|
91
|
+
# result is the record of whether this body was preprocessed
|
|
92
|
+
# under it. Nil leaves detection in charge, and detection on
|
|
93
|
+
# an unchanged body gives an unchanged answer.
|
|
94
|
+
def fresh?(name, source, size:, multicolor: nil)
|
|
95
|
+
e = entry(name)
|
|
96
|
+
return false unless e && e["body"] && e["source"] == source && e["size"] == size
|
|
97
|
+
|
|
98
|
+
multicolor.nil? || e["multicolor"] == multicolor
|
|
66
99
|
end
|
|
67
100
|
|
|
68
101
|
def body(name) = entry(name)&.fetch("body", nil)
|
data/lib/fontico/manifest.rb
CHANGED
|
@@ -11,7 +11,7 @@ module Fontico
|
|
|
11
11
|
# Where local/ SVGs live when the manifest doesn't say otherwise.
|
|
12
12
|
LOCAL_PATH = "app/assets/icons"
|
|
13
13
|
|
|
14
|
-
attr_reader :path, :defaults, :providers, :targets, :icons
|
|
14
|
+
attr_reader :path, :defaults, :providers, :targets, :target_paths, :icons
|
|
15
15
|
|
|
16
16
|
def self.load(*paths)
|
|
17
17
|
paths = paths.flatten.compact.select { File.file?(_1) }
|
|
@@ -40,16 +40,29 @@ module Fontico
|
|
|
40
40
|
@path = path
|
|
41
41
|
@defaults = data["defaults"] || {}
|
|
42
42
|
@providers = data["providers"] || {}
|
|
43
|
-
@targets
|
|
43
|
+
@targets, @target_paths = parse_targets(data["targets"])
|
|
44
44
|
@icons = flatten(data["icons"] || {}).freeze
|
|
45
45
|
validate!
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
# Where a target's artifact goes, relative to the root, or nil to take the
|
|
49
|
+
# default name in output_dir.
|
|
50
|
+
def target_path(target) = target_options(target)["path"]
|
|
51
|
+
|
|
52
|
+
# Anything else the target was given in the manifest, e.g. a gfxfont size.
|
|
53
|
+
def target_options(target) = target_paths.fetch(target.to_s, {})
|
|
54
|
+
|
|
48
55
|
def default_provider = defaults.fetch("provider", "lucide")
|
|
49
56
|
def local_path = providers.dig("local", "path") || LOCAL_PATH
|
|
50
57
|
def size = defaults.fetch("size", 24).to_i
|
|
51
58
|
|
|
52
|
-
|
|
59
|
+
# Every icon() call in every template comes through here, and a linear
|
|
60
|
+
# scan charged for the size of the manifest rather than for the icons a
|
|
61
|
+
# page actually draws: 11.5us a call at 300 names against 5us at 92, so
|
|
62
|
+
# naming icons you never draw slowed down the ones you do. Backwards for
|
|
63
|
+
# a gem whose pitch is 353,000 icons to choose from. The table is built
|
|
64
|
+
# once and @icons is frozen, so there is nothing to invalidate.
|
|
65
|
+
def [](name) = index[name]
|
|
53
66
|
|
|
54
67
|
# Vendor icons grouped by provider so the resolver can batch one HTTP
|
|
55
68
|
# request per provider instead of one per icon.
|
|
@@ -61,6 +74,43 @@ module Fontico
|
|
|
61
74
|
|
|
62
75
|
private
|
|
63
76
|
|
|
77
|
+
# A list names the targets and each takes its default filename inside
|
|
78
|
+
# output_dir. A mapping also says where one goes, which is what firmware
|
|
79
|
+
# needs: icons.h belongs in the device's include/ directory, not in
|
|
80
|
+
# app/assets/builds next to the web artifacts. A target with more to say
|
|
81
|
+
# than a path takes a block instead.
|
|
82
|
+
#
|
|
83
|
+
# targets: [sprite, ttf]
|
|
84
|
+
#
|
|
85
|
+
# targets:
|
|
86
|
+
# sprite: # default location
|
|
87
|
+
# ttf: devs/data/icons.ttf # just a path
|
|
88
|
+
# gfxfont: # a path and a size
|
|
89
|
+
# path: devs/include/icons_font.h
|
|
90
|
+
# size: 24
|
|
91
|
+
def parse_targets(value)
|
|
92
|
+
case value
|
|
93
|
+
when nil then [["sprite"], {}]
|
|
94
|
+
when Array then [value.map(&:to_s), {}]
|
|
95
|
+
when Hash
|
|
96
|
+
[value.keys.map(&:to_s),
|
|
97
|
+
value.compact.to_h { [_1.to_s, target_settings(_1, _2)] }]
|
|
98
|
+
else
|
|
99
|
+
raise Error, "targets: must be a list or a mapping, got #{value.class}"
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def target_settings(target, value)
|
|
104
|
+
case value
|
|
105
|
+
when String then { "path" => value }
|
|
106
|
+
when Hash then value.to_h { [_1.to_s, _2] }
|
|
107
|
+
else
|
|
108
|
+
raise Error, "targets: #{target} must be a path or a mapping, got #{value.class}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def index = @index ||= icons.to_h { [_1.name, _1] }
|
|
113
|
+
|
|
64
114
|
def flatten(tree, prefix = nil)
|
|
65
115
|
tree.flat_map do |key, value|
|
|
66
116
|
name = [prefix, key].compact.join(".")
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Rasterise glyphs out of a built TTF into 1-bit bitmaps.
|
|
2
|
+
//
|
|
3
|
+
// Reading the font rather than the SVG bodies is deliberate: build_font.mjs
|
|
4
|
+
// has already normalised every icon to the em box and put it on a common
|
|
5
|
+
// baseline, so bitmaps taken from here inherit the same geometry the PDF
|
|
6
|
+
// uses. Rasterising the SVGs separately would re-derive all of that and drift.
|
|
7
|
+
//
|
|
8
|
+
// opentype.js is already a toolchain dependency, so this needs nothing new —
|
|
9
|
+
// it gives us outlines, and a scanline fill turns those into pixels.
|
|
10
|
+
//
|
|
11
|
+
// stdin: { ttf, size, glyphs: [{ name, codepoint }] }
|
|
12
|
+
// stdout: { glyphs: [{ name, codepoint, width, height, xAdvance,
|
|
13
|
+
// xOffset, yOffset, bytes: [...] }], yAdvance }
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
import opentype from 'opentype.js';
|
|
16
|
+
|
|
17
|
+
const job = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
18
|
+
|
|
19
|
+
// Coverage is sampled this many times per pixel on each axis. 4 is enough to
|
|
20
|
+
// place an edge convincingly at 24px and still runs in milliseconds; the
|
|
21
|
+
// result is thresholded to one bit regardless, so more buys very little.
|
|
22
|
+
const SS = 4;
|
|
23
|
+
// Curve subdivision. Flat enough that the error sits well under a subsample.
|
|
24
|
+
const CURVE_STEPS = 12;
|
|
25
|
+
|
|
26
|
+
const lerp = (a, b, t) => a + (b - a) * t;
|
|
27
|
+
|
|
28
|
+
function cubicAt(x0, y0, x1, y1, x2, y2, x3, y3, t) {
|
|
29
|
+
const ax = lerp(x0, x1, t), ay = lerp(y0, y1, t);
|
|
30
|
+
const bx = lerp(x1, x2, t), by = lerp(y1, y2, t);
|
|
31
|
+
const cx = lerp(x2, x3, t), cy = lerp(y2, y3, t);
|
|
32
|
+
const dx = lerp(ax, bx, t), dy = lerp(ay, by, t);
|
|
33
|
+
const ex = lerp(bx, cx, t), ey = lerp(by, cy, t);
|
|
34
|
+
return { x: lerp(dx, ex, t), y: lerp(dy, ey, t) };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function quadAt(x0, y0, x1, y1, x2, y2, t) {
|
|
38
|
+
const ax = lerp(x0, x1, t), ay = lerp(y0, y1, t);
|
|
39
|
+
const bx = lerp(x1, x2, t), by = lerp(y1, y2, t);
|
|
40
|
+
return { x: lerp(ax, bx, t), y: lerp(ay, by, t) };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Path commands to closed polygons. Every contour is implicitly closed: a
|
|
44
|
+
// fill has no notion of an open one, and leaving it open drops an edge.
|
|
45
|
+
function contoursOf(path) {
|
|
46
|
+
const out = [];
|
|
47
|
+
let cur = null;
|
|
48
|
+
let cx = 0, cy = 0, sx = 0, sy = 0;
|
|
49
|
+
|
|
50
|
+
const close = () => {
|
|
51
|
+
if (cur && cur.length > 2) out.push(cur);
|
|
52
|
+
cur = null;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
for (const cmd of path.commands) {
|
|
56
|
+
switch (cmd.type) {
|
|
57
|
+
case 'M':
|
|
58
|
+
close();
|
|
59
|
+
cur = [{ x: cmd.x, y: cmd.y }];
|
|
60
|
+
cx = sx = cmd.x; cy = sy = cmd.y;
|
|
61
|
+
break;
|
|
62
|
+
case 'L':
|
|
63
|
+
if (cur) cur.push({ x: cmd.x, y: cmd.y });
|
|
64
|
+
cx = cmd.x; cy = cmd.y;
|
|
65
|
+
break;
|
|
66
|
+
case 'C':
|
|
67
|
+
for (let i = 1; i <= CURVE_STEPS; i++) {
|
|
68
|
+
cur.push(cubicAt(cx, cy, cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.x, cmd.y, i / CURVE_STEPS));
|
|
69
|
+
}
|
|
70
|
+
cx = cmd.x; cy = cmd.y;
|
|
71
|
+
break;
|
|
72
|
+
case 'Q':
|
|
73
|
+
for (let i = 1; i <= CURVE_STEPS; i++) {
|
|
74
|
+
cur.push(quadAt(cx, cy, cmd.x1, cmd.y1, cmd.x, cmd.y, i / CURVE_STEPS));
|
|
75
|
+
}
|
|
76
|
+
cx = cmd.x; cy = cmd.y;
|
|
77
|
+
break;
|
|
78
|
+
case 'Z':
|
|
79
|
+
close();
|
|
80
|
+
cx = sx; cy = sy;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
close();
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function edgesOf(contours) {
|
|
89
|
+
const edges = [];
|
|
90
|
+
for (const c of contours) {
|
|
91
|
+
for (let i = 0; i < c.length; i++) {
|
|
92
|
+
const a = c[i], b = c[(i + 1) % c.length];
|
|
93
|
+
if (a.y !== b.y) edges.push([a.x, a.y, b.x, b.y]);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return edges;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Add a horizontal span's contribution, splitting it across the pixels it
|
|
100
|
+
// partially covers so edges land between pixels rather than on one side.
|
|
101
|
+
function addSpan(cov, w, row, xa, xb, weight) {
|
|
102
|
+
const lo = Math.max(0, xa), hi = Math.min(w, xb);
|
|
103
|
+
if (hi <= lo) return;
|
|
104
|
+
for (let px = Math.floor(lo); px < Math.ceil(hi); px++) {
|
|
105
|
+
const l = Math.max(lo, px), r = Math.min(hi, px + 1);
|
|
106
|
+
if (r > l) cov[row + px] += (r - l) * weight;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Scanline fill with the nonzero winding rule, which is what TrueType uses —
|
|
111
|
+
// even-odd would punch holes in any glyph whose contours overlap.
|
|
112
|
+
function coverage(edges, x0, y0, w, h) {
|
|
113
|
+
const cov = new Float64Array(w * h);
|
|
114
|
+
const weight = 1 / SS;
|
|
115
|
+
|
|
116
|
+
for (let py = 0; py < h; py++) {
|
|
117
|
+
for (let s = 0; s < SS; s++) {
|
|
118
|
+
const sy = y0 + py + (s + 0.5) / SS;
|
|
119
|
+
const hits = [];
|
|
120
|
+
for (const [ex0, ey0, ex1, ey1] of edges) {
|
|
121
|
+
const lo = Math.min(ey0, ey1), hi = Math.max(ey0, ey1);
|
|
122
|
+
if (sy < lo || sy >= hi) continue;
|
|
123
|
+
hits.push({ x: ex0 + ((sy - ey0) / (ey1 - ey0)) * (ex1 - ex0), dir: ey1 > ey0 ? 1 : -1 });
|
|
124
|
+
}
|
|
125
|
+
if (hits.length < 2) continue;
|
|
126
|
+
hits.sort((a, b) => a.x - b.x);
|
|
127
|
+
|
|
128
|
+
let wind = 0;
|
|
129
|
+
for (let i = 0; i < hits.length - 1; i++) {
|
|
130
|
+
wind += hits[i].dir;
|
|
131
|
+
if (wind !== 0) addSpan(cov, w, py * w, hits[i].x - x0, hits[i + 1].x - x0, weight);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return cov;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// GFXfont bitmaps are one continuous MSB-first bitstream with no padding
|
|
139
|
+
// between rows — drawBitmap's row-aligned layout is a different format, and
|
|
140
|
+
// mixing them up shears the glyph.
|
|
141
|
+
function pack(cov, w, h) {
|
|
142
|
+
const bytes = [];
|
|
143
|
+
let acc = 0, n = 0;
|
|
144
|
+
for (let i = 0; i < w * h; i++) {
|
|
145
|
+
acc = (acc << 1) | (cov[i] > 0.5 ? 1 : 0);
|
|
146
|
+
if (++n === 8) { bytes.push(acc & 0xff); acc = 0; n = 0; }
|
|
147
|
+
}
|
|
148
|
+
if (n) bytes.push((acc << (8 - n)) & 0xff);
|
|
149
|
+
return bytes;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const font = opentype.parse(new Uint8Array(fs.readFileSync(job.ttf)).buffer);
|
|
153
|
+
const size = job.size;
|
|
154
|
+
const scale = size / font.unitsPerEm;
|
|
155
|
+
const out = [];
|
|
156
|
+
|
|
157
|
+
for (const g of job.glyphs) {
|
|
158
|
+
const glyph = font.charToGlyph(String.fromCodePoint(g.codepoint));
|
|
159
|
+
// getPath flips y so it grows downward from the baseline, which is the
|
|
160
|
+
// direction GFXglyph's yOffset is measured in too.
|
|
161
|
+
const contours = contoursOf(glyph.getPath(0, 0, size));
|
|
162
|
+
const edges = edgesOf(contours);
|
|
163
|
+
const advance = Math.round((glyph.advanceWidth ?? font.unitsPerEm) * scale);
|
|
164
|
+
|
|
165
|
+
if (!edges.length) {
|
|
166
|
+
out.push({ ...g, width: 0, height: 0, xAdvance: advance, xOffset: 0, yOffset: 0, bytes: [], blank: true });
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Tight box: a GFXglyph stores only the inked area and offsets it, so
|
|
171
|
+
// whitespace costs nothing in flash.
|
|
172
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
173
|
+
for (const [ex0, ey0, ex1, ey1] of edges) {
|
|
174
|
+
minX = Math.min(minX, ex0, ex1); maxX = Math.max(maxX, ex0, ex1);
|
|
175
|
+
minY = Math.min(minY, ey0, ey1); maxY = Math.max(maxY, ey0, ey1);
|
|
176
|
+
}
|
|
177
|
+
const x0 = Math.floor(minX), y0 = Math.floor(minY);
|
|
178
|
+
const w = Math.max(1, Math.ceil(maxX) - x0);
|
|
179
|
+
const h = Math.max(1, Math.ceil(maxY) - y0);
|
|
180
|
+
|
|
181
|
+
const cov = coverage(edges, x0, y0, w, h);
|
|
182
|
+
const bytes = pack(cov, w, h);
|
|
183
|
+
const inked = cov.some((v) => v > 0.5);
|
|
184
|
+
|
|
185
|
+
out.push({
|
|
186
|
+
...g,
|
|
187
|
+
width: w, height: h, xAdvance: advance, xOffset: x0, yOffset: y0,
|
|
188
|
+
bytes, blank: !inked
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
process.stdout.write(JSON.stringify({ glyphs: out, yAdvance: Math.round(size * 1.2) }));
|
data/lib/fontico/version.rb
CHANGED
data/lib/fontico.rb
CHANGED
|
@@ -23,6 +23,8 @@ module Fontico
|
|
|
23
23
|
autoload :Sprite, "fontico/emitters/sprite"
|
|
24
24
|
autoload :Font, "fontico/emitters/font"
|
|
25
25
|
autoload :Stylesheet, "fontico/emitters/stylesheet"
|
|
26
|
+
autoload :Header, "fontico/emitters/header"
|
|
27
|
+
autoload :GfxFont, "fontico/emitters/gfx_font"
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
class << self
|
|
@@ -46,6 +48,24 @@ module Fontico
|
|
|
46
48
|
def manifest = @manifest ||= Manifest.load(*load_path)
|
|
47
49
|
def sprite_file = File.join(root, output_dir, "icons.svg")
|
|
48
50
|
|
|
51
|
+
# Rendered markup for bare icon() calls, minus the sprite path. Keyed by
|
|
52
|
+
# name and bounded by the manifest, so it cannot grow with traffic.
|
|
53
|
+
def icon_cache = @icon_cache ||= {}
|
|
54
|
+
|
|
55
|
+
# Inline mode embeds the sprite in every response, and it used to be
|
|
56
|
+
# re-read off disk on every call: 13us and a fresh 10KB string per
|
|
57
|
+
# request, more than a whole page of icon() calls costs. Held instead,
|
|
58
|
+
# behind a stat, which is cheap next to the read and means a rebuild from
|
|
59
|
+
# anywhere — the dev watcher, or `rake fontico:build` in another terminal
|
|
60
|
+
# — is picked up without waiting for a reset!.
|
|
61
|
+
def sprite_markup
|
|
62
|
+
stamp = File.stat(sprite_file).then { [_1.mtime, _1.size] }
|
|
63
|
+
return @sprite_markup if @sprite_stamp == stamp
|
|
64
|
+
|
|
65
|
+
@sprite_stamp = stamp
|
|
66
|
+
@sprite_markup = File.read(sprite_file).freeze
|
|
67
|
+
end
|
|
68
|
+
|
|
49
69
|
# Rails engines that ship config/icons.yml (or icons.yml at the gem
|
|
50
70
|
# root), then the app's file. The app is last on purpose.
|
|
51
71
|
def discover(app)
|
|
@@ -117,7 +137,12 @@ module Fontico
|
|
|
117
137
|
end
|
|
118
138
|
|
|
119
139
|
def glyph(name) = [codepoint(name)].pack("U")
|
|
120
|
-
|
|
140
|
+
# Drops everything derived from a build. The dev watcher calls this after
|
|
141
|
+
# every rebuild, which is what keeps the caches above honest.
|
|
142
|
+
def reset!
|
|
143
|
+
@manifest = @build_error = @missing_icons = nil
|
|
144
|
+
@icon_cache = @sprite_markup = @sprite_stamp = nil
|
|
145
|
+
end
|
|
121
146
|
|
|
122
147
|
def configure = yield(self)
|
|
123
148
|
end
|
data/lib/tasks/fontico.rake
CHANGED
|
@@ -67,10 +67,13 @@ if Rake::Task.task_defined?("assets:clobber")
|
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
namespace :fontico do
|
|
70
|
+
# Only what lands in output_dir. A target given an explicit path in the
|
|
71
|
+
# manifest is pointing somewhere the app owns — a firmware include/ that is
|
|
72
|
+
# very likely committed — and deleting that is not precompile's business.
|
|
70
73
|
desc "Remove generated icon artifacts (icons.lock is kept: it is source)"
|
|
71
74
|
task :clobber do
|
|
72
75
|
require "fontico"
|
|
73
|
-
%w[icons.svg icons.css icons.ttf].each do |name|
|
|
76
|
+
%w[icons.svg icons.css icons.ttf icons.h icons_font.h].each do |name|
|
|
74
77
|
path = File.join(Fontico.root, Fontico.output_dir, name)
|
|
75
78
|
File.delete(path) if File.exist?(path)
|
|
76
79
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fontico
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- nofxx
|
|
@@ -86,6 +86,8 @@ files:
|
|
|
86
86
|
- lib/fontico.rb
|
|
87
87
|
- lib/fontico/builder.rb
|
|
88
88
|
- lib/fontico/emitters/font.rb
|
|
89
|
+
- lib/fontico/emitters/gfx_font.rb
|
|
90
|
+
- lib/fontico/emitters/header.rb
|
|
89
91
|
- lib/fontico/emitters/sprite.rb
|
|
90
92
|
- lib/fontico/emitters/stylesheet.rb
|
|
91
93
|
- lib/fontico/helper.rb
|
|
@@ -95,6 +97,7 @@ files:
|
|
|
95
97
|
- lib/fontico/node/build_font.mjs
|
|
96
98
|
- lib/fontico/node/extract_glyphs.mjs
|
|
97
99
|
- lib/fontico/node/package.json
|
|
100
|
+
- lib/fontico/node/rasterize.mjs
|
|
98
101
|
- lib/fontico/node_runner.rb
|
|
99
102
|
- lib/fontico/outliner.rb
|
|
100
103
|
- lib/fontico/prawn.rb
|