WebP: Rely on browser for rendering (#3862)

This commit is contained in:
Alexander Zinchuk 2023-09-25 12:59:46 +02:00
parent 65f4a78527
commit 1f10dca00e
10 changed files with 6 additions and 230 deletions

View File

@ -11,7 +11,7 @@ import {
} from '../../config';
import { areSortedArraysIntersecting, unique } from '../../util/iteratees';
import { getServerTime } from '../../util/serverTime';
import { IS_OPUS_SUPPORTED, isWebpSupported } from '../../util/windowEnvironment';
import { IS_OPUS_SUPPORTED } from '../../util/windowEnvironment';
import { getGlobal } from '../index';
import { getChatTitle, isUserId } from './chats';
import { getUserFullName } from './users';
@ -227,8 +227,7 @@ export function getMessageContentFilename(message: ApiMessage) {
}
if (content.sticker) {
const extension = content.sticker.isLottie ? 'tgs' : content.sticker.isVideo
? 'webm' : isWebpSupported() ? 'webp' : 'png';
const extension = content.sticker.isLottie ? 'tgs' : content.sticker.isVideo ? 'webm' : 'webp';
return `${content.sticker.id}.${extension}`;
}

View File

@ -1,46 +1,24 @@
import { useLayoutEffect, useMemo, useState } from '../lib/teact/teact';
import { useMemo } from '../lib/teact/teact';
import { getGlobal } from '../global';
import type { ApiMessage, ApiSticker } from '../api/types';
import { DEBUG } from '../config';
import { getMessageMediaThumbDataUri } from '../global/helpers';
import { selectTheme } from '../global/selectors';
import { EMPTY_IMAGE_DATA_URI, webpToPngBase64 } from '../util/webpToPng';
import { isWebpSupported } from '../util/windowEnvironment';
export default function useThumbnail(media?: ApiMessage | ApiSticker) {
const isMessage = media && 'content' in media;
const thumbDataUri = isMessage ? getMessageMediaThumbDataUri(media) : media?.thumbnail?.dataUri;
const sticker = isMessage ? media.content?.sticker : media;
const shouldDecodeThumbnail = thumbDataUri && sticker && !isWebpSupported() && thumbDataUri.includes('image/webp');
const [thumbnailDecoded, setThumbnailDecoded] = useState(EMPTY_IMAGE_DATA_URI);
const id = media?.id;
useLayoutEffect(() => {
if (!shouldDecodeThumbnail) {
return;
}
webpToPngBase64(`b64-${id}`, thumbDataUri!)
.then(setThumbnailDecoded)
.catch((err) => {
if (DEBUG) {
// eslint-disable-next-line no-console
console.error(err);
}
});
}, [id, shouldDecodeThumbnail, thumbDataUri]);
// TODO Find a way to update thumbnail on theme change
const theme = selectTheme(getGlobal());
const dataUri = useMemo(() => {
const uri = shouldDecodeThumbnail ? thumbnailDecoded : thumbDataUri;
const uri = thumbDataUri;
if (!uri || theme !== 'dark') return uri;
return uri.replace('<svg', '<svg fill="white"');
}, [shouldDecodeThumbnail, thumbDataUri, thumbnailDecoded, theme]);
}, [thumbDataUri, theme]);
return dataUri;
}

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -1,51 +0,0 @@
/* eslint-disable */
importScripts(new URL('./webp_wasm.js', import.meta.url));
Module.onRuntimeInitialized = async () => {
self.postMessage({ type: 'initialized' });
};
self.onmessage = (event) => {
const { id, blob } = event.data;
const reader = new FileReader();
reader.addEventListener('loadend', () => {
const buffer = reader.result;
const size = buffer.byteLength;
const thisPtr = Module._malloc(size);
Module.HEAPU8.set(new Uint8Array(buffer), thisPtr);
const getInfo = Module.cwrap('getInfo', 'number', ['number', 'number']);
const ptr = getInfo(thisPtr, size);
const success = !!Module.getValue(ptr, 'i32');
if (!success) {
Module._free(ptr);
Module._free(thisPtr);
self.postMessage({
type: 'result', id, width: 0, height: 0, result: null,
});
return;
}
const width = Module.getValue(ptr + 4, 'i32');
const height = Module.getValue(ptr + 8, 'i32');
Module._free(ptr);
const decode = Module.cwrap('decode', 'number', ['number', 'number']);
const resultPtr = decode(thisPtr, size);
const resultView = new Uint8Array(Module.HEAPU8.buffer, resultPtr, width * height * 4);
const result = new Uint8ClampedArray(resultView);
Module._free(resultPtr);
Module._free(thisPtr);
self.postMessage({
type: 'result', id, width, height, result,
});
});
reader.readAsArrayBuffer(blob);
};

View File

