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 viewer supports fit to page #8812

Open
wants to merge 1 commit into
base: fundon/11_02_pdf_embed_view
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
PDFService,
PDFStatus,
} from '@affine/core/modules/pdf';
import type { PDFMeta } from '@affine/core/modules/pdf/renderer';
import type { PageSize } from '@affine/core/modules/pdf/renderer/types';
import { LoadingSvg, PDFPageCanvas } from '@affine/core/modules/pdf/views';
import { PeekViewService } from '@affine/core/modules/peek-view';
import { stopPropagation } from '@affine/core/utils';
Expand All @@ -30,9 +32,18 @@ import type { PDFViewerProps } from './pdf-viewer';
import * as styles from './styles.css';
import * as embeddedStyles from './styles.embedded.css';

function defaultMeta() {
return {
pageCount: 0,
pageSizes: [],
maxSize: { width: 0, height: 0 },
};
}

type PDFViewerEmbeddedInnerProps = PDFViewerProps;

export function PDFViewerEmbeddedInner({ model }: PDFViewerEmbeddedInnerProps) {
const scale = window.devicePixelRatio;
const peekView = useService(PeekViewService).peekView;
const pdfService = useService(PDFService);
const [pdfEntity, setPdfEntity] = useState<{
Expand All @@ -43,28 +54,25 @@ export function PDFViewerEmbeddedInner({ model }: PDFViewerEmbeddedInnerProps) {
page: PDFPage;
release: () => void;
} | null>(null);
const [pageSize, setPageSize] = useState<PageSize | null>(null);

const meta = useLiveData(
useMemo(() => {
return pdfEntity
? pdfEntity.pdf.state$.map(s => {
return s.status === PDFStatus.Opened
? s.meta
: { pageCount: 0, width: 0, height: 0 };
return s.status === PDFStatus.Opened ? s.meta : defaultMeta();
})
: new LiveData({ pageCount: 0, width: 0, height: 0 });
: new LiveData<PDFMeta>(defaultMeta());
}, [pdfEntity])
);
const img = useLiveData(
useMemo(() => {
return pageEntity ? pageEntity.page.bitmap$ : null;
}, [pageEntity])
useMemo(() => (pageEntity ? pageEntity.page.bitmap$ : null), [pageEntity])
);

const [isLoading, setIsLoading] = useState(true);
const [cursor, setCursor] = useState(0);
const viewerRef = useRef<HTMLDivElement>(null);
const [isLoading, setIsLoading] = useState(true);
const [visibility, setVisibility] = useState(false);
const viewerRef = useRef<HTMLDivElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);

const peek = useCallback(() => {
Expand Down Expand Up @@ -107,47 +115,51 @@ export function PDFViewerEmbeddedInner({ model }: PDFViewerEmbeddedInnerProps) {
if (!img) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const { width, height } = meta;
if (width * height === 0) return;

setIsLoading(false);

canvas.width = width * 2;
canvas.height = height * 2;
canvas.width = img.width;
canvas.height = img.height;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0);
}, [img, meta]);
}, [img]);

useEffect(() => {
if (!visibility) return;
if (!pageEntity) return;
if (!pageSize) return;

const { width, height } = meta;
if (width * height === 0) return;
const { width, height } = pageSize;

pageEntity.page.render({ width, height, scale: 2 });
pageEntity.page.render({ width, height, scale });

return () => {
pageEntity.page.render.unsubscribe();
};
}, [visibility, pageEntity, meta]);
}, [visibility, pageEntity, pageSize, scale]);

useEffect(() => {
if (!visibility) return;
if (!pdfEntity) return;

const { width, height } = meta;
if (width * height === 0) return;
const size = meta.pageSizes[cursor];
if (!size) return;

const pageEntity = pdfEntity.pdf.page(cursor, `${width}:${height}:2`);
const { width, height } = size;
const pageEntity = pdfEntity.pdf.page(
cursor,
`${width}:${height}:${scale}`
);

setPageEntity(pageEntity);
setPageSize(size);

return () => {
pageEntity.release();
setPageSize(null);
setPageEntity(null);
};
}, [visibility, pdfEntity, cursor, meta]);
}, [visibility, pdfEntity, cursor, meta, scale]);

