Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ Rules:
`Unreleased`.
- Add the date and the compare link once the version tag exists.

## [Unreleased]

### Fixed

- A PDF prints as the file itself, one page to a sheet. A PDF with a password
or with unsaved marks prints from the screen as before.
- Printing starts on the paper and orientation of the document, for example A4
or a landscape slide, where the printer has that paper.

## [1.48]

### Added
Expand Down
31 changes: 30 additions & 1 deletion OpenDocumentReader/CoreWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [

/// Whether the file is plain text, which takes typing but no formatting.
@objc var isPlainText: Bool { lock.withLock { textFile != nil } }
/// The page size the document is laid out for, in points; zero where it has none.
@objc private(set) var pageSize: CGSize = .zero
/// Whether the file is a pdf the print system can read as it is.
@objc private(set) var isPrintablePdf = false

private var document: OdrCoreObjC.Document?
private var textFile: TextFile?
Expand Down Expand Up @@ -120,14 +124,17 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [
textFile = nil
pdfFile = nil
isArchive = false
pageSize = .zero
isPrintablePdf = false

let fileTypes = (try? DecodedFile.listFileTypes(path: inputPath)) ?? []
guard !fileTypes.isEmpty else {
throw coreWrapperError(.unsupportedFileType, "odrcore does not recognise this file type")
}

var file = try DecodedFile.decode(path: inputPath)
if file.isPasswordEncrypted {
let encrypted = file.isPasswordEncrypted
if encrypted {
do {
file = try file.decrypt(withPassword: password ?? "")
} catch let error as NSError
Expand Down Expand Up @@ -184,6 +191,7 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [
let openedDocument: OdrCoreObjC.Document?
var openedTextFile: TextFile?
var openedPdfFile: PdfFile?
var openedPageSize: CGSize?
let service: HtmlService

if file.isDocumentFile {
Expand All @@ -194,6 +202,7 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [
// the document's own answer: a format odrcore renders but cannot write
// back would otherwise offer Edit and fail at the save
openedDocument = document.isEditable && document.isSavable ? document : nil
openedPageSize = Self.pageSize(of: document)
service = try HtmlTranslator.translate(document: document, config: config)
} else {
// `.unknown` keeps the single view each of these has -
Expand Down Expand Up @@ -227,12 +236,32 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [
self.document = openedDocument
self.textFile = openedTextFile
self.pdfFile = openedPdfFile
pageSize = openedPageSize ?? .zero
isPrintablePdf = file.isPdfFile && !encrypted

isArchive = file.isArchiveFile
pageNames = views.map(\.name)
pageURLs = views.map { base.appendingPathComponent($0.path) }
}

/// The first page's: a presentation has one size, and a drawing seldom more.
private static func pageSize(of document: OdrCoreObjC.Document) -> CGSize? {
guard let root = try? document.rootElement() else {
return nil
}
let layout: PageLayout?
switch document.documentType {
case .text: layout = (root as? TextRoot)?.pageLayout
case .presentation: layout = (root.firstChild as? Slide)?.pageLayout
case .drawing: layout = (root.firstChild as? OdrCoreObjC.Page)?.pageLayout
default: layout = nil
}
guard let layout else {
return nil
}
return PrintPaper.size(width: layout.width, height: layout.height)
}

/// The script the page hands its edits back through: the editor's log for
/// a document or a text file, the marks for a pdf.
@objc var editPayloadScript: String {
Expand Down
8 changes: 8 additions & 0 deletions OpenDocumentReader/Document.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ class Document: UIDocument {
public var isAnnotatable = false
/// Whether this is plain text - see `CoreWrapper.isPlainText`.
public var isPlainText = false
/// See `CoreWrapper.pageSize`; nil where it has none.
public var pageSize: CGSize?
/// See `CoreWrapper.isPrintablePdf`.
public var isPrintablePdf = false

override func load(fromContents contents: Any, ofType typeName: String?) throws {
parse()
Expand All @@ -103,6 +107,8 @@ class Document: UIDocument {
isEditable = false
isAnnotatable = false
isPlainText = false
pageSize = nil
isPrintablePdf = false
result = nil
pageURLs = nil
pageNames = nil
Expand Down Expand Up @@ -134,6 +140,8 @@ class Document: UIDocument {
isEditable = coreWrapper.isEditable
isAnnotatable = coreWrapper.isAnnotatable
isPlainText = coreWrapper.isPlainText
pageSize = coreWrapper.pageSize == .zero ? nil : coreWrapper.pageSize
isPrintablePdf = coreWrapper.isPrintablePdf

loadProgress.completedUnitCount = loadProgress.totalUnitCount

Expand Down
23 changes: 22 additions & 1 deletion OpenDocumentReader/DocumentViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1308,12 +1308,33 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel
printInfo.outputType = UIPrintInfo.OutputType.general
printInfo.jobName = "OpenDocument Reader - Document"

// the controller is shared, so each job states all of them
printController.printingItem = nil
printController.printFormatter = nil

let pageSize: CGSize?
// the file has no unsaved marks, and prints at its own page size
if let document, document.isPrintablePdf, !hasUnsavedEdits {
printController.printingItem = document.fileURL
pageSize = PrintPaper.firstPageSize(ofPDFAt: document.fileURL)
} else {
printController.printFormatter = webview.viewPrintFormatter()
pageSize = document?.pageSize
}

paperChooser = pageSize.map { PaperChooser(pageSize: $0) }
if let paperChooser {
printInfo.orientation = paperChooser.orientation
}
printController.delegate = paperChooser
printController.printInfo = printInfo
printController.printFormatter = webview.viewPrintFormatter()

printController.present(animated: true, completionHandler: nil)
}

/// Held here: the print controller holds its delegate weakly.
private var paperChooser: PaperChooser?

/// Loading and error pages follow system appearance.
private func loadMessage(_ body: String) {
webview.loadHTMLString(
Expand Down
66 changes: 66 additions & 0 deletions OpenDocumentReader/PrintPaper.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import CoreGraphics
import OdrCoreObjC
import UIKit

/// The sheet a document prints on, in points, the unit UIKit prints in.
enum PrintPaper {
/// Nil where either side is missing or not a length.
static func size(width: Measure?, height: Measure?) -> CGSize? {
guard let width, let height,
let widthPoints = points(width.magnitude, unit: width.unit),
let heightPoints = points(height.magnitude, unit: height.unit)
else {
return nil
}
return CGSize(width: widthPoints, height: heightPoints)
}

static func points(_ magnitude: Double, unit: String) -> CGFloat? {
let perUnit: Double
switch unit {
case "pt": perUnit = 1
case "in": perUnit = 72
case "cm": perUnit = 72 / 2.54
case "mm": perUnit = 72 / 25.4
case "pc": perUnit = 12
case "px": perUnit = 0.75
default: return nil
}
let result = magnitude * perUnit
return result > 0 ? CGFloat(result) : nil
}

/// The first page as a viewer shows it: the crop box, turned by `/Rotate`.
static func firstPageSize(ofPDFAt url: URL) -> CGSize? {
guard let page = CGPDFDocument(url as CFURL)?.page(at: 1) else {
return nil
}
let box = page.getBoxRect(.cropBox)
return page.rotationAngle % 180 == 0
? box.size : CGSize(width: box.height, height: box.width)
}
}

/// Picks the printer's paper for one page size. UIKit takes the size portrait
/// and turns the sheet by `UIPrintInfo.orientation`.
final class PaperChooser: NSObject, UIPrintInteractionControllerDelegate {
private let pageSize: CGSize

init(pageSize: CGSize) {
self.pageSize = pageSize
}

var orientation: UIPrintInfo.Orientation {
pageSize.width > pageSize.height ? .landscape : .portrait
}

func printInteractionController(
_ printInteractionController: UIPrintInteractionController,
choosePaper paperList: [UIPrintPaper]
) -> UIPrintPaper {
let portrait = CGSize(
width: min(pageSize.width, pageSize.height),
height: max(pageSize.width, pageSize.height))
return UIPrintPaper.bestPaper(forPageSize: portrait, withPapersFrom: paperList)
}
}
58 changes: 58 additions & 0 deletions OpenDocumentReaderTests/PrintPaperTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import XCTest

@testable import OpenDocumentReader

class PrintPaperTests: XCTestCase {
private let temporaryDirectory = NSTemporaryDirectory()

private func fixture(_ name: String, _ pathExtension: String) throws -> URL {
let documentsURL = try FileManager.default.url(
for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let url = documentsURL.appendingPathComponent(name + "." + pathExtension)
try? FileManager.default.removeItem(at: url)
let bundlePath = try XCTUnwrap(
Bundle(for: Self.self).path(forResource: name, ofType: pathExtension))
try FileManager.default.copyItem(at: URL(fileURLWithPath: bundlePath), to: url)
return url
}

private func translated(_ url: URL, password: String? = nil) throws -> CoreWrapper {
let wrapper = CoreWrapper()
try wrapper.translate(
url.path, into: temporaryDirectory, with: password, editable: false, scope: .document)
return wrapper
}

func testLengthsBecomePoints() {
XCTAssertEqual(PrintPaper.points(8.5, unit: "in"), 612)
XCTAssertEqual(try XCTUnwrap(PrintPaper.points(21, unit: "cm")), 595.28, accuracy: 0.01)
XCTAssertEqual(try XCTUnwrap(PrintPaper.points(297, unit: "mm")), 841.89, accuracy: 0.01)
XCTAssertEqual(PrintPaper.points(96, unit: "px"), 72)
XCTAssertNil(PrintPaper.points(50, unit: "%"))
XCTAssertNil(PrintPaper.points(0, unit: "cm"))
}

func testATextDocumentPrintsOnItsPage() throws {
let wrapper = try translated(try fixture("test", "odt"))
XCTAssertGreaterThan(wrapper.pageSize.height, wrapper.pageSize.width)
XCTAssertFalse(wrapper.isPrintablePdf)
}

func testASlidePrintsLandscape() throws {
let wrapper = try translated(try fixture("test", "odp"))
XCTAssertGreaterThan(wrapper.pageSize.width, wrapper.pageSize.height)
XCTAssertEqual(PaperChooser(pageSize: wrapper.pageSize).orientation, .landscape)
}

func testAPdfPrintsItself() throws {
let url = try fixture("test", "pdf")
let wrapper = try translated(url)
XCTAssertTrue(wrapper.isPrintablePdf)
XCTAssertNotNil(PrintPaper.firstPageSize(ofPDFAt: url))
}

func testAnEncryptedPdfPrintsThePage() throws {
let wrapper = try translated(try fixture("test-encrypted", "pdf"), password: "secret")
XCTAssertFalse(wrapper.isPrintablePdf)
}
}
Loading