
Сборка аккуратного PDF из разрозненных исходных файлов — рутинная, но кропотливая задача: титульная страница должна стоять в начале проектного задания, страницы с ценами должны быть внутри договора, а квартальная сводка объединяет диаграммы из десятка отчётов. При ручной работе приходится переключаться между несколькими программами для просмотра PDF и надеяться, что порядок страниц получится правильным, а несовпадающие размеры страниц только усугубляют проблему.
Spire.PDF for JavaScript переносит всю операцию в браузер. Работая на WebAssembly, он загружает, обрабатывает и сохраняет PDF-документы полностью на стороне клиента через виртуальную файловую систему (VFS), а это значит, что ни один файл не отправляется на сервер. В этой статье рассматриваются четыре различных метода копирования страниц PDF между документами — три для переноса целых страниц и один для извлечения содержимого страницы в виде многоразового шаблона — с полными примерами кода на React для каждого.
Инструкции по настройке проекта и установке см. в разделе Интеграция Spire.PDF for JavaScript в проект React. Приведённые ниже примеры предполагают, что Spire.PDF установлен, а модуль WebAssembly инициализирован.
Четыре способа копирования страниц PDF: краткий обзор
Прежде чем рассматривать каждый метод по отдельности, таблица ниже предлагает краткое сравнение. Первые три метода переносят целые страницы и автоматически сохраняют размеры, поворот и поля исходной страницы. Четвёртый отделяет содержимое от геометрии страницы, давая вам полный контроль над размером целевой страницы и позицией отрисовки.
| Метод | Вызов API | Что копируется | Размер страницы | Типичный сценарий использования |
|---|---|---|---|---|
| Вставка одной страницы | InsertPage |
Одна страница в выбранной вами позиции | Наследуется от источника | Добавление обложки или титульной страницы в начало |
| Вставка диапазона страниц | InsertPageRange |
Последовательный блок страниц | Наследуется от источника | Добавление определённого раздела, например таблиц с ценами |
| Добавление целого документа | AppendPage |
Все страницы исходного документа | Наследуется от источника | Объединение полных документов от начала до конца |
| Отрисовка содержимого страницы как шаблона |
CreateTemplate + DrawTemplate
|
Только содержимое страницы, отрисованное на любой странице | Вы задаёте целевой размер | Повторное использование содержимого на страницах разного размера или его многократное повторение |
Первые три метода — это простой перенос страниц: выберите источник, выберите назначение, а всё остальное библиотека сделает сама. Подход с шаблоном сложнее и открывает возможности, которые простое копирование страниц не может реализовать, например масштабирование содержимого под другой размер страницы или нанесение одного и того же содержимого на несколько страниц. Сначала мы рассмотрим три метода переноса страниц, а затем подробно изучим технику работы с шаблоном.
Копирование одной страницы в определённую позицию
Самый точный из четырёх методов, PdfDocument.InsertPage, копирует одну страницу из исходного документа и помещает её на точный индекс в целевом. Параметр resultPageIndex управляет тем, куда попадёт копия: передайте 0, чтобы вставить её в начало, передайте текущее количество страниц целевого документа, чтобы добавить её в конец, или укажите любой индекс между ними, чтобы вставить её в эту позицию. Если полностью опустить resultPageIndex, страница по умолчанию добавляется в конец.
function App() {
const copyPageAtPosition = async () => {
// Get the Spire.PDF WASM module
const pdfModule = window.wasmModule?.spirepdf;
// Check whether the module is ready
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// Load both the source and the target document into the VFS
const sourceFileName = 'SourceDocument.pdf';
const targetFileName = 'TargetDocument.pdf';
await window.spire.FetchFileToVFS(sourceFileName, "", `${process.env.PUBLIC_URL}/data/`);
await window.spire.FetchFileToVFS(targetFileName, "", `${process.env.PUBLIC_URL}/data/`);
// Load the two documents
const sourceDoc = new pdfModule.PdfDocument();
sourceDoc.LoadFromFile(sourceFileName);
const targetDoc = new pdfModule.PdfDocument();
targetDoc.LoadFromFile(targetFileName);
// Copy page 1 of the source document to the front of the target document
// pageIndex comes from the source document, resultPageIndex is where the copy lands
targetDoc.InsertPage({ ldDoc: sourceDoc, pageIndex: 0, resultPageIndex: 0 });
// Save the result document
const outputFileName = 'CopyPageAtPosition.pdf';
targetDoc.SaveToFile(outputFileName);
sourceDoc.Close();
targetDoc.Close();
// Read the generated file from the VFS and trigger the download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Copy Page at Position</h1>
<button onClick={copyPageAtPosition}>
Start
</button>
</div>
);
}
export default App;
Среди всех четырёх методов копирования
resultPageIndex— единственный параметр, который позволяет выбрать точку вставки. Значение 0 помещает страницу первой, 1 — второй, а передача текущего количества страниц целевого документа даёт тот же эффект, что и добавление в конец.
Целевой документ увеличивается с двух страниц до трёх, и первая страница исходного документа теперь занимает начальную позицию:

