Create PDF of UITableView to AirPrint Xcode and Swift - swift

Looking for a solution to create a pdf programatically with the contents of my UITabelView with a label on top and allow the users to print on standard A4 paper.
This is my not working code:
func createPdfFromView(aView: UIView, saveToDocumentsWithFileName fileName: String) {
let pdfTitle = "\(pilotName)'s Past Duty"
let pageSize = CGRect(x: 0, y: 0, width: 11 * 70, height: 8.5 * 70)
let pdfData = NSMutableData()
UIGraphicsBeginPDFContextToData(pdfData, pageSize, nil)
UIGraphicsBeginPDFPage()
let font = UIFont.preferredFont(forTextStyle: .largeTitle)
let attributedPDFTitle = NSAttributedString(string: pdfTitle, attributes: [NSAttributedString.Key.font: font])
let stringSize = attributedPDFTitle.size()
let stringRect = CGRect(x: (pageSize.width / 2 - stringSize.width / 2), y: 20, width: stringSize.width, height: stringSize.height)
attributedPDFTitle.draw(in: stringRect)
aView.layer.render(in: UIGraphicsGetCurrentContext()!)
UIGraphicsGetCurrentContext()!.translateBy(x: 0, y: 40)
UIGraphicsEndPDFContext()
if let documentDirectories = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first {
let documentsFileName = documentDirectories + "/" + fileName
debugPrint(documentsFileName)
pdfData.write(toFile: documentsFileName, atomically: true)
}
}

Related

How can I properly pass an array in iOS PDFKit

I'm trying to in an [string] array for a PDF. Below is what I have so far. I am guess I need to do a foreach somewhere, but I'm not entirely sure.
I thought something like this might work, but it does not.
for entry in body {
let attributedText = NSAttributedString(
string: entry,
attributes: textAttributes
)
}
private func addBody(body: [String], pageRect: CGRect, textTop: CGFloat) {
let pageWidth = 8.5 * 72.0
let pageHeight = 11 * 72.0
let pageRect = CGRect(x: 0, y: 0, width: pageWidth, height: pageHeight)
let bodyCG = addInstructor(instructor: "", pageRect: pageRect)
let textFont = UIFont.systemFont(ofSize: 12.0, weight: .regular)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .natural
paragraphStyle.lineBreakMode = .byWordWrapping
let textAttributes = [
NSAttributedString.Key.paragraphStyle: paragraphStyle,
NSAttributedString.Key.font: textFont
]
let attributedText = NSAttributedString(
string: body,
attributes: textAttributes
)
let textRect = CGRect(
x: 15,
y: bodyCG + 30,
width: pageRect.width - 20,
height: pageRect.height - textTop - pageRect.height / 5.0
)
attributedText.draw(in: textRect)
}
Adding some additional details. If I don't use [String] and just use String everything works fine. The PDF is generated, The concept I'm struggling to understand is how can I pass an array for the PDF.
var courseAttendees : [String] = ["name", "name", "name", "name"]
For Example, I want to pass courseAttendees and then loop through the array the names are just overlapped and shown below.
Final code.
private func addBody(body: [String], textTop: CGFloat) {
let pageWidth = 8.5 * 72.0
let pageHeight = 11 * 72.0
let pageRect = CGRect(x: 0, y: 0, width: pageWidth, height: pageHeight)
let bodyCG = addInstructor(instructor: "", pageRect: pageRect)
let textFont = UIFont.systemFont(ofSize: 12.0, weight: .regular)
let textAttributes: [NSAttributedString.Key: Any] =
[NSAttributedString.Key.font: textFont]
// keep track of the y position on the page. You might need
// to set this globally as you have multiple pages
var currentYPos: CGFloat = bodyCG
// Loop through the array
for entry in body {
let attributedText = NSAttributedString(
string: "\(entry)",
attributes: textAttributes
)
// Update the currentYPos
currentYPos += 15
// Use the currentYPos in the textRect
let textRect = CGRect(
x: 15,
y: currentYPos,
width: pageRect.width - 20,
height: pageRect.height - textTop - pageRect.height / 5.0
)
attributedText.draw(in: textRect)
}
}
Based on your question and image, I am assuming the PDF creation works fine but the data is not rendered as desired.
I think the two questions to answer here are:
Where to loop through your array
How to keep track of the current y coordinate in the page which is responsible for the vertical positioning
Here are some additions I made to try and fix your issue, I have added comments to what I have changed
// Somewhere appropriate in your code
var courseAttendees : [String] = ["name1", "name2", "name3", "name4"]
// Call the function, 15 is just a random number for textTop,
// give it what you feel is appropriate
addBody(body: courseAttendees, textTop: 15)
// I have removed the pageRect parameter since you create it
// in the function
private func addBody(body: [String], textTop: CGFloat) {
let pageWidth = 8.5 * 72.0
let pageHeight = 11 * 72.0
let pageRect = CGRect(x: 0, y: 0, width: pageWidth, height: pageHeight)
let bodyCG = addInstructor(instructor: "", pageRect: pageRect)
let textFont = UIFont.systemFont(ofSize: 12.0, weight: .regular)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .natural
paragraphStyle.lineBreakMode = .byWordWrapping
let textAttributes = [
NSAttributedString.Key.paragraphStyle: paragraphStyle,
NSAttributedString.Key.font: textFont
]
// keep track of the y position on the page. You might need
// to set this globally as you have multiple pages
var currentYPos: CGFloat = 0.0
// Loop through the array
for entry in body {
let attributedText = NSAttributedString(
string: entry,
attributes: textAttributes
)
// Update the currentYPos
currentYPos += bodyCG + 30
// Use the currentYPos in the textRect
let textRect = CGRect(
x: 15,
y: currentYPos,
width: pageRect.width - 20,
height: pageRect.height - textTop - pageRect.height / 5.0
)
attributedText.draw(in: textRect)
}
}
I have not tested the above so please give this a try and check if it solves your issue.
If not, comment and I will update this accordingly.

