偶々 Clojure で Atom を、Python で SVG を生成する機會が有った。どちらも XML だ。Python には標準でxml.etree.ElementTree
と云ふ class が含まれてゐて、これで XML を讀み書き出來る。
(require '[clojure.xml :as xml]) (->> {:tag :svg :attrs {:xmlns "http://www.w3.org/2000/svg" :height "148mm" :width "210mm"} :content [{:tag :g :content [{:tag :line :attrs {:stroke "black" :x1 "5mm" :x2 "200mm" :y1 "4mm" :y2 "144mm"}} {:tag :text :attrs {:fill "black" :font-family "sans-serif" :font-size "12pt" :x "25mm" :y (str (+ 25 (* 12 0.353)) "mm")} :content ["日本語"]}]}]} xml/emit with-out-str)
<?xml version='1.0' encoding='UTF-8'?> <svg xmlns="http://www.w3.org/2000/svg" height='148mm' width='210mm'> <g> <line stroke='black' x1='5mm' x2='200mm' y1='4mm' y2='144mm'/> <text fill='black' font-family='sans-serif' font-size='12pt' x='25mm' y='29.236mm'> 日本語 </text> </g> </svg>
と map を作ればよい。map は Clojure の得意とする data であるし、map の階層は XML のものと等しい。改行と escape の問題は有る。escape は自前で出來るし、改行に就いてはWhy clojure.xml/emit prints new lines around string contents inside tags? - Stack Overflowの如く patch を当てるかclojure.data.xml
を使ふ事に成る。
これが Python では、
import xml.etree.ElementTree as ET svg = ET.Element( "svg", { "xmlns": "http://www.w3.org/2000/svg", "height": "148mm", "width": "210mm", }, ) g = ET.SubElement(svg, "g") ET.SubElement( g, "line", { "stroke": "black", "x1": "5mm", "x2": "200mm", "y1": "4mm", "y2": "144mm", }, ) text = ET.SubElement( g, "text", { "fill": "black", "font-family": "sans-serif", "font-size": "12pt", "x": "25mm", "y": f"{25 + 12 * 0.353}mm", }, ) text.text = "日本語" ET.tostring(svg).decode("utf-8")
<svg xmlns="http://www.w3.org/2000/svg" height="148mm" width="210mm"><g><line stroke="black" x1="5mm" x2="200mm" y1="4mm" y2="144mm" /><text fill="black" font-family="sans-serif" font-size="12pt" x="25mm" y="29.236mm">日本語</text></g></svg>
何だこれ。見よこれが OOP だよ。いや Groovy のgroovy.xml.MarkupBuilder
も有るが…。
JavaScript も似た樣なものだ。
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("height", "148mm"); svg.setAttribute("width", "210mm"); const g = document.createElement("g"); svg.appendChild(g); const line = document.createElement("line"); line.setAttribute("stroke", "black"); line.setAttribute("x1", "5mm"); line.setAttribute("x2", "200mm"); line.setAttribute("y1", "4mm"); line.setAttribute("y2", "144mm"); g.appendChild(line); const text = document.createElement("text"); text.setAttribute("fill", "black"); text.setAttribute("font-family", "sans-serif"); text.setAttribute("font-size", "12pt"); text.setAttribute("x", "25mm"); text.setAttribute("y", `${25 + 12 * 0.353}mm`); text.textContent = "日本語"; g.appendChild(text); svg.outerHTML;
<svg height="148mm" width="210mm"><g><line stroke="black" x1="5mm" x2="200mm" y1="4mm" y2="144mm"></line><text fill="black" font-family="sans-serif" font-size="12pt" x="25mm" y="29.236mm">日本語</text></g></svg>
酷い。これが素で書き下せて嫌に成る。Ruby の REXML も殘念だがこれの類いである。
require 'rexml/document' REXML::Document.new.tap do |doc| doc << REXML::XMLDecl.new('1.0', 'UTF-8') doc << REXML::Element.new('svg').tap do |svg| svg.add_attributes( 'xmlns' => 'http://www.w3.org/2000/svg', 'height' => '148mm', 'width' => '210mm' ) svg << REXML::Element.new('g').tap do |g| g << REXML::Element.new('line').tap do |line| line.add_attributes( 'stroke' => 'black', 'x1' => '5mm', 'x2' => '200mm', 'y1' => '4mm', 'y2' => '144mm' ) end g << REXML::Element.new('text').tap do |text| text.add_attributes( 'fill' => 'black', 'font-family' => 'sans-serif', 'font-size' => '12pt', 'x' => '25mm', 'y' => "#{25 + 12 * 0.353}mm", ) text.text = '日本語' end end end end.to_s
<?xml version='1.0' encoding='UTF-8'?><svg height='148mm' width='210mm' xmlns='http://www.w3.org/2000/svg'><g><line stroke='black' x1='5mm' x2='200mm' y1='4mm' y2='144mm'/><text fill='black' font-family='sans-serif' font-size='12pt' x='25mm' y='29.236mm'>日本語</text></g></svg>
Object#tap
でごまかした。ごまかせるのは好い事だ。
Goovy だとこう書け、迚も好い。
import groovy.xml.MarkupBuilder def writer = new StringWriter() new MarkupBuilder(writer).svg(xmlns:'http://www.w3.org/2000/svg', width:'148mm', height: '210mm') { g { line(stroke:'black', x1:'5mm', x2:'200mm', y1:'4mm', y2:'144mm') text(fill:'black', 'font-family':'sans-serif', 'font-size':'12pt', x:'25mm', y: "${25 + 12 * 0.353}mm", '日本語') } } writer.toString()
<svg xmlns='http://www.w3.org/2000/svg' width='148mm' height='210mm'> <g> <line stroke='black' x1='5mm' x2='200mm' y1='4mm' y2='144mm' /> <text fill='black' font-family='sans-serif' font-size='12pt' x='25mm' y='29.236mm'>日本語</text> </g> </svg>
Clojure と同じく函數型である Erlang の xmerl を Elixir から使ふと…、
{:ok, pid} = StringIO.open("") IO.puts( pid, :xmerl.export_simple( [ {:xmlElement, :svg, :svg, [], {:xmlNamespace, :"http://www.w3.org/2000/svg", []}, [], 1, [ {:xmlAttribute, :xmlns, [], [], [], [svg: 1], 1, [], 'http://www.w3.org/2000/svg', false}, {:xmlAttribute, :height, [], [], [], [svg: 1], 2, [], '148mm', false}, {:xmlAttribute, :width, [], [], [], [svg: 1], 3, [], '210mm', false} ], [ {:xmlElement, :g, :g, [], {:xmlNamespace, :"http://www.w3.org/2000/svg", []}, [svg: 1], 1, [], [ {:xmlElement, :line, :line, [], {:xmlNamespace, :"http://www.w3.org/2000/svg", []}, [g: 1, svg: 1], 1, [ {:xmlAttribute, :stroke, [], [], [], [line: 1, g: 1, svg: 1], 1, [], 'black', false}, {:xmlAttribute, :x1, [], [], [], [line: 1, g: 1, svg: 1], 2, [], '5mm', false}, {:xmlAttribute, :x2, [], [], [], [line: 1, g: 1, svg: 1], 3, [], '200mm', false}, {:xmlAttribute, :y1, [], [], [], [line: 1, g: 1, svg: 1], 4, [], '4mm', false}, {:xmlAttribute, :y2, [], [], [], [line: 1, g: 1, svg: 1], 5, [], '144mm', false} ], [], [], '', :undeclared}, {:xmlElement, :text, :text, [], {:xmlNamespace, :"http://www.w3.org/2000/svg", []}, [g: 1, svg: 1], 2, [ {:xmlAttribute, :fill, [], [], [], [text: 2, g: 1, svg: 1], 1, [], 'black', false}, {:xmlAttribute, :"font-family", [], [], [], [text: 2, g: 1, svg: 1], 2, [], 'sans-serif', false}, {:xmlAttribute, :"font-size", [], [], [], [text: 2, g: 1, svg: 1], 3, [], '12pt', false}, {:xmlAttribute, :x, [], [], [], [text: 2, g: 1, svg: 1], 4, [], '25mm', false}, {:xmlAttribute, :y, [], [], [], [text: 2, g: 1, svg: 1], 5, [], String.to_charlist("#{25 + 12 * 0.353}mm"), false} ], [ {:xmlText, [text: 2, g: 1, svg: 1], 1, [], [26085, 26412, 35486], :text} ], [], '', :undeclared} ], [], '', :undeclared} ], [], '', :undeclared} ], :xmerl_xml ) ) {:ok, {_, contents}} = StringIO.close(pid) contents
<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg" height="148mm" width="210mm"><g><line stroke="black" x1="5mm" x2="200mm" y1="4mm" y2="144mm"/><text fill="black" font-family="sans-serif" font-size="12pt" x="25mm" y="29.236mm">日本語</text></g></svg>
これは Erlang の record が惡過ぎて書けない。
これらは全て標準 library だ。標準 library は優れたものである必要が有る。でなければ含まれてゐないはうが好い。
さて PyPI から適切なものを見繕って來れば好いのだらうが、手元で濟ませてしまおう。
from contextlib import contextmanager from functools import partial import typing as t import xml.etree.ElementTree as ET @contextmanager def e( tag: str, attrib: t.Dict[str, str] = {}, text: str = "", parent: ET.Element = None ) -> None: if parent is not None: element = ET.SubElement(parent, tag, attrib) else: element = ET.Element(tag, attrib) if text != "": element.text = text yield partial(e, parent=element)
これでこう成る。
svg = ET.Element( "svg", { "xmlns": "http://www.w3.org/2000/svg", "height": "148mm", "width": "210mm", }, ) with e("g", {}, parent=svg) as _e: with _e( "line", { "stroke": "black", "x1": "5mm", "x2": "200mm", "y1": "4mm", "y2": "144mm", }, ): pass with _e( "text", { "fill": "black", "font-family": "sans-serif", "font-size": "12pt", "x": "25mm", "y": f"{25 + 12 * 0.353}mm", }, "日本語", ): pass ET.tostring(svg).decode("utf-8")
<svg xmlns="http://www.w3.org/2000/svg" height="148mm" width="210mm"><g /><g><line stroke="black" x1="5mm" x2="200mm" y1="4mm" y2="144mm" /><text fill="black" font-family="sans-serif" font-size="12pt" x="25mm" y="29.236mm">日本語</text></g></svg>
XML での表現とそれを生成する code での表現とが一致してゐると好い。