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"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/filesize": "^3.6.0",
|
||||
"classnames": "^2.2.5",
|
||||
"comlink": "^3.0.3",
|
||||
"comlink-loader": "^1.0.0",
|
||||
"filesize": "^3.6.1",
|
||||
"material-components-web": "^0.32.0",
|
||||
"preact": "^8.2.7",
|
||||
"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 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 EncoderOptions = identity.EncodeOptions | mozJPEG.EncodeOptions;
|
||||
export type EncoderState =
|
||||
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 const encoderMap = {
|
||||
[identity.type]: identity,
|
||||
[mozJPEG.type]: mozJPEG,
|
||||
[browserPNG.type]: browserPNG,
|
||||
[browserJPEG.type]: browserJPEG,
|
||||
};
|
||||
|
||||
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 { EncodeOptions } from './encoder';
|
||||
import { bind } from '../../lib/util';
|
||||
import qualityOption from '../generic/quality-option';
|
||||
|
||||
type Props = {
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default qualityOption();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { h, Component } from 'preact';
|
||||
import { partial } from 'filesize';
|
||||
|
||||
import { bind, bitmapToImageData } from '../../lib/util';
|
||||
import * as style from './style.scss';
|
||||
import Output from '../output';
|
||||
@@ -8,6 +10,8 @@ import './custom-els/FileDrop';
|
||||
|
||||
import * as mozJPEG from '../../codecs/mozjpeg/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';
|
||||
|
||||
interface SourceImage {
|
||||
@@ -19,6 +23,7 @@ interface SourceImage {
|
||||
interface EncodedImage {
|
||||
encoderState: EncoderState;
|
||||
bmp?: ImageBitmap;
|
||||
size?: number;
|
||||
loading: boolean;
|
||||
/** Counter of the latest bmp currently encoding */
|
||||
loadingCounter: number;
|
||||
@@ -35,16 +40,20 @@ interface State {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const filesize = partial({});
|
||||
|
||||
async function compressImage(
|
||||
source: SourceImage,
|
||||
encodeData: EncoderState,
|
||||
): Promise<ImageBitmap> {
|
||||
): Promise<Blob> {
|
||||
// Special case for identity
|
||||
if (encodeData.type === identity.type) return source.bmp;
|
||||
if (encodeData.type === identity.type) return source.file;
|
||||
|
||||
const compressedData = await (() => {
|
||||
switch (encodeData.type) {
|
||||
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`);
|
||||
}
|
||||
})();
|
||||
@@ -53,8 +62,7 @@ async function compressImage(
|
||||
type: encoderMap[encodeData.type].mimeType,
|
||||
});
|
||||
|
||||
const bitmap = await createImageBitmap(blob);
|
||||
return bitmap;
|
||||
return blob;
|
||||
}
|
||||
|
||||
export default class App extends Component<Props, State> {
|
||||
@@ -189,11 +197,14 @@ export default class App extends Component<Props, State> {
|
||||
return;
|
||||
}
|
||||
|
||||
const bmp = await createImageBitmap(result);
|
||||
|
||||
images = this.state.images.slice() as [EncodedImage, EncodedImage];
|
||||
|
||||
images[index] = {
|
||||
...images[index],
|
||||
bmp: result,
|
||||
bmp,
|
||||
size: result.size,
|
||||
loading: image.loadingCounter !== loadingCounter,
|
||||
loadedCounter: loadingCounter,
|
||||
};
|
||||
@@ -220,6 +231,7 @@ export default class App extends Component<Props, State> {
|
||||
{images.map((image, index) => (
|
||||
<span class={index ? style.rightLabel : style.leftLabel}>
|
||||
{encoderMap[image.encoderState.type].label}
|
||||
{image.size && ` - ${filesize(image.size)}`}
|
||||
</span>
|
||||
))}
|
||||
{images.map((image, index) => (
|
||||
|
||||
@@ -2,14 +2,19 @@ import { h, Component } from 'preact';
|
||||
import * as style from './style.scss';
|
||||
import { bind } from '../../lib/util';
|
||||
import MozJpegEncoderOptions from '../../codecs/mozjpeg/options';
|
||||
import BrowserJPEGEncoderOptions from '../../codecs/browser-jpeg/options';
|
||||
|
||||
import { type as mozJPEGType } from '../../codecs/mozjpeg/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';
|
||||
|
||||
const encoderOptionsComponentMap = {
|
||||
[mozJPEGType]: MozJpegEncoderOptions,
|
||||
[identityType]: undefined
|
||||
[identityType]: undefined,
|
||||
[browserPNGType]: undefined,
|
||||
[browserJPEGType]: BrowserJPEGEncoderOptions,
|
||||
};
|
||||
|
||||
interface Props {
|
||||
@@ -50,8 +55,8 @@ export default class Options extends Component<Props, State> {
|
||||
{EncoderOptionComponent &&
|
||||
<EncoderOptionComponent
|
||||
options={
|
||||
// Casting options, as encoderOptionsComponentMap[encodeData.type] ensures the correct type,
|
||||
// but typescript isn't smart enough.
|
||||
// Casting options, as encoderOptionsComponentMap[encodeData.type] ensures the correct
|
||||
// type, but typescript isn't smart enough.
|
||||
encoderState.options as typeof EncoderOptionComponent['prototype']['props']['options']
|
||||
}
|
||||
onChange={onOptionsChange}
|
||||
|
||||
@@ -50,3 +50,17 @@ export function drawBitmapToCanvas(canvas: HTMLCanvasElement, bitmap: ImageBitma
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
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