# $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.collect(&: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, 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.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 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.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 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.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 = 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.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 = 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