Get Bounding Rectangle of String with NSLayoutManager - objective-c

I have a large amount of unique strings for which I want to compute their bounding rectangle when they would be laid out in an infinitly large rectangle. Currently I use a single NSTextStorage/NSLayoutManager and loop over all strings, collecting the rectangles:
// setup NSTextStorage and its NSLayoutManager, NSTextContainer
...
forall (NSAttributedString *astring in ...)
{
// put string into textstorage
[textStorage setAttributedString:astring];
// trigger glyph generation and layout
[textContainer setContainerSize: NSMakeSize (CGFLOAT_MAX, CGFLOAT_MAX)];
[layoutManager ensureLayoutForTextContainer: textContainer];
// finally get the bounding box
NSRect boundingBox = [layoutManager usedRectForTextContainer: textContainer];
...
}
The question is: is it possible to speed up the computation considering that the strings don't need to be drawn? I'm only interested in the rectangle's width and height.

Jut answering this myself after a few days of testing: no, unfortunately there is no faster way using layout-managers. Using CoreText seems too be about twice as fast, but CoreText has some nasty problems itself.

Related

NSTextView and determining glyph position

I am trying to define y axis position for certain lines and have label next to them in an NSTextView. I am using glyphRangeForCharacterRange.
It kind of works, but the text view can have a LOT of text (even ~4000-5000 lines) and is wrapped with insets. As I scroll to stuff that is out of view, the results are wrong. It seems to return values without wrapping, or so I figure. If I do an update on the positions while I have scrolled to corresponding text, they are calculated correctly.
This is the code without additional offsets:
NSRange characterRange = NSMakeRange(startPosition, length);
NSRange range = [[self.textView layoutManager] glyphRangeForCharacterRange:characterRange actualCharacterRange:nil];
label.frame = rect;
NSRect rect = [[self.textView layoutManager] boundingRectForGlyphRange:range inTextContainer:[self.textView textContainer]];
The same problem occurs if I try to set the label positions after the document awakes, so I calculate the labels on first keypress, which isn't too nice.
What would be the best way around this?
EDIT
The answer to this problem was just to ensure the layout for the NSLayoutManager before retrieving glyph coordinates.
[[_textView layoutManager] ensureLayoutForTextContainer:[_textView textContainer]];

Divide UIImage into two parts along a UIBezierPath

How to divide this UIImage by the black line into two parts. The upper contour set of UIBezierPath.
I need to get two resulting UIImages. So is it possible?
The following set of routines create versions of a UIImage with either only the content inside a path, or only content outside that path.
Both make use of the compositeImage method, which uses CGBlendMode. CGBlendMode is very powerful for masking anything you can draw against anything else you can draw. Calling compositeImage: with other blend modes can have interesting (if not always useful) effects. See the CGContext Reference for all the modes.
The clipping method I described in my comment to your OP does work and is probably faster, but only if you have UIBezierPaths defining all the regions you want to clip.
- (UIImage*) compositeImage:(UIImage*) sourceImage onPath:(UIBezierPath*) path usingBlendMode:(CGBlendMode) blend;
{
// Create a new image of the same size as the source.
UIGraphicsBeginImageContext([sourceImage size]);
// First draw an opaque path...
[path fill];
// ...then composite with the image.
[sourceImage drawAtPoint:CGPointZero blendMode:blend alpha:1.0];
// With drawing complete, store the composited image for later use.
UIImage *maskedImage = UIGraphicsGetImageFromCurrentImageContext();
// Graphics contexts must be ended manually.
UIGraphicsEndImageContext();
return maskedImage;
}
- (UIImage*) maskImage:(UIImage*) sourceImage toAreaInsidePath:(UIBezierPath*) maskPath;
{
return [self compositeImage:sourceImage onPath:maskPath usingBlendMode:kCGBlendModeSourceIn];
}
- (UIImage*) maskImage:(UIImage*) sourceImage toAreaOutsidePath:(UIBezierPath*) maskPath;
{
return [self compositeImage:sourceImage onPath:maskPath usingBlendMode:kCGBlendModeSourceOut];
}
I tested clipping, and in a few different tests it was 25% slower than masking to achieve the same result as the [maskImage: toAreaInsidePath:] method in my other answer. For completeness I include it here, but please don't use it without a good reason.
- (UIImage*) clipImage:(UIImage*) sourceImage toPath:(UIBezierPath*) path;
{
// Create a new image of the same size as the source.
UIGraphicsBeginImageContext([sourceImage size]);
// Clipping means drawing only happens within the path.
[path addClip];
// Draw the image to the context.
[sourceImage drawAtPoint:CGPointZero];
// With drawing complete, store the composited image for later use.
UIImage *clippedImage = UIGraphicsGetImageFromCurrentImageContext();
// Graphics contexts must be ended manually.
UIGraphicsEndImageContext();
return clippedImage;
}
This can be done but it requires some trigonometry. Let's consider the case for the upper image. First, determine the bottommost end point of the UIBezierPath and use UIGraphicsBeginImageContext to get the top part of the image above the line. This will look as follows:
Now, assuming that your line is straight, move pixel by pixel along the line drawing vertical strokes of clearColor (loop for top portion. Proceed on similar lines for bottom portion):
for(int currentPixel_x=0;currentPixel_x<your_ui_image_top.size.width)
UIGraphicsBeginImageContext(your_ui_image_top.size);
[your_ui_image_top drawInRect:CGRectMake(0, 0, your_ui_image_top.size.width, your_ui_image_top.size.height)];
CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), 1.0);
CGContextSetBlendMode(UIGraphicsGetCurrentContext(),kCGBlendModeClear);
CGContextSetStrokeColorWithColor(UIGraphicsGetCurrentContext(),[UIColor clearColor].CGColor);
CGContextBeginPath(UIGraphicsGetCurrentContext());
CGContextMoveToPoint(UIGraphicsGetCurrentContext(), currentPixel_x, m*currentPixel_x + c);
CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), currentPixel_x, your_ui_image_top.size.height);
CGContextStrokePath(UIGraphicsGetCurrentContext());
your_ui_image_top = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
}
Your UIBezierPath will have to be converted to a straight line of the form y = m*x + c. The x in this equation will be currentPixel_x above. Iterate through the width of the image, increasingcurrentPixel_x by 1 each time. next_y_point_on_your_line will be calculated as:
next_y_point_on_your_line = m*currentPixel_x + c
Each vertical stroke will be 1 pixel wide and its height will depend on how you traverse through them. After some iterations, your image will look roughly (please excuse my poor photo-editing skills!) like:
There are multiple ways of how you draw the clear strokes and this is just one way of going about it. You can also have clear strokes that are parallel to the given path if it gives better results.
Another way is to set the alpha of the pixels below the line to 0.

