I have several large PDF docs (70-200, pages each). The PDFs themselves are generated from HTML pages (I can't get the source code of the HTML pages which is why I am working with the PDFs). Anyway, what I want to do is parse the PDF into separate pages based on the converted H1 tag attribute. When I print out the PDF I get this:
Seller Tag (AST)
{
NSBaselineOffset = 0;
NSColor = "Device RGB colorspace 0.94118 0.32549 0.29804 1";
NSFont = "\"Helvetica 8.00 pt. P [] (0x7ff0f262e590) fobj=0x7ff0f4339680, spc=2.22\"";
}Table of Contents
{
NSBaselineOffset = 0;
NSColor = "Device RGB colorspace 0.94118 0.32549 0.29804 1";
NSFont = "\"Helvetica 34.00 pt. P [] (0x7ff0f262e590) fobj=0x7ff0f432f940, spc=9.45\"";
}...
which looks like a bunch of attributes contained in a Dictionary. But when I run this code:
let strContent = myAppManager.pdfToText(fromPDF:pdfDirPath.absoluteString "/" thisFile)
let strPDF:NSAttributedString = strContent
let strNSPDF = strPDF.string as NSString
let rangeOfString = NSMakeRange(0, strNSPDF.length)
let arrAttributes = strPDF.attributes(at: 0, longestEffectiveRange: nil, in: rangeOfString)
print(arrAttributes)
I get this output
[__C.NSAttributedStringKey(_rawValue: NSColor): Device RGB colorspace 0.94118 0.32549 0.29804 1, __C.NSAttributedStringKey(_rawValue: NSBaselineOffset): 0, __C.NSAttributedStringKey(_rawValue: NSFont): "Helvetica 8.00 pt. P [] (0x7ff0f441d490) fobj=0x7ff0f4339680, spc=2.22"]
I was kind of expecting a high number, like 1000 or more entries, not 1.
So snooping around, I know the H1 HTML tag gets converted to this:
Table of Contents
{
NSBaselineOffset = 0;
NSColor = "Device RGB colorspace 0.94118 0.32549 0.29804 1";
NSFont = "\"Helvetica 34.00 pt. P [] (0x7ff0f262e590) fobj=0x7ff0f432f940, spc=9.45\"";
}
So what I am looking to do is delimit the converted H1s so I can get the content between as a page and do stuff with it. Any ideas or suggestions would be appreciated.
CodePudding user response:
Quickly done, assuming you have:
someText[HEADER1]someText1[HEADER2]someText2[HEADER3]someText3...
Where [HEADERN]
have the same attributes (and you know them) but not the same as someTextN
.
We want in the end, and array of:
struct Page: CustomStringConvertible {
let title: NSAttributedString? //Tha's be the h1 tag content
let content: NSAttributedString?
var description: String {
return "Title: \(title?.string ?? "") - content: \(content?.string ?? "")"
}
}
Initial sample:
let htmlString = "<b>Title 1</b> Text for part one.\n <b>Title 2</b> Text for part two<b>Title 3</b>Text for part three"
let attributedString = try! NSAttributedString(data: Data(htmlString.utf8),
options: [.documentType : NSAttributedString.DocumentType.html],
documentAttributes: nil)
With:
let headerAttributes: [NSAttributedString.Key: Any] = [.font: UIFont.boldSystemFont(ofSize: 12)]
print("headerAttributes: \(headerAttributes)")
func headerOneAttributes(_ headerAttributes: [NSAttributedString.Key: Any], matches attributes: [NSAttributedString.Key: Any]?) -> Bool {
guard let attributes = attributes else { return false }
guard let attributesFont = attributes[.font] as? NSFont, let headerFont = headerAttributes[.font] as? NSFont else {
return false
}
return attributesFont.fontDescriptor.symbolicTraits == NSFontDescriptor.SymbolicTraits(rawValue: 268435458) //Here fonts arent' equal equal, some work here plus checking on other attributes too and font size?
// Do you own check
// return false
}
We can iterates the attributes to get all the headers ranges:
var headerRanges: [NSRange] = []
attributedString.enumerateAttributes(in: NSRange(location: 0, length: attributedString.length), options: []) { attributes, range, stop in
if headerOneAttributes(headerAttributes, matches: attributes) {
headerRanges.append(range)
}
}
With an iteration on the ranges:
var pages: [Page] = []
guard !headerRanges.isEmpty else { return }
//In case the first title doesn't "start at the beginning", we have a "content" with no title at start
if let first = headerRanges.first, first.location > 0 {
pages.append(Page(title: nil, content: attributedString.attributedSubstring(from: first)))
}
// Then we iterate
for (anIndex, aRange) in headerRanges.enumerated() {
print(pages)
let title = attributedString.attributedSubstring(from: aRange)
let subtext: NSAttributedString?
// If there is a "nextRange", then we get the end of subtext from it
if anIndex 1 <= headerRanges.count - 1 {
let next = headerRanges[anIndex 1]
let location = aRange.location aRange.length
let length = next.location - location
subtext = attributedString.attributedSubstring(from: NSRange(location: location, length: length))
} else {
//There is no next => Until the end
let location = aRange.location aRange.length
let length = attributedString.length - location
subtext = attributedString.attributedSubstring(from: NSRange(location: location, length: length))
}
pages.append(Page(title:title, content: subtext))
}
print(pages)
PS: UIFont/NSFont: ~the same, I tested on a macOS app, not iOS, that's why.
CodePudding user response:
Okay, so @Larme put me on the right track for what I was looking for. Posting the code in hopes it helps someone else. I've tested this on a 77 page document and it worked. I should have noted in the question that I am working on MacOS.
func parsePDF(_ strPDFContent:NSMutableAttributedString) -> Array<Dictionary<String, Any>> {
//some initial setup
let strNSPDF = strPDFContent.string as NSString
var arrDocSet:Array<Dictionary<String, Any>> = []
//get all the page headers
var arrRanges = [NSRange]()
strPDFContent.enumerateAttribute(NSAttributedString.Key.font, in: NSRange(0..<strPDFContent.length), options: .longestEffectiveRangeNotRequired) {
value, range, stop in
if let thisFont = value as? NSFont {
if thisFont.pointSize == 34 {
arrRanges.append(range)
}
}
}
//get the content and store data
for (idx, range) in arrRanges.enumerated() {
//get title
let strTitle = String(strNSPDF.substring(with: range))
var textRange = NSRange(location:0, length:0)
//skip opening junk
if !strTitle.contains("Table of Contents\n") {
if idx < arrRanges.count-1 {
textRange = NSRange(location: range.upperBound, length: arrRanges[idx 1].lowerBound - range.upperBound)
} else if idx == arrRanges.count-1 {
textRange = NSRange(location: range.upperBound, length: strNSPDF.length - range.upperBound)
}
let strContent = String(strNSPDF.substring(with: textRange))
arrDocSet.append(["title":strTitle, "content":strContent, "contentRange":textRange, "titleRange":range])
}
}
print(arrDocSet)
return arrDocSet
}
This will output:
["titleRange": {10001, 27}, "title": "Set up Placements with AST\n", "content": "This page contains a sample web page showing how Xandr\'s seller tag (AST) functions can be implemented in the header and body of a sample client page.\nSee AST API Reference for more details on using ...
...
ready.\nExample\n$sf.ext.status();\n", "title": " SafeFrame API Reference\n", "contentRange": {16930, 9841}
Let me know if there's places I could be more efficient.