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