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:
parent
492e7fb4dd
commit
6440bcb639
|
@ -291,6 +291,15 @@ class Boxes:
|
||||||
"""
|
"""
|
||||||
self.ctx.set_source_rgb(*color)
|
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):
|
def open(self):
|
||||||
"""
|
"""
|
||||||
Prepare for rendering
|
Prepare for rendering
|
||||||
|
@ -313,7 +322,7 @@ class Boxes:
|
||||||
self.set_source_color(Color.BLACK)
|
self.set_source_color(Color.BLACK)
|
||||||
|
|
||||||
self.spacing = 2 * self.burn + 0.5 * self.thickness
|
self.spacing = 2 * self.burn + 0.5 * self.thickness
|
||||||
self.ctx.select_font_face("sans-serif")
|
self.set_font("sans-serif")
|
||||||
self._buildObjects()
|
self._buildObjects()
|
||||||
if self.reference and self.format != 'svg_Ponoko':
|
if self.reference and self.format != 'svg_Ponoko':
|
||||||
self.move(10, 10, "up", before=True)
|
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
|
: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)
|
self.moveTo(x, y, angle)
|
||||||
text = text.split("\n")
|
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)
|
lines = len(text)
|
||||||
height = lines * lheight + (lines - 1) * 0.4 * lheight
|
height = lines * fontsize + (lines - 1) * 0.4 * fontsize
|
||||||
align = align.split()
|
align = align.split()
|
||||||
|
halign = "left"
|
||||||
moves = {
|
moves = {
|
||||||
"top": (0, -height),
|
"top": -height,
|
||||||
"middle": (0, -0.5 * height),
|
"middle": -0.5 * height,
|
||||||
"bottom": (0, 0),
|
"bottom": 0,
|
||||||
"left": (0, 0),
|
"left": "start",
|
||||||
"center": (-0.5 * width, 0),
|
"center": "middle",
|
||||||
"right": (-width, 0),
|
"right": "end",
|
||||||
}
|
}
|
||||||
for a in align:
|
for a in align:
|
||||||
if a in moves:
|
if a in moves:
|
||||||
self.moveTo(*moves[a])
|
if isinstance(moves[a], str):
|
||||||
|
halign = moves[a]
|
||||||
|
else:
|
||||||
|
self.moveTo(0, moves[a])
|
||||||
else:
|
else:
|
||||||
raise ValueError("Unknown alignment: %s" % align)
|
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):
|
for line in reversed(text):
|
||||||
self.ctx.show_text(line)
|
self.ctx.show_text(line, fs=fontsize, align=halign, rgb=color)
|
||||||
self.moveTo(0, 1.4 * -lheight)
|
self.moveTo(0, 1.4 * fontsize)
|
||||||
self.set_source_color(Color.BLACK)
|
|
||||||
self.ctx.set_font_size(10)
|
|
||||||
|
|
||||||
tx_sizes = {
|
tx_sizes = {
|
||||||
1 : 0.61,
|
1 : 0.61,
|
||||||
|
|
170
boxes/drawing.py
170
boxes/drawing.py
|
@ -25,7 +25,11 @@ def pdiff(p1, p2):
|
||||||
|
|
||||||
|
|
||||||
class Surface:
|
class Surface:
|
||||||
def __init__(self, fname, width, height):
|
|
||||||
|
scale = 1.0
|
||||||
|
invert_y = False
|
||||||
|
|
||||||
|
def __init__(self, fname):
|
||||||
self._fname = fname
|
self._fname = fname
|
||||||
self.parts = []
|
self.parts = []
|
||||||
self._p = self.new_part("default")
|
self._p = self.new_part("default")
|
||||||
|
@ -39,15 +43,33 @@ class Surface:
|
||||||
def finish(self):
|
def finish(self):
|
||||||
pass
|
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):
|
def render(self, renderer):
|
||||||
renderer.init(**self.args)
|
renderer.init(**self.args)
|
||||||
for p in self.parts:
|
for p in self.parts:
|
||||||
p.render(renderer)
|
p.render(renderer)
|
||||||
renderer.finish()
|
renderer.finish()
|
||||||
|
|
||||||
def move_offset(self, dx, dy):
|
def transform(self, m, invert_y=False):
|
||||||
for p in self.parts:
|
for p in self.parts:
|
||||||
p.move_offset(dx, dy)
|
p.transform(m, invert_y)
|
||||||
|
|
||||||
def new_part(self, name="part"):
|
def new_part(self, name="part"):
|
||||||
if self.parts and len(self.parts[-1].pathes) == 0:
|
if self.parts and len(self.parts[-1].pathes) == 0:
|
||||||
|
@ -82,10 +104,10 @@ class Part:
|
||||||
return Extents()
|
return Extents()
|
||||||
return sum([p.extents() for p in self.pathes])
|
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)
|
assert(not self.path)
|
||||||
for p in self.pathes:
|
for p in self.pathes:
|
||||||
p.move_offset(dx, dy)
|
p.transform(m, invert_y)
|
||||||
|
|
||||||
def append(self, *path):
|
def append(self, *path):
|
||||||
self.path.append(list(path))
|
self.path.append(list(path))
|
||||||
|
@ -139,16 +161,17 @@ class Path:
|
||||||
e.add(*p[1:3])
|
e.add(*p[1:3])
|
||||||
return e
|
return e
|
||||||
|
|
||||||
def move_offset(self, dx, dy):
|
def transform(self, m, invert_y=False):
|
||||||
for c in self.path:
|
for c in self.path:
|
||||||
C = c[0]
|
C = c[0]
|
||||||
c[1] += dx
|
c[1], c[2] = m * (c[1], c[2])
|
||||||
c[2] += dy
|
|
||||||
if C == 'C':
|
if C == 'C':
|
||||||
c[3] += dx
|
c[3], c[4] = m * (c[3], c[4])
|
||||||
c[4] += dy
|
c[5], c[6] = m * (c[5], c[6])
|
||||||
c[5] += dx
|
if C == "T":
|
||||||
c[6] += dy
|
c[3] = m * c[3]
|
||||||
|
if invert_y:
|
||||||
|
c[3] *= Affine.scale(1, -1)
|
||||||
|
|
||||||
def faster_edges(self):
|
def faster_edges(self):
|
||||||
for (i, p) in enumerate(self.path):
|
for (i, p) in enumerate(self.path):
|
||||||
|
@ -181,6 +204,7 @@ class Context:
|
||||||
self._lw = 0
|
self._lw = 0
|
||||||
self._rgb = (0, 0, 0)
|
self._rgb = (0, 0, 0)
|
||||||
self._ff = "sans-serif"
|
self._ff = "sans-serif"
|
||||||
|
self._fs = 10
|
||||||
self._last_path = None
|
self._last_path = None
|
||||||
|
|
||||||
def _update_bounds_(self, mx, my):
|
def _update_bounds_(self, mx, my):
|
||||||
|
@ -293,8 +317,10 @@ class Context:
|
||||||
self._xy = (0, 0)
|
self._xy = (0, 0)
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def select_font_face(self, ff):
|
def set_font(self, style, bold=False, italic=False):
|
||||||
self._ff = ff
|
if style not in ("serif", "sans-serif", "monospaced"):
|
||||||
|
raise ValueError("Unknown font style")
|
||||||
|
self._ff = (style, bold, italic)
|
||||||
|
|
||||||
def set_font_size(self, fs):
|
def set_font_size(self, fs):
|
||||||
self._fs = fs
|
self._fs = fs
|
||||||
|
@ -303,7 +329,8 @@ class Context:
|
||||||
params = {"ff": self._ff, "fs": self._fs, "lw": self._lw, "rgb": self._rgb}
|
params = {"ff": self._ff, "fs": self._fs, "lw": self._lw, "rgb": self._rgb}
|
||||||
params.update(args)
|
params.update(args)
|
||||||
mx0, my0 = self._m * self._xy
|
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):
|
def text_extents(self, text):
|
||||||
fs = self._fs
|
fs = self._fs
|
||||||
|
@ -337,6 +364,14 @@ class Context:
|
||||||
|
|
||||||
class SVGSurface(Surface):
|
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):
|
def _addTag(self, parent, tag, text, first=False):
|
||||||
if first:
|
if first:
|
||||||
t = ET.Element(tag)
|
t = ET.Element(tag)
|
||||||
|
@ -410,12 +445,9 @@ Creation date: {date}
|
||||||
root.insert(0, m)
|
root.insert(0, m)
|
||||||
|
|
||||||
def finish(self):
|
def finish(self):
|
||||||
extents = self.extents()
|
extents = self._adjust_coordinates()
|
||||||
|
w = extents.width * self.scale
|
||||||
self.move_offset(-extents.xmin + PADDING,
|
h = extents.height * self.scale
|
||||||
-extents.ymin + PADDING)
|
|
||||||
w = extents.width + 2 * PADDING
|
|
||||||
h = extents.height + 2 * PADDING
|
|
||||||
|
|
||||||
|
|
||||||
nsmap = {
|
nsmap = {
|
||||||
|
@ -467,13 +499,22 @@ Creation date: {date}
|
||||||
f"C {x1:.3f} {y1:.3f} {x2:.3f} {y2:.3f} {x:.3f} {y:.3f}"
|
f"C {x1:.3f} {y1:.3f} {x2:.3f} {y2:.3f} {x:.3f} {y:.3f}"
|
||||||
)
|
)
|
||||||
elif C == "T":
|
elif C == "T":
|
||||||
text, params = c[3:]
|
m, text, params = c[3:]
|
||||||
style = f"font: {params['ff']} ; fill: {rgb_to_svg_color(*params['rgb'])}"
|
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",
|
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)
|
style=style)
|
||||||
t.text = text
|
t.text = text
|
||||||
t.set("font-size", f"{params['fs']}px")
|
t.set("font-size", f"{params['fs']}px")
|
||||||
|
t.set("text-anchor", params.get('align', 'left'))
|
||||||
|
t.set("alignment-baseline", 'hanging')
|
||||||
else:
|
else:
|
||||||
print("Unknown", c)
|
print("Unknown", c)
|
||||||
color = (
|
color = (
|
||||||
|
@ -490,22 +531,55 @@ Creation date: {date}
|
||||||
|
|
||||||
class PSSurface(Surface):
|
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):
|
def finish(self):
|
||||||
|
|
||||||
extents = self.extents()
|
extents = self._adjust_coordinates()
|
||||||
|
w = extents.width
|
||||||
|
h = extents.height
|
||||||
|
|
||||||
self.move_offset(-extents.xmin + PADDING,
|
f = open(self._fname, "w", encoding="latin1", errors="replace")
|
||||||
-extents.ymin + PADDING)
|
|
||||||
|
|
||||||
w = extents.width + 2 * PADDING
|
|
||||||
h = extents.height + 2 * PADDING
|
|
||||||
|
|
||||||
f = open(self._fname, "w")
|
|
||||||
|
|
||||||
f.write("%!PS-Adobe-2.0\n")
|
f.write("%!PS-Adobe-2.0\n")
|
||||||
f.write(
|
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 \("
|
# f.write(f"%%DocumentMedia: \d+x\d+mm ((\d+) (\d+)) 0 \("
|
||||||
# dwg['width']=f'{w:.2f}mm'
|
# dwg['width']=f'{w:.2f}mm'
|
||||||
# dwg['height']=f'{h:.2f}mm'
|
# dwg['height']=f'{h:.2f}mm'
|
||||||
|
@ -513,7 +587,6 @@ class PSSurface(Surface):
|
||||||
for i, part in enumerate(self.parts):
|
for i, part in enumerate(self.parts):
|
||||||
if not part.pathes:
|
if not part.pathes:
|
||||||
continue
|
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):
|
for j, path in enumerate(part.pathes):
|
||||||
p = []
|
p = []
|
||||||
x, y = 0, 0
|
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"
|
f"{x1:.3f} {y1:.3f} {x2:.3f} {y2:.3f} {x:.3f} {y:.3f} curveto"
|
||||||
)
|
)
|
||||||
elif C == "T":
|
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"\)")
|
text = text.replace("(", "r\(").replace(")", r"\)")
|
||||||
color = " ".join((f"{c:.2f}"
|
color = " ".join((f"{c:.2f}"
|
||||||
for c in params["rgb"]))
|
for c in params["rgb"]))
|
||||||
f.write(f"/{params['ff']} findfont\n")
|
align = params.get('align', 'left')
|
||||||
f.write(f"{params['fs']*72 / 25.4} scalefont\n")
|
f.write(f"/{self.fonts[params['ff']]}-Latin1 findfont\n")
|
||||||
|
f.write(f"{params['fs']} scalefont\n")
|
||||||
f.write("setfont\n")
|
f.write("setfont\n")
|
||||||
|
#f.write(f"currentfont /Encoding ISOLatin1Encoding put\n")
|
||||||
f.write(f"{color} setrgbcolor\n")
|
f.write(f"{color} setrgbcolor\n")
|
||||||
f.write(f"{x:.3f} {y:.3f} moveto\n")
|
f.write("matrix currentmatrix") # save current matrix
|
||||||
f.write(f"({text}) show\n\n")
|
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:
|
else:
|
||||||
print("Unknown", c)
|
print("Unknown", c)
|
||||||
color = (
|
color = (
|
||||||
|
|
|
@ -58,25 +58,12 @@ class Formats:
|
||||||
return self._BASE_FORMATS
|
return self._BASE_FORMATS
|
||||||
|
|
||||||
def getSurface(self, fmt, filename):
|
def getSurface(self, fmt, filename):
|
||||||
|
|
||||||
width = height = 10000 # mm
|
|
||||||
|
|
||||||
if fmt in ("svg", "svg_Ponoko"):
|
if fmt in ("svg", "svg_Ponoko"):
|
||||||
surface = SVGSurface(filename, width, height)
|
surface = SVGSurface(filename)
|
||||||
mm2pt = 1.0
|
|
||||||
else:
|
else:
|
||||||
mm2pt = 72 / 25.4
|
surface = PSSurface(filename)
|
||||||
width *= mm2pt
|
|
||||||
height *= mm2pt # 3.543307
|
|
||||||
surface = PSSurface(filename, width, height)
|
|
||||||
|
|
||||||
ctx = Context(surface)
|
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
|
return surface, ctx
|
||||||
|
|
||||||
def convert(self, filename, fmt, metadata=None):
|
def convert(self, filename, fmt, metadata=None):
|
||||||
|
|
|
@ -507,9 +507,10 @@ class BServer:
|
||||||
extension = "svg"
|
extension = "svg"
|
||||||
http_headers.append(('Content-Disposition', 'attachment; filename="%s.%s"' % (box.__class__.__name__, extension)))
|
http_headers.append(('Content-Disposition', 'attachment; filename="%s.%s"' % (box.__class__.__name__, extension)))
|
||||||
start_response(status, http_headers)
|
start_response(status, http_headers)
|
||||||
result = open(box.output).readlines()
|
result = open(box.output, 'rb').readlines()
|
||||||
os.close(fd)
|
os.close(fd)
|
||||||
os.remove(box.output)
|
os.remove(box.output)
|
||||||
|
return (l for l in result)
|
||||||
return (l.encode("utf-8") for l in result)
|
return (l.encode("utf-8") for l in result)
|
||||||
|
|
||||||
if __name__=="__main__":
|
if __name__=="__main__":
|
||||||
|
|
Loading…
Reference in New Issue