diff --git a/pd/nw/dialog_prefs.html b/pd/nw/dialog_prefs.html
index 717f9ac07d98109c5aa18e0176135bc2b642612e..4f6184cca02c161a488121cc1d3306a3e9c00c43 100644
--- a/pd/nw/dialog_prefs.html
+++ b/pd/nw/dialog_prefs.html
@@ -292,6 +292,11 @@
               <option data-i18n="prefs.gui.presets.strongbad" value="strongbad">
               </option>
             </select>
+            <br/>
+            <label data-i18n="[title]prefs.gui.zoom.save_zoom_tt">
+              <input type="checkbox" id="save_zoom" name="save_zoom">
+              <span data-i18n="prefs.gui.zoom.save_zoom"></span>
+            </label>
           </div>
         </div>
 
@@ -386,6 +391,10 @@ function get_gui_preset() {
     return document.getElementById("gui_preset").selectedOptions[0].value;
 }
 
+function get_save_zoom() {
+    return +document.getElementById("save_zoom").checked;
+}
+
 // startup config data
 
 // XXXTODO: We should maybe include these in the dialog, but the startup tab
@@ -592,8 +601,9 @@ function apply(save_prefs) {
         midi_use_alsa ? get_attr("midi-outdev-names", attrs).length : 0
     );
 
-    // Send the name of the gui preset to Pd
-    pdgui.pdsend("pd gui-preset", get_gui_preset());
+    // Send the gui prefs (currently just the name of the gui preset and the
+    // status of the save-zoom toggle) to Pd
+    pdgui.pdsend("pd gui-prefs", get_gui_preset(), get_save_zoom());
 
     // Send the startup config data to Pd
     pdgui.pdsend.apply(null, ["pd path-dialog", startup_use_stdpath, startup_verbose].concat(get_path_array()));
@@ -832,7 +842,7 @@ function midi_prefs_callback(attrs) {
     pdgui.resize_window(pd_object_callback);
 }
 
-function gui_prefs_callback(name) {
+function gui_prefs_callback(name, save_zoom) {
     var s = document.getElementById("gui_preset"),
         i;
     for (i = 0; i < s.options.length; i++) {
@@ -841,6 +851,7 @@ function gui_prefs_callback(name) {
             break;
         }
     }
+    document.getElementById("save_zoom").checked = !!save_zoom;
 }
 
 // startup settings
diff --git a/pd/nw/locales/de/translation.json b/pd/nw/locales/de/translation.json
index 82448d0743d85e802ae1dbca1a5e654f1ded1355..d9d820c8a6aceae4690605513d4c7286ed6e0aeb 100644
--- a/pd/nw/locales/de/translation.json
+++ b/pd/nw/locales/de/translation.json
@@ -392,6 +392,10 @@
         "strongbad": "Strongbad",
         "extended": "Extended",
         "c64": "C64"
+      },
+      "zoom": {
+        "save_zoom": "Speichern/Laden der Vergrößerung im Patch",
+        "save_zoom_tt": "Speichere die aktuelle Vergrößerung mit dem Patch und stelle diese beim Laden des Patches wieder her"
       }
     },
     "audio": {
diff --git a/pd/nw/locales/en/translation.json b/pd/nw/locales/en/translation.json
index c4dbc6525cf71f29ff0daebacface002c9522550..14b758bc87b522bc63c27b264624f37cf77ea01c 100644
--- a/pd/nw/locales/en/translation.json
+++ b/pd/nw/locales/en/translation.json
@@ -392,6 +392,10 @@
         "strongbad": "Strongbad",
         "extended": "Extended",
         "c64": "C64"
+      },
+      "zoom": {
+        "save_zoom": "save/load zoom level with patch",
+        "save_zoom_tt": "Save the current zoom level with the patch and restore it when reloading the patch"
       }
     },
     "audio": {
diff --git a/pd/nw/pd_canvas.js b/pd/nw/pd_canvas.js
index d04d05f37a7134d949ca962ad61e9f41fa7013e0..77defb3647f0f34ab59d00a66a9b199a489b8399 100644
--- a/pd/nw/pd_canvas.js
+++ b/pd/nw/pd_canvas.js
@@ -788,6 +788,8 @@ function register_window_id(cid, attr_array) {
     // Trigger a "focus" event so that OSX updates the menu for this window
     nw_window_focus_callback(cid);
     canvas_events.normal();
+    // Initialize the zoom level to the value retrieved from the patch, if any.
+    nw.Window.get().zoomLevel = attr_array.zoom;
     pdgui.canvas_map(cid); // side-effect: triggers gui_canvas_get_scroll
     set_editmode_checkbox(attr_array.editmode !== 0 ? true : false);
     // For now, there is no way for the cord inspector to be turned on by
@@ -1219,6 +1221,7 @@ function nw_create_patch_window_menus(gui, w, name) {
             var z = gui.Window.get().zoomLevel;
             if (z < 8) { z++; }
             gui.Window.get().zoomLevel = z;
+            pdgui.pdsend(name, "zoom", z);
             pdgui.gui_canvas_get_scroll(name);
         }
     });
