UNPKG

@gltf-transform/core

Version:

glTF 2.0 SDK for JavaScript and TypeScript, on Web and Node.js.

179 lines (156 loc) 5.6 kB
import type { vec2 } from '../constants.js'; import { BufferUtils } from './buffer-utils.js'; /** Implements support for an image format in the {@link ImageUtils} class. */ export interface ImageUtilsFormat { match(buffer: Uint8Array): boolean; getSize(buffer: Uint8Array): vec2 | null; getChannels(buffer: Uint8Array): number | null; getVRAMByteLength?(buffer: Uint8Array): number | null; } /** JPEG image support. */ class JPEGImageUtils implements ImageUtilsFormat { match(array: Uint8Array): boolean { return array.length >= 3 && array[0] === 255 && array[1] === 216 && array[2] === 255; } getSize(array: Uint8Array): vec2 { // Skip 4 chars, they are for signature let view = new DataView(array.buffer, array.byteOffset + 4); let i: number, next: number; while (view.byteLength) { // read length of the next block i = view.getUint16(0, false); // i = buffer.readUInt16BE(0); // ensure correct format validateJPEGBuffer(view, i); // 0xFFC0 is baseline standard(SOF) // 0xFFC1 is baseline optimized(SOF) // 0xFFC2 is progressive(SOF2) next = view.getUint8(i + 1); if (next === 0xc0 || next === 0xc1 || next === 0xc2) { return [view.getUint16(i + 7, false), view.getUint16(i + 5, false)]; } // move to the next block view = new DataView(array.buffer, view.byteOffset + i + 2); } throw new TypeError('Invalid JPG, no size found'); } getChannels(_buffer: Uint8Array): number { return 3; } } /** * PNG image support. * * PNG signature: 'PNG\r\n\x1a\n' * PNG image header chunk name: 'IHDR' */ class PNGImageUtils implements ImageUtilsFormat { // Used to detect "fried" png's: http://www.jongware.com/pngdefry.html static PNG_FRIED_CHUNK_NAME = 'CgBI'; match(array: Uint8Array): boolean { return ( array.length >= 8 && array[0] === 0x89 && array[1] === 0x50 && array[2] === 0x4e && array[3] === 0x47 && array[4] === 0x0d && array[5] === 0x0a && array[6] === 0x1a && array[7] === 0x0a ); } getSize(array: Uint8Array): vec2 { const view = new DataView(array.buffer, array.byteOffset); const magic = BufferUtils.decodeText(array.slice(12, 16)); if (magic === PNGImageUtils.PNG_FRIED_CHUNK_NAME) { return [view.getUint32(32, false), view.getUint32(36, false)]; } return [view.getUint32(16, false), view.getUint32(20, false)]; } getChannels(_buffer: Uint8Array): number { return 4; } } /** * *Common utilities for working with image data.* * * @category Utilities */ export class ImageUtils { static impls: Record<string, ImageUtilsFormat> = { 'image/jpeg': new JPEGImageUtils(), 'image/png': new PNGImageUtils(), }; /** Registers support for a new image format; useful for certain extensions. */ public static registerFormat(mimeType: string, impl: ImageUtilsFormat): void { this.impls[mimeType] = impl; } /** * Returns detected MIME type of the given image buffer. Note that for image * formats with support provided by extensions, the extension must be * registered with an I/O class before it can be detected by ImageUtils. */ public static getMimeType(buffer: Uint8Array): string | null { for (const mimeType in this.impls) { if (this.impls[mimeType].match(buffer)) { return mimeType; } } return null; } /** Returns the dimensions of the image. */ public static getSize(buffer: Uint8Array, mimeType: string): vec2 | null { if (!this.impls[mimeType]) return null; return this.impls[mimeType].getSize(buffer); } /** * Returns a conservative estimate of the number of channels in the image. For some image * formats, the method may return 4 indicating the possibility of an alpha channel, without * the ability to guarantee that an alpha channel is present. */ public static getChannels(buffer: Uint8Array, mimeType: string): number | null { if (!this.impls[mimeType]) return null; return this.impls[mimeType].getChannels(buffer); } /** Returns a conservative estimate of the GPU memory required by this image. */ public static getVRAMByteLength(buffer: Uint8Array, mimeType: string): number | null { if (!this.impls[mimeType]) return null; if (this.impls[mimeType].getVRAMByteLength) { return this.impls[mimeType].getVRAMByteLength!(buffer); } let uncompressedBytes = 0; const channels = 4; // See https://github.com/donmccurdy/glTF-Transform/issues/151. const resolution = this.getSize(buffer, mimeType); if (!resolution) return null; while (resolution[0] > 1 || resolution[1] > 1) { uncompressedBytes += resolution[0] * resolution[1] * channels; resolution[0] = Math.max(Math.floor(resolution[0] / 2), 1); resolution[1] = Math.max(Math.floor(resolution[1] / 2), 1); } uncompressedBytes += 1 * 1 * channels; return uncompressedBytes; } /** Returns the preferred file extension for the given MIME type. */ public static mimeTypeToExtension(mimeType: string): string { if (mimeType === 'image/jpeg') return 'jpg'; return mimeType.split('/').pop()!; } /** Returns the MIME type for the given file extension. */ public static extensionToMimeType(extension: string): string { if (extension === 'jpg') return 'image/jpeg'; if (!extension) return ''; return `image/${extension}`; } } function validateJPEGBuffer(view: DataView, i: number): DataView { // index should be within buffer limits if (i > view.byteLength) { throw new TypeError('Corrupt JPG, exceeded buffer limits'); } // Every JPEG block must begin with a 0xFF if (view.getUint8(i) !== 0xff) { throw new TypeError('Invalid JPG, marker table corrupted'); } return view; }