diff --git a/pd/nw/locales/de/translation.json b/pd/nw/locales/de/translation.json
index d9d820c8a6aceae4690605513d4c7286ed6e0aeb..fa694938bcd3a9c6c8ce99b525683999c0a56071 100644
--- a/pd/nw/locales/de/translation.json
+++ b/pd/nw/locales/de/translation.json
@@ -109,6 +109,8 @@
     "save_tt": "Speichere einen Pd-Patch auf der Festplatte",
     "saveas": "Speichern unter...",
     "saveas_tt": "Speichere einen Pd-Patch unter einem neuen Dateinamen",
+    "print": "Drucken...",
+    "print_tt": "Drucke einen Pd-Patch und speichere das Resultat in einer PDF-Datei",
     "message": "Nachricht...",
     "message_tt": "Sende eine Nachricht direkt an die laufende Pd-Instanz",
 
diff --git a/pd/nw/locales/en/translation.json b/pd/nw/locales/en/translation.json
index 14b758bc87b522bc63c27b264624f37cf77ea01c..8e83ee1d7ead3b9337cb560bd74d85cc2333129c 100644
--- a/pd/nw/locales/en/translation.json
+++ b/pd/nw/locales/en/translation.json
@@ -109,6 +109,8 @@
     "save_tt": "Save a Pd patch to disk",
     "saveas": "Save as...",
     "saveas_tt": "Save a Pd patch by manually choosing a filename",
+    "print": "Print...",
+    "print_tt": "Print a Pd patch, saving the result to a PDF file",
     "message": "Message...",
     "message_tt": "Send a message directly to the running Pd instance",
 
diff --git a/pd/nw/pd_canvas.html b/pd/nw/pd_canvas.html
index a960a8498d4270e4310080eaca00c5b24ab27753..78460d810e6619ebcb819b64dcaac35d71252c3f 100644
--- a/pd/nw/pd_canvas.html
+++ b/pd/nw/pd_canvas.html
@@ -15,6 +15,10 @@
       <input style="display:none;" id="saveDialog" type="file"
              nwsaveas nwworkingdir accept=".pd" />
     </span>
+    <span id = "printDialogSpan">
+      <input style="display:none;" id="printDialog" type="file"
+             nwsaveas nwworkingdir accept=".pdf" />
+    </span>
     <input style="display:none;" id="openpanel_dialog" type="file" />
     <input style="display:none;" id="savepanel_dialog" type="file"
            nwsaveas nwworkingdir />
diff --git a/pd/nw/pd_canvas.js b/pd/nw/pd_canvas.js
index 77defb3647f0f34ab59d00a66a9b199a489b8399..8c066b8ad374a30b78f62bc3f003fd1adf91e4b7 100644
--- a/pd/nw/pd_canvas.js
+++ b/pd/nw/pd_canvas.js
@@ -1057,6 +1057,13 @@ function nw_create_patch_window_menus(gui, w, name) {
             pdgui.menu_saveas(name);
         }
     });
+    minit(m.file.print, {
+        enabled: true,
+        click: function (){
+            pdgui.canvas_check_geometry(name);
+            pdgui.menu_print(name);
+        }
+    });
     minit(m.file.message, {
         click: function() { pdgui.menu_send(name); }
     });
diff --git a/pd/nw/pd_menus.js b/pd/nw/pd_menus.js
index 829a684a423bc6b402cfc5f0599af6611cd307a8..85d8fce67d0dcd6f955293a1d81d2662da0abdca 100644
--- a/pd/nw/pd_menus.js
+++ b/pd/nw/pd_menus.js
@@ -99,6 +99,12 @@ function create_menu(gui, type) {
             modifiers: cmd_or_ctrl + "+shift",
             tooltip: l("menu.saveas_tt")
         }));
+        file_menu.append(m.file.print = new gui.MenuItem({
+            label: l("menu.print"),
+            key: "p",
+            modifiers: cmd_or_ctrl + "+shift",
+            tooltip: l("menu.print_tt")
+        }));
     }
     if (pdgui.k12_mode == 0) {
         file_menu.append(new gui.MenuItem({ type: "separator" }));
diff --git a/pd/nw/pdgui.js b/pd/nw/pdgui.js
index 6e340c9efeb213f83e517a3749a00e29573b3234..a8c6ac30daf9e112baed221a11b4442a63e9296f 100644
--- a/pd/nw/pdgui.js
+++ b/pd/nw/pdgui.js
@@ -756,6 +756,59 @@ function menu_saveas(name) {
 
 exports.menu_saveas = menu_saveas;
 
+function gui_canvas_print(name, initfile, initdir) {
+    // AG: This works mostly like gui_canvas_saveas above, except that we
+    // create a pdf file and use a different input element and callback.
+    var input, chooser,
+        span = patchwin[name].window.document.querySelector("#printDialogSpan");
+    if (!fs.existsSync(initdir)) {
+        initdir = pwd;
+    }
+    // If we don't have a ".pd" file extension (e.g., "Untitled-1", add one)
+    if (initfile.slice(-3) !== ".pd") {
+        initfile += ".pd";
+    }
+    // Adding an "f" now gives .pdf which is what we want.
+    initfile += "f";
+    input = build_file_dialog_string({
+        style: "display: none;",
+        type: "file",
+        id: "printDialog",
+        nwsaveas: path.join(initdir, initfile),
+        nwworkingdir: initdir,
+        accept: ".pdf"
+    });
+    span.innerHTML = input;
+    chooser = patchwin[name].window.document.querySelector("#printDialog");
+    chooser.onchange = function() {
+        print_callback(name, this.value);
+        // reset value so that we can open the same file twice
+        this.value = null;
+        console.log("tried to print something");
+    }
+    chooser.click();
+}
+
+function print_callback(cid, file) {
+    var filename = defunkify_windows_path(file);
+    // It probably isn't possible to arrive at the callback with an
+    // empty string.  But I've only tested on Debian so far...
+    if (filename === null) {
+        return;
+    }
+    // Let nw.js do the rest (requires nw.js 0.14.6+)
+    patchwin[cid].print({ pdf_path: filename, headerFooterEnabled: false });
+    post("printed to: " + filename);
+}
+
+exports.print_callback = print_callback;
+
+function menu_print(name) {
+    pdsend(name + " menuprint");
+}
+
+exports.menu_print = menu_print;
+
 function menu_new () {
     // try not to use a global here
     untitled_directory = get_pd_opendir();
diff --git a/pd/src/g_readwrite.c b/pd/src/g_readwrite.c
index 024be4c76f83874e5316045024f8a76b5a4f6fdb..ca888857e0a204e9d226633a555e206f7fe1187d 100644
--- a/pd/src/g_readwrite.c
+++ b/pd/src/g_readwrite.c
@@ -899,6 +899,12 @@ static void canvas_menusave(t_canvas *x, t_floatarg fdestroy)
     else canvas_menusaveas(x2, fdestroy);
 }
 
+static void canvas_menuprint(t_canvas *x)
+{
+    t_canvas *x2 = canvas_getrootfor(x);
+    gui_vmess("gui_canvas_print", "xss", x, x->gl_name->s_name, canvas_getdir(x2)->s_name);
+}
+
 void g_readwrite_setup(void)
 {
     class_addmethod(canvas_class, (t_method)glist_write,
@@ -916,6 +922,8 @@ void g_readwrite_setup(void)
         gensym("menusave"), A_DEFFLOAT, 0);
     class_addmethod(canvas_class, (t_method)canvas_menusaveas,
         gensym("menusaveas"), A_DEFFLOAT, 0);
+    class_addmethod(canvas_class, (t_method)canvas_menuprint,
+        gensym("menuprint"), 0);
 }
 
 void canvas_readwrite_for_class(t_class *c)
@@ -924,4 +932,6 @@ void canvas_readwrite_for_class(t_class *c)
         gensym("menusave"), 0);
     class_addmethod(c, (t_method)canvas_menusaveas,
         gensym("menusaveas"), 0);
+    class_addmethod(c, (t_method)canvas_menuprint,
+        gensym("menuprint"), 0);
 }