Compare commits
169 Commits
critters-a
...
abstractin
Author | SHA1 | Date | |
---|---|---|---|
f877dcaff9 | |||
43def798e1 | |||
02b0c022ca | |||
c82d0d1b88 | |||
e24d7865ce | |||
a79f95b305 | |||
49b40b1c3e | |||
11ee74e224 | |||
f335246673 | |||
ccb734aec6 | |||
568b9e9459 | |||
a43ea761f5 | |||
577c77cc30 | |||
d2f60baef9 | |||
64acc08cd7 | |||
a1f0b81dff | |||
48bb58dc89 | |||
765cc213d2 | |||
37f5c0dd76 | |||
b25d1eaf86 | |||
248676aa31 | |||
059c80c05d | |||
cfd42818b7 | |||
5e66e0acc4 | |||
c9fe5ffbcf | |||
1b630a092f | |||
09e60284cb | |||
76b34c62db | |||
9d7212bc1d | |||
1b69c9231d | |||
bcd88f6356 | |||
2a47f67214 | |||
5e8dc1b26c | |||
c591f1f37d | |||
4db43ccd4e | |||
ea5d3c2d78 | |||
700b1f15cd | |||
485ba174e3 | |||
32f6f8b941 | |||
54ad30a7ed | |||
170d75482e | |||
a8db2b30f2 | |||
e3b1b08424 | |||
8006a1a5e7 | |||
1ae65dd4a1 | |||
bff515b63f | |||
65c3ea826f | |||
602d5140f9 | |||
44f0700332 | |||
c90db020b0 | |||
ef4094885e | |||
b52d9d9194 | |||
d3f2836f48 | |||
27722f77f9 | |||
3a0db14c40 | |||
e0dc1b48ec | |||
009327c2c4 | |||
b16d60b52b | |||
c550fe9283 | |||
dce4fc70ac | |||
b3f3ecbf28 | |||
e8c0ddfc7f | |||
a002b376af | |||
2165383da4 | |||
5fbf6b297f | |||
9d5ad83ff8 | |||
07f17dece2 | |||
f2f467ecb8 | |||
2ea9e22b52 | |||
4ee5572d2f | |||
df7e112d22 | |||
13ac3ed5b2 | |||
b7c223bc0d | |||
0f08121596 | |||
b15545402a | |||
b310c97044 | |||
307c6b05ae | |||
77a6d21924 | |||
d22a343378 | |||
790a5b580d | |||
6e8f8bbe41 | |||
cc9d01a9ab | |||
526520c399 | |||
acbc31bc35 | |||
e8e151a926 | |||
835a537c55 | |||
23ea9fad49 | |||
491280935a | |||
900eda9a8e | |||
38d0057833 | |||
3867448aad | |||
807a76d443 | |||
3e26a0a3cc | |||
68729979e3 | |||
a09ec269b8 | |||
3f18c927f1 | |||
9add650b75 | |||
cc3ed168d8 | |||
3b9b1e9f2e | |||
7c220b1a92 | |||
3035a68b90 | |||
65847c0ed7 | |||
5303afe9ad | |||
579b8a494a | |||
56faf619d0 | |||
85e3a12c84 | |||
cab8d3f13c | |||
5c651a1716 | |||
ba0ad81646 | |||
695bbed12b | |||
6a6d478f77 | |||
d75a3aca9b | |||
91945da5ae | |||
00e73daabd | |||
60543dd0a5 | |||
850a019212 | |||
9c0e0b683e | |||
79dfe39978 | |||
96a61eb0b2 | |||
e62fc26dfd | |||
638c57b6fe | |||
7ff18e6ae1 | |||
9d8f885556 | |||
5245c5ca6e | |||
19342208d2 | |||
a9e1c38971 | |||
1533728f59 | |||
d4a616713a | |||
a7598b6602 | |||
e38e7154a6 | |||
7a5c8f5d6b | |||
49db0de05f | |||
8daaea5768 | |||
c2e2a1a0b6 | |||
7edb7f0de8 | |||
634dfe3717 | |||
1b4526ca1e | |||
5e2c4be0c6 | |||
e9eaf227bc | |||
6249ca8ac8 | |||
03a6716745 | |||
ddf8409127 | |||
bcf71f4702 | |||
31db4b9719 | |||
953a0c9124 | |||
444e59c69c | |||
b619427237 | |||
5f7f9e32a8 | |||
1196d4f54f | |||
e84d2dc7ee | |||
81aaadbabf | |||
311d0524db | |||
da53b5fedc | |||
c5e3f9e737 | |||
540b3c8154 | |||
06642fd047 | |||
3b47ee6fe5 | |||
058cce1d49 | |||
2078b57dae | |||
aa02cf2157 | |||
11bebfc836 | |||
dec93a724f | |||
411614b731 | |||
896d267de5 | |||
e0c59577a4 | |||
5936c57a82 | |||
3ba0a5a22a | |||
b911e960a8 | |||
718443de30 |
44
.babelrc
@ -1,33 +1,13 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"env",
|
||||
{
|
||||
"loose": true,
|
||||
"uglify": true,
|
||||
"modules": false,
|
||||
"targets": {
|
||||
"browsers": "last 2 versions"
|
||||
},
|
||||
"exclude": [
|
||||
"transform-regenerator",
|
||||
"transform-es2015-typeof-symbol"
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": [
|
||||
"syntax-dynamic-import",
|
||||
"transform-decorators-legacy",
|
||||
"transform-class-properties",
|
||||
"transform-object-rest-spread",
|
||||
"transform-react-constant-elements",
|
||||
"transform-react-remove-prop-types",
|
||||
[
|
||||
"transform-react-jsx",
|
||||
{
|
||||
"pragma": "h"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
"plugins": [
|
||||
"transform-class-properties",
|
||||
"transform-react-constant-elements",
|
||||
"transform-react-remove-prop-types",
|
||||
[
|
||||
"transform-react-jsx",
|
||||
{
|
||||
"pragma": "h"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
5
.gitignore
vendored
@ -1,3 +1,6 @@
|
||||
node_modules
|
||||
/build
|
||||
/*.log
|
||||
/*.log
|
||||
*.scss.d.ts
|
||||
*.css.d.ts
|
||||
*.o
|
||||
|
5
.travis.yml
Normal file
@ -0,0 +1,5 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- node
|
||||
cache: npm
|
||||
script: npm run build || npm run build # scss ts definitions need to be generated before an actual build
|
16
codecs/README.md
Normal file
@ -0,0 +1,16 @@
|
||||
# Codecs
|
||||
|
||||
This folder contains a self-contained sub-project for each encoder and decoder that squoosh supplies.
|
||||
|
||||
## Build
|
||||
|
||||
Each subproject can be built using [Docker](https://www.docker.com/) the following commands:
|
||||
|
||||
```
|
||||
$ npm install
|
||||
$ npm run build
|
||||
```
|
||||
|
||||
This will build two files: `<codec name>_<enc or dec>.js` and `<codec name>_<enc or dec>.wasm`. It will most likely be necessary to set [`Module["locateFile"]`](https://kripken.github.io/emscripten-site/docs/api_reference/module.html#affecting-execution) to sucessfully load the `.wasm` file. When the `.js` file is loaded, a global `<codec name>_<enc or dec>` is created with the same API as an [Emscripten `Module`](https://kripken.github.io/emscripten-site/docs/api_reference/module.html).
|
||||
|
||||
Each codec will document its API in its README.
|
BIN
codecs/example.png
Normal file
After Width: | Height: | Size: 3.3 MiB |
BIN
codecs/example.webp
Normal file
After Width: | Height: | Size: 74 KiB |
BIN
codecs/example_palette.png
Normal file
After Width: | Height: | Size: 239 KiB |
30
codecs/imagequant/README.md
Normal file
@ -0,0 +1,30 @@
|
||||
# ImageQuant
|
||||
|
||||
- Source: <https://github.com/ImageOptim/libimagequant>
|
||||
- Version: v2.12.1
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Docker
|
||||
|
||||
## Example
|
||||
|
||||
See `example.html`
|
||||
|
||||
## API
|
||||
|
||||
### `int version()`
|
||||
|
||||
Returns the version of libimagequant as a number. va.b.c is encoded as 0x0a0b0c
|
||||
|
||||
### `RawImage quantize(std::string buffer, int image_width, int image_height, int numColors, float dithering)`
|
||||
|
||||
Quantizes the given images, using at most `numColors`, a value between 2 and 256. `dithering` is a value between 0 and 1 controlling the amount of dithering. `RawImage` is a class with 3 fields: `buffer`, `width`, and `height`.
|
||||
|
||||
### `RawImage zx_quantize(std::string buffer, int image_width, int image_height, float dithering)`
|
||||
|
||||
???
|
||||
|
||||
### `void free_result()`
|
||||
|
||||
Frees the result created by `quantize()`.
|
48
codecs/imagequant/build.sh
Executable file
@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
export OPTIMIZE="-Os"
|
||||
export LDFLAGS="${OPTIMIZE}"
|
||||
export CFLAGS="${OPTIMIZE}"
|
||||
export CPPFLAGS="${OPTIMIZE}"
|
||||
|
||||
echo "============================================="
|
||||
echo "Compiling libimagequant"
|
||||
echo "============================================="
|
||||
(
|
||||
emcc \
|
||||
--bind \
|
||||
${OPTIMIZE} \
|
||||
-s ALLOW_MEMORY_GROWTH=1 \
|
||||
-s MODULARIZE=1 \
|
||||
-s 'EXPORT_NAME="imagequant"' \
|
||||
-I node_modules/libimagequant \
|
||||
--std=c99 \
|
||||
-c \
|
||||
node_modules/libimagequant/{libimagequant,pam,mediancut,blur,mempool,kmeans,nearest}.c
|
||||
)
|
||||
echo "============================================="
|
||||
echo "Compiling wasm module"
|
||||
echo "============================================="
|
||||
(
|
||||
emcc \
|
||||
--bind \
|
||||
${OPTIMIZE} \
|
||||
-s ALLOW_MEMORY_GROWTH=1 \
|
||||
-s MODULARIZE=1 \
|
||||
-s 'EXPORT_NAME="imagequant"' \
|
||||
-I node_modules/libimagequant \
|
||||
-o ./imagequant.js \
|
||||
--std=c++11 *.o \
|
||||
-x c++ \
|
||||
imagequant.cpp
|
||||
)
|
||||
echo "============================================="
|
||||
echo "Compiling wasm module done"
|
||||
echo "============================================="
|
||||
|
||||
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
||||
echo "Did you update your docker image?"
|
||||
echo "Run \`docker pull trzeci/emscripten\`"
|
||||
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
41
codecs/imagequant/example.html
Normal file
@ -0,0 +1,41 @@
|
||||
<!doctype html>
|
||||
<style>
|
||||
canvas {
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
</style>
|
||||
<script src='imagequant.js'></script>
|
||||
<script>
|
||||
const Module = imagequant();
|
||||
|
||||
async function loadImage(src) {
|
||||
// Load image
|
||||
const img = document.createElement('img');
|
||||
img.src = src;
|
||||
await new Promise(resolve => img.onload = resolve);
|
||||
// Make canvas same size as image
|
||||
const canvas = document.createElement('canvas');
|
||||
[canvas.width, canvas.height] = [img.width, img.height];
|
||||
// Draw image onto canvas
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
return ctx.getImageData(0, 0, img.width, img.height);
|
||||
}
|
||||
|
||||
Module.onRuntimeInitialized = async _ => {
|
||||
console.log('Version:', Module.version().toString(16));
|
||||
const image = await loadImage('../example.png');
|
||||
// const rawImage = Module.quantize(image.data, image.width, image.height, 256, 1.0);
|
||||
const rawImage = Module.zx_quantize(image.data, image.width, image.height, 1.0);
|
||||
console.log('done');
|
||||
Module.free_result();
|
||||
|
||||
const imageData = new ImageData(new Uint8ClampedArray(rawImage.buffer), rawImage.width, rawImage.height);
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
document.body.appendChild(canvas);
|
||||
};
|
||||
</script>
|
245
codecs/imagequant/imagequant.cpp
Normal file
@ -0,0 +1,245 @@
|
||||
#include "emscripten/bind.h"
|
||||
#include "emscripten/val.h"
|
||||
#include <stdlib.h>
|
||||
#include <inttypes.h>
|
||||
#include <limits.h>
|
||||
#include <math.h>
|
||||
|
||||
#include "libimagequant.h"
|
||||
|
||||
using namespace emscripten;
|
||||
|
||||
int version() {
|
||||
return (((LIQ_VERSION/10000) % 100) << 16) |
|
||||
(((LIQ_VERSION/100 ) % 100) << 8) |
|
||||
(((LIQ_VERSION/1 ) % 100) << 0);
|
||||
}
|
||||
|
||||
class RawImage {
|
||||
public:
|
||||
val buffer;
|
||||
int width;
|
||||
int height;
|
||||
|
||||
RawImage(val b, int w, int h)
|
||||
: buffer(b), width(w), height(h) {}
|
||||
};
|
||||
|
||||
|
||||
liq_attr *attr;
|
||||
liq_image *image;
|
||||
liq_result *res;
|
||||
uint8_t* result;
|
||||
RawImage quantize(std::string rawimage, int image_width, int image_height, int num_colors, float dithering) {
|
||||
const uint8_t* image_buffer = (uint8_t*)rawimage.c_str();
|
||||
int size = image_width * image_height;
|
||||
attr = liq_attr_create();
|
||||
image = liq_image_create_rgba(attr, image_buffer, image_width, image_height, 0);
|
||||
liq_set_max_colors(attr, num_colors);
|
||||
liq_image_quantize(image, attr, &res);
|
||||
liq_set_dithering_level(res, dithering);
|
||||
uint8_t* image8bit = (uint8_t*) malloc(size);
|
||||
result = (uint8_t*) malloc(size * 4);
|
||||
liq_write_remapped_image(res, image, image8bit, size);
|
||||
const liq_palette *pal = liq_get_palette(res);
|
||||
// Turn palletted image back into an RGBA image
|
||||
for(int i = 0; i < size; i++) {
|
||||
result[i * 4 + 0] = pal->entries[image8bit[i]].r;
|
||||
result[i * 4 + 1] = pal->entries[image8bit[i]].g;
|
||||
result[i * 4 + 2] = pal->entries[image8bit[i]].b;
|
||||
result[i * 4 + 3] = pal->entries[image8bit[i]].a;
|
||||
}
|
||||
free(image8bit);
|
||||
liq_result_destroy(res);
|
||||
liq_image_destroy(image);
|
||||
liq_attr_destroy(attr);
|
||||
return {
|
||||
val(typed_memory_view(image_width*image_height*4, result)),
|
||||
image_width,
|
||||
image_height
|
||||
};
|
||||
}
|
||||
|
||||
const liq_color zx_colors[] = {
|
||||
{.a = 255, .r = 0, .g = 0, .b = 0}, // regular black
|
||||
{.a = 255, .r = 0, .g = 0, .b = 215}, // regular blue
|
||||
{.a = 255, .r = 215, .g = 0, .b = 0}, // regular red
|
||||
{.a = 255, .r = 215, .g = 0, .b = 215}, // regular magenta
|
||||
{.a = 255, .r = 0, .g = 215, .b = 0}, // regular green
|
||||
{.a = 255, .r = 0, .g = 215, .b = 215}, // regular cyan
|
||||
{.a = 255, .r = 215, .g = 215, .b = 0}, // regular yellow
|
||||
{.a = 255, .r = 215, .g = 215, .b = 215}, // regular white
|
||||
{.a = 255, .r = 0, .g = 0, .b = 255}, // bright blue
|
||||
{.a = 255, .r = 255, .g = 0, .b = 0}, // bright red
|
||||
{.a = 255, .r = 255, .g = 0, .b = 255}, // bright magenta
|
||||
{.a = 255, .r = 0, .g = 255, .b = 0}, // bright green
|
||||
{.a = 255, .r = 0, .g = 255, .b = 255}, // bright cyan
|
||||
{.a = 255, .r = 255, .g = 255, .b = 0}, // bright yellow
|
||||
{.a = 255, .r = 255, .g = 255, .b = 255} // bright white
|
||||
};
|
||||
|
||||
uint8_t block[8 * 8 * 4];
|
||||
|
||||
/**
|
||||
* The ZX has one bit per pixel, but can assign two colours to an 8x8 block. The two colours must
|
||||
* both be 'regular' or 'bright'. Black exists as both regular and bright.
|
||||
*/
|
||||
RawImage zx_quantize(std::string rawimage, int image_width, int image_height, float dithering) {
|
||||
const uint8_t* image_buffer = (uint8_t*) rawimage.c_str();
|
||||
int size = image_width * image_height;
|
||||
int bytes_per_pixel = 4;
|
||||
result = (uint8_t*) malloc(size * bytes_per_pixel);
|
||||
uint8_t* image8bit = (uint8_t*) malloc(8 * 8);
|
||||
|
||||
// For each 8x8 grid
|
||||
for (int block_start_y = 0; block_start_y < image_height; block_start_y += 8) {
|
||||
for (int block_start_x = 0; block_start_x < image_width; block_start_x += 8) {
|
||||
int color_popularity[15] = {0};
|
||||
int block_index = 0;
|
||||
int block_width = 8;
|
||||
int block_height = 8;
|
||||
|
||||
// If the block hangs off the right/bottom of the image dimensions, make it smaller to fit.
|
||||
if (block_start_y + block_height > image_height) {
|
||||
block_height = image_height - block_start_y;
|
||||
}
|
||||
|
||||
if (block_start_x + block_width > image_width) {
|
||||
block_width = image_width - block_start_x;
|
||||
}
|
||||
|
||||
// For each pixel in that block:
|
||||
for (int y = block_start_y; y < block_start_y + block_height; y++) {
|
||||
for (int x = block_start_x; x < block_start_x + block_width; x++) {
|
||||
int pixel_start = (y * image_width * bytes_per_pixel) + (x * bytes_per_pixel);
|
||||
int smallest_distance = INT_MAX;
|
||||
int winning_index = -1;
|
||||
|
||||
// Copy pixel data for quantizing later
|
||||
block[block_index++] = image_buffer[pixel_start];
|
||||
block[block_index++] = image_buffer[pixel_start + 1];
|
||||
block[block_index++] = image_buffer[pixel_start + 2];
|
||||
block[block_index++] = image_buffer[pixel_start + 3];
|
||||
|
||||
// Which zx color is this pixel closest to?
|
||||
for (int color_index = 0; color_index < 15; color_index++) {
|
||||
liq_color color = zx_colors[color_index];
|
||||
|
||||
// Using Euclidean distance. LibQuant has better methods, but it requires conversion to
|
||||
// LAB, so I don't think it's worth it.
|
||||
int distance =
|
||||
pow(color.r - image_buffer[pixel_start + 0], 2) +
|
||||
pow(color.g - image_buffer[pixel_start + 1], 2) +
|
||||
pow(color.b - image_buffer[pixel_start + 2], 2);
|
||||
|
||||
if (distance < smallest_distance) {
|
||||
winning_index = color_index;
|
||||
smallest_distance = distance;
|
||||
}
|
||||
}
|
||||
color_popularity[winning_index]++;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the three most popular colours for the block.
|
||||
int first_color_index = 0;
|
||||
int second_color_index = 0;
|
||||
int third_color_index = 0;
|
||||
int highest_popularity = -1;
|
||||
int second_highest_popularity = -1;
|
||||
int third_highest_popularity = -1;
|
||||
|
||||
for (int color_index = 0; color_index < 15; color_index++) {
|
||||
if (color_popularity[color_index] > highest_popularity) {
|
||||
// Store this as the most popular pixel, and demote the current values:
|
||||
third_color_index = second_color_index;
|
||||
third_highest_popularity = second_highest_popularity;
|
||||
second_color_index = first_color_index;
|
||||
second_highest_popularity = highest_popularity;
|
||||
first_color_index = color_index;
|
||||
highest_popularity = color_popularity[color_index];
|
||||
} else if (color_popularity[color_index] > second_highest_popularity) {
|
||||
third_color_index = second_color_index;
|
||||
third_highest_popularity = second_highest_popularity;
|
||||
second_color_index = color_index;
|
||||
second_highest_popularity = color_popularity[color_index];
|
||||
} else if (color_popularity[color_index] > third_highest_popularity) {
|
||||
third_color_index = color_index;
|
||||
third_highest_popularity = color_popularity[color_index];
|
||||
}
|
||||
}
|
||||
|
||||
// ZX images can't mix bright and regular colours, except black which appears in both.
|
||||
// Resolve any conflict:
|
||||
while (1) {
|
||||
// If either colour is black, there's no conflict to resolve.
|
||||
if (first_color_index != 0 && second_color_index != 0) {
|
||||
if (first_color_index >= 8 && second_color_index < 8) {
|
||||
// Make the second color bright
|
||||
second_color_index = second_color_index + 7;
|
||||
} else if (first_color_index < 8 && second_color_index >= 8) {
|
||||
// Make the second color regular
|
||||
second_color_index = second_color_index - 7;
|
||||
}
|
||||
}
|
||||
|
||||
// If, during conflict resolving, we now have two of the same colour (because we initially
|
||||
// selected the bright & regular version of the same colour), retry again with the third
|
||||
// most popular colour.
|
||||
if (first_color_index == second_color_index) {
|
||||
second_color_index = third_color_index;
|
||||
} else break;
|
||||
}
|
||||
|
||||
// Quantize
|
||||
attr = liq_attr_create();
|
||||
image = liq_image_create_rgba(attr, block, block_width, block_height, 0);
|
||||
liq_set_max_colors(attr, 2);
|
||||
liq_image_add_fixed_color(image, zx_colors[first_color_index]);
|
||||
liq_image_add_fixed_color(image, zx_colors[second_color_index]);
|
||||
liq_image_quantize(image, attr, &res);
|
||||
liq_set_dithering_level(res, dithering);
|
||||
liq_write_remapped_image(res, image, image8bit, size);
|
||||
const liq_palette *pal = liq_get_palette(res);
|
||||
|
||||
// Turn palletted image back into an RGBA image, and write it into the full size result image.
|
||||
for(int y = 0; y < block_height; y++) {
|
||||
for(int x = 0; x < block_width; x++) {
|
||||
int image8BitPos = y * block_width + x;
|
||||
int resultStartPos = ((block_start_y + y) * bytes_per_pixel * image_width) + ((block_start_x + x) * bytes_per_pixel);
|
||||
result[resultStartPos + 0] = pal->entries[image8bit[image8BitPos]].r;
|
||||
result[resultStartPos + 1] = pal->entries[image8bit[image8BitPos]].g;
|
||||
result[resultStartPos + 2] = pal->entries[image8bit[image8BitPos]].b;
|
||||
result[resultStartPos + 3] = pal->entries[image8bit[image8BitPos]].a;
|
||||
}
|
||||
}
|
||||
|
||||
liq_result_destroy(res);
|
||||
liq_image_destroy(image);
|
||||
liq_attr_destroy(attr);
|
||||
}
|
||||
}
|
||||
|
||||
free(image8bit);
|
||||
return {
|
||||
val(typed_memory_view(image_width*image_height*4, result)),
|
||||
image_width,
|
||||
image_height
|
||||
};
|
||||
}
|
||||
|
||||
void free_result() {
|
||||
free(result);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_BINDINGS(my_module) {
|
||||
class_<RawImage>("RawImage")
|
||||
.property("buffer", &RawImage::buffer)
|
||||
.property("width", &RawImage::width)
|
||||
.property("height", &RawImage::height);
|
||||
|
||||
function("quantize", &quantize);
|
||||
function("zx_quantize", &zx_quantize);
|
||||
function("version", &version);
|
||||
function("free_result", &free_result);
|
||||
}
|
15
codecs/imagequant/imagequant.d.ts
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
interface RawImage {
|
||||
buffer: Uint8Array;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface QuantizerModule extends EmscriptenWasm.Module {
|
||||
quantize(data: BufferSource, width: number, height: number, numColors: number, dither: number): RawImage;
|
||||
zx_quantize(data: BufferSource, width: number, height: number, dither: number): RawImage;
|
||||
free_result(): void;
|
||||
}
|
||||
|
||||
export default function(opts: EmscriptenWasm.ModuleOpts): QuantizerModule;
|
||||
|
||||
|
24
codecs/imagequant/imagequant.js
Normal file
BIN
codecs/imagequant/imagequant.wasm
Normal file
1147
codecs/imagequant/package-lock.json
generated
Normal file
13
codecs/imagequant/package.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "imagequant",
|
||||
"scripts": {
|
||||
"install": "napa",
|
||||
"build": "docker run --rm -v $(pwd):/src trzeci/emscripten ./build.sh"
|
||||
},
|
||||
"napa": {
|
||||
"libimagequant": "ImageOptim/libimagequant#2.12.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"napa": "^3.0.0"
|
||||
}
|
||||
}
|
43
codecs/mozjpeg_enc/README.md
Normal file
@ -0,0 +1,43 @@
|
||||
# MozJPEG encoder
|
||||
|
||||
- Source: <https://github.com/mozilla/mozjpeg>
|
||||
- Version: v3.3.1
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Docker
|
||||
|
||||
## Example
|
||||
|
||||
See `example.html`
|
||||
|
||||
## API
|
||||
|
||||
### `int version()`
|
||||
|
||||
Returns the version of MozJPEG as a number. va.b.c is encoded as 0x0a0b0c
|
||||
|
||||
### `void free_result()`
|
||||
|
||||
Frees the result created by `encode()`.
|
||||
|
||||
### `Uint8Array encode(std::string image_in, int image_width, int image_height, MozJpegOptions opts)`
|
||||
|
||||
Encodes the given image with given dimension to JPEG. Options looks like this:
|
||||
|
||||
```c++
|
||||
struct MozJpegOptions {
|
||||
int quality;
|
||||
bool baseline;
|
||||
bool arithmetic;
|
||||
bool progressive;
|
||||
bool optimize_coding;
|
||||
int smoothing;
|
||||
int color_space;
|
||||
int quant_table;
|
||||
bool trellis_multipass;
|
||||
bool trellis_opt_zero;
|
||||
bool trellis_opt_table;
|
||||
int trellis_loops;
|
||||
};
|
||||
```
|
53
codecs/mozjpeg_enc/build.sh
Executable file
@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
export OPTIMIZE="-Os"
|
||||
export LDFLAGS="${OPTIMIZE}"
|
||||
export CFLAGS="${OPTIMIZE}"
|
||||
export CPPFLAGS="${OPTIMIZE}"
|
||||
|
||||
apt-get update
|
||||
apt-get install -qqy autoconf libtool libpng-dev pkg-config
|
||||
|
||||
echo "============================================="
|
||||
echo "Compiling mozjpeg"
|
||||
echo "============================================="
|
||||
(
|
||||
cd node_modules/mozjpeg
|
||||
autoreconf -fiv
|
||||
emconfigure ./configure --without-simd
|
||||
emmake make libjpeg.la
|
||||
)
|
||||
echo "============================================="
|
||||
echo "Compiling mozjpeg done"
|
||||
echo "============================================="
|
||||
|
||||
echo "============================================="
|
||||
echo "Compiling wasm bindings"
|
||||
echo "============================================="
|
||||
(
|
||||
emcc \
|
||||
--bind \
|
||||
${OPTIMIZE} \
|
||||
-s WASM=1 \
|
||||
-s ALLOW_MEMORY_GROWTH=1 \
|
||||
-s MODULARIZE=1 \
|
||||
-s 'EXPORT_NAME="mozjpeg_enc"' \
|
||||
-I node_modules/mozjpeg \
|
||||
-o ./mozjpeg_enc.js \
|
||||
-Wno-deprecated-register \
|
||||
-Wno-writable-strings \
|
||||
node_modules/mozjpeg/rdswitch.c \
|
||||
-x c++ -std=c++11 \
|
||||
mozjpeg_enc.cpp \
|
||||
node_modules/mozjpeg/.libs/libjpeg.a
|
||||
)
|
||||
echo "============================================="
|
||||
echo "Compiling wasm bindings done"
|
||||
echo "============================================="
|
||||
|
||||
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
||||
echo "Did you update your docker image?"
|
||||
echo "Run \`docker pull trzeci/emscripten\`"
|
||||
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
44
codecs/mozjpeg_enc/example.html
Normal file
@ -0,0 +1,44 @@
|
||||
<!doctype html>
|
||||
<script src='mozjpeg_enc.js'></script>
|
||||
<script>
|
||||
const module = mozjpeg_enc();
|
||||
|
||||
async function loadImage(src) {
|
||||
// Load image
|
||||
const img = document.createElement('img');
|
||||
img.src = src;
|
||||
await new Promise(resolve => img.onload = resolve);
|
||||
// Make canvas same size as image
|
||||
const canvas = document.createElement('canvas');
|
||||
[canvas.width, canvas.height] = [img.width, img.height];
|
||||
// Draw image onto canvas
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
return ctx.getImageData(0, 0, img.width, img.height);
|
||||
}
|
||||
|
||||
module.onRuntimeInitialized = async _ => {
|
||||
console.log('Version:', module.version().toString(16));
|
||||
const image = await loadImage('../example.png');
|
||||
const result = module.encode(image.data, image.width, image.height, {
|
||||
quality: 40,
|
||||
baseline: false,
|
||||
arithmetic: false,
|
||||
progressive: true,
|
||||
optimize_coding: true,
|
||||
smoothing: 0,
|
||||
color_space: 3,
|
||||
quant_table: 3,
|
||||
trellis_multipass: true,
|
||||
trellis_opt_zero: true,
|
||||
trellis_opt_table: true,
|
||||
trellis_loops: 1,
|
||||
});
|
||||
|
||||
const blob = new Blob([result], {type: 'image/jpeg'});
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
const img = document.createElement('img');
|
||||
img.src = blobURL;
|
||||
document.body.appendChild(img);
|
||||
};
|
||||
</script>
|
217
codecs/mozjpeg_enc/mozjpeg_enc.cpp
Normal file
@ -0,0 +1,217 @@
|
||||
#include <emscripten/bind.h>
|
||||
#include <emscripten/val.h>
|
||||
#include <stdlib.h>
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
#include <setjmp.h>
|
||||
#include <string.h>
|
||||
#include "config.h"
|
||||
#include "jpeglib.h"
|
||||
#include "cdjpeg.h"
|
||||
|
||||
using namespace emscripten;
|
||||
|
||||
// MozJPEG doesn’t expose a numeric version, so I have to do some fun C macro hackery to turn it
|
||||
// into a string. More details here: https://gcc.gnu.org/onlinedocs/cpp/Stringizing.html
|
||||
#define xstr(s) str(s)
|
||||
#define str(s) #s
|
||||
|
||||
struct MozJpegOptions {
|
||||
int quality;
|
||||
bool baseline;
|
||||
bool arithmetic;
|
||||
bool progressive;
|
||||
bool optimize_coding;
|
||||
int smoothing;
|
||||
int color_space;
|
||||
int quant_table;
|
||||
bool trellis_multipass;
|
||||
bool trellis_opt_zero;
|
||||
bool trellis_opt_table;
|
||||
int trellis_loops;
|
||||
};
|
||||
|
||||
int version() {
|
||||
char buffer[] = xstr(MOZJPEG_VERSION);
|
||||
int version = 0;
|
||||
int last_index = 0;
|
||||
for(int i = 0; i < strlen(buffer); i++) {
|
||||
if(buffer[i] == '.') {
|
||||
buffer[i] = '\0';
|
||||
version = version << 8 | atoi(&buffer[last_index]);
|
||||
buffer[i] = '.';
|
||||
last_index = i + 1;
|
||||
}
|
||||
}
|
||||
version = version << 8 | atoi(&buffer[last_index]);
|
||||
return version;
|
||||
}
|
||||
|
||||
uint8_t* last_result;
|
||||
struct jpeg_compress_struct cinfo;
|
||||
|
||||
val encode(std::string image_in, int image_width, int image_height, MozJpegOptions opts) {
|
||||
uint8_t* image_buffer = (uint8_t*) image_in.c_str();
|
||||
|
||||
// The code below is basically the `write_JPEG_file` function from
|
||||
// https://github.com/mozilla/mozjpeg/blob/master/example.c
|
||||
// I just write to memory instead of a file.
|
||||
|
||||
|
||||
/* This struct contains the JPEG compression parameters and pointers to
|
||||
* working space (which is allocated as needed by the JPEG library).
|
||||
* It is possible to have several such structures, representing multiple
|
||||
* compression/decompression processes, in existence at once. We refer
|
||||
* to any one struct (and its associated working data) as a "JPEG object".
|
||||
*/
|
||||
/* This struct represents a JPEG error handler. It is declared separately
|
||||
* because applications often want to supply a specialized error handler
|
||||
* (see the second half of this file for an example). But here we just
|
||||
* take the easy way out and use the standard error handler, which will
|
||||
* print a message on stderr and call exit() if compression fails.
|
||||
* Note that this struct must live as long as the main JPEG parameter
|
||||
* struct, to avoid dangling-pointer problems.
|
||||
*/
|
||||
struct jpeg_error_mgr jerr;
|
||||
/* More stuff */
|
||||
JSAMPROW row_pointer[1]; /* pointer to JSAMPLE row[s] */
|
||||
int row_stride; /* physical row width in image buffer */
|
||||
uint8_t* output;
|
||||
unsigned long size;
|
||||
|
||||
/* Step 1: allocate and initialize JPEG compression object */
|
||||
|
||||
/* We have to set up the error handler first, in case the initialization
|
||||
* step fails. (Unlikely, but it could happen if you are out of memory.)
|
||||
* This routine fills in the contents of struct jerr, and returns jerr's
|
||||
* address which we place into the link field in cinfo.
|
||||
*/
|
||||
cinfo.err = jpeg_std_error(&jerr);
|
||||
/* Now we can initialize the JPEG compression object. */
|
||||
jpeg_create_compress(&cinfo);
|
||||
|
||||
/* Step 2: specify data destination (eg, a file) */
|
||||
/* Note: steps 2 and 3 can be done in either order. */
|
||||
|
||||
/* Here we use the library-supplied code to send compressed data to a
|
||||
* stdio stream. You can also write your own code to do something else.
|
||||
* VERY IMPORTANT: use "b" option to fopen() if you are on a machine that
|
||||
* requires it in order to write binary files.
|
||||
*/
|
||||
// if ((outfile = fopen(filename, "wb")) == NULL) {
|
||||
// fprintf(stderr, "can't open %s\n", filename);
|
||||
// exit(1);
|
||||
// }
|
||||
jpeg_mem_dest(&cinfo, &output, &size);
|
||||
|
||||
/* Step 3: set parameters for compression */
|
||||
|
||||
/* First we supply a description of the input image.
|
||||
* Four fields of the cinfo struct must be filled in:
|
||||
*/
|
||||
cinfo.image_width = image_width; /* image width and height, in pixels */
|
||||
cinfo.image_height = image_height;
|
||||
cinfo.input_components = 4; /* # of color components per pixel */
|
||||
cinfo.in_color_space = JCS_EXT_RGBA; /* colorspace of input image */
|
||||
/* Now use the library's routine to set default compression parameters.
|
||||
* (You must set at least cinfo.in_color_space before calling this,
|
||||
* since the defaults depend on the source color space.)
|
||||
*/
|
||||
jpeg_set_defaults(&cinfo);
|
||||
|
||||
/* Now you can set any non-default parameters you wish to.
|
||||
* Here we just illustrate the use of quality (quantization table) scaling:
|
||||
*/
|
||||
jpeg_set_colorspace(&cinfo, (J_COLOR_SPACE) opts.color_space);
|
||||
|
||||
if (opts.quant_table != -1) {
|
||||
jpeg_c_set_int_param(&cinfo, JINT_BASE_QUANT_TBL_IDX, opts.quant_table);
|
||||
}
|
||||
|
||||
cinfo.optimize_coding = opts.optimize_coding;
|
||||
|
||||
if (opts.arithmetic) {
|
||||
cinfo.arith_code = TRUE;
|
||||
cinfo.optimize_coding = FALSE;
|
||||
}
|
||||
|
||||
cinfo.smoothing_factor = opts.smoothing;
|
||||
|
||||
jpeg_c_set_bool_param(&cinfo, JBOOLEAN_USE_SCANS_IN_TRELLIS, opts.trellis_multipass);
|
||||
jpeg_c_set_bool_param(&cinfo, JBOOLEAN_TRELLIS_EOB_OPT, opts.trellis_opt_zero);
|
||||
jpeg_c_set_bool_param(&cinfo, JBOOLEAN_TRELLIS_Q_OPT, opts.trellis_opt_table);
|
||||
jpeg_c_set_int_param(&cinfo, JINT_TRELLIS_NUM_LOOPS, opts.trellis_loops);
|
||||
|
||||
std::string quality_str = std::to_string(opts.quality);
|
||||
char const *pqual = quality_str.c_str();
|
||||
|
||||
set_quality_ratings(&cinfo, (char*) pqual, opts.baseline);
|
||||
|
||||
if (!opts.baseline && opts.progressive) {
|
||||
jpeg_simple_progression(&cinfo);
|
||||
} else {
|
||||
cinfo.num_scans = 0;
|
||||
cinfo.scan_info = NULL;
|
||||
}
|
||||
/* Step 4: Start compressor */
|
||||
|
||||
/* TRUE ensures that we will write a complete interchange-JPEG file.
|
||||
* Pass TRUE unless you are very sure of what you're doing.
|
||||
*/
|
||||
jpeg_start_compress(&cinfo, TRUE);
|
||||
|
||||
/* Step 5: while (scan lines remain to be written) */
|
||||
/* jpeg_write_scanlines(...); */
|
||||
|
||||
/* Here we use the library's state variable cinfo.next_scanline as the
|
||||
* loop counter, so that we don't have to keep track ourselves.
|
||||
* To keep things simple, we pass one scanline per call; you can pass
|
||||
* more if you wish, though.
|
||||
*/
|
||||
row_stride = image_width * 4; /* JSAMPLEs per row in image_buffer */
|
||||
|
||||
while (cinfo.next_scanline < cinfo.image_height) {
|
||||
/* jpeg_write_scanlines expects an array of pointers to scanlines.
|
||||
* Here the array is only one element long, but you could pass
|
||||
* more than one scanline at a time if that's more convenient.
|
||||
*/
|
||||
row_pointer[0] = & image_buffer[cinfo.next_scanline * row_stride];
|
||||
(void) jpeg_write_scanlines(&cinfo, row_pointer, 1);
|
||||
}
|
||||
|
||||
/* Step 6: Finish compression */
|
||||
|
||||
jpeg_finish_compress(&cinfo);
|
||||
/* Step 7: release JPEG compression object */
|
||||
|
||||
last_result = output;
|
||||
|
||||
/* And we're done! */
|
||||
return val(typed_memory_view(size, output));
|
||||
}
|
||||
|
||||
void free_result() {
|
||||
/* This is an important step since it will release a good deal of memory. */
|
||||
jpeg_destroy_compress(&cinfo);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_BINDINGS(my_module) {
|
||||
value_object<MozJpegOptions>("MozJpegOptions")
|
||||
.field("quality", &MozJpegOptions::quality)
|
||||
.field("baseline", &MozJpegOptions::baseline)
|
||||
.field("arithmetic", &MozJpegOptions::arithmetic)
|
||||
.field("progressive", &MozJpegOptions::progressive)
|
||||
.field("optimize_coding", &MozJpegOptions::optimize_coding)
|
||||
.field("smoothing", &MozJpegOptions::smoothing)
|
||||
.field("color_space", &MozJpegOptions::color_space)
|
||||
.field("quant_table", &MozJpegOptions::quant_table)
|
||||
.field("trellis_multipass", &MozJpegOptions::trellis_multipass)
|
||||
.field("trellis_opt_zero", &MozJpegOptions::trellis_opt_zero)
|
||||
.field("trellis_opt_table", &MozJpegOptions::trellis_opt_table)
|
||||
.field("trellis_loops", &MozJpegOptions::trellis_loops)
|
||||
;
|
||||
|
||||
function("version", &version);
|
||||
function("encode", &encode);
|
||||
function("free_result", &free_result);
|
||||
}
|
8
codecs/mozjpeg_enc/mozjpeg_enc.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
import { EncodeOptions } from '../../src/codecs/mozjpeg/encoder-meta';
|
||||
|
||||
interface MozJPEGModule extends EmscriptenWasm.Module {
|
||||
encode(data: BufferSource, width: number, height: number, options: EncodeOptions): Uint8Array;
|
||||
free_result(): void;
|
||||
}
|
||||
|
||||
export default function(opts: EmscriptenWasm.ModuleOpts): MozJPEGModule;
|
24
codecs/mozjpeg_enc/mozjpeg_enc.js
Normal file
BIN
codecs/mozjpeg_enc/mozjpeg_enc.wasm
Normal file
1147
codecs/mozjpeg_enc/package-lock.json
generated
Normal file
13
codecs/mozjpeg_enc/package.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "mozjpeg_enc",
|
||||
"scripts": {
|
||||
"install": "napa",
|
||||
"build": "docker run --rm -v $(pwd):/src trzeci/emscripten ./build.sh"
|
||||
},
|
||||
"napa": {
|
||||
"mozjpeg": "mozilla/mozjpeg#v3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"napa": "^3.0.0"
|
||||
}
|
||||
}
|
2
codecs/optipng/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
build/
|
||||
*.o
|
26
codecs/optipng/README.md
Normal file
@ -0,0 +1,26 @@
|
||||
# OptiPNG
|
||||
|
||||
- Source: <https://sourceforge.net/project/optipng>
|
||||
- Version: v0.7.7
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Docker
|
||||
|
||||
## Example
|
||||
|
||||
See `example.html`
|
||||
|
||||
## API
|
||||
|
||||
### `int version()`
|
||||
|
||||
Returns the version of optipng as a number. va.b.c is encoded as 0x0a0b0c
|
||||
|
||||
### `ArrayBuffer compress(std::string buffer, {level})`;
|
||||
|
||||
`compress` will re-compress the given PNG image via `buffer`. `level` is a number between 0 and 7.
|
||||
|
||||
### `void free_result()`
|
||||
|
||||
Frees the result created by `compress()`.
|
87
codecs/optipng/build.sh
Executable file
@ -0,0 +1,87 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
export OPTIMIZE="-Os"
|
||||
export PREFIX="/src/build"
|
||||
export CFLAGS="${OPTIMIZE} -I${PREFIX}/include/"
|
||||
export CPPFLAGS="${OPTIMIZE} -I${PREFIX}/include/"
|
||||
export LDFLAGS="${OPTIMIZE} -L${PREFIX}/lib/"
|
||||
|
||||
apt-get update
|
||||
apt-get install -qqy autoconf libtool
|
||||
|
||||
echo "============================================="
|
||||
echo "Compiling zlib"
|
||||
echo "============================================="
|
||||
test -n "$SKIP_ZLIB" || (
|
||||
cd node_modules/zlib
|
||||
emconfigure ./configure --prefix=${PREFIX}/
|
||||
emmake make
|
||||
emmake make install
|
||||
)
|
||||
echo "============================================="
|
||||
echo "Compiling zlib done"
|
||||
echo "============================================="
|
||||
|
||||
echo "============================================="
|
||||
echo "Compiling libpng"
|
||||
echo "============================================="
|
||||
test -n "$SKIP_LIBPNG" || (
|
||||
cd node_modules/libpng
|
||||
autoreconf -i
|
||||
emconfigure ./configure --with-zlib-prefix=${PREFIX}/ --prefix=${PREFIX}/
|
||||
emmake make
|
||||
emmake make install
|
||||
)
|
||||
echo "============================================="
|
||||
echo "Compiling libpng done"
|
||||
echo "============================================="
|
||||
|
||||
echo "============================================="
|
||||
echo "Compiling optipng"
|
||||
echo "============================================="
|
||||
(
|
||||
emcc \
|
||||
${OPTIMIZE} \
|
||||
-Wno-implicit-function-declaration \
|
||||
-I ${PREFIX}/include \
|
||||
-I node_modules/optipng/src/opngreduc \
|
||||
-I node_modules/optipng/src/pngxtern \
|
||||
-I node_modules/optipng/src/cexcept \
|
||||
-I node_modules/optipng/src/gifread \
|
||||
-I node_modules/optipng/src/pnmio \
|
||||
-I node_modules/optipng/src/minitiff \
|
||||
--std=c99 -c \
|
||||
node_modules/optipng/src/opngreduc/*.c \
|
||||
node_modules/optipng/src/pngxtern/*.c \
|
||||
node_modules/optipng/src/gifread/*.c \
|
||||
node_modules/optipng/src/minitiff/*.c \
|
||||
node_modules/optipng/src/pnmio/*.c \
|
||||
node_modules/optipng/src/optipng/*.c
|
||||
|
||||
emcc \
|
||||
--bind \
|
||||
${OPTIMIZE} \
|
||||
-s ALLOW_MEMORY_GROWTH=1 -s MODULARIZE=1 -s 'EXPORT_NAME="optipng"' \
|
||||
-I ${PREFIX}/include \
|
||||
-I node_modules/optipng/src/opngreduc \
|
||||
-I node_modules/optipng/src/pngxtern \
|
||||
-I node_modules/optipng/src/cexcept \
|
||||
-I node_modules/optipng/src/gifread \
|
||||
-I node_modules/optipng/src/pnmio \
|
||||
-I node_modules/optipng/src/minitiff \
|
||||
-o "optipng.js" \
|
||||
--std=c++11 \
|
||||
optipng.cpp \
|
||||
*.o \
|
||||
${PREFIX}/lib/libz.so ${PREFIX}/lib/libpng.a
|
||||
)
|
||||
echo "============================================="
|
||||
echo "Compiling optipng done"
|
||||
echo "============================================="
|
||||
|
||||
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
||||
echo "Did you update your docker image?"
|
||||
echo "Run \`docker pull trzeci/emscripten\`"
|
||||
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
19
codecs/optipng/example.html
Normal file
@ -0,0 +1,19 @@
|
||||
<!doctype html>
|
||||
<script src='optipng.js'></script>
|
||||
<script>
|
||||
const Module = optipng();
|
||||
|
||||
Module.onRuntimeInitialized = async _ => {
|
||||
console.log('Version:', Module.version().toString(16));
|
||||
const image = await fetch('../example_palette.png').then(r => r.arrayBuffer());
|
||||
const newImage = Module.compress(image, {level: 3});
|
||||
console.log('done');
|
||||
Module.free_result();
|
||||
|
||||
console.log(`Old size: ${image.byteLength}, new size: ${newImage.byteLength} (${newImage.byteLength/image.byteLength*100}%)`);
|
||||
const blobURL = URL.createObjectURL(new Blob([newImage], {type: 'image/png'}));
|
||||
const img = document.createElement('img');
|
||||
img.src = blobURL;
|
||||
document.body.appendChild(img);
|
||||
};
|
||||
</script>
|
53
codecs/optipng/optipng.cpp
Normal file
@ -0,0 +1,53 @@
|
||||
#include "emscripten/bind.h"
|
||||
#include "emscripten/val.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
using namespace emscripten;
|
||||
|
||||
extern "C" int main(int argc, char *argv[]);
|
||||
|
||||
int version() {
|
||||
// FIXME (@surma): Haven’t found a version in optipng :(
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct OptiPngOpts {
|
||||
int level;
|
||||
};
|
||||
|
||||
uint8_t* result;
|
||||
val compress(std::string png, OptiPngOpts opts) {
|
||||
remove("input.png");
|
||||
remove("output.png");
|
||||
FILE* infile = fopen("input.png", "wb");
|
||||
fwrite(png.c_str(), png.length(), 1, infile);
|
||||
fflush(infile);
|
||||
fclose(infile);
|
||||
|
||||
char optlevel[8];
|
||||
sprintf(&optlevel[0], "-o%d", opts.level);
|
||||
char* args[] = {"optipng", optlevel, "-out", "output.png", "input.png"};
|
||||
main(5, args);
|
||||
|
||||
FILE *outfile = fopen("output.png", "rb");
|
||||
fseek(outfile, 0, SEEK_END);
|
||||
int fsize = ftell(outfile);
|
||||
result = (uint8_t*) malloc(fsize);
|
||||
fseek(outfile, 0, SEEK_SET);
|
||||
fread(result, fsize, 1, outfile);
|
||||
return val(typed_memory_view(fsize, result));
|
||||
}
|
||||
|
||||
void free_result() {
|
||||
free(result);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_BINDINGS(my_module) {
|
||||
value_object<OptiPngOpts>("OptiPngOpts")
|
||||
.field("level", &OptiPngOpts::level);
|
||||
|
||||
function("version", &version);
|
||||
function("compress", &compress);
|
||||
function("free_result", &free_result);
|
||||
}
|
10
codecs/optipng/optipng.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
import {EncodeOptions} from "src/codecs/optipng/encoder";
|
||||
|
||||
export interface OptiPngModule extends EmscriptenWasm.Module {
|
||||
compress(data: BufferSource, opts: EncodeOptions): Uint8Array;
|
||||
free_result(): void;
|
||||
}
|
||||
|
||||
export default function(opts: EmscriptenWasm.ModuleOpts): OptiPngModule;
|
||||
|
||||
|
24
codecs/optipng/optipng.js
Normal file
BIN
codecs/optipng/optipng.wasm
Normal file
1457
codecs/optipng/package-lock.json
generated
Normal file
22
codecs/optipng/package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "optipng",
|
||||
"scripts": {
|
||||
"install": "tar-dependency install && napa",
|
||||
"build": "npm run build:wasm",
|
||||
"build:wasm": "docker run --rm -v $(pwd):/src -e SKIP_ZLIB=\"${SKIP_ZLIB}\" -e SKIP_LIBPNG=\"${SKIP_LIBPNG}\" trzeci/emscripten ./build.sh"
|
||||
},
|
||||
"tarDependencies": {
|
||||
"node_modules/optipng": {
|
||||
"url": "https://netcologne.dl.sourceforge.net/project/optipng/OptiPNG/optipng-0.7.7/optipng-0.7.7.tar.gz",
|
||||
"strip": 1
|
||||
}
|
||||
},
|
||||
"napa": {
|
||||
"libpng": "emscripten-ports/libpng",
|
||||
"zlib": "emscripten-ports/zlib"
|
||||
},
|
||||
"dependencies": {
|
||||
"napa": "^3.0.0",
|
||||
"tar-dependency": "0.0.3"
|
||||
}
|
||||
}
|
22
codecs/webp_dec/README.md
Normal file
@ -0,0 +1,22 @@
|
||||
# WebP decoder
|
||||
|
||||
- Source: <https://github.com/webmproject/libwebp>
|
||||
- Version: v0.6.1
|
||||
|
||||
## Example
|
||||
|
||||
See `example.html`
|
||||
|
||||
## API
|
||||
|
||||
### `int version()`
|
||||
|
||||
Returns the version of libwebp as a number. va.b.c is encoded as 0x0a0b0c
|
||||
|
||||
### `RawImage decode(std::string buffer)`
|
||||
|
||||
Decodes the given webp buffer into raw RGBA. `RawImage` is a class with 3 fields: `buffer`, `width`, and `height`.
|
||||
|
||||
### `void free_result()`
|
||||
|
||||
Frees the result created by `decode()`.
|
34
codecs/webp_dec/build.sh
Executable file
@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
export OPTIMIZE="-Os"
|
||||
export LDFLAGS="${OPTIMIZE}"
|
||||
export CFLAGS="${OPTIMIZE}"
|
||||
export CPPFLAGS="${OPTIMIZE}"
|
||||
|
||||
echo "============================================="
|
||||
echo "Compiling wasm bindings"
|
||||
echo "============================================="
|
||||
(
|
||||
emcc \
|
||||
${OPTIMIZE} \
|
||||
--bind \
|
||||
-s ALLOW_MEMORY_GROWTH=1 \
|
||||
-s MODULARIZE=1 \
|
||||
-s 'EXPORT_NAME="webp_dec"' \
|
||||
--std=c++11 \
|
||||
-I node_modules/libwebp \
|
||||
-o ./webp_dec.js \
|
||||
node_modules/libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c \
|
||||
-x c++ \
|
||||
webp_dec.cpp
|
||||
)
|
||||
echo "============================================="
|
||||
echo "Compiling wasm bindings done"
|
||||
echo "============================================="
|
||||
|
||||
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
||||
echo "Did you update your docker image?"
|
||||
echo "Run \`docker pull trzeci/emscripten\`"
|
||||
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
24
codecs/webp_dec/example.html
Normal file
@ -0,0 +1,24 @@
|
||||
<!doctype html>
|
||||
<script src='webp_dec.js'></script>
|
||||
<script>
|
||||
const Module = webp_dec();
|
||||
|
||||
async function loadFile(src) {
|
||||
const resp = await fetch(src);
|
||||
return await resp.arrayBuffer();
|
||||
}
|
||||
|
||||
Module.onRuntimeInitialized = async _ => {
|
||||
console.log('Version:', Module.version().toString(16));
|
||||
const image = await loadFile('../example.webp');
|
||||
const result = Module.decode(image);
|
||||
const imageData = new ImageData(new Uint8ClampedArray(result.buffer), result.width, result.height);
|
||||
Module.free_result();
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = result.width;
|
||||
canvas.height = result.height;
|
||||
document.body.appendChild(canvas);
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
};
|
||||
</script>
|
1147
codecs/webp_dec/package-lock.json
generated
Normal file
13
codecs/webp_dec/package.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "webp_dec",
|
||||
"scripts": {
|
||||
"install": "napa",
|
||||
"build": "docker run --rm -v $(pwd):/src trzeci/emscripten ./build.sh"
|
||||
},
|
||||
"napa": {
|
||||
"libwebp": "webmproject/libwebp#v1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"napa": "^3.0.0"
|
||||
}
|
||||
}
|
47
codecs/webp_dec/webp_dec.cpp
Normal file
@ -0,0 +1,47 @@
|
||||
#include "emscripten/bind.h"
|
||||
#include "emscripten/val.h"
|
||||
#include "src/webp/decode.h"
|
||||
#include "src/webp/demux.h"
|
||||
#include <string>
|
||||
|
||||
using namespace emscripten;
|
||||
|
||||
int version() {
|
||||
return WebPGetDecoderVersion();
|
||||
}
|
||||
|
||||
class RawImage {
|
||||
public:
|
||||
val buffer;
|
||||
int width;
|
||||
int height;
|
||||
|
||||
RawImage(val b, int w, int h)
|
||||
: buffer(b), width(w), height(h) {}
|
||||
};
|
||||
|
||||
uint8_t* last_result;
|
||||
RawImage decode(std::string buffer) {
|
||||
int width, height;
|
||||
last_result = WebPDecodeRGBA((const uint8_t*)buffer.c_str(), buffer.size(), &width, &height);
|
||||
return RawImage(
|
||||
val(typed_memory_view(width*height*4, last_result)),
|
||||
width,
|
||||
height
|
||||
);
|
||||
}
|
||||
|
||||
void free_result() {
|
||||
free(last_result);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_BINDINGS(my_module) {
|
||||
class_<RawImage>("RawImage")
|
||||
.property("buffer", &RawImage::buffer)
|
||||
.property("width", &RawImage::width)
|
||||
.property("height", &RawImage::height);
|
||||
|
||||
function("decode", &decode);
|
||||
function("version", &version);
|
||||
function("free_result", &free_result);
|
||||
}
|
13
codecs/webp_dec/webp_dec.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
interface RawImage {
|
||||
buffer: Uint8Array;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface WebPModule extends EmscriptenWasm.Module {
|
||||
decode(data: BufferSource): RawImage;
|
||||
free_result(): void;
|
||||
}
|
||||
|
||||
export default function(opts: EmscriptenWasm.ModuleOpts): WebPModule;
|
||||
|
24
codecs/webp_dec/webp_dec.js
Normal file
BIN
codecs/webp_dec/webp_dec.wasm
Normal file
26
codecs/webp_enc/README.md
Normal file
@ -0,0 +1,26 @@
|
||||
# WebP encoder
|
||||
|
||||
- Source: <https://github.com/webmproject/libwebp>
|
||||
- Version: v0.6.1
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Docker
|
||||
|
||||
## Example
|
||||
|
||||
See `example.html`
|
||||
|
||||
## API
|
||||
|
||||
### `int version()`
|
||||
|
||||
Returns the version of libwebp as a number. va.b.c is encoded as 0x0a0b0c
|
||||
|
||||
### `UInt8Array encode(uint8_t* image_buffer, int image_width, int image_height, WebPConfig config)`
|
||||
|
||||
Encodes the given image with given dimension to WebP.
|
||||
|
||||
### `void free_result()`
|
||||
|
||||
Frees the last result created by `encode()`.
|
34
codecs/webp_enc/build.sh
Executable file
@ -0,0 +1,34 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
export OPTIMIZE="-Os"
|
||||
export LDFLAGS="${OPTIMIZE}"
|
||||
export CFLAGS="${OPTIMIZE}"
|
||||
export CPPFLAGS="${OPTIMIZE}"
|
||||
|
||||
echo "============================================="
|
||||
echo "Compiling wasm bindings"
|
||||
echo "============================================="
|
||||
(
|
||||
emcc \
|
||||
${OPTIMIZE} \
|
||||
--bind \
|
||||
-s ALLOW_MEMORY_GROWTH=1 \
|
||||
-s MODULARIZE=1 \
|
||||
-s 'EXPORT_NAME="webp_enc"' \
|
||||
--std=c++11 \
|
||||
-I node_modules/libwebp \
|
||||
-o ./webp_enc.js \
|
||||
node_modules/libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c \
|
||||
-x c++ \
|
||||
webp_enc.cpp
|
||||
)
|
||||
echo "============================================="
|
||||
echo "Compiling wasm bindings done"
|
||||
echo "============================================="
|
||||
|
||||
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
||||
echo "Did you update your docker image?"
|
||||
echo "Run \`docker pull trzeci/emscripten\`"
|
||||
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
62
codecs/webp_enc/example.html
Normal file
@ -0,0 +1,62 @@
|
||||
<!doctype html>
|
||||
<script src='webp_enc.js'></script>
|
||||
<script>
|
||||
const module = webp_enc();
|
||||
|
||||
async function loadImage(src) {
|
||||
// Load image
|
||||
const img = document.createElement('img');
|
||||
img.src = src;
|
||||
await new Promise(resolve => img.onload = resolve);
|
||||
// Make canvas same size as image
|
||||
const canvas = document.createElement('canvas');
|
||||
[canvas.width, canvas.height] = [img.width, img.height];
|
||||
// Draw image onto canvas
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
return ctx.getImageData(0, 0, img.width, img.height);
|
||||
}
|
||||
|
||||
module.onRuntimeInitialized = async _ => {
|
||||
console.log('Version:', module.version().toString(16));
|
||||
const image = await loadImage('../example.png');
|
||||
const result = module.encode(image.data, image.width, image.height, {
|
||||
quality: 75,
|
||||
target_size: 0,
|
||||
target_PSNR: 0,
|
||||
method: 4,
|
||||
sns_strength: 50,
|
||||
filter_strength: 60,
|
||||
filter_sharpness: 0,
|
||||
filter_type: 1,
|
||||
partitions: 0,
|
||||
segments: 4,
|
||||
pass: 1,
|
||||
show_compressed: 0,
|
||||
preprocessing: 0,
|
||||
autofilter: 0,
|
||||
partition_limit: 0,
|
||||
alpha_compression: 1,
|
||||
alpha_filtering: 1,
|
||||
alpha_quality: 100,
|
||||
lossless: 0,
|
||||
exact: 0,
|
||||
image_hint: 0,
|
||||
emulate_jpeg_size: 0,
|
||||
thread_level: 0,
|
||||
low_memory: 0,
|
||||
near_lossless: 100,
|
||||
use_delta_palette: 0,
|
||||
use_sharp_yuv: 0,
|
||||
});
|
||||
console.log('size', result.length);
|
||||
const blob = new Blob([result], {type: 'image/webp'});
|
||||
|
||||
module.free_result();
|
||||
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
const img = document.createElement('img');
|
||||
img.src = blobURL;
|
||||
document.body.appendChild(img);
|
||||
};
|
||||
</script>
|
1147
codecs/webp_enc/package-lock.json
generated
Normal file
13
codecs/webp_enc/package.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "webp_enc",
|
||||
"scripts": {
|
||||
"install": "napa",
|
||||
"build": "docker run --rm -v $(pwd):/src trzeci/emscripten ./build.sh"
|
||||
},
|
||||
"napa": {
|
||||
"libwebp": "webmproject/libwebp#v1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"napa": "^3.0.0"
|
||||
}
|
||||
}
|
95
codecs/webp_enc/webp_enc.cpp
Normal file
@ -0,0 +1,95 @@
|
||||
#include <emscripten/bind.h>
|
||||
#include <emscripten/val.h>
|
||||
#include "src/webp/encode.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdexcept>
|
||||
|
||||
using namespace emscripten;
|
||||
|
||||
int version() {
|
||||
return WebPGetEncoderVersion();
|
||||
}
|
||||
|
||||
uint8_t* last_result;
|
||||
|
||||
val encode(std::string img, int width, int height, WebPConfig config) {
|
||||
uint8_t* img_in = (uint8_t*) img.c_str();
|
||||
|
||||
// A lot of this is duplicated from Encode in picture_enc.c
|
||||
WebPPicture pic;
|
||||
WebPMemoryWriter wrt;
|
||||
int ok;
|
||||
|
||||
if (!WebPPictureInit(&pic)) {
|
||||
// shouldn't happen, except if system installation is broken
|
||||
throw std::runtime_error("Unexpected error");
|
||||
}
|
||||
|
||||
pic.use_argb = !!config.lossless;
|
||||
pic.width = width;
|
||||
pic.height = height;
|
||||
pic.writer = WebPMemoryWrite;
|
||||
pic.custom_ptr = &wrt;
|
||||
|
||||
WebPMemoryWriterInit(&wrt);
|
||||
|
||||
ok = WebPPictureImportRGBA(&pic, (uint8_t*) img_in, width * 4) && WebPEncode(&config, &pic);
|
||||
WebPPictureFree(&pic);
|
||||
if (!ok) {
|
||||
WebPMemoryWriterClear(&wrt);
|
||||
throw std::runtime_error("Encode failed");
|
||||
}
|
||||
|
||||
last_result = wrt.mem;
|
||||
|
||||
return val(typed_memory_view(wrt.size, wrt.mem));
|
||||
}
|
||||
|
||||
void free_result() {
|
||||
WebPFree(last_result);
|
||||
}
|
||||
|
||||
|
||||
EMSCRIPTEN_BINDINGS(my_module) {
|
||||
enum_<WebPImageHint>("WebPImageHint")
|
||||
.value("WEBP_HINT_DEFAULT", WebPImageHint::WEBP_HINT_DEFAULT)
|
||||
.value("WEBP_HINT_PICTURE", WebPImageHint::WEBP_HINT_PICTURE)
|
||||
.value("WEBP_HINT_PHOTO", WebPImageHint::WEBP_HINT_PHOTO)
|
||||
.value("WEBP_HINT_GRAPH", WebPImageHint::WEBP_HINT_GRAPH)
|
||||
;
|
||||
|
||||
value_object<WebPConfig>("WebPConfig")
|
||||
.field("lossless", &WebPConfig::lossless)
|
||||
.field("quality", &WebPConfig::quality)
|
||||
.field("method", &WebPConfig::method)
|
||||
.field("image_hint", &WebPConfig::image_hint)
|
||||
.field("target_size", &WebPConfig::target_size)
|
||||
.field("target_PSNR", &WebPConfig::target_PSNR)
|
||||
.field("segments", &WebPConfig::segments)
|
||||
.field("sns_strength", &WebPConfig::sns_strength)
|
||||
.field("filter_strength", &WebPConfig::filter_strength)
|
||||
.field("filter_sharpness", &WebPConfig::filter_sharpness)
|
||||
.field("filter_type", &WebPConfig::filter_type)
|
||||
.field("autofilter", &WebPConfig::autofilter)
|
||||
.field("alpha_compression", &WebPConfig::alpha_compression)
|
||||
.field("alpha_filtering", &WebPConfig::alpha_filtering)
|
||||
.field("alpha_quality", &WebPConfig::alpha_quality)
|
||||
.field("pass", &WebPConfig::pass)
|
||||
.field("show_compressed", &WebPConfig::show_compressed)
|
||||
.field("preprocessing", &WebPConfig::preprocessing)
|
||||
.field("partitions", &WebPConfig::partitions)
|
||||
.field("partition_limit", &WebPConfig::partition_limit)
|
||||
.field("emulate_jpeg_size", &WebPConfig::emulate_jpeg_size)
|
||||
.field("thread_level", &WebPConfig::thread_level)
|
||||
.field("low_memory", &WebPConfig::low_memory)
|
||||
.field("near_lossless", &WebPConfig::near_lossless)
|
||||
.field("exact", &WebPConfig::exact)
|
||||
.field("use_delta_palette", &WebPConfig::use_delta_palette)
|
||||
.field("use_sharp_yuv", &WebPConfig::use_sharp_yuv)
|
||||
;
|
||||
|
||||
function("version", &version);
|
||||
function("encode", &encode);
|
||||
function("free_result", &free_result);
|
||||
}
|
9
codecs/webp_enc/webp_enc.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
import { EncodeOptions } from '../../src/codecs/webp/encoder-meta';
|
||||
|
||||
interface WebPModule extends EmscriptenWasm.Module {
|
||||
encode(data: BufferSource, width: number, height: number, options: EncodeOptions): Uint8Array;
|
||||
free_result(): void;
|
||||
}
|
||||
|
||||
|
||||
export default function(opts: EmscriptenWasm.ModuleOpts): WebPModule;
|
24
codecs/webp_enc/webp_enc.js
Normal file
BIN
codecs/webp_enc/webp_enc.wasm
Normal file
@ -1,409 +0,0 @@
|
||||
const path = require('path');
|
||||
const parse5 = require('parse5');
|
||||
const nwmatcher = require('nwmatcher');
|
||||
const css = require('css');
|
||||
const prettyBytes = require('pretty-bytes');
|
||||
|
||||
const treeAdapter = parse5.treeAdapters.htmlparser2;
|
||||
|
||||
const PLUGIN_NAME = 'critters-webpack-plugin';
|
||||
|
||||
const PARSE5_OPTS = {
|
||||
treeAdapter
|
||||
};
|
||||
|
||||
/** Critters: Webpack Plugin Edition!
|
||||
* @class
|
||||
* @param {Object} options
|
||||
* @param {Boolean} [options.external=true] Fetch and inline critical styles from external stylesheets
|
||||
* @param {Boolean} [options.async=false] Convert critical-inlined external stylesheets to load asynchronously (via link rel="preload" - see https://filamentgroup.com/lab/async-css.html)
|
||||
* @param {Boolean} [options.preload=false] (requires `async` option) Append a new <link rel="stylesheet"> into <body> instead of swapping the preload's rel attribute
|
||||
* @param {Boolean} [options.fonts] If `true`, keeps critical `@font-face` rules and preloads them. If `false`, removes the rules and does not preload the fonts
|
||||
* @param {Boolean} [options.preloadFonts=false] Preloads critical fonts (even those removed by `{fonts:false}`)
|
||||
* @param {Boolean} [options.removeFonts=false] Remove all fonts (even critical ones)
|
||||
* @param {Boolean} [options.compress=true] Compress resulting critical CSS
|
||||
*/
|
||||
module.exports = class CrittersWebpackPlugin {
|
||||
constructor (options) {
|
||||
this.options = options || {};
|
||||
this.urlFilter = this.options.filter;
|
||||
if (this.urlFilter instanceof RegExp) {
|
||||
this.urlFilter = this.urlFilter.test.bind(this.urlFilter);
|
||||
}
|
||||
}
|
||||
|
||||
/** Invoked by Webpack during plugin initialization */
|
||||
apply (compiler) {
|
||||
// hook into the compiler to get a Compilation instance...
|
||||
compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => {
|
||||
// ... which is how we get an "after" hook into html-webpack-plugin's HTML generation.
|
||||
compilation.hooks.htmlWebpackPluginAfterHtmlProcessing.tapAsync(PLUGIN_NAME, (htmlPluginData, callback) => {
|
||||
this.process(compiler, compilation, htmlPluginData)
|
||||
.then(result => { callback(null, result); })
|
||||
.catch(callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
readFile (filename, encoding) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.fs.readFile(filename, encoding, (err, data) => {
|
||||
if (err) reject(err);
|
||||
else resolve(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async process (compiler, compilation, htmlPluginData) {
|
||||
const outputPath = compiler.options.output.path;
|
||||
|
||||
// Parse the generated HTML in a DOM we can mutate
|
||||
const document = parse5.parse(htmlPluginData.html, PARSE5_OPTS);
|
||||
makeDomInteractive(document);
|
||||
|
||||
// `external:false` skips processing of external sheets
|
||||
if (this.options.external !== false) {
|
||||
const externalSheets = document.querySelectorAll('link[rel="stylesheet"]');
|
||||
await Promise.all(externalSheets.map(
|
||||
link => this.embedLinkedStylesheet(link, compilation, outputPath)
|
||||
));
|
||||
}
|
||||
|
||||
// go through all the style tags in the document and reduce them to only critical CSS
|
||||
const styles = document.querySelectorAll('style');
|
||||
await Promise.all(styles.map(
|
||||
style => this.processStyle(style, document)
|
||||
));
|
||||
|
||||
// serialize the document back to HTML and we're done
|
||||
const html = parse5.serialize(document, PARSE5_OPTS);
|
||||
return { html };
|
||||
}
|
||||
|
||||
/** Inline the target stylesheet referred to by a <link rel="stylesheet"> (assuming it passes `options.filter`) */
|
||||
async embedLinkedStylesheet (link, compilation, outputPath) {
|
||||
const href = link.getAttribute('href');
|
||||
const document = link.ownerDocument;
|
||||
|
||||
// skip filtered resources, or network resources if no filter is provided
|
||||
if (this.urlFilter ? this.urlFilter(href) : href.match(/^(https?:)?\/\//)) return Promise.resolve();
|
||||
|
||||
// path on disk
|
||||
const filename = path.resolve(outputPath, href.replace(/^\//, ''));
|
||||
|
||||
// try to find a matching asset by filename in webpack's output (not yet written to disk)
|
||||
const asset = compilation.assets[path.relative(outputPath, filename).replace(/^\.\//, '')];
|
||||
|
||||
// CSS loader is only injected for the first sheet, then this becomes an empty string
|
||||
let cssLoaderPreamble = `function $loadcss(u,l){(l=document.createElement('link')).rel='stylesheet';l.href=u;document.head.appendChild(l)}`;
|
||||
|
||||
const media = typeof this.options.media === 'string' ? this.options.media : 'all';
|
||||
|
||||
// { preload:'js', media:true }
|
||||
// { preload:'js', media:'print' }
|
||||
if (this.options.media) {
|
||||
cssLoaderPreamble = cssLoaderPreamble.replace('l.href', "l.media='only x';l.onload=function(){l.media='" + media + "'};l.href");
|
||||
}
|
||||
|
||||
// Attempt to read from assets, falling back to a disk read
|
||||
const sheet = asset ? asset.source() : await this.readFile(filename, 'utf8');
|
||||
|
||||
// the reduced critical CSS gets injected into a new <style> tag
|
||||
const style = document.createElement('style');
|
||||
style.appendChild(document.createTextNode(sheet));
|
||||
link.parentNode.insertBefore(style, link.nextSibling);
|
||||
|
||||
// drop a reference to the original URL onto the tag (used for reporting to console later)
|
||||
style.$$name = href;
|
||||
|
||||
// the `async` option changes any critical'd <link rel="stylesheet"> tags to async-loaded equivalents
|
||||
if (this.options.async) {
|
||||
link.setAttribute('rel', 'preload');
|
||||
link.setAttribute('as', 'style');
|
||||
if (this.options.preload === 'js') {
|
||||
const script = document.createElement('script');
|
||||
script.appendChild(document.createTextNode(`${cssLoaderPreamble}$loadcss(${JSON.stringify(href)})`));
|
||||
link.parentNode.insertBefore(script, link.nextSibling);
|
||||
cssLoaderPreamble = '';
|
||||
} else if (this.options.preload) {
|
||||
const bodyLink = document.createElement('link');
|
||||
bodyLink.setAttribute('rel', 'stylesheet');
|
||||
bodyLink.setAttribute('href', href);
|
||||
document.body.appendChild(bodyLink);
|
||||
} else if (this.options.media) {
|
||||
// @see https://github.com/filamentgroup/loadCSS/blob/af1106cfe0bf70147e22185afa7ead96c01dec48/src/loadCSS.js#L26
|
||||
link.setAttribute('rel', 'stylesheet');
|
||||
link.removeAttribute('as');
|
||||
link.setAttribute('media', 'only x');
|
||||
link.setAttribute('onload', "this.media='" + media + "'");
|
||||
} else {
|
||||
link.setAttribute('onload', "this.rel='stylesheet'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Parse the stylesheet within a <style> element, then reduce it to contain only rules used by the document. */
|
||||
async processStyle (style) {
|
||||
const options = this.options;
|
||||
const document = style.ownerDocument;
|
||||
const head = document.querySelector('head');
|
||||
|
||||
// basically `.textContent`
|
||||
let sheet = style.childNodes.length > 0 && style.childNodes.map(node => node.nodeValue).join('\n');
|
||||
|
||||
// store a reference to the previous serialized stylesheet for reporting stats
|
||||
const before = sheet;
|
||||
|
||||
// Skip empty stylesheets
|
||||
if (!sheet) return;
|
||||
|
||||
const ast = css.parse(sheet);
|
||||
|
||||
// a string to search for font names (very loose)
|
||||
let criticalFonts = '';
|
||||
|
||||
// Walk all CSS rules, transforming unused rules to comments (which get removed)
|
||||
visit(ast, rule => {
|
||||
if (rule.type === 'rule') {
|
||||
// Filter the selector list down to only those matche
|
||||
rule.selectors = rule.selectors.filter(sel => {
|
||||
// Strip pseudo-elements and pseudo-classes, since we only care that their associated elements exist.
|
||||
// This means any selector for a pseudo-element or having a pseudo-class will be inlined if the rest of the selector matches.
|
||||
sel = sel.replace(/::?(?:[a-z-]+)([.[#~&^:*]|\s|\n|$)/gi, '$1');
|
||||
return document.querySelector(sel, document) != null;
|
||||
});
|
||||
// If there are no matched selectors, remove the rule:
|
||||
if (rule.selectors.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rule.declarations) {
|
||||
for (let i = 0; i < rule.declarations.length; i++) {
|
||||
const decl = rule.declarations[i];
|
||||
if (decl.property.match(/\bfont\b/i)) {
|
||||
criticalFonts += ' ' + decl.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// keep font rules, they're handled in the second pass:
|
||||
if (rule.type === 'font-face') return;
|
||||
|
||||
// If there are no remaining rules, remove the whole rule:
|
||||
return !rule.rules || rule.rules.length !== 0;
|
||||
});
|
||||
|
||||
const preloadedFonts = [];
|
||||
visit(ast, rule => {
|
||||
// only process @font-face rules in the second pass
|
||||
if (rule.type !== 'font-face') return;
|
||||
|
||||
let family, src;
|
||||
for (let i = 0; i < rule.declarations.length; i++) {
|
||||
const decl = rule.declarations[i];
|
||||
if (decl.property === 'src') {
|
||||
// @todo parse this properly and generate multiple preloads with type="font/woff2" etc
|
||||
src = (decl.value.match(/url\s*\(\s*(['"]?)(.+?)\1\s*\)/) || [])[2];
|
||||
} else if (decl.property === 'font-family') {
|
||||
family = decl.value;
|
||||
}
|
||||
}
|
||||
|
||||
if (src && (options.fonts === true || options.preloadFonts) && preloadedFonts.indexOf(src) === -1) {
|
||||
preloadedFonts.push(src);
|
||||
const preload = document.createElement('link');
|
||||
preload.setAttribute('rel', 'preload');
|
||||
preload.setAttribute('as', 'font');
|
||||
if (src.match(/:\/\//)) {
|
||||
preload.setAttribute('crossorigin', 'anonymous');
|
||||
}
|
||||
preload.setAttribute('href', src.trim());
|
||||
head.appendChild(preload);
|
||||
}
|
||||
|
||||
// if we're missing info or the font is unused, remove the rule:
|
||||
if (!family || !src || criticalFonts.indexOf(family) === -1 || !options.fonts || options.removeFonts) return false;
|
||||
});
|
||||
|
||||
sheet = css.stringify(ast, { compress: this.options.compress !== false });
|
||||
|
||||
// If all rules were removed, get rid of the style element entirely
|
||||
if (sheet.trim().length === 0) {
|
||||
sheet.parentNode.removeChild(sheet);
|
||||
} else {
|
||||
// replace the inline stylesheet with its critical'd counterpart
|
||||
while (style.lastChild) {
|
||||
style.removeChild(style.lastChild);
|
||||
}
|
||||
style.appendChild(document.createTextNode(sheet));
|
||||
}
|
||||
|
||||
// output some stats
|
||||
const name = style.$$name ? style.$$name.replace(/^\//, '') : 'inline CSS';
|
||||
const percent = sheet.length / before.length * 100 | 0;
|
||||
console.log('\u001b[32mCritters: inlined ' + prettyBytes(sheet.length) + ' (' + percent + '% of original ' + prettyBytes(before.length) + ') of ' + name + '.\u001b[39m');
|
||||
}
|
||||
};
|
||||
|
||||
/** Recursively walk all rules in a stylesheet.
|
||||
* The iterator can explicitly return `false` to remove the current node.
|
||||
*/
|
||||
function visit (node, fn) {
|
||||
if (node.stylesheet) return visit(node.stylesheet, fn);
|
||||
|
||||
node.rules = node.rules.filter(rule => {
|
||||
if (rule.rules) {
|
||||
visit(rule, fn);
|
||||
}
|
||||
return fn(rule) !== false;
|
||||
});
|
||||
}
|
||||
|
||||
/** Enhance an htmlparser2-style DOM with basic manipulation methods. */
|
||||
function makeDomInteractive (document) {
|
||||
defineProperties(document, DocumentExtensions);
|
||||
// Find the first <html> element within the document
|
||||
// document.documentElement = document.childNodes.filter( child => String(child.tagName).toLowerCase()==='html' )[0];
|
||||
|
||||
// Extend Element.prototype with DOM manipulation methods.
|
||||
// Note: document.$$scratchElement is also used by createTextNode()
|
||||
const scratch = document.$$scratchElement = document.createElement('div');
|
||||
const elementProto = Object.getPrototypeOf(scratch);
|
||||
defineProperties(elementProto, ElementExtensions);
|
||||
elementProto.ownerDocument = document;
|
||||
|
||||
// nwmatcher is a selector engine that happens to work with Parse5's htmlparser2 DOM (they form the base of jsdom).
|
||||
// It is exposed to the document so that it can be used within Element.prototype methods.
|
||||
document.$match = nwmatcher({ document });
|
||||
document.$match.configure({
|
||||
CACHING: false,
|
||||
USE_QSAPI: false,
|
||||
USE_HTML5: false
|
||||
});
|
||||
}
|
||||
|
||||
/** Essentially Object.defineProperties() except any functions are assigned as values rather than descriptors. */
|
||||
function defineProperties (obj, properties) {
|
||||
for (const i in properties) {
|
||||
const value = properties[i];
|
||||
Object.defineProperty(obj, i, typeof value === 'function' ? { value } : value);
|
||||
}
|
||||
}
|
||||
|
||||
/** {document,Element}.getElementsByTagName() is the only traversal method required by nwmatcher.
|
||||
* Note: if perf issues arise, 2 faster but more verbose implementations are benchmarked here:
|
||||
* https://esbench.com/bench/5ac3b647f2949800a0f619e1
|
||||
*/
|
||||
function getElementsByTagName (tagName) {
|
||||
// Only return Element/Document nodes
|
||||
if ((this.nodeType !== 1 && this.nodeType !== 9) || this.type === 'directive') return [];
|
||||
return Array.prototype.concat.apply(
|
||||
// Add current element if it matches tag
|
||||
(tagName === '*' || (this.tagName && (this.tagName === tagName || this.nodeName === tagName.toUpperCase()))) ? [this] : [],
|
||||
// Check children recursively
|
||||
this.children.map(child => getElementsByTagName.call(child, tagName))
|
||||
);
|
||||
}
|
||||
|
||||
const reflectedProperty = attributeName => ({
|
||||
get () {
|
||||
return this.getAttribute(attributeName);
|
||||
},
|
||||
set (value) {
|
||||
this.setAttribute(attributeName, value);
|
||||
}
|
||||
});
|
||||
|
||||
/** Methods and descriptors to mix into Element.prototype */
|
||||
const ElementExtensions = {
|
||||
nodeName: {
|
||||
get () {
|
||||
return this.tagName.toUpperCase();
|
||||
}
|
||||
},
|
||||
id: reflectedProperty('id'),
|
||||
className: reflectedProperty('class'),
|
||||
insertBefore (child, referenceNode) {
|
||||
if (!referenceNode) return this.appendChild(child);
|
||||
treeAdapter.insertBefore(this, child, referenceNode);
|
||||
return child;
|
||||
},
|
||||
appendChild (child) {
|
||||
treeAdapter.appendChild(this, child);
|
||||
return child;
|
||||
},
|
||||
removeChild (child) {
|
||||
treeAdapter.detachNode(child);
|
||||
},
|
||||
setAttribute (name, value) {
|
||||
if (this.attribs == null) this.attribs = {};
|
||||
if (value == null) value = '';
|
||||
this.attribs[name] = value;
|
||||
},
|
||||
removeAttribute (name) {
|
||||
if (this.attribs != null) {
|
||||
delete this.attribs[name];
|
||||
}
|
||||
},
|
||||
getAttribute (name) {
|
||||
return this.attribs != null && this.attribs[name];
|
||||
},
|
||||
hasAttribute (name) {
|
||||
return this.attribs != null && this.attribs[name] != null;
|
||||
},
|
||||
getAttributeNode (name) {
|
||||
const value = this.getAttribute(name);
|
||||
if (value != null) return { specified: true, value };
|
||||
},
|
||||
getElementsByTagName
|
||||
};
|
||||
|
||||
/** Methods and descriptors to mix into the global document instance */
|
||||
const DocumentExtensions = {
|
||||
// document is just an Element in htmlparser2, giving it a nodeType of ELEMENT_NODE.
|
||||
// nwmatcher requires that it at least report a correct nodeType of DOCUMENT_NODE.
|
||||
nodeType: {
|
||||
get () {
|
||||
return 9;
|
||||
}
|
||||
},
|
||||
nodeName: {
|
||||
get () {
|
||||
return '#document';
|
||||
}
|
||||
},
|
||||
documentElement: {
|
||||
get () {
|
||||
// Find the first <html> element within the document
|
||||
return this.childNodes.filter(child => String(child.tagName).toLowerCase() === 'html')[0];
|
||||
}
|
||||
},
|
||||
body: {
|
||||
get () {
|
||||
return this.querySelector('body');
|
||||
}
|
||||
},
|
||||
createElement (name) {
|
||||
return treeAdapter.createElement(name, null, []);
|
||||
},
|
||||
createTextNode (text) {
|
||||
// there is no dedicated createTextNode equivalent in htmlparser2's DOM, so
|
||||
// we have to insert Text and then remove and return the resulting Text node.
|
||||
const scratch = this.$$scratchElement;
|
||||
treeAdapter.insertText(scratch, text);
|
||||
const node = scratch.lastChild;
|
||||
treeAdapter.detachNode(node);
|
||||
return node;
|
||||
},
|
||||
querySelector (sel) {
|
||||
return this.$match.first(sel, this.documentElement);
|
||||
},
|
||||
querySelectorAll (sel) {
|
||||
return this.$match.select(sel, this.documentElement);
|
||||
},
|
||||
getElementsByTagName,
|
||||
// nwmatcher uses inexistence of `document.addEventListener` to detect IE:
|
||||
// https://github.com/dperini/nwmatcher/blob/3edb471e12ce7f7d46dc1606c7f659ff45675a29/src/nwmatcher.js#L353
|
||||
addEventListener: Object
|
||||
};
|
@ -1,64 +0,0 @@
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
|
||||
module.exports = function (content) {
|
||||
const jsdom = require('jsdom');
|
||||
const preact = require('preact');
|
||||
const renderToString = require('preact-render-to-string');
|
||||
|
||||
this.cacheable && this.cacheable();
|
||||
|
||||
const callback = this.async();
|
||||
|
||||
// const dom = new jsdom.JSDOM(`<!DOCTYPE html><html><head></head><body></body></html>`, {
|
||||
const dom = new jsdom.JSDOM(content, {
|
||||
includeNodeLocations: false,
|
||||
runScripts: 'outside-only'
|
||||
});
|
||||
const { window } = dom;
|
||||
const { document } = window;
|
||||
|
||||
// console.log(content);
|
||||
|
||||
const root = document.getElementById('app');
|
||||
this.loadModule(path.join(__dirname, 'client-boot.js'), (err, source) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
console.log(source);
|
||||
|
||||
let mod = eval(source);
|
||||
let props = {};
|
||||
// console.log(mod);
|
||||
let vnode = preact.createElement(mod, props);
|
||||
let frag = document.createElement('div');
|
||||
frag.innerHTML = renderToString(vnode);
|
||||
root.parentNode.replaceChild(frag.firstChild, root);
|
||||
|
||||
let html = dom.serialize();
|
||||
callback(null, html);
|
||||
// return html = `module.exports = ${JSON.stringify(html)}`;
|
||||
// return 'module.exports = ' + JSON.stringify(content).replace(/\{\{PRERENDER\}\}/gi, `" + require("preact-render-to-string")(require("app-entry-point")) + "`);
|
||||
});
|
||||
|
||||
// global.window = global;
|
||||
// global.document = {};
|
||||
// return 'module.exports = ' + JSON.stringify(content).replace(/\{\{PRERENDER\}\}/gi, `" + require("preact-render-to-string")(require("app-entry-point")) + "`);
|
||||
|
||||
/*
|
||||
let callback = this.async();
|
||||
|
||||
let parts = content.split(/\{\{prerender\}\}/gi);
|
||||
|
||||
if (parts.length<2) {
|
||||
// callback(null, `module.exports = ${JSON.stringify(content)}`);
|
||||
callback(null, content);
|
||||
return;
|
||||
}
|
||||
|
||||
// let html = `
|
||||
// window = {};
|
||||
// module.exports = ${JSON.stringify(parts[0])} + require("preact-render-to-string")(require("app-entry-point")) + ${JSON.stringify(parts[1])}`;
|
||||
let html = `module.exports = ${JSON.stringify(parts[0])} + require("preact-render-to-string")(require("app-entry-point")) + ${JSON.stringify(parts[1])}`;
|
||||
callback(null, html);
|
||||
*/
|
||||
};
|
@ -1,20 +0,0 @@
|
||||
let path = require('path');
|
||||
let preact = require('preact');
|
||||
let renderToString = require('preact-render-to-string');
|
||||
|
||||
let appPath = path.join(__dirname, '../src/index');
|
||||
|
||||
module.exports = function(options) {
|
||||
options = options || {};
|
||||
let url = typeof options==='string' ? options : options.url;
|
||||
global.history = {};
|
||||
global.location = { href: url, pathname: url };
|
||||
|
||||
// let app = require('app-entry-point');
|
||||
let app = require(appPath);
|
||||
|
||||
let html = renderToString(preact.h(app, { url }));
|
||||
console.log(html);
|
||||
|
||||
return html;
|
||||
};
|
107
emscripten-wasm.d.ts
vendored
Normal file
@ -0,0 +1,107 @@
|
||||
// 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?
|
||||
declare namespace EmscriptenWasm {
|
||||
type EnvironmentType = "WEB" | "NODE" | "SHELL" | "WORKER";
|
||||
|
||||
// Options object for modularized Emscripten files. Shoe-horned by @surma.
|
||||
// FIXME: This an incomplete definition!
|
||||
interface ModuleOpts {
|
||||
noInitialRun?: boolean;
|
||||
locateFile?: (url: string) => string;
|
||||
onRuntimeInitialized?: () => void;
|
||||
}
|
||||
|
||||
interface Module {
|
||||
print(str: string): void;
|
||||
printErr(str: string): void;
|
||||
arguments: string[];
|
||||
environment: EnvironmentType;
|
||||
preInit: { (): void }[];
|
||||
preRun: { (): void }[];
|
||||
postRun: { (): void }[];
|
||||
preinitializedWebGLContext: WebGLRenderingContext;
|
||||
noInitialRun: boolean;
|
||||
noExitRuntime: boolean;
|
||||
logReadFiles: boolean;
|
||||
filePackagePrefixURL: string;
|
||||
wasmBinary: ArrayBuffer;
|
||||
|
||||
destroy(object: object): void;
|
||||
getPreloadedPackage(remotePackageName: string, remotePackageSize: number): ArrayBuffer;
|
||||
instantiateWasm(
|
||||
imports: WebAssembly.Imports,
|
||||
successCallback: (module: WebAssembly.Module) => void
|
||||
): WebAssembly.Exports;
|
||||
locateFile(url: string): string;
|
||||
onCustomMessage(event: MessageEvent): void;
|
||||
|
||||
Runtime: any;
|
||||
|
||||
ccall(ident: string, returnType: string | null, argTypes: string[], args: any[]): any;
|
||||
cwrap(ident: string, returnType: string | null, argTypes: string[]): any;
|
||||
|
||||
setValue(ptr: number, value: any, type: string, noSafe?: boolean): void;
|
||||
getValue(ptr: number, type: string, noSafe?: boolean): number;
|
||||
|
||||
ALLOC_NORMAL: number;
|
||||
ALLOC_STACK: number;
|
||||
ALLOC_STATIC: number;
|
||||
ALLOC_DYNAMIC: number;
|
||||
ALLOC_NONE: 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;
|
||||
UTF16ToString(ptr: number): string;
|
||||
stringToUTF16(str: string, outPtr: number): void;
|
||||
UTF32ToString(ptr: number): string;
|
||||
stringToUTF32(str: string, outPtr: number): void;
|
||||
|
||||
// USE_TYPED_ARRAYS == 1
|
||||
HEAP: Int32Array;
|
||||
IHEAP: Int32Array;
|
||||
FHEAP: Float64Array;
|
||||
|
||||
// USE_TYPED_ARRAYS == 2
|
||||
HEAP8: Int8Array;
|
||||
HEAP16: Int16Array;
|
||||
HEAP32: Int32Array;
|
||||
HEAPU8: Uint8Array;
|
||||
HEAPU16: Uint16Array;
|
||||
HEAPU32: Uint32Array;
|
||||
HEAPF32: Float32Array;
|
||||
HEAPF64: Float64Array;
|
||||
|
||||
TOTAL_STACK: number;
|
||||
TOTAL_MEMORY: number;
|
||||
FAST_MEMORY: number;
|
||||
|
||||
addOnPreRun(cb: () => any): void;
|
||||
addOnInit(cb: () => any): void;
|
||||
addOnPreMain(cb: () => any): void;
|
||||
addOnExit(cb: () => any): void;
|
||||
addOnPostRun(cb: () => any): void;
|
||||
|
||||
// Tools
|
||||
intArrayFromString(stringy: string, dontAddNull?: boolean, length?: number): number[];
|
||||
intArrayToString(array: number[]): string;
|
||||
writeStringToMemory(str: string, buffer: number, dontAddNull: boolean): void;
|
||||
writeArrayToMemory(array: number[], buffer: number): void;
|
||||
writeAsciiToMemory(str: string, buffer: number, dontAddNull: boolean): void;
|
||||
|
||||
addRunDependency(id: any): void;
|
||||
removeRunDependency(id: any): void;
|
||||
|
||||
|
||||
preloadedImages: any;
|
||||
preloadedAudios: any;
|
||||
|
||||
_malloc(size: number): number;
|
||||
_free(ptr: number): void;
|
||||
|
||||
// Augmentations below by @surma.
|
||||
onRuntimeInitialized: () => void | null;
|
||||
}
|
||||
}
|
||||
|
4
global.d.ts
vendored
@ -13,10 +13,6 @@ declare namespace JSX {
|
||||
interface IntrinsicElements { }
|
||||
}
|
||||
|
||||
declare module 'preact-i18n';
|
||||
declare module 'preact-material-components-drawer';
|
||||
declare module 'material-radial-progress';
|
||||
|
||||
declare module 'classnames' {
|
||||
export default function classnames(...args: any[]): string;
|
||||
}
|
||||
|
13616
package-lock.json
generated
111
package.json
@ -4,98 +4,67 @@
|
||||
"version": "0.0.0",
|
||||
"license": "apache-2.0",
|
||||
"scripts": {
|
||||
"start": "webpack serve --hot",
|
||||
"start": "webpack serve --host 0.0.0.0 --hot",
|
||||
"build": "webpack -p",
|
||||
"lint": "eslint src"
|
||||
"lint": "tslint -c tslint.json -t verbose 'src/**/*.{ts,tsx,js,jsx}'",
|
||||
"lintfix": "tslint -c tslint.json -t verbose --fix 'src/**/*.{ts,tsx,js,jsx}'"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"standard",
|
||||
"standard-jsx"
|
||||
],
|
||||
"rules": {
|
||||
"indent": [
|
||||
2,
|
||||
2
|
||||
],
|
||||
"semi": [
|
||||
2,
|
||||
"always"
|
||||
],
|
||||
"prefer-const": 1
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npm run lint"
|
||||
}
|
||||
},
|
||||
"eslintIgnore": [
|
||||
"build/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/node": "^9.4.7",
|
||||
"babel-loader": "^7.1.4",
|
||||
"@types/node": "^9.6.23",
|
||||
"@types/pretty-bytes": "^5.1.0",
|
||||
"@types/webassembly-js-api": "0.0.1",
|
||||
"@webcomponents/custom-elements": "^1.2.0",
|
||||
"babel-loader": "^7.1.5",
|
||||
"babel-plugin-jsx-pragmatic": "^1.0.2",
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.5",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||
"babel-plugin-transform-react-constant-elements": "^6.23.0",
|
||||
"babel-plugin-transform-react-jsx": "^6.24.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "^0.4.13",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "^0.4.14",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-register": "^6.26.0",
|
||||
"chalk": "^2.3.2",
|
||||
"clean-webpack-plugin": "^0.1.19",
|
||||
"copy-webpack-plugin": "^4.5.1",
|
||||
"css": "^2.2.1",
|
||||
"copy-webpack-plugin": "^4.5.2",
|
||||
"css-loader": "^0.28.11",
|
||||
"ejs-loader": "^0.3.1",
|
||||
"eslint": "^4.18.2",
|
||||
"eslint-config-standard": "^11.0.0",
|
||||
"eslint-config-standard-jsx": "^5.0.0",
|
||||
"eslint-plugin-import": "^2.10.0",
|
||||
"eslint-plugin-node": "^6.0.1",
|
||||
"eslint-plugin-promise": "^3.7.0",
|
||||
"eslint-plugin-react": "^7.7.0",
|
||||
"eslint-plugin-standard": "^3.0.1",
|
||||
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
||||
"fork-ts-checker-notifier-webpack-plugin": "^0.4.0",
|
||||
"fork-ts-checker-webpack-plugin": "^0.4.1",
|
||||
"html-webpack-plugin": "^3.0.6",
|
||||
"exports-loader": "^0.7.0",
|
||||
"file-loader": "^1.1.11",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"husky": "^1.0.0-rc.13",
|
||||
"if-env": "^1.0.4",
|
||||
"jsdom": "^11.6.2",
|
||||
"loader-utils": "^1.1.0",
|
||||
"mini-css-extract-plugin": "^0.3.0",
|
||||
"node-sass": "^4.7.2",
|
||||
"nwmatcher": "^1.4.4",
|
||||
"optimize-css-assets-webpack-plugin": "^4.0.0",
|
||||
"parse5": "^4.0.0",
|
||||
"preact-render-to-string": "^3.7.0",
|
||||
"preload-webpack-plugin": "github:GoogleChromeLabs/preload-webpack-plugin",
|
||||
"pretty-bytes": "^4.0.2",
|
||||
"node-sass": "^4.9.3",
|
||||
"optimize-css-assets-webpack-plugin": "^4.0.3",
|
||||
"progress-bar-webpack-plugin": "^1.11.0",
|
||||
"sass-loader": "^6.0.7",
|
||||
"raw-loader": "^0.5.1",
|
||||
"sass-loader": "^7.1.0",
|
||||
"script-ext-html-webpack-plugin": "^2.0.1",
|
||||
"source-map-loader": "^0.2.3",
|
||||
"style-loader": "^0.20.3",
|
||||
"ts-loader": "^4.0.1",
|
||||
"tslint": "^5.9.1",
|
||||
"style-loader": "^0.22.1",
|
||||
"ts-loader": "^4.4.2",
|
||||
"tslint": "^5.11.0",
|
||||
"tslint-config-airbnb": "^5.9.2",
|
||||
"tslint-config-semistandard": "^7.0.0",
|
||||
"tslint-react": "^3.5.1",
|
||||
"typescript": "^2.7.2",
|
||||
"typescript-loader": "^1.1.3",
|
||||
"tslint-react": "^3.6.0",
|
||||
"typescript": "^2.9.2",
|
||||
"typings-for-css-modules-loader": "^1.7.0",
|
||||
"webpack": "^4.3.0",
|
||||
"webpack-bundle-analyzer": "^2.11.1",
|
||||
"webpack-cli": "^2.0.13",
|
||||
"webpack-dev-server": "^3.1.1",
|
||||
"webpack": "^4.19.1",
|
||||
"webpack-bundle-analyzer": "^2.13.1",
|
||||
"webpack-cli": "^2.1.5",
|
||||
"webpack-dev-server": "^3.1.5",
|
||||
"webpack-plugin-replace": "^1.1.1",
|
||||
"workbox-webpack-plugin": "^3.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"classnames": "^2.2.5",
|
||||
"material-components-web": "^0.32.0",
|
||||
"material-radial-progress": "git+https://gist.github.com/02134901c77c5309924bfcf8b4435ebe.git",
|
||||
"preact": "^8.2.7",
|
||||
"preact-i18n": "^1.2.0",
|
||||
"preact-material-components": "^1.3.7",
|
||||
"preact-material-components-drawer": "git+https://gist.github.com/a78fceed440b98e62582e4440b86bfab.git",
|
||||
"preact-router": "^2.6.0"
|
||||
"classnames": "^2.2.6",
|
||||
"comlink": "^3.0.3",
|
||||
"linkstate": "^1.1.1",
|
||||
"preact": "^8.3.1",
|
||||
"pretty-bytes": "^5.1.0",
|
||||
"worker-plugin": "^1.1.1"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 15 KiB |
BIN
src/assets/icon-large.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
src/assets/icon-small.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 27 KiB |
11
src/codecs/browser-bmp/encoder-meta.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { canvasEncodeTest } from '../generic/util';
|
||||
|
||||
export interface EncodeOptions { }
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
|
||||
export const type = 'browser-bmp';
|
||||
export const label = 'Browser BMP';
|
||||
export const mimeType = 'image/bmp';
|
||||
export const extension = 'bmp';
|
||||
export const defaultOptions: EncodeOptions = {};
|
||||
export const featureTest = () => canvasEncodeTest(mimeType);
|
6
src/codecs/browser-bmp/encoder.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { mimeType } from './encoder-meta';
|
||||
import { canvasEncode } from '../../lib/util';
|
||||
|
||||
export function encode(data: ImageData) {
|
||||
return canvasEncode(data, mimeType);
|
||||
}
|
11
src/codecs/browser-gif/encoder-meta.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { canvasEncodeTest } from '../generic/util';
|
||||
|
||||
export interface EncodeOptions {}
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
|
||||
export const type = 'browser-gif';
|
||||
export const label = 'Browser GIF';
|
||||
export const mimeType = 'image/gif';
|
||||
export const extension = 'gif';
|
||||
export const defaultOptions: EncodeOptions = {};
|
||||
export const featureTest = () => canvasEncodeTest(mimeType);
|
6
src/codecs/browser-gif/encoder.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { mimeType } from './encoder-meta';
|
||||
import { canvasEncode } from '../../lib/util';
|
||||
|
||||
export function encode(data: ImageData) {
|
||||
return canvasEncode(data, mimeType);
|
||||
}
|
11
src/codecs/browser-jp2/encoder-meta.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { canvasEncodeTest } from '../generic/util';
|
||||
|
||||
export interface EncodeOptions { }
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
|
||||
export const type = 'browser-jp2';
|
||||
export const label = 'Browser JPEG 2000';
|
||||
export const mimeType = 'image/jp2';
|
||||
export const extension = 'jp2';
|
||||
export const defaultOptions: EncodeOptions = {};
|
||||
export const featureTest = () => canvasEncodeTest(mimeType);
|
6
src/codecs/browser-jp2/encoder.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { mimeType } from './encoder-meta';
|
||||
import { canvasEncode } from '../../lib/util';
|
||||
|
||||
export function encode(data: ImageData) {
|
||||
return canvasEncode(data, mimeType);
|
||||
}
|
8
src/codecs/browser-jpeg/encoder-meta.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface EncodeOptions { quality: number; }
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
|
||||
export const type = 'browser-jpeg';
|
||||
export const label = 'Browser JPEG';
|
||||
export const mimeType = 'image/jpeg';
|
||||
export const extension = 'jpg';
|
||||
export const defaultOptions: EncodeOptions = { quality: 0.5 };
|
6
src/codecs/browser-jpeg/encoder.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { EncodeOptions, mimeType } from './encoder-meta';
|
||||
import { canvasEncode } from '../../lib/util';
|
||||
|
||||
export function encode(data: ImageData, { quality }: EncodeOptions) {
|
||||
return canvasEncode(data, mimeType, quality);
|
||||
}
|
3
src/codecs/browser-jpeg/options.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import qualityOption from '../generic/quality-option';
|
||||
|
||||
export default qualityOption({ min: 0, max: 1, step: 0 });
|
11
src/codecs/browser-pdf/encoder-meta.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { canvasEncodeTest } from '../generic/util';
|
||||
|
||||
export interface EncodeOptions { }
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
|
||||
export const type = 'browser-pdf';
|
||||
export const label = 'Browser PDF';
|
||||
export const mimeType = 'application/pdf';
|
||||
export const extension = 'pdf';
|
||||
export const defaultOptions: EncodeOptions = {};
|
||||
export const featureTest = () => canvasEncodeTest(mimeType);
|
6
src/codecs/browser-pdf/encoder.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { mimeType } from './encoder-meta';
|
||||
import { canvasEncode } from '../../lib/util';
|
||||
|
||||
export function encode(data: ImageData) {
|
||||
return canvasEncode(data, mimeType);
|
||||
}
|
8
src/codecs/browser-png/encoder-meta.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface EncodeOptions {}
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
|
||||
export const type = 'browser-png';
|
||||
export const label = 'Browser PNG';
|
||||
export const mimeType = 'image/png';
|
||||
export const extension = 'png';
|
||||
export const defaultOptions: EncodeOptions = {};
|
6
src/codecs/browser-png/encoder.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import { mimeType } from './encoder-meta';
|
||||
import { canvasEncode } from '../../lib/util';
|
||||
|
||||
export function encode(data: ImageData) {
|
||||
return canvasEncode(data, mimeType);
|
||||
}
|
11
src/codecs/browser-tiff/encoder-meta.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { canvasEncodeTest } from '../generic/util';
|
||||
|
||||
export interface EncodeOptions { }
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
|
||||
export const type = 'browser-tiff';
|
||||
export const label = 'Browser TIFF';
|
||||
export const mimeType = 'image/tiff';
|
||||
export const extension = 'tiff';
|
||||
export const defaultOptions: EncodeOptions = {};
|
||||
export const featureTest = () => canvasEncodeTest(mimeType);
|
6
src/codecs/browser-tiff/encoder.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { mimeType } from './encoder-meta';
|
||||
import { canvasEncode } from '../../lib/util';
|
||||
|
||||
export function encode(data: ImageData) {
|
||||
return canvasEncode(data, mimeType);
|
||||
}
|
11
src/codecs/browser-webp/encoder-meta.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { canvasEncodeTest } from '../generic/util';
|
||||
|
||||
export interface EncodeOptions { quality: number; }
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
|
||||
export const type = 'browser-webp';
|
||||
export const label = 'Browser WebP';
|
||||
export const mimeType = 'image/webp';
|
||||
export const extension = 'webp';
|
||||
export const defaultOptions: EncodeOptions = { quality: 0.5 };
|
||||
export const featureTest = () => canvasEncodeTest(mimeType);
|
6
src/codecs/browser-webp/encoder.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { EncodeOptions, mimeType } from './encoder-meta';
|
||||
import { canvasEncode } from '../../lib/util';
|
||||
|
||||
export function encode(data: ImageData, { quality }: EncodeOptions) {
|
||||
return canvasEncode(data, mimeType, quality);
|
||||
}
|
3
src/codecs/browser-webp/options.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import qualityOption from '../generic/quality-option';
|
||||
|
||||
export default qualityOption({ min: 0, max: 1, step: 0 });
|
21
src/codecs/decoders.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { nativeDecode, sniffMimeType, canDecodeImage } from '../lib/util';
|
||||
import Processor from './processor';
|
||||
|
||||
// tslint:disable-next-line:max-line-length It’s a data URL. Whatcha gonna do?
|
||||
const webpFile = 'data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA=';
|
||||
const nativeWebPSupported = canDecodeImage(webpFile);
|
||||
|
||||
export async function decodeImage(blob: Blob, processor: Processor): Promise<ImageData> {
|
||||
const mimeType = await sniffMimeType(blob);
|
||||
|
||||
try {
|
||||
if (mimeType === 'image/webp' && !(await nativeWebPSupported)) {
|
||||
return await processor.webpDecode(blob);
|
||||
}
|
||||
|
||||
// Otherwise, just throw it at the browser's decoder.
|
||||
return await nativeDecode(blob);
|
||||
} catch (err) {
|
||||
throw Error("Couldn't decode image");
|
||||
}
|
||||
}
|
78
src/codecs/encoders.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import * as identity from './identity/encoder-meta';
|
||||
import * as optiPNG from './optipng/encoder-meta';
|
||||
import * as mozJPEG from './mozjpeg/encoder-meta';
|
||||
import * as webP from './webp/encoder-meta';
|
||||
import * as browserPNG from './browser-png/encoder-meta';
|
||||
import * as browserJPEG from './browser-jpeg/encoder-meta';
|
||||
import * as browserWebP from './browser-webp/encoder-meta';
|
||||
import * as browserGIF from './browser-gif/encoder-meta';
|
||||
import * as browserTIFF from './browser-tiff/encoder-meta';
|
||||
import * as browserJP2 from './browser-jp2/encoder-meta';
|
||||
import * as browserBMP from './browser-bmp/encoder-meta';
|
||||
import * as browserPDF from './browser-pdf/encoder-meta';
|
||||
|
||||
export interface EncoderSupportMap {
|
||||
[key: string]: boolean;
|
||||
}
|
||||
|
||||
export type EncoderState =
|
||||
identity.EncoderState |
|
||||
optiPNG.EncoderState |
|
||||
mozJPEG.EncoderState |
|
||||
webP.EncoderState |
|
||||
browserPNG.EncoderState |
|
||||
browserJPEG.EncoderState |
|
||||
browserWebP.EncoderState |
|
||||
browserGIF.EncoderState |
|
||||
browserTIFF.EncoderState |
|
||||
browserJP2.EncoderState |
|
||||
browserBMP.EncoderState |
|
||||
browserPDF.EncoderState;
|
||||
|
||||
export type EncoderOptions =
|
||||
identity.EncodeOptions |
|
||||
optiPNG.EncodeOptions |
|
||||
mozJPEG.EncodeOptions |
|
||||
webP.EncodeOptions |
|
||||
browserPNG.EncodeOptions |
|
||||
browserJPEG.EncodeOptions |
|
||||
browserWebP.EncodeOptions |
|
||||
browserGIF.EncodeOptions |
|
||||
browserTIFF.EncodeOptions |
|
||||
browserJP2.EncodeOptions |
|
||||
browserBMP.EncodeOptions |
|
||||
browserPDF.EncodeOptions;
|
||||
|
||||
export type EncoderType = keyof typeof encoderMap;
|
||||
|
||||
export const encoderMap = {
|
||||
[identity.type]: identity,
|
||||
[optiPNG.type]: optiPNG,
|
||||
[mozJPEG.type]: mozJPEG,
|
||||
[webP.type]: webP,
|
||||
[browserPNG.type]: browserPNG,
|
||||
[browserJPEG.type]: browserJPEG,
|
||||
[browserWebP.type]: browserWebP,
|
||||
// Safari & Firefox only:
|
||||
[browserBMP.type]: browserBMP,
|
||||
// Safari only:
|
||||
[browserGIF.type]: browserGIF,
|
||||
[browserTIFF.type]: browserTIFF,
|
||||
[browserJP2.type]: browserJP2,
|
||||
[browserPDF.type]: browserPDF,
|
||||
};
|
||||
|
||||
export const encoders = Array.from(Object.values(encoderMap));
|
||||
|
||||
/** Does this browser support a given encoder? Indexed by label */
|
||||
export const encodersSupported = Promise.resolve().then(async () => {
|
||||
const encodersSupported: EncoderSupportMap = {};
|
||||
|
||||
await Promise.all(encoders.map(async (encoder) => {
|
||||
// If the encoder provides a featureTest, call it, otherwise assume supported.
|
||||
const isSupported = !('featureTest' in encoder) || await encoder.featureTest();
|
||||
encodersSupported[encoder.type] = isSupported;
|
||||
}));
|
||||
|
||||
return encodersSupported;
|
||||
});
|
54
src/codecs/generic/quality-option.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import { h, Component } from 'preact';
|
||||
import { bind } from '../../lib/initial-util';
|
||||
import '../../custom-els/RangeInput';
|
||||
|
||||
interface EncodeOptions {
|
||||
quality: number;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
options: EncodeOptions,
|
||||
onChange(newOptions: EncodeOptions): void,
|
||||
};
|
||||
|
||||
interface QualityOptionArg {
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
}
|
||||
|
||||
export default function qualityOption(opts: QualityOptionArg = {}) {
|
||||
const {
|
||||
min = 0,
|
||||
max = 100,
|
||||
step = 1,
|
||||
} = opts;
|
||||
|
||||
class QualityOptions extends Component<Props, {}> {
|
||||
@bind
|
||||
onChange(event: Event) {
|
||||
const el = event.currentTarget as HTMLInputElement;
|
||||
this.props.onChange({ quality: Number(el.value) });
|
||||
}
|
||||
|
||||
render({ options }: Props) {
|
||||
return (
|
||||
<div>
|
||||
<label>
|
||||
Quality:
|
||||
<range-input
|
||||
name="quality"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step || 'any'}
|
||||
value={'' + options.quality}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return QualityOptions;
|
||||
}
|
13
src/codecs/generic/util.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { canvasEncode } from '../../lib/util';
|
||||
|
||||
export async function canvasEncodeTest(mimeType: string) {
|
||||
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;
|
||||
}
|
||||
}
|
6
src/codecs/identity/encoder-meta.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface EncodeOptions {}
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
|
||||
export const type = 'identity';
|
||||
export const label = 'Original image';
|
||||
export const defaultOptions: EncodeOptions = {};
|
76
src/codecs/imagequant/options.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
import { h, Component } from 'preact';
|
||||
import { bind } from '../../lib/initial-util';
|
||||
import { inputFieldValueAsNumber, konami } from '../../lib/util';
|
||||
import { QuantizeOptions } from './processor-meta';
|
||||
|
||||
const konamiPromise = konami();
|
||||
|
||||
interface Props {
|
||||
options: QuantizeOptions;
|
||||
onChange(newOptions: QuantizeOptions): void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
extendedSettings: boolean;
|
||||
}
|
||||
|
||||
export default class QuantizerOptions extends Component<Props, State> {
|
||||
state: State = { extendedSettings: false };
|
||||
|
||||
componentDidMount() {
|
||||
konamiPromise.then(() => {
|
||||
this.setState({ extendedSettings: true });
|
||||
});
|
||||
}
|
||||
|
||||
@bind
|
||||
onChange(event: Event) {
|
||||
const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement;
|
||||
|
||||
const options: QuantizeOptions = {
|
||||
zx: inputFieldValueAsNumber(form.zx),
|
||||
maxNumColors: inputFieldValueAsNumber(form.maxNumColors),
|
||||
dither: inputFieldValueAsNumber(form.dither),
|
||||
};
|
||||
this.props.onChange(options);
|
||||
}
|
||||
|
||||
render({ options }: Props, { extendedSettings }: State) {
|
||||
return (
|
||||
<form>
|
||||
<label style={{ display: extendedSettings ? '' : 'none' }}>
|
||||
Type:
|
||||
<select
|
||||
name="zx"
|
||||
value={'' + options.zx}
|
||||
onChange={this.onChange}
|
||||
>
|
||||
<option value="0">Standard</option>
|
||||
<option value="1">ZX</option>
|
||||
</select>
|
||||
</label>
|
||||
<label style={{ display: options.zx ? 'none' : '' }}>
|
||||
Palette Colors:
|
||||
<range-input
|
||||
name="maxNumColors"
|
||||
min="2"
|
||||
max="256"
|
||||
value={'' + options.maxNumColors}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Dithering:
|
||||
<range-input
|
||||
name="dither"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={'' + options.dither}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</label>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
11
src/codecs/imagequant/processor-meta.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export interface QuantizeOptions {
|
||||
zx: number;
|
||||
maxNumColors: number;
|
||||
dither: number;
|
||||
}
|
||||
|
||||
export const defaultOptions: QuantizeOptions = {
|
||||
zx: 0,
|
||||
maxNumColors: 256,
|
||||
dither: 1.0,
|
||||
};
|
21
src/codecs/imagequant/processor.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import imagequant, { QuantizerModule } from '../../../codecs/imagequant/imagequant';
|
||||
import wasmUrl from '../../../codecs/imagequant/imagequant.wasm';
|
||||
import { QuantizeOptions } from './processor-meta';
|
||||
import { initWasmModule } from '../util';
|
||||
|
||||
let emscriptenModule: Promise<QuantizerModule>;
|
||||
|
||||
export async function process(data: ImageData, opts: QuantizeOptions): Promise<ImageData> {
|
||||
if (!emscriptenModule) emscriptenModule = initWasmModule(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);
|
||||
|
||||
module.free_result();
|
||||
|
||||
return new ImageData(new Uint8ClampedArray(result.buffer), result.width, result.height);
|
||||
}
|
41
src/codecs/mozjpeg/encoder-meta.ts
Normal file
@ -0,0 +1,41 @@
|
||||
export 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;
|
||||
}
|
||||
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
|
||||
export const type = 'mozjpeg';
|
||||
export const label = 'MozJPEG';
|
||||
export const mimeType = 'image/jpeg';
|
||||
export const extension = 'jpg';
|
||||
export const defaultOptions: EncodeOptions = {
|
||||
quality: 75,
|
||||
baseline: false,
|
||||
arithmetic: false,
|
||||
progressive: true,
|
||||
optimize_coding: true,
|
||||
smoothing: 0,
|
||||
color_space: MozJpegColorSpace.YCbCr,
|
||||
quant_table: 3,
|
||||
trellis_multipass: false,
|
||||
trellis_opt_zero: false,
|
||||
trellis_opt_table: false,
|
||||
trellis_loops: 1,
|
||||
};
|
18
src/codecs/mozjpeg/encoder.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import mozjpeg_enc, { MozJPEGModule } from '../../../codecs/mozjpeg_enc/mozjpeg_enc';
|
||||
import wasmUrl from '../../../codecs/mozjpeg_enc/mozjpeg_enc.wasm';
|
||||
import { EncodeOptions } from './encoder-meta';
|
||||
import { initWasmModule } from '../util';
|
||||
|
||||
let emscriptenModule: Promise<MozJPEGModule>;
|
||||
|
||||
export async function encode(data: ImageData, options: EncodeOptions): Promise<ArrayBuffer> {
|
||||
if (!emscriptenModule) emscriptenModule = initWasmModule(mozjpeg_enc, wasmUrl);
|
||||
|
||||
const module = await emscriptenModule;
|
||||
const resultView = module.encode(data.data, data.width, data.height, options);
|
||||
const result = new Uint8Array(resultView);
|
||||
module.free_result();
|
||||
|
||||
// wasm can’t run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
|
||||
return result.buffer as ArrayBuffer;
|
||||
}
|
160
src/codecs/mozjpeg/options.tsx
Normal file
@ -0,0 +1,160 @@
|
||||
import { h, Component } from 'preact';
|
||||
import { bind } from '../../lib/initial-util';
|
||||
import { inputFieldChecked, inputFieldValueAsNumber } from '../../lib/util';
|
||||
import { EncodeOptions, MozJpegColorSpace } from './encoder-meta';
|
||||
import '../../custom-els/RangeInput';
|
||||
|
||||
type Props = {
|
||||
options: EncodeOptions,
|
||||
onChange(newOptions: EncodeOptions): void,
|
||||
};
|
||||
|
||||
export default class MozJPEGEncoderOptions extends Component<Props, {}> {
|
||||
@bind
|
||||
onChange(event: Event) {
|
||||
const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement;
|
||||
|
||||
const options: EncodeOptions = {
|
||||
// Copy over options the form doesn't currently care about, eg arithmetic
|
||||
...this.props.options,
|
||||
// And now stuff from the form:
|
||||
// .checked
|
||||
baseline: inputFieldChecked(form.baseline),
|
||||
progressive: inputFieldChecked(form.progressive),
|
||||
optimize_coding: inputFieldChecked(form.optimize_coding),
|
||||
trellis_multipass: inputFieldChecked(form.trellis_multipass),
|
||||
trellis_opt_zero: inputFieldChecked(form.trellis_opt_zero),
|
||||
trellis_opt_table: inputFieldChecked(form.trellis_opt_table),
|
||||
// .value
|
||||
quality: inputFieldValueAsNumber(form.quality),
|
||||
smoothing: inputFieldValueAsNumber(form.smoothing),
|
||||
color_space: inputFieldValueAsNumber(form.color_space),
|
||||
quant_table: inputFieldValueAsNumber(form.quant_table),
|
||||
trellis_loops: inputFieldValueAsNumber(form.trellis_loops),
|
||||
};
|
||||
this.props.onChange(options);
|
||||
}
|
||||
|
||||
render({ options }: Props) {
|
||||
// I'm rendering both lossy and lossless forms, as it becomes much easier when
|
||||
// gathering the data.
|
||||
return (
|
||||
<form>
|
||||
<label>
|
||||
Quality:
|
||||
<range-input
|
||||
name="quality"
|
||||
min="0"
|
||||
max="100"
|
||||
value={'' + options.quality}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
name="baseline"
|
||||
type="checkbox"
|
||||
checked={options.baseline}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<span>Baseline (worse but legacy-compatible)</span>
|
||||
</label>
|
||||
<label style={{ display: options.baseline ? 'none' : '' }}>
|
||||
<input
|
||||
name="progressive"
|
||||
type="checkbox"
|
||||
checked={options.progressive}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<span>Progressive multi-pass rendering</span>
|
||||
</label>
|
||||
<label style={{ display: options.baseline ? '' : 'none' }}>
|
||||
<input
|
||||
name="optimize_coding"
|
||||
type="checkbox"
|
||||
checked={options.optimize_coding}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<span>Optimize Huffman table</span>
|
||||
</label>
|
||||
<label>
|
||||
Smoothing:
|
||||
<range-input
|
||||
name="smoothing"
|
||||
min="0"
|
||||
max="100"
|
||||
value={'' + options.smoothing}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Output color space:
|
||||
<select
|
||||
name="color_space"
|
||||
value={'' + options.color_space}
|
||||
onChange={this.onChange}
|
||||
>
|
||||
<option value={MozJpegColorSpace.GRAYSCALE}>Grayscale</option>
|
||||
<option value={MozJpegColorSpace.RGB}>RGB (sub-optimal)</option>
|
||||
<option value={MozJpegColorSpace.YCbCr}>YCbCr (optimized for color)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Quantization table:
|
||||
<select
|
||||
name="quant_table"
|
||||
value={'' + options.quant_table}
|
||||
onChange={this.onChange}
|
||||
>
|
||||
<option value="0">JPEG Annex K</option>
|
||||
<option value="1">Flat</option>
|
||||
<option value="2">MSSIM-tuned Kodak</option>
|
||||
<option value="3">ImageMagick</option>
|
||||
<option value="4">PSNR-HVS-M-tuned Kodak</option>
|
||||
<option value="5">Klein et al</option>
|
||||
<option value="6">Watson et al</option>
|
||||
<option value="7">Ahumada et al</option>
|
||||
<option value="8">Peterson et al</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
name="trellis_multipass"
|
||||
type="checkbox"
|
||||
checked={options.trellis_multipass}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<span>Consider multiple scans during trellis quantization</span>
|
||||
</label>
|
||||
<label style={{ display: options.trellis_multipass ? '' : 'none' }}>
|
||||
<input
|
||||
name="trellis_opt_zero"
|
||||
type="checkbox"
|
||||
checked={options.trellis_opt_zero}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<span>Optimize runs of zero blocks</span>
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
name="trellis_opt_table"
|
||||
type="checkbox"
|
||||
checked={options.trellis_opt_table}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
<span>Optimize after trellis quantization</span>
|
||||
</label>
|
||||
<label>
|
||||
Trellis quantization passes:
|
||||
<range-input
|
||||
name="trellis_loops"
|
||||
min="1"
|
||||
max="50"
|
||||
value={'' + options.trellis_loops}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</label>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
13
src/codecs/optipng/encoder-meta.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export interface EncodeOptions {
|
||||
level: number;
|
||||
}
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
|
||||
export const type = 'png';
|
||||
export const label = 'OptiPNG';
|
||||
export const mimeType = 'image/png';
|
||||
export const extension = 'png';
|
||||
|
||||
export const defaultOptions: EncodeOptions = {
|
||||
level: 2,
|
||||
};
|
18
src/codecs/optipng/encoder.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import optipng, { OptiPngModule } from '../../../codecs/optipng/optipng';
|
||||
import wasmUrl from '../../../codecs/optipng/optipng.wasm';
|
||||
import { EncodeOptions } from './encoder-meta';
|
||||
import { initWasmModule } from '../util';
|
||||
|
||||
let emscriptenModule: Promise<OptiPngModule>;
|
||||
|
||||
export async function compress(data: BufferSource, options: EncodeOptions): Promise<ArrayBuffer> {
|
||||
if (!emscriptenModule) emscriptenModule = initWasmModule(optipng, wasmUrl);
|
||||
|
||||
const module = await emscriptenModule;
|
||||
const resultView = module.compress(data, options);
|
||||
const result = new Uint8Array(resultView);
|
||||
module.free_result();
|
||||
|
||||
// wasm can’t run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
|
||||
return result.buffer as ArrayBuffer;
|
||||
}
|
40
src/codecs/optipng/options.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { h, Component } from 'preact';
|
||||
import { bind } from '../../lib/initial-util';
|
||||
import { inputFieldValueAsNumber } from '../../lib/util';
|
||||
import { EncodeOptions } from './encoder-meta';
|
||||
|
||||
type Props = {
|
||||
options: EncodeOptions;
|
||||
onChange(newOptions: EncodeOptions): void;
|
||||
};
|
||||
|
||||
export default class OptiPNGEncoderOptions extends Component<Props, {}> {
|
||||
@bind
|
||||
onChange(event: Event) {
|
||||
const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement;
|
||||
|
||||
const options: EncodeOptions = {
|
||||
level: inputFieldValueAsNumber(form.level),
|
||||
};
|
||||
this.props.onChange(options);
|
||||
}
|
||||
|
||||
render({ options }: Props) {
|
||||
return (
|
||||
<form>
|
||||
<label>
|
||||
Effort:
|
||||
<input
|
||||
name="level"
|
||||
type="range"
|
||||
min="0"
|
||||
max="7"
|
||||
step="1"
|
||||
value={'' + options.level}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</label>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
23
src/codecs/preprocessors.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import {
|
||||
QuantizeOptions, defaultOptions as quantizerDefaultOptions,
|
||||
} from './imagequant/processor-meta';
|
||||
import { ResizeOptions, defaultOptions as resizeDefaultOptions } from './resize/processor-meta';
|
||||
|
||||
interface Enableable {
|
||||
enabled: boolean;
|
||||
}
|
||||
export interface PreprocessorState {
|
||||
quantizer: Enableable & QuantizeOptions;
|
||||
resize: Enableable & ResizeOptions;
|
||||
}
|
||||
|
||||
export const defaultPreprocessorState: PreprocessorState = {
|
||||
quantizer: {
|
||||
enabled: false,
|
||||
...quantizerDefaultOptions,
|
||||
},
|
||||
resize: {
|
||||
enabled: false,
|
||||
...resizeDefaultOptions,
|
||||
},
|
||||
};
|
41
src/codecs/processor-worker.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { expose } from 'comlink';
|
||||
import { EncodeOptions as MozJPEGEncoderOptions } from './mozjpeg/encoder-meta';
|
||||
import { QuantizeOptions } from './imagequant/processor-meta';
|
||||
import { EncodeOptions as OptiPNGEncoderOptions } from './optipng/encoder-meta';
|
||||
import { EncodeOptions as WebPEncoderOptions } from './webp/encoder-meta';
|
||||
|
||||
async function mozjpegEncode(
|
||||
data: ImageData, options: MozJPEGEncoderOptions,
|
||||
): Promise<ArrayBuffer> {
|
||||
const { encode } = await import('./mozjpeg/encoder');
|
||||
return encode(data, options);
|
||||
}
|
||||
|
||||
async function quantize(data: ImageData, opts: QuantizeOptions): Promise<ImageData> {
|
||||
const { process } = await import('./imagequant/processor');
|
||||
return process(data, opts);
|
||||
}
|
||||
|
||||
async function optiPngEncode(
|
||||
data: BufferSource, options: OptiPNGEncoderOptions,
|
||||
): Promise<ArrayBuffer> {
|
||||
const { compress } = await import('./optipng/encoder');
|
||||
return compress(data, options);
|
||||
}
|
||||
|
||||
async function webpEncode(
|
||||
data: ImageData, options: WebPEncoderOptions,
|
||||
): Promise<ArrayBuffer> {
|
||||
const { encode } = await import('./webp/encoder');
|
||||
return encode(data, options);
|
||||
}
|
||||
|
||||
async function webpDecode(data: ArrayBuffer): Promise<ImageData> {
|
||||
const { decode } = await import('./webp/decoder');
|
||||
return decode(data);
|
||||
}
|
||||
|
||||
const exports = { mozjpegEncode, quantize, optiPngEncode, webpEncode, webpDecode };
|
||||
export type ProcessorWorkerApi = typeof exports;
|
||||
|
||||
expose(exports, self);
|
205
src/codecs/processor.ts
Normal file
@ -0,0 +1,205 @@
|
||||
import { proxy } from 'comlink';
|
||||
import { QuantizeOptions } from './imagequant/processor-meta';
|
||||
import { ProcessorWorkerApi } from './processor-worker';
|
||||
import { canvasEncode, blobToArrayBuffer } from '../lib/util';
|
||||
import { EncodeOptions as MozJPEGEncoderOptions } from './mozjpeg/encoder-meta';
|
||||
import { EncodeOptions as OptiPNGEncoderOptions } from './optipng/encoder-meta';
|
||||
import { EncodeOptions as WebPEncoderOptions } from './webp/encoder-meta';
|
||||
import { EncodeOptions as BrowserJPEGOptions } from './browser-jpeg/encoder-meta';
|
||||
import { EncodeOptions as BrowserWebpEncodeOptions } from './browser-webp/encoder-meta';
|
||||
import { BitmapResizeOptions, VectorResizeOptions } from './resize/processor-meta';
|
||||
import { resize, vectorResize } from './resize/processor';
|
||||
import * as browserBMP from './browser-bmp/encoder';
|
||||
import * as browserPNG from './browser-png/encoder';
|
||||
import * as browserJPEG from './browser-jpeg/encoder';
|
||||
import * as browserWebP from './browser-webp/encoder';
|
||||
import * as browserGIF from './browser-gif/encoder';
|
||||
import * as browserTIFF from './browser-tiff/encoder';
|
||||
import * as browserJP2 from './browser-jp2/encoder';
|
||||
import * as browserPDF from './browser-pdf/encoder';
|
||||
|
||||
/** How long the worker should be idle before terminating. */
|
||||
const workerTimeout = 1000;
|
||||
|
||||
interface ProcessingJobOptions {
|
||||
needsWorker?: boolean;
|
||||
}
|
||||
|
||||
export default class Processor {
|
||||
/** Worker instance associated with this processor. */
|
||||
private _worker?: Worker;
|
||||
/** Comlinked worker API. */
|
||||
private _workerApi?: ProcessorWorkerApi;
|
||||
/** Rejector for a pending promise. */
|
||||
private _abortRejector?: (err: Error) => void;
|
||||
/** Is work currently happening? */
|
||||
private _busy = false;
|
||||
/** Incementing ID so we can tell if a job has been superseded. */
|
||||
private _latestJobId: number = 0;
|
||||
/** setTimeout ID for killing the worker when idle. */
|
||||
private _workerTimeoutId: number = 0;
|
||||
|
||||
/**
|
||||
* Decorator that manages the (re)starting of the worker and aborting existing jobs. Not all
|
||||
* processing jobs require a worker (e.g. the main thread canvas encodes), use the needsWorker
|
||||
* option to control this.
|
||||
*/
|
||||
private static _processingJob(options: ProcessingJobOptions = {}) {
|
||||
const { needsWorker = false } = options;
|
||||
|
||||
return (target: Processor, propertyKey: string, descriptor: PropertyDescriptor): void => {
|
||||
const processingFunc = descriptor.value;
|
||||
|
||||
descriptor.value = async function (this: Processor, ...args: any[]) {
|
||||
this._latestJobId += 1;
|
||||
const jobId = this._latestJobId;
|
||||
this.abortCurrent();
|
||||
|
||||
if (needsWorker) self.clearTimeout(this._workerTimeoutId);
|
||||
|
||||
if (!this._worker && needsWorker) {
|
||||
// worker-loader does magic here.
|
||||
// @ts-ignore - Typescript doesn't know about the 2nd param to new Worker, and the
|
||||
// definition can't be overwritten.
|
||||
this._worker = new Worker('./processor-worker.ts', { type: 'module' }) as Worker;
|
||||
// Need to do some TypeScript trickery to make the type match.
|
||||
this._workerApi = proxy(this._worker) as any as ProcessorWorkerApi;
|
||||
}
|
||||
|
||||
this._busy = true;
|
||||
|
||||
const returnVal = Promise.race([
|
||||
processingFunc.call(this, ...args),
|
||||
new Promise((_, reject) => { this._abortRejector = reject; }),
|
||||
]);
|
||||
|
||||
// Wait for the operation to settle.
|
||||
await returnVal.catch(() => {});
|
||||
|
||||
// If no other jobs are happening, cleanup.
|
||||
if (jobId === this._latestJobId) this._jobCleanup();
|
||||
|
||||
return returnVal;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
private _jobCleanup(): void {
|
||||
this._busy = false;
|
||||
|
||||
if (!this._worker) return;
|
||||
|
||||
// If the worker is unused for 10 seconds, remove it to save memory.
|
||||
this._workerTimeoutId = self.setTimeout(
|
||||
() => {
|
||||
if (this._busy) throw Error("Worker shouldn't be busy");
|
||||
if (!this._worker) return;
|
||||
this._worker.terminate();
|
||||
this._worker = undefined;
|
||||
},
|
||||
workerTimeout,
|
||||
);
|
||||
}
|
||||
|
||||
/** Abort the current job, if any */
|
||||
abortCurrent() {
|
||||
if (!this._busy) return;
|
||||
if (!this._worker || !this._abortRejector) {
|
||||
throw Error("There must be a worker/rejector if it's busy");
|
||||
}
|
||||
this._abortRejector(new DOMException('Aborted', 'AbortError'));
|
||||
this._worker.terminate();
|
||||
this._worker = undefined;
|
||||
this._abortRejector = undefined;
|
||||
this._busy = false;
|
||||
}
|
||||
|
||||
// Off main thread jobs:
|
||||
|
||||
@Processor._processingJob({ needsWorker: true })
|
||||
imageQuant(data: ImageData, opts: QuantizeOptions): Promise<ImageData> {
|
||||
return this._workerApi!.quantize(data, opts);
|
||||
}
|
||||
|
||||
@Processor._processingJob({ needsWorker: true })
|
||||
mozjpegEncode(
|
||||
data: ImageData, opts: MozJPEGEncoderOptions,
|
||||
): Promise<ArrayBuffer> {
|
||||
return this._workerApi!.mozjpegEncode(data, opts);
|
||||
}
|
||||
|
||||
@Processor._processingJob({ needsWorker: true })
|
||||
async optiPngEncode(
|
||||
data: ImageData, opts: OptiPNGEncoderOptions,
|
||||
): Promise<ArrayBuffer> {
|
||||
// OptiPNG expects PNG input.
|
||||
const pngBlob = await canvasEncode(data, 'image/png');
|
||||
const pngBuffer = await blobToArrayBuffer(pngBlob);
|
||||
return this._workerApi!.optiPngEncode(pngBuffer, opts);
|
||||
}
|
||||
|
||||
@Processor._processingJob({ needsWorker: true })
|
||||
webpEncode(data: ImageData, opts: WebPEncoderOptions): Promise<ArrayBuffer> {
|
||||
return this._workerApi!.webpEncode(data, opts);
|
||||
}
|
||||
|
||||
@Processor._processingJob({ needsWorker: true })
|
||||
async webpDecode(blob: Blob): Promise<ImageData> {
|
||||
const data = await blobToArrayBuffer(blob);
|
||||
return this._workerApi!.webpDecode(data);
|
||||
}
|
||||
|
||||
// Not-worker jobs:
|
||||
|
||||
@Processor._processingJob()
|
||||
browserBmpEncode(data: ImageData): Promise<Blob> {
|
||||
return browserBMP.encode(data);
|
||||
}
|
||||
|
||||
@Processor._processingJob()
|
||||
browserPngEncode(data: ImageData): Promise<Blob> {
|
||||
return browserPNG.encode(data);
|
||||
}
|
||||
|
||||
@Processor._processingJob()
|
||||
browserJpegEncode(data: ImageData, opts: BrowserJPEGOptions): Promise<Blob> {
|
||||
return browserJPEG.encode(data, opts);
|
||||
}
|
||||
|
||||
@Processor._processingJob()
|
||||
browserWebpEncode(data: ImageData, opts: BrowserWebpEncodeOptions): Promise<Blob> {
|
||||
return browserWebP.encode(data, opts);
|
||||
}
|
||||
|
||||
@Processor._processingJob()
|
||||
browserGifEncode(data: ImageData): Promise<Blob> {
|
||||
return browserGIF.encode(data);
|
||||
}
|
||||
|
||||
@Processor._processingJob()
|
||||
browserTiffEncode(data: ImageData): Promise<Blob> {
|
||||
return browserTIFF.encode(data);
|
||||
}
|
||||
|
||||
@Processor._processingJob()
|
||||
browserJp2Encode(data: ImageData): Promise<Blob> {
|
||||
return browserJP2.encode(data);
|
||||
}
|
||||
|
||||
@Processor._processingJob()
|
||||
browserPdfEncode(data: ImageData): Promise<Blob> {
|
||||
return browserPDF.encode(data);
|
||||
}
|
||||
|
||||
// Synchronous jobs
|
||||
|
||||
resize(data: ImageData, opts: BitmapResizeOptions) {
|
||||
this.abortCurrent();
|
||||
return resize(data, opts);
|
||||
}
|
||||
|
||||
vectorResize(data: HTMLImageElement, opts: VectorResizeOptions) {
|
||||
this.abortCurrent();
|
||||
return vectorResize(data, opts);
|
||||
}
|
||||
}
|
131
src/codecs/resize/options.tsx
Normal file
@ -0,0 +1,131 @@
|
||||
import { h, Component } from 'preact';
|
||||
import linkState from 'linkstate';
|
||||
import { bind } from '../../lib/initial-util';
|
||||
import { inputFieldValueAsNumber } from '../../lib/util';
|
||||
import { ResizeOptions } from './processor-meta';
|
||||
|
||||
interface Props {
|
||||
isVector: Boolean;
|
||||
options: ResizeOptions;
|
||||
aspect: number;
|
||||
onChange(newOptions: ResizeOptions): void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
maintainAspect: boolean;
|
||||
}
|
||||
|
||||
export default class ResizerOptions extends Component<Props, State> {
|
||||
state: State = {
|
||||
maintainAspect: true,
|
||||
};
|
||||
|
||||
form?: HTMLFormElement;
|
||||
|
||||
reportOptions() {
|
||||
const width = this.form!.width as HTMLInputElement;
|
||||
const height = this.form!.height as HTMLInputElement;
|
||||
|
||||
if (!width.checkValidity() || !height.checkValidity()) return;
|
||||
|
||||
const options: ResizeOptions = {
|
||||
width: inputFieldValueAsNumber(width),
|
||||
height: inputFieldValueAsNumber(height),
|
||||
method: this.form!.resizeMethod.value,
|
||||
fitMethod: this.form!.fitMethod.value,
|
||||
};
|
||||
this.props.onChange(options);
|
||||
}
|
||||
|
||||
@bind
|
||||
onChange(event: Event) {
|
||||
this.reportOptions();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
if (!prevState.maintainAspect && this.state.maintainAspect) {
|
||||
this.form!.height.value = Math.round(Number(this.form!.width.value) / this.props.aspect);
|
||||
this.reportOptions();
|
||||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
onWidthInput(event: Event) {
|
||||
if (!this.state.maintainAspect) return;
|
||||
|
||||
const width = inputFieldValueAsNumber(this.form!.width);
|
||||
this.form!.height.value = Math.round(width / this.props.aspect);
|
||||
}
|
||||
|
||||
@bind
|
||||
onHeightInput(event: Event) {
|
||||
if (!this.state.maintainAspect) return;
|
||||
|
||||
const height = inputFieldValueAsNumber(this.form!.height);
|
||||
this.form!.width.value = Math.round(height * this.props.aspect);
|
||||
}
|
||||
|
||||
render({ options, aspect, isVector }: Props, { maintainAspect }: State) {
|
||||
return (
|
||||
<form ref={el => this.form = el}>
|
||||
<label>
|
||||
Method:
|
||||
<select
|
||||
name="resizeMethod"
|
||||
value={options.method}
|
||||
onChange={this.onChange}
|
||||
>
|
||||
{isVector && <option value="vector">Vector</option>}
|
||||
<option value="browser-pixelated">Browser pixelated</option>
|
||||
<option value="browser-low">Browser low quality</option>
|
||||
<option value="browser-medium">Browser medium quality</option>
|
||||
<option value="browser-high">Browser high quality</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Width:
|
||||
<input
|
||||
required
|
||||
name="width"
|
||||
type="number"
|
||||
min="1"
|
||||
value={'' + options.width}
|
||||
onChange={this.onChange}
|
||||
onInput={this.onWidthInput}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
Height:
|
||||
<input
|
||||
required
|
||||
name="height"
|
||||
type="number"
|
||||
min="1"
|
||||
value={'' + options.height}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
name="maintainAspect"
|
||||
type="checkbox"
|
||||
checked={maintainAspect}
|
||||
onChange={linkState(this, 'maintainAspect')}
|
||||
/>
|
||||
Maintain aspect ratio
|
||||
</label>
|
||||
<label style={{ display: maintainAspect ? 'none' : '' }}>
|
||||
Fit method:
|
||||
<select
|
||||
name="fitMethod"
|
||||
value={options.fitMethod}
|
||||
onChange={this.onChange}
|
||||
>
|
||||
<option value="stretch">Stretch</option>
|
||||
<option value="cover">Cover</option>
|
||||
</select>
|
||||
</label>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|