@@ -1228,6 +1231,7 @@ function nw_create_patch_window_menus(gui, w, name) {
             var z = gui.Window.get().zoomLevel;
             if (z > -7) { z--; }
             gui.Window.get().zoomLevel = z;
+            pdgui.pdsend(name, "zoom", z);
             pdgui.gui_canvas_get_scroll(name);
         }
     });
@@ -1256,6 +1260,7 @@ function nw_create_patch_window_menus(gui, w, name) {
         enabled: true,
         click: function () {
             gui.Window.get().zoomLevel = 0;
+            pdgui.pdsend(name, "zoom", 0);
             pdgui.gui_canvas_get_scroll(name);
         }
     });
diff --git a/pd/nw/pdgui.js b/pd/nw/pdgui.js
index 23087bb349356137231f749b2ce13a0ca7a0989a..82c3ca2cebcd7c01acadc541e057316708281e44 100644
--- a/pd/nw/pdgui.js
+++ b/pd/nw/pdgui.js
@@ -1303,7 +1303,7 @@ function create_window(cid, type, width, height, xpos, ypos, attr_array) {
 }
 
 // create a new canvas
-function gui_canvas_new(cid, width, height, geometry, editmode, name, dir, dirty_flag, cargs) {
+function gui_canvas_new(cid, width, height, geometry, zoom, editmode, name, dir, dirty_flag, cargs) {
     // hack for buggy tcl popups... should go away for node-webkit
     //reset_ctrl_on_popup_window
 
@@ -1353,6 +1353,7 @@ function gui_canvas_new(cid, width, height, geometry, editmode, name, dir, dirty
             dir: dir,
             dirty: dirty_flag,
             args: cargs,
+            zoom: zoom,
             editmode: editmode
     });
 }
@@ -4567,9 +4568,9 @@ function gui_midi_properties(gfxstub, sys_indevs, sys_outdevs,
     }
 }
 
-function gui_gui_properties(dummy, name) {
+function gui_gui_properties(dummy, name, save_zoom) {
     if (dialogwin["prefs"] !== null) {
-        dialogwin["prefs"].window.gui_prefs_callback(name);
+        dialogwin["prefs"].window.gui_prefs_callback(name, save_zoom);
     }
 }
 
@@ -4920,6 +4921,7 @@ function do_optimalzoom(cid, hflag, vflag) {
     //post("bbox: "+width+"x"+height+"+"+x+"+"+y+" window size: "+min_width+"x"+min_height+" current zoom level: "+actz+" optimal zoom level: "+z);
     if (z != actz) {
         patchwin[cid].zoomLevel = z;
+        pdsend(cid, "zoom", z);
     }
 }
 
diff --git a/pd/src/g_canvas.c b/pd/src/g_canvas.c
index 83f872af0005f6e72c2fb1c894a85710f51dc84c..aec7efd3ed82efb42e11ad296b53ed457bbd92a1 100644
--- a/pd/src/g_canvas.c
+++ b/pd/src/g_canvas.c
@@ -14,6 +14,7 @@ to be different but are now unified except for some fossilized names.) */
 #include "g_canvas.h"
 #include "g_all_guis.h"
 #include <string.h>
