class Text2svg::Typography

Constants

IDEOGRAPHIC_SPACE
INTER_CHAR_SPACE_DIV
NEW_LINE
NOTDEF_GLYPH_ID
WHITESPACE

Public Class Methods

build(text, option) click to toggle source
# File lib/text2svg/typography.rb, line 17
def build(text, option)
  if Hash === option
    option = Option.from_hash(option)
  end
  content = path(text, option)
  svg = %(<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 #{content.width} #{content.height}">\n)
  svg << "<title>#{CGI.escapeHTML(text.to_s)}</title>\n"
  svg << content.data
  svg << "</svg>\n"
  Content.new(svg, content.width, content.height, content.notdef_indexes)
end
path(text, option) click to toggle source
# File lib/text2svg/typography.rb, line 29
def path(text, option)
  if Hash === option
    option = Option.from_hash(option)
  end
  text = String.try_convert(text)
  return Content.new('', 0, 0, []) if text.nil? || text.empty?

  option.encoding ||= Encoding::UTF_8
  option.text_align ||= :left
  text.force_encoding(option.encoding).encode!(Encoding::UTF_8)

  notdef_indexes = []
  unless option.font
    raise OptionError, 'should set `font\' option'
  end
  char_sizes = option.char_size.split(',').map(&:to_i)
  unless char_sizes.length == 4
    raise OptionError, 'char-size option should be four integer values'
  end
  FreeType::API::Font.open(File.expand_path(option.font)) do |f|
    f.set_char_size(*char_sizes)

    lines = []
    line = []
    lines << line
    min_hori_bearing_x_by_line = [0]

    space_width = f.glyph(' '.freeze).char_width
    text.each_char.with_index do |char, index|
      if NEW_LINE.match char
        min_hori_bearing_x_by_line[-1] = min_hori_bearing_x(line)
        min_hori_bearing_x_by_line << 0
        line = []
        lines << line
        next
      end

      glyph_id = f.char_index(char)
      glyph = if glyph_id == 0
        notdef_indexes << index
        f.notdef
      else
        f.glyph(char)
      end

      if glyph.outline.tags.empty?
        notdef_indexes << index
        glyph = f.notdef
      end

      glyph.bold if option.bold
      glyph.italic if option.italic

      metrics = FreeType::C::FT_Glyph_Metrics.new
      is_draw = if IDEOGRAPHIC_SPACE.match char
        metrics[:width] = space_width * 2r
        metrics[:height] = 0
        metrics[:horiBearingX] = space_width * 2r
        metrics[:horiBearingY] = 0
        metrics[:horiAdvance] = space_width * 2r
        metrics[:vertBearingX] = 0
        metrics[:vertBearingY] = 0
        metrics[:vertAdvance] = 0

        false
      elsif WHITESPACE.match char
        metrics[:width] = space_width
        metrics[:height] = 0
        metrics[:horiBearingX] = space_width
        metrics[:horiBearingY] = 0
        metrics[:horiAdvance] = space_width
        metrics[:vertBearingX] = 0
        metrics[:vertBearingY] = 0
        metrics[:vertAdvance] = 0

        false
      else
        FreeType::C::FT_Glyph_Metrics.members.each do |m|
          metrics[m] = glyph.metrics[m]
        end

        true
      end
      line << CharSet.new(char, metrics, is_draw, glyph.outline.svg_path_data)
    end

    min_hori_bearing_x_by_line[-1] = min_hori_bearing_x(line)
    inter_char_space = space_width / INTER_CHAR_SPACE_DIV
    min_hori_bearing_x_all = min_hori_bearing_x_by_line.min

    width_by_line = lines.map do |line|
      before_char = nil
      if line.empty?.!
        line.map { |cs|
          width = if cs.equal?(line.last)
            cs.metrics[:width] + cs.metrics[:horiBearingX]
          else
            cs.metrics[:horiAdvance]
          end
          w = width + f.kerning_unfitted(before_char, cs.char).x
          w.tap { before_char = cs.char }
        }.inject(:+) + (line.length - 1) * inter_char_space - min_hori_bearing_x_all
      else
        0
      end
    end
    max_width = width_by_line.max

    y = 0r
    output = ''
    output << %(<g #{option.attribute}>\n) if option.attribute

    lines.zip(width_by_line).each_with_index do |(line, line_width), index|
      x = 0r
      y += if index == 0
        f.face[:size][:metrics][:ascender] * option.scale
      else
        f.line_height * option.scale
      end
      before_char = nil

      case option.text_align.to_sym
      when :center
        x += (max_width - line_width) / 2r * option.scale
      when :right
        x += (max_width - line_width) * option.scale
      when :left
        # nothing
      else
        warn 'text_align must be left,right or center'
      end

      output << %!<g transform="matrix(1,0,0,1,0,#{y.round})">\n!

      x -= min_hori_bearing_x_all * option.scale
      sr = option.scale.rationalize
      line.each do |cs|
        x += f.kerning_unfitted(before_char, cs.char).x * option.scale
        if cs.draw?
          d = cs.outline2d.map do |cmd, *points|
            [
              cmd,
              *points.map { |i|
                i.rationalize * sr
              }.map do |i|
                i.denominator == 1 ? i.to_i : i.to_f
              end,
            ]
          end
          output << %!  <path transform="matrix(1,0,0,1,#{x.round},0)" d="#{d.join(' '.freeze)}"/>\n!
        end
        x += cs.metrics[:horiAdvance] * option.scale
        x += inter_char_space * option.scale if cs != line.last
        before_char = cs.char
      end
      output << "</g>\n".freeze
    end
    output << "</g>\n".freeze if option.attribute

    option_width = 0
    option_width += (space_width / 1.5 * option.scale) if option.italic
    Content.new(
      output,
      ((max_width + option_width) * option.scale).ceil,
      (y - f.face[:size][:metrics][:descender] * 1.2 * option.scale).ceil,
      notdef_indexes,
    )
  end
end

Private Class Methods

min_hori_bearing_x(line) click to toggle source
# File lib/text2svg/typography.rb, line 201
def min_hori_bearing_x(line)
  return 0 if line.empty?
  point = 0
  bearings = line.map do |cs|
    (cs.metrics[:horiBearingX] + point).tap do
      point += cs.metrics[:horiAdvance]
    end
  end
  bearings.min
end