diff --git a/packages/layout/src/steps/resolvePagination.js b/packages/layout/src/steps/resolvePagination.js index 98d6f3135..b3efe7f1a 100644 --- a/packages/layout/src/steps/resolvePagination.js +++ b/packages/layout/src/steps/resolvePagination.js @@ -3,7 +3,7 @@ /* eslint-disable prefer-destructuring */ import * as P from '@react-pdf/primitives'; -import { isNil, omit, compose } from '@react-pdf/fns'; +import { isNil, omit, asyncCompose } from '@react-pdf/fns'; import isFixed from '../node/isFixed'; import splitText from '../text/splitText'; @@ -17,6 +17,7 @@ import resolveTextLayout from './resolveTextLayout'; import resolveInheritance from './resolveInheritance'; import { resolvePageDimensions } from './resolveDimensions'; import { resolvePageStyles } from './resolveStyles'; +import resolveAssets from './resolveAssets'; const isText = (node) => node.type === P.Text; @@ -32,7 +33,8 @@ const allFixed = (nodes) => nodes.every(isFixed); const isDynamic = (node) => !isNil(node.props?.render); -const relayoutPage = compose( +const relayoutPage = asyncCompose( + resolveAssets, resolveTextLayout, resolvePageDimensions, resolveInheritance, @@ -175,19 +177,20 @@ const resolveDynamicNodes = (props, node) => { return Object.assign({}, node, { box, lines, children }); }; -const resolveDynamicPage = (props, page, fontStore, yoga) => { +const resolveDynamicPage = async (props, page, fontStore, yoga) => { if (shouldResolveDynamicNodes(page)) { const resolvedPage = resolveDynamicNodes(props, page); - return relayoutPage(resolvedPage, fontStore, yoga); + const relayoutedPage = await relayoutPage(resolvedPage, fontStore, yoga); + return relayoutedPage; } return page; }; -const splitPage = (page, pageNumber, fontStore, yoga) => { +const splitPage = async (page, pageNumber, fontStore, yoga) => { const wrapArea = getWrapArea(page); const contentArea = getContentArea(page); - const dynamicPage = resolveDynamicPage({ pageNumber }, page, fontStore, yoga); + const dynamicPage = await resolveDynamicPage({ pageNumber }, page, fontStore, yoga); const height = page.style.height; const [currentChilds, nextChilds] = splitNodes( @@ -196,10 +199,10 @@ const splitPage = (page, pageNumber, fontStore, yoga) => { dynamicPage.children, ); - const relayout = (node) => relayoutPage(node, fontStore, yoga); + const relayout = async node => relayoutPage(node, fontStore, yoga); const currentBox = { ...page.box, height }; - const currentPage = relayout( + const currentPage = await relayout( Object.assign({}, page, { box: currentBox, children: currentChilds }), ); @@ -209,7 +212,7 @@ const splitPage = (page, pageNumber, fontStore, yoga) => { const nextBox = omit('height', page.box); const nextProps = omit('bookmark', page.props); - const nextPage = relayout( + const nextPage = await relayout( Object.assign({}, page, { props: nextProps, box: nextBox, @@ -220,7 +223,7 @@ const splitPage = (page, pageNumber, fontStore, yoga) => { return [currentPage, nextPage]; }; -const resolvePageIndices = (fontStore, yoga, page, pageNumber, pages) => { +const resolvePageIndices = async (fontStore, yoga, page, pageNumber, pages) => { const totalPages = pages.length; const props = { @@ -233,24 +236,25 @@ const resolvePageIndices = (fontStore, yoga, page, pageNumber, pages) => { return resolveDynamicPage(props, page, fontStore, yoga); }; -const assocSubPageData = (subpages) => { +const assocSubPageData = (subpages, pageIndex) => { return subpages.map((page, i) => ({ ...page, + pageIndex, subPageNumber: i, subPageTotalPages: subpages.length, })); }; -const dissocSubPageData = (page) => { - return omit(['subPageNumber', 'subPageTotalPages'], page); +const dissocSubPageData = page => { + return omit(['pageIndex', 'subPageNumber', 'subPageTotalPages'], page); }; -const paginate = (page, pageNumber, fontStore, yoga) => { +const paginate = async (page, pageNumber, fontStore, yoga) => { if (!page) return []; if (page.props?.wrap === false) return [page]; - let splittedPage = splitPage(page, pageNumber, fontStore, yoga); + let splittedPage = await splitPage(page, pageNumber, fontStore, yoga); const pages = [splittedPage[0]]; let nextPage = splittedPage[1]; @@ -271,28 +275,40 @@ const paginate = (page, pageNumber, fontStore, yoga) => { }; /** - * Performs pagination. This is the step responsible of breaking the whole document - * into pages following pagiation rules, such as `fixed`, `break` and dynamic nodes. + * Performs pagination. This is the step responsible for breaking the whole document + * into pages following pagination rules, such as `fixed`, `break` and dynamic nodes. * * @param {Object} doc node * @param {Object} fontStore font store * @returns {Object} layout node */ -const resolvePagination = (doc, fontStore) => { +const resolvePagination = async (doc, fontStore) => { let pages = []; let pageNumber = 1; - for (let i = 0; i < doc.children.length; i += 1) { - const page = doc.children[i]; - let subpages = paginate(page, pageNumber, fontStore, doc.yoga); + await Promise.all( + doc.children.map(async (page, pageIndex) => { + let subpages = await paginate(page, pageNumber, fontStore, doc.yoga); - subpages = assocSubPageData(subpages); - pageNumber += subpages.length; - pages = pages.concat(subpages); - } + subpages = assocSubPageData(subpages, pageIndex); + pageNumber += subpages.length; + pages.push(...subpages); + }), + ); + + // because the subpages are pushed into the array according to the speed they are paginated, + // we sort them by their initial index, while keeping the subpages order. + pages.sort((a, b) => { + if (a.pageIndex !== b.pageIndex) { + return a.pageIndex - b.pageIndex; + } + return a.subPageNumber - b.subPageNumber; + }); - pages = pages.map((...args) => - dissocSubPageData(resolvePageIndices(fontStore, doc.yoga, ...args)), + pages = await Promise.all( + pages.map(async (...args) => + dissocSubPageData(await resolvePageIndices(fontStore, doc.yoga, ...args)), + ), ); return assingChildren(pages, doc); diff --git a/packages/layout/tests/steps/resolvePagination.test.js b/packages/layout/tests/steps/resolvePagination.test.jsx similarity index 58% rename from packages/layout/tests/steps/resolvePagination.test.js rename to packages/layout/tests/steps/resolvePagination.test.jsx index f84ba7215..dafc7d8fe 100644 --- a/packages/layout/tests/steps/resolvePagination.test.js +++ b/packages/layout/tests/steps/resolvePagination.test.jsx @@ -1,9 +1,9 @@ import { describe, expect, test } from 'vitest'; - -import { loadYoga } from '../../src/yoga'; +import { Text } from '@react-pdf/primitives'; import resolvePagination from '../../src/steps/resolvePagination'; import resolveDimensions from '../../src/steps/resolveDimensions'; +import { loadYoga } from '../../src/yoga'; // dimensions is required by pagination step and them are calculated here const calcLayout = (node) => resolvePagination(resolveDimensions(node)); @@ -53,7 +53,7 @@ describe('pagination step', () => { ], }; - const layout = calcLayout(root); + const layout = await calcLayout(root); const page = layout.children[0]; const view = layout.children[0].children[0]; @@ -103,7 +103,7 @@ describe('pagination step', () => { ], }; - const layout = calcLayout(root); + const layout = await calcLayout(root); const view1 = layout.children[0].children[0]; const view2 = layout.children[1].children[0]; @@ -140,7 +140,7 @@ describe('pagination step', () => { ], }; - const layout = calcLayout(root); + const layout = await calcLayout(root); const view1 = layout.children[0].children[0]; const view2 = layout.children[1].children[0]; @@ -151,6 +151,161 @@ describe('pagination step', () => { expect(view3.box.height).toBe(10); }); + test('should calculate height and keep the page order', async () => { + const yoga = await loadYoga(); + + const root = { + type: 'DOCUMENT', + yoga, + children: [ + { + type: 'PAGE', + box: {}, + style: { + width: 5, + height: 60, + }, + children: [ + { + type: 'VIEW', + box: {}, + style: { height: 18 }, + props: { fixed: true }, + children: [], + }, + { + type: 'VIEW', + box: {}, + style: { height: 30 }, + props: {}, + children: [], + }, + { + type: 'VIEW', + box: {}, + style: { height: 57 }, + props: {}, + children: [], + }, + { + type: 'VIEW', + box: {}, + style: { height: 15 }, + props: {}, + children: [], + }, + ], + }, + { + type: 'PAGE', + box: {}, + style: { + height: 50, + }, + children: [ + { + type: 'VIEW', + box: {}, + style: {}, + props: { + fixed: true, + render: () => rear window, + }, + children: [], + }, + { + type: 'VIEW', + box: {}, + style: { height: 22 }, + props: {}, + children: [], + }, + ], + }, + { + type: 'PAGE', + box: {}, + style: { + height: 40, + }, + children: [ + { + type: 'VIEW', + box: {}, + style: { height: 12 }, + props: {}, + children: [], + }, + ], + }, + { + type: 'PAGE', + box: {}, + style: { + height: 30, + }, + children: [ + { + type: 'VIEW', + box: {}, + style: {}, + props: {}, + children: [], + }, + ], + }, + ], + }; + + const layout = await calcLayout(root); + + const page1 = layout.children[0]; + const [view1, view2, view3] = page1.children; + + const page2 = layout.children[1]; + const [view4, view5] = page2.children; + + const page3 = layout.children[2]; + const [view6, view7, view8] = page3.children; + + const page4 = layout.children[3]; + const [view9, view10] = page4.children; + + const page5 = layout.children[4]; + const [view11] = page5.children; + + const page6 = layout.children[5]; + + // page 1 + expect(view1.box.height).toBe(18); // fixed header + expect(view2.box.height).toBe(30); + expect(view3.box.height).toBe(12); + expect(page1.box.height).toBe(60); + + // page 2 + expect(view4.box.height).toBe(18); // fixed header + expect(view5.box.height).toBe(42); + expect(page2.box.height).toBe(60); + + // page 3 + expect(view6.box.height).toBe(18); // fixed header + expect(view7.box.height).toBe(3); + expect(view8.box.height).toBe(15); + expect(page3.box.height).toBe(60); + + // page 4 + expect(view9.box.height).toBe(10); + expect(view10.box.height).toBe(22); + expect(page4.box.height).toBe(50); + + // page 5 + expect(page5.box.height).toBe(40); + expect(view11.box.height).toBe(12); + + // page 6 + expect(page6.box.height).toBe(30); + }); + test('should not wrap page with false wrap prop', async () => { const yoga = await loadYoga(); @@ -181,7 +336,7 @@ describe('pagination step', () => { ], }; - const layout = calcLayout(root); + const layout = await calcLayout(root); expect(layout.children.length).toBe(1); }); @@ -238,7 +393,7 @@ describe('pagination step', () => { ], }; - const layout = calcLayout(root); + const layout = await calcLayout(root); const page1 = layout.children[0]; const page2 = layout.children[1]; diff --git a/packages/renderer/tests/dynamicContent.test.jsx b/packages/renderer/tests/dynamicContent.test.jsx new file mode 100644 index 000000000..1e9c5e8e2 --- /dev/null +++ b/packages/renderer/tests/dynamicContent.test.jsx @@ -0,0 +1,28 @@ +import { describe, expect, test } from 'vitest'; + +import { Document, Image, Page, View } from '@react-pdf/renderer'; +import renderToImage from './renderComponent'; + +const mount = async children => { + const image = await renderToImage( + + {children} + , + ); + + return image; +}; + +describe('dynamic content', () => { + test('should render an image', async () => { + const url = + 'https://user-images.githubusercontent.com/5600341/27505816-c8bc37aa-587f-11e7-9a86-08a2d081a8b9.png'; + const image = await mount( + } + />, + ); + + expect(image).toMatchImageSnapshot(); + }, 10000); +}); diff --git a/packages/renderer/tests/snapshots/dynamic-content-test-jsx-tests-dynamic-content-test-jsx-dynamic-content-should-render-an-image-1-snap.png b/packages/renderer/tests/snapshots/dynamic-content-test-jsx-tests-dynamic-content-test-jsx-dynamic-content-should-render-an-image-1-snap.png new file mode 100644 index 000000000..dbb477f1f Binary files /dev/null and b/packages/renderer/tests/snapshots/dynamic-content-test-jsx-tests-dynamic-content-test-jsx-dynamic-content-should-render-an-image-1-snap.png differ