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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion OpenDocumentReader.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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" */ = {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 7 additions & 29 deletions OpenDocumentReader/CoreWrapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 ?? "")
Expand Down Expand Up @@ -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
Expand Down
102 changes: 0 additions & 102 deletions OpenDocumentReader/DocumentViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -889,8 +789,6 @@ class DocumentViewController: UIViewController, DocumentDelegate, UISearchBarDel
return
}

installFitToWidth(for: url)

documentNavigation = self.webview.load(URLRequest(url: url))
}

Expand Down
4 changes: 3 additions & 1 deletion OpenDocumentReaderTests/EditWorkflowTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
15 changes: 15 additions & 0 deletions OpenDocumentReaderTests/OpenDocumentReaderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down