How can I get all text from a PDF in Swift? - swift

I have a PDF document and would like to extract all its text.
I tried the following:
import Quartz
let url = NSBundle.mainBundle().URLForResource("test", withExtension: "pdf")
let pdf = PDFDocument(URL: url)
print(pdf.string())
It does get the text, however the order of the lines extracted is completely mixed up as compared to opening the PDF in Adobe, Edit Select All, Copy, Paste!
How can I get the same outcome in Swift, as opening the PDF, Select All, Copy/Paste!?

If you want only text content:
extension String
{
func readPDF() -> String
{
let path = "\(self)"
let url = URL(fileURLWithPath: path)
let pdf = PDFDocument(url: url)
return pdf!.string!
}
}

I did it. with this:
if let pdf = PDFDocument(url: url) {
let pageCount = pdf.pageCount
let documentContent = NSMutableAttributedString()
for i in 1 ..< pageCount {
guard let page = pdf.page(at: i) else { continue }
guard let pageContent = page.attributedString else { continue }
documentContent.append(pageContent)
}
}
Hope it helps.

That is unfortunately not possible.
At least not without some major work on your part. And it certainly is not possible in a general matter for all pdfs.
PDFs are (generally) a one-way street.
They were created to display text in the same way on every system without any difference and for printers to print a document without the printer having to know all fonts and stuff.
Extracting text is non-trivial and only possible for some PDFs where the basic image-pdf is accompanied by text (which it does not have to). All text information present in the PDF is coupled with location information to determine where it is to be shown.
If you have a table shown in the PDF where the left column contains the names of the entries and the right row contains its contents, both of those columns can be represented as completely different blocks of text which only appear to have some link between each other due to the their placement next to each other.
What the framework / your code would have to do is determine what parts of text that are visually linked are also logically linked and belong together. That is not (yet) possible. The reason you and I can read and understand and group the PDF is that in some fields our brain is still far better than computers.
Final note because it might cause confusion: It is certainly possible that Adobe and Apple as well do some of this grouping already and achieves a good result, but it is still not perfect. The PDF I just tested was pretty mangled up after extracting the text via the Mac Preview.

Here's an option using PDFKit:
import Cocoa
import Quartz
func pdfToText(fromPDF: String) -> String {
let urlPath = Bundle.main.url(forResource: fromPDF, withExtension: "pdf")
let docContent = NSMutableAttributedString()
if let pdf = PDFDocument(url: urlPath!) {
let pageCount = pdf.pageCount
for i in 1 ..< pageCount {
guard let page = pdf.page(at: i) else { continue }
guard let pageContent = page.attributedString else { continue }
docContent.append(pageContent)
}
}
return docContent.string
}
let pdfString = pdfToText(fromPDF: "documentName")
This gives you the option to get the PDF content as an attributed string. If you're just after the plain text, you can get it by attaching .string to the result like I did in the above example.
cf. Paul Hudson's snippet

Apple's documentation for the PDFDocument class says that string is "a convenience method, equivalent to creating a selection object for the entire document and then invoking the PDFSelection class’s string method."
So you should get the same results using it as copying and pasting in Preview.
Adobe's Acrobat may use some other routine to create a more logically useful flow, but you can't access that programmatically in MacOS.

Related

MacOS how is KIND implemented

I am trying to write a piece of code that instead of checking file extensions for the many different types of image files, but instead looking at the file attributes. What I can’t figure out from searching the docs is if KIND:Image is really a file attribute or simply a construct Apple created in the FinderApp to make things easier for the user.
I wrote a snippet that pulls the attributes for files with an extension of jpeg and for each file the fileType is returned as NSFileTypeRegular.
let attr = try filemanager.attributesOfItem(atPath: pathConfig+"/"+file) as NSDictionary
if file.hasSuffix(ext) {
   print ("Adding \(file) [ \(attr.fileSize()) \(attr.fileType())]")
   print ( attr.fileModificationDate() )
}
Does anybody know if MacOS retains an attribute for the category a file falls in to. e.g. IMAGE, DOCUMENT etc.
To achieve a functionality similar to the Kind search tag in Finder you can use UTType (Link to reference).
You can get the UTType of a file by initialising it with the file extension:
let fileType = UTType(filenameExtension: fileURL.pathExtension)
The cool thing about UTTypes is that they have a hierarchy, for example, UTType.jpeg is a subtype of UTType.image, along with others like .png.
If you want to check if a file is any kind of image, you can do it like this
let isImage = fileType.conforms(to: .image)
You can check the list for the kind of types you want to support as "Kinds", and filter using those UTTypes
This was my final solution based on the information provided by #EmilioPelaez I am not completely comfortable with Swift especially the unwrapping operations so if the code looks weird that might be why.
func imagesInDir(path: String?) -> [String] {
if let path {
let filemanager: FileManager = FileManager()
let files = filemanager.enumerator(atPath: path)
var array = [String]()
var urlFile: NSURL
while let file = files?.nextObject() as? String {
urlFile = NSURL(fileURLWithPath: file, isDirectory: false)
if (urlFile.isFileURL) {
if let pathExt = urlFile.pathExtension {
if let fileType = UTType(filenameExtension: pathExt) {
let isImage = fileType.conforms(to: .image)
if (isImage){
array.append(file)
print ("\(array.count) \(fileType)")
}
}
}
}
}
}
return array
}

