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();
}