Language bindings
Inkvec is a Rust library first. Every other language reaches the same pipeline through one of these, each a thin wrapper over the facade or the C ABI:
| Layer | Where | For |
|---|---|---|
inkvec crate (the facade) |
crates/inkvec |
Rust; the API every binding calls |
C ABI: inkvec_ffi.dll / libinkvec_ffi.so / libinkvec_ffi.dylib, static inkvec_ffi.lib / libinkvec_ffi.a, header include/inkvec.h |
crates/inkvec-ffi |
C, C++, and every language with a C FFI: Java (JNA), C# (P/Invoke), Swift, Ruby, PHP |
Python package inkvec |
crates/inkvec-py |
Python 3.9+, abi3 wheels built with PyO3 and maturin |
npm package @logolabs/inkvec |
packages/npm over crates/inkvec-wasm |
JavaScript and TypeScript: browsers, Node.js, Deno, Bun; a single-threaded and a threaded WebAssembly build |
Maven package com.logolabs:inkvec |
packages/java over the C ABI |
Java 8+, a JNA binding |
NuGet package LogoLabs.Inkvec |
packages/dotnet over crates/inkvec-ffi |
.NET: netstandard2.0 (.NET Framework 4.6.1+, Unity) and net8.0, P/Invoke |
HTTP service inkvec-server, as a Docker image |
crates/inkvec-server, services/docker/ |
Any language, over HTTP: POST an image, get SVG back |
Go module github.com/logolabs/inkvec-go |
packages/go over the C ABI compiled to wasm32-wasip1 |
Go 1.25+, pure Go through wazero: no cgo, no native library |
Swift package Inkvec, from the mirror github.com/logolabs/inkvec-swift |
packages/swift over the C ABI |
Swift 5.9+: macOS and iOS through a prebuilt XCFramework, Linux against libinkvec_ffi |
Composer package logolabs/inkvec |
packages/php over the C ABI |
PHP 8.1+ through ext-FFI, the library called in-process; preloadable for PHP-FPM |
The WebAssembly crate (crates/inkvec-wasm) calls the facade: trace_json and
trace_rgba_json take the options as JSON, default_options_json and options_schema_json
read them back. Its older positional trace export is what the web demo (web/worker.js) still
calls; it is kept until the page moves over.
It also exposes the denoiser pre-pass for a page that runs the network itself, which the web
demo does through ONNX Runtime Web: prepare returns an Intake — the pipeline stopped where
the restorer sits — whose denoiser_input / take_denoiser_output are inkvec_restore's own
network_input / network_output, whose residual is inkvec_restore::decide's signal for
--restore auto, and whose trace finishes the job with soft intake forced. trace is
prepare(...).traceOnce() with nothing in between. denoiser_model_url,
denoiser_model_sha256 and denoiser_threshold are the constants the CLI uses, so a page
cannot end up fetching or trusting a different model.
One source of truth
Nothing about an option is written twice.
inkvec::Options(crates/inkvec/src/options.rs) defines every option: its name, type, default (taken from the command line'sArgs::default()), doc comment and range. It derivesserdeandschemars::JsonSchema.bindings/options.schema.jsonis generated from it (JSON Schema draft 2020-12) and committed. Validation (Options::validate) reads its ranges back from the same schema, so the documented range and the enforced range are one attribute on the field.- Every binding takes options as a JSON object and hands it to
Options::from_json: the C ABI takes a JSON string, the Python package serialises its keyword arguments. No binding contains per-option code, so none of them change when an option does. - Typed surfaces are generated from the schema by
bindings/codegen/: the Python type stub, the npm package's TypeScriptOptions, the Go module'sOptionsstruct, and the options tables in this file and the READMEs. bindings/contract/holds inputs, options and what every binding must produce for them: the reported size or error everywhere, and the SVG byte for byte per build target.
Adding, changing or removing an option
- Add the field to
Optionsincrates/inkvec/src/options.rs, with a doc comment (it becomes the description in every language) and, for a number, its range (#[schemars(range(min = .., max = ..))]or#[schemars(extend("exclusiveMinimum" = 0))]). Give it its default inDefault for Optionsand map it inOptions::to_args. The compiler refuses to build until both are done, and a unit test fails if the option reaches nothing. cargo test -p inkvec-- regeneratesbindings/options.schema.jsonand fails once so the change is seen; run it again and it passes.python bindings/codegen/generate.py-- regenerates the Python stub, the TypeScript types and the options tables.- If the change alters output for the contract inputs:
INKVEC_BLESS=1 cargo test -p inkvec --release --test contract. - Commit. The C header, the C library's API, the Python module and every other binding are untouched.
CI fails on any stale artifact: the schema test, python bindings/codegen/generate.py --check,
and cbindgen --verify for the header.
Adding a language
A binding for a new language needs three things, all driven by files in this repository:
- Calls. Wrap the C ABI (
include/inkvec.h) or, for WebAssembly, the facade. Pass options as one JSON object; do not restate them. - Types. Add a generator to
bindings/codegen/-- a module withrender(options) -> {path: text}that reads the options throughschema.load()(seepython_stub.py) -- and list it ingenerate.py.schema.load()refuses an option type it has not been taught, so a new kind of option fails loudly rather than rendering wrongly. - Tests. Run every case in
bindings/contract/cases.jsonand compare with itsexpectblock.
The shared contract
Input
Two forms, in every language:
- Encoded image: the bytes of a PNG, JPEG, WebP, GIF, BMP or TIFF file. The container is read as well as the pixels: JPEG and lossy WebP are traced with the noise-aware intake the command line uses for them.
- Raw pixels: straight (not premultiplied) RGBA, 8 bits per channel, row-major, tightly
packed, exactly
width * height * 4bytes. The result is byte-identical to the encoded form on a PNG holding the same pixels.
Options
All optional; a missing option takes its default, which is the command line's. An unknown name,
a value of the wrong type, a non-finite number or a value out of range is an invalid_options
error naming the option.
| Option | Type | Default | Range | Meaning |
|---|---|---|---|---|
precision |
number | 0.1 |
> 0 | Sets the description-length cost of a coordinate, in pixels: lambda = ln(extent / precision). Smaller values buy more detail with more coordinates. It does not set the digits written; coordinates are always written at 2 decimals. |
min_area |
number | 2.0 |
> 0 | Discard features smaller than this area, in square pixels. |
colors |
integer | 64 |
>= 1 and <= 4096 | Maximum palette size. |
merge |
number | 0.035 |
>= 0 | OKLab distance below which two colours are treated as one ink. |
max_dim |
integer | 2048 |
- | Inputs larger than this on their longer side, in pixels, are traced at this size and the SVG is written at the original size. Trace time grows with the pixel count. 0 means no cap. |
time_budget |
number | 0.0 |
>= 0 | Advisory wall-clock budget, in seconds; 0 means none. Gradient-band merging stops at 60% of it and the boundary solve gets 25%; the output is still a correct trace, with more fills or a less polished outline. A nonzero budget makes the output depend on machine speed and load, so it is no longer reproducible. |
margin |
number | 0.0 |
>= 0 | Transparent margin around the output, as a fraction of the larger side. The viewBox grows; the geometry does not move. |
no_background |
bool | false |
- | Knock the background out: the face that covers the whole canvas is not painted, so the artwork sits on transparency. |
minify |
bool | false |
- | No ids or groups, no trailing zeros. Same geometry, typically about a tenth smaller. |
editability |
bool | false |
- | Spend parameters on structure an artist can edit: joins between curves made G1-smooth, handles snapped to the axes and to 45 degrees, handles of one curve made equal in length, nodes that nearly share a coordinate made to share it, and rings that are their own mirror image locked into exact mirrors. Every change is guarded to the fit's own tolerance -- 3 sigma of the source point plus half a pixel, or 1.5 px for a mirror lock -- so the picture stays within a fraction of a pixel of the default trace; the price measured on 25 icons is about 0.04 dE00. Off by default. |
native_alpha |
bool | true |
- | Trace transparency natively: each ink is a colour and an opacity, and the transparent ground is an ink of its own, instead of the image being composited onto a matte first. Holes stay holes, white artwork on a transparent ground traces, glows and shadows stay translucent, and a fade is one gradient of colour and opacity. An opaque input traces the same either way. On by default, as on the command line (where the environment variable INKVEC_NATIVE_ALPHA=0 turns the default off); false composites onto a matte first, as releases up to 0.1.3 did. |
cutout |
bool | false |
- | With native_alpha off, carry the input's transparency into the SVG: a face the source drew transparent becomes a hole, one drawn at a single opacity keeps it as fill-opacity, and white artwork on a transparent ground survives. Changes nothing for an opaque input, and nothing with native_alpha on (the default), which already carries the transparency out. |
content_units |
bool | false |
- | Scale the fit tolerances with the raster, so a large, simple drawing gets the parameter count of a small one. Trades fidelity for parsimony: small squares can come back as circles and thin rings broken. |
harmonize |
bool | true |
- | Shape harmonization (on by default): marks that repeat across the drawing are redrawn from one consensus geometry per cluster, which saves parameters. A mark takes the consensus only where that stays within 0.1 px of the boundary traced for it and costs fewer parameters; a face another face is drawn against, and a fitted circle or rounded rectangle, is never moved. Set it to false to skip the pass. |
harmonize_threshold |
number | 0.92 |
>= 0 and <= 1 | Shape-equivalence threshold for harmonization: the outline similarity (IoU after affine normalisation) above which two marks count as the same shape. |
merge_colors |
string | "" |
- | Colour groups: fills to draw as one, so the shapes between them join rather than being recoloured. Empty (the default) changes nothing. Groups are separated by ';' and members by ','; a member is a colour '#rrggbb' as it appears in a trace of the same image, or a gradient written as its stop colours joined by '>'. An optional '=' says what the group becomes: '=#rrggbb' a flat colour, '=@n' its n-th member (1-based; a gradient there is refitted over the whole group); without it, the member covering the most of the image. Example: '#c0392b,#e74c3c;#f00>#00f,#0a0=@1'. A group costs one extra trace. |
Output
The SVG document (UTF-8) and the input's width and height in pixels. The SVG's width and
height attributes carry the same numbers unless margin grew them. Its viewBox can be a
smaller coordinate space than the presented size when the input was reduced for tracing
(max_dim, or an exact pixel-block upscale that was undone).
Errors
| Kind | Rust | C status | Python |
|---|---|---|---|
invalid_image -- not a decodable image, or pixels that do not match the size given |
Error::InvalidImage |
INKVEC_ERR_INVALID_IMAGE (2) |
InvalidImageError |
invalid_options -- unknown option, wrong type, out of range |
Error::InvalidOptions |
INKVEC_ERR_INVALID_OPTIONS (3) |
InvalidOptionsError |
internal -- the pipeline failed or panicked |
Error::Internal |
INKVEC_ERR_INTERNAL (4) |
InternalError |
invalid call -- NULL pointer, struct_size too small |
- | INKVEC_ERR_INVALID_ARGUMENT (1) |
TypeError |
Every error carries a human-readable message. No panic crosses a language boundary.
Threading
Every entry point may be called from any number of threads at once. The pipeline is itself parallel (rayon, one global pool per process). Python releases the GIL for the whole trace.
Determinism
On one build target, the same input and options give byte-identical SVG across runs, thread counts and languages, with two exceptions:
- a nonzero
time_budgetstops stages by wall clock, so the output depends on machine speed and load; - the tracer reads a number of
INKVEC_*environment variables as research switches; leave them unset.
Across build targets the bytes can differ. The pipeline takes a few transcendental functions
(cube roots, trigonometry, logarithms) from the platform's maths library, whose last-bit
rounding differs between, for example, glibc and the MSVC runtime, and a last-bit difference can
tip a near-tie downstream. Measured on the 96 px contract sample: x86_64-linux-gnu and
x86_64-windows-msvc write the same shapes, but one compound path lists its subpaths in a
different order (9003 bytes against 8991); the two 64 px samples come out identical. Every
binding reports the target it was built for -- inkvec::build_target(),
inkvec_build_target(), inkvec.build_target() -- and the contract records hashes per target.
The maths library is the system's C runtime (glibc, the Windows UCRT, Apple's libm), loaded at
run time, so two versions of it could in principle differ as well; that has not been measured. The committed
x86_64-linux-gnu hashes were recorded on Debian 12 (glibc 2.36).
Transparency (native, on by default)
Every ink is a colour and an opacity, and the transparent ground is an ink of its own, so holes
stay holes, a translucent panel keeps its fill-opacity, white artwork on a transparent ground
traces, and a glow or fade is one gradient of stop-color and stop-opacity. An opaque input
traces the same either way. native_alpha set to false composites onto a matte first, as
releases up to 0.1.3 did; cutout matters only then. The default is the command line's, so,
as there, the environment variable INKVEC_NATIVE_ALPHA=0 turns it off in a process that sets
it (and makes cargo test -p inkvec report the schema as changed).
Shape harmonization (on by default)
Marks that repeat across the drawing -- a run of identical tabs, segmented rings, tiled glyphs
-- are matched by affine-normalised outline similarity (harmonize_threshold, default 0.92) and
redrawn from one consensus geometry per cluster, which saves parameters. Each mark is held to
its own evidence: it takes the consensus only where that lands within 0.1 px of the boundary
traced for it and costs fewer parameters, and a face another face is drawn against, or a fitted
circle or rounded rectangle, is never moved. On the 246-icon screen set it changes 2 icons, both
cheaper and neither worse. Releases up to 0.1.3 had no such guard, and there it raised mean
dE00 on that set from 0.151 to 0.299. Set harmonize to false to skip the pass.
Not included
The command line's optional neural pre-passes -- the trained restorer (--restore) and the
super-resolution pre-pass (--sr) -- need model weights and an ML runtime or an external
process, and are in no binding. Neither are the command line's research switches (--tau,
--bilevel, --strokes, --layers, ...); each becomes available everywhere by adding it to
Options as above.
Quick starts
Rust
[dependencies] inkvec = "0.1"
let png = std::fs::read("logo.png")?; let mut opts = inkvec::Options::default(); opts.colors = 16; let traced = inkvec::trace(&png, &opts)?; std::fs::write("logo.svg", &traced.svg)?; // Options as JSON, validated, exactly as the other bindings pass them: let opts = inkvec::Options::from_json(r#"{"colors": 16, "no_background": true}"#)?; let traced = inkvec::trace_rgba(&rgba, width, height, &opts)?;
Options is #[non_exhaustive]: start from Options::default() and assign fields.
C
Link against the library from a release archive (or cargo build --release -p inkvec-ffi, which
writes target/release/inkvec_ffi.dll + inkvec_ffi.dll.lib, or libinkvec_ffi.so /
libinkvec_ffi.dylib, and the static inkvec_ffi.lib / libinkvec_ffi.a), with
crates/inkvec-ffi/include on the include path. The library is inkvec_ffi rather than
inkvec because on Windows its debug-symbol file would collide with the command line's
inkvec.exe in one build directory.
#include "inkvec.h" InkvecResult r = INKVEC_RESULT_INIT; /* zeroed, struct_size recorded */ int status = inkvec_trace(png, png_len, "{\"colors\": 16}", &r); if (status == INKVEC_OK) fwrite(r.svg, 1, r.svg_len, out); else fprintf(stderr, "inkvec: %s\n", r.error); inkvec_result_free(&r); /* always, success or not */
- Options: a JSON object, or NULL /
""for the defaults.inkvec_options_schema()andinkvec_default_options()return the schema and the defaults as static JSON strings. - Ownership:
r.svgandr.errorbelong to the library untilinkvec_result_free. The strings frominkvec_version,inkvec_build_target,inkvec_options_schemaandinkvec_default_optionsare static. - Forward compatibility:
struct_sizelets a later library append fields toInkvecResultwithout writing past an older caller's struct.inkvec_abi_version()changes only on a breaking change. - Linking the static library needs the platform's system libraries as well. Measured with
cargo rustc --release -p inkvec-ffi --crate-type staticlib -- --print native-static-libs: on x86_64 Linux-lgcc_s -lutil -lrt -lpthread -lm -ldl -lc, on Windows (MSVC)kernel32.lib ntdll.lib userenv.lib ws2_32.lib dbghelp.lib. Run the same command for any other target. inkvec_build_target()names the target the library was built for (see Determinism), andinkvec_abi_version()the ABI.
A complete program is crates/inkvec-ffi/examples/c/trace.c.
Python
pip install inkvec
import inkvec traced = inkvec.trace("logo.png", colors=16) # path, bytes, file, PIL, numpy open("logo.svg", "w").write(traced.svg) traced = inkvec.trace_rgba(rgba_bytes, width, height) # or a numpy (H, W, 4) array inkvec.defaults() # every option at its default inkvec.options_schema() # the JSON Schema
The keyword arguments are typed in the stub (inkvec/__init__.pyi, generated). Errors derive
from inkvec.InkvecError.
JavaScript and TypeScript
npm install @logolabs/inkvec
import { trace, traceRGBA, defaults, optionsSchema } from "@logolabs/inkvec"; const svg = await trace(bytes, { colors: 16 }); // Uint8Array, Buffer, ArrayBuffer, Blob const svg2 = await traceRGBA(ctx.getImageData(0, 0, w, h)); // or (pixels, width, height, options)
One ES module entry for browsers, Node.js, Deno and Bun; @logolabs/inkvec/threads is the
threaded build (cross-origin isolated pages inside a Web Worker, or Node.js worker_threads).
The option names are the schema's, typed by the generated Options interface. Errors are
InkvecError with code set to the error kind, plus load_failed for a WebAssembly module
that could not be loaded. See packages/npm/README.md.
Java
cargo build --release -p inkvec-ffi # then: mvn -f packages/java/pom.xml install
TraceResult r = Inkvec.trace(png, InkvecOptions.builder().colors(16).build()); System.out.println(r.svg());
A JNA binding over the C ABI (inkvec_ffi):
Java 8 or later, one dependency. InkvecOptions is a generated, immutable builder; every
method also takes options as a raw JSON string. Errors are InkvecException subclasses
(InvalidImageException, InvalidOptionsException, InternalException). Not published to
Maven Central, and not planned to be: build it from the repository, or take the jar the
java workflow attaches to its runs, which carries all five platforms' native libraries.
See packages/java/README.md.
C
dotnet add package LogoLabs.Inkvec
using LogoLabs.Inkvec; var traced = Inkvec.TraceFile("logo.png", new InkvecOptions { Colors = 16 }); File.WriteAllText("logo.svg", traced.Svg);
P/Invoke over the C ABI, netstandard2.0 (.NET Framework 4.6.1+, Unity) and net8.0
(LibraryImport source generation there, DllImport on netstandard2.0, one shared
declarations file). InkvecOptions is generated from the schema, same as every other
binding's typed surface; a raw-JSON overload of Trace/TraceRgba reaches an option before
it has been regenerated for. Errors are InvalidImageException, InvalidOptionsException and
InkvecInternalException, all deriving from InkvecException. See packages/dotnet/README.md.
Swift
// Package.swift: .package(url: "https://github.com/logolabs/inkvec-swift", from: "0.1.3") import Inkvec let traced = try Inkvec.trace(contentsOf: url, options: InkvecOptions(colors: 16)) let fromPixels = try Inkvec.traceRGBA(rgba, width: w, height: h) // or a CGImage, UIImage, NSImage let raw = try Inkvec.trace(png, optionsJSON: #"{"no_background": true}"#)
packages/swift wraps the C ABI: options are encoded to one JSON object, errors are
InkvecError (invalidImage, invalidOptions, internal). InkvecOptions -- optional
camelCase properties, nil meaning the default -- and the package's copy of inkvec.h are
generated by bindings/codegen/swift.py. Apple platforms (macOS 10.15+, iOS 13+) get the
library as a static XCFramework from the mirror github.com/logolabs/inkvec-swift, whose
manifest packages/swift/scripts/render_mirror.py writes; on Linux the same package links
libinkvec_ffi as a system library. The tests run the contract through the typed and the
raw entry points. See packages/swift/README.md.
Go
go get github.com/logolabs/inkvec-go
traced, err := inkvec.Trace(ctx, png, &inkvec.Options{Colors: inkvec.Ptr(16)}) traced, err = inkvec.TraceRGBA(ctx, img.Pix, w, h, nil) // straight RGBA8 traced, err = inkvec.TraceJSON(ctx, png, `{"colors": 16}`) // options passed through errors.Is(err, inkvec.ErrInvalidOptions) // ErrInvalidImage, ErrInternal
Pure Go: the C ABI compiled to wasm32-wasip1 by tools/build_go_wasm.sh, embedded
(//go:embed) and run by wazero, one module instance per concurrent call; no cgo. The
Options struct (pointer fields, nil for the default) is generated by
bindings/codegen/golang.py. One trace runs on one core, about twice the native
single-threaded time. The build rewrites the module's floating-point selects as integer
ones (tools/wasm_float_select.py) around a wazero amd64 compiler bug that returned the wrong
operand. go get cannot run a build step, so users get the module from the mirror repository
github.com/logolabs/inkvec-go, which .github/workflows/go.yml fills from packages/go with
the compiled inkvec.wasm on a v* tag; here the .wasm is built, not committed. See
packages/go/README.md.
PHP
composer require logolabs/inkvec vendor/bin/inkvec-fetch-library # libinkvec_ffi for this platform, from the releases
use LogoLabs\Inkvec\Inkvec; use LogoLabs\Inkvec\Options; $traced = Inkvec::traceFile('logo.png', new Options(colors: 16)); file_put_contents('logo.svg', $traced->svg); $traced = Inkvec::trace($bytes, ['colors' => 16]); // options as an array $traced = Inkvec::traceRgba($rgba, $width, $height); // straight RGBA8
packages/php calls the C ABI through PHP's FFI extension: no subprocess and no copy of the
image on the way in (a PHP string is passed straight to inkvec_trace). The library is found
through Inkvec::useLibrary(), INKVEC_LIBRARY, the package's own lib/, a sibling
target/release/, or the system loader, and is refused unless its inkvec_abi_version()
matches. Options are the generated Options (camelCase properties, null meaning the
default, bindings/codegen/php.py), an array keyed by the tracer's own names, or raw JSON;
errors are InvalidImageException, InvalidOptionsException, InternalException and
LibraryException, all InkvecException. In a web SAPI, where ffi.enable defaults to
preload, Inkvec::preload() registers the library from an opcache.preload script and
every request shares it. See packages/php/README.md.
HTTP (Docker)
crates/inkvec-server is the facade behind axum: POST /trace (raw image bytes, or
multipart/form-data with an image part), options as ?options=<JSON>, an
X-Inkvec-Options header, generic query parameters typed against the schema below, or a
multipart options part -- all converted mechanically and handed to
inkvec::Options::from_json, same as every other binding. GET /options/schema,
/options/defaults, /healthz, /version and /openapi.json (an OpenAPI 3.1 document
generated by bindings/codegen/openapi.py from bindings/options.schema.json, embedded in
the binary) round it out. services/docker/Dockerfile builds it as a distroless, non-root
container; see crates/inkvec-server/README.md for endpoints, configuration and a curl
walkthrough. Not published anywhere; nothing here is pushed by this repository automatically
(see "Publishing" below).
Versions
There is one version: workspace.package.version in the root Cargo.toml. Every crate inherits
it, maturin reads it for the Python wheel (dynamic = ["version"]), inkvec_version() and
inkvec.__version__ return it, the C release archives are named after the tag, and
packages/npm/build.mjs writes it into the npm package's package.json. A binding added later
(Maven, NuGet, ...) should read it the same way rather than carry its own.
The files that cannot read it hold a copy: the npm package (packages/npm/build.mjs), the
Maven POM (packages/java/build.py), the OpenAPI document (bindings/codegen/generate.py),
both wasm-pack packages under web/, Inkvec Studio's manifests (its src-tauri is a
workspace of its own) and both Cargo.lock files. python tools/check_versions.py lists every
copy that disagrees with Cargo.toml and names the tool that rewrites it; CI runs it, and the
release job runs it with --tag.
The contract fixtures
bindings/contract/:
tiny.png(96 x 96, opaque) andwhite_on_clear.png(64 x 64, white on transparent), with their pixels as raw RGBA8 intiny.rgbaandwhite_on_clear.rgba.make_fixtures.pyrewrites them; they change rarely.-
cases.json: each case names an input, a form (encodedorrgba) and options, and records -
expect-- what every build must report: the width and height, orerror, the kind of error; svg-- per build target, the SVG's length in bytes and SHA-256. A binding built for a target with an entry must reproduce it exactly; on a target without one it checks everything else;same_svg_as(optional) -- a case whose SVG must be identical on every target: raw pixels and the PNG holding them, capped or not.
The cases are written by hand; expect and svg are produced by the Rust facade.
After a deliberate change to the tracer's output, regenerate the expectations with one command:
INKVEC_BLESS=1 cargo test -p inkvec --release --test contract
It rewrites expect and this target's hashes and drops every other target's, which the change
made stale. To record another target's hashes (its output unchanged), run the same test there
with INKVEC_BLESS=add; CI uploads a cases.json blessed this way from each platform it tests.
INKVEC_CONTRACT_REQUIRE_HASH=1 turns a target without recorded hashes from a note into a
failure. The committed hashes cover x86_64-windows-msvc, x86_64-linux-gnu,
wasm32-unknown -- both WebAssembly builds, recorded from the npm package with
INKVEC_BLESS=add node --test test/contract.test.mjs in packages/npm -- and wasm32-wasip1,
recorded from the Go module with INKVEC_BLESS=add go test -run TestContract in packages/go
(the same as wasm32-unknown on 8 of the 12 traced cases; the four uncapped 96 px ones
differ).
The Rust facade (crates/inkvec/tests/contract.rs), the C ABI from Rust
(crates/inkvec-ffi/src/lib.rs tests) and from a foreign caller (crates/inkvec-ffi/tests/test_c_abi.py,
through ctypes, plus the compiled C example), the Python package
(crates/inkvec-py/tests/test_inkvec.py) and the npm package, on both of its builds
(packages/npm/test/contract.test.mjs) all assert against it.
Building and testing locally
cargo test --release -p inkvec -p inkvec-ffi # facade, schema drift, contract, C ABI python bindings/codegen/generate.py --check # generated stubs and tables current cargo build --release -p inkvec-ffi # the C library cbindgen --config crates/inkvec-ffi/cbindgen.toml --crate inkvec-ffi \ --output crates/inkvec-ffi/include/inkvec.h # the header (add --verify to check) python -m pytest crates/inkvec-ffi/tests # the C ABI through ctypes pip install maturin maturin build --release -m crates/inkvec-py/Cargo.toml # abi3 wheel in target/wheels pip install target/wheels/inkvec-*.whl pytest pillow numpy python -m pytest crates/inkvec-py/tests cd packages/npm && npm ci # the npm package (needs wasm-pack and node build.mjs && npm test # a nightly toolchain with rust-src) cd packages/php && composer install # the PHP package (needs ext-ffi and vendor/bin/phpunit # the C library built above)
.github/workflows/bindings.yml does all of this on Linux, macOS and Windows and builds the
release artifacts: the C library for windows-x64, linux-x64, linux-arm64, macos-arm64 and
macos-x64, abi3 wheels for the same targets plus an sdist, and the LogoLabs.Inkvec nupkg
carrying whichever of those native libraries the C library job produced.
Publishing
Nothing is published automatically. Every publishing job in bindings.yml runs only on a v*
tag, and the registry ones only when the repository opts in.
| Registry | Name | Needs |
|---|---|---|
| crates.io | inkvec and the internal crates it depends on |
the repository variable PUBLISH_CRATES=true and a crates.io API token as the secret CARGO_REGISTRY_TOKEN. cargo publish --workspace publishes in dependency order: inkvec-core, inkvec-fit, inkvec-trace, inkvec-sr, inkvec-restore, inkvec-cli, inkvec (all seven pass cargo publish --workspace --dry-run). inkvec-ffi, inkvec-py and inkvec-wasm are publish = false. |
| PyPI | inkvec |
the repository variable PUBLISH_PYPI=true and this repository and workflow (environment pypi) registered as a trusted publisher of the PyPI project; or an API token as the secret PYPI_API_TOKEN, passed as password: to the publish step |
| GitHub Releases | C library archives inkvec-c-<version>-<platform> |
nothing beyond the workflow's own GITHUB_TOKEN |
| Go (mirror repository) | github.com/logolabs/inkvec-go |
the repository logolabs/inkvec-go with a main branch, and the private half of an SSH deploy key with write access to it as the secret GO_MIRROR_DEPLOY_KEY; .github/workflows/go.yml copies packages/go, the built inkvec.wasm, the licence and the contract fixtures there on a v* tag matching Cargo.toml, commits and tags it |
| npm | @logolabs/inkvec |
an npm organisation logolabs (the scope) and an automation token with publish rights to it as the secret NPM_TOKEN; .github/workflows/npm.yml publishes the tarball it built and tested, with provenance, on a v* tag whose version matches package.json |
| NuGet | LogoLabs.Inkvec |
the repository variable PUBLISH_NUGET=true and a NuGet.org API key as the secret NUGET_API_KEY; the dotnet-pack job in bindings.yml gathers the native libraries c-library built for every platform into one nupkg first |
| Packagist (mirror repository) | logolabs/inkvec |
Composer publishes from a repository root, so PHP users get the package from the mirror github.com/logolabs/inkvec-php: with the repository variable PUBLISH_PHP=true and the private half of an SSH deploy key with write access to it as the secret PHP_MIRROR_DEPLOY_KEY, .github/workflows/php.yml copies packages/php, the licence and the contract fixtures there on a v* tag, commits and tags it. The mirror carries no native library; vendor/bin/inkvec-fetch-library downloads the matching C release archive |
| SwiftPM (mirror repository) | github.com/logolabs/inkvec-swift, product Inkvec |
on a v* tag matching the workspace version, .github/workflows/swift.yml attaches InkvecFFI.xcframework.zip to this repository's release (its own GITHUB_TOKEN; the repository must be public for SwiftPM to download it), then -- with the repository variable PUBLISH_SWIFT=true and a token that may push to the mirror as the secret SWIFT_MIRROR_TOKEN -- commits the rendered mirror there and pushes the same tag |