diff --git a/.changeset/many-ads-share.md b/.changeset/many-ads-share.md new file mode 100644 index 000000000..9efed0484 --- /dev/null +++ b/.changeset/many-ads-share.md @@ -0,0 +1,5 @@ +--- +'@react-pdf/layout': minor +--- + +fix dynamic image render diff --git a/packages/layout/package.json b/packages/layout/package.json index 05f76b27f..fb83c6362 100644 --- a/packages/layout/package.json +++ b/packages/layout/package.json @@ -35,6 +35,12 @@ "@react-pdf/textkit": "^4.4.1", "@react-pdf/types": "^2.5.0", "cross-fetch": "^3.1.5", + "emoji-regex": "^10.2.1", + "lodash.flatten": "^4.4.0", + "queue": "^6.0.1" + }, + "devDependencies": { + "jest-fetch-mock": "^2.1.1", "emoji-regex": "^10.3.0", "queue": "^6.0.1", "yoga-layout": "^2.0.1" @@ -42,4 +48,4 @@ "files": [ "lib" ] -} +} \ No newline at end of file diff --git a/packages/layout/src/node/createInstances.js b/packages/layout/src/node/createInstances.js index 192608476..81c1ba91b 100644 --- a/packages/layout/src/node/createInstances.js +++ b/packages/layout/src/node/createInstances.js @@ -1,11 +1,16 @@ import { castArray } from '@react-pdf/fns'; -import { TextInstance } from '@react-pdf/primitives'; +import { TextInstance, Image } from '@react-pdf/primitives'; +import flatten from 'lodash.flatten'; + +import fetchImage from '../image/fetchImage'; const isString = (value) => typeof value === 'string'; const isNumber = (value) => typeof value === 'number'; -const isFragment = (value) => +const isImage = value => value.type === Image; + +const isFragment = value => value && value.type === Symbol.for('react.fragment'); /** @@ -16,7 +21,7 @@ const isFragment = (value) => * @param {Object} element React element * @returns {Object[]} parsed React elements */ -const createInstances = (element) => { +const createInstances = async element => { if (!element) return []; if (isString(element) || isNumber(element)) { @@ -34,16 +39,21 @@ const createInstances = (element) => { if (!isString(element.type)) { return createInstances(element.type(element.props)); } - const { type, props: { style = {}, children = [], ...props }, } = element; - const nextChildren = castArray(children).reduce( - (acc, child) => acc.concat(createInstances(child)), - [], + if (isImage(element)) { + const node = { props, type, style, box: {} }; + await fetchImage(node); + return [node]; + } + + const instances = await Promise.all( + castArray(children).map(child => createInstances(child)), ); + const nextChildren = flatten(instances); return [ { diff --git a/packages/layout/src/steps/resolvePagination.js b/packages/layout/src/steps/resolvePagination.js index 98d6f3135..d3cb0e2e6 100644 --- a/packages/layout/src/steps/resolvePagination.js +++ b/packages/layout/src/steps/resolvePagination.js @@ -150,44 +150,51 @@ const shouldResolveDynamicNodes = (node) => { return isDynamic(node) || children.some(shouldResolveDynamicNodes); }; -const resolveDynamicNodes = (props, node) => { +const resolveDynamicNodes = async (props, node) => { const isNodeDynamic = isDynamic(node); // Call render prop on dynamic nodes and append result to children - const resolveChildren = (children = []) => { + const resolveChildren = async (children = []) => { if (isNodeDynamic) { - const res = node.props.render(props); - return createInstances(res) - .filter(Boolean) - .map((n) => resolveDynamicNodes(props, n)); + const dynamicRenderResult = node.props.render(props); + const dynamicInstances = await createInstances(dynamicRenderResult); + const resolvedDynamicNodes = await Promise.all( + dynamicInstances + .filter(Boolean) + .map(n => resolveDynamicNodes(props, n)), + ); + return resolvedDynamicNodes; } - return children.map((c) => resolveDynamicNodes(props, c)); + const resolvedChildren = await Promise.all( + children.map(c => resolveDynamicNodes(props, c)), + ); + return resolvedChildren; }; // We reset dynamic text box so it can be computed again later on const resetHeight = isNodeDynamic && isText(node); const box = resetHeight ? { ...node.box, height: 0 } : node.box; - const children = resolveChildren(node.children); + const children = await resolveChildren(node.children); const lines = isNodeDynamic ? null : node.lines; return Object.assign({}, node, { box, lines, children }); }; -const resolveDynamicPage = (props, page, fontStore, yoga) => { +const resolveDynamicPage = async (props, page, fontStore) => { if (shouldResolveDynamicNodes(page)) { - const resolvedPage = resolveDynamicNodes(props, page); - return relayoutPage(resolvedPage, fontStore, yoga); + const resolvedPage = await resolveDynamicNodes(props, page); + return relayoutPage(resolvedPage, fontStore); } return page; }; -const splitPage = (page, pageNumber, fontStore, yoga) => { +const splitPage = async (page, pageNumber, fontStore) => { const wrapArea = getWrapArea(page); const contentArea = getContentArea(page); - const dynamicPage = resolveDynamicPage({ pageNumber }, page, fontStore, yoga); + const dynamicPage = await resolveDynamicPage({ pageNumber }, page, fontStore); const height = page.style.height; const [currentChilds, nextChilds] = splitNodes( @@ -220,7 +227,7 @@ const splitPage = (page, pageNumber, fontStore, yoga) => { return [currentPage, nextPage]; }; -const resolvePageIndices = (fontStore, yoga, page, pageNumber, pages) => { +const resolvePageIndices = async (fontStore, page, pageNumber, pages) => { const totalPages = pages.length; const props = { @@ -245,22 +252,22 @@ const dissocSubPageData = (page) => { return omit(['subPageNumber', 'subPageTotalPages'], page); }; -const paginate = (page, pageNumber, fontStore, yoga) => { +const paginate = async (page, pageNumber, fontStore) => { if (!page) return []; if (page.props?.wrap === false) return [page]; - let splittedPage = splitPage(page, pageNumber, fontStore, yoga); + let splittedPage = await splitPage(page, pageNumber, fontStore); const pages = [splittedPage[0]]; let nextPage = splittedPage[1]; while (nextPage !== null) { - splittedPage = splitPage( + // eslint-disable-next-line no-await-in-loop + splittedPage = await splitPage( nextPage, pageNumber + pages.length, fontStore, - yoga, ); pages.push(splittedPage[0]); @@ -278,24 +285,27 @@ const paginate = (page, pageNumber, fontStore, yoga) => { * @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); - + // eslint-disable-next-line no-restricted-syntax + for (const page of doc.children) { + // eslint-disable-next-line no-await-in-loop + let subpages = await paginate(page, pageNumber, fontStore); subpages = assocSubPageData(subpages); pageNumber += subpages.length; pages = pages.concat(subpages); } - pages = pages.map((...args) => - dissocSubPageData(resolvePageIndices(fontStore, doc.yoga, ...args)), - ); - - return assingChildren(pages, doc); + const nextPages = []; + // eslint-disable-next-line no-restricted-syntax + for (const [index, page] of pages.entries()) { + // eslint-disable-next-line no-await-in-loop + const indices = await resolvePageIndices(fontStore, page, index, pages); + nextPages.push(dissocSubPageData(indices)); + } + return assingChildren(nextPages, doc); }; export default resolvePagination;