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 re1718from blockdiag.utils import XY, Box, Size192021def splitlabel(string):22 """Split text to lines as generator.23 Every line will be stripped.24 If text includes characters "\n", treat as line separator.25 """26 string = re.sub(r'^\s*', '', string)27 string = re.sub(r'\s*$', '', string)28 string = re.sub(r'\xa5', '\\\\', string)29 string = re.sub('(\\\\){2}', '\x00', string)30 string = re.sub('\\\\n', '\n', string)31 for line in string.splitlines():32 yield re.sub('\x00', '\\\\', line).strip()333435def splittext(metrics, text, bound, measure='width'):36 folded = []37 if text == '':38 folded.append(' ')3940 for i in range(len(text), 0, -1):41 textsize = metrics.textsize(text[0:i])4243 if getattr(textsize, measure) <= bound:44 folded.append(text[0:i])45 if text[i:]:46 folded += splittext(metrics, text[i:], bound, measure)47 break4849 return folded505152def truncate_text(metrics, text, bound, measure='width'):53 for i in range(len(text), 0, -1):54 textsize = metrics.textsize(text[0:i] + ' ...')5556 if getattr(textsize, measure) <= bound:57 return text[0:i] + ' ...'5859 return text606162def get(*args, **kwargs):63 if kwargs.get('orientation') == 'vertical':64 return VerticalTextFolder(*args, **kwargs)65 else:66 return HorizontalTextFolder(*args, **kwargs)676869class VerticalTextFolder(object):70 def __init__(self, drawer, box, string, font, **kwargs):71 self.drawer = drawer72 self.box = box73 self.string = string74 self.font = font75 self.scale = 176 self.halign = kwargs.get('halign', 'center')77 self.valign = kwargs.get('valign', 'center')78 self.padding = kwargs.get('padding', 8)79 self.line_spacing = kwargs.get('line_spacing', 2)8081 if kwargs.get('adjustBaseline'):82 self.adjustBaseline = True83 else:84 self.adjustBaseline = False8586 self._result = self._lines()8788 def textsize(self, text, scaled=False):89 if isinstance(text, str):90 size = [self.drawer.textlinesize(c, self.font) for c in text]91 width = max(s.width for s in size)92 height = (sum(s.height for s in size) +93 self.line_spacing * (len(text) - 1))9495 textsize = Size(width, height)96 else:97 if text:98 size = [self.textsize(s) for s in text]99 height = max(s.height for s in size)100 width = (sum(s.width for s in size) +101 self.line_spacing * (len(text) - 1))102103 textsize = Size(width, height)104 else:105 textsize = Size(0, 0)106107 if scaled:108 textsize = Size(textsize.width * self.scale,109 textsize.height * self.scale)110111 return textsize112113114 def lines(self):115 textsize = self.textsize(self._result, scaled=True)116117 dx, _ = self.box.get_padding_for(textsize, halign=self.halign,118 padding=self.padding)119120 width = self.box.width - dx + self.line_spacing121 base_xy = XY(self.box.x1, self.box.y1)122 for string in self._result:123 textsize = self.textsize(string, scaled=True)124 _, dy = self.box.get_padding_for(textsize, valign=self.valign,125 padding=self.line_spacing)126127 height = dy128 width -= textsize.width + self.line_spacing129 for char in string:130 charsize = self.textsize(char, scaled=True)131132 if self.adjustBaseline:133 draw_xy = base_xy.shift(width, height + charsize.height)134 else:135 draw_xy = base_xy.shift(width, height)136137 yield char, draw_xy138139 height += charsize.height + self.line_spacing140141142 def outlinebox(self):143 corners = []144 for string, xy in self.lines:145 textsize = self.textsize(string)146 width = textsize[0] * self.scale147 height = textsize[1] * self.scale148149 if self.adjustBaseline:150 xy = XY(xy.x, xy.y - textsize[1])151152 corners.append(xy)153 corners.append(XY(xy.x + width, xy.y + height))154155 if corners:156 box = Box(min(p.x for p in corners) - self.padding,157 min(p.y for p in corners) - self.line_spacing,158 max(p.x for p in corners) + self.padding,159 max(p.y for p in corners) + self.line_spacing)160 else:161 box = Box(self.box[0], self.box[1], self.box[0], self.box[1])162163 return box164165 def _lines(self):166 lines = []167 measure = 'height'168 maxwidth, maxheight = self.box.size169170 width = 0171 finished = False172 for line in splitlabel(self.string):173 for folded in splittext(self, line, maxheight, measure):174 textsize = self.textsize(folded)175176 if width + textsize.width + self.line_spacing < maxwidth:177 lines.append(folded)178 width += textsize.width + self.line_spacing179 elif len(lines) > 0:180 lines[-1] = truncate_text(self, lines[-1],181 maxheight, measure)182 finished = True183 break184185 if finished:186 break187188 return lines189190191class HorizontalTextFolder(object):192 def __init__(self, drawer, box, string, font, **kwargs):193 self.drawer = drawer194 self.box = box195 self.string = string196 self.font = font197 self.scale = 1198 self.halign = kwargs.get('halign', 'center')199 self.valign = kwargs.get('valign', 'center')200 self.padding = kwargs.get('padding', 8)201 self.line_spacing = kwargs.get('line_spacing', 2)202203 if kwargs.get('adjustBaseline'):204 self.adjustBaseline = True205 else:206 self.adjustBaseline = False207208 self._result = self._lines()209210 def textsize(self, text, scaled=False):211 if isinstance(text, str):212 textsize = self.drawer.textlinesize(text, self.font)213 else:214 if text:215 size = [self.textsize(s) for s in text]216 width = max(s.width for s in size)217 height = (sum(s.height for s in size) +218 self.line_spacing * (len(text) - 1))219220 textsize = Size(width, height)221 else:222 textsize = Size(0, 0)223224 if scaled:225 textsize = Size(textsize.width * self.scale,226 textsize.height * self.scale)227228 return textsize229230231 def lines(self):232 textsize = self.textsize(self._result, scaled=True)233234 _, dy = self.box.get_padding_for(textsize, valign=self.valign,235 padding=self.line_spacing)236237 height = dy238 base_xy = XY(self.box.x1, self.box.y1)239 for string in self._result:240 textsize = self.textsize(string, scaled=True)241 dx, _ = self.box.get_padding_for(textsize, halign=self.halign,242 padding=self.padding)243244 if self.adjustBaseline:245 draw_xy = base_xy.shift(dx, height + textsize.height)246 else:247 draw_xy = base_xy.shift(dx, height)248249 yield string, draw_xy250251 height += textsize.height + self.line_spacing252253254 def outlinebox(self):255 corners = []256 for string, xy in self.lines:257 textsize = self.textsize(string)258 width = textsize[0] * self.scale259 height = textsize[1] * self.scale260261 if self.adjustBaseline:262 xy = XY(xy.x, xy.y - textsize[1])263264 corners.append(xy)265 corners.append(XY(xy.x + width, xy.y + height))266267 if corners:268 box = Box(min(p.x for p in corners) - self.padding,269 min(p.y for p in corners) - self.line_spacing,270 max(p.x for p in corners) + self.padding,271 max(p.y for p in corners) + self.line_spacing)272 else:273 box = Box(self.box[0], self.box[1], self.box[0], self.box[1])274275 return box276277 def _lines(self):278 lines = []279 measure = 'width'280 maxwidth, maxheight = self.box.size281282 height = 0283 finished = False284 for line in splitlabel(self.string):285 for folded in splittext(self, line, maxwidth, measure):286 textsize = self.textsize(folded)287288 if height + textsize.height + self.line_spacing < maxheight:289 lines.append(folded)290 height += textsize.height + self.line_spacing291 else:292 if len(lines) > 0:293 lines[-1] = truncate_text(self, lines[-1],294 maxwidth, measure)295296 finished = True297 break298299 if finished:300 break301302 return lines