Compare commits

...

153 Commits

Author SHA1 Message Date
5508fc47c8 Preventing form defaults. Fixes #294. 2018-11-20 09:30:07 +00:00
bb8f35ce09 Merge pull request #323 from GoogleChromeLabs/sass-downgrade
Downgrade node-sass (fixes #319)
2018-11-19 12:22:00 +00:00
ae9ae31ddc Downgrade node-sass (fixes #319) 2018-11-19 12:06:47 +00:00
67893817b5 Update issue templates to include feature request (#318) 2018-11-19 01:26:50 -08:00
f8da5b153d Merge pull request #304 : Create issue templates
Create issue templates
2018-11-19 11:55:40 +09:00
e2a956a088 ask to attach images 2018-11-19 11:51:08 +09:00
5c5b001fc7 Merge pull request #269 from DanielRuf/ci/test-nodejs-6-8-10-11
ci: test Node.js 8, 10 and 11
2018-11-18 13:24:37 +00:00
e4beafed97 ci: do not test on Node.js 6 2018-11-18 14:00:28 +01:00
553a504140 Merge pull request #306 from Jarrku/codec-readme-typo
Fix typo
2018-11-16 11:21:28 -08:00
44dd2ee808 Fix typo 2018-11-15 22:02:11 +01:00
b36c851b2a Create issue templates 2018-11-15 10:34:00 -08:00
0502d70cdf Preventing images from being dragged in Edge (#290) 2018-11-14 14:47:00 -08:00
86546574bb Further losslessly optimize logo.svg (#283) 2018-11-14 14:45:47 -08:00
f351712130 Building on #275 (#289)
* Upgrade devDependcies. Replace UglifyJS ⚰ with TerserJS 👶 Fix TypeScript compiler errors

* Remove babel and associated plugins

* Re-enable strictNullChecks and noImplicitAny

* Use surma's better ga type definition.
`ts-ignore` document.activeElement potential null warnings

* Avoiding ignores
2018-11-14 14:04:01 -08:00
c7f2ae2234 Merge pull request #279 from KraigWalker/bug/manifest-orientation
Fix #268 - change orientation to "any" from "portrait" in manifest.json
2018-11-14 08:25:09 -08:00
436f689115 fixes #268 - change orientation to "any" from "portrait" in manifest.json 2018-11-13 17:34:03 +00:00
951c7af724 Allow text fields next to range inputs be empty (yeah that's horrendous grammar but I'm very tired) (#273) 2018-11-13 07:48:25 -08:00
53b46f879f Avoid "update found" on initial load. 2018-11-13 07:37:07 -08:00
cbe82112ab ci: test Node.js 6, 8, 10 and 11 2018-11-13 11:21:51 +01:00
7f5562ccfe Update README.md 2018-11-12 10:35:58 -08:00
76ec946616 Merge pull request #264 from GoogleChromeLabs/readme-typos
Fix typos 🙈
2018-11-11 19:15:45 -08:00
68bb2edb39 Fix typos 🙈 2018-11-11 17:43:20 -08:00
9c85618aff Merge pull request #263 from GoogleChromeLabs/analytics-privacy
Adding readme, privacy section, reducing resolution of analytics data.
2018-11-11 06:07:52 -08:00
aebeff8b4c Adding readme, privacy section, reducing resolution of analytics data. 2018-11-11 05:11:28 -08:00
8d63125b13 Resetting pinch zoom (#261)
* Resetting pinch zoom

* Bumping version
2018-11-11 04:28:39 -08:00
2ca97ef586 Not entirely sure why this causes dev to fail, but this fixes it. 2018-11-10 16:10:25 -08:00
a1a00f0bfb Preload test (#262)
* Preload test

* Don't prerender analytics

* Version bump
2018-11-10 08:20:13 -08:00
6870b135b7 I'm calling this 1.0 2018-11-09 16:01:24 -08:00
a0f1379feb Adding manifest to headers 2018-11-09 12:08:55 -08:00
9b17322478 Removing old file from serviceworker 2018-11-09 11:11:34 -08:00
f562bad286 Add analytics script (fixes #174) (#245) 2018-11-09 10:53:10 -08:00
6994cc3d15 _headers & _redirects generation (#240)
* Generate `_headers` and `_redirects` by passing assets through ejs templates.

* PR feedback

* Excluding service worker stuff from prerender

* Build SW in dev

* Let's give this a try

* lol

* Is this how it works?
2018-11-09 10:49:01 -08:00
9b572f9541 Ta-da. Back button. (#254) 2018-11-09 09:13:32 -08:00
71f893cb44 Enhanced offline (#249)
* Notification of updates & reloading

* Using version in service worker & allowing version to appear elsewhere

* Stupid file

* Ditching changelog for now. Using package json.

* Ugh.
2018-11-09 09:13:14 -08:00
6b76ea0a6f Update file drop (#253) 2018-11-09 08:53:36 -08:00
7616d33883 Startup optimizations (#226)
* Startup optimisations

* I hate this file

* Inline main script

* Reverting change to do a fairer perf comparison

* Inlining again. Weeeeee!

* Lockfile
2018-11-09 16:01:02 +00:00
3c757bb2b2 Prevent browser pinch-zoom (#247) 2018-11-09 15:58:15 +00:00
a502df80ba Prevent logo taking over on smaller screens. (#250)
* Prevent logo taking over on smaller screens.

* I hate this file
2018-11-09 00:02:23 +00:00
921268ec58 Addressing nits from service worker PR. 2018-11-08 12:11:29 +00:00
7d42d4f973 Add a serviceworker (#234)
* Add a serviceworker

* rename + fix random extra character

* Fixing worker typings

* Fixing types properly this time.

* Once of those rare cases where this matters.

* Naming the things.

* Move registration to the app (so we can use snackbar later)

* Moving SW plugin later so it picks up things like HTML

* MVP service worker

* Two stage-service worker

* Fix prerendering by conditionally awaiting Custom Elements polyfill.

* Fix icon 404's

* add doc comment to autoswplugin

* Fix type
2018-11-08 12:02:05 +00:00
e4e130c5d6 Mark private function as private 2018-11-08 11:21:57 +00:00
bcf7a63118 Android tablet bugs (#246)
* Prevent two-up being lost under options. Fixes #241.

* Working around some glitching when page was scrolled.

* Prevent software keyboard popping up again in Android Chrome.
2018-11-07 08:59:11 +00:00
66aac12db7 Caught a bit of repetition in our utils (#242)
Caught a bit of repetition in our utils
2018-11-06 14:19:48 +00:00
59cd1f8930 Splitting PointerTracker into its own project (#238) 2018-11-06 14:19:24 +00:00
150e704d20 It's a dev package 2018-11-06 13:50:50 +00:00
b2d47f0fb8 fixing a deps issue. 2018-11-06 13:50:49 +00:00
bd3d33296d Updating based on changes to filedrop element. 2018-11-06 13:50:49 +00:00
f4c82ced97 #199 - Removes the file-drop custom element
+ Removes the custom element from the project
+ Replaces it with the externally maintined custom element
2018-11-06 13:50:48 +00:00
76188df0d3 Destructuring args. 2018-11-06 13:47:18 +00:00
9a58e4d339 Fixing lossless slider for webP. Previously you couldn't select "9" :D 2018-11-06 13:47:18 +00:00
f396a5b784 MozJPEG chroma subsampling and quality (#235)
* Adding chroma subsampling for mozjpeg

* Adding separate chroma quality.

* Preact sometimes removes the inline styles, this fixes it.

* Simplifying chroma subsample

* Adding comments
2018-11-06 13:46:52 +00:00
e572b853e2 Snackbar defaults & copy undo (#233)
* Fix snackbar defaults. Fixes #205.

* Undo copy settings across.

* Oops

* Fixing stupid minification bug

* Something weird happened with the last commit
2018-11-06 13:44:15 +00:00
726c2f195a Fixing graphics glitch. Fixes #166 (#232) 2018-11-06 13:41:08 +00:00
4599e51b1e Copy to side ui (#229)
* Copy settings to other side button

* Download button on the outside.

* Whoops
2018-11-06 13:39:03 +00:00
d93169cc5a Mobile ui (#227)
* Basic grid setup

* Fixing thumb on two-up

* Adding margin so you can still access the two-up

* Allow multi-panel to keep one open only

* Edge cases for one-open

* Abstracting results so it can be used as a heading.

* Ordering of items in mobile view. Changing scrolling element.

* Adding labels to collapsed view

* Adding height animation to multi-panel

* Fixing animation bugs

* Expand/collapse icon

* Allow two-up and pinch-zoom to work beneath controls

* Range bubble now behaves properly on mobile

* No longer need this.

* Prevent options overflow at larger widths
2018-11-06 13:37:52 +00:00
bdd3c11f1a Options ui (#222)
* wip

* Commenting stuff to keep the build happy

* Revealing sections

* Custom select elements & more form work

* Range input styles

* Text fields with inputs do the right thing

* Safari & Firefox fixes

* Large compress select

* oops

* MozJPEG options updated

* OptPNG options

* These asserts weren't true

* Generic options

* WebP options

* Hiding "edit" when "original image"

* Download icon

* Copy setting button - still not happy with this

* Progress indicator

* Loading icon enter/exit anim

* Preventing controls going under options

* Ahh so that's what was causing scrolling

* Ahh so that's what was causing outlines

* Simplifying range styles and fixing cross-browser

* Processing custom element styles

* Get precision from step by default

* I don't know how or when this happened.

* Don't need that many steps

* Avoid having an element that covers the pinch zoom

* Preventing overlap with zoom controls

* Prevent ts warning

* Fixing spinner position

* Simplifying FileSize
2018-11-06 13:36:23 +00:00
0cec90c7ca Main ui (#214)
* Class for file drop

* OCD

* We don't need the invalid state, as we'll accept all types.

* Flattening CSS

* Fixing zoom input in Firefox

* Fixing 'container' scaleTo

* two-up closer design match & smaller line

* Fixing edge bug
2018-11-06 13:31:58 +00:00
43def798e1 Two workers & worker termination (#198)
* Refactoring codecs

* Plugging in new processor

* Fixing decorator

* MozJPEG free issue

* Better worker aborting, and terminate workers that aren't used for 10 seconds

* Better comment

* Ooops, half-typed comment

* Uncommenting problematic line

* Surma fixed it!

* Abstracting WASM initialisation

* Better comment

* Don't need this.

* Adding ticket

* noInitalRun

* Reverting MozJPEG issue demo

* Making a const for worker timeout

* Inline docs

* Bail early rather than nesting

* Addressing nits
2018-10-28 09:17:43 +00:00
02b0c022ca Edge encode fix (#211)
* No canvas.toBlob in Edge.

* pffft
2018-10-20 14:50:37 +01:00
c82d0d1b88 Fix for loading SVG in Safari. (#212)
* Fix for loading SVG in Safari.

* Tidier code
2018-10-20 14:50:16 +01:00
e24d7865ce Lazy-loading the main part of the app (#197)
* Splitting main part of app out of the main bundle.

Also improving the transition from intro to compressor.

* Showing error if app fails to load.

* lol these aren't async

* Please don't tell anyone I did this

* Spinner if user selects a file before the app has loaded. (#208)
2018-10-20 12:53:36 +01:00
a79f95b305 Fix <range-input disabled> (#195)
* Fix .disabled property reflection for range-input (only attribute worked previously)

* Revert "Fix .disabled property reflection for range-input (only attribute worked previously)"

This reverts commit f5964635b2.

* Fix reflection

* Use hasAttribute()
2018-10-20 12:51:30 +01:00
49b40b1c3e Remove virtual files before running (fixes #217) (#220) 2018-10-19 12:07:01 +01:00
11ee74e224 Edge gets confused about custom properties in @supports. (#206) 2018-10-14 14:11:20 +01:00
f335246673 Use mozjpeg function to free result (#207) 2018-10-14 14:10:21 +01:00
ccb734aec6 Remove firebase travis deploy (for now) (#196)
* remove firebase (for now)

* Also rm .firebaserc
2018-10-14 09:35:07 +01:00
568b9e9459 Styling intro screen and adding demo images. 2018-10-12 14:12:32 +01:00
a43ea761f5 Missing bind 2018-10-12 14:04:18 +01:00
577c77cc30 Codecs cleanup (#189)
* Update imagequant processor

* Update mozjpeg encoder

* Update optipng encoder

* Update webp decoder

* Update webp encoder

* Remove old codec build tasks

* Add warning about docker image updates
2018-10-12 10:53:33 +01:00
d2f60baef9 Handle vectors (#187)
* Allow loading SVG. Fixes #138.

I also made the resizer vector-aware, so you can resize the image larger & stay sharp.

* Handling SVG without width/height set.

* Simplifying maths

* Doh, case sensitive
2018-10-11 14:15:01 +01:00
64acc08cd7 Improving image open time (#185)
* Moving intro into its own component

* Tidying JSX, and allowing image to render before first compression. Fixes #164.
2018-10-11 14:05:18 +01:00
a1f0b81dff Fixing resize again. Fixes #183. (#184) 2018-10-09 13:34:38 +01:00
48bb58dc89 Moving range input component 2018-10-09 12:19:24 +01:00
765cc213d2 Updating linting rules and fixing bug 2018-10-02 14:56:20 +01:00
37f5c0dd76 Design review fixes (#172)
* Design review fixes

* Adding styles for mozjpeg & fixing some merge errors
2018-10-02 14:53:21 +01:00
b25d1eaf86 <range-input> (#171) 2018-10-02 11:41:07 +01:00
248676aa31 Fixing webp sniffing. Fixes #178. (#179) 2018-10-02 11:30:38 +01:00
Ewa
059c80c05d Center the image canvas in PinchZoom (#170)
* Center the image canvas in PinchZoom

* Fixing pinch zoom
2018-10-01 13:57:26 +01:00
cfd42818b7 Edge file constructor fix (#180)
* Hacking around lack of `new File` in Edge.

* Less hacky solution - preserves types
2018-09-28 14:44:59 +01:00
5e66e0acc4 Adding custom element polyfill (#177) 2018-09-27 14:49:45 +01:00
c9fe5ffbcf Creating fallbacks for all ImageBitmap usage & refactoring. (#175)
Fixes #173
2018-09-27 14:44:54 +01:00
1b630a092f Dockerize mozjpeg (#169)
* Dockerize mozjpeg build process

* Update README
2018-09-26 12:31:26 +01:00
Ewa
09e60284cb Merge pull request #168 from GoogleChromeLabs/devnook-fix-105
Reposition the TwoUp handle on resize
2018-09-18 07:29:21 +02:00
76b34c62db Review code format fixes 2018-09-18 07:26:51 +02:00
9d7212bc1d Review code format fixes 2018-09-17 13:25:33 +02:00
1b69c9231d Keep relative screen division while resizing 2018-09-17 13:21:31 +02:00
bcd88f6356 Add fallback to window.onresize 2018-09-17 11:30:06 +02:00
2a47f67214 Reposition the TwoUp handle on resize 2018-09-17 11:08:54 +02:00
5e8dc1b26c Add multi-panel component (#95)
* add multi-panel component

* make _functions private

* fix spelling errors

* spacing fix

* spelling fix

* store result of _getClosestHeading and avoid calling twice.

* remove returns that are not needed

* move getClosestHeading as separate function

* re-thinking casting and logic

* reflect code review

* do not remove focus when hit spacebar

* add comments and fix type

* handle heading keyevents

* set expanded hight to auto
2018-09-12 14:57:01 +01:00
c591f1f37d Result cache LRU (#165)
* Give the result cache some LRU love

* Fixing LRU cache move-to-start, and simplifying
2018-09-11 17:31:08 +01:00
4db43ccd4e Button to copy settings to the other side 2018-09-06 13:05:17 +01:00
ea5d3c2d78 Adding resize preprocessor (#152)
* Adding resize preprocessor

* Using ! on form

* Haha oops

* Using createImageBitmapPolyfill

* Updating package.json

* Oops again

* Ooops again
2018-09-05 15:46:26 +01:00
700b1f15cd Pinch-zoom: scale around given origin. (#139)
Also setting a min scale.
2018-09-05 09:39:26 +01:00
485ba174e3 Adding result cache. Fixes #87. (#148) 2018-09-05 08:29:17 +01:00
32f6f8b941 Options UI (#135)
* Move gzipped size calculations into a worker and wrap it up in a `<GzipSize />` component that will also handle showing % of original size once that info is plumbed

* A couple tweaks for the app welcome (drop files) screen. We don't have mocks for this one, but this is at least a minor improvement.

* Prettier "pop" effect and styling for the drop zone/indicator.

* Styling for the quantization toggle to make it look like a disclosure triangle/button.

* Add controls bar (zoom in/out/to, background toggle). @todo: extract into its own component.

* When clicking/tapping the image area, give it focus.

* Utilities used by this PR

* Add a `two-up-handle` attribute to the handle for easier styling (classname gets mangled so it doesn't make for a good public API)

* Add a dummy comment to test netlify deploy

* Remove commented-out code.

* Fix styling of vertical split (which as it turns out is slightly different in the mocks anyway)

* Use a composited overlay for the dark background instead of animating background-color

* Move grayscale styling into `<two-up>` by default, then set colors via custom properties

* Remove commented-out svg fill

* Remove dummy comment

* Change `<GzipSize>` to be `<FileSize>`, add `compress` option that lets us show gzipped sizes later if we need. Defaults to `false`, and the gzip worker is only lazily instantiated the first time a compressed size calculation is requested.

* Dependency updates

* Remove color animations from dnd overlay

* Don't use a cyclical import for EncodedImage, instead just specify the types of the properties we Options actually uses.

* Pass source image through to FileSize component so it can compute delta

* Stylize size display with colors based on delta amount/direction

* Remove box-shadow animation.

* Simplify font stack

* Remove commented out code

* Remove gzip compression from size component

* Remove memoization bits

* Use specific flattend props instead of passing large context objects around.

* Remove unused packages.

* Remove unreachable String case in FileSize, and omit redundant File type

* Simplify calculateSize()

* Fix types for FileSize!

* Remove FileSize title

* Make delta variable consistent.

* Skip passing compareTo value for original image

* Remove manual focus

* Fix whitespace

* remove unused keyframes

* remove pointless flex-wrap property

* Remove unused resetZoom() method

* Remove pointless flex properties

* Use `on` prefix for event handling

* Remove pointless justify-self property

* Use an inline SVG for TwoUp's handle icon so it can be colored from outside the component..

* Move orientation state up from `<Output>` into `<App>` and share it with `<Options>`.

* Make the options panels responsive :)

* Show a plus sign for size increases `(+8%)`

* Use inline SVG for the zoom +/- icons, collect SVG icons into one file now that I've verified they get tree-shaken properly.

* Fix top/bottom options panels being reversed

* remove commented out code

* lockfile

* Revert quanitzation toggle styles so it's just a checkbox.

* Remove minimum delta for compare size

* Rename data prop to file.

* scale int -> float

* remove tabIndex

* Remove old icon files

* Add width to options panels

* Add vertical scrolling when options are taller than 80% of the screen height.
2018-09-05 08:21:54 +01:00
54ad30a7ed Optipng (#156)
* omg it’s compiling

* example actually works

* Expose compression level options

* Disable crypto and path module emulation in webpack

* Update README

* Remove small image

* Use -O3 on optipng

* Free memory after copy

* Handle unexpected file reader return types

* Rename level label to effort
2018-09-04 16:49:45 +01:00
170d75482e Merge pull request #155 from sendilkumarn/fix-name-replace
fix broken file renaming for file names with dot
2018-08-29 12:18:33 +01:00
a8db2b30f2 fix broken file renaming for file names with dot
remove grouping in regex
2018-08-29 13:12:33 +02:00
e3b1b08424 Make WebP decoder use memory views (#145)
* Make WebP decoder use memory views

* Update webp_dec README

* Port quantizer to memory views as well
2018-08-21 13:29:53 +01:00
8006a1a5e7 Memory view rather than pointers (#144). Part of #141.
* Returning an object seems to work well

* This doesn't work

* This does!

* Better cast?

* Updating usage in Squoosh
2018-08-21 09:27:04 +01:00
1ae65dd4a1 Mozjpeg opts (#140)
* Switching to embind

* Adding options to mozjpeg wasm

* Updating packages

* Ditching enum - causing more problems than it's worth

* Adding mozjpeg options UI

* Forgot about this enum

* Bools just work
2018-08-17 16:25:28 +01:00
bff515b63f Adding changed points and coalescing to pointer tracker. Fixes #133. (#134)
* Adding changed points and coalescing to pointer tracker. Fixes #133.

* Nits and exposing native pointer
2018-08-15 15:14:33 +01:00
65c3ea826f Avoid preprocessing images that have already been preprocessed. (#125)
* Avoid preprocessing images that have already been preprocessed.

* Using cleanMerge an cleanSet, and fixing bugs in our compression.
2018-08-10 12:59:29 +01:00
602d5140f9 Konami code fix. lol. Thanks @remy. 2018-08-08 14:47:58 +01:00
44f0700332 Adding clean-set (#124)
* Adding clean-set

* Moving to our own cleanSet and cleanMerge.

* Oops, this can be simpler

* Allow the path to be a number

* Better typing
2018-08-07 12:19:06 +01:00
c90db020b0 Snackbar (#99)
* Initial swing

* Finish up <snack-bar> implementation and integrate it

* Add missing types

* Use shift() since we dont care about referential equality

* Use `_` for private fields

* Remove rogue handler

* Remove impossible fallback value

* Make `<snack-bar>` actually contain its children

* will-change for the button ripple

* Guard against mutliple button action clicks

* `onhide()` -> `onremove()`

* remove transitionend

* Replace inline ref callback with linkRef

* showError only accepts strings

* Remove undefined initialization

* Throw on error

* Add missing error type.

* `SnackBar` ▶️ `Snack`

* Avoid child retaining a reference to parent, make show() return a Promise.

* async/await and avoid processing the stack if it is already being processed

* Add a meaningful return value to showSnackbar()
2018-08-06 14:32:48 +01:00
ef4094885e Easter egg (#123)
* lol zx quant

* Adding ZX option

* Improving colour selection so we don't end up with the same colour twice. Also fixing a bug with the colour conflict resolution.

* Putting it behind a konami code

* Better comments

* Adding comment

* Removing unnecessary malloc.
2018-08-06 12:42:23 +01:00
b52d9d9194 Merge pull request #122 from GoogleChromeLabs/quant-ui-fix
Quant fixes
2018-08-06 10:16:58 +01:00
d3f2836f48 Hide quantize options if original image. Fixes #120. 2018-08-02 16:59:13 +01:00
27722f77f9 Simplifying new object creation 2018-08-02 16:56:15 +01:00
3a0db14c40 Minor tweaks 2018-08-02 16:52:57 +01:00
e0dc1b48ec Creating new objects rather than mutating existing objects. Fixes #121. 2018-08-02 16:52:11 +01:00
009327c2c4 Merge pull request #115 from GoogleChromeLabs/imagequant
Implement image quantization via libimagequant
2018-08-01 12:50:08 +01:00
b16d60b52b No name on preprocessors 2018-08-01 12:36:31 +01:00
c550fe9283 Refactor preprocessors module 2018-08-01 12:32:45 +01:00
dce4fc70ac Clean up imagquant codec 2018-08-01 12:19:44 +01:00
b3f3ecbf28 Actually respect numColors lol 2018-07-31 12:46:32 +01:00
e8c0ddfc7f Remove lambda from JSX 2018-07-31 12:41:02 +01:00
a002b376af Hook up options UI to quantizer 2018-07-31 12:32:37 +01:00
2165383da4 Start integrating quantizer in the main data flow 2018-07-30 16:54:30 +01:00
5fbf6b297f Hardcode quantization pass 2018-07-30 16:28:37 +01:00
9d5ad83ff8 Transpile libimagequant 2018-07-30 15:51:57 +01:00
07f17dece2 Adding Travis Support (#113)
* Adds travis script that will build the project whenever the master branch is updated.

The key to upload is in travis env variable.

Note: This does not yet run the test suite, it will deploy if the build is successful.
Note: it has to build twice, because the first build will fail due to a loader issue.
2018-07-30 15:47:00 +01:00
f2f467ecb8 WebP encode options (#110)
* Flailing

* Holy shit struct binding

* Options in the encoder!

* Integrating webp options

* Addressing feedback

* This isn't needed anymore
2018-07-27 14:06:45 +01:00
2ea9e22b52 Merge pull request #91 from GoogleChromeLabs/webp-decoder
WASM WebpDecoder
2018-07-20 11:13:09 +01:00
4ee5572d2f Jake’s thoroughness is doing my head in :P 2018-07-20 11:12:42 +01:00
df7e112d22 Nits by Jake 2018-07-20 10:58:07 +01:00
13ac3ed5b2 Use createImageBitmap as hailmary 2018-07-20 10:56:40 +01:00
b7c223bc0d Remove baseline image decoders, refactor decodeFile 2018-07-20 10:56:40 +01:00
0f08121596 Implement mime type sniffing 2018-07-20 10:56:39 +01:00
b15545402a Adressing smaller review comments 2018-07-20 10:56:39 +01:00
b310c97044 Smaller inline webp image 2018-07-20 10:56:38 +01:00
307c6b05ae Write out logging command 2018-07-20 10:56:38 +01:00
77a6d21924 show decode errors reliably. 2018-07-20 10:56:37 +01:00
d22a343378 Actually move decoding to worker 2018-07-20 10:56:37 +01:00
790a5b580d Set up decoder infrastructure 2018-07-20 10:56:36 +01:00
6e8f8bbe41 Vertical two-up (#100)
* Fixing bad property name.

* Allowing two-up to work vertically at smaller widths.

* Switching to orientation attr

* Fixing type and getter/setter behaviour
2018-07-20 09:32:18 +01:00
cc9d01a9ab Basic webp integration (#103) 2018-07-17 10:37:42 +01:00
526520c399 Adds support for onpaste #97 (#98)
* Adds support for onpaste #97

+ file-drop listens for onpaste, if there is data and it matches the
   type it will use that and raise a custom event
+ Adds a new event on file drop 'onfiledrop'
+ App listens for this event and will map to onFileDrop

* Hoisting getFileData

* The linter seems to think Array.from is of type File, when it's not.

* Remove an entire type of event handler.
+ Removes onfilepaste, joins into 'onfiledrop'
+ Adds 'action' to let you distinguish between the paste or drop
+ Updates app so it just uses the one event.

* Fixing PR issues

+ null return types >>> undefiend.
+ FileDropAction type.
+ remove coercsion on the array types.
2018-07-16 14:00:47 +01:00
acbc31bc35 Merge pull request #96 from GoogleChromeLabs/fix-mjs-errors
Fix errors arising from strict .mjs handling
2018-07-10 10:59:06 -04:00
e8e151a926 Fix errors arising from strict .mjs handling. 2018-07-10 10:57:11 -04:00
835a537c55 Fix lint issues resulting from switching to airbnb (#94)
* Fix lint issues resulting from switching to airbnb.

* Case sensitivity change

* Fix lint script to actually lint tsx files
2018-07-10 14:01:09 +01:00
23ea9fad49 Merge pull request #89 from GoogleChromeLabs/mjs
Switch to .mjs for ES Modules.
2018-07-09 15:45:35 -04:00
491280935a Merge pull request #92 from GoogleChromeLabs/fix-workers
Rename encoding worker so it actually runs in a worker
2018-07-04 15:48:22 +01:00
900eda9a8e Rename encoding worker so it actually runs in a worker 2018-07-04 10:35:16 -04:00
38d0057833 Switch to .mjs for ES Modules. Fixes #88. 2018-07-04 10:02:20 -04:00
3867448aad All the encoders that Chrome/Safari/Firefox support out of the box. (#74)
* All the encoders that Chrome/Safari/Firefox support out of the box.

* Typo
2018-07-02 19:10:10 +01:00
807a76d443 Adding download support. Fixes #47. 2018-07-02 19:04:09 +01:00
3e26a0a3cc Fixing file drop for Safari & Edge (#73) 2018-07-02 18:55:46 +01:00
68729979e3 Adding browser's webp encoder (#72)
* Adding WebP (without feature detect in place)

* Adding WebP check

* Remove unused import
2018-07-02 15:49:52 +01:00
a09ec269b8 Adding native encoders (#71)
* Adding browser png

* Adding native JPEG & file size output

* Removing log

* Fixing blob typing

* Fix timing issue
2018-07-02 15:14:09 +01:00
3f18c927f1 Allow mouse wheel on two-up handle. Fixes #49 (#54) 2018-07-02 09:17:57 +01:00
9add650b75 Refactorings (#65)
* Refactorings

* Responding to feedback
2018-07-01 16:01:42 +01:00
195 changed files with 26969 additions and 1805 deletions

View File

@ -1,13 +0,0 @@
{
"plugins": [
"transform-class-properties",
"transform-react-constant-elements",
"transform-react-remove-prop-types",
[
"transform-react-jsx",
{
"pragma": "h"
}
]
]
}

View File

@ -1,5 +0,0 @@
{
"projects": {
"default": "squoosh-beta"
}
}

36
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View 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.

View 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.

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ node_modules
/*.log
*.scss.d.ts
*.css.d.ts
*.o

7
.travis.yml Normal file
View File

@ -0,0 +1,7 @@
language: node_js
node_js:
- node
- 10
- 8
cache: npm
script: npm run build || npm run build # scss ts definitions need to be generated before an actual build

View File

@ -1,5 +1,34 @@
# 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'll get an error on first build because of [a stupid bug we haven't fixed
yet](https://github.com/GoogleChromeLabs/squoosh/issues/251).
You can run the development server with:
```sh
npm start
```
[Squoosh]: https://squoosh.app

18
_headers.ejs Normal file
View 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

View File

@ -11,6 +11,6 @@ $ npm install
$ npm run build
```
This will build two files: `<codec name>_<enc or dec>.js` and `<codec name>_<enc or dec>.wasm`. It will most likely be necessary to set [`Module["locateFile"]`](https://kripken.github.io/emscripten-site/docs/api_reference/module.html#affecting-execution) to sucessfully load the `.wasm` file. When the `.js` file is loaded, a global `<codec name>_<enc or dec>` is created with the same API as an [Emscripten `Module`](https://kripken.github.io/emscripten-site/docs/api_reference/module.html).
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_palette.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

View 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
View 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 "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"

View 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>

View 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
View 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;

File diff suppressed because one or more lines are too long

Binary file not shown.

1147
codecs/imagequant/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View 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"
}
}

View File

@ -6,8 +6,6 @@
## Dependencies
- Docker
- Automake
- pkg-config
## Example
@ -19,26 +17,31 @@ See `example.html`
Returns the version of MozJPEG as a number. va.b.c is encoded as 0x0a0b0c
### `uint8_t* create_buffer(int width, int height)`
Allocates an RGBA buffer for an image with the given dimension.
### `void destroy_buffer(uint8_t* p)`
Frees a buffer created with `create_buffer`.
### `void encode(uint8_t* image_buffer, int image_width, int image_height, int quality)`
Encodes the given image with given dimension to JPEG. `quality` is a number between 0 and 100. The higher the number, the better the quality of the encoded image. The result is implicitly stored and can be accessed using the `get_result_*()` functions.
### `void free_result()`
Frees the result created by `encode()`.
### `int get_result_pointer()`
### `Uint8Array encode(std::string image_in, int image_width, int image_height, MozJpegOptions opts)`
Returns the pointer to the start of the buffer holding the encoded data.
Encodes the given image with given dimension to JPEG. Options looks like this:
### `int get_result_size()`
Returns the length of the buffer holding the encoded data.
```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
View 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 "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"

View File

@ -1,7 +1,7 @@
<!doctype html>
<script src='mozjpeg_enc.js'></script>
<script>
const Module = mozjpeg_enc();
const module = mozjpeg_enc();
async function loadImage(src) {
// Load image
@ -17,27 +17,27 @@
return ctx.getImageData(0, 0, img.width, img.height);
}
Module.onRuntimeInitialized = async _ => {
const api = {
version: Module.cwrap('version', 'number', []),
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
encode: Module.cwrap('encode', '', ['number', 'number', 'number', 'number']),
free_result: Module.cwrap('free_result', '', ['number']),
get_result_pointer: Module.cwrap('get_result_pointer', 'number', []),
get_result_size: Module.cwrap('get_result_size', 'number', []),
};
console.log('Version:', api.version().toString(16));
module.onRuntimeInitialized = async _ => {
console.log('Version:', module.version().toString(16));
const image = await loadImage('../example.png');
const p = api.create_buffer(image.width, image.height);
Module.HEAP8.set(image.data, p);
api.encode(p, image.width, image.height, 2);
const resultPointer = api.get_result_pointer();
const resultSize = api.get_result_size();
const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);
const result = new Uint8Array(resultView);
api.free_result(resultPointer);
api.destroy_buffer(p);
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);

View File

@ -1,17 +1,40 @@
#include "emscripten.h"
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <stdlib.h>
#include <inttypes.h>
#include <stdio.h>
#include <setjmp.h>
#include <string.h>
#include "jpeglib.h"
#include "config.h"
#include "jpeglib.h"
#include "cdjpeg.h"
// MozJPEG doesnt 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
using namespace emscripten;
// MozJPEG doesnt 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
EMSCRIPTEN_KEEPALIVE
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;
@ -28,27 +51,11 @@ int version() {
return version;
}
EMSCRIPTEN_KEEPALIVE
uint8_t* create_buffer(int width, int height) {
return malloc(width * height * 4 * sizeof(uint8_t));
}
uint8_t* last_result;
struct jpeg_compress_struct cinfo;
EMSCRIPTEN_KEEPALIVE
void destroy_buffer(uint8_t* p) {
free(p);
}
int result[2];
EMSCRIPTEN_KEEPALIVE
void encode(uint8_t* image_buffer, int image_width, int image_height, int quality) {
// Manually convert RGBA data into RGB
for(int y = 0; y < image_height; y++) {
for(int x = 0; x < image_width; x++) {
image_buffer[(y*image_width + x)*3 + 0] = image_buffer[(y*image_width + x)*4 + 0];
image_buffer[(y*image_width + x)*3 + 1] = image_buffer[(y*image_width + x)*4 + 1];
image_buffer[(y*image_width + x)*3 + 2] = image_buffer[(y*image_width + x)*4 + 2];
}
}
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
@ -61,7 +68,6 @@ void encode(uint8_t* image_buffer, int image_width, int image_height, int qualit
* compression/decompression processes, in existence at once. We refer
* to any one struct (and its associated working data) as a "JPEG object".
*/
struct jpeg_compress_struct cinfo;
/* This struct represents a JPEG error handler. It is declared separately
* because applications often want to supply a specialized error handler
* (see the second half of this file for an example). But here we just
@ -109,18 +115,57 @@ void encode(uint8_t* image_buffer, int image_width, int image_height, int qualit
*/
cinfo.image_width = image_width; /* image width and height, in pixels */
cinfo.image_height = image_height;
cinfo.input_components = 3; /* # of color components per pixel */
cinfo.in_color_space = JCS_RGB; /* colorspace of input image */
cinfo.input_components = 4; /* # of color components per pixel */
cinfo.in_color_space = JCS_EXT_RGBA; /* colorspace of input image */
/* Now use the library's routine to set default compression parameters.
* (You must set at least cinfo.in_color_space before calling this,
* since the defaults depend on the source color space.)
*/
jpeg_set_defaults(&cinfo);
/* Now you can set any non-default parameters you wish to.
* Here we just illustrate the use of quality (quantization table) scaling:
*/
jpeg_set_quality(&cinfo, quality, TRUE /* limit to baseline-JPEG values */);
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.
@ -136,7 +181,7 @@ void encode(uint8_t* image_buffer, int image_width, int image_height, int qualit
* To keep things simple, we pass one scanline per call; you can pass
* more if you wish, though.
*/
row_stride = image_width * 3; /* JSAMPLEs per row in image_buffer */
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.
@ -152,27 +197,38 @@ void encode(uint8_t* image_buffer, int image_width, int image_height, int qualit
jpeg_finish_compress(&cinfo);
/* Step 7: release JPEG compression object */
result[0] = (int)output;
result[1] = size;
/* This is an important step since it will release a good deal of memory. */
jpeg_destroy_compress(&cinfo);
last_result = output;
/* And we're done! */
return val(typed_memory_view(size, output));
}
EMSCRIPTEN_KEEPALIVE
void free_result() {
free(result[0]); // not sure if this is right with mozjpeg
/* This is an important step since it will release a good deal of memory. */
jpeg_destroy_compress(&cinfo);
}
EMSCRIPTEN_KEEPALIVE
int get_result_pointer() {
return result[0];
}
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)
;
EMSCRIPTEN_KEEPALIVE
int get_result_size() {
return result[1];
function("version", &version);
function("encode", &encode);
function("free_result", &free_result);
}

View File

@ -1 +1,8 @@
export default function(opts: EmscriptenWasm.ModuleOpts): EmscriptenWasm.Module;
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;

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -2,9 +2,7 @@
"name": "mozjpeg_enc",
"scripts": {
"install": "napa",
"build": "npm run build:library && npm run build:wasm",
"build:library": "cd node_modules/mozjpeg && autoreconf -fiv && docker run --rm -v $(pwd):/src trzeci/emscripten emconfigure ./configure --without-simd && docker run --rm -v $(pwd):/src trzeci/emscripten emmake make libjpeg.la",
"build:wasm": "docker run --rm -v $(pwd):/src trzeci/emscripten emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"cwrap\"]' -s ALLOW_MEMORY_GROWTH=1 -s MODULARIZE=1 -s 'EXPORT_NAME=\"mozjpeg_enc\"' -I node_modules/mozjpeg -o ./mozjpeg_enc.js mozjpeg_enc.c node_modules/mozjpeg/.libs/libjpeg.a"
"build": "docker run --rm -v $(pwd):/src trzeci/emscripten ./build.sh"
},
"napa": {
"mozjpeg": "mozilla/mozjpeg#v3.3.1"

2
codecs/optipng/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
build/
*.o

26
codecs/optipng/README.md Normal file
View File

@ -0,0 +1,26 @@
# OptiPNG
- Source: <https://sourceforge.net/project/optipng>
- Version: v0.7.7
## Dependencies
- Docker
## Example
See `example.html`
## API
### `int version()`
Returns the version of optipng as a number. va.b.c is encoded as 0x0a0b0c
### `ArrayBuffer compress(std::string buffer, {level})`;
`compress` will re-compress the given PNG image via `buffer`. `level` is a number between 0 and 7.
### `void free_result()`
Frees the result created by `compress()`.

87
codecs/optipng/build.sh Executable file
View 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 "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"

View 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>

View 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): Havent 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
View 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

File diff suppressed because one or more lines are too long

BIN
codecs/optipng/optipng.wasm Normal file

Binary file not shown.

1457
codecs/optipng/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View 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"
}
}

View File

@ -13,30 +13,10 @@ See `example.html`
Returns the version of libwebp as a number. va.b.c is encoded as 0x0a0b0c
### `uint8_t* create_buffer(int size)`
### `RawImage decode(std::string buffer)`
Allocates an buffer for the file data.
### `void destroy_buffer(uint8_t* p)`
Frees a buffer created with `create_buffer`.
### `void decode(uint8_t* img_in, int size)`
Decodes the given webp file into raw RGBA. The result is implicitly stored and can be accessed using the `get_result_*()` functions.
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()`.
### `int get_result_pointer()`
Returns the pointer to the start of the buffer holding the encoded data. Length is width x height x 4 bytes.
### `int get_result_width()`
Returns the width of the image.
### `int get_result_height()`
Returns the height of the image.

34
codecs/webp_dec/build.sh Executable file
View File

@ -0,0 +1,34 @@
#!/bin/bash
set -e
export OPTIMIZE="-Os"
export LDFLAGS="${OPTIMIZE}"
export CFLAGS="${OPTIMIZE}"
export CPPFLAGS="${OPTIMIZE}"
echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
(
emcc \
${OPTIMIZE} \
--bind \
-s ALLOW_MEMORY_GROWTH=1 \
-s MODULARIZE=1 \
-s 'EXPORT_NAME="webp_dec"' \
--std=c++11 \
-I node_modules/libwebp \
-o ./webp_dec.js \
node_modules/libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c \
-x c++ \
webp_dec.cpp
)
echo "============================================="
echo "Compiling wasm bindings done"
echo "============================================="
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
echo "Did you update your docker image?"
echo "Run \`docker pull trzeci/emscripten\`"
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"

View File

@ -9,35 +9,14 @@
}
Module.onRuntimeInitialized = async _ => {
const api = {
version: Module.cwrap('version', 'number', []),
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
decode: Module.cwrap('decode', '', ['number', 'number']),
free_result: Module.cwrap('free_result', '', ['number']),
get_result_pointer: Module.cwrap('get_result_pointer', 'number', []),
get_result_width: Module.cwrap('get_result_width', 'number', []),
get_result_height: Module.cwrap('get_result_height', 'number', []),
};
console.log('Version:', api.version().toString(16));
console.log('Version:', Module.version().toString(16));
const image = await loadFile('../example.webp');
const p = api.create_buffer(image.byteLength);
Module.HEAP8.set(new Uint8Array(image), p);
api.decode(p, image.byteLength);
const resultPointer = api.get_result_pointer();
if(resultPointer === 0) {
throw new Error("Could not decode image");
}
const resultWidth = api.get_result_width();
const resultHeight = api.get_result_height();
const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultWidth * resultHeight * 4);
const result = new Uint8ClampedArray(resultView);
const imageData = new ImageData(result, resultWidth, resultHeight);
api.free_result(resultPointer);
api.destroy_buffer(p);
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 = resultWidth;
canvas.height = resultHeight;
canvas.width = result.width;
canvas.height = result.height;
document.body.appendChild(canvas);
const ctx = canvas.getContext('2d');
ctx.putImageData(imageData, 0, 0);

View File

@ -2,7 +2,7 @@
"name": "webp_dec",
"scripts": {
"install": "napa",
"build": "docker run --rm -v $(pwd):/src trzeci/emscripten emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"cwrap\"]' -s ALLOW_MEMORY_GROWTH=1 -s MODULARIZE=1 -s 'EXPORT_NAME=\"webp_dec\"' -I node_modules/libwebp -o ./webp_dec.js webp_dec.c node_modules/libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c"
"build": "docker run --rm -v $(pwd):/src trzeci/emscripten ./build.sh"
},
"napa": {
"libwebp": "webmproject/libwebp#v1.0.0"

View File

@ -1,51 +0,0 @@
#include "emscripten.h"
#include "src/webp/decode.h"
#include "src/webp/demux.h"
#include <stdlib.h>
EMSCRIPTEN_KEEPALIVE
int version() {
return WebPGetDecoderVersion();
}
EMSCRIPTEN_KEEPALIVE
uint8_t* create_buffer(int size) {
return malloc(size);
}
EMSCRIPTEN_KEEPALIVE
void destroy_buffer(uint8_t* p) {
free(p);
}
int result[3];
EMSCRIPTEN_KEEPALIVE
void decode(uint8_t* img_in, int size) {
int width, height;
uint8_t* img_out = WebPDecodeRGBA(img_in, size, &width, &height);
result[0] = (int)img_out;
result[1] = width;
result[2] = height;
}
EMSCRIPTEN_KEEPALIVE
void free_result() {
WebPFree(result[0]);
}
EMSCRIPTEN_KEEPALIVE
int get_result_pointer() {
return result[0];
}
EMSCRIPTEN_KEEPALIVE
int get_result_width() {
return result[1];
}
EMSCRIPTEN_KEEPALIVE
int get_result_height() {
return result[2];
}

View 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
View 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;

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -17,26 +17,10 @@ See `example.html`
Returns the version of libwebp as a number. va.b.c is encoded as 0x0a0b0c
### `uint8_t* create_buffer(int width, int height)`
### `UInt8Array encode(uint8_t* image_buffer, int image_width, int image_height, WebPConfig config)`
Allocates an RGBA buffer for an image with the given dimension.
### `void destroy_buffer(uint8_t* p)`
Frees a buffer created with `create_buffer`.
### `void encode(uint8_t* image_buffer, int image_width, int image_height, float quality)`
Encodes the given image with given dimension to WebP. `quality` is a number between 0 and 100. The higher the number, the better the quality of the encoded image. The result is implicitly stored and can be accessed using the `get_result_*()` functions.
Encodes the given image with given dimension to WebP.
### `void free_result()`
Frees the result created by `encode()`.
### `int get_result_pointer()`
Returns the pointer to the start of the buffer holding the encoded data.
### `int get_result_size()`
Returns the length of the buffer holding the encoded data.
Frees the last result created by `encode()`.

34
codecs/webp_enc/build.sh Executable file
View File

@ -0,0 +1,34 @@
#!/bin/bash
set -e
export OPTIMIZE="-Os"
export LDFLAGS="${OPTIMIZE}"
export CFLAGS="${OPTIMIZE}"
export CPPFLAGS="${OPTIMIZE}"
echo "============================================="
echo "Compiling wasm bindings"
echo "============================================="
(
emcc \
${OPTIMIZE} \
--bind \
-s ALLOW_MEMORY_GROWTH=1 \
-s MODULARIZE=1 \
-s 'EXPORT_NAME="webp_enc"' \
--std=c++11 \
-I node_modules/libwebp \
-o ./webp_enc.js \
node_modules/libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c \
-x c++ \
webp_enc.cpp
)
echo "============================================="
echo "Compiling wasm bindings done"
echo "============================================="
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
echo "Did you update your docker image?"
echo "Run \`docker pull trzeci/emscripten\`"
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"

View File

@ -1,7 +1,7 @@
<!doctype html>
<script src='webp_enc.js'></script>
<script>
const Module = webp_enc();
const module = webp_enc();
async function loadImage(src) {
// Load image
@ -17,29 +17,43 @@
return ctx.getImageData(0, 0, img.width, img.height);
}
Module.onRuntimeInitialized = async _ => {
const api = {
version: Module.cwrap('version', 'number', []),
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
encode: Module.cwrap('encode', '', ['number', 'number', 'number', 'number']),
free_result: Module.cwrap('free_result', '', ['number']),
get_result_pointer: Module.cwrap('get_result_pointer', 'number', []),
get_result_size: Module.cwrap('get_result_size', 'number', []),
};
console.log('Version:', api.version().toString(16));
module.onRuntimeInitialized = async _ => {
console.log('Version:', module.version().toString(16));
const image = await loadImage('../example.png');
const p = api.create_buffer(image.width, image.height);
Module.HEAP8.set(image.data, p);
api.encode(p, image.width, image.height, 2);
const resultPointer = api.get_result_pointer();
const resultSize = api.get_result_size();
const resultView = new Uint8Array(Module.HEAP8.buffer, resultPointer, resultSize);
const result = new Uint8Array(resultView);
api.free_result(resultPointer);
api.destroy_buffer(p);
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;

View File

@ -2,7 +2,7 @@
"name": "webp_enc",
"scripts": {
"install": "napa",
"build": "docker run --rm -v $(pwd):/src trzeci/emscripten emcc -O3 -s WASM=1 -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"cwrap\"]' -s ALLOW_MEMORY_GROWTH=1 -s MODULARIZE=1 -s 'EXPORT_NAME=\"webp_enc\"' -I node_modules/libwebp -o ./webp_enc.js webp_enc.c node_modules/libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c"
"build": "docker run --rm -v $(pwd):/src trzeci/emscripten ./build.sh"
},
"napa": {
"libwebp": "webmproject/libwebp#v1.0.0"

View File

@ -1,44 +0,0 @@
#include "emscripten.h"
#include "src/webp/encode.h"
#include <stdlib.h>
EMSCRIPTEN_KEEPALIVE
int version() {
return WebPGetEncoderVersion();
}
EMSCRIPTEN_KEEPALIVE
uint8_t* create_buffer(int width, int height) {
return malloc(width * height * 4 * sizeof(uint8_t));
}
EMSCRIPTEN_KEEPALIVE
void destroy_buffer(uint8_t* p) {
free(p);
}
int result[2];
EMSCRIPTEN_KEEPALIVE
void encode(uint8_t* img_in, int width, int height, float quality) {
uint8_t* img_out;
size_t size;
size = WebPEncodeRGBA(img_in, width, height, width * 4, quality, &img_out);
result[0] = (int)img_out;
result[1] = size;
}
EMSCRIPTEN_KEEPALIVE
void free_result() {
WebPFree(result[0]);
}
EMSCRIPTEN_KEEPALIVE
int get_result_pointer() {
return result[0];
}
EMSCRIPTEN_KEEPALIVE
int get_result_size() {
return result[1];
}

View File

@ -0,0 +1,95 @@
#include <emscripten/bind.h>
#include <emscripten/val.h>
#include "src/webp/encode.h"
#include <stdlib.h>
#include <string.h>
#include <stdexcept>
using namespace emscripten;
int version() {
return WebPGetEncoderVersion();
}
uint8_t* last_result;
val encode(std::string img, int width, int height, WebPConfig config) {
uint8_t* img_in = (uint8_t*) img.c_str();
// A lot of this is duplicated from Encode in picture_enc.c
WebPPicture pic;
WebPMemoryWriter wrt;
int ok;
if (!WebPPictureInit(&pic)) {
// shouldn't happen, except if system installation is broken
throw std::runtime_error("Unexpected error");
}
pic.use_argb = !!config.lossless;
pic.width = width;
pic.height = height;
pic.writer = WebPMemoryWrite;
pic.custom_ptr = &wrt;
WebPMemoryWriterInit(&wrt);
ok = WebPPictureImportRGBA(&pic, (uint8_t*) img_in, width * 4) && WebPEncode(&config, &pic);
WebPPictureFree(&pic);
if (!ok) {
WebPMemoryWriterClear(&wrt);
throw std::runtime_error("Encode failed");
}
last_result = wrt.mem;
return val(typed_memory_view(wrt.size, wrt.mem));
}
void free_result() {
WebPFree(last_result);
}
EMSCRIPTEN_BINDINGS(my_module) {
enum_<WebPImageHint>("WebPImageHint")
.value("WEBP_HINT_DEFAULT", WebPImageHint::WEBP_HINT_DEFAULT)
.value("WEBP_HINT_PICTURE", WebPImageHint::WEBP_HINT_PICTURE)
.value("WEBP_HINT_PHOTO", WebPImageHint::WEBP_HINT_PHOTO)
.value("WEBP_HINT_GRAPH", WebPImageHint::WEBP_HINT_GRAPH)
;
value_object<WebPConfig>("WebPConfig")
.field("lossless", &WebPConfig::lossless)
.field("quality", &WebPConfig::quality)
.field("method", &WebPConfig::method)
.field("image_hint", &WebPConfig::image_hint)
.field("target_size", &WebPConfig::target_size)
.field("target_PSNR", &WebPConfig::target_PSNR)
.field("segments", &WebPConfig::segments)
.field("sns_strength", &WebPConfig::sns_strength)
.field("filter_strength", &WebPConfig::filter_strength)
.field("filter_sharpness", &WebPConfig::filter_sharpness)
.field("filter_type", &WebPConfig::filter_type)
.field("autofilter", &WebPConfig::autofilter)
.field("alpha_compression", &WebPConfig::alpha_compression)
.field("alpha_filtering", &WebPConfig::alpha_filtering)
.field("alpha_quality", &WebPConfig::alpha_quality)
.field("pass", &WebPConfig::pass)
.field("show_compressed", &WebPConfig::show_compressed)
.field("preprocessing", &WebPConfig::preprocessing)
.field("partitions", &WebPConfig::partitions)
.field("partition_limit", &WebPConfig::partition_limit)
.field("emulate_jpeg_size", &WebPConfig::emulate_jpeg_size)
.field("thread_level", &WebPConfig::thread_level)
.field("low_memory", &WebPConfig::low_memory)
.field("near_lossless", &WebPConfig::near_lossless)
.field("exact", &WebPConfig::exact)
.field("use_delta_palette", &WebPConfig::use_delta_palette)
.field("use_sharp_yuv", &WebPConfig::use_sharp_yuv)
;
function("version", &version);
function("encode", &encode);
function("free_result", &free_result);
}

9
codecs/webp_enc/webp_enc.d.ts vendored Normal file
View 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;

File diff suppressed because one or more lines are too long

Binary file not shown.

View 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
View 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);
}
};

View File

@ -1,54 +0,0 @@
{
"hosting": {
"public": "build",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
],
"headers": [
{
"source": "**/*.@(eot|otf|ttf|ttc|woff|font.css)",
"headers": [
{
"key": "Access-Control-Allow-Origin",
"value": "*"
}
]
},
{
"source": "**/*.@(jpg|jpeg|gif|png|ico)",
"headers": [
{
"key": "Cache-Control",
"value": "max-age=7200"
}
]
},
{
"source": "**/*.@(js|css|json|manifest|map)",
"headers": [
{
"key": "Cache-Control",
"value": "max-age=31536000"
}
]
},
{
"source": "sw.js",
"headers": [
{
"key": "Cache-Control",
"value": "max-age=0"
}
]
}
]
}
}

11
global.d.ts vendored
View File

@ -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;
}

16149
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,13 @@
{
"private": true,
"name": "squoosh",
"version": "0.0.0",
"version": "1.0.2",
"license": "apache-2.0",
"scripts": {
"build:mozjpeg_enc": "cd codecs/mozjpeg_enc && npm run build",
"build:codecs": "npm run build:mozjpeg_enc",
"start": "webpack serve --host 0.0.0.0 --hot",
"start": "webpack-dev-server --host 0.0.0.0 --hot",
"build": "webpack -p",
"lint": "tslint -c tslint.json -t verbose 'src/**/*.{ts,js}'",
"lintfix": "tslint -c tslint.json -t verbose --fix 'src/**/*.{ts,js}'"
"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}'"
},
"husky": {
"hooks": {
@ -17,58 +15,55 @@
}
},
"devDependencies": {
"@types/node": "^9.4.7",
"@types/node": "^10.12.6",
"@types/pretty-bytes": "^5.1.0",
"@types/webassembly-js-api": "0.0.1",
"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",
"@webcomponents/custom-elements": "^1.2.1",
"@webpack-cli/serve": "^0.1.2",
"assets-webpack-plugin": "^3.9.7",
"classnames": "^2.2.6",
"clean-webpack-plugin": "^1.0.0",
"comlink": "^3.0.3",
"copy-webpack-plugin": "^4.6.0",
"critters-webpack-plugin": "^2.0.1",
"css-loader": "^1.0.1",
"ejs": "^2.6.1",
"exports-loader": "^0.7.0",
"file-loader": "^1.1.11",
"html-webpack-plugin": "^3.0.6",
"husky": "^1.0.0-rc.9",
"file-drop-element": "^0.0.9",
"file-loader": "^2.0.0",
"html-webpack-plugin": "^3.2.0",
"husky": "^1.1.4",
"idb-keyval": "^3.1.0",
"if-env": "^1.0.4",
"linkstate": "^1.1.1",
"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",
"mini-css-extract-plugin": "^0.4.4",
"minimatch": "^3.0.4",
"node-sass": "^4.9.0",
"optimize-css-assets-webpack-plugin": "^5.0.1",
"pointer-tracker": "^2.0.3",
"preact": "^8.3.1",
"prerender-loader": "^1.2.0",
"pretty-bytes": "^5.1.0",
"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.10.0",
"tslint-config-airbnb": "^5.9.2",
"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.1.0",
"ts-loader": "^5.3.0",
"tslint": "^5.11.0",
"tslint-config-airbnb": "^5.11.0",
"tslint-config-semistandard": "^7.0.0",
"tslint-react": "^3.5.1",
"typescript": "^2.7.2",
"tslint-react": "^3.6.0",
"typescript": "^3.1.6",
"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"
},
"dependencies": {
"classnames": "^2.2.5",
"comlink": "^3.0.3",
"comlink-loader": "^1.0.0",
"material-components-web": "^0.32.0",
"preact": "^8.2.7",
"preact-i18n": "^1.2.0",
"preact-material-components": "^1.3.7",
"preact-router": "^2.6.0"
"url-loader": "^1.1.2",
"webpack": "^4.25.1",
"webpack-bundle-analyzer": "^3.0.3",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.10",
"worker-plugin": "^1.1.1"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 15 KiB

BIN
src/assets/icon-large.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
src/assets/icon-small.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

View 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);

View File

@ -0,0 +1,6 @@
import { mimeType } from './encoder-meta';
import { canvasEncode } from '../../lib/util';
export function encode(data: ImageData) {
return canvasEncode(data, mimeType);
}

View 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);

View File

@ -0,0 +1,6 @@
import { mimeType } from './encoder-meta';
import { canvasEncode } from '../../lib/util';
export function encode(data: ImageData) {
return canvasEncode(data, mimeType);
}

View 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);

View File

@ -0,0 +1,6 @@
import { mimeType } from './encoder-meta';
import { canvasEncode } from '../../lib/util';
export function encode(data: ImageData) {
return canvasEncode(data, mimeType);
}

View 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 };

View 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);
}

View File

@ -0,0 +1,3 @@
import qualityOption from '../generic/quality-option';
export default qualityOption({ min: 0, max: 1, step: 0.01 });

View 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);

View File

@ -0,0 +1,6 @@
import { mimeType } from './encoder-meta';
import { canvasEncode } from '../../lib/util';
export function encode(data: ImageData) {
return canvasEncode(data, mimeType);
}

View 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 = {};

View File

@ -0,0 +1,6 @@
import { mimeType } from './encoder-meta';
import { canvasEncode } from '../../lib/util';
export function encode(data: ImageData) {
return canvasEncode(data, mimeType);
}

View 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);

View File

@ -0,0 +1,6 @@
import { mimeType } from './encoder-meta';
import { canvasEncode } from '../../lib/util';
export function encode(data: ImageData) {
return canvasEncode(data, mimeType);
}

View 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);

View 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);
}

View 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
View 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");
}
}

View File

@ -1,13 +1,78 @@
import * as mozJPEG from './mozjpeg/encoder';
import * as identity from './identity/encoder';
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 EncoderState = identity.EncoderState | mozJPEG.EncoderState;
export type EncoderOptions = identity.EncodeOptions | mozJPEG.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;
});

View 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;
}

View File

@ -0,0 +1,13 @@
import { canvasEncode } from '../../lib/util';
export async function canvasEncodeTest(mimeType: string) {
try {
const blob = await canvasEncode(new ImageData(1, 1), mimeType);
// According to the spec, the blob should be null if the format isn't supported…
if (!blob) return false;
// …but Safari & Firefox fall back to PNG, so we need to check the mime type.
return blob.type === mimeType;
} catch (err) {
return false;
}
}

View File

@ -0,0 +1,91 @@
import { h, Component } from 'preact';
import { bind } from '../../lib/initial-util';
import { inputFieldValueAsNumber, konami, preventDefault } from '../../lib/util';
import { QuantizeOptions } from './processor-meta';
import * as style from '../../components/Options/style.scss';
import Expander from '../../components/expander';
import Select from '../../components/select';
import Range from '../../components/range';
const konamiPromise = konami();
interface Props {
options: QuantizeOptions;
onChange(newOptions: QuantizeOptions): void;
}
interface State {
extendedSettings: boolean;
}
export default class QuantizerOptions extends Component<Props, State> {
state: State = { extendedSettings: false };
componentDidMount() {
konamiPromise.then(() => {
this.setState({ extendedSettings: true });
});
}
@bind
onChange(event: Event) {
const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement;
const { options } = this.props;
const newOptions: QuantizeOptions = {
zx: inputFieldValueAsNumber(form.zx, options.zx),
maxNumColors: inputFieldValueAsNumber(form.maxNumColors, options.maxNumColors),
dither: inputFieldValueAsNumber(form.dither),
};
this.props.onChange(newOptions);
}
render({ options }: Props, { extendedSettings }: State) {
return (
<form class={style.optionsSection} onSubmit={preventDefault}>
<Expander>
{extendedSettings ?
<label class={style.optionTextFirst}>
Type:
<Select
name="zx"
value={'' + options.zx}
onChange={this.onChange}
>
<option value="0">Standard</option>
<option value="1">ZX</option>
</Select>
</label>
: null}
</Expander>
<Expander>
{options.zx ? null :
<div class={style.optionOneCell}>
<Range
name="maxNumColors"
min="2"
max="256"
value={options.maxNumColors}
onInput={this.onChange}
>
Colors:
</Range>
</div>
}
</Expander>
<div class={style.optionOneCell}>
<Range
name="dither"
min="0"
max="1"
step="0.01"
value={options.dither}
onInput={this.onChange}
>
Dithering:
</Range>
</div>
</form>
);
}
}

View File

@ -0,0 +1,11 @@
export interface QuantizeOptions {
zx: number;
maxNumColors: number;
dither: number;
}
export const defaultOptions: QuantizeOptions = {
zx: 0,
maxNumColors: 256,
dither: 1.0,
};

View File

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

View File

@ -1,79 +0,0 @@
import mozjpeg_enc from '../../../codecs/mozjpeg_enc/mozjpeg_enc';
// Using require() so TypeScript doesnt complain about this not being a module.
import { EncodeOptions } from './encoder';
const wasmBinaryUrl = require('../../../codecs/mozjpeg_enc/mozjpeg_enc.wasm');
// API exposed by wasm module. Details in the codecs README.
interface ModuleAPI {
version(): number;
create_buffer(width: number, height: number): number;
destroy_buffer(pointer: number): void;
encode(buffer: number, width: number, height: number, quality: number): void;
free_result(): void;
get_result_pointer(): number;
get_result_size(): number;
}
export default class MozJpegEncoder {
private emscriptenModule: Promise<EmscriptenWasm.Module>;
private api: Promise<ModuleAPI>;
constructor() {
this.emscriptenModule = new Promise((resolve) => {
const m = mozjpeg_enc({
// Just to be safe, dont automatically invoke any wasm functions
noInitialRun: false,
locateFile(url: string): string {
// Redirect the request for the wasm binary to whatever webpack gave us.
if (url.endsWith('.wasm')) {
return wasmBinaryUrl;
}
return url;
},
onRuntimeInitialized() {
// An Emscripten is a then-able that, for some reason, `then()`s itself,
// causing an infite loop when you wrap it in a real promise. Deleten the `then`
// prop solves this for now.
// See: https://github.com/kripken/emscripten/blob/incoming/src/postamble.js#L129
// TODO(surma@): File a bug with Emscripten on this.
delete (m as any).then;
resolve(m);
},
});
});
this.api = (async () => {
// Not sure why, but TypeScript complains that I am using
// `emscriptenModule` before its getting assigned, which is clearly not
// true :shrug: Using `any`
const m = await (this as any).emscriptenModule;
return {
version: m.cwrap('version', 'number', []),
create_buffer: m.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: m.cwrap('destroy_buffer', '', ['number']),
encode: m.cwrap('encode', '', ['number', 'number', 'number', 'number']),
free_result: m.cwrap('free_result', '', []),
get_result_pointer: m.cwrap('get_result_pointer', 'number', []),
get_result_size: m.cwrap('get_result_size', 'number', []),
};
})();
}
async encode(data: ImageData, options: EncodeOptions): Promise<ArrayBuffer> {
const m = await this.emscriptenModule;
const api = await this.api;
const p = api.create_buffer(data.width, data.height);
m.HEAP8.set(data.data, p);
api.encode(p, data.width, data.height, options.quality);
const resultPointer = api.get_result_pointer();
const resultSize = api.get_result_size();
const resultView = new Uint8Array(m.HEAP8.buffer, resultPointer, resultSize);
const result = new Uint8Array(resultView);
api.free_result();
api.destroy_buffer(p);
// wasm cant run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
return result.buffer as ArrayBuffer;
}
}

View File

@ -0,0 +1,49 @@
export enum MozJpegColorSpace {
GRAYSCALE = 1,
RGB,
YCbCr,
}
export interface EncodeOptions {
quality: number;
baseline: boolean;
arithmetic: boolean;
progressive: boolean;
optimize_coding: boolean;
smoothing: number;
color_space: MozJpegColorSpace;
quant_table: number;
trellis_multipass: boolean;
trellis_opt_zero: boolean;
trellis_opt_table: boolean;
trellis_loops: number;
auto_subsample: boolean;
chroma_subsample: number;
separate_chroma_quality: boolean;
chroma_quality: number;
}
export interface EncoderState { type: typeof type; options: EncodeOptions; }
export const type = 'mozjpeg';
export const label = 'MozJPEG';
export const mimeType = 'image/jpeg';
export const extension = 'jpg';
export const defaultOptions: EncodeOptions = {
quality: 75,
baseline: false,
arithmetic: false,
progressive: true,
optimize_coding: true,
smoothing: 0,
color_space: MozJpegColorSpace.YCbCr,
quant_table: 3,
trellis_multipass: false,
trellis_opt_zero: false,
trellis_opt_table: false,
trellis_loops: 1,
auto_subsample: true,
chroma_subsample: 2,
separate_chroma_quality: false,
chroma_quality: 75,
};

View File

@ -1,16 +1,18 @@
import EncoderWorker from './EncoderWorker';
import mozjpeg_enc, { MozJPEGModule } from '../../../codecs/mozjpeg_enc/mozjpeg_enc';
import wasmUrl from '../../../codecs/mozjpeg_enc/mozjpeg_enc.wasm';
import { EncodeOptions } from './encoder-meta';
import { initWasmModule } from '../util';
export interface EncodeOptions { quality: number; }
export interface EncoderState { type: typeof type; options: EncodeOptions; }
let emscriptenModule: Promise<MozJPEGModule>;
export const type = 'mozjpeg';
export const label = 'MozJPEG';
export const mimeType = 'image/jpeg';
export const extension = 'jpg';
export const defaultOptions: EncodeOptions = { quality: 7 };
export async function encode(data: ImageData, options: EncodeOptions): Promise<ArrayBuffer> {
if (!emscriptenModule) emscriptenModule = initWasmModule(mozjpeg_enc, wasmUrl);
export async function encode(data: ImageData, options: EncodeOptions) {
// We need to await this because it's been comlinked.
const encoder = await new EncoderWorker();
return encoder.encode(data, options);
const module = await emscriptenModule;
const resultView = module.encode(data.data, data.width, data.height, options);
const result = new Uint8Array(resultView);
module.free_result();
// wasm cant run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
return result.buffer as ArrayBuffer;
}

View File

@ -1,35 +1,258 @@
import { h, Component } from 'preact';
import { EncodeOptions } from './encoder';
import { bind } from '../../lib/util';
import { bind } from '../../lib/initial-util';
import { inputFieldChecked, inputFieldValueAsNumber, preventDefault } from '../../lib/util';
import { EncodeOptions, MozJpegColorSpace } from './encoder-meta';
import * as style from '../../components/Options/style.scss';
import Checkbox from '../../components/checkbox';
import Expander from '../../components/expander';
import Select from '../../components/select';
import Range from '../../components/range';
import linkState from 'linkstate';
type Props = {
options: EncodeOptions,
onChange(newOptions: EncodeOptions): void
};
interface Props {
options: EncodeOptions;
onChange(newOptions: EncodeOptions): void;
}
interface State {
showAdvanced: boolean;
}
export default class MozJPEGEncoderOptions extends Component<Props, State> {
state: State = {
showAdvanced: false,
};
export default class MozJpegCodecOptions extends Component<Props, {}> {
@bind
onChange(event: Event) {
const el = event.currentTarget as HTMLInputElement;
this.props.onChange({ quality: Number(el.value) });
const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement;
const { options } = this.props;
const newOptions: EncodeOptions = {
// Copy over options the form doesn't currently care about, eg arithmetic
...this.props.options,
// And now stuff from the form:
// .checked
baseline: inputFieldChecked(form.baseline, options.baseline),
progressive: inputFieldChecked(form.progressive, options.progressive),
optimize_coding: inputFieldChecked(form.optimize_coding, options.optimize_coding),
trellis_multipass: inputFieldChecked(form.trellis_multipass, options.trellis_multipass),
trellis_opt_zero: inputFieldChecked(form.trellis_opt_zero, options.trellis_opt_zero),
trellis_opt_table: inputFieldChecked(form.trellis_opt_table, options.trellis_opt_table),
auto_subsample: inputFieldChecked(form.auto_subsample, options.auto_subsample),
separate_chroma_quality:
inputFieldChecked(form.separate_chroma_quality, options.separate_chroma_quality),
// .value
quality: inputFieldValueAsNumber(form.quality, options.quality),
chroma_quality: inputFieldValueAsNumber(form.chroma_quality, options.chroma_quality),
chroma_subsample: inputFieldValueAsNumber(form.chroma_subsample, options.chroma_subsample),
smoothing: inputFieldValueAsNumber(form.smoothing, options.smoothing),
color_space: inputFieldValueAsNumber(form.color_space, options.color_space),
quant_table: inputFieldValueAsNumber(form.quant_table, options.quant_table),
trellis_loops: inputFieldValueAsNumber(form.trellis_loops, options.trellis_loops),
};
this.props.onChange(newOptions);
}
render({ options }: Props) {
render({ options }: Props, { showAdvanced }: State) {
// I'm rendering both lossy and lossless forms, as it becomes much easier when
// gathering the data.
return (
<div>
<label>
Quality:
<input
<form class={style.optionsSection} onSubmit={preventDefault}>
<div class={style.optionOneCell}>
<Range
name="quality"
type="range"
min="1"
min="0"
max="100"
step="1"
value={'' + options.quality}
onChange={this.onChange}
value={options.quality}
onInput={this.onChange}
>
Quality:
</Range>
</div>
<label class={style.optionInputFirst}>
<Checkbox
checked={showAdvanced}
onChange={linkState(this, 'showAdvanced')}
/>
Show advanced settings
</label>
</div>
<Expander>
{showAdvanced ?
<div>
<label class={style.optionTextFirst}>
Channels:
<Select
name="color_space"
value={options.color_space}
onChange={this.onChange}
>
<option value={MozJpegColorSpace.GRAYSCALE}>Grayscale</option>
<option value={MozJpegColorSpace.RGB}>RGB</option>
<option value={MozJpegColorSpace.YCbCr}>YCbCr</option>
</Select>
</label>
<Expander>
{options.color_space === MozJpegColorSpace.YCbCr ?
<div>
<label class={style.optionInputFirst}>
<Checkbox
name="auto_subsample"
checked={options.auto_subsample}
onChange={this.onChange}
/>
Auto subsample chroma
</label>
<Expander>
{options.auto_subsample ? null :
<div class={style.optionOneCell}>
<Range
name="chroma_subsample"
min="1"
max="4"
value={options.chroma_subsample}
onInput={this.onChange}
>
Subsample chroma by:
</Range>
</div>
}
</Expander>
<label class={style.optionInputFirst}>
<Checkbox
name="separate_chroma_quality"
checked={options.separate_chroma_quality}
onChange={this.onChange}
/>
Separate chroma quality
</label>
<Expander>
{options.separate_chroma_quality ?
<div class={style.optionOneCell}>
<Range
name="chroma_quality"
min="0"
max="100"
value={options.chroma_quality}
onInput={this.onChange}
>
Chroma quality:
</Range>
</div>
: null
}
</Expander>
</div>
: null
}
</Expander>
<label class={style.optionInputFirst}>
<Checkbox
name="baseline"
checked={options.baseline}
onChange={this.onChange}
/>
Pointless spec compliance
</label>
<Expander>
{options.baseline ? null :
<label class={style.optionInputFirst}>
<Checkbox
name="progressive"
checked={options.progressive}
onChange={this.onChange}
/>
Progressive rendering
</label>
}
</Expander>
<Expander>
{options.baseline ?
<label class={style.optionInputFirst}>
<Checkbox
name="optimize_coding"
checked={options.optimize_coding}
onChange={this.onChange}
/>
Optimize Huffman table
</label>
: null
}
</Expander>
<div class={style.optionOneCell}>
<Range
name="smoothing"
min="0"
max="100"
value={options.smoothing}
onInput={this.onChange}
>
Smoothing:
</Range>
</div>
<label class={style.optionTextFirst}>
Quantization:
<Select
name="quant_table"
value={options.quant_table}
onChange={this.onChange}
>
<option value="0">JPEG Annex K</option>
<option value="1">Flat</option>
<option value="2">MSSIM-tuned Kodak</option>
<option value="3">ImageMagick</option>
<option value="4">PSNR-HVS-M-tuned Kodak</option>
<option value="5">Klein et al</option>
<option value="6">Watson et al</option>
<option value="7">Ahumada et al</option>
<option value="8">Peterson et al</option>
</Select>
</label>
<label class={style.optionInputFirst}>
<Checkbox
name="trellis_multipass"
checked={options.trellis_multipass}
onChange={this.onChange}
/>
Trellis multipass
</label>
<Expander>
{options.trellis_multipass ?
<label class={style.optionInputFirst}>
<Checkbox
name="trellis_opt_zero"
checked={options.trellis_opt_zero}
onChange={this.onChange}
/>
Optimize zero block runs
</label>
: null
}
</Expander>
<label class={style.optionInputFirst}>
<Checkbox
name="trellis_opt_table"
checked={options.trellis_opt_table}
onChange={this.onChange}
/>
Optimize after trellis quantization
</label>
<div class={style.optionOneCell}>
<Range
name="trellis_loops"
min="1"
max="50"
value={options.trellis_loops}
onInput={this.onChange}
>
Trellis quantization passes:
</Range>
</div>
</div>
: null
}
</Expander>
</form>
);
}
}

View File

@ -0,0 +1,13 @@
export interface EncodeOptions {
level: number;
}
export interface EncoderState { type: typeof type; options: EncodeOptions; }
export const type = 'png';
export const label = 'OptiPNG';
export const mimeType = 'image/png';
export const extension = 'png';
export const defaultOptions: EncodeOptions = {
level: 2,
};

View File

@ -0,0 +1,18 @@
import optipng, { OptiPngModule } from '../../../codecs/optipng/optipng';
import wasmUrl from '../../../codecs/optipng/optipng.wasm';
import { EncodeOptions } from './encoder-meta';
import { initWasmModule } from '../util';
let emscriptenModule: Promise<OptiPngModule>;
export async function compress(data: BufferSource, options: EncodeOptions): Promise<ArrayBuffer> {
if (!emscriptenModule) emscriptenModule = initWasmModule(optipng, wasmUrl);
const module = await emscriptenModule;
const resultView = module.compress(data, options);
const result = new Uint8Array(resultView);
module.free_result();
// wasm cant run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
return result.buffer as ArrayBuffer;
}

View File

@ -0,0 +1,42 @@
import { h, Component } from 'preact';
import { bind } from '../../lib/initial-util';
import { inputFieldValueAsNumber, preventDefault } from '../../lib/util';
import { EncodeOptions } from './encoder-meta';
import Range from '../../components/range';
import * as style from '../../components/Options/style.scss';
type Props = {
options: EncodeOptions;
onChange(newOptions: EncodeOptions): void;
};
export default class OptiPNGEncoderOptions extends Component<Props, {}> {
@bind
onChange(event: Event) {
const form = (event.currentTarget as HTMLInputElement).closest('form') as HTMLFormElement;
const options: EncodeOptions = {
level: inputFieldValueAsNumber(form.level),
};
this.props.onChange(options);
}
render({ options }: Props) {
return (
<form class={style.optionsSection} onSubmit={preventDefault}>
<div class={style.optionOneCell}>
<Range
name="level"
min="0"
max="7"
step="1"
value={options.level}
onInput={this.onChange}
>
Effort:
</Range>
</div>
</form>
);
}
}

View File

@ -0,0 +1,23 @@
import {
QuantizeOptions, defaultOptions as quantizerDefaultOptions,
} from './imagequant/processor-meta';
import { ResizeOptions, defaultOptions as resizeDefaultOptions } from './resize/processor-meta';
interface Enableable {
enabled: boolean;
}
export interface PreprocessorState {
quantizer: Enableable & QuantizeOptions;
resize: Enableable & ResizeOptions;
}
export const defaultPreprocessorState: PreprocessorState = {
quantizer: {
enabled: false,
...quantizerDefaultOptions,
},
resize: {
enabled: false,
...resizeDefaultOptions,
},
};

View File

@ -0,0 +1,56 @@
import { expose } from 'comlink';
import { EncodeOptions as MozJPEGEncoderOptions } from './mozjpeg/encoder-meta';
import { QuantizeOptions } from './imagequant/processor-meta';
import { EncodeOptions as OptiPNGEncoderOptions } from './optipng/encoder-meta';
import { EncodeOptions as WebPEncoderOptions } from './webp/encoder-meta';
async function mozjpegEncode(
data: ImageData, options: MozJPEGEncoderOptions,
): Promise<ArrayBuffer> {
const { encode } = await import(
/* webpackChunkName: "process-mozjpeg-enc" */
'./mozjpeg/encoder',
);
return encode(data, options);
}
async function quantize(data: ImageData, opts: QuantizeOptions): Promise<ImageData> {
const { process } = await import(
/* webpackChunkName: "process-imagequant" */
'./imagequant/processor',
);
return process(data, opts);
}
async function optiPngEncode(
data: BufferSource, options: OptiPNGEncoderOptions,
): Promise<ArrayBuffer> {
const { compress } = await import(
/* webpackChunkName: "process-optipng" */
'./optipng/encoder',
);
return compress(data, options);
}
async function webpEncode(
data: ImageData, options: WebPEncoderOptions,
): Promise<ArrayBuffer> {
const { encode } = await import(
/* webpackChunkName: "process-webp-enc" */
'./webp/encoder',
);
return encode(data, options);
}
async function webpDecode(data: ArrayBuffer): Promise<ImageData> {
const { decode } = await import(
/* webpackChunkName: "process-webp-dec" */
'./webp/decoder',
);
return decode(data);
}
const exports = { mozjpegEncode, quantize, optiPngEncode, webpEncode, webpDecode };
export type ProcessorWorkerApi = typeof exports;
expose(exports, self);

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