From 5965998756562ae3811ce2b6708c61a98887cdc2 Mon Sep 17 00:00:00 2001 From: ddenysiuk Date: Wed, 24 Jul 2024 18:38:35 +0200 Subject: [PATCH 1/4] Compressed textures support --- package-lock.json | 224 +++++++++++++++++- packages/assetpack/package.json | 1 + packages/assetpack/src/image/compress.ts | 44 +++- .../src/image/utils/compressGpuTextures.ts | 110 +++++++++ .../src/image/utils/compressSharp.ts | 14 +- packages/assetpack/src/spine/AtlasView.ts | 2 +- .../assetpack/src/spine/spineAtlasCompress.ts | 20 +- .../texture-packer/texturePackerCompress.ts | 26 +- .../assetpack/test/image/Compress.test.ts | 27 +++ .../assetpack/test/manifest/Manifest.test.ts | 29 ++- .../test/spine/spineAtlasAll.test.ts | 27 ++- .../test/spine/spineAtlasCompress.test.ts | 6 + .../texture-packer/texturePackerAll.test.ts | 21 +- .../texturePackerCompress.test.ts | 6 + .../texturePackerManifest.test.ts | 15 +- packages/assetpack/vitest.config.js | 8 + 16 files changed, 527 insertions(+), 53 deletions(-) create mode 100644 packages/assetpack/src/image/utils/compressGpuTextures.ts diff --git a/package-lock.json b/package-lock.json index 480db27..66fa64f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3776,6 +3776,73 @@ "node": ">=8" } }, + "node_modules/@gpu-tex-enc/astc": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@gpu-tex-enc/astc/-/astc-4.7.0.tgz", + "integrity": "sha512-YBeXSYM/WP71hAxLgFxpaF6VEe2dwkS9rjTtKMPRDJEJoja/NIHWrqScwJC600UyxY7kLsjFTek7W6WtYzyi8A==", + "dependencies": { + "cpu-features": "^0.0.9" + }, + "bin": { + "astcenc-darwin-arm64-avx2": "bin/darwin/astcenc", + "astcenc-darwin-arm64-sse2": "bin/darwin/astcenc", + "astcenc-darwin-arm64-sse4.1": "bin/darwin/astcenc", + "astcenc-darwin-x64-avx2": "bin/darwin/astcenc", + "astcenc-darwin-x64-sse2": "bin/darwin/astcenc", + "astcenc-darwin-x64-sse4.1": "bin/darwin/astcenc", + "astcenc-linux-x64-avx2": "bin/linux-x64/astcenc-avx2", + "astcenc-linux-x64-sse2": "bin/linux-x64/astcenc-sse2", + "astcenc-linux-x64-sse4.1": "bin/linux-x64/astcenc-sse4.1", + "astcenc-win32-arm64-avx2": "bin/win32-arm64/astcenc-neon.exe", + "astcenc-win32-arm64-sse2": "bin/win32-arm64/astcenc-neon.exe", + "astcenc-win32-arm64-sse4.1": "bin/win32-arm64/astcenc-neon.exe", + "astcenc-win32-x64-avx2": "bin/win32-x64/astcenc-avx2.exe", + "astcenc-win32-x64-sse2": "bin/win32-x64/astcenc-sse2.exe", + "astcenc-win32-x64-sse4.1": "bin/win32-x64/astcenc-sse4.1.exe" + } + }, + "node_modules/@gpu-tex-enc/basis": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/@gpu-tex-enc/basis/-/basis-1.16.3.tgz", + "integrity": "sha512-u5ooQXTb5UasL8rI2kWz1CMuSARKgvTK2TjQqnOTVWg5gW/HZu0hn+2WkgjkqHKgkxtVIHHJuN2esCu+EmnShQ==", + "dependencies": { + "cpu-features": "^0.0.9" + }, + "bin": { + "basisu-darwin-arm64": "bin/darwin-arm64/basisu", + "basisu-darwin-arm64-sse": "bin/darwin-arm64/basisu", + "basisu-darwin-x64": "bin/darwin-x64/basisu", + "basisu-darwin-x64-sse": "bin/darwin-x64/basisu-sse", + "basisu-linux-arm64": "bin/linux-arm64/basisu", + "basisu-linux-arm64-sse": "bin/linux-arm64/basisu", + "basisu-linux-x64": "bin/linux-x64/basisu", + "basisu-linux-x64-sse": "bin/linux-x64/basisu-sse", + "basisu-win32-x64": "bin/win32-x64/basisu.exe", + "basisu-win32-x64-sse": "bin/win32-x64/basisu-sse.exe" + } + }, + "node_modules/@gpu-tex-enc/bc": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@gpu-tex-enc/bc/-/bc-1.0.11.tgz", + "integrity": "sha512-Nxp3uUS3MG02XMA28uwey9aLNltW0lxRV4gKqrckYGEC4V5R8MG29LXQcW7hJ3xIqhUG2kvt0E80tbq6WzO9Cg==", + "bin": { + "bc7enc-darwin-arm64": "bin/darwin-arm64/bc7enc", + "bc7enc-darwin-x64": "bin/darwin-x64/bc7enc", + "bc7enc-linux": "bin/linux/bc7enc", + "bc7enc-win32": "bin/win32/bc7enc.exe" + } + }, + "node_modules/@gpu-tex-enc/etc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@gpu-tex-enc/etc/-/etc-1.0.3.tgz", + "integrity": "sha512-KRGP0qua3bzj1wdNlkFxg/TV3beGCW5iOlFjVJ90+osDVqYBLd1BsEqRSlqxd3b/gG1S7dL/gX4igqZtUDobKQ==", + "bin": { + "etc2comp-darwin-arm64": "bin/darwin-arm64/EtcTool", + "etc2comp-darwin-x64": "bin/darwin-x64/EtcTool", + "etc2comp-linux": "bin/linux/EtcTool", + "etc2comp-win32": "bin/win32/EtcTool.exe" + } + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -10816,6 +10883,14 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, + "node_modules/buildcheck": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", + "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -12093,6 +12168,19 @@ } } }, + "node_modules/cpu-features": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.9.tgz", + "integrity": "sha512-AKjgn2rP2yJyfbepsmLfiYcmtNn/2eUvocUyM/09yB0YDiz39HteK/5/T4Onf0pmdYDMgkBoGvRLvEguzyL7wQ==", + "hasInstallScript": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.17.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -15571,7 +15659,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -16137,6 +16224,132 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gpu-tex-enc": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/gpu-tex-enc/-/gpu-tex-enc-1.2.4.tgz", + "integrity": "sha512-1dRld8Lovtw1xaHH5PEz/SUTiC+1SFS+C71psupWOfgoRknTBdSIdARCP5IwPUlvVdKeRdMK8HKD+u/asOjt5g==", + "dependencies": { + "@gpu-tex-enc/astc": "^4.7.0", + "@gpu-tex-enc/basis": "^1.16.3", + "@gpu-tex-enc/bc": "^1.0.11", + "@gpu-tex-enc/etc": "^1.0.3", + "yargs": "^17.7.2" + }, + "bin": { + "gputexenc": "src/cli.js" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "sharp": "^0.33.0" + } + }, + "node_modules/gpu-tex-enc/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/gpu-tex-enc/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/gpu-tex-enc/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gpu-tex-enc/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/gpu-tex-enc/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/gpu-tex-enc/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/gpu-tex-enc/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/gpu-tex-enc/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/gpu-tex-enc/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -23992,6 +24205,11 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/nan": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", + "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==" + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -27255,7 +27473,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -31782,7 +31999,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, "engines": { "node": ">=10" } @@ -31826,7 +32042,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, "engines": { "node": ">=12" } @@ -31944,6 +32159,7 @@ "fluent-ffmpeg": "^2.1.3", "fs-extra": "^11.2.0", "glob": "^10.4.1", + "gpu-tex-enc": "^1.2.4", "maxrects-packer": "^2.7.3", "merge": "^2.1.1", "minimatch": "9.0.4", diff --git a/packages/assetpack/package.json b/packages/assetpack/package.json index d9846ea..5772199 100644 --- a/packages/assetpack/package.json +++ b/packages/assetpack/package.json @@ -69,6 +69,7 @@ "fluent-ffmpeg": "^2.1.3", "fs-extra": "^11.2.0", "glob": "^10.4.1", + "gpu-tex-enc": "^1.2.4", "maxrects-packer": "^2.7.3", "merge": "^2.1.1", "minimatch": "9.0.4", diff --git a/packages/assetpack/src/image/compress.ts b/packages/assetpack/src/image/compress.ts index 92ba323..86a28f9 100644 --- a/packages/assetpack/src/image/compress.ts +++ b/packages/assetpack/src/image/compress.ts @@ -1,8 +1,10 @@ import sharp from 'sharp'; import { checkExt, createNewAssetAt } from '../core/index.js'; +import { compressGpuTextures } from './utils/compressGpuTextures.js'; import { compressSharp } from './utils/compressSharp.js'; import { resolveOptions } from './utils/resolveOptions.js'; +import type { AstcOptions, BasisOptions, BcOptions } from 'gpu-tex-enc'; import type { AvifOptions, JpegOptions, PngOptions, WebpOptions } from 'sharp'; import type { Asset, AssetPipe, PluginOptions } from '../core/index.js'; @@ -10,6 +12,9 @@ type CompressJpgOptions = Omit; type CompressWebpOptions = Omit; type CompressAvifOptions = Omit; type CompressPngOptions = Omit; +type CompressBc7Options = BcOptions; +type CompressAstcOptions = AstcOptions; +type CompressBasisOptions = BasisOptions; export interface CompressOptions extends PluginOptions { @@ -17,6 +22,9 @@ export interface CompressOptions extends PluginOptions webp?: CompressWebpOptions | boolean; avif?: CompressAvifOptions | boolean; jpg?: CompressJpgOptions | boolean; + bc7?: CompressBc7Options | boolean; + astc?: CompressAstcOptions | boolean; + basis?: CompressBasisOptions | boolean; } export interface CompressImageData @@ -26,6 +34,13 @@ export interface CompressImageData sharpImage: sharp.Sharp; } +export interface CompressImageDataResult +{ + format: CompressImageData['format'] | '.bc7.dds' | '.astc.ktx' | '.basis.ktx2'; + resolution: number; + buffer: Buffer; +} + export function compress(options: CompressOptions = {}): AssetPipe { const compress = resolveOptions(options, { @@ -33,6 +48,9 @@ export function compress(options: CompressOptions = {}): AssetPipe(compress.avif, { }); + + compress.bc7 = resolveOptions(compress.bc7, { + + }); + + compress.astc = resolveOptions(compress.astc, { + + }); + + compress.basis = resolveOptions(compress.basis, { + + }); } return { @@ -82,7 +112,10 @@ export function compress(options: CompressOptions = {}): AssetPipe { @@ -95,16 +128,11 @@ export function compress(options: CompressOptions = {}): AssetPipe image.sharpImage.toBuffer().then((buffer) => - { - newAssets[i].buffer = buffer; - })); - - await Promise.all(promises); - return newAssets; } catch (error) diff --git a/packages/assetpack/src/image/utils/compressGpuTextures.ts b/packages/assetpack/src/image/utils/compressGpuTextures.ts new file mode 100644 index 0000000..ca0a7c7 --- /dev/null +++ b/packages/assetpack/src/image/utils/compressGpuTextures.ts @@ -0,0 +1,110 @@ +import { + generateASTC as astc, + generateBasis as basis, + generateBC as bc, +} from 'gpu-tex-enc'; +import crypto from 'node:crypto'; +import { promises as fs } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { extname, join } from 'node:path'; + +import type { + AstcOptions, + BasisOptions, + BcOptions +} from 'gpu-tex-enc'; +import type sharp from 'sharp'; +import type { CompressImageData, CompressImageDataResult, CompressOptions } from '../compress.js'; + +export async function compressGpuTextures( + image: CompressImageData, + options: CompressOptions, +): Promise +{ + const compressed: CompressImageDataResult[] = []; + + if (!options.astc && !options.bc7 && !options.basis) + { + return compressed; + } + + const tmpDir = await fs.mkdtemp(join(tmpdir(), 'assetpack-tex-')); + + try + { + const imagePath = join(tmpDir, `${crypto.randomUUID()}.png`); + + const sharpImage = image.sharpImage; + const pngImage = image.format !== '.png' + ? sharpImage.clone().png({ quality: 100, force: true }) // most texture generators only support PNG. + : sharpImage.clone(); + + await pngImage.toFile(imagePath); + + if (options.astc) + { + const astcOpts = typeof options.astc === 'boolean' ? {} : options.astc as AstcOptions; + + compressed.push({ + format: '.astc.ktx', + resolution: 1, + buffer: await fs.readFile(await astc(imagePath, astcOpts.blocksize, astcOpts.quality, astcOpts.colorProfile, astcOpts.options)), + }); + } + + if (options.bc7) + { + const bc7Opts = typeof options.bc7 === 'boolean' ? {} : options.bc7 as BcOptions; + + compressed.push({ + format: '.bc7.dds', + resolution: 1, + buffer: await fs.readFile(await bc(imagePath, 'BC7', true, bc7Opts.options)), + }); + } + + if (options.basis) + { + const basisOpts = typeof options.basis === 'boolean' ? {} : options.basis as BasisOptions; + const adjustedImagePath = await adjustImageSize(pngImage, imagePath); + + compressed.push({ + format: '.basis.ktx2', + resolution: 1, + buffer: await fs.readFile(await basis(adjustedImagePath, 'UASTC', true, basisOpts.options)), + }); + } + } + finally + { + await fs.rm(tmpDir, { recursive: true, force: true }); + } + + return compressed; +} + +async function adjustImageSize(sharpImage: sharp.Sharp, imagePath: string): Promise +{ + const metadata = await sharpImage.metadata(); + const { width = 0, height = 0 } = metadata; + const right = width % 4 !== 0 ? 4 - (width % 4) : 0; + const bottom = height % 4 !== 0 ? 4 - (height % 4) : 0; + + if (right > 0 || bottom > 0) + { + const adjustedImagePath = `${imagePath}.1${extname(imagePath)}`; + + await sharpImage + .clone() + .extend({ + bottom, + right, + background: { r: 0, g: 0, b: 0, alpha: 0 }, + }) + .toFile(adjustedImagePath); + + return adjustedImagePath; + } + + return imagePath; +} diff --git a/packages/assetpack/src/image/utils/compressSharp.ts b/packages/assetpack/src/image/utils/compressSharp.ts index 2d8ea98..6be5442 100644 --- a/packages/assetpack/src/image/utils/compressSharp.ts +++ b/packages/assetpack/src/image/utils/compressSharp.ts @@ -1,12 +1,12 @@ import type { AvifOptions, JpegOptions, PngOptions, WebpOptions } from 'sharp'; -import type { CompressImageData, CompressOptions } from '../compress.js'; +import type { CompressImageData, CompressImageDataResult, CompressOptions } from '../compress.js'; export async function compressSharp( image: CompressImageData, options: CompressOptions -): Promise +): Promise { - const compressed: CompressImageData[] = []; + const compressed: CompressImageDataResult[] = []; const sharpImage = image.sharpImage; @@ -15,7 +15,7 @@ export async function compressSharp( compressed.push({ format: '.png', resolution: image.resolution, - sharpImage: sharpImage.clone().png({ ...options.png as PngOptions, force: true }), + buffer: await sharpImage.clone().png({ ...options.png as PngOptions, force: true }).toBuffer(), }); } @@ -24,7 +24,7 @@ export async function compressSharp( compressed.push({ format: '.webp', resolution: image.resolution, - sharpImage: sharpImage.clone().webp(options.webp as WebpOptions) + buffer: await sharpImage.clone().webp(options.webp as WebpOptions).toBuffer() }); } @@ -33,7 +33,7 @@ export async function compressSharp( compressed.push({ format: '.jpg', resolution: image.resolution, - sharpImage: sharpImage.clone().jpeg(options.jpg as JpegOptions) + buffer: await sharpImage.clone().jpeg(options.jpg as JpegOptions).toBuffer() }); } @@ -42,7 +42,7 @@ export async function compressSharp( compressed.push({ format: '.avif', resolution: image.resolution, - sharpImage: sharpImage.clone().avif(options.avif as AvifOptions) + buffer: await sharpImage.clone().avif(options.avif as AvifOptions).toBuffer() }); } diff --git a/packages/assetpack/src/spine/AtlasView.ts b/packages/assetpack/src/spine/AtlasView.ts index 67d0876..f62ee0f 100644 --- a/packages/assetpack/src/spine/AtlasView.ts +++ b/packages/assetpack/src/spine/AtlasView.ts @@ -10,7 +10,7 @@ export class AtlasView getTextures(): string[] { - const regex = /^.+?(?:\.png|\.jpg|\.jpeg|\.webp|\.avif)$/gm; + const regex = /^.+?(?:\.png|\.jpg|\.jpeg|\.webp|\.avif|\.dds|\.ktx)$/gm; const matches = this.rawAtlas.match(regex); diff --git a/packages/assetpack/src/spine/spineAtlasCompress.ts b/packages/assetpack/src/spine/spineAtlasCompress.ts index 0781256..85b2863 100644 --- a/packages/assetpack/src/spine/spineAtlasCompress.ts +++ b/packages/assetpack/src/spine/spineAtlasCompress.ts @@ -15,6 +15,9 @@ export function spineAtlasCompress(_options?: SpineAtlasCompressOptions): AssetP png: true, webp: true, avif: false, + astc: false, + bc7: false, + basis: false }, ..._options, }, @@ -28,23 +31,24 @@ export function spineAtlasCompress(_options?: SpineAtlasCompressOptions): AssetP }, async transform(asset: Asset, options) { - const formats = []; + const formats: Array<[format: string, extension: string]> = []; - if (options.avif)formats.push('avif'); - if (options.png)formats.push('png'); - if (options.webp)formats.push('webp'); + if (options.avif) formats.push(['avif', '.avif']); + if (options.png) formats.push(['png', '.png']); + if (options.webp) formats.push(['webp', '.webp']); + if (options.astc) formats.push(['astc', '.astc.ktx']); + if (options.bc7) formats.push(['bc7', '.bc7.dds']); + if (options.basis) formats.push(['basis', '.basis.ktx2']); const atlas = new AtlasView(asset.buffer); const textures = atlas.getTextures(); - const assets = formats.map((format) => + const assets = formats.map(([format, extension]) => { - const extension = `.${format}`; - const newAtlas = new AtlasView(asset.buffer); - const newFileName = swapExt(asset.filename, `${extension}.atlas`); + const newFileName = swapExt(asset.filename, `.${format}.atlas`); textures.forEach((texture) => { diff --git a/packages/assetpack/src/texture-packer/texturePackerCompress.ts b/packages/assetpack/src/texture-packer/texturePackerCompress.ts index c63cb36..3d666f5 100644 --- a/packages/assetpack/src/texture-packer/texturePackerCompress.ts +++ b/packages/assetpack/src/texture-packer/texturePackerCompress.ts @@ -16,6 +16,9 @@ export function texturePackerCompress( png: true, webp: true, avif: false, + astc: false, + bc7: false, + basis: false }, ..._options, }, @@ -33,29 +36,30 @@ export function texturePackerCompress( }, async transform(asset: Asset, options) { - const formats = []; + const formats: Array<[format: string, extension: string]> = []; - if (options.avif) formats.push('avif'); - if (options.png) formats.push('png'); - if (options.webp) formats.push('webp'); + if (options.avif) formats.push(['avif', '.avif']); + if (options.png) formats.push(['png', '.png']); + if (options.webp) formats.push(['webp', '.webp']); + if (options.astc) formats.push(['astc', '.astc.ktx']); + if (options.bc7) formats.push(['bc7', '.bc7.dds']); + if (options.basis) formats.push(['basis', '.basis.ktx2']); const json = JSON.parse(asset.buffer.toString()); - const assets = formats.map((format) => + const assets = formats.map(([format, extension]) => { - const extension = `.${format}`; - - const newFileName = swapExt(asset.filename, `${extension}.json`); - - json.meta.image = swapExt(json.meta.image, extension); + const newFileName = swapExt(asset.filename, `.${format}.json`); const newAsset = createNewAssetAt(asset, newFileName); const newJson = JSON.parse(JSON.stringify(json)); + newJson.meta.image = swapExt(newJson.meta.image, extension); + if (newJson.meta.related_multi_packs) { newJson.meta.related_multi_packs = (newJson.meta.related_multi_packs as string[]).map((pack) => - swapExt(pack, `${extension}.json`), + swapExt(pack, `.${format}.json`), ); } diff --git a/packages/assetpack/test/image/Compress.test.ts b/packages/assetpack/test/image/Compress.test.ts index d9c830b..8013634 100644 --- a/packages/assetpack/test/image/Compress.test.ts +++ b/packages/assetpack/test/image/Compress.test.ts @@ -41,6 +41,9 @@ describe('Compress', () => webp: true, avif: true, jpg: true, + astc: true, + basis: true, + bc7: true }), ] }); @@ -50,10 +53,17 @@ describe('Compress', () => expect(existsSync(`${outputDir}/testPng.png`)).toBe(true); expect(existsSync(`${outputDir}/testPng.webp`)).toBe(true); expect(existsSync(`${outputDir}/testPng.avif`)).toBe(true); + expect(existsSync(`${outputDir}/testPng.astc.ktx`)).toBe(true); + expect(existsSync(`${outputDir}/testPng.bc7.dds`)).toBe(true); + expect(existsSync(`${outputDir}/testPng.basis.ktx2`)).toBe(true); + expect(existsSync(`${outputDir}/testJpg.jpg`)).toBe(true); expect(existsSync(`${outputDir}/testJpg.webp`)).toBe(true); expect(existsSync(`${outputDir}/testJpg.avif`)).toBe(true); expect(existsSync(`${outputDir}/testJpg.png`)).toBe(false); + expect(existsSync(`${outputDir}/testJpg.astc.ktx`)).toBe(true); + expect(existsSync(`${outputDir}/testJpg.bc7.dds`)).toBe(true); + expect(existsSync(`${outputDir}/testJpg.basis.ktx2`)).toBe(true); }); it('should compress png with 1 plugin', async () => @@ -88,6 +98,9 @@ describe('Compress', () => webp: true, jpg: true, avif: true, + astc: true, + basis: true, + bc7: true }), ], assetSettings: [{ @@ -105,9 +118,16 @@ describe('Compress', () => expect(existsSync(`${outputDir}/testPng.png`)).toBe(true); expect(existsSync(`${outputDir}/testPng.webp`)).toBe(true); expect(existsSync(`${outputDir}/testPng.avif`)).toBe(true); + expect(existsSync(`${outputDir}/testPng.astc.ktx`)).toBe(true); + expect(existsSync(`${outputDir}/testPng.bc7.dds`)).toBe(true); + expect(existsSync(`${outputDir}/testPng.basis.ktx2`)).toBe(true); + expect(existsSync(`${outputDir}/testJpg.jpg`)).toBe(true); expect(existsSync(`${outputDir}/testJpg.webp`)).toBe(true); expect(existsSync(`${outputDir}/testJpg.avif`)).toBe(true); + expect(existsSync(`${outputDir}/testJpg.astc.ktx`)).toBe(true); + expect(existsSync(`${outputDir}/testJpg.bc7.dds`)).toBe(true); + expect(existsSync(`${outputDir}/testJpg.basis.ktx2`)).toBe(true); expect(existsSync(`${outputDir}/testJpg.png`)).toBe(false); }); @@ -150,9 +170,16 @@ describe('Compress', () => expect(existsSync(`${outputDir}/testPng.png`)).toBe(true); expect(existsSync(`${outputDir}/testPng.webp`)).toBe(false); expect(existsSync(`${outputDir}/testPng.avif`)).toBe(false); + expect(existsSync(`${outputDir}/testPng.astc.ktx`)).toBe(false); + expect(existsSync(`${outputDir}/testPng.bc7.dds`)).toBe(false); + expect(existsSync(`${outputDir}/testPng.basis.ktx2`)).toBe(false); + expect(existsSync(`${outputDir}/testJpg.jpg`)).toBe(true); expect(existsSync(`${outputDir}/testJpg.webp`)).toBe(false); expect(existsSync(`${outputDir}/testJpg.avif`)).toBe(false); expect(existsSync(`${outputDir}/testJpg.png`)).toBe(false); + expect(existsSync(`${outputDir}/testJpg.astc.ktx`)).toBe(false); + expect(existsSync(`${outputDir}/testJpg.bc7.dds`)).toBe(false); + expect(existsSync(`${outputDir}/testJpg.basis.ktx2`)).toBe(false); }); }); diff --git a/packages/assetpack/test/manifest/Manifest.test.ts b/packages/assetpack/test/manifest/Manifest.test.ts index cd29400..31f344e 100644 --- a/packages/assetpack/test/manifest/Manifest.test.ts +++ b/packages/assetpack/test/manifest/Manifest.test.ts @@ -128,8 +128,9 @@ describe('Manifest', () => jpg: true, webp: true, avif: false, + astc: true, }), - texturePackerCompress(), + texturePackerCompress({ astc: true }), pixiManifest(), spineAtlasManifestMod(), ], @@ -166,8 +167,10 @@ describe('Manifest', () => src: [ 'bundle/sprite@0.5x.webp', 'bundle/sprite@0.5x.png', + 'bundle/sprite@0.5x.astc.ktx', 'bundle/sprite.webp', 'bundle/sprite.png', + 'bundle/sprite.astc.ktx' ], data: { tags: { @@ -177,10 +180,14 @@ describe('Manifest', () => }, { alias: ['bundle/tps'], - src: ['bundle/tps-0@0.5x.webp.json', + src: [ + 'bundle/tps-0@0.5x.webp.json', 'bundle/tps-0@0.5x.png.json', + 'bundle/tps-0@0.5x.astc.json', 'bundle/tps-0.webp.json', - 'bundle/tps-0.png.json'], + 'bundle/tps-0.png.json', + 'bundle/tps-0.astc.json' + ], data: { tags: { tps: true, @@ -276,6 +283,7 @@ describe('Manifest', () => jpg: true, webp: true, avif: false, + astc: true, }), pixiManifest(), ], @@ -304,8 +312,10 @@ describe('Manifest', () => src: [ 'defaultFolder/mip/sprite0@0.5x.webp', 'defaultFolder/mip/sprite0@0.5x.png', + 'defaultFolder/mip/sprite0@0.5x.astc.ktx', 'defaultFolder/mip/sprite0.webp', 'defaultFolder/mip/sprite0.png', + 'defaultFolder/mip/sprite0.astc.ktx', ], data: { tags: {}, @@ -316,8 +326,10 @@ describe('Manifest', () => src: [ 'defaultFolder/mip/sprite1@0.5x.webp', 'defaultFolder/mip/sprite1@0.5x.png', + 'defaultFolder/mip/sprite1@0.5x.astc.ktx', 'defaultFolder/mip/sprite1.webp', 'defaultFolder/mip/sprite1.png', + 'defaultFolder/mip/sprite1.astc.ktx', ], data: { tags: {}, @@ -328,8 +340,10 @@ describe('Manifest', () => src: [ 'defaultFolder/mip/sprite2@0.5x.webp', 'defaultFolder/mip/sprite2@0.5x.png', + 'defaultFolder/mip/sprite2@0.5x.astc.ktx', 'defaultFolder/mip/sprite2.webp', 'defaultFolder/mip/sprite2.png', + 'defaultFolder/mip/sprite2.astc.ktx', ], data: { tags: {}, @@ -457,6 +471,7 @@ describe('Manifest', () => compress({ webp: true, png: true, + astc: true, }), pixiManifest({ createShortcuts: true, @@ -488,8 +503,10 @@ describe('Manifest', () => src: [ 'folder/sprite@0.5x.webp', 'folder/sprite@0.5x.png', + 'folder/sprite@0.5x.astc.ktx', 'folder/sprite.webp', 'folder/sprite.png', + 'folder/sprite.astc.ktx', ], }, { @@ -671,6 +688,7 @@ describe('Manifest', () => compress({ webp: true, png: true, + astc: true, }), pixiManifest({ createShortcuts: true, @@ -702,8 +720,10 @@ describe('Manifest', () => src: [ 'folder/sprite@0.5x.webp', 'folder/sprite@0.5x.png', + 'folder/sprite@0.5x.astc.ktx', 'folder/sprite.webp', 'folder/sprite.png', + 'folder/sprite.astc.ktx', ], }, { @@ -817,6 +837,7 @@ describe('Manifest', () => compress({ webp: true, png: true, + astc: true, }), pixiManifest({ createShortcuts: false, @@ -848,8 +869,10 @@ describe('Manifest', () => src: [ 'folder/sprite@0.5x.webp', 'folder/sprite@0.5x.png', + 'folder/sprite@0.5x.astc.ktx', 'folder/sprite.webp', 'folder/sprite.png', + 'folder/sprite.astc.ktx', ], }, { diff --git a/packages/assetpack/test/spine/spineAtlasAll.test.ts b/packages/assetpack/test/spine/spineAtlasAll.test.ts index 89de492..eefc4d5 100644 --- a/packages/assetpack/test/spine/spineAtlasAll.test.ts +++ b/packages/assetpack/test/spine/spineAtlasAll.test.ts @@ -49,6 +49,7 @@ describe('Spine Atlas All', () => png: true, webp: true, jpg: true, + astc: true, }), mipmap({ resolutions: { default: 1, low: 0.5 }, @@ -59,6 +60,7 @@ describe('Spine Atlas All', () => spineAtlasCompress({ png: true, webp: true, + astc: true, }), ] }); @@ -66,6 +68,7 @@ describe('Spine Atlas All', () => await assetpack.run(); const rawAtlasWebpHalf = readFileSync(`${outputDir}/dragon@0.5x.webp.atlas`); + const rawAtlasAstcHalf = readFileSync(`${outputDir}/dragon@0.5x.astc.atlas`); const rawAtlasHalf = readFileSync(`${outputDir}/dragon@0.5x.png.atlas`); expect(rawAtlasHalf.includes('dragon@0.5x.png')).toBeTruthy(); @@ -73,6 +76,9 @@ describe('Spine Atlas All', () => expect(rawAtlasWebpHalf.includes('dragon@0.5x.webp')).toBeTruthy(); expect(rawAtlasWebpHalf.includes('dragon2@0.5x.webp')).toBeTruthy(); + + expect(rawAtlasAstcHalf.includes('dragon@0.5x.astc.ktx')).toBeTruthy(); + expect(rawAtlasAstcHalf.includes('dragon2@0.5x.astc.ktx')).toBeTruthy(); }); it('should correctly create files when Mipmap and CacheBuster are used', async () => @@ -114,6 +120,7 @@ describe('Spine Atlas All', () => png: true, webp: true, jpg: true, + astc: true }), spineAtlasMipmap({ resolutions: { default: 1, low: 0.5 }, @@ -121,6 +128,7 @@ describe('Spine Atlas All', () => spineAtlasCompress({ png: true, webp: true, + astc: true }), cacheBuster(), spineAtlasCacheBuster() @@ -128,19 +136,21 @@ describe('Spine Atlas All', () => }); await assetpack.run(); - const globPath = `${outputDir}/*.{atlas,png,webp}`; + const globPath = `${outputDir}/*.{atlas,png,webp,astc.ktx}`; const files = await glob(globPath); // need two sets of files - expect(files.length).toBe(12); - expect(files.filter((file) => file.endsWith('.atlas')).length).toBe(4); + expect(files.length).toBe(18); + expect(files.filter((file) => file.endsWith('.atlas')).length).toBe(6); expect(files.filter((file) => file.endsWith('.png')).length).toBe(4); expect(files.filter((file) => file.endsWith('.webp')).length).toBe(4); + expect(files.filter((file) => file.endsWith('.astc.ktx')).length).toBe(4); expect(files.filter((file) => file.endsWith('.jpg')).length).toBe(0); const atlasFiles = files.filter((file) => file.endsWith('.atlas')); const pngFiles = files.filter((file) => file.endsWith('.png')); const webpFiles = files.filter((file) => file.endsWith('.webp')); + const astcFiles = files.filter((file) => file.endsWith('.astc.ktx')); // check that the files are correct atlasFiles.forEach((atlasFile) => @@ -149,6 +159,7 @@ describe('Spine Atlas All', () => const isHalfSize = atlasFile.includes('@0.5x'); const isWebp = atlasFile.includes('.webp'); const isPng = atlasFile.includes('.png'); + const isAstc = atlasFile.includes('.astc'); const checkFiles = (fileList: string[], isHalfSize: boolean, isFileType: boolean) => { @@ -157,7 +168,7 @@ describe('Spine Atlas All', () => // remove the outputDir file = file.replace(`${outputDir}/`, ''); const isFileHalfSize = file.includes('@0.5x'); - const isFileFileType = file.includes(isWebp ? '.webp' : '.png'); + const isFileFileType = file.includes(isWebp ? '.webp' : isAstc ? '.astc' : '.png'); const shouldExist = isHalfSize === isFileHalfSize && isFileType === isFileFileType; expect(rawAtlas.includes(file)).toBe(shouldExist); @@ -174,6 +185,10 @@ describe('Spine Atlas All', () => { checkFiles(pngFiles, true, true); } + else if (isAstc) + { + checkFiles(astcFiles, true, true); + } } else if (isWebp) @@ -184,6 +199,10 @@ describe('Spine Atlas All', () => { checkFiles(pngFiles, false, true); } + else if (isAstc) + { + checkFiles(astcFiles, false, true); + } }); }); }); diff --git a/packages/assetpack/test/spine/spineAtlasCompress.test.ts b/packages/assetpack/test/spine/spineAtlasCompress.test.ts index cecfa68..5c379dc 100644 --- a/packages/assetpack/test/spine/spineAtlasCompress.test.ts +++ b/packages/assetpack/test/spine/spineAtlasCompress.test.ts @@ -45,16 +45,19 @@ describe('Spine Atlas Compress', () => png: true, jpg: true, webp: true, + astc: true }), spineAtlasCompress({ png: true, webp: true, + astc: true }), ] }); await assetpack.run(); + const rawAtlasAstc = readFileSync(`${outputDir}/dragon.astc.atlas`); const rawAtlasWebp = readFileSync(`${outputDir}/dragon.webp.atlas`); const rawAtlas = readFileSync(`${outputDir}/dragon.png.atlas`); @@ -63,5 +66,8 @@ describe('Spine Atlas Compress', () => expect(rawAtlasWebp.includes('dragon.webp')).toBeTruthy(); expect(rawAtlasWebp.includes('dragon2.webp')).toBeTruthy(); + + expect(rawAtlasAstc.includes('dragon.astc.ktx')).toBeTruthy(); + expect(rawAtlasAstc.includes('dragon2.astc.ktx')).toBeTruthy(); }); }); diff --git a/packages/assetpack/test/texture-packer/texturePackerAll.test.ts b/packages/assetpack/test/texture-packer/texturePackerAll.test.ts index 44b31ef..3d39700 100644 --- a/packages/assetpack/test/texture-packer/texturePackerAll.test.ts +++ b/packages/assetpack/test/texture-packer/texturePackerAll.test.ts @@ -39,10 +39,12 @@ describe('Texture Packer All', () => png: true, jpg: true, webp: true, + astc: true }), texturePackerCompress({ png: true, webp: true, + astc: true }), cacheBuster(), texturePackerCacheBuster() @@ -51,19 +53,21 @@ describe('Texture Packer All', () => await assetpack.run(); - const globPath = `${outputDir}/*.{json,png,webp}`; + const globPath = `${outputDir}/*.{json,png,webp,astc.ktx}`; const files = await glob(globPath); // need two sets of files - expect(files.length).toBe(8); - expect(files.filter((file) => file.endsWith('.json')).length).toBe(4); + expect(files.length).toBe(12); + expect(files.filter((file) => file.endsWith('.json')).length).toBe(6); expect(files.filter((file) => file.endsWith('.png')).length).toBe(2); expect(files.filter((file) => file.endsWith('.webp')).length).toBe(2); + expect(files.filter((file) => file.endsWith('.astc.ktx')).length).toBe(2); expect(files.filter((file) => file.endsWith('.jpg')).length).toBe(0); const jsonFiles = files.filter((file) => file.endsWith('.json')); const pngFiles = files.filter((file) => file.endsWith('.png')); const webpFiles = files.filter((file) => file.endsWith('.webp')); + const astcFiles = files.filter((file) => file.endsWith('.astc.ktx')); // check that the files are correct jsonFiles.forEach((jsonFile) => @@ -72,6 +76,7 @@ describe('Texture Packer All', () => const isHalfSize = jsonFile.includes('@0.5x'); const isWebp = jsonFile.includes('.webp'); const isPng = jsonFile.includes('.png'); + const isAstc = jsonFile.includes('.astc'); const checkFiles = (fileList: string[], isHalfSize: boolean, isFileType: boolean) => { @@ -80,7 +85,7 @@ describe('Texture Packer All', () => // remove the outputDir file = file.replace(`${outputDir}/`, ''); const isFileHalfSize = file.includes('@0.5x'); - const isFileFileType = file.includes(isWebp ? '.webp' : '.png'); + const isFileFileType = file.includes(isWebp ? '.webp' : isAstc ? '.astc.ktx' : '.png'); const shouldExist = isHalfSize === isFileHalfSize && isFileType === isFileFileType; shouldExist ? expect(rawJson.meta.image).toEqual(file) : expect(rawJson.meta.image).not.toEqual(file); @@ -97,6 +102,10 @@ describe('Texture Packer All', () => { checkFiles(pngFiles, true, true); } + else if (isAstc) + { + checkFiles(astcFiles, true, true); + } } else if (isWebp) @@ -107,6 +116,10 @@ describe('Texture Packer All', () => { checkFiles(pngFiles, false, true); } + else if (isAstc) + { + checkFiles(astcFiles, false, true); + } }); }); }); diff --git a/packages/assetpack/test/texture-packer/texturePackerCompress.test.ts b/packages/assetpack/test/texture-packer/texturePackerCompress.test.ts index 23784b6..d9e867f 100644 --- a/packages/assetpack/test/texture-packer/texturePackerCompress.test.ts +++ b/packages/assetpack/test/texture-packer/texturePackerCompress.test.ts @@ -22,6 +22,8 @@ describe('Texture Packer Compression', () => png: true, jpg: true, webp: true, + astc: true, + basis: true }; const assetpack = new AssetPack({ @@ -43,8 +45,12 @@ describe('Texture Packer Compression', () => const sheetPng = fs.readJSONSync(`${outputDir}/sprites.png.json`); const sheetWebp = fs.readJSONSync(`${outputDir}/sprites.webp.json`); + const sheetAstc = fs.readJSONSync(`${outputDir}/sprites.astc.json`); + const sheetBasis = fs.readJSONSync(`${outputDir}/sprites.basis.json`); expect(sheetPng.meta.image).toEqual(`sprites.png`); expect(sheetWebp.meta.image).toEqual(`sprites.webp`); + expect(sheetAstc.meta.image).toEqual(`sprites.astc.ktx`); + expect(sheetBasis.meta.image).toEqual(`sprites.basis.ktx2`); }); }); diff --git a/packages/assetpack/test/texture-packer/texturePackerManifest.test.ts b/packages/assetpack/test/texture-packer/texturePackerManifest.test.ts index f7c626e..7245c96 100644 --- a/packages/assetpack/test/texture-packer/texturePackerManifest.test.ts +++ b/packages/assetpack/test/texture-packer/texturePackerManifest.test.ts @@ -61,9 +61,14 @@ describe('Texture Packer Compression', () => const outputDir = getOutputDir(pkg, testName); createTPSFolder(testName, pkg); + const compressOpt = { + astc: true, + basis: true + }; const assetpack = new AssetPack({ - entry: inputDir, cacheLocation: getCacheDir(pkg, testName), + entry: inputDir, + cacheLocation: getCacheDir(pkg, testName), output: outputDir, cache: false, pipes: [ @@ -73,8 +78,8 @@ describe('Texture Packer Compression', () => maximumTextureSize: 512, }, }), - compress(), - texturePackerCompress(), + compress(compressOpt), + texturePackerCompress(compressOpt), pixiManifest(), ] }); @@ -91,8 +96,12 @@ describe('Texture Packer Compression', () => src: [ 'sprites-0@0.5x.webp.json', 'sprites-0@0.5x.png.json', + 'sprites-0@0.5x.basis.json', + 'sprites-0@0.5x.astc.json', 'sprites-0.webp.json', 'sprites-0.png.json', + 'sprites-0.basis.json', + 'sprites-0.astc.json', ], data: { tags: { diff --git a/packages/assetpack/vitest.config.js b/packages/assetpack/vitest.config.js index 9529413..558d2fc 100644 --- a/packages/assetpack/vitest.config.js +++ b/packages/assetpack/vitest.config.js @@ -2,6 +2,14 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { + poolOptions: { + threads: { + // Due to the cpu-features and vitest threads incompatibility, see: + // https://github.com/vitest-dev/vitest/issues/1982 + // https://github.com/vitest-dev/vitest/issues/740 + singleThread: true + }, + }, // ... }, }); From 27b1dde60b6b685fdfa6f48c3ba2ab0e6fcbd6e7 Mon Sep 17 00:00:00 2001 From: ddenysiuk Date: Wed, 24 Jul 2024 20:53:17 +0200 Subject: [PATCH 2/4] BC7 disabled in tests due to the absence of libomp on the GitHub Actions runner: "error while loading shared libraries: libomp.so.5: cannot open shared object file: No such file or directory" --- packages/assetpack/test/image/Compress.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/assetpack/test/image/Compress.test.ts b/packages/assetpack/test/image/Compress.test.ts index 8013634..dc10805 100644 --- a/packages/assetpack/test/image/Compress.test.ts +++ b/packages/assetpack/test/image/Compress.test.ts @@ -43,7 +43,7 @@ describe('Compress', () => jpg: true, astc: true, basis: true, - bc7: true + bc7: false // Disabled due to the absence of libomp on the GitHub Actions runner: "error while loading shared libraries: libomp.so.5: cannot open shared object file: No such file or directory" }), ] }); @@ -54,16 +54,16 @@ describe('Compress', () => expect(existsSync(`${outputDir}/testPng.webp`)).toBe(true); expect(existsSync(`${outputDir}/testPng.avif`)).toBe(true); expect(existsSync(`${outputDir}/testPng.astc.ktx`)).toBe(true); - expect(existsSync(`${outputDir}/testPng.bc7.dds`)).toBe(true); expect(existsSync(`${outputDir}/testPng.basis.ktx2`)).toBe(true); + expect(existsSync(`${outputDir}/testPng.bc7.dds`)).toBe(false); expect(existsSync(`${outputDir}/testJpg.jpg`)).toBe(true); expect(existsSync(`${outputDir}/testJpg.webp`)).toBe(true); expect(existsSync(`${outputDir}/testJpg.avif`)).toBe(true); expect(existsSync(`${outputDir}/testJpg.png`)).toBe(false); expect(existsSync(`${outputDir}/testJpg.astc.ktx`)).toBe(true); - expect(existsSync(`${outputDir}/testJpg.bc7.dds`)).toBe(true); expect(existsSync(`${outputDir}/testJpg.basis.ktx2`)).toBe(true); + expect(existsSync(`${outputDir}/testJpg.bc7.dds`)).toBe(false); }); it('should compress png with 1 plugin', async () => @@ -100,7 +100,7 @@ describe('Compress', () => avif: true, astc: true, basis: true, - bc7: true + bc7: false }), ], assetSettings: [{ @@ -119,15 +119,15 @@ describe('Compress', () => expect(existsSync(`${outputDir}/testPng.webp`)).toBe(true); expect(existsSync(`${outputDir}/testPng.avif`)).toBe(true); expect(existsSync(`${outputDir}/testPng.astc.ktx`)).toBe(true); - expect(existsSync(`${outputDir}/testPng.bc7.dds`)).toBe(true); expect(existsSync(`${outputDir}/testPng.basis.ktx2`)).toBe(true); + expect(existsSync(`${outputDir}/testPng.bc7.dds`)).toBe(false); expect(existsSync(`${outputDir}/testJpg.jpg`)).toBe(true); expect(existsSync(`${outputDir}/testJpg.webp`)).toBe(true); expect(existsSync(`${outputDir}/testJpg.avif`)).toBe(true); expect(existsSync(`${outputDir}/testJpg.astc.ktx`)).toBe(true); - expect(existsSync(`${outputDir}/testJpg.bc7.dds`)).toBe(true); expect(existsSync(`${outputDir}/testJpg.basis.ktx2`)).toBe(true); + expect(existsSync(`${outputDir}/testJpg.bc7.dds`)).toBe(false); expect(existsSync(`${outputDir}/testJpg.png`)).toBe(false); }); From 63e8a63cd220890445833c1aee805e9cbca58af6 Mon Sep 17 00:00:00 2001 From: ddenysiuk Date: Wed, 24 Jul 2024 20:58:26 +0200 Subject: [PATCH 3/4] lint fix --- packages/assetpack/test/image/Compress.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/assetpack/test/image/Compress.test.ts b/packages/assetpack/test/image/Compress.test.ts index dc10805..54b78ef 100644 --- a/packages/assetpack/test/image/Compress.test.ts +++ b/packages/assetpack/test/image/Compress.test.ts @@ -43,7 +43,7 @@ describe('Compress', () => jpg: true, astc: true, basis: true, - bc7: false // Disabled due to the absence of libomp on the GitHub Actions runner: "error while loading shared libraries: libomp.so.5: cannot open shared object file: No such file or directory" + bc7: false // Disabled due to the absence of libomp on the GitHub Actions runner: "error while loading shared libraries: libomp.so.5" }), ] }); From 3335a8eca4608d876e3f9d47a56de3c3e103db2d Mon Sep 17 00:00:00 2001 From: Zyie <24736175+Zyie@users.noreply.github.com> Date: Tue, 3 Sep 2024 09:54:00 +0100 Subject: [PATCH 4/4] update --- .../src/image/utils/compressGpuTextures.ts | 15 +++++++++---- .../src/image/utils/compressSharp.ts | 18 +++++++++------- packages/docs/docs/guide/pipes/compress.mdx | 21 +++++++++++++------ packages/docs/docs/guide/pipes/spine.mdx | 4 ++++ .../docs/docs/guide/pipes/texture-packer.mdx | 4 ++++ 5 files changed, 45 insertions(+), 17 deletions(-) diff --git a/packages/assetpack/src/image/utils/compressGpuTextures.ts b/packages/assetpack/src/image/utils/compressGpuTextures.ts index ca0a7c7..7255907 100644 --- a/packages/assetpack/src/image/utils/compressGpuTextures.ts +++ b/packages/assetpack/src/image/utils/compressGpuTextures.ts @@ -21,7 +21,7 @@ export async function compressGpuTextures( options: CompressOptions, ): Promise { - const compressed: CompressImageDataResult[] = []; + let compressed: CompressImageDataResult[] = []; if (!options.astc && !options.bc7 && !options.basis) { @@ -48,7 +48,7 @@ export async function compressGpuTextures( compressed.push({ format: '.astc.ktx', resolution: 1, - buffer: await fs.readFile(await astc(imagePath, astcOpts.blocksize, astcOpts.quality, astcOpts.colorProfile, astcOpts.options)), + image: astc(imagePath, astcOpts.blocksize, astcOpts.quality, astcOpts.colorProfile, astcOpts.options), }); } @@ -59,7 +59,7 @@ export async function compressGpuTextures( compressed.push({ format: '.bc7.dds', resolution: 1, - buffer: await fs.readFile(await bc(imagePath, 'BC7', true, bc7Opts.options)), + image: bc(imagePath, 'BC7', true, bc7Opts.options), }); } @@ -71,9 +71,16 @@ export async function compressGpuTextures( compressed.push({ format: '.basis.ktx2', resolution: 1, - buffer: await fs.readFile(await basis(adjustedImagePath, 'UASTC', true, basisOpts.options)), + image: basis(adjustedImagePath, 'UASTC', true, basisOpts.options), }); } + + const results = await Promise.all(compressed.map(async (result) => ({ + ...result, + buffer: await fs.readFile(await result.image) + }))); + + compressed = results; } finally { diff --git a/packages/assetpack/src/image/utils/compressSharp.ts b/packages/assetpack/src/image/utils/compressSharp.ts index 6be5442..1d3043e 100644 --- a/packages/assetpack/src/image/utils/compressSharp.ts +++ b/packages/assetpack/src/image/utils/compressSharp.ts @@ -6,8 +6,7 @@ export async function compressSharp( options: CompressOptions ): Promise { - const compressed: CompressImageDataResult[] = []; - + const compressed: CompressImageData[] = []; const sharpImage = image.sharpImage; if (image.format === '.png' && options.png) @@ -15,7 +14,7 @@ export async function compressSharp( compressed.push({ format: '.png', resolution: image.resolution, - buffer: await sharpImage.clone().png({ ...options.png as PngOptions, force: true }).toBuffer(), + sharpImage: sharpImage.clone().png({ ...options.png as PngOptions, force: true }), }); } @@ -24,7 +23,7 @@ export async function compressSharp( compressed.push({ format: '.webp', resolution: image.resolution, - buffer: await sharpImage.clone().webp(options.webp as WebpOptions).toBuffer() + sharpImage: sharpImage.clone().webp(options.webp as WebpOptions) }); } @@ -33,7 +32,7 @@ export async function compressSharp( compressed.push({ format: '.jpg', resolution: image.resolution, - buffer: await sharpImage.clone().jpeg(options.jpg as JpegOptions).toBuffer() + sharpImage: sharpImage.clone().jpeg(options.jpg as JpegOptions) }); } @@ -42,9 +41,14 @@ export async function compressSharp( compressed.push({ format: '.avif', resolution: image.resolution, - buffer: await sharpImage.clone().avif(options.avif as AvifOptions).toBuffer() + sharpImage: sharpImage.clone().avif(options.avif as AvifOptions) }); } - return compressed; + const results = await Promise.all(compressed.map(async (result) => ({ + ...result, + buffer: await result.sharpImage.toBuffer() + }))); + + return results; } diff --git a/packages/docs/docs/guide/pipes/compress.mdx b/packages/docs/docs/guide/pipes/compress.mdx index 9211bbb..1d91eb2 100644 --- a/packages/docs/docs/guide/pipes/compress.mdx +++ b/packages/docs/docs/guide/pipes/compress.mdx @@ -1,11 +1,13 @@ --- sidebar_position: 4 --- + import { ImageToggle } from '@site/src/components/ImageToggle'; # Compression The compress plugin uses the Sharp library to compress images into different formats, such as JPEG, PNG, WebP, and AVIF. This helps reduce file sizes while maintaining image quality, ensuring faster load times and better performance. +This plugin also supports compressing images using the ASTC, ETC, ETC2, BCn (DXTn) and Basis supercompressed (ETC1S, UASTC) texture compression standard. ## Example @@ -24,6 +26,9 @@ export default { png: { quality: 90 }, webp: { quality: 80, alphaQuality: 80, }, avif: false, + bc7: false, + astc: false, + basis: false, }), ] }; @@ -31,12 +36,15 @@ export default { ## API -| Option | Type | Description | -| ------ | ----------------- | -------------------------------------------------------------------------------------------------------------------- | -| jpg | `object \| false` | Any settings supported by [sharp jpeg](https://sharp.pixelplumbing.com/api-output#jpeg). | -| png | `object \| false` | Any settings supported by [sharp png](https://sharp.pixelplumbing.com/api-output#png). | -| webp | `object \| false` | Any settings supported by [sharp webp](https://sharp.pixelplumbing.com/api-output#webp). | -| avif | `object \| false` | Any settings supported by [sharp avif](https://sharp.pixelplumbing.com/api-output#avif).
Defaults to `false`. | +| Option | Type | Description | +| ------ | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | +| jpg | `object \| false` | Any settings supported by [sharp jpeg](https://sharp.pixelplumbing.com/api-output#jpeg). | +| png | `object \| false` | Any settings supported by [sharp png](https://sharp.pixelplumbing.com/api-output#png). | +| webp | `object \| false` | Any settings supported by [sharp webp](https://sharp.pixelplumbing.com/api-output#webp). | +| avif | `object \| false` | Any settings supported by [sharp avif](https://sharp.pixelplumbing.com/api-output#avif).
Defaults to `false`. | +| bc7 | `object \| false` | Any settings supported by [bc7](https://github.com/ddenisyuk/gpu-tex-enc/blob/main/packages/%40gpu-tex-enc/bc/README.md).
Defaults to `false`. | +| astc | `object \| false` | Any settings supported by [astc](https://github.com/ddenisyuk/gpu-tex-enc/blob/main/packages/%40gpu-tex-enc/astc/README.md).
Defaults to `false`. | +| basis | `object \| false` | Any settings supported by [basis](https://github.com/ddenisyuk/gpu-tex-enc/blob/main/packages/%40gpu-tex-enc/basis/README.md).
Defaults to `false`. | ## Tags @@ -45,4 +53,5 @@ export default { | `nc` | `both` | If present the image(s) will not be compressed. | ### Example + diff --git a/packages/docs/docs/guide/pipes/spine.mdx b/packages/docs/docs/guide/pipes/spine.mdx index 6d406c2..3885138 100644 --- a/packages/docs/docs/guide/pipes/spine.mdx +++ b/packages/docs/docs/guide/pipes/spine.mdx @@ -10,6 +10,7 @@ AssetPack provides several plugins for transforming spine files that utilise `at ## Spine Atlas Compress The `spineAtlasCompress` plugin uses the Sharp library to compress images into different formats, such as JPEG, PNG, WebP, and AVIF. This helps reduce file sizes while maintaining image quality, ensuring faster load times and better performance. +This plugin also supports compressing images using the ASTC, ETC, ETC2, BCn (DXTn) and Basis supercompressed (ETC1S, UASTC) texture compression standard. This plugin should be used in conjunction with the [compress](/docs/guide/pipes/compress) plugin to ensure that the atlas file is compressed as well as the images. @@ -27,6 +28,9 @@ const options = { png: { quality: 90 }, webp: { quality: 80, alphaQuality: 80, }, avif: false, + bc7: false, + astc: false, + basis: false, }; export default { diff --git a/packages/docs/docs/guide/pipes/texture-packer.mdx b/packages/docs/docs/guide/pipes/texture-packer.mdx index d256bdc..dfb5551 100644 --- a/packages/docs/docs/guide/pipes/texture-packer.mdx +++ b/packages/docs/docs/guide/pipes/texture-packer.mdx @@ -81,6 +81,7 @@ export default { ## Texture Packer Compress To compress the texture atlases you can use the `texturePackerCompress` plugin. This plugin uses the Sharp library to compress images into different formats, such as JPEG, PNG, WebP, and AVIF. This helps reduce file sizes while maintaining image quality, ensuring faster load times and better performance. +This plugin also supports compressing images using the ASTC, ETC, ETC2, BCn (DXTn) and Basis supercompressed (ETC1S, UASTC) texture compression standard. ### Example @@ -96,6 +97,9 @@ const options = { png: { quality: 90 }, webp: { quality: 80, alphaQuality: 80, }, avif: false, + bc7: false, + astc: false, + basis: false, }; export default {