1# -*- coding: utf-8 -*-2# Copyright 2011 Takeshi KOMIYA3#4# Licensed under the Apache License, Version 2.0 (the "License");5# you may not use this file except in compliance with the License.6# You may obtain a copy of the License at7#8# http://www.apache.org/licenses/LICENSE-2.09#10# Unless required by applicable law or agreed to in writing, software11# distributed under the License is distributed on an "AS IS" BASIS,12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13# See the License for the specific language governing permissions and14# limitations under the License.1516import codecs17import importlib18import os19import re20import sys21import traceback22from optparse import SUPPRESS_HELP, OptionParser2324from blockdiag import imagedraw, plugins25from blockdiag.utils import images26from blockdiag.utils.config import ConfigParser27from blockdiag.utils.fontmap import FontMap, parse_fontpath28from blockdiag.utils.logging import error, warning293031class Application(object):32 module = None33 options = None3435 def __init__(self):36 self.cleanup_handlers = []3738 def __enter__(self):39 self.setup()40 return self4142 def __exit__(self, *args):43 self.cleanup()4445 def register_cleanup_handler(self, handler):46 self.cleanup_handlers.append(handler)4748 def run(self, args):49 try:50 self.parse_options(args)51 self.create_fontmap()52 self.setup()5354 parsed = self.parse_diagram()55 return self.build_diagram(parsed)56 except SystemExit as e:57 return e.code58 except UnicodeEncodeError:59 error("UnicodeEncodeError caught (check your font settings)")60 return -161 except Exception as e:62 if self.options and self.options.debug:63 traceback.print_exc()64 else:65 error("%s" % e)66 return -167 finally:68 self.cleanup()6970 def parse_options(self, args):71 self.options = Options(self.module).parse(args)7273 def create_fontmap(self):74 self.fontmap = create_fontmap(self.options)7576 def setup(self):77 images.setup(self)78 plugins.setup(self)7980 def parse_diagram(self):81 if self.options.input == '-':82 self.code = sys.stdin.read()83 if self.code.startswith('\ufeff'): # strip BOM84 self.code = self.code[1:]85 else:86 fp = codecs.open(self.options.input, 'r', 'utf-8-sig')87 self.code = fp.read()8889 return self.module.parser.parse_string(self.code)9091 def build_diagram(self, tree):92 ScreenNodeBuilder = self.module.builder.ScreenNodeBuilder93 try:94 diagram = ScreenNodeBuilder.build(tree, self.options)95 except Exception:96 diagram = ScreenNodeBuilder.build(tree) # old interface9798 DiagramDraw = self.module.drawer.DiagramDraw99 drawer = DiagramDraw(self.options.type, diagram,100 self.options.output, fontmap=self.fontmap,101 code=self.code, antialias=self.options.antialias,102 nodoctype=self.options.nodoctype,103 transparency=self.options.transparency)104 drawer.draw()105106 if self.options.size:107 output = drawer.save(size=self.options.size)108 else:109 output = drawer.save()110111 if self.options.output is None and output is not None:112 if isinstance(output, (bytes, bytearray)):113 sys.stdout.buffer.write(output)114 else:115 sys.stdout.write(output)116117 return 0118119 def cleanup(self):120 for handler in self.cleanup_handlers[:]:121 try:122 handler()123 except Exception as exc:124 error("%s" % exc)125 finally:126 self.cleanup_handlers.remove(handler)127128129class Options(object):130 def __init__(self, module):131 self.parser = None132 self.args = None133 self.options = None134 self.module = module135 self.build_parser()136137 def parse(self, args):138 self.options, self.args = self.parser.parse_args(args)139 self.validate()140 self.read_configfile()141142 return self.options143144 def build_parser(self):145 version = "%%prog %s" % self.module.__version__146 usage = "usage: %prog [options] infile"147 self.parser = p = OptionParser(usage=usage, version=version)148 p.add_option('-a', '--antialias', action='store_true',149 help='Pass diagram image to anti-alias filter')150 p.add_option('-c', '--config',151 help='read configurations from FILE', metavar='FILE')152 p.add_option('--debug', action='store_true',153 help='Enable debug mode')154 p.add_option('--module', dest='module', help='module name')155 p.add_option('-o', dest='output',156 help='write diagram to FILE', metavar='FILE')157 p.add_option('-f', '--font', default=[], action='append',158 help='use FONT to draw diagram', metavar='FONT')159 p.add_option('--fontmap',160 help='use FONTMAP file to draw diagram', metavar='FONT')161 p.add_option('--ignore-pil', dest='ignore_pil',162 default=False, action='store_true', help=SUPPRESS_HELP)163 p.add_option('--no-transparency', dest='transparency',164 default=True, action='store_false',165 help='do not make transparent background of diagram ' +166 '(PNG only)')167 p.add_option('--size',168 help='Size of diagram (ex. 320x240)')169 p.add_option('-T', dest='type', default='PNG',170 help='Output diagram as TYPE format')171 p.add_option('--nodoctype', action='store_true',172 help='Do not output doctype definition tags (SVG only)')173174 return p175176 def validate(self):177 if len(self.args) == 0:178 self.parser.print_help()179 sys.exit(0)180181 self.options.input = self.args.pop(0)182 if self.options.output:183 pass184 elif self.options.output == '-':185 self.options.output = None186 else:187 if self.options.input == '-':188 self.options.output = None189 else:190 basename = os.path.splitext(self.options.input)[0]191 ext = '.%s' % self.options.type.lower()192 self.options.output = basename + ext193194 self.options.type = self.options.type.upper()195 try:196 imagedraw.create(self.options.type, None, debug=self.options.debug)197 except Exception:198 msg = "unknown format: %s" % self.options.type199 raise RuntimeError(msg)200201 if self.options.size:202 matched = re.match(r'^(\d+)x(\d+)$', self.options.size)203 if matched:204 self.options.size = [int(n) for n in matched.groups()]205 else:206 msg = "--size option must be formatted as WIDTHxHEIGHT."207 raise RuntimeError(msg)208209 if self.options.type == 'PDF':210 try:211 import reportlab.pdfgen.canvas # noqa: F401212 except ImportError:213 msg = "could not output PDF format; Install reportlab."214 raise RuntimeError(msg)215216 if self.options.ignore_pil:217 warning("--ignore-pil option is deprecated "218 "(detect automatically).")219220 if self.options.nodoctype and self.options.type != 'SVG':221 msg = "--nodoctype option work in SVG images."222 raise RuntimeError(msg)223224 if self.options.transparency is False and self.options.type != 'PNG':225 msg = "--no-transparency option work in PNG images."226 raise RuntimeError(msg)227228 if self.options.config and not os.path.isfile(self.options.config):229 msg = "config file is not found: %s" % self.options.config230 raise RuntimeError(msg)231232 if self.options.fontmap and not os.path.isfile(self.options.fontmap):233 msg = "fontmap file is not found: %s" % self.options.fontmap234 raise RuntimeError(msg)235236 if self.options.module:237 try:238 module = importlib.import_module(self.options.module)239 importlib.import_module(f"{self.options.module}.builder")240 importlib.import_module(f"{self.options.module}.parser")241 importlib.import_module(f"{self.options.module}.drawer")242 self.options.module = module243 except ImportError:244 raise RuntimeError(f"could not load module; Make sure {self.options.module} is available.")245246 def read_configfile(self):247 if self.options.config:248 configpath = self.options.config249 elif os.environ.get('HOME'):250 configpath = '%s/.blockdiagrc' % os.environ.get('HOME')251 elif os.environ.get('USERPROFILE'):252 configpath = '%s/.blockdiagrc' % os.environ.get('USERPROFILE')253 else:254 configpath = ''255256 appname = self.module.__name__257 if os.path.isfile(configpath):258 config = ConfigParser()259 config.read(configpath)260261 if config.has_option(appname, 'fontpath'):262 fontpath = config.get(appname, 'fontpath')263 self.options.font.append(fontpath)264265 if config.has_option(appname, 'fontmap'):266 if self.options.fontmap is None:267 self.options.fontmap = config.get(appname, 'fontmap')268269 if config.has_option(appname, 'antialias'):270 antialias = config.get(appname, 'antialias')271 if antialias.lower() == 'true':272 self.options.antialias = True273274 if self.options.fontmap is None:275 self.options.fontmap = configpath276277278def detectfont(options):279 import glob280 fontdirs = [281 '/usr/share/fonts',282 '/Library/Fonts',283 '/System/Library/Fonts',284 'c:/windows/fonts',285 '/usr/local/share/font-*',286 ]287 fontfiles = [288 'ipagp.ttf',289 'ipagp.otf',290 'VL-PGothic-Regular.ttf',291 'Hiragino Sans GB W3.otf',292 'AppleGothic.ttf',293 'msgothic.ttf',294 'msgoth04.ttf',295 'msgothic.ttc',296 ]297298 fontpath = None299 if options.font:300 for path in options.font:301 _path, _ = parse_fontpath(path)302 if os.path.isfile(_path):303 fontpath = path304 break305 else:306 msg = 'fontfile is not found: %s' % options.font307 raise RuntimeError(msg)308309 if fontpath is None:310 globber = (glob.glob(d) for d in fontdirs)311 for fontdir in sum(globber, []):312 for root, _, files in os.walk(fontdir):313 for font in fontfiles:314 if font in files:315 fontpath = os.path.join(root, font)316 break317318 return fontpath319320321def create_fontmap(options):322 fontmap = FontMap(options.fontmap)323 if fontmap.find().path is None or options.font:324 fontpath = detectfont(options)325 fontmap.set_default_font(fontpath)326327 return fontmap