reze-engine 0.41.2 → 0.41.4
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 +18 -3
- package/dist/animation.d.ts +18 -0
- package/dist/animation.d.ts.map +1 -1
- package/dist/animation.js +26 -16
- package/dist/dds-loader.d.ts +12 -0
- package/dist/dds-loader.d.ts.map +1 -0
- package/dist/dds-loader.js +246 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +72 -15
- package/dist/model.d.ts +47 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +107 -22
- package/dist/pmx-loader.d.ts.map +1 -1
- package/dist/pmx-loader.js +8 -5
- package/dist/psd-loader.d.ts +13 -0
- package/dist/psd-loader.d.ts.map +1 -0
- package/dist/psd-loader.js +192 -0
- package/package.json +1 -1
- package/src/animation.ts +26 -16
- package/src/dds-loader.ts +236 -0
- package/src/engine.ts +78 -22
- package/src/model.ts +116 -22
- package/src/pmx-loader.ts +10 -5
- package/src/psd-loader.ts +193 -0
package/src/model.ts
CHANGED
|
@@ -92,6 +92,12 @@ export interface Bone {
|
|
|
92
92
|
appendRatio?: number // 0..1
|
|
93
93
|
appendRotate?: boolean
|
|
94
94
|
appendMove?: boolean
|
|
95
|
+
/** 軸制限: this bone rotates ONLY about this axis (the twist bones use it). */
|
|
96
|
+
fixedAxis?: [number, number, number]
|
|
97
|
+
/** 変形階層: MMD poses whole layers in order, not bones in array order.
|
|
98
|
+
* Parsed but not yet honoured — distinct from Model's `deformOrder`, which is
|
|
99
|
+
* this engine's parent-before-child traversal. */
|
|
100
|
+
deformLayer?: number
|
|
95
101
|
ikTargetIndex?: number // IK target bone index (if this bone is an IK effector)
|
|
96
102
|
ikIteration?: number // IK iteration count
|
|
97
103
|
ikLimitAngle?: number // IK rotation constraint (radians)
|
|
@@ -477,6 +483,9 @@ export class Model {
|
|
|
477
483
|
this.applyMorphs()
|
|
478
484
|
}
|
|
479
485
|
|
|
486
|
+
/** 軸制限 bones and their normalised axes — see applyFixedAxes. */
|
|
487
|
+
private fixedAxisBones: { index: number; x: number; y: number; z: number }[] = []
|
|
488
|
+
|
|
480
489
|
private initializeRuntimeSkeleton(): void {
|
|
481
490
|
const boneCount = this.skeleton.bones.length
|
|
482
491
|
|
|
@@ -490,6 +499,18 @@ export class Model {
|
|
|
490
499
|
worldMatrices[i] = Mat4.identity()
|
|
491
500
|
}
|
|
492
501
|
|
|
502
|
+
// 軸制限 bones, gathered once and normalised. A standard rig has four —
|
|
503
|
+
// the two arm twists and the two wrist twists — so this is a four-element
|
|
504
|
+
// walk per pose rather than a branch on every bone.
|
|
505
|
+
this.fixedAxisBones = []
|
|
506
|
+
for (let i = 0; i < boneCount; i++) {
|
|
507
|
+
const a = this.skeleton.bones[i].fixedAxis
|
|
508
|
+
if (!a) continue
|
|
509
|
+
const len = Math.hypot(a[0], a[1], a[2])
|
|
510
|
+
if (len < 1e-8) continue
|
|
511
|
+
this.fixedAxisBones.push({ index: i, x: a[0] / len, y: a[1] / len, z: a[2] / len })
|
|
512
|
+
}
|
|
513
|
+
|
|
493
514
|
this.runtimeSkeleton = {
|
|
494
515
|
localRotations,
|
|
495
516
|
localTranslations,
|
|
@@ -673,19 +694,42 @@ export class Model {
|
|
|
673
694
|
* Stages are the reason this exists: a door or a lift is rigged as a bone
|
|
674
695
|
* morph and there is no VMD anywhere that drives it.
|
|
675
696
|
*/
|
|
697
|
+
/**
|
|
698
|
+
* Take the last frame's bone-morph offsets back off, before any pose source
|
|
699
|
+
* runs.
|
|
700
|
+
*
|
|
701
|
+
* This cannot live at the top of applyBoneMorphs, which is where it used to
|
|
702
|
+
* be. A pose source writes only the bones its clip keys, and it writes them
|
|
703
|
+
* from the clip — so restoring afterwards overwrote the freshly animated pose
|
|
704
|
+
* with a snapshot taken a frame earlier, and the re-snapshot immediately
|
|
705
|
+
* below then saved that same stale value again. The result was not a drift or
|
|
706
|
+
* a lag: every bone any bone morph touched stayed pinned to the first frame
|
|
707
|
+
* the model was posed at, permanently, and at ANY weight — the restore ran
|
|
708
|
+
* over the touched set before the weight test, so a morph sitting at 0 froze
|
|
709
|
+
* its bones just as hard as one at 1.
|
|
710
|
+
*
|
|
711
|
+
* It reads as "the arms don't animate" because arms are what these morphs
|
|
712
|
+
* touch: character models routinely ship T-Pose / A-Pose / ShouderBlend /
|
|
713
|
+
* ElbowBlend adjusters on 腕 and ひじ, and nothing else in the rig is a
|
|
714
|
+
* comparably popular bone-morph target.
|
|
715
|
+
*/
|
|
716
|
+
private undoBoneMorphs(): void {
|
|
717
|
+
if (!this.boneMorphApplied) return
|
|
718
|
+
for (let i = 0; i < this.boneMorphBones.length; i++) {
|
|
719
|
+
const b = this.boneMorphBones[i]
|
|
720
|
+
this.runtimeSkeleton.localRotations[b].set(this.boneMorphRestoreR[i])
|
|
721
|
+
this.runtimeSkeleton.localTranslations[b].set(this.boneMorphRestoreT[i])
|
|
722
|
+
}
|
|
723
|
+
this.boneMorphApplied = false
|
|
724
|
+
}
|
|
725
|
+
|
|
676
726
|
private applyBoneMorphs(): void {
|
|
677
727
|
if (this.boneMorphPlan.length === 0) return
|
|
678
728
|
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
//
|
|
682
|
-
|
|
683
|
-
for (let i = 0; i < this.boneMorphBones.length; i++) {
|
|
684
|
-
const b = this.boneMorphBones[i]
|
|
685
|
-
this.runtimeSkeleton.localRotations[b].set(this.boneMorphRestoreR[i])
|
|
686
|
-
this.runtimeSkeleton.localTranslations[b].set(this.boneMorphRestoreT[i])
|
|
687
|
-
}
|
|
688
|
-
}
|
|
729
|
+
// Snapshot the pose as the sources left it, so undoBoneMorphs can hand these
|
|
730
|
+
// bones back unmorphed at the top of the next frame. The two halves are a
|
|
731
|
+
// pair: without the undo a stage — which has no clip to rewrite its bones —
|
|
732
|
+
// would compound the same offset every frame.
|
|
689
733
|
for (let i = 0; i < this.boneMorphBones.length; i++) {
|
|
690
734
|
const b = this.boneMorphBones[i]
|
|
691
735
|
this.boneMorphRestoreR[i].set(this.runtimeSkeleton.localRotations[b])
|
|
@@ -1922,6 +1966,39 @@ export class Model {
|
|
|
1922
1966
|
: frameA.weight
|
|
1923
1967
|
}
|
|
1924
1968
|
|
|
1969
|
+
/**
|
|
1970
|
+
* Hold 軸制限 bones to their own axis.
|
|
1971
|
+
*
|
|
1972
|
+
* A twist bone (腕捩 / 手捩) exists to rotate about the length of the limb and
|
|
1973
|
+
* nothing else, and PMX says so by giving it a fixed axis. A VMD still keys it
|
|
1974
|
+
* with a full quaternion, so without the constraint the bone bends as well as
|
|
1975
|
+
* twists — and because the elbow is parented to 腕捩 and 腕捩1..3 inherit a
|
|
1976
|
+
* fraction of it, that error is carried into the whole forearm and multiplied
|
|
1977
|
+
* three ways down the arm. It is the most visible thing on the model.
|
|
1978
|
+
*
|
|
1979
|
+
* Projecting the quaternion's vector part onto the axis and renormalising is
|
|
1980
|
+
* what keeps the twist and drops everything else; the same operation MMD and
|
|
1981
|
+
* every faithful runtime performs.
|
|
1982
|
+
*
|
|
1983
|
+
* Applied to the STORED local rotation rather than inside a matrix path,
|
|
1984
|
+
* because the append children read that array directly: constrain it once and
|
|
1985
|
+
* the bone, its inheritors and a hand-posed gizmo all agree.
|
|
1986
|
+
*/
|
|
1987
|
+
private applyFixedAxes(): void {
|
|
1988
|
+
for (const a of this.fixedAxisBones) {
|
|
1989
|
+
const q = this.runtimeSkeleton.localRotations[a.index]
|
|
1990
|
+
const dot = q.x * a.x + q.y * a.y + q.z * a.z
|
|
1991
|
+
const x = a.x * dot
|
|
1992
|
+
const y = a.y * dot
|
|
1993
|
+
const z = a.z * dot
|
|
1994
|
+
const len = Math.sqrt(x * x + y * y + z * z + q.w * q.w)
|
|
1995
|
+
if (len > 1e-8) {
|
|
1996
|
+
const inv = 1 / len
|
|
1997
|
+
q.setXYZW(x * inv, y * inv, z * inv, q.w * inv)
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
|
|
1925
2002
|
private applyPoseFromClip(clip: AnimationClip | null, frame: number): void {
|
|
1926
2003
|
if (!clip) return
|
|
1927
2004
|
this.applyIkFromClip(clip, frame)
|
|
@@ -1941,6 +2018,7 @@ export class Model {
|
|
|
1941
2018
|
this.runtimeSkeleton.localRotations[boneIdx].set(_animSlerp)
|
|
1942
2019
|
this.runtimeSkeleton.localTranslations[boneIdx].set(localTranslation)
|
|
1943
2020
|
}
|
|
2021
|
+
this.applyFixedAxes()
|
|
1944
2022
|
|
|
1945
2023
|
for (const [morphName, keyFrames] of clip.morphTracks.entries()) {
|
|
1946
2024
|
const weight = this.sampleMorphTrack(morphName, keyFrames, frame, this.morphTrackIndices)
|
|
@@ -2084,6 +2162,7 @@ export class Model {
|
|
|
2084
2162
|
this.runtimeSkeleton.localRotations[i].set(_blendQ)
|
|
2085
2163
|
this.runtimeSkeleton.localTranslations[i].set(localTranslation)
|
|
2086
2164
|
}
|
|
2165
|
+
this.applyFixedAxes()
|
|
2087
2166
|
|
|
2088
2167
|
const morphCount = morphAcc.length
|
|
2089
2168
|
for (let i = 0; i < morphCount; i++) {
|
|
@@ -2237,6 +2316,13 @@ export class Model {
|
|
|
2237
2316
|
const evWatch = this.clipEvents.size > 0 ? this.animationState.getCurrentAnimation() : null
|
|
2238
2317
|
const evPrevFrame = evWatch !== null ? this.animationState.getCurrentFrame() : 0
|
|
2239
2318
|
this.animationState.update(deltaTime)
|
|
2319
|
+
|
|
2320
|
+
// Hand the pose sources their bones unmorphed. A bone morph is an offset on
|
|
2321
|
+
// top of the pose, so last frame's offset has to come off before this
|
|
2322
|
+
// frame's pose goes on — taking it off afterwards is what froze every
|
|
2323
|
+
// morph-touched bone at its first posed frame.
|
|
2324
|
+
this.undoBoneMorphs()
|
|
2325
|
+
|
|
2240
2326
|
if (!this.clipApplySuspended) {
|
|
2241
2327
|
if (this.oneShot !== null) {
|
|
2242
2328
|
this.applyOneShot(deltaTime)
|
|
@@ -2378,7 +2464,9 @@ export class Model {
|
|
|
2378
2464
|
|
|
2379
2465
|
// Handle append transformations (same logic as computeWorldMatrices)
|
|
2380
2466
|
const appendParentIdx = b.appendParentIndex
|
|
2381
|
-
|
|
2467
|
+
// `|| appendMove`: a move-only append (no rotation) was skipped altogether,
|
|
2468
|
+
// because the guard demanded appendRotate before either branch could run.
|
|
2469
|
+
const hasAppend = (b.appendRotate || b.appendMove) &&
|
|
2382
2470
|
appendParentIdx !== undefined &&
|
|
2383
2471
|
appendParentIdx >= 0 &&
|
|
2384
2472
|
appendParentIdx < bones.length
|
|
@@ -2405,12 +2493,15 @@ export class Model {
|
|
|
2405
2493
|
scratchQuat[2].setIdentity()
|
|
2406
2494
|
Quat.slerpInto(scratchQuat[2], scratchQuat[0], absRatio, scratchQuat[1])
|
|
2407
2495
|
|
|
2408
|
-
//
|
|
2496
|
+
// MMD composes the append on the RIGHT: own animated rotation first, then
|
|
2497
|
+
// the inherited one (saba's `r = r * appendRotate`). We had it on the
|
|
2498
|
+
// left, which is a different rotation whenever a bone carries BOTH its
|
|
2499
|
+
// own key and an append — the twist bones, if a motion keys them.
|
|
2409
2500
|
const sx = scratchQuat[1].x, sy = scratchQuat[1].y, sz = scratchQuat[1].z, sw = scratchQuat[1].w
|
|
2410
|
-
const nx =
|
|
2411
|
-
const ny =
|
|
2412
|
-
const nz =
|
|
2413
|
-
const nw =
|
|
2501
|
+
const nx = fw * sx + fx * sw + fy * sz - fz * sy
|
|
2502
|
+
const ny = fw * sy - fx * sz + fy * sw + fz * sx
|
|
2503
|
+
const nz = fw * sz + fx * sy - fy * sx + fz * sw
|
|
2504
|
+
const nw = fw * sw - fx * sx - fy * sy - fz * sz
|
|
2414
2505
|
fx = nx; fy = ny; fz = nz; fw = nw
|
|
2415
2506
|
}
|
|
2416
2507
|
|
|
@@ -2469,7 +2560,7 @@ export class Model {
|
|
|
2469
2560
|
|
|
2470
2561
|
const appendParentIdx = b.appendParentIndex
|
|
2471
2562
|
const hasAppend =
|
|
2472
|
-
b.appendRotate && appendParentIdx !== undefined && appendParentIdx >= 0 && appendParentIdx < boneCount
|
|
2563
|
+
(b.appendRotate || b.appendMove) && appendParentIdx !== undefined && appendParentIdx >= 0 && appendParentIdx < boneCount
|
|
2473
2564
|
|
|
2474
2565
|
if (hasAppend) {
|
|
2475
2566
|
const ratio = b.appendRatio === undefined ? 1 : Math.max(-1, Math.min(1, b.appendRatio))
|
|
@@ -2487,12 +2578,15 @@ export class Model {
|
|
|
2487
2578
|
scratchQuat[2].setIdentity()
|
|
2488
2579
|
Quat.slerpInto(scratchQuat[2], scratchQuat[0], absRatio, scratchQuat[1])
|
|
2489
2580
|
|
|
2490
|
-
//
|
|
2581
|
+
// MMD composes the append on the RIGHT: own animated rotation first, then
|
|
2582
|
+
// the inherited one (saba's `r = r * appendRotate`). We had it on the
|
|
2583
|
+
// left, which is a different rotation whenever a bone carries BOTH its
|
|
2584
|
+
// own key and an append — the twist bones, if a motion keys them.
|
|
2491
2585
|
const sx = scratchQuat[1].x, sy = scratchQuat[1].y, sz = scratchQuat[1].z, sw = scratchQuat[1].w
|
|
2492
|
-
const nx =
|
|
2493
|
-
const ny =
|
|
2494
|
-
const nz =
|
|
2495
|
-
const nw =
|
|
2586
|
+
const nx = fw * sx + fx * sw + fy * sz - fz * sy
|
|
2587
|
+
const ny = fw * sy - fx * sz + fy * sw + fz * sx
|
|
2588
|
+
const nz = fw * sz + fx * sy - fy * sx + fz * sw
|
|
2589
|
+
const nw = fw * sw - fx * sx - fy * sy - fz * sz
|
|
2496
2590
|
fx = nx; fy = ny; fz = nz; fw = nw
|
|
2497
2591
|
}
|
|
2498
2592
|
|
package/src/pmx-loader.ts
CHANGED
|
@@ -354,6 +354,8 @@ export class PmxLoader {
|
|
|
354
354
|
ikIteration?: number
|
|
355
355
|
ikLimitAngle?: number
|
|
356
356
|
ikLinks?: IKLink[]
|
|
357
|
+
fixedAxis?: [number, number, number]
|
|
358
|
+
deformLayer?: number
|
|
357
359
|
}
|
|
358
360
|
const abs: AbsBone[] = new Array(count)
|
|
359
361
|
// PMX 2.x bone flags (best-effort common masks)
|
|
@@ -372,7 +374,7 @@ export class PmxLoader {
|
|
|
372
374
|
const y = this.getFloat32()
|
|
373
375
|
const z = this.getFloat32()
|
|
374
376
|
const parentIndex = this.getNonVertexIndex(this.boneIndexSize)
|
|
375
|
-
this.getInt32() //
|
|
377
|
+
const deformLayer = this.getInt32() // 変形階層
|
|
376
378
|
const flags = this.getUint16()
|
|
377
379
|
|
|
378
380
|
// Tail: bone index or offset vector3
|
|
@@ -397,11 +399,10 @@ export class PmxLoader {
|
|
|
397
399
|
appendMove = (flags & FLAG_APPEND_MOVE) !== 0
|
|
398
400
|
}
|
|
399
401
|
|
|
400
|
-
// Axis limit
|
|
402
|
+
// Axis limit (軸制限)
|
|
403
|
+
let fixedAxis: [number, number, number] | undefined = undefined
|
|
401
404
|
if ((flags & FLAG_AXIS_LIMIT) !== 0) {
|
|
402
|
-
this.getFloat32()
|
|
403
|
-
this.getFloat32()
|
|
404
|
-
this.getFloat32()
|
|
405
|
+
fixedAxis = [this.getFloat32(), this.getFloat32(), this.getFloat32()]
|
|
405
406
|
}
|
|
406
407
|
|
|
407
408
|
// Local axis (two vectors x and z)
|
|
@@ -467,6 +468,8 @@ export class PmxLoader {
|
|
|
467
468
|
appendRatio,
|
|
468
469
|
appendRotate,
|
|
469
470
|
appendMove,
|
|
471
|
+
fixedAxis,
|
|
472
|
+
deformLayer,
|
|
470
473
|
ikTargetIndex,
|
|
471
474
|
ikIteration,
|
|
472
475
|
ikLimitAngle,
|
|
@@ -487,6 +490,8 @@ export class PmxLoader {
|
|
|
487
490
|
appendRatio: a.appendRatio,
|
|
488
491
|
appendRotate: a.appendRotate,
|
|
489
492
|
appendMove: a.appendMove,
|
|
493
|
+
fixedAxis: a.fixedAxis,
|
|
494
|
+
deformLayer: a.deformLayer,
|
|
490
495
|
ikTargetIndex: a.ikTargetIndex,
|
|
491
496
|
ikIteration: a.ikIteration,
|
|
492
497
|
ikLimitAngle: a.ikLimitAngle,
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// PSD → RGBA8, from the merged composite.
|
|
2
|
+
//
|
|
3
|
+
// MMD texture packs are often shipped as the artist's working files, so a
|
|
4
|
+
// material's texture path points at a .psd that no browser will decode. The
|
|
5
|
+
// layers are none of our business — Photoshop writes a flattened composite of
|
|
6
|
+
// the whole document at the end of the file, which is exactly the image the
|
|
7
|
+
// artist saw, so that is what this reads.
|
|
8
|
+
//
|
|
9
|
+
// (A file saved with "Maximize Compatibility" off has no useful composite. There
|
|
10
|
+
// is nothing to be done about that here beyond failing clearly: reconstructing it
|
|
11
|
+
// would mean compositing every layer, blend mode and clipping group — a Photoshop,
|
|
12
|
+
// not a texture loader.)
|
|
13
|
+
|
|
14
|
+
import type { DecodedImage } from "./tga-loader"
|
|
15
|
+
|
|
16
|
+
const MAGIC = 0x38425053 // "8BPS", big-endian — PSD is big-endian throughout
|
|
17
|
+
|
|
18
|
+
const enum ColorMode {
|
|
19
|
+
Bitmap = 0,
|
|
20
|
+
Grayscale = 1,
|
|
21
|
+
Indexed = 2,
|
|
22
|
+
RGB = 3,
|
|
23
|
+
CMYK = 4,
|
|
24
|
+
Multichannel = 7,
|
|
25
|
+
Duotone = 8,
|
|
26
|
+
Lab = 9,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const MODE_NAMES: Record<number, string> = {
|
|
30
|
+
[ColorMode.Bitmap]: "bitmap",
|
|
31
|
+
[ColorMode.CMYK]: "CMYK",
|
|
32
|
+
[ColorMode.Multichannel]: "multichannel",
|
|
33
|
+
[ColorMode.Lab]: "Lab",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** True when these bytes are a PSD/PSB, by magic rather than by file extension. */
|
|
37
|
+
export function isPsd(buffer: ArrayBuffer): boolean {
|
|
38
|
+
return buffer.byteLength >= 26 && new DataView(buffer).getUint32(0, false) === MAGIC
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* PackBits, one row at a time.
|
|
43
|
+
*
|
|
44
|
+
* `end` bounds the row rather than the buffer: the row-length table says how many
|
|
45
|
+
* compressed bytes this row occupies, and trusting the control bytes past that
|
|
46
|
+
* would let one malformed row eat the next one's data.
|
|
47
|
+
*/
|
|
48
|
+
function unpackBits(src: Uint8Array, start: number, end: number, dst: Uint8Array, at: number, limit: number): number {
|
|
49
|
+
let i = start
|
|
50
|
+
let o = at
|
|
51
|
+
while (i < end && o < limit) {
|
|
52
|
+
const n = (src[i++] << 24) >> 24 // to signed
|
|
53
|
+
if (n >= 0) {
|
|
54
|
+
const count = Math.min(n + 1, limit - o, end - i)
|
|
55
|
+
for (let k = 0; k < count; k++) dst[o++] = src[i++]
|
|
56
|
+
} else if (n !== -128) {
|
|
57
|
+
// -128 is a no-op by the spec, not a run of 129.
|
|
58
|
+
const count = Math.min(1 - n, limit - o)
|
|
59
|
+
const b = src[i++]
|
|
60
|
+
for (let k = 0; k < count; k++) dst[o++] = b
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return o
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Decode a PSD's composite to RGBA8.
|
|
68
|
+
*
|
|
69
|
+
* RGB, grayscale, indexed and duotone at 8 or 16 bits per channel, raw or RLE —
|
|
70
|
+
* which is every texture that has actually turned up. CMYK and Lab throw: they
|
|
71
|
+
* need a colour conversion that would be guesswork without a profile, and a
|
|
72
|
+
* silently wrong-coloured texture is worse than a missing one.
|
|
73
|
+
*/
|
|
74
|
+
export function decodePsd(buffer: ArrayBuffer): DecodedImage {
|
|
75
|
+
const v = new DataView(buffer)
|
|
76
|
+
if (buffer.byteLength < 26 || v.getUint32(0, false) !== MAGIC) throw new Error("not a PSD")
|
|
77
|
+
|
|
78
|
+
const version = v.getUint16(4, false) // 1 = PSD, 2 = PSB
|
|
79
|
+
if (version !== 1 && version !== 2) throw new Error(`PSD unsupported version ${version}`)
|
|
80
|
+
const channels = v.getUint16(12, false)
|
|
81
|
+
const height = v.getUint32(14, false)
|
|
82
|
+
const width = v.getUint32(18, false)
|
|
83
|
+
const depth = v.getUint16(22, false)
|
|
84
|
+
const mode = v.getUint16(24, false)
|
|
85
|
+
|
|
86
|
+
if (width <= 0 || height <= 0) throw new Error(`PSD bad dimensions ${width}x${height}`)
|
|
87
|
+
if (depth !== 8 && depth !== 16) throw new Error(`PSD unsupported bit depth ${depth}`)
|
|
88
|
+
if (mode in MODE_NAMES) throw new Error(`PSD unsupported colour mode: ${MODE_NAMES[mode]}`)
|
|
89
|
+
|
|
90
|
+
// Three variable-length sections stand between the header and the pixels. Only
|
|
91
|
+
// the indexed palette is worth reading; the rest is skipped by its length.
|
|
92
|
+
let p = 26
|
|
93
|
+
const colorDataLen = v.getUint32(p, false)
|
|
94
|
+
const colorData = p + 4
|
|
95
|
+
p = colorData + colorDataLen
|
|
96
|
+
p += 4 + v.getUint32(p, false) // image resources
|
|
97
|
+
// PSB states this length in 8 bytes. Only the low word can matter — the high
|
|
98
|
+
// one would mean a layer section larger than 4GB.
|
|
99
|
+
if (version === 2) {
|
|
100
|
+
p += 8 + v.getUint32(p + 4, false)
|
|
101
|
+
} else {
|
|
102
|
+
p += 4 + v.getUint32(p, false)
|
|
103
|
+
}
|
|
104
|
+
if (p + 2 > buffer.byteLength) throw new Error("PSD truncated before the composite")
|
|
105
|
+
|
|
106
|
+
const compression = v.getUint16(p, false)
|
|
107
|
+
p += 2
|
|
108
|
+
|
|
109
|
+
// Only the channels that carry colour, plus one alpha. A PSD can hold spot and
|
|
110
|
+
// mask channels past those; they are stored after, so ignoring them is a matter
|
|
111
|
+
// of not reading that far.
|
|
112
|
+
const colourChannels = mode === ColorMode.RGB ? 3 : 1
|
|
113
|
+
const hasAlpha = channels > colourChannels
|
|
114
|
+
const used = colourChannels + (hasAlpha ? 1 : 0)
|
|
115
|
+
if (channels < colourChannels) throw new Error(`PSD has ${channels} channel(s), expected ${colourChannels}`)
|
|
116
|
+
|
|
117
|
+
const bytesPerSample = depth === 16 ? 2 : 1
|
|
118
|
+
const planeSamples = width * height
|
|
119
|
+
const planeBytes = planeSamples * bytesPerSample
|
|
120
|
+
const planes = new Uint8Array(used * planeBytes)
|
|
121
|
+
|
|
122
|
+
if (compression === 0) {
|
|
123
|
+
const need = used * planeBytes
|
|
124
|
+
if (p + need > buffer.byteLength) throw new Error("PSD truncated composite")
|
|
125
|
+
planes.set(new Uint8Array(buffer, p, need))
|
|
126
|
+
} else if (compression === 1) {
|
|
127
|
+
// A table of per-row compressed lengths for EVERY channel comes first,
|
|
128
|
+
// including the channels being skipped — so the table is read in full even
|
|
129
|
+
// though only the first `used` channels' rows are decoded.
|
|
130
|
+
const countBytes = version === 2 ? 4 : 2
|
|
131
|
+
const tableBytes = channels * height * countBytes
|
|
132
|
+
if (p + tableBytes > buffer.byteLength) throw new Error("PSD truncated row table")
|
|
133
|
+
const rowLengths = new Uint32Array(channels * height)
|
|
134
|
+
for (let i = 0; i < rowLengths.length; i++) {
|
|
135
|
+
rowLengths[i] = countBytes === 2 ? v.getUint16(p + i * 2, false) : v.getUint32(p + i * 4, false)
|
|
136
|
+
}
|
|
137
|
+
const src = new Uint8Array(buffer)
|
|
138
|
+
let at = p + tableBytes
|
|
139
|
+
for (let c = 0; c < channels; c++) {
|
|
140
|
+
for (let y = 0; y < height; y++) {
|
|
141
|
+
const len = rowLengths[c * height + y]
|
|
142
|
+
if (at + len > buffer.byteLength) throw new Error("PSD truncated composite")
|
|
143
|
+
if (c < used) {
|
|
144
|
+
const rowStart = c * planeBytes + y * width * bytesPerSample
|
|
145
|
+
unpackBits(src, at, at + len, planes, rowStart, rowStart + width * bytesPerSample)
|
|
146
|
+
}
|
|
147
|
+
at += len
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
// 2 and 3 are the Zip codes. They appear on layer data, effectively never on
|
|
152
|
+
// the composite, and inflating would mean shipping a decompressor.
|
|
153
|
+
throw new Error(`PSD unsupported compression ${compression}`)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 16-bit samples are big-endian; the high byte is the 8-bit value.
|
|
157
|
+
const sample = (plane: number, i: number): number =>
|
|
158
|
+
depth === 16 ? planes[plane * planeBytes + i * 2] : planes[plane * planeBytes + i]
|
|
159
|
+
|
|
160
|
+
const rgba = new Uint8Array(planeSamples * 4)
|
|
161
|
+
|
|
162
|
+
if (mode === ColorMode.Indexed) {
|
|
163
|
+
// The palette is the colour mode data: 256 reds, then 256 greens, then blues.
|
|
164
|
+
if (colorDataLen < 768) throw new Error("PSD indexed image has no palette")
|
|
165
|
+
const pal = new Uint8Array(buffer, colorData, 768)
|
|
166
|
+
for (let i = 0; i < planeSamples; i++) {
|
|
167
|
+
const idx = sample(0, i)
|
|
168
|
+
rgba[i * 4] = pal[idx]
|
|
169
|
+
rgba[i * 4 + 1] = pal[256 + idx]
|
|
170
|
+
rgba[i * 4 + 2] = pal[512 + idx]
|
|
171
|
+
rgba[i * 4 + 3] = hasAlpha ? sample(1, i) : 255
|
|
172
|
+
}
|
|
173
|
+
return { rgba, width, height }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (let i = 0; i < planeSamples; i++) {
|
|
177
|
+
if (mode === ColorMode.RGB) {
|
|
178
|
+
rgba[i * 4] = sample(0, i)
|
|
179
|
+
rgba[i * 4 + 1] = sample(1, i)
|
|
180
|
+
rgba[i * 4 + 2] = sample(2, i)
|
|
181
|
+
} else {
|
|
182
|
+
// Grayscale and duotone: one plane across all three. Duotone's ink colours
|
|
183
|
+
// live in the colour mode data, and the spec's own advice is to treat the
|
|
184
|
+
// data as grayscale — which is what every other reader does.
|
|
185
|
+
const g = sample(0, i)
|
|
186
|
+
rgba[i * 4] = g
|
|
187
|
+
rgba[i * 4 + 1] = g
|
|
188
|
+
rgba[i * 4 + 2] = g
|
|
189
|
+
}
|
|
190
|
+
rgba[i * 4 + 3] = hasAlpha ? sample(colourChannels, i) : 255
|
|
191
|
+
}
|
|
192
|
+
return { rgba, width, height }
|
|
193
|
+
}
|