Extracting string after x number of backlashes in swift

I can't find any way to extract a certain string value from another string in SwiftUi.
It is the following link:
"http://media.site.com/videos/3070/0003C305B74F77.mp4"
How would you go about extracting the numbers 0003C305B74F77?
It would be much easier to treat it as an URL. That's what it is. All you need it to get its last path component after deleting its path extension.
let link = "http://media.site.com/videos/3070/0003C305B74F77.mp4"
if let url = URL(string: link) {
let lastPathComponent = url.deletingPathExtension().lastPathComponent
print(lastPathComponent) // "0003C305B74F77"
}

Swift How do you edit the Metadata (ID3) of a .mp3 file in macOS

Swift How do you edit the Metadata (ID3) of a .mp3 file in macOS
I am writing a macOS app and have been trying for some while to change the wording in section of ‘More Info’ in an .mp3 and have done much searching of SO and found 2 snippets of code that will read the metadata but only one actually outputs the existing keys but I don’t understand how to write new data to any of them. I actually want to write data to a .mp3 file I have created and add an image if possible, as a newbie with some knowledge in Swift 3 can anybody help please. The output below is from a test song (Imagine_Test_Song) I have copied to my desktop.
I found this on SO Writing ID3 tags via AVMetaDataItem but I get a compiling error in these lines :-
soundFileMetadata.append(createMetadata(AVMetadataiTunesMetadataKeyArtist, "MyArtist")!) // compiler error here
soundFileMetadata.append(createMetadata(AVMetadataiTunesMetadataKeySongName, "MySong")!)
….
which says :- Missing argument label 'tagKey' in call. The func is this :-
func createMetadata(tagKey: String, _ tagValue: AnyObject?,
keySpace:String = AVMetadataKeySpaceiTunes) -> AVMutableMetadataItem? {
if let tagValue = tagValue {
let tag = AVMutableMetadataItem()
tag.keySpace = keySpace
tag.key = tagKey as NSCopying & NSObjectProtocol
tag.value = (tagValue as? String as! NSCopying & NSObjectProtocol) ?? (tagValue as? Int as! NSCopying & NSObjectProtocol)
return tag
}
return nil
}
Second snippet is my code below which does compile and outputs the various data but how do you edit the text and save the changes. Ideally I would also like to add “artwork” as well, is this possible?
let homeUrl = NSHomeDirectory()
let sourceFilePath = String(format: "%#/Desktop/%#.mp3", homeUrl, " Imagine_Test_Song")
let fileUrl = NSURL(fileURLWithPath: sourceFilePath)
var asset = AVAsset(url: fileUrl as URL) as AVAsset
//using the asset property to get the metadata of file
for metaDataItems in asset.commonMetadata {
//getting the title of the song
if metaDataItems.commonKey == "title" {
let titleData = metaDataItems.value as! NSString
print("title = \(titleData)")
}
//getting the "Artist of the mp3 file"
if metaDataItems.commonKey == "artist" {
let artistData = metaDataItems.value as! NSString
print("artist = \(artistData)")
}
//getting the "creator of the mp3 file"
if metaDataItems.commonKey == "creator" {
let creatorData = metaDataItems.value as! NSString
print("creator = \(creatorData)")
}
//getting the "Album of the mp3 file"
if metaDataItems.commonKey == "albumName" {
let albumNameData = metaDataItems.value as! NSString
print("albumName = \(albumNameData)")
}
Output :-
title = Imagine
creator = John Lennon
type = Singer-Songwriter
albumName = Imagine
Any help would be appreciated, thanks in advance.
After a lot of research on this issue, it seems the ability to modify and save AVMetaDataItems to an MP3 file is impossible using the AVFoundation API calls (even though the mp3 file type is listed as an option) due to licensing issues with MP3 files. So, the only way to do this, as far as I know, is to either roll your own id3 tag editor, or use a library/framework from another author.
I do not personally like using frameworks or libraries from other authors that do not provide the full source code because I have run into problems when the author stops maintaining the project and Apple changes something and the change breaks my apps.
However, I have found ID3TagEditor library by Fabrizio Duroni. Full source is available on github here. It's a straight forward and easy to use library. Fabrizio is actively maintaining the code as of this writing.

How do I retrieve all available Finder tags?

I'm trying to retrieve a list of all the available Finder tags.
I found NSWorkspace().fileLabels, which does return an array, but only an array of the tag colours, not the tags themselves:
print(NSWorkspace.shared().fileLabels) // prints ["None", "Gray", "Green", "Purple", "Blue", "Yellow", "Red", "Orange"]
Which as you can see is not even all the default tags, it's missing Home, Work and Important, and obviously doesn't have any of the custom ones that I created. It looks like it's just the nice names that go with fileLabelColors.
I found NSMetadataQuery for actually searching for things, but how do I get a list of all the tags I have created in the Finder?
After some digging with "Hopper Disassembler" and my own "Find Any File" (for text search in files), I figured out where the Tags are now stored since Monterey:
The file ~/Library/SyncedPreferences/com.apple.kvs/com.apple.KeyValueService-Production.sqlite contains the same plist data that was previously stored in ~/Library/SyncedPreferences/com.apple.finder.plist.
But it's now hidden inside a database record:
If you look into the ZSYSDMANAGEDKEYVALUE table, you'll find a single entry with ZKEY="FinderTagDict". The ZPLISTDATAVLUE contains a bplist record, which contains the pblist (binary plist) structure, from which you can then extract the tags.
NSWorkspace.shared().fileLabels only returns the system tags that were available when the user account was created (the default system tags).
There's unfortunately no API in macOS to retrieve the tags that you have created yourself in the Finder.
The solution is to parse the ~/Library/SyncedPreferences/com.apple.finder.plist:
func allTagLabels() -> [String] {
// this doesn't work if the app is Sandboxed:
// the users would have to point to the file themselves with NSOpenPanel
let url = URL(fileURLWithPath: "\(NSHomeDirectory())/Library/SyncedPreferences/com.apple.finder.plist")
let keyPath = "values.FinderTagDict.value.FinderTags"
if let d = try? Data(contentsOf: url) {
if let plist = try? PropertyListSerialization.propertyList(from: d, options: [], format: nil),
let pdict = plist as? NSDictionary,
let ftags = pdict.value(forKeyPath: keyPath) as? [[AnyHashable: Any]]
{
return ftags.flatMap { $0["n"] as? String }
}
}
return []
}
let all = allTagLabels()
print(all)
This gets all Finder tags labels.
You can also select only the custom tags (ignore the system ones):
func customTagLabels() -> [String] {
let url = URL(fileURLWithPath: "\(NSHomeDirectory())/Library/SyncedPreferences/com.apple.finder.plist")
let keyPath = "values.FinderTagDict.value.FinderTags"
if let d = try? Data(contentsOf: url) {
if let plist = try? PropertyListSerialization.propertyList(from: d, options: [], format: nil),
let pdict = plist as? NSDictionary,
let ftags = pdict.value(forKeyPath: keyPath) as? [[AnyHashable: Any]]
{
return ftags.flatMap { tag in
if let n = tag["n"] as? String,
tag.values.count != 2
{
return n
}
return nil
}
}
}
return []
}
This is an attempt to solve the question for Monterey (and it also works in earlier systems such as High Sierra).
It might also work in sandboxed apps, as long as you're allowed to use System Events in scripts (I know from own experience that it's generally not allowed for apps in the Mac App Store, though). At worst, you'd have to support user-installed scripts and then instruct your users to install the script below manually (and I also suggest to avoid bothering the user with this operation within the first hour since the first launch of the app, or the reviewer may get triggered and reject your app regardless).
The best I could come up with is to use AppleScript to read the tag names from the Finder's Preferences window, see below.
But it's lame because it has to actually open the Finders's Prefs window briefly for this. Still, better than nothing.
use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
set prefsWinWasOpen to false
tell application "Finder"
set prefs to Finder preferences
set prefsWin to window of prefs
tell prefsWin
try
get id
set prefsWinWasOpen to true
on error
set prefsWinWasOpen to false
open
end try
set current panel to Label Preferences panel
end tell
end tell
set rowNames to {}
tell application "System Events"
tell front window of (first application process whose name is "Finder")
set uiElems to entire contents
repeat with uiElem in uiElems
if class of uiElem is table then
set theTable to uiElem
exit repeat
end if
end repeat
set itsRows to rows of theTable
repeat with oneRow in itsRows
set end of rowNames to name of first UI element of oneRow
end repeat
end tell
end tell
if not prefsWinWasOpen then
tell application "Finder"
close prefsWin
end tell
end if
return rowNames

