// // File.swift // // // Created by Peter Tang on 12/9/2023. // import Foundation #if os(iOS) || os(visionOS) import UIKit #endif #if os(macOS) import AppKit #endif public class MTMathImage { public var font: MTFont? = MTFontManager.fontManager.defaultFont public var fontSize:CGFloat { set { _fontSize = newValue let font = font?.copy(withSize: newValue) self.font = font // also forces an update } get { _fontSize } } private var _fontSize:CGFloat = 0 public let textColor: MTColor public let labelMode: MTMathUILabelMode public let textAlignment: MTTextAlignment public var contentInsets: MTEdgeInsets = MTEdgeInsetsZero public let latex: String private(set) var intrinsicContentSize = CGSize.zero public init(latex: String, fontSize: CGFloat, textColor: MTColor, labelMode: MTMathUILabelMode = .display, textAlignment: MTTextAlignment = .center) { self.latex = latex self.textColor = textColor self.labelMode = labelMode self.textAlignment = textAlignment self.fontSize = fontSize } } extension MTMathImage { public var currentStyle: MTLineStyle { switch labelMode { case .display: return .display case .text: return .text } } private func intrinsicContentSize(_ displayList: MTMathListDisplay) -> CGSize { CGSize(width: displayList.width + contentInsets.left + contentInsets.right, height: displayList.ascent + displayList.descent + contentInsets.top + contentInsets.bottom) } public func asImage() -> (NSError?, MTImage?) { func layoutImage(size: CGSize, displayList: MTMathListDisplay) { var textX = CGFloat(0) switch self.textAlignment { case .left: textX = contentInsets.left case .center: textX = (size.width - contentInsets.left - contentInsets.right - displayList.width) / 2 + contentInsets.left case .right: textX = size.width - displayList.width - contentInsets.right } let availableHeight = size.height - contentInsets.bottom - contentInsets.top // center things vertically var height = displayList.ascent + displayList.descent if height < fontSize/2 { height = fontSize/2 // set height to half the font size } let textY = (availableHeight - height) / 2 + displayList.descent + contentInsets.bottom displayList.position = CGPoint(x: textX, y: textY) } var error: NSError? guard let mathList = MTMathListBuilder.build(fromString: latex, error: &error), error == nil, let displayList = MTTypesetter.createLineForMathList(mathList, font: font, style: currentStyle) else { return (error, nil) } intrinsicContentSize = intrinsicContentSize(displayList) displayList.textColor = textColor let size = intrinsicContentSize layoutImage(size: size, displayList: displayList) #if os(iOS) || os(visionOS) let renderer = UIGraphicsImageRenderer(size: size) let image = renderer.image { rendererContext in rendererContext.cgContext.saveGState() rendererContext.cgContext.concatenate(.flippedVertically(size.height)) displayList.draw(rendererContext.cgContext) rendererContext.cgContext.restoreGState() } return (nil, image) #endif #if os(macOS) let image = NSImage(size: size, flipped: false) { bounds in guard let context = NSGraphicsContext.current?.cgContext else { return false } context.saveGState() displayList.draw(context) context.restoreGState() return true } return (nil, image) #endif } } private extension CGAffineTransform { static func flippedVertically(_ height: CGFloat) -> CGAffineTransform { var transform = CGAffineTransform(scaleX: 1, y: -1) transform = transform.translatedBy(x: 0, y: -height) return transform } }