How to draw both stroked and filled text in drawLayer:inContext delegate - cocoa

this is my drawLayer method in a CALayer's delegate.
it's only responsible for drawing a string with length = 1.
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
CGRect boundingBox = CGContextGetClipBoundingBox(ctx);
NSAttributedString *string = [[NSAttributedString alloc] initWithString:self.letter attributes:[self attrs]];
CGContextSaveGState(ctx);
CGContextSetShadowWithColor(ctx, CGSizeZero, 3.0, CGColorCreateGenericRGB(1.0, 1.0, 0.922, 1.0));
CTLineRef line = CTLineCreateWithAttributedString((CFAttributedStringRef)string);
CGRect rect = CTLineGetImageBounds(line, ctx);
CGFloat xOffset = CGRectGetMidX(rect);
CGFloat yOffset = CGRectGetMidY(rect);
CGPoint pos = CGPointMake(CGRectGetMidX(boundingBox) - xOffset, CGRectGetMidY(boundingBox)- yOffset);
CGContextSetTextPosition(ctx, pos.x, pos.y);
CTLineDraw(line, ctx);
CGContextRestoreGState(ctx);
}
here's the attributes dictionary:
NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
[NSFont fontWithName:#"GillSans-Bold" size:72.0], NSFontAttributeName,
[NSColor blackColor], NSForegroundColorAttributeName,
[NSNumber numberWithFloat:1.0], NSStrokeWidthAttributeName,
[NSColor blackColor], NSStrokeColorAttributeName,
style, NSParagraphStyleAttributeName, nil];
as is, the stroke does not draw, but the fill does.
if i comment out the stroke attributes in the dictionary, the fill draws.
i know this can't be right, but i can't find any reference to this problem.
is this a known issue when drawing text with a delegate ?
as the string is one character, i was following the doc example not using any framesetter machinery, but tried that anyway as a fix attempt without success.

in reading this question's answer, i realized that i needed to be using a negative number for the stroke value. i was thinking of the stroke being applied to the outside of the letter drawn by CTLineDraw, rather then inside the text shape.
i'm answering my own question, in case this should help anyone else with this misunderstanding, as i didn't see the referenced doc covering this.

Related

NSLayoutManager/NSTextContainer Ignores Scale Factor

I am attempting to measure an NSAttributedString's height given a constant width using the following method:
-(CGFloat)calculateHeightForAttributedString:(NSAttributedString*)attributedNotes {
CGFloat scrollerWidth = [NSScroller scrollerWidthForControlSize:NSRegularControlSize scrollerStyle:NSScrollerStyleLegacy];
CGFloat width = self.tableView.frame.size.width - self.cellNotesWidthConstraint - scrollerWidth;
// http://www.cocoabuilder.com/archive/cocoa/54083-height-of-string-with-fixed-width-and-given-font.html
NSTextView *tv = [[NSTextView alloc] initWithFrame:NSMakeRect(0, 0, width - 20, 1e7)];
tv.font = [NSFont userFontOfSize:32];
[tv.textStorage setAttributedString:attributedNotes];
[self setScaleFactor:[self convertSliderValueToFontScale:self.fontScaleSlider] forTextView:tv];
[tv.layoutManager glyphRangeForTextContainer:tv.textContainer];
[tv.layoutManager ensureLayoutForTextContainer:tv.textContainer];
return [tv.layoutManager usedRectForTextContainer:tv.textContainer].size.height + 10.0f; // add a little bit of a buffer
}
Basically, the width is the table view's size minus the scroller and a little bit of each cell that is used to display other information. This method works really well as long as the text scale (via convertSliderValueToFontScale:) is 1.0. The result from usedRectForTextContainer is incorrect if I change the scale factor, however -- as if the scale factor was not being accounted for.
The scale is set in setScaleFactor:forTextView: on the NSTextView as follows (scalar is the actual scale amount):
[textView scaleUnitSquareToSize:NSMakeSize(scaler, scaler)];
Any ideas on how to fix this?
Edit: I've got a sample project to try here: Github. Strangely enough, things work if the scale is < 0, and they randomly seem to work in the 4.XXX range on occasion...
The answer turns out to be simple: add the NSTextView as a subview of an NSClipView with the same frame as the NSTextView.
The final height function is as follows:
-(CGFloat)calculateHeightForAttributedString:(NSAttributedString*)attributedNotes {
CGFloat width = self.textView.frame.size.width;
// http://www.cocoabuilder.com/archive/cocoa/54083-height-of-string-with-fixed-width-and-given-font.html
NSTextView *tv = [[NSTextView alloc] initWithFrame:NSMakeRect(0, 0, width, 1e7)];
tv.horizontallyResizable = NO;
tv.font = [NSFont userFontOfSize:32];
tv.alignment = NSTextAlignmentLeft;
[tv.textStorage setAttributedString:attributedNotes];
[self setScaleFactor:self.slider.floatValue forTextView:tv];
// In order for usedRectForTextContainer: to be accurate with a scale, you MUST
// set the NSTextView as a subview of an NSClipView!
NSClipView *clipView = [[NSClipView alloc] initWithFrame:NSMakeRect(0, 0, width, 1e7)];
[clipView addSubview:tv];
[tv.layoutManager glyphRangeForTextContainer:tv.textContainer];
[tv.layoutManager ensureLayoutForTextContainer:tv.textContainer];
return [tv.layoutManager usedRectForTextContainer:tv.textContainer].size.height;
}

Centering text in OS X Screensaver

I'm working on implementing a (very) simple screensaver using the Screensaver framework in OS X 10.10. Positioning on the center of the screen and displaying a two-line text works without problem, but setting the alignment to NSCenterTextAlignment somehow doesn't (the text is always displayed left-aligned).
- (void)animateOneFrame
{
// calculate font size based on screen
NSSize size = [self bounds].size;
CGFloat fontsize = size.height / 11;
// set text
NSMutableParagraphStyle *centredStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
[centredStyle setAlignment:NSCenterTextAlignment];
NSDictionary *textAttributes = [NSDictionary dictionaryWithObjectsAndKeys:
[NSFont fontWithName:#"Futura" size:fontsize], NSFontAttributeName,
[NSColor orangeColor], NSForegroundColorAttributeName,
centredStyle, NSParagraphStyleAttributeName,
nil];
NSString *theText = #"Simple text spanning\ntwo lines";
// position text on screen
NSRect boundingRect = [theText boundingRectWithSize:size options:0 attributes:textAttributes];
NSPoint point = NSMakePoint((size.width - boundingRect.size.width) / 2.0,
(size.height - boundingRect.size.height) / 2.0);
[theText drawAtPoint: point withAttributes: textAttributes];
}
Any pointers on how to solve this are appreciated.
PS: I know that I don't need to put everything into animateOneFrame but for the moment the goal is to get it working at all:-)

Updating the size of NSTextField with respect to length of the string

I am trying to update the height of the NSTextField w.r.t the length of the string but if the string is light the last portion gets cut off.This is my code.
self.solutionTextView.frame = CGRectMake(self.solutionTextView.frame.origin.x, self.solutionTextView.frame.origin.y, self.solutionTextView.frame.size.width + 25 ,self.solutionTextView.frame.size.height + 25);
//self.solutionTextView.sizeToFit()
What am i doing wrong?
If the NSTextField is allowed to wrap the string, the following steps are the standard (or usual? or one of more possible?) ways to determine the wanted height of the field. Assume the width of the field is given:
NSRect frame = [textField frame];
Now make it very high:
NSRect constraintBounds = frame;
constraintBounds.size.height = 10000.0;
[textField setFrame: constraintBounds];
and set the string:
[textField setStringValue:theString];
Ask now for the natural size:
NSSize naturalSize =
[[textField cell] cellSizeForBounds:constraintBounds];
And you are done:
frame.size = naturalSize;
[textField setFrame:frame];

Incorrect font measures

On a NSTextField I'm setting a custom font with a size of 140. The text is set to #"28". But as you can clearly see on the image, the text field has plenty of space on top. This only happens with certain type of fonts, not all of them. My question is what information from the font could be affecting the textfield that ends up cropping the text ? (Ascender, Cap Height ?). And if so, can I modify the font file to fix it ?
The baseline will vary between fonts. In addition, there are other metrics that vary. You can work around this problem with NSAttributedString. You could try varying the NSBaselineOffsetAttribute and from within a paragraph setMinimumLineHeight and setMaximumLineHeight. The following is an example. Make sure to create two textField labels and connect their outlets.
self.Label1.stringValue = #"Test Text";
//
// baseline is different for each font!
//
//self.Label2.stringValue = #"Test Text";
NSFont *otherFont = [NSFont fontWithName:#"MarkerFelt-Thin" size:40.0f];
NSNumber *baseline = [[NSNumber alloc] initWithFloat: 5.0f];
NSMutableParagraphStyle *paraStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
[paraStyle setParagraphSpacingBefore:20.0f];
[paraStyle setMinimumLineHeight:30.0f];
[paraStyle setMaximumLineHeight:50.0f];
NSDictionary *otherFDict = [NSDictionary dictionaryWithObjectsAndKeys: paraStyle, NSParagraphStyleAttributeName,
otherFont, NSFontAttributeName, baseline, NSBaselineOffsetAttributeName, nil];
NSMutableAttributedString *otherText = [[NSMutableAttributedString alloc] initWithString:#"Test Text" attributes:otherFDict];
self.Label2.attributedStringValue = otherText;

Drawing Text with Core Graphics

I need to draw centered text to a CGContext.
I started with a Cocoa approach. I created a NSCell with the text and tried to draw it thus:
NSGraphicsContext* newCtx = [NSGraphicsContext
graphicsContextWithGraphicsPort:bitmapContext flipped:true];
[NSGraphicsContext saveGraphicsState];
[NSGraphicsContext setCurrentContext:newCtx];
[pCell setFont:font];
[pCell drawWithFrame:rect inView:nil];
[NSGraphicsContext restoreGraphicsState];
But the CGBitmapContext doesn't seem to have the text rendered on it. Possibly because I have to pass nil for the inView: parameter.
So I tried switching text rendering to Core Graphics:
The simplest way seems to be to use CGContextSelectFont to select a font using its postscript name, and point size, but CGContextShowTextAtPoint only takes non-unicode characters, and there is no apparent way to fit the text to a rectangle: or to compute the extents of a line of text to manually lay out the rectangle.
Then, there is a CGFont that can be created, and set cia CGContextSetFont. Drawing this text requires CGContextShowGlyphsAtPoint, but again the CGContext seems to be lacking functions to compute the bounding rect of generated text, or to wrap the text to a rect. Plus how to transform a string to an array of CGGlyphs is not obvious.
The next option is to try using CoreText to render the string. But the Core Text classes are hugely complicated, and while there are samples that show how to display text, in a specified font, in a rect, there are no samples demonstrating how to compute the bounding rect of a CoreText string.
So:
Given a CGFont a CGContext how do I compute the bounding rect of some text, and how do I transform a text string into an array of CGGlyphs?
Given a string, a CGContext and a postscript name and point size, what Core Text objects do I need to create to compute the bounding rect of the string, and/or draw the string wrapped to a rect on the CGContext.
Given a string and a NSFont - how do I render the string onto a CGBitmapContext? I already know how to get its extents.
I finally managed to find answers after 4 days of searching. I really wish Apple makes better documentation. So here we go
I assume you already have CGFontRef with you. If not let me know I will tell you how to load a ttf from resource bundle into CgFontRef.
Below is the code snippet to compute the bounds of any string with any CGFontref
int charCount = [string length];
CGGlyph glyphs[charCount];
CGRect rects[charCount];
CTFontGetGlyphsForCharacters(theCTFont, (const unichar*)[string cStringUsingEncoding:NSUnicodeStringEncoding], glyphs, charCount);
CTFontGetBoundingRectsForGlyphs(theCTFont, kCTFontDefaultOrientation, glyphs, rects, charCount);
int totalwidth = 0, maxheight = 0;
for (int i=0; i < charCount; i++)
{
totalwidth += rects[i].size.width;
maxheight = maxheight < rects[i].size.height ? rects[i].size.height : maxheight;
}
dim = CGSizeMake(totalwidth, maxheight);
Reuse the same function CTFontGetGlyphsForCharacters to get the glyphs. To get a CTFontRef from a CGFontRef use CTFontCreateWithGraphicsFont() function
Also remember NSFont and CGFontRef are toll-free bridged, meaning they can casted into each other and they will work seamlessly without any extra work.
I would continue with your above approach but use NSAttributedString instead.
NSGraphicsContext* newCtx = [NSGraphicsContext graphicsContextWithGraphicsPort:bitmapContext flipped:true];
[NSGraphicsContext saveGraphicsState];
[NSGraphicsContext setCurrentContext:newCtx];
NSAttributedString *string = /* make a string with all of the desired attributes */;
[string drawInRect:locationToDraw];
[NSGraphicsContext restoreGraphicsState];
Swift 5 version!
let targetSize: CGSize = // the space you have available for drawing the text
let origin: CGPoint = // where you want to position the top-left corner
let string: String = // your string
let font: UIFont = // your font
let attrs: [NSAttributedString.Key:Any] = [.font: font]
let boundingRect = string.boundingRect(with: targetSize, options: [.usesLineFragmentOrigin], attributes: attrs, context: nil)
let textRect = CGRect(origin: origin, size: boundingRect.size)
text.draw(with: textRect, options: [.usesLineFragmentOrigin], attributes: attrs, context: nil)
A complete sample code (Objective-C):
// Need to create pdf Graphics context for Drawing text
CGContextRef pdfContextRef = NULL;
CFURLRef writeFileUrl = (CFURLRef)[NSURL fileURLWithPath:writeFilePath];
if(writeFileUrl != NULL){
pdfContextRef = CGPDFContextCreateWithURL(writeFileUrl, NULL, NULL);
}
// Start page in PDF context
CGPDFContextBeginPage(pdfContextRef, NULL);
NSGraphicsContext* pdfGraphicsContext = [NSGraphicsContext graphicsContextWithCGContext:pdfContextRef flipped:false];
[NSGraphicsContext saveGraphicsState];
// Need to set the current graphics context
[NSGraphicsContext setCurrentContext:pdfGraphicsContext];
NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:[NSFont fontWithName:#"Helvetica" size:26], NSFontAttributeName,[NSColor blackColor], NSForegroundColorAttributeName, nil];
NSAttributedString * currentText=[[NSAttributedString alloc] initWithString:#"Write Something" attributes: attributes];
// [currentText drawInRect: CGRectMake(0, 300, 500, 100 )];
[currentText drawAtPoint:NSMakePoint(100, 100)];
[NSGraphicsContext restoreGraphicsState];
// Close all the created pdf Contexts
CGPDFContextEndPage(pdfContextRef);
CGPDFContextClose(pdfContextRef);

Resources