Swift - some mp3 file metadata return nil - swift

I have code like below. For some file I got metadata like artist, title and other without any problem. For other files metadata list is nil but when I check metadata in editor like Tagger - title and other metadata exists. Furthermore when I change metadata in external editor for at least one key - my code starts work properly.
Could someone explain me where I make mistake ?
static func getBookInCatalog(url: URL) -> Book {
let book = Book(url: url)
let isDir: ObjCBool = false
var directoryContents = [URL]()
var totalTime: CMTime?
var size: UInt64 = 0
var chapters:Int = 0
do {
directoryContents = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [])
} catch let error as NSError {
print(error.localizedDescription)
return book
}
for item in directoryContents {
if !isDir.boolValue {
let result = appDelegate.fileTypes.filter { $0==item.pathExtension.lowercased() }
if !result.isEmpty {
chapters += 1
let fileSize = (try! FileManager.default.attributesOfItem(atPath: item.path)[FileAttributeKey.size] as! NSNumber).uint64Value
size += fileSize
let playerItem = AVPlayerItem(url: item)
let metadataList = playerItem.asset.commonMetadata
let asset = AVURLAsset(url: item, options: nil)
let audioDuration = asset.duration
if let _ = totalTime {
totalTime = totalTime! + audioDuration
} else {
totalTime = audioDuration
}
for metadata in metadataList {
guard let key = metadata.commonKey, let value = metadata.value else{
continue
}
switch key {
case "albumName":
if book.title == nil || book.title == "" {
book.title = (value as? String)!
}
case "artist":
if book.author == nil || book.author == "" {
book.author = (value as? String)!
}
case "artwork" where value is NSData:
if book.image == nil {
book.image = UIImage(data: (value as! NSData) as Data)
}
default:
continue
}
}
}
}
}
if let imageInsideCatalog = getImageFromFolder(url: url){
book.image = imageInsideCatalog
}
if book.title == nil {
book.title = url.deletingPathExtension().lastPathComponent
}
book.chapters = chapters
book.totalTime = totalTime
book.size = size
return book
}

MP3 meta data "standards" have gone through several major iterations over the years (see http://id3.org) . Your editor may be able to read older formats (that AVURLAsset may not support) and save them using the latest/current standard which would make them compatible after any change.

Related

Swift - Fetch elements from Firestore

I have a problem with this function:
func fetchGameFromDB(completionHandler: #escaping ([GamesObject]) -> Void) {
db.collection("games").getDocuments { (querySnapshot, err) in
if let err = err {
print("Error: \(err)")
} else {
self.gameObject = []
for document in querySnapshot!.documents {
print("document \(document.data())")
if let name = document.data()["name"] as? String {
let docRef = self.db.collection("games").document(name)
docRef.getDocument { document, error in
if let document = document {
let data = document.data()
let name = data?["name"] as? String ?? ""
let urlStanding = data?["urlStanding"] as? String ?? ""
let img = data?["gameImg"] as? String ?? ""
let urlUpcoming = data?["urlUpcoming"] as? String ?? ""
self.gameObject.append(GamesObject(name: name, gameImg: img, urlStanding: urlStanding, urlUpcoming: urlUpcoming))
// here i have elements in gameObject
}
// here i have elements in gameObject
}
// here gameObject = []
}
// here gameObject = []
}
completionHandler(self.gameObject)
// here gameObject = []
}
}
}
I get my data well and I add it to my array but when I get to the completionHandler the array is empty.
I find solution, i check if gameObject.count == querySnapshot?.count then I use my completionHandler
func fetchGameFromDB(completionHandler: #escaping ([GamesObject]) -> Void) {
db.collection("games").getDocuments { (querySnapshot, err) in
if let err = err {
print("Error: \(err)")
} else {
self.gameObject = []
querySnapshot?.documents.forEach({ (document) in
if let name = document.data()["name"] as? String {
let docRef = self.db.collection("games").document(name)
docRef.getDocument { document, error in
if let document = document {
let data = document.data()
let name = data?["name"] as? String ?? ""
let urlStanding = data?["urlStanding"] as? String ?? ""
let img = data?["gameImg"] as? String ?? ""
let urlUpcoming = data?["urlUpcoming"] as? String ?? ""
self.gameObject.append(GamesObject(name: name, gameImg: img, urlStanding: urlStanding, urlUpcoming: urlUpcoming))
if self.gameObject.count == querySnapshot?.count {
completionHandler(self.gameObject)
}
}
}
}
})
}
}
}
the first answer there is no problem as long as the missing documents do not exist. but, that cannot be escaped if any of the documents are missing.
how about use to 'DispatchGroup' ?
func fetchGameFromDB(completionHandler: #escaping([GamesObject]) -> Void) {
db.collection("games").getDocuments { (querySnapshot, error) in
guard let docs = querySnapshot?.documents, !docs.isEmpty else {
if let error = error {
print(error)
}
return
}
let group = DispatchGroup()
docs.forEach { doc in
group.enter()
guard let name = doc.data()["name"] as? String else {
group.leave()
return
}
let docRef = self.db.collection("games").document(name)
docRef.getDocument { document, error in
if let document = document, let data = document.data() {
//do something...
gameObjects.append(GamesObject(...)) //init object
}
group.leave()
}
}
group.notify(queue: .main) {
completionHandler(gameObjects)
}
}
}

pull details from local database

I want to pull details from local database but when I do the necessary operations, it returns null as a result. I can't understand where I went wrong.
var chosenCar=""
var chosenCarId : UUID?
the arrays I created, I transfer data to these arrays, there is no problem with that, I did a test
if chosenCar != "" {
//core data
let appDelegate = UIApplication.shared.delegate as! AppDelegate
let context = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "CarsInformation")
let idString = chosenCarId?.uuidString
fetchRequest.predicate = NSPredicate(format: "id = %#", idString!)
fetchRequest.returnsObjectsAsFaults = false
do {
let results = try context.fetch(fetchRequest)
if results.count > 0 {
for result in results as! [NSManagedObject] {
if let name = result.value(forKey: "models") as? String {
modelTextField.text = name
}
if let year = result.value(forKey: "year") as? Int {
yearTextField.text = String(year)
}
if let price = result.value(forKey: "price") as? Int {
priceTextField.text = String(price)
}
if let imageData = result.value(forKey: "image") as? Data {
let image = UIImage(data: imageData)
imageView.image = image
}
}
}
} catch{
print("error")
}
} else {
modelTextField.text = ""
priceTextField.text = ""
yearTextField.text = ""
}
After doing these operations, I can't get the result I want.

