# Conflicts:
#	codecs/cpp.Dockerfile
#	codecs/imagequant/example.html
#	codecs/webp/dec/webp_dec.d.ts
#	codecs/webp/dec/webp_dec.js
#	codecs/webp/dec/webp_dec.wasm
#	codecs/webp/enc/webp_enc.d.ts
#	codecs/webp/enc/webp_enc.js
#	codecs/webp/enc/webp_enc.wasm
#	package-lock.json
#	package.json
#	src/codecs/tiny.webp
#	src_old/codecs/encoders.ts
#	src_old/codecs/processor-worker/tiny.avif
#	src_old/codecs/processor-worker/tiny.webp
#	src_old/codecs/tiny.webp
#	src_old/components/compress/index.tsx
#	src_old/lib/util.ts
#	src_old/sw/util.ts
This commit is contained in:
Jake Archibald
2020-09-16 09:59:24 +01:00
parent dfee848a39
commit a6477b82fc
202 changed files with 10435 additions and 16953 deletions

7
.gitignore vendored
View File

@@ -1,6 +1,9 @@
.tmp
node_modules node_modules
/build
/*.log
*.scss.d.ts *.scss.d.ts
*.css.d.ts *.css.d.ts
build
*.o *.o
# Auto-generated by lib/image-worker-plugin.js
src/image-worker/index.ts

2
.nvmrc
View File

@@ -1 +1 @@
10.16.2 12.18.3

4
.prettierrc.json Normal file
View File

@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

View File

@@ -1,18 +0,0 @@
# Long-term cache by default.
/*
Cache-Control: max-age=31536000
# And here are the exceptions:
/
Cache-Control: no-cache
/serviceworker.js
Cache-Control: no-cache
/manifest.json
Cache-Control: must-revalidate, max-age=3600
# URLs in /assets do not include a hash and are mutable.
# But it isn't a big deal if the user gets an old version.
/assets/*
Cache-Control: must-revalidate, max-age=3600

View File

@@ -1,4 +1,4 @@
FROM emscripten/emsdk:1.40.0 FROM emscripten/emsdk:2.0.3
RUN apt-get update && apt-get install -qqy autoconf libtool pkg-config RUN apt-get update && apt-get install -qqy autoconf libtool pkg-config
ENV CFLAGS "-Os -flto" ENV CFLAGS "-Os -flto"
ENV CXXFLAGS "${CFLAGS} -std=c++17" ENV CXXFLAGS "${CFLAGS} -std=c++17"

View File

@@ -18,7 +18,9 @@ all: $(OUT_JS)
--closure 1 \ --closure 1 \
-s ALLOW_MEMORY_GROWTH=1 \ -s ALLOW_MEMORY_GROWTH=1 \
-s MODULARIZE=1 \ -s MODULARIZE=1 \
-s 'EXPORT_NAME="$(basename $(@F))"' \ -s TEXTDECODER=2 \
-s ENVIRONMENT='worker' \
-s EXPORT_ES6=1 \
-o $@ \ -o $@ \
$+ $+

View File

@@ -1,18 +1,17 @@
<!doctype html> <!DOCTYPE html>
<style> <style>
canvas { canvas {
image-rendering: pixelated; image-rendering: pixelated;
} }
</style> </style>
<script src='imagequant.js'></script> <script type="module">
<script> import imagequant from './imagequant.js';
const Module = imagequant();
async function loadImage(src) { async function loadImage(src) {
// Load image // Load image
const img = document.createElement('img'); const img = document.createElement('img');
img.src = src; img.src = src;
await new Promise(resolve => img.onload = resolve); await new Promise((resolve) => (img.onload = resolve));
// Make canvas same size as image // Make canvas same size as image
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
[canvas.width, canvas.height] = [img.width, img.height]; [canvas.width, canvas.height] = [img.width, img.height];
@@ -22,19 +21,32 @@
return ctx.getImageData(0, 0, img.width, img.height); return ctx.getImageData(0, 0, img.width, img.height);
} }
Module.onRuntimeInitialized = async _ => { async function main() {
console.log('Version:', Module.version().toString(16)); const module = await imagequant();
console.log('Version:', module.version().toString(16));
const image = await loadImage('../example.png'); const image = await loadImage('../example.png');
// const rawImage = Module.quantize(image.data, image.width, image.height, 256, 1.0); const rawImage = module.quantize(
const rawImage = Module.zx_quantize(image.data, image.width, image.height, 1.0); image.data,
image.width,
image.height,
256,
1.0,
);
console.log('done'); console.log('done');
const imageData = new ImageData(new Uint8ClampedArray(rawImage.buffer), image.width, image.height); const imageData = new ImageData(
new Uint8ClampedArray(rawImage.buffer),
image.width,
image.height,
);
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = image.width; canvas.width = image.width;
canvas.height = image.height; canvas.height = image.height;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
ctx.putImageData(imageData, 0, 0); ctx.putImageData(imageData, 0, 0);
document.body.appendChild(canvas); document.body.appendChild(canvas);
}; }
main();
</script> </script>

View File

@@ -1,6 +1,19 @@
interface QuantizerModule extends EmscriptenWasm.Module { export interface QuantizerModule extends EmscriptenWasm.Module {
quantize(data: BufferSource, width: number, height: number, numColors: number, dither: number): Uint8ClampedArray; quantize(
zx_quantize(data: BufferSource, width: number, height: number, dither: number): Uint8ClampedArray; data: BufferSource,
width: number,
height: number,
numColors: number,
dither: number,
): Uint8ClampedArray;
zx_quantize(
data: BufferSource,
width: number,
height: number,
dither: number,
): Uint8ClampedArray;
} }
export default function(opts: EmscriptenWasm.ModuleOpts): QuantizerModule; declare var moduleFactory: EmscriptenWasm.ModuleFactory<QuantizerModule>;
export default moduleFactory;

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -14,11 +14,13 @@ all: $(OUT_JS)
-I $(CODEC_DIR) \ -I $(CODEC_DIR) \
${CXXFLAGS} \ ${CXXFLAGS} \
${LDFLAGS} \ ${LDFLAGS} \
--bind \
--closure 1 \ --closure 1 \
--bind \
-s ALLOW_MEMORY_GROWTH=1 \ -s ALLOW_MEMORY_GROWTH=1 \
-s MODULARIZE=1 \ -s MODULARIZE=1 \
-s 'EXPORT_NAME="$(basename $(@F))"' \ -s TEXTDECODER=2 \
-s ENVIRONMENT='worker' \
-s EXPORT_ES6=1 \
-o $@ \ -o $@ \
$+ $+

View File

@@ -1,13 +1,12 @@
<!doctype html> <!DOCTYPE html>
<script src='mozjpeg_enc.js'></script> <script type="module">
<script> import mozjpeg_enc from './mozjpeg_enc.js';
const module = mozjpeg_enc();
async function loadImage(src) { async function loadImage(src) {
// Load image // Load image
const img = document.createElement('img'); const img = document.createElement('img');
img.src = src; img.src = src;
await new Promise(resolve => img.onload = resolve); await new Promise((resolve) => (img.onload = resolve));
// Make canvas same size as image // Make canvas same size as image
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
[canvas.width, canvas.height] = [img.width, img.height]; [canvas.width, canvas.height] = [img.width, img.height];
@@ -17,7 +16,9 @@
return ctx.getImageData(0, 0, img.width, img.height); return ctx.getImageData(0, 0, img.width, img.height);
} }
module.onRuntimeInitialized = async _ => { async function main() {
const module = await mozjpeg_enc();
console.log('Version:', module.version().toString(16)); console.log('Version:', module.version().toString(16));
const image = await loadImage('../example.png'); const image = await loadImage('../example.png');
const result = module.encode(image.data, image.width, image.height, { const result = module.encode(image.data, image.width, image.height, {
@@ -39,10 +40,12 @@
chroma_quality: 75, chroma_quality: 75,
}); });
const blob = new Blob([result], {type: 'image/jpeg'}); const blob = new Blob([result], { type: 'image/jpeg' });
const blobURL = URL.createObjectURL(blob); const blobURL = URL.createObjectURL(blob);
const img = document.createElement('img'); const img = document.createElement('img');
img.src = blobURL; img.src = blobURL;
document.body.appendChild(img); document.body.appendChild(img);
}; }
main();
</script> </script>

View File

@@ -1,7 +1,14 @@
import { EncodeOptions } from '../../src/codecs/mozjpeg/encoder-meta'; import { EncodeOptions } from 'image-worker/mozjpegEncode';
interface MozJPEGModule extends EmscriptenWasm.Module { export interface MozJPEGModule extends EmscriptenWasm.Module {
encode(data: BufferSource, width: number, height: number, options: EncodeOptions): Uint8Array; encode(
data: BufferSource,
width: number,
height: number,
options: EncodeOptions,
): Uint8Array;
} }
export default function(opts: EmscriptenWasm.ModuleOpts): MozJPEGModule; declare var moduleFactory: EmscriptenWasm.ModuleFactory<MozJPEGModule>;
export default moduleFactory;

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -1,4 +1,4 @@
CODEC_URL := https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-1.0.2.tar.gz CODEC_URL := https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-1.1.0.tar.gz
CODEC_DIR = node_modules/libwebp CODEC_DIR = node_modules/libwebp
CODEC_OUT_RELATIVE = src/.libs/libwebp.a CODEC_OUT_RELATIVE = src/.libs/libwebp.a
CODEC_OUT := $(addprefix $(CODEC_DIR)/, $(CODEC_OUT_RELATIVE)) CODEC_OUT := $(addprefix $(CODEC_DIR)/, $(CODEC_OUT_RELATIVE))
@@ -18,7 +18,9 @@ all: $(OUT_JS)
--closure 1 \ --closure 1 \
-s ALLOW_MEMORY_GROWTH=1 \ -s ALLOW_MEMORY_GROWTH=1 \
-s MODULARIZE=1 \ -s MODULARIZE=1 \
-s 'EXPORT_NAME="$(basename $(@F))"' \ -s TEXTDECODER=2 \
-s ENVIRONMENT='worker' \
-s EXPORT_ES6=1 \
-o $@ \ -o $@ \
$+ $+

View File

@@ -1,22 +1,24 @@
<!doctype html> <!DOCTYPE html>
<script src='webp_dec.js'></script> <script type="module">
<script> import webp_dec from './webp_dec.js';
const Module = webp_dec();
async function loadFile(src) { async function loadFile(src) {
const resp = await fetch(src); const resp = await fetch(src);
return await resp.arrayBuffer(); return await resp.arrayBuffer();
} }
Module.onRuntimeInitialized = async _ => { async function main() {
console.log('Version:', Module.version().toString(16)); const module = await webp_dec();
console.log('Version:', module.version().toString(16));
const image = await loadFile('../../example.webp'); const image = await loadFile('../../example.webp');
const imageData = Module.decode(image); const imageData = module.decode(image);
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = result.width; canvas.width = imageData.width;
canvas.height = result.height; canvas.height = imageData.height;
document.body.appendChild(canvas); document.body.appendChild(canvas);
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
ctx.putImageData(imageData, 0, 0); ctx.putImageData(imageData, 0, 0);
}; }
main();
</script> </script>

View File

@@ -1,5 +1,7 @@
interface WebPModule extends EmscriptenWasm.Module { export interface WebPModule extends EmscriptenWasm.Module {
decode(data: BufferSource): ImageData | null; decode(data: BufferSource): ImageData | null;
} }
export default function(opts: EmscriptenWasm.ModuleOpts): WebPModule; declare var moduleFactory: EmscriptenWasm.ModuleFactory<WebPModule>;
export default moduleFactory;

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -1,13 +1,12 @@
<!doctype html> <!DOCTYPE html>
<script src='webp_enc.js'></script> <script type="module">
<script> import webp_enc from './webp_enc.js';
const module = webp_enc();
async function loadImage(src) { async function loadImage(src) {
// Load image // Load image
const img = document.createElement('img'); const img = document.createElement('img');
img.src = src; img.src = src;
await new Promise(resolve => img.onload = resolve); await new Promise((resolve) => (img.onload = resolve));
// Make canvas same size as image // Make canvas same size as image
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
[canvas.width, canvas.height] = [img.width, img.height]; [canvas.width, canvas.height] = [img.width, img.height];
@@ -17,7 +16,9 @@
return ctx.getImageData(0, 0, img.width, img.height); return ctx.getImageData(0, 0, img.width, img.height);
} }
module.onRuntimeInitialized = async _ => { async function main() {
const module = await webp_enc();
console.log('Version:', module.version().toString(16)); console.log('Version:', module.version().toString(16));
const image = await loadImage('../../example.png'); const image = await loadImage('../../example.png');
const result = module.encode(image.data, image.width, image.height, { const result = module.encode(image.data, image.width, image.height, {
@@ -50,11 +51,13 @@
use_sharp_yuv: 0, use_sharp_yuv: 0,
}); });
console.log('size', result.length); console.log('size', result.length);
const blob = new Blob([result], {type: 'image/webp'}); const blob = new Blob([result], { type: 'image/webp' });
const blobURL = URL.createObjectURL(blob); const blobURL = URL.createObjectURL(blob);
const img = document.createElement('img'); const img = document.createElement('img');
img.src = blobURL; img.src = blobURL;
document.body.appendChild(img); document.body.appendChild(img);
}; }
main();
</script> </script>

View File

@@ -1,7 +1,14 @@
import { EncodeOptions } from '../../../src/codecs/webp/encoder-meta'; import { EncodeOptions } from 'image-worker/webpEncode';
interface WebPModule extends EmscriptenWasm.Module { export interface WebPModule extends EmscriptenWasm.Module {
encode(data: BufferSource, width: number, height: number, options: EncodeOptions): Uint8Array | null; encode(
data: BufferSource,
width: number,
height: number,
options: EncodeOptions,
): Uint8Array | null;
} }
export default function(opts: EmscriptenWasm.ModuleOpts): WebPModule; declare var moduleFactory: EmscriptenWasm.ModuleFactory<WebPModule>;
export default moduleFactory;

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -1,76 +0,0 @@
const DtsCreator = require('typed-css-modules');
const chokidar = require('chokidar');
const util = require('util');
const sass = require('node-sass');
const normalizePath = require('normalize-path');
const sassRender = util.promisify(sass.render);
async function sassToCss(path) {
const result = await sassRender({ file: path });
return result.css;
}
/**
* @typedef {Object} Opts
* @property {boolean} watch Watch for changes
*/
/**
* Create typing files for CSS & SCSS.
*
* @param {string[]} rootPaths Paths to search within
* @param {Opts} [opts={}] Options.
*/
function addCssTypes(rootPaths, opts = {}) {
return new Promise((resolve) => {
const { watch = false } = opts;
const paths = [];
const preReadyPromises = [];
let ready = false;
for (const rootPath of rootPaths) {
const rootPathUnix = normalizePath(rootPath);
// Look for scss & css in each path.
paths.push(rootPathUnix + '/**/*.scss');
paths.push(rootPathUnix + '/**/*.css');
}
// For simplicity, the watcher is used even if we're not watching.
// If we're not watching, we stop the watcher after the initial files are found.
const watcher = chokidar.watch(paths, {
// Avoid processing already-processed files.
ignored: '*.d.*',
// Without this, travis and netlify builds never complete. I'm not sure why, but it might be
// related to https://github.com/paulmillr/chokidar/pull/758
persistent: watch,
});
function change(path) {
const promise = (async function() {
const creator = new DtsCreator({ camelCase: true });
const result = path.endsWith('.scss') ?
await creator.create(path, await sassToCss(path)) :
await creator.create(path);
await result.writeFile();
})();
if (!ready) preReadyPromises.push(promise);
}
watcher.on('change', change);
watcher.on('add', change);
// 'ready' is when events have been fired for file discovery.
watcher.on('ready', () => {
ready = true;
// Wait for the current set of processing to finish.
Promise.all(preReadyPromises).then(resolve);
// And if we're not watching, close the watcher.
if (!watch) watcher.close();
});
});
}
module.exports = addCssTypes;

