Skip to content

feat(sharp): metadata/toFile objects, Buffer-input factory, .extract()/.sharpen(), SIMD resize#5448

Merged
proggeramlug merged 3 commits into
mainfrom
feat/sharp-metadata-object-extract-fastresize
Jun 19, 2026
Merged

feat(sharp): metadata/toFile objects, Buffer-input factory, .extract()/.sharpen(), SIMD resize#5448
proggeramlug merged 3 commits into
mainfrom
feat/sharp-metadata-object-extract-fastresize

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Four sharp improvements toward real-world parity, all pure-Rust (no libvips) — keeping Perry's self-contained static-binary model. Builds on #5444 (toBuffer real Buffer) and #5445 (fluent chaining).

1. .metadata() / .toFile() resolve real objects, not JSON strings

Before, (await sharp(x).metadata()).width was undefined — the Promise resolved with a JSON string. Now:

  • metadata(){ format, width, height, channels, space, hasAlpha }
  • toFile() → sharp's info object { format, width, height, channels, size }

Built on the main thread via resolve_with + js_object_alloc_with_shape/js_object_set_field (object allocation must happen on the main thread — the runtime arena is thread-local, #1824).

2. sharp(input) accepts a Buffer/Uint8Array

The factory routes through a new js_sharp_from_input, which recovers the underlying pointer from the raw NaN-boxed value and branches on the Buffer registry probe (js_buffer_is_buffer): Buffer → image::load_from_memory, string → image::open. Enables in-memory pipelines (sharp(buf).resize(...).toBuffer()).

3. .extract({left,top,width,height}) and .sharpen() — reachable + chainable

Both were implemented in perry-ext-sharp but had no dispatch rows. Now wired end-to-end: dispatch row (native_table/media.rs), FFI decl (stdlib_ffi.rs), manifest entry (entries.rs), and the fluent-chain allowlist (early_branches.rs). .extract() reads its options object's numeric fields by name (js_object_get_field_by_name_f64) and applies a region crop. This is the substantive follow-through on the earlier CodeRabbit note about allowlist completeness — added with their dispatch rows, not standalone.

4. resize() uses fast_image_resize (SIMD)

Swapped the image crate's scalar Lanczos3 for fast_image_resize, preserving the source pixel layout (Luma/LumaA/Rgb/Rgba 8-bit). Uncommon encodings (16-bit, float) convert to RGBA8; any fast-path failure falls back to the image resizer. Aspect-ratio (height <= 0) behavior preserved.

Validation

e2e (compiled, run, output asserted):

  • metadata()typeof object, 8 8 4 png srgb true; toFile()jpeg 20 10 4 size>0, file is a valid JPEG 20x10 components 3.
  • sharp(Buffer) → decode → resize(5,5).png().toBuffer() → PNG magic 137 80 78 71.
  • .extract({left:0,top:0,width:3,height:6}).metadata() → dims 3 6; .resize(8,8).sharpen().png().toBuffer() ok.
  • resize exact 12x7, aspect 16x16; grayscale→resize exercises the LumaA (U8x2) path (channels 2 space b-w).
  • perry-ext-sharp unit suite 6/6; API docs regenerated (reference.md, gate-clean).

Known nuance (follow-up)

toFile/metadata channels reflects the in-memory image's channel count (e.g. 4 for an RGBA source saved as JPEG, where the file is 3-channel) rather than the encoded output's.

Summary by CodeRabbit

Release Notes v0.5.1192

  • New Features

    • sharp() now accepts Buffer/Uint8Array for in-memory image loading
    • Added .extract({ left, top, width, height }) for cropping
    • Added .sharpen() for image enhancement
  • Improvements

    • resize() uses SIMD acceleration (with fallback) and improved dimension handling when height <= 0
    • metadata() now returns an object (format, width, height, channels, space, hasAlpha)
    • toFile() now resolves with an info object (format, width, height, channels, size)

…harpen, SIMD resize

Four pure-Rust sharp improvements toward real-world parity (no libvips):

- metadata()/toFile() resolve real objects instead of JSON strings:
  metadata -> {format,width,height,channels,space,hasAlpha};
  toFile -> sharp info {format,width,height,channels,size}. Built on the main
  thread via resolve_with + js_object_alloc_with_shape/js_object_set_field (#1824).
- sharp(input) accepts a Buffer/Uint8Array, not just a path: new js_sharp_from_input
  recovers the pointer from raw NaN-box bits and branches on js_buffer_is_buffer
  (Buffer -> load_from_memory, string -> open).
- .extract({left,top,width,height}) and .sharpen() are now reachable and chainable
  (dispatch rows + FFI decls + manifest entries + fluent-chain allowlist). extract
  reads its options object fields by name via js_object_get_field_by_name_f64.
- resize() uses fast_image_resize (SIMD Lanczos3), preserving source pixel layout
  (Luma/LumaA/Rgb/Rgba8; 16-bit/float fall back to RGBA8; fast-path failure falls
  back to the image crate). Aspect-ratio (height<=0) preserved.

Validated: metadata/toFile fields; sharp(Buffer) decode->resize->re-encode;
extract dims + sharpen chains; resize exact+aspect across RGBA/LumaA; valid JPEG
from toFile; perry-ext-sharp 6/6; api docs regenerated.
@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: 7249b96c-c01c-4c89-aa45-993d927373a8

📥 Commits

Reviewing files that changed from the base of the PR and between 0180f91 and 3d33465.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • CHANGELOG.md
  • crates/perry-ext-sharp/src/lib.rs
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/perry-ext-sharp/src/lib.rs

📝 Walkthrough

Walkthrough

Version 0.5.1192 adds two new sharp instance methods (extract and sharpen), a unified sharp(input) factory accepting Buffer/Uint8Array in addition to file paths, SIMD-accelerated resize via fast_image_resize with fallback, and changes toFile/metadata Promise resolutions from JSON strings to real JS objects.

Changes

Sharp feature additions

Layer / File(s) Summary
Codegen wiring: factory type, new method dispatch, FFI declarations
crates/perry-codegen/src/lower_call/native_table/media.rs, crates/perry-codegen/src/lower_call/early_branches.rs, crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs, crates/perry-api-manifest/src/entries.rs
Factory signatures in the native method table switch from NA_STR/js_sharp_from_file to NA_JSV/js_sharp_from_input; sharpen and extract added as instance-method entries; fluent-chain allowlist extended with both names; js_sharp_extract, js_sharp_from_input, and js_sharp_sharpen declared in stdlib FFI; manifest gains two new entries.
Core sharp Rust implementation
crates/perry-ext-sharp/Cargo.toml, crates/perry-ext-sharp/src/lib.rs
Adds fast_image_resize = "5" dependency; introduces NaN-boxed inspection helpers and opts_number_field; implements open_image_path/decode_image_bytes loaders; adds js_sharp_from_input factory branching on buffer vs string; adds fast_resize (SIMD Lanczos3 with image-crate fallback) and updates js_sharp_resize aspect-ratio logic; adds js_sharp_extract (reads left/top/width/height, calls crop_imm); changes js_sharp_to_file and js_sharp_metadata to resolve JS objects instead of strings.
Version bump, changelog, and API docs
Cargo.toml, CLAUDE.md, CHANGELOG.md, docs/src/api/reference.md
Workspace version bumped to 0.5.1192; changelog entry added describing all new sharp capabilities; API reference updated with extract and sharpen method entries and total count.

Sequence Diagram(s)

sequenceDiagram
    participant JS as JS Caller
    participant factory as js_sharp_from_input
    participant loader as open_image_path / decode_image_bytes
    participant resize as fast_resize (SIMD)
    participant fallback as image crate resize
    participant extract as js_sharp_extract
    participant meta as js_sharp_metadata / js_sharp_to_file

    JS->>factory: sharp(path | Buffer)
    factory->>factory: probe NaN-boxed type
    alt string path
        factory->>loader: open_image_path(path)
    else Buffer/Uint8Array
        factory->>loader: decode_image_bytes(bytes)
    end
    loader-->>factory: ImageHandle
    JS->>resize: .resize(w, h)
    resize->>resize: fast_image_resize Lanczos3
    alt unsupported layout or error
        resize->>fallback: image crate resize
    end
    JS->>extract: .extract({left,top,width,height})
    extract->>extract: opts_number_field + crop_imm
    JS->>meta: .metadata() / .toFile(path)
    meta-->>JS: JS object {format, width, height, channels, ...}
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • PerryTS/perry#5445: Directly extends the same native_method_returns_self_instance fluent-chain allowlist in early_branches.rs that this PR further updates with sharpen and extract.

Poem

🐇 Hip-hop, the pixels align,
A Buffer walks in — no path, no sign!
SIMD goes zoom with Lanczos three,
.extract() crops with glee!
metadata() returns a real object now,
The rabbit stamps v0.5.1192 — bow-wow! 🎨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description comprehensively covers all four features with technical details, implementation strategy, and validation evidence. However, it violates the template's explicit requirement to NOT edit CHANGELOG.md, CLAUDE.md, or workspace version—these files were modified in the changeset. The PR author should revert changes to CHANGELOG.md, CLAUDE.md, and Cargo.toml workspace version. The template explicitly states the maintainer handles these at merge time to avoid conflicts during frequent releases. These files should not be committed by contributors.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately captures the main changes: metadata/toFile as objects, Buffer input support, extract/sharpen methods, and SIMD resize via fast_image_resize.
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-metadata-object-extract-fastresize

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-ext-sharp/src/lib.rs`:
- Around line 427-451: The channels value is incorrectly sourced from
sharp.image.color().channel_count() which represents the pre-encoded pixels
rather than the actual encoded output. When format conversions occur (such as
RGBA to JPEG), the reported channels can diverge from the actual encoded file's
channel count. Instead of using sharp.image.color().channel_count() to determine
the channels value that gets set in js_object_set_field at field index 3, derive
the channel count from the encoded format itself using the sharp.format value
that is already being captured. Map the format to its corresponding channel
count (for example, JPEG typically has 3 channels, PNG with alpha has 4,
grayscale has 1) to ensure the returned metadata accurately reflects the actual
encoded output file.
- Around line 149-164: The js_sharp_from_input function does not validate that
non-buffer pointers are actually valid strings before attempting to read them as
StringHeader. After checking that the pointer is not a buffer using
js_buffer_is_buffer, add a tag validation check (checking for STRING_TAG or
SHORT_STRING_TAG) on the pointer before calling read_string. This ensures that
only pointers with valid string tags are processed as strings, preventing
arbitrary memory from being read as a StringHeader and protecting against
crashes or memory corruption from malformed or attacker-controlled pointers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8e72d2c9-d4ab-4865-8705-42b7b77bd74f

📥 Commits

Reviewing files that changed from the base of the PR and between 1db8e6b and 0180f91.

⛔ 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

Comment thread crates/perry-ext-sharp/src/lib.rs
Comment thread crates/perry-ext-sharp/src/lib.rs
proggeramlug and others added 2 commits June 19, 2026 03:06
…ls from output; rustfmt

CodeRabbit feedback on #5448:
- js_sharp_from_input: a POINTER_TAG value that isn't a registered Buffer is a
  plain object/array; js_get_string_pointer_unified hands back its heap pointer,
  which must not be read as a StringHeader (arbitrary-memory read). Reject it
  (return -1) like sharp's unsupported-input. Strings (long/short) and
  number-coerced keys aren't POINTER_TAG, so the path-string case is unaffected.
- toFile() info.channels now reflects the ENCODED output by re-reading the saved
  file (RGBA source saved as JPEG reports 3, not 4), falling back to the
  in-memory count. metadata() keeps in-memory channels (sharp semantics).
- rustfmt the fast_resize match arms.

Validated: toFile jpeg=3/png=4 channels; string & buffer factory width=8;
sharp({})/sharp([]) -> 0 (no crash).
@proggeramlug proggeramlug merged commit 24389a7 into main Jun 19, 2026
15 checks passed
@proggeramlug proggeramlug deleted the feat/sharp-metadata-object-extract-fastresize branch June 19, 2026 11:40
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