diff --git a/debugger/src/main/java/org/apache/pdfbox/debugger/pagepane/PagePane.java b/debugger/src/main/java/org/apache/pdfbox/debugger/pagepane/PagePane.java index ddcbb5459b0..831a4c60dd9 100644 --- a/debugger/src/main/java/org/apache/pdfbox/debugger/pagepane/PagePane.java +++ b/debugger/src/main/java/org/apache/pdfbox/debugger/pagepane/PagePane.java @@ -95,7 +95,7 @@ /** * Display the page number and a page rendering. - * + * * @author Tilman Hausherr * @author John Hewson */ @@ -298,7 +298,7 @@ private void initUI() pageLabel.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 20)); pageLabel.setBorder(BorderFactory.createEmptyBorder(5, 0, 10, 0)); panel.add(pageLabel); - + label = new JLabel(); label.addMouseMotionListener(this); label.addMouseListener(this); @@ -395,7 +395,7 @@ public void ancestorAdded(AncestorEvent ancestorEvent) { zoomMenu.addMenuListeners(this); zoomMenu.setEnableMenu(true); - + rotationMenu = RotationMenu.getInstance(); rotationMenu.addMenuListeners(this); rotationMenu.setEnableMenu(true); @@ -412,7 +412,7 @@ public void ancestorAdded(AncestorEvent ancestorEvent) JMenu menuInstance = viewMenu.getMenu(); int itemCount = menuInstance.getItemCount(); - + for (int i = 0; i< itemCount; i++) { JMenuItem item = menuInstance.getItem(i); @@ -435,7 +435,7 @@ public void ancestorRemoved(AncestorEvent ancestorEvent) JMenu menuInstance = viewMenu.getMenu(); int itemCount = menuInstance.getItemCount(); - + for (int i = 0; i< itemCount; i++) { JMenuItem item = menuInstance.getItem(i); @@ -705,7 +705,7 @@ protected BufferedImage doInBackground() throws IOException PDFRenderer renderer = new PDFRenderer(document); renderer.setSubsamplingAllowed(ViewMenu.isAllowSubsampling()); - //renderer.setHintingEnabled(ViewMenu.isHintingEnabled()) + renderer.setHintingEnabled(ViewMenu.isHintingEnabled()); long t0 = System.nanoTime(); BufferedImage image = renderer.renderImage(pageIndex, scale, ImageTypeMenu.getImageType(), RenderDestinationMenu.getRenderDestination()); @@ -716,13 +716,13 @@ protected BufferedImage doInBackground() throws IOException statuslabel.setText(labelText); // debug overlays - DebugTextOverlay debugText = new DebugTextOverlay(document, pageIndex, scale, + DebugTextOverlay debugText = new DebugTextOverlay(document, pageIndex, scale, showTextStripper, showTextStripperBeads, showFontBBox, ViewMenu.isShowGlyphBounds()); Graphics2D g = image.createGraphics(); debugText.renderTo(g); g.dispose(); - + return ImageUtil.getRotatedImage(image, rotation); } @@ -733,12 +733,12 @@ protected void done() { BufferedImage image = get(); - // We cannot use "label.setIcon(new ImageIcon(get()))" here - // because of blurry upscaling in JDK9. Instead, the label is now created with + // We cannot use "label.setIcon(new ImageIcon(get()))" here + // because of blurry upscaling in JDK9. Instead, the label is now created with // a smaller size than the image to compensate that the // image is scaled up with some screen configurations (e.g. 125% on windows). // See PDFBOX-3665 for more sample code and discussion. - label.setSize((int) Math.ceil(image.getWidth() / defaultTransform.getScaleX()), + label.setSize((int) Math.ceil(image.getWidth() / defaultTransform.getScaleX()), (int) Math.ceil(image.getHeight() / defaultTransform.getScaleY())); label.setIcon(new HighResolutionImageIcon(image, label.getWidth(), label.getHeight())); label.setText(null); diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/BytecodeStream.java b/fontbox/src/main/java/org/apache/fontbox/ttf/BytecodeStream.java new file mode 100644 index 00000000000..80c740f4bf1 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/BytecodeStream.java @@ -0,0 +1,140 @@ +/* + * 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.fontbox.ttf; + +/** + * A bounds-checked cursor over a TrueType bytecode program (fpgm, prep, a glyph instruction stream, or + * a function body). It owns the program counter so individual opcode handlers never index the array by + * hand - this removes a whole class of off-by-one and overrun bugs and is trivially testable on its + * own. Reads past the end throw {@link HintingException} so the per-glyph fallback can catch cleanly. + * + * @author Apache PDFBox + */ +class BytecodeStream +{ + private final byte[] code; + private int ip; + private int instructionStart; + + /** + * @param code the bytecode program; not copied + */ + public BytecodeStream(byte[] code) + { + this.code = code != null ? code : new byte[0]; + } + + /** + * @return the underlying bytecode array (not copied); used to record function entry points + */ + public byte[] getCode() + { + return code; + } + + /** + * @return true if there is at least one more byte to read + */ + public boolean hasNext() + { + return ip < code.length; + } + + /** + * @return the current program-counter position + */ + public int position() + { + return ip; + } + + /** + * Records the current position as the start of the instruction about to be read. Relative jumps + * ({@code JMPR}/{@code JROT}/{@code JROF}) are measured from here. + */ + public void markInstructionStart() + { + instructionStart = ip; + } + + /** + * @return the position recorded by the most recent {@link #markInstructionStart()} + */ + public int instructionStart() + { + return instructionStart; + } + + /** + * Moves the program counter to an absolute position. + * + * @param position the new position, within {@code [0, length]} + * @throws HintingException if the position is out of range + */ + public void seek(int position) + { + if (position < 0 || position > code.length) + { + throw new HintingException( + "bytecode seek out of range: " + position + " of " + code.length); + } + ip = position; + } + + /** + * Advances the program counter by a relative amount (may be negative). + * + * @param delta the number of bytes to skip + * @throws HintingException if the result is out of range + */ + public void skip(int delta) + { + seek(ip + delta); + } + + /** + * Reads the next byte as an unsigned 0-255 value (an opcode, or push operand). + * + * @return the next unsigned byte + * @throws HintingException if the stream is exhausted + */ + public int nextByte() + { + if (ip >= code.length) + { + throw new HintingException("bytecode read past end at " + ip); + } + return code[ip++] & 0xFF; + } + + /** + * Reads the next two bytes as a signed big-endian 16-bit word. + * + * @return the next signed word + * @throws HintingException if fewer than two bytes remain + */ + public int nextWord() + { + if (ip + 1 >= code.length) + { + throw new HintingException("bytecode word read past end at " + ip); + } + int hi = code[ip++] & 0xFF; + int lo = code[ip++] & 0xFF; + return (short) ((hi << 8) | lo); + } +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/ControlValueProgramTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/ControlValueProgramTable.java new file mode 100644 index 00000000000..e5c3ae48e03 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/ControlValueProgramTable.java @@ -0,0 +1,65 @@ +/* + * 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.fontbox.ttf; + +import java.io.IOException; + +/** + * The 'prep' (Control Value Program, also known as the pre-program) table. It holds TrueType hinting + * bytecode that is executed whenever the point size or transform changes, to prepare the Control Value + * Table and graphics state for the new ppem. The bytecode is stored here as raw bytes and run by the + * interpreter. + * + * @author Apache PDFBox + */ +public class ControlValueProgramTable extends TTFTable +{ + /** + * A tag that identifies this table type. + */ + public static final String TAG = "prep"; + + private byte[] program; + + ControlValueProgramTable() + { + } + + /** + * This will read the required data from the stream. + * + * @param ttf The font that is being read. + * @param data The stream to read the data from. + * @throws IOException If there is an error reading the data. + */ + @Override + void read(TrueTypeFont ttf, TTFDataStream data) throws IOException + { + program = data.read((int) getLength()); + initialized = true; + } + + /** + * Returns the raw control value program bytecode. + * + * @return the bytecode of the pre-program + */ + public byte[] getProgram() + { + return program; + } +} \ No newline at end of file diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/ControlValueTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/ControlValueTable.java new file mode 100644 index 00000000000..6d87318ac7a --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/ControlValueTable.java @@ -0,0 +1,82 @@ +/* + * 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.fontbox.ttf; + +import java.io.IOException; + +/** + * The 'cvt ' (Control Value) table. It holds an array of reference values - stem widths, heights and + * similar control measurements - used by the TrueType hinting bytecode. The values are stored here in + * raw font units (signed FWords); they are scaled to the active ppem by the interpreter, not at parse + * time. + * + * @author Apache PDFBox + */ +public class ControlValueTable extends TTFTable +{ + /** + * A tag that identifies this table type. + */ + public static final String TAG = "cvt "; + + private int[] values; + + ControlValueTable() + { + } + + /** + * This will read the required data from the stream. + * + * @param ttf The font that is being read. + * @param data The stream to read the data from. + * @throws IOException If there is an error reading the data. + */ + @Override + void read(TrueTypeFont ttf, TTFDataStream data) throws IOException + { + int count = (int) (getLength() / 2); + int[] cvt = new int[count]; + for (int i = 0; i < count; i++) + { + cvt[i] = data.readSignedShort(); + } + values = cvt; + initialized = true; + } + + /** + * Returns the raw control values in font units (FWords). The interpreter scales these to the + * active ppem. + * + * @return the control values in font units + */ + public int[] getValues() + { + return values; + } + + /** + * Returns the number of control values in this table. + * + * @return the entry count + */ + public int getValueCount() + { + return values != null ? values.length : 0; + } +} \ No newline at end of file diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/ExecutionContext.java b/fontbox/src/main/java/org/apache/fontbox/ttf/ExecutionContext.java new file mode 100644 index 00000000000..63f6c8bd5f4 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/ExecutionContext.java @@ -0,0 +1,504 @@ +/* + * 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.fontbox.ttf; + +/** + * All mutable state for a single run of the interpreter, bundled into one object so opcode handlers + * share a uniform {@code execute(ExecutionContext)} signature and new state can be added without + * touching every handler. It holds the operand stack, storage area, scaled control values, the two + * point zones, the {@link GraphicsState}, the current {@link BytecodeStream}, and the ppem the program + * is running at. + *
+ * Not all of that state is per-run. The storage area and the twilight zone are owned by the + * {@link TrueTypeInterpreter} and handed to every context at one size, because a font may compute values + * into them in {@code prep} and read them back from each glyph program - FreeType keeps both on the + * {@code TT_Size} for the same reason. + * + * @author Apache PDFBox + */ +class ExecutionContext +{ + private final TrueTypeInterpreter interpreter; + private final GraphicsState graphicsState; + + private final int[] stack; + private int stackPointer; + + // owned by the interpreter and shared by every context at one size, so prep can seed them + private final int[] storage; + private final Zone twilightZone; + + private final int[] controlValues; + private Zone glyphZone; + + private int ppem; + private int pointSize; + private int unitsPerEm; + + private BytecodeStream stream; + private int callDepth; + private boolean returnFromFunction; + + // Execution budget. TrueType can only loop through a backward jump or a LOOPCALL, so bounding + // those two bounds the whole program - without them a four-byte glyph program can spin forever. + // Both counters are per-context, and one context is one top-level program run, so they need no + // reset. After FreeType's neg_jump_counter / loopcall_counter in TT_RunIns. + private long negativeJumpCounter; + private long loopCallCounter; + private int executionBudget = -1; + + // v40 "backward compatibility" (grayscale subpixel) state. When set, point moves in the x + // direction are suppressed so stems are not grid-fit and darkened under antialiasing, and y moves + // are frozen once IUP has run on both axes. Only enabled for the glyph program, never fpgm/prep. + private boolean backwardCompatibility; + private boolean iupxCalled; + private boolean iupyCalled; + private boolean composite; + + /** + * @param interpreter the owning interpreter (for function calls) + * @param graphicsState the graphics state this run starts from + * @param maxStackElements operand stack capacity + * @param storage the interpreter's storage area, shared across the runs at one size + * @param controlValues the scaled control values (F26Dot6), or null + * @param twilightZone the interpreter's twilight zone, shared across the runs at one size + */ + public ExecutionContext(TrueTypeInterpreter interpreter, GraphicsState graphicsState, + int maxStackElements, int[] storage, int[] controlValues, Zone twilightZone) + { + this.interpreter = interpreter; + this.graphicsState = graphicsState; + this.stack = new int[Math.max(maxStackElements, 1)]; + this.storage = storage; + this.controlValues = controlValues != null ? controlValues : new int[0]; + this.twilightZone = twilightZone; + } + + /** @return the owning interpreter */ + public TrueTypeInterpreter getInterpreter() + { + return interpreter; + } + + /** @return the graphics state */ + public GraphicsState getGraphicsState() + { + return graphicsState; + } + + // --- operand stack --------------------------------------------------- + + /** + * Pushes a value onto the operand stack. + * + * @param value the value to push + * @throws HintingException on stack overflow + */ + public void push(int value) + { + if (stackPointer >= stack.length) + { + throw new HintingException("interpreter stack overflow at " + stackPointer); + } + stack[stackPointer++] = value; + } + + /** + * Pops a value from the operand stack. + * + * @return the popped value + * @throws HintingException on stack underflow + */ + public int pop() + { + if (stackPointer <= 0) + { + throw new HintingException("interpreter stack underflow"); + } + return stack[--stackPointer]; + } + + /** + * Returns the value {@code n} positions below the top without removing it ({@code peek(0)} is the + * top of stack). + * + * @param n depth below the top + * @return the value at that depth + * @throws HintingException if the depth is out of range + */ + public int peek(int n) + { + int index = stackPointer - 1 - n; + if (index < 0 || index >= stackPointer) + { + throw new HintingException("interpreter stack peek out of range: " + n); + } + return stack[index]; + } + + /** @return the current stack depth */ + public int getStackDepth() + { + return stackPointer; + } + + /** Empties the operand stack. */ + public void clearStack() + { + stackPointer = 0; + } + + // --- execution budget ------------------------------------------------- + + /** + * The maximum number of backward jumps, and separately of {@code LOOPCALL} iterations, this run may + * make before it is abandoned. Sized from the glyph's point count and the control value count the + * way FreeType sizes its counters, so a legitimately loop-heavy program still completes while a + * crafted one cannot run forever. Computed on first use, because the glyph zone is attached after + * the context is built. + * + * @return the per-run budget + */ + public int getExecutionBudget() + { + if (executionBudget < 0) + { + int points = glyphZone != null ? glyphZone.getPointCount() : 0; + executionBudget = Math.max(50, 10 * points) + Math.max(50, controlValues.length / 10); + } + return executionBudget; + } + + /** + * Records one backward jump, failing the run once {@link #getExecutionBudget()} is exhausted. + * + * @throws HintingException if too many backward jumps have been made + */ + public void countNegativeJump() + { + if (++negativeJumpCounter > getExecutionBudget()) + { + throw new HintingException( + "too many backward jumps, limit is " + getExecutionBudget()); + } + } + + /** + * Adds {@code count} iterations to the {@code LOOPCALL} budget, failing before the loop is entered + * rather than partway through it. The budget is cumulative across the run, so a program cannot slip + * past it by issuing many small loops. + * + * @param count the number of iterations about to be run, always positive + * @throws HintingException if the budget is exhausted + */ + public void countLoopCalls(int count) + { + loopCallCounter += count; + if (loopCallCounter > getExecutionBudget()) + { + throw new HintingException("LOOPCALL runs too long, limit is " + getExecutionBudget() + + " iterations, asked for " + loopCallCounter); + } + } + + // --- storage and control values -------------------------------------- + + /** @return the storage area, shared with every other run at this size */ + public int[] getStorage() + { + return storage; + } + + /** @return the scaled control values in F26Dot6 */ + public int[] getControlValues() + { + return controlValues; + } + + // --- zones ----------------------------------------------------------- + + /** @return the twilight zone (zone 0), shared with every other run at this size */ + public Zone getTwilightZone() + { + return twilightZone; + } + + /** @return the glyph zone (zone 1), or null if no glyph is loaded */ + public Zone getGlyphZone() + { + return glyphZone; + } + + /** @param zone the glyph zone (zone 1) */ + public void setGlyphZone(Zone zone) + { + this.glyphZone = zone; + } + + /** + * Resolves a zone pointer (0 = twilight, 1 = glyph) to its {@link Zone}. + * + * @param zonePointer the zone pointer value + * @return the corresponding zone + * @throws HintingException if the pointer is invalid or the glyph zone is unset + */ + public Zone getZone(int zonePointer) + { + if (zonePointer == 0) + { + return twilightZone; + } + if (zonePointer == 1) + { + if (glyphZone == null) + { + throw new HintingException("glyph zone referenced but not loaded"); + } + return glyphZone; + } + throw new HintingException("invalid zone pointer: " + zonePointer); + } + + // --- sizing ---------------------------------------------------------- + + /** @return the active pixels-per-em */ + public int getPpem() + { + return ppem; + } + + /** @param value the active pixels-per-em */ + public void setPpem(int value) + { + this.ppem = value; + } + + /** @return the point size */ + public int getPointSize() + { + return pointSize; + } + + /** @param value the point size */ + public void setPointSize(int value) + { + this.pointSize = value; + } + + /** @return the font's unitsPerEm */ + public int getUnitsPerEm() + { + return unitsPerEm; + } + + /** @param value the font's unitsPerEm */ + public void setUnitsPerEm(int value) + { + this.unitsPerEm = value; + } + + // --- projection / freedom vector math -------------------------------- + + /** + * Dot product of two F2Dot14 vectors (or a coordinate against an F2Dot14 vector), returning the + * result shifted back down by 14 bits with rounding. This is the projection primitive: projecting + * an F26Dot6 coordinate onto an F2Dot14 unit vector yields an F26Dot6 distance. + * + * @param ax first vector x + * @param ay first vector y + * @param bx second vector x (F2Dot14) + * @param by second vector y (F2Dot14) + * @return the rounded dot product + */ + public static int dot14(int ax, int ay, int bx, int by) + { + long product = (long) ax * bx + (long) ay * by; + return (int) ((product + 0x2000) >> 14); + } + + /** + * Projects a coordinate onto the projection vector. + * + * @param x the x coordinate in F26Dot6 + * @param y the y coordinate in F26Dot6 + * @return the projected distance in F26Dot6 + */ + public int project(int x, int y) + { + UnitVector pv = graphicsState.getProjectionVector(); + return dot14(x, y, pv.getX(), pv.getY()); + } + + /** + * Projects a coordinate onto the dual projection vector (used to measure original, unhinted + * positions). + * + * @param x the x coordinate in F26Dot6 + * @param y the y coordinate in F26Dot6 + * @return the projected distance in F26Dot6 + */ + public int dualProject(int x, int y) + { + UnitVector dv = graphicsState.getDualProjectionVector(); + return dot14(x, y, dv.getX(), dv.getY()); + } + + /** + * Returns the current (hinted) projected distance from point {@code p0} in {@code zone0} to point + * {@code p1} in {@code zone1}, measured along the projection vector. + */ + public int projectedDistance(Zone zone1, int p1, Zone zone0, int p0) + { + return project(zone1.getCurrentX()[p1] - zone0.getCurrentX()[p0], + zone1.getCurrentY()[p1] - zone0.getCurrentY()[p0]); + } + + /** + * Returns the original (unhinted) projected distance from point {@code p0} in {@code zone0} to + * point {@code p1} in {@code zone1}, measured along the dual projection vector. + */ + public int dualProjectedDistance(Zone zone1, int p1, Zone zone0, int p0) + { + return dualProject(zone1.getOriginalX()[p1] - zone0.getOriginalX()[p0], + zone1.getOriginalY()[p1] - zone0.getOriginalY()[p0]); + } + + /** + * Moves a point by the given projected distance along the freedom vector, touching the axes the + * freedom vector acts on. The displacement is {@code distance * freedom / (freedom . projection)}, + * which reduces to {@code distance} when both vectors are the same axis. + * + * @param zone the zone holding the point + * @param point the point index + * @param distance the projected distance to move, in F26Dot6 + */ + public void movePoint(Zone zone, int point, int distance) + { + UnitVector fv = graphicsState.getFreedomVector(); + UnitVector pv = graphicsState.getProjectionVector(); + int fDotP = dot14(fv.getX(), fv.getY(), pv.getX(), pv.getY()); + if (fDotP == 0) + { + fDotP = Fixed.ONE_F2DOT14; + } + if (fv.getX() != 0) + { + // backward-compatibility (v40 grayscale): never grid-fit in the x direction, so horizontal + // stems keep their natural sub-pixel position and are not darkened by antialiasing + if (!backwardCompatibility) + { + zone.getCurrentX()[point] += Fixed.mulDiv(distance, fv.getX(), fDotP); + } + zone.getTouchedX()[point] = true; + } + if (fv.getY() != 0) + { + // y moves are allowed until IUP has run on both axes; afterwards the glyph is frozen + if (!(backwardCompatibility && iupxCalled && iupyCalled)) + { + zone.getCurrentY()[point] += Fixed.mulDiv(distance, fv.getY(), fDotP); + } + zone.getTouchedY()[point] = true; + } + } + + /** @return whether v40 backward-compatibility (grayscale subpixel) movement rules are active */ + public boolean isBackwardCompatibility() + { + return backwardCompatibility; + } + + /** @param value whether to apply v40 backward-compatibility movement rules (glyph program only) */ + public void setBackwardCompatibility(boolean value) + { + this.backwardCompatibility = value; + } + + /** Marks IUP[x] as having run; resets each program run. */ + public void setIupxCalled() + { + this.iupxCalled = true; + } + + /** Marks IUP[y] as having run; resets each program run. */ + public void setIupyCalled() + { + this.iupyCalled = true; + } + + /** @return whether IUP has run on both axes (the glyph is frozen for backward compatibility) */ + public boolean isIupDone() + { + return iupxCalled && iupyCalled; + } + + /** @return whether the running program belongs to a composite glyph */ + public boolean isComposite() + { + return composite; + } + + /** @param value whether the running program belongs to a composite glyph */ + public void setComposite(boolean value) + { + this.composite = value; + } + + // --- execution cursor and call state --------------------------------- + + /** @return the current bytecode stream */ + public BytecodeStream getStream() + { + return stream; + } + + /** @param stream the current bytecode stream */ + public void setStream(BytecodeStream stream) + { + this.stream = stream; + } + + /** @return the current call nesting depth */ + public int getCallDepth() + { + return callDepth; + } + + /** Increments the call nesting depth. */ + public void enterCall() + { + callDepth++; + } + + /** Decrements the call nesting depth. */ + public void leaveCall() + { + callDepth--; + } + + /** @return true if an {@code ENDF} asked the current function body to return */ + public boolean isReturnFromFunction() + { + return returnFromFunction; + } + + /** @param value whether the current function body should return */ + public void setReturnFromFunction(boolean value) + { + this.returnFromFunction = value; + } +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/ExecutionTracer.java b/fontbox/src/main/java/org/apache/fontbox/ttf/ExecutionTracer.java new file mode 100644 index 00000000000..fc1b0c80cc0 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/ExecutionTracer.java @@ -0,0 +1,269 @@ +/* + * 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.fontbox.ttf; + +import java.io.PrintStream; + +/** + * A first-class, toggleable execution tracer for the interpreter. When attached to a + * {@link TrueTypeInterpreter} it emits one line per executed instruction, just before the instruction + * runs, in a format deliberately close to FreeType's {@code FT2_DEBUG=ttinterp} output: + * + *
+ * <pc> <MNEMONIC> # <top of stack, deepest..top> + *+ * + * The program counter and the operand stack are what matter for diffing: feeding the same glyph + * through this tracer and through FreeType and aligning the two traces by instruction index pinpoints + * the first instruction where control flow or an operand value diverges. See + * {@code src/test/resources/ttf/hinting/trace_diff.py} for the diff tool. + * + * @author Apache PDFBox + */ +class ExecutionTracer +{ + private static final String[] MNEMONICS = buildMnemonics(); + + /** Number of top-of-stack operands to print, matching FreeType's trace window. */ + private static final int STACK_WINDOW = 8; + + private final PrintStream out; + private final int tracePoint; + + /** + * @param out where to write trace lines + */ + public ExecutionTracer(PrintStream out) + { + this(out, -1); + } + + /** + * @param out where to write trace lines + * @param tracePoint a glyph-zone point index whose current coordinate is appended to each line + * (for localizing silent point-position divergence), or -1 to omit + */ + public ExecutionTracer(PrintStream out, int tracePoint) + { + this.out = out; + this.tracePoint = tracePoint; + } + + /** + * @param opcode an opcode value 0-255 + * @return the mnemonic for that opcode + */ + public static String mnemonic(int opcode) + { + return MNEMONICS[opcode & 0xFF]; + } + + /** + * Emits a trace line for the instruction about to execute. + * + * @param pc the program-counter position of the instruction + * @param opcode the opcode about to run + * @param ctx the execution context (its stack is sampled) + */ + void trace(int pc, int opcode, ExecutionContext ctx) + { + StringBuilder sb = new StringBuilder(); + sb.append(String.format("%06d %-11s", pc, MNEMONICS[opcode & 0xFF])); + if (tracePoint >= 0) + { + Zone zone = ctx.getGlyphZone(); + if (zone != null && tracePoint < zone.getPointCount()) + { + sb.append(String.format(" P%d=(%d,%d)", tracePoint, + zone.getCurrentX()[tracePoint], zone.getCurrentY()[tracePoint])); + } + } + sb.append(" #"); + // top of stack first, matching FreeType's ttinterp window + int window = Math.min(STACK_WINDOW, ctx.getStackDepth()); + for (int k = 0; k < window; k++) + { + sb.append(' ').append(ctx.peek(k)); + } + out.println(sb); + } + + private static String[] buildMnemonics() + { + String[] m = new String[256]; + for (int i = 0; i < 256; i++) + { + m[i] = String.format("INS_%02X", i); + } + // axis-variant vector setters: [y] for the even (0) code, [x] for the odd (1) code + put(m, 0x00, "SVTCA[y]"); + put(m, 0x01, "SVTCA[x]"); + put(m, 0x02, "SPVTCA[y]"); + put(m, 0x03, "SPVTCA[x]"); + put(m, 0x04, "SFVTCA[y]"); + put(m, 0x05, "SFVTCA[x]"); + put(m, 0x06, "SPVTL[||]"); + put(m, 0x07, "SPVTL[+]"); + put(m, 0x08, "SFVTL[||]"); + put(m, 0x09, "SFVTL[+]"); + put(m, 0x0A, "SPVFS"); + put(m, 0x0B, "SFVFS"); + put(m, 0x0C, "GPV"); + put(m, 0x0D, "GFV"); + put(m, 0x0E, "SFVTPV"); + put(m, 0x0F, "ISECT"); + put(m, 0x10, "SRP0"); + put(m, 0x11, "SRP1"); + put(m, 0x12, "SRP2"); + put(m, 0x13, "SZP0"); + put(m, 0x14, "SZP1"); + put(m, 0x15, "SZP2"); + put(m, 0x16, "SZPS"); + put(m, 0x17, "SLOOP"); + put(m, 0x18, "RTG"); + put(m, 0x19, "RTHG"); + put(m, 0x1A, "SMD"); + put(m, 0x1B, "ELSE"); + put(m, 0x1C, "JMPR"); + put(m, 0x1D, "SCVTCI"); + put(m, 0x1E, "SSWCI"); + put(m, 0x1F, "SSW"); + put(m, 0x20, "DUP"); + put(m, 0x21, "POP"); + put(m, 0x22, "CLEAR"); + put(m, 0x23, "SWAP"); + put(m, 0x24, "DEPTH"); + put(m, 0x25, "CINDEX"); + put(m, 0x26, "MINDEX"); + put(m, 0x27, "ALIGNPTS"); + put(m, 0x29, "UTP"); + put(m, 0x2A, "LOOPCALL"); + put(m, 0x2B, "CALL"); + put(m, 0x2C, "FDEF"); + put(m, 0x2D, "ENDF"); + put(m, 0x2E, "MDAP[nr]"); + put(m, 0x2F, "MDAP[rnd]"); + put(m, 0x30, "IUP[y]"); + put(m, 0x31, "IUP[x]"); + put(m, 0x32, "SHP[rp2]"); + put(m, 0x33, "SHP[rp1]"); + put(m, 0x34, "SHC[rp2]"); + put(m, 0x35, "SHC[rp1]"); + put(m, 0x36, "SHZ[rp2]"); + put(m, 0x37, "SHZ[rp1]"); + put(m, 0x38, "SHPIX"); + put(m, 0x39, "IP"); + put(m, 0x3A, "MSIRP[nr]"); + put(m, 0x3B, "MSIRP[rp0]"); + put(m, 0x3C, "ALIGNRP"); + put(m, 0x3D, "RTDG"); + put(m, 0x3E, "MIAP[nr]"); + put(m, 0x3F, "MIAP[rnd]"); + put(m, 0x40, "NPUSHB"); + put(m, 0x41, "NPUSHW"); + put(m, 0x42, "WS"); + put(m, 0x43, "RS"); + put(m, 0x44, "WCVTP"); + put(m, 0x45, "RCVT"); + put(m, 0x46, "GC[cur]"); + put(m, 0x47, "GC[org]"); + put(m, 0x48, "SCFS"); + put(m, 0x49, "MD[grid]"); + put(m, 0x4A, "MD[org]"); + put(m, 0x4B, "MPPEM"); + put(m, 0x4C, "MPS"); + put(m, 0x4D, "FLIPON"); + put(m, 0x4E, "FLIPOFF"); + put(m, 0x4F, "DEBUG"); + put(m, 0x50, "LT"); + put(m, 0x51, "LTEQ"); + put(m, 0x52, "GT"); + put(m, 0x53, "GTEQ"); + put(m, 0x54, "EQ"); + put(m, 0x55, "NEQ"); + put(m, 0x56, "ODD"); + put(m, 0x57, "EVEN"); + put(m, 0x58, "IF"); + put(m, 0x59, "EIF"); + put(m, 0x5A, "AND"); + put(m, 0x5B, "OR"); + put(m, 0x5C, "NOT"); + put(m, 0x5D, "DELTAP1"); + put(m, 0x5E, "SDB"); + put(m, 0x5F, "SDS"); + put(m, 0x60, "ADD"); + put(m, 0x61, "SUB"); + put(m, 0x62, "DIV"); + put(m, 0x63, "MUL"); + put(m, 0x64, "ABS"); + put(m, 0x65, "NEG"); + put(m, 0x66, "FLOOR"); + put(m, 0x67, "CEILING"); + put(m, 0x70, "WCVTF"); + put(m, 0x71, "DELTAP2"); + put(m, 0x72, "DELTAP3"); + put(m, 0x73, "DELTAC1"); + put(m, 0x74, "DELTAC2"); + put(m, 0x75, "DELTAC3"); + put(m, 0x76, "SROUND"); + put(m, 0x77, "S45ROUND"); + put(m, 0x78, "JROT"); + put(m, 0x79, "JROF"); + put(m, 0x7A, "ROFF"); + put(m, 0x7C, "RUTG"); + put(m, 0x7D, "RDTG"); + put(m, 0x7E, "SANGW"); + put(m, 0x7F, "AA"); + put(m, 0x80, "FLIPPT"); + put(m, 0x81, "FLIPRGON"); + put(m, 0x82, "FLIPRGOFF"); + put(m, 0x85, "SCANCTRL"); + put(m, 0x86, "SDPVTL[||]"); + put(m, 0x87, "SDPVTL[+]"); + put(m, 0x88, "GETINFO"); + put(m, 0x89, "IDEF"); + put(m, 0x8A, "ROLL"); + put(m, 0x8B, "MAX"); + put(m, 0x8C, "MIN"); + put(m, 0x8D, "SCANTYPE"); + put(m, 0x8E, "INSTCTRL"); + for (int k = 0; k < 4; k++) + { + put(m, 0x68 + k, "ROUND[" + k + "]"); + put(m, 0x6C + k, "NROUND[" + k + "]"); + } + for (int k = 0; k < 8; k++) + { + put(m, 0xB0 + k, "PUSHB[" + (k + 1) + "]"); + put(m, 0xB8 + k, "PUSHW[" + (k + 1) + "]"); + } + for (int op = 0xC0; op <= 0xDF; op++) + { + put(m, op, "MDRP[" + Integer.toHexString(op & 0x1F) + "]"); + } + for (int op = 0xE0; op <= 0xFF; op++) + { + put(m, op, "MIRP[" + Integer.toHexString(op & 0x1F) + "]"); + } + return m; + } + + private static void put(String[] m, int opcode, String name) + { + m[opcode] = name; + } +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/Fixed.java b/fontbox/src/main/java/org/apache/fontbox/ttf/Fixed.java new file mode 100644 index 00000000000..4f0d7623bd3 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/Fixed.java @@ -0,0 +1,210 @@ +/* + * 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.fontbox.ttf; + +/** + * Integer fixed-point math for the TrueType bytecode interpreter. + *
+ * The interpreter keeps coordinates in 26.6 fixed point (F26Dot6: 26 integer bits, 6 fractional bits, + * so 1 pixel == 64) and the projection/freedom vectors in 2.14 fixed point (F2Dot14). Mirroring + * FreeType's all-integer arithmetic - rather than mixing {@code float}/{@code double} - is what makes + * byte-exact comparison against a FreeType reference dump possible. + * + * @author Apache PDFBox + */ +final class Fixed +{ + /** One pixel in F26Dot6. */ + public static final int ONE = 64; + + /** Half a pixel in F26Dot6. */ + public static final int HALF = 32; + + /** 1.0 in F2Dot14. */ + public static final int ONE_F2DOT14 = 0x4000; + + private Fixed() + { + } + + /** + * Converts an integer to F26Dot6. + * + * @param value an integer pixel value + * @return the value in F26Dot6 + */ + public static int fromInt(int value) + { + return value << 6; + } + + /** + * Converts an F26Dot6 value back to an integer, rounding to nearest. + * + * @param value an F26Dot6 value + * @return the nearest integer + */ + public static int toInt(int value) + { + return (value + HALF) >> 6; + } + + /** + * Rounds an F26Dot6 value down to the pixel grid (towards negative infinity). + * + * @param value an F26Dot6 value + * @return the floored value, still in F26Dot6 + */ + public static int floor(int value) + { + return value & ~63; + } + + /** + * Rounds an F26Dot6 value up to the pixel grid. + * + * @param value an F26Dot6 value + * @return the ceiling value, still in F26Dot6 + */ + public static int ceil(int value) + { + return (value + 63) & ~63; + } + + /** + * Rounds an F26Dot6 value to the nearest pixel grid line (round-to-grid). + * + * @param value an F26Dot6 value + * @return the rounded value, still in F26Dot6 + */ + public static int round(int value) + { + return floor(value + HALF); + } + + /** + * Computes {@code round(a * b / c)} in 64-bit with correct sign handling, matching FreeType's + * {@code FT_MulDiv}. Used to build the F26Dot6 and F2Dot14 operators below. + * + * @param a first operand + * @param b second operand + * @param c divisor + * @return the rounded result + */ + public static int mulDiv(int a, int b, int c) + { + long la = a; + long lb = b; + long lc = c; + int sign = 1; + if (la < 0) + { + la = -la; + sign = -sign; + } + if (lb < 0) + { + lb = -lb; + sign = -sign; + } + if (lc < 0) + { + lc = -lc; + sign = -sign; + } + long result = lc != 0 ? (la * lb + lc / 2) / lc : 0x7FFFFFFFL; + return (int) (sign * result); + } + + /** + * Multiplies two F26Dot6 values, returning an F26Dot6 result (the TrueType {@code MUL} operator). + * + * @param a first F26Dot6 operand + * @param b second F26Dot6 operand + * @return {@code a * b} in F26Dot6 + */ + public static int mul(int a, int b) + { + return mulDiv(a, b, ONE); + } + + /** + * Divides two F26Dot6 values, returning an F26Dot6 result (the TrueType {@code DIV} operator). + * Division by zero yields zero. Unlike {@link #mul(int, int)} this truncates toward zero + * rather than rounding, matching FreeType's {@code DIV} opcode (which uses {@code FT_MulDiv_No_Round}). + * + * @param a F26Dot6 dividend + * @param b F26Dot6 divisor + * @return {@code a / b} in F26Dot6, or 0 if {@code b == 0} + */ + public static int div(int a, int b) + { + if (b == 0) + { + return 0; + } + long la = a; + long lb = b; + int sign = 1; + if (la < 0) + { + la = -la; + sign = -sign; + } + if (lb < 0) + { + lb = -lb; + sign = -sign; + } + return (int) (sign * (la * ONE / lb)); + } + + /** + * Multiplies an F26Dot6 value by an F2Dot14 value, returning F26Dot6. This is the building block + * for projecting a distance onto the projection/freedom vector. + * + * @param a an F26Dot6 value + * @param b an F2Dot14 value + * @return {@code a * b} in F26Dot6 + */ + public static int mul14(int a, int b) + { + return mulDiv(a, b, ONE_F2DOT14); + } + + /** + * Scales a coordinate from font units to F26Dot6 device pixels at the given ppem: + * {@code round(funits * ppem * 64 / unitsPerEm)}. Note this is not a flat {@code * 64} - + * that would only be correct when {@code unitsPerEm == ppem}. Both control values and glyph point + * coordinates are scaled with this. + * + * @param funits a value in font units + * @param ppem the active pixels-per-em + * @param unitsPerEm the font's unitsPerEm (from the head table) + * @return the value in F26Dot6 device pixels + */ + public static int scale(int funits, int ppem, int unitsPerEm) + { + if (unitsPerEm == 0) + { + return 0; + } + long numerator = (long) funits * ppem * ONE; + long rounded = numerator >= 0 ? numerator + unitsPerEm / 2 : numerator - unitsPerEm / 2; + return (int) (rounded / unitsPerEm); + } +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/FontProgramTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/FontProgramTable.java new file mode 100644 index 00000000000..2e3e3d7d349 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/FontProgramTable.java @@ -0,0 +1,64 @@ +/* + * 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.fontbox.ttf; + +import java.io.IOException; + +/** + * The 'fpgm' (Font Program) table. It holds TrueType hinting bytecode - typically a set of function + * definitions (FDEF) - that is executed once when the font is first used, before any glyph is hinted. + * The bytecode is stored here as raw bytes and run by the interpreter. + * + * @author Apache PDFBox + */ +public class FontProgramTable extends TTFTable +{ + /** + * A tag that identifies this table type. + */ + public static final String TAG = "fpgm"; + + private byte[] program; + + FontProgramTable() + { + } + + /** + * This will read the required data from the stream. + * + * @param ttf The font that is being read. + * @param data The stream to read the data from. + * @throws IOException If there is an error reading the data. + */ + @Override + void read(TrueTypeFont ttf, TTFDataStream data) throws IOException + { + program = data.read((int) getLength()); + initialized = true; + } + + /** + * Returns the raw font program bytecode. + * + * @return the bytecode of the font program + */ + public byte[] getProgram() + { + return program; + } +} \ No newline at end of file diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/FunctionDef.java b/fontbox/src/main/java/org/apache/fontbox/ttf/FunctionDef.java new file mode 100644 index 00000000000..11d44023fb9 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/FunctionDef.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.fontbox.ttf; + +/** + * A function defined by an {@code FDEF} instruction: the bytecode program it lives in (normally the + * {@code fpgm}) and the offset of its first body instruction, just past the {@code FDEF}. {@code CALL} + * and {@code LOOPCALL} run the body from this offset until the matching {@code ENDF}. + * + * @author Apache PDFBox + */ +class FunctionDef +{ + private final byte[] program; + private final int entryPoint; + + /** + * @param program the bytecode the function body lives in + * @param entryPoint the offset of the first instruction after {@code FDEF} + */ + public FunctionDef(byte[] program, int entryPoint) + { + this.program = program; + this.entryPoint = entryPoint; + } + + /** @return the bytecode the function body lives in */ + public byte[] getProgram() + { + return program; + } + + /** @return the offset of the first body instruction */ + public int getEntryPoint() + { + return entryPoint; + } +} diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/GaspTable.java b/fontbox/src/main/java/org/apache/fontbox/ttf/GaspTable.java new file mode 100644 index 00000000000..baf354e97b8 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/GaspTable.java @@ -0,0 +1,147 @@ +/* + * 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.fontbox.ttf; + +import java.io.IOException; + +/** + * The 'gasp' (Grid-fitting And Scan-conversion Procedure) table. It maps ppem ranges to flags that + * advise whether grid-fitting (hinting) and/or grayscale anti-aliasing should be applied at that size. + * The ranges are sorted by ascending maximum ppem; the last range always ends at 0xFFFF. + * + * @author Apache PDFBox + */ +public class GaspTable extends TTFTable +{ + /** + * A tag that identifies this table type. + */ + public static final String TAG = "gasp"; + + /** + * Use grid-fitting (i.e. execute the hinting bytecode) at this size. + */ + public static final int GASP_GRIDFIT = 0x0001; + + /** + * Use grayscale (anti-aliased) rendering at this size. + */ + public static final int GASP_DOGRAY = 0x0002; + + /** + * Use grid-fitting with ClearType symmetric smoothing (gasp version 1). + */ + public static final int GASP_SYMMETRIC_GRIDFIT = 0x0004; + + /** + * Use smoothing along multiple axes with ClearType (gasp version 1). + */ + public static final int GASP_SYMMETRIC_SMOOTHING = 0x0008; + + private int version; + private int[] rangeMaxPPEM; + private int[] rangeFlags; + + GaspTable() + { + } + + /** + * This will read the required data from the stream. + * + * @param ttf The font that is being read. + * @param data The stream to read the data from. + * @throws IOException If there is an error reading the data. + */ + @Override + void read(TrueTypeFont ttf, TTFDataStream data) throws IOException + { + version = data.readUnsignedShort(); + int numRanges = data.readUnsignedShort(); + rangeMaxPPEM = new int[numRanges]; + rangeFlags = new int[numRanges]; + for (int i = 0; i < numRanges; i++) + { + rangeMaxPPEM[i] = data.readUnsignedShort(); + rangeFlags[i] = data.readUnsignedShort(); + } + initialized = true; + } + + /** + * @return the table version (0 or 1) + */ + public int getVersion() + { + return version; + } + + /** + * Returns the upper ppem bound of each range, in ascending order. The last entry is 0xFFFF. + * + * @return the per-range maximum ppem values + */ + public int[] getRangeMaxPPEM() + { + return rangeMaxPPEM; + } + + /** + * Returns the flags for each range, parallel to {@link #getRangeMaxPPEM()}. + * + * @return the per-range flags + */ + public int[] getRangeFlags() + { + return rangeFlags; + } + + /** + * Returns the behavior flags that apply at the given ppem - those of the first range whose + * maximum ppem is greater than or equal to the requested ppem. + * + * @param ppem the pixels-per-em to look up + * @return the flags for that ppem, or 0 if the table has no ranges + */ + public int getFlags(int ppem) + { + if (rangeMaxPPEM == null) + { + return 0; + } + for (int i = 0; i < rangeMaxPPEM.length; i++) + { + if (ppem <= rangeMaxPPEM[i]) + { + return rangeFlags[i]; + } + } + // beyond the last range (should not happen as the last bound is 0xFFFF) + return rangeFlags.length > 0 ? rangeFlags[rangeFlags.length - 1] : 0; + } + + /** + * Convenience test for whether grid-fitting (hinting) is advised at the given ppem. + * + * @param ppem the pixels-per-em to look up + * @return true if {@link #GASP_GRIDFIT} is set for that ppem + */ + public boolean isGridFit(int ppem) + { + return (getFlags(ppem) & GASP_GRIDFIT) != 0; + } +} \ No newline at end of file diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphHinter.java b/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphHinter.java new file mode 100644 index 00000000000..82c8c6dc558 --- /dev/null +++ b/fontbox/src/main/java/org/apache/fontbox/ttf/GlyphHinter.java @@ -0,0 +1,518 @@ +/* + * 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.fontbox.ttf; + +import java.awt.geom.GeneralPath; +import java.io.IOException; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Applies TrueType bytecode hinting (grid-fitting) to a font's glyphs, producing grid-fitted paths. + *
+ * One hinter is created per {@link TrueTypeFont}. It lazily builds a {@link TrueTypeInterpreter} from + * the font's {@code maxp}/{@code head}/{@code cvt}/{@code fpgm}/{@code prep} tables, runs the font + * program once, and re-runs the control value program whenever the ppem changes. For each glyph it + * scales the outline into the pixel grid (appending the phantom points), runs the glyph's instructions + * and scales the grid-fitted result back into font units, so the rest of the rendering pipeline - which + * scales font units to device pixels at exactly this ppem - reproduces the grid-fitting. + *
+ * Hinting is best-effort: anything malformed, unsupported, or not applicable (a composite glyph, a + * glyph with no instructions, a ppem the {@code gasp} table excludes) falls back to {@code null}, and + * the caller renders the raw outline. One bad glyph never disables hinting for the rest of the font. + * Whether to hint at all is the caller's decision; this class always grid-fits when asked. + *
+ * The interpreter carries a great deal of mutable state - the storage area, the twilight zone, the
+ * post-{@code prep} template, the active ppem - so every entry point here is {@code synchronized} and
+ * one font hints one glyph at a time. That is correct but it does serialize: a font substituted from
+ * the system is held in a process-wide cache, so several rendering threads can share one instance and
+ * queue on this monitor. Embedded fonts are per-document and unaffected. If it ever measures as a
+ * bottleneck the answer is a per-thread or pooled interpreter, not a weaker lock; until then the simple
+ * thing is the right thing. {@code HintingConcurrencyTest} pins the current behavior.
+ *
+ * @author Apache PDFBox
+ */
+class GlyphHinter
+{
+ private static final Logger LOG = LogManager.getLogger(GlyphHinter.class);
+
+ private final TrueTypeFont font;
+
+ private boolean initialized;
+ private boolean available;
+ private boolean warned;
+ private TrueTypeInterpreter interpreter;
+ private GaspTable gasp;
+ private int unitsPerEm;
+ private int currentPpem = -1;
+
+ GlyphHinter(TrueTypeFont font)
+ {
+ this.font = font;
+ }
+
+ private synchronized void initialize() throws IOException
+ {
+ if (initialized)
+ {
+ return;
+ }
+ initialized = true;
+ available = false;
+
+ MaximumProfileTable maxp = font.getMaximumProfile();
+ FontProgramTable fpgm = font.getFontProgram();
+ ControlValueProgramTable prep = font.getControlValueProgram();
+ ControlValueTable cvt = font.getControlValues();
+
+ // hinting is only meaningful if the font carries a bytecode program
+ if (maxp == null || (fpgm == null && prep == null))
+ {
+ return;
+ }
+
+ unitsPerEm = font.getUnitsPerEm();
+ gasp = font.getGasp();
+
+ interpreter = new TrueTypeInterpreter(maxp.getMaxStackElements(), maxp.getMaxStorage(),
+ maxp.getMaxTwilightPoints(), unitsPerEm);
+ interpreter.setFontProgram(fpgm != null ? fpgm.getProgram() : null);
+ interpreter.setControlValueProgram(prep != null ? prep.getProgram() : null);
+ interpreter.setControlValues(cvt != null ? cvt.getValues() : null);
+ interpreter.prepareFontProgram();
+ available = true;
+ }
+
+ /**
+ * Returns the grid-fitted path of the glyph at the given ppem, or {@code null} if hinting does not
+ * apply and the caller should render the raw outline.
+ *
+ * @param gid the glyph id
+ * @param ppem the pixels-per-em to grid-fit to
+ * @return the hinted path in font units, or null
+ */
+ synchronized GeneralPath getPath(int gid, int ppem)
+ {
+ Hinted hinted = hint(gid, ppem);
+ if (hinted == null)
+ {
+ return null;
+ }
+ // scale the grid-fitted coordinates back into font units (drop the phantom points)
+ int[] hintedX = new int[hinted.pointCount];
+ int[] hintedY = new int[hinted.pointCount];
+ int[] curX = hinted.zone.getCurrentX();
+ int[] curY = hinted.zone.getCurrentY();
+ for (int i = 0; i < hinted.pointCount; i++)
+ {
+ hintedX[i] = toFontUnits(curX[i], ppem);
+ hintedY[i] = toFontUnits(curY[i], ppem);
+ }
+ return new GlyphRenderer(hinted.gd, hintedX, hintedY).getPath();
+ }
+
+ /**
+ * Returns the grid-fitted glyph points in F26Dot6 device coordinates (the raw interpreter output,
+ * before scaling back to font units, and excluding the phantom points), or {@code null} if hinting
+ * does not apply. This is the form compared against a FreeType reference dump by the golden tests.
+ *
+ * @param gid the glyph id
+ * @param ppem the pixels-per-em to grid-fit to
+ * @return a {@code {x[], y[]}} pair in F26Dot6, or null
+ */
+ synchronized int[][] getHintedPointsF26Dot6(int gid, int ppem)
+ {
+ Hinted hinted = hint(gid, ppem);
+ if (hinted == null)
+ {
+ return null;
+ }
+ int[] x = new int[hinted.pointCount];
+ int[] y = new int[hinted.pointCount];
+ System.arraycopy(hinted.zone.getCurrentX(), 0, x, 0, hinted.pointCount);
+ System.arraycopy(hinted.zone.getCurrentY(), 0, y, 0, hinted.pointCount);
+ return new int[][] { x, y };
+ }
+
+ /**
+ * Runs the control value program untraced, then grid-fits the glyph with an execution tracer
+ * attached, so the per-instruction trace can be diffed against FreeType's {@code ttinterp} trace.
+ * For development/debugging only.
+ *
+ * @param gid the glyph id
+ * @param ppem the pixels-per-em
+ * @param out where to write the trace
+ * @param tracePoint a glyph point index to log per instruction, or -1
+ * @throws IOException if the font could not be read
+ */
+ synchronized void traceGlyph(int gid, int ppem, java.io.PrintStream out, int tracePoint)
+ throws IOException
+ {
+ initialize();
+ if (!available)
+ {
+ return;
+ }
+ setActivePpem(ppem);
+ interpreter.setTracer(new ExecutionTracer(out, tracePoint));
+ try
+ {
+ hint(gid, ppem, 0);
+ }
+ finally
+ {
+ interpreter.setTracer(null);
+ }
+ }
+
+ /** Maximum composite nesting depth, to bound recursion on pathological fonts. */
+ private static final int MAX_COMPONENT_DEPTH = 8;
+
+ /** Runs all gating, then grid-fits the glyph, returning the executed zone or null on fallback. */
+ private Hinted hint(int gid, int ppem)
+ {
+ if (ppem <= 0)
+ {
+ return null;
+ }
+ try
+ {
+ initialize();
+ if (!available)
+ {
+ return null;
+ }
+ // gasp gate: if a gasp table is present and does not request grid-fitting here, skip
+ if (gasp != null && !gasp.isGridFit(ppem))
+ {
+ return null;
+ }
+ setActivePpem(ppem);
+ return hint(gid, ppem, 0);
+ }
+ catch (IOException | RuntimeException e)
+ {
+ logFailure(gid, ppem, e);
+ return null;
+ }
+ }
+
+ /**
+ * Reports a glyph that could not be hinted. Only the first failure in a font is a warning carrying
+ * the stack trace; the rest go to debug. Hinting is attempted per {@code (glyph, ppem)} pair, so a
+ * font whose bytecode never runs - a malformed program, or one using something unimplemented - would
+ * otherwise emit thousands of identical stack traces for a page of CJK text.
+ */
+ private void logFailure(int gid, int ppem, Exception e)
+ {
+ if (warned)
+ {
+ LOG.debug("hinting failed for glyph {} at {}ppem, using raw outline", gid, ppem, e);
+ return;
+ }
+ warned = true;
+ LOG.warn("hinting failed for glyph {} at {}ppem in font {}, using raw outline; further "
+ + "failures in this font are logged at debug level", gid, ppem, fontName(), e);
+ }
+
+ /** The font's PostScript name for the warning above, best-effort - we are already handling a fault. */
+ private String fontName()
+ {
+ try
+ {
+ return font.getName();
+ }
+ catch (IOException e)
+ {
+ return "
+ * It carries the execution engine - the {@link BytecodeStream} cursor driven dispatch loop, function
+ * definition and calling with a depth cap, branching, the push family, and the
+ * stack/arithmetic/logical/storage and point-moving opcodes - plus the size lifecycle: {@code fpgm}
+ * executed once, {@code prep} executed per ppem change with the result saved as the per-glyph
+ * template. An opcode with no handler and no {@code IDEF} binding throws {@link HintingException},
+ * which the caller catches per glyph and falls back to the raw outline.
+ *
+ * @author Apache PDFBox
+ */
+class TrueTypeInterpreter
+{
+ /** Maximum {@code CALL}/{@code LOOPCALL} nesting depth, matching FreeType. */
+ static final int MAX_CALL_DEPTH = 64;
+
+ /** A single opcode handler. */
+ @FunctionalInterface
+ private interface OpHandler
+ {
+ void execute(ExecutionContext ctx);
+ }
+
+ // opcodes referenced by the engine itself (control flow / push)
+ private static final int NPUSHB = 0x40;
+ private static final int NPUSHW = 0x41;
+ private static final int PUSHB_BASE = 0xB0;
+ private static final int PUSHW_BASE = 0xB8;
+ private static final int ELSE = 0x1B;
+ private static final int IF = 0x58;
+ private static final int EIF = 0x59;
+ private static final int FDEF = 0x2C;
+ private static final int ENDF = 0x2D;
+
+ private final OpHandler[] dispatch = new OpHandler[256];
+ private final Map
+ * Named after FreeType's {@code FT_UnitVector}, and like it the unit length is a convention rather than
+ * an enforced invariant: {@code SPVFS} and {@code SFVFS} write whatever the font pushed on the stack,
+ * which a malformed font need not have normalized.
+ *
+ * @author Apache PDFBox
+ */
+class UnitVector
+{
+ private int x;
+ private int y;
+
+ /**
+ * @param x the x component in F2Dot14
+ * @param y the y component in F2Dot14
+ */
+ public UnitVector(int x, int y)
+ {
+ this.x = x;
+ this.y = y;
+ }
+
+ /**
+ * @return the x axis unit vector (1, 0)
+ */
+ public static UnitVector xAxis()
+ {
+ return new UnitVector(Fixed.ONE_F2DOT14, 0);
+ }
+
+ /**
+ * @return the y axis unit vector (0, 1)
+ */
+ public static UnitVector yAxis()
+ {
+ return new UnitVector(0, Fixed.ONE_F2DOT14);
+ }
+
+ /**
+ * Builds a unit vector in F2Dot14 from a coordinate delta. A zero-length delta falls back to the
+ * x axis. (The square-root normalization is the one place the interpreter steps outside integer
+ * math; it only affects a direction vector, and is verified by the golden tests.)
+ *
+ * @param dx the x delta
+ * @param dy the y delta
+ * @return the normalized unit vector
+ */
+ public static UnitVector normalize(int dx, int dy)
+ {
+ double length = Math.hypot(dx, dy);
+ if (length == 0)
+ {
+ return xAxis();
+ }
+ int ux = (int) Math.round(dx / length * Fixed.ONE_F2DOT14);
+ int uy = (int) Math.round(dy / length * Fixed.ONE_F2DOT14);
+ return new UnitVector(ux, uy);
+ }
+
+ /**
+ * @return this vector rotated 90 degrees counter-clockwise, i.e. {@code (-y, x)}
+ */
+ public UnitVector perpendicular()
+ {
+ return new UnitVector(-y, x);
+ }
+
+ /**
+ * @return the x component in F2Dot14
+ */
+ public int getX()
+ {
+ return x;
+ }
+
+ /**
+ * @return the y component in F2Dot14
+ */
+ public int getY()
+ {
+ return y;
+ }
+
+ /**
+ * @param x the x component in F2Dot14
+ * @param y the y component in F2Dot14
+ */
+ public void set(int x, int y)
+ {
+ this.x = x;
+ this.y = y;
+ }
+
+ /**
+ * @return an independent copy of this vector
+ */
+ public UnitVector copy()
+ {
+ return new UnitVector(x, y);
+ }
+
+ @Override
+ public boolean equals(Object obj)
+ {
+ if (this == obj)
+ {
+ return true;
+ }
+ if (!(obj instanceof UnitVector))
+ {
+ return false;
+ }
+ UnitVector other = (UnitVector) obj;
+ return x == other.x && y == other.y;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return 31 * x + y;
+ }
+
+ @Override
+ public String toString()
+ {
+ return "UnitVector(" + x + ", " + y + ")";
+ }
+}
diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/Zone.java b/fontbox/src/main/java/org/apache/fontbox/ttf/Zone.java
new file mode 100644
index 00000000000..60d7aea9e21
--- /dev/null
+++ b/fontbox/src/main/java/org/apache/fontbox/ttf/Zone.java
@@ -0,0 +1,150 @@
+/*
+ * 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.fontbox.ttf;
+
+import java.util.Arrays;
+
+/**
+ * A set of points the interpreter can manipulate - either zone 0 (the twilight zone, holding phantom
+ * reference points) or zone 1 (the glyph's own outline points plus its appended phantom points). Each
+ * point has its scaled-but-unhinted "original" position and a "current" position the bytecode moves,
+ * both in F26Dot6, plus per-axis touch flags used by interpolation.
+ *
+ * @author Apache PDFBox
+ */
+class Zone
+{
+ private final int[] currentX;
+ private final int[] currentY;
+ private final int[] originalX;
+ private final int[] originalY;
+ private final int[] unscaledX;
+ private final int[] unscaledY;
+ private final boolean[] touchedX;
+ private final boolean[] touchedY;
+ private final boolean[] onCurve;
+ private final int[] contourEnds;
+
+ /**
+ * Allocates a zone of the given size.
+ *
+ * @param pointCount number of points (including any phantom points)
+ * @param contourCount number of contours (0 for the twilight zone)
+ */
+ public Zone(int pointCount, int contourCount)
+ {
+ currentX = new int[pointCount];
+ currentY = new int[pointCount];
+ originalX = new int[pointCount];
+ originalY = new int[pointCount];
+ unscaledX = new int[pointCount];
+ unscaledY = new int[pointCount];
+ touchedX = new boolean[pointCount];
+ touchedY = new boolean[pointCount];
+ onCurve = new boolean[pointCount];
+ contourEnds = new int[contourCount];
+ }
+
+ /**
+ * Zeroes every coordinate and flag. The twilight zone outlives a single program run - it is owned by
+ * the interpreter so values {@code prep} puts there survive into each glyph program - so it is reset
+ * rather than reallocated when the size changes, as FreeType does in {@code tt_size_run_prep}.
+ */
+ public void reset()
+ {
+ Arrays.fill(currentX, 0);
+ Arrays.fill(currentY, 0);
+ Arrays.fill(originalX, 0);
+ Arrays.fill(originalY, 0);
+ Arrays.fill(unscaledX, 0);
+ Arrays.fill(unscaledY, 0);
+ Arrays.fill(touchedX, false);
+ Arrays.fill(touchedY, false);
+ Arrays.fill(onCurve, false);
+ Arrays.fill(contourEnds, 0);
+ }
+
+ /** @return the number of points in this zone */
+ public int getPointCount()
+ {
+ return currentX.length;
+ }
+
+ /** @return the current (hinted) x coordinates in F26Dot6 */
+ public int[] getCurrentX()
+ {
+ return currentX;
+ }
+
+ /** @return the current (hinted) y coordinates in F26Dot6 */
+ public int[] getCurrentY()
+ {
+ return currentY;
+ }
+
+ /** @return the original (scaled, unhinted) x coordinates in F26Dot6 */
+ public int[] getOriginalX()
+ {
+ return originalX;
+ }
+
+ /** @return the original (scaled, unhinted) y coordinates in F26Dot6 */
+ public int[] getOriginalY()
+ {
+ return originalY;
+ }
+
+ /**
+ * @return the original unscaled x coordinates in font units. Interpolation and relative
+ * measurements use these for the ratio, matching FreeType's {@code orus}, because the unrounded
+ * font-unit values preserve precision the scaled F26Dot6 originals would lose.
+ */
+ public int[] getUnscaledX()
+ {
+ return unscaledX;
+ }
+
+ /** @return the original unscaled y coordinates in font units */
+ public int[] getUnscaledY()
+ {
+ return unscaledY;
+ }
+
+ /** @return per-point touch flags for the x axis */
+ public boolean[] getTouchedX()
+ {
+ return touchedX;
+ }
+
+ /** @return per-point touch flags for the y axis */
+ public boolean[] getTouchedY()
+ {
+ return touchedY;
+ }
+
+ /** @return per-point on-curve flags */
+ public boolean[] getOnCurve()
+ {
+ return onCurve;
+ }
+
+ /** @return the index of the last point of each contour */
+ public int[] getContourEnds()
+ {
+ return contourEnds;
+ }
+}
diff --git a/fontbox/src/test/java/org/apache/fontbox/ttf/BytecodeStreamTest.java b/fontbox/src/test/java/org/apache/fontbox/ttf/BytecodeStreamTest.java
new file mode 100644
index 00000000000..968463a9977
--- /dev/null
+++ b/fontbox/src/test/java/org/apache/fontbox/ttf/BytecodeStreamTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.fontbox.ttf;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for the {@link BytecodeStream} cursor, including bounds checking.
+ */
+class BytecodeStreamTest
+{
+ @Test
+ void testSequentialReads()
+ {
+ BytecodeStream s = new BytecodeStream(new byte[] { (byte) 0xB0, 0x05, (byte) 0xFF, 0x01 });
+ assertTrue(s.hasNext());
+ assertEquals(0xB0, s.nextByte());
+ assertEquals(5, s.nextByte());
+ // 0xFF01 as a signed word is negative
+ assertEquals((short) 0xFF01, s.nextWord());
+ assertFalse(s.hasNext());
+ }
+
+ @Test
+ void testByteReadPastEndThrows()
+ {
+ BytecodeStream s = new BytecodeStream(new byte[] { 0x01 });
+ assertEquals(1, s.nextByte());
+ assertThrows(HintingException.class, s::nextByte);
+ }
+
+ @Test
+ void testWordReadPastEndThrows()
+ {
+ // only one byte, but a word needs two
+ BytecodeStream s = new BytecodeStream(new byte[] { 0x01 });
+ assertThrows(HintingException.class, s::nextWord);
+ }
+
+ @Test
+ void testSeekOutOfRangeThrows()
+ {
+ BytecodeStream s = new BytecodeStream(new byte[] { 0x01, 0x02 });
+ s.seek(2); // end position is valid
+ assertFalse(s.hasNext());
+ assertThrows(HintingException.class, () -> s.seek(3));
+ assertThrows(HintingException.class, () -> s.seek(-1));
+ }
+
+ @Test
+ void testInstructionStartTracking()
+ {
+ BytecodeStream s = new BytecodeStream(new byte[] { 0x10, 0x11, 0x12 });
+ s.nextByte();
+ s.markInstructionStart();
+ assertEquals(1, s.instructionStart());
+ assertEquals(0x11, s.nextByte());
+ }
+}
diff --git a/fontbox/src/test/java/org/apache/fontbox/ttf/FixedTest.java b/fontbox/src/test/java/org/apache/fontbox/ttf/FixedTest.java
new file mode 100644
index 00000000000..f42b533b3bc
--- /dev/null
+++ b/fontbox/src/test/java/org/apache/fontbox/ttf/FixedTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.fontbox.ttf;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for the {@link Fixed} fixed-point math used by the interpreter.
+ */
+class FixedTest
+{
+ @Test
+ void testIntRoundTrip()
+ {
+ for (int n = -1000; n <= 1000; n++)
+ {
+ assertEquals(n, Fixed.toInt(Fixed.fromInt(n)), "round-trip " + n);
+ }
+ assertEquals(64, Fixed.fromInt(1));
+ assertEquals(-128, Fixed.fromInt(-2));
+ }
+
+ @Test
+ void testFloorCeilRound()
+ {
+ assertEquals(64, Fixed.floor(100)); // 1.5625px -> 1px
+ assertEquals(128, Fixed.ceil(100)); // -> 2px
+ assertEquals(64, Fixed.round(70)); // just above 1px rounds to 1px
+ assertEquals(128, Fixed.round(96)); // 1.5px rounds up to 2px
+ assertEquals(0, Fixed.round(31)); // just below half a pixel rounds to 0
+ assertEquals(64, Fixed.round(32)); // exactly half rounds up
+ }
+
+ @Test
+ void testMulDiv()
+ {
+ // 2.0 * 3.0 == 6.0
+ assertEquals(Fixed.fromInt(6), Fixed.mul(Fixed.fromInt(2), Fixed.fromInt(3)));
+ // 6.0 / 2.0 == 3.0
+ assertEquals(Fixed.fromInt(3), Fixed.div(Fixed.fromInt(6), Fixed.fromInt(2)));
+ // division by zero is defined as zero
+ assertEquals(0, Fixed.div(Fixed.fromInt(5), 0));
+ // signed rounding
+ assertEquals(-Fixed.fromInt(6), Fixed.mul(Fixed.fromInt(-2), Fixed.fromInt(3)));
+ }
+
+ @Test
+ void testMul14()
+ {
+ // multiplying by the F2Dot14 unit (1.0) is the identity
+ assertEquals(Fixed.fromInt(5), Fixed.mul14(Fixed.fromInt(5), Fixed.ONE_F2DOT14));
+ // multiplying by 0.5 in F2Dot14 halves the value
+ assertEquals(Fixed.fromInt(5) / 2, Fixed.mul14(Fixed.fromInt(5), Fixed.ONE_F2DOT14 / 2));
+ }
+
+ @Test
+ void testScale()
+ {
+ // 1000 font units at 16 ppem with unitsPerEm 2048 == 500 subpixel units == 7.8125px
+ assertEquals(500, Fixed.scale(1000, 16, 2048));
+ // a flat *64 would be wrong: it must depend on unitsPerEm
+ assertEquals(Fixed.fromInt(16), Fixed.scale(2048, 16, 2048)); // one em == ppem pixels
+ assertEquals(0, Fixed.scale(1234, 16, 0)); // guard against zero unitsPerEm
+ }
+}
diff --git a/fontbox/src/test/java/org/apache/fontbox/ttf/GlyphTraceTool.java b/fontbox/src/test/java/org/apache/fontbox/ttf/GlyphTraceTool.java
new file mode 100644
index 00000000000..ac9f7ec947f
--- /dev/null
+++ b/fontbox/src/test/java/org/apache/fontbox/ttf/GlyphTraceTool.java
@@ -0,0 +1,72 @@
+/*
+ * 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.fontbox.ttf;
+
+import java.io.InputStream;
+import java.io.PrintStream;
+
+import org.apache.pdfbox.io.RandomAccessReadBuffer;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Developer tool (not a real test): dumps the interpreter's per-instruction execution trace for one
+ * glyph, to be diffed against a FreeType {@code ttinterp} trace by {@code trace_diff.py}. Skipped
+ * unless the {@code trace.gid} system property is set, e.g.
+ *
+ *
+ * The reference is generated with FreeType's grayscale target ({@code FT_LOAD_TARGET_NORMAL}),
+ * i.e. the v40 "minimal" subpixel interpreter with backward compatibility, because PDFBox always
+ * rasterizes antialiased (Java2D). That mode is the right target for appearance but is a pile of
+ * heuristics rather than a clean algorithm, so the assertions here are property based rather than
+ * byte-exact coordinate matching:
+ * This is a property of a well-behaved font rather than a universal law: a glyph program may
+ * legally write storage, and FreeType would carry that into the next glyph too. LiberationSans does
+ * not, so any difference here is a bug on our side.
+ */
+ @Test
+ void testHintingIsIndependentOfGlyphOrder() throws IOException
+ {
+ TrueTypeFont font = parse("/ttf/LiberationSans-Regular.ttf");
+ int h = gid(font, 'H');
+ int eacute = gid(font, 0x00E9);
+ assertTrue(font.getGlyph().getGlyph(eacute).getNumberOfContours() < 0,
+ "expected e-acute to be a composite glyph");
+
+ double[] before = flatten(font.getHintedPath(h, 16));
+ for (char c : "oxn8".toCharArray())
+ {
+ font.getHintedPath(gid(font, c), 16);
+ }
+ font.getHintedPath(eacute, 16);
+ double[] after = flatten(font.getHintedPath(h, 16));
+
+ assertTrue(Arrays.equals(before, after),
+ "hinting other glyphs must not change the result for 'H'");
+ }
+
+ /**
+ * Returning to a ppem must reproduce the earlier result. A ppem change re-runs the control value
+ * program, which clears the storage area and twilight zone, so this covers the round trip out of a
+ * size and back into it.
+ */
+ @Test
+ void testHintingIsIndependentOfPpemOrder() throws IOException
+ {
+ TrueTypeFont font = parse("/ttf/LiberationSans-Regular.ttf");
+ int h = gid(font, 'H');
+
+ double[] before = flatten(font.getHintedPath(h, 16));
+ font.getHintedPath(h, 11);
+ font.getHintedPath(h, 24);
+ double[] after = flatten(font.getHintedPath(h, 16));
+
+ assertTrue(Arrays.equals(before, after),
+ "hinting at other ppems must not change the result at 16ppem");
+ }
+
+ @Test
+ void testFontWithoutBytecodeFallsBack() throws IOException
+ {
+ // Lohit-Bengali has a cvt but no fpgm and no prep: there is nothing to execute
+ TrueTypeFont font = parse("/ttf/Lohit-Bengali.ttf");
+ int gid = font.getUnicodeCmapLookup().getGlyphId(0x0985); // Bengali letter A
+ assertNull(font.getHintedPath(gid, 16));
+ }
+
+ @Test
+ void testEmptyGlyphFallsBack() throws IOException
+ {
+ TrueTypeFont font = parse("/ttf/LiberationSans-Regular.ttf");
+ // the space glyph has no contours; hinting must fall back rather than fail
+ assertNull(font.getHintedPath(gid(font, ' '), 16));
+ }
+
+ @Test
+ void testCommonGlyphsHintWithoutFallingBack() throws IOException
+ {
+ // a real-font smoke test: every one of these simple glyphs carries instructions and must
+ // grid-fit without throwing (which would silently fall back to null). This is what caught the
+ // MIRP stack-order bug.
+ TrueTypeFont font = parse("/ttf/LiberationSans-Regular.ttf");
+ String sample = "HILEToxn0123456789";
+ for (int ppem : new int[] { 11, 13, 16, 24 })
+ {
+ for (int i = 0; i < sample.length(); i++)
+ {
+ char c = sample.charAt(i);
+ assertNotNull(font.getHintedPath(gid(font, c), ppem),
+ "expected '" + c + "' to hint at " + ppem + "ppem");
+ }
+ }
+ }
+}
diff --git a/fontbox/src/test/java/org/apache/fontbox/ttf/HintingTablesTest.java b/fontbox/src/test/java/org/apache/fontbox/ttf/HintingTablesTest.java
new file mode 100644
index 00000000000..c3cad84ceea
--- /dev/null
+++ b/fontbox/src/test/java/org/apache/fontbox/ttf/HintingTablesTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.fontbox.ttf;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.pdfbox.io.RandomAccessReadBuffer;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests parsing of the TrueType hinting tables ('cvt ', 'fpgm', 'prep', 'gasp').
+ *
+ * LiberationSans-Regular was built as a hand-hinted, metric-compatible Arial replacement, so it
+ * carries a full bytecode hinting program and all four tables. The expected values below were taken
+ * from its on-disk table directory.
+ */
+class HintingTablesTest
+{
+ private static TrueTypeFont parse(String resource) throws IOException
+ {
+ try (InputStream is = HintingTablesTest.class.getResourceAsStream(resource))
+ {
+ assertNotNull(is, "missing test resource " + resource);
+ return new TTFParser().parse(new RandomAccessReadBuffer(is));
+ }
+ }
+
+ @Test
+ void testControlValueTable() throws IOException
+ {
+ TrueTypeFont font = parse("/ttf/LiberationSans-Regular.ttf");
+ ControlValueTable cvt = font.getControlValues();
+ assertNotNull(cvt);
+ // 648-byte table / 2 bytes per FWord
+ assertEquals(324, cvt.getValueCount());
+ assertEquals(324, cvt.getValues().length);
+ // CVT entries are signed FWords (raw font units, not yet scaled to ppem)
+ assertEquals(1484, cvt.getValues()[0]);
+ }
+
+ @Test
+ void testFontProgramTable() throws IOException
+ {
+ TrueTypeFont font = parse("/ttf/LiberationSans-Regular.ttf");
+ FontProgramTable fpgm = font.getFontProgram();
+ assertNotNull(fpgm);
+ assertEquals(1972, fpgm.getProgram().length);
+ }
+
+ @Test
+ void testControlValueProgramTable() throws IOException
+ {
+ TrueTypeFont font = parse("/ttf/LiberationSans-Regular.ttf");
+ ControlValueProgramTable prep = font.getControlValueProgram();
+ assertNotNull(prep);
+ assertEquals(835, prep.getProgram().length);
+ }
+
+ @Test
+ void testGaspTable() throws IOException
+ {
+ TrueTypeFont font = parse("/ttf/LiberationSans-Regular.ttf");
+ GaspTable gasp = font.getGasp();
+ assertNotNull(gasp);
+ assertEquals(0, gasp.getVersion());
+
+ // three ranges: (<=8: DOGRAY), (<=17: GRIDFIT), (<=65535: GRIDFIT|DOGRAY)
+ assertArrayEquals(new int[] { 8, 17, 65535 }, gasp.getRangeMaxPPEM());
+ assertArrayEquals(new int[] { GaspTable.GASP_DOGRAY, GaspTable.GASP_GRIDFIT,
+ GaspTable.GASP_GRIDFIT | GaspTable.GASP_DOGRAY }, gasp.getRangeFlags());
+
+ // ppem -> flags lookup, including range boundaries
+ assertEquals(GaspTable.GASP_DOGRAY, gasp.getFlags(8));
+ assertEquals(GaspTable.GASP_GRIDFIT, gasp.getFlags(9));
+ assertEquals(GaspTable.GASP_GRIDFIT, gasp.getFlags(17));
+ assertEquals(GaspTable.GASP_GRIDFIT | GaspTable.GASP_DOGRAY, gasp.getFlags(18));
+ assertEquals(GaspTable.GASP_GRIDFIT | GaspTable.GASP_DOGRAY, gasp.getFlags(2000));
+
+ // grid-fitting is off at 8 ppem and below, on above it
+ assertFalse(gasp.isGridFit(8));
+ assertTrue(gasp.isGridFit(9));
+ assertTrue(gasp.isGridFit(16));
+ }
+
+ @Test
+ void testAbsentTablesReturnNull() throws IOException
+ {
+ // None of the bundled fonts lacks all four tables, but the absent-table path is covered:
+ // JosefinSans-Italic has no 'cvt '/'fpgm' (but does have 'prep'/'gasp')...
+ TrueTypeFont josefin = parse("/ttf/JosefinSans-Italic.ttf");
+ assertNull(josefin.getControlValues());
+ assertNull(josefin.getFontProgram());
+ assertNotNull(josefin.getControlValueProgram());
+ assertNotNull(josefin.getGasp());
+
+ // ...and Lohit-Tamil has no 'gasp'. Absent accessors must return null, not throw.
+ TrueTypeFont lohit = parse("/ttf/Lohit-Tamil.ttf");
+ assertNull(lohit.getGasp());
+ assertNotNull(lohit.getControlValues());
+ assertNotNull(lohit.getFontProgram());
+ assertNotNull(lohit.getControlValueProgram());
+ }
+}
\ No newline at end of file
diff --git a/fontbox/src/test/java/org/apache/fontbox/ttf/PointOpsTest.java b/fontbox/src/test/java/org/apache/fontbox/ttf/PointOpsTest.java
new file mode 100644
index 00000000000..9c06b891b6b
--- /dev/null
+++ b/fontbox/src/test/java/org/apache/fontbox/ttf/PointOpsTest.java
@@ -0,0 +1,343 @@
+/*
+ * 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.fontbox.ttf;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Unit tests for the point-moving opcodes: each builds a glyph zone, runs a short program
+ * through the interpreter, and asserts the resulting coordinates. Coordinates are in F26Dot6 (64 per
+ * pixel). Byte-exact agreement with FreeType is proven by {@link GoldenHintingTest}; these check the
+ * per-opcode logic.
+ */
+class PointOpsTest
+{
+ private static TrueTypeInterpreter interpreter()
+ {
+ return new TrueTypeInterpreter(256, 16, 16, 2048);
+ }
+
+ /** Builds a single-contour glyph zone with the given x coordinates (original == current). */
+ private static Zone lineZone(int... xs)
+ {
+ Zone zone = new Zone(xs.length, 1);
+ for (int i = 0; i < xs.length; i++)
+ {
+ zone.getOriginalX()[i] = xs[i];
+ zone.getCurrentX()[i] = xs[i];
+ // these synthetic tests work directly in device units, so the unscaled originals (used by
+ // IP for its interpolation ratio) mirror the scaled ones
+ zone.getUnscaledX()[i] = xs[i];
+ }
+ zone.getContourEnds()[0] = xs.length - 1;
+ return zone;
+ }
+
+ private static ExecutionContext context(TrueTypeInterpreter interp, Zone glyph)
+ {
+ ExecutionContext ctx = interp.newContext(new GraphicsState());
+ ctx.setPpem(16);
+ ctx.setGlyphZone(glyph);
+ return ctx;
+ }
+
+ @Test
+ void testProjectionAndMove()
+ {
+ ExecutionContext ctx =
+ new ExecutionContext(null, new GraphicsState(), 16, new int[0], null, new Zone(0, 0));
+ Zone zone = lineZone(100, 0);
+ // default projection/freedom is the x axis: project returns the x coordinate
+ assertEquals(100, ctx.project(zone.getCurrentX()[0], zone.getCurrentY()[0]));
+ ctx.movePoint(zone, 0, 28); // move +28 along x
+ assertEquals(128, zone.getCurrentX()[0]);
+ assertTrue(zone.getTouchedX()[0]);
+ assertFalse(zone.getTouchedY()[0]);
+ }
+
+ @Test
+ void testMdapRoundsToGrid()
+ {
+ TrueTypeInterpreter interp = interpreter();
+ Zone zone = lineZone(100); // 1.5625px
+ ExecutionContext ctx = context(interp, zone);
+ // PUSHB[0] 0 ; MDAP[1] (round)
+ interp.run(ctx, new BytecodeStream(new byte[] { (byte) 0xB0, 0, 0x2F }));
+ assertEquals(128, zone.getCurrentX()[0]); // rounded to 2px
+ assertTrue(zone.getTouchedX()[0]);
+ }
+
+ @Test
+ void testMdrpRelativeToRp0()
+ {
+ TrueTypeInterpreter interp = interpreter();
+ Zone zone = lineZone(0, 100); // rp0 = point 0 at x=0, point 1 at 1.5625px
+ ExecutionContext ctx = context(interp, zone);
+ // PUSHB[0] 1 ; MDRP[round] (0xC4, round bit = 0x04) - grid-rounded distance from rp0
+ interp.run(ctx, new BytecodeStream(new byte[] { (byte) 0xB0, 1, (byte) 0xC4 }));
+ assertEquals(128, zone.getCurrentX()[1]); // distance 100 rounds to 128
+ }
+
+ @Test
+ void testMdrpRoundAndMinimumDistanceFlagsAreDistinct()
+ {
+ // guards the flag encoding: round is bit 0x04, minimum-distance is bit 0x08 (they were once
+ // swapped). A small original distance (30) below the minimum (64):
+ TrueTypeInterpreter interp = interpreter();
+ // round only (0xC4): 30 -> round(30) = 0, no minimum clamp
+ Zone roundZone = lineZone(0, 30);
+ interp.run(context(interp, roundZone), new BytecodeStream(new byte[] { (byte) 0xB0, 1, (byte) 0xC4 }));
+ assertEquals(0, roundZone.getCurrentX()[1]);
+
+ // minimum-distance only (0xC8): no rounding, but clamp the distance up to the minimum (64)
+ Zone minZone = lineZone(0, 30);
+ interp.run(context(interp, minZone), new BytecodeStream(new byte[] { (byte) 0xB0, 1, (byte) 0xC8 }));
+ assertEquals(64, minZone.getCurrentX()[1]);
+ }
+
+ @Test
+ void testMirpUsesControlValue()
+ {
+ TrueTypeInterpreter interp = interpreter();
+ // raw cvt 256 at 16ppem / 2048 upem scales to 128 (2px)
+ interp.setControlValues(new int[] { 256 });
+ interp.setPpem(16, 16);
+ Zone zone = lineZone(0, 100);
+ ExecutionContext ctx = interp.newContext(new GraphicsState());
+ ctx.setPpem(16);
+ ctx.setGlyphZone(zone);
+ // PUSHB[1] 1 0 (point=1 pushed first, cvtIndex=0 on top) ; MIRP[round] (0xE4, round = 0x04)
+ interp.run(ctx, new BytecodeStream(new byte[] { (byte) 0xB1, 1, 0, (byte) 0xE4 }));
+ assertEquals(128, zone.getCurrentX()[1]);
+ }
+
+ @Test
+ void testMsirpSetsExactDistance()
+ {
+ TrueTypeInterpreter interp = interpreter();
+ Zone zone = lineZone(0, 100);
+ ExecutionContext ctx = context(interp, zone);
+ // PUSHB[1] 1 64 (point=1, distance=1px) ; MSIRP[0] (0x3A)
+ interp.run(ctx, new BytecodeStream(new byte[] { (byte) 0xB1, 1, 64, 0x3A }));
+ assertEquals(64, zone.getCurrentX()[1]); // exactly 1px from rp0 at x=0
+ }
+
+ @Test
+ void testAlignRp()
+ {
+ TrueTypeInterpreter interp = interpreter();
+ Zone zone = lineZone(0, 100); // rp0 at 0
+ ExecutionContext ctx = context(interp, zone);
+ // PUSHB[0] 1 ; ALIGNRP (0x3C) - align point 1 onto rp0
+ interp.run(ctx, new BytecodeStream(new byte[] { (byte) 0xB0, 1, 0x3C }));
+ assertEquals(0, zone.getCurrentX()[1]);
+ }
+
+ @Test
+ void testIupInterpolatesUntouched()
+ {
+ TrueTypeInterpreter interp = interpreter();
+ // three points on one contour; the middle one is untouched
+ Zone zone = lineZone(0, 50, 100);
+ zone.getTouchedX()[0] = true;
+ zone.getTouchedX()[2] = true;
+ zone.getCurrentX()[2] = 120; // the right anchor was hinted +20
+ ExecutionContext ctx = context(interp, zone);
+ // IUP[1] (x axis)
+ interp.run(ctx, new BytecodeStream(new byte[] { 0x31 }));
+ // p1 interpolates proportionally: 0 + 50*(120-0)/100 = 60
+ assertEquals(60, zone.getCurrentX()[1]);
+ // touched anchors are never moved by IUP
+ assertEquals(0, zone.getCurrentX()[0]);
+ assertEquals(120, zone.getCurrentX()[2]);
+ }
+
+ @Test
+ void testIupShiftsWhenSingleTouchedPoint()
+ {
+ TrueTypeInterpreter interp = interpreter();
+ Zone zone = lineZone(0, 50, 100);
+ zone.getTouchedX()[1] = true;
+ zone.getCurrentX()[1] = 70; // the only touched point moved +20
+ ExecutionContext ctx = context(interp, zone);
+ interp.run(ctx, new BytecodeStream(new byte[] { 0x31 })); // IUP[1]
+ // with one touched point, every other point shifts by the same delta (+20)
+ assertEquals(20, zone.getCurrentX()[0]);
+ assertEquals(120, zone.getCurrentX()[2]);
+ }
+
+ @Test
+ void testIpInterpolatesBetweenReferencePoints()
+ {
+ TrueTypeInterpreter interp = interpreter();
+ Zone zone = lineZone(0, 50, 100);
+ // set rp1=0, rp2=2, move the anchors, then IP point 1
+ zone.getCurrentX()[2] = 120;
+ GraphicsState gs = new GraphicsState();
+ gs.setRp1(0);
+ gs.setRp2(2);
+ ExecutionContext ctx = interp.newContext(gs);
+ ctx.setPpem(16);
+ ctx.setGlyphZone(zone);
+ // PUSHB[0] 1 ; IP (0x39)
+ interp.run(ctx, new BytecodeStream(new byte[] { (byte) 0xB0, 1, 0x39 }));
+ assertEquals(60, zone.getCurrentX()[1]);
+ }
+
+ @Test
+ void testIpInterpolatesTwilightPointsUsingScaledOriginals()
+ {
+ // Regression (PDFBOX-3293): twilight-zone points have no font-unit source, so their unscaled
+ // coordinates are (0,0). Measuring IP's interpolation ratio against those zeros collapses every
+ // interpolated point onto the reference point. A prep program that builds the x-height control
+ // value this way then yields 0, flattening whole glyphs onto the baseline at small ppem (e.g.
+ // lowercase 'm' in a gasp-less Arial subset at 7ppem). For twilight points IP must measure the
+ // scaled originals instead, exactly as FreeType's Ins_IP does.
+ TrueTypeInterpreter interp = interpreter();
+ GraphicsState gs = new GraphicsState();
+ gs.setZp0(0); // all three zone pointers reference the twilight zone
+ gs.setZp1(0);
+ gs.setZp2(0);
+ gs.setRp1(0);
+ gs.setRp2(2);
+ ExecutionContext ctx = interp.newContext(gs);
+ ctx.setPpem(16);
+ Zone twilight = ctx.getTwilightZone();
+ // anchors at originals 0 and 100; p2's current is stretched to 120. The unscaled coordinates
+ // stay (0,0) for every point, exactly as MIAP leaves freshly placed twilight points.
+ int[] xs = { 0, 50, 100 };
+ for (int p = 0; p < xs.length; p++)
+ {
+ twilight.getOriginalX()[p] = xs[p];
+ twilight.getCurrentX()[p] = xs[p];
+ }
+ twilight.getCurrentX()[2] = 120;
+ // PUSHB[0] 1 ; IP (0x39)
+ interp.run(ctx, new BytecodeStream(new byte[] { (byte) 0xB0, 1, 0x39 }));
+ // interpolate by the scaled originals: 0 + 50*(120-0)/100 = 60 (the unscaled zeros would give 0)
+ assertEquals(60, twilight.getCurrentX()[1]);
+ }
+
+ @Test
+ void testSvtcaSetsProjectionVector()
+ {
+ TrueTypeInterpreter interp = interpreter();
+ ExecutionContext ctx = interp.newContext(new GraphicsState());
+ // SVTCA[0] (y axis) ; GPV
+ interp.run(ctx, new BytecodeStream(new byte[] { 0x00, 0x0C }));
+ assertEquals(Fixed.ONE_F2DOT14, ctx.peek(0)); // pv.y
+ assertEquals(0, ctx.peek(1)); // pv.x
+ }
+
+ @Test
+ void testRoundOpcode()
+ {
+ // PUSHB[0] 100 ; ROUND[0] (0x68) -> 128 under default round-to-grid
+ ExecutionContext ctx = interpreter().executeProgram(new byte[] { (byte) 0xB0, 100, 0x68 }, 16);
+ assertEquals(128, ctx.peek(0));
+ }
+
+ @Test
+ void testMdMeasuresSignedDistance()
+ {
+ // MD measures project(zp0[p1] - zp1[p2]); p1 is the deeper operand, p2 the top (FreeType sign).
+ TrueTypeInterpreter interp = interpreter();
+ Zone zone = lineZone(0, 100); // point 0 at x=0, point 1 at x=100
+ ExecutionContext ctx = context(interp, zone);
+ // PUSHB[1] 0 1 (p1=0 deeper, p2=1 top) ; MD[grid] (0x49)
+ interp.run(ctx, new BytecodeStream(new byte[] { (byte) 0xB1, 0, 1, 0x49 }));
+ assertEquals(-100, ctx.peek(0)); // x0 - x1 = -100
+ }
+
+ @Test
+ void testGcReadsProjectedCoordinate()
+ {
+ TrueTypeInterpreter interp = interpreter();
+ Zone zone = lineZone(192); // 3px
+ ExecutionContext ctx = context(interp, zone);
+ // PUSHB[0] 0 ; GC[0] (0x46) current coordinate
+ interp.run(ctx, new BytecodeStream(new byte[] { (byte) 0xB0, 0, 0x46 }));
+ assertEquals(192, ctx.peek(0));
+ }
+
+ @Test
+ void testIsectMovesPointToLineIntersection()
+ {
+ TrueTypeInterpreter interp = interpreter();
+ // p0 = the point to move; line A = horizontal y=64 (p1,p2); line B = vertical x=64 (p3,p4)
+ Zone zone = new Zone(5, 1);
+ setPoint(zone, 0, 0, 0);
+ setPoint(zone, 1, 0, 64);
+ setPoint(zone, 2, 128, 64);
+ setPoint(zone, 3, 64, 0);
+ setPoint(zone, 4, 64, 128);
+ ExecutionContext ctx = context(interp, zone);
+ // push p0,a0,a1,b0,b1 = 0 1 2 3 4 ; ISECT (0x0F) pops b1,b0,a1,a0,point
+ interp.run(ctx, new BytecodeStream(
+ new byte[] { (byte) 0xB4, 0, 1, 2, 3, 4, 0x0F }));
+ assertEquals(64, zone.getCurrentX()[0]);
+ assertEquals(64, zone.getCurrentY()[0]);
+ assertTrue(zone.getTouchedX()[0]);
+ assertTrue(zone.getTouchedY()[0]);
+ }
+
+ // --- oracle-free invariant checks ------------------------------------
+
+ @Test
+ void testIupNeverMovesATouchedPoint()
+ {
+ // invariant: IUP leaves every already-touched point exactly where it is
+ TrueTypeInterpreter interp = interpreter();
+ Zone zone = lineZone(0, 50, 100);
+ zone.getTouchedX()[1] = true;
+ zone.getCurrentX()[1] = 77; // a touched point at a deliberately off-grid position
+ ExecutionContext ctx = context(interp, zone);
+ interp.run(ctx, new BytecodeStream(new byte[] { 0x31 })); // IUP[x]
+ assertEquals(77, zone.getCurrentX()[1]);
+ }
+
+ @Test
+ void testTouchedPointsLandOnGridUnderRoundToGrid()
+ {
+ // invariant: under an integer round state, a rounded (touched) point lands on a grid line
+ TrueTypeInterpreter interp = interpreter();
+ Zone zone = lineZone(100, 150, 77); // off-grid positions
+ ExecutionContext ctx = context(interp, zone); // default round state is round-to-grid
+ // MDAP[1] each point (PUSHB[0] n ; MDAP[1])
+ interp.run(ctx, new BytecodeStream(new byte[] {
+ (byte) 0xB0, 0, 0x2F, (byte) 0xB0, 1, 0x2F, (byte) 0xB0, 2, 0x2F }));
+ for (int i = 0; i < 3; i++)
+ {
+ assertTrue(zone.getTouchedX()[i], "point " + i + " should be touched");
+ assertEquals(0, zone.getCurrentX()[i] % Fixed.ONE, "point " + i + " off the grid");
+ }
+ }
+
+ private static void setPoint(Zone zone, int i, int x, int y)
+ {
+ zone.getCurrentX()[i] = x;
+ zone.getCurrentY()[i] = y;
+ zone.getOriginalX()[i] = x;
+ zone.getOriginalY()[i] = y;
+ zone.getUnscaledX()[i] = x;
+ zone.getUnscaledY()[i] = y;
+ }
+}
diff --git a/fontbox/src/test/java/org/apache/fontbox/ttf/RoundStateTest.java b/fontbox/src/test/java/org/apache/fontbox/ttf/RoundStateTest.java
new file mode 100644
index 00000000000..00c7161e616
--- /dev/null
+++ b/fontbox/src/test/java/org/apache/fontbox/ttf/RoundStateTest.java
@@ -0,0 +1,85 @@
+/*
+ * 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.fontbox.ttf;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Table-driven tests of the {@link GraphicsState#round(int)} state machine across all round states.
+ */
+class RoundStateTest
+{
+ private static int round(int state, int distance)
+ {
+ GraphicsState gs = new GraphicsState();
+ gs.setRoundState(state);
+ return gs.round(distance);
+ }
+
+ @Test
+ void testRoundToGrid()
+ {
+ assertEquals(128, round(GraphicsState.ROUND_TO_GRID, 100)); // 1.56px -> 2px
+ assertEquals(64, round(GraphicsState.ROUND_TO_GRID, 95)); // 1.48px -> 1px
+ assertEquals(128, round(GraphicsState.ROUND_TO_GRID, 96)); // 1.5px rounds up
+ assertEquals(-128, round(GraphicsState.ROUND_TO_GRID, -100));
+ }
+
+ @Test
+ void testRoundDownAndUp()
+ {
+ assertEquals(64, round(GraphicsState.ROUND_DOWN_TO_GRID, 100));
+ assertEquals(64, round(GraphicsState.ROUND_DOWN_TO_GRID, 127));
+ assertEquals(128, round(GraphicsState.ROUND_UP_TO_GRID, 65));
+ assertEquals(64, round(GraphicsState.ROUND_UP_TO_GRID, 64));
+ }
+
+ @Test
+ void testRoundOff()
+ {
+ assertEquals(100, round(GraphicsState.ROUND_OFF, 100));
+ assertEquals(-37, round(GraphicsState.ROUND_OFF, -37));
+ }
+
+ @Test
+ void testRoundToDoubleGrid()
+ {
+ // double grid snaps to multiples of half a pixel (32)
+ assertEquals(96, round(GraphicsState.ROUND_TO_DOUBLE_GRID, 80)); // 1.25px -> 1.5px
+ assertEquals(64, round(GraphicsState.ROUND_TO_DOUBLE_GRID, 70)); // 1.09px -> 1.0px
+ }
+
+ @Test
+ void testRoundToHalfGrid()
+ {
+ // half grid snaps to (n + 0.5) pixels, i.e. 32, 96, 160, ...
+ assertEquals(96, round(GraphicsState.ROUND_TO_HALF_GRID, 100)); // -> 1.5px
+ assertEquals(32, round(GraphicsState.ROUND_TO_HALF_GRID, 50)); // -> 0.5px
+ }
+
+ @Test
+ void testSuperRoundReducesToGrid()
+ {
+ // SROUND with period=1px, phase=0, threshold=half is equivalent to round-to-grid
+ GraphicsState gs = new GraphicsState();
+ gs.setSuperRound(Fixed.ONE, 0x48); // 01 period=1.0, 00 phase=0, 1000 threshold=4*p/8=half
+ assertEquals(128, gs.round(100));
+ assertEquals(64, gs.round(95));
+ }
+}
diff --git a/fontbox/src/test/java/org/apache/fontbox/ttf/TrueTypeInterpreterTest.java b/fontbox/src/test/java/org/apache/fontbox/ttf/TrueTypeInterpreterTest.java
new file mode 100644
index 00000000000..d1cbe58407c
--- /dev/null
+++ b/fontbox/src/test/java/org/apache/fontbox/ttf/TrueTypeInterpreterTest.java
@@ -0,0 +1,352 @@
+/*
+ * 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.fontbox.ttf;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
+
+import java.time.Duration;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Program-tier (Tier 2) tests: hand-assembled bytecode fed through the dispatch loop with no font.
+ * These exercise the engine - dispatch, the push family, branching, function definition and calling,
+ * and the {@link BytecodeStream} bounds checks - independently of any glyph.
+ */
+class TrueTypeInterpreterTest
+{
+ // opcodes used to assemble test programs
+ private static final byte PUSHB1 = (byte) 0xB0; // PUSHB[0] - push one byte
+ private static final byte PUSHB2 = (byte) 0xB1; // PUSHB[1] - push two bytes
+ private static final byte NPUSHW = (byte) 0x41;
+ private static final byte ADD = 0x60;
+ private static final byte SUB = 0x61;
+ private static final byte MUL = 0x63;
+ private static final byte DUP = 0x20;
+ private static final byte SWAP = 0x23;
+ private static final byte DEPTH = 0x24;
+ private static final byte ROLL = (byte) 0x8A;
+ private static final byte GT = 0x52;
+ private static final byte IF = 0x58;
+ private static final byte ELSE = 0x1B;
+ private static final byte EIF = 0x59;
+ private static final byte PUSHW1 = (byte) 0xB8; // PUSHW[0] - push one signed word
+ private static final byte JMPR = 0x1C;
+ private static final byte JROT = 0x78;
+ private static final byte FDEF = 0x2C;
+ private static final byte ENDF = 0x2D;
+ private static final byte CALL = 0x2B;
+ private static final byte LOOPCALL = 0x2A;
+ private static final byte MPPEM = 0x4B;
+ private static final byte WS = 0x42;
+ private static final byte RS = 0x43;
+ private static final byte SZPS = 0x16;
+ private static final byte SCFS = 0x48;
+ private static final byte GC = 0x46;
+
+ private static TrueTypeInterpreter interpreter()
+ {
+ return new TrueTypeInterpreter(256, 16, 0, 2048);
+ }
+
+ private static int runTop(byte[] program)
+ {
+ ExecutionContext ctx = interpreter().executeProgram(program, 16);
+ return ctx.peek(0);
+ }
+
+ @Test
+ void testPushAndAdd()
+ {
+ // PUSHB[1] 2 3 ; ADD -> 5
+ assertEquals(5, runTop(new byte[] { PUSHB2, 2, 3, ADD }));
+ }
+
+ @Test
+ void testNpushwSigned()
+ {
+ // NPUSHW 1 0xFFFF ; -> -1 on the stack
+ assertEquals(-1, runTop(new byte[] { NPUSHW, 1, (byte) 0xFF, (byte) 0xFF }));
+ }
+
+ @Test
+ void testArithmetic()
+ {
+ // 10 - 3 == 7
+ assertEquals(7, runTop(new byte[] { PUSHB2, 10, 3, SUB }));
+ // 64(=1.0) * 192(=3.0) == 192(=3.0) ... use F26Dot6: PUSHB 64, then need words; use small ints
+ // 2.0 * 3.0 in F26Dot6: push 128 and 192 via NPUSHW
+ assertEquals(Fixed.fromInt(6),
+ runTop(new byte[] { NPUSHW, 2, 0, (byte) 128, 0, (byte) 192, MUL }));
+ }
+
+ @Test
+ void testStackOps()
+ {
+ // DUP: push 7, dup, add -> 14
+ assertEquals(14, runTop(new byte[] { PUSHB1, 7, DUP, ADD }));
+ // SWAP then SUB: push 3,10 swap -> 10,3 ; SUB pops b=3,a=10 -> 7
+ assertEquals(7, runTop(new byte[] { PUSHB2, 3, 10, SWAP, SUB }));
+ // DEPTH after pushing three values -> 3
+ assertEquals(3, runTop(new byte[] { PUSHB2, 1, 2, PUSHB1, 9, DEPTH }));
+ // ROLL: 1 2 3 -> 2 3 1, top is 1
+ assertEquals(1, runTop(new byte[] { PUSHB2, 1, 2, PUSHB1, 3, ROLL }));
+ }
+
+ @Test
+ void testIfElseTrueBranch()
+ {
+ // push 1 (true) ; IF push 10 ELSE push 20 EIF -> 10
+ assertEquals(10, runTop(new byte[] { PUSHB1, 1, IF, PUSHB1, 10, ELSE, PUSHB1, 20, EIF }));
+ }
+
+ @Test
+ void testIfElseFalseBranch()
+ {
+ // push 0 (false) ; IF push 10 ELSE push 20 EIF -> 20
+ assertEquals(20, runTop(new byte[] { PUSHB1, 0, IF, PUSHB1, 10, ELSE, PUSHB1, 20, EIF }));
+ }
+
+ @Test
+ void testNestedIf()
+ {
+ // outer true, inner (5>3) true -> 99
+ // PUSHB 1 ; IF [ PUSHB 5 3 ; GT ; IF PUSHB 99 ELSE PUSHB 1 EIF ] ELSE PUSHB 7 EIF
+ byte[] program = new byte[] {
+ PUSHB1, 1, IF,
+ PUSHB2, 5, 3, GT, IF,
+ PUSHB1, 99,
+ ELSE,
+ PUSHB1, 1,
+ EIF,
+ ELSE,
+ PUSHB1, 7,
+ EIF };
+ assertEquals(99, runTop(program));
+ }
+
+ @Test
+ void testJmpr()
+ {
+ // PUSHB 3 ; JMPR (jump +3 from the JMPR opcode) skips a push, lands on PUSHB 42
+ // layout: [0]PUSHB1 [1]3 [2]JMPR [3]PUSHB1 [4]7(skipped) [5]PUSHB1 [6]42
+ ExecutionContext ctx = interpreter().executeProgram(
+ new byte[] { PUSHB1, 3, JMPR, PUSHB1, 7, PUSHB1, 42 }, 16);
+ assertEquals(42, ctx.peek(0));
+ assertEquals(1, ctx.getStackDepth()); // the skipped push never ran
+ }
+
+ @Test
+ void testFunctionDefAndCall()
+ {
+ // define function 5 = "double the top" (DUP ADD); call it on 21 -> 42
+ TrueTypeInterpreter interp = interpreter();
+ interp.setFontProgram(new byte[] { PUSHB1, 5, FDEF, DUP, ADD, ENDF });
+ interp.prepareFontProgram();
+ assertEquals(1, interp.getFunctions().size());
+
+ ExecutionContext ctx = interp.executeProgram(new byte[] { PUSHB1, 21, PUSHB1, 5, CALL }, 16);
+ assertEquals(42, ctx.peek(0));
+ }
+
+ @Test
+ void testLoopCall()
+ {
+ // function 1 = "add 1"; LOOPCALL it 3 times starting from 0 -> 3
+ TrueTypeInterpreter interp = interpreter();
+ interp.setFontProgram(new byte[] { PUSHB1, 1, FDEF, PUSHB1, 1, ADD, ENDF });
+ interp.prepareFontProgram();
+
+ // stack: value=0, count=3, fn=1 ; LOOPCALL pops fn then count
+ ExecutionContext ctx = interp.executeProgram(
+ new byte[] { PUSHB1, 0, PUSHB2, 3, 1, LOOPCALL }, 16);
+ assertEquals(3, ctx.peek(0));
+ }
+
+ @Test
+ void testCallDepthLimitTrips()
+ {
+ // function 0 calls itself unconditionally -> must trip the depth cap, not StackOverflowError
+ TrueTypeInterpreter interp = interpreter();
+ interp.setFontProgram(new byte[] { PUSHB1, 0, FDEF, PUSHB1, 0, CALL, ENDF });
+ interp.prepareFontProgram();
+
+ HintingException ex = assertThrows(HintingException.class,
+ () -> interp.executeProgram(new byte[] { PUSHB1, 0, CALL }, 16));
+ assertEquals(true, ex.getMessage().contains("call depth"));
+ }
+
+ @Test
+ void testIdefDefinesOpcode()
+ {
+ // IDEF binds reserved opcode 0x83 to "push 42"; invoking 0x83 then runs that body.
+ // PUSHB[0] 0x83 ; IDEF ; PUSHB[0] 42 ; ENDF ; <0x83>
+ ExecutionContext ctx = interpreter().executeProgram(
+ new byte[] { PUSHB1, (byte) 0x83, (byte) 0x89, PUSHB1, 42, ENDF, (byte) 0x83 }, 16);
+ assertEquals(42, ctx.peek(0));
+ }
+
+ @Test
+ void testUndefinedFunctionThrows()
+ {
+ assertThrows(HintingException.class,
+ () -> interpreter().executeProgram(new byte[] { PUSHB1, 9, CALL }, 16));
+ }
+
+ @Test
+ void testMppemReflectsPpem()
+ {
+ ExecutionContext ctx = interpreter().executeProgram(new byte[] { MPPEM }, 19);
+ assertEquals(19, ctx.peek(0));
+ }
+
+ @Test
+ void testUnsupportedOpcodeThrows()
+ {
+ // 0x28 is a reserved/unused opcode; unimplemented opcodes must throw, not no-op
+ assertThrows(HintingException.class,
+ () -> interpreter().executeProgram(new byte[] { 0x28 }, 16));
+ }
+
+ @Test
+ void testControlValueScalingThroughPrep()
+ {
+ // raw cvt [2048] at 16 ppem, unitsPerEm 2048 -> scaled to 16px (1024 in F26Dot6)
+ TrueTypeInterpreter interp = interpreter();
+ interp.setControlValues(new int[] { 2048 });
+ interp.setPpem(16, 16);
+ // RCVT 0 -> the scaled value
+ ExecutionContext ctx = interp.executeProgram(new byte[] { PUSHB1, 0, 0x45 }, 16);
+ assertEquals(Fixed.fromInt(16), ctx.peek(0));
+ }
+
+ /**
+ * A backward jump is one of only two ways TrueType bytecode can loop, and this four-byte program -
+ * {@code PUSHW -3 ; JMPR}, which jumps back onto its own push - used to spin forever. It must now
+ * hit the execution budget and throw, so {@code GlyphHinter} falls back to the raw outline.
+ */
+ @Test
+ void testBackwardJumpIsBounded()
+ {
+ assertTimeoutPreemptively(Duration.ofSeconds(5), () ->
+ assertThrows(HintingException.class,
+ () -> interpreter().executeProgram(
+ new byte[] { PUSHW1, (byte) 0xFF, (byte) 0xFD, JMPR }, 16)));
+ }
+
+ /**
+ * A backward-jump loop that stays inside the budget must still run to completion - the bound exists
+ * to stop runaway programs, not legitimately loop-heavy ones.
+ */
+ @Test
+ void testBackwardJumpWithinBudgetCompletes()
+ {
+ // counter = 60; loop { counter -= 1; if (counter != 0) jump back } -> 59 backward jumps
+ // [0]PUSHW1 60 [3]PUSHB1 1 [5]SUB [6]DUP [7]PUSHW1 -8 [10]SWAP [11]JROT
+ byte[] program =
+ {
+ PUSHW1, 0, 60,
+ PUSHB1, 1, SUB, DUP, PUSHW1, (byte) 0xFF, (byte) 0xF8, SWAP, JROT
+ };
+ ExecutionContext ctx = assertTimeoutPreemptively(Duration.ofSeconds(5),
+ () -> interpreter().executeProgram(program, 16));
+ assertEquals(0, ctx.peek(0));
+ assertEquals(1, ctx.getStackDepth());
+ }
+
+ /**
+ * {@code LOOPCALL} takes its iteration count off the stack, so a crafted font can ask for billions.
+ * The whole loop is charged against the budget up front, so an absurd count fails before a single
+ * iteration runs.
+ */
+ @Test
+ void testLoopCallCountIsBounded()
+ {
+ TrueTypeInterpreter interp = interpreter();
+ interp.setFontProgram(new byte[] { PUSHB1, 1, FDEF, PUSHB1, 1, ADD, ENDF });
+ interp.prepareFontProgram();
+
+ // stack: value=0, count=32767, fn=1
+ assertTimeoutPreemptively(Duration.ofSeconds(5), () ->
+ assertThrows(HintingException.class,
+ () -> interp.executeProgram(
+ new byte[] { PUSHB1, 0, PUSHW1, 0x7F, (byte) 0xFF, PUSHB1, 1,
+ LOOPCALL }, 16)));
+ }
+
+ /**
+ * The spec calls the {@code LOOPCALL} count unsigned; FreeType runs nothing at all when it is not
+ * positive, so a negative count must be a no-op rather than an error or an underflowing loop.
+ */
+ @Test
+ void testNonPositiveLoopCallCountRunsNothing()
+ {
+ TrueTypeInterpreter interp = interpreter();
+ interp.setFontProgram(new byte[] { PUSHB1, 1, FDEF, PUSHB1, 1, ADD, ENDF });
+ interp.prepareFontProgram();
+
+ // stack: value=7, count=-1, fn=1 -> the function never runs, 7 is left untouched
+ ExecutionContext ctx = interp.executeProgram(
+ new byte[] { PUSHB1, 7, PUSHW1, (byte) 0xFF, (byte) 0xFF, PUSHB1, 1, LOOPCALL }, 16);
+ assertEquals(7, ctx.peek(0));
+ assertEquals(1, ctx.getStackDepth());
+ }
+
+ /**
+ * A font may compute values into the storage area in {@code prep} and read them back from every
+ * glyph program, so storage belongs to the size and not to one program run. Building a fresh
+ * storage array per run made {@code RS} read zeros - silently wrong outlines rather than a failure.
+ */
+ @Test
+ void testStoragePersistsFromPrepIntoGlyphProgram()
+ {
+ TrueTypeInterpreter interp = interpreter();
+ interp.setControlValueProgram(new byte[] { PUSHB2, 5, 42, WS }); // storage[5] = 42
+ interp.setPpem(16, 16);
+
+ ExecutionContext ctx = interp.executeProgram(new byte[] { PUSHB1, 5, RS }, 16);
+ assertEquals(42, ctx.peek(0));
+ }
+
+ /** The twilight zone belongs to the size for the same reason: {@code prep} seeds points there. */
+ @Test
+ void testTwilightPointsPersistFromPrep()
+ {
+ TrueTypeInterpreter interp = new TrueTypeInterpreter(256, 16, 4, 2048);
+ // prep: aim every zone pointer at the twilight zone, then set point 1's x to 128
+ interp.setControlValueProgram(new byte[] { PUSHB1, 0, SZPS, PUSHB2, 1, (byte) 128, SCFS });
+ interp.setPpem(16, 16);
+
+ ExecutionContext ctx = interp.executeProgram(new byte[] { PUSHB1, 0, SZPS, PUSHB1, 1, GC }, 16);
+ assertEquals(128, ctx.peek(0));
+ }
+
+ /** A new size starts clean, as FreeType clears both in {@code tt_size_run_prep}. */
+ @Test
+ void testStorageIsClearedOnPpemChange()
+ {
+ TrueTypeInterpreter interp = interpreter();
+ interp.setPpem(16, 16);
+ interp.executeProgram(new byte[] { PUSHB2, 5, 42, WS }, 16);
+ assertEquals(42, interp.executeProgram(new byte[] { PUSHB1, 5, RS }, 16).peek(0));
+
+ interp.setPpem(24, 24);
+ assertEquals(0, interp.executeProgram(new byte[] { PUSHB1, 5, RS }, 24).peek(0));
+ }
+}
diff --git a/fontbox/src/test/resources/ttf/hinting/LiberationSans-Regular-11.txt b/fontbox/src/test/resources/ttf/hinting/LiberationSans-Regular-11.txt
new file mode 100644
index 00000000000..cf92904ba8b
--- /dev/null
+++ b/fontbox/src/test/resources/ttf/hinting/LiberationSans-Regular-11.txt
@@ -0,0 +1,149 @@
+# 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.
+font LiberationSans-Regular.ttf
+ppem 11
+freetype 2.13.2
+glyph 43 H
+contours 11
+x 385 385 123 123 58 58 123 123 385 385 451 451
+y 0 265 265 0 0 512 512 320 320 512 512 0
+glyph 44 I
+contours 3
+x 65 65 131 131
+y 0 512 512 0
+glyph 47 L
+contours 5
+x 58 58 123 123 368 368
+y 0 512 512 54 54 0
+glyph 40 E
+contours 11
+x 58 58 425 425 123 123 405 405 123 123 439 439
+y 0 512 512 458 458 320 320 267 267 54 54 0
+glyph 55 T
+contours 7
+x 248 248 182 182 16 16 414 414
+y 458 0 0 458 458 512 512 458
+glyph 75 h
+contours 24
+x 109 129 185 228 289 346 346 346 284 284 284 270 237 207 164 111 111 111 49 49 111 111 111 108 108
+y 313 350 384 384 384 323 251 0 0 239 279 318 336 336 336 275 223 0 0 512 512 381 360 316 313
+glyph 82 o
+contours 10,22
+x 362 362 276 194 113 30 30 30 197 282 362 297 297 251 198 143 95 95 95 143 194 249 297
+y 192 95 0 0 0 99 192 384 384 384 291 192 269 338 338 338 267 192 119 46 46 46 117
+glyph 91 x
+contours 11
+x 275 175 75 8 140 14 83 175 267 337 210 344
+y 0 158 0 0 197 384 384 235 384 384 198 0
+glyph 81 n
+contours 26
+x 284 284 284 270 238 207 163 111 111 111 49 49 49 47 105 106 106 107 108 109 130 186 228 289 346 346 346
+y 0 239 277 318 336 336 336 274 219 0 0 302 369 384 384 382 365 343 313 313 352 384 384 384 322 251 0
+glyph 19 0
+contours 11,23
+x 364 364 279 195 112 28 28 28 109 197 283 364 301 301 253 197 140 90 90 90 141 195 250 301
+y 256 131 0 0 0 131 256 384 512 512 512 383 256 365 462 462 462 366 256 150 51 51 51 152
+glyph 20 1
+contours 10
+x 54 54 177 177 68 68 182 239 239 357 357
+y 0 53 53 453 384 443 512 512 53 53 0
+glyph 21 2
+contours 30
+x 35 35 53 103 159 214 258 285 285 285 238 196 157 105 101 38 45 130 197 270 349 349 349 323 272 200 160 113 103 356 356
+y 0 44 86 150 202 247 291 340 371 414 461 461 461 418 379 385 443 512 512 512 440 373 344 286 227 166 132 78 53 53 0
+glyph 22 3
+contours 40
+x 361 361 276 197 123 35 27 91 103 196 243 296 296 296 235 178 143 143 177 228 284 284 284 238 193 152 101 97 35 42 127 193 266 347 347 347 295 246 246 300 361
+y 162 85 0 0 0 66 131 137 51 51 51 107 163 212 266 266 266 320 320 320 357 390 423 461 461 461 419 381 386 445 512 512 512 452 398 356 304 295 293 287 216
+glyph 23 4
+contours 10,18
+x 303 303 244 244 16 16 238 303 303 371 371 244 244 225 221 97 78 73 244
+y 143 0 0 143 143 190 512 512 192 192 143 443 441 409 403 224 199 192 192
+glyph 24 5
+contours 28
+x 362 362 271 190 122 39 28 91 111 192 242 298 298 298 241 193 168 125 103 42 58 334 334 115 106 146 206 277 362
+y 167 89 0 0 0 59 116 123 51 51 51 112 165 212 269 269 269 253 234 234 512 512 459 459 289 320 320 320 235
+glyph 25 6
+contours 22,34
+x 361 361 278 205 123 36 36 36 126 209 319 347 288 270 208 155 97 97 114 175 215 282 361 298 298 246 200 156 103 103 103 159 202 247 298
+y 165 89 0 0 0 125 244 374 512 512 512 413 402 462 462 462 356 256 287 320 320 320 236 163 215 272 272 272 222 177 121 50 50 50 110
+glyph 26 7
+contours 11
+x 356 282 221 190 190 125 125 204 296 36 36 356
+y 461 341 204 71 0 0 99 317 459 459 512 512
+glyph 27 8
+contours 25,36,47
+x 361 361 276 196 119 31 31 31 85 127 127 88 42 42 42 125 195 266 349 349 349 303 263 263 309 361 285 285 195 151 105 105 105 152 195 239 285 297 297 243 195 148 95 95 95 197 248 297
+y 161 85 0 0 0 83 160 214 287 295 296 305 361 399 449 512 512 512 451 399 361 305 298 296 288 216 394 466 466 466 430 394 358 320 320 320 355 166 220 274 274 274 216 165 46 46 46 104
+glyph 28 9
+contours 23,36
+x 358 358 267 183 126 58 43 102 121 184 237 296 297 283 217 177 111 33 33 33 118 194 275 358 291 291 237 192 148 96 96 96 148 192 218 265 291
+y 266 138 0 0 0 48 101 110 50 50 50 159 261 230 192 192 192 280 352 427 512 512 512 389 336 393 462 462 462 403 352 301 241 241 241 265 308
+glyph 163 á
+contours 35,48,54
+x 142 86 30 30 30 106 190 274 274 274 235 194 153 115 111 46 62 196 266 337 337 337 337 351 372 381 392 392 369 344 309 278 276 274 250 187 157 191 243 274 274 274 206 163 118 94 94 94 127 142 142 217 288 288 174
+y 0 0 65 121 184 252 254 256 272 307 337 337 337 329 320 324 384 384 384 319 258 98 70 42 42 42 45 6 0 0 0 38 78 78 35 0 46 46 82 144 178 213 211 211 191 152 119 84 46 448 456 576 576 564 448
+glyph 162 à
+contours 35,48,54
+x 142 86 30 30 30 106 190 274 274 274 235 194 153 115 111 46 62 196 266 337 337 337 337 351 372 381 392 392 369 344 309 278 276 274 250 187 157 191 243 274 274 274 206 163 118 94 94 94 127 215 101 101 173 247 247
+y 0 0 65 121 184 252 254 256 272 307 337 337 337 329 320 324 384 384 384 319 258 98 70 42 42 42 45 6 0 0 0 38 78 78 35 0 46 46 82 144 178 213 211 211 191 152 119 84 46 448 564 576 576 456 448
+glyph 164 â
+contours 35,48,58
+x 142 86 30 30 30 106 190 274 274 274 235 194 153 115 111 46 62 196 266 337 337 337 337 351 372 381 392 392 369 344 309 278 276 274 250 187 157 191 243 274 274 274 206 163 118 94 94 94 127 303 303 267 191 191 111 75 75 155 226
+y 0 0 65 121 184 252 254 256 272 307 337 337 337 329 320 324 384 384 384 319 258 98 70 42 42 42 45 6 0 0 0 38 78 78 35 0 46 46 82 144 178 213 211 211 191 152 119 84 46 457 448 448 523 523 448 448 457 576 576
+glyph 166 ä
+contours 35,48,52,56
+x 142 86 30 30 30 106 190 274 274 274 235 194 153 115 111 46 62 196 266 337 337 337 337 351 372 381 392 392 369 344 309 278 276 274 250 187 157 191 243 274 274 274 206 163 118 94 94 94 127 232 232 288 288 96 96 153 153
+y 0 0 65 121 184 252 254 256 272 307 337 337 337 329 320 324 384 384 384 319 258 98 70 42 42 42 45 6 0 0 0 38 78 78 35 0 46 46 82 144 178 213 211 211 191 152 119 84 46 448 511 511 448 448 511 511 448
+glyph 165 ã
+contours 35,48,72
+x 142 86 30 30 30 106 190 274 274 274 235 194 153 115 111 46 62 196 266 337 337 337 337 351 372 381 392 392 369 344 309 278 276 274 250 187 157 191 243 274 274 274 206 163 118 94 94 94 127 253 239 210 183 159 148 129 110 107 76 80 96 124 146 161 190 217 241 251 285 291 323 317 283
+y 0 0 65 121 184 252 254 256 272 307 337 337 337 329 320 324 384 384 384 319 258 98 70 42 42 42 45 6 0 0 0 38 78 78 35 0 46 46 82 144 178 213 211 211 191 152 119 84 46 448 448 472 501 525 525 525 485 448 448 497 547 576 576 576 552 523 499 499 499 576 576 505 448
+glyph 171 é
+contours 18,25,31
+x 95 95 148 199 239 287 296 350 317 199 116 30 30 30 116 196 360 360 360 296 291 242 195 150 98 96 152 152 227 298 298 184
+y 209 131 46 46 46 92 128 109 0 0 0 98 194 286 384 384 384 216 209 256 299 338 338 338 294 256 448 456 576 576 564 448
+glyph 170 è
+contours 18,25,31
+x 95 95 148 199 239 287 296 350 317 199 116 30 30 30 116 196 360 360 360 296 291 242 195 150 98 96 226 112 112 184 258 258
+y 209 131 46 46 46 92 128 109 0 0 0 98 194 286 384 384 384 216 209 256 299 338 338 338 294 256 448 564 576 576 456 448
+glyph 169 ç
+contours 25,44
+x 95 95 142 189 222 266 271 333 326 249 190 112 30 30 30 112 189 246 321 331 268 263 224 188 139 95 258 258 164 144 133 133 150 163 213 213 213 167 153 148 170 207 193 226 258
+y 194 120 49 49 49 88 128 124 67 0 0 0 98 192 286 384 384 384 319 261 256 293 336 336 336 271 -88 -192 -192 -192 -191 -157 -159 -159 -159 -110 -64 -64 -64 -65 0 0 0 -2 -48
+glyph 179 ñ
+contours 26,50
+x 285 285 285 271 239 208 164 112 112 112 50 50 50 48 106 107 107 108 109 110 131 187 229 290 347 347 347 253 239 210 183 159 148 129 110 107 76 80 96 124 146 161 190 217 241 251 285 291 323 317 283
+y 0 239 277 318 336 336 336 274 219 0 0 302 369 384 384 382 365 343 313 313 352 384 384 384 322 251 0 448 448 472 501 525 525 525 485 448 448 497 547 576 576 576 552 523 499 499 499 576 576 505 448
+glyph 190 ü
+contours 26,30,34
+x 110 110 110 124 156 187 231 283 283 283 345 345 345 347 289 288 288 286 286 285 264 208 166 105 48 48 48 232 232 288 288 96 96 153 153
+y 384 145 107 66 48 48 48 110 165 384 384 82 15 0 0 2 19 41 71 71 32 0 0 0 62 133 384 448 511 511 448 448 511 511 448
+glyph 131 Á
+contours 7,16,22
+x 401 346 125 69 1 199 274 468 235 232 223 207 145 326 264 254 244 188 188 263 334 334 227
+y 0 141 141 0 0 512 512 0 463 452 420 370 192 192 371 398 431 576 586 704 704 689 576
+glyph 139 É
+contours 11,17
+x 58 58 425 425 123 123 405 405 123 123 439 439 190 190 265 336 336 229
+y 0 512 512 458 458 320 320 267 267 54 54 0 576 586 704 704 689 576
+glyph 147 Ñ
+contours 13,37
+x 372 113 114 116 116 58 58 134 396 392 392 392 451 451 312 298 269 242 217 207 188 169 166 135 139 155 183 205 220 249 276 299 310 344 350 382 376 342
+y 0 441 405 344 0 0 512 512 69 141 173 512 512 0 576 576 600 629 653 653 653 613 576 576 625 675 704 704 704 680 651 627 627 627 704 704 633 576
+glyph 158 Ü
+contours 19,23,27
+x 251 192 103 54 54 54 120 120 120 187 251 316 389 389 389 454 454 454 404 313 293 293 349 349 157 157 214 214
+y 0 0 45 131 191 512 512 195 125 53 53 53 128 199 512 512 197 136 47 0 576 639 639 576 576 639 639 576
diff --git a/fontbox/src/test/resources/ttf/hinting/LiberationSans-Regular-13.txt b/fontbox/src/test/resources/ttf/hinting/LiberationSans-Regular-13.txt
new file mode 100644
index 00000000000..762cae84615
--- /dev/null
+++ b/fontbox/src/test/resources/ttf/hinting/LiberationSans-Regular-13.txt
@@ -0,0 +1,149 @@
+# 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.
+font LiberationSans-Regular.ttf
+ppem 13
+freetype 2.13.2
+glyph 43 H
+contours 11
+x 455 455 146 146 68 68 146 146 455 455 533 533
+y 0 319 319 0 0 640 640 384 384 640 640 0
+glyph 44 I
+contours 3
+x 77 77 154 154
+y 0 640 640 0
+glyph 47 L
+contours 5
+x 68 68 146 146 435 435
+y 0 640 640 63 63 0
+glyph 40 E
+contours 11
+x 68 68 503 503 146 146 478 478 146 146 519 519
+y 0 640 640 577 577 384 384 321 321 63 63 0
+glyph 55 T
+contours 7
+x 293 293 215 215 19 19 489 489
+y 577 0 0 577 577 640 640 577
+glyph 75 h
+contours 24
+x 129 153 219 270 341 409 409 409 335 335 335 318 279 245 193 131 131 131 58 58 131 131 131 128 128
+y 365 408 448 448 448 377 293 0 0 279 326 371 392 392 392 320 260 0 0 640 640 459 431 370 365
+glyph 82 o
+contours 10,22
+x 428 428 326 230 133 35 35 35 232 333 428 351 351 297 233 169 112 112 112 168 229 295 351
+y 224 111 0 0 0 115 224 448 448 448 339 224 313 394 394 394 312 224 139 54 54 54 137
+glyph 91 x
+contours 11
+x 325 207 88 9 166 17 98 207 316 398 249 407
+y 0 184 0 0 230 448 448 274 448 448 231 0
+glyph 81 n
+contours 26
+x 335 335 335 318 281 245 192 131 131 131 58 58 58 55 125 125 126 127 128 129 154 221 270 342 409 409 409
+y 0 279 323 371 392 392 392 320 255 0 0 352 431 448 448 446 426 400 365 365 410 448 448 448 376 293 0
+glyph 19 0
+contours 11,23
+x 430 430 329 230 132 33 33 33 129 233 334 430 356 356 299 233 165 106 106 106 166 231 296 356
+y 320 164 0 0 0 163 320 480 640 640 640 478 320 458 581 581 581 459 320 185 60 60 60 188
+glyph 20 1
+contours 10
+x 63 63 209 209 80 80 215 283 283 422 422
+y 0 62 62 571 448 517 640 640 62 62 0
+glyph 21 2
+contours 30
+x 42 42 63 122 188 253 305 337 337 337 282 233 186 125 120 45 53 154 232 319 412 412 412 381 321 236 190 134 122 421 421
+y 0 51 104 185 250 307 363 424 463 519 580 580 580 529 483 490 559 640 640 640 549 465 428 355 282 205 162 94 62 62 0
+glyph 22 3
+contours 40
+x 426 426 325 232 145 42 32 107 122 232 287 350 350 350 278 210 169 169 209 269 335 335 335 281 228 180 120 115 41 49 150 228 314 410 410 410 349 290 290 354 426
+y 195 102 0 0 0 78 155 162 61 61 61 129 197 255 321 321 321 384 384 384 436 482 527 580 580 580 530 485 491 561 640 640 640 561 490 435 367 355 353 345 260
+glyph 23 4
+contours 10,18
+x 358 358 289 289 19 19 281 358 358 438 438 289 288 267 262 115 93 87 289
+y 134 0 0 134 134 199 640 640 192 192 134 558 555 509 500 238 202 192 192
+glyph 24 5
+contours 28
+x 428 428 320 225 144 46 33 107 130 226 285 352 352 352 285 228 198 147 121 50 69 394 394 136 125 173 243 328 428
+y 200 107 0 0 0 70 136 145 60 60 60 134 199 255 324 324 324 305 282 282 640 640 578 578 348 384 384 384 282
+glyph 25 6
+contours 22,34
+x 426 426 328 241 144 42 42 42 148 247 376 410 340 319 246 184 115 115 135 207 254 333 426 352 352 291 236 185 122 122 122 188 239 292 352
+y 198 106 0 0 0 156 305 467 640 640 640 524 511 581 581 581 441 309 346 384 384 384 283 195 259 328 328 328 267 213 145 59 59 59 132
+glyph 26 7
+contours 11
+x 421 333 261 225 225 148 148 241 350 43 43 421
+y 581 429 257 90 0 0 124 399 578 578 640 640
+glyph 27 8
+contours 25,36,47
+x 427 427 326 232 140 36 36 36 100 150 150 104 50 50 50 148 230 314 412 412 412 358 311 311 366 427 336 336 230 178 124 124 124 180 230 282 336 351 351 288 230 175 112 112 112 233 292 351
+y 193 102 0 0 0 100 193 257 346 355 357 368 442 492 558 640 640 640 559 490 441 366 357 355 345 258 487 586 586 586 536 487 437 384 384 384 433 200 264 329 329 329 259 198 55 55 55 124
+glyph 28 9
+contours 23,36
+x 423 423 315 216 149 68 51 121 143 218 280 349 351 335 256 209 132 39 39 39 140 229 325 423 344 344 280 227 174 113 113 113 174 226 258 313 344
+y 332 172 0 0 0 56 119 130 59 59 59 170 273 236 192 192 192 315 416 521 640 640 640 486 392 477 581 581 581 492 417 340 250 250 250 286 351
+glyph 163 á
+contours 35,48,54
+x 168 102 35 35 35 125 225 324 324 324 278 230 180 135 131 55 74 231 314 398 398 398 398 415 439 450 463 463 435 406 366 328 326 324 296 221 185 225 288 324 324 324 244 193 139 111 111 111 150 167 167 256 340 340 205
+y 0 0 65 121 184 252 254 256 282 341 392 392 392 358 320 327 448 448 448 373 301 114 82 49 49 49 52 6 0 0 0 44 91 91 41 0 55 55 87 143 173 205 204 203 186 150 121 89 55 512 520 640 640 628 512
+glyph 162 à
+contours 35,48,54
+x 168 102 35 35 35 125 225 324 324 324 278 230 180 135 131 55 74 231 314 398 398 398 398 415 439 450 463 463 435 406 366 328 326 324 296 221 185 225 288 324 324 324 244 193 139 111 111 111 150 253 119 119 203 291 291
+y 0 0 65 121 184 252 254 256 282 341 392 392 392 358 320 327 448 448 448 373 301 114 82 49 49 49 52 6 0 0 0 44 91 91 41 0 55 55 87 143 173 205 204 203 186 150 121 89 55 512 628 640 640 520 512
+glyph 164 â
+contours 35,48,58
+x 168 102 35 35 35 125 225 324 324 324 278 230 180 135 131 55 74 231 314 398 398 398 398 415 439 450 463 463 435 406 366 328 326 324 296 221 185 225 288 324 324 324 244 193 139 111 111 111 150 358 358 315 226 226 131 89 89 184 267
+y 0 0 65 121 184 252 254 256 282 341 392 392 392 358 320 327 448 448 448 373 301 114 82 49 49 49 52 6 0 0 0 44 91 91 41 0 55 55 87 143 173 205 204 203 186 150 121 89 55 521 512 512 587 587 512 512 521 640 640
+glyph 166 ä
+contours 35,48,52,56
+x 168 102 35 35 35 125 225 324 324 324 278 230 180 135 131 55 74 231 314 398 398 398 398 415 439 450 463 463 435 406 366 328 326 324 296 221 185 225 288 324 324 324 244 193 139 111 111 111 150 273 273 340 340 113 113 180 180
+y 0 0 65 121 184 252 254 256 282 341 392 392 392 358 320 327 448 448 448 373 301 114 82 49 49 49 52 6 0 0 0 44 91 91 41 0 55 55 87 143 173 205 204 203 186 150 121 89 55 512 587 587 512 512 587 587 512
+glyph 165 ã
+contours 35,48,72
+x 168 102 35 35 35 125 225 324 324 324 278 230 180 135 131 55 74 231 314 398 398 398 398 415 439 450 463 463 435 406 366 328 326 324 296 221 185 225 288 324 324 324 244 193 139 111 111 111 150 300 283 249 217 188 176 154 132 128 91 95 115 148 173 191 225 257 285 297 338 345 382 375 335
+y 0 0 65 121 184 252 254 256 282 341 392 392 392 358 320 327 448 448 448 373 301 114 82 49 49 49 52 6 0 0 0 44 91 91 41 0 55 55 87 143 173 205 204 203 186 150 121 89 55 512 512 533 558 579 579 579 544 512 512 561 611 640 640 640 619 594 573 573 573 640 640 569 512
+glyph 171 é
+contours 18,25,31
+x 112 112 175 235 282 340 350 414 375 235 137 35 35 35 137 232 426 426 426 350 344 285 231 178 115 113 179 179 268 352 352 217
+y 200 130 55 55 55 96 128 109 0 0 0 114 227 334 448 448 448 210 200 256 328 394 394 394 320 256 512 520 640 640 628 512
+glyph 170 è
+contours 18,25,31
+x 112 112 175 235 282 340 350 414 375 235 137 35 35 35 137 232 426 426 426 350 344 285 231 178 115 113 267 133 133 217 305 305
+y 200 130 55 55 55 96 128 109 0 0 0 114 227 334 448 448 448 210 200 256 328 394 394 394 320 256 512 628 640 640 520 512
+glyph 169 ç
+contours 25,44
+x 112 112 167 223 262 314 320 394 385 294 224 132 35 35 35 133 224 291 381 392 316 310 264 222 164 112 305 305 194 170 157 157 177 192 252 252 252 198 181 175 201 245 229 267 305
+y 226 141 58 58 58 92 128 124 67 0 0 0 115 224 333 448 448 448 383 325 320 353 391 391 391 316 -88 -192 -192 -192 -190 -151 -153 -153 -153 -107 -64 -64 -64 -65 0 0 0 -2 -48
+glyph 179 ñ
+contours 26,50
+x 337 337 337 320 283 247 194 133 133 133 60 60 60 57 127 127 128 129 130 131 156 223 272 344 411 411 411 300 283 249 217 188 176 154 132 128 91 95 115 148 173 191 225 257 285 297 338 345 382 375 335
+y 0 279 323 371 392 392 392 320 255 0 0 352 431 448 448 446 426 400 365 365 410 448 448 448 376 293 0 512 512 533 558 579 579 579 544 512 512 561 611 640 640 640 619 594 573 573 573 640 640 569 512
+glyph 190 ü
+contours 26,30,34
+x 130 130 130 147 184 220 273 334 334 334 407 407 407 409 340 340 339 338 337 336 311 244 195 123 56 56 56 274 274 341 341 114 114 181 181
+y 448 169 125 77 56 56 56 128 193 448 448 96 17 0 0 2 22 48 83 83 38 0 0 0 72 155 448 512 587 587 512 512 587 587 512
+glyph 131 Á
+contours 7,16,22
+x 474 409 148 82 2 235 323 553 278 274 264 244 171 386 312 300 289 222 222 311 395 395 269
+y 0 195 195 0 0 640 640 0 582 569 531 470 256 256 471 503 544 704 714 832 832 817 704
+glyph 139 É
+contours 11,17
+x 68 68 503 503 146 146 478 478 146 146 519 519 224 224 313 397 397 271
+y 0 640 640 577 577 384 384 321 321 63 63 0 704 714 832 832 817 704
+glyph 147 Ñ
+contours 13,37
+x 440 133 135 137 137 68 68 158 468 463 463 463 533 533 369 352 318 286 257 245 223 201 197 160 164 184 217 242 260 295 326 354 366 407 414 451 444 404
+y 0 556 511 434 0 0 640 640 82 173 213 640 640 0 704 704 725 750 771 771 771 736 704 704 753 803 832 832 832 811 786 765 765 765 832 832 761 704
+glyph 158 Ü
+contours 19,23,27
+x 297 227 122 64 64 64 142 142 142 221 296 373 459 459 459 537 537 537 478 370 346 346 413 413 186 186 253 253
+y 0 0 56 164 238 640 640 241 154 63 63 63 157 247 640 640 246 170 59 0 704 779 779 704 704 779 779 704
diff --git a/fontbox/src/test/resources/ttf/hinting/LiberationSans-Regular-16.txt b/fontbox/src/test/resources/ttf/hinting/LiberationSans-Regular-16.txt
new file mode 100644
index 00000000000..eb1d3879721
--- /dev/null
+++ b/fontbox/src/test/resources/ttf/hinting/LiberationSans-Regular-16.txt
@@ -0,0 +1,149 @@
+# 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.
+font LiberationSans-Regular.ttf
+ppem 16
+freetype 2.13.2
+glyph 43 H
+contours 11
+x 561 561 180 180 84 84 180 180 561 561 656 656
+y 0 384 384 0 0 768 768 464 464 768 768 0
+glyph 44 I
+contours 3
+x 95 95 190 190
+y 0 768 768 0
+glyph 47 L
+contours 5
+x 84 84 180 180 536 536
+y 0 768 768 78 78 0
+glyph 40 E
+contours 11
+x 84 84 619 619 180 180 589 589 180 180 639 639
+y 0 768 768 690 690 464 464 387 387 78 78 0
+glyph 55 T
+contours 7
+x 360 360 265 265 23 23 602 602
+y 690 0 0 690 690 768 768 690
+glyph 75 h
+contours 24
+x 159 188 269 332 420 503 503 503 413 413 413 392 344 301 238 161 161 161 71 71 161 161 161 158 157
+y 469 524 576 576 576 485 377 0 0 360 420 479 506 506 506 414 335 0 0 768 768 571 540 474 469
+glyph 82 o
+contours 10,22
+x 527 527 402 283 164 43 43 43 286 410 527 432 432 366 287 208 138 138 138 207 282 363 432
+y 289 143 0 0 0 148 289 576 576 576 436 289 404 509 509 509 402 289 178 67 67 67 174
+glyph 91 x
+contours 11
+x 401 255 109 12 204 21 120 255 389 490 306 501
+y 0 236 0 0 296 576 576 352 576 576 297 0
+glyph 81 n
+contours 26
+x 413 413 413 392 346 301 236 161 161 161 71 71 71 68 153 154 155 156 157 159 190 271 332 421 503 503 503
+y 0 360 417 479 506 506 506 412 329 0 0 453 554 576 576 573 548 515 469 469 527 576 576 576 483 377 0
+glyph 19 0
+contours 11,23
+x 530 530 405 284 162 40 40 40 159 287 411 530 438 438 368 287 204 131 131 131 205 285 364 438
+y 384 197 0 0 0 196 384 576 768 768 768 574 384 548 695 695 695 550 384 223 74 74 74 226
+glyph 20 1
+contours 10
+x 78 78 258 258 99 99 265 348 348 520 520
+y 0 77 77 682 576 661 768 768 77 77 0
+glyph 21 2
+contours 30
+x 52 52 78 151 232 312 376 415 415 415 347 287 229 155 148 56 66 189 286 393 507 507 507 470 396 291 234 166 151 518 518
+y 0 64 127 224 302 369 436 510 556 622 694 694 694 632 575 584 668 768 768 768 659 559 515 427 339 247 197 115 77 77 0
+glyph 22 3
+contours 40
+x 525 525 401 286 179 51 39 132 150 286 354 431 431 431 343 259 208 208 257 331 413 413 413 347 281 222 148 142 51 61 185 282 388 505 505 505 430 358 358 437 525
+y 236 123 0 0 0 97 191 200 75 75 75 157 237 307 386 386 386 464 464 464 525 579 632 694 694 694 633 578 585 671 768 768 768 673 589 524 442 428 426 416 313
+glyph 23 4
+contours 10,18
+x 441 441 356 356 24 24 347 441 441 540 540 356 355 329 323 142 115 107 356
+y 185 0 0 185 185 260 768 768 256 256 185 666 663 611 601 308 267 256 256
+glyph 24 5
+contours 28
+x 527 527 394 277 178 57 41 132 161 279 351 433 433 433 351 281 244 181 150 62 85 486 486 167 154 213 300 404 527
+y 267 143 0 0 0 67 130 138 74 74 74 176 265 342 438 438 438 415 388 388 768 768 691 691 467 512 512 512 376
+glyph 25 6
+contours 22,34
+x 525 525 404 297 178 52 52 52 183 304 464 505 419 393 303 226 142 142 167 256 313 411 525 433 433 358 291 228 151 151 151 231 294 359 433
+y 265 142 0 0 0 187 367 560 768 768 768 625 609 695 695 695 553 419 465 512 512 512 378 260 347 442 442 442 358 285 192 73 73 73 173
+glyph 26 7
+contours 11
+x 518 410 321 277 277 183 183 297 431 53 53 518
+y 695 513 308 107 0 0 149 477 691 691 768 768
+glyph 27 8
+contours 25,36,47
+x 525 525 401 285 172 45 45 45 124 185 185 128 61 61 61 182 283 387 508 508 508 441 383 383 450 525 414 414 283 220 153 153 153 222 284 348 414 432 432 354 284 215 138 138 138 287 360 432
+y 225 119 0 0 0 116 224 299 401 412 414 428 520 583 665 768 768 768 667 581 519 427 415 413 401 300 577 701 701 701 639 577 514 448 448 448 509 233 306 380 380 380 300 230 68 68 68 147
+glyph 28 9
+contours 23,36
+x 521 521 389 266 184 84 63 149 176 268 345 430 432 412 315 257 162 48 48 48 172 283 400 521 423 423 345 280 215 140 140 140 215 279 318 385 423
+y 399 207 0 0 0 70 147 161 73 73 73 219 355 310 256 256 256 396 512 632 768 768 768 584 485 580 695 695 695 596 512 427 327 327 327 367 439
+glyph 163 á
+contours 35,48,54
+x 208 126 44 44 44 155 278 399 399 399 343 283 223 168 162 68 91 285 387 490 490 490 490 511 541 554 570 570 536 501 451 405 402 399 365 273 228 277 354 399 399 399 300 237 171 136 136 136 184 206 206 315 418 418 253
+y 0 0 81 152 231 315 318 320 356 437 507 507 507 479 448 455 576 576 576 479 387 145 103 61 61 61 64 8 0 0 0 57 117 117 53 0 68 68 109 179 217 257 255 254 233 188 151 111 68 640 653 832 832 814 640
+glyph 162 à
+contours 35,48,54
+x 208 126 44 44 44 155 278 399 399 399 343 283 223 168 162 68 91 285 387 490 490 490 490 511 541 554 570 570 536 501 451 405 402 399 365 273 228 277 354 399 399 399 300 237 171 136 136 136 184 312 147 147 251 359 359
+y 0 0 81 152 231 315 318 320 356 437 507 507 507 479 448 455 576 576 576 479 387 145 103 61 61 61 64 8 0 0 0 57 117 117 53 0 68 68 109 179 217 257 255 254 233 188 151 111 68 640 814 832 832 653 640
+glyph 164 â
+contours 35,48,58
+x 208 126 44 44 44 155 278 399 399 399 343 283 223 168 162 68 91 285 387 490 490 490 490 511 541 554 570 570 536 501 451 405 402 399 365 273 228 277 354 399 399 399 300 237 171 136 136 136 184 440 440 388 278 277 161 109 109 226 328
+y 0 0 81 152 231 315 318 320 356 437 507 507 507 479 448 455 576 576 576 479 387 145 103 61 61 61 64 8 0 0 0 57 117 117 53 0 68 68 109 179 217 257 255 254 233 188 151 111 68 653 640 640 752 752 640 640 653 832 832
+glyph 166 ä
+contours 35,48,52,56
+x 208 126 44 44 44 155 278 399 399 399 343 283 223 168 162 68 91 285 387 490 490 490 490 511 541 554 570 570 536 501 451 405 402 399 365 273 228 277 354 399 399 399 300 237 171 136 136 136 184 338 338 419 419 141 141 223 223
+y 0 0 81 152 231 315 318 320 356 437 507 507 507 479 448 455 576 576 576 479 387 145 103 61 61 61 64 8 0 0 0 57 117 117 53 0 68 68 109 179 217 257 255 254 233 188 151 111 68 640 732 732 640 640 732 732 640
+glyph 165 ã
+contours 35,48,72
+x 208 126 44 44 44 155 278 399 399 399 343 283 223 168 162 68 91 285 387 490 490 490 490 511 541 554 570 570 536 501 451 405 402 399 365 273 228 277 354 399 399 399 300 237 171 136 136 136 184 369 348 306 267 231 216 188 161 157 111 117 141 181 213 235 277 316 350 365 415 424 470 461 411
+y 0 0 81 152 231 315 318 320 356 437 507 507 507 479 448 455 576 576 576 479 387 145 103 61 61 61 64 8 0 0 0 57 117 117 53 0 68 68 109 179 217 257 255 254 233 188 151 111 68 640 640 656 677 693 693 693 665 640 640 689 739 768 768 768 752 731 715 715 715 768 768 697 640
+glyph 171 é
+contours 18,25,31
+x 138 138 215 289 348 418 431 510 462 290 170 44 44 44 169 286 524 524 524 431 424 352 284 219 142 139 220 220 329 432 432 267
+y 251 163 68 68 68 102 128 109 0 0 0 147 292 429 576 576 576 264 251 320 419 509 509 509 408 320 640 653 832 832 814 640
+glyph 170 è
+contours 18,25,31
+x 138 138 215 289 348 418 431 510 462 290 170 44 44 44 169 286 524 524 524 431 424 352 284 219 142 139 329 164 164 268 376 376
+y 251 163 68 68 68 102 128 109 0 0 0 147 292 429 576 576 576 264 251 320 419 509 509 509 408 320 640 814 832 832 653 640
+glyph 169 ç
+contours 25,44
+x 138 138 206 274 322 387 394 485 475 363 277 163 44 44 44 164 276 359 468 482 390 383 326 274 202 138 376 376 239 211 194 194 218 237 310 310 310 244 222 215 248 301 282 329 376
+y 290 179 71 71 71 99 128 124 67 0 0 0 147 289 429 576 576 576 511 453 448 474 505 505 505 407 -152 -256 -256 -256 -254 -205 -208 -208 -208 -167 -128 -128 -128 -129 0 0 -64 -66 -112
+glyph 179 ñ
+contours 26,50
+x 415 415 415 394 348 303 238 163 163 163 73 73 73 70 155 156 157 158 159 161 192 273 334 423 505 505 505 369 348 306 267 231 216 188 161 157 111 117 141 181 213 235 277 316 350 365 415 424 470 461 411
+y 0 360 417 479 506 506 506 412 329 0 0 453 554 576 576 573 548 515 469 469 527 576 576 576 483 377 0 640 640 656 677 693 693 693 665 640 640 689 739 768 768 768 752 731 715 715 715 768 768 697 640
+glyph 190 ü
+contours 26,30,34
+x 160 160 160 181 227 272 337 412 412 412 502 502 502 505 420 420 419 417 416 415 384 302 242 153 70 70 70 339 339 420 420 142 142 224 224
+y 576 216 159 97 70 70 70 164 247 576 576 123 22 0 0 3 27 59 103 103 47 0 0 0 93 199 576 640 732 732 640 640 732 732 640
+glyph 131 Á
+contours 7,16,22
+x 584 503 182 101 2 290 398 681 343 338 326 301 211 475 385 371 357 274 274 383 486 486 331
+y 0 245 245 0 0 768 768 0 696 681 637 567 320 320 568 605 652 832 842 960 960 945 832
+glyph 139 É
+contours 11,17
+x 84 84 619 619 180 180 589 589 180 180 639 639 276 276 385 488 488 333
+y 0 768 768 690 690 464 464 387 387 78 78 0 832 842 960 960 945 832
+glyph 147 Ñ
+contours 13,37
+x 541 164 167 169 169 84 84 195 576 570 570 570 656 656 455 434 392 353 318 302 275 248 243 197 203 227 267 299 321 363 402 436 451 501 510 556 548 498
+y 0 663 609 517 0 0 768 768 101 209 258 768 768 0 832 832 848 869 885 885 885 857 832 832 881 931 960 960 960 944 923 907 907 907 960 960 889 832
+glyph 158 Ü
+contours 19,23,27
+x 366 279 150 79 79 79 175 175 175 273 366 461 566 566 566 661 661 661 588 456 427 427 508 508 230 230 312 312
+y 0 0 68 197 286 768 768 291 186 78 78 78 190 298 768 768 296 204 70 0 832 924 924 832 832 924 924 832
diff --git a/fontbox/src/test/resources/ttf/hinting/LiberationSans-Regular-24.txt b/fontbox/src/test/resources/ttf/hinting/LiberationSans-Regular-24.txt
new file mode 100644
index 00000000000..1dd286dd111
--- /dev/null
+++ b/fontbox/src/test/resources/ttf/hinting/LiberationSans-Regular-24.txt
@@ -0,0 +1,149 @@
+# 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.
+font LiberationSans-Regular.ttf
+ppem 24
+freetype 2.13.2
+glyph 43 H
+contours 11
+x 841 841 269 269 126 126 269 269 841 841 984 984
+y 0 520 520 0 0 1088 1088 640 640 1088 1088 0
+glyph 44 I
+contours 3
+x 142 142 285 285
+y 0 1088 1088 0
+glyph 47 L
+contours 5
+x 126 126 269 269 803 803
+y 0 1088 1088 117 117 0
+glyph 40 E
+contours 11
+x 126 126 928 928 269 269 883 883 269 269 959 959
+y 0 1088 1088 971 971 640 640 524 524 117 117 0
+glyph 55 T
+contours 7
+x 540 540 398 398 35 35 903 903
+y 971 0 0 971 971 1088 1088 971
+glyph 75 h
+contours 24
+x 238 282 404 498 630 755 755 755 619 619 619 588 516 452 357 242 242 242 107 107 242 242 242 236 236
+y 677 757 832 832 832 700 544 0 0 519 605 689 728 728 728 595 482 0 0 1088 1088 818 775 684 677
+glyph 82 o
+contours 10,22
+x 790 790 603 424 246 65 65 65 429 615 790 648 648 548 430 312 206 206 206 310 422 544 648
+y 417 206 0 0 0 214 417 832 832 832 630 417 582 732 732 732 579 417 259 100 100 100 254
+glyph 91 x
+contours 11
+x 601 383 163 17 306 31 180 383 584 734 459 752
+y 0 341 0 0 428 832 832 508 832 832 430 0
+glyph 81 n
+contours 26
+x 619 619 619 588 519 452 354 242 242 242 107 107 107 102 230 231 232 235 236 238 285 407 498 631 755 755 755
+y 0 519 599 689 728 728 728 593 474 0 0 654 800 832 832 828 791 743 677 677 762 832 832 832 698 544 0
+glyph 19 0
+contours 11,23
+x 794 794 607 425 243 60 60 60 238 430 616 794 657 657 551 430 306 197 197 197 307 427 546 657
+y 544 279 0 0 0 278 544 816 1088 1088 1088 813 544 772 978 978 978 775 544 319 110 110 110 323
+glyph 20 1
+contours 10
+x 117 117 386 386 148 148 398 522 522 779 779
+y 0 115 115 959 768 895 1088 1088 115 115 0
+glyph 21 2
+contours 30
+x 77 77 115 226 347 467 563 622 622 622 520 429 343 231 221 83 98 283 429 589 761 761 761 705 594 437 351 249 226 777 777
+y 0 95 184 320 431 525 620 723 789 878 977 977 977 883 798 811 938 1088 1088 1088 935 794 731 608 485 355 283 168 115 115 0
+glyph 22 3
+contours 40
+x 787 787 601 429 268 77 59 198 225 429 531 647 647 647 514 389 312 312 386 497 619 619 619 519 421 331 221 212 77 92 277 423 582 758 758 758 644 536 536 655 787
+y 323 169 0 0 0 145 286 299 112 112 112 220 326 419 523 523 523 640 640 640 729 808 886 977 977 977 885 802 813 942 1088 1088 1088 948 823 727 607 586 583 569 429
+glyph 23 4
+contours 10,18
+x 661 661 533 533 35 35 519 661 661 809 809 533 532 493 483 212 172 160 533
+y 277 0 0 277 277 381 1088 1088 384 384 277 936 932 862 848 454 399 384 384
+glyph 24 5
+contours 28
+x 790 790 591 415 267 86 62 198 241 418 527 650 650 650 526 421 366 271 224 92 128 728 728 251 230 318 448 605 790
+y 367 196 0 0 0 129 251 267 110 110 110 246 364 467 594 594 594 560 518 518 1088 1088 973 973 637 704 704 704 517
+glyph 25 6
+contours 22,34
+x 787 787 605 446 267 78 78 78 275 456 696 758 629 589 455 339 212 212 249 382 469 615 787 650 650 537 437 342 226 226 226 347 441 539 650
+y 364 195 0 0 0 266 519 794 1088 1088 1088 872 849 978 978 978 766 565 633 704 704 704 519 358 474 600 600 600 488 391 267 109 109 109 242
+glyph 26 7
+contours 11
+x 777 615 482 415 415 274 274 446 647 79 79 777
+y 978 722 433 151 0 0 209 672 973 973 1088 1088
+glyph 27 8
+contours 25,36,47
+x 788 788 602 428 258 67 67 67 186 278 278 192 92 92 92 273 425 580 761 761 761 661 574 574 675 788 621 621 425 330 230 230 230 333 426 521 621 647 647 530 424 322 206 206 206 429 539 647
+y 320 169 0 0 0 166 318 425 571 586 589 608 739 826 943 1088 1088 1088 946 825 738 608 591 588 571 428 817 987 987 987 901 817 730 640 640 640 723 332 435 539 539 539 427 329 101 101 101 211
+glyph 28 9
+contours 23,36
+x 782 782 583 399 276 126 94 223 263 401 518 645 648 618 473 386 243 72 72 72 258 424 600 782 635 635 518 419 322 209 209 209 322 418 476 577 635
+y 565 293 0 0 0 104 221 241 109 109 109 330 534 466 384 384 384 577 736 900 1088 1088 1088 827 700 825 978 978 978 848 736 623 491 491 491 543 639
+glyph 163 á
+contours 35,48,54
+x 310 188 65 65 65 231 416 598 598 598 514 424 333 250 242 101 136 427 580 734 734 734 734 766 810 829 854 854 803 750 675 607 602 598 546 409 341 416 531 598 598 598 450 355 257 204 204 204 275 309 309 472 627 627 380
+y 0 0 130 242 369 504 509 512 554 648 729 729 729 656 576 590 832 832 832 692 560 211 152 91 91 91 97 12 0 0 0 81 168 168 76 0 101 101 169 287 350 417 414 412 376 301 240 173 101 896 913 1152 1152 1128 896
+glyph 162 à
+contours 35,48,54
+x 310 188 65 65 65 231 416 598 598 598 514 424 333 250 242 101 136 427 580 734 734 734 734 766 810 829 854 854 803 750 675 607 602 598 546 409 341 416 531 598 598 598 450 355 257 204 204 204 275 468 221 221 376 539 539
+y 0 0 130 242 369 504 509 512 554 648 729 729 729 656 576 590 832 832 832 692 560 211 152 91 91 91 97 12 0 0 0 81 168 168 76 0 101 101 169 287 350 417 414 412 376 301 240 173 101 896 1128 1152 1152 913 896
+glyph 164 â
+contours 35,48,58
+x 310 188 65 65 65 231 416 598 598 598 514 424 333 250 242 101 136 427 580 734 734 734 734 766 810 829 854 854 803 750 675 607 602 598 546 409 341 416 531 598 598 598 450 355 257 204 204 204 275 661 661 582 418 416 242 164 164 340 493
+y 0 0 130 242 369 504 509 512 554 648 729 729 729 656 576 590 832 832 832 692 560 211 152 91 91 91 97 12 0 0 0 81 168 168 76 0 101 101 169 287 350 417 414 412 376 301 240 173 101 914 896 896 1045 1045 896 896 914 1152 1152
+glyph 166 ä
+contours 35,48,52,56
+x 310 188 65 65 65 231 416 598 598 598 514 424 333 250 242 101 136 427 580 734 734 734 734 766 810 829 854 854 803 750 675 607 602 598 546 409 341 416 531 598 598 598 450 355 257 204 204 204 275 505 505 628 628 210 210 334 334
+y 0 0 130 242 369 504 509 512 554 648 729 729 729 656 576 590 832 832 832 692 560 211 152 91 91 91 97 12 0 0 0 81 168 168 76 0 101 101 169 287 350 417 414 412 376 301 240 173 101 896 1034 1034 896 896 1034 1034 896
+glyph 165 ã
+contours 35,48,72
+x 310 188 65 65 65 231 416 598 598 598 514 424 333 250 242 101 136 427 580 734 734 734 734 766 810 829 854 854 803 750 675 607 602 598 546 409 341 416 531 598 598 598 450 355 257 204 204 204 275 553 522 459 400 347 324 283 242 235 167 175 211 272 319 352 415 474 526 548 623 636 705 692 617
+y 0 0 130 242 369 504 509 512 554 648 729 729 729 656 576 590 832 832 832 692 560 211 152 91 91 91 97 12 0 0 0 81 168 168 76 0 101 101 169 287 350 417 414 412 376 301 240 173 101 896 896 921 951 976 976 976 934 896 896 969 1044 1088 1088 1088 1063 1033 1008 1008 1008 1088 1088 982 896
+glyph 171 é
+contours 18,25,31
+x 207 207 323 434 521 627 646 764 691 433 253 65 65 65 253 428 786 786 786 647 636 528 427 328 214 209 330 330 493 648 648 401
+y 408 261 101 101 101 189 256 218 0 0 0 212 421 620 832 832 832 425 408 512 627 732 732 732 615 512 896 913 1152 1152 1128 896
+glyph 170 è
+contours 18,25,31
+x 207 207 323 434 521 627 646 764 691 433 253 65 65 65 253 428 786 786 786 647 636 528 427 328 214 209 493 246 246 401 564 564
+y 408 261 101 101 101 189 256 218 0 0 0 212 421 620 832 832 832 425 408 512 627 732 732 732 615 512 896 1128 1152 1152 913 896
+glyph 169 ç
+contours 25,44
+x 206 206 308 411 483 580 591 728 712 544 415 244 65 65 65 245 413 538 702 723 584 574 488 409 302 206 563 563 358 315 290 290 327 355 465 465 465 366 333 323 371 452 422 493 563
+y 420 260 107 107 107 180 256 247 135 0 0 0 213 417 619 832 832 832 701 587 576 645 726 726 726 586 -181 -320 -320 -320 -317 -243 -248 -248 -248 -186 -128 -128 -128 -130 0 0 -64 -66 -128
+glyph 179 ñ
+contours 26,50
+x 622 622 622 591 522 455 357 245 245 245 110 110 110 105 233 234 235 238 239 241 288 410 501 634 758 758 758 553 522 459 400 347 324 283 242 235 167 175 211 272 319 352 415 474 526 548 623 636 705 692 617
+y 0 519 599 689 728 728 728 593 474 0 0 654 800 832 832 828 791 743 677 677 762 832 832 832 698 544 0 896 896 921 951 976 976 976 934 896 896 969 1044 1088 1088 1088 1063 1033 1008 1008 1008 1088 1088 982 896
+glyph 190 ü
+contours 26,30,34
+x 241 241 241 272 341 408 506 618 618 618 753 753 753 757 630 629 628 626 624 622 575 453 362 229 105 105 105 507 507 630 630 212 212 336 336
+y 832 313 233 143 104 104 104 239 358 832 832 178 32 0 0 4 41 88 154 154 70 0 0 0 134 288 832 896 1034 1034 896 896 1034 1034 896
+glyph 131 Á
+contours 7,16,22
+x 875 755 273 152 3 434 597 1022 514 507 489 452 317 712 576 555 534 410 410 573 728 728 496
+y 0 336 336 0 0 1088 1088 0 980 959 896 798 448 448 799 852 917 1152 1168 1344 1344 1321 1152
+glyph 139 É
+contours 11,17
+x 126 126 928 928 269 269 883 883 269 269 959 959 413 413 576 731 731 499
+y 0 1088 1088 971 971 640 640 524 524 117 117 0 1152 1168 1344 1344 1321 1152
+glyph 147 Ñ
+contours 13,37
+x 812 246 250 254 254 126 126 293 864 855 855 855 984 984 682 651 588 529 476 453 411 371 364 296 304 340 401 448 481 544 603 655 677 752 765 834 821 746
+y 0 931 856 726 0 0 1088 1088 151 303 371 1088 1088 0 1152 1152 1177 1207 1232 1232 1232 1190 1152 1152 1225 1300 1344 1344 1344 1319 1289 1264 1264 1264 1344 1344 1238 1152
+glyph 158 Ü
+contours 19,23,27
+x 549 419 225 119 119 119 262 262 262 409 548 690 848 848 848 991 991 991 882 684 640 640 763 763 345 345 469 469
+y 0 0 96 279 405 1088 1088 416 269 116 116 116 274 426 1088 1088 419 289 100 0 1152 1290 1290 1152 1152 1290 1290 1152
diff --git a/fontbox/src/test/resources/ttf/hinting/README.md b/fontbox/src/test/resources/ttf/hinting/README.md
new file mode 100644
index 00000000000..66e4aff6f66
--- /dev/null
+++ b/fontbox/src/test/resources/ttf/hinting/README.md
@@ -0,0 +1,103 @@
+
+
+# TrueType hinting verification tooling
+
+These are developer/debugging tools for the FontBox TrueType bytecode interpreter
+(`org.apache.fontbox.ttf.instruction`). They use **FreeType only as an offline oracle** — FreeType is
+never linked, shipped, or a build dependency. The committed data files are plain coordinate/trace
+facts, not derivatives of FreeType (see `hinting_plan.md` "Oracle licensing").
+
+## Files
+
+| File | Purpose |
+|------|---------|
+| `generate_golden.py` | Dumps FreeType's post-hinting outline points for the Tier-A fonts to `- Allows to only render annotation accepted by the filter.
- *
+ *
* @param annotationFilter the AnnotationFilter
*/
public void setAnnotationFilter(AnnotationFilter annotationFilter)
{
this.annotationFilter = annotationFilter;
}
-
+
/**
* Returns the parent renderer.
- *
+ *
* @return the parent renderer
*/
public final PDFRenderer getRenderer()
@@ -234,7 +236,7 @@ public final PDFRenderer getRenderer()
/**
* Returns the underlying Graphics2D. May be null if drawPage has not yet been called.
- *
+ *
* @return the underlying Graphics2D
*/
protected final Graphics2D getGraphics()
@@ -244,7 +246,7 @@ protected final Graphics2D getGraphics()
/**
* Returns the current line path. This is reset to empty after each fill/stroke.
- *
+ *
* @return the current line path
*/
protected final GeneralPath getLinePath()
@@ -262,7 +264,7 @@ private void setRenderingHints()
/**
* Draws the page to the requested context.
- *
+ *
* @param g The graphics context to draw onto.
* @param pageSize The size of the page to draw.
* @throws IOException If there is an IO error while drawing the page.
@@ -320,13 +322,13 @@ void drawTilingPattern(Graphics2D g, PDTilingPattern pattern, PDColorSpace color
lastClips = null;
Shape savedInitialClip = initialClip;
initialClip = null;
-
+
boolean savedFlipTG = flipTG;
flipTG = true;
setRenderingHints();
processTilingPattern(pattern, color, colorSpace, patternMatrix);
-
+
flipTG = savedFlipTG;
graphics = savedGraphics;
linePath = savedLinePath;
@@ -337,15 +339,15 @@ void drawTilingPattern(Graphics2D g, PDTilingPattern pattern, PDColorSpace color
private float clampColor(float color)
{
- return color < 0 ? 0 : (color > 1 ? 1 : color);
+ return color < 0 ? 0 : (color > 1 ? 1 : color);
}
/**
* Returns an AWT paint for the given PDColor.
- *
+ *
* @param color The color to get a paint for. This can be an actual color or a pattern.
* @return an AWT paint for the given PDColor
- *
+ *
* @throws IOException if the AWT paint could not be created
*/
protected Paint getPaint(PDColor color) throws IOException
@@ -384,7 +386,7 @@ else if (!(colorSpace instanceof PDPattern))
else
{
// uncolored tiling pattern
- return tilingPaintFactory.create(tilingPattern,
+ return tilingPaintFactory.create(tilingPattern,
patternSpace.getUnderlyingColorSpace(), color, xform);
}
}
@@ -460,7 +462,7 @@ public void endText() throws IOException
{
endTextClip();
}
-
+
/**
* Begin buffering the text clipping path, if any.
*/
@@ -477,7 +479,7 @@ private void endTextClip()
{
PDGraphicsState state = getGraphicsState();
RenderingMode renderingMode = state.getTextState().getRenderingMode();
-
+
// apply the buffered clip as one area
if (renderingMode.isClip() && !textClippings.isEmpty())
{
@@ -488,7 +490,7 @@ private void endTextClip()
state.intersectClippingPath(path);
textClippings = new ArrayList<>();
- // PDFBOX-3681: lastClip needs to be reset, because after intersection it is still the same
+ // PDFBOX-3681: lastClip needs to be reset, because after intersection it is still the same
// object, thus setClip() would believe that it is cached.
lastClips = null;
}
@@ -510,13 +512,53 @@ protected void showFontGlyph(Matrix textRenderingMatrix, PDFont font, int code,
glyphCaches.put(font, cache);
}
- GeneralPath path = cache.getPathForCharacterCode(code);
+ // Grid-fitting is off by default; PDFRenderer.setHintingEnabled(true) turns it on. While it
+ // is off we never derive a ppem and take the plain, code-keyed cache path, so the feature
+ // costs nothing when disabled.
+ int ppem = 0;
+ if (hintingEnabled)
+ {
+ // hintingPpem expects the glyph-space-to-device transform, but 'at' only maps glyph space
+ // to PDF user space (points) - the device scale lives in 'xform', which the Graphics2D
+ // applies separately. Compose it in so the ppem is the true device pixels-per-em;
+ // otherwise we grid-fit at the font's point size (e.g. 7) instead of its rendered size
+ // (e.g. 29 at 300dpi).
+ AffineTransform deviceAt = at;
+ if (xform != null)
+ {
+ deviceAt = new AffineTransform(xform);
+ deviceAt.concatenate(at);
+ }
+ ppem = hintingPpem(deviceAt);
+ }
+ GeneralPath path = ppem > 0 ? cache.getPathForCharacterCode(code, ppem)
+ : cache.getPathForCharacterCode(code);
drawGlyph(path, font, code, displacement, at);
}
+ /**
+ * Derives the pixels-per-em for grid-fitting from the glyph-space-to-device transform, or returns
+ * 0 when the glyph is too small / degenerate to hint. The ppem is the magnitude of the transform's
+ * vertical basis vector, i.e. the device height of one em, so it is correct under rotation: the
+ * glyph is grid-fit in its own (upright) coordinate space and the full transform — including any
+ * rotation — is then applied to the hinted outline by the caller, exactly as FreeType does for
+ * rotated text (90-degree vertical CJK columns being the common case). The path fed through
+ * {@code at} is normalized to 1000 units/em, so one em is 1000 units in {@code at}'s input space.
+ *
+ * @param at the transform mapping normalized (1000/em) glyph coordinates to device space
+ * @return the ppem to hint at, or 0 to render unhinted
+ */
+ static int hintingPpem(AffineTransform at)
+ {
+ // length of the y basis vector = device pixels per normalized unit, rotation-invariant
+ double scaleY = Math.hypot(at.getShearX(), at.getScaleY());
+ int ppem = (int) Math.round(1000.0 * scaleY);
+ return ppem > 0 ? ppem : 0;
+ }
+
/**
* Renders a glyph.
- *
+ *
* @param path the GeneralPath for the glyph
* @param font the font
* @param code character code
@@ -625,7 +667,7 @@ private Paint applySoftMaskToPaint(Paint parentPaint, PDSoftMask softMask) throw
}
}
}
- TransparencyGroup transparencyGroup = new TransparencyGroup(form, true,
+ TransparencyGroup transparencyGroup = new TransparencyGroup(form, true,
softMask.getInitialTransformationMatrix(), backdropColor);
BufferedImage image = transparencyGroup.getImage();
if (image == null)
@@ -663,7 +705,7 @@ private BufferedImage adjustImage(BufferedImage gray)
Rectangle originalBounds = new Rectangle(gray.getWidth(), gray.getHeight());
Rectangle2D transformedBounds = at.createTransformedShape(originalBounds).getBounds2D();
- at.preConcatenate(AffineTransform.getTranslateInstance(-transformedBounds.getMinX(),
+ at.preConcatenate(AffineTransform.getTranslateInstance(-transformedBounds.getMinX(),
-transformedBounds.getMinY()));
int width = (int) Math.ceil(transformedBounds.getWidth());
@@ -851,7 +893,7 @@ public void fillPath(int windingRule) throws IOException
graphics.setPaint(getNonStrokingPaint());
graphics.fill(shape);
}
-
+
linePath.reset();
if (noAntiAlias)
@@ -973,7 +1015,7 @@ public void clip(int windingRule)
getGraphicsState().intersectClippingPath(adjustClip(linePath));
}
- // PDFBOX-3836: lastClip needs to be reset, because after intersection it is still the same
+ // PDFBOX-3836: lastClip needs to be reset, because after intersection it is still the same
// object, thus setClip() would believe that it is cached.
lastClips = null;
@@ -1016,7 +1058,7 @@ public void endPath()
{
linePath.reset();
}
-
+
/**
* PDFBOX-5715 / PR#73: This was added to fix a problem with missing fine lines when printing
* on MacOS. Lines vanish because CPrinterJob sets graphics scale to 1 for Printable so after
@@ -1028,7 +1070,7 @@ public void endPath()
* here.
*
* @param linePath
- * @return
+ * @return
*/
private GeneralPath adjustClip(GeneralPath linePath)
{
@@ -1125,10 +1167,10 @@ public void drawImage(PDImage pdImage) throws IOException
{
// The earlier code for stencils (see "else") doesn't work with patterns because the
// CTM is not taken into consideration.
- // this code is based on the fact that it is easily possible to draw the mask and
+ // this code is based on the fact that it is easily possible to draw the mask and
// the paint at the correct place with the existing code, but not in one step.
// Thus what we do is to draw both in separate images, then combine the two and draw
- // the result.
+ // the result.
// Note that the device scale is not used. In theory, some patterns can get better
// at higher resolutions but the stencil would become more and more "blocky".
// If anybody wants to do this, have a look at the code in showTransparencyGroup().
@@ -1511,7 +1553,7 @@ private void drawBufferedImage(PDImage pdImage, BufferedImage image, AffineTrans
// will trigger the workaround. Because of the slowness we only do it if the user
// expects quality rendering and interpolation.
Matrix imageTransformMatrix = new Matrix(imageTransform);
- Matrix graphicsTransformMatrix = new Matrix(originalTransform);
+ Matrix graphicsTransformMatrix = new Matrix(originalTransform);
float scaleX = Math.abs(imageTransformMatrix.getScalingFactorX() * graphicsTransformMatrix.getScalingFactorX());
float scaleY = Math.abs(imageTransformMatrix.getScalingFactorY() * graphicsTransformMatrix.getScalingFactorY());
@@ -1580,7 +1622,7 @@ private BufferedImage applyTransferFunction(BufferedImage image, COSBase transfe
bim = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB);
}
- // prepare transfer functions (either one per color or one for all)
+ // prepare transfer functions (either one per color or one for all)
// and maps (actually arrays[256] to be faster) to avoid calculating values several times
Integer[] rMap;
Integer[] gMap;
@@ -1840,7 +1882,7 @@ public void showTransparencyGroup(PDTransparencyGroup form) throws IOException
/**
* For advanced users, to extract the transparency group into a separate graphics device.
- *
+ *
* @param form the transparency group to be extracted
* @param graphics the target graphics device
* @throws IOException if the transparency group could not be extracted
@@ -1944,7 +1986,7 @@ private final class TransparencyGroup
* masks.
* @throws IOException
*/
- private TransparencyGroup(PDTransparencyGroup form, boolean isSoftMask, Matrix ctm,
+ private TransparencyGroup(PDTransparencyGroup form, boolean isSoftMask, Matrix ctm,
PDColor backdropColor) throws IOException
{
Graphics2D savedGraphics = graphics;
@@ -2049,8 +2091,8 @@ private TransparencyGroup(PDTransparencyGroup form, boolean isSoftMask, Matrix c
}
if (isSoftMask && backdropColor != null)
{
- // "If the subtype is Luminosity, the transparency group XObject G shall be
- // composited with a fully opaque backdrop whose colour is everywhere defined
+ // "If the subtype is Luminosity, the transparency group XObject G shall be
+ // composited with a fully opaque backdrop whose colour is everywhere defined
// by the soft-mask dictionary's BC entry."
g.setBackground(new Color(backdropColor.toRGB()));
g.clearRect(0, 0, width, height);
@@ -2103,7 +2145,7 @@ private TransparencyGroup(PDTransparencyGroup form, boolean isSoftMask, Matrix c
((GroupGraphics) graphics).removeBackdrop(backdropImage, backdropX, backdropY);
}
}
- finally
+ finally
{
flipTG = savedFlipTG;
lastClips = savedLastClips;
@@ -2118,7 +2160,7 @@ private TransparencyGroup(PDTransparencyGroup form, boolean isSoftMask, Matrix c
}
// http://stackoverflow.com/a/21181943/535646
- private BufferedImage create2ByteGrayAlphaImage(int width, int height)
+ private BufferedImage create2ByteGrayAlphaImage(int width, int height)
{
// gray + alpha
int[] bandOffsets = {1, 0};
@@ -2339,7 +2381,7 @@ private boolean isHiddenOCMD(PDOptionalContentMembershipDictionary ocmd)
List
+ *
+ *
+ * @author Apache PDFBox
+ */
+class GraphicsState
+{
+ /** Round to grid - the default round state. */
+ public static final int ROUND_TO_GRID = 0;
+ /** Round to half grid. */
+ public static final int ROUND_TO_HALF_GRID = 1;
+ /** Round to double grid. */
+ public static final int ROUND_TO_DOUBLE_GRID = 2;
+ /** Round down to grid. */
+ public static final int ROUND_DOWN_TO_GRID = 3;
+ /** Round up to grid. */
+ public static final int ROUND_UP_TO_GRID = 4;
+ /** Rounding off. */
+ public static final int ROUND_OFF = 5;
+ /** Super round (set by SROUND). */
+ public static final int ROUND_SUPER = 6;
+ /** Super round 45 degrees (set by S45ROUND). */
+ public static final int ROUND_SUPER_45 = 7;
+
+ private UnitVector projectionVector;
+ private UnitVector freedomVector;
+ private UnitVector dualProjectionVector;
+
+ private int rp0;
+ private int rp1;
+ private int rp2;
+
+ private int zp0;
+ private int zp1;
+ private int zp2;
+
+ private int loop;
+ private int roundState;
+ // derived rounding parameters (all F26Dot6), configured from the round state
+ private int roundPeriod;
+ private int roundPhase;
+ private int roundThreshold;
+ private boolean roundOff;
+
+ private int minimumDistance;
+ private int controlValueCutIn;
+ private int singleWidthCutIn;
+ private int singleWidthValue;
+ private int deltaBase;
+ private int deltaShift;
+ private boolean autoFlip;
+ private int scanControl;
+ private int scanType;
+ private int instructControl;
+
+ /**
+ * Creates a graphics state with the spec-mandated default values. This is the state before the
+ * font's {@code fpgm}/{@code prep} programs run.
+ */
+ public GraphicsState()
+ {
+ projectionVector = UnitVector.xAxis();
+ freedomVector = UnitVector.xAxis();
+ dualProjectionVector = UnitVector.xAxis();
+ rp0 = 0;
+ rp1 = 0;
+ rp2 = 0;
+ zp0 = 1;
+ zp1 = 1;
+ zp2 = 1;
+ loop = 1;
+ setRoundState(ROUND_TO_GRID);
+ minimumDistance = Fixed.ONE; // 1 pixel
+ controlValueCutIn = 17 * Fixed.ONE / 16; // 17/16 pixel = 68
+ singleWidthCutIn = 0;
+ singleWidthValue = 0;
+ deltaBase = 9;
+ deltaShift = 3;
+ autoFlip = true;
+ scanControl = 0;
+ scanType = 0;
+ instructControl = 0;
+ }
+
+ private GraphicsState(GraphicsState src)
+ {
+ projectionVector = src.projectionVector.copy();
+ freedomVector = src.freedomVector.copy();
+ dualProjectionVector = src.dualProjectionVector.copy();
+ rp0 = src.rp0;
+ rp1 = src.rp1;
+ rp2 = src.rp2;
+ zp0 = src.zp0;
+ zp1 = src.zp1;
+ zp2 = src.zp2;
+ loop = src.loop;
+ roundState = src.roundState;
+ roundPeriod = src.roundPeriod;
+ roundPhase = src.roundPhase;
+ roundThreshold = src.roundThreshold;
+ roundOff = src.roundOff;
+ minimumDistance = src.minimumDistance;
+ controlValueCutIn = src.controlValueCutIn;
+ singleWidthCutIn = src.singleWidthCutIn;
+ singleWidthValue = src.singleWidthValue;
+ deltaBase = src.deltaBase;
+ deltaShift = src.deltaShift;
+ autoFlip = src.autoFlip;
+ scanControl = src.scanControl;
+ scanType = src.scanType;
+ instructControl = src.instructControl;
+ }
+
+ /**
+ * Returns an independent deep copy, with the {@link UnitVector} fields cloned rather than shared.
+ *
+ * @return a deep copy of this graphics state
+ */
+ public GraphicsState copy()
+ {
+ return new GraphicsState(this);
+ }
+
+ /**
+ * Resets the per-glyph graphics state fields to their defaults, as the TrueType spec requires at
+ * the start of each glyph's instruction stream. The projection, freedom and dual-projection
+ * vectors return to the x axis; the reference points reset to 0; the zone pointers reset to the
+ * glyph zone (1); the loop counter resets to 1. Fields configured by {@code prep} (round state,
+ * cut-ins, minimum distance, delta base/shift, single width, auto-flip, scan control) are left
+ * untouched.
+ */
+ public void resetForGlyph()
+ {
+ projectionVector.set(Fixed.ONE_F2DOT14, 0);
+ freedomVector.set(Fixed.ONE_F2DOT14, 0);
+ dualProjectionVector.set(Fixed.ONE_F2DOT14, 0);
+ rp0 = 0;
+ rp1 = 0;
+ rp2 = 0;
+ zp0 = 1;
+ zp1 = 1;
+ zp2 = 1;
+ loop = 1;
+ }
+
+ /** @return the projection vector */
+ public UnitVector getProjectionVector()
+ {
+ return projectionVector;
+ }
+
+ /** @return the freedom vector */
+ public UnitVector getFreedomVector()
+ {
+ return freedomVector;
+ }
+
+ /** @return the dual projection vector */
+ public UnitVector getDualProjectionVector()
+ {
+ return dualProjectionVector;
+ }
+
+ /** @return reference point 0 */
+ public int getRp0()
+ {
+ return rp0;
+ }
+
+ /** @param value reference point 0 */
+ public void setRp0(int value)
+ {
+ rp0 = value;
+ }
+
+ /** @return reference point 1 */
+ public int getRp1()
+ {
+ return rp1;
+ }
+
+ /** @param value reference point 1 */
+ public void setRp1(int value)
+ {
+ rp1 = value;
+ }
+
+ /** @return reference point 2 */
+ public int getRp2()
+ {
+ return rp2;
+ }
+
+ /** @param value reference point 2 */
+ public void setRp2(int value)
+ {
+ rp2 = value;
+ }
+
+ /** @return zone pointer 0 */
+ public int getZp0()
+ {
+ return zp0;
+ }
+
+ /** @param value zone pointer 0 */
+ public void setZp0(int value)
+ {
+ zp0 = value;
+ }
+
+ /** @return zone pointer 1 */
+ public int getZp1()
+ {
+ return zp1;
+ }
+
+ /** @param value zone pointer 1 */
+ public void setZp1(int value)
+ {
+ zp1 = value;
+ }
+
+ /** @return zone pointer 2 */
+ public int getZp2()
+ {
+ return zp2;
+ }
+
+ /** @param value zone pointer 2 */
+ public void setZp2(int value)
+ {
+ zp2 = value;
+ }
+
+ /** @return the loop counter */
+ public int getLoop()
+ {
+ return loop;
+ }
+
+ /** @param value the loop counter */
+ public void setLoop(int value)
+ {
+ loop = value;
+ }
+
+ /** @return the round state */
+ public int getRoundState()
+ {
+ return roundState;
+ }
+
+ /**
+ * Sets the round state and derives the period/phase/threshold the {@link #round(int)} engine uses.
+ * The simple states are expressed as special cases of the super-round parameters. {@code SROUND}
+ * and {@code S45ROUND} call {@link #setSuperRound(int, int)} instead.
+ *
+ * @param value one of the {@code ROUND_*} constants
+ */
+ public void setRoundState(int value)
+ {
+ roundState = value;
+ roundOff = false;
+ switch (value)
+ {
+ case ROUND_TO_GRID:
+ roundPeriod = Fixed.ONE;
+ roundPhase = 0;
+ roundThreshold = Fixed.HALF;
+ break;
+ case ROUND_TO_HALF_GRID:
+ roundPeriod = Fixed.ONE;
+ roundPhase = Fixed.HALF;
+ roundThreshold = Fixed.HALF;
+ break;
+ case ROUND_TO_DOUBLE_GRID:
+ roundPeriod = Fixed.HALF;
+ roundPhase = 0;
+ roundThreshold = Fixed.HALF / 2;
+ break;
+ case ROUND_DOWN_TO_GRID:
+ roundPeriod = Fixed.ONE;
+ roundPhase = 0;
+ roundThreshold = 0;
+ break;
+ case ROUND_UP_TO_GRID:
+ roundPeriod = Fixed.ONE;
+ roundPhase = 0;
+ roundThreshold = Fixed.ONE - 1;
+ break;
+ case ROUND_OFF:
+ roundOff = true;
+ break;
+ default:
+ // ROUND_SUPER / ROUND_SUPER_45 are configured by setSuperRound
+ break;
+ }
+ }
+
+ /**
+ * Configures super-round parameters for {@code SROUND}/{@code S45ROUND} from the selector byte,
+ * per the TrueType specification.
+ *
+ * @param gridPeriod the base grid period in F26Dot6 (one pixel for SROUND; the diagonal for
+ * S45ROUND)
+ * @param selector the operand byte controlling period, phase and threshold
+ */
+ public void setSuperRound(int gridPeriod, int selector)
+ {
+ switch (selector & 0xC0)
+ {
+ case 0x00:
+ roundPeriod = gridPeriod / 2;
+ break;
+ case 0x80:
+ roundPeriod = gridPeriod * 2;
+ break;
+ default:
+ roundPeriod = gridPeriod;
+ break;
+ }
+ if (roundPeriod < 1)
+ {
+ roundPeriod = 1;
+ }
+ switch (selector & 0x30)
+ {
+ case 0x00:
+ roundPhase = 0;
+ break;
+ case 0x10:
+ roundPhase = roundPeriod / 4;
+ break;
+ case 0x20:
+ roundPhase = roundPeriod / 2;
+ break;
+ default:
+ roundPhase = roundPeriod * 3 / 4;
+ break;
+ }
+ int n = selector & 0x0F;
+ roundThreshold = n == 0 ? roundPeriod - 1 : (n - 4) * roundPeriod / 8;
+ roundState = ROUND_SUPER;
+ roundOff = false;
+ }
+
+ /**
+ * Rounds a distance according to the current round state. Engine compensation (the black/white/
+ * grey distance bias FreeType applies) is treated as zero, which is correct for an anti-aliased
+ * Java2D target.
+ *
+ * @param distance the distance in F26Dot6
+ * @return the rounded distance in F26Dot6
+ */
+ public int round(int distance)
+ {
+ if (roundOff)
+ {
+ return distance;
+ }
+ int val;
+ if (distance >= 0)
+ {
+ val = Math.floorDiv(distance - roundPhase + roundThreshold, roundPeriod) * roundPeriod;
+ if (val < 0)
+ {
+ val = 0;
+ }
+ val += roundPhase;
+ }
+ else
+ {
+ val = -(Math.floorDiv(roundThreshold - roundPhase - distance, roundPeriod) * roundPeriod);
+ if (val > 0)
+ {
+ val = 0;
+ }
+ val -= roundPhase;
+ }
+ return val;
+ }
+
+ /** @return the minimum distance in F26Dot6 */
+ public int getMinimumDistance()
+ {
+ return minimumDistance;
+ }
+
+ /** @param value the minimum distance in F26Dot6 */
+ public void setMinimumDistance(int value)
+ {
+ minimumDistance = value;
+ }
+
+ /** @return the control value cut-in in F26Dot6 */
+ public int getControlValueCutIn()
+ {
+ return controlValueCutIn;
+ }
+
+ /** @param value the control value cut-in in F26Dot6 */
+ public void setControlValueCutIn(int value)
+ {
+ controlValueCutIn = value;
+ }
+
+ /** @return the single width cut-in in F26Dot6 */
+ public int getSingleWidthCutIn()
+ {
+ return singleWidthCutIn;
+ }
+
+ /** @param value the single width cut-in in F26Dot6 */
+ public void setSingleWidthCutIn(int value)
+ {
+ singleWidthCutIn = value;
+ }
+
+ /** @return the single width value in F26Dot6 */
+ public int getSingleWidthValue()
+ {
+ return singleWidthValue;
+ }
+
+ /** @param value the single width value in F26Dot6 */
+ public void setSingleWidthValue(int value)
+ {
+ singleWidthValue = value;
+ }
+
+ /** @return the delta base */
+ public int getDeltaBase()
+ {
+ return deltaBase;
+ }
+
+ /** @param value the delta base */
+ public void setDeltaBase(int value)
+ {
+ deltaBase = value;
+ }
+
+ /** @return the delta shift */
+ public int getDeltaShift()
+ {
+ return deltaShift;
+ }
+
+ /** @param value the delta shift */
+ public void setDeltaShift(int value)
+ {
+ deltaShift = value;
+ }
+
+ /** @return whether auto-flip is enabled */
+ public boolean isAutoFlip()
+ {
+ return autoFlip;
+ }
+
+ /** @param value whether auto-flip is enabled */
+ public void setAutoFlip(boolean value)
+ {
+ autoFlip = value;
+ }
+
+ /** @return the scan control flags */
+ public int getScanControl()
+ {
+ return scanControl;
+ }
+
+ /** @param value the scan control flags */
+ public void setScanControl(int value)
+ {
+ scanControl = value;
+ }
+
+ /** @return the scan type */
+ public int getScanType()
+ {
+ return scanType;
+ }
+
+ /** @param value the scan type */
+ public void setScanType(int value)
+ {
+ scanType = value;
+ }
+
+ /** @return the instruction control flags */
+ public int getInstructControl()
+ {
+ return instructControl;
+ }
+
+ /** @param value the instruction control flags */
+ public void setInstructControl(int value)
+ {
+ instructControl = value;
+ }
+}
diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/HintingException.java b/fontbox/src/main/java/org/apache/fontbox/ttf/HintingException.java
new file mode 100644
index 00000000000..6ace71dc70e
--- /dev/null
+++ b/fontbox/src/main/java/org/apache/fontbox/ttf/HintingException.java
@@ -0,0 +1,46 @@
+/*
+ * 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.fontbox.ttf;
+
+/**
+ * Thrown when the TrueType bytecode interpreter encounters a malformed or unsupported program. It is
+ * unchecked so that opcode handlers stay terse; the caller catches it per glyph and falls back to raw,
+ * unhinted coordinates rather than letting one bad glyph disable hinting for the whole font.
+ *
+ * @author Apache PDFBox
+ */
+class HintingException extends RuntimeException
+{
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * @param message describes the failure
+ */
+ public HintingException(String message)
+ {
+ super(message);
+ }
+
+ /**
+ * @param message describes the failure
+ * @param cause the underlying cause
+ */
+ public HintingException(String message, Throwable cause)
+ {
+ super(message, cause);
+ }
+}
diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/TTFParser.java b/fontbox/src/main/java/org/apache/fontbox/ttf/TTFParser.java
index 69a5a7e2c10..5a8de6d5fff 100644
--- a/fontbox/src/main/java/org/apache/fontbox/ttf/TTFParser.java
+++ b/fontbox/src/main/java/org/apache/fontbox/ttf/TTFParser.java
@@ -377,6 +377,18 @@ private TTFTable readTableDirectory(TTFDataStream raf) throws IOException
case GlyphSubstitutionTable.TAG:
table = new GlyphSubstitutionTable();
break;
+ case ControlValueTable.TAG:
+ table = new ControlValueTable();
+ break;
+ case FontProgramTable.TAG:
+ table = new FontProgramTable();
+ break;
+ case ControlValueProgramTable.TAG:
+ table = new ControlValueProgramTable();
+ break;
+ case GaspTable.TAG:
+ table = new GaspTable();
+ break;
default:
table = readTable(tag);
break;
diff --git a/fontbox/src/main/java/org/apache/fontbox/ttf/TrueTypeFont.java b/fontbox/src/main/java/org/apache/fontbox/ttf/TrueTypeFont.java
index 90ecd9ece29..07ff1a00130 100644
--- a/fontbox/src/main/java/org/apache/fontbox/ttf/TrueTypeFont.java
+++ b/fontbox/src/main/java/org/apache/fontbox/ttf/TrueTypeFont.java
@@ -54,7 +54,9 @@ public class TrueTypeFont implements FontBoxFont, Closeable
private final Object lockReadtable = new Object();
private final Object lockPSNames = new Object();
+ private final Object lockHinter = new Object();
private final List
+ * mvn -pl fontbox test -Dtest=GlyphTraceTool \
+ * -Dtrace.font=src/test/resources/ttf/LiberationSans-Regular.ttf \
+ * -Dtrace.gid=22 -Dtrace.ppem=11 -Dtrace.out=/tmp/our-trace.txt
+ *
+ */
+class GlyphTraceTool
+{
+ @Test
+ void dumpTrace() throws Exception
+ {
+ String gidProp = System.getProperty("trace.gid");
+ Assumptions.assumeTrue(gidProp != null, "set -Dtrace.gid to dump a trace");
+
+ int gid = Integer.parseInt(gidProp);
+ int ppem = Integer.parseInt(System.getProperty("trace.ppem", "11"));
+ int point = Integer.parseInt(System.getProperty("trace.point", "-1"));
+ String fontPath = System.getProperty("trace.font",
+ "src/test/resources/ttf/LiberationSans-Regular.ttf");
+ String outPath = System.getProperty("trace.out");
+
+ TrueTypeFont font;
+ try (InputStream is = new java.io.FileInputStream(fontPath))
+ {
+ // isEmbedded=true tolerates subset fonts that drop the otherwise-mandatory 'post' table
+ font = new TTFParser(true).parse(new RandomAccessReadBuffer(is));
+ }
+
+ PrintStream out = outPath != null ? new PrintStream(outPath, "UTF-8") : System.out;
+ try
+ {
+ new GlyphHinter(font).traceGlyph(gid, ppem, out, point);
+ }
+ finally
+ {
+ if (outPath != null)
+ {
+ out.close();
+ }
+ }
+ }
+}
diff --git a/fontbox/src/test/java/org/apache/fontbox/ttf/GoldenHintingTest.java b/fontbox/src/test/java/org/apache/fontbox/ttf/GoldenHintingTest.java
new file mode 100644
index 00000000000..dce85c4ff6f
--- /dev/null
+++ b/fontbox/src/test/java/org/apache/fontbox/ttf/GoldenHintingTest.java
@@ -0,0 +1,233 @@
+/*
+ * 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.fontbox.ttf;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.pdfbox.io.RandomAccessReadBuffer;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Golden (Tier 3) test comparing the FontBox interpreter's grid-fitted glyph points against a FreeType
+ * reference dump (same font, glyph and ppem). The reference lives in {@code ttf/hinting/-
+ *
+ * Coordinates are integer F26Dot6 (64 units per pixel).
+ */
+class GoldenHintingTest
+{
+ private static final int[] PPEMS = { 11, 13, 16, 24 };
+
+ /**
+ * Horizontal grid-fitting is an exact match to FreeType's grayscale (v40) output: with backward
+ * compatibility, x moves are suppressed so horizontal stems keep their sub-pixel position and are not
+ * darkened by antialiasing. Every x coordinate agrees to within 1/64 px for simple and composite
+ * glyphs alike.
+ */
+ @Test
+ void testHorizontalGridFittingMatchesFreeType() throws IOException
+ {
+ Stats simple = compare(false);
+ Stats composite = compare(true);
+ assertTrue(simple.maxDx <= 1, simple.summary("simple"));
+ assertTrue(composite.maxDx <= 1, composite.summary("composite"));
+ }
+
+ /**
+ * Vertical hinting snaps the glyph to the pixel grid without collapsing it: the hinted y bounding box
+ * matches FreeType within one pixel (so the baseline and cap/x-height land on grid rows, and a
+ * degenerate outline - the symptom of the twilight-zone IP bug - would be caught), and the majority of
+ * interior y coordinates stay within 1/64 px of FreeType.
+ */
+ @Test
+ void testVerticalHintingTracksFreeTypeWithoutCollapse() throws IOException
+ {
+ Stats simple = compare(false);
+ Stats composite = compare(true);
+ assertTrue(simple.maxBboxYDelta <= 64, simple.summary("simple"));
+ assertTrue(composite.maxBboxYDelta <= 64, composite.summary("composite"));
+ assertTrue(simple.yWithinOnePercent() >= 50, simple.summary("simple"));
+ assertTrue(composite.yWithinOnePercent() >= 50, composite.summary("composite"));
+ }
+
+ private Stats compare(boolean composite) throws IOException
+ {
+ TrueTypeFont font;
+ try (InputStream is = getClass().getResourceAsStream("/ttf/LiberationSans-Regular.ttf"))
+ {
+ font = new TTFParser().parse(new RandomAccessReadBuffer(is));
+ }
+ GlyphHinter hinter = new GlyphHinter(font);
+ Stats s = new Stats();
+
+ for (int ppem : PPEMS)
+ {
+ List