Module: VivlioStarter::CLI::Build::QpdfOverlay

Defined in:
lib/vivlio_starter/cli/build/qpdf_overlay.rb

Constant Summary collapse

SCALING_BOXES =

等倍重畳の妨げになるページボックス(MediaBox 以外)

%w[/TrimBox /BleedBox /CropBox /ArtBox].freeze

Class Method Summary collapse

Class Method Details

.apply!(target_pdf, overlay_pdf, repeat: false) ⇒ Boolean

overlay_pdf の内容を target_pdf へ等倍で重畳する。

Parameters:

  • target_pdf (String)

    重畳先。成功時に上書きされる

  • overlay_pdf (String)

    重ねる PDF

  • repeat (Boolean) (defaults to: false)

    true: overlay の 1 ページ目を全ページへ繰り返す / false: overlay の N ページ目を target の N ページ目へ 1:1 で重ねる

Returns:

  • (Boolean)

    重畳に成功したか



40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/vivlio_starter/cli/build/qpdf_overlay.rb', line 40

def apply!(target_pdf, overlay_pdf, repeat: false)
  return false unless File.exist?(target_pdf) && File.exist?(overlay_pdf)

  # --- Phase: 等倍で重なるようページボックスを退避 ---
  saved_boxes = detach_scaling_boxes!(target_pdf)
  return false if saved_boxes.nil?

  # --- Phase: 重畳 ---
  success = overlay!(target_pdf, overlay_pdf, repeat:)

  # --- Phase: ページボックスの復帰(重畳でオブジェクト ID が動くため再走査する) ---
  success && attach_boxes!(target_pdf, saved_boxes)
end

.attach_boxes!(pdf_path, saved_boxes) ⇒ Object

控えたページボックスを書き戻す



77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/vivlio_starter/cli/build/qpdf_overlay.rb', line 77

def attach_boxes!(pdf_path, saved_boxes)
  return true if saved_boxes.all?(&:empty?)

  header, objects, pages = QpdfJson.read(pdf_path)
  return false unless pages

  updates = pages.each_with_index.to_h do |page, index|
    value = objects["obj:#{page['object']}"]['value'].merge(saved_boxes[index] || {})
    ["obj:#{page['object']}", { 'value' => value }]
  end

  QpdfJson.apply!(pdf_path, header, updates)
end

.detach_scaling_boxes!(pdf_path) ⇒ Array<Hash>?

MediaBox 以外のページボックスを取り除き、ページ順に控えを返す。 退避すべきボックスが無ければ PDF に触れず空配列を返す(高速パス)。

Returns:

  • (Array<Hash>, nil)

    ページ順のボックス控え。読み取り失敗時 nil



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/vivlio_starter/cli/build/qpdf_overlay.rb', line 58

def detach_scaling_boxes!(pdf_path)
  header, objects, pages = QpdfJson.read(pdf_path)
  return nil unless pages

  saved = pages.map do |page|
    value = objects["obj:#{page['object']}"]['value']
    value.slice(*SCALING_BOXES)
  end
  return saved if saved.all?(&:empty?)

  updates = pages.each_with_index.to_h do |page, index|
    value = objects["obj:#{page['object']}"]['value'].reject { |key, _| saved[index].key?(key) }
    ["obj:#{page['object']}", { 'value' => value }]
  end

  QpdfJson.apply!(pdf_path, header, updates) ? saved : nil
end

.overlay!(target_pdf, overlay_pdf, repeat:) ⇒ Object

qpdf --overlay 本体



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/vivlio_starter/cli/build/qpdf_overlay.rb', line 92

def overlay!(target_pdf, overlay_pdf, repeat:)
  overlaid = "#{target_pdf}.overlay.tmp.pdf"
  page_args = repeat ? ['--from=', '--repeat=1'] : ['--to=1-z', '--from=1-z']
  success = system('qpdf', target_pdf, overlaid, '--overlay', overlay_pdf, *page_args, '--',
                   out: File::NULL, err: File::NULL)

  if success && File.exist?(overlaid)
    FileUtils.mv(overlaid, target_pdf)
    true
  else
    FileUtils.rm_f(overlaid)
    Common.log_warn('[qpdf] --overlay による重畳に失敗しました')
    false
  end
end