class byA_Path(byA_FrozenClass): def __init__(self, *segments, **kw): byA_FrozenClass.__init__(self) self._segments = Path() for p in segments: assert isinstance(p, byA_Line) or isinstance(p, byA_CubicBezier) if isinstance(p, byA_Line): self.insert(-1, p) if isinstance(p, byA_CubicBezier): self.insert(-1, p) if 'closed' in kw: self._segments.closed = kw['closed'] # DEPRECATED self._freeze("byA_Path") def insert(self, index, value): assert isinstance(value, byA_Line) or isinstance( value, byA_CubicBezier) if isinstance(value, byA_Line): self._segments.insert(index, value._svgline) if isinstance(value, byA_CubicBezier): self._segments.insert(index, value._svgcubicbezier) def append(self, value): assert isinstance(value, byA_Line) or isinstance( value, byA_CubicBezier) if isinstance(value, byA_Line): self._segments.append(value._svgline) if isinstance(value, byA_CubicBezier): self._segments.append(value._cubicbezier) def toStr(self): return self._segments.d()
def snap(self, tree, threshold): def process(points): for i, p in enumerate(points): best, _, dist = tree.nearest_neighbor([p.real, p.imag]) if dist < threshold: points[i] = complex(best[0], best[1]) return points path = parse_path(self['d']) newPath = Path() for seg in path: points = process([seg.start, seg.end]) if isinstance(seg, Line): newSeg = Line(*points) newPath.append(newSeg) elif isinstance(seg, CubicBezier): newSeg = CubicBezier(points[0], seg.control1, seg.control2, points[1]) newPath.append(newSeg) self['d'] = newPath.d() return self
def d(self, useSandT=False, use_closed_attrib=False, rel=False): """Returns a path d-string for the path object. For an explanation of useSandT and use_closed_attrib, see the compatibility notes in the README.""" segments = [s._segment for s in self._segments] path = Path(*segments) return path.d(useSandT, use_closed_attrib, rel)
def generateInBetweens(poseA, poseB, steps): inv = Inventory() # make pairs pairs = [] for key in ORDER: if key in poseA.inv and key in poseB.inv: partA = poseA.inv[key] partB = poseB.inv[key] if len(partA) != 1 or len(partB) != 1: print('Too many parts {0} - A: {1} B: {2}'.format( key, partA.keys(), partB.keys())) continue pairs.append((key, partA.values()[0], partB.values()[0])) # If there are 3 steps, there are 4 gaps between start and finish # |------1------2------3------| gaps = steps + 1 # process pairs for key, a, b in pairs: pathA = parse_path(a['d']) pathB = parse_path(b['d']) if len(pathA) != len(pathB): print('Unmatched segments {0} - A: {1} B: {2}'.format( key, pathA, pathB)) continue for step in range(1, gaps): newPath = Path() for i in range(len(pathA)): segA = pathA[i] segB = pathB[i] if isinstance(segA, Line): points = _deltaPoints([segA.start, segA.end], [segB.start, segB.end], step, gaps) newPath.append(Line(*points)) elif isinstance(segA, CubicBezier): points = _deltaPoints( [segA.start, segA.control1, segA.control2, segA.end], [segB.start, segB.control1, segB.control2, segB.end], step, gaps) newPath.append(CubicBezier(*points)) newPart = Part(newPath.d()) newPart['x'] = int(_delta(a['x'], b['x'], step, gaps)) newPart['y'] = int(_delta(a['y'], b['y'], step, gaps)) newPart['z'] = int(_delta(a['z'], b['z'], step, gaps)) inv.addPart(key, newPart) print(key, step, newPart) return inv
def splitSingleLine(start_end, unitLength, toPoint=False): ''' return strings :param start_end: :param unitLength: :return: ''' if UB.pointEquals(start_end[0], start_end[1]): return [] pathStr = getStraightPath(start_end) path = parse_path(pathStr) try: l = path.length() if l > unitLength: paths = [] t = unitLength / l ts = 0 te = t for i in range(int(math.ceil(l / unitLength))): seg = Line(path.point(ts), path.point(te)) p = Path(seg) if toPoint: paths.append([ UB.getPointFromComplex(path.point(ts)), UB.getPointFromComplex(path.point(te)) ]) else: paths.append(p.d()) ts += t te += t te = min(1, te) if toPoint: paths.append([ UB.getPointFromComplex(path.point(te)), UB.getPointFromComplex(path.point(1)) ]) else: paths.append( getStraightPath([ UB.getPointFromComplex(path.point(te)), UB.getPointFromComplex(path.point(1)) ])) return paths if toPoint: return [start_end] else: return [pathStr] except Exception as e: print(start_end, "something wrong with the splitLine", e) return []
def add_outline_path(self, paths, width, height, radii=dict(bl=3, br=3, tl=3, tr=3), type="path"): path = Path(*paths) self.outline.shape.height = height self.outline.shape.width = width self.outline.shape.radii = radii self.outline.shape.type = type self.outline.shape.value = absolute_to_relative_path(path.d())
def replace_point(self, ptbefore, ptafter): pathstr = self._g.elements[0].tostring() pathstr = pathstr.split('"')[3] pathbefore = parse_path(pathstr) pathafter = Path() for i in pathbefore: if isinstance(i, Line): if (isclose(i.start.real, ptbefore._x) and isclose(i.start.imag, ptbefore._y)): i.start = ptafter._x + 1j * ptafter._y if (isclose(i.end.real, ptbefore._x) and isclose(i.end.imag, ptbefore._y)): i.end = ptafter._x + 1j * ptafter._y pathafter.append(i) self.add(svgwrite.path.Path(d=pathafter.d()))
def flatten_beziers(svg_d): sp = parse_path(svg_d) spn = SVGPath() for seg in sp: if isinstance(seg, Line): spn.append(seg) elif isinstance(seg, CubicBezier): B = [seg.bpoints()] foo = np.dot(B, CUBIC_TO_POLY_SAMPLE) spn.extend([Line(x, y) for x, y in zip(foo[0, :-1], foo[0, 1:])]) else: raise RuntimeError(f"unsupported {seg}") return spn.d()
def add_route(self, paths, layer='bottom', stroke_width=0.4, style="stroke", type="path"): path = Path(*paths) self._add_route_to_layer( { "stroke-width": stroke_width, "style": style, "type": type, "value": absolute_to_relative_path(path.d()) }, layer=layer)
def quantize(self): path = parse_path(self['d']) newPath = Path() for seg in path: if isinstance(seg, Line): newSeg = Line( complex(round(seg.start.real), round(seg.start.imag)), complex(round(seg.end.real), round(seg.end.imag))) newPath.append(newSeg) elif isinstance(seg, CubicBezier): newSeg = CubicBezier( complex(round(seg.start.real), round(seg.start.imag)), complex(round(seg.control1.real), round(seg.control1.imag)), complex(round(seg.control2.real), round(seg.control2.imag)), complex(round(seg.end.real), round(seg.end.imag))) newPath.append(newSeg) self['d'] = newPath.d() return self
def text2pathd(text, group_transform=(1, 0, 0, 1, 0, 0)): attributes = dom2dict(text) if "font-size" in attributes: font_size = float(attributes["font-size"]) elif "style" in attributes: if attributes["style"].find("font-size") >= 0: font_size = attributes["style"].split("font-size:")[1].split( ";")[0] font_size = float(font_size.replace("px", "")) else: font_size = 12 else: font_size = 12 if "x" in attributes: x_global_offset = float(attributes["x"]) else: x_global_offset = 0 if "y" in attributes: y_global_offset = float(attributes["y"]) else: y_global_offset = 0 if hasattr(text.childNodes[0], "data"): text_string = text.childNodes[0].data else: flow_para = text.getElementsByTagName('flowPara') if flow_para: text_string = flow_para[0].childNodes[0].data # strip newline characters from the string, they aren't rendered in svg text_string = text_string.replace("\n", "").replace("\r", "") def tuple_to_imag(t): return t[0] + t[1] * 1j # keep fonts with repository, as dealing with importing fonts across platforms is a # nightmare foldername = os_path.dirname(os_path.abspath(__file__)) face = Face(os_path.join(foldername, 'Vera.ttf')) face.set_char_size(48 * 64) scale = font_size / face.size.height outlines = [] current_x = 0 transform = get_transform(text) transform = combine_transforms(transform, group_transform) x_global_offset, y_global_offset = transform_point( [x_global_offset, y_global_offset], transform) for i, letter in enumerate(text_string): face.load_char(letter) outline = face.glyph.outline if i != 0: kerning = face.get_kerning(text_string[i - 1], text_string[i]) kerning_x = kerning.x else: kerning_x = 0 if text_string[i] == ' ': # a space is usually 30% of the widest character, capital W char_width = face.size.max_advance * 0.3 char_height = 0 char_offset = 0 else: char_width = outline.get_bbox().xMax char_offset = face.size.height - outline.get_bbox().yMax char_height = outline.get_bbox().yMax outline_dict = {} current_x += kerning_x outline_dict["points"] = [ (scale * (p[0] + current_x) + x_global_offset, scale * (char_offset + char_height - p[1]) + y_global_offset) for p in outline.points ] outline_dict["contours"] = outline.contours outline_dict["tags"] = outline.tags outlines.append(outline_dict) current_x += char_width paths = [] for outline in outlines: start, end = 0, 0 for i in range(len(outline["contours"])): end = outline["contours"][i] points = outline["points"][start:end + 1] points.append(points[0]) tags = outline["tags"][start:end + 1] tags.append(tags[0]) segments = [ [ points[0], ], ] for j in range(1, len(points)): segments[-1].append(points[j]) if tags[j] and j < (len(points) - 1): segments.append([ points[j], ]) for segment in segments: if len(segment) == 2: paths.append( Line(start=tuple_to_imag(segment[0]), end=tuple_to_imag(segment[1]))) elif len(segment) == 3: paths.append( QuadraticBezier(start=tuple_to_imag(segment[0]), control=tuple_to_imag(segment[1]), end=tuple_to_imag(segment[2]))) elif len(segment) == 4: C = ((segment[1][0] + segment[2][0]) / 2.0, (segment[1][1] + segment[2][1]) / 2.0) paths.append( QuadraticBezier(start=tuple_to_imag(segment[0]), control=tuple_to_imag(segment[1]), end=tuple_to_imag(C))) paths.append( QuadraticBezier(start=tuple_to_imag(C), control=tuple_to_imag(segment[2]), end=tuple_to_imag(segment[3]))) start = end + 1 path = Path(*paths) return path.d()
angle = 45 translate_y = 1 def c(a): return cos(a * pi / 180.) def s(a): return sin(a * pi / 180.) transform1 = c(angle), s(angle), -s(angle), c(angle), 0, 0 transform2 = 1, 0, 0, 1, 0, translate_y transform = combine_transforms(transform1, transform2) transform_m = "matrix({},{},{},{},{},{})".format(*transform) dg = dwg.add(dwg.g(transform=transform_m)) dg.add(dwg.path(d=line.d(), stroke_width=1, stroke=rgb(255, 0, 0))) line_transformed = parse_path(transform_path(transform, line.d())) print(transform_path(transform, line.d())) print(line_transformed) dwg.add(dwg.path(d=line_transformed.d(), stroke_width=1, stroke=rgb(0, 255, 0))) ''' dwg.add(dwg.path(d=line.d(), stroke_width=1, stroke=rgb(0, 0, 255))) dwg.add(dwg.path(d=line.rotated(-45).d(), stroke_width=1, stroke=rgb(0, 0, 255))) ''' dwg.save() xml = xml.dom.minidom.parse(path_filename) open(path_filename, "w").write(xml.toprettyxml())
class Char2SvgPath: ''' ''' CHAR_SIZE = 2048 def __init__(self, char: str, font: str, load_gpos_table=True): ''' ''' self.char = char self.font = font # load font self.face = freetype.Face(self.font) self.face.set_char_size(self.CHAR_SIZE, self.CHAR_SIZE) # -> x_ppem = y_ppem = 32 ''' The character widths and heights are specified in 1/64th of points. A point is a physical distance, equaling 1/72th of an inch. Normally, it is not equivalent to a pixel. Value of 0 for the character width means ‘same as character height’, value of 0 for the character height means ‘same as character width’. Otherwise, it is possible to specify different character widths and heights. The horizontal and vertical device resolutions are expressed in dots-per-inch, or dpi. Standard values are 72 or 96 dpi for display devices like the screen. The resolution is used to compute the character pixel size from the character point size. Value of 0 for the horizontal resolution means ‘same as vertical resolution’, value of 0 for the vertical resolution means ‘same as horizontal resolution’. If both values are zero, 72 dpi is used for both dimensions. ''' # initialize a character - option FT_LOAD_NO_SCALE mandatory self.face.load_char( self.char, freetype.FT_LOAD_NO_SCALE | freetype.FT_LOAD_NO_BITMAP | freetype.FT_KERNING_UNSCALED) # glyph info self.glyph_index = self.face.get_char_index(self.char) self.glyph_adv = self.face.get_advance( self.glyph_index, freetype.FT_LOAD_NO_SCALE | freetype.FT_LOAD_NO_BITMAP) self.bbox = self.face.glyph.outline.get_bbox() # evaluating the path: # -------------------- # values for y-flip - if not set, use bbox self.yflip_value = None # values for x-translation - if not set, use bbox self.xshift_value = None # to eval self.path = None # GPOS kerning - with ziafont ! if load_gpos_table: self.gpos = self.get_gpos_table() def get_gpos_table(self): ''' from ziafont! ''' import struct from datetime import datetime, timedelta from collections import namedtuple Table = namedtuple('Table', ['checksum', 'offset', 'length']) class FontReader(io.BytesIO): ''' Class for reading from Font File ''' # TTF/OTF is big-endian (>) def readuint32(self, offset: int = None) -> int: ''' Read 32-bit unsigned integer ''' if offset: self.seek(offset) return struct.unpack('>I', self.read(4))[0] def readuint16(self, offset: int = None) -> int: ''' Read 16-bit unsigned integer ''' if offset: self.seek(offset) return struct.unpack('>H', self.read(2))[0] def readuint8(self, offset: int = None) -> int: ''' Read 8-bit unsigned integer ''' if offset: self.seek(offset) return struct.unpack('>B', self.read(1))[0] def readint8(self, offset: int = None) -> int: ''' Read 8-bit signed integer ''' if offset: self.seek(offset) return struct.unpack('>b', self.read(1))[0] def readdate(self, offset: int = None) -> datetime: ''' Read datetime ''' if offset: self.seek(offset) mtime = self.readuint32() * 0x100000000 + self.readuint32() fontepoch = datetime(1904, 1, 1) mdate = fontepoch + timedelta(seconds=mtime) return mdate def readint16(self, offset: int = None) -> int: ''' Read 16-bit signed integer ''' if offset: self.seek(offset) return struct.unpack('>h', self.read(2))[0] def readshort(self, offset: int = None) -> float: ''' Read "short" fixed point number (S1.14) ''' x = self.readint16() return float(x) * 2**-14 def readvaluerecord(self, fmt: int) -> dict: ''' Read a GPOS "ValueRecord" into a dictionary. Zero values will be omitted. ''' record = {} if fmt & 0x0001: record['x'] = self.readint16() if fmt & 0x0002: record['y'] = self.readint16() if fmt & 0x0004: record['xadvance'] = self.readint16() if fmt & 0x0008: record['yadvance'] = self.readint16() if fmt & 0x0010: record['xpladeviceoffset'] = self.readuint16() if fmt & 0x0020: record['ypladeviceoffset'] = self.readuint16() if fmt & 0x0040: record['xadvdeviceoffset'] = self.readuint16() if fmt & 0x0080: record['yadvdeviceoffset'] = self.readuint16() return record with open(self.font, 'rb') as f: self.fontfile = FontReader(f.read()) self.fontfile.seek(0) scalartype = self.fontfile.readuint32() numtables = self.fontfile.readuint16() searchrange = self.fontfile.readuint16() entryselector = self.fontfile.readuint16() rangeshift = self.fontfile.readuint16() # numtables*16-searchrange # Table Directory (table 5) self.tables = {} for i in range(numtables): tag = self.fontfile.read(4).decode() self.tables[tag] = Table(checksum=self.fontfile.readuint32(), offset=self.fontfile.readuint32(), length=self.fontfile.readuint32()) #for table in self.tables.keys(): # if table != 'head': # self._verifychecksum(table) if 'glyf' not in self.tables: raise ValueError('Unsupported font (no glyf table).') if 'GPOS' in self.tables: return gpos.Gpos(self.tables['GPOS'].offset, self.fontfile) else: return None def set_yflip_value(self, val): ''' ''' self.yflip_value = val def set_xshift_value(self, val): ''' ''' self.xshift_value = val def calc_shift(self, next_ch: str): ''' ''' self.face.has_kerning shift = self.glyph_adv adv = 0 if self.face.has_kerning: if self.gpos: # Only getting x-advance for first glyph. next_cc = Char2SvgPath(next_ch, self.font, load_gpos_table=False) adv = self.gpos.kern(self.glyph_index, next_cc.glyph_index)[0].get( 'xadvance', 0) else: vector = self.face.get_kerning(self.char, next_ch) adv = vector.x #x += std::floor((glyph.lsb_delta - prevRsbDelta + kerning + 31) / 64.0); return shift + adv def calc_path(self, fontsize: float) -> Path: ''' fontsize: svg font-size in px ''' def tuple2complex(t): return t[0] + t[1] * 1j yflip = self.yflip_value if yflip == None: yflip = self.bbox.yMax xshift = self.xshift_value if xshift == None: xshift = self.bbox.xMin # extra scaling scaling = fontsize / self.CHAR_SIZE ''' You'll need to flip the y values of the points in order to render the characters right-side-up: ''' outline: freetype.Outline = self.face.glyph.outline # shift and flip the points outline_points = [((pt[0] - xshift) * scaling, (yflip - pt[1]) * scaling) for pt in outline.points] ''' The face has three lists of interest: the points, the tags, and the contours. The points are the x/y coordinates of the start and end points of lines and control points. The tags indicate what type of point it is, where tag values of 0 are control points. Finally, the contours are the end point list index for each shape. Characters like i or ! have two shapes, most others have only one contour. So, for each contour, we want to pick out only the tags and points for that contour. ''' start, end = 0, 0 paths = [] for i in range(len(outline.contours)): end = outline.contours[i] points = outline_points[start:end + 1] points.append(points[0]) tags = outline.tags[start:end + 1] tags.append(tags[0]) ''' Next, we want to split the points up into path segments, using the tags. If the tags are 0, add the point to the current segment, else create a new segment, so that control points stay with their path segments: ''' segments = [ [ points[0], ], ] for j in range(1, len(points)): segments[-1].append(points[j]) if tags[j] and j < (len(points) - 1): segments.append([ points[j], ]) ''' Then convert the segments to lines. ''' for segment in segments: #print("segment (len=%d)" % len(segment)) if len(segment) == 2: paths.append( Line(start=tuple2complex(segment[0]), end=tuple2complex(segment[1]))) elif len(segment) == 3: C12 = segment[1] P1 = segment[0] P2 = segment[2] paths.append( QuadraticBezier(start=tuple2complex(P1), control=tuple2complex(C12), end=tuple2complex(P2))) elif len(segment) == 4: C12 = segment[1] C23 = segment[2] P1 = segment[0] P2 = ((segment[1][0] + segment[2][0]) / 2.0, (segment[1][1] + segment[2][1]) / 2.0) P3 = segment[3] paths.append( QuadraticBezier(start=tuple2complex(P1), control=tuple2complex(C12), end=tuple2complex(P2))) paths.append( QuadraticBezier(start=tuple2complex(P2), control=tuple2complex(C23), end=tuple2complex(P3))) elif len(segment) == 5: C12 = segment[1] C23 = segment[2] C34 = segment[3] P1 = segment[0] P2 = ((segment[1][0] + segment[2][0]) / 2.0, (segment[1][1] + segment[2][1]) / 2.0) P3 = ((segment[2][0] + segment[3][0]) / 2.0, (segment[2][1] + segment[3][1]) / 2.0) P4 = segment[4] paths.append( QuadraticBezier(start=tuple2complex(P1), control=tuple2complex(C12), end=tuple2complex(P2))) paths.append( QuadraticBezier(start=tuple2complex(P2), control=tuple2complex(C23), end=tuple2complex(P3))) paths.append( QuadraticBezier(start=tuple2complex(P3), control=tuple2complex(C34), end=tuple2complex(P4))) else: # with algo N = len(segment) - 1 # first Ps = segment[0] Ctrl = segment[1] Pe = ((segment[1][0] + segment[2][0]) / 2.0, (segment[1][1] + segment[2][1]) / 2.0) paths.append( QuadraticBezier(start=tuple2complex(Ps), control=tuple2complex(Ctrl), end=tuple2complex(Pe))) # second - ... for k in range(2, len(segment) - 2): Ps = ((segment[k - 1][0] + segment[k][0]) / 2.0, (segment[k - 1][1] + segment[k][1]) / 2.0) Ctrl = segment[k] Pe = ((segment[k][0] + segment[k + 1][0]) / 2.0, (segment[k][1] + segment[k + 1][1]) / 2.0) paths.append( QuadraticBezier(start=tuple2complex(Ps), control=tuple2complex(Ctrl), end=tuple2complex(Pe))) # last Ps = ((segment[N - 2][0] + segment[N - 1][0]) / 2.0, (segment[N - 2][1] + segment[N - 1][1]) / 2.0) Ctrl = segment[N - 1] Pe = segment[N] paths.append( QuadraticBezier(start=tuple2complex(Ps), control=tuple2complex(Ctrl), end=tuple2complex(Pe))) ''' Set the start location to the end location and continue. You can use the svgpathtools Path to merge the paths: ''' start = end + 1 self.path = Path(*paths) return self.path def calc_path_freetype_decompose(self, fontsize: float) -> Path: ''' ''' outline: freetype.Outline = self.face.glyph.outline def move_to(a, ctx): ctx.append("M {},{}".format(a.x, a.y)) def line_to(a, ctx): ctx.append("L {},{}".format(a.x, a.y)) def conic_to(a, b, ctx): ctx.append("Q {},{} {},{}".format(a.x, a.y, b.x, b.y)) def cubic_to(a, b, c, ctx): ctx.append("C {},{} {},{} {},{}".format(a.x, a.y, b.x, b.y, c.x, c.y)) ctx: List[str] = [] outline.decompose(ctx, move_to=move_to, line_to=line_to, conic_to=conic_to, cubic_to=cubic_to) path_str = " ".join(ctx) # build a Path instance path = parse_path(path_str) # this path is not at the right position - it has to be scaled, shifted and flipped yflip = self.yflip_value if yflip == None: yflip = self.bbox.yMax xshift = self.xshift_value if xshift == None: xshift = self.bbox.xMin # extra scaling scaling = fontsize / self.CHAR_SIZE # let's go path = path.translated(-xshift) # flipping means a special transformation ... matrix(1,0, 0,-1, 0,7.5857) apath = Path() tf = np.identity(3) tf[1][1] = -1 for seg in path: aseg = xxpath.transform(seg, tf) apath.append(aseg) # and the companion translation path = apath.translated(yflip * 1j) # finally path = path.scaled(scaling, scaling) self.path = path return self.path def write_path(self, prefix=""): print("path %s: %s" % (prefix, self.path.d())) wsvg(self.path, filename="char2path_%s_%s_convert.svg" % (self.char, prefix))
def renderLabel(self, inString): dwg = svgwrite.Drawing() # SVG drawing in memory strIdx = 0 # Used to iterate over inString xOffset = 100 # Cumulative character placement offset yOffset = 0 # Cumulative character placement offset charSizeX = 8 # Character size constant charSizeY = 8 # Character size constant baseline = 170 # Y value of text baseline glyphBounds = [ ] # List of boundingBox objects to track rendered character size finalSegments = [] # List of output paths escaped = False # Track whether the current character was preceded by a '\' lineover = False # Track whether the current character needs to be lined over lineoverList = [] # If we can't find the typeface that the user requested, we have to quit try: face = Face( os.path.dirname(os.path.abspath(__file__)) + '/typeface/' + self.fontName + '.ttf') face.set_char_size(charSizeX, charSizeY, 200, 200) except Exception as e: print(e) print("WARN: No Typeface found with the name " + self.fontName + ".ttf") sys.exit(0) # quit Python # If the typeface that the user requested exists, but there's no position table for it, we'll continue with a warning try: table = __import__( 'KiBuzzard.KiBuzzard.buzzard.typeface.' + self.fontName, globals(), locals(), ['glyphPos']) glyphPos = table.glyphPos spaceDistance = table.spaceDistance except: glyphPos = 0 spaceDistance = 60 print( "WARN: No Position Table found for this typeface. Composition will be haphazard at best." ) # If there's lineover text, drop the text down to make room for the line dropBaseline = False a = False x = 0 while x < len(inString): if x > 0 and inString[x] == '\\': a = True if x != len(inString) - 1: x += 1 if inString[x] == '!' and not a: dropBaseline = True a = False x += 1 if dropBaseline: baseline = 190 # Draw and compose the glyph portion of the tag for charIdx in range(len(inString)): # Check whether this character is a space if inString[charIdx] == ' ': glyphBounds.append(boundingBox(0, 0, 0, 0)) xOffset += spaceDistance continue # Check whether this character is a backslash that isn't escaped # and isn't the first character (denoting a backslash-shaped tag) if inString[charIdx] == '\\' and charIdx > 0 and not escaped: glyphBounds.append(boundingBox(0, 0, 0, 0)) escaped = True continue # If this is a non-escaped '!' mark the beginning of lineover if inString[charIdx] == '!' and not escaped: glyphBounds.append(boundingBox(0, 0, 0, 0)) lineover = True # If we've hit the end of the string but not the end of the lineover # go ahead and finish it out if charIdx == len(inString) - 1 and len(lineoverList) > 0: linePaths = [] linePaths.append( Line(start=complex(lineoverList[0], 10), end=complex(xOffset, 10))) linePaths.append( Line(start=complex(xOffset, 10), end=complex(xOffset, 30))) linePaths.append( Line(start=complex(xOffset, 30), end=complex(lineoverList[0], 30))) linePaths.append( Line(start=complex(lineoverList[0], 30), end=complex(lineoverList[0], 10))) linepath = Path(*linePaths) linepath = elPath(linepath.d()) finalSegments.append(linepath) lineover = False lineoverList.clear() continue # All special cases end in 'continue' so if we've gotten here we can clear our flags if escaped: escaped = False face.load_char( inString[charIdx]) # Load character curves from font outline = face.glyph.outline # Save character curves to var y = [t[1] for t in outline.points] # flip the points outline_points = [(p[0], max(y) - p[1]) for p in outline.points] start, end = 0, 0 paths = [] box = 0 yOffset = 0 for i in range(len(outline.contours)): end = outline.contours[i] points = outline_points[start:end + 1] points.append(points[0]) tags = outline.tags[start:end + 1] tags.append(tags[0]) segments = [ [ points[0], ], ] box = boundingBox(points[0][0], points[0][1], points[0][0], points[0][1]) for j in range(1, len(points)): if not tags[j]: # if this point is off-path if tags[j - 1]: # and the last point was on-path segments[-1].append( points[j]) # toss this point onto the segment elif not tags[j - 1]: # and the last point was off-path # get center point of two newPoint = ((points[j][0] + points[j - 1][0]) / 2.0, (points[j][1] + points[j - 1][1]) / 2.0) segments[-1].append( newPoint ) # toss this new point onto the segment segments.append( [ newPoint, points[j], ] ) # and start a new segment with the new point and this one elif tags[j]: # if this point is on-path segments[-1].append( points[j]) # toss this point onto the segment if j < (len(points) - 1): segments.append( [ points[j], ] ) # and start a new segment with this point if we're not at the end for segment in segments: if len(segment) == 2: paths.append( Line(start=tuple_to_imag(segment[0]), end=tuple_to_imag(segment[1]))) elif len(segment) == 3: paths.append( QuadraticBezier(start=tuple_to_imag(segment[0]), control=tuple_to_imag(segment[1]), end=tuple_to_imag(segment[2]))) start = end + 1 # Derive bounding box of character for segment in paths: i = 0 while i < 10: point = segment.point(0.1 * i) if point.real > box.xMax: box.xMax = point.real if point.imag > box.yMax: box.yMax = point.imag if point.real < box.xMin: box.xMin = point.real if point.imag < box.yMin: box.yMin = point.imag i += 1 glyphBounds.append(box) path = Path(*paths) if glyphPos != 0: try: xOffset += glyphPos[inString[charIdx]].real yOffset = glyphPos[inString[charIdx]].imag except: pass if lineover and len(lineoverList) == 0: lineoverList.append(xOffset) lineover = False if (lineover and len(lineoverList) > 0): linePaths = [] linePaths.append( Line(start=complex(lineoverList[0], 10), end=complex(xOffset, 10))) linePaths.append( Line(start=complex(xOffset, 10), end=complex(xOffset, 30))) linePaths.append( Line(start=complex(xOffset, 30), end=complex(lineoverList[0], 30))) linePaths.append( Line(start=complex(lineoverList[0], 30), end=complex(lineoverList[0], 10))) linepath = Path(*linePaths) linepath = elPath(linepath.d()) finalSegments.append(linepath) lineover = False lineoverList.clear() pathTransform = Matrix.translate(xOffset, baseline + yOffset - box.yMax) path = elPath(path.d()) * pathTransform path = elPath(path.d()) finalSegments.append(path) xOffset += 30 if glyphPos != 0: try: xOffset -= glyphPos[inString[charIdx]].real except: pass xOffset += (glyphBounds[charIdx].xMax - glyphBounds[charIdx].xMin) strIdx += 1 if self.leftCap == '' and self.rightCap == '': for i in range(len(finalSegments)): svgObj = dwg.add(dwg.path(finalSegments[i].d())) svgObj['fill'] = "#000000" else: #draw the outline of the label as a filled shape and #subtract each latter from it tagPaths = [] if self.rightCap == 'round': tagPaths.append( Line(start=complex(100, 0), end=complex(xOffset, 0))) tagPaths.append( Arc(start=complex(xOffset, 0), radius=complex(100, 100), rotation=180, large_arc=1, sweep=1, end=complex(xOffset, 200))) elif self.rightCap == 'square': tagPaths.append( Line(start=complex(100, 0), end=complex(xOffset, 0))) tagPaths.append( Line(start=complex(xOffset, 0), end=complex(xOffset + 50, 0))) tagPaths.append( Line(start=complex(xOffset + 50, 0), end=complex(xOffset + 50, 200))) tagPaths.append( Line(start=complex(xOffset + 50, 200), end=complex(xOffset, 200))) elif self.rightCap == 'pointer': tagPaths.append( Line(start=complex(100, 0), end=complex(xOffset, 0))) tagPaths.append( Line(start=complex(xOffset, 0), end=complex(xOffset + 50, 0))) tagPaths.append( Line(start=complex(xOffset + 50, 0), end=complex(xOffset + 100, 100))) tagPaths.append( Line(start=complex(xOffset + 100, 100), end=complex(xOffset + 50, 200))) tagPaths.append( Line(start=complex(xOffset + 50, 200), end=complex(xOffset, 200))) elif self.rightCap == 'flagtail': tagPaths.append( Line(start=complex(100, 0), end=complex(xOffset, 0))) tagPaths.append( Line(start=complex(xOffset, 0), end=complex(xOffset + 100, 0))) tagPaths.append( Line(start=complex(xOffset + 100, 0), end=complex(xOffset + 50, 100))) tagPaths.append( Line(start=complex(xOffset + 50, 100), end=complex(xOffset + 100, 200))) tagPaths.append( Line(start=complex(xOffset + 100, 200), end=complex(xOffset, 200))) elif self.rightCap == 'fslash': tagPaths.append( Line(start=complex(100, 0), end=complex(xOffset, 0))) tagPaths.append( Line(start=complex(xOffset, 0), end=complex(xOffset + 50, 0))) tagPaths.append( Line(start=complex(xOffset + 50, 0), end=complex(xOffset, 200))) elif self.rightCap == 'bslash': tagPaths.append( Line(start=complex(100, 0), end=complex(xOffset, 0))) tagPaths.append( Line(start=complex(xOffset, 0), end=complex(xOffset + 50, 200))) tagPaths.append( Line(start=complex(xOffset + 50, 200), end=complex(xOffset, 200))) elif self.rightCap == '' and self.leftCap != '': tagPaths.append( Line(start=complex(100, 0), end=complex(xOffset, 0))) tagPaths.append( Line(start=complex(xOffset, 0), end=complex(xOffset, 200))) if self.leftCap == 'round': tagPaths.append( Line(start=complex(xOffset, 200), end=complex(100, 200))) tagPaths.append( Arc(start=complex(100, 200), radius=complex(100, 100), rotation=180, large_arc=0, sweep=1, end=complex(100, 0))) elif self.leftCap == 'square': tagPaths.append( Line(start=complex(xOffset, 200), end=complex(100, 200))) tagPaths.append( Line(start=complex(100, 200), end=complex(50, 200))) tagPaths.append( Line(start=complex(50, 200), end=complex(50, 0))) tagPaths.append(Line(start=complex(50, 0), end=complex(100, 0))) elif self.leftCap == 'pointer': tagPaths.append( Line(start=complex(xOffset, 200), end=complex(100, 200))) tagPaths.append( Line(start=complex(100, 200), end=complex(50, 200))) tagPaths.append( Line(start=complex(50, 200), end=complex(0, 100))) tagPaths.append(Line(start=complex(0, 100), end=complex(50, 0))) tagPaths.append(Line(start=complex(50, 0), end=complex(100, 0))) elif self.leftCap == 'flagtail': tagPaths.append( Line(start=complex(xOffset, 200), end=complex(100, 200))) tagPaths.append( Line(start=complex(100, 200), end=complex(0, 200))) tagPaths.append( Line(start=complex(0, 200), end=complex(50, 100))) tagPaths.append(Line(start=complex(50, 100), end=complex(0, 0))) tagPaths.append(Line(start=complex(0, 0), end=complex(100, 0))) elif self.leftCap == 'fslash': tagPaths.append( Line(start=complex(xOffset, 200), end=complex(100, 200))) tagPaths.append( Line(start=complex(100, 200), end=complex(50, 200))) tagPaths.append( Line(start=complex(50, 200), end=complex(100, 0))) elif self.leftCap == 'bslash': tagPaths.append( Line(start=complex(xOffset, 200), end=complex(100, 200))) tagPaths.append( Line(start=complex(100, 200), end=complex(50, 0))) tagPaths.append(Line(start=complex(50, 0), end=complex(100, 0))) elif self.leftCap == '' and self.rightCap != '': tagPaths.append( Line(start=complex(xOffset, 200), end=complex(100, 200))) tagPaths.append( Line(start=complex(100, 200), end=complex(100, 0))) path = Path(*tagPaths) for i in range(len(finalSegments)): path = elPath(path.d() + " " + finalSegments[i].reverse()) tagObj = dwg.add(dwg.path(path.d())) tagObj['fill'] = "#000000" dwg['width'] = xOffset + 100 dwg['height'] = 250 #dwg.saveas('out.svg') print('create svg') return dwg
def flatten_shape(i, all_paths, merge_paths): dwg = Drawing("merge_output%s.svg" % i, profile='tiny') def draw_line(start, end, offset=0.0): start += offset end += offset dwg.add( dwg.line(start=(start.real, start.imag), end=(end.real, end.imag), stroke_width=4, stroke=rgb(255, 0, 0))) dwg.add( dwg.path( **{ 'd': all_paths[i].d(), 'fill': "none", 'stroke-width': 4, 'stroke': rgb(0, 0, 0) })) dwg.add( dwg.path( **{ 'd': merge_paths[i].d(), 'fill': "none", 'stroke-width': 4, 'stroke': rgb(255, 0, 0) })) bbox = calc_overall_bbox(all_paths[i]) width, height = abs(bbox[1] - bbox[0]), abs(bbox[3] - bbox[2]) margin = 40 lower = min(bbox[2], bbox[3]) + height + margin left = min(bbox[0], bbox[1]) + margin def draw_marker(loc, col=rgb(255, 0, 0), offset=(left, lower)): dwg.add( dwg.circle(center=(loc.real + offset[0], loc.imag + offset[1]), r=4, fill=col)) max_axis = max(width, height) num_lines = 10 points = [merge_paths[i].point(j / num_lines) for j in range(num_lines)] + [merge_paths[i].point(1.0)] angles = [ asin((points[j + 1].imag - points[j].imag) / abs(points[j + 1] - points[j])) for j in range(num_lines) ] ends = [max_axis * (sin(angle) + cos(angle) * 1j) for angle in angles] intersection_clips = [] for j, end in enumerate(ends): end_point = end + points[j] intersections = other_paths[i].intersect( Line(start=points[j], end=end_point)) for intersection in intersections[0]: intersection_point = intersection[1].point(intersection[2]) target = merge_paths[i].length() * ( 1 - j / num_lines) + abs(intersection_point - points[j]) * 1j intersection_clips.append( PathClip(index=other_paths[i].index(intersection[1]), t=intersection[2], target=target)) if j % 10 == 0: draw_line(points[j], intersection_point) draw_marker(intersection_point, rgb(0, 255, 0), (0, 0)) break # make the flexed points by chopping the chunks of the other paths out, then # translating and rotating them such that their end points line up with the diff lines def transform_side(sides, targets, angle_offset=0): def angle(point1, point2): diff = point1 - point2 if diff.real == 0: return 90.0 return atan(diff.imag / diff.real) * 180.0 / pi # change this so that it has two targets transformed_side = Path(*sides) source_angle = angle(transformed_side.end, transformed_side.start) - \ angle(targets[0], targets[1]) transformed_side = transformed_side.rotated(-source_angle + angle_offset) source = transformed_side.end if angle_offset == 0 else transformed_side.start diff = targets[1] - source transformed_side = transformed_side.translated(diff) draw_marker(targets[0], rgb(0, 200, 200)) draw_marker(targets[1], rgb(0, 255, 255)) transformed_diff = abs(transformed_side.start - transformed_side.end) targets_diff = abs(targets[0] - targets[1]) if transformed_diff < targets_diff: transformed_side.insert( 0, Line(start=targets[0], end=transformed_side.start)) elif transformed_diff > targets_diff: # pop elements off until the transformed diff is smaller while transformed_diff > targets_diff: transformed_side.pop(0) transformed_diff = abs(transformed_side.start - transformed_side.end) print("path", transformed_side) print("path is longer", transformed_diff - targets_diff) return transformed_side start_index = 0 curr_t = 0 flexed_path = [] t_resolution = 0.01 if intersection_clips[0].index > intersection_clips[-1].index or \ (intersection_clips[0].index == intersection_clips[-1].index and intersection_clips[0].t > intersection_clips[-1].t): intersection_clips.reverse() # add the end of the shape to the intersection clips intersection_clips.append( PathClip(index=len(other_paths[i]) - 1, t=1.0, target=merge_paths[i].length())) last_target = 0 for clip in intersection_clips: sides = [] print("boundaries", start_index, clip.index, curr_t, clip.t) upper_t = clip.t if start_index == clip.index else 1.0 while start_index <= clip.index and curr_t < upper_t: curr_seg = other_paths[i][start_index] while curr_t < upper_t: max_t = curr_t + t_resolution if curr_t + t_resolution < clip.t else clip.t sides.append( Line(start=curr_seg.point(curr_t), end=curr_seg.point(max_t))) curr_t += t_resolution curr_t = upper_t if start_index != clip.index: curr_t = 0.0 if upper_t == 1.0: start_index += 1 upper_t = clip.t if start_index == clip.index else 1.0 if len(sides) != 0: flexed_path.append( transform_side(sides, [last_target, clip.target])) last_target = clip.target straight_path = [Line(start=0, end=merge_paths[i].length())] for p in flexed_path: p = p.translated(left + lower * 1j) dwg.add( dwg.path(d=p.d(), fill="none", stroke_width=4, stroke=rgb(255, 0, 0))) transformed_path = flexed_path + straight_path transformed_path = Path(*transformed_path).translated(left + lower * 1j) dwg.add( dwg.path(d=transformed_path.d(), fill="none", stroke_width=4, stroke=rgb(0, 0, 0))) bbox = calc_overall_bbox(list(all_paths[i]) + list(transformed_path)) width, height = abs(bbox[1] - bbox[0]), abs(bbox[3] - bbox[2]) dwg.viewbox(min(bbox[0], bbox[1]), min(bbox[2], bbox[3]), width, height) dwg.save() return flexed_path
from svgpathtools import Path, Line, QuadraticBezier, CubicBezier, Arc, svg2paths, parse_path, wsvg from pcbmode.utils.svg import absolute_to_relative_path print("Build SVG Path from lines") seg1 = CubicBezier(300+100j, 100+100j, 200+200j, 200+300j) # A cubic beginning at (300, 100) and ending at (200, 300) seg2 = Line(200+300j, 250+350j) # A line beginning at (200, 300) and ending at (250, 350) path = Path(seg1, seg2) # A path traversing the cubic and then the line # (svgpathtools only creates absolute path co-ordinates) print(path.d()) print("Create SVG") #(svgpathtools only creates absolute path co-ordinates) outline_path = parse_path("m 10.858333,11 c 2.140681,-0.340482 4.281363,-0.680964 6.422044,-1.0214454 1.69236,-1.0123173 3.384721,-2.0246345 5.077081,-3.0369518 1.2656,-2.1059322 2.531201,-4.2118644 3.796801,-6.31779663 0.663858,-2.34543537 1.327716,-4.69087077 1.991574,-7.03630647 -0.223524,-1.829043 -0.447047,-3.6580857 -0.670571,-5.4871287 -0.600087,-0.758457 -1.200175,-1.516914 -1.800262,-2.275371 -17.1166667,0 -34.2333333,0 -51.35,0 -0.600087,0.758457 -1.200175,1.516914 -1.800262,2.275371 -0.223524,1.829043 -0.447047,3.6580857 -0.670571,5.4871287 0.663858,2.3454357 1.327716,4.6908711 1.991574,7.03630647 1.2656,2.10593223 2.531201,4.21186443 3.796801,6.31779663 1.69236,1.0123173 3.384721,2.0246345 5.077081,3.0369518 2.140681,0.3404814 4.281363,0.6809634 6.422044,1.0214454 7.2388887,0 14.4777773,0 21.716666,0 z", current_pos=0) wsvg(outline_path, filename='svg/out/output1.svg') p = parse_path("m 4.6147194,11.014797 c 0.04717,0.459725 -0.00339,0.582458 0.024033,1.056225", current_pos=0) print p print("Parse SVG") paths, attributes = svg2paths('svg/in/triangle_relative.svg') for path in paths: print absolute_to_relative_path(path.d())
def char(self, ch): def tuple_to_imag(t): return t[0] + t[1] * 1j #face = Face('/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf') face = Face(ddd.DATA_DIR + '/fonts/OpenSansEmoji.ttf') face.set_char_size(self.char_size) face.load_char(ch) #kerning = face.get_kerning(ch, 'x') # or from previous, actually? #print(kerning) outline = face.glyph.outline y = [t[1] for t in outline.points] # flip the points outline_points = [(p[0], max(y) - p[1]) for p in outline.points] start, end = 0, 0 paths = [] for i in range(len(outline.contours)): end = outline.contours[i] points = outline_points[start:end + 1] points.append(points[0]) tags = outline.tags[start:end + 1] tags.append(tags[0]) segments = [ [ points[0], ], ] for j in range(1, len(points)): segments[-1].append(points[j]) if tags[j] and j < (len(points) - 1): segments.append([ points[j], ]) for segment in segments: if len(segment) == 2: paths.append( Line(start=tuple_to_imag(segment[0]), end=tuple_to_imag(segment[1]))) elif len(segment) == 3: paths.append( QuadraticBezier(start=tuple_to_imag(segment[0]), control=tuple_to_imag(segment[1]), end=tuple_to_imag(segment[2]))) elif len(segment) == 4: paths.append( CubicBezier(start=tuple_to_imag(segment[0]), control1=tuple_to_imag(segment[1]), control2=tuple_to_imag(segment[2]), end=tuple_to_imag(segment[3]))) #C = ((segment[1][0] + segment[2][0]) / 2.0, # (segment[1][1] + segment[2][1]) / 2.0) #paths.append(QuadraticBezier(start=tuple_to_imag(segment[0]), # control=tuple_to_imag(segment[1]), # end=tuple_to_imag(C))) #paths.append(QuadraticBezier(start=tuple_to_imag(C), # control=tuple_to_imag(segment[2]), # end=tuple_to_imag(segment[3]))) start = end + 1 path = Path(*paths) #wsvg(path, filename="/tmp/test.svg") path_d = path.d() # https://gis.stackexchange.com/questions/301605/how-to-create-shape-in-shapely-from-an-svg-path-element # This page also has info about SVG reading! #svgpath = 'M10 10 C 20 20, 40 20, 50 10Z' mpl_path = parse_path(path_d) coords = mpl_path.to_polygons(closed_only=True) item = None for c in coords: # coords[1:]: if len(c) < 3: continue ng = ddd.polygon(c) #.clean(eps=char_size / 100) #.convex_hull() #ng.show() if item is None: item = ng elif item.contains(ng): item = item.subtract(ng) else: item = item.union(ng) item = item.clean( eps=self.char_size / 200) # Note that this is effectively limiting resolution #result = ddd.group([ddd.polygon(c) for c in coords], empty=2) result = item result = result.scale([1.0 / self.char_size, -1.0 / self.char_size]) result = result.simplify( 0.005) # Note that this is effectively limiting resolution return (result, face)