mirror of
https://github.com/GoogleChromeLabs/squoosh.git
synced 2025-11-16 10:39:53 +00:00
All options
This commit is contained in:
@@ -7,12 +7,12 @@
|
|||||||
"include": [
|
"include": [
|
||||||
"src/features/**/client/**/*",
|
"src/features/**/client/**/*",
|
||||||
"src/features/**/shared/**/*",
|
"src/features/**/shared/**/*",
|
||||||
// Not really clean, but we need to access the type of the functions here
|
"src/features/client-utils/**/*",
|
||||||
|
"src/shared/**/*",
|
||||||
|
"src/client/**/*",
|
||||||
|
// Not really clean, but we need these to access the type of the functions
|
||||||
// for comlink
|
// for comlink
|
||||||
"src/features/**/worker/**/*",
|
"src/features/**/worker/**/*",
|
||||||
// And again.
|
"src/features-worker/**/*"
|
||||||
"src/features-worker/**/*",
|
|
||||||
"src/shared/**/*",
|
|
||||||
"src/client/**/*"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,6 +214,11 @@ export default function () {
|
|||||||
([path, name]) => `import * as ${name}ProcessorMeta from '${path}';`,
|
([path, name]) => `import * as ${name}ProcessorMeta from '${path}';`,
|
||||||
),
|
),
|
||||||
`interface Enableable { enabled: boolean; }`,
|
`interface Enableable { enabled: boolean; }`,
|
||||||
|
`export interface ProcessorOptions {`,
|
||||||
|
processorMetaTsNames.map(
|
||||||
|
([_, name]) => ` ${name}: ${name}ProcessorMeta.Options;`,
|
||||||
|
),
|
||||||
|
`}`,
|
||||||
`export interface ProcessorState {`,
|
`export interface ProcessorState {`,
|
||||||
processorMetaTsNames.map(
|
processorMetaTsNames.map(
|
||||||
([_, name]) => ` ${name}: Enableable & ${name}ProcessorMeta.Options;`,
|
([_, name]) => ` ${name}: Enableable & ${name}ProcessorMeta.Options;`,
|
||||||
|
|||||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -2312,6 +2312,12 @@
|
|||||||
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
|
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"linkstate": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/linkstate/-/linkstate-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-5SICdxQG9FpWk44wSEoM2WOCUNuYfClp10t51XAIV5E7vUILF/dTYxf0vJw6bW2dUd2wcQon+LkNtRijpNLrig==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"version": "10.4.0",
|
"version": "10.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-10.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-10.4.0.tgz",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"file-drop-element": "^1.0.1",
|
"file-drop-element": "^1.0.1",
|
||||||
"husky": "^4.3.0",
|
"husky": "^4.3.0",
|
||||||
"idb-keyval": "^3.2.0",
|
"idb-keyval": "^3.2.0",
|
||||||
|
"linkstate": "^1.1.1",
|
||||||
"lint-staged": "^10.4.0",
|
"lint-staged": "^10.4.0",
|
||||||
"lodash.camelcase": "^4.3.0",
|
"lodash.camelcase": "^4.3.0",
|
||||||
"mime-types": "^2.1.27",
|
"mime-types": "^2.1.27",
|
||||||
|
|||||||
23
src/client/lazy-app/Compress/Options/Checkbox/index.tsx
Normal file
23
src/client/lazy-app/Compress/Options/Checkbox/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { h, Component } from 'preact';
|
||||||
|
import * as style from './style.css';
|
||||||
|
import 'add-css:./styles.css';
|
||||||
|
import { UncheckedIcon, CheckedIcon } from '../../../icons';
|
||||||
|
|
||||||
|
interface Props extends preact.JSX.HTMLAttributes {}
|
||||||
|
interface State {}
|
||||||
|
|
||||||
|
export default class Checkbox extends Component<Props, State> {
|
||||||
|
render(props: Props) {
|
||||||
|
return (
|
||||||
|
<div class={style.checkbox}>
|
||||||
|
{props.checked ? (
|
||||||
|
<CheckedIcon class={`${style.icon} ${style.checked}`} />
|
||||||
|
) : (
|
||||||
|
<UncheckedIcon class={style.icon} />
|
||||||
|
)}
|
||||||
|
{/* @ts-ignore - TS bug https://github.com/microsoft/TypeScript/issues/16019 */}
|
||||||
|
<input class={style.realCheckbox} type="checkbox" {...props} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/client/lazy-app/Compress/Options/Checkbox/style.css
Normal file
22
src/client/lazy-app/Compress/Options/Checkbox/style.css
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
.checkbox {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
--size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.real-checkbox {
|
||||||
|
top: 0;
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: block;
|
||||||
|
width: var(--size);
|
||||||
|
height: var(--size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checked {
|
||||||
|
fill: #34b9eb;
|
||||||
|
}
|
||||||
80
src/client/lazy-app/Compress/Options/Expander/index.tsx
Normal file
80
src/client/lazy-app/Compress/Options/Expander/index.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { h, Component, ComponentChild, ComponentChildren } from 'preact';
|
||||||
|
import * as style from './style.css';
|
||||||
|
import 'add-css:./styles.css';
|
||||||
|
import { transitionHeight } from '../../../util';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ComponentChildren;
|
||||||
|
}
|
||||||
|
interface State {
|
||||||
|
outgoingChildren: ComponentChild[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Expander extends Component<Props, State> {
|
||||||
|
state: State = {
|
||||||
|
outgoingChildren: [],
|
||||||
|
};
|
||||||
|
private lastElHeight: number = 0;
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps: Props) {
|
||||||
|
const children = this.props.children as ComponentChild[];
|
||||||
|
const nextChildren = nextProps.children as ComponentChild[];
|
||||||
|
|
||||||
|
if (!nextChildren[0] && children[0]) {
|
||||||
|
// Cache the current children for the shrink animation.
|
||||||
|
this.setState({ outgoingChildren: children });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUpdate(nextProps: Props) {
|
||||||
|
const children = this.props.children as ComponentChild[];
|
||||||
|
const nextChildren = nextProps.children as ComponentChild[];
|
||||||
|
|
||||||
|
// Only interested if going from empty to not-empty, or not-empty to empty.
|
||||||
|
if ((children[0] && nextChildren[0]) || (!children[0] && !nextChildren[0]))
|
||||||
|
return;
|
||||||
|
this.lastElHeight = (this
|
||||||
|
.base as HTMLElement).getBoundingClientRect().height;
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentDidUpdate(previousProps: Props) {
|
||||||
|
const children = this.props.children as ComponentChild[];
|
||||||
|
const previousChildren = previousProps.children as ComponentChild[];
|
||||||
|
|
||||||
|
// Only interested if going from empty to not-empty, or not-empty to empty.
|
||||||
|
if (
|
||||||
|
(children[0] && previousChildren[0]) ||
|
||||||
|
(!children[0] && !previousChildren[0])
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// What height do we need to transition to?
|
||||||
|
(this.base as HTMLElement).style.height = '';
|
||||||
|
(this.base as HTMLElement).style.overflow = 'hidden';
|
||||||
|
const newHeight = children[0]
|
||||||
|
? (this.base as HTMLElement).getBoundingClientRect().height
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
await transitionHeight(this.base as HTMLElement, {
|
||||||
|
duration: 300,
|
||||||
|
from: this.lastElHeight,
|
||||||
|
to: newHeight,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Unset the height & overflow, so element changes do the right thing.
|
||||||
|
(this.base as HTMLElement).style.height = '';
|
||||||
|
(this.base as HTMLElement).style.overflow = '';
|
||||||
|
if (this.state.outgoingChildren[0]) this.setState({ outgoingChildren: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
render(props: Props, { outgoingChildren }: State) {
|
||||||
|
const children = props.children as ComponentChild[];
|
||||||
|
const childrenExiting = !children[0] && outgoingChildren[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={childrenExiting ? style.childrenExiting : ''}>
|
||||||
|
{children[0] ? children : outgoingChildren}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
5
src/client/lazy-app/Compress/Options/Expander/style.css
Normal file
5
src/client/lazy-app/Compress/Options/Expander/style.css
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.children-exiting {
|
||||||
|
& > * {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import PointerTracker from 'pointer-tracker';
|
||||||
|
import * as style from './style.css';
|
||||||
|
import 'add-css:./styles.css';
|
||||||
|
|
||||||
|
const RETARGETED_EVENTS = ['focus', 'blur'];
|
||||||
|
const UPDATE_EVENTS = ['input', 'change'];
|
||||||
|
const REFLECTED_PROPERTIES = [
|
||||||
|
'name',
|
||||||
|
'min',
|
||||||
|
'max',
|
||||||
|
'step',
|
||||||
|
'value',
|
||||||
|
'disabled',
|
||||||
|
];
|
||||||
|
const REFLECTED_ATTRIBUTES = [
|
||||||
|
'name',
|
||||||
|
'min',
|
||||||
|
'max',
|
||||||
|
'step',
|
||||||
|
'value',
|
||||||
|
'disabled',
|
||||||
|
];
|
||||||
|
|
||||||
|
function getPrescision(value: string): number {
|
||||||
|
const afterDecimal = value.split('.')[1];
|
||||||
|
return afterDecimal ? afterDecimal.length : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RangeInputElement extends HTMLElement {
|
||||||
|
private _input: HTMLInputElement;
|
||||||
|
private _valueDisplay?: HTMLDivElement;
|
||||||
|
private _ignoreChange = false;
|
||||||
|
|
||||||
|
static get observedAttributes() {
|
||||||
|
return REFLECTED_ATTRIBUTES;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this._input = document.createElement('input');
|
||||||
|
this._input.type = 'range';
|
||||||
|
this._input.className = style.input;
|
||||||
|
|
||||||
|
const tracker = new PointerTracker(this._input, {
|
||||||
|
start: (): boolean => {
|
||||||
|
if (tracker.currentPointers.length !== 0) return false;
|
||||||
|
this._input.classList.add(style.touchActive);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
end: () => {
|
||||||
|
this._input.classList.remove(style.touchActive);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const event of RETARGETED_EVENTS) {
|
||||||
|
this._input.addEventListener(event, this._retargetEvent, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const event of UPDATE_EVENTS) {
|
||||||
|
this._input.addEventListener(event, this._update, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
if (this.contains(this._input)) return;
|
||||||
|
this.innerHTML =
|
||||||
|
`<div class="${style.thumbWrapper}">` +
|
||||||
|
`<div class="${style.thumb}"></div>` +
|
||||||
|
`<div class="${style.valueDisplay}"></div>` +
|
||||||
|
'</div>';
|
||||||
|
|
||||||
|
this.insertBefore(this._input, this.firstChild);
|
||||||
|
this._valueDisplay = this.querySelector(
|
||||||
|
'.' + style.valueDisplay,
|
||||||
|
) as HTMLDivElement;
|
||||||
|
// Set inline styles (this is useful when used with frameworks which might clear inline styles)
|
||||||
|
this._update();
|
||||||
|
}
|
||||||
|
|
||||||
|
get labelPrecision(): string {
|
||||||
|
return this.getAttribute('label-precision') || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
set labelPrecision(precision: string) {
|
||||||
|
this.setAttribute('label-precision', precision);
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback(
|
||||||
|
name: string,
|
||||||
|
oldValue: string,
|
||||||
|
newValue: string | null,
|
||||||
|
) {
|
||||||
|
if (this._ignoreChange) return;
|
||||||
|
if (newValue === null) {
|
||||||
|
this._input.removeAttribute(name);
|
||||||
|
} else {
|
||||||
|
this._input.setAttribute(name, newValue);
|
||||||
|
}
|
||||||
|
this._reflectAttributes();
|
||||||
|
this._update();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _retargetEvent = (event: Event) => {
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
const retargetted = new Event(event.type, event);
|
||||||
|
this.dispatchEvent(retargetted);
|
||||||
|
};
|
||||||
|
|
||||||
|
private _update = () => {
|
||||||
|
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();
|
||||||
|
|
||||||
|
this._valueDisplay!.textContent = displayValue;
|
||||||
|
this.style.setProperty('--value-percent', percent + '%');
|
||||||
|
this.style.setProperty('--value-width', '' + displayValue.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
private _reflectAttributes() {
|
||||||
|
this._ignoreChange = true;
|
||||||
|
for (const attributeName of REFLECTED_ATTRIBUTES) {
|
||||||
|
if (this._input.hasAttribute(attributeName)) {
|
||||||
|
this.setAttribute(
|
||||||
|
attributeName,
|
||||||
|
this._input.getAttribute(attributeName)!,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.removeAttribute(attributeName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._ignoreChange = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RangeInputElement {
|
||||||
|
name: string;
|
||||||
|
min: string;
|
||||||
|
max: string;
|
||||||
|
step: string;
|
||||||
|
value: string;
|
||||||
|
disabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const prop of REFLECTED_PROPERTIES) {
|
||||||
|
Object.defineProperty(RangeInputElement.prototype, prop, {
|
||||||
|
get() {
|
||||||
|
return this._input[prop];
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this._input[prop] = val;
|
||||||
|
this._reflectAttributes();
|
||||||
|
this._update();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RangeInputElement;
|
||||||
|
|
||||||
|
customElements.define('range-input', RangeInputElement);
|
||||||
9
src/client/lazy-app/Compress/Options/Range/custom-els/RangeInput/missing-types.d.ts
vendored
Normal file
9
src/client/lazy-app/Compress/Options/Range/custom-els/RangeInput/missing-types.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
declare module 'preact' {
|
||||||
|
namespace createElement.JSX {
|
||||||
|
interface IntrinsicElements {
|
||||||
|
'range-input': HTMLAttributes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
range-input {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
height: 18px;
|
||||||
|
width: 130px;
|
||||||
|
margin: 2px;
|
||||||
|
font: inherit;
|
||||||
|
line-height: 16px;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disabled inputs are greyed out */
|
||||||
|
range-input[disabled] {
|
||||||
|
filter: grayscale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
range-input::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
border-radius: 1px;
|
||||||
|
box-shadow: 0 -0.5px 0 rgba(0, 0, 0, 0.3),
|
||||||
|
inset 0 0.5px 0 rgba(255, 255, 255, 0.2), 0 0.5px 0 rgba(255, 255, 255, 0.3);
|
||||||
|
background: linear-gradient(#34b9eb, #218ab1) 0 / var(--value-percent, 0%)
|
||||||
|
100% no-repeat #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb {
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 3px;
|
||||||
|
left: var(--value-percent, 0%);
|
||||||
|
margin-left: -6px;
|
||||||
|
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><circle cx="5" cy="5" r="1" fill="%235D509E" /></svg>')
|
||||||
|
center no-repeat #34b9eb;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
box-shadow: 0 0.5px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
left: 6px;
|
||||||
|
right: 6px;
|
||||||
|
bottom: 0;
|
||||||
|
height: 0;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-display {
|
||||||
|
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="62" fill="none"><path fill="%2334B9EB" d="M27.3 27.3C25 29.6 17 35.8 17 43v3c0 3 2.5 5 3.2 5.8a6 6 0 1 1-8.5 0C12.6 51 15 49 15 46v-3c0-7.2-8-13.4-10.3-15.7A16 16 0 0 1 16 0a16 16 0 0 1 11.3 27.3z"/><circle cx="16" cy="56" r="1" fill="%235D509E"/></svg>')
|
||||||
|
top center no-repeat;
|
||||||
|
position: absolute;
|
||||||
|
box-sizing: border-box;
|
||||||
|
left: var(--value-percent, 0%);
|
||||||
|
bottom: 3px;
|
||||||
|
width: 32px;
|
||||||
|
height: 62px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 8px 3px 0;
|
||||||
|
margin: 0 0 0 -16px;
|
||||||
|
filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.3));
|
||||||
|
transform-origin: 50% 90%;
|
||||||
|
opacity: 0.0001;
|
||||||
|
transform: scale(0.2);
|
||||||
|
color: #fff;
|
||||||
|
font: inherit;
|
||||||
|
font-size: calc(100% - var(--value-width, 3) / 5 * 0.2em);
|
||||||
|
text-overflow: clip;
|
||||||
|
text-shadow: 0 -0.5px 0 rgba(0, 0, 0, 0.4);
|
||||||
|
transition: all 200ms ease;
|
||||||
|
transition-property: opacity, transform;
|
||||||
|
will-change: transform;
|
||||||
|
pointer-events: none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.touch-active + .thumb-wrapper .value-display {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.touch-active + .thumb-wrapper .thumb {
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
55
src/client/lazy-app/Compress/Options/Range/index.tsx
Normal file
55
src/client/lazy-app/Compress/Options/Range/index.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { h, Component } from 'preact';
|
||||||
|
import * as style from './style.css';
|
||||||
|
import 'add-css:./styles.css';
|
||||||
|
import RangeInputElement from './custom-els/RangeInput';
|
||||||
|
import '../../custom-els/RangeInput';
|
||||||
|
import { linkRef } from 'shared/initial-app/util';
|
||||||
|
|
||||||
|
interface Props extends preact.JSX.HTMLAttributes {}
|
||||||
|
interface State {}
|
||||||
|
|
||||||
|
export default class Range extends Component<Props, State> {
|
||||||
|
rangeWc?: RangeInputElement;
|
||||||
|
|
||||||
|
private onTextInput = (event: Event) => {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
const value = input.value.trim();
|
||||||
|
if (!value) return;
|
||||||
|
this.rangeWc!.value = input.value;
|
||||||
|
this.rangeWc!.dispatchEvent(
|
||||||
|
new InputEvent('input', {
|
||||||
|
bubbles: event.bubbles,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render(props: Props) {
|
||||||
|
const { children, ...otherProps } = props;
|
||||||
|
|
||||||
|
const { value, min, max, step } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label class={style.range}>
|
||||||
|
<span class={style.labelText}>{children}</span>
|
||||||
|
{/* On interaction, Safari gives focus to the first element in the label, so the
|
||||||
|
<range-input> is deliberately first. */}
|
||||||
|
<div class={style.rangeWcContainer}>
|
||||||
|
<range-input
|
||||||
|
ref={linkRef(this, 'rangeWc')}
|
||||||
|
class={style.rangeWc}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class={style.textInput}
|
||||||
|
value={value}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
|
step={step}
|
||||||
|
onInput={this.onTextInput}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
55
src/client/lazy-app/Compress/Options/Range/style.css
Normal file
55
src/client/lazy-app/Compress/Options/Range/style.css
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
.range {
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-text {
|
||||||
|
color: #fff; /* TEMP */
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-wc-container {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
grid-row: 2 / 3;
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.range-wc {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-input {
|
||||||
|
grid-row: 1 / 2;
|
||||||
|
grid-column: 2 / 3;
|
||||||
|
|
||||||
|
text-align: right;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
border: none;
|
||||||
|
padding: 2px 5px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-style: dotted;
|
||||||
|
text-underline-position: under;
|
||||||
|
width: 48px;
|
||||||
|
position: relative;
|
||||||
|
left: 5px;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background: #fff;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the number controls
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
|
||||||
|
&::-webkit-outer-spin-button,
|
||||||
|
&::-webkit-inner-spin-button {
|
||||||
|
-moz-appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/client/lazy-app/Compress/Options/Select/index.tsx
Normal file
27
src/client/lazy-app/Compress/Options/Select/index.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { h, Component } from 'preact';
|
||||||
|
import * as style from './style.css';
|
||||||
|
import 'add-css:./styles.css';
|
||||||
|
|
||||||
|
interface Props extends preact.JSX.HTMLAttributes {
|
||||||
|
large?: boolean;
|
||||||
|
}
|
||||||
|
interface State {}
|
||||||
|
|
||||||
|
export default class Select extends Component<Props, State> {
|
||||||
|
render(props: Props) {
|
||||||
|
const { large, ...otherProps } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={style.select}>
|
||||||
|
{/* @ts-ignore - TS bug https://github.com/microsoft/TypeScript/issues/16019 */}
|
||||||
|
<select
|
||||||
|
class={`${style.builtinSelect} ${large ? style.large : ''}`}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
<svg class={style.arrow} viewBox="0 0 10 5">
|
||||||
|
<path d="M0 0l5 5 5-5z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/client/lazy-app/Compress/Options/Select/style.css
Normal file
33
src/client/lazy-app/Compress/Options/Select/style.css
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
.select {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.builtin-select {
|
||||||
|
background: #2f2f2f;
|
||||||
|
border-radius: 4px;
|
||||||
|
font: inherit;
|
||||||
|
padding: 4px 25px 4px 10px;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
border: none;
|
||||||
|
color: #fff;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
fill: #fff;
|
||||||
|
width: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.large {
|
||||||
|
padding: 10px 35px 10px 10px;
|
||||||
|
background: #151515;
|
||||||
|
|
||||||
|
& .arrow {
|
||||||
|
right: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
190
src/client/lazy-app/Compress/Options/index.tsx
Normal file
190
src/client/lazy-app/Compress/Options/index.tsx
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
import { h, Component } from 'preact';
|
||||||
|
|
||||||
|
import * as style from './style.css';
|
||||||
|
import 'add-css:./styles.css';
|
||||||
|
import { cleanSet, cleanMerge } from '../../util/clean-modify';
|
||||||
|
|
||||||
|
import type { SourceImage, OutputType } from '..';
|
||||||
|
import {
|
||||||
|
EncoderOptions,
|
||||||
|
EncoderState,
|
||||||
|
ProcessorState,
|
||||||
|
ProcessorOptions,
|
||||||
|
encoderMap,
|
||||||
|
} from '../../feature-meta';
|
||||||
|
import Expander from './Expander';
|
||||||
|
import Checkbox from './Checkbox';
|
||||||
|
import Select from './Select';
|
||||||
|
import { Options as QuantOptionsComponent } from 'features/processors/quantize/client';
|
||||||
|
import { Options as ResizeOptionsComponent } from 'features/processors/resize/client';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
mobileView: boolean;
|
||||||
|
source?: SourceImage;
|
||||||
|
encoderState?: EncoderState;
|
||||||
|
processorState: ProcessorState;
|
||||||
|
onEncoderTypeChange(newType: OutputType): void;
|
||||||
|
onEncoderOptionsChange(newOptions: EncoderOptions): void;
|
||||||
|
onProcessorOptionsChange(newOptions: ProcessorState): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
supportedEncoderMap?: PartialButNotUndefined<typeof encoderMap>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PartialButNotUndefined<T> = {
|
||||||
|
[P in keyof T]: T[P];
|
||||||
|
};
|
||||||
|
|
||||||
|
const supportedEncoderMapP: Promise<PartialButNotUndefined<
|
||||||
|
typeof encoderMap
|
||||||
|
>> = (async () => {
|
||||||
|
const supportedEncoderMap: PartialButNotUndefined<typeof encoderMap> = {
|
||||||
|
...encoderMap,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter out entries where the feature test fails
|
||||||
|
await Promise.all(
|
||||||
|
Object.entries(encoderMap).map(async ([encoderName, details]) => {
|
||||||
|
if ('featureTest' in details && !(await details.featureTest())) {
|
||||||
|
delete supportedEncoderMap[encoderName as keyof typeof encoderMap];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return supportedEncoderMap;
|
||||||
|
})();
|
||||||
|
|
||||||
|
export default class Options extends Component<Props, State> {
|
||||||
|
state: State = {
|
||||||
|
supportedEncoderMap: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
supportedEncoderMapP.then((supportedEncoderMap) =>
|
||||||
|
this.setState({ supportedEncoderMap }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onEncoderTypeChange = (event: Event) => {
|
||||||
|
const el = event.currentTarget as HTMLSelectElement;
|
||||||
|
|
||||||
|
// The select element only has values matching encoder types,
|
||||||
|
// so 'as' is safe here.
|
||||||
|
const type = el.value as OutputType;
|
||||||
|
this.props.onEncoderTypeChange(type);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onProcessorEnabledChange = (event: Event) => {
|
||||||
|
const el = event.currentTarget as HTMLInputElement;
|
||||||
|
const processor = el.name.split('.')[0] as keyof ProcessorState;
|
||||||
|
|
||||||
|
this.props.onProcessorOptionsChange(
|
||||||
|
cleanSet(this.props.processorState, `${processor}.enabled`, el.checked),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onQuantizerOptionsChange = (opts: ProcessorOptions['quantize']) => {
|
||||||
|
this.props.onProcessorOptionsChange(
|
||||||
|
cleanMerge(this.props.processorState, 'quantizer', opts),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onResizeOptionsChange = (opts: ProcessorOptions['resize']) => {
|
||||||
|
this.props.onProcessorOptionsChange(
|
||||||
|
cleanMerge(this.props.processorState, 'resize', opts),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render(
|
||||||
|
{ source, encoderState, processorState, onEncoderOptionsChange }: Props,
|
||||||
|
{ supportedEncoderMap }: State,
|
||||||
|
) {
|
||||||
|
const encoder = encoderState && encoderMap[encoderState.type];
|
||||||
|
const EncoderOptionComponent =
|
||||||
|
encoder && 'Options' in encoder ? encoder.Options : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={style.optionsScroller}>
|
||||||
|
<Expander>
|
||||||
|
{!encoderState ? null : (
|
||||||
|
<div>
|
||||||
|
<h3 class={style.optionsTitle}>Edit</h3>
|
||||||
|
<label class={style.sectionEnabler}>
|
||||||
|
<Checkbox
|
||||||
|
name="resize.enable"
|
||||||
|
checked={!!processorState.resize.enabled}
|
||||||
|
onChange={this.onProcessorEnabledChange}
|
||||||
|
/>
|
||||||
|
Resize
|
||||||
|
</label>
|
||||||
|
<Expander>
|
||||||
|
{processorState.resize.enabled ? (
|
||||||
|
<ResizeOptionsComponent
|
||||||
|
isVector={Boolean(source && source.vectorImage)}
|
||||||
|
inputWidth={source ? source.preprocessed.width : 1}
|
||||||
|
inputHeight={source ? source.preprocessed.height : 1}
|
||||||
|
options={processorState.resize}
|
||||||
|
onChange={this.onResizeOptionsChange}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Expander>
|
||||||
|
|
||||||
|
<label class={style.sectionEnabler}>
|
||||||
|
<Checkbox
|
||||||
|
name="quantizer.enable"
|
||||||
|
checked={!!processorState.quantize.enabled}
|
||||||
|
onChange={this.onProcessorEnabledChange}
|
||||||
|
/>
|
||||||
|
Reduce palette
|
||||||
|
</label>
|
||||||
|
<Expander>
|
||||||
|
{processorState.quantize.enabled ? (
|
||||||
|
<QuantOptionsComponent
|
||||||
|
options={processorState.quantize}
|
||||||
|
onChange={this.onQuantizerOptionsChange}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Expander>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Expander>
|
||||||
|
|
||||||
|
<h3 class={style.optionsTitle}>Compress</h3>
|
||||||
|
|
||||||
|
<section class={`${style.optionOneCell} ${style.optionsSection}`}>
|
||||||
|
{supportedEncoderMap ? (
|
||||||
|
<Select
|
||||||
|
value={encoderState ? encoderState.type : ''}
|
||||||
|
onChange={this.onEncoderTypeChange}
|
||||||
|
large
|
||||||
|
>
|
||||||
|
<option value="">Original Image</option>
|
||||||
|
{Object.entries(supportedEncoderMap).map(([type, encoder]) => (
|
||||||
|
<option value={type}>{encoder.meta.label}</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
|
<Select large>
|
||||||
|
<option>Loading…</option>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<Expander>
|
||||||
|
{EncoderOptionComponent && encoderState && (
|
||||||
|
<EncoderOptionComponent
|
||||||
|
options={
|
||||||
|
// Casting options, as encoderOptionsComponentMap[encodeData.type] ensures
|
||||||
|
// the correct type, but typescript isn't smart enough.
|
||||||
|
encoderState.options as any
|
||||||
|
}
|
||||||
|
onChange={onEncoderOptionsChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Expander>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/client/lazy-app/Compress/Options/style.css
Normal file
58
src/client/lazy-app/Compress/Options/style.css
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
.options-scroller {
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
--horizontal-padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-title {
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px var(--horizontal-padding);
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
border-bottom: 1px solid #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-text-first {
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
grid-template-columns: 87px 1fr;
|
||||||
|
grid-gap: 0.7em;
|
||||||
|
padding: 10px var(--horizontal-padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-one-cell {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
padding: 10px var(--horizontal-padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-input-first,
|
||||||
|
.section-enabler {
|
||||||
|
cursor: pointer;
|
||||||
|
display: grid;
|
||||||
|
align-items: center;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
grid-gap: 0.7em;
|
||||||
|
padding: 10px var(--horizontal-padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-enabler {
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.options-section {
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-field {
|
||||||
|
background: #fff;
|
||||||
|
color: #000;
|
||||||
|
font: inherit;
|
||||||
|
border: none;
|
||||||
|
padding: 2px 0 2px 10px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
.output {
|
.output {
|
||||||
composes: abs-fill from '../../../../shared/initial-app/util.scss';
|
composes: abs-fill from '../../../../shared/initial-app/util.css';
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
@@ -19,12 +19,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.two-up {
|
.two-up {
|
||||||
composes: abs-fill from '../../../../shared/initial-app/util.scss';
|
composes: abs-fill from '../../../../shared/initial-app/util.css';
|
||||||
--accent-color: var(--button-fg);
|
--accent-color: var(--button-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.pinch-zoom {
|
.pinch-zoom {
|
||||||
composes: abs-fill from '../../../../shared/initial-app/util.scss';
|
composes: abs-fill from '../../../../shared/initial-app/util.css';
|
||||||
outline: none;
|
outline: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -32,11 +32,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.pinch-target {
|
.pinch-target {
|
||||||
// This fixes a severe painting bug in Chrome.
|
/* This fixes a severe painting bug in Chrome.
|
||||||
// We should try to remove this once the issue is fixed.
|
* We should try to remove this once the issue is fixed.
|
||||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=870222#c10
|
* https://bugs.chromium.org/p/chromium/issues/detail?id=870222#c10 */
|
||||||
will-change: auto;
|
will-change: auto;
|
||||||
// Prevent the image becoming misshapen due to default flexbox layout.
|
/* Prevent the image becoming misshapen due to default flexbox layout. */
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
contain: content;
|
contain: content;
|
||||||
|
|
||||||
// Allow clicks to fall through to the pinch zoom area
|
/* Allow clicks to fall through to the pinch zoom area */
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
& > * {
|
& > * {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
|||||||
@@ -27,13 +27,14 @@ import Options from './Options';
|
|||||||
import ResultCache from './result-cache';
|
import ResultCache from './result-cache';
|
||||||
import { cleanMerge, cleanSet } from '../util/clean-modify';
|
import { cleanMerge, cleanSet } from '../util/clean-modify';
|
||||||
import './custom-els/MultiPanel';
|
import './custom-els/MultiPanel';
|
||||||
|
// TODO: you are here
|
||||||
import Results from '../results';
|
import Results from '../results';
|
||||||
import { ExpandIcon, CopyAcrossIconProps } from '../../lib/icons';
|
|
||||||
import SnackBarElement from '../../lib/SnackBar';
|
|
||||||
import WorkerBridge from '../worker-bridge';
|
import WorkerBridge from '../worker-bridge';
|
||||||
import { resize } from 'features/processors/resize/client';
|
import { resize } from 'features/processors/resize/client';
|
||||||
|
import type SnackBarElement from 'shared/initial-app/custom-els/snack-bar';
|
||||||
|
import { CopyAcrossIconProps, ExpandIcon } from '../icons';
|
||||||
|
|
||||||
type OutputType = EncoderType | 'identity';
|
export type OutputType = EncoderType | 'identity';
|
||||||
|
|
||||||
export interface SourceImage {
|
export interface SourceImage {
|
||||||
file: File;
|
file: File;
|
||||||
@@ -761,7 +762,7 @@ export default class Compress extends Component<Props, State> {
|
|||||||
<Options
|
<Options
|
||||||
source={source}
|
source={source}
|
||||||
mobileView={mobileView}
|
mobileView={mobileView}
|
||||||
preprocessorState={side.latestSettings.processorState}
|
processorState={side.latestSettings.processorState}
|
||||||
encoderState={side.latestSettings.encoderState}
|
encoderState={side.latestSettings.encoderState}
|
||||||
onEncoderTypeChange={this.onEncoderTypeChange.bind(
|
onEncoderTypeChange={this.onEncoderTypeChange.bind(
|
||||||
this,
|
this,
|
||||||
@@ -771,7 +772,7 @@ export default class Compress extends Component<Props, State> {
|
|||||||
this,
|
this,
|
||||||
index as 0 | 1,
|
index as 0 | 1,
|
||||||
)}
|
)}
|
||||||
onPreprocessorOptionsChange={this.onProcessorOptionsChange.bind(
|
onProcessorOptionsChange={this.onProcessorOptionsChange.bind(
|
||||||
this,
|
this,
|
||||||
index as 0 | 1,
|
index as 0 | 1,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -41,9 +41,9 @@ Encoders must have the following:
|
|||||||
- `EncodeOptions` - An interface for the codec's options.
|
- `EncodeOptions` - An interface for the codec's options.
|
||||||
- `defaultOptions` - An object of type `EncodeOptions`.
|
- `defaultOptions` - An object of type `EncodeOptions`.
|
||||||
|
|
||||||
`client/index.ts` which exposes the following.
|
`client/index.ts` which exposes the following:
|
||||||
|
|
||||||
- `encode` - A method which takes args
|
- `encode` - A method which takes args:
|
||||||
- `AbortSignal`
|
- `AbortSignal`
|
||||||
- `WorkerBridge`
|
- `WorkerBridge`
|
||||||
- `ImageData`
|
- `ImageData`
|
||||||
@@ -51,4 +51,15 @@ Encoders must have the following:
|
|||||||
|
|
||||||
And returns (a promise for) an `ArrayBuffer`.
|
And returns (a promise for) an `ArrayBuffer`.
|
||||||
|
|
||||||
Optionally it may include a method `featureTest`, which returns a boolean indicating support for this decoder.
|
Optionally it may export a method `featureTest`, which returns a boolean indicating support for this decoder.
|
||||||
|
|
||||||
|
Optionally it may export a component, `Options`, with the following props:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface Props {
|
||||||
|
options: EncodeOptions;
|
||||||
|
onChange(newOptions: EncodeOptions): void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
…where `EncodeOptions` are the options for that encoder.
|
||||||
|
|||||||
58
src/features/client-utils/index.tsx
Normal file
58
src/features/client-utils/index.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { h, Component } from 'preact';
|
||||||
|
import * as style from 'client/lazy-app/Compress/Options/style.css';
|
||||||
|
import Range from 'client/lazy-app/Compress/Options/Range';
|
||||||
|
|
||||||
|
interface EncodeOptions {
|
||||||
|
quality: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
options: EncodeOptions;
|
||||||
|
onChange(newOptions: EncodeOptions): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QualityOptionArg {
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
step?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Constructor<T extends {} = {}> = new (...args: any[]) => T;
|
||||||
|
|
||||||
|
// TypeScript requires an exported type for returned classes. This serves as the
|
||||||
|
// type for the class returned by `qualityOption`.
|
||||||
|
export interface QualityOptionsInterface extends Component<Props, {}> {}
|
||||||
|
|
||||||
|
export function qualityOption(
|
||||||
|
opts: QualityOptionArg = {},
|
||||||
|
): Constructor<QualityOptionsInterface> {
|
||||||
|
const { min = 0, max = 100, step = 1 } = opts;
|
||||||
|
|
||||||
|
class QualityOptions extends Component<Props, {}> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { EncodeOptions } from '../shared/meta';
|
|
||||||
import type WorkerBridge from 'client/lazy-app/worker-bridge';
|
|
||||||
|
|
||||||
export const encode = (
|
|
||||||
signal: AbortSignal,
|
|
||||||
workerBridge: WorkerBridge,
|
|
||||||
imageData: ImageData,
|
|
||||||
options: EncodeOptions,
|
|
||||||
) => workerBridge.avifEncode(signal, imageData, options);
|
|
||||||
361
src/features/encoders/avif/client/index.tsx
Normal file
361
src/features/encoders/avif/client/index.tsx
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
import { EncodeOptions, defaultOptions } from '../shared/meta';
|
||||||
|
import type WorkerBridge from 'client/lazy-app/worker-bridge';
|
||||||
|
import { h, Component } from 'preact';
|
||||||
|
import { preventDefault, shallowEqual } from 'client/lazy-app/util';
|
||||||
|
import * as style from 'client/lazy-app/Compress/Options/style.css';
|
||||||
|
import Checkbox from 'client/lazy-app/Compress/Options/Checkbox';
|
||||||
|
import Expander from 'client/lazy-app/Compress/Options/Expander';
|
||||||
|
import Select from 'client/lazy-app/Compress/Options/Select';
|
||||||
|
import Range from 'client/lazy-app/Compress/Options/Range';
|
||||||
|
import linkState from 'linkstate';
|
||||||
|
|
||||||
|
export const encode = (
|
||||||
|
signal: AbortSignal,
|
||||||
|
workerBridge: WorkerBridge,
|
||||||
|
imageData: ImageData,
|
||||||
|
options: EncodeOptions,
|
||||||
|
) => workerBridge.avifEncode(signal, imageData, options);
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
options: EncodeOptions;
|
||||||
|
onChange(newOptions: EncodeOptions): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
options: EncodeOptions;
|
||||||
|
lossless: boolean;
|
||||||
|
maxQuality: number;
|
||||||
|
minQuality: number;
|
||||||
|
separateAlpha: boolean;
|
||||||
|
losslessAlpha: boolean;
|
||||||
|
maxAlphaQuality: number;
|
||||||
|
minAlphaQuality: number;
|
||||||
|
showAdvanced: boolean;
|
||||||
|
grayscale: boolean;
|
||||||
|
subsample: number;
|
||||||
|
tileRows: number;
|
||||||
|
tileCols: number;
|
||||||
|
effort: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxQuant = 63;
|
||||||
|
const maxSpeed = 10;
|
||||||
|
|
||||||
|
export class Options extends Component<Props, State> {
|
||||||
|
static getDerivedStateFromProps(
|
||||||
|
props: Props,
|
||||||
|
state: State,
|
||||||
|
): Partial<State> | null {
|
||||||
|
if (state.options && shallowEqual(state.options, props.options)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { options } = props;
|
||||||
|
|
||||||
|
const lossless = options.maxQuantizer === 0 && options.minQuantizer === 0;
|
||||||
|
const minQuantizerValue = lossless
|
||||||
|
? defaultOptions.minQuantizer
|
||||||
|
: options.minQuantizer;
|
||||||
|
const maxQuantizerValue = lossless
|
||||||
|
? defaultOptions.maxQuantizer
|
||||||
|
: options.maxQuantizer;
|
||||||
|
const losslessAlpha =
|
||||||
|
options.maxQuantizerAlpha === 0 && options.minQuantizerAlpha === 0;
|
||||||
|
const minQuantizerAlphaValue = losslessAlpha
|
||||||
|
? defaultOptions.minQuantizerAlpha
|
||||||
|
: options.minQuantizerAlpha;
|
||||||
|
const maxQuantizerAlphaValue = losslessAlpha
|
||||||
|
? defaultOptions.maxQuantizerAlpha
|
||||||
|
: options.maxQuantizerAlpha;
|
||||||
|
|
||||||
|
// Create default form state from options
|
||||||
|
return {
|
||||||
|
options,
|
||||||
|
lossless,
|
||||||
|
losslessAlpha,
|
||||||
|
maxQuality: maxQuant - minQuantizerValue,
|
||||||
|
minQuality: maxQuant - maxQuantizerValue,
|
||||||
|
separateAlpha:
|
||||||
|
options.maxQuantizer !== options.maxQuantizerAlpha ||
|
||||||
|
options.minQuantizer !== options.minQuantizerAlpha,
|
||||||
|
maxAlphaQuality: maxQuant - minQuantizerAlphaValue,
|
||||||
|
minAlphaQuality: maxQuant - maxQuantizerAlphaValue,
|
||||||
|
grayscale: options.subsample === 0,
|
||||||
|
subsample:
|
||||||
|
options.subsample === 0 || lossless
|
||||||
|
? defaultOptions.subsample
|
||||||
|
: options.subsample,
|
||||||
|
tileRows: options.tileRowsLog2,
|
||||||
|
tileCols: options.tileColsLog2,
|
||||||
|
effort: maxSpeed - options.speed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The rest of the defaults are set in getDerivedStateFromProps
|
||||||
|
state: State = {
|
||||||
|
showAdvanced: false,
|
||||||
|
} as State;
|
||||||
|
|
||||||
|
private _inputChangeCallbacks = new Map<string, (event: Event) => void>();
|
||||||
|
|
||||||
|
private _inputChange = (prop: keyof State, type: 'number' | 'boolean') => {
|
||||||
|
// Cache the callback for performance
|
||||||
|
if (!this._inputChangeCallbacks.has(prop)) {
|
||||||
|
this._inputChangeCallbacks.set(prop, (event: Event) => {
|
||||||
|
const formEl = event.target as HTMLInputElement | HTMLSelectElement;
|
||||||
|
const newVal =
|
||||||
|
type === 'boolean'
|
||||||
|
? 'checked' in formEl
|
||||||
|
? formEl.checked
|
||||||
|
: !!formEl.value
|
||||||
|
: Number(formEl.value);
|
||||||
|
|
||||||
|
const newState: Partial<State> = {
|
||||||
|
[prop]: newVal,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ensure that min cannot be greater than max
|
||||||
|
switch (prop) {
|
||||||
|
case 'maxQuality':
|
||||||
|
if (newVal < this.state.minQuality) {
|
||||||
|
newState.minQuality = newVal as number;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'minQuality':
|
||||||
|
if (newVal > this.state.maxQuality) {
|
||||||
|
newState.maxQuality = newVal as number;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'maxAlphaQuality':
|
||||||
|
if (newVal < this.state.minAlphaQuality) {
|
||||||
|
newState.minAlphaQuality = newVal as number;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'minAlphaQuality':
|
||||||
|
if (newVal > this.state.maxAlphaQuality) {
|
||||||
|
newState.maxAlphaQuality = newVal as number;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const optionState = {
|
||||||
|
...this.state,
|
||||||
|
...newState,
|
||||||
|
};
|
||||||
|
|
||||||
|
const maxQuantizer = optionState.lossless
|
||||||
|
? 0
|
||||||
|
: maxQuant - optionState.minQuality;
|
||||||
|
const minQuantizer = optionState.lossless
|
||||||
|
? 0
|
||||||
|
: maxQuant - optionState.maxQuality;
|
||||||
|
|
||||||
|
const newOptions: EncodeOptions = {
|
||||||
|
maxQuantizer,
|
||||||
|
minQuantizer,
|
||||||
|
maxQuantizerAlpha: optionState.separateAlpha
|
||||||
|
? optionState.losslessAlpha
|
||||||
|
? 0
|
||||||
|
: maxQuant - optionState.minAlphaQuality
|
||||||
|
: maxQuantizer,
|
||||||
|
minQuantizerAlpha: optionState.separateAlpha
|
||||||
|
? optionState.losslessAlpha
|
||||||
|
? 0
|
||||||
|
: maxQuant - optionState.maxAlphaQuality
|
||||||
|
: minQuantizer,
|
||||||
|
// Always set to 4:4:4 if lossless
|
||||||
|
subsample: optionState.grayscale
|
||||||
|
? 0
|
||||||
|
: optionState.lossless
|
||||||
|
? 3
|
||||||
|
: optionState.subsample,
|
||||||
|
tileColsLog2: optionState.tileCols,
|
||||||
|
tileRowsLog2: optionState.tileRows,
|
||||||
|
speed: maxSpeed - optionState.effort,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Updating options, so we don't recalculate in getDerivedStateFromProps.
|
||||||
|
newState.options = newOptions;
|
||||||
|
|
||||||
|
this.setState(
|
||||||
|
// It isn't clear to me why I have to cast this :)
|
||||||
|
newState as State,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.props.onChange(newOptions);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._inputChangeCallbacks.get(prop)!;
|
||||||
|
};
|
||||||
|
|
||||||
|
render(
|
||||||
|
_: Props,
|
||||||
|
{
|
||||||
|
effort,
|
||||||
|
grayscale,
|
||||||
|
lossless,
|
||||||
|
losslessAlpha,
|
||||||
|
maxAlphaQuality,
|
||||||
|
maxQuality,
|
||||||
|
minAlphaQuality,
|
||||||
|
minQuality,
|
||||||
|
separateAlpha,
|
||||||
|
showAdvanced,
|
||||||
|
subsample,
|
||||||
|
tileCols,
|
||||||
|
tileRows,
|
||||||
|
}: State,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<form class={style.optionsSection} onSubmit={preventDefault}>
|
||||||
|
<label class={style.optionInputFirst}>
|
||||||
|
<Checkbox
|
||||||
|
checked={lossless}
|
||||||
|
onChange={this._inputChange('lossless', 'boolean')}
|
||||||
|
/>
|
||||||
|
Lossless
|
||||||
|
</label>
|
||||||
|
<Expander>
|
||||||
|
{!lossless && (
|
||||||
|
<div>
|
||||||
|
<div class={style.optionOneCell}>
|
||||||
|
<Range
|
||||||
|
min="0"
|
||||||
|
max="62"
|
||||||
|
value={maxQuality}
|
||||||
|
onInput={this._inputChange('maxQuality', 'number')}
|
||||||
|
>
|
||||||
|
Max quality:
|
||||||
|
</Range>
|
||||||
|
</div>
|
||||||
|
<div class={style.optionOneCell}>
|
||||||
|
<Range
|
||||||
|
min="0"
|
||||||
|
max="62"
|
||||||
|
value={minQuality}
|
||||||
|
onInput={this._inputChange('minQuality', 'number')}
|
||||||
|
>
|
||||||
|
Min quality:
|
||||||
|
</Range>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Expander>
|
||||||
|
<label class={style.optionInputFirst}>
|
||||||
|
<Checkbox
|
||||||
|
checked={separateAlpha}
|
||||||
|
onChange={this._inputChange('separateAlpha', 'boolean')}
|
||||||
|
/>
|
||||||
|
Separate alpha quality
|
||||||
|
</label>
|
||||||
|
<Expander>
|
||||||
|
{separateAlpha && (
|
||||||
|
<div>
|
||||||
|
<label class={style.optionInputFirst}>
|
||||||
|
<Checkbox
|
||||||
|
checked={losslessAlpha}
|
||||||
|
onChange={this._inputChange('losslessAlpha', 'boolean')}
|
||||||
|
/>
|
||||||
|
Lossless alpha
|
||||||
|
</label>
|
||||||
|
<Expander>
|
||||||
|
{!losslessAlpha && (
|
||||||
|
<div>
|
||||||
|
<div class={style.optionOneCell}>
|
||||||
|
<Range
|
||||||
|
min="0"
|
||||||
|
max="62"
|
||||||
|
value={maxAlphaQuality}
|
||||||
|
onInput={this._inputChange('maxAlphaQuality', 'number')}
|
||||||
|
>
|
||||||
|
Max alpha quality:
|
||||||
|
</Range>
|
||||||
|
</div>
|
||||||
|
<div class={style.optionOneCell}>
|
||||||
|
<Range
|
||||||
|
min="0"
|
||||||
|
max="62"
|
||||||
|
value={minAlphaQuality}
|
||||||
|
onInput={this._inputChange('minAlphaQuality', 'number')}
|
||||||
|
>
|
||||||
|
Min alpha quality:
|
||||||
|
</Range>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Expander>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Expander>
|
||||||
|
<label class={style.optionInputFirst}>
|
||||||
|
<Checkbox
|
||||||
|
checked={showAdvanced}
|
||||||
|
onChange={linkState(this, 'showAdvanced')}
|
||||||
|
/>
|
||||||
|
Show advanced settings
|
||||||
|
</label>
|
||||||
|
<Expander>
|
||||||
|
{showAdvanced && (
|
||||||
|
<div>
|
||||||
|
{/*<label class={style.optionInputFirst}>
|
||||||
|
<Checkbox
|
||||||
|
data-set-state="grayscale"
|
||||||
|
checked={grayscale}
|
||||||
|
onChange={this._inputChange('grayscale', 'boolean')}
|
||||||
|
/>
|
||||||
|
Grayscale
|
||||||
|
</label>*/}
|
||||||
|
<Expander>
|
||||||
|
{!grayscale && !lossless && (
|
||||||
|
<label class={style.optionTextFirst}>
|
||||||
|
Subsample chroma:
|
||||||
|
<Select
|
||||||
|
data-set-state="subsample"
|
||||||
|
value={subsample}
|
||||||
|
onChange={this._inputChange('subsample', 'number')}
|
||||||
|
>
|
||||||
|
<option value="1">4:2:0</option>
|
||||||
|
{/*<option value="2">4:2:2</option>*/}
|
||||||
|
<option value="3">4:4:4</option>
|
||||||
|
</Select>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</Expander>
|
||||||
|
<div class={style.optionOneCell}>
|
||||||
|
<Range
|
||||||
|
min="0"
|
||||||
|
max="6"
|
||||||
|
value={tileRows}
|
||||||
|
onInput={this._inputChange('tileRows', 'number')}
|
||||||
|
>
|
||||||
|
Log2 of tile rows:
|
||||||
|
</Range>
|
||||||
|
</div>
|
||||||
|
<div class={style.optionOneCell}>
|
||||||
|
<Range
|
||||||
|
min="0"
|
||||||
|
max="6"
|
||||||
|
value={tileCols}
|
||||||
|
onInput={this._inputChange('tileCols', 'number')}
|
||||||
|
>
|
||||||
|
Log2 of tile cols:
|
||||||
|
</Range>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Expander>
|
||||||
|
<div class={style.optionOneCell}>
|
||||||
|
<Range
|
||||||
|
min="0"
|
||||||
|
max="10"
|
||||||
|
value={effort}
|
||||||
|
onInput={this._inputChange('effort', 'number')}
|
||||||
|
>
|
||||||
|
Effort:
|
||||||
|
</Range>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright 2020 Google Inc. All Rights Reserved.
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
/// <reference path="../../../../../missing-types.d.ts" />
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { canvasEncode } from 'client/lazy-app/util';
|
import { canvasEncode } from 'client/lazy-app/util';
|
||||||
import WorkerBridge from 'client/lazy-app/worker-bridge';
|
import WorkerBridge from 'client/lazy-app/worker-bridge';
|
||||||
|
import { qualityOption } from 'features/client-utils';
|
||||||
import { mimeType, EncodeOptions } from '../shared/meta';
|
import { mimeType, EncodeOptions } from '../shared/meta';
|
||||||
|
|
||||||
export const encode = (
|
export const encode = (
|
||||||
@@ -8,3 +9,5 @@ export const encode = (
|
|||||||
imageData: ImageData,
|
imageData: ImageData,
|
||||||
options: EncodeOptions,
|
options: EncodeOptions,
|
||||||
) => canvasEncode(imageData, mimeType, options.quality);
|
) => canvasEncode(imageData, mimeType, options.quality);
|
||||||
|
|
||||||
|
export const Options = qualityOption({ min: 0, max: 1, step: 0.01 });
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { EncodeOptions } from '../shared/meta';
|
|
||||||
import type WorkerBridge from 'client/lazy-app/worker-bridge';
|
|
||||||
|
|
||||||
export function encode(
|
|
||||||
signal: AbortSignal,
|
|
||||||
workerBridge: WorkerBridge,
|
|
||||||
imageData: ImageData,
|
|
||||||
options: EncodeOptions,
|
|
||||||
) {
|
|
||||||
return workerBridge.mozjpegEncode(signal, imageData, options);
|
|
||||||
}
|
|
||||||
299
src/features/encoders/mozjpeg/client/index.tsx
Normal file
299
src/features/encoders/mozjpeg/client/index.tsx
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
import { EncodeOptions, MozJpegColorSpace } from '../shared/meta';
|
||||||
|
import type WorkerBridge from 'client/lazy-app/worker-bridge';
|
||||||
|
import { h, Component } from 'preact';
|
||||||
|
import {
|
||||||
|
inputFieldChecked,
|
||||||
|
inputFieldValueAsNumber,
|
||||||
|
preventDefault,
|
||||||
|
} from 'client/lazy-app/util';
|
||||||
|
import * as style from 'client/lazy-app/Compress/Options/style.css';
|
||||||
|
import linkState from 'linkstate';
|
||||||
|
import Range from 'client/lazy-app/Compress/Options/Range';
|
||||||
|
import Checkbox from 'client/lazy-app/Compress/Options/Checkbox';
|
||||||
|
import Expander from 'client/lazy-app/Compress/Options/Expander';
|
||||||
|
import Select from 'client/lazy-app/Compress/Options/Select';
|
||||||
|
|
||||||
|
export function encode(
|
||||||
|
signal: AbortSignal,
|
||||||
|
workerBridge: WorkerBridge,
|
||||||
|
imageData: ImageData,
|
||||||
|
options: EncodeOptions,
|
||||||
|
) {
|
||||||
|
return workerBridge.mozjpegEncode(signal, imageData, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
options: EncodeOptions;
|
||||||
|
onChange(newOptions: EncodeOptions): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
showAdvanced: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Options extends Component<Props, State> {
|
||||||
|
state: State = {
|
||||||
|
showAdvanced: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
onChange = (event: Event) => {
|
||||||
|
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, { showAdvanced }: State) {
|
||||||
|
// I'm rendering both lossy and lossless forms, as it becomes much easier when
|
||||||
|
// gathering the data.
|
||||||
|
return (
|
||||||
|
<form class={style.optionsSection} onSubmit={preventDefault}>
|
||||||
|
<div class={style.optionOneCell}>
|
||||||
|
<Range
|
||||||
|
name="quality"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
value={options.quality}
|
||||||
|
onInput={this.onChange}
|
||||||
|
>
|
||||||
|
Quality:
|
||||||
|
</Range>
|
||||||
|
</div>
|
||||||
|
<label class={style.optionInputFirst}>
|
||||||
|
<Checkbox
|
||||||
|
checked={showAdvanced}
|
||||||
|
onChange={linkState(this, 'showAdvanced')}
|
||||||
|
/>
|
||||||
|
Show advanced settings
|
||||||
|
</label>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import {
|
|
||||||
canvasEncode,
|
|
||||||
abortable,
|
|
||||||
blobToArrayBuffer,
|
|
||||||
} from 'client/lazy-app/util';
|
|
||||||
import { EncodeOptions } from '../shared/meta';
|
|
||||||
import type WorkerBridge from 'client/lazy-app/worker-bridge';
|
|
||||||
|
|
||||||
export async function encode(
|
|
||||||
signal: AbortSignal,
|
|
||||||
workerBridge: WorkerBridge,
|
|
||||||
imageData: ImageData,
|
|
||||||
options: EncodeOptions,
|
|
||||||
) {
|
|
||||||
const pngBlob = await abortable(signal, canvasEncode(imageData, 'image/png'));
|
|
||||||
const pngBuffer = await abortable(signal, blobToArrayBuffer(pngBlob));
|
|
||||||
return workerBridge.oxipngEncode(signal, pngBuffer, options);
|
|
||||||
}
|
|
||||||
59
src/features/encoders/oxipng/client/index.tsx
Normal file
59
src/features/encoders/oxipng/client/index.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import {
|
||||||
|
canvasEncode,
|
||||||
|
abortable,
|
||||||
|
blobToArrayBuffer,
|
||||||
|
} from 'client/lazy-app/util';
|
||||||
|
import { EncodeOptions } from '../shared/meta';
|
||||||
|
import type WorkerBridge from 'client/lazy-app/worker-bridge';
|
||||||
|
import { h, Component } from 'preact';
|
||||||
|
import { inputFieldValueAsNumber, preventDefault } from 'client/lazy-app/util';
|
||||||
|
import * as style from 'client/lazy-app/Compress/Options/style.css';
|
||||||
|
import Range from 'client/lazy-app/Compress/Options/Range';
|
||||||
|
|
||||||
|
export async function encode(
|
||||||
|
signal: AbortSignal,
|
||||||
|
workerBridge: WorkerBridge,
|
||||||
|
imageData: ImageData,
|
||||||
|
options: EncodeOptions,
|
||||||
|
) {
|
||||||
|
const pngBlob = await abortable(signal, canvasEncode(imageData, 'image/png'));
|
||||||
|
const pngBuffer = await abortable(signal, blobToArrayBuffer(pngBlob));
|
||||||
|
return workerBridge.oxipngEncode(signal, pngBuffer, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
options: EncodeOptions;
|
||||||
|
onChange(newOptions: EncodeOptions): void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Options extends Component<Props, {}> {
|
||||||
|
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="3"
|
||||||
|
step="1"
|
||||||
|
value={options.level}
|
||||||
|
onInput={this.onChange}
|
||||||
|
>
|
||||||
|
Effort:
|
||||||
|
</Range>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { EncodeOptions } from '../shared/meta';
|
|
||||||
import type WorkerBridge from 'client/lazy-app/worker-bridge';
|
|
||||||
|
|
||||||
export const encode = (
|
|
||||||
signal: AbortSignal,
|
|
||||||
workerBridge: WorkerBridge,
|
|
||||||
imageData: ImageData,
|
|
||||||
options: EncodeOptions,
|
|
||||||
) => workerBridge.webpEncode(signal, imageData, options);
|
|
||||||
407
src/features/encoders/webp/client/index.tsx
Normal file
407
src/features/encoders/webp/client/index.tsx
Normal file
@@ -0,0 +1,407 @@
|
|||||||
|
import { EncodeOptions } from '../shared/meta';
|
||||||
|
import type WorkerBridge from 'client/lazy-app/worker-bridge';
|
||||||
|
import { h, Component } from 'preact';
|
||||||
|
import {
|
||||||
|
inputFieldCheckedAsNumber,
|
||||||
|
inputFieldValueAsNumber,
|
||||||
|
preventDefault,
|
||||||
|
} from 'client/lazy-app/util';
|
||||||
|
import * as style from 'client/lazy-app/Compress/Options/style.css';
|
||||||
|
import linkState from 'linkstate';
|
||||||
|
import Range from 'client/lazy-app/Compress/Options/Range';
|
||||||
|
import Checkbox from 'client/lazy-app/Compress/Options/Checkbox';
|
||||||
|
import Expander from 'client/lazy-app/Compress/Options/Expander';
|
||||||
|
import Select from 'client/lazy-app/Compress/Options/Select';
|
||||||
|
|
||||||
|
export const encode = (
|
||||||
|
signal: AbortSignal,
|
||||||
|
workerBridge: WorkerBridge,
|
||||||
|
imageData: ImageData,
|
||||||
|
options: EncodeOptions,
|
||||||
|
) => workerBridge.webpEncode(signal, imageData, options);
|
||||||
|
|
||||||
|
const enum WebPImageHint {
|
||||||
|
WEBP_HINT_DEFAULT, // default preset.
|
||||||
|
WEBP_HINT_PICTURE, // digital picture, like portrait, inner shot
|
||||||
|
WEBP_HINT_PHOTO, // outdoor photograph, with natural lighting
|
||||||
|
WEBP_HINT_GRAPH, // Discrete tone image (graph, map-tile etc).
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
options: EncodeOptions;
|
||||||
|
onChange(newOptions: EncodeOptions): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
showAdvanced: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// From kLosslessPresets in config_enc.c
|
||||||
|
// The format is [method, quality].
|
||||||
|
const losslessPresets: [number, number][] = [
|
||||||
|
[0, 0],
|
||||||
|
[1, 20],
|
||||||
|
[2, 25],
|
||||||
|
[3, 30],
|
||||||
|
[3, 50],
|
||||||
|
[4, 50],
|
||||||
|
[4, 75],
|
||||||
|
[4, 90],
|
||||||
|
[5, 90],
|
||||||
|
[6, 100],
|
||||||
|
];
|
||||||
|
const losslessPresetDefault = 6;
|
||||||
|
|
||||||
|
function determineLosslessQuality(quality: number, method: number): number {
|
||||||
|
const index = losslessPresets.findIndex(
|
||||||
|
([presetMethod, presetQuality]) =>
|
||||||
|
presetMethod === method && presetQuality === quality,
|
||||||
|
);
|
||||||
|
if (index !== -1) return index;
|
||||||
|
// Quality doesn't match one of the presets.
|
||||||
|
// This can happen when toggling 'lossless'.
|
||||||
|
return losslessPresetDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Options extends Component<Props, State> {
|
||||||
|
state: State = {
|
||||||
|
showAdvanced: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
onChange = (event: Event) => {
|
||||||
|
const form = (event.currentTarget as HTMLInputElement).closest(
|
||||||
|
'form',
|
||||||
|
) as HTMLFormElement;
|
||||||
|
const lossless = inputFieldCheckedAsNumber(form.lossless);
|
||||||
|
const { options } = this.props;
|
||||||
|
const losslessPresetValue = inputFieldValueAsNumber(
|
||||||
|
form.lossless_preset,
|
||||||
|
determineLosslessQuality(options.quality, options.method),
|
||||||
|
);
|
||||||
|
|
||||||
|
const newOptions: EncodeOptions = {
|
||||||
|
// Copy over options the form doesn't care about, eg emulate_jpeg_size
|
||||||
|
...options,
|
||||||
|
// And now stuff from the form:
|
||||||
|
lossless,
|
||||||
|
// Special-cased inputs:
|
||||||
|
// In lossless mode, the quality is derived from the preset.
|
||||||
|
quality: lossless
|
||||||
|
? losslessPresets[losslessPresetValue][1]
|
||||||
|
: inputFieldValueAsNumber(form.quality, options.quality),
|
||||||
|
// In lossless mode, the method is derived from the preset.
|
||||||
|
method: lossless
|
||||||
|
? losslessPresets[losslessPresetValue][0]
|
||||||
|
: inputFieldValueAsNumber(form.method_input, options.method),
|
||||||
|
image_hint: inputFieldCheckedAsNumber(form.image_hint, options.image_hint)
|
||||||
|
? WebPImageHint.WEBP_HINT_GRAPH
|
||||||
|
: WebPImageHint.WEBP_HINT_DEFAULT,
|
||||||
|
// .checked
|
||||||
|
exact: inputFieldCheckedAsNumber(form.exact, options.exact),
|
||||||
|
alpha_compression: inputFieldCheckedAsNumber(
|
||||||
|
form.alpha_compression,
|
||||||
|
options.alpha_compression,
|
||||||
|
),
|
||||||
|
autofilter: inputFieldCheckedAsNumber(
|
||||||
|
form.autofilter,
|
||||||
|
options.autofilter,
|
||||||
|
),
|
||||||
|
filter_type: inputFieldCheckedAsNumber(
|
||||||
|
form.filter_type,
|
||||||
|
options.filter_type,
|
||||||
|
),
|
||||||
|
use_sharp_yuv: inputFieldCheckedAsNumber(
|
||||||
|
form.use_sharp_yuv,
|
||||||
|
options.use_sharp_yuv,
|
||||||
|
),
|
||||||
|
// .value
|
||||||
|
near_lossless:
|
||||||
|
100 -
|
||||||
|
inputFieldValueAsNumber(
|
||||||
|
form.near_lossless,
|
||||||
|
100 - options.near_lossless,
|
||||||
|
),
|
||||||
|
alpha_quality: inputFieldValueAsNumber(
|
||||||
|
form.alpha_quality,
|
||||||
|
options.alpha_quality,
|
||||||
|
),
|
||||||
|
alpha_filtering: inputFieldValueAsNumber(
|
||||||
|
form.alpha_filtering,
|
||||||
|
options.alpha_filtering,
|
||||||
|
),
|
||||||
|
sns_strength: inputFieldValueAsNumber(
|
||||||
|
form.sns_strength,
|
||||||
|
options.sns_strength,
|
||||||
|
),
|
||||||
|
filter_strength: inputFieldValueAsNumber(
|
||||||
|
form.filter_strength,
|
||||||
|
options.filter_strength,
|
||||||
|
),
|
||||||
|
filter_sharpness:
|
||||||
|
7 -
|
||||||
|
inputFieldValueAsNumber(
|
||||||
|
form.filter_sharpness,
|
||||||
|
7 - options.filter_sharpness,
|
||||||
|
),
|
||||||
|
pass: inputFieldValueAsNumber(form.pass, options.pass),
|
||||||
|
preprocessing: inputFieldValueAsNumber(
|
||||||
|
form.preprocessing,
|
||||||
|
options.preprocessing,
|
||||||
|
),
|
||||||
|
segments: inputFieldValueAsNumber(form.segments, options.segments),
|
||||||
|
partitions: inputFieldValueAsNumber(form.partitions, options.partitions),
|
||||||
|
};
|
||||||
|
this.props.onChange(newOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
private _losslessSpecificOptions(options: EncodeOptions) {
|
||||||
|
return (
|
||||||
|
<div key="lossless">
|
||||||
|
<div class={style.optionOneCell}>
|
||||||
|
<Range
|
||||||
|
name="lossless_preset"
|
||||||
|
min="0"
|
||||||
|
max="9"
|
||||||
|
value={determineLosslessQuality(options.quality, options.method)}
|
||||||
|
onInput={this.onChange}
|
||||||
|
>
|
||||||
|
Effort:
|
||||||
|
</Range>
|
||||||
|
</div>
|
||||||
|
<div class={style.optionOneCell}>
|
||||||
|
<Range
|
||||||
|
name="near_lossless"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
value={'' + (100 - options.near_lossless)}
|
||||||
|
onInput={this.onChange}
|
||||||
|
>
|
||||||
|
Slight loss:
|
||||||
|
</Range>
|
||||||
|
</div>
|
||||||
|
<label class={style.optionInputFirst}>
|
||||||
|
{/*
|
||||||
|
Although there are 3 different kinds of image hint, webp only
|
||||||
|
seems to do something with the 'graph' type, and I don't really
|
||||||
|
understand what it does.
|
||||||
|
*/}
|
||||||
|
<Checkbox
|
||||||
|
name="image_hint"
|
||||||
|
checked={options.image_hint === WebPImageHint.WEBP_HINT_GRAPH}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
Discrete tone image
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _lossySpecificOptions(options: EncodeOptions) {
|
||||||
|
const { showAdvanced } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key="lossy">
|
||||||
|
<div class={style.optionOneCell}>
|
||||||
|
<Range
|
||||||
|
name="method_input"
|
||||||
|
min="0"
|
||||||
|
max="6"
|
||||||
|
value={options.method}
|
||||||
|
onInput={this.onChange}
|
||||||
|
>
|
||||||
|
Effort:
|
||||||
|
</Range>
|
||||||
|
</div>
|
||||||
|
<div class={style.optionOneCell}>
|
||||||
|
<Range
|
||||||
|
name="quality"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
step="0.1"
|
||||||
|
value={options.quality}
|
||||||
|
onInput={this.onChange}
|
||||||
|
>
|
||||||
|
Quality:
|
||||||
|
</Range>
|
||||||
|
</div>
|
||||||
|
<label class={style.optionInputFirst}>
|
||||||
|
<Checkbox
|
||||||
|
checked={showAdvanced}
|
||||||
|
onChange={linkState(this, 'showAdvanced')}
|
||||||
|
/>
|
||||||
|
Show advanced settings
|
||||||
|
</label>
|
||||||
|
<Expander>
|
||||||
|
{showAdvanced ? (
|
||||||
|
<div>
|
||||||
|
<label class={style.optionInputFirst}>
|
||||||
|
<Checkbox
|
||||||
|
name="alpha_compression"
|
||||||
|
checked={!!options.alpha_compression}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
Compress alpha
|
||||||
|
</label>
|
||||||
|
<div class={style.optionOneCell}>
|
||||||
|
<Range
|
||||||
|
name="alpha_quality"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
value={options.alpha_quality}
|
||||||
|
onInput={this.onChange}
|
||||||
|
>
|
||||||
|
Alpha quality:
|
||||||
|
</Range>
|
||||||
|
</div>
|
||||||
|
<div class={style.optionOneCell}>
|
||||||
|
<Range
|
||||||
|
name="alpha_filtering"
|
||||||
|
min="0"
|
||||||
|
max="2"
|
||||||
|
value={options.alpha_filtering}
|
||||||
|
onInput={this.onChange}
|
||||||
|
>
|
||||||
|
Alpha filter quality:
|
||||||
|
</Range>
|
||||||
|
</div>
|
||||||
|
<label class={style.optionInputFirst}>
|
||||||
|
<Checkbox
|
||||||
|
name="autofilter"
|
||||||
|
checked={!!options.autofilter}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
Auto adjust filter strength
|
||||||
|
</label>
|
||||||
|
<Expander>
|
||||||
|
{options.autofilter ? null : (
|
||||||
|
<div class={style.optionOneCell}>
|
||||||
|
<Range
|
||||||
|
name="filter_strength"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
value={options.filter_strength}
|
||||||
|
onInput={this.onChange}
|
||||||
|
>
|
||||||
|
Filter strength:
|
||||||
|
</Range>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Expander>
|
||||||
|
<label class={style.optionInputFirst}>
|
||||||
|
<Checkbox
|
||||||
|
name="filter_type"
|
||||||
|
checked={!!options.filter_type}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
Strong filter
|
||||||
|
</label>
|
||||||
|
<div class={style.optionOneCell}>
|
||||||
|
<Range
|
||||||
|
name="filter_sharpness"
|
||||||
|
min="0"
|
||||||
|
max="7"
|
||||||
|
value={7 - options.filter_sharpness}
|
||||||
|
onInput={this.onChange}
|
||||||
|
>
|
||||||
|
Filter sharpness:
|
||||||
|
</Range>
|
||||||
|
</div>
|
||||||
|
<label class={style.optionInputFirst}>
|
||||||
|
<Checkbox
|
||||||
|
name="use_sharp_yuv"
|
||||||
|
checked={!!options.use_sharp_yuv}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
Sharp RGB→YUV conversion
|
||||||
|
</label>
|
||||||
|
<div class={style.optionOneCell}>
|
||||||
|
<Range
|
||||||
|
name="pass"
|
||||||
|
min="1"
|
||||||
|
max="10"
|
||||||
|
value={options.pass}
|
||||||
|
onInput={this.onChange}
|
||||||
|
>
|
||||||
|
Passes:
|
||||||
|
</Range>
|
||||||
|
</div>
|
||||||
|
<div class={style.optionOneCell}>
|
||||||
|
<Range
|
||||||
|
name="sns_strength"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
value={options.sns_strength}
|
||||||
|
onInput={this.onChange}
|
||||||
|
>
|
||||||
|
Spatial noise shaping:
|
||||||
|
</Range>
|
||||||
|
</div>
|
||||||
|
<label class={style.optionTextFirst}>
|
||||||
|
Preprocess:
|
||||||
|
<Select
|
||||||
|
name="preprocessing"
|
||||||
|
value={options.preprocessing}
|
||||||
|
onChange={this.onChange}
|
||||||
|
>
|
||||||
|
<option value="0">None</option>
|
||||||
|
<option value="1">Segment smooth</option>
|
||||||
|
<option value="2">Pseudo-random dithering</option>
|
||||||
|
</Select>
|
||||||
|
</label>
|
||||||
|
<div class={style.optionOneCell}>
|
||||||
|
<Range
|
||||||
|
name="segments"
|
||||||
|
min="1"
|
||||||
|
max="4"
|
||||||
|
value={options.segments}
|
||||||
|
onInput={this.onChange}
|
||||||
|
>
|
||||||
|
Segments:
|
||||||
|
</Range>
|
||||||
|
</div>
|
||||||
|
<div class={style.optionOneCell}>
|
||||||
|
<Range
|
||||||
|
name="partitions"
|
||||||
|
min="0"
|
||||||
|
max="3"
|
||||||
|
value={options.partitions}
|
||||||
|
onInput={this.onChange}
|
||||||
|
>
|
||||||
|
Partitions:
|
||||||
|
</Range>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</Expander>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render({ options }: Props) {
|
||||||
|
// I'm rendering both lossy and lossless forms, as it becomes much easier when
|
||||||
|
// gathering the data.
|
||||||
|
return (
|
||||||
|
<form class={style.optionsSection} onSubmit={preventDefault}>
|
||||||
|
<label class={style.optionInputFirst}>
|
||||||
|
<Checkbox
|
||||||
|
name="lossless"
|
||||||
|
checked={!!options.lossless}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
Lossless
|
||||||
|
</label>
|
||||||
|
{options.lossless
|
||||||
|
? this._losslessSpecificOptions(options)
|
||||||
|
: this._lossySpecificOptions(options)}
|
||||||
|
<label class={style.optionInputFirst}>
|
||||||
|
<Checkbox
|
||||||
|
name="exact"
|
||||||
|
checked={!!options.exact}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
Preserve transparent data
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
98
src/features/processors/quantize/client/index.tsx
Normal file
98
src/features/processors/quantize/client/index.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { h, Component } from 'preact';
|
||||||
|
import { Options as QuantizeOptions } from '../shared/meta';
|
||||||
|
import * as style from 'client/lazy-app/Compress/Options/style.css';
|
||||||
|
import {
|
||||||
|
inputFieldValueAsNumber,
|
||||||
|
konami,
|
||||||
|
preventDefault,
|
||||||
|
} from 'client/lazy-app/util';
|
||||||
|
import Expander from 'client/lazy-app/Compress/Options/Expander';
|
||||||
|
import Select from 'client/lazy-app/Compress/Options/Select';
|
||||||
|
import Range from 'client/lazy-app/Compress/Options/Range';
|
||||||
|
|
||||||
|
const konamiPromise = konami();
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
options: QuantizeOptions;
|
||||||
|
onChange(newOptions: QuantizeOptions): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
extendedSettings: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Options extends Component<Props, State> {
|
||||||
|
state: State = { extendedSettings: false };
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
konamiPromise.then(() => {
|
||||||
|
this.setState({ extendedSettings: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import {
|
|
||||||
builtinResize,
|
|
||||||
BuiltinResizeMethod,
|
|
||||||
drawableToImageData,
|
|
||||||
} from 'client/lazy-app/util';
|
|
||||||
import {
|
|
||||||
BrowserResizeOptions,
|
|
||||||
VectorResizeOptions,
|
|
||||||
WorkerResizeOptions,
|
|
||||||
Options,
|
|
||||||
workerResizeMethods,
|
|
||||||
} from '../shared/meta';
|
|
||||||
import { getContainOffsets } from '../shared/util';
|
|
||||||
import type { SourceImage } from 'client/lazy-app/Compress';
|
|
||||||
import type WorkerBridge from 'client/lazy-app/worker-bridge';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether a set of options are worker resize options.
|
|
||||||
*
|
|
||||||
* @param opts
|
|
||||||
*/
|
|
||||||
function isWorkerOptions(opts: Options): opts is WorkerResizeOptions {
|
|
||||||
return (workerResizeMethods as string[]).includes(opts.method);
|
|
||||||
}
|
|
||||||
|
|
||||||
function browserResize(data: ImageData, opts: BrowserResizeOptions): ImageData {
|
|
||||||
let sx = 0;
|
|
||||||
let sy = 0;
|
|
||||||
let sw = data.width;
|
|
||||||
let sh = data.height;
|
|
||||||
|
|
||||||
if (opts.fitMethod === 'contain') {
|
|
||||||
({ sx, sy, sw, sh } = getContainOffsets(sw, sh, opts.width, opts.height));
|
|
||||||
}
|
|
||||||
|
|
||||||
return builtinResize(
|
|
||||||
data,
|
|
||||||
sx,
|
|
||||||
sy,
|
|
||||||
sw,
|
|
||||||
sh,
|
|
||||||
opts.width,
|
|
||||||
opts.height,
|
|
||||||
opts.method.slice('browser-'.length) as BuiltinResizeMethod,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function vectorResize(
|
|
||||||
data: HTMLImageElement,
|
|
||||||
opts: VectorResizeOptions,
|
|
||||||
): ImageData {
|
|
||||||
let sx = 0;
|
|
||||||
let sy = 0;
|
|
||||||
let sw = data.width;
|
|
||||||
let sh = data.height;
|
|
||||||
|
|
||||||
if (opts.fitMethod === 'contain') {
|
|
||||||
({ sx, sy, sw, sh } = getContainOffsets(sw, sh, opts.width, opts.height));
|
|
||||||
}
|
|
||||||
|
|
||||||
return drawableToImageData(data, {
|
|
||||||
sx,
|
|
||||||
sy,
|
|
||||||
sw,
|
|
||||||
sh,
|
|
||||||
width: opts.width,
|
|
||||||
height: opts.height,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function resize(
|
|
||||||
signal: AbortSignal,
|
|
||||||
source: SourceImage,
|
|
||||||
options: Options,
|
|
||||||
workerBridge: WorkerBridge,
|
|
||||||
) {
|
|
||||||
if (options.method === 'vector') {
|
|
||||||
if (!source.vectorImage) throw Error('No vector image available');
|
|
||||||
return vectorResize(source.vectorImage, options);
|
|
||||||
}
|
|
||||||
if (isWorkerOptions(options)) {
|
|
||||||
return workerBridge.resize(signal, source.preprocessed, options);
|
|
||||||
}
|
|
||||||
return browserResize(source.preprocessed, options);
|
|
||||||
}
|
|
||||||
334
src/features/processors/resize/client/index.tsx
Normal file
334
src/features/processors/resize/client/index.tsx
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
import {
|
||||||
|
builtinResize,
|
||||||
|
BuiltinResizeMethod,
|
||||||
|
drawableToImageData,
|
||||||
|
} from 'client/lazy-app/util';
|
||||||
|
import {
|
||||||
|
BrowserResizeOptions,
|
||||||
|
VectorResizeOptions,
|
||||||
|
WorkerResizeOptions,
|
||||||
|
Options as ResizeOptions,
|
||||||
|
workerResizeMethods,
|
||||||
|
} from '../shared/meta';
|
||||||
|
import { getContainOffsets } from '../shared/util';
|
||||||
|
import type { SourceImage } from 'client/lazy-app/Compress';
|
||||||
|
import type WorkerBridge from 'client/lazy-app/worker-bridge';
|
||||||
|
import { h, Component } from 'preact';
|
||||||
|
import linkState from 'linkstate';
|
||||||
|
import {
|
||||||
|
inputFieldValueAsNumber,
|
||||||
|
inputFieldValue,
|
||||||
|
preventDefault,
|
||||||
|
inputFieldChecked,
|
||||||
|
} from 'client/lazy-app/util';
|
||||||
|
import * as style from 'client/lazy-app/Compress/Options/style.css';
|
||||||
|
import { linkRef } from 'shared/initial-app/util';
|
||||||
|
import Select from 'client/lazy-app/Compress/Options/Select';
|
||||||
|
import Expander from 'client/lazy-app/Compress/Options/Expander';
|
||||||
|
import Checkbox from 'client/lazy-app/Compress/Options/Checkbox';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether a set of options are worker resize options.
|
||||||
|
*
|
||||||
|
* @param opts
|
||||||
|
*/
|
||||||
|
function isWorkerOptions(opts: ResizeOptions): opts is WorkerResizeOptions {
|
||||||
|
return (workerResizeMethods as string[]).includes(opts.method);
|
||||||
|
}
|
||||||
|
|
||||||
|
function browserResize(data: ImageData, opts: BrowserResizeOptions): ImageData {
|
||||||
|
let sx = 0;
|
||||||
|
let sy = 0;
|
||||||
|
let sw = data.width;
|
||||||
|
let sh = data.height;
|
||||||
|
|
||||||
|
if (opts.fitMethod === 'contain') {
|
||||||
|
({ sx, sy, sw, sh } = getContainOffsets(sw, sh, opts.width, opts.height));
|
||||||
|
}
|
||||||
|
|
||||||
|
return builtinResize(
|
||||||
|
data,
|
||||||
|
sx,
|
||||||
|
sy,
|
||||||
|
sw,
|
||||||
|
sh,
|
||||||
|
opts.width,
|
||||||
|
opts.height,
|
||||||
|
opts.method.slice('browser-'.length) as BuiltinResizeMethod,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function vectorResize(
|
||||||
|
data: HTMLImageElement,
|
||||||
|
opts: VectorResizeOptions,
|
||||||
|
): ImageData {
|
||||||
|
let sx = 0;
|
||||||
|
let sy = 0;
|
||||||
|
let sw = data.width;
|
||||||
|
let sh = data.height;
|
||||||
|
|
||||||
|
if (opts.fitMethod === 'contain') {
|
||||||
|
({ sx, sy, sw, sh } = getContainOffsets(sw, sh, opts.width, opts.height));
|
||||||
|
}
|
||||||
|
|
||||||
|
return drawableToImageData(data, {
|
||||||
|
sx,
|
||||||
|
sy,
|
||||||
|
sw,
|
||||||
|
sh,
|
||||||
|
width: opts.width,
|
||||||
|
height: opts.height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resize(
|
||||||
|
signal: AbortSignal,
|
||||||
|
source: SourceImage,
|
||||||
|
options: ResizeOptions,
|
||||||
|
workerBridge: WorkerBridge,
|
||||||
|
) {
|
||||||
|
if (options.method === 'vector') {
|
||||||
|
if (!source.vectorImage) throw Error('No vector image available');
|
||||||
|
return vectorResize(source.vectorImage, options);
|
||||||
|
}
|
||||||
|
if (isWorkerOptions(options)) {
|
||||||
|
return workerBridge.resize(signal, source.preprocessed, options);
|
||||||
|
}
|
||||||
|
return browserResize(source.preprocessed, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isVector: Boolean;
|
||||||
|
inputWidth: number;
|
||||||
|
inputHeight: number;
|
||||||
|
options: ResizeOptions;
|
||||||
|
onChange(newOptions: ResizeOptions): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
maintainAspect: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sizePresets = [0.25, 0.3333, 0.5, 1, 2, 3, 4];
|
||||||
|
|
||||||
|
export class Options extends Component<Props, State> {
|
||||||
|
state: State = {
|
||||||
|
maintainAspect: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
private form?: HTMLFormElement;
|
||||||
|
private presetWidths: { [idx: number]: number } = {};
|
||||||
|
private presetHeights: { [idx: number]: number } = {};
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.generatePresetValues(props.inputWidth, props.inputHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private reportOptions() {
|
||||||
|
const form = this.form!;
|
||||||
|
const width = form.width as HTMLInputElement;
|
||||||
|
const height = form.height as HTMLInputElement;
|
||||||
|
const { options } = this.props;
|
||||||
|
|
||||||
|
if (!width.checkValidity() || !height.checkValidity()) return;
|
||||||
|
|
||||||
|
const newOptions: ResizeOptions = {
|
||||||
|
width: inputFieldValueAsNumber(width),
|
||||||
|
height: inputFieldValueAsNumber(height),
|
||||||
|
method: form.resizeMethod.value,
|
||||||
|
premultiply: inputFieldChecked(form.premultiply, true),
|
||||||
|
linearRGB: inputFieldChecked(form.linearRGB, true),
|
||||||
|
// Casting, as the formfield only returns the correct values.
|
||||||
|
fitMethod: inputFieldValue(
|
||||||
|
form.fitMethod,
|
||||||
|
options.fitMethod,
|
||||||
|
) as ResizeOptions['fitMethod'],
|
||||||
|
};
|
||||||
|
this.props.onChange(newOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onChange = () => {
|
||||||
|
this.reportOptions();
|
||||||
|
};
|
||||||
|
|
||||||
|
private getAspect() {
|
||||||
|
return this.props.inputWidth / this.props.inputHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||||
|
if (!prevState.maintainAspect && this.state.maintainAspect) {
|
||||||
|
this.form!.height.value = Math.round(
|
||||||
|
Number(this.form!.width.value) / this.getAspect(),
|
||||||
|
);
|
||||||
|
this.reportOptions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps: Props) {
|
||||||
|
if (
|
||||||
|
this.props.inputWidth !== nextProps.inputWidth ||
|
||||||
|
this.props.inputHeight !== nextProps.inputHeight
|
||||||
|
) {
|
||||||
|
this.generatePresetValues(nextProps.inputWidth, nextProps.inputHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onWidthInput = () => {
|
||||||
|
if (this.state.maintainAspect) {
|
||||||
|
const width = inputFieldValueAsNumber(this.form!.width);
|
||||||
|
this.form!.height.value = Math.round(width / this.getAspect());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reportOptions();
|
||||||
|
};
|
||||||
|
|
||||||
|
private onHeightInput = () => {
|
||||||
|
if (this.state.maintainAspect) {
|
||||||
|
const height = inputFieldValueAsNumber(this.form!.height);
|
||||||
|
this.form!.width.value = Math.round(height * this.getAspect());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reportOptions();
|
||||||
|
};
|
||||||
|
|
||||||
|
private generatePresetValues(width: number, height: number) {
|
||||||
|
for (const preset of sizePresets) {
|
||||||
|
this.presetWidths[preset] = Math.round(width * preset);
|
||||||
|
this.presetHeights[preset] = Math.round(height * preset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPreset(): number | string {
|
||||||
|
const { width, height } = this.props.options;
|
||||||
|
|
||||||
|
for (const preset of sizePresets) {
|
||||||
|
if (
|
||||||
|
width === this.presetWidths[preset] &&
|
||||||
|
height === this.presetHeights[preset]
|
||||||
|
)
|
||||||
|
return preset;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'custom';
|
||||||
|
}
|
||||||
|
|
||||||
|
private onPresetChange = (event: Event) => {
|
||||||
|
const select = event.target as HTMLSelectElement;
|
||||||
|
if (select.value === 'custom') return;
|
||||||
|
const multiplier = Number(select.value);
|
||||||
|
(this.form!.width as HTMLInputElement).value =
|
||||||
|
Math.round(this.props.inputWidth * multiplier) + '';
|
||||||
|
(this.form!.height as HTMLInputElement).value =
|
||||||
|
Math.round(this.props.inputHeight * multiplier) + '';
|
||||||
|
this.reportOptions();
|
||||||
|
};
|
||||||
|
|
||||||
|
render({ options, isVector }: Props, { maintainAspect }: State) {
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
ref={linkRef(this, 'form')}
|
||||||
|
class={style.optionsSection}
|
||||||
|
onSubmit={preventDefault}
|
||||||
|
>
|
||||||
|
<label class={style.optionTextFirst}>
|
||||||
|
Method:
|
||||||
|
<Select
|
||||||
|
name="resizeMethod"
|
||||||
|
value={options.method}
|
||||||
|
onChange={this.onChange}
|
||||||
|
>
|
||||||
|
{isVector && <option value="vector">Vector</option>}
|
||||||
|
<option value="lanczos3">Lanczos3</option>
|
||||||
|
<option value="mitchell">Mitchell</option>
|
||||||
|
<option value="catrom">Catmull-Rom</option>
|
||||||
|
<option value="triangle">Triangle (bilinear)</option>
|
||||||
|
<option value="hqx">hqx (pixel art)</option>
|
||||||
|
<option value="browser-pixelated">Browser pixelated</option>
|
||||||
|
<option value="browser-low">Browser low quality</option>
|
||||||
|
<option value="browser-medium">Browser medium quality</option>
|
||||||
|
<option value="browser-high">Browser high quality</option>
|
||||||
|
</Select>
|
||||||
|
</label>
|
||||||
|
<label class={style.optionTextFirst}>
|
||||||
|
Preset:
|
||||||
|
<Select value={this.getPreset()} onChange={this.onPresetChange}>
|
||||||
|
{sizePresets.map((preset) => (
|
||||||
|
<option value={preset}>{preset * 100}%</option>
|
||||||
|
))}
|
||||||
|
<option value="custom">Custom</option>
|
||||||
|
</Select>
|
||||||
|
</label>
|
||||||
|
<label class={style.optionTextFirst}>
|
||||||
|
Width:
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
class={style.textField}
|
||||||
|
name="width"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={'' + options.width}
|
||||||
|
onInput={this.onWidthInput}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label class={style.optionTextFirst}>
|
||||||
|
Height:
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
class={style.textField}
|
||||||
|
name="height"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
value={'' + options.height}
|
||||||
|
onInput={this.onHeightInput}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<Expander>
|
||||||
|
{isWorkerOptions(options) ? (
|
||||||
|
<label class={style.optionInputFirst}>
|
||||||
|
<Checkbox
|
||||||
|
name="premultiply"
|
||||||
|
checked={options.premultiply}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
Premultiply alpha channel
|
||||||
|
</label>
|
||||||
|
) : null}
|
||||||
|
{isWorkerOptions(options) ? (
|
||||||
|
<label class={style.optionInputFirst}>
|
||||||
|
<Checkbox
|
||||||
|
name="linearRGB"
|
||||||
|
checked={options.linearRGB}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
Linear RGB
|
||||||
|
</label>
|
||||||
|
) : null}
|
||||||
|
</Expander>
|
||||||
|
<label class={style.optionInputFirst}>
|
||||||
|
<Checkbox
|
||||||
|
name="maintainAspect"
|
||||||
|
checked={maintainAspect}
|
||||||
|
onChange={linkState(this, 'maintainAspect')}
|
||||||
|
/>
|
||||||
|
Maintain aspect ratio
|
||||||
|
</label>
|
||||||
|
<Expander>
|
||||||
|
{maintainAspect ? null : (
|
||||||
|
<label class={style.optionTextFirst}>
|
||||||
|
Fit method:
|
||||||
|
<Select
|
||||||
|
name="fitMethod"
|
||||||
|
value={options.fitMethod}
|
||||||
|
onChange={this.onChange}
|
||||||
|
>
|
||||||
|
<option value="stretch">Stretch</option>
|
||||||
|
<option value="contain">Contain</option>
|
||||||
|
</Select>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</Expander>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user