@flighthq/texture-formats 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/byteReader.d.ts +14 -0
  2. package/dist/byteReader.d.ts.map +1 -0
  3. package/dist/byteReader.js +51 -0
  4. package/dist/byteReader.js.map +1 -0
  5. package/dist/detectTextureContainer.d.ts +2 -0
  6. package/dist/detectTextureContainer.d.ts.map +1 -0
  7. package/dist/detectTextureContainer.js +31 -0
  8. package/dist/detectTextureContainer.js.map +1 -0
  9. package/dist/index.d.ts +7 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +7 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/parseAtf.d.ts +3 -0
  14. package/dist/parseAtf.d.ts.map +1 -0
  15. package/dist/parseAtf.js +139 -0
  16. package/dist/parseAtf.js.map +1 -0
  17. package/dist/parseBasis.d.ts +3 -0
  18. package/dist/parseBasis.d.ts.map +1 -0
  19. package/dist/parseBasis.js +82 -0
  20. package/dist/parseBasis.js.map +1 -0
  21. package/dist/parseDds.d.ts +3 -0
  22. package/dist/parseDds.d.ts.map +1 -0
  23. package/dist/parseDds.js +151 -0
  24. package/dist/parseDds.js.map +1 -0
  25. package/dist/parseKtx2.d.ts +3 -0
  26. package/dist/parseKtx2.d.ts.map +1 -0
  27. package/dist/parseKtx2.js +173 -0
  28. package/dist/parseKtx2.js.map +1 -0
  29. package/dist/selectTextureContainer.d.ts +4 -0
  30. package/dist/selectTextureContainer.d.ts.map +1 -0
  31. package/dist/selectTextureContainer.js +15 -0
  32. package/dist/selectTextureContainer.js.map +1 -0
  33. package/dist/textureLevelLayout.d.ts +8 -0
  34. package/dist/textureLevelLayout.d.ts.map +1 -0
  35. package/dist/textureLevelLayout.js +109 -0
  36. package/dist/textureLevelLayout.js.map +1 -0
  37. package/package.json +37 -0
  38. package/src/byteReader.test.ts +105 -0
  39. package/src/detectTextureContainer.test.ts +33 -0
  40. package/src/parseAtf.test.ts +288 -0
  41. package/src/parseBasis.test.ts +124 -0
  42. package/src/parseDds.test.ts +146 -0
  43. package/src/parseKtx2.test.ts +135 -0
  44. package/src/selectTextureContainer.test.ts +38 -0
  45. package/src/textureLevelLayout.test.ts +45 -0