How can mp3 data in memory be loaded into an AVAudioPCMBuffer in Swift?

I have a class method to read an mp3 file into an AVAudioPCMBuffer as follows:
private(set) var fullAudio: AVAudioPCMBuffer?
func initAudio(audioFileURL: URL) -> Bool {
var status = true
do {
let audioFile = try AVAudioFile(forReading: audioFileURL)
let audioFormat = audioFile.processingFormat
let audioFrameLength = UInt32(audioFile.length)
fullAudio = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: audioFrameLength)
if let fullAudio = fullAudio {
try audioFile.read(into: fullAudio)
// processing of full audio
}
} catch {
status = false
}
return status
}
However, I now need to be able to read the same mp3 info from memory (rather than a file) into the AVAudioPCMBuffer without using the file system, where the info is held in the Data type, for example using a function declaration of the form
func initAudio(audioFileData: Data) -> Bool {
// some code setting up fullAudio
}
How can this be done? I've looked to see whether there is a route from Data holding mp3 info to AVAudioPCMBuffer (e.g. via AVAudioBuffer or AVAudioCompressedBuffer), but haven't seen a way forward.
I went down the rabbit hole on this one. Here is what probably amounts to a Rube Goldberg-esque solution:
A lot of the pain comes from using C from Swift.
func data_AudioFile_ReadProc(_ inClientData: UnsafeMutableRawPointer, _ inPosition: Int64, _ requestCount: UInt32, _ buffer: UnsafeMutableRawPointer, _ actualCount: UnsafeMutablePointer<UInt32>) -> OSStatus {
let data = inClientData.assumingMemoryBound(to: Data.self).pointee
let bufferPointer = UnsafeMutableRawBufferPointer(start: buffer, count: Int(requestCount))
let copied = data.copyBytes(to: bufferPointer, from: Int(inPosition) ..< Int(inPosition) + Int(requestCount))
actualCount.pointee = UInt32(copied)
return noErr
}
func data_AudioFile_GetSizeProc(_ inClientData: UnsafeMutableRawPointer) -> Int64 {
let data = inClientData.assumingMemoryBound(to: Data.self).pointee
return Int64(data.count)
}
extension Data {
func convertedTo(_ format: AVAudioFormat) -> AVAudioPCMBuffer? {
var data = self
var af: AudioFileID? = nil
var status = AudioFileOpenWithCallbacks(&data, data_AudioFile_ReadProc, nil, data_AudioFile_GetSizeProc(_:), nil, 0, &af)
guard status == noErr, af != nil else {
return nil
}
defer {
AudioFileClose(af!)
}
var eaf: ExtAudioFileRef? = nil
status = ExtAudioFileWrapAudioFileID(af!, false, &eaf)
guard status == noErr, eaf != nil else {
return nil
}
defer {
ExtAudioFileDispose(eaf!)
}
var clientFormat = format.streamDescription.pointee
status = ExtAudioFileSetProperty(eaf!, kExtAudioFileProperty_ClientDataFormat, UInt32(MemoryLayout.size(ofValue: clientFormat)), &clientFormat)
guard status == noErr else {
return nil
}
if let channelLayout = format.channelLayout {
var clientChannelLayout = channelLayout.layout.pointee
status = ExtAudioFileSetProperty(eaf!, kExtAudioFileProperty_ClientChannelLayout, UInt32(MemoryLayout.size(ofValue: clientChannelLayout)), &clientChannelLayout)
guard status == noErr else {
return nil
}
}
var frameLength: Int64 = 0
var propertySize: UInt32 = UInt32(MemoryLayout.size(ofValue: frameLength))
status = ExtAudioFileGetProperty(eaf!, kExtAudioFileProperty_FileLengthFrames, &propertySize, &frameLength)
guard status == noErr else {
return nil
}
guard let pcmBuffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: AVAudioFrameCount(frameLength)) else {
return nil
}
let bufferSizeFrames = 512
let bufferSizeBytes = Int(format.streamDescription.pointee.mBytesPerFrame) * bufferSizeFrames
let numBuffers = format.isInterleaved ? 1 : Int(format.channelCount)
let numInterleavedChannels = format.isInterleaved ? Int(format.channelCount) : 1
let audioBufferList = AudioBufferList.allocate(maximumBuffers: numBuffers)
for i in 0 ..< numBuffers {
audioBufferList[i] = AudioBuffer(mNumberChannels: UInt32(numInterleavedChannels), mDataByteSize: UInt32(bufferSizeBytes), mData: malloc(bufferSizeBytes))
}
defer {
for buffer in audioBufferList {
free(buffer.mData)
}
free(audioBufferList.unsafeMutablePointer)
}
while true {
var frameCount: UInt32 = UInt32(bufferSizeFrames)
status = ExtAudioFileRead(eaf!, &frameCount, audioBufferList.unsafeMutablePointer)
guard status == noErr else {
return nil
}
if frameCount == 0 {
break
}
let src = audioBufferList
let dst = UnsafeMutableAudioBufferListPointer(pcmBuffer.mutableAudioBufferList)
if src.count != dst.count {
return nil
}
for i in 0 ..< src.count {
let srcBuf = src[i]
let dstBuf = dst[i]
memcpy(dstBuf.mData?.advanced(by: Int(dstBuf.mDataByteSize)), srcBuf.mData, Int(srcBuf.mDataByteSize))
}
pcmBuffer.frameLength += frameCount
}
return pcmBuffer
}
}
A more robust solution would probably read the sample rate and channel count and give the option to preserve them.
Tested using:
let url = URL(fileURLWithPath: "/tmp/test.mp3")
let data = try! Data(contentsOf: url)
let format = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 44100, channels: 1, interleaved: false)!
if let d = data.convertedTo(format) {
let avf = try! AVAudioFile(forWriting: URL(fileURLWithPath: "/tmp/foo.wav"), settings: format.settings, commonFormat: format.commonFormat, interleaved: format.isInterleaved)
try! avf.write(from: d)
}

