跳到主要内容

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 MiBPromise<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 MiBPromise<VfpThumbnail>

“区段 CRC”是存储态 payload 的 CRC。对于 RLE 区段,Reader 仍返回压缩/存储态字节,并不解码后再校验语义内容。

解析与校验

parse(bytes: ArrayBuffer): VfpDocument

const document = reader.parse(bytes);

parse() 是“快速得到可信目录”的入口。它会:

  1. 检查最小文件长度(Header 64 B + Footer 64 B)。
  2. 检查 Header magic VFPK、Header 大小和支持的版本(当前 1.2、1.3)。
  3. 文件最后 64 字节 Footer读取活动目录位置,而不是信任 Header 中可能过期的目录指针。
  4. 检查 Footer magic VFPF、版本、目录范围和目录 CRC。
  5. 检查 DIR0 magic、版本、条目数与总长度。
  6. 检查每个条目的 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 / minorVersionnumberHeader/Footer 中一致的 VFP 版本,例如 1 / 3
fileSizenumber输入 ArrayBuffer 的总字节数。
sourceHashstringHeader 中 32 字节 hash 的小写十六进制表现。Reader 不重算它。
directoryOffset / directoryLengthnumber最终 Footer 指向的活动 DIR0 位置和长度。
generationnumberFooter 记录的目录代际。
sectionsArray<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:一个目录项

字段类型语义
indexnumber在当前 DIR0 的序号;extractSection() 用它确认 Section 来自同一份输入。
tagstring四字符 ASCII 区段标识,例如 METAPRVWVOX0
codecnumber当前容器目录允许 0=RAW1=RLE8。Reader 不在提取时解码。
flagsnumber原始目录 flags;当前 Reader 只接受 0
chunkIdnumber所属 chunk;全局区段是 -1
offset / lengthnumber文件内存储态 payload 的字节位置和实际长度。
rawLengthnumber格式声明的解码后长度;可能与 length 不同。
crc32number存储态 payload 的 CRC32。

不要修改 VfpSection 字段后再交给 Reader。它是描述对象,不是写入 API。

VfpThumbnail:可选 PNG 缩略图

VfpThumbnailextractThumbnail() 成功后返回的轻量描述对象:

字段类型语义
mimeTypestring当前固定为 image/png
width / heightnumber从 PNG 首个 IHDR 读取的像素尺寸。
bytesArrayBuffer已通过 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=-1codec=RAW(0)flags=0length=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 MBURI 文件超过 Reader 限制改用容量受控的业务服务/未来流式能力。
VFP PRVW 不是有效 GLB 2.0预览缓存错误或格式不符忽略 PRVW;不要把它当作权威数据。
VFP 不包含 THMB 缩略图文件没有可选缩略图区段隐藏封面占位,不应拒绝模型预览。
VFP THMB 格式无效:仅支持全局 RAW PNGTHMB 不是当前 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。

相关页面