View File

@@ -1,47 +0,0 @@
const path = require('path');
const fs = require('fs');
const ejs = require('ejs');
const AssetsPlugin = require('assets-webpack-plugin');
module.exports = class AssetTemplatePlugin extends AssetsPlugin {
constructor(options) {
options = options || {};
if (!options.template) throw Error('AssetTemplatePlugin: template option is required.');
super({
useCompilerPath: true,
filename: options.filename,
processOutput: files => this._processOutput(files)
});
this._template = path.resolve(process.cwd(), options.template);
const ignore = options.ignore || /(manifest\.json|\.DS_Store)$/;
this._ignore = typeof ignore === 'function' ? ({ test: ignore }) : ignore;
}
_processOutput(files) {
const mapping = {
all: [],
byType: {},
entries: {}
};
for (const entryName in files) {
// non-entry-point-derived assets are collected under an empty string key
// since that's a bit awkward, we'll call them "assets"
const name = entryName === '' ? 'assets' : entryName;
const listing = files[entryName];
const entry = mapping.entries[name] = {
all: [],
byType: {}
};
for (let type in listing) {
const list = [].concat(listing[type]).filter(file => !this._ignore.test(file));
if (!list.length) continue;
mapping.all = mapping.all.concat(list);
mapping.byType[type] = (mapping.byType[type] || []).concat(list);
entry.all = entry.all.concat(list);
entry.byType[type] = (entry.byType[type] || []).concat(list);
}
}
mapping.files = mapping.all;
return ejs.render(fs.readFileSync(this._template, 'utf8'), mapping);
}
};

View File

@@ -1,29 +0,0 @@
let loaderUtils = require('loader-utils');
let componentPath = require.resolve('./async-component');
module.exports = function () { };
module.exports.pitch = function (remainingRequest) {
this.cacheable && this.cacheable();
let query = loaderUtils.getOptions(this) || {};
let routeName = typeof query.name === 'function' ? query.name(this.resourcePath) : null;
let name;
if (routeName !== null) {
name = routeName;
}
else if ('name' in query) {
name = query.name;
}
else if ('formatName' in query) {
name = query.formatName(this.resourcePath);
}
return `
import async from ${JSON.stringify(componentPath)};
function load(cb) {
require.ensure([], function (require) {
cb( require(${loaderUtils.stringifyRequest(this, '!!' + remainingRequest)}) );
}${name ? (', ' + JSON.stringify(name)) : ''});
}
export default async(load);
`;
};

View File

@@ -1,30 +0,0 @@
import { h, Component } from 'preact';
export default function (req) {
function Async() {
Component.call(this);
let b, old;
this.componentWillMount = () => {
b = this.base = this.nextBase || this.__b; // short circuits 1st render
req(m => {
this.setState({ child: m.default || m });
});
};
this.shouldComponentUpdate = (_, nxt) => {
nxt = nxt.child === void 0;
if (nxt && old === void 0 && !!b) {
old = h(b.nodeName, { dangerouslySetInnerHTML: { __html: b.innerHTML } });
}
else {
old = ''; // dump it
}
return !nxt;
};
this.render = (p, s) => s.child ? h(s.child, p) : old;
}
(Async.prototype = new Component()).constructor = Async;
return Async;
}

View File

