Author: tilman
Date: Mon Jul 20 08:02:46 2026
New Revision: 1936347
Log:
PDFBOX-4951: add glyph layout, by Volker Kunert
Added:
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/ContentStreamForGlyphLayoutInterface.java
- copied unchanged from r1936107,
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/ContentStreamForGlyphLayoutInterface.java
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphLayoutProcessorInterface.java
- copied unchanged from r1936107,
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphLayoutProcessorInterface.java
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphsAndPositions.java
- copied unchanged from r1936107,
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphsAndPositions.java
Modified:
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDAbstractContentStream.java
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroForm.java
Copied:
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/ContentStreamForGlyphLayoutInterface.java
(from r1936107,
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/ContentStreamForGlyphLayoutInterface.java)
==============================================================================
--- /dev/null 00:00:00 1970 (empty, because file is newly added)
+++
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/ContentStreamForGlyphLayoutInterface.java
Mon Jul 20 08:02:46 2026 (r1936347, copy of r1936107,
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/ContentStreamForGlyphLayoutInterface.java)
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pdfbox.pdmodel;
+
+import java.io.IOException;
+
+public interface ContentStreamForGlyphLayoutInterface
+{
+
+ /**
+ * Show the given glyphs at the specified positions
+ *
+ * @param glyphsAndPositions List of glyphs and positions
+ * @throws IOException if an IO error occurs
+ */
+ void showGlyphsWithPositioning(GlyphsAndPositions glyphsAndPositions)
throws IOException;
+
+ /**
+ * Shows the glyphs for the given glyph codes
+ *
+ * @param glyphCodes Array of glyph codes of the content font
+ * @throws IOException if an I/O exception occurs
+ */
+ void showGlyphCodes(int[] glyphCodes) throws IOException;
+
+ /**
+ * Set the text rise value, i.e. move the baseline up or down. This is
useful for drawing
+ * superscripts or subscripts.
+ *
+ * @param rise Specifies the distance, in unscaled text space units, to
move the baseline up or
+ * down from its default location. 0 restores the default location.
+ * @throws IOException If the content stream could not be written.
+ */
+ void setTextRise(float rise) throws IOException;
+}
Copied:
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphLayoutProcessorInterface.java
(from r1936107,
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphLayoutProcessorInterface.java)
==============================================================================
--- /dev/null 00:00:00 1970 (empty, because file is newly added)
+++
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphLayoutProcessorInterface.java
Mon Jul 20 08:02:46 2026 (r1936347, copy of r1936107,
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphLayoutProcessorInterface.java)
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pdfbox.pdmodel;
+
+import org.apache.pdfbox.pdmodel.font.PDFont;
+import org.apache.pdfbox.pdmodel.font.PDType0Font;
+
+import java.io.IOException;
+
+/**
+ * Interface for glyph layout that is independent of a specific implementation
so that more
+ * implementations can be tried in the future.
+ *
+ * @author Volker Kunert
+ */
+public interface GlyphLayoutProcessorInterface
+{
+
+ /**
+ * Checks if the font is supported
+ *
+ * @param font to be checked
+ * @return true if glyph layout is supported for this font and this font
is a PDType0Font
+ */
+ boolean supportsFont(PDFont font);
+
+ /**
+ * Shows a text using glyph positioning (if needed)
+ *
+ * @param contentStream the content stream
+ * @param font to be used
+ * @param fontSize font size
+ * @param text text to show
+ * @throws IOException if an I/O exception occurs
+ * @throws IllegalArgumentException if glyphs are missing
+ */
+ void showText(ContentStreamForGlyphLayoutInterface contentStream,
PDType0Font font, float fontSize, String text) throws IOException;
+}
Copied:
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphsAndPositions.java
(from r1936107,
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphsAndPositions.java)
==============================================================================
--- /dev/null 00:00:00 1970 (empty, because file is newly added)
+++
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphsAndPositions.java
Mon Jul 20 08:02:46 2026 (r1936347, copy of r1936107,
pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/GlyphsAndPositions.java)
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.pdfbox.pdmodel;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * Stores sublists of glyphs and positions in a list
+ *
+ * @author Volker Kunert
+ */
+public class GlyphsAndPositions
+{
+
+ private final ArrayList<Object> list = new ArrayList<>();
+
+ /**
+ * Sublist to store adjacent glyphs
+ */
+ public static class GlyphSubList extends ArrayList<Integer>
+ {
+ }
+
+ /**
+ * Adds a glyph
+ *
+ * @param glyph to be added
+ */
+ public void add(Integer glyph)
+ {
+ Object last = !list.isEmpty() ? list.get(list.size() - 1) : null;
+ GlyphSubList glyphSubList;
+ if (!(last instanceof GlyphSubList))
+ {
+ glyphSubList = new GlyphSubList();
+ list.add(glyphSubList);
+ }
+ else
+ {
+ glyphSubList = (GlyphSubList) last;
+ }
+ glyphSubList.add(glyph);
+ }
+
+ /**
+ * Add a position
+ *
+ * @param position to be added
+ */
+ public void add(Float position)
+ {
+ list.add(position);
+ }
+
+ /**
+ * Checks if the list is empty
+ *
+ * @return true if it is empty
+ */
+ public boolean isEmpty()
+ {
+ return list.isEmpty();
+ }
+
+ /**
+ * Clears the list
+ */
+ public void clear()
+ {
+ list.clear();
+ }
+
+ /**
+ * Converts GlyphsAndPositions to an array of objects (GlyphSubList and
Float)
+ *
+ * @return the array
+ */
+ public Object[] toArray()
+ {
+ return Collections.unmodifiableList(list).toArray();
+ }
+}
Modified:
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDAbstractContentStream.java
==============================================================================
---
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDAbstractContentStream.java
Mon Jul 20 07:30:14 2026 (r1936346)
+++
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/PDAbstractContentStream.java
Mon Jul 20 08:02:46 2026 (r1936347)
@@ -74,7 +74,7 @@ import org.apache.pdfbox.util.StringUtil
*
* @author Ben Litchfield
*/
-abstract class PDAbstractContentStream implements Closeable
+abstract class PDAbstractContentStream implements
ContentStreamForGlyphLayoutInterface, Closeable
{
private static final Log LOG =
LogFactory.getLog(PDAbstractContentStream.class);
@@ -85,6 +85,7 @@ abstract class PDAbstractContentStream i
protected boolean inTextMode = false;
protected final Deque<PDFont> fontStack = new ArrayDeque<>();
+ protected final Deque<Float> fontSizeStack = new ArrayDeque<>();
protected final Deque<PDColorSpace> nonStrokingColorSpaceStack = new
ArrayDeque<>();
protected final Deque<PDColorSpace> strokingColorSpaceStack = new
ArrayDeque<>();
@@ -95,6 +96,7 @@ abstract class PDAbstractContentStream i
private final Map<PDType0Font, GsubWorker> gsubWorkerMap = new HashMap<>();
private final GsubWorkerFactory gsubWorkerFactory = new
GsubWorkerFactory();
+ private GlyphLayoutProcessorInterface glyphLayoutProcessor;
/**
* Create a new appearance stream.
@@ -114,6 +116,16 @@ abstract class PDAbstractContentStream i
}
/**
+ * Sets the glyph layout processor
+ *
+ * @param glyphLayoutProcessor glyph layout processor
+ */
+ public void setGlyphLayoutProcessor(GlyphLayoutProcessorInterface
glyphLayoutProcessor)
+ {
+ this.glyphLayoutProcessor = glyphLayoutProcessor;
+ }
+
+ /**
* Sets the maximum number of digits allowed for fractional numbers.
*
* @see NumberFormat#setMaximumFractionDigits(int)
@@ -177,6 +189,16 @@ abstract class PDAbstractContentStream i
fontStack.push(font);
}
+ if (fontSizeStack.isEmpty())
+ {
+ fontSizeStack.add(fontSize);
+ }
+ else
+ {
+ fontSizeStack.pop();
+ fontSizeStack.push(fontSize);
+ }
+
// keep track of fonts which are configured for subsetting
if (font.willBeSubset())
{
@@ -234,6 +256,16 @@ abstract class PDAbstractContentStream i
*/
public void showTextWithPositioning(Object[] textWithPositioningArray)
throws IOException
{
+ if (!inTextMode)
+ {
+ throw new IllegalStateException("Must call beginText() before
showTextWithPositioning()");
+ }
+
+ if (fontStack.isEmpty())
+ {
+ throw new IllegalStateException("Must call setFont() before
showTextWithPositioning()");
+ }
+
write("[");
for (Object obj : textWithPositioningArray)
{
@@ -255,6 +287,49 @@ abstract class PDAbstractContentStream i
}
/**
+ * Show the given glyphs at the specified positions. This method is meant
to be called from
+ * within a GlyphLayoutProcessorInterface implementation and only for
PDType0Font.
+ *
+ * @param glyphsAndPositions List of glyphs and positions
+ * @throws IOException if an IO error occurs
+ * @throws IllegalStateException if the current font isn't a PDType0Font.
+ */
+ @Override
+ public void showGlyphsWithPositioning(GlyphsAndPositions
glyphsAndPositions) throws IOException
+ {
+ write("[");
+
+ for (Object obj : glyphsAndPositions.toArray())
+ {
+ if (obj instanceof GlyphsAndPositions.GlyphSubList)
+ {
+ GlyphsAndPositions.GlyphSubList glyphSubList =
(GlyphsAndPositions.GlyphSubList) obj;
+ int[] intGlyphArray = new int[glyphSubList.size()];
+ // Convert Type to int[]
+ for (int i = 0; i < intGlyphArray.length; i++)
+ {
+ intGlyphArray[i] = glyphSubList.get(i);
+ }
+ writeTextPDType0Font(intGlyphArray);
+ }
+ else if (obj instanceof Float)
+ {
+ writeOperand((Float) obj);
+ }
+ else
+ {
+ if (obj == null)
+ {
+ throw new NullPointerException("Argument contains null
entry");
+ }
+ throw new IllegalArgumentException("Argument must consist of
array of Float and GlyphsAndPositions.GlyphSubList types, not " +
obj.getClass().getName());
+ }
+ }
+ write("] ");
+ writeOperator(OperatorName.SHOW_TEXT_ADJUSTED);
+ }
+
+ /**
* Shows the given text at the location specified by the current text
matrix.
*
* @param text The Unicode text to show.
@@ -263,30 +338,104 @@ abstract class PDAbstractContentStream i
*/
public void showText(String text) throws IOException
{
- showTextInternal(text);
+ if (!inTextMode)
+ {
+ throw new IllegalStateException("Must call beginText() before
showText()");
+ }
+ if (fontStack.isEmpty())
+ {
+ throw new IllegalStateException("Must call setFont() before
showText()");
+ }
+ if (fontSizeStack.isEmpty())
+ {
+ throw new IllegalStateException("Font is set, but fontSize is not
set");
+ }
+ PDFont font = fontStack.peek();
+ if (glyphLayoutProcessor != null &&
glyphLayoutProcessor.supportsFont(font))
+ {
+ float fontSize = fontSizeStack.peek();
+ glyphLayoutProcessor.showText(this, (PDType0Font) font, fontSize,
text);
+ }
+ else
+ {
+ showTextInternal(text);
+ write(" ");
+ writeOperator(OperatorName.SHOW_TEXT);
+ }
+ }
+
+ /**
+ * Shows the glyphs for the given glyph codes - only for PDType0Font
+ *
+ * @param glyphCodes Array of glyph codes of the content font
+ * @throws IOException if an I/O exception occurs
+ * @throws IllegalStateException if the current font isn't a PDType0Font.
+ */
+ @Override
+ public void showGlyphCodes(int[] glyphCodes) throws IOException
+ {
+ writeTextPDType0Font(glyphCodes);
write(" ");
writeOperator(OperatorName.SHOW_TEXT);
}
/**
- * Outputs a string using the correct encoding and subsetting as required.
+ * Outputs the given glyph codes - only for PDType0Font
*
- * @param text The Unicode text to show.
- *
- * @throws IOException If an io exception occurs.
+ * @param glyphCodes The glyph codes to write
+ *
+ * @throws IOException in case of I/O error
+ * @throws IllegalStateException if the current font isn't a PDType0Font.
*/
- protected void showTextInternal(String text) throws IOException
+ protected void writeTextPDType0Font(int[] glyphCodes) throws IOException
{
if (!inTextMode)
{
- throw new IllegalStateException("Must call beginText() before
showText()");
+ throw new IllegalStateException("Must call beginText() before
writeTextPDType0Font()");
}
-
if (fontStack.isEmpty())
{
- throw new IllegalStateException("Must call setFont() before
showText()");
+ throw new IllegalStateException("Must call setFont() before
writeTextPDType0Font()");
+ }
+ PDFont font = fontStack.peek();
+ if (!(font instanceof PDType0Font))
+ {
+ throw new IllegalStateException("Must be called with current font
instance of PDType0Font");
+ }
+ PDType0Font pdType0Font = (PDType0Font) font;
+
+ // encode glyphs, update set of used glyphs
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ Set<Integer> glyphIds = new HashSet<>();
+
+ for (int glyphCode : glyphCodes)
+ {
+ out.write(pdType0Font.encodeGlyphId(glyphCode));
+ if (glyphCode < 0xFFFF)
+ {
+ glyphIds.add(glyphCode);
+ }
+ }
+ byte[] encodedText = out.toByteArray();
+
+ // add glyphs to subset
+ if (pdType0Font.willBeSubset())
+ {
+ pdType0Font.addGlyphsToSubset(glyphIds);
}
+ // write encoded text and the PDF operator
+ COSWriter.writeString(encodedText, outputStream);
+ }
+ /**
+ * Outputs a string using the correct encoding and subsetting as required.
+ *
+ * @param text The Unicode text to show.
+ *
+ * @throws IOException If an io exception occurs.
+ */
+ protected void showTextInternal(String text) throws IOException
+ {
PDFont font = fontStack.peek();
// complex text layout
Modified:
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java
==============================================================================
---
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java
Mon Jul 20 07:30:14 2026 (r1936346)
+++
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/AppearanceGeneratorHelper.java
Mon Jul 20 08:02:46 2026 (r1936347)
@@ -34,6 +34,7 @@ import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.cos.COSString;
import org.apache.pdfbox.pdfparser.PDFStreamParser;
import org.apache.pdfbox.pdfwriter.ContentStreamWriter;
+import org.apache.pdfbox.pdmodel.GlyphLayoutProcessorInterface;
import org.apache.pdfbox.pdmodel.PDResources;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.font.PDFont;
@@ -487,6 +488,11 @@ class AppearanceGeneratorHelper
{
try (PDAppearanceContentStream contents = new
PDAppearanceContentStream(appearanceStream, output))
{
+ GlyphLayoutProcessorInterface glyphLayoutProcessor =
field.getAcroForm().getGlyphLayoutProcessor();
+ if (glyphLayoutProcessor != null)
+ {
+ contents.setGlyphLayoutProcessor(glyphLayoutProcessor);
+ }
PDRectangle bbox = resolveBoundingBox(widget, appearanceStream);
// Acrobat calculates the left and right padding dependent on the
offset of the border edge
Modified:
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroForm.java
==============================================================================
---
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroForm.java
Mon Jul 20 07:30:14 2026 (r1936346)
+++
pdfbox/branches/3.0/pdfbox/src/main/java/org/apache/pdfbox/pdmodel/interactive/form/PDAcroForm.java
Mon Jul 20 08:02:46 2026 (r1936347)
@@ -37,6 +37,7 @@ import org.apache.pdfbox.cos.COSArray;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSDictionary;
import org.apache.pdfbox.cos.COSName;
+import org.apache.pdfbox.pdmodel.GlyphLayoutProcessorInterface;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
@@ -78,6 +79,8 @@ public final class PDAcroForm implements
private final Map<COSName, SoftReference<PDFont>> directFontCache = new
HashMap<>();
+ private GlyphLayoutProcessorInterface glyphLayoutProcessor;
+
/**
* Constructor.
*
@@ -103,6 +106,26 @@ public final class PDAcroForm implements
}
/**
+ * Sets the glyph layout processor
+ *
+ * @param glyphLayoutProcessor glyph layout processor
+ */
+ public void setGlyphLayoutProcessor(GlyphLayoutProcessorInterface
glyphLayoutProcessor)
+ {
+ this.glyphLayoutProcessor = glyphLayoutProcessor;
+ }
+
+ /**
+ * Returns the glyph layout processor or null
+ *
+ * @return the glyph layout processor or null
+ */
+ public GlyphLayoutProcessorInterface getGlyphLayoutProcessor()
+ {
+ return glyphLayoutProcessor;
+ }
+
+ /**
* This will get the document associated with this form.
*
* @return The PDF document.
@@ -280,6 +303,10 @@ public final class PDAcroForm implements
try (PDPageContentStream contentStream = new
PDPageContentStream(
document, page, AppendMode.APPEND, true,
!isContentStreamWrapped))
{
+ if (glyphLayoutProcessor != null)
+ {
+
contentStream.setGlyphLayoutProcessor(glyphLayoutProcessor);
+ }
isContentStreamWrapped = true;
PDAppearanceStream appearanceStream =
annotation.getNormalAppearanceStream();