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
-
.apply_replacements(document, replacements) ⇒ Object
Walk every object's value tree, replacing References whose oid is in
replacementswith a Reference to the canonical oid. - .call(document, io:, **opts) ⇒ Object
-
.dedup_streams!(document) ⇒ Object
Deduplicate identical stream objects within the document.
-
.downsample_images!(document, factor: 2) ⇒ Object
Walk every image XObject and (for FlateDecode-encoded 8-bpc non-palette images) halve each dimension by
factorusing nearest-neighbour. - .replace_ref(ref, replacements) ⇒ Object
- .rewrite_array(arr, replacements) ⇒ Object
- .rewrite_hash(hash, replacements) ⇒ Object
-
.rewrite_references(value, replacements) ⇒ Object
Recursively rewrite references inside a value.
- .stream_dedup_key(stream) ⇒ Object
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.
92 93 94 95 96 97 98 99 100 |
# File 'lib/pdfrb/task/optimize.rb', line 92 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 77 78 79 80 |
# File 'lib/pdfrb/task/optimize.rb', line 56 def dedup_streams!(document) canonical_for = {} document.each_indirect_object do |obj| next unless obj.is_a?(Pdfrb::Model::Cos::Stream) next unless obj.indirect? next if obj.value[:Type] == :ObjStm key = stream_dedup_key(obj) canonical_for[key] ||= obj.oid end replacements = {} document.each_indirect_object do |obj| next unless obj.is_a?(Pdfrb::Model::Cos::Stream) next unless obj.indirect? 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
136 137 138 139 140 141 |
# File 'lib/pdfrb/task/optimize.rb', line 136 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
129 130 131 132 133 134 |
# File 'lib/pdfrb/task/optimize.rb', line 129 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
122 123 124 125 126 127 |
# File 'lib/pdfrb/task/optimize.rb', line 122 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.
105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/pdfrb/task/optimize.rb', line 105 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
82 83 84 85 86 |
# File 'lib/pdfrb/task/optimize.rb', line 82 def stream_dedup_key(stream) data = stream.stream || +"" filter = stream.value[:Filter] [data.bytesize, filter, Digest::SHA1.digest(data)].hash end |