Skip to content

feat(sharp): EXIF auto-orient, .extend(), .trim(), .composite()#5455

Merged
proggeramlug merged 1 commit into
mainfrom
feat/sharp-orient-composite-extend-trim
Jun 19, 2026
Merged

feat(sharp): EXIF auto-orient, .extend(), .trim(), .composite()#5455
proggeramlug merged 1 commit into
mainfrom
feat/sharp-orient-composite-extend-trim

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

PR 1 of 2 in the next sharp batch — pure-Rust (no libvips), keeping Perry's static-binary model. Builds on #5444/#5445/#5448. (PR 2 will add AVIF encode + SVG rasterization, which pull heavier deps.)

EXIF auto-orient

The orientation tag (1–8) is read at load (kamadak-exif) and stored on the handle.

  • .autoOrient() applies the rotate/flip so pixels are upright, then clears the tag.
  • .rotate() with no angle also auto-orients (classic sharp behavior; a missing arg arrives as NaN).

A 6×4 JPEG tagged orientation-6 → upright 4×6; images without EXIF are unchanged.

.extend({ top, bottom, left, right, background })

Pads the image with a background colour ({ r, g, b, alpha }, r/g/b 0–255, alpha 0–1; default opaque black).

.trim()

Auto-crops a uniform border, detected from the top-left pixel with sharp's default colour tolerance (10).

.composite([{ input, top, left }, …])

Overlays layers (each input a path or Buffer) onto the base with alpha blending, walking the JS array of layer objects at the ext-crate boundary (js_array_length/js_array_get + per-element field reads).

Wiring & cleanup

All four wired end-to-end (dispatch rows in native_table/media.rs, FFI decls, manifest entries, fluent-chain allowlist). Loaders unified so file and Buffer inputs share the EXIF-reading path — js_sharp_from_file/from_buffer now delegate to open_image_path/decode_image_bytes.

Validation

  • .autoOrient() and .rotate() both turn the orientation-6 JPEG into 4×6; no-EXIF PNG stays 8×8.
  • .extend() grows 8×8 → 14×12.
  • .trim() round-trip: extend a white border then trim back to exactly 8×8 (proves both extend's background fill and trim's border detection/crop).
  • .composite() overlay → valid PNG; full autoOrient→resize→extend→jpeg→toBuffer chain works.
  • perry-ext-sharp unit suite 9/9 (incl. new orientation-transform tests); API docs regenerated (2810→2814 entries).

Not in this PR

  • Animated WebP — the image crate has no animated-WebP encoder; true animation is a much larger lift than AVIF/SVG, so it's deliberately out (would need a different codec path).
  • Deleting perry-stdlib/src/sharp.rs — on inspection it's still compiled into the default full feature set (only stripped at link time by the well-known flip), so removing it is a feature-graph change that deserves its own link-validated PR rather than riding along here.

Summary by CodeRabbit

  • New Features
    • EXIF auto-orientation support (including .autoOrient()), automatically reading and applying orientation metadata.
    • Image padding with .extend() using RGBA background color.
    • Smart border removal with .trim() to crop uniform borders.
    • Layer compositing with .composite() with alpha-blended overlays (supports inputs from paths or buffers).
    • Chaining support across these operations for fluent pipelines.
  • Bug Fixes
    • .rotate() now auto-applies EXIF orientation when called without a valid angle (matching sharp behavior).
  • Documentation
    • Updated the supported API reference to include autoOrient, extend, trim, and composite.

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d453a258-1fa0-4d59-bcd9-985e0abac351

📥 Commits

Reviewing files that changed from the base of the PR and between cad035b and c1bd787.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • CHANGELOG.md
  • CLAUDE.md
  • Cargo.toml
  • crates/perry-api-manifest/src/entries.rs
  • crates/perry-codegen/src/lower_call/early_branches.rs
  • crates/perry-codegen/src/lower_call/native_table/media.rs
  • crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs
  • crates/perry-ext-sharp/Cargo.toml
  • crates/perry-ext-sharp/src/lib.rs
  • docs/src/api/reference.md
