#!/usr/bin/perl
#
# pdf2gerb 1.6
#
# (c) 2010 Matthew M. Swann, swannman@mac.com - initial versions
# (c) 2012 djulien17@thejuliens.net (1.5 + 1.6) - I offer up these enhancements to our Grand Designer, and hoping to make it easier for other hobbyists to create PCBs.
#
# Recent rev history:
# Version  Date     Who  What
# 1.4      7/2011   MS   last public version from Matt
# 1.5a     4/7/12   DJ   add support for PDF 1.4 compression (flate decode)
# 1.5b     4/9/12   DJ   handle scale transform (was giving incorrect dimensions), warn about file too big and use consts (seems safer)
# 1.5c     4/10/12  DJ   fix filled circles, change drill fmt to 2.4 (drill coords were interpreted as 10x)
# 1.5d     4/11/12  DJ   set origin to lower left corner of PCB, draw large circles on silk screen using line segments
# 1.5e     4/12/12  DJ   use rectangular apertures for square/rectangular pads, accept multiple files (top + bottom + silk screen) and concatenate to look like 1 file with multiple layers, update usage message
# 1.5f     4/14/12  DJ   fix "." and \s in regex, added G04 for easier debug, add inverted/filled areas (layer polarity), placeholders for top + bottom solder masks
# 1.5g     4/20/12  DJ   restructured drawing loop to handle multiple stoke vs. fill commands (to support thermal pads, ground planes, solder masks), restructured main line code, only emit tool commands when needed, turned on strict + warnings, explicitly declare locals/globals ("my", "our")
# 1.5h     4/24/12  DJ   map scaled aperture and trace sizes to standard values, consolidate hole lists to minimize drill tool swapping, change aperture lists to use hash (faster lookups), undo larger holes if smaller hole found in same location
# 1.5i     4/25/12  DJ   generate solder masks (invert + enlarge all pads, no holes)
# 1.5j     4/28/12  DJ   added polygon fill (needed for ground plane and no-fill areas), allow metric units for non-US people
# 1.5k     5/1/12   DJ   added panelization; fixed polygon fill (nudge edges for more accurate edges); generate separate outline layer
# 1.6      5/5/12   DJ   misc fixes, released for testing
# 1.6a     5/6/12   DJ   trim panel overhangs even with 1 x 1 (by default), added some pad/hole sizes, allow rotated PDFs (landscape prints), allow x + y pad around panelization
# 1.6b     5/21/12  DJ   pre-scan multiple layers for PCB outline, don't use clip rect for outline, generate drill file on any layer (for Matt's test file)
# 1.6c     1/7/13   DJ   initialize visibility to Tristate value so both holes + pads will be recognized if no fill/stroke color set in PDF, treat singleton layer as copper, not silk
# 1.6d     1/30/13  DJ   insert dummy G54D10 command at start, in case there are no traces (avoids ViewPlot D00 message for outline file)
# 1.6e     2/1/13   DJ   added DRILL_FMT to allow 2.3 or 2.4 drill format, show version# in output files
# 1.6f     3/21/13  DJ   made \n after "stream" optional (newer PDFCreator omits it?); default WANT_STREAMS to FALSE; extract max 100 streams (for safety); use REDUCE_TOLERANCE const for adjustable tolerance on reduce logic
# 1.6g     3/28/13  GDM/DJ implement gray space drawing attr; change "\1" to "$1" to prevent perl warning; substitute circles for clip rects (SUBST_CIRCLE_CLIPRECT)
# 1.6h     4/11/13  DJ   allow \r\n between "<<" and "/FlateDecode"; make \n optional between commands; join commands that are split across lines; added more debug; force input to Unicode
# 1.6i     7/14/14  DJ   avoid /0 error for nudge line segment or polygon edge, avoid infinite loops for outline/fill unknown shapes, fix handling of 2 adjacent polygon edges parallel (shouldn't happen, though)
# 1.6j     9/30/15  DJ   fix an additional subscript error; perl short-circuit IF doesn't seem to be working
# 1.6k     1/2/16   DJ   undo attempt to compensate for Unicode; broke parser logic
# 1.6L     1/24/16  DJ   handle "re W" on same line, draw/fill bezier curves on silk screen (fill requires additional module), allow stand-alone line fill, add placeholder for curve offset
#
# TODO maybe:
# -elliptical pads? (draw short line seg using round aperture)
# -use G02/03/75 circular commands instead of drawing circles with line segments?
# -use hollow apertures? (pads are currently solid circles and hole is in center; this seems okay)
# -make it run faster? (not too bad now)
# -add command-line parameters instead of editing config constants?
# -exclude selected layers?
#
# Notes/Current limitations:
# - PCB outline is assumed to be rectangular
# - Holes in PDFs must be white circles; copper areas any color except white
# - Some CAD packages have origin in top left, but PDF is bottom left
# - Polygons and larger pads are filled with .001" lines; for non-rectangular ground planes, any points and intersections will be at least this wide (even if source CAD software shows them as points).
# - Polygons (ground planes) where the edges define internal "cut-out" areas will be treated as such, even if the CAD software fills them.
# - Larger pads that are filled will not have a solder mask opening (we don't want a solder mask opening on ground planes, for example).
# - Panelization will squash text or other display elements outside the PCB border to avoid interference with adjacent panels (by design).
#
# Helpful background links:
# (Gerber)
# Gerber intro:  http://www.apcircuits.com/resources/information/gerber_data.html
# G-codes + D-codes:  http://www.artwork.com/gerber/appl2.htm
# 274X format:  http://www.artwork.com/gerber/274x/rs274x.htm
# KiCAD Gerbers:  http://www.kxcad.net/visualcam/visualcam/tutorials/gerber_for_beginners.htm
# Excellon (drill file):  http://www.excellon.com/manuals/program.htm
# Creating Gerbers:  http://www.sparkfun.com/tutorials/109
# Gerbv (viewer):  http://gerbv.gpleda.org/index.html
# Viewplot (viewer):  http://www.viewplot.com
# Pdf2Gerb:  http://swannman.github.com/pdf2gerb/
# (Other)
# Cubic Bezier curves for circles:  http://www.tinaja.com/glib/ellipse4.pdf
# Polygon fill algorithm:  http://alienryderflex.com/polygon_fill/
# Point-in-polygon algoritm:  http://alienryderflex.com/polygon/
# Perl help:  http://www.perlmonks.org 
# PDFCreator 1.3.2 (CAREFUL: TURN OFF SPYWARE DURING INSTALL):  http://sourceforge.net/projects/pdfcreator/
# Strawberry Perl (for Windows):  http://www.strawberryperl.com
#
# More information about this work can be found at the following URL:
# http://swannman.github.com/pdf2gerb/
#
# This work is released under the terms and conditions set forth under
# the GNU General Public License 3.0.  For more details, see the following:
# http://www.gnu.org/licenses/gpl-3.0.txt
#
###########################################################################
use strict; #trap undef vars, etc (easier debug)
use warnings; #other useful info (easier debug)

