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