MacOS how is KIND implemented - swift

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
}

Related

How can I iterate inside each directory in resource path

I have a simple scenario, there is a folder called content that contains image file, I want all the image file starting with nssl to be saved to an array , so I do below code, but I cannot seem to think or know a way to find out how I can move in each directory and search for such a file and append to my array , here is my code below , I can get the names of all the directories, but what to do next ?
let path = Bundle.main.resourcePath!
let fm = FileManager.default
do {
let items = try fm.contentsOfDirectory(atPath: path)
for item in items {
}
} catch {
}
FileManager is not needed.
Bundle provides urls(forResourcesWithExtension: subdirectory:) which returns multiple urls for a specific extension
if let urls = Bundle.main.urls(forResourcesWithExtension: "png", subdirectory: "content") {
for url in urls where url.lastPathComponent.hasPrefix("nssl") {
}
}
Change the png extension to the desired type.

Link app object to file on disk with metadata

Following this topic : iOS PDFkit cannot add custom attribute to pdf document
My app is using PDFKit to save files
I'm trying to set custom key metadata to PDFDocument I save on the device.
The object in my app ('Test') has two important properties :
id: a UUID to be able to retrieve the file on disk (the linked file on disk URL is this_UUID.jpg).
name: a human-readable string set by the user.
This cause some problems :
the file name is a UUID not human readable, so it's bad user experience.
If the user renames the file, the app won't be able to get the file.
So the id is to have a human-readable label for the file. So when the user opens the File app he can find it easily. And add metadata with the id so my app can retrieve it even if renamed. Looks like a nice solution right?
// First I create my own attribute
fileprivate extension PDFDocumentAttribute {
static let testId = PDFDocumentAttribute(rawValue: "com.sc.testID")
}
// Then I set the news attributes before saving the file, in the 'test' class
func setDocument(pdf: PDFDocument) {
let fileURL = self.getPDFDocumentURL()
print("the metadata is \(pdf.documentAttributes)") // print an empty dictionary
pdf.documentAttributes?[PDFDocumentAttribute.testId] = self.id
pdf.documentAttributes?[PDFDocumentAttribute.titleAttribute] = self.name // I suppose the ddisplay name of the document ? It's not, so what is that ?
print("the metadata is now \(pdf.documentAttributes)") // both are printed, it looks ok
//pdf.write(to: fileURL) // I tested this one too, same issues
let data = pdf.dataRepresentation()
do {
try data?.write(to: fileURL, options: .completeFileProtection)
} catch {
print(error.localizedDescription)
}
}
From here it looks ok, when I want to retrieve the pdf document I will check in the folder the id of each doc and return the doc when id match. But the problem is when I get the documentAttributes the attribute 'testId' isn't in. Note the native title, is set correctly.
So I could get the id from there but that looks pretty inappropriate
//still in 'Test' class
func getPDFDocument() -> PDFDocument? {
// get all docs in the folder ad check metadata for each
let fileManager = FileManager.default
let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
do {
let fileURLs = try fileManager.contentsOfDirectory(at: SchoolTest.getSubjectFolderURL(subject: self.subject!), includingPropertiesForKeys: nil)
for url in fileURLs {
print("the doc attributes are : \(PDFDocument(url: url)?.documentAttributes)") // contain title and others preset by Apple but not my custom 'testId'
if let doc = PDFDocument(url: url), doc.documentAttributes?[PDFDocumentAttribute.titleAttribute/*testId*/] as? String == self.documentName {
return doc // this work temporary
}
}
} catch {
print("Error while enumerating files \(documentsURL.path): \(error.localizedDescription)")
}
return nil
}
Display name:
Currently, the display name/label displayed in the File app is the file name (from URL).
This can cause problems too because if two 'Test' have the same name, their linked file gonna have the same URL. So when the most recent one will be saved on disk it will overwrite the other.
That's a problem I don't have when using the 'Test' id property for the file URL.
If I could set a display name for the file and keep the URL with the UUID that should resolve the problem.
Directories have the same localizing issue, I have named them with English but Apple native directories are localized. A localized name could be nice for user experience.
After hours of research, I can't find a way to localize or apply a display name to my files/directories.
// I tried that without positive result
var url = fileURL as NSURL
try url.setResourceValue("localized label", forKey: .localizedLabelKey)
print("localized name is \(try url.resourceValues(forKeys: [.localizedLabelKey]))")
let newURL = url as URL
try data?.write(to: newURL, options: .completeFileProtection)
Am I doing something badly? How should we do when adding custom metada to a file?

How to sort the filemanager array by creation date

I have written code that retrieves CSV paths in the documents directory and loads them into a tableview. I am trying to sort the files by creation date so that they list from newest to oldest in the tableview. Anyone got any advice on how to accomplish this?
I've not tried anything yet, because I am a little stuck
override func viewDidLoad() {
super.viewDidLoad()
csvFiles = listCsvs()
tblViewDataCSV.dataSource = self
tblViewDataCSV.delegate = self
}
func listCsvs() -> [URL] {
let fileManager = FileManager.default
let documentDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
let files = try? fileManager.contentsOfDirectory(
at: documentDirectory,
includingPropertiesForKeys: nil,
options: [.skipsSubdirectoryDescendants, .skipsHiddenFiles]
).filter {
$0.lastPathComponent.hasSuffix(".csv")
}
print(files as Any)
return files ?? []
}
I need the array sorted by .creationdate and not alphanumerically. Many thanks for your help.
You need to declare a struct that has a URL (or filename String) and a Date. Populate an array of this struct from the files (and their creation dates) you query from FileManager.
Use that array of struct as your data model for the table view. You can sort the array on filename or date (or any other attributes you might add in the future).
You can get the creation date of each file by first adding [.creationDateKey] to the includingPropertiesForKeys parameter of contentsOfDirectory. Then access the creation date using resourceValues on each URL. See How can I get the file creation date using URL resourceValues method in Swift 3? for more details on getting the creation date.
It may help to use the enumerator method of FileManager instead of contentsOfDirectory. This will make it easier to get the needs URL attributes and populate the array of struct.
You can build a struct like the below:
struct yourStruct {
var path:URL
var filedate:Date
}
For each file in folder, append your struct into array.
let s = yourStruct(
path: csvUrl,
filedate: csvFileDate)
myArray.append(s)
At this point, you have an array with your files and filedates.
And finally sort the array with:
let newArr = myArray.sorted { $0.filedate < $1.filedate }

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 can I get all text from a PDF in 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.