@forgeax/engine-image 0.1.19 → 0.1.21
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.
- package/README.md +61 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/__tests__/pixel-surface.unit.test.d.ts +2 -0
- package/dist/__tests__/pixel-surface.unit.test.d.ts.map +1 -0
- package/dist/__tests__/texture-source-descriptor.integration.test.d.ts +2 -0
- package/dist/__tests__/texture-source-descriptor.integration.test.d.ts.map +1 -0
- package/dist/decode-image-from-file.mjs +2 -1
- package/dist/decode-image-from-file.mjs.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/hdr-decoder.mjs +2 -1
- package/dist/hdr-decoder.mjs.map +1 -1
- package/dist/image-importer.d.ts.map +1 -1
- package/dist/image-importer.mjs +222 -16
- package/dist/image-importer.mjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +341 -43
- package/dist/index.mjs.map +1 -1
- package/dist/parse-image.mjs +2 -1
- package/dist/parse-image.mjs.map +1 -1
- package/dist/pixel-surface.d.ts +53 -0
- package/dist/pixel-surface.d.ts.map +1 -0
- package/dist/runtime/asset-decoders.d.ts.map +1 -1
- package/dist/texture/importer.d.ts +42 -0
- package/dist/texture/importer.d.ts.map +1 -0
- package/dist/texture/source-descriptor.d.ts +24 -0
- package/dist/texture/source-descriptor.d.ts.map +1 -0
- package/package.json +6 -6
- package/src/__tests__/errors.test-d.ts +11 -3
- package/src/__tests__/image-importer-conversion-failure.unit.test.ts +31 -2
- package/src/__tests__/image-importer-topology.unit.test.ts +2 -2
- package/src/__tests__/image.unit.test.ts +2 -4
- package/src/__tests__/ktx2-basis-importer.unit.test.ts +2 -3
- package/src/__tests__/pixel-surface.unit.test.ts +94 -0
- package/src/__tests__/texture-source-descriptor.integration.test.ts +117 -0
- package/src/errors.ts +2 -0
- package/src/image-importer.ts +24 -14
- package/src/index.ts +8 -0
- package/src/pixel-surface.ts +428 -0
- package/src/runtime/asset-decoders.ts +40 -11
- package/src/texture/importer.ts +198 -0
- package/src/texture/source-descriptor.ts +123 -0
package/dist/image-importer.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { err, ok, ImportError, IMPORT_ERROR_HINTS, IMAGE_ERROR_HINTS } from '@forgeax/engine-types';
|
|
1
|
+
import { err, ok, ImportError, IMPORT_ERROR_HINTS, deriveTextureLayout, validateTextureShape, IMAGE_ERROR_HINTS } from '@forgeax/engine-types';
|
|
2
2
|
import { parseKtx2, ktx2ColorSpace } from '@forgeax/engine-codec';
|
|
3
3
|
import { basisEncode } from '@forgeax/engine-codec/encode';
|
|
4
4
|
import * as jpeg from 'jpeg-js';
|
|
@@ -34,7 +34,8 @@ var init_errors = __esm({
|
|
|
34
34
|
// .hint after switch (err.code) without parsing the message).
|
|
35
35
|
"atlas-empty-input": "images.length >= 1",
|
|
36
36
|
"atlas-size-exceeded": "image width x height <= maxAtlasSize^2 and each image fits in the atlas footprint",
|
|
37
|
-
"atlas-region-mismatch": "sum(regions[i].w x regions[i].h) <= atlasWidth x atlasHeight"
|
|
37
|
+
"atlas-region-mismatch": "sum(regions[i].w x regions[i].h) <= atlasWidth x atlasHeight",
|
|
38
|
+
"image-surface-invalid": "PixelSurface dimensions and authoring inputs satisfy the RGBA8 contract"
|
|
38
39
|
};
|
|
39
40
|
ImageErrorImpl = class extends Error {
|
|
40
41
|
code;
|
|
@@ -441,6 +442,208 @@ function parseImage(bytes, mime, opts = {}) {
|
|
|
441
442
|
mipmap: opts.mipmap ?? true
|
|
442
443
|
});
|
|
443
444
|
}
|
|
445
|
+
function record(value) {
|
|
446
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
447
|
+
}
|
|
448
|
+
function positiveInteger(value) {
|
|
449
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
450
|
+
}
|
|
451
|
+
function parseShape(value) {
|
|
452
|
+
if (!record(value) || !record(value.extent)) return void 0;
|
|
453
|
+
const extent = value.extent;
|
|
454
|
+
if (value.viewDimension === "2d" && positiveInteger(extent.width) && positiveInteger(extent.height)) {
|
|
455
|
+
if (extent.layers === void 0 && extent.depth === void 0) {
|
|
456
|
+
return { viewDimension: "2d", extent: { width: extent.width, height: extent.height } };
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (value.viewDimension === "2d-array" && positiveInteger(extent.width) && positiveInteger(extent.height) && positiveInteger(extent.layers) && extent.depth === void 0) {
|
|
460
|
+
return {
|
|
461
|
+
viewDimension: "2d-array",
|
|
462
|
+
extent: { width: extent.width, height: extent.height, layers: extent.layers }
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
if (value.viewDimension === "3d" && positiveInteger(extent.width) && positiveInteger(extent.height) && positiveInteger(extent.depth) && extent.layers === void 0) {
|
|
466
|
+
return {
|
|
467
|
+
viewDimension: "3d",
|
|
468
|
+
extent: { width: extent.width, height: extent.height, depth: extent.depth }
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
return void 0;
|
|
472
|
+
}
|
|
473
|
+
function parseMips(value) {
|
|
474
|
+
if (!record(value)) return void 0;
|
|
475
|
+
if (value.kind === "none" || value.kind === "generate") return { kind: value.kind };
|
|
476
|
+
if (value.kind === "packed" && positiveInteger(value.levelCount)) {
|
|
477
|
+
return { kind: "packed", levelCount: value.levelCount };
|
|
478
|
+
}
|
|
479
|
+
return void 0;
|
|
480
|
+
}
|
|
481
|
+
function descriptorError(field, actual) {
|
|
482
|
+
return {
|
|
483
|
+
code: "texture-source-descriptor-invalid",
|
|
484
|
+
expected: "version 1 descriptor with shape, format, colorSpace, mips, and rawSibling",
|
|
485
|
+
hint: "repair the texture descriptor and re-import the same texture GUID",
|
|
486
|
+
detail: { field, actual }
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function parseTextureSourceDescriptor(value) {
|
|
490
|
+
if (!record(value)) return err(descriptorError("descriptor", value));
|
|
491
|
+
if (value.schemaVersion !== "1")
|
|
492
|
+
return err(descriptorError("schemaVersion", value.schemaVersion));
|
|
493
|
+
const shape = parseShape(value.shape);
|
|
494
|
+
if (shape === void 0) return err(descriptorError("shape", value.shape));
|
|
495
|
+
const mips = parseMips(value.mips);
|
|
496
|
+
if (mips === void 0) return err(descriptorError("mips", value.mips));
|
|
497
|
+
if (typeof value.format !== "string") return err(descriptorError("format", value.format));
|
|
498
|
+
if (value.colorSpace !== "srgb" && value.colorSpace !== "linear") {
|
|
499
|
+
return err(descriptorError("colorSpace", value.colorSpace));
|
|
500
|
+
}
|
|
501
|
+
if (typeof value.rawSibling !== "string" || value.rawSibling.trim().length === 0) {
|
|
502
|
+
return err(descriptorError("rawSibling", value.rawSibling));
|
|
503
|
+
}
|
|
504
|
+
const shapeResult = validateTextureShape(shape, mips, value.format);
|
|
505
|
+
if (!shapeResult.ok) return shapeResult;
|
|
506
|
+
return ok({
|
|
507
|
+
schemaVersion: "1",
|
|
508
|
+
shape,
|
|
509
|
+
format: value.format,
|
|
510
|
+
colorSpace: value.colorSpace,
|
|
511
|
+
mips,
|
|
512
|
+
rawSibling: value.rawSibling
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// src/texture/importer.ts
|
|
517
|
+
function sourceReadError(input, reason) {
|
|
518
|
+
return {
|
|
519
|
+
code: "source-read-failed",
|
|
520
|
+
expected: `readable raw sibling "${input.descriptor && typeof input.descriptor === "object" && "rawSibling" in input.descriptor ? input.descriptor.rawSibling : "rawSibling"}"`,
|
|
521
|
+
hint: "repair the raw sibling path or bytes and re-import the same texture GUID",
|
|
522
|
+
detail: {
|
|
523
|
+
sourceKey: input.sourceKey,
|
|
524
|
+
sibling: input.descriptor && typeof input.descriptor === "object" && "rawSibling" in input.descriptor ? String(input.descriptor.rawSibling) : "rawSibling",
|
|
525
|
+
reason: reason instanceof Error ? reason.message : String(reason)
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
function bodyMediaType(format) {
|
|
530
|
+
return format === "r8unorm" ? "application/x-forgeax-r8" : `application/x-forgeax-${format}`;
|
|
531
|
+
}
|
|
532
|
+
async function produceTextureSource(input) {
|
|
533
|
+
const descriptorResult = parseTextureSourceDescriptor(input.descriptor);
|
|
534
|
+
if (!descriptorResult.ok) return descriptorResult;
|
|
535
|
+
const descriptor = descriptorResult.value;
|
|
536
|
+
const sibling = await input.readSibling(descriptor.rawSibling);
|
|
537
|
+
if (!sibling.ok) return err(sourceReadError(input, sibling.error));
|
|
538
|
+
const layout = deriveTextureLayout({
|
|
539
|
+
shape: descriptor.shape,
|
|
540
|
+
format: descriptor.format,
|
|
541
|
+
mips: descriptor.mips,
|
|
542
|
+
actualByteLength: sibling.value.byteLength,
|
|
543
|
+
order: "mip-major,image-major,row-major"
|
|
544
|
+
});
|
|
545
|
+
if (!layout.ok) return layout;
|
|
546
|
+
const data = new Uint8Array(sibling.value);
|
|
547
|
+
return ok({
|
|
548
|
+
guid: input.guid,
|
|
549
|
+
kind: "texture",
|
|
550
|
+
payload: {
|
|
551
|
+
kind: "texture",
|
|
552
|
+
shape: descriptor.shape,
|
|
553
|
+
format: descriptor.format,
|
|
554
|
+
colorSpace: descriptor.colorSpace,
|
|
555
|
+
mips: descriptor.mips,
|
|
556
|
+
data
|
|
557
|
+
},
|
|
558
|
+
refs: [],
|
|
559
|
+
artifacts: {
|
|
560
|
+
body: {
|
|
561
|
+
mediaType: bodyMediaType(descriptor.format),
|
|
562
|
+
assetCodec: { name: descriptor.format, version: "1" },
|
|
563
|
+
bytes: data
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
function sourceValidationError(ctx, error) {
|
|
569
|
+
const detail = "detail" in error ? error.detail : { field: "descriptor", actual: error };
|
|
570
|
+
return new ImportError({
|
|
571
|
+
code: "source-validation-failed",
|
|
572
|
+
expected: error.expected,
|
|
573
|
+
hint: error.hint,
|
|
574
|
+
detail: {
|
|
575
|
+
diagnostics: [
|
|
576
|
+
{
|
|
577
|
+
code: `texture-source-${error.code}`,
|
|
578
|
+
severity: "error",
|
|
579
|
+
sourcePath: `${ctx.source}#${"field" in detail ? detail.field : "rawSibling"}`,
|
|
580
|
+
sourceRange: { start: 0, end: 0, line: 1, column: 1 },
|
|
581
|
+
rule: "texture-source-descriptor",
|
|
582
|
+
expected: error.expected,
|
|
583
|
+
actual: JSON.stringify(detail),
|
|
584
|
+
hint: error.hint
|
|
585
|
+
}
|
|
586
|
+
]
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
async function importTextureSource(ctx) {
|
|
591
|
+
const source = await ctx.readSource();
|
|
592
|
+
if (!source.ok) {
|
|
593
|
+
const reason = String(source.error);
|
|
594
|
+
return {
|
|
595
|
+
ok: false,
|
|
596
|
+
error: new ImportError({
|
|
597
|
+
code: "source-read-failed",
|
|
598
|
+
expected: `readable texture descriptor at "${ctx.source}"`,
|
|
599
|
+
hint: "repair the texture descriptor path and retry the import",
|
|
600
|
+
detail: { source: ctx.source, reason }
|
|
601
|
+
})
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
let descriptor;
|
|
605
|
+
try {
|
|
606
|
+
descriptor = JSON.parse(new TextDecoder().decode(source.value));
|
|
607
|
+
} catch (error) {
|
|
608
|
+
return {
|
|
609
|
+
ok: false,
|
|
610
|
+
error: sourceValidationError(ctx, {
|
|
611
|
+
code: "texture-source-descriptor-invalid",
|
|
612
|
+
expected: "JSON texture source descriptor",
|
|
613
|
+
hint: "repair the descriptor JSON and retry the import",
|
|
614
|
+
detail: {
|
|
615
|
+
field: "descriptor",
|
|
616
|
+
actual: error instanceof Error ? error.message : String(error)
|
|
617
|
+
}
|
|
618
|
+
})
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
const subAsset = ctx.subAssets.length === 1 ? ctx.subAssets[0] : void 0;
|
|
622
|
+
if (subAsset === void 0 || subAsset.kind !== "texture" || subAsset.sourceIndex !== 0) {
|
|
623
|
+
return {
|
|
624
|
+
ok: false,
|
|
625
|
+
error: sourceValidationError(ctx, {
|
|
626
|
+
code: "texture-source-descriptor-invalid",
|
|
627
|
+
expected: "one texture subAsset at sourceIndex 0",
|
|
628
|
+
hint: "repair Meta subAssets and retry the same texture GUID",
|
|
629
|
+
detail: { field: "subAssets", actual: ctx.subAssets }
|
|
630
|
+
})
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
const produced = await produceTextureSource({
|
|
634
|
+
descriptor,
|
|
635
|
+
guid: subAsset.guid,
|
|
636
|
+
sourceKey: subAsset.sourceKey ?? `${ctx.source}:texture`,
|
|
637
|
+
readSibling: ctx.readSibling
|
|
638
|
+
});
|
|
639
|
+
if (!produced.ok) return { ok: false, error: sourceValidationError(ctx, produced.error) };
|
|
640
|
+
const parsed = parseTextureSourceDescriptor(descriptor);
|
|
641
|
+
const sibling = parsed.ok ? parsed.value.rawSibling : ctx.source;
|
|
642
|
+
return {
|
|
643
|
+
ok: true,
|
|
644
|
+
value: { assets: [produced.value], sourceDependencies: [ctx.source, sibling] }
|
|
645
|
+
};
|
|
646
|
+
}
|
|
444
647
|
|
|
445
648
|
// src/image-importer.ts
|
|
446
649
|
function mimeFromSource(source) {
|
|
@@ -658,13 +861,14 @@ async function importKtx2Source(ctx, bytes) {
|
|
|
658
861
|
kind: "texture",
|
|
659
862
|
payload: {
|
|
660
863
|
kind: "texture",
|
|
661
|
-
|
|
662
|
-
|
|
864
|
+
shape: {
|
|
865
|
+
viewDimension: "2d",
|
|
866
|
+
extent: { width: inspection.width, height: inspection.height }
|
|
867
|
+
},
|
|
663
868
|
format: colorSpaceToFormat(inspection.colorSpace),
|
|
664
869
|
data: bytes,
|
|
665
870
|
colorSpace: inspection.colorSpace,
|
|
666
|
-
|
|
667
|
-
mipLevelCount: inspection.levelCount
|
|
871
|
+
mips: inspection.levelCount > 1 ? { kind: "packed", levelCount: inspection.levelCount } : { kind: "none" }
|
|
668
872
|
},
|
|
669
873
|
refs: [],
|
|
670
874
|
artifacts: {
|
|
@@ -813,13 +1017,14 @@ async function importBasisSource(ctx, bytes) {
|
|
|
813
1017
|
kind: "texture",
|
|
814
1018
|
payload: {
|
|
815
1019
|
kind: "texture",
|
|
816
|
-
|
|
817
|
-
|
|
1020
|
+
shape: {
|
|
1021
|
+
viewDimension: "2d",
|
|
1022
|
+
extent: { width: inspection.width, height: inspection.height }
|
|
1023
|
+
},
|
|
818
1024
|
format: colorSpaceToFormat(colorSpace),
|
|
819
1025
|
data: bytes,
|
|
820
1026
|
colorSpace,
|
|
821
|
-
|
|
822
|
-
mipLevelCount: inspection.levelCount
|
|
1027
|
+
mips: inspection.levelCount > 1 ? { kind: "packed", levelCount: inspection.levelCount } : { kind: "none" }
|
|
823
1028
|
},
|
|
824
1029
|
refs: [],
|
|
825
1030
|
artifacts: {
|
|
@@ -869,6 +1074,9 @@ async function maybeEncodeTextureBytes(ctx, pixels, width, height, compressionMo
|
|
|
869
1074
|
return { ok: true, value: result.value.ktx2 };
|
|
870
1075
|
}
|
|
871
1076
|
async function importImage(ctx) {
|
|
1077
|
+
if (ctx.source.toLowerCase().endsWith(".texture.json")) {
|
|
1078
|
+
return importTextureSource(ctx);
|
|
1079
|
+
}
|
|
872
1080
|
const requiredKind = requiredImageOutputKind(ctx.source);
|
|
873
1081
|
if (requiredKind !== void 0) {
|
|
874
1082
|
const topologyError = validateImageOutputTopology(ctx, requiredKind);
|
|
@@ -990,12 +1198,11 @@ async function importImage(ctx) {
|
|
|
990
1198
|
if (sub.kind !== "texture") continue;
|
|
991
1199
|
const payload = {
|
|
992
1200
|
kind: "texture",
|
|
993
|
-
width: dec.width,
|
|
994
|
-
height: dec.height,
|
|
1201
|
+
shape: { viewDimension: "2d", extent: { width: dec.width, height: dec.height } },
|
|
995
1202
|
format: colorSpaceToFormat(colorSpace),
|
|
996
1203
|
data: encodedBytes ?? dec.bytes,
|
|
997
1204
|
colorSpace,
|
|
998
|
-
mipmap
|
|
1205
|
+
mips: mipmap ? { kind: "generate" } : { kind: "none" }
|
|
999
1206
|
};
|
|
1000
1207
|
out.push({
|
|
1001
1208
|
guid: sub.guid,
|
|
@@ -1062,11 +1269,10 @@ var decodeImageForImport = async (bytes, mimeType, importSettings) => {
|
|
|
1062
1269
|
texture: {
|
|
1063
1270
|
kind: "texture",
|
|
1064
1271
|
data: cookedBytes,
|
|
1065
|
-
width: tex.width,
|
|
1066
|
-
height: tex.height,
|
|
1272
|
+
shape: { viewDimension: "2d", extent: { width: tex.width, height: tex.height } },
|
|
1067
1273
|
format: colorSpaceToFormat(colorSpace),
|
|
1068
1274
|
colorSpace,
|
|
1069
|
-
mipmap
|
|
1275
|
+
mips: mipmap ? { kind: "generate" } : { kind: "none" }
|
|
1070
1276
|
},
|
|
1071
1277
|
bytes: cookedBytes,
|
|
1072
1278
|
mediaType,
|