Копирование диапазона страниц в конец
Когда вам нужно больше одной страницы, но меньше целого документа, PdfDocument.InsertPageRange копирует непрерывный блок страниц, заданный начальным и конечным индексом. В отличие от InsertPage, этот метод принимает позиционные аргументы, а не объект с параметрами, и всегда добавляет скопированные страницы в конец целевого документа — параметра для выбора позиции вставки нет. Конечный индекс включается в диапазон, поэтому передача (sourceDoc, 1, 2) копирует страницы 2 и 3 (с нумерацией от нуля).
function App() {
const appendPageRange = async () => {
// Get the Spire.PDF WASM module
const pdfModule = window.wasmModule?.spirepdf;
// Check whether the module is ready
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// Load both the source and the target document into the VFS
const sourceFileName = 'SourceDocument.pdf';
const targetFileName = 'TargetDocument.pdf';
await window.spire.FetchFileToVFS(sourceFileName, "", `${process.env.PUBLIC_URL}/data/`);
await window.spire.FetchFileToVFS(targetFileName, "", `${process.env.PUBLIC_URL}/data/`);
// Load the two documents
const sourceDoc = new pdfModule.PdfDocument();
sourceDoc.LoadFromFile(sourceFileName);
const targetDoc = new pdfModule.PdfDocument();
targetDoc.LoadFromFile(targetFileName);
// Append pages 2 to 3 of the source document to the end of the target document
// Note: these are positional arguments, not an object; endIndex is inclusive
targetDoc.InsertPageRange(sourceDoc, 1, 2);
// Save the result document
const outputFileName = 'CopyPageRange.pdf';
targetDoc.SaveToFile(outputFileName);
sourceDoc.Close();
targetDoc.Close();
// Read the generated file from the VFS and trigger the download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Copy Page Range</h1>
<button onClick={appendPageRange}>
Copy pages 2-3
</button>
</div>
);
}
export default App;
Целевой документ получает две дополнительные страницы, и их общее количество увеличивается с двух до четырёх:

