
Папку с изображениями неудобно передавать кому-либо. PDF — это один файл, который открывается везде, предсказуемо печатается и — что на самом деле важно людям — сохраняет фиксированный порядок. Именно поэтому отсканированные страницы, наборы фотографий, фото чеков и экспортированные макеты дизайна так часто собираются в PDF, прежде чем куда-либо отправиться.
Создание такого PDF в браузере — задача иная, чем рендеринг PDF в изображение. Вы не декодируете что-то уже существующее; вы принимаете решения, которые обычно за вас принимает формат документа: каков размер страницы, где на ней расположено изображение, что происходит, если форма изображения отличается от формы страницы, и в каком порядке идут страницы.
Spire.PDF for JavaScript предоставляет доступ к этим решениям через холст страницы. Вы добавляете страницу, загружаете изображение, рисуете его на этой странице в вычисленном вами размере и сохраняете. Всё выполняется на стороне клиента через WebAssembly, поэтому изображения никуда не загружаются.
Почему изображения попадают в PDF
Все эти сценарии объединяет одно: несколько изображений, которые должны вести себя как один документ.
- Отсканированные или сфотографированные многостраничные документы — договор, сфотографированный страница за страницей и собранный в один файл, который можно подшить в дело или отправить по электронной почте.
- Наборы фотографий и портфолио — одно изображение на страницу, в выбранном кем-то порядке.
- Чеки и отчёты о расходах — дюжина фотографий с телефона, которые бухгалтерия хочет получить одним вложением.
- Экспорт макетов и диаграмм — кадры, экспортированные из инструмента и собранные во что-то, что можно просмотреть.
В каждом случае дело не столько в самом формате PDF. Речь о том, чтобы получить из груды изображений стабильный, единый упорядоченный артефакт.
Предварительные требования
В этом руководстве предполагается, что у вас есть проект на React с установленным Spire.PDF for JavaScript и инициализированным модулем WASM. О настройке см. Интеграция Spire.PDF for JavaScript в проект React.
Вам понадобится:
- Один или несколько файлов изображений, загруженных в VFS
- Модуль WASM, доступный по адресу
window.wasmModule.spirepdf
Одно изображение — одна страница
Базовый процесс состоит из четырёх шагов: создать документ, добавить страницу, загрузить изображение, нарисовать его. Самое интересное — это рисование: нужно решить, какого размера изображение должно быть на странице.
function App() {
const convertImageToPDF = async () => {
// Get the Spire.PDF WASM module
const pdfModule = window.wasmModule?.spirepdf;
// Check if the WASM module is ready
if (!pdfModule) {
alert('Spire.PDF is not ready yet');
return;
}
// Load the image file into VFS
const inputFileName = 'Scenery.png';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
// Create a PdfDocument object
let doc = new pdfModule.PdfDocument();
// Add a page
let page = doc.Pages.Add();
// Load the image
let image = pdfModule.PdfImage.FromFile(inputFileName);
// Calculate the scale ratio so the image fits the page completely
let widthFitRate = image.PhysicalDimension.Width / page.Canvas.ClientSize.Width;
let heightFitRate = image.PhysicalDimension.Height / page.Canvas.ClientSize.Height;
let fitRate = Math.max(widthFitRate, heightFitRate);
// Calculate the scaled dimensions of the image
let fitWidth = image.PhysicalDimension.Width / fitRate;
let fitHeight = image.PhysicalDimension.Height / fitRate;
// Center the image on the page
let x = (page.Canvas.ClientSize.Width - fitWidth) / 2;
let y = (page.Canvas.ClientSize.Height - fitHeight) / 2;
// Draw the image onto the page
page.Canvas.DrawImage({ image: image, x: x, y: y, width: fitWidth, height: fitHeight });
const outputFileName = 'ImageToPDF.pdf';
// Save as PDF format
doc.SaveToFile({ fileName: outputFileName, fileFormat: pdfModule.FileFormat.PDF });
doc.Close();
// Read the converted file from VFS and trigger 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>Convert Image To PDF</h1>
<button onClick={convertImageToPDF}>
Generate
</button>
</div>
);
}
export default App;
PDF-документ, созданный после загрузки изображения через PdfImage.FromFile и его отрисовки с помощью Canvas.DrawImage