Draw waveform in NSView

I need to draw a waveform in an NSView. (I have all the samples in an array). The drawing must be efficient and really fast without clipping, flickering, etc, it must be smooth. The waveform will be "moving" according to the song position and some changes to the samples (DSP processing) will be shown as visual representation onto NSView in realtime.
I'm familiar drawing lines, arcs, etc onto canvas objects and I have developed apps doing such things but not on Mac OS X ...
I want to ask if anyone can guide me where to start drawing! Core Animation, OpenGL, simple override drawing methods, ??, etc. Which would be the best practice - API to use?
I would keep it simple and create an NSView subclass with an audioData property that uses Cocoa drawing. You could call [view setAudioData:waveArray] which would in turn call [self setNeedsDisplay:YES].
In your drawRect: method you could then iterate through the samples and use NSRectFill() accordingly. Here sample's value is between 0 and 1.
- (void)drawRect:(NSRect)dirtyRect {
[[NSColor blueColor]set];
for (id sample in self.waveArray) {
NSRect drawingRect = NSZeroRect;
drawingRect.origin.x = [self bounds].origin.x;
drawingRect.origin.y = [self bounds].origin.y + ([self.waveArray indexOfObject:sample]/([self.waveArray count] - 1.0));
drawingRect.size.width = [self bounds].size.width/[self.waveArray count];
drawingRect.size.height = [self bounds].size.height * [sample value];
NSRectFill(drawingRect);
}
}
This code isn't exact, and you should be sure to make it more efficent by only drawing samples inside dirtyRect.
I would start with a really long and thin image to represent a single bar/column for the waveform.
My plan would be to have a NSTimer that moves all bars of the wave one to the left every 0.01 seconds.
So something like this in the loop.
for (int x; x < [WaveArray count] ; x++)
{
UIImageView * Bar = [WaveArray ObjectAtIndex: x];
[Bar setCenter:CGPointMake(Bar.center.x-1,Bar.center.y)];
}
Now all you have to do is create the objects at the correct hight and add them to the WaveArray and they all will be moved to the left.

Repeat a semi-transparent NSImage

