Compare commits

..

3 Commits

Author SHA1 Message Date
7e91f4e5c5 Whoops 2018-11-06 13:38:32 +00:00
854d20af12 Download button on the outside. 2018-11-06 13:38:32 +00:00
97778c5ba6 Copy settings to other side button 2018-11-06 13:38:31 +00:00
16 changed files with 133 additions and 231 deletions

View File

@ -39,9 +39,5 @@ struct MozJpegOptions {
bool trellis_opt_zero;
bool trellis_opt_table;
int trellis_loops;
bool auto_subsample;
int chroma_subsample;
bool separate_chroma_quality;
int chroma_quality;
};
```

View File

@ -21,7 +21,7 @@
console.log('Version:', module.version().toString(16));
const image = await loadImage('../example.png');
const result = module.encode(image.data, image.width, image.height, {
quality: 75,
quality: 40,
baseline: false,
arithmetic: false,
progressive: true,
@ -29,14 +29,10 @@
smoothing: 0,
color_space: 3,
quant_table: 3,
trellis_multipass: false,
trellis_opt_zero: false,
trellis_opt_table: false,
trellis_multipass: true,
trellis_opt_zero: true,
trellis_opt_table: true,
trellis_loops: 1,
auto_subsample: true,
chroma_subsample: 2,
separate_chroma_quality: false,
chroma_quality: 75,
});
const blob = new Blob([result], {type: 'image/jpeg'});

View File

@ -29,10 +29,6 @@ struct MozJpegOptions {
bool trellis_opt_zero;
bool trellis_opt_table;
int trellis_loops;
bool auto_subsample;
int chroma_subsample;
bool separate_chroma_quality;
int chroma_quality;
};
int version() {
@ -123,6 +119,9 @@ val encode(std::string image_in, int image_width, int image_height, MozJpegOptio
*/
jpeg_set_defaults(&cinfo);
/* Now you can set any non-default parameters you wish to.
* Here we just illustrate the use of quality (quantization table) scaling:
*/
jpeg_set_colorspace(&cinfo, (J_COLOR_SPACE) opts.color_space);
if (opts.quant_table != -1) {
@ -143,23 +142,11 @@ val encode(std::string image_in, int image_width, int image_height, MozJpegOptio
jpeg_c_set_bool_param(&cinfo, JBOOLEAN_TRELLIS_Q_OPT, opts.trellis_opt_table);
jpeg_c_set_int_param(&cinfo, JINT_TRELLIS_NUM_LOOPS, opts.trellis_loops);
// A little hacky to build a string for this, but it means we can use set_quality_ratings which
// does some useful heuristic stuff.
std::string quality_str = std::to_string(opts.quality);
if (opts.separate_chroma_quality && opts.color_space == JCS_YCbCr) {
quality_str += "," + std::to_string(opts.chroma_quality);
}
char const *pqual = quality_str.c_str();
set_quality_ratings(&cinfo, (char*) pqual, opts.baseline);
if (!opts.auto_subsample && opts.color_space == JCS_YCbCr) {
cinfo.comp_info[0].h_samp_factor = opts.chroma_subsample;
cinfo.comp_info[0].v_samp_factor = opts.chroma_subsample;
}
if (!opts.baseline && opts.progressive) {
jpeg_simple_progression(&cinfo);
} else {
@ -222,10 +209,6 @@ EMSCRIPTEN_BINDINGS(my_module) {
.field("trellis_opt_zero", &MozJpegOptions::trellis_opt_zero)
.field("trellis_opt_table", &MozJpegOptions::trellis_opt_table)
.field("trellis_loops", &MozJpegOptions::trellis_loops)
.field("chroma_subsample", &MozJpegOptions::chroma_subsample)
.field("auto_subsample", &MozJpegOptions::auto_subsample)
.field("separate_chroma_quality", &MozJpegOptions::separate_chroma_quality)
.field("chroma_quality", &MozJpegOptions::chroma_quality)
;
function("version", &version);

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -17,10 +17,6 @@ export interface EncodeOptions {
trellis_opt_zero: boolean;
trellis_opt_table: boolean;
trellis_loops: number;
auto_subsample: boolean;
chroma_subsample: number;
separate_chroma_quality: boolean;
chroma_quality: number;
}
export interface EncoderState { type: typeof type; options: EncodeOptions; }
@ -42,8 +38,4 @@ export const defaultOptions: EncodeOptions = {
trellis_opt_zero: false,
trellis_opt_table: false,
trellis_loops: 1,
auto_subsample: true,
chroma_subsample: 2,
separate_chroma_quality: false,
chroma_quality: 75,
};

View File

@ -39,13 +39,8 @@ export default class MozJPEGEncoderOptions extends Component<Props, State> {
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),
@ -80,72 +75,6 @@ export default class MozJPEGEncoderOptions extends Component<Props, State> {
<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"
@ -190,6 +119,18 @@ export default class MozJPEGEncoderOptions extends Component<Props, State> {
Smoothing:
</Range>
</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>
<label class={style.optionTextFirst}>
Quantization:
<Select

View File

@ -26,10 +26,8 @@ const losslessPresets:[number, number][] = [
];
const losslessPresetDefault = 6;
function determineLosslessQuality(quality: number, method: number): number {
const index = losslessPresets.findIndex(
([presetMethod, presetQuality]) => presetMethod === method && presetQuality === quality,
);
function determineLosslessQuality(quality: number): number {
const index = losslessPresets.findIndex(item => item[1] === quality);
if (index !== -1) return index;
// Quality doesn't match one of the presets.
// This can happen when toggling 'lossless'.
@ -47,7 +45,7 @@ export default class WebPEncoderOptions extends Component<Props, State> {
const lossless = inputFieldCheckedAsNumber(form.lossless);
const { options } = this.props;
const losslessPresetValue = inputFieldValueAsNumber(
form.lossless_preset, determineLosslessQuality(options.quality, options.method),
form.lossless_preset, determineLosslessQuality(options.quality),
);
const newOptions: EncodeOptions = {
@ -99,7 +97,7 @@ export default class WebPEncoderOptions extends Component<Props, State> {
name="lossless_preset"
min="0"
max="9"
value={determineLosslessQuality(options.quality, options.method)}
value={determineLosslessQuality(options.quality)}
onInput={this.onChange}
>
Effort:

View File

@ -4,7 +4,7 @@ import { bind, linkRef, Fileish } from '../../lib/initial-util';
import * as style from './style.scss';
import { FileDropEvent } from './custom-els/FileDrop';
import './custom-els/FileDrop';
import SnackBarElement, { SnackOptions } from '../../lib/SnackBar';
import SnackBarElement from '../../lib/SnackBar';
import '../../lib/SnackBar';
import Intro from '../intro';
import '../custom-els/LoadingSpinner';
@ -39,7 +39,7 @@ export default class App extends Component<Props, State> {
import('../compress').then((module) => {
this.setState({ Compress: module.default });
}).catch(() => {
this.showSnack('Failed to load app');
this.showError('Failed to load app');
});
// In development, persist application state across hot reloads:
@ -66,9 +66,9 @@ export default class App extends Component<Props, State> {
}
@bind
private showSnack(message: string, options: SnackOptions = {}): Promise<string> {
private showError(error: string) {
if (!this.snackbar) throw Error('Snackbar missing');
return this.snackbar.showSnackbar(message, options);
this.snackbar.showSnackbar({ message: error });
}
render({}: Props, { file, Compress }: State) {
@ -76,9 +76,9 @@ export default class App extends Component<Props, State> {
<div id="app" class={style.app}>
<file-drop accept="image/*" onfiledrop={this.onFileDrop} class={style.drop}>
{(!file)
? <Intro onFile={this.onIntroPickFile} showSnack={this.showSnack} />
? <Intro onFile={this.onIntroPickFile} onError={this.showError} />
: (Compress)
? <Compress file={file} showSnack={this.showSnack} />
? <Compress file={file} onError={this.showError} />
: <loading-spinner class={style.appLoader}/>
}
<snack-bar ref={linkRef(this, 'snackbar')} />

View File

@ -129,8 +129,4 @@
.output-canvas {
flex-shrink: 0;
// This fixes a severe painting bug in Chrome.
// We should try to remove this once the issue is fixed.
// https://bugs.chromium.org/p/chromium/issues/detail?id=870222#c10
will-change: auto;
}

View File

@ -35,7 +35,6 @@ import { VectorResizeOptions, BitmapResizeOptions } from '../../codecs/resize/pr
import './custom-els/MultiPanel';
import Results from '../results';
import { ExpandIcon, CopyAcrossIconProps } from '../../lib/icons';
import SnackBarElement from 'src/lib/SnackBar';
export interface SourceImage {
file: File | Fileish;
@ -59,7 +58,7 @@ interface EncodedImage {
interface Props {
file: File | Fileish;
showSnack: SnackBarElement['showSnackbar'];
onError: (msg: string) => void;
}
interface State {
@ -251,24 +250,12 @@ export default class Compress extends Component<Props, State> {
}
}
private async onCopyToOtherClick(index: 0 | 1) {
private onCopyToOtherClick(index: 0 | 1) {
const otherIndex = (index + 1) % 2;
const oldSettings = this.state.images[otherIndex];
this.setState({
images: cleanSet(this.state.images, otherIndex, this.state.images[index]),
});
const result = await this.props.showSnack('Settings copied across', {
timeout: 5000,
actions: ['undo', 'dismiss'],
});
if (result !== 'undo') return;
this.setState({
images: cleanSet(this.state.images, otherIndex, oldSettings),
});
}
@bind
@ -331,7 +318,7 @@ export default class Compress extends Component<Props, State> {
console.error(err);
// Another file has been opened before this one processed.
if (this.state.loadingCounter !== loadingCounter) return;
this.props.showSnack('Invalid image');
this.props.onError('Invalid image');
this.setState({ loading: false });
}
}
@ -390,7 +377,7 @@ export default class Compress extends Component<Props, State> {
}
} catch (err) {
if (err.name === 'AbortError') return;
this.props.showSnack(`Processing error (type=${image.encoderState.type}): ${err}`);
this.props.onError(`Processing error (type=${image.encoderState.type}): ${err}`);
throw err;
}
}

View File

@ -12,7 +12,6 @@ import artworkIcon from './imgs/demos/artwork-icon.jpg';
import deviceScreenIcon from './imgs/demos/device-screen-icon.jpg';
import logoIcon from './imgs/demos/logo-icon.png';
import * as style from './style.scss';
import SnackBarElement from '../../lib/SnackBar';
const demos = [
{
@ -43,7 +42,7 @@ const demos = [
interface Props {
onFile: (file: File | Fileish) => void;
showSnack: SnackBarElement['showSnackbar'];
onError: (error: string) => void;
}
interface State {
fetchingDemoIndex?: number;
@ -80,7 +79,7 @@ export default class Intro extends Component<Props, State> {
this.props.onFile(file);
} catch (err) {
this.setState({ fetchingDemoIndex: undefined });
this.props.showSnack("Couldn't fetch demo image");
this.props.onError("Couldn't fetch demo image");
}
}

View File

@ -57,8 +57,6 @@ class RangeInputElement extends HTMLElement {
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 {

View File

@ -1,96 +1,114 @@
import * as style from './styles.css';
import './styles.css';
const DEFAULT_TIMEOUT = 2750;
export interface SnackOptions {
message: string;
timeout?: number;
actions?: string[];
actionText?: string;
actionHandler?: () => boolean | null;
}
function createSnack(message: string, options: SnackOptions): [Element, Promise<string>] {
const {
timeout = 0,
actions = [],
} = options;
export interface SnackShowResult {
action: boolean;
}
// Provide a default 'dismiss' action
if (!timeout && actions.length === 0) actions.push('dismiss');
class Snack {
private _onremove: ((result: SnackShowResult) => void)[] = [];
private _options: SnackOptions;
private _element: Element = document.createElement('div');
private _text: Element = document.createElement('div');
private _button: Element = document.createElement('button');
private _showing = false;
private _closeTimer?: number;
private _result: SnackShowResult = {
action: false,
};
const el = document.createElement('div');
el.className = style.snackbar;
el.setAttribute('aria-live', 'assertive');
el.setAttribute('aria-atomic', 'true');
el.setAttribute('aria-hidden', 'false');
constructor (options: SnackOptions, callback?: (result: SnackShowResult) => void) {
this._options = options;
const text = document.createElement('div');
text.className = style.text;
text.textContent = message;
el.appendChild(text);
this._element.className = 'snackbar';
this._element.setAttribute('aria-live', 'assertive');
this._element.setAttribute('aria-atomic', 'true');
this._element.setAttribute('aria-hidden', 'true');
const result = new Promise<string>((resolve) => {
let timeoutId: number;
this._text.className = 'snackbar--text';
this._text.textContent = options.message;
this._element.appendChild(this._text);
// Add action buttons
for (const action of actions) {
const button = document.createElement('button');
button.className = style.button;
button.textContent = action;
button.addEventListener('click', () => {
clearTimeout(timeoutId);
resolve(action);
if (options.actionText) {
this._button.className = 'snackbar--button';
this._button.textContent = options.actionText;
this._button.addEventListener('click', () => {
if (this._showing) {
if (options.actionHandler && options.actionHandler() === false) return;
this._result.action = true;
}
this.hide();
});
el.appendChild(button);
this._element.appendChild(this._button);
}
// Add timeout
if (timeout) {
timeoutId = self.setTimeout(
() => resolve(''),
timeout,
);
if (callback) {
this._onremove.push(callback);
}
});
}
return [el, result];
}
cancelTimer () {
if (this._closeTimer != null) clearTimeout(this._closeTimer);
}
export default class SnackBarElement extends HTMLElement {
private _snackbars: [string, SnackOptions, (action: Promise<string>) => void][] = [];
private _processingQueue = false;
/**
* Show a snackbar. Returns a promise for the name of the action clicked, or an empty string if no
* action is clicked.
*/
showSnackbar(message: string, options: SnackOptions = {}): Promise<string> {
return new Promise<string>((resolve) => {
this._snackbars.push([message, options, resolve]);
if (!this._processingQueue) this._processQueue();
show (parent: Element): Promise<SnackShowResult> {
if (this._showing) return Promise.resolve(this._result);
this._showing = true;
this.cancelTimer();
if (parent !== this._element.parentNode) {
parent.appendChild(this._element);
}
this._element.removeAttribute('aria-hidden');
this._closeTimer = setTimeout(this.hide.bind(this), this._options.timeout || DEFAULT_TIMEOUT);
return new Promise((resolve) => {
this._onremove.push(resolve);
});
}
private async _processQueue() {
this._processingQueue = true;
hide () {
if (!this._showing) return;
this._showing = false;
this.cancelTimer();
this._element.addEventListener('animationend', this.remove.bind(this));
this._element.setAttribute('aria-hidden', 'true');
}
while (this._snackbars[0]) {
const [message, options, resolver] = this._snackbars[0];
const [el, result] = createSnack(message, options);
// Pass the result back to the original showSnackbar call.
resolver(result);
this.appendChild(el);
remove () {
this.cancelTimer();
const parent = this._element.parentNode;
if (parent) parent.removeChild(this._element);
this._onremove.forEach(f => f(this._result));
this._onremove = [];
}
}
// Wait for the user to click an action, or for the snack to timeout.
await result;
export default class SnackBarElement extends HTMLElement {
private _snackbars: Snack[] = [];
private _processingStack = false;
// Transition the snack away.
el.setAttribute('aria-hidden', 'true');
await new Promise((resolve) => {
el.addEventListener('animationend', () => resolve());
});
el.remove();
showSnackbar (options: SnackOptions): Promise<SnackShowResult> {
return new Promise((resolve) => {
const snack = new Snack(options, resolve);
this._snackbars.push(snack);
this._processStack();
});
}
this._snackbars.shift();
}
this._processingQueue = false;
private async _processStack () {
if (this._processingStack === true || this._snackbars.length === 0) return;
this._processingStack = true;
await this._snackbars[0].show(this);
this._snackbars.shift();
this._processingStack = false;
this._processStack();
}
}

View File

@ -22,6 +22,7 @@ snack-bar {
transform-origin: center;
color: #eee;
z-index: 100;
pointer-events: none;
cursor: default;
will-change: transform;
animation: snackbar-show 300ms ease forwards 1;
@ -52,13 +53,13 @@ snack-bar {
}
}
.text {
.snackbar--text {
flex: 1 1 auto;
padding: 16px;
font-size: 100%;
}
.button {
.snackbar--button {
position: relative;
flex: 0 1 auto;
padding: 8px;
@ -74,15 +75,16 @@ snack-bar {
font-size: 100%;
text-transform: uppercase;
text-align: center;
pointer-events: all;
cursor: pointer;
overflow: hidden;
transition: background-color 200ms ease;
outline: none;
}
.button:hover {
.snackbar--button:hover {
background-color: rgba(0,0,0,0.15);
}
.button:focus:before {
.snackbar--button:focus:before {
content: '';
position: absolute;
left: 50%;

View File

@ -25,7 +25,6 @@ module.exports = function (_, env) {
path.join(__dirname, 'src/components'),
path.join(__dirname, 'src/codecs'),
path.join(__dirname, 'src/custom-els'),
path.join(__dirname, 'src/lib'),
];
return {
@ -108,7 +107,7 @@ module.exports = function (_, env) {
loader: 'typings-for-css-modules-loader',
options: {
modules: true,
localIdentName: isProd ? '[hash:base64:5]' : '[local]__[hash:base64:5]',
localIdentName: '[local]__[hash:base64:5]',
namedExport: true,
camelCase: true,
importLoaders: 1,
@ -200,11 +199,8 @@ module.exports = function (_, env) {
new OptimizeCssAssetsPlugin({
cssProcessorOptions: {
postcssReduceIdents: {
counterStyle: false,
gridTemplate: false,
keyframes: false
}
zindex: false,
discardComments: { removeAll: true }
}
}),