Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
668acf2698 | ||
|
|
7042491257 | ||
|
|
307e1f9356 |
5
.firebaserc
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"projects": {
|
||||||
|
"default": "squoosh-beta"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
.gitignore
vendored
@@ -3,4 +3,3 @@ node_modules
|
|||||||
/*.log
|
/*.log
|
||||||
*.scss.d.ts
|
*.scss.d.ts
|
||||||
*.css.d.ts
|
*.css.d.ts
|
||||||
*.o
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
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
|
|
||||||
20
Dockerfile
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
FROM selenium/node-chrome:latest
|
||||||
|
|
||||||
|
USER root
|
||||||
|
|
||||||
|
RUN apt-get update -qqy \
|
||||||
|
&& apt-get -qqy install python git build-essential \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* /var/cache/apt/* \
|
||||||
|
&& rm /bin/sh && ln -s /bin/bash /bin/sh \
|
||||||
|
&& chown seluser /usr/local
|
||||||
|
|
||||||
|
ENV NVM_DIR /usr/local/nvm
|
||||||
|
RUN wget -qO- https://raw.githubusercontent.com/creationix/nvm/v0.33.2/install.sh | bash \
|
||||||
|
&& source $NVM_DIR/nvm.sh \
|
||||||
|
&& nvm install v8
|
||||||
|
|
||||||
|
ENV CHROME_BIN /opt/google/chrome/chrome
|
||||||
|
ENV INSIDE_DOCKER=1
|
||||||
|
|
||||||
|
WORKDIR /usr/src
|
||||||
|
ENTRYPOINT source $NVM_DIR/nvm.sh && npm i && npm test
|
||||||
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 239 KiB |
@@ -1,30 +0,0 @@
|
|||||||
# 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()`.
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
#!/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 "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
#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
@@ -1,15 +0,0 @@
|
|||||||
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;
|
|
||||||
|
|
||||||
|
|
||||||
1147
codecs/imagequant/package-lock.json
generated
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,6 +6,8 @@
|
|||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
- Docker
|
- Docker
|
||||||
|
- Automake
|
||||||
|
- pkg-config
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
@@ -17,27 +19,26 @@ See `example.html`
|
|||||||
|
|
||||||
Returns the version of MozJPEG as a number. va.b.c is encoded as 0x0a0b0c
|
Returns the version of MozJPEG as a number. va.b.c is encoded as 0x0a0b0c
|
||||||
|
|
||||||
|
### `uint8_t* create_buffer(int width, int height)`
|
||||||
|
|
||||||
|
Allocates an RGBA buffer for an image with the given dimension.
|
||||||
|
|
||||||
|
### `void destroy_buffer(uint8_t* p)`
|
||||||
|
|
||||||
|
Frees a buffer created with `create_buffer`.
|
||||||
|
|
||||||
|
### `void encode(uint8_t* image_buffer, int image_width, int image_height, int quality)`
|
||||||
|
|
||||||
|
Encodes the given image with given dimension to JPEG. `quality` is a number between 0 and 100. The higher the number, the better the quality of the encoded image. The result is implicitly stored and can be accessed using the `get_result_*()` functions.
|
||||||
|
|
||||||
### `void free_result()`
|
### `void free_result()`
|
||||||
|
|
||||||
Frees the result created by `encode()`.
|
Frees the result created by `encode()`.
|
||||||
|
|
||||||
### `Uint8Array encode(std::string image_in, int image_width, int image_height, MozJpegOptions opts)`
|
### `int get_result_pointer()`
|
||||||
|
|
||||||
Encodes the given image with given dimension to JPEG. Options looks like this:
|
Returns the pointer to the start of the buffer holding the encoded data.
|
||||||
|
|
||||||
```c++
|
### `int get_result_size()`
|
||||||
struct MozJpegOptions {
|
|
||||||
int quality;
|
Returns the length of the buffer holding the encoded data.
|
||||||
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;
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
#!/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 "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<script src='mozjpeg_enc.js'></script>
|
<script src='mozjpeg_enc.js'></script>
|
||||||
<script>
|
<script>
|
||||||
const module = mozjpeg_enc();
|
const Module = mozjpeg_enc();
|
||||||
|
|
||||||
async function loadImage(src) {
|
async function loadImage(src) {
|
||||||
// Load image
|
// Load image
|
||||||
@@ -17,23 +17,26 @@
|
|||||||
return ctx.getImageData(0, 0, img.width, img.height);
|
return ctx.getImageData(0, 0, img.width, img.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.onRuntimeInitialized = async _ => {
|
Module.onRuntimeInitialized = async _ => {
|
||||||
console.log('Version:', module.version().toString(16));
|
const api = {
|
||||||
|
version: Module.cwrap('version', 'number', []),
|
||||||
|
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
|
||||||
|
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
|
||||||
|
encode: Module.cwrap('encode', '', ['number', 'number', 'number', 'number']),
|
||||||
|
free_result: Module.cwrap('free_result', '', ['number']),
|
||||||
|
get_result_pointer: Module.cwrap('get_result_pointer', 'number', []),
|
||||||
|
get_result_size: Module.cwrap('get_result_size', 'number', []),
|
||||||
|
};
|
||||||
const image = await loadImage('../example.png');
|
const image = await loadImage('../example.png');
|
||||||
const result = module.encode(image.data, image.width, image.height, {
|
const p = api.create_buffer(image.width, image.height);
|
||||||
quality: 40,
|
Module.HEAP8.set(image.data, p);
|
||||||
baseline: false,
|
api.encode(p, image.width, image.height, 2);
|
||||||
arithmetic: false,
|
const resultPointer = api.get_result_pointer();
|
||||||
progressive: true,
|
const resultSize = api.get_result_size();
|
||||||
optimize_coding: true,
|
const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);
|
||||||
smoothing: 0,
|
const result = new Uint8Array(resultView);
|
||||||
color_space: 3,
|
api.free_result(resultPointer);
|
||||||
quant_table: 3,
|
api.destroy_buffer(p);
|
||||||
trellis_multipass: true,
|
|
||||||
trellis_opt_zero: true,
|
|
||||||
trellis_opt_table: true,
|
|
||||||
trellis_loops: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
const blob = new Blob([result], {type: 'image/jpeg'});
|
const blob = new Blob([result], {type: 'image/jpeg'});
|
||||||
const blobURL = URL.createObjectURL(blob);
|
const blobURL = URL.createObjectURL(blob);
|
||||||
|
|||||||
@@ -1,36 +1,17 @@
|
|||||||
#include <emscripten/bind.h>
|
#include "emscripten.h"
|
||||||
#include <emscripten/val.h>
|
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <inttypes.h>
|
#include <inttypes.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <setjmp.h>
|
#include <setjmp.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include "config.h"
|
|
||||||
#include "jpeglib.h"
|
#include "jpeglib.h"
|
||||||
#include "cdjpeg.h"
|
#include "config.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
|
||||||
|
|
||||||
// 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 xstr(s) str(s)
|
||||||
#define str(s) #s
|
#define str(s) #s
|
||||||
|
|
||||||
struct MozJpegOptions {
|
EMSCRIPTEN_KEEPALIVE
|
||||||
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() {
|
int version() {
|
||||||
char buffer[] = xstr(MOZJPEG_VERSION);
|
char buffer[] = xstr(MOZJPEG_VERSION);
|
||||||
int version = 0;
|
int version = 0;
|
||||||
@@ -47,11 +28,27 @@ int version() {
|
|||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t* last_result;
|
EMSCRIPTEN_KEEPALIVE
|
||||||
struct jpeg_compress_struct cinfo;
|
uint8_t* create_buffer(int width, int height) {
|
||||||
|
return malloc(width * height * 4 * sizeof(uint8_t));
|
||||||
|
}
|
||||||
|
|
||||||
val encode(std::string image_in, int image_width, int image_height, MozJpegOptions opts) {
|
EMSCRIPTEN_KEEPALIVE
|
||||||
uint8_t* image_buffer = (uint8_t*) image_in.c_str();
|
void destroy_buffer(uint8_t* p) {
|
||||||
|
free(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
int result[2];
|
||||||
|
EMSCRIPTEN_KEEPALIVE
|
||||||
|
void encode(uint8_t* image_buffer, int image_width, int image_height, int quality) {
|
||||||
|
// Manually convert RGBA data into RGB
|
||||||
|
for(int y = 0; y < image_height; y++) {
|
||||||
|
for(int x = 0; x < image_width; x++) {
|
||||||
|
image_buffer[(y*image_width + x)*3 + 0] = image_buffer[(y*image_width + x)*4 + 0];
|
||||||
|
image_buffer[(y*image_width + x)*3 + 1] = image_buffer[(y*image_width + x)*4 + 1];
|
||||||
|
image_buffer[(y*image_width + x)*3 + 2] = image_buffer[(y*image_width + x)*4 + 2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The code below is basically the `write_JPEG_file` function from
|
// The code below is basically the `write_JPEG_file` function from
|
||||||
// https://github.com/mozilla/mozjpeg/blob/master/example.c
|
// https://github.com/mozilla/mozjpeg/blob/master/example.c
|
||||||
@@ -64,6 +61,7 @@ val encode(std::string image_in, int image_width, int image_height, MozJpegOptio
|
|||||||
* compression/decompression processes, in existence at once. We refer
|
* compression/decompression processes, in existence at once. We refer
|
||||||
* to any one struct (and its associated working data) as a "JPEG object".
|
* to any one struct (and its associated working data) as a "JPEG object".
|
||||||
*/
|
*/
|
||||||
|
struct jpeg_compress_struct cinfo;
|
||||||
/* This struct represents a JPEG error handler. It is declared separately
|
/* This struct represents a JPEG error handler. It is declared separately
|
||||||
* because applications often want to supply a specialized error handler
|
* because applications often want to supply a specialized error handler
|
||||||
* (see the second half of this file for an example). But here we just
|
* (see the second half of this file for an example). But here we just
|
||||||
@@ -111,48 +109,18 @@ val encode(std::string image_in, int image_width, int image_height, MozJpegOptio
|
|||||||
*/
|
*/
|
||||||
cinfo.image_width = image_width; /* image width and height, in pixels */
|
cinfo.image_width = image_width; /* image width and height, in pixels */
|
||||||
cinfo.image_height = image_height;
|
cinfo.image_height = image_height;
|
||||||
cinfo.input_components = 4; /* # of color components per pixel */
|
cinfo.input_components = 3; /* # of color components per pixel */
|
||||||
cinfo.in_color_space = JCS_EXT_RGBA; /* colorspace of input image */
|
cinfo.in_color_space = JCS_RGB; /* colorspace of input image */
|
||||||
/* Now use the library's routine to set default compression parameters.
|
/* Now use the library's routine to set default compression parameters.
|
||||||
* (You must set at least cinfo.in_color_space before calling this,
|
* (You must set at least cinfo.in_color_space before calling this,
|
||||||
* since the defaults depend on the source color space.)
|
* since the defaults depend on the source color space.)
|
||||||
*/
|
*/
|
||||||
jpeg_set_defaults(&cinfo);
|
jpeg_set_defaults(&cinfo);
|
||||||
|
|
||||||
/* Now you can set any non-default parameters you wish to.
|
/* Now you can set any non-default parameters you wish to.
|
||||||
* Here we just illustrate the use of quality (quantization table) scaling:
|
* Here we just illustrate the use of quality (quantization table) scaling:
|
||||||
*/
|
*/
|
||||||
jpeg_set_colorspace(&cinfo, (J_COLOR_SPACE) opts.color_space);
|
jpeg_set_quality(&cinfo, quality, TRUE /* limit to baseline-JPEG values */);
|
||||||
|
|
||||||
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 */
|
/* Step 4: Start compressor */
|
||||||
|
|
||||||
/* TRUE ensures that we will write a complete interchange-JPEG file.
|
/* TRUE ensures that we will write a complete interchange-JPEG file.
|
||||||
@@ -168,7 +136,7 @@ val encode(std::string image_in, int image_width, int image_height, MozJpegOptio
|
|||||||
* To keep things simple, we pass one scanline per call; you can pass
|
* To keep things simple, we pass one scanline per call; you can pass
|
||||||
* more if you wish, though.
|
* more if you wish, though.
|
||||||
*/
|
*/
|
||||||
row_stride = image_width * 4; /* JSAMPLEs per row in image_buffer */
|
row_stride = image_width * 3; /* JSAMPLEs per row in image_buffer */
|
||||||
|
|
||||||
while (cinfo.next_scanline < cinfo.image_height) {
|
while (cinfo.next_scanline < cinfo.image_height) {
|
||||||
/* jpeg_write_scanlines expects an array of pointers to scanlines.
|
/* jpeg_write_scanlines expects an array of pointers to scanlines.
|
||||||
@@ -184,34 +152,27 @@ val encode(std::string image_in, int image_width, int image_height, MozJpegOptio
|
|||||||
jpeg_finish_compress(&cinfo);
|
jpeg_finish_compress(&cinfo);
|
||||||
/* Step 7: release JPEG compression object */
|
/* Step 7: release JPEG compression object */
|
||||||
|
|
||||||
last_result = output;
|
result[0] = (int)output;
|
||||||
|
result[1] = size;
|
||||||
|
|
||||||
/* 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. */
|
/* This is an important step since it will release a good deal of memory. */
|
||||||
jpeg_destroy_compress(&cinfo);
|
jpeg_destroy_compress(&cinfo);
|
||||||
|
|
||||||
|
/* And we're done! */
|
||||||
}
|
}
|
||||||
|
|
||||||
EMSCRIPTEN_BINDINGS(my_module) {
|
EMSCRIPTEN_KEEPALIVE
|
||||||
value_object<MozJpegOptions>("MozJpegOptions")
|
void free_result() {
|
||||||
.field("quality", &MozJpegOptions::quality)
|
free(result[0]); // not sure if this is right with mozjpeg
|
||||||
.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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EMSCRIPTEN_KEEPALIVE
|
||||||
|
int get_result_pointer() {
|
||||||
|
return result[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
EMSCRIPTEN_KEEPALIVE
|
||||||
|
int get_result_size() {
|
||||||
|
return result[1];
|
||||||
|
}
|
||||||
|
|
||||||
9
codecs/mozjpeg_enc/mozjpeg_enc.d.ts
vendored
@@ -1,8 +1 @@
|
|||||||
import { EncodeOptions } from '../../src/codecs/mozjpeg/encoder-meta';
|
export default function(opts: EmscriptenWasm.ModuleOpts): EmscriptenWasm.Module;
|
||||||
|
|
||||||
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;
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
"name": "mozjpeg_enc",
|
"name": "mozjpeg_enc",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"install": "napa",
|
"install": "napa",
|
||||||
"build": "docker run --rm -v $(pwd):/src trzeci/emscripten ./build.sh"
|
"build": "npm run build:library && npm run build:wasm",
|
||||||
|
"build:library": "cd node_modules/mozjpeg && autoreconf -fiv && docker run --rm -v $(pwd):/src trzeci/emscripten emconfigure ./configure --without-simd && docker run --rm -v $(pwd):/src trzeci/emscripten emmake make libjpeg.la",
|
||||||
|
"build:wasm": "docker run --rm -v $(pwd):/src trzeci/emscripten emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"cwrap\"]' -s ALLOW_MEMORY_GROWTH=1 -s MODULARIZE=1 -s 'EXPORT_NAME=\"mozjpeg_enc\"' -I node_modules/mozjpeg -o ./mozjpeg_enc.js mozjpeg_enc.c node_modules/mozjpeg/.libs/libjpeg.a"
|
||||||
},
|
},
|
||||||
"napa": {
|
"napa": {
|
||||||
"mozjpeg": "mozilla/mozjpeg#v3.3.1"
|
"mozjpeg": "mozilla/mozjpeg#v3.3.1"
|
||||||
|
|||||||
2
codecs/optipng/.gitignore
vendored
@@ -1,2 +0,0 @@
|
|||||||
build/
|
|
||||||
*.o
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# 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()`.
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
#!/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 "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<!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>
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
#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
@@ -1,10 +0,0 @@
|
|||||||
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;
|
|
||||||
|
|
||||||
|
|
||||||
1457
codecs/optipng/package-lock.json
generated
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 20 MiB |
@@ -1,22 +0,0 @@
|
|||||||
# 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()`.
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
#!/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 "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<!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
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
#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
@@ -1,13 +0,0 @@
|
|||||||
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;
|
|
||||||
|
|
||||||
@@ -17,10 +17,26 @@ See `example.html`
|
|||||||
|
|
||||||
Returns the version of libwebp as a number. va.b.c is encoded as 0x0a0b0c
|
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)`
|
### `uint8_t* create_buffer(int width, int height)`
|
||||||
|
|
||||||
Encodes the given image with given dimension to WebP.
|
Allocates an RGBA buffer for an image with the given dimension.
|
||||||
|
|
||||||
|
### `void destroy_buffer(uint8_t* p)`
|
||||||
|
|
||||||
|
Frees a buffer created with `create_buffer`.
|
||||||
|
|
||||||
|
### `void encode(uint8_t* image_buffer, int image_width, int image_height, float quality)`
|
||||||
|
|
||||||
|
Encodes the given image with given dimension to WebP. `quality` is a number between 0 and 100. The higher the number, the better the quality of the encoded image. The result is implicitly stored and can be accessed using the `get_result_*()` functions.
|
||||||
|
|
||||||
### `void free_result()`
|
### `void free_result()`
|
||||||
|
|
||||||
Frees the last result created by `encode()`.
|
Frees the result created by `encode()`.
|
||||||
|
|
||||||
|
### `int get_result_pointer()`
|
||||||
|
|
||||||
|
Returns the pointer to the start of the buffer holding the encoded data.
|
||||||
|
|
||||||
|
### `int get_result_size()`
|
||||||
|
|
||||||
|
Returns the length of the buffer holding the encoded data.
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
#!/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 \
|
|
||||||
-D WEBP_USE_THREAD=1 \
|
|
||||||
-s USE_PTHREADS=1 \
|
|
||||||
-s ASSERTIONS=1 \
|
|
||||||
-s PTHREAD_POOL_SIZE=4 \
|
|
||||||
-s TOTAL_MEMORY=268435456 \
|
|
||||||
-s WASM_MEM_MAX=268435456 \
|
|
||||||
--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 "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<script src='webp_enc.js'></script>
|
<script src='webp_enc.js'></script>
|
||||||
<script>
|
<script>
|
||||||
// const Module = webp_enc();
|
const Module = webp_enc();
|
||||||
|
|
||||||
async function loadImage(src) {
|
async function loadImage(src) {
|
||||||
// Load image
|
// Load image
|
||||||
@@ -18,45 +18,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
Module.onRuntimeInitialized = async _ => {
|
Module.onRuntimeInitialized = async _ => {
|
||||||
console.log('Version:', Module.version().toString(16));
|
const api = {
|
||||||
const image = await loadImage('../really_big.jpg');
|
version: Module.cwrap('version', 'number', []),
|
||||||
let start = performance.now();
|
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
|
||||||
const result = Module.encode(image.data, image.width, image.height, {
|
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
|
||||||
quality: 75,
|
encode: Module.cwrap('encode', '', ['number', 'number', 'number', 'number']),
|
||||||
target_size: 0,
|
free_result: Module.cwrap('free_result', '', ['number']),
|
||||||
target_PSNR: 0,
|
get_result_pointer: Module.cwrap('get_result_pointer', 'number', []),
|
||||||
method: 4,
|
get_result_size: Module.cwrap('get_result_size', 'number', []),
|
||||||
sns_strength: 50,
|
};
|
||||||
filter_strength: 60,
|
const image = await loadImage('../example.png');
|
||||||
filter_sharpness: 0,
|
const p = api.create_buffer(image.width, image.height);
|
||||||
filter_type: 1,
|
Module.HEAP8.set(image.data, p);
|
||||||
partitions: 0,
|
api.encode(p, image.width, image.height, 2);
|
||||||
segments: 4,
|
const resultPointer = api.get_result_pointer();
|
||||||
pass: 1,
|
const resultSize = api.get_result_size();
|
||||||
show_compressed: 0,
|
const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);
|
||||||
preprocessing: 0,
|
const result = new Uint8Array(resultView);
|
||||||
autofilter: 0,
|
api.free_result(resultPointer);
|
||||||
partition_limit: 0,
|
api.destroy_buffer(p);
|
||||||
alpha_compression: 1,
|
|
||||||
alpha_filtering: 1,
|
|
||||||
alpha_quality: 100,
|
|
||||||
lossless: 0,
|
|
||||||
exact: 0,
|
|
||||||
image_hint: 0,
|
|
||||||
emulate_jpeg_size: 0,
|
|
||||||
thread_level: 1,
|
|
||||||
low_memory: 0,
|
|
||||||
near_lossless: 100,
|
|
||||||
use_delta_palette: 0,
|
|
||||||
use_sharp_yuv: 0,
|
|
||||||
});
|
|
||||||
let stop = performance.now();
|
|
||||||
console.log('size', result.length);
|
|
||||||
console.log('time', stop - start);
|
|
||||||
const blob = new Blob([new Uint8Array(result)], {type: 'image/webp'});
|
|
||||||
|
|
||||||
Module.free_result();
|
|
||||||
|
|
||||||
|
const blob = new Blob([result], {type: 'image/jpeg'});
|
||||||
const blobURL = URL.createObjectURL(blob);
|
const blobURL = URL.createObjectURL(blob);
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
img.src = blobURL;
|
img.src = blobURL;
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
"name": "webp_enc",
|
"name": "webp_enc",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"install": "napa",
|
"install": "napa",
|
||||||
"build": "docker run --rm -v $(pwd):/src trzeci/emscripten ./build.sh"
|
"build": "docker run --rm -v $(pwd):/src trzeci/emscripten emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"cwrap\"]' -s ALLOW_MEMORY_GROWTH=1 -s MODULARIZE=1 -s 'EXPORT_NAME=\"webp_enc\"' -I node_modules/libwebp -o ./webp_enc.js webp_enc.c node_modules/libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c"
|
||||||
},
|
},
|
||||||
"napa": {
|
"napa": {
|
||||||
"libwebp": "webmproject/libwebp#v1.0.0"
|
"libwebp": "webmproject/libwebp#v0.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"napa": "^3.0.0"
|
"napa": "^3.0.0"
|
||||||
|
|||||||
@@ -1,192 +0,0 @@
|
|||||||
// Copyright 2015 The Emscripten Authors. All rights reserved.
|
|
||||||
// Emscripten is available under two separate licenses, the MIT license and the
|
|
||||||
// University of Illinois/NCSA Open Source License. Both these licenses can be
|
|
||||||
// found in the LICENSE file.
|
|
||||||
|
|
||||||
// Pthread Web Worker startup routine:
|
|
||||||
// This is the entry point file that is loaded first by each Web Worker
|
|
||||||
// that executes pthreads on the Emscripten application.
|
|
||||||
|
|
||||||
// Thread-local:
|
|
||||||
var threadInfoStruct = 0; // Info area for this thread in Emscripten HEAP (shared). If zero, this worker is not currently hosting an executing pthread.
|
|
||||||
var selfThreadId = 0; // The ID of this thread. 0 if not hosting a pthread.
|
|
||||||
var parentThreadId = 0; // The ID of the parent pthread that launched this thread.
|
|
||||||
var tempDoublePtr = 0; // A temporary memory area for global float and double marshalling operations.
|
|
||||||
|
|
||||||
// Thread-local: Each thread has its own allocated stack space.
|
|
||||||
var STACK_BASE = 0;
|
|
||||||
var STACKTOP = 0;
|
|
||||||
var STACK_MAX = 0;
|
|
||||||
|
|
||||||
// These are system-wide memory area parameters that are set at main runtime startup in main thread, and stay constant throughout the application.
|
|
||||||
var buffer; // All pthreads share the same Emscripten HEAP as SharedArrayBuffer with the main execution thread.
|
|
||||||
var DYNAMICTOP_PTR = 0;
|
|
||||||
var TOTAL_MEMORY = 0;
|
|
||||||
var STATICTOP = 0;
|
|
||||||
var staticSealed = true; // When threads are being initialized, the static memory area has been already sealed a long time ago.
|
|
||||||
var DYNAMIC_BASE = 0;
|
|
||||||
|
|
||||||
var ENVIRONMENT_IS_PTHREAD = true;
|
|
||||||
|
|
||||||
// performance.now() is specced to return a wallclock time in msecs since that Web Worker/main thread launched. However for pthreads this can cause
|
|
||||||
// subtle problems in emscripten_get_now() as this essentially would measure time from pthread_create(), meaning that the clocks between each threads
|
|
||||||
// would be wildly out of sync. Therefore sync all pthreads to the clock on the main browser thread, so that different threads see a somewhat
|
|
||||||
// coherent clock across each of them (+/- 0.1msecs in testing)
|
|
||||||
var __performance_now_clock_drift = 0;
|
|
||||||
|
|
||||||
// Cannot use console.log or console.error in a web worker, since that would risk a browser deadlock! https://bugzilla.mozilla.org/show_bug.cgi?id=1049091
|
|
||||||
// Therefore implement custom logging facility for threads running in a worker, which queue the messages to main thread to print.
|
|
||||||
var Module = {};
|
|
||||||
|
|
||||||
// When error objects propagate from Web Worker to main thread, they lose helpful call stack and thread ID information, so print out errors early here,
|
|
||||||
// before that happens.
|
|
||||||
this.addEventListener('error', function(e) {
|
|
||||||
if (e.message.indexOf('SimulateInfiniteLoop') != -1) return e.preventDefault();
|
|
||||||
|
|
||||||
var errorSource = ' in ' + e.filename + ':' + e.lineno + ':' + e.colno;
|
|
||||||
console.error('Pthread ' + selfThreadId + ' uncaught exception' + (e.filename || e.lineno || e.colno ? errorSource : '') + ': ' + e.message + '. Error object:');
|
|
||||||
console.error(e.error);
|
|
||||||
});
|
|
||||||
|
|
||||||
function threadPrint() {
|
|
||||||
var text = Array.prototype.slice.call(arguments).join(' ');
|
|
||||||
console.log(text);
|
|
||||||
}
|
|
||||||
function threadPrintErr() {
|
|
||||||
var text = Array.prototype.slice.call(arguments).join(' ');
|
|
||||||
console.error(text);
|
|
||||||
console.error(new Error().stack);
|
|
||||||
}
|
|
||||||
function threadAlert() {
|
|
||||||
var text = Array.prototype.slice.call(arguments).join(' ');
|
|
||||||
postMessage({cmd: 'alert', text: text, threadId: selfThreadId});
|
|
||||||
}
|
|
||||||
out = threadPrint;
|
|
||||||
err = threadPrintErr;
|
|
||||||
this.alert = threadAlert;
|
|
||||||
|
|
||||||
// #if WASM
|
|
||||||
Module['instantiateWasm'] = function(info, receiveInstance) {
|
|
||||||
// Instantiate from the module posted from the main thread.
|
|
||||||
// We can just use sync instantiation in the worker.
|
|
||||||
instance = new WebAssembly.Instance(Module['wasmModule'], info);
|
|
||||||
// We don't need the module anymore; new threads will be spawned from the main thread.
|
|
||||||
delete Module['wasmModule'];
|
|
||||||
receiveInstance(instance);
|
|
||||||
return instance.exports;
|
|
||||||
}
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
this.onmessage = function(e) {
|
|
||||||
try {
|
|
||||||
if (e.data.cmd === 'load') { // Preload command that is called once per worker to parse and load the Emscripten code.
|
|
||||||
// Initialize the thread-local field(s):
|
|
||||||
tempDoublePtr = e.data.tempDoublePtr;
|
|
||||||
|
|
||||||
// Initialize the global "process"-wide fields:
|
|
||||||
Module['TOTAL_MEMORY'] = TOTAL_MEMORY = e.data.TOTAL_MEMORY;
|
|
||||||
STATICTOP = e.data.STATICTOP;
|
|
||||||
DYNAMIC_BASE = e.data.DYNAMIC_BASE;
|
|
||||||
DYNAMICTOP_PTR = e.data.DYNAMICTOP_PTR;
|
|
||||||
|
|
||||||
|
|
||||||
//#if WASM
|
|
||||||
if (e.data.wasmModule) {
|
|
||||||
// Module and memory were sent from main thread
|
|
||||||
Module['wasmModule'] = e.data.wasmModule;
|
|
||||||
Module['wasmMemory'] = e.data.wasmMemory;
|
|
||||||
buffer = Module['wasmMemory'].buffer;
|
|
||||||
} else {
|
|
||||||
//#else
|
|
||||||
buffer = e.data.buffer;
|
|
||||||
}
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
PthreadWorkerInit = e.data.PthreadWorkerInit;
|
|
||||||
if (typeof e.data.urlOrBlob === 'string') {
|
|
||||||
importScripts(e.data.urlOrBlob);
|
|
||||||
} else {
|
|
||||||
var objectUrl = URL.createObjectURL(e.data.urlOrBlob);
|
|
||||||
importScripts(objectUrl);
|
|
||||||
URL.revokeObjectURL(objectUrl);
|
|
||||||
}
|
|
||||||
//#if !ASMFS
|
|
||||||
if (typeof FS !== 'undefined' && typeof FS.createStandardStreams === 'function') FS.createStandardStreams();
|
|
||||||
//#endif
|
|
||||||
postMessage({ cmd: 'loaded' });
|
|
||||||
} else if (e.data.cmd === 'objectTransfer') {
|
|
||||||
PThread.receiveObjectTransfer(e.data);
|
|
||||||
} else if (e.data.cmd === 'run') { // This worker was idle, and now should start executing its pthread entry point.
|
|
||||||
__performance_now_clock_drift = performance.now() - e.data.time; // Sync up to the clock of the main thread.
|
|
||||||
threadInfoStruct = e.data.threadInfoStruct;
|
|
||||||
__register_pthread_ptr(threadInfoStruct, /*isMainBrowserThread=*/0, /*isMainRuntimeThread=*/0); // Pass the thread address inside the asm.js scope to store it for fast access that avoids the need for a FFI out.
|
|
||||||
assert(threadInfoStruct);
|
|
||||||
selfThreadId = e.data.selfThreadId;
|
|
||||||
parentThreadId = e.data.parentThreadId;
|
|
||||||
assert(selfThreadId);
|
|
||||||
assert(parentThreadId);
|
|
||||||
// TODO: Emscripten runtime has these variables twice(!), once outside the asm.js module, and a second time inside the asm.js module.
|
|
||||||
// Review why that is? Can those get out of sync?
|
|
||||||
STACK_BASE = STACKTOP = e.data.stackBase;
|
|
||||||
STACK_MAX = STACK_BASE + e.data.stackSize;
|
|
||||||
assert(STACK_BASE != 0);
|
|
||||||
assert(STACK_MAX > STACK_BASE);
|
|
||||||
Module['establishStackSpace'](e.data.stackBase, e.data.stackBase + e.data.stackSize);
|
|
||||||
var result = 0;
|
|
||||||
//#if STACK_OVERFLOW_CHECK
|
|
||||||
if (typeof writeStackCookie === 'function') writeStackCookie();
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
PThread.receiveObjectTransfer(e.data);
|
|
||||||
PThread.setThreadStatus(_pthread_self(), 1/*EM_THREAD_STATUS_RUNNING*/);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// pthread entry points are always of signature 'void *ThreadMain(void *arg)'
|
|
||||||
// Native codebases sometimes spawn threads with other thread entry point signatures,
|
|
||||||
// such as void ThreadMain(void *arg), void *ThreadMain(), or void ThreadMain().
|
|
||||||
// That is not acceptable per C/C++ specification, but x86 compiler ABI extensions
|
|
||||||
// enable that to work. If you find the following line to crash, either change the signature
|
|
||||||
// to "proper" void *ThreadMain(void *arg) form, or try linking with the Emscripten linker
|
|
||||||
// flag -s EMULATE_FUNCTION_POINTER_CASTS=1 to add in emulation for this x86 ABI extension.
|
|
||||||
result = Module['dynCall_ii'](e.data.start_routine, e.data.arg);
|
|
||||||
|
|
||||||
//#if STACK_OVERFLOW_CHECK
|
|
||||||
if (typeof checkStackCookie === 'function') checkStackCookie();
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
} catch(e) {
|
|
||||||
if (e === 'Canceled!') {
|
|
||||||
PThread.threadCancel();
|
|
||||||
return;
|
|
||||||
} else if (e === 'SimulateInfiniteLoop') {
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
Atomics.store(HEAPU32, (threadInfoStruct + 4 /*{{{ C_STRUCTS.pthread.threadExitCode }}}*/ ) >> 2, (e instanceof ExitStatus) ? e.status : -2 /*A custom entry specific to Emscripten denoting that the thread crashed.*/);
|
|
||||||
Atomics.store(HEAPU32, (threadInfoStruct + 0 /*{{{ C_STRUCTS.pthread.threadStatus }}}*/ ) >> 2, 1); // Mark the thread as no longer running.
|
|
||||||
_emscripten_futex_wake(threadInfoStruct + 0 /*{{{ C_STRUCTS.pthread.threadStatus }}}*/, 0x7FFFFFFF/*INT_MAX*/); // Wake all threads waiting on this thread to finish.
|
|
||||||
if (!(e instanceof ExitStatus)) throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// The thread might have finished without calling pthread_exit(). If so, then perform the exit operation ourselves.
|
|
||||||
// (This is a no-op if explicit pthread_exit() had been called prior.)
|
|
||||||
PThread.threadExit(result);
|
|
||||||
} else if (e.data.cmd === 'cancel') { // Main thread is asking for a pthread_cancel() on this thread.
|
|
||||||
if (threadInfoStruct && PThread.thisThreadCancelState == 0/*PTHREAD_CANCEL_ENABLE*/) {
|
|
||||||
PThread.threadCancel();
|
|
||||||
}
|
|
||||||
} else if (e.data.target === 'setimmediate') {
|
|
||||||
// no-op
|
|
||||||
} else if (e.data.cmd === 'processThreadQueue') {
|
|
||||||
if (threadInfoStruct) { // If this thread is actually running?
|
|
||||||
_emscripten_current_thread_process_queued_calls();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err('pthread-main.js received unknown command ' + e.data.cmd);
|
|
||||||
console.error(e.data);
|
|
||||||
}
|
|
||||||
} catch(e) {
|
|
||||||
console.error('pthread-main.js onmessage() captured an uncaught exception: ' + e);
|
|
||||||
console.error(e.stack);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
44
codecs/webp_enc/webp_enc.c
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
#include "emscripten.h"
|
||||||
|
#include "src/webp/encode.h"
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
EMSCRIPTEN_KEEPALIVE
|
||||||
|
int version() {
|
||||||
|
return WebPGetEncoderVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
EMSCRIPTEN_KEEPALIVE
|
||||||
|
uint8_t* create_buffer(int width, int height) {
|
||||||
|
return malloc(width * height * 4 * sizeof(uint8_t));
|
||||||
|
}
|
||||||
|
|
||||||
|
EMSCRIPTEN_KEEPALIVE
|
||||||
|
void destroy_buffer(uint8_t* p) {
|
||||||
|
free(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
int result[2];
|
||||||
|
EMSCRIPTEN_KEEPALIVE
|
||||||
|
void encode(uint8_t* img_in, int width, int height, float quality) {
|
||||||
|
uint8_t* img_out;
|
||||||
|
size_t size;
|
||||||
|
size = WebPEncodeRGBA(img_in, width, height, width * 4, quality, &img_out);
|
||||||
|
result[0] = (int)img_out;
|
||||||
|
result[1] = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
EMSCRIPTEN_KEEPALIVE
|
||||||
|
void free_result() {
|
||||||
|
WebPFree(result[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
EMSCRIPTEN_KEEPALIVE
|
||||||
|
int get_result_pointer() {
|
||||||
|
return result[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
EMSCRIPTEN_KEEPALIVE
|
||||||
|
int get_result_size() {
|
||||||
|
return result[1];
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
#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
@@ -1,9 +0,0 @@
|
|||||||
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;
|
|
||||||
54
firebase.json
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"hosting": {
|
||||||
|
"public": "build",
|
||||||
|
"ignore": [
|
||||||
|
"firebase.json",
|
||||||
|
"**/.*",
|
||||||
|
"**/node_modules/**"
|
||||||
|
],
|
||||||
|
"rewrites": [
|
||||||
|
{
|
||||||
|
"source": "**",
|
||||||
|
"destination": "/index.html"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"source": "**/*.@(eot|otf|ttf|ttc|woff|font.css)",
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"key": "Access-Control-Allow-Origin",
|
||||||
|
"value": "*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "**/*.@(jpg|jpeg|gif|png|ico)",
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"key": "Cache-Control",
|
||||||
|
"value": "max-age=7200"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "**/*.@(js|css|json|manifest|map)",
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"key": "Cache-Control",
|
||||||
|
"value": "max-age=31536000"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "sw.js",
|
||||||
|
"headers": [
|
||||||
|
{
|
||||||
|
"key": "Cache-Control",
|
||||||
|
"value": "max-age=0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
4
global.d.ts
vendored
@@ -13,6 +13,10 @@ declare namespace JSX {
|
|||||||
interface IntrinsicElements { }
|
interface IntrinsicElements { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare module 'preact-i18n';
|
||||||
|
declare module 'preact-material-components-drawer';
|
||||||
|
declare module 'material-radial-progress';
|
||||||
|
|
||||||
declare module 'classnames' {
|
declare module 'classnames' {
|
||||||
export default function classnames(...args: any[]): string;
|
export default function classnames(...args: any[]): string;
|
||||||
}
|
}
|
||||||
|
|||||||
113
karma.conf.js
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
|
||||||
|
function readJsonFile(path) {
|
||||||
|
// TypeScript puts lots of comments in the default `tsconfig.json`, so you
|
||||||
|
// can’t use `require()` to read it. Hence this hack.
|
||||||
|
return eval("(" + fs.readFileSync(path).toString("utf-8") + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeScriptConfig = readJsonFile("./tsconfig.json");
|
||||||
|
const babel = readJsonFile("./.babelrc");
|
||||||
|
|
||||||
|
module.exports = function(config) {
|
||||||
|
const options = {
|
||||||
|
// base path that will be used to resolve all patterns (eg. files, exclude)
|
||||||
|
basePath: "",
|
||||||
|
|
||||||
|
// frameworks to use
|
||||||
|
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
|
||||||
|
frameworks: ["mocha", "chai", "karma-typescript"],
|
||||||
|
|
||||||
|
// list of files / patterns to load in the browser
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
pattern: "test/**/*.ts",
|
||||||
|
type: "module"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "src/**/*.ts",
|
||||||
|
included: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
// list of files / patterns to exclude
|
||||||
|
exclude: [],
|
||||||
|
// preprocess matching files before serving them to the browser // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
|
||||||
|
preprocessors: {
|
||||||
|
"src/**/*.ts": ["karma-typescript", "babel"],
|
||||||
|
"test/**/*.ts": ["karma-typescript", "babel"]
|
||||||
|
},
|
||||||
|
babelPreprocessor: {
|
||||||
|
options: babel
|
||||||
|
},
|
||||||
|
karmaTypescriptConfig: {
|
||||||
|
// Inline `tsconfig.json` so that the right TS libs are loaded
|
||||||
|
...typeScriptConfig,
|
||||||
|
// Coverage is a thing that karma-typescript forces on you and only
|
||||||
|
// creates problems. This is the simplest way of disabling it that I
|
||||||
|
// could find.
|
||||||
|
coverageOptions: {
|
||||||
|
exclude: /.*/
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mime: {
|
||||||
|
// Default mimetype for .ts files is video/mp2t but we need
|
||||||
|
// text/javascript for modules to work.
|
||||||
|
"text/javascript": ["ts"]
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
// Load all modules whose name starts with "karma" (usually the default).
|
||||||
|
"karma-*",
|
||||||
|
// We don’t have file extensions on our imports as they are primarily
|
||||||
|
// consumed by webpack. With Karma, however, this turns into a real HTTP
|
||||||
|
// request for a non-existent file. This inline plugin is a middleware
|
||||||
|
// that appends `.ts` to the request URL.
|
||||||
|
{
|
||||||
|
"middleware:redirect_to_ts": [
|
||||||
|
"value",
|
||||||
|
(req, res, next) => {
|
||||||
|
if (req.url.startsWith("/base/src")) {
|
||||||
|
req.url += '.ts';
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// Run our middleware before all other middlewares.
|
||||||
|
beforeMiddleware: ["redirect_to_ts"],
|
||||||
|
|
||||||
|
// test results reporter to use
|
||||||
|
// possible values: 'dots', 'progress'
|
||||||
|
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
|
||||||
|
reporters: ["progress"],
|
||||||
|
|
||||||
|
// web server port
|
||||||
|
port: 9876,
|
||||||
|
|
||||||
|
// enable / disable colors in the output (reporters and logs)
|
||||||
|
colors: true,
|
||||||
|
|
||||||
|
// level of logging
|
||||||
|
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
|
||||||
|
logLevel: config.LOG_INFO,
|
||||||
|
|
||||||
|
// enable / disable watching file and executing tests whenever any file changes
|
||||||
|
autoWatch: false,
|
||||||
|
|
||||||
|
// start these browsers
|
||||||
|
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
|
||||||
|
browsers: ["ChromeHeadless"],
|
||||||
|
|
||||||
|
// Continuous Integration mode
|
||||||
|
// if true, Karma captures browsers, runs the tests and exits
|
||||||
|
singleRun: true,
|
||||||
|
|
||||||
|
// These custom files allow us to use ES6 modules in our tests.
|
||||||
|
// Remove these 2 lines (and files) once https://github.com/karma-runner/karma/pull/2834 lands.
|
||||||
|
customContextFile: "test/context.html",
|
||||||
|
customDebugFile: "test/debug.html"
|
||||||
|
};
|
||||||
|
|
||||||
|
config.set(options);
|
||||||
|
};
|
||||||
11760
package-lock.json
generated
109
package.json
@@ -4,67 +4,106 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "apache-2.0",
|
"license": "apache-2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"build:mozjpeg_enc": "cd codecs/mozjpeg_enc && npm run build",
|
||||||
|
"build:codecs": "npm run build:mozjpeg_enc",
|
||||||
"start": "webpack serve --host 0.0.0.0 --hot",
|
"start": "webpack serve --host 0.0.0.0 --hot",
|
||||||
"build": "webpack -p",
|
"build": "webpack -p",
|
||||||
"lint": "tslint -c tslint.json -t verbose 'src/**/*.{ts,tsx,js,jsx}'",
|
"lint": "eslint src",
|
||||||
"lintfix": "tslint -c tslint.json -t verbose --fix 'src/**/*.{ts,tsx,js,jsx}'"
|
"test": "npm run build && mocha -R spec && karma start"
|
||||||
},
|
},
|
||||||
"husky": {
|
"eslintConfig": {
|
||||||
"hooks": {
|
"extends": [
|
||||||
"pre-commit": "npm run lint"
|
"standard",
|
||||||
|
"standard-jsx"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"indent": [
|
||||||
|
2,
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"semi": [
|
||||||
|
2,
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"prefer-const": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"eslintIgnore": [
|
||||||
|
"build/*"
|
||||||
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^9.6.23",
|
"@types/chai": "^4.1.3",
|
||||||
"@types/pretty-bytes": "^5.1.0",
|
"@types/karma": "^1.7.3",
|
||||||
|
"@types/mocha": "^5.2.0",
|
||||||
|
"@types/node": "^9.4.7",
|
||||||
"@types/webassembly-js-api": "0.0.1",
|
"@types/webassembly-js-api": "0.0.1",
|
||||||
"@webcomponents/custom-elements": "^1.2.0",
|
"babel-loader": "^7.1.4",
|
||||||
"babel-loader": "^7.1.5",
|
|
||||||
"babel-plugin-jsx-pragmatic": "^1.0.2",
|
"babel-plugin-jsx-pragmatic": "^1.0.2",
|
||||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||||
"babel-plugin-transform-decorators-legacy": "^1.3.5",
|
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||||
"babel-plugin-transform-react-constant-elements": "^6.23.0",
|
"babel-plugin-transform-react-constant-elements": "^6.23.0",
|
||||||
"babel-plugin-transform-react-jsx": "^6.24.1",
|
"babel-plugin-transform-react-jsx": "^6.24.1",
|
||||||
"babel-plugin-transform-react-remove-prop-types": "^0.4.14",
|
"babel-plugin-transform-react-remove-prop-types": "^0.4.13",
|
||||||
"babel-preset-env": "^1.7.0",
|
"babel-preset-env": "^1.6.1",
|
||||||
"babel-register": "^6.26.0",
|
"babel-register": "^6.26.0",
|
||||||
|
"chai": "^4.1.2",
|
||||||
"clean-webpack-plugin": "^0.1.19",
|
"clean-webpack-plugin": "^0.1.19",
|
||||||
"copy-webpack-plugin": "^4.5.2",
|
"copy-webpack-plugin": "^4.5.1",
|
||||||
"css-loader": "^0.28.11",
|
"css-loader": "^0.28.11",
|
||||||
|
"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",
|
||||||
"exports-loader": "^0.7.0",
|
"exports-loader": "^0.7.0",
|
||||||
|
"express": "^4.16.3",
|
||||||
"file-loader": "^1.1.11",
|
"file-loader": "^1.1.11",
|
||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^3.0.6",
|
||||||
"husky": "^1.0.0-rc.13",
|
|
||||||
"if-env": "^1.0.4",
|
"if-env": "^1.0.4",
|
||||||
|
"karma": "^2.0.2",
|
||||||
|
"karma-babel-preprocessor": "^7.0.0",
|
||||||
|
"karma-chai": "^0.1.0",
|
||||||
|
"karma-chrome-launcher": "^2.2.0",
|
||||||
|
"karma-mocha": "^1.3.0",
|
||||||
|
"karma-typescript": "^3.0.12",
|
||||||
"loader-utils": "^1.1.0",
|
"loader-utils": "^1.1.0",
|
||||||
"mini-css-extract-plugin": "^0.3.0",
|
"mini-css-extract-plugin": "^0.3.0",
|
||||||
"node-sass": "^4.9.3",
|
"mocha": "^5.2.0",
|
||||||
"optimize-css-assets-webpack-plugin": "^4.0.3",
|
"node-sass": "^4.7.2",
|
||||||
|
"optimize-css-assets-webpack-plugin": "^4.0.0",
|
||||||
|
"prettier": "^1.12.1",
|
||||||
"progress-bar-webpack-plugin": "^1.11.0",
|
"progress-bar-webpack-plugin": "^1.11.0",
|
||||||
|
"puppeteer": "^1.3.0",
|
||||||
"raw-loader": "^0.5.1",
|
"raw-loader": "^0.5.1",
|
||||||
"sass-loader": "^7.1.0",
|
"sass-loader": "^6.0.7",
|
||||||
"script-ext-html-webpack-plugin": "^2.0.1",
|
"script-ext-html-webpack-plugin": "^2.0.1",
|
||||||
"source-map-loader": "^0.2.3",
|
"source-map-loader": "^0.2.3",
|
||||||
"style-loader": "^0.22.1",
|
"style-loader": "^0.20.3",
|
||||||
"ts-loader": "^4.4.2",
|
"ts-loader": "^4.0.1",
|
||||||
"tslint": "^5.11.0",
|
"tslint": "^5.9.1",
|
||||||
"tslint-config-airbnb": "^5.9.2",
|
|
||||||
"tslint-config-semistandard": "^7.0.0",
|
"tslint-config-semistandard": "^7.0.0",
|
||||||
"tslint-react": "^3.6.0",
|
"tslint-react": "^3.5.1",
|
||||||
"typescript": "^2.9.2",
|
"typescript": "^2.7.2",
|
||||||
"typings-for-css-modules-loader": "^1.7.0",
|
"typings-for-css-modules-loader": "^1.7.0",
|
||||||
"webpack": "^4.19.1",
|
"webpack": "^4.3.0",
|
||||||
"webpack-bundle-analyzer": "^2.13.1",
|
"webpack-bundle-analyzer": "^2.11.1",
|
||||||
"webpack-cli": "^2.1.5",
|
"webpack-cli": "^2.0.13",
|
||||||
"webpack-dev-server": "^3.1.5",
|
"webpack-dev-server": "^3.1.1",
|
||||||
"webpack-plugin-replace": "^1.1.1",
|
"webpack-plugin-replace": "^1.1.1"
|
||||||
"classnames": "^2.2.6",
|
},
|
||||||
"comlink": "^3.0.3",
|
"dependencies": {
|
||||||
"linkstate": "^1.1.1",
|
"classnames": "^2.2.5",
|
||||||
"preact": "^8.3.1",
|
"material-components-web": "^0.32.0",
|
||||||
"pretty-bytes": "^5.1.0",
|
"material-radial-progress": "git+https://gist.github.com/02134901c77c5309924bfcf8b4435ebe.git",
|
||||||
"worker-plugin": "^1.1.1"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 6.7 KiB |
BIN
src/assets/icon.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
@@ -1,11 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { mimeType } from './encoder-meta';
|
|
||||||
import { canvasEncode } from '../../lib/util';
|
|
||||||
|
|
||||||
export function encode(data: ImageData) {
|
|
||||||
return canvasEncode(data, mimeType);
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { mimeType } from './encoder-meta';
|
|
||||||
import { canvasEncode } from '../../lib/util';
|
|
||||||
|
|
||||||
export function encode(data: ImageData) {
|
|
||||||
return canvasEncode(data, mimeType);
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { mimeType } from './encoder-meta';
|
|
||||||
import { canvasEncode } from '../../lib/util';
|
|
||||||
|
|
||||||
export function encode(data: ImageData) {
|
|
||||||
return canvasEncode(data, mimeType);
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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 };
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { EncodeOptions, mimeType } from './encoder-meta';
|
|
||||||
import { canvasEncode } from '../../lib/util';
|
|
||||||
|
|
||||||
export function encode(data: ImageData, { quality }: EncodeOptions) {
|
|
||||||
return canvasEncode(data, mimeType, quality);
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import qualityOption from '../generic/quality-option';
|
|
||||||
|
|
||||||
export default qualityOption({ min: 0, max: 1, step: 0 });
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { mimeType } from './encoder-meta';
|
|
||||||
import { canvasEncode } from '../../lib/util';
|
|
||||||
|
|
||||||
export function encode(data: ImageData) {
|
|
||||||
return canvasEncode(data, mimeType);
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
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 = {};
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { mimeType } from './encoder-meta';
|
|
||||||
import { canvasEncode } from '../../lib/util';
|
|
||||||
|
|
||||||
export function encode(data: ImageData) {
|
|
||||||
return canvasEncode(data, mimeType);
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { mimeType } from './encoder-meta';
|
|
||||||
import { canvasEncode } from '../../lib/util';
|
|
||||||
|
|
||||||
export function encode(data: ImageData) {
|
|
||||||
return canvasEncode(data, mimeType);
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { EncodeOptions, mimeType } from './encoder-meta';
|
|
||||||
import { canvasEncode } from '../../lib/util';
|
|
||||||
|
|
||||||
export function encode(data: ImageData, { quality }: EncodeOptions) {
|
|
||||||
return canvasEncode(data, mimeType, quality);
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import qualityOption from '../generic/quality-option';
|
|
||||||
|
|
||||||
export default qualityOption({ min: 0, max: 1, step: 0 });
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
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;
|
|
||||||
});
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export interface EncodeOptions {}
|
|
||||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
|
||||||
|
|
||||||
export const type = 'identity';
|
|
||||||
export const label = 'Original image';
|
|
||||||
export const defaultOptions: EncodeOptions = {};
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
export interface QuantizeOptions {
|
|
||||||
zx: number;
|
|
||||||
maxNumColors: number;
|
|
||||||
dither: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const defaultOptions: QuantizeOptions = {
|
|
||||||
zx: 0,
|
|
||||||
maxNumColors: 256,
|
|
||||||
dither: 1.0,
|
|
||||||
};
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
type BitmapResizeMethods = 'browser-pixelated' | 'browser-low' | 'browser-medium' | 'browser-high';
|
|
||||||
|
|
||||||
export interface ResizeOptions {
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
method: 'vector' | BitmapResizeMethods;
|
|
||||||
fitMethod: 'stretch' | 'cover';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BitmapResizeOptions extends ResizeOptions {
|
|
||||||
method: BitmapResizeMethods;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface VectorResizeOptions extends ResizeOptions {
|
|
||||||
method: 'vector';
|
|
||||||
}
|
|
||||||
|
|
||||||
export const defaultOptions: ResizeOptions = {
|
|
||||||
// Width and height will always default to the image size.
|
|
||||||
// This is set elsewhere.
|
|
||||||
width: 1,
|
|
||||||
height: 1,
|
|
||||||
// This will be set to 'vector' if the input is SVG.
|
|
||||||
method: 'browser-high',
|
|
||||||
fitMethod: 'stretch',
|
|
||||||
};
|
|
||||||