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
This commit is contained in:
Florian Festi 2020-04-20 23:38:00 +02:00
parent 492e7fb4dd
commit 6440bcb639
4 changed files with 158 additions and 81 deletions

View File

@ -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,

View File

@ -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 = (

View File

@ -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):

View File

@ -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__":