From eac89409b80c8f61efc98a20b4d742c878cde637 Mon Sep 17 00:00:00 2001
From: jaseg <git@jaseg.de>
Date: Sat, 30 Jan 2021 00:27:43 +0100
Subject: Add KiCAD sexp output

---
 Makefile              |  7 +++--
 README.md             | 57 ++++++++++++++++++++++++++++++++++
 include/gerbolyze.hpp | 19 ++++++++++++
 src/main.cpp          | 25 +++++++++++++--
 src/out_sexp.cpp      | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 190 insertions(+), 4 deletions(-)
 create mode 100644 README.md
 create mode 100644 src/out_sexp.cpp

diff --git a/Makefile b/Makefile
index a188e87..520100b 100644
--- a/Makefile
+++ b/Makefile
@@ -16,6 +16,7 @@ SOURCES := src/svg_color.cpp \
 	src/main.cpp \
 	src/out_svg.cpp \
 	src/out_gerber.cpp \
+	src/out_sexp.cpp \
 	src/out_flattener.cpp \
 	src/lambda_sink.cpp \
 
@@ -37,7 +38,9 @@ CXXFLAGS += $(shell $(PKG_CONFIG) --cflags pangocairo pugixml opencv4)
 LDFLAGS := -lm -lc -lstdc++
 LDFLAGS += $(shell $(PKG_CONFIG) --libs pangocairo pugixml opencv4)
 
-all: $(BUILDDIR)/svg-render
+TARGET := svg-render
+
+all: $(BUILDDIR)/$(TARGET)
 
 test.gbr test.svg &: render
 	./render test.svg > test.gbr
@@ -46,7 +49,7 @@ $(BUILDDIR)/%.o: %.cpp
 	@mkdir -p $(dir $@) 
 	$(CXX) -c $(CXXFLAGS) $(CXXFLAGS) $(INCLUDES) -o $@ $^
 
-$(BUILDDIR)/svg-render: $(SOURCES:%.cpp=$(BUILDDIR)/%.o) $(BUILDDIR)/upstream/cpp-base64/base64.o
+$(BUILDDIR)/$(TARGET): $(SOURCES:%.cpp=$(BUILDDIR)/%.o) $(BUILDDIR)/upstream/cpp-base64/base64.o
 	@mkdir -p $(dir $@) 
 	$(CXX) $(CXXFLAGS) $(LDFLAGS) -o $@ $^
 	
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8fa711d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,57 @@
+build/svg-render 2.0
+
+Usage: build/svg-render [options]... [input_file] [output_file]
+
+Specify "-" for stdin/stdout.
+
+    -h, --help
+        Print help and exit
+    -v, --version
+        Print version and exit
+    -o, --format
+        Output format. Supported: gerber, svg, s-exp (KiCAD S-Expression)
+    -p, --precision
+        Number of decimal places use for exported coordinates (gerber: 1-9,
+        SVG: 0-*)
+    --clear-color
+        SVG color to use for "clear" areas (default: white)
+    --dark-color
+        SVG color to use for "dark" areas (default: black)
+    -d, --trace-space
+        Minimum feature size of elements in vectorized graphics
+        (trace/space) in mm. Default: 0.1mm.
+    --no-header
+        Do not export output format header/footer, only export the
+        primitives themselves
+    --flatten
+        Flatten output so it only consists of non-overlapping white
+        polygons. This perform composition at the vector level. Potentially slow.
+    --no-flatten
+        Disable automatic flattening for KiCAD S-Exp export
+    -g, --only-groups
+        Comma-separated list of group IDs to export.
+    -b, --vectorizer
+        Vectorizer to use for bitmap images. One of poisson-disc (default),
+        hex-grid, square-grid, binary-contours, dev-null.
+    --vectorizer-map
+        Map from image element id to vectorizer. Overrides --vectorizer.
+        Format: id1=vectorizer,id2=vectorizer,...
+    --force-svg
+        Force SVG input irrespective of file name
+    --force-png
+        Force bitmap graphics input irrespective of file name
+    -s, --size
+        Bitmap mode only: Physical size of output image in mm. Format: 12.34x56.78
+    --sexp-mod-name
+        Module name for KiCAD S-Exp output
+    --sexp-layer
+        Layer for KiCAD S-Exp output
+    -a, --preserve-aspect-ratio
+        Bitmap mode only: Preserve aspect ratio of image. Allowed values
+        are meet, slice. Can also parse full SVG preserveAspectRatio syntax.
+    --no-usvg
+        Do not preprocess input using usvg (do not use unless you know
+        *exactly* what you're doing)
+    -e, --exclude-groups
+        Comma-separated list of group IDs to exclude from export. Takes
+        precedence over --only-groups.
diff --git a/include/gerbolyze.hpp b/include/gerbolyze.hpp
index 1d4f9bd..af25571 100644
--- a/include/gerbolyze.hpp
+++ b/include/gerbolyze.hpp
@@ -220,6 +220,25 @@ namespace gerbolyze {
         d2p m_offset;
     };
 
+    class KicadSexpOutput : public StreamPolygonSink {
+    public:
+        KicadSexpOutput(std::ostream &out, std::string mod_name, std::string layer, bool only_polys=false, std::string m_ref_text="", std::string m_val_text="G*****", d2p ref_pos={0,10}, d2p val_pos={0,-10});
+        virtual ~KicadSexpOutput() {}
+        virtual KicadSexpOutput &operator<<(const Polygon &poly);
+        virtual KicadSexpOutput &operator<<(GerberPolarityToken pol);
+        virtual void header_impl(d2p origin, d2p size);
+        virtual void footer_impl();
+
+    private:
+        std::string m_mod_name;
+        std::string m_layer;
+        std::string m_ref_text;
+        std::string m_val_text;
+        d2p m_ref_pos;
+        d2p m_val_pos;
+    };
+
+
     /* TODO
     class SExpOutput : public StreamPolygonSink {
     public:
diff --git a/src/main.cpp b/src/main.cpp
index 915b71f..52110b6 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -46,6 +46,9 @@ int main(int argc, char **argv) {
             {"flatten", {"--flatten"},
                 "Flatten output so it only consists of non-overlapping white polygons. This perform composition at the vector level. Potentially slow.",
                 0},
+            {"no_flatten", {"--no-flatten"},
+                "Disable automatic flattening for KiCAD S-Exp export",
+                0},
             {"only_groups", {"-g", "--only-groups"},
                 "Comma-separated list of group IDs to export.",
                 1},
@@ -64,6 +67,12 @@ int main(int argc, char **argv) {
             {"size", {"-s", "--size"},
                 "Bitmap mode only: Physical size of output image in mm. Format: 12.34x56.78",
                 1},
+            {"sexp_mod_name", {"--sexp-mod-name"},
+                "Module name for KiCAD S-Exp output",
+                1},
+            {"sexp_layer", {"--sexp-layer"},
+                "Layer for KiCAD S-Exp output",
+                1},
             {"preserve_aspect_ratio", {"-a", "--preserve-aspect-ratio"},
                 "Bitmap mode only: Preserve aspect ratio of image. Allowed values are meet, slice. Can also parse full SVG preserveAspectRatio syntax.",
                 1},
@@ -151,6 +160,7 @@ int main(int argc, char **argv) {
     string fmt = args["ofmt"] ? args["ofmt"] : "gerber";
     transform(fmt.begin(), fmt.end(), fmt.begin(), [](unsigned char c){ return std::tolower(c); }); /* c++ yeah */
 
+    bool force_flatten = false;
     PolygonSink *sink = nullptr;
     PolygonSink *flattener = nullptr;
     if (fmt == "svg") {
@@ -161,6 +171,17 @@ int main(int argc, char **argv) {
     } else if (fmt == "gbr" || fmt == "grb" || fmt == "gerber") {
         sink = new SimpleGerberOutput(*out_f, only_polys, 4, precision);
 
+    } else if (fmt == "s-exp" || fmt == "sexp" || fmt == "kicad") {
+        if (!args["sexp_mod_name"] || !args["sexp_layer"]) {
+            cerr << "--sexp-mod-name and --sexp-layer must be given for sexp export" << endl;
+            argagg::fmt_ostream fmt(cerr);
+            fmt << usage.str() << argparser;
+            return EXIT_FAILURE;
+        }
+
+        sink = new KicadSexpOutput(*out_f, args["sexp_mod_name"], args["sexp_layer"], only_polys);
+        force_flatten = true;
+
     } else {
         cerr << "Unknown output format \"" << fmt << "\"" << endl;
         argagg::fmt_ostream fmt(cerr);
@@ -168,7 +189,7 @@ int main(int argc, char **argv) {
         return EXIT_FAILURE;
     }
 
-    if (args["flatten"]) {
+    if (args["flatten"] || (force_flatten && !args["no_flatten"])) {
         flattener = new Flattener(*sink);
     }
 
@@ -294,7 +315,7 @@ int main(int argc, char **argv) {
     }
 
     if (args["skip_usvg"]) {
-        cerr << "skippint usvg" << endl; 
+        cerr << "skipping usvg" << endl; 
         frob = barf;
 
     } else {
diff --git a/src/out_sexp.cpp b/src/out_sexp.cpp
new file mode 100644
index 0000000..90adef2
--- /dev/null
+++ b/src/out_sexp.cpp
@@ -0,0 +1,86 @@
+/*
+ * This file is part of gerbolyze, a vector image preprocessing toolchain 
+ * Copyright (C) 2021 Jan Sebastian Götte <gerbolyze@jaseg.de>
+ * 
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * 
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <cmath>
+#include <algorithm>
+#include <string>
+#include <iostream>
+#include <iomanip>
+#include <gerbolyze.hpp>
+#include <svg_import_defs.h>
+#include <ctime>
+
+using namespace gerbolyze;
+using namespace std;
+
+
+KicadSexpOutput::KicadSexpOutput(ostream &out, string mod_name, string layer, bool only_polys, string ref_text, string val_text, d2p ref_pos, d2p val_pos)
+    : StreamPolygonSink(out, only_polys),
+    m_mod_name(mod_name),
+    m_layer(layer),
+    m_val_text(val_text),
+    m_ref_pos(ref_pos),
+    m_val_pos(val_pos)
+{
+    if (ref_text.empty()) {
+        m_ref_text = mod_name;
+    } else {
+        m_ref_text = ref_text;
+    }
+}
+
+void KicadSexpOutput::header_impl(d2p, d2p) {
+    auto tedit = std::time(0);
+    m_out << "(module " << m_mod_name << " (layer F.Cu) (tedit " << std::hex << std::setfill('0') << std::setw(8) << tedit << ")" << endl;
+    m_out << "  (fp_text reference " << m_ref_text << " (at " << m_ref_pos[0] << " " << m_ref_pos[1] << ") (layer F.SilkS) hide" << endl;
+    m_out << "    (effects (font (size 1 1) (thickness 0.15)))" << endl;
+    m_out << "  )" << endl;
+    m_out << "  (fp_text value " << m_val_text << " (at " << m_val_pos[0] << " " << m_val_pos[1] << ") (layer F.SilkS) hide" << endl;
+    m_out << "    (effects (font (size 1 1) (thickness 0.15)))" << endl;
+    m_out << "  )" << endl;
+}
+
+KicadSexpOutput &KicadSexpOutput::operator<<(GerberPolarityToken pol) {
+    if (pol == GRB_POL_CLEAR) {
+        cerr << "Warning: clear polarity not supported since KiCAD manages to have an even worse graphics model than gerber, except it can't excuse itself by its age..... -.-" << endl;
+    }
+
+    return *this;
+}
+
+KicadSexpOutput &KicadSexpOutput::operator<<(const Polygon &poly) {
+    if (poly.size() < 3) {
+        cerr << "Warning: " << poly.size() << "-element polygon passed to KicadSexpOutput" << endl;
+        return *this;
+    }
+
+    m_out << "  (fp_poly (pts";
+    for (auto &p : poly) {
+        m_out << " (xy " << p[0] << " " << p[1] << ")";
+    }
+    m_out << ")";
+    m_out << " (layer " << m_layer << ") (width 0))" << endl;
+
+    return *this;
+}
+
+void KicadSexpOutput::footer_impl() {
+    m_out << ")" << endl;
+}
+
+
-- 
cgit