Converting TableView to PDF - Issue: final PDF image overlaps when creating new pages

I am converting my TableView to a PDF like so:
func pdfDataWithTableView(tableView: UITableView) -> Data? {
let priorBounds = tableView.bounds
let fittedSize = tableView.sizeThatFits(CGSize(
width: priorBounds.size.width,
height: tableView.contentSize.height
))
tableView.bounds = CGRect(
x: 0, y: 0,
width: fittedSize.width,
height: fittedSize.height
)
let pdfPageBounds = CGRect(
x :0, y: 0,
width: tableView.frame.width,
height: self.view.frame.height
)
let pdfData = NSMutableData()
UIGraphicsBeginPDFContextToData(pdfData, pdfPageBounds, nil)
var pageOriginY: CGFloat = 0
while pageOriginY < fittedSize.height {
UIGraphicsBeginPDFPageWithInfo(pdfPageBounds, nil)
UIGraphicsGetCurrentContext()!.saveGState()
UIGraphicsGetCurrentContext()!.translateBy(x: 0, y: -pageOriginY)
tableView.layer.render(in: UIGraphicsGetCurrentContext()!)
UIGraphicsGetCurrentContext()!.restoreGState()
pageOriginY += pdfPageBounds.size.height
tableView.contentOffset = CGPoint(x: 0, y: pageOriginY) // move "renderer"
}
UIGraphicsEndPDFContext()
tableView.bounds = priorBounds
var docURL = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)).last! as URL
docURL = docURL.appendingPathComponent("myDocument.pdf")
pdfData.write(to: docURL as URL, atomically: true)
return pdfData as Data
}
The code is working great; I can convert multiple different sized cells to a pdf displayed on multiple pages.
Now the issue is because it is just an image the PDF displays cut off when new page is shown... Example below:
I am looking for a solution to avoid this... Any suggestions would be greatly appreciated!

Swift macOS how to print with NSPrintInfo and NSAttributedString

I want to print out a list in my App.
However if the list is too large to fit on one page there is a problem with
the last and the first line on a page.
e.g. the last line on a page is cropped and some parts are shown on top of next page.
Screenshot of bottom of page 1
Screenshot of top of page 2
How can I manage to set up my print job that the text is set up correctly on every page?
Currently I have the following code snippet:
func makePDF(markup: String, amount: Int) {
let directoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let printOpts: [NSPrintInfo.AttributeKey: Any] = [NSPrintInfo.AttributeKey.jobDisposition: NSPrintInfo.JobDisposition.save, NSPrintInfo.AttributeKey.jobSavingURL: directoryURL]
let printInfo = NSPrintInfo(dictionary: printOpts)
printInfo.horizontalPagination = NSPrintInfo.PaginationMode.autoPagination
printInfo.verticalPagination = NSPrintInfo.PaginationMode.autoPagination
printInfo.topMargin = 20.0
printInfo.leftMargin = 20.0
printInfo.rightMargin = 20.0
printInfo.bottomMargin = 20.0
let view = NSView(frame: NSRect(x: 0, y: 0, width: 560, height: (60 + 36 * amount)))
if let htmlData = markup.data(using: String.Encoding.utf8) {
let attrStr = NSAttributedString(html: htmlData, options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding:
String.Encoding.utf8.rawValue], documentAttributes: nil)
let frameRect = NSRect(x: 0, y: 0, width: 560, height: (60 + 36 * amount))
let textField = NSTextField(frame: frameRect)
textField.attributedStringValue = attrStr!
view.addSubview(textField)
let printOperation = NSPrintOperation(view: view, printInfo: printInfo)
printOperation.showsPrintPanel = true
printOperation.showsProgressPanel = true
printOperation.run()
}
}

Setup NSView for printing under macOs

I've managed to setup a function that allows me to print out a list from my macOs app.
I've set up a function that takes a String containing HTML
func makePDF(markup: String, amount: Int) {
let directoryURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let printOpts: [NSPrintInfo.AttributeKey: Any] = [NSPrintInfo.AttributeKey.jobDisposition: NSPrintInfo.JobDisposition.save, NSPrintInfo.AttributeKey.jobSavingURL: directoryURL]
let printInfo = NSPrintInfo(dictionary: printOpts)
printInfo.horizontalPagination = NSPrintInfo.PaginationMode.autoPagination
printInfo.verticalPagination = NSPrintInfo.PaginationMode.autoPagination
printInfo.topMargin = 20.0
printInfo.leftMargin = 20.0
printInfo.rightMargin = 20.0
printInfo.bottomMargin = 20.0
let view = NSView(frame: NSRect(x: 0, y: 0, width: 560, height: (60 + 36 * amount)))
if let htmlData = markup.data(using: String.Encoding.utf8) {
let attrStr = NSAttributedString(html: htmlData, options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding:
String.Encoding.utf8.rawValue], documentAttributes: nil)
let frameRect = NSRect(x: 0, y: 0, width: 560, height: (60 + 36 * amount))
let textField = NSTextField(frame: frameRect)
textField.attributedStringValue = attrStr!
view.addSubview(textField)
let printOperation = NSPrintOperation(view: view, printInfo: printInfo)
printOperation.showsPrintPanel = true
printOperation.showsProgressPanel = true
printOperation.run()
}
}
However I have trouble with the correct layout and set up of a Page. The other thing is that I have not managed to have a HTML with a border ( The border attribute is ignored)
Are there some best practices to set up a printed page (US-Letter or DIN A4) that fits without having overflowing border to a next page?
How can I manage to have a border in my HTML Table to be printed out?
Thanks

NSImage Getting Resized when I draw Text on it

