Compare commits
31 Commits
dependabot
...
origin
Author | SHA1 | Date | |
---|---|---|---|
25239d46d5 | |||
b75244a309 | |||
0ce1443622 | |||
b433e03893 | |||
7766f64cc6 | |||
8a29ce25dc | |||
d87eff7645 | |||
f374068fb2 | |||
ecc715fe55 | |||
82caed4277 | |||
a7dff9475d | |||
d168f7a447 | |||
edf9cb755e | |||
a7503e69a2 | |||
5a9733563e | |||
2000e16ba2 | |||
7dbe0a7714 | |||
25bc43e409 | |||
cee51bf355 | |||
8d6daf0fc4 | |||
61209d0b62 | |||
d0b4855022 | |||
6cb64a59ca | |||
979fba0af1 | |||
b1df3e1d54 | |||
4f6138d97d | |||
6b6e3724d2 | |||
8ac5e6f678 | |||
a930e8d928 | |||
c814700cd2 | |||
dfdf2a7f71 |
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -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.
|
18
.github/ISSUE_TEMPLATE/feature_request.md
vendored
18
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -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.
|
22
.github/workflows/node.js.yml
vendored
22
.github/workflows/node.js.yml
vendored
@ -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
|
@ -1 +0,0 @@
|
||||
npx lint-staged
|
42
codecs/qoi/Makefile
Normal file
42
codecs/qoi/Makefile
Normal 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
|
30
codecs/qoi/dec/qoi_dec.cpp
Normal file
30
codecs/qoi/dec/qoi_dec.cpp
Normal 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
7
codecs/qoi/dec/qoi_dec.d.ts
vendored
Normal 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
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
BIN
codecs/qoi/dec/qoi_dec.wasm
Normal file
Binary file not shown.
36
codecs/qoi/enc/qoi_enc.cpp
Normal file
36
codecs/qoi/enc/qoi_enc.cpp
Normal 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
14
codecs/qoi/enc/qoi_enc.d.ts
vendored
Normal 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
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
BIN
codecs/qoi/enc/qoi_enc.wasm
Normal file
Binary file not shown.
7
codecs/qoi/package.json
Normal file
7
codecs/qoi/package.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "qoi",
|
||||
"scripts": {
|
||||
"build": "../build-cpp.sh"
|
||||
}
|
||||
}
|
||||
|
8676
package-lock.json
generated
8676
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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": {
|
||||
|
@ -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();
|
@ -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 {
|
||||
|
@ -47,6 +47,10 @@ range-input::before {
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
range-input:focus-within .thumb {
|
||||
outline: white solid 2px;
|
||||
}
|
||||
|
||||
.thumb-wrapper {
|
||||
position: absolute;
|
||||
left: 6px;
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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'}`
|
||||
}
|
||||
/>
|
||||
));
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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];
|
||||
|
@ -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();
|
||||
|
@ -46,7 +46,7 @@ export function qualityOption(
|
||||
value={options.quality}
|
||||
onInput={this.onChange}
|
||||
>
|
||||
Quality:
|
||||
质量:
|
||||
</Range>
|
||||
</div>
|
||||
</div>
|
||||
|
19
src/features/decoders/qoi/worker/qoiDecode.ts
Normal file
19
src/features/decoders/qoi/worker/qoiDecode.ts
Normal 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;
|
||||
}
|
@ -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>
|
||||
|
@ -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:
|
||||
噪声等效 ISO(NEQ 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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
11
src/features/encoders/qoi/client/index.tsx
Normal file
11
src/features/encoders/qoi/client/index.tsx
Normal 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);
|
||||
}
|
13
src/features/encoders/qoi/client/missing-types.d.ts
vendored
Normal file
13
src/features/encoders/qoi/client/missing-types.d.ts
vendored
Normal 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" />
|
19
src/features/encoders/qoi/shared/meta.ts
Normal file
19
src/features/encoders/qoi/shared/meta.ts
Normal 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 = {};
|
13
src/features/encoders/qoi/shared/missing-types.d.ts
vendored
Normal file
13
src/features/encoders/qoi/shared/missing-types.d.ts
vendored
Normal 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" />
|
13
src/features/encoders/qoi/worker/missing-types.d.ts
vendored
Normal file
13
src/features/encoders/qoi/worker/missing-types.d.ts
vendored
Normal 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" />
|
35
src/features/encoders/qoi/worker/qoiEncode.ts
Normal file
35
src/features/encoders/qoi/worker/qoiEncode.ts
Normal 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 can’t run on SharedArrayBuffers, so we hard-cast to ArrayBuffer.
|
||||
return resultView.buffer as ArrayBuffer;
|
||||
}
|
@ -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 RGB→YUV conversion
|
||||
锐利的RGB→YUV转换
|
||||
<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}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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 |
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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={{
|
||||
|
Reference in New Issue
Block a user