Merge pull request #8 from petersktang/main

Contribute off-screen drawing code to generate NSImage and UIImage
This commit is contained in:
mgriebling
2023-09-12 07:46:37 -04:00
committed by GitHub
7 changed files with 690 additions and 6 deletions

View File

@@ -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<CFError>? = 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<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 CTFontPair: Hashable {
let font: MathFont
let size: CGFloat
}
}

View File

@@ -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
}
}

View File

@@ -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 }

View File

@@ -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]()

View File

@@ -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
}
}

View File

@@ -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 }
}
}

View File

@@ -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)")
}
}
}
}