diff --git a/Sources/Csv2Img/Csv.swift b/Sources/Csv2Img/Csv.swift index 98e05f8..a5e78c1 100644 --- a/Sources/Csv2Img/Csv.swift +++ b/Sources/Csv2Img/Csv.swift @@ -140,511 +140,527 @@ public actor Csv { /// `pdfMetadata` stores pdf metadata which is used when ``Csv2Img.Csv.ExportType`` is `.png` private var pdfMetadata: PDFMetadata { didSet { - pdfMarker.set(metadata: pdfMetadata) - } - } - - /// ``maximumRowCount`` is the max number of Rows. this is fixed due to performance issue. - private let maximumRowCount: Int? = nil - - private let queue = DispatchQueue( - label: "dev.fummicc1.csv2img.csv-queue" - ) - - // MARK: Internal update functions - /// Internal method to update `Array` - func update( - rows: [Row] - ) { - self.rows = rows - } - /// Internal method to update `Array` - func update( - columns: [Column] - ) { - self.columns = columns - } - - func update( - columnStyles: [Column.Style] - ) { - columnStyles.enumerated().forEach { ( - i, - style - ) in - columns[i].style = style - } - } + pdfMarker.set( + metadata: pdfMetadata + ) + } + } + + /// ``maximumRowCount`` is the max number of Rows. this is fixed due to performance issue. + private let maximumRowCount: Int? = nil + + private let queue = DispatchQueue( + label: "dev.fummicc1.csv2img.csv-queue" + ) + + // MARK: Internal update functions + /// Internal method to update `Array` + func update( + rows: [Row] + ) { + self.rows = rows + } + /// Internal method to update `Array` + func update( + columns: [Column] + ) { + self.columns = columns + } + + func update( + columnStyles: [Column.Style] + ) { + columnStyles.enumerated().forEach { ( + i, + style + ) in + columns[i].style = style + } + } } extension Csv { - /** - `ExportType` is a enum that expresses - */ - public enum ExportType: String, Hashable, CaseIterable { - /// `png` output - case png - /// `pdf` output (Work In Progress) - case pdf - - public var fileExtension: String { - self.rawValue - } - - public var utType: UTType { - switch self { - case .png: - return .png - case .pdf: - return .pdf - } - } - } + /** + `ExportType` is a enum that expresses + */ + public enum ExportType: String, Hashable, CaseIterable { + /// `png` output + case png + /// `pdf` output (Work In Progress) + case pdf + + public var fileExtension: String { + self.rawValue + } + + public var utType: UTType { + switch self { + case .png: + return .png + case .pdf: + return .pdf + } + } + } } extension Csv { - - /// Generate `Csv` from `String` data. - /// - /// You cloud call `Csv.loadFromString` if you can own raw-CSV data. - /// - /// ```swift - /// let rawCsv = """ - /// a,b,c - /// 1,2,3 - /// 4,5,6 - /// 7,8,9 - /// 10,11,12 - /// """ - /// let csv = Csv.loadFromString(rawCsv) - /// Output: - /// | a | b | c | - /// | 1 | 2 | 3 | - /// | 4 | 5 | 6 | - /// | 7 | 8 | 9 | - /// | 10 | 11 | 12 | - ///``` - /// - /// You cloud change separator by giving value to `separator` parameter. - /// - ///```swift - /// let dotSeparated = """ - /// a.b.c - /// 1.2.3 - /// 4.5.6 - /// 7.8.9 - /// """ - /// let csv = Csv.loadFromString(dotSeparated, separator: ".") - /// Output: - /// | a | b | c | - /// | 1 | 2 | 3 | - /// | 4 | 5 | 6 | - /// | 7 | 8 | 9 | - /// | 10 | 11 | 12 | - /// ``` - /// - /// If certain row-item is very long, you could trim it with `maxLength`-th length. - /// - ///```swift - /// let longCsv = """ - /// a.b.c - /// 1.2.33333333333333333333333333333333333333333 - /// 4.5.6 - /// 7.8.9 - /// """ - /// let csv = Csv.loadFromString(dotSeparated, separator: ".", maxLength: 7) - /// Output: - /// | a | b | c | - /// | 1 | 2 | 3333333 | - /// | 4 | 5 | 6 | - /// | 7 | 8 | 9 | - /// | 10 | 11 | 12 | - /// ``` - /// - /// - Parameters: - /// - str: Row String - /// - encoding: `String.Encoding?`. specify the encoding style used in generating String data. - /// - separator: Default separator in a row is `","`. You cloud change it by giving separator to `separator` parameter. - /// - maxLength: Default value is nil. if `maxLength` is not nil, every row-item length is limited by `maxLength`. - /// - exportType: Default `exportType` is `.png`. If you use too big image size, I strongly recommend use `.pdf` instead. - public static func loadFromString( - _ str: String, - encoding: String.Encoding = .utf8, - separator: String = ",", - maxLength: Int? = nil, - exportType: ExportType = .png - ) -> Csv { - var lines = str - .components( - separatedBy: CharacterSet( - charactersIn: "\r\n" - ) - ) - .filter({ - !$0.isEmpty - }) - var columns: [Csv.Column] = [] - var rows: [Row] = [] - - if lines.count == 1 { - let count = lines[0] - .split( - separator: Character( - separator - ), - omittingEmptySubsequences: false - ) - .count - let columns = ( - 0.. maxLength { - str = String( - item.prefix( - maxLength - ) - ) + "..." - } else { - str = item - } - return str - } - let row = Row( - index: i, - values: items - ) - rows.append( - row - ) - } - } - return Csv( - separator: separator, - rawString: str, - encoding: encoding, - columns: columns, - rows: rows, - exportType: .pdf - ) - } - - /// Generate `Csv` from network url (like `HTTPS`). - /// - /// - Parameters: - /// - url: Network url, commonly `HTTPS` schema. - /// - separator: Default `separator` in a row is `","`. You cloud change it by giving separator to `separator` parameter. - /// - encoding: Default: `.utf8`. if you get the unexpected result after convert, please try changing this parameter into other encoding style. - /// - exportType: Default `exportType` is `.png`. If you use too big image size, I strongly recommend use `.pdf` instead. - public static func loadFromNetwork( - _ url: URL, - separator: String = ",", - encoding: String.Encoding = .utf8, - exportType: ExportType = .png - ) throws -> Csv { - let data = try Data( - contentsOf: url - ) - let str: String - if let _str = String( - data: data, - encoding: encoding - ) { - str = _str - } else { - throw Error.invalidDownloadResource( - url: url.absoluteString, - data: data - ) - } - return Csv.loadFromString( - str, - encoding: encoding, - separator: separator - ) - } - - /// Generate `Csv` from local disk url (like `file://Users/...`). - /// - /// - Parameters: - /// - file: Local disk url, commonly starts from `file://` schema. Relative-path method is not allowed, please specify by absolute-path method. - /// - separator: Default `separator` in a row is `","`. You cloud change it by giving separator to `separator` parameter. - /// - encoding: Default: `.utf8`. if you get the unexpected result after convert, please try changing this parameter into other encoding style. - /// - exportType: Default `exportType` is `.png`. If you use too big image size, I strongly recommend use `.pdf` instead. - public static func loadFromDisk( - _ file: URL, - separator: String = ",", - encoding: String.Encoding = .utf8, - exportType: ExportType = .png - ) throws -> Csv { - // https://www.hackingwithswift.com/forums/swift/accessing-files-from-the-files-app/8203 - let canAccess = file.startAccessingSecurityScopedResource() - defer { - file.stopAccessingSecurityScopedResource() - } - if canAccess { - let data = try Data( - contentsOf: file - ) - let str: String - if let _str = String( - data: data, - encoding: encoding - ) { - str = _str - } else { - throw Error.invalidLocalResource( - url: file.absoluteString, - data: data, - encoding: encoding - ) - } - return Csv.loadFromString( - str, - encoding: encoding, - separator: separator - ) - } - throw Error.cannotAccessFile( - url: file.absoluteString - ) - } - - /** - Generate Output (file-type is determined by `exportType` parameter) - - Parameters: - - fontSize: Determine the fontsize of characters in output-table image. - - exportType:Determine file-extension. type: ``ExportType``. default value: ``ExportType.png``. If you use too big image size, I recommend use `.pdf` instead of `.png`. - - Note: - `fontSize` determines the size of output image and it can be as large as you want. Please consider the case that output image is too large to open image. Although output image becomes large, it is recommended to set fontSize amply enough (maybe larger than `12pt`) to see image clearly. - - Returns: ``CsvExportable``. (either ``CGImage`` or ``PdfDocument``). - - Throws: Throws ``Csv.Error``. - */ - public func generate( - fontSize: Double? = nil, - exportType: ExportType = .png, - styles: [Csv.Column.Style]? = nil - ) async throws -> AnyCsvExportable { - if isLoading { - throw Csv.Error.workInProgress - } - isLoadingSubject.value = true - progressSubject.value = 0 - defer { - isLoadingSubject.value = false - } - if columns.isEmpty || rows.isEmpty { - throw Csv.Error.emptyData - } - self.exportType = exportType - if let styles { - update( - columnStyles: styles - ) - } - var maker: Any? - switch exportType { - case .png: - maker = self.imageMarker - case .pdf: - maker = self.pdfMarker - } - if let maker = maker as? ImageMaker { - if let fontSize = fontSize { - maker.set( - fontSize: fontSize - ) - } - let exportable: any CsvExportable = try await withCheckedThrowingContinuation { continuation in - queue.async { [weak self] in - guard let self = self else { - continuation.resume( - throwing: Csv.Error.underlying( - nil - ) - ) - return - } - Task { - do { - let img = try maker.make( - columns: await self.columns, - rows: await self.rows - ) { progress in - self.progressSubject.value = progress - } - continuation.resume( - returning: img - ) - } catch { - continuation.resume( - throwing: Csv.Error.underlying( - error - ) - ) - } - } - } - } - return AnyCsvExportable( - exportable - ) - } else if let maker = maker as? PdfMaker { - if let fontSize = fontSize { - maker.set( - fontSize: fontSize - ) - } - let exportable: PDFDocument = try await withCheckedThrowingContinuation { continuation in - queue.async { [weak self] in - guard let self = self else { - continuation.resume( - throwing: Csv.Error.underlying( - nil - ) - ) - return - } - Task { - do { - let doc = try maker.make( - columns: await self.columns, - rows: await self.rows - ) { progress in - self.progressSubject.value = progress - } - continuation.resume( - returning: doc - ) - } catch { - continuation.resume( - throwing: Csv.Error.underlying( - error - ) - ) - } - } - } - } - return AnyCsvExportable( - exportable - ) - } - throw Error.invalidExportType( - exportType - ) - } - - public func generate( - fontSize: Double? = nil, - exportType: ExportType = .png, - style: Csv.Column.Style - ) async throws -> AnyCsvExportable { - try await self.generate( - fontSize: fontSize, - exportType: exportType, - styles: columns.map { - _ in - style - } - ) - } - - /** - - parameters: - - to url: local file path where [png, pdf] image will be saved. - - Returns: If saving csv image to file, returns `true`. Otherwise, return `False`. - */ - public func write( - to url: URL - ) -> Data? { - let data: Data? - if exportType == .png { - data = imageMarker.latestOutput?.convertToData() - } else if exportType == .pdf { - pdfMarker.latestOutput?.write( - to: url - ) - return pdfMarker.latestOutput?.dataRepresentation() - } else { - data = nil - } - guard let data = data else { - return nil - } - do { - if !FileManager.default.fileExists( - atPath: url.absoluteString - ) { - FileManager.default.createFile( - atPath: url.absoluteString, - contents: data - ) - } else { - try data.write( - to: url - ) - } - return data - } catch { - print( - error - ) - return nil - } - } - - /** - - set ``PdfMetadata`` - */ - public func update(pdfMetadata: PDFMetadata) { + + /// Generate `Csv` from `String` data. + /// + /// You cloud call `Csv.loadFromString` if you can own raw-CSV data. + /// + /// ```swift + /// let rawCsv = """ + /// a,b,c + /// 1,2,3 + /// 4,5,6 + /// 7,8,9 + /// 10,11,12 + /// """ + /// let csv = Csv.loadFromString(rawCsv) + /// Output: + /// | a | b | c | + /// | 1 | 2 | 3 | + /// | 4 | 5 | 6 | + /// | 7 | 8 | 9 | + /// | 10 | 11 | 12 | + ///``` + /// + /// You cloud change separator by giving value to `separator` parameter. + /// + ///```swift + /// let dotSeparated = """ + /// a.b.c + /// 1.2.3 + /// 4.5.6 + /// 7.8.9 + /// """ + /// let csv = Csv.loadFromString(dotSeparated, separator: ".") + /// Output: + /// | a | b | c | + /// | 1 | 2 | 3 | + /// | 4 | 5 | 6 | + /// | 7 | 8 | 9 | + /// | 10 | 11 | 12 | + /// ``` + /// + /// If certain row-item is very long, you could trim it with `maxLength`-th length. + /// + ///```swift + /// let longCsv = """ + /// a.b.c + /// 1.2.33333333333333333333333333333333333333333 + /// 4.5.6 + /// 7.8.9 + /// """ + /// let csv = Csv.loadFromString(dotSeparated, separator: ".", maxLength: 7) + /// Output: + /// | a | b | c | + /// | 1 | 2 | 3333333 | + /// | 4 | 5 | 6 | + /// | 7 | 8 | 9 | + /// | 10 | 11 | 12 | + /// ``` + /// + /// - Parameters: + /// - str: Row String + /// - encoding: `String.Encoding?`. specify the encoding style used in generating String data. + /// - separator: Default separator in a row is `","`. You cloud change it by giving separator to `separator` parameter. + /// - maxLength: Default value is nil. if `maxLength` is not nil, every row-item length is limited by `maxLength`. + /// - exportType: Default `exportType` is `.png`. If you use too big image size, I strongly recommend use `.pdf` instead. + public static func loadFromString( + _ str: String, + encoding: String.Encoding = .utf8, + separator: String = ",", + maxLength: Int? = nil, + exportType: ExportType = .png + ) -> Csv { + var lines = str + .components( + separatedBy: CharacterSet( + charactersIn: "\r\n" + ) + ) + .filter({ + !$0.isEmpty + }) + var columns: [Csv.Column] = [] + var rows: [Row] = [] + + if lines.count == 1 { + let count = lines[0] + .split( + separator: Character( + separator + ), + omittingEmptySubsequences: false + ) + .count + let columns = ( + 0.. maxLength { + str = String( + item.prefix( + maxLength + ) + ) + "..." + } else { + str = item + } + return str + } + let row = Row( + index: i, + values: items + ) + rows.append( + row + ) + } + } + return Csv( + separator: separator, + rawString: str, + encoding: encoding, + columns: columns, + rows: rows, + exportType: .pdf + ) + } + + /// Generate `Csv` from network url (like `HTTPS`). + /// + /// - Parameters: + /// - url: Network url, commonly `HTTPS` schema. + /// - separator: Default `separator` in a row is `","`. You cloud change it by giving separator to `separator` parameter. + /// - encoding: Default: `.utf8`. if you get the unexpected result after convert, please try changing this parameter into other encoding style. + /// - exportType: Default `exportType` is `.png`. If you use too big image size, I strongly recommend use `.pdf` instead. + public static func loadFromNetwork( + _ url: URL, + separator: String = ",", + encoding: String.Encoding = .utf8, + exportType: ExportType = .png + ) throws -> Csv { + let data = try Data( + contentsOf: url + ) + let str: String + if let _str = String( + data: data, + encoding: encoding + ) { + str = _str + } else { + throw Error.invalidDownloadResource( + url: url.absoluteString, + data: data + ) + } + return Csv.loadFromString( + str, + encoding: encoding, + separator: separator + ) + } + + /// Generate `Csv` from local disk url (like `file://Users/...`). + /// + /// - Parameters: + /// - file: Local disk url, commonly starts from `file://` schema. Relative-path method is not allowed, please specify by absolute-path method. + /// - separator: Default `separator` in a row is `","`. You cloud change it by giving separator to `separator` parameter. + /// - encoding: Default: `.utf8`. if you get the unexpected result after convert, please try changing this parameter into other encoding style. + /// - exportType: Default `exportType` is `.png`. If you use too big image size, I strongly recommend use `.pdf` instead. + public static func loadFromDisk( + _ file: URL, + separator: String = ",", + encoding: String.Encoding = .utf8, + exportType: ExportType = .png + ) throws -> Csv { + // https://www.hackingwithswift.com/forums/swift/accessing-files-from-the-files-app/8203 + let canAccess = file.startAccessingSecurityScopedResource() + defer { + file.stopAccessingSecurityScopedResource() + } + if canAccess { + let data = try Data( + contentsOf: file + ) + let str: String + if let _str = String( + data: data, + encoding: encoding + ) { + str = _str + } else { + throw Error.invalidLocalResource( + url: file.absoluteString, + data: data, + encoding: encoding + ) + } + return Csv.loadFromString( + str, + encoding: encoding, + separator: separator + ) + } + throw Error.cannotAccessFile( + url: file.absoluteString + ) + } + + /** + Generate Output (file-type is determined by `exportType` parameter) + - Parameters: + - fontSize: Determine the fontsize of characters in output-table image. + - exportType:Determine file-extension. type: ``ExportType``. default value: ``ExportType.png``. If you use too big image size, I recommend use `.pdf` instead of `.png`. + - Note: + `fontSize` determines the size of output image and it can be as large as you want. Please consider the case that output image is too large to open image. Although output image becomes large, it is recommended to set fontSize amply enough (maybe larger than `12pt`) to see image clearly. + - Returns: ``CsvExportable``. (either ``CGImage`` or ``PdfDocument``). + - Throws: Throws ``Csv.Error``. + */ + public func generate( + fontSize: Double? = nil, + exportType: ExportType = .png, + styles: [Csv.Column.Style]? = nil + ) async throws -> AnyCsvExportable { + if isLoading { + throw Csv.Error.workInProgress + } + isLoadingSubject.value = true + progressSubject.value = 0 + defer { + isLoadingSubject.value = false + } + if columns.isEmpty || rows.isEmpty { + throw Csv.Error.emptyData + } + self.exportType = exportType + if let styles { + update( + columnStyles: styles + ) + } + var maker: Any? + switch exportType { + case .png: + maker = self.imageMarker + case .pdf: + maker = self.pdfMarker + } + if let maker = maker as? ImageMaker { + if let fontSize = fontSize { + maker.set( + fontSize: fontSize + ) + } + let exportable: any CsvExportable = try await withCheckedThrowingContinuation { continuation in + queue.async { [weak self] in + guard let self = self else { + continuation.resume( + throwing: Csv.Error.underlying( + nil + ) + ) + return + } + Task { + do { + let img = try maker.make( + columns: await self.columns, + rows: await self.rows + ) { progress in + self.progressSubject.value = progress + } + continuation.resume( + returning: img + ) + } catch { + continuation.resume( + throwing: Csv.Error.underlying( + error + ) + ) + } + } + } + } + return AnyCsvExportable( + exportable + ) + } else if let maker = maker as? PdfMaker { + if let fontSize = fontSize { + maker.set( + fontSize: fontSize + ) + } + let exportable: PDFDocument = try await withCheckedThrowingContinuation { continuation in + queue.async { [weak self] in + guard let self = self else { + continuation.resume( + throwing: Csv.Error.underlying( + nil + ) + ) + return + } + Task { + do { + let doc: PDFDocument + if let pdfSize = maker.metadata.size, let orientation = maker.metadata.orientation { + doc = try maker.make( + with: pdfSize, + orientation: orientation, + columns: await self.columns, + rows: await self.rows + ) { progress in + self.progressSubject.value = progress + } + } else { + doc = try maker.make( + columns: await self.columns, + rows: await self.rows + ) { progress in + self.progressSubject.value = progress + } + } + continuation.resume( + returning: doc + ) + } catch { + continuation.resume( + throwing: Csv.Error.underlying( + error + ) + ) + } + } + } + } + return AnyCsvExportable( + exportable + ) + } + throw Error.invalidExportType( + exportType + ) + } + + public func generate( + fontSize: Double? = nil, + exportType: ExportType = .png, + style: Csv.Column.Style + ) async throws -> AnyCsvExportable { + try await self.generate( + fontSize: fontSize, + exportType: exportType, + styles: columns.map { + _ in + style + } + ) + } + + /** + - parameters: + - to url: local file path where [png, pdf] image will be saved. + - Returns: If saving csv image to file, returns `true`. Otherwise, return `False`. + */ + public func write( + to url: URL + ) -> Data? { + let data: Data? + if exportType == .png { + data = imageMarker.latestOutput?.convertToData() + } else if exportType == .pdf { + pdfMarker.latestOutput?.write( + to: url + ) + return pdfMarker.latestOutput?.dataRepresentation() + } else { + data = nil + } + guard let data = data else { + return nil + } + do { + if !FileManager.default.fileExists( + atPath: url.absoluteString + ) { + FileManager.default.createFile( + atPath: url.absoluteString, + contents: data + ) + } else { + try data.write( + to: url + ) + } + return data + } catch { + print( + error + ) + return nil + } + } + + /** + - set ``PdfMetadata`` + */ + public func update( + pdfMetadata: PDFMetadata + ) { self.pdfMetadata = pdfMetadata } } diff --git a/Sources/Csv2Img/PdfMaker.swift b/Sources/Csv2Img/PdfMaker.swift index 1080077..d5532db 100644 --- a/Sources/Csv2Img/PdfMaker.swift +++ b/Sources/Csv2Img/PdfMaker.swift @@ -134,7 +134,7 @@ final class PdfMaker: PdfMakerType { let rowHeight = longestHeight + verticalSpace let columnWidth = longestWidth + horizontalSpace - let lineWidth: Double = 1 + let lineWidth: Double = fontSize / 10 let width = ( longestWidth + horizontalSpace @@ -343,10 +343,14 @@ final class PdfMaker: PdfMakerType { Double ) -> Void ) throws -> PDFDocument { + // NOTE: Anchor is bottom-left. + let horizontalSpace: Double = 8 + let verticalSpace: Double = 12 + let pageSize = pdfSize.size( orientation: orientation ) - + let totalRowCount = min( maximumRowCount ?? rows.count, rows.count @@ -359,17 +363,19 @@ final class PdfMaker: PdfMakerType { throw PdfMakingError.emptyRows } - let maxTextCount = rows.flatMap { $0.values }.map(\.count).max() ?? 0 - - set(fontSize: pageSize.width / Double(columns.count) / Double(maxTextCount) * 0.8) - let styles: [Csv.Column.Style] = columns.map( \.style ) - let rowHeight = pageSize.height / Double(rows.count + 1) - let columnWidth = pageSize.width / Double(columns.count) - let lineWidth: Double = 1 + let rowHeight = (rows[0].values.map { $0.getSize(fontSize: fontSize).height }.max() ?? 0) + verticalSpace + + let tableSize = CGSize( + width: pageSize.width * 0.8, height: rowHeight * Double(rows.count + 1) + ) + + let columnWidth = tableSize.width / Double(columns.count) + horizontalSpace + + let lineWidth: Double = fontSize / 10 let totalPageNumber = 1 @@ -462,11 +468,14 @@ final class PdfMaker: PdfMakerType { boxHeight: Double( rowHeight ), + xOffSet: (pageSize.width - tableSize.width) / 2, + // anchor is bottom-left. + yOffSet: pageSize.height - tableSize.height - min(24, (pageSize.height - tableSize.height) / 2), totalHeight: Double( - pageSize.height + tableSize.height ), totalWidth: Double( - pageSize.width + tableSize.width ) ) setRowText( @@ -484,11 +493,14 @@ final class PdfMaker: PdfMakerType { height: Double( rowHeight ), + xOffSet: (pageSize.width - tableSize.width) / 2, + // anchor is bottom-left. + yOffSet: pageSize.height - tableSize.height - min(24, (pageSize.height - tableSize.height) / 2), totalWidth: Double( - pageSize.width + tableSize.width ), totalHeight: Double( - pageSize.height + tableSize.height ) ) @@ -524,6 +536,7 @@ final class PdfMaker: PdfMakerType { metadata: PDFMetadata ) { self.metadata = metadata + print(metadata) } } @@ -537,6 +550,8 @@ extension PdfMaker { columnHeight: Double, width: Double, height: Double, + xOffSet: Double = 0, + yOffSet: Double = 0, totalWidth: Double, totalHeight: Double ) { @@ -547,16 +562,16 @@ extension PdfMaker { let row = rows[i] context.move( to: CGPoint( - x: 0, - y: totalHeight - Double( + x: xOffSet, + y: yOffSet + totalHeight - Double( i + 1 ) * height - columnHeight ) ) context.addLine( to: CGPoint( - x: totalWidth, - y: totalHeight - Double( + x: xOffSet + totalWidth, + y: yOffSet + totalHeight - Double( i + 1 ) * height - columnHeight ) @@ -584,13 +599,13 @@ extension PdfMaker { let leadingSpaceInBox = ( width - size.width ) / 2 - let originX = Double( + let originX = xOffSet + Double( j ) * width + leadingSpaceInBox let topSpaceInBox = ( height - size.height ) / 2 - let originY = totalHeight - ( + let originY = yOffSet + totalHeight - ( Double( i + 1 ) * height + size.height + topSpaceInBox @@ -633,21 +648,52 @@ extension PdfMaker { columns: [Csv.Column], boxWidth width: Double, boxHeight height: Double, + xOffSet: Double = 0, + yOffSet: Double = 0, totalHeight: Double, totalWidth: Double ) { + // Draw top `-`. context.move( to: CGPoint( - x: 0, - y: totalHeight - height + x: xOffSet, + y: yOffSet + totalHeight ) ) context.addLine( to: CGPoint( - x: totalWidth, - y: totalHeight - height + x: totalWidth + xOffSet, + y: yOffSet + totalHeight ) ) + + // Draw top-column `-`. + context.move( + to: CGPoint( + x: xOffSet, + y: yOffSet + totalHeight - height + ) + ) + context.addLine( + to: CGPoint( + x: totalWidth + xOffSet, + y: yOffSet + totalHeight - height + ) + ) + + // Draw right `|`. + context.move( + to: CGPoint( + x: xOffSet + totalWidth, + y: yOffSet + ) + ) + context.addLine( + to: CGPoint( + x: xOffSet + totalWidth, + y: yOffSet + totalHeight + ) + ) for ( i, column @@ -657,14 +703,14 @@ extension PdfMaker { ) context.move( to: CGPoint( - x: i * width, - y: 0 + x: xOffSet + i * width, + y: yOffSet ) ) context.addLine( to: CGPoint( - x: i * width, - y: totalHeight + x: xOffSet + i * width, + y: yOffSet + totalHeight ) ) let str = NSAttributedString( @@ -680,10 +726,10 @@ extension PdfMaker { let size = str.string.getSize( fontSize: fontSize ) - let originX = i * width + ( + let originX = xOffSet + i * width + ( width - size.width ) / 2 - let originY = totalHeight - ( + let originY = yOffSet + totalHeight - ( height + size.height ) / 2 let framesetter = CTFramesetterCreateWithAttributedString( diff --git a/Sources/Csv2ImgCmd/command.swift b/Sources/Csv2ImgCmd/command.swift index 9ba1a19..b4191da 100644 --- a/Sources/Csv2ImgCmd/command.swift +++ b/Sources/Csv2ImgCmd/command.swift @@ -124,6 +124,7 @@ public struct Csv2Img: AsyncParsableCommand { url ) } + await csv.update(pdfMetadata: .init(size: .b3, orientation: .landscape)) let exportable = try await csv.generate( fontSize: 12, exportType: exportType,