In a project I'm working on, I have 3 images: top, middle, and bottom. Top and bottom are fixed height, and middle should be repeated in between the two. (The window size will be changing.) They all are tinted with a color from the user preferences, then need to have their alpha set using a value from the preferences.
I can do pretty much everything. The part I get stuck at is drawing the middle. I decided using [NSColor +colorWithPaternImage:] would be the easiest thing to use. There's a lot of code that makes the actually images and colors, so just assume they exist and are not nil.
int problem; // just to help explain things
float alpha;
NSImage *middleTinted;
NSRect drawRect = [self bounds];
drawRect.size.height = self.bounds.size.height - topTinted.size.height - bottomTinted.size.height;
drawRect.origin.y = topTinted.size.height;
NSColor* colorOne = [NSColor colorWithPatternImage:middleTinted];
NSColor* colorTwo = [colorOne colorWithAlphaComponent:alpha];
if(problem == 1)
{
[colorOne set];
}
else if(problem == 2)
{
[colorTwo set];
}
[NSBezierPath fillRect:drawRect];
Assuming problem == 1, it draws the correct image, in the correct location and with the correct size, but no alpha. (Obviously, since I didn't specify one.)
When problem == 2, I'd expect it to do the same thing, but have the correct alpha value. Instead of this, I get a black box.
Is there a solution that will repeat the image with the correct alpha? I figure I could just draw the image manually in a loop, but I'd prefer a more reasonable solution if one exists.
I expect the problem is that pattern colors don't support -colorWithAlphaComponent:.
NSCell.h contains a method called NSDrawThreePartImage that does the work of drawing end caps and a tiled image in between. It also has an alphaFraction parameter that should meet your needs.
If that doesn't work for you, then you might get the pattern color approach to work by re-rendering your middleTinted image into a new NSImage, using the desired alpha value. (See NSImage's draw... methods.)

Drawing a large number of lines (CGContextBeginPath) on an iPad

I'm trying to make an ipad application that draws alot, but I really mean alot of lines on stage (10.000+)
using this simple forloop, my ipad crashes after 40~60 seconds (without showing result)
for ( int i = 0; i < 10000; i++ )
{
int r_x = rand() % 750;
int r_y = rand() % 1000;
CGPoint pointpoint = CGPointMake(r_x, r_y);
UIColor *st = [[GetColor alloc] getPixelColorAtLocation:pointpoint];
DrawLine *drawview = [[DrawLine alloc]initWithFrame:CGRectMake(r_x, r_y, 20, 20) selectedcolor:st];
[self.view addSubview:drawview];
[drawview release];
[DrawLine release];
[GetColor release];
}
and this is my "DrawLine" class:
- (id)initWithFrame:(CGRect)frame selectedcolor:colors{
if ((self = [super initWithFrame:frame])) {
selectedcolor_t = colors;
self.backgroundColor = [UIColor clearColor];
}
return self;
}
- (void)drawRect:(CGRect)frame{
CGContextRef c = UIGraphicsGetCurrentContext();
float* colors = CGColorGetComponents(selectedcolor_t.CGColor);
CGContextSetStrokeColor(c, colors);
CGContextBeginPath(c);
CGContextMoveToPoint(c, 0.0f, 0.0f);
CGContextAddLineToPoint(c, 20.0f, 20.0f);
CGContextStrokePath(c);
}
how can I solve this problem? How can I draw this much subviews without crashing the iOS?
thanks so much!! :)
Please reconsider what you are doing there:
In line 4 of your loop, you alloc an instance of GetColor — which you never use again. Ask yourself: Does that make any sense from a design point of view?
In that same line, if you don't violate Cocoa's naming-conventions, you create a UIColor that is never released...
Then in line 8 you release the class-object of DrawLine (ditto that for the next line and the GetColor-class). This is terribly, horribly wrong!
Please visit the Memory Management Programming Guide at the iOS Dev-Center and read the first two sections (again)!
Besides that, re-evaluate your design:
Should GetColor really be a class, so that you create instances? Wouldn't a simple helper-function for color interpolation make more sense in this context?
If it should be a class, why not create just one instance of it outside of the loop and simply query it repeatedly for the colors?
Do you really need a subclass of UIView to draw a single straight, solid, single-colored line? If the lines need not be updated, you should (as Richard and nacho4d suggested) draw all of them in one object (e.g. by a custom UIView or by a delegate of CALayer implementing the drawLayer:inContext: method). If you need to update those lines later, you could simply (ab)use CALayer...
In the latter case, your problem then becomes:
Calculate your random coordinates.
Calculate your color.
Create an opaque CALayer with
a) that color as its backgroundColor,
b) a width of 20 * sqrt(2),
c) a height of whatever-you-want-to-be-the-width-of-that-line,
d) your point as its origin and
e) a rotation of 45.
Add that layer as a sublayer to self.view's layer.
Cheers
Daniel
If your lines are static (not moving later, not animating, etc) , as they seem to be, you could also draw all the lines in a single drawRect: in one view without creating 1000 of CALayers.
I can't tell if this is faster than drawing 1000 CALayers (because CoreAnimation is hardware accelerated and CoreGraphics is not) but it's surely lighter since all the lines will be flattened in a single bitmap. (which is the context of your view)
Just move your for loop inside your drawRect: and follow danyowde advices.( you just need one color object or a helper function but not to create a color each iteration)
Good luck, Hope it helps;)