Let's imagine that I have some text: "Some attributed text in trapezoid."
I have NSAttributedString extension, which returns me UIImage with attributed text:
extension NSAttributedString {
func asImage() -> UIImage? {
defer {
UIGraphicsEndImageContext()
}
let size = boundingRect(with: CGSize.zero, options: [.usesLineFragmentOrigin, .truncatesLastVisibleLine], context: nil).size
UIGraphicsBeginImageContext(size)
draw(at: CGPoint.zero)
return UIGraphicsGetImageFromCurrentImageContext()
}
}
But this function returns me text in one line, because of using boundingRect
:
------------------------------------
|Some attributed text in trapezoid.|
------------------------------------
If I would use custom rect for drawing text it won't help much...
UIGraphicsBeginImageContext(CGRect(x: 0, y: 0, width: 100, height: 30))
draw(at: CGPoint.zero)
...because of text will be in rectangle:
--------------
|Some attribu|
|ted text in |
|trapezoid. |
--------------
What i need, is to draw text in a trapezoid with known corner positions (or in a circle with known radius). So each new line of text should start with a little offset, see example:
So I want to see something like that:
---------------
\Some attribut/
\ed text in /
\trapezoid/
---------
How can I achieve this result?
You'll have to drop down to CoreText levels here. The good new is, you will be able to draw text in just about any shape you wish!
extension NSAttributedString {
public func draw(in path: CGPath) {
let context = UIGraphicsGetCurrentContext()!
let transform = CGAffineTransform(scaleX: +1, y: -1)
let flippedPath = CGMutablePath()
flippedPath.addPath(path, transform: transform)
let range = CFRange(location: 0, length: 0)
let framesetter = CTFramesetterCreateWithAttributedString(self)
let frame = CTFramesetterCreateFrame(framesetter, range, flippedPath, nil)
context.saveGState()
// Debug: fill path.
context.setFillColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 0.5)
context.beginPath()
context.addPath(path)
context.fillPath()
context.concatenate(transform)
CTFrameDraw(frame, context)
context.restoreGState()
}
}
And you can use it like so:
let string = NSAttributedString(string: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit.")
let bounds = CGRect(x: 0, y: 0, width: 120, height: 120)
let path = CGMutablePath()
path.move(to: CGPoint(x: 10, y: 10))
path.addLine(to: CGPoint(x: 110, y: 10))
path.addLine(to: CGPoint(x: 90, y: 110))
path.addLine(to: CGPoint(x: 30, y: 110))
path.closeSubpath()
UIGraphicsBeginImageContextWithOptions(bounds.integral.size, true, 0)
defer { UIGraphicsEndImageContext() }
let context = UIGraphicsGetCurrentContext()!
context.setFillColor(UIColor.white.cgColor)
context.fill(.infinite)
string.draw(in: path)
let image = UIGraphicsGetImageFromCurrentImageContext()!
The bad news is, this solution does not give you an ellipsis at the end. If you really want to have that, you may need to make some adjustments to the last line the framesetter gives you.
With Christian Schnorr answer made it for my purposes:
import PlaygroundSupport
import UIKit
extension NSAttributedString {
/// Draws attributed string in selected CGPath.
func draw(in path: CGPath) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let transform = CGAffineTransform(scaleX: 1.0, y: -1.0)
let flippedPath = CGMutablePath()
flippedPath.addPath(path, transform: transform)
let range = CFRange(location: 0, length: 0)
let framesetter = CTFramesetterCreateWithAttributedString(self)
let frame = CTFramesetterCreateFrame(framesetter, range, flippedPath, nil)
context.saveGState()
context.concatenate(transform)
CTFrameDraw(frame, context)
context.restoreGState()
}
/// Renders attributed string.
///
/// - Parameters:
/// - size: A 'CGSize' for rendering string in trapezoid.
/// - degree: A `CGFloat`, representing trapezoid angles in degrees.
/// - Returns: An optional `UIImage` with rendered string.
func asTrapezoidImage(size: CGSize, degree: CGFloat) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(size, false, 0)
defer { UIGraphicsEndImageContext() }
draw(in: size.trapezoidPath(degree))
return UIGraphicsGetImageFromCurrentImageContext()
}
}
extension CGSize {
/// Converts CGSize into trapezoid CGPath.
///
/// - Parameter degree: A `CGFloat`, representing trapezoid angles in degrees.
/// - Returns: A `CGPath` with trapezoid.
func trapezoidPath(_ degree: CGFloat) -> CGPath {
var offset = height * tan(CGFloat.pi * degree / 180.0)
offset = max(0, min(width / 2.0, offset))
let path = CGMutablePath()
path.move(to: CGPoint.zero)
path.addLine(to: CGPoint(x: width, y: 0.0))
path.addLine(to: CGPoint(x: width - offset, y: height))
path.addLine(to: CGPoint(x: offset, y: height))
path.closeSubpath()
return path
}
}
Using:
let string = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit."
let attributes: [NSAttributedStringKey: Any] = [
.foregroundColor: UIColor.blue,
.backgroundColor: UIColor.white,
.font: UIFont.systemFont(ofSize: 24.0)
]
let attrString = NSAttributedString(string: string, attributes: attributes)
let size = CGSize(width: 400.0, height: 120.0)
let image = attrString.asTrapezoidImage(size: size, degree: 12.0)
let imageView = UIImageView(image: image)
imageView.frame = CGRect(origin: CGPoint.zero, size: size)
PlaygroundPage.current.needsIndefiniteExecution = false
PlaygroundPage.current.liveView = imageView
Note Degrees allowed from 0° to 90°
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With