Repository navigation
v0.0.6
Added
- (convert) frame tap — --opt encoder options, .avif => codec=av1, %d per-frame fan-out
- (convert) still-image sink planning — primary-still stream pin + lossless-narrowest encoder layout
- (convert) --help / -help usage synopsis generated from the parser's flag table
- (convert) arg-parsing hardening — '--' end-of-flags, empty-arg rejection, flag-table sync test
- (convert) golden plan snapshots + noise-free f32 JSON + shared op-lowering
- (convert) schema-validation guarantee on every emitted job + JSON round-trip pinning
- (convert) extract pure routing layer with matrix-pinned dispatch
- (convert) plan_to_render3d_job() for 3D→raster auto-route (Phase C-3b)
Fixed
- (convert) -thumbnail seeds the 3D render canvas + %d template rejected on non-fanout routes
Other
- (convert) where the time goes on 12 MP HEIC -> PNG — stage attribution table, before/after, remaining owners
- (convert) pin still-image sink output content before the r463 image-path work
- hide internal pub surface from rustdoc/semver (fleet rule 2026-09-01)
- (convert) fix two stale README claims against current behaviour
- (convert) adversarial-argv no-panic corpus + CHANGELOG rollup + planning-guarantees docs
- follow oxideav-pdf 0.2 (major-bump pin propagation)
- add CI / crates.io / docs.rs / MIT-license badges
- refresh to current status, drop per-round changelog cruft
- drop release-plz.toml — use release-plz defaults across the workspace
Added
-
Still-image sink planning (
image_sinkmodule). When the output
resolves to an image encoder and the input's container header can be
read, the planner (a) pins the track to one video stream — HEIF /
AVIF files carrying an image-sequence track open as stream 0 = the
primary still plus one stream per track, and the single-image muxers
used to refuse the second stream (.heics/.avis→.pngnow
writes the primary still, with a stderr note); and (b) for a plain
conversion, pins the encoder input to the accepted layout that loses
nothing the source carries (colour, alpha, bit depth) at the fewest
bits per pixel, via an explicit convert node. YUV → PNG is now RGB
instead of RGBA with a synthetic opaque alpha (identical colour
samples, 25% less data through deflate: 12 MP 8-bit HEIC → PNG CPU
5.7 s → 4.4 s); >8-bit YUV → PNG keeps its precision as 16-bit RGB. -
Frame tap (
frame_tapmodule) —convert's own encode + mux for
still-image outputs, fed decoded frames by the executor through the
reserved@outsink. Used when the plan needs it:--opt KEY=VALUE(repeatable): encoder options, validated at plan
time against the target encoder's declared options schema
(unknown key → typed error listing the known ones; values checked
against the declared kind) and handed to the encoder. The
pipeline's trackcodec_paramsnever reach the encoder, so this
is the only working option channel on the convert path..avif/.avifsoutputs infercodec=av1for theheif
encoder (they were written as HEVC with aheicbrand).%doutput templates on the pipeline route fan out one file per
frame (0-based): image sequences (the first non-still stream —
HEIF / AVIF put the primary still at stream 0), animations,
video. Filter ops compose; the frame geometry after them is read
off the frames.
Same encoder settings produce byte-identical files on both paths.
-
OXIDEAV_CONVERT_TIMING=1— per-stage wall-clock spans on stderr
for the pipeline route (plan incl. header probe, executor; on the
frame-tap path first-frame arrival, encode, mux + write). -
-depth 8|16— per-channel output depth for still-image sinks
(a 10-bit HEIC →-depth 8→ 8-bit PNG; 12 MP: 3.6 s CPU). -
Netpbm extensions pick their family:
.pbmbilevel,.pgm
grayscale,.ppmcolour (no alpha);.pamkeeps any layout. -
routemodule — pure routing layer.route::decide(&ConvertPlan) -> Result<Route, Error>classifies a plan into its execution path
(Help/Ping/Probe/PdfSideChannel/Mesh3dToMesh3d/
Mesh3dToRaster/IcoOutput/Pipeline, feature-gated variants
matching the cargo features) without performing any work;run()
is now a thin dispatcher over it.Routeis re-exported at the
crate root. The full media-kind × media-kind dispatch matrix is
pinned by IO-free unit tests, including precedence rules (help >
ping > probe > input classification > output classification). -
--help/-help— usage-synopsis mode, recognised anywhere on
the line, outranks every other mode.args::usage()generates the
flag list from the parser's ownKNOWN_FLAG_NAMEStable (correct
dash flavour per flag) so the help text cannot drift from what the
parser accepts.ConvertPlangrows ahelp: boolfield. -
--end-of-flags marker — everything after it is a positional even
when it starts with a dash, so files literally named-weird.png
are expressible. -
Schema-validation guarantee:
plan_to_joband
plan_to_render3d_jobrun the pipeline's ownJob::validate()
before returning, so planner bugs surface as typed errors at plan
time. Pinned by tests over a 33-entry representative op corpus,
together with a byte-stable JSON round-trip guarantee (serialise →
re-parse → re-serialise identical) that makes convert-planned jobs
storable / replayable asoxideav rundocuments. -
Golden plan snapshots (
tests/plan_golden.rs): byte-exact job JSON
pinned for representative argv lines end-to-end (bare transcode,
filter chain with sink params,-monochromeunroll, codec-less
video container, full-option 3D render), plus determinism checks. -
Flag-table sync test: every
KNOWN_FLAG_NAMESentry is driven
through the parser with a minimal valid value and must parseOk,
in both directions (stale table entry fails; accepted flag missing
from the table fails). -
Adversarial argv corpus:
args::parsemust return typed errors,
never panic, across malformed geometry / selectors / colours /
numbers / unicode / oversized tokens. -
plan_to_render3d_job(plan, ctx) -> Result<Job, Error>— public
helper that lowers a 3D-assetConvertPlaninto an
oxideav_pipeline::Jobwhose track input is a
TrackInput::Render3Dsource node. The pipeline executor hands the
source URI +backendname + opaqueoptsJSON to a caller-installed
RenderSourceFactory; cli-convert exposes the helper so the
consumer-side wiring (next-phase CLI dispatch) can route 3D inputs
through the same pipeline executor the regular pipeline path uses,
rather than the legacy side-channel runner.Op::Resizeand
Op::Backgroundare absorbed by the renderer (the canvas is produced
at the requested size directly with the requested clear colour);
every other op survives as a filter node wrapping the Render3D
source, so the post-rasterisation tonal / geometry ops keep their
existing JSON dialect.Mesh3DOptions::bgwins over anOp::Background
in source order, mirroringmesh3d_render::pick_render_bg's
precedence. TheoptsJSON shape mirrors
oxideav_render::RenderOptionsone-for-one (width,height,
background,shading,projection,fov_deg,light,camera,
aa) so the consumer'sRenderSourceFactorycan
serde_json::from_valueit straight into the typed struct. -
RENDER3D_DEFAULT_BACKEND— public&'static strconstant naming
the default backend ("scanline") handed toRender3DNode::backend.
Mirrors the onlyRenderBackendvariant the workspace ships today;
when raycast / pathtrace backends land, a-render-backend NAME
arg will override the default via the same field. -
15 new unit tests in
plan_to_job::testscovering: default plan +
options producing a 1024×1024 scanline source with transparent black
background;-resize WxHabsorbed into render dims (no surviving
video.resizefilter);-background COLORabsorbed into render
background (no surviving filter);-bgoption overriding
-backgroundop; everyMesh3DRenderModeround-tripping through
opts["shading"]as a stable lowercase tag (withnormal-debug/
depth-debugdash-cased to match the existing JSON dialect);
ProjectionModeround-trip;-lighttriple round-trip;
-cameratriple round-trip;-fov DEGREESround-trip;-aa N
round-trip with clamping to1..=8(matching
RenderOptions::validate); post-rasterisation ops (rotate / negate
/ sharpen / strip / quality) surviving as filter nodes wrapping the
Render3D leaf in source order;Op::Thumbnailabsorbing the resize
half and setting strip-metadata on codec params; output codec
resolving throughContainerRegistry;-format FMToverride
threading through to codec params;-define KEY=VALUElanding on
codec params; theoptspayload carrying every documented field.
Fixed
-
YUV sources →
.ppm/.pgm/.pamno longer come out as a 1-bit
thresholded bitmap: the pipeline's default picked the Netpbm
encoder's first accepted layout (MonoBlack) for any source it did
not accept as-is. -
.jpg/.jpegoutputs resolve to themjpegcodec (thejpeg
container's codec id) instead of failing with "codec not found:
jpeg". -
f32 op values no longer widen to f64 bit-patterns in the emitted
job JSON:-light 10,20,0.9used to serialise as
"intensity": 0.8999999761581421,-blur 1x2.3as
"sigma": 2.299999952316284. Values now serialise via the f32's
shortest decimal representation (the number the user typed);
-modulatehue→degrees translates in f64 on the clean decimal so
round percentages give round degrees (150 → 90.0). -
-thumbnail WxHnow seeds the 3D render canvas on both the
auto-route planner (pick_render3d_dims) and the side-channel
renderer (mesh3d_render::pick_dims); previously the geometry was
absorbed but the dims dropped, silently rendering at the 1024×1024
default. Last geometry op (-resizeor-thumbnail) in source
order wins. -
A
%dprintf template on the regular pipeline or.icoroutes is
now a typedunsupportederror instead of being written through as
a literal output filename (template fan-out only exists on the PDF
side-channel today). -
Empty positional arguments (typically an unset shell variable) are
rejected at parse time with an actionable message instead of
surfacing as an unopenable""filename deep inside a runner; an
empty input path is also rejected at plan time by the new
validation hook.
Changed
- The ~340-line op-lowering walk duplicated between
plan_to_joband
plan_to_render3d_jobis now one sharedlower_op(with an
absorb_canvasflag for the render path) plus a shared
finish_jobtail — float handling and future op additions happen
in exactly one place.