Search Docs
Loading...
Skip to content

Place PDF Pages in a Design

Place pages of a PDF into an existing design in Node.js, as editable groups, and combine PDF pages into one scene.

8 mins
estimated time
Download
StackBlitz
GitHub

The PDF importer has two ways to bring in a PDF. parse() replaces the scene with the whole document. loadAsBlocks() leaves the current scene as it is and returns one group per PDF page, with the page’s text, shapes and images as editable children. You decide where each group goes. For the editor, see the browser guide.

This guide covers how to fit a PDF page into a frame of an existing design, append every PDF page to a scene, read warnings, and export the result.

Install the Packages#

Install the Node.js SDK and the PDF importer. loadAsBlocks() with units: 'scene' needs @imgly/pdf-importer 0.3.0 or later. The importer has its own version numbers, so we do not pin it to the CE.SDK version.

Terminal window
npm install @cesdk/node@1.86.0 @imgly/pdf-importer dotenv

Initialize the Engine#

We load the environment and create a headless engine.

config();
const engine = await CreativeEngine.init({
// license: process.env.CESDK_LICENSE,
baseURL: process.env.IMGLY_LOCAL_ASSETS_URL
});

Create the Design#

The scene stands for a design that already exists: one square page in pixels with a background color.

// An existing design: one square page with a background color.
const scene = engine.scene.create();
const page = engine.block.create('page');
engine.block.setWidth(page, 1080);
engine.block.setHeight(page, 1080);
const background = engine.block.createFill('color');
engine.block.setFill(page, background);
engine.block.setColor(background, 'fill/color/value', {
r: 0.93,
g: 0.93,
b: 0.95,
a: 1
});
engine.block.appendChild(scene, page);

Create the Parser#

The importer resolves PDF fonts through the Google Fonts asset library, so we register it first. We create the parser on the engine that owns the scene, because the returned groups live in the engine that parsed them. The parser reads the PDF on the first call to loadAsBlocks() or getPageCount(), and later calls reuse the result. We also set one time limit and one block limit for all imports of this file.

await addGfontsAssetLibrary(engine);
const bytes = await readFile(
new URL('./public/summer-market.pdf', import.meta.url)
);
// The importer bundles its own engine types, hence the cast.
const parser = await PDFParser.fromFile(
engine as any,
bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)
);
// One time limit for all imports of this file.
const limits = { signal: AbortSignal.timeout(30_000), maxBlocks: 10000 };

Fit a Page into a Frame#

We import page 1 and fit it into a frame on the page.

const frame = { x: 90, y: 90, width: 900, height: 900 };
const { blocks, logger, dispose } = await parser.loadAsBlocks({
units: 'scene',
pageIndex: 0,
preservePageBounds: true,
...limits
});
const group = blocks[0];
if (group === undefined) {
throw new Error('PDF page 1 is empty');
}
try {
engine.block.appendChild(page, group);
const factor = Math.min(
frame.width / engine.block.getWidth(group),
frame.height / engine.block.getHeight(group)
);
engine.block.scale(group, factor);
engine.block.setPositionX(
group,
frame.x + (frame.width - engine.block.getWidth(group)) / 2
);
engine.block.setPositionY(
group,
frame.y + (frame.height - engine.block.getHeight(group)) / 2
);
} catch (error) {
dispose();
throw error;
}

units: 'scene' converts the content to the scene’s design unit, DPI and font-size unit, so the group keeps the physical size of the PDF page. Without units, the importer uses the deprecated legacy mode: inch values for geometry, whatever the scene’s unit is.

preservePageBounds: true keeps the white space of the PDF page. The group then has the size of the page, not only of its content, so the page fits into the frame as a whole. Without it, the group’s bounds follow the visible content.

signal and maxBlocks limit an import of a PDF from a user. When the signal fires, the import rejects with an AbortError. This also stops the wait for the PDF parse, but a parse that has started keeps running until it ends. maxBlocks rejects a page that needs more blocks than the limit. A failed import removes its own blocks.

We append the group before we scale it: engine.block.scale() throws for a block that is not in the scene yet. If placement fails after the import, dispose() destroys the groups and releases their unused images and spot colors.

Append Every PDF Page#

To combine PDF pages into one scene, we create a scene page for each PDF page and move the group into it. getPageCount() reuses the parse of the first import. We import each page with its own pageIndex and the same limits. A page without content produces no group, so an index into blocks from an all-pages import is not a PDF page number.

const pageCount = await parser.getPageCount();
// Import each PDF page on its own, so the source page number stays known.
for (let pageIndex = 0; pageIndex < pageCount; pageIndex++) {
const result = await parser.loadAsBlocks({
units: 'scene',
pageIndex,
...limits
});
const pageGroup = result.blocks[0];
if (pageGroup === undefined) {
continue;
}
const newPage = engine.block.create('page');
engine.block.setWidth(newPage, engine.block.getWidth(pageGroup));
engine.block.setHeight(newPage, engine.block.getHeight(pageGroup));
engine.block.appendChild(scene, newPage);
engine.block.appendChild(newPage, pageGroup);
engine.block.setPositionX(pageGroup, 0);
engine.block.setPositionY(pageGroup, 0);
engine.block.setName(newPage, `PDF page ${pageIndex + 1}`);
}

The same loop works for several PDFs: create one parser per file and import into the same scene.

Read Warnings#

The parser keeps one logger for all its calls, so we read it once after all imports. We print every message that is not info.

for (const message of logger.getMessages()) {
if (message.type !== 'info') {
console.warn(`[${message.code}] ${message.message}`);
}
}

An index outside the document returns no blocks and logs the PAGE_INDEX_OUT_OF_RANGE warning. A substituted font logs FONT_SUBSTITUTIONS.

Export the Result#

We export the design page as PNG and the whole scene as a PDF. The PDF has three pages: the design and the two PDF pages.

const outputDirectory = new URL('./output/', import.meta.url);
await mkdir(outputDirectory, { recursive: true });
const png = await engine.block.export(page, { mimeType: 'image/png' });
await writeFile(
new URL('placed-page.png', outputDirectory),
Buffer.from(await png.arrayBuffer())
);
const pdf = await engine.block.export(scene, {
mimeType: 'application/pdf'
});
await writeFile(
new URL('design.pdf', outputDirectory),
Buffer.from(await pdf.arrayBuffer())
);
console.log(`Exported ${engine.scene.getPages().length} pages`);

Clean Up#

We dispose the engine in a finally block, so it is released also when an import fails.

engine.dispose();

Troubleshooting#

  • Scaling throws “Block hasn’t been laid out yet”: you scaled the group before you appended it. Append it to a page first.
  • The import rejects with “The scene changed during PDF import” or “The scene or its units changed during PDF import”: another scene was created or loaded, or the scene’s design unit changed, while the PDF was importing. Run the import again in the current scene.
  • Imported content is very small: you used the legacy unit mode. Pass units: 'scene'.
  • The import rejects with an AbortError: the signal fired. Increase the timeout or check the PDF size before you import it.

Next Steps#