Rename package and address warnings

This commit is contained in:
Guille Gonzalez
2025-12-30 14:45:54 +01:00
parent c49362fcf1
commit 5774724d18
59 changed files with 19 additions and 112 deletions

View File

@@ -0,0 +1,160 @@
//
// MTFontMathTableV2.swift
//
//
// Created by Peter Tang on 15/9/2023.
//
import Foundation
import CoreGraphics
import CoreText
internal class MTFontMathTableV2: MTFontMathTable {
private let mathFont: MathFont
private let fontSize: CGFloat
private let unitsPerEm: UInt
private let mTable: NSDictionary
init(mathFont: MathFont, size: CGFloat, unitsPerEm: UInt) {
self.mathFont = mathFont
self.fontSize = size
self.unitsPerEm = unitsPerEm
mTable = mathFont.rawMathTable()
super.init(withFont: mathFont.mtfont(size: fontSize), mathTable: mTable)
super._mathTable = nil
// disable all possible access to _mathTable in superclass!
}
override var _mathTable: NSDictionary? {
set { fatalError("\(#function) change to _mathTable \(mathFont.rawValue) not allowed.") }
get { mTable }
}
override var muUnit: CGFloat { fontSize/18 }
override func fontUnitsToPt(_ fontUnits:Int) -> CGFloat {
CGFloat(fontUnits) * fontSize / CGFloat(unitsPerEm)
}
override func constantFromTable(_ constName: String) -> CGFloat {
guard let consts = mTable[kConstants] as? NSDictionary, let val = consts[constName] as? NSNumber else {
return .zero
}
return fontUnitsToPt(val.intValue)
}
override func percentFromTable(_ percentName: String) -> CGFloat {
guard let consts = mTable[kConstants] as? NSDictionary, let val = consts[percentName] as? NSNumber else {
return .zero
}
return CGFloat(val.floatValue) / 100
}
/** Returns an Array of all the vertical variants of the glyph if any. If
there are no variants for the glyph, the array contains the given glyph. */
override func getVerticalVariantsForGlyph(_ glyph: CGGlyph) -> [NSNumber?] {
guard let variants = mTable[kVertVariants] as? NSDictionary else { return [] }
return self.getVariantsForGlyph(glyph, inDictionary: variants)
}
/** Returns an Array of all the horizontal variants of the glyph if any. If
there are no variants for the glyph, the array contains the given glyph. */
override func getHorizontalVariantsForGlyph(_ glyph: CGGlyph) -> [NSNumber?] {
guard let variants = mTable[kHorizVariants] as? NSDictionary else { return [] }
return self.getVariantsForGlyph(glyph, inDictionary:variants)
}
override func getVariantsForGlyph(_ glyph: CGGlyph, inDictionary variants: NSDictionary) -> [NSNumber?] {
let font = mathFont.mtfont(size: fontSize)
let glyphName = font.get(nameForGlyph: glyph)
var glyphArray = [NSNumber]()
let variantGlyphs = variants[glyphName] as? NSArray
guard let variantGlyphs = variantGlyphs, variantGlyphs.count != .zero else {
// There are no extra variants, so just add the current glyph to it.
let glyph = font.get(glyphWithName: glyphName)
glyphArray.append(NSNumber(value:glyph))
return glyphArray
}
for gvn in variantGlyphs {
if let glyphVariantName = gvn as? String {
let variantGlyph = font.get(glyphWithName: glyphVariantName)
glyphArray.append(NSNumber(value:variantGlyph))
}
}
return glyphArray
}
/** Returns a larger vertical variant of the given glyph if any.
If there is no larger version, this returns the current glyph.
*/
override func getLargerGlyph(_ glyph: CGGlyph) -> CGGlyph {
let font = mathFont.mtfont(size: fontSize)
let glyphName = font.get(nameForGlyph: glyph)
guard let variants = mTable[kVertVariants] as? NSDictionary,
let variantGlyphs = variants[glyphName] as? NSArray, variantGlyphs.count != .zero else {
// There are no extra variants, so just returnt the current glyph.
return glyph
}
// Find the first variant with a different name.
for gvn in variantGlyphs {
if let glyphVariantName = gvn as? String, glyphVariantName != glyphName {
let variantGlyph = font.get(glyphWithName: glyphVariantName)
return variantGlyph
}
}
// We did not find any variants of this glyph so return it.
return glyph
}
/** Returns the italic correction for the given glyph if any. If there
isn't any this returns 0. */
override func getItalicCorrection(_ glyph: CGGlyph) -> CGFloat {
let font = mathFont.mtfont(size: fontSize)
let glyphName = font.get(nameForGlyph: glyph)
guard let italics = mTable[kItalic] as? NSDictionary, let val = italics[glyphName] as? NSNumber else {
return .zero
}
// if val is nil, this returns 0.
return fontUnitsToPt(val.intValue)
}
override func getTopAccentAdjustment(_ glyph: CGGlyph) -> CGFloat {
let font = mathFont.mtfont(size: fontSize)
let glyphName = font.get(nameForGlyph: glyph)
guard let accents = mTable[kAccents] as? NSDictionary, let val = accents[glyphName] as? NSNumber else {
// If no top accent is defined then it is the center of the advance width.
var glyph = glyph
var advances = CGSize.zero
CTFontGetAdvancesForGlyphs(font.ctFont, .horizontal, &glyph, &advances, 1)
return advances.width/2
}
return fontUnitsToPt(val.intValue)
}
override func getVerticalGlyphAssembly(forGlyph glyph: CGGlyph) -> [GlyphPart] {
let font = mathFont.mtfont(size: fontSize)
let glyphName = font.get(nameForGlyph: glyph)
guard let assemblyTable = mTable[kVertAssembly] as? NSDictionary,
let assemblyInfo = assemblyTable[glyphName] as? NSDictionary,
let parts = assemblyInfo[kAssemblyParts] as? NSArray else {
// No vertical assembly defined for glyph
// parts should always have been defined, but if it isn't return nil
return []
}
var rv = [GlyphPart]()
for part in parts {
guard let partInfo = part as? NSDictionary,
let adv = partInfo["advance"] as? NSNumber,
let end = partInfo["endConnector"] as? NSNumber,
let start = partInfo["startConnector"] as? NSNumber,
let ext = partInfo["extender"] as? NSNumber,
let glyphName = partInfo["glyph"] as? String else { continue }
let fullAdvance = fontUnitsToPt(adv.intValue)
let endConnectorLength = fontUnitsToPt(end.intValue)
let startConnectorLength = fontUnitsToPt(start.intValue)
let isExtender = ext.boolValue
let glyph = font.get(glyphWithName: glyphName)
let part = GlyphPart(glyph: glyph, fullAdvance: fullAdvance,
startConnectorLength: startConnectorLength,
endConnectorLength: endConnectorLength,
isExtender: isExtender)
rv.append(part)
}
return rv
}
}

View File

@@ -0,0 +1,68 @@
//
// MTFontV2.swift
//
//
// Created by Peter Tang on 15/9/2023.
//
import Foundation
import CoreGraphics
import CoreText
extension MathFont {
public func mtfont(size: CGFloat) -> MTFontV2 {
MTFontV2(font: self, size: size)
}
}
public final class MTFontV2: MTFont {
let font: MathFont
let size: CGFloat
private let _cgFont: CGFont
private let _ctFont: CTFont
private let unitsPerEm: UInt
private var _mathTab: MTFontMathTableV2?
init(font: MathFont = .latinModernFont, size: CGFloat) {
self.font = font
self.size = size
// MathFont cgfont and ctfont are fast & threadsafe, keep a local copy is cheaper than
// handling via NSLock
self._cgFont = font.cgFont()
self._ctFont = font.ctFont(withSize: size)
self.unitsPerEm = self._ctFont.unitsPerEm
super.init()
super.defaultCGFont = nil
super.ctFont = nil
super.mathTable = nil
super.rawMathTable = nil
}
override var defaultCGFont: CGFont! {
set { fatalError("\(#function): change to \(font.fontName) not allowed.") }
get { _cgFont }
}
override var ctFont: CTFont! {
set { fatalError("\(#function): change to \(font.fontName) not allowed.") }
get { _ctFont }
}
private let mtfontV2LockOnMathTable = NSLock()
override var mathTable: MTFontMathTable? {
set { fatalError("\(#function): change to \(font.rawValue) not allowed.") }
get {
guard _mathTab == nil else { return _mathTab }
//Note: lazy _mathTab initialization is now threadsafe.
mtfontV2LockOnMathTable.lock()
defer { mtfontV2LockOnMathTable.unlock() }
if _mathTab == nil {
_mathTab = MTFontMathTableV2(mathFont: font, size: size, unitsPerEm: unitsPerEm)
}
return _mathTab
}
}
override var rawMathTable: NSDictionary? {
set { fatalError("\(#function): change to \(font.rawValue) not allowed.") }
get { fatalError("\(#function): access to \(font.rawValue) not allowed.") }
}
public override func copy(withSize size: CGFloat) -> MTFont {
MTFontV2(font: font, size: size)
}
}

View File

@@ -0,0 +1,225 @@
//
// MathFont.swift
//
//
// Created by Peter Tang on 10/9/2023.
//
#if os(iOS) || os(visionOS)
import UIKit
#elseif os(macOS)
import AppKit
#endif
/// Now available for everyone to use
public enum MathFont: String, CaseIterable, Identifiable {
public var id: Self { self } // Makes things simpler for SwiftUI
case latinModernFont = "latinmodern-math"
case kpMathLightFont = "KpMath-Light"
case kpMathSansFont = "KpMath-Sans"
case xitsFont = "xits-math"
case termesFont = "texgyretermes-math"
case asanaFont = "Asana-Math"
case eulerFont = "Euler-Math"
case firaFont = "FiraMath-Regular"
case notoSansFont = "NotoSansMath-Regular"
case libertinusFont = "LibertinusMath-Regular"
case garamondFont = "Garamond-Math"
case leteSansFont = "LeteSansMath"
var fontFamilyName: String {
switch self {
case .latinModernFont: "Latin Modern Math"
case .kpMathLightFont: "KpMath"
case .kpMathSansFont: "KpMath"
case .xitsFont: "XITS Math"
case .termesFont: "TeX Gyre Termes Math"
case .asanaFont: "Asana Math"
case .eulerFont: "Euler Math"
case .firaFont: "Fira Math"
case .notoSansFont: "Noto Sans Math"
case .libertinusFont: "Libertinus Math"
case .garamondFont: "Garamond-Math" // PostScript name is "Garamond-Math", not "Garamond Math"
case .leteSansFont: "Lete Sans Math"
}
}
var postScriptName: String {
switch self {
case .latinModernFont: "LatinModernMath-Regular"
case .kpMathLightFont: "KpMath-Light"
case .kpMathSansFont: "KpMath-Sans"
case .xitsFont: "XITSMath"
case .termesFont: "TeXGyreTermesMath-Regular"
case .asanaFont: "Asana-Math"
case .eulerFont: "Euler-Math"
case .firaFont: "FiraMath-Regular"
case .notoSansFont: "NotoSansMath-Regular"
case .libertinusFont: "LibertinusMath-Regular"
case .garamondFont: "Garamond-Math"
case .leteSansFont: "LeteSansMath"
}
}
var fontName: String { self.rawValue }
public func cgFont() -> CGFont {
BundleManager.manager.obtainCGFont(font: self)
}
public func ctFont(withSize size: CGFloat) -> CTFont {
BundleManager.manager.obtainCTFont(font: self, withSize: size)
}
internal func rawMathTable() -> NSDictionary {
BundleManager.manager.obtainRawMathTable(font: self)
}
//Note: Below code are no longer supported, unable to tell if UIFont/NSFont is threadsafe, not used in SwiftMath.
// #if os(iOS) || os(visionOS)
// public func uiFont(withSize size: CGFloat) -> UIFont? {
// UIFont(name: fontName, size: size)
// }
// #endif
// #if os(macOS)
// public func nsFont(withSize size: CGFloat) -> NSFont? {
// NSFont(name: fontName, size: size)
// }
// #endif
}
internal extension CTFont {
/** The size of this font in points. */
var fontSize: CGFloat {
CTFontGetSize(self)
}
var unitsPerEm: UInt {
return UInt(CTFontGetUnitsPerEm(self))
}
}
private class BundleManager {
//Note: below should be lightweight and without threadsafe problem.
static internal let manager = BundleManager()
private var cgFonts = [MathFont: CGFont]()
private var ctFonts = [CTFontSizePair: CTFont]()
private var rawMathTables = [MathFont: NSDictionary]()
private let threadSafeQueue = DispatchQueue(label: "com.smartmath.mathfont.threadsafequeue",
qos: .userInitiated,
attributes: .concurrent)
private func registerCGFont(mathFont: MathFont) throws {
guard let frameworkBundleURL = Bundle.module.url(forResource: "mathFonts", withExtension: "bundle"),
let resourceBundleURL = Bundle(url: frameworkBundleURL)?.path(forResource: mathFont.rawValue, ofType: "otf") else {
throw FontError.fontPathNotFound
}
guard let fontData = NSData(contentsOfFile: resourceBundleURL), let dataProvider = CGDataProvider(data: fontData) else {
throw FontError.invalidFontFile
}
guard let defaultCGFont = CGFont(dataProvider) else {
throw FontError.initFontError
}
cgFonts[mathFont] = defaultCGFont
/// This does not load the complete math font, it only has about half the glyphs of the full math font.
/// In particular it does not have the math italic characters which breaks our variable rendering.
/// So we first load a CGFont from the file and then convert it to a CTFont.
var errorRef: Unmanaged<CFError>? = nil
guard CTFontManagerRegisterGraphicsFont(defaultCGFont, &errorRef) else {
throw FontError.registerFailed
}
let postsript = (defaultCGFont.postScriptName as? String) ?? ""
let cgfontName = (defaultCGFont.fullName as? String) ?? ""
let threadName = Thread.isMainThread ? "main" : "global"
debugPrint("mathFonts bundle resource: \(mathFont.rawValue), font: \(cgfontName), ps: \(postsript) registered on \(threadName).")
}
private func registerMathTable(mathFont: MathFont) throws {
guard let frameworkBundleURL = Bundle.module.url(forResource: "mathFonts", withExtension: "bundle"),
let mathTablePlist = Bundle(url: frameworkBundleURL)?.url(forResource: mathFont.rawValue, withExtension:"plist") else {
throw FontError.fontPathNotFound
}
guard let rawMathTable = NSDictionary(contentsOf: mathTablePlist),
let version = rawMathTable["version"] as? String,
version == "1.3" else {
throw FontError.invalidMathTable
}
rawMathTables[mathFont] = rawMathTable
let threadName = Thread.isMainThread ? "main" : "global"
debugPrint("mathFonts bundle resource: \(mathFont.rawValue).plist registered on \(threadName).")
}
private func onDemandRegistration(mathFont: MathFont) {
guard threadSafeQueue.sync(execute: { cgFonts[mathFont] }) == nil else { return }
// Note: resourceLoading is now serialized.
threadSafeQueue.sync(flags: .barrier, execute: { [weak self] in
if self?.cgFonts[mathFont] == nil {
do {
try BundleManager.manager.registerCGFont(mathFont: mathFont)
try BundleManager.manager.registerMathTable(mathFont: mathFont)
} catch {
fatalError("MTMathFonts:\(#function) ondemand loading failed, mathFont \(mathFont.rawValue), reason \(error)")
}
}
})
}
fileprivate func obtainCGFont(font: MathFont) -> CGFont {
onDemandRegistration(mathFont: font)
guard let cgFont = threadSafeQueue.sync(execute: { cgFonts[font] }) else {
fatalError("\(#function) unable to locate CGFont \(font.fontName)")
}
return cgFont
}
fileprivate func obtainCTFont(font: MathFont, withSize size: CGFloat) -> CTFont {
onDemandRegistration(mathFont: font)
let fontSizePair = CTFontSizePair(font: font, size: size)
let ctFont = threadSafeQueue.sync(execute: { ctFonts[fontSizePair] })
guard ctFont == nil else { return ctFont! }
guard let cgFont = threadSafeQueue.sync(execute: { cgFonts[font] }) else {
fatalError("\(#function) unable to locate CGFont \(font.fontName) to create CTFont")
}
//Note: ctfont creation and caching is now threadsafe.
guard threadSafeQueue.sync(execute: { ctFonts[fontSizePair] }) == nil else { return ctFonts[fontSizePair]! }
return threadSafeQueue.sync(flags: .barrier, execute: {
if let ctfont = ctFonts[fontSizePair] {
return ctfont
} else {
let result = CTFontCreateWithGraphicsFont(cgFont, size, nil, nil)
ctFonts[fontSizePair] = result
return result
}
})
}
fileprivate func obtainRawMathTable(font: MathFont) -> NSDictionary {
onDemandRegistration(mathFont: font)
guard let mathTable = threadSafeQueue.sync(execute: { rawMathTables[font] } ) else {
fatalError("\(#function) unable to locate mathTable: \(font.rawValue).plist")
}
return mathTable
}
deinit {
ctFonts.removeAll()
var errorRef: Unmanaged<CFError>? = nil
cgFonts.values.forEach { cgFont in
CTFontManagerUnregisterGraphicsFont(cgFont, &errorRef)
}
cgFonts.removeAll()
}
public enum FontError: Error {
case invalidFontFile
case fontPathNotFound
case initFontError
case registerFailed
case invalidMathTable
}
private struct CTFontSizePair: Hashable {
let font: MathFont
let size: CGFloat
}
}

View File

