Commit Graph
216 Commits
Author SHA1 Message Date
dotCooCoo bb6cbfb2b4 Images: SVG and EPS export write colorized grayscale and 1-bit images in their fill color
SVG export ignored an image's colorize paint and wrote the image's own
black, white and gray pixels. EPS export flattened every colorized image
into a process-color raster, so a spot color lost its plate. Both now
write the image in the fill color, with the coverage the canvas and PDF
export use (darkness times alpha), so white and transparent pixels take
no color.

- SVG writes a coverage PNG as a mask over a rectangle in the color,
  shared between repeated images while each keeps its own color. Linked
  mode writes the coverage PNG as a file, and SVG Tiny 1.2 writes a PNG
  in the color. A colorized image whose pixels are missing, or whose
  grayscale pixels can't be decoded, is left out with a warning.
- EPS writes fully covered or uncovered pixels as an imagemask in the
  color's process or Separation ink (Registration on every plate), and a
  grayscale image as ink samples scaled by coverage. Level 3 masks its
  uncovered pixels out and Level 2 writes them white; gray midtones are
  written over white, with the existing warnings.
- EPS flattening no longer counts a solid colorize paint on a grayscale
  or 1-bit image as transparency by itself. Opacity, blending modes,
  opacity masks, effects and overlapping transparent art still flatten
  it, and so does a colorize paint the writer can't draw.

The svg crate's tests turn on resvg's raster-images feature to render
the embedded PNGs.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-11-01 00:35:27 +07:00
Shunsuke Yoshizawa aaeb1b2b35 Respect SVG image-rendering pixelated attribute 2026-10-31 20:44:09 +07:00
SusieeTheFemboyandClaude Opus 5.5 2e7b2b7f96 SVG import: an embedded SVG image gets the feConvolveMatrix order check too
Files::svg parsed nested SVG images with usvg without the check the top-level
import makes, so an overflowing order inside an <image> still reached usvg's multiply.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 19:43:17 +07:00
SusieeTheFemboyandGLM-5.3-Flash c7dc443ee1 SVG import: a feConvolveMatrix order that overflows usvg's multiply is refused
usvg 0.48 casts the filter's order attribute through i32, so a value like
1066570220609 saturates and the u32 order_x * order_y multiply overflows.
Only debug and overflow-checks builds panic on that; release builds already
import the file — the multiply wraps, and VectorCraft drops the filter as
an unsupported primitive with a warning, so nothing reaches the document,
render or export (observed on main in a --release build: both repro files
import without error; with a one-entry kernelMatrix the wrapped product
2147483647² mod 2³² = 1 even passes usvg's length check and leaves a filter
claiming a 2147483647×2147483647 kernel, still dropped downstream).

The import now parses order the way usvg does (svgtypes numbers, the same
as-i32 cast with its saturation, the first side defaulting the second, both
positive or both back to the 3×3 default) and refuses exactly the one case:
when the product of the two sides overflows u32 (checked_mul). Any order
usvg can multiply keeps importing as before, however large on its own, and
a missing, unparseable or non-positive order still falls back to 3.

Regression tests cover the overflow repros, the release wrap case, large
but fitting orders (100, 80 80, 65535 65535, 2147483647 1), the
pass-through shapes, and sane kernels, in debug and in release.

