From 6440bcb63935844edfe631b58040cbcce14013f3 Mon Sep 17 00:00:00 2001 From: Florian Festi Date: Mon, 20 Apr 2020 23:38:00 +0200 Subject: [PATCH] Get text working for both SVG and PS Move coordiate translation to to finish() method Use Latin1 encoding for text PS in output Add Boxes.set_font() to support basic font styles: serif, sans-serif and monospaced in normal, bold, italic and both bold and italic --- boxes/__init__.py | 49 +++++++------ boxes/drawing.py | 170 +++++++++++++++++++++++++++++++++----------- boxes/formats.py | 17 +---- scripts/boxesserver | 3 +- 4 files changed, 158 insertions(+), 81 deletions(-) diff --git a/boxes/__init__.py b/boxes/__init__.py index 0039f38..46668f6 100755 --- a/boxes/__init__.py +++ b/boxes/__init__.py @@ -291,6 +291,15 @@ class Boxes: """ self.ctx.set_source_rgb(*color) + def set_font(self, style, bold=False, italic=False): + """ + Set font style used + :param style: "serif", "sans-serif" or "monospaced" + :param bold: Use bold font + :param italic: Use italic font + """ + self.ctx.set_font(style, bold, italic) + def open(self): """ Prepare for rendering @@ -313,7 +322,7 @@ class Boxes: self.set_source_color(Color.BLACK) self.spacing = 2 * self.burn + 0.5 * self.thickness - self.ctx.select_font_face("sans-serif") + self.set_font("sans-serif") self._buildObjects() if self.reference and self.format != 'svg_Ponoko': self.move(10, 10, "up", before=True) @@ -1210,43 +1219,33 @@ class Boxes: :param align: (Default value = "") string with combinations of (top|middle|bottom) and (left|center|right) separated by a space """ - self.ctx.set_font_size(fontsize) self.moveTo(x, y, angle) text = text.split("\n") - width = lheight = 0.0 - for line in text: - (tx, ty, w, h, dx, dy) = self.ctx.text_extents(line) - lheight = max(lheight, h) - width = max(width, w) lines = len(text) - height = lines * lheight + (lines - 1) * 0.4 * lheight + height = lines * fontsize + (lines - 1) * 0.4 * fontsize align = align.split() + halign = "left" moves = { - "top": (0, -height), - "middle": (0, -0.5 * height), - "bottom": (0, 0), - "left": (0, 0), - "center": (-0.5 * width, 0), - "right": (-width, 0), + "top": -height, + "middle": -0.5 * height, + "bottom": 0, + "left": "start", + "center": "middle", + "right": "end", } for a in align: if a in moves: - self.moveTo(*moves[a]) + if isinstance(moves[a], str): + halign = moves[a] + else: + self.moveTo(0, moves[a]) else: raise ValueError("Unknown alignment: %s" % align) - self.ctx.stroke() - self.set_source_color(Color.WHITE) - self.ctx.rectangle(0, 0, width, height) - self.ctx.stroke() - self.set_source_color(color) - self.ctx.scale(1, -1) for line in reversed(text): - self.ctx.show_text(line) - self.moveTo(0, 1.4 * -lheight) - self.set_source_color(Color.BLACK) - self.ctx.set_font_size(10) + self.ctx.show_text(line, fs=fontsize, align=halign, rgb=color) + self.moveTo(0, 1.4 * fontsize) tx_sizes = { 1 : 0.61, diff --git a/boxes/drawing.py b/boxes/drawing.py index 280e301..d21d73a 100644 --- a/boxes/drawing.py +++ b/boxes/drawing.py @@ -25,7 +25,11 @@ def pdiff(p1, p2): class Surface: - def __init__(self, fname, width, height): + + scale = 1.0 + invert_y = False + + def __init__(self, fname): self._fname = fname self.parts = [] self._p = self.new_part("default") @@ -39,15 +43,33 @@ class Surface: def finish(self): pass + def _adjust_coordinates(self): + extents = self.extents() + extents.xmin -= PADDING + extents.ymin -= PADDING + extents.xmax += PADDING + extents.ymax += PADDING + + m = Affine.translation(-extents.xmin, -extents.ymin) + if self.invert_y: + m = Affine.scale(self.scale, -self.scale) * m + m = Affine.translation(0, self.scale*extents.ymax) * m + else: + m = Affine.scale(self.scale, self.scale) * m + + self.transform(m, self.invert_y) + + return Extents(0, 0, extents.width * self.scale, extents.height * self.scale) + def render(self, renderer): renderer.init(**self.args) for p in self.parts: p.render(renderer) renderer.finish() - def move_offset(self, dx, dy): + def transform(self, m, invert_y=False): for p in self.parts: - p.move_offset(dx, dy) + p.transform(m, invert_y) def new_part(self, name="part"): if self.parts and len(self.parts[-1].pathes) == 0: @@ -82,10 +104,10 @@ class Part: return Extents() return sum([p.extents() for p in self.pathes]) - def move_offset(self, dx, dy): + def transform(self, m, invert_y=False): assert(not self.path) for p in self.pathes: - p.move_offset(dx, dy) + p.transform(m, invert_y) def append(self, *path): self.path.append(list(path)) @@ -139,16 +161,17 @@ class Path: e.add(*p[1:3]) return e - def move_offset(self, dx, dy): + def transform(self, m, invert_y=False): for c in self.path: C = c[0] - c[1] += dx - c[2] += dy + c[1], c[2] = m * (c[1], c[2]) if C == 'C': - c[3] += dx - c[4] += dy - c[5] += dx - c[6] += dy + c[3], c[4] = m * (c[3], c[4]) + c[5], c[6] = m * (c[5], c[6]) + if C == "T": + c[3] = m * c[3] + if invert_y: + c[3] *= Affine.scale(1, -1) def faster_edges(self): for (i, p) in enumerate(self.path): @@ -181,6 +204,7 @@ class Context: self._lw = 0 self._rgb = (0, 0, 0) self._ff = "sans-serif" + self._fs = 10 self._last_path = None def _update_bounds_(self, mx, my): @@ -293,8 +317,10 @@ class Context: self._xy = (0, 0) raise NotImplementedError() - def select_font_face(self, ff): - self._ff = ff + def set_font(self, style, bold=False, italic=False): + if style not in ("serif", "sans-serif", "monospaced"): + raise ValueError("Unknown font style") + self._ff = (style, bold, italic) def set_font_size(self, fs): self._fs = fs @@ -303,7 +329,8 @@ class Context: params = {"ff": self._ff, "fs": self._fs, "lw": self._lw, "rgb": self._rgb} params.update(args) mx0, my0 = self._m * self._xy - self._dwg.append("T", mx0, my0, text, params) + m = self._m + self._dwg.append("T", mx0, my0, m, text, params) def text_extents(self, text): fs = self._fs @@ -337,6 +364,14 @@ class Context: class SVGSurface(Surface): + invert_y = True + + fonts = { + 'serif' : 'TimesNewRoman, "Times New Roman", Times, Baskerville, Georgia, serif', + 'sans-serif' : '"Helvetica Neue", Helvetica, Arial, sans-serif', + 'monospaced' : '"Courier New", Courier, "Lucida Sans Typewriter"' + } + def _addTag(self, parent, tag, text, first=False): if first: t = ET.Element(tag) @@ -410,12 +445,9 @@ Creation date: {date} root.insert(0, m) def finish(self): - extents = self.extents() - - self.move_offset(-extents.xmin + PADDING, - -extents.ymin + PADDING) - w = extents.width + 2 * PADDING - h = extents.height + 2 * PADDING + extents = self._adjust_coordinates() + w = extents.width * self.scale + h = extents.height * self.scale nsmap = { @@ -467,13 +499,22 @@ Creation date: {date} f"C {x1:.3f} {y1:.3f} {x2:.3f} {y2:.3f} {x:.3f} {y:.3f}" ) elif C == "T": - text, params = c[3:] - style = f"font: {params['ff']} ; fill: {rgb_to_svg_color(*params['rgb'])}" + m, text, params = c[3:] + m = m * Affine.translation(0, -params['fs']) + tm = " ".join((f"{m[i]:.3f}" for i in (0, 3, 1, 4, 2, 5))) + font, bold, italic = params['ff'] + fontweight = ("normal", "bold")[bool(bold)] + fontstyle = ("normal", "italic")[bool(italic)] + + style = f"font-family: {font} ; font-weight: {fontweight}; font-style: {fontstyle}; fill: {rgb_to_svg_color(*params['rgb'])}" t = ET.SubElement(g, "text", - x=f"{x:.3f}", y=f"{y:.3f}", + #x=f"{x:.3f}", y=f"{y:.3f}", + transform=f"matrix( {tm} )", style=style) t.text = text t.set("font-size", f"{params['fs']}px") + t.set("text-anchor", params.get('align', 'left')) + t.set("alignment-baseline", 'hanging') else: print("Unknown", c) color = ( @@ -490,22 +531,55 @@ Creation date: {date} class PSSurface(Surface): + scale = 72 / 25.4 # 72 dpi + + fonts = { + ('serif', False, False) : 'Times-Roman', + ('serif', False, True) : 'Times-Italic', + ('serif', True, False) : 'Times-Bold', + ('serif', True, True) : 'Times-BoldItalic', + ('sans-serif', False, False) : 'Helvetica', + ('sans-serif', False, True) : 'Helvetica-Oblique', + ('sans-serif', True, False) : 'Helvetica-Bold', + ('sans-serif', True, True) : 'Helvetica-BoldOblique', + ('monospaced', False, False) : 'Courier', + ('monospaced', False, True) : 'Courier-Oblique', + ('monospaced', True, False) : 'Courier-Bold', + ('monospaced', True, True) : 'Courier-BoldOblique', + } + def finish(self): - extents = self.extents() + extents = self._adjust_coordinates() + w = extents.width + h = extents.height - self.move_offset(-extents.xmin + PADDING, - -extents.ymin + PADDING) - - w = extents.width + 2 * PADDING - h = extents.height + 2 * PADDING - - f = open(self._fname, "w") + f = open(self._fname, "w", encoding="latin1", errors="replace") f.write("%!PS-Adobe-2.0\n") f.write( - f"%%BoundingBox: 0 0 {w:.0f} {h:.0f}\n\n" - ) + f"""%%BoundingBox: 0 0 {w:.0f} {h:.0f} + +1 setlinecap +1 setlinejoin +0.0 0.0 0.0 setrgbcolor +""") + f.write(""" +/ReEncode { % inFont outFont encoding | - + /MyEncoding exch def + exch findfont + dup length dict + begin + {def} forall + /Encoding MyEncoding def + currentdict + end + definefont +} def + +""") + for font in self.fonts.values(): + f.write(f"/{font} /{font}-Latin1 ISOLatin1Encoding ReEncode\n") # f.write(f"%%DocumentMedia: \d+x\d+mm ((\d+) (\d+)) 0 \(" # dwg['width']=f'{w:.2f}mm' # dwg['height']=f'{h:.2f}mm' @@ -513,7 +587,6 @@ class PSSurface(Surface): for i, part in enumerate(self.parts): if not part.pathes: continue - # g = dwg.add( dwg.g(id=f'p-{i}',style='fill:none;stroke-linecap:round;stroke-linejoin:round;') ) for j, path in enumerate(part.pathes): p = [] x, y = 0, 0 @@ -532,16 +605,33 @@ class PSSurface(Surface): f"{x1:.3f} {y1:.3f} {x2:.3f} {y2:.3f} {x:.3f} {y:.3f} curveto" ) elif C == "T": - text, params = c[3:] + m, text, params = c[3:] + tm = " ".join((f"{m[i]:.3f}" for i in (0, 3, 1, 4, 2, 5))) text = text.replace("(", "r\(").replace(")", r"\)") color = " ".join((f"{c:.2f}" for c in params["rgb"])) - f.write(f"/{params['ff']} findfont\n") - f.write(f"{params['fs']*72 / 25.4} scalefont\n") + align = params.get('align', 'left') + f.write(f"/{self.fonts[params['ff']]}-Latin1 findfont\n") + f.write(f"{params['fs']} scalefont\n") f.write("setfont\n") + #f.write(f"currentfont /Encoding ISOLatin1Encoding put\n") f.write(f"{color} setrgbcolor\n") - f.write(f"{x:.3f} {y:.3f} moveto\n") - f.write(f"({text}) show\n\n") + f.write("matrix currentmatrix") # save current matrix + f.write(f"[ {tm} ] concat\n") + if align == "left": + f.write(f"0.0\n") + else: + f.write(f"({text}) stringwidth pop ") + if align == "middle": + f.write(f"-0.5 mul\n") + else: # end + f.write(f"neg\n") + # offset y by descender + f.write("currentfont dup /FontBBox get 1 get \n") + f.write("exch /FontMatrix get 3 get mul neg moveto \n") + + f.write(f"({text}) show\n") # text created by dup above + f.write("setmatrix\n\n") # restore matrix else: print("Unknown", c) color = ( diff --git a/boxes/formats.py b/boxes/formats.py index 3bc3aef..0fd8873 100644 --- a/boxes/formats.py +++ b/boxes/formats.py @@ -58,25 +58,12 @@ class Formats: return self._BASE_FORMATS def getSurface(self, fmt, filename): - - width = height = 10000 # mm - if fmt in ("svg", "svg_Ponoko"): - surface = SVGSurface(filename, width, height) - mm2pt = 1.0 + surface = SVGSurface(filename) else: - mm2pt = 72 / 25.4 - width *= mm2pt - height *= mm2pt # 3.543307 - surface = PSSurface(filename, width, height) + surface = PSSurface(filename) ctx = Context(surface) - if fmt in ("svg", "svg_Ponoko"): - ctx.translate(0, height) - ctx.scale(mm2pt, -mm2pt) - else: - ctx.scale(mm2pt, mm2pt) - return surface, ctx def convert(self, filename, fmt, metadata=None): diff --git a/scripts/boxesserver b/scripts/boxesserver index 43bca2d..d82157e 100755 --- a/scripts/boxesserver +++ b/scripts/boxesserver @@ -507,9 +507,10 @@ class BServer: extension = "svg" http_headers.append(('Content-Disposition', 'attachment; filename="%s.%s"' % (box.__class__.__name__, extension))) start_response(status, http_headers) - result = open(box.output).readlines() + result = open(box.output, 'rb').readlines() os.close(fd) os.remove(box.output) + return (l for l in result) return (l.encode("utf-8") for l in result) if __name__=="__main__":