Add maxRenderSize downsampling option to APNGImage#152
Merged
Conversation
Large APNGs (e.g. 10240x7680) allocate a full-resolution RGBA canvas (~300 MB) plus output/previous CGImages per displayed image. Several in a list exceed the per-app memory limit on iPhone and get jetsam-killed. Add an optional `maxSize: CGSize?` to every `APNGImage` initializer. When set, the decoder derives a `renderScale = min(1, fit)` and the whole compositing pipeline runs in that scaled space: the canvas buffer, every cached frame, and all frame rects shrink to fit, bounding memory. Frames still decode natively and Core Graphics downsamples them as they are drawn into the smaller destination rect. `maxSize: nil` (the default) keeps the native size and produces byte-identical geometry to before. The cache-size estimate also uses the scaled dimensions so a downsampled loop can regain caching. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
- scaledLength: keep a zero length at zero instead of clamping it up to one; extract a static core so it can be reused during init. - Reuse the static scaledLength for the cache-size estimate instead of duplicating the scaling arithmetic inline. - .previous disposal: integralize and clamp the render-space crop rect to the image bounds before CGImage.cropping(to:), which returns nil for a non-integral or out-of-bounds rectangle. - Clarify in the maxSize docs that it affects only rendering resolution and memory; the logical `size` / intrinsicContentSize stays native. - Add a unit test for scaledLength covering the zero, clamp, and rounding cases. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…/partial-frame tests - Rename `maxSize` parameter to `maxRenderSize` across all public initializers and internal plumbing to clarify it only affects rendering resolution, not the image's logical size or layout. - Move `.integral` into `renderRect(_:)` so all call sites get pixel-aligned rects, preventing fractional-coordinate issues with `CGImage.cropping(to:)` and 1px gaps between neighbouring partial-frame regions. - Add `testDownsamplingWithPreviousDisposal` (over_previous.apng) and `testDownsamplingWithPartialFrames` (spinfox.apng with non-power-of-two scale) to cover the two scenarios most likely to break under downsampling.
- Runner: macos-13 → macos-26 (Xcode 26.5, ARM64) - actions/checkout: v2 → v4 - actions/cache: v2 → v4 - Ruby: 2.7.8 → 3.3 (2.7 is EOL) - iOS simulator: iPhone 14 → iPhone 17
Ruby 2.7 is EOL; bump to 3.3.6 so gems (ffi, fastlane, cocoapods, etc.) can resolve to their latest versions.
Use proportional coordinates (fcTL values / native canvas size) instead of absolute pixel math for the frame-region overlay. This keeps the overlay correctly aligned regardless of whether the cached frames were downsampled by maxRenderSize.
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.
Summary
Based on #151 by @plateaukao — adds an optional
maxRenderSize: CGSize?to everyAPNGImageinitializer for memory-bounded rendering of oversized APNGs.Original PR contributions (by @plateaukao):
maxRenderSizeplumbing throughAPNGImage→APNGDecoder, derivingrenderScaleand render-space dimensionsAPNGImageRendererallocates and composites into a render-scaledCGContextReview fixes (this branch):
maxSize→maxRenderSizeto clarify it only affects rendering resolution, not logicalsizeor layout.integralintorenderRect(_:)so all call sites get pixel-aligned rects — preventsCGImage.cropping(to:)returningniland 1px gaps between partial-frame regionsscaledLengthto pass through zero instead of clamping to 1static scaledLength(_:scale:)to eliminate duplicated logic ininit.previousdisposal + downsampling and partial-frame + non-power-of-two scalemacos-13→macos-26, actions v4, Ruby 3.3, iPhone 17 simulatorTest plan
swift test)testDownsamplingWithPreviousDisposal— rendersover_previous.apngat half size through all framestestDownsamplingWithPartialFrames— rendersspinfox.apngat 2/3 scale (fractional) through all framesmacos-26runner