File: //usr/lib/ruby/vendor_ruby/rvg/misc.rb
# $Id: misc.rb,v 1.17 2010/03/21 01:43:01 baror Exp $
# Copyright (C) 2009 Timothy P. Hunter
module Magick
class RVG
# This is a standard deep_copy method that is used in most classes.
# Thanks to Robert Klemme.
module Duplicatable
def deep_copy(h = {})
# Prevent recursion. If we reach the
# object we started with, stop copying.
copy = h[__id__]
unless copy
h[__id__] = copy = self.class.allocate
ivars = instance_variables
ivars.each do |ivar|
ivalue = instance_variable_get(ivar)
cvalue = if ivalue.is_a?(NilClass) || ivalue.is_a?(Symbol) || ivalue.is_a?(Float) || ivalue.is_a?(Integer) || ivalue.is_a?(FalseClass) || ivalue.is_a?(TrueClass)
ivalue
elsif ivalue.respond_to?(:deep_copy)
ivalue.deep_copy(h)
elsif ivalue.respond_to?(:dup)
ivalue.dup
else
ivalue
end
copy.instance_variable_set(ivar, cvalue)
end
copy.freeze if frozen?
end
copy
end
end # module Duplicatable
# Convert an array of method arguments to Float objects. If any
# cannot be converted, raise ArgumentError and issue a message.
def self.fmsg(*args)
"at least one argument cannot be converted to Float (got #{args.map(&:class).join(', ')})"
end
def self.convert_to_float(*args)
allow_nil = false
if args.last == :allow_nil
allow_nil = true
args.pop
end
begin
fargs = args.map { |a| allow_nil && a.nil? ? a : Float(a) }
rescue ArgumentError, TypeError
raise ArgumentError, fmsg(*args)
end
fargs
end
def self.convert_one_to_float(arg)
begin
farg = Float(arg)
rescue ArgumentError, TypeError
raise ArgumentError, "argument cannot be converted to Float (got #{arg.class})"
end
farg
end
end # class RVG
end # module Magick
module Magick
class RVG
class Utility
class TextStrategy
def initialize(context)
@ctx = context
@ctx.shadow.affine = @ctx.text_attrs.affine
end
def enquote(text)
return text if text.length > 2 && /\A(?:\"[^\"]+\"|\'[^\']+\'|\{[^\}]+\})\z/.match(text)
if !text['\'']
text = '\'' + text + '\''
return text
elsif !text['"']
text = '"' + text + '"'
return text
elsif !(text['{'] || text['}'])
text = '{' + text + '}'
return text
end
# escape existing braces, surround with braces
text.gsub!(/[}]/) { |b| '\\' + b }
'{' + text + '}'
end
def glyph_metrics(glyph_orientation, glyph)
gm = @ctx.shadow.get_type_metrics('a' + glyph + 'a')
gm2 = @ctx.shadow.get_type_metrics('aa')
h = (gm.ascent - gm.descent + 0.5).to_i
w = gm.width - gm2.width
if glyph_orientation.zero? || glyph_orientation == 180
[w, h]
else
[h, w]
end
end
def text_rel_coords(text)
y_rel_coords = []
x_rel_coords = []
first_word = true
words = text.split(::Magick::RVG::WORD_SEP)
words.each do |word|
unless first_word
wx, wy = get_word_spacing
x_rel_coords << wx
y_rel_coords << wy
end
first_word = false
word.split('').each do |glyph|
wx, wy = get_letter_spacing(glyph)
x_rel_coords << wx
y_rel_coords << wy
end
end
[x_rel_coords, y_rel_coords]
end
def shift_baseline(glyph_orientation, glyph)
glyph_dimensions = @ctx.shadow.get_type_metrics(glyph)
x = if glyph_orientation.zero? || glyph_orientation == 180
glyph_dimensions.width
else
glyph_dimensions.ascent - glyph_dimensions.descent
end
case @ctx.text_attrs.baseline_shift
when :baseline
x = 0
when :sub
when :super
x = -x
when /[-+]?(\d+)%/
m = Regexp.last_match(1) == '-' ? -1.0 : 1.0
x = (m * x * Regexp.last_match(1).to_f / 100.0)
else
x = -@ctx.text_attrs.baseline_shift
end
x
end
def render_glyph(glyph_orientation, x, y, glyph)
if glyph_orientation.zero?
@ctx.gc.text(x, y, enquote(glyph))
else
@ctx.gc.push
@ctx.gc.translate(x, y)
@ctx.gc.rotate(glyph_orientation)
@ctx.gc.translate(-x, -y)
@ctx.gc.text(x, y, enquote(glyph))
@ctx.gc.pop
end
end
end # class TextStrategy
class LRTextStrategy < TextStrategy
def get_word_spacing
@word_space ||= glyph_metrics(@ctx.text_attrs.glyph_orientation_horizontal, ' ')[0]
[@word_space + @ctx.text_attrs.word_spacing, 0]
end
def get_letter_spacing(glyph)
gx, gy = glyph_metrics(@ctx.text_attrs.glyph_orientation_horizontal, glyph)
[gx + @ctx.text_attrs.letter_spacing, gy]
end
def render(x, y, text)
x_rel_coords, y_rel_coords = text_rel_coords(text)
dx = x_rel_coords.reduce(0) { |sum, a| sum + a }
dy = y_rel_coords.max
# We're handling the anchoring.
@ctx.gc.push
@ctx.gc.text_anchor(Magick::StartAnchor)
if @ctx.text_attrs.text_anchor == :end
x -= dx
elsif @ctx.text_attrs.text_anchor == :middle
x -= dx / 2
end
# Align the first glyph
case @ctx.text_attrs.glyph_orientation_horizontal
when 90
y -= dy
when 180
x += x_rel_coords.shift
x_rel_coords << 0
y -= dy
when 270
x += x_rel_coords[0]
end
y += shift_baseline(@ctx.text_attrs.glyph_orientation_horizontal, text[0, 1])
first_word = true
text.split(::Magick::RVG::WORD_SEP).each do |word|
x += x_rel_coords.shift unless first_word
first_word = false
word.split('').each do |glyph|
render_glyph(@ctx.text_attrs.glyph_orientation_horizontal, x, y, glyph)
x += x_rel_coords.shift
end
end
@ctx.gc.pop
[dx, 0]
end
end # class LRTextStrategy
class RLTextStrategy < TextStrategy
def render(_x, _y, _text)
raise NotImplementedError
end
end # class RLTextStrategy
class TBTextStrategy < TextStrategy
def get_word_spacing
@word_space ||= glyph_metrics(@ctx.text_attrs.glyph_orientation_vertical, ' ')[1]
[0, @word_space + @ctx.text_attrs.word_spacing]
end
def get_letter_spacing(glyph)
gx, gy = glyph_metrics(@ctx.text_attrs.glyph_orientation_vertical, glyph)
[gx, gy + @ctx.text_attrs.letter_spacing]
end
def render(x, y, text)
x_rel_coords, y_rel_coords = text_rel_coords(text)
dx = x_rel_coords.max
dy = y_rel_coords.reduce(0) { |sum, a| sum + a }
# We're handling the anchoring.
@ctx.gc.push
@ctx.gc.text_anchor(Magick::StartAnchor)
if @ctx.text_attrs.text_anchor == :end
y -= dy
elsif @ctx.text_attrs.text_anchor == :middle
y -= dy / 2
end
# Align the first glyph such that its center
# is aligned on x and its top is aligned on y.
case @ctx.text_attrs.glyph_orientation_vertical
when 0
x -= x_rel_coords.max / 2
y += y_rel_coords[0]
when 90
x -= x_rel_coords.max / 2
when 180
x += x_rel_coords.max / 2
when 270
x += x_rel_coords.max / 2
y += y_rel_coords.shift
y_rel_coords << 0 # since we used an element we need to add a dummy
end
x -= shift_baseline(@ctx.text_attrs.glyph_orientation_vertical, text[0, 1])
first_word = true
text.split(::Magick::RVG::WORD_SEP).each do |word|
unless first_word
y += y_rel_coords.shift
x_rel_coords.shift
end
first_word = false
word.split('').each do |glyph|
case @ctx.text_attrs.glyph_orientation_vertical.to_i
when 0, 90, 270
x_shift = (dx - x_rel_coords.shift) / 2
when 180
x_shift = -(dx - x_rel_coords.shift) / 2
end
render_glyph(@ctx.text_attrs.glyph_orientation_vertical, x + x_shift, y, glyph)
y += y_rel_coords.shift
end
end
@ctx.gc.pop
[0, dy]
end
end # class TBTextStrategy
# Handle "easy" text
class DefaultTextStrategy < TextStrategy
def render(x, y, text)
@ctx.gc.text(x, y, enquote(text))
tm = @ctx.shadow.get_type_metrics(text)
dx = case @ctx.text_attrs.text_anchor
when :start
tm.width
when :middle
tm.width / 2
when :end
0
end
[dx, 0]
end
end # class NormalTextStrategy
end # class Utility
end # class RVG
end # module Magick
module Magick
class RVG
class Utility
class TextAttributes
WRITING_MODE = %w[lr-tb lr rl-tb rl tb-rl tb]
def initialize
@affine = []
@affine << Magick::AffineMatrix.new(1, 0, 0, 1, 0, 0)
@baseline_shift = []
@baseline_shift << :baseline
@glyph_orientation_horizontal = []
@glyph_orientation_horizontal << 0
@glyph_orientation_vertical = []
@glyph_orientation_vertical << 90
@letter_spacing = []
@letter_spacing << 0
@text_anchor = []
@text_anchor << :start
@word_spacing = []
@word_spacing << 0
@writing_mode = []
@writing_mode << 'lr-tb'
end
def push
@affine.push(@affine.last.dup)
@baseline_shift.push(@baseline_shift.last)
@text_anchor.push(@text_anchor.last)
@writing_mode.push(@writing_mode.last.dup)
@glyph_orientation_vertical.push(@glyph_orientation_vertical.last)
@glyph_orientation_horizontal.push(@glyph_orientation_horizontal.last)
@letter_spacing.push(@letter_spacing.last)
@word_spacing.push(@word_spacing.last)
end
def pop
@affine.pop
@baseline_shift.pop
@text_anchor.pop
@writing_mode.pop
@glyph_orientation_vertical.pop
@glyph_orientation_horizontal.pop
@letter_spacing.pop
@word_spacing.pop
end
def set_affine(sx, rx, ry, sy, tx, ty)
@affine[-1].sx = sx
@affine[-1].rx = rx
@affine[-1].ry = ry
@affine[-1].sy = sy
@affine[-1].tx = tx
@affine[-1].ty = ty
end
def affine
@affine[-1]
end
def baseline_shift
@baseline_shift[-1]
end
def baseline_shift=(value)
@baseline_shift[-1] = value
end
def text_anchor
@text_anchor[-1]
end
def text_anchor=(anchor)
@text_anchor[-1] = anchor
end
def glyph_orientation_vertical
@glyph_orientation_vertical[-1]
end
def glyph_orientation_vertical=(angle)
@glyph_orientation_vertical[-1] = angle
end
def glyph_orientation_horizontal
@glyph_orientation_horizontal[-1]
end
def glyph_orientation_horizontal=(angle)
@glyph_orientation_horizontal[-1] = angle
end
def letter_spacing
@letter_spacing[-1]
end
def letter_spacing=(value)
@letter_spacing[-1] = value
end
def non_default?
@baseline_shift[-1] != :baseline || @letter_spacing[-1] != 0 ||
@word_spacing[-1] != 0 || @writing_mode[-1][/\Alr/].nil? ||
@glyph_orientation_horizontal[-1] != 0
end
def word_spacing
@word_spacing[-1]
end
def word_spacing=(value)
@word_spacing[-1] = value
end
def writing_mode
@writing_mode[-1]
end
def writing_mode=(mode)
@writing_mode[-1] = WRITING_MODE.include?(mode) ? mode : 'lr-tb'
end
end # class TextAttributes
class GraphicContext
FONT_STRETCH = {
normal: Magick::NormalStretch,
ultra_condensed: Magick::UltraCondensedStretch,
extra_condensed: Magick::ExtraCondensedStretch,
condensed: Magick::CondensedStretch,
semi_condensed: Magick::SemiCondensedStretch,
semi_expanded: Magick::SemiExpandedStretch,
expanded: Magick::ExpandedStretch,
extra_expanded: Magick::ExtraExpandedStretch,
ultra_expanded: Magick::UltraExpandedStretch
}
FONT_STYLE = {
normal: Magick::NormalStyle,
italic: Magick::ItalicStyle,
oblique: Magick::ObliqueStyle
}
FONT_WEIGHT = {
'normal' => Magick::NormalWeight,
'bold' => Magick::BoldWeight,
'bolder' => Magick::BolderWeight,
'lighter' => Magick::LighterWeight
}
TEXT_ANCHOR = {
start: Magick::StartAnchor,
middle: Magick::MiddleAnchor,
end: Magick::EndAnchor
}
ANCHOR_TO_ALIGN = {
start: Magick::LeftAlign,
middle: Magick::CenterAlign,
end: Magick::RightAlign
}
TEXT_DECORATION = {
none: Magick::NoDecoration,
underline: Magick::UnderlineDecoration,
overline: Magick::OverlineDecoration,
line_through: Magick::LineThroughDecoration
}
TEXT_STRATEGIES = {
'lr-tb' => LRTextStrategy,
'lr' => LRTextStrategy,
'rt-tb' => RLTextStrategy,
'rl' => RLTextStrategy,
'tb-rl' => TBTextStrategy,
'tb' => TBTextStrategy
}
def self.degrees_to_radians(deg)
Math::PI * (deg % 360.0) / 180.0
end
private
def init_matrix
@rx = @ry = 0
@sx = @sy = 1
@tx = @ty = 0
end
def concat_matrix
curr = @text_attrs.affine
sx = curr.sx * @sx + curr.ry * @rx
rx = curr.rx * @sx + curr.sy * @rx
ry = curr.sx * @ry + curr.ry * @sy
sy = curr.rx * @ry + curr.sy * @sy
tx = curr.sx * @tx + curr.ry * @ty + curr.tx
ty = curr.rx * @tx + curr.sy * @ty + curr.ty
@text_attrs.set_affine(sx, rx, ry, sy, tx, ty)
init_matrix
end
public
attr_reader :gc, :text_attrs
def initialize
@gc = Magick::Draw.new
@shadow = []
@shadow << Magick::Draw.new
@text_attrs = TextAttributes.new
init_matrix
end
def method_missing(meth_id, *args, &block)
@gc.__send__(meth_id, *args, &block)
end
def affine(sx, rx, ry, sy, tx, ty)
sx, rx, ry, sy, tx, ty = Magick::RVG.convert_to_float(sx, rx, ry, sy, tx, ty)
@gc.affine(sx, rx, ry, sy, tx, ty)
@text_attrs.set_affine(sx, rx, ry, sy, tx, ty)
nil
end
def baseline_shift(value)
@text_attrs.baseline_shift = case value
when 'baseline', 'sub', 'super'
value.to_sym
when /[-+]?\d+%/, Numeric
value
else
:baseline
end
nil
end
def font(name)
@gc.font(name)
@shadow[-1].font = name
nil
end
def font_family(name)
@gc.font_family(name)
@shadow[-1].font_family = name
nil
end
def font_size(points)
@gc.font_size(points)
@shadow[-1].pointsize = points
nil
end
def font_stretch(stretch)
stretch = FONT_STRETCH.fetch(stretch.to_sym, Magick::NormalStretch)
@gc.font_stretch(stretch)
@shadow[-1].font_stretch = stretch
nil
end
def font_style(style)
style = FONT_STYLE.fetch(style.to_sym, Magick::NormalStyle)
@gc.font_style(style)
@shadow[-1].font_style = style
nil
end
def font_weight(weight)
# If the arg is not in the hash use it directly. Handles numeric values.
weight = FONT_WEIGHT.fetch(weight) { |key| key }
@gc.font_weight(weight)
@shadow[-1].font_weight = weight
nil
end
def glyph_orientation_horizontal(deg)
deg = Magick::RVG.convert_one_to_float(deg)
@text_attrs.glyph_orientation_horizontal = (deg % 360) / 90 * 90
nil
end
def glyph_orientation_vertical(deg)
deg = Magick::RVG.convert_one_to_float(deg)
@text_attrs.glyph_orientation_vertical = (deg % 360) / 90 * 90
nil
end
def inspect
@gc.inspect
end
def letter_spacing(value)
@text_attrs.letter_spacing = Magick::RVG.convert_one_to_float(value)
nil
end
def push
@gc.push
@shadow.push(@shadow.last.dup)
@text_attrs.push
nil
end
def pop
@gc.pop
@shadow.pop
@text_attrs.pop
nil
end
def rotate(degrees)
degrees = Magick::RVG.convert_one_to_float(degrees)
@gc.rotate(degrees)
@sx = Math.cos(GraphicContext.degrees_to_radians(degrees))
@rx = Math.sin(GraphicContext.degrees_to_radians(degrees))
@ry = -Math.sin(GraphicContext.degrees_to_radians(degrees))
@sy = Math.cos(GraphicContext.degrees_to_radians(degrees))
concat_matrix
nil
end
def scale(sx, sy)
sx, sy = Magick::RVG.convert_to_float(sx, sy)
@gc.scale(sx, sy)
@sx = sx
@sy = sy
concat_matrix
nil
end
def shadow
@shadow.last
end
def skewX(degrees)
degrees = Magick::RVG.convert_one_to_float(degrees)
@gc.skewX(degrees)
@ry = Math.tan(GraphicContext.degrees_to_radians(degrees))
concat_matrix
nil
end
def skewY(degrees)
degrees = Magick::RVG.convert_one_to_float(degrees)
@gc.skewY(degrees)
@rx = Math.tan(GraphicContext.degrees_to_radians(degrees))
concat_matrix
nil
end
def stroke_width(width)
width = Magick::RVG.convert_one_to_float(width)
@gc.stroke_width(width)
@shadow[-1].stroke_width = width
nil
end
def text(x, y, text)
return if text.length.zero?
text_renderer = if @text_attrs.non_default?
TEXT_STRATEGIES[@text_attrs.writing_mode].new(self)
else
DefaultTextStrategy.new(self)
end
text_renderer.render(x, y, text)
end
def text_anchor(anchor)
anchor = anchor.to_sym
anchor_enum = TEXT_ANCHOR.fetch(anchor, Magick::StartAnchor)
@gc.text_anchor(anchor_enum)
align = ANCHOR_TO_ALIGN.fetch(anchor, Magick::LeftAlign)
@shadow[-1].align = align
@text_attrs.text_anchor = anchor
nil
end
def text_decoration(decoration)
decoration = TEXT_DECORATION.fetch(decoration.to_sym, Magick::NoDecoration)
@gc.decorate(decoration)
@shadow[-1].decorate = decoration
nil
end
def translate(tx, ty)
tx, ty = Magick::RVG.convert_to_float(tx, ty)
@gc.translate(tx, ty)
@tx = tx
@ty = ty
concat_matrix
nil
end
def word_spacing(value)
@text_attrs.word_spacing = Magick::RVG.convert_one_to_float(value)
nil
end
def writing_mode(mode)
@text_attrs.writing_mode = mode
nil
end
end # class GraphicContext
end # class Utility
end # class RVG
end # module Magick