From c8661b8a81bfb5692398dd189408f2a31a25c56c Mon Sep 17 00:00:00 2001 From: Fangdun Tsai Date: Mon, 28 Oct 2024 14:21:29 +0800 Subject: [PATCH] chore: improve pdf preview --- .../attachment-viewer/styles.css.ts | 21 +- .../components/attachment-viewer/titlebar.tsx | 13 +- .../src/components/attachment-viewer/utils.ts | 87 +++-- .../components/attachment-viewer/viewer.tsx | 328 +++++++++--------- .../attachment-viewer/worker/types.ts | 52 ++- .../attachment-viewer/worker/utils.ts | 20 ++ .../attachment-viewer/worker/worker.ts | 131 +++---- 7 files changed, 375 insertions(+), 277 deletions(-) create mode 100644 packages/frontend/component/src/components/attachment-viewer/worker/utils.ts diff --git a/packages/frontend/component/src/components/attachment-viewer/styles.css.ts b/packages/frontend/component/src/components/attachment-viewer/styles.css.ts index 193fa68e99391..8407b9367d6c6 100644 --- a/packages/frontend/component/src/components/attachment-viewer/styles.css.ts +++ b/packages/frontend/component/src/components/attachment-viewer/styles.css.ts @@ -103,8 +103,22 @@ export const errorBtns = style({ marginTop: '28px', }); -export const viewerPage = style({ +export const mainItemWrapper = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', margin: '20px auto', + selectors: { + '&:first-of-type': { + marginTop: 0, + }, + '&:last-of-type': { + marginBottom: 0, + }, + }, +}); + +export const viewerPage = style({ maxWidth: 'calc(100% - 40px)', background: cssVarV2('layer/white'), boxSizing: 'border-box', @@ -148,8 +162,11 @@ export const thumbnailsPages = style({ }, }); -export const thumbnailsPage = style({ +export const thumbnailsItemWrapper = style({ margin: '0px 12px 12px', +}); + +export const thumbnailsPage = style({ display: 'flex', overflow: 'clip', // width: '100%', diff --git a/packages/frontend/component/src/components/attachment-viewer/titlebar.tsx b/packages/frontend/component/src/components/attachment-viewer/titlebar.tsx index 0874a8e10ac8f..52323123811ac 100644 --- a/packages/frontend/component/src/components/attachment-viewer/titlebar.tsx +++ b/packages/frontend/component/src/components/attachment-viewer/titlebar.tsx @@ -12,7 +12,7 @@ import { useState } from 'react'; import { IconButton } from '../../ui/button'; import { Menu, MenuItem } from '../../ui/menu'; import * as styles from './styles.css'; -import { saveBufferToFile } from './utils'; +import { download } from './utils'; const items = [ { @@ -23,11 +23,7 @@ const items = [ { name: 'Download', icon: , - action(model: AttachmentBlockModel) { - const { sourceId, name } = model; - if (!sourceId) return; - saveBufferToFile(sourceId, name).catch(console.error); - }, + action: download, }, ]; @@ -65,7 +61,10 @@ export const Titlebar = ({ .{ext}
{size}
- }> + } + onClick={() => download(model)} + > } rootOptions={{ diff --git a/packages/frontend/component/src/components/attachment-viewer/utils.ts b/packages/frontend/component/src/components/attachment-viewer/utils.ts index 2238b3eb09857..d3c6103e74fe4 100644 --- a/packages/frontend/component/src/components/attachment-viewer/utils.ts +++ b/packages/frontend/component/src/components/attachment-viewer/utils.ts @@ -1,40 +1,63 @@ -import { fileTypeFromBuffer } from 'file-type'; +import type { AttachmentBlockModel } from '@blocksuite/affine/blocks'; -export async function saveBufferToFile(url: string, filename: string) { - // given input url may not have correct mime type - const blob = await attachmentUrlToBlob(url); - if (!blob) { - return; +export async function getAttachmentBlob(model: AttachmentBlockModel) { + const sourceId = model.sourceId; + if (!sourceId) { + return null; } - const blobUrl = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = blobUrl; - a.download = filename; - document.body.append(a); - a.click(); - a.remove(); - URL.revokeObjectURL(blobUrl); -} - -export async function attachmentUrlToBlob( - url: string -): Promise { - const buffer = await fetch(url).then(response => response.arrayBuffer()); + const doc = model.doc; + let blob = await doc.blobSync.get(sourceId); - if (!buffer) { - console.warn('Could not get blob'); - return; + if (blob) { + blob = new Blob([blob], { type: model.type }); } - try { - const type = await fileTypeFromBuffer(buffer); - if (!type) { + + return blob; +} + +export function download(model: AttachmentBlockModel) { + (async () => { + const blob = await getAttachmentBlob(model); + if (!blob) { return; } - const blob = new Blob([buffer], { type: type.mime }); - return blob; - } catch (error) { - console.error('Error converting attachment to blob', error); - } - return; + + const blobUrl = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = blobUrl; + a.download = model.name; + document.body.append(a); + a.click(); + a.remove(); + URL.revokeObjectURL(blobUrl); + })().catch(console.error); +} + +export function renderItem( + scroller: HTMLElement | null, + id: number, + imageBitmap: ImageBitmap +) { + if (!scroller) return; + + const wrapper = scroller.querySelector(`[data-index="${id}"]`); + if (!wrapper) return; + + const item = wrapper.firstElementChild; + if (!item) return; + if (item.firstElementChild) return; + + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('bitmaprenderer'); + if (!ctx) return; + + canvas.width = imageBitmap.width; + canvas.height = imageBitmap.height; + canvas.style.width = '100%'; + canvas.style.height = '100%'; + + ctx.transferFromImageBitmap(imageBitmap); + + item.append(canvas); } diff --git a/packages/frontend/component/src/components/attachment-viewer/viewer.tsx b/packages/frontend/component/src/components/attachment-viewer/viewer.tsx index 78b19d60b0210..8a747cfd62d0e 100644 --- a/packages/frontend/component/src/components/attachment-viewer/viewer.tsx +++ b/packages/frontend/component/src/components/attachment-viewer/viewer.tsx @@ -2,7 +2,7 @@ import type { AttachmentBlockModel } from '@blocksuite/affine/blocks'; import { CollapseIcon, ExpandIcon } from '@blocksuite/icons/rc'; import clsx from 'clsx'; import { debounce } from 'lodash-es'; -import type { HTMLAttributes, PropsWithChildren, ReactElement } from 'react'; +import type { ReactElement } from 'react'; import React, { useCallback, useEffect, @@ -10,15 +10,18 @@ import React, { useRef, useState, } from 'react'; -import type { VirtuosoHandle } from 'react-virtuoso'; +import type { VirtuosoHandle, VirtuosoProps } from 'react-virtuoso'; import { Virtuoso } from 'react-virtuoso'; import { IconButton } from '../../ui/button'; import { Scrollable } from '../../ui/scrollbar'; +import { observeResize } from '../../utils'; import * as styles from './styles.css'; -// import { observeResize } from '../../utils'; -import type { MessageData, MessageDataType } from './worker/types'; -import { MessageOp, State } from './worker/types'; +import { getAttachmentBlob, renderItem } from './utils'; +import type { DocInfo, MessageData, MessageDataType } from './worker/types'; +import { MessageOp, MessageState, RenderKind, State } from './worker/types'; + +type ItemProps = VirtuosoProps; const Page = React.memo( ({ @@ -34,17 +37,8 @@ const Page = React.memo( return (
- -
+ style={{ width: `${width}px`, height: `${height}px` }} + > ); } ); @@ -68,41 +62,50 @@ const Thumbnail = React.memo( onSelect: (index: number) => void; }) => { return ( -
onSelect(index)}> - -
+
onSelect(index)} + >
); } ); Thumbnail.displayName = 'viewer-thumbnail'; -const Scroller = React.forwardRef< - HTMLDivElement, - PropsWithChildren> ->(({ style, ...props }, ref) => { - return ( - - - - - ); -}); +const Scroller = React.forwardRef( + ({ ...props }, ref) => { + return ( + + + + + ); + } +); Scroller.displayName = 'viewer-scroller'; +const Item = React.forwardRef( + ({ ...props }, ref) => { + return
; + } +); + +Item.displayName = 'viewer-item'; + interface ViewerProps { model: AttachmentBlockModel; } export const Viewer = ({ model }: ViewerProps): ReactElement => { - const [connected, setConnected] = useState(false); - const [loaded, setLoaded] = useState(false); - const [docInfo, setDocInfo] = useState({ + const [state, setState] = useState(State.Connecting); + const [viewportInfo, setViewportInfo] = useState({ + dpi: window.devicePixelRatio, + width: 1, + height: 1, + }); + const [docInfo, setDocInfo] = useState({ cursor: 0, total: 0, width: 1, @@ -127,63 +130,26 @@ export const Viewer = ({ model }: ViewerProps): ReactElement => { }); const post = useCallback( - ( - type: T, - data?: MessageDataType[T], - transfer = [] - ) => { - workerRef.current?.postMessage( - { - state: State.Poll, - type, - [type]: data, - }, - transfer - ); + (type: T, data?: MessageDataType[T]) => { + workerRef.current?.postMessage({ + type, + [type]: data, + state: MessageState.Poll, + }); }, [workerRef] ); const render = useCallback( - (id: number, imageData: ImageData) => { - const el = scrollerRef.current; - if (!el) return; - - const canvas: HTMLCanvasElement | null = el.querySelector( - `[data-index="${id}"] canvas` + (id: number, kind: RenderKind, imageBitmap: ImageBitmap) => { + renderItem( + (kind === RenderKind.Page ? scrollerRef : thumbnailsScrollerRef) + .current, + id, + imageBitmap ); - if (!canvas) return; - if (canvas.dataset.rendered) return; - - // TODO(@fundon): improve - const ctx = canvas.getContext('2d'); - if (ctx) { - ctx.putImageData(imageData, 0, 0); - canvas.dataset.rendered = 'true'; - } - }, - [scrollerRef] - ); - - const renderThumbnail = useCallback( - (id: number, imageData: ImageData) => { - const el = thumbnailsScrollerRef.current; - if (!el) return; - - const canvas: HTMLCanvasElement | null = el.querySelector( - `[data-index="${id}"] canvas` - ); - if (!canvas) return; - if (canvas.dataset.rendered) return; - - // TODO(@fundon): improve - const ctx = canvas.getContext('2d'); - if (ctx) { - ctx.putImageData(imageData, 0, 0); - canvas.dataset.rendered = 'true'; - } }, - [thumbnailsScrollerRef] + [scrollerRef, thumbnailsScrollerRef] ); const onScroll = useCallback(() => { @@ -193,7 +159,10 @@ export const Viewer = ({ model }: ViewerProps): ReactElement => { const { scrollTop, scrollHeight } = el; setDocInfo(info => { - const cursor = Math.ceil(scrollTop / (scrollHeight / info.total)); + const cursor = Math.min( + Math.ceil(scrollTop / (scrollHeight / info.total)), + info.total - 1 + ); // thumbnailsScrollerHandleRef.current?.scrollToIndex(cursor) return { ...info, @@ -212,41 +181,46 @@ export const Viewer = ({ model }: ViewerProps): ReactElement => { ); const updateMainVisibleRange = useMemo( - () => debounce(setMainVisibleRange, 233, { leading: true, trailing: true }), + () => debounce(setMainVisibleRange, 233, { trailing: true }), [setMainVisibleRange] ); const updateThumbnailsVisibleRange = useMemo( - () => - debounce(setThumbnailsVisibleRange, 233, { - leading: true, - trailing: true, - }), + () => debounce(setThumbnailsVisibleRange, 233, { trailing: true }), [setThumbnailsVisibleRange] ); - // useEffect(() => { - // const el = viewerRef.current; - // if (!el) return; + useEffect(() => { + const el = viewerRef.current; + if (!el) return; + + return observeResize(el, entry => { + const rect = entry.contentRect; + setViewportInfo(info => ({ + ...info, + width: rect.width, + height: rect.height, + })); + }); + }, [viewerRef]); - // return observeResize(el, entry => { - // console.log(entry); - // }); - // }, []); + useEffect(() => { + post(MessageOp.SyncViewportInfo, viewportInfo); + }, [viewportInfo, post]); useEffect(() => { const { startIndex, endIndex } = mainVisibleRange; let index = startIndex; - for (; index < endIndex + 1; index++) { - post(MessageOp.Render, { index, kind: 'page' }); + for (; index <= endIndex; index++) { + post(MessageOp.Render, { index, kind: RenderKind.Page }); } }, [mainVisibleRange, post]); useEffect(() => { const { startIndex, endIndex } = thumbnailsVisibleRange; let index = startIndex; - for (; index < endIndex + 1; index++) { - post(MessageOp.Render, { index, kind: 'thumbnail' }); + for (; index <= endIndex; index++) { + post(MessageOp.Render, { index, kind: RenderKind.Thumbnail }); } }, [thumbnailsVisibleRange, post]); @@ -262,29 +236,30 @@ export const Viewer = ({ model }: ViewerProps): ReactElement => { const { type, state } = data; if (type === MessageOp.Init) { - setConnected(state === State.Ready); + setState( + state === MessageState.Ready ? State.Connected : State.Connecting + ); return; } - if (type === MessageOp.Open) { - setLoaded(state === State.Ready); + if (type === MessageOp.Open && state === MessageState.Ready) { + setState(State.Loaded); return; } - if (state === State.Poll) return; + if (state === MessageState.Poll) { + return; + } switch (type) { - case MessageOp.ReadInfo: { - const action = data[type]; - setDocInfo(info => ({ ...info, ...action })); + case MessageOp.SyncDocInfo: { + const updated = data[type]; + setDocInfo(info => ({ ...info, ...updated })); + setState(State.Synced); break; } case MessageOp.Rendered: { - const { index, imageData, kind } = data[type]; - if (kind === 'page') { - render(index, imageData); - } else { - renderThumbnail(index, imageData); - } + const { index, kind, imageBitmap } = data[type]; + render(index, kind, imageBitmap); break; } } @@ -297,56 +272,79 @@ export const Viewer = ({ model }: ViewerProps): ReactElement => { return () => { workerRef.current?.terminate(); }; - }, [model, post, render, renderThumbnail]); + }, [model, post, render]); useEffect(() => { - if (!connected) return; if (!model.sourceId) return; - model.doc.blobSync - .get(model.sourceId) - .then(blob => { - if (!blob) return; - post(MessageOp.Open, { blob, dpi: window.devicePixelRatio }); - }) - .catch(console.error); - }, [connected, model, post]); + if (state === State.Connected) { + getAttachmentBlob(model) + .then(blob => { + if (!blob) return; + setState(State.Loading); + post(MessageOp.Open, { blob }); + }) + .catch(console.error); + return; + } - useEffect(() => { - if (!loaded) return; - post(MessageOp.ReadInfo); - }, [loaded, post]); + if (state === State.Loaded) { + setState(State.Syncing); + post(MessageOp.SyncDocInfo); + return; + } + }, [state, post, model, docInfo]); - const pageContent = (index: number) => { - return ( - - ); - }; + const pageContent = useCallback( + (index: number) => { + return ( + + ); + }, + [docInfo] + ); - const thumbnailContent = (index: number) => { - return ( - - ); - }; + const thumbnailContent = useCallback( + (index: number) => { + return ( + + ); + }, + [docInfo, onSelect] + ); + + const mainComponents = useMemo(() => { + return { + Header: () =>
, + Footer: () =>
, + Item: (props: ItemProps) => ( + + ), + Scroller, + }; + }, []); - const components = useMemo(() => { + const thumbnailsComponents = useMemo(() => { return { + Item: (props: ItemProps) => ( + + ), Scroller, }; }, []); @@ -362,7 +360,7 @@ export const Viewer = ({ model }: ViewerProps): ReactElement => { ])} ref={viewerRef} > - onScroll={onScroll} ref={scrollerHandleRef} scrollerRef={scroller => { @@ -372,17 +370,17 @@ export const Viewer = ({ model }: ViewerProps): ReactElement => { className={styles.virtuoso} rangeChanged={updateMainVisibleRange} increaseViewportBy={{ - top: docInfo.height * Math.min(5, docInfo.total), - bottom: docInfo.height * Math.min(5, docInfo.total), + top: (docInfo.height + 20) * Math.min(5, docInfo.total), + bottom: (docInfo.height + 20) * Math.min(5, docInfo.total), }} totalCount={docInfo.total} itemContent={pageContent} - components={components} + components={mainComponents} />
{collapsed ? null : (
- style={{ height: Math.min(3, docInfo.total) * @@ -398,7 +396,7 @@ export const Viewer = ({ model }: ViewerProps): ReactElement => { className={styles.virtuoso} totalCount={docInfo.total} itemContent={thumbnailContent} - components={components} + components={thumbnailsComponents} />
)} diff --git a/packages/frontend/component/src/components/attachment-viewer/worker/types.ts b/packages/frontend/component/src/components/attachment-viewer/worker/types.ts index 4ec6b2a064931..b5f0da736b053 100644 --- a/packages/frontend/component/src/components/attachment-viewer/worker/types.ts +++ b/packages/frontend/component/src/components/attachment-viewer/worker/types.ts @@ -1,4 +1,26 @@ +export type DocInfo = { + cursor: number; + total: number; + width: number; + height: number; +}; + +export type ViewportInfo = { + dpi: number; + width: number; + height: number; +}; + export enum State { + Connecting = 0, + Connected, + Loading, + Loaded, + Syncing, + Synced, +} + +export enum MessageState { Poll, Ready, } @@ -6,28 +28,40 @@ export enum State { export enum MessageOp { Init, Open, - ReadInfo, + SyncDocInfo, + SyncViewportInfo, Render, Rendered, } -export type MessageDataMap = { +export enum RenderKind { + Page, + Thumbnail, +} + +export interface MessageDataMap { [MessageOp.Init]: undefined; - [MessageOp.Open]: { blob: Blob; dpi: number }; - [MessageOp.ReadInfo]: { total: number; width: number; height: number }; - [MessageOp.Render]: { index: number; kind: 'page' | 'thumbnail' }; + [MessageOp.Open]: { + blob: Blob; + }; + [MessageOp.SyncDocInfo]: Partial; + [MessageOp.SyncViewportInfo]: Partial; + [MessageOp.Render]: { + index: number; + kind: RenderKind; + }; [MessageOp.Rendered]: { index: number; - imageData: ImageData; - kind: 'page' | 'thumbnail'; + kind: RenderKind; + imageBitmap: ImageBitmap; }; -}; +} export type MessageDataType = { [P in keyof T]: T[P]; }; export type MessageData = { - state: State; + state: MessageState; type: T; } & P; diff --git a/packages/frontend/component/src/components/attachment-viewer/worker/utils.ts b/packages/frontend/component/src/components/attachment-viewer/worker/utils.ts new file mode 100644 index 0000000000000..495630212a496 --- /dev/null +++ b/packages/frontend/component/src/components/attachment-viewer/worker/utils.ts @@ -0,0 +1,20 @@ +export function resizeImageBitmap( + imageBitmap: ImageBitmap, + options: { + resizeWidth: number; + resizeHeight: number; + } +) { + return createImageBitmap( + imageBitmap, + 0, + 0, + imageBitmap.width, + imageBitmap.height, + { + colorSpaceConversion: 'none', + resizeQuality: 'pixelated', + ...options, + } + ); +} diff --git a/packages/frontend/component/src/components/attachment-viewer/worker/worker.ts b/packages/frontend/component/src/components/attachment-viewer/worker/worker.ts index d6423a6083148..ccefd4e66a3aa 100644 --- a/packages/frontend/component/src/components/attachment-viewer/worker/worker.ts +++ b/packages/frontend/component/src/components/attachment-viewer/worker/worker.ts @@ -8,55 +8,35 @@ import { } from '@toeverything/pdf-viewer'; import type { MessageData, MessageDataType } from './types'; -import { MessageOp, State } from './types'; +import { MessageOp, MessageState, RenderKind } from './types'; +import { resizeImageBitmap } from './utils'; -const logger = new DebugLogger('affine:pdf-worker'); +const logger = new DebugLogger('affine:worker:pdf'); -let dpi = 2; let inited = false; let viewer: Viewer | null = null; let doc: Document | undefined = undefined; -const cached = new Map(); +const canvas = new OffscreenCanvas(0, 0); +const ctx = canvas.getContext('2d'); +const cached = new Map(); const docInfo = { cursor: 0, total: 0, width: 1, height: 1 }; +const viewportInfo = { dpi: 2, width: 1, height: 1 }; const flags = PageRenderingflags.REVERSE_BYTE_ORDER | PageRenderingflags.ANNOT; function post(type: T, data?: MessageDataType[T]) { - self.postMessage({ state: State.Ready, type, [type]: data }); -} - -async function resizeImageData( - imageData: ImageData, - options: { - resizeWidth: number; - resizeHeight: number; - } -) { - const { resizeWidth: w, resizeHeight: h } = options; - const bitmap = await createImageBitmap( - imageData, - 0, - 0, - imageData.width, - imageData.height, - options - ); - const canvas = new OffscreenCanvas(w, h); - const ctx = canvas.getContext('2d'); - if (!ctx) return imageData; - ctx.drawImage(bitmap, 0, 0); - return ctx.getImageData(0, 0, w, h); + self.postMessage({ state: MessageState.Ready, type, [type]: data }); } async function start() { logger.debug('pdf worker pending'); - self.postMessage({ state: State.Poll, type: MessageOp.Init }); + self.postMessage({ state: MessageState.Poll, type: MessageOp.Init }); const pdfium = await createPDFium(); viewer = new Viewer(new Runtime(pdfium)); inited = true; - self.postMessage({ state: State.Ready, type: MessageOp.Init }); + self.postMessage({ state: MessageState.Ready, type: MessageOp.Init }); logger.debug('pdf worker ready'); } @@ -69,14 +49,13 @@ async function process({ data }: MessageEvent) { const { type, state } = data; - if (state !== State.Poll) return; + if (state !== MessageState.Poll) return; switch (type) { case MessageOp.Open: { const action = data[type]; if (!action?.blob) return; - dpi = action.dpi; doc = await viewer.openWithBlob(action.blob); if (!doc) return; @@ -85,17 +64,36 @@ async function process({ data }: MessageEvent) { break; } - case MessageOp.ReadInfo: { + case MessageOp.SyncViewportInfo: { + const updated = data[type]; + + if (updated) { + Object.assign(viewportInfo, updated); + } + + break; + } + + case MessageOp.SyncDocInfo: { if (!doc) return; + const updated = data[type]; + + if (updated) { + Object.assign(docInfo, updated); + } + const page = doc.page(0); + if (page) { - docInfo.cursor = 0; - docInfo.total = doc.pageCount(); - docInfo.height = page.height(); - docInfo.width = page.width(); + Object.assign(docInfo, { + cursor: 0, + total: doc.pageCount(), + height: Math.ceil(page.height()), + width: Math.ceil(page.width()), + }); page.close(); - post(MessageOp.ReadInfo, docInfo); + post(MessageOp.SyncDocInfo, docInfo); } break; } @@ -105,24 +103,23 @@ async function process({ data }: MessageEvent) { const { index, kind } = data[type]; - let imageData = cached.size > 0 ? cached.get(index) : undefined; - if (imageData) { - if (kind === 'thumbnail') { - const resizeWidth = (94 * dpi) >> 0; - const resizeHeight = - ((docInfo.height / docInfo.width) * resizeWidth) >> 0; - imageData = await resizeImageData(imageData, { - resizeWidth, - resizeHeight, + let imageBitmap = cached.size > 0 ? cached.get(index) : undefined; + if (imageBitmap) { + if (kind === RenderKind.Thumbnail) { + const rw = 94 * viewportInfo.dpi; + const rh = (docInfo.height / docInfo.width) * rw; + imageBitmap = await resizeImageBitmap(imageBitmap, { + resizeWidth: Math.ceil(rw), + resizeHeight: Math.ceil(rh), }); } - post(MessageOp.Rendered, { index, imageData, kind }); + post(MessageOp.Rendered, { index, kind, imageBitmap }); return; } - const width = Math.ceil(docInfo.width * dpi); - const height = Math.ceil(docInfo.height * dpi); + const width = Math.ceil(docInfo.width * viewportInfo.dpi); + const height = Math.ceil(docInfo.height * viewportInfo.dpi); const page = doc.page(index); if (page) { @@ -135,21 +132,31 @@ async function process({ data }: MessageEvent) { bitmap.close(); page.close(); - imageData = new ImageData(new Uint8ClampedArray(data), width, height); - - cached.set(index, imageData); - - if (kind === 'thumbnail') { - const resizeWidth = (94 * dpi) >> 0; - const resizeHeight = - ((docInfo.height / docInfo.width) * resizeWidth) >> 0; - imageData = await resizeImageData(imageData, { - resizeWidth, - resizeHeight, + if (canvas.width !== width || canvas.height !== height) { + canvas.width = width; + canvas.height = height; + } + const imageData = new ImageData( + new Uint8ClampedArray(data), + width, + height + ); + ctx?.clearRect(0, 0, width, height); + ctx?.putImageData(imageData, 0, 0); + imageBitmap = canvas.transferToImageBitmap(); + + cached.set(index, imageBitmap); + + if (kind === RenderKind.Thumbnail) { + const rw = 94 * viewportInfo.dpi; + const rh = (docInfo.height / docInfo.width) * rw; + imageBitmap = await resizeImageBitmap(imageBitmap, { + resizeWidth: Math.ceil(rw), + resizeHeight: Math.ceil(rh), }); } - post(MessageOp.Rendered, { index, imageData, kind }); + post(MessageOp.Rendered, { index, kind, imageBitmap }); } break;