✅ Files skipped from review due to trivial changes (5)
  • CLAUDE.md
  • Cargo.toml
  • CHANGELOG.md
  • crates/perry-ext-sharp/Cargo.toml
  • docs/src/api/reference.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • crates/perry-api-manifest/src/entries.rs
  • crates/perry-codegen/src/lower_call/early_branches.rs
  • crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs
  • crates/perry-codegen/src/lower_call/native_table/media.rs
  • crates/perry-ext-sharp/src/lib.rs

📝 Walkthrough

Walkthrough

Version bumped to 0.5.1194. Four new pure-Rust sharp API methods—autoOrient, extend, trim, and composite—are implemented in perry-ext-sharp. SharpHandle gains an orientation: u8 field populated from EXIF data at load time via kamadak-exif. Orientation is propagated through all existing transforms. All methods are wired end-to-end through FFI declarations, native dispatch tables, manifest entries, and fluent-chain allowlists.

Changes

Sharp: EXIF auto-orient, extend, trim, composite

Layer / File(s) Summary
SharpHandle orientation field, EXIF helpers, and decoder refactor
crates/perry-ext-sharp/Cargo.toml, crates/perry-ext-sharp/src/lib.rs
Adds kamadak-exif dependency; expands perry-ffi imports; adds orientation: u8 to SharpHandle; implements read_exif_orientation, apply_orientation, opts_field_bits, read_background, and decode_image_from_value; refactors js_sharp_from_file and js_sharp_from_buffer to delegate through shared open/decode helpers that populate orientation from EXIF data at load time.
autoOrient, extend, trim, and composite implementations
crates/perry-ext-sharp/src/lib.rs
js_sharp_rotate treats angle.is_nan() as auto-orient, calling apply_orientation on pixels and resetting orientation to 1. js_sharp_auto_orient applies the stored orientation to pixels and registers with orientation: 1. js_sharp_extend reads RGBA background via read_background. js_sharp_composite extracts each layer's input field via opts_field_bits and decode_image_from_value then alpha-blends at specified coordinates. JPEG output also preserves orientation.
Orientation propagation in existing transforms
crates/perry-ext-sharp/src/lib.rs
All existing transform functions (resize, flip, flop, grayscale, blur, sharpen, crop, png, webp) now carry the orientation field forward into the registered SharpHandle instead of dropping it.
Codegen dispatch table, FFI declarations, manifest, and fluent allowlist
crates/perry-api-manifest/src/entries.rs, crates/perry-codegen/src/lower_call/early_branches.rs, crates/perry-codegen/src/lower_call/native_table/media.rs, crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs
Registers autoOrient, extend, trim, and composite in the API manifest, the fluent-chain same-instance allowlist, the NativeModSig media table mapping each method to its js_sharp_* runtime, and the stdlib FFI declaration block.
Tests, version bump, changelog, and API docs
crates/perry-ext-sharp/src/lib.rs, Cargo.toml, CLAUDE.md, CHANGELOG.md, docs/src/api/reference.md
Test helper make_handle initializes orientation: 1; new unit tests cover apply_orientation for orientations 6 and 1, and read_exif_orientation defaulting to 1 on invalid input. Version bumped to 0.5.1194. Changelog and API reference updated.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • PerryTS/perry#5445: Modifies crates/perry-codegen/src/lower_call/early_branches.rs to introduce fluent-chain allowlist handling for sharp, which is the same file and mechanism this PR extends for autoOrient, extend, trim, and composite.
  • PerryTS/perry#5448: Modifies the same perry-ext-sharp file/Buffer factory and open/decode path, native dispatch table, fluent allowlist, and FFI declarations that this PR builds directly on top of.

Poem

🐰 Hop hop, I flip the bits just right,
EXIF tells me which way is up tonight!
.extend() pads with colors bright,
.composite() layers, what a sight!
.trim() the edges, crisp and neat—
Pure Rust sharp, no libvips feat! 🖼️

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description violates the template requirement that contributors must NOT bump workspace version or edit CLAUDE.md/CHANGELOG.md, which the maintainer handles at merge time. Revert changes to Cargo.toml [workspace.package] version, CLAUDE.md Current Version, and CHANGELOG.md before submitting. These will be bumped by maintainers at merge time per CONTRIBUTING.md guidelines.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main feature additions: EXIF auto-orientation, extend, trim, and composite methods for the sharp library.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sharp-orient-composite-extend-trim

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Comment @coderabbitai help to get the list of available commands and usage tips.

