Conversation
6c33bd6 to
933566a
Compare
|
Thank you, very interesting. From having a quick look I didn't see any breaking changes. I will set up some testing to see how the rendering is going. You don't need to include the liberation font, it's already there, and is used in existing tests: getResourceAsStream("/org/apache/pdfbox/resources/ttf/LiberationSans-Regular.ttf")); |
|
I'll get that font path touched up. |
|
I did a fast (human) visual compare by scrolling through the diff files. Besides the one chinese file that the issue is about, many were slightly different but the few where I had a closer look didn't seem to have any problem, one was slightly better, the horizontal bar in an "e" was more in the middle than without hinting. The rendering with hinting was faster (which is surprising, we'll see if this keeps happening) One difference has less quality: file: PDFJS-1732.pdf new rendering at 96 dpi (from the third notes line): another file also caught my attention: Another small thing: IMHO the enabling of the hinting feature should be in PDFRenderer, not in TrueType. This can be done like with |
|
Copilot comment: High-risk issues I found
The round-state implementation is central to many opcodes. I’d specifically re-check: Why it’s risky: This one isn’t obviously broken, but it is critical enough that I’d want more proof against FreeType for edge values and negative distances. These lines stand out: The likely issue is the exact definition of ODD/EVEN in TT bytecode. They depend on the rounded distance and parity at the 1/64 pixel level, and I’d want to verify the exact threshold behavior. This may already be covered by tests, but I’d still flag it as worth a deeper look. Current implementation: That is not obviously the standard TrueType ROLL behavior unless the stack order matches a specific convention. I’d double-check against the spec. ROLL is one of those stack-manipulation ops that can be silently wrong and then corrupt later execution. The implementation pops k, then pops k values, then pushes them back in a rearranged order. That logic is a common place for off-by-one or order bugs. I would explicitly test: I don’t think this should block the PR on its own, but it deserves scrutiny. This is one of the most important areas. Concern points: Potential issues: This is exactly the sort of logic that can work for tested fonts but fail on odd control-flow constructs. The function-definition model stores: Then later replays from the stored program using callBody(). Things I’d verify carefully: This area is conceptually sound, but it’s one of the easiest places for “it works for Liberation Sans” but not for other fonts. This is the riskiest logic in the whole patch. The method now suppresses movement in certain cases when: That may be correct for the intended render mode, but it is a policy layer mixed into the VM’s primitive point-move operation. Risks: If this is intended to emulate FreeType’s grayscale compatibility mode, I’d strongly suggest isolating it more explicitly so the core point move semantics stay clean. The code computes ppem from the transform’s vertical basis vector magnitude. That’s clever, but because hinting is sensitive to device resolution and transform composition, I’d want: You added some tests, which is good, but this is still a place where “looks right” can hide bugs. If I had to prioritize, I’d focus on: Those are the areas most likely to produce subtle regressions. |
|
I can't comment on the copilot comments because I didn't look at the new code yet (e.g. because I have no knowledge whatsoever about hinting, the only thing I know is that it adjusts the points to a grid). I mostly looked at changes in the existing code and it looked like the new feature peacefully coexist, which is nice. |
|
I'll start working through those copilot comments. Performance is an interesting one. On my test suite I also see no penalty for having hinting enabled. The caching works really well for most fonts. I referenced pdf.js on my initial research as I thought it would be a good reference. As I dug in though it turned out their hinting was coming from a native system library not from JavaScript. The two sample files you posted both fall into the "tricky" fonts category. FreeType has identified a list of fonts that need to be handled as special case. Differently being they need the bytecode to assembled/scale the glpyhs on every size. FreeTypes license is pretty flexible so I suspect it can be tied back in but it will take some legal work to properly update the LICENSE and NOTICE files. Pushed changes for your two comments. |
|
Re Freetype license, it is not mentioned on |
Adds the four tables a bytecode interpreter needs, as ordinary TTFTable implementations registered with TTFParser and exposed from TrueTypeFont: cvt ControlValueTable control values in font units fpgm FontProgramTable the font program, run once per font prep ControlValueProgramTable the control value program, run per size gasp GaspTable per-ppem grid-fitting and smoothing flags Parsing only - nothing executes these yet, and no existing behaviour changes. GaspTable resolves the flags for a ppem the way the specification describes: the first range whose upper limit is at or above the requested size, with the implied final 0xFFFF range. Expected values in HintingTablesTest were read from LiberationSans with ttx, so the test pins the parse against an independent tool rather than against itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The virtual machine that executes TrueType hinting programs, package-private
to org.apache.fontbox.ttf: nothing outside the package can reach it, and it
is not exported from the OSGi bundle.
TrueTypeInterpreter the driver and the 256-entry dispatch table
ExecutionContext per-run state: operand stack, zones, call depth
GraphicsState vectors, reference and zone pointers, round state
Zone a set of points with current/original/unscaled
coordinates and per-axis touch flags
BytecodeStream a bounds-checked cursor over a program
Fixed F26Dot6 and F2Dot14 integer math
UnitVector the projection, freedom and dual-projection vectors
FunctionDef an FDEF entry point
HintingException any failure; callers fall back to the raw outline
ExecutionTracer optional per-instruction trace, off in normal use
All 173 opcodes are implemented. The 32-variant MDRP and MIRP families and
the other flag-encoded groups decode their flags from the low bits of the
opcode rather than being enumerated, so roughly a hundred dispatch slots are
served by a dozen handlers.
Everything stays in integer fixed point, matching FreeType, so output can be
compared against it exactly rather than approximately. Fixed reproduces
FT_MulDiv including its behaviour when the divisor is zero, and DIV
truncates rather than rounds as FT_MulDiv_No_Round does.
Two bounds keep a crafted font from running forever, both sized as
FreeType sizes them in TT_RunIns: backward jumps and cumulative LOOPCALL
iterations. A backward jump and LOOPCALL are the only ways TrueType
bytecode can loop, so bounding them bounds the program; a four-byte glyph
program otherwise spins indefinitely. CALL nesting is capped at 64 as
FreeType does.
The storage area and twilight zone belong to the size rather than to one
program run, as in FreeType's TT_Size: a font may compute values into them
in prep and read them back from every glyph program. Both are cleared when
the ppem changes, before prep runs, as tt_size_run_prep does.
Unit tests cover the opcodes individually, the rounding state machine, the
fixed-point math, the bytecode cursor, and graphics state defaults, deep
copy and per-glyph reset. Byte-exact agreement with FreeType is established
separately by the golden comparison.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GlyphHinter drives the interpreter for one font: it builds the VM from the
font's maxp, head, cvt, fpgm and prep tables, runs the font program once,
re-runs the control value program whenever the ppem changes, and for each
glyph scales the outline into the pixel grid, appends the phantom points,
executes the glyph's instructions and scales the result back to font units.
TrueTypeFont.getHintedPath(gid, ppem) is the entry point.
Hinting is off by default. TrueTypeFont.SYSPROP_HINTING
("org.apache.fontbox.ttf.hinting") or setHintingEnabled(boolean) turns it
on; while it is off getHintedPath returns null and callers use the raw
outline, so existing output is unchanged.
It is best-effort throughout. A composite whose components cannot be
resolved, a glyph with no instructions, a ppem the gasp table excludes, a
malformed program - each falls back to null for that glyph alone, never for
the rest of the font. The first failure in a font is logged with a stack
trace and the rest at debug level, so a font that never hints does not
flood the log.
Composites are assembled the way FreeType assembles them: each component is
hinted on its own, then transformed and offset into the composite's space,
with the assembled positions becoming the originals the composite's own
instructions measure against. Component offsets are not grid-rounded.
Grayscale rendering follows FreeType's v40 interpreter: movement in x is
suppressed and y is frozen once IUP has run on both axes, which is what
stops stems being darkened under antialiasing.
Every entry point is synchronized, so one font hints one glyph at a time.
That matters because a system-substituted font is held in a process-wide
cache and several rendering threads can share one instance.
Known limitation: FreeType exempts a short list of "tricky" fonts - mostly
CJK fonts that assemble glyphs from sub-pixel-sized components - from the
grayscale movement restrictions and from the gasp grid-fit bit, because
they are unreadable without full bytecode control. Identifying them
requires a lookup table, which is not included here; those fonts render
unhinted, exactly as they do today. Adding that support is a follow-up.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PDVectorFont gains getHintedNormalizedPath(code, ppem), defaulting to null so nothing changes for fonts that cannot hint. PDTrueTypeFont and PDCIDFontType2 implement it for embedded glyf outlines, normalising to the 1000-unit em square exactly as getNormalizedPath does; PDType0Font delegates to its descendant. PageDrawer derives the ppem from the glyph-space-to-device transform. The text rendering matrix alone maps glyph space to PDF user space, so the device transform is composed in first - otherwise a 7pt font would be grid-fit at 7 pixels per em rather than the 29 it is actually rendered at on a 300dpi raster. The ppem is the magnitude of the transform's vertical basis vector, which is rotation-invariant: a rotated glyph is grid-fit in its own upright space and the full transform applied afterwards, as FreeType does for vertical CJK text. GlyphCache keeps hinted paths under a (code, ppem) key, since a hinted outline is only valid at the size it was fitted for, and caches the fallback under the same key so a glyph that does not hint is not retried. The existing code-keyed cache and its path are untouched: while hinting is off PageDrawer takes that path and the feature costs nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Unit tests establish that each opcode does what the specification says. They cannot establish that a glyph comes out where FreeType puts it, which is the only definition of correct that matters here, so the output is compared against FreeType directly. ttf/hinting/ holds the harness: a small C program links against FreeType and dumps the grid-fitted points of a glyph, a Python script drives it to produce the reference files, and a second script diffs a per-instruction trace from this interpreter against the equivalent FreeType trace to localise a divergence to the instruction that caused it. The reference files are checked in so the tests need neither FreeType nor Python to run; the README records how to regenerate them, and the generator emits the license header so a regeneration does not drop it. GoldenHintingTest compares LiberationSans at 11, 13, 16 and 24 ppem against those references. Every x coordinate agrees to within one 64th of a pixel for simple and composite glyphs alike. Vertical positions are checked for grid alignment without collapse rather than for exact equality, since the y axis is where the v40 backward-compatibility rules deliberately diverge. The harness found six bugs that the unit tests could not: interpolation using scaled rather than unscaled originals, grid-rounded composite component offsets, SHP/SHC/SHZ moving the reference point, DIV rounding instead of truncating, swapped MDRP/MIRP round and minimum-distance flag bits, and composite component originals taken from the unhinted rather than the assembled outline. GlyphTraceTool is the developer entry point for the trace side; it is not a test and asserts nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…touch up respective tests with source of truth.
…ueTypeFont and a system property.
7b011f5 to
7c68966
Compare
|
I've pushed the hinting hookup in the debugger. I think I was was wrong on the "tricky" fonts on those two samples, will continue to investigate. I have some notes on the issue and need to revisit them. I'll get you those contributor agreements soon. |



PDFBOX-3293