Skip to content

Commit

Permalink
Update CLI and unit tests for sharp compression.
Browse files Browse the repository at this point in the history
  • Loading branch information
donmccurdy committed Jan 8, 2023
1 parent 5d3c744 commit 2b836fe
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 203 deletions.
18 changes: 9 additions & 9 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1038,13 +1038,13 @@ program
const formats = micromatch.makeRe(String(options.formats), MICROMATCH_OPTIONS);
const slots = micromatch.makeRe(String(options.slots), MICROMATCH_OPTIONS);
return Session.create(io, logger, args.input, args.output)
.transform(textureCompress({codec: 'webp', encoder: sharp, formats, slots}));
.transform(textureCompress({targetFormat: 'webp', encoder: sharp, formats, slots}));
});

// OXIPNG
// PNG
program
.command('oxipng', 'OxiPNG texture compression')
.help(TEXTURE_COMPRESS_SUMMARY.replace(/{VARIANT}/g, 'OxiPNG'))
.command('png', 'PNG texture compression')
.help(TEXTURE_COMPRESS_SUMMARY.replace(/{VARIANT}/g, 'PNG'))
.argument('<input>', INPUT_DESC)
.argument('<output>', OUTPUT_DESC)
.option(
Expand All @@ -1061,13 +1061,13 @@ program
const formats = micromatch.makeRe(String(options.formats), MICROMATCH_OPTIONS);
const slots = micromatch.makeRe(String(options.slots), MICROMATCH_OPTIONS);
return Session.create(io, logger, args.input, args.output)
.transform(textureCompress({codec: 'png', encoder: sharp, formats, slots}));
.transform(textureCompress({targetFormat: 'png', encoder: sharp, formats, slots}));
});

// MOZJPEG
// JPEG
program
.command('mozjpeg', 'MozJPEG texture compression')
.help(TEXTURE_COMPRESS_SUMMARY.replace(/{VARIANT}/g, 'MozJPEG'))
.command('jpeg', 'JPEG texture compression')
.help(TEXTURE_COMPRESS_SUMMARY.replace(/{VARIANT}/g, 'JPEG'))
.argument('<input>', INPUT_DESC)
.argument('<output>', OUTPUT_DESC)
.option(
Expand All @@ -1084,7 +1084,7 @@ program
const formats = micromatch.makeRe(String(options.formats), MICROMATCH_OPTIONS);
const slots = micromatch.makeRe(String(options.slots), MICROMATCH_OPTIONS);
return Session.create(io, logger, args.input, args.output)
.transform(textureCompress({codec: 'jpeg', encoder: sharp, formats, slots}));
.transform(textureCompress({targetFormat: 'jpeg', encoder: sharp, formats, slots}));
});

