Filmkit Fujifilm Camera
Browser-based preset manager and RAW converter for Fujifilm X-series cameras using WebUSB and PTP protocol
- Skill ID
- reason-machines/trending-skills/filmkit-fujifilm-camera
- Publisher
- reason-machines
- Repository
- trending-skills
- Installs
- 0
- Files
- 1
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
reason-machines/trending-skills/filmkit-fujifilm-cameraInstalls these files- SKILL.md
What this skill tells the agent
FilmKit Fujifilm Camera Skill
Skill by ara.so — Daily 2026 Skills collection.
FilmKit is a browser-based, zero-install preset manager and RAW converter for Fujifilm X-series cameras. It uses WebUSB to communicate via PTP (Picture Transfer Protocol) — the same protocol as Fujifilm X RAW STUDIO — so the camera's own image processor handles RAW-to-JPEG conversion. It runs entirely client-side (hosted on GitHub Pages) and supports desktop and Android.
What FilmKit Does
- Preset Management: Read, edit, and write custom film simulation presets directly on-camera (slots D18E–D1A5 via PTP
GetDevicePropValue/SetDevicePropValue) - Local Preset Library: Save presets locally, drag-and-drop between camera and local storage
- RAW Conversion & Live Preview: Send RAF files to the camera, receive full-quality JPEGs back
- Preset Detection: Loading a RAF file auto-detects which preset was used to shoot it
- Import/Export: Presets as files, links, or text paste
- Mobile Support: Works on Android via Chrome's WebUSB support
Requirements
- Chromium-based browser (Google Chrome, Edge, Brave) on desktop or Android — WebUSB is required
- Fujifilm X-series camera connected via USB (tested on X100VI; likely works on X-T5, X-H2, X-T30, etc.)
- Linux udev rule (if running Chrome in Flatpak):
# /etc/udev/rules.d/99-fujifilm.rules
SUBSYSTEM=="usb", ATTR{idVendor}=="04cb", MODE="0666"Reload rules after adding:
sudo udevadm control --reload-rules && sudo udevadm triggerInstallation / Setup (Development)
FilmKit is a static TypeScript app. To run locally:
git clone https://github.com/eggricesoy/filmkit.git
cd filmkit
npm install
npm run devBuild for production:
npm run buildThe built output is a static site — no server required. Open in Chrome at http://localhost:5173 (or wherever Vite serves it).
Architecture Overview
PTP over WebUSB
FilmKit speaks PTP (Picture Transfer Protocol) directly over USB bulk transfers. Key operations:
| PTP Operation | Purpose |
|---|---|
GetDevicePropValue | Read a camera preset property |
SetDevicePropValue | Write a camera preset property |
InitiateOpenCapture | Start RAW conversion session |
SendObject | Send RAF file to camera |
GetObject | Retrieve converted JPEG from camera |
Preset Property Codes
Fujifilm X-series cameras expose film simulation parameters as device properties in the range 0xD18E–0xD1A5:
// Example property codes (from QUICK_REFERENCE.md)
const PROP_FILM_SIMULATION = 0xD18E;
const PROP_GRAIN_EFFECT = 0xD18F;
const PROP_COLOR_CHROME = 0xD190;
const PROP_WHITE_BALANCE = 0xD191;
const PROP_COLOR_TEMP = 0xD192;
const PROP_DYNAMIC_RANGE = 0xD193;
const PROP_HIGHLIGHT_TONE = 0xD194;
const PROP_SHADOW_TONE = 0xD195;
const PROP_COLOR = 0xD196;
const PROP_SHARPNESS = 0xD197;
const PROP_HIGH_ISO_NR = 0xD198; // Non-linear encoding!
const PROP_CLARITY = 0xD199;Native Profile Format
The camera's native d185 profile is 625 bytes and uses different field indices/encoding from RAF file metadata. FilmKit uses a patch-based approach:
// Conceptual patch approach
function applyPresetPatch(baseProfile: Uint8Array, changes: PresetChanges): Uint8Array {
// Copy base profile byte-for-byte
const patched = new Uint8Array(baseProfile);
// Only overwrite fields the user changed
// This preserves EXIF sentinel values in unchanged fields
for (const [fieldIndex, encodedValue] of Object.entries(changes)) {
writeFieldToProfile(patched, parseInt(fieldIndex), encodedValue);
}
return patched;
}Key Code Patterns
WebUSB Connection
// Request access to the Fujifilm camera
async function connectCamera(): Promise<USBDevice> {
const device = await navigator.usb.requestDevice({
filters: [{ vendorId: 0x04CB }] // Fujifilm vendor ID
});
await device.open();
await device.selectConfiguration(1);
await device.claimInterface(0);
return device;
}Sending a PTP Command
// PTP command packet structure
function buildPTPCommand(
operationCode: number,
transactionId: number,
params: number[] = []
): ArrayBuffer {
const paramCount = params.length;
const length = 12 + paramCount * 4;
const buffer = new ArrayBuffer(length);
const view = new DataView(buffer);
view.setUint32(0, length, true); // Length
view.setUint16(4, 0x0001, true); // Type: Command
view.setUint16(6, operationCode, true); // Operation code
view.setUint32(8, transactionId, true); // Transaction ID
params.forEach((p, i) => {
view.setUint32(12 + i * 4, p, true);
});
return buffer;
}
// Send a PTP operation and read response
async function ptpTransaction(
device: USBDevice,
operationCode: number,
transactionId: number,
params: number[] = [],
outData?: ArrayBuffer
): Promise<{ responseCode: number; data?: ArrayBuffer }> {
const endpointOut = 0x02; // Bulk OUT
const endpointIn = 0x81; // Bulk IN
// Send command
const cmd = buildPTPCommand(operationCode, transactionId, params);
await device.transferOut(endpointOut, cmd);
// Send data phase if present
if (outData) {
await device.transferOut(endpointOut, outData);
}
// Read data response (if expected)
const dataResult = await device.transferIn(endpointIn, 512);
// Read response packet
const respResult = await device.transferIn(endpointIn, 32);
const respView = new DataView(respResult.data!.buffer);
const responseCode = respView.getUint16(6, true);
return { responseCode, data: dataResult.data?.buffer };
}Reading a Preset Property
async function getDevicePropValue(
device: USBDevice,
propCode: number,
txId: number
): Promise<DataView> {
const PTP_OP_GET_DEVICE_PROP_VALUE = 0x1015;
const { data } = await ptpTransaction(
device,
PTP_OP_GET_DEVICE_PROP_VALUE,
txId,
[propCode]
);
if (!data) throw new Error(`No data for prop 0x${propCode.toString(16)}`);
// PTP data container: 12-byte header, then payload
return new DataView(data, 12);
}
// Example: read film simulation
const filmSimView = await getDevicePropValue(device, 0xD18E, txId++);
const filmSimValue = filmSimView.getUint16(0, true);
console.log('Film simulation code:', filmSimValue);Writing a Preset Property
async function setDevicePropValue(
device: USBDevice,
propCode: number,
value: number,
byteSize: 1 | 2 | 4,
txId: number
): Promise<void> {
const PTP_OP_SET_DEVICE_PROP_VALUE = 0x1016;
// Build data container
const dataLength = 12 + byteSize;
const dataBuffer = new ArrayBuffer(dataLength);
const view = new DataView(dataBuffer);
view.setUint32(0, dataLength, true); // Length
view.setUint16(4, 0x0002, true); // Type: Data
view.setUint16(6, PTP_OP_SET_DEVICE_PROP_VALUE, true);
view.setUint32(8, txId, true);
if (byteSize === 1) view.setUint8(12, value);
else if (byteSize === 2) view.setUint16(12, value, true);
else if (byteSize === 4) view.setUint32(12, value, true);
await ptpTransaction(
device,
PTP_OP_SET_DEVICE_PROP_VALUE,
txId,
[propCode],
dataBuffer
);
}
// Example: set White Balance to Color Temperature mode
await setDevicePropValue(device, 0xD191, 0x0012, 2, txId++);
// Now safe to set Color Temperature value
await setDevicePropValue(device, 0xD192, 4500, 2, txId++);HighIsoNR Special Encoding
HighIsoNR uses a non-linear proprietary encoding — do not write raw values directly:
// HighIsoNR encoding map (reverse-engineered via Wireshark)
const HIGH_ISO_NR_ENCODE: Record<number, number> = {
[-4]: 0x00,
[-3]: 0x01,
[-2]: 0x02,
[-1]: 0x03,
[0]: 0x04,
[1]: 0x08,
[2]: 0x0C,
[3]: 0x10,More from reason-machines
Use this skill in a project
Open any RiverX project, search the Skills panel for reason-machines/trending-skills/filmkit-fujifilm-camera, and install it in one click.
