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:
Jake Archibald
2018-07-02 15:14:09 +01:00
committed by GitHub
parent 3f18c927f1
commit a09ec269b8
11 changed files with 14755 additions and 44 deletions

14618
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View 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);
}

View File

@@ -0,0 +1,3 @@
import qualityOption from '../generic/quality-option';
export default qualityOption({ min: 0, max: 1, step: 0 });

View 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);
}

View File

@@ -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));

View 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;
}

View File

@@ -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>
);
}
}

View File

@@ -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) => (

View File

@@ -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}

View File

@@ -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;
}