filmkit-fujifilm-camera
Browser-based preset manager and RAW converter for Fujifilm X-series cameras using WebUSB and PTP protocol
What this skill does
# FilmKit Fujifilm Camera Skill
> Skill by [ara.so](https://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):
```bash
# /etc/udev/rules.d/99-fujifilm.rules
SUBSYSTEM=="usb", ATTR{idVendor}=="04cb", MODE="0666"
```
Reload rules after adding:
```bash
sudo udevadm control --reload-rules && sudo udevadm trigger
```
---
## Installation / Setup (Development)
FilmKit is a static TypeScript app. To run locally:
```bash
git clone https://github.com/eggricesoy/filmkit.git
cd filmkit
npm install
npm run dev
```
Build for production:
```bash
npm run build
```
The 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`:
```typescript
// 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**:
```typescript
// 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
```typescript
// 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
```typescript
// 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
```typescript
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
```typescript
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:
```typescript
// 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,
[Related in General
modeling-omnistudio-epc-catalog
IncludedSalesforce Industries CME EPC product-modeling skill for Product2-based catalog creation. Use when creating EPC products, configuring product attributes, building offer bundles with Product Child Items, or reviewing EPC DataPack JSON metadata for product catalog changes. TRIGGER when: user creates or updates Product2 EPC records, AttributeAssignment payloads, AttributeMetadata/AttributeDefaultValues, Offer bundles, or ProductChildItem relationships. DO NOT TRIGGER when: designing OmniScripts/FlexCards/Integration Procedures (use building-omnistudio-omniscript, building-omnistudio-flexcard, or building-omnistudio-integration-procedure), implementing Apex business logic (use generating-apex), or troubleshooting deployment pipelines (use deploying-metadata).
relationship-science-coach
IncludedUse this skill for direct, practical adult relationship coaching: couples conflict, repair, trust, marriage, dating, flirting, attachment patterns, emotional connection, sex, desire differences, eroticism, kink negotiation, affection, love languages, breakups, and long-term passion. Draw on Gottman, EFT and Hold Me Tight, attachment science, modern sex research, Perel, Nagoski, Kerner, Schnarch, Love and Stosny, and flexible love-language tools. Be concrete and low-hedge. Redirect only for imminent danger, abuse, coercive control, minors, non-consent, self-harm, stalking, or medical/legal/psychiatric decisions.
building-sf-integrations
IncludedSalesforce integration architecture and runtime plumbing with 120-point scoring. Use this skill to set up Named Credentials, External Credentials, External Services, REST/SOAP callout patterns, Platform Events, and Change Data Capture. TRIGGER when: user sets up Named Credentials, External Services, REST/SOAP callouts, Platform Events, CDC, or touches .namedCredential-meta.xml files. DO NOT TRIGGER when: Connected App/OAuth config (use configuring-connected-apps), Apex-only logic (use generating-apex), or data import/export (use handling-sf-data).
venue-templates
IncludedAccess comprehensive LaTeX templates, formatting requirements, and submission guidelines for major scientific publication venues (Nature, Science, PLOS, IEEE, ACM), academic conferences (NeurIPS, ICML, CVPR, CHI), research posters, and grant proposals (NSF, NIH, DOE, DARPA). This skill should be used when preparing manuscripts for journal submission, conference papers, research posters, or grant proposals and need venue-specific formatting requirements and templates.
let-fate-decide
IncludedDraws the 12 Houses of the Zodiac Tarot spread to inject entropy into planning when prompts are vague, ambiguous, or casually delegated. Interprets the spread to guide next steps. Use when the user says 'let fate decide', 'YOLO', 'whatever', 'idk', or other nonchalant phrases, makes Yu-Gi-Oh references, or when you are about to arbitrarily pick between multiple reasonable approaches. Prefer over ask-questions-if-underspecified when the user's tone is casual or playful rather than precision-seeking.
net-ops
IncludedCross-platform network troubleshooting (Windows, macOS, Linux) via local or remote shell. Use for: DNS broken, can't resolve hostnames, nslookup/dig works but apps fail, NRPT, WFP, scutil, /etc/resolver, systemd-resolved, /etc/resolv.conf, NetworkManager, VPN DNS leak residue (ProtonVPN/Mullvad/WireGuard/AnyConnect), AV/firewall blocking DNS or DoH, Tailscale DNS interaction, intermittent connectivity, remote diagnostics over SSH.