1/* 2 PPGNUstepGlue_BezierPathAliasing.m 3 4 Copyright 2014-2018 Josh Freeman 5 http://www.twilightedge.com 6 7 This file is part of PikoPixel for GNUstep. 8 PikoPixel is a graphical application for drawing & editing pixel-art images. 9 10 PikoPixel is free software: you can redistribute it and/or modify it under 11 the terms of the GNU Affero General Public License as published by the 12 Free Software Foundation, either version 3 of the License, or (at your 13 option) any later version approved for PikoPixel by its copyright holder (or 14 an authorized proxy). 15 16 PikoPixel is distributed in the hope that it will be useful, but WITHOUT ANY 17 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 18 FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more 19 details. 20 21 You should have received a copy of the GNU Affero General Public License 22 along with this program. If not, see <http://www.gnu.org/licenses/>. 23*/ 24 25// PPGNUstepGlue_BezierPathAliasing.m was initially meant as a workaround for GNUstep's 26// antialiasing setting (path drawing would always use antialiasing, even if the graphics 27// context's antialiasing setting was disabled). 28// The antialiasing setting has been since fixed for Cairo in the GNUstep trunk (2015-09-20), 29// however, un-antialiased Cairo paths are drawn in different shapes than on Mac OS X: some 30// pixels are missing, possibly due to roundoff. 31// Antialiased paths are also drawn differently between Cairo & OS X, however, the 32// workaround below accounted for those differences, so until a workaround is developed for the 33// unantialiased drawing differences, the current fix is to use antialiasing when drawing (set 34// in PPGNUstepGlue_BitmapGraphcisContext.m's ppGSPatch_SetAsCurrentGraphicsContext), and 35// continue to use the antialiasing workaround. 36 37#ifdef GNUSTEP 38 39#import <Cocoa/Cocoa.h> 40#import "NSObject_PPUtilities.h" 41#import "PPAppBootUtilities.h" 42#import "NSBezierPath_PPUtilities.h" 43#import "PPDocument.h" 44#import "PPGeometry.h" 45 46 47// disable clang warnings about fabsf() truncating passed double-type values to float-type 48#ifdef __clang__ 49# pragma clang diagnostic ignored "-Wabsolute-value" 50#endif // __clang__ 51 52 53@interface NSBezierPath (PPGNUstepGlue_BezierPathAliasingUtilities) 54 55- (NSBezierPath *) ppGSGlue_ResegmentedPathForDrawing; 56 57- (void) ppGSGlue_SegmentedLineToPoint: (NSPoint) endPoint lastPoint: (NSPoint) startPoint; 58 59@end 60 61 62@implementation NSObject (PPGNUstepGlue_BezierPathAliasing) 63 64+ (void) ppGSGlue_BezierPathAliasing_InstallPatches 65{ 66 macroSwizzleInstanceMethod(NSBezierPath, ppAppendSinglePixelLineAtPoint:, 67 ppGSPatch_AppendSinglePixelLineAtPoint:); 68 69 macroSwizzleInstanceMethod(NSBezierPath, ppAppendLineFromPixelAtPoint:toPixelAtPoint:, 70 ppGSPatch_AppendLineFromPixelAtPoint:toPixelAtPoint:); 71 72 73 macroSwizzleInstanceMethod(PPDocument, drawBezierPath:andFill:pathIsPixelated:, 74 ppGSPatch_BezierPathAliasing_DrawBezierPath:andFill: 75 pathIsPixelated:); 76} 77 78+ (void) load 79{ 80 macroPerformNSObjectSelectorAfterAppLoads(ppGSGlue_BezierPathAliasing_InstallPatches); 81} 82 83@end 84 85@implementation NSBezierPath (PPGNUstepGlue_BezierPathAliasing) 86 87// PATCH: -[NSBezierPath (PPUtilities) ppAppendSinglePixelLineAtPoint:] 88// GNUstep's -[NSBezierPath stroke] always uses antialiasing (despite graphics context setting), 89// so override uses a different method for filling in a pixel (draw a centered, 90// half-pixel-length square) 91 92- (void) ppGSPatch_AppendSinglePixelLineAtPoint: (NSPoint) point 93{ 94 NSRect pixelRect; 95 96 point = PPGeometry_PixelCenteredPoint(point); 97 98 pixelRect = NSMakeRect(floorf(point.x) + 0.4, floorf(point.y) + 0.4, 0.2, 0.2); 99 100 [self appendBezierPathWithRect: pixelRect]; 101 102 [self lineToPoint: point]; 103} 104 105// PATCH: -[NSBezierPath (PPUtilities) ppAppendLineFromPixelAtPoint:toPixelAtPoint:] 106// Overridden to use -[NSBezierPath (PPUtilities) ppAppendSinglePixelLineAtPoint:] when 107// drawing a single pixel (both line endpoints are the same) in order to use the patched 108// implementation (original's method for filling a pixel doesn't work due to GNUstep's 109// antialiasing) 110 111- (void) ppGSPatch_AppendLineFromPixelAtPoint: (NSPoint) startPoint 112 toPixelAtPoint: (NSPoint) endPoint 113{ 114 if (NSEqualPoints(startPoint, endPoint)) 115 { 116 [self ppAppendSinglePixelLineAtPoint: endPoint]; 117 return; 118 } 119 120 [self ppGSPatch_AppendLineFromPixelAtPoint: startPoint toPixelAtPoint: endPoint]; 121} 122 123@end 124 125@implementation PPDocument (PPGNUstepGlue_BezierPathAliasing) 126 127// PATCH: -[PPDocument (DrawingPrivateMethods) drawBezierPath:andFill:pathIsPixelated:] 128// GNUstep's -[NSBezierPath stroke] always uses antialiasing (despite graphics context setting), 129// which can fill in extra pixels around the path - overridden to call local utility method 130// before drawing, -[NSBezierPath pGSGlue_ResegmentedPathForDrawing], which resegments the 131// lines in the path into a series of horizontal, vertical, or 1:1 diagonal lines, because 132// lines with those slopes are drawn with minimal antialiasing 133 134- (void) ppGSPatch_BezierPathAliasing_DrawBezierPath: (NSBezierPath *) path 135 andFill: (bool) shouldFill 136 pathIsPixelated: (bool) pathIsPixelated 137{ 138 if (!pathIsPixelated) 139 { 140 path = [path ppGSGlue_ResegmentedPathForDrawing]; 141 } 142 143 [self ppGSPatch_BezierPathAliasing_DrawBezierPath: path 144 andFill: shouldFill 145 pathIsPixelated: pathIsPixelated]; 146} 147 148@end 149 150@implementation NSBezierPath (PPGNUstepGlue_BezierPathAliasingUtilities) 151 152// ppGSGlue_ResegmentedPathForDrawing: Utility method for resegmenting the lines in a path into 153// a series of horizontal, vertical, or 1:1 diagonal lines, which minimizes antialiasing when 154// the path is drawn 155 156- (NSBezierPath *) ppGSGlue_ResegmentedPathForDrawing 157{ 158 NSBezierPath *resegmentedPath; 159 NSInteger elementCount, elementIndex; 160 NSPoint currentPoint, lastPoint, elementPoints[3], pointsDelta; 161 162 resegmentedPath = [NSBezierPath bezierPath]; 163 164 if (!resegmentedPath) 165 return self; 166 167 elementCount = [self elementCount]; 168 169 for (elementIndex = 0; elementIndex < elementCount; elementIndex++) 170 { 171 switch ([self elementAtIndex: elementIndex associatedPoints: elementPoints]) 172 { 173 case NSMoveToBezierPathElement: 174 { 175 currentPoint = elementPoints[0]; 176 177 [resegmentedPath moveToPoint: currentPoint]; 178 179 lastPoint = currentPoint; 180 } 181 break; 182 183 case NSLineToBezierPathElement: 184 { 185 currentPoint = elementPoints[0]; 186 187 pointsDelta = PPGeometry_PointDifference(currentPoint, lastPoint); 188 189 if (!pointsDelta.x || !pointsDelta.y 190 || (pointsDelta.x == pointsDelta.y)) 191 { 192 [resegmentedPath lineToPoint: currentPoint]; 193 } 194 else 195 { 196 [resegmentedPath ppGSGlue_SegmentedLineToPoint: currentPoint 197 lastPoint: lastPoint]; 198 } 199 200 lastPoint = currentPoint; 201 } 202 break; 203 204 case NSCurveToBezierPathElement: 205 { 206 return self; 207 } 208 break; 209 210 case NSClosePathBezierPathElement: 211 { 212 [resegmentedPath closePath]; 213 } 214 break; 215 216 default: 217 break; 218 } 219 } 220 221 return resegmentedPath; 222} 223 224// ppGSGlue_SegmentedLineToPoint: Utility method to append a segmented line between 225// startPoint & endPoint, made up of a series of horizontal, vertical, or 1:1 diagonal lines, 226// in order to minimize antialiasing when drawing - used by ppGSGlue_ResegmentedPathForDrawing: 227// method above 228 229- (void) ppGSGlue_SegmentedLineToPoint: (NSPoint) endPoint lastPoint: (NSPoint) startPoint 230{ 231 NSPoint pointsDelta, absPointsDelta; 232 CGFloat x, y, startX, startY, stepX, stepY, endX, endY, ratio; 233 234 pointsDelta = PPGeometry_PointDifference(endPoint, startPoint); 235 absPointsDelta = NSMakePoint(fabsf(pointsDelta.x), fabsf(pointsDelta.y)); 236 237 if (absPointsDelta.x > absPointsDelta.y) 238 { 239 if (pointsDelta.y > 0) 240 { 241 startY = floor(startPoint.y + 1.0); 242 stepY = 1.0; 243 endY = floor(endPoint.y + 1.0); 244 } 245 else 246 { 247 startY = floorf(startPoint.y); 248 stepY = -1.0; 249 endY = floor(endPoint.y); 250 } 251 252 stepX = (pointsDelta.x > 0) ? 1.0 : -1.0; 253 254 ratio = pointsDelta.x / pointsDelta.y; 255 256 for (y = startY; y != endY; y += stepY) 257 { 258 x = round((y - startPoint.y) * ratio + startPoint.x) - stepX * 0.5; 259 260 [self lineToPoint: NSMakePoint(x, y - stepY * 0.5)]; 261 [self lineToPoint: NSMakePoint(x + stepX, y + stepY * 0.5)]; 262 } 263 264 [self lineToPoint: endPoint]; 265 } 266 else 267 { 268 if (pointsDelta.x > 0) 269 { 270 startX = floor(startPoint.x + 1.0); 271 stepX = 1.0; 272 endX = floor(endPoint.x + 1.0); 273 } 274 else 275 { 276 startX = floorf(startPoint.x); 277 stepX = -1.0; 278 endX = floor(endPoint.x); 279 } 280 281 stepY = (pointsDelta.y > 0) ? 1.0 : -1.0; 282 283 ratio = pointsDelta.y / pointsDelta.x; 284 285 for (x = startX; x != endX; x += stepX) 286 { 287 y = round((x - startPoint.x) * ratio + startPoint.y) - stepY * 0.5; 288 289 [self lineToPoint: NSMakePoint(x - stepX * 0.5, y)]; 290 [self lineToPoint: NSMakePoint(x + stepX * 0.5, y + stepY)]; 291 } 292 293 [self lineToPoint: endPoint]; 294 } 295} 296 297@end 298 299#endif // GNUSTEP 300 301