From 6306ab7c4b4c4668528afcfa1c7569e052765615 Mon Sep 17 00:00:00 2001 From: Peter Tang Date: Mon, 18 Sep 2023 14:47:45 +0800 Subject: [PATCH] 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) + """# + ] +}