diff --git a/Fixtures/outputs/category.png b/Fixtures/outputs/category.png index ec0c221..3a3015c 100644 Binary files a/Fixtures/outputs/category.png and b/Fixtures/outputs/category.png differ diff --git a/Sources/Csv2ImgCore/ImageMaker.swift b/Sources/Csv2ImgCore/ImageMaker.swift index 7a56799..6de9a3c 100644 --- a/Sources/Csv2ImgCore/ImageMaker.swift +++ b/Sources/Csv2ImgCore/ImageMaker.swift @@ -74,11 +74,13 @@ final class ImageMaker: ImageMakerType { throw ImageMakingError.noContextAvailable } #elseif os(iOS) - UIGraphicsBeginImageContext( + UIGraphicsBeginImageContextWithOptions( CGSize( width: width, height: height - ) + ), + true, + 0 ) guard let context = UIGraphicsGetCurrentContext() else { throw ImageMakingError.noContextAvailable diff --git a/Sources/Csv2ImgCore/ImageRenderer.swift b/Sources/Csv2ImgCore/ImageRenderer.swift index b5638ed..3c716f1 100644 --- a/Sources/Csv2ImgCore/ImageRenderer.swift +++ b/Sources/Csv2ImgCore/ImageRenderer.swift @@ -71,7 +71,7 @@ public class ImageRenderer { context: CGContext, text: String, frame: CGRect, style: Csv.Column.Style, fontSize: CGFloat ) { let attributes: [NSAttributedString.Key: Any] = [ - .font: Font.systemFont(ofSize: fontSize), + .font: text.getFont(ofSize: fontSize), .foregroundColor: style.displayableColor(), ] diff --git a/Sources/Csv2ImgCore/PdfMaker.swift b/Sources/Csv2ImgCore/PdfMaker.swift index 72beb28..495f479 100644 --- a/Sources/Csv2ImgCore/PdfMaker.swift +++ b/Sources/Csv2ImgCore/PdfMaker.swift @@ -591,9 +591,7 @@ extension PdfMaker { let str = NSAttributedString( string: text, attributes: [ - .font: Font.systemFont( - ofSize: fontSize - ), + .font: text.getFont(ofSize: fontSize), .foregroundColor: style.displayableColor(), ] ) @@ -717,10 +715,7 @@ extension PdfMaker { let str = NSAttributedString( string: column.name, attributes: [ - .font: Font.systemFont( - ofSize: fontSize, - weight: .bold - ), + .font: column.name.getFont(ofSize: fontSize), .foregroundColor: column.style.displayableColor(), ] ) diff --git a/Sources/Csv2ImgCore/String+Ex.swift b/Sources/Csv2ImgCore/String+Ex.swift index b800c62..f280a72 100644 --- a/Sources/Csv2ImgCore/String+Ex.swift +++ b/Sources/Csv2ImgCore/String+Ex.swift @@ -1,26 +1,30 @@ import Foundation - -#if canImport(AppKit) - import AppKit - typealias Font = NSFont -#elseif canImport(UIKit) - import UIKit - typealias Font = UIFont -#endif +import CoreText extension String { func getSize( fontSize: Double ) -> CGSize { - (self as NSString) - .size( - withAttributes: [ - .font: Font.systemFont( - ofSize: fontSize, - weight: .bold - ) - ] - ) + // Calculate the size of the string using CoreText + let font = getFont(ofSize: fontSize) + let attrString = NSAttributedString( + string: self, + attributes: [ + .font: font + ]) + let size = attrString.size() + return CGSize( + width: Int(size.width), + height: Int(size.height) + ) + } + + func getFont(ofSize fontSize: CGFloat) -> CTFont { + CTFontCreateWithName( + "San Francisco" as CFString, + fontSize, + nil + ) } } diff --git a/Tests/Csv2ImgCoreTests/ImageMakerTests.swift b/Tests/Csv2ImgCoreTests/ImageMakerTests.swift index 89438af..f26c43d 100644 --- a/Tests/Csv2ImgCoreTests/ImageMakerTests.swift +++ b/Tests/Csv2ImgCoreTests/ImageMakerTests.swift @@ -20,63 +20,124 @@ class ImageMakerTests: XCTestCase { Csv.Column.Style(color: Color.blue.cgColor), ] ) - let expectedImageRepresentation = CsvImageRepresentation( - width: 546, - height: 160, - backgroundColor: CGColor(red: 0.980392, green: 0.980392, blue: 0.980392, alpha: 1), - fontSize: 12.0, - columns: [ - CsvImageRepresentation.ColumnRepresentation( - name: "name", - style: Csv.Column.Style(color: Color.blue.cgColor, applyOnlyColumn: false), - frame: CGRect(x: 8.0, y: 12.0, width: 158.0, height: 25.0)), - CsvImageRepresentation.ColumnRepresentation( - name: "beginnerValue", - style: Csv.Column.Style(color: Color.blue.cgColor, applyOnlyColumn: false), - frame: CGRect(x: 174.0, y: 12.0, width: 106.0, height: 25.0)), - CsvImageRepresentation.ColumnRepresentation( - name: "middleValue", - style: Csv.Column.Style(color: Color.blue.cgColor, applyOnlyColumn: false), - frame: CGRect(x: 288.0, y: 12.0, width: 93.0, height: 25.0)), - CsvImageRepresentation.ColumnRepresentation( - name: "expertValue", - style: Csv.Column.Style(color: Color.blue.cgColor, applyOnlyColumn: false), - frame: CGRect(x: 389.0, y: 12.0, width: 92.0, height: 25.0)), - CsvImageRepresentation.ColumnRepresentation( - name: "unit", - style: Csv.Column.Style(color: Color.blue.cgColor, applyOnlyColumn: false), - frame: CGRect(x: 489.0, y: 12.0, width: 49.0, height: 25.0)), - ], - rows: [ - CsvImageRepresentation.RowRepresentation( - values: ["Requirements Analysis", "1.00", "1.00", "1.00", "H"], - frames: [ - CGRect(x: 8.0, y: 49.0, width: 158.0, height: 25.0), - CGRect(x: 174.0, y: 49.0, width: 106.0, height: 25.0), - CGRect(x: 288.0, y: 49.0, width: 93.0, height: 25.0), - CGRect(x: 389.0, y: 49.0, width: 92.0, height: 25.0), - CGRect(x: 489.0, y: 49.0, width: 49.0, height: 25.0), - ]), - CsvImageRepresentation.RowRepresentation( - values: ["Concept Design", "0.10", "0.50", "1.00", "H"], - frames: [ - CGRect(x: 8.0, y: 86.0, width: 158.0, height: 25.0), - CGRect(x: 174.0, y: 86.0, width: 106.0, height: 25.0), - CGRect(x: 288.0, y: 86.0, width: 93.0, height: 25.0), - CGRect(x: 389.0, y: 86.0, width: 92.0, height: 25.0), - CGRect(x: 489.0, y: 86.0, width: 49.0, height: 25.0), - ]), - CsvImageRepresentation.RowRepresentation( - values: ["Detail Design", "0.10", "0.50", "1.00", "page"], - frames: [ - CGRect(x: 8.0, y: 123.0, width: 158.0, height: 25.0), - CGRect(x: 174.0, y: 123.0, width: 106.0, height: 25.0), - CGRect(x: 288.0, y: 123.0, width: 93.0, height: 25.0), - CGRect(x: 389.0, y: 123.0, width: 92.0, height: 25.0), - CGRect(x: 489.0, y: 123.0, width: 49.0, height: 25.0), - ]), - ] - ) + let expectedImageRepresentation: CsvImageRepresentation + #if os(iOS) + expectedImageRepresentation = CsvImageRepresentation( + width: 500, + height: 152, + backgroundColor: CGColor(red: 0.980392, green: 0.980392, blue: 0.980392, alpha: 1), + fontSize: 12.0, + columns: [ + CsvImageRepresentation.ColumnRepresentation( + name: "name", + style: Csv.Column.Style(color: Color.blue.cgColor, applyOnlyColumn: false), + frame: CGRect(x: 8.0, y: 12.0, width: 142.0, height: 23.0)), + CsvImageRepresentation.ColumnRepresentation( + name: "beginnerValue", + style: Csv.Column.Style(color: Color.blue.cgColor, applyOnlyColumn: false), + frame: CGRect(x: 158.0, y: 12.0, width: 96.0, height: 23.0)), + CsvImageRepresentation.ColumnRepresentation( + name: "middleValue", + style: Csv.Column.Style(color: Color.blue.cgColor, applyOnlyColumn: false), + frame: CGRect(x: 262.0, y: 12.0, width: 85.0, height: 23.0)), + CsvImageRepresentation.ColumnRepresentation( + name: "expertValue", + style: Csv.Column.Style(color: Color.blue.cgColor, applyOnlyColumn: false), + frame: CGRect(x: 355.0, y: 12.0, width: 83.0, height: 23.0)), + CsvImageRepresentation.ColumnRepresentation( + name: "unit", + style: Csv.Column.Style(color: Color.blue.cgColor, applyOnlyColumn: false), + frame: CGRect(x: 446.0, y: 12.0, width: 46.0, height: 23.0)), + ], + rows: [ + CsvImageRepresentation.RowRepresentation( + values: ["Requirements Analysis", "1.00", "1.00", "1.00", "H"], + frames: [ + CGRect(x: 8.0, y: 47.0, width: 142.0, height: 23.0), + CGRect(x: 158.0, y: 47.0, width: 96.0, height: 23.0), + CGRect(x: 262.0, y: 47.0, width: 85.0, height: 23.0), + CGRect(x: 355.0, y: 47.0, width: 83.0, height: 23.0), + CGRect(x: 446.0, y: 47.0, width: 46.0, height: 23.0), + ]), + CsvImageRepresentation.RowRepresentation( + values: ["Concept Design", "0.10", "0.50", "1.00", "H"], + frames: [ + CGRect(x: 8.0, y: 82.0, width: 142.0, height: 23.0), + CGRect(x: 158.0, y: 82.0, width: 96.0, height: 23.0), + CGRect(x: 262.0, y: 82.0, width: 85.0, height: 23.0), + CGRect(x: 355.0, y: 82.0, width: 83.0, height: 23.0), + CGRect(x: 446.0, y: 82.0, width: 46.0, height: 23.0), + ]), + CsvImageRepresentation.RowRepresentation( + values: ["Detail Design", "0.10", "0.50", "1.00", "page"], + frames: [ + CGRect(x: 8.0, y: 117.0, width: 142.0, height: 23.0), + CGRect(x: 158.0, y: 117.0, width: 96.0, height: 23.0), + CGRect(x: 262.0, y: 117.0, width: 85.0, height: 23.0), + CGRect(x: 355.0, y: 117.0, width: 83.0, height: 23.0), + CGRect(x: 446.0, y: 117.0, width: 46.0, height: 23.0), + ]), + ] + ) + #elseif os(macOS) + expectedImageRepresentation = CsvImageRepresentation( + width: 500, + height: 148, + backgroundColor: CGColor(red: 0.980392, green: 0.980392, blue: 0.980392, alpha: 1), + fontSize: 12.0, + columns: [ + CsvImageRepresentation.ColumnRepresentation( + name: "name", + style: Csv.Column.Style(color: Color.blue.cgColor, applyOnlyColumn: false), + frame: CGRect(x: 8.0, y: 12.0, width: 142.0, height: 22.0)), + CsvImageRepresentation.ColumnRepresentation( + name: "beginnerValue", + style: Csv.Column.Style(color: Color.blue.cgColor, applyOnlyColumn: false), + frame: CGRect(x: 158.0, y: 12.0, width: 96.0, height: 22.0)), + CsvImageRepresentation.ColumnRepresentation( + name: "middleValue", + style: Csv.Column.Style(color: Color.blue.cgColor, applyOnlyColumn: false), + frame: CGRect(x: 262.0, y: 12.0, width: 85.0, height: 22.0)), + CsvImageRepresentation.ColumnRepresentation( + name: "expertValue", + style: Csv.Column.Style(color: Color.blue.cgColor, applyOnlyColumn: false), + frame: CGRect(x: 355.0, y: 12.0, width: 83.0, height: 22.0)), + CsvImageRepresentation.ColumnRepresentation( + name: "unit", + style: Csv.Column.Style(color: Color.blue.cgColor, applyOnlyColumn: false), + frame: CGRect(x: 446.0, y: 12.0, width: 46.0, height: 22.0)), + ], + rows: [ + CsvImageRepresentation.RowRepresentation( + values: ["Requirements Analysis", "1.00", "1.00", "1.00", "H"], + frames: [ + CGRect(x: 8.0, y: 46.0, width: 142.0, height: 22.0), + CGRect(x: 158.0, y: 46.0, width: 96.0, height: 22.0), + CGRect(x: 262.0, y: 46.0, width: 85.0, height: 22.0), + CGRect(x: 355.0, y: 46.0, width: 83.0, height: 22.0), + CGRect(x: 446.0, y: 46.0, width: 46.0, height: 22.0), + ]), + CsvImageRepresentation.RowRepresentation( + values: ["Concept Design", "0.10", "0.50", "1.00", "H"], + frames: [ + CGRect(x: 8.0, y: 80.0, width: 142.0, height: 22.0), + CGRect(x: 158.0, y: 80.0, width: 96.0, height: 22.0), + CGRect(x: 262.0, y: 80.0, width: 85.0, height: 22.0), + CGRect(x: 355.0, y: 80.0, width: 83.0, height: 22.0), + CGRect(x: 446.0, y: 80.0, width: 46.0, height: 22.0), + ]), + CsvImageRepresentation.RowRepresentation( + values: ["Detail Design", "0.10", "0.50", "1.00", "page"], + frames: [ + CGRect(x: 8.0, y: 114.0, width: 142.0, height: 22.0), + CGRect(x: 158.0, y: 114.0, width: 96.0, height: 22.0), + CGRect(x: 262.0, y: 114.0, width: 85.0, height: 22.0), + CGRect(x: 355.0, y: 114.0, width: 83.0, height: 22.0), + CGRect(x: 446.0, y: 114.0, width: 46.0, height: 22.0), + ]), + ] + ) + #endif let imageMaker = ImageMaker(maximumRowCount: nil, fontSize: 12) let columns = await csv.columns let rows = await csv.rows @@ -124,4 +185,27 @@ class ImageMakerTests: XCTestCase { XCTAssertEqual(actual.frames, expected.frames) } } + + func testMakeImage() async throws { + // Given + let outputFileURL = getRelativeFilePathFromPackageSource( + path: "Fixtures/outputs/category.png" + ) + let csv = Csv.loadFromString( + """ + name,beginnerValue,middleValue,expertValue,unit + Requirements Analysis,1.00,1.00,1.00,H + Concept Design,0.10,0.50,1.00,H + Detail Design,0.10,0.50,1.00,page + """ + ) + let imageMaker = ImageMaker(maximumRowCount: nil, fontSize: 12) + let columns = await csv.columns + let rows = await csv.rows + // When + let image = try imageMaker.make(columns: columns, rows: rows) { double in + } + // Then + try image.convertToData()?.write(to: outputFileURL, options: .atomic) + } } diff --git a/Tests/Csv2ImgCoreTests/String+ExTests.swift b/Tests/Csv2ImgCoreTests/String+ExTests.swift new file mode 100644 index 0000000..e21b8fc --- /dev/null +++ b/Tests/Csv2ImgCoreTests/String+ExTests.swift @@ -0,0 +1,28 @@ +// +// String+ExTests.swift +// Csv2Img +// +// Created by Fumiya Tanaka on 2024/10/19. +// + +import XCTest + +@testable import Csv2ImgCore + +class StringExtensionTests: XCTestCase { + func testGetSize() throws { + let sut = "Hello World" + let fontSize: CGFloat = 12 + + // Because font system is different between iOS and macOS, + // calculation logic might be different and result font size differs. + #if os(iOS) + let expected = CGSize(width: 61, height: 13) + #elseif os(macOS) + let expected = CGSize(width: 61, height: 12) + #endif + + let actual = sut.getSize(fontSize: fontSize) + XCTAssertEqual(actual, expected) + } +}