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..83faa110b7529 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',
@@ -116,12 +130,15 @@ export const viewerPage = style({
});
export const thumbnails = style({
+ display: 'flex',
+ flexDirection: 'column',
position: 'absolute',
boxSizing: 'border-box',
width: '120px',
padding: '12px 0',
right: '30px',
bottom: '30px',
+ maxHeight: 'calc(100% - 60px)',
borderRadius: '8px',
borderWidth: '1px',
borderStyle: 'solid',
@@ -135,8 +152,11 @@ export const thumbnails = style({
});
export const thumbnailsPages = style({
+ position: 'relative',
display: 'flex',
flexDirection: 'column',
+ maxHeight: '100%',
+ overflow: 'hidden',
// gap: '12px',
selectors: {
'&.collapsed': {
@@ -148,8 +168,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..0acc1fee61ebb 100644
--- a/packages/frontend/component/src/components/attachment-viewer/titlebar.tsx
+++ b/packages/frontend/component/src/components/attachment-viewer/titlebar.tsx
@@ -1,6 +1,6 @@
import type { AttachmentBlockModel } from '@blocksuite/affine/blocks';
import {
- EditIcon,
+ //EditIcon,
LocalDataIcon,
MoreHorizontalIcon,
ZoomDownIcon,
@@ -12,22 +12,20 @@ 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 = [
+ /*
{
name: 'Rename',
icon: ,
action(_model: AttachmentBlockModel) {},
},
+ */
{
name: 'Download',
icon: ,
- action(model: AttachmentBlockModel) {
- const { sourceId, name } = model;
- if (!sourceId) return;
- saveBufferToFile(sourceId, name).catch(console.error);
- },
+ action: download,
},
];
@@ -53,7 +51,7 @@ export const Titlebar = ({
ext,
size,
zoom = 100,
- isPDF = false,
+ //isPDF = false,
}: TitlebarProps) => {
const [openMenu, setOpenMenu] = useState(false);
@@ -65,7 +63,10 @@ export const Titlebar = ({
.{ext}
{size}
- }>
+ }
+ onClick={() => download(model)}
+ >
}
rootOptions={{
@@ -86,7 +87,8 @@ export const Titlebar = ({
styles.titlebarChild,
'zoom',
{
- show: isPDF,
+ // show: isPDF,
+ show: false,
},
])}
>
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..1b724a41fcd2c 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`
- );
- 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`
+ (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';
- }
},
- [thumbnailsScrollerRef]
+ [scrollerRef, thumbnailsScrollerRef]
);
const onScroll = useCallback(() => {
@@ -193,62 +159,74 @@ export const Viewer = ({ model }: ViewerProps): ReactElement => {
const { scrollTop, scrollHeight } = el;
setDocInfo(info => {
- const cursor = Math.ceil(scrollTop / (scrollHeight / info.total));
- // thumbnailsScrollerHandleRef.current?.scrollToIndex(cursor)
- return {
- ...info,
- cursor,
- };
+ const itemHeight = scrollHeight / info.total;
+ const n = scrollTop / itemHeight;
+ const t = n / info.total;
+ const index = Math.floor(n + t);
+ const cursor = Math.min(index, info.total - 1);
+
+ if (cursor === info.cursor) return info;
+ return { ...info, cursor };
});
- // }, [scrollerRef, thumbnailsScrollerHandleRef]);
}, [scrollerRef]);
const onSelect = useCallback(
(index: number) => {
- scrollerHandleRef.current?.scrollToIndex(index);
- setDocInfo(info => ({ ...info, cursor: index }));
+ scrollerHandleRef.current?.scrollToIndex({
+ index,
+ align: 'start',
+ behavior: 'smooth',
+ });
},
[scrollerHandleRef]
);
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(() => {
+ if (collapsed) return;
+
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]);
+ }, [collapsed, thumbnailsVisibleRange, post]);
useEffect(() => {
workerRef.current = new Worker(
@@ -262,29 +240,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,60 +276,93 @@ 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,
};
}, []);
+ const increaseViewportBy = useMemo(() => {
+ const size = Math.min(5, docInfo.total);
+ const itemHeight = docInfo.height + 20;
+ const height = Math.ceil(size * itemHeight);
+ return {
+ top: height,
+ bottom: height,
+ };
+ }, [docInfo]);
+
return (
{
])}
ref={viewerRef}
>
-
onScroll={onScroll}
ref={scrollerHandleRef}
scrollerRef={scroller => {
@@ -371,44 +383,37 @@ 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),
- }}
+ increaseViewportBy={increaseViewportBy}
totalCount={docInfo.total}
itemContent={pageContent}
- components={components}
+ components={mainComponents}
/>
- {collapsed ? null : (
-
- {
- if (thumbnailsScrollerRef.current) return;
- thumbnailsScrollerRef.current = scroller as HTMLElement;
- }}
- rangeChanged={updateThumbnailsVisibleRange}
- className={styles.virtuoso}
- totalCount={docInfo.total}
- itemContent={thumbnailContent}
- components={components}
- />
-
- )}
+
+
+ style={{
+ height: `${Math.min(viewportInfo.height - 60 - 24 - 24 - 2 - 8, docInfo.total * THUMBNAIL_WIDTH * ((docInfo.height + 12) / docInfo.width))}px`,
+ }}
+ ref={thumbnailsScrollerHandleRef}
+ scrollerRef={scroller => {
+ if (thumbnailsScrollerRef.current) return;
+ thumbnailsScrollerRef.current = scroller as HTMLElement;
+ }}
+ rangeChanged={updateThumbnailsVisibleRange}
+ className={styles.virtuoso}
+ totalCount={docInfo.total}
+ itemContent={thumbnailContent}
+ components={thumbnailsComponents}
+ />
+
- {docInfo.cursor + 1}/{docInfo.total}
+ {docInfo.total > 0 ? docInfo.cursor + 1 : 0}/
+ {docInfo.total}
:
}
- onClick={() => setCollapsed(state => !state)}
+ onClick={() => setCollapsed(!collapsed)}
/>
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;