Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Writing PDFs in Ruby DSL

Writing PDFs in Ruby DSL

Avatar for Hiromi Ogawa

Hiromi Ogawa

January 24, 2025
Tweet

More Decks by Hiromi Ogawa

Other Decks in Programming

Transcript

  1. Hiromi Ogawa (coord_e) Software Engineer (Site Reliability) @ Cookpad 仕事で書くのは

    .tf, .yaml, .rb, .rs Ruby 歴はだいたい 3 年ぐらい
  2. %PDF-1.3 1 0 obj << /Type /Catalog /Pages 2 0

    R >> endobj 2 0 obj << /Type /Pages /Kids [ 3 0 R ] /Count 1 >> endobj 3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [ 0 0 720 405 ] /Resources 4 0 R /Contents 5 0 R >> endobj 4 0 obj << /Font << /F1 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >> >> endobj 5 0 obj << /Length 46 >> stream BT /F1 60 Tf 190 180 Td (Hello, World!) Tj ET endstream endobj xref 0 6 0000000000 65535 f 0000000009 00000 n 0000000058 00000 n 0000000115 00000 n 0000000227 00000 n 0000000319 00000 n trailer << /Size 6 /Root 1 0 R >> startxref 414
  3. %PDF-1.3 1 0 obj << /Type /Catalog /Pages 2 0

    R >> endobj 2 0 obj << /Type /Pages /Kids [ 3 0 R ] /Count 1 >> endobj 3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [ 0 0 720 405 ] /Resources 4 0 R /Contents 5 0 R >> endobj 4 0 obj << /Font << /F1 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >> >> endobj 5 0 obj << /Length 46 >> stream BT /F1 60 Tf 190 180 Td (Hello, World!) Tj ET endstream endobj xref 0 6 0000000000 65535 f 0000000009 00000 n 0000000058 00000 n 0000000115 00000 n 0000000227 00000 n 0000000319 00000 n trailer << /Size 6 /Root 1 0 R >> startxref 414
  4. オブジェクトの構文 1 0 obj << /Type /Catalog /Pages 2 0

    R >> endobj 2 0 obj << /Type /Pages /Kids [ 3 0 R ] /Count 1 >> endobj 3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [ 0 0 720 405 ] /Resources 4 0 R /Contents 5 0 R >> endobj 名前: /Name 辞書: << /Key #{object} >> 定義: 1 0 obj #{object} 参照: 1 0 R
  5. ドキュメントの構造 1 0 obj << /Type /Catalog /Pages 2 0

    R >> endobj 2 0 obj << /Type /Pages /Kids [ 3 0 R ] /Count 1 >> endobj 3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [ 0 0 720 405 ] /Resources 4 0 R /Contents 5 0 R >> endobj Catalog Pages Page コンテンツ ストリーム
  6. %PDF-1.3 1 0 obj << /Type /Catalog /Pages 2 0

    R >> endobj 2 0 obj << /Type /Pages /Kids [ 3 0 R ] /Count 1 >> endobj 3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [ 0 0 720 405 ] /Resources 4 0 R /Contents 5 0 R >> endobj 4 0 obj << /Font << /F1 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >> >> endobj 5 0 obj << /Length 46 >> stream BT /F1 60 Tf 190 180 Td (Hello, World!) Tj ET endstream endobj xref 0 6 0000000000 65535 f 0000000009 00000 n 0000000058 00000 n 0000000115 00000 n 0000000227 00000 n 0000000319 00000 n trailer << /Size 6 /Root 1 0 R >> startxref 414
  7. コンテンツストリーム 5 0 obj << /Length 46 >> stream BT

    /F1 60 Tf 190 180 Td (Hello, World!) Tj ET endstream endobj
  8. コンテンツストリーム 5 0 obj << /Length 196 >> stream BT

    % Begin text object /F1 60 Tf % Set text font and size 190 180 Td % Move text position (Hello, World!) Tj % Show text ET % End text object endstream endobj
  9. %PDF-1.3 1 0 obj << /Type /Catalog /Pages 2 0

    R >> endobj 2 0 obj << /Type /Pages /Kids [ 3 0 R ] /Count 1 >> endobj 3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [ 0 0 720 405 ] /Resources 4 0 R /Contents 5 0 R >> endobj 4 0 obj << /Font << /F1 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >> >> endobj 5 0 obj << /Length 46 >> stream BT /F1 60 Tf 190 180 Td (Hello, World!) Tj ET endstream endobj xref 0 6 0000000000 65535 f 0000000009 00000 n 0000000058 00000 n 0000000115 00000 n 0000000227 00000 n 0000000319 00000 n trailer << /Size 6 /Root 1 0 R >> startxref 414
  10. %PDF-1.3 1 0 obj << /Type /Catalog /Pages 2 0

    R >> endobj 2 0 obj << /Type /Pages /Kids [ 3 0 R ] /Count 1 >> endobj 3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [ 0 0 720 405 ] /Resources 4 0 R /Contents 5 0 R >> endobj 4 0 obj << /Font << /F1 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >> >> endobj 5 0 obj << /Length 46 >> stream BT /F1 60 Tf 190 180 Td (Hello, World!) Tj ET endstream endobj xref 0 6 0000000000 65535 f 0000000009 00000 n 0000000058 00000 n 0000000115 00000 n 0000000227 00000 n 0000000319 00000 n trailer << /Size 6 /Root 1 0 R >> startxref 414 ?
  11. %PDF-1.3 1 0 obj << /Type /Catalog /Pages 2 0

    R >> endobj 2 0 obj << /Type /Pages /Kids [ 3 0 R ] /Count 1 >> endobj 3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [ 0 0 720 405 ] /Resources 4 0 R /Contents 5 0 R >> endobj 4 0 obj << /Font << /F1 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >> >> endobj 5 0 obj << /Length 46 >> stream BT /F1 60 Tf 190 180 Td (Hello, World!) Tj ET endstream endobj xref 0 6 0000000000 65535 f 0000000009 00000 n 0000000058 00000 n 0000000115 00000 n 0000000227 00000 n 0000000319 00000 n trailer << /Size 6 /Root 1 0 R >> startxref 414 Cross-Reference Table
  12. が PDF を書くのは? ゲーム性を出すためのレギュレーション: 出力した PDF が Adobe Acrobat Reader

    で読める 消費メモリが書くドキュメントの大きさに依存しない (ストリーミング)
  13. class OffsetIO attr_reader :offset def initialize(io) @io = io @offset

    = 0 end def <<(b) @io << b @offset += b.bytesize end end io = OffsetIO.new($stdout) io << "obj << /Type /Catalog >> endobj"
  14. io << "obj " io << "<< " io <<

    "/Type " io << "/Catalog" io << " >>" io << " endobj"
  15. def obj @io << "obj " … # ?? @io

    << " endobj" end データを受け取る? コールバック関数?
  16. def obj @io << "obj " yield @io << "

    endobj" end Ruby にはブロックがある
  17. def dict @io << "<< " yield @io << "

    >>" end def entry(key) @io << "/#{key} " yield end def name(n) @io << "/#{n}" end
  18. class PDFWriter def initialize(io) @io = OffsetIO.new(io) end def obj(&block)

    @io << "obj " instance_exec(&block) @io << " endobj" end end def PDF(io, &block) PDFWriter.new(io).instance_exec(&block) end
  19. class ObjectContext def name(n) = @io << "/#{n}" def dict(&block)

    @io << "<< " DictContext.new(@io).instance_exec(&block) @io << " >>" end end class PDFWriter def obj(&block) @io << "obj " ObjectContext.new(@io).instance_exec(&block) @io << " endobj" end end
  20. class PDFWriter def initialize(io) @io = OffsetIO.new(io) @xref = [

    nil ] end def obj(&block) @xref << @io.offset @io << "#{@xref.size - 1} 0 obj " ObjectContext.new(@io).instance_exec(&block) @io << " endobj" @xref.size - 1 end def xref @io << "xref " << "0 #{@xref.size} " @xref.each do |offset| @io << "#{offset.to_s.rjust(10, "0")} 0 n " end end end バイト位置を保存 まとめて出力
  21. 5 0 obj << /Length 46 >> stream BT /F1

    60 Tf 190 180 Td (Hello, World!) Tj ET endstream endobj
  22. content_stream do op("BT") op("Tf") { name "Tf"; num 60 }

    op("Td") { num 190; num 180 } op("Tj") { str "Hello, World!" } op("ET") end
  23. 5 0 obj << /Length 46 >> stream BT /F1

    60 Tf 190 180 Td (Hello, World!) Tj ET endstream endobj ?
  24. 5 0 obj << /Length 6 0 R >> stream

    BT /F1 60 Tf 190 180 Td (Hello, World!) Tj ET endstream endobj 6 0 obj 46
  25. len_obj = alloc_obj size = nil obj do dict {

    entry("Length").of_ref len_obj } size = content_stream do op("BT") op("Tf") { name "Tf"; num 60 } op("Td") { num 190; num 180 } op("Tj") { str "Hello, World!" } op("ET") end end obj(len_obj).of_num size
  26. len_obj = alloc_obj size = nil obj do dict {

    entry("Length").of_ref len_obj } content_stream do op("BT") op("Tf") { name "Tf"; num 60 } op("Td") { num 190; num 180 } op("Tj") { str "Hello, World!" } op("ET") end => size end obj(len_obj).of_num size
  27. header alloc_obj => pages_obj obj do dict do entry("Type") {

    name "Catalog" } entry("Pages") { ref pages_obj } end end => catalog_obj (.. other object definitions ..) xref trailer do entry("Root") { ref catalog_obj } end
  28. JotPDF::Core.write($stdout) do header alloc_obj => pages_obj; alloc_obj => contents_obj obj.of_dict

    { entry("Type").of_name "Catalog"; entry("Pages").of_ref pages_obj } => catalog_obj obj.of_dict { entry("Font").of_dict { entry("F1").of_dict { entry("Type").of_name "Font"; entry("Subtype").of_name "Type1" entry("BaseFont").of_name "Helvetica" } } } => resources_obj obj.of_dict do entry("Type").of_name "Page"; entry("Parent").of_ref pages_obj entry("MediaBox").of_array { num 0; num 0; num 612; num 792 } entry("Resources").of_ref resources_obj; entry("Contents").of_ref contents_obj end => page_obj obj(pages_obj).of_dict do entry("Type").of_name "Pages"; entry("Kids").of_array { ref page_obj } entry("Count").of_num 1 end alloc_obj => length_obj stream_size = nil obj(contents_obj) do dict { entry("Length").of_ref length_obj } content_stream do op("BT"); op("Tf") { name "F1"; num 24 } op("Td") { num 100; num 100 }; op("Tj") { str "Hello, World" }; op("ET") end => stream_size end obj(length_obj).of_num stream_size xref trailer { entry("Root").of_ref catalog_obj } end
  29. class PageContext def initialize(ctxt) = @ctxt = ctxt def text(x:,

    y:, size:, text:) @ctxt.instance_exec do op("BT"); op("Tf") { name "F1"; num size } op("Td") { num x; num y } op("Tj") { str text }; op("ET") end end end
  30. JotPDF::Document.write($stdout) do 10.times do |i| page width: 720, height: 405

    do text "#{i}th page", x: 190, y: 180, size: 60 end end end