Добавление целого документа
В самом простом случае — переносе всех страниц одного документа в другой — PdfDocument.AppendPage избавляет от необходимости вычислять индексы. Передайте объект исходного документа, и все его страницы будут добавлены к целевому в исходной последовательности. Чтобы объединить несколько документов, вызывайте AppendPage повторно для каждого исходного документа по очереди.
function App() {
const appendWholeDocument = async () => {
// Get the Spire.PDF WASM module
const pdfModule = window.wasmModule?.spirepdf;
// Check whether the module is ready
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// Load both the source and the target document into the VFS
const sourceFileName = 'SourceDocument.pdf';
const targetFileName = 'TargetDocument.pdf';
await window.spire.FetchFileToVFS(sourceFileName, "", `${process.env.PUBLIC_URL}/data/`);
await window.spire.FetchFileToVFS(targetFileName, "", `${process.env.PUBLIC_URL}/data/`);
// Load the two documents
const sourceDoc = new pdfModule.PdfDocument();
sourceDoc.LoadFromFile(sourceFileName);
const targetDoc = new pdfModule.PdfDocument();
targetDoc.LoadFromFile(targetFileName);
// Use AppendPage when the whole document has to be copied; all pages are appended in order
targetDoc.AppendPage({ doc: sourceDoc });
// Save the result document
const outputFileName = 'CopyAllPages.pdf';
targetDoc.SaveToFile(outputFileName);
sourceDoc.Close();
targetDoc.Close();
// Read the generated file from the VFS and trigger the download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Copy Whole Document</h1>
<button onClick={appendWholeDocument}>
Start
</button>
</div>
);
}
export default App;
Все четыре страницы из исходного документа присоединяются к целевому, увеличивая его с двух страниц до шести:

Копирование содержимого страницы с помощью шаблона
Три метода выше рассматривают страницу как неделимую единицу: она переносится с сохранением размера, поворота и полей. Но в реальной сборке документов часто требуется более тонкий контроль — размещение содержимого страницы на странице другого размера, его увеличение или уменьшение, а также нанесение одного и того же содержимого на несколько страниц. Здесь на сцену выходит PdfPageBase.CreateTemplate.
CreateTemplate извлекает визуальное содержимое страницы в объект PdfTemplate. Затем вы отрисовываете этот шаблон на любой странице с помощью Canvas.DrawTemplate, указывая позицию и размер области отрисовки. Шаблон отделён от геометрии исходной страницы, поэтому вы можете отобразить его в любом масштабе, в любом месте и на странице любого размера — и можете отрисовать один и тот же шаблон столько раз, сколько нужно.
Это делает шаблоны особенно полезными для таких сценариев, как:
- Размещение содержимого обложки A5 по центру страницы A4 без белой рамки
- Создание водяного знака или фонового узора из существующей страницы
- Дублирование макета формы на нескольких новых страницах в разных масштабах
function App() {
const copyPageWithTemplate = async () => {
// Get the Spire.PDF WASM module
const pdfModule = window.wasmModule?.spirepdf;
// Check whether the module is ready
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// Load the PDF file to work on into the VFS
const inputFileName = 'SourceDocument.pdf';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
// Load the document
const doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// Take the page to be reused and turn it into a template: read the content once, draw it many times
const sourcePage = doc.Pages.get_Item(0);
const template = sourcePage.CreateTemplate();
// First placement: insert an A4 page at position 2, a different size from the source,
// and draw the content scaled to 297.6 x 421.6 at (80, 80)
const page1 = doc.Pages.Insert(1, new pdfModule.SizeF(595.0, 842.0), new pdfModule.PdfMargins({ margin: 0.0 }));
page1.Canvas.DrawTemplate(template, new pdfModule.PointF(80.0, 80.0), new pdfModule.SizeF(297.6, 421.6));
// Second placement: insert another A4 page, drawing the same template smaller in the lower right
const page2 = doc.Pages.Insert(2, new pdfModule.SizeF(595.0, 842.0), new pdfModule.PdfMargins({ margin: 0.0 }));
page2.Canvas.DrawTemplate(template, new pdfModule.PointF(320.0, 460.0), new pdfModule.SizeF(200.0, 283.3));
// Save the result document
const outputFileName = 'CopyPageWithTemplate.pdf';
doc.SaveToFile(outputFileName);
doc.Close();
// Read the generated file from the VFS and trigger the download
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Copy Page with Template</h1>
<button onClick={copyPageWithTemplate}>
Start
</button>
</div>
);
}
export default App;
Несколько деталей, которые стоит отметить в DrawTemplate:
- Аргумент размера: Если третий аргумент (целевой размер) опущен, шаблон отрисовывается в исходных размерах без масштабирования. На более крупной целевой странице содержимое занимает лишь часть доступного пространства.
-
Создание страницы: Размеры и поля целевой страницы берутся из
Pages.Insert, а не из шаблона. В примере нулевые поля со всех сторон совмещают начало координат отрисовки с верхним левым углом страницы. -
Многократная отрисовка: Один и тот же объект
templateотрисовывается дважды на двух отдельных страницах в разных позициях и масштабах, демонстрируя возможность повторного использования.
Содержимое страницы 1 теперь появляется на двух новых вставленных страницах A4 в разных масштабах и позициях, увеличивая документ с четырёх страниц до шести:

Часто задаваемые вопросы
Создание страницы с new PdfMargins(0.0) вызывает Arg_NullReferenceException
Причина: Конструктор PdfMargins интерпретирует простой числовой аргумент как внутренний дескриптор, а не как значение поля. Поэтому вызов new pdfModule.PdfMargins(0.0) создаёт объект, который не представляет корректные поля, — доступ к его свойству Left или Top вызывает Arg_NullReferenceException, а передача его при создании страницы даёт неожиданные результаты.
Решение: Всегда передавайте поля как объект конфигурации. Для одинаковых нулевых полей используйте { margin: 0.0 }; для отдельных значений сторон указывайте каждую сторону явно:
// Zero margins on all four sides
const margins = new pdfModule.PdfMargins({ margin: 0.0 });
// Or set each side separately
const custom = new pdfModule.PdfMargins({ left: 20.0, top: 20.0, right: 20.0, bottom: 20.0 });
При копировании страниц возникает ошибка выхода за пределы диапазона или неверного порядка диапазона
Причина: Индексы страниц начинаются с нуля, а endIndex в InsertPageRange включается в диапазон. Поэтому допустимый диапазон — от 0 до Pages.Count - 1. Указание индекса вне этого диапазона вызывает Index out of range, а если задать startIndex больше endIndex, возникает ошибка The start index is greater then the end index.
Решение: Ограничьте верхнюю границу, приведя её к Pages.Count перед вызовом метода:
// To copy pages 2 to 4: start = 1, end = 3, with the page count as the upper bound
const start = 1;
const end = Math.min(3, sourceDoc.Pages.Count - 1);
targetDoc.InsertPageRange(sourceDoc, start, end);
После копирования повёрнутая страница получает неправильную ориентацию
Причина: CreateTemplate() захватывает отрисованное содержимое страницы, но не её угол поворота (запись /Rotate). Когда исходная страница имеет поворот, система координат шаблона не совпадает с целевой страницей — при прямой отрисовке содержимое оказывается за пределами видимой области, а у полученной копии Rotation равно 0.
Решение: Для повёрнутых исходных страниц предпочитайте копирование целой страницы, чтобы угол поворота переносился вместе с содержимым:
// Whole-page copy: the rotation angle comes with the page
targetDoc.InsertPage({ ldDoc: sourceDoc, pageIndex: 0, resultPageIndex: 1 });
Если без подхода с шаблоном не обойтись, временно сбросьте поворот исходной страницы перед извлечением шаблона, а затем восстановите исходный угол и для исходной, и для новой страницы:
const rotation = sourcePage.Rotation.value;
// Zero it temporarily so the template exports at the page's real coordinates
sourcePage.Rotation = 0;
const newPage = doc.Pages.Insert(1, sourcePage.Size, new pdfModule.PdfMargins({ margin: 0.0 }));
newPage.Canvas.DrawTemplate(sourcePage.CreateTemplate(), new pdfModule.PointF(0.0, 0.0));
// Restore the source page and give the copy the same angle
sourcePage.Rotation = rotation;
newPage.Rotation = rotation;
Чтобы удалить оценочный водяной знак из выходных документов или разблокировать полный доступ к функциям, свяжитесь с отделом продаж для получения временной 30-дневной лицензии.