+#include <math.h>
 
 extern int do_not_redraw;
 extern void canvas_drawconnection(t_canvas *x, int lx1, int ly1, int lx2, int ly2, t_int tag, int issignal);
@@ -350,6 +351,22 @@ t_symbol *canvas_field_templatesym; /* for "canvas" data type */
 t_word *canvas_field_vec;           /* for "canvas" data type */
 t_gpointer *canvas_field_gp;        /* parent for "canvas" data type */
 
+static int calculate_zoom(t_float zoom_hack)
+{
+  // This gives back the zoom level stored in the patch (cf. zoom_hack
+  // in g_readwrite.c). Make sure to round this value to handle any rounding
+  // errors due to the limited float precision in Pd's binbuf writer.
+  int zoom = round(zoom_hack*32);
+  // This is a 5 bit number in 2's complement, so we need to extend the sign.
+  zoom = zoom << 27 >> 27;
+  // To be on the safe side, we finally clamp the result to the range -7..8
+  // which are the zoom levels supported by Purr Data right now.
+  if (zoom < -7) zoom = -7;
+  if (zoom > 8) zoom = 8;
+  //post("read zoom level: %d", zoom);
+  return zoom;
+}
+
     /* make a new glist.  It will either be a "root" canvas or else
     it appears as a "text" object in another window (canvas_getcurrent() 
     tells us which.) */
@@ -370,6 +387,8 @@ t_canvas *canvas_new(void *dummy, t_symbol *sel, int argc, t_atom *argv)
     int vis = 0, width = GLIST_DEFCANVASWIDTH, height = GLIST_DEFCANVASHEIGHT;
     int xloc = 0, yloc = GLIST_DEFCANVASYLOC;
     int font = (owner ? owner->gl_font : sys_defaultfont);
+    int zoom = 0;
+    extern int sys_zoom;
 
     glist_init(x);
     //x->gl_magic_glass = magicGlass_new(x);
@@ -380,20 +399,28 @@ t_canvas *canvas_new(void *dummy, t_symbol *sel, int argc, t_atom *argv)
 
     if (argc == 5)  /* toplevel: x, y, w, h, font */
     {
+        t_float zoom_hack = atom_getfloatarg(3, argc, argv);
         xloc = atom_getintarg(0, argc, argv);
         yloc = atom_getintarg(1, argc, argv);
         width = atom_getintarg(2, argc, argv);
         height = atom_getintarg(3, argc, argv);
         font = atom_getintarg(4, argc, argv);
+        zoom_hack -= height;
+        if (sys_zoom && zoom_hack > 0)
+            zoom = calculate_zoom(zoom_hack);
     }
     else if (argc == 6)  /* subwindow: x, y, w, h, name, vis */
     {
+        t_float zoom_hack = atom_getfloatarg(3, argc, argv);
         xloc = atom_getintarg(0, argc, argv);
         yloc = atom_getintarg(1, argc, argv);
         width = atom_getintarg(2, argc, argv);
         height = atom_getintarg(3, argc, argv);
         s = atom_getsymbolarg(4, argc, argv);
         vis = atom_getintarg(5, argc, argv);
+        zoom_hack -= height;
+        if (sys_zoom && zoom_hack > 0)
+            zoom = calculate_zoom(zoom_hack);
     }
         /* (otherwise assume we're being created from the menu.) */
 
@@ -448,6 +475,7 @@ t_canvas *canvas_new(void *dummy, t_symbol *sel, int argc, t_atom *argv)
     x->gl_willvis = vis;
     x->gl_edit = !strncmp(x->gl_name->s_name, "Untitled", 8);
     x->gl_font = sys_nearestfontsize(font);
+    x->gl_zoom = zoom;
     pd_pushsym(&x->gl_pd);
 
     //dpsaha@vt.edu gop resize (refactored by mathieu)
