209 lines
9.6 KiB
Swift
209 lines
9.6 KiB
Swift
//
|
|
// 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)
|
|
"""#
|
|
]
|
|
}
|