From 5de5ea677e5f213aca404424c37c0fd52201701c Mon Sep 17 00:00:00 2001 From: Peter Tang Date: Mon, 18 Sep 2023 13:30:31 +0800 Subject: [PATCH 1/9] threadsafe MathFont, MTFontV2, MTFontMathTableV2 with concurrent testScripts. --- .../MathBundle/MTFontMathTableV2.swift | 8 +- Sources/SwiftMath/MathBundle/MTFontV2.swift | 31 ++++-- Sources/SwiftMath/MathBundle/MathFont.swift | 103 +++++++++--------- .../MTFontMathTableV2Tests.swift | 55 +++++++++- Tests/SwiftMathTests/MTFontV2Tests.swift | 67 +++++++++++- Tests/SwiftMathTests/MathFontTests.swift | 98 ++++++++++++++++- 6 files changed, 293 insertions(+), 69 deletions(-) diff --git a/Sources/SwiftMath/MathBundle/MTFontMathTableV2.swift b/Sources/SwiftMath/MathBundle/MTFontMathTableV2.swift index 6e580bc..d15cb09 100644 --- a/Sources/SwiftMath/MathBundle/MTFontMathTableV2.swift +++ b/Sources/SwiftMath/MathBundle/MTFontMathTableV2.swift @@ -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! diff --git a/Sources/SwiftMath/MathBundle/MTFontV2.swift b/Sources/SwiftMath/MathBundle/MTFontV2.swift index 53b7fe3..14c09a0 100644 --- a/Sources/SwiftMath/MathBundle/MTFontV2.swift +++ b/Sources/SwiftMath/MathBundle/MTFontV2.swift @@ -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.") } diff --git a/Sources/SwiftMath/MathBundle/MathFont.swift b/Sources/SwiftMath/MathBundle/MathFont.swift index 4c8a1b6..69cc63a 100644 --- a/Sources/SwiftMath/MathBundle/MathFont.swift +++ b/Sources/SwiftMath/MathBundle/MathFont.swift @@ -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 as UIFont/NSFont are not threadsafe and 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. */ @@ -67,16 +68,15 @@ internal extension CTFont { return UInt(CTFontGetUnitsPerEm(self)) } } -private class BundleManager { - static fileprivate(set) var manager: BundleManager = { - return BundleManager() - }() +internal class 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 var initializedOnceAlready: Bool = false + private let threadSafeQueue = DispatchQueue(label: "com.smartmath.mathfont.threadsafequeue", attributes: .concurrent) private func registerCGFont(mathFont: MathFont) throws { guard let frameworkBundleURL = Bundle.module.url(forResource: "mathFonts", withExtension: "bundle"), @@ -89,14 +89,21 @@ private class BundleManager { guard let defaultCGFont = CGFont(dataProvider) else { throw FontError.initFontError } + threadSafeQueue.sync(flags: .barrier) { + cgFonts[mathFont] = defaultCGFont + } - 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? = 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,25 +116,16 @@ 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)") - } + threadSafeQueue.sync(flags: .barrier) { + rawMathTables[mathFont] = rawMathTable } - initializedOnceAlready.toggle() + 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 } + guard threadSafeQueue.sync(execute: { cgFonts[mathFont] }) == nil else { return } + //Note: font registration is now threadsafe. do { try BundleManager.manager.registerCGFont(mathFont: mathFont) try BundleManager.manager.registerMathTable(mathFont: mathFont) @@ -139,7 +137,7 @@ private class BundleManager { 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 @@ -148,21 +146,24 @@ private class BundleManager { 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]! } + let newCTFont = CTFontCreateWithGraphicsFont(cgFont, size, nil, nil) + threadSafeQueue.sync(flags: .barrier) { + ctFonts[fontSizePair] = newCTFont + } + return newCTFont } - fileprivate func obtainMathTable(font: MathFont) -> NSDictionary { + fileprivate func obtainRawMathTable(font: MathFont) -> NSDictionary { // if !initializedOnceAlready { registerAllBundleResources() } 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 +184,7 @@ private class BundleManager { case invalidMathTable } - private struct CTFontPair: Hashable { + private struct CTFontSizePair: Hashable { let font: MathFont let size: CGFloat } diff --git a/Tests/SwiftMathTests/MTFontMathTableV2Tests.swift b/Tests/SwiftMathTests/MTFontMathTableV2Tests.swift index 0e2da91..e6bd154 100644 --- a/Tests/SwiftMathTests/MTFontMathTableV2Tests.swift +++ b/Tests/SwiftMathTests/MTFontMathTableV2Tests.swift @@ -1,6 +1,6 @@ // // MTFontMathTableV2Tests.swift -// +// // // Created by Peter Tang on 15/9/2023. // @@ -25,4 +25,57 @@ 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 =================") + } + } + 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} + print(values) // accessed these values on main thread. + } + self?.testCount += 1 + } + queue.async(group: group, execute: workitem) + } + } diff --git a/Tests/SwiftMathTests/MTFontV2Tests.swift b/Tests/SwiftMathTests/MTFontV2Tests.swift index 37c97ef..103a5a9 100644 --- a/Tests/SwiftMathTests/MTFontV2Tests.swift +++ b/Tests/SwiftMathTests/MTFontV2Tests.swift @@ -1,6 +1,6 @@ // // MTFontV2Tests.swift -// +// // // Created by Peter Tang on 15/9/2023. // @@ -18,4 +18,69 @@ 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 =================") + } + } + 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 =================") + } + } + 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) + } } diff --git a/Tests/SwiftMathTests/MathFontTests.swift b/Tests/SwiftMathTests/MathFontTests.swift index ed2f441..a73b55c 100644 --- a/Tests/SwiftMathTests/MathFontTests.swift +++ b/Tests/SwiftMathTests/MathFontTests.swift @@ -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,95 @@ 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 =================") + } + } + // 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) + } } From 6306ab7c4b4c4668528afcfa1c7569e052765615 Mon Sep 17 00:00:00 2001 From: Peter Tang Date: Mon, 18 Sep 2023 14:47:45 +0800 Subject: [PATCH 2/9] threadsafe MathImage and MTMathImag concurrent testScripts. --- Sources/SwiftMath/MathBundle/MathFont.swift | 2 +- Tests/SwiftMathTests/MathImageTests.swift | 179 ++++++++++++++++++++ 2 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 Tests/SwiftMathTests/MathImageTests.swift diff --git a/Sources/SwiftMath/MathBundle/MathFont.swift b/Sources/SwiftMath/MathBundle/MathFont.swift index 69cc63a..6b68099 100644 --- a/Sources/SwiftMath/MathBundle/MathFont.swift +++ b/Sources/SwiftMath/MathBundle/MathFont.swift @@ -47,7 +47,7 @@ public enum MathFont: String, CaseIterable { BundleManager.manager.obtainRawMathTable(font: self) } - //Note: Below code are no longer supported as UIFont/NSFont are not threadsafe and not used in SwiftMath. + //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) diff --git a/Tests/SwiftMathTests/MathImageTests.swift b/Tests/SwiftMathTests/MathImageTests.swift new file mode 100644 index 0000000..af96eaa --- /dev/null +++ b/Tests/SwiftMathTests/MathImageTests.swift @@ -0,0 +1,179 @@ +// +// MathImageTests.swift +// +// +// Created by Peter Tang on 18/9/2023. +// + +import XCTest +@testable import SwiftMath + +final class MathImageTests: XCTestCase { + 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 safeImage(fileName: String, pngData: Data) { + let imageFileURL = URL(fileURLWithPath: NSTemporaryDirectory().appending("image-\(fileName).png")) + try? pngData.write(to: imageFileURL, options: [.atomicWrite]) + } + private let executionQueue = DispatchQueue.main // DispatchQueue(label: "com.swiftmath.mathbundle", attributes: .concurrent) + private let executionGroup = DispatchGroup() + + let totalCases = 50 + 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 { + // if caseNumber % 2 == 0 { + // helperConcurrentMathImage(caseNumber, latex: latex, mathfont: mathfont, fontsize: size, in: executionGroup, on: executionQueue) + // } else { + // helperConcurrentMTMathImage(caseNumber, latex: latex, mathfont: mathfont, fontsize: size, in: executionGroup, on: executionQueue) + // } + helperConcurrentMTMathImage(caseNumber, latex: latex, mathfont: mathfont, fontsize: size, in: executionGroup, on: executionQueue) + } + executionGroup.notify(queue: .main) { [weak self] in + guard let self = self else { return } + XCTAssertEqual(self.testCount, totalCases) + let fileUrl = URL(fileURLWithPath: NSTemporaryDirectory()) + print("\(self.testCount) completed, check \(fileUrl.path) =================") + } + } + 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.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: "test-\(count)", pngData: imageData) + } + } + workitem.notify(queue: .main) { [weak self] in + // print("\(Thread.isMainThread ? "main" : "global") completed .....") + 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.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: "test-\(count)", pngData: imageData) + } + } + workitem.notify(queue: .main) { [weak self] in + // print("\(Thread.isMainThread ? "main" : "global") completed .....") + 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) + """# + ] +} From b637b18aceb8ad2820db465bb0a1d0c570c3055a Mon Sep 17 00:00:00 2001 From: Peter Tang Date: Mon, 18 Sep 2023 19:04:06 +0800 Subject: [PATCH 3/9] resource loading is serialised, font and mathtable dictionaries in BundleManager are threadsafe protected. --- Sources/SwiftMath/MathBundle/MathFont.swift | 35 ++++----- Sources/SwiftMath/MathBundle/MathImage.swift | 5 +- .../MTFontMathTableV2Tests.swift | 1 + Tests/SwiftMathTests/MTFontV2Tests.swift | 2 + Tests/SwiftMathTests/MathFontTests.swift | 1 + Tests/SwiftMathTests/MathImageTests.swift | 73 ++++++++++++++----- 6 files changed, 78 insertions(+), 39 deletions(-) diff --git a/Sources/SwiftMath/MathBundle/MathFont.swift b/Sources/SwiftMath/MathBundle/MathFont.swift index 6b68099..0b509cd 100644 --- a/Sources/SwiftMath/MathBundle/MathFont.swift +++ b/Sources/SwiftMath/MathBundle/MathFont.swift @@ -68,7 +68,7 @@ internal extension CTFont { return UInt(CTFontGetUnitsPerEm(self)) } } -internal class BundleManager { +private class BundleManager { //Note: below should be lightweight and without threadsafe problem. static internal let manager = BundleManager() @@ -77,7 +77,8 @@ internal class BundleManager { private var rawMathTables = [MathFont: NSDictionary]() private let threadSafeQueue = DispatchQueue(label: "com.smartmath.mathfont.threadsafequeue", attributes: .concurrent) - + private let resourceLoadingQueue = DispatchQueue(label: "com.smartmath.mathfont.resourceLoader") + 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 { @@ -89,9 +90,8 @@ internal class BundleManager { guard let defaultCGFont = CGFont(dataProvider) else { throw FontError.initFontError } - threadSafeQueue.sync(flags: .barrier) { - cgFonts[mathFont] = defaultCGFont - } + + 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. @@ -116,26 +116,29 @@ internal class BundleManager { version == "1.3" else { throw FontError.invalidMathTable } - threadSafeQueue.sync(flags: .barrier) { - rawMathTables[mathFont] = rawMathTable - } + + rawMathTables[mathFont] = rawMathTable + let threadName = Thread.isMainThread ? "main" : "global" debugPrint("mathFonts bundle resource: \(mathFont.rawValue).plist registered on \(threadName).") } private func onDemandRegistration(mathFont: MathFont) { guard threadSafeQueue.sync(execute: { cgFonts[mathFont] }) == nil else { return } - //Note: font registration is now threadsafe. - do { - try BundleManager.manager.registerCGFont(mathFont: mathFont) - try BundleManager.manager.registerMathTable(mathFont: mathFont) + // Note: resourceLoading is now serialized. + resourceLoadingQueue.sync { [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 = threadSafeQueue.sync(execute: { cgFonts[font] }) else { fatalError("\(#function) unable to locate CGFont \(font.fontName)") @@ -144,7 +147,6 @@ internal class BundleManager { } fileprivate func obtainCTFont(font: MathFont, withSize size: CGFloat) -> CTFont { - // if !initializedOnceAlready { registerAllBundleResources() } onDemandRegistration(mathFont: font) let fontSizePair = CTFontSizePair(font: font, size: size) let ctFont = threadSafeQueue.sync(execute: { ctFonts[fontSizePair] }) @@ -161,7 +163,6 @@ internal class BundleManager { return newCTFont } fileprivate func obtainRawMathTable(font: MathFont) -> NSDictionary { - // if !initializedOnceAlready { registerAllBundleResources() } onDemandRegistration(mathFont: font) guard let mathTable = threadSafeQueue.sync(execute: { rawMathTables[font] } ) else { fatalError("\(#function) unable to locate mathTable: \(font.rawValue).plist") diff --git a/Sources/SwiftMath/MathBundle/MathImage.swift b/Sources/SwiftMath/MathBundle/MathImage.swift index 8235a4e..ec745c6 100644 --- a/Sources/SwiftMath/MathBundle/MathImage.swift +++ b/Sources/SwiftMath/MathBundle/MathImage.swift @@ -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) diff --git a/Tests/SwiftMathTests/MTFontMathTableV2Tests.swift b/Tests/SwiftMathTests/MTFontMathTableV2Tests.swift index e6bd154..642197f 100644 --- a/Tests/SwiftMathTests/MTFontMathTableV2Tests.swift +++ b/Tests/SwiftMathTests/MTFontMathTableV2Tests.swift @@ -42,6 +42,7 @@ final class MTFontMathTableV2Tests: XCTestCase { 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 { diff --git a/Tests/SwiftMathTests/MTFontV2Tests.swift b/Tests/SwiftMathTests/MTFontV2Tests.swift index 103a5a9..6cda61c 100644 --- a/Tests/SwiftMathTests/MTFontV2Tests.swift +++ b/Tests/SwiftMathTests/MTFontV2Tests.swift @@ -33,6 +33,7 @@ final class MTFontV2Tests: XCTestCase { 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) @@ -69,6 +70,7 @@ final class MTFontV2Tests: XCTestCase { 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 { diff --git a/Tests/SwiftMathTests/MathFontTests.swift b/Tests/SwiftMathTests/MathFontTests.swift index a73b55c..daaa25c 100644 --- a/Tests/SwiftMathTests/MathFontTests.swift +++ b/Tests/SwiftMathTests/MathFontTests.swift @@ -78,6 +78,7 @@ final class MathFontTests: XCTestCase { 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 { diff --git a/Tests/SwiftMathTests/MathImageTests.swift b/Tests/SwiftMathTests/MathImageTests.swift index af96eaa..d945ec7 100644 --- a/Tests/SwiftMathTests/MathImageTests.swift +++ b/Tests/SwiftMathTests/MathImageTests.swift @@ -9,6 +9,11 @@ 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()! @@ -22,14 +27,44 @@ final class MathImageTests: XCTestCase { print("completed, check \(fileUrl.path) image-test.png =================") } } - func safeImage(fileName: String, pngData: Data) { - let imageFileURL = URL(fileURLWithPath: NSTemporaryDirectory().appending("image-\(fileName).png")) - try? pngData.write(to: imageFileURL, options: [.atomicWrite]) + 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") + } else { + print("failed 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") + } else { + print("failed image-\(caseNumber).png") + } + } + } + print("check: \(URL(fileURLWithPath: NSTemporaryDirectory()).path) ==") } - private let executionQueue = DispatchQueue.main // DispatchQueue(label: "com.swiftmath.mathbundle", attributes: .concurrent) + + private let executionQueue = DispatchQueue(label: "com.swiftmath.mathbundle", attributes: .concurrent) private let executionGroup = DispatchGroup() - let totalCases = 50 + let totalCases = 20 var testCount = 0 func testConcurrentMathImageScript() throws { @@ -37,46 +72,44 @@ final class MathImageTests: XCTestCase { var mathfont: MathFont { MathFont.allCases.randomElement()! } var size: CGFloat { CGFloat.random(in: 20 ... 40) } for caseNumber in 0 ..< totalCases { - // if caseNumber % 2 == 0 { - // helperConcurrentMathImage(caseNumber, latex: latex, mathfont: mathfont, fontsize: size, in: executionGroup, on: executionQueue) - // } else { - // helperConcurrentMTMathImage(caseNumber, latex: latex, mathfont: mathfont, fontsize: size, in: executionGroup, on: executionQueue) - // } - helperConcurrentMTMathImage(caseNumber, latex: latex, mathfont: mathfont, fontsize: size, in: executionGroup, on: executionQueue) + 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 - guard let self = self else { return } - XCTAssertEqual(self.testCount, totalCases) let fileUrl = URL(fileURLWithPath: NSTemporaryDirectory()) - print("\(self.testCount) completed, check \(fileUrl.path) =================") + 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.useMTMathImage(latex: latex, font: mathfont, fontSize: fontsize) + 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: "test-\(count)", pngData: imageData) + self?.safeImage(fileName: "\(count)", pngData: imageData) } } workitem.notify(queue: .main) { [weak self] in - // print("\(Thread.isMainThread ? "main" : "global") completed .....") 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.useMathImage(latex: latex, font: mathfont, fontSize: fontsize) + 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: "test-\(count)", pngData: imageData) + self?.safeImage(fileName: "\(count)", pngData: imageData) } } workitem.notify(queue: .main) { [weak self] in - // print("\(Thread.isMainThread ? "main" : "global") completed .....") self?.testCount += 1 } queue.async(group: group, execute: workitem) From c6ea427bfb0f737787ca6ab68f51b2d08c4a1017 Mon Sep 17 00:00:00 2001 From: Peter Tang Date: Mon, 18 Sep 2023 20:10:52 +0800 Subject: [PATCH 4/9] clean up unnecessary test script messages. --- Tests/SwiftMathTests/MTFontMathTableV2Tests.swift | 6 +++--- Tests/SwiftMathTests/MathImageTests.swift | 4 ---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Tests/SwiftMathTests/MTFontMathTableV2Tests.swift b/Tests/SwiftMathTests/MTFontMathTableV2Tests.swift index 642197f..7d066bc 100644 --- a/Tests/SwiftMathTests/MTFontMathTableV2Tests.swift +++ b/Tests/SwiftMathTests/MTFontMathTableV2Tests.swift @@ -55,9 +55,9 @@ final class MTFontMathTableV2Tests: XCTestCase { mTable?.fractionNumeratorDisplayStyleGapMin, mTable?.fractionNumeratorGapMin, ].compactMap{$0} - if count % 50 == 0 { - print(values) // accessed these values on global thread. - } + // if count % 50 == 0 { + // print(values) // accessed these values on global thread. + // } XCTAssertNotNil(mTable) } workitem.notify(queue: .main) { [weak self] in diff --git a/Tests/SwiftMathTests/MathImageTests.swift b/Tests/SwiftMathTests/MathImageTests.swift index d945ec7..d858923 100644 --- a/Tests/SwiftMathTests/MathImageTests.swift +++ b/Tests/SwiftMathTests/MathImageTests.swift @@ -42,8 +42,6 @@ final class MathImageTests: XCTestCase { safeImage(fileName: "\(caseNumber)", pngData: imageData) let fileUrl = URL(fileURLWithPath: NSTemporaryDirectory()) print("completed image-\(caseNumber).png") - } else { - print("failed image-\(caseNumber).png") } default: result = SwiftMathImageResult.useMTMathImage(latex: latex, font: mathfont, fontSize: fontsize) @@ -53,8 +51,6 @@ final class MathImageTests: XCTestCase { safeImage(fileName: "\(caseNumber)", pngData: imageData) let fileUrl = URL(fileURLWithPath: NSTemporaryDirectory()) print("completed image-\(caseNumber).png") - } else { - print("failed image-\(caseNumber).png") } } } From 045c2aab720b75a7180a2b7dd7764a5912941129 Mon Sep 17 00:00:00 2001 From: Peter Tang Date: Mon, 18 Sep 2023 20:13:34 +0800 Subject: [PATCH 5/9] clean up unnecessary test script messages. --- Tests/SwiftMathTests/MTFontMathTableV2Tests.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Tests/SwiftMathTests/MTFontMathTableV2Tests.swift b/Tests/SwiftMathTests/MTFontMathTableV2Tests.swift index 7d066bc..a6dcbf4 100644 --- a/Tests/SwiftMathTests/MTFontMathTableV2Tests.swift +++ b/Tests/SwiftMathTests/MTFontMathTableV2Tests.swift @@ -72,7 +72,9 @@ final class MTFontMathTableV2Tests: XCTestCase { mTable?.fractionNumeratorDisplayStyleGapMin, mTable?.fractionNumeratorGapMin, ].compactMap{$0} - print(values) // accessed these values on main thread. + // if count % 50 == 0 { + // print(values) + // } } self?.testCount += 1 } From 024468cdddaa716b8c549af729dfd17c812a1c47 Mon Sep 17 00:00:00 2001 From: Peter Tang Date: Mon, 18 Sep 2023 21:54:45 +0800 Subject: [PATCH 6/9] threadsafe protect CTFontCreateWithGraphicsFont --- Sources/SwiftMath/MathBundle/MathFont.swift | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/Sources/SwiftMath/MathBundle/MathFont.swift b/Sources/SwiftMath/MathBundle/MathFont.swift index 0b509cd..4bfd2b4 100644 --- a/Sources/SwiftMath/MathBundle/MathFont.swift +++ b/Sources/SwiftMath/MathBundle/MathFont.swift @@ -156,11 +156,15 @@ private class BundleManager { } //Note: ctfont creation and caching is now threadsafe. guard threadSafeQueue.sync(execute: { ctFonts[fontSizePair] }) == nil else { return ctFonts[fontSizePair]! } - let newCTFont = CTFontCreateWithGraphicsFont(cgFont, size, nil, nil) - threadSafeQueue.sync(flags: .barrier) { - ctFonts[fontSizePair] = newCTFont - } - return newCTFont + return threadSafeQueue.sync(flags: .barrier, execute: { + if let ctfont = ctFonts[fontSizePair] { + return ctfont + } else { + let result = CTFontCreateWithGraphicsFont(cgFont, size, nil, nil) + ctFonts[fontSizePair] = result + return result + } + }) } fileprivate func obtainRawMathTable(font: MathFont) -> NSDictionary { onDemandRegistration(mathFont: font) From 73a56c3d93ba1e1fe4ea538eedbfefb8ee93e2fa Mon Sep 17 00:00:00 2001 From: Peter Tang Date: Tue, 19 Sep 2023 09:21:07 +0800 Subject: [PATCH 7/9] consolidate to use threadSafeQueue only. --- Sources/SwiftMath/MathBundle/MathFont.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Sources/SwiftMath/MathBundle/MathFont.swift b/Sources/SwiftMath/MathBundle/MathFont.swift index 4bfd2b4..81fc7c1 100644 --- a/Sources/SwiftMath/MathBundle/MathFont.swift +++ b/Sources/SwiftMath/MathBundle/MathFont.swift @@ -77,7 +77,6 @@ private class BundleManager { private var rawMathTables = [MathFont: NSDictionary]() private let threadSafeQueue = DispatchQueue(label: "com.smartmath.mathfont.threadsafequeue", attributes: .concurrent) - private let resourceLoadingQueue = DispatchQueue(label: "com.smartmath.mathfont.resourceLoader") private func registerCGFont(mathFont: MathFont) throws { guard let frameworkBundleURL = Bundle.module.url(forResource: "mathFonts", withExtension: "bundle"), @@ -126,7 +125,7 @@ private class BundleManager { private func onDemandRegistration(mathFont: MathFont) { guard threadSafeQueue.sync(execute: { cgFonts[mathFont] }) == nil else { return } // Note: resourceLoading is now serialized. - resourceLoadingQueue.sync { [weak self] in + threadSafeQueue.sync(flags: .barrier, execute: { [weak self] in if self?.cgFonts[mathFont] == nil { do { try BundleManager.manager.registerCGFont(mathFont: mathFont) @@ -136,7 +135,7 @@ private class BundleManager { fatalError("MTMathFonts:\(#function) ondemand loading failed, mathFont \(mathFont.rawValue), reason \(error)") } } - } + }) } fileprivate func obtainCGFont(font: MathFont) -> CGFont { onDemandRegistration(mathFont: font) From 8cf541286fada2d9a64c07a943de4c1d64b5a399 Mon Sep 17 00:00:00 2001 From: Peter Tang Date: Wed, 20 Sep 2023 09:30:51 +0800 Subject: [PATCH 8/9] Delete Sources/Obsolete directory --- Sources/Obsolete/MathTable.swift | 310 ------------------------------- 1 file changed, 310 deletions(-) delete mode 100644 Sources/Obsolete/MathTable.swift diff --git a/Sources/Obsolete/MathTable.swift b/Sources/Obsolete/MathTable.swift deleted file mode 100644 index 5bd282c..0000000 --- a/Sources/Obsolete/MathTable.swift +++ /dev/null @@ -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 - } -} From 6959b42f81ab6a1c9f07caa54a718aa8e4dfb58e Mon Sep 17 00:00:00 2001 From: Peter Tang Date: Wed, 20 Sep 2023 09:31:17 +0800 Subject: [PATCH 9/9] Delete Tests/Obsolete directory --- Tests/Obsolete/MathTableTests.swift | 35 ----------------------------- 1 file changed, 35 deletions(-) delete mode 100644 Tests/Obsolete/MathTableTests.swift diff --git a/Tests/Obsolete/MathTableTests.swift b/Tests/Obsolete/MathTableTests.swift deleted file mode 100644 index f72509b..0000000 --- a/Tests/Obsolete/MathTableTests.swift +++ /dev/null @@ -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)") - // } - } -}