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

Writing PDFs in Ruby DSL

Writing PDFs in Ruby DSL

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