use Cwd; #gets current directory
use Compress::Zlib; #needed for PDF1.4 decompression
use File::Spec; #Path::Class; #for folder name manipulation
use Time::HiRes qw(time); #for elapsed time calculation
use List::Util qw[min max];
use Encode; #::Detect::Detector; #for detecting charset encoding
#use Math::Bezier; #http://search.cpan.org/~abw/Math-Bezier-0.01/Bezier.pm

#are fwd defs needed?
#sub inches; #ToInches;
#sub inchesX;
#sub inchesY;
#sub ToDrillInches;
#sub GetAperture;
#sub GetDrillAperture;
#sub ComputeBezier;
#sub DebugPrint;
#sub FillRect;
#sub SetPolarity;
##sub min;
##sub max;

use constant VERSION => '1.6L';
#just a little warning; set realistic expectations:
printf "Pdf2Gerb.pl %s\nThis is EXPERIMENTAL software.  \nGerber files MAY CONTAIN ERRORS.  Please CHECK them before fabrication!\n\n", VERSION;

#Perl constants can supposedly be optimized at compile time, so here are some:
use constant { TRUE => 1, FALSE => 0, MAYBE => 2 }; #tri-state values
use constant { MININT => - 2 ** 31 - 1, MAXINT => 2 ** 31 - 1}; #big enough for simple arithmetic purposes
use constant { K => 1024, M => 1024 * 1024 }; #used for more concise display of numbers
use constant PI => 4 * atan2(1, 1); #used for circumference calculations

use constant METRIC => FALSE; #set to TRUE for metric units (only affect final numbers in output files, not internal arithmetic)
use constant APERTURE_LIMIT => 0; #34; #generate warnings if too many apertures are used (0 to not check)
use constant DRILL_FMT => '2.4'; #'2.3'; #'2.4' is the default for PCB fab; change to '2.3' for CNC

