Skip to content
4 changes: 4 additions & 0 deletions iosMath.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
490465BF1D23DA8400F82033 /* MTTypesetterTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 490465BE1D23DA8400F82033 /* MTTypesetterTest.m */; };
49A1B2C41D23DA8400F82033 /* MTInkWidthTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 49A1B2C31D23DA8400F82033 /* MTInkWidthTest.m */; };
B5F0B5C103FA9EF7992D11F9 /* MTItalicCorrectionTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EEA6FC9DE3C76B9C2E7DF10 /* MTItalicCorrectionTest.m */; };
490465C11D23DA8400F82033 /* MTFontManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 490465C01D23DA8400F82033 /* MTFontManagerTest.m */; };
492EED0817DAEDD200939107 /* MTFontManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 492EECFA17DAED9000939107 /* MTFontManager.m */; };
492EED0917DAEDD200939107 /* MTFontMathTable.m in Sources */ = {isa = PBXBuildFile; fileRef = 492EECF817DAED9000939107 /* MTFontMathTable.m */; };
Expand Down Expand Up @@ -78,6 +79,7 @@
/* Begin PBXFileReference section */
490465BE1D23DA8400F82033 /* MTTypesetterTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTTypesetterTest.m; sourceTree = "<group>"; };
49A1B2C31D23DA8400F82033 /* MTInkWidthTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTInkWidthTest.m; sourceTree = "<group>"; };
4EEA6FC9DE3C76B9C2E7DF10 /* MTItalicCorrectionTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTItalicCorrectionTest.m; sourceTree = "<group>"; };
490465C01D23DA8400F82033 /* MTFontManagerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MTFontManagerTest.m; sourceTree = "<group>"; };
492EECF317DAED9000939107 /* MTMathListDisplay.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTMathListDisplay.h; sourceTree = "<group>"; };
492EECF417DAED9000939107 /* MTMathUILabel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MTMathUILabel.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -259,6 +261,7 @@
49B83EF517CF046A0014B739 /* MTMathListTest.m */,
490465BE1D23DA8400F82033 /* MTTypesetterTest.m */,
49A1B2C31D23DA8400F82033 /* MTInkWidthTest.m */,
4EEA6FC9DE3C76B9C2E7DF10 /* MTItalicCorrectionTest.m */,
490465C01D23DA8400F82033 /* MTFontManagerTest.m */,
49965F2417CBBA2700A555C5 /* Supporting Files */,
);
Expand Down Expand Up @@ -538,6 +541,7 @@
498730A817D548190041B02B /* MTMathListTest.m in Sources */,
490465BF1D23DA8400F82033 /* MTTypesetterTest.m in Sources */,
49A1B2C41D23DA8400F82033 /* MTInkWidthTest.m in Sources */,
B5F0B5C103FA9EF7992D11F9 /* MTItalicCorrectionTest.m in Sources */,
490465C11D23DA8400F82033 /* MTFontManagerTest.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
145 changes: 137 additions & 8 deletions iosMath/render/internal/MTTypesetter.m
Original file line number Diff line number Diff line change
Expand Up @@ -990,9 +990,15 @@ - (void) createDisplayAtoms:(NSArray*) preprocessed
if (_currentLine.length > 0) {
if (interElementSpace > 0) {
// add a kerning of that space to the previous character
NSRange prev = [_currentLine.string rangeOfComposedCharacterSequenceAtIndex:_currentLine.length - 1];
// Additive: that character may already carry an italic
// correction, and assigning would drop it.
NSNumber* kern = [_currentLine attribute:(NSString*) kCTKernAttributeName
atIndex:prev.location
effectiveRange:NULL];
[_currentLine addAttribute:(NSString*) kCTKernAttributeName
value:[NSNumber numberWithFloat:interElementSpace]
range:[_currentLine.string rangeOfComposedCharacterSequenceAtIndex:_currentLine.length - 1]];
value:@(kern.floatValue + interElementSpace)
range:prev];
}
} else {
// increase the space
Expand All @@ -1018,6 +1024,10 @@ - (void) createDisplayAtoms:(NSArray*) preprocessed
if (atom.fontStyle == kMTFontStyleItalic && atom.type == kMTMathAtomOrdinary) {
[self applyMathitFontToRoutableCharactersInRange:appendedRange];
}
// Deliberately not gated on atom type, unlike the \mathit stamp
// above: TeX82 §749 routes all seven noad classes through the same
// nucleus conversion, so §755's correction fires for all of them.
[self applyItalicCorrectionsInRange:appendedRange forAtom:atom];
// add the atom to the current range
if (_currentLineIndexRange.location == NSNotFound) {
_currentLineIndexRange = atom.indexRange;
Expand All @@ -1033,15 +1043,21 @@ - (void) createDisplayAtoms:(NSArray*) preprocessed

// add super scripts || subscripts
if (atom.subScript || atom.superScript) {
// stash the existing line
// We don't check _currentLine.length here since we want to allow empty lines with super/sub scripts.
MTCTLineDisplay* line = [self addDisplayLine];
CGFloat delta = 0;
if (atom.nucleus.length > 0) {
// Use the italic correction of the last character.
CGGlyph glyph = [self findGlyphForCharacterAtIndex:atom.nucleus.length - 1 inString:atom.nucleus];
delta = [_styleFont.mathTable getItalicCorrection:glyph];
// Read before the flush clears _currentLine. A non-empty
// nucleus was just appended, so the line's last composed
// sequence is this atom's last character. Keying on the
// atom rather than on _currentLine.length matters: for an
// empty nucleus the line's last character belongs to the
// previous atom, whose correction addDisplayLine has
// already carried into the pen.
NSRange last = [_currentLine.string rangeOfComposedCharacterSequenceAtIndex:_currentLine.length - 1];
delta = [self italicCorrectionInCurrentLineAtIndex:last.location];
}
// stash the existing line
// We don't check _currentLine.length here since we want to allow empty lines with super/sub scripts.
MTCTLineDisplay* line = [self addDisplayLine];
if (delta > 0 && !atom.subScript) {
// Add a kern of delta
_currentPosition.x += delta;
Expand All @@ -1065,6 +1081,32 @@ - (void) createDisplayAtoms:(NSArray*) preprocessed
}
}

// TeX82 §755 drops the correction on an interior character only when the run is
// set in a text font — a TFM whose FONTDIMEN 2 (SPACE) is nonzero. \math*
// selects a family, and LaTeX binds most of those families to text TFMs; only
// these three sit on a math TFM. A style added later must be classified against
// that table rather than inherit a branch, which is why there is no default:.
static BOOL MTStyleSuppressesInteriorItalicCorrection(MTFontStyle style)
{
switch (style) {
case kMTFontStyleDefault: // cmmi10
case kMTFontStyleCaligraphic: // cmsy10
case kMTFontStyleBoldItalic: // cmmib10
// \mathit re-families class-7 mathchars only, so anything still drawn in
// the math font here is cmmi10. Its companion half is suppressed by the
// face test in applyItalicCorrectionsInRange:forAtom:.
case kMTFontStyleItalic:
return NO;
case kMTFontStyleRoman: // cmr10
case kMTFontStyleBold: // cmbx10
case kMTFontStyleSansSerif: // cmss10
case kMTFontStyleTypewriter: // cmtt10
case kMTFontStyleFraktur: // eufm10
case kMTFontStyleBlackboard: // msbm10
return YES;
}
}

// Gives maximal runs of routable characters the \mathit companion face.
// Evaluated per character, not per atom: fusion merges a whole \mathit group
// into one atom, which can mix routable and non-routable characters
Expand All @@ -1088,6 +1130,93 @@ - (void) applyMathitFontToRoutableCharactersInRange:(NSRange) range
}
}

// The face stamped at `index`. Every appended range is stamped before anything
// reads it back, so a missing attribute is a broken invariant rather than
// something LaTeX input can produce. Defaulting to _styleFont would read the
// math table for a companion glyph and return a plausible wrong number, which
// is the defect this path exists to remove.
- (CTFontRef) faceInCurrentLineAtIndex:(NSUInteger) index
{
CTFontRef face = (__bridge CTFontRef) [_currentLine attribute:(NSString*) kCTFontAttributeName
atIndex:index
effectiveRange:NULL];
NSAssert(face != NULL, @"No font stamped at index %lu of '%@'",
(unsigned long) index, _currentLine.string);
return face;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// The italic correction of the composed character sequence at `index`, from the
// face that drew it. _currentLine can carry two faces — \mathit routes some
// characters to the companion — and a CoreText glyph id means nothing without
// its font, so the glyph is resolved against the same face the metric comes
// from. This must stay the only way to ask for this number.
- (CGFloat) italicCorrectionInCurrentLineAtIndex:(NSUInteger) index
{
CTFontRef face = [self faceInCurrentLineAtIndex:index];
NSString* string = _currentLine.string;
NSRange range = [string rangeOfComposedCharacterSequenceAtIndex:index];
unichar chars[range.length];
[string getCharacters:chars range:range];
CGGlyph glyphs[range.length];
if (!CTFontGetGlyphsForCharacters(face, chars, glyphs, range.length)) {
// Same convention as findGlyphForCharacterAtIndex:inString:. Measuring
// notdef would return a correction for a box that is never drawn.
return 0;
}
if (CFEqual(face, _styleFont.ctFont)) {
return [_styleFont.mathTable getItalicCorrection:glyphs[0]];
}
// No text-italic face available to us carries a MATH table, so the
// correction is the ink overhanging the advance — the metric MathJax bakes
// into its -tex-mathit table.
CGRect bounds = CTFontGetBoundingRectsForGlyphs(face, kCTFontOrientationDefault, glyphs, NULL, 1);
CGSize advance;
CTFontGetAdvancesForGlyphs(face, kCTFontOrientationDefault, glyphs, &advance, 1);
return MAX(0, CGRectGetMaxX(bounds) - advance.width);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

// TeX Rule 17: a kern of the glyph's italic correction after each character of
// `range` whose subscript is empty, with Rule 14's interior suppression.
- (void) applyItalicCorrectionsInRange:(NSRange) range forAtom:(MTMathAtom*) atom
{
NSString* string = _currentLine.string;
NSUInteger i = range.location;
while (i < NSMaxRange(range)) {
NSRange sequence = [string rangeOfComposedCharacterSequenceAtIndex:i];
NSUInteger next = NSMaxRange(sequence);
BOOL apply;
if (next >= NSMaxRange(range)) {
// Last character of the atom, and nothing ever suppresses the
// correction there. When it carries a script the script path applies
// it instead, so the two paths own this character exclusively.
apply = !atom.subScript && !atom.superScript;
} else {
CTFontRef face = [self faceInCurrentLineAtIndex:i];
if (!CFEqual(face, [self faceInCurrentLineAtIndex:next])) {
// A face change is a family change, so TeX never marks this
// character math_text_char and §755's AND cannot fire.
apply = YES;
} else {
// Interior of a single-face run: kept only for a math font.
apply = CFEqual(face, _styleFont.ctFont)
&& !MTStyleSuppressesInteriorItalicCorrection(atom.fontStyle);
}
}
// The MATH-table metric is signed and TeX applies it with its sign, so
// the test is != 0 rather than > 0.
CGFloat correction = apply ? [self italicCorrectionInCurrentLineAtIndex:i] : 0;
if (correction != 0) {
NSNumber* kern = [_currentLine attribute:(NSString*) kCTKernAttributeName
atIndex:i
effectiveRange:NULL];
[_currentLine addAttribute:(NSString*) kCTKernAttributeName
value:@(kern.floatValue + correction)
range:sequence];
}
i = next;
}
}

- (MTCTLineDisplay*) addDisplayLine
{
/*NSAssert(_currentLineIndexRange.length == numCodePoints(_currentLine.string),
Expand Down
29 changes: 27 additions & 2 deletions iosMathTests/MTInkWidthTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ - (void)testCTLineLeafInk {
MTCTLineDisplay* lineP = (MTCTLineDisplay*)dP.subDisplays.firstObject;
XCTAssertTrue([lineP isKindOfClass:[MTCTLineDisplay class]]);
XCTAssertGreaterThanOrEqual(lineP.inkWidth, 15.08 - 0.01); // P ink right = 15.08
XCTAssertGreaterThan(lineP.inkWidth, lineP.width); // 15.08 > advance 12.84
// ε (2.80) now exceeds P's 2.24 of protruding ink, so the advance alone
// covers the ink extent and inkWidth collapses onto width (LLD §5).
XCTAssertEqualWithAccuracy(lineP.inkWidth, lineP.width, 0.01);

// Control: x ink (10.54) < advance (11.44) → inkWidth stays the advance.
MTMathListDisplay* dx = [self displayFor:@"x"];
Expand Down Expand Up @@ -169,6 +171,24 @@ - (void)testInnerInk {
[self assertComposite:[MTInnerDisplay class] bare:@"\\left( V \\right." shifted:@"a\\left( V \\right."];
}

// The composites above all trail a V, which no longer overhangs, so their getters
// would still pass with the MAX over children deleted. No plain glyph overhangs
// its corrected advance any more, but \vec{f}'s accent glyph does (see
// testAccentGlyphInk), so nesting it is what still forces the child to drive the
// composite's inkWidth. \sum and \overrightarrow are absent because their own
// glyph is always wide enough to cover the base -- they can't overhang at all.
- (void)testCompositeInkTracksOverhangingChild {
for (NSArray* c in @[ @[ [MTFractionDisplay class], @"\\frac{1}{\\vec{f}}" ],
@[ [MTRadicalDisplay class], @"\\sqrt{\\vec{f}}" ],
@[ [MTLineDisplay class], @"\\overline{\\vec{f}}" ],
@[ [MTInnerDisplay class], @"\\left( \\vec{f} \\right." ] ]) {
MTDisplay* d = [self findDisplayOfClass:c[0] in:[self displayFor:c[1]]];
XCTAssertNotNil(d, @"%@", c[1]);
XCTAssertGreaterThan(d.inkWidth, d.width, @"%@", c[1]);
XCTAssertGreaterThanOrEqual(d.inkWidth, [self composedInkRightOf:d] - 0.01, @"%@", c[1]);
}
}

// Depth-first: the first display of the given class, or nil.
- (MTDisplay*)findDisplayOfClass:(Class)cls in:(MTDisplay*)d {
if ([d isKindOfClass:cls]) return d;
Expand Down Expand Up @@ -244,7 +264,12 @@ - (void)assertComposite:(Class)cls bare:(NSString*)bare shifted:(NSString*)shift
XCTAssertNotNil(s, @"no %@ in %@", NSStringFromClass(cls), shifted);
XCTAssertGreaterThanOrEqual(b.inkWidth, [self composedInkRightOf:b] - 0.01);
XCTAssertGreaterThanOrEqual(s.inkWidth, [self composedInkRightOf:s] - 0.01);
XCTAssertGreaterThan(b.inkWidth, b.width); // trailing child overhangs
// Every bare/shifted pair here ends in a trailing V. V's correction (4.28) now
// exceeds V's own protruding ink (the same LLD §5 mechanism as the P case in
// testCTLineLeafInk), so V no longer overhangs its own advance -- and since V is
// always the composite's rightmost child, the composite doesn't overhang either.
// "Trailing child overhangs" no longer holds; assert the collapse instead.
XCTAssertEqualWithAccuracy(b.inkWidth, b.width, 0.01);
XCTAssertGreaterThan(s.position.x, b.position.x); // shifted variant is further right
XCTAssertEqualWithAccuracy(s.inkWidth - s.width, b.inkWidth - b.width, 0.02); // basis-invariant
}
Expand Down
Loading
Loading