This is an automated email from the ASF dual-hosted git repository.

mattcasters pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new 12c6f33fa7 Add missing font for correct CJK rendering, fixes #8528 
(#8533)
12c6f33fa7 is described below

commit 12c6f33fa7de972a207cd91b8da7a8b81ec9300e
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Wed Sep 23 17:13:22 2026 +0200

    Add missing font for correct CJK rendering, fixes #8528 (#8533)
---
 .../org/apache/hop/core/svg/HopSvgGraphics2D.java  | 39 +++++++++++++++++-
 .../apache/hop/core/svg/HopSvgGraphics2DTest.java  | 47 ++++++++++++++++++++++
 docker/Dockerfile                                  |  6 ++-
 docker/unified.Dockerfile                          | 10 +++--
 docker/web.Dockerfile                              |  7 +++-
 .../main/java/org/apache/hop/core/gui/SvgGc.java   |  7 +++-
 6 files changed, 108 insertions(+), 8 deletions(-)

diff --git a/core/src/main/java/org/apache/hop/core/svg/HopSvgGraphics2D.java 
b/core/src/main/java/org/apache/hop/core/svg/HopSvgGraphics2D.java
index 6418e5a28f..ddae5999ed 100644
--- a/core/src/main/java/org/apache/hop/core/svg/HopSvgGraphics2D.java
+++ b/core/src/main/java/org/apache/hop/core/svg/HopSvgGraphics2D.java
@@ -20,6 +20,7 @@ package org.apache.hop.core.svg;
 import static org.apache.batik.svggen.DOMGroupManager.DRAW;
 import static org.apache.batik.svggen.DOMGroupManager.FILL;
 
+import java.awt.Font;
 import java.awt.font.TextLayout;
 import java.io.StringWriter;
 import java.text.DecimalFormat;
@@ -141,11 +142,42 @@ public class HopSvgGraphics2D extends SVGGraphics2D {
     return family + ", " + SANS_SERIF_STACK;
   }
 
+  /**
+   * The font to measure {@code text} with so that the measurement reflects 
what a browser draws.
+   *
+   * <p>Java 2D only substitutes glyphs a font lacks for the logical fonts, 
never for a physical one
+   * like the canvas font: a CJK name measured with 'DejaVu Sans' on a JVM 
without a CJK font is a
+   * row of missing-glyph boxes, each roughly half the width of the ideograph 
the browser draws from
+   * its own fallback font (#8528). For such text the logical SansSerif font, 
which falls back
+   * through the platform font configuration the way the browser does, gives a 
usable width.
+   *
+   * @param font the font the text is drawn with
+   * @param text the text to measure
+   * @return {@code font} when it has every glyph of {@code text}, otherwise a 
logical font of the
+   *     same style and size that does, or {@code null} when this JVM has no 
font for the text at
+   *     all
+   */
+  public static Font measuringFont(Font font, String text) {
+    if (font == null || text == null || text.isEmpty() || 
font.canDisplayUpTo(text) == -1) {
+      return font;
+    }
+    Font logical = new Font(Font.SANS_SERIF, font.getStyle(), font.getSize());
+    if (font.getSize2D() != font.getSize()) {
+      logical = logical.deriveFont(font.getSize2D());
+    }
+    return logical.canDisplayUpTo(text) == -1 ? logical : null;
+  }
+
   /**
    * Draw the string like Batik does, but pin the run to the width this JVM 
measured for it. The
    * browser may not have the font the text was laid out with; with {@code 
textLength} it stretches
    * or squeezes the letter spacing so that the text still starts and ends 
where the painter put it,
    * under the borders, hover areas and hop labels that were sized for it.
+   *
+   * <p>The width comes from {@link #measuringFont(Font, String)}: text this 
JVM has no glyphs for
+   * is not pinned at all, since the only width known for it, that of 
missing-glyph boxes, would
+   * squeeze the browser's glyphs into an unreadable run. The browser's 
natural layout is then the
+   * best width there is.
    */
   @Override
   public void drawString(String s, float x, float y) {
@@ -153,7 +185,12 @@ public class HopSvgGraphics2D extends SVGGraphics2D {
       super.drawString(s, x, y);
       return;
     }
-    double width = getFont().getStringBounds(s, 
getFontRenderContext()).getWidth();
+    Font measuringFont = measuringFont(getFont(), s);
+    if (measuringFont == null) {
+      super.drawString(s, x, y);
+      return;
+    }
+    double width = measuringFont.getStringBounds(s, 
getFontRenderContext()).getWidth();
     if (width <= 0) {
       super.drawString(s, x, y);
       return;
diff --git 
a/core/src/test/java/org/apache/hop/core/svg/HopSvgGraphics2DTest.java 
b/core/src/test/java/org/apache/hop/core/svg/HopSvgGraphics2DTest.java
index f205ed51da..ed8acc8e3f 100644
--- a/core/src/test/java/org/apache/hop/core/svg/HopSvgGraphics2DTest.java
+++ b/core/src/test/java/org/apache/hop/core/svg/HopSvgGraphics2DTest.java
@@ -19,6 +19,8 @@ package org.apache.hop.core.svg;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import com.fasterxml.jackson.databind.JsonNode;
@@ -112,4 +114,49 @@ class HopSvgGraphics2DTest {
     assertFalse(xml.contains("'Dialog'"), xml);
     assertFalse(xml.contains("'SansSerif'"), xml);
   }
+
+  /**
+   * A string no font on this JVM has glyphs for (CJK on a JVM without a CJK 
font, #8528) is not
+   * pinned: the only width known for it is that of missing-glyph boxes, so 
the browser must lay it
+   * out itself. U+0378 is unassigned, so no font on any platform has a glyph 
for it.
+   */
+  @Test
+  void testDrawStringDoesNotPinUndisplayableText() throws Exception {
+    HopSvgGraphics2D graphics2D = HopSvgGraphics2D.newDocument();
+    Font font = new Font(Font.SANS_SERIF, Font.PLAIN, 10);
+    graphics2D.setFont(font);
+    String text = "Table\u0378input";
+    assertTrue(font.canDisplayUpTo(text) >= 0, "test needs an undisplayable 
character");
+    assertNull(HopSvgGraphics2D.measuringFont(font, text));
+    graphics2D.drawString(text, 20, 40);
+    String xml = graphics2D.toXml();
+
+    assertTrue(xml.contains("<text"), xml);
+    assertFalse(xml.contains("textLength"), xml);
+    assertTrue(xml.contains("x=\"20\""), xml);
+    assertTrue(xml.contains("y=\"40\""), xml);
+  }
+
+  /**
+   * Text the canvas font lacks glyphs for is measured with the logical 
SansSerif font, which falls
+   * back through the platform font configuration, when that font can display 
it. A physical font
+   * that has the glyphs is kept as is.
+   */
+  @Test
+  void testMeasuringFontFallsBackToLogicalFont() {
+    Font physical = new Font(Font.SANS_SERIF, Font.BOLD, 10).deriveFont(10.5f);
+    assertSame(physical, HopSvgGraphics2D.measuringFont(physical, "Table 
input"));
+    assertSame(physical, HopSvgGraphics2D.measuringFont(physical, ""));
+    assertNull(HopSvgGraphics2D.measuringFont(null, "Table input"));
+
+    // Whether the JVM has a CJK font depends on the platform; whichever the 
answer, the outcome is
+    // either a logical font of the same style and size or no font at all, 
never the boxes.
+    String cjk = "\u8868\u8f93\u5165";
+    Font measuring = HopSvgGraphics2D.measuringFont(new Font("DejaVu Sans", 
Font.BOLD, 10), cjk);
+    if (measuring != null) {
+      assertEquals(-1, measuring.canDisplayUpTo(cjk));
+      assertEquals(Font.BOLD, measuring.getStyle());
+      assertEquals(10, measuring.getSize());
+    }
+  }
 }
diff --git a/docker/Dockerfile b/docker/Dockerfile
index a83a3c302e..39dc63efe0 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -97,13 +97,15 @@ ENV HOP_SERVER_MAX_OBJECT_TIMEOUT=
 ENV HOP_CONFIG_OPTIONS=
 
 # INSTALL REQUIRED PACKAGES AND ADJUST LOCALE
+# hop-server paints pipeline/workflow images server-side. font-dejavu is the 
family the web image
+# measures text with as well, so both lay text out the same; without 
font-noto-cjk the JVM measures
+# Chinese/Japanese/Korean names as missing-glyph boxes and lays them out too 
narrow (#8528)
 # procps: The package includes the programs 
ps/home/[email protected]/Projects/demo, top, vmstat, w, kill, free, 
slabtop, and skill
 RUN addgroup -g ${HOP_GID} -S hop \
     && adduser -u ${HOP_UID} -S -D -G hop hop \
     && chmod 777 -R /tmp && chmod o+t -R /tmp \
     && apk update \
-    && apk --no-cache add bash curl fontconfig msttcorefonts-installer 
openjdk21-jre procps \
-    && update-ms-fonts \
+    && apk --no-cache add bash curl fontconfig font-dejavu font-noto-cjk 
openjdk21-jre procps \
     && fc-cache -f \
     && rm -rf /var/cache/apk/* \
     && mkdir ${DEPLOYMENT_PATH} \
diff --git a/docker/unified.Dockerfile b/docker/unified.Dockerfile
index 424c3340fc..dbad5cd03d 100644
--- a/docker/unified.Dockerfile
+++ b/docker/unified.Dockerfile
@@ -327,8 +327,7 @@ RUN addgroup -g ${HOP_GID} -S hop \
     && adduser -u ${HOP_UID} -S -D -G hop hop \
     && chmod 777 -R /tmp && chmod o+t -R /tmp \
     && apk update \
-    && apk --no-cache add bash curl fontconfig msttcorefonts-installer 
openjdk21-jre procps \
-    && update-ms-fonts \
+    && apk --no-cache add bash curl fontconfig font-dejavu font-noto-cjk 
openjdk21-jre procps \
     && fc-cache -f \
     && rm -rf /var/cache/apk/* \
     && mkdir ${DEPLOYMENT_PATH} \
@@ -395,7 +394,12 @@ ENV CATALINA_OPTS='${HOP_OPTIONS} \
   -DHOP_GUI_ZOOM_FACTOR="${HOP_GUI_ZOOM_FACTOR}"'
 
 # Create Hop user
-RUN groupadd -r hop -g ${HOP_GID} \
+# fonts-noto-cjk: the canvas is painted server-side; without a CJK font the 
JVM measures
+# Chinese/Japanese/Korean names as missing-glyph boxes and lays them out too 
narrow (#8528)
+RUN apt-get update \
+    && apt-get install -y --no-install-recommends fonts-noto-cjk \
+    && rm -rf /var/lib/apt/lists/* \
+    && groupadd -r hop -g ${HOP_GID} \
     && useradd -d /home/hop -u ${HOP_UID} -m -s /bin/bash -g hop hop \
     && rm -rf webapps/* \
     && mkdir "${CATALINA_HOME}"/webapps/ROOT \
diff --git a/docker/web.Dockerfile b/docker/web.Dockerfile
index b4feff6a6b..4e7697d88f 100644
--- a/docker/web.Dockerfile
+++ b/docker/web.Dockerfile
@@ -68,7 +68,12 @@ ENV CATALINA_OPTS='${HOP_OPTIONS} \
   -DHOP_GUI_ZOOM_FACTOR="${HOP_GUI_ZOOM_FACTOR}"'
 
 # Create Hop user
-RUN groupadd -r hop -g ${HOP_GID} \
+# fonts-noto-cjk: the canvas is painted server-side; without a CJK font the 
JVM measures
+# Chinese/Japanese/Korean names as missing-glyph boxes and lays them out too 
narrow (#8528)
+RUN apt-get update \
+    && apt-get install -y --no-install-recommends fonts-noto-cjk \
+    && rm -rf /var/lib/apt/lists/* \
+    && groupadd -r hop -g ${HOP_GID} \
     && useradd -d /home/hop -u ${HOP_UID} -m -s /bin/bash -g hop hop \
     && rm -rf webapps/* \
     && mkdir "${CATALINA_HOME}"/webapps/ROOT \
diff --git a/engine/src/main/java/org/apache/hop/core/gui/SvgGc.java 
b/engine/src/main/java/org/apache/hop/core/gui/SvgGc.java
index 9ae5696332..97993c151d 100644
--- a/engine/src/main/java/org/apache/hop/core/gui/SvgGc.java
+++ b/engine/src/main/java/org/apache/hop/core/gui/SvgGc.java
@@ -22,6 +22,7 @@ import java.awt.BasicStroke;
 import java.awt.Color;
 import java.awt.Dimension;
 import java.awt.Font;
+import java.awt.FontMetrics;
 import java.awt.GraphicsEnvironment;
 import java.awt.Polygon;
 import java.awt.RenderingHints;
@@ -582,7 +583,11 @@ public class SvgGc implements IGc {
     String[] lines = text.split(Const.CR);
     int maxWidth = 0;
     for (String line : lines) {
-      Rectangle2D bounds = gc.getFontMetrics().getStringBounds(line, gc);
+      // The canvas font has no glyphs for CJK text; measure such lines with a 
font that does.
+      Font measuringFont = HopSvgGraphics2D.measuringFont(gc.getFont(), line);
+      FontMetrics metrics =
+          measuringFont == null ? gc.getFontMetrics() : 
gc.getFontMetrics(measuringFont);
+      Rectangle2D bounds = metrics.getStringBounds(line, gc);
       if (bounds.getWidth() > maxWidth) {
         maxWidth = (int) bounds.getWidth();
       }

Reply via email to