use constant WANT_DEBUG => 0; #10; #level of debug wanted; higher == more, lower == less, 0 == none
use constant GERBER_DEBUG => 0; #level of debug to include in Gerber file; DON'T USE FOR FABRICATION
use constant WANT_STREAMS => FALSE; #TRUE; #save decompressed streams to files (for debug)
use constant WANT_ALLINPUT => FALSE; #TRUE; #save entire input stream (for debug ONLY)

DebugPrint(sprintf("DEBUG: stdout %d, gerber %d, want streams? %d, all input? %d, O/S: $^O, Perl: $]\n", WANT_DEBUG, GERBER_DEBUG, WANT_STREAMS, WANT_ALLINPUT), 1);
#DebugPrint(sprintf("max int = %d, min int = %d\n", MAXINT, MININT), 1); 

#define standard trace and pad sizes to reduce scaling or PDF rendering errors:
#This avoids weird aperture settings and replaces them with more standardized values.
#(I'm not sure how photoplotters handle strange sizes).
#Fewer choices here gives more accurate mapping in the final Gerber files.
#units are in inches
use constant TOOL_SIZES => #add more as desired
(
#round or square pads (> 0) and drills (< 0):
    .031, -.014,  #used for vias
    .041, -.020,  #smallest non-filled plated hole
    .051, -.025,
    .056, -.029,  #useful for IC pins
    .070, -.033,
    .075, -.040,  #heavier leads
#    .090, -.043,  #NOTE: 600 dpi is not high enough resolution to reliably distinguish between .043" and .046", so choose 1 of the 2
    .100, -.046,
    .115, -.052,
    .130, -.061,
    .140, -.067,
    .150, -.079,
    .175, -.088,
    .190, -.093,
    .200, -.100,
    .220, -.110,
    .160, -.125,  #useful for mounting holes
#some additional pad sizes without holes (repeat a previous hole size if you just want the pad size):
    .090, -.040,  #want a .090 pad option, but use dummy hole size
    .065, -.040, #.035 x .065 rect pad
    .035, -.040, #.035 x .065 rect pad
#traces:
    .001,  #too thin for real traces; use only for board outlines
    .006,  #minimum real trace width; mainly used for text
    .008,  #mainly used for mid-sized text, not traces
    .010,  #minimum recommended trace width for low-current signals
    .012,
    .015,  #moderate low-voltage current
    .020,  #heavier trace for power, ground (even if a lighter one is adequate)
    .025,
    .030,  #heavy-current traces; be careful with these ones!
    .040,
    .050,
    .060,
    .080,
    .100,
    .120,
);
#Areas larger than the values below will be filled with parallel lines:
#This cuts down on the number of aperture sizes used.
#Set to 0 to always use an aperture or drill, regardless of size.
use constant { MAX_APERTURE => max((TOOL_SIZES)) + .004, MAX_DRILL => -min((TOOL_SIZES)) + .004 }; #max aperture and drill sizes (plus a little tolerance)
DebugPrint(sprintf("using %d standard tool sizes: %s, max aper %.3f, max drill %.3f\n", scalar((TOOL_SIZES)), join(", ", (TOOL_SIZES)), MAX_APERTURE, MAX_DRILL), 1);

#NOTE: Compare the PDF to the original CAD file to check the accuracy of the PDF rendering and parsing!
#for example, the CAD software I used generated the following circles for holes:
#CAD hole size:   parsed PDF diameter:      error:
#  .014                .016                +.002
#  .020                .02267              +.00267
#  .025                .026                +.001
#  .029                .03167              +.00267
#  .033                .036                +.003
#  .040                .04267              +.00267
#This was usually ~ .002" - .003" too big compared to the hole as displayed in the CAD software.
#To compensate for PDF rendering errors (either during CAD Print function or PDF parsing logic), adjust the values below as needed.
#units are pixels; for example, a value of 2.4 at 600 dpi = .004 inch, 2 at 600 dpi = .0033"
use constant
{
    HOLE_ADJUST => -2.6, #holes seemed to be slightly oversized (by .002" - .004"), so shrink them a little
    RNDPAD_ADJUST => -2, #-2.4, #round pads seemed to be slightly oversized, so shrink them a little
    SQRPAD_ADJUST => +.5, #square pads are sometimes too small by .00067, so bump them up a little
    RECTPAD_ADJUST => 0, #rectangular pads seem to be okay; actually, i didn't test them much :(
    TRACE_ADJUST => 0, #traces seemed to be okay
    REDUCE_TOLERANCE => .001, #allow this much variation when reducing circles and rects
};

