Как генерировать PDF на Rust с помощью Skia: полное руководство по крейту skia-safe
Илья Никсан
Программная генерация PDF — одна из тех задач, которые кажутся простыми, пока не начнёшь их решать. Большинство библиотек либо дают низкоуровневый поток байтов, с которым приходится бороться, либо шаблонизатор высокого уровня, который ломается, как только требуется точный контроль над вёрсткой, типографикой или графикой. Но есть золотая середина — тот же графический движок, который рендерит каждую веб-страницу при печати в PDF в Chrome, каждый экран Android и каждый кадр приложения на Flutter: Google Skia.
В этом руководстве разберём, как использовать Skia из Rust через крейт skia-safe для создания векторных PDF с полным контролем над текстом, формами, изображениями и многостраничной вёрсткой.
Ключевые выводы
- Skia — это open-source библиотека 2D-графики от Google, в активной разработке с 2005 года, с более чем 2 миллионами загрузок Rust-обёрток на crates.io.
- Крейт skia-safe (v0.93) предоставляет безопасные, идиоматичные Rust-обёртки с паттерном typestate, который предотвращает создание некорректных PDF на этапе компиляции.
- Skia создаёт настоящие векторные PDF с выделяемым текстом, а не растеризованные изображения — тот же подход, который Chrome использует для печати в PDF.
- Поддерживает архивное соответствие PDF/A, теговые/доступные PDF (PDF/UA), подмножества шрифтов через HarfBuzz и многостраничные документы с разными размерами страниц.
- Предсобранные бинарники поставляются по умолчанию, поэтому первая сборка занимает менее одной минуты вместо 20-40 минут из исходников.
Что такое Skia и зачем использовать её для генерации PDF на Rust
Skia — это open-source библиотека 2D-графики, которую поддерживает Google. Она находится в активной разработке с 2005 года и является одним из самых проверенных в бою движков рендеринга. Когда вы печатаете веб-страницу в PDF в Google Chrome, PDF-бэкенд Skia транслирует отрисованную страницу в PDF-файл. Когда Android отрисовывает свой интерфейс — за это отвечает Skia. Flutter использует Skia в качестве движка рендеринга для кроссплатформенных приложений.
Что делает Skia привлекательной для программной генерации PDF на Rust:
- Настоящий векторный вывод. Текст остаётся выделяемым, фигуры сохраняют чёткость при любом масштабе, а размеры файлов остаются разумными — потому что Skia транслирует свои операции рисования в нативные PDF-примитивы, а не растеризует пиксели.
- Полноценный 2D-графический API. Кривые Безье, градиенты, аффинные преобразования, области отсечения, режимы наложения, типографические элементы — всё это напрямую отображается в PDF.
- Теговые и доступные PDF. Skia поддерживает деревья структурных элементов PDF и идентификаторы узлов, что позволяет создавать документы, соответствующие стандартам доступности PDF/UA.
- Соответствие PDF/A. Один флаг в метаданных включает вывод PDF/A-2b для долгосрочного хранения документов — обязательное требование для юридических, медицинских и государственных документов.
- Подмножества шрифтов. При встраивании шрифтов Skia удаляет неиспользуемые глифы через HarfBuzz, сохраняя компактный размер файлов.
Настройка крейта skia-safe для генерации PDF на Rust
Крейт skia-safe предоставляет безопасные, идиоматичные Rust-обёртки для Skia. Он оборачивает C++-библиотеку через FFI, предоставляя API, который ощущается естественно в Rust — с семантикой владения, гарантиями времени жизни и паттерном typestate, который отлавливает целые категории ошибок на этапе компиляции.
Добавьте в проект:
[dependencies]
skia-safe = "0.93"
Фича pdf включена по умолчанию, дополнительных флагов не нужно. По умолчанию крейт скачивает предсобранные бинарники Skia для вашей платформы, поэтому первая сборка занимает меньше минуты, а не 20-40 минут, которые потребовались бы для компиляции Skia из исходников.
Как сгенерировать PDF-файл на Rust с помощью Skia
Основная концепция проста: создать документ, начать страницу, рисовать на холсте, завершить страницу, закрыть документ. Вот минимальный пример, создающий одностраничный PDF:
use skia_safe::pdf;
fn main() {
let mut output = Vec::new();
// Создаём документ — US Letter (612 x 792 точек)
let mut document = pdf::new_document(&mut output, None)
.begin_page((612, 792), None);
let canvas = document.canvas();
// Рисуем заполненный прямоугольник
let mut paint = skia_safe::Paint::default();
paint.set_color(skia_safe::Color::from_rgb(41, 98, 255));
canvas.draw_rect(
skia_safe::Rect::from_xywh(72.0, 72.0, 468.0, 100.0),
&paint,
);
// Рисуем текст
let font = skia_safe::Font::default();
paint.set_color(skia_safe::Color::WHITE);
canvas.draw_str("Hello from Skia + Rust", (90.0, 130.0), &font, &paint);
// Финализируем
document.end_page().close();
std::fs::write("hello.pdf", &output).unwrap();
}
Запустите, откройте hello.pdf — и вы увидите синий прямоугольник с белым текстом. Всё в виде векторной графики, полностью выделяемой и масштабируемой.