@@ -1,158 +0,0 @@
const util = require('util');
const minimatch = require('minimatch');
const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
const WebWorkerTemplatePlugin = require('webpack/lib/webworker/WebWorkerTemplatePlugin');
const ParserHelpers = require('webpack/lib/ParserHelpers');
const NAME = 'auto-sw-plugin';
const JS_TYPES = ['auto', 'esm', 'dynamic'];
/**
* Automatically finds and bundles Service Workers by looking for navigator.serviceWorker.register(..).
* An Array of webpack assets is injected into the Service Worker bundle as a `BUILD_ASSETS` global.
* Hidden and `.map` files are excluded by default, and this can be customized using the include & exclude options.
* @example
* // webpack config
* plugins: [
* new AutoSWPlugin({
* exclude: [
* '**\/.*', // don't expose hidden files (default)
* '**\/*.map', // don't precache sourcemaps (default)
* 'index.html' // don't cache the page itself
* ]
* })
* ]
* @param {Object} [options={}]
* @param {string[]} [options.exclude] Minimatch pattern(s) of which assets to omit from BUILD_ASSETS.
* @param {string[]} [options.include] Minimatch pattern(s) of assets to allow in BUILD_ASSETS.
*/
module.exports = class AutoSWPlugin {
constructor(options) {
this.options = Object.assign({
exclude: [
'**/*.map',
'**/.*'
]
}, options || {});
}
apply(compiler) {
const serviceWorkers = [];
compiler.hooks.emit.tapPromise(NAME, compilation => this.emit(compiler, compilation, serviceWorkers));
compiler.hooks.normalModuleFactory.tap(NAME, (factory) => {
for (const type of JS_TYPES) {
factory.hooks.parser.for(`javascript/${type}`).tap(NAME, parser => {
let counter = 0;
const processRegisterCall = expr => {
const dep = parser.evaluateExpression(expr.arguments[0]);
if (!dep.isString()) {
parser.state.module.warnings.push({
message: 'navigator.serviceWorker.register() will only be bundled if passed a String literal.'
});
return false;
}
const filename = dep.string;
const outputFilename = this.options.filename || 'serviceworker.js'
const context = parser.state.current.context;
serviceWorkers.push({
outputFilename,
filename,
context
});
const id = `__webpack__serviceworker__${++counter}`;
ParserHelpers.toConstantDependency(parser, id)(expr.arguments[0]);
return ParserHelpers.addParsedVariableToModule(parser, id, '__webpack_public_path__ + ' + JSON.stringify(outputFilename));
};
parser.hooks.call.for('navigator.serviceWorker.register').tap(NAME, processRegisterCall);
parser.hooks.call.for('self.navigator.serviceWorker.register').tap(NAME, processRegisterCall);
parser.hooks.call.for('window.navigator.serviceWorker.register').tap(NAME, processRegisterCall);
});
}
});
}
createFilter(list) {
const filters = [].concat(list);
for (let i=0; i<filters.length; i++) {
if (typeof filters[i] === 'string') {
filters[i] = minimatch.filter(filters[i]);
}
}
return filters;
}
async emit(compiler, compilation, serviceWorkers) {
let assetMapping = Object.keys(compilation.assets);
if (this.options.include) {
const filters = this.createFilter(this.options.include);
assetMapping = assetMapping.filter(filename => {
for (const filter of filters) {
if (filter(filename)) return true;
}
return false;
});
}
if (this.options.exclude) {
const filters = this.createFilter(this.options.exclude);
assetMapping = assetMapping.filter(filename => {
for (const filter of filters) {
if (filter(filename)) return false;
}
return true;
});
}
await Promise.all(serviceWorkers.map(
(serviceWorker, index) => this.compileServiceWorker(compiler, compilation, serviceWorker, index, assetMapping)
));
}
async compileServiceWorker(compiler, compilation, options, index, assetMapping) {
const entryFilename = options.filename;
const chunkFilename = compiler.options.output.chunkFilename.replace(/\.([a-z]+)$/i, '.serviceworker.$1');
const workerOptions = {
filename: options.outputFilename, // chunkFilename.replace(/\.?\[(?:chunkhash|contenthash|hash)(:\d+(?::\d+)?)?\]/g, ''),
chunkFilename: this.options.chunkFilename || chunkFilename,
globalObject: 'self'
};
const childCompiler = compilation.createChildCompiler(NAME, { filename: workerOptions.filename });
(new WebWorkerTemplatePlugin(workerOptions)).apply(childCompiler);
/* The duplication DefinePlugin ends up causing is problematic (it doesn't hoist injections), so we'll do it manually. */
// (new DefinePlugin({
// BUILD_ASSETS: JSON.stringify(assetMapping)
// })).apply(childCompiler);
(new SingleEntryPlugin(options.context, entryFilename, workerOptions.filename)).apply(childCompiler);
const subCache = `subcache ${__dirname} ${entryFilename} ${index}`;
let childCompilation;
childCompiler.hooks.compilation.tap(NAME, c => {
childCompilation = c;
if (childCompilation.cache) {
if (!childCompilation.cache[subCache]) childCompilation.cache[subCache] = {};
childCompilation.cache = childCompilation.cache[subCache];
}
});
await (util.promisify(childCompiler.runAsChild.bind(childCompiler)))();
const versionVar = this.options.version ?
`var VERSION = ${JSON.stringify(this.options.version)};` : '';
const original = childCompilation.assets[workerOptions.filename].source();
const source = `${versionVar}var BUILD_ASSETS=${JSON.stringify(assetMapping)};${original}`;
childCompilation.assets[workerOptions.filename] = {
source: () => source,
size: () => Buffer.byteLength(source, 'utf8')
};
Object.assign(compilation.assets, childCompilation.assets);
}
};

View File

@@ -1,30 +0,0 @@
const fs = require('fs');
/** A Webpack plugin to refresh file mtime values from disk before compiling.
* This is used in order to account for SCSS-generated .d.ts files written
* as part of compilation so they trigger only a single recompile per write.
*
* All credit for the technique and implementation goes to @reiv. See:
* https://github.com/Jimdo/typings-for-css-modules-loader/issues/48#issuecomment-347036461
*/
module.exports = class WatchTimestampsPlugin {
constructor(patterns) {
this.patterns = patterns;
}
apply(compiler) {
compiler.hooks.watchRun.tapAsync('watch-timestamps-plugin', (watch, callback) => {
const patterns = this.patterns;
const timestamps = watch.fileTimestamps;
for (const filepath of timestamps) {
if (patterns.some(pat => pat instanceof RegExp ? pat.test(filepath) : filepath.indexOf(pat) === 0)) {
let time = fs.statSync(filepath).mtime;
if (timestamps instanceof Map) timestamps.set(filepath, time);
else timestamps[filepath] = time;
}
}
callback();
});
}
};

View File

