def test_begintag_endtag(self): writer = XMLWriter(BytesIO()) writer.begintag("tag", attr="value") writer.write("content") writer.endtag("tag") self.assertEqual(HEADER + b'<tag attr="value">content</tag>', writer.file.getvalue())
def write_sbix_to_file(filename): basename = os.path.basename(filename) xml_filename = basename + '.xml' out_filename = os.path.join(os.getcwd(), xml_filename) if os.path.exists(out_filename): print('%s already exists. not extracting' % out_filename) return out_filename print('extracting sbix chunk to file %s' % out_filename) with open(out_filename, 'wb') as fx: mx = XMLWriter(fx) mx.begintag("root") font = TTFont(filename, fontNumber=1) bix = font['sbix'] bix.toXML(xmlWriter=mx, ttFont=font) mx.endtag("root") mx.close() return out_filename
def check_mti_file(self, name, tableTag=None): xml_expected_path = self.getpath("%s.ttx" % name + ('.'+tableTag if tableTag is not None else '')) with open(xml_expected_path, 'rt', encoding="utf-8") as xml_expected_file: xml_expected = xml_expected_file.read() font = self.create_font() with open(self.getpath("%s.txt" % name), 'rt', encoding="utf-8") as f: table = mtiLib.build(f, font, tableTag=tableTag) if tableTag is not None: self.assertEqual(tableTag, table.tableTag) tableTag = table.tableTag # Make sure it compiles. blob = table.compile(font) # Make sure it decompiles. decompiled = table.__class__() decompiled.decompile(blob, font) # XML from built object. writer = XMLWriter(StringIO()) writer.begintag(tableTag); writer.newline() table.toXML(writer, font) writer.endtag(tableTag); writer.newline() xml_built = writer.file.getvalue() # XML from decompiled object. writer = XMLWriter(StringIO()) writer.begintag(tableTag); writer.newline() decompiled.toXML(writer, font) writer.endtag(tableTag); writer.newline() xml_binary = writer.file.getvalue() self.expect_ttx(xml_binary, xml_built, fromfile='decompiled', tofile='built') self.expect_ttx(xml_expected, xml_built, fromfile=xml_expected_path, tofile='built') from fontTools.misc import xmlReader f = StringIO() f.write(xml_expected) f.seek(0) font2 = TTFont() font2.setGlyphOrder(font.getGlyphOrder()) reader = xmlReader.XMLReader(f, font2) reader.read(rootless=True) # XML from object read from XML. writer = XMLWriter(StringIO()) writer.begintag(tableTag); writer.newline() font2[tableTag].toXML(writer, font) writer.endtag(tableTag); writer.newline() xml_fromxml = writer.file.getvalue() self.expect_ttx(xml_expected, xml_fromxml, fromfile=xml_expected_path, tofile='fromxml')
def check_mti_file(self, name, tableTag=None): xml_expected_path = self.getpath("%s.ttx" % name + ('.'+tableTag if tableTag is not None else '')) with open(xml_expected_path, 'rt', encoding="utf-8") as xml_expected_file: xml_expected = xml_expected_file.read() font = self.create_font() with open(self.getpath("%s.txt" % name), 'rt', encoding="utf-8") as f: table = mtiLib.build(f, font, tableTag=tableTag) if tableTag is not None: self.assertEqual(tableTag, table.tableTag) tableTag = table.tableTag # Make sure it compiles. blob = table.compile(font) # Make sure it decompiles. decompiled = table.__class__() decompiled.decompile(blob, font) # XML from built object. writer = XMLWriter(StringIO(), newlinestr='\n') writer.begintag(tableTag); writer.newline() table.toXML(writer, font) writer.endtag(tableTag); writer.newline() xml_built = writer.file.getvalue() # XML from decompiled object. writer = XMLWriter(StringIO(), newlinestr='\n') writer.begintag(tableTag); writer.newline() decompiled.toXML(writer, font) writer.endtag(tableTag); writer.newline() xml_binary = writer.file.getvalue() self.expect_ttx(xml_binary, xml_built, fromfile='decompiled', tofile='built') self.expect_ttx(xml_expected, xml_built, fromfile=xml_expected_path, tofile='built') from fontTools.misc import xmlReader f = StringIO() f.write(xml_expected) f.seek(0) font2 = TTFont() font2.setGlyphOrder(font.getGlyphOrder()) reader = xmlReader.XMLReader(f, font2) reader.read(rootless=True) # XML from object read from XML. writer = XMLWriter(StringIO(), newlinestr='\n') writer.begintag(tableTag); writer.newline() font2[tableTag].toXML(writer, font) writer.endtag(tableTag); writer.newline() xml_fromxml = writer.file.getvalue() self.expect_ttx(xml_expected, xml_fromxml, fromfile=xml_expected_path, tofile='fromxml')
class SVGContext(BaseContext): _graphicsStateClass = SVGGraphicsState _shadowClass = SVGShadow _colorClass = SVGColor _gradientClass = SVGGradient _clipPathIDGenerator = _UniqueIDGenerator("clip") _svgFileClass = SVGFile _svgTagArguments = { "version": "1.1", "xmlns": "http://www.w3.org/2000/svg", "xmlns:xlink": "http://www.w3.org/1999/xlink" } _svgLineJoinStylesMap = { AppKit.NSMiterLineJoinStyle: "miter", AppKit.NSRoundLineJoinStyle: "round", AppKit.NSBevelLineJoinStyle: "bevel" } _svgLineCapStylesMap = { AppKit.NSButtLineCapStyle: "butt", AppKit.NSSquareLineCapStyle: "square", AppKit.NSRoundLineCapStyle: "round", } indentation = " " fileExtensions = ["svg"] saveImageOptions = [ ("multipage", "Output a numbered svg file for each page or frame in the document."), ] def __init__(self): super(SVGContext, self).__init__() self._pages = [] # not supported in a svg context def cmykFill(self, c, m, y, k, a=1): warnings.warn("cmykFill is not supported in a svg context") def cmykStroke(self, c, m, y, k, a=1): warnings.warn("cmykStroke is not supported in a svg context") def cmykLinearGradient(self, startPoint=None, endPoint=None, colors=None, locations=None): warnings.warn("cmykLinearGradient is not supported in a svg context") def cmykRadialGradient(self, startPoint=None, endPoint=None, colors=None, locations=None, startRadius=0, endRadius=100): warnings.warn("cmykRadialGradient is not supported in a svg context") def cmykShadow(self, offset, blur, color): warnings.warn("cmykShadow is not supported in a svg context") # svg overwrites def shadow(self, offset, blur, color): super(SVGContext, self).shadow(offset, blur, color) if self._state.shadow is not None: self._state.shadow.writeDefs(self._svgContext) def linearGradient(self, startPoint=None, endPoint=None, colors=None, locations=None): super(SVGContext, self).linearGradient(startPoint, endPoint, colors, locations) if self._state.gradient is not None: self._state.gradient.writeDefs(self._svgContext) def radialGradient(self, startPoint=None, endPoint=None, colors=None, locations=None, startRadius=0, endRadius=100): super(SVGContext, self).radialGradient(startPoint, endPoint, colors, locations, startRadius, endRadius) if startRadius != 0: warnings.warn("radialGradient will clip the startRadius to '0' in a svg context.") if self._state.gradient is not None: self._state.gradient.writeDefs(self._svgContext) # svg def _reset(self, other=None): self._embeddedFonts = set() self._embeddedImages = dict() def _newPage(self, width, height): if hasattr(self, "_svgContext"): self._svgContext.endtag("svg") self.reset() self.size(width, height) self._svgData = self._svgFileClass() self._pages.append(self._svgData) self._svgContext = XMLWriter(self._svgData, encoding="utf-8", indentwhite=self.indentation) self._svgContext.width = self.width self._svgContext.height = self.height self._svgContext.begintag("svg", width=self.width, height=self.height, **self._svgTagArguments) self._svgContext.newline() self._state.transformMatrix = self._state.transformMatrix.scale(1, -1).translate(0, -self.height) def _saveImage(self, path, options): multipage = options.get("multipage") if multipage is None: multipage = False self._svgContext.endtag("svg") fileName, fileExt = os.path.splitext(path) firstPage = 0 pageCount = len(self._pages) pathAdd = "_1" if not multipage: firstPage = pageCount - 1 pathAdd = "" for index in range(firstPage, pageCount): page = self._pages[index] svgPath = fileName + pathAdd + fileExt page.writeToFile(svgPath) pathAdd = "_%s" % (index + 2) def _save(self): pass def _restore(self): pass def _drawPath(self): if self._state.path: self._svgBeginClipPath() data = self._svgDrawingAttributes() data["d"] = self._svgPath(self._state.path) data["transform"] = self._svgTransform(self._state.transformMatrix) if self._state.shadow is not None: data["filter"] = "url(#%s)" % self._state.shadow.tagID if self._state.gradient is not None: data["fill"] = "url(#%s)" % self._state.gradient.tagID self._svgContext.simpletag("path", **data) self._svgContext.newline() self._svgEndClipPath() def _clipPath(self): uniqueID = self._clipPathIDGenerator.gen() self._svgContext.begintag("clipPath", id=uniqueID) self._svgContext.newline() data = dict() data["d"] = self._svgPath(self._state.path) data["transform"] = self._svgTransform(self._state.transformMatrix) data["clip-rule"] = "evenodd" self._svgContext.simpletag("path", **data) self._svgContext.newline() self._svgContext.endtag("clipPath") self._svgContext.newline() self._state.clipPathID = uniqueID def _textBox(self, txt, box, align): path, (x, y) = self._getPathForFrameSetter(box) canDoGradients = True if align == "justified": warnings.warn("justified text is not supported in a svg context") attrString = self.attributedString(txt, align=align) if self._state.hyphenation: attrString = self.hyphenateAttributedString(attrString, path) txt = attrString.string() setter = CoreText.CTFramesetterCreateWithAttributedString(attrString) box = CoreText.CTFramesetterCreateFrame(setter, (0, 0), path, None) self._svgBeginClipPath() defaultData = self._svgDrawingAttributes() data = { "text-anchor": "start", "transform": self._svgTransform(self._state.transformMatrix.translate(x, y + self.height).scale(1, -1)) } if self._state.shadow is not None: data["filter"] = "url(#%s_flipped)" % self._state.shadow.tagID self._svgContext.begintag("text", **data) self._svgContext.newline() ctLines = CoreText.CTFrameGetLines(box) origins = CoreText.CTFrameGetLineOrigins(box, (0, len(ctLines)), None) for i, (originX, originY) in enumerate(origins): ctLine = ctLines[i] # bounds = CoreText.CTLineGetImageBounds(ctLine, self._pdfContext) # if bounds.size.width == 0: # continue ctRuns = CoreText.CTLineGetGlyphRuns(ctLine) for ctRun in ctRuns: stringRange = CoreText.CTRunGetStringRange(ctRun) attributes = CoreText.CTRunGetAttributes(ctRun) font = attributes.get(AppKit.NSFontAttributeName) fontAttributes = font.fontDescriptor().fontAttributes() fillColor = attributes.get(AppKit.NSForegroundColorAttributeName) strokeColor = attributes.get(AppKit.NSStrokeColorAttributeName) strokeWidth = attributes.get(AppKit.NSStrokeWidthAttributeName, self._state.strokeWidth) baselineShift = attributes.get(AppKit.NSBaselineOffsetAttributeName, 0) openTypeFeatures = fontAttributes.get(CoreText.NSFontFeatureSettingsAttribute) fontName = font.fontName() fontSize = font.pointSize() spanData = dict(defaultData) fill = self._colorClass(fillColor).svgColor() if fill: c, a = fill spanData["fill"] = c if a != 1: spanData["fill-opacity"] = a stroke = self._colorClass(strokeColor).svgColor() if stroke: c, a = stroke spanData["stroke"] = c if a != 1: spanData["stroke-opacity"] = a spanData["stroke-width"] = formatNumber(abs(strokeWidth) * .5) spanData["font-family"] = fontName spanData["font-size"] = formatNumber(fontSize) if openTypeFeatures: featureTags = getFeatureTagsForFontAttributes(openTypeFeatures) spanData["style"] = self._svgStyle(**{ "font-feature-settings": self._svgStyleOpenTypeFeatures(featureTags) } ) if canDoGradients and self._state.gradient is not None: spanData["fill"] = "url(#%s_flipped)" % self._state.gradient.tagID self._save() runTxt = txt.substringWithRange_((stringRange.location, stringRange.length)) while runTxt and runTxt[-1] == " ": runTxt = runTxt[:-1] runTxt = runTxt.replace("\n", "") runTxt = runTxt.encode("utf-8") runPos = CoreText.CTRunGetPositions(ctRun, (0, 1), None) runX = runY = 0 if runPos: runX = runPos[0].x runY = runPos[0].y spanData["x"] = formatNumber(originX + runX) spanData["y"] = formatNumber(self.height - originY - runY + baselineShift) self._svgContext.begintag("tspan", **spanData) self._svgContext.newline() self._svgContext.write(runTxt) self._svgContext.newline() self._svgContext.endtag("tspan") self._svgContext.newline() self._restore() self._svgContext.endtag("text") self._svgContext.newline() self._svgEndClipPath() def _image(self, path, xy, alpha, pageNumber): # todo: # support embedding of images when the source is not a path but # a nsimage or a pdf / gif with a pageNumber x, y = xy self._svgBeginClipPath() if path.startswith("http"): url = AppKit.NSURL.URLWithString_(path) else: url = AppKit.NSURL.fileURLWithPath_(path) image = AppKit.NSImage.alloc().initByReferencingURL_(url) width, height = image.size() if path not in self._embeddedImages: # get a unique id for the image imageID = "image_%s" % (len(self._embeddedImages) + 1) # store it self._embeddedImages[path] = imageID _, ext = os.path.splitext(path) mimeSubtype = ext[1:].lower() # remove the dot, make lowercase if mimeSubtype == "jpg": mimeSubtype = "jpeg" if mimeSubtype not in ("png", "jpeg"): # the image is not a png or a jpeg # convert it to a png mimeSubtype = "png" imageRep = _makeBitmapImageRep(image) imageData = imageRep.representationUsingType_properties_(AppKit.NSPNGFileType, None) else: imageData = AppKit.NSData.dataWithContentsOfURL_(url).bytes() defData = [ ("id", imageID), ("width", width), ("height", height), ("xlink:href", "data:image/%s;base64,%s" % (mimeSubtype, base64.b64encode(imageData).decode("ascii"))) ] self._svgContext.begintag("defs") self._svgContext.newline() self._svgContext.simpletag("image", defData) self._svgContext.newline() self._svgContext.endtag("defs") self._svgContext.newline() else: imageID = self._embeddedImages[path] data = [ ("x", 0), ("y", 0), ("opacity", alpha), ("transform", self._svgTransform(self._state.transformMatrix.translate(x, y + height).scale(1, -1))), ("xlink:href", "#%s" % imageID) ] self._svgContext.simpletag("use", data) self._svgContext.newline() self._svgEndClipPath() def _transform(self, transform): self._state.transformMatrix = self._state.transformMatrix.transform(transform) # helpers def _svgTransform(self, transform): return "matrix(%s)" % (",".join([repr(s) for s in transform])) def _svgPath(self, path, transformMatrix=None): path = path.getNSBezierPath() if transformMatrix: path = path.copy() aT = AppKit.NSAffineTransform.transform() aT.setTransformStruct_(transformMatrix[:]) path.transformUsingAffineTransform_(aT) svg = "" for i in range(path.elementCount()): instruction, points = path.elementAtIndex_associatedPoints_(i) if instruction == AppKit.NSMoveToBezierPathElement: svg += "M%s,%s " % (formatNumber(points[0].x), formatNumber(points[0].y)) previousPoint = points[-1] elif instruction == AppKit.NSLineToBezierPathElement: x = points[0].x - previousPoint.x y = points[0].y - previousPoint.y svg += "l%s,%s " % (formatNumber(x), formatNumber(y)) previousPoint = points[-1] elif instruction == AppKit.NSCurveToBezierPathElement: offx1 = points[0].x - previousPoint.x offy1 = points[0].y - previousPoint.y offx2 = points[1].x - previousPoint.x offy2 = points[1].y - previousPoint.y x = points[2].x - previousPoint.x y = points[2].y - previousPoint.y svg += "c%s,%s,%s,%s,%s,%s " % (formatNumber(offx1), formatNumber(offy1), formatNumber(offx2), formatNumber(offy2), formatNumber(x), formatNumber(y)) previousPoint = points[-1] elif instruction == AppKit.NSClosePathBezierPathElement: svg += "Z " return svg.strip() def _svgBeginClipPath(self): if self._state.clipPathID: data = dict() data["clip-path"] = "url(#%s)" % self._state.clipPathID self._svgContext.begintag("g", **data) self._svgContext.newline() def _svgEndClipPath(self): if self._state.clipPathID: self._svgContext.endtag("g") self._svgContext.newline() def _svgDrawingAttributes(self): data = dict() fill = self._svgFillColor() if fill: c, a = fill data["fill"] = c if a != 1: data["fill-opacity"] = a else: data["fill"] = "none" stroke = self._svgStrokeColor() if stroke: c, a = stroke data["stroke"] = c if a != 1: data["stroke-opacity"] = a data["stroke-width"] = formatNumber(abs(self._state.strokeWidth)) if self._state.lineDash: data["stroke-dasharray"] = ",".join([str(i) for i in self._state.lineDash]) if self._state.lineJoin in self._svgLineJoinStylesMap: data["stroke-linejoin"] = self._svgLineJoinStylesMap[self._state.lineJoin] if self._state.lineCap in self._svgLineCapStylesMap: data["stroke-linecap"] = self._svgLineCapStylesMap[self._state.lineCap] return data def _svgFillColor(self): if self._state.fillColor: return self._state.fillColor.svgColor() return None def _svgStrokeColor(self): if self._state.strokeColor: return self._state.strokeColor.svgColor() return None def _svgStyleOpenTypeFeatures(self, featureTags): return ", ".join(["'%s'" % tag for tag in featureTags]) def _svgStyle(self, **kwargs): style = [] if self._state.blendMode is not None: style.append("mix-blend-mode: %s;" % self._state.blendMode) for key, value in kwargs.items(): style.append("%s: %s;" % (key, value)) return " ".join(style) def installFont(self, path): success, error = super(self.__class__, self).installFont(path) # if path not in self._embeddedFonts: # warnings.warn("Your font will be embedded and accessibele") # self._embeddedFonts.add(path) # f = open(path, "r") # fontData = f.read() # f.close() # fontName = self._fontNameForPath(path) # ctx = self._svgContext # ctx.begintag("defs") # ctx.newline() # ctx.begintag("style", type="text/css") # ctx.newline() # ctx.write("@font-face {") # ctx.newline() # ctx.indent() # ctx.write("font-family: %s;" % fontName) # ctx.newline() # if path.startswith("http"): # ctx.write("src: url(%s');" % path) # else: # ctx.write("src: url('data:application/font-woff;charset=utf-8;base64,%s');" % base64.b64encode(fontData)) # ctx.newline() # ctx.dedent() # ctx.write("}") # ctx.newline() # ctx.endtag("style") # ctx.newline() # ctx.endtag("defs") # ctx.newline() return success, error
def writeGlyphToString(glyphName, glyphObject=None, drawPointsFunc=None, writer=None): """Return .glif data for a glyph as a UTF-8 encoded string. The 'glyphObject' argument can be any kind of object (even None); the writeGlyphToString() method will attempt to get the following attributes from it: "width" the advance with of the glyph "unicodes" a list of unicode values for this glyph "note" a string "lib" a dictionary containing custom data All attributes are optional: if 'glyphObject' doesn't have the attribute, it will simply be skipped. To write outline data to the .glif file, writeGlyphToString() needs a function (any callable object actually) that will take one argument: an object that conforms to the PointPen protocol. The function will be called by writeGlyphToString(); it has to call the proper PointPen methods to transfer the outline to the .glif file. """ if writer is None: try: # try the newer location first from fontTools.misc.xmlWriter import XMLWriter except ImportError: from xmlWriter import XMLWriter aFile = StringIO() writer = XMLWriter(aFile, encoding="UTF-8") else: aFile = None writer.begintag("glyph", [("name", glyphName), ("format", "1")]) writer.newline() width = getattr(glyphObject, "width", None) if width is not None: if not isinstance(width, (int, float)): raise GlifLibError, "width attribute must be int or float" writer.simpletag("advance", width=repr(width)) writer.newline() unicodes = getattr(glyphObject, "unicodes", None) if unicodes: if isinstance(unicodes, int): unicodes = [unicodes] for code in unicodes: if not isinstance(code, int): raise GlifLibError, "unicode values must be int" hexCode = hex(code)[2:].upper() if len(hexCode) < 4: hexCode = "0" * (4 - len(hexCode)) + hexCode writer.simpletag("unicode", hex=hexCode) writer.newline() note = getattr(glyphObject, "note", None) if note is not None: if not isinstance(note, (str, unicode)): raise GlifLibError, "note attribute must be str or unicode" note = note.encode('utf-8') writer.begintag("note") writer.newline() for line in note.splitlines(): writer.write(line.strip()) writer.newline() writer.endtag("note") writer.newline() if drawPointsFunc is not None: writer.begintag("outline") writer.newline() pen = GLIFPointPen(writer) drawPointsFunc(pen) writer.endtag("outline") writer.newline() lib = getattr(glyphObject, "lib", None) if lib: from robofab.plistlib import PlistWriter if not isinstance(lib, dict): lib = dict(lib) writer.begintag("lib") writer.newline() plistWriter = PlistWriter(writer.file, indentLevel=writer.indentlevel, indent=writer.indentwhite, writeHeader=False) plistWriter.writeValue(lib) writer.endtag("lib") writer.newline() writer.endtag("glyph") writer.newline() if aFile is not None: return aFile.getvalue() else: return None
class WoffMetaDataWriter(object): def __init__(self, metaDataPath, fontPath): self.writer = XMLWriter(metaDataPath, encoding="UTF-8") self.font = Font(fontPath) self.beginMetaData() self.uniqueId() self.vendor() self.credits() self.description() self.license() self.copyright() self.trademark() self.endMetaData() self.writer.close() def beginMetaData(self): self.writer.begintag("metadata") self.writer.newline() def uniqueId(self): url = self.font.info.get("openTypeNameManufacturerURL") if not url: return reverseUrl = reverseDomain(url) name = "%s%s" % (self.font.info.familyName, self.font.info.styleName) name = name.replace(" ", "") self.writer.simpletag("uniqueid", id="%s.%s" % (reverseUrl, name)) self.writer.newline() def vendor(self): name = self.font.info.get("openTypeNameManufacturer") url = self.font.info.get("openTypeNameManufacturerURL") if not name or not url: return self.writer.simpletag("vendor", name=name, url=url) self.writer.newline() def credits(self): manufacturerName = self.font.info.get("openTypeNameManufacturer") manufacturerUrl = self.font.info.get("openTypeNameManufacturerURL") designerName = self.font.info.get("openTypeNameDesigner") designerUrl = self.font.info.get("openTypeNameDesignerURL") if not manufacturerName and not manufacturerUrl and not designerName and not designerUrl: return self.writer.begintag("credits") self.writer.newline() if manufacturerName and manufacturerUrl: manufacturerName = manufacturerName.encode("utf-8") self.writer.simpletag("credit", name=manufacturerName, url=manufacturerUrl, role="Foundry") self.writer.newline() if designerName and designerUrl: designerName = designerName.encode("utf-8") self.writer.simpletag("credit", name=designerName, url=designerUrl, role="Designer") self.writer.newline() self.writer.endtag("credits") self.writer.newline() def _addData(self, tag, infoAttr, extra=dict()): data = self.font.info.get(infoAttr) if not data: return data = data.encode("utf-8") self.writer.begintag(tag, **extra) self.writer.newline() self.writer.begintag("text", lang="en") self.writer.newline() self.writer.write(data) self.writer.endtag("text") self.writer.newline() self.writer.endtag(tag) self.writer.newline() def description(self): self._addData("description", "openTypeNameDescription") def license(self): extra = dict() licenseUrl = self.font.info.get("openTypeNameLicenseURL") if licenseUrl: extra["url"] = licenseUrl self._addData("license", "openTypeNameLicense", extra) def copyright(self): self._addData("copyright", "copyright") def trademark(self): self._addData("trademark", "trademark") def endMetaData(self): self.writer.endtag("metadata") self.writer.newline()
class Logger(object): def __init__(self): self._file = StringIO() self._writer = XMLWriter(self._file, encoding="utf-8") def __del__(self): self._writer = None self._file.close() def logStart(self): self._writer.begintag("xml") def logEnd(self): self._writer.endtag("xml") def logMainSettings(self, glyphNames, script, langSys): self._writer.begintag("initialSettings") self._writer.newline() self._writer.simpletag("string", value=" ".join(glyphNames)) self._writer.newline() self._writer.simpletag("script", value=script) self._writer.newline() self._writer.simpletag("langSys", value=langSys) self._writer.newline() self._writer.endtag("initialSettings") self._writer.newline() def logTableStart(self, table): name = table.__class__.__name__ self._writer.begintag("table", name=name) self._writer.newline() self.logTableFeatureStates(table) def logTableEnd(self): self._writer.endtag("table") def logTableFeatureStates(self, table): self._writer.begintag("featureStates") self._writer.newline() for tag in sorted(table.getFeatureList()): state = table.getFeatureState(tag) self._writer.simpletag("feature", name=tag, state=int(state)) self._writer.newline() self._writer.endtag("featureStates") self._writer.newline() def logApplicableLookups(self, table, lookups): self._writer.begintag("applicableLookups") self._writer.newline() if lookups: order = [] last = None for tag, lookup in lookups: if tag != last: if order: self._logLookupList(last, order) order = [] last = tag index = table.LookupList.Lookup.index(lookup) order.append(index) self._logLookupList(last, order) self._writer.endtag("applicableLookups") self._writer.newline() def _logLookupList(self, tag, lookups): lookups = " ".join([str(i) for i in lookups]) self._writer.simpletag("lookups", feature=tag, indices=lookups) self._writer.newline() def logProcessingStart(self): self._writer.begintag("processing") self._writer.newline() def logProcessingEnd(self): self._writer.endtag("processing") self._writer.newline() def logLookupStart(self, table, tag, lookup): index = table.LookupList.Lookup.index(lookup) self._writer.begintag("lookup", feature=tag, index=index) self._writer.newline() def logLookupEnd(self): self._writer.endtag("lookup") self._writer.newline() def logSubTableStart(self, lookup, subtable): index = lookup.SubTable.index(subtable) lookupType = subtable.__class__.__name__ self._writer.begintag("subTable", index=index, type=lookupType) self._writer.newline() def logSubTableEnd(self): self._writer.endtag("subTable") self._writer.newline() def logGlyphRecords(self, glyphRecords): for r in glyphRecords: self._writer.simpletag("glyphRecord", name=r.glyphName, xPlacement=r.xPlacement, yPlacement=r.yPlacement, xAdvance=r.xAdvance, yAdvance=r.yAdvance) self._writer.newline() def logInput(self, processed, unprocessed): self._writer.begintag("input") self._writer.newline() self._writer.begintag("processed") self._writer.newline() self.logGlyphRecords(processed) self._writer.endtag("processed") self._writer.newline() self._writer.begintag("unprocessed") self._writer.newline() self.logGlyphRecords(unprocessed) self._writer.endtag("unprocessed") self._writer.newline() self._writer.endtag("input") self._writer.newline() def logOutput(self, processed, unprocessed): self._writer.begintag("output") self._writer.newline() self._writer.begintag("processed") self._writer.newline() self.logGlyphRecords(processed) self._writer.endtag("processed") self._writer.newline() self._writer.begintag("unprocessed") self._writer.newline() self.logGlyphRecords(unprocessed) self._writer.endtag("unprocessed") self._writer.newline() self._writer.endtag("output") self._writer.newline() def logResults(self, processed): self._writer.begintag("results") self._writer.newline() self.logGlyphRecords(processed) self._writer.endtag("results") self._writer.newline() def getText(self): return self._file.getvalue()
class SVGContext(BaseContext): _graphicsStateClass = SVGGraphicsState _shadowClass = SVGShadow _colorClass = SVGColor _gradientClass = SVGGradient _clipPathIDGenerator = _UniqueIDGenerator("clip") _svgFileClass = SVGFile _svgTagArguments = [ ("version", "1.1"), ("xmlns", "http://www.w3.org/2000/svg"), ("xmlns:xlink", "http://www.w3.org/1999/xlink") ] _svgLineJoinStylesMap = { AppKit.NSMiterLineJoinStyle: "miter", AppKit.NSRoundLineJoinStyle: "round", AppKit.NSBevelLineJoinStyle: "bevel" } _svgLineCapStylesMap = { AppKit.NSButtLineCapStyle: "butt", AppKit.NSSquareLineCapStyle: "square", AppKit.NSRoundLineCapStyle: "round", } _svgUnderlineStylesMap = { AppKit.NSUnderlineStyleSingle: "", AppKit.NSUnderlineStyleThick: "", AppKit.NSUnderlineStyleDouble: "double", } indentation = " " fileExtensions = ["svg"] saveImageOptions = [ ("multipage", "Output a numbered svg file for each page or frame in the document."), ] def __init__(self): super(SVGContext, self).__init__() self._pages = [] # not supported in a svg context def cmykFill(self, c, m, y, k, a=1): warnings.warn("cmykFill is not supported in a svg context") def cmykStroke(self, c, m, y, k, a=1): warnings.warn("cmykStroke is not supported in a svg context") def cmykLinearGradient(self, startPoint=None, endPoint=None, colors=None, locations=None): warnings.warn("cmykLinearGradient is not supported in a svg context") def cmykRadialGradient(self, startPoint=None, endPoint=None, colors=None, locations=None, startRadius=0, endRadius=100): warnings.warn("cmykRadialGradient is not supported in a svg context") def cmykShadow(self, offset, blur, color): warnings.warn("cmykShadow is not supported in a svg context") # svg overwrites def shadow(self, offset, blur, color): super(SVGContext, self).shadow(offset, blur, color) if self._state.shadow is not None: self._state.shadow.writeDefs(self._svgContext) def linearGradient(self, startPoint=None, endPoint=None, colors=None, locations=None): super(SVGContext, self).linearGradient(startPoint, endPoint, colors, locations) if self._state.gradient is not None: self._state.gradient.writeDefs(self._svgContext) def radialGradient(self, startPoint=None, endPoint=None, colors=None, locations=None, startRadius=0, endRadius=100): super(SVGContext, self).radialGradient(startPoint, endPoint, colors, locations, startRadius, endRadius) if startRadius != 0: warnings.warn("radialGradient will clip the startRadius to '0' in a svg context.") if self._state.gradient is not None: self._state.gradient.writeDefs(self._svgContext) # svg def _reset(self, other=None): self._embeddedFonts = set() self._embeddedImages = dict() def _newPage(self, width, height): if hasattr(self, "_svgContext"): self._svgContext.endtag("svg") self.reset() self.size(width, height) self._svgData = self._svgFileClass() self._pages.append(self._svgData) self._svgContext = XMLWriter(self._svgData, encoding="utf-8", indentwhite=self.indentation) self._svgContext.width = self.width self._svgContext.height = self.height attrs = [('width', self.width), ('height', self.height), ('viewBox', f"0 0 {self.width} {self.height}")] self._svgContext.begintag("svg", attrs + self._svgTagArguments) self._svgContext.newline() self._state.transformMatrix = self._state.transformMatrix.scale(1, -1).translate(0, -self.height) def _saveImage(self, path, options): multipage = options.get("multipage") if multipage is None: multipage = False self._svgContext.endtag("svg") fileName, fileExt = os.path.splitext(path) firstPage = 0 pageCount = len(self._pages) pathAdd = "_1" if not multipage: firstPage = pageCount - 1 pathAdd = "" for index in range(firstPage, pageCount): page = self._pages[index] svgPath = fileName + pathAdd + fileExt page.writeToFile(svgPath) pathAdd = "_%s" % (index + 2) def _save(self): pass def _restore(self): pass def _drawPath(self): if self._state.path: self._svgBeginClipPath() data = self._svgDrawingAttributes() data["d"] = self._svgPath(self._state.path) if self._state.path.svgID: data["id"] = self._state.path.svgID if self._state.path.svgClass: data["class"] = self._state.path.svgClass data["transform"] = self._svgTransform(self._state.transformMatrix) if self._state.shadow is not None: data["filter"] = "url(#%s)" % self._state.shadow.tagID if self._state.gradient is not None: data["fill"] = "url(#%s)" % self._state.gradient.tagID if self._state.path.svgLink: self._svgContext.begintag("a", **{"xlink:href": self._state.path.svgLink}) self._svgContext.newline() self._svgContext.simpletag("path", **data) self._svgContext.newline() if self._state.path.svgLink: self._svgContext.endtag("a") self._svgContext.newline() self._svgEndClipPath() def _clipPath(self): uniqueID = self._clipPathIDGenerator.gen() self._svgContext.begintag("clipPath", id=uniqueID) self._svgContext.newline() data = dict() data["d"] = self._svgPath(self._state.path) data["transform"] = self._svgTransform(self._state.transformMatrix) data["clip-rule"] = "evenodd" self._svgContext.simpletag("path", **data) self._svgContext.newline() self._svgContext.endtag("clipPath") self._svgContext.newline() self._state.clipPathID = uniqueID def _textBox(self, rawTxt, box, align): path, (x, y) = self._getPathForFrameSetter(box) canDoGradients = True if align == "justified": warnings.warn("justified text is not supported in a svg context") attrString = self.attributedString(rawTxt, align=align) if self._state.hyphenation: attrString = self.hyphenateAttributedString(attrString, path) txt = attrString.string() setter = CoreText.CTFramesetterCreateWithAttributedString(attrString) box = CoreText.CTFramesetterCreateFrame(setter, (0, 0), path, None) self._svgBeginClipPath() defaultData = self._svgDrawingAttributes() data = { "text-anchor": "start", "transform": self._svgTransform(self._state.transformMatrix.translate(x, y + self.height).scale(1, -1)) } if self._state.shadow is not None: data["filter"] = "url(#%s_flipped)" % self._state.shadow.tagID if isinstance(rawTxt, FormattedString): if rawTxt.svgID: data["id"] = rawTxt.svgID if rawTxt.svgClass: data["class"] = rawTxt.svgClass if rawTxt.svgLink: self._svgContext.begintag("a", **{"xlink:href": rawTxt.svgLink}) self._svgContext.newline() self._svgContext.begintag("text", **data) self._svgContext.newline() ctLines = CoreText.CTFrameGetLines(box) origins = CoreText.CTFrameGetLineOrigins(box, (0, len(ctLines)), None) for i, (originX, originY) in enumerate(origins): ctLine = ctLines[i] # bounds = CoreText.CTLineGetImageBounds(ctLine, self._pdfContext) # if bounds.size.width == 0: # continue ctRuns = CoreText.CTLineGetGlyphRuns(ctLine) for ctRun in ctRuns: stringRange = CoreText.CTRunGetStringRange(ctRun) attributes = CoreText.CTRunGetAttributes(ctRun) font = attributes.get(AppKit.NSFontAttributeName) fontDescriptor = font.fontDescriptor() fillColor = attributes.get(AppKit.NSForegroundColorAttributeName) strokeColor = attributes.get(AppKit.NSStrokeColorAttributeName) strokeWidth = attributes.get(AppKit.NSStrokeWidthAttributeName, self._state.strokeWidth) baselineShift = attributes.get(AppKit.NSBaselineOffsetAttributeName, 0) openTypeFeatures = attributes.get("drawbot.openTypeFeatures") underline = attributes.get(AppKit.NSUnderlineStyleAttributeName) url = attributes.get(AppKit.NSLinkAttributeName) fontName = font.fontName() fontSize = font.pointSize() fontFallbacks = [fallbackFont.postscriptName() for fallbackFont in fontDescriptor.get(CoreText.NSFontCascadeListAttribute, [])] fontNames = ", ".join([fontName] + fontFallbacks) style = dict() spanData = dict(defaultData) fill = self._colorClass(fillColor).svgColor() if fill: c, a = fill spanData["fill"] = c if a != 1: spanData["fill-opacity"] = a stroke = self._colorClass(strokeColor).svgColor() if stroke: c, a = stroke spanData["stroke"] = c if a != 1: spanData["stroke-opacity"] = a spanData["stroke-width"] = formatNumber(abs(strokeWidth) * .5) spanData["font-family"] = fontNames spanData["font-size"] = formatNumber(fontSize) if openTypeFeatures: style["font-feature-settings"] = self._svgStyleOpenTypeFeatures(openTypeFeatures) if canDoGradients and self._state.gradient is not None: spanData["fill"] = "url(#%s_flipped)" % self._state.gradient.tagID if underline is not None: style["text-decoration"] = "underline" underlineStyle = self._svgUnderlineStylesMap.get(underline) if underlineStyle: style["text-decoration-style"] = underlineStyle if style: spanData["style"] = self._svgStyle(**style) self._save() runTxt = txt.substringWithRange_((stringRange.location, stringRange.length)) while runTxt and runTxt[-1] == " ": runTxt = runTxt[:-1] runTxt = runTxt.replace("\n", "") runTxt = runTxt.encode("utf-8") runPos = CoreText.CTRunGetPositions(ctRun, (0, 1), None) runX = runY = 0 if runPos: runX = runPos[0].x runY = runPos[0].y spanData["x"] = formatNumber(originX + runX) spanData["y"] = formatNumber(self.height - originY - runY + baselineShift) if url is not None: self._svgContext.begintag("a", href=url.absoluteString()) self._svgContext.newline() self._svgContext.begintag("tspan", **spanData) self._svgContext.newline() self._svgContext.write(runTxt) self._svgContext.newline() self._svgContext.endtag("tspan") self._svgContext.newline() if url is not None: self._svgContext.endtag("a") self._svgContext.newline() self._restore() self._svgContext.endtag("text") self._svgContext.newline() if isinstance(rawTxt, FormattedString) and rawTxt.svgLink: self._svgContext.endtag("a") self._svgContext.newline() self._svgEndClipPath() def _image(self, path, xy, alpha, pageNumber): # todo: # support embedding of images when the source is not a path but # a nsimage or a pdf / gif with a pageNumber x, y = xy self._svgBeginClipPath() if path.startswith("http"): url = AppKit.NSURL.URLWithString_(path) else: url = AppKit.NSURL.fileURLWithPath_(path) image = AppKit.NSImage.alloc().initByReferencingURL_(url) width, height = image.size() if path not in self._embeddedImages: # get a unique id for the image imageID = "image_%s" % (len(self._embeddedImages) + 1) # store it self._embeddedImages[path] = imageID _, ext = os.path.splitext(path) mimeSubtype = ext[1:].lower() # remove the dot, make lowercase if mimeSubtype == "jpg": mimeSubtype = "jpeg" if mimeSubtype not in ("png", "jpeg"): # the image is not a png or a jpeg # convert it to a png mimeSubtype = "png" imageRep = _makeBitmapImageRep(image) imageData = imageRep.representationUsingType_properties_(AppKit.NSPNGFileType, None) else: imageData = AppKit.NSData.dataWithContentsOfURL_(url).bytes() defData = [ ("id", imageID), ("width", width), ("height", height), ("xlink:href", "data:image/%s;base64,%s" % (mimeSubtype, base64.b64encode(imageData).decode("ascii"))) ] self._svgContext.begintag("defs") self._svgContext.newline() self._svgContext.simpletag("image", defData) self._svgContext.newline() self._svgContext.endtag("defs") self._svgContext.newline() else: imageID = self._embeddedImages[path] data = [ ("x", 0), ("y", 0), ("opacity", alpha), ("transform", self._svgTransform(self._state.transformMatrix.translate(x, y + height).scale(1, -1))), ("xlink:href", "#%s" % imageID) ] self._svgContext.simpletag("use", data) self._svgContext.newline() self._svgEndClipPath() def _transform(self, transform): self._state.transformMatrix = self._state.transformMatrix.transform(transform) # helpers def _svgTransform(self, transform): return "matrix(%s)" % (",".join([repr(s) for s in transform])) def _svgPath(self, path, transformMatrix=None): path = path.getNSBezierPath() if transformMatrix: path = path.copy() aT = AppKit.NSAffineTransform.transform() aT.setTransformStruct_(transformMatrix[:]) path.transformUsingAffineTransform_(aT) svg = "" for i in range(path.elementCount()): instruction, points = path.elementAtIndex_associatedPoints_(i) if instruction == AppKit.NSMoveToBezierPathElement: svg += "M%s,%s " % (formatNumber(points[0].x), formatNumber(points[0].y)) previousPoint = points[-1] elif instruction == AppKit.NSLineToBezierPathElement: x = points[0].x - previousPoint.x y = points[0].y - previousPoint.y svg += "l%s,%s " % (formatNumber(x), formatNumber(y)) previousPoint = points[-1] elif instruction == AppKit.NSCurveToBezierPathElement: offx1 = points[0].x - previousPoint.x offy1 = points[0].y - previousPoint.y offx2 = points[1].x - previousPoint.x offy2 = points[1].y - previousPoint.y x = points[2].x - previousPoint.x y = points[2].y - previousPoint.y svg += "c%s,%s,%s,%s,%s,%s " % (formatNumber(offx1), formatNumber(offy1), formatNumber(offx2), formatNumber(offy2), formatNumber(x), formatNumber(y)) previousPoint = points[-1] elif instruction == AppKit.NSClosePathBezierPathElement: svg += "Z " return svg.strip() def _svgBeginClipPath(self): if self._state.clipPathID: data = dict() data["clip-path"] = "url(#%s)" % self._state.clipPathID self._svgContext.begintag("g", **data) self._svgContext.newline() def _svgEndClipPath(self): if self._state.clipPathID: self._svgContext.endtag("g") self._svgContext.newline() def _svgDrawingAttributes(self): data = dict() fill = self._svgFillColor() if fill: c, a = fill data["fill"] = c if a != 1: data["fill-opacity"] = a else: data["fill"] = "none" stroke = self._svgStrokeColor() if stroke: c, a = stroke data["stroke"] = c if a != 1: data["stroke-opacity"] = a data["stroke-width"] = formatNumber(abs(self._state.strokeWidth)) if self._state.lineDash: data["stroke-dasharray"] = ",".join([str(i) for i in self._state.lineDash]) if self._state.lineJoin in self._svgLineJoinStylesMap: data["stroke-linejoin"] = self._svgLineJoinStylesMap[self._state.lineJoin] if self._state.lineCap in self._svgLineCapStylesMap: data["stroke-linecap"] = self._svgLineCapStylesMap[self._state.lineCap] return data def _svgFillColor(self): if self._state.fillColor: return self._state.fillColor.svgColor() return None def _svgStrokeColor(self): if self._state.strokeColor: return self._state.strokeColor.svgColor() return None def _svgStyleOpenTypeFeatures(self, featureTags): return ", ".join(["'%s' %s" % (tag, int(value)) for tag, value in featureTags.items()]) def _svgStyle(self, **kwargs): style = [] if self._state.blendMode is not None: style.append("mix-blend-mode: %s;" % self._state.blendMode) for key, value in sorted(kwargs.items()): style.append("%s: %s;" % (key, value)) return " ".join(style) def _linkURL(self, url, xywh): x, y, w, h = xywh rectData = dict( x=x, y=self.height-y-h, width=w, height=h, fill="transparent", ) self._svgContext.begintag("a", href=url) self._svgContext.newline() self._svgContext.simpletag('rect', **rectData) self._svgContext.newline() self._svgContext.endtag("a") self._svgContext.newline()
class SVGContext(BaseContext): _graphicsStateClass = SVGGraphicsState _shadowClass = SVGShadow _colorClass = SVGColor _gradientClass = SVGGradient _svgFileClass = SVGFile _svgTagArguments = { "version": "1.1", "xmlns": "http://www.w3.org/2000/svg", } _svgLineJoinStylesMap = { AppKit.NSMiterLineJoinStyle: "miter", AppKit.NSRoundLineJoinStyle: "round", AppKit.NSBevelLineJoinStyle: "bevel" } _svgLineCapStylesMap = { AppKit.NSButtLineCapStyle: "butt", AppKit.NSSquareLineCapStyle: "square", AppKit.NSRoundLineCapStyle: "round", } indentation = " " fileExtensions = ["svg"] def __init__(self): super(SVGContext, self).__init__() self._pages = [] # not supported in a svg context def cmykFill(self, c, m, y, k, a=1): warnings.warn("cmykFill is not supported in a svg context") def cmykStroke(self, c, m, y, k, a=1): warnings.warn("cmykStroke is not supported in a svg context") def cmykLinearGradient(self, startPoint=None, endPoint=None, colors=None, locations=None): warnings.warn("cmykLinearGradient is not supported in a svg context") def cmykRadialGradient(self, startPoint=None, endPoint=None, colors=None, locations=None, startRadius=0, endRadius=100): warnings.warn("cmykRadialGradient is not supported in a svg context") def cmykShadow(self, offset, blur, color): warnings.warn("cmykShadow is not supported in a svg context") # svg overwrites def shadow(self, offset, blur, color): super(SVGContext, self).shadow(offset, blur, color) if self._state.shadow is not None: self._state.shadow.writeDefs(self._svgContext) def linearGradient(self, startPoint=None, endPoint=None, colors=None, locations=None): super(SVGContext, self).linearGradient(startPoint, endPoint, colors, locations) if self._state.gradient is not None: self._state.gradient.writeDefs(self._svgContext) def radialGradient(self, startPoint=None, endPoint=None, colors=None, locations=None, startRadius=0, endRadius=100): super(SVGContext, self).radialGradient(startPoint, endPoint, colors, locations, startRadius, endRadius) if startRadius != 0: warnings.warn( "radialGradient will clip the startRadius to '0' in a svg context." ) if self._state.gradient is not None: self._state.gradient.writeDefs(self._svgContext) # svg def _reset(self, other=None): self._embeddedFonts = set() def _newPage(self, width, height): if hasattr(self, "_svgContext"): self._svgContext.endtag("svg") self.reset() self.size(width, height) self._svgData = self._svgFileClass() self._pages.append(self._svgData) self._svgContext = XMLWriter(self._svgData, encoding="utf-8", indentwhite=self.indentation) self._svgContext.width = self.width self._svgContext.height = self.height self._svgContext.begintag("svg", width=self.width, height=self.height, **self._svgTagArguments) self._svgContext.newline() self._state.transformMatrix = self._state.transformMatrix.scale( 1, -1).translate(0, -self.height) def _saveImage(self, path, multipage): if multipage is None: multipage = False self._svgContext.endtag("svg") fileName, fileExt = os.path.splitext(path) firstPage = 0 pageCount = len(self._pages) pathAdd = "_1" if not multipage: firstPage = pageCount - 1 pathAdd = "" for index in range(firstPage, pageCount): page = self._pages[index] svgPath = fileName + pathAdd + fileExt page.writeToFile(svgPath) pathAdd = "_%s" % (index + 2) def _save(self): pass def _restore(self): pass def _drawPath(self): if self._state.path: self._svgBeginClipPath() data = self._svgDrawingAttributes() data["d"] = self._svgPath(self._state.path) data["transform"] = self._svgTransform(self._state.transformMatrix) if self._state.shadow is not None: data["filter"] = "url(#%s)" % self._state.shadow.tagID if self._state.gradient is not None: data["fill"] = "url(#%s)" % self._state.gradient.tagID self._svgContext.simpletag("path", **data) self._svgContext.newline() self._svgEndClipPath() def _clipPath(self): uniqueID = self._getUniqueID() self._svgContext.begintag("clipPath", id=uniqueID) self._svgContext.newline() data = dict() data["d"] = self._svgPath(self._state.path) data["transform"] = self._svgTransform(self._state.transformMatrix) data["clip-rule"] = "evenodd" self._svgContext.simpletag("path", **data) self._svgContext.newline() self._svgContext.endtag("clipPath") self._svgContext.newline() self._state.clipPathID = uniqueID def _textBox(self, txt, box, align): path, (x, y) = self._getPathForFrameSetter(box) canDoGradients = True if align == "justified": warnings.warn("justified text is not supported in a svg context") attrString = self.attributedString(txt, align=align) if self._state.hyphenation: attrString = self.hyphenateAttributedString(attrString, path) txt = attrString.string() setter = CoreText.CTFramesetterCreateWithAttributedString(attrString) box = CoreText.CTFramesetterCreateFrame(setter, (0, 0), path, None) self._svgBeginClipPath() defaultData = self._svgDrawingAttributes() data = { "text-anchor": "start", "transform": self._svgTransform( self._state.transformMatrix.translate(x, y + self.height).scale( 1, -1)) } if self._state.shadow is not None: data["filter"] = "url(#%s_flipped)" % self._state.shadow.tagID self._svgContext.begintag("text", **data) self._svgContext.newline() ctLines = CoreText.CTFrameGetLines(box) origins = CoreText.CTFrameGetLineOrigins(box, (0, len(ctLines)), None) for i, (originX, originY) in enumerate(origins): ctLine = ctLines[i] # bounds = CoreText.CTLineGetImageBounds(ctLine, self._pdfContext) # if bounds.size.width == 0: # continue ctRuns = CoreText.CTLineGetGlyphRuns(ctLine) for ctRun in ctRuns: stringRange = CoreText.CTRunGetStringRange(ctRun) attributes = CoreText.CTRunGetAttributes(ctRun) font = attributes.get(AppKit.NSFontAttributeName) fontAttributes = font.fontDescriptor().fontAttributes() fillColor = attributes.get( AppKit.NSForegroundColorAttributeName) strokeColor = attributes.get(AppKit.NSStrokeColorAttributeName) strokeWidth = attributes.get(AppKit.NSStrokeWidthAttributeName, self._state.strokeWidth) baselineShift = attributes.get( AppKit.NSBaselineOffsetAttributeName, 0) openTypeFeatures = fontAttributes.get( CoreText.NSFontFeatureSettingsAttribute) fontName = font.fontName() fontSize = font.pointSize() spanData = dict(defaultData) fill = self._colorClass(fillColor).svgColor() if fill: c, a = fill spanData["fill"] = c if a != 1: spanData["fill-opacity"] = a stroke = self._colorClass(strokeColor).svgColor() if stroke: c, a = stroke spanData["stroke"] = c if a != 1: spanData["stroke-opacity"] = a spanData["stroke-width"] = formatNumber( abs(strokeWidth) * .5) spanData["font-family"] = fontName spanData["font-size"] = formatNumber(fontSize) if openTypeFeatures: featureTags = getFeatureTagsForFontAttributes( openTypeFeatures) spanData["style"] = self._svgStyle( **{ "font-feature-settings": self._svgStyleOpenTypeFeatures(featureTags) }) if canDoGradients and self._state.gradient is not None: spanData[ "fill"] = "url(#%s_flipped)" % self._state.gradient.tagID self._save() runTxt = txt.substringWithRange_( (stringRange.location, stringRange.length)) while runTxt and runTxt[-1] == " ": runTxt = runTxt[:-1] runTxt = runTxt.replace("\n", "") runTxt = runTxt.encode("utf-8") runPos = CoreText.CTRunGetPositions(ctRun, (0, 1), None) runX = runY = 0 if runPos: runX = runPos[0].x runY = runPos[0].y spanData["x"] = formatNumber(originX + runX) spanData["y"] = formatNumber(self.height - originY - runY + baselineShift) self._svgContext.begintag("tspan", **spanData) self._svgContext.newline() self._svgContext.write(runTxt) self._svgContext.newline() self._svgContext.endtag("tspan") self._svgContext.newline() self._restore() self._svgContext.endtag("text") self._svgContext.newline() self._svgEndClipPath() def _image(self, path, xy, alpha, pageNumber): # todo: # support embedding of images when the source is not a path but # a nsimage or a pdf / gif with a pageNumber x, y = xy self._svgBeginClipPath() if path.startswith("http"): url = AppKit.NSURL.URLWithString_(path) else: url = AppKit.NSURL.fileURLWithPath_(path) im = AppKit.NSImage.alloc().initByReferencingURL_(url) w, h = im.size() data = dict() data["x"] = 0 data["y"] = 0 data["width"] = w data["height"] = h data["opacity"] = alpha data["transform"] = self._svgTransform( self._state.transformMatrix.translate(x, y + h).scale(1, -1)) data["xlink:href"] = path self._svgContext.simpletag("image", **data) self._svgContext.newline() self._svgEndClipPath() def _transform(self, transform): self._state.transformMatrix = self._state.transformMatrix.transform( transform) # helpers def _getUniqueID(self): return uuid.uuid4().hex def _svgTransform(self, transform): return "matrix(%s)" % (",".join([str(s) for s in transform])) def _svgPath(self, path, transformMatrix=None): path = path.getNSBezierPath() if transformMatrix: path = path.copy() aT = AppKit.NSAffineTransform.transform() aT.setTransformStruct_(transformMatrix[:]) path.transformUsingAffineTransform_(aT) svg = "" for i in range(path.elementCount()): instruction, points = path.elementAtIndex_associatedPoints_(i) if instruction == AppKit.NSMoveToBezierPathElement: svg += "M%s,%s " % (formatNumber( points[0].x), formatNumber(points[0].y)) previousPoint = points[-1] elif instruction == AppKit.NSLineToBezierPathElement: x = points[0].x - previousPoint.x y = points[0].y - previousPoint.y svg += "l%s,%s " % (formatNumber(x), formatNumber(y)) previousPoint = points[-1] elif instruction == AppKit.NSCurveToBezierPathElement: offx1 = points[0].x - previousPoint.x offy1 = points[0].y - previousPoint.y offx2 = points[1].x - previousPoint.x offy2 = points[1].y - previousPoint.y x = points[2].x - previousPoint.x y = points[2].y - previousPoint.y svg += "c%s,%s,%s,%s,%s,%s " % ( formatNumber(offx1), formatNumber(offy1), formatNumber(offx2), formatNumber(offy2), formatNumber(x), formatNumber(y)) previousPoint = points[-1] elif instruction == AppKit.NSClosePathBezierPathElement: svg += "Z " return svg.strip() def _svgBeginClipPath(self): if self._state.clipPathID: data = dict() data["clip-path"] = "url(#%s)" % self._state.clipPathID self._svgContext.begintag("g", **data) self._svgContext.newline() def _svgEndClipPath(self): if self._state.clipPathID: self._svgContext.endtag("g") self._svgContext.newline() def _svgDrawingAttributes(self): data = dict() fill = self._svgFillColor() if fill: c, a = fill data["fill"] = c if a != 1: data["fill-opacity"] = a else: data["fill"] = "none" stroke = self._svgStrokeColor() if stroke: c, a = stroke data["stroke"] = c if a != 1: data["stroke-opacity"] = a data["stroke-width"] = formatNumber(abs(self._state.strokeWidth)) if self._state.lineDash: data["stroke-dasharray"] = ",".join( [str(i) for i in self._state.lineDash]) if self._state.lineJoin in self._svgLineJoinStylesMap: data["stroke-linejoin"] = self._svgLineJoinStylesMap[ self._state.lineJoin] if self._state.lineCap in self._svgLineCapStylesMap: data["stroke-linecap"] = self._svgLineCapStylesMap[ self._state.lineCap] return data def _svgFillColor(self): if self._state.fillColor: return self._state.fillColor.svgColor() return None def _svgStrokeColor(self): if self._state.strokeColor: return self._state.strokeColor.svgColor() return None def _svgStyleOpenTypeFeatures(self, featureTags): return ", ".join(["'%s'" % tag for tag in featureTags]) def _svgStyle(self, **kwargs): style = [] if self._state.blendMode is not None: style.append("mix-blend-mode: %s;" % self._state.blendMode) for key, value in kwargs.items(): style.append("%s: %s;" % (key, value)) return " ".join(style) def installFont(self, path): success, error = super(self.__class__, self).installFont(path) # if path not in self._embeddedFonts: # warnings.warn("Your font will be embedded and accessibele") # self._embeddedFonts.add(path) # f = open(path, "r") # fontData = f.read() # f.close() # fontName = self._fontNameForPath(path) # ctx = self._svgContext # ctx.begintag("defs") # ctx.newline() # ctx.begintag("style", type="text/css") # ctx.newline() # ctx.write("@font-face {") # ctx.newline() # ctx.indent() # ctx.write("font-family: %s;" % fontName) # ctx.newline() # if path.startswith("http"): # ctx.write("src: url(%s');" % path) # else: # ctx.write("src: url('data:application/font-woff;charset=utf-8;base64,%s');" % base64.b64encode(fontData)) # ctx.newline() # ctx.dedent() # ctx.write("}") # ctx.newline() # ctx.endtag("style") # ctx.newline() # ctx.endtag("defs") # ctx.newline() return success, error
class SVGContext(BaseContext): _graphicsStateClass = SVGGraphicsState _shadowClass = SVGShadow _colorClass = SVGColor _gradientClass = SVGGradient _svgFileClass = SVGFile _svgTagArguments = { "version": "1.1", "xmlns": "http://www.w3.org/2000/svg", } _svgLineJoinStylesMap = { AppKit.NSMiterLineJoinStyle: "miter", AppKit.NSRoundLineJoinStyle: "round", AppKit.NSBevelLineJoinStyle: "bevel" } _svgLineCapStylesMap = { AppKit.NSButtLineCapStyle: "butt", AppKit.NSSquareLineCapStyle: "square", AppKit.NSRoundLineCapStyle: "round", } indentation = " " fileExtensions = ["svg"] def __init__(self): super(SVGContext, self).__init__() self._pages = [] # not supported in a svg context def cmykFill(self, c, m, y, k, a=1): warnings.warn("cmykFill is not supported in a svg context") def cmykStroke(self, c, m, y, k, a=1): warnings.warn("cmykStroke is not supported in a svg context") def cmykLinearGradient(self, startPoint=None, endPoint=None, colors=None, locations=None): warnings.warn("cmykLinearGradient is not supported in a svg context") def cmykRadialGradient(self, startPoint=None, endPoint=None, colors=None, locations=None, startRadius=0, endRadius=100): warnings.warn("cmykRadialGradient is not supported in a svg context") def cmykShadow(self, offset, blur, color): warnings.warn("cmykShadow is not supported in a svg context") # svg overwrites def shadow(self, offset, blur, color): super(SVGContext, self).shadow(offset, blur, color) if self._state.shadow is not None: self._state.shadow.writeDefs(self._svgContext) def linearGradient(self, startPoint=None, endPoint=None, colors=None, locations=None): super(SVGContext, self).linearGradient(startPoint, endPoint, colors, locations) if self._state.gradient is not None: self._state.gradient.writeDefs(self._svgContext) def radialGradient(self, startPoint=None, endPoint=None, colors=None, locations=None, startRadius=0, endRadius=100): super(SVGContext, self).radialGradient(startPoint, endPoint, colors, locations, startRadius, endRadius) if startRadius != 0: warnings.warn("radialGradient will clip the startRadius to '0' in a svg context.") if self._state.gradient is not None: self._state.gradient.writeDefs(self._svgContext) # svg def _reset(self, other=None): self._embeddedFonts = set() def _newPage(self, width, height): if hasattr(self, "_svgContext"): self._svgContext.endtag("svg") self.reset() self.size(width, height) self._svgData = self._svgFileClass() self._pages.append(self._svgData) self._svgContext = XMLWriter(self._svgData, encoding="utf-8", indentwhite=self.indentation) self._svgContext.width = self.width self._svgContext.height = self.height self._svgContext.begintag("svg", width=self.width, height=self.height, **self._svgTagArguments) self._svgContext.newline() self._state.transformMatrix = self._state.transformMatrix.scale(1, -1).translate(0, -self.height) def _saveImage(self, path, multipage): if multipage is None: multipage = False self._svgContext.endtag("svg") fileName, fileExt = os.path.splitext(path) firstPage = 0 pageCount = len(self._pages) pathAdd = "_1" if not multipage: firstPage = pageCount - 1 pathAdd = "" for index in range(firstPage, pageCount): page = self._pages[index] svgPath = fileName + pathAdd + fileExt page.writeToFile(svgPath) pathAdd = "_%s" % (index + 2) def _save(self): pass def _restore(self): pass def _drawPath(self): if self._state.path: self._svgBeginClipPath() data = self._svgDrawingAttributes() data["d"] = self._svgPath(self._state.path) data["transform"] = self._svgTransform(self._state.transformMatrix) if self._state.shadow is not None: data["filter"] = "url(#%s)" % self._state.shadow.tagID if self._state.gradient is not None: data["fill"] = "url(#%s)" % self._state.gradient.tagID self._svgContext.simpletag("path", **data) self._svgContext.newline() self._svgEndClipPath() def _clipPath(self): uniqueID = self._getUniqueID() self._svgContext.begintag("clipPath", id=uniqueID) self._svgContext.newline() data = dict() data["d"] = self._svgPath(self._state.path) data["transform"] = self._svgTransform(self._state.transformMatrix) data["clip-rule"] = "evenodd" self._svgContext.simpletag("path", **data) self._svgContext.newline() self._svgContext.endtag("clipPath") self._svgContext.newline() self._state.clipPathID = uniqueID def _textBox(self, txt, box, align): path, (x, y) = self._getPathForFrameSetter(box) canDoGradients = True if align == "justified": warnings.warn("justified text is not supported in a svg context") attrString = self.attributedString(txt, align=align) if self._state.hyphenation: attrString = self.hyphenateAttributedString(attrString, path) txt = attrString.string() setter = CoreText.CTFramesetterCreateWithAttributedString(attrString) box = CoreText.CTFramesetterCreateFrame(setter, (0, 0), path, None) self._svgBeginClipPath() defaultData = self._svgDrawingAttributes() data = { "text-anchor": "start", "transform": self._svgTransform(self._state.transformMatrix.translate(x, y + self.height).scale(1, -1)) } if self._state.shadow is not None: data["filter"] = "url(#%s_flipped)" % self._state.shadow.tagID self._svgContext.begintag("text", **data) self._svgContext.newline() ctLines = CoreText.CTFrameGetLines(box) origins = CoreText.CTFrameGetLineOrigins(box, (0, len(ctLines)), None) for i, (originX, originY) in enumerate(origins): ctLine = ctLines[i] # bounds = CoreText.CTLineGetImageBounds(ctLine, self._pdfContext) # if bounds.size.width == 0: # continue ctRuns = CoreText.CTLineGetGlyphRuns(ctLine) for ctRun in ctRuns: stringRange = CoreText.CTRunGetStringRange(ctRun) attributes = CoreText.CTRunGetAttributes(ctRun) font = attributes.get(AppKit.NSFontAttributeName) fontAttributes = font.fontDescriptor().fontAttributes() fillColor = attributes.get(AppKit.NSForegroundColorAttributeName) strokeColor = attributes.get(AppKit.NSStrokeColorAttributeName) strokeWidth = attributes.get(AppKit.NSStrokeWidthAttributeName, self._state.strokeWidth) baselineShift = attributes.get(AppKit.NSBaselineOffsetAttributeName, 0) openTypeFeatures = fontAttributes.get(CoreText.NSFontFeatureSettingsAttribute) fontName = font.fontName() fontSize = font.pointSize() spanData = dict(defaultData) fill = self._colorClass(fillColor).svgColor() if fill: spanData["fill"] = fill stroke = self._colorClass(strokeColor).svgColor() if stroke: spanData["stroke"] = stroke spanData["stroke-width"] = formatNumber(abs(strokeWidth)) spanData["font-family"] = fontName spanData["font-size"] = formatNumber(fontSize) if openTypeFeatures: featureTags = getFeatureTagsForFontAttributes(openTypeFeatures) spanData["style"] = self._svgStyle(**{ "font-feature-settings": self._svgStyleOpenTypeFeatures(featureTags) } ) if canDoGradients and self._state.gradient is not None: spanData["fill"] = "url(#%s_flipped)" % self._state.gradient.tagID self._save() runTxt = txt.substringWithRange_((stringRange.location, stringRange.length)) while runTxt and runTxt[-1] == " ": runTxt = runTxt[:-1] runTxt = runTxt.replace("\n", "") runTxt = runTxt.encode("utf-8") runPos = CoreText.CTRunGetPositions(ctRun, (0, 1), None) runX = runY = 0 if runPos: runX = runPos[0].x runY = runPos[0].y spanData["x"] = formatNumber(originX + runX) spanData["y"] = formatNumber(self.height - originY - runY + baselineShift) self._svgContext.begintag("tspan", **spanData) self._svgContext.newline() self._svgContext.write(runTxt) self._svgContext.newline() self._svgContext.endtag("tspan") self._svgContext.newline() self._restore() self._svgContext.endtag("text") self._svgContext.newline() self._svgEndClipPath() def _image(self, path, (x, y), alpha, pageNumber): # todo: # support embedding of images when the source is not a path but # a nsimage or a pdf / gif with a pageNumber self._svgBeginClipPath() if path.startswith("http"): url = AppKit.NSURL.URLWithString_(path) else: url = AppKit.NSURL.fileURLWithPath_(path) im = AppKit.NSImage.alloc().initByReferencingURL_(url) w, h = im.size() data = dict() data["x"] = 0 data["y"] = 0 data["width"] = w data["height"] = h data["opacity"] = alpha data["transform"] = self._svgTransform(self._state.transformMatrix.translate(x, y + h).scale(1, -1)) data["xlink:href"] = path self._svgContext.simpletag("image", **data) self._svgContext.newline() self._svgEndClipPath()