#-- # PDF::Writer for Ruby. # http://rubyforge.org/projects/ruby-pdf/ # Copyright 2003 - 2005 Austin Ziegler. # # Licensed under a MIT-style licence. See LICENCE in the main distribution # for full licensing information. # # $Id: writer.rb,v 1.45 2005/10/12 14:41:40 austin Exp $ #++ require 'thread' require 'open-uri' require 'transaction/simple' require 'color' # A class to provide the core functionality to create a PDF document # without any requirement for additional modules. module PDF class Writer # The version of PDF::Writer. VERSION = '1.1.4' # Escape the text so that it's safe for insertion into the PDF # document. def self.escape(text) text.gsub(/\\/, '\\\\\\\\'). gsub(/\(/, '\\('). gsub(/\)/, '\\)'). gsub(/</, '<'). gsub(/>/, '>'). gsub(/&/, '&') end end end require 'pdf/math' require 'pdf/writer/lang' require 'pdf/writer/lang/en' begin require 'zlib' PDF::Writer::Compression = true rescue LoadError warn PDF::Writer::Lang[:no_zlib_no_compress] PDF::Writer::Compression = false end require 'pdf/writer/arc4' require 'pdf/writer/fontmetrics' require 'pdf/writer/object' require 'pdf/writer/object/action' require 'pdf/writer/object/annotation' require 'pdf/writer/object/catalog' require 'pdf/writer/object/contents' require 'pdf/writer/object/destination' require 'pdf/writer/object/encryption' require 'pdf/writer/object/font' require 'pdf/writer/object/fontdescriptor' require 'pdf/writer/object/fontencoding' require 'pdf/writer/object/image' require 'pdf/writer/object/info' require 'pdf/writer/object/outlines' require 'pdf/writer/object/outline' require 'pdf/writer/object/page' require 'pdf/writer/object/pages' require 'pdf/writer/object/procset' require 'pdf/writer/object/viewerpreferences' require 'pdf/writer/ohash' require 'pdf/writer/strokestyle' require 'pdf/writer/graphics' require 'pdf/writer/graphics/imageinfo' require 'pdf/writer/state' class PDF::Writer # The system font path. The sytem font path will be determined # differently for each operating system. # # Win32:: Uses ENV['SystemRoot']/Fonts as the system font path. There is # an extension that will handle this better, but until and # unless it is distributed with the standard Ruby Windows # installer, PDF::Writer will not depend upon it. # OS X:: The fonts are found in /System/Library/Fonts. # Linux:: The font path list will be found (usually) in # /etc/fonts/fonts.conf or /usr/etc/fonts/fonts.conf. This XML # file will be parsed (using REXML) to provide the value for # FONT_PATH. FONT_PATH = [] class << self require 'rexml/document' # Parse the fonts.conf XML file. def parse_fonts_conf(filename) doc = REXML::Document.new(File.open(filename, "rb")).root rescue nil if doc path = REXML::XPath.match(doc, '//dir').map do |el| el.text.gsub($/, '') end doc = nil else path = [] end path end private :parse_fonts_conf end case RUBY_PLATFORM when /mswin32/o # Windows font path. This is not the most reliable method. FONT_PATH << File.join(ENV['SystemRoot'], 'Fonts') when /darwin/o # Macintosh font path. FONT_PATH << '/System/Library/Fonts' else FONT_PATH.push(*parse_fonts_conf('/etc/fonts/fonts.conf')) FONT_PATH.push(*parse_fonts_conf('//usr/etc/fonts/fonts.conf')) end FONT_PATH.uniq! include PDF::Writer::Graphics # Contains all of the PDF objects, ready for final assembly. This is of # no interest to external consumers. attr_reader :objects #:nodoc: # The ARC4 encryption object. This is of no interest to external # consumers. attr_reader :arc4 #:nodoc: # The string that will be used to encrypt this PDF document. attr_accessor :encryption_key # The number of PDF objects in the document def size @objects.size end # Generate an ID for a new PDF object. def generate_id @mutex.synchronize { @current_id += 1 } end private :generate_id # Generate a new font ID. def generate_font_id @mutex.synchronize { @current_font_id += 1 } end private :generate_font_id class << self # Create the document with prepress options. Uses the same options as # PDF::Writer.new (:paper, :orientation, and # :version). It also supports the following options: # # :left_margin:: The left margin. # :right_margin:: The right margin. # :top_margin:: The top margin. # :bottom_margin:: The bottom margin. # :bleed_size:: The size of the bleed area in points. # Default 12. # :mark_length:: The length of the prepress marks in # points. Default 18. # # The prepress marks are added to the loose objects and will appear on # all pages. def prepress(options = { }) pdf = self.new(options) bleed_size = options[:bleed_size] || 12 mark_length = options[:mark_length] || 18 pdf.left_margin = options[:left_margin] if options[:left_margin] pdf.right_margin = options[:right_margin] if options[:right_margin] pdf.top_margin = options[:top_margin] if options[:top_margin] pdf.bottom_margin = options[:bottom_margin] if options[:bottom_margin] # This is in an "odd" order because the y-coordinate system in PDF # is from bottom to top. tx0 = pdf.pages.media_box[0] + pdf.left_margin ty0 = pdf.pages.media_box[3] - pdf.top_margin tx1 = pdf.pages.media_box[2] - pdf.right_margin ty1 = pdf.pages.media_box[1] + pdf.bottom_margin bx0 = tx0 - bleed_size by0 = ty0 - bleed_size bx1 = tx1 + bleed_size by1 = ty1 + bleed_size pdf.pages.trim_box = [ tx0, ty0, tx1, ty1 ] pdf.pages.bleed_box = [ bx0, by0, bx1, by1 ] all = pdf.open_object pdf.save_state kk = Color::CMYK.new(0, 0, 0, 100) pdf.stroke_color! kk pdf.fill_color! kk pdf.stroke_style! StrokeStyle.new(0.3) pdf.prepress_clip_mark(tx1, ty0, 0, mark_length, bleed_size) # Upper Right pdf.prepress_clip_mark(tx0, ty0, 90, mark_length, bleed_size) # Upper Left pdf.prepress_clip_mark(tx0, ty1, 180, mark_length, bleed_size) # Lower Left pdf.prepress_clip_mark(tx1, ty1, -90, mark_length, bleed_size) # Lower Right mid_x = pdf.pages.media_box[2] / 2.0 mid_y = pdf.pages.media_box[3] / 2.0 pdf.prepress_center_mark(mid_x, ty0, 0, mark_length, bleed_size) # Centre Top pdf.prepress_center_mark(tx0, mid_y, 90, mark_length, bleed_size) # Centre Left pdf.prepress_center_mark(mid_x, ty1, 180, mark_length, bleed_size) # Centre Bottom pdf.prepress_center_mark(tx1, mid_y, -90, mark_length, bleed_size) # Centre Right pdf.restore_state pdf.close_object pdf.add_object(all, :all) yield pdf if block_given? pdf end # Convert a measurement in centimetres to points, which are the # default PDF userspace units. def cm2pts(x) (x / 2.54) * 72 end # Convert a measurement in millimetres to points, which are the # default PDF userspace units. def mm2pts(x) (x / 25.4) * 72 end # Convert a measurement in inches to points, which are the default PDF # userspace units. def in2pts(x) x * 72 end end # Convert a measurement in centimetres to points, which are the default # PDF userspace units. def cm2pts(x) PDF::Writer.cm2pts(x) end # Convert a measurement in millimetres to points, which are the default # PDF userspace units. def mm2pts(x) PDF::Writer.mm2pts(x) end # Convert a measurement in inches to points, which are the default PDF # userspace units. def in2pts(x) PDF::Writer.in2pts(x) end # Standard page size names. One of these may be provided to # PDF::Writer.new as the :paper parameter. # # Page sizes supported are: # # * 4A0, 2A0 # * A0, A1 A2, A3, A4, A5, A6, A7, A8, A9, A10 # * B0, B1, B2, B3, B4, B5, B6, B7, B8, B9, B10 # * C0, C1, C2, C3, C4, C5, C6, C7, C8, C9, C10 # * RA0, RA1, RA2, RA3, RA4 # * SRA0, SRA1, SRA2, SRA3, SRA4 # * LETTER # * LEGAL # * FOLIO # * EXECUTIVE PAGE_SIZES = { # :value {...}: "4A0" => [0, 0, 4767.87, 6740.79], "2A0" => [0, 0, 3370.39, 4767.87], "A0" => [0, 0, 2383.94, 3370.39], "A1" => [0, 0, 1683.78, 2383.94], "A2" => [0, 0, 1190.55, 1683.78], "A3" => [0, 0, 841.89, 1190.55], "A4" => [0, 0, 595.28, 841.89], "A5" => [0, 0, 419.53, 595.28], "A6" => [0, 0, 297.64, 419.53], "A7" => [0, 0, 209.76, 297.64], "A8" => [0, 0, 147.40, 209.76], "A9" => [0, 0, 104.88, 147.40], "A10" => [0, 0, 73.70, 104.88], "B0" => [0, 0, 2834.65, 4008.19], "B1" => [0, 0, 2004.09, 2834.65], "B2" => [0, 0, 1417.32, 2004.09], "B3" => [0, 0, 1000.63, 1417.32], "B4" => [0, 0, 708.66, 1000.63], "B5" => [0, 0, 498.90, 708.66], "B6" => [0, 0, 354.33, 498.90], "B7" => [0, 0, 249.45, 354.33], "B8" => [0, 0, 175.75, 249.45], "B9" => [0, 0, 124.72, 175.75], "B10" => [0, 0, 87.87, 124.72], "C0" => [0, 0, 2599.37, 3676.54], "C1" => [0, 0, 1836.85, 2599.37], "C2" => [0, 0, 1298.27, 1836.85], "C3" => [0, 0, 918.43, 1298.27], "C4" => [0, 0, 649.13, 918.43], "C5" => [0, 0, 459.21, 649.13], "C6" => [0, 0, 323.15, 459.21], "C7" => [0, 0, 229.61, 323.15], "C8" => [0, 0, 161.57, 229.61], "C9" => [0, 0, 113.39, 161.57], "C10" => [0, 0, 79.37, 113.39], "RA0" => [0, 0, 2437.80, 3458.27], "RA1" => [0, 0, 1729.13, 2437.80], "RA2" => [0, 0, 1218.90, 1729.13], "RA3" => [0, 0, 864.57, 1218.90], "RA4" => [0, 0, 609.45, 864.57], "SRA0" => [0, 0, 2551.18, 3628.35], "SRA1" => [0, 0, 1814.17, 2551.18], "SRA2" => [0, 0, 1275.59, 1814.17], "SRA3" => [0, 0, 907.09, 1275.59], "SRA4" => [0, 0, 637.80, 907.09], "LETTER" => [0, 0, 612.00, 792.00], "LEGAL" => [0, 0, 612.00, 1008.00], "FOLIO" => [0, 0, 612.00, 936.00], "EXECUTIVE" => [0, 0, 521.86, 756.00] } # Creates a new PDF document as a writing canvas. It accepts three named # parameters: # # :paper:: Specifies the size of the default page in # PDF::Writer. This may be a four-element array # of coordinates specifying the lower-left # (xll, yll) and upper-right (xur, # yur) corners, a two-element array of # width and height in centimetres, or a page # name as defined in PAGE_SIZES. # :orientation:: The orientation of the page, either long # (:portrait) or wide (:landscape). This may be # used to swap the width and the height of the # page. # :version:: The feature set available to the document is # limited by the PDF version. Setting this # version restricts the feature set available to # PDF::Writer. PDF::Writer currently supports # PDF version 1.3 features and does not yet # support advanced features from PDF 1.4, 1.5, # or 1.6. def initialize(options = {}) paper = options[:paper] || "LETTER" orientation = options[:orientation] || :portrait version = options[:version] || PDF_VERSION_13 @mutex = Mutex.new @current_id = @current_font_id = 0 # Start the document @objects = [] @callbacks = [] @font_families = {} @fonts = {} @stack = [] @state_stack = StateStack.new @loose_objects = [] @current_text_state = "" @options = {} @destinations = {} @add_loose_objects = {} @images = [] @word_space_adjust = nil @current_stroke_style = PDF::Writer::StrokeStyle.new(1) @page_numbering = nil @arc4 = nil @encryption = nil @file_identifier = nil @active_objects = [] @columns = {} @columns_on = false @insert_mode = nil @catalog = PDF::Writer::Object::Catalog.new(self) @outlines = PDF::Writer::Object::Outlines.new(self) @pages = PDF::Writer::Object::Pages.new(self) @current_node = @pages @procset = PDF::Writer::Object::Procset.new(self) @info = PDF::Writer::Object::Info.new(self) @page = PDF::Writer::Object::Page.new(self) @current_text_render_style = 0 @first_page = @page @version = version # Initialize the default font families. init_font_families # Items formerly in EZWriter @font_size = 10 @pageset = [] if paper.kind_of?(Array) if paper.size == 4 size = paper # Coordinate Array else size = [0, 0, PDF::Writer.cm2pts(paper[0]), PDF::Writer.cm2pts(paper[1])] # Paper size in centimeters has been passed end else size = PAGE_SIZES[paper.upcase].dup end size[3], size[2] = size[2], size[3] if orientation == :landscape @pages.media_box = size @page_width = size[2] - size[0] @page_height = size[3] - size[1] @y = @page_height # Also set the margins to some reasonable defaults -- 1.27 cm, 36pt, # or 0.5 inches. margins_pt(36) # Set the current writing position to the top of the first page @y = absolute_top_margin # Get the ID of the page that was created during the instantiation # process. @pageset[1] = @pages.first_page fill_color! Color::RGB::Black stroke_color! Color::RGB::Black yield self if block_given? end PDF_VERSION_13 = '1.3' PDF_VERSION_14 = '1.4' PDF_VERSION_15 = '1.5' PDF_VERSION_16 = '1.6' # The version of PDF to which this document conforms. Should be one of # PDF_VERSION_13, PDF_VERSION_14, PDF_VERSION_15, or PDF_VERSION_16. attr_reader :version # The document catalog object (PDF::Writer::Object::Catalog). The # options in the catalog should be set with PDF::Writer#open_here, # PDF::Writer#viewer_preferences, and PDF::Writer#page_mode. # # This is of little interest to external clients. attr_accessor :catalog #:nodoc: # The PDF::Writer::Object::Pages object. This is of little interest to # external clients. attr_accessor :pages #:nodoc: # The PDF::Writer::Object::Procset object. This is of little interest to # external clients. attr_accessor :procset #:nodoc: # Sets the document to compressed (+true+) or uncompressed (+false+). # Defaults to uncompressed. This can ONLY be set once and should be set # as early as possible in the document creation process. attr_accessor :compressed def compressed=(cc) #:nodoc: @compressed = cc if @compressed.nil? end # Returns +true+ if the document is compressed. def compressed? @compressed == true end # The set of known labelled destinations. All destinations are of class # PDF::Writer::Object::Destination. This is of little interest to # external clients. attr_reader :destinations #:nodoc: # The PDF::Writer::Object::Info info object. This is used to provide # certain metadata. attr_reader :info # The current page for writing. This is of little interest to external # clients. attr_accessor :current_page #:nodoc: # Returns the current contents object to which raw PDF instructions may # be written. attr_reader :current_contents # The PDF::Writer::Object::Outlines object. This is currently used very # little. This is of little interest to external clients. attr_reader :outlines #:nodoc: # The complete set of page objects. This is of little interest to # external consumers. attr_reader :pageset #:nodoc: attr_accessor :left_margin attr_accessor :right_margin attr_accessor :top_margin attr_accessor :bottom_margin attr_reader :page_width attr_reader :page_height # The absolute x position of the left margin. attr_reader :absolute_left_margin def absolute_left_margin #:nodoc: @left_margin end # The absolute x position of the right margin. attr_reader :absolute_right_margin def absolute_right_margin #:nodoc: @page_width - @right_margin end # Returns the absolute y position of the top margin. attr_reader :absolute_top_margin def absolute_top_margin #:nodoc: @page_height - @top_margin end # Returns the absolute y position of the bottom margin. attr_reader :absolute_bottom_margin def absolute_bottom_margin #:nodoc: @bottom_margin end # The height of the margin area. attr_reader :margin_height def margin_height #:nodoc: absolute_top_margin - absolute_bottom_margin end # The width of the margin area. attr_reader :margin_width def margin_width #:nodoc: absolute_right_margin - absolute_left_margin end # The absolute x middle position. attr_reader :absolute_x_middle def absolute_x_middle #:nodoc: @page_width / 2.0 end # The absolute y middle position. attr_reader :absolute_y_middle def absolute_y_middle #:nodoc: @page_height / 2.0 end # The middle of the writing area between the left and right margins. attr_reader :margin_x_middle def margin_x_middle #:nodoc: (absolute_right_margin + absolute_left_margin) / 2.0 end # The middle of the writing area between the top and bottom margins. attr_reader :margin_y_middle def margin_y_middle #:nodoc: (absolute_top_margin + absolute_bottom_margin) / 2.0 end # The vertical position of the writing point. The vertical position is # constrained between the top and bottom margins. Any attempt to set it # outside of those margins will cause the y pointer to be placed # absolutely at the margins. attr_accessor :y def y=(yy) #:nodoc: @y = yy @y = absolute_top_margin if @y > absolute_top_margin @y = @bottom_margin if @y < @bottom_margin end # The vertical position of the writing point. If the vertical position # is outside of the bottom margin, a new page will be created. attr_accessor :pointer def pointer=(y) #:nodoc: @y = y start_new_page if @y < @bottom_margin end # Used to change the vertical position of the writing point. The pointer # is moved *down* the page by +dy+ (that is, #y is reduced by +dy+), so # if the pointer is to be moved up, a negative number must be used. # Moving up the page will not move to the previous page because of # limitations in the way that PDF::Writer works. The writing point will # be limited to the top margin position. # # If +make_space+ is true and a new page is forced, then the pointer # will be moved down on the new page. This will allow space to be # reserved for graphics. def move_pointer(dy, make_space = false) @y -= dy if @y < @bottom_margin start_new_page @y -= dy if make_space elsif @y > absolute_top_margin @y = absolute_top_margin end end # Define the margins in millimetres. def margins_mm(top, left = top, bottom = top, right = left) margins_pt(mm2pts(top), mm2pts(left), mm2pts(bottom), mm2pts(right)) end # Define the margins in centimetres. def margins_cm(top, left = top, bottom = top, right = left) margins_pt(cm2pts(top), cm2pts(left), cm2pts(bottom), cm2pts(right)) end # Define the margins in inches. def margins_in(top, left = top, bottom = top, right = left) margins_pt(in2pts(top), in2pts(left), in2pts(bottom), in2pts(right)) end # Define the margins in points. This will move the #y pointer # # # T L B R # pdf.margins_pt(36) # 36 36 36 36 # pdf.margins_pt(36, 54) # 36 54 36 54 # pdf.margins_pt(36, 54, 72) # 36 54 72 54 # pdf.margins_pt(36, 54, 72, 90) # 36 54 72 90 def margins_pt(top, left = top, bottom = top, right = left) # Set the margins to new values @top_margin = top @bottom_margin = bottom @left_margin = left @right_margin = right # Check to see if this means that the current writing position is # outside the writable area if @y > (@page_height - top) # Move y down @y = @page_height - top end start_new_page if @y < bottom # Make a new page end # Allows the user to find out what the ID is of the first page that was # created during startup - useful if they wish to add something to it # later. attr_reader :first_page # Add a new translation table for a font family. A font family will be # used to associate a single name and font styles with multiple fonts. # A style will be identified with a single-character style identifier or # a series of style identifiers. The only styles currently recognised # are: # # +b+:: Bold (or heavy) fonts. Examples: Helvetica-Bold, Courier-Bold, # Times-Bold. # +i+:: Italic (or oblique) fonts. Examples: Helvetica-Oblique, # Courier-Oblique, Times-Italic. # +bi+:: Bold italic fonts. Examples Helvetica-BoldOblique, # Courier-BoldOblique, Times-BoldItalic. # +ib+:: Italic bold fonts. Generally defined the same as +bi+ font # styles. Examples: Helvetica-BoldOblique, Courier-BoldOblique, # Times-BoldItalic. # # Each font family key is the base name for the font. attr_reader :font_families # Initialize the font families for the default fonts. def init_font_families # Set the known family groups. These font families will be used to # enable bold and italic markers to be included within text # streams. HTML forms will be used... @font_families["Helvetica"] = { "b" => 'Helvetica-Bold', "i" => 'Helvetica-Oblique', "bi" => 'Helvetica-BoldOblique', "ib" => 'Helvetica-BoldOblique' } @font_families['Courier'] = { "b" => 'Courier-Bold', "i" => 'Courier-Oblique', "bi" => 'Courier-BoldOblique', "ib" => 'Courier-BoldOblique' } @font_families['Times-Roman'] = { "b" => 'Times-Bold', "i" => 'Times-Italic', "bi" => 'Times-BoldItalic', "ib" => 'Times-BoldItalic' } end private :init_font_families # Contains a list of "active objects". These items are like loose # objects, but will be rendered only when the PDF is rendered. Each # object will be rendered in order and is responsible for its own # placement. # # An active object must respond to #render(pdf, debug). It will operate # on the PDF document externally, like PDF::SimpleTable. attr_reader :active_objects # Sets the trim box area. def trim_box(x0, y0, x1, y1) @pages.trim_box = [ x0, y0, x1, y1 ] end # Sets the bleed box area. def bleed_box(x0, y0, x1, y1) @pages.bleed_box = [ x0, y0, x1, y1 ] end # set the viewer preferences of the document, it is up to the browser to # obey these. def viewer_preferences(label, value = 0) @catalog.viewer_preferences ||= PDF::Writer::Object::ViewerPreferences.new(self) # This will only work if the label is one of the valid ones. if label.kind_of?(Hash) label.each { |kk, vv| @catalog.viewer_preferences.__send__("#{kk.downcase}=".intern, vv) } else @catalog.viewer_preferences.__send__("#{label.downcase}=".intern, value) end end # Add a link in the document to an external URL. def add_link(uri, x0, y0, x1, y1) PDF::Writer::Object::Annotation.new(self, :link, [x0, y0, x1, y1], uri) end # Add a link in the document to an internal destination (ie. within the # document) def add_internal_link(label, x0, y0, x1, y1) PDF::Writer::Object::Annotation.new(self, :ilink, [x0, y0, x1, y1], label) end # Add an outline item (Bookmark). def add_outline_item(label, title = label) PDF::Writer::Object::Outline.new(self, label, title) end # Standard encryption/DRM options. ENCRYPT_OPTIONS = { #:nodoc: :print => 4, :modify => 8, :copy => 16, :add => 32 } # Encrypts the document. This will set the user and owner passwords that # will be used to access the document and set the permissions the user # has with the document. The passwords are limited to 32 characters. # # The permissions provided are an array of symbols, allowing identified # users to perform particular actions: # :print:: Print. # :modify:: Modify text or objects. # :copy:: Copy text or objects. # :add:: Add text or objects. def encrypt(user_pass = nil, owner_pass = nil, permissions = []) perms = ["11000000"].pack("B8") permissions.each do |perm| perms += ENCRYPT_OPTIONS[perm] if ENCRYPT_OPTIONS[perm] end @arc4 ||= PDF::ARC4.new owner_pass ||= user_pass options = { :owner_pass => owner_pass, :user_pass => user_pass, :permissions => perms, } @encryption = PDF::Writer::Object::Encryption.new(self, options) end def encrypted? not @encryption.nil? end # should be used for internal checks, not implemented as yet def check_all_here end # Return the PDF stream as a string. def render(debug = false) @active_objects.each { |ao| ao.render(self, debug) } add_page_numbers # This method has been deprecated. @compression = false if $DEBUG or debug @arc4.init(@encryption_key) unless @arc4.nil? check_all_here xref = [] content = "%PDF-#{@version}\n%âãÏÓ\n" pos = content.size objects.each do |oo| cont = oo.to_s content << cont xref << pos pos += cont.size end # pos += 1 # Newline character before XREF content << "\nxref\n0 #{xref.size + 1}\n0000000000 65535 f \n" xref.each { |xx| content << "#{'%010d' % [xx]} 00000 n \n" } content << "\ntrailer\n" content << " << /Size #{xref.size + 1}\n" content << " /Root 1 0 R\n /Info #{@info.oid} 0 R\n" # If encryption has been applied to this document, then add the marker # for this dictionary if @arc4 and @encryption content << "/Encrypt #{@encryption.oid} 0 R\n" end if @file_identifier content << "/ID[<#{@file_identifier}><#{@file_identifier}>]\n" end content << " >>\nstartxref\n#{pos}\n%%EOF\n" content end alias :to_s :render # Loads the font metrics. This is now thread-safe. def load_font_metrics(font) metrics = PDF::Writer::FontMetrics.open(font) @mutex.synchronize do @fonts[font] = metrics @fonts[font].font_num = @fonts.size end metrics end private :load_font_metrics def find_font(fontname) name = File.basename(fontname, ".afm") @objects.detect do |oo| oo.kind_of?(PDF::Writer::Object::Font) and /#{oo.basefont}$/ =~ name end end private :find_font def font_file(fontfile) path = "#{fontfile}.pfb" return path if File.exists?(path) path = "#{fontfile}.ttf" return path if File.exists?(path) nil end private :font_file def load_font(font, encoding = nil) metrics = load_font_metrics(font) name = File.basename(font).gsub(/\.afm$/o, "") encoding_diff = nil case encoding when Hash encoding_name = encoding[:encoding] encoding_diff = encoding[:differences] encoding = PDF::Writer::Object::FontEncoding.new(self, encoding_name, encoding_diff) when NilClass encoding_name = encoding = 'WinAnsiEncoding' else encoding_name = encoding end wfo = PDF::Writer::Object::Font.new(self, name, encoding) # We have an Adobe Font Metrics (.afm) file. We need to find the # associated Type1 (.pfb) or TrueType (.ttf) files (we do not yet # support OpenType fonts); we need to load it into a # PDF::Writer::Object and put the references into the metrics object. base = metrics.path.sub(/\.afm$/o, "") fontfile = font_file(base) unless fontfile base = File.basename(base) FONT_PATH.each do |path| fontfile = font_file(File.join(path, base)) break if fontfile end end if font =~ /afm/o and fontfile font_type = File.extname(fontfile) # Find the array of font widths, and put that into an object. first_char = -1 last_char = 0 widths = {} metrics.c.each_value do |details| num = details["C"] if num >= 0 warn "Multiple definitions of #{num}" if widths.has_key?(num) widths[num] = details['WX'] first_char = num if num < first_char or first_char < 0 last_char = num if num > last_char end end # Adjust the widths for the differences array. if encoding_diff encoding_diff.each do |cnum, cname| (cnum - last_char).times { widths << 0 } if cnum > last_char last_char = cnum widths[cnum - firstchar] = fonts.c[cname]['WX'] if metrics.c[cname] end end widthid = PDF::Writer::Object::Contents.new(self, :raw) widthid << "[" (first_char .. last_char).each do |ii| if widths.has_key?(ii) widthid << " #{widths[ii].to_i}" else widthid << " 0" end end widthid << "]" # Load the pfb file, and put that into an object too. Note that PDF # supports only binary format Type1 font files and TrueType font # files. There is a simple utility to convert Type1 from pfa to pfb. data = File.open(fontfile, "rb") { |ff| ff.read } # Check to see if the font licence allows embedding. if font_type == ".ttf" offset = 4 tables = data[offset, 2].unpack('n')[0] offset += 8 found = false tables.times do if data[offset, 4] == 'OS/2' found = true break end offset += 4 + 12 end if found offset += 4 newoff = data[offset, 4].unpack('N')[0] offset = newoff + 8 licence = data[offset, 2].unpack('n')[0] rl = ((licence & 0x02) != 0) pp = ((licence & 0x04) != 0) ee = ((licence & 0x08) != 0) if rl and pp and ee warn PDF::Writer::Lang[:ttf_licence_no_embedding] % name end end end # Create the font descriptor. fdsc = PDF::Writer::Object::FontDescriptor.new(self) # Raw contents causes problems with Acrobat Reader. pfbc = PDF::Writer::Object::Contents.new(self) # Determine flags (more than a little flakey, hopefully will not # matter much). flags = 0 if encoding == "none" flags += 2 ** 2 else flags += 2 ** 6 if metrics.italicangle.nonzero? flags += 2 ** 0 if metrics.isfixedpitch == "true" flags += 2 ** 5 # Assume a non-symbolic font end # 1: FixedPitch: All glyphs have the same width (as opposed to # proportional or variable-pitch fonts, which have # different widths). # 2: Serif: Glyphs have serifs, which are short strokes drawn # at an angle on the top and bottom of glyph stems. # (Sans serif fonts do not have serifs.) # 3: Symbolic Font contains glyphs outside the Adobe standard # Latin character set. This flag and the Nonsymbolic # flag cannot both be set or both be clear (see # below). # 4: Script: Glyphs resemble cursive handwriting. # 6: Nonsymbolic: Font uses the Adobe standard Latin character set # or a subset of it (see below). # 7: Italic: Glyphs have dominant vertical strokes that are # slanted. # 17: AllCap: Font contains no lowercase letters; typically used # for display purposes, such as for titles or # headlines. # 18: SmallCap: Font contains both uppercase and lowercase # letters. The uppercase letters are similar to # those in the regular version of the same typeface # family. The glyphs for the lowercase letters have # the same shapes as the corresponding uppercase # letters, but they are sized and their proportions # adjusted so that they have the same size and # stroke weight as lowercase glyphs in the same # typeface family. # 19: ForceBold: See below. list = { 'Ascent' => 'Ascender', 'CapHeight' => 'CapHeight', 'Descent' => 'Descender', 'FontBBox' => 'FontBBox', 'ItalicAngle' => 'ItalicAngle' } fdopt = { 'Flags' => flags, 'FontName' => metrics.fontname, 'StemV' => 100 # Don't know what the value for this should be! } list.each do |kk, vv| zz = metrics.__send__(vv.downcase.intern) fdopt[kk] = zz if zz end # Determine the cruicial lengths within this file case font_type when ".pfb" fdopt['FontFile'] = pfbc.oid i1 = data.index('eexec') + 6 i2 = data.index('00000000') - i1 i3 = data.size - i2 - i1 pfbc.add('Length1' => i1, 'Length2' => i2, 'Length3' => i3) when ".ttf" fdopt['FontFile2'] = pfbc.oid pfbc.add('Length1' => data.size) end fdsc.options = fdopt # Embed the font program pfbc << data # Tell the font object about all this new stuff tmp = { 'BaseFont' => metrics.fontname, 'Widths' => widthid.oid, 'FirstChar' => first_char, 'LastChar' => last_char, 'FontDescriptor' => fdsc.oid } tmp['SubType'] = 'TrueType' if font_type == ".ttf" tmp.each { |kk, vv| wfo.__send__("#{kk.downcase}=".intern, vv) } end # Also set the differences here. Note that this means that these will # take effect only the first time that a font is selected, else they # are ignored. metrics.differences = encoding_diff unless encoding_diff.nil? metrics.encoding = encoding_name metrics end private :load_font # If the named +font+ is not loaded, then load it and make the required # PDF objects to represent the font. If the font is already loaded, then # make it the current font. # # The parameter +encoding+ applies only when the font is first being # loaded; it may not be applied later. It may either be an encoding name # or a hash. The Hash must contain two keys: # # :encoding:: The name of the encoding. Either *none*, # *WinAnsiEncoding*, *MacRomanEncoding*, or # *MacExpertEncoding*. For symbolic fonts, an # encoding of *none* is recommended with a # differences Hash. # :differences:: This Hash value is a mapping between character # byte values (0 .. 255) and character names # from the AFM file for the font. # # The standard PDF encodings are detailed fully in the PDF Reference # version 1.6, Appendix D. # # Note that WinAnsiEncoding is not the same as Windows code page 1252 # (roughly equivalent to latin-1), Most characters map, but not all. The # encoding value currently defaults to WinAnsiEncoding. # # If the font's "natural" encoding is desired, then it is necessary to # specify the +encoding+ parameter as { :encoding => nil }. def select_font(font, encoding = nil) load_font(font, encoding) unless @fonts[font] @current_base_font = font current_font! @current_base_font end # Selects the current font based on defined font families and the # current text state. As noted in #font_families, a "bi" font can be # defined differently than an "ib" font. It should not be possible to # have a "bb" text state, but if one were to show up, an entry for the # #font_families would have to be defined to select anything other than # the default font. This function is to be called whenever the current # text state is changed; it will update the current font to whatever the # appropriate font defined in the font family. # # When the user calls #select_font, both the current base font and the # current font will be reset; this function only changes the current # font, not the current base font. # # This will probably not be needed by end users. def current_font! select_font("Helvetica") unless @current_base_font font = File.basename(@current_base_font) if @font_families[font] and @font_families[font][@current_text_state] # Then we are in some state or another and this font has a family, # and the current setting exists within it select the font, then # return it. if File.dirname(@current_base_font) != '.' nf = File.join(File.dirname(@current_base_font), @font_families[font][@current_text_state]) else nf = @font_families[font][@current_text_state] end unless @fonts[nf] enc = { :encoding => @fonts[font].encoding, :differences => @fonts[font].differences } load_font(nf, enc) end @current_font = nf else @current_font = @current_base_font end end attr_reader :current_font attr_reader :current_base_font attr_accessor :font_size # add content to the currently active object def add_content(cc) @current_contents << cc end # Return the height in units of the current font in the given size. Uses # the current #font_size if size is not provided. def font_height(size = nil) size = @font_size if size.nil? or size <= 0 select_font("Helvetica") if @fonts.empty? hh = @fonts[@current_font].fontbbox[3].to_f - @fonts[@current_font].fontbbox[1].to_f (size * hh / 1000.0) end # Return the font descender, this will normally return a negative # number. If you add this number to the baseline, you get the level of # the bottom of the font it is in the PDF user units. Uses the current # #font_size if size is not provided. def font_descender(size = nil) size = @font_size if size.nil? or size <= 0 select_font("Helvetica") if @fonts.empty? hi = @fonts[@current_font].fontbbox[1].to_f (size * hi / 1000.0) end # Given a start position and information about how text is to be laid # out, calculate where on the page the text will end. def text_end_position(x, y, angle, size, wa, text) width = text_width(text, size) width += wa * (text.count(" ")) rad = PDF::Math.deg2rad(angle) [Math.cos(rad) * width + x, ((-Math.sin(rad)) * width + y)] end private :text_end_position # Wrapper function for #text_tags def quick_text_tags(text, ii, font_change) ret = text_tags(text, ii, font_change) [ret[0], ret[1], ret[2]] end private :quick_text_tags # Matches tags. MATCH_TAG_REPLACE_RE = %r{^r:(\w+)(?: (.*?))? */} #:nodoc: MATCH_TAG_DRAW_ONE_RE = %r{^C:(\w+)(?: (.*?))? */} #:nodoc: MATCH_TAG_DRAW_PAIR_RE = %r{^c:(\w+)(?: (.*))? *} #:nodoc: # Checks if +text+ contains a control tag at +pos+. Control tags are # XML-like tags that contain tag information. # # === Supported Tag Formats # <b>:: Adds +b+ to the end of the current # text state. If this is the closing # tag, </b>, +b+ is removed # from the end of the current text # state. # <i>:: Adds +i+ to the end of the current # text state. If this is the closing # tag, </i, +i+ is removed # from the end of the current text # state. # <r:TAG[ PARAMS]/>:: Calls a stand-alone replace callback # method of the form tag_TAG_replace. # PARAMS must be separated from the TAG # name by a single space. The PARAMS, if # present, are passed to the replace # callback unmodified, whose # responsibility it is to interpret the # parameters. The replace callback is # expected to return text that will be # used in the place of the tag. # #text_tags is called again immediately # so that if the replacement text has # tags, they will be dealt with # properly. # <C:TAG[ PARAMS]/>:: Calls a stand-alone drawing callback # method. The method will be provided an # information hash (see below for the # data provided). It is expected to use # this information to perform whatever # drawing tasks are needed to perform # its task. # <c:TAG[ PARAMS]>:: Calls a paired drawing callback # method. The method will be provided an # information hash (see below for the # data provided). It is expected to use # this information to perform whatever # drawing tasks are needed to perform # its task. It must have a corresponding # </c:TAG> closing tag. Paired # callback behaviours will be preserved # over page breaks and line changes. # # Drawing callback tags will be provided an information hash that tells # the callback method where it must perform its drawing tasks. # # === Drawing Callback Parameters # :x:: The current X position of the text. # :y:: The current y position of the text. # :angle:: The current text drawing angle. # :params:: Any parameters that may be important to the # callback. This value is only guaranteed to have # meaning when a stand-alone callback is made or the # opening tag is processed. # :status:: :start, :end, :start_line, :end_line # :cbid:: The identifier of this callback. This may be # used as a key into a different variable where # state may be kept. # :callback:: The name of the callback function. Only set for # stand-alone or opening callback tags. # :height:: The font height. # :descender:: The font descender size. # # ==== :status Values and Meanings # :start:: The callback has been started. This applies # either when the callback is a stand-alone # callback (<C:TAG/>) or the opening # tag of a paired tag (<c:TAG>). # :end:: The callback has been manually terminated with # a closing tag (</c:TAG>). # :start_line:: Called when a new line is to be drawn. This # allows the callback to perform any updates # necessary to permit paired callbacks to cross # line boundaries. This will usually involve # updating x, y positions. # :end_line:: Called when the end of a line is reached. This # permits the callback to perform any drawing # necessary to permit paired callbacks to cross # line boundaries. # # Drawing callback methods may return a hash of the :x and # :y position that the drawing pointer should take after the # callback is complete. # # === Known Callback Tags # <c:alink URI>:: makes an external link around text # between the opening and closing tags of # this callback. The URI may be any URL, # including http://, ftp://, and mailto:, # as long as there is a URL handler # registered. URI is of the form # uri="URI". # <c:ilink DEST>:: makes an internal link within the # document. The DEST must refer to a known # named destination within the document. # DEST is of the form dest="DEST". # <c:uline>:: underlines the specified text. # <C:bullet>:: Draws a solid bullet at the tag # position. # <C:disc>:: Draws a disc bullet at the tag position. def text_tags(text, pos, font_change, final = false, x = 0, y = 0, size = 0, angle = 0, word_space_adjust = 0) tag_size = 0 tag_match = %r!^<(/)?([^>]+)>!.match(text[pos..-1]) if tag_match closed, tag_name = tag_match.captures cts = @current_text_state # Alias for shorter lines. tag_size = tag_name.size + 2 + (closed ? 1 : 0) case tag_name when %r{^(?:b|strong)$}o if closed cts.slice!(-1, 1) if ?b == cts[-1] else cts << ?b end when %r{^(?:i|em)$}o if closed cts.slice!(-1, 1) if ?i == cts[-1] else cts << ?i end when %r{^r:}o _match = MATCH_TAG_REPLACE_RE.match(tag_name) if _match.nil? warn PDF::Writer::Lang[:callback_warning] % [ 'r:', tag_name ] tag_size = 0 else func = _match.captures[0] params = parse_tag_params(_match.captures[1] || "") tag = TAGS[:replace][func] if tag text[pos, tag_size] = tag[self, params] tag_size, text, font_change, x, y = text_tags(text, pos, font_change, final, x, y, size, angle, word_space_adjust) else warn PDF::Writer::Lang[:callback_warning] % [ 'r:', func ] tag_size = 0 end end when %r{^C:}o _match = MATCH_TAG_DRAW_ONE_RE.match(tag_name) if _match.nil? warn PDF::Writer::Lang[:callback_warning] % [ 'C:', tag_name ] tag_size = 0 else func = _match.captures[0] params = parse_tag_params(_match.captures[1] || "") tag = TAGS[:single][func] if tag font_change = false if final # Only call the function if this is the "final" call. Assess # the text position. Calculate the text width to this point. x, y = text_end_position(x, y, angle, size, word_space_adjust, text[0, pos]) info = { :x => x, :y => y, :angle => angle, :params => params, :status => :start, :cbid => @callbacks.size + 1, :callback => func, :height => font_height(size), :descender => font_descender(size) } ret = tag[self, info] if ret.kind_of?(Hash) ret.each do |rk, rv| x = rv if rk == :x y = rv if rk == :y font_change = rv if rk == :font_change end end end else warn PDF::Writer::Lang[:callback_Warning] % [ 'C:', func ] tag_size = 0 end end when %r{^c:}o _match = MATCH_TAG_DRAW_PAIR_RE.match(tag_name) if _match.nil? warn PDF::Writer::Lang[:callback_warning] % [ 'c:', tag_name ] tag_size = 0 else func = _match.captures[0] params = parse_tag_params(_match.captures[1] || "") tag = TAGS[:pair][func] if tag font_change = false if final # Only call the function if this is the "final" call. Assess # the text position. Calculate the text width to this point. x, y = text_end_position(x, y, angle, size, word_space_adjust, text[0, pos]) info = { :x => x, :y => y, :angle => angle, :params => params, } if closed info[:status] = :end info[:cbid] = @callbacks.size ret = tag[self, info] if ret.kind_of?(Hash) ret.each do |rk, rv| x = rv if rk == :x y = rv if rk == :y font_change = rv if rk == :font_change end end @callbacks.pop else info[:status] = :start info[:cbid] = @callbacks.size + 1 info[:tag] = tag info[:callback] = func info[:height] = font_height(size) info[:descender] = font_descender(size) @callbacks << info ret = tag[self, info] if ret.kind_of?(Hash) ret.each do |rk, rv| x = rv if rk == :x y = rv if rk == :y font_change = rv if rk == :font_change end end end end else warn PDF::Writer::Lang[:callback_warning] % [ 'c:', func ] tag_size = 0 end end else tag_size = 0 end end [ tag_size, text, font_change, x, y ] end private :text_tags TAG_PARAM_RE = %r{(\w+)=(?:"([^"]+)"|'([^']+)'|(\w+))} #:nodoc: def parse_tag_params(params) params ||= "" ph = {} params.scan(TAG_PARAM_RE) do |param| ph[param[0]] = param[1] || param[2] || param[3] end ph end private :parse_tag_params # Add +text+ to the document at (x, y) location at +size+ and # +angle+. The +word_space_adjust+ parameter is an internal parameter # that should not be used. # # As of PDF::Writer 1.1, +size+ and +text+ have been reversed and +size+ # is now optional, defaulting to the current #font_size if unset. def add_text(x, y, text, size = nil, angle = 0, word_space_adjust = 0) if text.kind_of?(Numeric) and size.kind_of?(String) text, size = size, text warn PDF::Writer::Lang[:add_text_parameters_reversed] % caller[0] end if size.nil? or size <= 0 size = @font_size end select_font("Helvetica") if @fonts.empty? text = text.to_s # If there are any open callbacks, then they should be called, to show # the start of the line @callbacks.reverse_each do |ii| info = ii.dup info[:x] = x info[:y] = y info[:angle] = angle info[:status] = :start_line info[:tag][self, info] end if angle == 0 add_content("\nBT %.3f %.3f Td" % [x, y]) else rad = PDF::Math.deg2rad(angle) tt = "\nBT %.3f %.3f %.3f %.3f %.3f %.3f Tm" tt = tt % [ Math.cos(rad), Math.sin(rad), -Math.sin(rad), Math.cos(rad), x, y ] add_content(tt) end if (word_space_adjust != 0) or not ((@word_space_adjust.nil?) and (@word_space_adjust != word_space_adjust)) @word_space_adjust = word_space_adjust add_content(" %.3f Tw" % word_space_adjust) end pos = -1 start = 0 loop do pos += 1 break if pos == text.size font_change = true tag_size, text, font_change = quick_text_tags(text, pos, font_change) if tag_size != 0 if pos > start part = text[start, pos - start] tt = " /F#{find_font(@current_font).font_id}" tt << " %.1f Tf %d Tr" % [ size, @current_text_render_style ] tt << " (#{PDF::Writer.escape(part)}) Tj" add_content(tt) end if font_change current_font! else add_content(" ET") xp = x yp = y tag_size, text, font_change, xp, yp = text_tags(text, pos, font_change, true, xp, yp, size, angle, word_space_adjust) # Restart the text object if angle.zero? add_content("\nBT %.3f %.3f Td" % [xp, yp]) else rad = PDF::Math.deg2rad(angle) tt = "\nBT %.3f %.3f %.3f %.3f %.3f %.3f Tm" tt = tt % [ Math.cos(rad), Math.sin(rad), -Math.sin(rad), Math.cos(rad), xp, yp ] add_content(tt) end if (word_space_adjust != 0) or (word_space_adjust != @word_space_adjust) @word_space_adjust = word_space_adjust add_content(" %.3f Tw" % [word_space_adjust]) end end pos += tag_size - 1 start = pos + 1 end end if start < text.size part = text[start..-1] tt = " /F#{find_font(@current_font).font_id}" tt << " %.1f Tf %d Tr" % [ size, @current_text_render_style ] tt << " (#{PDF::Writer.escape(part)}) Tj" add_content(tt) end add_content(" ET") # XXX: Experimental fix. @callbacks.reverse_each do |ii| info = ii.dup info[:x] = x info[:y] = y info[:angle] = angle info[:status] = :end_line info[:tag][self, info] end end def char_width(font, char) char = char[0] unless @fonts[font].c[char] if @fonts[font].differences and @fonts[font].c[char].nil? name = @fonts[font].differences[char] || 'M' width = @fonts[font].c[name]['WX'] if @fonts[font].c[name]['WX'] elsif @fonts[font].c[char] width = @fonts[font].c[char]['WX'] else width = @fonts[font].c['M']['WX'] end width end private :char_width # Calculate how wide a given text string will be on a page, at a given # size. This may be called externally, but is alse used by #text_width. # If +size+ is not specified, PDF::Writer will use the current # #font_size. # # The argument list is reversed from earlier versions. def text_line_width(text, size = nil) if text.kind_of?(Numeric) and size.kind_of?(String) text, size = size, text warn PDF::Writer::Lang[:text_width_parameters_reversed] % caller[0] end if size.nil? or size <= 0 size = @font_size end # This function should not change any of the settings, though it will # need to track any tag which change during calculation, so copy them # at the start and put them back at the end. t_CTS = @current_text_state.dup select_font("Helvetica") if @fonts.empty? # converts a number or a float to a string so it can get the width tt = text.to_s # hmm, this is where it all starts to get tricky - use the font # information to calculate the width of each character, add them up # and convert to user units width = 0 font = @current_font pos = -1 loop do pos += 1 break if pos == tt.size font_change = true tag_size, text, font_change = quick_text_tags(text, pos, font_change) if tag_size != 0 if font_change current_font! font = @current_font end pos += tag_size - 1 else if "<" == tt[pos, 4] width += char_width(font, '<') pos += 3 elsif ">" == tt[pos, 4] width += char_width(font, '>') pos += 3 elsif "&" == tt[pos, 5] width += char_width(font, '&') pos += 4 else width += char_width(font, tt[pos, 1]) end end end @current_text_state = t_CTS.dup current_font! (width * size / 1000.0) end # Calculate how wide a given text string will be on a page, at a given # size. If +size+ is not specified, PDF::Writer will use the current # #font_size. The difference between this method and #text_line_width is # that this method will iterate over lines separated with newline # characters. # # The argument list is reversed from earlier versions. def text_width(text, size = nil) if text.kind_of?(Numeric) and size.kind_of?(String) text, size = size, text warn PDF::Writer::Lang[:text_width_parameters_reversed] % caller[0] end if size.nil? or size <= 0 size = @font_size end max = 0 text.to_s.each do |line| width = text_line_width(line, size) max = width if width > max end max end # Partially calculate the values necessary to sort out the justification # of text. def adjust_wrapped_text(text, actual, width, x, just) adjust = 0 case just when :left nil when :right x += (width - actual) when :center x += (width - actual) / 2.0 when :full spaces = text.count(" ") adjust = (width - actual) / spaces.to_f if spaces > 0 end [x, adjust] end private :adjust_wrapped_text # Add text to the page, but ensure that it fits within a certain width. # If it does not fit then put in as much as possible, breaking at word # boundaries; return the remainder. +justification+ and +angle+ can also # be specified for the text. # # This will display the text; if it goes beyond the width +width+, it # will backttrack to the previous space or hyphen and return the # remainder of the text. # # +justification+:: :left, :right, :center, or :full def add_text_wrap(x, y, width, text, size = nil, justification = :left, angle = 0, test = false) if text.kind_of?(Numeric) and size.kind_of?(String) text, size = size, text warn PDF::Writer::Lang[:add_textw_parameters_reversed] % caller[0] end if size.nil? or size <= 0 size = @font_size end # Need to store the initial text state, as this will change during the # width calculation, but will need to be re-set before printing, so # that the chars work out right t_CTS = @current_text_state.dup select_font("Helvetica") if @fonts.empty? return "" if width <= 0 w = brk = brkw = 0 font = @current_font tw = width / size.to_f * 1000 pos = -1 loop do pos += 1 break if pos == text.size font_change = true tag_size, text, font_change = quick_text_tags(text, pos, font_change) if tag_size != 0 if font_change current_font! font = @current_font end pos += (tag_size - 1) else w += char_width(font, text[pos, 1]) if w > tw # We need to truncate this line if brk > 0 # There is somewhere to break the line. if text[brk] == " " tmp = text[0, brk] else tmp = text[0, brk + 1] end x, adjust = adjust_wrapped_text(tmp, brkw, width, x, justification) # Reset the text state @current_text_state = t_CTS.dup current_font! add_text(x, y, tmp, size, angle, adjust) unless test return text[brk + 1..-1] else # just break before the current character tmp = text[0, pos] # tmpw = (w - char_width(font, text[pos, 1])) * size / 1000.0 x, adjust = adjust_wrapped_text(tmp, brkw, width, x, justification) # Reset the text state @current_text_state = t_CTS.dup current_font! add_text(x, y, tmp, size, angle, adjust) unless test return text[pos..-1] end end if text[pos] == ?- brk = pos brkw = w * size / 1000.0 end if text[pos, 1] == " " brk = pos ctmp = text[pos] ctmp = @fonts[font].differences[ctmp] unless @fonts[font].differences.nil? z = @fonts[font].c[tmp].nil? ? 0 : @fonts[font].c[tmp]['WX'] brkw = (w - z) * size / 1000.0 end end end # There was no need to break this line. justification = :left if justification == :full tmpw = (w * size) / 1000.0 x, adjust = adjust_wrapped_text(text, tmpw, width, x, justification) # reset the text state @current_text_state = t_CTS.dup current_font! add_text(x, y, text, size, angle, adjust) unless test return "" end # Saves the state. def save_state PDF::Writer::State.new do |state| state.fill_color = @current_fill_color state.stroke_color = @current_stroke_color state.text_render_style = @current_text_render_style state.stroke_style = @current_stroke_style @state_stack.push state end add_content("\nq") end # This will be called at a new page to return the state to what it was # on the end of the previous page, before the stack was closed down. # This is to get around not being able to have open 'q' across pages. def reset_state_at_page_start @state_stack.each do |state| fill_color! state.fill_color stroke_color! state.stroke_color text_render_style! state.text_render_style stroke_style! state.stroke_style add_content("\nq") end end private :reset_state_at_page_start # Restore a previously saved state. def restore_state unless @state_stack.empty? state = @state_stack.pop @current_fill_color = state.fill_color @current_stroke_color = state.stroke_color @current_text_render_style = state.text_render_style @current_stroke_style = state.stroke_style stroke_style! end add_content("\nQ") end # Restore the state at the end of a page. def reset_state_at_page_finish add_content("\nQ" * @state_stack.size) end private :reset_state_at_page_finish # Make a loose object. The output will go into this object, until it is # closed, then will revert to the current one. This object will not # appear until it is included within a page. The function will return # the object reference. def open_object @stack << { :contents => @current_contents, :page => @current_page } @current_contents = PDF::Writer::Object::Contents.new(self) @loose_objects << @current_contents yield @current_contents if block_given? @current_contents end # Opens an existing object for editing. def reopen_object(id) @stack << { :contents => @current_contents, :page => @current_page } @current_contents = id # if this object is the primary contents for a page, then set the # current page to its parent @current_page = @current_contents.on_page unless @current_contents.on_page.nil? @current_contents end # Close an object for writing. def close_object unless @stack.empty? obj = @stack.pop @current_contents = obj[:contents] @current_page = obj[:page] end end # Stop an object from appearing on pages from this point on. def stop_object(id) obj = @loose_objects.detect { |ii| ii.oid == id.oid } @add_loose_objects[obj] = nil end # After an object has been created, it will only show if it has been # added, using this method. def add_object(id, where = :this_page) obj = @loose_objects.detect { |ii| ii == id } if obj and @current_contents != obj case where when :all_pages, :this_page @add_loose_objects[obj] = where if where == :all_pages @current_contents.on_page.contents << obj if @current_contents.on_page when :even_pages @add_loose_objects[obj] = where page = @current_contents.on_page add_object(id) if (page.info.page_number % 2) == 0 when :odd_pages @add_loose_objects[obj] = where page = @current_contents.on_page add_object(id) if (page.info.page_number % 2) == 1 when :all_following_pages @add_loose_objects[obj] = :all_pages when :following_even_pages @add_loose_objects[obj] = :even_pages when :following_odd_pages @add_loose_objects[obj] = :odd_pages end end end # Add content to the documents info object. def add_info(label, value = 0) # This will only work if the label is one of the valid ones. Modify # this so that arrays can be passed as well. If @label is an array # then assume that it is key => value pairs else assume that they are # both scalar, anything else will probably error. if label.kind_of?(Hash) label.each { |kk, vv| @info.__send__(kk.downcase.intern, vv) } else @info.__send__(label.downcase.intern, value) end end # Specify the Destination object where the document should open when it # first starts. +style+ must be one of the values detailed for # #destinations. The value of +style+ affects the interpretation of # +params+. Uses the current page as the starting location. def open_here(style, *params) open_at(@current_page, style, *params) end # Specify the Destination object where the document should open when it # first starts. +style+ must be one of the following values. The value # of +style+ affects the interpretation of +params+. Uses +page+ as the # starting location. def open_at(page, style, *params) d = PDF::Writer::Object::Destination.new(self, page, style, *params) @catalog.open_here = d end # Create a labelled destination within the document. The label is the # name which will be used for destinations. # # XYZ:: The viewport will be opened at position (left, top) # with +zoom+ percentage. +params+ must have three values # representing +left+, +top+, and +zoom+, respectively. If the # values are "null", the current parameter values are unchanged. # Fit:: Fit the page to the viewport (horizontal and vertical). # +params+ will be ignored. # FitH:: Fit the page horizontally to the viewport. The top of the # viewport is set to the first value in +params+. # FitV:: Fit the page vertically to the viewport. The left of the # viewport is set to the first value in +params+. # FitR:: Fits the page to the provided rectangle. +params+ must have # four values representing the +left+, +bottom+, +right+, and # +top+ positions, respectively. # FitB:: Fits the page to the bounding box of the page. +params+ is # ignored. # FitBH:: Fits the page horizontally to the bounding box of the page. # The top position is defined by the first value in +params+. # FitBV:: Fits the page vertically to the bounding box of the page. The # left position is defined by the first value in +params+. def add_destination(label, style, *params) @destinations[label] = PDF::Writer::Object::Destination.new(self, @current_page, style, *params) end # Set the page mode of the catalog. Must be one of the following: # UseNone:: Neither document outline nor thumbnail images are # visible. # UseOutlines:: Document outline visible. # UseThumbs:: Thumbnail images visible. # FullScreen:: Full-screen mode, with no menu bar, window controls, or # any other window visible. # UseOC:: Optional content group panel is visible. # def page_mode=(mode) @catalog.page_mode = value end include Transaction::Simple # The width of the currently active column. This will return zero (0) if # columns are off. attr_reader :column_width def column_width #:nodoc: return 0 unless @columns_on @columns[:width] end # The gutter between columns. This will return zero (0) if columns are # off. attr_reader :column_gutter def column_gutter #:nodoc: return 0 unless @columns_on @columns[:gutter] end # The current column number. Returns zero (0) if columns are off. attr_reader :column_number def column_number #:nodoc: return 0 unless @columns_on @columns[:current] end # The total number of columns. Returns zero (0) if columns are off. attr_reader :column_count def column_count #:nodoc: return 0 unless @columns_on @columns[:size] end # Indicates if columns are currently on. def columns? @columns_on end # Starts multi-column output. Creates +size+ number of columns with a # +gutter+ PDF unit space between each column. # # If columns are already started, this will return +false+. def start_columns(size = 2, gutter = 10) # Start from the current y-position; make the set number of columns. return false if @columns_on @columns = { :current => 1, :bot_y => @y } @columns_on = true # store the current margins @columns[:left] = @left_margin @columns[:right] = @right_margin @columns[:top] = @top_margin @columns[:bottom] = @bottom_margin # Reset the margins to suit the new columns. Safe enough to assume the # first column here, but start from the current y-position. @top_margin = @page_height - @y @columns[:size] = size || 2 @columns[:gutter] = gutter || 10 w = absolute_right_margin - absolute_left_margin @columns[:width] = (w - ((size - 1) * gutter)) / size.to_f @right_margin = @page_width - (@left_margin + @columns[:width]) end def restore_margins_after_columns @left_margin = @columns[:left] @right_margin = @columns[:right] @top_margin = @columns[:top] @bottom_margin = @columns[:bottom] end private :restore_margins_after_columns # Turns off multi-column output. If we are in the first column, or the # lowest point at which columns were written is higher than the bottom # of the page, then the writing pointer will be placed at the lowest # point. Otherwise, a new page will be started. def stop_columns return false unless @columns_on @columns_on = false @columns[:bot_y] = @y if @y < @columns[:bot_y] if (@columns[:bot_y] > @bottom_margin) or @column_number == 1 @y = @columns[:bot_y] else start_new_page end restore_margins_after_columns @columns = {} true end # Changes page insert mode. May be called as follows: # # pdf.insert_mode # => current insert mode # # The following four affect the insert mode without changing the # # insert page or insert position. # pdf.insert_mode(:on) # enables insert mode # pdf.insert_mode(true) # enables insert mode # pdf.insert_mode(:off) # disables insert mode # pdf.insert_mode(false) # disables insert mode # # # Changes the insert mode, the insert page, and the insert # # position at the same time. # opts = { # :on => true, # :page => :last, # :position => :before # } # pdf.insert_mode(opts) def insert_mode(options = {}) case options when :on, true @insert_mode = true when :off, false @insert_mode = false else return @insert_mode unless options @insert_mode = options[:on] unless options[:on].nil? unless options[:page].nil? if @pageset[options[:page]].nil? or options[:page] == :last @insert_page = @pageset[-1] else @insert_page = @pageset[options[:page]] end end @insert_position = options[:position] if options[:position] end end # Returns or changes the insert page property. # # pdf.insert_page # => current insert page # pdf.insert_page(35) # insert at page 35 # pdf.insert_page(:last) # insert at the last page def insert_page(page = nil) return @insert_page unless page if page == :last @insert_page = @pageset[-1] else @insert_page = @pageset[page] end end # Changes the #insert_page property to append to the page set. def append_page insert_mode(:last) end # Returns or changes the insert position to be before or after the # specified page. # # pdf.insert_position # => current insert position # pdf.insert_position(:before) # insert before #insert_page # pdf.insert_position(:after) # insert before #insert_page def insert_position(position = nil) return @insert_position unless position @insert_position = position end # Creates a new page. If multi-column output is turned on, this will # change the column to the next greater or create a new page as # necessary. If +force+ is true, then a new page will be created even if # multi-column output is on. def start_new_page(force = false) page_required = true if @columns_on # Check if this is just going to a new column. Increment the column # number. @columns[:current] += 1 if @columns[:current] <= @columns[:size] and not force page_required = false @columns[:bot_y] = @y if @y < @columns[:bot_y] else @columns[:current] = 1 @top_margin = @columns[:top] @columns[:bot_y] = absolute_top_margin end w = @columns[:width] g = @columns[:gutter] n = @columns[:current] - 1 @left_margin = @columns[:left] + n * (g + w) @right_margin = @page_width - (@left_margin + w) end if page_required or force # make a new page, setting the writing point back to the top. @y = absolute_top_margin # make the new page with a call to the basic class if @insert_mode id = new_page(true, @insert_page, @insert_position) @pageset << id # Manipulate the insert options so that inserted pages follow each # other @insert_page = id @insert_position = :after else @pageset << new_page end else @y = absolute_top_margin end @pageset end # Add a new page to the document. This also makes the new page the # current active object. This allows for mandatory page creation # regardless of multi-column output. # # For most purposes, #start_new_page is preferred. def new_page(insert = false, page = nil, pos = :after) reset_state_at_page_finish if insert # The id from the PDF::Writer class is the id of the contents of the # page, not the page object itself. Query that object to find the # parent. _new_page = PDF::Writer::Object::Page.new(self, { :rpage => page, :pos => pos }) else _new_page = PDF::Writer::Object::Page.new(self) end reset_state_at_page_start # If there has been a stroke or fill color set, transfer them. fill_color! stroke_color! stroke_style! # the call to the page object set @current_contents to the present page, # so this can be returned as the page id # @current_contents _new_page end # Returns the current generic page number. This is based exclusively on # the size of the page set. def current_page_number @pageset.size end # Put page numbers on the pages from the current page. Place them # relative to the coordinates (x, y) with the text horizontally # relative according to +pos+, which may be :left, # :right, or :center. The page numbers will be written # on each page using +pattern+. # # When +pattern+ is rendered, will be replaced with the # current page number; will be replaced with the total # number of pages in the page numbering scheme. The default +pattern+ is # " of ". # # If +starting+ is non-nil, this is the first page number. The number of # total pages will be adjusted to account for this. # # Each time page numbers are started, a new page number scheme will be # started. The scheme number will be returned. # # This method has been dprecated. def start_page_numbering(x, y, size, pos = nil, pattern = nil, starting = nil) warn PDF::Writer::Lang[:page_numbering_deprecated] % "start_page_numbering" pos ||= :left pattern ||= " of " starting ||= 1 @page_numbering ||= [] @page_numbering << (o = {}) page = @pageset.size - 1 o[page] = { :x => x, :y => y, :pos => pos, :pattern => pattern, :starting => starting, :size => size, :start => true } @page_numbering.index(o) end # Given a particular generic page number +page_num+ (numbered # sequentially from the beginning of the page set), return the page # number under a particular page numbering +scheme+ (defaults to the # first scheme turned on). Returns +nil+ if page numbering is not turned # on or if the page is not under the current numbering scheme. # # This method has been dprecated. def which_page_number(page_num, scheme = 0) warn PDF::Writer::Lang[:page_numbering_deprecated] % "which_page_number" return nil unless @page_numbering num = nil start = start_num = 1 @page_numbering[scheme].each do |kk, vv| if kk <= page_num if vv.kind_of?(Hash) unless vv[:starting].nil? start = vv[:starting] start_num = kk num = page_num - start_num + start end else num = nil end end end num end # Stop page numbering. Returns +false+ if page numbering is off. # # If +stop_total+ is true, then then the totaling of pages for this page # numbering +scheme+ will be stopped as well. If +stop_at+ is # :current, then the page numbering will stop at this page; # otherwise, it will stop at the next page. # # This method has been dprecated. def stop_page_numbering(stop_total = false, stop_at = :current, scheme = 0) warn PDF::Writer::Lang[:page_numbering_deprecated] % "stop_page_numbering" return false unless @page_numbering page = @pageset.size - 1 @page_numbering[scheme][page] ||= {} o = @page_numbering[scheme][page] case [ stop_total, stop_at == :current ] when [ true, true ] o[:stop] = :stop_total when [ true, false ] o[:stop] = :stop_total_next when [ false, true ] o[:stop] = :stop_next else o[:stop] = :stop end end def page_number_search(condition, scheme) res = nil scheme.each { |page, value| res = page if value[:stop] == condition } res end private :page_number_search def add_page_numbers # This will go through the @page_numbering array and add the page # numbers are required. if @page_numbering page_count = @pageset.size pn_tmp = @page_numbering.dup # Go through each of the page numbering schemes. pn_tmp.each do |scheme| # First, find the total pages for this schemes. page = page_number_search(:stop_total, scheme) if page total_pages = page else page = page_number_search(:stop_total_next, scheme) if page total_pages = page else total_pages = page_count end end status = nil delta = pattern = pos = x = y = size = nil @pageset.each_with_index do |page, index| next if status.nil? and scheme[index].nil? info = scheme[index] if info if info[:start] status = true if info[:starting] delta = info[:starting] - index else delta = index end pattern = info[:pattern] pos = info[:pos] x = info[:x] y = info[:y] size = info[:size] # Check for the special case of page numbering starting and # stopping on the same page. status = :stop_next if info[:stop] elsif [:stop, :stop_total].include?(info[:stop]) status = :stop_now elsif status == true and [:stop_next, :stop_total_next].include?(info[:stop]) status = :stop_next end end if status # Add the page numbering to this page num = index + delta.to_i total = total_pages + num - index patt = pattern.gsub(//, num.to_s).gsub(//, total.to_s) reopen_object(page.contents.first) case pos when :left # Write the page number from x. w = 0 when :right # Write the page number to x. w = text_width(patt, size) when :center # Write the page number around x. w = text_width(patt, size) / 2.0 end add_text(x - w, y, patt, size) close_object status = nil if [ :stop_now, :stop_next ].include?(status) end end end end end private :add_page_numbers def preprocess_text(text) text end private :preprocess_text # This will add a string of +text+ to the document, starting at the # current drawing position. It will wrap to keep within the margins, # including optional offsets from the left and the right. The text will # go to the start of the next line when a return code "\n" is found. # # Possible +options+ are: # :font_size:: The font size to be used. If not # specified, is either the last font size or # the default font size of 12 points. # Setting this value *changes* the current # #font_size. # :left:: number, gap to leave from the left margin # :right:: number, gap to leave from the right margin # :absolute_left:: number, absolute left position (overrides # :left) # :absolute_right:: number, absolute right position (overrides # :right) # :justification:: :left, :right, # :center, :full # :leading:: number, defines the total height taken by # the line, independent of the font height. # :spacing:: a Floating point number, though usually # set to one of 1, 1.5, 2 (line spacing as # used in word processing) # # Only one of :leading or :spacing should be specified # (leading overrides spacing). # # If the :test option is +true+, then this should just check to # see if the text is flowing onto a new page or not; returns +true+ or # +false+. Note that the new page test is only sensitive to exceeding # the bottom margin of the page. It is not known whether the writing of # the text will require a new physical page or whether it will require a # new column. def text(text, options = {}) # Apply the filtering which will make underlining (and other items) # function. text = preprocess_text(text) options ||= {} new_page_required = false __y = @y if options[:absolute_left] left = options[:absolute_left] else left = @left_margin left += options[:left] if options[:left] end if options[:absolute_right] right = options[:absolute_right] else right = absolute_right_margin right -= options[:right] if options[:right] end size = options[:font_size] || 0 if size <= 0 size = @font_size else @font_size = size end just = options[:justification] || :left if options[:leading] # leading instead of spacing height = options[:leading] elsif options[:spacing] height = options[:spacing] * font_height(size) else height = font_height(size) end text.each do |line| start = true loop do # while not line.empty? or start break if (line.nil? or line.empty?) and not start start = false @y -= height if @y < @bottom_margin if options[:test] new_page_required = true else # and then re-calc the left and right, in case they have # changed due to columns start_new_page @y -= height if options[:absolute_left] left = options[:absolute_left] else left = @left_margin left += options[:left] if options[:left] end if options[:absolute_right] right = options[:absolute_right] else right = absolute_right_margin right -= options[:right] if options[:right] end end end line = add_text_wrap(left, @y, right - left, line, size, just, 0, options[:test]) end end if options[:test] @y = __y new_page_required else @y end end def prepress_clip_mark(x, y, angle, mark_length = 18, bleed_size = 12) #:nodoc: save_state translate_axis(x, y) rotate_axis(angle) line(0, bleed_size, 0, bleed_size + mark_length).stroke line(bleed_size, 0, bleed_size + mark_length, 0).stroke restore_state end def prepress_center_mark(x, y, angle, mark_length = 18, bleed_size = 12) #:nodoc: save_state translate_axis(x, y) rotate_axis(angle) half_mark = mark_length / 2.0 c_x = 0 c_y = bleed_size + half_mark line((c_x - half_mark), c_y, (c_x + half_mark), c_y).stroke line(c_x, (c_y - half_mark), c_x, (c_y + half_mark)).stroke rad = (mark_length * 0.50) / 2.0 circle_at(c_x, c_y, rad).stroke restore_state end # Returns the estimated number of lines remaining given the default or # specified font size. def lines_remaining(font_size = nil) font_size ||= @font_size remaining = @y - @bottom_margin remaining / font_height(font_size).to_f end # Callback tag relationships. All relationships are of the form # "tagname" => CallbackClass. # # There are three types of tag callbacks: # :pair:: Paired callbacks, e.g., . # :single:: Single-tag callbacks, e.g., . # :replace:: Single-tag replacement callbacks, e.g., . TAGS = { :pair => { }, :single => { }, :replace => { } } TAGS.freeze # A callback to support the formation of clickable links to external # locations. class TagAlink # The default anchored link style. DEFAULT_STYLE = { :color => Color::RGB::Blue, :text_color => Color::RGB::Blue, :draw_line => true, :line_style => { :dash => PDF::Writer::StrokeStyle::SOLID_LINE }, :factor => 0.05 } class << self # Sets the style for callback underlines that follow. This # is expected to be a hash with the following keys: # # :color:: The colour to be applied to the link # underline. Default is Color::RGB::Blue. # :text_color:: The colour to be applied to the link text. # Default is Color::RGB::Blue. # :factor:: The size of the line, as a multiple of the # text height. Default is 0.05. # :draw_line:: Whether to draw the underline as part of # the link or not. Default is +true+. # :line_style:: The style modification hash supplied to # PDF::Writer::StrokeStyle.new. The default # is a solid line with normal cap, join, and # miter limit values. # # Set this to +nil+ to get the default style. attr_accessor :style def [](pdf, info) @style ||= DEFAULT_STYLE.dup case info[:status] when :start, :start_line # The beginning of the link. This should contain the URI for the # link as the :params entry, and will also contain the value of # :cbid. @links ||= {} @links[info[:cbid]] = { :x => info[:x], :y => info[:y], :angle => info[:angle], :descender => info[:descender], :height => info[:height], :uri => info[:params]["uri"] } pdf.save_state pdf.fill_color @style[:text_color] if @style[:text_color] if @style[:draw_line] pdf.stroke_color @style[:color] if @style[:color] sz = info[:height] * @style[:factor] pdf.stroke_style! StrokeStyle.new(sz, @style[:line_style]) end when :end, :end_line # The end of the link. Assume that it is the most recent opening # which has closed. start = @links[info[:cbid]] # Add underlining. theta = PDF::Math.deg2rad(start[:angle] - 90.0) if @style[:draw_line] drop = start[:height] * @style[:factor] * 1.5 drop_x = Math.cos(theta) * drop drop_y = -Math.sin(theta) * drop pdf.move_to(start[:x] - drop_x, start[:y] - drop_y) pdf.line_to(info[:x] - drop_x, info[:y] - drop_y).stroke end pdf.add_link(start[:uri], start[:x], start[:y] + start[:descender], info[:x], start[:y] + start[:descender] + start[:height]) pdf.restore_state end end end end TAGS[:pair]["alink"] = TagAlink # A callback for creating and managing links internal to the document. class TagIlink def self.[](pdf, info) case info[:status] when :start, :start_line @links ||= {} @links[info[:cbid]] = { :x => info[:x], :y => info[:y], :angle => info[:angle], :descender => info[:descender], :height => info[:height], :uri => info[:params]["dest"] } when :end, :end_line # The end of the link. Assume that it is the most recent opening # which has closed. start = @links[info[:cbid]] pdf.add_internal_link(start[:uri], start[:x], start[:y] + start[:descender], info[:x], start[:y] + start[:descender] + start[:height]) end end end TAGS[:pair]["ilink"] = TagIlink # A callback to support underlining. class TagUline # The default underline style. DEFAULT_STYLE = { :color => nil, :line_style => { :dash => PDF::Writer::StrokeStyle::SOLID_LINE }, :factor => 0.05 } class << self # Sets the style for callback underlines that follow. This # is expected to be a hash with the following keys: # # :factor:: The size of the line, as a multiple of the # text height. Default is 0.05. # # Set this to +nil+ to get the default style. attr_accessor :style def [](pdf, info) @style ||= DEFAULT_STYLE.dup case info[:status] when :start, :start_line @links ||= {} @links[info[:cbid]] = { :x => info[:x], :y => info[:y], :angle => info[:angle], :descender => info[:descender], :height => info[:height], :uri => nil } pdf.save_state pdf.stroke_color @style[:color] if @style[:color] sz = info[:height] * @style[:factor] pdf.stroke_style! StrokeStyle.new(sz, @style[:line_style]) when :end, :end_line start = @links[info[:cbid]] theta = PDF::Math.deg2rad(start[:angle] - 90.0) drop = start[:height] * @style[:factor] * 1.5 drop_x = Math.cos(theta) * drop drop_y = -Math.sin(theta) * drop pdf.move_to(start[:x] - drop_x, start[:y] - drop_y) pdf.line_to(info[:x] - drop_x, info[:y] - drop_y).stroke pdf.restore_state end end end end TAGS[:pair]["uline"] = TagUline # A callback function to support drawing of a solid bullet style. Use # with . class TagBullet # The default bullet color. DEFAULT_COLOR = Color::RGB::Black class << self # Sets the style for callback bullets that follow. # Default is Color::RGB::Black. # # Set this to +nil+ to get the default colour. attr_accessor :color def [](pdf, info) @color ||= DEFAULT_COLOR desc = info[:descender].abs xpos = info[:x] - (desc * 2.00) ypos = info[:y] + (desc * 1.05) pdf.save_state ss = StrokeStyle.new(desc) ss.cap = :butt ss.join = :miter pdf.stroke_style! ss pdf.stroke_color @color pdf.circle_at(xpos, ypos, 1).stroke pdf.restore_state end end end TAGS[:single]["bullet"] = TagBullet # A callback function to support drawing of a disc bullet style. class TagDisc # The default disc bullet foreground. DEFAULT_FOREGROUND = Color::RGB::Black # The default disc bullet background. DEFAULT_BACKGROUND = Color::RGB::White class << self # The foreground color for bullets. Default is # Color::RGB::Black. # # Set to +nil+ to get the default color. attr_accessor :foreground # The background color for bullets. Default is # Color::RGB::White. # # Set to +nil+ to get the default color. attr_accessor :background def [](pdf, info) @foreground ||= DEFAULT_FOREGROUND @background ||= DEFAULT_BACKGROUND desc = info[:descender].abs xpos = info[:x] - (desc * 2.00) ypos = info[:y] + (desc * 1.05) ss = StrokeStyle.new(desc) ss.cap = :butt ss.join = :miter pdf.stroke_style! ss pdf.stroke_color @foreground pdf.circle_at(xpos, ypos, 1).stroke pdf.stroke_color @background pdf.circle_at(xpos, ypos, 0.5).stroke end end end TAGS[:single]["disc"] = TagDisc # Opens a new PDF object for operating against. Returns the object's # identifier. To close the object, you'll need to do: # ob = open_new_object # Opens the object # # do stuff here # close_object # Closes the PDF document # # do stuff here # reopen_object(ob) # Reopens the custom object. # close_object # Closes it. # restore_state # Returns full control to the PDF document. # # ... I think. I haven't examined the full details to be sure of what # this is doing, but the code works. def open_new_object save_state oid = open_object close_object add_object(oid) reopen_object(oid) oid end # Save the PDF as a file to disk. def save_as(name) File.open(name, "wb") { |f| f.write self.render } end end