Категория

Копирование и повторное использование страниц PDF в разных документах с помощью JavaScript

2026-09-28 08:36:11 Allen Yang
AI Summarize:
ChatGPT
ChatGPT ✓
Claude ✓
Grok ✓
Perplexity ✓
Quick
Quick
Concise overview
Highlights
Key takeaways
Detailed
Structured explanation
Brief
One sentence summary
Summarize |

Copy and reuse PDF pages across documents with JavaScript

Сборка аккуратного 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 — второй, а передача текущего количества страниц целевого документа даёт тот же эффект, что и добавление в конец.

Целевой документ увеличивается с двух страниц до трёх, и первая страница исходного документа теперь занимает начальную позицию:

Page 1 of the source document now sits in front of the target document, which goes from 2 pages to 3


Копирование диапазона страниц в конец

Когда вам нужно больше одной страницы, но меньше целого документа, 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;

Целевой документ получает две дополнительные страницы, и их общее количество увеличивается с двух до четырёх:

After pages 2 and 3 of the source document are appended, the document has 4 pages


Добавление целого документа

В самом простом случае — переносе всех страниц одного документа в другой — 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;

Все четыре страницы из исходного документа присоединяются к целевому, увеличивая его с двух страниц до шести:

After all 4 pages of the source document are appended, the document has 6 pages


Копирование содержимого страницы с помощью шаблона

Три метода выше рассматривают страницу как неделимую единицу: она переносится с сохранением размера, поворота и полей. Но в реальной сборке документов часто требуется более тонкий контроль — размещение содержимого страницы на странице другого размера, его увеличение или уменьшение, а также нанесение одного и того же содержимого на несколько страниц. Здесь на сцену выходит 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 в разных масштабах и позициях, увеличивая документ с четырёх страниц до шести:

The content of page 1 in the source document is scaled onto two new A4 pages, taking the document from 4 pages to 6


Часто задаваемые вопросы

Создание страницы с 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-дневной лицензии.


См. также