@@ -0,0 +1,123 @@
//
// MathImage.swift
//
//
// Created by Peter Tang on 15/9/2023.
//
import Foundation
#if os(iOS) || os(visionOS)
import UIKit
#elseif os(macOS)
import AppKit
#endif
public struct MathImage {
public var font: MathFont = .latinModernFont
public var fontSize: CGFloat
public var textColor: MTColor
public var labelMode: MTMathUILabelMode
public var textAlignment: MTTextAlignment
public var contentInsets: MTEdgeInsets = MTEdgeInsetsZero
public let latex: String
private(set) var intrinsicContentSize = CGSize.zero
public init(latex: String, fontSize: CGFloat, textColor: MTColor, labelMode: MTMathUILabelMode = .display, textAlignment: MTTextAlignment = .center) {
self.latex = latex
self.fontSize = fontSize
self.textColor = textColor
self.labelMode = labelMode
self.textAlignment = textAlignment
}
}
extension MathImage {
public var currentStyle: MTLineStyle {
switch labelMode {
case .display: return .display
case .text: return .text
}
}
private func intrinsicContentSize(_ displayList: MTMathListDisplay) -> CGSize {
CGSize(width: displayList.width + contentInsets.left + contentInsets.right,
height: displayList.ascent + displayList.descent + contentInsets.top + contentInsets.bottom)
}
public struct LayoutInfo {
public var ascent: CGFloat = 0
public var descent: CGFloat = 0
public init(ascent: CGFloat, descent: CGFloat) {
self.ascent = ascent
self.descent = descent
}
}
public mutating func asImage() -> (NSError?, MTImage?, LayoutInfo?) {
func layoutImage(size: CGSize, displayList: MTMathListDisplay) {
var textX = CGFloat(0)
switch self.textAlignment {
case .left: textX = contentInsets.left
case .center: textX = (size.width - contentInsets.left - contentInsets.right - displayList.width) / 2 + contentInsets.left
case .right: textX = size.width - displayList.width - contentInsets.right
}
let availableHeight = size.height - contentInsets.bottom - contentInsets.top
// center things vertically
var height = displayList.ascent + displayList.descent
if height < fontSize/2 {
height = fontSize/2 // set height to half the font size
}
let textY = (availableHeight - height) / 2 + displayList.descent + contentInsets.bottom
displayList.position = CGPoint(x: textX, y: textY)
}
var error: NSError?
let mtfont: MTFont? = font.mtfont(size: fontSize)
guard let mathList = MTMathListBuilder.build(fromString: latex, error: &error), error == nil,
let displayList = MTTypesetter.createLineForMathList(mathList, font: mtfont, style: currentStyle) else {
return (error, nil, nil)
}
intrinsicContentSize = intrinsicContentSize(displayList)
displayList.textColor = textColor
let size = intrinsicContentSize.regularized
layoutImage(size: size, displayList: displayList)
#if os(iOS) || os(visionOS)
let renderer = UIGraphicsImageRenderer(size: size)
let image = renderer.image { rendererContext in
rendererContext.cgContext.saveGState()
rendererContext.cgContext.concatenate(.flippedVertically(size.height))
displayList.draw(rendererContext.cgContext)
rendererContext.cgContext.restoreGState()
}
return (nil, image, LayoutInfo(ascent: displayList.ascent, descent: displayList.descent))
#endif
#if os(macOS)
let image = NSImage(size: size, flipped: false) { bounds in
guard let context = NSGraphicsContext.current?.cgContext else { return false }
context.saveGState()
displayList.draw(context)
context.restoreGState()
return true
}
return (nil, image, LayoutInfo(ascent: displayList.ascent, descent: displayList.descent))
#endif
}
}
private extension CGAffineTransform {
static func flippedVertically(_ height: CGFloat) -> CGAffineTransform {
var transform = CGAffineTransform(scaleX: 1, y: -1)
transform = transform.translatedBy(x: 0, y: -height)
return transform
}
}
extension CGSize {
fileprivate var regularized: CGSize {
CGSize(width: ceil(width), height: ceil(height))
}
}

View File

@@ -0,0 +1,35 @@
//
// Created by Mike Griebling on 2022-12-31.
// Translated from an Objective-C implementation by .
//
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
import Foundation
#if os(macOS)
extension MTBezierPath {
func addLine(to point: CGPoint) {
self.line(to: point)
}
}
extension MTView {
var backgroundColor:MTColor? {
get {
MTColor(cgColor: self.layer?.backgroundColor ?? MTColor.clear.cgColor)
}
set {
self.layer?.backgroundColor = MTColor.clear.cgColor
self.wantsLayer = true
}
}
}
#endif

View File

@@ -0,0 +1,28 @@
//
// Created by Mike Griebling on 2022-12-31.
// Translated from an Objective-C implementation by Markus Sähn.
//
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
import Foundation
extension MTColor {
public convenience init?(fromHexString hexString:String) {
if hexString.isEmpty { return nil }
if !hexString.hasPrefix("#") { return nil }
var rgbValue = UInt64(0)
let scanner = Scanner(string: hexString)
scanner.charactersToBeSkipped = CharacterSet(charactersIn: "#")
scanner.scanHexInt64(&rgbValue)
self.init(red: CGFloat((rgbValue & 0xFF0000) >> 16)/255.0,
green: CGFloat((rgbValue & 0xFF00) >> 8)/255.0,
blue: CGFloat((rgbValue & 0xFF))/255.0,
alpha: 1.0)
}
}

View File

@@ -0,0 +1,40 @@
import Foundation
//
// Created by Mike Griebling on 2022-12-31.
// Translated from an Objective-C implementation by .
//
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
#if os(iOS) || os(visionOS)
import UIKit
public typealias MTView = UIView
public typealias MTColor = UIColor
public typealias MTBezierPath = UIBezierPath
public typealias MTLabel = UILabel
public typealias MTEdgeInsets = UIEdgeInsets
public typealias MTRect = CGRect
public typealias MTImage = UIImage
let MTEdgeInsetsZero = UIEdgeInsets.zero
func MTGraphicsGetCurrentContext() -> CGContext? { UIGraphicsGetCurrentContext() }
#else
import AppKit
public typealias MTView = NSView
public typealias MTColor = NSColor
public typealias MTBezierPath = NSBezierPath
public typealias MTEdgeInsets = NSEdgeInsets
public typealias MTRect = NSRect
public typealias MTImage = NSImage
let MTEdgeInsetsZero = NSEdgeInsets.init(top: 0, left: 0, bottom: 0, right: 0)
func MTGraphicsGetCurrentContext() -> CGContext? { NSGraphicsContext.current?.cgContext }
#endif

View File

@@ -0,0 +1,80 @@
import Foundation
import CoreText
//
// Created by Mike Griebling on 2022-12-31.
// Translated from an Objective-C implementation by Kostub Deshmukh.
//
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
public class MTFont {
var defaultCGFont: CGFont!
var ctFont: CTFont!
var mathTable: MTFontMathTable?
var rawMathTable: NSDictionary?
/// Fallback font for characters not supported by the main math font.
/// Defaults to the system font at the same size. This is particularly useful
/// for rendering text in \text{} commands with characters outside the math font's coverage
/// (e.g., Chinese, Japanese, Korean, emoji, etc.)
public var fallbackFont: CTFont?
init() {}
/// `MTFont(fontWithName:)` does not load the complete math font, it only has about half the glyphs of the full math font.
/// In particular it does not have the math italic characters which breaks our variable rendering.
/// So we first load a CGFont from the file and then convert it to a CTFont.
convenience init(fontWithName name: String, size:CGFloat) {
self.init()
//print("Loading font \(name)")
let bundle = MTFont.fontBundle
let fontPath = bundle.path(forResource: name, ofType: "otf")
let fontDataProvider = CGDataProvider(filename: fontPath!)
self.defaultCGFont = CGFont(fontDataProvider!)!
//print("Num glyphs: \(self.defaultCGFont.numberOfGlyphs)")
self.ctFont = CTFontCreateWithGraphicsFont(self.defaultCGFont, size, nil, nil);
//print("Loading associated .plist")
let mathTablePlist = bundle.url(forResource:name, withExtension:"plist")
self.rawMathTable = NSDictionary(contentsOf: mathTablePlist!)
self.mathTable = MTFontMathTable(withFont:self, mathTable:rawMathTable!)
}
static var fontBundle:Bundle {
// Uses bundle for class so that this can be access by the unit tests.
Bundle(url: Bundle.module.url(forResource: "mathFonts", withExtension: "bundle")!)!
}
/** Returns a copy of this font but with a different size. */
public func copy(withSize size: CGFloat) -> MTFont {
let newFont = MTFont()
newFont.defaultCGFont = self.defaultCGFont
newFont.ctFont = CTFontCreateWithGraphicsFont(self.defaultCGFont, size, nil, nil)
newFont.rawMathTable = self.rawMathTable
newFont.mathTable = MTFontMathTable(withFont: newFont, mathTable: newFont.rawMathTable!)
return newFont
}
func get(nameForGlyph glyph:CGGlyph) -> String {
let name = defaultCGFont.name(for: glyph) as? String
return name ?? ""
}
func get(glyphWithName name:String) -> CGGlyph {
defaultCGFont.getGlyphWithGlyphName(name: name as CFString)
}
/** The size of this font in points. */
public var fontSize:CGFloat { CTFontGetSize(self.ctFont) }
deinit {
self.ctFont = nil
self.defaultCGFont = nil
}
}

View File

@@ -0,0 +1,93 @@
//
// Created by Mike Griebling on 2022-12-31.
// Translated from an Objective-C implementation by Kostub Deshmukh.
//
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
import Foundation
public class MTFontManager {
static public private(set) var manager: MTFontManager = {
MTFontManager()
}()
let kDefaultFontSize = CGFloat(20)
static var fontManager : MTFontManager {
return manager
}
public init() { }
@RWLocked
var nameToFontMap = [String: MTFont]()
public func font(withName name:String, size:CGFloat) -> MTFont? {
var f = self.nameToFontMap[name]
if f == nil {
f = MTFont(fontWithName: name, size: size)
self.nameToFontMap[name] = f
}
if f!.fontSize == size { return f }
else { return f!.copy(withSize: size) }
}
public func latinModernFont(withSize size:CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: "latinmodern-math", size: size)
}
public func kpMathLightFont(withSize size:CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: "KpMath-Light", size: size)
}
public func kpMathSansFont(withSize size:CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: "KpMath-Sans", size: size)
}
public func xitsFont(withSize size:CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: "xits-math", size: size)
}
public func termesFont(withSize size:CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: "texgyretermes-math", size: size)
}
public func asanaFont(withSize size:CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: "Asana-Math", size: size)
}
public func eulerFont(withSize size:CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: "Euler-Math", size: size)
}
public func firaRegularFont(withSize size:CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: "FiraMath-Regular", size: size)
}
public func notoSansRegularFont(withSize size:CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: "NotoSansMath-Regular", size: size)
}
public func libertinusRegularFont(withSize size:CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: "LibertinusMath-Regular", size: size)
}
public func garamondMathFont(withSize size:CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: "Garamond-Math", size: size)
}
public func leteSansFont(withSize size:CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: "LeteSansMath", size: size)
}
public var defaultFont: MTFont? {
MTFontManager.fontManager.latinModernFont(withSize: kDefaultFontSize)
}
}

View File

