Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

741 lines (614 sloc) 27.469 kb
# $Id: misc.rb,v 1.15 2009/02/28 23:52:27 rmagick 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 = case
when NilClass === ivalue, Symbol === ivalue, Float === ivalue,
Fixnum === ivalue, FalseClass === ivalue, TrueClass === ivalue
ivalue
when ivalue.respond_to?(:deep_copy)
ivalue.deep_copy(h)
when ivalue.respond_to?(:dup)
ivalue.dup
else
ivalue
end
copy.instance_variable_set(ivar, cvalue)
end
copy.freeze if frozen?
end
return 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.collect {|a| a.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.collect { |a| (allow_nil && a.nil?) ? a : Float(a) }
rescue ArgumentError, TypeError
raise ArgumentError, self.fmsg(*args)
end
return 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
return 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)
if text.length > 2 && /\A(?:\"[^\"]+\"|\'[^\']+\'|\{[^\}]+\})\z/.match(text)
return text
elsif !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 }
return '{' + 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 == 0 || 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)
if glyph_orientation == 0 || glyph_orientation == 180
x = glyph_dimensions.width
else
x = 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 = $1 == '-' ? -1.0 : 1.0
x = (m * x * $1.to_f / 100.0)
else
x = -@ctx.text_attrs.baseline_shift
end
return x
end
def render_glyph(glyph_orientation, x, y, glyph)
if glyph_orientation == 0
@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.inject(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 0
;
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|
unless first_word
x += x_rel_coords.shift
end
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.inject(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
public
WRITING_MODE = %w{lr-tb lr rl-tb rl tb-rl tb}
def initialize()
@affine = Array.new
@affine << Magick::AffineMatrix.new(1, 0, 0, 1, 0, 0)
@baseline_shift = Array.new
@baseline_shift << :baseline
@glyph_orientation_horizontal = Array.new
@glyph_orientation_horizontal << 0
@glyph_orientation_vertical = Array.new
@glyph_orientation_vertical << 90
@letter_spacing = Array.new
@letter_spacing << 0
@text_anchor = Array.new
@text_anchor << :start
@word_spacing = Array.new
@word_spacing << 0
@writing_mode = Array.new
@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 GraphicContext.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 = Array.new
@shadow << Magick::Draw.new
@text_attrs = TextAttributes.new
init_matrix()
end
def method_missing(methID, *args, &block)
@gc.__send__(methID, *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.intern
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.intern, Magick::NormalStretch)
@gc.font_stretch(stretch)
@shadow[-1].font_stretch = stretch
nil
end
def font_style(style)
style = FONT_STYLE.fetch(style.intern, 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, @sy = sx, 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 == 0
if @text_attrs.non_default?
text_renderer = TEXT_STRATEGIES[@text_attrs.writing_mode].new(self)
else
text_renderer = DefaultTextStrategy.new(self)
end
return text_renderer.render(x, y, text)
end
def text_anchor(anchor)
anchor = anchor.intern
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.intern, 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, @ty = tx, 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
Jump to Line
Something went wrong with that request. Please try again.