Loading...
Loading...
Browser-based preset manager and RAW converter for Fujifilm X-series cameras using WebUSB and PTP protocol
npx skill4agent add aradotso/trending-skills filmkit-fujifilm-cameraSkill by ara.so — Daily 2026 Skills collection.
GetDevicePropValueSetDevicePropValue# /etc/udev/rules.d/99-fujifilm.rules
SUBSYSTEM=="usb", ATTR{idVendor}=="04cb", MODE="0666"sudo udevadm control --reload-rules && sudo udevadm triggergit clone https://github.com/eggricesoy/filmkit.git
cd filmkit
npm install
npm run devnpm run buildhttp://localhost:5173| PTP Operation | Purpose |
|---|---|
| Read a camera preset property |
| Write a camera preset property |
| Start RAW conversion session |
| Send RAF file to camera |
| Retrieve converted JPEG from camera |
0xD18E0xD1A5// 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;d185// 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;
}// 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;
}// 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 };
}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);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 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,
[4]: 0x14,
};
function encodeHighIsoNR(userValue: number): number {
const encoded = HIGH_ISO_NR_ENCODE[userValue];
if (encoded === undefined) throw new Error(`Invalid HighIsoNR value: ${userValue}`);
return encoded;
}
// Usage
await setDevicePropValue(device, 0xD198, encodeHighIsoNR(2), 1, txId++);const MONOCHROME_SIMULATIONS = new Set([
0x0009, // ACROS
0x000A, // ACROS+Ye
0x000B, // ACROS+R
0x000C, // ACROS+G
0x0012, // Monochrome
0x0013, // Monochrome+Ye
0x0014, // Monochrome+R
0x0015, // Monochrome+G
0x001A, // Eterna Cinema BW
]);
async function writePreset(device: USBDevice, preset: Preset, txId: number): Promise<number> {
const isMonochrome = MONOCHROME_SIMULATIONS.has(preset.filmSimulation);
await setDevicePropValue(device, 0xD18E, preset.filmSimulation, 2, txId++);
if (!isMonochrome) {
await setDevicePropValue(device, 0xD196, preset.color, 2, txId++);
}
await setDevicePropValue(device, 0xD198, encodeHighIsoNR(preset.highIsoNR), 1, txId++);
// ... write other properties
return txId;
}async function convertRAW(
device: USBDevice,
rafData: ArrayBuffer,
preset: Preset,
txId: number
): Promise<ArrayBuffer> {
// 1. Write preset properties to camera
txId = await writePreset(device, preset, txId);
// 2. Initiate open capture / conversion session
await ptpTransaction(device, 0x101C, txId++); // InitiateOpenCapture
// 3. Send the RAF file
const sendObjectOp = 0x100D;
await ptpTransaction(device, sendObjectOp, txId++, [], rafData);
// 4. Poll for completion and get JPEG back
const getObjectOp = 0x1009;
const { data: jpegData } = await ptpTransaction(device, getObjectOp, txId++);
if (!jpegData) throw new Error('No JPEG returned from camera');
return jpegData;
}interface FilmKitPreset {
name: string;
filmSimulation: number;
grainEffect: number;
colorChrome: number;
whiteBalance: number;
colorTemperature?: number; // Only used when WB = Color Temp mode (0x0012)
dynamicRange: number;
highlightTone: number;
shadowTone: number;
color: number;
sharpness: number;
highIsoNR: number; // User-facing value (-4 to +4), encode before writing
clarity: number;
}
// Export preset as shareable link
function exportPresetAsLink(preset: FilmKitPreset): string {
const encoded = btoa(JSON.stringify(preset));
return `https://filmkit.eggrice.soy/?preset=${encoded}`;
}
// Import preset from link/text
function importPreset(input: string): FilmKitPreset {
// Handle URL with ?preset= param
try {
const url = new URL(input);
const param = url.searchParams.get('preset');
if (param) return JSON.parse(atob(param));
} catch {}
// Handle raw base64 or JSON
try { return JSON.parse(atob(input)); } catch {}
try { return JSON.parse(input); } catch {}
throw new Error('Invalid preset format');
}USBPcap1:\\.\USBPcap1usb.transfer_type == 0x02.pcapng.pcapngchrome://flagsplugdevsudo usermod -aG plugdev $USER04cb# Check if udev rule is applied
lsusb | grep -i fuji
# Should show Fujifilm device
# Verify permissions
ls -la /dev/bus/usb/$(lsusb | grep -i fuji | awk '{print $2"/"$4}' | tr -d ':')
# Should show rw-rw-rw- or similar open permissionsColorColor Temperature0x0012HighIsoNRQUICK_REFERENCE.md