@@ -0,0 +1,173 @@
1
+ import { createByteReader, hasByteReaderBytes, readByteReaderU32, readByteReaderU64, skipByteReader, } from './byteReader';
2
+ // Parses a KTX2 container (Khronos KTX 2.0) into a `TextureContainer`, or returns `null` if the bytes
3
+ // are not KTX2, are truncated, or carry a `vkFormat`/supercompression this package does not map yet.
4
+ //
5
+ // Reads the 12-byte identifier, the fixed header (`vkFormat`, `pixelWidth/Height/Depth`, `layerCount`,
6
+ // `faceCount`, `levelCount`, `supercompressionScheme`), and the level index (per-level `byteOffset` /
7
+ // `byteLength`). The Data Format Descriptor and key/value data are skipped — `vkFormat` alone
8
+ // determines the format, except when it is `VK_FORMAT_UNDEFINED` (0), where the payload is Basis: a
9
+ // `BasisLZ`-supercompressed level is ETC1S, otherwise it is UASTC (the DFD's exact colorModel is not
10
+ // deep-parsed). Supercompressed levels are reported as single compressed blobs (they cannot be split
11
+ // per face/layer without inflating); uncompressed levels are split into their per-`(layer, face)`
12
+ // sub-images, nested mip-major → layer → face to match the KTX2 image order.
13
+ export function parseKtx2(bytes) {
14
+ if (!hasKtx2Identifier(bytes))
15
+ return null;
16
+ // 12 id + 68 header/index (17 u32 = 68 bytes) before the level index at offset 80.
17
+ if (bytes.byteLength < ktx2LevelIndexOffset)
18
+ return null;
19
+ const reader = createByteReader(bytes, 12);
20
+ const vkFormat = readByteReaderU32(reader);
21
+ skipByteReader(reader, 4); // typeSize
22
+ const pixelWidth = readByteReaderU32(reader);
23
+ const pixelHeight = readByteReaderU32(reader);
24
+ const pixelDepth = readByteReaderU32(reader);
25
+ const layerCount = readByteReaderU32(reader);
26
+ const faceCount = readByteReaderU32(reader);
27
+ const levelCount = readByteReaderU32(reader);
28
+ const supercompressionScheme = readByteReaderU32(reader);
29
+ // dfd/kvd/sgd index (2 u32 + 2 u32 + 2 u64 = 32 bytes) — not needed to locate levels.
30
+ const supercompression = ktx2Supercompression[supercompressionScheme];
31
+ if (supercompression === undefined)
32
+ return null;
33
+ const format = mapKtx2Format(vkFormat, supercompressionScheme);
34
+ if (format === null)
35
+ return null;
36
+ const width = Math.max(1, pixelWidth);
37
+ const height = Math.max(1, pixelHeight);
38
+ const depth = Math.max(1, pixelDepth);
39
+ const layers = Math.max(1, layerCount);
40
+ const faces = faceCount === 6 ? 6 : 1;
41
+ const levelCountPresent = Math.max(1, levelCount);
42
+ reader.offset = ktx2LevelIndexOffset;
43
+ if (!hasByteReaderBytes(reader, levelCountPresent * 24))
44
+ return null;
45
+ const levels = [];
46
+ const imagesPerLevel = layers * faces;
47
+ for (let mip = 0; mip < levelCountPresent; mip += 1) {
48
+ const byteOffset = readByteReaderU64(reader);
49
+ const byteLength = readByteReaderU64(reader);
50
+ skipByteReader(reader, 8); // uncompressedByteLength
51
+ if (byteOffset + byteLength > bytes.byteLength)
52
+ return null;
53
+ const mipWidth = Math.max(1, width >> mip);
54
+ const mipHeight = Math.max(1, height >> mip);
55
+ const splittable = supercompression === 'None' && imagesPerLevel > 1 && byteLength % imagesPerLevel === 0;
56
+ if (!splittable) {
57
+ levels.push({ byteLength, byteOffset, height: mipHeight, width: mipWidth });
58
+ continue;
59
+ }
60
+ const imageSize = byteLength / imagesPerLevel;
61
+ for (let image = 0; image < imagesPerLevel; image += 1) {
62
+ levels.push({
63
+ byteLength: imageSize,
64
+ byteOffset: byteOffset + image * imageSize,
65
+ height: mipHeight,
66
+ width: mipWidth,
67
+ });
68
+ }
69
+ }
70
+ return {
71
+ depth,
72
+ faces,
73
+ format,
74
+ height,
75
+ layers,
76
+ levels,
77
+ mipLevels: levelCountPresent,
78
+ supercompression,
79
+ width,
80
+ };
81
+ }
82
+ function hasKtx2Identifier(bytes) {
83
+ if (bytes.byteLength < 12)
84
+ return false;
85
+ for (let i = 0; i < 12; i += 1) {
86
+ if (bytes[i] !== ktx2Identifier[i])
87
+ return false;
88
+ }
89
+ return true;
90
+ }
91
+ function mapKtx2Format(vkFormat, supercompressionScheme) {
92
+ if (vkFormat === 0)
93
+ return supercompressionScheme === 1 ? 'etc1s' : 'uastc';
94
+ return ktx2VkFormat[vkFormat] ?? null;
95
+ }
96
+ // «KTX 20»\r\n\x1A\n
97
+ const ktx2Identifier = [0xab, 0x4b, 0x54, 0x58, 0x20, 0x32, 0x30, 0xbb, 0x0d, 0x0a, 0x1a, 0x0a];
98
+ // 12 (identifier) + 48 (header: 9 u32) + 20 (dfd/kvd offsets: 2 u32 each + 2 u64) = 80.
99
+ const ktx2LevelIndexOffset = 80;
100
+ const ktx2Supercompression = {
101
+ 0: 'None',
102
+ 1: 'BasisLZ',
103
+ 2: 'Zstd',
104
+ 3: 'ZLIB',
105
+ };
106
+ // Curated Vulkan `vkFormat` → `TextureContainerFormat`. Uncompressed 8-bit/float, the full BC set, ETC2/EAC,
107
+ // and ASTC LDR block sizes. ASTC sRGB codes collapse onto their unorm block (the vocabulary keys ASTC by
108
+ // block size, not color space); BC1 RGB and RGBA codes both map to `bc1`. Unmapped codes → `null`.
109
+ const ktx2VkFormat = {
110
+ 9: 'r8unorm',
111
+ 16: 'rg8unorm',
112
+ 37: 'rgba8unorm',
113
+ 43: 'rgba8Srgb',
114
+ 44: 'bgra8unorm',
115
+ 50: 'bgra8Srgb',
116
+ 97: 'rgba16f',
117
+ 109: 'rgba32f',
118
+ 131: 'bc1',
119
+ 132: 'bc1Srgb',
120
+ 133: 'bc1',
121
+ 134: 'bc1Srgb',
122
+ 135: 'bc2',
123
+ 136: 'bc2Srgb',
124
+ 137: 'bc3',
125
+ 138: 'bc3Srgb',
126
+ 139: 'bc4',
127
+ 140: 'bc4Snorm',
128
+ 141: 'bc5',
129
+ 142: 'bc5Snorm',
130
+ 143: 'bc6hUfloat',
131
+ 144: 'bc6hSfloat',
132
+ 145: 'bc7',
133
+ 146: 'bc7Srgb',
134
+ 147: 'etc2Rgb',
135
+ 148: 'etc2RgbSrgb',
136
+ 149: 'etc2RgbA1',
137
+ 150: 'etc2RgbA1Srgb',
138
+ 151: 'etc2Rgba',
139
+ 152: 'etc2RgbaSrgb',
140
+ 153: 'eacR11',
141
+ 154: 'eacR11Snorm',
142
+ 155: 'eacRg11',
143
+ 156: 'eacRg11Snorm',
144
+ 157: 'astc4x4',
145
+ 158: 'astc4x4',
146
+ 159: 'astc5x4',
147
+ 160: 'astc5x4',
148
+ 161: 'astc5x5',
149
+ 162: 'astc5x5',
150
+ 163: 'astc6x5',
151
+ 164: 'astc6x5',
152
+ 165: 'astc6x6',
153
+ 166: 'astc6x6',
154
+ 167: 'astc8x5',
155
+ 168: 'astc8x5',
156
+ 169: 'astc8x6',
157
+ 170: 'astc8x6',
158
+ 171: 'astc8x8',
159
+ 172: 'astc8x8',
160
+ 173: 'astc10x5',
161
+ 174: 'astc10x5',
162
+ 175: 'astc10x6',
163
+ 176: 'astc10x6',
164
+ 177: 'astc10x8',
165
+ 178: 'astc10x8',
166
+ 179: 'astc10x10',
167
+ 180: 'astc10x10',
168
+ 181: 'astc12x10',
169
+ 182: 'astc12x10',
170
+ 183: 'astc12x12',
171
+ 184: 'astc12x12',
172
+ };
173
+ //# sourceMappingURL=parseKtx2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parseKtx2.js","sourceRoot":"","sources":["../src/parseKtx2.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { TextureContainer } from '@flighthq/types';
2
+ import type { TextureContainerFormat } from '@flighthq/types';
3
+ export declare function selectTextureContainer(containers: readonly TextureContainer[], supportedFormats: readonly TextureContainerFormat[]): TextureContainer | null;
4
+ //# sourceMappingURL=selectTextureContainer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selectTextureContainer.d.ts","sourceRoot":"","sources":["../src/selectTextureContainer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAS9D,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,SAAS,gBAAgB,EAAE,EACvC,gBAAgB,EAAE,SAAS,sBAAsB,EAAE,GAClD,gBAAgB,GAAG,IAAI,CAKzB"}
@@ -0,0 +1,15 @@
1
+ // Picks the first container whose `format` a GPU supports from a set of peer `TextureContainer`s,
2
+ // returning `null` when none match. This is the consumer side of `parseAtf`'s peer array: an ATF holds
3
+ // the same texture in several GPU encodings (DXT + PVRTC + ETC, ...), and the runtime uploads whichever
4
+ // one its GPU can consume. `supportedFormats` is the caller's list of GPU-supported
5
+ // `TextureContainerFormat`s, in preference order within `containers` (the first supported container
6
+ // wins, so order `containers` by upload preference). Format-agnostic — it works on any
7
+ // `TextureContainer[]`, not only ATF's, so a caller can filter a KTX2/Basis format set the same way.
8
+ export function selectTextureContainer(containers, supportedFormats) {
9
+ for (const container of containers) {
10
+ if (supportedFormats.includes(container.format))
11
+ return container;
12
+ }
13
+ return null;
14
+ }
15
+ //# sourceMappingURL=selectTextureContainer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selectTextureContainer.js","sourceRoot":"","sources":["../src/selectTextureContainer.ts"],"names":[],"mappings":"AAGA,kGAAkG;AAClG,uGAAuG;AACvG,wGAAwG;AACxG,oFAAoF;AACpF,oGAAoG;AACpG,uFAAuF;AACvF,qGAAqG;AACrG,MAAM,UAAU,sBAAsB,CACpC,UAAuC,EACvC,gBAAmD;IAEnD,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,IAAI,gBAAgB,CAAC,QAAQ,CAAC,SAAS,CAAC,MAAM,CAAC;YAAE,OAAO,SAAS,CAAC;IACpE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { TextureContainerFormat } from '@flighthq/types';
2
+ import type { TextureContainerLevel } from '@flighthq/types';
3
+ export declare function computeTextureContainerLevels(format: TextureContainerFormat, baseWidth: number, baseHeight: number, mipLevels: number, layers: number, faces: number, startOffset: number): {
4
+ readonly levels: TextureContainerLevel[];
5
+ readonly endOffset: number;
6
+ } | null;
7
+ export declare function getTextureContainerLevelByteLength(format: TextureContainerFormat, width: number, height: number): number;
8
+ //# sourceMappingURL=textureLevelLayout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"textureLevelLayout.d.ts","sourceRoot":"","sources":["../src/textureLevelLayout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAC9D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAa7D,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,sBAAsB,EAC9B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAClB;IAAE,QAAQ,CAAC,MAAM,EAAE,qBAAqB,EAAE,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAiBjF;AAID,wBAAgB,kCAAkC,CAChD,MAAM,EAAE,sBAAsB,EAC9B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,MAAM,CAMR"}
@@ -0,0 +1,109 @@
1
+ // Block-size-aware mip-chain layout, shared by the container parsers. A `TextureContainerFormat` is
2
+ // either uncompressed (a 1x1 "block" of N bytes per pixel) or block-compressed (a fixed WxH block of N
3
+ // bytes); a mip level's byte length is `bytesPerBlock * ceil(width/blockWidth) * ceil(height/blockHeight)`.
4
+ // DDS stores no level index, so it computes every level's size here; KTX2 and Basis carry explicit
5
+ // per-level ranges and use this only where they still need a size.
6
+ // Builds the flat sub-image list for a mip chain that is laid out contiguously from `startOffset`, in
7
+ // DDS/D3D subresource order — layer-major, then face, then mip contiguous within a face
8
+ // (`subresource = mip + (face + layer * faces) * mipLevels`). Returns the built `levels` plus the
9
+ // offset just past the last level, or `null` if the format has no fixed block size (a transcoded
10
+ // Basis format never reaches this path). Dimensions halve per mip, floored at 1.
11
+ export function computeTextureContainerLevels(format, baseWidth, baseHeight, mipLevels, layers, faces, startOffset) {
12
+ if (getTextureContainerFormatBlockInfo(format) === null)
13
+ return null;
14
+ const levels = [];
15
+ let offset = startOffset;
16
+ for (let layer = 0; layer < layers; layer += 1) {
17
+ for (let face = 0; face < faces; face += 1) {
18
+ for (let mip = 0; mip < mipLevels; mip += 1) {
19
+ const width = Math.max(1, baseWidth >> mip);
20
+ const height = Math.max(1, baseHeight >> mip);
21
+ const byteLength = getTextureContainerLevelByteLength(format, width, height);
22
+ levels.push({ byteOffset: offset, byteLength, height, width });
23
+ offset += byteLength;
24
+ }
25
+ }
26
+ }
27
+ return { endOffset: offset, levels };
28
+ }
29
+ // Bytes occupied by a single mip level of the given format at the given pixel dimensions, or `-1` if the
30
+ // format has no fixed block size (`etc1s`, whose rate is variable and only known after transcoding).
31
+ export function getTextureContainerLevelByteLength(format, width, height) {
32
+ const block = getTextureContainerFormatBlockInfo(format);
33
+ if (block === null)
34
+ return -1;
35
+ const blocksWide = Math.ceil(width / block.blockWidth);
36
+ const blocksHigh = Math.ceil(height / block.blockHeight);
37
+ return blocksWide * blocksHigh * block.bytesPerBlock;
38
+ }
39
+ function getTextureContainerFormatBlockInfo(format) {
40
+ return formatBlockInfo[format];
41
+ }
42
+ function pixelBlock(bytesPerPixel) {
43
+ return { blockHeight: 1, blockWidth: 1, bytesPerBlock: bytesPerPixel };
44
+ }
45
+ function compressedBlock(blockWidth, blockHeight, bytesPerBlock) {
46
+ return { blockHeight, blockWidth, bytesPerBlock };
47
+ }
48
+ // Exhaustive so the compiler flags any new `TextureContainerFormat` that lacks block sizing. `etc1s` is
49
+ // intentionally null (variable-rate Basis ETC1S — its byte layout comes from the container's own slice
50
+ // table, not from a block formula).
51
+ const formatBlockInfo = {
52
+ astc4x4: compressedBlock(4, 4, 16),
53
+ astc5x4: compressedBlock(5, 4, 16),
54
+ astc5x5: compressedBlock(5, 5, 16),
55
+ astc6x5: compressedBlock(6, 5, 16),
56
+ astc6x6: compressedBlock(6, 6, 16),
57
+ astc8x5: compressedBlock(8, 5, 16),
58
+ astc8x6: compressedBlock(8, 6, 16),
59
+ astc8x8: compressedBlock(8, 8, 16),
60
+ astc10x5: compressedBlock(10, 5, 16),
61
+ astc10x6: compressedBlock(10, 6, 16),
62
+ astc10x8: compressedBlock(10, 8, 16),
63
+ astc10x10: compressedBlock(10, 10, 16),
64
+ astc12x10: compressedBlock(12, 10, 16),
65
+ astc12x12: compressedBlock(12, 12, 16),
66
+ bc1: compressedBlock(4, 4, 8),
67
+ bc1Srgb: compressedBlock(4, 4, 8),
68
+ bc2: compressedBlock(4, 4, 16),
69
+ bc2Srgb: compressedBlock(4, 4, 16),
70
+ bc3: compressedBlock(4, 4, 16),
71
+ bc3Srgb: compressedBlock(4, 4, 16),
72
+ bc4: compressedBlock(4, 4, 8),
73
+ bc4Snorm: compressedBlock(4, 4, 8),
74
+ bc5: compressedBlock(4, 4, 16),
75
+ bc5Snorm: compressedBlock(4, 4, 16),
76
+ bc6hSfloat: compressedBlock(4, 4, 16),
77
+ bc6hUfloat: compressedBlock(4, 4, 16),
78
+ bc7: compressedBlock(4, 4, 16),
79
+ bc7Srgb: compressedBlock(4, 4, 16),
80
+ bgra8Srgb: pixelBlock(4),
81
+ bgra8unorm: pixelBlock(4),
82
+ eacR11: compressedBlock(4, 4, 8),
83
+ eacR11Snorm: compressedBlock(4, 4, 8),
84
+ eacRg11: compressedBlock(4, 4, 16),
85
+ eacRg11Snorm: compressedBlock(4, 4, 16),
86
+ etc1: compressedBlock(4, 4, 8),
87
+ etc1s: null,
88
+ etc2Rgb: compressedBlock(4, 4, 8),
89
+ etc2RgbA1: compressedBlock(4, 4, 8),
90
+ etc2RgbA1Srgb: compressedBlock(4, 4, 8),
91
+ etc2RgbSrgb: compressedBlock(4, 4, 8),
92
+ etc2Rgba: compressedBlock(4, 4, 16),
93
+ etc2RgbaSrgb: compressedBlock(4, 4, 16),
94
+ // PVRTC v1 blocks are 64-bit (8 bytes): 4bpp packs a 4x4 pixel tile, 2bpp an 8x4 tile. The formula
95
+ // matches the tightly-packed (aligned) size; PVRTC's real minimum-size/overlap rules are a decode
96
+ // concern, not needed to locate ATF's explicitly length-prefixed blocks.
97
+ pvrtc2bppRgb: compressedBlock(8, 4, 8),
98
+ pvrtc2bppRgba: compressedBlock(8, 4, 8),
99
+ pvrtc4bppRgb: compressedBlock(4, 4, 8),
100
+ pvrtc4bppRgba: compressedBlock(4, 4, 8),
101
+ r8unorm: pixelBlock(1),
102
+ rg8unorm: pixelBlock(2),
103
+ rgba16f: pixelBlock(8),
104
+ rgba32f: pixelBlock(16),
105
+ rgba8Srgb: pixelBlock(4),
106
+ rgba8unorm: pixelBlock(4),
107
+ uastc: compressedBlock(4, 4, 16),
108
+ };
109
+ //# sourceMappingURL=textureLevelLayout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"textureLevelLayout.js","sourceRoot":"","sources":["../src/textureLevelLayout.ts"],"names":[],"mappings":"AAGA,oGAAoG;AACpG,uGAAuG;AACvG,4GAA4G;AAC5G,mGAAmG;AACnG,mEAAmE;AAEnE,sGAAsG;AACtG,wFAAwF;AACxF,kGAAkG;AAClG,iGAAiG;AACjG,iFAAiF;AACjF,MAAM,UAAU,6BAA6B,CAC3C,MAA8B,EAC9B,SAAiB,EACjB,UAAkB,EAClB,SAAiB,EACjB,MAAc,EACd,KAAa,EACb,WAAmB;IAEnB,IAAI,kCAAkC,CAAC,MAAM,CAAC,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAErE,MAAM,MAAM,GAA4B,EAAE,CAAC;IAC3C,IAAI,MAAM,GAAG,WAAW,CAAC;IACzB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QAC/C,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,KAAK,EAAE,IAAI,IAAI,CAAC,EAAE,CAAC;YAC3C,KAAK,IAAI,GAAG,GAAG,CAAC,EAAE,GAAG,GAAG,SAAS,EAAE,GAAG,IAAI,CAAC,EAAE,CAAC;gBAC5C,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,IAAI,GAAG,CAAC,CAAC;gBAC5C,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,IAAI,GAAG,CAAC,CAAC;gBAC9C,MAAM,UAAU,GAAG,kCAAkC,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;gBAC7E,MAAM,CAAC,IAAI,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;gBAC/D,MAAM,IAAI,UAAU,CAAC;YACvB,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AACvC,CAAC;AAED,yGAAyG;AACzG,qGAAqG;AACrG,MAAM,UAAU,kCAAkC,CAChD,MAA8B,EAC9B,KAAa,EACb,MAAc;IAEd,MAAM,KAAK,GAAG,kCAAkC,CAAC,MAAM,CAAC,CAAC;IACzD,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,CAAC,CAAC,CAAC;IAC9B,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,UAAU,CAAC,CAAC;IACvD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,WAAW,CAAC,CAAC;IACzD,OAAO,UAAU,GAAG,UAAU,GAAG,KAAK,CAAC,aAAa,CAAC;AACvD,CAAC;AAQD,SAAS,kCAAkC,CAAC,MAA8B;IACxE,OAAO,eAAe,CAAC,MAAM,CAAC,CAAC;AACjC,CAAC;AAED,SAAS,UAAU,CAAC,aAAqB;IACvC,OAAO,EAAE,WAAW,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,aAAa,EAAE,aAAa,EAAE,CAAC;AACzE,CAAC;AAED,SAAS,eAAe,CAAC,UAAkB,EAAE,WAAmB,EAAE,aAAqB;IACrF,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,CAAC;AACpD,CAAC;AAED,wGAAwG;AACxG,uGAAuG;AACvG,oCAAoC;AACpC,MAAM,eAAe,GAAkE;IACrF,OAAO,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;IAClC,OAAO,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;IAClC,OAAO,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;IAClC,OAAO,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;IAClC,OAAO,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;IAClC,OAAO,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;IAClC,OAAO,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;IAClC,OAAO,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;IAClC,QAAQ,EAAE,eAAe,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;IACpC,QAAQ,EAAE,eAAe,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;IACpC,QAAQ,EAAE,eAAe,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;IACpC,SAAS,EAAE,eAAe,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;IACtC,SAAS,EAAE,eAAe,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;IACtC,SAAS,EAAE,eAAe,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;IACtC,GAAG,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IAC7B,OAAO,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IACjC,GAAG,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;IAC9B,OAAO,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;IAClC,GAAG,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;IAC9B,OAAO,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;IAClC,GAAG,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IAC7B,QAAQ,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IAClC,GAAG,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;IAC9B,QAAQ,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;IACnC,UAAU,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;IACrC,UAAU,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;IACrC,GAAG,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;IAC9B,OAAO,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;IAClC,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC;IACxB,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC;IACzB,MAAM,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IAChC,WAAW,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IACrC,OAAO,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;IAClC,YAAY,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;IACvC,IAAI,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IAC9B,KAAK,EAAE,IAAI;IACX,OAAO,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IACjC,SAAS,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IACnC,aAAa,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IACvC,WAAW,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IACrC,QAAQ,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;IACnC,YAAY,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;IACvC,mGAAmG;IACnG,kGAAkG;IAClG,yEAAyE;IACzE,YAAY,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IACtC,aAAa,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IACvC,YAAY,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IACtC,aAAa,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IACvC,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC;IACtB,QAAQ,EAAE,UAAU,CAAC,CAAC,CAAC;IACvB,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC;IACtB,OAAO,EAAE,UAAU,CAAC,EAAE,CAAC;IACvB,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC;IACxB,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC;IACzB,KAAK,EAAE,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;CACjC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@flighthq/texture-formats",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src/**/*.test.ts",
16
+ "!dist/**/*.test.js",
17
+ "!dist/**/*.test.d.ts",
18
+ "!dist/**/*.test.js.map",
19
+ "!dist/**/*.test.d.ts.map"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -b",
23
+ "clean": "tsc -b --clean",
24
+ "test": "vitest run --config vitest.config.ts",
25
+ "test:watch": "vitest --watch --config vitest.config.ts",
26
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
27
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
+ },
29
+ "dependencies": {
30
+ "@flighthq/types": "0.1.0"
31
+ },
32
+ "devDependencies": {
33
+ "typescript": "^5.3.0"
34
+ },
35
+ "description": "Tree-shakable GPU texture-container parsers (KTX2 / DDS / Basis) — identify format and locate per-level byte ranges, no transcode",
36
+ "sideEffects": false
37
+ }
@@ -0,0 +1,105 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ createByteReader,
5
+ hasByteReaderBytes,
6
+ readByteReaderU16,
7
+ readByteReaderU24BigEndian,
8
+ readByteReaderU32,
9
+ readByteReaderU32BigEndian,
10
+ readByteReaderU64,
11
+ readByteReaderU8,
12
+ skipByteReader,
13
+ } from './byteReader';
14
+
15
+ describe('createByteReader', () => {
16
+ it('starts at the given offset', () => {
17
+ const reader = createByteReader(new Uint8Array([1, 2, 3, 4]), 2);
18
+ expect(reader.offset).toBe(2);
19
+ expect(readByteReaderU8(reader)).toBe(3);
20
+ });
21
+
22
+ it('views a subarray by its byteOffset', () => {
23
+ const backing = new Uint8Array([9, 9, 0x01, 0x00]);
24
+ const reader = createByteReader(backing.subarray(2));
25
+ expect(readByteReaderU16(reader)).toBe(1);
26
+ });
27
+ });
28
+
29
+ describe('hasByteReaderBytes', () => {
30
+ it('reports whether count bytes remain from the current offset', () => {
31
+ const reader = createByteReader(new Uint8Array(8), 4);
32
+ expect(hasByteReaderBytes(reader, 4)).toBe(true);
33
+ expect(hasByteReaderBytes(reader, 5)).toBe(false);
34
+ expect(hasByteReaderBytes(reader, 0)).toBe(true);
35
+ });
36
+ });
37
+
38
+ describe('readByteReaderU16', () => {
39
+ it('reads little-endian and advances 2', () => {
40
+ const reader = createByteReader(new Uint8Array([0x34, 0x12]));
41
+ expect(readByteReaderU16(reader)).toBe(0x1234);
42
+ expect(reader.offset).toBe(2);
43
+ });
44
+ });
45
+
46
+ describe('readByteReaderU24BigEndian', () => {
47
+ it('reads a 3-byte big-endian value and advances 3', () => {
48
+ const reader = createByteReader(new Uint8Array([0x12, 0x34, 0x56]));
49
+ expect(readByteReaderU24BigEndian(reader)).toBe(0x123456);
50
+ expect(reader.offset).toBe(3);
51
+ });
52
+
53
+ it('reads the full 24-bit range without sign issues', () => {
54
+ const reader = createByteReader(new Uint8Array([0xff, 0xff, 0xff]));
55
+ expect(readByteReaderU24BigEndian(reader)).toBe(0xffffff);
56
+ });
57
+ });
58
+
59
+ describe('readByteReaderU32', () => {
60
+ it('reads little-endian and advances 4', () => {
61
+ const reader = createByteReader(new Uint8Array([0x78, 0x56, 0x34, 0x12]));
62
+ expect(readByteReaderU32(reader)).toBe(0x12345678);
63
+ expect(reader.offset).toBe(4);
64
+ });
65
+ });
66
+
67
+ describe('readByteReaderU32BigEndian', () => {
68
+ it('reads a 4-byte big-endian value and advances 4', () => {
69
+ const reader = createByteReader(new Uint8Array([0x12, 0x34, 0x56, 0x78]));
70
+ expect(readByteReaderU32BigEndian(reader)).toBe(0x12345678);
71
+ expect(reader.offset).toBe(4);
72
+ });
73
+
74
+ it('reads the full 32-bit range as an unsigned value', () => {
75
+ const reader = createByteReader(new Uint8Array([0xff, 0xff, 0xff, 0xff]));
76
+ expect(readByteReaderU32BigEndian(reader)).toBe(0xffffffff);
77
+ });
78
+ });
79
+
80
+ describe('readByteReaderU64', () => {
81
+ it('reads little-endian as a number and advances 8', () => {
82
+ const bytes = new Uint8Array(8);
83
+ new DataView(bytes.buffer).setUint32(0, 0x1000, true);
84
+ new DataView(bytes.buffer).setUint32(4, 0x2, true);
85
+ const reader = createByteReader(bytes);
86
+ expect(readByteReaderU64(reader)).toBe(0x2 * 0x1_0000_0000 + 0x1000);
87
+ expect(reader.offset).toBe(8);
88
+ });
89
+ });
90
+
91
+ describe('readByteReaderU8', () => {
92
+ it('reads one byte and advances 1', () => {
93
+ const reader = createByteReader(new Uint8Array([0xab]));
94
+ expect(readByteReaderU8(reader)).toBe(0xab);
95
+ expect(reader.offset).toBe(1);
96
+ });
97
+ });
98
+
99
+ describe('skipByteReader', () => {
100
+ it('advances the offset without reading', () => {
101
+ const reader = createByteReader(new Uint8Array(8));
102
+ skipByteReader(reader, 3);
103
+ expect(reader.offset).toBe(3);
104
+ });
105
+ });
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { detectTextureContainer } from './detectTextureContainer';
4
+
5
+ const ktx2Magic = [0xab, 0x4b, 0x54, 0x58, 0x20, 0x32, 0x30, 0xbb, 0x0d, 0x0a, 0x1a, 0x0a];
6
+
7
+ describe('detectTextureContainer', () => {
8
+ it('sniffs the KTX2 identifier', () => {
9
+ expect(detectTextureContainer(new Uint8Array([...ktx2Magic, 0, 0, 0, 0]))).toBe('ktx2');
10
+ });
11
+
12
+ it('sniffs the ATF signature', () => {
13
+ expect(detectTextureContainer(new Uint8Array([0x41, 0x54, 0x46, 0, 0, 0]))).toBe('atf');
14
+ });
15
+
16
+ it('sniffs the DDS magic', () => {
17
+ expect(detectTextureContainer(new Uint8Array([0x44, 0x44, 0x53, 0x20, 0, 0]))).toBe('dds');
18
+ });
19
+
20
+ it('sniffs the Basis signature', () => {
21
+ expect(detectTextureContainer(new Uint8Array([0x73, 0x42, 0, 0]))).toBe('basis');
22
+ });
23
+
24
+ it('returns null for an unrecognized or too-short buffer', () => {
25
+ expect(detectTextureContainer(new Uint8Array([0x89, 0x50, 0x4e, 0x47]))).toBeNull(); // PNG
26
+ expect(detectTextureContainer(new Uint8Array([0x73]))).toBeNull();
27
+ expect(detectTextureContainer(new Uint8Array(0))).toBeNull();
28
+ });
29
+
30
+ it('does not mistake a truncated KTX2 identifier for KTX2', () => {
31
+ expect(detectTextureContainer(new Uint8Array(ktx2Magic.slice(0, 8)))).toBeNull();
32
+ });
33
+ });