#Also, my CAD's Print function or the PDF print driver I used was a little off for circles, so define some additional adjustment values here:
#Values are added to X/Y coordinates; units are pixels; for example, a value of 1 at 600 dpi would be ~= .002 inch
use constant
{
    CIRCLE_ADJUST_MINX => 0,
    CIRCLE_ADJUST_MINY => -1, #circles were a little too high, so nudge them a little lower
    CIRCLE_ADJUST_MAXX => +1, #circles were a little too far to the left, so nudge them a little to the right
    CIRCLE_ADJUST_MAXY => 0,
    SUBST_CIRCLE_CLIPRECT => TRUE #FALSE, #generate circle and substitute for clip rects (to compensate for the way some CAD software draws circles)
};

#allow .012 clearance around pads for solder mask:
#This value effectively adjusts pad sizes in the TOOL_SIZES list above (only for solder mask layers).
use constant SOLDER_MARGIN => +.012; #units are inches

#panelization:
#This will repeat the entire body the number of times indicated along the X or Y axes (files grow accordingly).
#Display elements that overhang PCB boundary can be squashed or left as-is (typically text or other silk screen markings).
#Set "overhangs" TRUE to allow over hangs, FALSE to truncate them.
#xpad and ypad allow margins to be added around outer edge of panelized PCB.
use constant PANELIZE => {'x' => 1, 'y' => 1, 'xpad' => 0, 'ypad' => 0, 'overhangs' => TRUE}; #number of times to repeat in X and Y directions

# Set this to 1 if you need TurboCAD support.
#$turboCAD = FALSE; #is this still needed as an option?

#PDF uses "points", each point = 1/72 inch
#combined with a PDF scale factor of .12, this gives 600 dpi resolution (1/72 * .12 = 600 dpi)
use constant INCHES_PER_POINT => 1/72; #0.0138888889; #multiply point-size by this to get inches

# The precision used when computing a bezier curve. Higher numbers are more precise but slower (and generate larger files).
#$bezierPrecision = 100;
use constant BEZIER_PRECISION => 36; #100; #use const; reduced for faster rendering (mainly used for silk screen and thermal pads)

# Ground planes and silk screen or larger copper rectangles or circles are filled line-by-line using this resolution.
use constant FILL_WIDTH => .01; #fill at most 0.01 inch at a time

# The max number of characters to read into memory
use constant MAX_BYTES => 10 * M; #bumped up to 10 MB, use const

my $runtime = time(); #Time::HiRes::gettimeofday(); #measure my execution time


###########################################################################
#Start of main logic:
###########################################################################

if ((scalar(@ARGV) < 1) || (scalar(@ARGV) > 3)) #allow up to 3 pdfs to define multiple layers in separate files
{
    my ($os, $prefix) = ($^O, ""); #$OSNAME
    if ($os =~ m/Win/) { $prefix = "perl"; } #bash-ify may not work on Windows (ie, without CygWin)
    print "Usage: $prefix pdf2gerb.pl <top-copper.pdf> [<bottom-copper.pdf>] [<top-silk.pdf>]\n";
    if ($prefix ne "") { print "On Windows, you may need to put \"perl\" at the start.\n"; }
    print "Output files will be placed in the current working folder.\n";
    exit;
}

# Used by the main routine to store layer names
our @layerTitles = ();

#moved up here so it's only done once:
# Which layer we're on
our $currentLayer = 0;

#keep track of overall board dimensions and origin:
our %pcbLayout = ();

#summary stats:
our ($numfiles, $totalLines, $warnings) = (0, 0, 0); #globals
our ($did_drill, $did_outline) = (FALSE, FALSE);

getfiles(); #read all input files
my $pdfContents = our $multiContents;

#debug input stream:
if (WANT_ALLINPUT) #save entire input stream (for debug ONLY)
{
    our $outputDir;
    my $filename = "all_input.txt";
    open my $outstream, ">$outputDir$filename";
    print $outstream $pdfContents;
    close $outstream;
    mywarn("[DEBUG] input stream saved to $outputDir$filename\n");
}

#pre-scan all layers to determine PCB size and origin (outline might not be on the first layer)
if (scalar(@layerTitles) > 1)
{
    our @lines = ();
    while ($pdfContents =~ m/BDC(.*?)EMC/gs)
    {
        my @morelines = split /\n/, $1;
        our $rot = shift(@morelines); #pull off rotation
        push(@lines, @morelines);
    }
    boundingRect(); #get pcb size and origin

    # Reset the match position to the beginning
    pos($pdfContents) = 0; #is this still needed?
}