I have the following code to draw a text over an NSImage.
But the resulting image is getting resized to smaller one when I save it to disk.
What i'm i doing wrong? Please advice
func drawText(image :NSImage) ->NSImage
{
let text = "Sample Text"
let font = NSFont.boldSystemFont(ofSize: 18)
let imageRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
let textRect = CGRect(x: 5, y: 5, width: image.size.width - 5, height: image.size.height - 5)
let textStyle = NSMutableParagraphStyle.default().mutableCopy() as! NSMutableParagraphStyle
let textFontAttributes = [
NSFontAttributeName: font,
NSForegroundColorAttributeName: NSColor.white,
NSParagraphStyleAttributeName: textStyle
]
let im:NSImage = NSImage(size: image.size)
let rep:NSBitmapImageRep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(image.size.width), pixelsHigh: Int(image.size.height), bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: NSCalibratedRGBColorSpace, bytesPerRow: 0, bitsPerPixel: 0)!
im.addRepresentation(rep)
im.lockFocus()
image.draw(in: imageRect)
text.draw(in: textRect, withAttributes: textFontAttributes)
im.unlockFocus()
return im
}
This is a different approach using a temporary NSView to draw the image and the text and cache the result in a new image (code is Swift 4). The benefit it you don't need to deal with pixels
class ImageView : NSView {
var image : NSImage
var text : String
init(image: NSImage, text: String)
{
self.image = image
self.text = text
super.init(frame: NSRect(origin: NSZeroPoint, size: image.size))
}
required init?(coder decoder: NSCoder) { fatalError() }
override func draw(_ dirtyRect: NSRect) {
let font = NSFont.boldSystemFont(ofSize: 18)
let textRect = CGRect(x: 5, y: 5, width: image.size.width - 5, height: image.size.height - 5)
image.draw(in: dirtyRect)
text.draw(in: textRect, withAttributes: [.font: font, .foregroundColor: NSColor.white])
}
var outputImage : NSImage {
let imageRep = bitmapImageRepForCachingDisplay(in: frame)!
cacheDisplay(in: frame, to:imageRep)
let tiffData = imageRep.tiffRepresentation!
return NSImage(data : tiffData)!
}
}
To use it, initialize a view
let image = ... // get some image
let view = ImageView(image: image, text: "Sample Text")
and get the new image
let imageWithText = view.outputImage
Note:
The paragraph style is not used at all, but if you want to create a mutable paragraph style just write
let textStyle = NSMutableParagraphStyle()
Mixed pixel vs point?
Depending on your screen 2x or 3x image is smaller 2 times or 3 times?
Here is more detailed info (scroll down to "Converting between pixels and points")
http://blog.fluidui.com/designing-for-mobile-101-pixels-points-and-resolutions/
But keep in mind that:
NSImage is resolution aware and uses a HiDPI graphics context when you lockFocus on a system with retina screen.
The image dimensions you pass to your NSBitmapImageRep initializer are in points (not pixels). An 150.0 point-wide image therefore uses 300 horizontal pixels in a #2x context.
Source:
How to save PNG file from NSImage (retina issues)
Following simple app works for me. Enjoy ;)
import Cocoa
class ViewController: NSViewController {
func save(image:NSImage, imageURL:String, format:String) -> Bool
{
let bMImg = NSBitmapImageRep(data: (image.tiffRepresentation)!)
switch format {
case ".png":
let filepath = URL(fileURLWithPath: imageURL+".png")
let dataToSave = bMImg?.representation(using: NSBitmapImageRep.FileType.png, properties: [NSBitmapImageRep.PropertyKey.compressionFactor : 1])
do
{
try dataToSave?.write(to: filepath)
return true
} catch {
return false
}
default:
return false
}
}
func draw(text:String, image:NSImage) -> NSImage
{
let font = NSFont.boldSystemFont(ofSize: 18)
let imageRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
let textRect = CGRect(x: 5, y: 5, width: image.size.width - 5, height: image.size.height - 5)
let textStyle = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
let textFontAttributes = [
NSAttributedStringKey.font: font,
NSAttributedStringKey.foregroundColor: NSColor.white,
NSAttributedStringKey.paragraphStyle: textStyle
]
let im:NSImage = NSImage(size: image.size)
let rep:NSBitmapImageRep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(image.size.width), pixelsHigh: Int(image.size.height), bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: NSColorSpaceName.calibratedRGB, bytesPerRow: 0, bitsPerPixel: 0)!
im.addRepresentation(rep)
im.lockFocus()
image.draw(in: imageRect)
text.draw(in: textRect, withAttributes: textFontAttributes)
im.unlockFocus()
return im
}
#IBAction func action(_ sender: NSButton) {
let dialog = NSOpenPanel();
dialog.title = "Choose a image...";
dialog.showsResizeIndicator = true;
dialog.showsHiddenFiles = false;
dialog.canChooseDirectories = true;
dialog.canCreateDirectories = true;
dialog.allowsMultipleSelection = false;
dialog.allowedFileTypes = ["png", "jpg"];
if (dialog.runModal() == NSApplication.ModalResponse.OK) {
guard let url = dialog.url,
let imageCIImage = CIImage(contentsOf: url) else {
return
}
let rep: NSCIImageRep = NSCIImageRep(ciImage: imageCIImage)
let nsImage = NSImage(size: rep.size)
nsImage.addRepresentation(rep)
let imageWithText = draw(text:"ABC", image: nsImage)
if (save(image: imageWithText, imageURL: "imageWithText", format: ".png")) {
print("Success")
} else {
print("ERROR:Failed to save image")
}
} else {
// User clicked on "Cancel"
return
}
}
}