How to get e-mail subject from message:// URL in OSX Swift

I have a desktop app that receives e-mail URLs ("message://" scheme) from the drag&drop pasteboard and I want to get the Subject from the relevant message. The only clue I have, so far, is that the QuickLook library might give me an information object where I can retrieve this info from.
Since the QuickLook API seems to be rather in flux at the moment and most examples show how to use it in iOS, I simply cannot find a way to set up my "Preview" object using a URL and get the information from there.
I would like to avoid setting up my project as a QuickLook plugin, or setting up the whole preview pane / view scaffolding; at the moment I just want to get out what QuickLook loads before it starts displaying, but I can't comprehend what paradigm Apple wants me to implement here.
XCode 7.3.1.
It turns out I misinterpreted the contents of draggingInfo.draggingPasteboard().types as a hierarchical list containing only one type of info (URL in this case).
Had to subscribe to dragged event type kUTTypeMessage as String and retrieve the e-mail subject from the pasteboard with stringForType("public.url-name")
EDIT: Note that the current Mail.app will sometimes create a stack of mails when you drag an e-mail thread. Although the method above still works to get the subject of the stack, there is no URL in the dragging info then and since there's no list of Message-IDs available either, I had to resort to scraping the user's mbox directory:
// See if we can resolve e-mail message meta data
if let mboxPath = pboard.stringForType("com.apple.mail.PasteboardTypeMessageTransfer") {
if let automatorPlist = pboard.propertyListForType("com.apple.mail.PasteboardTypeAutomator") {
// Get the latest e-mail in the thread
if let maxID = (automatorPlist.allObjects.flatMap({ $0["id"]! }) as AnyObject).valueForKeyPath("#max.self") as? Int {
// Read its meta data in the background
let emailItem = draggingEmailItem
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
// Find the e-mail file
if let path = Util.findEmlById(searchPath: mboxPath, id: maxID) {
// Read its contents
emailItem.properties = Util.metaDataFromEml(path)
dispatch_async(dispatch_get_main_queue(), {
// Update UI
});
}
}
}
}
}
Util funcs:
/* Searches the given path for <id>.eml[x] and returns its URL if found
*/
static func findEmlById(searchPath searchPath: String, id: Int)-> NSURL? {
let enumerator = NSFileManager.defaultManager().enumeratorAtPath(searchPath)
while let element = enumerator?.nextObject() as? NSString {
switch (element.lastPathComponent, element.pathExtension) {
case (let lpc, "emlx") where lpc.hasPrefix("\(id)"):
return NSURL(fileURLWithPath: searchPath).URLByAppendingPathComponent(element as String)!
case (let lpc, "eml") where lpc.hasPrefix("\(id)"):
return NSURL(fileURLWithPath: searchPath).URLByAppendingPathComponent(element as String)!
default: ()
}
}
return nil
}
/* Reads an eml[x] file and parses it, looking for e-mail meta data
*/
static func metaDataFromEml(path: NSURL)-> Dictionary<String, AnyObject> {
// TODO Support more fields
var properties: Dictionary<String, AnyObject> = [:]
do {
let emlxContent = try String(contentsOfURL: path, encoding: NSUTF8StringEncoding)
// Parse message ID from "...\nMessage-ID: <...>"
let messageIdStrMatches = emlxContent.regexMatches("[\\n\\r].*Message-ID:\\s*<([^\n\r]*)>")
if !messageIdStrMatches.isEmpty {
properties["messageId"] = messageIdStrMatches[0] as String
}
}
catch {
print("ERROR: Failed to open emlx file")
}
return properties
}
Note: If your app is sandboxed you will need the com.apple.security.temporary-exception.files.home-relative-path.read-only entitlement set to an array with one string in it: /Library/