├── .editorconfig ├── .gitignore ├── Package.swift ├── Sources └── CoreTextSwift │ ├── NSRange+Helpers.swift │ ├── CTStringAttribute.swift │ ├── CTGlyphInfo+Swift.swift │ ├── CFAttributedString+CoreText.swift │ ├── CTFramesetter+Swift.swift │ ├── Array+CGGlyph.swift │ ├── CTFrame+Swift.swift │ ├── CTFont+Swift.swift │ ├── FontManager+Swift.swift │ ├── CTLine+Swift.swift │ └── CTRun+Swift.swift ├── README.md └── LICENSE.md /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.swift] 4 | indent_style = space 5 | indent_size = 2 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | .swiftpm 7 | DerivedData -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.5 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "CoreTextSwift", 7 | platforms: [.macOS(.v10_15), .iOS(.v13)], 8 | products: [ 9 | .library(name: "CoreTextSwift", targets: ["CoreTextSwift"]) 10 | ], 11 | targets: [ 12 | .target(name: "CoreTextSwift") 13 | ] 14 | ) 15 | -------------------------------------------------------------------------------- /Sources/CoreTextSwift/NSRange+Helpers.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension NSRange { 4 | init(_ range: CFRange) { 5 | self = NSMakeRange(range.location == kCFNotFound ? NSNotFound : range.location, range.length) 6 | } 7 | } 8 | 9 | extension CFRange { 10 | init(_ range: NSRange) { 11 | self = CFRangeMake(range.location, range.length) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/CoreTextSwift/CTStringAttribute.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CoreText 3 | 4 | public extension NSAttributedString.Key { 5 | static let runDelegate = NSAttributedString.Key(kCTRunDelegateAttributeName as String) 6 | static let tracking = NSAttributedString.Key(kCTTrackingAttributeName as String) 7 | static let foregroundColorFromContext = NSAttributedString.Key(kCTForegroundColorFromContextAttributeName as String) 8 | } 9 | -------------------------------------------------------------------------------- /Sources/CoreTextSwift/CTGlyphInfo+Swift.swift: -------------------------------------------------------------------------------- 1 | import CoreText 2 | 3 | extension CTGlyphInfo { 4 | 5 | /// Gets the glyph name for a glyph info object if that object exists. 6 | public var glyphName: String? { 7 | CTGlyphInfoGetGlyphName(self) as String? 8 | } 9 | 10 | /// Gets the character identifier for a glyph info object. 11 | public var characterIdentifier: CGFontIndex { 12 | CTGlyphInfoGetCharacterIdentifier(self) 13 | } 14 | 15 | /// Gets the character collection for a glyph info object. 16 | public var characterCollection: CTCharacterCollection { 17 | CTGlyphInfoGetCharacterCollection(self) 18 | } 19 | 20 | public var glyph: CGGlyph { 21 | CTGlyphInfoGetGlyph(self) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Sources/CoreTextSwift/CFAttributedString+CoreText.swift: -------------------------------------------------------------------------------- 1 | import CoreText 2 | 3 | public extension CFAttributedString { 4 | 5 | /// Creates a single immutable line object directly from an attributed string. 6 | func line() -> CTLine { 7 | CTLineCreateWithAttributedString(self) 8 | } 9 | 10 | /// Creates an immutable framesetter object from an attributed string. 11 | func framesetter() -> CTFramesetter { 12 | CTFramesetterCreateWithAttributedString(self) 13 | } 14 | 15 | } 16 | 17 | 18 | #if canImport(UIKit) 19 | import UIKit 20 | #endif 21 | 22 | #if canImport(AppKit) 23 | import AppKit 24 | #endif 25 | 26 | #if canImport(UIKit) || canImport(AppKit) 27 | public extension NSAttributedString { 28 | func line() -> CTLine { 29 | (self as CFAttributedString).line() 30 | } 31 | 32 | func framesetter() -> CTFramesetter { 33 | (self as CFAttributedString).framesetter() 34 | } 35 | } 36 | #endif 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CoreTextSwift 2 | 3 | Swifty CoreText API. 4 | 5 | CoreText is C API. This library is a set of wrappers and extensions that makes it convenient to work with Swift. 6 | 7 | ## Example 8 | 9 | Draw line in currect graphics context 10 | 11 | ```swift 12 | guard let ctx = UIGraphicsGetCurrentContext() else { 13 | return 14 | } 15 | 16 | let attributedString = NSAttributedString(string: "abcdefgh") 17 | ctx.draw(attributedString.line()) 18 | ``` 19 | 20 | Use Glyph Run 21 | 22 | ```swift 23 | let attributedString = NSAttributedString(string: "abcdefgh") 24 | 25 | for run in attributedString.line().glyphRuns() { 26 | let font = run.font 27 | for glyph in run.glyphs() { 28 | let glyphPath = font.path(for: glyph) 29 | } 30 | } 31 | ``` 32 | 33 | Draw Glyph Run to CGContext 34 | 35 | ```swift 36 | guard let ctx = UIGraphicsGetCurrentContext() else { 37 | return 38 | } 39 | 40 | for run in attributedString.line().glyphRuns() { 41 | run.draw(in: ctx) 42 | } 43 | ``` 44 | -------------------------------------------------------------------------------- /Sources/CoreTextSwift/CTFramesetter+Swift.swift: -------------------------------------------------------------------------------- 1 | import CoreText 2 | import Foundation 3 | 4 | extension CTFramesetter { 5 | 6 | /// Creates an immutable frame using a framesetter. 7 | /// - Parameter rect: Rect of the frame 8 | /// - Parameter range: The range, of the attributed string that was used to create the framesetter, that is to be typeset in lines fitted into the frame. 9 | /// - Returns: A reference to a new CTFrame object. 10 | public func createFrame(_ rect: CGRect, stringRange: NSRange = NSRange()) -> CTFrame { 11 | CTFramesetterCreateFrame(self, CFRange(stringRange), CGPath(rect: rect, transform: nil), nil) 12 | } 13 | 14 | public func createFrame(_ path: CGPath, stringRange: NSRange = NSRange()) -> CTFrame { 15 | CTFramesetterCreateFrame(self, CFRange(stringRange), path, nil) 16 | } 17 | 18 | public func frameSize(suggested constraints: CGSize, stringRange: NSRange = NSRange()) -> CGSize { 19 | CTFramesetterSuggestFrameSizeWithConstraints(self, CFRange(stringRange), nil, constraints, nil) 20 | } 21 | 22 | } 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020-2023 Marcin Krzyzanowski. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Sources/CoreTextSwift/Array+CGGlyph.swift: -------------------------------------------------------------------------------- 1 | import CoreText 2 | 3 | extension Array where Element == CGGlyph { 4 | func advances(for font: CTFont, orientation: CTFontOrientation = .default) -> [CGSize] { 5 | [CGSize](unsafeUninitializedCapacity: count) { (bufferPointer, count) in 6 | CTFontGetAdvancesForGlyphs(font, orientation, self, bufferPointer.baseAddress!, self.count) 7 | count = self.count 8 | } 9 | } 10 | 11 | func boundingRects(for font: CTFont, orientation: CTFontOrientation = .default) -> [CGRect] { 12 | [CGRect](unsafeUninitializedCapacity: count) { (bufferPointer, count) in 13 | CTFontGetBoundingRectsForGlyphs(font, orientation, self, bufferPointer.baseAddress!, self.count) 14 | count = self.count 15 | } 16 | } 17 | 18 | func opticalBounds(for font: CTFont) -> [CGRect] { 19 | [CGRect](unsafeUninitializedCapacity: count) { (bufferPointer, count) in 20 | CTFontGetOpticalBoundsForGlyphs(font, self, bufferPointer.baseAddress!, self.count, CFOptionFlags()) 21 | count = self.count 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/CoreTextSwift/CTFrame+Swift.swift: -------------------------------------------------------------------------------- 1 | import CoreText 2 | import Foundation 3 | 4 | extension CTFrame { 5 | /// Returns an array of lines stored in the frame. 6 | public func lines() -> [CTLine] { 7 | CTFrameGetLines(self) as? [CTLine] ?? [] 8 | } 9 | 10 | /// Copies a range of line origins for a frame. 11 | /// - Parameter range: The range of line origins. 12 | /// - Returns: Each CGPoint in this array is the origin of the corresponding line in the array of lines returned by lines() 13 | /// relative to the origin of the path's bounding box, which can be obtained from CGPath.boundingBoxOfPath. 14 | public func lineOrigins(range: NSRange = NSRange()) -> [CGPoint] { 15 | let linesCount = lines().count 16 | return [CGPoint](unsafeUninitializedCapacity: linesCount) { (bufferPointer, count) in 17 | CTFrameGetLineOrigins(self, CFRange(range), bufferPointer.baseAddress!) 18 | count = linesCount 19 | } 20 | } 21 | 22 | /// Returns the path used to create the frame. 23 | public func path() -> CGPath { 24 | CTFrameGetPath(self) 25 | } 26 | 27 | /// Returns the range of characters that actually fit in the frame. 28 | public func visibleStringRange() -> NSRange { 29 | NSRange(CTFrameGetVisibleStringRange(self)) 30 | } 31 | 32 | /// Returns the range of characters originally requested to fill the frame. 33 | public func stringRange() -> NSRange { 34 | NSRange(CTFrameGetStringRange(self)) 35 | } 36 | } 37 | 38 | #if canImport(CoreGraphics) 39 | extension CTFrame { 40 | public func draw(in context: CGContext) { 41 | CTFrameDraw(self, context) 42 | } 43 | } 44 | 45 | extension CGContext { 46 | public func draw(_ frame: CTFrame) { 47 | frame.draw(in: self) 48 | } 49 | } 50 | #endif 51 | 52 | -------------------------------------------------------------------------------- /Sources/CoreTextSwift/CTFont+Swift.swift: -------------------------------------------------------------------------------- 1 | import CoreText 2 | import Foundation 3 | 4 | extension CTFont { 5 | 6 | public func size() -> CGFloat { 7 | CTFontGetSize(self) 8 | } 9 | 10 | public func slantAngle() -> CGFloat { 11 | CTFontGetSlantAngle(self) 12 | } 13 | 14 | public func xHeight() -> CGFloat { 15 | CTFontGetXHeight(self) 16 | } 17 | 18 | public func capHeight() -> CGFloat { 19 | CTFontGetCapHeight(self) 20 | } 21 | 22 | public func leading() -> CGFloat { 23 | CTFontGetLeading(self) 24 | } 25 | 26 | public func descent() -> CGFloat { 27 | CTFontGetDescent(self) 28 | } 29 | 30 | public func ascent() -> CGFloat { 31 | CTFontGetAscent(self) 32 | } 33 | 34 | /// Returns the number of glyphs of the given font. 35 | public var glyphCount: CFIndex { 36 | CTFontGetGlyphCount(self) 37 | } 38 | 39 | /// Calculates the advances for an array of glyphs and returns the summed advance. 40 | public func advances(of glyphs: [CGGlyph], orientation: CTFontOrientation = .default) -> [CGSize] { 41 | glyphs.advances(for: self, orientation: orientation) 42 | } 43 | 44 | public func boundingBox() -> CGRect { 45 | CTFontGetBoundingBox(self) 46 | } 47 | 48 | public func boundingRects(of glyphs: [CGGlyph], orientation: CTFontOrientation = .default) -> [CGRect] { 49 | glyphs.boundingRects(for: self, orientation: orientation) 50 | } 51 | 52 | /// Calculates the optical bounds for an array of glyphs and returns the overall optical bounds for the run. 53 | public func opticalBounds(of glyphs: [CGGlyph]) -> [CGRect] { 54 | glyphs.opticalBounds(for: self) 55 | } 56 | 57 | /// Calculates the summed advance of glyphs 58 | public func totalAdvance(of glyphs: [CGGlyph]) -> Double { 59 | advances(of: glyphs).reduce(0, { $0 + Double($1.width) }) 60 | } 61 | 62 | /// Creates a path for the specified glyph. 63 | public func path(for glyph: CGGlyph, transform: CGAffineTransform = .identity) -> CGPath? { 64 | var transform = transform 65 | return CTFontCreatePathForGlyph(self, glyph, &transform) 66 | } 67 | } 68 | 69 | #if canImport(CoreGraphics) 70 | extension CTFont { 71 | 72 | public func draw(glyphs: [CGGlyph], at positions: [CGPoint], in context: CGContext) { 73 | var glyphs = glyphs 74 | var positions = positions 75 | CTFontDrawGlyphs(self, &glyphs, &positions, glyphs.count, context) 76 | } 77 | } 78 | #endif 79 | -------------------------------------------------------------------------------- /Sources/CoreTextSwift/FontManager+Swift.swift: -------------------------------------------------------------------------------- 1 | import CoreText 2 | import Foundation 3 | 4 | public enum FontManager { 5 | 6 | /// A comparator function to compare font family names and sort them according to Apple guidelines. 7 | public static func availableFontFamilyNames() -> [String] { 8 | CTFontManagerCopyAvailableFontFamilyNames() as? [String] ?? [] 9 | } 10 | 11 | #if os(OSX) 12 | /// Returns an array of font URLs. 13 | @available(OSX 10.6, *) 14 | public static func availableFontURLs() -> [URL] { 15 | CTFontManagerCopyAvailableFontURLs() as? [URL] ?? [] 16 | } 17 | #endif 18 | 19 | /// Returns an array of unique PostScript font names for the fonts. 20 | public static func availablePostScriptNames() -> [String] { 21 | CTFontManagerCopyAvailablePostScriptNames() as? [String] ?? [] 22 | } 23 | 24 | /// Creates a font descriptor representing the font in the supplied data. 25 | public static func fontDescriptor(from data: Data) -> CTFontDescriptor? { 26 | CTFontManagerCreateFontDescriptorFromData(data as CFData) 27 | } 28 | 29 | /// Returns an array of font descriptors representing each of the fonts in the specified URL. 30 | public static func fontDescriptors(from fileURL: URL) -> [CTFontDescriptor] { 31 | CTFontManagerCreateFontDescriptorsFromURL(fileURL as CFURL) as? [CTFontDescriptor] ?? [] 32 | } 33 | 34 | /// Registers fonts from the specified font URL with the Font Manager. Registered fonts are discoverable through font descriptor matching. 35 | /// - Parameters: 36 | /// - fontURL: The font URL. 37 | /// - scope: Scope constant defining the availability and lifetime of the registration. See CTFontManagerScope for values to pass for this parameter. 38 | /// - Throws: An error in case of failed registration 39 | public static func registerFont(fontURL: URL, scope: CTFontManagerScope = .user) throws { 40 | guard let error = CFErrorCreate(nil, kCFErrorDomainPOSIX, 0, nil) else { 41 | return 42 | } 43 | 44 | var unmanagedError: Unmanaged? = Unmanaged.passRetained(error) 45 | CTFontManagerRegisterFontsForURL(fontURL as CFURL, scope, &unmanagedError) 46 | if let error = unmanagedError { 47 | throw error.takeRetainedValue() 48 | } 49 | } 50 | 51 | /// Unregisters fonts from the specified font URL with the Font Manager. Unregistered fonts are no longer discoverable through font descriptor matching. 52 | public static func unregisterFonts(fontURL: URL, scope: CTFontManagerScope = .user) throws { 53 | guard let error = CFErrorCreate(nil, kCFErrorDomainPOSIX, 0, nil) else { 54 | return 55 | } 56 | var unmanagedError: Unmanaged? = Unmanaged.passRetained(error) 57 | CTFontManagerUnregisterFontsForURL(fontURL as CFURL, scope, &unmanagedError) 58 | if let error = unmanagedError { 59 | throw error.takeRetainedValue() 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/CoreTextSwift/CTLine+Swift.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CoreText 3 | 4 | extension CTLine { 5 | 6 | public enum LineHeight { 7 | case typographic 8 | case normal // +20% 9 | } 10 | 11 | public func glyphRuns() -> [CTRun] { 12 | CTLineGetGlyphRuns(self) as! [CTRun] 13 | } 14 | 15 | public func typographicBounds() -> (ascent: CGFloat, descent: CGFloat, leading: CGFloat) { 16 | var ascent: CGFloat = 0 17 | var descent: CGFloat = 0 18 | var leading: CGFloat = 0 19 | CTLineGetTypographicBounds(self, &ascent, &descent, &leading) 20 | return (ascent: ascent, descent: descent, leading: leading) 21 | } 22 | 23 | public func typographicHeight() -> CGFloat { 24 | let (ascent, descent, leading) = typographicBounds() 25 | return ascent + descent + leading 26 | } 27 | 28 | public func height(_ kind: LineHeight = .typographic) -> CGFloat { 29 | let (ascent, descent, leading) = typographicBounds() 30 | 31 | switch kind { 32 | case .typographic: 33 | return ascent + descent + leading 34 | case .normal: 35 | // 1.20 factor does not perfectly gives CoreText value, but this is the factor in general 36 | return floor((ascent + descent + leading) * 1.20) 37 | } 38 | } 39 | 40 | /// Performs hit testing. 41 | public func characterIndex(forPosition position: CGPoint) -> CFIndex { 42 | CTLineGetStringIndexForPosition(self, position) 43 | } 44 | 45 | /// Offset for character Index (Index is frame string index, not line index) 46 | public func offsetForCharacterIndex(_ characterIndex: CFIndex) -> CGFloat { 47 | CTLineGetOffsetForStringIndex(self, characterIndex, nil) 48 | } 49 | 50 | public func stringRange() -> NSRange { 51 | NSRange(CTLineGetStringRange(self)) 52 | } 53 | 54 | /// The provided block is invoked once for each logical caret edge in the line, in left-to-right visual order. 55 | private func carretOffsets(offset: @escaping (_ offset: Double, _ charIndex: CFIndex) -> Void) { 56 | // The offset parameter is relative to the line origin 57 | CTLineEnumerateCaretOffsets(self) { offsetValue, charIndex, leadingEdge, stop in 58 | offset(offsetValue, charIndex) 59 | // stop.pointee = true 60 | } 61 | } 62 | 63 | public func carretOffsets() -> [Double] { 64 | var offsets: [Double] = [] 65 | offsets.reserveCapacity(300) 66 | carretOffsets { (offset, charIndex) in 67 | offsets += [offset] 68 | } 69 | return offsets 70 | } 71 | } 72 | 73 | #if canImport(CoreGraphics) 74 | extension CTLine { 75 | 76 | public func imageBounds(in context: CGContext) -> CGRect { 77 | CTLineGetImageBounds(self, context) 78 | } 79 | 80 | public func draw(in context: CGContext) { 81 | CTLineDraw(self, context) 82 | } 83 | } 84 | 85 | extension CGContext { 86 | public func draw(_ line: CTLine) { 87 | line.draw(in: self) 88 | } 89 | } 90 | #endif 91 | -------------------------------------------------------------------------------- /Sources/CoreTextSwift/CTRun+Swift.swift: -------------------------------------------------------------------------------- 1 | import CoreText 2 | import Foundation 3 | 4 | extension CTRun { 5 | 6 | public var glyphsCount: CFIndex { 7 | CTRunGetGlyphCount(self) 8 | } 9 | 10 | public func glyphs(range glyphsRange: NSRange = NSRange()) -> [CGGlyph] { 11 | let runGlyphsCount = self.glyphsCount 12 | return [CGGlyph](unsafeUninitializedCapacity: runGlyphsCount) { (bufferPointer, count) in 13 | CTRunGetGlyphs(self, CFRange(glyphsRange), bufferPointer.baseAddress!) 14 | count = runGlyphsCount 15 | } 16 | } 17 | 18 | public func glyphsAdvances() -> [CGSize] { 19 | font.advances(of: glyphs()) 20 | } 21 | 22 | public func typographicBounds(range: NSRange = NSRange()) -> (ascent: CGFloat, descent: CGFloat, leading: CGFloat) { 23 | var ascent: CGFloat = 0 24 | var descent: CGFloat = 0 25 | var leading: CGFloat = 0 26 | CTRunGetTypographicBounds(self, CFRange(range), &ascent, &descent, &leading) 27 | return (ascent: ascent, descent: descent, leading: leading) 28 | } 29 | 30 | public func baseAdvances(range: NSRange = NSRange()) -> [CGSize] { 31 | let runGlyphsCount = self.glyphsCount 32 | return [CGSize](unsafeUninitializedCapacity: runGlyphsCount) { (bufferPointer, count) in 33 | CTRunGetBaseAdvancesAndOrigins(self, CFRange(range), bufferPointer.baseAddress!, nil) 34 | count = runGlyphsCount 35 | } 36 | } 37 | 38 | public func baseOrigins(range: NSRange = NSRange()) -> [CGPoint] { 39 | let runGlyphsCount = self.glyphsCount 40 | return [CGPoint](unsafeUninitializedCapacity: runGlyphsCount) { (bufferPointer, count) in 41 | CTRunGetBaseAdvancesAndOrigins(self, CFRange(range), nil, bufferPointer.baseAddress!) 42 | count = runGlyphsCount 43 | } 44 | } 45 | 46 | /// The glyph positions in a run are relative to the origin of the line containing the run. 47 | public func glyphPositions(range: NSRange = NSRange()) -> [CGPoint] { 48 | let runGlyphsCount = self.glyphsCount 49 | return [CGPoint](unsafeUninitializedCapacity: runGlyphsCount) { (bufferPointer, count) in 50 | CTRunGetPositions(self, CFRange(range), bufferPointer.baseAddress!) 51 | count = runGlyphsCount 52 | } 53 | } 54 | 55 | public var attributes: [NSAttributedString.Key: Any] { 56 | let dict = (CTRunGetAttributes(self) as NSDictionary as! [String: Any]) 57 | return dict.reduce([:]) { (partialResult: [NSAttributedString.Key: Any], tuple: (key: String, value: Any)) in 58 | var result = partialResult 59 | let attributeName = NSAttributedString.Key(rawValue: tuple.key) 60 | result[attributeName] = tuple.value 61 | return result 62 | } 63 | } 64 | 65 | public var font: CTFont { 66 | attributes[.font] as! CTFont 67 | } 68 | 69 | public var foregroundColor: String? { 70 | attributes[.foregroundColor] as? String 71 | } 72 | 73 | public var runDelegate: Any? { 74 | attributes[.runDelegate] 75 | } 76 | 77 | /// Returns the run's status. 78 | public var status: CTRunStatus { 79 | CTRunGetStatus(self) 80 | } 81 | 82 | /// String indices stored in the run 83 | /// 84 | /// The indices are the character indices that originally spawned the glyphs that make up the run. 85 | /// They can be used to map the glyphs in the run back to the characters in the backing store. 86 | public func stringIndices(in range: NSRange = NSRange()) -> [CFIndex] { 87 | let runGlyphsCount = self.glyphsCount 88 | return [CFIndex](unsafeUninitializedCapacity: runGlyphsCount) { (bufferPointer, count) in 89 | CTRunGetStringIndices(self, CFRange(range), bufferPointer.baseAddress!) 90 | count = runGlyphsCount 91 | } 92 | } 93 | 94 | /// Gets the range of characters that originally spawned the glyphs in the run. 95 | public var stringRange: NSRange { 96 | NSRange(CTRunGetStringRange(self)) 97 | } 98 | } 99 | 100 | #if canImport(CoreGraphics) 101 | extension CTRun { 102 | 103 | /// Draws a complete run or part of one. 104 | public func draw(range: NSRange = NSRange(), in context: CGContext) { 105 | CTRunDraw(self, context, CFRange(range)) 106 | } 107 | 108 | /// Returns the text matrix needed to draw this run. 109 | public var textMatrix: CGAffineTransform { 110 | CTRunGetTextMatrix(self) 111 | } 112 | } 113 | 114 | extension CGContext { 115 | 116 | /// Draws a complete run or part of one. 117 | public func draw(_ run: CTRun) { 118 | run.draw(in: self) 119 | } 120 | 121 | } 122 | #endif 123 | --------------------------------------------------------------------------------