Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: pdf module #8720

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,26 @@
"which-typed-array": "npm:@nolyfill/which-typed-array@latest",
"@reforged/maker-appimage/@electron-forge/maker-base": "7.5.0",
"macos-alias": "npm:@napi-rs/[email protected]",
"fs-xattr": "npm:@napi-rs/xattr@latest"
"fs-xattr": "npm:@napi-rs/xattr@latest",
"@blocksuite/affine": "portal:/Users/fundon/dev/toeverything/blocksuite/packages/affine/all",
"@blocksuite/affine-block-embed": "portal:/Users/fundon/dev/toeverything/blocksuite/packages/affine/block-embed",
"@blocksuite/affine-block-list": "portal:/Users/fundon/dev/toeverything/blocksuite/packages/affine/block-list",
"@blocksuite/affine-block-paragraph": "portal:/Users/fundon/dev/toeverything/blocksuite/packages/affine/block-paragraph",
"@blocksuite/affine-block-surface": "portal:/Users/fundon/dev/toeverything/blocksuite/packages/affine/block-surface",
"@blocksuite/affine-components": "portal:/Users/fundon/dev/toeverything/blocksuite/packages/affine/components",
"@blocksuite/data-view": "portal:/Users/fundon/dev/toeverything/blocksuite/packages/affine/data-view",
"@blocksuite/affine-model": "portal:/Users/fundon/dev/toeverything/blocksuite/packages/affine/model",
"@blocksuite/affine-shared": "portal:/Users/fundon/dev/toeverything/blocksuite/packages/affine/shared",
"@blocksuite/affine-widget-scroll-anchoring": "portal:/Users/fundon/dev/toeverything/blocksuite/packages/affine/widget-scroll-anchoring",
"@blocksuite/blocks": "portal:/Users/fundon/dev/toeverything/blocksuite/packages/blocks",
"@blocksuite/block-std": "portal:/Users/fundon/dev/toeverything/blocksuite/packages/framework/block-std",
"@blocksuite/global": "portal:/Users/fundon/dev/toeverything/blocksuite/packages/framework/global",
"@blocksuite/inline": "portal:/Users/fundon/dev/toeverything/blocksuite/packages/framework/inline",
"@blocksuite/store": "portal:/Users/fundon/dev/toeverything/blocksuite/packages/framework/store",
"@blocksuite/sync": "portal:/Users/fundon/dev/toeverything/blocksuite/packages/framework/sync",
"@blocksuite/presets": "portal:/Users/fundon/dev/toeverything/blocksuite/packages/presets",
"@toeverything/pdf-viewer": "portal:/Users/fundon/dev/toeverything/pdfium-builder/packages/pdf-viewer",
"@toeverything/pdfium": "portal:/Users/fundon/dev/toeverything/pdfium-builder/packages/pdfium",
"@toeverything/pdf-viewer-types": "portal:/Users/fundon/dev/toeverything/pdfium-builder/packages/types"
}
}
2 changes: 1 addition & 1 deletion packages/frontend/component/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
"@radix-ui/react-visually-hidden": "^1.1.0",
"@toeverything/pdf-viewer": "^0.1.0",
"@toeverything/pdf-viewer": "^0.1.1",
"@toeverything/theme": "^1.0.17",
"@vanilla-extract/dynamic": "^2.1.0",
"check-password-strength": "^2.0.10",
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-toolbar": "^1.0.4",
"@sentry/react": "^8.0.0",
"@toeverything/pdf-viewer": "^0.1.0",
"@toeverything/pdf-viewer": "^0.1.1",
"@toeverything/theme": "^1.0.17",
"@vanilla-extract/dynamic": "^2.1.0",
"animejs": "^3.2.2",
Expand Down
193 changes: 87 additions & 106 deletions packages/frontend/core/src/components/attachment-viewer/viewer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { IconButton, observeResize, Scrollable } from '@affine/component';
import {
type PDFChannel,
PDFService,
type PDFWorker,
} from '@affine/core/modules/pdf';
import { MessageOp, RenderKind } from '@affine/core/modules/pdf/workers/types';
import type { AttachmentBlockModel } from '@blocksuite/affine/blocks';
import { CollapseIcon, ExpandIcon } from '@blocksuite/icons/rc';
import { LiveData, useLiveData, useService } from '@toeverything/infra';
import clsx from 'clsx';
import { debounce } from 'lodash-es';
import type { ReactElement } from 'react';
Expand All @@ -18,8 +25,6 @@ import { Virtuoso } from 'react-virtuoso';

import * as styles from './styles.css';
import { getAttachmentBlob, renderItem } from './utils';
import type { DocInfo, MessageData, MessageDataType } from './worker/types';
import { MessageOp, RenderKind, State } from './worker/types';

type ItemProps = VirtuosoProps<null, undefined>;

