This guide will walk you through creating a new format handler to add support for file formats in Convert to it!.
Handler Basics
Each conversion tool in Convert to it! is wrapped in a handler that implements the FormatHandler interface. This creates a standardized way to support different conversion libraries and tools.
Step-by-Step Guide
Create the handler file
Create a new TypeScript file in src/handlers/ following the naming convention:
If your tool is called dummy, the class should be called dummyHandler
The file should be called dummy.ts
// src/handlers/dummy.ts
import type { FileData , FileFormat , FormatHandler } from "../FormatHandler.ts" ;
import CommonFormats from "src/CommonFormats.ts" ;
class dummyHandler implements FormatHandler {
// Implementation goes here
}
export default dummyHandler ;
Implement required properties
Add the required properties from the FormatHandler interface: class dummyHandler implements FormatHandler {
public name : string = "dummy" ;
public supportedFormats ?: FileFormat [];
public ready : boolean = false ;
// Methods will be added next
}
Implement the init() method
The init() method initializes your handler and sets up supported formats: async init () {
this . supportedFormats = [
// Define supported formats here
];
this . ready = true ;
}
Implement the doConvert() method
The doConvert() method performs the actual conversion: async doConvert (
inputFiles : FileData [],
inputFormat : FileFormat ,
outputFormat : FileFormat
): Promise < FileData [] > {
const outputFiles: FileData [] = [];
// Conversion logic goes here
return outputFiles ;
}
Register the handler
Add your handler to src/handlers/index.ts: import dummyHandler from "./dummy.ts" ;
const handlers : FormatHandler [] = [];
// ... existing handlers
try { handlers . push ( new dummyHandler ()) } catch ( _ ) { };
You can define formats in two ways:
Use the builder pattern with predefined format definitions:
this . supportedFormats = [
CommonFormats . PNG . builder ( "png" )
. allowFrom ( true ) // Can convert FROM PNG
. allowTo ( true ) // Can convert TO PNG
. markLossless (), // PNG is lossless
CommonFormats . JPEG . builder ( "jpeg" )
. allowFrom ( true )
. allowTo ( true )
// JPEG is lossy by default
];
The string parameter in .builder("png") is the internal identifier used by your handler. It can match the format name or be something custom.
For formats not in CommonFormats, define them manually:
this . supportedFormats = [
{
name: "CompuServe Graphics Interchange Format (GIF)" ,
format: "gif" ,
extension: "gif" ,
mime: "image/gif" ,
from: true ,
to: false ,
internal: "gif" ,
category: [ "image" , "video" ],
lossless: false
}
];
Format Properties Explained
Real Example: canvasToBlobHandler
Here’s a simplified real handler that converts images using HTML5 Canvas:
src/handlers/canvasToBlob.ts
import CommonFormats from "src/CommonFormats.ts" ;
import type { FileData , FileFormat , FormatHandler } from "../FormatHandler.ts" ;
class canvasToBlobHandler implements FormatHandler {
public name : string = "canvasToBlob" ;
public supportedFormats : FileFormat [] = [
CommonFormats . PNG . supported ( "png" , true , true , true ),
CommonFormats . JPEG . supported ( "jpeg" , true , true ),
CommonFormats . WEBP . supported ( "webp" , true , true ),
CommonFormats . GIF . supported ( "gif" , true , false ),
CommonFormats . SVG . supported ( "svg" , true , false ),
];
#canvas ?: HTMLCanvasElement ;
#ctx ?: CanvasRenderingContext2D ;
public ready : boolean = false ;
async init () {
this . #canvas = document . createElement ( "canvas" );
this . #ctx = this . #canvas . getContext ( "2d" ) || undefined ;
this . ready = true ;
}
async doConvert (
inputFiles : FileData [],
inputFormat : FileFormat ,
outputFormat : FileFormat
) : Promise < FileData []> {
if ( ! this . #canvas || ! this . #ctx ) {
throw "Handler not initialized." ;
}
const outputFiles : FileData [] = [];
for ( const inputFile of inputFiles ) {
// Create blob from bytes
const blob = new Blob ([ inputFile . bytes ], { type: inputFormat . mime });
const url = URL . createObjectURL ( blob );
// Load image
const image = new Image ();
await new Promise (( resolve , reject ) => {
image . addEventListener ( "load" , resolve );
image . addEventListener ( "error" , reject );
image . src = url ;
});
// Draw to canvas
this . #canvas . width = image . naturalWidth ;
this . #canvas . height = image . naturalHeight ;
this . #ctx . drawImage ( image , 0 , 0 );
// Convert to output format
const bytes = await new Promise < Uint8Array >(( resolve , reject ) => {
this . #canvas ! . toBlob (( blob ) => {
if ( ! blob ) return reject ( "Canvas output failed" );
blob . arrayBuffer (). then ( buf => resolve ( new Uint8Array ( buf )));
}, outputFormat . mime );
});
// Set output filename
const name = inputFile . name . split ( "." )[ 0 ] + "." + outputFormat . extension ;
outputFiles . push ({ bytes , name });
}
return outputFiles ;
}
}
export default canvasToBlobHandler ;
Important Guidelines
File Naming Responsibility The handler is responsible for setting the output file’s name. This allows flexibility for cases where the full filename matters. In most cases, you’ll swap the file extension: const name = inputFile . name . split ( "." )[ 0 ] + "." + outputFormat . extension ;
Buffer Mutation Protection Handlers must ensure byte buffers entering or exiting do not get mutated: // Clone buffer if necessary
const safeBytes = new Uint8Array ( inputFile . bytes );
MIME Type Normalization
When handling MIME types, run them through normalizeMimeType first:
import normalizeMimeType from "../normalizeMimeType.ts" ;
const normalizedMime = normalizeMimeType ( detectedMimeType );
One file can have multiple valid MIME types, which isn’t great when matching algorithmically. Normalization ensures consistency.
When implementing a new file format, treat the file as the media that it represents , not the data that it contains. Example: An SVG handler should treat files as images , not as XML data.
Advanced Example: FFmpeg Handler
For a more complex example, see the FFmpeg handler which:
Dynamically discovers supported formats at initialization
Handles WebAssembly-based FFmpeg
Implements error recovery and retries
Supports multiple input files
src/handlers/FFmpeg.ts (excerpt)
class FFmpegHandler implements FormatHandler {
public name : string = "FFmpeg" ;
public supportedFormats : FileFormat [] = [];
public ready : boolean = false ;
#ffmpeg ?: FFmpeg ;
async init () {
this . #ffmpeg = new FFmpeg ();
await this . #ffmpeg . load ({
coreURL: "/convert/wasm/ffmpeg-core.js"
});
// Dynamically discover formats
const stdout = await this . getStdout ( async () => {
await this . execSafe ([ "-formats" , "-hide_banner" ], 3000 , 5 );
});
// Parse and populate supportedFormats...
this . ready = true ;
}
async doConvert (
inputFiles : FileData [],
inputFormat : FileFormat ,
outputFormat : FileFormat ,
args ?: string []
) : Promise < FileData []> {
// Write input files to FFmpeg virtual filesystem
for ( const file of inputFiles ) {
await this . #ffmpeg . writeFile ( file . name , new Uint8Array ( file . bytes ));
}
// Execute conversion
const command = [ "-i" , "input" , "-f" , outputFormat . internal , "output" ];
await this . #ffmpeg . exec ( command );
// Read output
const bytes = await this . #ffmpeg . readFile ( "output" );
return [{ bytes: new Uint8Array ( bytes ), name: "output." + outputFormat . extension }];
}
}
Testing Your Handler
Unit Testing
Test your handler with various input files:
Different file sizes
Edge cases (empty files, corrupted data)
Multiple files if supported
Integration Testing
Test your handler within the full application:
Verify it appears in the format list
Test conversions through the UI
Check multi-step conversions involving your handler
Performance Testing
Monitor memory usage with large files
Check conversion speed
Test browser compatibility
Common Patterns
Async Initialization
Many handlers need to load external libraries:
async init () {
// Load WebAssembly module
await loadWasmModule ();
// Initialize library
this . library = new ExternalLibrary ();
this . ready = true ;
}
Error Handling
async doConvert ( ... args ): Promise < FileData [] > {
if (!this.ready) {
throw "Handler not initialized." ;
}
try {
// Conversion logic
} catch (error) {
throw `Conversion failed: ${ error } `;
}
}
Processing Multiple Files
const outputFiles : FileData [] = [];
for ( const inputFile of inputFiles ) {
// Process each file
const bytes = await convertFile ( inputFile );
const name = inputFile . name . replace ( / \. [ ^ . ] + $ / , `. ${ outputFormat . extension } ` );
outputFiles . push ({ bytes , name });
}
return outputFiles ;
Next Steps
After creating your handler:
Add appropriate dependencies
Test thoroughly
Submit a pull request following the contribution guidelines
Update documentation if adding significant new functionality