Merge pull request #11 from petersktang/main

Contribute threadsafe code on MathFont, BundleManager, MTFontV2, MTFontMathTableV2
This commit is contained in:
mgriebling
2023-09-20 07:31:21 -04:00
committed by GitHub
10 changed files with 522 additions and 424 deletions

View File

@@ -1,310 +0,0 @@
//
// 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
let unitsPerEm: UInt
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 {
// 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
} 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 {
return font.get(glyphWithName: glyphVariantName)
}
}
// 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
let ctFont = font.ctFont(withSize: 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 else { continue }
let glyph = font.get(glyphWithName: glyphName)
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

@@ -1,6 +1,6 @@
//
// MTFontMathTableV2.swift
//
//
//
// Created by Peter Tang on 15/9/2023.
//
@@ -14,11 +14,11 @@ internal class MTFontMathTableV2: MTFontMathTable {
private let fontSize: CGFloat
private let unitsPerEm: UInt
private let mTable: NSDictionary
init(mathFont: MathFont, size: CGFloat) {
init(mathFont: MathFont, size: CGFloat, unitsPerEm: UInt) {
self.mathFont = mathFont
self.fontSize = size
mTable = mathFont.mathTable()
unitsPerEm = mathFont.ctFont(withSize: fontSize).unitsPerEm
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!

View File

@@ -1,6 +1,6 @@
//
// MTFontV2.swift
//
//
//
// Created by Peter Tang on 15/9/2023.
//
@@ -17,17 +17,18 @@ extension MathFont {
public final class MTFontV2: MTFont {
let font: MathFont
let size: CGFloat
private lazy var _cgFont: CGFont = {
font.cgFont()
}()
private lazy var _ctFont: CTFont = {
font.ctFont(withSize: size)
}()
private lazy var _mathTab = MTFontMathTableV2(mathFont: font, size: size)
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
@@ -43,9 +44,19 @@ public final class MTFontV2: MTFont {
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 { _mathTab }
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.") }

View File

@@ -1,6 +1,6 @@
//
// MathFont.swift
//
//
//
// Created by Peter Tang on 10/9/2023.
//
@@ -43,20 +43,21 @@ public enum MathFont: String, CaseIterable {
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 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)
// 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. */
@@ -68,16 +69,15 @@ internal extension CTFont {
}
}
private class BundleManager {
static fileprivate(set) var manager: BundleManager = {
return BundleManager()
}()
//Note: below should be lightweight and without threadsafe problem.
static internal let manager = BundleManager()
private var cgFonts = [MathFont: CGFont]()
private var ctFonts = [CTFontPair: CTFont]()
private var mathTables = [MathFont: NSDictionary]()
private var ctFonts = [CTFontSizePair: CTFont]()
private var rawMathTables = [MathFont: NSDictionary]()
private let threadSafeQueue = DispatchQueue(label: "com.smartmath.mathfont.threadsafequeue", attributes: .concurrent)
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 {
@@ -92,11 +92,17 @@ private class BundleManager {
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
}
debugPrint("mathFonts bundle resource: \(mathFont.rawValue), font: \(defaultCGFont.fullName!) registered.")
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 {
@@ -109,60 +115,59 @@ private class BundleManager {
version == "1.3" else {
throw FontError.invalidMathTable
}
mathTables[mathFont] = rawMathTable
debugPrint("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()
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 cgFonts[mathFont] == nil else { return }
do {
try BundleManager.manager.registerCGFont(mathFont: mathFont)
try BundleManager.manager.registerMathTable(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)")
}
} catch {
fatalError("MTMathFonts:\(#function) ondemand loading failed, mathFont \(mathFont.rawValue), reason \(error)")
}
}
})
}
fileprivate func obtainCGFont(font: MathFont) -> CGFont {
// if !initializedOnceAlready { registerAllBundleResources() }
onDemandRegistration(mathFont: font)
guard let cgFont = cgFonts[font] else {
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 {
// if !initializedOnceAlready { registerAllBundleResources() }
onDemandRegistration(mathFont: font)
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
}
fatalError("\(#function) unable to locate CGFont \(font.fontName), nor create CTFont")
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")
}
return 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 obtainMathTable(font: MathFont) -> NSDictionary {
// if !initializedOnceAlready { registerAllBundleResources() }
fileprivate func obtainRawMathTable(font: MathFont) -> NSDictionary {
onDemandRegistration(mathFont: font)
guard let mathTable = mathTables[font] else {
guard let mathTable = threadSafeQueue.sync(execute: { rawMathTables[font] } ) else {
fatalError("\(#function) unable to locate mathTable: \(font.rawValue).plist")
}
return mathTable
@@ -183,7 +188,7 @@ private class BundleManager {
case invalidMathTable
}
private struct CTFontPair: Hashable {
private struct CTFontSizePair: Hashable {
let font: MathFont
let size: CGFloat
}

View File

@@ -66,14 +66,15 @@ extension MathImage {
}
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)
}
intrinsicContentSize = intrinsicContentSize(displayList)
displayList.textColor = textColor
let size = intrinsicContentSize
layoutImage(size: size, displayList: displayList)

View File

@@ -1,35 +0,0 @@
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))
// 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)")
// }
}
}

View File

@@ -1,6 +1,6 @@
//
// MTFontMathTableV2Tests.swift
//
//
//
// Created by Peter Tang on 15/9/2023.
//
@@ -25,4 +25,60 @@ final class MTFontMathTableV2Tests: XCTestCase {
print("\($0.rawValue).plist: \(values)")
}
}
private let executionQueue = DispatchQueue(label: "com.swiftmath.mathbundle", attributes: .concurrent)
private let executionGroup = DispatchGroup()
let totalCases = 1000
var testCount = 0
func testConcurrentThreadsafeScript() throws {
testCount = 0
var mathFont: MathFont { .allCases.randomElement()! }
var size: CGFloat { CGFloat.random(in: 20 ... 40) }
let mtfonts = Array( 0 ..< 10 ).map { _ in mathFont.mtfont(size: size) }
for caseNumber in 0 ..< totalCases {
helperConcurrentMTFontMathTableV2(caseNumber, mtfont: mtfonts.randomElement()!, in: executionGroup, on: executionQueue)
}
executionGroup.notify(queue: .main) { [weak self] in
guard let self = self else { return }
XCTAssertEqual(self.testCount, totalCases)
print("\(self.testCount) completed =================")
}
executionGroup.wait()
}
func helperConcurrentMTFontMathTableV2(_ count: Int, mtfont: MTFontV2, in group: DispatchGroup, on queue: DispatchQueue) {
let workitem = DispatchWorkItem {
let mTable = mtfont.mathTable
let values = [
mTable?.fractionNumeratorDisplayStyleShiftUp,
mTable?.fractionNumeratorShiftUp,
mTable?.fractionDenominatorDisplayStyleShiftDown,
mTable?.fractionDenominatorShiftDown,
mTable?.fractionNumeratorDisplayStyleGapMin,
mTable?.fractionNumeratorGapMin,
].compactMap{$0}
// if count % 50 == 0 {
// print(values) // accessed these values on global thread.
// }
XCTAssertNotNil(mTable)
}
workitem.notify(queue: .main) { [weak self] in
// print("\(Thread.isMainThread ? "main" : "global") completed .....")
let mTable = mtfont.mathTable
if count % 70 == 0 {
let values = [
mTable?.fractionNumeratorDisplayStyleShiftUp,
mTable?.fractionNumeratorShiftUp,
mTable?.fractionDenominatorDisplayStyleShiftDown,
mTable?.fractionDenominatorShiftDown,
mTable?.fractionNumeratorDisplayStyleGapMin,
mTable?.fractionNumeratorGapMin,
].compactMap{$0}
// if count % 50 == 0 {
// print(values)
// }
}
self?.testCount += 1
}
queue.async(group: group, execute: workitem)
}
}

View File

@@ -1,6 +1,6 @@
//
// MTFontV2Tests.swift
//
//
//
// Created by Peter Tang on 15/9/2023.
//
@@ -18,4 +18,71 @@ final class MTFontV2Tests: XCTestCase {
XCTAssertNotNil(mTable)
}
}
private let executionQueue = DispatchQueue(label: "com.swiftmath.mathbundle", attributes: .concurrent)
private let executionGroup = DispatchGroup()
let totalCases = 1000
var testCount = 0
func testConcurrentThreadsafeScript() throws {
testCount = 0
var mathFont: MathFont { .allCases.randomElement()! }
for caseNumber in 0 ..< totalCases {
helperConcurrentMTFontV2(caseNumber, mathFont: mathFont, in: executionGroup, on: executionQueue)
}
executionGroup.notify(queue: .main) { [weak self] in
guard let self = self else { return }
XCTAssertEqual(self.testCount, totalCases)
print("\(self.testCount) completed =================")
}
executionGroup.wait()
}
func helperConcurrentMTFontV2(_ count: Int, mathFont: MathFont, in group: DispatchGroup, on queue: DispatchQueue) {
let size = CGFloat.random(in: 20 ... 40)
let workitem = DispatchWorkItem {
let fontV2 = mathFont.mtfont(size: size)
XCTAssertNotNil(fontV2)
let (cgfont, ctfont) = (fontV2.defaultCGFont, fontV2.ctFont)
XCTAssertNotNil(cgfont)
XCTAssertNotNil(ctfont)
}
workitem.notify(queue: .main) { [weak self] in
// print("\(Thread.isMainThread ? "main" : "global") completed .....")
let fontV2 = mathFont.mtfont(size: size)
XCTAssertNotNil(fontV2)
let (cgfont, ctfont) = (fontV2.defaultCGFont, fontV2.ctFont)
XCTAssertNotNil(cgfont)
XCTAssertNotNil(ctfont)
let mTable = mathFont.rawMathTable()
XCTAssertNotNil(mTable)
self?.testCount += 1
}
queue.async(group: group, execute: workitem)
}
func testConcurrentThreadsafeMathTableLockScript() throws {
testCount = 0
var mathFont: MathFont { .allCases.randomElement()! }
var size: CGFloat { CGFloat.random(in: 20 ... 40) }
let mtfonts = Array( 0 ..< 5 ).map { _ in mathFont.mtfont(size: size) }
for caseNumber in 0 ..< totalCases {
helperConcurrentMTFontV2MathTableLock(caseNumber, mtfont: mtfonts.randomElement()!, in: executionGroup, on: executionQueue)
}
executionGroup.notify(queue: .main) { [weak self] in
guard let self = self else { return }
XCTAssertEqual(self.testCount, totalCases)
print("\(self.testCount) completed =================")
}
executionGroup.wait()
}
func helperConcurrentMTFontV2MathTableLock(_ count: Int, mtfont: MTFontV2, in group: DispatchGroup, on queue: DispatchQueue) {
let workitem = DispatchWorkItem {
let mathTable = mtfont.mathTable as? MTFontMathTableV2
// each mathTable is initialized once per mtfont with a NSLock.
// this is even when mathTable is accessed via different threads.
XCTAssertNotNil(mathTable)
}
workitem.notify(queue: .main) { [weak self] in
// print("\(Thread.isMainThread ? "main" : "global") completed .....")
self?.testCount += 1
}
queue.async(group: group, execute: workitem)
}
}

View File

@@ -3,7 +3,7 @@ import XCTest
//
// MathFontTests.swift
//
//
//
// Created by Peter Tang on 12/9/2023.
//
@@ -16,7 +16,10 @@ final class MathFontTests: XCTestCase {
// 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")
XCTAssertEqual($0.ctFont(withSize: CGFloat(size)).fontSize, CGFloat(size), "ctFont fontSize != size.")
XCTAssertEqual($0.cgFont().postScriptName as? String, $0.fontName, "postscript Name != UIFont fontName")
// XCTAssertEqual($0.uiFont(withSize: CGFloat(size))?.familyName, $0.fontFamilyName, "uifont familyName != familyName.")
XCTAssertEqual(CTFontCopyFamilyName($0.ctFont(withSize: CGFloat(size))) as String, $0.fontFamilyName, "ctfont.family != familyName")
}
#if os(iOS)
// for family in UIFont.familyNames.sorted() {
@@ -50,4 +53,96 @@ final class MathFontTests: XCTestCase {
var fontFamilyNames: [String] {
MathFont.allCases.map { $0.fontFamilyName }
}
private let executionQueue = DispatchQueue(label: "com.swiftmath.mathbundle", attributes: .concurrent)
private let executionGroup = DispatchGroup()
let totalCases = 5000
var testCount = 0
func testConcurrentThreadsafeScript() throws {
var mathFont: MathFont { .allCases.randomElement()! }
for caseNumber in 0 ..< totalCases {
switch caseNumber % 3 {
case 0:
helperConcurrentCGFont(caseNumber, mathFont: mathFont, in: executionGroup, on: executionQueue)
case 1:
helperConcurrentCTFont(caseNumber, mathFont: mathFont, in: executionGroup, on: executionQueue)
case 2:
helperConcurrentMathTable(caseNumber, mathFont: mathFont, in: executionGroup, on: executionQueue)
default:
continue
}
}
executionGroup.notify(queue: .main) { [weak self] in
guard let self = self else { return }
XCTAssertEqual(self.testCount, totalCases)
print("\(self.testCount) completed =================")
}
executionGroup.wait()
}
// func helperConcurrentOnDemandRegistration(_ count: Int, mathFont: MathFont, in group: DispatchGroup, on queue: DispatchQueue) {
// let workitem = DispatchWorkItem {
// BundleManager.manager.onDemandRegistration(mathFont: mathFont)
// }
// workitem.notify(queue: .main) { [weak self] in
// self?.testCount += 1
// }
// queue.async(group: group, execute: workitem)
// }
// func helperConcurrentBundleRegistration(mathFont: MathFont, in group: DispatchGroup, on queue: DispatchQueue) {
// let workitem = DispatchWorkItem {
// // BundleManager.manager.onDemandRegistration(mathFont: mathFont)
// try? BundleManager.manager.registerCGFont(mathFont: mathFont)
// try? BundleManager.manager.registerMathTable(mathFont: mathFont)
// let font = BundleManager.manager.cgFonts[mathFont]
// XCTAssertNotNil(font, "font != nil")
// }
// workitem.notify(queue: .main) { [weak self] in
// // print("\(Thread.isMainThread ? "main" : "global") completed .....")
// let font = mathFont.cgFont()
// XCTAssertNotNil(font, "font != nil")
// self?.testCount += 1
// }
// queue.async(group: group, execute: workitem)
// }
func helperConcurrentCGFont(_ count: Int, mathFont: MathFont, in group: DispatchGroup, on queue: DispatchQueue) {
let workitem = DispatchWorkItem {
let font = mathFont.cgFont()
XCTAssertNotNil(font, "font != nil")
}
workitem.notify(queue: .main) { [weak self] in
// print("\(Thread.isMainThread ? "main" : "global") completed .....")
let font = mathFont.cgFont()
XCTAssertNotNil(font, "font != nil")
self?.testCount += 1
}
queue.async(group: group, execute: workitem)
}
func helperConcurrentCTFont(_ count: Int, mathFont: MathFont, in group: DispatchGroup, on queue: DispatchQueue) {
let size = CGFloat.random(in: 20 ... 40)
let workitem = DispatchWorkItem {
let font = mathFont.ctFont(withSize: size)
XCTAssertNotNil(font, "font != nil")
}
workitem.notify(queue: .main) { [weak self] in
// print("\(Thread.isMainThread ? "main" : "global") completed .....")
let font = mathFont.ctFont(withSize: size)
XCTAssertNotNil(font, "font != nil")
self?.testCount += 1
}
queue.async(group: group, execute: workitem)
}
func helperConcurrentMathTable(_ count: Int, mathFont: MathFont, in group: DispatchGroup, on queue: DispatchQueue) {
let workitem = DispatchWorkItem {
let mathtable = mathFont.rawMathTable()
XCTAssertNotNil(mathtable, "mathTable != nil")
}
workitem.notify(queue: .main) { [weak self] in
// print("\(Thread.isMainThread ? "main" : "global") completed .....")
let mathtable = mathFont.rawMathTable()
XCTAssertNotNil(mathtable, "mathTable != nil")
self?.testCount += 1
}
queue.async(group: group, execute: workitem)
}
}

View File

@@ -0,0 +1,208 @@
//
// MathImageTests.swift
//
//
// Created by Peter Tang on 18/9/2023.
//
import XCTest
@testable import SwiftMath
final class MathImageTests: XCTestCase {
func safeImage(fileName: String, pngData: Data) {
let imageFileURL = URL(fileURLWithPath: NSTemporaryDirectory().appending("image-\(fileName).png"))
try? pngData.write(to: imageFileURL, options: [.atomicWrite])
//print("\(#function) \(imageFileURL.path)")
}
func testMathImageScript() throws {
let latex = Latex.samples.randomElement()!
let mathfont = MathFont.allCases.randomElement()!
let fontsize = CGFloat.random(in: 24 ... 36)
let result = SwiftMathImageResult.useMathImage(latex: latex, font: mathfont, fontSize: fontsize)
XCTAssertNil(result.error)
XCTAssertNotNil(result.image)
if result.error == nil, let image = result.image, let imageData = image.pngData() {
safeImage(fileName: "test", pngData: imageData)
let fileUrl = URL(fileURLWithPath: NSTemporaryDirectory())
print("completed, check \(fileUrl.path) image-test.png =================")
}
}
func testSequentialMultipleImageScript() throws {
var latex: String { Latex.samples.randomElement()! }
var mathfont: MathFont { MathFont.allCases.randomElement()! }
var fontsize: CGFloat { CGFloat.random(in: 20 ... 40) }
for caseNumber in 0 ..< 20 {
let result: SwiftMathImageResult
switch caseNumber % 2 {
case 0:
result = SwiftMathImageResult.useMathImage(latex: latex, font: mathfont, fontSize: fontsize)
XCTAssertNil(result.error)
XCTAssertNotNil(result.image)
if result.error == nil, let image = result.image, let imageData = image.pngData() {
safeImage(fileName: "\(caseNumber)", pngData: imageData)
let fileUrl = URL(fileURLWithPath: NSTemporaryDirectory())
print("completed image-\(caseNumber).png")
}
default:
result = SwiftMathImageResult.useMTMathImage(latex: latex, font: mathfont, fontSize: fontsize)
XCTAssertNil(result.error)
XCTAssertNotNil(result.image)
if result.error == nil, let image = result.image, let imageData = image.pngData() {
safeImage(fileName: "\(caseNumber)", pngData: imageData)
let fileUrl = URL(fileURLWithPath: NSTemporaryDirectory())
print("completed image-\(caseNumber).png")
}
}
}
print("check: \(URL(fileURLWithPath: NSTemporaryDirectory()).path) ==")
}
private let executionQueue = DispatchQueue(label: "com.swiftmath.mathbundle", attributes: .concurrent)
private let executionGroup = DispatchGroup()
let totalCases = 20
var testCount = 0
func testConcurrentMathImageScript() throws {
var latex: String { Latex.samples.randomElement()! }
var mathfont: MathFont { MathFont.allCases.randomElement()! }
var size: CGFloat { CGFloat.random(in: 20 ... 40) }
for caseNumber in 0 ..< totalCases {
switch caseNumber % 2 {
case 0:
helperConcurrentMathImage(caseNumber, latex: latex, mathfont: mathfont, fontsize: size, in: executionGroup, on: executionQueue)
default:
helperConcurrentMTMathImage(caseNumber, latex: latex, mathfont: mathfont, fontsize: size, in: executionGroup, on: executionQueue)
}
}
executionGroup.notify(queue: .main) { [weak self] in
let fileUrl = URL(fileURLWithPath: NSTemporaryDirectory())
print("\(self?.testCount)/\(self?.totalCases) completed, check \(fileUrl.path) ===")
XCTAssertEqual(self?.testCount,self?.totalCases)
}
executionGroup.wait()
}
func helperConcurrentMathImage(_ count: Int, latex: String, mathfont: MathFont, fontsize: CGFloat, in group: DispatchGroup, on queue: DispatchQueue) {
let workitem = DispatchWorkItem { [weak self] in
let result = SwiftMathImageResult.useMathImage(latex: latex, font: mathfont, fontSize: fontsize)
XCTAssertNil(result.error)
XCTAssertNotNil(result.image)
if result.error == nil, let image = result.image, let imageData = image.pngData() {
self?.safeImage(fileName: "\(count)", pngData: imageData)
}
}
workitem.notify(queue: .main) { [weak self] in
self?.testCount += 1
}
queue.async(group: group, execute: workitem)
}
func helperConcurrentMTMathImage(_ count: Int, latex: String, mathfont: MathFont, fontsize: CGFloat, in group: DispatchGroup, on queue: DispatchQueue) {
let workitem = DispatchWorkItem { [weak self] in
let result = SwiftMathImageResult.useMTMathImage(latex: latex, font: mathfont, fontSize: fontsize)
XCTAssertNil(result.error)
XCTAssertNotNil(result.image)
if result.error == nil, let image = result.image, let imageData = image.pngData() {
self?.safeImage(fileName: "\(count)", pngData: imageData)
}
}
workitem.notify(queue: .main) { [weak self] in
self?.testCount += 1
}
queue.async(group: group, execute: workitem)
}
}
public struct SwiftMathImageResult {
let error: NSError?
let image: MTImage?
}
extension SwiftMathImageResult {
public static func useMTMathImage(latex: String, font: MathFont, fontSize: CGFloat, textColor: MTColor = MTColor.black) -> SwiftMathImageResult {
let alignment = MTTextAlignment.left
let formatter = MTMathImage(latex: latex, fontSize: fontSize - 1.0,
textColor: textColor,
labelMode: .text, textAlignment: alignment)
formatter.font = font.mtfont(size: fontSize)
let (error, image) = formatter.asImage()
return SwiftMathImageResult(error: error, image: image)
}
public static func useMathImage(latex: String, font: MathFont, fontSize: CGFloat, textColor: MTColor = MTColor.black) -> SwiftMathImageResult {
let alignment = MTTextAlignment.left
var formatter = MathImage(latex: latex, fontSize: fontSize - 1.0,
textColor: textColor,
labelMode: .text, textAlignment: alignment)
formatter.font = font
let (error, image) = formatter.asImage()
return SwiftMathImageResult(error: error, image: image)
}
}
#if os(macOS)
extension NSBitmapImageRep {
var png: Data? { representation(using: .png, properties: [:]) }
}
extension Data {
var bitmap: NSBitmapImageRep? { NSBitmapImageRep(data: self) }
}
extension NSImage {
func pngData() -> Data? {
tiffRepresentation?.bitmap?.png
}
}
#endif
enum Latex {
static let samples: [String] = [
#"(a_1 + a_2)^2 = a_1^2 + 2a_1a_2 + a_2^2"#,
#"x = \frac{-b \pm \sqrt{b^2-4ac}}{2a}"#,
#"\sigma = \sqrt{\frac{1}{N}\sum_{i=1}^N (x_i - \mu)^2}"#,
#"\neg(P\land Q) \iff (\neg P)\lor(\neg Q)"#,
#"\cos(\theta + \varphi) = \cos(\theta)\cos(\varphi) - \sin(\theta)\sin(\varphi)"#,
#"\lim_{x\to\infty}\left(1 + \frac{k}{x}\right)^x = e^k"#,
#"f(x) = \int\limits_{-\infty}^\infty\hat f(\xi)\,e^{2 \pi i \xi x}\,\mathrm{d}\xi"#,
#"{n \brace k} = \frac{1}{k!}\sum_{j=0}^k (-1)^{k-j}\binom{k}{j}(k-j)^n"#,
#"\int_{-\infty}^{\infty} \! e^{-x^2} dx = \sqrt{\pi}"#,
#"\frac{1}{n}\sum_{i=1}^{n}x_i \geq \sqrt[n]{\prod_{i=1}^{n}x_i}"#,
#"\left(\sum_{k=1}^n a_k b_k \right)^2 \le \left(\sum_{k=1}^n a_k^2\right)\left(\sum_{k=1}^n b_k^2\right)"#,
#"\left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)"#,
#"i\hbar\frac{\partial}{\partial t}\mathbf\Psi(\mathbf{x},t) = -\frac{\hbar}{2m}\nabla^2\mathbf\Psi(\mathbf{x},t) + V(\mathbf{x})\mathbf\Psi(\mathbf{x},t)"#,
#"""
\begin{gather}
\dot{x} = \sigma(y-x) \\
\dot{y} = \rho x - y - xz \\
\dot{z} = -\beta z + xy"
\end{gather}
"""#,
#"""
\vec \bf V_1 \times \vec \bf V_2 = \begin{vmatrix}
\hat \imath &\hat \jmath &\hat k \\
\frac{\partial X}{\partial u} & \frac{\partial Y}{\partial u} & 0 \\
\frac{\partial X}{\partial v} & \frac{\partial Y}{\partial v} & 0
\end{vmatrix}
"""#,
#"""
\begin{eqalign}
\nabla \cdot \vec{\bf E} & = \frac {\rho} {\varepsilon_0} \\
\nabla \cdot \vec{\bf B} & = 0 \\
\nabla \times \vec{\bf E} &= - \frac{\partial\vec{\bf B}}{\partial t} \\
\nabla \times \vec{\bf B} & = \mu_0\vec{\bf J} + \mu_0\varepsilon_0 \frac{\partial\vec{\bf E}}{\partial t}
\end{eqalign}
"""#,
#"\log_b(x) = \frac{\log_a(x)}{\log_a(b)}"#,
#"""
\begin{pmatrix}
a & b\\ c & d
\end{pmatrix}
\begin{pmatrix}
\alpha & \beta \\ \gamma & \delta
\end{pmatrix} =
\begin{pmatrix}
a\alpha + b\gamma & a\beta + b \delta \\
c\alpha + d\gamma & c\beta + d \delta
\end{pmatrix}
"""#,
#"""
\frak Q(\lambda,\hat{\lambda}) =
-\frac{1}{2} \mathbb P(O \mid \lambda ) \sum_s \sum_m \sum_t \gamma_m^{(s)} (t) +\\
\quad \left( \log(2 \pi ) + \log \left| \cal C_m^{(s)} \right| +
\left( o_t - \hat{\mu}_m^{(s)} \right) ^T \cal C_m^{(s)-1} \right)
"""#
]
}