#!/usr/bin/env ruby # -*- coding: utf-8 -*- #========================== # tree_graph.rb #========================== # # Parses an element list into a (non-SVG) graphical tree. # # This file is part of RSyntaxTree, which is a ruby port of Andre Eisenbach's # excellent program phpSyntaxTree. # # Copyright (c) 2007-2021 Yoichiro Hasebe # Copyright (c) 2003-2004 Andre Eisenbach require 'graph' require 'rmagick' include Magick class TreeGraph < Graph def initialize(e_list, metrics, symmetrize, color, leafstyle, multibyte, fontstyle, font, font_it, font_bd, font_itbd, font_math, font_cjk, font_size, margin, transparent) # Store class-specific parameters @fontstyle = fontstyle @font = multibyte ? font_cjk : font @font_size = font_size @font_it = font_it @font_bd = font_bd @font_itbd = font_itbd @font_math = font_math @font_cjk = font_cjk @margin = margin @transparent = transparent super(e_list, metrics, symmetrize, color, leafstyle, multibyte, @font, @font_size) # Initialize the image and colors @gc = Draw.new @gc.font = @font @gc.pointsize(@font_size) end def destroy @im.destroy! end def draw parse_list @im = Image.new(@width, @height) if @transparent @im.matte_reset! end # @im.interlace = PlaneInterlace @im.interlace = LineInterlace @gc.draw(@im) end def save(filename) draw @im.write(filename) end # inspired by the implementation of Gruff # by Geoffrey Grosenbach def to_blob(fileformat='PNG') draw @im.trim! if @transparent @im.border!(@margin, @margin, "transparent") else @im.border!(@margin, @margin, "white") end @im.format = fileformat @im.interlace = PlaneInterlace return @im.to_blob end :private # Add the element into the tree (draw it) def draw_element(x, y, w, string, type) string = string.sub(/\^\z/){""} # Calculate element dimensions and position if (type == ETYPE_LEAF) and @leafstyle == "nothing" top = row2px(y - 1) + (@font_size * 1.5) else top = row2px(y) end left = x + @m[:b_side] right = left + w # Split the string into the main part and the # subscript part of the element (if any) parts = string.split("_", 2) if(parts.length > 1 ) main = parts[0].strip sub = parts[1].gsub(/_/, " ").strip else main = parts[0].strip sub = "" end if /\A\=(.+)\=\z/ =~ main main = $1 main_decoration = OverlineDecoration elsif /\A\-(.+)\-\z/ =~ main main = $1 main_decoration = UnderlineDecoration elsif /\A\~(.+)\~\z/ =~ main main = $1 main_decoration = LineThroughDecoration else main_decoration = NoDecoration end main_font = @font if /\A\*\*\*(.+)\*\*\*\z/ =~ main main = $1 if !@multibyte main_font = @font_itbd end elsif /\A\*\*(.+)\*\*\z/ =~ main main = $1 if !@multibyte main_font = @font_bd end elsif /\A\*(.+)\*\z/ =~ main main = $1 if !@multibyte main_font = @font_it end end if /\A#(.+)#\z/ =~ main main = $1 main_font = @font_math end # Calculate text size for the main and the # subscript part of the element main_width = 0 main_height = 0 main.split(/\\n/).each do |l| l_width = img_get_txt_width(l, main_font, @font_size) main_width = l_width if main_width < l_width main_height += img_get_txt_height(l, @font, @font_size) end if /\A\=(.+)\=\z/ =~ sub sub = $1 sub_decoration = OverlineDecoration elsif /\A\-(.+)\-\z/ =~ sub sub = $1 sub_decoration = UnderlineDecoration elsif /\A\~(.+)\~z/ =~ sub sub = $1 sub_decoration = LineThroughDecoration else sub_decoration = NoDecoration end sub_font = @font if /\A\*\*\*(.+)\*\*\*\z/ =~ sub sub = $1 if !@multibyte sub_font = @font_itbd end elsif /\A\*\*(.+)\*\*\z/ =~ sub sub = $1 if !@multibyte sub_font = @font_bd end elsif /\A\*(.+)\*\z/ =~ sub sub = $1 if !@multibyte sub_font = @font_it end end if /\A#(.+)#\z/ =~ sub sub = $1 sub_font = @font_math end if sub != "" sub_width = img_get_txt_width(sub.to_s, sub_font, @sub_size) sub_height = img_get_txt_height(sub.to_s, sub_font, @sub_size) else sub_width = 0 sub_height = 0 end # Center text in the element txt_pos = left + (right - left) / 2 # Select apropriate color if(type == ETYPE_LEAF) col = @col_leaf else col = @col_node end if(main[0].chr == "<" && main[-1].chr == ">") col = @col_trace end @gc.stroke("none") @gc.fill(col) # Draw main text @gc.pointsize(@font_size) @gc.kerning = 0 @gc.interline_spacing = 0 @gc.interword_spacing = 0 main_x = txt_pos - sub_width / 2 main_y = top + @e_height - @m[:e_padd] # @gc.interline_spacing = -(@main_height / 3) @gc.font(main_font) @gc.decorate(main_decoration) numlines = main.count("\\n") if numlines > 1 @height = @height + @main_height * numlines end @gc.text_align(CenterAlign) @gc.text(main_x.ceil, main_y.ceil, main.gsub("\\n", "\n")) # Draw subscript text if (sub != "" ) @gc.pointsize(@sub_size) sub_x = main_x + (main_width / 2) + (sub_width / 2) sub_y = top + main_height + sub_height / 4 @height += sub_height / 4 @gc.font(sub_font) @gc.decorate(sub_decoration) @gc.text_align(CenterAlign) @gc.text(sub_x.ceil, sub_y.ceil, " " + sub.gsub("\\n", "\n")) end end # Draw a line between child/parent elements def line_to_parent(fromX, fromY, fromW, toX, toW) if (fromY == 0 ) return end fromTop = row2px(fromY) fromLeft = (fromX + fromW / 2 + @m[:b_side]) toBot = (row2px(fromY - 1 ) + @e_height) toLeft = (toX + toW / 2 + @m[:b_side]) @gc.fill("none") @gc.stroke @col_line @gc.stroke_width 1 * FONT_SCALING @gc.line(fromLeft.ceil, fromTop.ceil, toLeft.ceil, toBot.ceil) end # Draw a triangle between child/parent elements def triangle_to_parent(fromX, fromY, fromW, textW, symmetrize = true) if (fromY == 0) return end toX = fromX fromCenter = (fromX + fromW / 2 + @m[:b_side]) fromTop = row2px(fromY).ceil fromLeft1 = (fromCenter + textW / 2).ceil fromLeft2 = (fromCenter - textW / 2).ceil toBot = (row2px(fromY - 1) + @e_height) if symmetrize toLeft = (toX + textW / 2 + @m[:b_side]) else toLeft = (toX + textW / 2 + @m[:b_side] * 3) end @gc.fill("none") @gc.stroke @col_line @gc.stroke_width 1 * 2 @gc.line(fromLeft1, fromTop, toLeft, toBot) @gc.line(fromLeft2, fromTop, toLeft, toBot) @gc.line(fromLeft1, fromTop, fromLeft2, fromTop) end # If a node element text is wider than the sum of it's # child elements, then the child elements need to # be resized to even out the space. This function # recurses down the a child tree and sizes the # children appropriately. def fix_child_size(id, current, target) children = @e_list.get_children(id) @e_list.set_element_width(id, target) if(children.length > 0 ) delta = target - current target_delta = delta / children.length children.each do |child| child_width = @e_list.get_element_width(child) fix_child_size(child, child_width, child_width + target_delta) end end end def img_get_txt_width(text, font, font_size, multiline = true) parts = text.split("_", 2) main_before = parts[0].strip sub = parts[1] main = get_txt_only(main_before) main_metrics = img_get_txt_metrics(main, font, font_size, multiline) width = main_metrics.width if sub sub_metrics = img_get_txt_metrics(sub.strip, font, font_size * SUBSCRIPT_CONST, multiline) width += sub_metrics.width end return width end end