Безопасность на этапе компиляции с паттерном typestate
Одна из самых элегантных особенностей Rust-обёрток — паттерн typestate для типа Document. Документ переходит между двумя состояниями: Open (готов начать страницу) и OnPage (активно рисуем).
// Document<state::Open> — можно начать страницу или закрыть документ
let document = pdf::new_document(&mut output, None);
// Document<state::OnPage> — можно обращаться к холсту или завершить страницу
let page = document.begin_page((612, 792), None);
let canvas = page.canvas();
// Обратно к Document<state::Open> — можно начать другую страницу или закрыть
let document = page.end_page();
document.close();
Если вы попытаетесь вызвать canvas() у документа без открытой страницы или попытаетесь close() документ при активной странице — код не скомпилируется. Это не проверка времени выполнения и не assertion — это гарантия, обеспечиваемая системой типов Rust. Вы не можете создать некорректный PDF, потому что компилятор это предотвращает.
Сравните с библиотеками PDF на C++ или Python, где такие ошибки приводят к повреждённому выводу, тихим сбоям или падениям во время выполнения. В Rust с skia-safe компилятор — ваш корректор.
Добавление метаданных PDF для SEO и доступности
Метаданные PDF — это то, что поисковые системы, системы управления документами и инструменты доступности используют для понимания вашего документа. Skia предоставляет их через структуру Metadata:
use skia_safe::pdf::{self, Metadata};
let metadata = Metadata {
title: "Финансовый отчёт Q4".to_string(),
author: "Финансовый отдел".to_string(),
subject: "Квартальные результаты и прогнозы".to_string(),
creator: "Генератор отчётов v2.1".to_string(),
..Default::default()
};
let mut output = Vec::new();
let document = pdf::new_document(&mut output, Some(&metadata));
Для архивного соответствия включите PDF/A одним флагом:
let metadata = Metadata {
pdf_a: true,
..Default::default()
};
Это создаёт вывод, соответствующий PDF/A-2b — подходящий для юридических документов, медицинских записей и любого контента, который должен оставаться читаемым через десятилетия.
Создание многостраничных PDF-документов на Rust
Реальные документы редко бывают одностраничными. Skia естественно работает с многостраничными PDF — каждый вызов begin_page начинает новую страницу, причём страницы могут иметь разные размеры:
let mut output = Vec::new();
let document = pdf::new_document(&mut output, None);
// Страница 1 — A4 портретная
let mut page = document.begin_page((595, 842), None);
draw_cover_page(page.canvas());
let document = page.end_page();
// Страница 2 — A4 альбомная
let mut page = document.begin_page((842, 595), None);
draw_data_table(page.canvas());
let document = page.end_page();
// Страница 3 — снова портретная
let mut page = document.begin_page((595, 842), None);
draw_charts(page.canvas());
let document = page.end_page();
document.close();
Смешанные ориентации и размеры страниц в одном документе — то, с чем многие PDF-библиотеки испытывают трудности — работает из коробки.
Рисование фигур, текста и путей в Skia PDF
Canvas в Skia — это полноценная 2D-поверхность для рисования. Всё, что вы рисуете на ней, преобразуется в векторные PDF-примитивы.
Фигуры и пути
let canvas = page.canvas();
let mut paint = Paint::default();
paint.set_anti_alias(true);
// Заполненный прямоугольник
paint.set_color(Color::from_rgb(59, 130, 246));
paint.set_style(skia_safe::PaintStyle::Fill);
canvas.draw_rect(Rect::from_xywh(50.0, 50.0, 200.0, 100.0), &paint);
// Обведённый круг
paint.set_style(skia_safe::PaintStyle::Stroke);
paint.set_stroke_width(2.0);
canvas.draw_circle((300.0, 100.0), 50.0, &paint);
// Произвольный путь Безье
let mut path = skia_safe::Path::new();
path.move_to((400.0, 50.0));
path.cubic_to((450.0, 20.0), (500.0, 80.0), (550.0, 50.0));
path.line_to((550.0, 150.0));
path.close();
paint.set_style(skia_safe::PaintStyle::Fill);
canvas.draw_path(&path, &paint);
Рендеринг текста с выделяемым выводом
let typeface = skia_safe::Typeface::from_name("Helvetica", skia_safe::FontStyle::bold())
.unwrap_or_else(skia_safe::Typeface::default);
let font = skia_safe::Font::from_typeface(&typeface, 24.0);
let mut paint = Paint::default();
paint.set_color(Color::BLACK);
canvas.draw_str("Заголовок раздела", (72.0, 72.0), &font, &paint);
// Текст поменьше для основного содержимого
let body_font = skia_safe::Font::from_typeface(&typeface, 12.0);
canvas.draw_str("Основной текст.", (72.0, 100.0), &body_font, &paint);
Текст в итоговом PDF выделяем и доступен для поиска — это не растеризованные изображения символов.
Аффинные трансформации
canvas.save();
canvas.translate((200.0, 300.0));
canvas.rotate(45.0, None);
canvas.draw_rect(Rect::from_xywh(-50.0, -25.0, 100.0, 50.0), &paint);
canvas.restore();
Трансформации компонуемы и транслируются напрямую в матрицу преобразования PDF — без растеризации.
Встраивание изображений в PDF, сгенерированные на Rust
Встраивание изображений в PDF — это то, где многие библиотеки добавляют лишний объём файлу. Skia обрабатывает это интеллектуально:
let image_data = std::fs::read("photo.jpg").unwrap();
let data = skia_safe::Data::new_copy(&image_data);
let image = skia_safe::Image::from_encoded(data).expect("Не удалось декодировать изображение");
canvas.draw_image(&image, (72.0, 200.0), None);
Когда поддержка JPEG-кодирования настроена в метаданных, Skia передаёт JPEG-данные напрямую в PDF без перекодирования — сохраняя качество и минимальный размер файла. Без этого изображения сжимаются deflate-алгоритмом, что может значительно увеличить размер файла для фотографий.
Управление через метаданные:
let metadata = Metadata {
encoding_quality: 85, // Качество JPEG (101 = без потерь/deflate)
..Default::default()
};
Сравнение Rust-библиотек для генерации PDF: skia-safe vs printpdf vs krilla
Экосистема Rust предлагает несколько подходов к генерации PDF. Сравнение:
| Библиотека | Тип | Вектор | Подмножества шрифтов | PDF/A | Теговый PDF | Движок вёрстки | Зрелость |
|---|---|---|---|---|---|---|---|
| skia-safe | Обёртка Skia (C++ FFI) | Да | Да (HarfBuzz) | Да | Да | Нет (canvas API) | 20+ лет (Skia) |
| printpdf | Чистый Rust | Да | Ограничено | Нет | Нет | Нет | Стабильный |
| genpdf | Чистый Rust (на printpdf) | Да | Ограничено | Нет | Нет | Базовый | Не поддерживается |
| krilla | Чистый Rust (на pdf-writer) | Да | Да (OpenType) | Да | Да | Нет | Новее |
| Headless Chrome | Браузерный runtime | Да | Да | Нет | Нет | Полный HTML/CSS | Зрелый |
skia-safe даёт полноценный 2D-графический API с более чем 20-летним опытом разработки в Google. Векторный вывод, подмножества шрифтов, теговые PDF, поддержка PDF/A. Компромисс — зависимость от C++, хотя предсобранные бинарники делают это безболезненным на практике.
printpdf — чистый Rust без C-зависимостей. Хорошо работает для простых документов, но требует ручного позиционирования каждого элемента. Нет движка вёрстки, ограниченная поддержка типографики.
genpdf строит API вёрстки более высокого уровня поверх printpdf. Чистый Rust и проще в использовании для простых документов, но проект не поддерживается уже несколько лет.
krilla — более новая библиотека на чистом Rust с современным API, отличной поддержкой OpenType и функциями вроде градиентов и режимов наложения. Сильный выбор, если избежание C-зависимостей — приоритет, хотя она менее проверена, чем Skia.
Headless Chrome / Puppeteer даёт полный рендеринг HTML/CSS, но требует поставки браузерного runtime. Тяжеловесно, но полезно, когда нужно рендерить сложные веб-макеты.
Выбор зависит от ваших ограничений. Если вам нужен точный контроль над графикой и типографикой, проверенная надёжность, и вас устраивает C++-зависимость — Skia является наиболее мощным вариантом. Если нужно решение на чистом Rust — стоит оценить krilla или printpdf.
Известные ограничения PDF-бэкенда Skia
PDF-бэкенд Skia всеобъемлющ, но у него есть границы, о которых стоит знать:
Поддержка режимов наложения. Большинство стандартных PDF-режимов наложения работают (Multiply, Screen, Overlay и т.д.), но некоторые специфичные для Skia режимы, такие как SrcATop, DstATop, Xor и Plus, не имеют PDF-эквивалента. Если ваш рисунок использует их, вывод может отличаться от того, что вы видите на экране.
Растеризация как запасной вариант. Функции без нативного PDF-представления — такие как перспективные трансформации текста — растеризуются с DPI, указанным в метаданных (raster_dpi, по умолчанию 72). Для печатного качества увеличьте до 300 и выше, с компромиссом в виде больших файлов.
Сборка из исходников. Если вам нужно настроить функции Skia за пределами того, что предлагают предсобранные бинарники, сборка из исходников занимает 20-40 минут. Фича binary-cache (включена по умолчанию) позволяет этого избежать для стандартных конфигураций.
Продакшен-архитектура для генерации PDF на Rust
Для производственных систем, генерирующих множество документов — счетов, отчётов, сертификатов — чистая архитектура разделяет контент и рендеринг:
struct DocumentContent {
title: String,
sections: Vec<Section>,
footer: String,
}
struct Section {
heading: String,
body: String,
charts: Vec<ChartData>,
}
fn render_document(content: &DocumentContent) -> Vec<u8> {
let mut output = Vec::new();
let metadata = Metadata {
title: content.title.clone(),
creator: "My Document Engine".to_string(),
..Default::default()
};
let mut doc = pdf::new_document(&mut output, Some(&metadata));
for section in &content.sections {
let mut page = doc.begin_page((595, 842), None); // A4
render_section(page.canvas(), section);
doc = page.end_page();
}
doc.close();
output
}
Такое разделение означает, что ваша бизнес-логика никогда не касается PDF API напрямую. Контент поступает на вход, байты — на выход. Вы можете тестировать рендеринг независимо, заменить PDF-бэкенд без изменения бизнес-логики и параллелизировать генерацию документов между потоками — поскольку каждый Document владеет своим выходным буфером.
С чего начать генерацию PDF на Rust через Skia
Добавьте skia-safe в ваш Cargo.toml, напишите несколько строк кода для рисования и откройте полученный PDF. Кривая обучения пологая, если вы использовали любой 2D-графический API — Canvas, Cairo, Core Graphics или даже HTML5 Canvas. Концепции переносятся напрямую.
Крейт skia-safe находится на версии 0.93 с более чем 2 миллионами загрузок на crates.io. Кеш предсобранных бинарников означает, что первая сборка быстрая, а паттерн typestate гарантирует, что ваш первый PDF будет корректным.
Для приложений, которым нужен надёжный, качественный PDF-вывод — будь то счета, отчёты, билеты, сертификаты или любой документ, где точность вёрстки имеет значение — Skia через Rust даёт вам тот же движок рендеринга, которому доверяют миллиарды устройств, с гарантиями безопасности, которыми славится Rust.
Именно этот подход мы использовали при создании dxpdf — нашего open-source конвертера DOCX в PDF, написанного на Rust и использующего Skia. Он конвертирует документы Word в высококачественные PDF за ~115 мс без необходимости установки Microsoft Office или LibreOffice.
Часто задаваемые вопросы
Какая лучшая Rust-библиотека для генерации PDF?
Зависит от ваших требований. skia-safe — наиболее функционально полный вариант, предлагающий векторный вывод, подмножества шрифтов, теговые PDF и поддержку PDF/A через движок Google Skia. Для чистого Rust без C-зависимостей krilla и printpdf — сильные альтернативы. Для рендеринга HTML/CSS в PDF headless Chrome остаётся наиболее надёжным подходом.
Skia создаёт векторные или растеризованные PDF?
Skia создаёт настоящие векторные PDF. Текст выделяем и доступен для поиска, фигуры остаются чёткими при любом масштабе, а операции рисования транслируются в нативные PDF-примитивы. Растеризация происходит только как запасной вариант для функций, не имеющих нативного PDF-представления, таких как перспективные трансформации.
Можно ли создавать доступные и PDF/A-совместимые документы с skia-safe?
Да. Skia поддерживает деревья структурных элементов PDF и идентификаторы узлов для создания теговых, доступных PDF, соответствующих стандартам PDF/UA. Для архивного соответствия установка pdf_a: true в структуре Metadata создаёт вывод, соответствующий PDF/A-2b.
Сколько времени занимает сборка skia-safe?
С включённой по умолчанию фичей binary-cache первая сборка занимает менее одной минуты, поскольку для вашей платформы скачиваются предсобранные бинарники Skia. Без кеша (сборка Skia из C++-исходников) — ожидайте 20-40 минут.
Это тот же движок, который Chrome использует для печати в PDF?
Да. Когда вы используете функцию Chrome «Сохранить как PDF» или «Печать в PDF», браузер рендерит страницу через PDF-бэкенд Skia — тот же движок, который skia-safe предоставляет для Rust. Это также движок рендеринга, используемый Android для его интерфейса и Flutter для кроссплатформенных приложений.
Как skia-safe работает со шрифтами в PDF?
Skia встраивает шрифты напрямую в PDF. С интеграцией HarfBuzz (включается через фичу textlayout) выполняется подмножественное включение шрифтов — удаление неиспользуемых глифов для поддержания компактного размера файлов. Шрифты TrueType используют кодирование по идентификаторам глифов для точного рендеринга.