@@ -0,0 +1,322 @@
//
// Created by Mike Griebling on 2022-12-31.
// Translated from an Objective-C implementation by Kostub Deshmukh.
//
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
import Foundation
import CoreText
struct GlyphPart {
/// The glyph that represents this part
var glyph: CGGlyph!
/// Full advance width/height for this part, in the direction of the extension in points.
var fullAdvance: CGFloat = 0
/// Advance width/ height of the straight bar connector material at the beginning of the glyph in points.
var startConnectorLength: CGFloat = 0
/// Advance width/ height of the straight bar connector material at the end of the glyph in points.
var endConnectorLength: CGFloat = 0
/// If this part is an extender. If set, the part can be skipped or repeated.
var isExtender: Bool = false
}
/** This class represents the Math table of an open type font.
The math table is documented here: https://www.microsoft.com/typography/otspec/math.htm
How the constants in this class affect the display is documented here:
http://www.tug.org/TUGboat/tb30-1/tb94vieth.pdf
Note: We don't parse the math table from the open type font. Rather we parse it
in python and convert it to a .plist file which is easily consumed by this class.
This approach is preferable to spending an inordinate amount of time figuring out
how to parse the returned NSData object using the open type rules.
Remark: This class is not meant to be used outside of this library.
*/
class MTFontMathTable {
// The font for this math table.
public private(set) weak var font:MTFont? // @property (nonatomic, readonly, weak) MTFont* font;
var _unitsPerEm: UInt
var _fontSize: CGFloat
var _mathTable: NSDictionary!
let kConstants = "constants"
/** MU unit in points */
var muUnit:CGFloat { _fontSize/18 }
func fontUnitsToPt(_ fontUnits:Int) -> CGFloat {
CGFloat(fontUnits) * _fontSize / CGFloat(_unitsPerEm)
}
init(withFont font: MTFont?, mathTable:NSDictionary) {
assert(font != nil, "font has nil value")
assert(font!.ctFont != nil, "font.ctFont has nil value")
self.font = font
// do domething with font
_unitsPerEm = UInt(CTFontGetUnitsPerEm(font!.ctFont))
_fontSize = font!.fontSize;
_mathTable = mathTable
let version = _mathTable["version"] as! String
if version != "1.3" {
NSException(name: NSExceptionName.internalInconsistencyException, reason: "Invalid version of math table plist: \(version)").raise()
}
}
func constantFromTable(_ constName:String) -> CGFloat {
let consts = _mathTable[kConstants] as! NSDictionary?
let val = consts![constName] as! NSNumber?
return fontUnitsToPt(val!.intValue)
}
func percentFromTable(_ percentName:String) -> CGFloat {
let consts = _mathTable[kConstants] as! NSDictionary?
let val = consts![percentName] as! NSNumber?
return CGFloat(val!.floatValue) / 100
}
/// Math Font Metrics from the opentype specification
// MARK: - Fractions
var fractionNumeratorDisplayStyleShiftUp:CGFloat { constantFromTable("FractionNumeratorDisplayStyleShiftUp") } // \sigma_8 in TeX
var fractionNumeratorShiftUp:CGFloat { constantFromTable("FractionNumeratorShiftUp") } // \sigma_9 in TeX
var fractionDenominatorDisplayStyleShiftDown:CGFloat { constantFromTable("FractionDenominatorDisplayStyleShiftDown") } // \sigma_11 in TeX
var fractionDenominatorShiftDown:CGFloat { constantFromTable("FractionDenominatorShiftDown") } // \sigma_12 in TeX
var fractionNumeratorDisplayStyleGapMin:CGFloat { constantFromTable("FractionNumDisplayStyleGapMin") } // 3 * \xi_8 in TeX
var fractionNumeratorGapMin:CGFloat { constantFromTable("FractionNumeratorGapMin") } // \xi_8 in TeX
var fractionDenominatorDisplayStyleGapMin:CGFloat { constantFromTable("FractionDenomDisplayStyleGapMin") } // 3 * \xi_8 in TeX
var fractionDenominatorGapMin:CGFloat { constantFromTable("FractionDenominatorGapMin") } // \xi_8 in TeX
var fractionRuleThickness:CGFloat { constantFromTable("FractionRuleThickness") } // \xi_8 in TeX
var skewedFractionHorizonalGap:CGFloat { constantFromTable("SkewedFractionHorizontalGap") } // \sigma_20 in TeX
var skewedFractionVerticalGap:CGFloat { constantFromTable("SkewedFractionVerticalGap") } // \sigma_21 in TeX
// MARK: - Non-standard
/// FractionDelimiterSize and FractionDelimiterDisplayStyleSize are not constants
/// specified in the OpenType Math specification. Rather these are proposed LuaTeX extensions
/// for the TeX parameters \sigma_20 (delim1) and \sigma_21 (delim2). Since these do not
/// exist in the fonts that we have, we use the same approach as LuaTeX and use the fontSize
/// to determine these values. The constants used are the same as LuaTeX and KaTeX and match the
/// metrics values of the original TeX fonts.
/// Note: An alternative approach is to use DelimitedSubFormulaMinHeight for \sigma21 and use a factor
/// of 2 to get \sigma 20 as proposed in Vieth paper.
/// The XeTeX implementation sets \sigma21 = fontSize and \sigma20 = DelimitedSubFormulaMinHeight which
/// will produce smaller delimiters.
/// Of all the approaches we've implemented LuaTeX's approach since it mimics LaTeX most accurately.
var fractionDelimiterSize: CGFloat { 1.01 * _fontSize }
/// Modified constant from 2.4 to 2.39, it matches KaTeX and looks better.
var fractionDelimiterDisplayStyleSize: CGFloat { 2.39 * _fontSize }
// MARK: - Stacks
var stackTopDisplayStyleShiftUp:CGFloat { constantFromTable("StackTopDisplayStyleShiftUp") } // \sigma_8 in TeX
var stackTopShiftUp:CGFloat { constantFromTable("StackTopShiftUp") } // \sigma_10 in TeX
var stackDisplayStyleGapMin:CGFloat { constantFromTable("StackDisplayStyleGapMin") } // 7 \xi_8 in TeX
var stackGapMin:CGFloat { constantFromTable("StackGapMin") } // 3 \xi_8 in TeX
var stackBottomDisplayStyleShiftDown:CGFloat { constantFromTable("StackBottomDisplayStyleShiftDown") } // \sigma_11 in TeX
var stackBottomShiftDown:CGFloat { constantFromTable("StackBottomShiftDown") } // \sigma_12 in TeX
var stretchStackBottomShiftDown:CGFloat { constantFromTable("StretchStackBottomShiftDown") }
var stretchStackGapAboveMin:CGFloat { constantFromTable("StretchStackGapAboveMin") }
var stretchStackGapBelowMin:CGFloat { constantFromTable("StretchStackGapBelowMin") }
var stretchStackTopShiftUp:CGFloat { constantFromTable("StretchStackTopShiftUp") }
// MARK: - super/sub scripts
var superscriptShiftUp:CGFloat { constantFromTable("SuperscriptShiftUp") } // \sigma_13, \sigma_14 in TeX
var superscriptShiftUpCramped:CGFloat { constantFromTable("SuperscriptShiftUpCramped") } // \sigma_15 in TeX
var subscriptShiftDown:CGFloat { constantFromTable("SubscriptShiftDown") } // \sigma_16, \sigma_17 in TeX
var superscriptBaselineDropMax:CGFloat { constantFromTable("SuperscriptBaselineDropMax") } // \sigma_18 in TeX
var subscriptBaselineDropMin:CGFloat { constantFromTable("SubscriptBaselineDropMin") } // \sigma_19 in TeX
var superscriptBottomMin:CGFloat { constantFromTable("SuperscriptBottomMin") } // 1/4 \sigma_5 in TeX
var subscriptTopMax:CGFloat { constantFromTable("SubscriptTopMax") } // 4/5 \sigma_5 in TeX
var subSuperscriptGapMin:CGFloat { constantFromTable("SubSuperscriptGapMin") } // 4 \xi_8 in TeX
var superscriptBottomMaxWithSubscript:CGFloat { constantFromTable("SuperscriptBottomMaxWithSubscript") } // 4/5 \sigma_5 in TeX
var spaceAfterScript:CGFloat { constantFromTable("SpaceAfterScript") }
// MARK: - radicals
var radicalExtraAscender:CGFloat { constantFromTable("RadicalExtraAscender") } // \xi_8 in Tex
var radicalRuleThickness:CGFloat { constantFromTable("RadicalRuleThickness") } // \xi_8 in Tex
var radicalDisplayStyleVerticalGap:CGFloat { constantFromTable("RadicalDisplayStyleVerticalGap") } // \xi_8 + 1/4 \sigma_5 in Tex
var radicalVerticalGap:CGFloat { constantFromTable("RadicalVerticalGap") } // 5/4 \xi_8 in Tex
var radicalKernBeforeDegree:CGFloat { constantFromTable("RadicalKernBeforeDegree") } // 5 mu in Tex
var radicalKernAfterDegree:CGFloat { constantFromTable("RadicalKernAfterDegree") } // -10 mu in Tex
var radicalDegreeBottomRaisePercent:CGFloat { percentFromTable("RadicalDegreeBottomRaisePercent") } // 60% in Tex
// MARK: - Limits
var upperLimitBaselineRiseMin:CGFloat { constantFromTable("UpperLimitBaselineRiseMin") } // \xi_11 in TeX
var upperLimitGapMin:CGFloat { constantFromTable("UpperLimitGapMin") } // \xi_9 in TeX
var lowerLimitGapMin:CGFloat { constantFromTable("LowerLimitGapMin") } // \xi_10 in TeX
var lowerLimitBaselineDropMin:CGFloat { constantFromTable("LowerLimitBaselineDropMin") } // \xi_12 in TeX
var limitExtraAscenderDescender:CGFloat { 0 } // \xi_13 in TeX, not present in OpenType so we always set it to 0.
// MARK: - Underline
var underbarVerticalGap:CGFloat { constantFromTable("UnderbarVerticalGap") } // 3 \xi_8 in TeX
var underbarRuleThickness:CGFloat { constantFromTable("UnderbarRuleThickness") } // \xi_8 in TeX
var underbarExtraDescender:CGFloat { constantFromTable("UnderbarExtraDescender") } // \xi_8 in TeX
// MARK: - Overline
var overbarVerticalGap:CGFloat { constantFromTable("OverbarVerticalGap") } // 3 \xi_8 in TeX
var overbarRuleThickness:CGFloat { constantFromTable("OverbarRuleThickness") } // \xi_8 in TeX
var overbarExtraAscender:CGFloat { constantFromTable("OverbarExtraAscender") } // \xi_8 in TeX
// MARK: - Constants
var axisHeight:CGFloat { constantFromTable("AxisHeight") } // \sigma_22 in TeX
var scriptScaleDown:CGFloat { percentFromTable("ScriptPercentScaleDown") }
var scriptScriptScaleDown:CGFloat { percentFromTable("ScriptScriptPercentScaleDown") }
var mathLeading:CGFloat { constantFromTable("MathLeading") }
var delimitedSubFormulaMinHeight:CGFloat { constantFromTable("DelimitedSubFormulaMinHeight") }
// MARK: - Accent
var accentBaseHeight:CGFloat { constantFromTable("AccentBaseHeight") } // \fontdimen5 in TeX (x-height)
var flattenedAccentBaseHeight:CGFloat { constantFromTable("FlattenedAccentBaseHeight") }
// MARK: - Variants
let kVertVariants = "v_variants"
let kHorizVariants = "h_variants"
/** Returns an Array of all the vertical variants of the glyph if any. If
there are no variants for the glyph, the array contains the given glyph. */
func getVerticalVariantsForGlyph( _ glyph:CGGlyph) -> [NSNumber?] {
let variants = _mathTable[kVertVariants] as! NSDictionary?
return self.getVariantsForGlyph(glyph, inDictionary: variants!)
}
/** Returns an Array of all the horizontal variants of the glyph if any. If
there are no variants for the glyph, the array contains the given glyph. */
func getHorizontalVariantsForGlyph( _ glyph:CGGlyph) -> [NSNumber?] {
let variants = _mathTable[kHorizVariants] as! NSDictionary
return self.getVariantsForGlyph(glyph, inDictionary:variants)
}
func getVariantsForGlyph(_ glyph: CGGlyph, inDictionary variants:NSDictionary) -> [NSNumber?] {
let glyphName = self.font!.get(nameForGlyph: glyph)
let variantGlyphs = variants[glyphName] as! NSArray?
var glyphArray = [NSNumber]()
if variantGlyphs == nil || variantGlyphs?.count == 0 {
// There are no extra variants, so just add the current glyph to it.
let glyph = self.font!.get(glyphWithName: glyphName)
glyphArray.append(NSNumber(value:glyph))
return glyphArray
}
for gvn in variantGlyphs! {
let glyphVariantName = gvn as! String?
let variantGlyph = self.font?.get(glyphWithName: glyphVariantName!)
glyphArray.append(NSNumber(value:variantGlyph!))
}
return glyphArray
}
/** Returns a larger vertical variant of the given glyph if any.
If there is no larger version, this returns the current glyph.
*/
func getLargerGlyph(_ glyph:CGGlyph) -> CGGlyph {
let variants = _mathTable[kVertVariants] as! NSDictionary?
let glyphName = self.font?.get(nameForGlyph: glyph)
let variantGlyphs = variants![glyphName!] as! NSArray?
if variantGlyphs == nil || variantGlyphs?.count == 0 {
// There are no extra variants, so just returnt the current glyph.
return glyph
}
// Find the first variant with a different name.
for gvn in variantGlyphs! {
let glyphVariantName = gvn as! String?
if glyphVariantName != glyphName {
let variantGlyph = self.font?.get(glyphWithName: glyphVariantName!)
return variantGlyph!
}
}
// We did not find any variants of this glyph so return it.
return glyph;
}
// MARK: - Italic Correction
let kItalic = "italic"
/** Returns the italic correction for the given glyph if any. If there
isn't any this returns 0. */
func getItalicCorrection(_ glyph: CGGlyph) -> CGFloat {
let italics = _mathTable[kItalic] as! NSDictionary?
let glyphName = self.font?.get(nameForGlyph: glyph)
let val = italics![glyphName!] as! NSNumber?
// if val is nil, this returns 0.
return self.fontUnitsToPt(val?.intValue ?? 0)
}
// MARK: - Accents
let kAccents = "accents"
/** Returns the adjustment to the top accent for the given glyph if any.
If there isn't any this returns -1. */
func getTopAccentAdjustment(_ glyph: CGGlyph) -> CGFloat {
var glyph = glyph
let accents = _mathTable[kAccents] as! NSDictionary?
let glyphName = self.font?.get(nameForGlyph: glyph)
let val = accents![glyphName!] as! NSNumber?
if let val = val {
return self.fontUnitsToPt(val.intValue)
} else {
// If no top accent is defined then it is the center of the advance width.
var advances = CGSize.zero
CTFontGetAdvancesForGlyphs(self.font!.ctFont, .horizontal, &glyph, &advances, 1)
return advances.width/2
}
}
// MARK: - Glyph Construction
/** Minimum overlap of connecting glyphs during glyph construction */
var minConnectorOverlap:CGFloat { constantFromTable("MinConnectorOverlap") }
let kVertAssembly = "v_assembly"
let kAssemblyParts = "parts"
/** Returns an array of the glyph parts to be used for constructing vertical variants
of this glyph. If there is no glyph assembly defined, returns an empty array. */
func getVerticalGlyphAssembly(forGlyph glyph:CGGlyph) -> [GlyphPart] {
let assemblyTable = _mathTable[kVertAssembly] as! NSDictionary?
let glyphName = self.font?.get(nameForGlyph: glyph)
let assemblyInfo = assemblyTable![glyphName!] as! NSDictionary?
if assemblyInfo == nil {
// No vertical assembly defined for glyph
return []
}
let parts = assemblyInfo![kAssemblyParts] as! NSArray?
if parts == nil {
// parts should always have been defined, but if it isn't return nil
return []
}
var rv = [GlyphPart]()
for part in parts! {
let partInfo = part as! NSDictionary?
var part = GlyphPart()
let adv = partInfo!["advance"] as! NSNumber?
part.fullAdvance = self.fontUnitsToPt(adv!.intValue)
let end = partInfo!["endConnector"] as! NSNumber?
part.endConnectorLength = self.fontUnitsToPt(end!.intValue)
let start = partInfo!["startConnector"] as! NSNumber?
part.startConnectorLength = self.fontUnitsToPt(start!.intValue)
let ext = partInfo!["extender"] as! NSNumber?
part.isExtender = ext!.boolValue
let glyphName = partInfo!["glyph"] as! String?
part.glyph = self.font?.get(glyphWithName: glyphName!)
rv.append(part)
}
return rv
}
}

View File

