Compare commits

...

31 Commits

Author SHA1 Message Date
25239d46d5 update desc 2024-01-07 00:12:35 +08:00
b75244a309 更新描述 2024-01-07 00:03:56 +08:00
0ce1443622 增加了更新的中文翻译 2024-01-05 15:33:19 +08:00
b433e03893 增加了缺失的中文翻译 2024-01-04 14:02:36 +08:00
7766f64cc6 remove github workflow 2024-01-04 11:02:54 +08:00
8a29ce25dc 中文版本翻译 2024-01-04 10:55:53 +08:00
d87eff7645 Adding the Quite OK Image (QOI) Codec (#1384) 2023-10-20 09:28:59 +01:00
f374068fb2 check support and upgrade colors (#1356) 2023-09-29 14:03:28 +01:00
ecc715fe55 Merge pull request #1362 from Frank-Mayer/593
added snack bar inside noscript element
2023-04-12 14:31:49 -07:00
82caed4277 Merge branch 'dev' into 593 2023-04-12 23:13:11 +02:00
a7dff9475d Merge pull request #1348 from aryanpingle/keyboard-a11y
Better Keyboard A11y + Navigation
2023-04-12 09:11:16 -07:00
d168f7a447 Add focus animation to back button 2023-04-12 13:52:36 +05:30
edf9cb755e Add styles to save-import buttons 2023-04-12 13:52:36 +05:30
a7503e69a2 better wording wor error message 2023-04-11 11:17:19 +02:00
5a9733563e more information in the error message 2023-04-11 11:12:04 +02:00
2000e16ba2 Make text-fields match the dark theme 2023-04-08 22:35:55 +05:30
7dbe0a7714 Add focus effects to inputs
Provides better keyboard accessibility for:
* Checkboxes
* Range Inputs
* Copy-over Buttons
* Option-reveal Elements
Along with some minor design improvements
2023-04-08 22:35:55 +05:30
25bc43e409 added snack bar inside noscript element 2023-04-08 18:27:29 +02:00
cee51bf355 Merge pull request #1359 from harsh26shah03/save-and-import-side-settings
Feat : Save and import side settings
2023-04-04 15:41:45 -07:00
8d6daf0fc4 Merge branch 'dev' into save-and-import-side-settings 2023-04-04 15:39:09 -07:00
61209d0b62 Merge pull request #1358 from harsh26shah03/file-size-update-sample-data
Fix : Sample File Size Unit
2023-04-04 15:38:10 -07:00
d0b4855022 Merge branch 'dev' into file-size-update-sample-data 2023-04-04 15:36:38 -07:00
6cb64a59ca Merge pull request #1355 from harsh26shah03/sample-image-data-styles
Feat : added hover animation to sample data
2023-04-04 15:32:50 -07:00
979fba0af1 Feat : Save and import side settings
There were requests from multiple users that
they use squoosh for compression but for each
iteration side settings resets to default
causing issues and there is no way to save and
import side settings.

There will be two buttons adjacent to copy-over

save side settings : This will save side encoder
and latest settings to localstorage of browser

import side settings : This will import side encoder
and latest settings from localstorage of browser and
replace the existing settings

Also if there are saved settings in locaStorage then
whenever user loads the app it will take that settings
and populate the side so user do not have to repeatedly
enter same settings for similar compression operation
subject to user has saved side settings

Update:1

Import settings button remains disabled if there
is nothing to import

Whenever the side setting is saved there will be
event fired and eventually listened to enable import
button

All 2 operations show notifications now

Import notification has undo option

Update : 2

Changed Icon SVGs
2023-04-04 22:35:09 +05:30
b1df3e1d54 Feat : Save and import side settings
There were requests from multiple users that
they use squoosh for compression but for each
iteration side settings resets to default
causing issues and there is no way to save and
import side settings.

There will be two buttons adjacent to copy-over

save side settings : This will save side encoder
and latest settings to localstorage of browser

import side settings : This will import side encoder
and latest settings from localstorage of browser and
replace the existing settings

Also if there are saved settings in locaStorage then
whenever user loads the app it will take that settings
and populate the side so user do not have to repeatedly
enter same settings for similar compression operation
subject to user has saved side settings

Update:1

Import settings button remains disabled if there
is nothing to import

Whenever the side setting is saved there will be
event fired and eventually listened to enable import
button

All 2 operations show notifications now

Import notification has undo option
2023-04-04 15:20:06 +05:30
4f6138d97d Fix : Sample File Size Unit
Sample data had size label showing wrong memory units

13k instead of 13KB
2.8mb instead of 2.8MB

small b corresponds to bits and this would change entire
meaning of file so fixed it
2023-04-04 11:39:57 +05:30
6b6e3724d2 Undoing this as separate PR is raised 2023-04-04 11:34:40 +05:30
8ac5e6f678 Merge branch 'GoogleChromeLabs:dev' into sample-image-data-styles 2023-04-04 11:30:38 +05:30
a930e8d928 Merge pull request #1353 from harsh26shah03/filename-in-result-dropdowns
Feat : Original image name (file name)
2023-04-03 09:08:57 -07:00
c814700cd2 Feat : changed unit of sample data and added hover animation 2023-04-03 15:00:19 +05:30
dfdf2a7f71 Feat : Original image name (file name)
Inside Compress tab Original image string
will be appended by file name.

In mobile view due to space constrain there
will be only <file name>
2023-04-02 23:11:17 +05:30
52 changed files with 829 additions and 8942 deletions

View File

@ -1,36 +0,0 @@
---
name: Bug report
about: Something is not working as expected
labels:
---
**Before you start**
Please take a look at the [FAQ](https://github.com/GoogleChromeLabs/squoosh/wiki/FAQ) as well as the already opened issues! If nothing fits your problem, go ahead and fill out the following template:
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Version:**
- OS w/ version: [e.g. iOS 12]
- Browser w/ version [e.g. Chrome 70]
- Node version: [e.g. 10.11.0]
- npm version: [e.g. 6.4.1]
**Is your issue related to the quality of image compression?**
Please attach original and output images (you can drag & drop to attach).
- Original image
- Output image from Squoosh
**Additional context, screenshots, screencasts**
Add any other context about the problem here.

View File

@ -1,18 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
labels:
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Does other service/app have this feature?**
Add any service you know/use that has this feature (We want to know for research)
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@ -1,22 +0,0 @@
name: Node.js CI
on: [push, pull_request]
jobs:
build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v2
- id: nvmrc
uses: browniebroke/read-nvmrc-action@v1
- uses: actions/setup-node@v1
with:
node-version: '${{ steps.nvmrc.outputs.node_version }}'
- run: npm ci
- run: npm run build

View File

@ -1 +0,0 @@
npx lint-staged

42
codecs/qoi/Makefile Normal file
View File

@ -0,0 +1,42 @@
CODEC_URL = https://github.com/phoboslab/qoi/archive/8d35d93cdca85d2868246c2a8a80a1e2c16ba2a8.tar.gz
CODEC_DIR = node_modules/qoi
CODEC_BUILD_DIR:= $(CODEC_DIR)/build
ENVIRONMENT = worker
OUT_JS = enc/qoi_enc.js dec/qoi_dec.js
OUT_WASM := $(OUT_JS:.js=.wasm)
.PHONY: all clean
all: $(OUT_JS)
$(filter enc/%,$(OUT_JS)): enc/qoi_enc.o
$(filter dec/%,$(OUT_JS)): dec/qoi_dec.o
# ALL .js FILES
$(OUT_JS):
$(LD) \
$(LDFLAGS) \
--bind \
-s ENVIRONMENT=$(ENVIRONMENT) \
-s EXPORT_ES6=1 \
-o $@ \
$+
# ALL .o FILES
%.o: %.cpp $(CODEC_DIR)
$(CXX) -c \
$(CXXFLAGS) \
-I $(CODEC_DIR) \
-o $@ \
$<
# CREATE DIRECTORY
$(CODEC_DIR):
mkdir -p $(CODEC_DIR)
curl -sL $(CODEC_URL) | tar xz --strip 1 -C $(CODEC_DIR)
clean:
$(RM) $(OUT_JS) $(OUT_WASM)
$(MAKE) -C $(CODEC_DIR) clean

View File

@ -0,0 +1,30 @@
#include <emscripten/bind.h>
#include <emscripten/val.h>
#define QOI_IMPLEMENTATION
#include "qoi.h"
using namespace emscripten;
thread_local const val Uint8ClampedArray = val::global("Uint8ClampedArray");
thread_local const val ImageData = val::global("ImageData");
val decode(std::string qoiimage) {
qoi_desc desc;
uint8_t* rgba = (uint8_t*)qoi_decode(qoiimage.c_str(), qoiimage.length(), &desc, 4);
// Resultant width and height stored in descriptor
int decodedWidth = desc.width;
int decodedHeight = desc.height;
val result = ImageData.new_(
Uint8ClampedArray.new_(typed_memory_view(4 * decodedWidth * decodedHeight, rgba)),
decodedWidth, decodedHeight);
free(rgba);
return result;
}
EMSCRIPTEN_BINDINGS(my_module) {
function("decode", &decode);
}

7
codecs/qoi/dec/qoi_dec.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
export interface QOIModule extends EmscriptenWasm.Module {
decode(data: BufferSource): ImageData | null;
}
declare var moduleFactory: EmscriptenWasm.ModuleFactory<QOIModule>;
export default moduleFactory;

16
codecs/qoi/dec/qoi_dec.js generated Normal file

File diff suppressed because one or more lines are too long

BIN
codecs/qoi/dec/qoi_dec.wasm Normal file

Binary file not shown.

View File

@ -0,0 +1,36 @@
#include <emscripten/bind.h>
#include <emscripten/val.h>
#define QOI_IMPLEMENTATION
#include "qoi.h"
using namespace emscripten;
struct QoiOptions {};
thread_local const val Uint8Array = val::global("Uint8Array");
val encode(std::string buffer, int width, int height, QoiOptions options) {
int compressedSizeInBytes;
qoi_desc desc;
desc.width = width;
desc.height = height;
desc.channels = 4;
desc.colorspace = QOI_SRGB;
uint8_t* encodedData = (uint8_t*)qoi_encode(buffer.c_str(), &desc, &compressedSizeInBytes);
if (encodedData == NULL)
return val::null();
auto js_result =
Uint8Array.new_(typed_memory_view(compressedSizeInBytes, (const uint8_t*)encodedData));
free(encodedData);
return js_result;
}
EMSCRIPTEN_BINDINGS(my_module) {
value_object<QoiOptions>("QoiOptions");
function("encode", &encode);
}

14
codecs/qoi/enc/qoi_enc.d.ts vendored Normal file
View File

@ -0,0 +1,14 @@
export interface EncodeOptions {}
export interface QoiModule extends EmscriptenWasm.Module {
encode(
data: BufferSource,
width: number,
height: number,
options: EncodeOptions,
): Uint8Array;
}
declare var moduleFactory: EmscriptenWasm.ModuleFactory<QoiModule>;
export default moduleFactory;

16
codecs/qoi/enc/qoi_enc.js generated Normal file

File diff suppressed because one or more lines are too long

BIN
codecs/qoi/enc/qoi_enc.wasm Normal file

Binary file not shown.

7
codecs/qoi/package.json Normal file
View File

@ -0,0 +1,7 @@
{
"name": "qoi",
"scripts": {
"build": "../build-cpp.sh"
}
}

8676
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@
"debug": "node --inspect-brk node_modules/.bin/rollup -c",
"dev": "DEV_PORT=\"${DEV_PORT:=5000}\" run-p watch serve",
"watch": "rollup -cw",
"serve": "serve --listen=$DEV_PORT --config ../../../serve.json .tmp/build/static",
"serve": "serve --listen=5000 --config ../../../serve.json .tmp/build/static",
"prepare": "husky install"
},
"devDependencies": {

View File

@ -20,27 +20,12 @@ async function main() {
render(<App />, root);
}
main();
// Analytics
{
// Determine the current display mode.
const displayMode =
navigator.standalone ||
window.matchMedia('(display-mode: standalone)').matches
? 'standalone'
: 'browser';
// Setup analytics
window.ga = window.ga || ((...args) => (ga.q = ga.q || []).push(args));
ga('create', 'UA-128752250-1', 'auto');
ga('set', 'transport', 'beacon');
ga('set', 'dimension1', displayMode);
ga('send', 'pageview', '/index.html', { title: 'Squoosh' });
// Load the GA script without keeping the browser spinner going.
addEventListener('load', () => {
const script = document.createElement('script');
script.src = 'https://www.google-analytics.com/analytics.js';
document.head.appendChild(script);
});
function track() {
const url = new URL(location.href);
const from = url.searchParams.get('from') ?? '';
fetch(`https://go.mazhangjing.com/track-tuya-${from}`).catch(() => {});
}
main();
track();

View File

@ -2,6 +2,26 @@
display: inline-block;
position: relative;
--size: 17px;
&::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 200%;
height: 200%;
background-color: var(--main-theme-color);
border-radius: 999px;
opacity: 0.25;
transform: translate(-50%, -50%) scale(0);
transition-property: transform;
transition-duration: 250ms;
}
&:focus-within::before {
transform: translate(-50%, -50%) scale(1);
}
}
.real-checkbox {

View File

@ -47,6 +47,10 @@ range-input::before {
height: 12px;
}
range-input:focus-within .thumb {
outline: white solid 2px;
}
.thumb-wrapper {
position: absolute;
left: 6px;

View File

@ -11,6 +11,10 @@
padding: 3px calc(var(--thumb-size) / 2 + 3px);
}
.checkbox:focus-within .track {
outline: white solid 2px;
}
.thumb {
position: relative;
width: var(--thumb-size);

View File

@ -17,7 +17,7 @@ import Toggle from './Toggle';
import Select from './Select';
import { Options as QuantOptionsComponent } from 'features/processors/quantize/client';
import { Options as ResizeOptionsComponent } from 'features/processors/resize/client';
import { SwapIcon } from 'client/lazy-app/icons';
import { ImportIcon, SaveIcon, SwapIcon } from 'client/lazy-app/icons';
interface Props {
index: 0 | 1;
@ -29,10 +29,14 @@ interface Props {
onEncoderOptionsChange(index: 0 | 1, newOptions: EncoderOptions): void;
onProcessorOptionsChange(index: 0 | 1, newOptions: ProcessorState): void;
onCopyToOtherSideClick(index: 0 | 1): void;
onSaveSideSettingsClick(index: 0 | 1): void;
onImportSideSettingsClick(index: 0 | 1): void;
}
interface State {
supportedEncoderMap?: PartialButNotUndefined<typeof encoderMap>;
leftSideSettings?: string | null;
rightSideSettings?: string | null;
}
type PartialButNotUndefined<T> = {
@ -60,6 +64,8 @@ const supportedEncoderMapP: Promise<PartialButNotUndefined<typeof encoderMap>> =
export default class Options extends Component<Props, State> {
state: State = {
supportedEncoderMap: undefined,
leftSideSettings: localStorage.getItem('leftSideSettings'),
rightSideSettings: localStorage.getItem('rightSideSettings'),
};
constructor() {
@ -69,6 +75,29 @@ export default class Options extends Component<Props, State> {
);
}
private setLeftSideSettings = () => {
this.setState({
leftSideSettings: localStorage.getItem('leftSideSettings'),
});
};
private setRightSideSettings = () => {
this.setState({
rightSideSettings: localStorage.getItem('rightSideSettings'),
});
};
componentDidMount(): void {
// Changing the state when side setting is stored in localstorage
window.addEventListener('leftSideSettings', this.setLeftSideSettings);
window.addEventListener('rightSideSettings', this.setRightSideSettings);
}
componentWillUnmount(): void {
window.removeEventListener('leftSideSettings', this.setLeftSideSettings);
window.removeEventListener('removeSideSettings', this.setRightSideSettings);
}
private onEncoderTypeChange = (event: Event) => {
const el = event.currentTarget as HTMLSelectElement;
@ -110,6 +139,14 @@ export default class Options extends Component<Props, State> {
this.props.onCopyToOtherSideClick(this.props.index);
};
private onSaveSideSettingClick = () => {
this.props.onSaveSideSettingsClick(this.props.index);
};
private onImportSideSettingsClick = () => {
this.props.onImportSideSettingsClick(this.props.index);
};
render(
{ source, encoderState, processorState }: Props,
{ supportedEncoderMap }: State,
@ -131,18 +168,48 @@ export default class Options extends Component<Props, State> {
<div>
<h3 class={style.optionsTitle}>
<div class={style.titleAndButtons}>
Edit
<button
class={style.copyOverButton}
title="Copy settings to other side"
title="另一侧使用同样设置"
onClick={this.onCopyToOtherSideClick}
>
<SwapIcon />
</button>
<button
class={style.saveButton}
title="保存本侧设置"
onClick={this.onSaveSideSettingClick}
>
<SaveIcon />
</button>
<button
class={
style.importButton +
' ' +
(!this.state.leftSideSettings && this.props.index === 0
? style.buttonOpacity
: '') +
' ' +
(!this.state.rightSideSettings && this.props.index === 1
? style.buttonOpacity
: '')
}
title="导入本侧的设置"
onClick={this.onImportSideSettingsClick}
disabled={
// Disabled if this side's settings haven't been saved
(!this.state.leftSideSettings &&
this.props.index === 0) ||
(!this.state.rightSideSettings && this.props.index === 1)
}
>
<ImportIcon />
</button>
</div>
</h3>
<label class={style.sectionEnabler}>
Resize
<Toggle
name="resize.enable"
checked={!!processorState.resize.enabled}
@ -162,7 +229,7 @@ export default class Options extends Component<Props, State> {
</Expander>
<label class={style.sectionEnabler}>
Reduce palette
<Toggle
name="quantize.enable"
checked={!!processorState.quantize.enabled}
@ -181,7 +248,7 @@ export default class Options extends Component<Props, State> {
)}
</Expander>
<h3 class={style.optionsTitle}>Compress</h3>
<h3 class={style.optionsTitle}></h3>
<section class={`${style.optionOneCell} ${style.optionsSection}`}>
{supportedEncoderMap ? (
@ -190,14 +257,16 @@ export default class Options extends Component<Props, State> {
onChange={this.onEncoderTypeChange}
large
>
<option value="identity">Original Image</option>
<option value="identity">{`Original Image ${
this.props.source ? `(${this.props.source.file.name})` : ''
}`}</option>
{Object.entries(supportedEncoderMap).map(([type, encoder]) => (
<option value={type}>{encoder.meta.label}</option>
))}
</Select>
) : (
<Select large>
<option>Loading</option>
<option>...</option>
</Select>
)}
</section>

View File

@ -53,6 +53,16 @@
composes: option-toggle;
grid-template-columns: auto 1fr;
gap: 1em;
border-top: 1px solid #fff4;
transition-property: background-color;
transition-duration: 250ms;
}
.option-reveal:focus-within,
.option-reveal:hover {
background-color: #fff2;
}
.option-one-cell {
@ -73,11 +83,11 @@
}
.text-field {
background: var(--white);
color: var(--black);
background-color: var(--black);
color: var(--white);
font: inherit;
border: none;
padding: 6px 0 6px 10px;
padding: 6px 6px 6px 10px;
width: 100%;
box-sizing: border-box;
border-radius: 4px;
@ -118,4 +128,31 @@
svg {
fill: var(--header-text-color);
}
&:focus {
outline: var(--header-text-color) solid 2px;
outline-offset: 0.25em;
}
}
.save-button,
.import-button {
composes: title-button;
svg {
stroke: var(--header-text-color);
}
&:focus {
outline: var(--header-text-color) solid 2px;
outline-offset: 0.25em;
}
}
.button-opacity {
pointer-events: none;
cursor: not-allowed;
svg {
opacity: 0.5;
}
}

View File

@ -123,7 +123,7 @@ export default class Results extends Component<Props, State> {
class={showLoadingState ? style.downloadDisable : style.download}
href={downloadUrl}
download={imageFile ? imageFile.name : ''}
title="Download"
title="下载图片"
onClick={this.onDownload}
>
<svg class={style.downloadBlobs} viewBox="0 0 89.6 86.9">

View File

@ -111,6 +111,9 @@ async function decodeImage(
if (mimeType === 'image/webp2') {
return await workerBridge.wp2Decode(signal, blob);
}
if (mimeType === 'image/qoi') {
return await workerBridge.qoiDecode(signal, blob);
}
}
// Otherwise fall through and try built-in decoding for a laugh.
return await builtinDecode(signal, blob);
@ -281,24 +284,35 @@ export default class Compress extends Component<Props, State> {
source: undefined,
loading: false,
preprocessorState: defaultPreprocessorState,
// Tasking catched side settings if available otherwise taking default settings
sides: [
{
latestSettings: {
processorState: defaultProcessorState,
encoderState: undefined,
},
loading: false,
},
{
latestSettings: {
processorState: defaultProcessorState,
encoderState: {
type: 'mozJPEG',
options: encoderMap.mozJPEG.meta.defaultOptions,
localStorage.getItem('leftSideSettings')
? {
...JSON.parse(localStorage.getItem('leftSideSettings') as string),
loading: false,
}
: {
latestSettings: {
processorState: defaultProcessorState,
encoderState: undefined,
},
loading: false,
},
localStorage.getItem('rightSideSettings')
? {
...JSON.parse(localStorage.getItem('rightSideSettings') as string),
loading: false,
}
: {
latestSettings: {
processorState: defaultProcessorState,
encoderState: {
type: 'mozJPEG',
options: encoderMap.mozJPEG.meta.defaultOptions,
},
},
loading: false,
},
},
loading: false,
},
],
mobileView: this.widthQuery.matches,
};
@ -428,6 +442,99 @@ export default class Compress extends Component<Props, State> {
sides: cleanSet(this.state.sides, otherIndex, oldSettings),
});
};
/**
* This function saves encodedSettings and latestSettings of
* particular side in browser local storage
* @param index : (0|1)
* @returns
*/
private onSaveSideSettingsClick = async (index: 0 | 1) => {
if (index === 0) {
const leftSideSettings = JSON.stringify({
encodedSettings: this.state.sides[index].encodedSettings,
latestSettings: this.state.sides[index].latestSettings,
});
localStorage.setItem('leftSideSettings', leftSideSettings);
// Firing an event when we save side settings in localstorage
window.dispatchEvent(new CustomEvent('leftSideSettings'));
await this.props.showSnack('Left side settings saved', {
timeout: 1500,
actions: ['dismiss'],
});
return;
}
if (index === 1) {
const rightSideSettings = JSON.stringify({
encodedSettings: this.state.sides[index].encodedSettings,
latestSettings: this.state.sides[index].latestSettings,
});
localStorage.setItem('rightSideSettings', rightSideSettings);
// Firing an event when we save side settings in localstorage
window.dispatchEvent(new CustomEvent('rightSideSettings'));
await this.props.showSnack('Right side settings saved', {
timeout: 1500,
actions: ['dismiss'],
});
return;
}
};
/**
* This function sets the side state with catched localstorage
* value as per side index provided
* @param index : (0|1)
* @returns
*/
private onImportSideSettingsClick = async (index: 0 | 1) => {
const leftSideSettingsString = localStorage.getItem('leftSideSettings');
const rightSideSettingsString = localStorage.getItem('rightSideSettings');
if (index === 0 && leftSideSettingsString) {
const oldLeftSideSettings = this.state.sides[index];
const newLeftSideSettings = {
...this.state.sides[index],
...JSON.parse(leftSideSettingsString),
};
this.setState({
sides: cleanSet(this.state.sides, index, newLeftSideSettings),
});
const result = await this.props.showSnack('Left side settings imported', {
timeout: 3000,
actions: ['undo', 'dismiss'],
});
if (result === 'undo') {
this.setState({
sides: cleanSet(this.state.sides, index, oldLeftSideSettings),
});
}
return;
}
if (index === 1 && rightSideSettingsString) {
const oldRightSideSettings = this.state.sides[index];
const newRightSideSettings = {
...this.state.sides[index],
...JSON.parse(rightSideSettingsString),
};
this.setState({
sides: cleanSet(this.state.sides, index, newRightSideSettings),
});
const result = await this.props.showSnack(
'Right side settings imported',
{
timeout: 3000,
actions: ['undo', 'dismiss'],
},
);
if (result === 'undo') {
this.setState({
sides: cleanSet(this.state.sides, index, oldRightSideSettings),
});
}
return;
}
};
private onPreprocessorChange = async (
preprocessorState: PreprocessorState,
@ -829,6 +936,8 @@ export default class Compress extends Component<Props, State> {
onEncoderOptionsChange={this.onEncoderOptionsChange}
onProcessorOptionsChange={this.onProcessorOptionsChange}
onCopyToOtherSideClick={this.onCopyToOtherClick}
onSaveSideSettingsClick={this.onSaveSideSettingsClick}
onImportSideSettingsClick={this.onImportSideSettingsClick}
/>
));
@ -842,7 +951,7 @@ export default class Compress extends Component<Props, State> {
typeLabel={
side.latestSettings.encoderState
? encoderMap[side.latestSettings.encoderState.type].meta.label
: 'Original Image'
: `${side.file ? `${side.file.name}` : 'Original Image'}`
}
/>
));

View File

@ -113,6 +113,13 @@
& > svg {
width: 47px;
overflow: visible;
}
&:focus .back-blob {
stroke: var(--deep-blue);
stroke-width: 5px;
animation: strokePulse 500ms ease forwards;
}
@media (min-width: 600px) {
@ -124,6 +131,15 @@
}
}
@keyframes strokePulse {
from {
stroke-width: 8px;
}
to {
stroke-width: 5px;
}
}
.back-blob {
fill: var(--hot-pink);
opacity: 0.77;

View File

@ -97,3 +97,31 @@ export const SwapIcon = () => (
<path d="M5.5 3.6v6.8L2.1 7l3.4-3.4M7 0L0 7l7 7V0zm4 0v14l7-7-7-7z" />
</svg>
);
export const SaveIcon = () => (
<svg viewBox="0 0 24 24">
<g
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<path d="M12.501 20.93c-.866.25-1.914-.166-2.176-1.247a1.724 1.724 0 0 0-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 0 0-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 0 0 1.066-2.573c-.94-1.543.826-3.31 2.37-2.37c1 .608 2.296.07 2.572-1.065c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.074.26 1.49 1.296 1.252 2.158M19 22v-6m3 3l-3-3l-3 3" />
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0-6 0" />
</g>
</svg>
);
export const ImportIcon = () => (
<svg viewBox="0 0 24 24">
<g
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
>
<path d="M12.52 20.924c-.87.262-1.93-.152-2.195-1.241a1.724 1.724 0 0 0-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 0 0-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 0 0 1.066-2.573c-.94-1.543.826-3.31 2.37-2.37c1 .608 2.296.07 2.572-1.065c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.088.264 1.502 1.323 1.242 2.192M19 16v6m3-3l-3 3l-3-3" />
<path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0-6 0" />
</g>
</svg>
);

View File

@ -103,6 +103,7 @@ const magicNumberMapInput = [
[/^\x00\x00\x00 ftypavif\x00\x00\x00\x00/, 'image/avif'],
[/^\xff\x0a/, 'image/jxl'],
[/^\x00\x00\x00\x0cJXL \x0d\x0a\x87\x0a/, 'image/jxl'],
[/^qoif/, 'image/qoi'],
] as const;
export type ImageMimeTypes = typeof magicNumberMapInput[number][1];

View File

@ -103,7 +103,7 @@
async () => {
n
? location.reload()
: t('Ready to work offline', { timeout: 5e3 });
: t('应用现在可以离线使用', { timeout: 5e3 });
},
),
!n)
@ -112,9 +112,9 @@
const r = await navigator.serviceWorker.getRegistration();
r &&
(await a(r),
'reload' ===
(await t('Update available', {
actions: ['reload', 'dismiss'],
'重新加载' ===
(await t('有新版本可用', {
actions: ['重新加载', '取消'],
})) &&
(async function () {
const e = await navigator.serviceWorker.getRegistration();

View File

@ -46,7 +46,7 @@ export function qualityOption(
value={options.quality}
onInput={this.onChange}
>
Quality:
:
</Range>
</div>
</div>

View File

@ -0,0 +1,19 @@
import qoiDecoder, { QOIModule } from 'codecs/qoi/dec/qoi_dec';
import { initEmscriptenModule, blobToArrayBuffer } from 'features/worker-utils';
let emscriptenModule: Promise<QOIModule>;
export default async function decode(blob: Blob): Promise<ImageData> {
if (!emscriptenModule) {
emscriptenModule = initEmscriptenModule(qoiDecoder);
}
const [module, data] = await Promise.all([
emscriptenModule,
blobToArrayBuffer(blob),
]);
const result = module.decode(data);
if (!result) throw new Error('Decoding error');
return result;
}

View File

@ -172,7 +172,7 @@ export class Options extends Component<Props, State> {
return (
<form class={style.optionsSection} onSubmit={preventDefault}>
<label class={style.optionToggle}>
Lossless
<Checkbox
checked={lossless}
onChange={this._inputChange('lossless', 'boolean')}
@ -187,7 +187,7 @@ export class Options extends Component<Props, State> {
value={quality}
onInput={this._inputChange('quality', 'number')}
>
Quality:
:
</Range>
</div>
)}
@ -197,7 +197,7 @@ export class Options extends Component<Props, State> {
checked={showAdvanced}
onChange={linkState(this, 'showAdvanced')}
/>
Advanced settings
</label>
<Expander>
{showAdvanced && (
@ -206,7 +206,7 @@ export class Options extends Component<Props, State> {
{!lossless && (
<div>
<label class={style.optionTextFirst}>
Subsample chroma:
:
<Select
value={subsample}
onChange={this._inputChange('subsample', 'number')}
@ -217,7 +217,7 @@ export class Options extends Component<Props, State> {
</Select>
</label>
<label class={style.optionToggle}>
Separate alpha quality
Alpha
<Checkbox
checked={separateAlpha}
onChange={this._inputChange('separateAlpha', 'boolean')}
@ -235,13 +235,13 @@ export class Options extends Component<Props, State> {
'number',
)}
>
Alpha quality:
Alpha :
</Range>
</div>
)}
</Expander>
<label class={style.optionToggle}>
Extra chroma compression
<Checkbox
checked={chromaDeltaQ}
onChange={this._inputChange('chromaDeltaQ', 'boolean')}
@ -254,7 +254,7 @@ export class Options extends Component<Props, State> {
value={sharpness}
onInput={this._inputChange('sharpness', 'number')}
>
Sharpness:
:
</Range>
</div>
<div class={style.optionOneCell}>
@ -264,11 +264,11 @@ export class Options extends Component<Props, State> {
value={denoiseLevel}
onInput={this._inputChange('denoiseLevel', 'number')}
>
Noise synthesis:
:
</Range>
</div>
<label class={style.optionTextFirst}>
Tuning:
:
<Select
value={tune}
onChange={this._inputChange('tune', 'number')}
@ -288,7 +288,7 @@ export class Options extends Component<Props, State> {
value={tileRows}
onInput={this._inputChange('tileRows', 'number')}
>
Log2 of tile rows:
:
</Range>
</div>
<div class={style.optionOneCell}>
@ -298,7 +298,7 @@ export class Options extends Component<Props, State> {
value={tileCols}
onInput={this._inputChange('tileCols', 'number')}
>
Log2 of tile cols:
:
</Range>
</div>
</div>
@ -311,7 +311,7 @@ export class Options extends Component<Props, State> {
value={effort}
onInput={this._inputChange('effort', 'number')}
>
Effort:
:
</Range>
</div>
</form>

View File

@ -133,7 +133,7 @@ export class Options extends Component<Props, State> {
return (
<form class={style.optionsSection} onSubmit={preventDefault}>
<label class={style.optionToggle}>
Lossless
<Checkbox
name="lossless"
checked={lossless}
@ -143,7 +143,7 @@ export class Options extends Component<Props, State> {
<Expander>
{lossless && (
<label class={style.optionToggle}>
Slight loss
<Checkbox
name="slightLoss"
checked={slightLoss}
@ -163,11 +163,11 @@ export class Options extends Component<Props, State> {
value={quality}
onInput={this._inputChange('quality', 'number')}
>
Quality:
:
</Range>
</div>
<label class={style.optionToggle}>
Alternative lossy mode
<Checkbox
checked={quality < 7 ? true : alternativeLossy}
disabled={quality < 7}
@ -175,7 +175,7 @@ export class Options extends Component<Props, State> {
/>
</label>
<label class={style.optionToggle}>
Auto edge filter
<Checkbox
checked={autoEdgePreservingFilter}
onChange={this._inputChange(
@ -196,7 +196,7 @@ export class Options extends Component<Props, State> {
'number',
)}
>
Edge preserving filter:
:
</Range>
</div>
)}
@ -208,7 +208,7 @@ export class Options extends Component<Props, State> {
value={decodingSpeedTier}
onInput={this._inputChange('decodingSpeedTier', 'number')}
>
Optimise for decoding speed (worse compression):
():
</Range>
</div>
<div class={style.optionOneCell}>
@ -219,14 +219,14 @@ export class Options extends Component<Props, State> {
value={photonNoiseIso}
onInput={this._inputChange('photonNoiseIso', 'number')}
>
Noise equivalent to ISO:
ISONEQ ISO:
</Range>
</div>
</div>
)}
</Expander>
<label class={style.optionToggle}>
Progressive rendering
<Checkbox
name="progressive"
checked={progressive}
@ -240,7 +240,7 @@ export class Options extends Component<Props, State> {
value={effort}
onInput={this._inputChange('effort', 'number')}
>
Effort:
:
</Range>
</div>
</form>

View File

@ -114,7 +114,7 @@ export class Options extends Component<Props, State> {
value={options.quality}
onInput={this.onChange}
>
Quality:
:
</Range>
</div>
<label class={style.optionReveal}>
@ -122,13 +122,13 @@ export class Options extends Component<Props, State> {
checked={showAdvanced}
onChange={linkState(this, 'showAdvanced')}
/>
Advanced settings
</label>
<Expander>
{showAdvanced ? (
<div>
<label class={style.optionTextFirst}>
Channels:
:
<Select
name="color_space"
value={options.color_space}
@ -143,7 +143,7 @@ export class Options extends Component<Props, State> {
{options.color_space === MozJpegColorSpace.YCbCr ? (
<div>
<label class={style.optionToggle}>
Auto subsample chroma
<Checkbox
name="auto_subsample"
checked={options.auto_subsample}
@ -160,13 +160,13 @@ export class Options extends Component<Props, State> {
value={options.chroma_subsample}
onInput={this.onChange}
>
Subsample chroma by:
:
</Range>
</div>
)}
</Expander>
<label class={style.optionToggle}>
Separate chroma quality
<Checkbox
name="separate_chroma_quality"
checked={options.separate_chroma_quality}
@ -183,7 +183,7 @@ export class Options extends Component<Props, State> {
value={options.chroma_quality}
onInput={this.onChange}
>
Chroma quality:
:
</Range>
</div>
) : null}
@ -192,7 +192,7 @@ export class Options extends Component<Props, State> {
) : null}
</Expander>
<label class={style.optionToggle}>
Pointless spec compliance
<Checkbox
name="baseline"
checked={options.baseline}
@ -202,7 +202,7 @@ export class Options extends Component<Props, State> {
<Expander>
{options.baseline ? null : (
<label class={style.optionToggle}>
Progressive rendering
<Checkbox
name="progressive"
checked={options.progressive}
@ -214,7 +214,7 @@ export class Options extends Component<Props, State> {
<Expander>
{options.baseline ? (
<label class={style.optionToggle}>
Optimize Huffman table
Huffman
<Checkbox
name="optimize_coding"
checked={options.optimize_coding}
@ -231,11 +231,11 @@ export class Options extends Component<Props, State> {
value={options.smoothing}
onInput={this.onChange}
>
Smoothing:
:
</Range>
</div>
<label class={style.optionTextFirst}>
Quantization:
:
<Select
name="quant_table"
value={options.quant_table}
@ -253,7 +253,7 @@ export class Options extends Component<Props, State> {
</Select>
</label>
<label class={style.optionToggle}>
Trellis multipass
Trellis
<Checkbox
name="trellis_multipass"
checked={options.trellis_multipass}
@ -263,7 +263,7 @@ export class Options extends Component<Props, State> {
<Expander>
{options.trellis_multipass ? (
<label class={style.optionToggle}>
Optimize zero block runs
<Checkbox
name="trellis_opt_zero"
checked={options.trellis_opt_zero}
@ -273,7 +273,7 @@ export class Options extends Component<Props, State> {
) : null}
</Expander>
<label class={style.optionToggle}>
Optimize after trellis quantization
<Checkbox
name="trellis_opt_table"
checked={options.trellis_opt_table}
@ -288,7 +288,7 @@ export class Options extends Component<Props, State> {
value={options.trellis_loops}
onInput={this.onChange}
>
Trellis quantization passes:
Trellis :
</Range>
</div>
</div>

View File

@ -45,7 +45,7 @@ export class Options extends Component<Props, {}> {
return (
<form class={style.optionsSection} onSubmit={preventDefault}>
<label class={style.optionToggle}>
Interlace
<Checkbox
name="interlace"
checked={options.interlace}
@ -61,7 +61,7 @@ export class Options extends Component<Props, {}> {
value={options.level}
onInput={this.onChange}
>
Effort:
:
</Range>
</div>
</form>

View File

@ -0,0 +1,11 @@
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.qoiEncode(signal, imageData, options);
}

View File

@ -0,0 +1,13 @@
/**
* 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" />

View File

@ -0,0 +1,19 @@
/**
* 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.
*/
import { EncodeOptions } from 'codecs/qoi/enc/qoi_enc';
export { EncodeOptions };
export const label = 'QOI';
export const mimeType = 'image/qoi';
export const extension = 'qoi';
export const defaultOptions: EncodeOptions = {};

View File

@ -0,0 +1,13 @@
/**
* 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" />

View File

@ -0,0 +1,13 @@
/**
* 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" />

View File

@ -0,0 +1,35 @@
/**
* 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.
*/
import qoiEncoder, { QoiModule } from 'codecs/qoi/enc/qoi_enc';
import type { EncodeOptions } from '../shared/meta';
import { initEmscriptenModule } from 'features/worker-utils';
let emscriptenModule: Promise<QoiModule>;
async function init() {
return initEmscriptenModule(qoiEncoder);
}
export default async function encode(
data: ImageData,
options: EncodeOptions,
): Promise<ArrayBuffer> {
if (!emscriptenModule) {
emscriptenModule = init();
}
const module = await emscriptenModule;
const resultView = module.encode(data.data, data.width, data.height, options);
// wasm cant run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
return resultView.buffer as ArrayBuffer;
}

View File

@ -166,7 +166,7 @@ export class Options extends Component<Props, State> {
value={determineLosslessQuality(options.quality, options.method)}
onInput={this.onChange}
>
Effort:
:
</Range>
</div>
<div class={style.optionOneCell}>
@ -177,11 +177,11 @@ export class Options extends Component<Props, State> {
value={'' + (100 - options.near_lossless)}
onInput={this.onChange}
>
Slight loss:
:
</Range>
</div>
<label class={style.optionToggle}>
Discrete tone image
{/*
Although there are 3 different kinds of image hint, webp only
seems to do something with the 'graph' type, and I don't really
@ -210,7 +210,7 @@ export class Options extends Component<Props, State> {
value={options.method}
onInput={this.onChange}
>
Effort:
:
</Range>
</div>
<div class={style.optionOneCell}>
@ -222,7 +222,7 @@ export class Options extends Component<Props, State> {
value={options.quality}
onInput={this.onChange}
>
Quality:
:
</Range>
</div>
<label class={style.optionReveal}>
@ -230,13 +230,13 @@ export class Options extends Component<Props, State> {
checked={showAdvanced}
onChange={linkState(this, 'showAdvanced')}
/>
Advanced settings
</label>
<Expander>
{showAdvanced ? (
<div>
<label class={style.optionToggle}>
Compress alpha
Alpha
<Checkbox
name="alpha_compression"
checked={!!options.alpha_compression}
@ -251,7 +251,7 @@ export class Options extends Component<Props, State> {
value={options.alpha_quality}
onInput={this.onChange}
>
Alpha quality:
Alpha :
</Range>
</div>
<div class={style.optionOneCell}>
@ -262,11 +262,11 @@ export class Options extends Component<Props, State> {
value={options.alpha_filtering}
onInput={this.onChange}
>
Alpha filter quality:
Alpha :
</Range>
</div>
<label class={style.optionToggle}>
Auto adjust filter strength
<Checkbox
name="autofilter"
checked={!!options.autofilter}
@ -283,13 +283,13 @@ export class Options extends Component<Props, State> {
value={options.filter_strength}
onInput={this.onChange}
>
Filter strength:
:
</Range>
</div>
)}
</Expander>
<label class={style.optionToggle}>
Strong filter
<Checkbox
name="filter_type"
checked={!!options.filter_type}
@ -304,11 +304,11 @@ export class Options extends Component<Props, State> {
value={7 - options.filter_sharpness}
onInput={this.onChange}
>
Filter sharpness:
:
</Range>
</div>
<label class={style.optionToggle}>
Sharp RGBYUV conversion
RGBYUV转换
<Checkbox
name="use_sharp_yuv"
checked={!!options.use_sharp_yuv}
@ -323,7 +323,7 @@ export class Options extends Component<Props, State> {
value={options.pass}
onInput={this.onChange}
>
Passes:
:
</Range>
</div>
<div class={style.optionOneCell}>
@ -334,11 +334,11 @@ export class Options extends Component<Props, State> {
value={options.sns_strength}
onInput={this.onChange}
>
Spatial noise shaping:
:
</Range>
</div>
<label class={style.optionTextFirst}>
Preprocess:
:
<Select
name="preprocessing"
value={options.preprocessing}
@ -357,7 +357,7 @@ export class Options extends Component<Props, State> {
value={options.segments}
onInput={this.onChange}
>
Segments:
:
</Range>
</div>
<div class={style.optionOneCell}>
@ -368,7 +368,7 @@ export class Options extends Component<Props, State> {
value={options.partitions}
onInput={this.onChange}
>
Partitions:
:
</Range>
</div>
</div>
@ -384,7 +384,7 @@ export class Options extends Component<Props, State> {
return (
<form class={style.optionsSection} onSubmit={preventDefault}>
<label class={style.optionToggle}>
Lossless
<Checkbox
name="lossless"
checked={!!options.lossless}
@ -395,7 +395,7 @@ export class Options extends Component<Props, State> {
? this._losslessSpecificOptions(options)
: this._lossySpecificOptions(options)}
<label class={style.optionToggle}>
Preserve transparent data
<Checkbox
name="exact"
checked={!!options.exact}

View File

@ -156,7 +156,7 @@ export class Options extends Component<Props, State> {
return (
<form class={style.optionsSection} onSubmit={preventDefault}>
<label class={style.optionToggle}>
Lossless
<Checkbox
checked={lossless}
onChange={this._inputChange('lossless', 'boolean')}
@ -172,7 +172,7 @@ export class Options extends Component<Props, State> {
value={slightLoss}
onInput={this._inputChange('slightLoss', 'number')}
>
Slight loss:
:
</Range>
</div>
)}
@ -188,11 +188,11 @@ export class Options extends Component<Props, State> {
value={quality}
onInput={this._inputChange('quality', 'number')}
>
Quality:
:
</Range>
</div>
<label class={style.optionToggle}>
Separate alpha quality
Alpha
<Checkbox
checked={separateAlpha}
onChange={this._inputChange('separateAlpha', 'boolean')}
@ -208,7 +208,7 @@ export class Options extends Component<Props, State> {
value={alphaQuality}
onInput={this._inputChange('alphaQuality', 'number')}
>
Alpha Quality:
Alpha :
</Range>
</div>
)}
@ -218,7 +218,7 @@ export class Options extends Component<Props, State> {
checked={showAdvanced}
onChange={linkState(this, 'showAdvanced')}
/>
Advanced settings
</label>
<Expander>
{showAdvanced && (
@ -231,7 +231,7 @@ export class Options extends Component<Props, State> {
value={passes}
onInput={this._inputChange('passes', 'number')}
>
Passes:
:
</Range>
</div>
<div class={style.optionOneCell}>
@ -242,7 +242,7 @@ export class Options extends Component<Props, State> {
value={sns}
onInput={this._inputChange('sns', 'number')}
>
Spatial noise shaping:
:
</Range>
</div>
<div class={style.optionOneCell}>
@ -253,11 +253,11 @@ export class Options extends Component<Props, State> {
value={errorDiffusion}
onInput={this._inputChange('errorDiffusion', 'number')}
>
Error diffusion:
:
</Range>
</div>
<label class={style.optionTextFirst}>
Subsample chroma:
:
<Select
value={uvMode}
onInput={this._inputChange('uvMode', 'number')}
@ -269,7 +269,7 @@ export class Options extends Component<Props, State> {
</Select>
</label>
<label class={style.optionTextFirst}>
Color space:
:
<Select
value={colorSpace}
onInput={this._inputChange('colorSpace', 'number')}
@ -280,7 +280,7 @@ export class Options extends Component<Props, State> {
</Select>
</label>
<label class={style.optionToggle}>
Random matrix
<Checkbox
checked={useRandomMatrix}
onChange={this._inputChange(
@ -303,7 +303,7 @@ export class Options extends Component<Props, State> {
value={effort}
onInput={this._inputChange('effort', 'number')}
>
Effort:
:
</Range>
</div>
</form>

View File

@ -53,13 +53,13 @@ export class Options extends Component<Props, State> {
<Expander>
{extendedSettings ? (
<label class={style.optionTextFirst}>
Type:
:
<Select
name="zx"
value={'' + options.zx}
onChange={this.onChange}
>
<option value="0">Standard</option>
<option value="0"></option>
<option value="1">ZX</option>
</Select>
</label>
@ -75,7 +75,7 @@ export class Options extends Component<Props, State> {
value={options.maxNumColors}
onInput={this.onChange}
>
Colors:
:
</Range>
</div>
)}
@ -89,7 +89,7 @@ export class Options extends Component<Props, State> {
value={options.dither}
onInput={this.onChange}
>
Dithering:
:
</Range>
</div>
</form>

View File

@ -232,7 +232,7 @@ export class Options extends Component<Props, State> {
onSubmit={preventDefault}
>
<label class={style.optionTextFirst}>
Method:
:
<Select
name="resizeMethod"
value={options.method}
@ -251,7 +251,7 @@ export class Options extends Component<Props, State> {
</Select>
</label>
<label class={style.optionTextFirst}>
Preset:
:
<Select value={this.getPreset()} onChange={this.onPresetChange}>
{sizePresets.map((preset) => (
<option value={preset}>{preset * 100}%</option>
@ -260,7 +260,7 @@ export class Options extends Component<Props, State> {
</Select>
</label>
<label class={style.optionTextFirst}>
Width:
:
<input
required
class={style.textField}
@ -272,7 +272,7 @@ export class Options extends Component<Props, State> {
/>
</label>
<label class={style.optionTextFirst}>
Height:
:
<input
required
class={style.textField}
@ -286,7 +286,7 @@ export class Options extends Component<Props, State> {
<Expander>
{isWorkerOptions(options) ? (
<label class={style.optionToggle}>
Premultiply alpha channel
Aplha
<Checkbox
name="premultiply"
checked={options.premultiply}
@ -296,7 +296,7 @@ export class Options extends Component<Props, State> {
) : null}
{isWorkerOptions(options) ? (
<label class={style.optionToggle}>
Linear RGB
使线 RGB
<Checkbox
name="linearRGB"
checked={options.linearRGB}
@ -306,7 +306,7 @@ export class Options extends Component<Props, State> {
) : null}
</Expander>
<label class={style.optionToggle}>
Maintain aspect ratio
<Checkbox
name="maintainAspect"
checked={maintainAspect}
@ -316,14 +316,14 @@ export class Options extends Component<Props, State> {
<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>
<option value="stretch"></option>
<option value="contain"></option>
</Select>
</label>
)}

View File

@ -62,7 +62,7 @@ snack-bar {
position: relative;
flex: 0 1 auto;
padding: 8px;
height: 36px;
height: 100%;
margin: auto 8px auto -8px;
min-width: 5em;
background: none;
@ -78,6 +78,7 @@ snack-bar {
overflow: hidden;
transition: background-color 200ms ease;
outline: none;
text-decoration: none;
}
.button:hover {
background-color: rgba(0, 0, 0, 0.15);

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -3,7 +3,6 @@ import { h, Component } from 'preact';
import { linkRef } from 'shared/prerendered-app/util';
import '../../custom-els/loading-spinner';
import logo from 'url:./imgs/logo.svg';
import githubLogo from 'url:./imgs/github-logo.svg';
import largePhoto from 'url:./imgs/demos/demo-large-photo.jpg';
import artwork from 'url:./imgs/demos/demo-artwork.jpg';
import deviceScreen from 'url:./imgs/demos/demo-device-screen.png';
@ -24,28 +23,28 @@ import SlideOnScroll from './SlideOnScroll';
const demos = [
{
description: 'Large photo',
size: '2.8mb',
size: '2.8MB',
filename: 'photo.jpg',
url: largePhoto,
iconUrl: largePhotoIcon,
},
{
description: 'Artwork',
size: '2.9mb',
size: '2.9MB',
filename: 'art.jpg',
url: artwork,
iconUrl: artworkIcon,
},
{
description: 'Device screen',
size: '1.6mb',
size: '1.6MB',
filename: 'pixel3.png',
url: deviceScreen,
iconUrl: deviceScreenIcon,
},
{
description: 'SVG icon',
size: '13k',
size: '13KB',
filename: 'squoosh.svg',
url: logo,
iconUrl: logoIcon,
@ -207,14 +206,14 @@ export default class Intro extends Component<Props, State> {
try {
clipboardItems = await navigator.clipboard.read();
} catch (err) {
this.props.showSnack!(`No permission to access clipboard`);
this.props.showSnack!(`没有剪贴板访问权限`);
return;
}
const blob = await getImageClipboardItem(clipboardItems);
if (!blob) {
this.props.showSnack!(`No image found in the clipboard`);
this.props.showSnack!(`剪贴板中没有找到图片`);
return;
}
@ -244,7 +243,7 @@ export default class Intro extends Component<Props, State> {
<img
class={style.logo}
src={logoWithText}
alt="Squoosh"
alt="图压宝"
width="539"
height="162"
/>
@ -285,13 +284,13 @@ export default class Intro extends Component<Props, State> {
</svg>
</button>
<div>
<span class={style.dropText}>Drop </span>OR{' '}
<span class={style.dropText}></span>
{supportsClipboardAPI ? (
<button class={style.pasteBtn} onClick={this.onPasteClick}>
Paste
</button>
) : (
'Paste'
'粘贴图片到此'
)}
</div>
</div>
@ -310,7 +309,7 @@ export default class Intro extends Component<Props, State> {
</svg>
<div class={style.contentPadding}>
<p class={style.demoTitle}>
Or <strong>try one</strong> of these:
<strong></strong>
</p>
<ul class={style.demos}>
{demos.map((demo, i) => (
@ -319,7 +318,7 @@ export default class Intro extends Component<Props, State> {
class="unbutton"
onClick={(event) => this.onDemoClick(i, event)}
>
<div>
<div class={style.demoContainer}>
<div class={style.demoIconContainer}>
<img
class={style.demoIcon}
@ -355,10 +354,9 @@ export default class Intro extends Component<Props, State> {
<SlideOnScroll>
<div class={style.infoContent}>
<div class={style.infoTextWrapper}>
<h2 class={style.infoTitle}>Small</h2>
<h2 class={style.infoTitle}></h2>
<p class={style.infoCaption}>
Smaller images mean faster load times. Squoosh can reduce
file size and maintain high quality.
</p>
</div>
<div class={style.infoImgWrapper}>
@ -380,11 +378,9 @@ export default class Intro extends Component<Props, State> {
<SlideOnScroll>
<div class={style.infoContent}>
<div class={style.infoTextWrapper}>
<h2 class={style.infoTitle}>Simple</h2>
<h2 class={style.infoTitle}></h2>
<p class={style.infoCaption}>
Open your image, inspect the differences, then save
instantly. Feeling adventurous? Adjust the settings for even
smaller files.
</p>
</div>
<div class={style.infoImgWrapper}>
@ -406,10 +402,9 @@ export default class Intro extends Component<Props, State> {
<SlideOnScroll>
<div class={style.infoContent}>
<div class={style.infoTextWrapper}>
<h2 class={style.infoTitle}>Secure</h2>
<h2 class={style.infoTitle}></h2>
<p class={style.infoCaption}>
Worried about privacy? Images never leave your device since
Squoosh does all the work locally.
线使
</p>
</div>
<div class={style.infoImgWrapper}>
@ -438,16 +433,9 @@ export default class Intro extends Component<Props, State> {
<footer class={style.footerItems}>
<a
class={style.footerLink}
href="https://github.com/GoogleChromeLabs/squoosh/blob/dev/README.md#privacy"
href="#"
>
Privacy
</a>
<a
class={style.footerLinkWithLogo}
href="https://github.com/GoogleChromeLabs/squoosh"
>
<img src={githubLogo} alt="" width="10" height="10" />
Source on Github
</a>
</footer>
</div>
@ -455,7 +443,7 @@ export default class Intro extends Component<Props, State> {
</footer>
{beforeInstallEvent && (
<button class={style.installBtn} onClick={this.onInstallClick}>
Install
</button>
)}
</div>

View File

@ -321,6 +321,13 @@
}
}
.demo-container {
transition: scale 400ms ease-in-out;
&:hover {
scale: 1.05;
}
}
.demo-size {
background: var(--dim-blue);
border-radius: 1000px;

View File

@ -18,3 +18,14 @@ html {
/* Old stuff: */
--button-fg: rgb(95, 180, 228);
}
@media (dynamic-range: high) {
@supports (color: oklch(0% 0 0)) {
html {
--pink: oklch(75% 0.3 3);
--hot-pink: oklch(65% 0.3 3);
--blue: oklch(75% 0.3 248);
--deep-blue: oklch(65% 0.3 248);
}
}
}

View File

@ -55,8 +55,8 @@ interface Output {
const toOutput: Output = {
'index.html': renderPage(<IndexPage />),
'manifest.json': JSON.stringify({
name: 'Squoosh',
short_name: 'Squoosh',
name: '图压宝 - 免安装,压的狠,超清晰的图片压缩工具',
short_name: '图压宝',
start_url: '/?utm_medium=PWA&utm_source=launcher',
display: 'standalone',
orientation: 'any',
@ -76,7 +76,7 @@ const toOutput: Output = {
},
],
description:
'Compress and compare images with different codecs, right in your browser.',
'免安装,压的狠,超清晰的图片压缩工具,提供多种先进压缩算法和高级设置,压到您满意为止!',
lang: 'en',
categories: ['photo', 'productivity', 'utilities'],
screenshots,

View File

@ -19,20 +19,22 @@ import favicon from 'url:static-build/assets/favicon.ico';
import ogImage from 'url:static-build/assets/icon-large-maskable.png';
import { escapeStyleScriptContent, siteOrigin } from 'static-build/utils';
import Intro from 'shared/prerendered-app/Intro';
import snackbarCss from 'css:../../../shared/custom-els/snack-bar/styles.css';
import * as snackbarStyle from '../../../shared/custom-els/snack-bar/styles.css';
interface Props {}
const Index: FunctionalComponent<Props> = () => (
<html lang="en">
<head>
<title>Squoosh</title>
<title> - </title>
<meta
name="description"
content="Squoosh is the ultimate image optimizer that allows you to compress and compare images with different codecs in your browser."
content="图压宝是一款免安装,压的狠,超清晰的图片压缩和裁剪工具,提供多种先进压缩算法和高级设置,压到您满意为止!"
/>
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@SquooshApp" />
<meta property="og:title" content="Squoosh" />
<meta name="twitter:site" content="@图压宝" />
<meta property="og:title" content="图压宝" />
<meta property="og:type" content="website" />
<meta property="og:image" content={`${siteOrigin}${ogImage}`} />
<meta
@ -48,7 +50,7 @@ const Index: FunctionalComponent<Props> = () => (
/>
<meta
name="og:description"
content="Squoosh is the ultimate image optimizer that allows you to compress and compare images with different codecs in your browser."
content="图压宝是一款免安装,压的狠,超清晰的图片压缩和裁剪工具,提供多种先进压缩算法和高级设置,压到您满意为止"
/>
<meta
name="viewport"
@ -73,6 +75,29 @@ const Index: FunctionalComponent<Props> = () => (
<body>
<div id="app">
<Intro />
<noscript>
<style
dangerouslySetInnerHTML={{
__html: escapeStyleScriptContent(snackbarCss),
}}
/>
<snack-bar>
<div
class={snackbarStyle.snackbar}
aria-live="assertive"
aria-atomic="true"
aria-hidden="false"
>
<div class={snackbarStyle.text}>
Initialization error: This site requires JavaScript, which is
disabled in your browser.
</div>
<a class={snackbarStyle.button} href="/">
reload
</a>
</div>
</snack-bar>
</noscript>
</div>
<script
dangerouslySetInnerHTML={{