=begin pod

=head1 NAME

Vips::Native - Raku FFI wrapper for libvips with bundled prebuilts

=head1 SYNOPSIS

=begin code :lang<raku>

use Vips::Native;

# 1. Smart-crop + resize to fixed output dimensions, saved as PNG.
smart-resize("input.png", "thumb.png", 128, 128);

# 2. Letterbox-resize to a fixed square + return raw 8-bit RGB pixels.
#    Aspect ratio preserved; gap filled with the chosen background.
#    Output is suitable to feed to ML inference engines.
my Buf[uint8] $bytes = letterbox-to-buffer(
    'photo.png'.IO,
    :size(448),
    :bg(255, 255, 255),
);
say $bytes.bytes;   # 448 × 448 × 3 = 602112

=end code

=head1 DESCRIPTION

C<Vips::Native> is a Raku NativeCall wrapper around L<libvips|https://www.libvips.org/>,
shipped with prebuilt platform bundles so the common case (install,
import, run) doesn't require a working libvips toolchain on the host.

The module exposes two high-level helpers:

=item L<#smart-resize> — thumbnail-style: smart-crop to the requested
aspect ratio (using libvips's saliency / entropy / attention model),
then Lanczos3-resize, then save as PNG.

=item L<#letterbox-to-buffer> — ML-preprocess: scale-to-fit inside a
square canvas (no crop), pad the gap with a chosen colour, composite
RGBA over that colour, and return the raw RGB pixel buffer in NHWC
byte order. The preprocess pipeline image classifiers want.

The full (variadic) libvips C API is available via direct C<is native>
bindings under L<Vips::Native::FFI> — see the module's SYNOPSIS for
the bindings exported. Most of those go through a tiny non-variadic C
shim (C<src/vips_native_shim.c>) for ABI safety on Apple arm64 / Linux
aarch64, where Raku's NativeCall can't marshal C variadics correctly.

=head1 INSTALLATION

C<Vips::Native> tries three lookup paths at install + load time, in
order:

=item B<Prebuilt bundle (default)> — fetched from the project's
GitHub Releases at install time, sha256-pinned against
C<resources/checksums.txt>, staged under
C<$XDG_DATA_HOME/Vips-Native/<binary-tag>/lib/>. Bundles include
libvips, libglib, libgio, libgobject, the format loaders
(jpeg/png/webp/tiff/heif/avif/jxl/openslide/poppler/magick) and all
their transitive deps with C<@loader_path> (macOS) / C<$ORIGIN>
(Linux) / sibling-DLL (Windows) relocations baked in, so the bundle
is self-contained.

=item B<System libvips> — fallback when the prebuilt download fails
or no prebuilt exists for your platform. C<Vips::Native> probes for
an installed libvips itself and loads it by absolute path: first the
dynamic loader's own search variables (C<DYLD_LIBRARY_PATH> and
C<DYLD_FALLBACK_LIBRARY_PATH> on macOS, C<LD_LIBRARY_PATH> on Linux,
C<PATH> on Windows), then the platform's library directories —
including Homebrew's per-formula opt kegs, so an B<unlinked>
C<brew install vips> is still found — and finally C<ldconfig -p>'s
cache listing on Linux.
=begin item
On macOS:    C<brew install vips>
=end item
=begin item
On Debian/Ubuntu: C<sudo apt install libvips-dev>
=end item

=item B<Custom install> — set C<VIPS_NATIVE_LIB_DIR=/path/to/dir> to
point at a directory containing C<libvips.{dylib,so,dll}> and
C<libgobject-2.0.{dylib,so,dll}>. Caller takes responsibility for
ABI compatibility.

=head2 Supported platforms

=item macOS arm64
=item Linux x86_64 + aarch64 (glibc 2.35+)
=item Windows x86_64 + arm64

Every supported platform ships a working bundle with the varargs
shim included.

=head2 Build-time environment

