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)