Filmkit Fujifilm Camera

作者 reason-machines2384a003145a无许可证83 个星标收录于 2026年10月8日更新于 2026年10月8日仓库3个月前更新

Browser-based preset manager and RAW converter for Fujifilm X-series cameras using WebUSB and PTP protocol

AI 生成的概览

指导构建基于浏览器的富士 X 系列相机预设管理与 RAW 转换工具,通过 WebUSB 和 PTP 与相机通信。

功能
该技能介绍 FilmKit:一个纯客户端网页应用,用于管理富士 X 系列胶片模拟预设,并借助相机自身处理器把 RAF 文件转换为 JPEG。内容涵盖 WebUSB 连接建立、PTP 命令与数据包构造、0xD18E–0xD1A5 范围内预设属性的读写、625 字节原生配置文件的补丁式修改、HighIsoNR 编码、黑白胶片模拟的条件写入以及 RAW 转换流程。此外还说明预设导入导出格式、用于新增机型支持的 USB 抓包方法以及故障排查步骤。
适用场景
适用于实现或扩展通过 USB 读写富士相机预设的浏览器工具,或执行机内 RAW 转 JPEG 的场景。也适合排查与富士 X 系列机身的 WebUSB 或 PTP 通信问题。
运行要求
桌面或 Android 上支持 WebUSB 的 Chromium 内核浏览器、通过 USB 连接的富士 X 系列相机;Linux 上需为厂商 ID 04cb 添加 udev 规则(Flatpak 版 Chrome)。开发环境需要 git、npm 和 Vite 开发服务器。该技能不附带脚本,仅为说明文档。

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):
bash
# /etc/udev/rules.d/99-fujifilm.rulesSUBSYSTEM=="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.gitcd filmkitnpm installnpm 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 OperationPurpose
GetDevicePropValueRead a camera preset property
SetDevicePropValueWrite a camera preset property
InitiateOpenCaptureStart RAW conversion session
SendObjectSend RAF file to camera
GetObjectRetrieve 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 approachfunction 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 cameraasync 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 structurefunction 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 responseasync 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 simulationconst 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 modeawait setDevicePropValue(device, 0xD191, 0x0012, 2, txId++);// Now safe to set Color Temperature valueawait 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,  [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;}
// Usageawait setDevicePropValue(device, 0xD198, encodeHighIsoNR(2), 1, txId++);

Conditional Writes (Monochrome Film Simulations)

Monochrome film simulations reject Color property writes — guard against this:

typescript
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;}

RAW Conversion Flow

typescript
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;}

Preset Import/Export Format

Presets are exported as structured data (JSON or encoded strings). When importing:

typescript
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 linkfunction exportPresetAsLink(preset: FilmKitPreset): string {  const encoded = btoa(JSON.stringify(preset));  return `https://filmkit.eggrice.soy/?preset=${encoded}`;}
// Import preset from link/textfunction 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');}

Capturing USB Traffic for New Camera Support

To help add support for a new Fujifilm X-series camera:

  1. Install Wireshark with USBPcap
  2. Capture on USB bus: USBPcap1:\\.\USBPcap1
  3. Filter: usb.transfer_type == 0x02 (bulk transfers = PTP traffic)
  4. Perform these actions in X RAW STUDIO while capturing:
    • Profile read (connect and let app read camera state)
    • Preset save (change all preset values, save to a slot)
    • RAW conversion (load RAF, convert with a preset)
  5. Save each capture as .pcapng
  6. Open a GitHub issue with: camera model, firmware version, all three .pcapng files, and the parameter values used

Troubleshooting

WebUSB Not Available

  • Must use Chrome or Chromium-based browser (Firefox does not support WebUSB)
  • On Android, use Chrome (not Firefox for Android)
  • Check chrome://flags — ensure "Disable WebUSB" is not enabled

Camera Not Detected

  • Ensure the camera is in USB mode (MTP or PTP, not Mass Storage)
  • On Linux without Flatpak: check that your user is in the plugdev group: sudo usermod -aG plugdev $USER
  • On Linux with Flatpak Chrome: add udev rule for vendor 04cb and reload

Permission Denied on Linux

bash
# Check if udev rule is appliedlsusb | grep -i fuji# Should show Fujifilm device
# Verify permissionsls -la /dev/bus/usb/$(lsusb | grep -i fuji | awk '{print $2"/"$4}' | tr -d ':')# Should show rw-rw-rw- or similar open permissions

PTP Transaction Errors

  • Ensure no other app (X RAW STUDIO, Capture One, etc.) is connected to the camera simultaneously
  • Only one WebUSB consumer can hold the interface at a time
  • Disconnect and reconnect the camera if the interface gets stuck

Preset Write Rejected

  • Writing Color property on a monochrome film simulation will be rejected — this is expected behavior (see conditional writes above)
  • Writing Color Temperature requires WB mode set to 0x0012 first
  • HighIsoNR must use the non-linear encoded value, not the raw user-facing value

Debug Log

In the FilmKit UI, scroll to the Debug section at the bottom of the right sidebar → click Copy Log → paste into a GitHub issue for bug reports.


Key Links

来源与署名

来源:reason-machines/trending-skills位于skills/filmkit-fujifilm-camera提交2384a00

许可证: 无许可证

内容归原作者所有。SourceWeft 从公开仓库中收录这些内容。

举报或申请下架