filmkit-fujifilm-camera
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFilmKit Fujifilm Camera Skill
FilmKit 富士相机工具
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.
由ara.so开发的工具 — 属于Daily 2026 Skills合集。
FilmKit是一款基于浏览器、无需安装的富士X系列相机预设管理器与RAW格式转换器。它采用WebUSB通过PTP(图片传输协议)进行通信——该协议与富士X RAW STUDIO所用协议相同——因此相机自身的图像处理器会负责RAW转JPEG的转换工作。它完全在客户端运行(托管于GitHub Pages),支持桌面端和安卓系统。
What FilmKit Does
FilmKit的功能
- 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
- 预设管理:直接在相机上读取、编辑和写入自定义胶片模拟预设(通过PTP的/
GetDevicePropValue操作D18E–D1A5插槽)SetDevicePropValue - 本地预设库:将预设保存到本地,可在相机与本地存储间拖放操作
- RAW转换与实时预览:将RAF文件发送至相机,获取全质量JPEG文件
- 预设检测:加载RAF文件时自动检测拍摄时使用的预设
- 导入/导出:支持以文件、链接或文本粘贴的方式导入导出预设
- 移动端支持:通过Chrome的WebUSB支持在安卓设备上运行
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
undefined- 基于Chromium的浏览器(Google Chrome、Edge、Brave),桌面端或安卓系统均需支持WebUSB
- 富士X系列相机,通过USB连接(已在X100VI上测试;X-T5、X-H2、X-T30等机型大概率兼容)
- Linux udev规则(若使用Flatpak版Chrome):
bash
undefined/etc/udev/rules.d/99-fujifilm.rules
/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 triggerSUBSYSTEM=="usb", ATTR{idVendor}=="04cb", MODE="0666"
添加规则后重新加载:
```bash
sudo udevadm control --reload-rules && sudo udevadm triggerInstallation / 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 devBuild for production:
bash
npm run buildThe built output is a static site — no server required. Open in Chrome at (or wherever Vite serves it).
http://localhost:5173FilmKit是一个静态TypeScript应用。如需本地运行:
bash
git clone https://github.com/eggricesoy/filmkit.git
cd filmkit
npm install
npm run dev生产环境构建:
bash
npm run build构建输出为静态站点——无需服务器。在Chrome中打开(或Vite实际服务的地址)即可访问。
http://localhost:5173Architecture Overview
架构概述
PTP over WebUSB
基于WebUSB的PTP通信
FilmKit speaks PTP (Picture Transfer Protocol) directly over USB bulk transfers. Key operations:
| 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 |
FilmKit通过USB批量传输直接实现PTP(图片传输协议)通信。关键操作如下:
| PTP操作 | 用途 |
|---|---|
| 读取相机预设属性 |
| 写入相机预设属性 |
| 启动RAW转换会话 |
| 向相机发送RAF文件 |
| 从相机获取转换后的JPEG文件 |
Preset Property Codes
预设属性编码
Fujifilm X-series cameras expose film simulation parameters as device properties in the range –:
0xD18E0xD1A5typescript
// 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;富士X系列相机将胶片模拟参数作为设备属性暴露,编码范围为–:
0xD18E0xD1A5typescript
// 示例属性编码(来自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; // 非线性编码!
const PROP_CLARITY = 0xD199;Native Profile Format
原生配置文件格式
The camera's native profile is 625 bytes and uses different field indices/encoding from RAF file metadata. FilmKit uses a patch-based approach:
d185typescript
// 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;
}相机的原生配置文件大小为625字节,其字段索引和编码方式与RAF文件元数据不同。FilmKit采用补丁式处理:
d185typescript
// 补丁式处理的概念示例
function applyPresetPatch(baseProfile: Uint8Array, changes: PresetChanges): Uint8Array {
// 逐字节复制基础配置文件
const patched = new Uint8Array(baseProfile);
// 仅覆盖用户修改过的字段
// 这样可以保留未修改字段中的EXIF标记值
for (const [fieldIndex, encodedValue] of Object.entries(changes)) {
writeFieldToProfile(patched, parseInt(fieldIndex), encodedValue);
}
return patched;
}Key Code Patterns
核心代码模式
WebUSB Connection
WebUSB连接
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;
}typescript
// 请求访问富士相机
async function connectCamera(): Promise<USBDevice> {
const device = await navigator.usb.requestDevice({
filters: [{ vendorId: 0x04CB }] // 富士厂商ID
});
await device.open();
await device.selectConfiguration(1);
await device.claimInterface(0);
return device;
}Sending a PTP Command
发送PTP命令
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 };
}typescript
// PTP命令包结构
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); // 长度
view.setUint16(4, 0x0001, true); // 类型:命令
view.setUint16(6, operationCode, true); // 操作编码
view.setUint32(8, transactionId, true); // 事务ID
params.forEach((p, i) => {
view.setUint32(12 + i * 4, p, true);
});
return buffer;
}
// 发送PTP操作并读取响应
async function ptpTransaction(
device: USBDevice,
operationCode: number,
transactionId: number,
params: number[] = [],
outData?: ArrayBuffer
): Promise<{ responseCode: number; data?: ArrayBuffer }> {
const endpointOut = 0x02; // 批量输出端点
const endpointIn = 0x81; // 批量输入端点
// 发送命令
const cmd = buildPTPCommand(operationCode, transactionId, params);
await device.transferOut(endpointOut, cmd);
// 若存在数据阶段则发送
if (outData) {
await device.transferOut(endpointOut, outData);
}
// 读取数据响应(如有预期)
const dataResult = await device.transferIn(endpointIn, 512);
// 读取响应包
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);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(`未获取到属性0x${propCode.toString(16)}的数据`);
// PTP数据容器:12字节头部 + 负载
return new DataView(data, 12);
}
// 示例:读取胶片模拟参数
const filmSimView = await getDevicePropValue(device, 0xD18E, txId++);
const filmSimValue = filmSimView.getUint16(0, true);
console.log('胶片模拟编码:', 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++);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;
// 构建数据容器
const dataLength = 12 + byteSize;
const dataBuffer = new ArrayBuffer(dataLength);
const view = new DataView(dataBuffer);
view.setUint32(0, dataLength, true); // 长度
view.setUint16(4, 0x0002, true); // 类型:数据
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
);
}
// 示例:将白平衡设置为色温模式
await setDevicePropValue(device, 0xD191, 0x0012, 2, txId++);
// 现在可以安全设置色温值
await setDevicePropValue(device, 0xD192, 4500, 2, txId++);HighIsoNR Special Encoding
HighIsoNR的特殊编码
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;
}
// Usage
await setDevicePropValue(device, 0xD198, encodeHighIsoNR(2), 1, txId++);HighIsoNR采用非线性专有编码——请勿直接写入原始值:
typescript
// HighIsoNR编码映射(通过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(`无效的HighIsoNR值: ${userValue}`);
return encoded;
}
// 使用示例
await 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;
}黑白胶片模拟会拒绝Color属性的写入操作——需做防护处理:
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++);
// ... 写入其他属性
return txId;
}RAW Conversion Flow
RAW转换流程
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;
}typescript
async function convertRAW(
device: USBDevice,
rafData: ArrayBuffer,
preset: Preset,
txId: number
): Promise<ArrayBuffer> {
// 1. 将预设属性写入相机
txId = await writePreset(device, preset, txId);
// 2. 启动拍摄/转换会话
await ptpTransaction(device, 0x101C, txId++); // InitiateOpenCapture
// 3. 发送RAF文件
const sendObjectOp = 0x100D;
await ptpTransaction(device, sendObjectOp, txId++, [], rafData);
// 4. 轮询完成状态并获取JPEG文件
const getObjectOp = 0x1009;
const { data: jpegData } = await ptpTransaction(device, getObjectOp, txId++);
if (!jpegData) throw new Error('相机未返回JPEG文件');
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 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');
}预设以结构化数据(JSON或编码字符串)导出。导入时:
typescript
interface FilmKitPreset {
name: string;
filmSimulation: number;
grainEffect: number;
colorChrome: number;
whiteBalance: number;
colorTemperature?: number; // 仅在WB设为色温模式(0x0012)时生效
dynamicRange: number;
highlightTone: number;
shadowTone: number;
color: number;
sharpness: number;
highIsoNR: number; // 用户可见值(-4至+4),写入前需编码
clarity: number;
}
// 将预设导出为可分享链接
function exportPresetAsLink(preset: FilmKitPreset): string {
const encoded = btoa(JSON.stringify(preset));
return `https://filmkit.eggrice.soy/?preset=${encoded}`;
}
// 从链接/文本导入预设
function importPreset(input: string): FilmKitPreset {
// 处理带?preset=参数的URL
try {
const url = new URL(input);
const param = url.searchParams.get('preset');
if (param) return JSON.parse(atob(param));
} catch {}
// 处理原始base64或JSON
try { return JSON.parse(atob(input)); } catch {}
try { return JSON.parse(input); } catch {}
throw new Error('无效的预设格式');
}Capturing USB Traffic for New Camera Support
捕获USB流量以支持新相机
To help add support for a new Fujifilm X-series camera:
- Install Wireshark with USBPcap
- Capture on USB bus:
USBPcap1:\\.\USBPcap1 - Filter: (bulk transfers = PTP traffic)
usb.transfer_type == 0x02 - 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)
- Save each capture as
.pcapng - Open a GitHub issue with: camera model, firmware version, all three files, and the parameter values used
.pcapng
如需帮助添加对新富士X系列相机的支持:
- 安装带USBPcap的Wireshark
- 捕获USB总线流量:
USBPcap1:\\.\USBPcap1 - 过滤条件:(批量传输 = PTP流量)
usb.transfer_type == 0x02 - 在X RAW STUDIO中执行以下操作并同时捕获流量:
- 读取配置文件(连接相机,让应用读取相机状态)
- 保存预设(修改所有预设值,保存到插槽)
- RAW转换(加载RAF文件,用预设转换)
- 将每个捕获结果保存为文件
.pcapng - 在GitHub Issue中提交:相机型号、固件版本、三个文件以及使用的参数值
.pcapng
Troubleshooting
故障排查
WebUSB Not Available
WebUSB不可用
- Must use Chrome or Chromium-based browser (Firefox does not support WebUSB)
- On Android, use Chrome (not Firefox for Android)
- Check — ensure "Disable WebUSB" is not enabled
chrome://flags
- 必须使用Chrome或基于Chromium的浏览器(Firefox不支持WebUSB)
- 安卓设备需使用Chrome(而非Firefox for Android)
- 检查——确保"Disable WebUSB"未启用
chrome://flags
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 group:
plugdevsudo usermod -aG plugdev $USER - On Linux with Flatpak Chrome: add udev rule for vendor and reload
04cb
- 确保相机处于USB模式(MTP或PTP,而非大容量存储)
- 未使用Flatpak的Linux系统:检查当前用户是否在组中:
plugdevsudo usermod -aG plugdev $USER - 使用Flatpak版Chrome的Linux系统:添加厂商的udev规则并重新加载
04cb
Permission Denied on Linux
Linux系统权限被拒绝
bash
undefinedbash
undefinedCheck if udev rule is applied
检查udev规则是否生效
lsusb | grep -i fuji
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 ':')
ls -la /dev/bus/usb/$(lsusb | grep -i fuji | awk '{print $2"/"$4}' | tr -d ':')
Should show rw-rw-rw- or similar open permissions
应显示rw-rw-rw-或类似的开放权限
undefinedundefinedPTP Transaction Errors
PTP事务错误
- 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
- 确保没有其他应用(X RAW STUDIO、Capture One等)同时连接到相机
- 同一时间仅能有一个WebUSB客户端占用接口
- 若接口卡住,断开并重新连接相机
Preset Write Rejected
预设写入被拒绝
- Writing property on a monochrome film simulation will be rejected — this is expected behavior (see conditional writes above)
Color - Writing requires WB mode set to
Color Temperaturefirst0x0012 - must use the non-linear encoded value, not the raw user-facing value
HighIsoNR
- 向黑白胶片模拟写入Color属性会被拒绝——这是预期行为(见上文的条件写入)
- 写入Color Temperature需先将WB模式设置为
0x0012 - HighIsoNR必须使用非线性编码后的值,而非原始的用户可见值
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.
在FilmKit界面中,滚动至右侧边栏底部的调试区域 → 点击复制日志 → 粘贴到GitHub Issue中用于提交Bug报告。
Key Links
关键链接
- Live App: https://filmkit.eggrice.soy
- Protocol Reference:
QUICK_REFERENCE.md - Related Projects: rawji, fudge, libgphoto2
- Fuji X Weekly Presets: https://fujixweekly.com/
- 在线应用:https://filmkit.eggrice.soy
- 协议参考:
QUICK_REFERENCE.md - 相关项目:rawji, fudge, libgphoto2
- Fuji X Weekly预设:https://fujixweekly.com/