diff --git a/CHANGELOG.md b/CHANGELOG.md index 6788df3..48dd02a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,13 +19,18 @@ once the version tag exists. ## [1.41] +### Added + +- Documents follow your light or dark appearance. PDFs stay light. + ### Changed -- The engine is odrcore 6.7.1, 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. -- 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 @@ -40,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. @@ -48,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/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) + } +} 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