From 57ca1f3ece07f73fa405a869d63ae2e2b50ab1da Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Fri, 4 Sep 2026 08:54:04 +0200 Subject: [PATCH] Take odrcore 6.13.0 Two workarounds this app filed upstream come back fixed, and go: the fit-to-width user script, now that the engine's own fit holds the type at its size in a web view, and the markdown-by-name routing, which the engine does itself. The type in a fitted document is the size the document gives it. Webkit used to enlarge the body text of a page laid out wider than the screen, which left a heading barely larger than the prose under it. The spreadsheet limits are set as themselves rather than as boxed numbers - 6.13.0 gives HtmlConfig's optional settings their real Swift types. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Na3zwCmkKv5pmPUZMmcr6W --- CHANGELOG.md | 20 ++++ OpenDocumentReader.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- OpenDocumentReader/CoreWrapper.swift | 36 ++----- .../DocumentViewController.swift | 102 ------------------ .../EditWorkflowTests.swift | 4 +- .../OpenDocumentReaderTests.swift | 15 +++ 7 files changed, 48 insertions(+), 135 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2292478..e00b712 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,26 @@ open: **a second build under the same version goes under the already cut heading, not back under `Unreleased`.** Date the heading and add its compare link once the version tag exists. +## [Unreleased] + +### Changed + +- The engine is odrcore 6.13.0, up from 6.11.0. +- A document is fitted to the screen by the engine, and its text keeps the size + the document gives it instead of being enlarged. +- In a Word file a table draws its own borders, a floating picture sits where it + was placed, and a page break starts a new page. +- In an OpenDocument file a chart is drawn from the chart rather than the picture + saved beside it, a shape as the shape rather than as its box, and the older + drawings a document carries are drawn at all. +- Lines in a PDF written by Canva come out at the width they were drawn at. +- A password protected document can no longer be edited: saving it wrote the + content out without its password. + +### Fixed + +- A very wide or repetitive spreadsheet opens instead of running out of memory. + ## [1.43] ### Added diff --git a/OpenDocumentReader.xcodeproj/project.pbxproj b/OpenDocumentReader.xcodeproj/project.pbxproj index 5f9d5f4..2502410 100644 --- a/OpenDocumentReader.xcodeproj/project.pbxproj +++ b/OpenDocumentReader.xcodeproj/project.pbxproj @@ -830,7 +830,7 @@ repositoryURL = "https://github.com/opendocument-app/OpenDocument.core.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 6.11.0; + minimumVersion = 6.13.0; }; }; AD584FCD41577C8CDEE974AA /* XCRemoteSwiftPackageReference "swift-package-manager-google-mobile-ads" */ = { diff --git a/OpenDocumentReader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/OpenDocumentReader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ba8ce08..9feccd1 100644 --- a/OpenDocumentReader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/OpenDocumentReader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/opendocument-app/OpenDocument.core.git", "state" : { - "revision" : "3fa5603c9093ac2df3ca0596d612ad11390e7f95", - "version" : "6.11.0" + "revision" : "1b1599864aefe78a23f01da07d3501e617810a05", + "version" : "6.13.0" } }, { diff --git a/OpenDocumentReader/CoreWrapper.swift b/OpenDocumentReader/CoreWrapper.swift index 57c63c8..743b18d 100644 --- a/OpenDocumentReader/CoreWrapper.swift +++ b/OpenDocumentReader/CoreWrapper.swift @@ -91,37 +91,12 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [ private var document: OdrCoreObjC.Document? private let lock = NSRecursiveLock() - /// The largest sheet region translated, as on OpenDocument.droid. The - /// encoding is written out because Swift has no `@encode`. - private static let spreadsheetLimit: NSValue = withUnsafeBytes( - of: TableDimensions(rows: 100_000, columns: 500) - ) { NSValue(bytes: $0.baseAddress!, objCType: "{ODRTableDimensions=II}") } + /// The largest sheet region translated, as on OpenDocument.droid. + private static let spreadsheetLimit = TableDimensions(rows: 100_000, columns: 500) /// Bounds the rows by the sheet's width: the wider, the fewer it keeps. private static let spreadsheetCellLimit: UInt64 = 500_000 - /// As odrcore reads the bytes, or as the name says where that reading is - /// text. Markdown has no signature, so only the name can name it. - private func openFile(_ inputPath: String) throws -> DecodedFile { - let detected = try DecodedFile.decode(path: inputPath) - let declared = Odr.fileType(extension: URL(fileURLWithPath: inputPath).pathExtension) - - guard declared != .unknown, declared != detected.fileType, detected.isTextFile, - nameOutranksText(declared) - else { - return detected - } - - return (try? DecodedFile.decode(path: inputPath, as: declared)) ?? detected - } - - /// A document, or a format odrcore cannot detect from its bytes. Csv is - /// neither: odrcore reads that out of the text itself. - private func nameOutranksText(_ declared: FileType) -> Bool { - Odr.fileCategory(fileType: declared) == .document - || !Odr.capabilities(fileType: declared).detectByContent - } - @objc func translate( _ inputPath: String, cache cachePath: String, @@ -142,7 +117,7 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [ throw coreWrapperError(.unsupportedFileType, "odrcore does not recognise this file type") } - var file = try openFile(inputPath) + var file = try DecodedFile.decode(path: inputPath) if file.isPasswordEncrypted { do { file = try file.decrypt(withPassword: password ?? "") @@ -182,9 +157,12 @@ private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [ // odrcore's own css and js go into the page: there is no output // directory to put them beside config.embedShippedResources = true + // the page measures the view and fits itself to it: a web view does not + // fit a page to the screen the way a browser does + config.viewportMode = .fitWidthByView // stated rather than inherited: a sheet past the limit is cut off silently config.spreadsheetLimit = Self.spreadsheetLimit - config.spreadsheetCellLimit = NSNumber(value: Self.spreadsheetCellLimit) + config.spreadsheetCellLimit = Self.spreadsheetCellLimit config.spreadsheetLimitByContent = true let documentType: DocumentType diff --git a/OpenDocumentReader/DocumentViewController.swift b/OpenDocumentReader/DocumentViewController.swift index 351e3aa..1314c8f 100644 --- a/OpenDocumentReader/DocumentViewController.swift +++ b/OpenDocumentReader/DocumentViewController.swift @@ -75,105 +75,6 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel } } - /// 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 served = meta.content; - var natural = document.documentElement.scrollWidth; - - // The web view's own width, which the viewport named below does not - // change: the visual viewport is that many CSS pixels at that scale. - function available() { - var seen = window.visualViewport; - - return seen ? Math.round(seen.width * seen.scale) : window.innerWidth; - } - - function fit() { - meta.setAttribute( - 'content', - natural > available() ? 'width=' + natural + ',user-scalable=yes' : served); - } - - // Across a resize the browser keeps the reader's place by holding on - // to whatever was against the top of the screen, and here it gets it - // wrong: the scale changes with the width, and the page comes back - // hundreds of pixels down - a page or more of a long document on an - // iPad, which is where the width really does change. It settles - // there some frames after the resize, so the place the reader was - // actually at is re-asserted until it has finished, and dropped the - // moment they take hold of the page themselves. - var holding = []; - - function hold(place) { - holding.forEach(clearTimeout); - holding = [0, 16, 50, 150, 300, 500].map(function (ms) { - return setTimeout(function () { window.scrollTo(window.scrollX, place); }, ms); - }); - } - - window.addEventListener('touchstart', function () { - holding.forEach(clearTimeout); - holding = []; - }, { passive: true }); - - // On every resize, not just now: this first runs before the web view - // has the width it will keep, and a page held at a width it no - // longer needs is left scrolled off its own top. - fit(); - window.addEventListener('resize', function () { - var was = window.scrollY; - fit(); - hold(was); - }); - })(); - """ - } - - /// 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() @@ -285,7 +186,6 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel if let page = corePageInReserve { corePageInReserve = nil - installFitToWidth(for: page) documentNavigation = webview.load(URLRequest(url: page)) return @@ -889,8 +789,6 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel return } - installFitToWidth(for: url) - documentNavigation = self.webview.load(URLRequest(url: url)) } diff --git a/OpenDocumentReaderTests/EditWorkflowTests.swift b/OpenDocumentReaderTests/EditWorkflowTests.swift index 51edacf..4b0d7f4 100644 --- a/OpenDocumentReaderTests/EditWorkflowTests.swift +++ b/OpenDocumentReaderTests/EditWorkflowTests.swift @@ -107,7 +107,9 @@ class EditWorkflowTests: XCTestCase { """ (function () { var run = document.querySelector('[contenteditable]'); - var box = run.getBoundingClientRect(); + // not getBoundingClientRect: the view applies a zoom to fit, + // which webkit leaves out of it but elementFromPoint expects + var box = odr.getViewportRect(run); var hit = document.elementFromPoint(box.left + box.width / 2, box.top + box.height / 2); return hit ? hit.tagName : 'none'; diff --git a/OpenDocumentReaderTests/OpenDocumentReaderTests.swift b/OpenDocumentReaderTests/OpenDocumentReaderTests.swift index 1000dd5..6e2975d 100644 --- a/OpenDocumentReaderTests/OpenDocumentReaderTests.swift +++ b/OpenDocumentReaderTests/OpenDocumentReaderTests.swift @@ -227,6 +227,21 @@ class OpenDocumentReaderTests: XCTestCase { XCTAssertTrue(html.contains("(prefers-color-scheme: dark)"), html) } + /// The page measures the view and fits itself to it. A web view does not fit + /// a page to the screen the way a browser does, and the app used to carry a + /// script of its own for this. + func testAPageFitsItselfToTheView() throws { + let wrapper = CoreWrapper() + + try wrapper.translate( + documentURL.path, cache: temporaryDirectory, into: temporaryDirectory, with: nil, editable: false) + + let (data, _) = try fetch(try XCTUnwrap(wrapper.pageURLs.first)) + let html = try XCTUnwrap(String(data: data, encoding: .utf8)) + + XCTAssertTrue(html.contains("--odr-fit:view"), html) + } + /// The same URL would come back out of the web view's cache holding the /// pages the document had before the password or the edit. func testRetranslatingMovesThePagesToNewAddresses() throws {