@@ -0,0 +1,37 @@
//
// Created by Mike Griebling on 2022-12-31.
// Translated from an Objective-C implementation by .
//
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
import Foundation
import SwiftUI
#if os(macOS)
public class MTLabel : NSTextField {
init() {
super.init(frame: .zero)
self.stringValue = ""
self.isBezeled = false
self.drawsBackground = false
self.isEditable = false
self.isSelectable = false
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
// MARK: - Customized getter and setter methods for property text.
var text:String? {
get { super.stringValue }
set { super.stringValue = newValue! }
}
}
#endif

View File

@@ -0,0 +1,960 @@
//
// Created by Mike Griebling on 2022-12-31.
// Translated from an Objective-C implementation by Kostub Deshmukh.
//
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
import Foundation
/** A factory to create commonly used MTMathAtoms. */
public class MTMathAtomFactory {
public static let aliases = [
"lnot" : "neg",
"land" : "wedge",
"lor" : "vee",
"ne" : "neq",
"le" : "leq",
"ge" : "geq",
"lbrace" : "{",
"rbrace" : "}",
"Vert" : "|",
"gets" : "leftarrow",
"to" : "rightarrow",
"iff" : "Longleftrightarrow",
"AA" : "angstrom"
]
public static let delimiters = [
"." : "", // . means no delimiter
"(" : "(",
")" : ")",
"[" : "[",
"]" : "]",
"<" : "\u{2329}",
">" : "\u{232A}",
"/" : "/",
"\\" : "\\",
"|" : "|",
"lgroup" : "\u{27EE}",
"rgroup" : "\u{27EF}",
"||" : "\u{2016}",
"Vert" : "\u{2016}",
"vert" : "|",
"uparrow" : "\u{2191}",
"downarrow" : "\u{2193}",
"updownarrow" : "\u{2195}",
"Uparrow" : "\u{21D1}",
"Downarrow" : "\u{21D3}",
"Updownarrow" : "\u{21D5}",
"backslash" : "\\",
"rangle" : "\u{232A}",
"langle" : "\u{2329}",
"rbrace" : "}",
"}" : "}",
"{" : "{",
"lbrace" : "{",
"lceil" : "\u{2308}",
"rceil" : "\u{2309}",
"lfloor" : "\u{230A}",
"rfloor" : "\u{230B}"
]
private static let delimValueLock = NSLock()
static var _delimValueToName = [String: String]()
public static var delimValueToName: [String: String] {
if _delimValueToName.isEmpty {
var output = [String: String]()
for (key, value) in Self.delimiters {
if let existingValue = output[value] {
if key.count > existingValue.count {
continue
} else if key.count == existingValue.count {
if key.compare(existingValue) == .orderedDescending {
continue
}
}
}
output[value] = key
}
// protect lazily loading table in a multi-thread concurrent environment
delimValueLock.lock()
defer { delimValueLock.unlock() }
if _delimValueToName.isEmpty {
_delimValueToName = output
}
}
return _delimValueToName
}
public static let accents = [
"grave" : "\u{0300}",
"acute" : "\u{0301}",
"hat" : "\u{0302}", // In our implementation hat and widehat behave the same.
"tilde" : "\u{0303}", // In our implementation tilde and widetilde behave the same.
"bar" : "\u{0304}",
"breve" : "\u{0306}",
"dot" : "\u{0307}",
"ddot" : "\u{0308}",
"check" : "\u{030C}",
"vec" : "\u{20D7}",
"widehat" : "\u{0302}",
"widetilde" : "\u{0303}"
]
private static let accentValueLock = NSLock()
static var _accentValueToName: [String: String]? = nil
public static var accentValueToName: [String: String] {
if _accentValueToName == nil {
var output = [String: String]()
for (key, value) in Self.accents {
if let existingValue = output[value] {
if key.count > existingValue.count {
continue
} else if key.count == existingValue.count {
if key.compare(existingValue) == .orderedDescending {
continue
}
}
}
output[value] = key
}
// protect lazily loading table in a multi-thread concurrent environment
accentValueLock.lock()
defer { accentValueLock.unlock() }
if _accentValueToName == nil {
_accentValueToName = output
}
}
return _accentValueToName!
}
static var supportedLatexSymbolNames:[String] {
let commands = MTMathAtomFactory.supportedLatexSymbols
return commands.keys.map { String($0) }
}
static var supportedLatexSymbols: [String: MTMathAtom] = [
"square" : MTMathAtomFactory.placeholder(),
// Greek characters
"alpha" : MTMathAtom(type: .variable, value: "\u{03B1}"),
"beta" : MTMathAtom(type: .variable, value: "\u{03B2}"),
"gamma" : MTMathAtom(type: .variable, value: "\u{03B3}"),
"delta" : MTMathAtom(type: .variable, value: "\u{03B4}"),
"varepsilon" : MTMathAtom(type: .variable, value: "\u{03B5}"),
"zeta" : MTMathAtom(type: .variable, value: "\u{03B6}"),
"eta" : MTMathAtom(type: .variable, value: "\u{03B7}"),
"theta" : MTMathAtom(type: .variable, value: "\u{03B8}"),
"iota" : MTMathAtom(type: .variable, value: "\u{03B9}"),
"kappa" : MTMathAtom(type: .variable, value: "\u{03BA}"),
"lambda" : MTMathAtom(type: .variable, value: "\u{03BB}"),
"mu" : MTMathAtom(type: .variable, value: "\u{03BC}"),
"nu" : MTMathAtom(type: .variable, value: "\u{03BD}"),
"xi" : MTMathAtom(type: .variable, value: "\u{03BE}"),
"omicron" : MTMathAtom(type: .variable, value: "\u{03BF}"),
"pi" : MTMathAtom(type: .variable, value: "\u{03C0}"),
"rho" : MTMathAtom(type: .variable, value: "\u{03C1}"),
"varsigma" : MTMathAtom(type: .variable, value: "\u{03C1}"),
"sigma" : MTMathAtom(type: .variable, value: "\u{03C3}"),
"tau" : MTMathAtom(type: .variable, value: "\u{03C4}"),
"upsilon" : MTMathAtom(type: .variable, value: "\u{03C5}"),
"varphi" : MTMathAtom(type: .variable, value: "\u{03C6}"),
"chi" : MTMathAtom(type: .variable, value: "\u{03C7}"),
"psi" : MTMathAtom(type: .variable, value: "\u{03C8}"),
"omega" : MTMathAtom(type: .variable, value: "\u{03C9}"),
// We mark the following greek chars as ordinary so that we don't try
// to automatically italicize them as we do with variables.
// These characters fall outside the rules of italicization that we have defined.
"epsilon" : MTMathAtom(type: .ordinary, value: "\u{0001D716}"),
"vartheta" : MTMathAtom(type: .ordinary, value: "\u{0001D717}"),
"phi" : MTMathAtom(type: .ordinary, value: "\u{0001D719}"),
"varrho" : MTMathAtom(type: .ordinary, value: "\u{0001D71A}"),
"varpi" : MTMathAtom(type: .ordinary, value: "\u{0001D71B}"),
// Capital greek characters
"Gamma" : MTMathAtom(type: .variable, value: "\u{0393}"),
"Delta" : MTMathAtom(type: .variable, value: "\u{0394}"),
"Theta" : MTMathAtom(type: .variable, value: "\u{0398}"),
"Lambda" : MTMathAtom(type: .variable, value: "\u{039B}"),
"Xi" : MTMathAtom(type: .variable, value: "\u{039E}"),
"Pi" : MTMathAtom(type: .variable, value: "\u{03A0}"),
"Sigma" : MTMathAtom(type: .variable, value: "\u{03A3}"),
"Upsilon" : MTMathAtom(type: .variable, value: "\u{03A5}"),
"Phi" : MTMathAtom(type: .variable, value: "\u{03A6}"),
"Psi" : MTMathAtom(type: .variable, value: "\u{03A8}"),
"Omega" : MTMathAtom(type: .variable, value: "\u{03A9}"),
// Open
"lceil" : MTMathAtom(type: .open, value: "\u{2308}"),
"lfloor" : MTMathAtom(type: .open, value: "\u{230A}"),
"langle" : MTMathAtom(type: .open, value: "\u{27E8}"),
"lgroup" : MTMathAtom(type: .open, value: "\u{27EE}"),
// Close
"rceil" : MTMathAtom(type: .close, value: "\u{2309}"),
"rfloor" : MTMathAtom(type: .close, value: "\u{230B}"),
"rangle" : MTMathAtom(type: .close, value: "\u{27E9}"),
"rgroup" : MTMathAtom(type: .close, value: "\u{27EF}"),
// Arrows
"leftarrow" : MTMathAtom(type: .relation, value: "\u{2190}"),
"uparrow" : MTMathAtom(type: .relation, value: "\u{2191}"),
"rightarrow" : MTMathAtom(type: .relation, value: "\u{2192}"),
"downarrow" : MTMathAtom(type: .relation, value: "\u{2193}"),
"leftrightarrow" : MTMathAtom(type: .relation, value: "\u{2194}"),
"updownarrow" : MTMathAtom(type: .relation, value: "\u{2195}"),
"nwarrow" : MTMathAtom(type: .relation, value: "\u{2196}"),
"nearrow" : MTMathAtom(type: .relation, value: "\u{2197}"),
"searrow" : MTMathAtom(type: .relation, value: "\u{2198}"),
"swarrow" : MTMathAtom(type: .relation, value: "\u{2199}"),
"mapsto" : MTMathAtom(type: .relation, value: "\u{21A6}"),
"Leftarrow" : MTMathAtom(type: .relation, value: "\u{21D0}"),
"Uparrow" : MTMathAtom(type: .relation, value: "\u{21D1}"),
"Rightarrow" : MTMathAtom(type: .relation, value: "\u{21D2}"),
"Downarrow" : MTMathAtom(type: .relation, value: "\u{21D3}"),
"Leftrightarrow" : MTMathAtom(type: .relation, value: "\u{21D4}"),
"Updownarrow" : MTMathAtom(type: .relation, value: "\u{21D5}"),
"longleftarrow" : MTMathAtom(type: .relation, value: "\u{27F5}"),
"longrightarrow" : MTMathAtom(type: .relation, value: "\u{27F6}"),
"longleftrightarrow" : MTMathAtom(type: .relation, value: "\u{27F7}"),
"Longleftarrow" : MTMathAtom(type: .relation, value: "\u{27F8}"),
"Longrightarrow" : MTMathAtom(type: .relation, value: "\u{27F9}"),
"Longleftrightarrow" : MTMathAtom(type: .relation, value: "\u{27FA}"),
// Relations
"leq" : MTMathAtom(type: .relation, value: UnicodeSymbol.lessEqual),
"geq" : MTMathAtom(type: .relation, value: UnicodeSymbol.greaterEqual),
"neq" : MTMathAtom(type: .relation, value: UnicodeSymbol.notEqual),
"in" : MTMathAtom(type: .relation, value: "\u{2208}"),
"notin" : MTMathAtom(type: .relation, value: "\u{2209}"),
"ni" : MTMathAtom(type: .relation, value: "\u{220B}"),
"propto" : MTMathAtom(type: .relation, value: "\u{221D}"),
"mid" : MTMathAtom(type: .relation, value: "\u{2223}"),
"parallel" : MTMathAtom(type: .relation, value: "\u{2225}"),
"sim" : MTMathAtom(type: .relation, value: "\u{223C}"),
"simeq" : MTMathAtom(type: .relation, value: "\u{2243}"),
"cong" : MTMathAtom(type: .relation, value: "\u{2245}"),
"approx" : MTMathAtom(type: .relation, value: "\u{2248}"),
"asymp" : MTMathAtom(type: .relation, value: "\u{224D}"),
"doteq" : MTMathAtom(type: .relation, value: "\u{2250}"),
"equiv" : MTMathAtom(type: .relation, value: "\u{2261}"),
"gg" : MTMathAtom(type: .relation, value: "\u{226B}"),
"ll" : MTMathAtom(type: .relation, value: "\u{226A}"),
"prec" : MTMathAtom(type: .relation, value: "\u{227A}"),
"succ" : MTMathAtom(type: .relation, value: "\u{227B}"),
"subset" : MTMathAtom(type: .relation, value: "\u{2282}"),
"supset" : MTMathAtom(type: .relation, value: "\u{2283}"),
"subseteq" : MTMathAtom(type: .relation, value: "\u{2286}"),
"supseteq" : MTMathAtom(type: .relation, value: "\u{2287}"),
"sqsubset" : MTMathAtom(type: .relation, value: "\u{228F}"),
"sqsupset" : MTMathAtom(type: .relation, value: "\u{2290}"),
"sqsubseteq" : MTMathAtom(type: .relation, value: "\u{2291}"),
"sqsupseteq" : MTMathAtom(type: .relation, value: "\u{2292}"),
"models" : MTMathAtom(type: .relation, value: "\u{22A7}"),
"perp" : MTMathAtom(type: .relation, value: "\u{27C2}"),
"implies" : MTMathAtom(type: .relation, value: "\u{27F9}"),
// operators
"times" : MTMathAtomFactory.times(),
"div" : MTMathAtomFactory.divide(),
"pm" : MTMathAtom(type: .binaryOperator, value: "\u{00B1}"),
"dagger" : MTMathAtom(type: .binaryOperator, value: "\u{2020}"),
"ddagger" : MTMathAtom(type: .binaryOperator, value: "\u{2021}"),
"mp" : MTMathAtom(type: .binaryOperator, value: "\u{2213}"),
"setminus" : MTMathAtom(type: .binaryOperator, value: "\u{2216}"),
"ast" : MTMathAtom(type: .binaryOperator, value: "\u{2217}"),
"circ" : MTMathAtom(type: .binaryOperator, value: "\u{2218}"),
"bullet" : MTMathAtom(type: .binaryOperator, value: "\u{2219}"),
"wedge" : MTMathAtom(type: .binaryOperator, value: "\u{2227}"),
"vee" : MTMathAtom(type: .binaryOperator, value: "\u{2228}"),
"cap" : MTMathAtom(type: .binaryOperator, value: "\u{2229}"),
"cup" : MTMathAtom(type: .binaryOperator, value: "\u{222A}"),
"wr" : MTMathAtom(type: .binaryOperator, value: "\u{2240}"),
"uplus" : MTMathAtom(type: .binaryOperator, value: "\u{228E}"),
"sqcap" : MTMathAtom(type: .binaryOperator, value: "\u{2293}"),
"sqcup" : MTMathAtom(type: .binaryOperator, value: "\u{2294}"),
"oplus" : MTMathAtom(type: .binaryOperator, value: "\u{2295}"),
"ominus" : MTMathAtom(type: .binaryOperator, value: "\u{2296}"),
"otimes" : MTMathAtom(type: .binaryOperator, value: "\u{2297}"),
"oslash" : MTMathAtom(type: .binaryOperator, value: "\u{2298}"),
"odot" : MTMathAtom(type: .binaryOperator, value: "\u{2299}"),
"star" : MTMathAtom(type: .binaryOperator, value: "\u{22C6}"),
"cdot" : MTMathAtom(type: .binaryOperator, value: "\u{22C5}"),
"amalg" : MTMathAtom(type: .binaryOperator, value: "\u{2A3F}"),
// No limit operators
"log" : MTMathAtomFactory.operatorWithName( "log", limits: false),
"lg" : MTMathAtomFactory.operatorWithName( "lg", limits: false),
"ln" : MTMathAtomFactory.operatorWithName( "ln", limits: false),
"sin" : MTMathAtomFactory.operatorWithName( "sin", limits: false),
"arcsin" : MTMathAtomFactory.operatorWithName( "arcsin", limits: false),
"sinh" : MTMathAtomFactory.operatorWithName( "sinh", limits: false),
"cos" : MTMathAtomFactory.operatorWithName( "cos", limits: false),
"arccos" : MTMathAtomFactory.operatorWithName( "arccos", limits: false),
"cosh" : MTMathAtomFactory.operatorWithName( "cosh", limits: false),
"tan" : MTMathAtomFactory.operatorWithName( "tan", limits: false),
"arctan" : MTMathAtomFactory.operatorWithName( "arctan", limits: false),
"tanh" : MTMathAtomFactory.operatorWithName( "tanh", limits: false),
"cot" : MTMathAtomFactory.operatorWithName( "cot", limits: false),
"coth" : MTMathAtomFactory.operatorWithName( "coth", limits: false),
"sec" : MTMathAtomFactory.operatorWithName( "sec", limits: false),
"csc" : MTMathAtomFactory.operatorWithName( "csc", limits: false),
"arg" : MTMathAtomFactory.operatorWithName( "arg", limits: false),
"ker" : MTMathAtomFactory.operatorWithName( "ker", limits: false),
"dim" : MTMathAtomFactory.operatorWithName( "dim", limits: false),
"hom" : MTMathAtomFactory.operatorWithName( "hom", limits: false),
"exp" : MTMathAtomFactory.operatorWithName( "exp", limits: false),
"deg" : MTMathAtomFactory.operatorWithName( "deg", limits: false),
"mod" : MTMathAtomFactory.operatorWithName("mod", limits: false),
// Limit operators
"lim" : MTMathAtomFactory.operatorWithName( "lim", limits: true),
"limsup" : MTMathAtomFactory.operatorWithName( "lim sup", limits: true),
"liminf" : MTMathAtomFactory.operatorWithName( "lim inf", limits: true),
"max" : MTMathAtomFactory.operatorWithName( "max", limits: true),
"min" : MTMathAtomFactory.operatorWithName( "min", limits: true),
"sup" : MTMathAtomFactory.operatorWithName( "sup", limits: true),
"inf" : MTMathAtomFactory.operatorWithName( "inf", limits: true),
"det" : MTMathAtomFactory.operatorWithName( "det", limits: true),
"Pr" : MTMathAtomFactory.operatorWithName( "Pr", limits: true),
"gcd" : MTMathAtomFactory.operatorWithName( "gcd", limits: true),
// Large operators
"prod" : MTMathAtomFactory.operatorWithName( "\u{220F}", limits: true),
"coprod" : MTMathAtomFactory.operatorWithName( "\u{2210}", limits: true),
"sum" : MTMathAtomFactory.operatorWithName( "\u{2211}", limits: true),
"int" : MTMathAtomFactory.operatorWithName( "\u{222B}", limits: false),
"iint" : MTMathAtomFactory.operatorWithName( "\u{222C}", limits: false),
"iiint" : MTMathAtomFactory.operatorWithName( "\u{222D}", limits: false),
"iiiint" : MTMathAtomFactory.operatorWithName( "\u{2A0C}", limits: false),
"oint" : MTMathAtomFactory.operatorWithName( "\u{222E}", limits: false),
"bigwedge" : MTMathAtomFactory.operatorWithName( "\u{22C0}", limits: true),
"bigvee" : MTMathAtomFactory.operatorWithName( "\u{22C1}", limits: true),
"bigcap" : MTMathAtomFactory.operatorWithName( "\u{22C2}", limits: true),
"bigcup" : MTMathAtomFactory.operatorWithName( "\u{22C3}", limits: true),
"bigodot" : MTMathAtomFactory.operatorWithName( "\u{2A00}", limits: true),
"bigoplus" : MTMathAtomFactory.operatorWithName( "\u{2A01}", limits: true),
"bigotimes" : MTMathAtomFactory.operatorWithName( "\u{2A02}", limits: true),
"biguplus" : MTMathAtomFactory.operatorWithName( "\u{2A04}", limits: true),
"bigsqcup" : MTMathAtomFactory.operatorWithName( "\u{2A06}", limits: true),
// Latex command characters
"{" : MTMathAtom(type: .open, value: "{"),
"}" : MTMathAtom(type: .close, value: "}"),
"$" : MTMathAtom(type: .ordinary, value: "$"),
"&" : MTMathAtom(type: .ordinary, value: "&"),
"#" : MTMathAtom(type: .ordinary, value: "#"),
"%" : MTMathAtom(type: .ordinary, value: "%"),
"_" : MTMathAtom(type: .ordinary, value: "_"),
" " : MTMathAtom(type: .ordinary, value: " "),
"backslash" : MTMathAtom(type: .ordinary, value: "\\"),
// Punctuation
// Note: \colon is different from : which is a relation
"colon" : MTMathAtom(type: .punctuation, value: ":"),
"cdotp" : MTMathAtom(type: .punctuation, value: "\u{00B7}"),
// Other symbols
"degree" : MTMathAtom(type: .ordinary, value: "\u{00B0}"),
"neg" : MTMathAtom(type: .ordinary, value: "\u{00AC}"),
"angstrom" : MTMathAtom(type: .ordinary, value: "\u{00C5}"),
"aa" : MTMathAtom(type: .ordinary, value: "\u{00E5}"), // NEW å
"ae" : MTMathAtom(type: .ordinary, value: "\u{00E6}"), // NEW æ
"o" : MTMathAtom(type: .ordinary, value: "\u{00F8}"), // NEW ø
"oe" : MTMathAtom(type: .ordinary, value: "\u{0153}"), // NEW œ
"ss" : MTMathAtom(type: .ordinary, value: "\u{00DF}"), // NEW ß
"cc" : MTMathAtom(type: .ordinary, value: "\u{00E7}"), // NEW ç
"CC" : MTMathAtom(type: .ordinary, value: "\u{00C7}"), // NEW Ç
"O" : MTMathAtom(type: .ordinary, value: "\u{00D8}"), // NEW Ø
"AE" : MTMathAtom(type: .ordinary, value: "\u{00C6}"), // NEW Æ
"OE" : MTMathAtom(type: .ordinary, value: "\u{0152}"), // NEW Œ
"|" : MTMathAtom(type: .ordinary, value: "\u{2016}"),
"vert" : MTMathAtom(type: .ordinary, value: "|"),
"ldots" : MTMathAtom(type: .ordinary, value: "\u{2026}"),
"prime" : MTMathAtom(type: .ordinary, value: "\u{2032}"),
"hbar" : MTMathAtom(type: .ordinary, value: "\u{210F}"),
"lbar" : MTMathAtom(type: .ordinary, value: "\u{019B}"), // NEW ƛ
"Im" : MTMathAtom(type: .ordinary, value: "\u{2111}"),
"ell" : MTMathAtom(type: .ordinary, value: "\u{2113}"),
"wp" : MTMathAtom(type: .ordinary, value: "\u{2118}"),
"Re" : MTMathAtom(type: .ordinary, value: "\u{211C}"),
"mho" : MTMathAtom(type: .ordinary, value: "\u{2127}"),
"aleph" : MTMathAtom(type: .ordinary, value: "\u{2135}"),
"forall" : MTMathAtom(type: .ordinary, value: "\u{2200}"),
"exists" : MTMathAtom(type: .ordinary, value: "\u{2203}"),
"nexists" : MTMathAtom(type: .ordinary, value: "\u{2204}"),
"emptyset" : MTMathAtom(type: .ordinary, value: "\u{2205}"),
"nabla" : MTMathAtom(type: .ordinary, value: "\u{2207}"),
"infty" : MTMathAtom(type: .ordinary, value: "\u{221E}"),
"angle" : MTMathAtom(type: .ordinary, value: "\u{2220}"),
"top" : MTMathAtom(type: .ordinary, value: "\u{22A4}"),
"bot" : MTMathAtom(type: .ordinary, value: "\u{22A5}"),
"vdots" : MTMathAtom(type: .ordinary, value: "\u{22EE}"),
"cdots" : MTMathAtom(type: .ordinary, value: "\u{22EF}"),
"ddots" : MTMathAtom(type: .ordinary, value: "\u{22F1}"),
"triangle" : MTMathAtom(type: .ordinary, value: "\u{25B3}"),
"imath" : MTMathAtom(type: .ordinary, value: "\u{0001D6A4}"),
"jmath" : MTMathAtom(type: .ordinary, value: "\u{0001D6A5}"),
"upquote" : MTMathAtom(type: .ordinary, value: "\u{0027}"),
"partial" : MTMathAtom(type: .ordinary, value: "\u{0001D715}"),
// Spacing
"," : MTMathSpace(space: 3),
">" : MTMathSpace(space: 4),
";" : MTMathSpace(space: 5),
"!" : MTMathSpace(space: -3),
"quad" : MTMathSpace(space: 18), // quad = 1em = 18mu
"qquad" : MTMathSpace(space: 36), // qquad = 2em
// Style
"displaystyle" : MTMathStyle(style: .display),
"textstyle" : MTMathStyle(style: .text),
"scriptstyle" : MTMathStyle(style: .script),
"scriptscriptstyle" : MTMathStyle(style: .scriptOfScript),
]
static var supportedAccentedCharacters: [Character: (String, String)] = [
// Acute accents
"á": ("acute", "a"), "é": ("acute", "e"), "í": ("acute", "i"),
"ó": ("acute", "o"), "ú": ("acute", "u"), "ý": ("acute", "y"),
// Grave accents
"à": ("grave", "a"), "è": ("grave", "e"), "ì": ("grave", "i"),
"ò": ("grave", "o"), "ù": ("grave", "u"),
// Circumflex
"â": ("hat", "a"), "ê": ("hat", "e"), "î": ("hat", "i"),
"ô": ("hat", "o"), "û": ("hat", "u"),
// Umlaut/dieresis
"ä": ("ddot", "a"), "ë": ("ddot", "e"), "ï": ("ddot", "i"),
"ö": ("ddot", "o"), "ü": ("ddot", "u"), "ÿ": ("ddot", "y"),
// Tilde
"ã": ("tilde", "a"), "ñ": ("tilde", "n"), "õ": ("tilde", "o"),
// Special characters
"ç": ("cc", ""), "ø": ("o", ""), "å": ("aa", ""), "æ": ("ae", ""),
"œ": ("oe", ""), "ß": ("ss", ""),
"'": ("upquote", ""), // this may be dangerous in math mode
// Upper case variants
"Á": ("acute", "A"), "É": ("acute", "E"), "Í": ("acute", "I"),
"Ó": ("acute", "O"), "Ú": ("acute", "U"), "Ý": ("acute", "Y"),
"À": ("grave", "A"), "È": ("grave", "E"), "Ì": ("grave", "I"),
"Ò": ("grave", "O"), "Ù": ("grave", "U"),
"Â": ("hat", "A"), "Ê": ("hat", "E"), "Î": ("hat", "I"),
"Ô": ("hat", "O"), "Û": ("hat", "U"),
"Ä": ("ddot", "A"), "Ë": ("ddot", "E"), "Ï": ("ddot", "I"),
"Ö": ("ddot", "O"), "Ü": ("ddot", "U"),
"Ã": ("tilde", "A"), "Ñ": ("tilde", "N"), "Õ": ("tilde", "O"),
"Ç": ("CC", ""),
"Ø": ("O", ""),
"Å": ("AA", ""),
"Æ": ("AE", ""),
"Œ": ("OE", ""),
]
private static let textToLatexLock = NSLock()
static var _textToLatexSymbolName: [String: String]? = nil
public static var textToLatexSymbolName: [String: String] {
get {
if self._textToLatexSymbolName == nil {
var output = [String: String]()
for (key, atom) in Self.supportedLatexSymbols {
if atom.nucleus.count == 0 {
continue
}
if let existingText = output[atom.nucleus] {
// If there are 2 key for the same symbol, choose one deterministically.
if key.count > existingText.count {
// Keep the shorter command
continue
} else if key.count == existingText.count {
// If the length is the same, keep the alphabetically first
if key.compare(existingText) == .orderedDescending {
continue
}
}
}
output[atom.nucleus] = key
}
// protect lazily loading table in a multi-thread concurrent environment
textToLatexLock.lock()
defer { textToLatexLock.unlock() }
if self._textToLatexSymbolName == nil {
self._textToLatexSymbolName = output
}
}
return self._textToLatexSymbolName!
}
// make textToLatexSymbolName readonly (allows internal load)
// entries can be lazily added with NSLock protection.
// set {
// self._textToLatexSymbolName = newValue
// }
}
// public static let sharedInstance = MTMathAtomFactory()
static let fontStyles : [String: MTFontStyle] = [
"mathnormal" : .defaultStyle,
"mathrm": .roman,
"textrm": .roman,
"rm": .roman,
"mathbf": .bold,
"bf": .bold,
"textbf": .bold,
"mathcal": .caligraphic,
"cal": .caligraphic,
"mathtt": .typewriter,
"texttt": .typewriter,
"mathit": .italic,
"textit": .italic,
"mit": .italic,
"mathsf": .sansSerif,
"textsf": .sansSerif,
"mathfrak": .fraktur,
"frak": .fraktur,
"mathbb": .blackboard,
"mathbfit": .boldItalic,
"bm": .boldItalic,
"text": .roman,
]
public static func fontStyleWithName(_ fontName:String) -> MTFontStyle? {
fontStyles[fontName]
}
public static func fontNameForStyle(_ fontStyle:MTFontStyle) -> String {
switch fontStyle {
case .defaultStyle: return "mathnormal"
case .roman: return "mathrm"
case .bold: return "mathbf"
case .fraktur: return "mathfrak"
case .caligraphic: return "mathcal"
case .italic: return "mathit"
case .sansSerif: return "mathsf"
case .blackboard: return "mathbb"
case .typewriter: return "mathtt"
case .boldItalic: return "bm"
}
}
/// Returns an atom for the multiplication sign (i.e., \times or "*")
public static func times() -> MTMathAtom {
MTMathAtom(type: .binaryOperator, value: UnicodeSymbol.multiplication)
}
/// Returns an atom for the division sign (i.e., \div or "/")
public static func divide() -> MTMathAtom {
MTMathAtom(type: .binaryOperator, value: UnicodeSymbol.division)
}
/// Returns an atom which is a placeholder square
public static func placeholder() -> MTMathAtom {
MTMathAtom(type: .placeholder, value: UnicodeSymbol.whiteSquare)
}
/** Returns a fraction with a placeholder for the numerator and denominator */
public static func placeholderFraction() -> MTFraction {
let frac = MTFraction()
frac.numerator = MTMathList()
frac.numerator?.add(placeholder())
frac.denominator = MTMathList()
frac.denominator?.add(placeholder())
return frac
}
/** Returns a square root with a placeholder as the radicand. */
public static func placeholderSquareRoot() -> MTRadical {
let rad = MTRadical()
rad.radicand = MTMathList()
rad.radicand?.add(placeholder())
return rad
}
/** Returns a radical with a placeholder as the radicand. */
public static func placeholderRadical() -> MTRadical {
let rad = MTRadical()
rad.radicand = MTMathList()
rad.degree = MTMathList()
rad.radicand?.add(placeholder())
rad.degree?.add(placeholder())
return rad
}
public static func atom(fromAccentedCharacter ch: Character) -> MTMathAtom? {
if let symbol = supportedAccentedCharacters[ch] {
// first handle any special characters
if let atom = atom(forLatexSymbol: symbol.0) {
return atom
}
if let accent = MTMathAtomFactory.accent(withName: symbol.0) {
// The command is an accent
let list = MTMathList()
let ch = Array(symbol.1)[0]
list.add(atom(forCharacter: ch))
accent.innerList = list
return accent
}
}
return nil
}
// MARK: -
/** Gets the atom with the right type for the given character. If an atom
cannot be determined for a given character this returns nil.
This function follows latex conventions for assigning types to the atoms.
The following characters are not supported and will return nil:
- Any non-ascii character.
- Any control character or spaces (< 0x21)
- Latex control chars: $ % # & ~ '
- Chars with special meaning in latex: ^ _ { } \
All other characters, including those with accents, will have a non-nil atom returned.
*/
public static func atom(forCharacter ch: Character) -> MTMathAtom? {
let chStr = String(ch)
switch chStr {
case "\u{0410}"..."\u{044F}":
// Cyrillic alphabet
return MTMathAtom(type: .ordinary, value: chStr)
case _ where supportedAccentedCharacters.keys.contains(ch):
// support for áéíóúýàèìòùâêîôûäëïöüÿãñõçøåæœß'ÁÉÍÓÚÝÀÈÌÒÙÂÊÎÔÛÄËÏÖÜÃÑÕÇØÅÆŒ
return atom(fromAccentedCharacter: ch)
case _ where ch.utf32Char < 0x0021 || ch.utf32Char > 0x007E:
return nil
case "$", "%", "#", "&", "~", "\'", "^", "_", "{", "}", "\\":
return nil
case "(", "[":
return MTMathAtom(type: .open, value: chStr)
case ")", "]", "!", "?":
return MTMathAtom(type: .close, value: chStr)
case ",", ";":
return MTMathAtom(type: .punctuation, value: chStr)
case "=", ">", "<":
return MTMathAtom(type: .relation, value: chStr)
case ":":
// Math colon is ratio. Regular colon is \colon
return MTMathAtom(type: .relation, value: "\u{2236}")
case "-":
return MTMathAtom(type: .binaryOperator, value: "\u{2212}")
case "+", "*":
return MTMathAtom(type: .binaryOperator, value: chStr)
case ".", "0"..."9":
return MTMathAtom(type: .number, value: chStr)
case "a"..."z", "A"..."Z":
return MTMathAtom(type: .variable, value: chStr)
case "\"", "/", "@", "`", "|":
return MTMathAtom(type: .ordinary, value: chStr)
default:
assertionFailure("Unknown ASCII character '\(ch)'. Should have been handled earlier.")
return nil
}
}
/** Returns a `MTMathList` with one atom per character in the given string. This function
does not do any LaTeX conversion or interpretation. It simply uses `atom(forCharacter:)` to
convert the characters to atoms. Any character that cannot be converted is ignored. */
public static func atomList(for string: String) -> MTMathList {
let list = MTMathList()
for character in string {
if let newAtom = atom(forCharacter: character) {
list.add(newAtom)
}
}
return list
}
/** Returns an atom with the right type for a given latex symbol (e.g. theta)
If the latex symbol is unknown this will return nil. This supports LaTeX aliases as well.
*/
public static func atom(forLatexSymbol name: String) -> MTMathAtom? {
var name = name
if let canonicalName = aliases[name] {
name = canonicalName
}
if let atom = supportedLatexSymbols[name] {
return atom.copy()
}
return nil
}
/** Finds the name of the LaTeX symbol name for the given atom. This function is a reverse
of the above function. If no latex symbol name corresponds to the atom, then this returns `nil`
If nucleus of the atom is empty, then this will return `nil`.
Note: This is not an exact reverse of the above in the case of aliases. If an LaTeX alias
points to a given symbol, then this function will return the original symbol name and not the
alias.
Note: This function does not convert MathSpaces to latex command names either.
*/
public static func latexSymbolName(for atom: MTMathAtom) -> String? {
guard !atom.nucleus.isEmpty else { return nil }
return Self.textToLatexSymbolName[atom.nucleus]
}
/** Define a latex symbol for rendering. This function allows defining custom symbols that are
not already present in the default set, or override existing symbols with new meaning.
e.g. to define a symbol for "lcm" one can call:
`MTMathAtomFactory.add(latexSymbol:"lcm", value:MTMathAtomFactory.operatorWithName("lcm", limits: false))` */
public static func add(latexSymbol name: String, value: MTMathAtom) {
let _ = Self.textToLatexSymbolName
// above force textToLatexSymbolName to initialise first, _textToLatexSymbolName also initialized.
// protect lazily loading table in a multi-thread concurrent environment
textToLatexLock.lock()
defer { textToLatexLock.unlock() }
supportedLatexSymbols[name] = value
Self._textToLatexSymbolName?[value.nucleus] = name
}
/** Returns a large opertor for the given name. If limits is true, limits are set up on
the operator and displayed differently. */
public static func operatorWithName(_ name: String, limits: Bool) -> MTLargeOperator {
MTLargeOperator(value: name, limits: limits)
}
/** Returns an accent with the given name. The name of the accent is the LaTeX name
such as `grave`, `hat` etc. If the name is not a recognized accent name, this
returns nil. The `innerList` of the returned `MTAccent` is nil.
*/
public static func accent(withName name: String) -> MTAccent? {
if let accentValue = accents[name] {
return MTAccent(value: accentValue)
}
return nil
}
/** Returns the accent name for the given accent. This is the reverse of the above
function. */
public static func accentName(_ accent: MTAccent) -> String? {
accentValueToName[accent.nucleus]
}
/** Creates a new boundary atom for the given delimiter name. If the delimiter name
is not recognized it returns nil. A delimiter name can be a single character such
as '(' or a latex command such as 'uparrow'.
@note In order to distinguish between the delimiter '|' and the delimiter '\|' the delimiter '\|'
the has been renamed to '||'.
*/
public static func boundary(forDelimiter name: String) -> MTMathAtom? {
if let delimValue = Self.delimiters[name] {
return MTMathAtom(type: .boundary, value: delimValue)
}
return nil
}
/** Returns the delimiter name for a boundary atom. This is a reverse of the above function.
If the atom is not a boundary atom or if the delimiter value is unknown this returns `nil`.
@note This is not an exact reverse of the above function. Some delimiters have two names (e.g.
`<` and `langle`) and this function always returns the shorter name.
*/
public static func getDelimiterName(of boundary: MTMathAtom) -> String? {
guard boundary.type == .boundary else { return nil }
return Self.delimValueToName[boundary.nucleus]
}
/** Returns a fraction with the given numerator and denominator. */
public static func fraction(withNumerator num: MTMathList, denominator denom: MTMathList) -> MTFraction {
let frac = MTFraction()
frac.numerator = num
frac.denominator = denom
return frac
}
public static func mathListForCharacters(_ chars:String) -> MTMathList? {
let list = MTMathList()
for ch in chars {
if let atom = self.atom(forCharacter: ch) {
list.add(atom)
}
}
return list
}
/** Simplification of above function when numerator and denominator are simple strings.
This function converts the strings to a `MTFraction`. */
public static func fraction(withNumeratorString numStr: String, denominatorString denomStr: String) -> MTFraction {
let num = Self.atomList(for: numStr)
let denom = Self.atomList(for: denomStr)
return Self.fraction(withNumerator: num, denominator: denom)
}
static let matrixEnvs = [
"matrix": [],
"pmatrix": ["(", ")"],
"bmatrix": ["[", "]"],
"Bmatrix": ["{", "}"],
"vmatrix": ["vert", "vert"],
"Vmatrix": ["Vert", "Vert"],
"smallmatrix": [],
// Starred versions with optional alignment
"matrix*": [],
"pmatrix*": ["(", ")"],
"bmatrix*": ["[", "]"],
"Bmatrix*": ["{", "}"],
"vmatrix*": ["vert", "vert"],
"Vmatrix*": ["Vert", "Vert"]
]
/** Builds a table for a given environment with the given rows. Returns a `MTMathAtom` containing the
table and any other atoms necessary for the given environment. Returns nil and sets error
if the table could not be built.
@param env The environment to use to build the table. If the env is nil, then the default table is built.
@note The reason this function returns a `MTMathAtom` and not a `MTMathTable` is because some
matrix environments are have builtin delimiters added to the table and hence are returned as inner atoms.
*/
public static func table(withEnvironment env: String?, alignment: MTColumnAlignment? = nil, rows: [[MTMathList]], error:inout NSError?) -> MTMathAtom? {
let table = MTMathTable(environment: env)
for i in 0..<rows.count {
let row = rows[i]
for j in 0..<row.count {
table.set(cell: row[j], forRow: i, column: j)
}
}
if env == nil {
table.interColumnSpacing = 0
table.interRowAdditionalSpacing = 1
for i in 0..<table.numColumns {
table.set(alignment: .left, forColumn: i)
}
return table
} else if let env = env {
if let delims = matrixEnvs[env] {
table.environment = "matrix"
// smallmatrix uses script style and tighter spacing for inline use
let isSmallMatrix = (env == "smallmatrix")
table.interRowAdditionalSpacing = 0
table.interColumnSpacing = isSmallMatrix ? 6 : 18
let style = MTMathStyle(style: isSmallMatrix ? .script : .text)
for i in 0..<table.cells.count {
for j in 0..<table.cells[i].count {
table.cells[i][j].insert(style, at: 0)
}
}
// Apply alignment for starred matrix environments
if let align = alignment {
for col in 0..<table.numColumns {
table.set(alignment: align, forColumn: col)
}
}
if delims.count == 2 {
let inner = MTInner()
inner.leftBoundary = Self.boundary(forDelimiter: delims[0])
inner.rightBoundary = Self.boundary(forDelimiter: delims[1])
inner.innerList = MTMathList(atoms: [table])
return inner
} else {
return table
}
} else if env == "eqalign" || env == "split" || env == "aligned" {
if table.numColumns != 2 {
let message = "\(env) environment can only have 2 columns"
if error == nil {
error = NSError(domain: MTParseError, code: MTParseErrors.invalidNumColumns.rawValue, userInfo: [NSLocalizedDescriptionKey:message])
}
return nil
}
let spacer = MTMathAtom(type: .ordinary, value: "")
for i in 0..<table.cells.count {
if table.cells[i].count >= 2 {
table.cells[i][1].insert(spacer, at: 0)
}
}
table.interRowAdditionalSpacing = 1
table.interColumnSpacing = 0
table.set(alignment: .right, forColumn: 0)
table.set(alignment: .left, forColumn: 1)
return table
} else if env == "displaylines" || env == "gather" {
if table.numColumns != 1 {
let message = "\(env) environment can only have 1 column"
if error == nil {
error = NSError(domain: MTParseError, code: MTParseErrors.invalidNumColumns.rawValue, userInfo: [NSLocalizedDescriptionKey:message])
}
return nil
}
table.interRowAdditionalSpacing = 1
table.interColumnSpacing = 0
table.set(alignment: .center, forColumn: 0)
return table
} else if env == "eqnarray" {
if table.numColumns != 3 {
let message = "\(env) environment can only have 3 columns"
if error == nil {
error = NSError(domain: MTParseError, code: MTParseErrors.invalidNumColumns.rawValue, userInfo: [NSLocalizedDescriptionKey:message])
}
return nil
}
table.interRowAdditionalSpacing = 1
table.interColumnSpacing = 18
table.set(alignment: .right, forColumn: 0)
table.set(alignment: .center, forColumn: 1)
table.set(alignment: .left, forColumn: 2)
return table
} else if env == "cases" {
if table.numColumns != 1 && table.numColumns != 2 {
let message = "cases environment can have 1 or 2 columns"
if error == nil {
error = NSError(domain: MTParseError, code: MTParseErrors.invalidNumColumns.rawValue, userInfo: [NSLocalizedDescriptionKey:message])
}
return nil
}
table.interRowAdditionalSpacing = 0
table.interColumnSpacing = 18
table.set(alignment: .left, forColumn: 0)
if table.numColumns == 2 {
table.set(alignment: .left, forColumn: 1)
}
let style = MTMathStyle(style: .text)
for i in 0..<table.cells.count {
for j in 0..<table.cells[i].count {
table.cells[i][j].insert(style, at: 0)
}
}
let inner = MTInner()
inner.leftBoundary = Self.boundary(forDelimiter: "{")
inner.rightBoundary = Self.boundary(forDelimiter: ".")
let space = Self.atom(forLatexSymbol: ",")!
inner.innerList = MTMathList(atoms: [space, table])
return inner
} else {
let message = "Unknown environment \(env)"
error = NSError(domain: MTParseError, code: MTParseErrors.invalidEnv.rawValue, userInfo: [NSLocalizedDescriptionKey:message])
return nil
}
}
return nil
}
}

View File

@@ -0,0 +1,117 @@
//
// File.swift
//
//
// Created by Peter Tang on 12/9/2023.
//
import Foundation
#if os(iOS) || os(visionOS)
import UIKit
#endif
#if os(macOS)
import AppKit
#endif
public class MTMathImage {
public var font: MTFont? = MTFontManager.fontManager.defaultFont
public var fontSize:CGFloat {
set {
_fontSize = newValue
let font = font?.copy(withSize: newValue)
self.font = font // also forces an update
}
get { _fontSize }
}
private var _fontSize:CGFloat = 0
public let textColor: MTColor
public let labelMode: MTMathUILabelMode
public let textAlignment: MTTextAlignment
public var contentInsets: MTEdgeInsets = MTEdgeInsetsZero
public let latex: String
private(set) var intrinsicContentSize = CGSize.zero
public init(latex: String, fontSize: CGFloat, textColor: MTColor, labelMode: MTMathUILabelMode = .display, textAlignment: MTTextAlignment = .center) {
self.latex = latex
self.textColor = textColor
self.labelMode = labelMode
self.textAlignment = textAlignment
self.fontSize = fontSize
}
}
extension MTMathImage {
public var currentStyle: MTLineStyle {
switch labelMode {
case .display: return .display
case .text: return .text
}
}
private func intrinsicContentSize(_ displayList: MTMathListDisplay) -> CGSize {
CGSize(width: displayList.width + contentInsets.left + contentInsets.right,
height: displayList.ascent + displayList.descent + contentInsets.top + contentInsets.bottom)
}
public func asImage() -> (NSError?, MTImage?) {
func layoutImage(size: CGSize, displayList: MTMathListDisplay) {
var textX = CGFloat(0)
switch self.textAlignment {
case .left: textX = contentInsets.left
case .center: textX = (size.width - contentInsets.left - contentInsets.right - displayList.width) / 2 + contentInsets.left
case .right: textX = size.width - displayList.width - contentInsets.right
}
let availableHeight = size.height - contentInsets.bottom - contentInsets.top
// center things vertically
var height = displayList.ascent + displayList.descent
if height < fontSize/2 {
height = fontSize/2 // set height to half the font size
}
let textY = (availableHeight - height) / 2 + displayList.descent + contentInsets.bottom
displayList.position = CGPoint(x: textX, y: textY)
}
var error: NSError?
guard let mathList = MTMathListBuilder.build(fromString: latex, error: &error), error == nil,
let displayList = MTTypesetter.createLineForMathList(mathList, font: font, style: currentStyle) else {
return (error, nil)
}
intrinsicContentSize = intrinsicContentSize(displayList)
displayList.textColor = textColor
let size = intrinsicContentSize
layoutImage(size: size, displayList: displayList)
#if os(iOS) || os(visionOS)
let renderer = UIGraphicsImageRenderer(size: size)
let image = renderer.image { rendererContext in
rendererContext.cgContext.saveGState()
rendererContext.cgContext.concatenate(.flippedVertically(size.height))
displayList.draw(rendererContext.cgContext)
rendererContext.cgContext.restoreGState()
}
return (nil, image)
#endif
#if os(macOS)
let image = NSImage(size: size, flipped: false) { bounds in
guard let context = NSGraphicsContext.current?.cgContext else { return false }
context.saveGState()
displayList.draw(context)
context.restoreGState()
return true
}
return (nil, image)
#endif
}
}
private extension CGAffineTransform {
static func flippedVertically(_ height: CGFloat) -> CGAffineTransform {
var transform = CGAffineTransform(scaleX: 1, y: -1)
transform = transform.translatedBy(x: 0, y: -height)
return transform
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,831 @@
//
// Created by Mike Griebling on 2022-12-31.
// Translated from an Objective-C implementation by Kostub Deshmukh.
//
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
import Foundation
import QuartzCore
import CoreText
import SwiftUI
func isIos6Supported() -> Bool {
if !MTDisplay.initialized {
#if os(iOS) || os(visionOS)
let reqSysVer = "6.0"
let currSysVer = UIDevice.current.systemVersion
if currSysVer.compare(reqSysVer, options: .numeric) != .orderedAscending {
MTDisplay.supported = true
}
#else
MTDisplay.supported = true
#endif
MTDisplay.initialized = true
}
return MTDisplay.supported
}
// The Downshift protocol allows an MTDisplay to be shifted down by a given amount.
protocol DownShift {
var shiftDown:CGFloat { set get }
}
// MARK: - MTDisplay
/// The base class for rendering a math equation.
public class MTDisplay:NSObject {
// needed for isIos6Supported() func above
static var initialized = false
static var supported = false
/// Draws itself in the given graphics context.
public func draw(_ context:CGContext) {
if self.localBackgroundColor != nil {
context.saveGState()
context.setBlendMode(.normal)
context.setFillColor(self.localBackgroundColor!.cgColor)
context.fill(self.displayBounds())
context.restoreGState()
}
}
/// Gets the bounding rectangle for the MTDisplay
func displayBounds() -> CGRect {
CGRectMake(self.position.x, self.position.y - self.descent, self.width, self.ascent + self.descent)
}
/// For debugging. Shows the object in quick look in Xcode.
#if os(iOS) || os(visionOS)
func debugQuickLookObject() -> Any {
let size = CGSizeMake(self.width, self.ascent + self.descent);
UIGraphicsBeginImageContext(size);
// get a reference to that context we created
let context = UIGraphicsGetCurrentContext()!
// translate/flip the graphics context (for transforming from CG* coords to UI* coords
context.translateBy(x: 0, y: size.height);
context.scaleBy(x: 1.0, y: -1.0);
// move the position to (0,0)
context.translateBy(x: -self.position.x, y: -self.position.y);
// Move the line up by self.descent
context.translateBy(x: 0, y: self.descent);
// Draw self on context
self.draw(context)
// generate a new UIImage from the graphics context we drew onto
let img = UIGraphicsGetImageFromCurrentImageContext()
return img as Any
}
#endif
/// The distance from the axis to the top of the display
public var ascent:CGFloat = 0
/// The distance from the axis to the bottom of the display
public var descent:CGFloat = 0
/// The width of the display
public var width:CGFloat = 0
/// Position of the display with respect to the parent view or display.
var position = CGPoint.zero
/// The range of characters supported by this item
public var range:NSRange=NSMakeRange(0, 0)
/// Whether the display has a subscript/superscript following it.
public var hasScript:Bool = false
/// The text color for this display
var textColor: MTColor?
/// The local color, if the color was mutated local with the color command
var localTextColor: MTColor?
/// The background color for this display
var localBackgroundColor: MTColor?
}
/// Special class to be inherited from that implements the DownShift protocol
class MTDisplayDS : MTDisplay, DownShift {
var shiftDown: CGFloat = 0
}
// MARK: - MTCTLineDisplay
/// A rendering of a single CTLine as an MTDisplay
public class MTCTLineDisplay : MTDisplay {
/// The CTLine being displayed
public var line:CTLine!
/// The attributed string used to generate the CTLineRef. Note setting this does not reset the dimensions of
/// the display. So set only when
var attributedString:NSAttributedString? {
didSet {
line = CTLineCreateWithAttributedString(attributedString!)
}
}
/// An array of MTMathAtoms that this CTLine displays. Used for indexing back into the MTMathList
public fileprivate(set) var atoms = [MTMathAtom]()
init(withString attrString:NSAttributedString?, position:CGPoint, range:NSRange, font:MTFont?, atoms:[MTMathAtom]) {
super.init()
self.position = position
self.attributedString = attrString
self.line = CTLineCreateWithAttributedString(attrString!)
self.range = range
self.atoms = atoms
// We can't use typographic bounds here as the ascent and descent returned are for the font and not for the line.
self.width = CTLineGetTypographicBounds(line, nil, nil, nil);
if isIos6Supported() {
let bounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds)
self.ascent = max(0, CGRectGetMaxY(bounds) - 0);
self.descent = max(0, 0 - CGRectGetMinY(bounds));
// TODO: Should we use this width vs the typographic width? They are slightly different. Don't know why.
// _width = CGRectGetMaxX(bounds);
} else {
// Our own implementation of the ios6 function to get glyph path bounds.
self.computeDimensions(font)
}
}
override var textColor: MTColor? {
set {
super.textColor = newValue
let attrStr = attributedString!.mutableCopy() as! NSMutableAttributedString
let foregroundColor = NSAttributedString.Key(kCTForegroundColorAttributeName as String)
attrStr.addAttribute(foregroundColor, value:self.textColor!.cgColor, range:NSMakeRange(0, attrStr.length))
self.attributedString = attrStr
}
get { super.textColor }
}
func computeDimensions(_ font:MTFont?) {
let runs = CTLineGetGlyphRuns(line) as NSArray
for obj in runs {
let run = obj as! CTRun?
let numGlyphs = CTRunGetGlyphCount(run!)
var glyphs = [CGGlyph]()
glyphs.reserveCapacity(numGlyphs)
CTRunGetGlyphs(run!, CFRangeMake(0, numGlyphs), &glyphs);
let bounds = CTFontGetBoundingRectsForGlyphs(font!.ctFont, .horizontal, glyphs, nil, numGlyphs);
let ascent = max(0, CGRectGetMaxY(bounds) - 0);
// Descent is how much the line goes below the origin. However if the line is all above the origin, then descent can't be negative.
let descent = max(0, 0 - CGRectGetMinY(bounds));
if (ascent > self.ascent) {
self.ascent = ascent;
}
if (descent > self.descent) {
self.descent = descent;
}
}
}
override public func draw(_ context: CGContext) {
super.draw(context)
context.saveGState()
context.textPosition = self.position
CTLineDraw(line, context)
context.restoreGState()
}
}
// MARK: - MTMathListDisplay
/// An MTLine is a rendered form of MTMathList in one line.
/// It can render itself using the draw method.
public class MTMathListDisplay : MTDisplay {
/**
The type of position for a line, i.e. subscript/superscript or regular.
*/
public enum LinePosition : Int {
/// Regular
case regular
/// Positioned at a subscript
case ssubscript
/// Positioned at a superscript
case superscript
}
/// Where the line is positioned
public var type:LinePosition = .regular
/// An array of MTDisplays which are positioned relative to the position of the
/// the current display.
public fileprivate(set) var subDisplays = [MTDisplay]()
/// If a subscript or superscript this denotes the location in the parent MTList. For a
/// regular list this is NSNotFound
public var index: Int = 0
init(withDisplays displays:[MTDisplay], range:NSRange) {
super.init()
self.subDisplays = displays
self.position = CGPoint.zero
self.type = .regular
self.index = NSNotFound
self.range = range
self.recomputeDimensions()
}
override var textColor: MTColor? {
set {
super.textColor = newValue
for displayAtom in self.subDisplays {
if displayAtom.localTextColor == nil {
displayAtom.textColor = newValue
} else {
displayAtom.textColor = displayAtom.localTextColor
}
}
}
get { super.textColor }
}
override public func draw(_ context: CGContext) {
super.draw(context)
context.saveGState()
// Make the current position the origin as all the positions of the sub atoms are relative to the origin.
context.translateBy(x: self.position.x, y: self.position.y)
context.textPosition = CGPoint.zero
// draw each atom separately
for displayAtom in self.subDisplays {
displayAtom.draw(context)
}
context.restoreGState()
}
func recomputeDimensions() {
var max_ascent:CGFloat = 0
var max_descent:CGFloat = 0
var max_width:CGFloat = 0
for atom in self.subDisplays {
let ascent = max(0, atom.position.y + atom.ascent);
if (ascent > max_ascent) {
max_ascent = ascent;
}
let descent = max(0, 0 - (atom.position.y - atom.descent));
if (descent > max_descent) {
max_descent = descent;
}
let width = atom.width + atom.position.x;
if (width > max_width) {
max_width = width;
}
}
self.ascent = max_ascent;
self.descent = max_descent;
self.width = max_width;
}
}
// MARK: - MTFractionDisplay
/// Rendering of an MTFraction as an MTDisplay
public class MTFractionDisplay : MTDisplay {
/** A display representing the numerator of the fraction. Its position is relative
to the parent and is not treated as a sub-display.
*/
public fileprivate(set) var numerator:MTMathListDisplay?
/** A display representing the denominator of the fraction. Its position is relative
to the parent is not treated as a sub-display.
*/
public fileprivate(set) var denominator:MTMathListDisplay?
var numeratorUp:CGFloat=0 { didSet { self.updateNumeratorPosition() } }
var denominatorDown:CGFloat=0 { didSet { self.updateDenominatorPosition() } }
var linePosition:CGFloat=0
var lineThickness:CGFloat=0
init(withNumerator numerator:MTMathListDisplay?, denominator:MTMathListDisplay?, position:CGPoint, range:NSRange) {
super.init()
self.numerator = numerator;
self.denominator = denominator;
self.position = position;
self.range = range;
assert(self.range.length == 1, "Fraction range length not 1 - range (\(range.location), \(range.length)")
}
override public var ascent:CGFloat {
set { super.ascent = newValue }
get { numerator!.ascent + self.numeratorUp }
}
override public var descent:CGFloat {
set { super.descent = newValue }
get { denominator!.descent + self.denominatorDown }
}
override public var width:CGFloat {
set { super.width = newValue }
get { max(numerator!.width, denominator!.width) }
}
func updateDenominatorPosition() {
guard denominator != nil else { return }
denominator!.position = CGPointMake(self.position.x + (self.width - denominator!.width)/2, self.position.y - self.denominatorDown)
}
func updateNumeratorPosition() {
guard numerator != nil else { return }
numerator!.position = CGPointMake(self.position.x + (self.width - numerator!.width)/2, self.position.y + self.numeratorUp)
}
override var position: CGPoint {
set {
super.position = newValue
self.updateDenominatorPosition()
self.updateNumeratorPosition()
}
get { super.position }
}
override var textColor: MTColor? {
set {
super.textColor = newValue
numerator?.textColor = newValue
denominator?.textColor = newValue
}
get { super.textColor }
}
override public func draw(_ context:CGContext) {
super.draw(context)
numerator?.draw(context)
denominator?.draw(context)
context.saveGState()
self.textColor?.setStroke()
// draw the horizontal line
// Note: line thickness of 0 draws the thinnest possible line - we want no line so check for 0s
if self.lineThickness > 0 {
let path = MTBezierPath()
path.move(to: CGPointMake(self.position.x, self.position.y + self.linePosition))
path.addLine(to: CGPointMake(self.position.x + self.width, self.position.y + self.linePosition))
path.lineWidth = self.lineThickness
path.stroke()
}
context.restoreGState()
}
}
// MARK: - MTRadicalDisplay
/// Rendering of an MTRadical as an MTDisplay
class MTRadicalDisplay : MTDisplay {
/** A display representing the radicand of the radical. Its position is relative
to the parent is not treated as a sub-display.
*/
public fileprivate(set) var radicand:MTMathListDisplay?
/** A display representing the degree of the radical. Its position is relative
to the parent is not treated as a sub-display.
*/
public fileprivate(set) var degree:MTMathListDisplay?
override var position: CGPoint {
set {
super.position = newValue
self.updateRadicandPosition()
}
get { super.position }
}
override var textColor: MTColor? {
set {
super.textColor = newValue
self.radicand?.textColor = newValue
self.degree?.textColor = newValue
}
get { super.textColor }
}
private var _radicalGlyph:MTDisplay?
private var _radicalShift:CGFloat=0
var topKern:CGFloat=0
var lineThickness:CGFloat=0
init(withRadicand radicand:MTMathListDisplay?, glyph:MTDisplay, position:CGPoint, range:NSRange) {
super.init()
self.radicand = radicand
_radicalGlyph = glyph
_radicalShift = 0
self.position = position
self.range = range
}
func setDegree(_ degree:MTMathListDisplay?, fontMetrics:MTFontMathTable?) {
// sets up the degree of the radical
var kernBefore = fontMetrics!.radicalKernBeforeDegree;
let kernAfter = fontMetrics!.radicalKernAfterDegree;
let raise = fontMetrics!.radicalDegreeBottomRaisePercent * (self.ascent - self.descent);
// The layout is:
// kernBefore, raise, degree, kernAfter, radical
self.degree = degree;
// the radical is now shifted by kernBefore + degree.width + kernAfter
_radicalShift = kernBefore + degree!.width + kernAfter;
if _radicalShift < 0 {
// we can't have the radical shift backwards, so instead we increase the kernBefore such
// that _radicalShift will be 0.
kernBefore -= _radicalShift;
_radicalShift = 0;
}
// Note: position of degree is relative to parent.
self.degree!.position = CGPointMake(self.position.x + kernBefore, self.position.y + raise);
// Update the width by the _radicalShift
self.width = _radicalShift + _radicalGlyph!.width + self.radicand!.width;
// update the position of the radicand
self.updateRadicandPosition()
}
func updateRadicandPosition() {
// The position of the radicand includes the position of the MTRadicalDisplay
// This is to make the positioning of the radical consistent with fractions and
// have the cursor position finding algorithm work correctly.
// move the radicand by the width of the radical sign
self.radicand!.position = CGPointMake(self.position.x + _radicalShift + _radicalGlyph!.width, self.position.y);
}
override public func draw(_ context: CGContext) {
super.draw(context)
// draw the radicand & degree at its position
self.radicand?.draw(context)
self.degree?.draw(context)
context.saveGState();
self.textColor?.setStroke()
self.textColor?.setFill()
// Make the current position the origin as all the positions of the sub atoms are relative to the origin.
context.translateBy(x: self.position.x + _radicalShift, y: self.position.y);
context.textPosition = CGPoint.zero
// Draw the glyph.
_radicalGlyph?.draw(context)
// Draw the VBOX
// for the kern of, we don't need to draw anything.
let heightFromTop = topKern;
// draw the horizontal line with the given thickness
let path = MTBezierPath()
let lineStart = CGPointMake(_radicalGlyph!.width, self.ascent - heightFromTop - self.lineThickness / 2); // subtract half the line thickness to center the line
let lineEnd = CGPointMake(lineStart.x + self.radicand!.width, lineStart.y);
path.move(to: lineStart)
path.addLine(to: lineEnd)
path.lineWidth = lineThickness
path.lineCapStyle = .round
path.stroke()
context.restoreGState();
}
}
// MARK: - MTGlyphDisplay
/// Rendering a glyph as a display
class MTGlyphDisplay : MTDisplayDS {
var glyph:CGGlyph!
var font:MTFont?
init(withGlpyh glyph:CGGlyph, range:NSRange, font:MTFont?) {
super.init()
self.font = font
self.glyph = glyph
self.position = CGPoint.zero
self.range = range
}
override public func draw(_ context: CGContext) {
super.draw(context)
context.saveGState()
self.textColor?.setFill()
// Make the current position the origin as all the positions of the sub atoms are relative to the origin.
context.translateBy(x: self.position.x, y: self.position.y - self.shiftDown);
context.textPosition = CGPoint.zero
var pos = CGPoint.zero
CTFontDrawGlyphs(font!.ctFont, &glyph, &pos, 1, context);
context.restoreGState();
}
override var ascent:CGFloat {
set { super.ascent = newValue }
get { super.ascent - self.shiftDown }
}
override var descent:CGFloat {
set { super.descent = newValue }
get { super.descent + self.shiftDown }
}
}
// MARK: - MTGlyphConstructionDisplay
class MTGlyphConstructionDisplay:MTDisplayDS {
var glyphs = [CGGlyph]()
var positions = [CGPoint]()
var font:MTFont?
var numGlyphs:Int=0
init(withGlyphs glyphs:[NSNumber?], offsets:[NSNumber?], font:MTFont?) {
super.init()
assert(glyphs.count == offsets.count, "Glyphs and offsets need to match")
self.numGlyphs = glyphs.count;
self.glyphs = [CGGlyph](repeating: CGGlyph(), count: self.numGlyphs) //malloc(sizeof(CGGlyph) * _numGlyphs);
self.positions = [CGPoint](repeating: CGPoint.zero, count: self.numGlyphs) //malloc(sizeof(CGPoint) * _numGlyphs);
for i in 0 ..< self.numGlyphs {
self.glyphs[i] = glyphs[i]!.uint16Value
self.positions[i] = CGPointMake(0, CGFloat(offsets[i]!.floatValue))
}
self.font = font
self.position = CGPoint.zero
}
override public func draw(_ context: CGContext) {
super.draw(context)
context.saveGState()
self.textColor?.setFill()
// Make the current position the origin as all the positions of the sub atoms are relative to the origin.
context.translateBy(x: self.position.x, y: self.position.y - self.shiftDown)
context.textPosition = CGPoint.zero
// Draw the glyphs.
CTFontDrawGlyphs(font!.ctFont, glyphs, positions, numGlyphs, context)
context.restoreGState()
}
override var ascent:CGFloat {
set { super.ascent = newValue }
get { super.ascent - self.shiftDown }
}
override var descent:CGFloat {
set { super.descent = newValue }
get { super.descent + self.shiftDown }
}
}
// MARK: - MTLargeOpLimitsDisplay
/// Rendering a large operator with limits as an MTDisplay
class MTLargeOpLimitsDisplay : MTDisplay {
/** A display representing the upper limit of the large operator. Its position is relative
to the parent is not treated as a sub-display.
*/
var upperLimit:MTMathListDisplay?
/** A display representing the lower limit of the large operator. Its position is relative
to the parent is not treated as a sub-display.
*/
var lowerLimit:MTMathListDisplay?
var limitShift:CGFloat=0
var upperLimitGap:CGFloat=0 { didSet { self.updateUpperLimitPosition() } }
var lowerLimitGap:CGFloat=0 { didSet { self.updateLowerLimitPosition() } }
var extraPadding:CGFloat=0
var nucleus:MTDisplay?
init(withNucleus nucleus:MTDisplay?, upperLimit:MTMathListDisplay?, lowerLimit:MTMathListDisplay?, limitShift:CGFloat, extraPadding:CGFloat) {
super.init()
self.upperLimit = upperLimit;
self.lowerLimit = lowerLimit;
self.nucleus = nucleus;
var maxWidth = max(nucleus!.width, upperLimit?.width ?? 0)
maxWidth = max(maxWidth, lowerLimit?.width ?? 0)
self.limitShift = limitShift;
self.upperLimitGap = 0;
self.lowerLimitGap = 0;
self.extraPadding = extraPadding; // corresponds to \xi_13 in TeX
self.width = maxWidth;
}
override var ascent:CGFloat {
set { super.ascent = newValue }
get {
if self.upperLimit != nil {
return nucleus!.ascent + extraPadding + self.upperLimit!.ascent + upperLimitGap + self.upperLimit!.descent
} else {
return nucleus!.ascent
}
}
}
override var descent:CGFloat {
set { super.descent = newValue }
get {
if self.lowerLimit != nil {
return nucleus!.descent + extraPadding + lowerLimitGap + self.lowerLimit!.descent + self.lowerLimit!.ascent;
} else {
return nucleus!.descent;
}
}
}
override var position: CGPoint {
set {
super.position = newValue
self.updateLowerLimitPosition()
self.updateUpperLimitPosition()
self.updateNucleusPosition()
}
get { super.position }
}
func updateLowerLimitPosition() {
if self.lowerLimit != nil {
// The position of the lower limit includes the position of the MTLargeOpLimitsDisplay
// This is to make the positioning of the radical consistent with fractions and radicals
// Move the starting point to below the nucleus leaving a gap of _lowerLimitGap and subtract
// the ascent to to get the baseline. Also center and shift it to the left by _limitShift.
self.lowerLimit!.position = CGPointMake(self.position.x - limitShift + (self.width - lowerLimit!.width)/2,
self.position.y - nucleus!.descent - lowerLimitGap - self.lowerLimit!.ascent);
}
}
func updateUpperLimitPosition() {
if self.upperLimit != nil {
// The position of the upper limit includes the position of the MTLargeOpLimitsDisplay
// This is to make the positioning of the radical consistent with fractions and radicals
// Move the starting point to above the nucleus leaving a gap of _upperLimitGap and add
// the descent to to get the baseline. Also center and shift it to the right by _limitShift.
self.upperLimit!.position = CGPointMake(self.position.x + limitShift + (self.width - self.upperLimit!.width)/2,
self.position.y + nucleus!.ascent + upperLimitGap + self.upperLimit!.descent);
}
}
func updateNucleusPosition() {
// Center the nucleus
nucleus?.position = CGPointMake(self.position.x + (self.width - nucleus!.width)/2, self.position.y);
}
override var textColor: MTColor? {
set {
super.textColor = newValue
self.upperLimit?.textColor = newValue
self.lowerLimit?.textColor = newValue
nucleus?.textColor = newValue
}
get { super.textColor }
}
override func draw(_ context:CGContext) {
super.draw(context)
// Draw the elements.
self.upperLimit?.draw(context)
self.lowerLimit?.draw(context)
nucleus?.draw(context)
}
}
// MARK: - MTLineDisplay
/// Rendering of an list with an overline or underline
class MTLineDisplay : MTDisplay {
/** A display representing the inner list that is underlined. Its position is relative
to the parent is not treated as a sub-display.
*/
var inner:MTMathListDisplay?
var lineShiftUp:CGFloat=0
var lineThickness:CGFloat=0
init(withInner inner:MTMathListDisplay?, position:CGPoint, range:NSRange) {
super.init()
self.inner = inner;
self.position = position;
self.range = range;
}
override var textColor: MTColor? {
set {
super.textColor = newValue
inner?.textColor = newValue
}
get { super.textColor }
}
override var position: CGPoint {
set {
super.position = newValue
self.updateInnerPosition()
}
get { super.position }
}
override func draw(_ context:CGContext) {
super.draw(context)
self.inner?.draw(context)
context.saveGState();
self.textColor?.setStroke()
// draw the horizontal line
let path = MTBezierPath()
let lineStart = CGPointMake(self.position.x, self.position.y + self.lineShiftUp);
let lineEnd = CGPointMake(lineStart.x + self.inner!.width, lineStart.y);
path.move(to:lineStart)
path.addLine(to: lineEnd)
path.lineWidth = self.lineThickness;
path.stroke()
context.restoreGState();
}
func updateInnerPosition() {
self.inner?.position = CGPointMake(self.position.x, self.position.y);
}
}
// MARK: - MTAccentDisplay
/// Rendering an accent as a display
class MTAccentDisplay : MTDisplay {
/** A display representing the inner list that is accented. Its position is relative
to the parent is not treated as a sub-display.
*/
var accentee:MTMathListDisplay?
/** A display representing the accent. Its position is relative to the current display.
*/
var accent:MTGlyphDisplay?
init(withAccent glyph:MTGlyphDisplay?, accentee:MTMathListDisplay?, range:NSRange) {
super.init()
self.accent = glyph
self.accentee = accentee
self.accentee?.position = CGPoint.zero
self.range = range
}
override var textColor: MTColor? {
set {
super.textColor = newValue
accentee?.textColor = newValue
accent?.textColor = newValue
}
get { super.textColor }
}
override var position: CGPoint {
set {
super.position = newValue
self.updateAccenteePosition()
}
get { super.position }
}
func updateAccenteePosition() {
self.accentee?.position = CGPointMake(self.position.x, self.position.y);
}
override func draw(_ context:CGContext) {
super.draw(context)
self.accentee?.draw(context)
context.saveGState();
context.translateBy(x: self.position.x, y: self.position.y);
context.textPosition = CGPoint.zero
self.accent?.draw(context)
context.restoreGState();
}
}

View File

@@ -0,0 +1,197 @@
//
// Created by Mike Griebling on 2022-12-31.
// Translated from an Objective-C implementation by Kostub Deshmukh.
//
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
import Foundation
/**
* An index that points to a particular character in the MTMathList. The index is a LinkedList that represents
* a path from the beginning of the MTMathList to reach a particular atom in the list. The next node of the path
* is represented by the subIndex. The path terminates when the subIndex is nil.
*
* If there is a subIndex, the subIndexType denotes what branch the path takes (i.e. superscript, subscript,
* numerator, denominator etc.).
* e.g in the expression 25^{2/4} the index of the character 4 is represented as:
* (1, superscript) -> (0, denominator) -> (0, none)
* This can be interpreted as start at index 1 (i.e. the 5) go up to the superscript.
* Then look at index 0 (i.e. 2/4) and go to the denominator. Then look up index 0 (i.e. the 4) which this final
* index.
*
* The level of an index is the number of nodes in the LinkedList to get to the final path.
*/
public class MTMathListIndex {
/**
The type of the subindex.
The type of the subindex denotes what branch the path to the atom that this index points to takes.
*/
public enum MTMathListSubIndexType: Int {
/// The index denotes the whole atom, subIndex is nil.
case none = 0
/// The position in the subindex is an index into the nucleus
case nucleus
/// The subindex indexes into the superscript.
case superscript
/// The subindex indexes into the subscript
case ssubscript
/// The subindex indexes into the numerator (only valid for fractions)
case numerator
/// The subindex indexes into the denominator (only valid for fractions)
case denominator
/// The subindex indexes into the radicand (only valid for radicals)
case radicand
/// The subindex indexes into the degree (only valid for radicals)
case degree
}
/// The index of the associated atom.
var atomIndex: Int
/// The type of subindex, e.g. superscript, numerator etc.
var subIndexType: MTMathListSubIndexType = .none
/// The index into the sublist.
var subIndex: MTMathListIndex?
var finalIndex: Int {
if self.subIndexType == .none {
return self.atomIndex
} else {
return self.subIndex?.finalIndex ?? 0
}
}
/// Returns the previous index if present. Returns `nil` if there is no previous index.
func prevIndex() -> MTMathListIndex? {
if self.subIndexType == .none {
if self.atomIndex > 0 {
return MTMathListIndex(level0Index: self.atomIndex - 1)
}
} else {
if let prevSubIndex = self.subIndex?.prevIndex() {
return MTMathListIndex(at: self.atomIndex, with: prevSubIndex, type: self.subIndexType)
}
}
return nil
}
/// Returns the next index.
func nextIndex() -> MTMathListIndex {
if self.subIndexType == .none {
return MTMathListIndex(level0Index: self.atomIndex + 1)
} else if self.subIndexType == .nucleus {
return MTMathListIndex(at: self.atomIndex + 1, with: self.subIndex, type: self.subIndexType)
} else {
return MTMathListIndex(at: self.atomIndex, with: self.subIndex?.nextIndex(), type: self.subIndexType)
}
}
/**
* Returns true if this index represents the beginning of a line. Note there may be multiple lines in a MTMathList,
* e.g. a superscript or a fraction numerator. This returns true if the innermost subindex points to the beginning of a
* line.
*/
func isBeginningOfLine() -> Bool { self.finalIndex == 0 }
func isAtSameLevel(with index: MTMathListIndex?) -> Bool {
if self.subIndexType != index?.subIndexType {
return false
} else if self.subIndexType == .none {
// No subindexes, they are at the same level.
return true
} else if (self.atomIndex != index?.atomIndex) {
return false
} else {
return self.subIndex?.isAtSameLevel(with: index?.subIndex) ?? false
}
}
/** Returns the type of the innermost sub index. */
func finalSubIndexType() -> MTMathListSubIndexType {
if self.subIndex?.subIndex != nil {
return self.subIndex!.finalSubIndexType()
} else {
return self.subIndexType
}
}
/** Returns true if any of the subIndexes of this index have the given type. */
func hasSubIndex(ofType type: MTMathListSubIndexType) -> Bool {
if self.subIndexType == type {
return true
} else {
return self.subIndex?.hasSubIndex(ofType: type) ?? false
}
}
func levelUp(with subIndex: MTMathListIndex?, type: MTMathListSubIndexType) -> MTMathListIndex {
if self.subIndexType == .none {
return MTMathListIndex(at: self.atomIndex, with: subIndex, type: type)
}
return MTMathListIndex(at: self.atomIndex, with: self.subIndex?.levelUp(with: subIndex, type: type), type: self.subIndexType)
}
func levelDown() -> MTMathListIndex? {
if self.subIndexType == .none {
return nil
}
if let subIndexDown = self.subIndex?.levelDown() {
return MTMathListIndex(at: self.atomIndex, with: subIndexDown, type: self.subIndexType)
} else {
return MTMathListIndex(level0Index: self.atomIndex)
}
}
/** Factory function to create a `MTMathListIndex` with no subindexes.
@param index The index of the atom that the `MTMathListIndex` points at.
*/
public init(level0Index: Int) {
self.atomIndex = level0Index
}
public convenience init(at location: Int, with subIndex: MTMathListIndex?, type: MTMathListSubIndexType) {
self.init(level0Index: location)
self.subIndexType = type
self.subIndex = subIndex
}
}
extension MTMathListIndex: CustomStringConvertible {
public var description: String {
if self.subIndex != nil {
return "[\(self.atomIndex), \(self.subIndexType.rawValue):\(self.subIndex!)]"
}
return "[\(self.atomIndex)]"
}
}
extension MTMathListIndex: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(self.atomIndex)
hasher.combine(self.subIndexType)
hasher.combine(self.subIndex)
}
}
extension MTMathListIndex: Equatable {
public static func ==(lhs: MTMathListIndex, rhs: MTMathListIndex) -> Bool {
if lhs.atomIndex != rhs.atomIndex || lhs.subIndexType != rhs.subIndexType {
return false
}
if rhs.subIndex != nil {
return rhs.subIndex == lhs.subIndex
} else {
return lhs.subIndex == nil
}
}
}

