
Вы создаёте список документов для клиента, а клиент спрашивает: «можно ли посмотреть страницы перед скачиванием?» И вот у вас уже есть функция рендеринга, которую вы не планировали. Тот же запрос возникает повсюду — полоса миниатюр под списком входящих, карточка предпросмотра, вставленная в чат, слайд, извлечённый из презентации и опубликованный в соцсетях. В каждом случае PDF нельзя показать прямо на странице, но изображение его страниц — можно.
Как только такой переключатель сделан, четыре решения определяют, будет ли работа пригодной к использованию: какие страницы, какой формат, насколько чётко и как файлы попадут к пользователю. Большинство примеров преобразования PDF в изображения отвечают только на первый вопрос и оставляют вам разбираться с остальными уже в продакшене.
Spire.PDF для JavaScript выполняет рендеринг страниц в браузере через WebAssembly и возвращает вам каждую страницу в виде потока изображения. Все четыре этих решения принимаете вы, и каждое из них — это один аргумент или изменённое расширение файла. Файлы перемещаются туда и обратно через виртуальную файловую систему (VFS), так что ничего никуда не загружается.
В этой статье вы узнаете, как:
- Отрендерить каждую страницу PDF в PNG и скачать набор в виде ZIP-архива
- Ограничить преобразование одной страницей или диапазоном страниц
- Переключаться между PNG, JPEG и BMP в зависимости от назначения изображения
- Уравновесить чёткость и размер файла с помощью DPI
- Доставить результат в виде загрузки, отдельных файлов или сразу в ваш интерфейс React
- Исправить искажённый или пустой текст, вызванный отсутствующими шрифтами
Предварительные требования
В этом руководстве предполагается, что у вас есть проект React с установленным Spire.PDF для JavaScript и инициализированным модулем WASM. О настройке см. Интеграция Spire.PDF для JavaScript в проект React.
Вам понадобится:
- PDF-файл, загруженный в VFS
- Модуль WASM, доступный по адресу
window.wasmModule.spirepdf - Пакет
jszip, если вы хотите объединить несколько страниц в одну загрузку (npm install jszip)
Преобразование каждой страницы в PNG
Случай с полным документом — тот, к которому вы будете обращаться чаще всего: открыть PDF, пройти по каждой странице, отрендерить каждую в поток, записать каждый поток в папку в VFS, затем заархивировать эту папку и передать её браузеру для скачивания.
import JSZip from "jszip";
function App() {
const convertToImage = 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 PDF file and fonts into VFS
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
const inputFileName = 'Flowers.pdf';
await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);
// Create PdfDocument object and load the PDF document
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile(inputFileName);
// Create an output directory to hold the converted images
let outputDirectoryName = "ImagesFolders/";
window.dotnetRuntime.Module.FS.mkdirTree(outputDirectoryName);
// Loop through each page and save it as an image
for (let i = 0; i < doc.Pages.Count; i++) {
const outputFileName = outputDirectoryName + "ConvertedImages_" + i + ".png";
let stream = doc.SaveAsImage({ pageIndex: i });
stream.Save(outputFileName);
stream.Dispose();
}
doc.Dispose();
// Read the converted files from VFS and trigger download
const zip = new JSZip();
let items = await window.dotnetRuntime.Module.FS.readdir(outputDirectoryName);
items = items.filter((item) => item !== "." && item !== "..");
for (const item of items) {
const itemPath = `${outputDirectoryName}/${item}`;
const fileData = await window.dotnetRuntime.Module.FS.readFile(itemPath);
zip.file(item, fileData);
}
// Convert the ZIP to a Blob and trigger the browser download
const zipBlob = await zip.generateAsync({ type: "blob" });
const url = URL.createObjectURL(zipBlob);
const a = document.createElement('a');
a.href = url;
a.download = 'ImagesFolders';
a.click();
URL.revokeObjectURL(url);
};
return (
<div style={{ textAlign: 'center', height: '300px' }}>
<h1>Convert PDF To Image</h1>
<button onClick={convertToImage}>
Generate
</button>
</div>
);
}
export default App;
Каждая страница PDF экспортируется как изображение PNG через SaveAsImage и упаковывается в ZIP-файл для скачивания

