Skip to content

Commit

Permalink
fix: coordinate gap / text rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
fummicc1 committed Oct 19, 2024
1 parent 8d8ea90 commit 478276d
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 98 deletions.
Binary file modified Fixtures/outputs/category.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
177 changes: 99 additions & 78 deletions Sources/Csv2ImgCore/ImageMaker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,45 @@ final class ImageMaker: ImageMakerType {
self.fontSize = size
}

private func createContext(
width: Int,
height: Int
) throws -> CGContext {

#if os(macOS)
let canvas = NSImage(
size: NSSize(
width: width,
height: height
)
)
canvas.lockFocus()
guard let context = NSGraphicsContext.current?.cgContext else {
throw ImageMakingError.noContextAvailable
}
#elseif os(iOS)
UIGraphicsBeginImageContext(
CGSize(
width: width,
height: height
)
)
guard let context = UIGraphicsGetCurrentContext() else {
throw ImageMakingError.noContextAvailable
}
#endif

defer {
#if os(macOS)
canvas.unlockFocus()
#elseif os(iOS)
UIGraphicsEndImageContext()
#endif
}

return context
}

/// generate png-image data from ``Csv``.
internal func make(
columns: [Csv.Column],
Expand All @@ -66,22 +105,15 @@ final class ImageMaker: ImageMakerType {
) -> Void
) throws -> CGImage {
let representation = try build(columns: columns, rows: rows, progress: progress)
guard
let context = CGContext(
data: nil,
width: representation.width,
height: representation.height,
bitsPerComponent: 8,
bytesPerRow: 0,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
)
else {
throw ImageMakingError.noContextAvailable
}
guard let image = ImageRenderer().render(context: context, representation) else {
let context = try createContext(width: representation.width, height: representation.height)

let renderer = ImageRenderer()
let image = renderer.render(context: context, representation)
guard let image = image else {
throw ImageMakingError.failedCreateImage(context)
}

self.latestOutput = image
return image
}

Expand All @@ -103,100 +135,55 @@ final class ImageMaker: ImageMakerType {

let horizontalSpace = 8
let verticalSpace = 12
let textSizeList =
rows
.flatMap({
$0.values
})
.map({
$0.getSize(
fontSize: fontSize
)
})
+ columns
.map({
$0.name
})
.map({
$0.getSize(
fontSize: fontSize
)
})

let longestHeight = textSizeList.map({
$0.height
}).sorted().reversed()[0]
let longestWidth = textSizeList.map({
$0.width
}).sorted().reversed()[0]
let width =
(Int(
longestWidth
) + horizontalSpace) * columns.count
let height =
(rows.count + 1)
* (Int(
longestHeight
) + verticalSpace)

let backgroundColor = CGColor(red: 250 / 255, green: 250 / 255, blue: 250 / 255, alpha: 1)

let columnWidth = width / columns.count
let rowHeight = height / (rows.count + 1)
let columnWidths = calculateColumnWidths(columns: columns, rows: rows)
let width = columnWidths.reduce(0, +) + (columns.count + 1) * horizontalSpace

let rowHeights = calculateRowHeights(
columns: columns, rows: rows, columnWidths: columnWidths)
let height = rowHeights.reduce(0, +) + (rows.count + 2) * verticalSpace

var columnRepresentations: [CsvImageRepresentation.ColumnRepresentation] = []
var rowRepresentations: [CsvImageRepresentation.RowRepresentation] = []

let completeCount: Double = Double(rows.count + columns.count)
var completeFraction: Double = 0

var yOffset = verticalSpace
// ヘッダー行の描画
for (i, column) in columns.enumerated() {
let size = column.name.getSize(fontSize: fontSize)
let originX = i * columnWidth + columnWidth / 2 - Int(size.width) / 2
let originY = height - Int(size.height) / 2 - rowHeight / 2

let xOffset = columnWidths[0..<i].reduce(0, +) + (i + 1) * horizontalSpace
let frame = CGRect(
x: xOffset, y: yOffset, width: columnWidths[i], height: rowHeights[0])
columnRepresentations.append(
CsvImageRepresentation.ColumnRepresentation(
name: column.name,
style: column.style,
frame: CGRect(
x: originX,
y: originY,
width: columnWidth,
height: rowHeight
)
frame: frame
)
)

completeFraction += 1
progress(completeFraction / completeCount)
}
yOffset += rowHeights[0] + verticalSpace

// データ行の描画
for (i, row) in rows.enumerated() {
var rowFrames: [CGRect] = []
for (j, item) in row.values.enumerated() {
if columns.count <= j { continue }

let size = item.getSize(fontSize: fontSize)
let originX = j * columnWidth + columnWidth / 2 - Int(size.width) / 2
let originY = height - (i + 2) * rowHeight + Int(size.height) / 2

rowFrames.append(
CGRect(
x: originX,
y: originY,
width: columnWidth,
height: rowHeight
)
)
let xOffset = columnWidths[0..<j].reduce(0, +) + (j + 1) * horizontalSpace
let frame = CGRect(
x: xOffset, y: yOffset, width: columnWidths[j], height: rowHeights[i + 1])
rowFrames.append(frame)
}

rowRepresentations.append(
CsvImageRepresentation.RowRepresentation(
values: row.values,
frames: rowFrames
)
)
yOffset += rowHeights[i + 1] + verticalSpace

completeFraction += 1
progress(completeFraction / completeCount)
Expand All @@ -211,4 +198,38 @@ final class ImageMaker: ImageMakerType {
rows: rowRepresentations
)
}

private func calculateColumnWidths(columns: [Csv.Column], rows: [Csv.Row]) -> [Int] {
return columns.enumerated().map { (index, column) in
let headerWidth = column.name.getSize(fontSize: fontSize).width
let maxContentWidth =
rows.map { row in
row.values.count > index
? row.values[index].getSize(fontSize: fontSize).width : 0
}.max() ?? 0
return Int(max(headerWidth, maxContentWidth)) + 20 // 20はパディング
}
}

private func calculateRowHeights(columns: [Csv.Column], rows: [Csv.Row], columnWidths: [Int])
-> [Int]
{
let headerHeight =
Int(columns.map { $0.name.getSize(fontSize: fontSize).height }.max() ?? 0) + 10

let contentHeights = rows.map { row in
let maxHeight =
row.values.enumerated().map { (index, value) in
if index < columnWidths.count {
let size = value.getSize(fontSize: fontSize)
let lines = ceil(size.width / CGFloat(columnWidths[index]))
return size.height * lines
}
return 0
}.max() ?? 0
return Int(maxHeight) + 10 // 10はパディング
}

return [headerHeight] + contentHeights
}
}
46 changes: 37 additions & 9 deletions Sources/Csv2ImgCore/ImageRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import CoreGraphics
import CoreText
import Foundation

#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif

public class ImageRenderer {
public func render(context: CGContext, _ representation: CsvImageRepresentation) -> CGImage? {
let width = representation.width
Expand All @@ -10,9 +16,16 @@ public class ImageRenderer {
context.setFillColor(representation.backgroundColor)
context.fill(CGRect(x: 0, y: 0, width: width, height: height))

// Draw grid lines
context.setLineWidth(1)
context.setStrokeColor(CGColor(gray: 0.8, alpha: 1))
#if os(macOS)
context.setStrokeColor(
Color.separatorColor.cgColor
)
#elseif os(iOS)
context.setStrokeColor(
Color.separator.cgColor
)
#endif

for column in representation.columns {
context.move(to: CGPoint(x: column.frame.minX, y: 0))
Expand All @@ -31,14 +44,17 @@ public class ImageRenderer {

context.strokePath()

// Draw columns
#if os(macOS)
context.translateBy(x: 0, y: CGFloat(height))
context.scaleBy(x: 1.0, y: -1.0)
#endif

for column in representation.columns {
drawText(
context: context, text: column.name, frame: column.frame, style: column.style,
fontSize: representation.fontSize)
}

// Draw rows
for row in representation.rows {
for (index, (value, frame)) in zip(row.values, row.frames).enumerated() {
let column = representation.columns[index]
Expand All @@ -60,9 +76,10 @@ public class ImageRenderer {
]

let attributedString = NSAttributedString(string: text, attributes: attributes)
let textSize = (text as NSString).size(withAttributes: attributes)

let rect = CGRect(
let textSize = attributedString.size()

let textRect = CGRect(
x: frame.origin.x + (frame.width - textSize.width) / 2,
y: frame.origin.y + (frame.height - textSize.height) / 2,
width: textSize.width,
Expand All @@ -71,12 +88,23 @@ public class ImageRenderer {

context.saveGState()

// テキストの描画位置を設定
let textPath = CGPath(rect: textRect, transform: nil)
context.addPath(textPath)
context.clip()

// テキストを描画
#if os(macOS)
context.translateBy(x: 0, y: CGFloat(context.height))
context.scaleBy(x: 1.0, y: -1.0)
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = NSGraphicsContext(cgContext: context, flipped: true)
attributedString.draw(in: textRect)
NSGraphicsContext.restoreGraphicsState()
#else
UIGraphicsPushContext(context)
attributedString.draw(in: textRect)
UIGraphicsPopContext()
#endif

attributedString.draw(in: rect)
context.restoreGState()
}
}
Loading

0 comments on commit 478276d

Please sign in to comment.