How Complex Conversion Paths Work in Convert: Image to Audio and Beyond
Convert manages complex conversion paths like image to audio by modeling formats as nodes and handlers as edges in a weighted directed graph, then running Dijkstra's algorithm to find the optimal multi-step route while penalizing lossy or illogical category transitions.
The open-source project p2r3/convert automatically routes files through multi-step conversion pipelines without manual configuration. When direct conversion isn't available—such as converting a PNG image directly to an MP3 audio file—the system constructs complex conversion paths by chaining compatible format handlers. According to the Convert source code, this routing is handled by a graph traversal engine that balances conversion quality against path length.
Building the Conversion Graph in TraversionGraph.ts
Node and Edge Initialization
The TraversionGraph class in src/TraversionGraph.ts (lines 37-78) constructs the routing graph during initialization. For every registered FormatHandler, the system caches supported input (from) and output (to) formats in window.supportedFormatCache. The init method creates one node per unique MIME type and directed edges for every possible conversion a handler can perform.
// src/TraversionGraph.ts – graph creation (simplified)
this.handlers = handlers;
...
supportedFormatCache.forEach((formats, handler) => {
// …build nodes…
fromIndices.forEach(from => {
toIndices.forEach(to => {
if (from.index === to.index) return; // no self‑loops
this.edges.push({
from,
to,
handler,
cost: this.costFunction(...), // ← weight
});
this.nodes[from.index].edges.push(this.edges.length - 1);
});
});
});
Each edge stores a computed weight that reflects the "cost" of that conversion step, ensuring the pathfinder prefers efficient, high-quality routes.
Edge Weight Calculation Factors
The costFunction (lines 88-140) calculates edge weights using several factors to ensure sensible routing:
| Factor | Implementation | Impact |
|---|---|---|
| Base step cost | DEPTH_COST constant |
Guarantees every hop adds positive cost, favoring shorter paths |
| Category-change cost | categoryChangeCosts map (e.g., image→audio = 1.4) or DEFAULT_CATEGORY_CHANGE_COST |
Penalizes conversions between unrelated media types; if strictCategories is true, sums intermediate category costs |
| Handler priority | HANDLER_PRIORITY_COST * handlerIndex |
Slightly favors handlers registered earlier in the list |
| Format priority | FORMAT_PRIORITY_COST * indexInSupportedFormats |
Slightly favors formats listed earlier in a handler's supportedFormats array |
| Lossy multiplier | LOSSY_COST_MULTIPLIER (1.4) applied if !to.format.lossless |
Penalizes routes ending in lossy formats |
// src/TraversionGraph.ts – cost calculation logic (lines 88-140)
let cost = DEPTH_COST;
...
// category change handling (default & custom)
if (strictCategories) {
// sum all intermediate category costs
} else if (!fromCategories.some(c => toCategories.includes(c))) {
// apply DEFAULT_CATEGORY_CHANGE_COST
}
...
cost += HANDLER_PRIORITY_COST * handlerIndex;
cost += FORMAT_PRIORITY_COST * (handlerObj?.supportedFormats?.findIndex(f => f.mime === to.format.mime) ?? 0);
if (!to.format.lossless) cost *= LOSSY_COST_MULTIPLIER;
Penalizing Suboptimal Routes with Adaptive Costs
Some conversion sequences are particularly undesirable—such as image→video→audio when a direct image→audio route exists. The calculateAdaptiveCost method (lines 71-84) scans candidate paths for defined forbidden sequences in categoryAdaptiveCosts and adds massive penalty costs when detected.
// src/TraversionGraph.ts – adaptive cost application
this.categoryAdaptiveCosts.forEach(c => {
// look for the sequence of categories in the path
if (found) cost += c.cost; // c.cost is typically huge to force avoidance
});
This ensures Dijkstra's algorithm prunes nonsensical routes during the search phase, even if they technically connect the source and target formats.
Dead-End Detection and Dynamic Path Pruning
If an intermediate conversion step fails at runtime, Convert marks the partial path as a dead end to prevent future attempts. In src/main.ts (lines 65-71), the attemptConvertPath function catches errors and registers the failing path segment:
// src/main.ts – dead‑end handling
catch (e) {
const deadEndPath = path.slice(0, i + 2);
deadEndAttempts.push(deadEndPath);
window.traversionGraph.addDeadEndPath(deadEndPath);
…
}
The graph treats these dead-end paths as having infinite cost in subsequent searches via calculateAdaptiveCost, forcing the router to explore alternative sequences automatically.
Executing the Search with searchPath
The searchPath method (lines 283-317) implements a priority-queue-backed Dijkstra search:
- Start from the input format node
- Pop the lowest-cost node from the queue
- Expand all outgoing edges, accumulating costs including adaptive penalties
- Yield the path when reaching the target MIME type
// src/TraversionGraph.ts – Dijkstra main loop (lines 283-317)
while (queue.size() > 0) {
const current = queue.poll()!;
if (current.index === toIndex) {
// safety checks & simple‑mode handling …
yield current.path;
continue;
}
visited.push(current.index);
this.nodes[current.index].edges.forEach(edgeIndex => {
const edge = this.edges[edgeIndex];
// skip visited, missing handler, …
queue.add({
index: edge.to.index,
cost: current.cost + edge.cost + this.calculateAdaptiveCost(path),
path,
visitedBorder: visited.length
});
});
}
Complete Example: Image to Audio Conversion
The following demonstrates converting a PNG image to MP3 audio using the graph's automatic path discovery:
// Example: convert an image (PNG) → audio (MP3) in the browser console
const imgFile = await (await fetch('sample.png')).blob();
const imgBytes = new Uint8Array(await imgFile.arrayBuffer());
// Build format nodes for the source and target
const inputOption = window.traversionGraph.handlers
.flatMap(h => h.supportedFormats?.filter(f => f.mime.startsWith('image/')) ?? [])
.find(f => f.extension === 'png');
const outputOption = window.traversionGraph.handlers
.flatMap(h => h.supportedFormats?.filter(f => f.mime.startsWith('audio/')) ?? [])
.find(f => f.extension === 'mp3');
if (!inputOption || !outputOption) throw 'Formats not supported';
const inputNode = new ConvertPathNode(
window.traversionGraph.handlers.find(h => h.supportedFormats?.includes(inputOption))!,
inputOption
);
const outputNode = new ConvertPathNode(
window.traversionGraph.handlers.find(h => h.supportedFormats?.includes(outputOption))!,
outputOption
);
const result = await window.tryConvertByTraversing(
[{ name: 'sample.png', bytes: imgBytes }],
inputNode,
outputNode
);
if (result) {
console.log('Conversion succeeded via path:',
result.path.map(p => `${p.handler.name} → ${p.format.format}`).join(' → '));
// `result.files[0].bytes` now holds the MP3 data
}
The graph automatically discovers a multi-hop route such as image→video→audio (or a direct image-to-audio handler if available). The category-change and adaptive costs ensure that highly lossy or indirect routes receive severe penalties, while dead-end detection allows the system to recover and find alternative paths if intermediate steps fail.
Summary
- Graph Model: Convert builds a weighted directed graph where nodes are MIME types and edges are handler capabilities, defined in
src/TraversionGraph.ts. - Intelligent Routing: Dijkstra's algorithm in
searchPathfinds the lowest-cost route, balancing path length against quality penalties. - Quality Preservation: The
costFunctionpenalizes category changes (e.g., image→audio), lossy output formats, and undesirable handler orders using multipliers likeLOSSY_COST_MULTIPLIER(1.4). - Runtime Adaptation: The
calculateAdaptiveCostmechanism and dead-end tracking insrc/main.tsprevent the system from retrying failed or suboptimal paths.
Frequently Asked Questions
How does Convert handle conversions between unrelated format categories?
Convert applies a category-change cost via the costFunction in src/TraversionGraph.ts. When converting between categories like image and audio, the system either uses a specific cost from categoryChangeCosts (set to 1.4 for image→audio) or falls back to DEFAULT_CATEGORY_CHANGE_COST. If strictCategories is enabled, it sums the costs of every intermediate category crossed, making direct routes significantly cheaper than circuitous ones.
What happens if an intermediate conversion step fails during execution?
When a step fails, the attemptConvertPath function in src/main.ts (lines 65-71) catches the error and registers the partial path as a dead end via window.traversionGraph.addDeadEndPath. Subsequent searches treat this path as having infinite cost, forcing the graph to explore alternative routes automatically without user intervention.
Can the conversion path prioritize lossless formats?
Yes. The costFunction checks the lossless property of the target format. If !to.format.lossless, it multiplies the total edge cost by LOSSY_COST_MULTIPLIER (1.4). This penalty ensures that routes preserving lossless formats receive lower total costs, causing Dijkstra's algorithm to prefer them unless no lossless path exists.
Where is the conversion graph stored and accessed in the browser?
The graph instance is stored globally at window.traversionGraph and initialized during application startup. It caches supported formats in window.supportedFormatCache and exposes the primary conversion entry point as window.tryConvertByTraversing, allowing console access for debugging and programmatic conversion as shown in the image-to-audio example.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →