From 2d6fb41497c7a4f6b998b414aa2d653cff2df5cd Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Tue, 18 Aug 2026 21:58:43 +0200 Subject: [PATCH 1/3] Take odrcore 6.9.0 6.7.1 to 6.9.0. A document is now drawn in the reader's own light or dark appearance, which the engine learned in 6.8.0, and our loading and error pages follow it too. Printing is unaffected: a dark page still comes out on white paper, which the new test says. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01W2XM93Raj1jyvUuYmpQrcz --- CHANGELOG.md | 14 +++ OpenDocumentReader.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/swiftpm/Package.resolved | 4 +- OpenDocumentReader/CoreWrapper.swift | 4 + .../DocumentViewController.swift | 18 ++- .../OpenDocumentReaderTests.swift | 17 ++- .../PrintAppearanceTests.swift | 116 ++++++++++++++++++ 7 files changed, 166 insertions(+), 9 deletions(-) create mode 100644 OpenDocumentReaderTests/PrintAppearanceTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 6788df3..64c5fdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,20 @@ 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 + +### Added + +- Documents follow the system light and dark appearance again. Pdfs stay light. + +### Changed + +- The engine is odrcore 6.9.0, up from 6.7.1. Pdfs show filled-in forms and + markup, scanned pages are no longer blank, and a rotated page is turned the + right way up. Word documents keep their paragraph spacing and row heights. + Prose that happens to hold commas is read as text rather than as a table, and + a large text file opens at once instead of after half a minute. + ## [1.41] ### Changed diff --git a/OpenDocumentReader.xcodeproj/project.pbxproj b/OpenDocumentReader.xcodeproj/project.pbxproj index ac7dab8..21446e8 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.7.1; + minimumVersion = 6.9.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 6b3b76f..9fcc859 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" : "82bbb3d4b4d596f2ab28f76611510baaf86d19da", - "version" : "6.7.1" + "revision" : "fb696177bed250b4e2c0aebae326ea6025bca49e", + "version" : "6.9.0" } }, { diff --git a/OpenDocumentReader/CoreWrapper.swift b/OpenDocumentReader/CoreWrapper.swift index 42f6d3c..2a74f25 100644 --- a/OpenDocumentReader/CoreWrapper.swift +++ b/OpenDocumentReader/CoreWrapper.swift @@ -138,6 +138,10 @@ private func isCsv(_ file: DecodedFile) -> Bool { file.fileType == .commaSeparat // 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 + // the reader's own appearance: odrcore writes a dark sheet behind a + // `prefers-color-scheme: dark`, which the web view answers from the + // system setting. A pdf has no dark view and stays light. + config.colorScheme = .system // 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 diff --git a/OpenDocumentReader/DocumentViewController.swift b/OpenDocumentReader/DocumentViewController.swift index 37bb794..243ee77 100644 --- a/OpenDocumentReader/DocumentViewController.swift +++ b/OpenDocumentReader/DocumentViewController.swift @@ -760,11 +760,19 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel printController.present(animated: true, completionHandler: nil) } + /// A page of ours rather than a document: the word "loading", or the error. + /// The colour scheme is named because a web view paints a page that claims + /// none white, whatever the reader has the device set to. + private func loadMessage(_ body: String) { + webview.loadHTMLString( + "\(body)", + baseURL: nil) + } + func documentUpdateContent(_ doc: Document) { guard let url = doc.result else { documentNavigation = nil - self.webview.loadHTMLString( - "

\(NSLocalizedString("loading", comment: ""))

", baseURL: nil) + loadMessage("

\(NSLocalizedString("loading", comment: ""))

") return } @@ -838,9 +846,9 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel } documentNavigation = nil - self.webview.loadHTMLString( - "

\(NSLocalizedString("error", comment: ""))

\(NSLocalizedString("toast_error_generic", comment: ""))", - baseURL: nil) + loadMessage( + "

\(NSLocalizedString("error", comment: ""))

