diff --git a/Sources/SwiftMath/MathBundle/MathFont.swift b/Sources/SwiftMath/MathBundle/MathFont.swift new file mode 100644 index 0000000..d7abd7c --- /dev/null +++ b/Sources/SwiftMath/MathBundle/MathFont.swift @@ -0,0 +1,180 @@ +// +// File.swift +// +// +// Created by Peter Tang on 10/9/2023. +// + +#if os(iOS) +import UIKit +#endif + +#if os(macOS) +import AppKit +#endif + +public enum MathFont: String, CaseIterable { + + case latinModernFont = "latinmodern-math" + case kpMathLightFont = "KpMath-Light" + case kpMathSansFont = "KpMath-Sans" + case xitsFont = "xits-math" + case termesFont = "texgyretermes-math" + + var fontFamilyName: String { + switch self { + case .latinModernFont: return "Latin Modern Math" + case .kpMathLightFont: return "KpMath" + case .kpMathSansFont: return "KpMath" + case .xitsFont: return "XITS Math" + case .termesFont: return "TeX Gyre Termes Math" + } + } + var fontName: String { + switch self { + case .latinModernFont: return "LatinModernMath-Regular" + case .kpMathLightFont: return "KpMath-Light" + case .kpMathSansFont: return "KpMath-Sans" + case .xitsFont: return "XITSMath" + case .termesFont: return "TeXGyreTermesMath-Regular" + } + } + public func cgFont() -> CGFont? { + BundleManager.manager.obtainCGFont(font: self) + } + public func ctFont(withSize size: CGFloat) -> CTFont? { + BundleManager.manager.obtainCTFont(font: self, withSize: size) + } + #if os(iOS) + 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 func mathTable() -> NSDictionary? { + BundleManager.manager.obtainMathTable(font: self) + } + internal func get(nameForGlyph glyph: CGGlyph) -> String { + let name = cgFont()?.name(for: glyph) as? String + return name ?? "" + } + internal func get(glyphWithName name: String) -> CGGlyph? { + cgFont()?.getGlyphWithGlyphName(name: name as CFString) + } +} +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 { + static fileprivate(set) var manager: BundleManager = { + return BundleManager() + }() + + private var cgFonts = [MathFont: CGFont]() + private var ctFonts = [CTFontPair: CTFont]() + private var mathTables = [MathFont: NSDictionary]() + + private var initializedOnceAlready: Bool = false + + 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 + + var errorRef: Unmanaged? = nil + guard CTFontManagerRegisterGraphicsFont(defaultCGFont, &errorRef) else { + throw FontError.registerFailed + } + print("mathFonts bundle resource: \(mathFont.rawValue), font: \(defaultCGFont.fullName) registered.") + } + + 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 + } + //FIXME: mathTable = MTFontMathTable(withFont:self, mathTable:rawMathTable) + mathTables[mathFont] = rawMathTable + print("mathFonts bundle resource: \(mathFont.rawValue).plist registered.") + } + + private func registerAllBundleResources() { + guard !initializedOnceAlready else { return } + MathFont.allCases.forEach { font in + do { + try BundleManager.manager.registerCGFont(mathFont: font) + try BundleManager.manager.registerMathTable(mathFont: font) + } catch { + fatalError("MTMathFonts:\(#function) Couldn't load mathFont resource \(font.rawValue), reason \(error)") + } + } + initializedOnceAlready.toggle() + } + + fileprivate func obtainCGFont(font: MathFont) -> CGFont? { + if !initializedOnceAlready { registerAllBundleResources() } + return cgFonts[font] + } + + fileprivate func obtainCTFont(font: MathFont, withSize size: CGFloat) -> CTFont? { + if !initializedOnceAlready { registerAllBundleResources() } + let fontPair = CTFontPair(font: font, size: size) + guard let ctFont = ctFonts[fontPair] else { + if let cgFont = cgFonts[font] { + let ctFont = CTFontCreateWithGraphicsFont(cgFont, size, nil, nil) + ctFonts[fontPair] = ctFont + return ctFont + } + return nil + } + return ctFont + } + fileprivate func obtainMathTable(font: MathFont) -> NSDictionary? { + if !initializedOnceAlready { registerAllBundleResources() } + return mathTables[font] + } + deinit { + ctFonts.removeAll() + var errorRef: Unmanaged? = 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 CTFontPair: Hashable { + let font: MathFont + let size: CGFloat + } +} diff --git a/Sources/SwiftMath/MathBundle/MathTable.swift b/Sources/SwiftMath/MathBundle/MathTable.swift new file mode 100644 index 0000000..e86886c --- /dev/null +++ b/Sources/SwiftMath/MathBundle/MathTable.swift @@ -0,0 +1,310 @@ +// +// MathTable.swift +// +// +// Created by Peter Tang on 11/9/2023. +// + +import Foundation +import CoreText + +/** 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. + */ +internal struct MathTable { + let kConstants = "constants" + + let font: MathFont + private let unitsPerEm: UInt + private let fontSize: CGFloat + weak var fontMathTable: NSDictionary? + + init(withFont font: MathFont, fontSize: CGFloat, unitsPerEm: UInt) { + self.font = font + self.unitsPerEm = unitsPerEm + self.fontSize = fontSize + self.fontMathTable = font.mathTable() + } + func fontUnitsToPt(_ fontUnits: Int) -> CGFloat { + CGFloat(fontUnits) * fontSize / CGFloat(unitsPerEm) + } + func constantFromTable(_ constName: String) -> CGFloat { + guard let consts = fontMathTable?[kConstants] as? NSDictionary, let val = consts[constName] as? NSNumber else { + fatalError("\(#function) unable to extract \(constName) from plist") + } + return fontUnitsToPt(val.intValue) + } + func percentFromTable(_ percentName: String) -> CGFloat { + guard let consts = fontMathTable?[kConstants] as? NSDictionary, let val = consts[percentName] as? NSNumber else { + fatalError("\(#function) unable to extract \(percentName) from plist") + } + 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?] { + guard let variants = fontMathTable?[kVertVariants] as? NSDictionary else { + fatalError("\(#function) unable to extract \(glyph) from plist") + } + 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?] { + guard let variants = fontMathTable?[kHorizVariants] as? NSDictionary else { + fatalError("\(#function) unable to extract \(glyph) from plist") + } + return self.getVariantsForGlyph(glyph, inDictionary:variants) + } + + func getVariantsForGlyph(_ glyph: CGGlyph, inDictionary variants: NSDictionary) -> [NSNumber?] { + let glyphName = font.get(nameForGlyph: glyph) + let variantGlyphs = variants[glyphName] as? NSArray + var glyphArray = [NSNumber]() + if variantGlyphs == nil || variantGlyphs?.count == 0, let glyph = font.get(glyphWithName: glyphName) { + // There are no extra variants, so just add the current glyph to it. + glyphArray.append(NSNumber(value:glyph)) + return glyphArray + } else if let variantGlyphs = variantGlyphs { + 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. + */ + func getLargerGlyph(_ glyph:CGGlyph) -> CGGlyph { + let variants = fontMathTable?[kVertVariants] as? NSDictionary + let glyphName = 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! { + 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; + } + + // 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 = fontMathTable?[kItalic] as? NSDictionary + let glyphName = font.get(nameForGlyph: glyph) + let val = italics![glyphName] as? NSNumber + // if val is nil, this returns 0. + return 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 = fontMathTable?[kAccents] as? NSDictionary + let glyphName = 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 + guard let ctFont = font.ctFont(withSize: fontSize) else { + fatalError("\(#function) unable to obtain ctFont resource name: \(font.rawValue) with size \(fontSize)") + } + CTFontGetAdvancesForGlyphs(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 = fontMathTable?[kVertAssembly] as? NSDictionary + let glyphName = font.get(nameForGlyph: glyph) + guard 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 glyph = font.get(glyphWithName: glyphName) else { continue } + var part = GlyphPart(glyph: glyph) + if 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 partInfoGlyphName = partInfo["glyph"] as? String, partInfoGlyphName == glyphName { + part.fullAdvance = fontUnitsToPt(adv.intValue) + part.endConnectorLength = fontUnitsToPt(end.intValue) + part.startConnectorLength = fontUnitsToPt(start.intValue) + part.isExtender = ext.boolValue + rv.append(part) + } + } + return rv + } + +} +extension MathTable { + 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 + } +} diff --git a/Sources/SwiftMath/MathRender/MTConfig.swift b/Sources/SwiftMath/MathRender/MTConfig.swift index 1ef5c70..f936d9d 100644 --- a/Sources/SwiftMath/MathRender/MTConfig.swift +++ b/Sources/SwiftMath/MathRender/MTConfig.swift @@ -18,6 +18,7 @@ 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() } @@ -31,6 +32,7 @@ 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 } diff --git a/Sources/SwiftMath/MathRender/MTFontManager.swift b/Sources/SwiftMath/MathRender/MTFontManager.swift index 0ebb015..a2e0538 100644 --- a/Sources/SwiftMath/MathRender/MTFontManager.swift +++ b/Sources/SwiftMath/MathRender/MTFontManager.swift @@ -11,17 +11,16 @@ import Foundation public class MTFontManager { - static public private(set) var manager:MTFontManager! = nil + static public private(set) var manager: MTFontManager = { + MTFontManager() + }() let kDefaultFontSize = CGFloat(20) static var fontManager : MTFontManager { - if manager == nil { - manager = MTFontManager() - } - return manager! + return manager } - + public init() { } var nameToFontMap = [String: MTFont]() diff --git a/Sources/SwiftMath/MathRender/MTMathImage.swift b/Sources/SwiftMath/MathRender/MTMathImage.swift new file mode 100644 index 0000000..49bc043 --- /dev/null +++ b/Sources/SwiftMath/MathRender/MTMathImage.swift @@ -0,0 +1,111 @@ +// +// File.swift +// +// +// Created by Peter Tang on 12/9/2023. +// + +import Foundation + +#if os(iOS) + import UIKit +#endif + +#if os(macOS) + import AppKit +#endif + +public class MTMathImage { + public var font: MTFont? = nil + public let fontSize: CGFloat + 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.fontSize = fontSize + self.textColor = textColor + self.labelMode = labelMode + self.textAlignment = textAlignment + } +} +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) + } + if font == nil { + self.font = MTFontManager.fontManager.defaultFont + } + 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) + 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 + } +} diff --git a/Tests/SwiftMathTests/MathFontTests.swift b/Tests/SwiftMathTests/MathFontTests.swift new file mode 100644 index 0000000..dff78fb --- /dev/null +++ b/Tests/SwiftMathTests/MathFontTests.swift @@ -0,0 +1,46 @@ +import XCTest +@testable import SwiftMath + +// +// MathFontTests.swift +// +// +// Created by Peter Tang on 12/9/2023. +// + +final class MathFontTests: XCTestCase { + func testMathFontScript() throws { + let size = Int.random(in: 20 ... 40) + MathFont.allCases.forEach { + // print("\(#function) cgfont \($0.cgFont())") + // print("\(#function) ctfont \($0.ctFont(withSize: CGFloat(size)))") + XCTAssertNotNil($0.cgFont()) + XCTAssertNotNil($0.ctFont(withSize: CGFloat(size))) + XCTAssertEqual($0.ctFont(withSize: CGFloat(size))?.fontSize, CGFloat(size), "ctFont fontSize test") + } + #if os(iOS) + // for family in UIFont.familyNames.sorted() { + // let names = UIFont.fontNames(forFamilyName: family) + // print("Family: \(family) Font names: \(names)") + // } + fontNames.forEach { name in + XCTAssertNotNil(UIFont(name: name, size: CGFloat(size))) + } + fontFamilyNames.forEach { name in + XCTAssertNotNil(UIFont.fontNames(forFamilyName: name)) + } + #endif + #if os(macOS) + fontNames.forEach { name in + let font = NSFont(name: name, size: CGFloat(size)) + XCTAssertNotNil(font) + } + #endif + } + var fontNames: [String] { + MathFont.allCases.map { $0.fontName } + } + var fontFamilyNames: [String] { + MathFont.allCases.map { $0.fontFamilyName } + } +} diff --git a/Tests/SwiftMathTests/MathTableTests.swift b/Tests/SwiftMathTests/MathTableTests.swift new file mode 100644 index 0000000..21798b8 --- /dev/null +++ b/Tests/SwiftMathTests/MathTableTests.swift @@ -0,0 +1,36 @@ +import XCTest +@testable import SwiftMath + +// +// MathTableTests.swift +// +// +// Created by Peter Tang on 12/9/2023. +// + +final class MathTableTests: XCTestCase { + func testMathFontScript() throws { + let size = Int.random(in: 20 ... 40) + MathFont.allCases.forEach { + // print("\(#function) cgfont \($0.cgFont())") + // print("\(#function) ctfont \($0.ctFont(withSize: CGFloat(size)))") + // XCTAssertNotNil($0.cgFont()) + // XCTAssertNotNil($0.ctFont(withSize: CGFloat(size))) + // XCTAssertEqual($0.ctFont(withSize: CGFloat(size))?.fontSize, CGFloat(size), "ctFont fontSize test") + let ctFont = $0.ctFont(withSize: CGFloat(size)) + if let unitsPerEm = ctFont?.unitsPerEm { + let mathTable = MathTable(withFont: $0, fontSize: CGFloat(size), unitsPerEm: unitsPerEm) + + let values = [ + mathTable.fractionNumeratorDisplayStyleShiftUp, + mathTable.fractionNumeratorShiftUp, + mathTable.fractionDenominatorDisplayStyleShiftDown, + mathTable.fractionDenominatorShiftDown, + mathTable.fractionNumeratorDisplayStyleGapMin, + mathTable.fractionNumeratorGapMin, + ] + print("\(ctFont) -> \(values)") + } + } + } +}