#
# This file is licensed under the MIT No Attribution license.
#

Render groff 1.24.1 HTML image regions as selectable SVG objects.

Apply to the unmodified 1.24.1 source with patch --fuzz=0 -p1.
Requires Ghostscript, MuPDF 1.27.2's mutool, Perl, dvisvgm 3.6 with WOFF2,
and svg-text.py installed as groff-svg-text in PATH.  This changes only HTML
output; ordinary PDF and PostScript output are unaffected.  See README.adoc
beside this patch.

--- a/src/preproc/html/pre-html.cpp
+++ b/src/preproc/html/pre-html.cpp
@@ -156,14 +156,11 @@
 #include "pushback.h"
 #include "html-strings.h"
 
-#define DEFAULT_LINE_LENGTH 7	// inches wide
+#define IMAGE_PAGE_LENGTH 1000	// inches; keep HTML image regions together
 #define DEFAULT_IMAGE_RES 100	// number of pixels per inch resolution
-#define IMAGE_BORDER_PIXELS 0
+#define IMAGE_BORDER_PIXELS 2
 #define INLINE_LEADER_CHAR '\\'
 
-// Don't use colour names here!  Otherwise there is a dependency on
-// a file called 'rgb.txt' which maps names to colours.
-#define TRANSPARENT "-background rgb:f/f/f -transparent rgb:f/f/f"
 #define MIN_ALPHA_BITS 0
 #define MAX_ALPHA_BITS 4
 
@@ -187,21 +184,15 @@
 					// -1 means closed
 static const char *psFileName = 0 /* nullptr */;	// PostScript
 							// file name
-static const char *psPageName = 0 /* nullptr */;	// name of file
-							// containing
-							// current
-							// PostScript
-							// page
 static const char *regionFileName = 0 /* nullptr */;	// name of file
 							// containing
 							// all image
 							// regions
 static const char *imagePageName = 0 /* nullptr */;	// name of
-							// bitmap image
+							// PDF region
 							// file
 							// containing
-							// current page
-static const char *image_device = "pnmraw";
+							// current region
 static int image_res = DEFAULT_IMAGE_RES;
 static int vertical_offset = 0;
 static char *image_template = 0 /* nullptr */;	// image file name
@@ -215,11 +206,8 @@
 						// directory
 static int textAlphaBits = MAX_ALPHA_BITS;
 static int graphicAlphaBits = MAX_ALPHA_BITS;
-static const char *antiAlias = 0 /* nullptr */;	// anti-alias arguments
-						// to be passed to gs
 static bool want_progress_report = false;	// display page numbers
 						// as they are processed
-static int currentPageNo = -1;		// current image page number
 static bool debugging = false;
 static const char *troffFileName = 0 /* nullptr */;	// pre-html
 							// output sent
@@ -438,6 +426,8 @@
 		program_name, s);
     }
     close(saved_stdout);
+    if (status != 0)
+      fatal("image conversion failed");
   }
 }
 
@@ -613,25 +603,6 @@
 }
 
 /*
- *  setupAntiAlias - Set up the antialias string, used when we call gs.
- */
-
-static void setupAntiAlias(void)
-{
-  if (textAlphaBits == 0 && graphicAlphaBits == 0)
-    antiAlias = make_string(" ");
-  else if (textAlphaBits == 0)
-    antiAlias = make_string("-dGraphicsAlphaBits=%d ",
-			    graphicAlphaBits);
-  else if (graphicAlphaBits == 0)
-    antiAlias = make_string("-dTextAlphaBits=%d ", textAlphaBits);
-  else
-    antiAlias = make_string("-dTextAlphaBits=%d"
-			    " -dGraphicsAlphaBits=%d ", textAlphaBits,
-			    graphicAlphaBits);
-}
-
-/*
  *  checkImageDir - Check whether the image directory is available.
  */
 
@@ -692,7 +663,7 @@
     break;
   }
   writeString(image_template);
-  writeString(".png]");
+  writeString(".svg]");
   if (is_html)
     writeString("\\O[0]\\O[3]");
   else
