Разработка ПО

Как генерировать PDF на Rust с помощью Skia: полное руководство по крейту skia-safe

Илья НиксанИлья Никсан
21 марта 2026 г.
Как генерировать 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 используют кодирование по идентификаторам глифов для точного рендеринга.