diff --git a/.gitignore b/.gitignore index 82a80da24..8a170f6d3 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ out/ bin/ lib/ *.dir/ +*.dmg +*.ipa # CMake (generated). Tracked helper modules live under cmake/. CMakeCache.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index f2c5a082a..1062a531a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.9 FATAL_ERROR) +cmake_minimum_required(VERSION 3.10...3.28 FATAL_ERROR) set(target_name scantailor-advanced) @@ -72,6 +72,19 @@ endif() st_set_default_build_type(Release) +#=================================== Dependencies ===================================# + +# Detect iOS cross-compile — must happen before any find_package calls +if(CMAKE_SYSTEM_NAME STREQUAL "iOS") + set(IOS TRUE) +endif() + +# iOS: disable tests and portable version — not applicable on mobile +if(IOS) + set(BUILD_TESTS OFF CACHE BOOL "" FORCE) + set(PORTABLE_VERSION OFF CACHE BOOL "" FORCE) +endif() + # Must be set before Boost (Flatpak and other chroot builds use -DBUILD_TESTS=OFF; issue #61). option(BUILD_TESTS "Build unit tests (math_tests, imageproc_tests, core_tests, foundation_tests, qt_tests)" ON) @@ -86,7 +99,11 @@ if (NOT CMAKE_INSTALL_BINDIR) endif() endif() -#=================================== Dependencies ===================================# +if (APPLE) + if (NOT CMAKE_INSTALL_APPDIR) + set(CMAKE_INSTALL_APPDIR "Applications") + endif() +endif() if (WIN32) macro(real_path_find_glob Var Glob) @@ -134,15 +151,16 @@ endif() #=================================== JPEG ===================================# -find_package(JPEG REQUIRED) - -if (WIN32) - add_dynamic_library_locations(JPEG::JPEG "libjpeg-62.dll") +if(NOT IOS) + find_package(JPEG REQUIRED) + if (WIN32) + add_dynamic_library_locations(JPEG::JPEG "libjpeg-62.dll") + endif() endif() #=================================== ZLIB ===================================# -find_package(ZLIB REQUIRED) +find_package(ZLIB REQUIRED) # available in iOS SDK, safe to find everywhere if (WIN32) add_dynamic_library_locations(ZLIB::ZLIB) @@ -150,27 +168,27 @@ endif() #================================== PNG ==================================# -find_package(PNG REQUIRED) - -if (WIN32) - add_dynamic_library_locations(PNG::PNG "libpng16.dll") +if(NOT IOS) + find_package(PNG REQUIRED) + if (WIN32) + add_dynamic_library_locations(PNG::PNG "libpng16.dll") + endif() endif() #=================================== TIFF ===================================# -find_package(TIFF REQUIRED) - -if (WIN32) - add_dynamic_library_locations(TIFF::TIFF) - set_target_properties( - TIFF::TIFF PROPERTIES - INTERFACE_COMPILE_DEFINITIONS USE_LIBTIFF_DLL) +if(NOT IOS) + find_package(TIFF REQUIRED) + if (WIN32) + add_dynamic_library_locations(TIFF::TIFF) + set_target_properties( + TIFF::TIFF PROPERTIES + INTERFACE_COMPILE_DEFINITIONS USE_LIBTIFF_DLL) + endif() endif() #================================= Boost ================================# -#set(Boost_NO_WARN_NEW_VERSIONS ON) - if (WIN32) if (MINGW) if (CMAKE_SIZEOF_VOID_P EQUAL 8) @@ -180,7 +198,6 @@ if (WIN32) endif() set(Boost_ARCHITECTURE ${_boost_architecture} CACHE STRING "") - #set(Boost_DEBUG ON) string(REGEX MATCHALL "[0-9]+" _boost_compiler_version ${CMAKE_CXX_COMPILER_VERSION}) list(GET _boost_compiler_version 0 _boost_compiler_version_major) list(GET _boost_compiler_version 1 _boost_compiler_version_minor) @@ -219,17 +236,26 @@ endif() set(qt_min_version 5.9) -find_package( - Qt6 - COMPONENTS Core Gui Widgets Xml Network LinguistTools OpenGL Svg OpenGLWidgets Test - CONFIG) -if (Qt6_FOUND) - set(QT_BINDIR "${QT6_INSTALL_PREFIX}/bin") +if(IOS) + # iOS: find only the modules that are compatible and actually installed + find_package( + Qt6 + COMPONENTS Core Gui Widgets Xml Network LinguistTools OpenGL Svg OpenGLWidgets Test + CONFIG REQUIRED) + set(Qt6_FOUND TRUE) else() - find_package(Qt5 ${qt_min_version} - COMPONENTS Core Gui Widgets Xml Network LinguistTools OpenGL Svg Test - CONFIG REQUIRED) - set(QT_BINDIR "${QT5_INSTALL_PREFIX}/bin") + find_package( + Qt6 + COMPONENTS Core Gui Widgets Xml Network LinguistTools OpenGL Svg OpenGLWidgets Test + CONFIG) + if (Qt6_FOUND) + set(QT_BINDIR "${QT6_INSTALL_PREFIX}/bin") + else() + find_package(Qt5 ${qt_min_version} + COMPONENTS Core Gui Widgets Xml Network LinguistTools OpenGL Svg Test + CONFIG REQUIRED) + set(QT_BINDIR "${QT5_INSTALL_PREFIX}/bin") + endif() endif() if(Qt6_FOUND) @@ -253,7 +279,6 @@ else() set(Qt_Test_lib Qt5::Test) endif() - #=================================== Threads ===================================# if (UNIX) @@ -332,10 +357,12 @@ string(REGEX REPLACE "[0-9]+\\.[0-9]+\\.([0-9]+)" "\\1" APPLICATION_VERSION_PATC set(SCANTAILOR_RESOURCES_DIR "${CMAKE_SOURCE_DIR}/src/resources") set(SCANTAILOR_TRANSLATIONS_DIR "${CMAKE_SOURCE_DIR}/src/translations") -configure_file( - "${SCANTAILOR_RESOURCES_DIR}/unix/${target_name}.desktop.in" - "${CMAKE_BINARY_DIR}/${target_name}.desktop" - @ONLY) +if(NOT IOS) + configure_file( + "${SCANTAILOR_RESOURCES_DIR}/unix/${target_name}.desktop.in" + "${CMAKE_BINARY_DIR}/${target_name}.desktop" + @ONLY) +endif() add_subdirectory(src) @@ -351,6 +378,7 @@ if(Qt6_FOUND) else() Qt5_add_translation(qm_files ${translation_files}) endif() + add_custom_target(compile_translations ALL DEPENDS ${qm_files}) if (WIN32) install(FILES ${qm_files} DESTINATION "${CMAKE_INSTALL_BINDIR}/translations") @@ -358,7 +386,66 @@ else() install(FILES ${qm_files} DESTINATION "share/${APPLICATION_NAME}/translations") endif() -# Install extra files. +#=================================== iOS target properties ===================================# + +if(IOS) + # Bundle identifier: pass -DBUNDLE_ID=com.yourname.scantailor or + # let the build scripts set it automatically from $USER. + if(NOT BUNDLE_ID) + set(BUNDLE_ID "com.$ENV{USER}.scantailor") + endif() + + set_target_properties(${target_name} PROPERTIES + MACOSX_BUNDLE TRUE + MACOSX_BUNDLE_GUI_IDENTIFIER "${BUNDLE_ID}" + MACOSX_BUNDLE_BUNDLE_NAME "ScanTailor Advanced" + MACOSX_BUNDLE_BUNDLE_VERSION "${VERSION}" + MACOSX_BUNDLE_SHORT_VERSION_STRING "${VERSION}" + MACOSX_BUNDLE_INFO_PLIST "${CMAKE_SOURCE_DIR}/ios/Info.plist" + + # No real signing — app is fakesigned after build with ldid. + # Pass -DDEVELOPMENT_TEAM=XXXXXXXXXX to enable real signing. + XCODE_ATTRIBUTE_CODE_SIGN_STYLE "Manual" + XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "${DEVELOPMENT_TEAM}" + XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "" + XCODE_ATTRIBUTE_IPHONEOS_DEPLOYMENT_TARGET "18.0" + XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2" + XCODE_ATTRIBUTE_ENABLE_BITCODE "NO" + XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME "AppIcon" + ) + + # Asset catalog must be added as a source so Xcode's actool compiles the icons. + set(APPICONSET "${CMAKE_SOURCE_DIR}/build-ios/AppIcon.appiconset") + target_sources(${target_name} PRIVATE "${APPICONSET}") + set_source_files_properties("${APPICONSET}" + PROPERTIES MACOSX_PACKAGE_LOCATION Resources) + + # Qt's bundled JPEG/PNG/ZLIB/Freetype are compiled into Qt6::Gui and Qt6::Core + # on iOS — link the Qt modules directly, no separate bundled targets needed. + target_link_libraries(${target_name} PRIVATE + Qt6::Core + Qt6::Gui + Qt6::Widgets + Qt6::Svg + Qt6::OpenGL + Qt6::OpenGLWidgets + Qt6::Network + Qt6::Xml + ) + + # Import required static plugins — mandatory for iOS + qt6_import_plugins(${target_name} + INCLUDE + Qt6::QIOSIntegrationPlugin + Qt6::QJpegPlugin + Qt6::QSvgPlugin + Qt6::QSvgIconPlugin + Qt6::QMinimalIntegrationPlugin + ) +endif() + +#=================================== Install (desktop only) ===================================# + if (WIN32) macro (add_runtime_libs_to_install Configuration Libs) set(configurations "${Configuration}") @@ -460,7 +547,7 @@ if (WIN32) install(PROGRAMS ${ADDITIONAL_RUNTIME_LIBS_DEBUG} TYPE BIN CONFIGURATIONS Debug) endif() -if (UNIX) +if (UNIX AND NOT IOS) install(FILES "${CMAKE_BINARY_DIR}/${target_name}.desktop" DESTINATION "share/applications") install(FILES "${SCANTAILOR_RESOURCES_DIR}/appicon.svg" diff --git a/README.md b/README.md index bd0fd774e..57ef857fa 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,11 @@ If the video doesn't play, you may to have to login into your vimeo account. ## Contents: * [Description](#description) * [Building](#building) + * [Linux (Ubuntu / Debian)](#building-on-linux-ubuntu--debian) + * [macOS](#building-on-macos) + * [iOS](#building-for-ios) + * [Windows (cross-compile from Linux)](#building-a-windows-exe-from-linux-cross-compile) + * [Windows (native)](#building-on-windows-native) * [About this fork](#about-this-fork) * [Features](#features) * [**ScanTailor Enhanced**](#scantailor-enhanced) @@ -496,3 +501,129 @@ This produces `build-win-static/scantailor.exe`. Use `./build-windows.sh shared` #### Building on Windows (native) Go to [this repository](https://github.com/4lex4/scantailor-libs-build) and follow the instructions given there. + +#### Building on macOS + +ScanTailor Advanced builds as a native macOS app (`ScanTailor Advanced.app`) using Qt6 and CMake. The build script handles configuration, compilation, Qt framework bundling, ad-hoc code signing, and DMG creation automatically. + +**Requirements:** + +- macOS 12 or later (Apple Silicon or Intel) +- Xcode Command Line Tools +- Homebrew +- Qt 6.x for macOS (installed via `aqt` to `~/Qt/`) + +**Install Xcode Command Line Tools and Homebrew:** + +```bash +xcode-select --install + +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +``` + +**Install Homebrew dependencies:** + +```bash +brew install cmake jpeg libpng libtiff boost + +# Optionally install create-dmg for a nicer DMG layout +brew install create-dmg +``` + +**Install Qt 6 for macOS using aqt:** + +[`aqt`](https://github.com/miurahr/aqtinstall) (Another Qt installer) installs Qt without requiring a Qt account and places it at `~/Qt/`, which the build script locates automatically. + +```bash +pip install aqtinstall + +# Install Qt 6.12.0 for macOS (replace 6.12.0 with your preferred version) +aqt install-qt mac desktop 6.12.0 clang_64 \ + --outputdir ~/Qt \ + --modules qtsvg qtimageformats qtopengl qtnetwork qtxml +``` + +The build script will find Qt automatically under `~/Qt/`. If you install to a different location, set `QT_MACOS_DIR` as shown in the notes below. + +**Build:** + +```bash +git clone https://github.com/ScanTailor-Advanced/scantailor-advanced.git +cd scantailor-advanced +./build-macos.sh +``` + +This produces `scantailor-advanced__macos.dmg` in the project root. Open the DMG, drag `ScanTailor Advanced.app` to your Applications folder, and if macOS Gatekeeper blocks it on first launch run: + +```bash +xattr -cr "/Applications/ScanTailor Advanced.app" +``` + +**Notes:** +- The app is ad-hoc signed (no Apple Developer account required) and runs on your own Mac without issues. +- To use a custom Qt path: `QT_MACOS_DIR=/path/to/Qt/6.x.x/macos ./build-macos.sh` + +--- + +#### Building for iOS + +ScanTailor Advanced can be built for iPad and iPhone using Qt6 for iOS and Xcode. The result is an IPA that can be sideloaded onto a device using fakesigning (no Apple Developer account required). + +**Requirements:** + +- macOS with full Xcode installed (not just Command Line Tools) — download from the Mac App Store or [developer.apple.com](https://developer.apple.com/xcode/) +- Homebrew +- Qt 6.x for both **iOS** and **macOS** installed via `aqt` to `~/Qt/` — both targets must be the same version, as the macOS installation provides the host build tools required when cross-compiling for iOS +- `ldid` for fakesigning + +**Install Homebrew dependencies:** + +```bash +brew install cmake ldid +``` + +**Install Qt 6 for iOS and macOS using aqt:** + +Both the iOS and macOS Qt targets must be installed under the same version directory (e.g. `~/Qt/6.12.0/`). The build script expects them at `~/Qt/6.x.x/ios/` and `~/Qt/6.x.x/macos/`. + +```bash +pip install aqtinstall + +# Install the macOS host tools (required even for iOS builds) +aqt install-qt mac desktop 6.12.0 clang_64 \ + --outputdir ~/Qt \ + --modules qtsvg qtimageformats qtopengl qtnetwork qtxml + +# Install the iOS target +aqt install-qt mac ios 6.12.0 ios \ + --outputdir ~/Qt \ + --modules qtsvg qtimageformats qtopengl qtnetwork qtxml +``` + +Replace `6.12.0` with your preferred Qt version. Qt 6.8 or later is recommended; the minimum supported version is Qt 6.5. + +**Build:** + +```bash +git clone https://github.com/ScanTailor-Advanced/scantailor-advanced.git +cd scantailor-advanced +./build-ios.sh +``` + +This produces `scantailor-advanced.ipa` on your Desktop, fakesigned and ready to sideload. + +**Install on device:** + +Use [Sideloadly](https://sideloadly.io/), [AltStore](https://altstore.io/), or `ideviceinstaller`: + +```bash +# With ideviceinstaller +brew install ideviceinstaller +ideviceinstaller -i ~/Desktop/scantailor-advanced.ipa +``` + +**Notes:** +- Fakesigned IPAs installed with a free Apple ID via AltStore or Sideloadly expire after 7 days and need to be re-sideloaded. There is no such limitation when installed on a jailbroken device. +- The iOS file browser is rooted at the app's Documents folder. To make scans accessible, copy them to the ScanTailor Advanced folder via the Files app or Finder file sharing. +- Directory selection works fully with Magic Keyboard trackpad and keyboard navigation. +- To override the Qt path: `QT_IOS_DIR=~/Qt/6.x.x/ios QT_MACOS_DIR=~/Qt/6.x.x/macos ./build-ios.sh` \ No newline at end of file diff --git a/build-ios.sh b/build-ios.sh new file mode 100755 index 000000000..658464cd9 --- /dev/null +++ b/build-ios.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +# Build ScanTailor Advanced and create a fakesigned .ipa for iOS. +# Usage: ./build-ios.sh [build_dir] +# Requires: Qt 6.x with iOS target, Xcode, ldid (brew install ldid) +# +# The resulting .ipa is fakesigned and can be installed via: +# - Sideloadly (sideloadly.io) +# - AltStore (altstore.io) +# - TrollStore (if device is supported) +# - ideviceinstaller after re-signing with a real cert +# +# Environment variables (override defaults): +# QT_IOS_DIR Path to Qt iOS installation (e.g. ~/Qt/6.12.0/ios) +# QT_MACOS_DIR Path to Qt macOS installation (e.g. ~/Qt/6.12.0/macos) +# BUNDLE_ID App bundle identifier (e.g. com.yourname.scantailor) +# DEVICE_ID Target device UDID for install (optional, requires real cert) + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +BUILD_DIR="${1:-build-ios}" +mkdir -p "$BUILD_DIR" +BUILD_DIR="$(cd "$BUILD_DIR" && pwd)" + +# ── Check for ldid ───────────────────────────────────────────────────────────── + +if ! command -v ldid >/dev/null 2>&1; then + echo "Error: ldid not found. Install with: brew install ldid" >&2 + exit 1 +fi + +# ── Generate icons ───────────────────────────────────────────────────────────── + +if [[ -f "${SCRIPT_DIR}/generate-icons.sh" ]]; then + echo "Generating icons..." + bash "${SCRIPT_DIR}/generate-icons.sh" +else + echo "Warning: generate-icons.sh not found, skipping icon generation." >&2 +fi + +# ── Locate Qt ────────────────────────────────────────────────────────────────── + +if [[ -z "$QT_IOS_DIR" ]]; then + QT_IOS_DIR=$(find ~/Qt -name "Qt6Config.cmake" 2>/dev/null \ + | grep "/ios/" | head -1 | sed 's|/lib/cmake/Qt6/Qt6Config.cmake||') +fi +if [[ -z "$QT_MACOS_DIR" ]]; then + QT_MACOS_DIR=$(find ~/Qt -name "Qt6Config.cmake" 2>/dev/null \ + | grep "/macos/" | head -1 | sed 's|/lib/cmake/Qt6/Qt6Config.cmake||') +fi + +if [[ -z "$QT_IOS_DIR" || ! -d "$QT_IOS_DIR" ]]; then + echo "Error: Could not find Qt iOS installation." >&2 + echo "Install via: aqt install-qt mac ios ios --outputdir ~/Qt" >&2 + echo "Or set QT_IOS_DIR=/path/to/Qt/6.x.x/ios" >&2 + exit 1 +fi +if [[ -z "$QT_MACOS_DIR" || ! -d "$QT_MACOS_DIR" ]]; then + echo "Error: Could not find Qt macOS installation (needed for host tools)." >&2 + echo "Install via: aqt install-qt mac desktop clang_64 --outputdir ~/Qt" >&2 + echo "Or set QT_MACOS_DIR=/path/to/Qt/6.x.x/macos" >&2 + exit 1 +fi + +echo "Qt iOS: $QT_IOS_DIR" +echo "Qt macOS: $QT_MACOS_DIR" + +# ── Extract version ──────────────────────────────────────────────────────────── + +VERSION=$(sed -n 's/^#define VERSION "\([^"]*\)".*/\1/p' version.h.in) +if [[ -z "$VERSION" ]]; then + echo "Error: could not read VERSION from version.h.in" >&2 + exit 1 +fi + +BUNDLE_ID="${BUNDLE_ID:-com.$(id -un).scantailor}" +echo "Bundle ID: $BUNDLE_ID" +echo "Building ScanTailor Advanced ${VERSION} for iOS (fakesigned)" + +# ── Configure ───────────────────────────────────────────────────────────────── + +cd "$BUILD_DIR" +cmake -G Xcode \ + -DQt6_DIR="${QT_IOS_DIR}/lib/cmake/Qt6" \ + -DQT_HOST_PATH="${QT_MACOS_DIR}" \ + -DCMAKE_PREFIX_PATH="${QT_IOS_DIR};${QT_MACOS_DIR}" \ + -DCMAKE_TOOLCHAIN_FILE="${QT_IOS_DIR}/lib/cmake/Qt6/qt.toolchain.cmake" \ + -DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=BOTH \ + -DBUNDLE_ID="${BUNDLE_ID}" \ + "$SCRIPT_DIR" + +# ── Build (no real signing) ──────────────────────────────────────────────────── + +xcodebuild \ + -project "ScanTailor Advanced.xcodeproj" \ + -scheme "scantailor-advanced" \ + -destination "generic/platform=iOS" \ + -configuration Release \ + CODE_SIGNING_ALLOWED=NO \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGN_IDENTITY="" \ + build + +# ── Find built .app ──────────────────────────────────────────────────────────── + +APP=$(find "$BUILD_DIR" -name "scantailor-advanced.app" \ + -not -path "*/simulator/*" | head -1) + +if [[ -z "$APP" ]]; then + echo "Error: could not find built .app" >&2 + exit 1 +fi + +# ── Fakesign with ldid ───────────────────────────────────────────────────────── + +echo "Fakesigning with ldid..." +ENTITLEMENTS="${SCRIPT_DIR}/ios/scantailor.entitlements" +if [[ -f "$ENTITLEMENTS" ]]; then + ldid -S"$ENTITLEMENTS" "$APP/scantailor-advanced" +else + ldid -S "$APP/scantailor-advanced" +fi +# Sign any embedded frameworks/dylibs too +find "$APP" -name "*.dylib" -o -name "*.framework" | while read -r lib; do + ldid -S "$lib" 2>/dev/null || true +done + +# ── Package as IPA ──────────────────────────────────────────────────────────── + +IPA_DIR="/tmp/scantailor-ipa-$$" +mkdir -p "${IPA_DIR}/Payload" +cp -r "$APP" "${IPA_DIR}/Payload/" + +IPA_PATH="${BUILD_DIR}/scantailor-advanced_${VERSION}_ios.ipa" +cd "$IPA_DIR" +zip -r "$IPA_PATH" Payload/ +rm -rf "$IPA_DIR" + +echo "" +echo "Done: $IPA_PATH" +echo "" +echo "Install options:" +echo " Sideloadly: drag the .ipa onto sideloadly.io app" +echo " AltStore: use AltServer to sideload" +echo " TrollStore: if your device is supported" \ No newline at end of file diff --git a/build-macos.sh b/build-macos.sh new file mode 100755 index 000000000..13362ee0d --- /dev/null +++ b/build-macos.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +# Build ScanTailor Advanced and create an ad-hoc signed .dmg for macOS. +# Usage: ./build-macos.sh [build_dir] +# Requires: Qt 6.x (macOS), CMake, Xcode Command Line Tools +# +# The app is signed with an ad-hoc signature (-) so it runs on your own Mac +# without a developer certificate. Other users will need to run: +# xattr -cr /Applications/ScanTailor\ Advanced.app +# to remove the quarantine attribute after copying from the DMG. +# +# Environment variables (override defaults): +# QT_MACOS_DIR Path to Qt macOS installation (e.g. ~/Qt/6.12.0/macos) +# Falls back to Homebrew Qt if not set. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +BUILD_DIR="${1:-build-macos}" +mkdir -p "$BUILD_DIR" +BUILD_DIR="$(cd "$BUILD_DIR" && pwd)" + +# ── Generate icons ───────────────────────────────────────────────────────────── + +if [[ -f "${SCRIPT_DIR}/generate-icons.sh" ]]; then + echo "Generating icons..." + bash "${SCRIPT_DIR}/generate-icons.sh" +else + echo "Warning: generate-icons.sh not found, skipping icon generation." >&2 +fi + +# ── Locate Qt ────────────────────────────────────────────────────────────────── + +if [[ -z "$QT_MACOS_DIR" ]]; then + QT_MACOS_DIR=$(find ~/Qt -name "Qt6Config.cmake" 2>/dev/null \ + | grep "/macos/" | head -1 | sed 's|/lib/cmake/Qt6/Qt6Config.cmake||') +fi +if [[ -z "$QT_MACOS_DIR" || ! -d "$QT_MACOS_DIR" ]]; then + if [[ -d "/opt/homebrew/opt/qt" ]]; then + QT_MACOS_DIR="/opt/homebrew/opt/qt" + elif [[ -d "/usr/local/opt/qt" ]]; then + QT_MACOS_DIR="/usr/local/opt/qt" + fi +fi +if [[ -z "$QT_MACOS_DIR" || ! -d "$QT_MACOS_DIR" ]]; then + echo "Error: Could not find Qt macOS installation." >&2 + echo "Install via Homebrew: brew install qt" >&2 + echo "Or set QT_MACOS_DIR=/path/to/Qt/6.x.x/macos" >&2 + exit 1 +fi +echo "Qt macOS: $QT_MACOS_DIR" + +# ── Locate dependencies ──────────────────────────────────────────────────────── + +for pkg in jpeg libpng libtiff boost; do + if ! brew list "$pkg" &>/dev/null; then + echo "Installing missing dependency: $pkg" + brew install "$pkg" + fi +done + +# ── Extract version ──────────────────────────────────────────────────────────── + +VERSION=$(sed -n 's/^#define VERSION "\([^"]*\)".*/\1/p' version.h.in) +if [[ -z "$VERSION" ]]; then + echo "Error: could not read VERSION from version.h.in" >&2 + exit 1 +fi +echo "Building ScanTailor Advanced ${VERSION} for macOS" + +# ── Configure ───────────────────────────────────────────────────────────────── + +cd "$BUILD_DIR" +cmake -G "Unix Makefiles" \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_PREFIX_PATH="${QT_MACOS_DIR}" \ + -DCMAKE_INSTALL_PREFIX=/usr/local \ + "$SCRIPT_DIR" + +# ── Build ────────────────────────────────────────────────────────────────────── + +make -j"$(sysctl -n hw.logicalcpu)" + +# ── Bundle Qt frameworks ─────────────────────────────────────────────────────── + +APP_PATH="${BUILD_DIR}/ScanTailor Advanced.app" + +MACDEPLOYQT="${QT_MACOS_DIR}/bin/macdeployqt" +if [[ ! -x "$MACDEPLOYQT" ]]; then + MACDEPLOYQT=$(find /opt/homebrew /usr/local -name macdeployqt 2>/dev/null | head -1) +fi +if [[ -x "$MACDEPLOYQT" ]]; then + "$MACDEPLOYQT" "$APP_PATH" -verbose=1 +else + echo "Warning: macdeployqt not found, skipping Qt framework bundling." >&2 +fi + +# ── Ad-hoc sign (no developer account needed) ───────────────────────────────── + +echo "Ad-hoc signing..." +ENTITLEMENTS="${SCRIPT_DIR}/macos/scantailor.entitlements" +if [[ -f "$ENTITLEMENTS" ]]; then + codesign --force --deep --sign - --entitlements "$ENTITLEMENTS" "$APP_PATH" +else + codesign --force --deep --sign - "$APP_PATH" +fi + +# ── Create DMG ──────────────────────────────────────────────────────────────── + +DMG_PATH="${SCRIPT_DIR}/scantailor-advanced_${VERSION}_macos.dmg" +ICNS="${SCRIPT_DIR}/src/resources/appicon.icns" + +if command -v create-dmg >/dev/null 2>&1; then + create-dmg \ + --volname "ScanTailor Advanced ${VERSION}" \ + $([[ -f "$ICNS" ]] && echo "--volicon \"$ICNS\"") \ + --window-pos 200 120 \ + --window-size 600 400 \ + --icon-size 100 \ + --icon "scantailor-advanced.app" 175 190 \ + --hide-extension "scantailor-advanced.app" \ + --app-drop-link 425 190 \ + "$DMG_PATH" \ + "$APP_PATH" +else + hdiutil create -volname "ScanTailor Advanced" \ + -srcfolder "$APP_PATH" \ + -ov -format UDZO \ + "$DMG_PATH" +fi + +echo "" +echo "Done: $DMG_PATH" +echo "" +echo "Note: users installing from the DMG should run:" +echo " xattr -cr /Applications/ScanTailor\\ Advanced.app" \ No newline at end of file diff --git a/cmake/ios.toolchain.cmake b/cmake/ios.toolchain.cmake new file mode 100644 index 000000000..8c177878f --- /dev/null +++ b/cmake/ios.toolchain.cmake @@ -0,0 +1,4 @@ +set(CMAKE_SYSTEM_NAME iOS) +set(CMAKE_OSX_ARCHITECTURES "arm64") # arm64 for device; add x86_64 for simulator +set(CMAKE_OSX_DEPLOYMENT_TARGET "18.0") # minimum iOS version +set(CMAKE_XCODE_ATTRIBUTE_ONLY_ACTIVE_ARCH NO) \ No newline at end of file diff --git a/generate-icons.sh b/generate-icons.sh new file mode 100755 index 000000000..c8d558fc1 --- /dev/null +++ b/generate-icons.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +# Generate iOS and macOS app icons from a source image. +# Prefers src/resources/appicon-ios.png, falls back to appicon.svg. +# Usage: ./generate-icons.sh +# Requires: sips (macOS built-in) for PNG source, or librsvg/Inkscape for SVG + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PNG="${SCRIPT_DIR}/src/resources/appicon-ios.png" +SVG="${SCRIPT_DIR}/src/resources/appicon.svg" + +# ── Pick source and renderer ─────────────────────────────────────────────────── + +if [[ -f "$PNG" ]]; then + SOURCE="$PNG" + # sips is built into macOS — no install needed + render() { + sips -z "$1" "$1" --out "$2" "$SOURCE" >/dev/null 2>&1 + } + echo "Source: $SOURCE (PNG)" +elif [[ -f "$SVG" ]]; then + SOURCE="$SVG" + if command -v rsvg-convert >/dev/null 2>&1; then + render() { rsvg-convert -w "$1" -h "$1" -o "$2" "$SOURCE"; } + elif command -v inkscape >/dev/null 2>&1; then + render() { inkscape --export-type=png --export-width="$1" --export-filename="$2" "$SOURCE" 2>/dev/null; } + else + echo "Error: no SVG renderer found. Install: brew install librsvg" >&2 + exit 1 + fi + echo "Source: $SOURCE (SVG)" +else + echo "Error: no source icon found." >&2 + echo "Place a PNG at: src/resources/appicon-ios.png" >&2 + echo "Or an SVG at: src/resources/appicon.svg" >&2 + exit 1 +fi + +# ══════════════════════════════════════════════════════════════════════════════ +# iOS — AppIcon.appiconset +# ══════════════════════════════════════════════════════════════════════════════ + +IOS_DIR="${SCRIPT_DIR}/build-ios/AppIcon.appiconset" +mkdir -p "$IOS_DIR" + +echo "Generating iOS icons..." + +for size in 20 29 40 58 60 76 80 87 120 152 167 180 1024; do + out="${IOS_DIR}/icon_${size}.png" + echo " ${size}x${size} → icon_${size}.png" + render "$size" "$out" +done + +cat > "${IOS_DIR}/Contents.json" << 'JSON' +{ + "images": [ + { "idiom": "universal", "platform": "ios", "size": "1024x1024", "scale": "1x", "filename": "icon_1024.png" }, + { "idiom": "iphone", "size": "20x20", "scale": "2x", "filename": "icon_40.png" }, + { "idiom": "iphone", "size": "20x20", "scale": "3x", "filename": "icon_60.png" }, + { "idiom": "iphone", "size": "29x29", "scale": "2x", "filename": "icon_58.png" }, + { "idiom": "iphone", "size": "29x29", "scale": "3x", "filename": "icon_87.png" }, + { "idiom": "iphone", "size": "40x40", "scale": "2x", "filename": "icon_80.png" }, + { "idiom": "iphone", "size": "40x40", "scale": "3x", "filename": "icon_120.png" }, + { "idiom": "iphone", "size": "60x60", "scale": "2x", "filename": "icon_120.png" }, + { "idiom": "iphone", "size": "60x60", "scale": "3x", "filename": "icon_180.png" }, + { "idiom": "ipad", "size": "20x20", "scale": "1x", "filename": "icon_20.png" }, + { "idiom": "ipad", "size": "20x20", "scale": "2x", "filename": "icon_40.png" }, + { "idiom": "ipad", "size": "29x29", "scale": "1x", "filename": "icon_29.png" }, + { "idiom": "ipad", "size": "29x29", "scale": "2x", "filename": "icon_58.png" }, + { "idiom": "ipad", "size": "40x40", "scale": "1x", "filename": "icon_40.png" }, + { "idiom": "ipad", "size": "40x40", "scale": "2x", "filename": "icon_80.png" }, + { "idiom": "ipad", "size": "76x76", "scale": "1x", "filename": "icon_76.png" }, + { "idiom": "ipad", "size": "76x76", "scale": "2x", "filename": "icon_152.png" }, + { "idiom": "ipad", "size": "83.5x83.5","scale": "2x", "filename": "icon_167.png" } + ], + "info": { "author": "xcode", "version": 1 } +} +JSON + +echo "iOS icons → $IOS_DIR" + +# ══════════════════════════════════════════════════════════════════════════════ +# macOS — .icns via iconutil +# ══════════════════════════════════════════════════════════════════════════════ + +ICONSET="${SCRIPT_DIR}/src/resources/appicon.iconset" +ICNS="${SCRIPT_DIR}/src/resources/appicon.icns" +mkdir -p "$ICONSET" + +echo "Generating macOS icons..." + +declare -A MACOS=( + ["icon_16x16.png"]=16 + ["icon_16x16@2x.png"]=32 + ["icon_32x32.png"]=32 + ["icon_32x32@2x.png"]=64 + ["icon_128x128.png"]=128 + ["icon_128x128@2x.png"]=256 + ["icon_256x256.png"]=256 + ["icon_256x256@2x.png"]=512 + ["icon_512x512.png"]=512 + ["icon_512x512@2x.png"]=1024 +) + +for name in "${!MACOS[@]}"; do + size="${MACOS[$name]}" + echo " ${size}x${size} → $name" + render "$size" "${ICONSET}/${name}" +done + +iconutil -c icns "$ICONSET" -o "$ICNS" +rm -rf "$ICONSET" + +echo "macOS .icns → $ICNS" +echo "" +echo "Done." diff --git a/ios/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..b2824330c --- /dev/null +++ b/ios/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,23 @@ +{ + "images": [ + { "idiom": "universal", "platform": "ios", "size": "1024x1024", "scale": "1x", "filename": "icon_1024.png" }, + { "idiom": "iphone", "size": "20x20", "scale": "2x", "filename": "icon_40.png" }, + { "idiom": "iphone", "size": "20x20", "scale": "3x", "filename": "icon_60.png" }, + { "idiom": "iphone", "size": "29x29", "scale": "2x", "filename": "icon_58.png" }, + { "idiom": "iphone", "size": "29x29", "scale": "3x", "filename": "icon_87.png" }, + { "idiom": "iphone", "size": "40x40", "scale": "2x", "filename": "icon_80.png" }, + { "idiom": "iphone", "size": "40x40", "scale": "3x", "filename": "icon_120.png" }, + { "idiom": "iphone", "size": "60x60", "scale": "2x", "filename": "icon_120.png" }, + { "idiom": "iphone", "size": "60x60", "scale": "3x", "filename": "icon_180.png" }, + { "idiom": "ipad", "size": "20x20", "scale": "1x", "filename": "icon_20.png" }, + { "idiom": "ipad", "size": "20x20", "scale": "2x", "filename": "icon_40.png" }, + { "idiom": "ipad", "size": "29x29", "scale": "1x", "filename": "icon_29.png" }, + { "idiom": "ipad", "size": "29x29", "scale": "2x", "filename": "icon_58.png" }, + { "idiom": "ipad", "size": "40x40", "scale": "1x", "filename": "icon_40.png" }, + { "idiom": "ipad", "size": "40x40", "scale": "2x", "filename": "icon_80.png" }, + { "idiom": "ipad", "size": "76x76", "scale": "1x", "filename": "icon_76.png" }, + { "idiom": "ipad", "size": "76x76", "scale": "2x", "filename": "icon_152.png" }, + { "idiom": "ipad", "size": "83.5x83.5","scale": "2x", "filename": "icon_167.png" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/ios/Assets.xcassets/AppIcon.appiconset/icon_1024.png b/ios/Assets.xcassets/AppIcon.appiconset/icon_1024.png new file mode 100644 index 000000000..b0f982aad Binary files /dev/null and b/ios/Assets.xcassets/AppIcon.appiconset/icon_1024.png differ diff --git a/ios/Assets.xcassets/AppIcon.appiconset/icon_120.png b/ios/Assets.xcassets/AppIcon.appiconset/icon_120.png new file mode 100644 index 000000000..6de665d12 Binary files /dev/null and b/ios/Assets.xcassets/AppIcon.appiconset/icon_120.png differ diff --git a/ios/Assets.xcassets/AppIcon.appiconset/icon_152.png b/ios/Assets.xcassets/AppIcon.appiconset/icon_152.png new file mode 100644 index 000000000..99312470b Binary files /dev/null and b/ios/Assets.xcassets/AppIcon.appiconset/icon_152.png differ diff --git a/ios/Assets.xcassets/AppIcon.appiconset/icon_167.png b/ios/Assets.xcassets/AppIcon.appiconset/icon_167.png new file mode 100644 index 000000000..b54c3fa5e Binary files /dev/null and b/ios/Assets.xcassets/AppIcon.appiconset/icon_167.png differ diff --git a/ios/Assets.xcassets/AppIcon.appiconset/icon_180.png b/ios/Assets.xcassets/AppIcon.appiconset/icon_180.png new file mode 100644 index 000000000..4d853580a Binary files /dev/null and b/ios/Assets.xcassets/AppIcon.appiconset/icon_180.png differ diff --git a/ios/Assets.xcassets/AppIcon.appiconset/icon_20.png b/ios/Assets.xcassets/AppIcon.appiconset/icon_20.png new file mode 100644 index 000000000..fcedc4de7 Binary files /dev/null and b/ios/Assets.xcassets/AppIcon.appiconset/icon_20.png differ diff --git a/ios/Assets.xcassets/AppIcon.appiconset/icon_29.png b/ios/Assets.xcassets/AppIcon.appiconset/icon_29.png new file mode 100644 index 000000000..fa6236813 Binary files /dev/null and b/ios/Assets.xcassets/AppIcon.appiconset/icon_29.png differ diff --git a/ios/Assets.xcassets/AppIcon.appiconset/icon_40.png b/ios/Assets.xcassets/AppIcon.appiconset/icon_40.png new file mode 100644 index 000000000..98271d866 Binary files /dev/null and b/ios/Assets.xcassets/AppIcon.appiconset/icon_40.png differ diff --git a/ios/Assets.xcassets/AppIcon.appiconset/icon_58.png b/ios/Assets.xcassets/AppIcon.appiconset/icon_58.png new file mode 100644 index 000000000..f1df3839d Binary files /dev/null and b/ios/Assets.xcassets/AppIcon.appiconset/icon_58.png differ diff --git a/ios/Assets.xcassets/AppIcon.appiconset/icon_60.png b/ios/Assets.xcassets/AppIcon.appiconset/icon_60.png new file mode 100644 index 000000000..0e6f7bbb3 Binary files /dev/null and b/ios/Assets.xcassets/AppIcon.appiconset/icon_60.png differ diff --git a/ios/Assets.xcassets/AppIcon.appiconset/icon_76.png b/ios/Assets.xcassets/AppIcon.appiconset/icon_76.png new file mode 100644 index 000000000..9b70f30f7 Binary files /dev/null and b/ios/Assets.xcassets/AppIcon.appiconset/icon_76.png differ diff --git a/ios/Assets.xcassets/AppIcon.appiconset/icon_80.png b/ios/Assets.xcassets/AppIcon.appiconset/icon_80.png new file mode 100644 index 000000000..9cf2206ad Binary files /dev/null and b/ios/Assets.xcassets/AppIcon.appiconset/icon_80.png differ diff --git a/ios/Assets.xcassets/AppIcon.appiconset/icon_87.png b/ios/Assets.xcassets/AppIcon.appiconset/icon_87.png new file mode 100644 index 000000000..0cacb90d0 Binary files /dev/null and b/ios/Assets.xcassets/AppIcon.appiconset/icon_87.png differ diff --git a/ios/Assets.xcassets/Contents.json b/ios/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/ios/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Info.plist b/ios/Info.plist new file mode 100644 index 000000000..7c5d6607b --- /dev/null +++ b/ios/Info.plist @@ -0,0 +1,60 @@ + + + + + CFBundleName ScanTailor Advanced + CFBundleDisplayName ScanTailor Advanced + CFBundleIdentifier com.sunneva.scantailor + CFBundleVersion 1.0 + CFBundleShortVersionString1.0 + CFBundlePackageType APPL + CFBundleExecutable $(EXECUTABLE_NAME) + CFBundleIconName AppIcon + LSRequiresIPhoneOS + UIFileSharingEnabled + LSSupportsOpeningDocumentsInPlace + UISupportsDocumentBrowser + NSPhotoLibraryUsageDescription + ScanTailor needs access to your photos to process scanned images. + + CFBundleDocumentTypes + + + CFBundleTypeName Image + CFBundleTypeRole Viewer + LSItemContentTypes + + public.image + public.jpeg + public.png + public.tiff + + + + CFBundleTypeName ScanTailor Project + CFBundleTypeRole Editor + LSItemContentTypes + + public.data + + + + + UIRequiredDeviceCapabilities + arm64 + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/macos/scantailor.entitlements b/macos/scantailor.entitlements new file mode 100644 index 000000000..ad72b90c6 --- /dev/null +++ b/macos/scantailor.entitlements @@ -0,0 +1,13 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-write + + com.apple.security.files.downloads.read-write + + + diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 5bdd2ebe8..4686bc20a 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -1,8 +1,6 @@ set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) -set(target_name scantailor-advanced) - set(resource_files resources.qrc dark_scheme/dark_scheme.qrc @@ -69,27 +67,70 @@ if (WIN32) endif() endif() -add_executable( - ${target_name} WIN32 ${gui_only_sources} ${gui_only_ui_files} - ${resource_files} ${win32_resource_file}) +if(IOS) + list(APPEND gui_only_sources ios/IosDirectoryPicker.mm ios/IosDirectoryPicker.h) + set_source_files_properties(ios/IosDirectoryPicker.mm + PROPERTIES COMPILE_FLAGS "-x objective-c++") + # iOS app icon via asset catalog + set(IOS_ICON_CATALOG "${CMAKE_SOURCE_DIR}/ios/Assets.xcassets") + list(APPEND gui_only_sources "${IOS_ICON_CATALOG}") + set_source_files_properties("${IOS_ICON_CATALOG}" + PROPERTIES MACOSX_PACKAGE_LOCATION Resources) + # iOS: MACOSX_BUNDLE required; use all sources (UI will be stubbed later) + # Resource files excluded — .qrc with desktop assets won't compile for iOS + add_executable( + ${target_name} MACOSX_BUNDLE + ${gui_only_sources} ${gui_only_ui_files}) +elseif(APPLE) + add_executable( + ${target_name} MACOSX_BUNDLE + ${gui_only_sources} ${gui_only_ui_files} + ${resource_files}) +else() + add_executable( + ${target_name} WIN32 + ${gui_only_sources} ${gui_only_ui_files} + ${resource_files} ${win32_resource_file}) +endif() + target_link_libraries( ${target_name} PRIVATE core ${EXTRA_LIBS}) + target_include_directories( ${target_name} - PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}" - "$") -install(TARGETS ${target_name} RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") + PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") + +# TIFF include dir only available on desktop — not cross-compiled for iOS +if(NOT IOS) + target_include_directories( + ${target_name} + PRIVATE "$") +endif() + +install(TARGETS ${target_name} + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" + BUNDLE DESTINATION "${CMAKE_INSTALL_APPDIR}") -if (Qt6_FOUND AND WIN32) +if(Qt6_FOUND AND WIN32) #manage plugins with windeployqt. add_custom_command( - TARGET ${target_name} - POST_BUILD - COMMAND ${QT_BINDIR}/windeployqt $ - COMMENT "deploy qt files alongside binary (e.g. plugins)" - ) + TARGET ${target_name} + POST_BUILD + COMMAND ${QT_BINDIR}/windeployqt $ + COMMENT "deploy qt files alongside binary (e.g. plugins)") install(CODE "execute_process(COMMAND \"${QT_BINDIR}/windeployqt\" \"\$\{CMAKE_INSTALL_PREFIX\}\")") endif() translation_sources(${target_name} ${gui_only_sources} ${gui_only_ui_files}) + +# macOS: set app icon and bundle name +if(APPLE AND NOT IOS) + set_target_properties(${target_name} PROPERTIES OUTPUT_NAME "ScanTailor Advanced") + set(MACOS_ICON "${CMAKE_SOURCE_DIR}/src/resources/appicon.icns") + if(EXISTS "${MACOS_ICON}") + target_sources(${target_name} PRIVATE "${MACOS_ICON}") + set_source_files_properties("${MACOS_ICON}" PROPERTIES MACOSX_PACKAGE_LOCATION Resources) + set_target_properties(${target_name} PROPERTIES MACOSX_BUNDLE_ICON_FILE appicon.icns) + endif() +endif() diff --git a/src/app/MainWindow.cpp b/src/app/MainWindow.cpp index 0f07e5ca0..d708f8c7f 100644 --- a/src/app/MainWindow.cpp +++ b/src/app/MainWindow.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -1334,7 +1335,9 @@ void MainWindow::filterResult(const BackgroundTaskPtr& task, const FilterResultP if (cmd.isEmpty()) { QApplication::beep(); } else { +#if !defined(Q_OS_IOS) Q_UNUSED(std::system(cmd.toStdString().c_str())); +#endif } } @@ -1425,7 +1428,13 @@ void MainWindow::saveProjectAsTriggered() { } QString projectFile( - QFileDialog::getSaveFileName(this, QString(), projectDir, tr("Scan Tailor Projects") + " (*.ScanTailor)")); + QFileDialog::getSaveFileName(this, QString(), projectDir, tr("Scan Tailor Projects") + " (*.ScanTailor)", +#if defined(Q_OS_IOS) + nullptr, QFileDialog::DontUseNativeDialog +#else + nullptr +#endif + )); if (projectFile.isEmpty()) { return; } @@ -1468,9 +1477,18 @@ void MainWindow::openProject() { return; } +#if defined(Q_OS_IOS) + const QString projectDir(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)); +#else const QString projectDir(QSettings().value("project/lastDir").toString()); - const QString projectFile(QFileDialog::getOpenFileName(this, tr("Open Project"), projectDir, - tr("Scan Tailor Projects") + " (*.ScanTailor)")); +#endif + const QString projectFile(QFileDialog::getOpenFileName( + this, tr("Open Project"), projectDir, + tr("Scan Tailor Projects") + " (*.ScanTailor)" +#if defined(Q_OS_IOS) + , nullptr, QFileDialog::DontUseNativeDialog +#endif + )); if (projectFile.isEmpty()) { // Cancelled by user. return; diff --git a/src/app/OutOfMemoryDialog.cpp b/src/app/OutOfMemoryDialog.cpp index 691f2d167..6200b404c 100644 --- a/src/app/OutOfMemoryDialog.cpp +++ b/src/app/OutOfMemoryDialog.cpp @@ -59,7 +59,11 @@ void OutOfMemoryDialog::saveProjectAs() { } QString projectFile( - QFileDialog::getSaveFileName(this, QString(), projectDir, tr("Scan Tailor Projects") + " (*.ScanTailor)")); + QFileDialog::getSaveFileName(this, QString(), projectDir, tr("Scan Tailor Projects") + " (*.ScanTailor)" +#if defined(Q_OS_IOS) + , nullptr, QFileDialog::DontUseNativeDialog +#endif + )); if (projectFile.isEmpty()) { return; } diff --git a/src/app/ProjectFilesDialog.cpp b/src/app/ProjectFilesDialog.cpp index 87c5fbae4..a71d5821a 100644 --- a/src/app/ProjectFilesDialog.cpp +++ b/src/app/ProjectFilesDialog.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -16,6 +17,10 @@ #include "NonCopyable.h" #include "SmartFilenameOrdering.h" +#ifdef Q_OS_IOS +#include "ios/IosDirectoryPicker.h" +#endif + class ProjectFilesDialog::Item { public: enum Status { STATUS_DEFAULT, STATUS_LOAD_OK, STATUS_LOAD_FAILED }; @@ -230,7 +235,6 @@ QString ProjectFilesDialog::sanitizePath(const QString& path) { void ProjectFilesDialog::inpDirBrowse() { QSettings settings; - QString initialDir(inpDirLine->text()); if (initialDir.isEmpty() || !QDir(initialDir).exists()) { initialDir = settings.value("lastInputDir").toString(); @@ -240,26 +244,65 @@ void ProjectFilesDialog::inpDirBrowse() { } else { initialDir = QDir(initialDir).absolutePath(); } - - const QString dir(QFileDialog::getExistingDirectory(this, tr("Input Directory"), initialDir)); - - if (!dir.isEmpty()) { - setInputDir(dir); - settings.setValue("lastInputDir", dir); +#if defined(Q_OS_IOS) + { + const QString startDir = QStandardPaths::writableLocation( + QStandardPaths::DocumentsLocation); + const QString dir = QFileDialog::getExistingDirectory( + this, tr("Input Directory"), startDir, + QFileDialog::ShowDirsOnly | QFileDialog::DontUseNativeDialog); + if (!dir.isEmpty()) { + setInputDir(dir); + settings.setValue("lastInputDir", dir); + } + } +#else + { + const QString dir(QFileDialog::getExistingDirectory( + this,tr("Input Directory"), initialDir, +#if defined(Q_OS_MACOS) + QFileDialog::ShowDirsOnly | QFileDialog::DontUseNativeDialog +#else + QFileDialog::ShowDirsOnly +#endif + )); + if (!dir.isEmpty()) { + setInputDir(dir); + settings.setValue("lastInputDir", dir); + } } +#endif } - void ProjectFilesDialog::outDirBrowse() { QString initialDir(outDirLine->text()); if (initialDir.isEmpty() || !QDir(initialDir).exists()) { initialDir = QDir::home().absolutePath(); } - - const QString dir(QFileDialog::getExistingDirectory(this, tr("Output Directory"), initialDir)); - - if (!dir.isEmpty()) { - setOutputDir(dir); +#if defined(Q_OS_IOS) + { + const QString startDir = QStandardPaths::writableLocation( + QStandardPaths::DocumentsLocation); + const QString dir = QFileDialog::getExistingDirectory( + this, tr("Output Directory"), startDir, + QFileDialog::ShowDirsOnly | QFileDialog::DontUseNativeDialog); + if (!dir.isEmpty()) { + setOutputDir(dir); + } + } +#else + { + const QString dir(QFileDialog::getExistingDirectory(this, tr("Output Directory"), initialDir, +# if defined(Q_OS_MACOS) + QFileDialog::ShowDirsOnly | QFileDialog::DontUseNativeDialog +# else + QFileDialog::ShowDirsOnly +# endif + )); + if (!dir.isEmpty()) { + setOutputDir(dir); + } } +#endif } void ProjectFilesDialog::inpDirEdited(const QString& text) { diff --git a/src/app/ios/IosDirectoryPicker.h b/src/app/ios/IosDirectoryPicker.h new file mode 100644 index 000000000..7de0169cd --- /dev/null +++ b/src/app/ios/IosDirectoryPicker.h @@ -0,0 +1,9 @@ +// IosDirectoryPicker.h +#pragma once +#include +#include + +// Shows the native iOS folder/file picker asynchronously. +// The callback is called on the main thread with the selected directory path, +// or an empty string if cancelled. +void iosPickDirectory(const QString& title, std::function callback); diff --git a/src/app/ios/IosDirectoryPicker.mm b/src/app/ios/IosDirectoryPicker.mm new file mode 100644 index 000000000..5bd3a716b --- /dev/null +++ b/src/app/ios/IosDirectoryPicker.mm @@ -0,0 +1,82 @@ +// IosDirectoryPicker.mm +#import +#import +#include "IosDirectoryPicker.h" +#include +#include + +@interface STDirectoryPickerDelegate : NSObject +@property (nonatomic, copy) void (^completion)(NSURL* url); +@end + +@implementation STDirectoryPickerDelegate +- (void)documentPicker:(UIDocumentPickerViewController*)controller + didPickDocumentsAtURLs:(NSArray*)urls { + if (self.completion) { + self.completion(urls.firstObject); + self.completion = nil; + } +} +- (void)documentPickerWasCancelled:(UIDocumentPickerViewController*)controller { + if (self.completion) { + self.completion(nil); + self.completion = nil; + } +} +@end + +static UIViewController* topViewController() { + UIViewController* root = nil; + for (UIScene* scene in [UIApplication sharedApplication].connectedScenes) { + if ([scene isKindOfClass:[UIWindowScene class]]) { + UIWindowScene* ws = (UIWindowScene*)scene; + for (UIWindow* w in ws.windows) { + if (w.isKeyWindow) { root = w.rootViewController; break; } + } + } + } + while (root.presentedViewController) root = root.presentedViewController; + return root; +} + +// Keep delegate alive until picker is dismissed +static STDirectoryPickerDelegate* s_delegate = nil; + +void iosPickDirectory(const QString& title, + std::function callback) { + auto* cb = new std::function(std::move(callback)); + + dispatch_async(dispatch_get_main_queue(), ^{ + UIDocumentPickerViewController* picker = + [[UIDocumentPickerViewController alloc] + initForOpeningContentTypes:@[ + UTTypeImage, UTTypeJPEG, UTTypePNG, UTTypeTIFF, + UTTypeData, UTTypeFolder + ]]; + picker.allowsMultipleSelection = NO; + + s_delegate = [[STDirectoryPickerDelegate alloc] init]; + s_delegate.completion = ^(NSURL* url) { + QString path; + if (url) { + [url startAccessingSecurityScopedResource]; + NSNumber* isDir = nil; + [url getResourceValue:&isDir forKey:NSURLIsDirectoryKey error:nil]; + NSURL* dir = isDir.boolValue ? url : [url URLByDeletingLastPathComponent]; + path = QString::fromNSString(dir.path); + } + (*cb)(path); + delete cb; + s_delegate = nil; + }; + picker.delegate = s_delegate; + + UIViewController* top = topViewController(); + if (top) { + [top presentViewController:picker animated:YES completion:nil]; + } else { + (*cb)(QString()); + delete cb; + } + }); +} diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 333f1c843..4af820bac 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -119,7 +119,14 @@ set(common_sources add_library(core STATIC ${common_sources} ${common_ui_files}) -if(Qt6_FOUND) +# iOS: link only Qt modules; TIFF/PNG/JPEG/ZLIB are bundled inside Qt +if(IOS AND Qt6_FOUND) + target_link_libraries( + core + PUBLIC Qt::Core Qt::Gui Qt::Widgets Qt::Xml Qt::Network Qt::OpenGL Qt::Svg Qt::OpenGLWidgets + imageproc zones + fix_orientation page_split deskew select_content page_layout output) +elseif(Qt6_FOUND) target_link_libraries( core PRIVATE TIFF::TIFF PNG::PNG ZLIB::ZLIB JPEG::JPEG @@ -134,6 +141,7 @@ else() imageproc zones fix_orientation page_split deskew select_content page_layout output) endif() + target_include_directories(core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") set_target_properties(core PROPERTIES AUTOUIC ON) diff --git a/src/core/filters/output/CMakeLists.txt b/src/core/filters/output/CMakeLists.txt index d3c66d272..0d4193cbf 100644 --- a/src/core/filters/output/CMakeLists.txt +++ b/src/core/filters/output/CMakeLists.txt @@ -54,10 +54,10 @@ set(sources DepthPerception.cpp DepthPerception.h SplittingOptions.cpp SplittingOptions.h OptionsWidgetBinarizationOtsu.cpp OptionsWidgetBinarizationOtsu.h - OptionsWidgetBinarizationSauvola.cpp OptionsWidgetBinarizationSauvola.h - OptionsWidgetBinarizationWolf.cpp OptionsWidgetBinarizationWolf.h - OptionsWidgetBinarization.h - PictureShapeOptions.cpp PictureShapeOptions.h + OptionsWidgetBinarizationSauvola.cpp OptionsWidgetBinarizationSauvola.h + OptionsWidgetBinarizationWolf.cpp OptionsWidgetBinarizationWolf.h + OptionsWidgetBinarization.h + PictureShapeOptions.cpp PictureShapeOptions.h OutputProcessingParams.cpp OutputProcessingParams.h OutputImage.h OutputImageWithForeground.h @@ -69,6 +69,10 @@ set(sources add_library(output STATIC ${sources} ${ui_files}) target_link_libraries(output PUBLIC core PRIVATE dewarping) -target_include_directories(output PRIVATE "$") -translation_sources(scantailor ${sources} ${ui_files}) \ No newline at end of file +if(NOT IOS) + target_include_directories(output + PRIVATE "$") +endif() + +translation_sources(scantailor ${sources} ${ui_files}) diff --git a/src/foundation/CMakeLists.txt b/src/foundation/CMakeLists.txt index 75703b17f..478b78016 100644 --- a/src/foundation/CMakeLists.txt +++ b/src/foundation/CMakeLists.txt @@ -34,6 +34,11 @@ add_library(foundation STATIC ${sources}) target_link_libraries(foundation PUBLIC ${Qt_Core_lib} ${Qt_Xml_lib} ${Qt_Gui_lib}) target_include_directories(foundation PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") +if(IOS AND POLICY CMP0167) + # re-enable FindBoost.cmake + cmake_policy(SET CMP0167 OLD) +endif() + if (BUILD_TESTS) add_subdirectory(tests) -endif() +endif() \ No newline at end of file diff --git a/src/resources/appicon-ios.png b/src/resources/appicon-ios.png new file mode 100644 index 000000000..b0f982aad Binary files /dev/null and b/src/resources/appicon-ios.png differ diff --git a/src/resources/appicon.icns b/src/resources/appicon.icns new file mode 100644 index 000000000..1d7724506 Binary files /dev/null and b/src/resources/appicon.icns differ