@@ -825,6 +796,15 @@
   //     with no translation, for image file creation in the
   //     post-processor.
 
+  // The PostScript pass is only an intermediate layout for HTML images.
+  // Its page length must match grops below; paper boundaries must not
+  // split a table or equation into incomplete image regions.
+  if (device_format_selector == DEVICE_FORMAT(IMAGE_OUTPUT_FILTER)) {
+    const char *s = make_string(".pl %di\n", IMAGE_PAGE_LENGTH);
+    writeString(s);
+    free(const_cast<char *>(s));
+  }
+
   int idx = 0;
   char_block *element = head;
 
@@ -907,9 +887,7 @@
   void add(int x1, int y1, int x2, int y2,
 	   int page, int res, int maxx, char *name);
   void createImages(void);
-  int createPage(int pageno);
   void createImage(imageItem *i);
-  int getMaxX(int pageno);
 };
 
 /*
@@ -935,58 +913,6 @@
 }
 
 /*
- *  createPage - Create image of page `pageno` from PostScript file.
- */
-
-int imageList::createPage(int pageno)
-{
-  if (currentPageNo == pageno)
-    return 0;
-
-  if (currentPageNo >= 1) {
-    /*
-     *  We need to unlink the files which change each time a new page is
-     *  processed.  The final unlink is done by xtmpfile when
-     *  pre-grohtml exits.
-     */
-    unlink(imagePageName);
-    unlink(psPageName);
-  }
-
-  if (want_progress_report) {
-    fprintf(stderr, "[%d] ", pageno);
-    fflush(stderr);
-  }
-
-  if (debugging)
-    fprintf(stderr, "%s: debug: creating page %d\n", program_name,
-	    pageno);
-
-  const char *s = make_string("ps2ps -sPageList=%d %s %s",
-			      pageno, psFileName, psPageName);
-  html_system(s, 1);
-  assert(strlen(image_gen) > 0);
-  s = make_string("echo showpage | "
-		  "%s%s -q -dBATCH -dSAFER "
-		  "-dDEVICEHEIGHTPOINTS=792 "
-		  "-dDEVICEWIDTHPOINTS=%d -dFIXEDMEDIA=true "
-		  "-sDEVICE=%s -r%d %s "
-		  "-sOutputFile=%s %s -",
-		  image_gen,
-		  EXE_EXT,
-		  (getMaxX(pageno) * image_res) / postscriptRes,
-		  image_device,
-		  image_res,
-		  antiAlias,
-		  imagePageName,
-		  psPageName);
-  html_system(s, 1);
-  free(const_cast<char *>(s));
-  currentPageNo = pageno;
-  return 0;
-}
-
-/*
  *  min - Return the minimum of two numbers.
  */
 
