diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f1324c..fffbbb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ once the version tag exists. ### Changed +- A document wider than the screen opens fitted to it rather than running off + the edge, which is what it has always done on Android. - The way out of a document is a back chevron, not the words "Back to documents". - Documents that can be edited offer a pencil next to the search button. diff --git a/OpenDocumentReader/CoreWrapper.swift b/OpenDocumentReader/CoreWrapper.swift index 6aeed67..331c06f 100644 --- a/OpenDocumentReader/CoreWrapper.swift +++ b/OpenDocumentReader/CoreWrapper.swift @@ -128,15 +128,23 @@ private func isCsv(_ file: DecodedFile) -> Bool { file.fileType == .commaSeparat throw coreWrapperError(.unsupportedFileType, "not a document file") } + // the same answers OpenDocument.droid gives odrcore, so a document is + // the same document on both — the viewport meta each page carries is + // decided from these let config = HtmlConfig() config.editable = editable // resource paths are resolved relative to an output directory, and in // server mode there is none — odrcore rejects the combination config.relativeResourcePaths = false - // the side margins of a printed page, which is what it was written to look like + // the side margins of a printed page, which is what it was written to + // look like, and what makes odrcore call a text document paged: its + // pages are then fitted to the screen rather than shown at full size config.textDocumentMargin = true - // served with the pages rather than inlined as base64, as in OpenDocument.droid + // served with the pages rather than inlined as base64 config.embedImages = false + // odrcore's own css and js go into the page: there is no output + // directory to put them beside + config.embedShippedResources = true let documentType: DocumentType let openedDocument: OdrCoreObjC.Document? diff --git a/OpenDocumentReader/DocumentViewController.swift b/OpenDocumentReader/DocumentViewController.swift index 0e47961..8921c36 100644 --- a/OpenDocumentReader/DocumentViewController.swift +++ b/OpenDocumentReader/DocumentViewController.swift @@ -59,6 +59,61 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel private lazy var toolBarItems: [UIBarButtonItem] = toolBar.items ?? [] private lazy var toolBarItemsWithoutEdit: [UIBarButtonItem] = toolBarItems.filter { $0 !== editButton } + /// What OpenDocument.droid gets from `loadWithOverviewMode`, which iOS has + /// no setting for: a page wider than the screen is zoomed out until it fits + /// instead of running off the edge. + /// + /// odrcore asks for that by leaving the initial scale out of the viewport + /// meta - `width=device-width` alone, which every browser but a web view in + /// overview mode reads as "lay out at screen width and let the rest + /// overflow". A page that names its scale (a spreadsheet, a csv) means it, + /// and is left alone. + /// + /// Only for what odrcore served, which is why `origin` is checked here as + /// well as before the script is installed: the same web view shows the + /// formats odrcore does not handle, and follows links out of a document. + /// Their viewport is their author's to write, and rewriting it would throw + /// away what it says - `user-scalable=no`, a maximum scale, a `viewport-fit`. + private static func fitToWidthScript(servedFrom origin: String) -> String { + """ + (function () { + if (location.origin !== '\(origin)') { + return; + } + + var meta = document.querySelector('meta[name="viewport"]'); + if (!meta || (meta.content || '').indexOf('initial-scale') !== -1) { + return; + } + + var width = document.documentElement.scrollWidth; + if (width > window.innerWidth) { + meta.setAttribute('content', 'width=' + width + ',user-scalable=yes'); + } + })(); + """ + } + + /// Arms ``fitToWidthScript(servedFrom:)`` for a page that came off our own + /// server, and disarms it for anything else. At document end rather than on + /// `didFinish`, so the page is fitted before it is first drawn instead of + /// jumping once the images are in. + private func installFitToWidth(for url: URL) { + let scripts = webview.configuration.userContentController + scripts.removeAllUserScripts() + + guard CoreWrapper.isServedURL(url), + let scheme = url.scheme, let host = url.host, let port = url.port + else { + return + } + + scripts.addUserScript( + WKUserScript( + source: Self.fitToWidthScript(servedFrom: "\(scheme)://\(host):\(port)"), + injectionTime: .atDocumentEnd, forMainFrameOnly: true)) + } + /// Fills the banner slot when no ad does. Sits on top of `bannerSlot` rather than in the /// layout chain, so the slot keeps its height and nothing below it moves. private let houseAdView = HouseAdView() @@ -572,6 +627,8 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel return } + installFitToWidth(for: url) + // pages come off the loopback server; a file URL needs read access // granted along with it if url.isFileURL {