Redraw NSBezierPath multiple times, with scaling? - objective-c

I am currently writing many chunks of text on an NSView using the NSString helper method ... however it is very slow to write a large amount of in many cases repeated text. I am trying to re-write the code so that the text is converted to NSBezierPath's that are generated once, then drawn many times. The following will draw text at the bottom of the screen.
I am still trying to read through the apple documentation to understand how this works, but in the mean time I wonder if there is an easy way to alter this code to re-draw the path in multiple locations?
// Write a path to the view
NSBezierPath* path = [self bezierPathFromText: #"Hello world!" maxWidth: width];
[[NSColor grayColor] setFill];
[path fill];
Here is the method that writes some text into a path:
-(NSBezierPath*) bezierPathFromText: (NSString*) text maxWidth: (float) maxWidth {
// Create a container describing the shape of the text area,
// for testing done use the whole width of the NSView.
NSTextContainer* container = [[NSTextContainer alloc] initWithContainerSize:NSMakeSize(maxWidth - maxWidth/4, 60)];
// Create a storage object to hold an attributed version of the string to display
NSFont* font = [NSFont fontWithName:#"Helvetica" size: 26];
NSDictionary* attr = [NSDictionary dictionaryWithObjectsAndKeys: font, NSFontAttributeName, nil];
NSTextStorage* storage = [[NSTextStorage alloc] initWithString: text attributes: attr];
// Create a layout manager responsible for writing the text to the NSView
NSLayoutManager* layoutManger = [[NSLayoutManager alloc] init];
[layoutManger addTextContainer: container];
[layoutManger setTextStorage: storage];
NSRange glyphRange = [layoutManger glyphRangeForTextContainer: container];
NSGlyph glyphArray[glyphRange.length];
NSUInteger glyphCount = [layoutManger getGlyphs:glyphArray range:glyphRange];
NSBezierPath* path = [[NSBezierPath alloc] init];
//NSBezierPath *path = [NSBezierPath bezierPathWithRect:NSMakeRect(0, 0, 30, 30)];
[path moveToPoint: NSMakePoint(0, 7)];
[path appendBezierPathWithGlyphs:glyphArray count: glyphCount inFont:font];
// Deallocate unused objects
[layoutManger release];
[storage release];
[container release];
return [path autorelease];
}
Edit: I am attempting to optimise an application that outputs to screen, a sequence of large amounts text such as a sequence of 10,000 numbers. Each number has markings around it and/or differing amounts of space between them, and some numbers have dots and/or lines above, below or between them. Its like the example at the top of page two of this document but with much much more output.

You could start by removing the line:
[path moveToPoint: NSMakePoint(0, 7)];
so that your path isn't tied to a particular location in the view. With that done, you can call your method to get the path, move to a point, stroke the path, move to another point, stroke the path, and so on. If you want to move from the starting point within the path description, use -relativeMoveToPoint:.

It appears this might do the trick, however I am not sure if it is the best way to do it?
// Create the path
NSBezierPath* path = [self bezierPathFromText: #"Fish are fun to watch in a fish tank, but not fun to eat, or something like that." maxWidth: width];
// Draw a copy of it at a transformed (moved) location
NSAffineTransform* transform = [[NSAffineTransform alloc] init];
[transform translateXBy: 10 yBy: 10];
NSBezierPath* path2 = [path copy];
[path2 transformUsingAffineTransform: transform];
[[NSColor greenColor] setFill];
[path2 fill];
[path2 release];
[transform release];
[path2 release];
// Draw another copy of it at a transformed (moved) location
transform = [[NSAffineTransform alloc] init];
[transform translateXBy: 10 yBy: 40];
path2 = [path copy];
[path2 transformUsingAffineTransform: transform];
[[NSColor greenColor] setFill];
[path2 fill];
[path2 release];
[transform release];

Related

Draw Text with a border and background for a NSTableView cell

I have a cell-based tableview and I want to display some sort of tags within this tableview, preferably without having to use a view-based tableview.
Is there an elegant way to achieve something like the example here (HTML), ideally with a background-color as well.
If you want to stick with a cell-based table view, you can subclass NSCell and override:
- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView
{
NSRect insetRect = NSInsetRect(cellFrame, 2.0, 2.0);
NSBezierPath* path = [NSBezierPath bezierPathWithRoundedRect:insetRect xRadius:3.0 yRadius:3.0];
[path stroke];
[[NSColor whiteColor] setFill];
[path fill];
[[NSColor brownColor] setStroke];
[path stroke];
NSMutableAttributedString* content = [[NSMutableAttributedString alloc] initWithString:#"DUPLICATE"];
NSFontManager* fontManager = [NSFontManager sharedFontManager];
NSFont* font = [fontManager fontWithFamily:#"Verdana"
traits:NSBoldFontMask
weight:0
size:13.0];
NSDictionary* attributes = #{NSFontAttributeName:font,
NSForegroundColorAttributeName:[NSColor brownColor]};
[content setAttributes:attributes range:NSMakeRange(0, [content length])];
[content setAlignment:NSCenterTextAlignment range:NSMakeRange(0, [content length])];
[content drawInRect:cellFrame];
}
The above code generates a cell that vaguely resembles your button (you'll have to tweak fonts, colours, line-styles etc. yourself):
I also adjusted the row height in the table view delegate by providing:
- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row
{
return 24.0;
}

Scale Up NSImage and Save

I would like to scale up an image that's 64px to make it 512px (Even if it's blurry or pixelized)
I'm using this to get the image from my NSImageView and save it:
NSData *customimageData = [[customIcon image] TIFFRepresentation];
NSBitmapImageRep *customimageRep = [NSBitmapImageRep imageRepWithData:customimageData];
customimageData = [customimageRep representationUsingType:NSPNGFileType properties:nil];
NSString* customBundlePath = [[NSBundle mainBundle] pathForResource:#"customIcon" ofType:#"png"];
[customimageData writeToFile:customBundlePath atomically:YES];
I've tried setSize: but it still saves it 64px.
Thanks in advance!
You can't use the NSImage's size property as it bears only an indirect relationship to the pixel dimensions of an image representation. A good way to resize pixel dimensions is to use the drawInRect method of NSImageRep:
- (BOOL)drawInRect:(NSRect)rect
Draws the entire image in the specified rectangle, scaling it as needed to fit.
Here is a image resize method (creates a new NSImage at the pixel size you want).
- (NSImage*) resizeImage:(NSImage*)sourceImage size:(NSSize)size
{
NSRect targetFrame = NSMakeRect(0, 0, size.width, size.height);
NSImage* targetImage = nil;
NSImageRep *sourceImageRep =
[sourceImage bestRepresentationForRect:targetFrame
context:nil
hints:nil];
targetImage = [[NSImage alloc] initWithSize:size];
[targetImage lockFocus];
[sourceImageRep drawInRect: targetFrame];
[targetImage unlockFocus];
return targetImage;
}
It's from a more detailed answer I gave here: NSImage doesn't scale
Another resize method that works is the NSImage method drawInRect:fromRect:operation:fraction:respectFlipped:hints
- (void)drawInRect:(NSRect)dstSpacePortionRect
fromRect:(NSRect)srcSpacePortionRect
operation:(NSCompositingOperation)op
fraction:(CGFloat)requestedAlpha
respectFlipped:(BOOL)respectContextIsFlipped
hints:(NSDictionary *)hints
The main advantage of this method is the hints NSDictionary, in which you have some control over interpolation. This can yield widely differing results when enlarging an image. NSImageHintInterpolation is an enum that can take one of five values…
enum {
NSImageInterpolationDefault = 0,
NSImageInterpolationNone = 1,
NSImageInterpolationLow = 2,
NSImageInterpolationMedium = 4,
NSImageInterpolationHigh = 3
};
typedef NSUInteger NSImageInterpolation;
Using this method there is no need for the intermediate step of extracting an imageRep, NSImage will do the right thing...
- (NSImage*) resizeImage:(NSImage*)sourceImage size:(NSSize)size
{
NSRect targetFrame = NSMakeRect(0, 0, size.width, size.height);
NSImage* targetImage = [[NSImage alloc] initWithSize:size];
[targetImage lockFocus];
[sourceImage drawInRect:targetFrame
fromRect:NSZeroRect //portion of source image to draw
operation:NSCompositeCopy //compositing operation
fraction:1.0 //alpha (transparency) value
respectFlipped:YES //coordinate system
hints:#{NSImageHintInterpolation:
[NSNumber numberWithInt:NSImageInterpolationLow]}];
[targetImage unlockFocus];
return targetImage;
}

Blurred Screenshot of a view being drawn by UIBezierPath

I'm drawing my graph view using UIBezierPathmethods and coretext. I use addQuadCurveToPoint:controlPoint: method to draw curves on graph. I also use CATiledLayer for the purpose of rendering graph with large data set on x axis. I draw my whole graph in an image context and in drawrect: method of my view I draw this image in my whole view. Following is my code.
- (void)drawImage{
UIGraphicsBeginImageContextWithOptions(self.frame.size, NO, 0.0);
// Draw Curves
[self drawDiagonal];
UIImage *screenshot = UIGraphicsGetImageFromCurrentImageContext();
[screenshot retain];
UIGraphicsEndImageContext();
}
- (void)drawRect:(CGRect)rect{
NSLog(#"Draw iN rect with Bounds: %#",NSStringFromCGRect(rect));
[screenshot drawInRect:self.frame];
}
However in screenshot the curves drawn between two points are not smooth. I've also set the Render with Antialiasing to YES in my info plist. Please see screenshot.
We'd have to see how you construct the UIBezierPath, but in my experience, for smooth curves, the key issue is whether the slope of the line between a curve's control point and the end point of that particular segment of the curve is equal to the slope between the next segment of the curve's start point and its control point. I find that easier to draw general smoooth curves using addCurveToPoint rather than addQuadCurveToPoint, so that I can adjust the starting and ending control points to satisfy this criterion more generally.
To illustrate this point, the way I usually draw UIBezierPath curves is to have an array of points on the curve, and the angle that the curve should take at that point, and then the "weight" of the addCurveToPoint control points (i.e. how far out the control points should be). Thus, I use those parameters to dictate the second control point of a UIBezierPath and the first controlPoint of the next segment of the UIBezierPath. So, for example:
#interface BezierPoint : NSObject
#property CGPoint point;
#property CGFloat angle;
#property CGFloat weight;
#end
#implementation BezierPoint
- (id)initWithPoint:(CGPoint)point angle:(CGFloat)angle weight:(CGFloat)weight
{
self = [super init];
if (self)
{
self.point = point;
self.angle = angle;
self.weight = weight;
}
return self;
}
#end
And then, an example of how I use that:
- (void)loadBezierPointsArray
{
// clearly, you'd do whatever is appropriate for your chart.
// this is just a unclosed loop. But it illustrates the idea.
CGPoint startPoint = CGPointMake(self.view.frame.size.width / 2.0, 50);
_bezierPoints = [NSMutableArray arrayWithObjects:
[[BezierPoint alloc] initWithPoint:CGPointMake(startPoint.x, startPoint.y)
angle:M_PI_2 * 0.05
weight:100.0 / 1.7],
[[BezierPoint alloc] initWithPoint:CGPointMake(startPoint.x + 100.0, startPoint.y + 70.0)
angle:M_PI_2
weight:70.0 / 1.7],
[[BezierPoint alloc] initWithPoint:CGPointMake(startPoint.x, startPoint.y + 140.0)
angle:M_PI
weight:100.0 / 1.7],
[[BezierPoint alloc] initWithPoint:CGPointMake(startPoint.x - 100.0, startPoint.y + 70.0)
angle:M_PI_2 * 3.0
weight:70.0 / 1.7],
[[BezierPoint alloc] initWithPoint:CGPointMake(startPoint.x + 10.0, startPoint.y + 10)
angle:0.0
weight:100.0 / 1.7],
nil];
}
- (CGPoint)calculateForwardControlPoint:(NSUInteger)index
{
BezierPoint *bezierPoint = _bezierPoints[index];
return CGPointMake(bezierPoint.point.x + cosf(bezierPoint.angle) * bezierPoint.weight,
bezierPoint.point.y + sinf(bezierPoint.angle) * bezierPoint.weight);
}
- (CGPoint)calculateReverseControlPoint:(NSUInteger)index
{
BezierPoint *bezierPoint = _bezierPoints[index];
return CGPointMake(bezierPoint.point.x - cosf(bezierPoint.angle) * bezierPoint.weight,
bezierPoint.point.y - sinf(bezierPoint.angle) * bezierPoint.weight);
}
- (UIBezierPath *)bezierPath
{
UIBezierPath *path = [UIBezierPath bezierPath];
BezierPoint *bezierPoint = _bezierPoints[0];
[path moveToPoint:bezierPoint.point];
for (NSInteger i = 1; i < [_bezierPoints count]; i++)
{
bezierPoint = _bezierPoints[i];
[path addCurveToPoint:bezierPoint.point
controlPoint1:[self calculateForwardControlPoint:i - 1]
controlPoint2:[self calculateReverseControlPoint:i]];
}
return path;
}
When I render this into a UIImage (using the code below), I don't see any softening of the image, but admittedly the images are not identical. (I'm comparing the image rendered by capture against that which I capture manually with a screen snapshot by pressing power and home buttons on my physical device at the same time.)
If you're seeing some softening, I would suggest renderInContext (as shown below). I wonder if you writing the image as JPG (which is lossy). Maybe try PNG, if you used JPG.
- (void)drawBezier
{
UIBezierPath *path = [self bezierPath];
CAShapeLayer *oval = [[CAShapeLayer alloc] init];
oval.path = path.CGPath;
oval.strokeColor = [UIColor redColor].CGColor;
oval.fillColor = [UIColor clearColor].CGColor;
oval.lineWidth = 5.0;
oval.strokeStart = 0.0;
oval.strokeEnd = 1.0;
[self.view.layer addSublayer:oval];
}
- (void)capture
{
UIGraphicsBeginImageContextWithOptions(self.view.frame.size, NO, 0.0);
CGContextRef context = UIGraphicsGetCurrentContext();
[self.view.layer renderInContext:context];
UIImage *screenshot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// save the image
NSData *data = UIImagePNGRepresentation(screenshot);
NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *imagePath = [documentsPath stringByAppendingPathComponent:#"image.png"];
[data writeToFile:imagePath atomically:YES];
// send it to myself so I can look at the file
NSURL *url = [NSURL fileURLWithPath:imagePath];
UIActivityViewController *controller = [[UIActivityViewController alloc] initWithActivityItems:#[url]
applicationActivities:nil];
[self presentViewController:controller animated:YES completion:nil];
}

Converting NSImage to CIImage without degraded quality

I am trying to convert an NSImage to a CIImage. When I do this, there seems to be a huge loss in image quality. I think it is because of the "TIFFRepresentation". Does anyone have a better method? Thanks a lot.
NSImage *image = [[NSImage alloc] initWithData:[someSource dataRepresentation]];
NSData * tiffData = [image TIFFRepresentation];
CIImage *backgroundCIImage = [[CIImage alloc] initWithData:tiffData];
CIContext *ciContext = [[NSGraphicsContext currentContext] CIContext];
[ciContext drawImage:backgroundCIImage atPoint:CGPointZero fromRect:someRect];
Your problem is indeed converting to TIFF. PDF is a vector format, while TIFF is bitmap, so a TIFF will look blurry at larger sizes.
Your best bet is probably to get a CGImage from the NSImage and create the CIImage from that. Either that or just create the CIImage from the original data.
Try replacing the line
NSData * tiffData = [image TIFFRepresentation];
with
NSData * tiffData = [image TIFFRepresentationUsingCompression: NSTIFFCompressionNone factor: 0.0f];
because the documentation states that TIFFRepresentation uses the TIFF compression option associated with each image representation, which might not be NSTIFFCompressionNone. Thus, you should be explicit about wanting the tiffData uncompressed.
I finally solved the problem. Basically, I render the pdf document two times its normal resolution offscreen and then capture the image displayed by the view. For a more detailed image, just increase the scaling factor. Please see the code below for the proof of concept. I didn't show the CIImage but once you get the bitmap, just use the CIImage method to create the CIImage from the bitmap.
NSImage *pdfImage = [[NSImage alloc] initWithData:[[aPDFView activePage] dataRepresentation]];
NSSize size = [pdfImage size];
NSRect imageRect = NSMakeRect(0, 0, size.width, size.height);
imageRect.size.width *= 2; //Twice the scale factor
imageRect.size.height *= 2; //Twice the scale factor
PDFDocument *pageDocument = [[[PDFDocument alloc] init] autorelease];
[pageDocument insertPage:[aPDFView activePage] atIndex:0];
PDFView *pageView = [[[PDFView alloc] init] autorelease];
[pageView setDocument:pageDocument];
[pageView setAutoScales:YES];
NSWindow *offscreenWindow = [[NSWindow alloc] initWithContentRect:imageRect
styleMask:NSBorderlessWindowMask
backing:NSBackingStoreRetained
defer:NO];
[offscreenWindow setContentView:pageView];
[offscreenWindow display];
[[offscreenWindow contentView] display]; // Draw to the backing buffer
// Create the NSBitmapImageRep
[[offscreenWindow contentView] lockFocus];
NSBitmapImageRep* rep = [[NSBitmapImageRep alloc] initWithFocusedViewRect:imageRect];
// Clean up and delete the window, which is no longer needed.
[[offscreenWindow contentView] unlockFocus];
[compositeImage TIFFRepresentation]];
NSData *imageData = [rep representationUsingType: NSJPEGFileType properties: nil];
[imageData writeToFile:#"/Users/David/Desktop/out.jpg" atomically: YES];
[offscreenWindow release];

Obtaining the maximum height of a font

So I have an NSFont, and I want to get the maximum dimensions for any characters, ie. the pitch and letter height. [font maximumAdvancement] seems to return an NSSize of {pitch, 0}, so that's not helping. Bounding rect doesn't seem to work either, and the suggestion from jwz's similar question of creating a bezier path, appending a glyph and getting the bounding rectange is also giving me back {0, 0}. What gives here?
UPDATE: The code I'm using to get the bezier size is this:
NSBezierPath *bezier = [NSBezierPath bezierPath];
NSGlyph g;
{
NSTextStorage *ts = [[NSTextStorage alloc] initWithString:#" "];
[ts setFont:font];
NSLayoutManager *lm = [[NSLayoutManager alloc] init];
NSTextContainer *tc = [[NSTextContainer alloc] init];
[lm addTextContainer:tc];
[tc release]; // lm retains tc
[ts addLayoutManager:lm];
[lm release]; // ts retains lm
g = [lm glyphAtIndex:0];
[ts release];
}
NSPoint pt = {0.0f};
[bezier moveToPoint:pt];
[bezier appendBezierPathWithGlyph:g inFont:font];
NSRect bounds = [bezier bounds];
The glyph for the space character doesn't have any subpaths, so of course its bounds have size NSZeroSize. Try -[NSFont boundingRectForFont] instead.