mirror of
https://github.com/gnustep/libs-gui.git
synced 2025-05-31 22:10:47 +00:00
* Headers/AppKit/NSImageRep.h: Add
-drawInRect:fromRect:operation:fraction:respectFlipped:hints: method * Source/NSImage.m: * Source/NSImageRep.m: Refactor drawing code from NSImage to NSImageRep. This should cause no functionlity change, but it lets NSImageRep subclasses implement more efficient versions of -drawInRect:fromRect:... that don't involve the (expensive) drawing of the image in a temporary offscreen window and then drawing from there on to the destination surface. git-svn-id: svn+ssh://svn.gna.org/svn/gnustep/libs/gui/trunk@33764 72102866-910b-0410-8b05-ffd578937521
This commit is contained in:
parent
ea63bbc0ff
commit
f3a9150d7c
4 changed files with 397 additions and 439 deletions
11
ChangeLog
11
ChangeLog
|
@ -1,3 +1,14 @@
|
||||||
|
2011-08-18 Eric Wasylishen <ewasylishen@gmail.com>
|
||||||
|
|
||||||
|
* Headers/AppKit/NSImageRep.h: Add
|
||||||
|
-drawInRect:fromRect:operation:fraction:respectFlipped:hints: method
|
||||||
|
* Source/NSImage.m:
|
||||||
|
* Source/NSImageRep.m: Refactor drawing code from NSImage to NSImageRep.
|
||||||
|
This should cause no functionlity change, but it lets NSImageRep
|
||||||
|
subclasses implement more efficient versions of -drawInRect:fromRect:...
|
||||||
|
method that don't involve the wasteful drawing the image in a temporary
|
||||||
|
offscreen window and then drawing from there on to the destination surface.
|
||||||
|
|
||||||
2011-08-17 Eric Wasylishen <ewasylishen@gmail.com>
|
2011-08-17 Eric Wasylishen <ewasylishen@gmail.com>
|
||||||
|
|
||||||
* Source/Functions.m (NSDrawNinePartImage): Pixel-align drawing rect using
|
* Source/Functions.m (NSDrawNinePartImage): Pixel-align drawing rect using
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
#import <Foundation/NSGeometry.h>
|
#import <Foundation/NSGeometry.h>
|
||||||
#import <Foundation/NSObject.h>
|
#import <Foundation/NSObject.h>
|
||||||
#import <AppKit/AppKitDefines.h>
|
#import <AppKit/AppKitDefines.h>
|
||||||
|
#import <AppKit/NSGraphicsContext.h>
|
||||||
|
|
||||||
@class NSString;
|
@class NSString;
|
||||||
@class NSArray;
|
@class NSArray;
|
||||||
|
@ -147,6 +148,14 @@ enum {
|
||||||
- (BOOL)draw;
|
- (BOOL)draw;
|
||||||
- (BOOL)drawAtPoint:(NSPoint)aPoint;
|
- (BOOL)drawAtPoint:(NSPoint)aPoint;
|
||||||
- (BOOL)drawInRect:(NSRect)aRect;
|
- (BOOL)drawInRect:(NSRect)aRect;
|
||||||
|
#if OS_API_VERSION(MAC_OS_X_VERSION_10_6, GS_API_LATEST)
|
||||||
|
- (BOOL) drawInRect: (NSRect)dstRect
|
||||||
|
fromRect: (NSRect)srcRect
|
||||||
|
operation: (NSCompositingOperation)op
|
||||||
|
fraction: (float)delta
|
||||||
|
respectFlipped: (BOOL)respectFlipped
|
||||||
|
hints: (NSDictionary*)hints;
|
||||||
|
#endif
|
||||||
|
|
||||||
//
|
//
|
||||||
// Managing NSImageRep Subclasses
|
// Managing NSImageRep Subclasses
|
||||||
|
|
500
Source/NSImage.m
500
Source/NSImage.m
|
@ -55,24 +55,6 @@
|
||||||
#import "GNUstepGUI/GSDisplayServer.h"
|
#import "GNUstepGUI/GSDisplayServer.h"
|
||||||
#import "GSThemePrivate.h"
|
#import "GSThemePrivate.h"
|
||||||
|
|
||||||
|
|
||||||
/* Helpers. Would be nicer to use the C99 fmin/fmax functions, but that
|
|
||||||
isn't currently possible. */
|
|
||||||
static double gs_min(double x, double y)
|
|
||||||
{
|
|
||||||
if (x > y)
|
|
||||||
return y;
|
|
||||||
else
|
|
||||||
return x;
|
|
||||||
}
|
|
||||||
static double gs_max(double x, double y)
|
|
||||||
{
|
|
||||||
if (x < y)
|
|
||||||
return y;
|
|
||||||
else
|
|
||||||
return x;
|
|
||||||
}
|
|
||||||
|
|
||||||
BOOL NSImageForceCaching = NO; /* use on missmatch */
|
BOOL NSImageForceCaching = NO; /* use on missmatch */
|
||||||
|
|
||||||
@implementation NSBundle (NSImageAdditions)
|
@implementation NSBundle (NSImageAdditions)
|
||||||
|
@ -922,390 +904,9 @@ repd_for_rep(NSArray *_reps, NSImageRep *rep)
|
||||||
[self drawInRect: NSMakeRect(point.x, point.y, srcRect.size.width, srcRect.size.height)
|
[self drawInRect: NSMakeRect(point.x, point.y, srcRect.size.width, srcRect.size.height)
|
||||||
fromRect: srcRect
|
fromRect: srcRect
|
||||||
operation: op
|
operation: op
|
||||||
fraction: delta];
|
fraction: delta
|
||||||
}
|
respectFlipped: NO
|
||||||
|
hints: nil];
|
||||||
/* New code path that delegates as much as possible to the backend and whose
|
|
||||||
behavior precisely matches Cocoa. */
|
|
||||||
- (void) nativeDrawInRect: (NSRect)dstRect
|
|
||||||
fromRect: (NSRect)srcRect
|
|
||||||
operation: (NSCompositingOperation)op
|
|
||||||
fraction: (float)delta
|
|
||||||
{
|
|
||||||
NSGraphicsContext *ctxt = GSCurrentContext();
|
|
||||||
NSSize imgSize = [self size];
|
|
||||||
float widthScaleFactor;
|
|
||||||
float heightScaleFactor;
|
|
||||||
NSImageRep *rep;
|
|
||||||
|
|
||||||
if (NSEqualRects(srcRect, NSZeroRect))
|
|
||||||
{
|
|
||||||
srcRect.size = imgSize;
|
|
||||||
/* For -drawAtPoint:fromRect:operation:fraction: used with a zero rect */
|
|
||||||
if (NSEqualSizes(dstRect.size, NSZeroSize))
|
|
||||||
{
|
|
||||||
dstRect.size = imgSize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Choose a rep to use
|
|
||||||
|
|
||||||
rep = [self bestRepresentationForRect: dstRect
|
|
||||||
context: nil
|
|
||||||
hints: nil];
|
|
||||||
if (rep == nil)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!dstRect.size.width || !dstRect.size.height
|
|
||||||
|| !srcRect.size.width || !srcRect.size.height)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Clip to image bounds
|
|
||||||
if (srcRect.origin.x < 0)
|
|
||||||
srcRect.origin.x = 0;
|
|
||||||
if (srcRect.origin.y < 0)
|
|
||||||
srcRect.origin.y = 0;
|
|
||||||
if (NSMaxX(srcRect) > imgSize.width)
|
|
||||||
srcRect.size.width = imgSize.width - srcRect.origin.x;
|
|
||||||
if (NSMaxY(srcRect) > imgSize.height)
|
|
||||||
srcRect.size.height = imgSize.height - srcRect.origin.y;
|
|
||||||
|
|
||||||
widthScaleFactor = dstRect.size.width / srcRect.size.width;
|
|
||||||
heightScaleFactor = dstRect.size.height / srcRect.size.height;
|
|
||||||
|
|
||||||
if (![ctxt isDrawingToScreen])
|
|
||||||
{
|
|
||||||
/* We can't composite or dissolve if we aren't drawing to a screen,
|
|
||||||
so we'll just draw the right part of the image in the right
|
|
||||||
place. */
|
|
||||||
NSPoint p;
|
|
||||||
|
|
||||||
p.x = dstRect.origin.x / widthScaleFactor - srcRect.origin.x;
|
|
||||||
p.y = dstRect.origin.y / heightScaleFactor - srcRect.origin.y;
|
|
||||||
|
|
||||||
DPSgsave(ctxt);
|
|
||||||
DPSrectclip(ctxt, dstRect.origin.x, dstRect.origin.y,
|
|
||||||
dstRect.size.width, dstRect.size.height);
|
|
||||||
DPSscale(ctxt, widthScaleFactor, heightScaleFactor);
|
|
||||||
[self drawRepresentation: rep
|
|
||||||
inRect: NSMakeRect(p.x, p.y, imgSize.width, imgSize.height)];
|
|
||||||
DPSgrestore(ctxt);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* We cannot ask the backend to draw the image directly when the source rect
|
|
||||||
doesn't cover the whole image.
|
|
||||||
Cairo doesn't support to specify a source rect for a surface used as a
|
|
||||||
source, see cairo_set_source_surface()).
|
|
||||||
CoreGraphics is similarly limited, see CGContextDrawImage().
|
|
||||||
For now, we always use a two step process:
|
|
||||||
- draw the image data in a cache to apply the srcRect to inRect scaling
|
|
||||||
- draw the cache into the destination context
|
|
||||||
It might be worth to move the first step to the backend, so we don't have
|
|
||||||
to create a cache window but just an intermediate surface.
|
|
||||||
We create a cache every time but otherwise we are more efficient than the
|
|
||||||
old code path since the cache size is limited to what we actually draw
|
|
||||||
and doesn't involve drawing the whole image. */
|
|
||||||
{
|
|
||||||
/* An intermediate image used to scale the image to be drawn as needed */
|
|
||||||
NSCachedImageRep *cache;
|
|
||||||
/* The scaled image graphics state we used as the source from which we
|
|
||||||
draw into the destination (the current graphics context)*/
|
|
||||||
int gState;
|
|
||||||
/* The context of the cache window */
|
|
||||||
NSGraphicsContext *cacheCtxt;
|
|
||||||
NSSize repSize = [rep size];
|
|
||||||
/* The size of the cache window that will hold the scaled image */
|
|
||||||
NSSize cacheSize;
|
|
||||||
|
|
||||||
CGFloat imgToCacheWidthScaleFactor;
|
|
||||||
CGFloat imgToCacheHeightScaleFactor;;
|
|
||||||
|
|
||||||
NSRect srcRectInCache;
|
|
||||||
NSAffineTransform *transform, *backup;
|
|
||||||
|
|
||||||
if (NSEqualSizes(repSize, NSZeroSize))
|
|
||||||
{
|
|
||||||
repSize = dstRect.size;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (([rep pixelsWide] == NSImageRepMatchesDevice &&
|
|
||||||
[rep pixelsHigh] == NSImageRepMatchesDevice) &&
|
|
||||||
(dstRect.size.width > repSize.width ||
|
|
||||||
dstRect.size.height > repSize.height))
|
|
||||||
{
|
|
||||||
cacheSize = [[ctxt GSCurrentCTM] transformSize: dstRect.size];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
cacheSize = [[ctxt GSCurrentCTM] transformSize: repSize];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cacheSize.width < 0)
|
|
||||||
cacheSize.width *= -1;
|
|
||||||
if (cacheSize.height < 0)
|
|
||||||
cacheSize.height *= -1;
|
|
||||||
|
|
||||||
imgToCacheWidthScaleFactor = cacheSize.width / imgSize.width;
|
|
||||||
imgToCacheHeightScaleFactor = cacheSize.height / imgSize.height;
|
|
||||||
|
|
||||||
srcRectInCache = NSMakeRect(srcRect.origin.x * imgToCacheWidthScaleFactor,
|
|
||||||
srcRect.origin.y * imgToCacheHeightScaleFactor,
|
|
||||||
srcRect.size.width * imgToCacheWidthScaleFactor,
|
|
||||||
srcRect.size.height * imgToCacheHeightScaleFactor);
|
|
||||||
|
|
||||||
cache = [[NSCachedImageRep alloc]
|
|
||||||
initWithSize: NSMakeSize(ceil(cacheSize.width), ceil(cacheSize.height))
|
|
||||||
depth: [[NSScreen mainScreen] depth]
|
|
||||||
separate: YES
|
|
||||||
alpha: YES];
|
|
||||||
|
|
||||||
[[[cache window] contentView] lockFocus];
|
|
||||||
cacheCtxt = GSCurrentContext();
|
|
||||||
|
|
||||||
/* Clear the cache window surface */
|
|
||||||
DPScompositerect(cacheCtxt, 0, 0, ceil(cacheSize.width), ceil(cacheSize.height), NSCompositeClear);
|
|
||||||
gState = [cacheCtxt GSDefineGState];
|
|
||||||
|
|
||||||
//NSLog(@"Draw in cache size %@", NSStringFromSize(cacheSize));
|
|
||||||
|
|
||||||
/* We must not use -drawRepresentation:inRect: because the image must drawn
|
|
||||||
scaled even when -scalesWhenResized is NO */
|
|
||||||
[rep
|
|
||||||
drawInRect: NSMakeRect(0, 0, cacheSize.width, cacheSize.height)];
|
|
||||||
/* If we're doing a dissolve, use a DestinationIn composite to lower
|
|
||||||
the alpha of the pixels. */
|
|
||||||
if (delta != 1.0)
|
|
||||||
{
|
|
||||||
DPSsetalpha(cacheCtxt, delta);
|
|
||||||
DPScompositerect(cacheCtxt, 0, 0, ceil(cacheSize.width), ceil(cacheSize.height),
|
|
||||||
NSCompositeDestinationIn);
|
|
||||||
}
|
|
||||||
|
|
||||||
[[[cache window] contentView] unlockFocus];
|
|
||||||
|
|
||||||
//NSLog(@"Draw in %@ from %@ from cache rect %@", NSStringFromRect(dstRect),
|
|
||||||
// NSStringFromRect(srcRect), NSStringFromRect(srcRectInCache));
|
|
||||||
|
|
||||||
backup = [ctxt GSCurrentCTM];
|
|
||||||
|
|
||||||
transform = [NSAffineTransform transform];
|
|
||||||
[transform translateXBy: dstRect.origin.x yBy: dstRect.origin.y];
|
|
||||||
[transform scaleXBy: dstRect.size.width / srcRectInCache.size.width
|
|
||||||
yBy: dstRect.size.height / srcRectInCache.size.height];
|
|
||||||
[transform concat];
|
|
||||||
|
|
||||||
[ctxt GSdraw: gState
|
|
||||||
toPoint: NSMakePoint(0,0)
|
|
||||||
fromRect: srcRectInCache
|
|
||||||
operation: op
|
|
||||||
fraction: delta];
|
|
||||||
|
|
||||||
[ctxt GSSetCTM: backup];
|
|
||||||
|
|
||||||
[ctxt GSUndefineGState: gState];
|
|
||||||
DESTROY(cache);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Old code path that can probably partially be merged with the new native implementation.
|
|
||||||
Fallback for backends other than Cairo. */
|
|
||||||
- (void) guiDrawInRect: (NSRect)dstRect
|
|
||||||
fromRect: (NSRect)srcRect
|
|
||||||
operation: (NSCompositingOperation)op
|
|
||||||
fraction: (float)delta
|
|
||||||
{
|
|
||||||
NSGraphicsContext *ctxt = GSCurrentContext();
|
|
||||||
NSAffineTransform *transform;
|
|
||||||
NSSize s;
|
|
||||||
NSImageRep *rep;
|
|
||||||
|
|
||||||
s = [self size];
|
|
||||||
|
|
||||||
if (NSEqualRects(srcRect, NSZeroRect))
|
|
||||||
{
|
|
||||||
srcRect.size = s;
|
|
||||||
/* For -drawAtPoint:fromRect:operation:fraction: used with a zero rect */
|
|
||||||
if (NSEqualSizes(dstRect.size, NSZeroSize))
|
|
||||||
{
|
|
||||||
dstRect.size = s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Choose a rep to use
|
|
||||||
|
|
||||||
rep = [self bestRepresentationForRect: dstRect
|
|
||||||
context: nil
|
|
||||||
hints: nil];
|
|
||||||
|
|
||||||
if (rep == nil)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!dstRect.size.width || !dstRect.size.height
|
|
||||||
|| !srcRect.size.width || !srcRect.size.height)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// CLip to image bounds
|
|
||||||
if (srcRect.origin.x < 0)
|
|
||||||
srcRect.origin.x = 0;
|
|
||||||
if (srcRect.origin.y < 0)
|
|
||||||
srcRect.origin.y = 0;
|
|
||||||
if (NSMaxX(srcRect) > s.width)
|
|
||||||
srcRect.size.width = s.width - srcRect.origin.x;
|
|
||||||
if (NSMaxY(srcRect) > s.height)
|
|
||||||
srcRect.size.height = s.height - srcRect.origin.y;
|
|
||||||
|
|
||||||
if (![ctxt isDrawingToScreen])
|
|
||||||
{
|
|
||||||
/* We can't composite or dissolve if we aren't drawing to a screen,
|
|
||||||
so we'll just draw the right part of the image in the right
|
|
||||||
place. */
|
|
||||||
NSPoint p;
|
|
||||||
double fx, fy;
|
|
||||||
|
|
||||||
fx = dstRect.size.width / srcRect.size.width;
|
|
||||||
fy = dstRect.size.height / srcRect.size.height;
|
|
||||||
|
|
||||||
p.x = dstRect.origin.x / fx - srcRect.origin.x;
|
|
||||||
p.y = dstRect.origin.y / fy - srcRect.origin.y;
|
|
||||||
|
|
||||||
DPSgsave(ctxt);
|
|
||||||
DPSrectclip(ctxt, dstRect.origin.x, dstRect.origin.y,
|
|
||||||
dstRect.size.width, dstRect.size.height);
|
|
||||||
DPSscale(ctxt, fx, fy);
|
|
||||||
[self drawRepresentation: rep
|
|
||||||
inRect: NSMakeRect(p.x, p.y, s.width, s.height)];
|
|
||||||
DPSgrestore(ctxt);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Figure out what the effective transform from image space to
|
|
||||||
'window space' is. */
|
|
||||||
transform = [ctxt GSCurrentCTM];
|
|
||||||
|
|
||||||
[transform scaleXBy: dstRect.size.width / srcRect.size.width
|
|
||||||
yBy: dstRect.size.height / srcRect.size.height];
|
|
||||||
|
|
||||||
/* We can't composite or dissolve directly from the image reps, so we
|
|
||||||
create a temporary off-screen window large enough to hold the
|
|
||||||
transformed image, draw the image rep there, and composite from there
|
|
||||||
to the destination.
|
|
||||||
|
|
||||||
Optimization: Since we do the entire image at once, we might need a
|
|
||||||
huge buffer. If this starts hurting too much, there are a couple of
|
|
||||||
things we could do to:
|
|
||||||
|
|
||||||
1. Take srcRect into account and only process the parts of the image
|
|
||||||
we really need.
|
|
||||||
2. Take the clipping path into account. Desirable, especially if we're
|
|
||||||
being drawn as lots of small strips in a scrollview. We don't have
|
|
||||||
the clipping path here, though.
|
|
||||||
3. Allocate a permanent but small buffer and process the image
|
|
||||||
piecewise.
|
|
||||||
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
NSCachedImageRep *cache;
|
|
||||||
NSAffineTransformStruct ts;
|
|
||||||
NSPoint p;
|
|
||||||
double x0, y0, x1, y1, w, h;
|
|
||||||
int gState;
|
|
||||||
NSGraphicsContext *ctxt1;
|
|
||||||
|
|
||||||
/* Figure out how big we need to make the window that'll hold the
|
|
||||||
transformed image. */
|
|
||||||
p = [transform transformPoint: NSMakePoint(0, s.height)];
|
|
||||||
x0 = x1 = p.x;
|
|
||||||
y0 = y1 = p.y;
|
|
||||||
|
|
||||||
p = [transform transformPoint: NSMakePoint(s.width, 0)];
|
|
||||||
x0 = gs_min(x0, p.x);
|
|
||||||
y0 = gs_min(y0, p.y);
|
|
||||||
x1 = gs_max(x1, p.x);
|
|
||||||
y1 = gs_max(y1, p.y);
|
|
||||||
|
|
||||||
p = [transform transformPoint: NSMakePoint(s.width, s.height)];
|
|
||||||
x0 = gs_min(x0, p.x);
|
|
||||||
y0 = gs_min(y0, p.y);
|
|
||||||
x1 = gs_max(x1, p.x);
|
|
||||||
y1 = gs_max(y1, p.y);
|
|
||||||
|
|
||||||
p = [transform transformPoint: NSMakePoint(0, 0)];
|
|
||||||
x0 = gs_min(x0, p.x);
|
|
||||||
y0 = gs_min(y0, p.y);
|
|
||||||
x1 = gs_max(x1, p.x);
|
|
||||||
y1 = gs_max(y1, p.y);
|
|
||||||
|
|
||||||
x0 = floor(x0);
|
|
||||||
y0 = floor(y0);
|
|
||||||
x1 = ceil(x1);
|
|
||||||
y1 = ceil(y1);
|
|
||||||
|
|
||||||
w = x1 - x0;
|
|
||||||
h = y1 - y0;
|
|
||||||
|
|
||||||
/* This is where we want the origin of image space to be in our
|
|
||||||
window. */
|
|
||||||
p.x -= x0;
|
|
||||||
p.y -= y0;
|
|
||||||
|
|
||||||
cache = [[NSCachedImageRep alloc]
|
|
||||||
initWithSize: NSMakeSize(w, h)
|
|
||||||
depth: [[NSScreen mainScreen] depth]
|
|
||||||
separate: YES
|
|
||||||
alpha: YES];
|
|
||||||
|
|
||||||
[[[cache window] contentView] lockFocus];
|
|
||||||
// The context of the cache window
|
|
||||||
ctxt1 = GSCurrentContext();
|
|
||||||
DPScompositerect(ctxt1, 0, 0, w, h, NSCompositeClear);
|
|
||||||
|
|
||||||
/* Set up the effective transform. We also save a gState with this
|
|
||||||
transform to make it easier to do the final composite. */
|
|
||||||
ts = [transform transformStruct];
|
|
||||||
ts.tX = p.x;
|
|
||||||
ts.tY = p.y;
|
|
||||||
[transform setTransformStruct: ts];
|
|
||||||
[ctxt1 GSSetCTM: transform];
|
|
||||||
gState = [ctxt1 GSDefineGState];
|
|
||||||
|
|
||||||
|
|
||||||
/* We must not use -drawRepresentation:inRect: because the image must drawn
|
|
||||||
scaled even when -scalesWhenResized is NO */
|
|
||||||
|
|
||||||
// FIXME: should the background color be filled here?
|
|
||||||
// If I don't I get black backgrounds on images with xlib; maybe an xlib backend bug
|
|
||||||
PSgsave();
|
|
||||||
if (_color != nil)
|
|
||||||
{
|
|
||||||
[_color set];
|
|
||||||
NSRectFill(NSMakeRect(0, 0, s.width, s.height));
|
|
||||||
}
|
|
||||||
[rep drawInRect: NSMakeRect(0, 0, s.width, s.height)];
|
|
||||||
PSgrestore();
|
|
||||||
|
|
||||||
/* If we're doing a dissolve, use a DestinationIn composite to lower
|
|
||||||
the alpha of the pixels. */
|
|
||||||
if (delta != 1.0)
|
|
||||||
{
|
|
||||||
DPSsetalpha(ctxt1, delta);
|
|
||||||
DPScompositerect(ctxt1, 0, 0, s.width, s.height,
|
|
||||||
NSCompositeDestinationIn);
|
|
||||||
}
|
|
||||||
|
|
||||||
[[[cache window] contentView] unlockFocus];
|
|
||||||
|
|
||||||
|
|
||||||
DPScomposite(ctxt, srcRect.origin.x, srcRect.origin.y,
|
|
||||||
srcRect.size.width, srcRect.size.height, gState,
|
|
||||||
dstRect.origin.x, dstRect.origin.y, op);
|
|
||||||
|
|
||||||
[ctxt GSUndefineGState: gState];
|
|
||||||
|
|
||||||
DESTROY(cache);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) drawInRect: (NSRect)dstRect
|
- (void) drawInRect: (NSRect)dstRect
|
||||||
|
@ -1313,57 +914,78 @@ Fallback for backends other than Cairo. */
|
||||||
operation: (NSCompositingOperation)op
|
operation: (NSCompositingOperation)op
|
||||||
fraction: (float)delta
|
fraction: (float)delta
|
||||||
{
|
{
|
||||||
if ([GSCurrentContext() supportsDrawGState])
|
[self drawInRect: dstRect
|
||||||
{
|
fromRect: srcRect
|
||||||
[self nativeDrawInRect: dstRect fromRect: srcRect operation: op fraction: delta];
|
operation: op
|
||||||
}
|
fraction: delta
|
||||||
else
|
respectFlipped: NO
|
||||||
{
|
hints: nil];
|
||||||
[self guiDrawInRect: dstRect fromRect: srcRect operation: op fraction: delta];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) drawInRect: (NSRect)dstRect
|
/**
|
||||||
|
* Base drawing method in NSImage; all other draw methods call this one
|
||||||
|
*/
|
||||||
|
- (void) drawInRect: (NSRect)dstRect // Negative width/height => Nothing draws.
|
||||||
fromRect: (NSRect)srcRect
|
fromRect: (NSRect)srcRect
|
||||||
operation: (NSCompositingOperation)op
|
operation: (NSCompositingOperation)op
|
||||||
fraction: (float)delta
|
fraction: (float)delta
|
||||||
respectFlipped: (BOOL)respectFlipped
|
respectFlipped: (BOOL)respectFlipped
|
||||||
hints: (NSDictionary*)hints
|
hints: (NSDictionary*)hints
|
||||||
{
|
{
|
||||||
NSAffineTransform *backup = nil;
|
NSImageRep *rep;
|
||||||
NSGraphicsContext *ctx = GSCurrentContext();
|
NSGraphicsContext *ctxt;
|
||||||
BOOL compensateForFlip = (respectFlipped && [ctx isFlipped]);
|
NSSize imgSize, repSize;
|
||||||
|
NSRect repSrcRect;
|
||||||
|
|
||||||
// FIXME: Hints are currently ignored
|
ctxt = GSCurrentContext();
|
||||||
|
imgSize = [self size];
|
||||||
|
|
||||||
if (compensateForFlip)
|
// Handle abbreviated parameters
|
||||||
|
|
||||||
|
if (NSEqualRects(srcRect, NSZeroRect))
|
||||||
{
|
{
|
||||||
CGFloat height;
|
srcRect.size = imgSize;
|
||||||
NSAffineTransform *newXform;
|
}
|
||||||
|
if (NSEqualSizes(dstRect.size, NSZeroSize)) // For -drawAtPoint:fromRect:operation:fraction:
|
||||||
height = dstRect.size.height != 0 ?
|
{
|
||||||
dstRect.size.height : [self size].height;
|
dstRect.size = imgSize;
|
||||||
|
|
||||||
backup = [ctx GSCurrentCTM];
|
|
||||||
|
|
||||||
newXform = [backup copy];
|
|
||||||
[newXform translateXBy: dstRect.origin.x yBy: dstRect.origin.y + height];
|
|
||||||
[newXform scaleXBy: 1 yBy: -1];
|
|
||||||
[ctx GSSetCTM: newXform];
|
|
||||||
[newXform release];
|
|
||||||
|
|
||||||
dstRect.origin = NSMakePoint(0, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[self drawInRect: dstRect
|
if (imgSize.width <= 0 || imgSize.height <= 0)
|
||||||
fromRect: srcRect
|
return;
|
||||||
operation: op
|
|
||||||
fraction: delta];
|
|
||||||
|
|
||||||
if (compensateForFlip)
|
// Select a rep
|
||||||
{
|
|
||||||
[ctx GSSetCTM: backup];
|
rep = [self bestRepresentationForRect: dstRect
|
||||||
}
|
context: ctxt
|
||||||
|
hints: hints];
|
||||||
|
if (rep == nil)
|
||||||
|
return;
|
||||||
|
|
||||||
|
repSize = [rep size];
|
||||||
|
|
||||||
|
// Convert srcRect from image coordinate space to rep coordinate space
|
||||||
|
{
|
||||||
|
const CGFloat imgToRepWidthScaleFactor = repSize.width / imgSize.width;
|
||||||
|
const CGFloat imgToRepHeightScaleFactor = repSize.height / imgSize.height;
|
||||||
|
|
||||||
|
repSrcRect = NSMakeRect(srcRect.origin.x * imgToRepWidthScaleFactor,
|
||||||
|
srcRect.origin.y * imgToRepHeightScaleFactor,
|
||||||
|
srcRect.size.width * imgToRepWidthScaleFactor,
|
||||||
|
srcRect.size.height * imgToRepHeightScaleFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Insert caching code here which gets the cached
|
||||||
|
// copy of rep and draws from that, if caching is enabled.
|
||||||
|
|
||||||
|
// FIXME: Draw background?
|
||||||
|
|
||||||
|
[rep drawInRect: dstRect
|
||||||
|
fromRect: repSrcRect
|
||||||
|
operation: op
|
||||||
|
fraction: delta
|
||||||
|
respectFlipped: respectFlipped
|
||||||
|
hints: hints];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) addRepresentation: (NSImageRep *)imageRep
|
- (void) addRepresentation: (NSImageRep *)imageRep
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#import <Foundation/NSAffineTransform.h>
|
||||||
#import <Foundation/NSArray.h>
|
#import <Foundation/NSArray.h>
|
||||||
#import <Foundation/NSData.h>
|
#import <Foundation/NSData.h>
|
||||||
#import <Foundation/NSValue.h>
|
#import <Foundation/NSValue.h>
|
||||||
|
@ -36,14 +37,19 @@
|
||||||
#import <Foundation/NSNotification.h>
|
#import <Foundation/NSNotification.h>
|
||||||
#import <Foundation/NSUserDefaults.h>
|
#import <Foundation/NSUserDefaults.h>
|
||||||
#import <Foundation/NSDebug.h>
|
#import <Foundation/NSDebug.h>
|
||||||
|
#import "AppKit/NSAffineTransform.h"
|
||||||
#import "AppKit/NSImageRep.h"
|
#import "AppKit/NSImageRep.h"
|
||||||
#import "AppKit/NSBitmapImageRep.h"
|
#import "AppKit/NSBitmapImageRep.h"
|
||||||
|
#import "AppKit/NSCachedImageRep.h"
|
||||||
#import "AppKit/NSEPSImageRep.h"
|
#import "AppKit/NSEPSImageRep.h"
|
||||||
#import "AppKit/NSPasteboard.h"
|
#import "AppKit/NSPasteboard.h"
|
||||||
#import "AppKit/NSGraphicsContext.h"
|
#import "AppKit/NSGraphicsContext.h"
|
||||||
#import "AppKit/NSView.h"
|
#import "AppKit/NSView.h"
|
||||||
#import "AppKit/NSColor.h"
|
#import "AppKit/NSColor.h"
|
||||||
|
#import "AppKit/NSWindow.h"
|
||||||
|
#import "AppKit/NSScreen.h"
|
||||||
#import "AppKit/DPSOperators.h"
|
#import "AppKit/DPSOperators.h"
|
||||||
|
#import "AppKit/PSOperators.h"
|
||||||
#import "GNUstepGUI/GSGhostscriptImageRep.h"
|
#import "GNUstepGUI/GSGhostscriptImageRep.h"
|
||||||
#import "GNUstepGUI/GSImageMagickImageRep.h"
|
#import "GNUstepGUI/GSImageMagickImageRep.h"
|
||||||
|
|
||||||
|
@ -491,6 +497,316 @@ implement, so we can't do that. */
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* New code path that delegates as much as possible to the backend and whose
|
||||||
|
behavior precisely matches Cocoa. */
|
||||||
|
- (void) nativeDrawInRect: (NSRect)dstRect
|
||||||
|
fromRect: (NSRect)srcRect
|
||||||
|
operation: (NSCompositingOperation)op
|
||||||
|
fraction: (float)delta
|
||||||
|
{
|
||||||
|
NSGraphicsContext *ctxt = GSCurrentContext();
|
||||||
|
/* An intermediate image used to scale the image to be drawn as needed */
|
||||||
|
NSCachedImageRep *cache;
|
||||||
|
/* The scaled image graphics state we used as the source from which we
|
||||||
|
draw into the destination (the current graphics context)*/
|
||||||
|
int gState;
|
||||||
|
/* The context of the cache window */
|
||||||
|
NSGraphicsContext *cacheCtxt;
|
||||||
|
const NSSize repSize = [self size];
|
||||||
|
/* The size of the cache window */
|
||||||
|
NSSize cacheSize;
|
||||||
|
|
||||||
|
CGFloat repToCacheWidthScaleFactor;
|
||||||
|
CGFloat repToCacheHeightScaleFactor;
|
||||||
|
|
||||||
|
NSRect srcRectInCache;
|
||||||
|
NSAffineTransform *transform, *backup;
|
||||||
|
|
||||||
|
// FIXME: Revisit this calculation of cache size
|
||||||
|
|
||||||
|
if (([self pixelsWide] == NSImageRepMatchesDevice &&
|
||||||
|
[self pixelsHigh] == NSImageRepMatchesDevice) &&
|
||||||
|
(dstRect.size.width > repSize.width ||
|
||||||
|
dstRect.size.height > repSize.height))
|
||||||
|
{
|
||||||
|
cacheSize = [[ctxt GSCurrentCTM] transformSize: dstRect.size];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cacheSize = [[ctxt GSCurrentCTM] transformSize: repSize];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cacheSize.width < 0)
|
||||||
|
cacheSize.width *= -1;
|
||||||
|
if (cacheSize.height < 0)
|
||||||
|
cacheSize.height *= -1;
|
||||||
|
|
||||||
|
repToCacheWidthScaleFactor = cacheSize.width / repSize.width;
|
||||||
|
repToCacheHeightScaleFactor = cacheSize.height / repSize.height;
|
||||||
|
|
||||||
|
srcRectInCache = NSMakeRect(srcRect.origin.x * repToCacheWidthScaleFactor,
|
||||||
|
srcRect.origin.y * repToCacheHeightScaleFactor,
|
||||||
|
srcRect.size.width * repToCacheWidthScaleFactor,
|
||||||
|
srcRect.size.height * repToCacheHeightScaleFactor);
|
||||||
|
|
||||||
|
cache = [[NSCachedImageRep alloc]
|
||||||
|
initWithSize: NSMakeSize(ceil(cacheSize.width), ceil(cacheSize.height))
|
||||||
|
depth: [[NSScreen mainScreen] depth]
|
||||||
|
separate: YES
|
||||||
|
alpha: YES];
|
||||||
|
|
||||||
|
[[[cache window] contentView] lockFocus];
|
||||||
|
cacheCtxt = GSCurrentContext();
|
||||||
|
|
||||||
|
/* Clear the cache window surface */
|
||||||
|
DPScompositerect(cacheCtxt, 0, 0, ceil(cacheSize.width), ceil(cacheSize.height), NSCompositeClear);
|
||||||
|
gState = [cacheCtxt GSDefineGState];
|
||||||
|
|
||||||
|
//NSLog(@"Draw in cache size %@", NSStringFromSize(cacheSize));
|
||||||
|
|
||||||
|
[self drawInRect: NSMakeRect(0, 0, cacheSize.width, cacheSize.height)];
|
||||||
|
|
||||||
|
[[[cache window] contentView] unlockFocus];
|
||||||
|
|
||||||
|
//NSLog(@"Draw in %@ from %@ from cache rect %@", NSStringFromRect(dstRect),
|
||||||
|
// NSStringFromRect(srcRect), NSStringFromRect(srcRectInCache));
|
||||||
|
|
||||||
|
backup = [ctxt GSCurrentCTM];
|
||||||
|
|
||||||
|
transform = [NSAffineTransform transform];
|
||||||
|
[transform translateXBy: dstRect.origin.x yBy: dstRect.origin.y];
|
||||||
|
[transform scaleXBy: dstRect.size.width / srcRectInCache.size.width
|
||||||
|
yBy: dstRect.size.height / srcRectInCache.size.height];
|
||||||
|
[transform concat];
|
||||||
|
|
||||||
|
[ctxt GSdraw: gState
|
||||||
|
toPoint: NSMakePoint(0,0)
|
||||||
|
fromRect: srcRectInCache
|
||||||
|
operation: op
|
||||||
|
fraction: delta];
|
||||||
|
|
||||||
|
[ctxt GSSetCTM: backup];
|
||||||
|
|
||||||
|
[ctxt GSUndefineGState: gState];
|
||||||
|
DESTROY(cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Old code path that can probably partially be merged with the new native implementation.
|
||||||
|
Fallback for backends other than Cairo. */
|
||||||
|
- (void) guiDrawInRect: (NSRect)dstRect
|
||||||
|
fromRect: (NSRect)srcRect
|
||||||
|
operation: (NSCompositingOperation)op
|
||||||
|
fraction: (float)delta
|
||||||
|
{
|
||||||
|
NSGraphicsContext *ctxt = GSCurrentContext();
|
||||||
|
NSAffineTransform *transform;
|
||||||
|
NSSize repSize;
|
||||||
|
|
||||||
|
repSize = [self size];
|
||||||
|
|
||||||
|
/* Figure out what the effective transform from rep space to
|
||||||
|
'window space' is. */
|
||||||
|
transform = [ctxt GSCurrentCTM];
|
||||||
|
|
||||||
|
[transform scaleXBy: dstRect.size.width / srcRect.size.width
|
||||||
|
yBy: dstRect.size.height / srcRect.size.height];
|
||||||
|
|
||||||
|
/* We can't composite or dissolve directly from the image reps, so we
|
||||||
|
create a temporary off-screen window large enough to hold the
|
||||||
|
transformed image, draw the image rep there, and composite from there
|
||||||
|
to the destination.
|
||||||
|
|
||||||
|
Optimization: Since we do the entire image at once, we might need a
|
||||||
|
huge buffer. If this starts hurting too much, there are a couple of
|
||||||
|
things we could do to:
|
||||||
|
|
||||||
|
|
||||||
|
1. Take srcRect into account and only process the parts of the image
|
||||||
|
we really need.
|
||||||
|
2. Take the clipping path into account. Desirable, especially if we're
|
||||||
|
being drawn as lots of small strips in a scrollview. We don't have
|
||||||
|
the clipping path here, though.
|
||||||
|
3. Allocate a permanent but small buffer and process the image
|
||||||
|
piecewise.
|
||||||
|
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
NSCachedImageRep *cache;
|
||||||
|
NSAffineTransformStruct ts;
|
||||||
|
NSPoint p;
|
||||||
|
double x0, y0, x1, y1, w, h;
|
||||||
|
int gState;
|
||||||
|
NSGraphicsContext *ctxt1;
|
||||||
|
|
||||||
|
/* Figure out how big we need to make the window that'll hold the
|
||||||
|
transformed image. */
|
||||||
|
p = [transform transformPoint: NSMakePoint(0, repSize.height)];
|
||||||
|
x0 = x1 = p.x;
|
||||||
|
y0 = y1 = p.y;
|
||||||
|
|
||||||
|
p = [transform transformPoint: NSMakePoint(repSize.width, 0)];
|
||||||
|
x0 = MIN(x0, p.x);
|
||||||
|
y0 = MIN(y0, p.y);
|
||||||
|
x1 = MAX(x1, p.x);
|
||||||
|
y1 = MAX(y1, p.y);
|
||||||
|
|
||||||
|
p = [transform transformPoint: NSMakePoint(repSize.width, repSize.height)];
|
||||||
|
x0 = MIN(x0, p.x);
|
||||||
|
y0 = MIN(y0, p.y);
|
||||||
|
x1 = MAX(x1, p.x);
|
||||||
|
y1 = MAX(y1, p.y);
|
||||||
|
|
||||||
|
p = [transform transformPoint: NSMakePoint(0, 0)];
|
||||||
|
x0 = MIN(x0, p.x);
|
||||||
|
y0 = MIN(y0, p.y);
|
||||||
|
x1 = MAX(x1, p.x);
|
||||||
|
y1 = MAX(y1, p.y);
|
||||||
|
|
||||||
|
x0 = floor(x0);
|
||||||
|
y0 = floor(y0);
|
||||||
|
x1 = ceil(x1);
|
||||||
|
y1 = ceil(y1);
|
||||||
|
|
||||||
|
w = x1 - x0;
|
||||||
|
h = y1 - y0;
|
||||||
|
|
||||||
|
/* This is where we want the origin of image space to be in our
|
||||||
|
window. */
|
||||||
|
p.x -= x0;
|
||||||
|
p.y -= y0;
|
||||||
|
|
||||||
|
cache = [[NSCachedImageRep alloc]
|
||||||
|
initWithSize: NSMakeSize(w, h)
|
||||||
|
depth: [[NSScreen mainScreen] depth]
|
||||||
|
separate: YES
|
||||||
|
alpha: YES];
|
||||||
|
|
||||||
|
[[[cache window] contentView] lockFocus];
|
||||||
|
// The context of the cache window
|
||||||
|
ctxt1 = GSCurrentContext();
|
||||||
|
DPScompositerect(ctxt1, 0, 0, w, h, NSCompositeClear);
|
||||||
|
|
||||||
|
/* Set up the effective transform. We also save a gState with this
|
||||||
|
transform to make it easier to do the final composite. */
|
||||||
|
ts = [transform transformStruct];
|
||||||
|
ts.tX = p.x;
|
||||||
|
ts.tY = p.y;
|
||||||
|
[transform setTransformStruct: ts];
|
||||||
|
[ctxt1 GSSetCTM: transform];
|
||||||
|
gState = [ctxt1 GSDefineGState];
|
||||||
|
|
||||||
|
{
|
||||||
|
// Hack for xlib. Without it, transparent parts of images are black
|
||||||
|
[[NSColor clearColor] set];
|
||||||
|
NSRectFill(NSMakeRect(0, 0, repSize.width, repSize.height));
|
||||||
|
}
|
||||||
|
|
||||||
|
[self drawInRect: NSMakeRect(0, 0, repSize.width, repSize.height)];
|
||||||
|
|
||||||
|
/* If we're doing a dissolve, use a DestinationIn composite to lower
|
||||||
|
the alpha of the pixels. */
|
||||||
|
if (delta != 1.0)
|
||||||
|
{
|
||||||
|
DPSsetalpha(ctxt1, delta);
|
||||||
|
DPScompositerect(ctxt1, 0, 0, repSize.width, repSize.height,
|
||||||
|
NSCompositeDestinationIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
[[[cache window] contentView] unlockFocus];
|
||||||
|
|
||||||
|
DPScomposite(ctxt, srcRect.origin.x, srcRect.origin.y,
|
||||||
|
srcRect.size.width, srcRect.size.height, gState,
|
||||||
|
dstRect.origin.x, dstRect.origin.y, op);
|
||||||
|
|
||||||
|
[ctxt GSUndefineGState: gState];
|
||||||
|
|
||||||
|
DESTROY(cache);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fallback implementation for subclasses which don't implement their
|
||||||
|
* own direct drawing
|
||||||
|
* TODO: explain how -draw, -drawInRect:, -drawAtPoint: clear their background
|
||||||
|
*/
|
||||||
|
- (BOOL) drawInRect: (NSRect)dstRect
|
||||||
|
fromRect: (NSRect)srcRect
|
||||||
|
operation: (NSCompositingOperation)op
|
||||||
|
fraction: (float)delta
|
||||||
|
respectFlipped: (BOOL)respectFlipped
|
||||||
|
hints: (NSDictionary*)hints
|
||||||
|
{
|
||||||
|
NSAffineTransform *backup = nil;
|
||||||
|
NSGraphicsContext *ctx = GSCurrentContext();
|
||||||
|
const BOOL compensateForFlip = (respectFlipped && [ctx isFlipped]);
|
||||||
|
const NSSize repSize = [self size];
|
||||||
|
|
||||||
|
// Handle abbreviated parameters
|
||||||
|
|
||||||
|
if (NSEqualRects(srcRect, NSZeroRect))
|
||||||
|
{
|
||||||
|
srcRect.size = repSize;
|
||||||
|
}
|
||||||
|
if (NSEqualSizes(dstRect.size, NSZeroSize))
|
||||||
|
{
|
||||||
|
dstRect.size = repSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dstRect.size.width <= 0 || dstRect.size.height <= 0
|
||||||
|
|| srcRect.size.width <= 0 || srcRect.size.height <= 0)
|
||||||
|
return NO;
|
||||||
|
|
||||||
|
// Clip to image bounds
|
||||||
|
|
||||||
|
if (srcRect.origin.x < 0)
|
||||||
|
srcRect.origin.x = 0;
|
||||||
|
if (srcRect.origin.y < 0)
|
||||||
|
srcRect.origin.y = 0;
|
||||||
|
if (NSMaxX(srcRect) > repSize.width)
|
||||||
|
srcRect.size.width = repSize.width - srcRect.origin.x;
|
||||||
|
if (NSMaxY(srcRect) > repSize.height)
|
||||||
|
srcRect.size.height = repSize.height - srcRect.origin.y;
|
||||||
|
|
||||||
|
// FIXME: Hints are currently ignored
|
||||||
|
|
||||||
|
// Compensate for flip
|
||||||
|
|
||||||
|
if (compensateForFlip)
|
||||||
|
{
|
||||||
|
NSAffineTransform *newXform;
|
||||||
|
|
||||||
|
backup = [ctx GSCurrentCTM];
|
||||||
|
|
||||||
|
newXform = [backup copy];
|
||||||
|
[newXform translateXBy: dstRect.origin.x yBy: dstRect.origin.y + dstRect.size.height];
|
||||||
|
[newXform scaleXBy: 1 yBy: -1];
|
||||||
|
[ctx GSSetCTM: newXform];
|
||||||
|
[newXform release];
|
||||||
|
|
||||||
|
dstRect.origin = NSMakePoint(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw
|
||||||
|
|
||||||
|
if ([ctx supportsDrawGState])
|
||||||
|
{
|
||||||
|
[self nativeDrawInRect: dstRect fromRect: srcRect operation: op fraction: delta];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
[self guiDrawInRect: dstRect fromRect: srcRect operation: op fraction: delta];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Undo flip compensation
|
||||||
|
|
||||||
|
if (compensateForFlip)
|
||||||
|
{
|
||||||
|
[ctx GSSetCTM: backup];
|
||||||
|
}
|
||||||
|
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
// NSCopying protocol
|
// NSCopying protocol
|
||||||
- (id) copyWithZone: (NSZone *)zone
|
- (id) copyWithZone: (NSZone *)zone
|
||||||
{
|
{
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue