I have an NSTextView. I paste an image into it and see it. When I get the NSTextAttachment for the NSAttributedString of the text view, it's file wrapper is nil. How do I get the image data that was pasted into the text view?
I'm using a category on NSAttributedString to get the text attachments. I would prefer not to write to disk if it's possible.
- (NSArray *)allAttachments
{
NSError *error = NULL;
NSMutableArray *theAttachments = [NSMutableArray array];
NSRange theStringRange = NSMakeRange(0, [self length]);
if (theStringRange.length > 0)
{
NSUInteger N = 0;
do
{
NSRange theEffectiveRange;
NSDictionary *theAttributes = [self attributesAtIndex:N longestEffectiveRange:&theEffectiveRange inRange:theStringRange];
NSTextAttachment *theAttachment = [theAttributes objectForKey:NSAttachmentAttributeName];
if (theAttachment != NULL){
NSLog(#"filewrapper: %#", theAttachment.fileWrapper);
[theAttachments addObject:theAttachment];
}
N = theEffectiveRange.location + theEffectiveRange.length;
}
while (N < theStringRange.length);
}
return(theAttachments);
}
Enumerate the attachments. [NSTextStorage enumerateAttribute:...]
Get the attachment's filewrapper.
Write to a URL.
[textStorage enumerateAttribute:NSAttachmentAttributeName
inRange:NSMakeRange(0, textStorage.length)
options:0
usingBlock:^(id value, NSRange range, BOOL *stop)
{
NSTextAttachment* attachment = (NSTextAttachment*)value;
NSFileWrapper* attachmentWrapper = attachment.fileWrapper;
[attachmentWrapper writeToURL:outputURL options:NSFileWrapperWritingAtomic originalContentsURL:nil error:nil];
(*stop) = YES; // stop so we only write the first attachment
}];
This sample code will only write the first attachment to outputURL.
You can get the contained NSImage from the attachment cell.
Minimalistic example:
// assuming we have a NSTextStorage* textStorage object ready to go,
// and that we know it contains an attachment at some_index
// (in real code we would probably enumerate attachments).
NSRange range;
NSDictionary* textStorageAttrDict = [textStorage attributesAtIndex:some_index
longestEffectiveRange:&range
inRange:NSMakeRange(0,textStorage.length)];
NSTextAttachment* textAttachment = [textStorageAttributesDictionary objectForKey:#"NSAttachment"];
NSTextAttachmentCell* textAttachmentCell = textAttachment.attachmentCell;
NSImage* attachmentImage = textAttachmentCell.image;
EDITING:
OS X only (AppKit version)
#EmeraldWeapon's answer is good for Objective-C, but falls down in Swift, as in Swift the attachmentCell is not an NSTextAttachmentCell, but rather an NSTextAttachmentCellProtocol? (which does not provide .image) - so you need to cast it to a concrete instance before accessing the .image:
func firstImage(textStorage: NSTextStorage) -> NSImage? {
for idx in 0 ..< textStorage.string.count {
if
let attr = textStorage.attribute(NSAttributedString.Key.attachment, at: idx, effectiveRange: nil),
let attachment = attr as? NSTextAttachment,
let cell = attachment.attachmentCell as? NSTextAttachmentCell,
let image = cell.image {
return image
}
}
return nil
}
Related
I'm trying to nicely display paragraphs of highlighted in a NSTextView. Right now, I'm doing this by creating a NSAttributedString with a background color. Here's some simplified code:
NSDictionary *attributes = #{NSBackgroundColorAttributeName:NSColor.greenColor};
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:#"Here is a single line of text with single spacing" attributes:attributes];
[textView.textStorage setAttributedString:attrString];
This approach basically works, in that it produces highlighted text.
Unfortunately, when multiple lines exist, the highlight covers the vertical space between the lines in addition to the lines themselves, resulting in ugliness.
Does anyone know of a way to do this kind of highlighting in Cocoa? The picture below is basically what I'm looking for (ignore the shadow on the white boxes):
I'd be willing to use CoreText, html, or whatever is necessary to make things look nicer.
You will need to subclass NSLayoutManager and override:
- (void)fillBackgroundRectArray:(const CGRect *)rectArray
count:(NSUInteger)rectCount
forCharacterRange:(NSRange)charRange
color:(UIColor *)color;
This is the primitive method for drawing background color rectangles.
Try this:-
-(IBAction)chooseOnlylines:(id)sender
{
NSString *allTheText =[tv string];
NSArray *lines = [allTheText componentsSeparatedByString:#"\n"];
NSString *str=[[NSString alloc]init];
NSMutableAttributedString *attr;
BOOL isNext=YES;
[tv setString:#""];
for (str in lines)
{
attr=[[NSMutableAttributedString alloc]initWithString:str];
if ([str length] > 0)
{
NSRange range=NSMakeRange(0, [str length]);
[attr addAttribute:NSBackgroundColorAttributeName value:[NSColor greenColor] range:range];
[tv .textStorage appendAttributedString:attr];
isNext=YES;
}
else
{
NSString *str=#"\n";
NSAttributedString *attr=[[NSAttributedString alloc]initWithString:str];
[tv .textStorage appendAttributedString:attr];
isNext=NO;
}
if (isNext==YES)
{
NSString *str=#"\n";
NSAttributedString *attr=[[NSAttributedString alloc]initWithString:str];
[tv .textStorage appendAttributedString:attr];
}
}
}
The paragraph needs to be highlighted when user taps on it. this is how I implemented it and don't confuse with the highlight color, it is a custom NSAttributedString key I created for this purpose.
extension NSAttributedString.Key {
public static let highlightColor = NSAttributedString.Key.init("highlightColor")
}
class ReaderLayoutManager: NSLayoutManager {
// MARK: - Draw Background
override func drawBackground(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
super.drawBackground(forGlyphRange: glyphsToShow, at: origin)
self.enumerateLineFragments(forGlyphRange: glyphsToShow) { (_, usedRect, _, range, _) in
guard let highlightColor = self.currentHighlightColor(range: range) else { return }
guard let context = UIGraphicsGetCurrentContext() else { return }
var lineRect = usedRect
lineRect.origin.y += 10
lineRect.size.height -= 2
context.saveGState()
let path = UIBezierPath(roundedRect: lineRect, cornerRadius: 2)
highlightColor.setFill()
path.fill()
context.restoreGState()
}
}
private func currentHighlightColor(range: NSRange) -> UIColor? {
guard let textStorage = textStorage else { return nil }
guard let highlightColor = textStorage.attributes(at: range.location, effectiveRange: nil)[.highlightColor] as? UIColor else { return nil }
return highlightColor
}
}
when user clicks on it, I set the highlight color for the range and reset the TextView.
attributedString.addAttributes([.highlightColor: theme.textUnderlineColor], range: range)
I am trying to create a PDF report from an iPad app using xcode 4.6. I know a valid pdf file is being created when run on the simulator, because I can dig it out and preview it. The commented out code does this. The problem is that I can't write it somewhere I can get at it on the iPad.
I've tried using UIGraphicsBeginPDFContextToData instead and trying to write the image out to the PhotoAlbum instead. The problem here is that when I convert the NSMutableData into an image it returns nil.
Here is the code. Thanks for any help you can give me.
- (IBAction)makePDF:(UIButton *)sender
{
CFAttributedStringRef currentText = CFAttributedStringCreate(NULL, (CFStringRef)self.labelCopyright.text, NULL);
if (currentText)
{
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(currentText);
if (framesetter)
{
// NSString *rootPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, //NSUserDomainMask, YES) objectAtIndex:0];
// NSString *pdfPath = [rootPath stringByAppendingPathComponent:#"Nick.pdf"];
// NSLog(#"pdf is at %#",pdfPath);
// UIGraphicsBeginPDFContextToFile(pdfPath, CGRectZero, nil);
NSMutableData *data = [[NSMutableData alloc] initWithCapacity:100000];
UIGraphicsBeginPDFContextToData(data, CGRectZero, nil);
CFRange currentRange = CFRangeMake(0, 0);
NSInteger currentPage = 0;
BOOL done = NO;
do
{
UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, 612, 792), nil);
currentPage++;
// [self drawPageNumber:currentPage];
currentRange = [self renderPage:currentPage withTextRange:currentRange andFramesetter:framesetter];
if (currentRange.location == CFAttributedStringGetLength((CFAttributedStringRef)currentText)) done = YES;
}
while (!done);
UIGraphicsEndPDFContext();
UIImage* image = [UIImage imageWithData:data];
assert(image);
UIImageWriteToSavedPhotosAlbum(image, self, nil, nil);
CFRelease(framesetter);
}
else NSLog(#"Could not create the framesetter needed to lay out the atrributed string.");
CFRelease(currentText);
}
else NSLog(#"Could not create the attributed string for the framesetter");
}
- (CFRange)renderPage:(NSInteger)pageNum withTextRange:(CFRange)currentRange andFramesetter:(CTFramesetterRef)framesetter
{
CGContextRef currentContext = UIGraphicsGetCurrentContext();
CGContextSetTextMatrix(currentContext, CGAffineTransformIdentity);
CGRect frameRect = CGRectMake(72, 72, 468, 648);
CGMutablePathRef framePath = CGPathCreateMutable();
CGPathAddRect(framePath, NULL, frameRect);
CTFrameRef frameRef = CTFramesetterCreateFrame(framesetter, currentRange, framePath, NULL);
CGPathRelease(framePath);
CGContextTranslateCTM(currentContext, 0, 792);
CGContextScaleCTM(currentContext, 1.0, -1.0);
CTFrameDraw(frameRef, currentContext);
currentRange = CTFrameGetVisibleStringRange(frameRef);
currentRange.location += currentRange.length;
currentRange.length = 0;
CFRelease(frameRef);
return currentRange;
}
Save the mutable data to your documents directory
[data writeToFile:filePath atomically:YES]
Here's an example:
+(void) saveData: (NSData*) data ToFileName: (NSString*) filename {
// Retrieves the document directories from the iOS device
NSArray* documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask,YES);
NSString* documentDirectory = [documentDirectories objectAtIndex:0];
NSString* documentDirectoryFilename = [documentDirectory stringByAppendingPathComponent: filename];
// instructs the mutable data object to write its context to a file on disk
[data writeToFile:documentDirectoryFilename atomically:YES];
//NSLog(#"documentDirectoryFileName: %#",documentDirectoryFilename);
}
As for displaying the generated PDF on the device, the UIWebView object supports loading PDF files from NSData. Here is an example:
[self.webView loadData:pdfData MIMEType:#"application/pdf" textEncodingName:#"utf-8" baseURL:nil];
It is possible to attach an NSData object to an email as well. Here is an example:
//Check if we can send e-mails
if ([MFMailComposeViewController canSendMail]) {
//Create the Email view controller
MFMailComposeViewController *controller = [[MFMailComposeViewController alloc] init];
controller.mailComposeDelegate = self;
//Set the subject and body
[controller setSubject:#"Email Subject"];
[controller setMessageBody:#"Email body" isHTML:NO];
//Set the email address
[controller setToRecipients:#"test#test.com"];
//Add the current PDF as an attachment
NSString *fileName = #"file.pdf";
[controller addAttachmentData:self.retrievedPDF mimeType:#"application/pdf" fileName:fileName];
// show the email controller modally
[self.navigationController presentModalViewController: controller animated: YES];
}
Instead of writing the PDF to an NSMutableData object, write it to a file using UIGraphicsBeginPDFContextToFile.
The first argument is the file path. The best place would be the Documents directory. There are then many different ways to get the file out of the app:
iTunes file sharing
Email
iCloud
Sending to a 3rd party server (Dropbox, Box, Google Drive, etc.)
Open in another iOS app using UIDocumentInteractionController.
I am searching into a UITableView using this:
titles = [NSArray arrayWithArray:[datamanager titlesForEntriesBetween:(NSInteger)[slider minSelectedValue] and:(NSInteger)[slider maxSelectedValue]containing:searchText]];
How can I encode array value with NSASCIIStringEncoding during the search process?
(Array contains "tĂȘte" for example.. and when I search "tete" nothing matches.. so I will encode array value just for my search)
I would add change the third parameter to your datamanager function:
- (NSArray*)titlesForEntriesBetween:(NSInteger)startIndex
and:(NSInteger)stopIndex
withFunction:(BOOL(^)(NSString*))block {
NSMutableArray *retVal = [NSMutableArray array];
for(NSInteger i = startIndex; i <= stopIndex; ++i) {
NSString *string = [array_ objectAtIndex:i];
if (block(string)) {
[retVal insertObject:string];
}
}
return retVal;
}
And then I would call the function like this:
titles = [datamanager titlesForEntriesBetween:(NSInteger)[slider minSelectedValue] and:(NSInteger)[slider maxSelectedValue] withFunction:^(BOOL)(NSString *str) {
NSData *data = [str dataUsingEncoding:NSASCIIStringEncoding];
NSString *simpleString = [[[NSString alloc] initWithData:data usingEncoding: NSASCIIStringEncoding] autorelease];
return [simpleString isEqualToString:str];
}]];
Note: I just typed this in, I haven't tried to compile/run this.
I'm writing a Cocoa application, which uses NSURLs -- I need to remove the fragment portion of the URL (the #BLAH part).
example: http://example.com/#blah should end up as http://example.com/
I found some code in WebCore that seems to do it by using CFURL functionality, but it never finds the fragment portion in the URL. I've encapsulated it in a extension category:
-(NSURL *)urlByRemovingComponent:(CFURLComponentType)component {
CFRange fragRg = CFURLGetByteRangeForComponent((CFURLRef)self, component, NULL);
// Check to see if a fragment exists before decomposing the URL.
if (fragRg.location == kCFNotFound)
return self;
UInt8 *urlBytes, buffer[2048];
CFIndex numBytes = CFURLGetBytes((CFURLRef)self, buffer, 2048);
if (numBytes == -1) {
numBytes = CFURLGetBytes((CFURLRef)self, NULL, 0);
urlBytes = (UInt8 *)(malloc(numBytes));
CFURLGetBytes((CFURLRef)self, urlBytes, numBytes);
} else
urlBytes = buffer;
NSURL *result = (NSURL *)CFMakeCollectable(CFURLCreateWithBytes(NULL, urlBytes, fragRg.location - 1, kCFStringEncodingUTF8, NULL));
if (!result)
result = (NSURL *)CFMakeCollectable(CFURLCreateWithBytes(NULL, urlBytes, fragRg.location - 1, kCFStringEncodingISOLatin1, NULL));
if (urlBytes != buffer) free(urlBytes);
return result ? [result autorelease] : self;
}
-(NSURL *)urlByRemovingFragment {
return [self urlByRemovingComponent:kCFURLComponentFragment];
}
This is used as such:
NSURL *newUrl = [[NSURL URLWithString:#"http://example.com/#blah"] urlByRemovingFragment];
unfortunately, newUrl ends up being "http://example.com/#blah" because the first line in urlByRemovingComponent always returns kCFNotFound
I'm stumped. Is there a better way of going about this?
Working Code, thanks to nall
-(NSURL *)urlByRemovingFragment {
NSString *urlString = [self absoluteString];
// Find that last component in the string from the end to make sure to get the last one
NSRange fragmentRange = [urlString rangeOfString:#"#" options:NSBackwardsSearch];
if (fragmentRange.location != NSNotFound) {
// Chop the fragment.
NSString* newURLString = [urlString substringToIndex:fragmentRange.location];
return [NSURL URLWithString:newURLString];
} else {
return self;
}
}
How about this:
NSString* s = #"http://www.somewhere.org/foo/bar.html/#label";
NSURL* u = [NSURL URLWithString:s];
// Get the last path component from the URL. This doesn't include
// any fragment.
NSString* lastComponent = [u lastPathComponent];
// Find that last component in the string from the end to make sure
// to get the last one
NSRange fragmentRange = [s rangeOfString:lastComponent
options:NSBackwardsSearch];
// Chop the fragment.
NSString* newURLString = [s substringToIndex:fragmentRange.location + fragmentRange.length];
NSLog(#"%#", s);
NSLog(#"%#", newURLString);
This is quite an old question, and it has already been answered, but for another simple option this is how I did it:
NSString* urlAsString = [myURL absoluteString];
NSArray* components = [urlAsString componentsSeparatedByString:#"#"];
NSURL* myURLminusFragment = [NSURL URLWithString: components[0]];
if there is no fragment, urlMinusFragment will be the same as myURL
Swift 3.0
this will remove the fragment
if let fragment = url.fragment{
url = URL(string: url.absoluteString.replacingOccurrences(of: "#\(fragment)", with: "")!
}
I am writing a text editor for Mac OS X. I need to display hidden characters in an NSTextView (such as spaces, tabs, and special characters). I have spent a lot of time searching for how to do this but so far I have not found an answer. If anyone could point me in the right direction I would be grateful.
Here's a fully working and clean implementation
#interface GILayoutManager : NSLayoutManager
#end
#implementation GILayoutManager
- (void)drawGlyphsForGlyphRange:(NSRange)range atPoint:(NSPoint)point {
NSTextStorage* storage = self.textStorage;
NSString* string = storage.string;
for (NSUInteger glyphIndex = range.location; glyphIndex < range.location + range.length; glyphIndex++) {
NSUInteger characterIndex = [self characterIndexForGlyphAtIndex: glyphIndex];
switch ([string characterAtIndex:characterIndex]) {
case ' ': {
NSFont* font = [storage attribute:NSFontAttributeName atIndex:characterIndex effectiveRange:NULL];
[self replaceGlyphAtIndex:glyphIndex withGlyph:[font glyphWithName:#"periodcentered"]];
break;
}
case '\n': {
NSFont* font = [storage attribute:NSFontAttributeName atIndex:characterIndex effectiveRange:NULL];
[self replaceGlyphAtIndex:glyphIndex withGlyph:[font glyphWithName:#"carriagereturn"]];
break;
}
}
}
[super drawGlyphsForGlyphRange:range atPoint:point];
}
#end
To install, use:
[myTextView.textContainer replaceLayoutManager:[[GILayoutManager alloc] init]];
To find font glyph names, you have to go to CoreGraphics:
CGFontRef font = CGFontCreateWithFontName(CFSTR("Menlo-Regular"));
for (size_t i = 0; i < CGFontGetNumberOfGlyphs(font); ++i) {
printf("%s\n", [CFBridgingRelease(CGFontCopyGlyphNameForGlyph(font, i)) UTF8String]);
}
Have a look at the NSLayoutManager class. Your NSTextView will have a layout manager associated with it, and the layout manager is responsible for associating a character (space, tab, etc.) with a glyph (the image of that character drawn on the screen).
In your case, you would probably be most interested in the replaceGlyphAtIndex:withGlyph: method, which would allow you to replace individual glyphs.
I wrote a text editor a few years back - here's some meaningless code that should get you looking in (hopefully) the right direction (this is an NSLayoutManager subclass btw - and yes I know it's leaking like the proverbial kitchen sink):
- (void)drawGlyphsForGlyphRange:(NSRange)glyphRange atPoint:(NSPoint)containerOrigin
{
if ([[[[MJDocumentController sharedDocumentController] currentDocument] editor] showInvisibles])
{
//init glyphs
unichar crlf = 0x00B6;
NSString *CRLF = [[NSString alloc] initWithCharacters:&crlf length:1];
unichar space = 0x00B7;
NSString *SPACE = [[NSString alloc] initWithCharacters:&space length:1];
unichar tab = 0x2192;
NSString *TAB = [[NSString alloc] initWithCharacters:&tab length:1];
NSString *docContents = [[self textStorage] string];
NSString *glyph;
NSPoint glyphPoint;
NSRect glyphRect;
NSDictionary *attr = [[NSDictionary alloc] initWithObjectsAndKeys:[NSUnarchiver unarchiveObjectWithData:[[NSUserDefaults standardUserDefaults] objectForKey:#"invisiblesColor"]], NSForegroundColorAttributeName, nil];
//loop thru current range, drawing glyphs
int i;
for (i = glyphRange.location; i < NSMaxRange(glyphRange); i++)
{
glyph = #"";
//look for special chars
switch ([docContents characterAtIndex:i])
{
//space
case ' ':
glyph = SPACE;
break;
//tab
case '\t':
glyph = TAB;
break;
//eol
case 0x2028:
case 0x2029:
case '\n':
case '\r':
glyph = CRLF;
break;
//do nothing
default:
glyph = #"";
break;
}
//should we draw?
if ([glyph length])
{
glyphPoint = [self locationForGlyphAtIndex:i];
glyphRect = [self lineFragmentRectForGlyphAtIndex:i effectiveRange:NULL];
glyphPoint.x += glyphRect.origin.x;
glyphPoint.y = glyphRect.origin.y;
[glyph drawAtPoint:glyphPoint withAttributes:attr];
}
}
}
[super drawGlyphsForGlyphRange:glyphRange atPoint:containerOrigin];
}
I solved the problem of converting between NSGlyphs and the corresponding unichar in the NSTextView. The code below works beautifully and replaces spaces with bullets for visible text:
- (void)drawGlyphsForGlyphRange:(NSRange)range atPoint:(NSPoint)origin
{
NSFont *font = [[CURRENT_TEXT_VIEW typingAttributes]
objectForKey:NSFontAttributeName];
NSGlyph bullet = [font glyphWithName:#"bullet"];
for (int i = range.location; i != range.location + range.length; i++)
{
unsigned charIndex = [self characterIndexForGlyphAtIndex:i];
unichar c =[[[self textStorage] string] characterAtIndex:charIndex];
if (c == ' ')
[self replaceGlyphAtIndex:charIndex withGlyph:bullet];
}
[super drawGlyphsForGlyphRange:range atPoint:origin];
}
Perhaps -[NSLayoutManager setShowsControlCharacters:] and/or -[NSLayoutManager setShowsInvisibleCharacters:] will do what you want.
Here is Pol's solution in Swift:
class MyLayoutManager: NSLayoutManager {
override func drawGlyphsForGlyphRange(glyphsToShow: NSRange, atPoint origin: NSPoint) {
if let storage = self.textStorage {
let s = storage.string
let startIndex = s.startIndex
for var glyphIndex = glyphsToShow.location; glyphIndex < glyphsToShow.location + glyphsToShow.length; glyphIndex++ {
let characterIndex = self.characterIndexForGlyphAtIndex(glyphIndex)
let ch = s[startIndex.advancedBy(characterIndex)]
switch ch {
case " ":
let attrs = storage.attributesAtIndex(characterIndex, effectiveRange: nil)
if let font = attrs[NSFontAttributeName] {
let g = font.glyphWithName("periodcentered")
self.replaceGlyphAtIndex(glyphIndex, withGlyph: g)
}
case "\n":
let attrs = storage.attributesAtIndex(characterIndex, effectiveRange: nil)
if let font = attrs[NSFontAttributeName] {
// let g = font.glyphWithName("carriagereturn")
let g = font.glyphWithName("paragraph")
self.replaceGlyphAtIndex(glyphIndex, withGlyph: g)
}
case "\t":
let attrs = storage.attributesAtIndex(characterIndex, effectiveRange: nil)
if let font = attrs[NSFontAttributeName] {
let g = font.glyphWithName("arrowdblright")
self.replaceGlyphAtIndex(glyphIndex, withGlyph: g)
}
default:
break
}
}
}
super.drawGlyphsForGlyphRange(glyphsToShow, atPoint: origin)
}
}
And to list the glyph names:
func listFonts() {
let font = CGFontCreateWithFontName("Menlo-Regular")
for var i:UInt16 = 0; i < UInt16(CGFontGetNumberOfGlyphs(font)); i++ {
if let name = CGFontCopyGlyphNameForGlyph(font, i) {
print("name: \(name) at index \(i)")
}
}
}