Skip to content

Add maxRenderSize downsampling option to APNGImage#152

Merged
onevcat merged 6 commits into
masterfrom
feature/downsampling-max-size-review
Jun 22, 2026
Merged

Add maxRenderSize downsampling option to APNGImage#152
onevcat merged 6 commits into
masterfrom
feature/downsampling-max-size-review

Conversation

@onevcat

@onevcat onevcat commented Jun 22, 2026

Copy link
Copy Markdown
Owner

Summary

Based on #151 by @plateaukao — adds an optional maxRenderSize: CGSize? to every APNGImage initializer for memory-bounded rendering of oversized APNGs.

Original PR contributions (by @plateaukao):

  • maxRenderSize plumbing through APNGImageAPNGDecoder, deriving renderScale and render-space dimensions
  • APNGImageRenderer allocates and composites into a render-scaled CGContext
  • Tests for downsampling and no-upscale behavior

Review fixes (this branch):

  • Rename maxSizemaxRenderSize to clarify it only affects rendering resolution, not logical size or layout
  • Move .integral into renderRect(_:) so all call sites get pixel-aligned rects — prevents CGImage.cropping(to:) returning nil and 1px gaps between partial-frame regions
  • Fix scaledLength to pass through zero instead of clamping to 1
  • Use static scaledLength(_:scale:) to eliminate duplicated logic in init
  • Add tests for .previous disposal + downsampling and partial-frame + non-power-of-two scale
  • Update CI: macos-13macos-26, actions v4, Ruby 3.3, iPhone 17 simulator

Test plan

  • All 90 tests pass locally (swift test)
  • testDownsamplingWithPreviousDisposal — renders over_previous.apng at half size through all frames
  • testDownsamplingWithPartialFrames — renders spinfox.apng at 2/3 scale (fractional) through all frames
  • CI passes on macos-26 runner

plateaukao and others added 6 commits June 22, 2026 21:12
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.
@onevcat onevcat merged commit d40a7d0 into master Jun 22, 2026
6 checks passed
@onevcat onevcat deleted the feature/downsampling-max-size-review branch June 22, 2026 14:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants