Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

741 lines (614 sloc) 27.467 kB
# $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 = 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.