I have an application that is copying files to the clipboard. Here is the relevant code (arguments is an array of NSStrings containing file paths):
NSMutableArray *filesToCopy = [[NSMutableArray alloc] init];
int i;
for (i=1; i < [arguments count]; i++) {
NSString* pathToFile = [arguments objectAtIndex:i];
NSURL* fileURL = [[NSURL alloc] initFileURLWithPath:pathToFile];
[filesToCopy addObject:fileURL];
}
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
NSInteger changeCount = [pasteboard clearContents];
BOOL OK = [pasteboard writeObjects:filesToCopy];
However, in testing when trying to paste a file copied to the clipboard by the program into an empty email in Mail, often the file shows up as zero bytes and the following error appears on the console:
11/13/13 6:27:12.173 AM Mail[627]: Failed to obtain a valid sandbox extension for item: [789514] of flavor: [public.file-url] from the pasteboard.
11/13/13 6:27:12.174 AM Mail[627]: Failed to get a sandbox extensions for itemIdentifier (789514). The data for the sandbox extension was NULL
and then that is followed by the following error on the console:
11/13/13 8:24:41.947 AM sandboxd[172]: ([627]) Mail(627) deny file-read-xattr [full path of file]
What is strange is that if I copy the file from Finder then it pastes just fine with no errors every time. In other words, somehow Finder copies the file to the clipboard with different information than the way I am doing it. To verify this, I did a simple AppleScript to return clipboard info. After I copy a file to the clipboard that returns the following:
{{«class furl», 115}, {«class utf8», 115}, {«class ut16», 232}, {string, 115}, {Unicode text, 230}}
After I copy the same file to the clipboard using Finder, the Applescript returns the following:
{{«class furl», 33}, {«class icns», 795020}, {«class ut16», 112}, {«class utf8», 55}, {«class 8BPS», 1630436}, {«class BMP », 4194358}, {«class TPIC», 1059291}, {TIFF picture, 4197954}, {«class PNGf», 392648}, {«class jp2 », 213480}, {GIF picture, 121307}, {JPEG picture, 116181}, {Unicode text, 110}, {string, 55}}
So Finder is putting more information about the file on the clipboard and different information. For example, the furl class has a different length. This extra information is obviously what is cause Mail to successfully past a file copied from Finder while it has an error pasting a file copied by my program.
Any clues to what information I'm missing when I put the file on the clipboard or what extra information I should be adding to the paste? I'm guessing that Finder is pasting not just an array of NSURL's, but an array of Dictionary Keys that includes other file information. It also seems to be creating the furl class differently than I am. I've spent time pouring over documentation and I'm stuck on this one.
I believe I found the problem. It seems like when command line applications copy to the pasteboard, there is a permission related to sandboxing that is not transferred. The files copied could be pasted fine into any non-sandboxed app, but not into a sandboxed application. The solution in this case was to just create a regular Cocoa based .app program. I'm still not sure how, but it copies the files in a way that the permissions are properly transferred and the file can be pasted in both non-sandboxed and sandboxed applications.
There is is lot written out there about avoiding triggering the Sandboxing mechanism in Sandboxed Apps.
But all answers lack the most obvious trouble that can trigger Sandboxing.
When you make a copy of a NSURL (which is still based on NSString) into NSPasteboard without properly escaped characters still containing non-valid signs and spaces before transforming into 'NSURL'.
This is one of the ways URLs can become malicious, so it is only a little bit annoying that the Error Message you get while trying to expose the link to pasteboard is not telling you what the obvious reason was in detail. But maybe this is also because, "never give too much informations how to circumvent the safety barriers".
It just says:
[sandbox] CreateSandboxExtensionData failed: urlData: 0x0000000000XYZ length: 0 (-1 means null)
then you have to properly escape the string with
NSString *encodedStringForURL = [yourString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
this will exchange special characters that should not be part of n URL.
After that you can transform to NSURL as usually.
or
Failed to obtain a valid sandbox extension
then you also used a wrong NSPasteboardType that exposes a URL without proper type. In example when you exposed a String that poses as Link because you tried to avoid the [sandbox] trigger by being clever and telling the pasteboard this is only a random string. The Sandbox is still more clever when you prepend the string with file://.
Then the solution is to use NSPasteboardTypeFileURL and/or NSPasteboardTypeURL. This will inform the sandbox that you exposed links on purpose with proper type.
There are more pitfalls, but also remember you have to use YourURL.absoluteString when you paste to Pasteboard.
Related
I haven been working on this problem for a couple of hours but I still could not get it worked. I have tried all the solutions I found on stackoverflow but nothing worked. The following is my code:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSFileManager *fileManager;
NSString *currentPath;
NSString *content;
content = [[NSString alloc] init];
NSString *filename;
filename = #"/Tasty Noodle House.xlsx";
fileManager = [NSFileManager defaultManager];
currentPath = [fileManager currentDirectoryPath];
NSError* error;
content = [NSString stringWithContentsOfFile:[currentPath stringByAppendingString:filename] encoding:NSUTF16LittleEndianStringEncoding error:&error];
NSLog(#"current path: %#",currentPath);
NSLog(#"filecontent: %#",content);
NSLog( #"error: %#", error );
}
I have checked that the path is correct and the file did exist in that path(currentPath). However, the problem is that the line NSLog(#"filecontent: %#",content); is not printed out at all. The printout looks like the following:
2016-01-23 11:30:11.367 ScaryBugsMac[1377:222946] current path: /Users/username/Library/Developer/Xcode/DerivedData/ScaryBugsMac-buobqwjccigdqthjejrzmgasnzyf/Build/Products/Debug
2016-01-23 11:30:11.367 ScaryBugsMac[1377:222946] error: (null)
Based on my experience, it should print out at least "filecontent: (null)".But it is really weird nothing get printed out.
I have also tried using the enconding: NSUTF8StringEncoding, it gave me the error:
2016-01-23 11:32:44.708 ScaryBugsMac[1404:230157] current path: /Users/username/Library/Developer/Xcode/DerivedData/ScaryBugsMac-buobqwjccigdqthjejrzmgasnzyf/Build/Products/Debug
2016-01-23 11:32:44.708 ScaryBugsMac[1404:230157] filecontent: (null)
2016-01-23 11:32:44.709 ScaryBugsMac[1404:230157] error: Error Domain=NSCocoaErrorDomain Code=261 "The file “Tasty Noodle House.xlsx” couldn’t be opened using text encoding Unicode (UTF-8)." UserInfo=0x60000007a740 {NSFilePath=/Users/username/Library/Developer/Xcode/DerivedData/ScaryBugsMac-buobqwjccigdqthjejrzmgasnzyf/Build/Products/Debug/Tasty Noodle House.xlsx, NSStringEncoding=4}
Two other things:
I have tried all the encodings and the problem falls into one of the two cases above.
I could open the xlsx file using Excel on my Mac without any problem. So I don't think there is anything wrong with the file.
I would really appreciate any help, thank you!
More likely than not, the file contents are either compressed (IIRC, the XLSX definition allows the contents to be straight-compressed).
Or there are characters in the file that are, effectively, causing it to be considered corrupt.
Loading it into an NSString probably isn't that useful. If it is compressed, you'll need to decompress it first. If it truly is an XLSX file, then you'll want to use, at least, an XML reader to read the contents (and, better yet, some likely already available open source library that can read said file's contents).
Excel .xlsx files contain binary data. A .xlsx file is similar to a package bundle.
It does not contain plain text, therefore it cannot be encoded to NSString
My current workflow involves using Applescript to essentially delimit Excel data and format it into plain text files. We're pushing towards an all Swift environment, but I haven't yet found any sort of kits for parsing my Excel data into Swift.
The only thing I can think of is to use C or something and wrap it, but that's not ideal. Any better suggestions for parsing this data for use in Swift?
The goal is to eliminate Applescript, but I'm not sure if that will be possible while still interacting with Excel files. Scripting Excel via Applescript seems to be the only method.
EDIT: I don't have the option of eliminating Excel from this workflow. This is how the data will be coming to the application, thus I have to include it.
Being able to streamline the process of parsing this data then processing it will be paramount. I know Applescript has been good in the past with helping me to process it; however, it's getting a little too closed-off for me.
I've been looking at writing something in Swift/Cocoa, but that still might require the data to be extracted with an Applescript, right?
A big plus for pushing Swift is the readability. I don't know Objective-C all that well, and swift would be an easier transition, I feel.
My workflow on PC has been using the COM object, which as has been said, isn't available in the Mac Excel app. I'm only looking for data extraction at this point. Some previous apps did processing within the app, but I'm looking to make this very self-contained, thus all processing within the app I'm developing. Once the data is extracted from the .XLS or .XLSX files, I'll be doing some text editing via RegEx and perhaps a little number crunching. Nothing too crazy. As of now, it will run on the client side, but I'm looking to extend this to a server process.
In Mac OS X 10.6 Snow Leopard Apple introduced the AppleScriptObjC framework which makes it very easy to interact between Cocoa and AppleScript. AppleScript code and a Objective-C like syntax can be used in the same source file. It's much more convenient than Scripting Bridge and NSAppleScript.
AppleScriptObjC cannot be used directly in Swift because the command loadAppleScriptObjectiveCScripts of NSBundle is not bridged to Swift.
However you can use a Objective-C bridge class for example
ASObjC.h
#import Foundation;
#import AppleScriptObjC;
#interface NSObject (Excel)
- (void)openExcelDocument:(NSString *)filePath;
- (NSArray *)valueOfUsedRange;
#end
#interface ASObjC : NSObject
+ (ASObjC *)sharedASObjC;
#property id Excel;
#end
ASObjC.m
#import "ASObjC.h"
#implementation ASObjC
+ (void)initialize
{
if (self == [ASObjC class]) {
[[NSBundle mainBundle] loadAppleScriptObjectiveCScripts];
}
}
+ (ASObjC *)sharedASObjC
{
static id sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[ASObjC alloc] init];
});
return sharedInstance;
}
- (instancetype)init
{
self = [super init];
if (self) {
_Excel = NSClassFromString(#"ASExcel");
}
return self;
}
#end
Create a AppleScript source file form the AppleScriptObjC template
ASExcel.applescript
script ASExcel
property parent: class "NSObject"
on openExcelDocument:filePath
set asFilePath to filePath as text
tell application "Microsoft Excel"
set sourceBook to open workbook workbook file name asFilePath
repeat
try
get workbooks
return
end try
delay 0.5
end repeat
end tell
end openDocument
on valueOfUsedRange()
tell application "Microsoft Excel"
tell active sheet
set activeRange to used range
return value of activeRange
end tell
end tell
end valueOfUsedRange
end script
Link to the AppleScriptObjC framework if necessary.
Create the Bridging Header and import ASObjC.h
Then you can call AppleScriptObjC from Swift with
ASObjC.sharedASObjC().Excel.openExcelDocument("Macintosh HD:Users:MyUser:Path:To:ExcelFile.xlsx")
or
let excelData = ASObjC.sharedASObjC().Excel.valueOfUsedRange() as! Array<[String]>
It's somewhat unclear if you're trying to eliminate Excel as a dependency (which is not unreasonable: it costs money and not everyone has it) or AppleScript as a language (totally understandable, but a bad practical move as Apple's alternatives for application automation all suck).
There are third-party Excel-parsing libraries available for other languages, e.g. I've used Python's openpyxl (for .xlsx files) and xlrd (for .xsl) libraries successfully in my own projects. And I see through the magicks of Googles that someone's written an ObjC framework, DHlibxls, which [assuming no dynamic trickery] should be usable directly from Swift, but I've not used it myself so can't tell you anything more.
You can use ScriptingBridge or NSAppleScript to interact with Apple Scriptable stuff
ScriptingBridge can generate a header file from the Apple Script dictionary.
NSAppleScript can execute any AppleScript for you by passing a String
1. Export to plaintext CSV
If all you're trying to do is extract data from Excel to use elsewhere, as opposed to capturing Excel formulas and formatting, then you probably should not try to read the .xls file. XLS is a complex format. It's good for Excel, not for general data interchange.
Similarly, you probably don't need to use AppleScript or anything else to integrate with Excel, if all you want to do is save the data as plaintext. Excel already knows how to save data as plaintext. Just use Excel's "Save As" command. (That's what it's called on the Mac. I don't know about PCs.)
The question is then what plaintext format to use. One obvious choice for this is a plaintext comma-separated value file (CSV) because it's a simple de facto standard (as opposed to a complex official standard like XML). This will make it easy to consume in Swift, or in any other language.
2. Export in UTF-8 encoding if possible, otherwise as UTF-16
So how do you do that exactly? Plaintext is wonderfully simple, but one subtlety that you need to keep track of is the text encoding. A text encoding is a way of representing characters in a plaintext file. Unfortunately, you cannot reliably tell the encoding of a file just by inspecting the file, so you need to choose an encoding when you save it and remember to use that encoding when you read it. If you mess this up, accented characters, typographer's quotation marks, dashes, and other non-ASCII characters will get mangled. So what text encoding should you use? The short answer is, you should always use UTF-8 if possible.
But if you're working with an older version of Excel, then you may not be able to use UTF-8. In that case, you should use UTF-16. In particular, UTF-16 is, I believe, the only export option in Excel 2011 for Mac which produces a predictable result which will not depend in surprising ways on obscure locale settings or Microsoft-specific encodings.
So if you're on Excel 2011 for Mac, for instance, choose "UTF-16 Unicode Text" from Excel's Save As command.
This will cause Excel to save the file so that every row is a line of text, and every column is separated by a tab character. (So technically, this is a tab-separated value files, rather than a comma-separated value file.)
3. Import with Swift
Now you have a plaintext file, which you know was saved in a UTF-8 (or UTF-16) encoding. So now you can read it and parse it in Swift.
If your Excel data is complicated, you may need a full-featured CSV parser. The best choice is probably CHCSVParser.
Using CHCSV, you can parse the file with the following code:
NSURL * const inputFileURL = [NSURL fileURLWithPath:#"/path/to/exported/file.txt"];
unichar tabCharacter = '\t';
NSArray *rows = [NSArray arrayWithContentsOfCSVFile:inputFilePath options:CHCSVParserOptionsSanitizesFields
delimiter:tabCharacter];
(You could also call it from Swift, of course.)
On the other hand, if you're data is relatively simple (for instance, it has no escaped characters), then you might not need to use an external library at all. You can write some Swift code that parses tab-separated values just by reading in the file as a string, splitting on newlines, and then splitting on tabs.
This function will take a String representing TSV data and return an array of dictionaries:
/**
Reads a multiline, tab-separated String and returns an Array<NSictionary>, taking column names from the first line or an explicit parameter
*/
func JSONObjectFromTSV(tsvInputString:String, columnNames optionalColumnNames:[String]? = nil) -> Array<NSDictionary>
{
let lines = tsvInputString.componentsSeparatedByString("\n")
guard lines.isEmpty == false else { return [] }
let columnNames = optionalColumnNames ?? lines[0].componentsSeparatedByString("\t")
var lineIndex = (optionalColumnNames != nil) ? 0 : 1
let columnCount = columnNames.count
var result = Array<NSDictionary>()
for line in lines[lineIndex ..< lines.count] {
let fieldValues = line.componentsSeparatedByString("\t")
if fieldValues.count != columnCount {
// NSLog("WARNING: header has %u columns but line %u has %u columns. Ignoring this line", columnCount, lineIndex,fieldValues.count)
}
else
{
result.append(NSDictionary(objects: fieldValues, forKeys: columnNames))
}
lineIndex = lineIndex + 1
}
return result
}
So you only need to read the file into a string and pass it to this function. That snippet comes from this gist for a tsv-to-json converter. And if you need to know more about which text encodings Microsoft products produce, and which ones Cocoa can auto-detect, then this repo on text encoding contains the research on export specimens which led to the conclusion that UTF-16 is the way to go for old Microsoft products on the Mac.
(I realize I'm linking to my own repos here. Apologies?)
There is no need to export Excel files to CSV for Swift as you can use an existing open-source library for parsing XLSX files. If you use CocoaPods or Swift
Package Manager for integrating 3rd-party libraries, CoreXLSX supports those. After the library is integrated, you can use it like this:
import CoreXLSX
guard let file = XLSXFile(filepath: "./file.xlsx") else {
fatalError("XLSX file corrupted or does not exist")
}
for path in try file.parseWorksheetPaths() {
let ws = try file.parseWorksheet(at: path)
for row in ws.sheetData.rows {
for c in row.cells {
print(c)
}
}
}
This will open file.xlsx and print all cells within that file. You can also filter cells by references and access only cell data that you need for your automation.
I am using the NSPasteboard to read data off it and perform actions with the received data.
Specifically, I am interested in information of the types listed below in order of priority.
File path
Text data
Image data (this is not copying the image file, but actual parts of the image, for e.g. when you open an image in an editor, make a selection and press command+v)
This is what I have in code
_pasteBoard = [NSPasteboard generalPasteboard];
NSArray *types = [_pasteBoard types];
NSArray *files = [_pasteBoard propertyListForType: NSFilenamesPboardType];
NSString *text = [_pasteBoard propertyListForType: NSStringPboardType];
NSString *text2 = [_pasteBoard propertyListForType: NSPasteboardTypeString];
NSString *rtfText = [_pasteBoard propertyListForType: NSRTFPboardType];
NSData *imageData = [_pasteBoard dataForType: NSTIFFPboardType];
Right now, I am simply testing if I am able to get the required data or not. So When I select some files on the Destkop then files contains the selected file URLs. So that works.
Similarly when I select some parts of an image and copy it to the clipboard then imageData contains some data, which I then write to another file and can see that only the selected potion was copied, so that is OK too. Also, I understand that when I copy an image "file" to the clipboard then too imageData will not be nil, but that is ok because topmost priority goes to file URL and that is the case that will do what needs to be done.
The problem that I have run into is with the lines related to reading text from the pasteboard.
NSString *text = [_pasteBoard propertyListForType: NSStringPboardType];
NSString *text2 = [_pasteBoard propertyListForType: NSPasteboardTypeString];
NSString *rtfText = [_pasteBoard propertyListForType: NSRTFPboardType];
The Problem Descriptio:
I copied the URL of this page and both "text" and "text2" contain the string from the pasteboard. So it good here :)
I copy some lines from a .cs code file opened in TextEdit, the clipboard shows that I have "public.utf8-plain-text" and "NSStringPboardType" but "text" and "text2" are both nil;
From the same C#.NET .cs file, I select a single word, without any breaks for eg. "System.Threading", the clipboard shows that I have "public.utf8-plain-text" and "NSStringPboardType" and both "text" and "text2" have the copied text but any thing copied with spaces or lines will not. "myProject.Core.Helpers" will work but "namespace myProject.Core.Helpers" wont.
Similarly, I copy some text from a RTF file, the clipboard shows that I have a lot of RTF related information along with the same old "public.utf8-plain-text" and "NSStringPboardType" types too, but again "text", "text2" and, this time, "rtfText" are all nil.
So, how do I get the text that I need in all cases from the clipboard? I know that RTF data can contain a lot more then text but I am just concerned with getting the text out in a string file.
Any help will be greatly appreciated.
Thanks!
The problem is almost certainly that you're not accounting for multiple items on the pasteboard. In the old days (OS X 10.5 and earlier), there was only one item on the pasteboard. That item could have multiple types, which were just supposed to be representations of the same thing.
Now, the pasteboard can have multiple items. Each item can still have multiple types. The -types method returns a union of the types of all of the items. But -propertyListForType: will only examine the first item.
You could iterate over the array returned by -pasteboardItems and ask each for its types and then get the property list object for each type.
Or you could, for each type you're interested in, invoke -readObjectsForClasses:options: with a single-element array of the corresponding class. That wouldn't (and you generally shouldn't) distinguish among different plain text types. All of those would match NSString.
The most common approach, though, is to construct an array of all classes you can handle, in order of your app's preference (usually most expressive to least), and make a single call to -readObjectsForClasses:options:. Then process all objects returned. Each object will be for a single item on the pasteboard in the "best" available form.
Looking for some advice with an iOS application.
Essentially what my app does is generate a CSV file that logs certain events within a period of time. So users can press a button and an entry will be added to the log saying "Event of type X happened at Time T"
The way I'm doing this is by maintaining an NSMutableArray which stores NSStrings. Each event adds a string to the NSMutableArray.
When the user is done with a session, they can "export" the file. I'm using the NSMutableArray's writeToFile; then I use an e-mail interface to send that file as a CSV to a target e-mail.
It all works, except the CSV file that is generated has some meta-data in it. Specifically, I believe at the top of the file I see and at the beginning of each row of cells when opened in excel.
Is this something inherent in the data structure (NSMutableArray) or data type (NSString) that I am using? Is there a way for me to just get the raw data?
I can upload code if need be (I'm not near the work computer now though, so I'm testing the waters to see if there is something simple I can do to stop seeing this meta-data).
Thank you!
CSV is a very simple format. You can separate the strings with semi-colons and then write everything to a file using NSOutputStream.
This code assumes you already have a string array with CSV rows:
NSOutputStream* csvoutput = [NSOutputStream outputStreamToFileAtPath:filepath append:NO];
[csvoutput open];
for (NSString* str in array) {
NSString* tempStr = [str stringByAppendingString:#"\n"]; //new line
NSData *strData = [tempStr dataUsingEncoding:NSUTF8StringEncoding];
[csvoutput write:(uint8_t *)[strData bytes] maxLength:[strData length]];
}
[csvoutput close];
You better create a model class (Event) and fill the array with Event-instances instead of strings. Thats cleaner and more efficient. Then you would create the CSV-strings when exporting to a file.
Is there some blindingly obvious reason why this is producing a nil string instead of the actual text content of the file?
NSString *fromFile = [NSString stringWithContentsOfFile:
#"file://localhost/Users/username/Desktop/test.txt"];
NSLog(#"%#", fromFile);
PRINTS: "(null)"
The file is a plain ASCII text file saved from TextWrangler with contents ' abc '.
The path comes from dragging the actual file from the desktop into the Xcode editor window.
I've also tried without "file://localhost".
The method documentation says "Returns nil if the file can't be opened". There's nothing unusual about the file (not locked, etc.). It has default Unix permissions and was created by the same user as is running Xcode.
I know this method is deprecated -- trying to get this working first.
You have stringWithContentsOfFile: and stringWithContentsOfURL: mixed up.
If you are passing in a URL e.g.
#"file://localhost/Users/username/Desktop/test.txt"
the you want stringWithContentsOfURL: and make the parameter a NSURL e.g.
[NSURL URLWithString:#"file://localhost/Users/username/Desktop/test.txt"]
If you want to use stringWithContentsOfFile: the the parameter should be
#"/Users/username/Desktop/test.txt"
Have you tried ~/Desktop/test.txt or /Users/username/Desktop/test.txt?