I understand PDFKit allows extracting text+formatting as NSAttributedString, but I can't find any info on extracting each individual figures from any PDF document using Swift.
Any help would be greatly appreciated, thanks!
edit: https://stackoverflow.com/a/40788449/2303865 explains how to convert the whole page into image, however I need to parse all images already part of the a series of PDF documents, without knowing where they are located, so that solution is not appropriate to my question.
Here is a Swift function that extracts images, more specifically all Objects with Subtype "Image" from pdf pages:
import PDFKit
func extractImages(from pdf: PDFDocument, extractor: #escaping (ImageInfo)->Void) throws {
for pageNumber in 0..<pdf.pageCount {
guard let page = pdf.page(at: pageNumber) else {
throw PDFReadError.couldNotOpenPageNumber(pageNumber)
}
try extractImages(from: page, extractor: extractor)
}
}
func extractImages(from page: PDFPage, extractor: #escaping (ImageInfo)->Void) throws {
let pageNumber = page.label ?? "unknown page"
guard let page = page.pageRef else {
throw PDFReadError.couldNotOpenPage(pageNumber)
}
guard let dictionary = page.dictionary else {
throw PDFReadError.couldNotOpenDictionaryOfPage(pageNumber)
}
guard let resources = dictionary[CGPDFDictionaryGetDictionary, "Resources"] else {
throw PDFReadError.couldNotReadResources(pageNumber)
}
if let xObject = resources[CGPDFDictionaryGetDictionary, "XObject"] {
print("reading resources of page", pageNumber)
func extractImage(key: UnsafePointer<Int8>, object: CGPDFObjectRef, info: UnsafeMutableRawPointer?) -> Bool {
guard let stream: CGPDFStreamRef = object[CGPDFObjectGetValue, .stream] else { return true }
guard let dictionary = CGPDFStreamGetDictionary(stream) else {return true}
guard dictionary.getName("Subtype", CGPDFDictionaryGetName) == "Image" else {return true}
let colorSpaces = dictionary.getNameArray(for: "ColorSpace") ?? []
let filter = dictionary.getNameArray(for: "Filter") ?? []
var format = CGPDFDataFormat.raw
guard let data = CGPDFStreamCopyData(stream, &format) as Data? else { return false }
extractor(
ImageInfo(
name: String(cString: key),
colorSpaces: colorSpaces,
filter: filter,
format: format,
data: data
)
)
return true
}
CGPDFDictionaryApplyBlock(xObject, extractImage, nil)
}
}
struct ImageInfo: CustomDebugStringConvertible {
let name: String
let colorSpaces: [String]
let filter: [String]
let format: CGPDFDataFormat
let data: Data
var debugDescription: String {
"""
Image "\(name)"
- color spaces: \(colorSpaces)
- format: \(format == .JPEG2000 ? "JPEG2000" : format == .jpegEncoded ? "jpeg" : "raw")
- filters: \(filter)
- size: \(ByteCountFormatter.string(fromByteCount: Int64(data.count), countStyle: .binary))
"""
}
}
extension CGPDFObjectRef {
func getName<K>(_ key: K, _ getter: (OpaquePointer, K, UnsafeMutablePointer<UnsafePointer<Int8>?>)->Bool) -> String? {
guard let pointer = self[getter, key] else { return nil }
return String(cString: pointer)
}
func getName<K>(_ key: K, _ getter: (OpaquePointer, K, UnsafeMutableRawPointer?)->Bool) -> String? {
guard let pointer: UnsafePointer<UInt8> = self[getter, key] else { return nil }
return String(cString: pointer)
}
subscript<R, K>(_ getter: (OpaquePointer, K, UnsafeMutablePointer<R?>)->Bool, _ key: K) -> R? {
var result: R!
guard getter(self, key, &result) else { return nil }
return result
}
subscript<R, K>(_ getter: (OpaquePointer, K, UnsafeMutableRawPointer?)->Bool, _ key: K) -> R? {
var result: R!
guard getter(self, key, &result) else { return nil }
return result
}
func getNameArray(for key: String) -> [String]? {
var object: CGPDFObjectRef!
guard CGPDFDictionaryGetObject(self, key, &object) else { return nil }
if let name = object.getName(.name, CGPDFObjectGetValue) {
return [name]
} else {
guard let array: CGPDFArrayRef = object[CGPDFObjectGetValue, .array] else {return nil}
var names = [String]()
for index in 0..<CGPDFArrayGetCount(array) {
guard let name = array.getName(index, CGPDFArrayGetName) else { continue }
names.append(name)
}
return names
}
}
}
enum PDFReadError: Error {
case couldNotOpenPageNumber(Int)
case couldNotOpenPage(String)
case couldNotOpenDictionaryOfPage(String)
case couldNotReadResources(String)
case cannotReadXObjectStream(xObject: String, page: String)
}
You should know that images in PDFs can be represented in different ways. They can be embedded as self contained JPGs or they can be embedded as raw pixel data (lossless compressed or not) with meta information about the compression, color space, width, height, and so forth.
So if you want to export embedded JPGs: this code works just fine. But if you also want to visualise the raw images you will need even more parsing code. To get started you can look at the PDF 2.0 spec (or an older free version of the spec), and this gist which interprets JPGs in any color profile and raw images with any of the following color profiles:
DeviceGray
DeviceRGB
DeviceCMYK
Indexed
ICCBased
Related
I have a newBid object, that contains some data and images array. I want to upload all images and data to the server and zip those upload observables. If I create separate drivers for data, image1 and image2, I succeed.
But what I want to really do is to not hardcode images since array may contain from 0 to 10 images. I want to create observables array programmatically and zip them.
let dataSaved = saveTaps.withLatestFrom(newBid)
.flatMapLatest { bid in
return CustomerManager.shared.bidCreate(bid: bid)
.trackActivity(activityIndicator)
.asDriver(onErrorJustReturn: false)
}
let photoSaved0 = saveTaps.withLatestFrom(newBid)
.flatMapLatest { bid in
return CustomerManager.shared.bidUploadFile(image: bid.images[0])
.trackActivity(activityIndicator)
.asDriver(onErrorJustReturn: false)
}
let photoSaved1 = saveTaps.withLatestFrom(newBid)
.flatMapLatest { bid in
return CustomerManager.shared.bidUploadFile(image: bid.images[1])
.trackActivity(activityIndicator)
.asDriver(onErrorJustReturn: false)
}
saveCompleted = Driver.zip(dataSaved, photoSaved0, photoSaved1){ return $0 && $1 && $2 }
/*
// 0. Getting array of images from newBid
let images = newBid.map { bid in
return bid.images
}
// 1. Creating array of upload drivers from array of images
let imageUploads = images.map { (images: [UIImage]) -> [Driver<Bool>] in
var temp = [Driver<Bool>]()
return temp
}
// 2. Zipping array of upload drivers to photoSaved driver
photoSaved = Driver
.zip(imageUploads) // wait for all image requests to finish
.subscribe(onNext: { results in
// here you have every single image in the 'images' array
results.forEach { print($0) }
})
.disposed(by: disposeBag)*/
In the commented section if I try to zip imageUploads, I get error:
Argument type 'SharedSequence<DriverSharingStrategy, [SharedSequence<DriverSharingStrategy, Bool>]>' does not conform to expected type 'Collection'
How about something like this?
let saves = saveTaps.withLatestFrom(newBid)
.flatMapLatest { (bid: Bid) -> Observable<[Bool]> in
let dataSaved = CustomerManager.shared.bidCreate(bid: bid)
.catchErrorJustReturn(false)
let photosSaved = bid.images.map {
CustomerManager.shared.bidUploadFile(image: $0, bidID: bid.id)
.catchErrorJustReturn(false)
}
return Observable.zip([dataSaved] + photosSaved)
.trackActivity(activityIndicator)
}
.asDriver(onErrorJustReturn: []) // remove this line if you want an Observable<[Bool]>.
Final solution
let bidID: Driver<Int> = saveTaps.withLatestFrom(newBid)
.flatMapLatest { bid in
return CustomerManager.shared.bidCreate(bid: bid)
.trackActivity(activityIndicator)
.asDriver(onErrorJustReturn: 0)
}
saveCompleted = Driver.combineLatest(bidID, newBid) { bidID, newBid in
newBid.uploadedImages.map {
CustomerManager.shared.bidUploadFile(image: $0, bidID: bidID).asDriver(onErrorJustReturn: false)
}
}.flatMap { imageUploads in
return Driver.zip(imageUploads).trackActivity(activityIndicator).asDriver(onErrorJustReturn: [])
}.map{ (results:[Bool]) -> Bool in
return !results.contains(false)
}
It is a combined version of this which is equivalent:
let imageUploads: Driver<[Driver<Bool>]> = Driver.combineLatest(bidID, newBid) { bidID, newBid in
newBid.uploadedImages.map {
CustomerManager.shared.bidUploadFile(image: $0, bidID: bidID).asDriver(onErrorJustReturn: false)
}
}
let photosSaved: Driver<[Bool]> = imageUploads.flatMap { imageUploads in
return Driver.zip(imageUploads).trackActivity(activityIndicator).asDriver(onErrorJustReturn: [])
}
saveCompleted = photosSaved.map{ (results:[Bool]) -> Bool in
return !results.contains(false)
}
Reason For Post
There are so many different solutions & examples on how to build a proper networking layer, but every app has different constraints, and design decisions are made based on trade-offs, leaving me uncertain about the quality of code I've written. If there are any Anti-Patterns, redundancies, or flat out bad solutions within my code that I have overlooked or simply lacked the knowledge to address, please do critique. This is a project I'd like to add to my portfolio, so I'm posting it here to get eyes on it, with some advice/tips.
Thanks for your time in advanced!
Some characteristics of my networking layer that I think could raise eyebrows:
Method contains a GETALL case, to indicate a list of data that must be fetched. I have not seen this in any of the open source code I've read. Is this a code smell?
enum Method {
case GET
/// Indicates how JSON response should be handled differently to abastract a list of entities
case GETALL
case PUT
case DELETE
}
I've made it, so each Swift Entity conforms to JSONable protocol, meaning it can be initialized with json and converted to json.
protocol JSONable {
init?(json: [String: AnyObject])
func toJSON() -> Data?
}
JSONable in practice with one of my entities:
struct User {
var id: String
var name: String
var location: String
var rating: Double
var keywords: NSArray
var profileImageUrl: String
}
extension User: JSONable {
init?(json: [String : AnyObject]) {
guard let id = json[Constant.id] as? String, let name = json[Constant.name] as? String, let location = json[Constant.location] as? String, let rating = json[Constant.rating] as? Double, let keywords = json[Constant.keywords] as? NSArray, let profileImageUrl = json[Constant.profileImageUrl] as? String else {
return nil
}
self.init(id: id, name: name, location: location, rating: rating, keywords: keywords, profileImageUrl: profileImageUrl)
}
func toJSON() -> Data? {
let data: [String: Any] = [Constant.id: id, Constant.name: name, Constant.location: location, Constant.rating: rating, Constant.keywords: keywords, Constant.profileImageUrl: profileImageUrl]
let jsonData = try? JSONSerialization.data(withJSONObject: data, options: [])
return jsonData
}
}
This allows me to use generics to initialize all my entities in my client- FirebaseAPI, after I retrieve JSON response. I also haven't seen this technique in the code I've read.
In the code below, notice how GETALL is implemented to flatten the list of JSON objects. Should I have to do this at all? Is there a better way to handle any type of Json structure response?
AND Entities are initialized generically, and returned as an Observable ( Using RxSwift ).
Do you sense any code smells?
/// Responsible for Making actual API requests & Handling response
/// Returns an observable object that conforms to JSONable protocol.
/// Entities that confrom to JSONable just means they can be initialized with json & transformed from swift to JSON.
func rx_fireRequest<Entity: JSONable>(_ endpoint: FirebaseEndpoint, ofType _: Entity.Type ) -> Observable<[Entity]> {
return Observable.create { [weak self] observer in
self?.session.dataTask(with: endpoint.request, completionHandler: { (data, response, error) in
/// Parse response from request.
let parsedResponse = Parser(data: data, response: response, error: error)
.parse()
switch parsedResponse {
case .error(let error):
observer.onError(error)
return
case .success(let data):
var entities = [Entity]()
switch endpoint.method {
/// Flatten JSON strucuture to retrieve a list of entities.
/// Denoted by 'GETALL' method.
case .GETALL:
/// Key (underscored) is unique identifier for each entity
/// value is k/v pairs of entity attributes.
for (_, value) in data {
if let value = value as? [String: AnyObject], let entity = Entity(json: value) {
entities.append(entity)
}
}
/// Force downcast for generic type inference.
observer.onNext(entities as! [Entity])
//observer.onCompleted()
/// All other methods return JSON that can be used to initialize JSONable entities
default:
if let entity = Entity(json: data) {
observer.onNext([entity] as! [Entity])
//observer.onCompleted()
} else {
observer.onError(NetworkError.initializationFailure)
}
}
}
}).resume()
return Disposables.create()
}
}
}
I manage different endpoints like so:
enum FirebaseEndpoint {
case saveUser(data: [String: AnyObject])
case fetchUser(id: String)
case removeUser(id: String)
case saveItem(data: [String: AnyObject])
case fetchItem(id: String)
case fetchItems
case removeItem(id: String)
case saveMessage(data: [String: AnyObject])
case fetchMessages(chatroomId: String)
case removeMessage(id: String)
}
extension FirebaseEndpoint: Endpoint {
var base: String {
// Add this as a constant to APP Secrts struct & dont include secrets file when pushed to github.
return "https://AppName.firebaseio.com"
}
var path: String {
switch self {
case .saveUser(let data): return "/\(Constant.users)/\(data[Constant.id])"
case .fetchUser(let id): return "/\(Constant.users)/\(id)"
case .removeUser(let id): return "/\(Constant.users)/\(id)"
case .saveItem(let data): return "/\(Constant.items)/\(data[Constant.id])"
case .fetchItem(let id): return "/\(Constant.items)/\(id)"
case .fetchItems: return "/\(Constant.items)"
case .removeItem(let id): return "/\(Constant.items)/\(id)"
case .saveMessage(let data): return "/\(Constant.messages)/\(data[Constant.id])"
case .fetchMessages(let chatroomId): return "\(Constant.messages)/\(chatroomId)"
case .removeMessage(let id): return "/\(Constant.messages)/\(id)"
}
}
var method: Method {
switch self {
case .fetchUser, .fetchItem: return .GET
case .fetchItems, .fetchMessages: return .GETALL
case .saveUser, .saveItem, .saveMessage: return .PUT
case .removeUser, .removeItem, .removeMessage: return .DELETE
}
}
var body: [String : AnyObject]? {
switch self {
case .saveItem(let data), .saveUser(let data), .saveMessage(let data): return data
default: return nil
}
}
}
Last thing, I'd like someone with professional eyes to look at is, how I use MVVM. I make all network requests from view model, which comes out looking something like this:
struct SearchViewModel {
// Outputs
var collectionItems: Observable<[Item]>
var error: Observable<Error>
init(controlValue: Observable<Int>, api: FirebaseAPI, user: User) {
let serverItems = controlValue
.map { ItemCategory(rawValue: $0) }
.filter { $0 != nil }.map { $0! }
.flatMap { api.rx_fetchItems(for: user, category: $0)
.materialize()
}
.filter { !$0.isCompleted }
.shareReplayLatestWhileConnected()
collectionItems = serverItems.filter { $0.element != nil }.dematerialize()
error = serverItems.filter { $0.error != nil }.map { $0.error! }
}
}
In order to call api requests in a more expressive, formalized way, I am able to call api.rx_fetchItems(for:) inside flatmap above, because I extend FirebaseAPI to conform to FetchItemsAPI. I will probably have to follow the same pattern for most other requests.
extension FirebaseAPI: FetchItemsAPI {
// MARK: Fetch Items Protocol
func rx_fetchItems(for user: User, category: ItemCategory) -> Observable<[Item]> {
// fetched items returns all items in database as Observable<[Item]>
let fetchedItems = rx_fireRequest(.fetchItems, ofType: Item.self)
switch category {
case .Local:
let localItems = fetchedItems
.flatMapLatest { (itemList) -> Observable<[Item]> in
return self.rx_localItems(user: user, items: itemList)
}
return localItems
case .RecentlyAdded:
// Compare current date to creation date of item. If its within 24 hours, It makes the cut.
let recentlyAddedItems = fetchedItems
.flatMapLatest { (itemList) -> Observable<[Item]> in
return self.rx_recentlyAddedItems(items: itemList)
}
return recentlyAddedItems
case .Trending:
let trendingItems = fetchedItems
.flatMapLatest { (itemList) -> Observable<[Item]> in
return self.rx_trendingItems(items: itemList)
}
return trendingItems
default:
let stubItem = Item(id: "DEFAULT", createdById: "createdBy", creationDate: 1.3, expirationDate: 2.4, title: "title", price: 2, info: "info", imageUrl: "url", bidCount: 4, location: "LA")
return Observable.just([stubItem])
}
}
// MARK: Helper Methods
private func rx_localItems(user: User, items: [Item]) -> Observable<[Item]> {
return Observable<[Item]>.create { observer in
observer.onNext(items.filter { $0.location == user.location }) // LA Matches stubs in db
return Disposables.create()
}
}
func rx_recentlyAddedItems(items: [Item]) -> Observable<[Item]> {
return Observable<[Item]>.create { observer in
let recentItems = items
.filter {
let now = Date(timeIntervalSinceReferenceDate: 0)
let creationDate = Date(timeIntervalSince1970: $0.creationDate)
if let hoursAgo = now.offset(from: creationDate, units: [.hour], maxUnits: 1) {
return Int(hoursAgo)! < 24
} else {
return false
}
}
observer.onNext(recentItems)
return Disposables.create()
}
}
func rx_trendingItems(items: [Item]) -> Observable<[Item]> {
return Observable<[Item]>.create { observer in
observer.onNext(items.filter { $0.bidCount > 8 })
return Disposables.create()
}
}
}
I'm attempting to follow SOLID principles, and level up with RxSWift + MVVM, so I'm still unsure about the best OOP design for clean, maintainable code.
I have a method that loads an array of dictionaries from a propertylist. Then I change those arrays of dictionaries to array of a defined custom type;
I want to write that method in generic form so I call that method with the type I expect, then the method loads it and returns an array of my custom type rather than dictionaries
func loadPropertyList(fileName: String) -> [[String:AnyObject]]?
{
if let path = NSBundle.mainBundle().pathForResource(fileName, ofType: "plist")
{
if let plistXML = NSFileManager.defaultManager().contentsAtPath(path)
{
do {
if let temp = try NSPropertyListSerialization.propertyListWithData(plistXML, options: .Immutable, format: nil) as? [[String:AnyObject]]
{
return temp
}
}catch{}
}
}
return nil
}
//
func loadList<T>(fileName: String) -> [T]?{//**Here the answer I am expecting**}
I am assuming your function to read from a Plist works and that you don't want to subclass NSObject.
Since Swift reflecting does not support setting values this is not possible without some implementation for each Type you want this to work for.
It can however be done in a pretty elegant way.
struct PlistUtils { // encapsulate everything
static func loadPropertyList(fileName: String) -> [[String:AnyObject]]? {
if let path = NSBundle.mainBundle().pathForResource(fileName, ofType: "plist") {
if let plistXML = NSFileManager.defaultManager().contentsAtPath(path) {
do {
if let temp = try NSPropertyListSerialization.propertyListWithData(plistXML, options: .Immutable, format: nil) as? [[String:AnyObject]] {
return temp
}
} catch {
return nil
}
}
}
return nil
}
}
This protocol will be used in a generic fashion to get the Type name and read the corresponding Plist.
protocol PListConstructible {
static func read() -> [Self]
}
This protocol will be used to implement Key Value setters.
protocol KeyValueSettable {
static func set(fromKeyValueStore values:[String:AnyObject]) -> Self
}
This is the combination of both to generate an array of objects. This does require that the Plist is named after the Type.
extension PListConstructible where Self : KeyValueSettable {
static func read() -> [Self] {
let name = String(reflecting: self)
var instances : [Self] = []
if let data = PlistUtils.loadPropertyList(name) {
for entry in data {
instances.append(Self.set(fromKeyValueStore: entry))
}
}
return instances
}
}
This is some Type.
struct Some : PListConstructible {
var alpha : Int = 0
var beta : String = ""
}
All you have to do is implement the Key Value setter and it will now be able to be read from a Plist.
extension Some : KeyValueSettable {
static func set(fromKeyValueStore values: [String : AnyObject]) -> Some {
var some = Some()
some.alpha = (values["alpha"] as? Int) ?? some.alpha
some.beta = (values["beta"] as? String) ?? some.beta
return some
}
}
This is how you use it.
Some.read()
I'm working on getting more comfortable with generics and I have run into this problem multiple times. I get a compiler error where it tells me it can't convert 'type' to expected argument type '_'. I'm having trouble understanding this error. I thought that specifying a generic param allows you to pass in any type? Or is that not what I'm doing?
infix operator +++ { associativity left }
func +++<A, B>(a:A?, f:A -> B?) {
if let x = a {
f(x)
}
}
func stringToImage(string:String, completion:(Result<UIImage>) -> ()) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
if let image:UIImage = urlFormat(string) +++ dataFormat +++ imageFormat { --- Cannot convert value of type NSURL? to expected argument of type _?
}
})
}
func urlFormat(s:String) -> NSURL? {
if let url = NSURL(string: s) {
return url
}
return nil
}
func dataFormat(url:NSURL?) -> NSData? {
if let u = url {
if let d = NSData(contentsOfURL: u) {
return d
}
}
return nil
}
func imageFormat(d:NSData?) -> UIImage? {
if let data = d {
if let image = UIImage(data: data) {
return image
}
}
return nil
}
It's because your +++ func doesn't return anything.
Since there is nothing return from urlFormat(string) +++ dataFormat, calling +++ imageFormat doesn't work since there is nothing on the left side.
You just need to change +++ so it has a return value like this.
func +++<A, B>(a:A?, f:A -> B?) -> B? {
if let x = a {
return f(x)
}
return nil
}
This method was working while I was using Swift 1.2. But now, I had to update to Xcode and I had to switch my language to Swift 2. This is the method from swift 1.2 which I used well ;
static func findById(idToFind : Int64) -> T? {
let query = table.filter(id == idToFind)
var results: Payment?
if let item = query.first {
results : T = Payment(id: item[id], imageName: item[image], type: item[type], deliveredPriceStr: item[deliveredPrice], orderID: item[orderId])
}
return results
}
Now I modified it for Swift 2 but couldn't manage;
static func findById(idToFind : Int64) -> T? {
let query = table.filter(id == idToFind)
do {
let query = try table.filter(id == idToFind)
let item = try SQLiteDataStore.sharedInstance.SADB.pluck(query)
if try item != nil {
let results : T = Payment(id: item[id], imageName: item[image], type: item[type], deliveredPriceStr: item[deliveredPrice], orderID: item[orderId])
} else {
print("item not found")
}
} catch {
print("delete failed: \(error)")
}
}
return results
}
And I'm getting this error : "Cannot subscript a value of type Row" . My item's data type seems like changed to Row. How can I parse it ? What should I do ?
PS: I'm using swift2 branch.
Finally I figured out how to get values. It's easy now the row item has get method on swift-2 branch. So new method is ;
static func findById(idToFind : Int64) -> T? {
let query = table.filter(id == idToFind)
var results : T?
do {
let query = table.filter(id == idToFind)
let item = SQLiteDataStore.sharedInstance.SADB.pluck(query)
if try item != nil {
results = Payment( id: (item?.get(id))!, imageName: (item?.get(image))!, type: (item?.get(type))!, deliveredPriceStr: (item?.get(deliveredPrice))!, orderID: (item?.get(orderId))!)
} else {
print("item not found")
}
}
catch {
print("delete failed: \(error)")
}
return results
}
Hope that this helps someone.