10 May 2018

The common way to make modifications to sewing patterns is to trace out the cut lines on tracing paper, then cut and tape the pieces of paper together to form a new shape to cut out of fabric. However, as a software developer, every problem is a programming one, so in this post I’m going to show my code for extracting the cut lines from sewing patterns as SVGs, so that you can manipulate them in Inkscape.

The pattern I’m going to use in this demo is the Cheyenne Tunic by Hey June which I recommend purchasing. I use the pdfrw library for reading in the pattern, and investigating a pdf using this library can really help your understanding of the format.

Extracting data from PDFs is quite challenging. If you’re unfamiliar with PDFs, I recommend the O’Reilly book Developing with PDF.

Getting the Right Size

Most sewing patterns store the cut lines for different sizes in original content groups (known as layers in most PDF readers). This allows you to turn off the sizes you don’t want before printing.

the original content groups in the cheyenne tunic pattern

I set up a command argument parser to define the filename of the PDF and get the size to extract.

parser = argparse.ArgumentParser(
    description='Generate new pattern pieces from existing patterns')
parser.add_argument('--filename', type=str, help='The filename of the pdf pattern.')
parser.add_argument('--size', type=str, help="The size of the pattern to analyze.")

The content groups are defined both in the PDF document’s root, and on each page. The group keys for the content groups are defined under /Resources -> /Properties.

    args = parser.parse_args()
    x = PdfReader(args.filename, decompress=True)
    name = '(' + args.size + ')'
    shapes = []
    paths = []
    for page_num, page in enumerate(x.pages):
        if '/Resources' not in page:
            continue
        if '/Properties' not in page['/Resources']:
            continue
        oc_keyname = [key for key in page['/Resources']['/Properties']
                      if page['/Resources']['/Properties'][key]['/Name'] == name]

If there no original content groups matching the size we want, skip to the next page:

        if len(oc_keyname) == 0:
            continue

Another thing to extract out of the /Resources are the graphics states. If you want a faithful color representation of the graphics when converting PDF to SVG, you need these values. However, since these rules are a bit tricky, and I only care about the shapes, I am not doing anything meaningful with this information at the moment:

        gstates = {}
        if '/ExtGState' in page['/Resources']:
            gstates = page['/Resources']['/ExtGState']

The paths themselves will be in the page’s content stream. You want to start reading at the group key name, and then end at the end of that block, which is typically indicated by the EMC keyword:

        lines = page.Contents.stream.split('\n')
        start_index = [i for i, l in enumerate(lines) if l.find(oc_keyname) >= 0][0]
        end_index = \
        [i for i, l in enumerate(lines) if l.find('EMC') >= 0 and i > start_index][0]
        shape = "\n".join(lines[start_index:end_index])

PostScript Graphics to SVG

PDF graphics use a format similar to SVG for expressing shapes and paths, but uses a PostScript state machine for updating stylistic elements (like fill and stroke color) and for appending to paths. The instructions are written in reverse Polish notation, where the q operation pushes values onto the state machine and Q pops off the stack. I wrote a PostScript interpreter that goes over the instructions line by line, and adds shapes to a svgwrite Drawing. It’s by no means complete, but it’s good enough for my purposes. Some operators, like n, define clipping paths so that the cut lines render within the page window, and I ignore these.

def parse_shape(shape, i, gstates):
    # see https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PDF32000_2008.pdf
    output_filename = "page%s.svg" % i
    dwg = Drawing(output_filename, profile='tiny')
    fill = "none"
    stroke = rgb(0, 0, 0)
    stroke_width = 4
    transform = (1, 0, 0, 1, 0, 0)
    shapes_stack = []
    d = ""
    paths = []
    for line in shape.split("\n"):
        line = line.strip()
        parts = line.split(" ")
        nums = []
        for part in parts:
            try:
                nums.append(float(part))
            except ValueError:
                pass
        operation = parts[-1]
        if operation == 'BDC':
            continue
        elif operation == 'q':
            # q - start stack
            continue
        elif operation == 're':
            # rectangle
            vals = {'insert': (nums[0], nums[1]), 'size': (nums[2], nums[3])}
            if fill:
                vals['fill'] = fill
            if stroke:
                vals['stroke'] = stroke
            shapes_stack.append(dwg.rect(**vals))
        elif operation == 'n':
            # - clipping path
            continue
        elif operation == 'RG':
            # set stroke color
            stroke = rgb(*nums[0:3])
        elif operation == 'J':
            # not sure how to implement cap styles
            continue
        elif operation == 'cm':
            # current transformation matrix
            transform = nums[0:6]
        elif operation == 'F' or operation == 'f':
            # fill
            fill = rgb(*nums[0:3])
        elif operation == 'm':
            # move to
            d += "M " + format_pointstr(parts[0:2])
        elif operation == 'c':
            # curve
            d += " C " + format_pointstr(parts[0:6])
        elif operation == 'v':
            # append to bezier curve
            d += " S " + format_pointstr(parts[0:4])
        elif operation == 'y':
            d += " C " + format_pointstr(parts[0:4])+" "+format_pointstr(parts[2:4])
        elif operation == 'l':
            # line to
            d += " L " + format_pointstr(parts[0:2])
        elif operation == 'h':
            # make sure it's a closed path
            continue
        elif operation == 'S':
            # stroke to 4-unit width
            continue
        elif operation == 'Q':
            # end stack (draw)
            # apply transformation...
            for shape in shapes_stack:
                dwg.add(shape)
            if len(d) > 0:
                paths.append(d + " Z")
                d = " ".join([transform_str(p, transform) for p in d.split(" ")])
                vals = {'d': d}
                vals['stroke-width'] = stroke_width
                if fill:
                    vals['fill'] = fill
                if stroke:
                    vals['stroke'] = stroke

                dwg.add(dwg.path(**vals))
            d = ''
            shapes_stack = []
        elif operation == 'gs':
            key = parts[0]
            if key not in gstates:
                print("could not find state %s in dictionary")
            state = gstates[key]
            # color blending not yet implemented
            pass
        elif operation == 'w':
            stroke_width = nums[0]
        else:
            print("not sure what to do with %s %s" % (operation, line))
    dwg.save()
    return paths

PDF uses the same format as SVG for expressing transformations. Here’s my code for applying these transformations, which is copied from my fork of svgpathtools

def transform_point(point, matrix=(1, 0, 0, 1, 0, 0), format="float", relative=False):
    a, b, c, d, e, f = matrix
    if isinstance(point, list):
        x, y = point
    else:
        point_parts = point.split(',')
        if len(point_parts) >= 2:
            x, y = [float(x) for x in point_parts]
        else:
            # probably got a letter describing the point, i.e., m or z
            return point
    # if the transform is relative, don't apply the translation
    if relative:
        x, y = a * x + c * y, b * x + d * y
    else:
        x, y = a * x + c * y + e, b * x + d * y + f
    if format == "float":
        return x, y
    else:
        return "%s%s%s" % (x, point_separator, y)

Since the same cut lines are spread accross multiple pages, I want to eliminate duplicate cut lines, which I can do using set, since the shapes are all path strings:

paths = sorted(list(set(paths)))

Here’s all the unique cut lines for the XS shape:

all cut lines

Now that I have the cut lines in SVG format, I can programmatically manipulate them and then print them out again, but that’s a post for another day.



blog comments powered by Disqus