@ -1,8 +0,0 @@
const webpToPng = {
webpToPng() {
},
webpToPngBase64() {
},
};
export default webpToPng;

View File

@ -1,22 +0,0 @@
let isWebpSupportedCache: boolean | undefined;
export function isWebpSupported() {
return Boolean(isWebpSupportedCache);
}
function testWebp(): Promise<boolean> {
return new Promise((resolve) => {
const webp = new Image();
// eslint-disable-next-line max-len
webp.src = 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA';
const handleLoadOrError = () => {
resolve(webp.height === 2);
};
webp.onload = handleLoadOrError;
webp.onerror = handleLoadOrError;
});
}
testWebp().then((hasWebp) => {
isWebpSupportedCache = hasWebp;
});

View File

@ -15,9 +15,8 @@ import { callApi, cancelApiProgress } from '../api/gramjs';
import * as cacheApi from './cacheApi';
import { fetchBlob } from './files';
import { oggToWav } from './oggToWav';
import { webpToPng } from './webpToPng';
import {
IS_OPUS_SUPPORTED, IS_PROGRESSIVE_SUPPORTED, isWebpSupported,
IS_OPUS_SUPPORTED, IS_PROGRESSIVE_SUPPORTED,
} from './windowEnvironment';
const asCacheApiType = {
@ -142,13 +141,6 @@ async function fetchFromCacheOrRemote(
media = await oggToWav(media);
}
if (cached.type === 'image/webp' && !isWebpSupported() && media) {
const mediaPng = await webpToPng(url, media);
if (mediaPng) {
media = mediaPng;
}
}
const prepared = prepareMedia(media);
memoryCache.set(url, prepared);
@ -184,15 +176,6 @@ async function fetchFromCacheOrRemote(
mimeType = media.type;
}
if (mimeType === 'image/webp' && !isWebpSupported()) {
const blob = await fetchBlob(prepared as string);
URL.revokeObjectURL(prepared as string);
const media = await webpToPng(url, blob);
if (media) {
prepared = prepareMedia(media);
}
}
memoryCache.set(url, prepared);
return prepared;

View File

@ -1,100 +0,0 @@
import { blobToDataUri, dataUriToBlob } from './files';
import { pause } from './schedulers';
import { isWebpSupported } from './windowEnvironment';
const WORKER_INITIALIZATION_TIMEOUT = 2000;
let canvas: HTMLCanvasElement;
let worker: IWebpWorker;
export const EMPTY_IMAGE_DATA_URI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk'
+ 'YAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
export async function webpToPng(url: string, blob: Blob): Promise<Blob | undefined> {
initWebpWorker();
while (!worker.wasmReady) {
await pause(WORKER_INITIALIZATION_TIMEOUT);
}
const { result, width, height } = await getDecodePromise(url, blob);
if (!width || !height) {
return undefined;
}
return createPng({ result, width, height });
}
export async function webpToPngBase64(key: string, dataUri: string): Promise<string> {
if (isWebpSupported() || dataUri.substr(0, 15) !== 'data:image/webp') {
return dataUri;
}
initWebpWorker();
const pngBlob = await webpToPng(key, dataUriToBlob(dataUri));
if (!pngBlob) {
throw new Error(`Can't convert webp to png. Url: ${dataUri}`);
}
return blobToDataUri(pngBlob);
}
function initWebpWorker() {
if (!worker) {
worker = new Worker(new URL('../lib/webp/webp_wasm.worker.js', import.meta.url)) as IWebpWorker;
worker.wasmReady = false;
worker.onmessage = handleLibWebpMessage;
}
}
function createPng({ result, width, height }: TEncodedImage): Promise<Blob | undefined> {
if (!canvas) {
canvas = document.createElement('canvas');
}
return new Promise((resolve) => {
const img = new ImageData(result, width, height);
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d')!;
ctx.putImageData(img, 0, 0);
canvas.toBlob((blob) => {
resolve(blob ?? undefined);
}, 'image/png', 1);
});
}
function handleLibWebpMessage(e: MessageEvent) {
const { id } = e.data;
switch (e.data.type) {
case 'initialized': {
worker.wasmReady = true;
break;
}
case 'result': {
if (worker.requests.has(id)) {
const resolve = worker.requests.get(id)!;
worker.requests.delete(id);
resolve(e.data!);
}
break;
}
}
}
function getDecodePromise(url: string, blob: Blob): Promise<TEncodedImage> {
return new Promise((resolve) => {
if (!worker.requests) {
worker.requests = new Map();
}
worker.requests.set(url, resolve);
worker.postMessage({ id: url, blob });
});
}

View File

@ -4,8 +4,6 @@ import {
PRODUCTION_HOSTNAME,
} from '../config';
export * from './environmentWebp';
export function getPlatform() {
const { userAgent, platform } = window.navigator;