Compare commits

...

8 Commits

Author SHA1 Message Date
4e04053e4c LibSquoosh: add smoke test 2021-09-07 14:26:36 -04:00
255dfa434a Update pointer-tracker (#1147) 2021-09-07 14:43:19 +01:00
0bef05bcd4 Remove origin trial header 2021-09-06 15:00:48 +01:00
00cfdafdf3 Merge pull request #1138 from jcao219/fix-windows-path
Use pathify to calculate absolute paths for cross-platform compatibility
2021-08-31 13:34:28 +01:00
92f52319da Use pathify to calculate absolute paths for cross-platform compatibility 2021-08-31 00:18:21 -07:00
023304803f JPEG-XL: Better 'effort' and adding noise synth (#1039) 2021-08-27 14:18:54 +01:00
6b08cd2355 Aliasing toggle button (#1132) 2021-08-26 16:44:20 +01:00
db1a5138e6 Fix pinch zoom on iOS (#1133) 2021-08-26 15:59:42 +01:00
29 changed files with 372 additions and 2168 deletions

Binary file not shown.

Binary file not shown.

View File

@ -10,15 +10,13 @@ using namespace emscripten;
thread_local const val Uint8Array = val::global("Uint8Array");
struct JXLOptions {
// 1 = slowest
// 7 = fastest
int speed;
int effort;
float quality;
bool progressive;
int epf;
int nearLossless;
bool lossyPalette;
size_t decodingSpeedTier;
float photonNoiseIso;
};
val encode(std::string image, int width, int height, JXLOptions options) {
@ -33,11 +31,14 @@ val encode(std::string image, int width, int height, JXLOptions options) {
pool_ptr = &pool;
#endif
cparams.epf = options.epf;
cparams.speed_tier = static_cast<jxl::SpeedTier>(options.speed);
cparams.decoding_speed_tier = options.decodingSpeedTier;
size_t st = 10 - options.effort;
cparams.speed_tier = jxl::SpeedTier(st);
if (options.lossyPalette || options.nearLossless) {
cparams.epf = options.epf;
cparams.decoding_speed_tier = options.decodingSpeedTier;
cparams.photon_noise_iso = options.photonNoiseIso;
if (options.lossyPalette) {
cparams.lossy_palette = true;
cparams.palette_colors = 0;
cparams.options.predictor = jxl::Predictor::Zero;
@ -106,12 +107,12 @@ val encode(std::string image, int width, int height, JXLOptions options) {
EMSCRIPTEN_BINDINGS(my_module) {
value_object<JXLOptions>("JXLOptions")
.field("speed", &JXLOptions::speed)
.field("effort", &JXLOptions::effort)
.field("quality", &JXLOptions::quality)
.field("progressive", &JXLOptions::progressive)
.field("nearLossless", &JXLOptions::nearLossless)
.field("lossyPalette", &JXLOptions::lossyPalette)
.field("decodingSpeedTier", &JXLOptions::decodingSpeedTier)
.field("photonNoiseIso", &JXLOptions::photonNoiseIso)
.field("epf", &JXLOptions::epf);
function("encode", &encode);

View File

@ -1,11 +1,11 @@
export interface EncodeOptions {
speed: number;
effort: number;
quality: number;
progressive: boolean;
epf: number;
nearLossless: number;
lossyPalette: boolean;
decodingSpeedTier: number;
photonNoiseIso: number;
}
export interface JXLModule extends EmscriptenWasm.Module {

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,8 @@
"/build/*"
],
"scripts": {
"build": "rollup -c"
"build": "rollup -c",
"test": "uvu -r ts-node/register test"
},
"keywords": [],
"author": "Google Chrome Developers <chromium-dev@google.com>",
@ -34,7 +35,9 @@
"@types/node": "^15.6.1",
"rollup": "^2.46.0",
"rollup-plugin-terser": "^7.0.2",
"ts-node": "^10.2.1",
"typescript": "^4.1.3",
"uvu": "^0.5.1",
"which": "^2.0.2"
}
}

View File

@ -19,7 +19,7 @@ export function instantiateEmscriptenWasm<T extends EmscriptenWasm.Module>(
// These will have changed in the bundling process and
// we need to inject them here.
if (requestPath.endsWith('.wasm')) return pathify(path);
if (requestPath.endsWith('.worker.js')) return new URL(workerJS).pathname;
if (requestPath.endsWith('.worker.js')) return pathify(workerJS);
return requestPath;
},
});

View File

@ -0,0 +1,43 @@
import * as path from 'path';
import { test } from 'uvu';
import * as assert from 'uvu/assert';
import { ImagePool } from '..';
let imagePool: ImagePool;
test.after.each(async () => {
if (imagePool) {
try {
await imagePool.close();
} catch (e) {}
}
imagePool = undefined;
});
test('smoke test', async () => {
imagePool = new ImagePool(1);
const imagePath = path.resolve(__dirname, '../../icon-large-maskable.png');
const image = imagePool.ingestImage(imagePath);
const { bitmap } = await image.decoded;
assert.equal(bitmap.width, 1024);
await image.preprocess({
resize: {
enabled: true,
width: 100,
},
});
await image.encode({
mozjpeg: {},
});
const { size } = await image.encodedWith.mozjpeg;
// resulting image is 1554b
assert.ok(size > 500);
assert.ok(size < 5000);
});
test.run();

View File

@ -5,5 +5,12 @@
"types": ["node"],
"allowJs": true
},
"include": ["src/**/*", "../codecs/**/*"]
"include": ["src/**/*", "../codecs/**/*"],
"ts-node": {
"transpileOnly": true,
"compilerOptions": {
"module": "commonjs"
},
"include": ["tests/**/*"]
}
}

28
package-lock.json generated
View File

@ -32,7 +32,7 @@
"lodash.camelcase": "^4.3.0",
"mime-types": "^2.1.28",
"npm-run-all": "^4.1.5",
"pointer-tracker": "^2.4.0",
"pointer-tracker": "^2.5.3",
"postcss": "^7.0.35",
"postcss-modules": "^3.2.2",
"postcss-nested": "^4.2.3",
@ -48,6 +48,20 @@
"which": "^2.0.2"
}
},
"../pointer-tracker": {
"version": "2.5.0",
"extraneous": true,
"license": "Apache-2.0",
"devDependencies": {
"husky": "^4.2.5",
"lint-staged": "^10.2.11",
"prettier": "^2.0.5",
"rollup": "^2.23.1",
"rollup-plugin-terser": "^7.0.0",
"rollup-plugin-typescript2": "^0.27.2",
"typescript": "^3.9.7"
}
},
"node_modules/@babel/code-frame": {
"version": "7.10.4",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
@ -3871,9 +3885,9 @@
}
},
"node_modules/pointer-tracker": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/pointer-tracker/-/pointer-tracker-2.4.0.tgz",
"integrity": "sha512-pWI2tpaM/XNtc9mUTv42Rmjf6mkHvE8LT5DDEq0G7baPNhxNM9E3CepubPplSoSLk9E5bwQrAMyDcPVmJyTW4g==",
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/pointer-tracker/-/pointer-tracker-2.5.3.tgz",
"integrity": "sha512-LiJUeIbzk4dXq678YeyrZ++mdY17q4n/2sBHfU9wIuvmSzdiPgMvmvWN2g8mY4J7YwYOIrqrZUWP/MfFHVwYtg==",
"dev": true
},
"node_modules/postcss": {
@ -11946,9 +11960,9 @@
}
},
"pointer-tracker": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/pointer-tracker/-/pointer-tracker-2.4.0.tgz",
"integrity": "sha512-pWI2tpaM/XNtc9mUTv42Rmjf6mkHvE8LT5DDEq0G7baPNhxNM9E3CepubPplSoSLk9E5bwQrAMyDcPVmJyTW4g==",
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/pointer-tracker/-/pointer-tracker-2.5.3.tgz",
"integrity": "sha512-LiJUeIbzk4dXq678YeyrZ++mdY17q4n/2sBHfU9wIuvmSzdiPgMvmvWN2g8mY4J7YwYOIrqrZUWP/MfFHVwYtg==",
"dev": true
},
"postcss": {

View File

@ -32,7 +32,7 @@
"lodash.camelcase": "^4.3.0",
"mime-types": "^2.1.28",
"npm-run-all": "^4.1.5",
"pointer-tracker": "^2.4.0",
"pointer-tracker": "^2.5.3",
"postcss": "^7.0.35",
"postcss-modules": "^3.2.2",
"postcss-nested": "^4.2.3",

View File

@ -20,7 +20,7 @@ const REFLECTED_ATTRIBUTES = [
'disabled',
];
function getPrescision(value: string): number {
function getPrecision(value: string): number {
const afterDecimal = value.split('.')[1];
return afterDecimal ? afterDecimal.length : 0;
}
@ -112,18 +112,24 @@ class RangeInputElement extends HTMLElement {
this.dispatchEvent(retargetted);
};
private _getDisplayValue(value: number): string {
if (value >= 10000) return (value / 1000).toFixed(1) + 'k';
const labelPrecision =
Number(this.labelPrecision) || getPrecision(this.step) || 0;
return labelPrecision
? value.toFixed(labelPrecision)
: Math.round(value).toString();
}
private _update = () => {
// Not connected?
if (!this._valueDisplay) return;
const value = Number(this.value) || 0;
const min = Number(this.min) || 0;
const max = Number(this.max) || 100;
const labelPrecision =
Number(this.labelPrecision) || getPrescision(this.step) || 0;
const percent = (100 * (value - min)) / (max - min);
const displayValue = labelPrecision
? value.toFixed(labelPrecision)
: Math.round(value).toString();
const displayValue = this._getDisplayValue(value);
this._valueDisplay!.textContent = displayValue;
this.style.setProperty('--value-percent', percent + '%');

View File

@ -35,7 +35,7 @@
text-decoration-style: dotted;
text-decoration-color: var(--main-theme-color);
text-underline-position: under;
width: 48px;
width: 54px;
position: relative;
left: 5px;

View File

@ -1,5 +1,6 @@
import PointerTracker, { Pointer } from 'pointer-tracker';
import 'add-css:./styles.css';
import { isSafari } from 'client/lazy-app/util';
interface Point {
clientX: number;
@ -105,14 +106,23 @@ export default class PinchZoom extends HTMLElement {
const pointerTracker: PointerTracker = new PointerTracker(this, {
start: (pointer, event) => {
// We only want to track 2 pointers at most
if (pointerTracker.currentPointers.length === 2 || !this._positioningEl)
if (
pointerTracker.currentPointers.length === 2 ||
!this._positioningEl
) {
return false;
}
event.preventDefault();
return true;
},
move: (previousPointers) => {
this._onPointerMove(previousPointers, pointerTracker.currentPointers);
},
// Unfortunately Safari on iOS has a bug where pointer event capturing
// doesn't work in some cases, and we hit those cases due to our event
// retargeting in pinch-zoom.
// https://bugs.webkit.org/show_bug.cgi?id=220196
avoidPointerEvents: isSafari,
});
this.addEventListener('wheel', (event) => this._onWheel(event));

View File

@ -5,8 +5,10 @@ import './custom-els/PinchZoom';
import './custom-els/TwoUp';
import * as style from './style.css';
import 'add-css:./style.css';
import { shallowEqual } from '../../util';
import { shallowEqual, isSafari } from '../../util';
import {
ToggleAliasingIcon,
ToggleAliasingActiveIcon,
ToggleBackgroundIcon,
AddIcon,
RemoveIcon,
@ -19,7 +21,6 @@ import { cleanSet } from '../../util/clean-modify';
import type { SourceImage } from '../../Compress';
import { linkRef } from 'shared/prerendered-app/util';
import { drawDataToCanvas } from 'client/lazy-app/util/canvas';
interface Props {
source?: SourceImage;
preprocessorState?: PreprocessorState;
@ -35,6 +36,7 @@ interface State {
scale: number;
editingScale: boolean;
altBackground: boolean;
aliasing: boolean;
}
const scaleToOpts: ScaleToOpts = {
@ -49,6 +51,7 @@ export default class Output extends Component<Props, State> {
scale: 1,
editingScale: false,
altBackground: false,
aliasing: false,
};
canvasLeft?: HTMLCanvasElement;
canvasRight?: HTMLCanvasElement;
@ -145,6 +148,12 @@ export default class Output extends Component<Props, State> {
return props.rightCompressed || (props.source && props.source.preprocessed);
}
private toggleAliasing = () => {
this.setState((state) => ({
aliasing: !state.aliasing,
}));
};
private toggleBackground = () => {
this.setState({
altBackground: !this.state.altBackground,
@ -255,7 +264,7 @@ export default class Output extends Component<Props, State> {
render(
{ mobileView, leftImgContain, rightImgContain, source }: Props,
{ scale, editingScale, altBackground }: State,
{ scale, editingScale, altBackground, aliasing }: State,
) {
const leftDraw = this.leftDrawable();
const rightDraw = this.rightDrawable();
@ -275,7 +284,11 @@ export default class Output extends Component<Props, State> {
onTouchStartCapture={this.onRetargetableEvent}
onTouchEndCapture={this.onRetargetableEvent}
onTouchMoveCapture={this.onRetargetableEvent}
onPointerDownCapture={this.onRetargetableEvent}
onPointerDownCapture={
// We avoid pointer events in our PinchZoom due to a Safari bug.
// That means we also need to avoid them here too, else we end up preventing the fallback mouse events.
isSafari ? undefined : this.onRetargetableEvent
}
onMouseDownCapture={this.onRetargetableEvent}
onWheelCapture={this.onRetargetableEvent}
>
@ -285,7 +298,9 @@ export default class Output extends Component<Props, State> {
ref={linkRef(this, 'pinchZoomLeft')}
>
<canvas
class={style.pinchTarget}
class={`${style.pinchTarget} ${
aliasing ? style.pixelated : ''
}`}
ref={linkRef(this, 'canvasLeft')}
width={leftDraw && leftDraw.width}
height={leftDraw && leftDraw.height}
@ -301,7 +316,9 @@ export default class Output extends Component<Props, State> {
ref={linkRef(this, 'pinchZoomRight')}
>
<canvas
class={style.pinchTarget}
class={`${style.pinchTarget} ${
aliasing ? style.pixelated : ''
}`}
ref={linkRef(this, 'canvasRight')}
width={rightDraw && rightDraw.width}
height={rightDraw && rightDraw.height}
@ -345,10 +362,31 @@ export default class Output extends Component<Props, State> {
</button>
</div>
<div class={style.buttonGroup}>
<button class={style.firstButton} onClick={this.onRotateClick}>
<button
class={style.firstButton}
onClick={this.onRotateClick}
title="Rotate"
>
<RotateIcon />
</button>
<button class={style.lastButton} onClick={this.toggleBackground}>
{!isSafari && (
<button
class={style.button}
onClick={this.toggleAliasing}
title="Toggle smoothing"
>
{aliasing ? (
<ToggleAliasingActiveIcon />
) : (
<ToggleAliasingIcon />
)}
</button>
)}
<button
class={style.lastButton}
onClick={this.toggleBackground}
title="Toggle background"
>
{altBackground ? (
<ToggleBackgroundActiveIcon />
) : (

View File

@ -86,8 +86,7 @@
font-size: 1.2rem;
cursor: pointer;
&:focus {
/* box-shadow: 0 0 0 2px var(--hot-pink); */
&:focus-visible {
box-shadow: 0 0 0 2px #fff;
outline: none;
z-index: 1;
@ -161,3 +160,8 @@ input.zoom {
pointer-events: auto;
}
}
.pixelated {
image-rendering: crisp-edges;
image-rendering: pixelated;
}

View File

@ -11,6 +11,25 @@ const Icon = (props: preact.JSX.HTMLAttributes) => (
/>
);
export const ToggleAliasingIcon = (props: preact.JSX.HTMLAttributes) => (
<Icon {...props}>
<circle
cx="12"
cy="12"
r="8"
fill="none"
stroke="currentColor"
stroke-width="2"
/>
</Icon>
);
export const ToggleAliasingActiveIcon = (props: preact.JSX.HTMLAttributes) => (
<Icon {...props}>
<path d="M12 3h5v2h2v2h2v5h-2V9h-2V7h-2V5h-3V3M21 12v5h-2v2h-2v2h-5v-2h3v-2h2v-2h2v-3h2M12 21H7v-2H5v-2H3v-5h2v3h2v2h2v2h3v2M3 12V7h2V5h2V3h5v2H9v2H7v2H5v3H3" />
</Icon>
);
export const ToggleBackgroundIcon = (props: preact.JSX.HTMLAttributes) => (
<Icon {...props}>
<path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm2 4v-2H3c0 1.1.9 2 2 2zM3 9h2V7H3v2zm12 12h2v-2h-2v2zm4-18H9a2 2 0 0 0-2 2v10c0 1.1.9 2 2 2h10a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zm0 12H9V5h10v10zm-8 6h2v-2h-2v2zm-4 0h2v-2H7v2z" />

View File

@ -14,6 +14,11 @@
import * as WebCodecs from '../util/web-codecs';
import { drawableToImageData } from './canvas';
/** If render engine is Safari */
export const isSafari =
/Safari\//.test(navigator.userAgent) &&
!/Chrom(e|ium)\//.test(navigator.userAgent);
/**
* Compare two objects, returning a boolean indicating if
* they have the same properties and strictly equal values.

View File

@ -29,10 +29,9 @@ interface State {
slightLoss: boolean;
autoEdgePreservingFilter: boolean;
decodingSpeedTier: number;
photonNoiseIso: number;
}
const maxSpeed = 7;
export class Options extends Component<Props, State> {
static getDerivedStateFromProps(
props: Props,
@ -47,7 +46,7 @@ export class Options extends Component<Props, State> {
// Create default form state from options
return {
options,
effort: maxSpeed - options.speed,
effort: options.effort,
quality: options.quality,
progressive: options.progressive,
edgePreservingFilter: options.epf === -1 ? 2 : options.epf,
@ -55,6 +54,7 @@ export class Options extends Component<Props, State> {
slightLoss: options.lossyPalette,
autoEdgePreservingFilter: options.epf === -1,
decodingSpeedTier: options.decodingSpeedTier,
photonNoiseIso: options.photonNoiseIso,
};
}
@ -87,15 +87,15 @@ export class Options extends Component<Props, State> {
};
const newOptions: EncodeOptions = {
speed: maxSpeed - optionState.effort,
effort: optionState.effort,
quality: optionState.lossless ? 100 : optionState.quality,
progressive: optionState.progressive,
epf: optionState.autoEdgePreservingFilter
? -1
: optionState.edgePreservingFilter,
nearLossless: 0,
lossyPalette: optionState.lossless ? optionState.slightLoss : false,
decodingSpeedTier: optionState.decodingSpeedTier,
photonNoiseIso: optionState.photonNoiseIso,
};
// Updating options, so we don't recalculate in getDerivedStateFromProps.
@ -121,6 +121,7 @@ export class Options extends Component<Props, State> {
slightLoss,
autoEdgePreservingFilter,
decodingSpeedTier,
photonNoiseIso,
}: State,
) {
// I'm rendering both lossy and lossless forms, as it becomes much easier when
@ -164,7 +165,6 @@ export class Options extends Component<Props, State> {
<label class={style.optionToggle}>
Auto edge filter
<Checkbox
name="autoEdgeFilter"
checked={autoEdgePreservingFilter}
onChange={this._inputChange(
'autoEdgePreservingFilter',
@ -199,6 +199,17 @@ export class Options extends Component<Props, State> {
Optimise for decoding speed (worse compression):
</Range>
</div>
<div class={style.optionOneCell}>
<Range
min="0"
max="50000"
step="100"
value={photonNoiseIso}
onInput={this._inputChange('photonNoiseIso', 'number')}
>
Noise equivalent to ISO:
</Range>
</div>
</div>
)}
</Expander>
@ -212,8 +223,8 @@ export class Options extends Component<Props, State> {
</label>
<div class={style.optionOneCell}>
<Range
min="0"
max={maxSpeed - 1}
min="3"
max="9"
value={effort}
onInput={this._inputChange('effort', 'number')}
>

View File

@ -18,11 +18,11 @@ export const label = 'JPEG XL (beta)';
export const mimeType = 'image/jxl';
export const extension = 'jxl';
export const defaultOptions: EncodeOptions = {
speed: 4,
effort: 7,
quality: 75,
progressive: false,
epf: -1,
nearLossless: 0,
lossyPalette: false,
decodingSpeedTier: 0,
photonNoiseIso: 0,
};

View File

@ -25,18 +25,6 @@ import { lookup as lookupMime } from 'mime-types';
const manifestSize = ({ width, height }: { width: number; height: number }) =>
`${width}x${height}`;
// Set by Netlify
const branch = process.env.BRANCH;
const branchOriginTrialIds = new Map([
[
'live',
'Aj5GY7W9AHM8di+yvMCajIhLRHoYN7slruwOYXE/Iub5hgmW/r2RQt07vrUuT4eUTkWxcyNCAVkiI+5ugdVW3gAAAABUeyJvcmlnaW4iOiJodHRwczovL3NxdW9vc2guYXBwOjQ0MyIsImZlYXR1cmUiOiJXZWJBc3NlbWJseVNpbWQiLCJleHBpcnkiOjE2MjM4MDE1OTl9',
],
]);
const originTrialId = branchOriginTrialIds.get(branch || '');
interface Output {
[outputPath: string]: string;
}
@ -110,15 +98,6 @@ const toOutput: Output = {
/*
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin
# Origin trial for WebAssembly SIMD.
${
originTrialId
? ` Origin-Trial: ${originTrialId}`
: `# Cannot find origin trial ID. process.env.BRANCH is: ${JSON.stringify(
branch,
)}`
}
`,
};