Module: Pdfrb::Task::Optimize

Defined in:
lib/pdfrb/task/optimize.rb

Overview

Optimises a document for smaller file size by:

1. Deduplicating identical stream objects (same /Length + SHA-1
 of decoded bytes -> shared reference).
2. Packing eligible small objects into /Type /ObjStm streams.
3. Converting the classical xref table to an XRef stream.

The optimised document is written to the given IO. Returns the new byte count.

Class Method Summary collapse

Class Method Details

.apply_replacements(document, replacements) ⇒ Object

Walk every object's value tree, replacing References whose oid is in replacements with a Reference to the canonical oid. Also clears each replacement source from the document's modified-objects table so the writer doesn't emit it.



88
89
90
91
92
93
94
95
96
# File 'lib/pdfrb/task/optimize.rb', line 88

def apply_replacements(document, replacements)
  return replacements if replacements.empty?

  document.each_indirect_object do |obj|
    rewrite_references(obj, replacements)
  end
  replacements.each_key { |oid| document.forget(oid) }
  replacements
end

.call(document, io:, **opts) ⇒ Object



19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/pdfrb/task/optimize.rb', line 19

def call(document, io:, **opts)
  document.config["writer.compress_streams"] = true
  document.config["writer.use_xref_stream"] = true
  document.config["writer.pack_object_streams"] = true
  document.config["writer.object_stream_threshold"] =
    opts[:threshold] || 200

  dedup_streams!(document) unless opts[:dedup] == false
  downsample_images!(document, factor: opts[:downsample_factor]) unless opts[:downsample] == false
  document.write(io: io)
  io.string.bytesize
end

.dedup_streams!(document) ⇒ Object

Deduplicate identical stream objects within the document. Streams with identical decoded content + /Filter are merged into a single shared object; duplicates are replaced by a Reference to the canonical one. Returns a Hash of => canonical_oid that was applied.



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/pdfrb/task/optimize.rb', line 56

def dedup_streams!(document)
  canonical_for = {}
  document.each_indirect_of(Pdfrb::Model::Cos::Stream) do |obj|
    next if obj.value[:Type] == :ObjStm

    key = stream_dedup_key(obj)
    canonical_for[key] ||= obj.oid
  end

  replacements = {}
  document.each_indirect_of(Pdfrb::Model::Cos::Stream) do |obj|
    next if obj.value[:Type] == :ObjStm

    key = stream_dedup_key(obj)
    canonical_oid = canonical_for[key]
    next if canonical_oid == obj.oid

    replacements[obj.oid] = canonical_oid
  end
  apply_replacements(document, replacements)
end

.downsample_images!(document, factor: 2) ⇒ Object

Walk every image XObject and (for FlateDecode-encoded 8-bpc non-palette images) halve each dimension by factor using nearest-neighbour. JPEG/JP2K are skipped — re-encoding them in pure Ruby isn't tractable. Returns the count of images that were modified.



37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/pdfrb/task/optimize.rb', line 37

def downsample_images!(document, factor: 2)
  return 0 unless factor && factor >= 2

  count = 0
  document.each_indirect_object do |obj|
    next unless Pdfrb::Image::Audit.image_xobject?(obj)
    next unless Pdfrb::Image::Downsampler.eligible?(obj)

    changed = Pdfrb::Image::Downsampler.downsample!(obj, factor: factor)
    count += 1 if changed
  end
  count
end

.replace_ref(ref, replacements) ⇒ Object



132
133
134
135
136
137
# File 'lib/pdfrb/task/optimize.rb', line 132

def replace_ref(ref, replacements)
  canonical = replacements[ref.oid]
  return ref unless canonical

  Pdfrb::Model::Reference.new(canonical, 0)
end

.rewrite_array(arr, replacements) ⇒ Object



125
126
127
128
129
130
# File 'lib/pdfrb/task/optimize.rb', line 125

def rewrite_array(arr, replacements)
  arr.each_with_index do |v, i|
    arr[i] = rewrite_references(v, replacements)
  end
  arr
end

.rewrite_hash(hash, replacements) ⇒ Object



118
119
120
121
122
123
# File 'lib/pdfrb/task/optimize.rb', line 118

def rewrite_hash(hash, replacements)
  hash.each do |k, v|
    hash[k] = rewrite_references(v, replacements)
  end
  hash
end

.rewrite_references(value, replacements) ⇒ Object

Recursively rewrite references inside a value. Handles Dictionaries (Hash / Cos::Dictionary), Arrays / PdfArrays, and References. Returns the (possibly new) value.



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/pdfrb/task/optimize.rb', line 101

def rewrite_references(value, replacements)
  case value
  when Pdfrb::Model::Cos::Dictionary, Pdfrb::Model::Cos::Stream
    rewrite_hash(value.value, replacements)
  when ::Hash
    rewrite_hash(value, replacements)
  when Pdfrb::Model::PdfArray
    rewrite_array(value.value, replacements)
  when ::Array
    rewrite_array(value, replacements)
  when Pdfrb::Model::Reference
    replace_ref(value, replacements)
  else
    value
  end
end

.stream_dedup_key(stream) ⇒ Object



78
79
80
81
82
# File 'lib/pdfrb/task/optimize.rb', line 78

def stream_dedup_key(stream)
  data = stream.stream || +""
  filter = stream.value[:Filter]
  [data.bytesize, filter, Digest::SHA1.digest(data)].hash
end