From bb68f3e59c8469b5f7f1990b08f3ae14972430e9 Mon Sep 17 00:00:00 2001 From: Tal Leming Date: Tue, 27 Dec 2011 04:59:23 +0000 Subject: [PATCH] a place for an experimental rewrite of objectsRF using defcon as the core. if this works, it will merge back into objectsRF. git-svn-id: http://svn.robofab.com/branches/ufo3k@521 b5fa9d6c-a76f-4ffd-b3cb-f825fc41095c --- Lib/robofab/objects/objectsDefcon.py | 1210 ++++++++++++++++++++++++++ 1 file changed, 1210 insertions(+) create mode 100755 Lib/robofab/objects/objectsDefcon.py diff --git a/Lib/robofab/objects/objectsDefcon.py b/Lib/robofab/objects/objectsDefcon.py new file mode 100755 index 000000000..4cb6a51b7 --- /dev/null +++ b/Lib/robofab/objects/objectsDefcon.py @@ -0,0 +1,1210 @@ +""" +This is the new doc for the objectsRF module. +""" + +from robofab import RoboFabError, RoboFabWarning +from robofab.objects.objectsBase import BaseFont, BaseKerning, BaseGroups, BaseInfo, BaseFeatures, BaseLib,\ + BaseGlyph, BaseContour, BaseSegment, BasePoint, BaseBPoint, BaseAnchor, BaseGuide, BaseComponent, \ + relativeBCPIn, relativeBCPOut, absoluteBCPIn, absoluteBCPOut, _box,\ + _interpolate, _interpolatePt, roundPt, addPt,\ + MOVE, LINE, CORNER, CURVE, QCURVE, OFFCURVE,\ + BasePostScriptFontHintValues, postScriptHintDataLibKey, BasePostScriptGlyphHintValues + +import os + + +__all__ = [ "CurrentFont", + "CurrentGlyph", 'OpenFont', + 'RFont', 'RGlyph', 'RContour', + 'RPoint', 'RBPoint', 'RAnchor', + 'RComponent' + ] + + + +def CurrentFont(): + """CurrentFont is not available in objectsRF.""" + return None + +def CurrentGlyph(): + """CurrentGlyph is not available in objectsRF.""" + return None + +def OpenFont(path=None, note=None): + """Open a font from a path. If path is not given, present the user with a dialog (availability of dialogs will vary)""" + if not note: + note = 'select a .ufo directory' + if not path: + from robofab.interface.all.dialogs import GetFolder + path = GetFolder(note) + if path: + try: + return RFont(path) + except OSError: + from robofab.interface.all.dialogs import Message + Message("%s is not a valid .UFO font. But considering it's all XML, why don't you have a look inside with a simple text editor."%(path)) + else: + return None + +def NewFont(familyName=None, styleName=None): + """Make a new font object. Initialise with familyName and styleName.""" + new = RFont() + if familyName is not None: + new.info.familyName = familyName + if styleName is not None: + new.info.styleName = styleName + return new + +def AllFonts(): + """AllFonts is not available in objectsRF.""" + raise NotImplementedError + + +class PostScriptFontHintValues(BasePostScriptFontHintValues): + """ Font level PostScript hints object for objectsRF usage. + + If there are values in the lib, use those. + If there are no values in the lib, use defaults. + + The psHints attribute for objectsRF.RFont is basically just the + data read from the Lib. When the object saves to UFO, the + hints are written back to the lib, which is then saved. + + """ + + def __init__(self, aFont=None, data=None): + self.setParent(aFont) + BasePostScriptFontHintValues.__init__(self) + if aFont is not None: + # in version 1, this data was stored in the lib + # if it is still there, guess that it is correct + # move it to font info and remove it from the lib. + libData = aFont.lib.get(postScriptHintDataLibKey) + if libData is not None: + self.fromDict(libData) + del libData[postScriptHintDataLibKey] + if data is not None: + self.fromDict(data) + +def getPostScriptHintDataFromLib(aFont, fontLib): + hintData = fontLib.get(postScriptHintDataLibKey) + psh = PostScriptFontHintValues(aFont) + psh.fromDict(hintData) + return psh + +class PostScriptGlyphHintValues(BasePostScriptGlyphHintValues): + """ Glyph level PostScript hints object for objectsRF usage. + If there are values in the lib, use those. + If there are no values in the lib, be empty. + + """ + def __init__(self, aGlyph=None, data=None): + # read the data from the glyph.lib, it won't be anywhere else + BasePostScriptGlyphHintValues.__init__(self) + if aGlyph is not None: + self.setParent(aGlyph) + self._loadFromLib(aGlyph.lib) + if data is not None: + self.fromDict(data) + + +class RFont(BaseFont): + """ + Font object representing the data in an UFO. + + - myFontInstace['A'] access the font as a dictionary with glyphnames as key + + """ + + _title = "RoboFabFont" + + def __init__(self, path=None): + BaseFont.__init__(self) + if path is not None: + self._path = os.path.normpath(os.path.abspath(path)) + else: + self._path = None + self._object = {} + + self._glyphSet = None + self._scheduledForDeletion = [] # this is a place for storing glyphs that need to be removed when the font is saved + + self.kerning = RKerning() + self.kerning.setParent(self) + self.info = RInfo() + self.info.setParent(self) + self.features = RFeatures() + self.features.setParent(self) + self.groups = RGroups() + self.groups.setParent(self) + self.lib = RLib() + self.lib.setParent(self) + if path: + self._loadData(path) + else: + self.psHints = PostScriptFontHintValues(self) + self.psHints.setParent(self) + + def __setitem__(self, glyphName, glyph): + """Set a glyph at key.""" + self._object[glyphName] = glyph + + def __cmp__(self, other): + """Compare this font with another, compare if they refer to the same file.""" + if not hasattr(other, '_path'): + return -1 + if self._object._path == other._object._path and self._object._path is not None: + return 0 + else: + return -1 + + def __len__(self): + if self._glyphSet is None: + return 0 + return len(self._glyphSet) + + def _loadData(self, path): + from ufoLib import UFOReader + reader = UFOReader(path) + fontLib = reader.readLib() + # info + reader.readInfo(self.info) + # kerning + self.kerning.update(reader.readKerning()) + self.kerning.setChanged(False) + # groups + self.groups.update(reader.readGroups()) + # features + if reader.formatVersion == 1: + # migrate features from the lib + features = [] + classes = fontLib.get("org.robofab.opentype.classes") + if classes is not None: + del fontLib["org.robofab.opentype.classes"] + features.append(classes) + splitFeatures = fontLib.get("org.robofab.opentype.features") + if splitFeatures is not None: + order = fontLib.get("org.robofab.opentype.featureorder") + if order is None: + order = splitFeatures.keys() + order.sort() + else: + del fontLib["org.robofab.opentype.featureorder"] + del fontLib["org.robofab.opentype.features"] + for tag in order: + oneFeature = splitFeatures.get(tag) + if oneFeature is not None: + features.append(oneFeature) + features = "\n".join(features) + else: + features = reader.readFeatures() + self.features.text = features + # hint data + self.psHints = PostScriptFontHintValues(self) + if postScriptHintDataLibKey in fontLib: + del fontLib[postScriptHintDataLibKey] + # lib + self.lib.update(fontLib) + # glyphs + self._glyphSet = reader.getGlyphSet() + self._hasNotChanged(doGlyphs=False) + + def _loadGlyph(self, glyphName): + """Load a single glyph from the glyphSet, on request.""" + from robofab.pens.rfUFOPen import RFUFOPointPen + g = RGlyph() + g.name = glyphName + pen = RFUFOPointPen(g) + self._glyphSet.readGlyph(glyphName=glyphName, glyphObject=g, pointPen=pen) + g.setParent(self) + g.psHints._loadFromLib(g.lib) + self._object[glyphName] = g + self._object[glyphName]._hasNotChanged() + return g + + #def _prepareSaveDir(self, dir): + # path = os.path.join(dir, 'glyphs') + # if not os.path.exists(path): + # os.makedirs(path) + + def _hasNotChanged(self, doGlyphs=True): + #set the changed state of the font + if doGlyphs: + for glyph in self: + glyph._hasNotChanged() + self.setChanged(False) + + # + # attributes + # + + def _get_path(self): + return self._path + + path = property(_get_path, doc="path of the font") + + # + # methods for imitating GlyphSet? + # + + def keys(self): + # the keys are the superset of self._objects.keys() and + # self._glyphSet.keys(), minus self._scheduledForDeletion + keys = self._object.keys() + if self._glyphSet is not None: + keys.extend(self._glyphSet.keys()) + d = dict() + for glyphName in keys: + d[glyphName] = None + for glyphName in self._scheduledForDeletion: + if glyphName in d: + del d[glyphName] + return d.keys() + + def has_key(self, glyphName): + # XXX ditto, see above. + if self._glyphSet is not None: + hasGlyph = glyphName in self._object or glyphName in self._glyphSet + else: + hasGlyph = glyphName in self._object + return hasGlyph and not glyphName in self._scheduledForDeletion + + __contains__ = has_key + + def getWidth(self, glyphName): + if self._object.has_key(glyphName): + return self._object[glyphName].width + raise IndexError # or return None? + + def getReverseComponentMapping(self): + """ + Get a reversed map of component references in the font. + { + 'A' : ['Aacute', 'Aring'] + 'acute' : ['Aacute'] + 'ring' : ['Aring'] + etc. + } + """ + # a NON-REVERESED map is stored in the lib. + # this is done because a reveresed map could + # contain faulty data. for example: "Aacute" contains + # a component that references "A". Glyph "Aacute" is + # then deleted. The reverse map would still say that + # "A" is referenced by "Aacute" even though the + # glyph has been deleted. So, the stored lib works like this: + # { + # 'Aacute' : [ + # # the last known mod time of the GLIF + # 1098706856.75, + # # component references in a glyph + # ['A', 'acute'] + # ] + # } + import time + import os + import re + componentSearch_RE = re.compile( + "]*?" # anything EXCEPT > + "base\s*=\s*[\"\']" # base=" + "(.*?)" # foo + "[\"\']" # " + ) + rightNow = time.time() + libKey = "org.robofab.componentMapping" + previousMap = None + if self.lib.has_key(libKey): + previousMap = self.lib[libKey] + basicMap = {} + reverseMap = {} + for glyphName in self.keys(): + componentsToMap = None + modTime = None + # get the previous bits of data + previousModTime = None + previousList = None + if previousMap is not None and previousMap.has_key(glyphName): + previousModTime, previousList = previousMap[glyphName] + # the glyph has been loaded. + # simply get the components from it. + if self._object.has_key(glyphName): + componentsToMap = [component.baseGlyph for component in self._object[glyphName].components] + # the glyph has not been loaded. + else: + glyphPath = os.path.join(self._glyphSet.dirName, self._glyphSet.contents[glyphName]) + scanGlyph = True + # test the modified time of the GLIF + fileModTime = os.path.getmtime(glyphPath) + if previousModTime is not None and fileModTime == previousModTime: + # the GLIF almost* certianly has not changed. + # *theoretically, a user could replace a GLIF + # with another GLIF that has precisely the same + # mod time. + scanGlyph = False + componentsToMap = previousList + modTime = previousModTime + else: + # the GLIF is different + modTime = fileModTime + if scanGlyph: + # use regex to extract component + # base glyphs from the file + f = open(glyphPath, 'rb') + data = f.read() + f.close() + componentsToMap = componentSearch_RE.findall(data) + if componentsToMap is not None: + # store the non-reversed map + basicMap[glyphName] = (modTime, componentsToMap) + # reverse the map for the user + if componentsToMap: + for baseGlyphName in componentsToMap: + if not reverseMap.has_key(baseGlyphName): + reverseMap[baseGlyphName] = [] + reverseMap[baseGlyphName].append(glyphName) + # if a glyph has been loaded, we do not store data about it in the lib. + # this is done becuase there is not way to determine the proper mod time + # for a loaded glyph. + if modTime is None: + del basicMap[glyphName] + # store the map in the lib for re-use + self.lib[libKey] = basicMap + return reverseMap + + + def save(self, destDir=None, doProgress=False, formatVersion=2): + """Save the Font in UFO format.""" + # XXX note that when doing "save as" by specifying the destDir argument + # _all_ glyphs get loaded into memory. This could be optimized by either + # copying those .glif files that have not been edited or (not sure how + # well that would work) by simply clearing out self._objects after the + # save. + from ufoLib import UFOWriter + from robofab.tools.fontlabFeatureSplitter import splitFeaturesForFontLab + # if no destination is given, or if + # the given destination is the current + # path, this is not a save as operation + if destDir is None or destDir == self._path: + saveAs = False + destDir = self._path + else: + saveAs = True + # start a progress bar + nonGlyphCount = 5 + bar = None + if doProgress: + from robofab.interface.all.dialogs import ProgressBar + bar = ProgressBar("Exporting UFO", nonGlyphCount + len(self._object.keys())) + # write + writer = UFOWriter(destDir, formatVersion=formatVersion) + try: + # make a shallow copy of the lib. stuff may be added to it. + fontLib = dict(self.lib) + # info + if bar: + bar.label("Saving info...") + writer.writeInfo(self.info) + if bar: + bar.tick() + # kerning + if self.kerning.changed or saveAs: + if bar: + bar.label("Saving kerning...") + writer.writeKerning(self.kerning.asDict()) + if bar: + bar.tick() + # groups + if bar: + bar.label("Saving groups...") + writer.writeGroups(self.groups) + if bar: + bar.tick() + # features + if bar: + bar.label("Saving features...") + features = self.features.text + if features is None: + features = "" + if formatVersion == 2: + writer.writeFeatures(features) + elif formatVersion == 1: + classes, features = splitFeaturesForFontLab(features) + if classes: + fontLib["org.robofab.opentype.classes"] = classes.strip() + "\n" + if features: + featureDict = {} + for featureName, featureText in features: + featureDict[featureName] = featureText.strip() + "\n" + fontLib["org.robofab.opentype.features"] = featureDict + fontLib["org.robofab.opentype.featureorder"] = [featureName for featureName, featureText in features] + if bar: + bar.tick() + # lib + if formatVersion == 1: + fontLib[postScriptHintDataLibKey] = self.psHints.asDict() + if bar: + bar.label("Saving lib...") + writer.writeLib(fontLib) + if bar: + bar.tick() + # glyphs + glyphNameToFileNameFunc = self.getGlyphNameToFileNameFunc() + + glyphSet = writer.getGlyphSet(glyphNameToFileNameFunc) + if len(self._scheduledForDeletion) != 0: + if bar: + bar.label("Removing deleted glyphs...") + for glyphName in self._scheduledForDeletion: + if glyphSet.has_key(glyphName): + glyphSet.deleteGlyph(glyphName) + if bar: + bar.tick() + if bar: + bar.label("Saving glyphs...") + count = nonGlyphCount + if saveAs: + glyphNames = self.keys() + else: + glyphNames = self._object.keys() + for glyphName in glyphNames: + glyph = self[glyphName] + glyph.psHints._saveToLib(glyph.lib) + glyph._saveToGlyphSet(glyphSet, glyphName=glyphName, force=saveAs) + if bar and not count % 10: + bar.tick(count) + count = count + 1 + glyphSet.writeContents() + self._glyphSet = glyphSet + # only blindly stop if the user says to + except KeyboardInterrupt: + bar.close() + bar = None + # kill the progress bar + if bar: + bar.close() + # reset internal stuff + self._path = destDir + self._scheduledForDeletion = [] + self.setChanged(False) + + def newGlyph(self, glyphName, clear=True): + """Make a new glyph with glyphName + if the glyph exists and clear=True clear the glyph""" + if clear and glyphName in self: + g = self[glyphName] + g.clear() + w = self.info.postscriptDefaultWidthX + if w is None: + w = 0 + g.width = w + return g + g = RGlyph() + g.setParent(self) + g.name = glyphName + w = self.info.postscriptDefaultWidthX + if w is None: + w = 0 + g.width = w + g._hasChanged() + self._object[glyphName] = g + # is the user adding a glyph that has the same + # name as one that was deleted earlier? + if glyphName in self._scheduledForDeletion: + self._scheduledForDeletion.remove(glyphName) + return self.getGlyph(glyphName) + + def insertGlyph(self, glyph, name=None): + """returns a new glyph that has been inserted into the font""" + if name is None: + name = glyph.name + glyph = glyph.copy() + glyph.name = name + glyph.setParent(self) + glyph._hasChanged() + self._object[name] = glyph + # is the user adding a glyph that has the same + # name as one that was deleted earlier? + if name in self._scheduledForDeletion: + self._scheduledForDeletion.remove(name) + return self.getGlyph(name) + + def removeGlyph(self, glyphName): + """remove a glyph from the font""" + # XXX! Potential issue with removing glyphs. + # if a glyph is removed from a font, but it is still referenced + # by a component, it will give pens some trouble. + # where does the resposibility for catching this fall? + # the removeGlyph method? the addComponent method + # of the various pens? somewhere else? hm... tricky. + # + #we won't actually remove it, we will just store it for removal + # but only if the glyph does exist + if self.has_key(glyphName) and glyphName not in self._scheduledForDeletion: + self._scheduledForDeletion.append(glyphName) + # now delete the object + if self._object.has_key(glyphName): + del self._object[glyphName] + self._hasChanged() + + def getGlyph(self, glyphName): + # XXX getGlyph may have to become private, to avoid duplication + # with __getitem__ + n = None + if self._object.has_key(glyphName): + # have we served this glyph before? it should be in _object + n = self._object[glyphName] + else: + # haven't served it before, is it in the glyphSet then? + if self._glyphSet is not None and glyphName in self._glyphSet: + # yes, read the .glif file from disk + n = self._loadGlyph(glyphName) + if n is None: + raise KeyError, glyphName + return n + + +class RGlyph(BaseGlyph): + + _title = "RGlyph" + + def __init__(self): + BaseGlyph.__init__(self) + self.contours = [] + self.components = [] + self.anchors = [] + self._unicodes = [] + self.width = 0 + self.note = None + self._name = "Unnamed Glyph" + self.selected = False + self._properties = None + self._lib = RLib() + self._lib.setParent(self) + self.psHints = PostScriptGlyphHintValues() + self.psHints.setParent(self) + + def __len__(self): + return len(self.contours) + + def __getitem__(self, index): + if index < len(self.contours): + return self.contours[index] + raise IndexError + + def _hasNotChanged(self): + for contour in self.contours: + contour.setChanged(False) + for segment in contour.segments: + segment.setChanged(False) + for point in segment.points: + point.setChanged(False) + for component in self.components: + component.setChanged(False) + for anchor in self.anchors: + anchor.setChanged(False) + self.setChanged(False) + + # + # attributes + # + + def _get_lib(self): + return self._lib + + def _set_lib(self, obj): + self._lib.clear() + self._lib.update(obj) + + lib = property(_get_lib, _set_lib) + + def _get_name(self): + return self._name + + def _set_name(self, value): + prevName = self._name + newName = value + if newName == prevName: + return + self._name = newName + self.setChanged(True) + font = self.getParent() + if font is not None: + # but, this glyph could be linked to a + # FontLab font, because objectsFL.RGlyph.copy() + # creates an objectsRF.RGlyph with the parent + # set to an objectsFL.RFont object. so, check to see + # if this is a legitimate RFont before trying to + # do the objectsRF.RFont glyph name change + if isinstance(font, RFont): + font._object[newName] = self + # is the user changing a glyph's name to the + # name of a glyph that was deleted earlier? + if newName in font._scheduledForDeletion: + font._scheduledForDeletion.remove(newName) + font.removeGlyph(prevName) + + name = property(_get_name, _set_name) + + def _get_unicodes(self): + return self._unicodes + + def _set_unicodes(self, value): + if not isinstance(value, list): + raise RoboFabError, "unicodes must be a list" + self._unicodes = value + self._hasChanged() + + unicodes = property(_get_unicodes, _set_unicodes, doc="all unicode values for the glyph") + + def _get_unicode(self): + if len(self._unicodes) == 0: + return None + return self._unicodes[0] + + def _set_unicode(self, value): + uni = self._unicodes + if value is not None: + if value not in uni: + self.unicodes.insert(0, value) + elif uni.index(value) != 0: + uni.insert(0, uni.pop(uni.index(value))) + self.unicodes = uni + + unicode = property(_get_unicode, _set_unicode, doc="first unicode value for the glyph") + + def getPointPen(self): + from robofab.pens.rfUFOPen import RFUFOPointPen + return RFUFOPointPen(self) + + def appendComponent(self, baseGlyph, offset=(0, 0), scale=(1, 1)): + """append a component to the glyph""" + new = RComponent(baseGlyph, offset, scale) + new.setParent(self) + self.components.append(new) + self._hasChanged() + + def appendAnchor(self, name, position, mark=None): + """append an anchor to the glyph""" + new = RAnchor(name, position, mark) + new.setParent(self) + self.anchors.append(new) + self._hasChanged() + + def removeContour(self, index): + """remove a specific contour from the glyph""" + del self.contours[index] + self._hasChanged() + + def removeAnchor(self, anchor): + """remove a specific anchor from the glyph""" + del self.anchors[anchor.index] + self._hasChanged() + + def removeComponent(self, component): + """remove a specific component from the glyph""" + del self.components[component.index] + self._hasChanged() + + def center(self, padding=None): + """Equalise sidebearings, set to padding if wanted.""" + left = self.leftMargin + right = self.rightMargin + if padding: + e_left = e_right = padding + else: + e_left = (left + right)/2 + e_right = (left + right) - e_left + self.leftMargin = e_left + self.rightMargin = e_right + + def decompose(self): + """Decompose all components""" + for i in range(len(self.components)): + self.components[-1].decompose() + self._hasChanged() + + def clear(self, contours=True, components=True, anchors=True, guides=True): + """Clear all items marked as True from the glyph""" + if contours: + self.clearContours() + if components: + self.clearComponents() + if anchors: + self.clearAnchors() + if guides: + self.clearHGuides() + self.clearVGuides() + + def clearContours(self): + """clear all contours""" + self.contours = [] + self._hasChanged() + + def clearComponents(self): + """clear all components""" + self.components = [] + self._hasChanged() + + def clearAnchors(self): + """clear all anchors""" + self.anchors = [] + self._hasChanged() + + def clearHGuides(self): + """clear all horizontal guides""" + self.hGuides = [] + self._hasChanged() + + def clearVGuides(self): + """clear all vertical guides""" + self.vGuides = [] + self._hasChanged() + + def getAnchors(self): + return self.anchors + + def getComponents(self): + return self.components + + # + # stuff related to Glyph Properties + # + + + +class RContour(BaseContour): + + _title = "RoboFabContour" + + def __init__(self, object=None): + #BaseContour.__init__(self) + self.segments = [] + self.selected = False + + def __len__(self): + return len(self.segments) + + def __getitem__(self, index): + if index < len(self.segments): + return self.segments[index] + raise IndexError + + def _get_index(self): + return self.getParent().contours.index(self) + + def _set_index(self, index): + ogIndex = self.index + if index != ogIndex: + contourList = self.getParent().contours + contourList.insert(index, contourList.pop(ogIndex)) + + + index = property(_get_index, _set_index, doc="index of the contour") + + def _get_points(self): + points = [] + for segment in self.segments: + for point in segment.points: + points.append(point) + return points + + points = property(_get_points, doc="view the contour as a list of points") + + def _get_bPoints(self): + bPoints = [] + for segment in self.segments: + segType = segment.type + if segType == MOVE: + bType = CORNER + elif segType == LINE: + bType = CORNER + elif segType == CURVE: + if segment.smooth: + bType = CURVE + else: + bType = CORNER + else: + raise RoboFabError, "encountered unknown segment type" + b = RBPoint() + b.setParent(segment) + bPoints.append(b) + return bPoints + + bPoints = property(_get_bPoints, doc="view the contour as a list of bPoints") + + def appendSegment(self, segmentType, points, smooth=False): + """append a segment to the contour""" + segment = self.insertSegment(index=len(self.segments), segmentType=segmentType, points=points, smooth=smooth) + return segment + + def insertSegment(self, index, segmentType, points, smooth=False): + """insert a segment into the contour""" + segment = RSegment(segmentType, points, smooth) + segment.setParent(self) + self.segments.insert(index, segment) + self._hasChanged() + return segment + + def removeSegment(self, index): + """remove a segment from the contour""" + del self.segments[index] + self._hasChanged() + + def reverseContour(self): + """reverse the contour""" + from robofab.pens.reverseContourPointPen import ReverseContourPointPen + index = self.index + glyph = self.getParent() + pen = glyph.getPointPen() + reversePen = ReverseContourPointPen(pen) + self.drawPoints(reversePen) + # we've drawn the reversed contour onto our parent glyph, + # so it sits at the end of the contours list: + newContour = glyph.contours.pop(-1) + for segment in newContour.segments: + segment.setParent(self) + self.segments = newContour.segments + self._hasChanged() + + def setStartSegment(self, segmentIndex): + """set the first segment on the contour""" + # this obviously does not support open contours + if len(self.segments) < 2: + return + if segmentIndex == 0: + return + if segmentIndex > len(self.segments)-1: + raise IndexError, 'segment index not in segments list' + oldStart = self.segments[0] + oldLast = self.segments[-1] + #check to see if the contour ended with a curve on top of the move + #if we find one delete it, + if oldLast.type == CURVE or oldLast.type == QCURVE: + startOn = oldStart.onCurve + lastOn = oldLast.onCurve + if startOn.x == lastOn.x and startOn.y == lastOn.y: + del self.segments[0] + # since we deleted the first contour, the segmentIndex needs to shift + segmentIndex = segmentIndex - 1 + # if we DO have a move left over, we need to convert it to a line + if self.segments[0].type == MOVE: + self.segments[0].type = LINE + # slice up the segments and reassign them to the contour + segments = self.segments[segmentIndex:] + self.segments = segments + self.segments[:segmentIndex] + # now, draw the contour onto the parent glyph + glyph = self.getParent() + pen = glyph.getPointPen() + self.drawPoints(pen) + # we've drawn the new contour onto our parent glyph, + # so it sits at the end of the contours list: + newContour = glyph.contours.pop(-1) + for segment in newContour.segments: + segment.setParent(self) + self.segments = newContour.segments + self._hasChanged() + + +class RSegment(BaseSegment): + + _title = "RoboFabSegment" + + def __init__(self, segmentType=None, points=[], smooth=False): + BaseSegment.__init__(self) + self.selected = False + self.points = [] + self.smooth = smooth + if points: + #the points in the segment should be RPoints, so create those objects + for point in points[:-1]: + x, y = point + p = RPoint(x, y, pointType=OFFCURVE) + p.setParent(self) + self.points.append(p) + aX, aY = points[-1] + p = RPoint(aX, aY, segmentType) + p.setParent(self) + self.points.append(p) + + def _get_type(self): + return self.points[-1].type + + def _set_type(self, pointType): + onCurve = self.points[-1] + ocType = onCurve.type + if ocType == pointType: + return + #we are converting a cubic line into a cubic curve + if pointType == CURVE and ocType == LINE: + onCurve.type = pointType + parent = self.getParent() + prev = parent._prevSegment(self.index) + p1 = RPoint(prev.onCurve.x, prev.onCurve.y, pointType=OFFCURVE) + p1.setParent(self) + p2 = RPoint(onCurve.x, onCurve.y, pointType=OFFCURVE) + p2.setParent(self) + self.points.insert(0, p2) + self.points.insert(0, p1) + #we are converting a cubic move to a curve + elif pointType == CURVE and ocType == MOVE: + onCurve.type = pointType + parent = self.getParent() + prev = parent._prevSegment(self.index) + p1 = RPoint(prev.onCurve.x, prev.onCurve.y, pointType=OFFCURVE) + p1.setParent(self) + p2 = RPoint(onCurve.x, onCurve.y, pointType=OFFCURVE) + p2.setParent(self) + self.points.insert(0, p2) + self.points.insert(0, p1) + #we are converting a quad curve to a cubic curve + elif pointType == CURVE and ocType == QCURVE: + onCurve.type == CURVE + #we are converting a cubic curve into a cubic line + elif pointType == LINE and ocType == CURVE: + p = self.points.pop(-1) + self.points = [p] + onCurve.type = pointType + self.smooth = False + #we are converting a cubic move to a line + elif pointType == LINE and ocType == MOVE: + onCurve.type = pointType + #we are converting a quad curve to a line: + elif pointType == LINE and ocType == QCURVE: + p = self.points.pop(-1) + self.points = [p] + onCurve.type = pointType + self.smooth = False + # we are converting to a quad curve where just about anything is legal + elif pointType == QCURVE: + onCurve.type = pointType + else: + raise RoboFabError, 'unknown segment type' + + type = property(_get_type, _set_type, doc="type of the segment") + + def _get_index(self): + return self.getParent().segments.index(self) + + index = property(_get_index, doc="index of the segment") + + def insertPoint(self, index, pointType, point): + x, y = point + p = RPoint(x, y, pointType=pointType) + p.setParent(self) + self.points.insert(index, p) + self._hasChanged() + + def removePoint(self, index): + del self.points[index] + self._hasChanged() + + +class RBPoint(BaseBPoint): + + _title = "RoboFabBPoint" + + def _setAnchorChanged(self, value): + self._anchorPoint.setChanged(value) + + def _setNextChanged(self, value): + self._nextOnCurve.setChanged(value) + + def _get__parentSegment(self): + return self.getParent() + + _parentSegment = property(_get__parentSegment, doc="") + + def _get__nextOnCurve(self): + pSeg = self._parentSegment + contour = pSeg.getParent() + #could this potentially return an incorrect index? say, if two segments are exactly the same? + return contour.segments[(contour.segments.index(pSeg) + 1) % len(contour.segments)] + + _nextOnCurve = property(_get__nextOnCurve, doc="") + + def _get_index(self): + return self._parentSegment.index + + index = property(_get_index, doc="index of the bPoint on the contour") + + +class RPoint(BasePoint): + + _title = "RoboFabPoint" + + def __init__(self, x=0, y=0, pointType=None, name=None): + self.selected = False + self._type = pointType + self._x = x + self._y = y + self._name = None + + def _get_x(self): + return self._x + + def _set_x(self, value): + self._x = value + self._hasChanged() + + x = property(_get_x, _set_x, doc="") + + def _get_y(self): + return self._y + + def _set_y(self, value): + self._y = value + self._hasChanged() + + y = property(_get_y, _set_y, doc="") + + def _get_type(self): + return self._type + + def _set_type(self, value): + self._type = value + self._hasChanged() + + type = property(_get_type, _set_type, doc="") + + def _get_name(self): + return self._name + + def _set_name(self, value): + self._name = value + self._hasChanged() + + name = property(_get_name, _set_name, doc="") + + +class RAnchor(BaseAnchor): + + _title = "RoboFabAnchor" + + def __init__(self, name=None, position=None, mark=None): + BaseAnchor.__init__(self) + self.selected = False + self.name = name + if position is None: + self.x = self.y = None + else: + self.x, self.y = position + self.mark = mark + + def _get_index(self): + if self.getParent() is None: return None + return self.getParent().anchors.index(self) + + index = property(_get_index, doc="index of the anchor") + + def _get_position(self): + return (self.x, self.y) + + def _set_position(self, value): + self.x = value[0] + self.y = value[1] + self._hasChanged() + + position = property(_get_position, _set_position, doc="position of the anchor") + + def move(self, (x, y)): + """Move the anchor""" + self.x = self.x + x + self.y = self.y + y + self._hasChanged() + + +class RComponent(BaseComponent): + + _title = "RoboFabComponent" + + def __init__(self, baseGlyphName=None, offset=(0,0), scale=(1,1)): + BaseComponent.__init__(self) + self.selected = False + self._baseGlyph = baseGlyphName + self._offset = offset + self._scale = scale + + def _get_index(self): + if self.getParent() is None: return None + return self.getParent().components.index(self) + + index = property(_get_index, doc="index of the component") + + def _get_baseGlyph(self): + return self._baseGlyph + + def _set_baseGlyph(self, glyphName): + # XXXX needs to be implemented in objectsFL for symmetricity's sake. Eventually. + self._baseGlyph = glyphName + self._hasChanged() + + baseGlyph = property(_get_baseGlyph, _set_baseGlyph, doc="") + + def _get_offset(self): + return self._offset + + def _set_offset(self, value): + self._offset = value + self._hasChanged() + + offset = property(_get_offset, _set_offset, doc="the offset of the component") + + def _get_scale(self): + return self._scale + + def _set_scale(self, (x, y)): + self._scale = (x, y) + self._hasChanged() + + scale = property(_get_scale, _set_scale, doc="the scale of the component") + + def move(self, (x, y)): + """Move the component""" + self.offset = (self.offset[0] + x, self.offset[1] + y) + + def decompose(self): + """Decompose the component""" + baseGlyphName = self.baseGlyph + parentGlyph = self.getParent() + # if there is no parent glyph, there is nothing to decompose to + if baseGlyphName is not None and parentGlyph is not None: + parentFont = parentGlyph.getParent() + # we must have a parent glyph with the baseGlyph + # if not, we will simply remove the component from + # the parent glyph thereby decomposing the component + # to nothing. + if parentFont is not None and parentFont.has_key(baseGlyphName): + from robofab.pens.adapterPens import TransformPointPen + oX, oY = self.offset + sX, sY = self.scale + baseGlyph = parentFont[baseGlyphName] + for contour in baseGlyph.contours: + pointPen = parentGlyph.getPointPen() + transPen = TransformPointPen(pointPen, (sX, 0, 0, sY, oX, oY)) + contour.drawPoints(transPen) + parentGlyph.components.remove(self) + + +class RKerning(BaseKerning): + + _title = "RoboFabKerning" + + +class RGroups(BaseGroups): + + _title = "RoboFabGroups" + +class RLib(BaseLib): + + _title = "RoboFabLib" + + +class RInfo(BaseInfo): + + _title = "RoboFabFontInfo" + +class RFeatures(BaseFeatures): + + _title = "RoboFabFeatures" +