# coding: utf-8
require 'matrix'
module PDF
class Reader
class PageState
DEFAULT_GRAPHICS_STATE = {
:ctm => Matrix.identity(3),
:char_spacing => 0,
:word_spacing => 0,
:h_scaling => 100,
:text_leading => 0,
:text_font => nil,
:text_font_size => nil,
:text_mode => 0,
:text_rise => 0,
:text_knockout => 0
}
# starting a new page
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]
end
#####################################################
# Graphics State Operators
#####################################################
def save_graphics_state
@stack.push clone_state
end
def restore_graphics_state
@stack.pop
end
#####################################################
# Matrix Operators
#####################################################
# 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.
#
def concatenate_matrix(a, b, c, d, e, f)
transform = Matrix[
[a, b, 0],
[c, d, 0],
[e, f, 1]
]
if state[:ctm]
state[:ctm] = transform * state[:ctm]
else
state[:ctm] = transform
end
end
#####################################################
# Text Object Operators
#####################################################
def begin_text_object
@text_matrix = Matrix.identity(3)
@text_line_matrix = Matrix.identity(3)
end
def end_text_object
@text_matrix = Matrix.identity(3)
@text_line_matrix = Matrix.identity(3)
end
#####################################################
# Text State Operators
#####################################################
def set_character_spacing(char_spacing)
state[:char_spacing] = char_spacing
end
def set_horizontal_text_scaling(h_scaling)
state[:h_scaling] = h_scaling
end
def set_text_font_and_size(label, size)
state[:text_font] = label
state[:text_font_size] = size
end
def font_size
state[:text_font_size] * @text_matrix[0,0]
end
def set_text_leading(leading)
state[:text_leading] = leading
end
def set_text_rendering_mode(mode)
state[:text_mode] = mode
end
def set_text_rise(rise)
state[:text_rise] = rise
end
def set_word_spacing(word_spacing)
state[:word_spacing] = word_spacing
end
#####################################################
# Text Positioning Operators
#####################################################
def move_text_position(x, y) # Td
temp_matrix = Matrix[
[1, 0, 0],
[0, 1, 0],
[x, y, 1]
]
@text_matrix = @text_line_matrix = temp_matrix * @text_line_matrix
end
def move_text_position_and_set_leading(x, y) # TD
set_text_leading(-1 * y)
move_text_position(x, y)
end
def set_text_matrix_and_text_line_matrix(a, b, c, d, e, f) # Tm
@text_matrix = @text_line_matrix = Matrix[
[a, b, 0],
[c, d, 0],
[e, f, 1]
]
end
def move_to_start_of_next_line # T*
move_text_position(0, -state[:text_leading])
end
#####################################################
# Text Showing Operators
#####################################################
def show_text_with_positioning(params) # TJ
# TODO record position changes in state here
end
def move_to_next_line_and_show_text(str) # '
move_to_start_of_next_line
end
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
#####################################################
# XObjects
#####################################################
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
#####################################################
# Public Visible State
#####################################################
# transform x and y co-ordinates from the current user space to the
# underlying device space.
#
def ctm_transform(x, y, z = 1)
[
(ctm[0,0] * x) + (ctm[1,0] * y) + (ctm[2,0] * z),
(ctm[0,1] * x) + (ctm[1,1] * y) + (ctm[2,1] * z)
]
end
# transform x and y co-ordinates from the current text space to the
# underlying device space.
#
def trm_transform(x, y, z = 1)
trm = text_rendering_matrix
[
(trm[0,0] * x) + (trm[1,0] * y) + (trm[2,0] * z),
(trm[0,1] * x) + (trm[1,1] * y) + (trm[2,1] * z)
]
end
def current_font
find_font(state[:text_font])
end
def find_font(label)
dict = @font_stack.detect { |fonts|
fonts.has_key?(label)
}
dict ? dict[label] : nil
end
def find_color_space(label)
dict = @cs_stack.detect { |colorspaces|
colorspaces.has_key?(label)
}
dict ? dict[label] : nil
end
def find_xobject(label)
dict = @xobject_stack.detect { |xobjects|
xobjects.has_key?(label)
}
dict ? dict[label] : nil
end
private
def text_rendering_matrix
state_matrix = Matrix[
[font_size * state[:h_scaling], 0, 0],
[0, font_size, 0],
[0, state[:text_rise], 1]
]
state_matrix * @text_matrix * ctm
end
# return the current transformation matrix
#
def ctm
state[:ctm]
end
def state
@stack.last
end
# wrap the raw PDF Font objects in handy ruby Font objects.
#
def build_fonts(raw_fonts)
wrapped_fonts = raw_fonts.map { |label, font|
[label, PDF::Reader::Font.new(@objects, @objects.deref(font))]
}
::Hash[wrapped_fonts]
end
# 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.
#
# 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.
#
def clone_state
if @stack.empty?
{}
else
Marshal.load Marshal.dump(@stack.last)
end
end
end
end
end