Module: Pdfrb::Image::Downsampler
- Defined in:
- lib/pdfrb/image/downsampler.rb
Overview
Pure-Ruby nearest-neighbour downsampler for FlateDecode-encoded image XObjects. JPEG (DCTDecode) and JPEG2000 (JPXDecode) streams can't be re-encoded without native deps, so they're skipped.
The decoder side uses Filter::FlateDecode and the existing PNGPredictor for /DecodeParms row reconstruction. The encoder re-deflates and writes predictor rows with filter byte 0 (None), which the spec allows.
Class Method Summary collapse
-
.decode(image, width, _height, bytes_per_pixel) ⇒ Object
Decode the stream back to raw pixel bytes.
-
.downsample!(image, factor:) ⇒ Object
Downsample
imagebyfactor(>=2) using nearest-neighbour. -
.eligible?(image) ⇒ Boolean
Eligibility: FlateDecode (with or without PNG predictor), 8 bpc, single-component (gray), RGB, or CMYK (not Indexed, which has a palette that mustn't be re-sampled).
-
.encode(image, pixels, new_w, new_h, bytes_per_pixel) ⇒ Object
Re-encode
pixelsas FlateDecode, applying the same predictor scheme the image originally used (or uncompressed if none). -
.sample_nearest(decoded, _orig_w, new_w, new_h, bytes_per_pixel, factor) ⇒ Object
Nearest-neighbour sampling.
Class Method Details
.decode(image, width, _height, bytes_per_pixel) ⇒ Object
Decode the stream back to raw pixel bytes. Handles PNG predictor rows when /DecodeParms is present.
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/pdfrb/image/downsampler.rb', line 71 def decode(image, width, _height, bytes_per_pixel) inflated = Zlib::Inflate.inflate(image.stream) params = image.value[:DecodeParms] return inflated unless params predictor = params.is_a?(::Hash) ? params[:Predictor] : nil return inflated unless predictor && predictor >= 10 columns = (params.is_a?(::Hash) ? params[:Columns] : nil) || width Pdfrb::Filter::PNGPredictor.decode( inflated, predictor: predictor, columns: columns, colors: (params[:Colors] if params.is_a?(::Hash)) || bytes_per_pixel, bits_per_component: (params[:BitsPerComponent] if params.is_a?(::Hash)) || 8 ) end |
.downsample!(image, factor:) ⇒ Object
Downsample image by factor (>=2) using nearest-neighbour.
Mutates the image's stream and /Width, /Height, /Length keys.
Returns the image if changes were made, nil otherwise.
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/pdfrb/image/downsampler.rb', line 21 def downsample!(image, factor:) return nil unless factor && factor >= 2 return nil unless eligible?(image) width = image.value[:Width] height = image.value[:Height] bpc = image.value[:BitsPerComponent] || 8 channels = Audit.channels_for(image.value[:ColorSpace]) bytes_per_pixel = (channels * bpc) / 8 return nil unless bytes_per_pixel.positive? decoded = decode(image, width, height, bytes_per_pixel) return nil unless decoded new_w = width / factor new_h = height / factor return nil if new_w.zero? || new_h.zero? sampled = sample_nearest(decoded, width, new_w, new_h, bytes_per_pixel, factor) encoded = encode(image, sampled, new_w, new_h, bytes_per_pixel) return nil unless encoded image.stream = encoded image.value[:Width] = new_w image.value[:Height] = new_h image.value[:Length] = encoded.bytesize image end |
.eligible?(image) ⇒ Boolean
Eligibility: FlateDecode (with or without PNG predictor), 8 bpc, single-component (gray), RGB, or CMYK (not Indexed, which has a palette that mustn't be re-sampled).
54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/pdfrb/image/downsampler.rb', line 54 def eligible?(image) return false unless image.is_a?(Pdfrb::Model::Cos::Stream) filter = image.value[:Filter] return false unless filter == :FlateDecode return false unless (image.value[:BitsPerComponent] || 8) == 8 color_space = image.value[:ColorSpace] return false if color_space == :Indexed return false if color_space.is_a?(::Array) && color_space.first == :Indexed image.value[:Width] && image.value[:Height] && image.stream end |
.encode(image, pixels, new_w, new_h, bytes_per_pixel) ⇒ Object
Re-encode pixels as FlateDecode, applying the same predictor
scheme the image originally used (or uncompressed if none).
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/pdfrb/image/downsampler.rb', line 107 def encode(image, pixels, new_w, new_h, bytes_per_pixel) params = image.value[:DecodeParms] predictor = params.is_a?(::Hash) ? params[:Predictor] : nil if predictor && predictor >= 10 row_len = new_w * bytes_per_pixel prefixed = +"" new_h.times do |row| offset = row * row_len prefixed << "\x00".b prefixed << pixels.byteslice(offset, row_len) end pixels = prefixed end Zlib::Deflate.deflate(pixels) end |
.sample_nearest(decoded, _orig_w, new_w, new_h, bytes_per_pixel, factor) ⇒ Object
Nearest-neighbour sampling. Picks every factor-th pixel in both dimensions.
92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/pdfrb/image/downsampler.rb', line 92 def sample_nearest(decoded, _orig_w, new_w, new_h, bytes_per_pixel, factor) out = +"" new_h.times do |new_y| src_y = new_y * factor new_w.times do |new_x| src_x = new_x * factor offset = ((src_y * _orig_w) + src_x) * bytes_per_pixel out << decoded.byteslice(offset, bytes_per_pixel) end end out.force_encoding(Encoding::BINARY) end |