Class: PDF::Reader::PageState
- Inherits:
-
Object
- Object
- PDF::Reader::PageState
- Defined in:
- lib/pdf/reader/page_state.rb
Overview
encapsulates logic for tracking graphics state as the instructions for a single page are processed. Most of the public methods correspond directly to PDF operators.
Constant Summary collapse
- DEFAULT_GRAPHICS_STATE =
{ :char_spacing => 0, :word_spacing => 0, :h_scaling => 1.0, :text_leading => 0, :text_font => nil, :text_font_size => 0, :text_mode => 0, :text_rise => 0, :text_knockout => 0 }
Instance Method Summary collapse
-
#begin_text_object ⇒ Object
Text Object Operators.
-
#clone_state ⇒ Object
This returns a deep clone of the current state, ensuring changes are keep separate from earlier states.
-
#concatenate_matrix(a, b, c, d, e, f) ⇒ Object
update the current transformation matrix.
-
#ctm_transform(x, y) ⇒ Object
transform x and y co-ordinates from the current user space to the underlying device space.
-
#ctm_transform_point(pt, opt_y = -1)) ⇒ Object
transform x and y co-ordinates from the current user space to the underlying device space.
- #current_font ⇒ Object
- #end_text_object ⇒ Object
- #find_color_space(label) ⇒ Object
- #find_font(label) ⇒ Object
- #find_xobject(label) ⇒ Object
- #font_size ⇒ Object
-
#initialize(page) ⇒ PageState
constructor
starting a new page.
-
#invoke_xobject(label) ⇒ Object
XObjects.
-
#move_text_position(x, y) ⇒ Object
Text Positioning Operators.
-
#move_text_position_and_set_leading(x, y) ⇒ Object
TD.
-
#move_to_next_line_and_show_text(str) ⇒ Object
'.
-
#move_to_start_of_next_line ⇒ Object
T*.
-
#process_glyph_displacement(w0, tj, word_boundary) ⇒ Object
after each glyph is painted onto the page the text matrix must be modified.
-
#restore_graphics_state ⇒ Object
Restore the state to the previous value on the stack.
-
#save_graphics_state ⇒ Object
Clones the current graphics state and push it onto the top of the stack.
-
#set_character_spacing(char_spacing) ⇒ Object
Text State Operators.
- #set_horizontal_text_scaling(h_scaling) ⇒ Object
-
#set_spacing_next_line_show_text(aw, ac, string) ⇒ Object
".
- #set_text_font_and_size(label, size) ⇒ Object
- #set_text_leading(leading) ⇒ Object
-
#set_text_matrix_and_text_line_matrix(a, b, c, d, e, f) ⇒ Object
Tm.
- #set_text_rendering_mode(mode) ⇒ Object
- #set_text_rise(rise) ⇒ Object
- #set_word_spacing(word_spacing) ⇒ Object
-
#show_text_with_positioning(params) ⇒ Object
Text Showing Operators.
-
#stack_depth ⇒ Object
when save_graphics_state is called, we need to push a new copy of the current state onto the stack.
-
#trm_transform(x, y) ⇒ Object
transform x and y co-ordinates from the current text space to the underlying device space.
-
#trm_transform_point(pt, opt_y = -1)) ⇒ Object
transform x and y co-ordinates from the current text space to the underlying device space.
Constructor Details
#initialize(page) ⇒ PageState
starting a new page
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/pdf/reader/page_state.rb', line 27 def initialize(page) @page = page @cache = page.cache @objects = page.objects @font_stack = [build_fonts(page.fonts)] @xobject_stack = [page.xobjects] @cs_stack = [page.color_spaces] @stack = [DEFAULT_GRAPHICS_STATE.dup] state[:ctm] = identity_matrix # These are only valid when inside a `BT` block and we re-initialize them on each # `BT`. However, we need the instance variables set so PDFs with the text operators # out order don't trigger NoMethodError when these are nil @text_matrix = identity_matrix @text_line_matrix = identity_matrix end |
Instance Method Details
#begin_text_object ⇒ Object
Text Object Operators
93 94 95 96 97 |
# File 'lib/pdf/reader/page_state.rb', line 93 def begin_text_object @text_matrix = identity_matrix @text_line_matrix = identity_matrix @font_size = nil end |
#clone_state ⇒ Object
This returns a deep clone of the current state, ensuring changes are keep separate from earlier states.
Marshal is used to round-trip the state through a string to easily perform the deep clone. Kinda hacky, but effective.
342 343 344 345 346 347 348 |
# File 'lib/pdf/reader/page_state.rb', line 342 def clone_state if @stack.empty? {} else Marshal.load Marshal.dump(@stack.last) end end |
#concatenate_matrix(a, b, c, d, e, f) ⇒ Object
update the current transformation matrix.
If the CTM is currently undefined, just store the new values.
If there's an existing CTM, then multiply the existing matrix with the new matrix to form the updated matrix.
75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/pdf/reader/page_state.rb', line 75 def concatenate_matrix(a, b, c, d, e, f) if state[:ctm] ctm = state[:ctm] state[:ctm] = TransformationMatrix.new(a,b,c,d,e,f).multiply!( ctm.a, ctm.b, ctm.c, ctm.d, ctm.e, ctm.f ) else state[:ctm] = TransformationMatrix.new(a,b,c,d,e,f) end @text_rendering_matrix = nil # invalidate cached value end |
#ctm_transform(x, y) ⇒ Object
transform x and y co-ordinates from the current user space to the underlying device space.
Deprecated. Prefer ctm_transform_point()
226 227 228 229 230 231 232 233 |
# File 'lib/pdf/reader/page_state.rb', line 226 def ctm_transform(x, y) # TODO print a deprecation warning res = ctm_transform_point(x, y) [ res.x, res.y ] end |
#ctm_transform_point(pt, opt_y = -1)) ⇒ Object
247 248 249 250 251 252 253 254 255 |
# File 'lib/pdf/reader/page_state.rb', line 247 def ctm_transform_point(pt, opt_y = -1) unless pt.is_a?(Point) pt = Point.new(pt.to_i, opt_y.to_i) end Point.new( (ctm.a * pt.x) + (ctm.c * pt.y) + (ctm.e), (ctm.b * pt.x) + (ctm.d * pt.y) + (ctm.f) ) end |
#current_font ⇒ Object
303 304 305 |
# File 'lib/pdf/reader/page_state.rb', line 303 def current_font find_font(state[:text_font]) end |
#end_text_object ⇒ Object
99 100 101 |
# File 'lib/pdf/reader/page_state.rb', line 99 def end_text_object # don't need to do anything end |
#find_color_space(label) ⇒ Object
314 315 316 317 318 319 |
# File 'lib/pdf/reader/page_state.rb', line 314 def find_color_space(label) dict = @cs_stack.detect { |colorspaces| colorspaces.has_key?(label) } dict ? dict[label] : nil end |
#find_font(label) ⇒ Object
307 308 309 310 311 312 |
# File 'lib/pdf/reader/page_state.rb', line 307 def find_font(label) dict = @font_stack.detect { |fonts| fonts.has_key?(label) } dict ? dict[label] : nil end |
#find_xobject(label) ⇒ Object
321 322 323 324 325 326 |
# File 'lib/pdf/reader/page_state.rb', line 321 def find_xobject(label) dict = @xobject_stack.detect { |xobjects| xobjects.has_key?(label) } dict ? dict[label] : nil end |
#font_size ⇒ Object
120 121 122 123 124 125 126 |
# File 'lib/pdf/reader/page_state.rb', line 120 def font_size @font_size ||= begin bl = trm_transform_point(Point::ZERO_ZERO) tr = trm_transform_point(Point::ONE_ONE) (bl.y - tr.y).abs.round(10) end end |
#invoke_xobject(label) ⇒ Object
XObjects
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 |
# File 'lib/pdf/reader/page_state.rb', line 194 def invoke_xobject(label) save_graphics_state xobject = find_xobject(label) raise MalformedPDFError, "XObject #{label} not found" if xobject.nil? matrix = xobject.hash[:Matrix] concatenate_matrix(*matrix) if matrix if xobject.hash[:Subtype] == :Form form = PDF::Reader::FormXObject.new(@page, xobject, :cache => @cache) @font_stack.unshift(form.font_objects) @xobject_stack.unshift(form.xobjects) yield form if block_given? @font_stack.shift @xobject_stack.shift else yield xobject if block_given? end restore_graphics_state end |
#move_text_position(x, y) ⇒ Object
Text Positioning Operators
148 149 150 151 152 |
# File 'lib/pdf/reader/page_state.rb', line 148 def move_text_position(x, y) # Td @text_line_matrix.prepend_translation!(x, y) @text_matrix = @text_line_matrix.dup @font_size = @text_rendering_matrix = nil # invalidate cached value end |
#move_text_position_and_set_leading(x, y) ⇒ Object
TD
154 155 156 157 |
# File 'lib/pdf/reader/page_state.rb', line 154 def move_text_position_and_set_leading(x, y) # TD set_text_leading(-1 * y) move_text_position(x, y) end |
#move_to_next_line_and_show_text(str) ⇒ Object
'
181 182 183 |
# File 'lib/pdf/reader/page_state.rb', line 181 def move_to_next_line_and_show_text(str) # ' move_to_start_of_next_line end |
#move_to_start_of_next_line ⇒ Object
T*
169 170 171 |
# File 'lib/pdf/reader/page_state.rb', line 169 def move_to_start_of_next_line # T* move_text_position(0, -state[:text_leading]) end |
#process_glyph_displacement(w0, tj, word_boundary) ⇒ Object
after each glyph is painted onto the page the text matrix must be modified. There's no defined operator for this, but depending on the use case some receivers may need to mutate the state with this while walking a page.
NOTE: some of the variable names in this method are obscure because they mirror variable names from the PDF spec
NOTE: see Section 9.4.4, PDF 32000-1:2008, pp 252
Arguments:
w0 - the glyph width in text space. This generally means the width in glyph space should be divded by 1000 before being passed to this function tj - any kerning that should be applied to the text matrix before the following glyph is painted. This is usually the numeric arguments in the array passed to a TJ operator word_boundary - a boolean indicating if a word boundary was just reached. Depending on the current state extra space may need to be added
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 |
# File 'lib/pdf/reader/page_state.rb', line 372 def process_glyph_displacement(w0, tj, word_boundary) fs = state[:text_font_size] tc = state[:char_spacing] if word_boundary tw = state[:word_spacing] else tw = 0 end th = state[:h_scaling] # optimise the common path to reduce Float allocations if th == 1 && tj == 0 && tc == 0 && tw == 0 tx = w0 * fs elsif tj != 0 # don't apply spacing to TJ displacement tx = (w0 - (tj/1000.0)) * fs * th else # apply horizontal scaling to spacing values but not font size tx = ((w0 * fs) + tc + tw) * th end # TODO: support ty > 0 ty = 0 @text_matrix.prepend_translation!(tx, ty) @text_rendering_matrix = nil # invalidate cached value # we used to invalidate @font_size here too, but it's not required # font_size depends only on trm.b/trm.d (scale/rotation), not trm.e/f (translation), # so it remains valid after a glyph displacement and does not need invalidation end |
#restore_graphics_state ⇒ Object
Restore the state to the previous value on the stack.
60 61 62 |
# File 'lib/pdf/reader/page_state.rb', line 60 def restore_graphics_state @stack.pop end |
#save_graphics_state ⇒ Object
Clones the current graphics state and push it onto the top of the stack. Any changes that are subsequently made to the state can then by reversed by calling restore_graphics_state.
53 54 55 |
# File 'lib/pdf/reader/page_state.rb', line 53 def save_graphics_state @stack.push clone_state end |
#set_character_spacing(char_spacing) ⇒ Object
Text State Operators
107 108 109 |
# File 'lib/pdf/reader/page_state.rb', line 107 def set_character_spacing(char_spacing) state[:char_spacing] = char_spacing end |
#set_horizontal_text_scaling(h_scaling) ⇒ Object
111 112 113 |
# File 'lib/pdf/reader/page_state.rb', line 111 def set_horizontal_text_scaling(h_scaling) state[:h_scaling] = h_scaling / 100.0 end |
#set_spacing_next_line_show_text(aw, ac, string) ⇒ Object
"
185 186 187 188 189 |
# File 'lib/pdf/reader/page_state.rb', line 185 def set_spacing_next_line_show_text(aw, ac, string) # " set_word_spacing(aw) set_character_spacing(ac) move_to_next_line_and_show_text(string) end |
#set_text_font_and_size(label, size) ⇒ Object
115 116 117 118 |
# File 'lib/pdf/reader/page_state.rb', line 115 def set_text_font_and_size(label, size) state[:text_font] = label state[:text_font_size] = size end |
#set_text_leading(leading) ⇒ Object
128 129 130 |
# File 'lib/pdf/reader/page_state.rb', line 128 def set_text_leading(leading) state[:text_leading] = leading end |
#set_text_matrix_and_text_line_matrix(a, b, c, d, e, f) ⇒ Object
Tm
159 160 161 162 163 164 165 166 167 |
# File 'lib/pdf/reader/page_state.rb', line 159 def set_text_matrix_and_text_line_matrix(a, b, c, d, e, f) # Tm @text_matrix = TransformationMatrix.new( a, b, c, d, e, f ) @text_line_matrix = @text_matrix.dup @font_size = @text_rendering_matrix = nil # invalidate cached value end |
#set_text_rendering_mode(mode) ⇒ Object
132 133 134 |
# File 'lib/pdf/reader/page_state.rb', line 132 def set_text_rendering_mode(mode) state[:text_mode] = mode end |
#set_text_rise(rise) ⇒ Object
136 137 138 |
# File 'lib/pdf/reader/page_state.rb', line 136 def set_text_rise(rise) state[:text_rise] = rise end |
#set_word_spacing(word_spacing) ⇒ Object
140 141 142 |
# File 'lib/pdf/reader/page_state.rb', line 140 def set_word_spacing(word_spacing) state[:word_spacing] = word_spacing end |
#show_text_with_positioning(params) ⇒ Object
Text Showing Operators
177 178 179 |
# File 'lib/pdf/reader/page_state.rb', line 177 def show_text_with_positioning(params) # TJ # TODO record position changes in state here end |
#stack_depth ⇒ Object
when save_graphics_state is called, we need to push a new copy of the current state onto the stack. That way any modifications to the state will be undone once restore_graphics_state is called.
332 333 334 |
# File 'lib/pdf/reader/page_state.rb', line 332 def stack_depth @stack.size end |
#trm_transform(x, y) ⇒ Object
transform x and y co-ordinates from the current text space to the underlying device space.
Deprecated. Prefer trm_transform_point()
263 264 265 266 267 268 269 270 |
# File 'lib/pdf/reader/page_state.rb', line 263 def trm_transform(x, y) # TODO print a deprecation warning res = trm_transform_point(x, y) [ res.x, res.y ] end |
#trm_transform_point(pt, opt_y = -1)) ⇒ Object
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 |
# File 'lib/pdf/reader/page_state.rb', line 287 def trm_transform_point(pt, opt_y = -1) unless pt.is_a?(Point) pt = Point.new(pt.to_i, opt_y.to_i) end trm = text_rendering_matrix if pt.x == 0 && pt.y == 0 Point.new(trm.e, trm.f) else Point.new( (trm.a * pt.x) + (trm.c * pt.y) + (trm.e), (trm.b * pt.x) + (trm.d * pt.y) + (trm.f) ) end end |