# -*- encoding: utf-8 -*- # #-- # This file is part of HexaPDF. # # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby # Copyright (C) 2016 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. #++ require 'hexapdf/content/graphics_state' require 'hexapdf/content/operator' require 'hexapdf/serializer' require 'hexapdf/utils/math_helpers' require 'hexapdf/content/graphic_object' require 'hexapdf/stream' module HexaPDF module Content # This class provides the basic drawing operations supported by PDF. # # == General Information # # A canvas object is used for modifying content streams on a level higher than text. It would # be possible to write a content stream by hand since PDF uses a simplified reversed polish # notation for specifying operators: First come the operands, then comes the operator and no # operator returns any result. However, it is easy to make mistakes this way and one has to # know all operators and their operands. # # This is rather tedious and therefore this class exists. It allows one to modify a content # stream by invoking methods that should be familiar to anyone that has ever used a graphic # API. There are methods for moving the current point, drawing lines and curves, setting the # color, line width and so on. # # The PDF operators themselves are implemented as classes, see Operator. The canvas class uses # the Operator::BaseOperator#invoke and Operator::BaseOperator#serialize methods for applying # changes and serialization, with one exception: color setters don't invoke the corresponding # operator implementation but directly work on the graphics state. # # # == PDF Graphics # # === Graphics Operators and Objects # # There are about 60 PDF content stream operators. Some are used for changing the graphics # state, some for drawing paths and others for showing text. This is all abstracted through # the Canvas class. # # PDF knows about five different graphics objects: path objects, text objects, external # objects, inline image objects and shading objects. If none of the five graphics objects is # current, the content stream is at the so called page description level (in between graphics # objects). # # Additionally the PDF operators are divided into several groups, like path painting or text # showing operators, and such groups of operators are allowed to be used only in certain # graphics objects or the page description level. # # Have a look at the PDF specification (PDF1.7 s8.2) for more details. # # HexaPDF tries to ensure the proper use of the operators and graphics objects and if it # cannot do it, an error is raised. So if you don't modify a content stream directly but via # the Canvas methods, you generally don't have to worry about the low-level inner workings. # # === Graphics State # # Some operators modify the so called graphics state (see Content::GraphicsState). The graphics # state is a collection of settings that is used during processing or creating a content stream. # For example, the path painting operators don't have operands to specify the line width or the # stroke color but take this information from the graphics state. # # One important thing about the graphics state is that it is only possible to restore a prior # state using the save and restore methods. It is not possible to reset the graphics state # while creating the content stream! # # === Paths # # A PDF path object consists of one or more subpaths. Each subpath can be a rectangle or can # consist of lines and cubic bezier curves. No other types of subpaths are known to PDF. # However, the Canvas class contains additional methods that use the basic path construction # methods for drawing other paths like circles. # # When a subpath is started, the current graphics object is changed to :path. After all path # constructions are finished, a path painting method needs to be invoked to change back to the # page description level. Optionally, the path painting method may be preceeded by a clipping # path method to change the current clipping path (see #clip_path). # # There are four kinds of path painting methods: # # * Those that stroke the path, # * those that fill the path, # * those that stroke and fill the path and # * one to neither stroke or fill the path (used, for example, to just set the clipping path). # # In addition filling may be done using either the nonzero winding number rule or the even-odd # rule. # # # == Special Graphics State Methods # # These methods are only allowed when the current graphics object is :none, i.e. operations are # done on the page description level. # # * #save_graphics_state # * #restore_graphics_state # * #transform, #rotate, #scale, #translate, #skew # # See: PDF1.7 s8, s9 class Canvas include HexaPDF::Utils::MathHelpers # The context for which the canvas was created (a HexaPDF::Type::Page or HexaPDF::Type::Form # object). attr_reader :context # The serialized contents produced by the various canvas operations up to this point. # # Note that the returned string may not be a completely valid PDF content stream since a # graphic object may be open or the graphics state not completely restored. # # See: #stream_data attr_reader :contents # A StreamData object representing the serialized contents produced by the various canvas # operations. # # In contrast to #contents, it is ensured that an open graphics object is closed and all saved # graphics states are restored when the contents of the stream data object is read. *Note* # that this means that reading the stream data object may change the state of the canvas. attr_reader :stream_data # The Content::GraphicsState object containing the current graphics state. # # The graphics state must not be changed directly, only by using the provided methods. If it # is changed directly, the output will not be correct. attr_reader :graphics_state # The current graphics object. # # The graphics object should not be changed directly. It is automatically updated according # to the invoked methods. # # This attribute can have the following values: # # :none:: No current graphics object, i.e. 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 a text object. # # See: PDF1.7 s8.2 attr_accessor :graphics_object # The current point [x, y] of the path. # # This attribute holds the current point which is only valid if the current graphics objects # is :path. # # When the current point changes, the array is modified in place instead of creating a new # array! attr_reader :current_point # The operator name/implementation map used when invoking or serializing an operator. attr_reader :operators # Creates a new Canvas object for the given context object (either a Page or a Form). def initialize(context) @context = context @operators = Operator::DEFAULT_OPERATORS.dup @graphics_state = GraphicsState.new @graphics_object = :none @font = nil @serializer = HexaPDF::Serializer.new @current_point = [0, 0] @start_point = [0, 0] @contents = ''.b @stream_data = HexaPDF::StreamData.new do case graphics_object when :path, :clipping_path then end_path when :text then end_text end restore_graphics_state while graphics_state.saved_states? @contents end end # Returns the resource dictionary of the context object. def resources @context.resources end # :call-seq: # canvas.save_graphics_state => canvas # canvas.save_graphics_state { block } => canvas # # Saves the current graphics state and returns self. # # If invoked without a block a corresponding call to #restore_graphics_state must be done. # Otherwise the graphics state is automatically restored when the block is finished. # # Examples: # # # With a block # canvas.save_graphics_state do # canvas.line_width(10) # canvas.line(100, 100, 200, 200) # end # # # Same without a block # canvas.save_graphics_state # canvas.line_width(10) # canvas.line(100, 100, 200, 200) # canvas.restore_graphics_state # # See: PDF1.7 s8.4.2, #restore_graphics_state def save_graphics_state raise_unless_at_page_description_level invoke0(:q) if block_given? yield restore_graphics_state end self end # :call-seq: # canvas.restore_graphics_state => canvas # # Restores the current graphics state and returns self. # # Must not be invoked more times than #save_graphics_state. # # See: PDF1.7 s8.4.2, #save_graphics_state def restore_graphics_state raise_unless_at_page_description_level invoke0(:Q) self end # :call-seq: # canvas.transform(a, b, c, d, e, f) => canvas # canvas.transform(a, b, c, d, e, f) { block } => canvas # # Transforms the user space by applying the given matrix to the current transformation # matrix and returns self. # # If invoked with a block, the transformation is only active during the block by saving and # restoring the graphics state. # # The given values are interpreted as a matrix in the following way: # # a b 0 # c d 0 # e f 1 # # Examples: # # canvas.transform(1, 0, 0, 1, 100, 100) do # Translate origin to (100, 100) # canvas.line(0, 0, 100, 100) # Actually from (100, 100) to (200, 200) # end # canvas.line(0, 0, 100, 100) # Again from (0, 0) to (100, 100) # # See: PDF1.7 s8.3, s8.4.4 def transform(a, b, c, d, e, f) raise_unless_at_page_description_level save_graphics_state if block_given? invoke(:cm, a, b, c, d, e, f) if block_given? yield restore_graphics_state end self end # :call-seq: # canvas.rotate(angle, origin: nil) => canvas # canvas.rotate(angle, origin: nil) { block } => canvas # # Rotates the user space +angle+ degrees around the coordinate system origin or around the # given point and returns self. # # If invoked with a block, the rotation of the user space is only active during the block by # saving and restoring the graphics state. # # Note that the origin of the coordinate system itself doesn't change! # # origin:: # The point around which the user space should be rotated. # # Examples: # # canvas.rotate(90) do # Positive x-axis is now pointing upwards # canvas.line(0, 0, 100, 0) # Actually from (0, 0) to (0, 100) # end # canvas.line(0, 0, 100, 0) # Again from (0, 0) to (100, 0) # # canvas.rotate(90, origin: [100, 100]) do # canvas.line(100, 100, 200, 0) # Actually from (100, 100) to (100, 200) # end # # See: #transform def rotate(angle, origin: nil, &block) cos = Math.cos(deg_to_rad(angle)) sin = Math.sin(deg_to_rad(angle)) # Rotation is performed around the coordinate system origin but points are translated so # that the rotated rotation origin coincides with the unrotated one. tx = (origin ? origin[0] - (origin[0] * cos - origin[1] * sin) : 0) ty = (origin ? origin[1] - (origin[0] * sin + origin[1] * cos) : 0) transform(cos, sin, -sin, cos, tx, ty, &block) end # :call-seq: # canvas.scale(sx, sy = sx, origin: nil) => canvas # canvas.scale(sx, sy = sx, origin: nil) { block } => canvas # # Scales the user space +sx+ units in the horizontal and +sy+ units in the vertical # direction and returns self. If the optional +origin+ is specified, scaling is done from # that point. # # If invoked with a block, the scaling is only active during the block by saving and # restoring the graphics state. # # Note that the origin of the coordinate system itself doesn't change! # # origin:: # The point from which the user space should be scaled. # # Examples: # # canvas.scale(2, 3) do # Point (1, 1) is now actually (2, 3) # canvas.line(50, 50, 100, 100) # Actually from (100, 150) to (200, 300) # end # canvas.line(0, 0, 100, 0) # Again from (0, 0) to (100, 0) # # canvas.scale(2, 3, origin: [50, 50]) do # canvas.line(50, 50, 100, 100) # Actually from (50, 50) to (200, 300) # end # # See: #transform def scale(sx, sy = sx, origin: nil, &block) # As with rotation, scaling is performed around the coordinate system origin but points # are translated so that the scaled scaling origin coincides with the unscaled one. tx = (origin ? origin[0] - origin[0] * sx : 0) ty = (origin ? origin[1] - origin[1] * sy : 0) transform(sx, 0, 0, sy, tx, ty, &block) end # :call-seq: # canvas.translate(x, y) => canvas # canvas.translate(x, y) { block } => canvas # # Translates the user space coordinate system origin to the given +x+ and +y+ coordinates # and returns self. # # If invoked with a block, the translation of the user space is only active during the block # by saving and restoring the graphics state. # # Examples: # # canvas.translate(100, 100) do # Origin is now at (100, 100) # canvas.line(0, 0, 100, 0) # Actually from (100, 100) to (200, 100) # end # canvas.line(0, 0, 100, 0) # Again from (0, 0) to (100, 0) # # See: #transform def translate(x, y, &block) transform(1, 0, 0, 1, x, y, &block) end # :call-seq: # canvas.skew(a, b, origin: nil) => canvas # canvas.skew(a, b, origin: nil) { block } => canvas # # Skews the the x-axis by +a+ degrees and the y-axis by +b+ degress and returns self. If the # optional +origin+ is specified, skewing is done from that point. # # If invoked with a block, the skewing is only active during the block by saving and # restoring the graphics state. # # Note that the origin of the coordinate system itself doesn't change! # # origin:: # The point from which the axes are skewed. # # Examples: # # canvas.skew(0, 45) do # Point (1, 1) is now actually (2, 1) # canvas.line(50, 50, 100, 100) # Actually from (100, 50) to (200, 100) # end # canvas.line(0, 0, 100, 0) # Again from (0, 0) to (100, 0) # # canvas.skew(0, origin: [50, 50]) do # canvas.line(50, 50, 100, 100) # Actually from (50, 50) to (200, 300) # end # # See: #transform def skew(a, b, origin: nil, &block) tan_a = Math.tan(deg_to_rad(a)) tan_b = Math.sin(deg_to_rad(b)) # As with rotation, skewing is performed around the coordinate system origin but points # are translated so that the skewed skewing origin coincides with the unskewed one. tx = (origin ? -origin[1] * tan_b : 0) ty = (origin ? -origin[0] * tan_a : 0) transform(1, tan_a, tan_b, 1, tx, ty, &block) end # :call-seq: # canvas.line_width => current_line_width # canvas.line_width(width) => canvas # canvas.line_width(width) { block } => canvas # # The line width determines the thickness of a stroked path. # # Returns the current line width (see Content::GraphicsState#line_width) when no argument is # given. Otherwise sets the line width to the given +width+ and returns self. The setter # version can also be called in the line_width= form. # # If the +width+ and a block are provided, the changed line width is only active during the # block by saving and restoring the graphics state. # # Examples: # # canvas.line_width(10) # canvas.line_width # => 10 # canvas.line_width = 5 # => 5 # # canvas.line_width(10) do # canvas.line_width # => 10 # end # canvas.line_width # => 5 # # See: PDF1.7 s8.4.3.2 def line_width(width = nil, &block) gs_getter_setter(:line_width, :w, width, &block) end alias :line_width= :line_width # :call-seq: # canvas.line_cap_style => current_line_cap_style # canvas.line_cap_style(style) => canvas # canvas.line_cap_style(style) { block } => canvas # # The line cap style specifies how the ends of stroked open paths should look like. The # +style+ parameter can either be a valid integer or one of the symbols +:butt+, +:round+ or # +:projecting_square+ (see Content::LineCapStyle.normalize for details). Note that the return # value is always a normalized line cap style. # # Returns the current line cap style (see Content::GraphicsState#line_cap_style) when no # argument is given. Otherwise sets the line cap style to the given +style+ and returns self. # The setter version can also be called in the line_cap_style= form. # # If the +style+ and a block are provided, the changed line cap style is only active during # the block by saving and restoring the graphics state. # # Examples: # # canvas.line_cap_style(:butt) # canvas.line_cap_style # => # # canvas.line_cap_style = :round # => # # # canvas.line_cap_style(:butt) do # canvas.line_cap_style # => # # end # canvas.line_cap_style # => # # # See: PDF1.7 s8.4.3.3 def line_cap_style(style = nil, &block) gs_getter_setter(:line_cap_style, :J, style && LineCapStyle.normalize(style), &block) end alias :line_cap_style= :line_cap_style # :call-seq: # canvas.line_join_style => current_line_join_style # canvas.line_join_style(style) => canvas # canvas.line_join_style(style) { block } => canvas # # The line join style specifies the shape that is used at the corners of stroked paths. The # +style+ parameter can either be a valid integer or one of the symbols +:miter+, +:round+ or # +:bevel+ (see Content::LineJoinStyle.normalize for details). Note that the return value is # always a normalized line join style. # # Returns the current line join style (see Content::GraphicsState#line_join_style) when no # argument is given. Otherwise sets the line join style to the given +style+ and returns self. # The setter version can also be called in the line_join_style= form. # # If the +style+ and a block are provided, the changed line join style is only active during # the block by saving and restoring the graphics state. # # Examples: # # canvas.line_join_style(:miter) # canvas.line_join_style # => # # canvas.line_join_style = :round # => # # # canvas.line_join_style(:bevel) do # canvas.line_join_style # => # # end # canvas.line_join_style # => # # # See: PDF1.7 s8.4.3.4 def line_join_style(style = nil, &block) gs_getter_setter(:line_join_style, :j, style && LineJoinStyle.normalize(style), &block) end alias :line_join_style= :line_join_style # :call-seq: # canvas.miter_limit => current_miter_limit # canvas.miter_limit(limit) => canvas # canvas.miter_limit(limit) { block } => canvas # # The miter limit specifies the maximum ratio of the miter length to the line width for # mitered line joins (see #line_join_style). When the limit is exceeded, a bevel join is # used instead of a miter join. # # Returns the current miter limit (see Content::GraphicsState#miter_limit) when no argument is # given. Otherwise sets the miter limit to the given +limit+ and returns self. The setter # version can also be called in the miter_limit= form. # # If the +limit+ and a block are provided, the changed miter limit is only active during the # block by saving and restoring the graphics state. # # Examples: # # canvas.miter_limit(10) # canvas.miter_limit # => 10 # canvas.miter_limit = 5 # => 5 # # canvas.miter_limit(10) do # canvas.miter_limit # => 10 # end # canvas.miter_limit # => 5 # # See: PDF1.7 s8.4.3.5 def miter_limit(limit = nil, &block) gs_getter_setter(:miter_limit, :M, limit, &block) end alias :miter_limit= :miter_limit # :call-seq: # canvas.line_dash_pattern => current_line_dash_pattern # canvas.line_dash_pattern(line_dash_pattern) => canvas # canvas.line_dash_pattern(length, phase = 0) => canvas # canvas.line_dash_pattern(array, phase = 0) => canvas # canvas.line_dash_pattern(value, phase = 0) { block } => canvas # # The line dash pattern defines the appearance of a stroked path (line _or_ curve), ie. if # it is solid or if it contains dashes and gaps. # # There are multiple ways to set the line dash pattern: # # * By providing a Content::LineDashPattern object # * By providing a single Integer/Float that is used for both dashes and gaps # * By providing an array of Integers/Floats that specify the alternating dashes and gaps # # The phase (i.e. the distance into the dashes/gaps at which to start) can additionally be # set in the last two cases. # # A solid line can be achieved by using 0 for the length or by using an empty array. # # Returns the current line dash pattern (see Content::GraphicsState#line_dash_pattern) when no # argument is given. Otherwise sets the line dash pattern using the given arguments and # returns self. The setter version can also be called in the line_dash_pattern= form (but only # without the second argument!). # # If arguments and a block are provided, the changed line dash pattern is only active during # the block by saving and restoring the graphics state. # # Examples: # # canvas.line_dash_pattern(10) # canvas.line_dash_pattern # => LineDashPattern.new([10], 0) # canvas.line_dash_pattern(10, 2) # canvas.line_dash_pattern([5, 3, 1], 2) # canvas.line_dash_pattern = LineDashPattern.new([5, 3, 1], 1) # # canvas.line_dash_pattern(10) do # canvas.line_dash_pattern # => LineDashPattern.new([10], 0) # end # canvas.line_dash_pattern # => LineDashPattern.new([5, 3, 1], 1) # # See: PDF1.7 s8.4.3.5, LineDashPattern def line_dash_pattern(value = nil, phase = 0, &block) case value when nil, LineDashPattern when Array value = LineDashPattern.new(value, phase) when 0 value = LineDashPattern.new([], 0) else value = LineDashPattern.new([value], phase) end gs_getter_setter(:line_dash_pattern, :d, value, &block) end alias :line_dash_pattern= :line_dash_pattern # :call-seq: # canvas.rendering_intent => current_rendering_intent # canvas.rendering_intent(intent) => canvas # canvas.rendering_intent(intent) { block } => canvas # # The rendering intent is used to specify the intent on how colors should be rendered since # sometimes compromises have to be made when the capabilities of an output device are not # sufficient. The +intent+ parameter can be one of the following symbols: # # * +:AbsoluteColorimetric+ # * +:RelativeColorimetric+ # * +:Saturation+ # * +:Perceptual+ # # Returns the current rendering intent (see Content::GraphicsState#rendering_intent) when no # argument is given. Otherwise sets the rendering intent using the +intent+ argument and # returns self. The setter version can also be called in the rendering_intent= form. # # If the +intent+ and a block are provided, the changed rendering intent is only active # during the block by saving and restoring the graphics state. # # Examples: # # canvas.rendering_intent(:Perceptual) # canvas.rendering_intent # => :Perceptual # canvas.rendering_intent = :Saturation # => :Saturation # # canvas.rendering_intent(:Perceptual) do # canvas.rendering_intent # => :Perceptual # end # canvas.rendering_intent # => :Saturation # # See: PDF1.7 s8.6.5.8, RenderingIntent def rendering_intent(intent = nil, &bk) gs_getter_setter(:rendering_intent, :ri, intent && RenderingIntent.normalize(intent), &bk) end alias :rendering_intent= :rendering_intent # :call-seq: # canvas.stroke_color => current_stroke_color # canvas.stroke_color(gray) => canvas # canvas.stroke_color(r, g, b) => canvas # canvas.stroke_color(c, m, y, k) => canvas # canvas.stroke_color(string) => canvas # canvas.stroke_color(color_object) => canvas # canvas.stroke_color(array) => canvas # canvas.stroke_color(color_spec) { block } => canvas # # The stroke color defines the color used for stroking operations, i.e. for painting paths. # # There are several ways to define the color that should be used: # # * A single numeric argument specifies a gray color (see # Content::ColorSpace::DeviceGray::Color). # * Three numeric arguments specify an RGB color (see Content::ColorSpace::DeviceRGB::Color). # * A string in the format "RRGGBB" where "RR" is the hexadecimal number for the red, "GG" # for the green and "BB" for the blue color value also specifies an RGB color. # * Four numeric arguments specify a CMYK color (see Content::ColorSpace::DeviceCMYK::Color). # * A color object is used directly (normally used for color spaces other than DeviceRGB, # DeviceCMYK and DeviceGray). # * An array is treated as if its items were specified separately as arguments. # # Returns the current stroke color (see Content::GraphicsState#stroke_color) when no argument # is given. Otherwise sets the stroke color using the given arguments and returns self. The # setter version can also be called in the stroke_color= form. # # If the arguments and a block are provided, the changed stroke color is only active during # the block by saving and restoring the graphics state. # # Examples: # # # With no arguments just returns the current color # canvas.stroke_color # => DeviceGray.color(0.0) # # # Same gray color because integer values are normalized to the range of 0.0 to 1.0 # canvas.stroke_color(102) # canvas.stroke_color(0.4) # # # Specifying RGB colors # canvas.stroke_color(255, 255, 0) # canvas.stroke_color("FFFF00") # # # Specifying CMYK colors # canvas.stroke_color(255, 255, 0, 128) # # # Can use a color object directly # color = HexaPDF::Content::ColorSpace::DeviceRGB.color(255, 255, 0) # canvas.stroke_color(color) # # # An array argument is destructured - these calls are all equal # cnavas.stroke_color(255, 255, 0) # canvas.stroke_color([255, 255, 0]) # canvas.stroke_color = [255, 255, 0] # # # As usual, can be invoked with a block to limit the effects # canvas.stroke_color(102) do # canvas.stroke_color # => ColorSpace::DeviceGray.color(0.4) # end # # See: PDF1.7 s8.6, ColorSpace def stroke_color(*color, &block) color_getter_setter(:stroke_color, color, :RG, :G, :K, :CS, :SCN, &block) end alias :stroke_color= :stroke_color # The fill color defines the color used for non-stroking operations, i.e. for filling paths. # # Works exactly the same #stroke_color but for the fill color. See #stroke_color for # details on invocation and use. def fill_color(*color, &block) color_getter_setter(:fill_color, color, :rg, :g, :k, :cs, :scn, &block) end alias :fill_color= :fill_color # :call-seq: # canvas.opacity => current_values # canvas.opacity(fill_alpha:) => canvas # canvas.opacity(stroke_alpha:) => canvas # canvas.opacity(fill_alpha:, stroke_alpha:) => canvas # canvas.opacity(fill_alpha:, stroke_alpha:) { block } => canvas # # The fill and stroke alpha values determine how opaque drawn elements will be. Note that # the fill alpha value applies not just to fill values but to all non-stroking operations # (e.g. images, ...). # # Returns the current fill alpha (see Content::GraphicsState#fill_alpha) and stroke alpha (see # Content::GraphicsState#stroke_alpha) values using a hash with the keys +:fill_alpha+ and # +:stroke_alpha+ when no argument is given. Otherwise sets the fill and stroke alpha values # and returns self. The setter version can also be called in the #opacity= form. # # If the values are set and a block is provided, the changed alpha values are only active # during the block by saving and restoring the graphics state. # # Examples: # # canvas.opacity(fill_alpha: 0.5) # canvas.opacity # => {fill_alpha: 0.5, stroke_alpha: 1.0} # canvas.opacity(fill_alpha: 0.4, stroke_alpha: 0.9) # canvas.opacity # => {fill_alpha: 0.4, stroke_alpha: 0.9} # # canvas.opacity(stroke_alpha: 0.7) do # canvas.opacity # => {fill_alpha: 0.4, stroke_alpha: 0.7} # end # canvas.opacity # => {fill_alpha: 0.4, stroke_alpha: 0.9} # # See: PDF1.7 s11.6.4.4 def opacity(fill_alpha: nil, stroke_alpha: nil) if !fill_alpha.nil? || !stroke_alpha.nil? raise_unless_at_page_description_level_or_in_text save_graphics_state if block_given? if (!fill_alpha.nil? && graphics_state.fill_alpha != fill_alpha) || (!stroke_alpha.nil? && graphics_state.stroke_alpha != stroke_alpha) dict = {Type: :ExtGState} dict[:CA] = stroke_alpha unless stroke_alpha.nil? dict[:ca] = fill_alpha unless fill_alpha.nil? dict[:AIS] = false if graphics_state.alpha_source invoke1(:gs, resources.add_ext_gstate(dict)) end if block_given? yield restore_graphics_state end self elsif block_given? raise ArgumentError, "Block only allowed with an argument" else {fill_alpha: graphics_state.fill_alpha, stroke_alpha: graphics_state.stroke_alpha} end end # :call-seq: # canvas.move_to(x, y) => canvas # # Begins a new subpath (and possibly a new path) by moving the current point to the given # point. # # Examples: # # canvas.move_to(100, 50) def move_to(x, y) raise_unless_at_page_description_level_or_in_path invoke2(:m, x, y) @current_point[0] = @start_point[0] = x @current_point[1] = @start_point[1] = y self end # :call-seq: # canvas.line_to(x, y) => canvas # # Appends a straight line segment from the current point to the given point (which becomes the # new current point) to the current subpath. # # Examples: # # canvas.line_to(100, 100) def line_to(x, y) raise_unless_in_path invoke2(:l, x, y) @current_point[0] = x @current_point[1] = y self end # :call-seq: # canvas.curve_to(x, y, p1:, p2:) => canvas # canvas.curve_to(x, y, p1:) => canvas # canvas.curve_to(x, y, p2:) => canvas # # Appends a cubic Bezier curve to the current subpath starting from the current point. The end # point becomes the new current point. # # A Bezier curve consists of the start point, the end point and the two control points +p1+ # and +p2+. The start point is always the current point and the end point is specified as # +x+ and +y+ arguments. # # Additionally, either the first control point +p1+ or the second control +p2+ or both # control points have to be specified (as arrays containing two numbers). If the first # control point is not specified, the current point is used as first control point. If the # second control point is not specified, the end point is used as the second control point. # # Examples: # # canvas.curve_to(100, 100, p1: [100, 50], p2: [50, 100]) # canvas.curve_to(100, 100, p1: [100, 50]) # canvas.curve_to(100, 100, p2: [50, 100]) def curve_to(x, y, p1: nil, p2: nil) raise_unless_in_path if p1 && p2 invoke(:c, *p1, *p2, x, y) elsif p1 invoke(:y, *p1, x, y) elsif p2 invoke(:v, *p2, x, y) else raise ArgumentError, "At least one control point must be specified for Bézier curves" end @current_point[0] = x @current_point[1] = y self end # :call-seq: # canvas.rectangle(x, y, width, height, radius: 0) => canvas # # Appends a rectangle to the current path as a complete subpath (drawn in counterclockwise # direction), with the lower-left corner specified by +x+ and +y+ and the given +width+ and # +height+. # # If +radius+ is greater than 0, the corners are rounded with the given radius. # # If there is no current path when the method is invoked, a new path is automatically begun. # # The current point is set to the lower-left corner if +radius+ is zero, otherwise it is set # to (x, y + radius). # # Examples: # # canvas.rectangle(100, 100, 100, 50) # canvas.rectangle(100, 100, 100, 50, radius: 10) def rectangle(x, y, width, height, radius: 0) raise_unless_at_page_description_level_or_in_path if radius == 0 invoke(:re, x, y, width, height) @current_point[0] = @start_point[0] = x @current_point[1] = @start_point[1] = y self else polygon(x, y, x + width, y, x + width, y + height, x, y + height, radius: radius) end end # :call-seq: # canvas.close_subpath => canvas # # Closes the current subpath by appending a straight line from the current point to the # start point of the subpath which also becomes the new current point. def close_subpath raise_unless_in_path invoke0(:h) @current_point = @start_point self end # :call-seq: # canvas.line(x0, y0, x1, y1) => canvas # # Moves the current point to (x0, y0) and appends a line to (x1, y1) to the current path. # # This method is equal to "canvas.move_to(x0, y0).line_to(x1, y1)". # # Examples: # # canvas.line(10, 10, 100, 100) def line(x0, y0, x1, y1) move_to(x0, y0) line_to(x1, y1) end # :call-seq: # canvas.polyline(x0, y0, x1, y1, x2, y2, ...) => canvas # # Moves the current point to (x0, y0) and appends line segments between all given # consecutive points, i.e. between (x0, y0) and (x1, y1), between (x1, y1) and (x2, y2) and # so on. The last point becomes the new current point. # # Examples: # # canvas.polyline(0, 0, 100, 0, 100, 100, 0, 100, 0, 0) def polyline(*points) check_poly_points(points) move_to(points[0], points[1]) i = 2 while i < points.length line_to(points[i], points[i + 1]) i += 2 end self end # :call-seq: # canvas.polygon(x0, y0, x1, y1, x2, y2, ..., radius: 0) => canvas # # Appends a polygon consisting of the given points to the path as a complete subpath. The # point (x0, y0 + radius) becomes the new current point. # # If +radius+ is greater than 0, the corners are rounded with the given radius. # # If there is no current path when the method is invoked, a new path is automatically begun. # # Examples: # # canvas.polygon(0, 0, 100, 0, 100, 100, 0, 100) # canvas.polygon(0, 0, 100, 0, 100, 100, 0, 100, radius: 10) def polygon(*points, radius: 0) if radius == 0 polyline(*points) else check_poly_points(points) move_to(*point_on_line(points[0], points[1], points[2], points[3], distance: radius)) points.concat(points[0, 4]) 0.step(points.length - 6, 2) {|i| line_with_rounded_corner(*points[i, 6], radius)} end close_subpath end # :call-seq: # canvas.circle(cx, cy, radius) => canvas # # Appends a circle with center (cx, cy) and the given radius (in degrees) to the path as a # complete subpath (drawn in counterclockwise direction). The point (center_x + radius, # center_y) becomes the new current point. # # If there is no current path when the method is invoked, a new path is automatically begun. # # Examples: # # canvas.circle(100, 100, 10) # # See: #arc (for approximation accuracy) def circle(cx, cy, radius) arc(cx, cy, a: radius) close_subpath end # :call-seq: # canvas.ellipse(cx, cy, a:, b:, inclination: 0) => canvas # # Appends an ellipse with center (cx, cy), semi-major axis +a+, semi-minor axis +b+ and an # inclination from the x-axis of +inclination+ degrees to the path as a complete subpath. The # outer-most point on the semi-major axis becomes the new current point. # # If there is no current path when the method is invoked, a new path is automatically begun. # # Examples: # # # Ellipse aligned to x-axis and y-axis # canvas.ellipse(100, 100, a: 10, b: 5) # # # Inclined ellipse # canvas.ellipse(100, 100, a: 10, b: 5, inclination: 45) # # See: #arc (for approximation accuracy) def ellipse(cx, cy, a:, b:, inclination: 0) arc(cx, cy, a: a, b: b, inclination: inclination) close_subpath end # :call-seq: # canvas.arc(cx, cy, a:, b: a, start_angle: 0, end_angle: 360, clockwise: false, inclination: 0) => canvas # # Appends an elliptical arc to the path. The endpoint of the arc becomes the new current # point. # # +cx+:: # x-coordinate of the center point of the arc # # +cy+:: # y-coordinate of the center point of the arc # # +a+:: # Length of semi-major axis # # +b+:: # Length of semi-minor axis (default: +a+) # # +start_angle+:: # Angle in degrees at which to start the arc (default: 0) # # +end_angle+:: # Angle in degrees at which to end the arc (default: 360) # # +clockwise+:: # If +true+ the arc is drawn in clockwise direction, otherwise in counterclockwise # direction. # # +inclination+:: # Angle in degrees between the x-axis and the semi-major axis (default: 0) # # If +a+ and +b+ are equal, a circular arc is drawn. If the difference of the start angle # and end angle is equal to 360, a full ellipse (or circle) is drawn. # # If there is no current path when the method is invoked, a new path is automatically begun. # # Since PDF doesn't have operators for drawing elliptical or circular arcs, they have to be # approximated using Bezier curves (see #curve_to). The accuracy of the approximation can be # controlled using the configuration option 'graphic_object.arc.max_curves'. # # Examples: # # canvas.arc(0, 0, a: 10) # Circle at (0, 0) with radius 10 # canvas.arc(0, 0, a: 10, b: 5) # Ellipse at (0, 0) with radii 10 and 5 # canvas.arc(0, 0, a: 10, b: 5, inclination: 45) # The above ellipse inclined 45 degrees # # # Circular and elliptical arcs from 45 degrees to 135 degrees # canvas.arc(0, 0, a: 10, start_angle: 45, end_angle: 135) # canvas.arc(0, 0, a: 10, b: 5, start_angle: 45, end_angle: 135) # # # Arcs from 135 degrees to 15 degrees, the first in counterclockwise direction (i.e. the # # big arc), the other in clockwise direction (i.e. the small arc) # canvas.arc(0, 0, a: 10, start_angle: 135, end_angle: 15) # canvas.arc(0, 0, a: 10, start_angle: 135, end_angle: 15, clockwise: true) # # See: Content::GraphicObject::Arc def arc(cx, cy, a:, b: a, start_angle: 0, end_angle: 360, clockwise: false, inclination: 0) arc = GraphicObject::Arc.configure(cx: cx, cy: cy, a: a, b: b, start_angle: start_angle, end_angle: end_angle, clockwise: clockwise, inclination: inclination) arc.draw(self) self end # :call-seq: # canvas.graphic_object(obj, **options) => obj # canvas.graphic_object(name, **options) => graphic_object # # Returns the named graphic object, configured with the given options. # # If an object responding to :configure is given, it is used. Otherwise the graphic object # is looked up via the given name in the configuration option 'graphic_object.map'. Then the # graphic object is configured with the given options if at least one is given. # # Examples: # # obj = canvas.graphic_object(:arc, cx: 10, cy: 10) # canvas.draw(obj) def graphic_object(obj, **options) unless obj.respond_to?(:configure) obj = context.document.config.constantize('graphic_object.map', obj) end obj = obj.configure(options) if options.size > 0 || !obj.respond_to?(:draw) obj end # :call-seq: # canvas.draw(obj, **options) => canvas # canvas.draw(name, **options) => canvas # # Draws the given graphic object on the canvas. # # See #graphic_object for information on the arguments. # # Examples: # # canvas.draw(:arc, cx: 10, cy: 10) def draw(name, **options) graphic_object(name, **options).draw(self) self end # :call-seq: # canvas.stroke => canvas # # Strokes the path. # # See: PDF1.7 s8.5.3.1, s8.5.3.2 def stroke raise_unless_in_path_or_clipping_path invoke0(:S) self end # :call-seq: # canvas.close_stroke => canvas # # Closes the last subpath and then strokes the path. # # See: PDF1.7 s8.5.3.1, s8.5.3.2 def close_stroke raise_unless_in_path_or_clipping_path invoke0(:s) self end # :call-seq: # canvas.fill(rule = :nonzero) => canvas # # Fills the path using the given rule. # # The argument +rule+ may either be +:nonzero+ to use the nonzero winding number rule or # +:even_odd+ to use the even-odd rule for determining which regions to fill in. # # Any open subpaths are implicitly closed before being filled. # # See: PDF1.7 s8.5.3.1, s8.5.3.3 def fill(rule = :nonzero) raise_unless_in_path_or_clipping_path invoke0(rule == :nonzero ? :f : :'f*') self end # :call-seq: # canvas.fill_stroke(rule = :nonzero) => canvas # # Fills and then strokes the path using the given rule. # # The argument +rule+ may either be +:nonzero+ to use the nonzero winding number rule or # +:even_odd+ to use the even-odd rule for determining which regions to fill in. # # See: PDF1.7 s8.5.3 def fill_stroke(rule = :nonzero) raise_unless_in_path_or_clipping_path invoke0(rule == :nonzero ? :B : :'B*') self end # :call-seq: # canvas.close_fill_stroke(rule = :nonzero) => canvas # # Closes the last subpath and then fills and strokes the path using the given rule. # # The argument +rule+ may either be +:nonzero+ to use the nonzero winding number rule or # +:even_odd+ to use the even-odd rule for determining which regions to fill in. # # See: PDF1.7 s8.5.3 def close_fill_stroke(rule = :nonzero) raise_unless_in_path_or_clipping_path invoke0(rule == :nonzero ? :b : :'b*') self end # :call-seq: # canvas.end_path => canvas # # Ends the path without stroking or filling it. # # This method is normally used in conjunction with the clipping path methods to define the # clipping. # # See: PDF1.7 s8.5.3.1 #clip def end_path raise_unless_in_path_or_clipping_path invoke0(:n) self end # :call-seq: # canvas.clip_path(rule = :nonzero) => canvas # # Modifies the clipping path by intersecting it with the current path. # # The argument +rule+ may either be +:nonzero+ to use the nonzero winding number rule or # +:even_odd+ to use the even-odd rule for determining which regions lie inside the clipping # path. # # Note that the current path cannot be modified after invoking this method! This means that # one of the path painting methods or #end_path must be called immediately afterwards. # # See: PDF1.7 s8.5.4 def clip_path(rule = :nonzero) raise_unless_in_path invoke0(rule == :nonzero ? :W : :'W*') self end # :call-seq: # canvas.xobject(filename, at:, width: nil, height: nil) => xobject # canvas.xobject(io, at:, width: nil, height: nil) => xobject # canvas.xobject(image_object, at:, width: nil, height: nil) => image_object # canvas.xobject(form_object, at:, width: nil, height: nil) => form_object # # Draws the given XObject (either an image XObject or a form XObject) at the specified # position and returns the XObject. # # Any image format for which a HexaPDF::ImageLoader object is available and registered with # the configuration option 'image_loader' can be used. PNG and JPEG images are supported out # of the box. # # If the filename or the IO specifies a PDF file, the first page of this file is used to # create a form XObject which is then drawn. # # The +at+ argument has to be an array containing two numbers specifying the lower-left # corner at which to draw the XObject. # # If +width+ and +height+ are specified, the drawn XObject will have exactly these # dimensions. If only one of them is specified, the other dimension is automatically # calculated so that the aspect ratio is retained. If neither is specified, the width and # height of the XObject are used (for images, 1 pixel being represented by 1 PDF point, i.e. # 72 DPI). # # *Note*: If a form XObject is drawn, all currently set graphics state parameters influence # the rendering of the form XObject. This means, for example, that when the line width is # set to 20, all lines of the form XObject are drawn with that line width unless the line # width is changed in the form XObject itself. # # Examples: # # canvas.xobject('test.png', at: [100, 100]) # canvas.xobject('test.pdf', at: [100, 100]) # # File.new('test.jpg', 'rb') do |io| # canvas.xobject(io, at: [100, 200], width: 300) # end # # image = document.object(5) # Object with oid=5 is an image XObject in this example # canvas.xobject(image, at: [100, 200], width: 200, heigth: 300) # # See: PDF1.7 s8.8, s.8.10.1 def xobject(obj, at:, width: nil, height: nil) unless obj.kind_of?(HexaPDF::Stream) obj = context.document.images.add(obj) end if obj[:Subtype] == :Image width, height = calculate_dimensions(obj[:Width], obj[:Height], rwidth: width, rheight: height) else width, height = calculate_dimensions(obj.box.width, obj.box.height, rwidth: width, rheight: height) width /= obj.box.width.to_f height /= obj.box.height.to_f at[0] -= obj.box.left at[1] -= obj.box.bottom end transform(width, 0, 0, height, at[0], at[1]) do invoke1(:Do, resources.add_xobject(obj)) end obj end alias :image :xobject # :call-seq: # canvas.character_spacing => current_character_spacing # canvas.character_spacing(amount) => canvas # canvas.character_spacing(amount) { block } => canvas # # The character spacing determines how much additional space is added between two # consecutive characters. For horizontal writing positive values increase the distance # between two characters, whereas for vertical writing negative values increase the # distance. # # Returns the current character spacing value (see Content::GraphicsState#character_spacing) # when no argument is given. Otherwise sets the character spacing using the +amount+ argument # and returns self. The setter version can also be called in the character_spacing= form. # # If the +amount+ and a block are provided, the changed character spacing is only active # during the block by saving and restoring the graphics state. # # Examples: # # canvas.character_spacing(0.25) # canvas.character_spacing # => 0.25 # canvas.character_spacing = 0.5 # => 0.5 # # canvas.character_spacing(0.10) do # canvas.character_spacing # => 0.10 # end # canvas.character_spacing # => 0.5 # # See: PDF1.7 s9.3.2 def character_spacing(amount = nil, &bk) gs_getter_setter(:character_spacing, :Tc, amount, &bk) end alias :character_spacing= :character_spacing # :call-seq: # canvas.word_spacing => current_word_spacing # canvas.word_spacing(amount) => canvas # canvas.word_spacing(amount) { block } => canvas # # The word spacing determines how much additional space is added when the ASCII space # character is encountered in a text. For horizontal writing positive values increase the # distance between two words, whereas for vertical writing negative values increase the # distance. # # Returns the current word spacing value (see Content::GraphicsState#word_spacing) when no # argument is given. Otherwise sets the word spacing using the +amount+ argument and returns # self. The setter version can also be called in the word_spacing= form. # # If the +amount+ and a block are provided, the changed word spacing is only active during # the block by saving and restoring the graphics state. # # Examples: # # canvas.word_spacing(0.25) # canvas.word_spacing # => 0.25 # canvas.word_spacing = 0.5 # => 0.5 # # canvas.word_spacing(0.10) do # canvas.word_spacing # => 0.10 # end # canvas.word_spacing # => 0.5 # # See: PDF1.7 s9.3.3 def word_spacing(amount = nil, &bk) gs_getter_setter(:word_spacing, :Tw, amount, &bk) end alias :word_spacing= :word_spacing # :call-seq: # canvas.horizontal_scaling => current_horizontal_scaling # canvas.horizontal_scaling(percent) => canvas # canvas.horizontal_scaling(percent) { block } => canvas # # The horizontal scaling adjusts the width of text character glyphs by stretching or # compressing them in the horizontal direction. The value is specified as percent of the # normal width. # # Returns the current horizontal scaling value (see Content::GraphicsState#horizontal_scaling) # when no argument is given. Otherwise sets the horizontal scaling using the +percent+ # argument and returns self. The setter version can also be called in the horizontal_scaling= # form. # # If the +percent+ and a block are provided, the changed horizontal scaling is only active # during the block by saving and restoring the graphics state. # # Examples: # # canvas.horizontal_scaling(50) # each glyph has only 50% width # canvas.horizontal_scaling # => 50 # canvas.horizontal_scaling = 125 # => 125 # # canvas.horizontal_scaling(75) do # canvas.horizontal_scaling # => 75 # end # canvas.horizontal_scaling # => 125 # # See: PDF1.7 s9.3.4 def horizontal_scaling(amount = nil, &bk) gs_getter_setter(:horizontal_scaling, :Tz, amount, &bk) end alias :horizontal_scaling= :horizontal_scaling # :call-seq: # canvas.leading => current_leading # canvas.leading(amount) => canvas # canvas.leading(amount) { block } => canvas # # The leading specifies the vertical distance between the baselines of adjacent text lines. # # Returns the current leading value (see Content::GraphicsState#leading) when no argument is # given. Otherwise sets the leading using the +amount+ argument and returns self. The setter # version can also be called in the leading= form. # # If the +amount+ and a block are provided, the changed leading is only active during the # block by saving and restoring the graphics state. # # Examples: # # canvas.leading(14.5) # canvas.leading # => 14.5 # canvas.leading = 10 # => 10 # # canvas.leading(25) do # canvas.leading # => 25 # end # canvas.leading # => 10 # # See: PDF1.7 s9.3.5 def leading(amount = nil, &bk) gs_getter_setter(:leading, :TL, amount, &bk) end alias :leading= :leading # :call-seq: # canvas.text_rendering_mode => current_text_rendering_mode # canvas.text_rendering_mode(mode) => canvas # canvas.text_rendering_mode(mode) { block } => canvas # # The text rendering mode determines if and how glyphs are rendered. The +mode+ parameter # can either be a valid integer or one of the symbols +:fill+, +:stroke+, +:fill_stroke+, # +:invisible+, +:fill_clip+, +:stroke_clip+, +:fill_stroke_clip+ or +:clip+ (see # TextRenderingMode.normalize for details). Note that the return value is always a # normalized text rendering mode value. # # Returns the current text rendering mode value (see # Content::GraphicsState#text_rendering_mode) when no argument is given. Otherwise sets the # text rendering mode using the +mode+ argument and returns self. The setter version can also # be called in the text_rendering_mode= form. # # If the +mode+ and a block are provided, the changed text rendering mode is only active # during the block by saving and restoring the graphics state. # # Examples: # # canvas.text_rendering_mode(:fill) # canvas.text_rendering_mode # => # # canvas.text_rendering_mode = :stroke # => # # # canvas.text_rendering_mode(3) do # canvas.text_rendering_mode # => # # end # canvas.text_rendering_mode # => # # # See: PDF1.7 s9.3.6 def text_rendering_mode(m = nil, &bk) gs_getter_setter(:text_rendering_mode, :Tr, m && TextRenderingMode.normalize(m), &bk) end alias :text_rendering_mode= :text_rendering_mode # :call-seq: # canvas.text_rise => current_text_rise # canvas.text_rise(amount) => canvas # canvas.text_rise(amount) { block } => canvas # # The text rise specifies the vertical distance to move the baseline up or down from its # default location. Positive values move the baseline up, negative values down. # # Returns the current text rise value (see Content::GraphicsState#text_rise) when no argument # is given. Otherwise sets the text rise using the +amount+ argument and returns self. The # setter version can also be called in the text_rise= form. # # If the +amount+ and a block are provided, the changed text rise is only active during the # block by saving and restoring the graphics state. # # Examples: # # canvas.text_rise(5) # canvas.text_rise # => 5 # canvas.text_rise = 10 # => 10 # # canvas.text_rise(15) do # canvas.text_rise # => 15 # end # canvas.text_rise # => 10 # # See: PDF1.7 s9.3.7 def text_rise(amount = nil, &bk) gs_getter_setter(:text_rise, :Ts, amount, &bk) end alias :text_rise= :text_rise # :call-seq: # canvas.begin_text(force_new: false) -> canvas # # Begins a new text object. # # If +force+ is +true+ and the current graphics object is already a text object, it is ended # and a new text object is begun. # # See: PDF1.7 s9.4.1 def begin_text(force_new: false) raise_unless_at_page_description_level_or_in_text end_text if force_new invoke0(:BT) if graphics_object == :none self end # :call-seq: # canvas.end_text -> canvas # # Ends the current text object. # # See: PDF1.7 s9.4.1 def end_text raise_unless_at_page_description_level_or_in_text invoke0(:ET) if graphics_object == :text self end # :call-seq: # canvas.text_matrix(a, b, c, d, e, f) => canvas # # Sets the text matrix (and the text line matrix) to the given matrix and returns self. # # The given values are interpreted as a matrix in the following way: # # a b 0 # c d 0 # e f 1 # # Examples: # # canvas.begin_text # canvas.text_matrix(1, 0, 0, 1, 100, 100) # # See: PDF1.7 s9.4.2 def text_matrix(a, b, c, d, e, f) begin_text invoke(:Tm, a, b, c, d, e, f) self end # :call-seq: # canvas.move_text_cursor(offset: nil, absolute: true) -> canvas # # Moves the text cursor by modifying the text and text line matrices. # # If +offset+ is not specified, the text cursor is moved to the start of the next text line # using #leading as vertical offset. # # Otherwise, the arguments +offset+, which has to be an array of the form [x, y], and # +absolute+ work together: # # * If +absolute+ is +true+, then the text and text line matrices are set to [1, 0, 0, 1, x, # y], placing the origin of text space, and therefore the text cursor, at [x, y]. # # Note that +absolute+ has to be understood in terms of the text matrix since for the actual # rendering the current transformation matrix is multiplied with the text matrix. # # * If +absolute+ is +false+, then the text cursor is moved to the start of the next line, # offset from the start of the current line (the origin of the text line matrix) by # +offset+. # # See: #show_glyphs def move_text_cursor(offset: nil, absolute: true) begin_text if offset if absolute text_matrix(1, 0, 0, 1, offset[0], offset[1]) else invoke2(:Td, offset[0], offset[1]) end else invoke0(:"T*") end self end # :call-seq: # canvas.text_cursor -> [x, y] # # Returns the position of the text cursor, i.e. the origin of the current text matrix. # # Note that this method can only be called while the current graphic object is a text object # since the text matrix is otherwise undefined. def text_cursor raise_unless_in_text graphics_state.tm.evaluate(0, 0) end # :call-seq: # canvas.font => current_font # canvas.font(name, size: nil, **options) => canvas # # Specifies the font that should be used when showing text. # # A valid font size need to be provided on the first invocation, otherwise an error is raised. # # *Note* that this method returns the font object itself, not the PDF dictionary representing # the font! # # If +size+ is specified, the #font_size method is invoked with it as argument. All other # options are passed on to the font loaders (see HexaPDF::FontLoader) that are used for # loading the specified font. # # Returns the current font object when no argument is given. # # Examples: # # canvas.font("Times", variant: :bold, size: 12) # canvas.font # => font object # canvas.font = "Times" # # See: PDF1.7 s9.2.2 def font(name = nil, size: nil, **options) if name @font = context.document.fonts.load(name, options) if size font_size(size) else size = font_size raise HexaPDF::Error, "No valid font size set" if size <= 0 invoke_font_operator(@font.dict, size) end self else @font end end alias :font= :font # :call-seq: # canvas.font_size => font_size # canvas.font_size(size, leading: size * 1.2) => canvas # # Specifies the font size. # # Note that an error is raised if no font has been set before! # # The leading can be additionally set and defaults to the font size times 1.2. If the leading # should not be changed, +nil+ has to be passed for +leading+. # # Returns the current font size when no argument is given. # # Examples: # # canvas.font_size(12) # canvas.font_size # => 12 # canvas.font_size(12, leading: 20) # canvas.font_size = 12 # # See: PDF1.7 s9.2.2 def font_size(size = nil, leading: size && size * 1.2) if size unless @font raise HexaPDF::Error, "A font needs to be set before the font size can be set" end invoke_font_operator(@font.dict, size) self.leading(leading) if leading self else graphics_state.font_size end end alias :font_size= :font_size # :call-seq: # canvas.text(text) -> canvas # canvas.text(text, at: [x, y]) -> canvas # # Shows the given text string. # # If no position is provided, the text is positioned at the current position of the text # cursor (the origin in case of a new text object or otherwise after the last shown text). # # The text string may contain any valid Unicode newline separator and if so, multiple lines # are shown, using #leading for offsetting the lines. # # Note that there are no provisions to make sure that all text is visible! So if the text # string is too long, it will just flow off the page and be cut off. # # Examples: # # canvas.font('Times', size: 12) # canvas.text("This is a \n multiline text", at: [100, 100]) # # See: http://www.unicode.org/reports/tr18/#Line_Boundaries def text(text, at: nil) move_text_cursor(offset: at) if at lines = text.split(/\u{D A}|(?!\u{D A})[\u{A}-\u{D}\u{85}\u{2028}\u{2029}]/, -1) lines.each_with_index do |str, index| show_glyphs(@font.decode_utf8(str)) move_text_cursor unless index == lines.length - 1 end self end # :call-seq: # canvas.show_glyphs(glyphs) -> canvas # # Low-level method for actually showing text on the canvas. # # The argument +data+ needs to be a an array of glyph objects valid for the current font, # optionally interspersed with numbers for kerning. # # Text is always shown at the current position of the text cursor, i.e. the origin of the text # matrix. To move the text cursor to somewhere else use #move_text_cursor before calling this # method. # # The text matrix is updated to correctly represent the graphics state after the invocation. # # This method is usually not invoked directly but by higher level methods like #show_text. def show_glyphs(data) begin_text result = [''.b] offset = 0 data.each do |item| if item.kind_of?(Numeric) result << item << ''.b offset -= item * graphics_state.scaled_font_size else encoded = @font.encode(item) result[-1] << encoded offset += item.width * graphics_state.scaled_font_size + graphics_state.scaled_character_spacing offset += graphics_state.scaled_word_spacing if encoded.length == 1 && item.space? end end invoke1(:TJ, result) graphics_state.tm.translate(offset, 0) self end private # Invokes the given operator with the operands and serializes it. def invoke(operator, *operands) @operators[operator].invoke(self, *operands) serialize(operator, *operands) end # Serializes the operator with the operands to the content stream. def serialize(operator, *operands) @contents << @operators[operator].serialize(@serializer, *operands) end # Optimized method for zero operands. def invoke0(operator) @operators[operator].invoke(self) @contents << @operators[operator].serialize(@serializer) end # Optimized method for one operand. def invoke1(operator, op1) @operators[operator].invoke(self, op1) @contents << @operators[operator].serialize(@serializer, op1) end # Optimized method for two operands. def invoke2(operator, op1, op2) @operators[operator].invoke(self, op1, op2) @contents << @operators[operator].serialize(@serializer, op1, op2) end # Invokes the font operator using the given PDF font dictionary. def invoke_font_operator(font, font_size) if graphics_state.font != font || graphics_state.font_size != font_size invoke(:Tf, resources.add_font(font), font_size) end end # Raises an error unless the current graphics object is a path. def raise_unless_in_path if graphics_object != :path raise HexaPDF::Error, "Operation only allowed when current graphics object is a path" end end # Raises an error unless the current graphics object is a path or a clipping path. def raise_unless_in_path_or_clipping_path if graphics_object != :path && graphics_object != :clipping_path raise HexaPDF::Error, "Operation only allowed when current graphics object is a " \ "path or clipping path" end end # Raises an error unless the current graphics object is none, i.e. the page description # level. def raise_unless_at_page_description_level end_text if graphics_object == :text if graphics_object != :none raise HexaPDF::Error, "Operation only allowed when there is no current graphics object" end end # Raises an error unless the current graphics object is none or a text object. def raise_unless_at_page_description_level_or_in_text if graphics_object != :none && graphics_object != :text raise HexaPDF::Error, "Operation only allowed when current graphics object is a " \ "text object or if there is no current object" end end # Raises an error unless the current graphics object is none or a path object. def raise_unless_at_page_description_level_or_in_path end_text if graphics_object == :text if graphics_object != :none && graphics_object != :path raise HexaPDF::Error, "Operation only allowed when current graphics object is a " \ "path object or if there is no current object" end end # Raises an error unless the current graphics object is a text object. def raise_unless_in_text if graphics_object != :text raise HexaPDF::Error, "Operation only allowed when current graphics object is a " \ "text object" end end # Utility method that abstracts the implementation of the stroke and fill color methods. def color_getter_setter(name, color, rg, g, k, cs, scn) color.flatten! if color.length > 0 raise_unless_at_page_description_level_or_in_text color = color_from_specification(color) save_graphics_state if block_given? if color != graphics_state.send(name) case color.color_space.family when :DeviceRGB then serialize(rg, *color.components) when :DeviceGray then serialize(g, *color.components) when :DeviceCMYK then serialize(k, *color.components) else if color.color_space != graphics_state.send(name).color_space serialize(cs, resources.add_color_space(color.color_space)) end serialize(scn, *color.components) end graphics_state.send(:"#{name}=", color) end if block_given? yield restore_graphics_state end self elsif block_given? raise ArgumentError, "Block only allowed with arguments" else graphics_state.send(name) end end # Creates a color object from the given color specification. See #stroke_color for details # on the possible color specifications. def color_from_specification(spec) if spec.length == 1 && spec[0].kind_of?(String) resources.color_space(:DeviceRGB).color(*spec[0].scan(/../).map!(&:hex)) elsif spec.length == 1 && spec[0].respond_to?(:color_space) spec[0] else resources.color_space(color_space_for_components(spec)).color(*spec) end end # Returns the name of the device color space that should be used for creating a color object # from the components array. def color_space_for_components(components) case components.length when 1 then :DeviceGray when 3 then :DeviceRGB when 4 then :DeviceCMYK else raise ArgumentError, "Invalid number of color components, 1|3|4 expected, " \ "#{components.length} given" end end # Utility method that abstracts the implementation of a graphics state parameter # getter/setter method with a call sequence of: # # canvas.method # => cur_value # canvas.method(new_value) # => canvas # canvas.method(new_value) { block } # => canvas # # +name+:: # The name (Symbol) of the graphics state parameter for fetching the value from the # GraphicState. # # +op+:: # The operator (Symbol) which should be invoked if the value is different from the current # value of the graphics state parameter. # # +value+:: # The new value of the graphics state parameter, or +nil+ if the getter functionality is # needed. def gs_getter_setter(name, op, value) if !value.nil? raise_unless_at_page_description_level_or_in_text save_graphics_state if block_given? if graphics_state.send(name) != value value.respond_to?(:to_operands) ? invoke(op, *value.to_operands) : invoke1(op, value) end if block_given? yield restore_graphics_state end self elsif block_given? raise ArgumentError, "Block only allowed with an argument" else graphics_state.send(name) end end # Modifies and checks the array +points+ so that polylines and polygons work correctly. def check_poly_points(points) if points.length < 4 raise ArgumentError, "At least two points needed to make one line segment" elsif points.length.odd? raise ArgumentError, "Missing y-coordinate for last point" end end # Used for calculating the optimal distance of the control points. # # See: http://itc.ktu.lt/itc354/Riskus354.pdf, p373 right column KAPPA = 0.55191496 #:nodoc: # Appends a line with a rounded corner from the current point. The corner is specified by # the three points (x0, y0), (x1, y1) and (x2, y2) where (x1, y1) is the corner point. def line_with_rounded_corner(x0, y0, x1, y1, x2, y2, radius) p0 = point_on_line(x1, y1, x0, y0, distance: radius) p3 = point_on_line(x1, y1, x2, y2, distance: radius) p1 = point_on_line(p0[0], p0[1], x1, y1, distance: KAPPA * radius) p2 = point_on_line(p3[0], p3[1], x1, y1, distance: KAPPA * radius) line_to(p0[0], p0[1]) curve_to(p3[0], p3[1], p1: p1, p2: p2) end # Given two points p0 = (x0, y0) and p1 = (x1, y1), returns the point on the line through # these points that is +distance+ units away from p0. # # v = p1 - p0 # result = p0 + distance * v/norm(v) def point_on_line(x0, y0, x1, y1, distance:) norm = Math.sqrt((x1 - x0)**2 + (y1 - y0)**2) [x0 + distance / norm * (x1 - x0), y0 + distance / norm * (y1 - y0)] end # Calculates and returns the requested dimensions for the rectangular object with the given # +width+ and +height+ based on the options. # # +rwidth+:: # The requested width. If +rheight+ is not specified, it is chosen so that the aspect # ratio is maintained # # +rheight+:: # The requested height. If +rwidth+ is not specified, it is chosen so that the aspect # ratio is maintained def calculate_dimensions(width, height, rwidth: nil, rheight: nil) if rwidth && rheight [rwidth, rheight] elsif rwidth [rwidth, height * rwidth / width.to_f] elsif rheight [width * rheight / height.to_f, rheight] else [width, height] end end end end end