@@ -1,7 +1,11 @@
// These types roughly model the object that the JS files generated by Emscripten define. Copied from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/emscripten/index.d.ts and turned into a type definition rather than a global to support our way of using Emscripten. // These types roughly model the object that the JS files generated by Emscripten define. Copied from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/emscripten/index.d.ts and turned into a type definition rather than a global to support our way of using Emscripten.
// TODO(@surma): Upstream this? // TODO(@surma): Upstream this?
declare namespace EmscriptenWasm { declare namespace EmscriptenWasm {
type EnvironmentType = "WEB" | "NODE" | "SHELL" | "WORKER"; type ModuleFactory<T extends Module = Module> = (
moduleOverrides?: ModuleOpts,
) => Promise<T>;
type EnvironmentType = 'WEB' | 'NODE' | 'SHELL' | 'WORKER';
// Options object for modularized Emscripten files. Shoe-horned by @surma. // Options object for modularized Emscripten files. Shoe-horned by @surma.
// FIXME: This an incomplete definition! // FIXME: This an incomplete definition!
@@ -16,9 +20,9 @@ declare namespace EmscriptenWasm {
printErr(str: string): void; printErr(str: string): void;
arguments: string[]; arguments: string[];
environment: EnvironmentType; environment: EnvironmentType;
preInit: { (): void }[]; preInit: { (): void }[];
preRun: { (): void }[]; preRun: { (): void }[];
postRun: { (): void }[]; postRun: { (): void }[];
preinitializedWebGLContext: WebGLRenderingContext; preinitializedWebGLContext: WebGLRenderingContext;
noInitialRun: boolean; noInitialRun: boolean;
noExitRuntime: boolean; noExitRuntime: boolean;
@@ -27,17 +31,25 @@ declare namespace EmscriptenWasm {
wasmBinary: ArrayBuffer; wasmBinary: ArrayBuffer;
destroy(object: object): void; destroy(object: object): void;
getPreloadedPackage(remotePackageName: string, remotePackageSize: number): ArrayBuffer; getPreloadedPackage(
remotePackageName: string,
remotePackageSize: number,
): ArrayBuffer;
instantiateWasm( instantiateWasm(
imports: WebAssembly.Imports, imports: WebAssembly.Imports,
successCallback: (module: WebAssembly.Module) => void successCallback: (module: WebAssembly.Module) => void,
): WebAssembly.Exports; ): WebAssembly.Exports;
locateFile(url: string): string; locateFile(url: string): string;
onCustomMessage(event: MessageEvent): void; onCustomMessage(event: MessageEvent): void;
Runtime: any; Runtime: any;
ccall(ident: string, returnType: string | null, argTypes: string[], args: any[]): any; ccall(
ident: string,
returnType: string | null,
argTypes: string[],
args: any[],
): any;
cwrap(ident: string, returnType: string | null, argTypes: string[]): any; cwrap(ident: string, returnType: string | null, argTypes: string[]): any;
setValue(ptr: number, value: any, type: string, noSafe?: boolean): void; setValue(ptr: number, value: any, type: string, noSafe?: boolean): void;
@@ -50,7 +62,12 @@ declare namespace EmscriptenWasm {
ALLOC_NONE: number; ALLOC_NONE: number;
allocate(slab: any, types: string, allocator: number, ptr: number): number; allocate(slab: any, types: string, allocator: number, ptr: number): number;
allocate(slab: any, types: string[], allocator: number, ptr: number): number; allocate(
slab: any,
types: string[],
allocator: number,
ptr: number,
): number;
Pointer_stringify(ptr: number, length?: number): string; Pointer_stringify(ptr: number, length?: number): string;
UTF16ToString(ptr: number): string; UTF16ToString(ptr: number): string;
@@ -67,7 +84,7 @@ declare namespace EmscriptenWasm {
HEAP8: Int8Array; HEAP8: Int8Array;
HEAP16: Int16Array; HEAP16: Int16Array;
HEAP32: Int32Array; HEAP32: Int32Array;
HEAPU8: Uint8Array; HEAPU8: Uint8Array;
HEAPU16: Uint16Array; HEAPU16: Uint16Array;
HEAPU32: Uint32Array; HEAPU32: Uint32Array;
HEAPF32: Float32Array; HEAPF32: Float32Array;
@@ -84,16 +101,23 @@ declare namespace EmscriptenWasm {
addOnPostRun(cb: () => any): void; addOnPostRun(cb: () => any): void;
// Tools // Tools
intArrayFromString(stringy: string, dontAddNull?: boolean, length?: number): number[]; intArrayFromString(
stringy: string,
dontAddNull?: boolean,
length?: number,
): number[];
intArrayToString(array: number[]): string; intArrayToString(array: number[]): string;
writeStringToMemory(str: string, buffer: number, dontAddNull: boolean): void; writeStringToMemory(
str: string,
buffer: number,
dontAddNull: boolean,
): void;
writeArrayToMemory(array: number[], buffer: number): void; writeArrayToMemory(array: number[], buffer: number): void;
writeAsciiToMemory(str: string, buffer: number, dontAddNull: boolean): void; writeAsciiToMemory(str: string, buffer: number, dontAddNull: boolean): void;
addRunDependency(id: any): void; addRunDependency(id: any): void;
removeRunDependency(id: any): void; removeRunDependency(id: any): void;
preloadedImages: any; preloadedImages: any;
preloadedAudios: any; preloadedAudios: any;
@@ -104,4 +128,3 @@ declare namespace EmscriptenWasm {
onRuntimeInitialized: () => void | null; onRuntimeInitialized: () => void | null;
} }
} }

22
generic-tsconfig.json Normal file
View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2019",
"downlevelIteration": true,
"module": "esnext",
"jsx": "react",
"jsxFactory": "h",
"strict": true,
"moduleResolution": "node",
"composite": true,
"declarationMap": true,
"baseUrl": "./",
"rootDir": "./",
"outDir": ".tmp/ts",
"allowSyntheticDefaultImports": true,
"paths": {
"static-build/*": ["src/static-build/*"],
"image-worker/*": ["src/image-worker/*"],
"worker-main-shared/*": ["src/worker-main-shared/*"]
}
}
}

23
global.d.ts vendored
View File

@@ -1,23 +0,0 @@
declare const __webpack_public_path__: string;
declare const PRERENDER: boolean;
declare interface NodeModule {
hot: any;
}
declare interface Window {
STATE: any;
ga: typeof ga;
}
declare namespace JSX {
interface Element { }
interface IntrinsicElements { }
interface HTMLAttributes {
decoding?: string;
}
}
declare module 'classnames' {
export default function classnames(...args: any[]): string;
}

75
lib/asset-plugin.js Normal file
View File

@@ -0,0 +1,75 @@
/**
* 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.
*/
import { promises as fs } from 'fs';
import { basename } from 'path';
const defaultOpts = {
prefix: 'url',
};
export default function urlPlugin(opts) {
opts = Object.assign({}, defaultOpts, opts);
/** @type {Map<string, Buffer>} */
let assetIdToSourceBuffer;
const prefix = opts.prefix + ':';
return {
name: 'url-plugin',
buildStart() {
assetIdToSourceBuffer = new Map();
},
augmentChunkHash(info) {
// Get the sources for all assets imported by this chunk.
const buffers = Object.keys(info.modules)
.map((moduleId) => assetIdToSourceBuffer.get(moduleId))
.filter(Boolean);
if (buffers.length === 0) return;
for (const moduleId of Object.keys(info.modules)) {
const buffer = assetIdToSourceBuffer.get(moduleId);
if (buffer) buffers.push(buffer);
}
const combinedBuffer =
buffers.length === 1 ? buffers[0] : Buffer.concat(buffers);
return combinedBuffer;
},
async resolveId(id, importer) {
if (!id.startsWith(prefix)) return;
const realId = id.slice(prefix.length);
const resolveResult = await this.resolve(realId, importer);
if (!resolveResult) {
throw Error(`Cannot find ${realId}`);
}
// Add an additional .js to the end so it ends up with .js at the end in the _virtual folder.
return prefix + resolveResult.id + '.js';
},
async load(id) {
if (!id.startsWith(prefix)) return;
const realId = id.slice(prefix.length, -'.js'.length);
const source = await fs.readFile(realId);
assetIdToSourceBuffer.set(id, source);
this.addWatchFile(realId);
return `export default import.meta.ROLLUP_FILE_URL_${this.emitFile({
type: 'asset',
source,
name: basename(realId),
})}`;
},
};
}

163
lib/client-bundle-plugin.js Normal file
View File

@@ -0,0 +1,163 @@
/**
* 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.
*/
import rollup from 'rollup';
const prefix = 'client-bundle:';
const entryPathPlaceholder = 'CLIENT_BUNDLE_PLUGIN_ENTRY_PATH';
const importsPlaceholder = 'CLIENT_BUNDLE_PLUGIN_IMPORTS';
export function getDependencies(clientOutput, item) {
const crawlDependencies = new Set([item.fileName]);
for (const fileName of crawlDependencies) {
const chunk = clientOutput.find((v) => v.fileName === fileName);
for (const dep of chunk.imports) {
crawlDependencies.add(dep);
}
}
// Don't add self as dependency
crawlDependencies.delete(item.fileName);
return [...crawlDependencies];
}
export default function (inputOptions, outputOptions, resolveFileUrl) {
let cache;
let entryPointPlaceholderMap;
let exportCounter;
let clientBundle;
let clientOutput;
return {
name: 'client-bundle',
buildStart() {
entryPointPlaceholderMap = new Map();
exportCounter = 0;
},
async resolveId(id, importer) {
if (!id.startsWith(prefix)) return null;
const realId = id.slice(prefix.length);
const resolveResult = await this.resolve(realId, importer);
// Add an additional .js to the end so it ends up with .js at the end in the _virtual folder.
if (resolveResult) return prefix + resolveResult.id + '.js';
// This Rollup couldn't resolve it, but maybe the inner one can.
return id + '.js';
},
load(id) {
if (!id.startsWith(prefix)) return;
const realId = id.slice(prefix.length, -'.js'.length);
exportCounter++;
entryPointPlaceholderMap.set(exportCounter, realId);
return [
`export default import.meta.${entryPathPlaceholder + exportCounter};`,
`export const imports = import.meta.${
importsPlaceholder + exportCounter
};`,
].join('\n');
},
async buildEnd(error) {
const entryPoints = [...entryPointPlaceholderMap.values()];
// The static-build is done, so now we can perform our client build.
// Exit early if there's nothing to build.
if (error || entryPoints.length === 0) return;
clientBundle = await rollup.rollup({
...inputOptions,
cache,
input: entryPoints,
});
cache = clientBundle.cache;
},
async renderStart(staticBuildOutputOpts) {
// The static-build has started generating output, so we can do the same for our client build.
// Exit early if there's nothing to build.
if (!clientBundle) return;
const copiedOutputOptions = {
assetFileNames: staticBuildOutputOpts.assetFileNames,
};
clientOutput = (
await clientBundle.generate({
...copiedOutputOptions,
...outputOptions,
})
).output;
},
resolveImportMeta(property, { moduleId, format }) {
// Pick up the placeholder exports we created earlier, and fill in the correct details.
let num = undefined;
if (property.startsWith(entryPathPlaceholder)) {
num = Number(property.slice(entryPathPlaceholder.length));
} else if (property.startsWith(importsPlaceholder)) {
num = Number(property.slice(importsPlaceholder.length));
} else {
// This isn't one of our placeholders.
return;
}
const id = entryPointPlaceholderMap.get(num);
const clientEntry = clientOutput.find(
(item) => item.facadeModuleId === id,
);
if (property.startsWith(entryPathPlaceholder)) {
return resolveFileUrl({
fileName: clientEntry.fileName,
moduleId,
format,
});
}
const dependencies = getDependencies(clientOutput, clientEntry);
return (
'[' +
dependencies
.map((item) => {
const entry = clientOutput.find((v) => v.fileName === item);
return resolveFileUrl({
fileName: entry.fileName,
moduleId,
format: outputOptions.format,
});
})
.join(',') +
']'
);
},
async generateBundle(options, bundle) {
// Exit early if there's nothing to build.
if (!clientOutput) return;
// Copy everything from the client bundle into the main bundle.
for (const clientEntry of clientOutput) {
// Skip if the file already exists
if (clientEntry.fileName in bundle) continue;
this.emitFile({
type: 'asset',
source: clientEntry.code || clientEntry.source,
fileName: clientEntry.fileName,
});
}
},
};
}

195
lib/css-plugin.js Normal file
View File

@@ -0,0 +1,195 @@
/**
* 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.
*/
import { promises as fsp, readFileSync } from 'fs';
import { createHash } from 'crypto';
import { promisify } from 'util';
import { parse as parsePath, resolve as resolvePath, dirname } from 'path';
import postcss from 'postcss';
import postCSSNested from 'postcss-nested';
import postCSSUrl from 'postcss-url';
import postCSSModules from 'postcss-modules';
import postCSSImport from 'postcss-import';
import postCSSSimpleVars from 'postcss-simple-vars';
import cssNano from 'cssnano';
import camelCase from 'lodash.camelcase';
import glob from 'glob';
const globP = promisify(glob);
const moduleSuffix = '.css';
const prefix = 'css-bundle:';
const assetRe = new RegExp('/fake/path/to/asset/([^/]+)/', 'g');
export default function (resolveFileUrl) {
/** @type {string[]} */
let emittedCSSIds;
/** @type {Map<string, string>} */
let hashToId;
/** @type {Map<string, { module: string, css: string }>} */
let pathToResult;
async function loadBundledCSS(path, rollupContext) {
const parsedPath = parsePath(path);
if (!pathToResult.has(path)) {
throw Error(`Cannot find ${path} in pathToResult`);
}
const file = pathToResult.get(path).css;
const cssResult = await postcss([
postCSSImport({
path: ['./', 'static-build/'],
load(path) {
if (!pathToResult.has(path)) {
throw Error(`Cannot find ${path} in pathToResult`);
}
return pathToResult.get(path).css;
},
}),
postCSSUrl({
url: ({ relativePath, url }) => {
if (/^https?:\/\//.test(url)) return url;
const parsedPath = parsePath(relativePath);
const source = readFileSync(resolvePath(dirname(path), relativePath));
const fileId = rollupContext.emitFile({
type: 'asset',
name: parsedPath.base,
source,
});
const hash = createHash('md5');
hash.update(source);
const md5 = hash.digest('hex');
hashToId.set(md5, fileId);
return `/fake/path/to/asset/${md5}/`;
},
}),
cssNano,
]).process(file, {
from: path,
});
const fileId = rollupContext.emitFile({
type: 'asset',
source: cssResult.css,
name: parsedPath.base,
});
emittedCSSIds.push(fileId);
return [
`export default import.meta.ROLLUP_FILE_URL_${fileId}`,
`export const inline = ${JSON.stringify(cssResult.css)}`,
].join('\n');
}
return {
name: 'css',
async buildStart() {
emittedCSSIds = [];
hashToId = new Map();
pathToResult = new Map();
const cssPaths = await globP('src/static-build/**/*.css', {
nodir: true,
absolute: true,
});
await Promise.all(
cssPaths.map(async (path) => {
this.addWatchFile(path);
const file = await fsp.readFile(path);
let moduleJSON;
const cssResult = await postcss([
postCSSNested,
postCSSSimpleVars(),
postCSSModules({
getJSON(_, json) {
moduleJSON = json;
},
}),
]).process(file, {
from: undefined,
});
const cssClassExports = Object.entries(moduleJSON).map(
([key, val]) =>
`export const $${camelCase(key)} = ${JSON.stringify(val)};`,
);
const defs = Object.keys(moduleJSON)
.map((key) => `export const $${camelCase(key)}: string;`)
.join('\n');
const defPath = path + '.d.ts';
const currentDefFileContent = await fsp
.readFile(defPath, { encoding: 'utf8' })
.catch(() => undefined);
// Only write the file if contents have changed, otherwise it causes a loop with
// TypeScript's file watcher.
if (defs !== currentDefFileContent) {
await fsp.writeFile(defPath, defs);
}
pathToResult.set(path, {
module: cssClassExports.join('\n'),
css: cssResult.css,
});
}),
);
},
async resolveId(id, importer) {
if (!id.startsWith(prefix)) return;
const resolved = await this.resolve(id.slice(prefix.length), importer);
if (!resolved) throw Error(`Couldn't resolve ${id} from ${importer}`);
return prefix + resolved.id;
},
async load(id) {
if (id.startsWith(prefix)) {
return loadBundledCSS(id.slice(prefix.length), this);
}
if (id.endsWith(moduleSuffix)) {
if (!pathToResult.has(id)) {
throw Error(`Cannot find ${id} in pathToResult`);
}
return pathToResult.get(id).module;
}
},
async generateBundle(options, bundle) {
const cssAssets = emittedCSSIds.map((id) => this.getFileName(id));
for (const cssAsset of cssAssets) {
bundle[cssAsset].source = bundle[cssAsset].source.replace(
assetRe,
(_, p1) =>
resolveFileUrl({
fileName: this.getFileName(hashToId.get(p1)),
}),
);
}
for (const item of Object.values(bundle)) {
if (item.type === 'asset') continue;
item.code = item.code.replace(assetRe, (match, p1) =>
resolveFileUrl({
fileName: this.getFileName(hashToId.get(p1)),
}),
);
}
},
};
}

37
lib/emit-files-plugin.js Normal file
View File

@@ -0,0 +1,37 @@
/**
* 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.
*/
import * as path from 'path';
import { promises as fs } from 'fs';
import glob from 'glob';
import { promisify } from 'util';
const globP = promisify(glob);
export default function emitFiles({ root, include }) {
return {
name: 'emit-files-plugin',
async buildStart() {
const paths = await globP(include, { nodir: true, cwd: root });
await Promise.all(
paths.map(async (filePath) => {
return this.emitFile({
type: 'asset',
source: await fs.readFile(path.join(root, filePath)),
fileName: 'static/' + filePath,
});
}),
);
},
};
}

View File

@@ -0,0 +1,55 @@
/**
* 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.
*/
import { promisify } from 'util';
import * as path from 'path';
import { promises as fsp } from 'fs';
import glob from 'glob';
const globP = promisify(glob);
export default function () {
return {
name: 'image-worker-plugin',
async buildStart() {
const base = path.join(process.cwd(), 'src', 'image-worker');
const dirs = (
await globP('*/', {
cwd: base,
})
).map((dir) => dir.slice(0, -1));
const file = [
`// This file is autogenerated by lib/image-worker-plugin.js`,
`import { expose } from 'comlink';`,
`import { timed } from './util';`,
dirs.map((dir) => `import ${dir} from './${dir}';`),
`const exports = {`,
dirs.map((dir) => [
` ${dir}(`,
` ...args: Parameters<typeof ${dir}>`,
` ): ReturnType<typeof ${dir}> {`,
` return timed('${dir}', () => ${dir}(...args));`,
` },`,
]),
`};`,
`export type ProcessorWorkerApi = typeof exports;`,
`expose(exports, self);`,
]
.flat(Infinity)
.join('\n');
await fsp.writeFile(path.join(base, 'index.ts'), file);
},
};
}

19
lib/move-output.js Normal file
View File

@@ -0,0 +1,19 @@
/**
* 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.
*/
// Move .tmp/build/static to docs/
const fs = require('fs');
const del = require('del');
const path = require('path');
del.sync('build');
fs.renameSync(path.join('.tmp', 'build', 'static'), 'build');

View File

@@ -0,0 +1,24 @@
/**
* 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.
*/
// Check that a node module exists, but treat it as external.
export default function () {
return {
name: 'node-external',
resolveId(id) {
try {
require.resolve(id);
return { id, external: true };
} catch (err) {}
},
};
}

84
lib/omt.ejs Normal file
View File

@@ -0,0 +1,84 @@
/**
* Copyright 2018 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.
*/
// If the loader is already loaded, just stop.
if (!self.<%- amdFunctionName %>) {
const singleRequire = async name => {
if (name === 'require') return require;
const url = '/c/' + name.slice(2) + '.js';
name = './static' + url;
if (registry[name]) return registry[name];
if (!registry[name]) {
<% if (useEval) { %>
const text = await fetch(url).then(resp => resp.text());
eval(text);
<% } else { %>
if ("document" in self) {
await new Promise(resolve => {
const script = document.createElement("script");
script.src = url;
document.head.appendChild(script);
script.onload = resolve;
});
} else {
importScripts(url);
}
<% } %>
}
if (!registry[name]) {
throw new Error(`Module ${name} didnt register its module`);
}
return registry[name];
};
const require = (names, resolve) => {
Promise.all(names.map(singleRequire))
.then(modules => resolve(modules.length === 1 ? modules[0] : modules));
};
const registry = {
require: Promise.resolve(require)
};
self.<%- amdFunctionName %> = (moduleName, depsNames, factory) => {
if (registry[moduleName]) {
// Module is already loading or loaded.
return;
}
registry[moduleName] = Promise.resolve().then(() => {
let exports = {};
const module = {
uri: location.origin + moduleName.slice(1)
};
return Promise.all(
depsNames.map(depName => {
switch(depName) {
case "exports":
return exports;
case "module":
return module;
default:
return singleRequire(depName);
}
})
).then(deps => {
const facValue = factory(...deps);
if(!exports.default) {
exports.default = facValue;
}
return exports;
});
});
};
}

View File

@@ -0,0 +1,36 @@
/**
* 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.
*/
import { posix as pathUtils } from 'path';
export default function resolveDirs(paths) {
const pathBaseDir = paths.map((path) => [
pathUtils.basename(path),
pathUtils.dirname(path),
]);
return {
name: 'resolve-dirs',
async resolveId(id) {
const match = pathBaseDir.find(
([pathId]) => id === pathId || id.startsWith(pathId + '/'),
);
if (!match) return;
const pathDir = match[1];
const resolveResult = await this.resolve(`./${pathDir}/${id}`, './');
if (!resolveResult) {
throw new Error(`Couldn't find ${'./' + id}`);
}
return pathUtils.resolve(resolveResult.id);
},
};
}

34
lib/run-script.js Normal file
View File

@@ -0,0 +1,34 @@
/**
* 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.
*/
import { fork } from 'child_process';
export default function runScript(path) {
return {
name: 'run-script',
writeBundle() {
return new Promise((resolve, reject) => {
const proc = fork(path, {
stdio: 'inherit',
});
proc.on('exit', (code) => {
if (code !== 0) {
reject(Error('Static build failed'));
return;
}
resolve();
});
});
},
};
}

130
lib/simple-ts.js Normal file
View File

@@ -0,0 +1,130 @@
/**
* 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.
*/
import { spawn } from 'child_process';
import { relative, join } from 'path';
import { promises as fsp } from 'fs';
import { promisify } from 'util';
import * as ts from 'typescript';
import glob from 'glob';
const globP = promisify(glob);
const extRe = /\.tsx?$/;
function loadConfig(mainPath) {
const fileName = ts.findConfigFile(mainPath, ts.sys.fileExists);
if (!fileName) throw Error('tsconfig not found');
const text = ts.sys.readFile(fileName);
const loadedConfig = ts.parseConfigFileTextToJson(fileName, text).config;
const parsedTsConfig = ts.parseJsonConfigFileContent(
loadedConfig,
ts.sys,
process.cwd(),
undefined,
fileName,
);
return parsedTsConfig;
}
export default function simpleTS(mainPath, { noBuild, watch } = {}) {
const config = loadConfig(mainPath);
const args = ['-b', mainPath];
let tsBuildDone;
async function watchBuiltFiles(rollupContext) {
const matches = await globP(config.options.outDir + '/**/*.js');
for (const match of matches) rollupContext.addWatchFile(match);
}
async function tsBuild(rollupContext) {
if (tsBuildDone) {
// Watch lists are cleared on each build, so we need to rewatch all the JS files.
await watchBuiltFiles(rollupContext);
return tsBuildDone;
}
if (noBuild) {
return (tsBuildDone = Promise.resolve());
}
tsBuildDone = Promise.resolve().then(async () => {
await new Promise((resolve) => {
const proc = spawn('tsc', args, {
stdio: 'inherit',
});
proc.on('exit', (code) => {
if (code !== 0) {
throw Error('TypeScript build failed');
}
resolve();
});
});
await watchBuiltFiles(rollupContext);
if (watch) {
tsBuildDone.then(() => {
spawn('tsc', [...args, '--watch', '--preserveWatchOutput'], {
stdio: 'inherit',
});
});
}
});
return tsBuildDone;
}
return {
name: 'simple-ts',
resolveId(id, importer) {
// If there isn't an importer, it's an entry point, so we don't need to resolve it relative
// to something.
if (!importer) return null;
const tsResolve = ts.resolveModuleName(
id,
importer,
config.options,
ts.sys,
);
if (
// It didn't find anything
!tsResolve.resolvedModule ||
// Or if it's linking to a definition file, it's something in node_modules,
// or something local like css.d.ts
tsResolve.resolvedModule.extension === '.d.ts'
) {
return null;
}
return tsResolve.resolvedModule.resolvedFileName;
},
async load(id) {
if (!extRe.test(id)) return null;
// TypeScript building is deferred until the first TS file load.
// This allows prerequisites to happen first,
// such as css.d.ts generation in css-plugin.
await tsBuild(this);
// Look for the JS equivalent in the tmp folder
const newId = join(
config.options.outDir,
relative(process.cwd(), id),
).replace(extRe, '.js');
return fsp.readFile(newId, { encoding: 'utf8' });
},
};
}

23
missing-types.d.ts vendored Normal file
View File

@@ -0,0 +1,23 @@
/**
* 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="./emscripten-types.d.ts" />
declare module 'url:*' {
const value: string;
export default value;
}
declare module 'omt:*' {
const value: string;
export default value;
}

17399
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,77 +1,46 @@
{ {
"private": true, "private": true,
"name": "squoosh", "name": "squoosh",
"version": "1.12.0", "version": "2.0.0",
"license": "apache-2.0", "license": "apache-2.0",
"scripts": { "scripts": {
"start": "webpack-dev-server --host 0.0.0.0 --hot", "build": "rollup -c && node lib/move-output.js",
"build": "webpack -p", "dev": "rollup -cw & npm run serve",
"lint": "tslint -c tslint.json -p tsconfig.json -t verbose", "serve": "serve --config server.json .tmp/build/static"
"lintfix": "tslint -c tslint.json -p tsconfig.json -t verbose --fix 'src/**/*.{ts,tsx,js,jsx}'", },
"sizereport": "sizereport --config" "devDependencies": {
"@rollup/plugin-commonjs": "^15.0.0",
"@rollup/plugin-node-resolve": "^9.0.0",
"@surma/rollup-plugin-off-main-thread": "^1.4.1",
"@types/node": "^14.10.1",
"comlink": "^4.3.0",
"cssnano": "^4.1.10",
"del": "^5.1.0",
"husky": "^4.3.0",
"lint-staged": "^10.3.0",
"lodash.camelcase": "^4.3.0",
"postcss": "^7.0.32",
"postcss-import": "^12.0.1",
"postcss-modules": "^3.2.2",
"postcss-nested": "^4.2.3",
"postcss-simple-vars": "^5.0.2",
"postcss-url": "^8.0.0",
"preact": "^10.4.8",
"preact-render-to-string": "^5.1.10",
"prettier": "^2.1.1",
"rollup": "^2.26.11",
"rollup-plugin-terser": "^7.0.2",
"serve": "^11.3.2",
"typescript": "^4.0.2"
}, },
"husky": { "husky": {
"hooks": { "hooks": {
"pre-commit": "npm run lint" "pre-commit": "lint-staged"
} }
}, },
"devDependencies": { "lint-staged": {
"@types/node": "10.14.15", "*.{js,css,json,md,ts,tsx}": [
"@types/pretty-bytes": "5.1.0", "prettier --write"
"@types/webassembly-js-api": "0.0.3", ]
"@webcomponents/custom-elements": "1.2.4",
"@webpack-cli/serve": "0.1.8",
"assets-webpack-plugin": "3.9.10",
"chalk": "2.4.2",
"chokidar": "3.0.2",
"classnames": "2.2.6",
"clean-webpack-plugin": "1.0.1",
"comlink": "3.1.1",
"copy-webpack-plugin": "5.0.4",
"critters-webpack-plugin": "2.4.0",
"css-loader": "1.0.1",
"ejs": "2.6.2",
"escape-string-regexp": "2.0.0",
"exports-loader": "0.7.0",
"file-drop-element": "0.2.0",
"file-loader": "4.2.0",
"gzip-size": "5.1.1",
"html-webpack-plugin": "3.2.0",
"husky": "3.0.4",
"idb-keyval": "3.2.0",
"linkstate": "1.1.1",
"loader-utils": "1.2.3",
"mini-css-extract-plugin": "0.8.0",
"minimatch": "3.0.4",
"node-fetch": "2.6.0",
"node-sass": "4.13.0",
"normalize-path": "^3.0.0",
"optimize-css-assets-webpack-plugin": "5.0.1",
"pointer-tracker": "2.0.3",
"preact": "8.4.2",
"prerender-loader": "1.3.0",
"pretty-bytes": "5.3.0",
"progress-bar-webpack-plugin": "1.12.1",
"raw-loader": "3.1.0",
"readdirp": "3.1.2",
"sass-loader": "7.3.1",
"script-ext-html-webpack-plugin": "2.1.4",
"source-map-loader": "0.2.4",
"style-loader": "1.0.0",
"terser-webpack-plugin": "1.4.1",
"travis-size-report": "1.1.0",
"ts-loader": "6.0.3",
"tslint": "5.19.0",
"tslint-config-airbnb": "5.11.1",
"tslint-config-semistandard": "8.0.1",
"tslint-react": "4.0.0",
"typed-css-modules": "0.4.2",
"typescript": "3.5.3",
"url-loader": "2.1.0",
"webpack": "4.39.3",
"webpack-bundle-analyzer": "3.4.1",
"webpack-cli": "3.3.4",
"webpack-dev-server": "3.8.0",
"worker-plugin": "3.1.0"
} }
} }