Expand Down Expand Up @@ -100,22 +105,31 @@ interface ViewerProps {

export const Viewer = ({ model }: ViewerProps): ReactElement => {
const { showBoundary } = useErrorBoundary();
const [state, setState] = useState(State.Connecting);
const service = useService(PDFService);
const [worker, setWorker] = useState<PDFWorker | null>(null);
const docInfo = useLiveData(
useMemo(
() =>
worker
? worker.docInfo$
: new LiveData({
total: 0,
width: 1,
height: 1,
}),
[worker]
)
);
const [channel, setChannel] = useState<PDFChannel | null>(null);
const [cursor, setCursor] = useState(0);
const [viewportInfo, setViewportInfo] = useState({
dpi: window.devicePixelRatio,
width: 1,
height: 1,
});
const [docInfo, setDocInfo] = useState<DocInfo>({
total: 0,
width: 1,
height: 1,
});
const [cursor, setCursor] = useState(0);
const viewerRef = useRef<HTMLDivElement | null>(null);
const scrollerRef = useRef<HTMLElement | null>(null);
const scrollerHandleRef = useRef<VirtuosoHandle | null>(null);
const workerRef = useRef<Worker | null>(null);

const [mainVisibleRange, setMainVisibleRange] = useState({
startIndex: 0,
Expand All @@ -130,22 +144,6 @@ export const Viewer = ({ model }: ViewerProps): ReactElement => {
endIndex: 0,
});

const post = useCallback(
<T extends MessageOp>(
type: T,
data?: MessageDataType[T],
transfers?: Transferable[]
) => {
const message = { type, [type]: data };
if (transfers?.length) {
workerRef.current?.postMessage(message, transfers);
return;
}
workerRef.current?.postMessage(message);
},
[workerRef]
);

const render = useCallback(
(id: number, kind: RenderKind, imageData: ImageData) => {
const isPage = kind === RenderKind.Page;
Expand Down Expand Up @@ -209,85 +207,35 @@ export const Viewer = ({ model }: ViewerProps): ReactElement => {
}, [viewerRef]);

useEffect(() => {
post(MessageOp.Render, {
range: mainVisibleRange,
kind: RenderKind.Page,
scale: 1 * viewportInfo.dpi,
});
}, [viewportInfo, mainVisibleRange, post]);
if (!channel) return;

useEffect(() => {
if (collapsed) return;
const { startIndex, endIndex } = mainVisibleRange;
const scale = 1 * viewportInfo.dpi;

post(MessageOp.Render, {
range: thumbnailsVisibleRange,
kind: RenderKind.Thumbnail,
scale: (THUMBNAIL_WIDTH / docInfo.width) * viewportInfo.dpi,
});
}, [collapsed, docInfo, viewportInfo, thumbnailsVisibleRange, post]);

useLayoutEffect(() => {
workerRef.current = new Worker(
/* webpackChunkName: "pdf.worker" */ new URL(
'./worker/worker.ts',
import.meta.url
)
);

async function process({ data }: MessageEvent<MessageData>) {
const { type } = data;

switch (type) {
case MessageOp.Init: {
setState(State.Connecting);
break;
}

case MessageOp.Inited: {
setState(State.Connected);
break;
}

case MessageOp.Opened: {
const info = data[type];
setDocInfo(o => ({ ...o, ...info }));
setState(State.Opened);
break;
}

case MessageOp.Rendered: {
const { index, kind, imageData } = data[type];
render(index, kind, imageData);
break;
}
}
for (let i = startIndex; i <= endIndex; i++) {
channel.post(MessageOp.Render, {
index: i,
scale,
kind: RenderKind.Page,
});
}
}, [viewportInfo, mainVisibleRange, channel]);

workerRef.current.addEventListener('message', event => {
process(event).catch(console.error);
});
useEffect(() => {
if (collapsed) return;
if (!channel) return;

return () => {
workerRef.current?.terminate();
};
}, [model, post, render]);
const { startIndex, endIndex } = thumbnailsVisibleRange;
const scale = (THUMBNAIL_WIDTH / docInfo.width) * viewportInfo.dpi;

useEffect(() => {
if (!model.sourceId) return;
if (state !== State.Connected) return;

getAttachmentBlob(model)
.then(blob => {
if (!blob) return;
return blob.arrayBuffer();
})
.then(buffer => {
if (!buffer) return;
setState(State.Opening);
post(MessageOp.Open, buffer, [buffer]);
})
.catch(showBoundary);
}, [showBoundary, state, post, model, docInfo]);
for (let i = startIndex; i <= endIndex; i++) {
channel.post(MessageOp.Render, {
index: i,
scale,
kind: RenderKind.Thumbnail,
});
}
}, [collapsed, docInfo, viewportInfo, thumbnailsVisibleRange, channel]);

const pageContent = useCallback(
(index: number) => {
Expand Down Expand Up @@ -348,10 +296,7 @@ export const Viewer = ({ model }: ViewerProps): ReactElement => {
const size = Math.min(5, docInfo.total);
const itemHeight = docInfo.height + 20;
const height = Math.ceil(size * itemHeight);
return {
top: height,
bottom: height,
};
return { top: height, bottom: height };
}, [docInfo]);

const mainStyle = useMemo(() => {
Expand All @@ -361,11 +306,47 @@ export const Viewer = ({ model }: ViewerProps): ReactElement => {
vh - 60 - 24 - 24 - 2 - 8,
t * THUMBNAIL_WIDTH * (h / w) + (t - 1) * 12
);
return {
height: `${height}px`,
};
return { height: `${height}px` };
}, [docInfo, viewportInfo]);

useLayoutEffect(() => {
const { worker, release } = service.get(model.id);

setWorker(worker);

const channel = worker.channel();
channel
.on(({ index, kind, imageData }) => render(index, kind, imageData))
.start();

setChannel(channel);

const disposables = worker.on({
ready: () => {
if (worker.docInfo$.value.total) {
return;
}

getAttachmentBlob(model)
.then(blob => {
if (!blob) return;
return blob.arrayBuffer();
})
.then(buffer => {
if (!buffer) return;
worker.open(buffer);
})
.catch(showBoundary);
},
});

return () => {
channel.dispose();
disposables[Symbol.dispose]();
release();
};
}, [showBoundary, render, service, model]);

return (
<div
data-testid="attachment-viewer"
Expand Down

This file was deleted.

Loading
Loading