@@ -548,6 +576,7 @@ t_glist *glist_addglist(t_glist *g, t_symbol *sym,
     x->gl_pixheight = py2 - py1;
     x->gl_font =  (canvas_getcurrent() ?
         canvas_getcurrent()->gl_font : sys_defaultfont);
+    x->gl_zoom = 0;
     x->gl_screenx1 = x->gl_screeny1 = 0;
     x->gl_screenx2 = 450;
     x->gl_screeny2 = 300;
diff --git a/pd/src/g_canvas.h b/pd/src/g_canvas.h
index 1c29534bf0e110d3f76249d13a83aa462b61dad9..768aeb18a7085f3e47357ce8aca5ce07813c0f52 100644
--- a/pd/src/g_canvas.h
+++ b/pd/src/g_canvas.h
@@ -200,6 +200,7 @@ struct _glist
     t_editor *gl_editor;        /* editor structure when visible */
     t_symbol *gl_name;          /* symbol bound here */
     int gl_font;                /* nominal font size in points, e.g., 10 */
+    int gl_zoom;                /* current zoom level (-7..8) */
     struct _glist *gl_next;         /* link in list of toplevels */
     t_canvasenvironment *gl_env;    /* root canvases and abstractions only */
     unsigned int gl_havewindow:1;   /* true if we own a window */
diff --git a/pd/src/g_editor.c b/pd/src/g_editor.c
index 598f245ad519f1510877f07726f540b8e1e643ba..1dd4e7a98aff32f9383cb4a4b3b4646ad4f2024b 100644
--- a/pd/src/g_editor.c
+++ b/pd/src/g_editor.c
@@ -2471,11 +2471,12 @@ void canvas_vis(t_canvas *x, t_floatarg f)
             //fprintf(stderr,"new\n");
             canvas_create_editor(x);
             canvas_args_to_string(argsbuf, x);
-            gui_vmess("gui_canvas_new", "xiisissis",
+            gui_vmess("gui_canvas_new", "xiisiissis",
                 x,
                 (int)(x->gl_screenx2 - x->gl_screenx1),
                 (int)(x->gl_screeny2 - x->gl_screeny1),
                 geobuf,
+                x->gl_zoom,
                 x->gl_edit,
                 x->gl_name->s_name,
                 canvas_getdir(x)->s_name,
@@ -7394,6 +7395,12 @@ static void canvas_font(t_canvas *x, t_floatarg font, t_floatarg oldfont,
     canvas_dofont(x2, font, realresize, realresize);
 }
 
+static void canvas_zoom(t_canvas *x, t_floatarg zoom)
+{
+    x->gl_zoom = zoom;
+    //post("store zoom level: %d", x->gl_zoom);
+}
+
 void glist_getnextxy(t_glist *gl, int *xpix, int *ypix)
 {
     if (canvas_last_glist == gl)
@@ -7592,6 +7599,8 @@ void g_editor_setup(void)
         gensym("menufont"), A_NULL);
     class_addmethod(canvas_class, (t_method)canvas_font,
         gensym("font"), A_FLOAT, A_FLOAT, A_FLOAT, A_FLOAT, A_NULL);
+    class_addmethod(canvas_class, (t_method)canvas_zoom,
+        gensym("zoom"), A_FLOAT, A_NULL);
     class_addmethod(canvas_class, (t_method)canvas_find,
         gensym("find"), A_SYMBOL, A_FLOAT, A_NULL);
     class_addmethod(canvas_class, (t_method)canvas_find_again,
diff --git a/pd/src/g_readwrite.c b/pd/src/g_readwrite.c
index a3b4ba824887da90c4e7447b8c9551e3a810f4dc..024be4c76f83874e5316045024f8a76b5a4f6fdb 100644
--- a/pd/src/g_readwrite.c
+++ b/pd/src/g_readwrite.c
@@ -646,6 +646,28 @@ static void glist_write(t_glist *x, t_symbol *filename, t_symbol *format)
 
 /* ------ routines to save and restore canvases (patches) recursively. ----*/
 
+static double zoom_hack(int x, int zoom)
+{
+  // AG: This employs an interesting little hack made possible by a loophole
+  // in Pd's patch parser (binbuf_eval), which will happily read a float value
+  // where an int is expected, and cast it to an int anyway. Applied to the #N
+  // header of a canvas, this lets us store the zoom level of a patch in the
+  // fractional part of a window geometry argument x, so that we can restore
+  // it when the patch is reloaded. Since vanilla and other Pd flavors won't
+  // even notice the extra data, the same patch can still be loaded by any
+  // other Pd version without any problems.
+
+  // Encoding of the zoom level: Purr Data's zoom levels go from -7 to 8 so
+  // that we can encode them as 4 bit numbers in two's complement. However, it
+  // makes sense to leave 1 extra bit for future extensions, so our encoding
+  // actually uses nonnegative 5 bit integer multiples of 2^-5 = 0.03125 in a
+  // way that leaves the integer part of the height parameter intact. Note
+  // that using 2's complement here has the advantage that a zero zoom level
+  // maps to a zero fraction so that we don't need an extra bit to detect
+  // whether there's a zoom level when reading the patch.
+  return x + (zoom & 31) * 0.03125;
+}
+
     /* save to a binbuf, called recursively; cf. canvas_savetofile() which
     saves the document, and is only called on root canvases. */
 static void canvas_saveto(t_canvas *x, t_binbuf *b)
@@ -653,6 +675,7 @@ static void canvas_saveto(t_canvas *x, t_binbuf *b)
     t_gobj *y;
     t_linetraverser t;
     t_outconnect *oc;
+    extern int sys_zoom;
         /* subpatch */
     if (x->gl_owner && !x->gl_env)
     {
@@ -671,25 +694,53 @@ static void canvas_saveto(t_canvas *x, t_binbuf *b)
         patchsym = atom_getsymbolarg(name_index,
             binbuf_getnatom(bz), binbuf_getvec(bz));
         binbuf_free(bz);
-        binbuf_addv(b, "ssiiiisi;", gensym("#N"), gensym("canvas"),
-            (int)(x->gl_screenx1),
-            (int)(x->gl_screeny1),
-            (int)(x->gl_screenx2 - x->gl_screenx1),
-            (int)(x->gl_screeny2 - x->gl_screeny1),
-            (patchsym != &s_ ? patchsym: gensym("(subpatch)")),
-            x->gl_mapped);
+        if (sys_zoom && x->gl_zoom != 0) {
+          // This uses the hack described above to store the zoom factor in
+          // the fractional part of the windows height parameter. Note that
+          // any of the other canvas geometry parameters would do just as
+          // well, as they are all measured in pixels and thus unlikely to
+          // become fractional in the future.
+          binbuf_addv(b, "ssiiifsi;", gensym("#N"), gensym("canvas"),
+                      (int)(x->gl_screenx1),
+                      (int)(x->gl_screeny1),
+                      (int)(x->gl_screenx2 - x->gl_screenx1),
+                      zoom_hack(x->gl_screeny2 - x->gl_screeny1, x->gl_zoom),
+                      (patchsym != &s_ ? patchsym: gensym("(subpatch)")),
+                      x->gl_mapped);
+        } else {
+          // This is the standard (vanilla) code. This is also used in the
+          // case of a zero zoom level, so unzoomed patches will always
+          // produce a standard vanilla patch file when saved.
+          binbuf_addv(b, "ssiiiisi;", gensym("#N"), gensym("canvas"),
+                      (int)(x->gl_screenx1),
+                      (int)(x->gl_screeny1),
+                      (int)(x->gl_screenx2 - x->gl_screenx1),
+                      (int)(x->gl_screeny2 - x->gl_screeny1),
+                      (patchsym != &s_ ? patchsym: gensym("(subpatch)")),
+                      x->gl_mapped);
+        }
         /* Not sure what the following commented line was doing... */
         //t_text *xt = (t_text *)x;
     }
         /* root or abstraction */
     else 
     {
-        binbuf_addv(b, "ssiiiii;", gensym("#N"), gensym("canvas"),
-            (int)(x->gl_screenx1),
-            (int)(x->gl_screeny1),
-            (int)(x->gl_screenx2 - x->gl_screenx1),
-            (int)(x->gl_screeny2 - x->gl_screeny1),
-                (int)x->gl_font);
+        // See above.
+        if (sys_zoom && x->gl_zoom != 0) {
+          binbuf_addv(b, "ssiiifi;", gensym("#N"), gensym("canvas"),
+                      (int)(x->gl_screenx1),
+                      (int)(x->gl_screeny1),
+                      (int)(x->gl_screenx2 - x->gl_screenx1),
+                      zoom_hack(x->gl_screeny2 - x->gl_screeny1, x->gl_zoom),
+                      (int)x->gl_font);
+        } else {
+          binbuf_addv(b, "ssiiiii;", gensym("#N"), gensym("canvas"),
+                      (int)(x->gl_screenx1),
+                      (int)(x->gl_screeny1),
+                      (int)(x->gl_screenx2 - x->gl_screenx1),
+                      (int)(x->gl_screeny2 - x->gl_screeny1),
+                      (int)x->gl_font);
+        }
         canvas_savedeclarationsto(x, b);
     }
     for (y = x->gl_list; y; y = y->g_next)
diff --git a/pd/src/m_glob.c b/pd/src/m_glob.c
index 3f5e448fb0d2623899b7e2028e5a5b96675e48bc..1f2b49bbff9a19718e87201fc7ea5aae765cf760 100644
--- a/pd/src/m_glob.c
+++ b/pd/src/m_glob.c
@@ -75,16 +75,18 @@ static void glob_perf(t_pd *dummy, float f)
     sys_perf = (f != 0);
 }
 
+extern int sys_zoom;
 extern t_symbol *sys_gui_preset;
-static void glob_gui_preset(t_pd *dummy, t_symbol *s)
+static void glob_gui_prefs(t_pd *dummy, t_symbol *s, float f)
 {
     sys_gui_preset = s;
+    sys_zoom = !!(int)f;
 }
 
-/* just the gui-preset for now */
+/* just the gui-preset and the save-zoom toggle for now */
 static void glob_gui_properties(t_pd *dummy)
 {
-    gui_vmess("gui_gui_properties", "xs", 0, sys_gui_preset->s_name);
+    gui_vmess("gui_gui_properties", "xsi", 0, sys_gui_preset->s_name, sys_zoom);
 }
 
 // ths one lives inside g_editor so that it can access the clipboard
@@ -167,8 +169,8 @@ void glob_init(void)
         gensym("perf"), A_FLOAT, 0);
     class_addmethod(glob_pdobject, (t_method)glob_clipboard_text,
         gensym("clipboardtext"), A_FLOAT, 0);
-    class_addmethod(glob_pdobject, (t_method)glob_gui_preset,
-        gensym("gui-preset"), A_SYMBOL, 0);
+    class_addmethod(glob_pdobject, (t_method)glob_gui_prefs,
+        gensym("gui-prefs"), A_SYMBOL, A_FLOAT, 0);
     class_addmethod(glob_pdobject, (t_method)glob_gui_properties,
         gensym("gui-properties"), 0);
     class_addmethod(glob_pdobject, (t_method)glob_recent_files,
diff --git a/pd/src/s_file.c b/pd/src/s_file.c
index 3c4873760bca30ef2ebd81cc62e690efcbcc55e4..44a168d149170cf4d6fd36825272f92cfe3a475f 100644
--- a/pd/src/s_file.c
+++ b/pd/src/s_file.c
@@ -40,7 +40,7 @@
 #define snprintf sprintf_s
 #endif
 
-int sys_defeatrt;
+int sys_defeatrt, sys_zoom;
 t_symbol *sys_flags = &s_;
 void sys_doflags( void);
 
@@ -618,6 +618,8 @@ void sys_loadpreferences( void)
     }
     if (sys_getpreference("defeatrt", prefbuf, MAXPDSTRING))
         sscanf(prefbuf, "%d", &sys_defeatrt);
+    if (sys_getpreference("savezoom", prefbuf, MAXPDSTRING))
+        sscanf(prefbuf, "%d", &sys_zoom);
     if (sys_getpreference("guipreset", prefbuf, MAXPDSTRING))
     {
         char preset_buf[MAXPDSTRING];
@@ -755,6 +757,8 @@ void glob_savepreferences(t_pd *dummy)
     sys_putpreference("nloadlib", buf1);
     sprintf(buf1, "%d", sys_defeatrt);
     sys_putpreference("defeatrt", buf1);
+    sprintf(buf1, "%d", sys_zoom);
+    sys_putpreference("savezoom", buf1);
     sys_putpreference("guipreset", sys_gui_preset->s_name);
     sys_putpreference("flags", 
         (sys_flags ? sys_flags->s_name : ""));