@@ -26,16 +26,16 @@ struct PreviewTextOverlaySpec: Sendable {
2626// MARK: - Renderer + cache
2727
2828final class TextOverlayPreviewCIRenderer : @unchecked Sendable {
29-
29+
3030 static let shared = TextOverlayPreviewCIRenderer ( )
31-
31+
3232 private let cache = NSCache < NSString , CIImage > ( )
3333 private static let referenceCompositionHeight : CGFloat = 1080
34-
34+
3535 private init ( ) {
3636 cache. countLimit = 48
3737 }
38-
38+
3939 func ciImageFullFrame( for spec: PreviewTextOverlaySpec , renderSize: CGSize ) -> CIImage ? {
4040 guard renderSize. width > 1 , renderSize. height > 1 else { return nil }
4141 let key = Self . cacheKey ( for: spec, renderSize: renderSize) as NSString
@@ -46,19 +46,25 @@ final class TextOverlayPreviewCIRenderer: @unchecked Sendable {
4646 cache. setObject ( image, forKey: key)
4747 return image
4848 }
49-
49+
5050 func invalidateAllCaches( ) {
5151 cache. removeAllObjects ( )
5252 }
53-
53+
5454 // MARK: - Raster (background thread safe; no UIKit)
55-
55+
5656 private static func rasterize( spec: PreviewTextOverlaySpec , renderSize: CGSize ) -> CIImage ? {
5757 let w = Int ( max ( 1 , renderSize. width. rounded ( . down) ) )
5858 let h = Int ( max ( 1 , renderSize. height. rounded ( . down) ) )
59- let box = layoutBox ( transform: spec. transform, renderSize: CGSize ( width: w, height: h) )
59+
60+ // 1. EN ÖNEMLİ ADIM: UI (Top-Left) koordinatını CoreGraphics (Bottom-Left) koordinatına çeviriyoruz.
61+ var correctedTransform = spec. transform
62+ correctedTransform. normalizedCenter. y = 1.0 - correctedTransform. normalizedCenter. y
63+
64+ // Kutu pozisyonunu düzeltilmiş transform ile hesaplıyoruz
65+ let box = layoutBox ( transform: correctedTransform, renderSize: CGSize ( width: w, height: h) )
6066 guard box. width >= 1 , box. height >= 1 else { return nil }
61-
67+
6268 let colorSpace = CGColorSpace ( name: CGColorSpace . sRGB) ?? CGColorSpaceCreateDeviceRGB ( )
6369 guard let ctx = CGContext (
6470 data: nil ,
@@ -69,53 +75,59 @@ final class TextOverlayPreviewCIRenderer: @unchecked Sendable {
6975 space: colorSpace,
7076 bitmapInfo: CGImageAlphaInfo . premultipliedLast. rawValue
7177 ) else { return nil }
72-
73- ctx. translateBy ( x: 0 , y: CGFloat ( h) )
74- ctx. scaleBy ( x: 1 , y: - 1 )
75-
78+
79+ // DİKKAT: Eski kodundaki ctx.translateBy(x: 0, y: CGFloat(h)) ve ctx.scaleBy(x: 1, y: -1)
80+ // satırlarını SİLDİK. CoreText'in yazıyı düz (upright) çizebilmesi için context'in
81+ // orijinal Bottom-Left yapısında kalması gerekiyor.
82+
7683 ctx. saveGState ( )
84+
85+ // Rotasyon Merkezini ayarlıyoruz
7786 ctx. translateBy ( x: box. midX, y: box. midY)
78- ctx. rotate ( by: spec. transform. rotationAngle)
87+ // UIKit'te rotasyon saat yönündedir, Bottom-Left sistemde tam tersidir.
88+ // Bu yüzden eksi (-) işareti koyuyoruz.
89+ ctx. rotate ( by: - spec. transform. rotationAngle)
7990 ctx. translateBy ( x: - box. midX, y: - box. midY)
80-
91+
92+ // Arkaplan Rengi (varsa)
8193 if let bgHex = spec. descriptor. backgroundColorHex,
8294 let bg = rgbColor ( fromHex: bgHex) {
8395 ctx. setFillColor ( bg)
8496 ctx. fill ( box)
8597 }
86-
98+
8799 let fontSize = scaledPointSize ( from: spec. descriptor, compositionHeight: CGFloat ( h) )
88100 let ctFont = CTFontCreateWithName ( spec. descriptor. fontName as CFString , fontSize, nil )
89-
90101 let textColor = rgbColor ( fromHex: spec. descriptor. textColorHex) ?? CGColor ( gray: 1 , alpha: 1 )
91-
92102 let paragraph = Self . makeParagraphStyle ( alignment: spec. descriptor. alignmentMode. ctAlignment)
93-
103+
94104 let attrs : [ NSAttributedString . Key : Any ] = [
95105 kCTFontAttributeName as NSAttributedString . Key: ctFont,
96106 kCTForegroundColorAttributeName as NSAttributedString . Key: textColor,
97107 kCTParagraphStyleAttributeName as NSAttributedString . Key: paragraph,
98108 ]
109+
99110 let attrString = NSAttributedString ( string: spec. descriptor. text, attributes: attrs)
100111 let framesetter = CTFramesetterCreateWithAttributedString ( attrString as CFAttributedString )
101112 let path = CGPath ( rect: box. insetBy ( dx: 6 , dy: 4 ) , transform: nil )
102113 let frame = CTFramesetterCreateFrame ( framesetter, CFRange ( location: 0 , length: attrString. length) , path, nil )
114+
103115 CTFrameDraw ( frame, ctx)
104-
116+
105117 ctx. restoreGState ( )
106-
118+
107119 guard let cgImage = ctx. makeImage ( ) else { return nil }
108120 return CIImage ( cgImage: cgImage)
109121 }
110-
122+
111123 private static func layoutBox( transform: TransformEffect , renderSize: CGSize ) -> CGRect {
112124 let w = max ( transform. normalizedSize. width * renderSize. width * transform. normalizedScale, 44 )
113125 let h = max ( transform. normalizedSize. height * renderSize. height * transform. normalizedScale, 1 )
114126 let cx = transform. normalizedCenter. x * renderSize. width
115127 let cy = transform. normalizedCenter. y * renderSize. height
116128 return CGRect ( x: cx - w / 2 , y: cy - h / 2 , width: w, height: h)
117129 }
118-
130+
119131 private static func makeParagraphStyle( alignment: CTTextAlignment ) -> CTParagraphStyle {
120132 var align = alignment
121133 return withUnsafePointer ( to: & align) { ptr in
@@ -127,12 +139,12 @@ final class TextOverlayPreviewCIRenderer: @unchecked Sendable {
127139 return CTParagraphStyleCreate ( [ setting] , 1 )
128140 }
129141 }
130-
142+
131143 private static func scaledPointSize( from descriptor: TextOverlayDescriptor , compositionHeight: CGFloat ) -> CGFloat {
132144 let scale = compositionHeight / referenceCompositionHeight
133145 return max ( 1 , descriptor. fontSize * scale)
134146 }
135-
147+
136148 private static func cacheKey( for spec: PreviewTextOverlaySpec , renderSize: CGSize ) -> String {
137149 let d = spec. descriptor
138150 let t = spec. transform
@@ -145,21 +157,21 @@ final class TextOverlayPreviewCIRenderer: @unchecked Sendable {
145157 \( t. normalizedScale) | \( t. rotationAngle) | \( spec. opacity) | \( w) x \( h)
146158 """
147159 }
148-
160+
149161 private static func rgbColor( fromHex hexString: String ) -> CGColor ? {
150162 let hex = hexString. trimmingCharacters ( in: CharacterSet . alphanumerics. inverted)
151163 var int = UInt64 ( )
152164 guard Scanner ( string: hex) . scanHexInt64 ( & int) else { return nil }
153165 let a , r , g , b : UInt64
154166 switch hex. count {
155- case 3 :
156- ( a, r, g, b) = ( 255 , ( int >> 8 ) * 17 , ( int >> 4 & 0xF ) * 17 , ( int & 0xF ) * 17 )
157- case 6 :
158- ( a, r, g, b) = ( 255 , int >> 16 , int >> 8 & 0xFF , int & 0xFF )
159- case 8 :
160- ( a, r, g, b) = ( int >> 24 , int >> 16 & 0xFF , int >> 8 & 0xFF , int & 0xFF )
161- default :
162- return nil
167+ case 3 :
168+ ( a, r, g, b) = ( 255 , ( int >> 8 ) * 17 , ( int >> 4 & 0xF ) * 17 , ( int & 0xF ) * 17 )
169+ case 6 :
170+ ( a, r, g, b) = ( 255 , int >> 16 , int >> 8 & 0xFF , int & 0xFF )
171+ case 8 :
172+ ( a, r, g, b) = ( int >> 24 , int >> 16 & 0xFF , int >> 8 & 0xFF , int & 0xFF )
173+ default :
174+ return nil
163175 }
164176 return CGColor (
165177 red: CGFloat ( r) / 255 ,
@@ -175,18 +187,18 @@ final class TextOverlayPreviewCIRenderer: @unchecked Sendable {
175187private extension TextOverlayTextAlignment {
176188 var ctAlignment : CTTextAlignment {
177189 switch self {
178- case . natural: return . natural
179- case . left: return . left
180- case . center: return . center
181- case . right: return . right
190+ case . natural: return . natural
191+ case . left: return . left
192+ case . center: return . center
193+ case . right: return . right
182194 }
183195 }
184196}
185197
186198// MARK: - Compositor helper
187199
188200enum PreviewTextOverlayCompositor {
189-
201+
190202 static func composite(
191203 specs: [ PreviewTextOverlaySpec ] ,
192204 onto base: CIImage ? ,
0 commit comments