breadkit-render 0.1.0 → 0.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/.yamllint +16 -0
- data/CHANGELOG.md +33 -3
- data/Dockerfile +21 -0
- data/Dockerfile.dockerignore +15 -0
- data/README.md +62 -40
- data/docs/REFERENCE.md +115 -0
- data/docs/images/led-netlist.png +0 -0
- data/docs/images/led-schematic.png +0 -0
- data/lib/breadkit/render/apng_encoder.rb +79 -0
- data/lib/breadkit/render/cli.rb +547 -22
- data/lib/breadkit/render/netlist_renderer.rb +138 -0
- data/lib/breadkit/render/orthogonal_router.rb +137 -0
- data/lib/breadkit/render/rasterizer.rb +138 -18
- data/lib/breadkit/render/schematic_renderer.rb +245 -0
- data/lib/breadkit/render/state_selection.rb +23 -0
- data/lib/breadkit/render/svg_renderer.rb +604 -101
- data/lib/breadkit/render/svg_template.rb +84 -0
- data/lib/breadkit/render/theme.rb +46 -0
- data/lib/breadkit/render/version.rb +1 -1
- data/lib/breadkit/render.rb +7 -0
- data/scripts/check-contrast.mjs +28 -0
- data/sig/breadkit/render.rbs +25 -2
- data/site/images/sensor-demo.png +0 -0
- data/site/images/sensor-demo.svg +8 -8
- data/site/index.html +5 -3
- metadata +36 -10
- data/docs/images/sensor-demo.png +0 -0
- data/docs/images/sensor-demo.svg +0 -45
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1e7769bd0125dce1d2f251ecf06b67829dbf9b6759f5bdacad4b9a8355d6f8c1
|
|
4
|
+
data.tar.gz: 5c5e45131260ac0d96b4eb05cfedeced949232656944d8646c6ef136338f7c49
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 275e5ac4dcda42f55d7ebab5ee917abeda204022448bad366c87299d4fe82dae81db31d4fe6542a93307762fd0fe6442b311fedc8e4b404518dcaf7e7351f09f
|
|
7
|
+
data.tar.gz: d752e27594c7ee9f09ed823be0d0766c0246d2b68d0e7f16affd57a3791d59c346dbb6805d3d73b7b9bfdb40532b0b40bef7560f45138191c57dc4e6db37d228
|
data/.yamllint
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.2.0 — 2026-09-28
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
### Breadboard diagrams
|
|
6
6
|
|
|
7
|
-
-
|
|
7
|
+
- Draw custom left and right rail strips vertically and center rails horizontally, including named boards; show rail-mounted parts as logical edge pin maps at their declared holes.
|
|
8
|
+
- Show DIP functional pin names in SVG hover titles and optional printed legends; draw TO-92 transistors, trimmer potentiometers, model-specific seven-segment displays, and RGB LEDs.
|
|
9
|
+
- Render safe custom SVG part bodies from `render.svg` fragments in part YAML.
|
|
10
|
+
- Add optional right-angle routes around component bodies, flat jumper styling, a colorblind palette, validated custom palettes, and embedded fonts. `--color-by net` overrides declared wire colors.
|
|
11
|
+
- Highlight the exact routes reported by short-circuit annotations; mark on-board pins and offboard module targets in lint annotations, and outline large nets instead of circling every hole.
|
|
12
|
+
- Reduce SVG hole markup with reusable shapes and show the hole ID and net on occupied or connected hole hover.
|
|
13
|
+
- Add `--focus` and `--highlight-net` to emphasize selected components and nets.
|
|
14
|
+
- Fall back to visible colors for invalid wire and LED colors.
|
|
15
|
+
|
|
16
|
+
### Viewers and assembly guides
|
|
17
|
+
|
|
18
|
+
- Add a standalone HTML viewer with zoom, pan, layer controls, and net hover. Resolve named switch states directly and reject viewers that exceed the 256-state budget.
|
|
19
|
+
- Add `--state` and `--layer` for selected switch states and static layer output; hide markers and net labels from omitted layers.
|
|
20
|
+
- Add `--diff OLD NEW` with colored added and removed wires in a two-panel HTML viewer.
|
|
21
|
+
- Export printable assembly guides with bills of materials and staged diagrams, excluding nonphysical annotation wires.
|
|
22
|
+
- Reload watched HTML after successful changes.
|
|
23
|
+
|
|
24
|
+
### Image export
|
|
25
|
+
|
|
26
|
+
- Add optional headless Chrome PNG rendering with checked output dimensions, an optional `resvg` PNG backend, and `--render-timeout` for external raster conversion.
|
|
27
|
+
- Export APNG animations from assembly steps or switch states with configurable frame timing; reject switch animations above the 256-frame budget.
|
|
28
|
+
- Provide a source-built container image with librsvg and Noto fonts.
|
|
29
|
+
|
|
30
|
+
### Compatibility and diagnostics
|
|
31
|
+
|
|
32
|
+
- Require Breadkit 0.2.x.
|
|
33
|
+
- Show all circuit diagnostics on stderr, including warnings and errors when `--force` is used.
|
|
34
|
+
|
|
35
|
+
## 0.1.0 — 2026-09-26
|
|
36
|
+
|
|
37
|
+
- Initial release.
|
data/Dockerfile
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
FROM ruby:4.0-slim-bookworm
|
|
2
|
+
|
|
3
|
+
RUN apt-get update \
|
|
4
|
+
&& apt-get install -y --no-install-recommends build-essential ca-certificates git librsvg2-bin fonts-noto-cjk fontconfig \
|
|
5
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
6
|
+
|
|
7
|
+
COPY breadkit/ /opt/breadkit/
|
|
8
|
+
COPY breadkit-render/ /opt/breadkit-render/
|
|
9
|
+
WORKDIR /opt/breadkit-render
|
|
10
|
+
|
|
11
|
+
RUN printf "source 'https://rubygems.org'\ngem 'breadkit', path: '/opt/breadkit'\ngem 'breadkit-render', path: '/opt/breadkit-render'\n" > Gemfile.container \
|
|
12
|
+
&& BUNDLE_GEMFILE=/opt/breadkit-render/Gemfile.container bundle install \
|
|
13
|
+
&& BUNDLE_GEMFILE=/opt/breadkit-render/Gemfile.container bundle exec ruby exe/bkrender --version \
|
|
14
|
+
&& fc-match 'Noto Sans CJK JP' \
|
|
15
|
+
&& printf 'board :mini\n' > /tmp/smoke.bk.rb \
|
|
16
|
+
&& BUNDLE_GEMFILE=/opt/breadkit-render/Gemfile.container bundle exec ruby exe/bkrender /tmp/smoke.bk.rb -o /tmp/smoke.png \
|
|
17
|
+
&& ruby -e 'abort "PNG smoke check failed" unless File.binread("/tmp/smoke.png").start_with?("\x89PNG".b)'
|
|
18
|
+
|
|
19
|
+
ENV BUNDLE_GEMFILE=/opt/breadkit-render/Gemfile.container
|
|
20
|
+
WORKDIR /work
|
|
21
|
+
ENTRYPOINT ["bundle", "exec", "ruby", "/opt/breadkit-render/exe/bkrender"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
**
|
|
2
|
+
!breadkit/
|
|
3
|
+
!breadkit/breadkit.gemspec
|
|
4
|
+
!breadkit/lib/
|
|
5
|
+
!breadkit/lib/**
|
|
6
|
+
!breadkit/data/
|
|
7
|
+
!breadkit/data/**
|
|
8
|
+
!breadkit/schema/
|
|
9
|
+
!breadkit/schema/**
|
|
10
|
+
!breadkit-render/
|
|
11
|
+
!breadkit-render/breadkit-render.gemspec
|
|
12
|
+
!breadkit-render/lib/
|
|
13
|
+
!breadkit-render/lib/**
|
|
14
|
+
!breadkit-render/exe/
|
|
15
|
+
!breadkit-render/exe/**
|
data/README.md
CHANGED
|
@@ -1,58 +1,80 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="site/favicon.svg" width="72" height="72" alt="">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">breadkit-render</h1>
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>Turn breadboard circuits into clear, shareable diagrams.</strong>
|
|
9
|
+
</p>
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://rubygems.org/gems/breadkit-render"><img src="https://img.shields.io/gem/v/breadkit-render.svg" alt="RubyGems version"></a>
|
|
13
|
+
<a href="https://github.com/breadkit/breadkit-render/actions/workflows/ci.yml"><img src="https://github.com/breadkit/breadkit-render/actions/workflows/ci.yml/badge.svg" alt="CI status"></a>
|
|
14
|
+
<img src="https://img.shields.io/badge/Ruby-%3E%3D%203.3-CC342D.svg" alt="Ruby 3.3 or newer">
|
|
15
|
+
<a href="LICENSE.txt"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT license"></a>
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
`bkrender` turns [Breadkit](https://github.com/breadkit/breadkit) circuits into
|
|
19
|
+
breadboard, schematic, and netlist views. Export SVG, HTML, images, PDF, or
|
|
20
|
+
animated assembly steps.
|
|
21
|
+
|
|
22
|
+
<p align="center">
|
|
23
|
+
<img src="site/images/sensor-demo.png" width="800" alt="RP2040 sensor circuit on a full-size breadboard with OLED, two SHT31 modules, switches, and IR modules">
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
<p align="center">
|
|
27
|
+
<a href="https://breadkit.github.io/breadkit-render/images/sensor-demo.svg">Open the interactive sensor demo</a>
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
Install with Ruby 3.3 or newer:
|
|
8
33
|
|
|
9
34
|
```sh
|
|
10
|
-
|
|
11
|
-
bkrender circuit.bk.rb -o circuit.
|
|
12
|
-
bkrender circuit.bk.rb -o circuit.svg --orientation landscape
|
|
13
|
-
bkrender circuit.bk.rb -o circuit.svg --rail-pattern '+--+'
|
|
14
|
-
bkrender circuit.bk.rb --format svg > circuit.svg
|
|
35
|
+
gem install breadkit-render
|
|
36
|
+
bkrender circuit.bk.rb --theme dark -o circuit.svg
|
|
15
37
|
```
|
|
16
38
|
|
|
17
|
-
|
|
39
|
+
This README follows main (0.2.0), which requires Breadkit core 0.2.x. If the
|
|
40
|
+
published gems are older, use sibling source checkouts:
|
|
18
41
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
42
|
+
```sh
|
|
43
|
+
git clone https://github.com/breadkit/breadkit.git
|
|
44
|
+
git clone https://github.com/breadkit/breadkit-render.git
|
|
45
|
+
cd breadkit-render
|
|
46
|
+
bundle install
|
|
47
|
+
bundle exec ruby exe/bkrender ../breadkit/examples/01_led_button.bk.rb -o circuit.svg
|
|
48
|
+
```
|
|
22
49
|
|
|
23
|
-
|
|
50
|
+
SVG works without a conversion backend. PNG, JPEG, WebP, PDF, and APNG need a
|
|
51
|
+
supported backend; see the [format and backend reference](docs/REFERENCE.md#raster-backends).
|
|
24
52
|
|
|
25
|
-
|
|
53
|
+
## Choose a view
|
|
26
54
|
|
|
27
|
-
|
|
55
|
+
```sh
|
|
56
|
+
bkrender circuit.bk.rb --view schematic -o schematic.svg
|
|
57
|
+
bkrender circuit.bk.rb --view netlist -o netlist.svg
|
|
58
|
+
bkrender circuit.bk.rb -o circuit.html
|
|
59
|
+
```
|
|
28
60
|
|
|
29
|
-
|
|
61
|
+
The default breadboard view shows physical placement. The schematic groups
|
|
62
|
+
pins by electrical connection; the netlist lists resolved nets. HTML adds
|
|
63
|
+
interactive inspection. See the [full command reference](docs/REFERENCE.md)
|
|
64
|
+
for layers, rail patterns, themes, annotations, assembly guides, animations,
|
|
65
|
+
and output options.
|
|
30
66
|
|
|
31
|
-
|
|
67
|
+
The [sensor demo source](https://github.com/breadkit/breadkit/blob/main/examples/05_sensor_demo.bk.rb)
|
|
68
|
+
uses an RP2040, OLED, two SHT31 modules, switches, and IR modules. Its 5 V
|
|
69
|
+
emitter layer is an alternative: disconnect the emitter's 3.3 V wire before
|
|
70
|
+
using it. Keep the receiver at 3.3 V.
|
|
32
71
|
|
|
33
|
-
|
|
34
|
-
| --- | --- | --- |
|
|
35
|
-
| `-o, --output PATH` | stdout | Write to a file; `.svg`, `.png`, `.jpg`, and `.jpeg` select the format. |
|
|
36
|
-
| `-f, --format FORMAT` | inferred or `svg` | `svg`, `png`, or `jpeg`. Conflicting extensions are errors. |
|
|
37
|
-
| `--scale N` | `2` | Raster output scale. |
|
|
38
|
-
| `--theme NAME` | `light` | `light`, `dark`, or `print`. |
|
|
39
|
-
| `--orientation NAME` | `portrait` | `portrait` for a readable vertical board; `landscape` for a wide view. |
|
|
40
|
-
| `--rail-pattern PATTERN` | board layout | Assign `+`/`-` to the four rails in portrait order: left outer, left inner, right inner, right outer. Accepted patterns: `+--+`, `+-+-`, `-+-+`, `-++-`. Rail connections move with their assigned polarity. |
|
|
41
|
-
| `--color-by MODE` | `wire` | Use declared wire colors or deterministic net colors. |
|
|
42
|
-
| `--show-nets` | off | Add net labels to the diagram. |
|
|
43
|
-
| `--legend` | off | Add the title, connected net names, and representative wire colors. |
|
|
44
|
-
| `--crop MODE` | `auto` | `auto` crops to circuit content; `none` shows the full board. |
|
|
45
|
-
| `--annotations FILE` | none | Overlay offenses from `bklint --format json`. |
|
|
46
|
-
| `--backend NAME` | `auto` | Raster backend: `rsvg`, `vips`, or `magick`. |
|
|
47
|
-
| `--background COLOR` | white | JPEG background: basic CSS color name, `#RGB`, or `#RRGGBB`. PNG and SVG reject this option. |
|
|
48
|
-
| `--quality N` | `90` | JPEG quality. |
|
|
49
|
-
| `--static` | off | Omit SVG layer controls and embedded scripts. |
|
|
50
|
-
| `--force` | off | Draw resolved elements even when the input has layout errors. |
|
|
72
|
+
Ruby DSL files execute code; render only files you trust. JSON IR is data-only.
|
|
51
73
|
|
|
52
|
-
##
|
|
74
|
+
## Development
|
|
53
75
|
|
|
54
|
-
|
|
76
|
+
From the sibling checkout above, run `bundle exec rake` for the local checks.
|
|
55
77
|
|
|
56
|
-
|
|
78
|
+
## License
|
|
57
79
|
|
|
58
|
-
|
|
80
|
+
[MIT](LICENSE.txt).
|
data/docs/REFERENCE.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# breadkit-render reference
|
|
2
|
+
|
|
3
|
+
Start with the [README](../README.md) for installation and the first SVG.
|
|
4
|
+
Run `bkrender --help` for the complete option list.
|
|
5
|
+
|
|
6
|
+
## Views and formats
|
|
7
|
+
|
|
8
|
+
The default breadboard view shows physical placement and jumper routes.
|
|
9
|
+
`--view schematic` draws components and their resolved net connections;
|
|
10
|
+
`--view netlist` groups terminals by net. These electrical views do not show
|
|
11
|
+
physical jumper positions. They accept SVG, PNG, JPEG, WebP, and PDF, but not
|
|
12
|
+
HTML or breadboard-only controls.
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<img src="images/led-schematic.png" width="700" alt="Schematic showing a USB supply, switch, resistor, and LED connected by named nets">
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<img src="images/led-netlist.png" width="700" alt="Netlist view grouping component terminals by resolved net">
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
Output format follows the extension passed to `-o`. SVG requires no external
|
|
23
|
+
converter; `--format svg` also writes to standard output. Breadboard HTML
|
|
24
|
+
pairs the breadboard and schematic with zoom, pan, layer controls, net
|
|
25
|
+
highlighting, and switch-state selection.
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
bkrender circuit.bk.rb --theme dark -o circuit.svg
|
|
29
|
+
bkrender circuit.bk.rb -o circuit.html
|
|
30
|
+
bkrender circuit.bk.rb --view schematic -o schematic.svg
|
|
31
|
+
bkrender circuit.bk.rb --view netlist -o netlist.svg
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Breadboard controls
|
|
35
|
+
|
|
36
|
+
| Option | Effect |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| `--rail-pattern '+--+'` | Set left outer, left inner, right inner, and right outer rail polarities. Also accepts `+-+-`, `-+-+`, and `-++-`. |
|
|
39
|
+
| `--orientation landscape`, `--crop none` | Arrange named boards side by side or show the complete board. |
|
|
40
|
+
| `--theme dark`, `--theme colorblind` | Choose a built-in palette; `colorblind` is breadboard-only. `--color-by net` colors wires by resolved net. |
|
|
41
|
+
| `--label-density compact`, `--legend` | Reduce body labels or print a pin legend. |
|
|
42
|
+
| `--wire-routing auto`, `--wire-style flat` | Route on-board wires around bodies or use thin jumper lines. |
|
|
43
|
+
| `--state SW1`, `--layer "2 I2C"` | Show selected switch connectivity or one named visual layer. |
|
|
44
|
+
| `--focus R1`, `--highlight-net VCC` | Emphasize a part or a resolved net. |
|
|
45
|
+
| `--annotations lint.json` | Draw targets from `bklint --format json`. |
|
|
46
|
+
| `--diff old.bk.rb new.bk.rb -o changes.html` | Compare added and removed wires in an HTML viewer. |
|
|
47
|
+
|
|
48
|
+
`--rail-pattern` applies to one unnamed breadboard; named-board circuits
|
|
49
|
+
use each board's own rail definition. A circuit's `layer:` values group
|
|
50
|
+
components and wires for SVG and HTML. `--watch -o diagram.html circuit.bk.rb`
|
|
51
|
+
updates output when the input or a nearby part file changes, and reloads an
|
|
52
|
+
open HTML viewer after a successful render.
|
|
53
|
+
|
|
54
|
+
A custom JSON palette inherits a built-in theme:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{"base":"dark","colors":{"board":"#17251f","text":"#f3f8f4"}}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Pass it with `--theme-file palette.json` for breadboard output. Values are
|
|
61
|
+
six-digit hexadecimal colors. `--font-file font.woff2` embeds a licensed font
|
|
62
|
+
in a standalone SVG.
|
|
63
|
+
|
|
64
|
+
## Assembly and print output
|
|
65
|
+
|
|
66
|
+
Numbered `step` blocks in the [Breadkit DSL](https://github.com/breadkit/breadkit/blob/main/docs/dsl.md#assembly-steps)
|
|
67
|
+
mark assembly stages. Declarations outside steps appear in every stage.
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
bkrender circuit.bk.rb --step 1 -o step-1.svg
|
|
71
|
+
bkrender circuit.bk.rb --assembly-guide -o guide.html
|
|
72
|
+
bkrender circuit.bk.rb --animate steps --frame-delay 800 -o assembly.apng
|
|
73
|
+
bkrender circuit.bk.rb --print-template -o template.pdf
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
An assembly guide needs at least one step. APNG needs at least two frames;
|
|
77
|
+
`--animate states` uses switch states and rejects more than 256 frames.
|
|
78
|
+
`--print-template` uses 2.54 mm hole spacing and must be printed at 100%;
|
|
79
|
+
it requires `rsvg-convert`.
|
|
80
|
+
|
|
81
|
+
## Custom part bodies
|
|
82
|
+
|
|
83
|
+
A part definition can draw a small SVG fragment around its placed pins:
|
|
84
|
+
|
|
85
|
+
```yaml
|
|
86
|
+
render:
|
|
87
|
+
shape: generic
|
|
88
|
+
fill: "#304050"
|
|
89
|
+
svg: '<circle cx="0" cy="0" r="5" fill="{{fill}}"/>'
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The available variables are `{{ref}}`, `{{value}}`, `{{fill}}`,
|
|
93
|
+
`{{stroke}}`, and `{{text_color}}`. Basic SVG shapes, paths, groups, and
|
|
94
|
+
text are allowed; scripts, event handlers, URLs, styles, and external
|
|
95
|
+
references are rejected. Physical leads and pins remain visible.
|
|
96
|
+
|
|
97
|
+
## Raster backends
|
|
98
|
+
|
|
99
|
+
PNG and APNG use the first available backend: `rsvg-convert`, `resvg`,
|
|
100
|
+
`ruby-vips`, ImageMagick, then Chrome. JPEG and WebP use `ruby-vips` or
|
|
101
|
+
ImageMagick. PDF requires `rsvg-convert` and retains vector content. Install
|
|
102
|
+
librsvg with `brew install librsvg` or `apt install librsvg2-bin` for PDF.
|
|
103
|
+
Choose a backend with `--backend NAME`; `--render-timeout SECONDS` limits
|
|
104
|
+
external conversion. `--scale` sets raster size.
|
|
105
|
+
|
|
106
|
+
The [container image](https://github.com/breadkit/breadkit-render/pkgs/container/breadkit-render)
|
|
107
|
+
includes Ruby, Breadkit core, librsvg, and Noto fonts:
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
docker run --rm -v "$PWD:/work" ghcr.io/breadkit/breadkit-render:main \
|
|
111
|
+
circuit.bk.rb --theme dark -o circuit.png
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Pin a `sha-<render commit>` image tag for reproducible output. Ruby DSL
|
|
115
|
+
files execute code; render only files you trust. JSON IR is data-only.
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
|
|
5
|
+
module Breadkit
|
|
6
|
+
module Render
|
|
7
|
+
class ApngEncoder
|
|
8
|
+
SIGNATURE = "\x89PNG\r\n\x1a\n".b.freeze
|
|
9
|
+
|
|
10
|
+
def encode(images, delay_ms: 800)
|
|
11
|
+
raise Error, "APNG requires at least two frames" unless images.length >= 2
|
|
12
|
+
raise Error, "frame delay must be 1..65535 milliseconds" unless delay_ms.is_a?(Integer) && delay_ms.between?(1, 65_535)
|
|
13
|
+
|
|
14
|
+
frames = images.map { |image| parse_png(image) }
|
|
15
|
+
header, prelude = frames.first.values_at(:header, :prelude)
|
|
16
|
+
unless frames.all? { |frame| frame[:header] == header && frame[:palette] == frames.first[:palette] }
|
|
17
|
+
raise Error, "APNG frames need matching dimensions and color format"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
width, height = header.unpack("N2")
|
|
21
|
+
output = SIGNATURE + png_chunk("IHDR", header) + prelude.join + png_chunk("acTL", [frames.length, 0].pack("N2"))
|
|
22
|
+
sequence = 0
|
|
23
|
+
frames.each_with_index do |frame, index|
|
|
24
|
+
control = [sequence, width, height, 0, 0, delay_ms, 1000, 0, 0].pack("N5n2C2")
|
|
25
|
+
output << png_chunk("fcTL", control)
|
|
26
|
+
sequence += 1
|
|
27
|
+
frame[:data].each do |data|
|
|
28
|
+
output << if index.zero?
|
|
29
|
+
png_chunk("IDAT", data)
|
|
30
|
+
else
|
|
31
|
+
chunk = png_chunk("fdAT", [sequence].pack("N") + data)
|
|
32
|
+
sequence += 1
|
|
33
|
+
chunk
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
output << png_chunk("IEND", "".b)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def parse_png(image)
|
|
43
|
+
raise Error, "invalid PNG frame" unless image.start_with?(SIGNATURE)
|
|
44
|
+
|
|
45
|
+
position = SIGNATURE.bytesize
|
|
46
|
+
chunks = []
|
|
47
|
+
while position + 12 <= image.bytesize
|
|
48
|
+
length = image.byteslice(position, 4).unpack1("N")
|
|
49
|
+
finish = position + length + 12
|
|
50
|
+
raise Error, "invalid PNG frame" if finish > image.bytesize
|
|
51
|
+
|
|
52
|
+
type = image.byteslice(position + 4, 4)
|
|
53
|
+
data = image.byteslice(position + 8, length)
|
|
54
|
+
crc = image.byteslice(finish - 4, 4).unpack1("N")
|
|
55
|
+
raise Error, "invalid PNG frame checksum" unless crc == Zlib.crc32(type + data)
|
|
56
|
+
|
|
57
|
+
chunks << [type, data]
|
|
58
|
+
position = finish
|
|
59
|
+
break if type == "IEND"
|
|
60
|
+
end
|
|
61
|
+
raise Error, "invalid PNG frame" unless position == image.bytesize && chunks.first&.first == "IHDR" &&
|
|
62
|
+
chunks.first.last.bytesize == 13 && chunks.last&.first == "IEND"
|
|
63
|
+
|
|
64
|
+
first_data = chunks.index { |type, _| type == "IDAT" }
|
|
65
|
+
raise Error, "PNG frame has no image data" unless first_data
|
|
66
|
+
prelude = chunks[1...first_data]
|
|
67
|
+
raise Error, "already animated PNG frames are unsupported" if chunks.any? { |type, _| %w[acTL fcTL fdAT].include?(type) }
|
|
68
|
+
|
|
69
|
+
{ header: chunks.first.last, prelude: prelude.map { |type, data| png_chunk(type, data) },
|
|
70
|
+
palette: prelude.select { |type, _| %w[PLTE tRNS].include?(type) },
|
|
71
|
+
data: chunks.filter_map { |type, data| data if type == "IDAT" } }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def png_chunk(type, data)
|
|
75
|
+
[data.bytesize].pack("N") + type + data + [Zlib.crc32(type + data)].pack("N")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|