Image contains decoded, interleaved HWC pixels. Pixel modes determine the sample dtype:
| Modes | Pixel dtype | Channels, respectively |
|---|---|---|
| L, LA, RGB, RGBA | UInt8 | 1, 2, 3, 4 |
| L16, LA16, RGB16, RGBA16 | UInt16 | 1, 2, 3, 4 |
| RGB32F, RGBA32F | Float32 | 3, 4 |
Grayscale images retain a channel axis of length one. Float32 pixels must be finite; HDR values outside [0, 1] are retained. Floating alpha is straight alpha, with 1 representing opaque.
| Python | SQL | Storage |
|---|---|---|
vane.image_type() |
IMAGE |
STRUCT with variable mode and dimensions |
vane.image_type('RGB') |
IMAGE('RGB') |
STRUCT with fixed mode and variable dimensions |
vane.image_type('RGB', H, W) |
IMAGE('RGB', H, W) |
UInt8 ARRAY of length H × W × C |
The dynamic STRUCT fields, in order, are data: pixel[], channel: UInt16,
height: UInt32, width: UInt32, and mode: UInt8. Mode codes follow the
rows of the mode table: L=1 through RGBA32F=10. A known mode stores its native
pixel dtype in the LIST or fixed ARRAY. Generic IMAGE stores Float32 so a
single column can contain all ten modes without loss: Float32 represents every
UInt8 and UInt16 sample exactly. Its row mode determines the dtype restored
when materializing an ndarray. Generic integer rows must contain integral,
in-range values. Generic UInt8 images therefore use four times the pixel
storage of IMAGE('RGB') or another known UInt8 mode. Declare a known mode
when the column has one. A non-NULL image requires every field and pixel to be
non-NULL. Width and height must be positive. A fixed shape requires both
dimensions and a mode, and its pixel count must fit a signed 32-bit Arrow
fixed-size-list length. These representation limits do not reserve a memory
budget for an application; the number and size of materialized images still
contribute to query memory use. Fixed Image columns keep dense engine ARRAY
storage, but vector initialization does not reserve a full batch of pixels.
Pixel buffers grow to the rows actually written, including NULL padding.
Row-wise writers reuse owned capacity and grow it geometrically, so cumulative
allocation and copying remain proportional to the materialized pixel count.
Shared slices and borrowed Arrow buffers detach when they grow.
Copies and constant broadcasts operate on contiguous image rows. Materializing
many large images still consumes memory proportional to their actual pixel count.
Query descriptions display an Image's mode and dimensions instead of expanding
its pixels into strings. This also applies to Images nested inside containers.
SQL literals retain their complete pixel payload for reconstruction.
C API writers receive the raw writable capacity promised by their container:
created/reset chunks and table-function outputs reserve a standard batch;
duckdb_create_vector reserves its requested capacity; scalar, aggregate and
cast callbacks reserve their output span. This also applies to nested Images
and explicit list-capacity growth. Large fixed Images therefore require a
corresponding memory budget when creating a writable C API batch. Reading a
query result through the C API preserves its materialized pixel span.
dtype.is_image(), dtype.is_fixed_shape_image(), and dtype.image_mode
inspect the logical type. dtype.shape returns (height, width) for a fixed
Image and raises for a dynamic Image. ImageMode, ImageFormat, and
ImageProperty accept their string values and round-trip with str().
ImageFormat names PNG, JPEG, TIFF, GIF, and BMP. encode_image implements all five formats under the matrix below.
Fetched cells are detached, C-contiguous numpy.ndarray values with shape
(height, width, channels) and the mode's numpy.uint8, numpy.uint16, or numpy.float32 dtype. vane.Image is a typing
alias for that array, with no separate value wrapper or value methods.
Scalar Image parameters and plan serialization retain packed typed pixels;
binding an ndarray does not allocate an engine Value object for each sample.
Constant constructors and casts keep one pixel payload per batch, and Image
attribute functions read metadata without expanding constant pixels.
import numpy as np
import vane
pixels = np.zeros((48, 64, 3), dtype=np.uint8)
value = vane.Value(pixels, vane.image_type('RGB', 48, 64))
con = vane.connect()
image = con.execute('SELECT $1', [value]).fetchone()[0]
assert image.shape == (48, 64, 3)An ndarray acquires Image semantics through a declared Image type; ordinary ndarray inference continues to describe an ordinary array. Typed inputs may be strided: the boundary copies their pixels into HWC order without changing dtype or colors. Masked arrays, wrong pixel dtypes, unsupported channel counts, and incompatible declared dimensions or mode raise an error.
A PIL.Image.Image in a supported mode infers dynamic IMAGE. Pillow is
optional for the base Image type and is required only for PIL input or the
Python codec backend. PIL inputs do not undergo implicit color conversion.
ImageFile.decode() and the VideoFile value reader methods retain their PIL
return contracts; their expression/SQL counterparts produce engine Images.
Python functions, Expression methods, and SQL expose image_width,
image_height, image_channel, and image_mode. image_attribute(image, name) accepts height, width, channel, or mode, including
vane.ImageProperty members in Python. Results are UINTEGER and NULL inputs
produce NULL. Attribute access requires no codec or I/O.
expr.as_image(mode=None, height=None, width=None) validates an existing
Image expression against the selected Image type. Ordinary casts never
convert colors or resize pixels. TRY_CAST produces NULL for a layout
mismatch. Raw STRUCT, ARRAY, and BLOB values cannot acquire Image semantics
through ordinary casts. The SQL image(bytes, width, height, channels, mode)
constructor accepts decoded, native-endian bytes of the mode's pixel dtype
and validates byte alignment, sample values and layout.
Combining equal Image types preserves that type. Different shapes with the
same known mode widen to IMAGE(mode); different or unknown modes widen to
IMAGE. Assignment may widen constraints. Narrowing mode or dimensions
requires an explicit cast, including within nested containers.
The following functions have the same arguments in Python, Expression methods, and SQL:
| Python function | Expression method | SQL | Result |
|---|---|---|---|
vane.crop(image, bbox) |
expr.crop(bbox) |
crop(image, bbox) |
Dynamic Image, preserving the input mode constraint |
vane.encode_image(image, image_format) |
expr.encode_image(image_format) |
encode_image(image, image_format) |
Encoded bytes / BLOB |
bbox is (x, y, width, height), following the coordinate order of
Daft's crop API.
Python accepts a tuple/list of integers or an Expression. SQL accepts an integer
LIST or a four-element integer ARRAY. Floating-point coordinates and booleans
are rejected instead of rounded. Origins fit signed BIGINT; width and height
are positive UINTEGER values. Pixels outside the input are filled with zero in
every channel, including alpha. Empty crops are rejected because Image requires
positive dimensions. A fixed input still produces a dynamic crop result:
IMAGE('RGB', H, W) becomes IMAGE('RGB'); generic IMAGE remains generic.
Encoding follows this explicit mode matrix:
| Format | Accepted modes | Behavior |
|---|---|---|
| PNG | All eight integer modes | Lossless, 8-bit or 16-bit, straight alpha |
| TIFF | All ten modes | Uncompressed strips, native pixel depth, straight alpha |
| JPEG | L, RGB | Lossy; grayscale for L, full-range 4:4:4 for RGB |
| GIF | L, RGB | One frame, at most 256 palette colors |
| BMP | L, RGB | Lossless colors; native encoding stores RGB pixels |
Use convert_image explicitly for an unsupported combination. Format strings
are case-insensitive; Python also accepts ImageFormat members. Encoded bytes
and compression layout may differ by backend. Both JPEG encoders use libjpeg
quality 95, accurate integer DCT and 4:4:4 sampling for RGB. Both GIF encoders
preserve grayscale and images with at most 256 distinct RGB colors exactly.
Larger palettes use the same deterministic weighted median-cut algorithm over
a bounded 5-bit-per-channel histogram, with original 8-bit sample means and no
dithering. Codec versions can still affect compressed bytes.
NULL Images or NULL bbox/format arguments produce NULL. A non-NULL bbox must contain exactly four non-NULL integers. Invalid arguments, resource failures, missing dependencies, and cancellation propagate as errors.
Each operator accepts at most 100 million pixels and 256 MiB of pixel data per input Image. Crop applies the same pixel limit to its output; both operations limit materialized output to 256 MiB per vector batch. These checks do not replace the application's memory budget for source data, retained results, concurrent queries, or codec working memory. Input constants keep their single pixel payload even when other arguments vary. Native crop copies bounded spans; native PNG encoding streams through a bounded zlib buffer. Python crop uses NumPy buffer views; Python codecs use Pillow 10.4 or later, tifffile and imagecodecs with bounded output buffers. Both paths check interruption while processing data.
Backend selection uses image_backend='python'|'native', with Python as the
default. The native functions are provided by the optional Vane
native_media extension. Load it explicitly before choosing native execution; an
unavailable native backend raises during binding. There is no automatic
fallback. Arrow, UDF, and Ray paths retain the declared Image result type.
import vane
con = vane.connect()
vane.load_installed_extension("native_media", connection=con)
con.execute("SET image_backend='native'")
result = con.sql("""
SELECT encode_image(
crop(decode_image_file(image_file('photo.png'), 'RGBA'), [10, 20, 64, 48]),
'PNG'
) AS thumbnail
""").fetchone()[0]ImageFile decoding in this example performs governed FILE I/O. Crop and encoding operate only on its decoded pixels. Byte decoding and hashing use the same backend selection described below.
Python vane.decode_image(bytes_expr, on_error="raise", mode="RGB"),
expr.decode_image(on_error="raise", mode="RGB"), and
SQL decode_image(bytes, on_error => 'raise', mode => 'RGB') decode one
PNG, JPEG, TIFF, GIF, BMP or WebP image from a BINARY value. They perform no I/O.
The default output is IMAGE('RGB'); a constant mode binds IMAGE(mode).
mode=None/SQL NULL preserves the decoded pixel depth and binds generic
IMAGE. Palette images expand to RGBA. Animated GIF/WebP and multi-page TIFF
return the first frame/page. No orientation, ICC or transfer-function
transformation is applied.
GIF metadata validates the complete logical screen descriptor and any declared global color table. Its encoded mode is P, including identity grayscale palettes, and mode-less expression decoding expands the first frame to RGBA.
Native JPEG metadata validates the frame's sample precision and reports L16
or RGB16 for grayscale or three-component samples wider than eight bits.
It rejects wide four-component frame headers. Python JPEG decoding uses
Pillow and accepts eight-bit samples. Header inspection does not require pixel
decoding. Native eight-bit JPEG decoding uses libjpeg with accurate integer
IDCT and fancy chroma upsampling, matching Pillow's full-size decode settings.
CMYK JPEG expands to RGB in both backends when no output mode is requested;
metadata retains CMYK. Wider native JPEG coding processes use FFmpeg.
Eight-bit CMYK decoding follows Pillow's inverted sample convention in both backends, including files without an Adobe APP14 marker. Files storing ordinary (non-inverted) CMYK samples can therefore render incorrect colors in both backends; Vane does not infer their sample polarity from the missing marker. Normalize such files to RGB with a tool that understands their sample convention before decoding.
WebP decoding accepts lossy/lossless RGB and RGBA, including the first composited animation frame. Native decoding uses libwebp with bounded output storage. Both metadata backends read only RIFF/VP8/VP8L/VP8X headers (25 or 30 bytes) and report WEBP with RGB or RGBA mode. Expression decoding checks dimensions and output budgets before creating a WebP pixel decoder, including in Python. WebP encoding is not part of the encoding format matrix.
TIFF supports stripped, top-left images with RGB or black/white grayscale
photometric interpretation, contiguous or separate planes, 8/16-bit unsigned
samples or 32-bit floating RGB(A), and unassociated alpha. Unsupported
compression/layout, malformed bytes and MIME mismatches are content errors;
TIFF tiles and associated alpha are rejected. on_error='null' only suppresses
content errors. NULL bytes or a NULL error policy yield NULL. Missing codec
dependencies, allocation failures, resource limits and cancellation propagate.
The wide PNG decoder classifies libpng content diagnostics separately from
allocation and unknown codec failures; on_error='null' never suppresses the latter.
BMP metadata and decoding accept uncompressed RGB, bottom-up RLE8/RLE4 at
their matching bit depths, and BI_BITFIELDS at 16/32 bits. Other compression
values, mismatched depths and top-down RLE are content errors.
BMP BI_ALPHABITFIELDS (compression 6) is rejected by both backends. Supported
32-bit BI_BITFIELDS headers with an explicit alpha mask preserve RGBA pixels.
Bitfield masks must be nonzero for RGB, contiguous, disjoint and within the
declared depth. Supported layouts are RGB555/RGB565 at 16 bits and BGRX/BGRA,
XBGR/ABGR or RGBA byte layouts at 32 bits; partial alpha masks and other layouts
are rejected during metadata probing and decoding. Supported DIB headers contain
12, 40, 56, 64, 108 or 124 bytes. Indexed BMPs retain their declared 1/4/8-bit
packing when decoding compact grayscale palettes, including black/white tables
stored with 4-bit or 8-bit indices.
The declared pixel offset must follow the complete DIB header, bitfield masks
and color table; a table overlapping the pixel array is malformed content.
Native header probing reuses overlapping cached bytes and charges only newly
fetched bytes against its read budget; an exact BMP header budget is sufficient.
Both byte and ImageFile expression decoding support all ten output modes.
ImageFile decoding reads only its governed position/size window, validates
MIME and resolves credentials on the executing worker. Header metadata avoids
pixel decoding and applies the same TIFF layout checks as decoding; native TIFF
metadata lets libtiff read the first directory and its strip arrays through
bounded callbacks, reusing the already-read TIFF signature without charging
the read budget twice. Python metadata uses tifffile's format
and field definitions for its allocation preflight, then lets tifffile parse the
pixel layout. References beyond the logical FILE size are malformed content;
valid references outside the buffered metadata window are resource-limit errors.
Reading exactly to the window boundary succeeds. Python ImageFile.metadata()
raises ImageFileLimitError for insufficient budgets so callers can retry with
a larger budget. SQL ImageFile functions accept named
options, including image_file_metadata(f, max_pixels => 1000000) and
decode_image_file(f, on_error => 'null'). An omitted ImageFile decode mode
preserves the encoded mode; named limits can be supplied independently.
ImageFile.decode() remains a PIL value method; its output is limited to modes
Pillow can represent. Use the expression API for RGB16 and floating RGB(A).
Encoded inputs and each result column payload are capped at 256 MiB;
byte decoding has a separate 512 MiB working-pixel budget in both backends.
ImageFile expression decoding checks the encoded-input cap before parsing its
header, including when a larger max_input_bytes is supplied. This limit cannot
be suppressed by on_error='null'.
Image operators are capped at 100 million pixels. Header-only ImageFile metadata
can use a larger max_pixels budget without decoding pixels. The generic column
budget uses four bytes per sample. Retained results and codec scratch require additional
application memory. Python pixel validation scans floating storage in bounded
chunks; canonical UInt8/UInt16 arrays need no numerical validation scratch.
ImageFile max_decoded_bytes reserves decoder working
pixels, the decoded source, converted pixels, and output column storage.
Its 512 MiB default can be raised with a positive UBIGINT limit in either
backend; the independent encoded-input, pixel, output and codec allocation
limits still apply. Source pixels can exceed 256 MiB when conversion produces
an output column within that cap and the full working set fits the caller's
max_decoded_bytes budget.
This conservative per-row check also covers conversion scratch and Python's
spool copy, and includes Float32 column storage even when the returned ndarray
uses UInt8 or UInt16; native frame alignment may
require a larger limit. TIFF native library allocations have separate 256 MiB
single and cumulative limits, a bounded callback read budget and a 30-second
cooperative deadline. Calls into codecs are cancellation boundaries; they are
not preemptively interrupted inside a codec call.
Python vane.image_hash(image, *, method="phash", hash_size=8, binbits=3, segments=3), expr.image_hash(...) and SQL image_hash(image, method => 'phash', hash_size => 8, binbits => 3, segments => 3) accept all Image modes.
Options must be non-NULL constants, including parameters bound to constants.
NULL Images yield NULL. The result is FIXEDBINARY(n) in SQL and Arrow
FixedSizeBinary, with MSB-first bits and zero padding in the last byte.
Fixed-width values can enter ordinary BLOB functions without an explicit cast.
Comparisons, joins, unions and conditional expressions preserve equal fixed
widths and widen mixed widths or a fixed-width/BLOB pair to BLOB. The same rule
applies inside containers; NULL alone does not discard a fixed-width type.
BLOB-to-FIXEDBINARY casts and width changes require an explicit cast and validate
the exact byte width. FIXEDBINARY(0) preserves zero-width Arrow binary columns;
its non-NULL values are empty bytes, distinct from NULL.
| Method | Calculation | Output bits |
|---|---|---|
| ahash | Triangle resize, grayscale samples above mean | hash_size² |
| dhash | Adjacent horizontal grayscale increase | hash_size² |
| dhash_vertical | Adjacent vertical grayscale increase | hash_size² |
| phash | Low-frequency 2D DCT coefficients above median | hash_size² |
| phash_simple | Row DCT, omit DC, coefficients above mean | hash_size² |
| whash | Haar low-frequency block means above global mean | hash_size² |
| colorhash | Black, gray and 12 HSV color bins | 14 × binbits |
| crop_resistant | Row-major grid of segment phashes | segments² × hash_size² |
hash_size is 2..64, binbits 1..8, and segments 1..16. whash requires
a power-of-two hash size. Crop-resistant hashing requires at least one pixel
per grid segment; boundaries use integer fractions of the original size.
It concatenates segment bits before adding final byte padding. Output length
is the ceiling of the bit count divided by eight.
Hash version 1 uses full-range BT.601 grayscale, ignores alpha, scales UInt16 and Float32 to [0,255], and rounds/clamps to UInt8. Triangle downsampling is antialiased, reduces the larger ratio first, and rounds each pass. DCT uses a 4×hash_size image and coefficients quantized to 1e-6 before thresholding. Haar uses the largest power-of-two square no larger than the shorter side, or hash_size for smaller images. Histogram counts use ordinary binary fields. Both backends implement this contract independently. These hashes are content similarity features, not integrity checksums or unique identifiers; no bit-for-bit contract with another library is implied.
Sampling scratch is bounded separately at 256 MiB and checks cancellation in blocks. Native hashing uses C++ kernels and never calls a Python helper.
Python vane.image_to_tensor(image), Expression expr.image_to_tensor(), and
SQL image_to_tensor(image) expose the same conversion of decoded pixels:
| Input type | Result type | Shape |
|---|---|---|
IMAGE |
TENSOR(FLOAT, [NULL, NULL, NULL]) |
Per-row height, width, channels |
IMAGE(mode) |
TENSOR(pixel, [NULL, NULL, C]) |
Per-row height/width, known channel count |
IMAGE(mode, H, W) |
TENSOR(pixel, [H, W, C]) |
Fixed HWC dimensions |
Pixel values, physical storage dtype, interleaved HWC order, and every channel
are preserved. pixel is UTINYINT, USMALLINT or FLOAT according to the known
mode. A generic Image yields a Float32 Tensor because its source column already
uses Float32; the conversion shares that buffer.
Grayscale retains a channel dimension of one. There is no normalization,
resizing, axis permutation, or color conversion. NULL inputs produce NULL
Tensors; empty inputs retain the inferred result type. Non-Image SQL arguments
are rejected. Python accepts Image-typed values, HWC ndarrays, and supported PIL
inputs through the existing Image input boundary.
This operator runs directly in the base C++ engine, independently of
image_backend and without loading an optional extension. It shares the
input's dense pixel buffer and retains its owner: fixed Images also preserve
constant/dictionary representation, while dynamic Images produce LIST offsets
and three dimensions per row. It validates active Image layouts and retains
the existing per-input limit of 100 million pixels / 256 MiB. The conversion
stays in execution instead of scalar constant folding, which would expand a
Tensor into individual engine values. It does not allocate a second batch of
pixel data. Downstream materialization,
Arrow export, Python values, and consumers may copy or broadcast those pixels
and still require memory proportional to their output.
Fixed numeric Tensor vectors now allocate element storage for written rows, using the same mechanism as fixed Images. The full writable capacity promised by C API containers is still reserved. Plain SQL ARRAY and other Tensor element types retain their existing allocation behavior. Materialized relation query descriptions contain row counts and column types; generating a description does not scan or stringify Tensor elements.
Arrow preserves arrow.fixed_shape_tensor or arrow.variable_shape_tensor,
including dtype and shape constraints, through IPC, UDFs and Ray/Flight. Python
scalar materialization follows the existing Tensor contract: variable Tensors
produce HWC ndarrays; fixed Tensors produce flat tuples with shape carried by
their declared type. Fixed Tensors are also accepted by vane.func and
vane.func.batch, including registered SQL UDFs, with logical shape validation
at the output boundary. Fixed Tensor row outputs must be flat lists or tuples
of the declared length; textual array encodings are rejected, including when
the Tensor is nested inside another output type.
Fixed Tensor Arrow batches support to_numpy_ndarray()
to obtain (rows, H, W, C) arrays. NULL rows must be handled before calling
that Arrow method.
con = vane.connect()
result = con.sql("""
SELECT image_to_tensor(convert_image(
resize(decode_image_file(image_file('photo.png'), 'RGB')::IMAGE('RGB'), 224, 224),
'RGB'
)) AS pixels
""")
assert result.types == [vane.tensor_type(vane.sqltypes.UTINYINT, (224, 224, 3))]
batch = result.to_arrow_table().column('pixels').combine_chunks().to_numpy_ndarray()The Image preprocessing operators in this example use the selected image
backend. image_to_tensor itself performs no file I/O or codec work.
| Python function | Expression method | SQL |
|---|---|---|
vane.resize(image, w, h, antialias=False) |
expr.resize(w, h, antialias=False) |
resize(image, w, h[, antialias]) |
vane.convert_image(image, mode) |
expr.convert_image(mode) |
convert_image(image, mode) |
Both operators accept all ten Image modes.
They operate on decoded pixels without file I/O. Python accepts Image-typed
values, HWC ndarrays and supported PIL inputs through the existing Image input
boundary. Width and height accept integers or Expressions; mode accepts a
string, ImageMode or Expression. Python keyword names match the table above.
SQL uses positional scalar arguments. Boolean, floating-point, decimal and
string dimensions are rejected; dimensions must be positive UINTEGER values.
Mode strings are case-insensitive, with no whitespace trimming or implicit
conversion from other SQL types. Unsupported modes raise an error.
| Operation and bind-time constraints | Result type |
|---|---|
resize, input mode and both target dimensions known |
IMAGE(mode, h, w) |
resize, input mode known and target dimensions vary by row |
IMAGE(mode) |
resize, input mode unknown |
IMAGE |
convert_image, fixed input and target mode known |
Fixed Image with the new mode and original dimensions |
convert_image, dynamic input and target mode known |
IMAGE(mode) |
convert_image, target mode varies by row |
IMAGE |
An option is known at binding when its expression is foldable and non-NULL; supplied parameter values participate in this inference. A per-row dimension or mode never changes the declared result type during execution. NULL Images or NULL option arguments produce NULL. Statically invalid options raise during binding, including for empty inputs; invalid per-row options raise when a non-NULL row is evaluated. Empty relations retain the inferred Image type. Ordinary casts still validate layout and never resize or convert colors.
Resize maps each output pixel center to (index + 0.5) * source_size / target_size - 0.5 independently on each axis, clamps it to the source edges,
and applies bilinear interpolation. It stretches to exactly the requested
width and height. The default antialias=False retains this sampling rule.
With antialias=True, downsampling widens a separable Triangle filter by the
source/target ratio on each shrinking axis and normalizes the in-bounds weights.
Intermediate samples retain double precision; rounding occurs after both passes.
The smaller intermediate image is chosen and capped at 256 MiB. Pure upsampling
and identity resizes retain the existing path. The option accepts per-row booleans;
NULL produces NULL, and implicit casts from other option types are rejected.
There is no gamma, transfer-function or color-profile conversion. Integer channel results are clamped to the
dtype range and rounded to the nearest integer, with halves rounded up. Float32
results retain their finite values without integer rounding.
For all alpha-bearing modes, resize interpolates premultiplied color and alpha, then unpremultiplies using the unrounded interpolated alpha. A nonpositive interpolated alpha gives zero color channels. The returned pixels use straight alpha. Resizing to the original dimensions copies all bytes, including hidden colors under transparent pixels. Backend floating-point arithmetic can differ at rounding boundaries; bitwise equality across backends is not a requirement.
Color conversion preserves dimensions and uses full-range RGB luma:
L = (299*R + 587*G + 114*B + 500) // 1000. Gray-to-RGB copies L into each
color channel. Existing alpha is preserved when the output has alpha; adding
alpha uses 255, 65535 or 1 according to the output dtype. Conversion between
pixel depths scales the full range (255 ↔ 65535 ↔ 1), clamps integer outputs
and rounds halves up. Float32 outputs preserve finite HDR values. Dropping alpha keeps the color values without compositing
against a background. A conversion to the current mode copies all pixels.
import vane
con = vane.connect()
vane.load_installed_extension("native_media", connection=con)
con.execute("SET image_backend='native'")
prepared = con.sql("""
SELECT resize(convert_image(decode_image_file(image_file('photo.png')), 'RGB'),
224, 224) AS image
""")
assert prepared.types == [vane.image_type('RGB', 224, 224)]These operators use the existing image_backend='python'|'native' selection.
The native implementation runs C++ pixel kernels in the explicitly loaded
DuckDB image extension. Python executes independent bounded NumPy helpers.
Neither pixel path requires Pillow; PIL inputs and Python codecs still do.
An unavailable native backend raises during binding, with no automatic fallback.
The existing 100-million-pixel per-image limit and 256 MiB input-image and output-batch limits apply. Fixed output batches include NULL-row pixel padding in their budget and are checked before pixel work. Dynamic outputs charge each materialized row before allocation. Constant operands retain a single source payload, and entirely constant calls produce one output payload per batch. Kernels check cancellation in bounded pixel blocks; the Python helper also bounds its coordinate and floating-point scratch arrays independently of image dimensions. These payload limits do not bound process RSS: vector growth, scratch space, input columns and downstream state consume additional memory. Validation, allocation, resource and interruption errors propagate. Arrow, registered row/batch UDFs, Flight and Ray retain dynamic or fixed Image types.
Arrow uses the vane.image extension type over the physical STRUCT or
FixedSizeList. Its metadata contains exactly mode, height, and
width; absent constraints are JSON null. Arrow IPC and Python pickling retain that metadata, including NULL values.
PyArrow Parquet round-trips retain it for dynamic images and non-NULL fixed
images. PyArrow 25 cannot read NULL FixedSizeList values from Parquet, including
values beneath NULL parents; use IPC or database storage for those fixed Image
columns. Import validates the
physical dtype, mode, dimensions, pixel count, and active pixel validity.
Image type and layout also survive Flight and worker plan transport.
Row UDFs receive HWC ndarray cells and accept ndarray/PIL outputs under a
declared Image return type. Batch UDFs receive Arrow columns carrying
vane.image. Outputs may use that exact extension type or its canonical
storage under the explicitly declared Image return type. A conflicting
extension mode or shape is rejected, even when pixel counts happen to match.
NULL parents and inactive UNION children do not expose hidden pixel payloads
for value validation. SQL-registered UDFs apply the same contracts.
Image ndarrays are unhashable. Maps whose keys contain Image use the existing
parallel {'key': [...], 'value': [...]} Python representation for unhashable
map keys.
The type, storage, attributes, and transport belong to the base engine. Pixel
operators and codecs belong to the existing DuckDB image extension.
image_backend='python'|'native' selects explicit pixel and codec paths;
loading the extension does not change the Image value representation.