Module: Pdfrb::Task::Thumbnail
- Defined in:
- lib/pdfrb/task/thumbnail.rb
Overview
Generates a low-resolution thumbnail for each page (or a single page) and attaches it via the page-level /Thumb key. PDF viewers use /Thumb to render page previews quickly without parsing the full content stream.
The thumbnail is a Form XObject rendered to a small image XObject. Pure-Ruby implementation: renders only the page's text (no image / form XObject content) at the target resolution using the standard 14 AFM metrics when available, otherwise a placeholder rectangle.
Constant Summary collapse
- DEFAULT_DPI =
72- DEFAULT_MAX_WIDTH =
200
Class Method Summary collapse
-
.build_thumbnail(document, page, dpi: DEFAULT_DPI, max_width: DEFAULT_MAX_WIDTH) ⇒ Object
Build a single thumbnail image XObject for
pageat the given DPI, capped atmax_widthpixels. -
.call(document, dpi: DEFAULT_DPI, max_width: DEFAULT_MAX_WIDTH, force: false) ⇒ Object
Generate thumbnails for every page in
document. - .draw_horizontal_line(bytes, width, height, x0, y, x1, value) ⇒ Object
- .draw_rect(bytes, width, height, x0, y0, x1, y1, value) ⇒ Object
-
.page_pixel_dimensions(media_box, dpi, max_width) ⇒ Object
Compute thumbnail pixel dimensions, capped at
max_width. -
.render_placeholder(width, height, _page) ⇒ Object
Render a 1-bpp-style grayscale placeholder: white background, a black border, and a few black "text line" rectangles representing where text content might appear.
- .require_zlib ⇒ Object
- .set_pixel(bytes, width, height, x, y, value) ⇒ Object
Class Method Details
.build_thumbnail(document, page, dpi: DEFAULT_DPI, max_width: DEFAULT_MAX_WIDTH) ⇒ Object
Build a single thumbnail image XObject for page at the
given DPI, capped at max_width pixels. Returns the image
XObject or nil if the page has no media box.
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/pdfrb/task/thumbnail.rb', line 41 def build_thumbnail(document, page, dpi: DEFAULT_DPI, max_width: DEFAULT_MAX_WIDTH) media_box = page.value[:MediaBox] return nil unless media_box.is_a?(::Array) && media_box.length == 4 width, height = page_pixel_dimensions(media_box, dpi, max_width) return nil unless width.positive? && height.positive? # Single-component grayscale image — 1 byte per pixel. # White background, black "text" rectangle placeholder. Real # text rendering would require running the content processor # against a rasteriser; that's a much larger feature. pixel_data = render_placeholder(width, height, page) compressed = require_zlib.deflate(pixel_data) image = document.add( { Type: :XObject, Subtype: :Image, Width: width, Height: height, ColorSpace: :DeviceGray, BitsPerComponent: 8, Filter: :FlateDecode, Length: compressed.bytesize }, type: Pdfrb::Model::Cos::Stream ) image.stream = compressed image end |
.call(document, dpi: DEFAULT_DPI, max_width: DEFAULT_MAX_WIDTH, force: false) ⇒ Object
Generate thumbnails for every page in document. Returns the
count of thumbnails added. Idempotent: existing /Thumb
entries are left alone unless force: is true.
24 25 26 27 28 29 30 31 32 33 34 35 36 |
# File 'lib/pdfrb/task/thumbnail.rb', line 24 def call(document, dpi: DEFAULT_DPI, max_width: DEFAULT_MAX_WIDTH, force: false) count = 0 document.pages.each do |page| next if page.value[:Thumb] && !force thumb = build_thumbnail(document, page, dpi: dpi, max_width: max_width) next unless thumb page.value[:Thumb] = Pdfrb::Model::Reference.new(thumb.oid, thumb.gen) count += 1 end count end |
.draw_horizontal_line(bytes, width, height, x0, y, x1, value) ⇒ Object
115 116 117 118 119 |
# File 'lib/pdfrb/task/thumbnail.rb', line 115 def draw_horizontal_line(bytes, width, height, x0, y, x1, value) x0.upto([x1 - 1, width - 1].min) do |x| set_pixel(bytes, width, height, x, y, value) end end |
.draw_rect(bytes, width, height, x0, y0, x1, y1, value) ⇒ Object
104 105 106 107 108 109 110 111 112 113 |
# File 'lib/pdfrb/task/thumbnail.rb', line 104 def draw_rect(bytes, width, height, x0, y0, x1, y1, value) x0.upto([x1 - 1, width - 1].min) do |x| set_pixel(bytes, width, height, x, y0, value) set_pixel(bytes, width, height, x, y1 - 1, value) end y0.upto([y1 - 1, height - 1].min) do |y| set_pixel(bytes, width, height, x0, y, value) set_pixel(bytes, width, height, x1 - 1, y, value) end end |
.page_pixel_dimensions(media_box, dpi, max_width) ⇒ Object
Compute thumbnail pixel dimensions, capped at max_width.
71 72 73 74 75 76 77 78 79 80 |
# File 'lib/pdfrb/task/thumbnail.rb', line 71 def page_pixel_dimensions(media_box, dpi, max_width) _x0, _y0, x1, y1 = media_box pt_w = x1.to_f pt_h = y1.to_f return [0, 0] if pt_w.zero? || pt_h.zero? scale = (dpi / 72.0) * (max_width.to_f / pt_w) scale = 1.0 if scale > 1.0 [(pt_w * scale).round, (pt_h * scale).round] end |
.render_placeholder(width, height, _page) ⇒ Object
Render a 1-bpp-style grayscale placeholder: white background, a black border, and a few black "text line" rectangles representing where text content might appear. Pure visual cue, not a real render.
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/pdfrb/task/thumbnail.rb', line 86 def render_placeholder(width, height, _page) bytes = Array.new(width * height, 255) # white draw_rect(bytes, width, height, 0, 0, width, height, 0) # border (black) # Determine text area: roughly 80% of page width, centred margin = (width * 0.1).round text_top = (height * 0.85).round line_height = (height * 0.02).round line_height = 1 if line_height.zero? line_count = [(height * 0.6 / line_height).floor, 3].max line_count.times do |i| y = text_top - (i * line_height * 2) draw_horizontal_line(bytes, width, height, margin, y, width - margin, 0) end bytes.pack("C*").b end |
.require_zlib ⇒ Object
127 128 129 130 |
# File 'lib/pdfrb/task/thumbnail.rb', line 127 def require_zlib require "zlib" ::Zlib end |
.set_pixel(bytes, width, height, x, y, value) ⇒ Object
121 122 123 124 125 |
# File 'lib/pdfrb/task/thumbnail.rb', line 121 def set_pixel(bytes, width, height, x, y, value) return if x.negative? || y.negative? || x >= width || y >= height bytes[(y * width) + x] = value end |