Formats and Encodings
Choose a format for each boundary between components or systems. The format
must preserve the required data. It must also work with existing tools and
resource limits.
QIP uses stable, open formats with simple parsers. Stable formats improve
long-term compatibility. Open formats reduce vendor lock-in. Simple formats
help people and coding agents write correct implementations.
Many older formats were built for computers with little memory. These formats
often divide data into records, rows, or chunks. A program can process one unit
and then reuse its working memory. This avoids a complete object tree and a
separate allocation for each node.
Sequential processing can reduce peak memory and parser complexity. However,
older formats are not always more efficient. BMP is simple to decode, but BMP
files are large. TAR supports sequential access, but TAR does not compress
data. SQLite supports indexed queries, but SQLite needs random access to its
pages.
Before you select a format, answer these questions:
- Which data and metadata must the boundary preserve?
- Which formats do both systems support?
- Does the operation need sequential access or random access?
- Is the main constraint file size, CPU time, or working memory?
Images #
Use an uncompressed image inside a processing pipeline when components need
direct access to pixels. Use a compressed image at a storage, download, or
application boundary.
| Format | Use it for | Tradeoff |
image/bmp | A simple uncompressed raster boundary. | Parsing is simple, but files are large and row order can vary. |
image/svg+xml | Editable or resolution-independent paths, diagrams, and text. | A raster consumer must render the SVG before processing pixels. |
| PNG | Lossless storage and broad interoperability. | Decoding needs more code and CPU time than BMP or QIP KTX2. |
| JPEG | Small photographs for systems that need lossy compression and broad support. | JPEG does not preserve alpha and changes pixel values. |
| WebP or AVIF | Compressed application and web boundaries. | Confirm that all target tools can decode the selected format. |
| JPEG XL | Lossless images or lossy images when the recipient supports JPEG XL. | Decoder support is less common than JPEG or WebP; check the target system. |
| JPEG 2000, GIF, or ICO | Interoperation with a system that requires that format. | Convert to a QIP working format after input enters the pipeline. |
QIP can convert PNG, JPEG, WebP, AVIF, JPEG 2000, and SVG to a working raster
format. QIP can write PNG, JPEG, WebP, AVIF, JPEG XL, and ICO. QIP can also process GIF.
Raster limits
Raster converters accept no more than 25,000,000 decoded pixels. Neither
dimension can be more than 8192 pixels. Compressed PNG, JPEG, JPEG 2000, and
WebP input also has a 64 MiB byte limit. Both limits apply. A small compressed
file is still invalid if its decoded image exceeds the pixel limit.
The ICO component writes one BMP-backed image. The image can be no larger than
256 by 256 pixels. See Hard Limits for host memory and
execution controls.
QIP KTX2 profiles #
KTX2 is QIP's canonical raster container between Content image components. GUI
components also use it for complete rendered frames. QIP supports a narrow,
uncompressed subset so components can address pixels directly.
QIP supports two payload layouts in a small subset of KTX2:
| Profile | Payload | Use |
| RGBA8 sRGB | VK_FORMAT_R8G8B8A8_SRGB, 4 bytes per pixel | General 8-bit Content images. |
| RGBA32F | VK_FORMAT_R32G32B32A32_SFLOAT, 16 bytes per pixel | Linear BT.709, linear Display P3, or transfer-encoded Display P3 data. |
Declaring the profile in the content type
image/ktx2 alone does not say which profile a component reads or writes, so
components declare it with content-type parameters that mirror the KTX2 header
and Data Format Descriptor fields:
image/ktx2;vkFormat=R8G8B8A8_SRGB;colorPrimaries=BT709;transferFunction=SRGB
image/ktx2;vkFormat=B8G8R8A8_SRGB;colorPrimaries=BT709;transferFunction=SRGB
image/ktx2;vkFormat=R32G32B32A32_SFLOAT;colorPrimaries=BT709;transferFunction=LINEAR
image/ktx2;vkFormat=R32G32B32A32_SFLOAT;colorPrimaries=DISPLAYP3;transferFunction=LINEAR
image/ktx2;vkFormat=R32G32B32A32_SFLOAT;colorPrimaries=DISPLAYP3;transferFunction=SRGB
The values are the Vulkan VK_FORMAT_* and Khronos Data Format
KHR_DF_PRIMARIES_* and KHR_DF_TRANSFER_* enumerants without their prefixes,
in the specifications' own casing. Parameter names are case-insensitive;
values are case-sensitive. The canonical spelling is the lowercase media type
followed by ;name=value for each parameter with no whitespace anywhere, and
the runtime rejects other spellings.
A pipeline matches content types on the media type. A parameter a component
declares must match when the incoming content also carries it, so an RGBA8
output is refused by an RGBA32F input before the component runs. A bare
image/ktx2 still matches any profile, which keeps older components valid.
Example pipelines
Recipes specifies how hosts validate every step of a pipeline;
these examples show the KTX2 parameters at work.
An 8-bit pipeline. The PNG decoder emits image/ktx2, the resizer declares
and emits the RGBA8 sRGB profile, and the WebP encoder accepts any image/ktx2:
npx @qip.dev/qipx qip.dev run \
image/png/png-to-ktx2-r8g8b8a8-srgb.wasm \
image/ktx2/ktx2-r8g8b8a8-srgb-resize-down-lanczos3.wasm \
image/ktx2/ktx2-r8g8b8a8-srgb-to-webp-lossy.wasm \
< photo.png > photo.webp
A float pipeline. The two converters move between profiles, and the look
component declares linear BT.709 RGBA32F on both sides:
npx @qip.dev/qipx qip.dev run \
image/png/png-to-ktx2-r8g8b8a8-srgb.wasm \
image/ktx2/ktx2-r8g8b8a8-srgb-to-ktx2-rgba32float.wasm \
image/ktx2/ktx2-rgba32float-look-warm-fade.wasm \
image/ktx2/ktx2-rgba32float-to-ktx2-r8g8b8a8-srgb.wasm \
image/ktx2/ktx2-r8g8b8a8-or-b8g8r8a8-srgb-to-png.wasm \
< photo.png > warm.png
An invalid pipeline. The resizer emits RGBA8 sRGB straight into the look
component, which needs RGBA32F. Both media types are image/ktx2, so only
the parameters reveal the mistake:
npx @qip.dev/qipx qip.dev run \
image/png/png-to-ktx2-r8g8b8a8-srgb.wasm \
image/ktx2/ktx2-r8g8b8a8-srgb-resize-down-lanczos3.wasm \
image/ktx2/ktx2-rgba32float-look-warm-fade.wasm \
< photo.png > warm.png
Every QIP host refuses it before running any component and exits with status
qip, the Node.js qipx, and the Rust qipx print the same message; the
Rust tool prefixes it with qipx: . The message names the step that could not
accept its input and its declared type, what it got and which step produced
that, and each parameter that disagrees. When both sides share the media type,
the got part lists only the incoming parameters:
step 3 image/ktx2/ktx2-rgba32float-look-warm-fade.wasm expected image/ktx2;vkFormat=R32G32B32A32_SFLOAT;colorPrimaries=BT709;transferFunction=LINEAR, got vkFormat=R8G8B8A8_SRGB;colorPrimaries=BT709;transferFunction=SRGB from step 2 image/ktx2/ktx2-r8g8b8a8-srgb-resize-down-lanczos3.wasm: vkFormat expected R32G32B32A32_SFLOAT got R8G8B8A8_SRGB; transferFunction expected LINEAR got SRGB
When the media types themselves differ, got shows the whole incoming type
and the detail reads media type expected image/ktx2 got image/png.
Inserting ktx2-r8g8b8a8-srgb-to-ktx2-rgba32float.wasm between the resizer
and the look component makes the pipeline valid again.
Component filenames may abbreviate a profile, such as
ktx2-rgba32float-display-p3-linear. The content-type declaration and KTX2
header define the format; the filename does not have to repeat the MIME
parameter names or their exact values.
Each profile contains one uncompressed image. The profiles do not accept
compressed textures, mipmaps, arrays, or cubemaps. Keep linear float data
between operations that need it. Convert the final image when transfer size is
the main constraint. See the
KTX2 component documentation
for the complete profiles.
Text, markup, and structured data #
Text and markup
QIP uses UTF-8 for all text pipelines.
| Format | Use it for | Tradeoff |
| Plain UTF-8 text | Text that has no document structure. | The format cannot preserve structure that is not in the text itself. |
text/markdown | Documents that people must read and edit. | Markdown has fewer document semantics than HTML. |
text/html | Browser documents and structured document fragments. | A Document Object Model parser can allocate a large object tree. |
text/css and text/javascript | Web source transformations. | Treat source text as code when it crosses a trust boundary. |
text/uri-list | A sequence of resource identifiers. | The format carries a list, not resource metadata. |
text/vnd.mermaid | Diagrams that must remain text. | A presentation system must render the diagram. |
The repository also uses registered text/* types for C, Swift, and Zig source
text. A streaming parser can process many text formats with bounded working
memory. A Document Object Model (DOM) parser allocates a tree instead.
Structured data and databases
| Format | Use it for | Tradeoff |
application/json | Small structured messages and broad application interoperation. | Many JSON APIs allocate a complete value tree. |
application/design-tokens+json | Named design values shared between design and development tools. | The DTCG structure is more verbose than application-specific JSON. Pin components to a published specification version. |
application/xml | Existing XML protocols, namespaces, or mixed text and elements. | XML has more syntax and processing rules than JSON. |
text/csv | Flat records that consumers can process in sequence. | CSV loses nested structure and data types unless the application defines a schema. |
application/vnd.sqlite3 | Tables, indexes, transactions, and selective queries. | SQLite needs page access and a larger runtime than CSV. |
QIP palette extractors target the stable
DTCG 2025.10 format, not
the changing preview draft.
JSON and XML permit streaming parsers. Some APIs still build a complete tree
and increase peak memory. Check the parser API when working memory is limited.
SQLite can query a data set without loading the complete data set into memory.
Use SQLite when consumers need random access or relational operations. Do not
convert indexed or relational data to CSV when consumers still need those
features.
| Format | Use it for | Tradeoff |
application/x-tar | A file collection that a component reads or writes in order. | TAR does not include compression. |
application/zip | A compressed archive for users and desktop tools. | Some ZIP operations must read the central directory at the end of the file. |
application/gzip | One compressed byte stream, such as a .tar.gz or .vcf.gz file. | A reader must decompress from the start, unless the file is BGZF. |
application/zstd | One Zstandard byte stream, such as a .tar.zst file or an HTTP body sent with Content-Encoding: zstd. | Frames compress on their own, so small frames compress poorly. |
application/x-www-form-urlencoded | Small named UTF-8 form fields. | The format is not suitable for file bodies. |
multipart/form-data | Forms with files or separate metadata for each part. | Boundary parsing is more complex than URL-encoded form parsing. |
QIP uses TAR as the sequential archive inside pipelines and ZIP at external
boundaries. The ZIP-to-TAR component accepts bounded classic ZIP archives. It
supports stored and DEFLATE entries. It rejects ZIP64, encryption, split
archives, special file types, and unsafe extraction paths.
bytes/gzip-compress.wasm writes one gzip member. bytes/bgzf-compress.wasm
writes BGZF: gzip members of at most 64 KiB each, followed by an empty EOF
member. tabix, samtools, and other htslib tools need BGZF to read a file at an
indexed offset, and any gunzip still reads it. On 600 KB text files, its
output is 3% to 6% larger than one gzip member, because each member
compresses on its own and cannot refer to earlier members. Use
bytes/gzip-decompress.wasm for any gzip or BGZF input. Use
bytes/bgzf-decompress.wasm when the input must be complete, valid BGZF. It
rejects plain gzip and files that have lost their EOF member.
bytes/zstd-compress.wasm writes one Zstandard frame with the content size
and an XXH64 checksum, and no dictionary. It stores literals raw and codes
matches with the predefined FSE tables, so on text its output is about 1.3 to
1.6 times the size of zstd -3. bytes/zstd-decompress.wasm reads any
frames that zstd writes without a dictionary, as long as the window is at
most 8 MiB, the limit browsers use. Concatenated frames decode to the
concatenation of their contents and skippable frames are ignored, so a server
can join compressed fragments without decompressing them.
WARC web snapshots #
application/warc carries a complete routed site between whole-site
components. Each response record preserves its URI, status, headers, and body.
This lets one component check links across pages while another adds routes or
converts the finished site to a static TAR archive.
Use qip router warc ... to produce the archive. WARC is larger and less
widely supported than ZIP or TAR, so convert it at the boundary where another
system needs ordinary files.
Documents and fonts #
| Format | Use it for | Tradeoff |
application/pdf | Fixed page layout and vector artwork. | PDF is harder to edit than the source document or SVG. |
font/ttf | SFNT fonts with TrueType glyf outlines. | Consumers need a font parser or an outline conversion step. |
QIP can extract text and images from PDF. QIP can also convert a strict SVG
subset to PDF/A-2b. Font components can convert TrueType outlines to SVG paths
or CSV data.
WebAssembly components #
application/wasm is both QIP's executable component format and a Content
format that other components can process. Components such as validators,
counters, translators, and instrumentation passes accept a Wasm module and
return facts, source code, or another module. This higher-level boundary lets a
component inspect or transform another component without adding that logic to
the host.
Wasm is executable input, not a general data format. Before you run an
untrusted module, apply the validation and execution policies in
Hard Limits.
Encodings and pipeline compatibility #
A format defines file or container semantics. Examples include
image/svg+xml, application/warc, and image/bmp. An encoding defines the
representation of bytes or values in a processing stage.
QIP supports these processing encodings:
- UTF-8 for Content text through
input_utf8_cap and output_utf8_cap.
RGBA32Float for image filter tiles through tile_rgba32float_64x64.
- RGBA8 sRGB in canonical KTX2 for GUI component output. See
GUI Components.
Valid UTF-8 is also valid raw byte input. Therefore, UTF-8 Content output can
feed a raw-byte Content input. Arbitrary raw bytes cannot feed a UTF-8 input
until the host validates the bytes.
RGBA32Float Tile data is not a subtype of Content bytes. The host uses the
image bridge to cross this boundary. The host decodes Content image bytes to
tiles before a contiguous Tile group. The host encodes the tiles as Content
image bytes after the Tile group.
Validation and trade-offs #
Validate data at an untrusted boundary
A content type is a component precondition or guarantee. An image/png
component can rely on its documented PNG profile. It does not have to repeat
complete validation before processing starts.
A file extension or an HTTP Content-Type value does not prove that the bytes
are valid. Validate untrusted data before another component relies on the
format:
untrusted bytes -> validate PNG -> valid image/png -> transform PNG
A pass-through validator can reject malformed data and return accepted bytes
as image/png. A recipe must not infer validation from a claimed content type.
When not to use these formats
Use an application-specific format when the formats on this page cannot
preserve the required semantics. Keep a simple format between QIP components,
and add an adapter at system ingress or egress.
Do not use an uncompressed working format for downloads when transfer size is
the main constraint. Do not use QIP's narrow KTX2 profiles when another system
needs general KTX2 features such as compressed textures or mipmaps. Do not
replace a normal application database or object model only to use a QIP
pipeline.