Repository navigation
v0.0.5
Added
- (convert) -roll ±X±Y — IM-style circular pixel shift
- (convert) -quality validates 0..=100 at parse time
- (convert) 'did you mean?' hint on unknown -flag errors
- (convert) -trim + -fuzz N[%] — IM auto-crop with tolerance state
- (convert) -monochrome — IM 1-bit B/W shorthand (gray + 2 colors + Floyd-Steinberg)
- (convert) -extent WxH[±X±Y] — canvas re-window with active background
- (convert) comma-list + negative page-selector atoms
- (convert) .ico output + -define icon:auto-resize multi-resolution
- (convert) debug render modes (normal/depth) + N×N supersampling AA
- (convert) gouraud/phong shading + camera/light/projection/fov/bg + FBX wire
- (convert) wire USDZ encoder + 3D→raster software renderer
- (convert) -resize geometry suffixes + -thumbnail + -define
- (convert) --probe --watch + did-you-mean hints for unknown extensions
- (convert) --probe surfaces PDF /Info, mesh3d per-mesh detail, container metadata
- (convert) --probe dry-run structural inspection (+--json)
- (convert) -stl-format and -gltf-format encoder option flags
- (convert) 3D-asset side-channel — STL/OBJ/glTF/GLB/USDZ
- (convert) -vignette / -colorize / -equalize / -auto-gamma + PDF tonal-op side-channel
- (convert) IM tonal / colour-grading flags wired to image-filter
- (convert) -rotate / -flip / -flop / -crop / -negate inline ops
- (convert) -ping flag for IM-style header-only inspection
Fixed
- (convert) revert pdf_runner BMP API change
Other
- (mesh3d) route renderer through oxideav-render::make_renderer
- wildcard arm for non-exhaustive SourceOutput
- bump oxideav-webp pin 0.1 → 0.2
- (convert) pin PDF→raster DPI math + headline-command end-to-end
- stub encode_webp after oxideav-webp clean-room rebuild
- ico_out test: replace 5-tuple with named DirEntry struct for clippy
- absorb oxideav-bmp 0.1.3 BmpImage.palette + encode_bmp tuple return
- drop alias table — registry is the only source of truth
- registry-driven output codec lookup via ContainerRegistry
- rustfmt sweep on round-2 PDF / fan-out additions
- bump oxideav-webp dep to 0.1 (published)
- bump oxideav-pdf dep to 0.1 (published)
- refresh for PDF / page selectors / multi-format fan-out
- round 2: page selector + multi-format raster fan-out + SVG/PDF page extraction
- add PDF input + Scene-aware fan-out for ImageMagick-style PDF rendering
Added
-
-roll ±X±Y— IM-style circular pixel shift.dxshifts columns
to the right bydxpixels (negative = left),dyshifts rows
down bydypixels (negative = up); pixels that fall off one
edge wrap around to the opposite edge so the visible image
translates as a rigid block. Width and height stay unchanged.
Grammar mirrors-extent's±X±Ytail (shared parser): both
offsets must be present and signed (-roll +5+10,-roll -3+2,
-roll +5-10); a single offset (-roll +5), an unsigned value
(-roll 5+10), or an empty argument is rejected at parse time
with a clear message. Shifts larger than the dimension are
reduced mod width / height by the inline implementation so
-roll +width+heightis the identity. Wired through both paths:
the generic pipeline (video.rollfactory in
oxideav-image-filter) and the inline PDF / mesh3d side-channel
(newpixel_xform::rollworking onRgbaImagewith both RGB-24
and RGBA stride layouts via two in-placeslice::rotate_right
passes — one over each row, one over the row block). -
-qualitynow validates the IM-conventional0..=100range at
parse time. Out-of-band values (-quality 1000,-quality 150)
previously fell through the encoder's "drop unknown key" path and
looked to the user like the flag had been honoured; they now emit
a clearconvert: -quality: N out of range (must be in 0..=100; common values: 75 default, 85 web, 95 archival)error. Endpoints
0(JPEG's smallest acceptable) and100(highest) stay
accepted; the existing non-numeric / negative-integer path
(-quality high,-quality -5) still trips the pre-cap
not a non-negative integerbranch with its original message.
Pinned by three new args-parser tests covering the
endpoints-accepted, over-range-rejected, and non-numeric-
unchanged contracts. -
"Did you mean?" hint on unknown-flag errors. A bogus
--prefixed
flag is matched against the full known-flag table (-resize,
-quality,-colorspace, …, plus the double-dash session modes
--probe/--json/--watch) via the existing
suggest::closest_matchLevenshtein helper. Close edits emit a
(did you mean '-quality'?)clause appended to the bare
unknown flag 'X'error; distant typos (-fnord) get the bare
error unchanged. The lead the user typed is preserved on the
suggestion —--prbereattaches--to suggest--probe,
-quailtyreattaches-to suggest-quality— so the hint is
copy-paste-correct. The known-flag table is a single sorted slice
near the top ofargs.rsthat must stay in sync with the parser's
match arms; a flag missing from the table just downgrades hint
quality on its own typos. Adds three unit tests pinning the
transposition (-quailty→-quality,-reszie→-resize)
and the dash-flavour-preservation (--prbe→--probe) cases,
plus a negative-assert on the existing-fnordtest confirming
distant tokens still produce no hint. -
-trim(valueless) paired with-fuzz N[%]— IM's auto-crop op
that collapses an image down to the bounding box of pixels
differing from the corner-pixel reference background by more
than the active-fuzztolerance. The-fuzzflag updates a
parser-state value rather than emitting an op of its own; a
following-trimcaptures the value at parse time so source-
order semantics survive into the plan walker (-fuzz 5 -trim -fuzz 20 -trimlands as two trims with two distinct
tolerances, not one). Accepts both the raw byte grammar
(-fuzz 12,0..=255) and IM's percent grammar (-fuzz 10%,
0..=100%→0..=255rounded). Wired through three paths:
the args parser (newOp::Trim { fuzz }variant + per-flag
state plumbing), the generic pipeline path (video.trim
factory inoxideav-image-filter), and the inline PDF /
mesh3d side-channel (new shape-recovering helper in
pixel_xformthat inverts the packed-plane stride to read
back the trimmed dimensions, since the bbox isn't known at
apply-time). Uniform-background inputs collapse to a
representable1x1frame per the factory contract. -
-monochrome— IM's valueless shorthand that emits a 1-bit
black-and-white image with Floyd-Steinberg error diffusion. Lowers
to the canonical primitive chain
-colorspace gray -colors 2 -dither floyd_steinbergat args-parse
time, preserving source-order semantics so any neighbours (e.g.
a preceding-resizeand a trailing-strip) sit on either side
of the expansion rather than being pushed around it. The dither
choice is hardcoded toFloydSteinbergregardless of any prior
-dither noneto match IM's "monochrome is self-contained"
contract; users who want the un-dithered cut still write the chain
by hand. Lives in the args parser only — every downstream runner
(PDF side-channel, 3D-render side-channel, regular pipeline) sees
the lowered ops it already knows how to handle, so no plan_to_job
/ pixel_xform / image-filter changes were needed. -
-extent WxH[±X±Y]— IM's canvas re-window op. Places the source at
signed offset(x, y)(default(0, 0)) on a freshWxHcanvas,
painting pixels outside the source rectangle with the active
-backgroundcolour (defaulting to opaque white when none was set).
Negative offsets translate the source toward the upper-left so its
right / bottom edge can fit inside the window; sources fully outside
the canvas yield an all-background image (no error, matching IM).
Source-order semantics are preserved: each-extentcaptures the
-backgroundthat preceded it, so a later-backgrounddoes NOT
retroactively repaint earlier extents. Wired through three paths:
the args parser (parse_extentwith full±X±Ygrammar), the
generic pipeline path (video.extentfactory in
oxideav-image-filter), and the inline PDF / mesh3d side-channel
(newpixel_xform::extentworking onRgbaImagewith both RGB-24
and RGBA stride layouts). -
Direct unit tests for the PDF-page → RGBA rasteriser
(pdf_runner::render_page_to_rgba) pinning the
point-to-pixel scaling at multiple DPIs: 72-DPI US Letter
(612 pt → 612 px), 300-DPI US Letter (612 pt → 2550 px,
792 pt → 3300 px), 150-DPI A4 (595 pt → 1240 px, 842 pt
→ 1754 px), last-density-wins on a chained-density
sequence, and an opaque-colour-backgroundpaints every
pixel of an empty page. Round-trips the spec's claim that
PDF page sizes are in PostScript points (1/72 inch). -
End-to-end pdf_to_png test
headline_command_letter_at_300_dpi_emits_2550x3300_rgb_pngs
exercising the literal round-90 invocation
-density 300 input.pdf -background white -alpha remove -alpha off page-%03d.png
on a US-Letter-sized fixture. Asserts both output PNGs are
exactly 2550×3300 and that the PNG IHDR colour-type field
is2(Truecolor RGB), confirming-alpha offactually
drops the alpha channel from the encode rather than
emitting an RGBA buffer withA=255everywhere. -
Comma-separated and negative page-selector atoms on PDF inputs.
convert input.pdf[-1] cover.pngnow writes the last page of the
document;convert input.pdf[0,2,4] page-%d.pngfans out the
user-listed pages in source order;convert input.pdf[0-2,5,-1] ...mixes ranges, singles, and negative offsets in one selector;
convert input.pdf[5--1] ...runs page 5 through the last page.
Atomic specs (PageAtom::Single(isize)/::Range(isize, isize))
carry the signed indices;PageSelector::resolve(total_pages)
maps-ktototal_pages - kand propagates a precise "negative
index out of range" error otherwise. -
op::PageAtomenum + a newPageSelector::List(Vec<PageAtom>)
variant. Single-atom selectors continue to land as
PageSelector::Single/::Rangeso existing pattern-match
callers don't have to walk a one-element list. -
Source order is preserved on resolve (so
[2,0]writes page 2
before page 0); duplicates are retained matching IM
([0,0,0]writes the same page three times). -
13 new unit tests in
args::tests(negative-index parse, negative
range endpoint(s), comma-list parse, single-atom-list collapse to
Single, empty-atom / bare-dash rejection, resolver coverage for
negatives / lists / duplicates / out-of-range propagation) plus 4
new end-to-end tests intests/pdf_to_png.rs(pdf[-1]→ PNG,
pdf[0,1]→ PNG fan-out,pdf[0,-1]→ PNG fan-out,pdf[-5]
out-of-range error). -
.ico(Windows icon) output target.oxideav convert src.png out.ico
writes a 1-entry ICO at the source dimensions; pair with
-define icon:auto-resize=W1,W2,…(IM convention) for a
multi-resolution icon — each comma-separatedWbecomes anW × W
sub-image bilinear-downscaled from the source through
oxideav-image-filter'sResizefactory. The writer follows the
oxideav-icoWriteOptions::default()policy (PNG for sub-images
withmin(w,h) >= 64, BMP otherwise — what Windows 10+ tooling
produces). ICO format limits per-axis dimensions to1..=256;
out-of-range sizes (0,257, …) are rejected up front. -
crate::ico_runnermodule hosts the new side-channel runner.
Decode covers PNG (oxideav_png::decode_png_to_rgba) and BMP
(oxideav_bmp::decode_bmp); other inputs surface a clean
"not yet supported on the ICO writer path — convert to PNG first"
error rather than silently bailing. -
icocargo feature (default-on, sibling tomesh3d). Pulls
oxideav-icoas an optional dep so slim builds (--no-default-features --features generator) drop the side-channel and.icofalls through
to the regular pipeline path (which then errors out as an unknown
sink extension — exactly what it should). -
3D → raster renderer learns two diagnostic visualisation modes
plus N×N supersampling anti-aliasing.-render normal-debug(aliasesnormal/normals) paints each
pixel(n + 1) / 2 * 255per channel — the classic "normal map"
colour-key (positive +X is red-ish, +Y green, +Z blue). Lighting
and material are ignored; useful for verifying the geometry
pipeline / normal-loading path.-render depth-debug(aliasesdepth/z) paints each pixel
a grayscale value derived from the interpolated NDC z (near =
white, far = black). Lighting / material / camera-orbit ignored;
useful for spotting Z-fighting and picking sane near/far planes.-aa NforN ∈ 1..=8rasterises the scene atN × output_w
byN × output_hthen box-filters back down.N = 1is the
round-44 baseline (off);2/4typical;8capped so a
1024² render stays under ~80 MB framebuffer + z-buffer. Stored
onMesh3DOptions::aa; works with every-rendermode (the
debug modes also benefit from cleaner triangle edges).
-
op::Mesh3DRenderMode::NormalDebug/::DepthDebugvariants;
op::Mesh3DOptionsgains anaa: Option<u32>field. -
11 new arg-parser tests (
-render normal-debug/depth-debug+
short aliases; updated unknown-mode error mentions both new modes;
-aafactor parses,1allowed,0/>=9rejected, non-integer
rejected), 9 new renderer unit tests (normal_to_byte/depth_to_byte
endpoint mappings; normal-debug + depth-debug rasterisers paint
expected pixel patterns; AA default / 1× / 2× output dimensions and
edge softening;downsample_boxaveraging on uniform + split fields),
and 4 new end-to-endmesh3d_converttests (-render normal-debug
PNG,-render depth-debugPNG,-aa 4keeps output dims at 64×64,
-aa 1no-op succeeds). -
3D → raster renderer learns Gouraud and Phong shading.
-render gouraudevaluates a Lambert+ambient term at every vertex
(usingPrimitive::normalswhen present, else the face normal
computed from the three triangle positions in world space) and
bilinearly interpolates the resulting RGB across the triangle.-render phonginterpolates the per-vertex normal across the
triangle and evaluates the lighting equation at every pixel —
smoother surface contour at the cost of one normalise + dot per
fragment.-render flatand-render wireframecontinue to behave as
they did in round 43; the default stays Flat.
-
Camera and lighting control on the 3D → raster path:
-light AZIMUTH,ELEVATION,INTENSITY— directional light
override. Default is45°,45°,1.0from the upper-right-front
quadrant; only Gouraud / Phong consult the light (Flat and
Wireframe ignore it).-camera ELEVATION,AZIMUTH,DISTANCE— orbit camera override
(distanceis a multiplier of the auto-framed default). Without
-camerathe renderer keeps the bbox-fit auto-frame.-projection perspective|orthographic— projection mode. Default
is perspective;ortho/oaliases supported.-fov DEGREES— vertical FOV for perspective projection. Default
60°, valid range(0, 180). Ignored for orthographic.-bg COLOR— render-canvas background fill (CSS L3 named or
#hex). Defaults to transparent black. Kept distinct from
-background(which keeps its IM canvas-fill semantics for
-alpha removeand the PDF runner).
-
FBX (Filmbox) wired as a 3D input.
oxideav convert in.fbx out.gltf
now decodes throughoxideav-fbx0.0.1 and re-encodes through the
existing glTF / OBJ / GLB / USDZ encoders. FBX is decode-only —
no encoder ships yet, so.fbxdoesn't appear in the
MESH3D_OUTPUT_EXTSallow-list. -
crate::mesh3d_runner::populate_registry(&mut Mesh3DRegistry)—
workspace-aware registry builder that wraps
oxideav_meta::populate_mesh3d_registryand tacks on
oxideav_fbx::register(meta 0.0.1 predates the FBX publish so its
generated populator doesn't know about it). Once a meta release
knows about FBX, this layer collapses back to a one-liner. -
op::Mesh3DRenderMode::Gouraud/::Phongvariants;
op::ProjectionMode;op::LightSpec;op::CameraSpec. -
op::Mesh3DOptionsgainslight,camera,projection,
fov_deg,bgfields (allOption<…>, defaulting to renderer
baseline behaviour when unset). -
18 new arg-parser tests (
-render gouraud|phong|wire;-light/
-camerathree-component grammar + invalid-input rejection;
-projection orthographic+ aliases;-fovin-range + out-of-range
rejection;-bgnamed / hex / transparent), 4 new renderer unit
tests (gouraud / phong pixel-drawn, render-bg priority, camera +
ortho-projection matrix construction), and 4 new end-to-end
mesh3d_converttests (-render gouraudPNG,-render phongPNG,
the full-projection/-camera/-fov/-light/-bgstack, FBX
extension recognition + decoder hand-off). -
USDZ wired as a 3D-output target.
oxideav convert in.obj out.usdz
now produces a STORED-only PKZip carrying ascene.usdaDefault
Layer — the matching encoder ships inoxideav-usdz0.0.1 (the
workspace populator already pointedMesh3DRegistryat it; only
the runner's allow-listMESH3D_OUTPUT_EXTSwas gating it out). -
3D → raster software renderer.
oxideav convert in.gltf out.png
decodes the 3D asset through the sameMesh3DRegistrythe
STL/OBJ/glTF/USDZ round-trip flow uses, then rasterises the
Scene3Dthrough a tiny built-in software pipeline:- Camera auto-frames the scene's axis-aligned bounding box at
a 60° vertical FOV with a 20% margin, looking down-Z. - Walk every
Node, composing the world matrix top-down; each
Mesh::primitiveis projected, back-face-culled, and rasterised
with a half-space edge-function pipeline + per-pixel z-buffer. - Triangle / strip / fan / line / line-strip / line-loop / point
topologies all dispatch through one tessellation step into the
same triangle-list inner loop. - Flat shading — one constant colour per
Primitive, pulled
frommaterial.base_color(linear → sRGB) or fallback grey when
no material is bound. -render flat/-render wireframeselects shaded vs.
edge-only output. Wireframe draws Bresenham lines on each
triangle's three edges.-resize WxHseeds the canvas dimensions (otherwise 1024×1024);
-background COLORpaints the canvas before rasterisation;
-alpha …and the inline pixel-transform chain
(-rotate/-flip/-flop/-crop/-negate) all run
post-rasterisation, sharing the same plumbing as the PDF
side-channel.- Output codec follows the extension:
.png/.jpg/.bmp/
.webp(the same set the PDF runner emits);-quality N
forwards into the JPEG encoder.
- Camera auto-frames the scene's axis-aligned bounding box at
-
New
mesh3d_rendermodule hosts the renderer; newraster_io
module centralises theRgbaImagehandoff buffer + per-format
encoders so the PDF runner and the 3D-render runner share one
pixel-encode codepath. -
Op::Mesh3DRenderModeenum +-render flat|wireframe|wirearg
(lives onMesh3DOptions::render_mode, parallel to
-stl-format/-gltf-format; silently ignored when the
input/output pair doesn't trigger the 3D→raster path). -
IM-style geometry-modifier suffixes on
-resize(and on the new
-thumbnail):WxH!— force exact dimensions, ignore aspect ratio (was
bang = true; nowResizeMode::Force).WxH^— fill the box: scale both axes by the LARGER of
(req_w/src_w, req_h/src_h)so neither output dim is below the
request. Pairs naturally with a follow-up-cropto land on
exactlyWxH.WxH>— only resize when the input is LARGER thanWxH;
otherwise pass through unchanged.WxH<— only resize when the input is SMALLER thanWxH;
otherwise pass through unchanged.WxH%— interpretWandHas integer percentages of the
source dimensions (50%halves both axes;200x100%doubles
width and leaves height alone). The bareN%form replicates,
matching IM.WxH@— interpretW*Has the TARGET pixel area; both output
dims scale bysqrt(target_area / source_area)so the aspect
ratio is preserved AND the output area matches the request.
-
Op::Resize { width, height, mode: ResizeMode }(wasbang: bool).
The PDF side-channel resolves every mode against the actual source
dims; the regular pipeline path forwards a stable lowercase tag
(default/force/fill/shrink/grow/percent/
area) on the resize filter's JSON params so a future executor
pass can resolve the source-aware variants too. -
-thumbnail WxH[!^<>%@]— IM convenience flag. Same geometry
grammar as-resize. Unrolls into aResizeplusStrippair
(auto-orient is a documented follow-up — needs an EXIF reader on
the source side). -
-define KEY[=VALUE]— opaque codec-specific tunable forwarded
literally onto the sink track'sparamsbag. Keys keep their:
namespace separator (e.g.jpeg:dct-method=floatlands as
params["jpeg:dct-method"] = "float"); bare-define KEY(no
=VALUE) becomesparams[KEY] = true. Multiple-defineflags
all stack onto the same params object. Sink encoders that don't
recognise a key silently ignore it, mirroring IM's
tolerant-of-irrelevant-options posture. -
pixel_xformlearns to honourOp::Resize/Op::Thumbnail
end-to-end (it already knew the source dims, so theFill/
Shrink/Grow/Percent/Areamodes work today on the
PDF side-channel — no waiting on the regular-pipeline executor
pass). -
New
run_image_filter_resizehelper inpixel_xformthat lets
shape-changing filters (Resize) declare the output dimensions
directly, fixing a latent bug whererun_image_filteralways
inherited the input width/height. -
op::ResizeModeenum + helpers (split_suffix,as_tag,
resolve(req_w, req_h, src_w, src_h)) re-exported from the
crate root. -
16 new unit tests covering the geometry parsing (
-resize 200x100^
/100x100>/1024x1024</50x200%/75%/640x480@/
-thumbnail 128x128^/ etc.), the-definegrammar
(KEY=VALUE/ bareKEY/ empty-KEYrejection), and the
pixel-transform chain dispatch (apply_chain_resize_*). -
7 new end-to-end tests in
tests/pdf_to_png.rsproving the
geometry modes,-thumbnail, and-defineall work on the PDF
→ PNG / JPG path with byte-level checks on the output dimensions. -
--probeextensions surfacing more upstream metadata that the
round-40 baseline left on the table:- PDF:
is_encrypted(always present), and the/Infodictionary
fields lifted ontoScene::metadatabyoxideav-pdf—
title,author,subject,keywords,creator,producer,
creation_date,modification_date. Each is reported only when
populated (the absence ofproduceris a real signal — "this PDF
wasn't tagged" — distinct from the empty string).is_encrypted
is read viaDocumentReader::open()+is_encrypted(); ifopen()
itself fails (encrypted PDF with non-empty password we don't have)
the field reportsyesrather than failing the whole probe. - 3D (mesh3d feature): per-mesh, per-material, per-animation
detail (capped at 64 entries each — totals always reported via
mesh_count/material_count/animation_count).meshes[i]:index,name(or(unnamed)),primitive_count,
vertex_count,triangle_count,bounding_box(per-mesh AABB
computed fromPrimitive::positions).materials[i]:index,name.animations[i]:index,name,channel_count,duration_s
(max keyframe time across every channel's sampler).
- Container fallback: container-level
metadatagroup
(title,artist,album, etc. viaDemuxer::metadata()).
Demuxers that carry no metadata produce an empty group so the
field stays predictable.
- PDF:
-
probe::render(plan, ctx) -> Result<String>— same data
probe::runwould print, returned as aStringfor in-process
callers (tests, embedders) that don't want to fight stdout capture. -
tests/probe.rscoverage for the new fields (+2 tests, total 10):
populated/Infodict round-trips through the JSON formatter; STL
per-mesh detail surfacesvertex_count/triangle_count/
bounding_boxand the emptymaterials/animationsarrays are
emitted (not omitted). -
--watchflag (paired with--probe) — re-runs the probe whenever
the input file's mtime changes, polling once per second via
std::fs::metadata+modified(). Each fresh report is emitted in
the same format the one-shot mode would have used; in--jsonform
each report is its own line so the output is JSON-lines-compatible.
The loop runs forever and exits only on Ctrl+C / SIGINT. Soft-
failing re-probes (file truncated mid-write, transient I/O failure)
print a diagnostic to stderr and the loop continues — losing one
frame of output is strictly better than tearing down the watch
session.--watchwithout--probeis a parser-level error
("--watch requires --probe"). -
op::ConvertPlan::probe_watchfield carrying the parsed mode switch. -
probe::runroutes throughrun_watchwhenprobe_watchis set;
the one-shot path is factored intoprint_one. -
"Did you mean?" hint helper —
crate::suggestprovides
closest_matchandformat_hintover Levenshtein edit distance,
cutoffmax(2, len/3). Wired intomesh3d_runner(unknown input /
output extension) andlib.rs(3D-input → non-3D-output mismatch)
so e.g..gtlftypo'd from.gltfproduces
(did you mean '.gltf'?)rather than just listing the supported
set. Among ties the candidate with the closest length wins, so
4-letter typos prefer 4-letter candidates. -
+5 unit testsincrate::suggest(Levenshtein basics, case
insensitivity, distance threshold, length-tiebreak);+3 integration testsinmesh3d_runner(input typo / output typo /
unrelated extension);+3 testsintests/probe.rs(--watch
parses;--watchwithout--probeerrors;--watch --json
parses). -
--probedry-run structural-inspection mode. Decodes the input far
enough to extract metadata (page count, mesh count, sample rate, …),
prints a compact summary to stdout, and skips any output write.
Mutually exclusive with an output positional —--probe in.gltf
is the supported shape;--probe in.gltf out.objerrors at the
parser. Fields surfaced per input class:- Raster image / video / audio: container, codec, width × height,
bit_depth, color_space, alpha presence, frame_rate, sample_rate_hz,
channels, channel_layout, sample_format, duration_s, bit_rate
(best-effort fromStreamInfo/CodecParameters; absent fields
omitted from the output). - PDF: file_size_bytes, page_count, per-page width_pt × height_pt
× orientation_deg (capped at 32 entries; total is always reported
viapage_count), embedded_image_count (walked from the
VectorFrametree). embedded_font_count is reported asunknown
pending an upstream font-resource census. - SVG: file_size_bytes, single-page width_pt × height_pt,
embedded_image_count. - 3D (STL/OBJ/glTF/GLB/USDZ/MTL) (mesh3d feature): mesh_count,
primitive_count, vertex_count, triangle_count (uses
Primitive::triangle_countfor triangle topologies; non-triangle
topologies report 0), material_count, texture_count,
animation_count, skin_count, node_count, root_count, topologies
(per-topology histogram), bounding_box (computed from positions
when not embedded).
- Raster image / video / audio: container, codec, width × height,
-
--jsonflag — pair with--probefor a single-line machine-
readable JSON object instead of the default pretty-printed
key: valueblock. Without--probethe parser rejects--json
with a clear "needs --probe" message rather than silently swallowing
the flag. -
op::ConvertPlan::probe+op::ConvertPlan::probe_jsonfields
carrying the parsed mode switches. -
New
crate::probemodule hosting the structural-inspection runner,
the input-shape decision tree (PDF / 3D / SVG / container fallback),
and a hand-rolled JSON serialiser kept in sync field-by-field with
the pretty-printer so adiffbetween two probes stays meaningful. -
End-to-end coverage in
tests/probe.rs(8 tests): PDF pretty + JSON,
SVG pretty, STL pretty + JSON, mutual-exclusion error,--json-
without---probeerror, no-input error. -
Per-format encoder option flags for the 3D side-channel
(-stl-format ascii|binaryand-gltf-format glb|embedded|external)
with case-insensitive synonyms (bin/text,binary/json-embedded/
json-external). When set, the encoder is constructed directly via
the format crate (oxideav_stl::encoder::StlEncoder::new(StlFormat::…),
oxideav_gltf::GltfEncoder::with_output(OutputFlavour::…)), bypassing
the parameter-less factory closures stored inMesh3DRegistry. The
default code path is unchanged — flag-less convert invocations still
pick up the registry default for every format. -
op::Mesh3DOptionsfield onConvertPlancarrying the parsed
per-format choices; threaded through tomesh3d_runner::runwhich
validates the flag↔output-extension pairing up-front (e.g.
-stl-format asciipaired with a.gltfoutput emits a clear
"set but output extension is '.gltf', not '.stl'" error rather
than silently dropping). -
oxideav-stl/oxideav-obj/oxideav-gltfas direct optional
deps (gated onmesh3d) so the convert verb can call the typed
encoder constructors.oxideav-metaalready pulls them via the
3dfeature so this adds no new transitive crates. -
End-to-end coverage in
tests/format_flags.rs(11 tests):
STL→STL ASCII / binary on-disk byte signatures, glTF JsonEmbedded
via.glbextension and Glb via.gltfextension (override-the-
extension paths), unknown-value parser rejection, mismatched-output
rejection,externalfollow-up surfacing.
Cross-crate follow-ups
-
oxideav-objrN — publish 0.0.1 withObjEncoder::with_negative_indicesobj::SerializeOptions::negative_indices(already on master in this
workspace); needed to wire-obj-negative-indicesthrough convert.
Per-decimal-precision option also needs to be added to
mtl::serialize_mtl/obj::SerializeOptionsfor-mtl-precision N.
-
oxideav-gltfrN — extendOutputFlavourwith aJsonExternal
variant that emits a.gltfJSON document referencing a sidecar
.binfile (encoder needs to return bothVec<u8>blobs or a
caller-side helper that splits them). Once published, swap the
externalarm inmesh3d_runner::build_gltf_encoderfrom
Err(unsupported)to the real flavour. -
3D-asset side-channel —
convert cube.stl cube.obj,
convert model.obj model.gltf,convert scene.gltf scene.glb,
convert archive.usdz extracted.gltf, etc. work end-to-end.
Inputs sniffed by extension (.stl/.obj/.gltf/.glb/
.usdz/.mtl); decode → re-encode runs through a
oxideav_mesh3d::Mesh3DRegistrypopulated by
oxideav_meta::populate_mesh3d_registry. USDZ is read-only today
(decoder registered, no encoder factory). Raster ops (-resize,
-blur, …) are silently ignored for 3D-asset conversions — they
have no pixel grid. -
mesh3d_runnermodule withis_mesh3d_input/is_mesh3d_output
recognisers and arun(input, output)driver that picks decoder /
encoder by file extension and surfaces friendly error messages
("no 3D decoder registered for input extension '.xyz' (known: .stl, .obj, .gltf, .glb, .usdz, .mtl)"). -
mesh3dcargo feature (default-on). Pullsoxideav-mesh3dplus
oxideav-meta = { version = "0.0", default-features = false, features = ["3d"] }so the convert verb only drags the four
format codecs (stl/obj/gltf/usdz) into its dep tree, not every
audio/video/image sibling that meta's defaultallpreset would
bring. Slim builds (--no-default-features --features generator)
drop the side-channel and 3D inputs fall through cleanly. -
3D→non-3D output and 3D +
%d-template inputs are rejected
before decode with a clear, actionable error message. -
End-to-end integration tests in
tests/mesh3d_convert.rscovering
STL→OBJ, STL→glTF (JSON envelope check), STL→GLB (glTFmagic
check), OBJ→glTF round-trip, plus the two error paths above. -
Four more IM colour-grading flags wired to the matching
oxideav-image-filterfactory (registers asvignette/colorize/
equalize/auto-gammaonce the consumer pulls a published
image-filter that includes them; the CLI emits the JSON dialect
unconditionally):-vignette R[+S][+X[+Y]]→video.vignette { x, y, radius, sigma }
(Sdefaults toR/2;X/Ydefault to image centre — passed
as normalised[0.0, 1.0]offsets to stay resolution-independent).-colorize C[xC[xC]]/A%→video.colorize { color: [R,G,B,A], amount }. Colour part accepts CSS L3 named,#hex3/4/6/8, and
IM's per-channelR[xG[xB]]triplet (single-component value
replicates);/A%accepts a percentage or unit-scalar amount.-equalize(no value) →video.equalize {}.-auto-gamma(no value) →video.auto-gamma {}.
-
PDF→raster side-channel now honours the round-37 tonal /
colour-grading ops (-sharpen,-unsharp,-gamma,
-brightness-contrast,-contrast,-sepia,-modulate,-level,
-normalize,-threshold,-posterize,-solarize,
-colorspace gray|grey).pixel_xform::apply_pixel_transform_chain
dispatches to the matchingoxideav-image-filterconstructor on the
rendered RGBA buffer before alpha-grammar / encode, keeping PDF
inputs pixel-identical to non-PDF inputs at the same op chain.
Locked down by an integration test that decodes a-sharpen 1x0.5
PDF render and asserts byte-for-byte match against the standalone
Sharpenfilter applied to the plain render. -
RgbaImage::is_rgbis nowpub(was private topdf_runner) so the
pixel_xformtonal dispatch can pick the rightPixelFormattag. -
IM-style tonal / colour-grading flags wired through to the matching
oxideav-image-filterfactory via awrap(chain, "video.<name>", json!(…))step on the regular pipeline path:-sharpen RxS→video.sharpen { radius, sigma }.-unsharp RxS+amount+threshold→
video.unsharp { radius, sigma, amount, threshold }.-gamma G→video.gamma { value }(G > 0enforced).-brightness-contrast B[,C](alsoBxC) →
video.brightness-contrast { brightness, contrast }(each in
[-100..=100]).-contrast(no value) →video.contrast { value: 5.0 }per
flag — repeated-contrastchains accumulate.-sepia N%/-sepia 0.5→video.sepia { threshold }.-modulate B[,S[,H]]→video.modulate { brightness, saturation, hue_degrees }with IM's
0..200-around-100hue translated to±180°.-level B[/G[/W]](alsoB,G,W; black/white acceptNorN%)
→video.level { black, gamma, white }(gamma > 0; black ≤ white).-normalize→video.normalize {}.-threshold N/-threshold N%→video.threshold { value }.-posterize N(N >= 2) →video.posterize { levels }.-solarize N/-solarize N%→video.solarize { value }.-colorspace gray|grey→video.grayscale { preserve_alpha: true };
-colorspace rgb|srgbis recorded as a no-op (input keeps its
colourspace). Other colourspaces continue to error cleanly.- The previously inline-only round-1 ops (
-rotate,-flip,-flop,
-crop,-negate) now also wire through to the matching
video.rotate / .flip / .flop / .crop / .negatefactories on the
regular pipeline path so non-PDF inputs honour them too. The PDF
side-channel still applies them viapixel_xformfor parity.
-
Argument parsers for each new flag with friendly error messages
(out of range,must be > 0,levels must be >= 2,
not yet wired, …). -
Round-trip tests in
plan_to_job::tests:- JSON-shape coverage for every newly-wired op (per-key value
assertions on the emittedFilterNode.params). - End-to-end registry-build coverage that hands the CLI-emitted JSON
back to aRuntimeContextcarryingoxideav_image_filter::register
— proves the JSON dialect matches the factory's parameter schema. - Pixel-exact match: synthesise a 4×4 RGBA edge fixture, run it
through bothSharpendirectly and the registry-built filter, and
assert byte-for-byte equality. The registry-build assertions
skip silently when the linked image-filter pre-dates the
round-next factories (published0.1.1only registers
blur/edge/resize), so the test suite stays green
through the producer-publishes-consumer-lands cycle.
- JSON-shape coverage for every newly-wired op (per-key value
-
Inline geometry / negate ops on the PDF→raster path:
-rotate NforN ∈ {±90, ±180, ±270}(other angles rejected
cleanly with "only multiples of 90 supported (got N)"). 90/270
swap output width and height.-flip— vertical flip (rows reversed).-flop— horizontal flip (columns reversed within each row).-crop WxH+X+Y— extract aWxHbbox at offset(X,Y). Bbox
that overruns the input dims errors with "bbox WxH+X+Y exceeds
input W'xH'". IM geometry modifiers (%,!,^,<,>,@)
are rejected with a clear "round-1 supports plain WxH+X+Y only"
message.-negate— per-pixelout = 255 - inon the colour channels;
alpha (when present) is unchanged.
-
pixel_xformmodule exposing the underlyingflip,flop,
negate,rotate,crop, andapply_pixel_transform_chain
primitives so callers can drive the same transforms directly on an
RgbaImage. -
RgbaImageis nowpub(waspub(crate)) so external callers can
build inputs for the pixel-transform primitives. -
-pingflag — read only the headers of the input and print one
IM-format line per "image" (page / video stream) to stdout, then
exit without decoding pixels or writing any output. Output line
shape:<path> <FORMAT> <W>x<H> <W>x<H>+0+0 <DEPTH>-bit <COLORSPACE> <BYTES>B. Multi-page PDFs emit one line per selected
page with[N]suffix. Output positional becomes optional when
-pingis on. -
pingmodule with aPixelFormat→ (depth, colorspace) mapping
table covering the common cases (8/10/12/16-bit YUV, RGB, Gray,
CMYK, palette, mono). -
ConvertPlan::ping: bool.