swift CGPDFDocument parsing

I'm trying to use Swift to parse the contents of PDF documents, following Apple's programming guide (in which all the examples are ObjC...)
let filepath = "/Users/ben/Desktop/Test.pdf"
let localUrl = filepath as CFString
if let pdfURL = CFURLCreateWithFileSystemPath(nil, localUrl, CFURLPathStyle.cfurlposixPathStyle, false) {
if let pdf = CGPDFDocument(pdfURL) {
if let inf = pdf.info {
CGPDFDictionaryApplyFunction(inf, { (key, object, info) -> Void in
print("\(key), \(object), \(info)")
}, nil)
}
if let cat = pdf.catalog {
CGPDFDictionaryApplyFunction(cat, { (key, object, info) -> Void in
print("\(key), \(object), \(info)")
}, nil)
}
}
}
While this seems to produce some results, it's just strings of hex digits.
0x00007ff29f43ce00, 0x00007ff29f492bd0, nil
0x00007ff29f443b60, 0x00007ff29f492cd0, nil
0x00007ff29f482590, 0x00007ff29f492dd0, nil
0x00007ff29f482a40, 0x00007ff29f492ed0, nil
0x00007ff29f482e30, 0x00007ff29f492fe0, nil
0x00007ff29f47da20, 0x00007ff29f4930e0, nil
0x00007ff29f474ac0, 0x00007ff29f842b50, nil
0x00007ff29f43f5d0, 0x00007ff29f842bf0, nil
0x00007ff29f485eb0, 0x00007ff29f842a60, nil
0x00007ff29f482f70, 0x00007ff29f842ab0, nil
0x00007ff29f48b1c0, 0x00007ff29f48f6d0, nil
So how do I get the actual data? Ideally, I'm trying to get at the document metadata and things like fonts contained.
Swift 4 - Here is an updated version of Daniel's excellent example which compiles in Swift 4.
import Foundation
import Quartz
print("Hello, World!")
func printPDFKeys( key: UnsafePointer<Int8>, object: CGPDFObjectRef) { //, info: UnsafeMutableRawPointer) {
// let _: CGPDFDictionaryRef = CGPDFDictionaryRef(info)
let keyString = String(cString: UnsafePointer<CChar>(key), encoding: .isoLatin1)
let objectType = CGPDFObjectGetType(object)
if keyString == nil {
return
}
print("key \(keyString!) is present in dictionary, type \(objectType.rawValue)")
var ptrObjectValue:UnsafePointer<Int8>? = nil
switch objectType {
// ObjectType is enum of:
// Null
// Boolean
// Integer
// Real
// Name
// String
// Array
// Dictionary
// Stream
case .boolean:
// Boolean
var objectBoolean:CGPDFBoolean = 0
if CGPDFObjectGetValue(object, objectType, &objectBoolean) {
let testbool = NSNumber(value: objectBoolean)
print("Boolean value \(testbool)")
}
case .integer:
// Integer
var objectInteger:CGPDFInteger? = nil
if CGPDFObjectGetValue(object, objectType, &objectInteger) {
print("Integer value \(objectInteger)")
}
case .real:
// Real
var objectReal:CGPDFReal? = nil
if CGPDFObjectGetValue(object, objectType, &objectReal) {
print("Real value \(objectReal)")
}
case .name:
// Name
if (CGPDFObjectGetValue(object, objectType, &ptrObjectValue)) {
let stringName = String(cString: UnsafePointer<CChar>(ptrObjectValue!), encoding: String.Encoding.isoLatin1)
print("Name value: \(stringName!)")
}
case .string:
// String
_ = CGPDFObjectGetValue(object, objectType, &ptrObjectValue)
let stringValue = CGPDFStringCopyTextString(OpaquePointer(ptrObjectValue!))
print("String value: \(stringValue!)")
case .array:
// Array
print("Array")
var objectArray:CGPDFArrayRef? = nil
if (CGPDFObjectGetValue(object, objectType, &objectArray))
{
print("array: \(arrayFromPDFArray(pdfArray: objectArray!))")
}
case .dictionary:
// Dictionary
var objectDictionary:CGPDFDictionaryRef? = nil
if (CGPDFObjectGetValue(object, objectType, &objectDictionary)) {
let count = CGPDFDictionaryGetCount(objectDictionary!)
print("Found dictionary with \(count) entries")
if !(keyString == "Parent") && !(keyString == "P") {
//catalogLevel = catalogLevel + 1
CGPDFDictionaryApplyFunction(objectDictionary!, { (key, object, info) -> Void in
printPDFKeys(key: key, object: object) // , info: info)
}, nil)
// CGPDFDictionaryApplyFunction(objectDictionary!, printPDFKeys as! CGPDFDictionaryApplierFunction, nil)
//catalogLevel = catalogLevel - 1
}
}
case .stream:
// Stream
print("Stream")
var objectStream:CGPDFStreamRef? = nil
if (CGPDFObjectGetValue(object, objectType, &objectStream)) {
let _: CGPDFDictionaryRef = CGPDFStreamGetDictionary( objectStream! )!
var fmt: CGPDFDataFormat = .raw
let streamData: CFData = CGPDFStreamCopyData(objectStream!, &fmt)!;
let data = NSData(data: streamData as Data)
let dataString = NSString(data: data as Data, encoding: String.Encoding.utf8.rawValue)
let dataLength: Int = CFDataGetLength(streamData)
print("data stream (length=\(dataLength)):")
if dataLength < 400 {
print(dataString)
}
}
default:
print("Null")
}
}
// convert a PDF array into an objC one
func arrayFromPDFArray(pdfArray: CGPDFArrayRef ) -> NSMutableArray {
var _:Int = 0
let tmpArray: NSMutableArray = NSMutableArray()
let count = CGPDFArrayGetCount(pdfArray)
for i in 0..<count {
var value:CGPDFObjectRef? = nil
if (CGPDFArrayGetObject(pdfArray, i, &value)) {
if let object = objectForPDFObject(object: value!) {
tmpArray.add(object)
}
}
}
return tmpArray
}
func objectForPDFObject( object: CGPDFObjectRef) -> AnyObject? {
let objectType: CGPDFObjectType = CGPDFObjectGetType(object)
var ptrObjectValue:UnsafePointer<Int8>? = nil
switch (objectType) {
case .boolean:
// Boolean
var objectBoolean = CGPDFBoolean()
if CGPDFObjectGetValue(object, objectType, &objectBoolean) {
let testbool = NSNumber(value: objectBoolean)
return testbool
}
case .integer:
// Integer
var objectInteger = CGPDFInteger()
if CGPDFObjectGetValue(object, objectType, &objectInteger) {
return objectInteger as AnyObject
}
case .real:
// Real
var objectReal = CGPDFReal()
if CGPDFObjectGetValue(object, objectType, &objectReal) {
return objectReal as AnyObject
}
case .string:
_ = CGPDFObjectGetValue(object, objectType, &ptrObjectValue)
let stringValue = CGPDFStringCopyTextString(OpaquePointer(ptrObjectValue!))
return stringValue
case .dictionary:
// Dictionary
var objectDictionary:CGPDFDictionaryRef? = nil
if (CGPDFObjectGetValue(object, objectType, &objectDictionary)) {
let count = CGPDFDictionaryGetCount(objectDictionary!)
print("In array, found dictionary with \(count) entries")
CGPDFDictionaryApplyFunction(objectDictionary!, { (key, object, info) -> Void in
printPDFKeys(key: key, object: object) // , info: info)
}, nil)
// CGPDFDictionaryApplyFunction(objectDictionary!, printPDFKeys as! CGPDFDictionaryApplierFunction, nil)
}
case .stream:
// Stream
var objectStream:CGPDFStreamRef? = nil
if (CGPDFObjectGetValue(object, objectType, &objectStream)) {
let _: CGPDFDictionaryRef = CGPDFStreamGetDictionary( objectStream! )!
var fmt: CGPDFDataFormat = .raw
let streamData: CFData = CGPDFStreamCopyData(objectStream!, &fmt)!;
let data = NSData(data: streamData as Data)
let dataString = NSString(data: data as Data, encoding: String.Encoding.utf8.rawValue)
print("data stream (length=\(CFDataGetLength(streamData))):")
return dataString
}
default:
return nil
}
return nil
}
func parse () {
let filepath = ("~/Desktop/doc.pdf" as NSString).expandingTildeInPath
let urlDocument = NSURL(fileURLWithPath: filepath)
let myDocument = CGPDFDocument(urlDocument)
if myDocument != nil {
let numPages = myDocument?.numberOfPages
print("Number of pages: \(numPages)")
// Get complete catalog
let myCatalog = myDocument?.catalog
CGPDFDictionaryApplyFunction(myCatalog!, { (key, object, info) -> Void in
printPDFKeys(key: key, object: object) // , info: info)
}, nil)
// CGPDFDictionaryApplyFunction(myCatalog!, printPDFKeys, nil)
let myInfo = myDocument?.info
CGPDFDictionaryApplyFunction(myInfo!, { (key, object, info) -> Void in
printPDFKeys(key: key, object: object) // , info: info)
}, nil)
// CGPDFDictionaryApplyFunction(myInfo!, printPDFKeys, nil)
} else {
print("Cannot open PDF document")
}
}
parse()
Your parsing retrieving high level dictionary and info data is correct, but you need to expand the decoding in CGPDFDictionaryApplyFunction to display the values of PDF data according their types (integer, string, array, dictionary, and so on). The syntax of the CGPDFDictionaryApplierFunction you are calling is:
typealias CGPDFDictionaryApplierFunction = (UnsafePointer<Int8>, COpaquePointer, UnsafeMutablePointer<()>) -> Void
Your program is displaying the pointers to the data, you could access the data values according their types as below (Swift 2):
let filepath = "/Users/ben/Desktop/Test.pdf"
let urlDocument = NSURL(fileURLWithPath: filepath)
let myDocument = CGPDFDocumentCreateWithURL(urlDocument)
if myDocument != nil {
let numPages = CGPDFDocumentGetNumberOfPages(myDocument)
print("Number of pages: \(numPages)")
// Get complete catalog
let myCatalog = CGPDFDocumentGetCatalog(myDocument)
CGPDFDictionaryApplyFunction(myCatalog, printPDFKeys, nil)
let myInfo = CGPDFDocumentGetInfo(myDocument)
CGPDFDictionaryApplyFunction(myInfo, printPDFKeys, nil)
} else {
print("Cannot open PDF document")
}
In order to be called from the CGPDFDictionaryApplyFunction, the printPDFKeys is to be called as a global function (outside your main class), alternately you could insert the code in a closure of CGPDFDictionaryApplyFunction as in your example above. The below code is shortened and is not including complete protection against errors and null values.
func printPDFKeys( key: UnsafePointer<Int8>, object: COpaquePointer, info: UnsafeMutablePointer<()>) {
let contentDict: CGPDFDictionaryRef = CGPDFDictionaryRef(info)
let keyString = String(CString: UnsafePointer<CChar>(key), encoding: NSISOLatin1StringEncoding)
let objectType = CGPDFObjectGetType(object)
if keyString == nil {
return
}
print("key \(keyString!) is present in dictionary, type \(objectType.rawValue)")
var ptrObjectValue = UnsafePointer<Int8>()
switch objectType {
// ObjectType is enum of:
// Null
// Boolean
// Integer
// Real
// Name
// String
// Array
// Dictionary
// Stream
case .Boolean:
// Boolean
var objectBoolean = CGPDFBoolean()
if CGPDFObjectGetValue(object, objectType, &objectBoolean) {
let testbool = NSNumber(unsignedChar: objectBoolean)
print("Boolean value \(testbool)")
}
case .Integer:
// Integer
var objectInteger = CGPDFInteger()
if CGPDFObjectGetValue(object, objectType, &objectInteger) {
print("Integer value \(objectInteger)")
}
case .Real:
// Real
var objectReal = CGPDFReal()
if CGPDFObjectGetValue(object, objectType, &objectReal) {
print("Real value \(objectReal)")
}
case .Name:
// Name
if (CGPDFObjectGetValue(object, objectType, &ptrObjectValue)) {
let stringName = String(CString: UnsafePointer<CChar>(ptrObjectValue), encoding: NSISOLatin1StringEncoding)
print("Name value: \(stringName!)")
}
case .String:
// String
let valueFound = CGPDFObjectGetValue(object, objectType, &ptrObjectValue)
let stringValue = CGPDFStringCopyTextString(COpaquePointer(ptrObjectValue))
print("String value: \(stringValue!)")
case .Array:
// Array
print("Array")
var objectArray = CGPDFArrayRef()
if (CGPDFObjectGetValue(object, objectType, &objectArray))
{
print("array: \(arrayFromPDFArray(objectArray))")
}
case .Dictionary:
// Dictionary
var objectDictionary = CGPDFDictionaryRef()
if (CGPDFObjectGetValue(object, objectType, &objectDictionary)) {
let count = CGPDFDictionaryGetCount(objectDictionary)
print("Found dictionary with \(count) entries")
if !(keyString == "Parent") && !(keyString == "P") {
//catalogLevel = catalogLevel + 1
CGPDFDictionaryApplyFunction(objectDictionary, printPDFKeys, nil)
//catalogLevel = catalogLevel - 1
}
}
case .Stream:
// Stream
print("Stream")
var objectStream = CGPDFStreamRef()
if (CGPDFObjectGetValue(object, objectType, &objectStream)) {
let dict: CGPDFDictionaryRef = CGPDFStreamGetDictionary( objectStream )
var fmt: CGPDFDataFormat = .Raw
let streamData: CFDataRef = CGPDFStreamCopyData(objectStream, &fmt)!;
let data = NSData(data: streamData)
let dataString = NSString(data: data, encoding: NSUTF8StringEncoding)
let dataLength: Int = CFDataGetLength(streamData)
print("data stream (length=\(dataLength)):")
if dataLength < 400 {
print(dataString)
}
}
default:
print("Null")
}
}
// convert a PDF array into an objC one
func arrayFromPDFArray(pdfArray: CGPDFArrayRef ) -> NSMutableArray {
var i:Int = 0
var tmpArray: NSMutableArray = NSMutableArray()
let count = CGPDFArrayGetCount(pdfArray)
for i in 0..<count {
var value = CGPDFObjectRef()
if (CGPDFArrayGetObject(pdfArray, i, &value)) {
if let object = objectForPDFObject(value) {
tmpArray.addObject(object)
}
}
}
return tmpArray
}
func objectForPDFObject( object: CGPDFObjectRef) -> AnyObject? {
let objectType: CGPDFObjectType = CGPDFObjectGetType(object)
var ptrObjectValue = UnsafePointer<Int8>()
switch (objectType) {
case .Boolean:
// Boolean
var objectBoolean = CGPDFBoolean()
if CGPDFObjectGetValue(object, objectType, &objectBoolean) {
let testbool = NSNumber(unsignedChar: objectBoolean)
return testbool
}
case .Integer:
// Integer
var objectInteger = CGPDFInteger()
if CGPDFObjectGetValue(object, objectType, &objectInteger) {
return objectInteger
}
case .Real:
// Real
var objectReal = CGPDFReal()
if CGPDFObjectGetValue(object, objectType, &objectReal) {
return objectReal
}
case .String:
let valueFound = CGPDFObjectGetValue(object, objectType, &ptrObjectValue)
let stringValue = CGPDFStringCopyTextString(COpaquePointer(ptrObjectValue))
return stringValue
case .Dictionary:
// Dictionary
var objectDictionary = CGPDFDictionaryRef()
if (CGPDFObjectGetValue(object, objectType, &objectDictionary)) {
let count = CGPDFDictionaryGetCount(objectDictionary)
print("In array, found dictionary with \(count) entries")
CGPDFDictionaryApplyFunction(objectDictionary, printPDFKeys, nil)
}
case .Stream:
// Stream
var objectStream = CGPDFStreamRef()
if (CGPDFObjectGetValue(object, objectType, &objectStream)) {
let dict: CGPDFDictionaryRef = CGPDFStreamGetDictionary( objectStream )
var fmt: CGPDFDataFormat = .Raw
let streamData: CFDataRef = CGPDFStreamCopyData(objectStream, &fmt)!;
let data = NSData(data: streamData)
let dataString = NSString(data: data, encoding: NSUTF8StringEncoding)
print("data stream (length=\(CFDataGetLength(streamData))):")
return dataString
}
default:
return nil
}
return nil
}
Made a parser (based on previous answers) that crawls the PDF hierarchy and gives you a JSON.
// Parse PDF into JSON.
PDFParser.parse(pdfUrl: pdfFileURL, into: jsonFileURL)
// Parse PDF into Dictionary.
let pdf: [String:Any?] = PDFParser.parse(pdfUrl: pdfFileURL)
Gives you:
{
"Catalog" : {
"Pages<Dictionary>" : {
"MediaBox<Array>" : [
0,
0,
612,
792
],
"Type<Name>" : "Pages",
"Kids<Array>" : [
{
"Rotate<Integer>" : 0,
"MediaBox<Array>" : [
0,
0,
595.27499999999998,
841.88999999999999
],
"Parent<Dictionary>" : "<PARENT_NOT_SERIALIZED>",
"Resources<Dictionary>" : {
"ColorSpace<Dictionary>" : {
"Cs1<Array>" : [
"ICCBased",
{
"N<Integer>" : 3,
"Filter<Name>" : "FlateDecode",
"Alternate<Name>" : "DeviceRGB",
"Length<Integer>" : 2612
}
]
}
...
To get from CGPDFDocument (like original question):
// Get document catalog.
guard
let document = CGPDFDocument(pdfFileURL as CFURL),
let catalog = document.catalog
else { return }
// Parse into dictionary.
let catalogDictionary = PDFParser.value(from: catalog)
Gives you a pretty usual Swift dictionary. Console output:
Optional(["Pages<Dictionary>": Optional({
"Count<Integer>" = 1;
"Kids<Array>" = (
{
"ArtBox<Array>" = (
"28.3465",
"325.193",
"393.389",
"813.543"
);
"Contents<Stream>" = {
Data = "q Q q 0 0 595.276 841.89 re W n 1 0 1 0 k /Gs1 gs 201.8862 420.9449 m 201.8862\n473.8269 244.7562 516.6959 297.6372 516.6959 c 350.5192 516.6959 393.3892\n473.8269 393.3892 420.9449 c 393.3892 368.0629 350.5192 325.1939 297.6372\n325.1939 c 244.7562 325.1939 201.8862 368.0629 201.8862 420.9449 c f Q q 28.346 530.078 283.464 283.465\nre W n 0 0 0 1 k /Gs1 gs BT 12 0 0 12 28.3467 803.499 Tm /Tc1 1 Tf [ (h) 4\n(ttp://epp) 7 (z.eu) ] TJ ET Q";
"Filter<Name>" = FlateDecode;
"Length<Integer>" = 237;
};
"MediaBox<Array>" = (
0,
0,
"595.2760000000001",
"841.89"
);
"Parent<Dictionary>" = "<PARENT_NOT_SERIALIZED>";
"Resources<Dictionary>" = {
"ExtGState<Dictionary>" = {
"Gs1<Dictionary>" = {
"OPM<Integer>" = 1;
"Type<Name>" = ExtGState;
};
};
...
ParsePDF.swift:
//
// PDFParser.swift
// PDFParser
//
// Copyright (c) 2020 Geri Borbás http://www.twitter.com/_eppz
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import Foundation
import PDFKit
class PDFParser
{
/// Shorthand for type strings.
static let namesForTypes: [CGPDFObjectType:String] =
[
.null : "Null",
.boolean : "Boolean",
.integer : "Integer",
.real : "Real",
.name : "Name",
.string : "String",
.array : "Array",
.dictionary : "Dictionary",
.stream : "Stream",
CGPDFObjectTypeObject : "Object",
]
struct Message
{
static let parentNotSerialized = "<PARENT_NOT_SERIALIZED>"
static let couldNotParseValue = "<COULD_NOT_PARSE_VALUE>"
static let couldNotGetStreamData = "<COULD_NOT_GET_STREAM_DATA>"
static let unknownStreamDataFormat = "<UNKNOWN_STREAM_DATA_FORMAT>"
}
/// Parse a PDF file into a JSON file.
static func parse(pdfUrl: URL, into jsonURL: URL)
{
do
{
let pdf = PDFParser.parse(pdfUrl: pdfUrl)
let data = try JSONSerialization.data(withJSONObject: pdf, options: .prettyPrinted)
try data.write(to: jsonURL, options: [])
}
catch
{ print(error) }
}
/// Parse a PDF file into a JSON file.
static func parse(pdfUrl: URL) -> [String:Any?]
{
// Document.
guard
let document = CGPDFDocument(pdfUrl as CFURL),
let catalog = document.catalog,
let info = document.info
else
{
print("Cannot open PDF.")
return [:]
}
// Parse.
return [
"Catalog" : PDFParser.value(from: catalog),
"Info" : PDFParser.value(from: info)
]
}
static func value(from object: CGPDFObjectRef) -> Any?
{
switch (CGPDFObjectGetType(object))
{
case .null:
return nil
case .boolean:
var valueRef: CGPDFBoolean = 0
if CGPDFObjectGetValue(object, .boolean, &valueRef)
{ return Bool(valueRef == 0x01) }
case .integer:
var valueRef: CGPDFInteger = 0
if CGPDFObjectGetValue(object, .integer, &valueRef)
{ return valueRef as Int }
case .real:
var valueRef: CGPDFReal = 0.0
if CGPDFObjectGetValue(object, .real, &valueRef)
{ return Double(valueRef) }
case .name:
var objectRefOrNil: UnsafePointer<Int8>? = nil
if
CGPDFObjectGetValue(object, .name, &objectRefOrNil),
let objectRef = objectRefOrNil,
let string = String(cString: objectRef, encoding: String.Encoding.isoLatin1)
{ return string }
case .string:
var objectRefOrNil: UnsafePointer<Int8>? = nil
if
CGPDFObjectGetValue(object, .string, &objectRefOrNil),
let objectRef = objectRefOrNil,
let stringRef = CGPDFStringCopyTextString(OpaquePointer(objectRef))
{ return stringRef as String }
case .array:
var arrayRefOrNil: CGPDFArrayRef? = nil
if
CGPDFObjectGetValue(object, .array, &arrayRefOrNil),
let arrayRef = arrayRefOrNil
{
var array: [Any] = []
for index in 0 ..< CGPDFArrayGetCount(arrayRef)
{
var eachObjectRef: CGPDFObjectRef? = nil
if
CGPDFArrayGetObject(arrayRef, index, &eachObjectRef),
let eachObject = eachObjectRef,
let eachValue = PDFParser.value(from: eachObject)
{ array.append(eachValue) }
}
return array
}
case .stream:
var streamRefOrNil: CGPDFStreamRef? = nil
if
CGPDFObjectGetValue(object, .stream, &streamRefOrNil),
let streamRef = streamRefOrNil,
let streamDictionaryRef = CGPDFStreamGetDictionary(streamRef)
{
// Get stream dictionary.
var streamNSMutableDictionary = NSMutableDictionary()
Self.collectObjects(from: streamDictionaryRef, into: &streamNSMutableDictionary)
var streamDictionary = streamNSMutableDictionary as! [String: Any?]
// Get data.
var dataString: String? = Message.couldNotGetStreamData
var streamDataFormat: CGPDFDataFormat = .raw
if let streamData: CFData = CGPDFStreamCopyData(streamRef, &streamDataFormat)
{
switch streamDataFormat
{
case .raw: dataString = String(data: NSData(data: streamData as Data) as Data, encoding: String.Encoding.utf8)
case .jpegEncoded, .JPEG2000: dataString = NSData(data: streamData as Data).base64EncodedString()
#unknown default: dataString = Message.unknownStreamDataFormat
}
}
// Add to dictionary.
streamDictionary["Data"] = dataString
return streamDictionary
}
case .dictionary:
var dictionaryRefOrNil: CGPDFDictionaryRef? = nil
if
CGPDFObjectGetValue(object, .dictionary, &dictionaryRefOrNil),
let dictionaryRef = dictionaryRefOrNil
{
var dictionary = NSMutableDictionary()
Self.collectObjects(from: dictionaryRef, into: &dictionary)
return dictionary as! [String: Any?]
}
#unknown default:
var dictionary = NSMutableDictionary()
Self.collectObjects(from: object, into: &dictionary)
return dictionary as! [String: Any?]
}
// No known case.
return nil
}
static func collectObjects(from dictionaryRef: CGPDFDictionaryRef, into dictionaryPointer: UnsafeMutableRawPointer?)
{
CGPDFDictionaryApplyFunction(
dictionaryRef,
{
(eachKeyPointer, eachObject, eachContextOrNil: UnsafeMutableRawPointer?) -> Void in
// Unwrap dictionary.
guard let dictionary = eachContextOrNil?.assumingMemoryBound(to: NSMutableDictionary.self).pointee
else { return print("Could not unwrap dictionary.") }
// Unwrap key.
guard let eachKey = String(cString: UnsafePointer<CChar>(eachKeyPointer), encoding: .isoLatin1)
else { return print("Could not unwrap key.") }
// Type.
guard let eachTypeName = PDFParser.namesForTypes[CGPDFObjectGetType(eachObject)]
else { return print("Could not unwrap type.") }
// Assemble.
let eachDictionaryKey = "\(eachKey)<\(eachTypeName)>" as NSString
// Skip parent.
guard eachKey != "Parent"
else
{
dictionary.setObject(Message.parentNotSerialized, forKey: eachDictionaryKey)
return
}
// Parse value.
guard let eachValue = PDFParser.value(from: eachObject)
else
{
dictionary.setObject(Message.couldNotParseValue, forKey: eachDictionaryKey)
fatalError("😭")
// return
}
// Set.
dictionary.setObject(eachValue, forKey: eachDictionaryKey)
},
dictionaryPointer
)
}
}

Displaying Artwork for .MP3 file

I am trying to currently display the album artwork for a locally stored .MP3 track in an ImageView. Does anyone know how to fetch this artwork in Swift in order to accomplish this?
I have found this solution (iOS AVFoundation: How do I fetch artwork from an mp3 file?) but the code is written in Objective C. I simply want to grab the image embedded in my MP3 and display it in my ImageView.
I've looked at the API documentation for the MPMediaItemArtwork and found an example that also accomplishes what I am trying to accomplish in Objective C as well here(http://www.codeitive.com/0zHjkUjUWX/not-able-to-get-the-uiimage-from-mpmediaitempropertyartwork.html) but cannot come up with a solution. My code is as follows:
import UIKit
import AVFoundation
import MediaPlayer
class ViewController: UIViewController {
let audioPath:NSURL! = NSBundle.mainBundle().URLForResource("SippinOnFire", withExtension: "mp3")
#IBOutlet var artistImage: UIImageView!
#IBOutlet var trackLabel: UILabel!
#IBOutlet var artistLabel: UILabel!
#IBOutlet var sliderValue: UISlider!
var player:AVAudioPlayer = AVAudioPlayer()
#IBAction func play(sender: AnyObject) {
let audioInfo = MPNowPlayingInfoCenter.defaultCenter()
println(audioInfo)
player.play()
//println("Playing \(audioPath)")
let playerItem = AVPlayerItem(URL: audioPath)
let metadataList = playerItem.asset.metadata as! [AVMetadataItem]
for item in metadataList {
if let stringValue = item.value {
println(item.commonKey)
if item.commonKey == "title" {
trackLabel.text = stringValue as? String
}
if item.commonKey == "artist" {
artistLabel.text = stringValue as? String
}
if item.commonKey == "artwork" {
if let audioImage = UIImage(data: item.value as! NSData) {
let audioArtwork = MPMediaItemArtwork(image: audioImage)
println(audioImage.description)
}
}
}
}
}
#IBAction func pause(sender: AnyObject) {
player.pause()
}
#IBAction func stop(sender: AnyObject) {
player.stop()
player.currentTime = 0;
}
#IBAction func sliderChanged(sender: AnyObject) {
player.volume = sliderValue.value
}
override func viewDidLoad() {
super.viewDidLoad()
var error:NSError? = nil
player = AVAudioPlayer(contentsOfURL: audioPath!, error: &error)
player.volume = 0.5;
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Here is a screen shot of my sample .mp3 file. As you can see there is indeed album artwork that is both visible in the "get info" section of Finder. I've also opened the .mp3 in my iTunes to make sure and have confirmed there is artwork in the "get info" section of it there as well as under the "artwork" tab.
However, when trying to use the commonKey to assign the image to my imageView I find that there is no commonKey for "artwork".
Thanks
Change your snippet of code into this (I already tested it):
I added println lines commented in places of interest, Feel free to uncomment in order to see what is happening.
for item in metadataList {
if item.commonKey == nil{
continue
}
if let key = item.commonKey, let value = item.value {
//println(key)
//println(value)
if key == "title" {
trackLabel.text = value as? String
}
if key == "artist" {
artistLabel.text = value as? String
}
if key == "artwork" {
if let audioImage = UIImage(data: value as! NSData) {
//println(audioImage.description)
artistImage.image = audioImage
}
}
}
}
UPDATE: A bit of clean up of this code
for item in metadataList {
guard let key = item.commonKey, let value = item.value else{
continue
}
switch key {
case "title" : trackLabel.text = value as? String
case "artist": artistLabel.text = value as? String
case "artwork" where value is NSData : artistImage.image = UIImage(data: value as! NSData)
default:
continue
}
}
UPDATE: For Swift 4
for item in metadataList {
guard let key = item.commonKey?.rawValue, let value = item.value else{
continue
}
switch key {
case "title" : trackLabel.text = value as? String
case "artist": artistLabel.text = value as? String
case "artwork" where value is Data : artistImage.image = UIImage(data: value as! Data)
default:
continue
}
}
edit/update Swift 4 or later:
import MediaPlayer
var nowPlayingInfo: [String: Any] = [:]
let playerItem = AVPlayerItem(url: url)
let metadataList = playerItem.asset.metadata
for item in metadataList {
switch item.commonKey {
case .commonKeyTitle?:
nowPlayingInfo[MPMediaItemPropertyTitle] = item.stringValue ?? ""
case .commonKeyType?:
nowPlayingInfo[MPMediaItemPropertyGenre] = item.stringValue ?? ""
case .commonKeyAlbumName?:
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = item.stringValue ?? ""
case .commonKeyArtist?:
nowPlayingInfo[MPMediaItemPropertyArtist] = item.stringValue ?? ""
case .commonKeyArtwork?:
if let data = item.dataValue,
let image = UIImage(data: data) {
nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in image }
}
case .none: break
default: break
}
}
let audioInfo = MPNowPlayingInfoCenter.default()
audioInfo.nowPlayingInfo = nowPlayingInfo
Note: You will have to invoke beginReceivingRemoteControlEvents() otherwise it will not work on the actual device. You will also need to set your app Background Modes (Audio and AirPlay) and set your AVAudioSession category to AVAudioSessionCategoryPlayback and set it active:
do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.mixWithOthers, .allowAirPlay])
print("Playback OK")
try AVAudioSession.sharedInstance().setActive(true)
print("Session is Active")
} catch {
print(error)
}
Try this:
It appears that sometimes iOS 8 returns nil at first attempt of obtaining this info:
if let audioCenter = MPNowPlayingInfoCenter.defaultCenter(){
if let audioInfo = audioCenter.nowPlayingInfo{
if let artwork = audioInfo[MPMediaItemPropertyArtwork] as? MPMediaItemArtwork
{
var image: UIImage? = artwork.imageWithSize(artwork.bounds.size)
if image == nil {
image = artwork.imageWithSize(artwork.bounds.size);
}
if image != nil{
println("image loaded")
}
}
}
}