Pure-Rust sharp parity additions (no libvips):

- EXIF auto-orient: read Orientation (1-8) at load via kamadak-exif, store on
  the handle. .autoOrient() applies the rotate/flip and clears it; .rotate()
  with no angle (NaN arg) also auto-orients. Loaders unified so file & Buffer
  inputs share the EXIF path (from_file/from_buffer delegate to
  open_image_path/decode_image_bytes).
- .extend({top,bottom,left,right,background}) pads with a {r,g,b,alpha} colour
  (default opaque black).
- .trim() auto-crops a uniform border (top-left pixel, tolerance 10).
- .composite([{input,top,left}]) overlays layers (path/Buffer) with alpha
  blend, walking the JS array of layer objects at the ext boundary.

All wired end-to-end (media.rs rows, stdlib_ffi decls, entries.rs manifest,
fluent-chain allowlist) + docs regen (2810->2814).

Validated: orientation-6 jpeg -> 4x6 via autoOrient & rotate(); no-EXIF png
unchanged; extend 8x8 -> 14x12; trim round-trip (white-border extend then trim
-> 8x8); composite -> valid png; full autoOrient->resize->extend->jpeg->toBuffer
chain. perry-ext-sharp 9/9 unit tests.
@proggeramlug proggeramlug force-pushed the feat/sharp-orient-composite-extend-trim branch from cad035b to c1bd787 Compare June 19, 2026 13:32
@proggeramlug proggeramlug merged commit 4a73b0e into main Jun 19, 2026
15 checks passed
@proggeramlug proggeramlug deleted the feat/sharp-orient-composite-extend-trim branch June 19, 2026 13:42
proggeramlug pushed a commit that referenced this pull request Jun 19, 2026
- .avif(quality): AVIF output via image's `avif` feature (pure-Rust ravif/rav1e).
  Encode-only — AVIF decode needs C dav1d, kept out for the static-binary model.
- encode_to_vec helper honours quality for JPEG (JpegEncoder::new_with_quality)
  and AVIF (AvifEncoder::new_with_speed_quality); fixes the pre-existing gap where
  .jpeg(q) quality was ignored by the default write_to path. toFile encodes by the
  output path extension (so .toFile('x.avif') works) and honours quality.

Wired end-to-end (media.rs row, stdlib_ffi decl, entries.rs manifest, fluent
allowlist) + docs regen. Validated: .avif().toBuffer() -> AVIF ftyp/avif box;
.toFile('x.avif') -> valid AVIF (ISO Media); jpeg quality affects size; 9/9 tests.

Stacked on the EXIF/extend/trim/composite branch (#5455).
proggeramlug added a commit that referenced this pull request Jun 19, 2026
…5456)

- .avif(quality): AVIF output via image's `avif` feature (pure-Rust ravif/rav1e).
  Encode-only — AVIF decode needs C dav1d, kept out for the static-binary model.
- encode_to_vec helper honours quality for JPEG (JpegEncoder::new_with_quality)
  and AVIF (AvifEncoder::new_with_speed_quality); fixes the pre-existing gap where
  .jpeg(q) quality was ignored by the default write_to path. toFile encodes by the
  output path extension (so .toFile('x.avif') works) and honours quality.

Wired end-to-end (media.rs row, stdlib_ffi decl, entries.rs manifest, fluent
allowlist) + docs regen. Validated: .avif().toBuffer() -> AVIF ftyp/avif box;
.toFile('x.avif') -> valid AVIF (ISO Media); jpeg quality affects size; 9/9 tests.

Stacked on the EXIF/extend/trim/composite branch (#5455).

Co-authored-by: Ralph Küpper <ralph2@skelpo.com>
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.

1 participant