View File

@@ -0,0 +1,383 @@
//
// Created by Mike Griebling on 2022-12-31.
// Translated from an Objective-C implementation by Kostub Deshmukh.
//
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
import Foundation
import CoreText
/**
Different display styles supported by the `MTMathUILabel`.
The only significant difference between the two modes is how fractions
and limits on large operators are displayed.
*/
public enum MTMathUILabelMode {
/// Display mode. Equivalent to $$ in TeX
case display
/// Text mode. Equivalent to $ in TeX.
case text
}
/**
Horizontal text alignment for `MTMathUILabel`.
*/
public enum MTTextAlignment : UInt {
/// Align left.
case left
/// Align center.
case center
/// Align right.
case right
}
/** The main view for rendering math.
`MTMathLabel` accepts either a string in LaTeX or an `MTMathList` to display. Use
`MTMathList` directly only if you are building it programmatically (e.g. using an
editor), otherwise using LaTeX is the preferable method.
The math display is centered vertically in the label. The default horizontal alignment is
is left. This can be changed by setting `textAlignment`. The math is default displayed in
*Display* mode. This can be changed using `labelMode`.
When created it uses `[MTFontManager defaultFont]` as its font. This can be changed using
the `font` parameter.
*/
@IBDesignable
public class MTMathUILabel : MTView {
/** The `MTMathList` to render. Setting this will remove any
`latex` that has already been set. If `latex` has been set, this will
return the parsed `MTMathList` if the `latex` parses successfully. Use this
setting if the `MTMathList` has been programmatically constructed, otherwise it
is preferred to use `latex`.
*/
public var mathList:MTMathList? {
set {
_mathList = newValue
_error = nil
_latex = MTMathListBuilder.mathListToString(newValue)
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
get { _mathList }
}
private var _mathList:MTMathList?
/** The latex string to be displayed. Setting this will remove any `mathList` that
has been set. If latex has not been set, this will return the latex output for the
`mathList` that is set.
@see error */
@IBInspectable
public var latex:String {
set {
_latex = newValue
_error = nil
var error : NSError? = nil
_mathList = MTMathListBuilder.build(fromString: newValue, error: &error)
if error != nil {
_mathList = nil
_error = error
self.errorLabel?.text = error!.localizedDescription
self.errorLabel?.frame = self.bounds
self.errorLabel?.isHidden = !self.displayErrorInline
} else {
self.errorLabel?.isHidden = true
}
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
get { _latex }
}
private var _latex = ""
/** This contains any error that occurred when parsing the latex. */
public var error:NSError? { _error }
private var _error:NSError?
/** If true, if there is an error it displays the error message inline. Default true. */
public var displayErrorInline = true
/** The MTFont to use for rendering. */
public var font:MTFont? {
set {
guard newValue != nil else { return }
_font = newValue
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
get { _font }
}
private var _font:MTFont?
/** Convenience method to just set the size of the font without changing the fontface. */
@IBInspectable
public var fontSize:CGFloat {
set {
_fontSize = newValue
let font = font?.copy(withSize: newValue)
self.font = font // also forces an update
}
get { _fontSize }
}
private var _fontSize:CGFloat=0
/** This sets the text color of the rendered math formula. The default color is black. */
@IBInspectable
public var textColor:MTColor? {
set {
guard newValue != nil else { return }
_textColor = newValue
self.displayList?.textColor = newValue
self.setNeedsDisplay()
}
get { _textColor }
}
private var _textColor:MTColor?
/** The minimum distance from the margin of the view to the rendered math. This value is
`UIEdgeInsetsZero` by default. This is useful if you need some padding between the math and
the border/background color. sizeThatFits: will have its returned size increased by these insets.
*/
@IBInspectable
public var contentInsets:MTEdgeInsets {
set {
_contentInsets = newValue
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
get { _contentInsets }
}
private var _contentInsets = MTEdgeInsetsZero
/** The Label mode for the label. The default mode is Display */
public var labelMode:MTMathUILabelMode {
set {
_labelMode = newValue
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
get { _labelMode }
}
private var _labelMode = MTMathUILabelMode.display
/** Horizontal alignment for the text. The default is align left. */
public var textAlignment:MTTextAlignment {
set {
_textAlignment = newValue
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
get { _textAlignment }
}
private var _textAlignment = MTTextAlignment.left
/** The internal display of the MTMathUILabel. This is for advanced use only. */
public var displayList: MTMathListDisplay? { _displayList }
private var _displayList:MTMathListDisplay?
/** The preferred maximum width (in points) for a multiline label.
Set this property to enable line wrapping based on available width. */
public var preferredMaxLayoutWidth: CGFloat {
set {
_preferredMaxLayoutWidth = newValue
_displayList = nil // Clear cached display list when width constraint changes
self.invalidateIntrinsicContentSize()
self.setNeedsLayout()
}
get { _preferredMaxLayoutWidth }
}
private var _preferredMaxLayoutWidth: CGFloat = 0
public var currentStyle:MTLineStyle {
switch _labelMode {
case .display: return .display
case .text: return .text
}
}
public var errorLabel: MTLabel?
public override init(frame: CGRect) {
super.init(frame: frame)
self.initCommon()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
self.initCommon()
}
func initCommon() {
#if os(macOS)
self.layer?.isGeometryFlipped = true
#else
self.layer.isGeometryFlipped = true
self.clipsToBounds = true
#endif
_fontSize = 20
_contentInsets = MTEdgeInsetsZero
_labelMode = .display
let font = MTFontManager.fontManager.defaultFont
self.font = font
_textAlignment = .left
_displayList = nil
displayErrorInline = true
self.backgroundColor = MTColor.clear
_textColor = MTColor.black
let label = MTLabel()
self.errorLabel = label
#if os(macOS)
label.layer?.isGeometryFlipped = true
#else
label.layer.isGeometryFlipped = true
#endif
label.isHidden = true
label.textColor = MTColor.red
self.addSubview(label)
}
override public func draw(_ dirtyRect: MTRect) {
super.draw(dirtyRect)
if self.mathList == nil { return }
if self.font == nil { return }
// Ensure display list is created before drawing
if _displayList == nil {
_layoutSubviews()
}
guard let displayList = _displayList else { return }
// drawing code
let context = MTGraphicsGetCurrentContext()!
context.saveGState()
displayList.draw(context)
context.restoreGState()
}
func _layoutSubviews() {
guard _mathList != nil && self.font != nil else {
_displayList = nil
errorLabel?.frame = self.bounds
self.setNeedsDisplay()
return
}
// Ensure we have a valid font before attempting to typeset
if self.font == nil {
// No valid font - try to get default font
if let defaultFont = MTFontManager.fontManager.defaultFont {
self._font = defaultFont
} else {
// Cannot typeset without a font, clear display list
_displayList = nil
errorLabel?.frame = self.bounds
self.setNeedsDisplay()
return
}
}
// Use the effective width for layout
let effectiveWidth = _preferredMaxLayoutWidth > 0 ? _preferredMaxLayoutWidth : bounds.size.width
let availableWidth = effectiveWidth - contentInsets.left - contentInsets.right
_displayList = MTTypesetter.createLineForMathList(_mathList, font: self.font, style: currentStyle, maxWidth: availableWidth)
_displayList!.textColor = textColor
var textX = CGFloat(0)
switch self.textAlignment {
case .left: textX = contentInsets.left
case .center: textX = (bounds.size.width - contentInsets.left - contentInsets.right - _displayList!.width) / 2 + contentInsets.left
case .right: textX = bounds.size.width - _displayList!.width - contentInsets.right
}
let availableHeight = bounds.size.height - contentInsets.bottom - contentInsets.top
// center things vertically
var height = _displayList!.ascent + _displayList!.descent
if height < fontSize/2 {
height = fontSize/2 // set height to half the font size
}
let textY = (availableHeight - height) / 2 + _displayList!.descent + contentInsets.bottom
_displayList!.position = CGPointMake(textX, textY)
errorLabel?.frame = self.bounds
self.setNeedsDisplay()
}
func _sizeThatFits(_ size:CGSize) -> CGSize {
guard _mathList != nil else {
// No content - return no-intrinsic-size marker
return CGSize(width: -1, height: -1)
}
// Ensure we have a valid font before attempting to typeset
if self.font == nil {
// No valid font - try to get default font
if let defaultFont = MTFontManager.fontManager.defaultFont {
self._font = defaultFont
} else {
// Cannot typeset without a font
return CGSize(width: -1, height: -1)
}
}
// Determine the maximum width to use
var maxWidth: CGFloat = 0
if _preferredMaxLayoutWidth > 0 {
maxWidth = _preferredMaxLayoutWidth - contentInsets.left - contentInsets.right
} else if size.width > 0 {
maxWidth = size.width - contentInsets.left - contentInsets.right
}
var displayList:MTMathListDisplay? = nil
displayList = MTTypesetter.createLineForMathList(_mathList, font: self.font, style: currentStyle, maxWidth: maxWidth)
guard displayList != nil else {
// Failed to create display list
return CGSize(width: -1, height: -1)
}
var resultWidth = displayList!.width + contentInsets.left + contentInsets.right
let resultHeight = displayList!.ascent + displayList!.descent + contentInsets.top + contentInsets.bottom
// Ensure we don't exceed the width constraints
if _preferredMaxLayoutWidth > 0 && resultWidth > _preferredMaxLayoutWidth {
resultWidth = _preferredMaxLayoutWidth
} else if _preferredMaxLayoutWidth == 0 && size.width > 0 && resultWidth > size.width {
resultWidth = size.width
}
return CGSize(width: resultWidth, height: resultHeight)
}
#if os(macOS)
public func sizeThatFits(_ size: CGSize) -> CGSize {
return _sizeThatFits(size)
}
#else
public override func sizeThatFits(_ size: CGSize) -> CGSize {
return _sizeThatFits(size)
}
#endif
#if os(macOS)
func setNeedsDisplay() { self.needsDisplay = true }
func setNeedsLayout() { self.needsLayout = true }
public override var fittingSize: CGSize { _sizeThatFits(CGSizeZero) }
public override var intrinsicContentSize: CGSize { _sizeThatFits(CGSizeZero) }
override public func layout() {
self._layoutSubviews()
super.layout()
}
#else
public override var intrinsicContentSize: CGSize { _sizeThatFits(CGSizeZero) }
override public func layoutSubviews() { _layoutSubviews() }
#endif
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,89 @@
//
// Created by Mike Griebling on 2022-12-31.
// Translated from an Objective-C implementation by Kostub Deshmukh.
//
// This software may be modified and distributed under the terms of the
// MIT license. See the LICENSE file for details.
//
import Foundation
public struct UnicodeSymbol {
static let multiplication = "\u{00D7}"
static let division = "\u{00F7}"
static let fractionSlash = "\u{2044}"
static let whiteSquare = "\u{25A1}"
static let blackSquare = "\u{25A0}"
static let lessEqual = "\u{2264}"
static let greaterEqual = "\u{2265}"
static let notEqual = "\u{2260}"
static let squareRoot = "\u{221A}" // \sqrt
static let cubeRoot = "\u{221B}"
static let infinity = "\u{221E}" // \infty
static let angle = "\u{2220}" // \angle
static let degree = "\u{00B0}" // \circ
static let capitalGreekStart = UInt32(0x0391)
static let capitalGreekEnd = UInt32(0x03A9)
static let lowerGreekStart = UInt32(0x03B1)
static let lowerGreekEnd = UInt32(0x03C9)
static let planksConstant = UInt32(0x210e)
static let lowerItalicStart = UInt32(0x1D44E)
static let capitalItalicStart = UInt32(0x1D434)
static let greekLowerItalicStart = UInt32(0x1D6FC)
static let greekCapitalItalicStart = UInt32(0x1D6E2)
static let greekSymbolItalicStart = UInt32(0x1D716)
static let mathCapitalBoldStart = UInt32(0x1D400)
static let mathLowerBoldStart = UInt32(0x1D41A)
static let greekCapitalBoldStart = UInt32(0x1D6A8)
static let greekLowerBoldStart = UInt32(0x1D6C2)
static let greekSymbolBoldStart = UInt32(0x1D6DC)
static let numberBoldStart = UInt32(0x1D7CE)
static let mathCapitalBoldItalicStart = UInt32(0x1D468)
static let mathLowerBoldItalicStart = UInt32(0x1D482)
static let greekCapitalBoldItalicStart = UInt32(0x1D71C)
static let greekLowerBoldItalicStart = UInt32(0x1D736)
static let greekSymbolBoldItalicStart = UInt32(0x1D750)
static let mathCapitalScriptStart = UInt32(0x1D49C)
static let mathCapitalTTStart = UInt32(0x1D670)
static let mathLowerTTStart = UInt32(0x1D68A)
static let numberTTStart = UInt32(0x1D7F6)
static let mathCapitalSansSerifStart = UInt32(0x1D5A0)
static let mathLowerSansSerifStart = UInt32(0x1D5BA)
static let numberSansSerifStart = UInt32(0x1D7E2)
static let mathCapitalFrakturStart = UInt32(0x1D504)
static let mathLowerFrakturStart = UInt32(0x1D51E)
static let mathCapitalBlackboardStart = UInt32(0x1D538)
static let mathLowerBlackboardStart = UInt32(0x1D552)
static let numberBlackboardStart = UInt32(0x1D7D8)
}
extension Character {
var utf32Char: UTF32Char { self.unicodeScalars.map { $0.value }.reduce(0, +) }
var isLowerEnglish : Bool { self >= "a" && self <= "z" }
var isUpperEnglish : Bool { self >= "A" && self <= "Z" }
var isNumber : Bool { self >= "0" && self <= "9" }
var isLowerGreek : Bool {
let uch = self.utf32Char
return uch >= UnicodeSymbol.lowerGreekStart && uch <= UnicodeSymbol.lowerGreekEnd
}
var isCapitalGreek : Bool {
let uch = self.utf32Char
return uch >= UnicodeSymbol.capitalGreekStart && uch <= UnicodeSymbol.capitalGreekEnd
}
var greekSymbolOrder : UInt32? {
let greekSymbols : [UTF32Char] = [0x03F5, 0x03D1, 0x03F0, 0x03D5, 0x03F1, 0x03D6]
let index = greekSymbols.firstIndex(of: self.utf32Char)
if let pos = index { return UInt32(pos) }
return nil
}
var isGreekSymbol : Bool { self.greekSymbolOrder != nil }
}

View File

@@ -0,0 +1,58 @@
import Foundation
final class RWLock {
init() {
pthread_rwlock_init(&lock, nil)
}
deinit {
pthread_rwlock_destroy(&lock)
}
func read<T>(_ block: () -> T) -> T {
pthread_rwlock_rdlock(&lock)
defer { pthread_rwlock_unlock(&lock) }
return block()
}
func readWrite<T>(_ block: () -> T) -> T {
pthread_rwlock_wrlock(&lock)
defer { pthread_rwlock_unlock(&lock) }
return block()
}
private var lock = pthread_rwlock_t()
}
@propertyWrapper
struct RWLocked<T> {
init(wrappedValue: T) {
value = wrappedValue
}
var wrappedValue: T {
get {
lock.read {
value
}
}
set {
lock.readWrite {
value = newValue
}
}
}
@discardableResult
mutating func readWrite(_ block: (inout T) -> Void) -> (oldValue: T, newValue: T) {
lock.readWrite {
let old = value
block(&value)
return (old, value)
}
}
private var value: T
private let lock = RWLock()
}

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
% This is a preliminary version (2006-09-30), barring acceptance from
% the LaTeX Project Team and other feedback, of the GUST Font License.
% (GUST is the Polish TeX Users Group, http://www.gust.org.pl)
%
% For the most recent version of this license see
% http://www.gust.org.pl/fonts/licenses/GUST-FONT-LICENSE.txt
% or
% http://tug.org/fonts/licenses/GUST-FONT-LICENSE.txt
%
% This work may be distributed and/or modified under the conditions
% of the LaTeX Project Public License, either version 1.3c of this
% license or (at your option) any later version.
%
% Please also observe the following clause:
% 1) it is requested, but not legally required, that derived works be
% distributed only after changing the names of the fonts comprising this
% work and given in an accompanying "manifest", and that the
% files comprising the Work, as listed in the manifest, also be given
% new names. Any exceptions to this request are also given in the
% manifest.
%
% We recommend the manifest be given in a separate file named
% MANIFEST-<fontid>.txt, where <fontid> is some unique identification
% of the font family. If a separate "readme" file accompanies the Work,
% we recommend a name of the form README-<fontid>.txt.
%
% The latest version of the LaTeX Project Public License is in
% http://www.latex-project.org/lppl.txt and version 1.3c or later
% is part of all distributions of LaTeX version 2006/05/20 or later.

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2013 MathChat
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,103 @@
STIX Font License
24 May 2010
Copyright (c) 2001-2010 by the STI Pub Companies, consisting of the American
Institute of Physics, the American Chemical Society, the American Mathematical
Society, the American Physical Society, Elsevier, Inc., and The Institute of
Electrical and Electronic Engineers, Inc. (www.stixfonts.org), with Reserved
Font Name STIX Fonts, STIX Fonts (TM) is a trademark of The Institute of
Electrical and Electronics Engineers, Inc.
Portions copyright (c) 1998-2003 by MicroPress, Inc. (www.micropress-inc.com),
with Reserved Font Name TM Math. To obtain additional mathematical fonts, please
contact MicroPress, Inc., 68-30 Harrow Street, Forest Hills, NY 11375, USA,
Phone: (718) 575-1816.
Portions copyright (c) 1990 by Elsevier, Inc.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
---------------------------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
---------------------------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide development
of collaborative font projects, to support the font creation efforts of academic
and linguistic communities, and to provide a free and open framework in which
fonts may be shared and improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and redistributed
freely as long as they are not sold by themselves. The fonts, including any
derivative works, can be bundled, embedded, redistributed and/or sold with any
software provided that any reserved names are not used by derivative works. The
fonts and derivatives, however, cannot be released under any other type of license.
The requirement for fonts to remain under this license does not apply to any
document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright Holder(s) under
this license and clearly marked as such. This may include source files, build
scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the copyright
statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting, or
substituting -- in part or in whole -- any of the components of the Original Version,
by changing formats or by porting the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical writer or other
person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining a copy of the
Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell
modified and unmodified copies of the Font Software, subject to the following
conditions:
1) Neither the Font Software nor any of its individual components, in Original or
Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled, redistributed
and/or sold with any software, provided that each copy contains the above copyright
notice and this license. These can be included either as stand-alone text files,
human-readable headers or in the appropriate machine-readable metadata fields within
text or binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless
explicit written permission is granted by the corresponding Copyright Holder. This
restriction only applies to the primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall
not be used to promote, endorse or advertise any Modified Version, except to
acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with
their explicit written permission.
5) The Font Software, modified or unmodified, in part or in whole, must be distributed
entirely under this license, and must not be distributed under any other license. The
requirement for fonts to remain under this license does not apply to any document
created using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER
RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR
INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,205 @@
#!/usr/local/bin/python3
import plistlib
import sys
from fontTools.ttLib import TTFont
def usage(code):
print('Usage math_table_to_plist.py <fontfile> <plistfile>')
sys.exit(code)
def process_font(font_file, out_file):
font = TTFont(font_file)
math_table = font['MATH'].table
constants = get_constants(math_table)
italic_c = get_italic_correction(math_table)
v_variants = get_v_variants(math_table)
h_variants = get_h_variants(math_table)
assembly = get_v_assembly(math_table)
accents = get_accent_attachments(math_table)
pl = {
"version" : "1.3",
"constants": constants,
"v_variants" : v_variants,
"h_variants" : h_variants,
"italic" : italic_c,
"accents" : accents,
"v_assembly" : assembly }
ofile = open(out_file, 'w+b')
plistlib.dump(pl, ofile)
ofile.close()
def get_constants(math_table):
constants = math_table.MathConstants
if constants is None:
raise 'Cannot find MathConstants in MATH table'
int_consts = [ 'ScriptPercentScaleDown',
'ScriptScriptPercentScaleDown',
'DelimitedSubFormulaMinHeight',
'DisplayOperatorMinHeight',
'RadicalDegreeBottomRaisePercent']
consts = { c : getattr(constants, c) for c in int_consts }
record_consts = [ 'MathLeading',
'AxisHeight',
'AccentBaseHeight',
'FlattenedAccentBaseHeight',
'SubscriptShiftDown',
'SubscriptTopMax',
'SubscriptBaselineDropMin',
'SuperscriptShiftUp',
'SuperscriptShiftUpCramped',
'SuperscriptBottomMin',
'SuperscriptBaselineDropMax',
'SubSuperscriptGapMin',
'SuperscriptBottomMaxWithSubscript',
'SpaceAfterScript',
'UpperLimitGapMin',
'UpperLimitBaselineRiseMin',
'LowerLimitGapMin',
'LowerLimitBaselineDropMin',
'StackTopShiftUp',
'StackTopDisplayStyleShiftUp',
'StackBottomShiftDown',
'StackBottomDisplayStyleShiftDown',
'StackGapMin',
'StackDisplayStyleGapMin',
'StretchStackTopShiftUp',
'StretchStackBottomShiftDown',
'StretchStackGapAboveMin',
'StretchStackGapBelowMin',
'FractionNumeratorShiftUp',
'FractionNumeratorDisplayStyleShiftUp',
'FractionDenominatorShiftDown',
'FractionDenominatorDisplayStyleShiftDown',
'FractionNumeratorGapMin',
'FractionNumDisplayStyleGapMin',
'FractionRuleThickness',
'FractionDenominatorGapMin',
'FractionDenomDisplayStyleGapMin',
'SkewedFractionHorizontalGap',
'SkewedFractionVerticalGap',
'OverbarVerticalGap',
'OverbarRuleThickness',
'OverbarExtraAscender',
'UnderbarVerticalGap',
'UnderbarRuleThickness',
'UnderbarExtraDescender',
'RadicalVerticalGap',
'RadicalDisplayStyleVerticalGap',
'RadicalRuleThickness',
'RadicalExtraAscender',
'RadicalKernBeforeDegree',
'RadicalKernAfterDegree',
]
consts_2 = { c : getattr(constants, c).Value for c in record_consts }
consts.update(consts_2)
variants = math_table.MathVariants
consts['MinConnectorOverlap'] = variants.MinConnectorOverlap
return consts
def get_italic_correction(math_table):
glyph_info = math_table.MathGlyphInfo
if glyph_info is None:
raise "Cannot find MathGlyphInfo in MATH table."
italic = glyph_info.MathItalicsCorrectionInfo
if italic is None:
raise "Cannot find Italic Correction in GlyphInfo"
glyphs = italic.Coverage.glyphs
count = italic.ItalicsCorrectionCount
records = italic.ItalicsCorrection
italic_dict = {}
for i in range(count):
name = glyphs[i]
record = records[i]
if record.DeviceTable is not None:
raise "Don't know how to process device table for italic correction."
italic_dict[name] = record.Value
return italic_dict
def get_accent_attachments(math_table):
glyph_info = math_table.MathGlyphInfo
if glyph_info is None:
raise "Cannot find MathGlyphInfo in MATH table."
attach = glyph_info.MathTopAccentAttachment
if attach is None:
raise "Cannot find Top Accent Attachment in GlyphInfo"
glyphs = attach.TopAccentCoverage.glyphs
count = attach.TopAccentAttachmentCount
records = attach.TopAccentAttachment
attach_dict = {}
for i in range(count):
name = glyphs[i]
record = records[i]
if record.DeviceTable is not None:
raise "Don't know how to process device table for accent attachment."
attach_dict[name] = record.Value
return attach_dict
def get_v_variants(math_table):
variants = math_table.MathVariants
vglyphs = variants.VertGlyphCoverage.glyphs
vconstruction = variants.VertGlyphConstruction
count = variants.VertGlyphCount
variant_dict = {}
for i in range(count):
name = vglyphs[i]
record = vconstruction[i]
glyph_variants = [x.VariantGlyph for x in
record.MathGlyphVariantRecord]
variant_dict[name] = glyph_variants
return variant_dict
def get_h_variants(math_table):
variants = math_table.MathVariants
hglyphs = variants.HorizGlyphCoverage.glyphs
hconstruction = variants.HorizGlyphConstruction
count = variants.HorizGlyphCount
variant_dict = {}
for i in range(count):
name = hglyphs[i]
record = hconstruction[i]
glyph_variants = [x.VariantGlyph for x in
record.MathGlyphVariantRecord]
variant_dict[name] = glyph_variants
return variant_dict
def get_v_assembly(math_table):
variants = math_table.MathVariants
vglyphs = variants.VertGlyphCoverage.glyphs
vconstruction = variants.VertGlyphConstruction
count = variants.VertGlyphCount
assembly_dict = {}
for i in range(count):
name = vglyphs[i]
record = vconstruction[i]
assembly = record.GlyphAssembly
if assembly is not None:
# There is an assembly for this glyph
italic = assembly.ItalicsCorrection.Value
parts = [part_dict(part) for part in assembly.PartRecords]
assembly_dict[name] = {
"italic" : assembly.ItalicsCorrection.Value,
"parts" : parts }
return assembly_dict
def part_dict(part):
return {
"glyph": part.glyph,
"startConnector" : part.StartConnectorLength,
"endConnector" : part.EndConnectorLength,
"advance" : part.FullAdvance,
"extender" : (part.PartFlags == 1) }
def main():
if len(sys.argv) != 3:
usage(1)
font_file = sys.argv[1]
plist_file = sys.argv[2]
process_font(font_file, plist_file)
if __name__ == '__main__':
main()

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff