#--
# 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