From e90a9cac36df1bdcdac32c4380a0608e949406c2 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sat, 1 Aug 2026 11:27:42 +0200 Subject: [PATCH 1/4] feat: take odrcore from the OdrCore Swift package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the conan build of odrcore and the ObjC++ `CoreWrapper` that talked to its C++ API directly. The app now links a prebuilt xcframework through SwiftPM, so building it needs Xcode and nothing else — no conan install, no `conan/setup-all.sh`, no generated xcconfig, and no asset copy phase: the framework carries the renderer's css/js and the libmagic database itself and points odrcore at them from `+load`, before `main`. `CoreWrapper` is swift now and keeps the interface its callers already used. What it does is unchanged — the same view selection as OpenDocument.droid, the same loopback server with a fresh prefix per translation so the web view cannot serve a cached page from before a password or an edit. The package is referenced by path for now, so this needs the xcframework built next to it: cd ../OpenDocument.core && apple/build_xcframework.py slice && \ apple/build_xcframework.py assemble ODR_XCFRAMEWORK=OdrCoreObjC.xcframework xcodebuild ... -scheme "ODR Full" It becomes a versioned `.package(url:from:)` once odrcore cuts a release. All 27 tests pass on the simulator, including translating odt/ods/odp, serving them over loopback, loading one in a WKWebView and writing an edit back. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VYR3UqA1asTaTTNRm29csV --- OpenDocumentReader.xcodeproj/project.pbxproj | 77 ++--- OpenDocumentReader/BridgingHeader.h | 5 - OpenDocumentReader/CoreWrapper.h | 55 --- OpenDocumentReader/CoreWrapper.mm | 345 ------------------- OpenDocumentReader/CoreWrapper.swift | 188 ++++++++++ 5 files changed, 213 insertions(+), 457 deletions(-) delete mode 100644 OpenDocumentReader/BridgingHeader.h delete mode 100644 OpenDocumentReader/CoreWrapper.h delete mode 100644 OpenDocumentReader/CoreWrapper.mm create mode 100644 OpenDocumentReader/CoreWrapper.swift diff --git a/OpenDocumentReader.xcodeproj/project.pbxproj b/OpenDocumentReader.xcodeproj/project.pbxproj index 19e1d86..cfedad9 100644 --- a/OpenDocumentReader.xcodeproj/project.pbxproj +++ b/OpenDocumentReader.xcodeproj/project.pbxproj @@ -21,29 +21,30 @@ ACA46529244E403900A5DA7A /* AppType.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACA46528244E403900A5DA7A /* AppType.swift */; }; ACD9BE3C2444A371009014E6 /* ConfigurationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACD9BE2D2444A371009014E6 /* ConfigurationManager.swift */; }; BFFB694BE9E3E744FD4F92E0 /* CrashManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25ACB597DDB073F3AFC74345 /* CrashManager.swift */; }; + C43B65A97E4029F7790DA729 /* CoreWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02C00CD0A89EF1F64FF879A1 /* CoreWrapper.swift */; }; D2DCD7104EBCC0F1A784E116 /* GoogleMobileAds in Frameworks */ = {isa = PBXBuildFile; productRef = B5F033C5AA89A6E9C19EA27D /* GoogleMobileAds */; }; E1A78CF72C1A53DB00CD43E4 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1EB6C492C1A510D003EC5A0 /* Foundation.framework */; }; E2064DFE22CFA1BA006441F8 /* iAd.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E2064DDF22CFA1BA006441F8 /* iAd.framework */; }; E22B252F2557F0E2001D0C52 /* OpenDocumentReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22B252E2557F0E2001D0C52 /* OpenDocumentReaderTests.swift */; }; - E2A17B0400000000000000A4 /* PageTabBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A17B0300000000000000A3 /* PageTabBarTests.swift */; }; E22EB716226B621200053B86 /* DocumentBrowserTransitioningDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22EB6F7226B621200053B86 /* DocumentBrowserTransitioningDelegate.swift */; }; E22EB718226B633500053B86 /* Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22EB717226B633500053B86 /* Document.swift */; }; E22EB71A226B65E300053B86 /* DocumentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E22EB719226B65E300053B86 /* DocumentViewController.swift */; }; - E2A17B0200000000000000A2 /* PageTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A17B0100000000000000A1 /* PageTabBar.swift */; }; E22EB71C226B66B300053B86 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E22EB71B226B66B300053B86 /* Main.storyboard */; }; E23795302274844400BA7238 /* AdSupport.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E237952F2274844400BA7238 /* AdSupport.framework */; }; E24110312586349500800247 /* test.odt in Resources */ = {isa = PBXBuildFile; fileRef = E24110232586349500800247 /* test.odt */; }; + E26C39392250DC6E009C484A /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E26C39382250DC6E009C484A /* WebKit.framework */; }; + E2A17B0200000000000000A2 /* PageTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A17B0100000000000000A1 /* PageTabBar.swift */; }; + E2A17B0400000000000000A4 /* PageTabBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A17B0300000000000000A3 /* PageTabBarTests.swift */; }; E2A17B1000000000000000B0 /* test.ods in Resources */ = {isa = PBXBuildFile; fileRef = E2A17B1200000000000000B2 /* test.ods */; }; E2A17B1100000000000000B1 /* test.odp in Resources */ = {isa = PBXBuildFile; fileRef = E2A17B1300000000000000B3 /* test.odp */; }; - E26C39392250DC6E009C484A /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E26C39382250DC6E009C484A /* WebKit.framework */; }; E2A17B2000000000000000C0 /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E2A17B2200000000000000C2 /* CoreFoundation.framework */; }; E2A17B2100000000000000C1 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E2A17B2300000000000000C3 /* CFNetwork.framework */; }; - E2C008FA220F1CF80097C594 /* CoreWrapper.mm in Sources */ = {isa = PBXBuildFile; fileRef = E2C008F9220F1CF80097C594 /* CoreWrapper.mm */; }; E2D0B3D9226D945400534FCC /* StoreReviewHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D0B3D8226D945400534FCC /* StoreReviewHelper.swift */; }; E2F7ED53220B54D600D63515 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F7ED52220B54D600D63515 /* AppDelegate.swift */; }; E2F7ED55220B54D600D63515 /* DocumentBrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F7ED54220B54D600D63515 /* DocumentBrowserViewController.swift */; }; E2F7ED5E220B54D700D63515 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E2F7ED5D220B54D700D63515 /* Assets.xcassets */; }; E2F7ED61220B54D700D63515 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E2F7ED5F220B54D700D63515 /* LaunchScreen.storyboard */; }; + FB494C85E264BD924C1EA54E /* OdrCore in Frameworks */ = {isa = PBXBuildFile; productRef = E381918B979121CD165C966A /* OdrCore */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -57,6 +58,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 02C00CD0A89EF1F64FF879A1 /* CoreWrapper.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CoreWrapper.swift; path = CoreWrapper.swift; sourceTree = ""; }; 25ACB597DDB073F3AFC74345 /* CrashManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrashManager.swift; sourceTree = ""; }; 2DF2708F60BE3388B124CABF /* AnalyticsManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AnalyticsManager.swift; sourceTree = ""; }; 523A371228CCF27400876C77 /* AdServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdServices.framework; path = System/Library/Frameworks/AdServices.framework; sourceTree = SDKROOT; }; @@ -89,27 +91,23 @@ B01C1B2BA00A7917FF7D7726 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; B94897965C815527C6C06996 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; C4579E708F099784EF583B35 /* SceneDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - D14280D860FAA1DD046BCB5C /* conan.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = conan.xcconfig; sourceTree = ""; }; E1EB6C492C1A510D003EC5A0 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; E2064DDF22CFA1BA006441F8 /* iAd.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = iAd.framework; path = System/Library/Frameworks/iAd.framework; sourceTree = SDKROOT; }; E22B252E2557F0E2001D0C52 /* OpenDocumentReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenDocumentReaderTests.swift; sourceTree = ""; }; - E2A17B0300000000000000A3 /* PageTabBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageTabBarTests.swift; sourceTree = ""; }; E22B25302557F0E2001D0C52 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; E22EB6F7226B621200053B86 /* DocumentBrowserTransitioningDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DocumentBrowserTransitioningDelegate.swift; sourceTree = ""; }; E22EB717226B633500053B86 /* Document.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Document.swift; sourceTree = ""; }; E22EB719226B65E300053B86 /* DocumentViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DocumentViewController.swift; sourceTree = ""; }; - E2A17B0100000000000000A1 /* PageTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageTabBar.swift; sourceTree = ""; }; E22EB71B226B66B300053B86 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; E237952F2274844400BA7238 /* AdSupport.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AdSupport.framework; path = System/Library/Frameworks/AdSupport.framework; sourceTree = SDKROOT; }; E24110232586349500800247 /* test.odt */ = {isa = PBXFileReference; lastKnownFileType = file; path = test.odt; sourceTree = ""; }; + E26C39382250DC6E009C484A /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; + E2A17B0100000000000000A1 /* PageTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageTabBar.swift; sourceTree = ""; }; + E2A17B0300000000000000A3 /* PageTabBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageTabBarTests.swift; sourceTree = ""; }; E2A17B1200000000000000B2 /* test.ods */ = {isa = PBXFileReference; lastKnownFileType = file; path = test.ods; sourceTree = ""; }; E2A17B1300000000000000B3 /* test.odp */ = {isa = PBXFileReference; lastKnownFileType = file; path = test.odp; sourceTree = ""; }; - E26C39382250DC6E009C484A /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; E2A17B2200000000000000C2 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = System/Library/Frameworks/CoreFoundation.framework; sourceTree = SDKROOT; }; E2A17B2300000000000000C3 /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; }; - E2BB4B60220EF3A10056176B /* BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = ""; }; - E2C008F9220F1CF80097C594 /* CoreWrapper.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = CoreWrapper.mm; sourceTree = ""; }; - E2C008FC220F1D570097C594 /* CoreWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CoreWrapper.h; sourceTree = ""; }; E2D0B3D8226D945400534FCC /* StoreReviewHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreReviewHelper.swift; sourceTree = ""; }; E2F7ED52220B54D600D63515 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; E2F7ED54220B54D600D63515 /* DocumentBrowserViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentBrowserViewController.swift; sourceTree = ""; }; @@ -140,6 +138,7 @@ 523A371328CCF27400876C77 /* AdServices.framework in Frameworks */, E1A78CF72C1A53DB00CD43E4 /* Foundation.framework in Frameworks */, D2DCD7104EBCC0F1A784E116 /* GoogleMobileAds in Frameworks */, + FB494C85E264BD924C1EA54E /* OdrCore in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -187,7 +186,6 @@ E17AF34D2C258C5100FBED6A /* configs */ = { isa = PBXGroup; children = ( - D14280D860FAA1DD046BCB5C /* conan.xcconfig */, ); path = configs; sourceTree = ""; @@ -252,14 +250,12 @@ E2F7ED5F220B54D700D63515 /* LaunchScreen.storyboard */, E2F7ED62220B54D700D63515 /* Info.plist */, ACF1A3E42469F8DE000BA420 /* Info-Lite.plist */, - E2BB4B60220EF3A10056176B /* BridgingHeader.h */, - E2C008F9220F1CF80097C594 /* CoreWrapper.mm */, - E2C008FC220F1D570097C594 /* CoreWrapper.h */, E2D0B3D8226D945400534FCC /* StoreReviewHelper.swift */, AC125F182435311A008AD515 /* Localizable.strings */, 3FF7388D538126B646A5ACC6 /* Privacy */, 9113B590877803CA250DDFB7 /* NonFree */, C4579E708F099784EF583B35 /* SceneDelegate.swift */, + 02C00CD0A89EF1F64FF879A1 /* CoreWrapper.swift */, ); path = OpenDocumentReader; sourceTree = ""; @@ -292,7 +288,6 @@ E2F7ED4B220B54D600D63515 /* Sources */, E2F7ED4C220B54D600D63515 /* Frameworks */, E2F7ED4D220B54D600D63515 /* Resources */, - E158977C2DF640E700319830 /* Copy Conan Resources */, ); buildRules = ( ); @@ -301,6 +296,7 @@ name = OpenDocumentReader; packageProductDependencies = ( B5F033C5AA89A6E9C19EA27D /* GoogleMobileAds */, + E381918B979121CD165C966A /* OdrCore */, ); productName = "OpenDocument Reader"; productReference = 52A348B12A9262D100DACAB9 /* OpenDocumentReader.app */; @@ -353,6 +349,7 @@ mainGroup = E2F7ED46220B54D600D63515; packageReferences = ( AD584FCD41577C8CDEE974AA /* XCRemoteSwiftPackageReference "swift-package-manager-google-mobile-ads" */, + 78200B97C10509E2539B4FE5 /* XCLocalSwiftPackageReference "OpenDocument.core" */, ); productRefGroup = E2F7ED46220B54D600D63515; projectDirPath = ""; @@ -390,29 +387,6 @@ }; /* End PBXResourcesBuildPhase section */ -/* Begin PBXShellScriptBuildPhase section */ - E158977C2DF640E700319830 /* Copy Conan Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PROJECT_DIR}/conan-assets/input-files.xcfilelist", - ); - inputPaths = ( - ); - name = "Copy Conan Resources"; - outputFileListPaths = ( - "${PROJECT_DIR}/conan-assets/output-files.xcfilelist", - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PROJECT_DIR}/conan/copy-resources.sh\"\n"; - }; -/* End PBXShellScriptBuildPhase section */ - /* Begin PBXSourcesBuildPhase section */ E22B25282557F0E2001D0C52 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -432,7 +406,6 @@ AC384BCD23B4FFA700C7BF47 /* ContentViewController.swift in Sources */, E22EB718226B633500053B86 /* Document.swift in Sources */, AC384BCF23B4FFA700C7BF47 /* PageViewController.swift in Sources */, - E2C008FA220F1CF80097C594 /* CoreWrapper.mm in Sources */, E2D0B3D9226D945400534FCC /* StoreReviewHelper.swift in Sources */, E22EB716226B621200053B86 /* DocumentBrowserTransitioningDelegate.swift in Sources */, ACA46529244E403900A5DA7A /* AppType.swift in Sources */, @@ -443,6 +416,7 @@ 32B6BA42C6D6D443ED2A9697 /* AnalyticsManager.swift in Sources */, BFFB694BE9E3E744FD4F92E0 /* CrashManager.swift in Sources */, 229BA8DA70F3CA88BA547FA9 /* SceneDelegate.swift in Sources */, + C43B65A97E4029F7790DA729 /* CoreWrapper.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -557,7 +531,6 @@ }; AC8468CC244E255400E96707 /* Debug Lite */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D14280D860FAA1DD046BCB5C /* conan.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; BUNDLE_DISPLAY_NAME = "OpenDocumentReader Lite"; @@ -568,7 +541,6 @@ CURRENT_PROJECT_VERSION = 42; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; - DEVELOPMENT_ASSET_PATHS = "conan-assets"; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -587,7 +559,6 @@ PRODUCT_NAME = "$(BUNDLE_DISPLAY_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; - SWIFT_OBJC_BRIDGING_HEADER = OpenDocumentReader/BridgingHeader.h; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -654,7 +625,6 @@ }; AC8468D0244E256900E96707 /* Release Lite */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D14280D860FAA1DD046BCB5C /* conan.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; BUNDLE_DISPLAY_NAME = "OpenDocumentReader Lite"; @@ -664,7 +634,6 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 42; DEFINES_MODULE = YES; - DEVELOPMENT_ASSET_PATHS = "conan-assets"; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -682,7 +651,6 @@ PRODUCT_NAME = "$(BUNDLE_DISPLAY_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; - SWIFT_OBJC_BRIDGING_HEADER = OpenDocumentReader/BridgingHeader.h; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -890,7 +858,6 @@ }; E2F7ED66220B54D700D63515 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D14280D860FAA1DD046BCB5C /* conan.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; BUNDLE_DISPLAY_NAME = OpenDocumentReader; @@ -901,7 +868,6 @@ CURRENT_PROJECT_VERSION = 42; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEFINES_MODULE = YES; - DEVELOPMENT_ASSET_PATHS = "conan-assets"; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -920,7 +886,6 @@ PRODUCT_NAME = "$(BUNDLE_DISPLAY_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; - SWIFT_OBJC_BRIDGING_HEADER = OpenDocumentReader/BridgingHeader.h; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -929,7 +894,6 @@ }; E2F7ED67220B54D700D63515 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D14280D860FAA1DD046BCB5C /* conan.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; BUNDLE_DISPLAY_NAME = OpenDocumentReader; @@ -939,7 +903,6 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 42; DEFINES_MODULE = YES; - DEVELOPMENT_ASSET_PATHS = "conan-assets"; ENABLE_BITCODE = NO; ENABLE_MODULE_VERIFIER = YES; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -957,7 +920,6 @@ PRODUCT_NAME = "$(BUNDLE_DISPLAY_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; - SWIFT_OBJC_BRIDGING_HEADER = OpenDocumentReader/BridgingHeader.h; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -1001,6 +963,13 @@ }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + 78200B97C10509E2539B4FE5 /* XCLocalSwiftPackageReference "OpenDocument.core" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = ../OpenDocument.core; + }; +/* End XCLocalSwiftPackageReference section */ + /* Begin XCRemoteSwiftPackageReference section */ AD584FCD41577C8CDEE974AA /* XCRemoteSwiftPackageReference "swift-package-manager-google-mobile-ads" */ = { isa = XCRemoteSwiftPackageReference; @@ -1018,6 +987,10 @@ package = AD584FCD41577C8CDEE974AA /* XCRemoteSwiftPackageReference "swift-package-manager-google-mobile-ads" */; productName = GoogleMobileAds; }; + E381918B979121CD165C966A /* OdrCore */ = { + isa = XCSwiftPackageProductDependency; + productName = OdrCore; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = E2F7ED47220B54D600D63515 /* Project object */; diff --git a/OpenDocumentReader/BridgingHeader.h b/OpenDocumentReader/BridgingHeader.h deleted file mode 100644 index e502096..0000000 --- a/OpenDocumentReader/BridgingHeader.h +++ /dev/null @@ -1,5 +0,0 @@ -// -// Use this file to import your target's public headers that you would like to expose to Swift. -// - -#include "CoreWrapper.h" diff --git a/OpenDocumentReader/CoreWrapper.h b/OpenDocumentReader/CoreWrapper.h deleted file mode 100644 index 3bcf757..0000000 --- a/OpenDocumentReader/CoreWrapper.h +++ /dev/null @@ -1,55 +0,0 @@ -// -// CoreWrapper.h -// OpenDocument Reader -// -// Created by Thomas Taschauer on 09.02.19. -// Copyright © 2019 Thomas Taschauer. All rights reserved. -// - -#ifndef CoreWrapper_h -#define CoreWrapper_h - -#import - -NS_ASSUME_NONNULL_BEGIN - -extern NSErrorDomain const CoreWrapperErrorDomain; - -typedef NS_ERROR_ENUM(CoreWrapperErrorDomain, CoreWrapperError) { - /// odrcore threw something we have no specific handling for. - CoreWrapperErrorUnknown = 1, - /// The document is encrypted and the supplied password did not open it. - CoreWrapperErrorWrongPassword = 2, - /// Not a document odrcore can translate. PDFs land here on purpose. - CoreWrapperErrorUnsupportedFileType = 3, -}; - -@interface CoreWrapper : NSObject - -@property (nonatomic, copy, readonly) NSArray *pageNames; - -/// One URL per page, in the order they should be shown. Loopback HTTP URLs -/// served by odrcore, or `file:` URLs below the output path when the app is -/// built to render offline - see `kCoreWrapperServesOverHttp`. -@property (nonatomic, copy, readonly) NSArray *pageURLs; - -- (BOOL)translate:(NSString *)inputPath - cache:(NSString *)cachePath - into:(NSString *)outputPath - with:(nullable NSString *)password - editable:(BOOL)editable - error:(NSError **)error; - -- (BOOL)backTranslate:(NSString *)diff - into:(NSString *)outputPath - error:(NSError **)error; - -/// Whether this URL is one odrcore is serving, rather than somewhere a link in -/// the document leads. -+ (BOOL)isServedURL:(NSURL *)url; - -@end - -NS_ASSUME_NONNULL_END - -#endif /* CoreWrapper_h */ diff --git a/OpenDocumentReader/CoreWrapper.mm b/OpenDocumentReader/CoreWrapper.mm deleted file mode 100644 index c989c23..0000000 --- a/OpenDocumentReader/CoreWrapper.mm +++ /dev/null @@ -1,345 +0,0 @@ -// -// CoreWrapper.mm -// OpenDocument Reader -// -// Created by Thomas Taschauer on 09.02.19. -// Copyright © 2019 Thomas Taschauer. All rights reserved. -// - -#import - -#import "CoreWrapper.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -/// Whether the translated HTML reaches the web view over loopback HTTP instead -/// of being written to the output directory as files. -/// -/// HTTP is what OpenDocument.droid does and where this is headed. odrcore then -/// renders a page when the web view asks for it, on one of the server's -/// threads, rather than rendering every page up front on whichever thread -/// called `translate`. Set this to `NO` to go back to files - the offline path -/// is kept working until the migration is finished, not as a runtime option. -static const BOOL kCoreWrapperServesOverHttp = YES; - -/// Loopback address for that server. Nothing off the device can reach it. -static NSString *const kCoreWrapperHttpHost = @"127.0.0.1"; - -NSErrorDomain const CoreWrapperErrorDomain = @"app.opendocument.CoreWrapperErrorDomain"; - -static NSError *CoreWrapperMakeError(CoreWrapperError code, NSString *description) { - return [NSError errorWithDomain:CoreWrapperErrorDomain - code:code - userInfo:@{NSLocalizedDescriptionKey: description}]; -} - -/// The view odrcore names "document" holds the whole file in one page: every -/// slide of a presentation, every page of a PDF, the entire text document. -static bool CoreWrapperIsCombinedView(const odr::HtmlView &view) { - return view.name() == "document"; -} - -/// Picks the views to show as pages, the same way OpenDocument.droid does. -/// -/// Spreadsheets get one tab per sheet, because scrolling through every sheet of -/// a workbook in one page is not how anyone reads a spreadsheet. Everything else -/// gets the combined view and nothing else - a presentation would otherwise show -/// its slides twice, once inside the combined view and once per slide view, and -/// a PDF would list one tab per page next to the tab that already has them all. -/// -/// Services without a combined view - plain text and images among them - keep -/// whatever views they do offer. -static odr::HtmlViews CoreWrapperSelectViews(const odr::HtmlViews &views, - odr::DocumentType documentType) { - bool isSpreadsheet = documentType == odr::DocumentType::spreadsheet; - bool hasCombinedView = std::any_of(views.begin(), views.end(), CoreWrapperIsCombinedView); - - odr::HtmlViews selected; - for (const odr::HtmlView &view : views) { - bool isCombinedView = CoreWrapperIsCombinedView(view); - bool skip = isSpreadsheet ? isCombinedView : (hasCombinedView && !isCombinedView); - if (skip) { - continue; - } - - selected.push_back(view); - } - - return selected; -} - -static std::optional g_server; - -/// The port `bind` handed us, and with it whether there is a server at all. -/// Zero until one is running. -static std::atomic g_serverPort{0}; - -/// Brings up the one server the app has, on first use, and leaves it running -/// for the rest of the process. Returns NO when the socket could not be opened, -/// in which case translate falls back to files. -/// -/// Only attempted once: retrying would fork the state of `g_server`. -static BOOL CoreWrapperStartServer() { - static BOOL running = NO; - static dispatch_once_t onceToken; - - dispatch_once(&onceToken, ^{ - try { - g_server.emplace(); - // port 0 asks for whichever port is free and bind() reports back - // which one that was. Nothing outside this process needs to know it - // in advance, and a fixed port would be one the other flavor of the - // app, or anything else on the device, could already be holding - g_serverPort.store(g_server->bind([kCoreWrapperHttpHost UTF8String], 0)); - } catch (...) { - g_server.reset(); - - return; - } - - // the socket is bound and taking connections into its backlog from here - // on, so there is nothing to wait for before handing out URLs. - // listen() only returns once the server is stopped, and the server is - // stopped when the process ends, so this thread is never joined - std::thread([] { - try { - g_server->listen(); - } catch (...) { - } - }).detach(); - - running = YES; - }); - - return running; -} - -/// Where the server serves a view: `HttpServer` routes `/file//` -/// to the service connected under that prefix, and the pages ask for their -/// images and fonts relative to that, so those are covered by the same route. -static NSURL *CoreWrapperPageURL(const std::string &prefix, const std::string &path) { - NSString *escapedPath = [[NSString stringWithUTF8String:path.c_str()] - stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLPathAllowedCharacterSet]]; - - NSString *url = [NSString stringWithFormat:@"http://%@:%u/file/%s/%@", kCoreWrapperHttpHost, - static_cast(g_serverPort.load()), - prefix.c_str(), escapedPath]; - - return [NSURL URLWithString:url]; -} - -/// odrcore's data files ship inside the app bundle. The path never changes at -/// runtime, so it is set once instead of on every translate call. -static void CoreWrapperEnsureDataPath() { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - NSString *bundlePath = [[NSBundle mainBundle] bundlePath]; - std::string dataPath = std::string([bundlePath UTF8String]) + "/odrcore"; - odr::GlobalParams::set_odr_core_data_path(dataPath); - }); -} - -@implementation CoreWrapper { - std::optional _document; - std::optional _html; -} - -- (BOOL)translate:(NSString *)inputPath - cache:(NSString *)cachePath - into:(NSString *)outputPath - with:(NSString *)password - editable:(BOOL)editable - error:(NSError **)error { - @synchronized(self) { - _pageNames = nil; - _pageURLs = nil; - - try { - CoreWrapperEnsureDataPath(); - - _html.reset(); - - BOOL overHttp = kCoreWrapperServesOverHttp && CoreWrapperStartServer(); - - odr::HtmlConfig config; - config.editable = editable; - // resource paths are resolved relative to an output directory, and - // in server mode there is none - odrcore rejects the combination - config.relative_resource_paths = !overHttp; - - std::string inputPathCpp = std::string([inputPath UTF8String]); - - std::vector fileTypes; - try { - fileTypes = odr::list_file_types(inputPathCpp); - } catch (odr::UnsupportedFileType &) { - fileTypes.clear(); - } - if (fileTypes.empty()) { - if (error) { - *error = CoreWrapperMakeError(CoreWrapperErrorUnsupportedFileType, - @"odrcore does not recognise this file type"); - } - return NO; - } - - // PDFs are handed to WKWebView instead, which renders them natively - if (std::find(fileTypes.begin(), fileTypes.end(), - odr::FileType::portable_document_format) != fileTypes.end()) { - if (error) { - *error = CoreWrapperMakeError(CoreWrapperErrorUnsupportedFileType, - @"PDF is rendered by the web view, not by odrcore"); - } - return NO; - } - - std::string outputPathCpp = std::string([outputPath UTF8String]); - std::string cachePathCpp = std::string([cachePath UTF8String]); - - odr::DecodedFile file = odr::open(inputPathCpp); - if (file.password_encrypted()) { - std::string passwordCpp = password != nil ? std::string([password UTF8String]) : std::string(); - try { - file = file.decrypt(passwordCpp); - } catch (odr::WrongPasswordError &) { - if (error) { - *error = CoreWrapperMakeError(CoreWrapperErrorWrongPassword, - @"wrong password"); - } - return NO; - } - } - - if (!file.is_document_file()) { - if (error) { - *error = CoreWrapperMakeError(CoreWrapperErrorUnsupportedFileType, - @"not a document file"); - } - return NO; - } - - odr::DocumentFile documentFile = file.as_document_file(); - odr::DocumentType documentType = documentFile.document_type(); - _document = documentFile.document(); - - odr::HtmlService service = odr::html::translate(*_document, cachePathCpp, config); - - // the views are picked before they are rendered: bringing all of them - // offline first would write out every slide of a presentation only to - // throw the files away again - odr::HtmlViews views = CoreWrapperSelectViews(service.list_views(), documentType); - if (views.empty()) { - if (error) { - *error = CoreWrapperMakeError(CoreWrapperErrorUnknown, - @"odrcore produced no displayable page"); - } - return NO; - } - - NSMutableArray *pageNames = [[NSMutableArray alloc] init]; - NSMutableArray *pageURLs = [[NSMutableArray alloc] init]; - - if (overHttp) { - // a fresh prefix for every translation, because the web view - // caches by URL: re-translating after a password or an edit has - // to end up at an address it has not seen before - static std::atomic translation{0}; - std::string prefix = "odr" + std::to_string(++translation); - - // drops the service of the document shown before this one, whose - // pages nobody is going to ask for again. what it was translated - // into stays in the cache directory, which is the system's to - // empty rather than the server's - g_server->clear(); - g_server->connect_service(service, prefix); - - for (const odr::HtmlView &view : views) { - [pageNames addObject:[NSString stringWithUTF8String:view.name().c_str()]]; - [pageURLs addObject:CoreWrapperPageURL(prefix, view.path())]; - } - } else { - _html = service.bring_offline(outputPathCpp, views); - - for (const auto &page : _html->pages()) { - [pageNames addObject:[NSString stringWithUTF8String:page.name.c_str()]]; - [pageURLs addObject:[NSURL fileURLWithPath:[NSString stringWithUTF8String:page.path.c_str()]]]; - } - } - - _pageNames = pageNames; - _pageURLs = pageURLs; - - return YES; - } catch (odr::UnknownFileType &) { - if (error) { - *error = CoreWrapperMakeError(CoreWrapperErrorUnsupportedFileType, - @"unknown file type"); - } - return NO; - } catch (std::exception &e) { - if (error) { - *error = CoreWrapperMakeError(CoreWrapperErrorUnknown, - [NSString stringWithUTF8String:e.what()]); - } - return NO; - } catch (...) { - if (error) { - *error = CoreWrapperMakeError(CoreWrapperErrorUnknown, @"unknown failure in odrcore"); - } - return NO; - } - } -} - -+ (BOOL)isServedURL:(NSURL *)url { - std::uint32_t port = g_serverPort.load(); - - return port != 0 && [url.scheme isEqualToString:@"http"] - && [url.host isEqualToString:kCoreWrapperHttpHost] - && url.port.unsignedIntValue == port; -} - -- (BOOL)backTranslate:(NSString *)diff into:(NSString *)outputPath error:(NSError **)error { - @synchronized(self) { - if (!_document.has_value()) { - if (error) { - *error = CoreWrapperMakeError(CoreWrapperErrorUnknown, - @"no document has been translated yet"); - } - return NO; - } - - try { - odr::html::edit(*_document, [diff UTF8String]); - _document->save([outputPath UTF8String]); - - return YES; - } catch (std::exception &e) { - if (error) { - *error = CoreWrapperMakeError(CoreWrapperErrorUnknown, - [NSString stringWithUTF8String:e.what()]); - } - return NO; - } catch (...) { - if (error) { - *error = CoreWrapperMakeError(CoreWrapperErrorUnknown, @"unknown failure in odrcore"); - } - return NO; - } - } -} - -@end diff --git a/OpenDocumentReader/CoreWrapper.swift b/OpenDocumentReader/CoreWrapper.swift new file mode 100644 index 0000000..c09c357 --- /dev/null +++ b/OpenDocumentReader/CoreWrapper.swift @@ -0,0 +1,188 @@ +// +// CoreWrapper.swift +// OpenDocument Reader +// +// Replaces the ObjC++ CoreWrapper that talked to odrcore's C++ API directly. +// odrcore now arrives as the `OdrCore` Swift package, so the app links a +// prebuilt xcframework instead of building the library through conan. +// + +import Foundation +import OdrCore +import OdrCoreObjC + +let CoreWrapperErrorDomain = "app.opendocument.CoreWrapperErrorDomain" + +@objc enum CoreWrapperError: Int { + /// odrcore threw something we have no specific handling for. + case unknown = 1 + /// The document is encrypted and the supplied password did not open it. + case wrongPassword = 2 + /// Not a document odrcore can translate. PDFs land here on purpose. + case unsupportedFileType = 3 +} + +private func coreWrapperError(_ code: CoreWrapperError, _ description: String) -> NSError { + NSError( + domain: CoreWrapperErrorDomain, code: code.rawValue, + userInfo: [NSLocalizedDescriptionKey: description]) +} + +/// The one server the app has, brought up on first use and left running. +private final class PageServer { + static let shared = PageServer() + + private let lock = NSLock() + private var server: HttpServer? + private var handle: HttpServer.ServerHandle? + private var translation: UInt64 = 0 + + /// Bound port, or 0 when there is no server. + var port: UInt32 { lock.withLock { handle?.port ?? 0 } } + + /// Connects `service` under a prefix nothing has been served under before, + /// and returns the base URL for it. Nil if the socket could not be opened. + /// + /// A fresh prefix every time because the web view caches by URL: + /// re-translating after a password or an edit has to end up at an address + /// it has not seen. + func connect(_ service: HtmlService) -> (prefix: String, base: URL)? { + lock.lock() + defer { lock.unlock() } + + if server == nil { + let server = HttpServer() + guard let handle = try? server.serve() else { return nil } + self.server = server + self.handle = handle + } + guard let server, let handle else { return nil } + + translation += 1 + let prefix = "odr\(translation)" + + // drops the service of the document shown before this one, whose pages + // nobody is going to ask for again + try? server.clear() + guard (try? server.connect(service, prefix: prefix)) != nil else { return nil } + + return (prefix, handle.url(prefix: prefix)) + } +} + +/// The view odrcore names "document" holds the whole file in one page: every +/// slide of a presentation, every page of a PDF, the entire text document. +private func isCombinedView(_ view: HtmlView) -> Bool { view.name == "document" } + +/// Picks the views to show as pages, the same way OpenDocument.droid does. +/// +/// Spreadsheets get one tab per sheet, because scrolling through every sheet of +/// a workbook in one page is not how anyone reads a spreadsheet. Everything else +/// gets the combined view and nothing else — a presentation would otherwise show +/// its slides twice, and a PDF would list one tab per page next to the tab that +/// already has them all. +private func selectViews(_ views: [HtmlView], _ documentType: DocumentType) -> [HtmlView] { + let isSpreadsheet = documentType == .spreadsheet + let hasCombinedView = views.contains(where: isCombinedView) + + return views.filter { view in + isSpreadsheet ? !isCombinedView(view) : (!hasCombinedView || isCombinedView(view)) + } +} + +@objc final class CoreWrapper: NSObject { + @objc private(set) var pageNames: [String] = [] + @objc private(set) var pageURLs: [URL] = [] + + private var document: OdrCoreObjC.Document? + private let lock = NSRecursiveLock() + + @objc func translate( + _ inputPath: String, + cache cachePath: String, + into outputPath: String, + with password: String?, + editable: Bool + ) throws { + lock.lock() + defer { lock.unlock() } + + pageNames = [] + pageURLs = [] + + let fileTypes = (try? DecodedFile.listFileTypes(path: inputPath)) ?? [] + guard !fileTypes.isEmpty else { + throw coreWrapperError(.unsupportedFileType, "odrcore does not recognise this file type") + } + // PDFs are handed to WKWebView instead, which renders them natively + guard !fileTypes.contains(NSNumber(value: FileType.portableDocumentFormat.rawValue)) else { + throw coreWrapperError( + .unsupportedFileType, "PDF is rendered by the web view, not by odrcore") + } + + var file = try DecodedFile.decode(path: inputPath) + if file.isPasswordEncrypted { + do { + file = try file.decrypt(withPassword: password ?? "") + } catch let error as NSError + where error.code == ODRError.wrongPassword.rawValue + { + throw coreWrapperError(.wrongPassword, "wrong password") + } + } + + guard file.isDocumentFile else { + throw coreWrapperError(.unsupportedFileType, "not a document file") + } + + let documentFile = try file.asDocumentFile() + let documentType = documentFile.documentType + let document = try documentFile.document() + self.document = document + + 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 + + let service = try HtmlTranslator.translate( + document: document, cachePath: cachePath, config: config) + + // the views are picked before they are rendered: bringing all of them + // offline first would write out every slide of a presentation only to + // throw the files away again + let views = selectViews(service.views, documentType) + guard !views.isEmpty else { + throw coreWrapperError(.unknown, "odrcore produced no displayable page") + } + + guard let connected = PageServer.shared.connect(service) else { + throw coreWrapperError(.unknown, "could not serve the translated document") + } + + pageNames = views.map(\.name) + pageURLs = views.map { connected.base.appendingPathComponent($0.path) } + } + + @objc func backTranslate(_ diff: String, into outputPath: String) throws { + lock.lock() + defer { lock.unlock() } + + guard let document else { + throw coreWrapperError(.unknown, "no document has been translated yet") + } + + try HtmlTranslator.edit(document: document, diff: diff) + try document.save(to: outputPath) + } + + /// Whether this URL is one odrcore is serving, rather than somewhere a link + /// in the document leads. + @objc static func isServedURL(_ url: URL) -> Bool { + let port = PageServer.shared.port + + return port != 0 && url.scheme == "http" && url.host == "127.0.0.1" + && url.port.map(UInt32.init) == port + } +} From f47b51bda688401d7f9988923b94917c85507ac2 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sat, 1 Aug 2026 12:33:58 +0200 Subject: [PATCH 2/4] build: drop conan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nothing needs it any more: odrcore arrives as a prebuilt xcframework through SwiftPM, so the recipes, the profiles, the deployer, the generated xcconfig, the asset copy phase and the `conan-odr-index` submodule have no consumer left. Building the app is opening the project in Xcode. CI loses `.github/actions/setup-odrcore` with them — the ten steps it existed to share were all conan. What is left is selecting an Xcode version. Until odrcore cuts a release carrying the xcframework the package is referenced by path, so CI cannot resolve it and stays red; the README says what to build locally in the meantime. That ends when the reference becomes `.package(url:from:)`. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VYR3UqA1asTaTTNRm29csV --- .github/actions/setup-odrcore/action.yml | 100 ----------------------- .github/workflows/build_test.yml | 17 +--- .github/workflows/format.yml | 4 +- .github/workflows/release.yml | 8 +- .gitignore | 2 - .gitmodules | 3 - README.md | 39 ++++----- conan-odr-index | 1 - conan/conandeployer.py | 52 ------------ conan/conanfile.py | 23 ------ conan/copy-resources.sh | 9 -- conan/profiles/ios | 5 -- conan/profiles/ios-base | 21 ----- conan/profiles/ios-simulator-arm64 | 5 -- conan/profiles/ios-simulator-x64 | 5 -- conan/setup-all.sh | 53 ------------ configs/conan.xcconfig | 4 - 17 files changed, 22 insertions(+), 329 deletions(-) delete mode 100644 .github/actions/setup-odrcore/action.yml delete mode 100644 .gitmodules delete mode 160000 conan-odr-index delete mode 100644 conan/conandeployer.py delete mode 100644 conan/conanfile.py delete mode 100755 conan/copy-resources.sh delete mode 100644 conan/profiles/ios delete mode 100644 conan/profiles/ios-base delete mode 100644 conan/profiles/ios-simulator-arm64 delete mode 100644 conan/profiles/ios-simulator-x64 delete mode 100755 conan/setup-all.sh delete mode 100644 configs/conan.xcconfig diff --git a/.github/actions/setup-odrcore/action.yml b/.github/actions/setup-odrcore/action.yml deleted file mode 100644 index 9cb0544..0000000 --- a/.github/actions/setup-odrcore/action.yml +++ /dev/null @@ -1,100 +0,0 @@ -name: setup odrcore -description: > - Installs the toolchain and resolves odrcore and its dependencies with conan, - leaving the generated xcconfigs in conan-output and the runtime assets in - conan-assets. Every job that compiles the app needs the exact same ten steps, - so they live here instead of being copied into each workflow. - -inputs: - xcode-version: - description: Xcode version to select on the runner - required: true - profile: - description: conan host profile under conan/profiles - required: true - configuration: - description: Xcode configuration to resolve the dependencies for - required: true - cache-flavor: - description: > - Cache namespace. The simulator and the device slice resolve a different - dependency set, so they must not overwrite each other's cache entry. - required: true - save-cache: - description: > - Whether this job may write the conan cache back. Jobs that only consume - what another job built restore without saving. - required: false - default: "true" - -outputs: - conan-cache-key: - description: Cache key of the conan package cache, to restore it elsewhere - value: ${{ steps.conan-cache-key.outputs.key }} - -runs: - using: composite - steps: - - name: checkout conan-odr-index - shell: bash - run: git submodule update --init --depth 1 conan-odr-index - - # 3.12+ is required by the conan-odr-index helper scripts - - uses: actions/setup-python@v7 - with: - python-version: "3.12" - - - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: ${{ inputs.xcode-version }} - - # conan itself is pinned by conan-odr-index so recipes and client stay in sync - - name: install conan - shell: bash - run: pip3 install -r conan-odr-index/requirements.txt - - - name: conan profile - shell: bash - run: conan profile detect - - - name: conan cache key - id: conan-cache-key - shell: bash - run: echo "key=conan2-${{ runner.os }}-xcode${{ inputs.xcode-version }}-${{ inputs.cache-flavor }}-index$(git rev-parse HEAD:conan-odr-index)-${{ hashFiles('conan/conanfile.py', 'conan/profiles/*') }}" >> "$GITHUB_OUTPUT" - - # restore before exporting the recipes: the export must run on top of the - # restored cache so current recipes win over stale ones from the archive - - name: conan cache - if: inputs.save-cache == 'true' - uses: actions/cache@v6 - with: - path: ~/.conan2/p - key: ${{ steps.conan-cache-key.outputs.key }} - restore-keys: | - conan2-${{ runner.os }}-xcode${{ inputs.xcode-version }}-${{ inputs.cache-flavor }}- - - - name: restore conan cache - if: inputs.save-cache != 'true' - uses: actions/cache/restore@v6 - with: - path: ~/.conan2/p - key: ${{ steps.conan-cache-key.outputs.key }} - restore-keys: | - conan2-${{ runner.os }}-xcode${{ inputs.xcode-version }}-${{ inputs.cache-flavor }}- - - # recipes are exported from the submodule into the local cache, which is - # why no private remote has to be configured - - name: export conan-odr-index - shell: bash - run: python3 conan-odr-index/scripts/conan_export_all_packages.py - - - name: conan install - shell: bash - run: > - conan install conan/ - --output-folder=conan-output - --build=missing - --profile:host=conan/profiles/${{ inputs.profile }} - --deployer=conan/conandeployer.py - --deployer-folder=conan-assets - -o "configuration=${{ inputs.configuration }}" diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml index a70080c..c83a90f 100644 --- a/.github/workflows/build_test.yml +++ b/.github/workflows/build_test.yml @@ -38,20 +38,13 @@ jobs: with: bundler-cache: true - - uses: ./.github/actions/setup-odrcore + - uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: ${{ env.xcode_version }} - profile: ios-simulator-arm64 - configuration: Debug - cache-flavor: simulator - name: run tests run: bundle exec fastlane tests - # drop temporary download/source/build folders so only built packages get cached - - name: clean conan cache - run: conan cache clean "*" - - uses: actions/upload-artifact@v7 if: always() with: @@ -73,12 +66,9 @@ jobs: - name: checkout uses: actions/checkout@v7 - - uses: ./.github/actions/setup-odrcore + - uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: ${{ env.xcode_version }} - profile: ios - configuration: ${{ matrix.configuration }} - cache-flavor: device # signing needs secrets this workflow does not have, so this only proves # that the device slice compiles and links @@ -92,6 +82,3 @@ jobs: -destination 'generic/platform=iOS' CODE_SIGNING_ALLOWED=NO build - - - name: clean conan cache - run: conan cache clean "*" diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index f93eb21..28e87a2 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -1,7 +1,7 @@ name: format -# swift-format comes with the Xcode toolchain, so this needs neither conan nor -# ruby and reports style breakage in a minute instead of after a full build. +# swift-format comes with the Xcode toolchain, so this needs no ruby and reports +# style breakage in a minute instead of after a full build. # Unlike build_test this has no paths-ignore: every file the formatter touches # gets checked. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 85baca9..7342f43 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -57,15 +57,9 @@ jobs: with: bundler-cache: true - # a release consumes what build_test already built and never writes the - # cache back, so an untested release cannot poison it - - uses: ./.github/actions/setup-odrcore + - uses: maxim-lobanov/setup-xcode@v1 with: xcode-version: ${{ env.xcode_version }} - profile: ios - configuration: ${{ matrix.flavor == 'pro' && 'Release' || 'Release Lite' }} - cache-flavor: device - save-cache: "false" # a throwaway keychain so the distribution certificate never outlives the job - name: import signing certificate diff --git a/.gitignore b/.gitignore index cd06563..5d336b6 100644 --- a/.gitignore +++ b/.gitignore @@ -64,7 +64,5 @@ fastlane/test_output fastlane/report.xml -conan-output/ -conan-assets/ graph_info.json .venv/ diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 97d2b5b..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "conan-odr-index"] - path = conan-odr-index - url = https://github.com/opendocument-app/conan-odr-index.git diff --git a/README.md b/README.md index 9bb8984..b89c753 100644 --- a/README.md +++ b/README.md @@ -5,26 +5,23 @@ This is an iOS frontend for our C++ [OpenDocument.core](https://github.com/opend ## Setup -Our C++ dependencies come from [conan-odr-index](https://github.com/opendocument-app/conan-odr-index), -which is checked out as a submodule and exported into the local conan cache. No -private conan remote is involved. - -The helper scripts in that submodule need python 3.12 or newer — they use PEP -701 f-strings, which are a syntax error on 3.11. - -1. `git submodule update --init --depth 1 conan-odr-index` -2. `python3.12 -m venv .venv && source .venv/bin/activate` -3. install conan into that venv: `pip install -r conan-odr-index/requirements.txt` -4. `conan profile detect` -5. `conan/setup-all.sh` — exports the recipes and generates the xcconfigs for - every configuration and architecture. The first run builds odrcore and its - dependencies from source and takes a while. -6. open `OpenDocumentReader.xcodeproj` in Xcode - -Everything else comes from Swift Package Manager and is resolved by Xcode. +Open `OpenDocumentReader.xcodeproj` in Xcode. Everything comes from Swift +Package Manager and is resolved by Xcode — odrcore included, as the prebuilt +`OdrCoreObjC.xcframework` of the +[OdrCore](https://github.com/opendocument-app/OpenDocument.core) package. There +is no conan step and no C++ toolchain to set up. + +While odrcore has no release carrying the xcframework, the package is +referenced by path and needs one built next to this checkout: + +```sh +cd ../OpenDocument.core +apple/build_xcframework.py slice && apple/build_xcframework.py assemble +``` -`conan/setup-all.sh` has to be re-run whenever the odrcore version in -`conan/conanfile.py` or the `conan-odr-index` submodule changes. +and every `xcodebuild` invocation needs `ODR_XCFRAMEWORK=OdrCoreObjC.xcframework` +in its environment. That goes away once the reference becomes +`.package(url:from:)`. ## How a document reaches the screen @@ -70,9 +67,7 @@ committing; CI runs `scripts/format.sh --check` and fails on any difference. | `release` | manual upload to App Store Connect, see below | `format` needs nothing but the Xcode toolchain and reports style breakage in a -minute, so it is kept apart from the native build. Everything that does need -odrcore shares `.github/actions/setup-odrcore`, which resolves the C++ -dependencies with conan and caches them per Xcode version and profile. +minute, so it is kept apart from the build. ## Releasing diff --git a/conan-odr-index b/conan-odr-index deleted file mode 160000 index 36caf08..0000000 --- a/conan-odr-index +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 36caf08e2de1327132c7b19b57ba33b3be66264d diff --git a/conan/conandeployer.py b/conan/conandeployer.py deleted file mode 100644 index d000f5a..0000000 --- a/conan/conandeployer.py +++ /dev/null @@ -1,52 +0,0 @@ -import shutil -from pathlib import Path - - -def deploy(graph, output_folder: str, **kwargs): - project_folder = Path(__file__).parent.parent - output_folder = Path(output_folder).resolve() - - conanfile = graph.root.conanfile - conanfile.output.info(f"Custom deployer to {output_folder}") - - symlinks = conanfile.conf.get( - "tools.deployer:symlinks", check_type=bool, default=True - ) - arch = conanfile.settings.get_safe("arch") - - conanfile.output.info(f"Symlinks: {symlinks}") - conanfile.output.info(f"Arch: {arch}") - - deps = {dep.ref.name: dep for dep in conanfile.dependencies.values()} - - print(f"Dependencies: {list(deps.keys())}") - - copytree_kwargs = {"symlinks": symlinks, "dirs_exist_ok": True} - - conan_files = output_folder / "files" - - if "odrcore" in deps: - dep = deps["odrcore"] - conanfile.output.info(f"Deploying odrcore to {conan_files}") - shutil.copytree( - f"{dep.package_folder}/share", - f"{conan_files}/odrcore", - **copytree_kwargs, - ) - - # odrcore 6 dropped the pdf2htmlEX and wvWare backends, so pdf2htmlex, - # poppler-data and fontconfig are no longer in the graph and nothing but - # odrcore's own data is left to deploy - - with ( - open(f"{output_folder}/input-files.xcfilelist", "w") as f_in, - open(f"{output_folder}/output-files.xcfilelist", "w") as f_out, - ): - for file in Path(conan_files).glob("**/*"): - file = file.relative_to(conan_files) - if file.suffix == ".xcfilelist": - continue - f_in.write(f"$(PROJECT_DIR)/{conan_files.relative_to(project_folder)}/{file}\n") - f_out.write( - f"${{TARGET_BUILD_DIR}}/${{UNLOCALIZED_RESOURCES_FOLDER_PATH}}/{file}\n" - ) diff --git a/conan/conanfile.py b/conan/conanfile.py deleted file mode 100644 index 92563e4..0000000 --- a/conan/conanfile.py +++ /dev/null @@ -1,23 +0,0 @@ -from conan import ConanFile -from conan.tools.apple import XcodeDeps, XcodeToolchain - -class Pkg(ConanFile): - settings = "os", "compiler", "arch", "build_type" - options = {"configuration": ["Debug", "Debug Lite", "Release", "Release Lite"]} - default_options = { - "configuration": "Debug", - "odrcore/*:shared": False, - "odrcore/*:with_libmagic": False, - "odrcore/*:with_http_server": True, - "odrcore/*:with_cli": False, - } - requires = "odrcore/6.0.0" - - def generate(self): - xcode = XcodeDeps(self) - xcode.configuration = self.options.configuration - xcode.generate() - - tc = XcodeToolchain(self) - tc.configuration = self.options.configuration - tc.generate() diff --git a/conan/copy-resources.sh b/conan/copy-resources.sh deleted file mode 100755 index 60a8e3c..0000000 --- a/conan/copy-resources.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh - -set -e -set -u -set -o pipefail - -mkdir -p "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}" - -rsync -av "${PROJECT_DIR}/conan-assets/files/" "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/" diff --git a/conan/profiles/ios b/conan/profiles/ios deleted file mode 100644 index ab40f92..0000000 --- a/conan/profiles/ios +++ /dev/null @@ -1,5 +0,0 @@ -include(ios-base) - -[settings] -os.sdk=iphoneos -arch=armv8 diff --git a/conan/profiles/ios-base b/conan/profiles/ios-base deleted file mode 100644 index e8ab16c..0000000 --- a/conan/profiles/ios-base +++ /dev/null @@ -1,21 +0,0 @@ -include(default) - -[settings] -os=iOS -os.version=15.0 - -compiler.cppstd=20 - -# dependencies are plain release builds, odrcore keeps debug info so crash -# reports from the field stay symbolicatable -build_type=Release -odrcore/*:build_type=RelWithDebInfo - -[conf] -# with the http server enabled, cpp-httplib resolves host names through CFHost -# on Apple platforms. odrcore's CMake links neither CoreFoundation nor CFNetwork -# (where CFHost lives), so its `cli/server` tool fails to link - the library -# itself builds fine, and the app links both frameworks itself. Only executables -# need this, hence exelinkflags. (`ODR_CLI=OFF` would be the tidier fix but -# odrcore 5.5.0 installs the cli targets whether or not it built them.) -odrcore/*:tools.build:exelinkflags=["-framework", "CoreFoundation", "-framework", "CFNetwork"] diff --git a/conan/profiles/ios-simulator-arm64 b/conan/profiles/ios-simulator-arm64 deleted file mode 100644 index 02ec81b..0000000 --- a/conan/profiles/ios-simulator-arm64 +++ /dev/null @@ -1,5 +0,0 @@ -include(ios-base) - -[settings] -os.sdk=iphonesimulator -arch=armv8 diff --git a/conan/profiles/ios-simulator-x64 b/conan/profiles/ios-simulator-x64 deleted file mode 100644 index d3ebbc8..0000000 --- a/conan/profiles/ios-simulator-x64 +++ /dev/null @@ -1,5 +0,0 @@ -include(ios-base) - -[settings] -os.sdk=iphonesimulator -arch=x86_64 diff --git a/conan/setup-all.sh b/conan/setup-all.sh deleted file mode 100755 index 31101b0..0000000 --- a/conan/setup-all.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -# https://stackoverflow.com/a/246128/198996 -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" -cd "$DIR/.." - -CONFIGURATIONS=("Debug" "Debug Lite" "Release" "Release Lite") - -if [ ! -f conan-odr-index/scripts/conan_export_all_packages.py ]; then - echo "conan-odr-index submodule is missing. Run:" >&2 - echo " git submodule update --init --depth 1 conan-odr-index" >&2 - exit 1 -fi - -# our recipes live in the conan-odr-index submodule and get exported into the -# local cache, so no private remote is involved -python3 conan-odr-index/scripts/conan_export_all_packages.py - -# XcodeDeps appends to the per-package include lists rather than rewriting them, -# so leftovers from earlier runs keep shadowing freshly generated files -rm -rf conan-output conan-assets - -install() { - local profile="$1" - local configuration="$2" - shift 2 - - conan install conan/ \ - --output-folder=conan-output \ - --build=missing \ - --profile:host="conan/profiles/${profile}" \ - -o "configuration=${configuration}" \ - "$@" -} - -# assets, deployed into the app bundle as odrcore's runtime data -install ios "Release" \ - --deployer=conan/conandeployer.py \ - --deployer-folder=conan-assets - -# device -for configuration in "${CONFIGURATIONS[@]}"; do - install ios "${configuration}" -done - -# simulator -for arch in "arm64" "x64"; do - for configuration in "${CONFIGURATIONS[@]}"; do - install "ios-simulator-${arch}" "${configuration}" - done -done diff --git a/configs/conan.xcconfig b/configs/conan.xcconfig deleted file mode 100644 index 8096f64..0000000 --- a/configs/conan.xcconfig +++ /dev/null @@ -1,4 +0,0 @@ -// generated by conan/setup-all.sh -// this used to sit behind four per-configuration files whose only other job was -// to include the matching Pods xcconfig and undo its LIBRARY_SEARCH_PATHS -#include "conan-output/conan_config.xcconfig" From 404a65639049b554357032e44f1f3bf0ebb2c0e8 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sat, 1 Aug 2026 12:53:21 +0200 Subject: [PATCH 3/4] docs: say plainly that the build jobs wait for the odrcore release Compiling odrcore in this repository's CI to bridge the gap would mean cloning and building it on every pull request, for a state that ends with the next release. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VYR3UqA1asTaTTNRm29csV --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b89c753..7136ef6 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,9 @@ apple/build_xcframework.py slice && apple/build_xcframework.py assemble ``` and every `xcodebuild` invocation needs `ODR_XCFRAMEWORK=OdrCoreObjC.xcframework` -in its environment. That goes away once the reference becomes +in its environment. CI does not do this and the build jobs stay red until then — +compiling odrcore on every pull request to bridge a gap that closes with the +next release is not worth it. Both end when the reference becomes `.package(url:from:)`. ## How a document reaches the screen From c3093c6c7e02e5f3eb72bc509a89823694d5c911 Mon Sep 17 00:00:00 2001 From: Andreas Stefl Date: Sun, 2 Aug 2026 15:53:05 +0200 Subject: [PATCH 4/4] build: take odrcore from its 6.2.0 release odrcore 6.2.0 ships `OdrCoreObjC.xcframework.zip`, so the package reference is a versioned `.package(url:from:)` and nothing local is needed any more: SwiftPM downloads the artifact and CI resolves it like any other dependency. This is what the previous three commits said they were waiting for, and it turns the build jobs green. The local checkout and `ODR_XCFRAMEWORK` stay documented as the way to try an unreleased odrcore, which is what odrcore's `Package.swift` keeps them for. The simulator tests pass 27/27 and the device slice builds in Release against the downloaded xcframework. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01PFrL3ExWSVfLM3VCYGjfAs --- OpenDocumentReader.xcodeproj/project.pbxproj | 18 ++++++++++-------- .../xcshareddata/swiftpm/Package.resolved | 11 ++++++++++- README.md | 18 ++++++++---------- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/OpenDocumentReader.xcodeproj/project.pbxproj b/OpenDocumentReader.xcodeproj/project.pbxproj index cfedad9..5b4e585 100644 --- a/OpenDocumentReader.xcodeproj/project.pbxproj +++ b/OpenDocumentReader.xcodeproj/project.pbxproj @@ -349,7 +349,7 @@ mainGroup = E2F7ED46220B54D600D63515; packageReferences = ( AD584FCD41577C8CDEE974AA /* XCRemoteSwiftPackageReference "swift-package-manager-google-mobile-ads" */, - 78200B97C10509E2539B4FE5 /* XCLocalSwiftPackageReference "OpenDocument.core" */, + 78200B97C10509E2539B4FE5 /* XCRemoteSwiftPackageReference "OpenDocument.core" */, ); productRefGroup = E2F7ED46220B54D600D63515; projectDirPath = ""; @@ -963,14 +963,15 @@ }; /* End XCConfigurationList section */ -/* Begin XCLocalSwiftPackageReference section */ - 78200B97C10509E2539B4FE5 /* XCLocalSwiftPackageReference "OpenDocument.core" */ = { - isa = XCLocalSwiftPackageReference; - relativePath = ../OpenDocument.core; - }; -/* End XCLocalSwiftPackageReference section */ - /* Begin XCRemoteSwiftPackageReference section */ + 78200B97C10509E2539B4FE5 /* XCRemoteSwiftPackageReference "OpenDocument.core" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/opendocument-app/OpenDocument.core.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 6.2.0; + }; + }; AD584FCD41577C8CDEE974AA /* XCRemoteSwiftPackageReference "swift-package-manager-google-mobile-ads" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/googleads/swift-package-manager-google-mobile-ads.git"; @@ -989,6 +990,7 @@ }; E381918B979121CD165C966A /* OdrCore */ = { isa = XCSwiftPackageProductDependency; + package = 78200B97C10509E2539B4FE5 /* XCRemoteSwiftPackageReference "OpenDocument.core" */; productName = OdrCore; }; /* End XCSwiftPackageProductDependency section */ diff --git a/OpenDocumentReader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/OpenDocumentReader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index e6b95c3..121caff 100644 --- a/OpenDocumentReader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/OpenDocumentReader.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,15 @@ { - "originHash" : "3b9752ff554d5fdc91a7c01c2322a5d851b0979c36f02eb55188d9ab9e3d7e77", + "originHash" : "fb74b10a80d07407e1cef4ff366c543de15a310f0a4ae23e7fdda72de7a12f43", "pins" : [ + { + "identity" : "opendocument.core", + "kind" : "remoteSourceControl", + "location" : "https://github.com/opendocument-app/OpenDocument.core.git", + "state" : { + "revision" : "72661abacff00bbcafbc142a3655f1bd075e95df", + "version" : "6.2.0" + } + }, { "identity" : "swift-package-manager-google-mobile-ads", "kind" : "remoteSourceControl", diff --git a/README.md b/README.md index 7136ef6..d937a15 100644 --- a/README.md +++ b/README.md @@ -7,23 +7,21 @@ This is an iOS frontend for our C++ [OpenDocument.core](https://github.com/opend Open `OpenDocumentReader.xcodeproj` in Xcode. Everything comes from Swift Package Manager and is resolved by Xcode — odrcore included, as the prebuilt -`OdrCoreObjC.xcframework` of the -[OdrCore](https://github.com/opendocument-app/OpenDocument.core) package. There -is no conan step and no C++ toolchain to set up. +`OdrCoreObjC.xcframework` the +[OdrCore](https://github.com/opendocument-app/OpenDocument.core) package +downloads from its release. There is no conan step and no C++ toolchain to set +up. -While odrcore has no release carrying the xcframework, the package is -referenced by path and needs one built next to this checkout: +To try an unreleased odrcore, point the package reference at a local checkout +and build the xcframework there: ```sh cd ../OpenDocument.core apple/build_xcframework.py slice && apple/build_xcframework.py assemble ``` -and every `xcodebuild` invocation needs `ODR_XCFRAMEWORK=OdrCoreObjC.xcframework` -in its environment. CI does not do this and the build jobs stay red until then — -compiling odrcore on every pull request to bridge a gap that closes with the -next release is not worth it. Both end when the reference becomes -`.package(url:from:)`. +Its `Package.swift` then takes `ODR_XCFRAMEWORK=OdrCoreObjC.xcframework` from the +environment of every `xcodebuild` invocation instead of the release artifact. ## How a document reaches the screen