=item C<VIPS_NATIVE_PREFER_SYSTEM=1> — skip prebuilt download, force
system libvips at runtime.
=item C<VIPS_NATIVE_BINARY_ONLY=1> — refuse to fall back to system
libvips; fail loudly if the prebuilt can't be installed (CI / pinned
deployments).
=item C<VIPS_NATIVE_DATA_DIR=/path> — override staged-libs root
(default C<$XDG_DATA_HOME> or C<~/.local/share>).
=item C<VIPS_NATIVE_CACHE_DIR=/path> — override prebuilt-archive
download cache (default C<$XDG_CACHE_HOME> or C<~/.cache>).
=item C<VIPS_NATIVE_BINARY_URL=https://…> — override the GitHub
Release base URL (custom mirror).
=item C<VIPS_NATIVE_KEEP_OLD_STAGES=1> — opt out of the orphaned-stage
GC that runs after every install. Use when intentionally pinning
multiple versions side-by-side.

=head1 EXTERNAL API

=head2 smart-resize

=begin code :lang<raku>
sub smart-resize(
    Str $in-path,
    Str $out-path,
    Int $out-width,
    Int $out-height,
    --> Bool
) is export
=end code

Smart-crop the source image to match the output aspect ratio (using
libvips's C<VIPS_INTERESTING_ATTENTION> saliency model — picks the
visually-important rectangle, not just the centre), then Lanczos3-
resize the cropped image to C<$out-width × $out-height> and save as
PNG to C<$out-path>.

Returns C<True> on success; C<fail>s with the underlying error if
libvips errors out.

A C<vips_smart_resize> shell binary is also installed:

  vips_smart_resize --in=/path/to/image.png --out=/path/to/save.png \
                    --width=180 --height=180

=head2 letterbox-to-buffer

=begin code :lang<raku>
sub letterbox-to-buffer(
    IO::Path() $path,
    UInt :$size = 448,
    :@bg = (255, 255, 255),
    --> Buf[uint8]
) is export
=end code

Resize C<$path> to fit inside a C<$size × $size> square while
preserving aspect ratio, pad the unfilled region with the C<@bg>
colour, composite alpha over that colour for RGBA inputs, and return
the raw 8-bit RGB pixel buffer in NHWC byte order. Length is always
exactly C<$size × $size × 3>.

The preprocess pipeline image classifiers want: deterministic gap
fill, fixed input dimensions, no GC entanglement with libvips's
heap. Pair with L<ONNX::Native> or your inference engine of choice
to feed RGB pixels directly.

The buffer is owned by the caller — internally we copy from the
C<vips_image_write_to_memory> result into a Raku-owned C<buf8> and
release the source via C<vips_shim_free>, so there's no lifetime
trap.

Inputs may be 3-band RGB or 4-band RGBA (UCHAR pixels only — 16-bit
PNG / HDR / float images are out of scope for v1). Greyscale and
CMYK fail loudly with details.

=head2 Vips::Native::FFI

The complete (variadic + non-variadic) libvips C bindings are
exported from C<Vips::Native::FFI>:

=item C<vips_init>, C<vips_image_new_from_file>
=item C<vips_image_get_width> / C<get_height> / C<get_bands> / C<get_format>
=item C<vips_smartcrop>, C<vips_resize>, C<vips_flatten>, C<vips_embed>
=item C<vips_pngsave>, C<vips_image_write_to_memory>
=item C<vips_shim_free> (libglib heap free for write-to-memory output)
=item C<g_object_unref> (release VipsImage handles)
=item C<VIPS_KERNEL_*>, C<VIPS_INTERESTING_*>, C<VIPS_EXTEND_*>,
C<VIPS_FORMAT_*> constants

C<vips_flatten> and C<vips_embed> require the libvips_shim (their
options take a C<VipsArrayDouble *> that's awkward to construct
directly from Raku). Every prebuilt bundle includes it; if you're
running against system libvips, the shim is compiled at install time
via the platform's C toolchain and falls back gracefully if no
toolchain is available.

=head1 AUTHOR

=item Matt Doughty <m.doughty@hushmail.com>

=head1 COPYRIGHT AND LICENSE

Copyright 2025-2026 Matt Doughty.

This library is free software; you can redistribute it and/or
modify it under the Artistic License 2.0.

=end pod
