diff --git a/.appveyor.yml b/.appveyor.yml index 6d2d2eba47..486228e15e 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,4 +1,4 @@ -image: Visual Studio 2019 +image: Visual Studio 2022 platform: - x64 branches: @@ -10,19 +10,8 @@ environment: MBASH: C:\msys64\usr\bin\sh -lc MSYS2_FC_CACHE_SKIP: true matrix: - - MSYSTEM: MINGW32 - MBITS: 32 -for: - - - branches: - only: - - /release\/.*/ - environment: - global: - APPVEYOR_CONFIG_PROD: 1 - matrix: # This extends the default, so 32 bit is still built - - MSYSTEM: MINGW64 - MBITS: 64 + - MSYSTEM: UCRT64 + MBITS: 64 clone_depth: 10 @@ -30,7 +19,6 @@ clone_depth: 10 #cache: # - 'C:\msys64\var\cache\pacman\pkg' install: - - git clone --depth=1 --branch=master-local https://github.com/fontforge/fontforgebuilds.git - >- move C:\msys64 C:\msys64_old && curl -kLo msys2.tar.xz https://github.com/msys2/msys2-installer/releases/download/nightly-x86_64/msys2-base-x86_64-latest.tar.xz && @@ -40,20 +28,20 @@ install: - call %MBASH% "pacman -Syuu --noconfirm" - taskkill /f /fi "MODULES eq msys-2.0.dll" || exit /b 0 - call %MBASH% "pacman -Suu --noconfirm" - - call %MBASH% "pacman -Suu --noconfirm" - - call %MBASH% "cd $APPVEYOR_BUILD_FOLDER/fontforgebuilds; exec 0> $GITHUB_ENV + echo "PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig:/usr/local/opt/libffi/lib/pkgconfig" >> $GITHUB_ENV + echo "PATH=/usr/local/opt/ruby/bin:/usr/local/opt/gettext/bin:$PATH" >> $GITHUB_ENV + echo "PYTHONPATH=$PREFIX" >> $GITHUB_ENV + - name: Build FontForge + working-directory: repo + run: | + FFCONFIG="-DCMAKE_INSTALL_PREFIX=$PREFIX -DENABLE_FONTFORGE_EXTRAS=ON -DCMAKE_FIND_ROOT_PATH=/usr/local/opt/gettext" + mkdir build && pushd build && cmake -GNinja $FFCONFIG .. && popd + ninja -C build install + + # --- Generate + import a throwaway self-signed code-signing cert ------- + # Use the system (LibreSSL) openssl so the PKCS#12 is readable by the + # macOS keychain without needing the `-legacy` flag. + - name: Generate and import self-signed certificate + run: | + WORK="$RUNNER_TEMP/selfsign" + mkdir -p "$WORK" + cat > "$WORK/cert.conf" <<'EOF' + [req] + distinguished_name = dn + x509_extensions = v3 + prompt = no + [dn] + CN = FontForge Rehearsal Self-Signed + [v3] + basicConstraints = critical,CA:FALSE + keyUsage = critical,digitalSignature + extendedKeyUsage = critical,codeSigning + EOF + /usr/bin/openssl req -x509 -newkey rsa:2048 -nodes \ + -keyout "$WORK/key.pem" -out "$WORK/cert.pem" \ + -days 2 -config "$WORK/cert.conf" + /usr/bin/openssl pkcs12 -export \ + -inkey "$WORK/key.pem" -in "$WORK/cert.pem" \ + -out "$WORK/cert.p12" -passout pass:rehearsal \ + -name "FontForge Rehearsal Self-Signed" + + KEYCHAIN="$RUNNER_TEMP/rehearsal.keychain-db" + security create-keychain -p actions "$KEYCHAIN" + security set-keychain-settings -lut 3600 "$KEYCHAIN" + security unlock-keychain -p actions "$KEYCHAIN" + security import "$WORK/cert.p12" -k "$KEYCHAIN" -P rehearsal -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple: -s -k actions "$KEYCHAIN" + security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"') + security find-identity -v -p codesigning "$KEYCHAIN" + + # Drive ffosxbuild.sh down its Developer ID branch (hardened runtime + + # entitlements). NOTE: we intentionally do NOT set any notary secrets, + # so notarization is correctly skipped -- self-signed cannot notarize. + echo "CODESIGN_IDENTITY=FontForge Rehearsal Self-Signed" >> "$GITHUB_ENV" + + - name: Build App Bundle (hardened runtime, self-signed) + working-directory: repo/build + run: ninja macbundle + + # --- Verify the signing we just rehearsed ----------------------------- + - name: Verify signature and hardened runtime + working-directory: repo/build/osx + run: | + set -e + APP="$PWD/FontForge.app" + MAIN="$APP/Contents/Resources/opt/local/bin/fontforge" + + echo "::group::codesign --verify (whole bundle)" + codesign --verify --strict --verbose=2 "$APP" + echo "::endgroup::" + + echo "::group::Main executable signature (expect runtime flag + entitlements)" + codesign -dvvv "$MAIN" 2>&1 | grep -Ei 'Identifier|Signature|flags|TeamIdentifier' || true + codesign -d --entitlements :- "$MAIN" 2>&1 || true + echo "::endgroup::" + + # Sanity: the hardened runtime flag must be present on the main exec. + if ! codesign -dvvv "$MAIN" 2>&1 | grep -q 'flags=.*runtime'; then + echo "ERROR: hardened runtime flag not set on main executable" + exit 1 + fi + + # Expected: spctl REJECTS a self-signed (un-notarized) bundle. This is + # not a failure of our signing -- only Developer ID + notarization + # passes spctl. We log it for visibility but do not fail on it. + echo "::group::spctl assessment (expected to be rejected)" + spctl -a -t exec -vv "$APP" || echo "(expected) spctl rejected the self-signed bundle" + echo "::endgroup::" + + - name: Test embedded interpreter loads a foreign C extension + working-directory: repo/build/osx + env: + FOREIGN_PKG: ${{ inputs.foreign_test_package }} + run: | + set -e + APP="$PWD/FontForge.app" + FF="$APP/Contents/Resources/opt/local/bin/fontforge" + FFPY="$APP/Contents/MacOS/FFPython" + SITE="$RUNNER_TEMP/foreign-site" + + # Install the foreign package AFTER the bundle was signed, so its .so + # keeps its own (non-our-identity) signature. Importing it through the + # hardened-runtime `fontforge` binary therefore exercises + # disable-library-validation: without that entitlement, Library + # Validation would reject the foreign-signed module. + "$FFPY" -m pip install --target "$SITE" --no-input "$FOREIGN_PKG" + + cat > "$RUNNER_TEMP/import_test.py" <> $GITHUB_ENV echo "PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig:/usr/local/opt/libffi/lib/pkgconfig" >> $GITHUB_ENV echo "PATH=/usr/local/opt/ruby/bin:/usr/local/opt/gettext/bin:$PATH" >> $GITHUB_ENV - # System Python libraries will be discovered by CMake - echo "PYTHONPATH=$PREFIX" >> $GITHUB_ENV - name: Build FontForge working-directory: repo run: | @@ -112,60 +118,161 @@ jobs: run: | # There can be several Python versions installed. We must use the exact one discovered by CMake. PYTHON=`grep -m1 'Python3_EXECUTABLE.*=' build/CMakeCache.txt | cut -d= -f2` - PYTHONPATH=$PYTHONPATH:$PREFIX/$($PYTHON -c "import sysconfig as sc; print(sc.get_path('platlib', 'posix_prefix', vars={'platbase': '.'}))") + export PYTHONPATH=$PYTHONPATH:$PREFIX/$($PYTHON "$GITHUB_WORKSPACE/repo/pyhook/get_pyhook_install_dir.py" "" "APPLE") $PYTHON tests/pyhook_smoketest.py + # Import a Developer ID certificate if one is configured. Without the + # MACOS_CERT_P12 secret this step is skipped and CODESIGN_IDENTITY stays + # unset, so the bundle is ad-hoc signed (launchable on arm64 via the + # Gatekeeper bypass). Populate these secrets to enable real signing (#5112). + - name: Import signing certificate + env: + MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }} + MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }} + MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }} + if: env.MACOS_CERT_P12 != '' + run: | + KEYCHAIN="$RUNNER_TEMP/build.keychain-db" + security create-keychain -p actions "$KEYCHAIN" + security set-keychain-settings -lut 3600 "$KEYCHAIN" + security unlock-keychain -p actions "$KEYCHAIN" + echo "$MACOS_CERT_P12" | base64 --decode > "$RUNNER_TEMP/cert.p12" + security import "$RUNNER_TEMP/cert.p12" -k "$KEYCHAIN" -P "$MACOS_CERT_PASSWORD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple: -s -k actions "$KEYCHAIN" + security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"') + echo "CODESIGN_IDENTITY=$MACOS_SIGN_IDENTITY" >> "$GITHUB_ENV" - name: Build App Bundle working-directory: repo/build run: ninja macbundle + # Notarize + staple the dmg when notarization credentials are configured. + # Skipped (no-op) otherwise. Uses an App Store Connect API key. + - name: Notarize and staple + working-directory: repo/build/osx + env: + NOTARY_API_KEY: ${{ secrets.NOTARY_API_KEY }} + NOTARY_KEY_ID: ${{ secrets.NOTARY_KEY_ID }} + NOTARY_ISSUER: ${{ secrets.NOTARY_ISSUER }} + if: env.NOTARY_API_KEY != '' + run: | + echo "$NOTARY_API_KEY" | base64 --decode > "$RUNNER_TEMP/notary.p8" + DMG=$(ls FontForge-*.dmg) + xcrun notarytool submit "$DMG" \ + --key "$RUNNER_TEMP/notary.p8" \ + --key-id "$NOTARY_KEY_ID" \ + --issuer "$NOTARY_ISSUER" \ + --wait + xcrun stapler staple "$DMG" - name: App Bundle artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: - name: appbundle + name: macOS_appbundle_${{ matrix.arch }} path: repo/build/osx/FontForge-*.dmg + windows: - runs-on: windows-latest + runs-on: ${{ matrix.runner }} env: MSYS2_FC_CACHE_SKIP: true strategy: matrix: - arch: ["MINGW32", "MINGW64"] + include: + - arch: UCRT64 + runner: windows-latest + - arch: CLANGARM64 + runner: windows-11-arm steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: path: repo - - name: Checkout FontForgeBuilds - uses: actions/checkout@v4 - with: - repository: fontforge/fontforgebuilds - ref: master-local - path: fontforgebuilds + fetch-depth: 0 - name: Setup MSYS2 uses: msys2/setup-msys2@v2 with: msystem: ${{ matrix.arch }} update: true install: git - - name: Run FontForgeBulds initialisation - working-directory: fontforgebuilds + - name: Install dependencies + working-directory: repo/Packaging/win shell: msys2 {0} + env: + BUILD_DIR: ${{ runner.temp }}/ffbuild run: ./ffbuild.sh --github-actions --depsonly - - name: Run FontForgeBuilds - working-directory: fontforgebuilds + - name: Build and package FontForge + working-directory: repo/Packaging/win shell: msys2 {0} + env: + BUILD_DIR: ${{ runner.temp }}/ffbuild run: | ./ffbuild.sh --github-actions FFPATH=`cygpath -m "$GITHUB_WORKSPACE/repo"` ./make-portable-package.sh ci - name: Run InnoSetup if: github.ref == 'refs/heads/master' - working-directory: fontforgebuilds/fontforge-setup - run: | - winget install --id JRSoftware.InnoSetup -e -s winget - $env:path += ";$env:LOCALAPPDATA/Programs/Inno Setup 6" - iscc -Qp fontforgesetup.iss -DMSYSTEM=${{ matrix.arch }} + working-directory: repo/Packaging/win/setup + env: + BUILD_DIR: ${{ runner.temp }}/ffbuild + run: iscc -Qp fontforgesetup.iss -DMSYSTEM=${{ matrix.arch }} -DBUILD_DIR="${{ runner.temp }}\ffbuild" - name: Build artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: - name: ${{ matrix.arch }} + name: Win_${{ matrix.arch }} path: | - fontforgebuilds/*.7z - fontforgebuilds/fontforge-setup/*.exe + repo/Packaging/win/*.7z + repo/Packaging/win/setup/*.exe + + # Test Python module build with MSVC toolchain + # This ensures compatibility with Python.org Windows builds (which use MSVC) + windows-msvc: + runs-on: windows-latest + env: + VCPKG_BINARY_SOURCES: "clear;files,${{ github.workspace }}/vcpkg-cache,readwrite" + steps: + - uses: actions/checkout@v6 + - name: Restore vcpkg binary cache + id: vcpkg-cache + uses: actions/cache/restore@v4 + with: + path: vcpkg-cache + key: vcpkg-files-${{ runner.os }}-${{ hashFiles('vcpkg.json') }} + restore-keys: | + vcpkg-files-${{ runner.os }}- + - name: Create vcpkg cache directory + run: New-Item -ItemType Directory -Force -Path vcpkg-cache + - name: Setup vcpkg + uses: lukka/run-vcpkg@v11 + with: + vcpkgJsonGlob: 'vcpkg.json' + - name: Setup MSVC + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Configure + run: | + $pythonExe = (Get-Command python).Source + Write-Host "Using Python: $pythonExe" + cmake -B build -G "NMake Makefiles" ` + -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" ` + -DCMAKE_BUILD_TYPE=Release ` + -DPython3_EXECUTABLE="$pythonExe" ` + -DENABLE_GUI=OFF ` + -DENABLE_PYTHON_SCRIPTING=ON ` + -DENABLE_PYTHON_EXTENSION=ON ` + -DENABLE_LIBSPIRO=OFF ` + -DENABLE_WOFF2=OFF ` + -DENABLE_DOCS=OFF ` + -DBUILD_SHARED_LIBS=ON + - name: Build + run: cmake --build build + - name: Run pyhook tests + run: | + # Copy DLLs to lib directory so Python can find them + Copy-Item build\bin\*.dll build\lib\ -Force + Copy-Item build\vcpkg_installed\x64-windows\bin\*.dll build\lib\ -Force -ErrorAction SilentlyContinue + ctest --test-dir build -R "_pyhook" --output-on-failure + - name: Save vcpkg binary cache + if: always() + uses: actions/cache/save@v4 + with: + path: vcpkg-cache + key: vcpkg-files-${{ runner.os }}-${{ hashFiles('vcpkg.json') }} diff --git a/.github/workflows/scripts/ffosxbuild.sh b/.github/workflows/scripts/ffosxbuild.sh index a7d5c2abec..0a20336adc 100755 --- a/.github/workflows/scripts/ffosxbuild.sh +++ b/.github/workflows/scripts/ffosxbuild.sh @@ -33,7 +33,7 @@ fi # Now we bundle the Python libraries echo "Bundling Python libraries..." -PY_DLLS_PATH=`$PYTHON -c "import sysconfig as sc; print(sc.get_path('platlib', 'posix_prefix', vars={'platbase': '.'}))"` +PY_DLLS_PATH=`$PYTHON "$GITHUB_WORKSPACE/repo/pyhook/get_pyhook_install_dir.py" "" "APPLE"` PYLIB=$(otool -L $APPDIR/Contents/Resources/opt/local/bin/fontforge | grep -i python | sed -e 's/ \(.*\)//') PYVER=$(echo $PYLIB | rev | cut -d/ -f2 | rev) @@ -63,6 +63,53 @@ popd mkdir -p $APPDIR/Contents/MacOS ln -s ../Frameworks/Python.framework/Versions/$PYVER/bin/$PYTHON "$APPDIR/Contents/MacOS/FFPython" +# --- Code signing ----------------------------------------------------------- +# This MUST run after lddx: its install_name_tool rewrites above invalidate the +# linker's ad-hoc signatures on the collected dylibs/modules. On Apple Silicon +# every Mach-O must carry a valid signature to load, so we re-sign everything. +# +# Defaults to ad-hoc ("-"), which makes the bundle launchable on arm64 with the +# usual Gatekeeper bypass (right-click -> Open). To produce a hardened-runtime, +# notarizable bundle instead, set CODESIGN_IDENTITY to a real +# "Developer ID Application: Name (TEAMID)" identity (see #5112); CI sets this +# automatically when signing secrets are present. +IDENTITY="${CODESIGN_IDENTITY:--}" +ENTITLEMENTS="${CODESIGN_ENTITLEMENTS:-$SCRIPT_BASE/../../../osx/entitlements.plist}" + +if [ "$IDENTITY" = "-" ]; then + echo "Ad-hoc signing the bundle..." + LIB_SIGN=(--force --timestamp=none --sign -) + APP_SIGN=(--force --timestamp=none --sign -) +else + echo "Signing the bundle with identity: $IDENTITY" + # Hardened runtime + secure timestamp are required for notarization. + # Entitlements (incl. disable-library-validation, so the embedded Python + # interpreter can load third-party C extensions) go on the main app only. + LIB_SIGN=(--force --options runtime --timestamp --sign "$IDENTITY") + APP_SIGN=(--force --options runtime --timestamp --sign "$IDENTITY" --entitlements "$ENTITLEMENTS") +fi + +# 1. Sign every nested Mach-O (dylibs, Python .so modules, helper executables). +# -type f skips the symlinks created above. +echo "Signing nested Mach-O files..." +find "$APPDIR" -type f | while read -r f; do + if file "$f" | grep -q "Mach-O"; then + codesign "${LIB_SIGN[@]}" "$f" + fi +done + +# 2. Sign the embedded Python framework version. +codesign "${LIB_SIGN[@]}" "$APPDIR/Contents/Frameworks/Python.framework/Versions/$PYVER" + +# 3. Sign the app bundle itself last (main executable receives entitlements). +codesign "${APP_SIGN[@]}" "$APPDIR" + +# Verify integrity. Do NOT use spctl here: it rejects ad-hoc signatures by +# design and would fail CI spuriously. notarytool (when enabled) is the real +# Gatekeeper-level check. +codesign --verify --strict --verbose=2 "$APPDIR" +# --------------------------------------------------------------------------- + # Package it up if [ ! -z "$CI" ]; then echo "Creating the dmg..." diff --git a/.github/workflows/scripts/setup_linux_deps.sh b/.github/workflows/scripts/setup_linux_deps.sh index c8b6be1e24..4ed551ee9f 100755 --- a/.github/workflows/scripts/setup_linux_deps.sh +++ b/.github/workflows/scripts/setup_linux_deps.sh @@ -20,7 +20,7 @@ echo "DEPSPREFIX=$DEPSPREFIX" >> $GITHUB_ENV echo "PATH=$PATH:$DEPSPREFIX/bin:$PREFIX/bin:~/.local/bin" >> $GITHUB_ENV echo "LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$DEPSPREFIX/lib:$PREFIX/lib" >> $GITHUB_ENV echo "PKG_CONFIG_PATH=$PKG_CONFIG_PATH:$DEPSPREFIX/lib/pkgconfig" >> $GITHUB_ENV -echo "PYTHONPATH=$PYTHONPATH:$PREFIX/$($PYTHON -c "import sysconfig as sc; print(sc.get_path('platlib', vars={'platbase': '.'}))")" >> $GITHUB_ENV +echo "PYTHONPATH=$PYTHONPATH:$PREFIX/$($PYTHON "$GITHUB_WORKSPACE/repo/pyhook/get_pyhook_install_dir.py" "${DESTDIR}${PREFIX}")" >> $GITHUB_ENV if [ ! -d deps/install ]; then echo "Custom dependencies not present - will build them" diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml new file mode 100644 index 0000000000..2dbc734a01 --- /dev/null +++ b/.github/workflows/wheels.yml @@ -0,0 +1,158 @@ +# Build Python wheels for PyPI distribution +# Triggered on YYYYMMDD version tags (e.g., 20260220) +# Wheels are built with CalVer version (e.g., 2026.2.20) + +name: Build Wheels + +on: + # Build on YYYYMMDD version tags + push: + tags: + - '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]' + # Allow manual trigger for testing + workflow_dispatch: + inputs: + debug: + description: 'Enable debug output' + required: false + default: false + type: boolean + +jobs: + build_wheels: + name: Build ${{ matrix.os }} ${{ matrix.arch }} wheels + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + # Linux x86_64 + - os: linux + arch: x86_64 + runner: ubuntu-latest + # Linux aarch64 (uses QEMU emulation) + - os: linux + arch: aarch64 + runner: ubuntu-latest + # macOS x86_64 + - os: macos + arch: x86_64 + runner: macos-13 + # macOS arm64 (native runner) + - os: macos + arch: arm64 + runner: macos-14 + # Windows x86_64 + - os: windows + arch: AMD64 + runner: windows-latest + + steps: + - uses: actions/checkout@v6 + + # Set up QEMU for Linux aarch64 emulation + - name: Set up QEMU + if: matrix.os == 'linux' && matrix.arch == 'aarch64' + uses: docker/setup-qemu-action@v3 + with: + platforms: arm64 + + - name: Build wheels + uses: pypa/cibuildwheel@v2.21 + env: + # Only build for the current architecture + CIBW_ARCHS: ${{ matrix.arch }} + # Debug output if requested + CIBW_BUILD_VERBOSITY: ${{ inputs.debug && '3' || '1' }} + + - name: Upload wheels + uses: actions/upload-artifact@v7 + with: + name: wheels-${{ matrix.os }}-${{ matrix.arch }} + path: ./wheelhouse/*.whl + + build_sdist: + name: Build source distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Build sdist + run: pipx run build --sdist + + - name: Upload sdist + uses: actions/upload-artifact@v7 + with: + name: sdist + path: dist/*.tar.gz + + # Test that the sdist can be installed from source + test_sdist: + name: Test source distribution + needs: build_sdist + runs-on: ubuntu-latest + steps: + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + cmake ninja-build \ + libfreetype-dev libxml2-dev zlib1g-dev \ + libpng-dev libjpeg-dev libtiff-dev + + - name: Download sdist + uses: actions/download-artifact@v4 + with: + name: sdist + + - name: Install from sdist + run: pip install fontforge-*.tar.gz -v + + - name: Test import + run: python -c "import fontforge; import psMat; print('fontforge version:', fontforge.version())" + + # Combine all artifacts into a single download + combine_artifacts: + name: Combine artifacts + needs: [build_wheels, build_sdist, test_sdist] + runs-on: ubuntu-latest + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + + - name: List artifacts + run: ls -la dist/ + + - name: Upload combined artifacts + uses: actions/upload-artifact@v7 + with: + name: all-packages + path: dist/ + + # PyPI upload - DISABLED for now + # Uncomment and configure trusted publishing when ready + # See: https://docs.pypi.org/trusted-publishers/ + # + # publish: + # name: Publish to PyPI + # needs: [build_wheels, build_sdist] + # runs-on: ubuntu-latest + # # Only publish on tag pushes + # if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + # environment: + # name: pypi + # url: https://pypi.org/p/fontforge + # permissions: + # id-token: write # Required for trusted publishing + # steps: + # - name: Download all artifacts + # uses: actions/download-artifact@v4 + # with: + # path: dist + # merge-multiple: true + # + # - name: Publish to PyPI + # uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index 8f745bf771..7569fa431a 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ tags __pycache__ *.py[co] +build-cpp/ diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000000..ac4497f706 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,16 @@ +# ReadTheDocs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html + +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +sphinx: + configuration: doc/sphinx/conf.py + +python: + install: + - requirements: doc/sphinx/requirements.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index 7280ea8ad4..215d90ba83 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,27 @@ cmake_minimum_required(VERSION 3.5) # Update the version for each new release project(fontforge VERSION 20251009 LANGUAGES C CXX) +# Detect if we're being built by scikit-build-core for wheel packaging +# SKBUILD is set automatically by scikit-build-core +if(DEFINED SKBUILD) + set(BUILDING_WHEEL TRUE) + message(STATUS "Building for Python wheel (detected scikit-build-core)") +else() + set(BUILDING_WHEEL FALSE) +endif() + +# Generate Python-compatible CalVer version from YYYYMMDD format +# 20251009 -> 2025.10.9 +string(SUBSTRING "${PROJECT_VERSION}" 0 4 _FF_YEAR) +string(SUBSTRING "${PROJECT_VERSION}" 4 2 _FF_MONTH) +string(SUBSTRING "${PROJECT_VERSION}" 6 2 _FF_DAY) +math(EXPR _FF_MONTH_INT "${_FF_MONTH}") +math(EXPR _FF_DAY_INT "${_FF_DAY}") +set(FONTFORGE_PYTHON_VERSION "${_FF_YEAR}.${_FF_MONTH_INT}.${_FF_DAY_INT}") +if(BUILDING_WHEEL) + message(STATUS "Python wheel version: ${FONTFORGE_PYTHON_VERSION}") +endif() + # No in source builds if("${PROJECT_SOURCE_DIR}" STREQUAL "${PROJECT_BINARY_DIR}") message(FATAL_ERROR "In-source builds are disallowed. Create a build folder to run CMake from.") @@ -56,6 +77,7 @@ build_option(ENABLE_LIBPNG AUTO ON "Enables PNG support") build_option(ENABLE_LIBREADLINE AUTO ON "Enables Readline support") build_option(ENABLE_LIBTIFF AUTO ON "Enables TIFF support") build_option(ENABLE_WOFF2 AUTO ON "Enables WOFF2 support") +build_option(ENABLE_LIBUNIBREAK AUTO ON "Enables libunibreak for Unicode line breaking") build_option(ENABLE_HARFBUZZ AUTO ON "Enables HarfBuzz shaper support") build_option(ENABLE_DOCS AUTO ON "Enables building and installing documentation. Sphinx is required to build it.") @@ -105,14 +127,19 @@ set_font_family(label "${FONT_FAMILIES_LABEL}") set_font_family(mono "${FONT_FAMILIES_MONO}") # Required and default dependencies -# Note: When adding a dependency, ensure it has an imported target (wherever appropriate) +# Note: When adding a dependency, ensure it has an imported target (wherever appropriate) find_package(Freetype 2.3.7 REQUIRED) -find_package(Gettext REQUIRED) -find_package_with_target(Intl REQUIRED) -if(UNIX AND NOT APPLE) - find_package(GLIB 2.6 REQUIRED COMPONENTS gio gobject) -else() - find_package(GLIB 2.58 REQUIRED COMPONENTS gio gobject) +# Gettext/Intl and GLIB only needed for GUI builds +if(ENABLE_GUI) + find_package(PkgConfig REQUIRED) + find_package(Gettext REQUIRED) + find_package_with_target(Intl REQUIRED) + if(UNIX AND NOT APPLE) + find_package(GLIB 2.6 REQUIRED COMPONENTS gio gobject) + else() + find_package(GLIB 2.58 REQUIRED COMPONENTS gio gobject) + endif() + pkg_check_modules(GLibMM REQUIRED IMPORTED_TARGET glibmm-2.4) endif() find_package(Iconv REQUIRED) find_package(LibXml2 REQUIRED) @@ -124,13 +151,34 @@ if(HAVE_PTHREAD_H) find_package(Threads) endif() +if (ENABLE_HARFBUZZ) + unset(_harfbuzz_required) + if(NOT ${ENABLE_HARFBUZZ} STREQUAL "AUTO") + set(_harfbuzz_required REQUIRED) + endif() + + if(MSVC) + find_package(harfbuzz CONFIG ${_harfbuzz_required}) + set(ENABLE_HARFBUZZ_RESULT ${harfbuzz_FOUND}) + if(harfbuzz_FOUND) + # Can't use ALIAS because harfbuzz::harfbuzz is itself an alias + add_library(PkgConfig::HarfBuzz INTERFACE IMPORTED) + target_link_libraries(PkgConfig::HarfBuzz INTERFACE harfbuzz::harfbuzz) + endif() + else() + find_package(PkgConfig REQUIRED) + pkg_check_modules(HarfBuzz ${_harfbuzz_required} IMPORTED_TARGET harfbuzz) + set(ENABLE_HARFBUZZ_RESULT ${HarfBuzz_FOUND}) + endif() +endif() + # Requirements for options if(ENABLE_GUI) - find_package(GDK3 3.10 REQUIRED) - find_package(PkgConfig) + pkg_check_modules(GDK3 REQUIRED IMPORTED_TARGET gdk-3.0>=3.10) + include(${CMAKE_CURRENT_LIST_DIR}/cmake/TargetUtils.cmake) + alias_imported_target(GDK3::GDK3 PkgConfig::GDK3) pkg_check_modules(GTK3 REQUIRED IMPORTED_TARGET gtk+-3.0) - pkg_check_modules(GTKMM REQUIRED IMPORTED_TARGET gtkmm-3.0) - find_package(PkgConfig) + pkg_check_modules(GTKMM REQUIRED IMPORTED_TARGET gtkmm-3.0) if (ENABLE_HARFBUZZ) pkg_check_modules(HarfBuzz REQUIRED IMPORTED_TARGET harfbuzz) endif() @@ -149,25 +197,36 @@ set(Python3_FIND_REGISTRY LAST) if(BUILD_SHARED_LIBS) set(Python3_USE_STATIC_LIBS FALSE) endif() -find_package_auto(ENABLE_PYTHON_SCRIPTING Python3 3.8 COMPONENTS Development Interpreter) +# Wheel builds only need Development.Module (not Embed) +# Full builds need Development (Module + Embed) for fontforgeexe +if(BUILDING_WHEEL) + find_package_auto(ENABLE_PYTHON_SCRIPTING Python3 3.8 COMPONENTS Development.Module Interpreter) +else() + find_package_auto(ENABLE_PYTHON_SCRIPTING Python3 3.8 COMPONENTS Development Interpreter) +endif() find_package_auto(ENABLE_DOCS Sphinx) # Should come after ENABLE_PYTHON_SCRIPTING find_package_auto(ENABLE_LIBSPIRO Libspiro) find_package_auto(ENABLE_LIBGIF GIF) find_package_auto(ENABLE_LIBJPEG JPEG) find_package_auto(ENABLE_LIBPNG PNG) -find_package_auto(ENABLE_LIBREADLINE Readline) +find_package(PkgConfig) # Needed for pkg_check_auto calls below +pkg_check_auto(ENABLE_LIBREADLINE Readline readline) find_package_auto(ENABLE_LIBTIFF TIFF) -find_package_auto(ENABLE_WOFF2 WOFF2) +pkg_check_auto(ENABLE_WOFF2 WOFF2 "libwoff2enc libwoff2dec") +pkg_check_auto(ENABLE_LIBUNIBREAK Libunibreak libunibreak) find_package_auto(ENABLE_CODE_COVERAGE Gcov) add_subdirectory(inc) +add_subdirectory(extern) add_subdirectory(Unicode) add_subdirectory(gutils) add_subdirectory(fontforge) if(ENABLE_GUI) add_subdirectory(gdraw) endif() -add_subdirectory(fontforgeexe) +if(NOT BUILDING_WHEEL) + add_subdirectory(fontforgeexe) +endif() if(ENABLE_PYTHON_EXTENSION) if(ENABLE_PYTHON_SCRIPTING_RESULT AND BUILD_SHARED_LIBS) set(ENABLE_PYTHON_EXTENSION_RESULT ON) @@ -181,8 +240,10 @@ if(ENABLE_PYTHON_EXTENSION) add_subdirectory(pyhook) endif() endif() -add_subdirectory(contrib) -if(ENABLE_PYTHON_SCRIPTING_RESULT) +if(NOT BUILDING_WHEEL) + add_subdirectory(contrib) +endif() +if(ENABLE_PYTHON_SCRIPTING_RESULT AND NOT BUILDING_WHEEL) add_subdirectory(pycontrib) endif() if(BUILD_TESTING) @@ -191,14 +252,25 @@ endif() if(ENABLE_GUI) add_subdirectory(desktop) endif() -if(ENABLE_DOCS AND Sphinx_FOUND) +if(ENABLE_DOCS AND Sphinx_FOUND AND NOT BUILDING_WHEEL) add_subdirectory(doc) endif() -add_subdirectory(po) -add_subdirectory(share) -if(APPLE) +if(NOT BUILDING_WHEEL AND ENABLE_GUI) + add_subdirectory(po) +endif() +if(NOT BUILDING_WHEEL) + add_subdirectory(share) +endif() +if(APPLE AND ENABLE_GUI) add_subdirectory(osx) endif() setup_cpack() print_build_options() + +add_custom_target( + format + COMMAND cat .clang-files | xargs -I % sh -c 'clang-format --verbose -i --style=file:.clang-format %' + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMENT "Running clang-format" +) diff --git a/INSTALL.md b/INSTALL.md index 3595a0e7a1..bc3dfc8520 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -6,7 +6,7 @@ The exact method to do this depends on your OS distribution. To download all dependencies on Ubuntu, run: ```sh -sudo apt-get install libjpeg-dev libtiff5-dev libpng-dev libfreetype-dev libgif-dev libgtk-3-dev libxml2-dev libpango1.0-dev libcairo2-dev libspiro-dev libwoff-dev python3-dev ninja-build cmake build-essential gettext; +sudo apt-get install libjpeg-dev libtiff5-dev libpng-dev libfreetype-dev libgif-dev libgtk-3-dev libgtkmm-3.0-dev libxml2-dev libpango1.0-dev libcairo2-dev libspiro-dev libwoff-dev python3-dev ninja-build cmake build-essential gettext; ``` Now run the build and installation scripts: @@ -22,6 +22,16 @@ sudo ninja install For even more details on the build system itself, see [this wiki page](https://github.com/fontforge/fontforge/wiki/CMake-guide-for-FontForge). +## Python Module + +FontForge's Python bindings are also available directly from PyPI: + +```sh +pip install fontforge +``` + +If you install the Python module via pip, building and installing FontForge from source will detect this and skip installing the Python module to avoid conflicts. To switch back to app-installed modules, first run `pip uninstall fontforge`. + **Attention, Designers Who Love TrueType Hinting:** Run `cmake` with `-DENABLE_FREETYPE_DEBUGGER=/path/to/freetype/source`. This option enables advanced features for debugging TrueType font hints, such as stepping through hinting instructions one by one. diff --git a/Packaging/win/README.md b/Packaging/win/README.md new file mode 100644 index 0000000000..6d29ff01db --- /dev/null +++ b/Packaging/win/README.md @@ -0,0 +1,56 @@ +# Windows Build Scripts + +This directory contains scripts for building FontForge on Windows using MSYS2/MinGW-w64. + +## Directory Structure + +- `ffbuild.sh` - Main build script that installs dependencies and compiles FontForge +- `make-portable-package.sh` - Creates portable 7z archives +- `strip-python.sh` - Prepares a stripped-down Python distribution for bundling +- `launcher/` - Source for the `run_fontforge.exe` wrapper and icons +- `runtime/` - Template files copied into the release package (batch scripts, fontconfig) +- `setup/` - Inno Setup installer configuration +- `debug/` - Debug utilities (GDB scripts) + +## Building Locally + +Requirements: MSYS2 with UCRT64 or CLANGARM64 environment. + +```bash +# From MSYS2 UCRT64 or CLANGARM64 shell: +cd Packaging/win + +# Install dependencies only +./ffbuild.sh --depsonly + +# Full build +./ffbuild.sh + +# Create portable package +./make-portable-package.sh +``` + +By default, build outputs go to `Packaging/win/build/`. You can override this +with the `BUILD_DIR` environment variable: + +```bash +BUILD_DIR=/tmp/ffbuild ./ffbuild.sh +``` + +The build creates `$BUILD_DIR/ReleasePackage/` containing the distributable files. + +## CI/CD + +The GitHub Actions workflow (`.github/workflows/main.yml`) automates this process, +building for both x64 (UCRT64) and ARM64 (CLANGARM64) architectures. + +## Credits + +These scripts were originally developed by Jeremy Tan as the separate +[fontforgebuilds](https://github.com/fontforge/fontforgebuilds) repository, +based on earlier work by Matthew Petroff. + +## License + +- Build scripts (`ffbuild.sh`, etc.): BSD 2-clause license +- Icons and graphics by Matthew Petroff: Creative Commons Attribution 3.0 Unported diff --git a/Packaging/win/debug/ffdebugscript.txt b/Packaging/win/debug/ffdebugscript.txt new file mode 100644 index 0000000000..5488a8fe8b --- /dev/null +++ b/Packaging/win/debug/ffdebugscript.txt @@ -0,0 +1,18 @@ +run +bt +print "locals..." +info locals +up 1 +info locals +up 1 +info locals +up 1 +info locals +up 1 +info locals +up 1 +info locals +up 1 +info locals +up 1 +q \ No newline at end of file diff --git a/Packaging/win/debug/fontforge-debug.bat b/Packaging/win/debug/fontforge-debug.bat new file mode 100644 index 0000000000..115a93d437 --- /dev/null +++ b/Packaging/win/debug/fontforge-debug.bat @@ -0,0 +1,37 @@ +@ECHO OFF +set "FF=%~dp0" +set FF_XPORT=11 +set DISPLAY=127.0.0.1:%FF_XPORT%.0 +set "XLOCALEDIR=%FF%bin\VcXsrv\locale" +set "XLOCALELIBDIR=%XLOCALEDIR%" +set AUTOTRACE=potrace +set "HOME=%FF%" +set FF_PORTABLE=TRUE + +::Set this item to enable experimental Unicode filename support +::set LC_ALL=C.UTF-8 + +::Set this to your language code to change the FontForge UI language +::See share/locale/ for a list of supported language codes +::set LANGUAGE=en + +::Only add to path once +if not defined FF_PATH_ADDED ( +set "PATH=%FF%;%FF%\bin;%PATH:"=%" +set FF_PATH_ADDED=TRUE +) + +echo Running FontForge in debug mode, expect it to be slower! + +"%FF%\bin\VcXsrv_util.exe" -exists || ( +start /B "" "%FF%\bin\VcXsrv\vcxsrv.exe" :%FF_XPORT% -multiwindow -clipboard -silent-dup-error +) + +"%FF%\bin\VcXsrv_util.exe" -wait + +"%FF%\bin\gdb.exe" --batch --command="%FF%\ffdebugscript.txt" --args "%FF%\bin\fontforge.exe" -nosplash %* 2>&1 | wtee "%TEMP%\FontForge-Debug-Output.txt" +explorer "%TEMP%\FontForge-Debug-Output.txt" + +"%FF%\bin\VcXsrv_util.exe" -close +:: bye + diff --git a/Packaging/win/ffbuild.sh b/Packaging/win/ffbuild.sh new file mode 100644 index 0000000000..48ee71a096 --- /dev/null +++ b/Packaging/win/ffbuild.sh @@ -0,0 +1,569 @@ +#!/bin/bash +# FontForge build script. +# Uses MSYS2/MinGW-w64 +# Author: Jeremy Tan +# This script retrieves and installs all libraries required to build FontForge. +# It then attempts to compile the latest version of FontForge, and to +# subsequently make a redistributable package. + +reconfigure=0 +nomake=0 +yes=0 +makedebug=0 +appveyor=0 +ghactions=0 +depsonly=0 +qt=0 +github="fontforge" + +function dohelp() { + echo "Usage: `basename $0` [options]" + echo " -h, --help Prints this help message" + echo " -g, --github Use forked FontForge repository by github " + echo " -r, --reconfigure Forces the configure script to be rerun for the currently" + echo " worked-on package." + echo " -n, --nomake Don't make/make install FontForge but do everything else" + echo " -y, --yes Say yes to all build script prompts" + echo " -d, --makedebug Adds in debugging utilities into the build (adds a gdb" + echo " automation script)" + echo " -t, --qt Builds the UI using Qt instead of GDK" + echo " -a, --appveyor AppVeyor specific settings (in-source build)" + echo " --github-actions GitHub Actions specific settings (in-source build)" + echo " -l, --depsonly Only install the dependencies and not FontForge itself." + exit $1 +} + +# Colourful text +# Red text +function log_error() { + echo -ne "\e[31m"; echo "$@"; echo -ne "\e[0m" +} + +# Yellow text +function log_status() { + echo -ne "\e[33m"; echo "$@"; echo -ne "\e[0m" +} + +# Green text +function log_note() { + echo -ne "\e[32m"; echo "$@"; echo -ne "\e[0m" +} + +function bail () { + echo -ne "\e[31m\e[1m"; echo "!!! Build failed at: ${@}"; echo -ne "\e[0m" + exit 1 +} + +function detect_arch_switch () { + mkdir -p "$BUILD_DIR" + local from="$(ls -1 "$BUILD_DIR"/.building-* 2>/dev/null)" + local to="$BUILD_DIR/.building-$1" + + if [ ! -z "$from" ] && [ "$from" != "$to" ]; then + if (($yes)); then + rm -rf "$RELEASE" || bail "Could not reset ReleasePackage" + else + read -p "Architecture change detected! ReleasePackage must be reset. Continue? [y/N]: " arch_confirm + case $arch_confirm in + [Yy]* ) + rm -rf "$RELEASE" || bail "Could not reset ReleasePackage" + rm -rf "$DBSYMBOLS" + mkdir -p "$DBSYMBOLS" + ;; + * ) bail "Not overwriting ReleasePackage" ;; + esac + fi + fi + + rm -f $from + touch $to +} + +function get_archive() { + local archive=$1 + local url=$2 + local url2=$3 + + if [ ! -f "$archive" ]; then + log_note "$archive does not exist, downloading from $url" + wget --tries 4 "$url" -O "$archive" || ([ ! -z "$url2" ] && wget --tries 4 "$url2" -O "$archive") + fi +} + +# Preamble +log_note "MSYS2 FontForge build script..." + +# Retrieve input arguments to script +optspec=":hrnydalg-:" +while getopts "$optspec" optchar; do + case "${optchar}" in + -) + case "${OPTARG}" in + reconfigure) + reconfigure=$((1-reconfigure)) ;; + nomake) + nomake=$((1-nomake)) ;; + makedebug) + makedebug=$((1-makedebug)) ;; + appveyor) + appveyor=$((1-appveyor)) ;; + github-actions) + ghactions=$((1-ghactions)) ;; + depsonly) + depsonly=$((1-depsonly)) ;; + yes) + yes=$((1-yes)) ;; + qt) + qt=$((1-qt)) ;; + github) + github="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 )) ;; + help) + dohelp 0;; + *) + log_error "Unknown option --${OPTARG}" + dohelp 1 ;; + esac;; + r) + reconfigure=$((1-reconfigure)) ;; + n) + nomake=$((1-nomake)) ;; + d) + makedebug=$((1-makedebug)) ;; + a) + appveyor=$((1-appveyor)) ;; + l) + depsonly=$((1-depsonly)) ;; + y) + yes=$((1-yes)) ;; + t) + qt=$((1-qt)) ;; + g) + github="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 )) ;; + h) + dohelp 0 ;; + *) + log_error "Unknown argument -${OPTARG}" + dohelp 1 ;; + esac +done + +# Set working folders +BASE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +BUILD_DIR=${BUILD_DIR:-$(cd "$BASE/../.." && pwd)/build} +# Convert Windows paths to Unix paths if needed (for MSYS2 compatibility) +if command -v cygpath &> /dev/null; then + BUILD_DIR=$(cygpath -u "$BUILD_DIR") +fi + +# Source directories (in repo) +LAUNCHER=$BASE/launcher/ +RUNTIME=$BASE/runtime/ +DEBUGFILES=$BASE/debug/ + +# Build directories (ephemeral) +SOURCE=$BUILD_DIR/downloads/ +BINARY=$BUILD_DIR/downloads/ +RELEASE=$BUILD_DIR/ReleasePackage/ +DBSYMBOLS=$BUILD_DIR/debugging-symbols/.debug/ + +# Determine if we're building x86 or arm64 bit. +if [ "$MSYSTEM" = "UCRT64" ]; then + log_note "Building x86 64-bit ucrt version!" + + ARCHNUM="64" + MINGVER=ucrt64 + PMARCH=ucrt-x86_64 +elif [ "$MSYSTEM" = "CLANGARM64" ]; then + log_note "Building ARM 64-bit version!" + + ARCHNUM="64" + MINGVER=clangarm64 + PMARCH=clang-aarch64 +else + bail "Unknown build system!" +fi +export PMPREFIX="mingw-w64-$PMARCH" + +# Early detection +detect_arch_switch $MINGVER + +# Common options +TARGET=$BUILD_DIR/target/$MINGVER/ +WORK=$BUILD_DIR/work/$MINGVER/ +PMTEST="$BUILD_DIR/.pacman-$MINGVER-installed" +PYINST=python + +# Check for AppVeyor specific settings +if (($appveyor)); then + echo "AppVeyor build settings enabled!" + yes=1 + TAR="tar axf" + export PYTHONHOME=/$MINGVER + FFPATH=`cygpath -m $APPVEYOR_BUILD_FOLDER` +elif (($ghactions)); then + echo "GitHub Actions build settings enabled!" + yes=1 + TAR="tar axf" + export PYTHONHOME=/$MINGVER + FFPATH=`cygpath -m $GITHUB_WORKSPACE/repo` +else + echo "Local build settings enabled!" + FFPATH=$(cd "$BASE/../.." && pwd) + TAR="tar axvf" +fi + +# Make the output directories +mkdir -p "$BUILD_DIR" +mkdir -p "$SOURCE" +mkdir -p "$BINARY" +mkdir -p "$WORK" +mkdir -p "$RELEASE/bin" +mkdir -p "$RELEASE/lib" +mkdir -p "$RELEASE/share/glib-2.0" +mkdir -p "$DBSYMBOLS" + +# Set pkg-config path to also search mingw libs +export PATH="$TARGET/bin:$PATH" +export PKG_CONFIG_PATH="/$MINGVER/lib/pkgconfig:/usr/local/lib/pkgconfig:/lib/pkgconfig:/usr/local/share/pkgconfig" +# aclocal path +export ACLOCAL_PATH="m4:/$MINGVER/share/aclocal" +export ACLOCAL="/bin/aclocal" +export M4="/bin/m4" +# Compiler flags +export LDFLAGS="-L/$MINGVER/lib -L/usr/local/lib -L/lib -Wl,--allow-multiple-definition" +export CFLAGS="-DWIN32 -I/$MINGVER/include -I/usr/local/include -I/include -g" +export CPPFLAGS="${CFLAGS}" +export LIBS="" + +# Install all the available precompiled binaries +if (( ! $nomake )) && [ ! -f $PMTEST ]; then + log_status "First time run; installing MSYS and MinGW libraries..." + + pacman -Syy --noconfirm + + IOPTS="-S --noconfirm --needed" + + if (( $appveyor+$ghactions )); then + # Try to fix python issues + pacman -Rcns --noconfirm $PMPREFIX-python3 + # Upgrade gcc + pacman $IOPTS --nodeps $PMPREFIX-{gcc,gcc-libs} + fi + + # Install the base MSYS packages needed + pacman $IOPTS diffutils findutils make patch tar pkgconf winpty + + # Install MinGW related stuff + pacman $IOPTS $PMPREFIX-{gcc,gmp,ntldd-git,gettext,libiconv,cmake,ninja,ccache} + + ## Other libs + pacman $IOPTS $PMPREFIX-$PYINST + pacman $IOPTS $PMPREFIX-$PYINST-pip + pacman $IOPTS $PMPREFIX-$PYINST-setuptools + + log_status "Installing precompiled devel libraries..." + # Libraries + pacman $IOPTS $PMPREFIX-libspiro + pacman $IOPTS $PMPREFIX-libuninameslist + pacman $IOPTS $PMPREFIX-{zlib,libpng,giflib,libtiff,libjpeg-turbo,libxml2,potrace} + pacman $IOPTS $PMPREFIX-{freetype,fontconfig,glib2,pixman,harfbuzz,woff2} + + if (($qt)); then + pacman $IOPTS $PMPREFIX-{qt6,qt6-tools} + else + pacman $IOPTS $PMPREFIX-{gtk3,gtkmm3} + fi + + touch $PMTEST + log_note "Finished installing precompiled libraries!" +else + log_note "Detected that precompiled libraries are already installed." + log_note " Delete '$PMTEST' and run this script again if" + log_note " this is not the case." +fi # pacman installed + +FREETYPE_VERSION="$(pacman -Qi $PMPREFIX-freetype | awk '/Version/{print $3}' | cut -d- -f1)" +FREETYPE_NAME="freetype-${FREETYPE_VERSION}" +FREETYPE_ARCHIVE="${FREETYPE_NAME}.tar.xz" +if [ -z "$FREETYPE_VERSION" ]; then + bail "Failed to infer the installed FreeType version" +fi +log_note "Inferred installed FreeType version as $FREETYPE_VERSION" + +PYVER="$(pacman -Qi $PMPREFIX-${PYINST} | awk '/Version/{print $3}' | cut -d- -f1 | cut -d. -f1-2)" +if [ -z "$PYVER" ]; then + bail "Failed to infer the installed Python version" +fi +PYVER="python${PYVER}" +log_note "Inferred installed Python version as $PYVER" + +log_status "Retrieving supplementary archives (if necessary)" +get_archive "$SOURCE/$FREETYPE_ARCHIVE" \ + "http://download.savannah.gnu.org/releases/freetype/$FREETYPE_ARCHIVE" \ + "https://sourceforge.net/projects/freetype/files/freetype2/${FREETYPE_VERSION}/${FREETYPE_ARCHIVE}" || bail "FreeType2 archive retreival" + +cd $WORK + +# run_fontforge, TODO - rethink this, now that gdk is used +if [ ! -f run_fontforge/run_fontforge.complete ]; then + log_status "Building run_fontforge..." + mkdir -p run_fontforge + cd run_fontforge + windres "$LAUNCHER/run_fontforge.rc" -O coff -o run_fontforge.res + gcc -Wall -Werror -pedantic -std=c99 -O2 -mwindows -municode -o run_fontforge.exe "$LAUNCHER/run_fontforge.c" run_fontforge.res \ + || bail "run_fontforge" + touch run_fontforge.complete + cd .. +fi + +if (($depsonly)); then + log_note "Installation of dependencies complete." + exit 0 +fi + +if (( ! $nomake )); then + # For the source only; to enable the debugger in FontForge + if [ ! -d "$WORK/$FREETYPE_NAME" ]; then + log_status "Extracting the FreeType $FREETYPE_VERSION source to $WORK..." + tar -C "$WORK" -axf "$SOURCE/$FREETYPE_ARCHIVE" || bail "FreeType extraction" + fi + + log_status "Finished installing prerequisites, proceeding to build FontForge!" + cd "$FFPATH"; + + if [ -f CMakeLists.txt ]; then + if [ ! -f "$BUILD_DIR/fontforge.configure-complete" ] || (($reconfigure)); then + log_status "Running the configure script..." + + if (($appveyor+$ghactions)); then + EXTRA_CMAKE_OPTS="-DENABLE_FONTFORGE_EXTRAS=yes -DENABLE_DOCS=yes -DSPHINX_USE_VENV=yes" + else + log_note "Will use ccache when building FontForge" + EXTRA_CMAKE_OPTS="-DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_BUILD_TYPE=Debug" + fi + + if (($qt)); then + EXTRA_CMAKE_OPTS="-DENABLE_QT=yes" + fi + + mkdir -p "$BUILD_DIR" && cd "$BUILD_DIR" + time cmake -GNinja \ + -DCMAKE_INSTALL_PREFIX="$TARGET" \ + -DENABLE_FREETYPE_DEBUGGER="$WORK/$FREETYPE_NAME" \ + -DENABLE_LIBREADLINE=no \ + -DPython3_EXECUTABLE=/$MINGVER/bin/python.exe \ + $EXTRA_CMAKE_OPTS \ + "$FFPATH" \ + || bail "FontForge configure" + touch fontforge.configure-complete + cd "$FFPATH" + fi + + log_status "Compiling FontForge..." + time ninja -C "$BUILD_DIR" || bail "FontForge build" + + if (($appveyor+$ghactions)); then + log_status "Running the test suite..." + CTEST_PARALLEL_LEVEL=100 ninja -C "$BUILD_DIR" check || bail "FontForge check" + fi + + log_status "Installing FontForge..." + ninja -C "$BUILD_DIR" install || bail "FontForge install" + else + if [ ! -f fontforge.configure-complete ] || (($reconfigure)); then + log_status "Running the configure script..." + + if [ ! -f configure ]; then + log_note "No configure script detected; running ./boostrap..." + ./bootstrap --force || bail "FontForge autogen" + fi + + # libreadline is disabled because it causes issues when used from the command line (e.g Ctrl+C doesn't work) + # windows-cross-compile to disable check for libuuid + # gdi32 linking is needed for AddFontResourceEx + LIBS="${LIBS} -lgdi32" \ + PYTHON=$PYINST \ + time ./configure \ + --prefix $TARGET \ + --enable-static \ + --enable-shared \ + --enable-gdk=gdk3 \ + --with-freetype-source="$WORK/$FREETYPE_NAME" \ + --without-libreadline \ + --enable-fontforge-extras \ + --enable-woff2 \ + || bail "FontForge configure" + touch fontforge.configure-complete + fi + + log_status "Compiling FontForge..." + time make -j$(($(nproc)+1)) || bail "FontForge make" + + if (($appveyor+$ghactions)); then + log_status "Running the test suite..." + make check -j$(($(nproc)+1)) || bail "FontForge check" + fi + + log_status "Installing FontForge..." + make -j$(($(nproc)+1)) install || bail "FontForge install" + fi +fi + +log_status "Assembling the release package..." + +log_status "Copying runtime templates..." +cp -f "$RUNTIME/fontforge.bat" "$RELEASE/" +cp -f "$RUNTIME/fontforge-console.bat" "$RELEASE/" +cp -f "$RUNTIME/CHANGELOG.txt" "$RELEASE/" + +log_status "Copying glib spawn helpers..." +strip "/$MINGVER/bin/gspawn-win$ARCHNUM-helper.exe" -so "$RELEASE/bin/gspawn-win$ARCHNUM-helper.exe" || bail "Glib spawn helper not found!" +strip "/$MINGVER/bin/gspawn-win$ARCHNUM-helper-console.exe" -so "$RELEASE/bin/gspawn-win$ARCHNUM-helper-console.exe" || bail "Glib spawn helper not found!" + +log_status "Copying the shared folder of FontForge..." +cp -rf $TARGET/share/fontforge "$RELEASE/share/" +cp -rf $TARGET/share/locale "$RELEASE/share/" +rm -f "$RELEASE/share/prefs" + +log_note "Installing custom binaries..." +cd $WORK + +# potrace - http://potrace.sourceforge.net/#downloading +if [ ! -f $RELEASE/bin/potrace.exe ]; then + log_status "Installing potrace..." + strip "/$MINGVER/bin/potrace.exe" -so $RELEASE/bin/potrace.exe +fi + + +log_status "Installing run_fontforge..." +strip $WORK/run_fontforge/run_fontforge.exe -so "$RELEASE/run_fontforge.exe" \ + || bail "run_fontforge" + +log_status "Copying UI fonts..." +#Remove the old/outdated Inconsolata/Cantarell from pixmaps +rm "$RELEASE/share/fontforge/pixmaps/"*.ttf > /dev/null 2>&1 +rm "$RELEASE/share/fontforge/pixmaps/"*.otf > /dev/null 2>&1 +#Copy standard fonts +mkdir -p "$RELEASE/share/fonts" +cp $TARGET/share/fontforge/pixmaps/Cantarell* "$RELEASE/share/fonts" 2>/dev/null || true + +log_status "Copying sfd icon..." +cp "$LAUNCHER/sfd-icon.ico" "$RELEASE/share/fontforge/" + +log_status "Copying the Python executable and libraries..." +# Name the python binary to something custom to avoid clobbering any Python installation that the user already has +strip "/$MINGVER/bin/$PYINST.exe" -so "$RELEASE/bin/ffpython.exe" +cd $BASE +if [ -d "$RELEASE/lib/$PYVER" ]; then + log_note "Skipping python library copy because folder already exists, and copying is slow." +else + if [ ! -d "$BINARY/$PYVER" ]; then + log_note "Python folder not found - running 'strip-python'..." + export BUILD_DIR + $BASE/strip-python.sh "$PYVER" || bail "Python generation failed" + fi + cp -r "$BINARY/$PYVER" "$RELEASE/lib" || bail "Python folder could not be copied" +fi +if [ -d "$RELEASE/lib/tcl8.6" ]; then + log_note "Skipping copying tcl/tk folder because it already exists" +else + log_status "Copying tcl/tk..." + cp -r /$MINGVER/lib/{tcl,tk}8.* "$RELEASE/lib/" || bail "Couldn't copy tcl/tk" +fi + +if [ -d "$RELEASE/lib/gdk-pixbuf-2.0" ]; then + log_note "Skipping copying gdk-pixbuf folder because it already exists" +else + log_status "Copying gdk-pixbuf..." + cp -r /$MINGVER/lib/gdk-pixbuf-2.0 "$RELEASE/lib/" || bail "Couldn't copy gdk-pixbuf" +fi + +if [ -d "$RELEASE/share/glib-2.0/schemas" ]; then + log_note "Skipping copying schemas folder because it already exists" +else + log_status "Copying schemas..." + cp -r /$MINGVER/share/glib-2.0/schemas "$RELEASE/share/glib-2.0/" || bail "Couldn't copy schemas" +fi + +cd $WORK + +log_status "Stripping Python cache files (*.pyc,*.pyo,__pycache__)..." +find "$RELEASE/lib/$PYVER" -regextype sed -regex ".*\.py[co]" | xargs rm -rfv +find "$RELEASE/lib/$PYVER" -name "__pycache__" | xargs rm -rfv + +PY_DLLS_SRC_PATH=`/$MINGVER/bin/python.exe "$BASE/../../pyhook/get_pyhook_install_dir.py" "${DESTDIR}${TARGET}"` + +log_status "Copying the Python extension dlls..." +cp -f "$TARGET/$PY_DLLS_SRC_PATH/fontforge.pyd" "$RELEASE/lib/$PYVER/site-packages/" || bail "Couldn't copy pyhook dlls" +cp -f "$TARGET/$PY_DLLS_SRC_PATH/psMat.pyd" "$RELEASE/lib/$PYVER/site-packages/" || bail "Couldn't copy pyhook dlls" + + +ffex=`which fontforge.exe` +MSYSROOT=`cygpath -w /` +FFEXROOT=`cygpath -w $(dirname "${ffex}")` +log_note "The executable: $ffex" +log_note "MSYS root: $MSYSROOT" +log_note "FFEX root: $FFEXROOT" + +fflibs=`ntldd -D "$(dirname \"${ffex}\")" -R "$ffex" $RELEASE/lib/$PYVER/lib-dynload/*.dll $RELEASE/lib/gdk-pixbuf-2.0/*/loaders/*.dll $RELEASE/bin/potrace.exe \ +| grep =.*dll \ +| sed -e '/^[^\t]/ d' \ +| sed -e 's/\t//' \ +| sed -e 's/.*=..//' \ +| sed -e 's/ (0.*)//' \ +| grep -F -e "$MSYSROOT" -e "$FFEXROOT" \ +` + +log_status "Copying the FontForge executable..." +strip "$ffex" -so "$RELEASE/bin/fontforge.exe" +#cp "$ffex" "$RELEASE/bin/fontforge.exe" +objcopy --only-keep-debug "$ffex" "$DBSYMBOLS/fontforge.debug" +objcopy --add-gnu-debuglink="$DBSYMBOLS/fontforge.debug" "$RELEASE/bin/fontforge.exe" +log_status "Copying the libraries required by FontForge..." +for f in $fflibs; do + filename="$(basename $f)" + filenoext="${filename%.*}" + strip "$f" -so "$RELEASE/bin/$filename" + #cp "$f" "$RELEASE/bin/" + if [ -f "$TARGET/bin/$filename" ]; then + #Only create debug files for the ones we compiled! + objcopy --only-keep-debug "$f" "$DBSYMBOLS/$filenoext.debug" + objcopy --add-gnu-debuglink="$DBSYMBOLS/$filenoext.debug" "$RELEASE/bin/$filename" + fi +done + +if (($qt)); then + log_status "Running windeployqt..." + windeployqt-qt6 "$RELEASE/bin/fontforge.exe" +fi + +log_status "Generating the version file..." +current_date=`date "+%c %z"` +actual_branch=`git -C $FFPATH rev-parse --abbrev-ref HEAD` +actual_hash=`git -C $FFPATH rev-parse HEAD` +if (($appveyor+$ghactions)); then + version_hash=`git -C $FFPATH ls-remote origin master | awk '{ printf $1 }'` +else + version_hash=`git -C $FFPATH rev-parse master` +fi + + +printf "FontForge Windows build ($ARCHNUM-bit)\r\n$current_date\r\n$actual_hash [$actual_branch]\r\nBased on master: $version_hash\r\n\r\n" > $RELEASE/VERSION.txt +printf "Latest changes:\r\n\r\n" >> $RELEASE/VERSION.txt +git -C "$FFPATH" log -n 100 --date=short \ + --pretty=format:'%ad [%an] %s%n%b%n' | sed 's/$/\r/' >> $RELEASE/VERSION.txt +printf "\r\n" >> $RELEASE/VERSION.txt + +if (($makedebug)); then + log_note "Adding in debugging utilities..." + cp -f "$DEBUGFILES/ffdebugscript.txt" "$RELEASE/" || bail "Couldn't copy debug script" + cp -f "$DEBUGFILES/fontforge-debug.bat" "$RELEASE/" || bail "Couldn't copy fontforge-debug.bat" + cp -f "$BINARY/gdb-$ARCHNUM.exe" "$RELEASE/bin/gdb.exe" || bail "Couldn't copy GDB" + cp -f "$BINARY/wtee.exe" "$RELEASE/bin/" || bail "Couldn't copy wtee" + cp -rf "$DBSYMBOLS" "$RELEASE/bin/" || bail "Couldn't copy debugging symbols" +fi + +# Might as well auto-generate everything +#sed -bi "s/^git .*$/git $version_hash ($current_date).\r/g" $RELEASE/VERSION.txt + +log_note "Build complete." diff --git a/Packaging/win/launcher/fontforge-icon.ico b/Packaging/win/launcher/fontforge-icon.ico new file mode 100644 index 0000000000..e5bb71a57e Binary files /dev/null and b/Packaging/win/launcher/fontforge-icon.ico differ diff --git a/Packaging/win/launcher/fontforge-installer-icon.svg b/Packaging/win/launcher/fontforge-installer-icon.svg new file mode 100644 index 0000000000..0c0428c939 --- /dev/null +++ b/Packaging/win/launcher/fontforge-installer-icon.svg @@ -0,0 +1,1661 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + jakub Steiner + + + http://jimmac.musichall.cz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Packaging/win/launcher/fontforge-uninstaller-icon.svg b/Packaging/win/launcher/fontforge-uninstaller-icon.svg new file mode 100644 index 0000000000..359cfa0b13 --- /dev/null +++ b/Packaging/win/launcher/fontforge-uninstaller-icon.svg @@ -0,0 +1,1670 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + jakub Steiner + + + http://jimmac.musichall.cz + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Packaging/win/launcher/run_fontforge.c b/Packaging/win/launcher/run_fontforge.c new file mode 100644 index 0000000000..8b92bd47f6 --- /dev/null +++ b/Packaging/win/launcher/run_fontforge.c @@ -0,0 +1,78 @@ +#include +#include + +int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE hPrev, PWSTR lpCmdLine, int nCmdShow) { + // Define Variables + wchar_t wszAppPath[MAX_PATH]; + wchar_t wszCmdPath[MAX_PATH]; + wchar_t wszCmdArgs[8192]; + wchar_t *pwszTail; + PROCESS_INFORMATION pi; + STARTUPINFO si = { 0 }; + DWORD dwRet; + + si.cb = sizeof(STARTUPINFO); + si.dwFlags = STARTF_USESHOWWINDOW; + si.wShowWindow = SW_HIDE; + + //Get the path to cmd.exe + dwRet = GetSystemDirectory(wszAppPath, MAX_PATH); + if (dwRet == 0 || dwRet >= MAX_PATH) { + MessageBox(NULL, L"Path too long - could not determine system directory.", + NULL, MB_OK | MB_ICONEXCLAMATION); + return 1; + } + dwRet = StringCchPrintf(wszCmdPath, MAX_PATH, L"%s\\cmd.exe", wszAppPath); + if (FAILED(dwRet)) { + MessageBox(NULL, L"Path too long - could not get path to command line interpreter (cmd.exe).", + NULL, MB_OK | MB_ICONEXCLAMATION); + return 1; + } + + // Get path of executable + dwRet = GetModuleFileName(NULL, wszAppPath, MAX_PATH); + if (dwRet == 0 || dwRet >= MAX_PATH) { + MessageBox(NULL, L"Path too long - place FontForge in another directory.", + NULL, MB_OK | MB_ICONEXCLAMATION); + return 1; + } else if ((pwszTail = wcsrchr(wszAppPath, L'\\')) == NULL) { + MessageBox(NULL, L"Could not determine executable location.", + NULL, MB_OK | MB_ICONEXCLAMATION); + return 1; + } + + //Construct the parameters to pass to cmd.exe + *pwszTail = L'\0'; + dwRet = StringCchPrintf(wszCmdArgs, sizeof(wszCmdArgs)/sizeof(wchar_t), L"/c \"\"%s\\fontforge.bat\" %s\"", + wszAppPath, lpCmdLine); + if (FAILED(dwRet)) { + MessageBox(NULL, L"Command parameter is too long.", + NULL, MB_OK | MB_ICONEXCLAMATION); + return 1; + } + + // Run batch file without visible window + dwRet = CreateProcess(wszCmdPath, wszCmdArgs, NULL, NULL, FALSE, + CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi); + if (dwRet == 0) { + MessageBox(NULL, L"Could not launch FontForge.", NULL, MB_OK | MB_ICONEXCLAMATION); + return 1; + } + + //Wait for the process to end and get the exit code + dwRet = WaitForSingleObject(pi.hProcess, 10000); + if (dwRet == WAIT_OBJECT_0) { + GetExitCodeProcess(pi.hProcess, &dwRet); + if (dwRet != 0) { + wchar_t buf[100]; + StringCchPrintf(buf, sizeof(buf)/sizeof(wchar_t), L"Could not launch FontForge (exit code %d).", (int)dwRet); + MessageBox(NULL, buf, NULL, MB_OK | MB_ICONEXCLAMATION); + } + } else { + dwRet = 0; + } + + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + return dwRet; +} diff --git a/Packaging/win/launcher/run_fontforge.rc b/Packaging/win/launcher/run_fontforge.rc new file mode 100644 index 0000000000..ee0d07ff14 --- /dev/null +++ b/Packaging/win/launcher/run_fontforge.rc @@ -0,0 +1,21 @@ +id ICON "fontforge-icon.ico" + +1 VERSIONINFO +FILEVERSION 1,0,2,0 +PRODUCTVERSION 1,0,2,0 +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904E4" + BEGIN + VALUE "CompanyName", "Jeremy Tan" + VALUE "FileDescription", "FontForge" + VALUE "FileVersion", "1.2" + VALUE "InternalName", "run_fontforge" + VALUE "LegalCopyright", "" + VALUE "OriginalFilename", "run_fontforge.exe" + VALUE "ProductName", "FontForge" + VALUE "ProductVersion", "1.2" + END + END +END diff --git a/Packaging/win/launcher/sfd-icon.ico b/Packaging/win/launcher/sfd-icon.ico new file mode 100644 index 0000000000..ea23516f49 Binary files /dev/null and b/Packaging/win/launcher/sfd-icon.ico differ diff --git a/Packaging/win/launcher/sfd-icon.svg b/Packaging/win/launcher/sfd-icon.svg new file mode 100644 index 0000000000..5dcd4d961f --- /dev/null +++ b/Packaging/win/launcher/sfd-icon.svg @@ -0,0 +1,1572 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + 2005-10-15 + + + Andreas Nilsson + + + + + edit + copy + + + + + + Jakub Steiner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Packaging/win/make-portable-package.sh b/Packaging/win/make-portable-package.sh new file mode 100644 index 0000000000..930219c5bb --- /dev/null +++ b/Packaging/win/make-portable-package.sh @@ -0,0 +1,101 @@ +#!/bin/bash + +#Detect 7-zip version to use +WSZIP="/c/Program Files/7-Zip/7z.exe" +if [ -f "$WSZIP" ]; then + SZIP="$WSZIP" +else + SZIP="7za" +fi + +# Colourful text +# Red text +function log_error() { + echo -e "\e[31m$@\e[0m" +} + +# Yellow text +function log_status() { + echo -e "\e[33m$@\e[0m" +} + +# Green text +function log_note() { + echo -e "\e[32m$@\e[0m" +} + +function bail () { + echo -e "\e[31m\e[1m!!! Build failed at: ${@}\e[0m" + exit 1 +} + +function check_overwrite() { + if [ -f "$1" ]; then + read -p "File exists. Overwrite? [y/N]: " overwrite + case $overwrite in + [Yy]* ) rm -fv "$1" || bail "Could not remove $1"; break;; + * ) bail "Not overwriting $1." ;; + esac + fi +} + +function szip_folder() { + # Enable dotglob to include hidden files/directories (like .debug) + shopt -s dotglob + "$SZIP" a -t7z -mx=9 -m0=lzma2 -md=128m "$1" "$2"/* + local result=$? + shopt -u dotglob + return $result +} + +if [ -z "$1" ]; then + postfix="r1" +else + postfix="$1" +fi + +if [ "$MSYSTEM" = "UCRT64" ]; then + MINGVER=ucrt64 + PMARCH=ucrt-x86_64 +elif [ "$MSYSTEM" = "CLANGARM64" ]; then + MINGVER=clangarm64 + PMARCH=clang-aarch64 +else + bail "Unknown build system!" +fi +PKGPREFIX="FontForge-mingw-w64-$PMARCH" + +BASE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +BUILD_DIR=${BUILD_DIR:-$BASE/build} +# Convert Windows paths to Unix paths if needed +if command -v cygpath &> /dev/null; then + BUILD_DIR=$(cygpath -u "$BUILD_DIR") +fi +SETUP=$BASE/setup +WORK=$BUILD_DIR/work/$MINGVER +RELEASE=$BUILD_DIR/ReleasePackage +DEBUG=$BUILD_DIR/debugging-symbols + +log_note "Packaging $MINGVER release..." +pacman -S --noconfirm --needed p7zip > /dev/null 2>&1 + +if [ -z "$FFPATH" ]; then + FFPATH=$WORK/fontforge +fi + +version_hash=`git -C $FFPATH rev-parse master -- || git -C $FFPATH log --pretty=format:%H -n 1` +version_hash=${version_hash:0:6} +log_status "Version hash is $version_hash" +log_status "Working directory: `pwd`" + +log_status "Building the release archive..." +filename="$PKGPREFIX-$version_hash-$postfix.7z" +log_status "Name is $filename" +check_overwrite "$filename" +szip_folder "$filename" "$RELEASE" + +log_status "Building the debug archive..." +debugname="$PKGPREFIX-$version_hash-$postfix-debugging-symbols.7z" +log_status "Name is $debugname" +check_overwrite "$debugname" +szip_folder "$debugname" "$DEBUG" diff --git a/Packaging/win/runtime/fontforge-console.bat b/Packaging/win/runtime/fontforge-console.bat new file mode 100644 index 0000000000..66fbdc55c6 --- /dev/null +++ b/Packaging/win/runtime/fontforge-console.bat @@ -0,0 +1,19 @@ +@echo off +echo Configuring the system path to add FontForge... +set FF=%~dp0 +set "PYTHONHOME=%FF%" + +if not defined FF_PATH_ADDED ( +set "PATH=%FF%;%FF%\bin;%PATH:"=%" +set FF_PATH_ADDED=TRUE +) + +for /F "tokens=* USEBACKQ" %%f IN (`dir /b "%FF%lib\python*"`) do ( +set "PYTHONPATH=%FF%lib\%%f" +) + +echo Configuration complete. You can now call 'fontforge' from the console. +echo You may also use the bundled Python distribution by calling `ffpython`. +echo Extra Python modules that you require may be installed via `ffpython`. +echo. +cmd /k \ No newline at end of file diff --git a/Packaging/win/runtime/fontforge.bat b/Packaging/win/runtime/fontforge.bat new file mode 100644 index 0000000000..1af4ee09da --- /dev/null +++ b/Packaging/win/runtime/fontforge.bat @@ -0,0 +1,25 @@ +@ECHO OFF +set "FF=%~dp0" +set "HOME=%FF%" +set "PYTHONHOME=%FF%" +set AUTOTRACE=potrace +set FF_PORTABLE=TRUE + +::Set this to your language code to change the FontForge UI language +::See share/locale/ for a list of supported language codes +::set LANGUAGE=en + +::Only add to path once +if not defined FF_PATH_ADDED ( +set "PATH=%FF%;%FF%\bin;%PATH:"=%" +set FF_PATH_ADDED=TRUE +) + +for /F "tokens=* USEBACKQ" %%f IN (`dir /b "%FF%lib\python*"`) do ( +set "PYTHONPATH=%FF%lib\%%f" +) + +"%FF%\bin\fontforge.exe" -nosplash %* + +:: bye + diff --git a/Packaging/win/fontforge-installer-icon.ico b/Packaging/win/setup/Graphics/fontforge-installer-icon.ico similarity index 100% rename from Packaging/win/fontforge-installer-icon.ico rename to Packaging/win/setup/Graphics/fontforge-installer-icon.ico diff --git a/Packaging/win/Uninstall.ico b/Packaging/win/setup/Graphics/fontforge-uninstaller-icon.ico similarity index 100% rename from Packaging/win/Uninstall.ico rename to Packaging/win/setup/Graphics/fontforge-uninstaller-icon.ico diff --git a/Packaging/win/setup/Graphics/fontforge-wizard.bmp b/Packaging/win/setup/Graphics/fontforge-wizard.bmp new file mode 100644 index 0000000000..4b6d1739f8 Binary files /dev/null and b/Packaging/win/setup/Graphics/fontforge-wizard.bmp differ diff --git a/Packaging/win/setup/fontforge.bat b/Packaging/win/setup/fontforge.bat new file mode 100644 index 0000000000..ac3807a5ae --- /dev/null +++ b/Packaging/win/setup/fontforge.bat @@ -0,0 +1,24 @@ +@ECHO OFF +set "FF=%~dp0" +set "HOME=%FF%" +set "PYTHONHOME=%FF%" +set AUTOTRACE=potrace + +::Set this to your language code to change the FontForge UI language +::See share/locale/ for a list of supported language codes +::set LANGUAGE=en + +::Only add to path once +if not defined FF_PATH_ADDED ( +set "PATH=%FF%;%FF%\bin;%PATH:"=%" +set FF_PATH_ADDED=TRUE +) + +for /F "tokens=* USEBACKQ" %%f IN (`dir /b "%FF%lib\python*"`) do ( +set "PYTHONPATH=%FF%lib\%%f" +) + +"%FF%\bin\fontforge.exe" -nosplash %* + +:: bye + diff --git a/Packaging/win/setup/fontforgesetup.iss b/Packaging/win/setup/fontforgesetup.iss new file mode 100644 index 0000000000..4d47216551 --- /dev/null +++ b/Packaging/win/setup/fontforgesetup.iss @@ -0,0 +1,287 @@ +; Script generated by the Inno Setup Script Wizard. +; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! + +; BUILD_DIR can be passed via command line: iscc -DBUILD_DIR="C:\path\to\build" +#ifndef BUILD_DIR +# define BUILD_DIR "..\build" +#endif + +#define MyAppName "FontForge" +#define MyAppFolder "FontForgeBuilds" +#define MyAppVersion GetDateTimeString('yyyymmdd', '', ''); +#define MyAppVersionDesc GetDateTimeString('yyyy-mm-dd', '', ''); +#define MyAppPublisher "FontForgeBuilds" +#define MyAppURL "http://www.fontforge.org" +#define MyAppExeName "run_fontforge.exe" +#define MyInstallEpoch "1" + +#ifdef MSYSTEM +# if MSYSTEM == "UCRT64" +# define ARCH="x64compatible" +# define ARCHDESC="x64" +# endif +# if MSYSTEM == "CLANGARM64" +# define ARCH="arm64" +# define ARCHDESC="arm64" +# endif +#endif +#ifndef ARCH +# define ARCH="x64" +# define ARCHDESC="x64_unknown" +#endif +#define ARCHBITS="64" +#define OTHERBITS="32" + +[Setup] +; NOTE: The value of AppId uniquely identifies this application. +; Do not use the same AppId value in installers for other applications. +; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) +AppId={{56748B9C-19AE-4689-B8C5-5A45AE0A993A} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +;AppVerName={#MyAppName} {#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +ArchitecturesInstallIn64BitMode={#ARCH} +DefaultDirName={commonpf}\{#MyAppFolder} +DefaultGroupName={#MyAppName} +AllowNoIcons=yes +InfoBeforeFile={#BUILD_DIR}\ReleasePackage\VERSION.txt +OutputDir=. +OutputBaseFilename=FontForge-{#MyAppVersionDesc}-Windows-{#ARCHDESC} +SetupIconFile=Graphics\fontforge-installer-icon.ico +UninstallDisplayIcon={app}\fontforge-installer-icon.ico +;WizardImageFile=Graphics\fontforge-wizard.bmp +WizardSmallImageFile=Graphics\fontforge-wizard.bmp +Compression=lzma2/max +SolidCompression=yes +ChangesAssociations=yes +PrivilegesRequired=poweruser + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" +Name: "armenian"; MessagesFile: "compiler:Languages\Armenian.isl" +Name: "brazilianportuguese"; MessagesFile: "compiler:Languages\BrazilianPortuguese.isl" +Name: "catalan"; MessagesFile: "compiler:Languages\Catalan.isl" +Name: "corsican"; MessagesFile: "compiler:Languages\Corsican.isl" +Name: "czech"; MessagesFile: "compiler:Languages\Czech.isl" +Name: "danish"; MessagesFile: "compiler:Languages\Danish.isl" +Name: "dutch"; MessagesFile: "compiler:Languages\Dutch.isl" +Name: "finnish"; MessagesFile: "compiler:Languages\Finnish.isl" +Name: "french"; MessagesFile: "compiler:Languages\French.isl" +Name: "german"; MessagesFile: "compiler:Languages\German.isl" +Name: "hebrew"; MessagesFile: "compiler:Languages\Hebrew.isl" +;Name: "icelandic"; MessagesFile: "compiler:Languages\Icelandic.isl" +Name: "italian"; MessagesFile: "compiler:Languages\Italian.isl" +Name: "japanese"; MessagesFile: "compiler:Languages\Japanese.isl" +Name: "norwegian"; MessagesFile: "compiler:Languages\Norwegian.isl" +Name: "polish"; MessagesFile: "compiler:Languages\Polish.isl" +Name: "portuguese"; MessagesFile: "compiler:Languages\Portuguese.isl" +Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl" +Name: "slovak"; MessagesFile: "compiler:Languages\Slovak.isl" +Name: "slovenian"; MessagesFile: "compiler:Languages\Slovenian.isl" +Name: "spanish"; MessagesFile: "compiler:Languages\Spanish.isl" +Name: "turkish"; MessagesFile: "compiler:Languages\Turkish.isl" +Name: "ukrainian"; MessagesFile: "compiler:Languages\Ukrainian.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked +Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 0,6.1 + +[Files] +Source: "{#BUILD_DIR}\ReleasePackage\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs; Excludes: "fontforge.bat" +Source: "fontforge.bat"; DestDir: "{app}"; Flags: ignoreversion; +Source: "Graphics\fontforge-installer-icon.ico"; DestDir: "{app}"; Flags: ignoreversion; +; NOTE: Don't use "Flags: ignoreversion" on any shared system files + +[Icons] +Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" +Name: "{group}\{#MyAppName} console"; Filename: "{app}\fontforge.bat" +Name: "{group}\{#MyAppName} interactive console"; Filename: "{app}\fontforge-console.bat" +Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" +Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon + +[Registry] +;Root: HKLM; Subkey: "Software\FontForge"; Flags: uninsdeletekey +Root: HKCR; Subkey: ".sfd"; ValueType: string; ValueName: ""; ValueData: "FontForgeProject"; Flags: uninsdeletevalue +Root: HKCR; Subkey: "FontForgeProject"; ValueType: string; ValueName: ""; ValueData: "FontForge Project"; Flags: uninsdeletekey +Root: HKCR; Subkey: "FontForgeProject\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\share\fontforge\sfd-icon.ico" +Root: HKCR; Subkey: "FontForgeProject\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1""" + +[Run] +Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent + +[Code] +procedure RecursiveDelete(Folder : String; Pattern : String; Extensions : TStringList); +var + SearchPath : String; + FilePath : String; + FindRec : TFindRec; +begin + //First search the current folder for any files matching the pattern + SearchPath := ExpandConstant(Folder + '\' + Pattern); + Log('[Recursive] searching ' + SearchPath); + if FindFirst(SearchPath, FindRec) then begin + try + repeat + if FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY = 0 then begin + if Extensions.IndexOf(ExtractFileExt(FindRec.Name)) <> -1 then begin + FilePath := ExpandConstant(Folder + '\' + FindRec.Name); + Log('[Recursive] Deleting ' + FilePath); + DeleteFile(FilePath); + end; + end; + until not FindNext(FindRec); + finally + FindClose(FindRec); + end; + end; + + //Now recursively search any subdirectories + SearchPath := ExpandConstant(Folder + '\*'); + Log('[Recursive] searching ' + SearchPath); + if FindFirst(SearchPath, FindRec) then begin + try + repeat + if ((FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY) <> 0) and + (CompareStr(FindRec.Name, '.') <> 0) and (CompareStr(FindRec.Name, '..') <> 0) then begin + FilePath := ExpandConstant(Folder + '\' + FindRec.Name); + Log('[Recursive] now searching: ' + FilePath); + RecursiveDelete(FilePath, Pattern, Extensions); + end; + until not FindNext(FindRec); + finally + FindClose(FindRec); + end; + end; +end; + + +procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); +var ExtensionsToDelete : TStringList; +begin + if CurUninstallStep = usUninstall then + begin + if MsgBox('Do you want to remove user preferences?', mbConfirmation, MB_YESNO) = IDYES then + begin + DelTree(ExpandConstant('{userappdata}\FontForge'), True, True, True); + end; + + Log('Removing Python cache files (.pyc/.pyo)...'); + ExtensionsToDelete := TStringList.Create; + ExtensionsToDelete.Add('.pyc'); + ExtensionsToDelete.Add('.pyo'); + RecursiveDelete('{app}\lib\python3.*', '*.py*', ExtensionsToDelete); + ExtensionsToDelete.Free; + end; +end; + + +///////////////////////////////////////////////////////////////////// +function GetUninstallString(): String; +var + sUnInstKey: Integer; + sUnInstPath: String; + sUnInstallString: String; + sPreviousInstallEpoch: String; + bIsOtherArch: Boolean; +begin + sUnInstKey := HKLM{#ARCHBITS}; + sUnInstPath := ExpandConstant('Software\Microsoft\Windows\CurrentVersion\Uninstall\{#emit SetupSetting("AppId")}_is1'); + sUnInstallString := ''; + bIsOtherArch := false; + if not RegQueryStringValue(sUnInstKey, sUnInstPath, 'UninstallString', sUnInstallString) then begin + sUnInstKey := HKCU{#ARCHBITS}; + if not RegQueryStringValue(sUnInstKey, sUnInstPath, 'UninstallString', sUnInstallString) then begin + sUnInstKey := HKLM{#OTHERBITS}; + if not RegQueryStringValue(sUnInstKey, sUnInstPath, 'UninstallString', sUnInstallString) then begin + sUnInstKey := HKCU{#OTHERBITS}; + RegQueryStringValue(sUnInstKey, sUnInstPath, 'UninstallString', sUnInstallString); + end; + if sUninstallString <> '' then begin + bIsOtherArch := true; + end; + end; + end; + if (sUninstallString <> '') and (not bIsOtherArch) then + begin + sPreviousInstallEpoch := GetPreviousData('InstallEpoch', '') + Log('Previous install epoch is: ' + sPreviousInstallEpoch + ' vs current: {#MyInstallEpoch}') + if sPreviousInstallEpoch = '{#MyInstallEpoch}' then begin + sUninstallString := ''; + end + end; + Result := sUnInstallString; +end; + + +///////////////////////////////////////////////////////////////////// +function IsUpgrade(): Boolean; +begin + Result := (GetUninstallString() <> ''); +end; + + +///////////////////////////////////////////////////////////////////// +function UnInstallOldVersion(): Integer; +var + sUnInstallString: String; + iResultCode: Integer; +begin +// Return Values: +// 1 - uninstall string is empty +// 2 - error executing the UnInstallString +// 3 - successfully executed the UnInstallString + + // default return value + Result := 0; + + // get the uninstall string of the old app + sUnInstallString := GetUninstallString(); + if sUnInstallString <> '' then begin + sUnInstallString := RemoveQuotes(sUnInstallString); + if Exec(sUnInstallString, '/SILENT /NORESTART /SUPPRESSMSGBOXES','', SW_HIDE, ewWaitUntilTerminated, iResultCode) then + Result := 3 + else + Result := 2; + end else + Result := 1; +end; + +///////////////////////////////////////////////////////////////////// +function InitializeSetup(): Boolean; +var + iUninstallOldResult : Integer; +begin + Result := true + if (IsUpgrade()) then + begin + if MsgBox('The previous version of FontForge must be uninstalled first. Continue?', mbConfirmation, MB_YESNO) = IDYES then + begin + iUninstallOldResult := UnInstallOldVersion(); + Result := (iUninstallOldResult <> 2); + end else + Result := False; + end; +end; + +procedure RegisterPreviousData(PreviousDataKey: Integer); +begin + SetPreviousData(PreviousDataKey, 'InstallEpoch', '{#MyInstallEpoch}'); +end; + +procedure CurStepChanged(CurStep: TSetupStep); +begin + if (CurStep=ssInstall) then + begin + if (IsUpgrade()) then + begin + if MsgBox('The previous version of FontForge must be uninstalled first. Continue?', mbConfirmation, MB_YESNO) = IDYES then + UnInstallOldVersion() + else + Abort; + end; + end; +end; diff --git a/Packaging/win/strip-python.sh b/Packaging/win/strip-python.sh new file mode 100644 index 0000000000..f4363f1aa0 --- /dev/null +++ b/Packaging/win/strip-python.sh @@ -0,0 +1,37 @@ +#!/bin/sh +# Copies the Python Lib directory and strips it down to a reasonable size. +# FontForge essentially ships with its own version of Python + +set -eo pipefail + +export LC_ALL=C + +BINARY=$BUILD_DIR/downloads/ + +PACKAGE=$PMPREFIX-python +PIP_PACKAGE=$PMPREFIX-python-pip +SETUPTOOLS_PACKAGE=$PMPREFIX-python-setuptools + +PYNAME="$PACKAGE" +PYFULLVER=$(pacman -Qi $PACKAGE | grep -m1 Version | cut -d':' -f2 | xargs) +PIPVER=$(pacman -Qi $PIP_PACKAGE | grep -m1 Version | cut -d':' -f2 | xargs) +SETUPTOOLSVER=$(pacman -Qi $SETUPTOOLS_PACKAGE | grep -m1 Version | cut -d':' -f2 | xargs) +PYVER=python$(echo $PYFULLVER | cut -d. -f1-2) +PYARCH=$(ls -1 "/var/cache/pacman/pkg/$PYNAME-$PYFULLVER-any.pkg.tar."* | head -n1) + +mkdir -p "$BINARY" +cd "$BINARY" || exit 1 + +echo "Python: $PYNAME $PYFULLVER ($PYVER): $PYARCH" + +if [ ! -d $PYVER ]; then + tar axf "/var/cache/pacman/pkg/$PYNAME-$PYFULLVER-any.pkg.tar.zst" --strip-components=2 --wildcards '*/lib/python3.*' + tar axf "/var/cache/pacman/pkg/$PIP_PACKAGE-$PIPVER-any.pkg.tar.zst" --strip-components=2 --wildcards '*/lib/python3.*' + tar axf "/var/cache/pacman/pkg/$SETUPTOOLS_PACKAGE-$SETUPTOOLSVER-any.pkg.tar.zst" --strip-components=2 --wildcards '*/lib/python3.*' +fi + +cd $PYVER +echo "Stripping Python directory..." +rm -rf "config-${PYVER}" idlelib test turtledemo config +find . -regextype sed -regex ".*\.py[co]" | xargs rm -rf +find . -name __pycache__ | xargs rm -rf diff --git a/README_PYPI.md b/README_PYPI.md new file mode 100644 index 0000000000..fd66c33575 --- /dev/null +++ b/README_PYPI.md @@ -0,0 +1,120 @@ +# FontForge Python Module + +FontForge is a powerful, open-source font editor. This package provides the +Python bindings for FontForge, allowing you to create, edit, and convert fonts +programmatically. + +## Installation + +```bash +pip install fontforge +``` + +## Quick Start + +```python +import fontforge + +# Create a new font +font = fontforge.font() +font.familyname = "MyFont" +font.fontname = "MyFont-Regular" + +# Create a glyph +glyph = font.createChar(ord('A')) +pen = glyph.glyphPen() +# Draw the glyph... + +# Open an existing font +font = fontforge.open("path/to/font.ttf") + +# Access glyphs +for glyph in font.glyphs(): + print(glyph.glyphname, glyph.unicode) + +# Generate output +font.generate("output.otf") +``` + +## Features + +- **Read/Write Font Formats**: OTF, TTF, UFO, Type1, WOFF, WOFF2, and more +- **Glyph Editing**: Create and modify glyph outlines programmatically +- **OpenType Features**: Build and manipulate GSUB/GPOS tables +- **Font Conversion**: Convert between font formats +- **Metrics & Kerning**: Set sidebearings, create kerning pairs +- **Validation**: Check fonts for common issues +- **Batch Processing**: Script operations across multiple fonts + +## Included Modules + +This package installs two Python modules: + +- **`fontforge`** - The main font manipulation module +- **`psMat`** - PostScript transformation matrix utilities + +```python +import fontforge +import psMat + +# Create a transformation matrix (scale 2x) +matrix = psMat.scale(2.0) + +# Apply to a glyph layer +glyph.transform(matrix) +``` + +## Documentation + +- [Python Scripting Guide](https://fontforge.org/docs/scripting/python.html) +- [Python API Reference](https://fontforge.org/docs/scripting/python/fontforge.html) +- [FontForge Documentation](https://fontforge.org/docs/) + +## Requirements + +- Python 3.8 or later +- No additional Python dependencies required + +The wheel includes all required native libraries (FreeType, libxml2, etc.). + +## Platform Support + +Pre-built wheels are available for: + +- **Linux**: x86_64, aarch64 (manylinux_2_28) +- **macOS**: x86_64, arm64 (macOS 11.0+) +- **Windows**: x86_64 + +## Security Considerations + +FontForge was designed for interactive use with trusted font files. The +codebase has known vulnerabilities, including buffer overruns, that can cause +crashes and may be exploitable. **Do not process untrusted input without +thorough sandboxing.** + +## Building from Source + +If no wheel is available for your platform, pip will attempt to build from source. This requires: + +- CMake 3.15+ +- C/C++ compiler +- FreeType, libxml2, zlib development headers + +See the [build documentation](https://github.com/fontforge/fontforge#building) for details. + +## FontForge Application + +This package provides only the Python module. For the full FontForge GUI application, see: + +- [FontForge Downloads](https://fontforge.org/en-US/downloads/) +- [GitHub Releases](https://github.com/fontforge/fontforge/releases) + +## License + +FontForge is licensed under the [GPLv3+](https://github.com/fontforge/fontforge/blob/master/COPYING.gplv3). + +## Links + +- [Homepage](https://fontforge.org) +- [GitHub Repository](https://github.com/fontforge/fontforge) +- [Issue Tracker](https://github.com/fontforge/fontforge/issues) diff --git a/Unicode/CMakeLists.txt b/Unicode/CMakeLists.txt index 908cc226d1..603fc3aa07 100644 --- a/Unicode/CMakeLists.txt +++ b/Unicode/CMakeLists.txt @@ -3,11 +3,11 @@ add_library(gunicode OBJECT ArabicForms.c char.c - memory.c - ucharmap.c + memory.cpp + ucharmap.cpp unialt.c uninames.c - ustring.c + ustring.cpp utype.c ) @@ -19,14 +19,14 @@ if(${CMAKE_VERSION} VERSION_LESS "3.12.0") make_object_interface(gunicode fontforge_common_headers Iconv::Iconv - Intl::Intl + $<$:Intl::Intl> ) else() target_link_libraries(gunicode PUBLIC fontforge_common_headers Iconv::Iconv - Intl::Intl + $<$:Intl::Intl> ) endif() diff --git a/Unicode/makeutype.py b/Unicode/makeutype.py index 5a8895f7e5..8f7e643e38 100644 --- a/Unicode/makeutype.py +++ b/Unicode/makeutype.py @@ -244,8 +244,8 @@ def __init__(self, version): (0x20000, 0x2FFFD, "Supplementary Ideographic Plane"), (0x30000, 0x3FFFD, "Tertiary Ideographic Plane"), (0xE0000, 0xEFFFD, "Supplementary Special-purpose Plane"), - (0xF0000, 0xFFFFD, "Supplementary Private Use Area-A"), - (0x100000, 0x10FFFD, "Supplementary Private Use Area-B"), + (0xF0000, 0xFFFFD, "Plane 15 (Supplementary Private Use Area-A)"), + (0x100000, 0x10FFFD, "Plane 16 (Supplementary Private Use Area-B)"), ] planes = [ (s, e, s, sum(1 for i in range(s, e + 1) if table[i]), n) diff --git a/Unicode/memory.c b/Unicode/memory.cpp similarity index 88% rename from Unicode/memory.c rename to Unicode/memory.cpp index 774b138fd1..a5b506a9ca 100644 --- a/Unicode/memory.c +++ b/Unicode/memory.cpp @@ -29,7 +29,7 @@ #include "ustring.h" -#include +#include #include #include @@ -39,6 +39,13 @@ void NoMoreMemMessage(void) { fprintf(stderr, "Out of memory\n" ); } +void ExpandBuffer(void** p_buf, size_t elem_size, size_t increment, + int* p_maxalloc) { + *p_buf = realloc(*p_buf, (*p_maxalloc + increment) * elem_size); + memset((char*)*p_buf + (*p_maxalloc) * elem_size, 0, increment * elem_size); + *p_maxalloc += increment; +} + char *copy(const char *str) { return str ? strdup(str) : NULL; } @@ -75,21 +82,21 @@ char *vsmprintf(const char *fmt, va_list args) { int len; va_copy(args2, args); - len = g_vsnprintf(NULL, 0, fmt, args2); + len = vsnprintf(NULL, 0, fmt, args2); va_end(args2); if (len < 0) { return NULL; } - ret = malloc(len + 1); + ret = (char*)malloc(len + 1); if (ret == NULL) { perror("smprintf"); abort(); return NULL; } - len = g_vsnprintf(ret, len + 1, fmt, args); + len = vsnprintf(ret, len + 1, fmt, args); if (len < 0) { free(ret); diff --git a/Unicode/ucharmap.c b/Unicode/ucharmap.cpp similarity index 89% rename from Unicode/ucharmap.c rename to Unicode/ucharmap.cpp index 2442f2d08b..509c331ce4 100644 --- a/Unicode/ucharmap.c +++ b/Unicode/ucharmap.cpp @@ -100,12 +100,12 @@ char *u2def_strncpy(char *to, const unichar_t *ufrom, size_t n) { static void* do_iconv(iconv_t cd, const void* inbuf, size_t incount, size_t inunitsize, size_t outunitsize) { size_t outbytes = (incount + 1) * outunitsize, outremain = outbytes; incount *= inunitsize; - char *buf = malloc(outbytes), *dst = buf; + char *buf = (char*) malloc(outbytes), *dst = buf; while (incount > 0 && buf) { if (iconv(cd, (iconv_arg2_t)&inbuf, &incount, &dst, &outremain) == (size_t)-1) { if (errno == E2BIG) { - buf = realloc(buf, outbytes*2); + buf = (char*) realloc(buf, outbytes*2); if (buf) { dst = buf + outbytes; outremain += outbytes; @@ -122,7 +122,7 @@ static void* do_iconv(iconv_t cd, const void* inbuf, size_t incount, size_t inun if (outremain < outunitsize) { outbytes += outunitsize - outremain; outremain = outunitsize; - buf = realloc(buf, outbytes); + buf = (char*) realloc(buf, outbytes); } if (buf) { memset(buf + (outbytes - outremain), 0, outremain); @@ -137,7 +137,7 @@ unichar_t *def2u_copy(const char *from) { return NULL; else if (is_local_encoding_utf8) return utf82u_copy(from); - return do_iconv(to_unicode, from, strlen(from), sizeof(from[0]), sizeof(unichar_t)); + return (unichar_t*) do_iconv(to_unicode, from, strlen(from), sizeof(from[0]), sizeof(unichar_t)); } char *u2def_copy(const unichar_t *ufrom) { @@ -145,7 +145,7 @@ char *u2def_copy(const unichar_t *ufrom) { return NULL; else if (is_local_encoding_utf8) return u2utf8_copy(ufrom); - return do_iconv(from_unicode, ufrom, u_strlen(ufrom), sizeof(ufrom[0]), sizeof(char)); + return (char*) do_iconv(from_unicode, ufrom, u_strlen(ufrom), sizeof(ufrom[0]), sizeof(char)); } char *def2utf8_copy(const char *from) { @@ -153,7 +153,7 @@ char *def2utf8_copy(const char *from) { return NULL; else if (is_local_encoding_utf8) return copy(from); - return do_iconv(to_utf8, from, strlen(from), sizeof(from[0]), sizeof(char)); + return (char*) do_iconv(to_utf8, from, strlen(from), sizeof(from[0]), sizeof(char)); } char *utf82def_copy(const char *ufrom) { @@ -161,7 +161,7 @@ char *utf82def_copy(const char *ufrom) { return NULL; else if (is_local_encoding_utf8) return copy(ufrom); - return do_iconv(from_utf8, ufrom, strlen(ufrom), sizeof(ufrom[0]), sizeof(char)); + return (char*) do_iconv(from_utf8, ufrom, strlen(ufrom), sizeof(ufrom[0]), sizeof(char)); } char *utf82def_copy_safe(const char *ufrom) { @@ -172,7 +172,7 @@ char *utf82def_copy_safe(const char *ufrom) { ret_val = utf82def_copy(ufrom); if (ret_val == NULL) { // This should never fail, unless iconv() is severely broken. - ret_val = do_iconv(from_utf8_safe, ufrom, strlen(ufrom), sizeof(ufrom[0]), sizeof(char)); + ret_val = (char*) do_iconv(from_utf8_safe, ufrom, strlen(ufrom), sizeof(ufrom[0]), sizeof(char)); } if (ret_val == NULL) { // We should never get here. diff --git a/Unicode/uninames_data.h b/Unicode/uninames_data.h index 1238b6fba2..c43c56c011 100644 --- a/Unicode/uninames_data.h +++ b/Unicode/uninames_data.h @@ -403,8 +403,8 @@ static const struct unicode_range unicode_planes[] = { {0xC0000, 0xCFFFD, 0xC0000, 0, N_("")}, {0xD0000, 0xDFFFD, 0xD0000, 0, N_("")}, {0xE0000, 0xEFFFD, 0xE0000, 337, N_("Supplementary Special-purpose Plane")}, - {0xF0000, 0xFFFFD, 0xF0000, 65534, N_("Supplementary Private Use Area-A")}, - {0x100000, 0x10FFFD, 0x100000, 65534, N_("Supplementary Private Use Area-B")}, + {0xF0000, 0xFFFFD, 0xF0000, 65534, N_("Plane 15 (Supplementary Private Use Area-A)")}, + {0x100000, 0x10FFFD, 0x100000, 65534, N_("Plane 16 (Supplementary Private Use Area-B)")}, }; /* lexicon data */ diff --git a/Unicode/ustring.c b/Unicode/ustring.cpp similarity index 95% rename from Unicode/ustring.c rename to Unicode/ustring.cpp index b0fe3387ef..6e2de63dac 100644 --- a/Unicode/ustring.c +++ b/Unicode/ustring.cpp @@ -27,12 +27,14 @@ #include -#include "ffglib.h" #include "ustring.h" #include "utype.h" +#include "ffglib_compat.h" + #include #include +#include long uc_strcmp(const unichar_t *str1,const char *str2) { long ch1, ch2; @@ -119,21 +121,21 @@ return(ch1-ch2); } void cu_strcpy(char *to, const unichar_t *from) { - register unichar_t ch; + unichar_t ch; while ( (ch = *from++) != '\0' ) *(to++) = ch; *to = 0; } void uc_strcpy(unichar_t *to, const char *from) { - register unichar_t ch; + unichar_t ch; while ( (ch = *(unsigned char *) from++) != '\0' ) *(to++) = ch; *to = 0; } void u_strcpy(unichar_t *to, const unichar_t *from) { - register unichar_t ch; + unichar_t ch; while ( (ch = *from++) != '\0' ) *(to++) = ch; *to = 0; @@ -149,22 +151,22 @@ char *cc_strncpy(char *to, const char *from, int len) { } -void u_strncpy(register unichar_t *to, const unichar_t *from, int len) { - register unichar_t ch; +void u_strncpy(unichar_t *to, const unichar_t *from, int len) { + unichar_t ch; while ( (ch = *from++) != '\0' && --len>=0 ) *(to++) = ch; *to = 0; } -void cu_strncpy(register char *to, const unichar_t *from, int len) { - register unichar_t ch; +void cu_strncpy(char *to, const unichar_t *from, int len) { + unichar_t ch; while ( (ch = *from++) != '\0' && --len>=0 ) *(to++) = ch; *to = 0; } -void uc_strncpy(register unichar_t *to, const char *from, int len) { - register unichar_t ch; +void uc_strncpy(unichar_t *to, const char *from, int len) { + unichar_t ch; while ( (ch = *(unsigned char *) from++) != '\0' && --len>=0 ) *(to++) = ch; *to = 0; @@ -195,8 +197,8 @@ void u_strncat(unichar_t *to, const unichar_t *from, int len) { } -int u_strlen(register const unichar_t *str) { - register int len = 0; +int u_strlen(const unichar_t *str) { + int len = 0; while ( *str++!='\0' ) ++len; @@ -212,7 +214,7 @@ int c_strlen( const char * p ) unichar_t *u_strchr(const unichar_t *str ,unichar_t ch) { - register unichar_t test; + unichar_t test; while ( (test=*(str++))!='\0' ) if ( test==ch ) @@ -222,7 +224,8 @@ return( NULL ); } unichar_t *u_strrchr(const unichar_t *str ,unichar_t ch) { - register unichar_t test, *last = NULL; + unichar_t test; + unichar_t *last = NULL; while ( (test=*(str++))!='\0' ) if ( test==ch ) @@ -321,7 +324,7 @@ unichar_t *u_copynallocm(const unichar_t *pt, long n, long m) { if ( n*sizeof(unichar_t)>=MEMORY_MASK ) n = MEMORY_MASK/sizeof(unichar_t)-1; #endif - res = malloc((m+1)*sizeof(unichar_t)); + res = (unichar_t *) malloc((m+1)*sizeof(unichar_t)); memcpy(res,pt,n*sizeof(unichar_t)); res[n]='\0'; return(res); @@ -535,7 +538,7 @@ unichar_t *utf82u_strncpy(unichar_t *ubuf,const char *utf8buf,int len) { } unichar_t *utf82u_strcpy(unichar_t *ubuf,const char *utf8buf) { -return( utf82u_strncpy(ubuf,utf8buf,strlen(utf8buf)+1)); +return( utf82u_strncpy(ubuf,utf8buf,c_strlen(utf8buf)+1)); } unichar_t *utf82u_copyn(const char *utf8buf,int len) { @@ -860,7 +863,7 @@ long utf82u_strlen(const char *utf8_str) { return( len ); } -void utf8_strncpy(register char *to, const char *from, int len) { +void utf8_strncpy(char *to, const char *from, int len) { /* copy n characters NOT bytes */ const char *old = from; while ( len && *old ) { @@ -989,7 +992,7 @@ return( newcr ); int AllAscii(const char *txt) { /* Verify string only All ASCII printable characters */ - register unsigned char ch; + unsigned char ch; if ( txt==NULL ) return( false ); @@ -1044,16 +1047,16 @@ int endswith(const char *haystack,const char *needle) { int nedlen = strlen( needle ); if( haylen < nedlen ) return 0; - char* p = strstr( haystack + haylen - nedlen, needle ); + const char* p = strstr( haystack + haylen - nedlen, needle ); return p == ( haystack + haylen - nedlen ); } int endswithi(const char *haystackZ,const char *needleZ) { - gchar* haystack = g_ascii_strdown(haystackZ,-1); - gchar* needle = g_ascii_strdown(needleZ,-1); + char* haystack = ff_ascii_strdown(haystackZ,-1); + char* needle = ff_ascii_strdown(needleZ,-1); int ret = endswith( haystack, needle ); - g_free( haystack ); - g_free( needle ); + free( haystack ); + free( needle ); return ret; } @@ -1062,8 +1065,8 @@ int endswithi_partialExtension( const char *haystackZ,const char *needleZ) { if( nedlen == 0 ) { return 0; } - gchar* haystack = g_ascii_strdown(haystackZ,-1); - gchar* needle = g_ascii_strdown(needleZ,-1); + char* haystack = ff_ascii_strdown(haystackZ,-1); + char* needle = ff_ascii_strdown(needleZ,-1); int ret = 0; int i = nedlen-1; ret |= endswith( haystack, needle ); @@ -1071,8 +1074,8 @@ int endswithi_partialExtension( const char *haystackZ,const char *needleZ) { needle[i] = '\0'; ret |= endswith( haystack, needle ); } - g_free( haystack ); - g_free( needle ); + free( haystack ); + free( needle ); return ret; } @@ -1169,3 +1172,12 @@ char* tostr( int v ) snprintf(buf,bufsz,"%d",v); return buf; } + +void realloc_tail(char** p_buf, size_t size_delta, char** p_tail, + char** p_proc) { + size_t new_size = size_delta + (*p_tail - *p_buf); + char* new_buf = (char*) realloc(*p_buf, new_size); + *p_tail = new_buf + new_size; + if (p_proc) *p_proc = new_buf + (*p_proc - *p_buf); + *p_buf = new_buf; +} diff --git a/WHEEL_BUILD_STATUS.md b/WHEEL_BUILD_STATUS.md new file mode 100644 index 0000000000..16d301be9b --- /dev/null +++ b/WHEEL_BUILD_STATUS.md @@ -0,0 +1,45 @@ +# Wheel Build Status + +Temporary tracking document for PyPI wheel distribution work. + +For technical documentation, see `doc/sphinx/scripting/python-extension.rst`. + +## cibuildwheel Configuration + +```toml +[tool.cibuildwheel] +skip = ["pp*", "*-win32", "*-manylinux_i686", "*-musllinux*"] + +[tool.cibuildwheel.linux] +manylinux-x86_64-image = "manylinux_2_28" +repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel}" + +[tool.cibuildwheel.macos] +archs = ["x86_64", "arm64"] +repair-wheel-command = "delocate-wheel -w {dest_dir} {wheel}" + +[tool.cibuildwheel.windows] +archs = ["AMD64"] +repair-wheel-command = "delvewheel repair -w {dest_dir} {wheel}" +``` + +## Implementation Checklist + +- [x] `BUILDING_WHEEL` detection in CMakeLists.txt +- [x] Python version generation (CalVer) +- [x] Wheel install layout in pyhook/CMakeLists.txt +- [x] pyproject.toml with scikit-build-core +- [x] Custom version provider +- [x] cibuildwheel in GitHub Actions +- [x] README_PYPI.md +- [x] Context-aware version (CalVer vs numeric) +- [x] dist-info on app install +- [ ] PyPI trusted publishing +- [ ] sdist build testing +- [ ] Initial PyPI release + +## Notes + +- Windows wheels require MSVC build (separate from MinGW GUI build) +- Wheels bundle all dependencies via auditwheel/delocate/delvewheel +- Python versions: support current CPython releases (check pyproject.toml) diff --git a/_build_meta/version_provider.py b/_build_meta/version_provider.py new file mode 100644 index 0000000000..49f24dc71d --- /dev/null +++ b/_build_meta/version_provider.py @@ -0,0 +1,46 @@ +# FontForge version provider for scikit-build-core +# Extracts version from CMakeLists.txt and converts YYYYMMDD to CalVer YYYY.M.D + +from __future__ import annotations + +import re +from pathlib import Path +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from collections.abc import Mapping + +__all__ = ["dynamic_metadata"] + + +def dynamic_metadata( + field: str, + settings: Mapping[str, Any], +) -> str: + if field != "version": + msg = f"This provider only supports 'version', not {field!r}" + raise RuntimeError(msg) + + # Read CMakeLists.txt from project root + cmake_file = Path("CMakeLists.txt") + if not cmake_file.exists(): + msg = "CMakeLists.txt not found" + raise RuntimeError(msg) + + content = cmake_file.read_text(encoding="utf-8") + + # Extract version from: project(fontforge VERSION 20251009 ...) + match = re.search(r"project\s*\(\s*fontforge\s+VERSION\s+(\d{8})", content) + if not match: + msg = "Could not find project VERSION in CMakeLists.txt" + raise RuntimeError(msg) + + version_str = match.group(1) + + # Parse YYYYMMDD format + year = version_str[0:4] + month = int(version_str[4:6]) # int() strips leading zero + day = int(version_str[6:8]) + + # Return CalVer format: YYYY.M.D + return f"{year}.{month}.{day}" diff --git a/cmake/BuildUtils.cmake b/cmake/BuildUtils.cmake index 6c6ea3af33..e77b31f1e2 100644 --- a/cmake/BuildUtils.cmake +++ b/cmake/BuildUtils.cmake @@ -145,7 +145,7 @@ function(set_font_family type value) if(APPLE) set(FONTFORGE_UI_FAMILIES "system-ui,Deja Vu Sans,Lucida Grande,sans" PARENT_SCOPE) elseif(WIN32 OR CYGWIN) - set(FONTFORGE_UI_FAMILIES "system-ui,Deja Vu Sans,Calibri" PARENT_SCOPE) + set(FONTFORGE_UI_FAMILIES "windows-cjk-workaround,system-ui,Deja Vu Sans,Calibri" PARENT_SCOPE) else() set(FONTFORGE_UI_FAMILIES "system-ui,Deja Vu Sans" PARENT_SCOPE) endif() diff --git a/cmake/FontForgeConfigure.cmake b/cmake/FontForgeConfigure.cmake index 3184dc2bc2..2b90e65f43 100644 --- a/cmake/FontForgeConfigure.cmake +++ b/cmake/FontForgeConfigure.cmake @@ -58,8 +58,10 @@ function(fontforge_generate_config template destination) check_function_exists(realpath HAVE_REALPATH) cmake_pop_check_state() - # These are hard requirements/unsupported, should get rid of these - set(HAVE_LIBINTL_H 1) + # Set HAVE_LIBINTL_H only if Intl was found + if(Intl_FOUND) + set(HAVE_LIBINTL_H 1) + endif() # Configurable settings set(FONTFORGE_CONFIG_SHOW_RAW_POINTS ${ENABLE_DEBUG_RAW_POINTS}) @@ -79,6 +81,7 @@ function(fontforge_generate_config template destination) _set_negated(_NO_LIBJPEG "${ENABLE_LIBJPEG_RESULT}") _set_negated(_NO_LIBPNG "${ENABLE_LIBPNG_RESULT}") _set_negated(_NO_LIBSPIRO "${ENABLE_LIBSPIRO_RESULT}") + _set_negated(_NO_LIBUNIBREAK "${ENABLE_LIBUNIBREAK_RESULT}") _set_negated(_NO_LIBTIFF "${ENABLE_LIBTIFF_RESULT}") _set_negated(_NO_LIBUNGIF "${ENABLE_LIBGIF_RESULT}") _set_negated(_NO_PYTHON "${ENABLE_PYTHON_SCRIPTING_RESULT}") diff --git a/cmake/PackageUtils.cmake b/cmake/PackageUtils.cmake index 9be37d02f2..bc47ad7870 100644 --- a/cmake/PackageUtils.cmake +++ b/cmake/PackageUtils.cmake @@ -42,3 +42,28 @@ macro(find_package_auto auto_option) set(${auto_option}_RESULT ${${ARGV1}_FOUND}) endif() endmacro() + +# pkg_check_auto - pkg-config based alternative to find_package_auto +# +# Usage: pkg_check_auto(ENABLE_OPTION TargetName pkg-config-name) +# +# If ENABLE_OPTION is truthy, calls pkg_check_modules to find the package. +# Creates an aliased target TargetName::TargetName from PkgConfig::TargetName. +# Sets ENABLE_OPTION_RESULT to the result. +# +macro(pkg_check_auto auto_option target_name pkg_name) + if(${auto_option}) + unset(_pkg_check_auto_required) + if(NOT ${${auto_option}} STREQUAL "AUTO") + set(_pkg_check_auto_required REQUIRED) + endif() + + pkg_check_modules(${target_name} ${_pkg_check_auto_required} IMPORTED_TARGET ${pkg_name}) + set(${auto_option}_RESULT ${${target_name}_FOUND}) + + if(${target_name}_FOUND AND NOT TARGET ${target_name}::${target_name}) + include(${PROJECT_SOURCE_DIR}/cmake/TargetUtils.cmake) + alias_imported_target(${target_name}::${target_name} PkgConfig::${target_name}) + endif() + endif() +endmacro() diff --git a/cmake/TestUtils.cmake b/cmake/TestUtils.cmake index cc7546c94f..f5d61e63fd 100644 --- a/cmake/TestUtils.cmake +++ b/cmake/TestUtils.cmake @@ -63,7 +63,10 @@ function(_add_systest test_mode binary test_script) endfunction() function(add_ff_test test_script) - _add_systest(ff "$" "${test_script}" ${ARGN}) + # Native scripting tests require fontforgeexe, not available in wheel builds + if(NOT BUILDING_WHEEL) + _add_systest(ff "$" "${test_script}" ${ARGN}) + endif() endfunction() function(add_py_test test_script) @@ -73,7 +76,10 @@ function(add_py_test test_script) set(_disable_pyhook 1) endif() - _add_systest(py "$" "${test_script}" ${ARGN}) + # fontforgeexe-based Python tests not available in wheel builds + if(NOT BUILDING_WHEEL) + _add_systest(py "$" "${test_script}" ${ARGN}) + endif() if(ENABLE_PYTHON_EXTENSION_RESULT AND NOT _disable_pyhook) _add_systest(pyhook "${Python3_EXECUTABLE}" "${test_script}" ${ARGN}) endif() diff --git a/cmake/packages/FindCairo.cmake b/cmake/packages/FindCairo.cmake deleted file mode 100644 index 1a9d9f5157..0000000000 --- a/cmake/packages/FindCairo.cmake +++ /dev/null @@ -1,102 +0,0 @@ -# Copyright (C) 2012 Raphael Kubo da Costa -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions -# are met: -# 1. Redistributions of source code must retain the above copyright -# notice, this list of conditions and the following disclaimer. -# 2. Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND ITS CONTRIBUTORS ``AS -# IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR ITS -# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; -# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF -# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -#[=======================================================================[.rst: -FindCairo ---------- - -Find the Cairo library. - -Imported Targets -^^^^^^^^^^^^^^^^ - -This module defines the following :prop_tgt:`IMPORTED` target: - -``Cairo::Cairo`` - The Cairo ``cairo`` library, if found - -Result Variables -^^^^^^^^^^^^^^^^ - -This module will set the following variables in your project: - -``Cairo_FOUND`` - true if the Cairo headers and libraries were found -``Cairo_INCLUDE_DIRS`` - directories containing the Cairo headers. -``Cairo_LIBRARIES`` - the library to link against -``Cairo_VERSION`` - the version of Cairo found - -#]=======================================================================] - -find_package(PkgConfig) -pkg_check_modules(PC_CAIRO QUIET cairo) - -find_path(Cairo_INCLUDE_DIRS - NAMES cairo.h - HINTS ${PC_CAIRO_INCLUDEDIR} - ${PC_CAIRO_INCLUDE_DIRS} - PATH_SUFFIXES cairo -) - -find_library(Cairo_LIBRARIES - NAMES cairo - HINTS ${PC_CAIRO_LIBDIR} - ${PC_CAIRO_LIBRARY_DIRS} -) - -if(Cairo_INCLUDE_DIRS) - if(EXISTS "${Cairo_INCLUDE_DIRS}/cairo-version.h") - FILE(READ "${Cairo_INCLUDE_DIRS}/cairo-version.h" CAIRO_VERSION_CONTENT) - - STRING(REGEX MATCH "#define +CAIRO_VERSION_MAJOR +([0-9]+)" _dummy "${CAIRO_VERSION_CONTENT}") - SET(Cairo_VERSION_MAJOR "${CMAKE_MATCH_1}") - - STRING(REGEX MATCH "#define +CAIRO_VERSION_MINOR +([0-9]+)" _dummy "${CAIRO_VERSION_CONTENT}") - SET(Cairo_VERSION_MINOR "${CMAKE_MATCH_1}") - - STRING(REGEX MATCH "#define +CAIRO_VERSION_MICRO +([0-9]+)" _dummy "${CAIRO_VERSION_CONTENT}") - SET(Cairo_VERSION_MICRO "${CMAKE_MATCH_1}") - - SET(Cairo_VERSION "${Cairo_VERSION_MAJOR}.${Cairo_VERSION_MINOR}.${Cairo_VERSION_MICRO}") - endif() -endif() - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(Cairo REQUIRED_VARS Cairo_INCLUDE_DIRS Cairo_LIBRARIES - VERSION_VAR Cairo_VERSION) - -mark_as_advanced( - Cairo_INCLUDE_DIRS - Cairo_LIBRARIES -) - -if(Cairo_FOUND AND NOT TARGET Cairo::Cairo) - add_library(Cairo::Cairo INTERFACE IMPORTED) - set_property(TARGET Cairo::Cairo PROPERTY - INTERFACE_INCLUDE_DIRECTORIES "${Cairo_INCLUDE_DIRS}") - set_property(TARGET Cairo::Cairo PROPERTY - INTERFACE_LINK_LIBRARIES "${Cairo_LIBRARIES}") -endif() diff --git a/cmake/packages/FindGDK3.cmake b/cmake/packages/FindGDK3.cmake deleted file mode 100644 index 59672d9ab2..0000000000 --- a/cmake/packages/FindGDK3.cmake +++ /dev/null @@ -1,47 +0,0 @@ -# Distributed under the original FontForge BSD 3-clause license - -#[=======================================================================[.rst: -FindGDK3 ---------- - -Find the GDK3 library. This currently depends on pkg-config to work. - -Imported Targets -^^^^^^^^^^^^^^^^ - -This module defines the following :prop_tgt:`IMPORTED` target: - -``GDK3::GDK3`` - The GDK3 ``GDK3`` library, if found - -Result Variables -^^^^^^^^^^^^^^^^ - -This module will set the following variables in your project: - -``GDK3_FOUND`` - true if the GDK3 headers and libraries were found -``GDK3_INCLUDE_DIRS`` - directories containing the GDK3 headers. -``GDK3_LIBRARIES`` - the library to link against -``GDK3_VERSION`` - the version of GDK3 found - -#]=======================================================================] - -find_package(PkgConfig) -pkg_check_modules(GDK3 QUIET IMPORTED_TARGET gdk-3.0) - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args( - GDK3 - REQUIRED_VARS - GDK3_LIBRARIES - GDK3_INCLUDE_DIRS - VERSION_VAR - GDK3_VERSION -) - -include(${CMAKE_CURRENT_LIST_DIR}/../TargetUtils.cmake) -alias_imported_target(GDK3::GDK3 PkgConfig::GDK3) diff --git a/cmake/packages/FindPango.cmake b/cmake/packages/FindPango.cmake deleted file mode 100644 index 2f78353619..0000000000 --- a/cmake/packages/FindPango.cmake +++ /dev/null @@ -1,51 +0,0 @@ -# Distributed under the original FontForge BSD 3-clause license - -#[=======================================================================[.rst: -FindPango ---------- - -Find the Pango library. This currently depends on pkg-config to work. - -If additional ``COMPONENTS`` are passed in, these are passed on to -``pkg_check_modules()``. Typical usages for this are to include additional -dependencies on ``pangocairo``, ``pangoft2``, ``pangoxft`` and ``pangowin32``. - -Imported Targets -^^^^^^^^^^^^^^^^ - -This module defines the following :prop_tgt:`IMPORTED` target: - -``Pango::Pango`` - The Pango ``pango`` library, if found - -Result Variables -^^^^^^^^^^^^^^^^ - -This module will set the following variables in your project: - -``Pango_FOUND`` - true if the Pango headers and libraries were found -``Pango_INCLUDE_DIRS`` - directories containing the Pango headers. -``Pango_LIBRARIES`` - the library to link against -``Pango_VERSION`` - the version of Pango found - -#]=======================================================================] - -find_package(PkgConfig) -pkg_check_modules(Pango QUIET IMPORTED_TARGET pango ${Pango_FIND_COMPONENTS}) - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args( - Pango - REQUIRED_VARS - Pango_LIBRARIES - Pango_INCLUDE_DIRS - VERSION_VAR - Pango_pango_VERSION -) - -include(${CMAKE_CURRENT_LIST_DIR}/../TargetUtils.cmake) -alias_imported_target(Pango::Pango PkgConfig::Pango) \ No newline at end of file diff --git a/cmake/packages/FindReadline.cmake b/cmake/packages/FindReadline.cmake deleted file mode 100644 index 644df60c8b..0000000000 --- a/cmake/packages/FindReadline.cmake +++ /dev/null @@ -1,76 +0,0 @@ -# Distributed under the original FontForge BSD 3-clause license - -#[=======================================================================[.rst: -FindReadline ------------- - -Find the Readline library. - -Imported Targets -^^^^^^^^^^^^^^^^ - -This module defines the following :prop_tgt:`IMPORTED` target: - -``Readline::Readline`` - The Readline ``readline`` library, if found - -Result Variables -^^^^^^^^^^^^^^^^ - -This module will set the following variables in your project: - -``Readline_FOUND`` - true if the Readline headers and libraries were found -``Readline_INCLUDE_DIRS`` - directories containing the Readline headers. -``Readline_LIBRARIES`` - the library to link against -``Readline_VERSION`` - the version of Readline found - -#]=======================================================================] - -find_path(Readline_INCLUDE_DIRS - NAMES readline.h - HINTS ${Readline_ROOT_DIR} - PATH_SUFFIXES readline -) - -find_library(Readline_LIBRARIES - NAMES readline termcap - HINTS ${Readline_ROOT_DIR} -) - -if(Readline_INCLUDE_DIRS) - if(EXISTS "${Readline_INCLUDE_DIRS}/readline.h") - file(READ "${Readline_INCLUDE_DIRS}/readline.h" READLINE_H_CONTENTS) - string(REGEX MATCH "#define RL_VERSION_MAJOR[\t ]+([0-9]+)" _dummy "${READLINE_H_CONTENTS}") - set(Readline_VERSION_MAJOR "${CMAKE_MATCH_1}") - string(REGEX MATCH "#define RL_VERSION_MINOR[\t ]+([0-9]+)" _dummy "${READLINE_H_CONTENTS}") - set(Readline_VERSION_MINOR "${CMAKE_MATCH_1}") - set(Readline_VERSION "${Readline_VERSION_MAJOR}.${Readline_VERSION_MINOR}") - endif() -endif() - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args( - Readline - REQUIRED_VARS - Readline_LIBRARIES - Readline_INCLUDE_DIRS - VERSION_VAR - Readline_VERSION -) - -mark_as_advanced( - Readline_INCLUDE_DIRS - Readline_LIBRARIES -) - -if(Readline_FOUND AND NOT TARGET Readline::Readline) - add_library(Readline::Readline INTERFACE IMPORTED) - set_property(TARGET Readline::Readline PROPERTY - INTERFACE_INCLUDE_DIRECTORIES "${Readline_INCLUDE_DIRS}") - set_property(TARGET Readline::Readline PROPERTY - INTERFACE_LINK_LIBRARIES "${Readline_LIBRARIES}") -endif() diff --git a/cmake/packages/FindWOFF2.cmake b/cmake/packages/FindWOFF2.cmake deleted file mode 100644 index 523cea0155..0000000000 --- a/cmake/packages/FindWOFF2.cmake +++ /dev/null @@ -1,50 +0,0 @@ -# Distributed under the original FontForge BSD 3-clause license - -#[=======================================================================[.rst: -FindWOFF2 ---------- - -Find the WOFF2 library. - -Imported Targets -^^^^^^^^^^^^^^^^ - -This module defines the following :prop_tgt:`IMPORTED` target: - -``WOFF2::WOFF2`` - The WOFF2 ``woff2`` library, if found - -Result Variables -^^^^^^^^^^^^^^^^ - -This module will set the following variables in your project: - -``WOFF2_FOUND`` - true if the WOFF2 headers and libraries were found -``WOFF2_INCLUDE_DIRS`` - directories containing the WOFF2 headers. -``WOFF2_LIBRARIES`` - the library to link against -``WOFF2_VERSION`` - the version of WOFF2 found - -#]=======================================================================] - -find_package(PkgConfig) -pkg_check_modules(WOFF2 QUIET IMPORTED_TARGET libwoff2enc libwoff2dec) - -if(WOFF2_FOUND) - set(WOFF2_VERSION ${WOFF2_libwoff2enc_VERSION}) -endif() - -include(FindPackageHandleStandardArgs) -find_package_handle_standard_args( - WOFF2 - REQUIRED_VARS - WOFF2_LIBRARIES - VERSION_VAR - WOFF2_VERSION -) - -include(${CMAKE_CURRENT_LIST_DIR}/../TargetUtils.cmake) -alias_imported_target(WOFF2::WOFF2 PkgConfig::WOFF2) diff --git a/doc/sphinx/appendices/bibliography.rst b/doc/sphinx/appendices/bibliography.rst index 8453461892..4e3afd4940 100644 --- a/doc/sphinx/appendices/bibliography.rst +++ b/doc/sphinx/appendices/bibliography.rst @@ -68,25 +68,25 @@ Font File Formats (Sadly different sources have slightly different definitions of less important parts of the standard, be warned) - * `Apple `__ (I find Apple's prose + * `Apple `__ (I find Apple's prose difficult, and sometimes misleading. I suggest using a different source when possible) * `Microsoft `__ * `random useful site `__ * `TTC `__ -- True Type Font Collection -* `Apple Advanced Typography `__ +* `Apple Advanced Typography `__ extensions to TrueType * Apple distortable font (variation tables) -- vaguely equivalent to Multiple Master fonts for TrueType - * `fvar `__ (font + * `fvar `__ (font variations) - * `gvar `__ (glyph + * `gvar `__ (glyph variations) - * `cvar `__ (cvt + * `cvar `__ (cvt variations) - * `avar `__ (axis + * `avar `__ (axis variations) * .. _bibliography.OpenType: diff --git a/doc/sphinx/conf.py b/doc/sphinx/conf.py index c016c71ccb..72716fcfd9 100644 --- a/doc/sphinx/conf.py +++ b/doc/sphinx/conf.py @@ -16,8 +16,11 @@ copyright = '2000-2012 by George Williams, 2012-2025 by FontForge authors' author = 'FontForge authors' -# The full version, including alpha/beta/rc tags -release = '20251009' +# The full version - read from environment (set by ReadTheDocs) or default +# ReadTheDocs sets READTHEDOCS_VERSION to the tag/branch name +release = os.environ.get('READTHEDOCS_VERSION', '20251009') +if release.startswith('v'): + release = release[1:] # Strip 'v' prefix from tags # -- General configuration --------------------------------------------------- diff --git a/doc/sphinx/faq.rst b/doc/sphinx/faq.rst index 2762ccf0d7..4e9f287683 100644 --- a/doc/sphinx/faq.rst +++ b/doc/sphinx/faq.rst @@ -952,7 +952,7 @@ Random questions postscript file. It is based on the postscript font name but suffers a transformation. Don't try to rename this file. Basically the rules are (see - `Adobe Technical Note 0091 `__): + `Adobe Technical Note 0091 `__): * The fontname is broken into chunks where each chunk starts with a capital letter. diff --git a/doc/sphinx/fontutils.rst b/doc/sphinx/fontutils.rst index 84f735fc48..1b178254ed 100644 --- a/doc/sphinx/fontutils.rst +++ b/doc/sphinx/fontutils.rst @@ -13,12 +13,11 @@ https://github.com/fontforge/fontforge/tree/master/contrib/fonttools ShowTTF ------- -showttf is a program which will decompose a truetype (or opentype) font file +``showttf`` is a program which will decompose a truetype (or opentype) font file into its various tables and display the contents of those tables as best it can. It will also point out the errors it finds in the file. This is similar to -MicroSoft's `ttfdump `_ -(except mine shows some of Apple's tables which ttfdump does not), and to -Apple's `ftxdumperfuser `_ +fontTools `ttx `_ +utility, and to Apple's `ftxdumperfuser `_ (except mine shows some OpenType tables which Apple's ignores). And, of course, mine is open source and runs on most any system. @@ -30,14 +29,14 @@ mine is open source and runs on most any system. Ttf2Eps ------- -ttf2eps will allow you to extract one (or all) glyphs from a font and convert +``ttf2eps`` will allow you to extract one (or all) glyphs from a font and convert them into eps files. PfaDecrypt ----------- -pfadecrypt will undo Adobe's encryption on pfa/pfb files and converts the byte +``pfadecrypt`` will undo Adobe's encryption on pfa/pfb files and converts the byte codes into readable postscript like instructions. diff --git a/doc/sphinx/images/bitmapsavail.png b/doc/sphinx/images/bitmapsavail.png index 35ee6191d9..7b4872e2da 100644 Binary files a/doc/sphinx/images/bitmapsavail.png and b/doc/sphinx/images/bitmapsavail.png differ diff --git a/doc/sphinx/images/cvscalemode.gif b/doc/sphinx/images/cvscalemode.gif new file mode 100644 index 0000000000..934b6c5647 Binary files /dev/null and b/doc/sphinx/images/cvscalemode.gif differ diff --git a/doc/sphinx/images/findprobs-att.png b/doc/sphinx/images/findprobs-att.png index d9980c51c3..4686e36795 100644 Binary files a/doc/sphinx/images/findprobs-att.png and b/doc/sphinx/images/findprobs-att.png differ diff --git a/doc/sphinx/images/findprobs-bb.png b/doc/sphinx/images/findprobs-bb.png index 8792a4168d..3b55e17a2c 100644 Binary files a/doc/sphinx/images/findprobs-bb.png and b/doc/sphinx/images/findprobs-bb.png differ diff --git a/doc/sphinx/images/findprobs-cid.png b/doc/sphinx/images/findprobs-cid.png index 9e9b90a91d..454e6d47e8 100644 Binary files a/doc/sphinx/images/findprobs-cid.png and b/doc/sphinx/images/findprobs-cid.png differ diff --git a/doc/sphinx/images/findprobs-hint.png b/doc/sphinx/images/findprobs-hint.png index 843b323a14..16eb43a1f4 100644 Binary files a/doc/sphinx/images/findprobs-hint.png and b/doc/sphinx/images/findprobs-hint.png differ diff --git a/doc/sphinx/images/findprobs-paths.png b/doc/sphinx/images/findprobs-paths.png index 017fa9020c..e659bad4f3 100644 Binary files a/doc/sphinx/images/findprobs-paths.png and b/doc/sphinx/images/findprobs-paths.png differ diff --git a/doc/sphinx/images/findprobs-random.png b/doc/sphinx/images/findprobs-random.png index ff00889bd0..470a415bc3 100644 Binary files a/doc/sphinx/images/findprobs-random.png and b/doc/sphinx/images/findprobs-random.png differ diff --git a/doc/sphinx/images/findprobs-refs.png b/doc/sphinx/images/findprobs-refs.png index dcf8a75e6d..4a829137fe 100644 Binary files a/doc/sphinx/images/findprobs-refs.png and b/doc/sphinx/images/findprobs-refs.png differ diff --git a/doc/sphinx/images/findprobs.png b/doc/sphinx/images/findprobs.png index ee28face0d..f290a5865d 100644 Binary files a/doc/sphinx/images/findprobs.png and b/doc/sphinx/images/findprobs.png differ diff --git a/doc/sphinx/images/greymapsavail.png b/doc/sphinx/images/greymapsavail.png index 9a788e6efd..50d7609d8d 100644 Binary files a/doc/sphinx/images/greymapsavail.png and b/doc/sphinx/images/greymapsavail.png differ diff --git a/doc/sphinx/images/histogram.png b/doc/sphinx/images/histogram.png index c1eb1f8776..b52ba3bbf1 100644 Binary files a/doc/sphinx/images/histogram.png and b/doc/sphinx/images/histogram.png differ diff --git a/doc/sphinx/images/lang-dlg.png b/doc/sphinx/images/lang-dlg.png index 4f891e8815..7850e93256 100644 Binary files a/doc/sphinx/images/lang-dlg.png and b/doc/sphinx/images/lang-dlg.png differ diff --git a/doc/sphinx/requirements.txt b/doc/sphinx/requirements.txt new file mode 100644 index 0000000000..dae33fb0bb --- /dev/null +++ b/doc/sphinx/requirements.txt @@ -0,0 +1,2 @@ +# Sphinx documentation build requirements +sphinx>=4.0 diff --git a/doc/sphinx/scripting/python-extension.rst b/doc/sphinx/scripting/python-extension.rst new file mode 100644 index 0000000000..8311c4cad8 --- /dev/null +++ b/doc/sphinx/scripting/python-extension.rst @@ -0,0 +1,128 @@ +Python Extension Technical Details +================================== + +This document covers the technical details of FontForge's Python extension +module, including building wheels for PyPI distribution. + +Modules +------- + +FontForge provides two Python modules: + +- ``fontforge`` - Main font manipulation API +- ``psMat`` - PostScript matrix operations + +Build Scenarios +--------------- + +.. list-table:: + :header-rows: 1 + + * - Scenario + - Detection + - GUI + - Install Location + * - Wheel (pip) + - ``SKBUILD`` defined + - OFF + - Wheel root + * - System install + - Manual cmake + - ON/OFF + - site-packages + +Dependencies +------------ + +Required (always linked): + libxml2, zlib, libiconv + +Bundled in wheels: + FreeType, libpng, libjpeg, libtiff, libspiro, woff2 + +Not needed for wheels (GUI-only): + GLIB, Gettext/Intl, Readline + +Versioning +---------- + +The source uses ``PROJECT_VERSION = 20251009`` (YYYYMMDD format). +Wheels use CalVer format ``2025.10.9``. + +``fontforge.version()`` returns: + +- CalVer when imported as a module (from site-packages or wheel) +- Numeric format when Python is embedded in the running FontForge application + +CMake transformation:: + + string(SUBSTRING "${PROJECT_VERSION}" 0 4 _FF_YEAR) + string(SUBSTRING "${PROJECT_VERSION}" 4 2 _FF_MONTH) + string(SUBSTRING "${PROJECT_VERSION}" 6 2 _FF_DAY) + math(EXPR _FF_MONTH_INT "${_FF_MONTH}") + math(EXPR _FF_DAY_INT "${_FF_DAY}") + set(FONTFORGE_PYTHON_VERSION "${_FF_YEAR}.${_FF_MONTH_INT}.${_FF_DAY_INT}") + +Windows MSVC Build +------------------ + +Windows wheels require MSVC because Python.org's Windows Python is built +with MSVC. The MinGW build continues for the GUI application. + +MSVC compatibility patterns:: + + // UNUSED macro + #ifdef _MSC_VER + # define UNUSED(x) x + #elif defined(__GNUC__) + # define UNUSED(x) UNUSED_ ## x __attribute__((unused)) + #endif + +DLL exports: MinGW uses ``--export-all-symbols``. MSVC needs explicit +``__declspec(dllexport)`` or a DEF file. + +vcpkg provides dependencies:: + + { + "name": "fontforge", + "dependencies": ["freetype", "libxml2", "glib", "gettext", "libiconv", "zlib"] + } + +Build Configuration +------------------- + +Wheels are built using scikit-build-core. See ``pyproject.toml`` for the +current configuration. + +:: + + [build-system] + requires = ["scikit-build-core>=0.5"] + build-backend = "scikit_build_core.build" + + [project] + name = "fontforge" + dynamic = ["version"] + + [tool.scikit-build] + cmake.args = ["-DENABLE_GUI=OFF", "-DENABLE_PYTHON_EXTENSION=ON"] + wheel.install-dir = "." + +Wheel Target Platforms +---------------------- + +The project builds wheels for: + +- Linux: x86_64, aarch64 (manylinux_2_28) +- macOS: x86_64, arm64 (separate wheels, macOS 11.0+) +- Windows: x64 (MSVC) + +Install Conflict Handling +------------------------- + +App installs create ``fontforge-{version}.dist-info/`` with +``INSTALLER = fontforge-app``. This allows ``pip list`` to show the install +and ``pip uninstall`` to work. + +If a pip-installed version exists, app install skips and suggests +``pip uninstall fontforge``. diff --git a/doc/sphinx/scripting/python.rst b/doc/sphinx/scripting/python.rst index c8b6296f04..44e8da4be8 100644 --- a/doc/sphinx/scripting/python.rst +++ b/doc/sphinx/scripting/python.rst @@ -57,13 +57,40 @@ Trivial example .. _python.extension: -FontForge as a python extension +FontForge as a Python extension ------------------------------- -In addition to embedding Python, FontForge typically installs a Python -module accessible to the system's ``python`` executable, which can be -accessed using: +FontForge's Python modules can be used independently of the GUI application. + +Installation via pip +^^^^^^^^^^^^^^^^^^^^ + +:: + + pip install fontforge + +Pre-built wheels are available for: + +- Linux: x86_64, aarch64 +- macOS: x86_64, arm64 +- Windows: x86_64 + +The wheels include all required native libraries (FreeType, libxml2, etc.). + +Installation from system packages +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +When FontForge is installed via system package manager or built from source +with ``-DENABLE_PYTHON_EXTENSION=ON``, the modules are installed to the +system Python's site-packages. + +Usage +^^^^^ :: >>> import fontforge + >>> import psMat + +For technical details on building wheels and the extension architecture, +see :doc:`python-extension`. diff --git a/doc/sphinx/scripting/python/fontforge.rst b/doc/sphinx/scripting/python/fontforge.rst index d9c6ede00d..875325a3b4 100644 --- a/doc/sphinx/scripting/python/fontforge.rst +++ b/doc/sphinx/scripting/python/fontforge.rst @@ -345,6 +345,11 @@ Module functions Returns a tuple listing the directory paths which are searched for python scripts during FontForge initialization. +.. function:: userConfigPath() + + Returns the path to the user's FontForge configuration directory, which should + be writable. + .. function:: fonts() Returns a tuple of all fonts currently loaded into FontForge for editing @@ -409,6 +414,15 @@ Module functions Retain all recognized font tables that do not have a native format. + This function can also be used with the ``with`` statement, in which case + there is no need to call :meth:`font.close()` explicitly: + + :: + + with fontforge.open('somefont.sfd') as fnt: + # do something + fnt.generate('somefont.ttf') + .. function:: parseTTInstrs(string) Returns a binary string each byte of which corresponds to a truetype @@ -439,6 +453,9 @@ Module functions and computing the optical left and right side bearings (for 'lfbd' and 'rtbd' features). For more information see its own section. +.. function:: onAppClosing(hook) + + Add a python function which is called when FontForge is closing down. .. _fontforge.ui_functions: @@ -682,6 +699,16 @@ Not a very useful example. should be a comma separated list of extensions. It may be omitted, in which case it defaults to being the same as the "extension" argument above. +.. function:: getConvexNib(context) + + Returns the specified 'Convex' nib as a layer. ``context`` may be + ``"default"``, ``"freehand"``, or ``"ui"``. + +.. function:: setConvexNib(nib, context) + + Sets the specified 'Convex' to a layer/contour. ``context`` may be + ``"default"``, ``"freehand"``, or ``"ui"``. + .. function:: logWarning(msg) Adds the message (a string) to FontForge's Warnings window. (if you wish to @@ -1323,6 +1350,10 @@ Layers may be compared to see if their contours are similar. the current layer and the first argument. If amount is 0 the result will look like the current layer, if 1 then like the first argument. +.. method:: layer.reverseDirection() + + Reverse the orientation of each contour in the layer. + .. method:: layer.round([factor]) Rounds the x and y coordinates. If factor is specified then :: @@ -1440,14 +1471,14 @@ Layers may be compared to see if their contours are similar. .. method:: layer.xBoundsAtY(ybottom[, ytop]) - Finds the minimum and maximum x positions attained by the contour when y is + Finds the minimum and maximum x positions attained by the layer when y is between ybottom and ytop (if ytop is not specified it is assumed the same as ybottom). If the layer does not have any y values in the specified range then FontForge will return ``None``. .. method:: layer.yBoundsAtX(xleft[, xright]) - Finds the minimum and maximum y positions attained by the contour when x is + Finds the minimum and maximum y positions attained by the layer when x is between xleft and xright (if xright is not specified it is assumed the same as xleft). If the layer does not have any x values in the specified range then FontForge will return ``None``. @@ -1670,6 +1701,11 @@ must be created through the font. Whether this glyph has been modified. This is (should be) maintained automatically, but you may set it if you wish. +.. attribute:: glyph.codepoint + + Unicode code point for this glyph in U+XXXX format for the Basic Multilingual + Plane, and up to U+XXXXXX for the supplementary planes, or ``None``. (readonly) + .. attribute:: glyph.color The color of the glyph in the fontview. A 6 hex-digit RGB number or -1 for @@ -2702,15 +2738,32 @@ must be created through the font. validated, if force is unspecified (or specified as false) then it will return the cached value if it is known, otherwise will validate it. +.. method:: glyph.xBoundsAtY(ybottom[, ytop, layer=]) + + Finds the minimum and maximum x positions attained by the glyph when y is + between ybottom and ytop (if ytop is not specified it is assumed the same as + ybottom). If the glyph does not have any y values in the specified range + then FontForge will return ``None``. A layer name or index may be provided + to only find bounds for a particular layer. + +.. method:: glyph.yBoundsAtX(xleft[, xright, layer=]) + + Finds the minimum and maximum y positions attained by the glyph when x is + between xleft and xright (if xright is not specified it is assumed the same + as xleft). If the glyph does not have any x values in the specified range + then FontForge will return ``None``. A layer name or index may be provided + to only find bounds for a particular layer. + .. method:: glyph.draw(pen) Draw the glyph's outline to the `pen argument. `_ -.. method:: glyph.glyphPen([replace=False]) +.. method:: glyph.glyphPen([replace=True]) Creates a new glyphPen which will draw into the current glyph. By default the pen will replace any existing contours and references, but setting the optional keyword argument, ``replace`` to false will retain the old contents. + Replacing the foreground layer will also reset the advance. .. method:: glyph.addInflections() @@ -2750,6 +2803,10 @@ This type may not be pickled. This is read-only. +.. attribute:: selection.font + + Returns the font for which this is a selection. + .. method:: selection.__iter__() Returns an iterator for the selection which will return all selected @@ -3293,6 +3350,10 @@ This type may not be pickled. PostScript copyright notice +.. attribute:: font.creationtime + + Font creation time. (readonly) + .. attribute:: font.cvt Returns a sequence object containing the font's cvt table. Changes made @@ -3522,6 +3583,16 @@ This type may not be pickled. (`source `_) +.. attribute:: font.markClasses + + A tuple each entry of which is itself a tuple containing a mark-class-name + and a tuple of glyph-names. + +.. attribute:: font.markSets + + A tuple each entry of which is itself a tuple containing a mark-set-name + and a tuple of glyph-names. + .. attribute:: font.layer_cnt The number of layers in the font. (Read only. Can change using ``add`` @@ -3947,7 +4018,13 @@ This type may not be pickled. .. attribute:: font.style_set_names - A tuple, each entry of which is a 3-element tuple containing the language name (e.g. ``"English (US)"``), the style set tag (e.g. ``"ss01"``) and the style set name. + A tuple, each entry of which is a 3-element tuple containing: + * For style sets, the language name (e.g. ``"English (US)"``), the style set + tag (e.g. ``"ss01"``) and the style set name. + * For character variants, the language name, the character variant tag (e.g. + ``"cv01"``) and a 5-element tuple containing the feature name, the tooltip text, + the sample text (these three can be ``None``), a tuple containing parameter + names (can be empty) and ``None`` (reserved for the character list). .. attribute:: font.temporary @@ -4049,6 +4126,9 @@ This type may not be pickled. might have no lower case letters because it was upper case only, or didn't include glyphs for a script with lower case letters). +.. attribute:: font.xuid + + PostScript eXtended Unique ID. .. method:: font.__iter__() @@ -4114,7 +4194,7 @@ This type may not be pickled. * ``gsub_ligature`` * ``gsub_context`` * ``gsub_contextchain`` - * ``gsub_revesechain`` + * ``gsub_reversecchain`` * ``morx_indic`` * ``morx_context`` * ``morx_insert`` @@ -4129,7 +4209,7 @@ This type may not be pickled. * ``kern_statemachine`` The flags argument is a tuple of strings, or ``None``. At most one of these - strings may be the name of a mark class. The others are: + strings may be the name of a mark class or a mark set. The others are: * ``right_to_left`` * ``ignore_bases`` @@ -4169,8 +4249,10 @@ This type may not be pickled. The ``type`` should be one of the strings "glyph", "class", "coverage" or "reversecoverage". The ``rule`` should be a string specifying a string to - match and a set of lookups to apply once the match has been made. (See - below for more details). + match and a set of lookups to apply once the match has been made. For + "glyph" and "class" subtables, ``rule`` may also be a sequence of strings; + passing a sequence creates one subtable containing multiple contextual rules. + (See below for more details). The remaining arguments are optional, keyword arguments. @@ -4348,6 +4430,10 @@ This type may not be pickled. Removes the current subfont from a cid-keyed font. +.. method:: font.clearSpecialData() + + Clear special data not accessible in FontForge. + .. method:: font.close() Frees memory for the current font. @@ -4536,6 +4622,11 @@ This type may not be pickled. Include a 'TeX ' table in an ttf/otf file + .. object:: no-mac-names + + Do not include Mac names used on Classic Mac OS. This option does not + affect native macOS (formerly known as Mac OS X) applications. + .. object:: round Round PS coordinates to integers @@ -4654,7 +4745,7 @@ This type may not be pickled. x-height and another for the top of capitals and ascenders (and perhaps a fourth for descenders). Each such zone is specified by the ``vMap`` argument which is a tuple of 3-tuples, each 3-tuple specifying a zone with: Original - location, original width, and final location. + location, final location, and original width. .. note:: @@ -4725,7 +4816,7 @@ This type may not be pickled. Returns whether the named subtable contains a vertical kerning data -.. method:: font.italicize(italic_angle=, ia=lc_condense=, lc=uc_condense=, uc=symbol_condense=, symbol=deserif_flat=, deserif_slant=, deserif_pen=, baseline_serifs=, xheight_serifs=, ascent_serifs=, descent_serifs=, diagonal_serifs=, a=, f=, u0438=, u043f=, u0442=, u0444=, u0448=, u0452=, u045f=) +.. method:: font.italicize(italic_angle=, ia=lc_condense=, lc=uc_condense=, uc=symbol_condense=, symbol=deserif_flat=, deserif_slant=, deserif_pen=, baseline_serifs=, xheight_serifs=, ascent_serifs=, descent_serifs=, diagonal_serifs=, a=, f=, u0438=, u043f=, u0442=, u0444=, u0448=, u0452=, u045f=, xheight_percent=) This function uses keyword parameters. None are required, if omitted a default value will be used. Some keywords have abbreviations ("ia" for @@ -4752,6 +4843,10 @@ This type may not be pickled. which looks like a rotated version of its head, and setting it to 2 will simply extend the main stem of "f" below the baseline. + The ``xheight_percent`` keyword specifies how much the x-height should be + changed by the transformation. The default value is 0.95, which reduces the + x-height to 95% of its original size; a value of 1.0 means no change. + .. method:: font.lookupSetFeatureList(lookup_name, feature_script_lang_tuple) Sets the feature list of indicated lookup. The feature-script-lang tuple is diff --git a/doc/sphinx/scripting/scripting.rst b/doc/sphinx/scripting/scripting.rst index 6d7721a445..b3b1777fa9 100644 --- a/doc/sphinx/scripting/scripting.rst +++ b/doc/sphinx/scripting/scripting.rst @@ -12,6 +12,7 @@ means use the old interpreter) scripting-alpha python + python-extension .. contents:: :backlinks: none diff --git a/doc/sphinx/techref/TrueOpenTables.rst b/doc/sphinx/techref/TrueOpenTables.rst index b92f47a51b..5da808b625 100644 --- a/doc/sphinx/techref/TrueOpenTables.rst +++ b/doc/sphinx/techref/TrueOpenTables.rst @@ -1,7 +1,7 @@ TrueType and OpenType tables supported by FontForge =================================================== -`Apple `__, +`Apple `__, `MS `__ and `Adobe `__ all have their descriptions of TrueType and OpenType. MS and Adobe's versions @@ -19,7 +19,7 @@ are essentially the same, while Apple's differ significantly from either. * - avar - axis variation - Used to specify piecewise linear sections of axes for distortable fonts - - `avar `__ + - `avar `__ - * * - BASE - baseline data @@ -30,7 +30,7 @@ are essentially the same, while Apple's differ significantly from either. * - bsln - baseline data - Provides information on baseline positioning for AAT. - - `bsln `__ + - `bsln `__ - * * - BDF - X11 BDF header @@ -43,24 +43,24 @@ are essentially the same, while Apple's differ significantly from either. - bitmap data - Provides the actual bitmap data for all bitmap glyphs in Apple fonts. 'EBDT' is used by MS/Adobe instead (and has the same format) - - `bdat `__ + - `bdat `__ - * * - bhed - bitmap font header - Replaces the 'head' table in Apple's bitmap only fonts. (similar to 'head') - - `bhed `__ + - `bhed `__ - * * - bloc - bitmap location data - Provides pointers to the appropriate bitmap data for each bitmap glyph in Apple fonts. 'EBLC' is used by MS/Adobe instead (and has the same format) - - `bloc `__ + - `bloc `__ - * * - CFF - - `Compact Font Format `__ + - `Compact Font Format `__ - Provides all the outline data (and sometimes glyph names as well) for the PostScript - `Type2 `__ + `Type2 `__ font that is the heart of an OpenType font. Vaguely equivalent to 'glyf' , 'loca' and 'post' combined. - * @@ -76,18 +76,18 @@ are essentially the same, while Apple's differ significantly from either. For fonts with special 1-byte encodings (such as symbol) it will provide a "symbol" encoding which maps a known page in the corporate use area to the glyphs. - - `cmap `__ + - `cmap `__ - `cmap `__ * - cvar - variations on cvt table - Used to specify hinting differences in distortable fonts - - `cvar `__ + - `cvar `__ - * * - cvt - control value table - FontForge uses this when it attempts to hint truetype fonts (FontForge will read it from a font and save it into another). - - `cvt `__ + - `cvt `__ - `cvt `__ * - EBDT - embedded bitmap data @@ -106,14 +106,14 @@ are essentially the same, while Apple's differ significantly from either. - Provides information on how to scale bitmaps (on those rare occasions where this is desirable). FontForge uses it when making bitmap only fonts for windows. - - `EBSC `__ + - `EBSC `__ - `EBSC `__ * - feat - layout feature table - Maps features specified in Apple's 'morx' (or 'mort') tables into names provided in the 'name' table. FontForge generates this when it generates a 'morx' table. - - `feat `__ + - `feat `__ - * * - FFTM - FontForge timestamp @@ -126,20 +126,20 @@ are essentially the same, while Apple's differ significantly from either. - font program - FontForge never generates this, but it will preserve it and allows users to edit it. - - `fpgm `__ + - `fpgm `__ - `fpgm `__ * - fvar - font variations - Provides top level information about distortable fonts. Specifies the types of distortions possible. - - `fvar `__ + - `fvar `__ - * * - gasp - grid-fitting and scan conversion - When FontForge does not attempt to hint a truetype font it will generate this table which tells the rasterizer not to to do grid-fitting (hinting) but to do anti-aliasing instead. - - `gasp `__ + - `gasp `__ - `gasp `__ * - GDEF - glyph definition @@ -152,7 +152,7 @@ are essentially the same, while Apple's differ significantly from either. - glyph outline - Provides outline information for truetype glyphs. Vaguely equivalent to 'CFF '. - - `glyf `__ + - `glyf `__ - `glyf `__ * - GPOS - glyph positioning @@ -172,43 +172,43 @@ are essentially the same, while Apple's differ significantly from either. - glyph variations - This table contains the meat of a distortable font. It specifies how each glyph can be distorted. - - `gvar `__ + - `gvar `__ - * * - head - font header - Contains general font information, such as the size of the em-square, a time stamp, check sum information, etc. - - `head `__ + - `head `__ - `head `__ * - hhea - horizontal header - This table contains font-wide horizontal metric information. - - `hhea `__ + - `hhea `__ - `hhea `__ * - hmtx - horizontal metrics - This contains the per-glyph horizontal metrics. - - `hmtx `__ + - `hmtx `__ - `hmtx `__ * - kern - kerning - Provides kerning information for Apple's truetype fonts (and for older MS truetype fonts). In OpenType fonts this information is contained in the GPOS table. - - `kern `__ + - `kern `__ - `kern `__ * - lcar - ligature caret - This table provides the location of carets within ligatures in an Apple font. This information is contained in the GDEF table in opentype fonts. - - `lcar `__ + - `lcar `__ - * * - loca - glyph location - This provides a pointer into the 'glyf' table for each glyph in the font. It is required for truetype and meaningless for opentype (where vaguely equivalent information is provided in the CFF table). - - `loca `__ + - `loca `__ - `loca `__ * - MATH - mathematical typesetting @@ -223,14 +223,14 @@ are essentially the same, while Apple's differ significantly from either. - Provides general "maximum" information about the font. This contains: the maximum number of glyphs in a font, the maximum number of points in a glyph, the maximum number of references in a glyph, etc. - - `maxp `__ + - `maxp `__ - `maxp `__ * - mort - metamorphosis - FontForge will read this table but not generate it. This table has been replaced by the 'morx' table which will be generated instead. It is vaguely equivalent to the GSUB table. - - `mort `__ + - `mort `__ - * * - morx - extended metamorphosis @@ -238,21 +238,21 @@ are essentially the same, while Apple's differ significantly from either. FontForge can read and write this table. See the :ref:`chapter on morx ` in FontForge for more information. Vaguely equivalent to 'GSUB'. - - `morx `__ + - `morx `__ - * * - name - name - Provides certain standard strings relevant to the font (font name, family name, license, etc.) in various languages. In Apple fonts also provides the names of various features of the 'morx'/'mort' table. - - `name `__ + - `name `__ - `name `__ * - opbd - optical bounds - This table provides optical bound information for each glyph in the table in an Apple font. This information may also be provided in the GPOS table of an opentype font. - - `opbd `__ + - `opbd `__ - * * - PfEd - FontForge's personal table @@ -264,19 +264,19 @@ are essentially the same, while Apple's differ significantly from either. - glyph name and postscript compatibility - This table provides some additional postscript information (italic angle), but mostly it provides names for all glyphs. - - `post `__ + - `post `__ - `post `__ * - prep - cvt program - FontForge never generatest this table itself, but it will retain it when reading other fonts, and it allows users to edit it. - - `prep `__ + - `prep `__ - `prep `__ * - prop - glyph properties - Provides the unicode properties of each glyph for an Apple font. This table bears some similarities to the GDEF table. - - `prop `__ + - `prop `__ - * * - TeX - TeX information @@ -288,13 +288,13 @@ are essentially the same, while Apple's differ significantly from either. - vertical header - FontForge generates this table for fonts with vertical metrics. This table contains font-wide vertical metric information. - - `vhea `__ + - `vhea `__ - `vhea `__ * - vmtx - vertical metrics - FontForge generates this table for fonts with vertical metrics. This contains the per-glyph vertical metrics. - - `vmtx `__ + - `vmtx `__ - `vmtx `__ :doc:`Advanced Typography tables in FontForge ` \ No newline at end of file diff --git a/doc/sphinx/techref/cliargs.rst b/doc/sphinx/techref/cliargs.rst index 622cadd6bd..462ddc3d34 100644 --- a/doc/sphinx/techref/cliargs.rst +++ b/doc/sphinx/techref/cliargs.rst @@ -47,30 +47,10 @@ FontForge recognizes the following options: Will read a font from "foo.sfd" and then generate a truetype font from it called "foo.ttf" -.. option:: -cmap type - - Where type may be - - Current | Copy | Private - - Gives the user some control over :ref:`colormap handling ` on - 8bit screens. - -.. option:: -depth val - - Specifies that FontForge should search for a visual with the given depth - .. option:: -display name Specifies the name of the display on which FontForge will open its windows -.. option:: -dontopenxdevices - - Various people have complained that when FontForge attempts to open the - devices of the wacom graphics tablet, the X server gives a BadDevice error. I - can't duplicate this, the open works fine on my system, but this argument - allows them to tell fontforge not to try to use the tablet. - .. option:: -help Bring up a :doc:`browser ` looking at this documentation. @@ -85,33 +65,6 @@ FontForge recognizes the following options: Prints out the source version and exits. -.. option:: -keyboard type - - .. warning:: Deprecated option, may not do anything - - Where type may be - - ibm | mac | sun | ppc | 0 | 1 | 2 | 3 - - Allows you to specify the type of keyboard. Currently this is only relevant - when generating menus. The modifier keys are in different locations on - different keyboards (under different operating systems) and if FontForge - knows what keyboard you are using it can make the hot-keys have better - labels. - - * ibm | 0 - - Uses the Control and Alt keys - * mac | 1 - - Uses the Control and Option keys (Mac OS/X, Mac keyboard) - * ppc | 3 - - Uses the Control and Command keys (Suse ppc linux, Mac keyboard) - * sun | 2 - - Uses the Control and Meta keys - .. option:: -last Opens the last sfd file closed. If used more than once will open the last @@ -190,16 +143,6 @@ FontForge recognizes the following options: Display a brief description of the options -.. option:: -vc type - - Where type may be: - - StaticGray GrayScale StaticColor PseudoColor TrueColor DirectColor - - (See the X manuals for a description of what these mean). FontForge will - search through the visuals in an attempt to find one with the desired - VisualClass (given here) and depth (given with the -depth option). - .. _cliargs.Environment: diff --git a/doc/sphinx/techref/gposgsub.rst b/doc/sphinx/techref/gposgsub.rst index e5de5848eb..99a4060cd4 100644 --- a/doc/sphinx/techref/gposgsub.rst +++ b/doc/sphinx/techref/gposgsub.rst @@ -357,17 +357,17 @@ Apple Advanced Typography As above I do not go deeply into the abilities of these tables, for more information see Apple's docs: -* `The 'bsln' (baseline) table `__ -* `The 'kern' table `__ -* `The 'lcar' (ligature caret) table `__ -* `The 'morx' (extended glyph metamorphosis) table `__ +* `The 'bsln' (baseline) table `__ +* `The 'kern' table `__ +* `The 'lcar' (ligature caret) table `__ +* `The 'morx' (extended glyph metamorphosis) table `__ - * `The 'mort' (older version of 'morx') table `__ - * `The 'feat' (feature) table `__ + * `The 'mort' (older version of 'morx') table `__ + * `The 'feat' (feature) table `__ * `Apple's Font Feature registry `__ - * `Description of the subtables of the 'mort'/'morx' tables `__ -* `The 'opbd' (optical bounds) table `__ -* `The 'prop' (glyph properties) table `__ + * `Description of the subtables of the 'mort'/'morx' tables `__ +* `The 'opbd' (optical bounds) table `__ +* `The 'prop' (glyph properties) table `__ FontForge will currently read and produce (if Apple mode is set in font generation) the following tables: diff --git a/doc/sphinx/techref/hinting.rst b/doc/sphinx/techref/hinting.rst index 04b8d717b0..fe1cdc9664 100644 --- a/doc/sphinx/techref/hinting.rst +++ b/doc/sphinx/techref/hinting.rst @@ -213,10 +213,10 @@ Hint questions: I find the hint documentation inadequate for me to do a good job at hinting. * Section 2.4 of - `T1_Supp.pdf `__ + `T1_Supp.pdf `__ says that vertical counters are offset from the lbearing, but the example 2.6 shows them being offset from 0. -* In `Type2 `__ is +* In `Type2 `__ is it ok to use Counter mask if LanguageGroup is not 1 and the stems don't fit into a \*stem3 pattern? Or can cntrmask only be used for \*stem3 in latin letters? * .. image:: /images/NoPointHint.png diff --git a/doc/sphinx/tutorial/editexample8.rst b/doc/sphinx/tutorial/editexample8.rst index 36da3200d2..47051de2bc 100644 --- a/doc/sphinx/tutorial/editexample8.rst +++ b/doc/sphinx/tutorial/editexample8.rst @@ -64,12 +64,12 @@ Creating Bitmap strikes If you want to add a bitmap strike with a given pixel size to a font then use :ref:`Element->Bitmap Strikes Available `. Simply add the -pixel size you are interested in to the list at the bottom of the screen. +pixel size you are interested in to the list at the "Pixel sizes" field. -If you are more comfortable dealing in point sizes, be aware that those depend -on screen resolution. This dialog shows you the conversion between point and -pixel sizes for two standard resolutions. The resolutions differ depending on -what windowing system you use. +For convenience, the available pixel sizes are also shown in point sizes, which +depend on screen resolution. This dialog shows you the conversion between point +and pixel sizes for the actual resolution of your monitor, as reported by the +operating system, and for the typical 96 PPI resolution. You may add more than one pixel size, of course. You may also remove pixel sizes if you no longer want them. diff --git a/doc/sphinx/ui/dialogs/fontinfo.rst b/doc/sphinx/ui/dialogs/fontinfo.rst index deaed02690..d66c5286d4 100644 --- a/doc/sphinx/ui/dialogs/fontinfo.rst +++ b/doc/sphinx/ui/dialogs/fontinfo.rst @@ -400,7 +400,7 @@ PANOSE This sub-dialog allows you to describe your font in 10 different dimensions. The exact meanings of many of these entries vary from script to script, and even the Latin ones are not clear to me, I have merely typed them in as specified in the -`true type docs `__. Better +`true type docs `__. Better information is available from `HP `__, `MS `__, `RS `__, @@ -584,7 +584,7 @@ The various strings and a brief description of their meanings are: Whatever. These are described in the -`original true type docs `__, +`original true type docs `__, but they apply to `open type `__ as well. diff --git a/doc/sphinx/ui/dialogs/histogram.rst b/doc/sphinx/ui/dialogs/histogram.rst index cad7a1c9ec..2e32cd0d3d 100644 --- a/doc/sphinx/ui/dialogs/histogram.rst +++ b/doc/sphinx/ui/dialogs/histogram.rst @@ -1,8 +1,7 @@ The histogram dialog ==================== -.. image:: /images/histogram.png - :align: right +.. figure:: /images/histogram.png The histogram dialog is used to display three things which are useful for setting the some of the global hinting values that live in the PostScript @@ -18,31 +17,33 @@ setting the some of the global hinting values that live in the PostScript which is helpful in setting [BlueValues] and [OtherBlues] -Moving the mouse around the histogram at the top of the dialog will cause little -popups to appear showing how many stems there are with a given width, how that +Moving the mouse around the histogram at the top of the dialog will cause a +tooltip to appear showing how many stems there are with a given width, how that compares to the maximum count, and what glyphs have that width. For both HStem and VStem, if you click on a histogram bar then that width will become the new value for Std[H/V]W, and will be the sole value for StemSnap[H/V]. Shift-clicking on a bar will add that width to StemSnap[H/V]. -For BlueValues, entries must be made in pairs, and FontForge can't tell until -the second value is selected where the pair should be placed. So for BlueValues -you must click on two histogram bars (click on the leftmost one first). Then if -the second value is negative both will be added to OtherBlues, otherwise both -will be added to BlueValues. +For BlueValues, entries must be made in pairs, and FontForge uses the first +value to determine where the pair should be placed. If the first value is +negative it will be added to OtherBlues, otherwise it will be added to +BlueValues. The missing second value is marked by ``?`` in the dialog until you +click on a second bar, at which point the pair will be completed. -The BarWidth textfield allows you to change the width of the histogram bars. The +The "Bar Width" textfield allows you to change the width of the histogram bars. The default value is 6 pixels wide. -The SumAround textfield is an attempt to give you a feel for the distribution if -it has been smeared out. If SumAround is 0 then the count displayed for a given -width is the number of stems with that width, if SumAround is 1 the count also -includes the number of stems that are one unit wider or less wide than the given -value, and higher values of SumAround give wider smearing. Just because a given -width is at a local maxima does not mean that it is the appropriate value to be -entered in the StemSnap array, you may find that if you smear things out a bit -that a near-by value would be a slightly better choice. +The "Smoothing Window" textfield computes the moving avarage of the histogram +data. This is an attempt to give you a feel for the distribution if it has been +smeared out. If the window size is above 1 then the count displayed for a given +width is the average of stems with a similar width, laying within that window. +For example, if the window size is 3 the count also includes the number of +stems that are one unit wider or less wide than the given value, and larger +window sizes give wider averaging. Just because a given width is at a local +maximum does not mean that it is the appropriate value to be entered in the +StemSnap array, you may find that if you average things out a bit then a near-by +value would be a slightly better choice. Pressing [OK] will set the indicated values in the font's private dictionary. diff --git a/doc/sphinx/ui/dialogs/multiplemaster.rst b/doc/sphinx/ui/dialogs/multiplemaster.rst index 32926525cd..cd469fb8d1 100644 --- a/doc/sphinx/ui/dialogs/multiplemaster.rst +++ b/doc/sphinx/ui/dialogs/multiplemaster.rst @@ -10,11 +10,11 @@ What is a multiple master font? ------------------------------- A multiple master font is a -(`PostScript Type1 `__) +(`PostScript Type1 `__) font which contains, in one font description several extreme styles of a font family and code for interpolating between them to produce intermediate designs. Apple has a -`similar concept `__ +`similar concept `__ for TrueType -- The only name I've seen given to this is "Apple Distortable Font" -- I call both multiple master. diff --git a/doc/sphinx/ui/dialogs/prefs.rst b/doc/sphinx/ui/dialogs/prefs.rst index 4edb3623d6..7c79e78d99 100644 --- a/doc/sphinx/ui/dialogs/prefs.rst +++ b/doc/sphinx/ui/dialogs/prefs.rst @@ -617,7 +617,7 @@ many languages as you like. The Mac Mapping dialog allows you to define a mapping between OTF ``GSUB`` feature tags and Apple's -`mort/morx `__ +`mort/morx `__ Feature/Setting codes. `Apple's published list `__ diff --git a/doc/sphinx/ui/dialogs/problems.rst b/doc/sphinx/ui/dialogs/problems.rst index 92670b1012..3216f98698 100644 --- a/doc/sphinx/ui/dialogs/problems.rst +++ b/doc/sphinx/ui/dialogs/problems.rst @@ -283,7 +283,7 @@ Refs Deeper Than ---------------- Appendix B of the the -`Type2 `__ spec +`Type2 `__ spec says that an interpreter is only required to support subroutine nesting up to 10 levels. FontForge uses subroutine calls to handle referenced glyphs and sometimes also to handle hinting. Hinting will take up a maximum of 1 level of @@ -369,7 +369,7 @@ More Hints Than --------------- Appendix B of the the -`Type2 `__ spec +`Type2 `__ spec says that an interpreter is only required to support a total of 96 horizontal and vertical hints. diff --git a/doc/sphinx/ui/dialogs/statemachine.rst b/doc/sphinx/ui/dialogs/statemachine.rst index d662e8f901..29eff1d712 100644 --- a/doc/sphinx/ui/dialogs/statemachine.rst +++ b/doc/sphinx/ui/dialogs/statemachine.rst @@ -106,14 +106,14 @@ For a complete description of what these state machines can do you should read Apple's documentation * On the - `'morx' `__ and - `'mort' `__ + `'morx' `__ and + `'mort' `__ tables * On the - `'kern' `__ table + `'kern' `__ table (format 1) -* `On state machines in general `__ -* `The 'feat' (feature) table `__ +* `On state machines in general `__ +* `The 'feat' (feature) table `__ * `Apple's Font Feature registry `__ The general idea is that a word processor converts text into a string of glyphs, diff --git a/doc/sphinx/ui/dialogs/ttfinstrs.rst b/doc/sphinx/ui/dialogs/ttfinstrs.rst index e9d89751c4..1acacdda56 100644 --- a/doc/sphinx/ui/dialogs/ttfinstrs.rst +++ b/doc/sphinx/ui/dialogs/ttfinstrs.rst @@ -5,10 +5,10 @@ Viewing and Editing truetype instructions in FontForge. .. image:: /images/ttfinstrs-edit.png -* `Apple's overview on instructing glyphs `__ -* `Apple's list of truetype instructions `__ -* `Apple's description of the truetype graphics state `__ -* `Apple's summary of truetype instructions `__ +* `Apple's overview on instructing glyphs `__ +* `Apple's list of truetype instructions `__ +* `Apple's description of the truetype graphics state `__ +* `Apple's summary of truetype instructions `__ I shall not attempt to describe the mechanics of instructing truetype, if you want to understand it I refer you to the above links. diff --git a/doc/sphinx/ui/mainviews/charview.rst b/doc/sphinx/ui/mainviews/charview.rst index 0167fa4041..279a0e80d7 100644 --- a/doc/sphinx/ui/mainviews/charview.rst +++ b/doc/sphinx/ui/mainviews/charview.rst @@ -519,14 +519,28 @@ The scale tool :align: left This tool allows you to scale the selection by eye rather than by a set amount -(if there is no selection then everything in the current layer will be scaled). -The location of the press will be the origin of the transformation, the further -you move the point up and to the right the more it will be scaled in that -dimension. If you want the scaling to be uniform or only in one dimension then -hold down the shift key. +(if there is no selection then everything in the current layer will be scaled, +as though all points in that layer were selected). -Double clicking on this will bring up the transform dialog with the "Scale..." -option selected. +Double clicking on this tool icon will bring up the transform dialog with the +"Scale..." option selected, useful for typing in scale percentages manually. + +When the scale tool is active, FontForge shows a bounding box around the +selection, with draggable resize handles at the corners and edges: + +.. image:: /images/cvscalemode.gif + :align: center + +* Dragging a corner resize handle scales the selection while keeping the opposite corner fixed. + +* Dragging a left/right edge handle scales only horizontally while keeping the opposite edge fixed. + +* Dragging a top/bottom edge handle scales only vertically while keeping the opposite edge fixed. + +Clicking or dragging anywhere other than a bounding box handle does not start a +scale operation, and is ignored. To change the selected points to scale, switch +to the "pointer tool" to make a new selection, then come back to the scale tool +to resize the selection. .. _charview.flip: @@ -978,4 +992,4 @@ Multiple Layer Editing If you wish to :doc:`edit type3 fonts `, FontForge can display a glyph broken down into a series of strokes and fills and allow you to edit each -one. \ No newline at end of file +one. diff --git a/doc/sphinx/ui/menus/elementmenu.rst b/doc/sphinx/ui/menus/elementmenu.rst index 235f8b1cf2..6890706279 100644 --- a/doc/sphinx/ui/menus/elementmenu.rst +++ b/doc/sphinx/ui/menus/elementmenu.rst @@ -154,10 +154,9 @@ FontForge. FontForge deals in pixel sizes, not point sizes. The conversion between pixels and points differs on different systems and indeed on different - screens. A point is (approximately) 1/72 of an inch, a pixel is however big a + screens. A typographic point is 1/72 of an inch, a pixel is however big a pixel happens to be on your screen. Usually pixels range from about 1/72 of - an inch to about 1/144 of an inch. Different systems support different screen - resolutions as "standard", and FontForge tries to know about these standards. + an inch to about 1/144 of an inch. .. list-table:: Some conversions between points and pixels :header-rows: 1 @@ -206,19 +205,14 @@ FontForge. - 33 - 40 - Sadly your screen will probably not match one of the standard screens - precisely. On X the standard resolutions are 75 and 100dpi, on MS Windows 96 - and 120dpi, and on the Mac 72dpi. This dialog provides the conversion between - pixel size and point sizes at these resolutions. + Most modern monitors report their resolution to the OS. This dialog provides + the conversion between pixel sizes and point sizes at your resolution, and + for the typical 96 PPI monitor. - Normally the new glyphs are created by rasterizing the outline font. If your - system has the freetype2 library installed (and you checked the "Use - FreeType" box) then FontForge will use the FreeType rasterizer to generate - bitmaps, otherwise it will use FontForge's built-in rasterizer (which isn't - as good, but involves a little less overhead). - - Finally, if you have no outline font then the new glyphs will be created by - scaling the (bitmap) font displayed in the font view. + Normally the new glyphs are created by rasterizing the outline font. + FontForge uses the FreeType rasterizer to generate bitmaps. If you have no + outline font then the new glyphs will be created by scaling the (bitmap) + font displayed in the font view. If you want to generate a blank strike (one containing no glyphs) then turn off the ``[] Create Rasterized Strikes`` checkbox. diff --git a/doc/sphinx/ui/misc/HotKeys.rst b/doc/sphinx/ui/misc/HotKeys.rst index e42e8cdca5..5314961e89 100644 --- a/doc/sphinx/ui/misc/HotKeys.rst +++ b/doc/sphinx/ui/misc/HotKeys.rst @@ -134,12 +134,6 @@ I am aware of the following significant differences: when not using X) * On Suns the meta (diamond) key is equivalent to the Alt modifier key. -FontForge will attempt to guess what keyboard you are using and produce menus -with hot-key indicators that match the host machine. If you are displaying on a -different machine from the one you are running on the menu names may be wrong. -You can fix this up with the :ref:`keyboard resource `, or the --keyboard command line argument. - Tool modifiers in the Outline Glyph Window ------------------------------------------ diff --git a/doc/sphinx/ui/misc/xres.rst b/doc/sphinx/ui/misc/xres.rst index fbe435b685..065d6fbbf7 100644 --- a/doc/sphinx/ui/misc/xres.rst +++ b/doc/sphinx/ui/misc/xres.rst @@ -674,10 +674,6 @@ Misc Windows Sets the font used in the Groups dialog -.. object:: fontforge.Histogram.Font - - Sets the font used in the Histogram dialog - .. object:: fontforge.KernClass.TextColor Color for kerning class names. @@ -864,71 +860,6 @@ GDraw Whether to synchronize the display before raising the first window. -GDraw (X backend only) -~~~~~~~~~~~~~~~~~~~~~~ - -These are not included in the appearance editor and need to be set -some other way, perhaps through the normal X Resources system. - -.. object:: Gdraw.Depth - - An integer (1, 8, 16, 32) - - You can use this to request a different depth than the default one. Not all - servers will support all depths. If FontForge can't find a visual with the - desired depth it will use the default depth. - -.. object:: Gdraw.VisualClass - - A string ("StaticGray", "GrayScale", "StaticColor", "PseudoColor", - "TrueColor", "DirectColor") - - FontForge will search for a visual with the given class (and possibly depth - if the depth argument is specified too). - -.. _xres.Colormap: - -.. object:: Gdraw.Colormap - - An string ("Current", "Copy", "Private") - - You can use this to control what FontForge does about the colormap on an 8bit - screen - - * Current -- FontForge will attempt to allocate its colors in the current - colormap. - * Copy -- FontForge will allocate what colors it can and then copy the current - color map into a private copy. This means FontForge has access to a much - wider range of colors, and (as long as the shared colormap doesn't change) - FontForge's colormap will match that of the rest of the screen. - * Private -- FontForge will allocate a private colormap and set the colors just - as it wants them. It will almost certainly not match the shared colormap. - -.. _xres.Keyboard: - -.. object:: Gdraw.Keyboard - - ibm | mac | sun | ppc | 0 | 1 | 2 | 3 - - Allows you to specify the type of keyboard. Currently this is only relevant - when generating menus. The modifier keys are in different locations on - different keyboards (under different operating systems) and if FontForge - knows what keyboard you are using it can make the hot-keys have better - labels. - - * ibm | 0 - - Uses the Control and Alt keys - * mac | 1 - - Uses the Command and Option keys (Mac OS/X, Mac keyboard) - * ppc | 3 - - Uses the Control and Command keys (Suse ppc linux, Mac keyboard) - * sun | 2 - - Uses the Control and Meta keys - Popup ~~~~~ @@ -1590,11 +1521,6 @@ Almost all keyboards now-a-days will have the needed modifier keys, but which key is used for what will depend on the keyboard and the OS (for instance XDarwin and suse linux use quite different mappings for the modifier keys on the mac keyboard). Usually this is only relevant for menus (and mnemonics). -FontForge tries to guess the keyboard from the environment in which it was -compiled. But with X this may not always be appropriate. So the -":ref:`Gdraw.Keyboard `" resource above may be used to change -this. (Currently this setting only control the labels that appear in menus for -the hotkeys). Mice are more problematic. On PCs we usually have two button mice and on mac single button mice. Many linuxes that run on a PC will give you an option of diff --git a/extern/CMakeLists.txt b/extern/CMakeLists.txt new file mode 100644 index 0000000000..b22816e18c --- /dev/null +++ b/extern/CMakeLists.txt @@ -0,0 +1,11 @@ +# Vendored third-party libraries + +# mINI - header-only INI file parser +add_library(mINI INTERFACE) +target_include_directories(mINI INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/mINI) +# Enable case-sensitive mode for GKeyFile compatibility +target_compile_definitions(mINI INTERFACE MINI_CASE_SENSITIVE) + +# cxxopts - header-only command line argument parser +add_library(cxxopts INTERFACE) +target_include_directories(cxxopts INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/cxxopts) diff --git a/extern/cxxopts/cxxopts.hpp b/extern/cxxopts/cxxopts.hpp new file mode 100644 index 0000000000..537b2e79f5 --- /dev/null +++ b/extern/cxxopts/cxxopts.hpp @@ -0,0 +1,2923 @@ +/* + +Copyright (c) 2014-2022 Jarryd Beck + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +*/ + +// vim: ts=2:sw=2:expandtab + +#ifndef CXXOPTS_HPP_INCLUDED +#define CXXOPTS_HPP_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef CXXOPTS_NO_EXCEPTIONS +#include +#endif + +#if defined(__GNUC__) && !defined(__clang__) +# if (__GNUC__ * 10 + __GNUC_MINOR__) < 49 +# define CXXOPTS_NO_REGEX true +# endif +#endif +#if defined(_MSC_VER) && !defined(__clang__) +#define CXXOPTS_LINKONCE_CONST __declspec(selectany) extern +#define CXXOPTS_LINKONCE __declspec(selectany) extern +#else +#define CXXOPTS_LINKONCE_CONST +#define CXXOPTS_LINKONCE +#endif + +#ifndef CXXOPTS_NO_REGEX +# include +#endif // CXXOPTS_NO_REGEX + +// Nonstandard before C++17, which is coincidentally what we also need for +#ifdef __has_include +# if __has_include() +# include +# ifdef __cpp_lib_optional +# define CXXOPTS_HAS_OPTIONAL +# endif +# endif +# if __has_include() +# include +# ifdef __cpp_lib_filesystem +# define CXXOPTS_HAS_FILESYSTEM +# endif +# endif +#endif + +#define CXXOPTS_FALLTHROUGH +#ifdef __has_cpp_attribute + #if __has_cpp_attribute(fallthrough) + #undef CXXOPTS_FALLTHROUGH + #define CXXOPTS_FALLTHROUGH [[fallthrough]] + #endif +#endif + +#if __cplusplus >= 201603L +#define CXXOPTS_NODISCARD [[nodiscard]] +#else +#define CXXOPTS_NODISCARD +#endif + +#ifndef CXXOPTS_VECTOR_DELIMITER +#define CXXOPTS_VECTOR_DELIMITER ',' +#endif + +#define CXXOPTS__VERSION_MAJOR 3 +#define CXXOPTS__VERSION_MINOR 3 +#define CXXOPTS__VERSION_PATCH 1 + +#if (__GNUC__ < 10 || (__GNUC__ == 10 && __GNUC_MINOR__ < 1)) && __GNUC__ >= 6 + #define CXXOPTS_NULL_DEREF_IGNORE +#endif + +#if defined(__GNUC__) +#define DO_PRAGMA(x) _Pragma(#x) +#define CXXOPTS_DIAGNOSTIC_PUSH DO_PRAGMA(GCC diagnostic push) +#define CXXOPTS_DIAGNOSTIC_POP DO_PRAGMA(GCC diagnostic pop) +#define CXXOPTS_IGNORE_WARNING(x) DO_PRAGMA(GCC diagnostic ignored x) +#else +// define other compilers here if needed +#define CXXOPTS_DIAGNOSTIC_PUSH +#define CXXOPTS_DIAGNOSTIC_POP +#define CXXOPTS_IGNORE_WARNING(x) +#endif + +#ifdef CXXOPTS_NO_RTTI +#define CXXOPTS_RTTI_CAST static_cast +#else +#define CXXOPTS_RTTI_CAST dynamic_cast +#endif + +namespace cxxopts { +static constexpr struct { + uint8_t major, minor, patch; +} version = { + CXXOPTS__VERSION_MAJOR, + CXXOPTS__VERSION_MINOR, + CXXOPTS__VERSION_PATCH +}; +} // namespace cxxopts + +//when we ask cxxopts to use Unicode, help strings are processed using ICU, +//which results in the correct lengths being computed for strings when they +//are formatted for the help output +//it is necessary to make sure that can be found by the +//compiler, and that icu-uc is linked in to the binary. + +#ifdef CXXOPTS_USE_UNICODE +#include + +namespace cxxopts { + +using String = icu::UnicodeString; + +inline +String +toLocalString(std::string s) +{ + return icu::UnicodeString::fromUTF8(std::move(s)); +} + +// GNU GCC with -Weffc++ will issue a warning regarding the upcoming class, we want to silence it: +// warning: base class 'class std::enable_shared_from_this' has accessible non-virtual destructor +CXXOPTS_DIAGNOSTIC_PUSH +CXXOPTS_IGNORE_WARNING("-Wnon-virtual-dtor") +// This will be ignored under other compilers like LLVM clang. +class UnicodeStringIterator +{ + public: + + using iterator_category = std::forward_iterator_tag; + using value_type = int32_t; + using difference_type = std::ptrdiff_t; + using pointer = value_type*; + using reference = value_type&; + + UnicodeStringIterator(const icu::UnicodeString* string, int32_t pos) + : s(string) + , i(pos) + { + } + + value_type + operator*() const + { + return s->char32At(i); + } + + bool + operator==(const UnicodeStringIterator& rhs) const + { + return s == rhs.s && i == rhs.i; + } + + bool + operator!=(const UnicodeStringIterator& rhs) const + { + return !(*this == rhs); + } + + UnicodeStringIterator& + operator++() + { + ++i; + return *this; + } + + UnicodeStringIterator + operator+(int32_t v) + { + return UnicodeStringIterator(s, i + v); + } + + private: + const icu::UnicodeString* s; + int32_t i; +}; +CXXOPTS_DIAGNOSTIC_POP + +inline +String& +stringAppend(String&s, String a) +{ + return s.append(std::move(a)); +} + +inline +String& +stringAppend(String& s, std::size_t n, UChar32 c) +{ + for (std::size_t i = 0; i != n; ++i) + { + s.append(c); + } + + return s; +} + +template +String& +stringAppend(String& s, Iterator begin, Iterator end) +{ + while (begin != end) + { + s.append(*begin); + ++begin; + } + + return s; +} + +inline +size_t +stringLength(const String& s) +{ + return static_cast(s.length()); +} + +inline +std::string +toUTF8String(const String& s) +{ + std::string result; + s.toUTF8String(result); + + return result; +} + +inline +bool +empty(const String& s) +{ + return s.isEmpty(); +} + +} // namespace cxxopts + +namespace std { + +inline +cxxopts::UnicodeStringIterator +begin(const icu::UnicodeString& s) +{ + return cxxopts::UnicodeStringIterator(&s, 0); +} + +inline +cxxopts::UnicodeStringIterator +end(const icu::UnicodeString& s) +{ + return cxxopts::UnicodeStringIterator(&s, s.length()); +} + +} // namespace std + +//ifdef CXXOPTS_USE_UNICODE +#else + +namespace cxxopts { + +using String = std::string; + +template +T +toLocalString(T&& t) +{ + return std::forward(t); +} + +inline +std::size_t +stringLength(const String& s) +{ + return s.length(); +} + +inline +String& +stringAppend(String&s, const String& a) +{ + return s.append(a); +} + +inline +String& +stringAppend(String& s, std::size_t n, char c) +{ + return s.append(n, c); +} + +template +String& +stringAppend(String& s, Iterator begin, Iterator end) +{ + return s.append(begin, end); +} + +template +std::string +toUTF8String(T&& t) +{ + return std::forward(t); +} + +inline +bool +empty(const std::string& s) +{ + return s.empty(); +} + +} // namespace cxxopts + +//ifdef CXXOPTS_USE_UNICODE +#endif + +namespace cxxopts { + +namespace { +CXXOPTS_LINKONCE_CONST std::string LQUOTE("\'"); +CXXOPTS_LINKONCE_CONST std::string RQUOTE("\'"); +} // namespace + +// GNU GCC with -Weffc++ will issue a warning regarding the upcoming class, we +// want to silence it: warning: base class 'class +// std::enable_shared_from_this' has accessible non-virtual +// destructor This will be ignored under other compilers like LLVM clang. +CXXOPTS_DIAGNOSTIC_PUSH +CXXOPTS_IGNORE_WARNING("-Wnon-virtual-dtor") + +// some older versions of GCC warn under this warning +CXXOPTS_IGNORE_WARNING("-Weffc++") +class Value : public std::enable_shared_from_this +{ + public: + + virtual ~Value() = default; + + virtual + std::shared_ptr + clone() const = 0; + + virtual void + add(const std::string& text) const = 0; + + virtual void + parse(const std::string& text) const = 0; + + virtual void + parse() const = 0; + + virtual bool + has_default() const = 0; + + virtual bool + is_container() const = 0; + + virtual bool + has_implicit() const = 0; + + virtual std::string + get_default_value() const = 0; + + virtual std::string + get_implicit_value() const = 0; + + virtual std::shared_ptr + default_value(const std::string& value) = 0; + + virtual std::shared_ptr + implicit_value(const std::string& value) = 0; + + virtual std::shared_ptr + no_implicit_value() = 0; + + virtual bool + is_boolean() const = 0; +}; + +CXXOPTS_DIAGNOSTIC_POP + +namespace exceptions { + +class exception : public std::exception +{ + public: + explicit exception(std::string message) + : m_message(std::move(message)) + { + } + + CXXOPTS_NODISCARD + const char* + what() const noexcept override + { + return m_message.c_str(); + } + + private: + std::string m_message; +}; + +class specification : public exception +{ + public: + + explicit specification(const std::string& message) + : exception(message) + { + } +}; + +class parsing : public exception +{ + public: + explicit parsing(const std::string& message) + : exception(message) + { + } +}; + +class option_already_exists : public specification +{ + public: + explicit option_already_exists(const std::string& option) + : specification("Option " + LQUOTE + option + RQUOTE + " already exists") + { + } +}; + +class invalid_option_format : public specification +{ + public: + explicit invalid_option_format(const std::string& format) + : specification("Invalid option format " + LQUOTE + format + RQUOTE) + { + } +}; + +class invalid_option_syntax : public parsing { + public: + explicit invalid_option_syntax(const std::string& text) + : parsing("Argument " + LQUOTE + text + RQUOTE + + " starts with a - but has incorrect syntax") + { + } +}; + +class no_such_option : public parsing +{ + public: + explicit no_such_option(const std::string& option) + : parsing("Option " + LQUOTE + option + RQUOTE + " does not exist") + { + } +}; + +class missing_argument : public parsing +{ + public: + explicit missing_argument(const std::string& option) + : parsing( + "Option " + LQUOTE + option + RQUOTE + " is missing an argument" + ) + { + } +}; + +class option_requires_argument : public parsing +{ + public: + explicit option_requires_argument(const std::string& option) + : parsing( + "Option " + LQUOTE + option + RQUOTE + " requires an argument" + ) + { + } +}; + +class gratuitous_argument_for_option : public parsing +{ + public: + gratuitous_argument_for_option + ( + const std::string& option, + const std::string& arg + ) + : parsing( + "Option " + LQUOTE + option + RQUOTE + + " does not take an argument, but argument " + + LQUOTE + arg + RQUOTE + " given" + ) + { + } +}; + +class requested_option_not_present : public parsing +{ + public: + explicit requested_option_not_present(const std::string& option) + : parsing("Option " + LQUOTE + option + RQUOTE + " not present") + { + } +}; + +class option_has_no_value : public exception +{ + public: + explicit option_has_no_value(const std::string& option) + : exception( + !option.empty() ? + ("Option " + LQUOTE + option + RQUOTE + " has no value") : + "Option has no value") + { + } +}; + +class incorrect_argument_type : public parsing +{ + public: + explicit incorrect_argument_type + ( + const std::string& arg + ) + : parsing( + "Argument " + LQUOTE + arg + RQUOTE + " failed to parse" + ) + { + } +}; + +} // namespace exceptions + + +template +void throw_or_mimic(const std::string& text) +{ + static_assert(std::is_base_of::value, + "throw_or_mimic only works on std::exception and " + "deriving classes"); + +#ifndef CXXOPTS_NO_EXCEPTIONS + // If CXXOPTS_NO_EXCEPTIONS is not defined, just throw + throw T{text}; +#else + // Otherwise manually instantiate the exception, print what() to stderr, + // and exit + T exception{text}; + std::cerr << exception.what() << std::endl; + std::exit(EXIT_FAILURE); +#endif +} + +using OptionNames = std::vector; + +namespace values { + +namespace parser_tool { + +struct IntegerDesc +{ + std::string negative = ""; + std::string base = ""; + std::string value = ""; +}; +struct ArguDesc { + std::string arg_name = ""; + bool grouping = false; + bool set_value = false; + std::string value = ""; +}; + +#ifdef CXXOPTS_NO_REGEX +inline IntegerDesc SplitInteger(const std::string &text) +{ + if (text.empty()) + { + throw_or_mimic(text); + } + IntegerDesc desc; + const char *pdata = text.c_str(); + if (*pdata == '-') + { + pdata += 1; + desc.negative = "-"; + } + if (strncmp(pdata, "0x", 2) == 0) + { + pdata += 2; + desc.base = "0x"; + } + if (*pdata != '\0') + { + desc.value = std::string(pdata); + } + else + { + throw_or_mimic(text); + } + return desc; +} + +inline bool IsTrueText(const std::string &text) +{ + const char *pdata = text.c_str(); + if (*pdata == 't' || *pdata == 'T') + { + pdata += 1; + if (strncmp(pdata, "rue\0", 4) == 0) + { + return true; + } + } + else if (strncmp(pdata, "1\0", 2) == 0) + { + return true; + } + return false; +} + +inline bool IsFalseText(const std::string &text) +{ + const char *pdata = text.c_str(); + if (*pdata == 'f' || *pdata == 'F') + { + pdata += 1; + if (strncmp(pdata, "alse\0", 5) == 0) + { + return true; + } + } + else if (strncmp(pdata, "0\0", 2) == 0) + { + return true; + } + return false; +} + +inline OptionNames split_option_names(const std::string &text) +{ + OptionNames split_names; + + std::string::size_type token_start_pos = 0; + auto length = text.length(); + + if (length == 0) + { + throw_or_mimic(text); + } + + while (token_start_pos < length) { + const auto &npos = std::string::npos; + auto next_non_space_pos = text.find_first_not_of(' ', token_start_pos); + if (next_non_space_pos == npos) { + throw_or_mimic(text); + } + token_start_pos = next_non_space_pos; + auto next_delimiter_pos = text.find(',', token_start_pos); + if (next_delimiter_pos == token_start_pos) { + throw_or_mimic(text); + } + if (next_delimiter_pos == npos) { + next_delimiter_pos = length; + } + auto token_length = next_delimiter_pos - token_start_pos; + // validate the token itself matches the regex /([:alnum:][-_[:alnum:]]*/ + { + const char* option_name_valid_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789" + "_-.?"; + + if (!std::isalnum(text[token_start_pos], std::locale::classic()) || + text.find_first_not_of(option_name_valid_chars, token_start_pos) < next_delimiter_pos) { + throw_or_mimic(text); + } + } + split_names.emplace_back(text.substr(token_start_pos, token_length)); + token_start_pos = next_delimiter_pos + 1; + } + return split_names; +} + +inline ArguDesc ParseArgument(const char *arg, bool &matched) +{ + ArguDesc argu_desc; + const char *pdata = arg; + matched = false; + if (strncmp(pdata, "--", 2) == 0) + { + pdata += 2; + if (isalnum(*pdata, std::locale::classic())) + { + argu_desc.arg_name.push_back(*pdata); + pdata += 1; + while (isalnum(*pdata, std::locale::classic()) || *pdata == '-' || *pdata == '_' || *pdata == '.') + { + argu_desc.arg_name.push_back(*pdata); + pdata += 1; + } + if (argu_desc.arg_name.length() > 1) + { + if (*pdata == '=') + { + argu_desc.set_value = true; + pdata += 1; + if (*pdata != '\0') + { + argu_desc.value = std::string(pdata); + } + matched = true; + } + else if (*pdata == '\0') + { + matched = true; + } + } + } + } + else if (strncmp(pdata, "-", 1) == 0) + { + pdata += 1; + if(isalnum(*pdata, std::locale::classic())) { + argu_desc.grouping = true; + argu_desc.arg_name = std::string(pdata); + matched = true; + } + } + return argu_desc; +} + +#else // CXXOPTS_NO_REGEX + +namespace { +CXXOPTS_LINKONCE +const char* const integer_pattern = + "(-)?(0x)?([0-9a-zA-Z]+)|((0x)?0)"; +CXXOPTS_LINKONCE +const char* const truthy_pattern = + "(t|T)(rue)?|1"; +CXXOPTS_LINKONCE +const char* const falsy_pattern = + "(f|F)(alse)?|0"; +CXXOPTS_LINKONCE +const char* const option_pattern = + "--([[:alnum:]][-_[:alnum:]\\.]+)(=(.*))?|-([[:alnum:]].*)"; +CXXOPTS_LINKONCE +const char* const option_specifier_pattern = + "([[:alnum:]][-_[:alnum:]\\.]*)(,[ ]*[[:alnum:]][-_[:alnum:]]*)*"; +CXXOPTS_LINKONCE +const char* const option_specifier_separator_pattern = ", *"; + +} // namespace + +inline IntegerDesc SplitInteger(const std::string &text) +{ + static const std::basic_regex integer_matcher(integer_pattern); + + std::smatch match; + std::regex_match(text, match, integer_matcher); + + if (match.length() == 0) + { + throw_or_mimic(text); + } + + IntegerDesc desc; + desc.negative = match[1]; + desc.base = match[2]; + desc.value = match[3]; + + if (match.length(4) > 0) + { + desc.base = match[5]; + desc.value = "0"; + return desc; + } + + return desc; +} + +inline bool IsTrueText(const std::string &text) +{ + static const std::basic_regex truthy_matcher(truthy_pattern); + std::smatch result; + std::regex_match(text, result, truthy_matcher); + return !result.empty(); +} + +inline bool IsFalseText(const std::string &text) +{ + static const std::basic_regex falsy_matcher(falsy_pattern); + std::smatch result; + std::regex_match(text, result, falsy_matcher); + return !result.empty(); +} + +// Gets the option names specified via a single, comma-separated string, +// and returns the separate, space-discarded, non-empty names +// (without considering which or how many are single-character) +inline OptionNames split_option_names(const std::string &text) +{ + static const std::basic_regex option_specifier_matcher(option_specifier_pattern); + if (!std::regex_match(text.c_str(), option_specifier_matcher)) + { + throw_or_mimic(text); + } + + OptionNames split_names; + + static const std::basic_regex option_specifier_separator_matcher(option_specifier_separator_pattern); + constexpr int use_non_matches { -1 }; + auto token_iterator = std::sregex_token_iterator( + text.begin(), text.end(), option_specifier_separator_matcher, use_non_matches); + std::copy(token_iterator, std::sregex_token_iterator(), std::back_inserter(split_names)); + return split_names; +} + +inline ArguDesc ParseArgument(const char *arg, bool &matched) +{ + static const std::basic_regex option_matcher(option_pattern); + std::match_results result; + std::regex_match(arg, result, option_matcher); + matched = !result.empty(); + + ArguDesc argu_desc; + if (matched) { + argu_desc.arg_name = result[1].str(); + argu_desc.set_value = result[2].length() > 0; + argu_desc.value = result[3].str(); + if (result[4].length() > 0) + { + argu_desc.grouping = true; + argu_desc.arg_name = result[4].str(); + } + } + + return argu_desc; +} + +#endif // CXXOPTS_NO_REGEX +#undef CXXOPTS_NO_REGEX +} // namespace parser_tool + +namespace detail { + +template +struct SignedCheck; + +template +struct SignedCheck +{ + template + void + operator()(bool negative, U u, const std::string& text) + { + if (negative) + { + if (u > static_cast((std::numeric_limits::min)())) + { + throw_or_mimic(text); + } + } + else + { + if (u > static_cast((std::numeric_limits::max)())) + { + throw_or_mimic(text); + } + } + } +}; + +template +struct SignedCheck +{ + template + void + operator()(bool, U, const std::string&) const {} +}; + +template +void +check_signed_range(bool negative, U value, const std::string& text) +{ + SignedCheck::is_signed>()(negative, value, text); +} + +} // namespace detail + +template +void +checked_negate(R& r, T&& t, const std::string&, std::true_type) +{ + // if we got to here, then `t` is a positive number that fits into + // `R`. So to avoid MSVC C4146, we first cast it to `R`. + // See https://github.com/jarro2783/cxxopts/issues/62 for more details. + r = static_cast(-static_cast(t-1)-1); +} + +template +void +checked_negate(R&, T&&, const std::string& text, std::false_type) +{ + throw_or_mimic(text); +} + +template +void +integer_parser(const std::string& text, T& value) +{ + parser_tool::IntegerDesc int_desc = parser_tool::SplitInteger(text); + + using US = typename std::make_unsigned::type; + constexpr bool is_signed = std::numeric_limits::is_signed; + + const bool negative = int_desc.negative.length() > 0; + const uint8_t base = int_desc.base.length() > 0 ? 16 : 10; + const std::string & value_match = int_desc.value; + + US result = 0; + + for (char ch : value_match) + { + US digit = 0; + + if (ch >= '0' && ch <= '9') + { + digit = static_cast(ch - '0'); + } + else if (base == 16 && ch >= 'a' && ch <= 'f') + { + digit = static_cast(ch - 'a' + 10); + } + else if (base == 16 && ch >= 'A' && ch <= 'F') + { + digit = static_cast(ch - 'A' + 10); + } + else + { + throw_or_mimic(text); + } + + US limit = 0; + if (negative) + { + limit = static_cast(std::abs(static_cast((std::numeric_limits::min)()))); + } + else + { + limit = (std::numeric_limits::max)(); + } + + if (base != 0 && result > limit / base) + { + throw_or_mimic(text); + } + if (result * base > limit - digit) + { + throw_or_mimic(text); + } + + result = static_cast(result * base + digit); + } + + detail::check_signed_range(negative, result, text); + + if (negative) + { + checked_negate(value, result, text, std::integral_constant()); + } + else + { + value = static_cast(result); + } +} + +template +void stringstream_parser(const std::string& text, T& value) +{ + std::stringstream in(text); + in >> value; + if (!in) { + throw_or_mimic(text); + } +} + +template ::value>::type* = nullptr + > +void parse_value(const std::string& text, T& value) +{ + integer_parser(text, value); +} + +inline +void +parse_value(const std::string& text, bool& value) +{ + if (parser_tool::IsTrueText(text)) + { + value = true; + return; + } + + if (parser_tool::IsFalseText(text)) + { + value = false; + return; + } + + throw_or_mimic(text); +} + +inline +void +parse_value(const std::string& text, std::string& value) +{ + value = text; +} + +// The fallback parser. It uses the stringstream parser to parse all types +// that have not been overloaded explicitly. It has to be placed in the +// source code before all other more specialized templates. +template ::value>::type* = nullptr + > +void +parse_value(const std::string& text, T& value) { + stringstream_parser(text, value); +} + +#ifdef CXXOPTS_HAS_OPTIONAL +template +void +parse_value(const std::string& text, std::optional& value) +{ + T result; + parse_value(text, result); + value = std::move(result); +} +#endif + +#ifdef CXXOPTS_HAS_FILESYSTEM +inline +void +parse_value(const std::string& text, std::filesystem::path& value) +{ + value.assign(text); +} +#endif + +inline +void parse_value(const std::string& text, char& c) +{ + if (text.length() != 1) + { + throw_or_mimic(text); + } + + c = text[0]; +} + +template void add_value(const std::string& text, std::vector& value); + +template +void +parse_value(const std::string& text, std::vector& value) +{ + if (text.empty()) { + return; + } + std::stringstream in(text); + std::string token; + while(!in.eof() && std::getline(in, token, CXXOPTS_VECTOR_DELIMITER)) { + add_value(token, value); + } +} + +template +void +add_value(const std::string& text, T& value) +{ + parse_value(text, value); +} + +template +void +add_value(const std::string& text, std::vector& value) +{ + T v; + add_value(text, v); + value.emplace_back(std::move(v)); +} + +template +struct type_is_container +{ + static constexpr bool value = false; +}; + +template +struct type_is_container> +{ + static constexpr bool value = true; +}; + +template +class abstract_value : public Value +{ + using Self = abstract_value; + + public: + abstract_value() + : m_result(std::make_shared()) + , m_store(m_result.get()) + { + } + + explicit abstract_value(T* t) + : m_store(t) + { + } + + ~abstract_value() override = default; + + abstract_value& operator=(const abstract_value&) = default; + + abstract_value(const abstract_value& rhs) + { + if (rhs.m_result) + { + m_result = std::make_shared(); + m_store = m_result.get(); + } + else + { + m_store = rhs.m_store; + } + + m_default = rhs.m_default; + m_implicit = rhs.m_implicit; + m_default_value = rhs.m_default_value; + m_implicit_value = rhs.m_implicit_value; + } + + void + add(const std::string& text) const override + { + add_value(text, *m_store); + } + + void + parse(const std::string& text) const override + { + parse_value(text, *m_store); + } + + bool + is_container() const override + { + return type_is_container::value; + } + + void + parse() const override + { + parse_value(m_default_value, *m_store); + } + + bool + has_default() const override + { + return m_default; + } + + bool + has_implicit() const override + { + return m_implicit; + } + + std::shared_ptr + default_value(const std::string& value) override + { + m_default = true; + m_default_value = value; + return shared_from_this(); + } + + std::shared_ptr + implicit_value(const std::string& value) override + { + m_implicit = true; + m_implicit_value = value; + return shared_from_this(); + } + + std::shared_ptr + no_implicit_value() override + { + m_implicit = false; + return shared_from_this(); + } + + std::string + get_default_value() const override + { + return m_default_value; + } + + std::string + get_implicit_value() const override + { + return m_implicit_value; + } + + bool + is_boolean() const override + { + return std::is_same::value; + } + + const T& + get() const + { + if (m_store == nullptr) + { + return *m_result; + } + return *m_store; + } + + protected: + std::shared_ptr m_result{}; + T* m_store{}; + + bool m_default = false; + bool m_implicit = false; + + std::string m_default_value{}; + std::string m_implicit_value{}; +}; + +template +class standard_value : public abstract_value +{ + public: + using abstract_value::abstract_value; + + CXXOPTS_NODISCARD + std::shared_ptr + clone() const override + { + return std::make_shared>(*this); + } +}; + +template <> +class standard_value : public abstract_value +{ + public: + standard_value() + { + set_default_and_implicit(); + } + + explicit standard_value(bool* b) + : abstract_value(b) + { + m_implicit = true; + m_implicit_value = "true"; + } + + std::shared_ptr + clone() const override + { + return std::make_shared>(*this); + } + + private: + + void + set_default_and_implicit() + { + m_default = true; + m_default_value = "false"; + m_implicit = true; + m_implicit_value = "true"; + } +}; + +} // namespace values + +template +std::shared_ptr +value() +{ + return std::make_shared>(); +} + +template +std::shared_ptr +value(T& t) +{ + return std::make_shared>(&t); +} + +class OptionAdder; + +CXXOPTS_NODISCARD +inline +const std::string& +first_or_empty(const OptionNames& long_names) +{ + static const std::string empty{""}; + return long_names.empty() ? empty : long_names.front(); +} + +class OptionDetails +{ + public: + OptionDetails + ( + std::string short_, + OptionNames long_, + String desc, + std::shared_ptr val + ) + : m_short(std::move(short_)) + , m_long(std::move(long_)) + , m_desc(std::move(desc)) + , m_value(std::move(val)) + , m_count(0) + { + m_hash = std::hash{}(first_long_name() + m_short); + } + + OptionDetails(const OptionDetails& rhs) + : m_desc(rhs.m_desc) + , m_value(rhs.m_value->clone()) + , m_count(rhs.m_count) + { + } + + OptionDetails(OptionDetails&& rhs) = default; + + CXXOPTS_NODISCARD + const String& + description() const + { + return m_desc; + } + + CXXOPTS_NODISCARD + const Value& + value() const { + return *m_value; + } + + CXXOPTS_NODISCARD + std::shared_ptr + make_storage() const + { + return m_value->clone(); + } + + CXXOPTS_NODISCARD + const std::string& + short_name() const + { + return m_short; + } + + CXXOPTS_NODISCARD + const std::string& + first_long_name() const + { + return first_or_empty(m_long); + } + + CXXOPTS_NODISCARD + const std::string& + essential_name() const + { + return m_long.empty() ? m_short : m_long.front(); + } + + CXXOPTS_NODISCARD + const OptionNames & + long_names() const + { + return m_long; + } + + std::size_t + hash() const + { + return m_hash; + } + + private: + std::string m_short{}; + OptionNames m_long{}; + String m_desc{}; + std::shared_ptr m_value{}; + int m_count; + + std::size_t m_hash{}; +}; + +struct HelpOptionDetails +{ + std::string s; + OptionNames l; + String desc; + bool has_default; + std::string default_value; + bool has_implicit; + std::string implicit_value; + std::string arg_help; + bool is_container; + bool is_boolean; +}; + +struct HelpGroupDetails +{ + std::string name{}; + std::string description{}; + std::vector options{}; +}; + +class OptionValue +{ + public: + void + add + ( + const std::shared_ptr& details, + const std::string& text + ) + { + ensure_value(details); + ++m_count; + m_value->add(text); + m_long_names = &details->long_names(); + } + + void + parse + ( + const std::shared_ptr& details, + const std::string& text + ) + { + ensure_value(details); + ++m_count; + m_value->parse(text); + m_long_names = &details->long_names(); + } + + void + parse_default(const std::shared_ptr& details) + { + ensure_value(details); + m_default = true; + m_long_names = &details->long_names(); + m_value->parse(); + } + + void + parse_no_value(const std::shared_ptr& details) + { + m_long_names = &details->long_names(); + } + +#if defined(CXXOPTS_NULL_DEREF_IGNORE) +CXXOPTS_DIAGNOSTIC_PUSH +CXXOPTS_IGNORE_WARNING("-Wnull-dereference") +#endif + + CXXOPTS_NODISCARD + std::size_t + count() const noexcept + { + return m_count; + } + +#if defined(CXXOPTS_NULL_DEREF_IGNORE) +CXXOPTS_DIAGNOSTIC_POP +#endif + + // TODO: maybe default options should count towards the number of arguments + CXXOPTS_NODISCARD + bool + has_default() const noexcept + { + return m_default; + } + + template + const T& + as() const + { + if (m_value == nullptr) { + throw_or_mimic( + m_long_names == nullptr ? "" : first_or_empty(*m_long_names)); + } + + return CXXOPTS_RTTI_CAST&>(*m_value).get(); + } + +#ifdef CXXOPTS_HAS_OPTIONAL + template + std::optional + as_optional() const + { + if (m_value == nullptr) { + return std::nullopt; + } + return as(); + } +#endif + + private: + void + ensure_value(const std::shared_ptr& details) + { + if (m_value == nullptr) + { + m_value = details->make_storage(); + } + } + + + const OptionNames * m_long_names = nullptr; + // Holding this pointer is safe, since OptionValue's only exist in key-value pairs, + // where the key has the string we point to. + std::shared_ptr m_value{}; + std::size_t m_count = 0; + bool m_default = false; +}; + +class KeyValue +{ + public: + KeyValue(std::string key_, std::string value_) noexcept + : m_key(std::move(key_)) + , m_value(std::move(value_)) + { + } + + CXXOPTS_NODISCARD + const std::string& + key() const + { + return m_key; + } + + CXXOPTS_NODISCARD + const std::string& + value() const + { + return m_value; + } + + template + T + as() const + { + T result; + values::parse_value(m_value, result); + return result; + } + + private: + std::string m_key; + std::string m_value; +}; + +using ParsedHashMap = std::unordered_map; +using NameHashMap = std::unordered_map; + +class ParseResult +{ + public: + class Iterator + { + public: + using iterator_category = std::forward_iterator_tag; + using value_type = KeyValue; + using difference_type = void; + using pointer = const KeyValue*; + using reference = const KeyValue&; + + Iterator() = default; + Iterator(const Iterator&) = default; + +// GCC complains about m_iter not being initialised in the member +// initializer list +CXXOPTS_DIAGNOSTIC_PUSH +CXXOPTS_IGNORE_WARNING("-Weffc++") + Iterator(const ParseResult *pr, bool end=false) + : m_pr(pr) + { + if (end) + { + m_sequential = false; + m_iter = m_pr->m_defaults.end(); + } + else + { + m_sequential = true; + m_iter = m_pr->m_sequential.begin(); + + if (m_iter == m_pr->m_sequential.end()) + { + m_sequential = false; + m_iter = m_pr->m_defaults.begin(); + } + } + } +CXXOPTS_DIAGNOSTIC_POP + + Iterator& operator++() + { + ++m_iter; + if(m_sequential && m_iter == m_pr->m_sequential.end()) + { + m_sequential = false; + m_iter = m_pr->m_defaults.begin(); + return *this; + } + return *this; + } + + Iterator operator++(int) + { + Iterator retval = *this; + ++(*this); + return retval; + } + + bool operator==(const Iterator& other) const + { + return (m_sequential == other.m_sequential) && (m_iter == other.m_iter); + } + + bool operator!=(const Iterator& other) const + { + return !(*this == other); + } + + const KeyValue& operator*() + { + return *m_iter; + } + + const KeyValue* operator->() + { + return m_iter.operator->(); + } + + private: + const ParseResult* m_pr; + std::vector::const_iterator m_iter; + bool m_sequential = true; + }; + + ParseResult() = default; + ParseResult(const ParseResult&) = default; + + ParseResult(NameHashMap&& keys, ParsedHashMap&& values, std::vector sequential, + std::vector default_opts, std::vector&& unmatched_args) + : m_keys(std::move(keys)) + , m_values(std::move(values)) + , m_sequential(std::move(sequential)) + , m_defaults(std::move(default_opts)) + , m_unmatched(std::move(unmatched_args)) + { + } + + ParseResult& operator=(ParseResult&&) = default; + ParseResult& operator=(const ParseResult&) = default; + + Iterator + begin() const + { + return Iterator(this); + } + + Iterator + end() const + { + return Iterator(this, true); + } + + std::size_t + count(const std::string& o) const + { + auto iter = m_keys.find(o); + if (iter == m_keys.end()) + { + return 0; + } + + auto viter = m_values.find(iter->second); + + if (viter == m_values.end()) + { + return 0; + } + + return viter->second.count(); + } + + bool + contains(const std::string& o) const + { + return static_cast(count(o)); + } + + const OptionValue& + operator[](const std::string& option) const + { + auto iter = m_keys.find(option); + + if (iter == m_keys.end()) + { + throw_or_mimic(option); + } + + auto viter = m_values.find(iter->second); + + if (viter == m_values.end()) + { + throw_or_mimic(option); + } + + return viter->second; + } + +#ifdef CXXOPTS_HAS_OPTIONAL + template + std::optional + as_optional(const std::string& option) const + { + auto iter = m_keys.find(option); + if (iter != m_keys.end()) + { + auto viter = m_values.find(iter->second); + if (viter != m_values.end()) + { + return viter->second.as_optional(); + } + } + return std::nullopt; + } +#endif + + const std::vector& + arguments() const + { + return m_sequential; + } + + const std::vector& + unmatched() const + { + return m_unmatched; + } + + const std::vector& + defaults() const + { + return m_defaults; + } + + const std::string + arguments_string() const + { + std::string result; + for(const auto& kv: m_sequential) + { + result += kv.key() + " = " + kv.value() + "\n"; + } + for(const auto& kv: m_defaults) + { + result += kv.key() + " = " + kv.value() + " " + "(default)" + "\n"; + } + return result; + } + + private: + NameHashMap m_keys{}; + ParsedHashMap m_values{}; + std::vector m_sequential{}; + std::vector m_defaults{}; + std::vector m_unmatched{}; +}; + +struct Option +{ + Option + ( + std::string opts, + std::string desc, + std::shared_ptr value = ::cxxopts::value(), + std::string arg_help = "" + ) + : opts_(std::move(opts)) + , desc_(std::move(desc)) + , value_(std::move(value)) + , arg_help_(std::move(arg_help)) + { + } + + std::string opts_; + std::string desc_; + std::shared_ptr value_; + std::string arg_help_; +}; + +using OptionMap = std::unordered_map>; +using PositionalList = std::vector; +using PositionalListIterator = PositionalList::const_iterator; + +class OptionParser +{ + public: + OptionParser(const OptionMap& options, const PositionalList& positional, bool allow_unrecognised) + : m_options(options) + , m_positional(positional) + , m_allow_unrecognised(allow_unrecognised) + { + } + + ParseResult + parse(int argc, const char* const* argv); + + bool + consume_positional(const std::string& a, PositionalListIterator& next); + + void + checked_parse_arg + ( + int argc, + const char* const* argv, + int& current, + const std::shared_ptr& value, + const std::string& name + ); + + void + add_to_option(const std::shared_ptr& value, const std::string& arg); + + void + parse_option + ( + const std::shared_ptr& value, + const std::string& name, + const std::string& arg = "" + ); + + void + parse_default(const std::shared_ptr& details); + + void + parse_no_value(const std::shared_ptr& details); + + private: + + void finalise_aliases(); + + const OptionMap& m_options; + const PositionalList& m_positional; + + std::vector m_sequential{}; + std::vector m_defaults{}; + bool m_allow_unrecognised; + + ParsedHashMap m_parsed{}; + NameHashMap m_keys{}; +}; + +class Options +{ + public: + + explicit Options(std::string program_name, std::string help_string = "") + : m_program(std::move(program_name)) + , m_help_string(toLocalString(std::move(help_string))) + , m_custom_help("[OPTION...]") + , m_positional_help("positional parameters") + , m_show_positional(false) + , m_allow_unrecognised(false) + , m_width(76) + , m_tab_expansion(false) + , m_options(std::make_shared()) + { + } + + Options& + positional_help(std::string help_text) + { + m_positional_help = std::move(help_text); + return *this; + } + + Options& + custom_help(std::string help_text) + { + m_custom_help = std::move(help_text); + return *this; + } + + Options& + show_positional_help() + { + m_show_positional = true; + return *this; + } + + Options& + allow_unrecognised_options() + { + m_allow_unrecognised = true; + return *this; + } + + Options& + set_width(std::size_t width) + { + m_width = width; + return *this; + } + + Options& + set_tab_expansion(bool expansion=true) + { + m_tab_expansion = expansion; + return *this; + } + + ParseResult + parse(int argc, const char* const* argv); + + OptionAdder + add_options(std::string group = ""); + + void + add_options + ( + const std::string& group, + std::initializer_list