mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-13 01:07:18 +00:00
Adding native encoders (#71)
* Adding browser png * Adding native JPEG & file size output * Removing log * Fixing blob typing * Fix timing issue
This commit is contained in:
14618
package-lock.json
generated
Normal file
14618
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -62,9 +62,11 @@
|
|||||||
"webpack-plugin-replace": "^1.1.1"
|
"webpack-plugin-replace": "^1.1.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/filesize": "^3.6.0",
|
||||||
"classnames": "^2.2.5",
|
"classnames": "^2.2.5",
|
||||||
"comlink": "^3.0.3",
|
"comlink": "^3.0.3",
|
||||||
"comlink-loader": "^1.0.0",
|
"comlink-loader": "^1.0.0",
|
||||||
|
"filesize": "^3.6.1",
|
||||||
"material-components-web": "^0.32.0",
|
"material-components-web": "^0.32.0",
|
||||||
"preact": "^8.2.7",
|
"preact": "^8.2.7",
|
||||||
"preact-i18n": "^1.2.0",
|
"preact-i18n": "^1.2.0",
|
||||||
|
|||||||
14
src/codecs/browser-jpeg/encoder.ts
Normal file
14
src/codecs/browser-jpeg/encoder.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { canvasEncode } from '../../lib/util';
|
||||||
|
|
||||||
|
export interface EncodeOptions { quality: number; }
|
||||||
|
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||||
|
|
||||||
|
export const type = 'browser-jpeg';
|
||||||
|
export const label = 'Browser JPEG';
|
||||||
|
export const mimeType = 'image/jpeg';
|
||||||
|
export const extension = 'jpg';
|
||||||
|
export const defaultOptions: EncodeOptions = { quality: 0.5 };
|
||||||
|
|
||||||
|
export function encode(data: ImageData, { quality }: EncodeOptions) {
|
||||||
|
return canvasEncode(data, mimeType, quality);
|
||||||
|
}
|
||||||
3
src/codecs/browser-jpeg/options.ts
Normal file
3
src/codecs/browser-jpeg/options.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import qualityOption from '../generic/quality-option';
|
||||||
|
|
||||||
|
export default qualityOption({ min: 0, max: 1, step: 0 });
|
||||||
14
src/codecs/browser-png/encoder.tsx
Normal file
14
src/codecs/browser-png/encoder.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { canvasEncode } from '../../lib/util';
|
||||||
|
|
||||||
|
export interface EncodeOptions {}
|
||||||
|
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||||
|
|
||||||
|
export const type = 'browser-png';
|
||||||
|
export const label = 'Browser PNG';
|
||||||
|
export const mimeType = 'image/png';
|
||||||
|
export const extension = 'png';
|
||||||
|
export const defaultOptions: EncodeOptions = {};
|
||||||
|
|
||||||
|
export function encode(data: ImageData, options: EncodeOptions) {
|
||||||
|
return canvasEncode(data, mimeType);
|
||||||
|
}
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
import * as mozJPEG from './mozjpeg/encoder';
|
import * as mozJPEG from './mozjpeg/encoder';
|
||||||
import * as identity from './identity/encoder';
|
import * as identity from './identity/encoder';
|
||||||
|
import * as browserPNG from './browser-png/encoder';
|
||||||
|
import * as browserJPEG from './browser-jpeg/encoder';
|
||||||
|
|
||||||
export type EncoderState = identity.EncoderState | mozJPEG.EncoderState;
|
export type EncoderState =
|
||||||
export type EncoderOptions = identity.EncodeOptions | mozJPEG.EncodeOptions;
|
identity.EncoderState | mozJPEG.EncoderState | browserPNG.EncoderState | browserJPEG.EncoderState;
|
||||||
|
export type EncoderOptions =
|
||||||
|
identity.EncodeOptions | mozJPEG.EncodeOptions | browserPNG.EncodeOptions |
|
||||||
|
browserJPEG.EncodeOptions;
|
||||||
export type EncoderType = keyof typeof encoderMap;
|
export type EncoderType = keyof typeof encoderMap;
|
||||||
|
|
||||||
export const encoderMap = {
|
export const encoderMap = {
|
||||||
[identity.type]: identity,
|
[identity.type]: identity,
|
||||||
[mozJPEG.type]: mozJPEG,
|
[mozJPEG.type]: mozJPEG,
|
||||||
|
[browserPNG.type]: browserPNG,
|
||||||
|
[browserJPEG.type]: browserJPEG,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const encoders = Array.from(Object.values(encoderMap));
|
export const encoders = Array.from(Object.values(encoderMap));
|
||||||
|
|||||||
54
src/codecs/generic/quality-option.tsx
Normal file
54
src/codecs/generic/quality-option.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { h, Component } from 'preact';
|
||||||
|
import { bind } from '../../lib/util';
|
||||||
|
|
||||||
|
interface EncodeOptions {
|
||||||
|
quality: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
options: EncodeOptions,
|
||||||
|
onChange(newOptions: EncodeOptions): void,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface QualityOptionArg {
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
step?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function qualityOption(opts: QualityOptionArg = {}) {
|
||||||
|
const {
|
||||||
|
min = 0,
|
||||||
|
max = 100,
|
||||||
|
step = 1,
|
||||||
|
} = opts;
|
||||||
|
|
||||||
|
class QualityOptions extends Component<Props, {}> {
|
||||||
|
@bind
|
||||||
|
onChange(event: Event) {
|
||||||
|
const el = event.currentTarget as HTMLInputElement;
|
||||||
|
this.props.onChange({ quality: Number(el.value) });
|
||||||
|
}
|
||||||
|
|
||||||
|
render({ options }: Props) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label>
|
||||||
|
Quality:
|
||||||
|
<input
|
||||||
|
name="quality"
|
||||||
|
type="range"
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={step || 'any'}
|
||||||
|
value={'' + options.quality}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return QualityOptions;
|
||||||
|
}
|
||||||
@@ -1,35 +1,3 @@
|
|||||||
import { h, Component } from 'preact';
|
import qualityOption from '../generic/quality-option';
|
||||||
import { EncodeOptions } from './encoder';
|
|
||||||
import { bind } from '../../lib/util';
|
|
||||||
|
|
||||||
type Props = {
|
export default qualityOption();
|
||||||
options: EncodeOptions,
|
|
||||||
onChange(newOptions: EncodeOptions): void
|
|
||||||
};
|
|
||||||
|
|
||||||
export default class MozJpegCodecOptions extends Component<Props, {}> {
|
|
||||||
@bind
|
|
||||||
onChange(event: Event) {
|
|
||||||
const el = event.currentTarget as HTMLInputElement;
|
|
||||||
this.props.onChange({ quality: Number(el.value) });
|
|
||||||
}
|
|
||||||
|
|
||||||
render({ options }: Props) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<label>
|
|
||||||
Quality:
|
|
||||||
<input
|
|
||||||
name="quality"
|
|
||||||
type="range"
|
|
||||||
min="1"
|
|
||||||
max="100"
|
|
||||||
step="1"
|
|
||||||
value={'' + options.quality}
|
|
||||||
onChange={this.onChange}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { h, Component } from 'preact';
|
import { h, Component } from 'preact';
|
||||||
|
import { partial } from 'filesize';
|
||||||
|
|
||||||
import { bind, bitmapToImageData } from '../../lib/util';
|
import { bind, bitmapToImageData } from '../../lib/util';
|
||||||
import * as style from './style.scss';
|
import * as style from './style.scss';
|
||||||
import Output from '../output';
|
import Output from '../output';
|
||||||
@@ -8,6 +10,8 @@ import './custom-els/FileDrop';
|
|||||||
|
|
||||||
import * as mozJPEG from '../../codecs/mozjpeg/encoder';
|
import * as mozJPEG from '../../codecs/mozjpeg/encoder';
|
||||||
import * as identity from '../../codecs/identity/encoder';
|
import * as identity from '../../codecs/identity/encoder';
|
||||||
|
import * as browserPNG from '../../codecs/browser-png/encoder';
|
||||||
|
import * as browserJPEG from '../../codecs/browser-jpeg/encoder';
|
||||||
import { EncoderState, EncoderType, EncoderOptions, encoderMap } from '../../codecs/encoders';
|
import { EncoderState, EncoderType, EncoderOptions, encoderMap } from '../../codecs/encoders';
|
||||||
|
|
||||||
interface SourceImage {
|
interface SourceImage {
|
||||||
@@ -19,6 +23,7 @@ interface SourceImage {
|
|||||||
interface EncodedImage {
|
interface EncodedImage {
|
||||||
encoderState: EncoderState;
|
encoderState: EncoderState;
|
||||||
bmp?: ImageBitmap;
|
bmp?: ImageBitmap;
|
||||||
|
size?: number;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
/** Counter of the latest bmp currently encoding */
|
/** Counter of the latest bmp currently encoding */
|
||||||
loadingCounter: number;
|
loadingCounter: number;
|
||||||
@@ -35,16 +40,20 @@ interface State {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filesize = partial({});
|
||||||
|
|
||||||
async function compressImage(
|
async function compressImage(
|
||||||
source: SourceImage,
|
source: SourceImage,
|
||||||
encodeData: EncoderState,
|
encodeData: EncoderState,
|
||||||
): Promise<ImageBitmap> {
|
): Promise<Blob> {
|
||||||
// Special case for identity
|
// Special case for identity
|
||||||
if (encodeData.type === identity.type) return source.bmp;
|
if (encodeData.type === identity.type) return source.file;
|
||||||
|
|
||||||
const compressedData = await (() => {
|
const compressedData = await (() => {
|
||||||
switch (encodeData.type) {
|
switch (encodeData.type) {
|
||||||
case mozJPEG.type: return mozJPEG.encode(source.data, encodeData.options);
|
case mozJPEG.type: return mozJPEG.encode(source.data, encodeData.options);
|
||||||
|
case browserPNG.type: return browserPNG.encode(source.data, encodeData.options);
|
||||||
|
case browserJPEG.type: return browserJPEG.encode(source.data, encodeData.options);
|
||||||
default: throw Error(`Unexpected encoder name`);
|
default: throw Error(`Unexpected encoder name`);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
@@ -53,8 +62,7 @@ async function compressImage(
|
|||||||
type: encoderMap[encodeData.type].mimeType,
|
type: encoderMap[encodeData.type].mimeType,
|
||||||
});
|
});
|
||||||
|
|
||||||
const bitmap = await createImageBitmap(blob);
|
return blob;
|
||||||
return bitmap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class App extends Component<Props, State> {
|
export default class App extends Component<Props, State> {
|
||||||
@@ -189,11 +197,14 @@ export default class App extends Component<Props, State> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bmp = await createImageBitmap(result);
|
||||||
|
|
||||||
images = this.state.images.slice() as [EncodedImage, EncodedImage];
|
images = this.state.images.slice() as [EncodedImage, EncodedImage];
|
||||||
|
|
||||||
images[index] = {
|
images[index] = {
|
||||||
...images[index],
|
...images[index],
|
||||||
bmp: result,
|
bmp,
|
||||||
|
size: result.size,
|
||||||
loading: image.loadingCounter !== loadingCounter,
|
loading: image.loadingCounter !== loadingCounter,
|
||||||
loadedCounter: loadingCounter,
|
loadedCounter: loadingCounter,
|
||||||
};
|
};
|
||||||
@@ -220,6 +231,7 @@ export default class App extends Component<Props, State> {
|
|||||||
{images.map((image, index) => (
|
{images.map((image, index) => (
|
||||||
<span class={index ? style.rightLabel : style.leftLabel}>
|
<span class={index ? style.rightLabel : style.leftLabel}>
|
||||||
{encoderMap[image.encoderState.type].label}
|
{encoderMap[image.encoderState.type].label}
|
||||||
|
{image.size && ` - ${filesize(image.size)}`}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{images.map((image, index) => (
|
{images.map((image, index) => (
|
||||||
|
|||||||
@@ -2,14 +2,19 @@ import { h, Component } from 'preact';
|
|||||||
import * as style from './style.scss';
|
import * as style from './style.scss';
|
||||||
import { bind } from '../../lib/util';
|
import { bind } from '../../lib/util';
|
||||||
import MozJpegEncoderOptions from '../../codecs/mozjpeg/options';
|
import MozJpegEncoderOptions from '../../codecs/mozjpeg/options';
|
||||||
|
import BrowserJPEGEncoderOptions from '../../codecs/browser-jpeg/options';
|
||||||
|
|
||||||
import { type as mozJPEGType } from '../../codecs/mozjpeg/encoder';
|
import { type as mozJPEGType } from '../../codecs/mozjpeg/encoder';
|
||||||
import { type as identityType } from '../../codecs/identity/encoder';
|
import { type as identityType } from '../../codecs/identity/encoder';
|
||||||
|
import { type as browserPNGType } from '../../codecs/browser-png/encoder';
|
||||||
|
import { type as browserJPEGType } from '../../codecs/browser-jpeg/encoder';
|
||||||
import { EncoderState, EncoderType, EncoderOptions, encoders } from '../../codecs/encoders';
|
import { EncoderState, EncoderType, EncoderOptions, encoders } from '../../codecs/encoders';
|
||||||
|
|
||||||
const encoderOptionsComponentMap = {
|
const encoderOptionsComponentMap = {
|
||||||
[mozJPEGType]: MozJpegEncoderOptions,
|
[mozJPEGType]: MozJpegEncoderOptions,
|
||||||
[identityType]: undefined
|
[identityType]: undefined,
|
||||||
|
[browserPNGType]: undefined,
|
||||||
|
[browserJPEGType]: BrowserJPEGEncoderOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -50,8 +55,8 @@ export default class Options extends Component<Props, State> {
|
|||||||
{EncoderOptionComponent &&
|
{EncoderOptionComponent &&
|
||||||
<EncoderOptionComponent
|
<EncoderOptionComponent
|
||||||
options={
|
options={
|
||||||
// Casting options, as encoderOptionsComponentMap[encodeData.type] ensures the correct type,
|
// Casting options, as encoderOptionsComponentMap[encodeData.type] ensures the correct
|
||||||
// but typescript isn't smart enough.
|
// type, but typescript isn't smart enough.
|
||||||
encoderState.options as typeof EncoderOptionComponent['prototype']['props']['options']
|
encoderState.options as typeof EncoderOptionComponent['prototype']['props']['options']
|
||||||
}
|
}
|
||||||
onChange={onOptionsChange}
|
onChange={onOptionsChange}
|
||||||
|
|||||||
@@ -50,3 +50,17 @@ export function drawBitmapToCanvas(canvas: HTMLCanvasElement, bitmap: ImageBitma
|
|||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
ctx.drawImage(bitmap, 0, 0);
|
ctx.drawImage(bitmap, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function canvasEncode(data: ImageData, type: string, quality?: number) {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = data.width;
|
||||||
|
canvas.height = data.height;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) throw Error('Canvas not initialized');
|
||||||
|
ctx.putImageData(data, 0, 0);
|
||||||
|
|
||||||
|
const blob = await new Promise<Blob | null>(r => canvas.toBlob(r, type, quality));
|
||||||
|
|
||||||
|
if (!blob) throw Error('Encoding failed');
|
||||||
|
return blob;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user