# -*- encoding: utf-8; frozen_string_literal: true -*- # #-- # This file is part of HexaPDF. # # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby # Copyright (C) 2014-2021 Thomas Leitner # # HexaPDF is free software: you can redistribute it and/or modify it # under the terms of the GNU Affero General Public License version 3 as # published by the Free Software Foundation with the addition of the # following permission added to Section 15 as permitted in Section 7(a): # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON # INFRINGEMENT OF THIRD PARTY RIGHTS. # # HexaPDF is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public # License for more details. # # You should have received a copy of the GNU Affero General Public License # along with HexaPDF. If not, see . # # The interactive user interfaces in modified source and object code # versions of HexaPDF must display Appropriate Legal Notices, as required # under Section 5 of the GNU Affero General Public License version 3. # # In accordance with Section 7(b) of the GNU Affero General Public # License, a covered work must retain the producer line in every PDF that # is created or manipulated using HexaPDF. # # If the GNU Affero General Public License doesn't fit your need, # commercial licenses are available at . #++ require 'hexapdf/content/operator' require 'hexapdf/content/graphics_state' module HexaPDF module Content # This class is used for processing content operators extracted from a content stream. # # == General Information # # When a content stream is read, operators and their operands are extracted. After extracting # these operators are normally processed with a Processor instance that ensures that the needed # setup (like modifying the graphics state) is done before further processing. # # == How Processing Works # # The operator implementations (see the Operator module) are called first and they ensure that # the processing state is consistent. For example, operators that modify the graphics state do # actually modify the #graphics_state object. However, operator implementations are *only* used # for this task and not more, so they are very specific and normally don't need to be changed. # # After that methods corresponding to the operator names are invoked on the processor object (if # they exist). Each PDF operator name is mapped to a nicer message name via the # OPERATOR_MESSAGE_NAME_MAP constant. For example, the operator 'q' is mapped to # 'save_graphics_state". # # The task of these methods is to do something useful with the content itself, it doesn't need # to concern itself with ensuring the consistency of the processing state. For example, the # processor could use the processing state to extract the text. Or paint the content on a # canvas. # # For inline images only the 'BI' operator mapped to 'inline_image' is used. Although also the # operators 'ID' and 'EI' exist for inline images, they are not used because they are consumed # while parsing inline images and do not reflect separate operators. # # == Text Processing # # Two utility methods #decode_text and #decode_text_with_positioning for extracting text are # provided. Both can directly be invoked from the 'show_text' and 'show_text_with_positioning' # methods. # class Processor # Represents an (immutable) glyph box with positioning information. # # Since the glyph may have been transformed by an affine matrix, the bounding may not be a # rectangle in all cases but it is always a parallelogram. class GlyphBox # The code point representing the glyph. attr_reader :code_point # The Unicode value of the code point. attr_reader :string # Creates a new glyph box for the given code point/Unicode value pair with the lower left # coordinate [llx, lly], the lower right coordinate [lrx, lry], and the upper left # coordinate [ulx, uly]. def initialize(code_point, string, llx, lly, lrx, lry, ulx, uly) @code_point = code_point @string = string.freeze @llx = llx @lly = lly @lrx = lrx @lry = lry @ulx = ulx @uly = uly freeze end # :call-seq: # fragment.lower_left -> [llx, lly] # # Returns the lower left coordinate def lower_left [@llx, @lly] end # :call-seq: # fragment.lower_right -> [lrx, lry] # # Returns the lower right coordinate def lower_right [@lrx, @lry] end # :call-seq: # fragment.upper_left -> [ulx, uly] # # Returns the upper left coordinate def upper_left [@ulx, @uly] end # :call-seq: # fragment.upper_right -> [urx, ury] # # Returns the upper right coordinate which is computed by using the other three points of # the parallelogram. def upper_right [@ulx + (@lrx - @llx), @uly + (@lry - @lly)] end # :call-seq: # fragment.points -> [llx, lly, lrx, lry, urx, ury, ulx, uly] # # Returns the four corners of the box as an array of coordinates, starting with the lower # left corner and going counterclockwise. def points [@llx, @lly, @lrx, @lry, @ulx + (@lrx - @llx), @uly + (@lry - @lly), @ulx, @uly] end end # Represents a box composed of GlyphBox objects. # # The bounding box methods #lower_left, #lower_right, #upper_left, #upper_right are computed # by just using the first and last boxes, assuming the boxes are arranged from left to right # in a straight line. class CompositeBox # The text boxes contained in this positioned text object. attr_reader :boxes # Creates an empty object. def initialize @boxes = [] end # Appends the given text glyph box. def <<(glyph_box) @boxes << glyph_box self end # Returns the glyph box at the given index, or +nil+ if the index is out of range. def [](index) @boxes[index] end # :call-seq: # composite.each {|glyph_box| block} -> composite # composite.each -> Enumerator # # Iterates over all contained glyph boxes. def each(&block) return to_enum(__method__) unless block_given? @boxes.each(&block) self end # Returns the concatenated text of the boxes. def string @boxes.map(&:string).join('') end # :call-seq: # text.lower_left -> [llx, lly] # # Returns the lower left coordinate def lower_left @boxes[0].lower_left end # :call-seq: # text.lower_right -> [lrx, lry] # # Returns the lower right coordinate def lower_right @boxes[-1].lower_right end # :call-seq: # text.upper_left -> [ulx, uly] # # Returns the upper left coordinate def upper_left @boxes[0].upper_left end # :call-seq: # text.upper_right -> [urx, ury] # # Returns the upper right coordinate. def upper_right @boxes[-1].upper_right end end # Mapping of PDF operator names to message names that are sent to renderer implementations. OPERATOR_MESSAGE_NAME_MAP = { q: :save_graphics_state, Q: :restore_graphics_state, cm: :concatenate_matrix, w: :set_line_width, J: :set_line_cap_style, j: :set_line_join_style, M: :set_miter_limit, d: :set_line_dash_pattern, ri: :set_rendering_intent, i: :set_flatness_tolerance, gs: :set_graphics_state_parameters, CS: :set_stroking_color_space, cs: :set_non_stroking_color_space, SC: :set_stroking_color, SCN: :set_stroking_color, sc: :set_non_stroking_color, scn: :set_non_stroking_color, G: :set_device_gray_stroking_color, g: :set_device_gray_non_stroking_color, RG: :set_device_rgb_stroking_color, rg: :set_device_rgb_non_stroking_color, K: :set_device_cmyk_stroking_color, k: :set_device_cmyk_non_stroking_color, m: :move_to, l: :line_to, c: :curve_to, v: :curve_to_no_first_control_point, y: :curve_to_no_second_control_point, h: :close_subpath, re: :append_rectangle, S: :stroke_path, s: :close_and_stroke_path, f: :fill_path_non_zero, F: :fill_path_non_zero, 'f*'.to_sym => :fill_path_even_odd, B: :fill_and_stroke_path_non_zero, 'B*'.to_sym => :fill_and_stroke_path_even_odd, b: :close_fill_and_stroke_path_non_zero, 'b*'.to_sym => :close_fill_and_stroke_path_even_odd, n: :end_path, W: :clip_path_non_zero, 'W*'.to_sym => :clip_path_even_odd, BT: :begin_text, ET: :end_text, Tc: :set_character_spacing, Tw: :set_word_spacing, Tz: :set_horizontal_scaling, TL: :set_leading, Tf: :set_font_and_size, Tr: :set_text_rendering_mode, Ts: :set_text_rise, Td: :move_text, TD: :move_text_and_set_leading, Tm: :set_text_matrix, 'T*'.to_sym => :move_text_next_line, Tj: :show_text, '\''.to_sym => :move_text_next_line_and_show_text, '"'.to_sym => :set_spacing_move_text_next_line_and_show_text, TJ: :show_text_with_positioning, d0: :set_glyph_width, # only for Type 3 fonts d1: :set_glyph_width_and_bounding_box, # only for Type 3 fonts sh: :paint_shading, BI: :inline_image, # ID and EI are not sent because the complete image has been read Do: :paint_xobject, MP: :designate_marked_content_point, DP: :designate_marked_content_point_with_property_list, BMC: :begin_marked_content, BDC: :begin_marked_content_with_property_list, EMC: :end_marked_content, BX: :begin_compatibility_section, EX: :end_compatibility_section, }.freeze # Mapping from operator name (Symbol) to a callable object. # # This hash is prepopulated with the default operator implementations (see # Operator::DEFAULT_OPERATORS). If a default operator implementation is not satisfactory, it # can easily be changed by modifying this hash. attr_reader :operators # The resources dictionary used during processing. attr_reader :resources # The GraphicsState object containing the current graphics state. # # It is not advised that this attribute is changed manually, it is automatically adjusted # according to the processed operators! attr_reader :graphics_state # The current graphics object. # # It is not advised that this attribute is changed manually, it is automatically adjusted # according to the processed operators! # # This attribute can have the following values: # # :none:: No current graphics object, i.e. the processor is at the page description level. # :path:: The current graphics object is a path. # :clipping_path:: The current graphics object is a clipping path. # :text:: The current graphics object is text. # # See: PDF1.7 s8.2 attr_accessor :graphics_object # Initializes a new processor that uses the resources PDF dictionary for resolving resources # while processing operators. # # It is not mandatory to set the resources dictionary on initialization but it needs to be set # prior to processing operators! def initialize(resources = nil) @operators = Operator::DEFAULT_OPERATORS.dup @graphics_state = GraphicsState.new @graphics_object = :none @original_resources = nil self.resources = resources end # Sets the resources dictionary used during processing. # # The first time resources are set, they are also stored as the "original" resources. This is # needed because form XObject don't need to have a resources dictionary and can use the page's # resources dictionary instead. def resources=(res) @original_resources = res if @original_resources.nil? @resources = res end # Processes the operator with the given operands. # # The operator is first processed with an operator implementation (if any) to ensure correct # operations and then the corresponding method on this object is invoked. def process(operator, operands = []) @operators[operator].invoke(self, *operands) if @operators.key?(operator) msg = OPERATOR_MESSAGE_NAME_MAP[operator] send(msg, *operands) if msg && respond_to?(msg, true) end protected # Provides a default implementation for the 'Do' operator. # # It checks if the XObject is a Form XObject and if so, processes the contents of the Form # XObject. def paint_xobject(name) xobject = resources.xobject(name) return unless xobject[:Subtype] == :Form res = resources graphics_state.save graphics_state.ctm.premultiply(*xobject[:Matrix]) if xobject.key?(:Matrix) xobject.process_contents(self, original_resources: @original_resources) graphics_state.restore self.resources = res end # Decodes the given text object and returns it as UTF-8 string. # # The argument may either be a simple text string (+Tj+ operator) or an array that contains # text strings together with positioning information (+TJ+ operator). def decode_text(data) if data.kind_of?(Array) data = data.each_with_object(''.b) {|obj, result| result << obj if obj.kind_of?(String) } end font = graphics_state.font font.decode(data).map {|code_point| font.to_utf8(code_point) }.join('') end # Decodes the given text object and returns it as a CompositeBox object. # # The argument may either be a simple text string (+Tj+ operator) or an array that contains # text strings together with positioning information (+TJ+ operator). # # For each glyph a GlyphBox object is computed. For horizontal fonts the width is # predetermined but not the height. The latter is chosen to be the height and offset of the # font's bounding box. def decode_text_with_positioning(data) data = Array(data) if graphics_state.font.writing_mode == :horizontal decode_horizontal_text(data) else decode_vertical_text(data) end end private # Decodes the given array containing text and positioning information while assuming that the # writing direction is horizontal. # # See: PDF1.7 s9.4.4 def decode_horizontal_text(array) font = graphics_state.font scaled_char_space = graphics_state.scaled_character_spacing scaled_word_space = (font.word_spacing_applicable? ? graphics_state.scaled_word_spacing : 0) scaled_font_size = graphics_state.scaled_font_size below_baseline = font.bounding_box[1] * scaled_font_size / \ graphics_state.scaled_horizontal_scaling + graphics_state.text_rise above_baseline = font.bounding_box[3] * scaled_font_size / \ graphics_state.scaled_horizontal_scaling + graphics_state.text_rise text = CompositeBox.new array.each do |item| if item.kind_of?(Numeric) graphics_state.tm.translate(-item * scaled_font_size, 0) else font.decode(item).each do |code_point| char = font.to_utf8(code_point) width = font.width(code_point) * scaled_font_size + scaled_char_space + \ (code_point == 32 ? scaled_word_space : 0) matrix = graphics_state.ctm.dup.premultiply(*graphics_state.tm) fragment = GlyphBox.new(code_point, char, *matrix.evaluate(0, below_baseline), *matrix.evaluate(width, below_baseline), *matrix.evaluate(0, above_baseline)) text << fragment graphics_state.tm.translate(width, 0) end end end text.freeze end # Decodes the given array containing text and positioning information while assuming that the # writing direction is vertical. def decode_vertical_text(_data) raise NotImplementedError end end end end