Генерация PDF отчётов на Rust: библиотека printpdf

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Генерация PDF отчётов на Rust с библиотекой printpdf

В современном мире программирования Rust всё чаще используется для создания высокопроизводительных серверных приложений и инструментов командной строки. Одной из востребованных задач является генерация PDF-отчётов — будь то аналитические дашборды, счета-фактуры или документация. Библиотека printpdf предоставляет лёгкий и надёжный способ создавать PDF-документы на чистом Rust без зависимостей от внешних систем (например, LaTeX или wkhtmltopdf).

Что такое printpdf?

printpdf — это pure-Rust библиотека для генерации PDF-документов с нуля. Она поддерживает:

  • Текст с различными шрифтами (включая встраиваемые TrueType/OpenType).
  • Векторную графику (линии, прямоугольники, эллипсы).
  • Изображения (PNG, JPEG) с прозрачностью.
  • Таблицы и сложное форматирование через ручное позиционирование.
  • Страницы любого размера (A4, Letter, кастомные).

Библиотека не использует внешние процессы, что делает её идеальной для встраивания в веб-сервисы, CLI-утилиты или десктопные приложения на Rust.

Установка

Добавьте в Cargo.toml:

[dependencies]
printpdf = "0.7"

Также вам понадобится крейт rusttype для работы со шрифтами (он автоматически подтягивается как зависимость).

Базовый пример: создание простого отчёта

Напишем программу, которая создаёт PDF с заголовком, текстом и линией. Результат сохраним в файл report.pdf.

use printpdf::*;
use std::fs::File;
use std::io::BufWriter;

fn main() {
    // Создаём документ формата A4
    let (doc, page1, layer1) = PdfDocument::new(
        "Мой отчёт",
        Mm(210.0),
        Mm(297.0),
        "Layer 1",
    );

    // Получаем шрифт (встроенный Helvetica)
    let font = doc.add_builtin_font(BuiltinFont::Helvetica).unwrap();

    // Начинаем рисовать на первом слое
    let current_layer = doc.get_page(page1).get_layer(layer1);

    // Текст: заголовок
    current_layer.use_text(
        "Отчёт о продажах",
        24.0,
        Mm(20.0),
        Mm(270.0),
        &font,
    );

    // Текст: обычный
    current_layer.use_text(
        "Дата отчёта: 2025-03-15",
        12.0,
        Mm(20.0),
        Mm(250.0),
        &font,
    );

    // Рисуем линию-разделитель
    current_layer.set_outline_color(Color::Rgb(Rgb::new(0.5, 0.5, 0.5)));
    current_layer.set_stroke_width(1.0);
    current_layer.add_line(
        Point::new(Mm(20.0), Mm(240.0)),
        Point::new(Mm(190.0), Mm(240.0)),
    );

    // Сохраняем в файл
    let file = File::create("report.pdf").unwrap();
    let writer = BufWriter::new(file);
    doc.save(&mut std::io::BufWriter::new(writer)).unwrap();

    println!("PDF отчёт создан: report.pdf");
}

Этот код создаёт одностраничный PDF с заголовком, датой и серой линией. Все координаты задаются в миллиметрах (можно использовать и пункты).

Добавление таблицы с данными

Для отчётов часто нужны таблицы. В printpdf нет готовых табличных структур, но их легко реализовать вручную:

use printpdf::*;
use std::fs::File;
use std::io::BufWriter;

fn draw_table(
    layer: &PdfLayerReference,
    font: &IndirectFontRef,
    data: Vec<[&str; 3]>,
    start_x: Mm,
    start_y: Mm,
) {
    let col_widths = [Mm(40.0), Mm(60.0), Mm(50.0)];
    let row_height = Mm(10.0);
    let header_bg = Color::Rgb(Rgb::new(0.9, 0.9, 0.9));

    for (row_idx, row_data) in data.iter().enumerate() {
        let y = start_y - row_height * (row_idx as f32);

        // Фон для заголовка
        if row_idx == 0 {
            layer.set_fill_color(header_bg);
            layer.add_rectangle(
                start_x,
                y - row_height,
                col_widths[0] + col_widths[1] + col_widths[2],
                row_height,
            );
            layer.fill();
        }

        // Рисуем текст в каждой колонке
        for (col_idx, cell_text) in row_data.iter().enumerate() {
            let x = start_x
                + col_widths[..col_idx].iter().sum::<Mm                    

Рекомендаций