@@ -1011,63 +937,67 @@
 }
 
 /*
- *  getMaxX - Return the largest right-hand position for any image
- *            on `pageno`.
- */
-
-int imageList::getMaxX(int pageno)
-{
-  imageItem *h = head;
-  int x = postscriptRes * DEFAULT_LINE_LENGTH;
-
-  while (h != 0 /* nullptr */) {
-    if (h->pageNo == pageno)
-      x = max(h->X2, x);
-    h = h->next;
-  }
-  return x;
-}
-
-/*
- *  createImage - Generate a minimal PNG file from the set of page
- *                images.
+ *  createImage - Extract one vector region, preserving fonts and lines.
+ *
+ *  Render directly from the original PostScript: ps2ps clips to its
+ *  paper size before the old raster crop can reach wide image regions.
+ *  A small PDF canvas and translated origin retain only this region;
+ *  no bitmap of the long intermediate page is allocated.  dvisvgm embeds
+ *  fonts in SVG, keeping the text selectable without external font files.
  */
 
 void imageList::createImage(imageItem *i)
 {
   if (i->X1 != -1) {
-    int x1 = max(min(i->X1, i->X2) * image_res / postscriptRes
+    int x1 = max(int(min(i->X1, i->X2) * double(image_res) / postscriptRes)
 		   - IMAGE_BORDER_PIXELS,
 		 0);
     int y1 = max(image_res * vertical_offset / 72
-		   + min(i->Y1, i->Y2) * image_res / postscriptRes
+		   + int(min(i->Y1, i->Y2) * double(image_res) / postscriptRes)
 		   - IMAGE_BORDER_PIXELS,
 		 0);
-    int x2 = max(i->X1, i->X2) * image_res / postscriptRes
+    int x2 = int(max(i->X1, i->X2) * double(image_res) / postscriptRes)
 	     + IMAGE_BORDER_PIXELS;
     int y2 = image_res * vertical_offset / 72
-	     + max(i->Y1, i->Y2) * image_res / postscriptRes
+	     + int(max(i->Y1, i->Y2) * double(image_res) / postscriptRes)
 	     + 1 + IMAGE_BORDER_PIXELS;
-    if (createPage(i->pageNo) == 0) {
-      const char *s = make_string("pamcut%s %d %d %d %d < %s "
-				  "| pnmcrop%s " PNMTOOLS_QUIET
-				  "| pnmtopng%s " PNMTOOLS_QUIET " %s"
-				  "> %s",
-				  EXE_EXT,
-				  x1, y1, x2 - x1 + 1, y2 - y1 + 1,
-				  imagePageName,
-				  EXE_EXT,
-				  EXE_EXT,
-				  TRANSPARENT,
-				  i->imageName);
-      html_system(s, 0);
-      free(const_cast<char *>(s));
-    }
-    else {
-      fprintf(stderr, "%s: failed to generate image of page %d\n",
-	      program_name, i->pageNo);
-      fflush(stderr);
-    }
+    // Match the former PNG display size while retaining vector paths.
+    // -r72 avoids PDF interpreter coordinate limits on the virtual page.
+    assert(strlen(image_gen) > 0);
+    const char *s = make_string(
+      "%s%s -q -dBATCH -dNOPAUSE -dSAFER "
+      "-dFirstPage=%d -dLastPage=%d -dFIXEDMEDIA=true -r72 "
+      "-dDEVICEWIDTHPOINTS=%d -dDEVICEHEIGHTPOINTS=%d "
+      "-sDEVICE=pdfwrite -sOutputFile=%s "
+      "-c '<</PageOffset [%d %.6f] "
+      "/BeginPage {pop %.9f dup scale}>> setpagedevice' -f %s",
+      image_gen, EXE_EXT, i->pageNo, i->pageNo,
+      x2 - x1 + 1, y2 - y1 + 1, imagePageName, -x1,
+      (y2 + 1) - IMAGE_PAGE_LENGTH * double(image_res),
+      image_res / 72.0, psFileName);
+    html_system(s, 1);
+    free(const_cast<char *>(s));
+    // Convert PDF points to CSS pixels without changing the viewBox.
+    // Reading stdin also accepts the extensionless temporary PDF name.
+    s = make_string("DVISVGM_PDF_PROC=mutool dvisvgm --pdf --stdin "
+                    "--font-format=woff2 --zoom=0.75 --verbosity=3 "
+                    "--output=%s < %s", i->imageName, imagePageName);
+    html_system(s, 1);
+    free(const_cast<char *>(s));
+    // Keep the order of embedded font declarations reproducible.
+    // Preserve spaces and fractional character widths in the browser.
+    s = make_string("perl -0777 -i -pe "
+                    "'s~(?:\\@font-face\\{[^}]*\\}\\n?)+~"
+                    "join(\"\\n\", sort split /\\n/, $&) . \"\\n\"~ge;"
+                    "s~(<style[^>]*>\\n)~$1text { white-space: pre; "
+                    "text-rendering: geometricPrecision; }\\n~' %s",
+                    i->imageName);
+    html_system(s, 1);
+    free(const_cast<char *>(s));
+    s = make_string("groff-svg-text %s %s",
+                    imagePageName, i->imageName);
+    html_system(s, 1);
+    free(const_cast<char *>(s));
 #if defined(DEBUGGING)
   }
   else {
@@ -1536,9 +1466,10 @@
   argv = addRegDef(argc, argv, s.contents());
   argc++;
 
-  // Override local settings and produce a letter-size PostScript page
-  // file.
-  argv = addRegDef(argc, argv, "-P-pletter");
+  // Match the long virtual page used by the image-layout pass.
+  const char *paper = make_string("-P-p%di,8.5i", IMAGE_PAGE_LENGTH);
+  argv = addRegDef(argc, argv, paper);
+  free(const_cast<char *>(paper));
   argc++;
 
   if (dialect == xhtml) {
@@ -1752,18 +1683,11 @@
   psFileName = DEBUG_FILE("prehtml-ps");
   regionFileName = DEBUG_FILE("prehtml-region");
   imagePageName = DEBUG_FILE("prehtml-page");
-  psPageName = DEBUG_FILE("prehtml-psn");
   troffFileName = DEBUG_FILE("prehtml-troff");
   htmlFileName = DEBUG_FILE("prehtml-html");
   FILE *f;
 
-  // psPageName contains a single page of PostScript.
-  f = xtmpfile(&psPageName, PS_TEMPLATE_LONG, PS_TEMPLATE_SHORT);
-  if (0 /* nullptr */ == f)
-    sys_fatal("xtmpfile");
-  fclose(f);
-
-  // imagePageName contains a bitmap image of a single PostScript page.
+  // imagePageName contains a single cropped PDF image region.
   f = xtmpfile(&imagePageName, PAGE_TEMPLATE_LONG, PAGE_TEMPLATE_SHORT);
   if (0 /* nullptr */ == f)
     sys_fatal("xtmpfile");
@@ -1839,7 +1763,6 @@
   if (postscriptRes < 1) // TODO: what's a more sane minimum value?
     fatal("'res' directive missing or invalid in file '%1'",
 	  devps_desc);
-  setupAntiAlias();
   checkImageDir();
   makeFileName();
   bool have_file_operand = false;
--- a/src/devices/grohtml/post-html.cpp
+++ b/src/devices/grohtml/post-html.cpp
@@ -2606,10 +2606,36 @@
   while ((filename != 0 /* nullptr */) && (' ' == filename[0]))
     filename++;
   if (exists(filename)) {
-    *s += string("<img src=\"") + filename + "\" "
-	  + "alt=\"Image " + filename + "\">";
-    if (xhtml == dialect)
-      *s += "</img>";
+    const char *suffix = strrchr(filename, '.');
+    if (suffix && strcmp(suffix, ".svg") == 0) {
+      string size;
+      // Reserve the space before the object loads to keep layout stable.
+      // The generated SVG viewBox is measured in CSS pixels.
+      FILE *image = fopen(filename, "r");
+      if (image) {
+        char line[1024];
+        while (fgets(line, sizeof line, image)) {
+          const char *box = strstr(line, "viewBox=");
+          if (box) {
+            int width, height;
+            if (sscanf(box + 9, "%*d %*d %d %d", &width, &height) == 2
+                && width > 0 && height > 0)
+              size = string(" width=\"") + as_string(width)
+                     + "\" height=\"" + as_string(height) + "\"";
+            break;
+          }
+        }
+        fclose(image);
+      }
+      *s += string("<object type=\"image/svg+xml\" data=\"") + filename
+            + "\"" + size + ">Image " + filename + "</object>";
+    }
+    else {
+      *s += string("<img src=\"") + filename + "\" "
+            + "alt=\"Image " + filename + "\">";
+      if (xhtml == dialect)
+        *s += "</img>";
+    }
   }
   return *s;
 }
--- a/src/roff/troff/mtsm.cpp
+++ b/src/roff/troff/mtsm.cpp
@@ -397,3 +397,5 @@ void mtsm::inherit(statem *s, int reset_bool)
   if (!stack.empty()) {
-    statem top = stack.top();
+    // Consume inherited breaks in the saved state, so they are not
+    // repeated for every glyph in a diversion.
+    statem &top = stack.top();
     if (s->units_values[MTSM_IN].is_known
--- a/tmac/www.tmac.in
+++ b/tmac/www.tmac.in
@@ -1266,7 +1266,7 @@
 .  ie '\*[.T]'html' \{\
 .    www-make-unique-name
 .    nr www-drop-width (100u * \\n[.v]u * 3u / \\n[.l]u)
-.    MPIMG -L \\*[www-unique-name].png \\n[www-drop-width]%
+.    MPIMG -L \\*[www-unique-name].svg \\n[www-drop-width]%
 .  \}
 .  el \{\
 .    ie r ps4html \{\
@@ -1278,7 +1278,7 @@
 .      bp
 .      ev www-DC
 .      vs 320p
-.      nop \O[5i\\*[www-unique-name].png]\O[1]
+.      nop \O[5i\\*[www-unique-name].svg]\O[1]
 .      nop \m[\\*[www-dropcolor]]\s[160]\O[3]\\$1\O[4]
 .      nop \O[2]\O[0]
 .      br
@@ -1304,7 +1304,7 @@
 .\" supplementary macros used by other macro sets
 .\"
 .\" here are some tags specially for -Tps or -Thtml when invoked by
-.\" pre-html to generate PNG images from postscript.
+.\" pre-html to generate SVG images from PostScript.
 .
 .\" --------------------------------------------------------------------
 .\" HTML-DO-IMAGE - tell troff to issue an image marker which can be
@@ -1312,9 +1312,9 @@
 .\"
 .de HTML-DO-IMAGE
 .  if r ps4html \
-.    nop \O[5\\$2\\$1.png]\O[1]\O[3]
+.    nop \O[5\\$2\\$1.svg]\O[1]\O[3]
 .  if \\n[www-html] \
-.    nop \O[5\\$2\\$1.png]\O[0]\O[3]
+.    nop \O[5\\$2\\$1.svg]\O[0]\O[3]
 ..
 .
 .\" --------------------------------------------------------------------
@@ -1396,9 +1396,9 @@
 .  ie r xhtml \{\
 .    if !(\\n[.O] == 0) \{\
 .      if r ps4html \
-.        nop \O[5\\$2\\$1.png]\O[1]\O[3]
+.        nop \O[5\\$2\\$1.svg]\O[1]\O[3]
 .      if \\n[www-html] \
-.        nop \O[5\\$2\\$1.png]\O[0]\O[3]
+.        nop \O[5\\$2\\$1.svg]\O[0]\O[3]
 .    \}
 .  \}
 .  el \
--- a/tmac/an.tmac
+++ b/tmac/an.tmac
@@ -1399,7 +1399,6 @@
 .  el         .nr 3usekeeps 1
 .  if \\n[an*is-output-html] \{\
 .    nr an-TS-ll \\n[.l]
-.    ll 1000n
 .  \}
 .  HTML-IMAGE
 ..
@@ -1428,7 +1427,6 @@
 .de1 EQ
 .  if \\n[an*is-output-html] \{\
 .    nr an-EQ-ll \\n[.l]
-.    ll 1000n
 .  \}
 .  HTML-IMAGE
 ..
--- a/tmac/s.tmac
+++ b/tmac/s.tmac
@@ -1788,6 +1788,9 @@
 .\" ****************************
 .\" Table of contents generation.
 .de @XS
+.\" Close the HTML heading before diverting a printed contents entry.
+.if \\n[s@devtag-needs-end-of-heading] .DEVTAG-EO-H
+.nr s@devtag-needs-end-of-heading 0
 .da toc*div
 .ev h
 .ie \\n[.$] .XA "\\$1"
@@ -1936,6 +1939,8 @@
 .		\}
 .	\}
 .	rm eqn*msg
+.	if '\*(.T'html' .nr eqn*type 3
+.	if r ps4html .nr eqn*type 3
 .\}
 .di eqn*div
 .in 0
