mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-16 10:39:53 +00:00
Structure for browser decoders. Some work on decoding.
This commit is contained in:
@@ -72,7 +72,7 @@ export default function () {
|
||||
await Promise.all([
|
||||
fsp.writeFile(
|
||||
path.join(workerBasePath, 'tsconfig.json'),
|
||||
JSON.stringify(workerTsConfig, null, ' '),
|
||||
autoGenComment + JSON.stringify(workerTsConfig, null, ' '),
|
||||
),
|
||||
fsp.writeFile(path.join(workerBasePath, 'index.ts'), workerFile),
|
||||
]);
|
||||
|
||||
@@ -1,34 +1,18 @@
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import { bind, Fileish } from '../../lib/initial-util';
|
||||
import { blobToImg, drawableToImageData, blobToText } from '../../lib/util';
|
||||
import * as style from './style.scss';
|
||||
import {
|
||||
blobToImg,
|
||||
drawableToImageData,
|
||||
blobToText,
|
||||
builtinDecode,
|
||||
sniffMimeType,
|
||||
canDecodeImageType,
|
||||
} from '../util';
|
||||
import * as style from './style.css';
|
||||
import 'add-css:./style.css';
|
||||
import Output from '../Output';
|
||||
import Options from '../Options';
|
||||
import ResultCache from './result-cache';
|
||||
import * as identity from '../../codecs/identity/encoder-meta';
|
||||
import * as oxiPNG from '../../codecs/oxipng/encoder-meta';
|
||||
import * as mozJPEG from '../../codecs/mozjpeg/encoder-meta';
|
||||
import * as webP from '../../codecs/webp/encoder-meta';
|
||||
import * as avif from '../../codecs/avif/encoder-meta';
|
||||
import * as browserPNG from '../../codecs/browser-png/encoder-meta';
|
||||
import * as browserJPEG from '../../codecs/browser-jpeg/encoder-meta';
|
||||
import * as browserWebP from '../../codecs/browser-webp/encoder-meta';
|
||||
import * as browserGIF from '../../codecs/browser-gif/encoder-meta';
|
||||
import * as browserTIFF from '../../codecs/browser-tiff/encoder-meta';
|
||||
import * as browserJP2 from '../../codecs/browser-jp2/encoder-meta';
|
||||
import * as browserBMP from '../../codecs/browser-bmp/encoder-meta';
|
||||
import * as browserPDF from '../../codecs/browser-pdf/encoder-meta';
|
||||
import {
|
||||
EncoderState,
|
||||
EncoderType,
|
||||
EncoderOptions,
|
||||
encoderMap,
|
||||
} from '../../codecs/encoders';
|
||||
import {
|
||||
PreprocessorState,
|
||||
defaultPreprocessorState,
|
||||
} from '../../codecs/preprocessors';
|
||||
import { decodeImage } from '../../codecs/decoders';
|
||||
import { cleanMerge, cleanSet } from '../../lib/clean-modify';
|
||||
import Processor from '../../codecs/processor';
|
||||
@@ -46,9 +30,10 @@ import {
|
||||
InputProcessorState,
|
||||
defaultInputProcessorState,
|
||||
} from '../../codecs/input-processors';
|
||||
import WorkerBridge from '../worker-bridge';
|
||||
|
||||
export interface SourceImage {
|
||||
file: File | Fileish;
|
||||
file: File;
|
||||
decoded: ImageData;
|
||||
processed: ImageData;
|
||||
vectorImage?: HTMLImageElement;
|
||||
@@ -62,20 +47,16 @@ interface SideSettings {
|
||||
|
||||
interface Side {
|
||||
preprocessed?: ImageData;
|
||||
file?: Fileish;
|
||||
file?: File;
|
||||
downloadUrl?: string;
|
||||
data?: ImageData;
|
||||
latestSettings: SideSettings;
|
||||
encodedSettings?: SideSettings;
|
||||
loading: boolean;
|
||||
/** Counter of the latest bmp currently encoding */
|
||||
loadingCounter: number;
|
||||
/** Counter of the latest bmp encoded */
|
||||
loadedCounter: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
file: File | Fileish;
|
||||
file: File;
|
||||
showSnack: SnackBarElement['showSnackbar'];
|
||||
onBack: () => void;
|
||||
}
|
||||
@@ -85,7 +66,6 @@ interface State {
|
||||
sides: [Side, Side];
|
||||
/** Source image load */
|
||||
loading: boolean;
|
||||
loadingCounter: number;
|
||||
error?: string;
|
||||
mobileView: boolean;
|
||||
}
|
||||
@@ -94,6 +74,30 @@ interface UpdateImageOptions {
|
||||
skipPreprocessing?: boolean;
|
||||
}
|
||||
|
||||
async function decodeImage(
|
||||
signal: AbortSignal,
|
||||
blob: Blob,
|
||||
workerBridge: WorkerBridge,
|
||||
): Promise<ImageData> {
|
||||
const mimeType = await sniffMimeType(blob);
|
||||
const canDecode = await canDecodeImageType(mimeType);
|
||||
|
||||
try {
|
||||
if (!canDecode) {
|
||||
if (mimeType === 'image/avif') {
|
||||
return await workerBridge.avifDecode(signal, blob);
|
||||
}
|
||||
if (mimeType === 'image/webp') {
|
||||
return await workerBridge.webpDecode(signal, blob);
|
||||
}
|
||||
// If it's not one of those types, fall through and try built-in decoding for a laugh.
|
||||
}
|
||||
return await builtinDecode(blob);
|
||||
} catch (err) {
|
||||
throw Error("Couldn't decode image");
|
||||
}
|
||||
}
|
||||
|
||||
async function processInput(
|
||||
data: ImageData,
|
||||
inputProcessData: InputProcessorState,
|
||||
@@ -154,7 +158,7 @@ async function compressImage(
|
||||
encodeData: EncoderState,
|
||||
sourceFilename: string,
|
||||
processor: Processor,
|
||||
): Promise<Fileish> {
|
||||
): Promise<File> {
|
||||
const compressedData = await (() => {
|
||||
switch (encodeData.type) {
|
||||
case oxiPNG.type:
|
||||
@@ -188,7 +192,7 @@ async function compressImage(
|
||||
|
||||
const encoder = encoderMap[encodeData.type];
|
||||
|
||||
return new Fileish(
|
||||
return new File(
|
||||
[compressedData],
|
||||
sourceFilename.replace(/.[^.]*$/, `.${encoder.extension}`),
|
||||
{ type: encoder.mimeType },
|
||||
@@ -300,10 +304,9 @@ export default class Compress extends Component<Props, State> {
|
||||
import('../../lib/sw-bridge').then(({ mainAppLoaded }) => mainAppLoaded());
|
||||
}
|
||||
|
||||
@bind
|
||||
private onMobileWidthChange() {
|
||||
private onMobileWidthChange = () => {
|
||||
this.setState({ mobileView: this.widthQuery.matches });
|
||||
}
|
||||
};
|
||||
|
||||
private onEncoderTypeChange(index: 0 | 1, newType: EncoderType): void {
|
||||
this.setState({
|
||||
@@ -412,10 +415,9 @@ export default class Compress extends Component<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
private async onInputProcessorChange(
|
||||
private onInputProcessorChange = async (
|
||||
options: InputProcessorState,
|
||||
): Promise<void> {
|
||||
): Promise<void> => {
|
||||
const source = this.state.source;
|
||||
if (!source) return;
|
||||
|
||||
@@ -470,10 +472,9 @@ export default class Compress extends Component<Props, State> {
|
||||
this.props.showSnack('Processing error');
|
||||
this.setState({ loading: false });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@bind
|
||||
private async updateFile(file: File | Fileish) {
|
||||
private updateFile = async (file: File) => {
|
||||
const loadingCounter = this.state.loadingCounter + 1;
|
||||
// Either processor is good enough here.
|
||||
const processor = this.leftProcessor;
|
||||
@@ -545,7 +546,7 @@ export default class Compress extends Component<Props, State> {
|
||||
this.props.showSnack('Invalid image');
|
||||
this.setState({ loading: false });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Debounce the heavy lifting of updateImage.
|
||||
@@ -589,7 +590,7 @@ export default class Compress extends Component<Props, State> {
|
||||
const side = sides[index];
|
||||
const settings = side.latestSettings;
|
||||
|
||||
let file: File | Fileish | undefined;
|
||||
let file: File | undefined;
|
||||
let preprocessed: ImageData | undefined;
|
||||
let data: ImageData | undefined;
|
||||
const cacheResult = this.encodeCache.match(
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { EncoderState, ProcessorState } from '../feature-meta';
|
||||
import { shallowEqual } from '../../util';
|
||||
|
||||
import * as identity from '../../codecs/identity/encoder-meta';
|
||||
|
||||
interface CacheResult {
|
||||
preprocessed: ImageData;
|
||||
data: ImageData;
|
||||
@@ -21,8 +19,9 @@ export default class ResultCache {
|
||||
private readonly _entries: CacheEntry[] = [];
|
||||
|
||||
add(entry: CacheEntry) {
|
||||
if (entry.encoderState.type === identity.type)
|
||||
if (entry.encoderState.type === 'identity') {
|
||||
throw Error('Cannot cache identity encodes');
|
||||
}
|
||||
// Add the new entry to the start
|
||||
this._entries.unshift(entry);
|
||||
// Remove the last entry if we're now bigger than SIZE
|
||||
@@ -46,13 +45,15 @@ export default class ResultCache {
|
||||
(processorState as any)[prop],
|
||||
(entry.processorState as any)[prop],
|
||||
)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check detailed encoder options
|
||||
if (!shallowEqual(encoderState.options, entry.encoderState.options))
|
||||
if (!shallowEqual(encoderState.options, entry.encoderState.options)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
flex-flow: column;
|
||||
overflow: hidden;
|
||||
|
||||
// Reorder so headings appear after content:
|
||||
/* Reorder so headings appear after content: */
|
||||
& > :nth-child(1) {
|
||||
order: 2;
|
||||
margin-bottom: 10px;
|
||||
@@ -71,5 +71,5 @@
|
||||
}
|
||||
|
||||
:focus .expand-icon {
|
||||
fill: #34B9EB;
|
||||
fill: #34b9eb;
|
||||
}
|
||||
@@ -1,3 +1,322 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/**
|
||||
* Compare two objects, returning a boolean indicating if
|
||||
* they have the same properties and strictly equal values.
|
||||
*/
|
||||
export function shallowEqual(one: any, two: any) {
|
||||
for (const i in one) if (one[i] !== two[i]) return false;
|
||||
for (const i in two) if (!(i in one)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Replace the contents of a canvas with the given data */
|
||||
export function drawDataToCanvas(canvas: HTMLCanvasElement, data: ImageData) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) throw Error('Canvas not initialized');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.putImageData(data, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode some image data in a given format using the browser's encoders
|
||||
*
|
||||
* @param {ImageData} data
|
||||
* @param {string} type A mime type, eg image/jpeg.
|
||||
* @param {number} [quality] Between 0-1.
|
||||
*/
|
||||
export async function canvasEncode(
|
||||
data: ImageData,
|
||||
type: string,
|
||||
quality?: number,
|
||||
): Promise<Blob> {
|
||||
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);
|
||||
|
||||
let blob: Blob | null;
|
||||
|
||||
if ('toBlob' in canvas) {
|
||||
blob = await new Promise<Blob | null>((r) =>
|
||||
canvas.toBlob(r, type, quality),
|
||||
);
|
||||
} else {
|
||||
// Welcome to Edge.
|
||||
// TypeScript thinks `canvas` is 'never', so it needs casting.
|
||||
const dataUrl = (canvas as HTMLCanvasElement).toDataURL(type, quality);
|
||||
const result = /data:([^;]+);base64,(.*)$/.exec(dataUrl);
|
||||
|
||||
if (!result) throw Error('Data URL reading failed');
|
||||
|
||||
const outputType = result[1];
|
||||
const binaryStr = atob(result[2]);
|
||||
const data = new Uint8Array(binaryStr.length);
|
||||
|
||||
for (let i = 0; i < data.length; i += 1) {
|
||||
data[i] = binaryStr.charCodeAt(i);
|
||||
}
|
||||
|
||||
blob = new Blob([data], { type: outputType });
|
||||
}
|
||||
|
||||
if (!blob) throw Error('Encoding failed');
|
||||
return blob;
|
||||
}
|
||||
|
||||
async function decodeImage(url: string): Promise<HTMLImageElement> {
|
||||
const img = new Image();
|
||||
img.decoding = 'async';
|
||||
img.src = url;
|
||||
const loaded = new Promise((resolve, reject) => {
|
||||
img.onload = () => resolve();
|
||||
img.onerror = () => reject(Error('Image loading error'));
|
||||
});
|
||||
|
||||
if (img.decode) {
|
||||
// Nice off-thread way supported in Safari/Chrome.
|
||||
// Safari throws on decode if the source is SVG.
|
||||
// https://bugs.webkit.org/show_bug.cgi?id=188347
|
||||
await img.decode().catch(() => null);
|
||||
}
|
||||
|
||||
// Always await loaded, as we may have bailed due to the Safari bug above.
|
||||
await loaded;
|
||||
return img;
|
||||
}
|
||||
|
||||
/** Caches results from canDecodeImageType */
|
||||
const canDecodeCache = new Map<string, Promise<boolean>>();
|
||||
|
||||
/**
|
||||
* Tests whether the browser supports a particular image mime type.
|
||||
*
|
||||
* @param type Mimetype
|
||||
* @example await canDecodeImageType('image/avif')
|
||||
*/
|
||||
export function canDecodeImageType(type: string): Promise<boolean> {
|
||||
if (!canDecodeCache.has(type)) {
|
||||
const resultPromise = (async () => {
|
||||
const picture = document.createElement('picture');
|
||||
const img = document.createElement('img');
|
||||
const source = document.createElement('source');
|
||||
source.srcset = 'data:,x';
|
||||
source.type = type;
|
||||
picture.append(source, img);
|
||||
|
||||
// Wait a single microtick just for the `img.currentSrc` to get populated.
|
||||
await 0;
|
||||
// At this point `img.currentSrc` will contain "data:,x" if format is supported and ""
|
||||
// otherwise.
|
||||
return !!img.currentSrc;
|
||||
})();
|
||||
|
||||
canDecodeCache.set(type, resultPromise);
|
||||
}
|
||||
|
||||
return canDecodeCache.get(type)!;
|
||||
}
|
||||
|
||||
export function blobToArrayBuffer(blob: Blob): Promise<ArrayBuffer> {
|
||||
return new Response(blob).arrayBuffer();
|
||||
}
|
||||
|
||||
export function blobToText(blob: Blob): Promise<string> {
|
||||
return new Response(blob).text();
|
||||
}
|
||||
|
||||
const magicNumberToMimeType = new Map<RegExp, string>([
|
||||
[/^%PDF-/, 'application/pdf'],
|
||||
[/^GIF87a/, 'image/gif'],
|
||||
[/^GIF89a/, 'image/gif'],
|
||||
[/^\x89PNG\x0D\x0A\x1A\x0A/, 'image/png'],
|
||||
[/^\xFF\xD8\xFF/, 'image/jpeg'],
|
||||
[/^BM/, 'image/bmp'],
|
||||
[/^I I/, 'image/tiff'],
|
||||
[/^II*/, 'image/tiff'],
|
||||
[/^MM\x00*/, 'image/tiff'],
|
||||
[/^RIFF....WEBPVP8[LX ]/, 'image/webp'],
|
||||
[/^\x00\x00\x00 ftypavif\x00\x00\x00\x00/, 'image/avif'],
|
||||
]);
|
||||
|
||||
export async function sniffMimeType(blob: Blob): Promise<string> {
|
||||
const firstChunk = await blobToArrayBuffer(blob.slice(0, 16));
|
||||
const firstChunkString = Array.from(new Uint8Array(firstChunk))
|
||||
.map((v) => String.fromCodePoint(v))
|
||||
.join('');
|
||||
for (const [detector, mimeType] of magicNumberToMimeType) {
|
||||
if (detector.test(firstChunkString)) {
|
||||
return mimeType;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export async function blobToImg(blob: Blob): Promise<HTMLImageElement> {
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
try {
|
||||
return await decodeImage(url);
|
||||
} finally {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
interface DrawableToImageDataOptions {
|
||||
width?: number;
|
||||
height?: number;
|
||||
sx?: number;
|
||||
sy?: number;
|
||||
sw?: number;
|
||||
sh?: number;
|
||||
}
|
||||
|
||||
export function drawableToImageData(
|
||||
drawable: ImageBitmap | HTMLImageElement,
|
||||
opts: DrawableToImageDataOptions = {},
|
||||
): ImageData {
|
||||
const {
|
||||
width = drawable.width,
|
||||
height = drawable.height,
|
||||
sx = 0,
|
||||
sy = 0,
|
||||
sw = drawable.width,
|
||||
sh = drawable.height,
|
||||
} = opts;
|
||||
|
||||
// Make canvas same size as image
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
// Draw image onto canvas
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) throw new Error('Could not create canvas context');
|
||||
ctx.drawImage(drawable, sx, sy, sw, sh, 0, 0, width, height);
|
||||
return ctx.getImageData(0, 0, width, height);
|
||||
}
|
||||
|
||||
export async function builtinDecode(blob: Blob): Promise<ImageData> {
|
||||
// Prefer createImageBitmap as it's the off-thread option for Firefox.
|
||||
const drawable =
|
||||
'createImageBitmap' in self
|
||||
? await createImageBitmap(blob)
|
||||
: await blobToImg(blob);
|
||||
|
||||
return drawableToImageData(drawable);
|
||||
}
|
||||
|
||||
export type BuiltinResizeMethod = 'pixelated' | 'low' | 'medium' | 'high';
|
||||
|
||||
export function builtinResize(
|
||||
data: ImageData,
|
||||
sx: number,
|
||||
sy: number,
|
||||
sw: number,
|
||||
sh: number,
|
||||
dw: number,
|
||||
dh: number,
|
||||
method: BuiltinResizeMethod,
|
||||
): ImageData {
|
||||
const canvasSource = document.createElement('canvas');
|
||||
canvasSource.width = data.width;
|
||||
canvasSource.height = data.height;
|
||||
drawDataToCanvas(canvasSource, data);
|
||||
|
||||
const canvasDest = document.createElement('canvas');
|
||||
canvasDest.width = dw;
|
||||
canvasDest.height = dh;
|
||||
const ctx = canvasDest.getContext('2d');
|
||||
if (!ctx) throw new Error('Could not create canvas context');
|
||||
|
||||
if (method === 'pixelated') {
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
} else {
|
||||
ctx.imageSmoothingQuality = method;
|
||||
}
|
||||
|
||||
ctx.drawImage(canvasSource, sx, sy, sw, sh, 0, 0, dw, dh);
|
||||
return ctx.getImageData(0, 0, dw, dh);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param field An HTMLInputElement, but the casting is done here to tidy up onChange.
|
||||
* @param defaultVal Value to return if 'field' doesn't exist.
|
||||
*/
|
||||
export function inputFieldValueAsNumber(
|
||||
field: any,
|
||||
defaultVal: number = 0,
|
||||
): number {
|
||||
if (!field) return defaultVal;
|
||||
return Number(inputFieldValue(field));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param field An HTMLInputElement, but the casting is done here to tidy up onChange.
|
||||
* @param defaultVal Value to return if 'field' doesn't exist.
|
||||
*/
|
||||
export function inputFieldCheckedAsNumber(
|
||||
field: any,
|
||||
defaultVal: number = 0,
|
||||
): number {
|
||||
if (!field) return defaultVal;
|
||||
return Number(inputFieldChecked(field));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param field An HTMLInputElement, but the casting is done here to tidy up onChange.
|
||||
* @param defaultVal Value to return if 'field' doesn't exist.
|
||||
*/
|
||||
export function inputFieldChecked(
|
||||
field: any,
|
||||
defaultVal: boolean = false,
|
||||
): boolean {
|
||||
if (!field) return defaultVal;
|
||||
return (field as HTMLInputElement).checked;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param field An HTMLInputElement, but the casting is done here to tidy up onChange.
|
||||
* @param defaultVal Value to return if 'field' doesn't exist.
|
||||
*/
|
||||
export function inputFieldValue(field: any, defaultVal: string = ''): string {
|
||||
if (!field) return defaultVal;
|
||||
return (field as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a promise that resolves when the user types the konami code.
|
||||
*/
|
||||
export function konami(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
// Keycodes for: ↑ ↑ ↓ ↓ ← → ← → B A
|
||||
const expectedPattern = '38384040373937396665';
|
||||
let rollingPattern = '';
|
||||
|
||||
const listener = (event: KeyboardEvent) => {
|
||||
rollingPattern += event.keyCode;
|
||||
rollingPattern = rollingPattern.slice(-expectedPattern.length);
|
||||
if (rollingPattern === expectedPattern) {
|
||||
window.removeEventListener('keydown', listener);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', listener);
|
||||
});
|
||||
}
|
||||
|
||||
interface TransitionOptions {
|
||||
from?: number;
|
||||
to?: number;
|
||||
@@ -41,6 +360,13 @@ export async function transitionHeight(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple event listener that prevents the default.
|
||||
*/
|
||||
export function preventDefault(event: Event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a signal and promise, and returns a promise that rejects with an AbortError if the abort is
|
||||
* signalled, otherwise resolves with the promise.
|
||||
@@ -61,11 +387,16 @@ export async function abortable<T>(
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two objects, returning a boolean indicating if they have the same properties and strictly
|
||||
* equal values.
|
||||
* Test whether <canvas> can encode to a particular type.
|
||||
*/
|
||||
export function shallowEqual(one: any, two: any) {
|
||||
for (const i in one) if (one[i] !== two[i]) return false;
|
||||
for (const i in two) if (!(i in one)) return false;
|
||||
return true;
|
||||
export async function canvasEncodeTest(mimeType: string): Promise<boolean> {
|
||||
try {
|
||||
const blob = await canvasEncode(new ImageData(1, 1), mimeType);
|
||||
// According to the spec, the blob should be null if the format isn't supported…
|
||||
if (!blob) return false;
|
||||
// …but Safari & Firefox fall back to PNG, so we need to check the mime type.
|
||||
return blob.type === mimeType;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,12 @@
|
||||
"lib": ["esnext", "dom", "dom.iterable"],
|
||||
"types": []
|
||||
},
|
||||
"references": [{ "path": "../features-worker" }, { "path": "../shared" }]
|
||||
"references": [
|
||||
{ "path": "../features-worker" },
|
||||
{ "path": "../shared" },
|
||||
{ "path": "../features/encoders/identity/shared" },
|
||||
{ "path": "../features/encoders/browserGIF/shared" },
|
||||
{ "path": "../features/encoders/browserJPEG/shared" },
|
||||
{ "path": "../features/encoders/browserPNG/shared" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,367 +0,0 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/** Compare two objects, returning a boolean indicating if
|
||||
* they have the same properties and strictly equal values.
|
||||
*/
|
||||
export function shallowEqual(one: any, two: any) {
|
||||
for (const i in one) if (one[i] !== two[i]) return false;
|
||||
for (const i in two) if (!(i in one)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Replace the contents of a canvas with the given data */
|
||||
export function drawDataToCanvas(canvas: HTMLCanvasElement, data: ImageData) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) throw Error('Canvas not initialized');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.putImageData(data, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode some image data in a given format using the browser's encoders
|
||||
*
|
||||
* @param {ImageData} data
|
||||
* @param {string} type A mime type, eg image/jpeg.
|
||||
* @param {number} [quality] Between 0-1.
|
||||
*/
|
||||
export async function canvasEncode(
|
||||
data: ImageData,
|
||||
type: string,
|
||||
quality?: number,
|
||||
): Promise<Blob> {
|
||||
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);
|
||||
|
||||
let blob: Blob | null;
|
||||
|
||||
if ('toBlob' in canvas) {
|
||||
blob = await new Promise<Blob | null>((r) =>
|
||||
canvas.toBlob(r, type, quality),
|
||||
);
|
||||
} else {
|
||||
// Welcome to Edge.
|
||||
// TypeScript thinks `canvas` is 'never', so it needs casting.
|
||||
const dataUrl = (canvas as HTMLCanvasElement).toDataURL(type, quality);
|
||||
const result = /data:([^;]+);base64,(.*)$/.exec(dataUrl);
|
||||
|
||||
if (!result) throw Error('Data URL reading failed');
|
||||
|
||||
const outputType = result[1];
|
||||
const binaryStr = atob(result[2]);
|
||||
const data = new Uint8Array(binaryStr.length);
|
||||
|
||||
for (let i = 0; i < data.length; i += 1) {
|
||||
data[i] = binaryStr.charCodeAt(i);
|
||||
}
|
||||
|
||||
blob = new Blob([data], { type: outputType });
|
||||
}
|
||||
|
||||
if (!blob) throw Error('Encoding failed');
|
||||
return blob;
|
||||
}
|
||||
|
||||
async function decodeImage(url: string): Promise<HTMLImageElement> {
|
||||
const img = new Image();
|
||||
img.decoding = 'async';
|
||||
img.src = url;
|
||||
const loaded = new Promise((resolve, reject) => {
|
||||
img.onload = () => resolve();
|
||||
img.onerror = () => reject(Error('Image loading error'));
|
||||
});
|
||||
|
||||
if (img.decode) {
|
||||
// Nice off-thread way supported in Safari/Chrome.
|
||||
// Safari throws on decode if the source is SVG.
|
||||
// https://bugs.webkit.org/show_bug.cgi?id=188347
|
||||
await img.decode().catch(() => null);
|
||||
}
|
||||
|
||||
// Always await loaded, as we may have bailed due to the Safari bug above.
|
||||
await loaded;
|
||||
return img;
|
||||
}
|
||||
|
||||
/** Caches results from canDecodeImageType */
|
||||
const canDecodeCache = new Map<string, Promise<boolean>>();
|
||||
|
||||
/**
|
||||
* Tests whether the browser supports a particular image mime type.
|
||||
*
|
||||
* @param type Mimetype
|
||||
* @example await canDecodeImageType('image/avif')
|
||||
*/
|
||||
export function canDecodeImageType(type: string): Promise<boolean> {
|
||||
if (!canDecodeCache.has(type)) {
|
||||
const resultPromise = (async () => {
|
||||
const picture = document.createElement('picture');
|
||||
const img = document.createElement('img');
|
||||
const source = document.createElement('source');
|
||||
source.srcset = 'data:,x';
|
||||
source.type = type;
|
||||
picture.append(source, img);
|
||||
|
||||
// Wait a single microtick just for the `img.currentSrc` to get populated.
|
||||
await 0;
|
||||
// At this point `img.currentSrc` will contain "data:,x" if format is supported and ""
|
||||
// otherwise.
|
||||
return !!img.currentSrc;
|
||||
})();
|
||||
|
||||
canDecodeCache.set(type, resultPromise);
|
||||
}
|
||||
|
||||
return canDecodeCache.get(type)!;
|
||||
}
|
||||
|
||||
export function blobToArrayBuffer(blob: Blob): Promise<ArrayBuffer> {
|
||||
return new Response(blob).arrayBuffer();
|
||||
}
|
||||
|
||||
export function blobToText(blob: Blob): Promise<string> {
|
||||
return new Response(blob).text();
|
||||
}
|
||||
|
||||
const magicNumberToMimeType = new Map<RegExp, string>([
|
||||
[/^%PDF-/, 'application/pdf'],
|
||||
[/^GIF87a/, 'image/gif'],
|
||||
[/^GIF89a/, 'image/gif'],
|
||||
[/^\x89PNG\x0D\x0A\x1A\x0A/, 'image/png'],
|
||||
[/^\xFF\xD8\xFF/, 'image/jpeg'],
|
||||
[/^BM/, 'image/bmp'],
|
||||
[/^I I/, 'image/tiff'],
|
||||
[/^II*/, 'image/tiff'],
|
||||
[/^MM\x00*/, 'image/tiff'],
|
||||
[/^RIFF....WEBPVP8[LX ]/, 'image/webp'],
|
||||
[/^\x00\x00\x00 ftypavif\x00\x00\x00\x00/, 'image/avif'],
|
||||
]);
|
||||
|
||||
export async function sniffMimeType(blob: Blob): Promise<string> {
|
||||
const firstChunk = await blobToArrayBuffer(blob.slice(0, 16));
|
||||
const firstChunkString = Array.from(new Uint8Array(firstChunk))
|
||||
.map((v) => String.fromCodePoint(v))
|
||||
.join('');
|
||||
for (const [detector, mimeType] of magicNumberToMimeType) {
|
||||
if (detector.test(firstChunkString)) {
|
||||
return mimeType;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export async function blobToImg(blob: Blob): Promise<HTMLImageElement> {
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
try {
|
||||
return await decodeImage(url);
|
||||
} finally {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}
|
||||
|
||||
interface DrawableToImageDataOptions {
|
||||
width?: number;
|
||||
height?: number;
|
||||
sx?: number;
|
||||
sy?: number;
|
||||
sw?: number;
|
||||
sh?: number;
|
||||
}
|
||||
|
||||
export function drawableToImageData(
|
||||
drawable: ImageBitmap | HTMLImageElement,
|
||||
opts: DrawableToImageDataOptions = {},
|
||||
): ImageData {
|
||||
const {
|
||||
width = drawable.width,
|
||||
height = drawable.height,
|
||||
sx = 0,
|
||||
sy = 0,
|
||||
sw = drawable.width,
|
||||
sh = drawable.height,
|
||||
} = opts;
|
||||
|
||||
// Make canvas same size as image
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
// Draw image onto canvas
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) throw new Error('Could not create canvas context');
|
||||
ctx.drawImage(drawable, sx, sy, sw, sh, 0, 0, width, height);
|
||||
return ctx.getImageData(0, 0, width, height);
|
||||
}
|
||||
|
||||
export async function builtinDecode(blob: Blob): Promise<ImageData> {
|
||||
// Prefer createImageBitmap as it's the off-thread option for Firefox.
|
||||
const drawable =
|
||||
'createImageBitmap' in self
|
||||
? await createImageBitmap(blob)
|
||||
: await blobToImg(blob);
|
||||
|
||||
return drawableToImageData(drawable);
|
||||
}
|
||||
|
||||
export type BuiltinResizeMethod = 'pixelated' | 'low' | 'medium' | 'high';
|
||||
|
||||
export function builtinResize(
|
||||
data: ImageData,
|
||||
sx: number,
|
||||
sy: number,
|
||||
sw: number,
|
||||
sh: number,
|
||||
dw: number,
|
||||
dh: number,
|
||||
method: BuiltinResizeMethod,
|
||||
): ImageData {
|
||||
const canvasSource = document.createElement('canvas');
|
||||
canvasSource.width = data.width;
|
||||
canvasSource.height = data.height;
|
||||
drawDataToCanvas(canvasSource, data);
|
||||
|
||||
const canvasDest = document.createElement('canvas');
|
||||
canvasDest.width = dw;
|
||||
canvasDest.height = dh;
|
||||
const ctx = canvasDest.getContext('2d');
|
||||
if (!ctx) throw new Error('Could not create canvas context');
|
||||
|
||||
if (method === 'pixelated') {
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
} else {
|
||||
ctx.imageSmoothingQuality = method;
|
||||
}
|
||||
|
||||
ctx.drawImage(canvasSource, sx, sy, sw, sh, 0, 0, dw, dh);
|
||||
return ctx.getImageData(0, 0, dw, dh);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param field An HTMLInputElement, but the casting is done here to tidy up onChange.
|
||||
* @param defaultVal Value to return if 'field' doesn't exist.
|
||||
*/
|
||||
export function inputFieldValueAsNumber(
|
||||
field: any,
|
||||
defaultVal: number = 0,
|
||||
): number {
|
||||
if (!field) return defaultVal;
|
||||
return Number(inputFieldValue(field));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param field An HTMLInputElement, but the casting is done here to tidy up onChange.
|
||||
* @param defaultVal Value to return if 'field' doesn't exist.
|
||||
*/
|
||||
export function inputFieldCheckedAsNumber(
|
||||
field: any,
|
||||
defaultVal: number = 0,
|
||||
): number {
|
||||
if (!field) return defaultVal;
|
||||
return Number(inputFieldChecked(field));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param field An HTMLInputElement, but the casting is done here to tidy up onChange.
|
||||
* @param defaultVal Value to return if 'field' doesn't exist.
|
||||
*/
|
||||
export function inputFieldChecked(
|
||||
field: any,
|
||||
defaultVal: boolean = false,
|
||||
): boolean {
|
||||
if (!field) return defaultVal;
|
||||
return (field as HTMLInputElement).checked;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param field An HTMLInputElement, but the casting is done here to tidy up onChange.
|
||||
* @param defaultVal Value to return if 'field' doesn't exist.
|
||||
*/
|
||||
export function inputFieldValue(field: any, defaultVal: string = ''): string {
|
||||
if (!field) return defaultVal;
|
||||
return (field as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a promise that resolves when the user types the konami code.
|
||||
*/
|
||||
export function konami(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
// Keycodes for: ↑ ↑ ↓ ↓ ← → ← → B A
|
||||
const expectedPattern = '38384040373937396665';
|
||||
let rollingPattern = '';
|
||||
|
||||
const listener = (event: KeyboardEvent) => {
|
||||
rollingPattern += event.keyCode;
|
||||
rollingPattern = rollingPattern.slice(-expectedPattern.length);
|
||||
if (rollingPattern === expectedPattern) {
|
||||
window.removeEventListener('keydown', listener);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', listener);
|
||||
});
|
||||
}
|
||||
|
||||
interface TransitionOptions {
|
||||
from?: number;
|
||||
to?: number;
|
||||
duration?: number;
|
||||
easing?: string;
|
||||
}
|
||||
|
||||
export async function transitionHeight(
|
||||
el: HTMLElement,
|
||||
opts: TransitionOptions,
|
||||
): Promise<void> {
|
||||
const {
|
||||
from = el.getBoundingClientRect().height,
|
||||
to = el.getBoundingClientRect().height,
|
||||
duration = 1000,
|
||||
easing = 'ease-in-out',
|
||||
} = opts;
|
||||
|
||||
if (from === to || duration === 0) {
|
||||
el.style.height = to + 'px';
|
||||
return;
|
||||
}
|
||||
|
||||
el.style.height = from + 'px';
|
||||
// Force a style calc so the browser picks up the start value.
|
||||
getComputedStyle(el).transform;
|
||||
el.style.transition = `height ${duration}ms ${easing}`;
|
||||
el.style.height = to + 'px';
|
||||
|
||||
return new Promise<void>((resolve) => {
|
||||
const listener = (event: Event) => {
|
||||
if (event.target !== el) return;
|
||||
el.style.transition = '';
|
||||
el.removeEventListener('transitionend', listener);
|
||||
el.removeEventListener('transitioncancel', listener);
|
||||
resolve();
|
||||
};
|
||||
|
||||
el.addEventListener('transitionend', listener);
|
||||
el.addEventListener('transitioncancel', listener);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple event listener that prevents the default.
|
||||
*/
|
||||
export function preventDefault(event: Event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
@@ -12,16 +12,20 @@
|
||||
*/
|
||||
import avifDecoder, { AVIFModule } from 'codecs/avif/dec/avif_dec';
|
||||
import wasmUrl from 'url:codecs/avif/dec/avif_dec.wasm';
|
||||
import { initEmscriptenModule } from 'features/util';
|
||||
import { initEmscriptenModule, blobToArrayBuffer } from 'features/util';
|
||||
|
||||
let emscriptenModule: Promise<AVIFModule>;
|
||||
|
||||
export default async function decode(data: ArrayBuffer): Promise<ImageData> {
|
||||
export default async function decode(blob: Blob): Promise<ImageData> {
|
||||
if (!emscriptenModule) {
|
||||
emscriptenModule = initEmscriptenModule(avifDecoder, wasmUrl);
|
||||
}
|
||||
|
||||
const module = await emscriptenModule;
|
||||
const [module, data] = await Promise.all([
|
||||
emscriptenModule,
|
||||
blobToArrayBuffer(blob),
|
||||
]);
|
||||
|
||||
const result = module.decode(data);
|
||||
if (!result) throw new Error('Decoding error');
|
||||
return result;
|
||||
|
||||
@@ -12,16 +12,20 @@
|
||||
*/
|
||||
import webpDecoder, { WebPModule } from 'codecs/webp/dec/webp_dec';
|
||||
import wasmUrl from 'url:codecs/webp/dec/webp_dec.wasm';
|
||||
import { initEmscriptenModule } from 'features/util';
|
||||
import { initEmscriptenModule, blobToArrayBuffer } from 'features/util';
|
||||
|
||||
let emscriptenModule: Promise<WebPModule>;
|
||||
|
||||
export default async function decode(data: ArrayBuffer): Promise<ImageData> {
|
||||
export default async function decode(blob: Blob): Promise<ImageData> {
|
||||
if (!emscriptenModule) {
|
||||
emscriptenModule = initEmscriptenModule(webpDecoder, wasmUrl);
|
||||
}
|
||||
|
||||
const module = await emscriptenModule;
|
||||
const [module, data] = await Promise.all([
|
||||
emscriptenModule,
|
||||
blobToArrayBuffer(blob),
|
||||
]);
|
||||
|
||||
const result = module.decode(data);
|
||||
if (!result) throw new Error('Decoding error');
|
||||
return result;
|
||||
|
||||
5
src/features/encoders/browserGIF/client/index.ts
Normal file
5
src/features/encoders/browserGIF/client/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { canvasEncodeTest, canvasEncode } from 'client/lazy-app/util';
|
||||
import { mimeType } from '../shared/meta';
|
||||
|
||||
export const featureTest = () => canvasEncodeTest(mimeType);
|
||||
export const encode = (data: ImageData) => canvasEncode(data, mimeType);
|
||||
13
src/features/encoders/browserGIF/client/missing-types.d.ts
vendored
Normal file
13
src/features/encoders/browserGIF/client/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../../../../missing-types.d.ts" />
|
||||
13
src/features/encoders/browserGIF/client/tsconfig.json
Normal file
13
src/features/encoders/browserGIF/client/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../../../../generic-tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["esnext", "dom", "dom.iterable"],
|
||||
"types": []
|
||||
},
|
||||
"include": [
|
||||
"./*.ts",
|
||||
"../../../../client/lazy-app/util.ts",
|
||||
"../shared/*.ts"
|
||||
],
|
||||
"references": [{ "path": "../shared" }]
|
||||
}
|
||||
18
src/features/encoders/browserGIF/shared/meta.ts
Normal file
18
src/features/encoders/browserGIF/shared/meta.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export interface EncodeOptions {}
|
||||
|
||||
export const label = 'Browser GIF';
|
||||
export const mimeType = 'image/gif';
|
||||
export const extension = 'gif';
|
||||
export const defaultOptions: EncodeOptions = {};
|
||||
13
src/features/encoders/browserGIF/shared/missing-types.d.ts
vendored
Normal file
13
src/features/encoders/browserGIF/shared/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../../../../missing-types.d.ts" />
|
||||
7
src/features/encoders/browserGIF/shared/tsconfig.json
Normal file
7
src/features/encoders/browserGIF/shared/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../../../../generic-tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["webworker", "esnext"]
|
||||
},
|
||||
"references": [{ "path": "../../../" }]
|
||||
}
|
||||
6
src/features/encoders/browserJPEG/client/index.ts
Normal file
6
src/features/encoders/browserJPEG/client/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { canvasEncode } from 'client/lazy-app/util';
|
||||
import { mimeType, EncodeOptions } from '../shared/meta';
|
||||
|
||||
export function encode(data: ImageData, { quality }: EncodeOptions) {
|
||||
return canvasEncode(data, mimeType, quality);
|
||||
}
|
||||
13
src/features/encoders/browserJPEG/client/missing-types.d.ts
vendored
Normal file
13
src/features/encoders/browserJPEG/client/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../../../../missing-types.d.ts" />
|
||||
13
src/features/encoders/browserJPEG/client/tsconfig.json
Normal file
13
src/features/encoders/browserJPEG/client/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../../../../generic-tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["esnext", "dom", "dom.iterable"],
|
||||
"types": []
|
||||
},
|
||||
"include": [
|
||||
"./*.ts",
|
||||
"../../../../client/lazy-app/util.ts",
|
||||
"../shared/*.ts"
|
||||
],
|
||||
"references": [{ "path": "../shared" }]
|
||||
}
|
||||
20
src/features/encoders/browserJPEG/shared/meta.ts
Normal file
20
src/features/encoders/browserJPEG/shared/meta.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export interface EncodeOptions {
|
||||
quality: number;
|
||||
}
|
||||
|
||||
export const label = 'Browser JPEG';
|
||||
export const mimeType = 'image/jpeg';
|
||||
export const extension = 'jpg';
|
||||
export const defaultOptions: EncodeOptions = { quality: 0.75 };
|
||||
13
src/features/encoders/browserJPEG/shared/missing-types.d.ts
vendored
Normal file
13
src/features/encoders/browserJPEG/shared/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../../../../missing-types.d.ts" />
|
||||
7
src/features/encoders/browserJPEG/shared/tsconfig.json
Normal file
7
src/features/encoders/browserJPEG/shared/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../../../../generic-tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["webworker", "esnext"]
|
||||
},
|
||||
"references": [{ "path": "../../../" }]
|
||||
}
|
||||
4
src/features/encoders/browserPNG/client/index.ts
Normal file
4
src/features/encoders/browserPNG/client/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { canvasEncode } from 'client/lazy-app/util';
|
||||
import { mimeType } from '../shared/meta';
|
||||
|
||||
export const encode = (data: ImageData) => canvasEncode(data, mimeType);
|
||||
13
src/features/encoders/browserPNG/client/missing-types.d.ts
vendored
Normal file
13
src/features/encoders/browserPNG/client/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../../../../missing-types.d.ts" />
|
||||
13
src/features/encoders/browserPNG/client/tsconfig.json
Normal file
13
src/features/encoders/browserPNG/client/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../../../../generic-tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["esnext", "dom", "dom.iterable"],
|
||||
"types": []
|
||||
},
|
||||
"include": [
|
||||
"./*.ts",
|
||||
"../../../../client/lazy-app/util.ts",
|
||||
"../shared/*.ts"
|
||||
],
|
||||
"references": [{ "path": "../shared" }]
|
||||
}
|
||||
18
src/features/encoders/browserPNG/shared/meta.ts
Normal file
18
src/features/encoders/browserPNG/shared/meta.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export interface EncodeOptions {}
|
||||
|
||||
export const label = 'Browser PNG';
|
||||
export const mimeType = 'image/png';
|
||||
export const extension = 'png';
|
||||
export const defaultOptions: EncodeOptions = {};
|
||||
13
src/features/encoders/browserPNG/shared/missing-types.d.ts
vendored
Normal file
13
src/features/encoders/browserPNG/shared/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../../../../missing-types.d.ts" />
|
||||
7
src/features/encoders/browserPNG/shared/tsconfig.json
Normal file
7
src/features/encoders/browserPNG/shared/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../../../../generic-tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["webworker", "esnext"]
|
||||
},
|
||||
"references": [{ "path": "../../../" }]
|
||||
}
|
||||
15
src/features/encoders/identity/shared/meta.ts
Normal file
15
src/features/encoders/identity/shared/meta.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export interface EncodeOptions {}
|
||||
export const label = 'Original image';
|
||||
export const defaultOptions: EncodeOptions = {};
|
||||
13
src/features/encoders/identity/shared/missing-types.d.ts
vendored
Normal file
13
src/features/encoders/identity/shared/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/// <reference path="../../../../../missing-types.d.ts" />
|
||||
7
src/features/encoders/identity/shared/tsconfig.json
Normal file
7
src/features/encoders/identity/shared/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../../../../generic-tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["webworker", "esnext"]
|
||||
},
|
||||
"references": [{ "path": "../../../" }]
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
builtinResize,
|
||||
BuiltinResizeMethod,
|
||||
drawableToImageData,
|
||||
} from 'client/util';
|
||||
} from 'client/lazy-app/util';
|
||||
import { BrowserResizeOptions, VectorResizeOptions } from '../shared/meta';
|
||||
import { getContainOffsets } from '../shared/util';
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
"lib": ["esnext", "dom", "dom.iterable"],
|
||||
"types": []
|
||||
},
|
||||
"include": ["./*.ts", "../../../../client/util.ts", "../shared/*.ts"],
|
||||
"include": [
|
||||
"./*.ts",
|
||||
"../../../../client/lazy-app/util.ts",
|
||||
"../shared/*.ts"
|
||||
],
|
||||
"references": [{ "path": "../shared" }]
|
||||
}
|
||||
|
||||
@@ -20,3 +20,7 @@ export function initEmscriptenModule<T extends EmscriptenWasm.Module>(
|
||||
locateFile: () => wasmUrl,
|
||||
});
|
||||
}
|
||||
|
||||
export function blobToArrayBuffer(blob: Blob): Promise<ArrayBuffer> {
|
||||
return new Response(blob).arrayBuffer();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user