106
rollup.config.js Normal file
View File

@@ -0,0 +1,106 @@
/**
* 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.
*/
import * as path from 'path';
import { promises as fsp } from 'fs';
import del from 'del';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import { terser } from 'rollup-plugin-terser';
import OMT from '@surma/rollup-plugin-off-main-thread';
import simpleTS from './lib/simple-ts';
import clientBundlePlugin from './lib/client-bundle-plugin';
import nodeExternalPlugin from './lib/node-external-plugin';
import cssPlugin from './lib/css-plugin';
import assetPlugin from './lib/asset-plugin';
import resolveDirsPlugin from './lib/resolve-dirs-plugin';
import runScript from './lib/run-script';
import emitFiles from './lib/emit-files-plugin';
import imageWorkerPlugin from './lib/image-worker-plugin';
function resolveFileUrl({ fileName }) {
return JSON.stringify(fileName.replace(/^static\//, '/'));
}
// With AMD output, Rollup always uses document.baseURI, which breaks in workers.
// This fixes it:
function resolveImportMeta(property, { chunkId }) {
if (property !== 'url') return;
return `new URL(${resolveFileUrl({ fileName: chunkId })}, location).href`;
}
export default async function ({ watch }) {
const omtLoaderPromise = fsp.readFile(
path.join(__dirname, 'lib', 'omt.ejs'),
'utf-8',
);
await del('.tmp/build');
const tsPluginInstance = simpleTS('.', {
watch,
});
const commonPlugins = () => [
tsPluginInstance,
resolveDirsPlugin([
'src/static-build',
'src/client',
'src/image-worker',
'src/worker-main-shared',
'codecs',
]),
assetPlugin(),
cssPlugin(resolveFileUrl),
];
const dir = '.tmp/build';
const staticPath = 'static/c/[name]-[hash][extname]';
return {
input: 'src/static-build/index.tsx',
output: {
dir,
format: 'cjs',
assetFileNames: staticPath,
exports: 'named',
},
// Don't watch the ts files. Instead we watch the output from the ts compiler.
watch: { clearScreen: false, exclude: ['**/*.ts', '**/*.tsx'] },
preserveModules: true,
plugins: [
{ resolveFileUrl, resolveImportMeta },
clientBundlePlugin(
{
plugins: [
{ resolveFileUrl, resolveImportMeta },
OMT({ loader: await omtLoaderPromise }),
...commonPlugins(),
commonjs(),
resolve(),
terser({ module: true }),
],
},
{
dir,
format: 'amd',
chunkFileNames: staticPath.replace('[extname]', '.js'),
entryFileNames: staticPath.replace('[extname]', '.js'),
},
resolveFileUrl,
),
...commonPlugins(),
emitFiles({ include: '**/*', root: path.join(__dirname, 'src', 'copy') }),
nodeExternalPlugin(),
imageWorkerPlugin(),
runScript(dir + '/index.js'),
],
};
}

13
serve.json Normal file
View File

@@ -0,0 +1,13 @@
{
"headers": [
{
"source": "**/*",
"headers": [
{
"key": "Cache-Control",
"value": "no-cache"
}
]
}
]
}

View File

@@ -1,14 +0,0 @@
const escapeRE = require("escape-string-regexp");
module.exports = {
repo: "GoogleChromeLabs/squoosh",
path: "build/**/!(*.map)",
branch: "dev",
findRenamed(path, newPaths) {
const nameParts = /^(.+\.)[a-f0-9]+(\..+)$/.exec(path);
if (!nameParts) return;
const matchRe = new RegExp(`^${escapeRE(nameParts[1])}[a-f0-9]+${escapeRE(nameParts[2])}$`);
return newPaths.find(newPath => matchRe.test(newPath));
}
};

59
src/client/index.tsx Normal file
View File

@@ -0,0 +1,59 @@
/**
* 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.
*/
import { wrap } from 'comlink';
import workerURL from 'omt:image-worker';
import imgURL from 'url:./tmp.png';
import type { ProcessorWorkerApi } from 'image-worker/index';
const worker = new Worker(workerURL);
const api = wrap<ProcessorWorkerApi>(worker);
async function demo() {
const img = document.createElement('img');
img.src = imgURL;
await img.decode();
// Make canvas same size as image
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d')!;
ctx.drawImage(img, 0, 0);
const data = ctx.getImageData(0, 0, img.width, img.height);
const result = await api.mozjpegEncode(data, {
quality: 75,
baseline: false,
arithmetic: false,
progressive: true,
optimize_coding: true,
smoothing: 0,
color_space: 3,
quant_table: 3,
trellis_multipass: false,
trellis_opt_zero: false,
trellis_opt_table: false,
trellis_loops: 1,
auto_subsample: true,
chroma_subsample: 2,
separate_chroma_quality: false,
chroma_quality: 75,
});
{
const resultUrl = URL.createObjectURL(new Blob([result]));
const img = new Image();
img.src = resultUrl;
document.body.append(img);
}
}
demo();

13
src/client/missing-types.d.ts vendored Normal file
View 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" />

BIN
src/client/tmp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

8
src/client/tsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"extends": "../../generic-tsconfig.json",
"compilerOptions": {
"lib": ["esnext", "dom", "dom.iterable"],
"types": []
},
"references": [{ "path": "../image-worker" }]
}

View File

@@ -1,19 +0,0 @@
import imagequant, { QuantizerModule } from '../../../codecs/imagequant/imagequant';
import wasmUrl from '../../../codecs/imagequant/imagequant.wasm';
import { QuantizeOptions } from './processor-meta';
import { initEmscriptenModule } from '../util';
let emscriptenModule: Promise<QuantizerModule>;
export async function process(data: ImageData, opts: QuantizeOptions): Promise<ImageData> {
if (!emscriptenModule) emscriptenModule = initEmscriptenModule(imagequant, wasmUrl);
const module = await emscriptenModule;
const result = opts.zx ?
module.zx_quantize(data.data, data.width, data.height, opts.dither)
:
module.quantize(data.data, data.width, data.height, opts.maxNumColors, opts.dither);
return new ImageData(result, data.width, data.height);
}

View File

@@ -1,39 +0,0 @@
import { WorkerResizeOptions } from './processor-meta';
import { getContainOffsets } from './util';
import { resize as codecResize } from '../../../codecs/resize/pkg';
function crop(data: ImageData, sx: number, sy: number, sw: number, sh: number): ImageData {
const inputPixels = new Uint32Array(data.data.buffer);
// Copy within the same buffer for speed and memory efficiency.
for (let y = 0; y < sh; y += 1) {
const start = ((y + sy) * data.width) + sx;
inputPixels.copyWithin(y * sw, start, start + sw);
}
return new ImageData(
new Uint8ClampedArray(inputPixels.buffer.slice(0, sw * sh * 4)),
sw, sh,
);
}
/** Resize methods by index */
const resizeMethods: WorkerResizeOptions['method'][] = [
'triangle', 'catrom', 'mitchell', 'lanczos3',
];
export async function resize(data: ImageData, opts: WorkerResizeOptions): Promise<ImageData> {
let input = data;
if (opts.fitMethod === 'contain') {
const { sx, sy, sw, sh } = getContainOffsets(data.width, data.height, opts.width, opts.height);
input = crop(input, Math.round(sx), Math.round(sy), Math.round(sw), Math.round(sh));
}
const result = codecResize(
new Uint8Array(input.data.buffer), input.width, input.height, opts.width, opts.height,
resizeMethods.indexOf(opts.method), opts.premultiply, opts.linearRGB,
);
return new ImageData(new Uint8ClampedArray(result.buffer), opts.width, opts.height);
}

5
src/copy/_headers Normal file
View File

@@ -0,0 +1,5 @@
/*
Cache-Control: no-cache
/c/*
Cache-Control: max-age=31536000

13
src/image-worker/missing-types.d.ts vendored Normal file
View 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" />

View File

@@ -0,0 +1,56 @@
/**
* 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.
*/
import mozjpeg_enc, { MozJPEGModule } from 'codecs/mozjpeg_enc/mozjpeg_enc';
import wasmUrl from 'url:codecs/mozjpeg_enc/mozjpeg_enc.wasm';
import { initEmscriptenModule } from '../util';
export const enum MozJpegColorSpace {
GRAYSCALE = 1,
RGB,
YCbCr,
}
export interface EncodeOptions {
quality: number;
baseline: boolean;
arithmetic: boolean;
progressive: boolean;
optimize_coding: boolean;
smoothing: number;
color_space: MozJpegColorSpace;
quant_table: number;
trellis_multipass: boolean;
trellis_opt_zero: boolean;
trellis_opt_table: boolean;
trellis_loops: number;
auto_subsample: boolean;
chroma_subsample: number;
separate_chroma_quality: boolean;
chroma_quality: number;
}
let emscriptenModule: Promise<MozJPEGModule>;
export default async function encode(
data: ImageData,
options: EncodeOptions,
): Promise<ArrayBuffer> {
if (!emscriptenModule) {
emscriptenModule = initEmscriptenModule(mozjpeg_enc, wasmUrl);
}
const module = await emscriptenModule;
const resultView = module.encode(data.data, data.width, data.height, options);
// wasm cant run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
return resultView.buffer as ArrayBuffer;
}

View File

@@ -0,0 +1,52 @@
/**
* 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.
*/
import imagequant, { QuantizerModule } from 'codecs/imagequant/imagequant';
import wasmUrl from 'url:codecs/imagequant/imagequant.wasm';
import { initEmscriptenModule } from '../util';
export interface QuantizeOptions {
zx: number;
maxNumColors: number;
dither: number;
}
export const defaultOptions: QuantizeOptions = {
zx: 0,
maxNumColors: 256,
dither: 1.0,
};
let emscriptenModule: Promise<QuantizerModule>;
export default async function process(
data: ImageData,
opts: QuantizeOptions,
): Promise<ImageData> {
if (!emscriptenModule) {
emscriptenModule = initEmscriptenModule(imagequant, wasmUrl);
}
const module = await emscriptenModule;
const result = opts.zx
? module.zx_quantize(data.data, data.width, data.height, opts.dither)
: module.quantize(
data.data,
data.width,
data.height,
opts.maxNumColors,
opts.dither,
);
return new ImageData(result, data.width, data.height);
}

View File

@@ -0,0 +1,7 @@
{
"extends": "../../generic-tsconfig.json",
"compilerOptions": {
"lib": ["webworker", "esnext"]
},
"references": []
}

39
src/image-worker/util.ts Normal file
View File

@@ -0,0 +1,39 @@
/**
* 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 function initEmscriptenModule<T extends EmscriptenWasm.Module>(
moduleFactory: EmscriptenWasm.ModuleFactory<T>,
wasmUrl: string,
): Promise<T> {
return moduleFactory({
// Just to be safe, don't automatically invoke any wasm functions
noInitialRun: true,
locateFile: () => wasmUrl,
});
}
interface ClampOpts {
min?: number;
max?: number;
}
export function clamp(
num: number,
{ min = Number.MIN_VALUE, max = Number.MAX_VALUE }: ClampOpts,
): number {
return Math.min(Math.max(num, min), max);
}
export function timed<T>(name: string, func: () => Promise<T>) {
console.time(name);
return func().finally(() => console.timeEnd(name));
}

View File

@@ -0,0 +1,26 @@
/**
* 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.
*/
import webpDecoder, { WebPModule } from 'codecs/webp/dec/webp_dec';
import wasmUrl from 'url:codecs/webp/dec/webp_dec.wasm';
import { initEmscriptenModule } from '../util';
let emscriptenModule: Promise<WebPModule>;
export default async function decode(data: ArrayBuffer): Promise<ImageData> {
if (!emscriptenModule) {
emscriptenModule = initEmscriptenModule(webpDecoder, wasmUrl);
}
const module = await emscriptenModule;
return module.decode(data);
}

View File

@@ -0,0 +1,61 @@
/**
* 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.
*/
import webpEncoder, { WebPModule } from 'codecs/webp/enc/webp_enc';
import wasmUrl from 'url:codecs/webp/enc/webp_enc.wasm';
import { initEmscriptenModule } from '../util';
export interface EncodeOptions {
quality: number;
target_size: number;
target_PSNR: number;
method: number;
sns_strength: number;
filter_strength: number;
filter_sharpness: number;
filter_type: number;
partitions: number;
segments: number;
pass: number;
show_compressed: number;
preprocessing: number;
autofilter: number;
partition_limit: number;
alpha_compression: number;
alpha_filtering: number;
alpha_quality: number;
lossless: number;
exact: number;
image_hint: number;
emulate_jpeg_size: number;
thread_level: number;
low_memory: number;
near_lossless: number;
use_delta_palette: number;
use_sharp_yuv: number;
}
let emscriptenModule: Promise<WebPModule>;
export default async function encode(
data: ImageData,
options: EncodeOptions,
): Promise<ArrayBuffer> {
if (!emscriptenModule) {
emscriptenModule = initEmscriptenModule(webpEncoder, wasmUrl);
}
const module = await emscriptenModule;
const resultView = module.encode(data.data, data.width, data.height, options);
// wasm cant run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
return resultView.buffer as ArrayBuffer;
}

View File

@@ -0,0 +1,3 @@
html {
background: green;
}

View File

@@ -0,0 +1,44 @@
/**
* 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.
*/
import { h, FunctionalComponent, RenderableProps } from 'preact';
import styles from 'css-bundle:./all.css';
import clientBundleURL, { imports } from 'client-bundle:client/index.tsx';
interface Props {
title?: string;
}
const BasePage: FunctionalComponent<Props> = ({
children,
title,
}: RenderableProps<Props>) => {
return (
<html lang="en">
<head>
<title>{title ? `${title} - ` : ''}Squoosh</title>
<meta
name="viewport"
content="width=device-width, minimum-scale=1.0"
></meta>
<link rel="stylesheet" href={styles} />
<script src={clientBundleURL} defer />
{imports.map((v) => (
<link rel="preload" as="script" href={v} />
))}
</head>
<body>{children}</body>
</html>
);
};
export default BasePage;

View File

@@ -0,0 +1,25 @@
/**
* 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.
*/
import { h } from 'preact';
import { renderPage, writeFiles } from './utils';
import IndexPage from './pages/index';
interface Output {
[outputPath: string]: string;
}
const toOutput: Output = {
'index.html': renderPage(<IndexPage />),
};
writeFiles(toOutput);

25
src/static-build/missing-types.d.ts vendored Normal file
View File

@@ -0,0 +1,25 @@
/**
* 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" />
declare module 'client-bundle:*' {
const url: string;
export default url;
export const imports: string[];
}
declare module 'css-bundle:*' {
const url: string;
export default url;
export const inline: string;
}

View File

@@ -0,0 +1,21 @@
/**
* 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.
*/
import { h, FunctionalComponent } from 'preact';
import BasePage from 'static-build/components/base';
const IndexPage: FunctionalComponent<{}> = () => (
<BasePage>
<h1>Hi</h1>
</BasePage>
);
export default IndexPage;

View File

@@ -0,0 +1,8 @@
{
"extends": "../../generic-tsconfig.json",
"compilerOptions": {
"lib": ["esnext", "dom"],
"types": ["node"]
},
"references": []
}

View File

@@ -0,0 +1,46 @@
/**
* 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.
*/
import { promises as fsp } from 'fs';
import { join as joinPath } from 'path';
import render from 'preact-render-to-string';
import { VNode } from 'preact';
export function renderPage(vnode: VNode) {
return '<!DOCTYPE html>' + render(vnode);
}
interface OutputMap {
[path: string]: string;
}
export function writeFiles(toOutput: OutputMap) {
Promise.all(
Object.entries(toOutput).map(async ([path, content]) => {
const pathParts = ['.tmp', 'build', 'static', ...path.split('/')];
await fsp.mkdir(joinPath(...pathParts.slice(0, -1)), { recursive: true });
const fullPath = joinPath(...pathParts);
try {
await fsp.writeFile(fullPath, content, {
encoding: 'utf8',
});
} catch (err) {
console.error('Failed to write ' + fullPath);
throw err;
}
}),
).catch((err) => {
console.error(err);
process.exit(1);
});
}

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -1,7 +1,10 @@
import { canvasEncodeTest } from '../generic/util'; import { canvasEncodeTest } from '../generic/util';
export interface EncodeOptions { } export interface EncodeOptions {}
export interface EncoderState { type: typeof type; options: EncodeOptions; } export interface EncoderState {
type: typeof type;
options: EncodeOptions;
}
export const type = 'browser-bmp'; export const type = 'browser-bmp';
export const label = 'Browser BMP'; export const label = 'Browser BMP';

View File

@@ -1,7 +1,10 @@
import { canvasEncodeTest } from '../generic/util'; import { canvasEncodeTest } from '../generic/util';
export interface EncodeOptions {} export interface EncodeOptions {}
export interface EncoderState { type: typeof type; options: EncodeOptions; } export interface EncoderState {
type: typeof type;
options: EncodeOptions;
}
export const type = 'browser-gif'; export const type = 'browser-gif';
export const label = 'Browser GIF'; export const label = 'Browser GIF';

View File

@@ -1,7 +1,10 @@
import { canvasEncodeTest } from '../generic/util'; import { canvasEncodeTest } from '../generic/util';
export interface EncodeOptions { } export interface EncodeOptions {}
export interface EncoderState { type: typeof type; options: EncodeOptions; } export interface EncoderState {
type: typeof type;
options: EncodeOptions;
}
export const type = 'browser-jp2'; export const type = 'browser-jp2';
export const label = 'Browser JPEG 2000'; export const label = 'Browser JPEG 2000';

View File

@@ -1,5 +1,10 @@
export interface EncodeOptions { quality: number; } export interface EncodeOptions {
export interface EncoderState { type: typeof type; options: EncodeOptions; } quality: number;
}
export interface EncoderState {
type: typeof type;
options: EncodeOptions;
}
export const type = 'browser-jpeg'; export const type = 'browser-jpeg';
export const label = 'Browser JPEG'; export const label = 'Browser JPEG';

View File

@@ -1,7 +1,10 @@
import { canvasEncodeTest } from '../generic/util'; import { canvasEncodeTest } from '../generic/util';
export interface EncodeOptions { } export interface EncodeOptions {}
export interface EncoderState { type: typeof type; options: EncodeOptions; } export interface EncoderState {
type: typeof type;
options: EncodeOptions;
}
export const type = 'browser-pdf'; export const type = 'browser-pdf';
export const label = 'Browser PDF'; export const label = 'Browser PDF';

View File

@@ -1,5 +1,8 @@
export interface EncodeOptions {} export interface EncodeOptions {}
export interface EncoderState { type: typeof type; options: EncodeOptions; } export interface EncoderState {
type: typeof type;
options: EncodeOptions;
}
export const type = 'browser-png'; export const type = 'browser-png';
export const label = 'Browser PNG'; export const label = 'Browser PNG';

View File

@@ -1,7 +1,10 @@
import { canvasEncodeTest } from '../generic/util'; import { canvasEncodeTest } from '../generic/util';
export interface EncodeOptions { } export interface EncodeOptions {}
export interface EncoderState { type: typeof type; options: EncodeOptions; } export interface EncoderState {
type: typeof type;
options: EncodeOptions;
}
export const type = 'browser-tiff'; export const type = 'browser-tiff';
export const label = 'Browser TIFF'; export const label = 'Browser TIFF';

View File

@@ -1,7 +1,12 @@
import { canvasEncodeTest } from '../generic/util'; import { canvasEncodeTest } from '../generic/util';
export interface EncodeOptions { quality: number; } export interface EncodeOptions {
export interface EncoderState { type: typeof type; options: EncodeOptions; } quality: number;
}
export interface EncoderState {
type: typeof type;
options: EncodeOptions;
}
export const type = 'browser-webp'; export const type = 'browser-webp';
export const label = 'Browser WebP'; export const label = 'Browser WebP';

View File

@@ -1,7 +1,10 @@
import { builtinDecode, sniffMimeType, canDecodeImageType } from '../lib/util'; import { builtinDecode, sniffMimeType, canDecodeImageType } from '../lib/util';
import Processor from './processor'; import Processor from './processor';
export async function decodeImage(blob: Blob, processor: Processor): Promise<ImageData> { export async function decodeImage(
blob: Blob,
processor: Processor,
): Promise<ImageData> {
const mimeType = await sniffMimeType(blob); const mimeType = await sniffMimeType(blob);
const canDecode = await canDecodeImageType(mimeType); const canDecode = await canDecodeImageType(mimeType);

View File

@@ -17,34 +17,34 @@ export interface EncoderSupportMap {
} }
export type EncoderState = export type EncoderState =
identity.EncoderState | | identity.EncoderState
oxiPNG.EncoderState | | oxiPNG.EncoderState
mozJPEG.EncoderState | | mozJPEG.EncoderState
webP.EncoderState | | webP.EncoderState
avif.EncoderState | | avif.EncoderState
browserPNG.EncoderState | | browserPNG.EncoderState
browserJPEG.EncoderState | | browserJPEG.EncoderState
browserWebP.EncoderState | | browserWebP.EncoderState
browserGIF.EncoderState | | browserGIF.EncoderState
browserTIFF.EncoderState | | browserTIFF.EncoderState
browserJP2.EncoderState | | browserJP2.EncoderState
browserBMP.EncoderState | | browserBMP.EncoderState
browserPDF.EncoderState; | browserPDF.EncoderState;
export type EncoderOptions = export type EncoderOptions =
identity.EncodeOptions | | identity.EncodeOptions
oxiPNG.EncodeOptions | | oxiPNG.EncodeOptions
mozJPEG.EncodeOptions | | mozJPEG.EncodeOptions
webP.EncodeOptions | | webP.EncodeOptions
avif.EncodeOptions | | avif.EncodeOptions
browserPNG.EncodeOptions | | browserPNG.EncodeOptions
browserJPEG.EncodeOptions | | browserJPEG.EncodeOptions
browserWebP.EncodeOptions | | browserWebP.EncodeOptions
browserGIF.EncodeOptions | | browserGIF.EncodeOptions
browserTIFF.EncodeOptions | | browserTIFF.EncodeOptions
browserJP2.EncodeOptions | | browserJP2.EncodeOptions
browserBMP.EncodeOptions | | browserBMP.EncodeOptions
browserPDF.EncodeOptions; | browserPDF.EncodeOptions;
export type EncoderType = keyof typeof encoderMap; export type EncoderType = keyof typeof encoderMap;
@@ -72,11 +72,14 @@ export const encoders = Array.from(Object.values(encoderMap));
export const encodersSupported = Promise.resolve().then(async () => { export const encodersSupported = Promise.resolve().then(async () => {
const encodersSupported: EncoderSupportMap = {}; const encodersSupported: EncoderSupportMap = {};
await Promise.all(encoders.map(async (encoder) => { await Promise.all(
// If the encoder provides a featureTest, call it, otherwise assume supported. encoders.map(async (encoder) => {
const isSupported = !('featureTest' in encoder) || await encoder.featureTest(); // If the encoder provides a featureTest, call it, otherwise assume supported.
encodersSupported[encoder.type] = isSupported; const isSupported =
})); !('featureTest' in encoder) || (await encoder.featureTest());
encodersSupported[encoder.type] = isSupported;
}),
);
return encodersSupported; return encodersSupported;
}); });

View File

@@ -8,8 +8,8 @@ interface EncodeOptions {
} }
type Props = { type Props = {
options: EncodeOptions, options: EncodeOptions;
onChange(newOptions: EncodeOptions): void, onChange(newOptions: EncodeOptions): void;
}; };
interface QualityOptionArg { interface QualityOptionArg {
@@ -19,11 +19,7 @@ interface QualityOptionArg {
} }
export default function qualityOption(opts: QualityOptionArg = {}) { export default function qualityOption(opts: QualityOptionArg = {}) {
const { const { min = 0, max = 100, step = 1 } = opts;
min = 0,
max = 100,
step = 1,
} = opts;
class QualityOptions extends Component<Props, {}> { class QualityOptions extends Component<Props, {}> {
@bind @bind

Some files were not shown because too many files have changed in this diff Show More