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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b3d7b06780f100a70e85853569136f7123912ce7708629d9d797f22190396c4f
4
- data.tar.gz: beadb4b786adbe2b8250f0bc1a9fae04eab46eed4df6e011c0809cc5259070c3
3
+ metadata.gz: 1e7769bd0125dce1d2f251ecf06b67829dbf9b6759f5bdacad4b9a8355d6f8c1
4
+ data.tar.gz: 5c5e45131260ac0d96b4eb05cfedeced949232656944d8646c6ef136338f7c49
5
5
  SHA512:
6
- metadata.gz: e69e57aec01703ba0d6bdf4fe1c74228ee1b68f2c4dfb92f8e4c063e7294f31a1c423696710e43619bbf4ad6bd16476108bba24edc7657808f04b14783459d53
7
- data.tar.gz: 8faa7a544c38cbe3ac009b82588d2eb9357810eccd891c27dcbd4a396a3d3a6de116f795dee9a257fdcba78b7b14426047a2fd8d21a15ba05ed9d803047ad2f8
6
+ metadata.gz: 275e5ac4dcda42f55d7ebab5ee917abeda204022448bad366c87299d4fe82dae81db31d4fe6542a93307762fd0fe6442b311fedc8e4b404518dcaf7e7351f09f
7
+ data.tar.gz: d752e27594c7ee9f09ed823be0d0766c0246d2b68d0e7f16affd57a3791d59c346dbb6805d3d73b7b9bfdb40532b0b40bef7560f45138191c57dc4e6db37d228
data/.yamllint ADDED
@@ -0,0 +1,16 @@
1
+ extends: default
2
+ ignore: |
3
+ build/
4
+ node_modules/
5
+ runtime/build/
6
+ tmp/
7
+ **/*.dSYM/**
8
+
9
+ rules:
10
+ comments:
11
+ min-spaces-from-content: 1
12
+ document-start: disable
13
+ line-length:
14
+ max: 200
15
+ truthy:
16
+ check-keys: false
data/CHANGELOG.md CHANGED
@@ -1,7 +1,37 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.2.0 — 2026-09-28
4
4
 
5
- ## 0.1.0 — 2026-09-27
5
+ ### Breadboard diagrams
6
6
 
7
- - Initial release.
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
- # breadkit-render
1
+ <p align="center">
2
+ <img src="site/favicon.svg" width="72" height="72" alt="">
3
+ </p>
2
4
 
3
- Project site: https://breadkit.github.io/breadkit-render/
5
+ <h1 align="center">breadkit-render</h1>
4
6
 
5
- `bkrender` turns a Breadkit DSL or resolved IR JSON file into a standalone SVG diagram. Breadkit DSL input is executable Ruby; only render files you trust. IR JSON is the data-only alternative.
7
+ <p align="center">
8
+ <strong>Turn breadboard circuits into clear, shareable diagrams.</strong>
9
+ </p>
6
10
 
7
- Install `breadkit-render` directly; RubyGems installs its compatible `breadkit` core dependency. Shared circuit examples are in the [breadkit repository](https://github.com/breadkit/breadkit/tree/main/examples).
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
- bkrender circuit.bk.rb -o circuit.svg --show-nets --legend
11
- bkrender circuit.bk.rb -o circuit.png --scale 3 --theme light
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
- ## Example output
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
- Generated from the shared [RP2040 sensor demo](https://github.com/breadkit/breadkit/blob/main/examples/05_sensor_demo.bk.rb). It uses the full-size 830-point board model. The image crops to the used area: the controller occupies rows 1–20, with switches and IR connections below and OLED/SHT31 modules beside it.
20
-
21
- Rendered with `bkrender ../breadkit/examples/05_sensor_demo.bk.rb -o docs/images/sensor-demo.png --crop auto --rail-pattern '+--+' --scale 2`:
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
- <img src="docs/images/sensor-demo.png" alt="RP2040 sensor demo on a full-size breadboard with OLED, two SHT31 modules, two switches, and IR receiver and emitter wiring" width="800">
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
- [Open the interactive SVG](docs/images/sensor-demo.svg). Its layer tabs isolate power, I2C, switches, IR, and the optional 5 V emitter wiring. The 5 V view is a dashed, visual-only alternative: remove the emitter's 3.3 V rail wire before applying it.
53
+ ## Choose a view
26
54
 
27
- The shared example defines the board and IR connectors in the breadkit repository's [`examples/parts`](https://github.com/breadkit/breadkit/tree/main/examples/parts). Pin `type:` values such as `power`, `ground`, `clock`, `data`, `address`, and `interrupt` color the markers. `offboard` modules use a neutral surface color; connected pin names are emphasized, unused pins are muted, and `address:` text is shown on the module. Assign `layer:` to wires and components to create matching interactive views. `route: :edge` routes long wires outside the board and separates wires that would otherwise overlap.
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
- Resistors use four color bands by default. Set `bands: 5` on a resistor for three significant digits and a 1% brown tolerance band.
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
- ## Options
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
- | Option | Default | Description |
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
- ## Raster backends
74
+ ## Development
53
75
 
54
- SVG is rendered without extra dependencies. PNG uses the first available backend in the order `rsvg-convert`, `ruby-vips`, ImageMagick. JPEG uses `ruby-vips` or ImageMagick and is flattened onto the configured background. Install `librsvg` (`brew install librsvg` or `apt install librsvg2-bin`) or ImageMagick for raster output. `ruby-vips` also needs a libvips build with SVG support.
76
+ From the sibling checkout above, run `bundle exec rake` for the local checks.
55
77
 
56
- Connected breadboard holes and physically occupied holes use different markers. LED colors accept CSS color names and hexadecimal colors such as `#6d5af0`.
78
+ ## License
57
79
 
58
- The SVG uses a system font stack including Noto Sans JP. Rasterized Japanese labels need a Japanese font installed in the environment.
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