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 |
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. |
|
Push changes to address the review comment for github and fix for genko_oc_shiryo1.pdf bolding. PDFJS-1732.pdf is a different beast, hinting shouldn't be applied on this one, still looking for a why. |
|
Is this referring to a file in the FreeType distribution? |
|
I've touched up that wording. FreeType uses "oracle" quite a bit when comparing to a golden set. I've renamed it "reference" to make it more clear. I've also pulled out references to my local notes I used to build the plan for this. I've been testing withe the sneaky changes on/off and I think it will be worth while figuring out how to handle the licensing as it's a real value add against my test set. |
|
Please merge the latest changes from the trunk, I have removed the trailing spaces so that the actual change is better visible. Here's the latest copilot comments: Blocking review comments BytecodeStream.java @@
Other important findings TrueTypeInterpreterTest.java @@
|
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.
head.lowestRecPPEM is the vendor's "smallest readable size in pixels" for the outlines. Fonts that ship bitmap strikes for small sizes set it above the sizes the strikes cover - MS Mincho/Gothic, MingLiU and PMingLiU say 25, the Ricoh HG* family 28, Founder FZ* 20 - and their instructions, written for black-and-white output, force every thin stroke to a full pixel when run under grayscale at those sizes; PDF producers strip the strikes, so the text renders noticeably heavier than unhinted. Text fonts sit at 6-9 (Times, Arial, Liberation, DejaVu, Noto), so the gate never fires for them. Verified against FreeType: our hinted output for MS Mincho at 14ppem has the same ink as FreeType's grayscale interpreter (1478 vs 1481, unhinted 1147), so the weight is inherent to the bytecode, not an interpreter fault; the field is simply the vendor telling us not to run it there. A gasp "GRIDFIT without DOGRAY" gate was considered and rejected: Arial and Liberation flag 9-17ppem that way, so it would switch hinting off for Latin body text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… F2Dot14 Two mismatches against FreeType's ttinterp.c found while re-checking the rounding paths for a review: ODD and EVEN rounded their operand with plain round-to-grid regardless of the round state. The spec, and FreeType's Ins_ODD/Ins_EVEN, round with the current round state and then test the parity of the resulting pixel ((v & 127) == 64 / == 0), so under RTHG, RDTG, RUTG, SROUND or ROFF the result could differ. SROUND/S45ROUND derived period, phase and threshold directly in F26Dot6. FreeType derives them in F2Dot14 from the grid period (0x4000 for SROUND, 0x2D41 = sqrt(2)/2 for S45ROUND) and shifts afterwards, which makes the S45ROUND "period - 1" threshold (0x2D41 - 1) >> 8 = 45 rather than 44. setSuperRound now takes the F2Dot14 grid period and mirrors SetSuperRound. The other modes (RTG, RTHG, RTDG, RDTG, RUTG, ROFF, super) were checked line by line against Round_To_Grid & co. and match, including the negative distance mirror and the zero clamp; RoundStateTest now pins those, the grid-value fixed points and the S45 parameters. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
FreeType's Ins_SHPIX, under TT_SUPPORT_SUBPIXEL_HINTING_MINIMAL with backward compatibility on, treats a glyph-zone SHPIX like DELTAP: the point is moved (in y only) if it is already touched in y - or the glyph is a composite and the freedom vector has a y component - and IUP has not yet run on both axes; twilight-zone points always move. Ours ran SHPIX through the plain move, so before IUP it nudged untouched points off their interpolated positions. FreeType's comment cites older Rokkitt and DTL Argo glitching without this. doShpix now shares deltaPointAllowed() with DELTAP. BackwardCompatibilityMoveTest pins the movement rules directly on ExecutionContext.movePoint (a transcription of FreeType's Direct_Move): flag off is the plain move; flag on suppresses x but still touches, allows y until IUP has run on both axes, gates a diagonal freedom vector per axis; and the new SHPIX behaviour. The golden comparison against FreeType's grayscale output still passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Hardening of the interpreter's control flow, prompted by a review; every change fails toward "glyph renders unhinted" rather than toward a wrong outline, and each matches FreeType's behaviour: - A function body now runs bounded by its own ENDF (FunctionDef records the end; BytecodeStream.setEnd). A jump that leaves the body is an error, as IP > Def->end is in FreeType's JMPR. - ENDF outside a function body is an error (ENDF_In_Exec_Stream) instead of silently ending the program. - A nested FDEF/IDEF inside a body is an error (Nested_DEFS). - CINDEX/MINDEX with an out-of-range index consume the index and leave the stack alone, as FreeType does outside pedantic mode, instead of throwing NegativeArraySizeException. MINDEX is rewritten to read as "pop the k-1 above, pop the moved one, push back", with no tmp[k-1] arithmetic. Tests written from FreeType's semantics rather than from this code: MINDEX on the whole stack for k = 1, 2, 3 and 5 (k=2 is SWAP, k=3 is ROLL); CINDEX; ROLL's full permutation; forward JMPR, a backward-JROT countdown loop and JROF both ways; nested IF/ELSE whose skipped branches contain push data equal to the IF/ELSE/EIF opcode bytes; a body containing ENDF as push data, a function calling another (return flag must not leak), a recursive function; and the three new errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…transforms Tests only. hintingPpem takes the magnitude of the transform's vertical basis vector, so it is rotation-invariant; a review asked for that to be shown rather than argued. The new cases use the transform PageDrawer really hands over (device DPI scale and y flip, composed with the text rendering matrix and the 1/1000 font matrix): 7pt at 300dpi is 29, 10.5pt at 96dpi is 14, unchanged under 0/30/45/90/180/270/-90 degree rotation; Tz 50% and 200% leave the ppem alone while a 1.5x vertical stretch changes it; and fractional device sizes round half-up like a 26.6 size in FreeType (18.64 -> 19, 12.5 -> 13, 12.49 -> 12, 0.49 -> 0). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
59b0a8f to
b11f250
Compare
|
OK thanks and I'll start taking a look at those comments. |
Fixes from a review of the interpreter against FreeType, then from a point-by-point comparison on nine fonts (Liberation Sans/Serif, DejaVu Sans Bold, Arial, Times, Courier New, Georgia, Andale Mono, Comic Sans; printable ASCII plus accented letters at 9-32 ppem). Every coordinate now matches FreeType 2.13.2 exactly, and GoldenHintingTest asserts that. Review findings: - INSTCTRL follows Ins_INSTCTRL: selectors 1-3 set or clear one bit, only in prep (selector 3 also per glyph). GlyphHinter honours the prep flags: bit 1 disables hinting, bit 2 starts glyphs from the default graphics state, bit 4 waives backward compatibility. ExecutionContext gains a CodeRange to tell fpgm, prep and glyph programs apart. - A prep that throws no longer leaves stale CVT/storage behind a cached ppem. - SPVTL sets the dual vector equal to the projection vector. - SxVTL takes the top point from zp2 and the next from zp1, with the line running from the zp2 point; a zero-length line gives the x axis even when perpendicular (and SDPVTL keeps FreeType's dropped-flag quirk). - MIRP and MSIRP place twilight points from rp0's original position (MSIRP via the new moveOriginal); MIAP places them along the freedom vector. Comparison findings: - ROUND_XY_TO_GRID rounds a component's y offset (v40 leaves x). Accents sat up to half a pixel low. - MDRP measures the original distance in font units and scales it (orus), so a 31.5/64 distance rounds to a pixel, not to zero. - IUP interpolates on font units with FT_DivFix/FT_MulFix and orders the references by them; under backward compatibility a third IUP is a no-op. - A composite's program sees the assembled hinted points as its font units, at scale 1. - Simple glyphs whose lsb differs from xMin are hinted unshifted and translated by -pp1.x afterwards, as FreeType does, rather than shifted in font units before scaling. - UnitVector.normalize ports FT_Vector_NormLen; SPVFS/SFVFS sign-extend and normalize; moves use FreeType 2.13's F_dot_P with FT_MulDiv. Tooling: HintedPointsDumpTool, ft_points_dump.py and compare_points.py compare hinted points against FreeType for any local fonts, optionally against a baseline build. ft_point_trace.c now traces the grayscale target by default (it used mono, which disables backward compatibility), with "mono" as an option. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rget Review follow-ups: - BytecodeStream.nextByte/nextWord check the function body's end, as seek already did. Scanning a body skips push operands, so only a jump into the middle of a push's operands could make a push read the ENDF and the caller's next byte; reads never left the code array. Such bytecode now falls back to the unhinted outline. New test: testFunctionBodyCannotReadOperandsPastEndf. - The README states the reference contract: FreeType 2.13.2 with FT_LOAD_NO_AUTOHINT | FT_LOAD_TARGET_NORMAL (v40, grayscale, backward compatibility), used by every script. trace_diff.py loaded with TARGET_MONO and now uses the grayscale target; the generate_golden.py docstring said monochrome while its code used grayscale. - GoldenHintingTest requires every golden composite to carry instructions of its own, so its exact match covers the composite program and not only the component programs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Copilot writes this: fontbox/src/main/java/org/apache/fontbox/ttf/Fixed.java:83-85 — ceil() is incorrect for negative non-integral values. The implementation (value + 63) & ~63 only behaves like a ceiling for some negative inputs. For example, ceil(-65) returns -128, but the mathematical ceiling in F26Dot6 should be -64. Since the interpreter exposes this through the TrueType CEILING opcode, fonts using CEILING with negative distances will receive a value one pixel too small, potentially shifting hinted points and causing incorrect outlines. Please implement ceiling explicitly for signed values and add negative-value tests, e.g. ceil(-1) == 0, ceil(-63) == 0, ceil(-65) == -64, and ceil(-128) == -128. I have no idea if this is realistic, i.e. if such values can occur (I tried with the regression tests and found nothing). Also this part of the code was there before so I wonder why copilot didn't complain until now. Btw fontbox has not only some more ttf fonts in the repository (I see you did some tests with them), it also loads some ttf fonts during the tests. There's a dejavu font (you mentioned the font in a commit message), plus Keyboard.ttf, NotoEmoji-Regular.ttf, NotoMono-Regular.ttf, ipag.ttf. They are in the directory target/fonts. |
…oke test Review follow-ups: - The execution budget used FreeType's glyph formula for every program, so fpgm/prep (no glyph points) got only 100 LOOPCALL iterations. FreeType's TT_RunIns gives them 300 + 22 * cvtSize. Keyboard.ttf's prep loops 240 times, so hinting failed for the whole font. Test: testPrepLoopCallBudgetIsSizedFromTheControlValues. - HintingSmokeTest hints every glyph of each TrueType test font (the bundled ones and those the build downloads to target/fonts) at 9-48 ppem and requires that none fails; it catches the Keyboard.ttf case. GlyphHinter counts failures (getFailureCount/getFirstFailure) so a failure can be told apart from a glyph that is legitimately not hinted. - Fixed.ceil is already a true ceiling for negative values ((v + 63) & ~63, FreeType's FT_PIX_CEIL_LONG); FixedTest now pins negative floor/ceil. - Javadoc of GlyphHinter and TrueTypeFont.getHintedPath no longer says composite glyphs are not hinted, and names the PDFRenderer switch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
I've been testing with sample.pdf from the PDFBOX-3293 ticket and it's a good example of "tricky" vs. "No tricky". Render fine when enabled and skips hinting without it. I'll take another look at finding another way to detect these fonts. |
|
Thank you; please change GlyphHinter line 232 from " catch (IOException | RuntimeException e)" to "catch (IOException | HintingException e)", we want to know the hard way if something really nasty happens. I have looked at the test coverage, there are parts of TrueTypeInterpreter that aren't covered so I ran my visual tests to see if any of the missing parts were hit. Only two were, flipRange and NPUSHW, and only with microsoft fonts, e.g. with this code: So we can't really test this on the CI, but locally with people who have windows (me). Could you add some code like this that checks that the result is what is expected? (I'm not expecting 100% test coverage, our code is only about 60%, this is just about improving when it's possible) |



PDFBOX-3293