From 607951b403d4cdf4f49e783cd5cfdba8521f2945 Mon Sep 17 00:00:00 2001 From: Yegor Pelykh Date: Fri, 10 May 2024 20:01:33 +0300 Subject: [PATCH] feat: Support for PVR (PVRTC) format --- src/formats/image-format.ts | 1 + src/formats/pvr-decoder.ts | 792 ++++++++++++++++++++++ src/formats/pvr-encoder.ts | 398 +++++++++++ src/formats/pvr/pvr-apple-info.ts | 101 +++ src/formats/pvr/pvr-bit-utility.ts | 112 +++ src/formats/pvr/pvr-color-bounding-box.ts | 27 + src/formats/pvr/pvr-color-rgb-core.ts | 7 + src/formats/pvr/pvr-color-rgb.ts | 74 ++ src/formats/pvr/pvr-color-rgba.ts | 96 +++ src/formats/pvr/pvr-format.ts | 9 + src/formats/pvr/pvr-packet.ts | 338 +++++++++ src/formats/pvr/pvr2-info.ts | 106 +++ src/formats/pvr/pvr3-info.ts | 106 +++ src/index.ts | 57 ++ 14 files changed, 2224 insertions(+) create mode 100644 src/formats/pvr-decoder.ts create mode 100644 src/formats/pvr-encoder.ts create mode 100644 src/formats/pvr/pvr-apple-info.ts create mode 100644 src/formats/pvr/pvr-bit-utility.ts create mode 100644 src/formats/pvr/pvr-color-bounding-box.ts create mode 100644 src/formats/pvr/pvr-color-rgb-core.ts create mode 100644 src/formats/pvr/pvr-color-rgb.ts create mode 100644 src/formats/pvr/pvr-color-rgba.ts create mode 100644 src/formats/pvr/pvr-format.ts create mode 100644 src/formats/pvr/pvr-packet.ts create mode 100644 src/formats/pvr/pvr2-info.ts create mode 100644 src/formats/pvr/pvr3-info.ts diff --git a/src/formats/image-format.ts b/src/formats/image-format.ts index 9b1d0f9..b841604 100644 --- a/src/formats/image-format.ts +++ b/src/formats/image-format.ts @@ -8,6 +8,7 @@ export enum ImageFormat { png, pnm, psd, + pvr, tga, tiff, webp, diff --git a/src/formats/pvr-decoder.ts b/src/formats/pvr-decoder.ts new file mode 100644 index 0000000..6164deb --- /dev/null +++ b/src/formats/pvr-decoder.ts @@ -0,0 +1,792 @@ +/** @format */ + +import { InputBuffer } from '../common/input-buffer'; +import { TypedArray } from '../common/typings'; +import { MemoryImage } from '../image/image'; +import { DecodeInfo } from './decode-info'; +import { Decoder, DecoderDecodeOptions } from './decoder'; +import { ImageFormat } from './image-format'; +import { PvrAppleInfo } from './pvr/pvr-apple-info'; +import { PvrPacket } from './pvr/pvr-packet'; +import { Pvr2Info } from './pvr/pvr2-info'; +import { Pvr3Info } from './pvr/pvr3-info'; + +/** + * Ported from Jeffrey Lim's PVRTC encoder/decoder, + * https://bitbucket.org/jthlim/pvrtccompressor + */ +export class PvrDecoder implements Decoder { + private static readonly pvrHeaderSize = 52; + + private _data: Uint8Array | undefined; + private _info: DecodeInfo | undefined; + + public get format(): ImageFormat { + return ImageFormat.pvr; + } + + public get numFrames(): number { + return 1; + } + + private decodePvr3Header(bytes: Uint8Array): DecodeInfo | undefined { + const input = new InputBuffer({ + buffer: bytes, + }); + + const size = input.readUint32(); + if (size !== PvrDecoder.pvrHeaderSize) { + return undefined; + } + + const version = input.readUint32(); + const pvr3Signature = 0x03525650; + if (version !== pvr3Signature) { + return undefined; + } + + const flags = input.readUint32(); + const format = input.readUint32(); + const order = [input.read(), input.read(), input.read(), input.read()]; + const colorSpace = input.readUint32(); + const channelType = input.readUint32(); + const height = input.readUint32(); + const width = input.readUint32(); + const depth = input.readUint32(); + const numSurfaces = input.readUint32(); + const numFaces = input.readUint32(); + const mipCount = input.readUint32(); + const metadataSize = input.readUint32(); + + const info = new Pvr3Info({ + flags: flags, + format: format, + order: order, + colorSpace: colorSpace, + channelType: channelType, + height: height, + width: width, + depth: depth, + numSurfaces: numSurfaces, + numFaces: numFaces, + mipCount: mipCount, + metadataSize: metadataSize, + }); + + return info; + } + + private decodePvr2Header(bytes: Uint8Array): DecodeInfo | undefined { + const input = new InputBuffer({ + buffer: bytes, + }); + + const size = input.readUint32(); + if (size !== PvrDecoder.pvrHeaderSize) { + return undefined; + } + + const height = input.readUint32(); + const width = input.readUint32(); + const mipCount = input.readUint32(); + const flags = input.readUint32(); + const texDataSize = input.readUint32(); + const bitsPerPixel = input.readUint32(); + const redMask = input.readUint32(); + const greenMask = input.readUint32(); + const blueMask = input.readUint32(); + const alphaMask = input.readUint32(); + const magic = input.readUint32(); + const numTex = input.readUint32(); + + const info = new Pvr2Info({ + height: height, + width: width, + mipCount: mipCount, + flags: flags, + texDataSize: texDataSize, + bitsPerPixel: bitsPerPixel, + redMask: redMask, + greenMask: greenMask, + blueMask: blueMask, + alphaMask: alphaMask, + magic: magic, + numTex: numTex, + }); + + const pvr2Signature = 0x21525650; + if (info.magic !== pvr2Signature) { + return undefined; + } + + return info; + } + + private decodeApplePvrtcHeader(bytes: Uint8Array): DecodeInfo | undefined { + const fileSize = bytes.length; + + const input = new InputBuffer({ + buffer: bytes, + }); + + // Header + const sz = input.readUint32(); + if (sz !== 0) { + return undefined; + } + + let height = input.readUint32(); + let width = input.readUint32(); + const mipCount = input.readUint32(); + const flags = input.readUint32(); + const texDataSize = input.readUint32(); + const bitsPerPixel = input.readUint32(); + const redMask = input.readUint32(); + const greenMask = input.readUint32(); + const blueMask = input.readUint32(); + const magic = input.readUint32(); + + const info = new PvrAppleInfo({ + height: height, + width: width, + mipCount: mipCount, + flags: flags, + texDataSize: texDataSize, + bitsPerPixel: bitsPerPixel, + redMask: redMask, + greenMask: greenMask, + blueMask: blueMask, + magic: magic, + }); + + const appleSignature = 0x21525650; + if (info.magic === appleSignature) { + return undefined; + } + + let mode = 1; + let res = 8; + + // this is a tough one, could be 2bpp 8x8, 4bpp 8x8 + if (fileSize === 32) { + // assume 4bpp, 8x8 + mode = 0; + res = 8; + } else { + // Detect if it's 2bpp or 4bpp + let shift = 0; + // 16x16 + const test2bpp = 0x40; + // 16x16 + const test4bpp = 0x80; + + while (shift < 10) { + const s2 = shift << 1; + + if (((test2bpp << s2) & fileSize) !== 0) { + res = 16 << shift; + mode = 1; + //format = PVRTC2; + break; + } + + if (((test4bpp << s2) & fileSize) !== 0) { + res = 16 << shift; + mode = 0; + //format = PVRTC4; + break; + } + + ++shift; + } + + if (shift === 10) { + // no mode could be found. + return undefined; + } + } + + // there is no reliable way to know if it's a 2bpp or 4bpp file. Assuming + width = res; + height = res; + const bpp = (mode + 1) * 2; + + if (bpp === 4) { + // 2bpp is currently unsupported + return undefined; + } + + info.width = width; + info.height = height; + info.bitsPerPixel = bpp; + + return info; + } + + private decodePvr2(data: Uint8Array): MemoryImage | undefined { + const length = data.length; + + const pvrTexCubemap = 1 << 12; + const pvrPixelTypeMask = 0xff; + const pvrTypeRgba4444 = 0x10; + const pvrTypeRgba5551 = 0x11; + const pvrTypeRgba8888 = 0x12; + const pvrTypeRgb565 = 0x13; + const pvrTypeRgb555 = 0x14; + const pvrTypeRgb888 = 0x15; + const pvrTypeI8 = 0x16; + const pvrTypeAI8 = 0x17; + const pvrTypePvrtc2 = 0x18; + const pvrTypePvrtc4 = 0x19; + + if (length < PvrDecoder.pvrHeaderSize || this._info === undefined) { + return undefined; + } + + const info = this._info as Pvr2Info; + + const input = new InputBuffer({ + buffer: data, + }); + + // Header + input.skip(PvrDecoder.pvrHeaderSize); + + let numTex = info.numTex; + if (numTex < 1) { + numTex = (info.flags & pvrTexCubemap) !== 0 ? 6 : 1; + } + + if (numTex !== 1) { + // only 1 surface supported currently + return undefined; + } + + if ( + (info.width * info.height * info.bitsPerPixel) / 8 > + length - PvrDecoder.pvrHeaderSize + ) { + return undefined; + } + + const pType = info.flags & pvrPixelTypeMask; + + switch (pType) { + case pvrTypeRgba4444: { + const image = new MemoryImage({ + width: info.width, + height: info.height, + numChannels: 4, + }); + for (const p of image) { + const v1 = input.read(); + const v2 = input.read(); + const a = (v1 & 0x0f) << 4; + const b = v1 & 0xf0; + const g = (v2 & 0x0f) << 4; + const r = v2 & 0xf0; + p.r = r; + p.g = g; + p.b = b; + p.a = a; + } + return image; + } + case pvrTypeRgba5551: { + const image = new MemoryImage({ + width: info.width, + height: info.height, + numChannels: 4, + }); + for (const p of image) { + const v = input.readUint16(); + const r = (v & 0xf800) >> 8; + const g = (v & 0x07c0) >> 3; + const b = (v & 0x003e) << 2; + const a = (v & 0x0001) !== 0 ? 255 : 0; + p.r = r; + p.g = g; + p.b = b; + p.a = a; + } + return image; + } + case pvrTypeRgba8888: { + const image = new MemoryImage({ + width: info.width, + height: info.height, + numChannels: 4, + }); + for (const p of image) { + p.r = input.read(); + p.g = input.read(); + p.b = input.read(); + p.a = input.read(); + } + return image; + } + case pvrTypeRgb565: { + const image = new MemoryImage({ + width: info.width, + height: info.height, + }); + for (const p of image) { + const v = input.readUint16(); + const b = (v & 0x001f) << 3; + const g = (v & 0x07e0) >> 3; + const r = (v & 0xf800) >> 8; + p.r = r; + p.g = g; + p.b = b; + } + return image; + } + case pvrTypeRgb555: { + const image = new MemoryImage({ + width: info.width, + height: info.height, + }); + for (const p of image) { + const v = input.readUint16(); + const r = (v & 0x001f) << 3; + const g = (v & 0x03e0) >> 2; + const b = (v & 0x7c00) >> 7; + p.r = r; + p.g = g; + p.b = b; + } + return image; + } + case pvrTypeRgb888: { + const image = new MemoryImage({ + width: info.width, + height: info.height, + }); + for (const p of image) { + p.r = input.read(); + p.g = input.read(); + p.b = input.read(); + } + return image; + } + case pvrTypeI8: { + const image = new MemoryImage({ + width: info.width, + height: info.height, + numChannels: 1, + }); + for (const p of image) { + const i = input.read(); + p.r = i; + } + return image; + } + case pvrTypeAI8: { + const image = new MemoryImage({ + width: info.width, + height: info.height, + numChannels: 4, + }); + for (const p of image) { + const a = input.read(); + const i = input.read(); + p.r = i; + p.g = i; + p.b = i; + p.a = a; + } + return image; + } + case pvrTypePvrtc2: + // Currently unsupported + return undefined; + case pvrTypePvrtc4: + return info.alphaMask === 0 + ? this.decodeRgb4bpp(info.width, info.height, input.toUint8Array()) + : this.decodeRgba4bpp(info.width, info.height, input.toUint8Array()); + } + + // Unknown format + return undefined; + } + + private decodePvr3(data: Uint8Array): MemoryImage | undefined { + if (this._info instanceof Pvr3Info) { + return undefined; + } + + // const PVR3_PVRTC_2BPP_RGB = 0; + // const PVR3_PVRTC_2BPP_RGBA = 1; + const pvr3Pvrtc4bppRgb = 2; + const pvr3Pvrtc4bppRgba = 3; + // const PVR3_PVRTC2_2BPP = 4; + // const PVR3_PVRTC2_4BPP = 5; + // const PVR3_ETC1 = 6; + // const PVR3_DXT1 = 7; + // const PVR3_DXT2 = 8; + // const PVR3_DXT3 = 9; + // const PVR3_DXT4 = 10; + // const PVR3_DXT5 = 11; + // const PVR3_BC1 = 7; + // const PVR3_BC2 = 9; + // const PVR3_BC3 = 11; + // const PVR3_BC4 = 12; + // const PVR3_BC5 = 13; + // const PVR3_BC6 = 14; + // const PVR3_BC7 = 15; + // const PVR3_UYVY = 16; + // const PVR3_YUY2 = 17; + // const PVR3_BW_1BPP = 18; + // const PVR3_R9G9B9E5 = 19; + // const PVR3_RGBG8888 = 20; + // const PVR3_GRGB8888 = 21; + // const PVR3_ETC2_RGB = 22; + // const PVR3_ETC2_RGBA = 23; + // const PVR3_ETC2_RGB_A1 = 24; + // const PVR3_EAC_R11_U = 25; + // const PVR3_EAC_R11_S = 26; + // const PVR3_EAC_RG11_U = 27; + // const PVR3_EAC_RG11_S = 28; + + const input = new InputBuffer({ + buffer: data, + }); + + // Header + input.skip(PvrDecoder.pvrHeaderSize); + + const info = this._info as Pvr3Info; + + input.skip(info.metadataSize); + + if (info.order[0] === 0) { + switch (info.format) { + case pvr3Pvrtc4bppRgb: + return this.decodeRgb4bpp( + info.width, + info.height, + input.toUint8Array() + ); + case pvr3Pvrtc4bppRgba: + return this.decodeRgba4bpp( + info.width, + info.height, + input.toUint8Array() + ); + // case PVR3_PVRTC_2BPP_RGB: + // return undefined; + // case PVR3_PVRTC_2BPP_RGBA: + // return undefined; + // case PVR3_PVRTC2_2BPP: + // return undefined; + // case PVR3_PVRTC2_4BPP: + // return undefined; + // case PVR3_ETC1: + // return undefined; + // case PVR3_DXT1: + // return undefined; + // case PVR3_DXT2: + // return undefined; + // case PVR3_DXT3: + // return undefined; + // case PVR3_DXT4: + // return undefined; + // case PVR3_DXT5: + // return undefined; + // case PVR3_BC1: + // return undefined; + // case PVR3_BC2: + // return undefined; + // case PVR3_BC3: + // return undefined; + // case PVR3_BC4: + // return undefined; + // case PVR3_BC5: + // return undefined; + // case PVR3_BC6: + // return undefined; + // case PVR3_BC7: + // return undefined; + // case PVR3_UYVY: + // return undefined; + // case PVR3_YUY2: + // return undefined; + // case PVR3_BW_1BPP: + // return undefined; + // case PVR3_R9G9B9E5: + // return undefined; + // case PVR3_RGBG8888: + // return undefined; + // case PVR3_GRGB8888: + // return undefined; + // case PVR3_ETC2_RGB: + // return undefined; + // case PVR3_ETC2_RGBA: + // return undefined; + // case PVR3_ETC2_RGB_A1: + // return undefined; + // case PVR3_EAC_R11_U: + // return undefined; + // case PVR3_EAC_R11_S: + // return undefined; + // case PVR3_EAC_RG11_U: + // return undefined; + // case PVR3_EAC_RG11_S: + // return undefined; + } + } + + return undefined; + } + + private countBits(x: number): number { + let _x = x; + _x = (_x - ((_x >> 1) & 0x55555555)) & 0xffffffff; + _x = ((_x & 0x33333333) + ((_x >> 2) & 0x33333333)) & 0xffffffff; + _x = (_x + (_x >> 4)) & 0xffffffff; + _x &= 0xf0f0f0f; + _x = ((_x * 0x01010101) & 0xffffffff) >> 24; + return _x; + } + + private decodeRgb4bpp( + width: number, + height: number, + data: TypedArray + ): MemoryImage { + const result = new MemoryImage({ + width: width, + height: height, + }); + + const blocks = Math.trunc(width / 4); + const blockMask = blocks - 1; + + const packet = new PvrPacket(data); + const p0 = new PvrPacket(data); + const p1 = new PvrPacket(data); + const p2 = new PvrPacket(data); + const p3 = new PvrPacket(data); + const factors = PvrPacket.bilinearFactors; + const weights = PvrPacket.weights; + + for (let y = 0, y4 = 0; y < blocks; ++y, y4 += 4) { + for (let x = 0, x4 = 0; x < blocks; ++x, x4 += 4) { + packet.setBlock(x, y); + + let mod = packet.modulationData; + const weightIndex = packet.usePunchthroughAlpha ? 4 : 0; + let factorIndex = 0; + + for (let py = 0; py < 4; ++py) { + const yOffset = py < 2 ? -1 : 0; + const y0 = (y + yOffset) & blockMask; + const y1 = (y0 + 1) & blockMask; + + for (let px = 0; px < 4; ++px) { + const xOffset = px < 2 ? -1 : 0; + const x0 = (x + xOffset) & blockMask; + const x1 = (x0 + 1) & blockMask; + + p0.setBlock(x0, y0); + p1.setBlock(x1, y0); + p2.setBlock(x0, y1); + p3.setBlock(x1, y1); + + const ca = p0 + .getColorRgbA() + .mul(factors[factorIndex][0]) + .add( + p1 + .getColorRgbA() + .mul(factors[factorIndex][1]) + .add( + p2 + .getColorRgbA() + .mul(factors[factorIndex][2]) + .add(p3.getColorRgbA().mul(factors[factorIndex][3])) + ) + ); + + const cb = p0 + .getColorRgbB() + .mul(factors[factorIndex][0]) + .add( + p1 + .getColorRgbB() + .mul(factors[factorIndex][1]) + .add( + p2 + .getColorRgbB() + .mul(factors[factorIndex][2]) + .add(p3.getColorRgbB().mul(factors[factorIndex][3])) + ) + ); + + const w = weights[(weightIndex + mod) & 3]; + + const r = (ca.r * w[0] + cb.r * w[1]) >> 7; + const g = (ca.g * w[0] + cb.g * w[1]) >> 7; + const b = (ca.b * w[0] + cb.b * w[1]) >> 7; + result.setPixelRgb(px + x4, py + y4, r, g, b); + + mod >>= 2; + factorIndex++; + } + } + } + } + + return result; + } + + private decodeRgba4bpp( + width: number, + height: number, + data: TypedArray + ): MemoryImage { + const result = new MemoryImage({ + width: width, + height: height, + numChannels: 4, + }); + + const blocks = Math.trunc(width / 4); + const blockMask = blocks - 1; + + const packet = new PvrPacket(data); + const p0 = new PvrPacket(data); + const p1 = new PvrPacket(data); + const p2 = new PvrPacket(data); + const p3 = new PvrPacket(data); + const factors = PvrPacket.bilinearFactors; + const weights = PvrPacket.weights; + + for (let y = 0, y4 = 0; y < blocks; ++y, y4 += 4) { + for (let x = 0, x4 = 0; x < blocks; ++x, x4 += 4) { + packet.setBlock(x, y); + + let mod = packet.modulationData; + const weightIndex = packet.usePunchthroughAlpha ? 4 : 0; + let factorIndex = 0; + + for (let py = 0; py < 4; ++py) { + const yOffset = py < 2 ? -1 : 0; + const y0 = (y + yOffset) & blockMask; + const y1 = (y0 + 1) & blockMask; + + for (let px = 0; px < 4; ++px) { + const xOffset = px < 2 ? -1 : 0; + const x0 = (x + xOffset) & blockMask; + const x1 = (x0 + 1) & blockMask; + + p0.setBlock(x0, y0); + p1.setBlock(x1, y0); + p2.setBlock(x0, y1); + p3.setBlock(x1, y1); + + const ca = p0 + .getColorRgbaA() + .mul(factors[factorIndex][0]) + .add( + p1 + .getColorRgbaA() + .mul(factors[factorIndex][1]) + .add( + p2 + .getColorRgbaA() + .mul(factors[factorIndex][2]) + .add(p3.getColorRgbaA().mul(factors[factorIndex][3])) + ) + ); + + const cb = p0 + .getColorRgbaB() + .mul(factors[factorIndex][0]) + .add( + p1 + .getColorRgbaB() + .mul(factors[factorIndex][1]) + .add( + p2 + .getColorRgbaB() + .mul(factors[factorIndex][2]) + .add(p3.getColorRgbaB().mul(factors[factorIndex][3])) + ) + ); + + const w = weights[(weightIndex + mod) & 3]; + + const r = (ca.r * w[0] + cb.r * w[1]) >> 7; + const g = (ca.g * w[0] + cb.g * w[1]) >> 7; + const b = (ca.b * w[0] + cb.b * w[1]) >> 7; + const a = (ca.a * w[2] + cb.a * w[3]) >> 7; + result.setPixelRgba(px + x4, py + y4, r, g, b, a); + + mod >>= 2; + factorIndex++; + } + } + } + } + + return result; + } + + public isValidFile(bytes: Uint8Array): boolean { + return this.startDecode(bytes) !== undefined; + } + + public startDecode(bytes: Uint8Array): DecodeInfo | undefined { + // Use a heuristic to detect potential apple PVRTC formats + if (this.countBits(bytes.length) === 1) { + // very likely to be apple PVRTC + const info = this.decodeApplePvrtcHeader(bytes); + if (info !== undefined) { + this._data = bytes; + return (this._info = info); + } + } + + let info = this.decodePvr3Header(bytes); + if (info !== undefined) { + this._data = bytes; + return (this._info = info); + } + + info = this.decodePvr2Header(bytes); + if (info !== undefined) { + this._data = bytes; + return (this._info = info); + } + + return undefined; + } + + public decode(opt: DecoderDecodeOptions): MemoryImage | undefined { + if (this.startDecode(opt.bytes) === undefined) { + return undefined; + } + return this.decodeFrame(opt.frameIndex ?? 0); + } + + public decodeFrame(_frameIndex: number): MemoryImage | undefined { + if (this._info === undefined || this._data === undefined) { + return undefined; + } + + if (this._info instanceof PvrAppleInfo) { + return this.decodeRgba4bpp( + this._info.width, + this._info.height, + this._data + ); + } else if (this._info instanceof Pvr2Info) { + return this.decodePvr2(this._data); + } else if (this._info instanceof Pvr3Info) { + return this.decodePvr3(this._data); + } + + return undefined; + } +} diff --git a/src/formats/pvr-encoder.ts b/src/formats/pvr-encoder.ts new file mode 100644 index 0000000..af0732c --- /dev/null +++ b/src/formats/pvr-encoder.ts @@ -0,0 +1,398 @@ +/** @format */ + +import { OutputBuffer } from '../common/output-buffer'; +import { LibError } from '../error/lib-error'; +import { MemoryImage } from '../image/image'; +import { Encoder, EncoderEncodeOptions } from './encoder'; +import { PvrBitUtility } from './pvr/pvr-bit-utility'; +import { PvrColorBoundingBox } from './pvr/pvr-color-bounding-box'; +import { PvrColorRgb } from './pvr/pvr-color-rgb'; +import { PvrColorRgba } from './pvr/pvr-color-rgba'; +import { PvrFormat } from './pvr/pvr-format'; +import { PvrPacket } from './pvr/pvr-packet'; + +/** + * Ported from Jeffrey Lim's PVRTC encoder/decoder, + * https://bitbucket.org/jthlim/pvrtccompressor + */ +export class PvrEncoder implements Encoder { + private readonly _format: PvrFormat; + + private _supportsAnimation = false; + public get supportsAnimation(): boolean { + return this._supportsAnimation; + } + + constructor(format: PvrFormat = PvrFormat.auto) { + this._format = format; + } + + private static calculateBoundingBoxRgb( + bitmap: MemoryImage, + blockX: number, + blockY: number + ): PvrColorBoundingBox { + const pixel = (x: number, y: number): PvrColorRgb => { + const p = bitmap.getPixel(blockX + x, blockY + y); + return new PvrColorRgb(Math.trunc(p.r), Math.trunc(p.g), Math.trunc(p.b)); + }; + const cbb = new PvrColorBoundingBox(pixel(0, 0), pixel(0, 0)); + cbb.add(pixel(1, 0)); + cbb.add(pixel(2, 0)); + cbb.add(pixel(3, 0)); + cbb.add(pixel(0, 1)); + cbb.add(pixel(1, 1)); + cbb.add(pixel(1, 2)); + cbb.add(pixel(1, 3)); + cbb.add(pixel(2, 0)); + cbb.add(pixel(2, 1)); + cbb.add(pixel(2, 2)); + cbb.add(pixel(2, 3)); + cbb.add(pixel(3, 0)); + cbb.add(pixel(3, 1)); + cbb.add(pixel(3, 2)); + cbb.add(pixel(3, 3)); + return cbb; + } + + private static calculateBoundingBoxRgba( + bitmap: MemoryImage, + blockX: number, + blockY: number + ): PvrColorBoundingBox { + const pixel = (x: number, y: number): PvrColorRgba => { + const p = bitmap.getPixel(blockX + x, blockY + y); + return new PvrColorRgba( + Math.trunc(p.r), + Math.trunc(p.g), + Math.trunc(p.b), + Math.trunc(p.a) + ); + }; + const cbb = new PvrColorBoundingBox(pixel(0, 0), pixel(0, 0)); + cbb.add(pixel(1, 0)); + cbb.add(pixel(2, 0)); + cbb.add(pixel(3, 0)); + cbb.add(pixel(0, 1)); + cbb.add(pixel(1, 1)); + cbb.add(pixel(1, 2)); + cbb.add(pixel(1, 3)); + cbb.add(pixel(2, 0)); + cbb.add(pixel(2, 1)); + cbb.add(pixel(2, 2)); + cbb.add(pixel(2, 3)); + cbb.add(pixel(3, 0)); + cbb.add(pixel(3, 1)); + cbb.add(pixel(3, 2)); + cbb.add(pixel(3, 3)); + return cbb; + } + + public encode(opt: EncoderEncodeOptions): Uint8Array { + const output = new OutputBuffer(); + + let format = this._format; + + let pvrtc: Uint8Array | undefined = undefined; + switch (format) { + case PvrFormat.auto: + if (opt.image.numChannels === 3) { + pvrtc = this.encodeRgb4bpp(opt.image); + format = PvrFormat.rgb4; + } else { + pvrtc = this.encodeRgba4bpp(opt.image); + format = PvrFormat.rgba4; + } + break; + case PvrFormat.rgb2: + pvrtc = this.encodeRgb4bpp(opt.image); + break; + case PvrFormat.rgba2: + pvrtc = this.encodeRgba4bpp(opt.image); + break; + case PvrFormat.rgb4: + pvrtc = this.encodeRgb4bpp(opt.image); + break; + case PvrFormat.rgba4: + pvrtc = this.encodeRgba4bpp(opt.image); + break; + } + + const version = 55727696; + const flags = 0; + const pixelFormat = format - 1; + const channelOrder = 0; + const colorSpace = 0; + const channelType = 0; + const height = opt.image.height; + const width = opt.image.width; + const depth = 1; + const numSurfaces = 1; + const numFaces = 1; + const mipmapCount = 1; + const metaDataSize = 0; + + output.writeUint32(version); + output.writeUint32(flags); + output.writeUint32(pixelFormat); + output.writeUint32(channelOrder); + output.writeUint32(colorSpace); + output.writeUint32(channelType); + output.writeUint32(height); + output.writeUint32(width); + output.writeUint32(depth); + output.writeUint32(numSurfaces); + output.writeUint32(numFaces); + output.writeUint32(mipmapCount); + output.writeUint32(metaDataSize); + output.writeBytes(pvrtc); + + return output.getBytes(); + } + + public encodeRgb4bpp(bitmap: MemoryImage): Uint8Array { + if (bitmap.width !== bitmap.height) { + throw new LibError('PVRTC requires a square image.'); + } + + if (!PvrBitUtility.isPowerOf2(bitmap.width)) { + throw new LibError('PVRTC requires a power-of-two sized image.'); + } + + const size = bitmap.width; + const blocks = Math.trunc(size / 4); + const blockMask = blocks - 1; + + // Allocate enough data for encoding the image. + const outputData = new Uint8Array( + Math.trunc((bitmap.width * bitmap.height) / 2) + ); + const packet = new PvrPacket(outputData); + const p0 = new PvrPacket(outputData); + const p1 = new PvrPacket(outputData); + const p2 = new PvrPacket(outputData); + const p3 = new PvrPacket(outputData); + + for (let y = 0; y < blocks; ++y) { + for (let x = 0; x < blocks; ++x) { + const cbb = PvrEncoder.calculateBoundingBoxRgb(bitmap, x, y); + packet.setBlock(x, y); + packet.usePunchthroughAlpha = false; + packet.setColorRgbA(cbb.min as PvrColorRgb); + packet.setColorRgbB(cbb.max as PvrColorRgb); + } + } + + const factors = PvrPacket.bilinearFactors; + + for (let y = 0, y4 = 0; y < blocks; ++y, y4 += 4) { + for (let x = 0, x4 = 0; x < blocks; ++x, x4 += 4) { + let factorIndex = 0; + let modulationData = 0; + + for (let py = 0; py < 4; ++py) { + const yOffset = py < 2 ? -1 : 0; + const y0 = (y + yOffset) & blockMask; + const y1 = (y0 + 1) & blockMask; + + for (let px = 0; px < 4; ++px) { + const xOffset = px < 2 ? -1 : 0; + const x0 = (x + xOffset) & blockMask; + const x1 = (x0 + 1) & blockMask; + + p0.setBlock(x0, y0); + p1.setBlock(x1, y0); + p2.setBlock(x0, y1); + p3.setBlock(x1, y1); + + const ca = p0 + .getColorRgbA() + .mul(factors[factorIndex][0]) + .add( + p1 + .getColorRgbA() + .mul(factors[factorIndex][1]) + .add( + p2 + .getColorRgbA() + .mul(factors[factorIndex][2]) + .add(p3.getColorRgbA().mul(factors[factorIndex][3])) + ) + ); + + const cb = p0 + .getColorRgbB() + .mul(factors[factorIndex][0]) + .add( + p1 + .getColorRgbB() + .mul(factors[factorIndex][1]) + .add( + p2 + .getColorRgbB() + .mul(factors[factorIndex][2]) + .add(p3.getColorRgbB().mul(factors[factorIndex][3])) + ) + ); + + const pi = bitmap.getPixel(x4 + px, y4 + py); + const r = Math.trunc(pi.r); + const g = Math.trunc(pi.g); + const b = Math.trunc(pi.b); + + const d = cb.sub(ca); + const p = new PvrColorRgb(r * 16, g * 16, b * 16); + const v = p.sub(ca); + + // PVRTC uses weightings of 0, 3/8, 5/8 and 1 + // The boundaries for these are 3/16, 1/2 (=8/16), 13/16 + const projection = v.dotProd(d) * 16; + const lengthSquared = d.dotProd(d); + if (projection > 3 * lengthSquared) { + modulationData++; + } + if (projection > 8 * lengthSquared) { + modulationData++; + } + if (projection > 13 * lengthSquared) { + modulationData++; + } + + modulationData = PvrBitUtility.rotateRight(modulationData, 2); + + factorIndex++; + } + } + + packet.setBlock(x, y); + packet.modulationData = modulationData; + } + } + + return outputData; + } + + public encodeRgba4bpp(bitmap: MemoryImage): Uint8Array { + if (bitmap.width !== bitmap.height) { + throw new LibError('PVRTC requires a square image.'); + } + + if (!PvrBitUtility.isPowerOf2(bitmap.width)) { + throw new LibError('PVRTC requires a power-of-two sized image.'); + } + + const size = bitmap.width; + const blocks = Math.trunc(size / 4); + const blockMask = blocks - 1; + + // Allocate enough data for encoding the image. + const outputData = new Uint8Array( + Math.trunc((bitmap.width * bitmap.height) / 2) + ); + const packet = new PvrPacket(outputData); + const p0 = new PvrPacket(outputData); + const p1 = new PvrPacket(outputData); + const p2 = new PvrPacket(outputData); + const p3 = new PvrPacket(outputData); + + for (let y = 0, y4 = 0; y < blocks; ++y, y4 += 4) { + for (let x = 0, x4 = 0; x < blocks; ++x, x4 += 4) { + const cbb = PvrEncoder.calculateBoundingBoxRgba(bitmap, x4, y4); + packet.setBlock(x, y); + packet.usePunchthroughAlpha = false; + packet.setColorRgbaA(cbb.min as PvrColorRgba); + packet.setColorRgbaB(cbb.max as PvrColorRgba); + } + } + + const factors = PvrPacket.bilinearFactors; + + for (let y = 0, y4 = 0; y < blocks; ++y, y4 += 4) { + for (let x = 0, x4 = 0; x < blocks; ++x, x4 += 4) { + let factorIndex = 0; + let modulationData = 0; + + for (let py = 0; py < 4; ++py) { + const yOffset = py < 2 ? -1 : 0; + const y0 = (y + yOffset) & blockMask; + const y1 = (y0 + 1) & blockMask; + + for (let px = 0; px < 4; ++px) { + const xOffset = px < 2 ? -1 : 0; + const x0 = (x + xOffset) & blockMask; + const x1 = (x0 + 1) & blockMask; + + p0.setBlock(x0, y0); + p1.setBlock(x1, y0); + p2.setBlock(x0, y1); + p3.setBlock(x1, y1); + + const ca = p0 + .getColorRgbaA() + .mul(factors[factorIndex][0]) + .add( + p1 + .getColorRgbaA() + .mul(factors[factorIndex][1]) + .add( + p2 + .getColorRgbaA() + .mul(factors[factorIndex][2]) + .add(p3.getColorRgbaA().mul(factors[factorIndex][3])) + ) + ); + + const cb = p0 + .getColorRgbaB() + .mul(factors[factorIndex][0]) + .add( + p1 + .getColorRgbaB() + .mul(factors[factorIndex][1]) + .add( + p2 + .getColorRgbaB() + .mul(factors[factorIndex][2]) + .add(p3.getColorRgbaB().mul(factors[factorIndex][3])) + ) + ); + + const bp = bitmap.getPixel(x4 + px, y4 + py); + const r = Math.trunc(bp.r); + const g = Math.trunc(bp.g); + const b = Math.trunc(bp.b); + const a = Math.trunc(bp.a); + + const d = cb.sub(ca); + const p = new PvrColorRgba(r * 16, g * 16, b * 16, a * 16); + const v = p.sub(ca); + + // PVRTC uses weightings of 0, 3/8, 5/8 and 1 + // The boundaries for these are 3/16, 1/2 (=8/16), 13/16 + const projection = v.dotProd(d) * 16; + const lengthSquared = d.dotProd(d); + + if (projection > 3 * lengthSquared) { + modulationData++; + } + if (projection > 8 * lengthSquared) { + modulationData++; + } + if (projection > 13 * lengthSquared) { + modulationData++; + } + + modulationData = PvrBitUtility.rotateRight(modulationData, 2); + + factorIndex++; + } + } + + packet.setBlock(x, y); + packet.modulationData = modulationData; + } + } + + return outputData; + } +} diff --git a/src/formats/pvr/pvr-apple-info.ts b/src/formats/pvr/pvr-apple-info.ts new file mode 100644 index 0000000..dea8e47 --- /dev/null +++ b/src/formats/pvr/pvr-apple-info.ts @@ -0,0 +1,101 @@ +/** @format */ + +import { Color } from '../../color/color'; +import { DecodeInfo } from '../decode-info'; + +export interface PvrAppleInfoOptions { + width: number; + height: number; + mipCount: number; + flags: number; + texDataSize: number; + bitsPerPixel: number; + redMask: number; + greenMask: number; + blueMask: number; + magic: number; +} + +export class PvrAppleInfo implements DecodeInfo { + private readonly _backgroundColor: Color | undefined = undefined; + public get backgroundColor(): Color | undefined { + return this._backgroundColor; + } + + private readonly _numFrames: number = 1; + public get numFrames(): number { + return this._numFrames; + } + + private _width: number = 0; + public get width(): number { + return this._width; + } + public set width(v: number) { + this._width = v; + } + + private _height: number = 0; + public get height(): number { + return this._height; + } + public set height(v: number) { + this._height = v; + } + + private _mipCount: number = 0; + public get mipCount(): number { + return this._mipCount; + } + + private _flags: number = 0; + public get flags(): number { + return this._flags; + } + + private _texDataSize: number = 0; + public get texDataSize(): number { + return this._texDataSize; + } + + private _bitsPerPixel: number = 0; + public get bitsPerPixel(): number { + return this._bitsPerPixel; + } + public set bitsPerPixel(v: number) { + this._bitsPerPixel = v; + } + + private _redMask: number = 0; + public get redMask(): number { + return this._redMask; + } + + private _greenMask: number = 0; + public get greenMask(): number { + return this._greenMask; + } + + private _blueMask: number = 0; + public get blueMask(): number { + return this._blueMask; + } + + private _magic: number = 0; + public get magic(): number { + return this._magic; + } + + constructor(opt: PvrAppleInfoOptions) { + this._width = opt.width; + this._height = opt.height; + this._mipCount = opt.mipCount; + this._flags = opt.flags; + this._texDataSize = opt.texDataSize; + this._bitsPerPixel = opt.bitsPerPixel; + this._redMask = opt.redMask; + this._greenMask = opt.greenMask; + this._blueMask = opt.blueMask; + this._magic = opt.magic; + } +} diff --git a/src/formats/pvr/pvr-bit-utility.ts b/src/formats/pvr/pvr-bit-utility.ts new file mode 100644 index 0000000..2c28d55 --- /dev/null +++ b/src/formats/pvr/pvr-bit-utility.ts @@ -0,0 +1,112 @@ +/** @format */ + +export class PvrBitUtility { + public static isPowerOf2(x: number): boolean { + return (x & (x - 1)) === 0; + } + + public static rotateRight(value: number, shift: number): number { + return (value >> shift) | (value << (32 - shift)); + } + + public static readonly bitScale5To8 = [ + 0, 8, 16, 24, 32, 41, 49, 57, 65, 74, 82, 90, 98, 106, 115, 123, 131, 139, + 148, 156, 164, 172, 180, 189, 197, 205, 213, 222, 230, 238, 246, 255, + ]; + + public static readonly bitScale4To8 = [ + 0, 17, 34, 51, 68, 85, 102, 119, 136, 153, 170, 187, 204, 221, 238, 255, + ]; + + public static readonly bitScale3To8 = [0, 36, 72, 109, 145, 182, 218, 255]; + + public static readonly bitScale8To5Floor = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, + 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, + 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, + 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 16, 16, + 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, + 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, + 20, 21, 21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 22, 23, + 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, + 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, + 27, 27, 28, 28, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 30, + 30, 30, 30, 30, 30, 30, 30, 31, + ]; + + public static readonly bitScale8To4Floor = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 12, 12, + 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 15, + ]; + + public static readonly bitScale8To3Floor = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 7, + ]; + + public static readonly bitScale8To5Ceil = [ + 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, + 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, + 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 10, 10, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 12, 12, 12, + 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, + 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 16, 16, + 17, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, 18, 18, 19, 19, + 19, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, + 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 23, 23, 23, + 23, 24, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, + 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, + 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 30, 30, + 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, + ]; + + public static readonly bitScale8To4Ceil = [ + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, + 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 12, + 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, + 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, + 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, + 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, + ]; + + public static readonly bitScale8To3Ceil = [ + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, + 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, + ]; +} diff --git a/src/formats/pvr/pvr-color-bounding-box.ts b/src/formats/pvr/pvr-color-bounding-box.ts new file mode 100644 index 0000000..c3a0f32 --- /dev/null +++ b/src/formats/pvr/pvr-color-bounding-box.ts @@ -0,0 +1,27 @@ +/** @format */ + +import { PvrColorRgbCore } from './pvr-color-rgb-core'; + +export type PvrColor = PvrColorRgbCore; + +export class PvrColorBoundingBox { + private _min: PvrColor; + public get min(): PvrColor { + return this._min; + } + + private _max: PvrColor; + public get max(): PvrColor { + return this._max; + } + + constructor(min: PvrColor, max: PvrColor) { + this._min = min.copy(); + this._max = max.copy(); + } + + public add(c: PvrColor): void { + this._min.setMin(c); + this._max.setMax(c); + } +} diff --git a/src/formats/pvr/pvr-color-rgb-core.ts b/src/formats/pvr/pvr-color-rgb-core.ts new file mode 100644 index 0000000..d3db37b --- /dev/null +++ b/src/formats/pvr/pvr-color-rgb-core.ts @@ -0,0 +1,7 @@ +/** @format */ + +export interface PvrColorRgbCore { + copy(): T; + setMin(c: T): void; + setMax(c: T): void; +} diff --git a/src/formats/pvr/pvr-color-rgb.ts b/src/formats/pvr/pvr-color-rgb.ts new file mode 100644 index 0000000..092f1a3 --- /dev/null +++ b/src/formats/pvr/pvr-color-rgb.ts @@ -0,0 +1,74 @@ +/** @format */ + +import { PvrColorRgbCore } from './pvr-color-rgb-core'; + +export class PvrColorRgb implements PvrColorRgbCore { + private _r: number; + public get r(): number { + return this._r; + } + + private _g: number; + public get g(): number { + return this._g; + } + + private _b: number; + public get b(): number { + return this._b; + } + + constructor(r: number = 0, g: number = 0, b: number = 0) { + this._r = r; + this._g = g; + this._b = b; + } + + public static from(other: PvrColorRgb): PvrColorRgb { + return new PvrColorRgb(other._r, other._g, other._b); + } + + public copy(): PvrColorRgb { + return PvrColorRgb.from(this); + } + + public setMin(c: PvrColorRgb): void { + if (c._r < this._r) { + this._r = c._r; + } + if (c._g < this._g) { + this._g = c._g; + } + if (c._b < this._b) { + this._b = c._b; + } + } + + public setMax(c: PvrColorRgb): void { + if (c._r > this._r) { + this._r = c._r; + } + if (c._g > this._g) { + this._g = c._g; + } + if (c._b > this._b) { + this._b = c._b; + } + } + + public mul(x: number): PvrColorRgb { + return new PvrColorRgb(this._r * x, this._g * x, this._b * x); + } + + public add(x: PvrColorRgb): PvrColorRgb { + return new PvrColorRgb(this._r + x._r, this._g + x._g, this._b + x._b); + } + + public sub(x: PvrColorRgb): PvrColorRgb { + return new PvrColorRgb(this._r - x._r, this._g - x._g, this._b - x._b); + } + + public dotProd(x: PvrColorRgb): number { + return this._r * x._r + this._g * x._g + this._b * x._b; + } +} diff --git a/src/formats/pvr/pvr-color-rgba.ts b/src/formats/pvr/pvr-color-rgba.ts new file mode 100644 index 0000000..a31963d --- /dev/null +++ b/src/formats/pvr/pvr-color-rgba.ts @@ -0,0 +1,96 @@ +/** @format */ + +import { PvrColorRgbCore } from './pvr-color-rgb-core'; + +export class PvrColorRgba implements PvrColorRgbCore { + private _r: number; + public get r(): number { + return this._r; + } + + private _g: number; + public get g(): number { + return this._g; + } + + private _b: number; + public get b(): number { + return this._b; + } + + private _a: number; + public get a(): number { + return this._a; + } + + constructor(r: number = 0, g: number = 0, b: number = 0, a: number = 0) { + this._r = r; + this._g = g; + this._b = b; + this._a = a; + } + + public static from(other: PvrColorRgba): PvrColorRgba { + return new PvrColorRgba(other._r, other._g, other._b, other._a); + } + + public copy(): PvrColorRgba { + return PvrColorRgba.from(this); + } + + public setMin(c: PvrColorRgba): void { + if (c._r < this._r) { + this._r = c._r; + } + if (c._g < this._g) { + this._g = c._g; + } + if (c._b < this._b) { + this._b = c._b; + } + if (c._a < this._a) { + this._a = c._a; + } + } + + public setMax(c: PvrColorRgba): void { + if (c._r > this._r) { + this._r = c._r; + } + if (c._g > this._g) { + this._g = c._g; + } + if (c._b > this._b) { + this._b = c._b; + } + if (c._a > this._a) { + this._a = c._a; + } + } + + public mul(x: number): PvrColorRgba { + return new PvrColorRgba(this._r * x, this._g * x, this._b * x, this._a * x); + } + + public add(x: PvrColorRgba): PvrColorRgba { + return new PvrColorRgba( + this._r + x._r, + this._g + x._g, + this._b + x._b, + this._a + x._a + ); + } + + public sub(x: PvrColorRgba): PvrColorRgba { + return new PvrColorRgba( + this._r - x._r, + this._g - x._g, + this._b - x._b, + this._a - x._a + ); + } + + public dotProd(x: PvrColorRgba): number { + return this._r * x._r + this._g * x._g + this._b * x._b + this._a * x._a; + } +} diff --git a/src/formats/pvr/pvr-format.ts b/src/formats/pvr/pvr-format.ts new file mode 100644 index 0000000..d413dde --- /dev/null +++ b/src/formats/pvr/pvr-format.ts @@ -0,0 +1,9 @@ +/** @format */ + +export enum PvrFormat { + auto, + rgb2, + rgba2, + rgb4, + rgba4, +} diff --git a/src/formats/pvr/pvr-packet.ts b/src/formats/pvr/pvr-packet.ts new file mode 100644 index 0000000..c7090d6 --- /dev/null +++ b/src/formats/pvr/pvr-packet.ts @@ -0,0 +1,338 @@ +/** @format */ + +import { TypedArray } from '../../common/typings'; +import { PvrBitUtility } from './pvr-bit-utility'; +import { PvrColorRgb } from './pvr-color-rgb'; +import { PvrColorRgba } from './pvr-color-rgba'; + +/** + * Ported from Jeffrey Lim's PVRTC encoder/decoder, + * https://bitbucket.org/jthlim/pvrtccompressor + */ +export class PvrPacket { + private static readonly bits14 = (1 << 14) - 1; + private static readonly bits15 = (1 << 15) - 1; + + private static readonly mortonTable = [ + 0x0000, 0x0001, 0x0004, 0x0005, 0x0010, 0x0011, 0x0014, 0x0015, 0x0040, + 0x0041, 0x0044, 0x0045, 0x0050, 0x0051, 0x0054, 0x0055, 0x0100, 0x0101, + 0x0104, 0x0105, 0x0110, 0x0111, 0x0114, 0x0115, 0x0140, 0x0141, 0x0144, + 0x0145, 0x0150, 0x0151, 0x0154, 0x0155, 0x0400, 0x0401, 0x0404, 0x0405, + 0x0410, 0x0411, 0x0414, 0x0415, 0x0440, 0x0441, 0x0444, 0x0445, 0x0450, + 0x0451, 0x0454, 0x0455, 0x0500, 0x0501, 0x0504, 0x0505, 0x0510, 0x0511, + 0x0514, 0x0515, 0x0540, 0x0541, 0x0544, 0x0545, 0x0550, 0x0551, 0x0554, + 0x0555, 0x1000, 0x1001, 0x1004, 0x1005, 0x1010, 0x1011, 0x1014, 0x1015, + 0x1040, 0x1041, 0x1044, 0x1045, 0x1050, 0x1051, 0x1054, 0x1055, 0x1100, + 0x1101, 0x1104, 0x1105, 0x1110, 0x1111, 0x1114, 0x1115, 0x1140, 0x1141, + 0x1144, 0x1145, 0x1150, 0x1151, 0x1154, 0x1155, 0x1400, 0x1401, 0x1404, + 0x1405, 0x1410, 0x1411, 0x1414, 0x1415, 0x1440, 0x1441, 0x1444, 0x1445, + 0x1450, 0x1451, 0x1454, 0x1455, 0x1500, 0x1501, 0x1504, 0x1505, 0x1510, + 0x1511, 0x1514, 0x1515, 0x1540, 0x1541, 0x1544, 0x1545, 0x1550, 0x1551, + 0x1554, 0x1555, 0x4000, 0x4001, 0x4004, 0x4005, 0x4010, 0x4011, 0x4014, + 0x4015, 0x4040, 0x4041, 0x4044, 0x4045, 0x4050, 0x4051, 0x4054, 0x4055, + 0x4100, 0x4101, 0x4104, 0x4105, 0x4110, 0x4111, 0x4114, 0x4115, 0x4140, + 0x4141, 0x4144, 0x4145, 0x4150, 0x4151, 0x4154, 0x4155, 0x4400, 0x4401, + 0x4404, 0x4405, 0x4410, 0x4411, 0x4414, 0x4415, 0x4440, 0x4441, 0x4444, + 0x4445, 0x4450, 0x4451, 0x4454, 0x4455, 0x4500, 0x4501, 0x4504, 0x4505, + 0x4510, 0x4511, 0x4514, 0x4515, 0x4540, 0x4541, 0x4544, 0x4545, 0x4550, + 0x4551, 0x4554, 0x4555, 0x5000, 0x5001, 0x5004, 0x5005, 0x5010, 0x5011, + 0x5014, 0x5015, 0x5040, 0x5041, 0x5044, 0x5045, 0x5050, 0x5051, 0x5054, + 0x5055, 0x5100, 0x5101, 0x5104, 0x5105, 0x5110, 0x5111, 0x5114, 0x5115, + 0x5140, 0x5141, 0x5144, 0x5145, 0x5150, 0x5151, 0x5154, 0x5155, 0x5400, + 0x5401, 0x5404, 0x5405, 0x5410, 0x5411, 0x5414, 0x5415, 0x5440, 0x5441, + 0x5444, 0x5445, 0x5450, 0x5451, 0x5454, 0x5455, 0x5500, 0x5501, 0x5504, + 0x5505, 0x5510, 0x5511, 0x5514, 0x5515, 0x5540, 0x5541, 0x5544, 0x5545, + 0x5550, 0x5551, 0x5554, 0x5555, + ]; + + public static readonly bilinearFactors = [ + [4, 4, 4, 4], + [2, 6, 2, 6], + [8, 0, 8, 0], + [6, 2, 6, 2], + [2, 2, 6, 6], + [1, 3, 3, 9], + [4, 0, 12, 0], + [3, 1, 9, 3], + [8, 8, 0, 0], + [4, 12, 0, 0], + [16, 0, 0, 0], + [12, 4, 0, 0], + [6, 6, 2, 2], + [3, 9, 1, 3], + [12, 0, 4, 0], + [9, 3, 3, 1], + ]; + + // Weights are { colorA, colorB, alphaA, alphaB } + public static readonly weights = [ + // Weights for Mode = 0 + [8, 0, 8, 0], + [5, 3, 5, 3], + [3, 5, 3, 5], + [0, 8, 0, 8], + + // Weights for Mode = 1 + [8, 0, 8, 0], + [4, 4, 4, 4], + [4, 4, 0, 0], + [0, 8, 0, 8], + ]; + + private _rawData: Uint32Array; + public get rawData(): Uint32Array { + return this._rawData; + } + + private _index: number = 0; + public get index(): number { + return this._index; + } + + public get modulationData(): number { + return this._rawData[this._index]; + } + public set modulationData(x: number) { + this._rawData[this._index] = x; + } + + public get colorData(): number { + return this._rawData[this._index + 1]; + } + public set colorData(x: number) { + this._rawData[this._index + 1] = x; + } + + private _usePunchthroughAlpha: boolean = false; + public get usePunchthroughAlpha(): boolean { + return this._usePunchthroughAlpha; + } + public set usePunchthroughAlpha(x: boolean) { + this._usePunchthroughAlpha = x; + this.colorData = this.getColorData(); + } + + private _colorA: number = 0; + public get colorA(): number { + return this._colorA; + } + public set colorA(x: number) { + this._colorA = x; + this.colorData = this.getColorData(); + } + + private _colorAIsOpaque: boolean = false; + public get colorAIsOpaque(): boolean { + return this._colorAIsOpaque; + } + public set colorAIsOpaque(x: boolean) { + this._colorAIsOpaque = x; + this.colorData = this.getColorData(); + } + + private _colorB: number = 0; + public get colorB(): number { + return this._colorB; + } + public set colorB(x: number) { + this._colorB = x; + this.colorData = this.getColorData(); + } + + private _colorBIsOpaque: boolean = false; + public get colorBIsOpaque(): boolean { + return this._colorBIsOpaque; + } + public set colorBIsOpaque(x: boolean) { + this._colorBIsOpaque = x; + this.colorData = this.getColorData(); + } + + constructor(data: TypedArray) { + this._rawData = new Uint32Array(data.buffer); + } + + private static getMortonNumber(x: number, y: number): number { + return ( + (this.mortonTable[x >> 8] << 17) | + (this.mortonTable[y >> 8] << 16) | + (this.mortonTable[x & 0xff] << 1) | + this.mortonTable[y & 0xff] + ); + } + + private getColorData(): number { + return ( + (this.usePunchthroughAlpha ? 1 : 0) | + ((this.colorA & PvrPacket.bits14) << 1) | + ((this.colorAIsOpaque ? 1 : 0) << 15) | + ((this.colorB & PvrPacket.bits15) << 16) | + ((this.colorBIsOpaque ? 1 : 0) << 31) + ); + } + + private update(): void { + const x = this.colorData; + this.usePunchthroughAlpha = (x & 1) === 1; + this.colorA = (x >> 1) & PvrPacket.bits14; + this.colorAIsOpaque = ((x >> 15) & 1) === 1; + this.colorB = (x >> 16) & PvrPacket.bits15; + this.colorBIsOpaque = ((x >> 31) & 1) === 1; + } + + public setBlock(x: number, y: number): void { + this.setIndex(PvrPacket.getMortonNumber(x, y)); + } + + public setIndex(i: number): void { + // A PvrPacket uses 2 uint32 values, so get the physical index + // from the logical index by multiplying by 2. + this._index = i << 1; + // Pull in the values from the raw data. + this.update(); + } + + public setColorRgbA(c: PvrColorRgb): void { + const r = PvrBitUtility.bitScale8To5Floor[c.r]; + const g = PvrBitUtility.bitScale8To5Floor[c.g]; + const b = PvrBitUtility.bitScale8To4Floor[c.b]; + this.colorA = (r << 9) | (g << 4) | b; + this.colorAIsOpaque = true; + } + + public setColorRgbaA(c: PvrColorRgba): void { + const a = PvrBitUtility.bitScale8To3Floor[c.a]; + if (a === 7) { + const r = PvrBitUtility.bitScale8To5Floor[c.r]; + const g = PvrBitUtility.bitScale8To5Floor[c.g]; + const b = PvrBitUtility.bitScale8To4Floor[c.b]; + this.colorA = (r << 9) | (g << 4) | b; + this.colorAIsOpaque = true; + } else { + const r = PvrBitUtility.bitScale8To4Floor[c.r]; + const g = PvrBitUtility.bitScale8To4Floor[c.g]; + const b = PvrBitUtility.bitScale8To3Floor[c.b]; + this.colorA = (a << 11) | (r << 7) | (g << 3) | b; + this.colorAIsOpaque = false; + } + } + + public setColorRgbB(c: PvrColorRgb): void { + const r = PvrBitUtility.bitScale8To5Ceil[c.r]; + const g = PvrBitUtility.bitScale8To5Ceil[c.g]; + const b = PvrBitUtility.bitScale8To5Ceil[c.b]; + this.colorB = (r << 10) | (g << 5) | b; + this.colorBIsOpaque = false; + } + + public setColorRgbaB(c: PvrColorRgba): void { + const a = PvrBitUtility.bitScale8To3Ceil[c.a]; + if (a === 7) { + const r = PvrBitUtility.bitScale8To5Ceil[c.r]; + const g = PvrBitUtility.bitScale8To5Ceil[c.g]; + const b = PvrBitUtility.bitScale8To5Ceil[c.b]; + this.colorB = (r << 10) | (g << 5) | b; + this.colorBIsOpaque = true; + } else { + const r = PvrBitUtility.bitScale8To4Ceil[c.r]; + const g = PvrBitUtility.bitScale8To4Ceil[c.g]; + const b = PvrBitUtility.bitScale8To4Ceil[c.b]; + this.colorB = (a << 12) | (r << 8) | (g << 4) | b; + this.colorBIsOpaque = false; + } + } + + public getColorRgbA(): PvrColorRgb { + if (this.colorAIsOpaque) { + const r = this.colorA >> 9; + const g = (this.colorA >> 4) & 0x1f; + const b = this.colorA & 0xf; + return new PvrColorRgb( + PvrBitUtility.bitScale5To8[r], + PvrBitUtility.bitScale5To8[g], + PvrBitUtility.bitScale4To8[b] + ); + } else { + const r = (this.colorA >> 7) & 0xf; + const g = (this.colorA >> 3) & 0xf; + const b = this.colorA & 7; + return new PvrColorRgb( + PvrBitUtility.bitScale4To8[r], + PvrBitUtility.bitScale4To8[g], + PvrBitUtility.bitScale3To8[b] + ); + } + } + + public getColorRgbaA(): PvrColorRgba { + if (this.colorAIsOpaque) { + const r = this.colorA >> 9; + const g = (this.colorA >> 4) & 0x1f; + const b = this.colorA & 0xf; + return new PvrColorRgba( + PvrBitUtility.bitScale5To8[r], + PvrBitUtility.bitScale5To8[g], + PvrBitUtility.bitScale4To8[b], + 255 + ); + } else { + const a = (this.colorA >> 11) & 7; + const r = (this.colorA >> 7) & 0xf; + const g = (this.colorA >> 3) & 0xf; + const b = this.colorA & 7; + return new PvrColorRgba( + PvrBitUtility.bitScale4To8[r], + PvrBitUtility.bitScale4To8[g], + PvrBitUtility.bitScale3To8[b], + PvrBitUtility.bitScale3To8[a] + ); + } + } + + public getColorRgbB(): PvrColorRgb { + if (this.colorBIsOpaque) { + const r = this.colorB >> 10; + const g = (this.colorB >> 5) & 0x1f; + const b = this.colorB & 0x1f; + return new PvrColorRgb( + PvrBitUtility.bitScale5To8[r], + PvrBitUtility.bitScale5To8[g], + PvrBitUtility.bitScale5To8[b] + ); + } else { + const r = (this.colorB >> 8) & 0xf; + const g = (this.colorB >> 4) & 0xf; + const b = this.colorB & 0xf; + return new PvrColorRgb( + PvrBitUtility.bitScale4To8[r], + PvrBitUtility.bitScale4To8[g], + PvrBitUtility.bitScale4To8[b] + ); + } + } + + public getColorRgbaB(): PvrColorRgba { + if (this.colorBIsOpaque) { + const r = this.colorB >> 10; + const g = (this.colorB >> 5) & 0x1f; + const b = this.colorB & 0x1f; + return new PvrColorRgba( + PvrBitUtility.bitScale5To8[r], + PvrBitUtility.bitScale5To8[g], + PvrBitUtility.bitScale5To8[b], + 255 + ); + } else { + const a = (this.colorB >> 12) & 7; + const r = (this.colorB >> 8) & 0xf; + const g = (this.colorB >> 4) & 0xf; + const b = this.colorB & 0xf; + return new PvrColorRgba( + PvrBitUtility.bitScale4To8[r], + PvrBitUtility.bitScale4To8[g], + PvrBitUtility.bitScale4To8[b], + PvrBitUtility.bitScale3To8[a] + ); + } + } +} diff --git a/src/formats/pvr/pvr2-info.ts b/src/formats/pvr/pvr2-info.ts new file mode 100644 index 0000000..bc586f3 --- /dev/null +++ b/src/formats/pvr/pvr2-info.ts @@ -0,0 +1,106 @@ +/** @format */ + +import { Color } from '../../color/color'; +import { DecodeInfo } from '../decode-info'; + +export interface Pvr2InfoOptions { + width: number; + height: number; + mipCount: number; + flags: number; + texDataSize: number; + bitsPerPixel: number; + redMask: number; + greenMask: number; + blueMask: number; + alphaMask: number; + magic: number; + numTex: number; +} + +export class Pvr2Info implements DecodeInfo { + private readonly _backgroundColor: Color | undefined = undefined; + public get backgroundColor(): Color | undefined { + return this._backgroundColor; + } + + private readonly _numFrames: number = 1; + public get numFrames(): number { + return this._numFrames; + } + + private _width: number = 0; + public get width(): number { + return this._width; + } + + private _height: number = 0; + public get height(): number { + return this._height; + } + + private _mipCount: number = 0; + public get mipCount(): number { + return this._mipCount; + } + + private _flags: number = 0; + public get flags(): number { + return this._flags; + } + + private _texDataSize: number = 0; + public get texDataSize(): number { + return this._texDataSize; + } + + private _bitsPerPixel: number = 0; + public get bitsPerPixel(): number { + return this._bitsPerPixel; + } + + private _redMask: number = 0; + public get redMask(): number { + return this._redMask; + } + + private _greenMask: number = 0; + public get greenMask(): number { + return this._greenMask; + } + + private _blueMask: number = 0; + public get blueMask(): number { + return this._blueMask; + } + + private _alphaMask: number = 0; + public get alphaMask(): number { + return this._alphaMask; + } + + private _magic: number = 0; + public get magic(): number { + return this._magic; + } + + private _numTex: number = 0; + public get numTex(): number { + return this._numTex; + } + + constructor(opt: Pvr2InfoOptions) { + this._width = opt.width; + this._height = opt.height; + this._mipCount = opt.mipCount; + this._flags = opt.flags; + this._texDataSize = opt.texDataSize; + this._bitsPerPixel = opt.bitsPerPixel; + this._redMask = opt.redMask; + this._greenMask = opt.greenMask; + this._blueMask = opt.blueMask; + this._alphaMask = opt.alphaMask; + this._magic = opt.magic; + this._numTex = opt.numTex; + } +} diff --git a/src/formats/pvr/pvr3-info.ts b/src/formats/pvr/pvr3-info.ts new file mode 100644 index 0000000..e60395d --- /dev/null +++ b/src/formats/pvr/pvr3-info.ts @@ -0,0 +1,106 @@ +/** @format */ + +import { Color } from '../../color/color'; +import { DecodeInfo } from '../decode-info'; + +export interface Pvr3InfoOptions { + width: number; + height: number; + mipCount: number; + flags: number; + format: number; + order: number[]; + colorSpace: number; + channelType: number; + depth: number; + numSurfaces: number; + numFaces: number; + metadataSize: number; +} + +export class Pvr3Info implements DecodeInfo { + private readonly _backgroundColor: Color | undefined = undefined; + public get backgroundColor(): Color | undefined { + return this._backgroundColor; + } + + private readonly _numFrames: number = 1; + public get numFrames(): number { + return this._numFrames; + } + + private _width: number = 0; + public get width(): number { + return this._width; + } + + private _height: number = 0; + public get height(): number { + return this._height; + } + + private _mipCount: number = 0; + public get mipCount(): number { + return this._mipCount; + } + + private _flags: number = 0; + public get flags(): number { + return this._flags; + } + + private _format: number = 0; + public get format(): number { + return this._format; + } + + private _order: number[] = [0, 0, 0, 0]; + public get order(): number[] { + return this._order; + } + + private _colorSpace: number = 0; + public get colorSpace(): number { + return this._colorSpace; + } + + private _channelType: number = 0; + public get channelType(): number { + return this._channelType; + } + + private _depth: number = 0; + public get depth(): number { + return this._depth; + } + + private _numSurfaces: number = 0; + public get numSurfaces(): number { + return this._numSurfaces; + } + + private _numFaces: number = 0; + public get numFaces(): number { + return this._numFaces; + } + + private _metadataSize: number = 0; + public get metadataSize(): number { + return this._metadataSize; + } + + constructor(opt: Pvr3InfoOptions) { + this._width = opt.width; + this._height = opt.height; + this._mipCount = opt.mipCount; + this._flags = opt.flags; + this._format = opt.format; + this._order = opt.order; + this._colorSpace = opt.colorSpace; + this._channelType = opt.channelType; + this._depth = opt.depth; + this._numSurfaces = opt.numSurfaces; + this._numFaces = opt.numFaces; + this._metadataSize = opt.metadataSize; + } +} diff --git a/src/index.ts b/src/index.ts index 4cd7c7c..0ea452e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,8 @@ import { WebPDecoder } from './formats/webp-decoder'; import { PnmDecoder } from './formats/pnm-decoder'; import { ImageFormat } from './formats/image-format'; import { PsdDecoder } from './formats/psd-decoder'; +import { PvrEncoder } from './formats/pvr-encoder'; +import { PvrDecoder } from './formats/pvr-decoder'; // Export types from 'color' directory export { ChannelOrder, ChannelOrderLength } from './color/channel-order'; @@ -266,6 +268,20 @@ export { PsdImageResource } from './formats/psd/psd-image-resource'; export { PsdLayer } from './formats/psd/psd-layer'; export { PsdMask } from './formats/psd/psd-mask'; +export { + PvrAppleInfo, + PvrAppleInfoOptions, +} from './formats/pvr/pvr-apple-info'; +export { PvrBitUtility } from './formats/pvr/pvr-bit-utility'; +export { PvrColorBoundingBox } from './formats/pvr/pvr-color-bounding-box'; +export { PvrColorRgb } from './formats/pvr/pvr-color-rgb'; +export { PvrColorRgbCore } from './formats/pvr/pvr-color-rgb-core'; +export { PvrColorRgba } from './formats/pvr/pvr-color-rgba'; +export { PvrFormat } from './formats/pvr/pvr-format'; +export { PvrPacket } from './formats/pvr/pvr-packet'; +export { Pvr2Info, Pvr2InfoOptions } from './formats/pvr/pvr2-info'; +export { Pvr3Info, Pvr3InfoOptions } from './formats/pvr/pvr3-info'; + export { TgaImageType, TgaImageTypeLength } from './formats/tga/tga-image-type'; export { TgaInfo, TgaInfoInitOptions } from './formats/tga/tga-info'; @@ -338,6 +354,8 @@ export { PngDecoder } from './formats/png-decoder'; export { PngEncoder, PngEncoderInitOptions } from './formats/png-encoder'; export { PnmDecoder } from './formats/pnm-decoder'; export { PsdDecoder } from './formats/psd-decoder'; +export { PvrDecoder } from './formats/pvr-decoder'; +export { PvrEncoder } from './formats/pvr-encoder'; export { TgaDecoder } from './formats/tga-decoder'; export { TgaEncoder } from './formats/tga-encoder'; export { TiffDecoder } from './formats/tiff-decoder'; @@ -526,6 +544,8 @@ export function findDecoderForMimeType(mimeType: string): Decoder | undefined { case 'image/x-portable-pixmap': case 'image/x-portable-anymap': return new PnmDecoder(); + case 'image/x-pvr': + return new PvrDecoder(); default: return undefined; } @@ -572,6 +592,9 @@ export function findDecoderForNamedImage(name: string): Decoder | undefined { ) { return new PnmDecoder(); } + if (n.endsWith('.pvr')) { + return new PvrDecoder(); + } return undefined; } @@ -599,6 +622,8 @@ export function findEncoderForMimeType(mimeType: string): Encoder | undefined { case 'image/x-icon': case 'image/vnd.microsoft.icon': return new IcoEncoder(); + case 'image/x-pvr': + return new PvrEncoder(); default: return undefined; } @@ -634,6 +659,9 @@ export function findEncoderForNamedImage(name: string): Encoder | undefined { if (n.endsWith('.cur')) { return new IcoEncoder(); } + if (n.endsWith('.pvr')) { + return new PvrEncoder(); + } return undefined; } @@ -667,6 +695,8 @@ export function findDecoderForFormat(format: ImageFormat): Decoder | undefined { return new PnmDecoder(); case ImageFormat.psd: return new PsdDecoder(); + case ImageFormat.pvr: + return new PvrDecoder(); case ImageFormat.tga: return new TgaDecoder(); case ImageFormat.tiff: @@ -736,6 +766,11 @@ export function findDecoderForData(data: TypedArray): Decoder | undefined { return ico; } + const pvr = new PvrDecoder(); + if (pvr.isValidFile(bytes)) { + return pvr; + } + const pnm = new PnmDecoder(); if (pnm.isValidFile(bytes)) { return pnm; @@ -1080,3 +1115,25 @@ export function encodeIco(opt: EncodeAnimatedOptions): Uint8Array { export function encodeIcoImages(opt: EncodeIcoImagesOptions): Uint8Array { return new IcoEncoder().encodeImages(opt.images); } + +/** + * Decode an PVR image. + */ +export function decodePvr(opt: DecodeImageOptions): MemoryImage | undefined { + const dataUint8 = new Uint8Array(opt.data); + return new PvrDecoder().decode({ + bytes: dataUint8, + frameIndex: opt.frameIndex, + }); +} + +/** + * Encode an image to the PVR format. + */ +export function encodePvr(opt: EncodeAnimatedOptions): Uint8Array { + const singleFrame = opt.singleFrame ?? false; + return new PvrEncoder().encode({ + image: opt.image, + singleFrame: singleFrame, + }); +}