Compare commits
369 Commits
prerenderi
...
v1.3.3
Author | SHA1 | Date | |
---|---|---|---|
e801170496 | |||
91e7c9c5ad | |||
ca5162ed32 | |||
0bf87d0c87 | |||
ce91eb5bae | |||
8d68056bca | |||
d0de8e444a | |||
dfef1f21cc | |||
2440ac4e87 | |||
e90db78697 | |||
5ae15d429c | |||
89d6b46f3e | |||
e086f64779 | |||
9ed3b4f11e | |||
ece3fa12b4 | |||
9a35224535 | |||
ef3faa58bc | |||
b6a8f7eeba | |||
d1203d9c42 | |||
a834b6ae38 | |||
e7982a73ad | |||
717342c80c | |||
075f0e62fd | |||
bcca31fbed | |||
007891fc11 | |||
f8e41952d1 | |||
e4d64f8a79 | |||
1654f69ec1 | |||
cb16fb5437 | |||
36f5fa2c47 | |||
51ad22e72c | |||
1a355c0c16 | |||
fe5ba08963 | |||
7fc994d4af | |||
a0a8285e02 | |||
da2e35f613 | |||
09bdc25352 | |||
ad263a9c36 | |||
c8d8d4e43d | |||
94249b8a93 | |||
edd2c51eb6 | |||
1d24e9399f | |||
3a0062276d | |||
1993cf3f6c | |||
c97aac31c6 | |||
507921cbe8 | |||
887db675c8 | |||
3917618e4e | |||
3c42d2e6a4 | |||
db8777b7f7 | |||
18c2cddee2 | |||
3ff9d3a1fa | |||
6503667c78 | |||
0fa95f84d4 | |||
cf91a90270 | |||
690052f989 | |||
b3e935f7e4 | |||
17314ebd29 | |||
adc437cd51 | |||
0e97b74510 | |||
9ffb475cac | |||
faa2b030c5 | |||
e3b3b10e2a | |||
b569cf268c | |||
b154b77556 | |||
84c0f30a7c | |||
16463ff76d | |||
8314e9e24b | |||
a33c557818 | |||
6fbdc65ad0 | |||
9c9b6c4711 | |||
46278d04c3 | |||
c1c16508b5 | |||
ed1b983711 | |||
ec23e28eda | |||
d48b49e8e4 | |||
14308970c6 | |||
38e86e1012 | |||
e9a33af831 | |||
6a63e5dbb2 | |||
1e1892a3d5 | |||
8bff9a2973 | |||
cbe753dd29 | |||
b047845b43 | |||
1bebc75381 | |||
93c46bfc8d | |||
a3d0f5963e | |||
006b82bf05 | |||
c36e37ac6b | |||
3cf6d7385a | |||
9045b2fa97 | |||
be6f3b9c6d | |||
5a699b7ce9 | |||
f366a78e87 | |||
c63c7ead51 | |||
ecfa5902cd | |||
444027b496 | |||
9c5dcb93c7 | |||
9594221271 | |||
01823d3b75 | |||
db07a90139 | |||
962d0928d3 | |||
e67d50c8e6 | |||
f9b2f17852 | |||
9746a9f5ed | |||
be0877ecb0 | |||
d2fcdfae43 | |||
2c9eb46941 | |||
d30a85fd48 | |||
9260bed1b1 | |||
f6d12985a9 | |||
10c9b1db7c | |||
4fb17be8de | |||
b592b1a088 | |||
0544a6507e | |||
1e20ff15ed | |||
04a0ec0645 | |||
f355292fe3 | |||
32e4d813de | |||
f960f5ea87 | |||
aa6f83e2fa | |||
c09e1f1895 | |||
7c311928dd | |||
5f1c8bcb6b | |||
93bc20f014 | |||
d29d9571c6 | |||
3d47dfc820 | |||
d7846c9add | |||
4d6fe9d641 | |||
205feba75d | |||
ca7663b94a | |||
83e45f054b | |||
783e893a67 | |||
0a941866a9 | |||
04edfe0085 | |||
6cae634eca | |||
8c7bf278dc | |||
f6106650b5 | |||
166e606034 | |||
c997e6a3e4 | |||
2a1b6dc9da | |||
129c33fa12 | |||
3245987113 | |||
593ad62cbb | |||
a625a76e9e | |||
c2a305304b | |||
7389c507fb | |||
68f0f23016 | |||
dc809dde30 | |||
80dfa03b94 | |||
fca7a5350d | |||
1b693fb57a | |||
7723bd3b5f | |||
723fc142ec | |||
06d4d946d9 | |||
428b7d976d | |||
32f2b4e573 | |||
b3ab983f02 | |||
e011724af4 | |||
f11a6cb38a | |||
adf6d3c60d | |||
bb8f35ce09 | |||
ae9ae31ddc | |||
67893817b5 | |||
f8da5b153d | |||
e2a956a088 | |||
5c5b001fc7 | |||
e4beafed97 | |||
553a504140 | |||
44dd2ee808 | |||
b36c851b2a | |||
0502d70cdf | |||
86546574bb | |||
f351712130 | |||
c7f2ae2234 | |||
436f689115 | |||
951c7af724 | |||
53b46f879f | |||
cbe82112ab | |||
7f5562ccfe | |||
76ec946616 | |||
68bb2edb39 | |||
9c85618aff | |||
aebeff8b4c | |||
8d63125b13 | |||
2ca97ef586 | |||
a1a00f0bfb | |||
6870b135b7 | |||
a0f1379feb | |||
9b17322478 | |||
f562bad286 | |||
6994cc3d15 | |||
9b572f9541 | |||
71f893cb44 | |||
6b76ea0a6f | |||
7616d33883 | |||
3c757bb2b2 | |||
a502df80ba | |||
921268ec58 | |||
7d42d4f973 | |||
e4e130c5d6 | |||
bcf7a63118 | |||
66aac12db7 | |||
59cd1f8930 | |||
150e704d20 | |||
b2d47f0fb8 | |||
bd3d33296d | |||
f4c82ced97 | |||
76188df0d3 | |||
9a58e4d339 | |||
f396a5b784 | |||
e572b853e2 | |||
726c2f195a | |||
4599e51b1e | |||
d93169cc5a | |||
bdd3c11f1a | |||
0cec90c7ca | |||
43def798e1 | |||
02b0c022ca | |||
c82d0d1b88 | |||
e24d7865ce | |||
a79f95b305 | |||
49b40b1c3e | |||
11ee74e224 | |||
f335246673 | |||
ccb734aec6 | |||
568b9e9459 | |||
a43ea761f5 | |||
577c77cc30 | |||
d2f60baef9 | |||
64acc08cd7 | |||
a1f0b81dff | |||
48bb58dc89 | |||
765cc213d2 | |||
37f5c0dd76 | |||
b25d1eaf86 | |||
248676aa31 | |||
059c80c05d | |||
cfd42818b7 | |||
5e66e0acc4 | |||
c9fe5ffbcf | |||
1b630a092f | |||
09e60284cb | |||
76b34c62db | |||
9d7212bc1d | |||
1b69c9231d | |||
bcd88f6356 | |||
2a47f67214 | |||
5e8dc1b26c | |||
c591f1f37d | |||
4db43ccd4e | |||
ea5d3c2d78 | |||
700b1f15cd | |||
485ba174e3 | |||
32f6f8b941 | |||
54ad30a7ed | |||
170d75482e | |||
a8db2b30f2 | |||
e3b1b08424 | |||
8006a1a5e7 | |||
1ae65dd4a1 | |||
bff515b63f | |||
65c3ea826f | |||
602d5140f9 | |||
44f0700332 | |||
c90db020b0 | |||
ef4094885e | |||
b52d9d9194 | |||
d3f2836f48 | |||
27722f77f9 | |||
3a0db14c40 | |||
e0dc1b48ec | |||
009327c2c4 | |||
b16d60b52b | |||
c550fe9283 | |||
dce4fc70ac | |||
b3f3ecbf28 | |||
e8c0ddfc7f | |||
a002b376af | |||
2165383da4 | |||
5fbf6b297f | |||
9d5ad83ff8 | |||
07f17dece2 | |||
f2f467ecb8 | |||
2ea9e22b52 | |||
4ee5572d2f | |||
df7e112d22 | |||
13ac3ed5b2 | |||
b7c223bc0d | |||
0f08121596 | |||
b15545402a | |||
b310c97044 | |||
307c6b05ae | |||
77a6d21924 | |||
d22a343378 | |||
790a5b580d | |||
6e8f8bbe41 | |||
cc9d01a9ab | |||
526520c399 | |||
acbc31bc35 | |||
e8e151a926 | |||
835a537c55 | |||
23ea9fad49 | |||
491280935a | |||
900eda9a8e | |||
38d0057833 | |||
3867448aad | |||
807a76d443 | |||
3e26a0a3cc | |||
68729979e3 | |||
a09ec269b8 | |||
3f18c927f1 | |||
9add650b75 | |||
cc3ed168d8 | |||
3b9b1e9f2e | |||
7c220b1a92 | |||
3035a68b90 | |||
65847c0ed7 | |||
5303afe9ad | |||
579b8a494a | |||
56faf619d0 | |||
85e3a12c84 | |||
cab8d3f13c | |||
5c651a1716 | |||
ba0ad81646 | |||
695bbed12b | |||
6a6d478f77 | |||
d75a3aca9b | |||
91945da5ae | |||
00e73daabd | |||
60543dd0a5 | |||
850a019212 | |||
9c0e0b683e | |||
79dfe39978 | |||
96a61eb0b2 | |||
e62fc26dfd | |||
638c57b6fe | |||
7ff18e6ae1 | |||
9d8f885556 | |||
5245c5ca6e | |||
19342208d2 | |||
a9e1c38971 | |||
1533728f59 | |||
d4a616713a | |||
a7598b6602 | |||
e38e7154a6 | |||
7a5c8f5d6b | |||
49db0de05f | |||
8daaea5768 | |||
c2e2a1a0b6 | |||
7edb7f0de8 | |||
634dfe3717 | |||
1b4526ca1e | |||
5e2c4be0c6 | |||
e9eaf227bc | |||
6249ca8ac8 | |||
03a6716745 | |||
ddf8409127 | |||
bcf71f4702 | |||
31db4b9719 | |||
953a0c9124 | |||
444e59c69c | |||
b619427237 | |||
5f7f9e32a8 | |||
1196d4f54f | |||
da53b5fedc | |||
c5e3f9e737 | |||
3b47ee6fe5 | |||
aa02cf2157 |
31
.babelrc
@ -1,31 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"env",
|
||||
{
|
||||
"loose": true,
|
||||
"uglify": false,
|
||||
"modules": false,
|
||||
"targets": {
|
||||
"browsers": "last 2 versions"
|
||||
},
|
||||
"exclude": [
|
||||
"transform-regenerator",
|
||||
"transform-es2015-typeof-symbol"
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": [
|
||||
"transform-decorators-legacy",
|
||||
"transform-class-properties",
|
||||
"transform-react-constant-elements",
|
||||
"transform-react-remove-prop-types",
|
||||
[
|
||||
"transform-react-jsx",
|
||||
{
|
||||
"pragma": "h"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Something is not working as expected
|
||||
labels:
|
||||
|
||||
---
|
||||
|
||||
**Before you start**
|
||||
Please take a look at the [FAQ](https://github.com/GoogleChromeLabs/squoosh/wiki/FAQ) as well as the already opened issues! If nothing fits your problem, go ahead and fill out the following template:
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Version:**
|
||||
- OS w/ version: [e.g. iOS 12]
|
||||
- Browser w/ version [e.g. Chrome 70]
|
||||
- Node version: [e.g. 10.11.0]
|
||||
- npm version: [e.g. 6.4.1]
|
||||
|
||||
**Is your issue related to the quality of image compression?**
|
||||
Please attach original and output images (you can drag & drop to attach).
|
||||
- Original image
|
||||
- Output image from Squoosh
|
||||
|
||||
**Additional context, screenshots, screencasts**
|
||||
Add any other context about the problem here.
|
18
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
labels:
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Does other service/app have this feature?**
|
||||
Add any service you know/use that has this feature (We want to know for research)
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
5
.gitignore
vendored
@ -1,3 +1,6 @@
|
||||
node_modules
|
||||
/build
|
||||
/*.log
|
||||
/*.log
|
||||
*.scss.d.ts
|
||||
*.css.d.ts
|
||||
*.o
|
||||
|
4
.travis.yml
Normal file
@ -0,0 +1,4 @@
|
||||
language: node_js
|
||||
cache: npm
|
||||
script: npm run build
|
||||
after_success: npm run sizereport
|
32
README.md
@ -1,5 +1,31 @@
|
||||
# Squoosh!
|
||||
# [Squoosh]!
|
||||
|
||||
Squoosh will be an image compression web app that allows you to dive into the
|
||||
advanced options provided by various image compressors.
|
||||
[Squoosh] is an image compression web app that allows you to dive into the advanced options provided
|
||||
by various image compressors.
|
||||
|
||||
# Privacy
|
||||
|
||||
Google Analytics is used to record the following:
|
||||
|
||||
* [Basic visit data](https://support.google.com/analytics/answer/6004245?ref_topic=2919631).
|
||||
* Before and after image size once an image is downloaded. These values are rounded to the nearest
|
||||
kilobyte.
|
||||
|
||||
Image compression is handled locally; no additional data is sent to the server.
|
||||
|
||||
# Building locally
|
||||
|
||||
Clone the repo, and:
|
||||
|
||||
```sh
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can run the development server with:
|
||||
|
||||
```sh
|
||||
npm start
|
||||
```
|
||||
|
||||
[Squoosh]: https://squoosh.app
|
||||
|
18
_headers.ejs
Normal file
@ -0,0 +1,18 @@
|
||||
# Long-term cache by default.
|
||||
/*
|
||||
Cache-Control: max-age=31536000
|
||||
|
||||
# And here are the exceptions:
|
||||
/
|
||||
Cache-Control: no-cache
|
||||
|
||||
/serviceworker.js
|
||||
Cache-Control: no-cache
|
||||
|
||||
/manifest.json
|
||||
Cache-Control: must-revalidate, max-age=3600
|
||||
|
||||
# URLs in /assets do not include a hash and are mutable.
|
||||
# But it isn't a big deal if the user gets an old version.
|
||||
/assets/*
|
||||
Cache-Control: must-revalidate, max-age=3600
|
2
_redirects.ejs
Normal file
@ -0,0 +1,2 @@
|
||||
/index.html / 301
|
||||
/* /index.html 301
|
16
codecs/README.md
Normal file
@ -0,0 +1,16 @@
|
||||
# Codecs
|
||||
|
||||
This folder contains a self-contained sub-project for each encoder and decoder that squoosh supplies.
|
||||
|
||||
## Build
|
||||
|
||||
Each subproject can be built using [Docker](https://www.docker.com/) the following commands:
|
||||
|
||||
```
|
||||
$ npm install
|
||||
$ npm run build
|
||||
```
|
||||
|
||||
This will build two files: `<codec name>_<enc or dec>.js` and `<codec name>_<enc or dec>.wasm`. It will most likely be necessary to set [`Module["locateFile"]`](https://kripken.github.io/emscripten-site/docs/api_reference/module.html#affecting-execution) to successfully load the `.wasm` file. When the `.js` file is loaded, a global `<codec name>_<enc or dec>` is created with the same API as an [Emscripten `Module`](https://kripken.github.io/emscripten-site/docs/api_reference/module.html).
|
||||
|
||||
Each codec will document its API in its README.
|
BIN
codecs/example.png
Normal file
After Width: | Height: | Size: 3.3 MiB |
BIN
codecs/example.webp
Normal file
After Width: | Height: | Size: 74 KiB |
BIN
codecs/example_palette.png
Normal file
After Width: | Height: | Size: 239 KiB |
30
codecs/imagequant/README.md
Normal file
@ -0,0 +1,30 @@
|
||||
# ImageQuant
|
||||
|
||||
- Source: <https://github.com/ImageOptim/libimagequant>
|
||||
- Version: v2.12.1
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Docker
|
||||
|
||||
## Example
|
||||
|
||||
See `example.html`
|
||||
|
||||
## API
|
||||
|
||||
### `int version()`
|
||||
|
||||
Returns the version of libimagequant as a number. va.b.c is encoded as 0x0a0b0c
|
||||
|
||||
### `RawImage quantize(std::string buffer, int image_width, int image_height, int numColors, float dithering)`
|
||||
|
||||
Quantizes the given images, using at most `numColors`, a value between 2 and 256. `dithering` is a value between 0 and 1 controlling the amount of dithering. `RawImage` is a class with 3 fields: `buffer`, `width`, and `height`.
|
||||
|
||||
### `RawImage zx_quantize(std::string buffer, int image_width, int image_height, float dithering)`
|
||||
|
||||
???
|
||||
|
||||
### `void free_result()`
|
||||
|
||||
Frees the result created by `quantize()`.
|
48
codecs/imagequant/build.sh
Executable file
@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
export OPTIMIZE="-Os"
|
||||
export LDFLAGS="${OPTIMIZE}"
|
||||
export CFLAGS="${OPTIMIZE}"
|
||||
export CPPFLAGS="${OPTIMIZE}"
|
||||
|
||||
echo "============================================="
|
||||
echo "Compiling libimagequant"
|
||||
echo "============================================="
|
||||
(
|
||||
emcc \
|
||||
--bind \
|
||||
${OPTIMIZE} \
|
||||
-s ALLOW_MEMORY_GROWTH=1 \
|
||||
-s MODULARIZE=1 \
|
||||
-s 'EXPORT_NAME="imagequant"' \
|
||||
-I node_modules/libimagequant \
|
||||
--std=c99 \
|
||||
-c \
|
||||
node_modules/libimagequant/{libimagequant,pam,mediancut,blur,mempool,kmeans,nearest}.c
|
||||
)
|
||||
echo "============================================="
|
||||
echo "Compiling wasm module"
|
||||
echo "============================================="
|
||||
(
|
||||
emcc \
|
||||
--bind \
|
||||
${OPTIMIZE} \
|
||||
-s ALLOW_MEMORY_GROWTH=1 \
|
||||
-s MODULARIZE=1 \
|
||||
-s 'EXPORT_NAME="imagequant"' \
|
||||
-I node_modules/libimagequant \
|
||||
-o ./imagequant.js \
|
||||
--std=c++11 *.o \
|
||||
-x c++ \
|
||||
imagequant.cpp
|
||||
)
|
||||
echo "============================================="
|
||||
echo "Compiling wasm module done"
|
||||
echo "============================================="
|
||||
|
||||
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
||||
echo "Did you update your docker image?"
|
||||
echo "Run \`docker pull trzeci/emscripten\`"
|
||||
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
41
codecs/imagequant/example.html
Normal file
@ -0,0 +1,41 @@
|
||||
<!doctype html>
|
||||
<style>
|
||||
canvas {
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
</style>
|
||||
<script src='imagequant.js'></script>
|
||||
<script>
|
||||
const Module = imagequant();
|
||||
|
||||
async function loadImage(src) {
|
||||
// Load image
|
||||
const img = document.createElement('img');
|
||||
img.src = src;
|
||||
await new Promise(resolve => img.onload = resolve);
|
||||
// Make canvas same size as image
|
||||
const canvas = document.createElement('canvas');
|
||||
[canvas.width, canvas.height] = [img.width, img.height];
|
||||
// Draw image onto canvas
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
return ctx.getImageData(0, 0, img.width, img.height);
|
||||
}
|
||||
|
||||
Module.onRuntimeInitialized = async _ => {
|
||||
console.log('Version:', Module.version().toString(16));
|
||||
const image = await loadImage('../example.png');
|
||||
// const rawImage = Module.quantize(image.data, image.width, image.height, 256, 1.0);
|
||||
const rawImage = Module.zx_quantize(image.data, image.width, image.height, 1.0);
|
||||
console.log('done');
|
||||
Module.free_result();
|
||||
|
||||
const imageData = new ImageData(new Uint8ClampedArray(rawImage.buffer), rawImage.width, rawImage.height);
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
document.body.appendChild(canvas);
|
||||
};
|
||||
</script>
|
245
codecs/imagequant/imagequant.cpp
Normal file
@ -0,0 +1,245 @@
|
||||
#include "emscripten/bind.h"
|
||||
#include "emscripten/val.h"
|
||||
#include <stdlib.h>
|
||||
#include <inttypes.h>
|
||||
#include <limits.h>
|
||||
#include <math.h>
|
||||
|
||||
#include "libimagequant.h"
|
||||
|
||||
using namespace emscripten;
|
||||
|
||||
int version() {
|
||||
return (((LIQ_VERSION/10000) % 100) << 16) |
|
||||
(((LIQ_VERSION/100 ) % 100) << 8) |
|
||||
(((LIQ_VERSION/1 ) % 100) << 0);
|
||||
}
|
||||
|
||||
class RawImage {
|
||||
public:
|
||||
val buffer;
|
||||
int width;
|
||||
int height;
|
||||
|
||||
RawImage(val b, int w, int h)
|
||||
: buffer(b), width(w), height(h) {}
|
||||
};
|
||||
|
||||
|
||||
liq_attr *attr;
|
||||
liq_image *image;
|
||||
liq_result *res;
|
||||
uint8_t* result;
|
||||
RawImage quantize(std::string rawimage, int image_width, int image_height, int num_colors, float dithering) {
|
||||
const uint8_t* image_buffer = (uint8_t*)rawimage.c_str();
|
||||
int size = image_width * image_height;
|
||||
attr = liq_attr_create();
|
||||
image = liq_image_create_rgba(attr, image_buffer, image_width, image_height, 0);
|
||||
liq_set_max_colors(attr, num_colors);
|
||||
liq_image_quantize(image, attr, &res);
|
||||
liq_set_dithering_level(res, dithering);
|
||||
uint8_t* image8bit = (uint8_t*) malloc(size);
|
||||
result = (uint8_t*) malloc(size * 4);
|
||||
liq_write_remapped_image(res, image, image8bit, size);
|
||||
const liq_palette *pal = liq_get_palette(res);
|
||||
// Turn palletted image back into an RGBA image
|
||||
for(int i = 0; i < size; i++) {
|
||||
result[i * 4 + 0] = pal->entries[image8bit[i]].r;
|
||||
result[i * 4 + 1] = pal->entries[image8bit[i]].g;
|
||||
result[i * 4 + 2] = pal->entries[image8bit[i]].b;
|
||||
result[i * 4 + 3] = pal->entries[image8bit[i]].a;
|
||||
}
|
||||
free(image8bit);
|
||||
liq_result_destroy(res);
|
||||
liq_image_destroy(image);
|
||||
liq_attr_destroy(attr);
|
||||
return {
|
||||
val(typed_memory_view(image_width*image_height*4, result)),
|
||||
image_width,
|
||||
image_height
|
||||
};
|
||||
}
|
||||
|
||||
const liq_color zx_colors[] = {
|
||||
{.a = 255, .r = 0, .g = 0, .b = 0}, // regular black
|
||||
{.a = 255, .r = 0, .g = 0, .b = 215}, // regular blue
|
||||
{.a = 255, .r = 215, .g = 0, .b = 0}, // regular red
|
||||
{.a = 255, .r = 215, .g = 0, .b = 215}, // regular magenta
|
||||
{.a = 255, .r = 0, .g = 215, .b = 0}, // regular green
|
||||
{.a = 255, .r = 0, .g = 215, .b = 215}, // regular cyan
|
||||
{.a = 255, .r = 215, .g = 215, .b = 0}, // regular yellow
|
||||
{.a = 255, .r = 215, .g = 215, .b = 215}, // regular white
|
||||
{.a = 255, .r = 0, .g = 0, .b = 255}, // bright blue
|
||||
{.a = 255, .r = 255, .g = 0, .b = 0}, // bright red
|
||||
{.a = 255, .r = 255, .g = 0, .b = 255}, // bright magenta
|
||||
{.a = 255, .r = 0, .g = 255, .b = 0}, // bright green
|
||||
{.a = 255, .r = 0, .g = 255, .b = 255}, // bright cyan
|
||||
{.a = 255, .r = 255, .g = 255, .b = 0}, // bright yellow
|
||||
{.a = 255, .r = 255, .g = 255, .b = 255} // bright white
|
||||
};
|
||||
|
||||
uint8_t block[8 * 8 * 4];
|
||||
|
||||
/**
|
||||
* The ZX has one bit per pixel, but can assign two colours to an 8x8 block. The two colours must
|
||||
* both be 'regular' or 'bright'. Black exists as both regular and bright.
|
||||
*/
|
||||
RawImage zx_quantize(std::string rawimage, int image_width, int image_height, float dithering) {
|
||||
const uint8_t* image_buffer = (uint8_t*) rawimage.c_str();
|
||||
int size = image_width * image_height;
|
||||
int bytes_per_pixel = 4;
|
||||
result = (uint8_t*) malloc(size * bytes_per_pixel);
|
||||
uint8_t* image8bit = (uint8_t*) malloc(8 * 8);
|
||||
|
||||
// For each 8x8 grid
|
||||
for (int block_start_y = 0; block_start_y < image_height; block_start_y += 8) {
|
||||
for (int block_start_x = 0; block_start_x < image_width; block_start_x += 8) {
|
||||
int color_popularity[15] = {0};
|
||||
int block_index = 0;
|
||||
int block_width = 8;
|
||||
int block_height = 8;
|
||||
|
||||
// If the block hangs off the right/bottom of the image dimensions, make it smaller to fit.
|
||||
if (block_start_y + block_height > image_height) {
|
||||
block_height = image_height - block_start_y;
|
||||
}
|
||||
|
||||
if (block_start_x + block_width > image_width) {
|
||||
block_width = image_width - block_start_x;
|
||||
}
|
||||
|
||||
// For each pixel in that block:
|
||||
for (int y = block_start_y; y < block_start_y + block_height; y++) {
|
||||
for (int x = block_start_x; x < block_start_x + block_width; x++) {
|
||||
int pixel_start = (y * image_width * bytes_per_pixel) + (x * bytes_per_pixel);
|
||||
int smallest_distance = INT_MAX;
|
||||
int winning_index = -1;
|
||||
|
||||
// Copy pixel data for quantizing later
|
||||
block[block_index++] = image_buffer[pixel_start];
|
||||
block[block_index++] = image_buffer[pixel_start + 1];
|
||||
block[block_index++] = image_buffer[pixel_start + 2];
|
||||
block[block_index++] = image_buffer[pixel_start + 3];
|
||||
|
||||
// Which zx color is this pixel closest to?
|
||||
for (int color_index = 0; color_index < 15; color_index++) {
|
||||
liq_color color = zx_colors[color_index];
|
||||
|
||||
// Using Euclidean distance. LibQuant has better methods, but it requires conversion to
|
||||
// LAB, so I don't think it's worth it.
|
||||
int distance =
|
||||
pow(color.r - image_buffer[pixel_start + 0], 2) +
|
||||
pow(color.g - image_buffer[pixel_start + 1], 2) +
|
||||
pow(color.b - image_buffer[pixel_start + 2], 2);
|
||||
|
||||
if (distance < smallest_distance) {
|
||||
winning_index = color_index;
|
||||
smallest_distance = distance;
|
||||
}
|
||||
}
|
||||
color_popularity[winning_index]++;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the three most popular colours for the block.
|
||||
int first_color_index = 0;
|
||||
int second_color_index = 0;
|
||||
int third_color_index = 0;
|
||||
int highest_popularity = -1;
|
||||
int second_highest_popularity = -1;
|
||||
int third_highest_popularity = -1;
|
||||
|
||||
for (int color_index = 0; color_index < 15; color_index++) {
|
||||
if (color_popularity[color_index] > highest_popularity) {
|
||||
// Store this as the most popular pixel, and demote the current values:
|
||||
third_color_index = second_color_index;
|
||||
third_highest_popularity = second_highest_popularity;
|
||||
second_color_index = first_color_index;
|
||||
second_highest_popularity = highest_popularity;
|
||||
first_color_index = color_index;
|
||||
highest_popularity = color_popularity[color_index];
|
||||
} else if (color_popularity[color_index] > second_highest_popularity) {
|
||||
third_color_index = second_color_index;
|
||||
third_highest_popularity = second_highest_popularity;
|
||||
second_color_index = color_index;
|
||||
second_highest_popularity = color_popularity[color_index];
|
||||
} else if (color_popularity[color_index] > third_highest_popularity) {
|
||||
third_color_index = color_index;
|
||||
third_highest_popularity = color_popularity[color_index];
|
||||
}
|
||||
}
|
||||
|
||||
// ZX images can't mix bright and regular colours, except black which appears in both.
|
||||
// Resolve any conflict:
|
||||
while (1) {
|
||||
// If either colour is black, there's no conflict to resolve.
|
||||
if (first_color_index != 0 && second_color_index != 0) {
|
||||
if (first_color_index >= 8 && second_color_index < 8) {
|
||||
// Make the second color bright
|
||||
second_color_index = second_color_index + 7;
|
||||
} else if (first_color_index < 8 && second_color_index >= 8) {
|
||||
// Make the second color regular
|
||||
second_color_index = second_color_index - 7;
|
||||
}
|
||||
}
|
||||
|
||||
// If, during conflict resolving, we now have two of the same colour (because we initially
|
||||
// selected the bright & regular version of the same colour), retry again with the third
|
||||
// most popular colour.
|
||||
if (first_color_index == second_color_index) {
|
||||
second_color_index = third_color_index;
|
||||
} else break;
|
||||
}
|
||||
|
||||
// Quantize
|
||||
attr = liq_attr_create();
|
||||
image = liq_image_create_rgba(attr, block, block_width, block_height, 0);
|
||||
liq_set_max_colors(attr, 2);
|
||||
liq_image_add_fixed_color(image, zx_colors[first_color_index]);
|
||||
liq_image_add_fixed_color(image, zx_colors[second_color_index]);
|
||||
liq_image_quantize(image, attr, &res);
|
||||
liq_set_dithering_level(res, dithering);
|
||||
liq_write_remapped_image(res, image, image8bit, size);
|
||||
const liq_palette *pal = liq_get_palette(res);
|
||||
|
||||
// Turn palletted image back into an RGBA image, and write it into the full size result image.
|
||||
for(int y = 0; y < block_height; y++) {
|
||||
for(int x = 0; x < block_width; x++) {
|
||||
int image8BitPos = y * block_width + x;
|
||||
int resultStartPos = ((block_start_y + y) * bytes_per_pixel * image_width) + ((block_start_x + x) * bytes_per_pixel);
|
||||
result[resultStartPos + 0] = pal->entries[image8bit[image8BitPos]].r;
|
||||
result[resultStartPos + 1] = pal->entries[image8bit[image8BitPos]].g;
|
||||
result[resultStartPos + 2] = pal->entries[image8bit[image8BitPos]].b;
|
||||
result[resultStartPos + 3] = pal->entries[image8bit[image8BitPos]].a;
|
||||
}
|
||||
}
|
||||
|
||||
liq_result_destroy(res);
|
||||
liq_image_destroy(image);
|
||||
liq_attr_destroy(attr);
|
||||
}
|
||||
}
|
||||
|
||||
free(image8bit);
|
||||
return {
|
||||
val(typed_memory_view(image_width*image_height*4, result)),
|
||||
image_width,
|
||||
image_height
|
||||
};
|
||||
}
|
||||
|
||||
void free_result() {
|
||||
free(result);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_BINDINGS(my_module) {
|
||||
class_<RawImage>("RawImage")
|
||||
.property("buffer", &RawImage::buffer)
|
||||
.property("width", &RawImage::width)
|
||||
.property("height", &RawImage::height);
|
||||
|
||||
function("quantize", &quantize);
|
||||
function("zx_quantize", &zx_quantize);
|
||||
function("version", &version);
|
||||
function("free_result", &free_result);
|
||||
}
|
15
codecs/imagequant/imagequant.d.ts
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
interface RawImage {
|
||||
buffer: Uint8Array;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface QuantizerModule extends EmscriptenWasm.Module {
|
||||
quantize(data: BufferSource, width: number, height: number, numColors: number, dither: number): RawImage;
|
||||
zx_quantize(data: BufferSource, width: number, height: number, dither: number): RawImage;
|
||||
free_result(): void;
|
||||
}
|
||||
|
||||
export default function(opts: EmscriptenWasm.ModuleOpts): QuantizerModule;
|
||||
|
||||
|
24
codecs/imagequant/imagequant.js
Normal file
BIN
codecs/imagequant/imagequant.wasm
Normal file
1147
codecs/imagequant/package-lock.json
generated
Normal file
13
codecs/imagequant/package.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "imagequant",
|
||||
"scripts": {
|
||||
"install": "napa",
|
||||
"build": "docker run --rm -v $(pwd):/src trzeci/emscripten ./build.sh"
|
||||
},
|
||||
"napa": {
|
||||
"libimagequant": "ImageOptim/libimagequant#2.12.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"napa": "3.0.0"
|
||||
}
|
||||
}
|
47
codecs/mozjpeg_enc/README.md
Normal file
@ -0,0 +1,47 @@
|
||||
# MozJPEG encoder
|
||||
|
||||
- Source: <https://github.com/mozilla/mozjpeg>
|
||||
- Version: v3.3.1
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Docker
|
||||
|
||||
## Example
|
||||
|
||||
See `example.html`
|
||||
|
||||
## API
|
||||
|
||||
### `int version()`
|
||||
|
||||
Returns the version of MozJPEG as a number. va.b.c is encoded as 0x0a0b0c
|
||||
|
||||
### `void free_result()`
|
||||
|
||||
Frees the result created by `encode()`.
|
||||
|
||||
### `Uint8Array encode(std::string image_in, int image_width, int image_height, MozJpegOptions opts)`
|
||||
|
||||
Encodes the given image with given dimension to JPEG. Options looks like this:
|
||||
|
||||
```c++
|
||||
struct MozJpegOptions {
|
||||
int quality;
|
||||
bool baseline;
|
||||
bool arithmetic;
|
||||
bool progressive;
|
||||
bool optimize_coding;
|
||||
int smoothing;
|
||||
int color_space;
|
||||
int quant_table;
|
||||
bool trellis_multipass;
|
||||
bool trellis_opt_zero;
|
||||
bool trellis_opt_table;
|
||||
int trellis_loops;
|
||||
bool auto_subsample;
|
||||
int chroma_subsample;
|
||||
bool separate_chroma_quality;
|
||||
int chroma_quality;
|
||||
};
|
||||
```
|
53
codecs/mozjpeg_enc/build.sh
Executable file
@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
export OPTIMIZE="-Os"
|
||||
export LDFLAGS="${OPTIMIZE}"
|
||||
export CFLAGS="${OPTIMIZE}"
|
||||
export CPPFLAGS="${OPTIMIZE}"
|
||||
|
||||
apt-get update
|
||||
apt-get install -qqy autoconf libtool libpng-dev pkg-config
|
||||
|
||||
echo "============================================="
|
||||
echo "Compiling mozjpeg"
|
||||
echo "============================================="
|
||||
(
|
||||
cd node_modules/mozjpeg
|
||||
autoreconf -fiv
|
||||
emconfigure ./configure --without-simd
|
||||
emmake make libjpeg.la
|
||||
)
|
||||
echo "============================================="
|
||||
echo "Compiling mozjpeg done"
|
||||
echo "============================================="
|
||||
|
||||
echo "============================================="
|
||||
echo "Compiling wasm bindings"
|
||||
echo "============================================="
|
||||
(
|
||||
emcc \
|
||||
--bind \
|
||||
${OPTIMIZE} \
|
||||
-s WASM=1 \
|
||||
-s ALLOW_MEMORY_GROWTH=1 \
|
||||
-s MODULARIZE=1 \
|
||||
-s 'EXPORT_NAME="mozjpeg_enc"' \
|
||||
-I node_modules/mozjpeg \
|
||||
-o ./mozjpeg_enc.js \
|
||||
-Wno-deprecated-register \
|
||||
-Wno-writable-strings \
|
||||
node_modules/mozjpeg/rdswitch.c \
|
||||
-x c++ -std=c++11 \
|
||||
mozjpeg_enc.cpp \
|
||||
node_modules/mozjpeg/.libs/libjpeg.a
|
||||
)
|
||||
echo "============================================="
|
||||
echo "Compiling wasm bindings done"
|
||||
echo "============================================="
|
||||
|
||||
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
||||
echo "Did you update your docker image?"
|
||||
echo "Run \`docker pull trzeci/emscripten\`"
|
||||
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
48
codecs/mozjpeg_enc/example.html
Normal file
@ -0,0 +1,48 @@
|
||||
<!doctype html>
|
||||
<script src='mozjpeg_enc.js'></script>
|
||||
<script>
|
||||
const module = mozjpeg_enc();
|
||||
|
||||
async function loadImage(src) {
|
||||
// Load image
|
||||
const img = document.createElement('img');
|
||||
img.src = src;
|
||||
await new Promise(resolve => img.onload = resolve);
|
||||
// Make canvas same size as image
|
||||
const canvas = document.createElement('canvas');
|
||||
[canvas.width, canvas.height] = [img.width, img.height];
|
||||
// Draw image onto canvas
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
return ctx.getImageData(0, 0, img.width, img.height);
|
||||
}
|
||||
|
||||
module.onRuntimeInitialized = async _ => {
|
||||
console.log('Version:', module.version().toString(16));
|
||||
const image = await loadImage('../example.png');
|
||||
const result = module.encode(image.data, image.width, image.height, {
|
||||
quality: 75,
|
||||
baseline: false,
|
||||
arithmetic: false,
|
||||
progressive: true,
|
||||
optimize_coding: true,
|
||||
smoothing: 0,
|
||||
color_space: 3,
|
||||
quant_table: 3,
|
||||
trellis_multipass: false,
|
||||
trellis_opt_zero: false,
|
||||
trellis_opt_table: false,
|
||||
trellis_loops: 1,
|
||||
auto_subsample: true,
|
||||
chroma_subsample: 2,
|
||||
separate_chroma_quality: false,
|
||||
chroma_quality: 75,
|
||||
});
|
||||
|
||||
const blob = new Blob([result], {type: 'image/jpeg'});
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
const img = document.createElement('img');
|
||||
img.src = blobURL;
|
||||
document.body.appendChild(img);
|
||||
};
|
||||
</script>
|
234
codecs/mozjpeg_enc/mozjpeg_enc.cpp
Normal file
@ -0,0 +1,234 @@
|
||||
#include <emscripten/bind.h>
|
||||
#include <emscripten/val.h>
|
||||
#include <stdlib.h>
|
||||
#include <inttypes.h>
|
||||
#include <stdio.h>
|
||||
#include <setjmp.h>
|
||||
#include <string.h>
|
||||
#include "config.h"
|
||||
#include "jpeglib.h"
|
||||
#include "cdjpeg.h"
|
||||
|
||||
using namespace emscripten;
|
||||
|
||||
// MozJPEG doesn’t expose a numeric version, so I have to do some fun C macro hackery to turn it
|
||||
// into a string. More details here: https://gcc.gnu.org/onlinedocs/cpp/Stringizing.html
|
||||
#define xstr(s) str(s)
|
||||
#define str(s) #s
|
||||
|
||||
struct MozJpegOptions {
|
||||
int quality;
|
||||
bool baseline;
|
||||
bool arithmetic;
|
||||
bool progressive;
|
||||
bool optimize_coding;
|
||||
int smoothing;
|
||||
int color_space;
|
||||
int quant_table;
|
||||
bool trellis_multipass;
|
||||
bool trellis_opt_zero;
|
||||
bool trellis_opt_table;
|
||||
int trellis_loops;
|
||||
bool auto_subsample;
|
||||
int chroma_subsample;
|
||||
bool separate_chroma_quality;
|
||||
int chroma_quality;
|
||||
};
|
||||
|
||||
int version() {
|
||||
char buffer[] = xstr(MOZJPEG_VERSION);
|
||||
int version = 0;
|
||||
int last_index = 0;
|
||||
for(int i = 0; i < strlen(buffer); i++) {
|
||||
if(buffer[i] == '.') {
|
||||
buffer[i] = '\0';
|
||||
version = version << 8 | atoi(&buffer[last_index]);
|
||||
buffer[i] = '.';
|
||||
last_index = i + 1;
|
||||
}
|
||||
}
|
||||
version = version << 8 | atoi(&buffer[last_index]);
|
||||
return version;
|
||||
}
|
||||
|
||||
uint8_t* last_result;
|
||||
struct jpeg_compress_struct cinfo;
|
||||
|
||||
val encode(std::string image_in, int image_width, int image_height, MozJpegOptions opts) {
|
||||
uint8_t* image_buffer = (uint8_t*) image_in.c_str();
|
||||
|
||||
// The code below is basically the `write_JPEG_file` function from
|
||||
// https://github.com/mozilla/mozjpeg/blob/master/example.c
|
||||
// I just write to memory instead of a file.
|
||||
|
||||
|
||||
/* This struct contains the JPEG compression parameters and pointers to
|
||||
* working space (which is allocated as needed by the JPEG library).
|
||||
* It is possible to have several such structures, representing multiple
|
||||
* compression/decompression processes, in existence at once. We refer
|
||||
* to any one struct (and its associated working data) as a "JPEG object".
|
||||
*/
|
||||
/* This struct represents a JPEG error handler. It is declared separately
|
||||
* because applications often want to supply a specialized error handler
|
||||
* (see the second half of this file for an example). But here we just
|
||||
* take the easy way out and use the standard error handler, which will
|
||||
* print a message on stderr and call exit() if compression fails.
|
||||
* Note that this struct must live as long as the main JPEG parameter
|
||||
* struct, to avoid dangling-pointer problems.
|
||||
*/
|
||||
struct jpeg_error_mgr jerr;
|
||||
/* More stuff */
|
||||
JSAMPROW row_pointer[1]; /* pointer to JSAMPLE row[s] */
|
||||
int row_stride; /* physical row width in image buffer */
|
||||
uint8_t* output;
|
||||
unsigned long size;
|
||||
|
||||
/* Step 1: allocate and initialize JPEG compression object */
|
||||
|
||||
/* We have to set up the error handler first, in case the initialization
|
||||
* step fails. (Unlikely, but it could happen if you are out of memory.)
|
||||
* This routine fills in the contents of struct jerr, and returns jerr's
|
||||
* address which we place into the link field in cinfo.
|
||||
*/
|
||||
cinfo.err = jpeg_std_error(&jerr);
|
||||
/* Now we can initialize the JPEG compression object. */
|
||||
jpeg_create_compress(&cinfo);
|
||||
|
||||
/* Step 2: specify data destination (eg, a file) */
|
||||
/* Note: steps 2 and 3 can be done in either order. */
|
||||
|
||||
/* Here we use the library-supplied code to send compressed data to a
|
||||
* stdio stream. You can also write your own code to do something else.
|
||||
* VERY IMPORTANT: use "b" option to fopen() if you are on a machine that
|
||||
* requires it in order to write binary files.
|
||||
*/
|
||||
// if ((outfile = fopen(filename, "wb")) == NULL) {
|
||||
// fprintf(stderr, "can't open %s\n", filename);
|
||||
// exit(1);
|
||||
// }
|
||||
jpeg_mem_dest(&cinfo, &output, &size);
|
||||
|
||||
/* Step 3: set parameters for compression */
|
||||
|
||||
/* First we supply a description of the input image.
|
||||
* Four fields of the cinfo struct must be filled in:
|
||||
*/
|
||||
cinfo.image_width = image_width; /* image width and height, in pixels */
|
||||
cinfo.image_height = image_height;
|
||||
cinfo.input_components = 4; /* # of color components per pixel */
|
||||
cinfo.in_color_space = JCS_EXT_RGBA; /* colorspace of input image */
|
||||
/* Now use the library's routine to set default compression parameters.
|
||||
* (You must set at least cinfo.in_color_space before calling this,
|
||||
* since the defaults depend on the source color space.)
|
||||
*/
|
||||
jpeg_set_defaults(&cinfo);
|
||||
|
||||
jpeg_set_colorspace(&cinfo, (J_COLOR_SPACE) opts.color_space);
|
||||
|
||||
if (opts.quant_table != -1) {
|
||||
jpeg_c_set_int_param(&cinfo, JINT_BASE_QUANT_TBL_IDX, opts.quant_table);
|
||||
}
|
||||
|
||||
cinfo.optimize_coding = opts.optimize_coding;
|
||||
|
||||
if (opts.arithmetic) {
|
||||
cinfo.arith_code = TRUE;
|
||||
cinfo.optimize_coding = FALSE;
|
||||
}
|
||||
|
||||
cinfo.smoothing_factor = opts.smoothing;
|
||||
|
||||
jpeg_c_set_bool_param(&cinfo, JBOOLEAN_USE_SCANS_IN_TRELLIS, opts.trellis_multipass);
|
||||
jpeg_c_set_bool_param(&cinfo, JBOOLEAN_TRELLIS_EOB_OPT, opts.trellis_opt_zero);
|
||||
jpeg_c_set_bool_param(&cinfo, JBOOLEAN_TRELLIS_Q_OPT, opts.trellis_opt_table);
|
||||
jpeg_c_set_int_param(&cinfo, JINT_TRELLIS_NUM_LOOPS, opts.trellis_loops);
|
||||
|
||||
// A little hacky to build a string for this, but it means we can use set_quality_ratings which
|
||||
// does some useful heuristic stuff.
|
||||
std::string quality_str = std::to_string(opts.quality);
|
||||
|
||||
if (opts.separate_chroma_quality && opts.color_space == JCS_YCbCr) {
|
||||
quality_str += "," + std::to_string(opts.chroma_quality);
|
||||
}
|
||||
|
||||
char const *pqual = quality_str.c_str();
|
||||
|
||||
set_quality_ratings(&cinfo, (char*) pqual, opts.baseline);
|
||||
|
||||
if (!opts.auto_subsample && opts.color_space == JCS_YCbCr) {
|
||||
cinfo.comp_info[0].h_samp_factor = opts.chroma_subsample;
|
||||
cinfo.comp_info[0].v_samp_factor = opts.chroma_subsample;
|
||||
}
|
||||
|
||||
if (!opts.baseline && opts.progressive) {
|
||||
jpeg_simple_progression(&cinfo);
|
||||
} else {
|
||||
cinfo.num_scans = 0;
|
||||
cinfo.scan_info = NULL;
|
||||
}
|
||||
/* Step 4: Start compressor */
|
||||
|
||||
/* TRUE ensures that we will write a complete interchange-JPEG file.
|
||||
* Pass TRUE unless you are very sure of what you're doing.
|
||||
*/
|
||||
jpeg_start_compress(&cinfo, TRUE);
|
||||
|
||||
/* Step 5: while (scan lines remain to be written) */
|
||||
/* jpeg_write_scanlines(...); */
|
||||
|
||||
/* Here we use the library's state variable cinfo.next_scanline as the
|
||||
* loop counter, so that we don't have to keep track ourselves.
|
||||
* To keep things simple, we pass one scanline per call; you can pass
|
||||
* more if you wish, though.
|
||||
*/
|
||||
row_stride = image_width * 4; /* JSAMPLEs per row in image_buffer */
|
||||
|
||||
while (cinfo.next_scanline < cinfo.image_height) {
|
||||
/* jpeg_write_scanlines expects an array of pointers to scanlines.
|
||||
* Here the array is only one element long, but you could pass
|
||||
* more than one scanline at a time if that's more convenient.
|
||||
*/
|
||||
row_pointer[0] = & image_buffer[cinfo.next_scanline * row_stride];
|
||||
(void) jpeg_write_scanlines(&cinfo, row_pointer, 1);
|
||||
}
|
||||
|
||||
/* Step 6: Finish compression */
|
||||
|
||||
jpeg_finish_compress(&cinfo);
|
||||
/* Step 7: release JPEG compression object */
|
||||
|
||||
last_result = output;
|
||||
|
||||
/* And we're done! */
|
||||
return val(typed_memory_view(size, output));
|
||||
}
|
||||
|
||||
void free_result() {
|
||||
/* This is an important step since it will release a good deal of memory. */
|
||||
jpeg_destroy_compress(&cinfo);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_BINDINGS(my_module) {
|
||||
value_object<MozJpegOptions>("MozJpegOptions")
|
||||
.field("quality", &MozJpegOptions::quality)
|
||||
.field("baseline", &MozJpegOptions::baseline)
|
||||
.field("arithmetic", &MozJpegOptions::arithmetic)
|
||||
.field("progressive", &MozJpegOptions::progressive)
|
||||
.field("optimize_coding", &MozJpegOptions::optimize_coding)
|
||||
.field("smoothing", &MozJpegOptions::smoothing)
|
||||
.field("color_space", &MozJpegOptions::color_space)
|
||||
.field("quant_table", &MozJpegOptions::quant_table)
|
||||
.field("trellis_multipass", &MozJpegOptions::trellis_multipass)
|
||||
.field("trellis_opt_zero", &MozJpegOptions::trellis_opt_zero)
|
||||
.field("trellis_opt_table", &MozJpegOptions::trellis_opt_table)
|
||||
.field("trellis_loops", &MozJpegOptions::trellis_loops)
|
||||
.field("chroma_subsample", &MozJpegOptions::chroma_subsample)
|
||||
.field("auto_subsample", &MozJpegOptions::auto_subsample)
|
||||
.field("separate_chroma_quality", &MozJpegOptions::separate_chroma_quality)
|
||||
.field("chroma_quality", &MozJpegOptions::chroma_quality)
|
||||
;
|
||||
|
||||
function("version", &version);
|
||||
function("encode", &encode);
|
||||
function("free_result", &free_result);
|
||||
}
|
8
codecs/mozjpeg_enc/mozjpeg_enc.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
import { EncodeOptions } from '../../src/codecs/mozjpeg/encoder-meta';
|
||||
|
||||
interface MozJPEGModule extends EmscriptenWasm.Module {
|
||||
encode(data: BufferSource, width: number, height: number, options: EncodeOptions): Uint8Array;
|
||||
free_result(): void;
|
||||
}
|
||||
|
||||
export default function(opts: EmscriptenWasm.ModuleOpts): MozJPEGModule;
|
24
codecs/mozjpeg_enc/mozjpeg_enc.js
Normal file
BIN
codecs/mozjpeg_enc/mozjpeg_enc.wasm
Normal file
1147
codecs/mozjpeg_enc/package-lock.json
generated
Normal file
13
codecs/mozjpeg_enc/package.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "mozjpeg_enc",
|
||||
"scripts": {
|
||||
"install": "napa",
|
||||
"build": "docker run --rm -v $(pwd):/src trzeci/emscripten ./build.sh"
|
||||
},
|
||||
"napa": {
|
||||
"mozjpeg": "mozilla/mozjpeg#v3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"napa": "3.0.0"
|
||||
}
|
||||
}
|
2
codecs/optipng/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
build/
|
||||
*.o
|
26
codecs/optipng/README.md
Normal file
@ -0,0 +1,26 @@
|
||||
# OptiPNG
|
||||
|
||||
- Source: <http://optipng.sourceforge.net/>
|
||||
- Version: v0.7.7
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Docker
|
||||
|
||||
## Example
|
||||
|
||||
See `example.html`
|
||||
|
||||
## API
|
||||
|
||||
### `int version()`
|
||||
|
||||
Returns the version of optipng as a number. va.b.c is encoded as 0x0a0b0c
|
||||
|
||||
### `ArrayBuffer compress(std::string buffer, {level})`;
|
||||
|
||||
`compress` will re-compress the given PNG image via `buffer`. `level` is a number between 0 and 7.
|
||||
|
||||
### `void free_result()`
|
||||
|
||||
Frees the result created by `compress()`.
|
87
codecs/optipng/build.sh
Executable file
@ -0,0 +1,87 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
export OPTIMIZE="-Os"
|
||||
export PREFIX="/src/build"
|
||||
export CFLAGS="${OPTIMIZE} -I${PREFIX}/include/"
|
||||
export CPPFLAGS="${OPTIMIZE} -I${PREFIX}/include/"
|
||||
export LDFLAGS="${OPTIMIZE} -L${PREFIX}/lib/"
|
||||
|
||||
apt-get update
|
||||
apt-get install -qqy autoconf libtool
|
||||
|
||||
echo "============================================="
|
||||
echo "Compiling zlib"
|
||||
echo "============================================="
|
||||
test -n "$SKIP_ZLIB" || (
|
||||
cd node_modules/zlib
|
||||
emconfigure ./configure --prefix=${PREFIX}/
|
||||
emmake make
|
||||
emmake make install
|
||||
)
|
||||
echo "============================================="
|
||||
echo "Compiling zlib done"
|
||||
echo "============================================="
|
||||
|
||||
echo "============================================="
|
||||
echo "Compiling libpng"
|
||||
echo "============================================="
|
||||
test -n "$SKIP_LIBPNG" || (
|
||||
cd node_modules/libpng
|
||||
autoreconf -i
|
||||
emconfigure ./configure --with-zlib-prefix=${PREFIX}/ --prefix=${PREFIX}/
|
||||
emmake make
|
||||
emmake make install
|
||||
)
|
||||
echo "============================================="
|
||||
echo "Compiling libpng done"
|
||||
echo "============================================="
|
||||
|
||||
echo "============================================="
|
||||
echo "Compiling optipng"
|
||||
echo "============================================="
|
||||
(
|
||||
emcc \
|
||||
${OPTIMIZE} \
|
||||
-Wno-implicit-function-declaration \
|
||||
-I ${PREFIX}/include \
|
||||
-I node_modules/optipng/src/opngreduc \
|
||||
-I node_modules/optipng/src/pngxtern \
|
||||
-I node_modules/optipng/src/cexcept \
|
||||
-I node_modules/optipng/src/gifread \
|
||||
-I node_modules/optipng/src/pnmio \
|
||||
-I node_modules/optipng/src/minitiff \
|
||||
--std=c99 -c \
|
||||
node_modules/optipng/src/opngreduc/*.c \
|
||||
node_modules/optipng/src/pngxtern/*.c \
|
||||
node_modules/optipng/src/gifread/*.c \
|
||||
node_modules/optipng/src/minitiff/*.c \
|
||||
node_modules/optipng/src/pnmio/*.c \
|
||||
node_modules/optipng/src/optipng/*.c
|
||||
|
||||
emcc \
|
||||
--bind \
|
||||
${OPTIMIZE} \
|
||||
-s ALLOW_MEMORY_GROWTH=1 -s MODULARIZE=1 -s 'EXPORT_NAME="optipng"' \
|
||||
-I ${PREFIX}/include \
|
||||
-I node_modules/optipng/src/opngreduc \
|
||||
-I node_modules/optipng/src/pngxtern \
|
||||
-I node_modules/optipng/src/cexcept \
|
||||
-I node_modules/optipng/src/gifread \
|
||||
-I node_modules/optipng/src/pnmio \
|
||||
-I node_modules/optipng/src/minitiff \
|
||||
-o "optipng.js" \
|
||||
--std=c++11 \
|
||||
optipng.cpp \
|
||||
*.o \
|
||||
${PREFIX}/lib/libz.so ${PREFIX}/lib/libpng.a
|
||||
)
|
||||
echo "============================================="
|
||||
echo "Compiling optipng done"
|
||||
echo "============================================="
|
||||
|
||||
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
||||
echo "Did you update your docker image?"
|
||||
echo "Run \`docker pull trzeci/emscripten\`"
|
||||
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
19
codecs/optipng/example.html
Normal file
@ -0,0 +1,19 @@
|
||||
<!doctype html>
|
||||
<script src='optipng.js'></script>
|
||||
<script>
|
||||
const Module = optipng();
|
||||
|
||||
Module.onRuntimeInitialized = async _ => {
|
||||
console.log('Version:', Module.version().toString(16));
|
||||
const image = await fetch('../example_palette.png').then(r => r.arrayBuffer());
|
||||
const newImage = Module.compress(image, {level: 3});
|
||||
console.log('done');
|
||||
Module.free_result();
|
||||
|
||||
console.log(`Old size: ${image.byteLength}, new size: ${newImage.byteLength} (${newImage.byteLength/image.byteLength*100}%)`);
|
||||
const blobURL = URL.createObjectURL(new Blob([newImage], {type: 'image/png'}));
|
||||
const img = document.createElement('img');
|
||||
img.src = blobURL;
|
||||
document.body.appendChild(img);
|
||||
};
|
||||
</script>
|
53
codecs/optipng/optipng.cpp
Normal file
@ -0,0 +1,53 @@
|
||||
#include "emscripten/bind.h"
|
||||
#include "emscripten/val.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
using namespace emscripten;
|
||||
|
||||
extern "C" int main(int argc, char *argv[]);
|
||||
|
||||
int version() {
|
||||
// FIXME (@surma): Haven’t found a version in optipng :(
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct OptiPngOpts {
|
||||
int level;
|
||||
};
|
||||
|
||||
uint8_t* result;
|
||||
val compress(std::string png, OptiPngOpts opts) {
|
||||
remove("input.png");
|
||||
remove("output.png");
|
||||
FILE* infile = fopen("input.png", "wb");
|
||||
fwrite(png.c_str(), png.length(), 1, infile);
|
||||
fflush(infile);
|
||||
fclose(infile);
|
||||
|
||||
char optlevel[8];
|
||||
sprintf(&optlevel[0], "-o%d", opts.level);
|
||||
char* args[] = {"optipng", optlevel, "-out", "output.png", "input.png"};
|
||||
main(5, args);
|
||||
|
||||
FILE *outfile = fopen("output.png", "rb");
|
||||
fseek(outfile, 0, SEEK_END);
|
||||
int fsize = ftell(outfile);
|
||||
result = (uint8_t*) malloc(fsize);
|
||||
fseek(outfile, 0, SEEK_SET);
|
||||
fread(result, fsize, 1, outfile);
|
||||
return val(typed_memory_view(fsize, result));
|
||||
}
|
||||
|
||||
void free_result() {
|
||||
free(result);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_BINDINGS(my_module) {
|
||||
value_object<OptiPngOpts>("OptiPngOpts")
|
||||
.field("level", &OptiPngOpts::level);
|
||||
|
||||
function("version", &version);
|
||||
function("compress", &compress);
|
||||
function("free_result", &free_result);
|
||||
}
|
10
codecs/optipng/optipng.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
import {EncodeOptions} from "src/codecs/optipng/encoder";
|
||||
|
||||
export interface OptiPngModule extends EmscriptenWasm.Module {
|
||||
compress(data: BufferSource, opts: EncodeOptions): Uint8Array;
|
||||
free_result(): void;
|
||||
}
|
||||
|
||||
export default function(opts: EmscriptenWasm.ModuleOpts): OptiPngModule;
|
||||
|
||||
|
24
codecs/optipng/optipng.js
Normal file
BIN
codecs/optipng/optipng.wasm
Normal file
1457
codecs/optipng/package-lock.json
generated
Normal file
22
codecs/optipng/package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "optipng",
|
||||
"scripts": {
|
||||
"install": "tar-dependency install && napa",
|
||||
"build": "npm run build:wasm",
|
||||
"build:wasm": "docker run --rm -v $(pwd):/src -e SKIP_ZLIB=\"${SKIP_ZLIB}\" -e SKIP_LIBPNG=\"${SKIP_LIBPNG}\" trzeci/emscripten ./build.sh"
|
||||
},
|
||||
"tarDependencies": {
|
||||
"node_modules/optipng": {
|
||||
"url": "https://netcologne.dl.sourceforge.net/project/optipng/OptiPNG/optipng-0.7.7/optipng-0.7.7.tar.gz",
|
||||
"strip": 1
|
||||
}
|
||||
},
|
||||
"napa": {
|
||||
"libpng": "emscripten-ports/libpng",
|
||||
"zlib": "emscripten-ports/zlib"
|
||||
},
|
||||
"dependencies": {
|
||||
"napa": "3.0.0",
|
||||
"tar-dependency": "0.0.3"
|
||||
}
|
||||
}
|
2
codecs/rotate/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
target
|
||||
Cargo.lock
|
14
codecs/rotate/Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "rotate"
|
||||
version = "0.1.0"
|
||||
authors = ["Surma <surma@google.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
name = "rotate"
|
||||
path = "rotate.rs"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
opt-level = "s"
|
17
codecs/rotate/Dockerfile
Normal file
@ -0,0 +1,17 @@
|
||||
FROM ubuntu
|
||||
RUN apt-get update && \
|
||||
apt-get install -qqy git build-essential cmake python2.7
|
||||
RUN git clone --recursive https://github.com/WebAssembly/wabt /usr/src/wabt
|
||||
RUN mkdir -p /usr/src/wabt/build
|
||||
WORKDIR /usr/src/wabt/build
|
||||
RUN cmake .. -DCMAKE_INSTALL_PREFIX=/opt/wabt && \
|
||||
make && \
|
||||
make install
|
||||
|
||||
FROM rust
|
||||
RUN rustup install nightly && \
|
||||
rustup target add --toolchain nightly wasm32-unknown-unknown
|
||||
|
||||
COPY --from=0 /opt/wabt /opt/wabt
|
||||
ENV PATH="/opt/wabt/bin:${PATH}"
|
||||
WORKDIR /src
|
25
codecs/rotate/build.sh
Executable file
@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "============================================="
|
||||
echo "Compiling wasm"
|
||||
echo "============================================="
|
||||
(
|
||||
rustup run nightly \
|
||||
cargo build \
|
||||
--target wasm32-unknown-unknown \
|
||||
--release
|
||||
cp target/wasm32-unknown-unknown/release/rotate.wasm .
|
||||
wasm-strip rotate.wasm
|
||||
)
|
||||
echo "============================================="
|
||||
echo "Compiling wasm done"
|
||||
echo "============================================="
|
||||
|
||||
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
||||
echo "Did you update your docker image?"
|
||||
echo "Run \`docker pull ubuntu\`"
|
||||
echo "Run \`docker pull rust\`"
|
||||
echo "Run \`docker build -t squoosh-rotate .\`"
|
||||
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
7
codecs/rotate/package.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "rotate",
|
||||
"scripts": {
|
||||
"build:image": "docker build -t squoosh-rotate .",
|
||||
"build": "docker run --rm -v $(pwd):/src squoosh-rotate ./build.sh"
|
||||
}
|
||||
}
|
90
codecs/rotate/rotate.rs
Normal file
@ -0,0 +1,90 @@
|
||||
use std::slice::from_raw_parts_mut;
|
||||
|
||||
// This function is taken from
|
||||
// https://rustwasm.github.io/book/reference/code-size.html
|
||||
#[cfg(not(debug_assertions))]
|
||||
#[inline]
|
||||
pub fn unwrap_abort<T>(o: Option<T>) -> T {
|
||||
use std::process;
|
||||
match o {
|
||||
Some(t) => t,
|
||||
None => process::abort(),
|
||||
}
|
||||
}
|
||||
|
||||
// Normal panic-y behavior for debug builds
|
||||
#[cfg(debug_assertions)]
|
||||
unsafe fn unchecked_unwrap<T>(o: Option<T>) -> T {
|
||||
o.unwrap()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
fn rotate(input_width: isize, input_height: isize, rotate: isize) {
|
||||
let mut i = 0isize;
|
||||
|
||||
// In the straight-copy case, d1 is x, d2 is y.
|
||||
// x starts at 0 and increases.
|
||||
// y starts at 0 and increases.
|
||||
let mut d1_start: isize = 0;
|
||||
let mut d1_limit: isize = input_width;
|
||||
let mut d1_advance: isize = 1;
|
||||
let mut d1_multiplier: isize = 1;
|
||||
let mut d2_start: isize = 0;
|
||||
let mut d2_limit: isize = input_height;
|
||||
let mut d2_advance: isize = 1;
|
||||
let mut d2_multiplier: isize = input_width;
|
||||
|
||||
if rotate == 90 {
|
||||
// d1 is y, d2 is x.
|
||||
// y starts at its max value and decreases.
|
||||
// x starts at 0 and increases.
|
||||
d1_start = input_height - 1;
|
||||
d1_limit = input_height;
|
||||
d1_advance = -1;
|
||||
d1_multiplier = input_width;
|
||||
d2_start = 0;
|
||||
d2_limit = input_width;
|
||||
d2_advance = 1;
|
||||
d2_multiplier = 1;
|
||||
} else if rotate == 180 {
|
||||
// d1 is x, d2 is y.
|
||||
// x starts at its max and decreases.
|
||||
// y starts at its max and decreases.
|
||||
d1_start = input_width - 1;
|
||||
d1_limit = input_width;
|
||||
d1_advance = -1;
|
||||
d1_multiplier = 1;
|
||||
d2_start = input_height - 1;
|
||||
d2_limit = input_height;
|
||||
d2_advance = -1;
|
||||
d2_multiplier = input_width;
|
||||
} else if rotate == 270 {
|
||||
// d1 is y, d2 is x.
|
||||
// y starts at 0 and increases.
|
||||
// x starts at its max and decreases.
|
||||
d1_start = 0;
|
||||
d1_limit = input_height;
|
||||
d1_advance = 1;
|
||||
d1_multiplier = input_width;
|
||||
d2_start = input_width - 1;
|
||||
d2_limit = input_width;
|
||||
d2_advance = -1;
|
||||
d2_multiplier = 1;
|
||||
}
|
||||
|
||||
let num_pixels = (input_width * input_height) as usize;
|
||||
let in_b: &mut [u32];
|
||||
let out_b: &mut [u32];
|
||||
unsafe {
|
||||
in_b = from_raw_parts_mut::<u32>(4 as *mut u32, num_pixels);
|
||||
out_b = from_raw_parts_mut::<u32>((input_width * input_height * 4 + 4) as *mut u32, num_pixels);
|
||||
}
|
||||
|
||||
for d2 in 0..d2_limit {
|
||||
for d1 in 0..d1_limit {
|
||||
let in_idx = (d1_start + d1 * d1_advance) * d1_multiplier + (d2_start + d2 * d2_advance) * d2_multiplier;
|
||||
*unwrap_abort(out_b.get_mut(i as usize)) = *unwrap_abort(in_b.get(in_idx as usize));
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
BIN
codecs/rotate/rotate.wasm
Executable file
22
codecs/webp_dec/README.md
Normal file
@ -0,0 +1,22 @@
|
||||
# WebP decoder
|
||||
|
||||
- Source: <https://github.com/webmproject/libwebp>
|
||||
- Version: v1.0.2
|
||||
|
||||
## 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()`.
|
60
codecs/webp_dec/build.sh
Executable file
@ -0,0 +1,60 @@
|
||||
#!/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 libwebp"
|
||||
echo "============================================="
|
||||
test -n "$SKIP_LIBWEBP" || (
|
||||
cd node_modules/libwebp
|
||||
autoreconf -fiv
|
||||
rm -rf build || true
|
||||
mkdir -p build && cd build
|
||||
emconfigure ../configure \
|
||||
--disable-libwebpdemux \
|
||||
--disable-wic \
|
||||
--disable-gif \
|
||||
--disable-tiff \
|
||||
--disable-jpeg \
|
||||
--disable-png \
|
||||
--disable-sdl \
|
||||
--disable-gl \
|
||||
--disable-threading \
|
||||
--disable-neon-rtcd \
|
||||
--disable-neon \
|
||||
--disable-sse2 \
|
||||
--disable-sse4.1
|
||||
emmake make
|
||||
)
|
||||
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 \
|
||||
-x c++ \
|
||||
webp_dec.cpp \
|
||||
node_modules/libwebp/build/src/.libs/libwebp.a
|
||||
)
|
||||
echo "============================================="
|
||||
echo "Compiling wasm bindings done"
|
||||
echo "============================================="
|
||||
|
||||
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
||||
echo "Did you update your docker image?"
|
||||
echo "Run \`docker pull trzeci/emscripten\`"
|
||||
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
24
codecs/webp_dec/example.html
Normal file
@ -0,0 +1,24 @@
|
||||
<!doctype html>
|
||||
<script src='webp_dec.js'></script>
|
||||
<script>
|
||||
const Module = webp_dec();
|
||||
|
||||
async function loadFile(src) {
|
||||
const resp = await fetch(src);
|
||||
return await resp.arrayBuffer();
|
||||
}
|
||||
|
||||
Module.onRuntimeInitialized = async _ => {
|
||||
console.log('Version:', Module.version().toString(16));
|
||||
const image = await loadFile('../example.webp');
|
||||
const result = Module.decode(image);
|
||||
const imageData = new ImageData(new Uint8ClampedArray(result.buffer), result.width, result.height);
|
||||
Module.free_result();
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = result.width;
|
||||
canvas.height = result.height;
|
||||
document.body.appendChild(canvas);
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
};
|
||||
</script>
|
1147
codecs/webp_dec/package-lock.json
generated
Normal file
13
codecs/webp_dec/package.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "webp_dec",
|
||||
"scripts": {
|
||||
"install": "napa",
|
||||
"build": "docker run --rm -v $(pwd):/src trzeci/emscripten ./build.sh"
|
||||
},
|
||||
"napa": {
|
||||
"libwebp": "webmproject/libwebp#v1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"napa": "3.0.0"
|
||||
}
|
||||
}
|
47
codecs/webp_dec/webp_dec.cpp
Normal file
@ -0,0 +1,47 @@
|
||||
#include "emscripten/bind.h"
|
||||
#include "emscripten/val.h"
|
||||
#include "src/webp/decode.h"
|
||||
#include "src/webp/demux.h"
|
||||
#include <string>
|
||||
|
||||
using namespace emscripten;
|
||||
|
||||
int version() {
|
||||
return WebPGetDecoderVersion();
|
||||
}
|
||||
|
||||
class RawImage {
|
||||
public:
|
||||
val buffer;
|
||||
int width;
|
||||
int height;
|
||||
|
||||
RawImage(val b, int w, int h)
|
||||
: buffer(b), width(w), height(h) {}
|
||||
};
|
||||
|
||||
uint8_t* last_result;
|
||||
RawImage decode(std::string buffer) {
|
||||
int width, height;
|
||||
last_result = WebPDecodeRGBA((const uint8_t*)buffer.c_str(), buffer.size(), &width, &height);
|
||||
return RawImage(
|
||||
val(typed_memory_view(width*height*4, last_result)),
|
||||
width,
|
||||
height
|
||||
);
|
||||
}
|
||||
|
||||
void free_result() {
|
||||
free(last_result);
|
||||
}
|
||||
|
||||
EMSCRIPTEN_BINDINGS(my_module) {
|
||||
class_<RawImage>("RawImage")
|
||||
.property("buffer", &RawImage::buffer)
|
||||
.property("width", &RawImage::width)
|
||||
.property("height", &RawImage::height);
|
||||
|
||||
function("decode", &decode);
|
||||
function("version", &version);
|
||||
function("free_result", &free_result);
|
||||
}
|
13
codecs/webp_dec/webp_dec.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
interface RawImage {
|
||||
buffer: Uint8Array;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
interface WebPModule extends EmscriptenWasm.Module {
|
||||
decode(data: BufferSource): RawImage;
|
||||
free_result(): void;
|
||||
}
|
||||
|
||||
export default function(opts: EmscriptenWasm.ModuleOpts): WebPModule;
|
||||
|
24
codecs/webp_dec/webp_dec.js
Normal file
BIN
codecs/webp_dec/webp_dec.wasm
Normal file
26
codecs/webp_enc/README.md
Normal file
@ -0,0 +1,26 @@
|
||||
# WebP encoder
|
||||
|
||||
- Source: <https://github.com/webmproject/libwebp>
|
||||
- Version: v1.0.2
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Docker
|
||||
|
||||
## Example
|
||||
|
||||
See `example.html`
|
||||
|
||||
## API
|
||||
|
||||
### `int version()`
|
||||
|
||||
Returns the version of libwebp as a number. va.b.c is encoded as 0x0a0b0c
|
||||
|
||||
### `UInt8Array encode(uint8_t* image_buffer, int image_width, int image_height, WebPConfig config)`
|
||||
|
||||
Encodes the given image with given dimension to WebP.
|
||||
|
||||
### `void free_result()`
|
||||
|
||||
Frees the last result created by `encode()`.
|
61
codecs/webp_enc/build.sh
Executable file
@ -0,0 +1,61 @@
|
||||
#!/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 libwebp"
|
||||
echo "============================================="
|
||||
test -n "$SKIP_LIBWEBP" || (
|
||||
cd node_modules/libwebp
|
||||
autoreconf -fiv
|
||||
rm -rf build || true
|
||||
mkdir -p build && cd build
|
||||
emconfigure ../configure \
|
||||
--disable-libwebpdemux \
|
||||
--disable-wic \
|
||||
--disable-gif \
|
||||
--disable-tiff \
|
||||
--disable-jpeg \
|
||||
--disable-png \
|
||||
--disable-sdl \
|
||||
--disable-gl \
|
||||
--disable-threading \
|
||||
--disable-neon-rtcd \
|
||||
--disable-neon \
|
||||
--disable-sse2 \
|
||||
--disable-sse4.1
|
||||
emmake make
|
||||
)
|
||||
echo "============================================="
|
||||
echo "Compiling wasm bindings"
|
||||
echo "============================================="
|
||||
(
|
||||
emcc \
|
||||
${OPTIMIZE} \
|
||||
--bind \
|
||||
-s ALLOW_MEMORY_GROWTH=1 \
|
||||
-s MODULARIZE=1 \
|
||||
-s 'EXPORT_NAME="webp_enc"' \
|
||||
--std=c++11 \
|
||||
-I node_modules/libwebp \
|
||||
-o ./webp_enc.js \
|
||||
-x c++ \
|
||||
webp_enc.cpp \
|
||||
node_modules/libwebp/build/src/.libs/libwebp.a
|
||||
)
|
||||
echo "============================================="
|
||||
echo "Compiling wasm bindings done"
|
||||
echo "============================================="
|
||||
|
||||
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
||||
echo "Did you update your docker image?"
|
||||
echo "Run \`docker pull trzeci/emscripten\`"
|
||||
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
|
62
codecs/webp_enc/example.html
Normal file
@ -0,0 +1,62 @@
|
||||
<!doctype html>
|
||||
<script src='webp_enc.js'></script>
|
||||
<script>
|
||||
const module = webp_enc();
|
||||
|
||||
async function loadImage(src) {
|
||||
// Load image
|
||||
const img = document.createElement('img');
|
||||
img.src = src;
|
||||
await new Promise(resolve => img.onload = resolve);
|
||||
// Make canvas same size as image
|
||||
const canvas = document.createElement('canvas');
|
||||
[canvas.width, canvas.height] = [img.width, img.height];
|
||||
// Draw image onto canvas
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
return ctx.getImageData(0, 0, img.width, img.height);
|
||||
}
|
||||
|
||||
module.onRuntimeInitialized = async _ => {
|
||||
console.log('Version:', module.version().toString(16));
|
||||
const image = await loadImage('../example.png');
|
||||
const result = module.encode(image.data, image.width, image.height, {
|
||||
quality: 75,
|
||||
target_size: 0,
|
||||
target_PSNR: 0,
|
||||
method: 4,
|
||||
sns_strength: 50,
|
||||
filter_strength: 60,
|
||||
filter_sharpness: 0,
|
||||
filter_type: 1,
|
||||
partitions: 0,
|
||||
segments: 4,
|
||||
pass: 1,
|
||||
show_compressed: 0,
|
||||
preprocessing: 0,
|
||||
autofilter: 0,
|
||||
partition_limit: 0,
|
||||
alpha_compression: 1,
|
||||
alpha_filtering: 1,
|
||||
alpha_quality: 100,
|
||||
lossless: 0,
|
||||
exact: 0,
|
||||
image_hint: 0,
|
||||
emulate_jpeg_size: 0,
|
||||
thread_level: 0,
|
||||
low_memory: 0,
|
||||
near_lossless: 100,
|
||||
use_delta_palette: 0,
|
||||
use_sharp_yuv: 0,
|
||||
});
|
||||
console.log('size', result.length);
|
||||
const blob = new Blob([result], {type: 'image/webp'});
|
||||
|
||||
module.free_result();
|
||||
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
const img = document.createElement('img');
|
||||
img.src = blobURL;
|
||||
document.body.appendChild(img);
|
||||
};
|
||||
</script>
|
1147
codecs/webp_enc/package-lock.json
generated
Normal file
13
codecs/webp_enc/package.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "webp_enc",
|
||||
"scripts": {
|
||||
"install": "napa",
|
||||
"build": "docker run --rm -v $(pwd):/src trzeci/emscripten ./build.sh"
|
||||
},
|
||||
"napa": {
|
||||
"libwebp": "webmproject/libwebp#v1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"napa": "3.0.0"
|
||||
}
|
||||
}
|
96
codecs/webp_enc/webp_enc.cpp
Normal file
@ -0,0 +1,96 @@
|
||||
#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");
|
||||
}
|
||||
|
||||
// Only use use_argb if we really need it, as it's slower.
|
||||
pic.use_argb = config.lossless || config.use_sharp_yuv || config.preprocessing > 0;
|
||||
pic.width = width;
|
||||
pic.height = height;
|
||||
pic.writer = WebPMemoryWrite;
|
||||
pic.custom_ptr = &wrt;
|
||||
|
||||
WebPMemoryWriterInit(&wrt);
|
||||
|
||||
ok = WebPPictureImportRGBA(&pic, (uint8_t*) img_in, width * 4) && WebPEncode(&config, &pic);
|
||||
WebPPictureFree(&pic);
|
||||
if (!ok) {
|
||||
WebPMemoryWriterClear(&wrt);
|
||||
throw std::runtime_error("Encode failed");
|
||||
}
|
||||
|
||||
last_result = wrt.mem;
|
||||
|
||||
return val(typed_memory_view(wrt.size, wrt.mem));
|
||||
}
|
||||
|
||||
void free_result() {
|
||||
WebPFree(last_result);
|
||||
}
|
||||
|
||||
|
||||
EMSCRIPTEN_BINDINGS(my_module) {
|
||||
enum_<WebPImageHint>("WebPImageHint")
|
||||
.value("WEBP_HINT_DEFAULT", WebPImageHint::WEBP_HINT_DEFAULT)
|
||||
.value("WEBP_HINT_PICTURE", WebPImageHint::WEBP_HINT_PICTURE)
|
||||
.value("WEBP_HINT_PHOTO", WebPImageHint::WEBP_HINT_PHOTO)
|
||||
.value("WEBP_HINT_GRAPH", WebPImageHint::WEBP_HINT_GRAPH)
|
||||
;
|
||||
|
||||
value_object<WebPConfig>("WebPConfig")
|
||||
.field("lossless", &WebPConfig::lossless)
|
||||
.field("quality", &WebPConfig::quality)
|
||||
.field("method", &WebPConfig::method)
|
||||
.field("image_hint", &WebPConfig::image_hint)
|
||||
.field("target_size", &WebPConfig::target_size)
|
||||
.field("target_PSNR", &WebPConfig::target_PSNR)
|
||||
.field("segments", &WebPConfig::segments)
|
||||
.field("sns_strength", &WebPConfig::sns_strength)
|
||||
.field("filter_strength", &WebPConfig::filter_strength)
|
||||
.field("filter_sharpness", &WebPConfig::filter_sharpness)
|
||||
.field("filter_type", &WebPConfig::filter_type)
|
||||
.field("autofilter", &WebPConfig::autofilter)
|
||||
.field("alpha_compression", &WebPConfig::alpha_compression)
|
||||
.field("alpha_filtering", &WebPConfig::alpha_filtering)
|
||||
.field("alpha_quality", &WebPConfig::alpha_quality)
|
||||
.field("pass", &WebPConfig::pass)
|
||||
.field("show_compressed", &WebPConfig::show_compressed)
|
||||
.field("preprocessing", &WebPConfig::preprocessing)
|
||||
.field("partitions", &WebPConfig::partitions)
|
||||
.field("partition_limit", &WebPConfig::partition_limit)
|
||||
.field("emulate_jpeg_size", &WebPConfig::emulate_jpeg_size)
|
||||
.field("thread_level", &WebPConfig::thread_level)
|
||||
.field("low_memory", &WebPConfig::low_memory)
|
||||
.field("near_lossless", &WebPConfig::near_lossless)
|
||||
.field("exact", &WebPConfig::exact)
|
||||
.field("use_delta_palette", &WebPConfig::use_delta_palette)
|
||||
.field("use_sharp_yuv", &WebPConfig::use_sharp_yuv)
|
||||
;
|
||||
|
||||
function("version", &version);
|
||||
function("encode", &encode);
|
||||
function("free_result", &free_result);
|
||||
}
|
9
codecs/webp_enc/webp_enc.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
import { EncodeOptions } from '../../src/codecs/webp/encoder-meta';
|
||||
|
||||
interface WebPModule extends EmscriptenWasm.Module {
|
||||
encode(data: BufferSource, width: number, height: number, options: EncodeOptions): Uint8Array;
|
||||
free_result(): void;
|
||||
}
|
||||
|
||||
|
||||
export default function(opts: EmscriptenWasm.ModuleOpts): WebPModule;
|
24
codecs/webp_enc/webp_enc.js
Normal file
BIN
codecs/webp_enc/webp_enc.wasm
Normal file
74
config/add-css-types.js
Normal file
@ -0,0 +1,74 @@
|
||||
const DtsCreator = require('typed-css-modules');
|
||||
const chokidar = require('chokidar');
|
||||
const util = require('util');
|
||||
const sass = require('node-sass');
|
||||
|
||||
const sassRender = util.promisify(sass.render);
|
||||
|
||||
async function sassToCss(path) {
|
||||
const result = await sassRender({ file: path });
|
||||
return result.css;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} Opts
|
||||
* @property {boolean} watch Watch for changes
|
||||
*/
|
||||
/**
|
||||
* Create typing files for CSS & SCSS.
|
||||
*
|
||||
* @param {string[]} rootPaths Paths to search within
|
||||
* @param {Opts} [opts={}] Options.
|
||||
*/
|
||||
function addCssTypes(rootPaths, opts = {}) {
|
||||
return new Promise((resolve) => {
|
||||
const { watch = false } = opts;
|
||||
|
||||
const paths = [];
|
||||
const preReadyPromises = [];
|
||||
let ready = false;
|
||||
|
||||
for (const rootPath of rootPaths) {
|
||||
// Look for scss & css in each path.
|
||||
paths.push(rootPath + '/**/*.scss');
|
||||
paths.push(rootPath + '/**/*.css');
|
||||
}
|
||||
|
||||
// For simplicity, the watcher is used even if we're not watching.
|
||||
// If we're not watching, we stop the watcher after the initial files are found.
|
||||
const watcher = chokidar.watch(paths, {
|
||||
// Avoid processing already-processed files.
|
||||
ignored: '*.d.*',
|
||||
// Without this, travis and netlify builds never complete. I'm not sure why, but it might be
|
||||
// related to https://github.com/paulmillr/chokidar/pull/758
|
||||
persistent: watch,
|
||||
});
|
||||
|
||||
function change(path) {
|
||||
const promise = (async function() {
|
||||
const creator = new DtsCreator({ camelCase: true });
|
||||
const result = path.endsWith('.scss') ?
|
||||
await creator.create(path, await sassToCss(path)) :
|
||||
await creator.create(path);
|
||||
|
||||
await result.writeFile();
|
||||
})();
|
||||
|
||||
if (!ready) preReadyPromises.push(promise);
|
||||
}
|
||||
|
||||
watcher.on('change', change);
|
||||
watcher.on('add', change);
|
||||
|
||||
// 'ready' is when events have been fired for file discovery.
|
||||
watcher.on('ready', () => {
|
||||
ready = true;
|
||||
// Wait for the current set of processing to finish.
|
||||
Promise.all(preReadyPromises).then(resolve);
|
||||
// And if we're not watching, close the watcher.
|
||||
if (!watch) watcher.close();
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = addCssTypes;
|
47
config/asset-template-plugin.js
Normal file
@ -0,0 +1,47 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const ejs = require('ejs');
|
||||
const AssetsPlugin = require('assets-webpack-plugin');
|
||||
|
||||
module.exports = class AssetTemplatePlugin extends AssetsPlugin {
|
||||
constructor(options) {
|
||||
options = options || {};
|
||||
if (!options.template) throw Error('AssetTemplatePlugin: template option is required.');
|
||||
super({
|
||||
useCompilerPath: true,
|
||||
filename: options.filename,
|
||||
processOutput: files => this._processOutput(files)
|
||||
});
|
||||
this._template = path.resolve(process.cwd(), options.template);
|
||||
const ignore = options.ignore || /(manifest\.json|\.DS_Store)$/;
|
||||
this._ignore = typeof ignore === 'function' ? ({ test: ignore }) : ignore;
|
||||
}
|
||||
|
||||
_processOutput(files) {
|
||||
const mapping = {
|
||||
all: [],
|
||||
byType: {},
|
||||
entries: {}
|
||||
};
|
||||
for (const entryName in files) {
|
||||
// non-entry-point-derived assets are collected under an empty string key
|
||||
// since that's a bit awkward, we'll call them "assets"
|
||||
const name = entryName === '' ? 'assets' : entryName;
|
||||
const listing = files[entryName];
|
||||
const entry = mapping.entries[name] = {
|
||||
all: [],
|
||||
byType: {}
|
||||
};
|
||||
for (let type in listing) {
|
||||
const list = [].concat(listing[type]).filter(file => !this._ignore.test(file));
|
||||
if (!list.length) continue;
|
||||
mapping.all = mapping.all.concat(list);
|
||||
mapping.byType[type] = (mapping.byType[type] || []).concat(list);
|
||||
entry.all = entry.all.concat(list);
|
||||
entry.byType[type] = (entry.byType[type] || []).concat(list);
|
||||
}
|
||||
}
|
||||
mapping.files = mapping.all;
|
||||
return ejs.render(fs.readFileSync(this._template, 'utf8'), mapping);
|
||||
}
|
||||
};
|
158
config/auto-sw-plugin.js
Normal file
@ -0,0 +1,158 @@
|
||||
const util = require('util');
|
||||
const minimatch = require('minimatch');
|
||||
const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
|
||||
const WebWorkerTemplatePlugin = require('webpack/lib/webworker/WebWorkerTemplatePlugin');
|
||||
const ParserHelpers = require('webpack/lib/ParserHelpers');
|
||||
|
||||
const NAME = 'auto-sw-plugin';
|
||||
const JS_TYPES = ['auto', 'esm', 'dynamic'];
|
||||
|
||||
/**
|
||||
* Automatically finds and bundles Service Workers by looking for navigator.serviceWorker.register(..).
|
||||
* An Array of webpack assets is injected into the Service Worker bundle as a `BUILD_ASSETS` global.
|
||||
* Hidden and `.map` files are excluded by default, and this can be customized using the include & exclude options.
|
||||
* @example
|
||||
* // webpack config
|
||||
* plugins: [
|
||||
* new AutoSWPlugin({
|
||||
* exclude: [
|
||||
* '**\/.*', // don't expose hidden files (default)
|
||||
* '**\/*.map', // don't precache sourcemaps (default)
|
||||
* 'index.html' // don't cache the page itself
|
||||
* ]
|
||||
* })
|
||||
* ]
|
||||
* @param {Object} [options={}]
|
||||
* @param {string[]} [options.exclude] Minimatch pattern(s) of which assets to omit from BUILD_ASSETS.
|
||||
* @param {string[]} [options.include] Minimatch pattern(s) of assets to allow in BUILD_ASSETS.
|
||||
*/
|
||||
module.exports = class AutoSWPlugin {
|
||||
constructor(options) {
|
||||
this.options = Object.assign({
|
||||
exclude: [
|
||||
'**/*.map',
|
||||
'**/.*'
|
||||
]
|
||||
}, options || {});
|
||||
}
|
||||
|
||||
apply(compiler) {
|
||||
const serviceWorkers = [];
|
||||
|
||||
compiler.hooks.emit.tapPromise(NAME, compilation => this.emit(compiler, compilation, serviceWorkers));
|
||||
|
||||
compiler.hooks.normalModuleFactory.tap(NAME, (factory) => {
|
||||
for (const type of JS_TYPES) {
|
||||
factory.hooks.parser.for(`javascript/${type}`).tap(NAME, parser => {
|
||||
let counter = 0;
|
||||
|
||||
const processRegisterCall = expr => {
|
||||
const dep = parser.evaluateExpression(expr.arguments[0]);
|
||||
|
||||
if (!dep.isString()) {
|
||||
parser.state.module.warnings.push({
|
||||
message: 'navigator.serviceWorker.register() will only be bundled if passed a String literal.'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const filename = dep.string;
|
||||
const outputFilename = this.options.filename || 'serviceworker.js'
|
||||
const context = parser.state.current.context;
|
||||
serviceWorkers.push({
|
||||
outputFilename,
|
||||
filename,
|
||||
context
|
||||
});
|
||||
|
||||
const id = `__webpack__serviceworker__${++counter}`;
|
||||
ParserHelpers.toConstantDependency(parser, id)(expr.arguments[0]);
|
||||
return ParserHelpers.addParsedVariableToModule(parser, id, '__webpack_public_path__ + ' + JSON.stringify(outputFilename));
|
||||
};
|
||||
|
||||
parser.hooks.call.for('navigator.serviceWorker.register').tap(NAME, processRegisterCall);
|
||||
parser.hooks.call.for('self.navigator.serviceWorker.register').tap(NAME, processRegisterCall);
|
||||
parser.hooks.call.for('window.navigator.serviceWorker.register').tap(NAME, processRegisterCall);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createFilter(list) {
|
||||
const filters = [].concat(list);
|
||||
for (let i=0; i<filters.length; i++) {
|
||||
if (typeof filters[i] === 'string') {
|
||||
filters[i] = minimatch.filter(filters[i]);
|
||||
}
|
||||
}
|
||||
return filters;
|
||||
}
|
||||
|
||||
async emit(compiler, compilation, serviceWorkers) {
|
||||
let assetMapping = Object.keys(compilation.assets);
|
||||
if (this.options.include) {
|
||||
const filters = this.createFilter(this.options.include);
|
||||
assetMapping = assetMapping.filter(filename => {
|
||||
for (const filter of filters) {
|
||||
if (filter(filename)) return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
if (this.options.exclude) {
|
||||
const filters = this.createFilter(this.options.exclude);
|
||||
assetMapping = assetMapping.filter(filename => {
|
||||
for (const filter of filters) {
|
||||
if (filter(filename)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
await Promise.all(serviceWorkers.map(
|
||||
(serviceWorker, index) => this.compileServiceWorker(compiler, compilation, serviceWorker, index, assetMapping)
|
||||
));
|
||||
}
|
||||
|
||||
async compileServiceWorker(compiler, compilation, options, index, assetMapping) {
|
||||
const entryFilename = options.filename;
|
||||
|
||||
const chunkFilename = compiler.options.output.chunkFilename.replace(/\.([a-z]+)$/i, '.serviceworker.$1');
|
||||
const workerOptions = {
|
||||
filename: options.outputFilename, // chunkFilename.replace(/\.?\[(?:chunkhash|contenthash|hash)(:\d+(?::\d+)?)?\]/g, ''),
|
||||
chunkFilename: this.options.chunkFilename || chunkFilename,
|
||||
globalObject: 'self'
|
||||
};
|
||||
|
||||
const childCompiler = compilation.createChildCompiler(NAME, { filename: workerOptions.filename });
|
||||
(new WebWorkerTemplatePlugin(workerOptions)).apply(childCompiler);
|
||||
|
||||
/* The duplication DefinePlugin ends up causing is problematic (it doesn't hoist injections), so we'll do it manually. */
|
||||
// (new DefinePlugin({
|
||||
// BUILD_ASSETS: JSON.stringify(assetMapping)
|
||||
// })).apply(childCompiler);
|
||||
(new SingleEntryPlugin(options.context, entryFilename, workerOptions.filename)).apply(childCompiler);
|
||||
|
||||
const subCache = `subcache ${__dirname} ${entryFilename} ${index}`;
|
||||
let childCompilation;
|
||||
childCompiler.hooks.compilation.tap(NAME, c => {
|
||||
childCompilation = c;
|
||||
if (childCompilation.cache) {
|
||||
if (!childCompilation.cache[subCache]) childCompilation.cache[subCache] = {};
|
||||
childCompilation.cache = childCompilation.cache[subCache];
|
||||
}
|
||||
});
|
||||
|
||||
await (util.promisify(childCompiler.runAsChild.bind(childCompiler)))();
|
||||
|
||||
const versionVar = this.options.version ?
|
||||
`var VERSION = ${JSON.stringify(this.options.version)};` : '';
|
||||
const original = childCompilation.assets[workerOptions.filename].source();
|
||||
const source = `${versionVar}var BUILD_ASSETS=${JSON.stringify(assetMapping)};${original}`;
|
||||
childCompilation.assets[workerOptions.filename] = {
|
||||
source: () => source,
|
||||
size: () => Buffer.byteLength(source, 'utf8')
|
||||
};
|
||||
|
||||
Object.assign(compilation.assets, childCompilation.assets);
|
||||
}
|
||||
};
|
204
config/size-report.js
Normal file
@ -0,0 +1,204 @@
|
||||
const path = require('path');
|
||||
const { URL } = require('url');
|
||||
|
||||
const gzipSize = require('gzip-size');
|
||||
const fetch = require('node-fetch');
|
||||
const prettyBytes = require('pretty-bytes');
|
||||
const escapeRE = require('escape-string-regexp');
|
||||
const readdirp = require('readdirp');
|
||||
const chalk = new require('chalk').constructor({ level: 4 });
|
||||
|
||||
function fetchTravis(path, options = {}) {
|
||||
const url = new URL(path, 'https://api.travis-ci.org');
|
||||
url.search = new URLSearchParams(options);
|
||||
|
||||
return fetch(url, {
|
||||
headers: { 'Travis-API-Version': '3' },
|
||||
});
|
||||
}
|
||||
|
||||
function fetchTravisBuildInfo(user, repo, branch) {
|
||||
return fetchTravis(`/repo/${encodeURIComponent(`${user}/${repo}`)}/builds`, {
|
||||
'branch.name': branch,
|
||||
state: 'passed',
|
||||
limit: 1,
|
||||
}).then(r => r.json());
|
||||
}
|
||||
|
||||
function fetchTravisText(path) {
|
||||
return fetchTravis(path).then(r => r.text());
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively-read a directory and turn it into an array of { name, size, gzipSize }
|
||||
*/
|
||||
async function dirToInfoArray(startPath, {
|
||||
namePrefix = '',
|
||||
} = {}) {
|
||||
const results = await new Promise((resolve, reject) => {
|
||||
readdirp({ root: startPath }, (err, results) => {
|
||||
if (err) reject(err); else resolve(results);
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(
|
||||
results.files.map(async (entry) => ({
|
||||
name: entry.path,
|
||||
gzipSize: await gzipSize.file(entry.fullPath),
|
||||
size: entry.stat.size,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to treat two entries with different file name hashes as the same file.
|
||||
*/
|
||||
function findHashedMatch(name, buildInfo) {
|
||||
const nameParts = /^(.+\.)[a-f0-9]+(\..+)$/.exec(name);
|
||||
if (!nameParts) return;
|
||||
|
||||
const matchRe = new RegExp(`^${escapeRE(nameParts[1])}[a-f0-9]+${escapeRE(nameParts[2])}$`);
|
||||
const matchingEntry = buildInfo.find(entry => matchRe.test(entry.name));
|
||||
return matchingEntry;
|
||||
}
|
||||
|
||||
const buildSizePrefix = '=== BUILD SIZES: ';
|
||||
const buildSizePrefixRe = new RegExp(`^${escapeRE(buildSizePrefix)}(.+)$`, 'm');
|
||||
|
||||
async function getPreviousBuildInfo() {
|
||||
const buildData = await fetchTravisBuildInfo('GoogleChromeLabs', 'squoosh', 'master');
|
||||
const jobUrl = buildData.builds[0].jobs[0]['@href'];
|
||||
const log = await fetchTravisText(jobUrl + '/log.txt');
|
||||
const reResult = buildSizePrefixRe.exec(log);
|
||||
|
||||
if (!reResult) return;
|
||||
return JSON.parse(reResult[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an array that represents the difference between builds.
|
||||
* Returns an array of { beforeName, afterName, beforeSize, afterSize }.
|
||||
* Sizes are gzipped size.
|
||||
* Before/after properties are missing if resource isn't in the previous/new build.
|
||||
*/
|
||||
function getChanges(previousBuildInfo, buildInfo) {
|
||||
const buildChanges = [];
|
||||
const alsoInPreviousBuild = new Set();
|
||||
|
||||
for (const oldEntry of previousBuildInfo) {
|
||||
const newEntry = buildInfo.find(entry => entry.name === oldEntry.name) ||
|
||||
findHashedMatch(oldEntry.name, buildInfo);
|
||||
|
||||
// Entry is in previous build, but not the new build.
|
||||
if (!newEntry) {
|
||||
buildChanges.push({
|
||||
beforeName: oldEntry.name,
|
||||
beforeSize: oldEntry.gzipSize,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Mark this entry so we know we've dealt with it.
|
||||
alsoInPreviousBuild.add(newEntry);
|
||||
|
||||
// If they're the same, just ignore.
|
||||
// Using size rather than gzip size. I've seen different platforms produce different zipped
|
||||
// sizes.
|
||||
if (
|
||||
oldEntry.size === newEntry.size &&
|
||||
oldEntry.name === newEntry.name
|
||||
) continue;
|
||||
|
||||
// Entry is in both builds (maybe renamed).
|
||||
buildChanges.push({
|
||||
beforeName: oldEntry.name,
|
||||
afterName: newEntry.name,
|
||||
beforeSize: oldEntry.gzipSize,
|
||||
afterSize: newEntry.gzipSize,
|
||||
});
|
||||
}
|
||||
|
||||
// Look for entries that are only in the new build.
|
||||
for (const newEntry of buildInfo) {
|
||||
if (alsoInPreviousBuild.has(newEntry)) continue;
|
||||
|
||||
buildChanges.push({
|
||||
afterName: newEntry.name,
|
||||
afterSize: newEntry.gzipSize,
|
||||
});
|
||||
}
|
||||
|
||||
return buildChanges;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Output the current build sizes for later retrieval.
|
||||
const buildInfo = await dirToInfoArray(__dirname + '/../build');
|
||||
console.log(buildSizePrefix + JSON.stringify(buildInfo));
|
||||
console.log('\nBuild change report:');
|
||||
|
||||
let previousBuildInfo;
|
||||
|
||||
try {
|
||||
previousBuildInfo = await getPreviousBuildInfo();
|
||||
} catch (err) {
|
||||
console.log(` Couldn't parse previous build info`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!previousBuildInfo) {
|
||||
console.log(` Couldn't find previous build info`);
|
||||
return;
|
||||
}
|
||||
|
||||
const buildChanges = getChanges(previousBuildInfo, buildInfo);
|
||||
|
||||
if (buildChanges.length === 0) {
|
||||
console.log(' No changes');
|
||||
return;
|
||||
}
|
||||
|
||||
// One letter references, so it's easier to get the spacing right.
|
||||
const y = chalk.yellow;
|
||||
const g = chalk.green;
|
||||
const r = chalk.red;
|
||||
|
||||
for (const change of buildChanges) {
|
||||
// New file.
|
||||
if (!change.beforeSize) {
|
||||
console.log(` ${g('ADDED')} ${change.afterName} - ${prettyBytes(change.afterSize)}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Removed file.
|
||||
if (!change.afterSize) {
|
||||
console.log(` ${r('REMOVED')} ${change.beforeName} - was ${prettyBytes(change.beforeSize)}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Changed file.
|
||||
let size;
|
||||
|
||||
if (change.beforeSize === change.afterSize) {
|
||||
// Just renamed.
|
||||
size = `${prettyBytes(change.afterSize)} -> no change`;
|
||||
} else {
|
||||
const color = change.afterSize > change.beforeSize ? r : g;
|
||||
const sizeDiff = prettyBytes(change.afterSize - change.beforeSize, { signed: true });
|
||||
const relativeDiff = Math.round((change.afterSize / change.beforeSize) * 1000) / 1000;
|
||||
|
||||
size = `${prettyBytes(change.beforeSize)} -> ${prettyBytes(change.afterSize)}` +
|
||||
' (' +
|
||||
color(`${sizeDiff}, ${relativeDiff}x`) +
|
||||
')';
|
||||
}
|
||||
|
||||
console.log(` ${y('CHANGED')} ${change.afterName} - ${size}`);
|
||||
|
||||
if (change.beforeName !== change.afterName) {
|
||||
console.log(` Renamed from: ${change.beforeName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
107
emscripten-wasm.d.ts
vendored
Normal file
@ -0,0 +1,107 @@
|
||||
// These types roughly model the object that the JS files generated by Emscripten define. Copied from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/emscripten/index.d.ts and turned into a type definition rather than a global to support our way of using Emscripten.
|
||||
// TODO(@surma): Upstream this?
|
||||
declare namespace EmscriptenWasm {
|
||||
type EnvironmentType = "WEB" | "NODE" | "SHELL" | "WORKER";
|
||||
|
||||
// Options object for modularized Emscripten files. Shoe-horned by @surma.
|
||||
// FIXME: This an incomplete definition!
|
||||
interface ModuleOpts {
|
||||
noInitialRun?: boolean;
|
||||
locateFile?: (url: string) => string;
|
||||
onRuntimeInitialized?: () => void;
|
||||
}
|
||||
|
||||
interface Module {
|
||||
print(str: string): void;
|
||||
printErr(str: string): void;
|
||||
arguments: string[];
|
||||
environment: EnvironmentType;
|
||||
preInit: { (): void }[];
|
||||
preRun: { (): void }[];
|
||||
postRun: { (): void }[];
|
||||
preinitializedWebGLContext: WebGLRenderingContext;
|
||||
noInitialRun: boolean;
|
||||
noExitRuntime: boolean;
|
||||
logReadFiles: boolean;
|
||||
filePackagePrefixURL: string;
|
||||
wasmBinary: ArrayBuffer;
|
||||
|
||||
destroy(object: object): void;
|
||||
getPreloadedPackage(remotePackageName: string, remotePackageSize: number): ArrayBuffer;
|
||||
instantiateWasm(
|
||||
imports: WebAssembly.Imports,
|
||||
successCallback: (module: WebAssembly.Module) => void
|
||||
): WebAssembly.Exports;
|
||||
locateFile(url: string): string;
|
||||
onCustomMessage(event: MessageEvent): void;
|
||||
|
||||
Runtime: any;
|
||||
|
||||
ccall(ident: string, returnType: string | null, argTypes: string[], args: any[]): any;
|
||||
cwrap(ident: string, returnType: string | null, argTypes: string[]): any;
|
||||
|
||||
setValue(ptr: number, value: any, type: string, noSafe?: boolean): void;
|
||||
getValue(ptr: number, type: string, noSafe?: boolean): number;
|
||||
|
||||
ALLOC_NORMAL: number;
|
||||
ALLOC_STACK: number;
|
||||
ALLOC_STATIC: number;
|
||||
ALLOC_DYNAMIC: number;
|
||||
ALLOC_NONE: number;
|
||||
|
||||
allocate(slab: any, types: string, allocator: number, ptr: number): number;
|
||||
allocate(slab: any, types: string[], allocator: number, ptr: number): number;
|
||||
|
||||
Pointer_stringify(ptr: number, length?: number): string;
|
||||
UTF16ToString(ptr: number): string;
|
||||
stringToUTF16(str: string, outPtr: number): void;
|
||||
UTF32ToString(ptr: number): string;
|
||||
stringToUTF32(str: string, outPtr: number): void;
|
||||
|
||||
// USE_TYPED_ARRAYS == 1
|
||||
HEAP: Int32Array;
|
||||
IHEAP: Int32Array;
|
||||
FHEAP: Float64Array;
|
||||
|
||||
// USE_TYPED_ARRAYS == 2
|
||||
HEAP8: Int8Array;
|
||||
HEAP16: Int16Array;
|
||||
HEAP32: Int32Array;
|
||||
HEAPU8: Uint8Array;
|
||||
HEAPU16: Uint16Array;
|
||||
HEAPU32: Uint32Array;
|
||||
HEAPF32: Float32Array;
|
||||
HEAPF64: Float64Array;
|
||||
|
||||
TOTAL_STACK: number;
|
||||
TOTAL_MEMORY: number;
|
||||
FAST_MEMORY: number;
|
||||
|
||||
addOnPreRun(cb: () => any): void;
|
||||
addOnInit(cb: () => any): void;
|
||||
addOnPreMain(cb: () => any): void;
|
||||
addOnExit(cb: () => any): void;
|
||||
addOnPostRun(cb: () => any): void;
|
||||
|
||||
// Tools
|
||||
intArrayFromString(stringy: string, dontAddNull?: boolean, length?: number): number[];
|
||||
intArrayToString(array: number[]): string;
|
||||
writeStringToMemory(str: string, buffer: number, dontAddNull: boolean): void;
|
||||
writeArrayToMemory(array: number[], buffer: number): void;
|
||||
writeAsciiToMemory(str: string, buffer: number, dontAddNull: boolean): void;
|
||||
|
||||
addRunDependency(id: any): void;
|
||||
removeRunDependency(id: any): void;
|
||||
|
||||
|
||||
preloadedImages: any;
|
||||
preloadedAudios: any;
|
||||
|
||||
_malloc(size: number): number;
|
||||
_free(ptr: number): void;
|
||||
|
||||
// Augmentations below by @surma.
|
||||
onRuntimeInitialized: () => void | null;
|
||||
}
|
||||
}
|
||||
|
11
global.d.ts
vendored
@ -1,22 +1,23 @@
|
||||
declare const __webpack_public_path__: string;
|
||||
declare const PRERENDER: boolean;
|
||||
|
||||
declare interface NodeModule {
|
||||
hot: any;
|
||||
}
|
||||
|
||||
declare interface Window {
|
||||
STATE: any
|
||||
STATE: any;
|
||||
ga: typeof ga;
|
||||
}
|
||||
|
||||
declare namespace JSX {
|
||||
interface Element { }
|
||||
interface IntrinsicElements { }
|
||||
interface HTMLAttributes {
|
||||
decoding?: string;
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'preact-i18n';
|
||||
declare module 'preact-material-components-drawer';
|
||||
declare module 'material-radial-progress';
|
||||
|
||||
declare module 'classnames' {
|
||||
export default function classnames(...args: any[]): string;
|
||||
}
|
||||
|
19486
package-lock.json
generated
142
package.json
@ -1,91 +1,75 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "squoosh",
|
||||
"version": "0.0.0",
|
||||
"version": "1.3.3",
|
||||
"license": "apache-2.0",
|
||||
"scripts": {
|
||||
"start": "webpack serve --hot",
|
||||
"start": "webpack-dev-server --host 0.0.0.0 --hot",
|
||||
"build": "webpack -p",
|
||||
"lint": "eslint src"
|
||||
"lint": "tslint -c tslint.json -p tsconfig.json -t verbose 'src/**/*.{ts,tsx,js,jsx}'",
|
||||
"lintfix": "tslint -c tslint.json -p tsconfig.json -t verbose --fix 'src/**/*.{ts,tsx,js,jsx}'",
|
||||
"sizereport": "node config/size-report.js"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"standard",
|
||||
"standard-jsx"
|
||||
],
|
||||
"rules": {
|
||||
"indent": [
|
||||
2,
|
||||
2
|
||||
],
|
||||
"semi": [
|
||||
2,
|
||||
"always"
|
||||
],
|
||||
"prefer-const": 1
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npm run lint"
|
||||
}
|
||||
},
|
||||
"eslintIgnore": [
|
||||
"build/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/node": "^9.4.7",
|
||||
"babel-loader": "^7.1.4",
|
||||
"babel-plugin-jsx-pragmatic": "^1.0.2",
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||
"babel-plugin-transform-react-constant-elements": "^6.23.0",
|
||||
"babel-plugin-transform-react-jsx": "^6.24.1",
|
||||
"babel-plugin-transform-react-remove-prop-types": "^0.4.13",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-register": "^6.26.0",
|
||||
"clean-webpack-plugin": "^0.1.19",
|
||||
"copy-webpack-plugin": "^4.5.1",
|
||||
"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",
|
||||
"html-webpack-plugin": "^3.0.6",
|
||||
"if-env": "^1.0.4",
|
||||
"loader-utils": "^1.1.0",
|
||||
"mini-css-extract-plugin": "^0.3.0",
|
||||
"node-sass": "^4.7.2",
|
||||
"optimize-css-assets-webpack-plugin": "^4.0.0",
|
||||
"preload-webpack-plugin": "^3.0.0-alpha.3",
|
||||
"progress-bar-webpack-plugin": "^1.11.0",
|
||||
"raw-loader": "^0.5.1",
|
||||
"sass-loader": "^6.0.7",
|
||||
"script-ext-html-webpack-plugin": "^2.0.1",
|
||||
"source-map-loader": "^0.2.3",
|
||||
"style-loader": "^0.20.3",
|
||||
"ts-loader": "^4.0.1",
|
||||
"tslint": "^5.9.1",
|
||||
"tslint-config-semistandard": "^7.0.0",
|
||||
"tslint-react": "^3.5.1",
|
||||
"typescript": "^2.7.2",
|
||||
"typings-for-css-modules-loader": "^1.7.0",
|
||||
"webpack": "^4.3.0",
|
||||
"webpack-bundle-analyzer": "^2.11.1",
|
||||
"webpack-cli": "^2.0.13",
|
||||
"webpack-dev-server": "^3.1.1",
|
||||
"webpack-plugin-replace": "^1.1.1",
|
||||
"workbox-webpack-plugin": "^3.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"classnames": "^2.2.5",
|
||||
"material-components-web": "^0.32.0",
|
||||
"material-radial-progress": "git+https://gist.github.com/02134901c77c5309924bfcf8b4435ebe.git",
|
||||
"preact": "^8.2.7",
|
||||
"preact-i18n": "^1.2.0",
|
||||
"preact-material-components": "^1.3.7",
|
||||
"preact-material-components-drawer": "git+https://gist.github.com/a78fceed440b98e62582e4440b86bfab.git",
|
||||
"preact-router": "^2.6.0"
|
||||
"@types/node": "10.12.26",
|
||||
"@types/pretty-bytes": "5.1.0",
|
||||
"@types/webassembly-js-api": "0.0.2",
|
||||
"@webcomponents/custom-elements": "1.2.1",
|
||||
"@webpack-cli/serve": "0.1.3",
|
||||
"assets-webpack-plugin": "3.9.7",
|
||||
"chokidar": "2.1.1",
|
||||
"chalk": "2.4.2",
|
||||
"classnames": "2.2.6",
|
||||
"clean-webpack-plugin": "1.0.1",
|
||||
"comlink": "3.1.1",
|
||||
"copy-webpack-plugin": "4.6.0",
|
||||
"critters-webpack-plugin": "2.2.0",
|
||||
"css-loader": "1.0.1",
|
||||
"ejs": "2.6.1",
|
||||
"escape-string-regexp": "1.0.5",
|
||||
"exports-loader": "0.7.0",
|
||||
"file-drop-element": "0.0.9",
|
||||
"file-loader": "3.0.1",
|
||||
"gzip-size": "5.0.0",
|
||||
"html-webpack-plugin": "3.2.0",
|
||||
"husky": "1.3.1",
|
||||
"idb-keyval": "3.1.0",
|
||||
"linkstate": "1.1.1",
|
||||
"loader-utils": "1.2.3",
|
||||
"mini-css-extract-plugin": "0.5.0",
|
||||
"minimatch": "3.0.4",
|
||||
"node-fetch": "2.3.0",
|
||||
"node-sass": "4.11.0",
|
||||
"optimize-css-assets-webpack-plugin": "5.0.1",
|
||||
"pointer-tracker": "2.0.3",
|
||||
"preact": "8.4.2",
|
||||
"prerender-loader": "1.2.0",
|
||||
"pretty-bytes": "5.1.0",
|
||||
"progress-bar-webpack-plugin": "1.12.1",
|
||||
"raw-loader": "1.0.0",
|
||||
"readdirp": "2.2.1",
|
||||
"sass-loader": "7.1.0",
|
||||
"script-ext-html-webpack-plugin": "2.1.3",
|
||||
"source-map-loader": "0.2.4",
|
||||
"style-loader": "0.23.1",
|
||||
"terser-webpack-plugin": "1.2.2",
|
||||
"ts-loader": "5.3.3",
|
||||
"tslint": "5.12.1",
|
||||
"tslint-config-airbnb": "5.11.1",
|
||||
"tslint-config-semistandard": "7.0.0",
|
||||
"tslint-react": "3.6.0",
|
||||
"typed-css-modules": "0.3.7",
|
||||
"typescript": "3.2.4",
|
||||
"url-loader": "1.1.2",
|
||||
"webpack": "4.28.0",
|
||||
"webpack-bundle-analyzer": "3.0.4",
|
||||
"webpack-cli": "3.2.3",
|
||||
"webpack-dev-server": "3.1.14",
|
||||
"worker-plugin": "3.0.0"
|
||||
}
|
||||
}
|
||||
|
5
renovate.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base"
|
||||
]
|
||||
}
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 15 KiB |
BIN
src/assets/icon-large.png
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
src/assets/icon-small.png
Normal file
After Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 27 KiB |
11
src/codecs/browser-bmp/encoder-meta.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { canvasEncodeTest } from '../generic/util';
|
||||
|
||||
export interface EncodeOptions { }
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
|
||||
export const type = 'browser-bmp';
|
||||
export const label = 'Browser BMP';
|
||||
export const mimeType = 'image/bmp';
|
||||
export const extension = 'bmp';
|
||||
export const defaultOptions: EncodeOptions = {};
|
||||
export const featureTest = () => canvasEncodeTest(mimeType);
|
6
src/codecs/browser-bmp/encoder.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { mimeType } from './encoder-meta';
|
||||
import { canvasEncode } from '../../lib/util';
|
||||
|
||||
export function encode(data: ImageData) {
|
||||
return canvasEncode(data, mimeType);
|
||||
}
|
11
src/codecs/browser-gif/encoder-meta.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { canvasEncodeTest } from '../generic/util';
|
||||
|
||||
export interface EncodeOptions {}
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
|
||||
export const type = 'browser-gif';
|
||||
export const label = 'Browser GIF';
|
||||
export const mimeType = 'image/gif';
|
||||
export const extension = 'gif';
|
||||
export const defaultOptions: EncodeOptions = {};
|
||||
export const featureTest = () => canvasEncodeTest(mimeType);
|
6
src/codecs/browser-gif/encoder.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { mimeType } from './encoder-meta';
|
||||
import { canvasEncode } from '../../lib/util';
|
||||
|
||||
export function encode(data: ImageData) {
|
||||
return canvasEncode(data, mimeType);
|
||||
}
|
11
src/codecs/browser-jp2/encoder-meta.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { canvasEncodeTest } from '../generic/util';
|
||||
|
||||
export interface EncodeOptions { }
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
|
||||
export const type = 'browser-jp2';
|
||||
export const label = 'Browser JPEG 2000';
|
||||
export const mimeType = 'image/jp2';
|
||||
export const extension = 'jp2';
|
||||
export const defaultOptions: EncodeOptions = {};
|
||||
export const featureTest = () => canvasEncodeTest(mimeType);
|
6
src/codecs/browser-jp2/encoder.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { mimeType } from './encoder-meta';
|
||||
import { canvasEncode } from '../../lib/util';
|
||||
|
||||
export function encode(data: ImageData) {
|
||||
return canvasEncode(data, mimeType);
|
||||
}
|
8
src/codecs/browser-jpeg/encoder-meta.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface EncodeOptions { quality: number; }
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
|
||||
export const type = 'browser-jpeg';
|
||||
export const label = 'Browser JPEG';
|
||||
export const mimeType = 'image/jpeg';
|
||||
export const extension = 'jpg';
|
||||
export const defaultOptions: EncodeOptions = { quality: 0.75 };
|
6
src/codecs/browser-jpeg/encoder.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { EncodeOptions, mimeType } from './encoder-meta';
|
||||
import { canvasEncode } from '../../lib/util';
|
||||
|
||||
export function encode(data: ImageData, { quality }: EncodeOptions) {
|
||||
return canvasEncode(data, mimeType, quality);
|
||||
}
|
3
src/codecs/browser-jpeg/options.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import qualityOption from '../generic/quality-option';
|
||||
|
||||
export default qualityOption({ min: 0, max: 1, step: 0.01 });
|
11
src/codecs/browser-pdf/encoder-meta.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { canvasEncodeTest } from '../generic/util';
|
||||
|
||||
export interface EncodeOptions { }
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
|
||||
export const type = 'browser-pdf';
|
||||
export const label = 'Browser PDF';
|
||||
export const mimeType = 'application/pdf';
|
||||
export const extension = 'pdf';
|
||||
export const defaultOptions: EncodeOptions = {};
|
||||
export const featureTest = () => canvasEncodeTest(mimeType);
|
6
src/codecs/browser-pdf/encoder.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { mimeType } from './encoder-meta';
|
||||
import { canvasEncode } from '../../lib/util';
|
||||
|
||||
export function encode(data: ImageData) {
|
||||
return canvasEncode(data, mimeType);
|
||||
}
|
8
src/codecs/browser-png/encoder-meta.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface EncodeOptions {}
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
|
||||
export const type = 'browser-png';
|
||||
export const label = 'Browser PNG';
|
||||
export const mimeType = 'image/png';
|
||||
export const extension = 'png';
|
||||
export const defaultOptions: EncodeOptions = {};
|
6
src/codecs/browser-png/encoder.tsx
Normal file
@ -0,0 +1,6 @@
|
||||
import { mimeType } from './encoder-meta';
|
||||
import { canvasEncode } from '../../lib/util';
|
||||
|
||||
export function encode(data: ImageData) {
|
||||
return canvasEncode(data, mimeType);
|
||||
}
|
11
src/codecs/browser-tiff/encoder-meta.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { canvasEncodeTest } from '../generic/util';
|
||||
|
||||
export interface EncodeOptions { }
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
|
||||
export const type = 'browser-tiff';
|
||||
export const label = 'Browser TIFF';
|
||||
export const mimeType = 'image/tiff';
|
||||
export const extension = 'tiff';
|
||||
export const defaultOptions: EncodeOptions = {};
|
||||
export const featureTest = () => canvasEncodeTest(mimeType);
|
6
src/codecs/browser-tiff/encoder.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { mimeType } from './encoder-meta';
|
||||
import { canvasEncode } from '../../lib/util';
|
||||
|
||||
export function encode(data: ImageData) {
|
||||
return canvasEncode(data, mimeType);
|
||||
}
|
11
src/codecs/browser-webp/encoder-meta.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { canvasEncodeTest } from '../generic/util';
|
||||
|
||||
export interface EncodeOptions { quality: number; }
|
||||
export interface EncoderState { type: typeof type; options: EncodeOptions; }
|
||||
|
||||
export const type = 'browser-webp';
|
||||
export const label = 'Browser WebP';
|
||||
export const mimeType = 'image/webp';
|
||||
export const extension = 'webp';
|
||||
export const defaultOptions: EncodeOptions = { quality: 0.75 };
|
||||
export const featureTest = () => canvasEncodeTest(mimeType);
|
6
src/codecs/browser-webp/encoder.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { EncodeOptions, mimeType } from './encoder-meta';
|
||||
import { canvasEncode } from '../../lib/util';
|
||||
|
||||
export function encode(data: ImageData, { quality }: EncodeOptions) {
|
||||
return canvasEncode(data, mimeType, quality);
|
||||
}
|
3
src/codecs/browser-webp/options.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import qualityOption from '../generic/quality-option';
|
||||
|
||||
export default qualityOption({ min: 0, max: 1, step: 0.01 });
|
20
src/codecs/decoders.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { nativeDecode, sniffMimeType, canDecodeImage } from '../lib/util';
|
||||
import Processor from './processor';
|
||||
import webpDataUrl from 'url-loader!./tiny.webp';
|
||||
|
||||
const nativeWebPSupported = canDecodeImage(webpDataUrl);
|
||||
|
||||
export async function decodeImage(blob: Blob, processor: Processor): Promise<ImageData> {
|
||||
const mimeType = await sniffMimeType(blob);
|
||||
|
||||
try {
|
||||
if (mimeType === 'image/webp' && !(await nativeWebPSupported)) {
|
||||
return await processor.webpDecode(blob);
|
||||
}
|
||||
|
||||
// Otherwise, just throw it at the browser's decoder.
|
||||
return await nativeDecode(blob);
|
||||
} catch (err) {
|
||||
throw Error("Couldn't decode image");
|
||||
}
|
||||
}
|
78
src/codecs/encoders.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import * as identity from './identity/encoder-meta';
|
||||
import * as optiPNG from './optipng/encoder-meta';
|
||||
import * as mozJPEG from './mozjpeg/encoder-meta';
|
||||
import * as webP from './webp/encoder-meta';
|
||||
import * as browserPNG from './browser-png/encoder-meta';
|
||||
import * as browserJPEG from './browser-jpeg/encoder-meta';
|
||||
import * as browserWebP from './browser-webp/encoder-meta';
|
||||
import * as browserGIF from './browser-gif/encoder-meta';
|
||||
import * as browserTIFF from './browser-tiff/encoder-meta';
|
||||
import * as browserJP2 from './browser-jp2/encoder-meta';
|
||||
import * as browserBMP from './browser-bmp/encoder-meta';
|
||||
import * as browserPDF from './browser-pdf/encoder-meta';
|
||||
|
||||
export interface EncoderSupportMap {
|
||||
[key: string]: boolean;
|
||||
}
|
||||
|
||||
export type EncoderState =
|
||||
identity.EncoderState |
|
||||
optiPNG.EncoderState |
|
||||
mozJPEG.EncoderState |
|
||||
webP.EncoderState |
|
||||
browserPNG.EncoderState |
|
||||
browserJPEG.EncoderState |
|
||||
browserWebP.EncoderState |
|
||||
browserGIF.EncoderState |
|
||||
browserTIFF.EncoderState |
|
||||
browserJP2.EncoderState |
|
||||
browserBMP.EncoderState |
|
||||
browserPDF.EncoderState;
|
||||
|
||||
export type EncoderOptions =
|
||||
identity.EncodeOptions |
|
||||
optiPNG.EncodeOptions |
|
||||
mozJPEG.EncodeOptions |
|
||||
webP.EncodeOptions |
|
||||
browserPNG.EncodeOptions |
|
||||
browserJPEG.EncodeOptions |
|
||||
browserWebP.EncodeOptions |
|
||||
browserGIF.EncodeOptions |
|
||||
browserTIFF.EncodeOptions |
|
||||
browserJP2.EncodeOptions |
|
||||
browserBMP.EncodeOptions |
|
||||
browserPDF.EncodeOptions;
|
||||
|
||||
export type EncoderType = keyof typeof encoderMap;
|
||||
|
||||
export const encoderMap = {
|
||||
[identity.type]: identity,
|
||||
[optiPNG.type]: optiPNG,
|
||||
[mozJPEG.type]: mozJPEG,
|
||||
[webP.type]: webP,
|
||||
[browserPNG.type]: browserPNG,
|
||||
[browserJPEG.type]: browserJPEG,
|
||||
[browserWebP.type]: browserWebP,
|
||||
// Safari & Firefox only:
|
||||
[browserBMP.type]: browserBMP,
|
||||
// Safari only:
|
||||
[browserGIF.type]: browserGIF,
|
||||
[browserTIFF.type]: browserTIFF,
|
||||
[browserJP2.type]: browserJP2,
|
||||
[browserPDF.type]: browserPDF,
|
||||
};
|
||||
|
||||
export const encoders = Array.from(Object.values(encoderMap));
|
||||
|
||||
/** Does this browser support a given encoder? Indexed by label */
|
||||
export const encodersSupported = Promise.resolve().then(async () => {
|
||||
const encodersSupported: EncoderSupportMap = {};
|
||||
|
||||
await Promise.all(encoders.map(async (encoder) => {
|
||||
// If the encoder provides a featureTest, call it, otherwise assume supported.
|
||||
const isSupported = !('featureTest' in encoder) || await encoder.featureTest();
|
||||
encodersSupported[encoder.type] = isSupported;
|
||||
}));
|
||||
|
||||
return encodersSupported;
|
||||
});
|
56
src/codecs/generic/quality-option.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { h, Component } from 'preact';
|
||||
import { bind } from '../../lib/initial-util';
|
||||
import * as style from '../../components/Options/style.scss';
|
||||
import Range from '../../components/range';
|
||||
|
||||
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 class={style.optionsSection}>
|
||||
<div class={style.optionOneCell}>
|
||||
<Range
|
||||
name="quality"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step || 'any'}
|
||||
value={options.quality}
|
||||
onInput={this.onChange}
|
||||
>
|
||||
Quality:
|
||||
</Range>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return QualityOptions;
|
||||
}
|