2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rustのprintpdfクレートがサブセットフォントの埋め込みに対応して歓喜した話

Posted at

背景

テキストファイルを手軽にPDF化したくて、以下のprintpdfを試していた。
ただ、日本語を含む場合フォントが埋め込まれるため数ページのPDFが数MBになってしまい、頭を抱えていた。
後処理として、lopdfを使ってフォントを取り除けないか調査していたところ、表題に戻る。

おまけ

printpdfがv0.8.1になったのは10時間(v0.8.0も2日)ぐらい前のため、README.mdにあるFontの部分がコピペでは動かない。
とりあえず、ReadMe.mdを動くように変更したものとテキストファイルをPDF化したものを置いておく

ReadMeを修正したもの

use printpdf::*;

fn main() {
    let mut doc = PdfDocument::new("My first PDF");

    let roboto_bytes = include_bytes!("assets/fonts/RobotoMedium.ttf").unwrap()
    let font_index = 0;
    let mut warnings = Vec::new();
    let font = ParsedFont::from_bytes(&roboto_bytes, font_index, &mut warnings).unwrap(); // 引数の数が違う 参照渡し

    // If you need custom text shaping (uses the `allsorts` font shaper internally)
    // let glyphs = font.shape(text);

    // printpdf automatically keeps track of which fonts are used in the PDF
    let font_id = doc.add_font(&font); // 参照渡し

    let text_pos = Point {
        x: Mm(10.0).into(),
        y: Mm(100.0).into(),
    }; // from bottom left

    let page1_contents = vec![
        Op::SetLineHeight { lh: Pt(33.0) },
        Op::SetWordSpacing { pt: Pt(33.0) }, // percent から pt に変更
        Op::SetCharacterSpacing { multiplier: 10.0 },
        Op::SetTextCursor { pos: text_pos },

        // Op::WriteCodepoints { ... }
        // Op::WriteCodepointsWithKerning { ... }
        // 構造体(とenum)がネストされていて形が異なる
        Op::WriteText {
            items: vec![TextItem::Text("Lorem ipsum".to_string())],
            font: font_id.clone(),
        },
        Op::AddLineBreak,
        Op::WriteText {
            items: vec![TextItem::Text("dolor sit amet".to_string())],
            font: font_id.clone(),
        },
        Op::AddLineBreak,
    ];

    let save_options = PdfSaveOptions {
        subset_fonts: true, // auto-subset fonts on save
        ..Default::default()
    };

    let page1 = PdfPage::new(Mm(10.0), Mm(250.0), page1_contents);
    let mut warnings = Vec::new();
    let pdf_bytes: Vec<u8> = doc
        .with_pages(vec![page1])
        .save(&save_options, &mut warnings); // 引数の数が違う
}

テキスト変換

use printpdf::*;
use std::fs::File;
use std::io::{BufReader, Read};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // PDFドキュメントを作成
    let mut doc = PdfDocument::new("from text file");

    // フォントをシステムから読み込む
    let font_bytes = std::fs::read("C:/Windows/Fonts/UDDigiKyokashoN-R.ttc")?;

    // 警告メッセージの格納場所を作成
    let font_index = 0;
    let mut warnings = Vec::new();

    // フォントをロード - ParsedFontを使って処理します(追加の引数が必要)
    let font = match ParsedFont::from_bytes(&font_bytes, font_index, &mut warnings) {
        Some(font) => font,
        None => {
            let msg = format!("フォントの読み込みに失敗しました : {:?}", warnings);
            eprintln!("{}", msg);
            return Err(msg.into());
        }
    };

    // フォントをドキュメントに追加
    let font_id = doc.add_font(&font);

    // テキストファイルを読み込む
    let input_file = File::open("src/main.rs")?;
    let mut reader = BufReader::new(input_file);
    let mut text = String::new();
    reader.read_to_string(&mut text)?;

    // 複数のページを格納するベクター
    let mut pages = Vec::new();

    // テキスト行のイテレータを作成
    let lines = text.lines().collect::<Vec<_>>();
    let mut line_index = 0;

    // ページを作成してテキストを追加する
    while line_index < lines.len() {
        // 新しいページのコンテンツを準備
        let mut page_contents = vec![
            Op::SetLineHeight { lh: Pt(14.0) },          // 適切な行間に調整
            Op::SetWordSpacing { pt: Pt(0.0) },          // 単語間隔を標準に
            Op::SetCharacterSpacing { multiplier: 0.0 }, // 文字間隔を標準に
        ];

        // フォントを設定
        page_contents.push(Op::SetFontSize {
            font: font_id.clone(),
            size: Pt(12.0),
        });

        // 各ページの開始Y位置
        let mut y_pos = Mm(280.0); // A4の上部から開始(余白を考慮)

        // このページに表示する行を追加
        while line_index < lines.len() {
            let line = lines[line_index];

            // テキストセクション開始
            page_contents.push(Op::StartTextSection);

            // テキスト位置を設定
            let text_pos = Point {
                x: Mm(10.0).into(),
                y: y_pos.into(),
            };
            page_contents.push(Op::SetTextCursor { pos: text_pos });

            // テキストを追加
            page_contents.push(Op::WriteText {
                items: vec![TextItem::Text(line.to_string())],
                font: font_id.clone(),
            });

            // テキストセクション終了
            page_contents.push(Op::EndTextSection);

            // Y位置を更新
            y_pos -= Mm(6.0); // 行間を適切に調整

            // 次の行に進む
            line_index += 1;

            // ページ下限に達したら、このページを終了
            if y_pos < Mm(20.0) {
                break;
            }
        }

        // ページを作成して追加
        let page = PdfPage::new(Mm(210.0), Mm(297.0), page_contents); // A4サイズ
        pages.push(page);

        println!("ページ {} 完成", pages.len());
    }

    // 保存オプションを設定
    let save_options = PdfSaveOptions {
        subset_fonts: false,
        // optimize: false, // デバッグのために最適化を無効化
        ..Default::default()
    };

    // PDFを生成して保存
    let mut warnings = Vec::new();
    let page_length = pages.len();
    let pdf_bytes = doc.with_pages(pages).save(&save_options, &mut warnings);
    // 警告メッセージがあれば表示
    if !warnings.is_empty() {
        println!("警告メッセージ: {:?}", warnings);
    }

    // ファイルに書き込み
    match std::fs::write("output.pdf", pdf_bytes) {
        Ok(_) => println!("PDF作成完了: output.pdf (全{}ページ)", page_length),
        Err(e) => {
            eprintln!("PDFファイルの保存に失敗しました: {}", e);
            return Err(e.into());
        }
    };

    Ok(())
}

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?