useEffect(() => {
if (!visibility) return;
Expand Down Expand Up @@ -191,7 +203,7 @@ export function PDFViewerEmbeddedInner({ model }: PDFViewerEmbeddedInnerProps) {
justifyContent: 'center',
alignItems: 'center',
width: '100%',
minHeight: '759px',
minHeight: '253px',
}}
>
<PDFPageCanvas ref={canvasRef} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
} from 'react-virtuoso';

import * as styles from './styles.css';
import { calculatePageNum } from './utils';
import { calculatePageNum, fitToPage } from './utils';

const THUMBNAIL_WIDTH = 94;

Expand Down Expand Up @@ -81,17 +81,27 @@ export const PDFViewerInner = ({ pdf, state }: PDFViewerInnerProps) => {
(
index: number,
_: unknown,
{ width, height, onPageSelect, pageClassName }: PDFVirtuosoContext
{
viewportInfo,
meta,
onPageSelect,
pageClassName,
resize,
isThumbnail,
}: PDFVirtuosoContext
) => {
return (
<PDFPageRenderer
key={index}
key={`${pageClassName}-${index}`}
pdf={pdf}
width={width}
height={height}
pageNum={index}
onSelect={onPageSelect}
className={pageClassName}
viewportInfo={viewportInfo}
actualSize={meta.pageSizes[index]}
maxSize={meta.maxSize}
onSelect={onPageSelect}
resize={resize}
isThumbnail={isThumbnail}
/>
);
},
Expand All @@ -100,22 +110,47 @@ export const PDFViewerInner = ({ pdf, state }: PDFViewerInnerProps) => {

const thumbnailsConfig = useMemo(() => {
const { height: vh } = viewportInfo;
const { pageCount: t, height: h, width: w } = state.meta;
const p = h / (w || 1);
const pw = THUMBNAIL_WIDTH;
const ph = Math.ceil(pw * p);
const height = Math.min(vh - 60 - 24 - 24 - 2 - 8, t * ph + (t - 1) * 12);
const { pageCount, pageSizes, maxSize } = state.meta;
const t = Math.min(maxSize.width / maxSize.height, 1);
const pw = THUMBNAIL_WIDTH / t;
const newMaxSize = {
width: pw,
height: pw * (maxSize.height / maxSize.width),
};
const newPageSizes = pageSizes.map(({ width, height }) => {
const w = newMaxSize.width * (width / maxSize.width);
return {
width: w,
height: w * (height / width),
};
});
const height = Math.min(
vh - 60 - 24 - 24 - 2 - 8,
newPageSizes.reduce((h, { height }) => h + height * t, 0) +
(pageCount - 1) * 12
);
return {
context: {
width: pw,
height: ph,
onPageSelect,
viewportInfo: {
width: pw,
height,
},
meta: {
pageCount,
maxSize: newMaxSize,
pageSizes: newPageSizes,
},
resize: fitToPage,
isThumbnail: true,
pageClassName: styles.pdfThumbnail,
},
style: { height },
};
}, [state, viewportInfo, onPageSelect]);

// 1. works fine if they are the same size
// 2. uses the `observeIntersection` when targeting different sizes
const scrollSeekConfig = useMemo<ScrollSeekConfiguration>(() => {
return {
enter: velocity => Math.abs(velocity) > 1024,
Expand Down Expand Up @@ -154,8 +189,12 @@ export const PDFViewerInner = ({ pdf, state }: PDFViewerInnerProps) => {
ScrollSeekPlaceholder,
}}
context={{
width: state.meta.width,
height: state.meta.height,
viewportInfo: {
width: viewportInfo.width - 40,
height: viewportInfo.height - 40,
},
meta: state.meta,
resize: fitToPage,
pageClassName: styles.pdfPage,
}}
scrollSeekConfiguration={scrollSeekConfig}
Expand All @@ -174,9 +213,9 @@ export const PDFViewerInner = ({ pdf, state }: PDFViewerInnerProps) => {
Scroller,
ScrollSeekPlaceholder,
}}
scrollSeekConfiguration={scrollSeekConfig}
style={thumbnailsConfig.style}
context={thumbnailsConfig.context}
scrollSeekConfiguration={scrollSeekConfig}
/>
</div>
<div className={clsx(['indicator', styles.pdfIndicator])}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Loading } from '@affine/component';
import { type PDF, PDFService, PDFStatus } from '@affine/core/modules/pdf';
import { LoadingSvg } from '@affine/core/modules/pdf/views';
import type { AttachmentBlockModel } from '@blocksuite/affine/blocks';
import { useLiveData, useService } from '@toeverything/infra';
import { useEffect, useState } from 'react';
Expand All @@ -10,7 +10,7 @@ function PDFViewerStatus({ pdf, ...props }: PDFViewerProps & { pdf: PDF }) {
const state = useLiveData(pdf.state$);

if (state?.status !== PDFStatus.Opened) {
return <LoadingSvg />;
return <PDFLoading />;
}

return <PDFViewerInner {...props} pdf={pdf} state={state} />;
Expand All @@ -31,12 +31,20 @@ export function PDFViewer({ model, ...props }: PDFViewerProps) {
const { pdf, release } = pdfService.get(model);
setPdf(pdf);

return release;
return () => {
release();
};
}, [model, pdfService, setPdf]);

if (!pdf) {
return <LoadingSvg />;
return <PDFLoading />;
}

return <PDFViewerStatus {...props} model={model} pdf={pdf} />;
}

const PDFLoading = () => (
<div style={{ margin: 'auto' }}>
<Loading />
</div>
);
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ export const pdfPage = style({
'0px 4px 20px 0px var(--transparent-black-200, rgba(0, 0, 0, 0.10))',
overflow: 'hidden',
maxHeight: 'max-content',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
});

export const pdfThumbnails = style({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export const pdfContainer = style({
background: cssVar('--affine-background-primary-color'),
userSelect: 'none',
contentVisibility: 'visible',
display: 'flex',
minHeight: 'fit-content',
height: '100%',
flexDirection: 'column',
justifyContent: 'space-between',
});

export const pdfViewer = style({
Expand All @@ -21,6 +26,7 @@ export const pdfViewer = style({
padding: '12px',
overflow: 'hidden',
background: cssVarV2('layer/background/secondary'),
flex: 1,
});

export const pdfPlaceholder = style({
Expand Down
27 changes: 27 additions & 0 deletions packages/frontend/core/src/components/attachment-viewer/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { PageSize } from '@affine/core/modules/pdf/renderer/types';
import type { AttachmentBlockModel } from '@blocksuite/affine/blocks';
import { filesize } from 'filesize';

Expand Down Expand Up @@ -46,3 +47,29 @@ export function calculatePageNum(el: HTMLElement, pageCount: number) {
const cursor = Math.min(index, pageCount - 1);
return cursor;
}

export function fitToPage(
viewportInfo: PageSize,
actualSize: PageSize,
maxSize: PageSize,
isThumbnail?: boolean
) {
const { width: vw, height: vh } = viewportInfo;
const { width: w, height: h } = actualSize;
const { width: mw, height: mh } = maxSize;
let width = 0;
let height = 0;
if (h / w > vh / vw) {
height = vh * (h / mh);
width = (w / h) * height;
} else {
const t = isThumbnail ? Math.min(w / h, 1) : w / mw;
width = vw * t;
height = (h / w) * width;
}
return {
width: Math.ceil(width),
height: Math.ceil(height),
aspectRatio: width / height,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,8 @@ export const useSharingUrl = ({ workspaceId, pageId }: UseSharingUrl) => {
if (sharingUrl) {
copyTextToClipboard(sharingUrl)
.then(success => {
if (success) {
notify.success({ title: t['Copied link to clipboard']() });
}
if (!success) return;
notify.success({ title: t['Copied link to clipboard']() });
})
.catch(err => {
console.error(err);
Expand Down
5 changes: 3 additions & 2 deletions packages/frontend/core/src/modules/pdf/entities/pdf-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
LiveData,
mapInto,
} from '@toeverything/infra';
import { map, switchMap } from 'rxjs';
import { filter, map, switchMap } from 'rxjs';

import type { RenderPageOpts } from '../renderer';
import type { PDF } from './pdf';
Expand All @@ -25,7 +25,8 @@ export class PDFPage extends Entity<{ pdf: PDF; pageNum: number }> {
pageNum: this.pageNum,
})
),
map(data => data.bitmap),
map(data => data?.bitmap),
filter(Boolean),
mapInto(this.bitmap$),
catchErrorInto(this.error$, error => {
logger.error('Failed to render page', error);
Expand Down
Loading
Loading