# Break the file into layers (BDC...EMC)
while ($pdfContents =~ m/BDC(.*?)EMC/gs)
{
    # Break the layer into separate lines
    our @lines = split /\n/, $1;
    our $rot = shift(@lines); #pull off rotation

    # Make up a layer title if there wasn't one defined in the file
    if (scalar(@layerTitles) <= $currentLayer) { push(@layerTitles, "pdf2gerb"); } #layer type suffix will be added later
    DebugPrint("starting layer# $currentLayer $layerTitles[$currentLayer], rot $rot\n", 1);

    #moved down to here so it can be reset for each layer
    # Used by GetAperture as well as the main routine to store aperture defn's
    our %apertures = (); #changed to hash
    # Used by GetDrillAperture
    our %drillApertures = (); #changed to hash

    # Multiply value in points by this to get value in inches
    our $scaleFactor = INCHES_PER_POINT; #0.0138888889; #use const
    our ($offsetX, $offsetY) = (0, 0); #note: default PDF coordinate space has origin at lower left

    our $lastAperture = "";
    our $currentDrillAperture = "";
    our $lastStrokeWeight = 1; #default to 1 point
    #remember stroke vs. fill colors separately:
#    our %visibleFillColor = ('f' => TRUE, 's' => TRUE); #0 == white (hidden), !0 == !white (visible)
    our %visibleFillColor = ('f' => MAYBE, 's' => TRUE); #0 == white (hidden), !0 == !white (visible)
    our $layerPolarity = TRUE; #remember last LPD/LPC emitted; initial default = visible
    our ($startPositionX, $startPositionY) = (0, 0); #remember subpath start in case path needs to be closed again later (sometimes needed)
    our ($currentX, $currentY) = (0, 0); #current location in subpath
    my $currentLine = 0; #helpful for debug
    our @drawPath = (); #drawing path
    our %holes = (); #used for overlapped hole detection
    our %masks = (); #solder masks for each pad

    our $body = ""; # list of commands generated for current layer
    our %drillBody = (); #list of holes for each drill tool size; changed to hash

    #SetAperture(1); #xform scale factor not set yet
    boundingRect(); #get/check pcb size and origin

    foreach our $line (@lines) #main loop to process PDF drawing commands
    {
        ++$currentLine; #not too useful since it's relative to embedded PDF stream, but track it anyway for debug
        DebugPrint("line $currentLine: \"$line\"\n", 19);

        #process various types of PDF commands:
        if (ignore()) { next; }
        if (transforms()) { next; }
        if (drawingAttrs()) { next; }
        if (subpaths()) { next; }
        if (drawshapes()) { next; }
        #contact the authors if any others are important for your PCB
        mywarn(sprintf("ignored: line# $currentLine/%d", scalar(@lines)) . "$line\n");
    }
    $totalLines += $currentLine;
    refillholes(); #undo unneeded holes
    DebugPrint(sprintf("body length: %.0fK, drill body len: %.0fK\n", length($body)/K, length(join("", values %drillBody))/K), 2);

    #generate output files:
#    if ($currentLayer + 1 == scalar(@layerTitles)) { copper("silk"); } #assume LAST layer is silk screen
    if ($currentLayer && ($currentLayer + 1 == scalar(@layerTitles))) { copper("silk"); } #assume LAST layer is silk screen if not also first layer
    else #top and bottom copper
    {
        copper("copper");
        solder();
    }
    #only need one drill or outline file (should be the same for top + bottom); create for FIRST layer only:
    drill();
    edges();

    # Increment our layer counter
    DebugPrint("DONE with layer# $currentLayer $layerTitles[$currentLayer]\n", 1);
    ++$currentLayer;
    
    #print $header . $body . "M02*\n";
}
$runtime -= time(); #Time::HiRes::gettimeofday();
DebugPrint(sprintf("files processed: %d, layers: $currentLayer, src lines: $totalLines, warnings: $warnings\n", $numfiles), 0);
if ($numfiles) #show PCB sizes
{
    printf "pcb size is %5.3f x %5.3f, origin at (%5.3f, %5.3f) %s\n", inchesX($pcbLayout{'xmax'}), inchesY($pcbLayout{'ymax'}), inchesX($pcbLayout{'xmin'}), inchesY($pcbLayout{'ymin'}), METRIC? "mm": "inches";
    if (PANELIZE->{'x'} * PANELIZE->{'y'} > 1) { printf "panelized size is %5.3f x %5.3f %s\n", PANELIZE->{'x'} * inchesX($pcbLayout{'xmax'}), PANELIZE->{'y'} * inchesY($pcbLayout{'ymax'}), METRIC? "mm": "inches"; }
}
printf "total input stream size: %.0fK, processing time: %.2f sec\n-end-\n", length($pdfContents)/K, -$runtime; #time() - $^T; #$BASETIME