\(NSLocalizedString("toast_error_generic", comment: ""))" + ) AnalyticsManager.shared.report( "load_error", diff --git a/OpenDocumentReaderTests/OpenDocumentReaderTests.swift b/OpenDocumentReaderTests/OpenDocumentReaderTests.swift index b5e9721..fe6748d 100644 --- a/OpenDocumentReaderTests/OpenDocumentReaderTests.swift +++ b/OpenDocumentReaderTests/OpenDocumentReaderTests.swift @@ -215,6 +215,20 @@ class OpenDocumentReaderTests: XCTestCase { } } + /// The page carries both sheets and picks between them itself, so turning + /// the device dark needs no second translation. + func testAPageFollowsTheReadersAppearance() 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("(prefers-color-scheme: dark)"), 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 { @@ -380,7 +394,8 @@ class OpenDocumentReaderTests: XCTestCase { } /// Fulfills its expectation once, whichever way the navigation ends. -private class NavigationRecorder: NSObject, WKNavigationDelegate { +/// Also used by `PrintAppearanceTests`. +class NavigationRecorder: NSObject, WKNavigationDelegate { let finished: XCTestExpectation private(set) var error: Error? private var isDone = false diff --git a/OpenDocumentReaderTests/PrintAppearanceTests.swift b/OpenDocumentReaderTests/PrintAppearanceTests.swift new file mode 100644 index 0000000..76e9cce --- /dev/null +++ b/OpenDocumentReaderTests/PrintAppearanceTests.swift @@ -0,0 +1,116 @@ +import UIKit +import WebKit +import XCTest + +@testable import OpenDocumentReader + +/// Print takes the page the web view is showing, and since odrcore renders a +/// document in the reader's own appearance that page can be dark. Paper is not: +/// a dark page printed as it stands is pale ink on white, which is nothing. +class PrintAppearanceTests: XCTestCase { + + /// The web view prints in light whatever the device is set to, so the menu + /// needs nothing of its own. This says so, and would say if that changed. + func testPrintingADarkPageComesOutOnWhitePaper() throws { + let webview = WKWebView(frame: CGRect(x: 0, y: 0, width: 390, height: 700)) + + // in a window, because that is where a view is told what appearance it + // is in: a web view on its own stays light whatever is set on it + let window = UIWindow(frame: webview.frame) + window.overrideUserInterfaceStyle = .dark + window.rootViewController = UIViewController() + window.rootViewController?.view.addSubview(webview) + window.makeKeyAndVisible() + + let recorder = NavigationRecorder(finished: expectation(description: "loaded")) + webview.navigationDelegate = recorder + + // the shape odrcore emits: a dark sheet gated on the reader's preference + webview.loadHTMLString( + """ + +

Hello