program.command('', '\n\n⏯ ANIMATION ────────────────────────────────────────');
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/properties/texture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ export class Texture extends ExtensibleProperty<ITexture> {
*/
public setURI(uri: string): this {
this.set('uri', uri);
this.set('mimeType', ImageUtils.extensionToMimeType(FileUtils.extension(uri)));
const mimeType = ImageUtils.extensionToMimeType(FileUtils.extension(uri));
if (mimeType) this.set('mimeType', mimeType);
return this;
}

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/utils/image-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export class ImageUtils {
/** 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}`;
}
}
Expand Down
66 changes: 47 additions & 19 deletions packages/functions/src/texture-compress.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,33 @@
import { BufferUtils, Document, ImageUtils, TextureChannel, Transform } from '@gltf-transform/core';
import { BufferUtils, Document, ImageUtils, Texture, TextureChannel, Transform } from '@gltf-transform/core';
import { TextureWebP } from '@gltf-transform/extensions';
import { getTextureChannelMask } from './list-texture-channels';
import { listTextureSlots } from './list-texture-slots';
import type sharp from 'sharp';
import { formatBytes } from './utils';

const CODECS = ['jpeg', 'png', 'webp'] as const;
const NAME = 'textureCompress';

type Format = typeof FORMATS[number];
const FORMATS = ['jpeg', 'png', 'webp'] as const;
const SUPPORTED_MIME_TYPES = ['image/jpeg', 'image/png', 'image/webp'];

export interface TextureCompressOptions {
/** Instance of the Sharp encoder, which must be installed from the
* 'sharp' package and provided by the caller.
*/
encoder: unknown;
codec?: typeof CODECS[number];
/**
* Target image format. If specified, included textures in other formats
* will be converted. Default: original format.
*/
targetFormat?: Format;
/** Pattern matching the format(s) to be compressed or converted. Default: /.*\/. */
formats?: RegExp;
/** Pattern matching the material texture slot(s) to be compressed or converted. Default: /.*\/. */
slots?: RegExp;
}

export const TEXTURE_COMPRESS_DEFAULTS: Required<Omit<Omit<TextureCompressOptions, 'encoder'>, 'codec'>> = {
export const TEXTURE_COMPRESS_DEFAULTS: Required<Omit<Omit<TextureCompressOptions, 'encoder'>, 'targetFormat'>> = {
formats: /.*/,
slots: /.*/,
};
Expand Down Expand Up @@ -49,10 +61,10 @@ export const TEXTURE_COMPRESS_DEFAULTS: Required<Omit<Omit<TextureCompressOption
export const textureCompress = function (_options: TextureCompressOptions): Transform {
const options = { ...TEXTURE_COMPRESS_DEFAULTS, ..._options } as Required<TextureCompressOptions>;
const encoder = options.encoder as typeof sharp | null;
const codec = options.codec;
const targetFormat = options.targetFormat as Format | undefined;

if (!encoder) {
throw new Error(`${codec}: encoder dependency required — install "sharp".`);
throw new Error(`${targetFormat}: encoder dependency required — install "sharp".`);
}

return async (document: Document): Promise<void> => {
Expand All @@ -67,7 +79,7 @@ export const textureCompress = function (_options: TextureCompressOptions): Tran
texture.getURI() ||
texture.getName() ||
`${textureIndex + 1}/${document.getRoot().listTextures().length}`;
const prefix = `${codec}:texture(${textureLabel})`;
const prefix = `${NAME}(${textureLabel})`;

// FILTER: Exclude textures that don't match (a) 'slots' or (b) expected formats.

Expand All @@ -80,36 +92,43 @@ export const textureCompress = function (_options: TextureCompressOptions): Tran
} else if (slots.length && !slots.some((slot) => options.slots.test(slot))) {
logger.debug(`${prefix}: Skipping, [${slots.join(', ')}] excluded by "slots" parameter.`);
return;
} else if (options.codec === 'jpeg' && channels & TextureChannel.A) {
} else if (options.targetFormat === 'jpeg' && channels & TextureChannel.A) {
logger.warn(`${prefix}: Skipping, [${slots.join(', ')}] requires alpha channel.`);
return;
}

logger.debug(`${prefix}: Slots → [${slots.join(', ')}]`);
const srcFormat = getFormat(texture);
const dstFormat = targetFormat || srcFormat;
const srcMimeType = texture.getMimeType();
const dstMimeType = `image/${dstFormat}`;

logger.debug(`${prefix}: Format = ${srcFormat}${dstFormat}`);
logger.debug(`${prefix}: Slots = [${slots.join(', ')}]`);

// COMPRESS: Run compression library.

const srcImage = texture.getImage()!;
const srcByteLength = srcImage.byteLength;

const instance = encoder(srcImage);
if (codec) instance.toFormat(codec);

const dstImage = BufferUtils.toView(await instance.toBuffer());
const dstByteLength = dstImage.byteLength;
// Convert if target and source formats differ.
if (srcMimeType !== dstMimeType) {
instance.toFormat(dstFormat);
}

const dstImage = BufferUtils.toView(await instance.toBuffer());
texture.setImage(dstImage);

const srcMimeType = texture.getMimeType();
const dstMimeType = `image/${codec}`;
// Update path and MIME type if target and source formats differ.
if (srcMimeType !== dstMimeType) {
const srcExtension = ImageUtils.mimeTypeToExtension(srcMimeType);
const dstExtension = ImageUtils.mimeTypeToExtension(dstMimeType);
const dstURI = texture.getURI().replace(new RegExp(`\\.${srcExtension}$`), `.${dstExtension}`);
texture.setMimeType(dstExtension).setURI(dstURI);
texture.setMimeType(dstMimeType).setURI(dstURI);
}

logger.debug(`${prefix}: ${formatBytes(srcByteLength)}${formatBytes(dstByteLength)}`);
const srcByteLength = srcImage.byteLength;
const dstByteLength = dstImage.byteLength;
logger.debug(`${prefix}: Size = ${formatBytes(srcByteLength)}${formatBytes(dstByteLength)}`);
})
);

Expand All @@ -118,6 +137,15 @@ export const textureCompress = function (_options: TextureCompressOptions): Tran
document.createExtension(TextureWebP).setRequired(true);
}

logger.debug(`${codec}: Complete.`);
logger.debug(`${NAME}: Complete.`);
};
};

function getFormat(texture: Texture): Format {
const mimeType = texture.getMimeType();
const format = mimeType.split('/').pop() as Format | undefined;
if (!format || !FORMATS.includes(format)) {
throw new Error(`Unknown MIME type "${mimeType}".`);
}
return format;
}
174 changes: 0 additions & 174 deletions packages/functions/test/squoosh.test.ts

This file was deleted.

Loading

0 comments on commit 2b836fe

Please sign in to comment.