def __convert_to_svgpath(self): location_A = (0 + 0j) location_B = (self.xB + self.yB * self.thickness_ratio * 1j) location_C = (1 + 0j) location_D = (self.xD + (self.yB - 1.0) * self.thickness_ratio * 1j) control_1 = (0 + self.y1 * location_B.imag * 1j) control_2 = (self.x2 * location_B.real + location_B.imag * 1j) control_3 = (self.x3 * (1.0 - location_B.real) + location_B.real + location_B.imag * 1j) control_6 = (self.x6 * (1.0 - location_D.real) + location_D.real + location_D.imag * 1j) control_7 = (self.x2 * location_D.real + location_D.imag * 1j) control_8 = (0 + self.y8 * location_D.imag * 1j) y4 = location_C.imag + self.y4 * (location_B.imag - location_C.imag) x4 = location_C.real - self.x4 * (location_C.real - control_3.real) control_4 = (x4 + y4 * 1j) x5 = control_4.real - self.x5 * (control_4.real - control_6.real) y5 = control_4.imag - self.y5 * (control_4.imag - control_6.imag) control_5 = (x5 + y5 * 1j) return svg.Path( svg.CubicBezier(start=location_A, control1=control_1, control2=control_2, end=location_B), svg.CubicBezier(start=location_B, control1=control_3, control2=control_4, end=location_C), svg.CubicBezier(start=location_C, control1=control_5, control2=control_6, end=location_D), svg.CubicBezier(start=location_D, control1=control_7, control2=control_8, end=location_A))
def fromCircleDef(cls, center: float, radius: float) -> 'SvgPath': ''' PyCut Tab import in svg viewer ''' NB_SEGMENTS = 12 angles = [ float(k * M_PI) / (NB_SEGMENTS) for k in range(NB_SEGMENTS * 2 + 1) ] discretized_svg_path : List[complex] = [ complex( \ center[0] + radius*math.cos(angle), center[1] + radius*math.sin(angle) ) for angle in angles] svg_path = svgpathtools.Path() for i in range(len(discretized_svg_path) - 1): start = discretized_svg_path[i] end = discretized_svg_path[i + 1] svg_path.append(svgpathtools.Line(start, end)) return SvgPath("pycut_tab", {'d': svg_path.d()})
def add_fillet_to_path(self, d): p = svgpathtools.parse_path(d) p = remove_zero_length_segments( p) # for z, a zero length line segment is possibly added if len(p) <= 1: return d new_p = svgpathtools.Path() self._prev_t = 0 # used as cache self._very_first_t = None # update first segment if closed if isclosedac(p): for i in range(len(p)): new_p.extend(self._calc_fillet_for_joint(p, i)) if not isclosedac(new_p): del new_p[0] # remove first segment if closed else: for i in range(len(p) - 1): new_p.extend(self._calc_fillet_for_joint(p, i)) new_p = round_path(new_p, 6) # inkex.errormsg(d_str(new_p, use_closed_attrib=True, rel=True)) return d_str(new_p, use_closed_attrib=True, rel=True)
def colorize(args): input_file = args.infile output_file = args.outfile if input_file == "" or output_file == "": logging.error("Input or output file names are missing") raise ValueError("Input and output files are needed") logging.info("input file {} output file {}".format(input_file, output_file)) paths, attributes, svg_attributes = spt.svg2paths2(input_file) # each segment must be a path in order to assign a color new_paths = [] for path in paths: for segment in path: new_paths.append(spt.Path(segment)) nb_segments = len(new_paths) col = ['r', 'g', 'b', 'k'] segment_col = ''.join([col[i % len(col)] for i in range(nb_segments)]) spt.wsvg(new_paths, colors=segment_col, stroke_widths=[2] * nb_segments, filename=output_file)
def _bbox(self): return SPT.Path(self._path().d()).bbox()
def convert_and_write_svg(cubic, filename): cubic_path = svgpathtools.Path(cubic) cubic_ctrl = svgpathtools.Path( svgpathtools.Line(cubic.start, cubic.control1), svgpathtools.Line(cubic.control1, cubic.control2), svgpathtools.Line(cubic.control2, cubic.end)) cubic_color = (50, 50, 200) cubic_ctrl_color = (150, 150, 150) r = 4.0 paths = [cubic_path, cubic_ctrl] colors = [cubic_color, cubic_ctrl_color] dots = [ cubic_path[0].start, cubic_path[0].control1, cubic_path[0].control2, cubic_path[0].end ] ncols = ['green', 'green', 'green', 'green'] nradii = [r, r, r, r] stroke_widths = [3.0, 1.5] def add_quadratic(q): paths.append(q) q_ctrl = svgpathtools.Path(svgpathtools.Line(q.start, q.control), svgpathtools.Line(q.control, q.end)) paths.append(q_ctrl) colors.append((200, 50, 50)) # q_color colors.append((150, 150, 150)) # q_ctrl_color dots.append(q.start) dots.append(q.control) dots.append(q.end) ncols.append('purple') ncols.append('purple') ncols.append('purple') nradii.append(r) nradii.append(r) nradii.append(r) stroke_widths.append(3.0) stroke_widths.append(1.5) prec = 1.0 queue = [cubic] num_quadratics = 0 while len(queue) > 0: c = queue[-1] queue = queue[:-1] # Criteria for conversion # http://caffeineowl.com/graphics/2d/vectorial/cubic2quad01.html p = c.end - 3 * c.control2 + 3 * c.control1 - c.start d = math.sqrt(p.real * p.real + p.imag * p.imag) * math.sqrt(3.0) / 36 t = math.pow(1.0 / d, 1.0 / 3.0) if t < 1.0: c0, c1 = split_cubic(c, 0.5) queue.append(c0) queue.append(c1) else: quadratic = cubic_to_quadratic(c) print(quadratic) add_quadratic(quadratic) num_quadratics += 1 print('num_quadratics:', num_quadratics) svgpathtools.wsvg(paths, colors=colors, stroke_widths=stroke_widths, nodes=dots, node_colors=ncols, node_radii=nradii, filename=filename)
def remove_zero_length_segments(p, eps=1e-6): "z will add a zero length line segment" return svgpathtools.Path(*filter(lambda seg: seg.length() > eps, p))
def _calc_fillet_for_joint(self, p, i): seg1 = p[(i) % len(p)] seg2 = p[(i + 1) % len(p)] ori_p = svgpathtools.Path(seg1, seg2) new_p = svgpathtools.Path() # ignore the node if G1 continuity tg1 = seg1.unit_tangent(1.0) tg2 = seg2.unit_tangent(0.0) cosA = abs(tg1.real * tg2.real + tg1.imag * tg2.imag) if abs(cosA - 1.0) < 1e-6: new_p.append(seg1.cropped(self._prev_t, 1.0)) self._prev_t = 0.0 if self._very_first_t is None: self._very_first_t = 1.0 if not isclosedac(p) and i == len(p) - 2: new_p.append(seg2.cropped( 0.0, 1.0)) # add last segment if not closed else: cir = self.circle(seg1.end, self.options.radius) # new_p.extend(cir) intersects = ori_p.intersect(cir) if len(intersects) != 2: inkex.errormsg( "Some fillet or chamfer may not be drawn: %d intersections!" % len(intersects)) new_p.append(seg1.cropped(self._prev_t, 1.0)) self._prev_t = 0.0 if self._very_first_t is None: self._very_first_t = 1.0 if not isclosedac(p) and i == len(p) - 2: new_p.append(seg2.cropped( 0.0, 1.0)) # add last segment if not closed else: cb = [] segs = [] ts = [] for (T1, seg1, t1), (T2, seg2, t2) in intersects: c1 = seg1.point(t1) tg1 = seg1.unit_tangent(t1) * (self.options.radius * KAPPA) cb.extend([c1, tg1]) segs.append(seg1) ts.append(t1) # cir1 = self.circle(c1, self.options.radius * KAPPA) # new_p.extend(cir1) # new_p.append(svgpathtools.Line(c1, c1+tg1)) assert len(cb) == 4 new_p.append(segs[0].cropped(self._prev_t, ts[0])) if self.options.fillet_type == 'fillet': fillet = svgpathtools.CubicBezier(cb[0], cb[0] + cb[1], cb[2] - cb[3], cb[2]) else: fillet = svgpathtools.Line(cb[0], cb[2]) new_p.append(fillet) self._prev_t = ts[1] if self._very_first_t is None: self._very_first_t = ts[0] if isclosedac(p) and i == len(p) - 1: new_p.append(segs[1].cropped( ts[1], self._very_first_t)) # update first segment if closed elif not isclosedac(p) and i == len(p) - 2: new_p.append(segs[1].cropped( ts[1], 1.0)) # add last segment if not closed # # fix for the first segment # if p.isclosed(): # new_p[0] = p[0].cropped(ts[1], self._very_first_t) # new_p.append(segs[0].cropped(ts[0], 1.0)) # new_p.append(segs[1].cropped(0.0, ts[1])) # if self.options.fillet_type == 'fillet': # fillet = svgpathtools.CubicBezier(cb[0], cb[0]+cb[1], cb[2]-cb[3], cb[2]) # else: # fillet = svgpathtools.Line(cb[0], cb[2]) # new_p.append(fillet.reversed()) return new_p
def offset_paths(path, offset_distance, steps=100, debug=False): """Takes an svgpathtools.path.Path object, `path`, and a float distance, `offset_distance`, and returns the parallel offset curves (in the form of a list of svgpathtools.path.Path objects).""" def is_enclosed(path, check_paths): """`path` is an svgpathtools.path.Path object, `check_paths` is a list of svgpath.path.Path objects. This function returns True if `path` lies inside any of the paths in `check_paths`, and returns False if it lies outside all of them.""" seg = path[0] point = seg.point(0.5) for i in range(len(check_paths)): test_path = check_paths[i] if path == test_path: continue # find outside_point, which lies outside other_path (xmin, xmax, ymin, ymax) = test_path.bbox() outside_point = complex(xmax+100, ymax+100) if svgpathtools.path_encloses_pt(point, outside_point, test_path): if debug: print("point is within path", i, file=sys.stderr) return True return False # This only works on closed paths. if debug: print("input path:", file=sys.stderr) if debug: print(path, file=sys.stderr) if debug: print("offset:", offset_distance, file=sys.stderr) assert(path.isclosed()) # # First generate a list of Path elements (Lines and Arcs), # corresponding to the offset versions of the Path elements in the # input path. # if debug: print("generating offset segments...", file=sys.stderr) offset_path_list = [] for seg in path: if type(seg) == svgpathtools.path.Line: start = seg.point(0) + (offset_distance * seg.normal(0)) end = seg.point(1) + (offset_distance * seg.normal(1)) offset_path_list.append(svgpathtools.Line(start, end)) if debug: print(" ", offset_path_list[-1], file=sys.stderr) elif type(seg) == svgpathtools.path.Arc and (seg.radius.real == seg.radius.imag): # Circular arcs remain arcs, elliptical arcs become linear # approximations below. # # Polygons (input paths) are counter-clockwise. # # Positive offsets are to the inside of the polygon, negative # offsets are to the outside. # # If this arc is counter-clockwise (sweep == False), # *subtract* the `offset_distance` from its radius, so # insetting makes the arc smaller and outsetting makes # it larger. # # If this arc is clockwise (sweep == True), *add* the # `offset_distance` from its radius, so insetting makes the # arc larger and outsetting makes it smaller. # # If the radius of the offset arc is negative, use its # absolute value and invert the sweep. if seg.sweep == False: new_radius = seg.radius.real - offset_distance else: new_radius = seg.radius.real + offset_distance start = seg.point(0) + (offset_distance * seg.normal(0)) end = seg.point(1) + (offset_distance * seg.normal(1)) sweep = seg.sweep flipped = False if new_radius < 0.0: if debug: print(" inverting Arc!", file=sys.stderr) flipped = True new_radius = abs(new_radius) sweep = not sweep if new_radius > 0.002: radius = complex(new_radius, new_radius) offset_arc = svgpathtools.path.Arc( start = start, end = end, radius = radius, rotation = seg.rotation, large_arc = seg.large_arc, sweep = sweep ) offset_path_list.append(offset_arc) elif new_radius > epsilon: # Offset Arc radius is smaller than the minimum that # LinuxCNC accepts, replace with a Line. if debug: print(" arc too small, replacing with a line", file=sys.stderr) if flipped: old_start = start start = end end = old_start offset_arc = svgpathtools.path.Line(start = start, end = end) offset_path_list.append(offset_arc) else: # Zero-radius Arc, it disappeared. if debug: print(" arc way too small, removing", file=sys.stderr) continue if debug: print(" ", offset_path_list[-1], file=sys.stderr) else: # Deal with any segment that's not a line or a circular arc. # This includes elliptic arcs and bezier curves. Use linear # approximation. # # FIXME: Steps should probably be computed dynamically to make # the length of the *offset* line segments manageable. points = [] for k in range(steps+1): t = k / float(steps) normal = seg.normal(t) offset_vector = offset_distance * normal points.append(seg.point(t) + offset_vector) for k in range(len(points)-1): start = points[k] end = points[k+1] offset_path_list.append(svgpathtools.Line(start, end)) if debug: print(" (long list of short lines)", file=sys.stderr) # # Find all the places where one segment intersects the next, and # trim to the intersection. # if debug: print("trimming intersecting segments...", file=sys.stderr) for i in range(len(offset_path_list)): this_seg = offset_path_list[i] if (i+1) < len(offset_path_list): next_seg = offset_path_list[i+1] else: next_seg = offset_path_list[0] # FIXME: I'm not sure about this part. if debug: print("intersecting", file=sys.stderr) if debug: print(" this", this_seg, file=sys.stderr) if debug: print(" next", next_seg, file=sys.stderr) intersections = this_seg.intersect(next_seg) if debug: print(" intersections:", intersections, file=sys.stderr) if len(intersections) > 0: intersection = intersections[0] point = this_seg.point(intersection[0]) if debug: print(" intersection point:", point, file=sys.stderr) if not complex_close_enough(point, this_seg.end): this_seg.end = this_seg.point(intersection[0]) next_seg.start = this_seg.end # # Find all the places where adjacent segments do not end/start close # to each other, and join them with Arcs. # if debug: print("joining non-connecting segments with arcs...", file=sys.stderr) joined_offset_path_list = [] for i in range(len(offset_path_list)): this_seg = offset_path_list[i] if (i+1) < len(offset_path_list): next_seg = offset_path_list[i+1] else: next_seg = offset_path_list[0] if complex_close_enough(this_seg.end, next_seg.start): joined_offset_path_list.append(this_seg) continue if debug: print("these segments don't touch end to end:", file=sys.stderr) if debug: print(this_seg, file=sys.stderr) if debug: print(next_seg, file=sys.stderr) if debug: print(" error:", this_seg.end-next_seg.start, file=sys.stderr) # FIXME: Choose values for `large_arc` and `sweep` correctly here. # I think the goal is to make the joining arc tangent to the segments it joins. # large_arc should always be False # sweep means "clockwise" (but +Y is down) if debug: print("determining joining arc:", file=sys.stderr) if debug: print(" this_seg ending normal:", this_seg.normal(1), file=sys.stderr) if debug: print(" next_seg starting normal:", next_seg.normal(0), file=sys.stderr) sweep_arc = svgpathtools.path.Arc( start = this_seg.end, end = next_seg.start, radius = complex(offset_distance, offset_distance), rotation = 0, large_arc = False, sweep = True ) sweep_start_error = this_seg.normal(1) - sweep_arc.normal(0) sweep_end_error = next_seg.normal(0) - sweep_arc.normal(1) sweep_error = pow(abs(sweep_start_error), 2) + pow(abs(sweep_end_error), 2) if debug: print(" sweep arc starting normal:", sweep_arc.normal(0), file=sys.stderr) if debug: print(" sweep arc ending normal:", sweep_arc.normal(1), file=sys.stderr) if debug: print(" sweep starting error:", sweep_start_error, file=sys.stderr) if debug: print(" sweep end error:", sweep_end_error, file=sys.stderr) if debug: print(" sweep error:", sweep_error, file=sys.stderr) antisweep_arc = svgpathtools.path.Arc( start = this_seg.end, end = next_seg.start, radius = complex(offset_distance, offset_distance), rotation = 0, large_arc = False, sweep = False ) antisweep_start_error = this_seg.normal(1) - antisweep_arc.normal(0) antisweep_end_error = next_seg.normal(0) - antisweep_arc.normal(1) antisweep_error = pow(abs(antisweep_start_error), 2) + pow(abs(antisweep_end_error), 2) if debug: print(" antisweep arc starting normal:", antisweep_arc.normal(0), file=sys.stderr) if debug: print(" antisweep arc ending normal:", antisweep_arc.normal(1), file=sys.stderr) if debug: print(" antisweep starting error:", antisweep_start_error, file=sys.stderr) if debug: print(" antisweep end error:", antisweep_end_error, file=sys.stderr) if debug: print(" antisweep error:", antisweep_error, file=sys.stderr) joining_arc = None if sweep_error < antisweep_error: if debug: print("joining arc is sweep", file=sys.stderr) joining_arc = sweep_arc else: if debug: print("joining arc is antisweep", file=sys.stderr) joining_arc = antisweep_arc if debug: print("joining arc:", file=sys.stderr) if debug: print(joining_arc, file=sys.stderr) if debug: print(" length:", joining_arc.length(), file=sys.stderr) if debug: print(" start-end distance:", joining_arc.start-joining_arc.end, file=sys.stderr) # FIXME: this is kind of arbitrary joining_seg = joining_arc if joining_arc.length() < 1e-4: joining_seg = svgpathtools.path.Line(joining_arc.start, joining_arc.end) if debug: print(" too short! replacing with a line:", joining_seg, file=sys.stderr) joined_offset_path_list.append(this_seg) joined_offset_path_list.append(joining_seg) offset_path_list = joined_offset_path_list # # Find the places where the path intersects itself, split into # multiple separate paths in those places. # if debug: print("splitting path at intersections...", file=sys.stderr) offset_paths_list = split_path_at_intersections(offset_path_list) # # Smooth the path: adjacent segments whose start/end points are # "close enough" to each other are adjusted so they actually touch. # # FIXME: is this still needed? # if debug: print("smoothing paths...", file=sys.stderr) for path_list in offset_paths_list: for i in range(len(path_list)): this_seg = path_list[i] if (i+1) < len(path_list): next_seg = path_list[i+1] else: next_seg = path_list[0] if complex_close_enough(this_seg.end, next_seg.start): next_seg.start = this_seg.end else: if debug: print("gap in the path (seg %d and following):" % i, file=sys.stderr) if debug: print(" this_seg.end:", this_seg.end, file=sys.stderr) if debug: print(" next_seg.start:", next_seg.start, file=sys.stderr) # # Convert each path list to a Path object and sanity check. # if debug: print("converting path lists to paths...", file=sys.stderr) offset_paths = [] for path_list in offset_paths_list: offset_path = svgpathtools.Path(*path_list) if debug: print("offset path:", file=sys.stderr) if debug: print(offset_path, file=sys.stderr) assert(offset_path.isclosed()) offset_paths.append(offset_path) # # The set of paths we got from split_path_at_intersections() has # zero or more 'true paths' that we actually want to return, plus # zero or more 'false paths' that should be discarded. # # When offsetting a path to the inside, the false paths will be # outside the true path and will wind in the opposite direction of # the input path. # # When offsetting a path to the outside, the false paths will be # inside the true paths, and will wind in the same direction as the # input path. # # [citation needed] # if debug: print("pruning false paths...", file=sys.stderr) path_area = approximate_path_area(path) if debug: print("input path area:", path_area, file=sys.stderr) keepers = [] if offset_distance > 0: # The offset is positive (inwards), discard paths with opposite # direction from input path. for offset_path in offset_paths: if debug: print("checking path:", offset_path, file=sys.stderr) offset_path_area = approximate_path_area(offset_path) if debug: print("offset path area:", offset_path_area, file=sys.stderr) if path_area * offset_path_area < 0.0: # Input path and offset path go in the opposite directions, # drop offset path. if debug: print("wrong direction, dropping", file=sys.stderr) continue keepers.append(offset_path) else: # The offset is negative (outwards), discard paths that lie # inside any other path and have the same winding direction as # the input path. for offset_path in offset_paths: if debug: print("checking path:", offset_path, file=sys.stderr) if is_enclosed(offset_path, offset_paths): if debug: print(" enclosed", file=sys.stderr) # This path is enclosed, check the winding direction. offset_path_area = approximate_path_area(offset_path) if debug: print("offset path area:", offset_path_area, file=sys.stderr) if path_area * offset_path_area > 0.0: if debug: print(" winding is the same as input, dropping", file=sys.stderr) continue else: if debug: print(" winding is opposite input", file=sys.stderr) else: if debug: print(" not enclosed", file=sys.stderr) if debug: print(" keeping", file=sys.stderr) keepers.append(offset_path) offset_paths = keepers return offset_paths
def extract_lines(filepath, page_dir): doc = svgpathtools.Document(filepath) content_group = doc.get_group([None, "content"]) # bounds are in the format (xmin, xmax, ymin, ymax) page_bounds = doc.paths_from_group(content_group)[0].bbox() line_height = (page_bounds[3] - page_bounds[2]) / 15 lines = {} debug_lines = {} debug_nodes = {} for line_number in range(1, 16): lines[line_number] = svgpathtools.Path() debug_lines[line_number] = svgpathtools.Path() debug_nodes[line_number] = [] full_path = svgpathtools.Path() for doc_path in doc.paths(): for sub_path in doc_path: full_path.append(sub_path) indeterminate_paths = [] for _path in full_path.d().split("M"): if len(_path.strip()) > 0: glyph_path = svgpathtools.parse_path(f"M{_path}") line_number = detect_line_number(glyph_path.bbox(), page_bounds[2], line_height) if line_number: for path_command in glyph_path: lines[line_number].append(path_command) else: indeterminate_paths.append(glyph_path) indeterminate_paths = [ indeterminate_path_info(p, page_bounds[2], line_height) for p in indeterminate_paths ] indeterminate_num = len(indeterminate_paths) print(f"Found {indeterminate_num} indeterminate paths") indeterminate_paths.sort(key=lambda x: x[2]) for i, indeterminate_path in enumerate(indeterminate_paths): start_time = time.time() determination = detect_indeterminate_line(indeterminate_path, lines) if determination[0]: if determination[1]: debug_nodes[determination[0]].append(determination[1]) for path_command in indeterminate_path[0]: lines[determination[0]].append(path_command) debug_lines[determination[0]].append(path_command) print( f"Completed {i+1}/{indeterminate_num} path determations in {time.time() - start_time:.2} seconds" ) attribs = {"fill": "#000000", "fill-rule": "evenodd"} debug_attribs = { "stroke": "#FF0000", "stroke-width": "0.5", "fill-opacity": "0" } svg_attribs = { "xml:space": "preserve", "viewBox": "0 0 345 50", "width": "", "height": "" } for svg_line in lines.items(): if len(svg_line[1]) == 0: continue line_number = svg_line[0] y_pos = floor(svg_line[1].bbox()[2]) svg_attribs["viewBox"] = f"0 {y_pos} 345 50" filename = path.join(page_dir, f"{line_number}.svg") _paths = [svg_line[1]] _attribs = [attribs] _nodes = [] if debug_mode: _nodes = debug_nodes[line_number] if len(debug_lines[line_number]) > 0: _paths.append(debug_lines[line_number]) _attribs.append(debug_attribs) svgpathtools.wsvg(_paths, filename=filename, attributes=_attribs, svg_attributes=svg_attribs, nodes=_nodes) if debug_mode: filename = path.join(page_dir, "debug.svg") debug_paths = [full_path] svg_attribs["viewBox"] = "0 0 345 550" _attribs = [attribs] for line in range(1, 16): debug_paths.append( debug_overlay_path(page_bounds[2], line_height, line)) _attribs.append(debug_attribs) svgpathtools.wsvg( debug_paths, filename=filename, attributes=_attribs, svg_attributes=svg_attribs, )
def circle (r) : arcs = [] arcs.append(svg.Arc(-r + 0j, r + r * 1j, 0, False, False, r + 0j)) arcs.append(svg.Arc(-r + 0j, r + r * 1j, 0, False, True, r + 0j)) return svg.Path(*arcs)