+ """, baseURL: nil) + + wait(for: [recorder.finished], timeout: 30) + XCTAssertNil(recorder.error) + + // or the rest of this passes without ever having been dark + XCTAssertEqual(try evaluate("matchMedia('(prefers-color-scheme: dark)').matches", on: webview) as? Bool, true) + + let page = try printFirstPage(of: webview) + let (mean, darkest) = try brightness(of: page) + + XCTAssertGreaterThan(mean, 0.9, "the paper came out dark") + XCTAssertLessThan(darkest, 0.3, "nothing dark was printed: the text went white on white") + } + + private func evaluate(_ script: String, on webview: WKWebView) throws -> Any? { + var result: Any? + let done = expectation(description: script) + + webview.evaluateJavaScript(script) { value, error in + result = value + XCTAssertNil(error) + done.fulfill() + } + + wait(for: [done], timeout: 30) + + return result + } + + /// One page of what `printDocument` hands the print controller, drawn onto + /// white the way paper is. + private func printFirstPage(of webview: WKWebView) throws -> UIImage { + // US Letter at 72dpi, which is what UIPrintPageRenderer measures in + let paper = CGRect(x: 0, y: 0, width: 612, height: 792) + + let renderer = UIPrintPageRenderer() + renderer.addPrintFormatter(webview.viewPrintFormatter(), startingAtPageAt: 0) + // the two rects have no setters of their own + renderer.setValue(paper, forKey: "paperRect") + renderer.setValue(paper.insetBy(dx: 36, dy: 36), forKey: "printableRect") + + UIGraphicsBeginImageContextWithOptions(paper.size, true, 1) + defer { UIGraphicsEndImageContext() } + + UIColor.white.setFill() + UIRectFill(paper) + renderer.prepare(forDrawingPages: NSRange(location: 0, length: 1)) + renderer.drawPage(at: 0, in: paper) + + return try XCTUnwrap(UIGraphicsGetImageFromCurrentImageContext()) + } + + private func brightness(of image: UIImage) throws -> (mean: Double, darkest: Double) { + let cgImage = try XCTUnwrap(image.cgImage) + let width = cgImage.width + let height = cgImage.height + + var pixels = [UInt8](repeating: 0, count: width * height * 4) + let context = try XCTUnwrap( + CGContext( + data: &pixels, width: width, height: height, bitsPerComponent: 8, + bytesPerRow: width * 4, space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)) + context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height)) + + var total = 0.0 + var darkest = 1.0 + + for pixel in stride(from: 0, to: pixels.count, by: 4) { + let value = + (0.299 * Double(pixels[pixel]) + 0.587 * Double(pixels[pixel + 1]) + + 0.114 * Double(pixels[pixel + 2])) / 255 + + total += value + darkest = min(darkest, value) + } + + return (total / Double(width * height), darkest) + } +} From 1a733c65a041dd5eaa9d8567e3d3ef58ef294b8b Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Tue, 18 Aug 2026 22:02:12 +0200 Subject: [PATCH 2/3] File the engine and the dark appearance under 1.41 1.41 is submitted but not out, so this build goes under its heading rather than opening a new one. Its engine entry now names 6.9.0. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01W2XM93Raj1jyvUuYmpQrcz --- CHANGELOG.md | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64c5fdd..0fb26a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ 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 +## [1.41] ### Added @@ -25,18 +25,11 @@ once the version tag exists. ### Changed -- The engine is odrcore 6.9.0, up from 6.7.1. Pdfs show filled-in forms and - markup, scanned pages are no longer blank, and a rotated page is turned the - right way up. Word documents keep their paragraph spacing and row heights. - Prose that happens to hold commas is read as text rather than as a table, and - a large text file opens at once instead of after half a minute. - -## [1.41] - -### Changed - -- The engine is odrcore 6.7.1, up from 6.6.0. Pdfs, text files and archive +- The engine is odrcore 6.9.0, up from 6.6.0. Pdfs, text files and archive listings can be searched too, and a pdf's text sits where the file puts it. + A pdf also shows what was filled into its forms and its scanned pages, Word + documents keep their paragraph spacing, text that merely holds commas is no + longer read as a table, and a large text file opens at once. - PDFs are rendered by odrcore instead of being handed to the web view, and a password protected one takes the prompt the other formats use. They can be searched like a document. From 71e19979212f8d6f0f2c81af3beb4f44ebf05e6a Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Tue, 18 Aug 2026 22:05:34 +0200 Subject: [PATCH 3/3] Say what changed, not what renders it The changelog entries read as what a reader of the app notices, and no longer name the engine or its version. The English store copy picks up the dark appearance. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01W2XM93Raj1jyvUuYmpQrcz --- CHANGELOG.md | 19 +++++++++---------- fastlane/metadata/en-US/changelogs/1.41.txt | 2 ++ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fb26a1..48dd02a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,18 +21,16 @@ once the version tag exists. ### Added -- Documents follow the system light and dark appearance again. Pdfs stay light. +- Documents follow your light or dark appearance. PDFs stay light. ### Changed -- The engine is odrcore 6.9.0, up from 6.6.0. Pdfs, text files and archive - listings can be searched too, and a pdf's text sits where the file puts it. - A pdf also shows what was filled into its forms and its scanned pages, Word - documents keep their paragraph spacing, text that merely holds commas is no - longer read as a table, and a large text file opens at once. -- PDFs are rendered by odrcore instead of being handed to the web view, and a - password protected one takes the prompt the other formats use. They can be - searched like a document. +- PDFs, text files and archives can be searched, and a PDF's text sits where the + file puts it. +- PDFs show what was filled into their forms, a scanned page is no longer blank, + Word documents keep the space between their paragraphs, and a large text file + opens at once. +- A password protected PDF asks for its password the way the other formats do. - The search button leaves the tool bar when the page cannot be searched, rather than greying out - the same as the edit button. - The pencil turns into a save button while editing, as on Android. Saving from @@ -47,7 +45,7 @@ once the version tag exists. - Flat XML documents (`.fodt`, `.fodp`, `.fods`, `.fodg`), `.otm`, `.xlt` and `.xlm` can be picked in the document browser instead of being greyed out. - odrcore rendered them already; the app claimed no type that reached them. + The app could read them already; it claimed no type that reached them. - The app is translated again. Every language but English was showing English for the privacy screen and the banner, and Danish, Catalan, Turkish and Czech showed it for most of the rest. @@ -55,6 +53,7 @@ once the version tag exists. never came up before. - A document opens at the top of its first page. On a wide screen it opened far enough down to cut off the heading. +- A text file with commas in it is shown as text, not as a table. ### Removed diff --git a/fastlane/metadata/en-US/changelogs/1.41.txt b/fastlane/metadata/en-US/changelogs/1.41.txt index 1753aed..2becf61 100644 --- a/fastlane/metadata/en-US/changelogs/1.41.txt +++ b/fastlane/metadata/en-US/changelogs/1.41.txt @@ -1,4 +1,6 @@ +- Documents follow your light or dark appearance - PDFs can be searched, and password protected ones ask for the password like other documents +- PDFs show what was filled into their forms, and scanned pages are no longer blank - Tapping a document while editing puts the cursor there, and saving is now on the bar rather than in the menu - LibreOffice's flat XML documents and Excel templates open from the document browser instead of being greyed out - The three welcome pages are gone, so the app opens straight on your documents