Compare commits

..

1 Commits

Author SHA1 Message Date
94157a3ef3 Hack together not-inline CSS 2022-02-21 12:40:16 +00:00
19 changed files with 237 additions and 75 deletions

View File

@ -1,7 +1,3 @@
# Project no longer maintained
Unfortunately, due to a few people leaving the team, and staffing issues resulting from the current economic climate (ugh), this package is no longer actively maintained. I know that sucks, but there simply isn't the time & people to work on this. If anyone from the community wants to fork it, you have my blessing. The [squoosh.app](https://squoosh.app) will continue to be supported and improved.
# Squoosh CLI
Squoosh CLI is an _experimental_ way to run all the codecs you know from the [Squoosh] web app on your command line using WebAssembly. The Squoosh CLI uses a worker pool to parallelize processing images. This way you can apply the same codec to many images at once.

4
cli/package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "@squoosh/cli",
"version": "0.7.3",
"version": "0.7.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@squoosh/cli",
"version": "0.7.3",
"version": "0.7.2",
"license": "Apache-2.0",
"dependencies": {
"@squoosh/lib": "^0.4.0",

View File

@ -1,6 +1,6 @@
{
"name": "@squoosh/cli",
"version": "0.7.3",
"version": "0.7.2",
"description": "A CLI for Squoosh",
"public": true,
"type": "module",

View File

@ -11,10 +11,23 @@
* limitations under the License.
*/
import { promisify } from 'util';
import { promises as fsp, readFileSync } from 'fs';
import path from 'path';
import { posix } from 'path';
import glob from 'glob';
import postcss from 'postcss';
import postCSSNested from 'postcss-nested';
import postCSSUrl from 'postcss-url';
import postCSSModules from 'postcss-modules';
import postCSSSimpleVars from 'postcss-simple-vars';
import cssNano from 'cssnano';
import {
parse as parsePath,
resolve as resolvePath,
dirname,
normalize as nomalizePath,
sep as pathSep,
} from 'path';
const globP = promisify(glob);
@ -30,26 +43,72 @@ export default function initialCssPlugin() {
async load(id) {
if (id !== initialCssModule) return;
const matches = await globP('shared/prerendered-app/**/*.css', {
nodir: true,
cwd: path.join(process.cwd(), 'src'),
});
const matches = (
await globP('shared/prerendered-app/**/*.css', {
nodir: true,
cwd: path.join(process.cwd(), 'src'),
absolute: true,
})
).map((cssPath) =>
// glob() returns windows paths with a forward slash. Normalise it:
path.normalize(cssPath),
);
// Sort the matches so the parentmost items appear first.
// This is a bit of a hack, but it means the util stuff appears in the cascade first.
const sortedMatches = matches
.map((match) => path.normalize(match).split(path.sep))
.sort((a, b) => a.length - b.length)
.map((match) => posix.join(...match));
.map((match) => '/' + posix.join(...match));
const imports = sortedMatches
.map((id, i) => `import css${i} from 'css:${id}';\n`)
.join('');
const cssSources = await Promise.all(
sortedMatches.map(async (path) => {
this.addWatchFile(path);
const file = await fsp.readFile(path);
return (
imports +
`export default ${sortedMatches.map((_, i) => `css${i}`).join(' + ')};`
const cssResult = await postcss([
postCSSNested,
postCSSSimpleVars(),
postCSSModules({
root: '',
}),
postCSSUrl({
url: ({ relativePath, url }) => {
if (/^((https?|data):|#)/.test(url)) return url;
const parsedPath = parsePath(relativePath);
const source = readFileSync(
resolvePath(dirname(path), relativePath),
);
const fileId = this.emitFile({
type: 'asset',
name: parsedPath.base,
source,
});
const hash = createHash('md5');
hash.update(source);
const md5 = hash.digest('hex');
hashToId.set(md5, fileId);
return `/fake/path/to/asset/${md5}/`;
},
}),
cssNano,
]).process(file, {
from: path,
});
return cssResult.css;
}),
);
const css = cssSources.join('\n');
const fileId = this.emitFile({
type: 'asset',
source: css,
name: 'initial.css',
});
return `export default import.meta.ROLLUP_FILE_URL_${fileId};`;
},
};
}

View File

@ -1,7 +1,3 @@
# Project no longer maintained
Unfortunately, due to a few people leaving the team, and staffing issues resulting from the current economic climate (ugh), this package is no longer actively maintained. I know that sucks, but there simply isn't the time & people to work on this. If anyone from the community wants to fork it, you have my blessing. The [squoosh.app](https://squoosh.app) will continue to be supported and improved.
# libSquoosh
libSquoosh is an _experimental_ way to run all the codecs you know from the [Squoosh] web app directly inside your own JavaScript program. libSquoosh uses a worker pool to parallelize processing images. This way you can apply the same codec to many images at once.

View File

@ -1,12 +1,12 @@
{
"name": "@squoosh/lib",
"version": "0.5.2",
"version": "0.4.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@squoosh/lib",
"version": "0.5.2",
"version": "0.4.0",
"license": "Apache-2.0",
"dependencies": {
"wasm-feature-detect": "^1.2.11",
@ -1764,9 +1764,9 @@
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true
},
"node_modules/picomatch": {
@ -3720,9 +3720,9 @@
"dev": true
},
"path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"dev": true
},
"picomatch": {

View File

@ -1,6 +1,6 @@
{
"name": "@squoosh/lib",
"version": "0.5.2",
"version": "0.5.0",
"description": "A Node library for Squoosh",
"public": true,
"main": "./build/index.js",

View File

@ -40,8 +40,7 @@ two-up[legacy-clip-compat] > :not(.two-up-handle) {
}
.scrubber {
display: grid;
align-content: center;
display: flex;
position: absolute;
top: 50%;
left: 50%;
@ -55,6 +54,10 @@ two-up[legacy-clip-compat] > :not(.two-up-handle) {
padding: 0 calc(var(--thumb-size) * 0.24);
}
.scrubber svg {
flex: 1;
}
.arrow-left {
fill: var(--pink);
}

View File

@ -49,7 +49,6 @@
align-self: center;
padding: 9px 66px;
position: relative;
gap: 6px;
/* Allow clicks to fall through to the pinch zoom area */
pointer-events: none;
@ -69,6 +68,7 @@
display: flex;
position: relative;
z-index: 100;
margin: 0 3px;
}
.button,
@ -95,7 +95,6 @@
.button {
color: #fff;
margin: 0;
&:hover {
background: rgba(50, 50, 50, 0.92);
@ -115,7 +114,7 @@
.last-button {
composes: button;
border-radius: 0 6px 6px 0;
border-right-width: 1px;
border-left-width: 1px;
}
.zoom {

View File

@ -114,7 +114,7 @@ async function decodeImage(
}
}
// Otherwise fall through and try built-in decoding for a laugh.
return await builtinDecode(signal, blob);
return await builtinDecode(signal, blob, mimeType);
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') throw err;
console.log(err);

View File

@ -63,17 +63,35 @@ interface DrawableToImageDataOptions {
sh?: number;
}
function getWidth(
drawable: ImageBitmap | HTMLImageElement | VideoFrame,
): number {
if ('displayWidth' in drawable) {
return drawable.displayWidth;
}
return drawable.width;
}
function getHeight(
drawable: ImageBitmap | HTMLImageElement | VideoFrame,
): number {
if ('displayHeight' in drawable) {
return drawable.displayHeight;
}
return drawable.height;
}
export function drawableToImageData(
drawable: ImageBitmap | HTMLImageElement,
drawable: ImageBitmap | HTMLImageElement | VideoFrame,
opts: DrawableToImageDataOptions = {},
): ImageData {
const {
width = drawable.width,
height = drawable.height,
width = getWidth(drawable),
height = getHeight(drawable),
sx = 0,
sy = 0,
sw = drawable.width,
sh = drawable.height,
sw = getWidth(drawable),
sh = getHeight(drawable),
} = opts;
// Make canvas same size as image

View File

@ -10,6 +10,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as WebCodecs from '../util/web-codecs';
import { drawableToImageData } from './canvas';
/** If render engine is Safari */
@ -137,7 +139,15 @@ export async function blobToImg(blob: Blob): Promise<HTMLImageElement> {
export async function builtinDecode(
signal: AbortSignal,
blob: Blob,
mimeType: string,
): Promise<ImageData> {
// If WebCodecs are supported, use that.
if (await WebCodecs.isTypeSupported(mimeType)) {
assertSignal(signal);
try {
return await abortable(signal, WebCodecs.decode(blob, mimeType));
} catch (e) {}
}
assertSignal(signal);
// Prefer createImageBitmap as it's the off-thread option for Firefox.

View File

@ -0,0 +1,33 @@
import { drawableToImageData } from '../canvas';
const hasImageDecoder = typeof ImageDecoder !== 'undefined';
export async function isTypeSupported(mimeType: string): Promise<boolean> {
if (!hasImageDecoder) return false;
// Some old versions of this API threw here.
// It only impacted folks with experimental web platform flags enabled in Chrome 90.
// The API was updated in Chrome 91.
try {
return await ImageDecoder.isTypeSupported(mimeType);
} catch (err) {
return false;
}
}
export async function decode(
blob: Blob | File,
mimeType: string,
): Promise<ImageData> {
if (!hasImageDecoder) {
throw Error(
`This browser does not support ImageDecoder. This function should not have been called.`,
);
}
const decoder = new ImageDecoder({
type: mimeType,
// Non-obvious way to turn an Blob into a ReadableStream
data: new Response(blob).body!,
});
const { image } = await decoder.decode();
return drawableToImageData(image);
}

View File

@ -0,0 +1,60 @@
interface ImageDecoderInit {
type: string;
data: BufferSource | ReadableStream;
premultiplyAlpha?: PremultiplyAlpha;
colorSpaceConversion?: ColorSpaceConversion;
desiredWidth?: number;
desiredHeight?: number;
preferAnimation?: boolean;
}
interface ImageDecodeOptions {
frameIndex: number;
completeFramesOnly: boolean;
}
interface ImageDecodeResult {
image: VideoFrame;
complete: boolean;
}
// I didnt do all the types because the class is kinda complex.
// I focused on what we need.
// See https://w3c.github.io/webcodecs/#videoframe
declare class VideoFrame {
displayWidth: number;
displayHeight: number;
}
// Add VideoFrame to canvas drawImage()
interface CanvasDrawImage {
drawImage(
image: CanvasImageSource | VideoFrame,
dx: number,
dy: number,
): void;
drawImage(
image: CanvasImageSource | VideoFrame,
dx: number,
dy: number,
dw: number,
dh: number,
): void;
drawImage(
image: CanvasImageSource | VideoFrame,
sx: number,
sy: number,
sw: number,
sh: number,
dx: number,
dy: number,
dw: number,
dh: number,
): void;
}
declare class ImageDecoder {
static isTypeSupported(type: string): Promise<boolean>;
constructor(desc: ImageDecoderInit);
decode(opts?: Partial<ImageDecodeOptions>): Promise<ImageDecodeResult>;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

View File

@ -19,39 +19,15 @@ import * as iconLarge from 'img-url:static-build/assets/icon-large.png';
import * as screenshot1 from 'img-url:static-build/assets/screenshot1.png';
import * as screenshot2 from 'img-url:static-build/assets/screenshot2.jpg';
import * as screenshot3 from 'img-url:static-build/assets/screenshot3.jpg';
import * as screenshot4 from 'img-url:static-build/assets/screenshot4.png';
import * as screenshot5 from 'img-url:static-build/assets/screenshot5.jpg';
import * as screenshot6 from 'img-url:static-build/assets/screenshot6.jpg';
import dedent from 'dedent';
import { lookup as lookupMime } from 'mime-types';
interface Dimensions {
width: number;
height: number;
}
const manifestSize = ({ width, height }: Dimensions) => `${width}x${height}`;
const formFactor = ({ width, height }: Dimensions) =>
width > height ? 'wide' : 'narrow';
const screenshots = [
screenshot1,
screenshot2,
screenshot3,
screenshot4,
screenshot5,
screenshot6,
].map((screenshot) => ({
src: screenshot.default,
type: lookupMime(screenshot.default),
sizes: manifestSize(screenshot),
form_factor: formFactor(screenshot),
}));
const manifestSize = ({ width, height }: { width: number; height: number }) =>
`${width}x${height}`;
interface Output {
[outputPath: string]: string;
}
const toOutput: Output = {
'index.html': renderPage(<IndexPage />),
'manifest.json': JSON.stringify({
@ -79,7 +55,23 @@ const toOutput: Output = {
'Compress and compare images with different codecs, right in your browser.',
lang: 'en',
categories: ['photo', 'productivity', 'utilities'],
screenshots,
screenshots: [
{
src: screenshot1.default,
type: lookupMime(screenshot1.default),
sizes: manifestSize(screenshot1),
},
{
src: screenshot2.default,
type: lookupMime(screenshot2.default),
sizes: manifestSize(screenshot2),
},
{
src: screenshot3.default,
type: lookupMime(screenshot3.default),
sizes: manifestSize(screenshot3),
},
],
share_target: {
action: '/?utm_medium=PWA&utm_source=share-target&share-target',
method: 'POST',

View File

@ -64,11 +64,7 @@ const Index: FunctionalComponent<Props> = () => (
<style
dangerouslySetInnerHTML={{ __html: escapeStyleScriptContent(baseCss) }}
/>
<style
dangerouslySetInnerHTML={{
__html: escapeStyleScriptContent(initialCss),
}}
/>
<link rel="stylesheet" href={initialCss} />
</head>
<body>
<div id="app">