| 1 | # FLM: UFO Central |
|---|
| 2 | """Export and import UFOs with metadata. Version 0.2""" |
|---|
| 3 | |
|---|
| 4 | import os |
|---|
| 5 | import glob |
|---|
| 6 | from robofab.pens.pointPen import AbstractPointPen |
|---|
| 7 | from robofab.world import AllFonts, OpenFont, NewFont |
|---|
| 8 | from robofab.interface.all.dialogs import GetFolder |
|---|
| 9 | from dialogKit import * |
|---|
| 10 | from FL import * |
|---|
| 11 | import fl_cmd |
|---|
| 12 | |
|---|
| 13 | |
|---|
| 14 | class InstructionPointPen(AbstractPointPen): |
|---|
| 15 | |
|---|
| 16 | def __init__(self): |
|---|
| 17 | self._instructions = [] |
|---|
| 18 | |
|---|
| 19 | def beginPath(self): |
|---|
| 20 | d = { |
|---|
| 21 | "method":"beginPath" |
|---|
| 22 | } |
|---|
| 23 | self._instructions.append(d) |
|---|
| 24 | |
|---|
| 25 | def endPath(self): |
|---|
| 26 | d = { |
|---|
| 27 | "method":"endPath" |
|---|
| 28 | } |
|---|
| 29 | self._instructions.append(d) |
|---|
| 30 | |
|---|
| 31 | def addPoint(self, pt, segmentType=None, smooth=False, name=None, **kwargs): |
|---|
| 32 | d = { |
|---|
| 33 | "method":"addPoint", |
|---|
| 34 | "pt":pt, |
|---|
| 35 | } |
|---|
| 36 | if segmentType is not None: |
|---|
| 37 | d["segmentType"] = segmentType |
|---|
| 38 | if smooth is not None: |
|---|
| 39 | d["smooth"] = smooth |
|---|
| 40 | if name is not None: |
|---|
| 41 | d["name"] = name |
|---|
| 42 | self._instructions.append(d) |
|---|
| 43 | |
|---|
| 44 | def addComponent(self, baseGlyphName, transformation): |
|---|
| 45 | d = { |
|---|
| 46 | "method":"addComponent", |
|---|
| 47 | "baseGlyphName":baseGlyphName, |
|---|
| 48 | "transformation":transformation |
|---|
| 49 | } |
|---|
| 50 | |
|---|
| 51 | def getInstructions(self): |
|---|
| 52 | # filter out any single point contours (anchors) |
|---|
| 53 | instructions = [] |
|---|
| 54 | pointStack = [] |
|---|
| 55 | for instruction in self._instructions: |
|---|
| 56 | pointStack.append(instruction) |
|---|
| 57 | if instruction["method"] == "endPath": |
|---|
| 58 | if pointStack: |
|---|
| 59 | if len(pointStack) > 3: |
|---|
| 60 | instructions.extend(pointStack) |
|---|
| 61 | pointStack = [] |
|---|
| 62 | return instructions |
|---|
| 63 | |
|---|
| 64 | |
|---|
| 65 | def _drawPointStack(stack, pointPen): |
|---|
| 66 | for instruction in stack: |
|---|
| 67 | meth = instruction["method"] |
|---|
| 68 | if meth == "beginPath": |
|---|
| 69 | pointPen.beginPath() |
|---|
| 70 | elif meth == "endPath": |
|---|
| 71 | pointPen.endPath() |
|---|
| 72 | elif meth == "addPoint": |
|---|
| 73 | pt = instruction["pt"] |
|---|
| 74 | smooth = instruction.get("smooth") |
|---|
| 75 | segmentType = instruction.get("segmentType") |
|---|
| 76 | name = instruction.get("name") |
|---|
| 77 | pointPen.addPoint(pt, segmentType, smooth, name) |
|---|
| 78 | elif meth == "addComponent": |
|---|
| 79 | baseGlyphName = instruction["baseGlyphName"] |
|---|
| 80 | transformation = instruction["transformation"] |
|---|
| 81 | pointPen.addComponent(baseGlyphName, transformation) |
|---|
| 82 | else: |
|---|
| 83 | raise NotImplementedError, meth |
|---|
| 84 | |
|---|
| 85 | def instructionsDrawPoints(instructions, pointPen): |
|---|
| 86 | """draw instructions created by InstructionPointPen""" |
|---|
| 87 | # filter out single point contours (anchors) |
|---|
| 88 | pointStack = [] |
|---|
| 89 | for instruction in instructions: |
|---|
| 90 | pointStack.append(instruction) |
|---|
| 91 | meth = instruction["method"] |
|---|
| 92 | if meth == "endPath": |
|---|
| 93 | if len(pointStack) > 3: |
|---|
| 94 | _drawPointStack(pointStack, pointPen) |
|---|
| 95 | pointStack = [] |
|---|
| 96 | |
|---|
| 97 | |
|---|
| 98 | MASK_LIB_KEY = "org.robofab.fontlab.maskData" |
|---|
| 99 | MARK_LIB_KEY = "org.robofab.fontlab.mark" |
|---|
| 100 | |
|---|
| 101 | |
|---|
| 102 | def exportUFOWithMetaData(font, ufoPath=None, doMask=True, doMark=True, doProgress=False): |
|---|
| 103 | from robofab.pens.digestPen import DigestPointPen |
|---|
| 104 | # make sure that the font is the top most font |
|---|
| 105 | fl.ifont = font.fontIndex |
|---|
| 106 | # the mask |
|---|
| 107 | if doMask or doMark: |
|---|
| 108 | for glyph in font: |
|---|
| 109 | if doMask: |
|---|
| 110 | # open a glyph window |
|---|
| 111 | fl.EditGlyph(glyph.index) |
|---|
| 112 | # switch to the mask layer |
|---|
| 113 | fl.CallCommand(fl_cmd.ViewEditMask) |
|---|
| 114 | # if the mask is empty, skip this step |
|---|
| 115 | if not len(glyph): |
|---|
| 116 | # switch back to the edit layer |
|---|
| 117 | fl.CallCommand(fl_cmd.ViewEditMask) |
|---|
| 118 | continue |
|---|
| 119 | # get the mask data |
|---|
| 120 | pen = InstructionPointPen() |
|---|
| 121 | glyph.drawPoints(pen) |
|---|
| 122 | # switch back to the edit layer |
|---|
| 123 | fl.CallCommand(fl_cmd.ViewEditMask) |
|---|
| 124 | # write the mask data to the glyph lib |
|---|
| 125 | instructions = pen.getInstructions() |
|---|
| 126 | if instructions: |
|---|
| 127 | glyph.lib[MASK_LIB_KEY] = instructions |
|---|
| 128 | if doMark: |
|---|
| 129 | mark = glyph.mark |
|---|
| 130 | glyph.lib[MARK_LIB_KEY] = mark |
|---|
| 131 | # close all glyph windows. sometimes this actually works. |
|---|
| 132 | fl.CallCommand(fl_cmd.WindowCloseAllGlyphWindows) |
|---|
| 133 | # export the UFO |
|---|
| 134 | font.writeUFO(ufoPath, doProgress=doProgress) |
|---|
| 135 | # clear the data from the glyph lib |
|---|
| 136 | if doMask or doMark: |
|---|
| 137 | for glyph in font: |
|---|
| 138 | lib = glyph.lib |
|---|
| 139 | if lib.has_key(MASK_LIB_KEY): |
|---|
| 140 | del lib[MASK_LIB_KEY] |
|---|
| 141 | if lib.has_key(MARK_LIB_KEY): |
|---|
| 142 | del lib[MARK_LIB_KEY] |
|---|
| 143 | glyph.update() |
|---|
| 144 | # |
|---|
| 145 | font.update() |
|---|
| 146 | |
|---|
| 147 | def importUFOWithMetaData(font, ufoPath, doMask=True, doMark=True, doKerning=True, doProgress=False): |
|---|
| 148 | # make sure that the font is the top most font |
|---|
| 149 | fl.ifont = font.fontIndex |
|---|
| 150 | # import the UFO |
|---|
| 151 | font.readUFO(ufoPath, doProgress=doProgress) |
|---|
| 152 | # add the mask & mark data |
|---|
| 153 | if doMask or doMark: |
|---|
| 154 | for glyph in font: |
|---|
| 155 | lib = glyph.lib |
|---|
| 156 | # mask |
|---|
| 157 | if doMask: |
|---|
| 158 | if lib.has_key(MASK_LIB_KEY): |
|---|
| 159 | # open a glyph window |
|---|
| 160 | fl.EditGlyph(glyph.index) |
|---|
| 161 | # switch to the mask layer |
|---|
| 162 | fl.CallCommand(fl_cmd.ViewEditMask) |
|---|
| 163 | # add the mask data |
|---|
| 164 | instructions = lib[MASK_LIB_KEY] |
|---|
| 165 | pen = glyph.getPointPen() |
|---|
| 166 | instructionsDrawPoints(instructions, pen) |
|---|
| 167 | # switch back to the edit layer |
|---|
| 168 | fl.CallCommand(fl_cmd.ViewEditMask) |
|---|
| 169 | # clear the mask data from the glyph lib |
|---|
| 170 | del lib[MASK_LIB_KEY] |
|---|
| 171 | # mark |
|---|
| 172 | if doMark: |
|---|
| 173 | if lib.has_key(MARK_LIB_KEY): |
|---|
| 174 | glyph.mark = lib[MARK_LIB_KEY] |
|---|
| 175 | glyph.update() |
|---|
| 176 | # close all glyph windows. sometimes this actually works. |
|---|
| 177 | fl.CallCommand(fl_cmd.WindowCloseAllGlyphWindows) |
|---|
| 178 | # clear the kerning if it is not to be imported |
|---|
| 179 | if not doKerning: |
|---|
| 180 | font.kerning.clear() |
|---|
| 181 | # |
|---|
| 182 | font.update() |
|---|
| 183 | |
|---|
| 184 | |
|---|
| 185 | class UFOCentral(object): |
|---|
| 186 | |
|---|
| 187 | def __init__(self): |
|---|
| 188 | self.files = {} |
|---|
| 189 | self.mode = "export" |
|---|
| 190 | # |
|---|
| 191 | self.w = ModalDialog((375, 350), "UFO Central", okCallback=self.okCallback) |
|---|
| 192 | self.w.fileList = List((12, 12, 200, -60), []) |
|---|
| 193 | # |
|---|
| 194 | self.w.doExportCheckBox = CheckBox((220, 12, -12, 20), "Export UFOs", callback=self.doExportCallback, value=True) |
|---|
| 195 | self.w.doImportCheckBox = CheckBox((220, 35, -12, 20), "Import UFOs", callback=self.doImportCallback) |
|---|
| 196 | # |
|---|
| 197 | self.w.selectFileOrFolderButton = Button((220, 75, -12, 20), "Select file or folder", callback=self.selectFileOrFolderButtonCallback) |
|---|
| 198 | self.w.selectAllOpenButton = Button((220, 105, -12, 20), "All open fonts", callback=self.selectAllOpenCallback) |
|---|
| 199 | # |
|---|
| 200 | self.w.doMaskCheckBox = CheckBox((220, 145, -12, 20), "Process masks", value=True) |
|---|
| 201 | self.w.doMarkCheckBox = CheckBox((220, 170, -12, 20), "Process marks", value=True) |
|---|
| 202 | self.w.doKerningCheckBox = CheckBox((220, 195, -12, 20), "Import kerning") |
|---|
| 203 | self.w.doKerningCheckBox.enable(False) |
|---|
| 204 | # |
|---|
| 205 | self.w.open() |
|---|
| 206 | |
|---|
| 207 | def _modeChange(self): |
|---|
| 208 | self.files = {} |
|---|
| 209 | self.w.fileList.set([]) |
|---|
| 210 | if self.mode == "export": |
|---|
| 211 | self.w.selectAllOpenButton.enable(True) |
|---|
| 212 | self.w.doExportCheckBox.set(True) |
|---|
| 213 | self.w.doImportCheckBox.set(False) |
|---|
| 214 | self.w.doKerningCheckBox.enable(False) |
|---|
| 215 | else: |
|---|
| 216 | self.w.selectAllOpenButton.enable(False) |
|---|
| 217 | self.w.doExportCheckBox.set(False) |
|---|
| 218 | self.w.doImportCheckBox.set(True) |
|---|
| 219 | self.w.doKerningCheckBox.enable(True) |
|---|
| 220 | |
|---|
| 221 | def _updateFileList(self): |
|---|
| 222 | fileNames = [os.path.basename(p) for p in self.files.keys()] |
|---|
| 223 | fileNames.sort() |
|---|
| 224 | self.w.fileList.set(fileNames) |
|---|
| 225 | |
|---|
| 226 | def doExportCallback(self, sender): |
|---|
| 227 | if sender.get(): |
|---|
| 228 | self.mode = "export" |
|---|
| 229 | else: |
|---|
| 230 | self.mode = "import" |
|---|
| 231 | self._modeChange() |
|---|
| 232 | |
|---|
| 233 | def doImportCallback(self, sender): |
|---|
| 234 | if sender.get(): |
|---|
| 235 | self.mode = "import" |
|---|
| 236 | else: |
|---|
| 237 | self.mode = "export" |
|---|
| 238 | self._modeChange() |
|---|
| 239 | |
|---|
| 240 | def selectFileOrFolderButtonCallback(self, sender): |
|---|
| 241 | path = GetFolder() |
|---|
| 242 | if path is not None: |
|---|
| 243 | if os.path.isdir(path): |
|---|
| 244 | if os.path.splitext(path)[-1] == ".ufo" and self.mode == "import": |
|---|
| 245 | self.files[path] = None |
|---|
| 246 | else: |
|---|
| 247 | if self.mode == "export": |
|---|
| 248 | pattern = "*.vfb" |
|---|
| 249 | else: |
|---|
| 250 | pattern = "*.ufo" |
|---|
| 251 | for fileName in glob.glob(os.path.join(path, pattern)): |
|---|
| 252 | self.files[fileName] = None |
|---|
| 253 | else: |
|---|
| 254 | ext = os.path.splitext(path)[-1] |
|---|
| 255 | if ext == ".vfb" and self.mode == "export": |
|---|
| 256 | self.files[path] = None |
|---|
| 257 | elif ext == ".ufo" and self.mode == "import": |
|---|
| 258 | self.files[path] = None |
|---|
| 259 | self._updateFileList() |
|---|
| 260 | |
|---|
| 261 | def selectAllOpenCallback(self, sender): |
|---|
| 262 | all = [f for f in AllFonts() if f.path is not None] |
|---|
| 263 | for font in all: |
|---|
| 264 | self.files[font.path] = font |
|---|
| 265 | self._updateFileList() |
|---|
| 266 | |
|---|
| 267 | def okCallback(self, sender): |
|---|
| 268 | if self.mode == "export": |
|---|
| 269 | doMask = self.w.doMaskCheckBox.get() |
|---|
| 270 | doMark = self.w.doMarkCheckBox.get() |
|---|
| 271 | for path, font in self.files.items(): |
|---|
| 272 | if font is None: |
|---|
| 273 | font = OpenFont(path) |
|---|
| 274 | exportUFOWithMetaData(font, doMask=doMask, doMark=doMark, doProgress=True) |
|---|
| 275 | else: |
|---|
| 276 | doMask = self.w.doMaskCheckBox.get() |
|---|
| 277 | doMark = self.w.doMarkCheckBox.get() |
|---|
| 278 | doKerning = self.w.doKerningCheckBox.get() |
|---|
| 279 | for path, null in self.files.items(): |
|---|
| 280 | font = NewFont() |
|---|
| 281 | importUFOWithMetaData(font, ufoPath=path, doMask=doMask, doMark=doMark, doKerning=doKerning, doProgress=True) |
|---|
| 282 | vfbPath = os.path.splitext(path)[0] + ".vfb" |
|---|
| 283 | font.save(vfbPath) |
|---|
| 284 | font.close() |
|---|
| 285 | |
|---|
| 286 | |
|---|
| 287 | if __name__ == "__main__": |
|---|
| 288 | UFOCentral() |
|---|