Что делает код:
-
doc.SaveAsImage({ pageIndex: i })выполняет рендеринг одной страницы и возвращает поток изображения. Индексы страниц начинаются с нуля, поэтомуpageIndex: 0— это первая страница. -
stream.Save(outputFileName)записывает этот поток в VFS. Расширение, которое вы здесь указываете, задаёт формат — подробнее об этом ниже. -
stream.Dispose()освобождает поток перед рендерингом следующей страницы. Если пропустить это на длинном документе, память будет расходоваться впустую без всякой пользы. - Последовательность
FS.mkdirTree→FS.readdir→FS.readFileсуществует потому, что модуль WASM записывает файлы в свою собственную файловую систему, а не в папку загрузок браузера. Именно JSZip превращает эту папку во что-то, что пользователь может сохранить. - Рендеринг работает только для чтения. Исходный PDF открывается, читается и освобождается; он никогда не изменяется.
Примечание: Это создаёт изображение каждой страницы, что и нужно для предпросмотров, миниатюр и обмена. Если же вы хотите получить оригинальные файлы изображений, встроенные в PDF, — фактический график или фотографию в их собственном разрешении, а не снимок страницы, на которой они находятся, — это другая операция: см. Извлечение изображений из PDF в JavaScript (React).
Преобразование только определённых страниц
Рендеринг документа из 200 страниц ради создания трёх миниатюр — это напрасная работа. Поскольку обход страниц — это всего лишь цикл for по doc.Pages.Count, сузить его — вопрос изменения границ.
Одна страница — обложка или конкретный экспонат:
// Render only the first page
const outputFileName = outputDirectoryName + "cover.png";
let stream = doc.SaveAsImage({ pageIndex: 0 });
stream.Save(outputFileName);
stream.Dispose();
Непрерывный диапазон — страницы с 3 по 5, с использованием индексов от 2 до 4 (начиная с нуля):
const startPage = 2;
const endPage = 4;
for (let i = startPage; i <= endPage && i < doc.Pages.Count; i++) {
let stream = doc.SaveAsImage({ pageIndex: i });
stream.Save(outputDirectoryName + "page_" + (i + 1) + ".png");
stream.Dispose();
}
Вручную выбранный набор — страницы, которые не идут друг за другом:
const pagesToRender = [0, 7, 12];
for (const pageIndex of pagesToRender) {
if (pageIndex >= doc.Pages.Count) continue;
let stream = doc.SaveAsImage({ pageIndex });
stream.Save(outputDirectoryName + "page_" + (pageIndex + 1) + ".png");
stream.Dispose();
}
Проверка i < doc.Pages.Count важна, когда список страниц поступает от пользователя, а не задан константой в коде — индекс за пределами диапазона — это ошибка времени выполнения, которую вы вряд ли захотите выпустить в продакшен.
Обратите внимание на (i + 1) в именах файлов. Индексы, начинающиеся с нуля, правильны для API и неправильны для людей; именование файлов по номеру страницы, который видит пользователь, избавляет от целого класса обращений в поддержку в будущем.
Рекомендуемая статья: Когда вам нужны оригинальные объекты изображений из PDF, а не отрендеренная страница — встроенный график или фотография в собственном разрешении — см. Извлечение изображений из PDF в JavaScript (React).
Выбор формата вывода
Формат полностью определяется расширением, которое вы передаёте в stream.Save. Больше ничего в конвейере не меняется.
stream.Save(outputDirectoryName + "page_1.png"); // PNG
stream.Save(outputDirectoryName + "page_1.jpg"); // JPEG
stream.Save(outputDirectoryName + "page_1.bmp"); // BMP
| Формат | Расширение | Что вы получаете | Когда его выбирать |
|---|---|---|---|
| PNG | .png |
Без потерь. Края текста и тонкие линии остаются чёткими. Файлы крупнее. | Страницы документов, предпросмотры интерфейса, всё, что содержит текст или диаграммы |
| JPEG | .jpg |
С потерями. Намного меньше. Без прозрачности, и артефакты сжатия заметны вокруг мелкого текста. | Фотографические или отсканированные страницы, вложения электронной почты, доставка с ограничением по размеру |
| BMP | .bmp |
Без сжатия. Очень крупные. | Устаревшие конвейеры, которые специально требуют несжатых растровых изображений |
Практическое правило: если страница в основном текстовая, используйте PNG. Сжатие JPEG настроено на фотографический контент, и на странице с большим количеством текста оно создаёт заметные размытия вокруг краёв символов, которые не устраняются никакими настройками качества. Если страница — это отсканированная фотография, JPEG значительно сократит размер файла с малой видимой ценой.
Настройка разрешения (DPI)
SaveAsImage по умолчанию выполняет рендеринг с разрешением 96 DPI, что соответствует типичному экрану. Это правильная настройка для миниатюр и предпросмотров на экране. Её недостаточно, когда кто-то увеличивает изображение, печатает страницу или подаёт изображение на вход OCR.
Передайте dpiX и dpiY, чтобы изменить это:
// Export each page as an image at 150 DPI
for (let i = 0; i < doc.Pages.Count; i++) {
let stream = doc.SaveAsImage({ pageIndex: i, dpiX: 150, dpiY: 150 });
stream.Save(outputDirectoryName + "highres_" + i + ".png");
stream.Dispose();
}
Разрешение — это не бесплатное улучшение: количество пикселей растёт пропорционально квадрату DPI, поэтому затраты быстро увеличиваются:
| DPI | Относительное количество пикселей по сравнению с 96 DPI | Сопоставимо с | Разумно для |
|---|---|---|---|
| 96 (по умолчанию) | 1× | Стандартный экран | Миниатюры, встроенные предпросмотры, галерейные полосы |
| 150 | ~2,4× | Экран Retina, лёгкое увеличение | Вложения электронной почты, предпросмотры, которые пользователи могут увеличить, черновая печать |
| 300 | ~9,8× | Печатное качество | Печать, входные данные для OCR, архивные мастер-копии |
Из этой таблицы следуют две вещи. Во-первых, переход с 96 на 300 умножает работу и объём хранилища примерно на десять, поэтому не выбирайте 300 для документа из 200 страниц, если кто-то действительно не собирается его печатать. Во-вторых, dpiX и dpiY — это отдельные аргументы не просто так: обычно им задают одинаковое значение, а при разных значениях страница будет растянута.
Упаковка или предпросмотр изображений
Рендеринг страниц — это половина работы; другая половина — донести их до пользователя. Три шаблона покрывают почти всё.
Объединить всё в один ZIP-архив — подход из первого примера. Лучше всего подходит, когда пользователю нужна папка с файлами, и это единственный вариант, который масштабируется на десятки страниц, не засыпая браузер запросами на скачивание.
Скачать одно изображение напрямую — JSZip не нужен, когда файл только один:
const outputFileName = 'cover.png';
let stream = doc.SaveAsImage({ pageIndex: 0 });
stream.Save(outputFileName);
stream.Dispose();
const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
const blob = new Blob([fileArray], { type: 'image/png' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = outputFileName;
a.click();
URL.revokeObjectURL(url);
Оставить изображения внутри вашего интерфейса React — вообще без скачивания. Это правильный выбор для панелей предпросмотра и полос миниатюр, где изображения временны, а папка загрузок, полная файлов page_1.png, была бы просто беспорядком:
const renderPreviewStrip = async () => {
const pdfModule = window.wasmModule?.spirepdf;
if (!pdfModule) return;
await window.spire.FetchFileToVFS('Flowers.pdf', "", `${process.env.PUBLIC_URL}/data/`);
let doc = new pdfModule.PdfDocument();
doc.LoadFromFile('Flowers.pdf');
const previewUrls = [];
for (let i = 0; i < doc.Pages.Count; i++) {
const tempName = `preview_${i}.png`;
let stream = doc.SaveAsImage({ pageIndex: i, dpiX: 72, dpiY: 72 });
stream.Save(tempName);
stream.Dispose();
const fileArray = window.dotnetRuntime.Module.FS.readFile(tempName);
const blob = new Blob([fileArray], { type: 'image/png' });
previewUrls.push(URL.createObjectURL(blob));
}
doc.Dispose();
// previewUrls can now be rendered as <img src={url} /> in a thumbnail strip
return previewUrls;
};
Обратите внимание на dpiX: 72 в случае предпросмотра: миниатюрам не нужно полное разрешение, и их рендеринг в меньшем размере сохраняет отзывчивость полосы. Не забудьте вызывать URL.revokeObjectURL(url) для каждого URL при размонтировании компонента, иначе вы будете терять память при каждом повторном рендере.
Вам также может понравиться: Работа с набором изображений, а не с PDF — сборка сканов или фотографий в один документ? Это Преобразование изображений в PDF в JavaScript (React).
Исправление искажённого текста: загрузка шрифтов
PDF обычно встраивает используемые шрифты, но не всегда — и когда он этого не делает, средству рендеринга приходится что-то подставлять. В браузере нет библиотеки шрифтов операционной системы, к которой можно обратиться, поэтому отсутствующий шрифт превращается в пустое место или квадраты.
Решение — поместить файл шрифта туда, где среда выполнения WASM ожидает его найти, перед рендерингом:
// Load a TrueType font into the VFS font directory before converting
await window.spire.FetchFileToVFS('ARIAL.TTF', '/Library/Fonts/', `${process.env.PUBLIC_URL}/font/`);
О ARIALUNI.TTF стоит знать: он охватывает распространённые символы CJK, а также латиницу, что делает его более безопасным вариантом по умолчанию, когда ваши пользователи загружают документы на языках, которые вы не перечисляли. Если вы всегда имеете дело только с английскими документами, меньший шрифт сделает загрузку легче.
Поскольку это выполняется до рендеринга любой страницы, одной проверки отсутствующих шрифтов в начале вашей функции преобразования достаточно — вам не нужно перезагружать шрифты для каждой страницы.
Часто задаваемые вопросы
Изменяет ли преобразование PDF в изображения исходный PDF?
Нет. SaveAsImage выполняет рендеринг страниц и возвращает новые данные изображения; исходный документ открывается только для чтения и затем освобождается. Ваш исходный PDF остаётся нетронутым.
Как преобразовать только первую страницу?
Вызовите SaveAsImage один раз с pageIndex: 0 вместо цикла. См. Преобразование только определённых страниц.
Почему мой результат больше исходного PDF?
Потому что отрендеренная страница хранит пиксели, тогда как PDF хранит инструкции по отрисовке. Страница PDF с большим количеством текста часто занимает несколько килобайт; PNG той же страницы в 300 DPI может занимать мегабайты. Снизьте до 96–150 DPI или переключитесь на JPEG, если размер важнее точности.
Могу ли я показывать изображения в моём приложении вместо их скачивания?
Да — создайте объектные URL из данных изображения и используйте их как источники для <img>. См. Упаковка или предпросмотр изображений. Только не забудьте отозвать URL, когда закончите.
Вызовет ли проблемы очень длинный документ?
Страницы рендерятся по одной внутри цикла, поэтому модуль WASM хранит примерно одно изображение страницы за раз, а не весь документ. Для чрезвычайно длинных файлов преобразуйте по частям и добавляйте в архив по ходу, а не храните каждое изображение в массиве.
В чём разница между преобразованием страницы в изображение и извлечением изображений из PDF?
Преобразование рендерит всю страницу — текст, макет, изображения, фон — как одну плоскую картинку. Извлечение вытаскивает оригинальные объекты изображений, встроенные в документ, в их собственном разрешении. Используйте преобразование для предпросмотров и обмена; используйте извлечение, когда вам нужна сама исходная графика.
Куда на самом деле попадают преобразованные файлы?
В виртуальную файловую систему WASM, а не на ваш сервер и не на диск пользователя. Ничего не покидает браузер — единственный способ, которым файлы попадают к пользователю, — это шаг скачивания или создания объектного URL, который вы пишете сами.
См. также
Ищете обратное направление — превращение изображений обратно в PDF? Это Преобразование изображений в PDF в JavaScript (React). Остальная часть конвейера работы со страницами PDF в React: