VFP Reader API
VfpPackageReader 是无状态、只读、与 ArkGraphics 无关的 VFP 容器 API。每个方法都围绕一份调用方提供的 ArrayBuffer 或 DocumentPicker URI 工作;它不缓存文件内容、不修改源文件,也不要求页面挂载 VoxelViewer。
如果你还不确定 Reader 适合哪种需求,先读 Reader API 概览。如果你的业务要决定资产能否编辑、发布或作为权威数据保存,请务必同时读 VFP 可信边界。
最小工作流
import { VoxelKit, VfpDocument, VfpPackageReader, VfpSection, VfpThumbnail } from 'voxel-kit';
const reader: VfpPackageReader = VoxelKit.createVfpReader();
// Picker URI 先读取成字节,后续可复用这份 bytes 做检查和提取。
const bytes: ArrayBuffer = await reader.readUri(pickerUri);
const document: VfpDocument = reader.validate(bytes);
console.info(`VFP ${document.majorVersion}.${document.minorVersion}`);
console.info(`区段:${document.sectionTags().join(', ')}`);
const meta: VfpSection | undefined = document.findSection('META');
if (meta) {
const manifestText: string = reader.readManifestText(bytes);
console.info(manifestText);
}
readUri() 适用于后续要提取区段的情况。parseUri() 和 validateUri() 只返回目录视图,完成后不会保留原始文件内容;需要 payload 时仍应自己保存 readUri() 的返回值。
初始化:VoxelKit
VoxelKit.version(): string
console.info('Voxel Kit version=' + VoxelKit.version());
返回当前 HAR 的版本字符串,可用于诊断日志或你的能力协商记录。它不是 VFP 文件版本;VFP 版本应从 VfpDocument.majorVersion / minorVersion 读取。
VoxelKit.createVfpReader(): VfpPackageReader
const reader = VoxelKit.createVfpReader();
创建一个无状态 Reader。Reader 不持有文件、URI、FD 或 ArkGraphics 资源,因此可以按页面/任务创建;为了让代码表达清楚,通常一个文件读取流程复用一个实例即可。
方法总表:解析、校验、读取与提取
| 方法 | Header/Footer/DIR0 | 目录 CRC | 区段 CRC | 是否读取全文件 | 返回 |
|---|---|---|---|---|---|
parse(bytes) | 是 | 是 | 否 | 调用方已提供 | VfpDocument |
validate(bytes) | 是 | 是 | 全部 | 调用方已提供 | VfpDocument |
readUri(uri) | 否 | 否 | 否 | 是,最大 64 MiB | Promise<ArrayBuffer> |
parseUri(uri) | 是 | 是 | 否 | 是 | Promise<VfpDocument> |
validateUri(uri) | 是 | 是 | 全部 | 是 | Promise<VfpDocument> |
extractSection(bytes, section) | 是 | 是 | 指定项 | 调用方已提供 | 独立 ArrayBuffer |
extractByTag(bytes, tag, chunkId?) | 是 | 是 | 指定项 | 调用方已提供 | 独立 ArrayBuffer |
extractTextByTag(bytes, tag, chunkId?) | 是 | 是 | 指定项 | 调用方已提供 | UTF-8 string |
readManifestText(bytes) | 是 | 是 | META | 调用方已提供 | UTF-8 string |
extractPreviewGlb(bytes) | 是 | 是 | PRVW + GLB 头 | 调用方已提供 | GLB 2.0 ArrayBuffer |
extractThumbnail(bytes) | 是 | 是 | THMB + PNG 头 | 调用方已提供 | VfpThumbnail |
extractThumbnailUri(uri) | 是 | 是 | THMB + PNG 头 | 是,最大 64 MiB | Promise<VfpThumbnail> |
“区段 CRC”是存储态 payload 的 CRC。对于 RLE 区段,Reader 仍返回压缩/存储态字节,并不解码后再校验语义内容。
解析与校验
parse(bytes: ArrayBuffer): VfpDocument
const document = reader.parse(bytes);
parse() 是“快速得到可信目录”的入口。它会:
- 检查最小文件长度(Header 64 B + Footer 64 B)。
- 检查 Header magic
VFPK、Header 大小和支持的版本(当前 1.2、1.3)。 - 从文件最后 64 字节 Footer读取活动目录位置,而不是信任 Header 中可能过期的目录指针。
- 检查 Footer magic
VFPF、版本、目录范围和目录 CRC。 - 检查
DIR0magic、版本、条目数与总长度。 - 检查每个条目的 tag、codec、flags/reserved、payload 范围。
它不会遍历每个 payload 做 CRC,也不会解码 VOX0、校验 sourceHash 或解释 META 的 JSON 结构。适用于“先展示文件信息”“列出区段”“准备按需提取”的路径。
validate(bytes: ArrayBuffer): VfpDocument
const document = reader.validate(bytes);
validate() 先完成 parse(),再顺序计算每一个目录项的存储态 payload CRC 并与目录值比对。它适用于“下载后做完整性检查”“导入前允许继续操作”等场景。
它仍然不等价于 VFP 语义验证:没有恢复 VOX0、没有 RLE 解码、没有 PAL0 语义校验,也没有重算 Header/META 的 source hash。不要根据它的成功显示“模型内容已验证”。
URI 读取:readUri / parseUri / validateUri
const bytes = await reader.readUri(uri);
const parsed = reader.parse(bytes);
const checked = reader.validate(bytes);
// 简洁但不保留 bytes 的写法:
const directoryOnly = await reader.parseUri(uri);
const validatedDirectory = await reader.validateUri(uri);
| 方法 | 适合场景 | FD/内存语义 |
|---|---|---|
readUri(uri) | 还要提取 META/PRVW/任意区段 | Reader 打开并关闭 FD;返回的完整字节由调用方持有。 |
parseUri(uri) | 只要版本和目录 | 内部读完整文件、解析后释放缓冲和 FD。 |
validateUri(uri) | 只要完整性结果和目录 | 内部读完整文件、校验全部 CRC、释放缓冲和 FD。 |
三个 URI 方法都要求 URI 仍具备 DocumentPicker 授权,单文件最大 64 MiB。请传入 Picker 原样返回的 URI;不要将它变成 POSIX 路径、提前关闭 FD,或在 await 中间改变它的授权上下文。
VfpDocument:目录视图
VfpDocument 不保留区段 payload,只记录当前输入文件的目录信息。它很轻,适合保存到页面状态中;需要实际字节时仍调用 Reader 并传入同一份原始 ArrayBuffer。
字段
| 字段 | 类型 | 含义与使用建议 |
|---|---|---|
majorVersion / minorVersion | number | Header/Footer 中一致的 VFP 版本,例如 1 / 3。 |
fileSize | number | 输入 ArrayBuffer 的总字节数。 |
sourceHash | string | Header 中 32 字节 hash 的小写十六进制表现。Reader 不重算它。 |
directoryOffset / directoryLength | number | 最终 Footer 指向的活动 DIR0 位置和长度。 |
generation | number | Footer 记录的目录代际。 |
sections | Array<VfpSection> | 目录项数组,顺序与文件的 DIR0 一致。 |
sectionTags(): Array<string>
const tags = document.sectionTags(); // 例如 ['META', 'PAL0', 'CHIX', 'VOX0', 'PMSH']
返回每个 tag 一次,按它在目录中第一次出现的顺序。适合构建“文件包含哪些能力”的概要 UI;若要得到同一 tag 的每个 chunk,使用 entries(tag)。
entries(tag: string): Array<VfpSection>
const voxelChunks = document.entries('VOX0');
返回该四字符 tag 的所有目录项,保持目录顺序。未找到时返回空数组,不抛错。对可能按 chunk 分段的区段,应使用它枚举所有项,而不是只调用 findSection()。
findSection(tag: string, chunkId = -1): VfpSection | undefined
const meta = document.findSection('META'); // 默认全局 chunk -1
const chunk = document.findSection('VOX0', 17); // 只取 chunk 17
返回第一个同时满足 tag 和 chunkId 的项;未找到返回 undefined,因此调用 extractSection() 前要先判断。全局区段使用 chunkId -1,不要把不存在的全局项当作 chunk 0。
VfpSection:一个目录项
| 字段 | 类型 | 语义 |
|---|---|---|
index | number | 在当前 DIR0 的序号;extractSection() 用它确认 Section 来自同一份输入。 |
tag | string | 四字符 ASCII 区段标识,例如 META、PRVW、VOX0。 |
codec | number | 当前容器目录允许 0=RAW、1=RLE8。Reader 不在提取时解码。 |
flags | number | 原始目录 flags;当前 Reader 只接受 0。 |
chunkId | number | 所属 chunk;全局区段是 -1。 |
offset / length | number | 文件内存储态 payload 的字节位置和实际长度。 |
rawLength | number | 格式声明的解码后长度;可能与 length 不同。 |
crc32 | number | 存储态 payload 的 CRC32。 |
不要修改 VfpSection 字段后再交给 Reader。它是描述对象,不是写入 API。
VfpThumbnail:可选 PNG 缩略图
VfpThumbnail 是 extractThumbnail() 成功后返回的轻量描述对象:
| 字段 | 类型 | 语义 |
|---|---|---|
mimeType | string | 当前固定为 image/png。 |
width / height | number | 从 PNG 首个 IHDR 读取的像素尺寸。 |
bytes | ArrayBuffer | 已通过 VFP 区段 CRC 与 PNG 头检查的独立 PNG 字节副本。 |
Reader 不创建 PixelMap、不写入沙箱文件,也不显示图片;宿主可按自己的缓存、图片解码和 UI 策略使用 bytes。这保证 Reader 不会接管页面资源生命周期。
按区段提取
extractSection(bytes, section): ArrayBuffer
const meta = document.findSection('META');
if (meta) {
const payload = reader.extractSection(bytes, meta);
}
它会重新解析 bytes,确认 section.index 在范围内、该位置的 tag/chunkId 与传入对象一致,校验该项 CRC,再返回独立副本。因此:
- 不能将 A 文件的
VfpSection传给 B 文件;会抛出“区段不属于当前文件”。 - 返回值可由业务保存/传递,不与输入 ArrayBuffer 共享 payload 视图。
- 返回的是存储态字节,codec 为 RLE8 时不是展开后的体素数据。
extractByTag(bytes, tag, chunkId = -1): ArrayBuffer
const palettePayload = reader.extractByTag(bytes, 'PAL0');
const voxelChunkPayload = reader.extractByTag(bytes, 'VOX0', 7);
这是 findSection() + extractSection() 的便捷组合。不存在的 tag/chunk 会抛出 VFP 不包含区段:TAG[chunkId]。它适合调用方已确定必需区段的流程;用于可选区段时先 findSection(),避免把“可选缺失”当作异常。
extractTextByTag(bytes, tag, chunkId = -1): string
const sceneText = reader.extractTextByTag(bytes, 'SCNE');
提取一个区段、校验其 CRC,再按 UTF-8 解码为字符串。它不会验证字符串是不是 JSON,也不会验证区段在 VFP 规范中是否应为文本。若业务需要 JSON,自己在捕获异常后 JSON.parse()。
readManifestText(bytes): string
const metaJsonText = reader.readManifestText(bytes);
const meta = JSON.parse(metaJsonText);
这是 extractTextByTag(bytes, 'META') 的语义别名。它保证 META payload 的 CRC 已通过,但不保证其 UTF-8 文本一定是可解析 JSON;格式语义校验仍由调用方/完整格式工具完成。
提取 PRVW 内嵌 GLB
extractPreviewGlb(bytes): ArrayBuffer
const previewSection = document.findSection('PRVW');
if (previewSection) {
const glb: ArrayBuffer = reader.extractPreviewGlb(bytes);
// 交给宿主自己的文件保存、分享或 GLB 检查逻辑。
}
该方法会提取并 CRC 校验 PRVW,随后验证:
- 至少有 12 字节 GLB Header;
- magic 为
glTF; - GLB version 为 2;
- Header 声明的文件长度等于实际 payload 长度。
它不是“从 VFP 导出 GLB”。若没有 PRVW,或 payload 并非合法 GLB 2.0,就会抛错;Reader 不会从 VOX0 重建新的 GLB。没有 PRVW 不影响 VFP 的目录解析或 VoxelViewer 正式体素预览。
提取 THMB 缩略图
extractThumbnail(bytes): VfpThumbnail
const thumbnailSection = document.findSection('THMB');
if (thumbnailSection) {
const thumbnail: VfpThumbnail = reader.extractThumbnail(bytes);
console.info(thumbnail.mimeType); // image/png
console.info(`${thumbnail.width}×${thumbnail.height}`);
// thumbnail.bytes 是独立 PNG ArrayBuffer;交给宿主的图片层或缓存层处理。
}
当前支持的 THMB v1 形式是一个全局、直接存储的 PNG:chunkId=-1、codec=RAW(0)、
flags=0、length=rawLength。Reader 会校验 THMB 区段 CRC、PNG signature、首个 IHDR 和正的宽高;
它不会完整解码 PNG 像素,也不会验证图片是否和当前体素模型画面一致。
THMB 是展示元数据,不是模型预览缓存,更不是权威体素源。它适合用于“最近打开”“作品列表”或“文件详情”的封面;VoxelViewer 的模型导入路径不会读取它,所以缩略图不会拖慢 ArkGraphics 场景创建或首帧。
extractThumbnailUri(uri): Promise<VfpThumbnail>
try {
const thumbnail = await reader.extractThumbnailUri(pickerUri);
this.coverWidth = thumbnail.width;
this.coverHeight = thumbnail.height;
// 在此将 thumbnail.bytes 交给你的图片解码/缓存逻辑。
} catch (error) {
// 文件没有 THMB 或缩略图不符合当前 PNG 约定时,只隐藏封面。
// 不应因此拒绝同一文件的 VoxelViewer 预览。
}
该便捷方法在 Reader 内部读取 Picker URI、关闭 FD 后返回缩略图;它适合只需要封面的列表页。
若同一流程还要读取 META、PRVW 或多个区段,请先 readUri() 一次,再基于同一个 ArrayBuffer 重复提取,避免重复读取整份 VFP。
错误与恢复策略
| 错误/现象 | 典型原因 | 调用方应做什么 |
|---|---|---|
VFP 文件过短 / 不是有效的 VFP Header | 非 VFP、文件截断或 Header 损坏 | 拒绝文件;若你的选择器允许 JSON,交给 JSON 分支。 |
不支持的 VFP 版本 | 不是 1.2/1.3 | 提示升级 SDK 或使用兼容工具。 |
VFP Footer 无效 / 目录位置无效 / DIR0 长度无效 | Footer/目录损坏或越界 | 视为不可读取,不提取任何 payload。 |
VFP 目录 CRC 校验失败 | 目录在传输/写入中损坏 | 拒绝文件,重新下载/导出。 |
VFP 区段 CRC 校验失败 | 特定 payload 损坏 | 不使用该项;validate() 则会拒绝整份完整性检查。 |
VFP 区段标识无效 / codec/flags/reserved 无效 | 目录不符合当前 Reader 的容器约束 | 拒绝,不能猜测未定义语义。 |
VFP 偏移超出安全范围 | 超出 ArkTS number 安全表示范围 | 拒绝,避免不安全寻址。 |
VFP 文件不能超过 64 MB | URI 文件超过 Reader 限制 | 改用容量受控的业务服务/未来流式能力。 |
VFP PRVW 不是有效 GLB 2.0 | 预览缓存错误或格式不符 | 忽略 PRVW;不要把它当作权威数据。 |
VFP 不包含 THMB 缩略图 | 文件没有可选缩略图区段 | 隐藏封面占位,不应拒绝模型预览。 |
VFP THMB 格式无效:仅支持全局 RAW PNG | THMB 不是当前 v1 的全局 RAW 形式 | 忽略缩略图;不要猜测压缩或私有格式。 |
VFP THMB 不是有效 PNG / VFP THMB PNG 尺寸无效 | 图片 payload 或 IHDR 损坏 | 隐藏封面,并保留 VFP 其他读取能力。 |
所有同步 API 直接 throw Error,异步 URI API 以 rejected Promise 返回。把错误消息展示给用户前,可统一替换成业务文案;日志中记录 Reader 版本、文件大小和错误类别,避免记录完整私有 URI 或 payload。
两种推荐用法
1. 文件信息页:快速解析,再按需读取
const bytes = await reader.readUri(uri);
const document = reader.parse(bytes);
this.title = `VFP ${document.majorVersion}.${document.minorVersion}`;
this.tags = document.sectionTags();
this.metaText = document.findSection('META') ? reader.readManifestText(bytes) : '';
适用于用户先浏览信息,再决定是否打开预览/分享。要显示“文件未损坏”,改用 validate(bytes),并用可信边界页面定义你的产品文案。
2. 只拿预览 GLB:先检查可选区段
const bytes = await reader.readUri(uri);
const document = reader.parse(bytes);
if (!document.findSection('PRVW')) {
// 业务可以隐藏“导出内嵌预览”按钮,不应视为文件错误。
return;
}
const glb = reader.extractPreviewGlb(bytes);
此流程只提取原文件已经带有的缓存预览;它没有将体素数据转换为 GLB 的成本或语义保证。
3. 资产列表:按需显示缩略图
const bytes = await reader.readUri(uri);
const document = reader.parse(bytes);
if (document.findSection('THMB')) {
const thumbnail = reader.extractThumbnail(bytes);
// 仅在业务图片层成功解码时显示封面;失败时显示默认封面即可。
}
不要用 THMB 的存在与否判断模型是否可预览、可编辑或可发布。它只代表文件可选地携带了一张 PNG。
相关页面
- VFP 可信边界:理解 parse/validate/preview 成功分别说明什么。
- VoxelViewer API:在页面中把 JSON/VFP 直接显示为体素模型。
- VFP 技术标准:获取容器、区段和缓存的格式规范。