Replace RMagick rendering with a pure-Ruby indexed PNG encoder - #1
Open
epistrephein wants to merge 1 commit into
Open
Replace RMagick rendering with a pure-Ruby indexed PNG encoder#1epistrephein wants to merge 1 commit into
epistrephein wants to merge 1 commit into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR removes the RMagick gem dependency and use pure-Ruby PNG generation, improving performance by a lot. Produced with Fable.
Problem
Generating the default 4020×4020 image took ~1.33s, almost entirely spent in the rendering pipeline. Profiling broke the time down as:
draw!— 256 separateMagick::Drawrectangle passespicture.write— ImageMagick PNG encodeThe PNG encode has a ~650ms floor caused by ImageMagick's per-pixel palette scan over 16M pixels; no combination of encoder options (compression level, filter, strategy, explicit color-type) gets below it.
Approach
The image is a known quantity: at most a handful of flat colors on a regular grid. Rasterizing 16M pixels and then re-scanning them to discover a palette is unnecessary — the indexed PNG can be emitted directly from
sequenceand the grid geometry using only stdlibZlib. Onlyrows + 1distinct scanline patterns exist (the background row plus one per grid row), so each is built once and shared across all pixel rows.Two details keep the output identical to the RMagick renderer:
Draw#rectanglefills its coordinates inclusively, so the square side is((size - gap) * multiplier).round + 1.Output for the default seed is byte-for-byte pixel-identical to the previous renderer (
magick compare -metric AE= 0). The one behavioral difference: custom geometries whose pixel coordinates land on fractions get crisp rounded edges instead of ImageMagick's anti-aliasing (defaults and theclassicgeometry are unaffected).Results
Deflate level 9 proved both faster and ~2.6× smaller than level 6 on this data, so
Zlib::BEST_COMPRESSIONis used.Changes
lib/png.rb(new) —Squarecraft::Png, a minimal indexed-color PNG writer: signature + IHDR/PLTE/IDAT/IEND chunks, bit depth 4 for ≤16 palette colors (8 for up to 256), one deflate call over filter-0 scanlines. Repeated scanline Strings are packed once via acompare_by_identitymemo.lib/generator.rb—draw!now builds palette-index scanlines fromsequence+ the existingcoordsmath and returns a PNG blob (String) instead of aMagick::Image;require "rmagick"dropped.bin/squarecraft,bin/examples— write the blob withFile.binwriteinstead ofMagick::Image#write.Gemfile/Gemfile.lock— RMagick dependency removed; the project is now pure Ruby..github/workflows/main.yml— ImageMagick apt install step removed (it existed only for RMagick's native extension).spec/generator_spec.rb—pictureexpectations updated to check the PNG signature and the IHDR dimensions.README.md— ImageMagick/RMagick prerequisites removed,draw!description updated.