Co-Authored-By: GLM-5.3-Flash <noreply@z.ai>
2026-10-31 19:43:17 +07:00
Don Omondi 9259434a89 SVG export: a gradient mesh patch is cut no finer than its colour changes
Each patch of a gradient mesh was cut into 8 x 8 flat quads for SVG export,
Object > Expand and the other outputs that cannot shade a mesh, whatever its
colours, so a patch of one colour became 64 identical pieces. Neighbouring
quads are now joined into square blocks wherever the colour changes by no
more than about one 8-bit step across the block; the block's outline runs
through its quads' corners, and a patch that folds over itself is not joined.
2026-10-31 19:43:16 +07:00
Dylan M. Taylor a24d811c7b Update prompt shows the app icon and accent color instead of a letter (#4671)
UPDATE_APP (native builds; craft-update is a native-only dependency) carries our 128 px icon and accent (#e8573f, its colour in ArtCraft Launcher). The updater, the self-update
helper and craft-update's startup prompt all get them through crate::UPDATE_APP; the
prompt draws the icon, the accent and "by ArtCraft" (craft-update's default developer).

craft-update is pinned to storytold/craft-libs main at 1065653e7 (#434).
Clippy fixes for Rust 1.99 in untouched code (same logic):
- crates/svg/src/import/fx.rs
- crates/tools/src/text/tool.rs
2026-10-30 01:27:35 -04:00
bflatastic beb0ae6515 SVG export: rectangles, circles, ellipses, lines and straight paths as SVG shape elements (#4652) 2026-10-30 09:45:59 +07:00
bflatastic 7fd9313d06 SVG: the placeholder comment names the constant 2026-10-29 22:12:34 +07:00
el-j dc4150d7db perf(svg): compute unique ID prefix via single-pass placeholder replacement (#4555) 2026-10-29 22:12:21 +07:00
Ridzzz0Alam e110c6c770 Fix #3209: SVG export writes type sizes and spacing in CSS with px
With Styling set to Internal CSS, Inline Style or Style Entities, font-size
and letter-spacing were written as bare numbers. Only presentation attributes
take a bare number; in CSS a browser drops them, so the type was drawn at its
default size. They are now written in px, SVG's user unit, and presentation
attributes keep bare numbers.
2026-10-29 22:12:07 +07:00
Max MittelstädtandGPT-6-Luna 2b37302fe5 Preserve comment markers inside CSS strings
Found while testing crates/svg/src/import/css.rs:346

Co-Authored-By: GPT-6-Luna <noreply@openai.com>
2026-10-29 18:45:51 +07:00
dotCooCoo dfa9f883c0 Fix Drop Shadow Darkness in source color space 2026-10-29 14:36:41 +07:00
dotCooCoo ab69d8f6d9 Envelopes: Anti-Alias and Preserve Shape Using change how distorted images draw
Envelope Options stored Anti-Alias and Preserve Shape Using, but nothing that
draws an image read them: a distorted image was always smoothed and always
cut into clip groups.

- With Anti-Alias off, each piece of a distorted image is marked pixelated
  (saved as `pixelated`). The renderer draws it with nearest-neighbor
  sampling and without mips, SVG export writes `image-rendering: pixelated`,
  and PDF export writes it without /Interpolate.
- With Preserve Shape Using Transparency, each piece is the image with a
  white opacity mask in the piece's shape instead of a clip group. The
  renderer paints an image whose mask is one plain white path straight into
  that path, as it does for an image in a clip group, when the path lies
  inside the image's frame.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-29 11:07:57 +07:00
bflatastic cfe21231c8 SVG import: import NodeId and SvgError in importer.rs (#4299 made its imports explicit) 2026-10-29 09:57:33 +07:00
el-j 39341ef4c8 Fix #3564: report TooManyImages error when nested SVG image cap is reached 2026-10-29 09:57:33 +07:00
el-j b65fb23338 fix(svg): cap converted objects and nested images (#3564) 2026-10-29 09:57:33 +07:00
Dylan M. Taylor 7e44df55a9 Make renderer and SVG module dependencies explicit 2026-10-28 05:03:00 +00:00
NetterSilverandClaude Opus 5.5 19d4adb39c Make SVG export def ids linear instead of quadratic
Writer::fresh_id probed prefix-1, prefix-2, ... from 1 on every call,
formatting and hashing each candidate, so writing N gradients, clip
paths or masks cost O(N^2). Ids are never released, so every number
below the last one handed out for a prefix is taken: remember where
the search for each prefix stopped and resume there. The ids written
are the same as before.

examples/still-life.vectorcraft (12k gradients, 6.3 MB SVG) exported
in about 8.6 s (median); it now takes about 0.15 s. Output is byte-identical
for every document in examples/.

Add crates/engine/examples/export_bench.rs to time document.export
on the example documents, and a test pinning the generated id
sequence when object names already take some of the numbers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 11:52:22 +01:00
aiongg 412dbcd632 refactor(svg): move import.rs items to import/{convert,importer,layers,prepass}.rs 2026-10-26 22:01:25 +00:00
aiongg 7e52760174 refactor(svg): move export.rs items to export/{nodes,output,paint,text}.rs 2026-10-26 21:45:32 +00:00
aiongg 1101e04da5 refactor(svg): move import/css.rs tests to import/css/tests.rs 2026-10-26 18:18:18 +00:00
NetterSilverandClaude Haiku 5.5 29a98b750a Respect quotes and parentheses when splitting SVG CSS declarations
Found while testing crates/svg/src/import/css.rs:21 (parse_decls)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-26 13:43:20 +07:00
ham9zah e56aa356b5 Type on a path: joined scripts bend along the path so their joins stay connected
On a curved path each glyph was placed rigidly (turned to the tangent at
its middle), so its baseline ends stood off the curve and neighbours
turned by different angles: in scripts whose letters join, every join
opened into a notch. Measured with Arial on circles: at 29 pt on a
radius of 110 pt, 0.64 pt at the baseline and 1.24 pt at the height of
the joining stroke (a quarter of the size); 14.7 pt at 72 pt on a radius
of 60. Placing glyphs on the chord alone closes the baseline but leaves
about h (a_i + a_{i+1}) / 2R above it (0.71 pt here), so joined glyphs
are bent instead.

- Shaping marks each glyph of a segment whose script joins (Arabic,
  Syriac, N'Ko, Mongolian, Adlam and the other cursive scripts), by the
  segment's script, so a word bends as a whole.
- With the Rainbow effect such a glyph's outline is mapped point by
  point: pen space (x, y) -> P(s + x step/advance) + (y + rise) N there,
  each segment cut into pieces short enough for 0.01 pt (capped). A
  glyph the path doesn't turn under stays exactly as it was; Skew, 3D
  Ribbon, Stair Step and Gravity, vertical type, inline art and colour
  glyphs are unchanged. `PositionedGlyph::bend` records it; `xf` stays
  the straight glyph for the caret, hit testing and tools.
- PDF export draws bent glyphs as outlines and writes them again as
  invisible text where they stand, so they can be selected, searched
  and copied. SVG export outlines them (right-to-left type already was),
  never as a <textPath>, and labels the group with its text
  (role="img", aria-label) for screen readers and search.

Tests (crates/text/src/tests_path_joins.rs; installed Arabic font, skip
without one): the gap at the baseline and at join height is <= 0.01 pt
at 12, 29 and 72 pt on radii 60, 110 and 300, on both sides of the
path, with finite outlines; Latin on a curve and joined type on a
straight path are byte-for-byte what they were; the chord-only
prediction is checked.

vectorcraft-cli perf (load average 43 before, 34 after: not trustworthy;
the synthetic document has no type on a path, so bending doesn't run):
render fit 113.8 -> 132.7 ms, pan 115.5 -> 140.0 ms, 400% 32.9 -> 43.3
ms, hit test 1.36 -> 1.59 ms, save 81.5 -> 88.2 ms, open 306 -> 401 ms,
SVG 426 -> 439 ms, Pathfinder 93.7 -> 85.5 ms, handle drag 5.5 -> 6.7
ms, segment drag 5.1 -> 6.7 ms.
2026-10-26 09:09:47 +07:00
NetterSilverandClaude Haiku 5.5 a579ca7073 Require two hex digits in SVG href percent escapes
Found while testing crates/svg/src/import/files.rs:82 (percent_decoded)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 23:32:52 +07:00
dotCooCoo bc041e15ef Type: the Numbered List button numbers paragraphs 1, 2, 3
Squashed from #3960 onto main.
2026-10-24 19:02:08 +07:00
NetterSilverandClaude Haiku 5.5 f8ad2edad8 Match !important case-insensitively in SVG import CSS parser
Found while testing crates/svg/src/import/css.rs:25

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-24 17:19:59 +07:00
aiongg 8cc69ed45c CI: one test binary per crate, nextest, line tables only (#3855)
* CI: line tables only for workspace crates, no debug info for dependencies

* CI: one integration-test binary per crate

Each crate's tests/*.rs files become modules of tests/it/main.rs. Files that change
process-wide state (Color Settings, the font database, installed plug-ins, the working
directory), the fuzz sweeps and the test targets the Affinity corpus workflow names stay
their own binaries.

* Docs: name the integration tests' new paths

* CI: xtask test with nextest and doctests, one wasm check, clippy --keep-going

cargo xtask ci runs the tests with cargo nextest (cargo test when it is not installed),
builds them with --lib --bins --tests, runs the doctests as their own step, and checks
the wasm crates in one cargo check --keep-going call.
2026-10-23 21:36:10 -04:00
Jürg Lehni deb517fbbe Type: an SVG glyph's image is an SVG image by its plain href first
The check for images of SVG documents looked at href and xlink:href alike, so an image whose href is a PNG was cut for an SVG xlink:href; it now reads href, else xlink:href, as the importer does (SVG 2).
2026-10-24 07:12:16 +07:00
Jürg Lehni 4d1b4a3392 Type: SVG glyphs leave out images of SVG documents
The importer expanded an image whose data is an SVG document into art; the SVG table asks for it to be ignored, and a glyph draws no documents inside it.
2026-10-24 06:51:20 +07:00
Jürg Lehni caaccaf5be Type: images in SVG glyph documents draw with the glyph
They were left out because glyph art had no document to keep them in; since #3879 they hold their own pixels until a document takes them in.
2026-10-24 06:00:22 +07:00
BFlat e0c1548049 Merge pull request #3896 from storytold/fix/3869-svg-type-features
SVG export: type with small capitals, superscript, subscript or tab leaders is written as outlines (#3869)
2026-10-24 04:51:54 +07:00
aiongg 6877d6535f Links: resolve stored paths with craft-docpath's resolve_absolute (#3267)
* Links: use resolve_absolute for stored paths

* CI: release only with git dependencies from storytold

* Release: check git sources from the parsed Cargo.lock and cargo config

The grep let any character stand for the dot in github.com, passed a missing or stale
Cargo.lock, and missed source replacement in .cargo/config.
2026-10-23 17:51:28 -04:00
bflatastic 166afce360 SVG export: type with small capitals, superscript, subscript or tab leaders is written as outlines (#3869) 2026-10-24 04:49:08 +07:00
bflatastic c2ad9f8bbb Type: emoji go with type that effects reshape and Expand Appearance
Squashed from #3879 onto main.
2026-10-24 04:49:06 +07:00
bflatastic 9827bfbcbb SVG import: the shared appearance needs no conversion (clippy) 2026-10-24 03:52:24 +07:00
bflatastic d9e524df30 SVG entity scan: look for a reference's ';' no further than the longest name 2026-10-24 03:52:24 +07:00
Dylan M. Taylor 98d11ad66b Audit: cap SVG entity expansion, filter layer stacks and export id/companion probing; share compound-path appearances; escape-proof font feature tags 2026-10-24 03:52:24 +07:00
dotCooCoo 2a2e63856c Type: the Paragraph panel's Bullets button turns paragraphs into a bulleted list
VectorCraft had no lists. A bullet could only be typed as a character,
and the tab after it did not stop at the left indent. The reference app
turns paragraphs into a bulleted list from the Paragraph panel, the
context menu and Type › Bullets and Numbering › Apply Bullets.

text.setFormat takes list ("none" or "bullet") and bullet ("none",
"dot", "dash", "circle" or "square"). The layout draws the bullet before
the first line of each list paragraph that has text, in the font, size
and color of its first character, and starts the text at the next tab
stop, with the left indent counted as a stop. Wrapped lines start at
the left indent, and type on a path draws the bullet too. In a
horizontal right-to-left list paragraph the indents are mirrored: the
bullet hangs at the right, and the wrapped lines start at the right
where the first line's text starts after the bullet. The bullet is not
a character of the text. A paragraph whose left and first-line indents
are both 0 gets 18 pt and -18 pt when it joins a list. Return continues
the list, paragraph styles carry it, Reset Panel turns it off, and the
native file saves it only when it is set.

The Paragraph panel has a Bullets button with a list of the five
presets, and Type › Bullets and Numbering › Apply Bullets and the
context menu run type.applyBullets. Live SVG text writes the bullet's
character, and SVG writes type on a path with bullets as outlines. DXF
writes the bullet as a TEXT entity of its own, and legacy .ai export
warns that bulleted lists are left out. The caret, selections, Touch
Type and Show Hidden Characters skip the bullet.

Live SVG text of horizontally scaled type now writes each tab in a
tspan of its own. Before, the characters before a tab shared its tspan,
and its textLength made a viewer stretch them across the tab's gap.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-24 03:09:11 +07:00
bflatastic 422ecc0c77 Artboards: Artboard Options shows video safe areas and sets the pixel aspect ratio that View › Rulers › Show Video Rulers counts the artboard in
Squashed from #3831 onto main.
2026-10-24 03:09:07 +07:00
Jürg Lehni a26ac22794 SVG: type written as outlines stacks colour glyphs under their run's paint
As on the canvas: each run's bitmap, COLR and SVG glyphs come before the run's path, so a plain character set over a colour one stays on top.
2026-10-24 02:06:13 +07:00
Jürg Lehni 863a954cd2 Type: SVG glyph documents follow the SVG table's coordinate rules
A root size without a viewBox sets up the em, a viewBox that isn't square is centred in it, and a glyph element nested in others is drawn on its own, as if used.
2026-10-24 02:06:09 +07:00
Louise FoxandClaude Opus 5.5 8906abf30d SVG: keep other applications' attributes from import to export
Many tools note things on SVG objects in attributes of their own XML
namespace: a plotter's pen, a cutter's passes, a game engine's
collision flag. Import dropped them, so a file that went through
VectorCraft lost what other software had set on its objects. #522
asked for unknown and data-* attributes to survive a round trip; #580
kept the data-* ones, and this keeps the ones in other applications'
namespaces.

- Import keeps every attribute in a namespace that isn't SVG's,
  XLink's, XML's, Inkscape's, Sodipodi's or VectorCraft's own, on the
  object or layer it belongs to, in order, with its namespace URI and
  the prefix the file gave it (ObjectAttributes::foreign, beside the
  data-* attributes and the event handlers). The file is untrusted: at
  most 64 per object and 64 KiB per value, and what is left out is
  warned about. Files that declare no other namespace skip the scan.
- They are the object's own, as its data-* attributes are: a pasted
  copy and a duplicate keep them, and deleting the object takes them
  with it. The native file keeps them in the object's attributes, a
  key written only when an object has some.
- Export writes them back on the same elements and declares each
  namespace once, on the root, under the file's prefix. A prefix the
  export uses itself, or that another namespace already has, becomes
  prefix2, prefix3...; one XML keeps for itself (xml...) becomes ns. A
  name or value XML can't hold is left out with a warning.

Nothing in VectorCraft reads them, and nothing in the UI lists them.

svg/tests/foreign_attributes.rs: a layer's and a shape's attributes
survive import, export and import again, Inkscape's and Sodipodi's
own aside and with no warnings, and the native form round-trips;
prefixes are made unique, and bad names and values are left out with
a warning; the caps hold and are warned about once.
engine tests_attributes.rs: a pasted copy and a duplicate keep them, a
deleted object leaves none behind, and the native file and SVG export
keep them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhvnnnD5fTFupK8WWq35un
2026-10-23 10:56:00 +07:00
Jürg Lehni c28ad99678 Type: OpenType SVG colour fonts draw and export as vector art
Each glyph's SVG document is read by the SVG importer, which the engine registers with the text crate.
2026-10-23 09:07:54 +07:00
Jürg Lehni 9de4b40241 Type: COLR colour fonts draw and export as vector art
Glyphs whose paints documents have no form for (sweep gradients, compositing modes) are drawn as images of the glyph.
2026-10-23 05:14:47 +07:00
BFlat f44dd739b2 Merge pull request #3688 from addibble/perf/svg-import-css-data-urls
SVG import: leave out a data URL inside url(…) too, and jump over each one
2026-10-23 02:18:29 +07:00
Drew DibbleandClaude Opus 5.5 2d85603bcc SVG import: leave out the data URL of a url(…) in any attribute value too
A follow-up to #3677. A data URL in CSS, `style="fill:url(data:…)"` or a paint's
`url(…)`, stayed in the text the prepass searches, since only a value beginning with `data:`
was left out (CodeRabbit's review of #3677). Each `url(` in an element's quoted value whose URL
begins with `data:` is now left out up to the byte that closes it, its quote or `)`, so what
follows it, `display:none` say, stays. A `url(` that does not close inside its value is kept
whole: where the end is unclear, less is left out, never more.

The scan now jumps over a data URL with one search for the byte that ends it, instead of
stepping through it a byte at a time. The markers' capacity is a saturating sum, so no range can
panic it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 02:04:46 +07:00
Taha-Mahmoodi 2bad646821 Gradients: a Perceptual gradient mixes its colours in OKLab, and SVG and PDF write it as an image
Illustrator 30.0's gradient Method, from the public documentation
(https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-gradients/control-perceptual-interpolation-in-gradients.html):
Classic or Perceptual; perceptual interpolation matches colour transitions to
how the eye perceives differences between colours, and perceptual gradients
are rasterised when saved as PDF or SVG.

- Gradient gains `method` (Classic | Perceptual; serde default Classic,
  written only when Perceptual, so older files open as Classic and Classic
  files are written as before). The colour crate's OKLab helpers open up for it.
- Perceptual mixes neighbouring stop colours in OKLab (sample, and
  sample_keep_model for the stroke meshes, Expand and blends); opacity stays
  linear. Renderers that mix linearly get stops on the perceptual curve,
  placed by halving each half segment where a straight line strays more than
  0.4 of a colour level (a nearly straight segment needs none). Classic
  gradients expand exactly as before.
- draws_as_pixels includes Perceptual gradients, so SVG and PDF write a
  Perceptual fill or stroke as an image at the document's raster effects
  resolution, clipped to what it paints. A gradient along or across a stroke
  stays slices (PDF no longer draws it as a within-stroke image).
- With Dither: a dithered Perceptual fill is the perceptual mix with dither's
  noise (the canvas's dithered grids are keyed by method too); SVG and PDF
  write it as an image with the noise, and a dithered Perceptual stroke as an
  image without it, as the canvas draws it (GradientPaint::pixels, SVG's
  raster_paint and PDF's area are told whether they paint a fill).
2026-10-23 01:53:29 +07:00
bflatastic 997dd8d2bd SVG import: images in images share the depth cap, SVGZ images one unpacking cap (#3562, #3563)
Squashed from #3676 onto main.
2026-10-23 01:28:52 +07:00
Drew DibbleandClaude Fable 5.1 5b919c0529 SVG import: read the text once for the prepass's markers, with the data URLs left out
The prepass decided what to look for in the tree by a dozen substring searches over the whole
SVG text (`:not(`, the event attributes, `script`, `transform:` and nine more), and a separate
pass counted the elements' depth before roxmltree. With an embedded picture the text is mostly
its data URL: a 1.35 MB card SVG (a 968 KB PNG) paid about 7 ms of searches and 1 ms of depth
scan per import, on a 64 ms render, and a 54-page PDF export 54 times that.

`depth::prescan` is the one pass: it counts the depth as `xml_too_deep` did (the DOCTYPE entity
count included) and, in the same walk, notes every `data:` attribute value, answering the text
with those values left out, their quotes kept. The prepass and `fx::blends` search that text;
the edits are still made in the source. A data URL can hold no tag, attribute name, selector or
CSS declaration, and everything else, a `style="display:none"` included, is kept, so what is
found is what was found before.

`vectorcraft-cli perf --deep` gains a tracked row, "import SVG, one embedded PNG", beside the
export row it had.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-23 01:01:27 +07:00
bflatastic 26b9cf84ad Type: colour emoji fonts with bitmap glyphs draw and export
Squashed from #3561 onto main.
2026-10-23 00:44:31 +07:00