###########################################################################
#Input file parsing:
###########################################################################

#concatenate all input files:
#This is an alternative to defining multiple layers in a single PDF file.
#parameters: none (uses globals)
#return value: none (uses globals)
sub getfiles
{
    our ($numfiles, $multiContents, $outputDir, $grab_streams) = (0, "", "", 0); #initialize globals
    foreach my $pdfFilePath (@ARGV) #added outer loop
    {
        ++$numfiles;
        DebugPrint("processing file#$numfiles: $pdfFilePath ...\n", 0);

        # Calculate the output dir from the input file path
        #$pdfFilePath =~ m/^(.+)\/.+$/;
        if ($outputDir eq "") #set output dir first time only, then place all output files there
        {
            my ($vol, $dir, $filename) = File::Spec->splitpath($pdfFilePath);
            #just place output files into current directory (better for separation):
            ##$dir =~ s/\.\.\\//g; #place output in subfolder even if source files are in parent
            #$outputDir = $vol . $dir;
            if ($outputDir eq "") { $outputDir = cwd() . "/"; } #default to current directory
            DebugPrint("vol $vol, dir $dir, file $filename, outdir $outputDir\n", 5);
        }

        # Open the file for reading
        #added file size warning:
        unless (-e $pdfFilePath) { --$numfiles; mywarn("file missing: $pdfFilePath"); next; }
        my $filesize = -s $pdfFilePath;
        my $sizewarn = ($filesize > MAX_BYTES)? sprintf("TOO BIG (> %dMB)", MAX_BYTES / 1024 / 1024): "ok";
        DebugPrint("opening file $pdfFilePath, size $filesize $sizewarn ...\n", 1);

        open my $pdfFile, "< $pdfFilePath";
        binmode $pdfFile; #PDF 1.4 flate coding is binary, not ascii

        # Read in up to MAXBYTES
        read $pdfFile, my $rawPdfContents, MAX_BYTES;
        close $pdfFile; #close file after reading
#        $rawPdfContents = decode_utf8($rawPdfContents);
#NO        $rawPdfContents = Encode::decode('iso-8859-1', $rawPdfContents); #convert to Unicode
#        my $enctype = Encode::Detect::Detector::detect($rawPdfContents);
        DebugPrint(sprintf("got %d chars from input file $pdfFilePath\n", length($rawPdfContents)), 2);

        # Fix a problem where content lines end in \r (0x0D) and are unprintable
        #@rawLines = split /(\r\n|\n\r|\n|\r)/, $rawPdfContents;
        my @rawLines = split /(\r\n|\n\r|\n|\r)/, decompress($rawPdfContents, $pdfFilePath); #PDF 1.4 requires decompress
        chomp(@rawLines);
        my $pdfContents = join("\n", @rawLines);
        $pdfContents =~ s/\r//gs; #remove DOS carriage returns
        $pdfContents =~ s/\n\n/\n/gs; #remove blank lines
#        $pdfContents =~ s/\n(W\*? n)/ \1/gs; #join clip command with prev line to avoid confusion with regular rects
        $pdfContents =~ s/\n(W\*? n)/ $1/gs; #join clip command with prev line to avoid confusion with regular rects

        #some PDF editors join/split commands on a line, which makes parsing more complicated
        #try to fix it here:
        $pdfContents =~ s/(-?\d+\.?\d*)\s*\n\s*(c|-?\d+\.?\d*)\s+/$1 $2 /gs; #join c or other commands that are split across lines
        $pdfContents =~ s/(-?\d+\.?\d*\s+)(c|m)\s+(-?\d+\.?\d*)/$1$2\n$3/gs; #split c and m commands if on same line
        $pdfContents =~ s/(re|c|m|l)\s+(f|h|S|W)/$1\n$2/gs; #split re/c/m/l and f/h/S commands if on same line; also W
#        open my $outstream, ">$outputDir" . "pdfdebug.txt";
#        print $outstream $pdfContents;
#        close $outstream;
#        printf "wrote pdf contents to pdfdebug.txt\n";

        #silk screen layer seems to have a lot of independent strokes
        #string them together to cut down on silk layer size: