Browse Source

Use blurred canvas for thumbnails

master
Eduard Kuzmenko 3 years ago
parent
commit
408db34135
  1. 2
      src/components/appMediaViewerBase.ts
  2. 2
      src/components/audio.ts
  3. 6
      src/components/sidebarLeft/tabs/background.ts
  4. 15
      src/components/wrappers.ts
  5. 3
      src/environment/canvasFilterSupport.ts
  6. 90
      src/helpers/blur.ts
  7. 3
      src/helpers/bytes/bytesToDataURL.ts
  8. 4
      src/helpers/dom/renderImageFromUrl.ts
  9. 28
      src/helpers/heavyQueue.ts
  10. 5
      src/lib/appManagers/appDocsManager.ts
  11. 21
      src/lib/appManagers/appPhotosManager.ts
  12. 6
      src/scss/style.scss

2
src/components/appMediaViewerBase.ts

@ -1292,7 +1292,7 @@ export default class AppMediaViewerBase<
const size = appPhotosManager.setAttachmentSize(media, container, maxWidth, maxHeight, mediaSizes.isMobile ? false : true, undefined, !!(isDocument && media.w && media.h)).photoSize; const size = appPhotosManager.setAttachmentSize(media, container, maxWidth, maxHeight, mediaSizes.isMobile ? false : true, undefined, !!(isDocument && media.w && media.h)).photoSize;
if(useContainerAsTarget) { if(useContainerAsTarget) {
const cacheContext = appDownloadManager.getCacheContext(media, size.type); const cacheContext = appDownloadManager.getCacheContext(media, size.type);
let img: HTMLImageElement; let img: HTMLImageElement | HTMLCanvasElement;
if(cacheContext.downloaded) { if(cacheContext.downloaded) {
img = new Image(); img = new Image();
img.src = cacheContext.url; img.src = cacheContext.url;

2
src/components/audio.ts

@ -532,7 +532,7 @@ export default class AudioElement extends HTMLElement {
}; };
if(doc.thumbs?.length) { if(doc.thumbs?.length) {
const imgs: HTMLImageElement[] = []; const imgs: HTMLElement[] = [];
const wrapped = wrapPhoto({ const wrapped = wrapPhoto({
photo: doc, photo: doc,
message: null, message: null,

6
src/components/sidebarLeft/tabs/background.ts

@ -425,14 +425,14 @@ export default class AppBackgroundTab extends SliderSuperTab {
const cacheContext = appDownloadManager.getCacheContext(doc); const cacheContext = appDownloadManager.getCacheContext(doc);
if(background.blur) { if(background.blur) {
setTimeout(() => { setTimeout(() => {
blur(cacheContext.url, 12, 4) const {canvas, promise} = blur(cacheContext.url, 12, 4)
.then(url => { promise.then(() => {
if(!middleware()) { if(!middleware()) {
deferred.resolve(); deferred.resolve();
return; return;
} }
onReady(url); onReady(canvas.toDataURL());
}); });
}, 200); }, 200);
} else { } else {

15
src/components/wrappers.ts

@ -625,7 +625,7 @@ export function wrapDocument({message, withTime, fontWeight, voiceAsMusic, showS
if((doc.thumbs?.length || (message.pFlags.is_outgoing && cacheContext.url && doc.type === 'photo'))/* && doc.mime_type !== 'image/gif' */) { if((doc.thumbs?.length || (message.pFlags.is_outgoing && cacheContext.url && doc.type === 'photo'))/* && doc.mime_type !== 'image/gif' */) {
docDiv.classList.add('document-with-thumb'); docDiv.classList.add('document-with-thumb');
let imgs: HTMLImageElement[] = []; let imgs: (HTMLImageElement | HTMLCanvasElement)[] = [];
// ! WARNING, use thumbs for check when thumb will be generated for media // ! WARNING, use thumbs for check when thumb will be generated for media
if(message.pFlags.is_outgoing && ['photo', 'video'].includes(doc.type)) { if(message.pFlags.is_outgoing && ['photo', 'video'].includes(doc.type)) {
icoDiv.innerHTML = `<img src="${cacheContext.url}">`; icoDiv.innerHTML = `<img src="${cacheContext.url}">`;
@ -887,7 +887,7 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT
let isFit = true; let isFit = true;
let loadThumbPromise: Promise<any> = Promise.resolve(); let loadThumbPromise: Promise<any> = Promise.resolve();
let thumbImage: HTMLImageElement; let thumbImage: HTMLImageElement | HTMLCanvasElement;
let image: HTMLImageElement; let image: HTMLImageElement;
let cacheContext: ThumbCache; let cacheContext: ThumbCache;
const isGif = photo._ === 'document' && photo.mime_type === 'image/gif' && !size; const isGif = photo._ === 'document' && photo.mime_type === 'image/gif' && !size;
@ -1000,8 +1000,10 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT
if(middleware && !middleware()) return Promise.resolve(); if(middleware && !middleware()) return Promise.resolve();
if(blurAfter) { if(blurAfter) {
return blur(cacheContext.url, 12).then(url => { const result = blur(cacheContext.url, 12);
return renderOnLoad(url); return result.promise.then(() => {
// image = result.canvas;
return renderOnLoad(result.canvas.toDataURL());
}); });
} }
@ -1069,12 +1071,13 @@ export function wrapPhoto({photo, message, container, boxWidth, boxHeight, withT
}; };
} }
export function renderImageWithFadeIn(container: HTMLElement, export function renderImageWithFadeIn(
container: HTMLElement,
image: HTMLImageElement, image: HTMLImageElement,
url: string, url: string,
needFadeIn: boolean, needFadeIn: boolean,
aspecter = container, aspecter = container,
thumbImage?: HTMLImageElement thumbImage?: HTMLElement
) { ) {
if(needFadeIn) { if(needFadeIn) {
image.classList.add('fade-in'); image.classList.add('fade-in');

3
src/environment/canvasFilterSupport.ts

@ -0,0 +1,3 @@
const IS_CANVAS_FILTER_SUPPORTED = 'filter' in (document.createElement('canvas').getContext('2d') || {});
export default IS_CANVAS_FILTER_SUPPORTED;

90
src/helpers/blur.ts

@ -6,14 +6,14 @@
import type fastBlur from '../vendor/fastBlur'; import type fastBlur from '../vendor/fastBlur';
import addHeavyTask from './heavyQueue'; import addHeavyTask from './heavyQueue';
import IS_CANVAS_FILTER_SUPPORTED from '../environment/canvasFilterSupport';
const RADIUS = 2; const RADIUS = 2;
const ITERATIONS = 2; const ITERATIONS = 2;
const isFilterAvailable = 'filter' in (document.createElement('canvas').getContext('2d') || {});
let requireBlurPromise: Promise<any>; let requireBlurPromise: Promise<any>;
let fastBlurFunc: typeof fastBlur; let fastBlurFunc: typeof fastBlur;
if(!isFilterAvailable) { if(!IS_CANVAS_FILTER_SUPPORTED) {
requireBlurPromise = import('../vendor/fastBlur').then(m => { requireBlurPromise = import('../vendor/fastBlur').then(m => {
fastBlurFunc = m.default; fastBlurFunc = m.default;
}); });
@ -21,14 +21,17 @@ if(!isFilterAvailable) {
requireBlurPromise = Promise.resolve(); requireBlurPromise = Promise.resolve();
} }
function processBlurNext(img: HTMLImageElement, radius: number, iterations: number) { function processBlurNext(
return new Promise<string>((resolve) => { img: HTMLImageElement,
const canvas = document.createElement('canvas'); radius: number,
iterations: number,
canvas: HTMLCanvasElement = document.createElement('canvas')
) {
canvas.width = img.width; canvas.width = img.width;
canvas.height = img.height; canvas.height = img.height;
const ctx = canvas.getContext('2d', {alpha: false}); const ctx = canvas.getContext('2d', {alpha: false});
if(isFilterAvailable) { if(IS_CANVAS_FILTER_SUPPORTED) {
ctx.filter = `blur(${radius}px)`; ctx.filter = `blur(${radius}px)`;
ctx.drawImage(img, -radius * 2, -radius * 2, canvas.width + radius * 4, canvas.height + radius * 4); ctx.drawImage(img, -radius * 2, -radius * 2, canvas.width + radius * 4, canvas.height + radius * 4);
} else { } else {
@ -36,65 +39,62 @@ function processBlurNext(img: HTMLImageElement, radius: number, iterations: numb
fastBlurFunc(ctx, 0, 0, canvas.width, canvas.height, radius, iterations); fastBlurFunc(ctx, 0, 0, canvas.width, canvas.height, radius, iterations);
} }
resolve(canvas.toDataURL()); return canvas;
/* if(DEBUG) {
console.log(`[blur] end, radius: ${radius}, iterations: ${iterations}, time: ${performance.now() - perf}`);
} */
/* canvas.toBlob(blob => {
resolve(URL.createObjectURL(blob));
if(DEBUG) {
console.log(`[blur] end, radius: ${radius}, iterations: ${iterations}, time: ${performance.now() - perf}`);
}
}); */
});
} }
const blurPromises: Map<string, Promise<string>> = new Map(); type CacheValue = {canvas: HTMLCanvasElement, promise: Promise<void>};
const CACHE_SIZE = 1000; const cache: Map<string, CacheValue> = new Map();
const CACHE_SIZE = 150;
export default function blur(dataUri: string, radius: number = RADIUS, iterations: number = ITERATIONS) { export default function blur(dataUri: string, radius: number = RADIUS, iterations: number = ITERATIONS) {
if(!dataUri) { if(!dataUri) {
console.error('no dataUri for blur', dataUri); throw 'no dataUri for blur: ' + dataUri;
return Promise.resolve(dataUri);
} }
if(blurPromises.size > CACHE_SIZE) { if(cache.size > CACHE_SIZE) {
blurPromises.clear(); cache.clear();
} }
if(blurPromises.has(dataUri)) return blurPromises.get(dataUri); const canvas = document.createElement('canvas');
const promise = new Promise<string>((resolve) => { canvas.className = 'canvas-thumbnail';
let cached = cache.get(dataUri);
if(!cached) {
const promise: CacheValue['promise'] = new Promise((resolve) => {
//return resolve(dataUri); //return resolve(dataUri);
requireBlurPromise.then(() => { requireBlurPromise.then(() => {
const img = new Image(); const img = new Image();
img.onload = () => { img.onload = () => {
if(isFilterAvailable) { // if(IS_CANVAS_FILTER_SUPPORTED) {
processBlurNext(img, radius, iterations).then(resolve); // resolve(processBlurNext(img, radius, iterations));
} else { // } else {
addHeavyTask({ const promise = addHeavyTask({
items: [[img, radius, iterations]], items: [[img, radius, iterations, canvas]],
context: null, context: null,
process: processBlurNext process: processBlurNext
}, 'unshift').then(results => { }, 'unshift');
resolve(results[0]);
promise.then(() => {
resolve();
}); });
} // }
}; };
img.src = dataUri; img.src = dataUri;
/* addHeavyTask({
items: [[dataUri, radius, iterations]],
context: null,
process: processBlur
}, 'unshift').then(results => {
resolve(results[0]);
}); */
}); });
}); });
blurPromises.set(dataUri, promise); cache.set(dataUri, cached = {
canvas,
promise
});
} else {
canvas.width = cached.canvas.width;
canvas.height = cached.canvas.height;
canvas.getContext('2d').drawImage(cached.canvas, 0, 0);
}
return promise; return {
...cached,
canvas
};
} }

3
src/helpers/bytes/bytesToDataURL.ts

@ -0,0 +1,3 @@
export default function bytesToDataURL(bytes: Uint8Array, mimeType: string = 'image/jpeg') {
return `data:${mimeType};base64,${btoa(String.fromCharCode(...bytes))}`;
}

4
src/helpers/dom/renderImageFromUrl.ts

@ -17,7 +17,7 @@ const set = (elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLVideoE
export default function renderImageFromUrl( export default function renderImageFromUrl(
elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLVideoElement, elem: HTMLElement | HTMLImageElement | SVGImageElement | HTMLVideoElement,
url: string, url: string,
callback?: (err?: Event) => void, callback?: () => void,
useCache = true useCache = true
) { ) {
if(!url) { if(!url) {
@ -61,7 +61,7 @@ export default function renderImageFromUrl(
} }
export function renderImageFromUrlPromise(elem: Parameters<typeof renderImageFromUrl>[0], url: string, useCache?: boolean) { export function renderImageFromUrlPromise(elem: Parameters<typeof renderImageFromUrl>[0], url: string, useCache?: boolean) {
return new Promise<Event>((resolve) => { return new Promise<void>((resolve) => {
renderImageFromUrl(elem, url, resolve, useCache); renderImageFromUrl(elem, url, resolve, useCache);
}); });
} }

28
src/helpers/heavyQueue.ts

@ -7,26 +7,27 @@
import deferredPromise, { CancellablePromise } from "./cancellablePromise"; import deferredPromise, { CancellablePromise } from "./cancellablePromise";
import { getHeavyAnimationPromise } from "../hooks/useHeavyAnimationCheck"; import { getHeavyAnimationPromise } from "../hooks/useHeavyAnimationCheck";
import { fastRaf } from "./schedulers"; import { fastRaf } from "./schedulers";
import { ArgumentTypes } from "../types";
type HeavyQueue<T> = { type HeavyQueue<T extends HeavyQueue<any>> = {
items: any[], items: ArgumentTypes<T['process']>[],
process: (...args: any[]) => T, process: (...args: any[]) => ReturnType<T['process']>,
context: any, context: any,
promise?: CancellablePromise<ReturnType<HeavyQueue<T>['process']>[]> promise?: CancellablePromise<ReturnType<T['process']>[]>
}; };
const heavyQueue: HeavyQueue<any>[] = []; const heavyQueue: HeavyQueue<any>[] = [];
let processingQueue = false; let processingQueue = false;
export default function addHeavyTask<T>(queue: HeavyQueue<T>, method: 'push' | 'unshift' = 'push') { export default function addHeavyTask<T extends HeavyQueue<T>>(queue: T, method: 'push' | 'unshift' = 'push') {
if(!queue.items.length) { if(!queue.items.length) {
return Promise.resolve([]); return Promise.resolve([]) as typeof promise;
} }
queue.promise = deferredPromise<T[]>(); const promise = queue.promise = deferredPromise();
heavyQueue[method](queue); heavyQueue[method](queue);
processHeavyQueue(); processHeavyQueue();
return queue.promise; return promise;
} }
function processHeavyQueue() { function processHeavyQueue() {
@ -41,23 +42,24 @@ function processHeavyQueue() {
} }
} }
function timedChunk<T>(queue: HeavyQueue<T>) { function timedChunk<T extends HeavyQueue<T>>(queue: HeavyQueue<T>) {
if(!queue.items.length) { if(!queue.items.length) {
queue.promise.resolve([]); queue.promise.resolve([] as any);
return Promise.resolve([]); return Promise.resolve([]);
} }
const todo = queue.items.slice(); const todo = queue.items.slice();
const results: T[] = []; const results: ReturnType<T['process']>[] = [];
return new Promise<T[]>((resolve, reject) => { return new Promise<typeof results>((resolve, reject) => {
const f = async() => { const f = async() => {
const start = performance.now(); const start = performance.now();
do { do {
await getHeavyAnimationPromise(); await getHeavyAnimationPromise();
const possiblePromise = queue.process.apply(queue.context, todo.shift()); const possiblePromise = queue.process.apply(queue.context, todo.shift());
let realResult: T; let realResult: typeof results[0];
// @ts-ignore
if(possiblePromise instanceof Promise) { if(possiblePromise instanceof Promise) {
try { try {
realResult = await possiblePromise; realResult = await possiblePromise;

5
src/lib/appManagers/appDocsManager.ts

@ -314,8 +314,9 @@ export class AppDocsManager {
const cacheContext = appDownloadManager.getCacheContext(doc, thumb.type); const cacheContext = appDownloadManager.getCacheContext(doc, thumb.type);
if(!cacheContext.url) { if(!cacheContext.url) {
if('bytes' in thumb) { if('bytes' in thumb) {
promise = blur(appPhotosManager.getPreviewURLFromBytes(thumb.bytes, !!doc.sticker)).then(url => { const result = blur(appPhotosManager.getPreviewURLFromBytes(thumb.bytes, !!doc.sticker));
cacheContext.url = url; promise = result.promise.then(() => {
cacheContext.url = result.canvas.toDataURL();
}) as any; }) as any;
} else { } else {
//return this.getFileURL(doc, false, thumb); //return this.getFileURL(doc, false, thumb);

21
src/lib/appManagers/appPhotosManager.ts

@ -28,6 +28,7 @@ import windowSize from "../../helpers/windowSize";
import bytesFromHex from "../../helpers/bytes/bytesFromHex"; import bytesFromHex from "../../helpers/bytes/bytesFromHex";
import isObject from "../../helpers/object/isObject"; import isObject from "../../helpers/object/isObject";
import safeReplaceArrayInObject from "../../helpers/object/safeReplaceArrayInObject"; import safeReplaceArrayInObject from "../../helpers/object/safeReplaceArrayInObject";
import bytesToDataURL from "../../helpers/bytes/bytesToDataURL";
export type MyPhoto = Photo.photo; export type MyPhoto = Photo.photo;
@ -171,8 +172,7 @@ export class AppPhotosManager {
mimeType = 'image/jpeg'; mimeType = 'image/jpeg';
} }
const blob = new Blob([arr], {type: mimeType}); return bytesToDataURL(arr, mimeType);
return URL.createObjectURL(blob);
} }
/** /**
@ -210,14 +210,19 @@ export class AppPhotosManager {
public getImageFromStrippedThumb(photo: MyPhoto | MyDocument, thumb: PhotoSize.photoCachedSize | PhotoSize.photoStrippedSize, useBlur: boolean) { public getImageFromStrippedThumb(photo: MyPhoto | MyDocument, thumb: PhotoSize.photoCachedSize | PhotoSize.photoStrippedSize, useBlur: boolean) {
const url = this.getPreviewURLFromThumb(photo, thumb, false); const url = this.getPreviewURLFromThumb(photo, thumb, false);
const image = new Image(); let element: HTMLImageElement | HTMLCanvasElement, loadPromise: Promise<void>;
image.classList.add('thumbnail'); if(!useBlur) {
element = new Image();
loadPromise = renderImageFromUrlPromise(element, url);
} else {
const result = blur(url);
element = result.canvas;
loadPromise = result.promise;
}
const loadPromise = (useBlur ? blur(url) : Promise.resolve(url)).then(url => { element.classList.add('thumbnail');
return renderImageFromUrlPromise(image, url);
});
return {image, loadPromise}; return {image: element, loadPromise};
} }
public setAttachmentSize( public setAttachmentSize(

6
src/scss/style.scss

@ -1213,6 +1213,12 @@ middle-ellipsis-element {
fill: rgba(0, 0, 0, .08); fill: rgba(0, 0, 0, .08);
} }
.canvas-thumbnail {
position: absolute;
width: 100%;
height: 100%;
}
.media-photo, .media-photo,
.media-video, .media-video,
.media-sticker, .media-sticker,

Loading…
Cancel
Save