Вынесите расчёт размера в отдельную вспомогательную функцию, потому что это единственная часть этого кода, которую вы будете переиспользовать во всех остальных рецептах ниже. Математика: возьмите page.Canvas.ClientSize — область страницы, доступную для рисования, в пунктах — в качестве вашего бюджета, сравните её с естественным PhysicalDimension изображения и разделите оба измерения на единый коэффициент, чтобы сохранить соотношение сторон. Спрячьте это в функцию, чтобы подверженная ошибкам часть находилась ровно в одном месте:
// Contain: scale until the whole image fits inside the page
function fitContain(imgW, imgH, pageW, pageH) {
const rate = Math.max(imgW / pageW, imgH / pageH);
const width = imgW / rate;
const height = imgH / rate;
return { width, height, x: (pageW - width) / 2, y: (pageH - height) / 2 };
}
Теперь вызов отрисовки из примера выше сокращается до трёх строк, а решение «contain или cover» переходит из математики прямо в имя функции:
let page = doc.Pages.Add();
let box = fitContain(
image.PhysicalDimension.Width, image.PhysicalDimension.Height,
page.Canvas.ClientSize.Width, page.Canvas.ClientSize.Height
);
page.Canvas.DrawImage({ image: image, x: box.x, y: box.y, width: box.width, height: box.height });
Math.max — это выбор «contain»: масштабирование по более ограничивающей оси, чтобы всё изображение оставалось видимым. Если же вы хотите заполнить страницу и обрезать выходящее за края, замените на Math.min; в разделе о подгонке размера приведён аналог fitCover и вариант с полями.
Много изображений — один документ
Одно изображение на страницу означает один Pages.Add() и один DrawImage на каждое изображение. Пройдитесь циклом по массиву имён файлов — и порядок массива станет порядком страниц, что как раз то, что нужно, когда пользователь только что закончил перетаскивать миниатюры в нужную последовательность.
const combineImagesToPdf = async () => {
const pdfModule = window.wasmModule?.spirepdf;
if (!pdfModule) return;
// The order of this array is the order of pages in the PDF
const imageFiles = ['scan_01.png', 'scan_02.png', 'scan_03.png', 'scan_04.png'];
for (const fileName of imageFiles) {
await window.spire.FetchFileToVFS(fileName, "", `${process.env.PUBLIC_URL}/data/`);
}
let doc = new pdfModule.PdfDocument();
for (const fileName of imageFiles) {
let page = doc.Pages.Add();
let image = pdfModule.PdfImage.FromFile(fileName);
let box = fitContain(
image.PhysicalDimension.Width, image.PhysicalDimension.Height,
page.Canvas.ClientSize.Width, page.Canvas.ClientSize.Height
);
page.Canvas.DrawImage({ image: image, x: box.x, y: box.y, width: box.width, height: box.height });
}
const outputFileName = 'ScannedDocument.pdf';
doc.SaveToFile({ fileName: outputFileName, fileFormat: pdfModule.FileFormat.PDF });
doc.Close();
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);
};
Два практических замечания. Поскольку каждая страница подгоняется независимо, изображения с разными размерами — не проблема: горизонтальная фотография и вертикальный скан могут находиться в одном документе без какой-либо особой обработки. А поскольку весь документ собирается в памяти до SaveToFile, скачивание происходит один раз в конце, независимо от того, сколько изображений было добавлено.
Вам также может понравиться: Сборка изображений — это обратная операция по отношению к рендерингу. Если у вас уже есть PDF и вы хотите получить каждую его страницу в виде картинки, см. Как преобразовать страницы PDF в изображения в JavaScript (React).
Подгонка размера: как вписать изображение в страницу
Есть два разумных способа разместить изображение на странице, и выбор зависит от того, допустима ли потеря части изображения.
Contain (Math.max) |
Cover (Math.min) |
|
|---|---|---|
| Что делает | Масштабирует, пока всё изображение не поместится | Масштабирует, пока страница не заполнится |
| Всё изображение видно | Да | Нет — выходящее за края обрезается |
| Пустое пространство | Возможно, по одной оси | Отсутствует |
| Подходит для | Сканов, документов, всего, что должно оставаться целым | Фотографий на всю страницу, обложек, слайдов |
В первом примере используется contain — вспомогательная функция fitContain. Переход к cover — это зеркальное отражение этой функции: Math.min вместо Math.max, заполнение страницы и предоставление холсту обрезать всё, что выходит за края, при этом смещения центрирования становятся отрицательными:
// Cover: fill the page, clipping whatever overflows
function fitCover(imgW, imgH, pageW, pageH) {
const rate = Math.min(imgW / pageW, imgH / pageH);
const width = imgW / rate;
const height = imgH / rate;
return {
width, height,
x: (pageW - width) / 2, // negative when the image is wider than the page
y: (pageH - height) / 2 // negative when it is taller
};
}
Если вместо вывода от края до края вы хотите видимые поля, уменьшайте не изображение, а полезную область — передайте бюджет с учётом полей в ту же вспомогательную функцию fitContain:
const margin = 36; // 36 points = 0.5 inch
let usableWidth = page.Canvas.ClientSize.Width - margin * 2;
let usableHeight = page.Canvas.ClientSize.Height - margin * 2;
let box = fitContain(
image.PhysicalDimension.Width, image.PhysicalDimension.Height,
usableWidth, usableHeight
);
page.Canvas.DrawImage({ image: image, x: box.x, y: box.y, width: box.width, height: box.height });
Об PhysicalDimension стоит знать одно: оно отражает физический размер изображения, который не всегда совпадает с его размером в пикселях. Фотография 4000 × 3000, сохранённая с другим значением DPI, вернёт не те числа, которых вы могли бы ожидать. Именно поэтому подход на основе коэффициентов, описанный выше, надёжнее жёсткого задания размеров в пикселях — он работает независимо от того, как было помечено изображение.
Загрузка изображений из памяти
PdfImage.FromFile ожидает, что изображение уже находится в VFS. Но ваши изображения не всегда там — ответ API, blob из базы данных или экспорт из canvas дают вам байты в памяти. PdfImage.FromStream принимает эти байты напрямую.
// Read image bytes from VFS and build a memory stream
let bytes = window.dotnetRuntime.Module.FS.readFile(inputFileName);
let stream = new pdfModule.Stream(bytes);
// Load the image from the memory stream
let image = pdfModule.PdfImage.FromStream(stream);
Далее всё так же, как с любым другим изображением — вычислите размер и нарисуйте его:
let page = doc.Pages.Add();
let box = fitContain(
image.PhysicalDimension.Width, image.PhysicalDimension.Height,
page.Canvas.ClientSize.Width, page.Canvas.ClientSize.Height
);
page.Canvas.DrawImage({ image: image, x: box.x, y: box.y, width: box.width, height: box.height });
Одна и та же форма bytes подходит независимо от источника. Если ваши изображения приходят как ArrayBuffer из fetch, оберните его в Uint8Array перед созданием потока:
const response = await fetch('/api/images/invoice-001');
const bytes = new Uint8Array(await response.arrayBuffer());
let stream = new pdfModule.Stream(bytes);
let image = pdfModule.PdfImage.FromStream(stream);
Это шаблон, к которому стоит прибегать, когда PDF собирается из изображений, инициированных на сервере, загруженных пользователем файлов, хранящихся в состоянии, или чего-либо, сгенерированного во время выполнения на canvas — без обращения к VFS.
Рекомендуемая статья: Spire.PDF умеет также рисовать на страницах уже открытого PDF, а не только в новых документах, как выше. О размещении изображений в существующем документе см. Как добавить изображения в PDF в JavaScript (React).
Типичные проблемы
Изображение получается растянутым или сжатым.
Почти всегда причина в двух разных коэффициентах масштабирования. Вычислите единый fitRate и разделите на него и ширину, и высоту — никогда не масштабируйте оси независимо.
Изображение крошечное посреди большой пустой страницы. Это ожидаемо, когда соотношение сторон изображения сильно отличается от соотношения сторон страницы. Панорамная фотография на вертикальной странице всегда оставит полосы сверху и снизу. Либо смиритесь с этим (правильно для документов), либо переключитесь на cover, либо используйте вариант с полями, чтобы хотя бы пустое пространство оставалось симметричным.
Изображение обрезано по краям.
Вы используете поведение cover — намеренно или нет. Проверьте, использовал ли fitRate функцию Math.min; переключитесь на Math.max, если всё изображение должно быть видно.
Фотография высокого разрешения даёт огромный PDF.
Изображение встраивается со своим собственным разрешением. Если размер файла важен, уменьшите масштаб перед отрисовкой — нарисуйте его на canvas нужного размера, экспортируйте и используйте эти байты с PdfImage.FromStream.
При первом нажатии ничего не происходит.
Модуль WASM загружается асинхронно. Проверка if (!pdfModule) return; существует именно для этого; в настоящем приложении блокируйте кнопку до готовности модуля, а не показывайте alert.
Часто задаваемые вопросы
Можно ли вставлять изображения в существующий PDF вместо создания нового?
Да. Примеры здесь создают новый документ, но вы можете открыть существующий PDF и рисовать на его страницах точно так же. См. Как добавить изображения в PDF в JavaScript (React) для этого сценария.
Какие форматы изображений можно загружать?
Распространённые растровые форматы — PNG, JPEG, BMP и подобные — поддерживаются PdfImage.FromFile и PdfImage.FromStream. Используйте FromStream, когда формат неизвестен на этапе сборки или байты приходят из сетевого ответа.
Можно ли управлять порядком страниц?
Да. Страницы создаются в том порядке, в котором вы вызываете Pages.Add(), поэтому сортировка массива имён файлов сортирует и результат. Именно на этом механизме основаны интерфейсы с перетаскиванием для изменения порядка: измените порядок массива — пересоберите PDF.
Требуется ли здесь серверная часть?
Нет. Документ собирается в браузере модулем WebAssembly, а готовый PDF возвращается в виде байтов, которые вы превращаете в Blob. Изображения никогда не покидают устройство.
Можно ли смешивать вертикальные и горизонтальные изображения в одном PDF?
Да. Каждая страница подгоняется и рисуется независимо, поэтому вертикальный скан и горизонтальная фотография могут стоять рядом. Если вам нужна единая ориентация страниц, это повод использовать фиксированный размер страницы и позволить изображениям масштабироваться под него.
У меня есть PDF, и я хочу получить его страницы в виде изображений, а не наоборот.
Это обратная операция — рендеринг, а не сборка. См. Как преобразовать страницы PDF в изображения в JavaScript (React).
Нужно ли помещать изображение в VFS?
Только для FromFile. FromStream принимает байты из любого источника — ответа fetch, экспорта из canvas или состояния — и полностью обходится без VFS.
См. также
Рецепты сборки здесь создают совершенно новый документ. Если ваши изображения нужно поместить в существующий PDF — нарисовать на уже имеющихся страницах — см. Как добавить изображения в PDF в JavaScript (React). Другие полезные части конвейера «изображение → PDF»: