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

fix for dynamic image render #2465

Open
wants to merge 2 commits into
base: master
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
5 changes: 5 additions & 0 deletions .changeset/many-ads-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@react-pdf/layout': minor
---

fix dynamic image render
1 change: 1 addition & 0 deletions packages/layout/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@react-pdf/yoga": "^4.1.2",
"cross-fetch": "^3.1.5",
"emoji-regex": "^10.2.1",
"lodash.flatten": "^4.4.0",
"queue": "^6.0.1"
},
"devDependencies": {
Expand Down
22 changes: 16 additions & 6 deletions packages/layout/src/node/createInstances.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
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 isImage = value => value.type === Image;

const isFragment = value =>
value && value.type === Symbol.for('react.fragment');

Expand All @@ -16,7 +21,7 @@ const isFragment = value =>
* @param {Object} React element
* @returns {Array} parsed react elements
*/
const createInstances = element => {
const createInstances = async element => {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is correct architectural-wise. We are mixing concepts here. createInstances should not fetch images, that's done separately on another layer. Can we preserve that?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not very familiar with this project and was just looking for a quick solution. Perhaps this issue would be better addressed by someone more acquainted with the codebase.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggested 2 possible approaches here #1369 (comment)
Do they make sense @diegomura?

if (!element) return [];

if (isString(element) || isNumber(element)) {
Expand All @@ -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 [
{
Expand Down
67 changes: 41 additions & 26 deletions packages/layout/src/steps/resolvePagination.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,44 +147,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) => {
const resolveDynamicPage = async (props, page, fontStore) => {
if (shouldResolveDynamicNodes(page)) {
const resolvedPage = resolveDynamicNodes(props, page);
const resolvedPage = await resolveDynamicNodes(props, page);
return relayoutPage(resolvedPage, fontStore);
}

return page;
};

const splitPage = (page, pageNumber, fontStore) => {
const splitPage = async (page, pageNumber, fontStore) => {
const wrapArea = getWrapArea(page);
const contentArea = getContentArea(page);
const dynamicPage = resolveDynamicPage({ pageNumber }, page, fontStore);
const dynamicPage = await resolveDynamicPage({ pageNumber }, page, fontStore);
const height = page.style.height;

const [currentChilds, nextChilds] = splitNodes(
Expand Down Expand Up @@ -217,7 +224,7 @@ const splitPage = (page, pageNumber, fontStore) => {
return [currentPage, nextPage];
};

const resolvePageIndices = (fontStore, page, pageNumber, pages) => {
const resolvePageIndices = async (fontStore, page, pageNumber, pages) => {
const totalPages = pages.length;

const props = {
Expand All @@ -242,18 +249,23 @@ const dissocSubPageData = page => {
return omit(['subPageNumber', 'subPageTotalPages'], page);
};

const paginate = (page, pageNumber, fontStore) => {
const paginate = async (page, pageNumber, fontStore) => {
if (!page) return [];

if (page.props?.wrap === false) return [page];

let splittedPage = splitPage(page, pageNumber, fontStore);
let splittedPage = await splitPage(page, pageNumber, fontStore);

const pages = [splittedPage[0]];
let nextPage = splittedPage[1];

while (nextPage !== null) {
splittedPage = splitPage(nextPage, pageNumber + pages.length, fontStore);
// eslint-disable-next-line no-await-in-loop
splittedPage = await splitPage(
nextPage,
pageNumber + pages.length,
fontStore,
);

pages.push(splittedPage[0]);
nextPage = splittedPage[1];
Expand All @@ -270,24 +282,27 @@ const paginate = (page, pageNumber, fontStore) => {
* @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);

// 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, ...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;