diff --git a/packages/frontend/core/src/components/attachment-viewer/pdf-viewer-embedded-inner.tsx b/packages/frontend/core/src/components/attachment-viewer/pdf-viewer-embedded-inner.tsx index 7948f1b0b477f..660b13dcb39db 100644 --- a/packages/frontend/core/src/components/attachment-viewer/pdf-viewer-embedded-inner.tsx +++ b/packages/frontend/core/src/components/attachment-viewer/pdf-viewer-embedded-inner.tsx @@ -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'; @@ -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<{ @@ -43,28 +54,25 @@ export function PDFViewerEmbeddedInner({ model }: PDFViewerEmbeddedInnerProps) { page: PDFPage; release: () => void; } | null>(null); + const [pageSize, setPageSize] = useState(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(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(null); + const [isLoading, setIsLoading] = useState(true); const [visibility, setVisibility] = useState(false); + const viewerRef = useRef(null); const canvasRef = useRef(null); const peek = useCallback(() => { @@ -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; @@ -191,7 +203,7 @@ export function PDFViewerEmbeddedInner({ model }: PDFViewerEmbeddedInnerProps) { justifyContent: 'center', alignItems: 'center', width: '100%', - minHeight: '759px', + minHeight: '253px', }} > diff --git a/packages/frontend/core/src/components/attachment-viewer/pdf-viewer-inner.tsx b/packages/frontend/core/src/components/attachment-viewer/pdf-viewer-inner.tsx index ac2eac56b1480..554efc52ce6f4 100644 --- a/packages/frontend/core/src/components/attachment-viewer/pdf-viewer-inner.tsx +++ b/packages/frontend/core/src/components/attachment-viewer/pdf-viewer-inner.tsx @@ -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; @@ -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 ( ); }, @@ -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(() => { return { enter: velocity => Math.abs(velocity) > 1024, @@ -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} @@ -174,9 +213,9 @@ export const PDFViewerInner = ({ pdf, state }: PDFViewerInnerProps) => { Scroller, ScrollSeekPlaceholder, }} - scrollSeekConfiguration={scrollSeekConfig} style={thumbnailsConfig.style} context={thumbnailsConfig.context} + scrollSeekConfiguration={scrollSeekConfig} />
diff --git a/packages/frontend/core/src/components/attachment-viewer/pdf-viewer.tsx b/packages/frontend/core/src/components/attachment-viewer/pdf-viewer.tsx index c93292162c6c1..2eb508ef6ab34 100644 --- a/packages/frontend/core/src/components/attachment-viewer/pdf-viewer.tsx +++ b/packages/frontend/core/src/components/attachment-viewer/pdf-viewer.tsx @@ -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'; @@ -10,7 +10,7 @@ function PDFViewerStatus({ pdf, ...props }: PDFViewerProps & { pdf: PDF }) { const state = useLiveData(pdf.state$); if (state?.status !== PDFStatus.Opened) { - return ; + return ; } return ; @@ -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 ; + return ; } return ; } + +const PDFLoading = () => ( +
+ +
+); diff --git a/packages/frontend/core/src/components/attachment-viewer/styles.css.ts b/packages/frontend/core/src/components/attachment-viewer/styles.css.ts index 1e85631d4e6c5..bdd269ae32c66 100644 --- a/packages/frontend/core/src/components/attachment-viewer/styles.css.ts +++ b/packages/frontend/core/src/components/attachment-viewer/styles.css.ts @@ -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({ diff --git a/packages/frontend/core/src/components/attachment-viewer/styles.embedded.css.ts b/packages/frontend/core/src/components/attachment-viewer/styles.embedded.css.ts index 06d2332c33fdf..6a328b63f839a 100644 --- a/packages/frontend/core/src/components/attachment-viewer/styles.embedded.css.ts +++ b/packages/frontend/core/src/components/attachment-viewer/styles.embedded.css.ts @@ -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({ @@ -21,6 +26,7 @@ export const pdfViewer = style({ padding: '12px', overflow: 'hidden', background: cssVarV2('layer/background/secondary'), + flex: 1, }); export const pdfPlaceholder = style({ diff --git a/packages/frontend/core/src/components/attachment-viewer/utils.ts b/packages/frontend/core/src/components/attachment-viewer/utils.ts index b2cb707fef6a5..445dbf2ece5c6 100644 --- a/packages/frontend/core/src/components/attachment-viewer/utils.ts +++ b/packages/frontend/core/src/components/attachment-viewer/utils.ts @@ -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'; @@ -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, + }; +} diff --git a/packages/frontend/core/src/modules/pdf/entities/pdf-page.ts b/packages/frontend/core/src/modules/pdf/entities/pdf-page.ts index b1f91b14ea172..ab71a03264530 100644 --- a/packages/frontend/core/src/modules/pdf/entities/pdf-page.ts +++ b/packages/frontend/core/src/modules/pdf/entities/pdf-page.ts @@ -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'; @@ -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); diff --git a/packages/frontend/core/src/modules/pdf/renderer/types.ts b/packages/frontend/core/src/modules/pdf/renderer/types.ts index 3e79550a1d80f..6ade529ce669e 100644 --- a/packages/frontend/core/src/modules/pdf/renderer/types.ts +++ b/packages/frontend/core/src/modules/pdf/renderer/types.ts @@ -1,16 +1,23 @@ -export type PDFMeta = { - pageCount: number; +export type PageSize = { width: number; height: number; }; +export type PDFMeta = { + pageCount: number; + maxSize: PageSize; + pageSizes: PageSize[]; +}; + +export type PageSizeOpts = { + pageNum: number; +}; + export type RenderPageOpts = { pageNum: number; - width: number; - height: number; scale?: number; -}; +} & PageSize; -export type RenderedPage = RenderPageOpts & { +export type RenderedPage = { bitmap: ImageBitmap; }; diff --git a/packages/frontend/core/src/modules/pdf/renderer/worker.ts b/packages/frontend/core/src/modules/pdf/renderer/worker.ts index cd5f9351f940b..14dd1987a703c 100644 --- a/packages/frontend/core/src/modules/pdf/renderer/worker.ts +++ b/packages/frontend/core/src/modules/pdf/renderer/worker.ts @@ -14,6 +14,7 @@ import { map, Observable, ReplaySubject, + retry, share, switchMap, } from 'rxjs'; @@ -32,7 +33,7 @@ class PDFRendererBackend extends OpConsumer { private readonly doc$ = this.binary$.pipe( filter(Boolean), - combineLatestWith(this.viewer$), + combineLatestWith(this.viewer$.pipe(retry(1))), switchMap(([buffer, viewer]) => { return new Observable(observer => { const doc = viewer.open(buffer); @@ -45,7 +46,9 @@ class PDFRendererBackend extends OpConsumer { observer.next(doc); return () => { - doc.close(); + setTimeout(() => { + doc.close(); + }, 1000); // Waits for ObjectPool GC }; }); }), @@ -60,15 +63,32 @@ class PDFRendererBackend extends OpConsumer { throw new Error('Document not opened'); } - const firstPage = doc.page(0); - if (!firstPage) { - throw new Error('Document has no pages'); + const pageCount = doc.pageCount(); + const pageSizes = []; + let i = 0; + let maxWidth = 0; + let maxHeight = 0; + + for (; i < pageCount; i++) { + const page = doc.page(i); + if (!page) { + throw new Error('Page not found'); + } + const size = page.size(); + const width = Math.ceil(size.width); + const height = Math.ceil(size.height); + + maxWidth = Math.max(maxWidth, width); + maxHeight = Math.max(maxHeight, height); + + pageSizes.push({ width, height }); + page.close(); } return { - pageCount: doc.pageCount(), - width: firstPage.width(), - height: firstPage.height(), + pageCount, + pageSizes, + maxSize: { width: maxWidth, height: maxHeight }, }; }) ); @@ -100,7 +120,6 @@ class PDFRendererBackend extends OpConsumer { async renderPage(viewer: Viewer, doc: Document, opts: RenderPageOpts) { const page = doc.page(opts.pageNum); - if (!page) return; const scale = opts.scale ?? 1; diff --git a/packages/frontend/core/src/modules/pdf/views/components.tsx b/packages/frontend/core/src/modules/pdf/views/components.tsx index c7af3e29ca89b..e5aa7e2b6865e 100644 --- a/packages/frontend/core/src/modules/pdf/views/components.tsx +++ b/packages/frontend/core/src/modules/pdf/views/components.tsx @@ -3,11 +3,20 @@ import clsx from 'clsx'; import { type CSSProperties, forwardRef, memo } from 'react'; import type { ScrollSeekPlaceholderProps, VirtuosoProps } from 'react-virtuoso'; +import type { PDFMeta } from '../renderer'; +import type { PageSize } from '../renderer/types'; import * as styles from './styles.css'; export type PDFVirtuosoContext = { - width: number; - height: number; + viewportInfo: PageSize; + meta: PDFMeta; + resize: ( + viewportInfo: PageSize, + actualSize: PageSize, + maxSize: PageSize, + isThumbnail?: boolean + ) => { aspectRatio: number } & PageSize; + isThumbnail?: boolean; pageClassName?: string; onPageSelect?: (index: number) => void; }; @@ -32,16 +41,29 @@ export const ScrollSeekPlaceholder = forwardRef< ScrollSeekPlaceholderProps & { context?: PDFVirtuosoContext; } ->(({ context }, ref) => { +>(({ context, index }, ref) => { const className = context?.pageClassName; - const width = context?.width ?? 537; - const height = context?.height ?? 759; - const style = { width, aspectRatio: `${width} / ${height}` }; + const isThumbnail = context?.isThumbnail; + const size = context?.meta.pageSizes[index]; + const maxSize = context?.meta.maxSize; + const height = size?.height ?? 759; + const style = + context?.viewportInfo && size && maxSize + ? context.resize(context.viewportInfo, size, maxSize, isThumbnail) + : undefined; return ( -
- -
+ +
+ +
+
); }); @@ -114,8 +136,18 @@ export const LoadingSvg = memo( LoadingSvg.displayName = 'pdf-loading'; -export const PDFPageCanvas = forwardRef((props, ref) => { - return ; +export const PDFPageCanvas = forwardRef< + HTMLCanvasElement, + { style?: CSSProperties } +>(({ style, ...props }, ref) => { + return ( + + ); }); PDFPageCanvas.displayName = 'pdf-page-canvas'; diff --git a/packages/frontend/core/src/modules/pdf/views/page-renderer.tsx b/packages/frontend/core/src/modules/pdf/views/page-renderer.tsx index e9ad029df53f1..8ff27de510eb3 100644 --- a/packages/frontend/core/src/modules/pdf/views/page-renderer.tsx +++ b/packages/frontend/core/src/modules/pdf/views/page-renderer.tsx @@ -1,85 +1,164 @@ +import { observeIntersection } from '@affine/component'; import { useI18n } from '@affine/i18n'; import { useLiveData } from '@toeverything/infra'; -import { useEffect, useRef, useState } from 'react'; +import { debounce } from 'lodash-es'; +import { forwardRef, useEffect, useMemo, useRef, useState } from 'react'; import type { PDF } from '../entities/pdf'; import type { PDFPage } from '../entities/pdf-page'; +import type { PageSize } from '../renderer/types'; import { LoadingSvg, PDFPageCanvas } from './components'; import * as styles from './styles.css'; interface PDFPageProps { pdf: PDF; - width: number; - height: number; pageNum: number; + actualSize: PageSize; + maxSize: PageSize; + viewportInfo: PageSize; + resize: ( + viewportInfo: PageSize, + actualSize: PageSize, + maxSize: PageSize, + isThumbnail?: boolean + ) => { aspectRatio: number } & PageSize; scale?: number; className?: string; onSelect?: (pageNum: number) => void; + isThumbnail?: boolean; } export const PDFPageRenderer = ({ pdf, - width, - height, pageNum, className, + actualSize, + maxSize, + viewportInfo, onSelect, + resize, + isThumbnail, scale = window.devicePixelRatio, }: PDFPageProps) => { const t = useI18n(); - const [pdfPage, setPdfPage] = useState(null); + const pageViewRef = useRef(null); const canvasRef = useRef(null); - const img = useLiveData(pdfPage?.bitmap$ ?? null); - const error = useLiveData(pdfPage?.error$ ?? null); - const style = { width, aspectRatio: `${width} / ${height}` }; + const [page, setPage] = useState(null); + const img = useLiveData(useMemo(() => (page ? page.bitmap$ : null), [page])); + const error = useLiveData(page?.error$ ?? null); + const size = useMemo( + () => resize(viewportInfo, actualSize, maxSize, isThumbnail), + [resize, viewportInfo, actualSize, maxSize, isThumbnail] + ); + const [visibility, setVisibility] = useState(false); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + if (!img) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + canvas.width = img.width; + canvas.height = img.height; + ctx.drawImage(img, 0, 0); + }, [img]); useEffect(() => { + if (!visibility) return; + if (!page) return; + + const width = size.width; + const height = size.height; if (width * height === 0) return; - const { page, release } = pdf.page(pageNum, `${width}:${height}:${scale}`); - setPdfPage(page); + page.render({ + width, + height, + scale, + }); - return release; - }, [pdf, width, height, pageNum, scale]); + return () => { + page.render.unsubscribe(); + }; + }, [visibility, page, size, scale]); useEffect(() => { - if (width * height === 0) return; + if (!visibility) return; + if (!pdf) return; + + const width = size.width; + const height = size.height; + const key = `${width}:${height}:${scale}`; + const { page, release } = pdf.page(pageNum, key); - pdfPage?.render({ width, height, scale }); + setPage(page); - return pdfPage?.render.unsubscribe; - }, [pdfPage, width, height, scale]); + return () => { + release(); + setPage(null); + }; + }, [visibility, pdf, pageNum, size, scale]); useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) return; - if (!img) return; - const ctx = canvas.getContext('2d'); - if (!ctx) return; - if (width * height === 0) return; + const pageView = pageViewRef.current; + if (!pageView) return; - canvas.width = width * scale; - canvas.height = height * scale; - ctx.drawImage(img, 0, 0); - }, [img, width, height, scale]); - - if (error) { - return ( -
-

- {t['com.affine.pdf.page.render.error']()} -

-
+ return observeIntersection( + pageView, + debounce( + entry => { + setVisibility(entry.isIntersecting); + }, + 377, + { + trailing: true, + } + ) ); - } + }, []); return (
onSelect?.(pageNum)} > - {img === null ? : } +
); }; + +interface PageRendererInnerProps { + img: ImageBitmap | null; + err: string | null; +} + +const PageRendererInner = forwardRef( + ({ img, err }, ref) => { + if (img) { + return ( + + ); + } + + if (err) { + return

{err}

; + } + + return ; + } +); + +PageRendererInner.displayName = 'pdf-page-renderer-inner'; diff --git a/packages/frontend/core/src/modules/pdf/views/styles.css.ts b/packages/frontend/core/src/modules/pdf/views/styles.css.ts index 6584895a85514..44b53a0607fc7 100644 --- a/packages/frontend/core/src/modules/pdf/views/styles.css.ts +++ b/packages/frontend/core/src/modules/pdf/views/styles.css.ts @@ -10,6 +10,7 @@ export const virtuosoList = style({ flexDirection: 'column', alignItems: 'center', justifyContent: 'center', + minHeight: 'calc(100% - 40px)', gap: '20px', selectors: { '&.small-gap': { @@ -40,15 +41,14 @@ export const pdfPageError = style({ }); export const pdfPageCanvas = style({ - width: '100%', + maxWidth: '100%', }); export const pdfLoading = style({ display: 'flex', alignSelf: 'center', - margin: 'auto', - width: '100%', - height: '100%', - maxWidth: '537px', + width: '179.66px', + height: '253px', + aspectRatio: '539 / 759', overflow: 'hidden', });