Files
swiftui-math/Tests/SwiftMathTests/MathImageTests.swift
2024-12-15 12:21:08 -05:00

209 lines
9.7 KiB
Swift
Executable File

//
// 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)
"""#
]
}