diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..1039c520 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: + - espresso3389 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..02c80476 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# Set update schedule for GitHub Actions +# https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot + +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every week + interval: "weekly" diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml new file mode 100644 index 00000000..2a2023d6 --- /dev/null +++ b/.github/workflows/build-test.yml @@ -0,0 +1,287 @@ +name: Build Test + +on: + push: + branches: [ master, main ] + paths-ignore: + - '**.md' + - 'doc/**' + + pull_request: + branches: [ master, main ] + paths-ignore: + - '**.md' + - 'doc/**' + workflow_dispatch: + inputs: + build_android: + description: 'Build Android' + required: false + type: boolean + default: true + build_ios: + description: 'Build iOS' + required: false + type: boolean + default: true + build_macos: + description: 'Build macOS' + required: false + type: boolean + default: true + build_linux: + description: 'Build Linux' + required: false + type: boolean + default: true + build_linux_arm64: + description: 'Build Linux ARM64' + required: false + type: boolean + default: true + build_windows: + description: 'Build Windows' + required: false + type: boolean + default: true + build_windows_arm64: + description: 'Build Windows ARM64' + required: false + type: boolean + default: true + build_web: + description: 'Build Web' + required: false + type: boolean + default: true + +jobs: + # Android build + android: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.build_android }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Setup Java + uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0 + with: + distribution: 'temurin' + java-version: '18' + + - name: Setup Flutter + run: | + git clone https://github.com/flutter/flutter.git --depth 1 --branch stable ~/flutter + echo "$HOME/flutter/bin" >> $GITHUB_PATH + ~/flutter/bin/flutter config --no-enable-linux-desktop + ~/flutter/bin/flutter channel stable + ~/flutter/bin/flutter doctor -v + yes | ~/flutter/bin/flutter doctor --android-licenses + + - name: Install dependencies + working-directory: packages/pdfrx/example/viewer + run: flutter pub get + + - name: Build App Bundle + working-directory: packages/pdfrx/example/viewer + run: flutter build apk --debug --verbose + + # iOS build + ios: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.build_ios }} + runs-on: macos-latest + strategy: + matrix: + package_manager: [cocoapods, swiftpm] + name: ios-${{ matrix.package_manager }} + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Setup Flutter + run: | + git clone https://github.com/flutter/flutter.git --depth 1 --branch stable ~/flutter + echo "$HOME/flutter/bin" >> $GITHUB_PATH + ~/flutter/bin/flutter config --no-enable-android --no-enable-macos-desktop + ~/flutter/bin/flutter channel stable + ~/flutter/bin/flutter doctor -v + + - name: Configure package manager + run: | + if [ "${{ matrix.package_manager }}" = "swiftpm" ]; then + ~/flutter/bin/flutter config --enable-swift-package-manager + else + ~/flutter/bin/flutter config --no-enable-swift-package-manager + fi + + - name: Install dependencies + working-directory: packages/pdfrx/example/viewer + run: flutter pub get + + - name: Build iOS (no signing) - ${{ matrix.package_manager }} + working-directory: packages/pdfrx/example/viewer + run: flutter build ios --debug --no-codesign --verbose + + # macOS build + macos: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.build_macos }} + runs-on: macos-latest + strategy: + matrix: + package_manager: [cocoapods, swiftpm] + name: macos-${{ matrix.package_manager }} + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Setup Flutter + run: | + git clone https://github.com/flutter/flutter.git --depth 1 --branch stable ~/flutter + echo "$HOME/flutter/bin" >> $GITHUB_PATH + ~/flutter/bin/flutter config --no-enable-android --no-enable-ios + ~/flutter/bin/flutter channel stable + ~/flutter/bin/flutter doctor -v + + - name: Configure package manager + run: | + if [ "${{ matrix.package_manager }}" = "swiftpm" ]; then + ~/flutter/bin/flutter config --enable-swift-package-manager + else + ~/flutter/bin/flutter config --no-enable-swift-package-manager + fi + + - name: Install dependencies + working-directory: packages/pdfrx/example/viewer + run: flutter pub get + + - name: Build macOS - ${{ matrix.package_manager }} + working-directory: packages/pdfrx/example/viewer + run: flutter build macos --debug --verbose + + # Linux build + linux: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.build_linux }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Install Linux dependencies + run: | + sudo apt-get update -y + sudo apt-get install -y ninja-build libgtk-3-dev + + - name: Setup Flutter + run: | + git clone https://github.com/flutter/flutter.git --depth 1 --branch stable ~/flutter + echo "$HOME/flutter/bin" >> $GITHUB_PATH + ~/flutter/bin/flutter config --no-enable-android + ~/flutter/bin/flutter channel stable + ~/flutter/bin/flutter doctor -v + + - name: Install dependencies + working-directory: packages/pdfrx/example/viewer + run: flutter pub get + + - name: Build Linux + working-directory: packages/pdfrx/example/viewer + run: flutter build linux --debug --verbose + + # Linux ARM64 build + linux-arm64: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.build_linux_arm64 }} + runs-on: ubuntu-24.04-arm + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Install Linux dependencies + run: | + sudo apt-get update -y + sudo apt-get install -y ninja-build libgtk-3-dev + + - name: Setup Flutter + run: | + git clone https://github.com/flutter/flutter.git --depth 1 --branch stable ~/flutter + echo "$HOME/flutter/bin" >> $GITHUB_PATH + ~/flutter/bin/flutter config --no-enable-android + ~/flutter/bin/flutter channel stable + ~/flutter/bin/flutter doctor -v + + - name: Install dependencies + working-directory: packages/pdfrx/example/viewer + run: flutter pub get + + - name: Build Linux + working-directory: packages/pdfrx/example/viewer + run: flutter build linux --debug --verbose + + # Windows build + windows: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.build_windows }} + runs-on: windows-latest + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Setup Flutter + shell: pwsh + run: | + git clone https://github.com/flutter/flutter.git --depth 1 --branch stable C:\flutter + echo "C:\flutter\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + C:\flutter\bin\flutter.bat config --no-enable-android + C:\flutter\bin\flutter.bat channel stable + C:\flutter\bin\flutter.bat doctor -v + + - name: Install dependencies + working-directory: packages/pdfrx/example/viewer + run: C:\flutter\bin\flutter.bat pub get + + - name: Build Windows + working-directory: packages/pdfrx/example/viewer + run: C:\flutter\bin\flutter.bat build windows --debug --verbose + + # Windows ARM64 build (requires ARM64 runner or cross-compilation) + windows-arm64: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.build_windows_arm64 }} + runs-on: windows-11-arm + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Setup Flutter (master branch for ARM64 support) + shell: pwsh + run: | + git clone https://github.com/flutter/flutter.git --depth 1 C:\flutter + echo "C:\flutter\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + C:\flutter\bin\flutter.bat config --no-enable-android + C:\flutter\bin\flutter.bat doctor -v + + - name: Install dependencies + working-directory: packages/pdfrx/example/viewer + run: C:\flutter\bin\flutter.bat pub get + + - name: Build Windows ARM64 + working-directory: packages/pdfrx/example/viewer + run: C:\flutter\bin\flutter.bat build windows --debug --verbose + + # Web build + web: + if: ${{ github.event_name != 'workflow_dispatch' || inputs.build_web }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Setup Flutter + run: | + git clone https://github.com/flutter/flutter.git --depth 1 --branch stable ~/flutter + echo "$HOME/flutter/bin" >> $GITHUB_PATH + ~/flutter/bin/flutter config --no-enable-android --no-enable-linux-desktop + ~/flutter/bin/flutter channel stable + ~/flutter/bin/flutter doctor -v + + - name: Install dependencies + working-directory: packages/pdfrx/example/viewer + run: flutter pub get + + - name: Build Web + working-directory: packages/pdfrx/example/viewer + run: flutter build web --verbose + + - name: Build Web (WASM) + working-directory: packages/pdfrx/example/viewer + run: flutter build web --wasm --verbose diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml new file mode 100644 index 00000000..def5c851 --- /dev/null +++ b/.github/workflows/claude.yml @@ -0,0 +1,37 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude') && contains(vars.CLAUDE_ALLOWED_USERS, github.event.comment.user.login)) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude') && contains(vars.CLAUDE_ALLOWED_USERS, github.event.comment.user.login)) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude') && contains(vars.CLAUDE_ALLOWED_USERS, github.event.review.user.login)) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) && contains(vars.CLAUDE_ALLOWED_USERS, github.event.issue.user.login)) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@beta + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + diff --git a/.github/workflows/github-pages.yml b/.github/workflows/github-pages.yml index bd073e4f..1efcd7ff 100644 --- a/.github/workflows/github-pages.yml +++ b/.github/workflows/github-pages.yml @@ -4,42 +4,191 @@ on: push: branches: - master + paths-ignore: + - '**.md' + - 'doc/**' jobs: - build-and-deploy: + build: runs-on: ubuntu-latest - # NOTE: This workflow automatically update gh-pages branch and it requires write permissions - permissions: - contents: write + strategy: + matrix: + example: [viewer, pdf_combine] steps: - name: Checkout repository - uses: actions/checkout@v4 - - name: Set up Flutter - uses: subosito/flutter-action@v2 - with: - channel: stable + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - name: Setup Flutter + run: | + git clone https://github.com/flutter/flutter.git --depth 1 --branch stable ~/flutter + echo "$HOME/flutter/bin" >> $GITHUB_PATH + ~/flutter/bin/flutter config --no-enable-android --no-enable-linux-desktop + ~/flutter/bin/flutter channel stable - name: Check Flutter SDK version run: | flutter --version FLUTTER_VERSION=$(flutter --version | head -n 1 | awk '{print $2}') echo "FLUTTER_VERSION=$FLUTTER_VERSION" >> $GITHUB_ENV + - name: Check pdfrx version run: | PDFRX_VERSION=$(awk -F": " '/^version:/ {print $2}' pubspec.yaml) echo "PDFRX_VERSION=$PDFRX_VERSION" >> $GITHUB_ENV - - name: Enable Flutter Web - run: flutter config --enable-web - name: Install dependencies run: flutter pub get - name: Build Flutter Web App (WASM) run: | - cd example/viewer/ - flutter build web --wasm --release --dart-define=GITHUB_COMMIT=$GITHUB_SHA --dart-define=PDFRX_VERSION=$PDFRX_VERSION --dart-define=FLUTTER_VERSION=$FLUTTER_VERSION - sed -i \ - -e 's|||' \ - -e "s/__CONFIGS__/pdfrx=${PDFRX_VERSION},commit=${GITHUB_SHA},flutter=${FLUTTER_VERSION}/g" \ - build/web/index.html + cd packages/pdfrx/example/${{ matrix.example }}/ + flutter build web --wasm --release --base-href "/pdfrx/${{ matrix.example }}/" --dart-define=GITHUB_COMMIT=$GITHUB_SHA --dart-define=PDFRX_VERSION=$PDFRX_VERSION + sed -i "s/__CONFIGS__/pdfrx=${PDFRX_VERSION},commit=${GITHUB_SHA},flutter=${FLUTTER_VERSION}/g" build/web/index.html + - name: Upload build artifact + uses: actions/upload-artifact@v6.0.0 + with: + name: ${{ matrix.example }}-web + path: packages/pdfrx/example/${{ matrix.example }}/build/web/ + retention-days: 1 + + deploy: + runs-on: ubuntu-latest + needs: build + # NOTE: This workflow automatically update gh-pages branch and it requires write permissions + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - name: Download all artifacts + uses: actions/download-artifact@v7 + with: + path: artifacts/ + - name: Prepare deployment directory + run: | + mkdir -p deploy_temp + + # Copy each example to its subdirectory + cp -r artifacts/viewer-web deploy_temp/viewer + cp -r artifacts/pdf_combine-web deploy_temp/pdf_combine + + # Extract version info from artifact + VERSION_INFO=$(grep 'pdfrx-configs' artifacts/viewer-web/index.html | sed 's/.*content="\([^"]*\)".*/\1/') + PDFRX_VERSION=$(echo "$VERSION_INFO" | sed 's/.*pdfrx=\([^,]*\).*/\1/') + FLUTTER_VERSION=$(echo "$VERSION_INFO" | sed 's/.*flutter=\([^,]*\).*/\1/') + echo "PDFRX_VERSION=$PDFRX_VERSION" >> $GITHUB_ENV + echo "FLUTTER_VERSION=$FLUTTER_VERSION" >> $GITHUB_ENV + + # Create root index.html + cat > deploy_temp/index.html << 'EOF' + + + + + + + pdfrx - Flutter PDF Library + + + +

pdfrx - Flutter PDF Library

+

A powerful and flexible PDF viewer and manipulation library for Flutter.

+ +
+
+

PDF Viewer

+

A full-featured PDF viewer with zoom, pan, text selection, and more.

+ Launch Viewer Demo +
+ +
+

PDF Combine

+

Combine multiple PDF files into a single document.

+ Launch PDF Combine Demo +
+
+ + + + + EOF + + # Add version info to index.html + COMMIT_SHORT=${GITHUB_SHA:0:7} + sed -i "s/__CONFIGS__/pdfrx=${PDFRX_VERSION},commit=${GITHUB_SHA},flutter=${FLUTTER_VERSION}/g" deploy_temp/index.html + sed -i "s/__PDFRX_VERSION__/${PDFRX_VERSION}/g" deploy_temp/index.html + sed -i "s/__COMMIT__/${GITHUB_SHA}/g" deploy_temp/index.html + sed -i "s/__COMMIT_SHORT__/${COMMIT_SHORT}/g" deploy_temp/index.html + sed -i "s/__FLUTTER_VERSION__/${FLUTTER_VERSION}/g" deploy_temp/index.html - name: Configure Git for deployment run: | git config user.email "action@github.com" @@ -47,7 +196,7 @@ jobs: git remote set-url origin https://x-access-token:${{ secrets.TOKEN_FOR_GHPAGE_DEPLOYMENT }}@github.com/${{ github.repository }}.git - name: Deploy to GitHub Pages using subtree push run: | - git add -f example/viewer/build/web + git add -f deploy_temp git commit -m "$PDFRX_VERSION $GITHUB_SHA" - git subtree split --prefix example/viewer/build/web -b tmp + git subtree split --prefix deploy_temp -b tmp git push -f origin tmp:gh-pages diff --git a/.github/workflows/pana-analysis.yml b/.github/workflows/pana-analysis.yml new file mode 100644 index 00000000..37f40d0b --- /dev/null +++ b/.github/workflows/pana-analysis.yml @@ -0,0 +1,54 @@ +name: Pana Analysis + +on: + push: + branches: [ master, main ] + paths-ignore: + - '**.md' + - 'doc/**' + pull_request: + branches: [ master, main ] + paths-ignore: + - '**.md' + - 'doc/**' + workflow_dispatch: + +jobs: + pana: + name: Run pana analysis + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - package_name: pdfrx_engine + package_path: packages/pdfrx_engine + - package_name: pdfrx + package_path: packages/pdfrx + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Install Linux dependencies + run: | + sudo apt-get update -y + sudo apt-get install -y webp + + - name: Setup Flutter + run: | + git clone https://github.com/flutter/flutter.git --depth 1 --branch stable ~/flutter + echo "$HOME/flutter/bin" >> $GITHUB_PATH + ~/flutter/bin/flutter config --no-enable-android --no-enable-linux-desktop + ~/flutter/bin/flutter channel stable + ~/flutter/bin/flutter doctor -v + + - name: Pub get (workspace) + run: flutter pub get + + - name: Activate pana + run: | + dart pub global activate pana + echo "$HOME/.pub-cache/bin" >> $GITHUB_PATH + + - name: Run pana (${{ matrix.package_name }}) + working-directory: ${{ matrix.package_path }} + run: ~/.pub-cache/bin/pana --json --no-warning --exit-code-threshold 0 | jq -e '.tags | any(. == "is:wasm-ready")' diff --git a/.github/workflows/pdfium-apple-release.yml b/.github/workflows/pdfium-apple-release.yml index c0dac841..c7923d2f 100644 --- a/.github/workflows/pdfium-apple-release.yml +++ b/.github/workflows/pdfium-apple-release.yml @@ -6,13 +6,36 @@ on: jobs: build: runs-on: macos-latest + strategy: + matrix: + target: [ios, macos] steps: - name: Checkout - uses: actions/checkout@v2 - - name: Build PDFium - run: ./darwin/pdfium/build + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - name: Build PDFium ${{ matrix.target }} + run: ./packages/pdfrx/darwin/pdfium/build ${{ matrix.target }} + - name: Upload PDFium artifact + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: pdfium-${{ matrix.target }} + path: ./packages/pdfrx/darwin/pdfium/pdfium-${{ matrix.target }}.zip + + release: + needs: build + runs-on: macos-latest + steps: + - name: Download iOS artifact + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v6.0.0 + with: + name: pdfium-ios + path: ./packages/pdfrx/darwin/pdfium/ + - name: Download macOS artifact + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v6.0.0 + with: + name: pdfium-macos + path: ./packages/pdfrx/darwin/pdfium/ - name: Release PDFium - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v0.1.15 with: token: ${{ secrets.TOKEN_FOR_RELEASE }} tag_name: ${{ github.ref_name }} @@ -20,5 +43,5 @@ jobs: prerelease: false body: iOS/macOS PDFium prebuilt binary distribution for pdfrx (${{ github.ref_name }}). files: | - ./darwin/pdfium/pdfium-ios.tgz - ./darwin/pdfium/pdfium-macos.tgz + ./packages/pdfrx/darwin/pdfium/pdfium-ios.zip + ./packages/pdfrx/darwin/pdfium/pdfium-macos.zip diff --git a/.gitignore b/.gitignore index f15d08a2..f06a6c18 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ .history .svn/ migrate_working_dir/ +.tmp/ # IntelliJ related *.iml @@ -19,6 +20,10 @@ migrate_working_dir/ # C++ .cxx +# For pdfium builds on macOS +.build/ +.swiftpm/ + # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. @@ -27,8 +32,21 @@ migrate_working_dir/ # Flutter/Dart/Pub related # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. /pubspec.lock -**/doc/api/ .dart_tool/ -build/ -/test/.tmp +.claude/ + +pubspec_overrides.yaml +.mcp.json + +.serena/ + +# SHOULD WE NEED THIS??: iOS/macOS build artifacts that change frequently +**/ios/Podfile.lock +**/macos/Podfile.lock +**/ios/**/*.xcodeproj/project.pbxproj +**/macos/**/*.xcodeproj/project.pbxproj + +build/ios/ + +.flutter-plugins-dependencies diff --git a/.pubignore b/.pubignore deleted file mode 100644 index 1d508c00..00000000 --- a/.pubignore +++ /dev/null @@ -1,2 +0,0 @@ -wasm/ -example/ diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 2670dd6d..7e52833e 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -1,21 +1,48 @@ { "env": { - "ndkVersion": "21.1.6352462" + "ndkVersion": "27.0.12077973" }, "configurations": [ { "name": "Win32", "includePath": [ - "${workspaceFolder}/android/.lib/include", - "${workspaceFolder}/iOS/Classes/*", - "${env:LOCALAPPDATA}/Android/Sdk/ndk/${ndkVersion}/sysroot/usr/include/*" + "${workspaceFolder}/packages/pdfrx/android/.lib/latest/include", + "${workspaceFolder}/packages/pdfrx/example/viewer/build/windows/x64/.lib/latest/include", + "${workspaceFolder}/packages/pdfrx/src", + "${env.LOCALAPPDATA}/Android/Sdk/ndk/${ndkVersion}/sysroot/usr/include/*" ], "defines": [], "cStandard": "c17", "cppStandard": "c++17", - "intelliSenseMode": "linux-clang-arm64", - "configurationProvider": "go2sh.cmake-integration" + "intelliSenseMode": "msvc-x64" + }, + { + "name": "Linux", + "includePath": [ + "${workspaceFolder}/packages/pdfrx/android/.lib/latest/include", + "${workspaceFolder}/packages/pdfrx/example/viewer/build/linux/x64/.lib/latest/include", + "${workspaceFolder}/packages/pdfrx/src" + ], + "defines": [], + "cStandard": "c17", + "cppStandard": "c++17", + "intelliSenseMode": "linux-clang-arm64" + }, + { + "name": "Mac", + "includePath": [ + "${workspaceFolder}/packages/pdfrx/android/.lib/latest/include", + "${workspaceFolder}/packages/pdfrx/darwin/pdfrx/Sources/interop", + "${workspaceFolder}/packages/pdfrx/src", + "${workspaceFolder}/packages/pdfrx/darwin/pdfium/.lib/**", + "${workspaceFolder}/packages/pdfrx/example/viewer/build/**" + ], + "defines": [], + "cStandard": "c17", + "cppStandard": "c++17", + "intelliSenseMode": "macos-clang-arm64" } ], - "version": 4 + "version": 4, + "enableConfigurationSquiggles": true } \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..e57075c2 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "dart-code.dart-code", + "dart-code.flutter", + "vknabel.vscode-swiftformat", + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json index dfe3c507..134f4d1d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,17 +5,37 @@ "version": "0.2.0", "configurations": [ { - "name": "viewer example", + "name": "Example", "request": "launch", "type": "dart", - "program": "example/viewer/lib/main.dart" + "program": "packages/pdfrx/example/viewer/lib/main.dart" }, { - "name": "viewer example (WASM)", + "name": "WASM", "request": "launch", "type": "dart", - "program": "example/viewer/lib/main.dart", + "program": "packages/pdfrx/example/viewer/lib/main.dart", "args": ["-d", "chrome", "--wasm"] + }, + { + "name": "WASM-Server", + "request": "launch", + "type": "dart", + "program": "packages/pdfrx/example/viewer/lib/main.dart", + "args": ["-d", "web-server", "--wasm", "--web-port", "8080", "--web-hostname", "127.0.0.1"] + }, + { + "name": "PDF Combine", + "request": "launch", + "type": "dart", + "program": "packages/pdfrx/example/pdf_combine/lib/main.dart" + }, + { + "name": "pdfrx:remove_wasm_modules", + "request": "launch", + "type": "dart", + "program": "packages/pdfrx/bin/remove_wasm_modules.dart", + "args": [] } ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index d8f1a982..827beeee 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,13 +1,17 @@ { "cSpell.words": [ "AACTION", + "AARRGGBB", "ACRO", "alloc", + "allprojects", "ANNOT", "Annots", "Antialiasing", + "appbundle", "APPEARANCEMODE", "ARGB", + "armeabi", "autofocus", "bblanchon", "Bezier", @@ -15,10 +19,13 @@ "bgra", "BSTR", "buflen", + "buildscript", "BYTESTRING", "CALGRAY", "calloc", "CALRGB", + "ccall", + "CGPDF", "Charcodes", "clippath", "cmap", @@ -26,10 +33,15 @@ "COLORSPACE", "COLORTYPE", "COMBOBOX", + "contextmenu", + "coregraphics", "credentialless", "Cupertino", + "cwrap", "d_reclen", + "dartdoc", "dartify", + "deinit", "Dests", "DEVICECMYK", "DEVICEGRAY", @@ -46,12 +58,14 @@ "emscripten", "endtemplate", "errn", + "externref", "fcntl", "ffigen", "FILEACCESS", "FILEATTACHMENT", "FILEHANDLER", "FILEIDTYPE", + "FILEWRITE", "fillmode", "findwhat", "FITBH", @@ -73,6 +87,7 @@ "FPDFPAGE", "fpdfview", "FREETEXT", + "FSDK", "FXCT", "getdents", "glyphpath", @@ -104,6 +119,7 @@ "LISTBOX", "listenables", "localtime", + "LOGFONT", "longjmp", "LTRB", "LTWH", @@ -114,11 +130,13 @@ "maxage", "microtask", "MOVETO", + "Naskh", "NATIVETEXT", "NESW", "newfstatat", "NOEXPORT", "NOROTATE", + "noto", "NOVIEW", "NOZOOM", "NULLOBJ", @@ -141,6 +159,7 @@ "POLYLINE", "PRINTERMARK", "PRINTMODE", + "Pthread", "pubspec", "QUADPOINTSF", "RADIOBUTTON", @@ -157,6 +176,7 @@ "SCHHANDLE", "Schyler", "scrollable", + "Scrollables", "selectable", "selectables", "SIZEF", @@ -172,6 +192,8 @@ "sublist", "Subrect", "supercedes", + "swiftpm", + "SYSFONTINFO", "SYSTEMTIME", "TEXTONLY", "TEXTPAGE", @@ -182,6 +204,7 @@ "TRAPNET", "TRUETYPE", "tzset", + "uleb", "unawaited", "uncollapsed", "unlinkat", @@ -189,6 +212,7 @@ "VBEAM", "VIEWERREF", "viiii", + "vrtl", "vsync", "WCHAR", "WIDESTRING", @@ -198,7 +222,9 @@ "XFAWIDGET", "xobject", "YESNO", - "YESNOCANCEL" + "YESNOCANCEL", + "YMDHMS", + "ZapfDingbats" ], "editor.codeActionsOnSave": { "source.fixAll": "always", @@ -277,5 +303,23 @@ "xloctime": "cpp", "xstring": "cpp" }, - "cmake.ignoreCMakeListsMissing": true -} \ No newline at end of file + "cmake.ignoreCMakeListsMissing": true, + "[dart]": { + "editor.formatOnSave": true, + "editor.formatOnType": true, + "editor.rulers": [ + 120 + ] + }, + "[swift]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "vknabel.vscode-swiftformat", + "editor.rulers": [ + 120 + ] + }, + "swiftformat.enable": true, + "swiftformat.onlyEnableOnSwiftPMProjects": false, + "swiftformat.onlyEnableWithConfig": false, + "java.jdt.ls.vmargs": "-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -Dsun.zip.disableMemoryMapping=true -Xmx2G -Xms100m -Xlog:disable" +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..3880e83d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,31 @@ +# AGENTS.md + +This file provides guidance to AI agents and developers when working with code in this repository. + +## Quick Start for Agents + +- Keep existing user changes intact; if you notice unexpected edits you didn't make, pause and ask the user how to proceed. +- Prefer fast, non-destructive tools (`rg`, `rg --files`, targeted tests) and run commands with an explicit `workdir`; avoid wandering `cd` commands. +- Leave release artifacts (`CHANGELOG.md`, version numbers, tags) untouched unless the task is explicitly about publishing. +- Default to ASCII output and add only brief clarifying comments when the code is non-obvious. + +## Documentation Index + +Detailed guidance is split into focused files in `doc/agents/`: + +- [doc/agents/PROJECT-STRUCTURE.md](doc/agents/PROJECT-STRUCTURE.md) - Package overview, dependencies, and architecture +- [doc/agents/COMMANDS.md](doc/agents/COMMANDS.md) - Common commands, testing, and platform builds +- [doc/agents/RELEASING.md](doc/agents/RELEASING.md) - Release process and publishing checklist +- [doc/agents/CODE-STYLE.md](doc/agents/CODE-STYLE.md) - Code style, documentation, and dependency policies + +## Project Overview (Summary) + +pdfrx is a monorepo containing five packages: + +1. **pdfium_dart** - Low-level Dart FFI bindings for PDFium +2. **pdfium_flutter** - Flutter FFI plugin bundling PDFium binaries +3. **pdfrx_engine** - Platform-agnostic PDF rendering API (pure Dart) +4. **pdfrx** - Cross-platform PDF viewer Flutter plugin +5. **pdfrx_coregraphics** - CoreGraphics-backed renderer for iOS/macOS (experimental) + +See [doc/agents/PROJECT-STRUCTURE.md](doc/agents/PROJECT-STRUCTURE.md) for detailed package descriptions. diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 0c6d8c10..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,695 +0,0 @@ -# 1.1.11 - -- Color.withOpacity -> Color.withValues, Color.value -> Color.toARGB32() - -# 1.1.10 - -- Update project structure to conform to [Package layout conventions](https://dart.dev/tools/pub/package-layout) -- revert: example code move on 1.1.9 - -# 1.1.9 - -- Move back the example viewer to example directory - -# 1.1.8 - -- Internal refactoring to improve the code integrity - -# 1.1.7 - -- Introducing allowDataOwnershipTransfer on PdfDocument.openData to allow transfer data ownership of the passed data; it is false by default to keep consistency with the previous behavior - - This actually fixes #303 but the drawback is that extra memory may be consumed on Flutter Web... - -# 1.1.6 - -- "Bleeding edge" Pdfium WASM support (disabled by default) - -# 1.1.5 - -- Explicitly specify web support on pubspec.yaml - -# 1.1.4 - -- SDK constraint gets back to `>=3.7.0-323.0.dev` - -# 1.1.3 - -- Further WASM compatibility updates -- Demo page: CORS override for GitHub Pages using [gzuidhof/coi-serviceworker](https://github.com/gzuidhof/coi-serviceworker) - -# 1.1.2 - -- FIXED: if running with WASM enabled on Flutter Web, certain PDF file could not be loaded correctly -- Debug log to know WASM/SharedArrayBuffer status on Flutter Web - -# 1.1.1 - -- Supporting Flutter 3.29.0/Dart 3.7.0 (Stable) with workaround for breaking changes on Flutter 3.29.0 (#295) - - It breaks compatibility with older stable Flutter versions :( - -# 1.0.103 - -- Change the default CDN for pdf.js to `https://cdn.jsdelivr.net/npm/pdfjs-dist@/build/pdf.js` to deal with CORS error on loading CMAP files -- FIXED: pdfjsGetDocumentFromData, which is used by various PdfDocument open functions, does not propagate cMapUrl/cMapPacked to the pdf.js - -# 1.0.102 - -- dart2wasm compatibility updates -- Pdf.js 4.10.38 -- PdfTextSearcher correctly releases its listeners on dispose -- Example viewer code updates - -# 1.0.101 - -- Revert commit d66fb3f that breaks consistency; Color.withValues -> Color.withOpacity -- Update pdfium ffi bindings - -# 1.0.100 - -- PdfTextSearcher introduces text caches (#293) -- PdfTextSearcher search reset issue (#291) -- collection's version spec. reverted to pre-1.0.95 - -# 1.0.99 - -- Introduces Pdfrx.fontPaths to set pdfium font loading path (#140) - -# 1.0.98 - -- Introduces PdfViewerController.calcFitZoomMatrices to realize fit-to-width easier - -# 1.0.97 - -- Document updates - -# 1.0.96 - -- FIXED: #260 onTextSelectionChange callback cant be called - -# 1.0.95 - -- FIXED: #273; apart from the ream WASM support, it fixes several compilation issues with --wasm option - -# 1.0.94 - -- Merge PR #272; Fix minScale is not used - -# 1.0.93 - -- Merge PR #264; Check for non-existent zoom element in PdfDest.params in some PDFs -- FIXED: Widget tests starts to fail when using PdfViewer widget #263 - -# 1.0.92 - -- Merge PR #262; Remove redundant check that breaks building on some systems - -# 1.0.91 - -- Fixes selection issues caused by the changes on 1.0.90 - -# 1.0.90 - -- Introduces selectableRegionInjector/perPageSelectableRegionInjector (#256) - -# 1.0.89 - -- web 1.1.0 support (#254) - -# 1.0.88 - -- Merge PR #251 - -# 1.0.87 - -- BREAKING CHANGE: add more parameters to PdfViewerParams.normalizeMatrix to make it easier to handle more complex situations (#239) - -# 1.0.86 - -- Add PdfViewerParams.normalizeMatrix to customize the transform matrix restriction; customizing existing logic on _PdfViewerState._makeMatrixInSafeRange; for issues like #239 - -# 1.0.85 - -- Fixes single-page layout issue on viewer start (#247) -- Fixes blurry image issues (#245, #232) - -# 1.0.84 - -- Merge PR #230 to add try-catch on UTF-8 decoding of URI path - -# 1.0.83 - -- Web related improvements - - PDF.js 4.5.136 - - Remove dependency to dart:js_interop_unsafe - - Remove unnecessary synchronized call -- Improve text selection stability (#4, #185) -- Add more mounted checks to improve PdfViewer stability and speed - -# 1.0.82 - -- collection/rxdart dependency workaround (#211) - -# 1.0.81 - -- Introduces PdfViewerController.useDocument to make it easy to use PdfDocument safely -- Introduces PdfViewerController.pageCount to get page count without explicitly access PdfViewerController.pages -- PdfViewerController.document/PdfViewerController.pages are now deprecated - -# 1.0.80 - -- BREAKING CHANGE: PdfViewerParams.viewerOverlayBuilder introduces third parameter named handleLinkTap, which is used with GestureDetector to handle link-tap events on user code (#175) -- Fix typos on README.md - -# 1.0.79 - -- FIXED: RangeError on PdfViewer.uri when missing "Expires" header (#206) - -# 1.0.78 - -- Add packagingOptions pickFirst to workaround multiple libpdfium.so problem on Android build (#8) -- FIXED: \_relayoutPages may cause null access -- Update README.md to explain PdfViewerParam.linkHandlerParams for link handling - -# 1.0.77 - -- #175: Woops, just missing synchronized to call loadLinks causes multiple load invocations... - -# 1.0.76 - -- Add several tweaks to reduce PdfLink's memory footprint (Related: #175) -- Introduces PdfViewerParam.linkHandlerParams and PdfLinkHandlerParams to show/handle PDF links without using Flutter Widgets (#175) - -# 1.0.75 - -- PDF.js 4.4.168 - -# 1.0.74 - -- Introduces PdfViewerController.getPdfPageHitTestResult -- Introduces PdfViewerController.layout to get page layout - -# 1.0.73 - -- Introduces PdfViewerParams.onViewSizeChanged, which is called on view size change - - The feature can be used to keep the screen center on device screen rotation (#194) - -# 1.0.72 - -- FIXED: Example code is not compilable -- FIXED: Marker could not be placed correctly on the example code (#189) -- FIXED: Updated podspec file not to download the same archive again and again (#154) -- Introduces chromium/6555 for all platforms - - Darwin uses pdfium-apple-v9 (chromium/6555) - - ~~Improves memory consumption by pdfium's internal caching feature (#184)~~ - -# 1.0.71 - -- Introduces withCredentials for Web to download PDF file using current session credentials (Cookie) (#182) -- FIXED: Re-download logic error that causes 416 on certain web site (#183) - -# 1.0.70 - -- PdfViewer calls re-layout logic on every zoom ratio changes (#131) -- Add PdfViewerParams.interactionEndFrictionCoefficient (#176) -- Minor fix for downloading cache -- rxdart gets back to 0.27.7 because 0.28.0 causes incompatibility with several other plugins... - -# 1.0.69 - -- FIXED: Small Page Size PDF Not Scaling to Fit Screen (#174) - -# 1.0.68 - -- Introduces PdfViewerController.setCurrentPageNumber (#152) -- BREAKING CHANGE: Current page number behavior change (#152) -- BREAKING CHANGE: PdfPageAnchor behavior changes for existing PdfPageAnchor enumeration values. -- Introduces PdfPageAnchor.top/left/right/bottom -- Introduces PdfViewerController.calcMatrixToEnsureRectVisible - -# 1.0.67 - -- FIXED: LateInitializationError: Field '\_cacheBlockCount@1436474497' has not been initialized (#167) - -# 1.0.66 - -- FIXED: PdfException: Failed to load PDF document (FPDF_GetLastError=3) (#166) -- Add explicit HTTP error handling code (to show the error detail) -- bblanchon/pdfium-binaries 127.0.6517.0 (chromium/6517) (iOS/macOS is still using 6406) - -# 1.0.65 - -- Remove dependency to intl (#151) - -# 1.0.64 - -- Android: minSdkVersion to 21 (related #158) - -# 1.0.63 - -- Workaround for SelectionEventType.selectParagraph that is introduced in master (#156/PR #157) - - The code uses `default` to handle the case but we should update it with the "right" code when it is introduced to the stable - -# 1.0.62 - -- iOS/macOS also uses bblanchon/pdfium-binaries 125.0.6406.0 (chromium/6406) -- Additional fix for [#147](https://github.com/espresso3389/pdfrx/issues/147) -- Additional implementation for [#132](https://github.com/espresso3389/pdfrx/issues/132) - -# 1.0.61 - -- Introduces PdfViewerParams.pageDropShadow -- Introduces PdfViewerParams.pageBackgroundPaintCallbacks - -# 1.0.60 - -- bblanchon/pdfium-binaries 125.0.6406.0 (chromium/6406) - - default_min_sdk_version=21 to support lower API level devices ([#145](https://github.com/espresso3389/pdfrx/issues/145)) - -# 1.0.59 - -- Fixes concurrency issue on PdfDocument dispose (#143) -- FIXED: Null check operator used on \_guessCurrentPage ([#147](https://github.com/espresso3389/pdfrx/issues/147)) - -# 1.0.58 - -- Any API calls that wraps PDFium are now completely synchronized. They are run in an app-wide single worker isolate - - This is because PDFium does not support any kind of concurrency and even different PdfDocument instances could not be called concurrently - -# 1.0.57 - -- FIXED: possible double-dispose on race condition (#136) -- Add mechanism to cancel partial real size rendering (#137) -- WIP: Custom HTTP header for downloading PDF files (#132) -- Text search match color customization (#142) - -# 1.0.56 - -- Reduce total number of Isolates used when opening PDF documents -- Add PdfViewerParams.calculateCurrentPageNumber -- FIXED: Could not handle certain destination coordinates correctly (#135) - -# 1.0.55 - -- Improve memory consumption by opening/closing page handle every time pdfrx need it (PR #125) - -# 1.0.54 - -- Improves [End] button behavior to reach the actual end of document rather than the top of the last page - - PdfViewerParams.pageAnchorEnd for specifying anchor for the "virtual" page next to the last page -- PdfViewerParams.onePassRenderingScaleThreshold to specify maximum scale that is rendered in single rendering call - - If a page is scaled over the threshold scale, the page is once rendered in the threshold scale and after a some delay, the real scaled image is rendered partially that fits in the view port -- PdfViewerParams.perPageSelectionAreaInjector is introduced to customize text selection behavior - -# 1.0.53 - -- Fixes flicker on scrolling/zooming that was introduced on 1.0.52 -- Revival of high resolution partial rendering - -# 1.0.52 - -- Fixes memory consumption control issues (Related: #121) - -# 1.0.51 - -- FIXED: memory leak on \_PdfPageViewState (#110) -- Remove dependency on dart:js_util (#109) -- FIXED: Crash on \_PdfViewerScrollThumbState (#86) - -# 1.0.50 - -- Introduces PdfViewerParams.useAlternativeFitScaleAsMinScale but it's not recommended to set the value to false because it may degrade the viewer performance - -# 1.0.49 - -- iOS minimum deployment target 12.0 - -# 1.0.11 - -- intl 0.18.1 (#87) - -# 1.0.10+1 - -- Add note for Flutter 3.19/Dart 3.3 support on 1.0.0+ - -# 1.0.10 - -- FIXED: calcZoomStopTable hangs app if zoom ratio is almost 0 (#79) - -# 1.0.9 - -- PdfRect.toRect: scaledTo -> scaledPageSize -- FIXED: PdfJsConfiguration.cMapUrl/cMapPacked does not have correct default values - -# 1.0.8 - -- Condition analysis warnings on auto-generated pdfium_bindings.dart - -# 1.0.7 - -- Requires Flutter 3.19/Dart 3.3 again (pub.dev is upgraded to the stable🎉) -- dart:js_interop based pdf.js interop implementation (remove dependency on package:js) - -# 1.0.6 - -- Due to the pub.dev version issues, the version introduces a "temporary workaround", which downgrades several packages: - - `sdk: '>=3.3.0-76.0.dev <4.0.0'` - - `flutter: '>=3.19.0-0.4.pre'` - - `web: ^0.4.2` - I'll update them as soon as [pub.dev upgrades their toolchains](https://github.com/dart-lang/pub-dev/issues/7484#issuecomment-1948206197) -- pdf.js interop refactoring - -# 1.0.5 - -_NOTE: On pub.dev, 1.0.0+ versions gets [[ANALYSIS ISSUE]](https://pub.dev/packages/pdfrx/versions/1.0.5-testing-version-constraints-1/score). It does not affect your code consistency but API reference is not available until [pub.dev upgrades their toolchains](https://github.com/dart-lang/pub-dev/issues/7484#issuecomment-1948206197)._ - -- Requires Flutter 3.19/Dart 3.3 - -# 1.0.4 - -- Rollback version constraints to the older stable versions... - - I've created an issue for pub.dev: - -# 1.0.3 - -- Again, `flutter: '>=3.19.0-0.4.pre'` - -# 1.0.2 - -- To make the pub.dev analyzer work, we should use `sdk: '>=3.3.0-76.0.dev <4.0.0'` as version constraint... - -# 1.0.1 - -- PdfViewerController.addListener/removeListener independently has listener list on it to make it work regardless of PdfViewer attached or not (#74) - -# 1.0.0 - -- Requires Flutter 3.19/Dart 3.3 -- Update Web code to use package:web (removing dependency to dart:html) - -# 0.4.44 - -- FIXED: PdfViewerParams.boundaryMargin does not work correctly. - -# 0.4.43 - -- Add note for dark/night mode support on README.md; the trick is originally introduced by [pckimlong](https://github.com/pckimlong) on #46. -- FIXED: wrong PdfPageAnchor behavior with landscape pages - -# 0.4.42 - -- FIXED: PdfDocumentRefData's operator== is broken (#66) - -# 0.4.41 - -- Marker example for PdfViewerParams.onTextSelectionChange (#65) -- Add more explanation for sourceName (#66) - -# 0.4.40 - -- Introduces PdfViewerParams.onTextSelectionChange (#65) to know the last text selection - -# 0.4.39 - -- Minor updates on text selection (still experimental......) - -# 0.4.38 - -- Minor updates on text selection (still experimental...) -- Minor fix on PdfPageView - -# 0.4.37 - -- CMake version "3.18.1+" for #48, #62 - -# 0.4.36 - -- Introduces PdfJsConfiguration to configure pdf.js download URLs - -# 0.4.35 - -- Download cache mechanism update (#57/#58) - -# 0.4.34 - -- Document update - -# 0.4.33 - -- Document update - -# 0.4.32 - -- Add PdfViewerParams.calculateInitialPageNumber to calculate the initial page number dynamically -- Add PdfViewerParams.onViewerReady to know when the viewer gets ready - -# 0.4.31 - -- Remove explicit CMake version spec 3.18.1 - -# 0.4.30 - -- FIXED: Link URI contains null-terminator -- Add support text/links on rotated pages -- Stability updates for PdfTextSearcher -- README.md/example updates -- Revival of PdfViewer.data/PdfViewer.custom - -# 0.4.29 - -- Minor fixes to PdfTextSearcher - -# 0.4.28 - -- README.md/example updates - -# 0.4.27 - -- Minor updates and README.md updates - -# 0.4.26 - -- Introduces PdfTextSearcher that helps you to implement search UI feature (#47) -- Example code is vastly changed to explain more about the widget functions - -# 0.4.25 - -- FIXED: Able to scroll outside document area - -# 0.4.24 - -- Huge refactoring on PdfViewerController; it's no longer TransformationController but just a `ValueListenable` - - This fixes an "Unhandled Exception: Null check operator used on a null value" on widget state disposal (#46) - -# 0.4.23 - -- Introduces PdfDocumentViewBuilder/PdfPageView widgets -- Example code is super updated with index and thumbnails. - -# 0.4.22 - -- Web: Now pdf.js is loaded automatically and no modification to index.html is required! -- Default implementation for PdfViewerParams.errorBannerBuilder to show internally thrown errors -- PdfPasswordException is introduced to notify password error -- PdfDocumentRef now has stackTrace for error -- PdfFileCache now uses dedicated http.Client instance - -# 0.4.21 - -- Now PdfDocumentRef has const constructor and PdfViewer.documentRef is also const - -# 0.4.20 - -- Removes PdfDocumentProvider (Actually PdfDocumentRef does everything) -- Fixes breakage introduced by 0.4.18 - -# 0.4.19 - -- firstAttemptByEmptyPassword should be true by default - -# 0.4.18 - -- PdfDocumentProvider supercedes PdfDocumentStore (PR #42) -- PDFium 6259 for Windows, Linux, and Android -- FIXED: Bug: Tests fail due to null operator check on PdfViewerController #44 - -# 0.4.17 - -- Additional fixes to text selection mechanism - -# 0.4.16 - -- Remove password parameters; use passwordProvider instead. -- Fixes several resource leak scenarios on PdfDocument open failures -- Restrict text selection if PDF permission does not allow copying -- Remove PdfViewer.documentRef; unnamed constructor is enough for the purpose - -# 0.4.15 - -- Introduces PdfViewer.documentRef (#36) -- FIXED: PdfViewer.uri is broken on web for non relative paths #37 -- FIXED: Don't Animate to initialPage #39 - -# 0.4.14 - -- Introduces PdfViewerParams.onDocumentChanged event -- Introduces PdfDocument.loadOutline to load outline (a.k.a. bookmark) - -# 0.4.13 - -- Improves document password handling by async PasswordProvider (#20) -- Introduces PdfViewerParams.errorBannerBuilder - -# 0.4.12 - -- Introduces PdfViewerParams.maxImageBytesCachedOnMemory, which restricts the maximum cache memory consumption - - Better than logic based on maxThumbCacheCount -- Remove the following parameters from PdfViewerParams: - - maxThumbCacheCount - - maxRealSizeImageCount - - enableRealSizeRendering - -# 0.4.11 - -- Add support for PDF Destination (Page links) - -# 0.4.10 - -- FIXED: isEncrypted property of document returns always true even the document is not encrypted (#29) - -# 0.4.9 - -- FIXED: SelectionArea makes Web version almost unusable (#31) - -# 0.4.8 - -- FIXED: Unhandled Exception: type 'Null' is not a subtype of type 'PdfPageRenderCancellationTokenPdfium' in type cast (#26) - -# 0.4.7 - -- FIXED: Android build broken? Cannot find libpdfium.so error (#25) -- PdfViewerParams.loadingBannerBuilder to customize HTTP download progress -- PdfViewerParams.linkWidgetBuilder to support embedded links -- WIP: Updated text selection mechanism, which is faster and stable but still certain issues - - Pan-to-scroll does not work on Desktop/Web - - Selection does not work as expected on mobile devices -- Support Linux running on arm64 Raspberry PI (#23/#24) - -# 0.4.6 - -- Introduces PdfPage.render cancellation mechanism - - PdfPageRenderCancellationToken to cancel the rendering process - - BREAKING CHANGE: PdfPage.render may return null if the rendering process is canceled -- PdfPageRender.render limits render resolution up to 300-dpi unless you use getPageRenderingScale - - Even with the restriction, image size may get large and you'd better implement getPageRenderingScale to restrict such large image rendering -- PdfViewerParams default changes: - - scrollByMouseWheel default is 0.2 - - maxRealSizeImageCount default is 3 -- PdfViewerParams.scrollByArrowKey to enable keyboard navigation - -# 0.4.5 - -- PdfViewerParams updates - - PdfViewerParams.onPageChanged replaces onPageChanged parameter on PdfViewer factories - - PdfViewerParams.pageAnchor replaces anchor parameter on PdfViewer factories -- pdfDocumentFromUri/PdfFileCache improves mechanism to cache downloaded PDF file - - ETag check to invalidate the existing cache - - Better downloaded region handling - -# 0.4.4 - -- PdfPage.render can render Annotations and FORMS -- PdfFileCache: More realistic file cache mechanism -- Introduces PasswordProvider to repeatedly test passwords (only API layer) - -# 0.4.3 - -- FIXED: cache mechanism is apparently broken (#12) - -# 0.4.2 - -- PdfViewerParams.pageOverlayBuilder to customize PDF page (#17) -- Updating README.md - -# 0.4.1 - -- Add PdfViewerParams.enableRenderAnnotations to enable annotations on rendering (#18,#19) - -# 0.4.0 - -- Many breaking changes but they improve the code integrity: - - PdfDocument.pages supersedes PdfDocument.getPage - - PdfDocument.pageCount is removed - - PdfViewerParams.devicePixelRatioOverride is removed; use getPageRenderingScale instead -- Add PdfPageAnchor.all -- PdfViewerParams.viewerOverlayBuilder/PdfViewerScrollThumb to support scroll thumbs - -# 0.3.6 - -- PageLayout -> PdfPageLayout - -# 0.3.5 - -- PageLayout class change to ease page layout customization - - Add example use case in API document - -# 0.3.4 - -- Rewriting page rendering code - - Due to the internal structure change, page drawing customization parameters are once removed: - - pageDecoration - - pageOverlaysBuilder -- Example code does not enables enableTextSelection; it's still too experimental... - -# 0.3.3 - -- FIXED: Downloading of small PDF file causes internal loading error - -# 0.3.2 - -- Support mouse-wheel-to-scroll on Desktop platforms - -# 0.3.1 - -- Minor API changes -- Internal integrity updates that controls the viewer behaviors -- FIX: example code does not have android.permission.INTERNET on AndroidManifest.xml -- PdfViewerParams.devicePixelRatioOverride is deprecated and introduces PdfViewerParams.getPageRenderingScale - -# 0.3.0 - -- Many renaming of the APIs that potentially breaks existing apps - -# 0.2.4 - -- Now uses plugin_ffi. (Not containing any Flutter plugin stab) - -# 0.2.3 - -- Fixed: #6 PdfPageWeb.render behavior is different from PdfPagePdfium.render - -# 0.2.2 - -- Explicitly specify Flutter 3.16/Dart 3.2 as NativeCallable.listener does not accept non-static function (#5) - -# 0.2.1 - -- Stabilizing API surface - - Introducing PdfViewer.asset/file/uri/custom - - PdfViewer has documentLoader to accept function to load PdfDocument -- Fixes minor issues on PdfViewer - -# 0.2.0 - -- Introducing PdfDocument.openUri/PdfFileCache\* classes -- Introducing PdfPermissions -- PdfPage.loadText/PdfPageText for text extraction -- Android NDK CMake to 3.18.1 - -# 0.1.1 - -- Document updates -- pdf.js 3.11.174 - -# 0.1.0 - -- First release (Documentation is not yet ready) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..b0c347bb --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,3 @@ +# NOTE for Claude + +Read and follow the instructions in AGENTS.md. diff --git a/README.md b/README.md index d4a6b56d..72c08900 100644 --- a/README.md +++ b/README.md @@ -1,207 +1,136 @@ # pdfrx -[pdfrx](https://pub.dartlang.org/packages/pdfrx) is a rich and fast PDF viewer implementation built on the top of [PDFium](https://pdfium.googlesource.com/pdfium/). -The plugin supports Android, iOS, Windows, macOS, Linux, and Web. +This repository contains multiple Dart/Flutter packages for PDF rendering, viewing, and manipulation: -## Interactive Demo +## Packages -A [demo site](https://espresso3389.github.io/pdfrx/) using Flutter Web +### [pdfrx_engine](https://pub.dev/packages/pdfrx_engine) -![pdfrx](https://github.com/espresso3389/pdfrx/assets/1311400/b076ac0b-e2cb-48f0-8772-9891537ade7b) +A platform-agnostic PDF rendering and manipulation API built on top of [PDFium](https://pdfium.googlesource.com/pdfium/). -## Multi-platform support +- Pure Dart package (no Flutter dependencies) +- Provides low-level PDF document API for viewing and editing +- Supports page re-arrangement, PDF combining, image import, and document manipulation +- Can be used in CLI applications or non-Flutter Dart projects +- Supports all platforms: Android, iOS, Windows, macOS, Linux -- Android -- iOS -- Windows -- macOS -- Linux (even on Raspberry PI) -- Web (\*using [PDF.js](https://mozilla.github.io/pdf.js/)) or Pdfium WASM (\*experimental) +[View repo](packages/pdfrx_engine/) | [API reference](https://pub.dev/documentation/pdfrx_engine/latest/) -## Example Code +### [pdfrx](https://pub.dev/packages/pdfrx) -The following fragment illustrates the easiest way to show a PDF file in assets: +A cross-platform PDF viewer and manipulation plugin for Flutter. -```dart -import 'package:pdfrx/pdfrx.dart'; +- Flutter plugin with UI widgets +- Built on top of [pdfrx_engine](https://pub.dev/packages/pdfrx_engine) +- Provides high-level viewer widgets and overlays +- Includes text selection, search, zoom controls, and more +- Supports PDF editing features like page manipulation, document combining, and image import -... +[View repo](packages/pdfrx/) | [API reference](https://pub.dev/documentation/pdfrx/latest/) -class _MyAppState extends State { - @override - Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('Pdfrx example'), - ), - body: PdfViewer.asset('assets/hello.pdf'), - ), - ); - } -} -``` +### [pdfrx_coregraphics](https://pub.dev/packages/pdfrx_coregraphics) -Anyway, please follow the instructions below to install on your environment. +**⚠️ EXPERIMENTAL** - CoreGraphics-backed renderer for [pdfrx](https://pub.dev/packages/pdfrx) on iOS/macOS. -## Getting Started +- Uses PDFKit/CoreGraphics instead of [PDFium](https://pdfium.googlesource.com/pdfium/) on Apple platforms +- Drop-in replacement for teams preferring the system PDF stack +- Maintains full compatibility with [pdfrx](https://pub.dev/packages/pdfrx) widget API +- iOS and macOS only -### Installation +[View repo](packages/pdfrx_coregraphics/) | [API reference](https://pub.dev/documentation/pdfrx_coregraphics/latest/) -Add this to your package's `pubspec.yaml` file and execute `flutter pub get`: +### [pdfium_dart](https://pub.dev/packages/pdfium_dart) -```yaml -dependencies: - pdfrx: ^1.1.11 -``` +Low-level Dart FFI bindings for the [PDFium](https://pdfium.googlesource.com/pdfium/) library. -### Note for Windows +- Pure Dart package with auto-generated FFI bindings using [ffigen](https://pub.dev/packages/ffigen) +- Provides direct access to PDFium's C API from Dart +- Includes [getPdfium()](https://pub.dev/documentation/pdfium_dart/latest/pdfium_dart/getPdfium.html) function that downloads PDFium binaries on demand +- Used as a foundation by higher-level packages -**Ensure your Windows installation enables [Developer Mode](https://learn.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development#activate-developer-mode).** +[View repo](packages/pdfium_dart/) | [API reference](https://pub.dev/documentation/pdfium_dart/latest/) -The build process internally uses *symbolic links* and it requires Developer Mode to be enabled. -Without this, you may encounter errors [like this](https://github.com/espresso3389/pdfrx/issues/34). +### [pdfium_flutter](https://pub.dev/packages/pdfium_flutter) -### "Bleeding Edge" Pdfium WASM support on Web +Flutter FFI plugin for loading [PDFium](https://pdfium.googlesource.com/pdfium/) native libraries. -pdfrx now supports "bleeding edge" Pdfium WASM support on Web. -This is still not production-ready, but you can try it by adding additional [pdfrx_wasm](https://pub.dartlang.org/packages/pdfrx_wasm) to your dependencies: +- Bundles pre-built PDFium binaries for all Flutter platforms (Android, iOS, Windows, macOS, Linux) +- Provides utilities for loading PDFium at runtime +- Re-exports [pdfium_dart](https://pub.dev/packages/pdfium_dart) FFI bindings +- Simplifies PDFium integration in Flutter applications + +[View repo](packages/pdfium_flutter/) | [API reference](https://pub.dev/documentation/pdfium_flutter/latest/) + +## When to Use Which Package + +- **Use [pdfrx](https://pub.dev/packages/pdfrx)** if you're building a Flutter application and need PDF viewing and manipulation capabilities with UI +- **Use [pdfrx_engine](https://pub.dev/packages/pdfrx_engine)** if you need PDF rendering and manipulation without Flutter dependencies (e.g., server-side PDF processing, CLI tools, PDF combining utilities) +- **Use [pdfrx_coregraphics](https://pub.dev/packages/pdfrx_coregraphics)** (experimental) if you want to use CoreGraphics/PDFKit instead of [PDFium](https://pdfium.googlesource.com/pdfium/) on iOS/macOS +- **Use [pdfium_dart](https://pub.dev/packages/pdfium_dart)** if you need low-level PDFium FFI bindings for Dart projects or want on-demand PDFium binary downloads +- **Use [pdfium_flutter](https://pub.dev/packages/pdfium_flutter)** if you're building a Flutter plugin that needs PDFium integration with bundled binaries + +## Getting Started + +### For Flutter Applications + +Add [pdfrx](https://pub.dev/packages/pdfrx) to your `pubspec.yaml`: ```yaml dependencies: - pdfrx: ^1.1.11 - pdfrx_wasm: ^1.1.6 + pdfrx: ^2.2.15 ``` -And then, enable Pdfium WASM by adding the following line to somewhere that runs before calling any pdfrx functions (typically `main` function): +### For Pure Dart Applications -```dart -Pdfrx.webRuntimeType = PdfrxWebRuntimeType.pdfiumWasm; -``` +Add [pdfrx_engine](https://pub.dev/packages/pdfrx_engine) to your `pubspec.yaml`: -The plugin, `pdfrx_wasm`, is a satellite plugin for `pdfrx` that contains files required to run Pdfium WASM. Because the WASM binary/support files are relatively large (about 4MB), it is not included in the main `pdfrx` package and you need to add `pdfrx_wasm` to your dependencies. +```yaml +dependencies: + pdfrx_engine: ^0.3.0 +``` -## Open PDF File +## Documentation -[PdfViewer](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer-class.html) supports following functions to open PDF file on specific medium: +Comprehensive documentation is available in the [doc/](doc/) directory, including: -- [PdfViewer.asset](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer/PdfViewer.asset.html) - - Open PDF of Flutter's asset -- [PdfViewer.file](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer/PdfViewer.file.html) - - Open PDF from file - - macOS: may be blocked by [App Sandbox](https://github.com/espresso3389/pdfrx/wiki/macOS:-Deal-with-App-Sandbox) -- [PdfViewer.uri](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer/PdfViewer.uri.html) - - Open PDF from URI (`https://...` or relative path) - - Flutter Web: may be blocked by [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - - macOS: may be blocked by [App Sandbox](https://github.com/espresso3389/pdfrx/wiki/macOS:-Deal-with-App-Sandbox) +- Getting started guides +- Feature tutorials +- Platform-specific configurations +- Code examples -### Deal with Password Protected PDF Files +## Development -```dart -PdfViewer.asset( - 'assets/test.pdf', - // Most easiest way to return some password - passwordProvider: () => 'password', +This is a monorepo managed with pub workspaces. Just do `dart pub get` on some directory inside the repo to obtain all the dependencies. - ... -), -``` +## Example Applications -For more customization and considerations, see [Deal with Password Protected PDF Files using PasswordProvider](https://github.com/espresso3389/pdfrx/wiki/Deal-with-Password-Protected-PDF-Files-using-PasswordProvider). +### PDF Viewer -## Customizations/Features +The example viewer application is located in [packages/pdfrx/example/viewer/](packages/pdfrx/example/viewer/). It demonstrates the full capabilities of the [pdfrx](https://pub.dev/packages/pdfrx) Flutter plugin. -You can customize the behaviors and the viewer look and feel by configuring [PdfViewerParams](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams-class.html). +```bash +cd packages/pdfrx/example/viewer +flutter run +``` -### Text Selection +### PDF Combine -The following fragment enables text selection feature: +The [packages/pdfrx/example/pdf_combine/](packages/pdfrx/example/pdf_combine/) application demonstrates PDF page manipulation and combining features: -```dart -PdfViewer.asset( - 'assets/test.pdf', - params: PdfViewerParams( - enableTextSelection: true, - ... - ), - ... -), -``` +- Drag-and-drop interface for page re-arrangement +- Visual thumbnails of PDF pages +- Support for combining multiple PDF documents +- Platform file drag-and-drop support -For more text selection customization, see [Text Selection](https://github.com/espresso3389/pdfrx/wiki/Text-Selection). - -### PDF Feature Support - -- [PDF Link Handling](https://github.com/espresso3389/pdfrx/wiki/PDF-Link-Handling) -- [Document Outline (a.k.a Bookmarks)](https://github.com/espresso3389/pdfrx/wiki/Document-Outline-(a.k.a-Bookmarks)) -- [Text Search](https://github.com/espresso3389/pdfrx/wiki/Text-Search) - -### Viewer Customization - -- [Page Layout (Horizontal Scroll View/Facing Pages)](https://github.com/espresso3389/pdfrx/wiki/Page-Layout-Customization) -- [Showing Scroll Thumbs](https://github.com/espresso3389/pdfrx/wiki/Showing-Scroll-Thumbs) -- [Dark/Night Mode Support](https://github.com/espresso3389/pdfrx/wiki/Dark-Night-Mode-Support) -- [Document Loading Indicator](https://github.com/espresso3389/pdfrx/wiki/Document-Loading-Indicator) -- [Viewer Customization using Widget Overlay](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/viewerOverlayBuilder.html) - -### Additional Customizations - -- [Double‐tap to Zoom](https://github.com/espresso3389/pdfrx/wiki/Double%E2%80%90tap-to-Zoom) -- [Adding Page Number on Page Bottom](https://github.com/espresso3389/pdfrx/wiki/Adding-Page-Number-on-Page-Bottom) -- [Per-page Customization using Widget Overlay](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/pageOverlaysBuilder.html) -- [Per-page Customization using Canvas](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/pagePaintCallbacks.html) - -## Additional Widgets - -### PdfDocumentViewBuilder/PdfPageView - -[PdfPageView](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfPageView-class.html) is just another PDF widget that shows only one page. It accepts [PdfDocument](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocument-class.html) and page number to show a page within the document. - -[PdfDocumentViewBuilder](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocumentViewBuilder-class.html) is used to safely manage [PdfDocument](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocument-class.html) inside widget tree and it accepts `builder` parameter that creates child widgets. - -The following fragment is a typical use of these widgets: - -```dart -PdfDocumentViewBuilder.asset( - 'asset/test.pdf', - builder: (context, document) => ListView.builder( - itemCount: document?.pages.length ?? 0, - itemBuilder: (context, index) { - return Container( - margin: const EdgeInsets.all(8), - height: 240, - child: Column( - children: [ - SizedBox( - height: 220, - child: PdfPageView( - document: document, - pageNumber: index + 1, - alignment: Alignment.center, - ), - ), - Text( - '${index + 1}', - ), - ], - ), - ); - }, - ), -), +```bash +cd packages/pdfrx/example/pdf_combine +flutter run ``` -## PdfDocument Management +## Contributing -[PdfDocumentViewBuilder](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocumentViewBuilder-class.html) can accept [PdfDocumentRef](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocumentRef-class.html) from [PdfViewer](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer-class.html) to safely share the same [PdfDocument](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocument-class.html) instance. For more information, see [example/viewer/lib/thumbnails_view.dart](example/viewer/lib/thumbnails_view.dart). +Contributions are welcome! Please read the individual package READMEs for specific development guidelines. -## Low Level PDF API +## License -- Easy to use Flutter widgets - - [PdfViewer](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer-class.html) - - [PdfDocumentViewBuilder](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocumentViewBuilder-class.html) - - [PdfPageView](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfPageView-class.html) -- Easy to use PDF APIs - - [PdfDocument](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocument-class.html) -- PDFium bindings - - Not encouraged but you can import [package:pdfrx/src/pdfium/pdfium_bindings.dart](https://github.com/espresso3389/pdfrx/blob/master/lib/src/pdfium/pdfium_bindings.dart) +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/android/settings.gradle b/android/settings.gradle deleted file mode 100644 index bccb4d82..00000000 --- a/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'pdfrx' diff --git a/darwin/Classes/pdfrx.cpp b/darwin/Classes/pdfrx.cpp deleted file mode 100644 index 2e0ee1fb..00000000 --- a/darwin/Classes/pdfrx.cpp +++ /dev/null @@ -1,3 +0,0 @@ -// Relative import to be able to reuse the C sources. -// See the comment in ../{projectName}}.podspec for more information. -#include "../../src/pdfium_interop.cpp" diff --git a/darwin/pdfium/.gitignore b/darwin/pdfium/.gitignore deleted file mode 100644 index 58c18afd..00000000 --- a/darwin/pdfium/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.lib/ -.tmp/ -ios/ -macos/ -*.tgz diff --git a/darwin/pdfium/build b/darwin/pdfium/build deleted file mode 100755 index 1aa3940d..00000000 --- a/darwin/pdfium/build +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/zsh -e - -SCRIPT_DIR=$(cd $(dirname $0) && pwd) -cd $SCRIPT_DIR - -# for iOS/iPhoneSimulator -if [[ ! -d ios/pdfium.xcframework ]]; then - ./build-config.sh ios arm64 - ./build-config.sh iossim arm64 - ./build-config.sh iossim x64 - - mkdir -p .tmp/out/lib/iossim-release - lipo -create .tmp/out/lib/iossim-arm64-release/libpdfium.a .tmp/out/lib/iossim-x64-release/libpdfium.a -output .tmp/out/lib/iossim-release/libpdfium.a - - mkdir -p ios/ - xcodebuild -create-xcframework -library .tmp/out/lib/ios-arm64-release/libpdfium.a -headers .tmp/out/include -library .tmp/out/lib/iossim-release/libpdfium.a -headers .tmp/out/include -output ios/pdfium.xcframework - - tar -czvf pdfium-ios.tgz ios -fi - -# for macOS -if [[ ! -d macos/pdfium.xcframework ]]; then - ./build-config.sh macos arm64 - ./build-config.sh macos x64 - - mkdir -p .tmp/out/lib/macos-release - lipo -create .tmp/out/lib/macos-arm64-release/libpdfium.a .tmp/out/lib/macos-x64-release/libpdfium.a -output .tmp/out/lib/macos-release/libpdfium.a - - mkdir -p macos/ - rm -rf macos/pdfium.xcframework - xcodebuild -create-xcframework -library .tmp/out/lib/macos-release/libpdfium.a -headers .tmp/out/include -output macos/pdfium.xcframework - - tar -czvf pdfium-macos.tgz macos -fi diff --git a/darwin/pdfium/build-config.sh b/darwin/pdfium/build-config.sh deleted file mode 100755 index df1f4016..00000000 --- a/darwin/pdfium/build-config.sh +++ /dev/null @@ -1,157 +0,0 @@ -#!/bin/zsh -e - -if [ "$2" = "" ]; then - echo "Usage: $0 linux|android|macos|ios|iossim x86|x64|arm|arm64" - exit 1 -fi - -# https://pdfium.googlesource.com/pdfium/+/refs/heads/chromium/6555 -LAST_KNOWN_GOOD_COMMIT=5a6a8741b0e111a6b5bd9ab4e1036377c98885dc - -SCRIPT_DIR=$(cd $(dirname $0) && pwd) - -# linux, android, macos, ios -TARGET_OS_ORIG=$1 -# x64, x86, arm64, ... -TARGET_ARCH=$2 -# static or dll -STATIC_OR_DLL=static -# release or debug -REL_OR_DBG=release - - -if [[ "$TARGET_OS_ORIG" == "iossim" ]]; then - TARGET_OS=ios - TARGET_ENVIRONMENT=simulator -elif [[ "$TARGET_OS_ORIG" == "macos" ]]; then - TARGET_OS=mac -else - TARGET_OS=$TARGET_OS_ORIG - # only for ios; simulator or device - TARGET_ENVIRONMENT=device -fi - - -WORK_ROOT_DIR=$SCRIPT_DIR/.tmp -DIST_DIR=$WORK_ROOT_DIR/out - -DEPOT_DIR=$WORK_ROOT_DIR/depot_tools -WORK_DIR=$WORK_ROOT_DIR/work - -mkdir -p $WORK_ROOT_DIR $WORK_DIR $DIST_DIR - -if [[ ! -d $DEPOT_DIR ]]; then - pushd $WORK_ROOT_DIR - git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git - popd -fi - -export PATH=$DEPOT_DIR:$PATH - -IS_SHAREDLIB=false - -if [[ "$REL_OR_DBG" = "release" ]]; then - IS_DEBUG=false - DEBUG_DIR_SUFFIX= -else - IS_DEBUG=true - DEBUG_DIR_SUFFIX=/debug -fi - -if [[ "$TARGET_OS" == "macos" || "$TARGET_OS" == "ios" || "$TARGET_OS" == "android" ]]; then - IS_CLANG=true -else - IS_CLANG=false -fi - -cd $WORK_DIR -if [[ ! -e pdfium/.git/index ]]; then - fetch pdfium -fi - -PDFIUM_SRCDIR=$WORK_DIR/pdfium -BUILDDIR=$PDFIUM_SRCDIR/out/$TARGET_OS_ORIG-$TARGET_ARCH-$REL_OR_DBG -mkdir -p $BUILDDIR - -if [[ "$LAST_KNOWN_GOOD_COMMIT" != "" ]]; then - pushd $PDFIUM_SRCDIR - git reset --hard - git checkout $LAST_KNOWN_GOOD_COMMIT - cd $PDFIUM_SRCDIR/build - git reset --hard - cd $PDFIUM_SRCDIR/third_party/libjpeg_turbo - git reset --hard - popd -fi - -INCLUDE_DIR=$DIST_DIR/include -if [[ ! -d $INCLUDE_DIR ]]; then - mkdir -p $INCLUDE_DIR - cp -r $PDFIUM_SRCDIR/public/* $INCLUDE_DIR -fi - -if [[ "$TARGET_OS" == "ios" ]]; then - # (cd $PDFIUM_SRCDIR && git diff > ../../../patches/ios/pdfium.patch) - pushd $PDFIUM_SRCDIR - git apply --reject --whitespace=fix $SCRIPT_DIR/patches/ios/pdfium.patch - popd - # (cd $PDFIUM_SRCDIR/third_party/libjpeg_turbo && git diff > ../../../../../patches/ios/libjpeg_turbo.patch) - pushd $PDFIUM_SRCDIR/third_party/libjpeg_turbo/ - git apply --reject --whitespace=fix $SCRIPT_DIR/patches/ios/libjpeg_turbo.patch - popd -fi - -if [[ "$TARGET_OS" == "mac" ]]; then - # (cd $PDFIUM_SRCDIR && git diff > ../../../patches/macos/pdfium.patch) - pushd $PDFIUM_SRCDIR - git apply --reject --whitespace=fix $SCRIPT_DIR/patches/macos/pdfium.patch - popd - # (cd $PDFIUM_SRCDIR/build && git diff > ../../../../patches/macos/build-config.patch) - pushd $PDFIUM_SRCDIR/build - git apply --reject --whitespace=fix $SCRIPT_DIR/patches/macos/build-config.patch - popd -fi - -cat < $BUILDDIR/args.gn -is_clang = $IS_CLANG -target_os = "$TARGET_OS" -target_cpu = "$TARGET_ARCH" -pdf_is_complete_lib = true -pdf_is_standalone = true -is_component_build = $IS_SHAREDLIB -is_debug = $IS_DEBUG -enable_iterator_debugging = $IS_DEBUG -pdf_enable_xfa = false -pdf_enable_v8 = false -EOF - -if [[ "$TARGET_OS" == "ios" ]]; then - # See ios/pdfium/.tmp/work/pdfium/build/config/ios/rules.gni - cat <> $BUILDDIR/args.gn -ios_enable_code_signing = false -ios_deployment_target = "12.0" -use_custom_libcxx = false -pdf_use_partition_alloc = false -target_environment = "$TARGET_ENVIRONMENT" -EOF -fi - -if [[ "$TARGET_OS" == "mac" ]]; then - cat <> $BUILDDIR/args.gn -use_custom_libcxx = false -pdf_use_partition_alloc = false -EOF -fi - -pushd $BUILDDIR -gn gen . -popd - -ninja -C $BUILDDIR pdfium - -LIB_DIR=$DIST_DIR/lib/$TARGET_OS_ORIG-$TARGET_ARCH-$REL_OR_DBG -rm -rf $LIB_DIR -mkdir -p $LIB_DIR -cp $BUILDDIR/obj/libpdfium.a $LIB_DIR - -cd $SCRIPT_DIR diff --git a/darwin/pdfium/patches/ios/libjpeg_turbo.patch b/darwin/pdfium/patches/ios/libjpeg_turbo.patch deleted file mode 100644 index 5f633124..00000000 --- a/darwin/pdfium/patches/ios/libjpeg_turbo.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/BUILD.gn b/BUILD.gn -index b39d278..596ea7a 100644 ---- a/BUILD.gn -+++ b/BUILD.gn -@@ -12,7 +12,7 @@ if (current_cpu == "arm" || current_cpu == "arm64") { - } - - assert( -- use_blink, -+ true, - "This is not used if blink is not enabled, don't drag it in unintentionally") - - source_set("libjpeg_headers") { diff --git a/darwin/pdfium/patches/ios/pdfium.patch b/darwin/pdfium/patches/ios/pdfium.patch deleted file mode 100644 index 0071669a..00000000 --- a/darwin/pdfium/patches/ios/pdfium.patch +++ /dev/null @@ -1,39 +0,0 @@ -diff --git a/core/fxge/BUILD.gn b/core/fxge/BUILD.gn -index 215a63b14..357d96735 100644 ---- a/core/fxge/BUILD.gn -+++ b/core/fxge/BUILD.gn -@@ -164,7 +164,7 @@ source_set("fxge") { - sources += [ "linux/fx_linux_impl.cpp" ] - } - -- if (is_mac) { -+ if (is_mac || is_ios) { - sources += [ - "apple/fx_apple_impl.cpp", - "apple/fx_apple_platform.cpp", -diff --git a/public/fpdfview.h b/public/fpdfview.h -index b374088b4..b1c896e26 100644 ---- a/public/fpdfview.h -+++ b/public/fpdfview.h -@@ -214,7 +214,7 @@ typedef int FPDF_OBJECT_TYPE; - #endif // defined(FPDF_IMPLEMENTATION) - #else - #if defined(FPDF_IMPLEMENTATION) --#define FPDF_EXPORT __attribute__((visibility("default"))) -+#define FPDF_EXPORT __attribute__((visibility("default"))) __attribute__((used)) - #else - #define FPDF_EXPORT - #endif // defined(FPDF_IMPLEMENTATION) -diff --git a/testing/test.gni b/testing/test.gni -index 6ad2c2d4a..bae5117d8 100644 ---- a/testing/test.gni -+++ b/testing/test.gni -@@ -207,7 +207,7 @@ template("test") { - } - - _bundle_id_suffix = target_name -- if (ios_automatically_manage_certs) { -+ if (true) { - # Use the same bundle identifier for all unit tests when managing - # certificates automatically as the number of free certs is limited. - _bundle_id_suffix = "generic-unit-test" diff --git a/darwin/pdfium/patches/macos/build-config.patch b/darwin/pdfium/patches/macos/build-config.patch deleted file mode 100644 index 7cd856db..00000000 --- a/darwin/pdfium/patches/macos/build-config.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/config/mac/BUILD.gn b/config/mac/BUILD.gn -index 85a668d33..54691a2e0 100644 ---- a/config/mac/BUILD.gn -+++ b/config/mac/BUILD.gn -@@ -52,6 +52,8 @@ config("compiler") { - if (export_libcxxabi_from_executables) { - ldflags += [ "-Wl,-undefined,dynamic_lookup" ] - } -+ -+ cflags += [ "-Wno-unknown-warning-option" ] - } - - # This is included by reference in the //build/config/compiler:runtime_library diff --git a/darwin/pdfium/patches/macos/pdfium.patch b/darwin/pdfium/patches/macos/pdfium.patch deleted file mode 100644 index ef254320..00000000 --- a/darwin/pdfium/patches/macos/pdfium.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/public/fpdfview.h b/public/fpdfview.h -index b374088b4..b1c896e26 100644 ---- a/public/fpdfview.h -+++ b/public/fpdfview.h -@@ -214,7 +214,7 @@ typedef int FPDF_OBJECT_TYPE; - #endif // defined(FPDF_IMPLEMENTATION) - #else - #if defined(FPDF_IMPLEMENTATION) --#define FPDF_EXPORT __attribute__((visibility("default"))) -+#define FPDF_EXPORT __attribute__((visibility("default"))) __attribute__((used)) - #else - #define FPDF_EXPORT - #endif // defined(FPDF_IMPLEMENTATION) diff --git a/darwin/pdfrx.podspec b/darwin/pdfrx.podspec deleted file mode 100644 index ba08aa0f..00000000 --- a/darwin/pdfrx.podspec +++ /dev/null @@ -1,54 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint pdfrx.podspec` to validate before publishing. -# -lib_tag = 'pdfium-apple-v9' - -Pod::Spec.new do |s| - s.name = 'pdfrx' - s.version = '0.0.3' - s.summary = 'Yet another PDF renderer for Flutter using PDFium.' - s.description = <<-DESC - Yet another PDF renderer for Flutter using PDFium. - DESC - s.homepage = 'https://github.com/espresso3389/pdfrx' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Takashi Kawasaki' => 'espresso3389@gmail.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - - s.ios.deployment_target = '12.0' - s.ios.dependency 'Flutter' - s.ios.private_header_files = "pdfium/.lib/#{lib_tag}/ios/pdfium.xcframework/ios-arm64/Headers/*.h" - s.ios.vendored_frameworks = "pdfium/.lib/#{lib_tag}/ios/pdfium.xcframework" - # Flutter.framework does not contain a i386 slice. - s.ios.pod_target_xcconfig = { - 'DEFINES_MODULE' => 'YES', - 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', - } - - s.osx.deployment_target = '10.11' - s.osx.dependency 'FlutterMacOS' - s.osx.private_header_files = "pdfium/.lib/#{lib_tag}/macos/pdfium.xcframework/macos-arm64_x86_64/Headers/*.h" - s.osx.vendored_frameworks = "pdfium/.lib/#{lib_tag}/macos/pdfium.xcframework" - s.osx.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } - - s.prepare_command = <<-CMD - mkdir -p pdfium/.lib/#{lib_tag} - cd pdfium/.lib/#{lib_tag} - if [ ! -f ios.tgz ]; then - curl -Lo ios.tgz https://github.com/espresso3389/pdfrx/releases/download/#{lib_tag}/pdfium-ios.tgz - fi - if [ ! -d ios ]; then - tar xzf ios.tgz - fi - if [ ! -f macos.tgz ]; then - curl -Lo macos.tgz https://github.com/espresso3389/pdfrx/releases/download/#{lib_tag}/pdfium-macos.tgz - fi - if [ ! -d macos ]; then - tar xzf macos.tgz - fi - CMD - - s.swift_version = '5.0' -end diff --git a/doc/Adding-Page-Number-on-Page-Bottom.md b/doc/Adding-Page-Number-on-Page-Bottom.md new file mode 100644 index 00000000..12e1ecb7 --- /dev/null +++ b/doc/Adding-Page-Number-on-Page-Bottom.md @@ -0,0 +1,17 @@ +# Adding Page Numbers to Pages + +If you want to add page number on each page, you can do that by [PdfViewerParams.pageOverlaysBuilder](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/pageOverlaysBuilder.html): + +```dart +pageOverlaysBuilder: (context, pageRect, page) { + return [ + Align( + alignment: Alignment.bottomCenter, + child: Text( + page.pageNumber.toString(), + style: const TextStyle(color: Colors.red), + ), + ), + ]; +}, +``` diff --git a/doc/Coordinate-Conversion.md b/doc/Coordinate-Conversion.md new file mode 100644 index 00000000..8877c722 --- /dev/null +++ b/doc/Coordinate-Conversion.md @@ -0,0 +1,384 @@ +# Coordinate Conversion + +This guide explains how to work with different coordinate systems in pdfrx and convert between them. + +## Understanding Coordinate Systems + +pdfrx uses four distinct coordinate systems: + +1. **Global Coordinates** - Screen/window coordinates (origin at top-left of the screen) +2. **Local Coordinates** - Widget's local coordinate system (origin at top-left of the widget) +3. **Document Coordinates** - PDF document layout coordinates (72 DPI, unzoomed, with pages laid out according to the layout mode) +4. **Page Coordinates** - Individual PDF page coordinates (origin at bottom-left corner, following PDF standard) + +### Coordinate System Diagram + +``` +Global (Screen) Local (Widget) Document (Layout) Page (PDF) +┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ +│ (0,0) │ │ (0,0) │ │ (0,0) │ │ │ +│ │ │ │ │ ┌────┐ │ │ │ +│ ┌──────┐ │ │ Content │ │ │Page│ │ │ (0,h) │ +│ │Widget│ │ → │ │ → │ └────┘ │ → │ │ +│ └──────┘ │ │ │ │ ┌────┐ │ └─────────────┘ +│ │ │ │ │ │Page│ │ (0,0) (w,0) +└─────────────┘ └─────────────┘ └──┴────┴─────┘ +``` + +## Converting Between Coordinate Systems + +### Widget Local ↔ Global Coordinates + +Use [PdfViewerController.globalToLocal](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerController/globalToLocal.html) and [PdfViewerController.localToGlobal](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerController/localToGlobal.html): + +```dart +final controller = PdfViewerController(); +... + +// Global to local (e.g., from GestureDetector.onTapDown) +final globalPos = details.globalPosition; +final localPos = controller.globalToLocal(globalPos); + +// Local to global +final localPos = Offset(100, 200); +final globalPos = controller.localToGlobal(localPos); +``` + +### Global ↔ Document Coordinates + +Use [PdfViewerController.globalToDocument](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerController/globalToDocument.html) and [PdfViewerController.documentToGlobal](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerController/documentToGlobal.html): + +```dart +// Global to document (accounts for zoom and pan) +final globalPos = details.globalPosition; +final docPos = controller.globalToDocument(globalPos); + +// Document to global +final docPos = Offset(100, 200); +final globalPos = controller.documentToGlobal(docPos); +``` + +### Widget Local ↔ Document Coordinates + +Use [PdfViewerController.localToDocument](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerController/localToDocument.html) and [PdfViewerController.documentToLocal](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerController/documentToLocal.html): + +```dart +// Local to document +final localPos = Offset(100, 200); +final docPos = controller.localToDocument(localPos); + +// Document to local +final docPos = Offset(100, 200); +final localPos = controller.documentToLocal(docPos); +``` + +### Document ↔ Page Coordinates + +Convert between document layout coordinates and individual page coordinates: + +```dart +import 'package:pdfrx/pdfrx.dart'; + +// Get the page layout rectangles +final pageLayouts = controller.layout.pageLayouts; +final pageRect = pageLayouts[pageIndex]; // Document coordinates + +// Document position to page position +final docPos = Offset(150, 300); +if (pageRect.contains(docPos)) { + // Offset within the page (top-left origin) + final pageOffset = docPos - pageRect.topLeft; + + // Convert to PDF page coordinates (bottom-left origin) + // IMPORTANT: This automatically handles page rotation + final pdfPoint = pageOffset.toPdfPoint( + page: page, + scaledPageSize: pageRect.size, + ); +} + +// Page coordinates to document position +final pdfPoint = PdfPoint(100, 200); // PDF coordinates (bottom-left origin) +// IMPORTANT: This automatically handles page rotation +final pageOffset = pdfPoint.toOffset( + page: page, + scaledPageSize: pageRect.size, +); // Top-left origin +final docPos = pageOffset.translate(pageRect.left, pageRect.top); +``` + +**Important Note about Page Rotation**: PDF pages can have rotation metadata (0°, 90°, 180°, or 270°). When converting between document and page coordinates, the conversion methods automatically account for the page's rotation. The coordinate system is rotated so that (0,0) in page coordinates always represents the bottom-left corner of the page as it appears when rendered (after rotation is applied). + +## Common Use Cases + +### Example 1: Finding Which Page Was Tapped + +```dart +PdfViewerController controller; + +void onTapDown(TapDownDetails details) { + // Convert global position to document coordinates + final docPos = controller.globalToDocument(details.globalPosition); + if (docPos == null) return; + + // Find which page contains this position + final pageLayouts = controller.layout.pageLayouts; + final pageIndex = pageLayouts.indexWhere((rect) => rect.contains(docPos)); + + if (pageIndex >= 0) { + final pageNumber = pageIndex + 1; + print('Tapped on page $pageNumber'); + + // Get position within the page + final pageRect = pageLayouts[pageIndex]; + final offsetInPage = docPos - pageRect.topLeft; + print('Position in page: $offsetInPage'); + } +} +``` + +### Example 2: Highlighting a Specific PDF Rectangle + +```dart +Widget buildPageOverlay(BuildContext context, Rect pageRect, PdfPage page) { + // Rectangle in PDF page coordinates (bottom-left origin) + final pdfRect = PdfRect( + left: 100, + top: 200, + right: 300, + bottom: 100, + ); + + // Convert to document coordinates (top-left origin, scaled) + final rect = pdfRect.toRect( + page: page, + scaledPageSize: pageRect.size, + ); + + // Position it within the page overlay + return Positioned( + left: rect.left, + top: rect.top, + width: rect.width, + height: rect.height, + child: Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.yellow, width: 2), + ), + ), + ); +} +``` + +### Example 3: Converting Search Results to Overlay Positions + +```dart +// Search for text in the PDF +final matches = await controller.document.pages[pageNumber - 1].loadText(); +final searchResults = matches.searchText('keyword'); + +// Build overlays for search results +Widget buildSearchHighlights(BuildContext context, Rect pageRect, PdfPage page) { + return Stack( + children: searchResults.map((result) { + // result.bounds is in PDF page coordinates + final rect = result.bounds.toRect( + page: page, + scaledPageSize: pageRect.size, + ); + + return Positioned( + left: rect.left, + top: rect.top, + width: rect.width, + height: rect.height, + child: Container( + color: Colors.yellow.withOpacity(0.3), + ), + ); + }).toList(), + ); +} +``` + +### Example 4: Getting Visible Document Area + +```dart +// Get the currently visible area in document coordinates +final visibleRect = controller.visibleRect; + +// Check if a specific position is visible +final docPos = Offset(100, 200); +final isVisible = visibleRect.contains(docPos); + +// Scroll to make a position visible +final targetPos = Offset(500, 1000); +if (!visibleRect.contains(targetPos)) { + controller.goToPosition(targetPos); +} +``` + +## Understanding Zoom and Scale + +When working with coordinates, it's important to understand how zoom affects positioning: + +- **Document coordinates** are always at 72 DPI (unzoomed), regardless of the current zoom level +- **Page overlay coordinates** must be scaled by the current zoom factor when positioning widgets + +```dart +// In pageOverlaysBuilder +Widget buildOverlay(BuildContext context, Rect pageRect, PdfPage page) { + // pageRect is already zoomed (in widget coordinates) + // To position something at a specific document coordinate: + final docOffset = Offset(100, 200); // In document coordinates + + // Method 1: Use documentToLocal (recommended) + final localOffset = controller.documentToLocal( + docOffset.translate(pageRect.left, pageRect.top) + ); + + // Method 2: Manual scaling + final zoom = controller.currentZoom; + final scaledOffset = docOffset * zoom; + + return Positioned( + left: scaledOffset.dx, + top: scaledOffset.dy, + child: YourWidget(), + ); +} +``` + +## Important Notes + +### Y-Axis Orientation + +- **Flutter/Widget coordinates**: Y increases downward (top to bottom) +- **PDF page coordinates**: Y increases upward (bottom to top) + +The conversion extensions ([PdfPoint.toOffset](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfPointExt/toOffset.html) and [Offset.toPdfPoint](https://pub.dev/documentation/pdfrx/latest/pdfrx/OffsetPdfPointExt/toPdfPoint.html)) handle this Y-axis flipping automatically. + +### Page Rotation + +PDF pages can have rotation metadata (0°, 90°, 180°, or 270°) that affects how the page is displayed. This rotation is stored in the PDF file and accessible via [PdfPage.rotation](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfPage/rotation.html). + +#### How Rotation Affects Coordinates + +When a page is rotated: + +- The page's width and height are **swapped** for 90° and 270° rotations +- The coordinate system is **rotated** so that (0,0) in page coordinates represents the bottom-left corner of the page **as it appears after rotation** +- In-page offsets must account for this rotation when converting to/from PDF page coordinates + +#### Automatic Rotation Handling + +The conversion methods automatically account for page rotation when you use the extension methods: + +```dart +// Automatic rotation handling (uses page.rotation) +final pdfPoint = PdfPoint(100, 200); +final offset = pdfPoint.toOffset(page: page); // Rotation applied automatically + +// Converting back also handles rotation +final pageOffset = Offset(50, 100); +final pdfPoint = pageOffset.toPdfPoint(page: page); // Rotation reversed automatically +``` + +#### Manual Rotation Override + +You can override the rotation if needed: + +```dart +// Force a specific rotation (useful for preview or testing) +final offset = pdfPoint.toOffset(page: page, rotation: 90); +final pdfPoint = pageOffset.toPdfPoint(page: page, rotation: 0); +``` + +#### Example: Handling Rotated Pages + +```dart +void highlightAreaOnRotatedPage(PdfPage page, PdfRect area) { + // Get page layout in document + final pageRect = controller.layout.pageLayouts[page.pageNumber - 1]; + + // Check the page rotation + print('Page rotation: ${page.rotation.index * 90}°'); // 0, 90, 180, or 270 + + // Convert PDF rect to document coordinates (rotation handled automatically) + final rect = area.toRect( + page: page, + scaledPageSize: pageRect.size, + ); // rotation is automatically applied + + // For 90° or 270° rotations, note that width and height are swapped + if (page.rotation.index == 1 || page.rotation.index == 3) { + print('Page is rotated 90° or 270° - dimensions are swapped'); + print('Original page size: ${page.width} x ${page.height}'); + print('Rendered page size: ${pageRect.width} x ${pageRect.height}'); + } +} +``` + +**Key Points**: + +- Always use the conversion extension methods ([PdfPoint.toOffset](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfPointExt/toOffset.html), [Offset.toPdfPoint](https://pub.dev/documentation/pdfrx/latest/pdfrx/OffsetPdfPointExt/toPdfPoint.html), [PdfRect.toRect](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfRectExt/toRect.html)) instead of manual calculations +- These methods handle rotation, Y-axis flipping, and scaling automatically +- The page layout rectangle (`pageRect`) already reflects the rotated dimensions +- In-page offsets calculated by subtracting `pageRect.topLeft` are relative to the **rotated** page as displayed + +### Null Safety + +Some conversion methods return nullable values because they may fail if: + +- The widget is not yet rendered (`globalToLocal`, `localToGlobal`) +- The position is outside the document bounds + +Always check for null: + +```dart +final docPos = controller.globalToDocument(globalPos); +if (docPos == null) { + // Widget not ready or position invalid + return; +} +// Proceed with docPos +``` + +## Coordinate Converter Interface + +For advanced use cases where you need to perform conversions from within custom builders, use [PdfViewerCoordinateConverter](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerCoordinateConverter-class.html): + +```dart +// Available in viewerOverlayBuilder and pageOverlaysBuilder +final converter = controller.doc2local; + +// Convert with BuildContext +final localPos = converter.offsetToLocal(context, docPos); +final localRect = converter.rectToLocal(context, docRect); +``` + +## API Reference + +### PdfViewerController Methods + +- [globalToLocal](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerController/globalToLocal.html) - Global → Local +- [localToGlobal](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerController/localToGlobal.html) - Local → Global +- [globalToDocument](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerController/globalToDocument.html) - Global → Document +- [documentToGlobal](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerController/documentToGlobal.html) - Document → Global +- [localToDocument](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerController/localToDocument.html) - Local → Document +- [documentToLocal](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerController/documentToLocal.html) - Document → Local +- [currentZoom](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerController/currentZoom.html) - Get current zoom factor +- [visibleRect](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerController/visibleRect.html) - Get visible area in document coordinates + +### Extension Methods + +- [PdfPoint.toOffset](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfPointExt/toOffset.html) - PDF page → Document offset +- [Offset.toPdfPoint](https://pub.dev/documentation/pdfrx/latest/pdfrx/OffsetPdfPointExt/toPdfPoint.html) - Document offset → PDF page +- [PdfRect.toRect](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfRectExt/toRect.html) - PDF page rect → Document rect +- [PdfRect.toRectInDocument](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfRectExt/toRectInDocument.html) - PDF page rect → Full document position + +## See Also + +- [Deal with viewerOverlayBuilder and pageOverlaysBuilder](Deal-with-viewerOverlayBuilder-and-pageOverlaysBuilder.md) - Practical example using coordinate conversion +- [Text Selection](Text-Selection.md) - Uses coordinate conversion for selection rectangles +- [PDF Link Handling](PDF-Link-Handling.md) - Uses coordinate conversion for link areas diff --git a/doc/Customizing-Key-Handling-on-PdfViewer.md b/doc/Customizing-Key-Handling-on-PdfViewer.md new file mode 100644 index 00000000..0413c193 --- /dev/null +++ b/doc/Customizing-Key-Handling-on-PdfViewer.md @@ -0,0 +1,45 @@ +# Customizing Key Handling on PdfViewer + +Because [PdfViewer](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer-class.html) handles certain keys to allow users to scroll/zoom PDF view by keyboards, it may sometimes interfere with other widget's key handling, such as [TextField](https://api.flutter.dev/flutter/material/TextField-class.html)'s text input. + +To customize the key handling behavior, you can use [PdfViewerParams.onKey](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/onKey.html) and [PdfViewerParams.keyHandlerParams](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/keyHandlerParams.html). + +## Default implementation + +By default, [PdfViewer](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer-class.html) handles the following keys: + +Key | Description +------------|---------------------- +[pageUp](https://api.flutter.dev/flutter/services/LogicalKeyboardKey/pageUp-constant.html) | Scroll up +[pageDown](https://api.flutter.dev/flutter/services/LogicalKeyboardKey/pageDown-constant.html) | Scroll up +[home](https://api.flutter.dev/flutter/services/LogicalKeyboardKey/home-constant.html) | Go to first page +[end](https://api.flutter.dev/flutter/services/LogicalKeyboardKey/end-constant.html) | go to last page +[equal](https://api.flutter.dev/flutter/services/LogicalKeyboardKey/equal-constant.html) | Combination with `⌘`/`Ctrl` to zoom up (**currently not I18N-ed**) +[minus](https://api.flutter.dev/flutter/services/LogicalKeyboardKey/equal-constant.html) | Combination with `⌘`/`Ctrl` to zoom down +[arrowUp](https://api.flutter.dev/flutter/services/LogicalKeyboardKey/arrowUp-constant.html) | Scroll upward +[arrowDown](https://api.flutter.dev/flutter/services/LogicalKeyboardKey/arrowDown-constant.html) | Scroll downward +[arrowLeft](https://api.flutter.dev/flutter/services/LogicalKeyboardKey/arrowLeft-constant.html) | Scroll to left +[arrowRight](https://api.flutter.dev/flutter/services/LogicalKeyboardKey/arrowRight-constant.html) | Scroll to right + +And, the other keys are **not handled** and handled by other widgets. + +## Overriding the default implementation + +The following fragment illustrates how to use [PdfViewerParams.onKey](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/onKey.html): + +```dart +onKey: (params, key, isRealKeyPress) { + if (key == LogicalKeyboardKey.space) { + // handling the key inside the function + handleSpace(); + return true; + } + if (key == LogicalKeyboardKey.arrowLeft || key == LogicalKeyboardKey.arrowRight) { + // returning false to disable the default logic + return false; + } + // returning null to let the default logic to handle the keys + return null; +}, +``` + diff --git a/doc/Dark-Night-Mode-Support.md b/doc/Dark-Night-Mode-Support.md new file mode 100644 index 00000000..889561f6 --- /dev/null +++ b/doc/Dark-Night-Mode-Support.md @@ -0,0 +1,12 @@ +# Dark/Night Mode Support + +[PdfViewer](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer-class.html) does not have any native dark (or night) mode support but it can be easily implemented using [ColorFiltered](https://api.flutter.dev/flutter/widgets/ColorFiltered-class.html) widget: + +```dart +ColorFiltered( + colorFilter: ColorFilter.mode(Colors.white, darkMode ? BlendMode.difference : BlendMode.dst), + child: PdfViewer.file(filePath, ...), +), +``` + +The trick is originally introduced by [pckimlong](https://github.com/pckimlong). \ No newline at end of file diff --git a/doc/Deal-with-Password-Protected-PDF-Files-using-PasswordProvider.md b/doc/Deal-with-Password-Protected-PDF-Files-using-PasswordProvider.md new file mode 100644 index 00000000..6e855fd4 --- /dev/null +++ b/doc/Deal-with-Password-Protected-PDF-Files-using-PasswordProvider.md @@ -0,0 +1,51 @@ +# Handling Password-Protected PDF Files + +To support password protected PDF files, use [passwordProvider](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfPasswordProvider.html) to supply passwords interactively: + +```dart +PdfViewer.asset( + 'assets/test.pdf', + // Most easiest way to return some password + passwordProvider: _showPasswordDialog, + + ... +), + +... + +Future _showPasswordDialog() async { + final textController = TextEditingController(); + return await showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + return AlertDialog( + title: const Text('Enter password'), + content: TextField( + controller: textController, + autofocus: true, + keyboardType: TextInputType.visiblePassword, + obscureText: true, + onSubmitted: (value) => Navigator.of(context).pop(value), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(null), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(textController.text), + child: const Text('OK'), + ), + ], + ); + }, + ); +} +``` + +When [PdfViewer](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer-class.html) tries to open a password protected document, it calls the function passed to `passwordProvider` (except the first attempt; see [below](#firstattemptbyemptypassword)) repeatedly to get a new password until the document is successfully opened. And if the function returns null, the viewer will give up the password trials and the function is no longer called. + +### `firstAttemptByEmptyPassword` + +By default, the first password attempt uses empty password. This is because encrypted PDF files frequently use empty password for viewing purpose. It's _normally_ useful but if you want to use authoring password, it can be disabled by setting `firstAttemptByEmptyPassword` to false. \ No newline at end of file diff --git a/doc/Deal-with-viewerOverlayBuilder-and-pageOverlaysBuilder.md b/doc/Deal-with-viewerOverlayBuilder-and-pageOverlaysBuilder.md new file mode 100644 index 00000000..d50032c7 --- /dev/null +++ b/doc/Deal-with-viewerOverlayBuilder-and-pageOverlaysBuilder.md @@ -0,0 +1,56 @@ +# Working with Viewer Overlays and Page Overlays + +The following fragment shows red-dot on user tapped location: + +```dart +int? _pageNumber; +Offset? _offsetInPage; + +... + +viewerOverlayBuilder: (context, size, handleLinkTap) => [ + Positioned.fill(child: GestureDetector( + onTapDown: (details) { + // global position -> in-document position + final posInDoc = controller.globalToDocument(details.globalPosition); + if (posInDoc == null) return; + // determine which page contains the point (position) + final pageIndex = controller.layout.pageLayouts.indexWhere((pageRect) => pageRect.contains(posInDoc)); + if (pageIndex < 0) return; + // in-document position -> in-page offset + _offsetInPage = posInDoc - controller.layout.pageLayouts[pageIndex].topLeft; + _pageNumber = pageIndex + 1; + + // NOTE: you're hosting PdfViewer inside some StatefulWidget + // or inside StatefulBuilder + setState(() {}); + }, + )), +], +pageOverlaysBuilder: (context, pageRect, page) { + return [ + if (_pageNumber == page.pageNumber && _offsetInPage != null) + Positioned( + left: _offsetInPage!.dx * controller.currentZoom, // position should be zoomed + top: _offsetInPage!.dy * controller.currentZoom, + child: Container( + width: 10, + height: 10, + color: Colors.red, + ), + ), + ]; +}, +``` + +On [PdfViewerParams.viewerOverlayBuilder](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/viewerOverlayBuilder.html); + +- Convert the global tap position to in-document position using [PdfViewerController.globalToDocument](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerController/globalToDocument.html) + - The in-document position is position in document structure (i.e., page layout in 72-dpi). +- Determine which page contains the position using [PdfViewerController.layout.pageLayouts](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfPageLayout/pageLayouts.html) +- Convert the in-document position to the in-page position (just subtract the page's top-left position) + +On [PdfViewerParams.pageOverlaysBuilder](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/pageOverlaysBuilder.html), + +- `pageRect` is the zoomed page rectangle inside the view +- To correctly locate the position, it must be zoomed (by [PdfViewerController.currentZoom](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerController/currentZoom.html)) diff --git a/doc/Document-Loading-Indicator.md b/doc/Document-Loading-Indicator.md new file mode 100644 index 00000000..2065a014 --- /dev/null +++ b/doc/Document-Loading-Indicator.md @@ -0,0 +1,15 @@ +# Document Loading Indicator + +[PdfViewer.uri](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer/PdfViewer.uri.html) may take long time to download PDF file and you want to show some loading indicator. You can do that by [PdfViewerParams.loadingBannerBuilder](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/loadingBannerBuilder.html): + +```dart +loadingBannerBuilder: (context, bytesDownloaded, totalBytes) { + return Center( + child: CircularProgressIndicator( + // totalBytes may not be available on certain case + value: totalBytes != null ? bytesDownloaded / totalBytes : null, + backgroundColor: Colors.grey, + ), + ); +} +``` \ No newline at end of file diff --git a/doc/Document-Outline-(a.k.a-Bookmarks).md b/doc/Document-Outline-(a.k.a-Bookmarks).md new file mode 100644 index 00000000..3b915208 --- /dev/null +++ b/doc/Document-Outline-(a.k.a-Bookmarks).md @@ -0,0 +1,13 @@ +# Document Outline (Bookmarks) + +PDF defines document outline ([PdfOutlineNode](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfOutlineNode-class.html)), which is sometimes called as bookmarks or index. And you can access it by [PdfDocument.loadOutline](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocument/loadOutline.html). + +The following fragment obtains it on [PdfViewerParams.onViewerReady](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/onViewerReady.html): + +```dart +onViewerReady: (document, controller) async { + outline.value = await document.loadOutline(); +}, +``` + +[PdfOutlineNode](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfOutlineNode-class.html) is tree structured data and for more information, see the usage on the [example code](https://github.com/espresso3389/pdfrx/blob/master/example/viewer/lib/outline_view.dart). diff --git a/doc/Double-tap-to-Zoom.md b/doc/Double-tap-to-Zoom.md new file mode 100644 index 00000000..c22b3322 --- /dev/null +++ b/doc/Double-tap-to-Zoom.md @@ -0,0 +1,30 @@ + +# Double-tap to Zoom + +You can implement double-tap-to-zoom feature using [PdfViewerParams.viewerOverlayBuilder](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/viewerOverlayBuilder.html): + +```dart +viewerOverlayBuilder: (context, size, handleLinkTap) => [ + GestureDetector( + behavior: HitTestBehavior.translucent, + // Your code here: + onDoubleTap: () { + controller.zoomUp(loop: true); + }, + // If you use GestureDetector on viewerOverlayBuilder, it breaks link-tap handling + // and you should manually handle it using onTapUp callback + onTapUp: (details) { + handleLinkTap(details.localPosition); + }, + // Make the GestureDetector covers all the viewer widget's area + // but also make the event go through to the viewer. + child: IgnorePointer( + child: + SizedBox(width: size.width, height: size.height), + ), + ), + ... +], +``` + +If you want to use [PdfViewerScrollThumb](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerScrollThumb-class.html) with double-tap-to-zoom enabled, place the double-tap-to-zoom code before [PdfViewerScrollThumb](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerScrollThumb-class.html). \ No newline at end of file diff --git a/doc/Importing-Images-to-PDF.md b/doc/Importing-Images-to-PDF.md new file mode 100644 index 00000000..d987fefd --- /dev/null +++ b/doc/Importing-Images-to-PDF.md @@ -0,0 +1,313 @@ +# Importing Images to PDF + +pdfrx provides powerful APIs for importing images into PDF documents. This feature enables you to convert images to PDF format, insert images as new pages into existing PDFs, and create PDFs from scanned documents or photos. + +## Overview + +The image import feature allows you to: + +- Convert images (JPEG, PNG, etc.) to PDF format +- Create PDF documents from JPEG images +- Insert images as new pages into existing PDFs +- Build PDFs from scanned documents or photos +- Control image dimensions and placement in the PDF + +## Creating PDF Documents from Images + +### PdfDocument.createFromJpegData + +[PdfDocument.createFromJpegData](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDocument/createFromJpegData.html) creates a new PDF document containing a single page with the specified JPEG image: + +```dart +import 'dart:ui' as ui; +import 'package:pdfrx/pdfrx.dart'; +import 'package:image/image.dart' as img; + +// Load and decode an image +final imageBytes = await File('photo.jpg').readAsBytes(); + +// If the image is already JPEG, you can use it directly +// Otherwise, convert it to JPEG format +Uint8List jpegData; +if (imagePath.toLowerCase().endsWith('.jpg') || imagePath.toLowerCase().endsWith('.jpeg')) { + jpegData = imageBytes; +} else { + // Decode the image + final image = img.decodeImage(imageBytes); + if (image == null) throw Exception('Failed to decode image'); + + // Encode as JPEG with quality 90 + jpegData = Uint8List.fromList(img.encodeJpg(image, quality: 90)); +} + +// Create PDF with JPEG data (width and height in PDF units: 1/72 inch) +final imageDoc = await PdfDocument.createFromJpegData( + jpegData, + width: 595, // A4 width in points (8.27 inches) + height: 842, // A4 height in points (11.69 inches) + sourceName: 'photo.pdf', +); + +// Encode to PDF bytes +final pdfBytes = await imageDoc.encodePdf(); +await File('output.pdf').writeAsBytes(pdfBytes); + +// Clean up +imageDoc.dispose(); +``` + +**Important Note:** The `width` and `height` parameters only control the visible page size in the PDF document. They do NOT resize or compress the actual image data. The JPEG image is embedded in the PDF as-is. If you need to reduce the PDF file size, you must resize the image before converting it to JPEG. + +### Understanding Image Dimensions + +PDF uses points as its unit of measurement, where 1 point = 1/72 inch. When creating a PDF from an image, you need to specify the page dimensions in points. + +#### Common Page Sizes (in points) + +```dart +// A4 (210mm × 297mm) +width: 595, height: 842 + +// Letter (8.5" × 11") +width: 612, height: 792 + +// Legal (8.5" × 14") +width: 612, height: 1008 + +// A3 (297mm × 420mm) +width: 842, height: 1191 +``` + +#### Calculating Dimensions from Image Size + +If you want to preserve the image's aspect ratio and size based on DPI, you need to decode the JPEG to get its dimensions: + +```dart +import 'package:image/image.dart' as img; + +// Decode JPEG to get dimensions +final jpegImage = img.decodeJpg(jpegData); +if (jpegImage == null) throw Exception('Failed to decode JPEG'); + +// Assume image DPI (common values: 72, 96, 150, 300) +const double assumedDpi = 300; + +// Calculate PDF page size from image dimensions +final width = jpegImage.width * 72 / assumedDpi; +final height = jpegImage.height * 72 / assumedDpi; + +final imageDoc = await PdfDocument.createFromJpegData( + jpegData, + width: width, + height: height, + sourceName: 'image.pdf', +); +``` + +**Note:** Many JPEG files contain DPI information in their metadata, but it's often unreliable (typically 96 DPI or 72 DPI), which can result in very large PDF pages. It's usually better to use an assumed DPI (like 300) for better results. + +## Inserting Images into Existing PDFs + +You can combine `PdfDocument.createFromJpegData` with [page manipulation](PDF-Page-Manipulation.md) to insert images into existing PDF documents: + +```dart +import 'package:image/image.dart' as img; + +// Load existing PDF +final doc = await PdfDocument.openFile('document.pdf'); + +// Load and convert image to JPEG +final imageBytes = await File('photo.png').readAsBytes(); +final image = img.decodeImage(imageBytes); +if (image == null) throw Exception('Failed to decode image'); + +// Encode as JPEG +final jpegData = Uint8List.fromList(img.encodeJpg(image, quality: 90)); + +final imageDoc = await PdfDocument.createFromJpegData( + jpegData, + width: 595, + height: 842, + sourceName: 'temp-image.pdf', +); + +// Insert image page at position 2 (after first page) +doc.pages = [ + doc.pages[0], // Page 1 (original) + imageDoc.pages[0], // Page 2 (new image) + ...doc.pages.sublist(1), // Remaining pages +]; + +// Save the result +final pdfBytes = await doc.encodePdf(); +await File('output.pdf').writeAsBytes(pdfBytes); + +// Clean up +doc.dispose(); +imageDoc.dispose(); +``` + +## Converting Multiple Images to a Single PDF + +You can create a PDF document containing multiple images by combining pages from multiple image-based PDFs: + +```dart +import 'package:image/image.dart' as img; + +// Create a new PDF document +final combinedDoc = await PdfDocument.createNew(sourceName: 'multi-image.pdf'); + +// List to store image documents +final List imageDocs = []; + +try { + // Load and convert multiple images + for (final imagePath in ['image1.jpg', 'image2.png', 'image3.jpg']) { + final imageBytes = await File(imagePath).readAsBytes(); + + // Decode the image + final image = img.decodeImage(imageBytes); + if (image == null) { + print('Failed to decode $imagePath'); + continue; + } + + // Encode as JPEG + final jpegData = Uint8List.fromList(img.encodeJpg(image, quality: 90)); + + // Calculate dimensions (assuming 300 DPI) + final width = image.width * 72 / 300; + final height = image.height * 72 / 300; + + final imageDoc = await PdfDocument.createFromJpegData( + jpegData, + width: width, + height: height, + sourceName: imagePath, + ); + imageDocs.add(imageDoc); + } + + // Combine all image pages + combinedDoc.pages = imageDocs.map((doc) => doc.pages[0]).toList(); + + // Encode to PDF + final pdfBytes = await combinedDoc.encodePdf(); + await File('output.pdf').writeAsBytes(pdfBytes); + +} finally { + // Clean up resources + for (final doc in imageDocs) { + doc.dispose(); + } + combinedDoc.dispose(); +} +``` + +## Controlling PDF File Size + +### Image Resolution and File Size + +The `width` and `height` parameters in `createFromJpegData()` **only control the page dimensions**, not the image resolution. The JPEG image data is embedded in the PDF file as-is. + +**Example:** + +```dart +import 'package:image/image.dart' as img; + +// This creates a small page, but the PDF file will still contain +// the full 4000x3000 pixel JPEG data +final largeImage = img.decodeImage(bytes); // 4000x3000 pixels +final jpegData = Uint8List.fromList(img.encodeJpg(largeImage!, quality: 90)); + +final doc = await PdfDocument.createFromJpegData( + jpegData, + width: 200, // Small page width + height: 150, // Small page height + sourceName: 'small-page-large-file.pdf', +); +// Result: Small visible page, but LARGE file size! +``` + +### How to Reduce PDF File Size + +To reduce the PDF file size, you must resize the image **before** converting it to JPEG: + +```dart +import 'package:image/image.dart' as img; + +// Load and decode original image +final originalImage = img.decodeImage(imageBytes); +if (originalImage == null) throw Exception('Failed to decode image'); + +// Target page size at desired DPI (e.g., A4 at 150 DPI) +const targetDpi = 150.0; +const pageWidthInches = 8.27; // A4 width in inches +const pageHeightInches = 11.69; // A4 height in inches +const pageWidthPixels = (pageWidthInches * targetDpi).round(); // 1240 pixels +const pageHeightPixels = (pageHeightInches * targetDpi).round(); // 1754 pixels + +// Calculate aspect ratios +final imageAspect = originalImage.width / originalImage.height; +final pageAspect = pageWidthPixels / pageHeightPixels; + +// Calculate target dimensions that fit within the page while preserving aspect ratio +int targetWidth, targetHeight; +if (imageAspect > pageAspect) { + // Image is wider than page - fit to width + targetWidth = pageWidthPixels; + targetHeight = (pageWidthPixels / imageAspect).round(); +} else { + // Image is taller than page - fit to height + targetHeight = pageHeightPixels; + targetWidth = (pageHeightPixels * imageAspect).round(); +} + +// Resize the image +final resizedImage = img.copyResize( + originalImage, + width: targetWidth, + height: targetHeight, + interpolation: img.Interpolation.linear, +); + +// Encode as JPEG with quality setting (1-100) +// Lower quality = smaller file size but lower image quality +final jpegData = Uint8List.fromList(img.encodeJpg(resizedImage, quality: 85)); + +// Calculate PDF page dimensions +const pdfPageWidth = pageWidthInches * 72; // A4 width in points (595) +const pdfPageHeight = pageHeightInches * 72; // A4 height in points (842) + +final doc = await PdfDocument.createFromJpegData( + jpegData, + width: pdfPageWidth, + height: pdfPageHeight, + sourceName: 'optimized.pdf', +); + +// Encode to PDF +final pdfBytes = await doc.encodePdf(); +await File('output.pdf').writeAsBytes(pdfBytes); + +// Clean up +doc.dispose(); +``` + +### Recommended Image Resolutions + +Choose your image resolution based on the intended use: + +| Use Case | DPI | A4 Page Size (pixels) | Description | +|----------|-----|----------------------|-------------| +| Screen viewing | 72-96 | 595×842 to 794×1123 | Smallest file size | +| Standard printing | 150 | 1240×1754 | Good balance | +| High-quality printing | 300 | 2480×3508 | Professional quality | +| Archival/Photo printing | 600 | 4960×7016 | Maximum quality | + +**Rule of thumb:** Match the image pixel dimensions to your target DPI and page size to avoid unnecessarily large files. + +## Related Documentation + +- [PDF Page Manipulation](PDF-Page-Manipulation.md) - Learn how to combine and rearrange pages +- [pdf_combine example app](../packages/pdfrx/example/pdf_combine/) - Visual interface for combining PDFs and images diff --git a/doc/Interoperability-with-other-PDFium-Libraries.md b/doc/Interoperability-with-other-PDFium-Libraries.md new file mode 100644 index 00000000..b46fb88b --- /dev/null +++ b/doc/Interoperability-with-other-PDFium-Libraries.md @@ -0,0 +1,355 @@ +# Interoperability with other PDFium Libraries + +This document explains how pdfrx can coexist with other PDFium-based libraries in the same application, and the mechanisms provided to ensure safe concurrent usage. + +## The Challenge + +PDFium is not thread-safe. When multiple libraries in the same application use PDFium, they can interfere with each other causing: + +- Crashes due to concurrent access +- Data corruption +- Unexpected behavior + +This is particularly important when your application uses: + +- Multiple PDF libraries (e.g., pdfrx alongside another PDFium wrapper) +- Direct PDFium calls through FFI +- Native plugins that internally use PDFium + +## Solution: Coordinated PDFium Access + +pdfrx provides mechanisms to coordinate PDFium access across different libraries through the `PdfrxEntryFunctions` class. + +## Low-Level PDFium Bindings + +For advanced use cases, the [pdfium_dart](https://pub.dev/packages/pdfium_dart) package exposes direct access to PDFium's C API through FFI bindings. This allows you to: + +- Call any PDFium function directly +- Implement custom PDF processing not covered by the high-level API +- Integrate with existing PDFium-based code + +**Important**: When using low-level bindings, you must: + +1. Initialize PDFium first using `pdfrxFlutterInitialize()` or `pdfrxInitialize()` (See [pdfrx Initialization](pdfrx-Initialization.md)) +2. Wrap all PDFium calls with `suspendPdfiumWorkerDuringAction()` to prevent conflicts +3. Properly manage memory allocation and deallocation + +See [Example 2](#example-2-low-level-pdfium-bindings-access) below for detailed usage. + +### PDFium Initialization + +PDFium requires initialization through its C API functions `FPDF_InitLibrary()` or `FPDF_InitLibraryWithConfig()` before any PDF operations can be performed. Starting from pdfrx v2.1.15, this initialization is handled automatically when you call: + +- `pdfrxFlutterInitialize()` - For Flutter applications +- `pdfrxInitialize()` - For pure Dart applications + +These functions internally call the PDFium's `FPDF_InitLibraryWithConfig()` to properly initialize the PDFium library. This ensures: + +- PDFium is initialized exactly once for all libraries +- The initialization happens at the right time +- The PDFium instance can be shared across multiple libraries + +**Important**: pdfrx does relatively important PDFium initialization process with `FPDF_InitLibraryWithConfig()`, so it's recommended to call `pdfrxFlutterInitialize()` or `pdfrxInitialize()` for initialization rather than calling `FPDF_InitLibrary()` or `FPDF_InitLibraryWithConfig()` on your code or by other libraries without any other important reason. + +### Suspending PDFium Worker + +The most important feature for interoperability is `PdfrxEntryFunctions.suspendPdfiumWorkerDuringAction()`. This function temporarily suspends pdfrx's internal PDFium operations while you call other PDFium-based libraries: + +```dart +import 'package:pdfrx/pdfrx.dart'; + +// Suspend pdfrx's PDFium operations while calling another library +final result = await PdfrxEntryFunctions.instance.suspendPdfiumWorkerDuringAction(() async { + // Safe to call other PDFium-based libraries here + // pdfrx won't interfere with these calls + return await otherPdfLibrary.processPdf(); +}); +``` + +## Implementation Details + +### Native Platforms (iOS, Android, Windows, macOS, Linux) + +On native platforms, pdfrx uses a background isolate worker to handle PDFium operations. The `suspendPdfiumWorkerDuringAction` method: + +1. Pauses the background worker's PDFium operations +2. Executes your action (which can safely call PDFium) +3. Resumes the background worker + +This ensures that pdfrx and other libraries never call PDFium simultaneously. + +### Web Platform + +On web, pdfrx uses a Web Worker with PDFium WASM. Since the WASM instance is isolated within the worker, there's no risk of interference with other libraries. The `suspendPdfiumWorkerDuringAction` method simply executes the action without additional synchronization. + +## Usage Examples + +### Example 1: Using pdfrx with Another PDFium Library + +```dart +import 'package:pdfrx/pdfrx.dart'; +import 'package:another_pdf_lib/another_pdf_lib.dart' as other; + +class PdfProcessor { + // Initialize both libraries + static Future initialize() async { + // Initialize pdfrx (which calls FPDF_InitLibraryWithConfig internally) + pdfrxFlutterInitialize(); + } + + // Process PDF with the other library + Future extractTextWithOtherLibrary(String path) async { + // Suspend pdfrx operations during the other library's work + return await PdfrxEntryFunctions.instance.suspendPdfiumWorkerDuringAction(() async { + final doc = await other.PdfDocument.open(path); + final text = await doc.extractText(); + await doc.close(); + return text; + }); + } + + // Continue using pdfrx normally + Future renderWithPdfrx(String path) async { + final doc = await PdfDocument.openFile(path); + // ... render pages ... + doc.dispose(); + } +} +``` + +### Example 2: Low-Level PDFium Bindings Access + +The [pdfium_dart](https://pub.dev/packages/pdfium_dart) package provides direct access to PDFium bindings for advanced use cases. You can import the low-level bindings to make direct PDFium API calls: + +```dart +import 'dart:ffi'; +import 'dart:typed_data'; +import 'package:ffi/ffi.dart'; +import 'package:pdfrx/pdfrx.dart'; +// Access low-level PDFium bindings from pdfium_dart +import 'package:pdfium_dart/pdfium_dart.dart'; + +class LowLevelPdfiumAccess { + Future useDirectBindings() async { + // Initialize PDFium through pdfrx + await pdfrxFlutterInitialize(); + + // Get PDFium bindings + final pdfium = await getPdfium(); + + // Now you can use all PDFium C API functions through the bindings + // Remember to wrap calls with suspendPdfiumWorkerDuringAction + await PdfrxEntryFunctions.instance.suspendPdfiumWorkerDuringAction(() { + // You can call any PDFium function from the bindings + // pdfium.FPDF_LoadDocument(...) + // pdfium.FPDF_GetPageCount(...) + // etc. + }); + } + + Future> extractCustomMetadata(Uint8List pdfData) async { + final pdfium = await getPdfium(); + + return await PdfrxEntryFunctions.instance.suspendPdfiumWorkerDuringAction(() { + return using((arena) { + final dataPtr = arena(pdfData.length); + dataPtr.asTypedList(pdfData.length).setAll(0, pdfData); + + // Load document using low-level API + final doc = pdfium.FPDF_LoadMemDocument( + dataPtr.cast(), + pdfData.length, + nullptr, + ); + + if (doc == nullptr) { + throw Exception('Failed to load PDF'); + } + + try { + // Access document metadata + final metadata = {}; + + // Get page count + final pageCount = pdfium.FPDF_GetPageCount(doc); + metadata['pageCount'] = pageCount.toString(); + + // Get document metadata tags + for (final tag in ['Title', 'Author', 'Subject', 'Keywords', 'Creator']) { + final buffer = arena(256); + final tagPtr = tag.toNativeUtf8(allocator: arena); + final len = pdfium.FPDF_GetMetaText( + doc, + tagPtr.cast(), + buffer.cast(), + 256, + ); + if (len > 0) { + // PDFium returns UTF-16 encoded text + final text = buffer.cast().toDartString(length: (len ~/ 2) - 1); + metadata[tag] = text; + } + } + + return metadata; + } finally { + pdfium.FPDF_CloseDocument(doc); + } + }); + }); + } +} +``` + +**Important Notes about Low-Level Bindings:** + +- Import `package:pdfium_dart/pdfium_dart.dart` for the FFI binding definitions and `getPdfium()` function +- Always wrap PDFium calls with `suspendPdfiumWorkerDuringAction()` to prevent conflicts +- Remember to properly manage memory (use `Arena` for automatic memory management) +- PDFium text APIs often return UTF-16 encoded strings + +### Example 3: Batch Processing with Multiple Libraries + +```dart +import 'package:pdfrx/pdfrx.dart'; + +class BatchProcessor { + Future processBatch(List files) async { + pdfrxFlutterInitialize(); + + for (final file in files) { + // Use pdfrx for rendering + final doc = await PdfDocument.openFile(file); + final pageImage = await doc.pages[0].render(); + doc.dispose(); + + // Use another library for text extraction + // (wrapped to prevent interference) + final text = await PdfrxEntryFunctions.instance.suspendPdfiumWorkerDuringAction(() async { + return await otherLibrary.extractText(file); + }); + + // Process results... + } + } +} +``` + +## Best Practices + +### 1. Initialize Once + +PDFium must be initialized only once at application startup. The initialization calls `FPDF_InitLibraryWithConfig()` internally: + +```dart +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Initialize PDFium for all libraries (calls FPDF_InitLibraryWithConfig) + pdfrxFlutterInitialize(); + + runApp(MyApp()); +} +``` + +**Note**: If you have other PDFium-based libraries, ensure they don't call `FPDF_InitLibrary()` or `FPDF_InitLibraryWithConfig()` again. + +### 2. Always Wrap External PDFium Calls + +When calling other PDFium-based libraries, always use `suspendPdfiumWorkerDuringAction`: + +```dart +// ❌ Bad - May cause crashes +final result = await otherPdfLibrary.process(data); + +// ✅ Good - Safe concurrent access +final result = await PdfrxEntryFunctions.instance.suspendPdfiumWorkerDuringAction( + () => otherPdfLibrary.process(data) +); +``` + +### 3. Error Handling + +Always handle errors appropriately: + +```dart +try { + final result = await PdfrxEntryFunctions.instance.suspendPdfiumWorkerDuringAction(() async { + return await riskyPdfiumOperation(); + }); +} catch (e) { + // Handle PDFium-related errors + print('PDFium operation failed: $e'); +} +``` + +## Platform-Specific Considerations + +### Native Platforms + +- PDFium binary is shared across all libraries in the process +- Only one `FPDF_InitLibrary()`/`FPDF_InitLibraryWithConfig()` call is needed (and recommended) +- Thread safety must be managed through suspension mechanism + +### Web Platform + +- Each library typically has its own WASM instance +- Suspension is less critical but still provided for API consistency +- Memory usage may be higher due to multiple WASM instances + +## Migration Guide + +If you're adding pdfrx to an existing application that already uses PDFium: + +1. **Check Initialization**: Ensure `FPDF_InitLibrary()`/`FPDF_InitLibraryWithConfig()` is called only once +2. **Identify Conflict Points**: Find where both libraries might access PDFium simultaneously +3. **Add Suspension**: Wrap external PDFium calls with `suspendPdfiumWorkerDuringAction` +4. **Test Thoroughly**: Test concurrent operations to ensure stability + +## Troubleshooting + +### Common Issues + +**Issue**: Random crashes when using multiple PDF libraries +**Solution**: Ensure all external PDFium calls are wrapped with `suspendPdfiumWorkerDuringAction` + +**Issue**: "PDFium already initialized" errors +**Solution**: Remove duplicate `FPDF_InitLibrary()`/`FPDF_InitLibraryWithConfig()` calls; let one library handle initialization + +**Issue**: Deadlocks or hangs +**Solution**: Check for nested suspension calls or circular dependencies + +### Debug Mode + +Enable verbose logging to debug interoperability issues: + +```dart +// Enable detailed logging (development only) +if (kDebugMode) { + // Monitor suspension events + PdfrxEntryFunctions.instance.suspendPdfiumWorkerDuringAction(() { + print('PDFium operations suspended'); + // Your code here + print('PDFium operations resumed'); + }); +} +``` + +## API Reference + +### PdfrxEntryFunctions + +The main class for PDFium interoperability: + +- [`suspendPdfiumWorkerDuringAction(action)`](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfrxEntryFunctions/suspendPdfiumWorkerDuringAction.html) - Suspend pdfrx operations during action execution + +### Initialization Functions + +- [`pdfrxFlutterInitialize()`](https://pub.dev/documentation/pdfrx/latest/pdfrx/pdfrxFlutterInitialize.html) - Initialize for Flutter apps (calls `FPDF_InitLibraryWithConfig` internally) +- [`pdfrxInitialize()`](https://pub.dev/documentation/pdfrx/latest/pdfrx/pdfrxInitialize.html) - Initialize for Dart-only apps (calls `FPDF_InitLibraryWithConfig` internally) + +## See Also + +- [pdfrx Initialization](pdfrx-Initialization.md) - General initialization guide +- [API Documentation](https://pub.dev/documentation/pdfrx/latest/pdfrx/) - Complete API reference +- [GitHub Issues](https://github.com/espresso3389/pdfrx/issues) - Report problems or request features diff --git a/doc/Loading-Fonts-Dynamically.md b/doc/Loading-Fonts-Dynamically.md new file mode 100644 index 00000000..2cf12282 --- /dev/null +++ b/doc/Loading-Fonts-Dynamically.md @@ -0,0 +1,140 @@ +# Loading Fonts Dynamically + +When rendering PDFs, PDFium may require fonts that are not embedded in the PDF file itself. This is especially important for iOS and Web because PDFium does not have access to system's preinstalled fonts on these platforms due to security sandbox. + +## Overview + +pdfrx provides APIs to dynamically load font data at runtime. The fonts are cached and used by PDFium when rendering PDF pages that reference those fonts. + +Please note that PDFium's font system basically supports TrueType/OpenType font files (`ttf`/`ttc`/`otf`/`otc`). Fonts of other formats may not be supported. + +## Basic Usage + +### Loading Font Data + +Use [PdfrxEntryFunctions.addFontData](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfrxEntryFunctions/addFontData.html) to add font data dynamically: + +```dart +import 'package:pdfrx/pdfrx.dart'; +import 'package:http/http.dart' as http; + +Future loadFont() async { + // Download font from a URL + final response = await http.get(Uri.parse('https://example.com/fonts/MyFont.ttf')); + final fontData = response.bodyBytes; + + // Add font data to PDFium + await PdfrxEntryFunctions.instance.addFontData( + face: 'MyFont', // font name should be unique but don't have to be meaningful name + data: fontData, + ); + + // Instruct PDFium to reload fonts + await PdfrxEntryFunctions.instance.reloadFonts(); +} +``` + +The fonts loaded by [PdfrxEntryFunctions.addFontData](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfrxEntryFunctions/addFontData.html) are **cached on memory**. So don't load so many/large fonts. + +For non-Web platforms, you can alternatively [place fonts on file system](#place-fonts-on-file-system). + +### Reload PdfDocument Instances + +[PdfrxEntryFunctions.reloadFonts](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfrxEntryFunctions/reloadFonts.html) instructs PDFium to reload the fonts but it does not reload [PdfDocument](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocument-class.html) instances already loaded. You must close/re-open these loaded documents by yourself. + +For [PdfViewer](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer-class.html) or such widgets, you can use [PdfDocumentRef](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocumentRef-class.html) to reload the loaded document: + +```dart +// loading fonts +await PdfrxEntryFunctions.instance.addFontData(...); +await PdfrxEntryFunctions.instance.reloadFonts(); + +// and reload document using PdfViewerController.documentRef +await controller.documentRef.resolveListenable().load(forceReload: true); +``` + +### Clearing Font Cache on Memory + +To clear all fonts loaded on memory: + +```dart +await PdfrxEntryFunctions.instance.clearAllFontData(); +await PdfrxEntryFunctions.instance.reloadFonts(); +``` + +## Place Fonts on File System + +On native platforms (non-Web), you can places fonts on PDFium's font path. + +The following fragment illustrates how to add a directory path to [Pdfrx.fontPaths](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/Pdfrx/fontPaths.html): + +```dart +import 'package:pdfrx/pdfrx.dart'; +import 'package:path_provider/path_provider.dart'; + +... + +// Pdfrx.fontPaths must be set **before** calling any pdfrx functions (ideally in main) +final appDocDir = await getApplicationDocumentsDirectory(); +final fontsDir = Directory('${appDocDir.path}/fonts'); +await fontsDir.create(recursive: true); + +// Add to PDFium font paths +Pdfrx.fontPaths.add(fontsDir.path); + +// Initialize pdfrx +pdfrxFlutterInitialize(); +``` + +You can add fonts anytime on your program but you should call [PdfrxEntryFunctions.reloadFonts](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfrxEntryFunctions/reloadFonts.html) and reload the documents as explained above. + +## Handling Missing Fonts + +You can listen to [PdfDocument.events](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocument/events.html) to get [PdfDocumentMissingFontsEvent](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocumentMissingFontsEvent-class.html) on missing fonts and load the required fonts dynamically: + +```dart +document.events.listen((event) async { + if (event is PdfDocumentMissingFontsEvent) { + for (final query in event.missingFonts) { + print('Missing font: ${query.face}, charset: ${query.charset}'); + + // Load the font based on the query + final fontData = await fetchFontForQuery(query); + if (fontData != null) { + await addFontData(face: query.face, data: fontData); + } + } + + // and reload the document + .... + } +}); + +... + +Future fetchFontForQuery(PdfFontQuery query) async { + // Implement your font loading logic here + // You can use query.charset, query.weight, query.isItalic, etc. + return null; +} +``` + +The [`PdfFontQuery`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfFontQuery-class.html) object provides information about the requested font: + +- `face`: Font family name +- `weight`: Font weight (100-900) +- `isItalic`: Whether italic style is needed +- `charset`: Character set (e.g., [`PdfFontCharset.shiftJis`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfFontCharset.html) for Japanese) +- `pitchFamily`: Font pitch and family flags + +## Advanced Example + +For a more sophisticated implementation with caching and multiple font sources, see the example app's [main.dart](https://github.com/espresso3389/pdfrx/blob/master/packages/pdfrx/example/viewer/lib/main.dart#L406-L427) and [noto_google_fonts.dart](https://github.com/espresso3389/pdfrx/blob/master/packages/pdfrx/example/viewer/lib/noto_google_fonts.dart). + +## See Also + +- [PdfrxEntryFunctions.addFontData](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfrxEntryFunctions/addFontData.html) - Add font data to PDFium +- [PdfrxEntryFunctions.clearAllFontData](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfrxEntryFunctions/clearAllFontData.html) - Clear all cached fonts +- [PdfrxEntryFunctions.reloadFonts](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfrxEntryFunctions/reloadFonts.html) - Reload fonts from file system +- [Pdfrx.fontPaths](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/Pdfrx/fontPaths.html) - Font directory paths +- [PdfFontQuery](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfFontQuery-class.html) - Font query information diff --git a/doc/Low-Level-PDFium-Bindings-Access.md b/doc/Low-Level-PDFium-Bindings-Access.md new file mode 100644 index 00000000..2a4930d9 --- /dev/null +++ b/doc/Low-Level-PDFium-Bindings-Access.md @@ -0,0 +1,215 @@ +# Low-Level PDFium Bindings Access + +This document explains how to access low-level PDFium bindings directly when working with the pdfrx_engine package. + +## Overview + +While pdfrx_engine provides high-level Dart APIs for PDF operations, you may occasionally need direct access to PDFium's native functions. The low-level PDFium bindings are provided by the [pdfium_dart](https://pub.dev/packages/pdfium_dart) package: + +- `package:pdfium_dart/pdfium_dart.dart` - Raw FFI bindings generated from PDFium headers + +## Importing Low-Level Bindings + +### Raw FFI Bindings + +```dart +import 'package:pdfium_dart/pdfium_dart.dart'; +``` + +This import provides access to the auto-generated FFI bindings that directly map to PDFium's C API. These bindings are generated using `ffigen` from PDFium headers and include all PDFium functions with their original names (e.g., `FPDF_InitLibrary`, `FPDF_LoadDocument`, etc.). + +The `pdfium_dart` package provides: + +- The `PDFium` class for accessing PDFium functions +- Auto-generated FFI bindings for all PDFium C API functions +- `getPdfium()` function for on-demand PDFium binary downloads + +### Initialization + +PDFium must be initialized before use. The high-level API handles this automatically, but when using raw bindings directly, you may need to ensure initialization. + +There are basically three ways to initialize PDFium: + +#### Manual Initialization + +```dart +import 'package:pdfium_dart/pdfium_dart.dart'; +import 'dart:ffi'; + +// Load PDFium library manually +final pdfium = PDFium(DynamicLibrary.open('path/to/libpdfium.so')); +pdfium.FPDF_InitLibrary(); // or pdfium.FPDF_InitLibraryWithConfig(...) +``` + +#### Initialization for Flutter App + +```dart +import 'package:pdfrx/pdfrx.dart'; + +pdfrxFlutterInitialize(); +``` + +#### Initialization for pure Dart + +```dart +import 'package:pdfrx_engine/pdfrx_engine.dart'; + +await pdfrxInitialize(); +``` + +For more information about initialization, see [pdfrx Initialization](pdfrx-Initialization.md). + +**Important:** pdfrx does not support unloading PDFium. Never call `FPDF_DestroyLibrary` as it will cause undefined behavior. PDFium remains loaded for the lifetime of the application. + +## Usage Examples + +### Basic PDFium Function Access + +```dart +import 'package:pdfium_dart/pdfium_dart.dart'; +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +void example() async { + // Get PDFium bindings (downloads binaries if needed) + final pdfium = await getPdfium(); + + // Use arena to automatically manage memory + using((arena) { + // Access PDFium functions + final doc = pdfium.FPDF_LoadDocument( + 'path/to/file.pdf'.toNativeUtf8(allocator: arena).cast(), + nullptr, + ); + + if (doc != nullptr) { + final pageCount = pdfium.FPDF_GetPageCount(doc); + print('Page count: $pageCount'); + + // Don't forget to clean up + pdfium.FPDF_CloseDocument(doc); + } + }); +} +``` + +### Working with Pages + +```dart +import 'package:pdfium_dart/pdfium_dart.dart'; + +void workWithPage(PDFium pdfium, FPDF_DOCUMENT doc) { + final page = pdfium.FPDF_LoadPage(doc, 0); // Load first page + + if (page != nullptr) { + final width = pdfium.FPDF_GetPageWidth(page); + final height = pdfium.FPDF_GetPageHeight(page); + print('Page dimensions: ${width}x${height}'); + + pdfium.FPDF_ClosePage(page); + } +} +``` + +### Memory Management + +When working with raw bindings, you're responsible for proper memory management. Always use `Arena` to ensure allocated memory is properly released: + +```dart +import 'package:pdfium_dart/pdfium_dart.dart'; +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +void memoryExample() async { + final pdfium = await getPdfium(); + + // Use arena for automatic memory management + using((arena) { + final pathPtr = 'path/to/file.pdf'.toNativeUtf8(allocator: arena); + + final doc = pdfium.FPDF_LoadDocument( + pathPtr.cast(), + nullptr, + ); + + if (doc != nullptr) { + // Use document... + pdfium.FPDF_CloseDocument(doc); + } + // Memory allocated by arena is automatically freed when the using block ends + }); +} + +// Alternative: Manual memory management (not recommended) +void manualMemoryExample() async { + final pdfium = await getPdfium(); + final pathPtr = 'path/to/file.pdf'.toNativeUtf8(); + + try { + final doc = pdfium.FPDF_LoadDocument( + pathPtr.cast(), + nullptr, + ); + + if (doc != nullptr) { + // Use document... + pdfium.FPDF_CloseDocument(doc); + } + } finally { + // Must manually free allocated memory + calloc.free(pathPtr); + } +} +``` + +## Important Considerations + +### Thread Safety + +PDFium is not thread-safe. Ensure all PDFium calls are made from the same thread, typically the main isolate. + +### Error Handling + +Always check return values from PDFium functions: + +```dart +final pdfium = await getPdfium(); + +using((arena) { + final pathPtr = 'path/to/file.pdf'.toNativeUtf8(allocator: arena); + final doc = pdfium.FPDF_LoadDocument(pathPtr.cast(), nullptr); + + if (doc == nullptr) { + final error = pdfium.FPDF_GetLastError(); + print('Failed to load document. Error code: $error'); + // Handle error... + } else { + // Use document... + pdfium.FPDF_CloseDocument(doc); + } +}); +``` + +## When to Use Low-Level Bindings + +Consider using low-level bindings when: + +1. You need functionality not exposed by the high-level API +2. You're implementing performance-critical operations +3. You need fine-grained control over memory management +4. You're extending pdfrx_engine with new features + +## Warning + +Using low-level bindings bypasses many safety checks and conveniences provided by the high-level API. Ensure you: + +- Properly manage memory allocation and deallocation +- Handle errors appropriately +- Follow PDFium's threading requirements +- Test thoroughly on all target platforms + +## See Also + +- [PDFium API Documentation](https://pdfium.googlesource.com/pdfium/+/refs/heads/main/public/) +- [FFI Package Documentation](https://pub.dev/packages/ffi) +- [pdfrx_engine API Reference](https://pub.dev/documentation/pdfrx_engine/latest/) diff --git a/doc/PDF-Link-Handling.md b/doc/PDF-Link-Handling.md new file mode 100644 index 00000000..f47e1dc8 --- /dev/null +++ b/doc/PDF-Link-Handling.md @@ -0,0 +1,88 @@ +# PDF Link Handling + +To enable links in PDF file, you should set [PdfViewerParams.linkHandlerParams](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/linkHandlerParams.html). + +The following fragment handles user's tap on link: + +```dart +linkHandlerParams: PdfLinkHandlerParams( + onLinkTap: (link) { + // handle URL or Dest + if (link.url != null) { + // FIXME: Don't open the link without prompting user to do so or validating the link destination + launchUrl(link.url!); + } else if (link.dest != null) { + controller.goToDest(link.dest); + } + }, +), +``` + +## Security Considerations on Link Navigation + +It's too dangerous to open link URL without prompting user to do so/validating it. + +The following fragment is an example code to prompt user to open the URL: + +```dart +Future shouldOpenUrl(BuildContext context, Uri url) async { + final result = await showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + return AlertDialog( + title: const Text('Navigate to URL?'), + content: SelectionArea( + child: Text.rich( + TextSpan( + children: [ + const TextSpan( + text: + 'Do you want to navigate to the following location?\n'), + TextSpan( + text: url.toString(), + style: const TextStyle(color: Colors.blue), + ), + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: const Text('Go'), + ), + ], + ); + }, + ); + return result ?? false; +} +``` + +## PDF Destinations + +For PDF destinations, you can use [PdfViewerController.goToDest](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerController/goToDest.html) to go to the destination. Or you can use [PdfViewerController.calcMatrixForDest](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerController/calcMatrixForDest.html) to get the matrix for it. + +## Link Appearance + +For link appearance, you can change its color using [PdfLinkHandlerParams.linkColor](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfLinkHandlerParams/linkColor.html). + +For more further customization, you can use [PdfLinkHandlerParams.customPainter](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfLinkHandlerParams/customPainter.html): + +```dart +customPainter: (canvas, pageRect, page, links) { + final paint = Paint() + ..color = Colors.red.withOpacity(0.2) + ..style = PaintingStyle.fill; + for (final link in links) { + // you can customize here to make your own link appearance + final rect = link.rect.toRectInPageRect(page: page, pageRect: pageRect); + canvas.drawRect(rect, paint); + } +} +``` diff --git a/doc/PDF-Page-Manipulation.md b/doc/PDF-Page-Manipulation.md new file mode 100644 index 00000000..97d44fed --- /dev/null +++ b/doc/PDF-Page-Manipulation.md @@ -0,0 +1,184 @@ +# PDF Page Manipulation + +pdfrx provides powerful APIs for manipulating PDF pages, including re-arranging pages within a document and combining pages from multiple PDF documents. This feature enables you to create PDF merge tools, page extractors, and document reorganization utilities. + +## Overview + +The page manipulation feature allows you to: + +- Re-arrange pages within a PDF document +- Combine pages from multiple PDF documents into one +- Extract specific pages from a document +- Duplicate pages within a document +- Rotate pages to different orientations +- Create new PDF documents from selected pages + +## Key Concepts + +### Creating PDF Documents for Page Manipulation + +There are multiple ways to create a [PdfDocument](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDocument-class.html) for page manipulation: + +#### Using Existing PDF Files + +When you open an existing PDF file using methods like [PdfDocument.openFile](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDocument/openFile.html) or [PdfDocument.openData](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDocument/openData.html), the document inherits all settings from the original PDF, including: + +- Security settings and encryption +- Document metadata +- PDF version and features +- Form fields and annotations + +```dart +// Open existing PDF - inherits all original settings +final doc = await PdfDocument.openFile('document.pdf'); +``` + +This is useful when you want to preserve the original PDF's properties while manipulating its pages. + +#### Creating New PDF Documents with PdfDocument.createNew + +[PdfDocument.createNew](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDocument/createNew.html) creates a completely new, empty PDF document from scratch without inheriting any settings: + +```dart +// Create a brand new empty PDF +final newDoc = await PdfDocument.createNew(sourceName: 'combined.pdf'); + +// Add pages from other documents +newDoc.pages = [doc1.pages[0], doc2.pages[1], doc3.pages[2]]; + +// Encode to PDF bytes +final pdfBytes = await newDoc.encodePdf(); +``` + +**Use cases for PdfDocument.createNew:** + +- Combining pages from multiple PDFs without inheriting any security or metadata settings +- Creating a clean PDF document without any legacy properties +- Building PDFs programmatically where you want full control over document properties + +### PdfDocument.pages Property + +The [PdfDocument.pages](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDocument/pages.html) property is both readable and writable: + +```dart +// Read pages +final pages = document.pages; // List + +// Re-arrange pages +// same page can be listed several times +document.pages = [pages[2], pages[0], pages[1], pages[1]]; +``` + +### Cross-Document Page References + +[PdfPage](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPage-class.html) instances can be used across different [PdfDocument](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDocument-class.html) instances. This means you can take pages from one document and add them to another: + +```dart +final doc1 = await PdfDocument.openFile('document1.pdf'); +final doc2 = await PdfDocument.openFile('document2.pdf'); + +// Combine pages from both documents +doc1.pages = [ + doc1.pages[0], // Page 1 from doc1 + doc2.pages[0], // Page 1 from doc2 + doc1.pages[1], // Page 2 from doc1 + doc2.pages[1], // Page 2 from doc2 +]; +``` + +### Page Rotation + +The [PdfPageWithRotationExtension](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPageWithRotationExtension.html) provides several page rotation methods to apply rotation to pages when manipulating PDF documents. + +```dart +final doc = await PdfDocument.openFile('document.pdf'); + +// Use the rotated page in page manipulation +doc.pages = [ + doc.pages[0], // Page 2 with original rotation + doc.pages[1], // Page 2 with original rotation + doc.pages[2].rotatedCW90(), // Page 3 rotated right +]; +``` + +Technically, the functions on [PdfPageWithRotationExtension](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPageWithRotationExtension.html) creates a proxy object with the specified rotation, which can be used in page manipulation operations. + +**Important Notes:** + +- If the specified rotation matches the page's current rotation, the original page is returned unchanged + +### Encoding to PDF + +After manipulating pages, use [PdfDocument.encodePdf](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDocument/encodePdf.html) to generate the final PDF file: + +```dart +final pdfBytes = await document.encodePdf(); +await File('output.pdf').writeAsBytes(pdfBytes); + +// dispose the documents after use +doc1.dispose(); +doc2.dispose(); +``` + +### The assemble() Function + +When you combine pages from multiple documents, the resulting document maintains references to the source documents. The [PdfDocument.assemble](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDocument/assemble.html) function re-organizes the document to be self-contained, removing dependencies on other [PdfDocument](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDocument-class.html) instances: + +```dart +// After combining pages from multiple documents +doc1.pages = [...doc1.pages, ...doc2.pages, ...doc3.pages]; + +// Assemble to make doc1 independent +await doc1.assemble(); + +// Now you can safely dispose doc2 and doc3 +doc2.dispose(); +doc3.dispose(); + +// doc1 is still valid and can be encoded later +final bytes = await doc1.encodePdf(); + +doc1.dispose(); +``` + +**Important Notes:** + +- [PdfDocument.assemble](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDocument/assemble.html) is automatically called by [PdfDocument.encodePdf](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDocument/encodePdf.html), so you don't need to call it explicitly before encoding +- Call [PdfDocument.assemble](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDocument/assemble.html) explicitly when you want to release source documents early to free memory +- After calling [PdfDocument.assemble](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDocument/assemble.html), the document becomes independent and source documents can be safely disposed + +## Complete Working Examples + +### Command-Line PDF Combiner + +The [pdfcombine.dart](../packages/pdfrx_engine/example/pdfcombine.dart) example demonstrates a full-featured command-line tool for combining PDFs: + +```bash +# Combine entire documents +dart run pdfrx_engine:pdfcombine -o output.pdf doc1.pdf doc2.pdf doc3.pdf -- a b c + +# Combine specific page ranges +dart run pdfrx_engine:pdfcombine -o output.pdf doc1.pdf doc2.pdf -- a[1-10] b[5-15] + +# Mix pages from multiple documents +dart run pdfrx_engine:pdfcombine -o output.pdf doc1.pdf doc2.pdf -- a[1-3] b a[4-6] b[1-2] +``` + +Key features: + +- Flexible page specification syntax +- Support for page ranges (`[1-10]`) and individual pages (`[1,3,5]`) +- Can interleave pages from multiple documents +- Validates page numbers and file existence + +### Flutter PDF Combine App + +The [pdf_combine](../packages/pdfrx/example/pdf_combine/) Flutter app provides a visual interface for combining PDFs: + +Key features: + +- Drag-and-drop interface for page re-arrangement +- Visual thumbnails of PDF pages +- Support for multiple source documents +- Live preview of the combined result +- Export to file diff --git a/doc/Page-Layout-Customization.md b/doc/Page-Layout-Customization.md new file mode 100644 index 00000000..e0912556 --- /dev/null +++ b/doc/Page-Layout-Customization.md @@ -0,0 +1,88 @@ +# Page Layout Customization + +> NOTE: setting [PdfViewerParams.layoutPages](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/layoutPages.html) dynamically does not refresh the viewer. You should call [PdfViewerController.invalidate](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerController/invalidate.html), [setState](https://api.flutter.dev/flutter/widgets/State/setState.html) or some equivalent function. + +## Horizontal Scroll View + +By default, the pages are laid out vertically. +You can customize the layout logic by [PdfViewerParams.layoutPages](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/layoutPages.html): + +```dart +layoutPages: (pages, params) { + final height = + pages.fold(0.0, (prev, page) => max(prev, page.height)) + + params.margin * 2; + final pageLayouts = []; + double x = params.margin; + for (var page in pages) { + pageLayouts.add( + Rect.fromLTWH( + x, + (height - page.height) / 2, // center vertically + page.width, + page.height, + ), + ); + x += page.width + params.margin; + } + return PdfPageLayout( + pageLayouts: pageLayouts, + documentSize: Size(x, height), + ); +}, +``` + +## Facing Pages + +The following code will show pages in "facing-sequential-layout" that is often used in PDF viewer apps: + +```dart +/// Page reading order; true to L-to-R that is commonly used by books like manga or such +var isRightToLeftReadingOrder = false; +/// Use the first page as cover page +var needCoverPage = true; + +... + +layoutPages: (pages, params) { + final width = pages.fold( + 0.0, (prev, page) => max(prev, page.width)); + + final pageLayouts = []; + final offset = needCoverPage ? 1 : 0; + double y = params.margin; + for (int i = 0; i < pages.length; i++) { + final page = pages[i]; + final pos = i + offset; + final isLeft = isRightToLeftReadingOrder + ? (pos & 1) == 1 + : (pos & 1) == 0; + + final otherSide = (pos ^ 1) - offset; + final h = 0 <= otherSide && otherSide < pages.length + ? max(page.height, pages[otherSide].height) + : page.height; + + pageLayouts.add( + Rect.fromLTWH( + isLeft + ? width + params.margin - page.width + : params.margin * 2 + width, + y + (h - page.height) / 2, + page.width, + page.height, + ), + ); + if (pos & 1 == 1 || i + 1 == pages.length) { + y += h + params.margin; + } + } + return PdfPageLayout( + pageLayouts: pageLayouts, + documentSize: Size( + (params.margin + width) * 2 + params.margin, + y, + ), + ); +}, +``` diff --git a/doc/Progressive-Loading.md b/doc/Progressive-Loading.md new file mode 100644 index 00000000..d3c85054 --- /dev/null +++ b/doc/Progressive-Loading.md @@ -0,0 +1,356 @@ +# Progressive Loading + +Progressive loading is a feature that allows PDF documents to be loaded page-by-page instead of loading all pages at once. This is particularly useful for large PDF files, as it significantly reduces initial load time and memory usage. + +## Overview + +When you open a PDF document, pdfrx can operate in two modes: + +1. **Standard Loading** (default): All pages are loaded immediately when the document is opened +2. **Progressive Loading**: Only the first page is loaded initially, and additional pages are loaded on-demand + +Progressive loading is especially beneficial when: + +- Working with large PDF files (hundreds of pages) +- Memory is constrained +- You want faster initial document load times +- Users typically don't view all pages in a session + +**Note**: [`PdfViewer`](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer-class.html) uses progressive loading by default, while [`PdfDocument`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDocument-class.html) requires explicit opt-in. + +## Understanding Page Loading States + +When progressive loading is enabled, pages can be in one of two states: + +### Loaded Pages + +Pages that are fully loaded have complete information: + +- Accurate [`width`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPage/width.html), [`height`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPage/height.html), and [`rotation`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPage/rotation.html) values +- Can be rendered properly +- Text extraction works ([`loadText()`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPage/loadText.html), [`loadStructuredText()`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPageBaseExtensions/loadStructuredText.html)) +- Link extraction works ([`loadLinks()`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPage/loadLinks.html)) +- [`isLoaded`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPage/isLoaded.html) property returns `true` + +### Unloaded Pages (Progressive Loading Only) + +Pages that haven't been loaded yet have limited functionality: + +- [`width`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPage/width.html), [`height`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPage/height.html), and [`rotation`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPage/rotation.html) are **estimated values** (may be incorrect) +- Rendering produces an empty page with the specified background color +- Text extraction returns `null` +- Link extraction returns an empty list +- [`isLoaded`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPage/isLoaded.html) property returns `false` + +## Enabling Progressive Loading + +### For PdfViewer + +[`PdfViewer`](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer-class.html) uses progressive loading **by default** (`useProgressiveLoading: true`). This means PDF documents are loaded page-by-page automatically as you scroll, providing optimal performance for large files. + +All [`PdfViewer`](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer-class.html) constructors support the `useProgressiveLoading` parameter: + +```dart +// Uses progressive loading by default +PdfViewer.file('path/to/document.pdf') + +// Explicitly enable progressive loading (same as default) +PdfViewer.asset( + 'assets/large-document.pdf', + useProgressiveLoading: true, +) + +// Disable progressive loading (load all pages at once) +PdfViewer.uri( + Uri.parse('https://example.com/document.pdf'), + useProgressiveLoading: false, +) +``` + +When using [`PdfViewer`](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer-class.html), progressive loading happens automatically in the background as you scroll through the document. You don't need to manually call [`loadPagesProgressively()`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDocument/loadPagesProgressively.html). + +### For PdfDocument (Engine-Level API) + +When using [`PdfDocument`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDocument-class.html) directly (without [`PdfViewer`](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer-class.html)), progressive loading is **disabled by default** (`useProgressiveLoading: false`). You need to explicitly enable it: + +```dart +import 'package:pdfrx_engine/pdfrx_engine.dart'; + +// Open a document with progressive loading enabled +final document = await PdfDocument.openFile( + 'path/to/document.pdf', + useProgressiveLoading: true, +); + +// At this point, only the first page is loaded +print('First page loaded: ${document.pages[0].isLoaded}'); // true +print('Second page loaded: ${document.pages[1].isLoaded}'); // false +``` + +## Working with Progressively Loaded Documents + +### Checking Page Load Status + +Use the [`isLoaded`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPage/isLoaded.html) property to check if a page is fully loaded: + +```dart +final page = document.pages[5]; + +if (page.isLoaded) { + // Page is fully loaded - all operations work normally + final text = await page.loadText(); + print(text?.text); +} else { + // Page is not loaded yet - dimensions may be estimates + print('Page not loaded yet'); +} +``` + +### Waiting for a Page to Load + +Use the [`ensureLoaded()`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPageBaseExtensions/ensureLoaded.html) extension method to wait for a specific page to load. This method waits indefinitely and always returns a loaded page: + +```dart +final page = document.pages[10]; + +// Wait for the page to load (waits indefinitely, never returns null) +final loadedPage = await page.ensureLoaded(); +final text = await loadedPage.loadText(); +print(text?.text); +``` + +If you need to set a timeout, use [`waitForLoaded()`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPageBaseExtensions/waitForLoaded.html) instead. This method returns `null` if the timeout occurs: + +```dart +final page = document.pages[10]; + +// Wait for the page to load with a timeout +final loadedPage = await page.waitForLoaded( + timeout: Duration(seconds: 5), +); + +if (loadedPage != null) { + // Page loaded successfully + final text = await loadedPage.loadText(); + print(text?.text); +} else { + // Timeout occurred + print('Page failed to load within timeout'); +} +``` + +**Important**: The [`ensureLoaded()`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPageBaseExtensions/ensureLoaded.html) method may return a **different instance** of [`PdfPage`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPage-class.html) than the original. Always use the returned instance: + +```dart +// ❌ WRONG - using the old page instance +final page = document.pages[10]; +await page.ensureLoaded(); +final text = await page.loadText(); // May not work as expected + +// ✅ CORRECT - using the returned loaded page instance +final page = document.pages[10]; +final loadedPage = await page.ensureLoaded(); +final text = await loadedPage.loadText(); // Works correctly +``` + +### Monitoring Page Load Events + +You can listen to page status changes using the [`events`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPageBaseExtensions/events.html) stream. The event provides the latest page instance directly via the `page` property: + +```dart +final page = document.pages[5]; + +// Listen for status changes on this specific page +page.events.listen((change) { + // The change.page property provides the newest page instance + final updatedPage = change.page; + print('Page ${updatedPage.pageNumber} status changed'); + print('Is loaded: ${updatedPage.isLoaded}'); + + if (updatedPage.isLoaded) { + print('Page dimensions: ${updatedPage.width} x ${updatedPage.height}'); + } +}); +``` + +### Getting Latest Page Instance + +The [`latestPageStream`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPageBaseExtensions/latestPageStream.html) provides the most recent page instance whenever the page status changes: + +```dart +final page = document.pages[10]; + +page.latestPageStream.listen((latestPage) { + print('Page updated, isLoaded: ${latestPage.isLoaded}'); + if (latestPage.isLoaded) { + // Use the latest loaded instance + } +}); +``` + +## Manually Triggering Progressive Loading + +When using [`PdfDocument`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDocument-class.html) directly (not [`PdfViewer`](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer-class.html)), you need to manually trigger progressive loading using [`loadPagesProgressively()`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDocument/loadPagesProgressively.html): + +```dart +final document = await PdfDocument.openFile( + 'path/to/document.pdf', + useProgressiveLoading: true, +); + +// Load pages progressively with progress callback +await document.loadPagesProgressively( + onPageLoadProgress: (data, loadedPageCount, totalPageCount) { + print('Loaded $loadedPageCount of $totalPageCount pages'); + + // Return true to continue loading, false to stop + return true; + }, + loadUnitDuration: Duration(milliseconds: 250), +); +``` + +The callback is invoked periodically (every `loadUnitDuration`) as pages are loaded. Return `false` from the callback to stop the loading process early. + +## Common Pitfalls and Solutions + +### Issue: Text Extraction Returns Null + +**Problem**: Trying to extract text from an unloaded page returns `null`. + +```dart +final page = document.pages[50]; +final text = await page.loadText(); // Returns null if page not loaded +``` + +**Solution**: Always wait for the page to load first: + +```dart +final page = document.pages[50]; +final loadedPage = await page.ensureLoaded(); +final text = await loadedPage.loadText(); +print(text?.text); +``` + +### Issue: Page Dimensions Are Incorrect + +**Problem**: Using [`width`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPage/width.html), [`height`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPage/height.html), or [`rotation`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPage/rotation.html) values from unloaded pages gives estimated values that may be wrong. + +```dart +final page = document.pages[20]; +print('Width: ${page.width}'); // May be an estimate if page is not loaded +``` + +**Solution**: Check [`isLoaded`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPage/isLoaded.html) or use [`ensureLoaded()`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPageBaseExtensions/ensureLoaded.html): + +```dart +final page = document.pages[20]; +final loadedPage = await page.ensureLoaded(); +print('Actual width: ${loadedPage.width}'); +print('Actual height: ${loadedPage.height}'); +``` + +### Issue: Processing All Pages Fails Silently + +**Problem**: Iterating through all pages without waiting for them to load: + +```dart +// ❌ WRONG - pages may not be loaded yet +for (final page in document.pages) { + final text = await page.loadText(); // May return null + processText(text?.text); // Silently skips unloaded pages +} +``` + +**Solution**: Ensure pages are loaded before processing: + +```dart +// ✅ CORRECT - ensure each page is loaded +for (final page in document.pages) { + final loadedPage = await page.ensureLoaded(); + final text = await loadedPage.loadText(); + processText(text?.text); +} + +// Alternative: Load all pages first using loadPagesProgressively() +await document.loadPagesProgressively(); +for (final page in document.pages) { + final text = await page.loadText(); + processText(text?.text); +} +``` + +## Performance Considerations + +### When to Use Progressive Loading + +**Use progressive loading when:** + +- Working with large PDFs (100+ pages) +- Initial load time is critical +- Users typically view only a few pages +- Memory usage is a concern +- Loading from network (reduces initial bandwidth) + +**Avoid progressive loading when:** + +- Working with small PDFs (< 20 pages) +- You need to process all pages immediately +- All pages will be accessed anyway +- Simplicity is preferred over optimization + +### Memory Management + +Progressive loading reduces initial memory usage but doesn't automatically unload pages. Once a page is loaded, it stays in memory until the document is disposed. For very large documents, consider: + +- Loading and processing pages in batches +- Disposing and reopening the document periodically if processing thousands of pages +- Using [`PdfViewer`](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer-class.html) which handles page lifecycle automatically + +## Example: Processing Pages with Progress Indicator + +Here's a complete example showing how to process all pages in a large PDF with a progress indicator: + +```dart +import 'package:pdfrx_engine/pdfrx_engine.dart'; + +Future processPdfPages(String filePath) async { + // Open document with progressive loading + final document = await PdfDocument.openFile( + filePath, + useProgressiveLoading: true, + ); + + try { + // Load pages progressively with progress reporting + await document.loadPagesProgressively( + onPageLoadProgress: (_, loadedCount, totalCount) { + final progress = (loadedCount / totalCount * 100).toStringAsFixed(1); + print('Loading pages: $progress% ($loadedCount/$totalCount)'); + return true; // Continue loading + }, + ); + + // Now all pages are loaded, safe to process + for (int i = 0; i < document.pages.length; i++) { + final page = document.pages[i]; + + // Extract text from page + final text = await page.loadText(); + print('Page ${i + 1}: ${text?.text.substring(0, 100)}...'); + + // Extract links + final links = await page.loadLinks(); + print('Page ${i + 1} has ${links.length} links'); + } + } finally { + await document.dispose(); + } +} +``` + +## Related Documentation + +- [Document Loading Indicator](Document-Loading-Indicator.md) - Show loading progress in UI +- [Low-Level PDFium Bindings Access](Low-Level-PDFium-Bindings-Access.md) - Advanced PDFium usage +- [pdfrx Initialization](pdfrx-Initialization.md) - Setting up pdfrx diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 00000000..812ce55f --- /dev/null +++ b/doc/README.md @@ -0,0 +1,52 @@ +# pdfrx Documentation + +Welcome to the pdfrx documentation! This guide provides comprehensive information about using the pdfrx PDF viewer plugin for Flutter. + +## Getting Started + +- [pdfrx Initialization](pdfrx-Initialization.md) - How to properly initialize pdfrx in your app +- [Progressive Loading](Progressive-Loading.md) - Understanding and using progressive page loading for large PDFs + +## Core Features + +### Text Features + +- [Text Search](Text-Search.md) - Implementing text search functionality +- [Text Selection](Text-Selection.md) - Enabling text selection + +### Navigation & Display + +- [Document Outline (Bookmarks)](Document-Outline-(a.k.a-Bookmarks).md) - Working with PDF bookmarks +- [PDF Link Handling](PDF-Link-Handling.md) - Handling links within PDFs +- [Page Layout Customization](Page-Layout-Customization.md) - Customizing page layouts +- [Double-tap to Zoom](Double-tap-to-Zoom.md) - Implementing zoom gestures + +### UI Customization + +- [Adding Page Number on Page Bottom](Adding-Page-Number-on-Page-Bottom.md) - Display page numbers +- [Document Loading Indicator](Document-Loading-Indicator.md) - Show loading progress +- [Showing Scroll Thumbs](Showing-Scroll-Thumbs.md) - Display scroll indicators +- [Dark/Night Mode Support](Dark-Night-Mode-Support.md) - Implement dark mode + +### Advanced Topics + +- [Coordinate Conversion](Coordinate-Conversion.md) - Understanding and converting between coordinate systems +- [Customizing Key Handling](Customizing-Key-Handling-on-PdfViewer.md) - Keyboard shortcuts +- [Password Protected PDFs](Deal-with-Password-Protected-PDF-Files-using-PasswordProvider.md) - Handle encrypted PDFs +- [Overlay Builders](Deal-with-viewerOverlayBuilder-and-pageOverlaysBuilder.md) - Custom overlays +- [Loading Fonts Dynamically](Loading-Fonts-Dynamically.md) - Add custom fonts +- [Low-Level PDFium Bindings Access](Low-Level-PDFium-Bindings-Access.md) - Using PDFium function directly +- [Interoperability with other PDFium Libraries](Interoperability-with-other-PDFium-Libraries.md) - Using pdfrx alongside other PDFium-based libraries + +### PDF Editing + +- [PDF Page Manipulation](PDF-Page-Manipulation.md) - Re-arrange, combine, and extract PDF pages +- [Importing Images to PDF](Importing-Images-to-PDF.md) - Convert images to PDF and insert images into PDFs + +## Platform-Specific + +- [macOS: App Sandbox]([macOS]-Deal-with-App-Sandbox.md) - macOS sandbox configuration + +## API Reference + +For detailed API documentation, visit the [pub.dev documentation](https://pub.dev/documentation/pdfrx/latest/). diff --git a/doc/Showing-Scroll-Thumbs.md b/doc/Showing-Scroll-Thumbs.md new file mode 100644 index 00000000..072277b4 --- /dev/null +++ b/doc/Showing-Scroll-Thumbs.md @@ -0,0 +1,42 @@ +# Showing Scroll Thumbs + +By default, the viewer does never show any scroll bars nor scroll thumbs. +You can add scroll thumbs by using [PdfViewerParams.viewerOverlayBuilder](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/viewerOverlayBuilder.html): + +```dart +viewerOverlayBuilder: (context, size, handleLinkTap) => [ + // Add vertical scroll thumb on viewer's right side + PdfViewerScrollThumb( + controller: controller, + orientation: ScrollbarOrientation.right, + thumbSize: const Size(40, 25), + thumbBuilder: + (context, thumbSize, pageNumber, controller) => + Container( + color: Colors.black, + // Show page number on the thumb + child: Center( + child: Text( + pageNumber.toString(), + style: const TextStyle(color: Colors.white), + ), + ), + ), + ), + // Add horizontal scroll thumb on viewer's bottom + PdfViewerScrollThumb( + controller: controller, + orientation: ScrollbarOrientation.bottom, + thumbSize: const Size(80, 30), + thumbBuilder: + (context, thumbSize, pageNumber, controller) => + Container( + color: Colors.red, + ), + ), +], +``` + +Basically, [PdfViewerParams.viewerOverlayBuilder](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/viewerOverlayBuilder.html) can be used to insert any widgets under viewer's internal [Stack](https://api.flutter.dev/flutter/widgets/Stack-class.html). + +But if you want to place many visual objects that does not interact with user, you'd better use [PdfViewerParams.pagePaintCallback](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/pagePaintCallbacks.html). \ No newline at end of file diff --git a/doc/Text-Search.md b/doc/Text-Search.md new file mode 100644 index 00000000..b974e3db --- /dev/null +++ b/doc/Text-Search.md @@ -0,0 +1,82 @@ +# Text Search + +[TextSearcher](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfTextSearcher-class.html) is just a helper class that helps you to implement text searching feature on your app. + +The following fragment illustrates the overall usage of the [TextSearcher](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfTextSearcher-class.html): + +```dart +class _MainPageState extends State { + final controller = PdfViewerController(); + // create a PdfTextSearcher and add a listener to update the GUI on search result changes + late final textSearcher = PdfTextSearcher(controller)..addListener(_update); + + void _update() { + if (mounted) { + setState(() {}); + } + } + + @override + void dispose() { + // dispose the PdfTextSearcher + textSearcher.removeListener(_update); + textSearcher.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Pdfrx example'), + ), + body: PdfViewer.asset( + 'assets/hello.pdf', + controller: controller, + params: PdfViewerParams( + // add pageTextMatchPaintCallback that paints search hit highlights + pagePaintCallbacks: [ + textSearcher.pageTextMatchPaintCallback + ], + ), + ) + ); + } + ... +} +``` + +On the fragment above, it does: + +- Create [TextSearcher](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfTextSearcher-class.html) instance +- Add a listener (Using [PdfTextSearcher.addListener](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfTextSearcher/addListener.html)) to update UI on search result change +- Add [TextSearcher.pageTextMatchPaintCallback](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfTextSearcher/pageTextMatchPaintCallback.html) to [PdfViewerParams.pagePaintCallbacks](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/pagePaintCallbacks.html) to show search matches + +Then, you can use [TextSearcher.startTextSearch](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfTextSearcher/startTextSearch.html) to search text in the PDF document: + +```dart +textSearcher.startTextSearch('hello', caseInsensitive: true); +``` + +The search starts running in background and the search progress is notified by the listener. + +There are several functions that helps you to navigate user to the search matches: + +- [TextSearcher.goToMatchOfIndex](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfTextSearcher/goToMatchOfIndex.html) to go to the match of the specified index +- [TextSearcher.goToNextMatch](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfTextSearcher/goToNextMatch.html) to go to the next match +- [TextSearcher.goToPrevMatch](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfTextSearcher/goToPrevMatch.html) to go to the previous match + +You can get the search result (even during the search running) in the list of [PdfPageTextRange](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfPageTextRange-class.html) by [PdfTextSearcher.matches](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfTextSearcher/matches.html): + +```dart +for (final match in textSearcher.matches) { + print(match.pageNumber); + ... +} +``` + +You can also cancel the background search: + +```dart +textSearcher.resetTextSearch(); +``` \ No newline at end of file diff --git a/doc/Text-Selection.md b/doc/Text-Selection.md new file mode 100644 index 00000000..ea4c26ef --- /dev/null +++ b/doc/Text-Selection.md @@ -0,0 +1,122 @@ +# Text Selection + +On pdfrx 2.1.X, text selection related parameters are moved to [PdfViewerParams.textSelectionParams](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/textSelectionParams.html). + +## Enabling/Disabling Text Selection + +Text selection feature is enabled by default and if you want to disable it, do like the following fragment: + +```dart +PdfViewer.asset( + 'assets/test.pdf', + params: PdfViewerParams( + textSelectionParams: PdfTextSelectionParams( + enabled: false, + ), + ... + ), + ... +), +``` + +## Handling Text Selection Changes + +If you want to handle text selection changes, you can use [PdfTextSelectionParams.onTextSelectionChange](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfTextSelectionParams/onTextSelectionChange.html). + +The handler function receives a parameter of [PdfTextSelection](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfTextSelection-class.html) and you can obtain the current text selection and its associated text ranges: + +```dart +PdfViewer.asset( + 'assets/test.pdf', + params: PdfViewerParams( + textSelectionParams: PdfTextSelectionParams( + onTextSelectionChange: (selections) async { + // Get the selected string + final String text = selections.getSelectedText(); + }, + ), + ... + ), + ... +), +``` + +## Programmatic Text Selection Control + +Starting from commit [941c2ab](https://github.com/espresso3389/pdfrx/commit/941c2abb3c1c608d628f0e824edfb61628768314), you can programmatically control text selection using [PdfTextSelectionDelegate](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfTextSelectionDelegate-class.html). This is particularly useful for implementing save/restore functionality for text selections (see [#513](https://github.com/espresso3389/pdfrx/issues/513)). + +### Getting Current Text Selection + +You can obtain the current text selection range using [textSelectionPointRange](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfTextSelection/textSelectionPointRange.html): + +```dart +final controller = PdfViewerController(); + +// Get the current text selection point range +final PdfTextSelectionRange? range = controller.textSelection.textSelectionPointRange; + +if (range != null) { + // Access start and end points + final PdfTextSelectionPoint start = range.start; + final PdfTextSelectionPoint end = range.end; + + // Each point contains: + // - text: The PdfPageText object for the page + // - index: The character index within that page's text + print('Selection from page ${start.text.pageNumber}, char ${start.index} ' + 'to page ${end.text.pageNumber}, char ${end.index}'); +} +``` + +### Setting/Restoring Text Selection + +You can programmatically set the text selection using [setTextSelectionPointRange](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfTextSelectionDelegate/setTextSelectionPointRange.html). To create a text selection, you need the page number and character indices: + +```dart +// The code below assumes that the controller is associated to a PdfViewer +final controller = PdfViewerController(); +... + +// First, load the page text for the target page +final page = await controller.document![pageNumber - 1]; +final pageText = await page?.loadStructuredText(); + +if (pageText != null) { + // Create selection points with page text and character indices + final startPoint = PdfTextSelectionPoint(pageText, startCharIndex); + final endPoint = PdfTextSelectionPoint(pageText, endCharIndex); + + // Create range and set the selection + final range = PdfTextSelectionRange.fromPoints(startPoint, endPoint); + await controller.textSelection.setTextSelectionPointRange(range); +} +``` + +**Note:** Text selection can span across multiple pages. The start and end points can be on different pages: + +```dart +// Example: Select from the beginning of page 1 to the end of page 3 +final startPage = await controller.document.pages[0]; +final startPageText = await startPage.loadStructuredText(); + +final endPage = await controller.document.pages[2]; +final endPageText = await endPage.loadStructuredText(); + +if (startPageText != null && endPageText != null && endPageText.fullText.isNotEmpty) { + final startPoint = PdfTextSelectionPoint(startPageText, 0); + // NOTE: The index is inclusive - it points to the last selected character. + // To select to the end of page, use (fullText.length - 1). + // This assumes the page has text (fullText.length > 0). + final endPoint = PdfTextSelectionPoint(endPageText, endPageText.fullText.length - 1); + final range = PdfTextSelectionRange.fromPoints(startPoint, endPoint); + await controller.textSelection.setTextSelectionPointRange(range); +} +``` + +After obtaining [textSelectionPointRange](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfTextSelection/textSelectionPointRange.html), you can use it with [setTextSelectionPointRange](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfTextSelectionDelegate/setTextSelectionPointRange.html) to restore the text selection unless the PDF structure is modified; i.e. page insertion/modification and so on. + +### Important Notes + +- [PdfTextSelectionPoint](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfTextSelectionPoint-class.html) represents a point in the document's text, combining a [PdfPageText](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPageText-class.html) object and a character index +- The character index in both start and end points is **inclusive**; for the end point, it points to the last selected character (not one past it) +- [PdfTextSelectionRange.fromPoints](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfTextSelectionRange/PdfTextSelectionRange.fromPoints.html) automatically ensures that `start` comes before `end`, regardless of the order you pass the points diff --git a/doc/[macOS]-Deal-with-App-Sandbox.md b/doc/[macOS]-Deal-with-App-Sandbox.md new file mode 100644 index 00000000..636a767e --- /dev/null +++ b/doc/[macOS]-Deal-with-App-Sandbox.md @@ -0,0 +1,24 @@ +# macOS App Sandbox Configuration + +For macOS, Flutter app restricts its capability by enabling [App Sandbox](https://developer.apple.com/documentation/security/app_sandbox) by default. You can change the behavior by editing your app's entitlements files depending on your configuration. + +- [`macos/Runner/Release.entitlements`](https://github.com/espresso3389/pdfrx/blob/master/packages/pdfrx/example/viewer/macos/Runner/Release.entitlements) +- [`macos/Runner/DebugProfile.entitlements`](https://github.com/espresso3389/pdfrx/blob/master/packages/pdfrx/example/viewer/macos/Runner/DebugProfile.entitlements) + +#### Deal with App Sandbox + +The easiest option to access files on your local storage (i.e. SSD or HDD), set [`com.apple.security.app-sandbox`](https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_app-sandbox) to false on your entitlements file though it is not recommended for releasing apps because it completely disables [App Sandbox](https://developer.apple.com/documentation/security/app_sandbox). + +Another option is to use [`com.apple.security.files.user-selected.read-only`](https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_files_user-selected_read-only) along with [file_selector_macos](https://pub.dev/packages/file_selector_macos). The option is better in security than the previous option. + +Anyway, the example code for the plugin illustrates how to download and preview internet hosted PDF file. It uses +[`com.apple.security.network.client`](https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_network_client) along with [flutter_cache_manager](https://pub.dev/packages/flutter_cache_manager): + +```xml + + com.apple.security.app-sandbox + + com.apple.security.network.client + + +``` \ No newline at end of file diff --git a/doc/agents/CODE-STYLE.md b/doc/agents/CODE-STYLE.md new file mode 100644 index 00000000..ef380c0f --- /dev/null +++ b/doc/agents/CODE-STYLE.md @@ -0,0 +1,50 @@ +# Code Style and Documentation + +## Code Style + +- Single quotes for strings +- 120 character line width +- Relative imports within `lib/` +- Follow flutter_lints with custom rules in `analysis_options.yaml` + +## Formatting + +```bash +dart format . +# or +flutter format . +``` + +## Documentation Guidelines + +### General Principles + +- Use proper grammar and spelling +- Use clear and concise language +- Use consistent terminology +- Use proper headings for sections +- Use code blocks for code snippets +- Use bullet points for lists +- Use backticks (`` ` ``) for code references and file/directory/path names + +### Dart Comments + +- Use `///` (dartdoc comments) for public API comments +- Use reference links for classes, enums, and functions in documentation +- Even important private APIs should have dartdoc comments + +### Markdown Files + +- Include links to issues/PRs when relevant + - Format: `[#NNN](https://github.com/espresso3389/pdfrx/issues/NNN)` +- Use links to [API reference](https://pub.dev/documentation/pdfrx/latest/pdfrx/) for public APIs +- `README.md` should provide an overview of the project, how to use it, and important notes + +### Changelog + +- Follow [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) principles +- Focus on user-facing changes, new features, bug fixes, and breaking changes +- Do NOT include implementation details +- Use sections for different versions +- Use bullet points for changes +- Update only when releasing a new version diff --git a/doc/agents/COMMANDS.md b/doc/agents/COMMANDS.md new file mode 100644 index 00000000..1969b11c --- /dev/null +++ b/doc/agents/COMMANDS.md @@ -0,0 +1,137 @@ +# Commands Reference + +## Environment Notes + +- This project uses a **pub workspace**. Running `dart pub get` in any directory fetches dependencies for all packages. +- Prefer `rg`/`rg --files` for search and discovery tasks; they are significantly faster than alternatives. + +## Windows-Specific Notes (Claude Code) + +When running on Windows, Claude Code's Bash tool runs in a POSIX-like shell environment. Be aware of these issues: + +### Path Handling + +- **Use forward slashes** or properly escaped backslashes in paths +- **Windows paths like `d:\pdfrx`** may not work directly; wrap commands with `pwsh.exe -Command "..."` +- When using `cd`, the path may fail silently; prefer running commands with full paths or use PowerShell + +### Command Execution + +```bash +# WRONG - may fail with path issues +cd d:\pdfrx\packages\pdfrx && flutter pub get + +# CORRECT - use PowerShell wrapper +pwsh.exe -Command "cd 'd:\pdfrx\packages\pdfrx'; flutter pub get" +``` + +### Git Commands + +```bash +# WRONG - cd may not work as expected +cd d:\pdfrx && git status + +# CORRECT - use -C flag for git +git -C "d:\pdfrx" status +git -C "d:\pdfrx" log --oneline -10 + +# Or use PowerShell +pwsh.exe -Command "cd 'd:\pdfrx'; git status" +``` + +### Publishing Packages + +```bash +# Use PowerShell for pub publish +pwsh.exe -Command "cd 'd:\pdfrx\packages\pdfrx'; flutter pub publish --force" +pwsh.exe -Command "cd 'd:\pdfrx\packages\pdfrx_engine'; dart pub publish --force" +``` + +### GitHub CLI (gh) + +```bash +# gh commands work directly but use proper quoting +gh issue comment 123 --repo espresso3389/pdfrx --body "Comment text here" +``` + +## Common Commands + +Commands below use standard shell syntax. On Windows with Claude Code, wrap with `pwsh.exe -Command "..."` as shown in the Windows-Specific Notes section above. + +### Flutter Plugin (packages/pdfrx) + +```bash +cd packages/pdfrx +flutter pub get +flutter analyze +flutter test +flutter format . +``` + +### Core Engine (packages/pdfrx_engine) + +```bash +cd packages/pdfrx_engine +dart pub get +dart analyze +dart test +dart format . +``` + +## Platform Builds + +```bash +cd packages/pdfrx/example/viewer +flutter run +flutter build appbundle # Android +flutter build ios # iOS +flutter build web --wasm # Web +flutter build linux # Linux +flutter build windows # Windows +flutter build macos # macOS +``` + +## FFI Bindings + +FFI bindings for PDFium are maintained in the `pdfium_dart` package and generated using `ffigen`. + +### Prerequisites + +The `ffigen` process requires LLVM/Clang: + +- **macOS**: `brew install llvm` +- **Linux (Ubuntu/Debian)**: `apt-get install libclang-dev` +- **Linux (Fedora)**: `dnf install clang-devel` +- **Windows**: Install LLVM from [llvm.org](https://releases.llvm.org/) + +### Generating Bindings + +```bash +# For pdfium_dart package +cd packages/pdfium_dart +dart test # Downloads PDFium headers automatically +dart run ffigen + +# For pdfrx_engine (if needed) +cd packages/pdfrx_engine +dart test +dart run ffigen +``` + +### On-Demand PDFium Downloads + +The `pdfium_dart` package provides a `getPdfium()` function that downloads PDFium binaries on demand. Useful for testing or CLI applications. + +## Testing + +Tests download PDFium binaries automatically for supported platforms. + +```bash +# Test pdfrx_engine +cd packages/pdfrx_engine +dart test + +# Test pdfrx Flutter plugin +cd packages/pdfrx +flutter test +``` diff --git a/doc/agents/PROJECT-STRUCTURE.md b/doc/agents/PROJECT-STRUCTURE.md new file mode 100644 index 00000000..0457cb39 --- /dev/null +++ b/doc/agents/PROJECT-STRUCTURE.md @@ -0,0 +1,89 @@ +# Project Structure + +pdfrx is a monorepo containing five packages with the following dependency hierarchy: + +``` +pdfium_dart (FFI bindings) + ├──→ pdfium_flutter (bundles PDFium binaries) + │ ↓ + └──→ pdfrx_engine (PDF API, pure Dart) + ├──→ pdfrx (Flutter widgets) ←── pdfium_flutter + └──→ pdfrx_coregraphics (alternative backend for Apple platforms) +``` + +## Packages + +### pdfium_dart (`packages/pdfium_dart/`) + +Low-level Dart FFI bindings for PDFium. + +- Pure Dart package with auto-generated FFI bindings using `ffigen` +- Provides direct access to PDFium's C API +- Includes `getPdfium()` function for on-demand PDFium binary downloads +- Used as a foundation by higher-level packages + +### pdfium_flutter (`packages/pdfium_flutter/`) + +Flutter FFI plugin for loading PDFium native libraries. + +- Bundles pre-built PDFium binaries for all Flutter platforms (Android, iOS, Windows, macOS, Linux) +- Provides utilities for loading PDFium at runtime +- Re-exports `pdfium_dart` FFI bindings + +### pdfrx_engine (`packages/pdfrx_engine/`) + +Platform-agnostic PDF rendering API built on top of PDFium. + +- Pure Dart package with no Flutter dependencies +- Depends on `pdfium_dart` for PDFium bindings +- Provides core PDF document API +- Can be used independently for non-Flutter Dart applications + +### pdfrx (`packages/pdfrx/`) + +Cross-platform PDF viewer plugin for Flutter. + +- Depends on pdfrx_engine for PDF rendering functionality +- Depends on pdfium_flutter for bundled PDFium binaries +- Provides Flutter widgets and UI components +- Supports iOS, Android, Windows, macOS, Linux, and Web +- Uses PDFium for native platforms and PDFium WASM for web platforms + +### pdfrx_coregraphics (`packages/pdfrx_coregraphics/`) + +CoreGraphics-backed renderer for iOS/macOS. + +- Experimental package using PDFKit/CoreGraphics instead of PDFium +- Drop-in replacement for Apple platforms +- iOS and macOS only + +## Platform-Specific Notes + +### iOS/macOS + +- Uses pre-built PDFium binaries from [GitHub releases](https://github.com/espresso3389/pdfrx/releases) +- CocoaPods integration via `packages/pdfium_flutter/darwin/pdfium_flutter.podspec` +- Binaries downloaded during pod install (or use Swift Package Manager) + +### Android + +- Uses CMake for native build +- Requires Android NDK +- Downloads PDFium binaries during build + +### Web + +- `packages/pdfrx/assets/pdfium.wasm` - prebuilt PDFium WASM binary +- `packages/pdfrx/assets/pdfium_worker.js` - worker script with PDFium WASM shim +- `packages/pdfrx/assets/pdfium_client.js` - API for pdfrx_engine's web implementation + +### Windows/Linux + +- CMake-based build +- Downloads PDFium binaries during build + +## Architecture Resources + +- `README.md` - High-level overview +- `packages/pdfrx_engine/README.md` - Engine internals and FFI notes +- `packages/pdfrx/README.md` - Flutter plugin structure, widgets, and overlays diff --git a/doc/agents/RELEASING.md b/doc/agents/RELEASING.md new file mode 100644 index 00000000..d705d8ec --- /dev/null +++ b/doc/agents/RELEASING.md @@ -0,0 +1,147 @@ +# Release Process + +## Important Rules for Agents + +- **Never bump versions or changelog entries preemptively** - only when explicitly releasing +- Surface blockers or uncertainties to the user before continuing a release flow +- `CHANGELOG.md` should be updated only when releasing a new version +- Skip CI/CD updates and meta-doc changes (`CLAUDE.md`, `AGENTS.md`, and `doc/agents/*md`) in changelogs unless significant + +## Release Order + +Packages must be published in dependency order: + +1. **pdfium_dart** (if changed) +2. **pdfium_flutter** (depends on pdfium_dart) +3. **pdfrx_engine** (depends on pdfium_dart) +4. **pdfrx_coregraphics** (depends on pdfrx_engine) +5. **pdfrx** (depends on pdfrx_engine, pdfium_flutter) + +## Versioning + +Follow semantic versioning: + +- **Patch** (`X.Y.Z -> X.Y.Z+1`): Bug fixes, non-breaking changes +- **Minor** (`X.Y.Z -> X.Y+1.0`): New features, breaking changes +- **Major** (`X.Y.Z -> X+1.0.0`): Major breaking changes + +## Pre-Release Checklist + +For each package being released: + +1. Update `CHANGELOG.md` with user-facing changes +2. Update version in `pubspec.yaml` +3. Update dependency versions in dependent packages +4. Update version references in `README.md` examples +5. Update the root `README.md` if necessary +6. Run dry-run: `dart pub publish --dry-run` or `flutter pub publish --dry-run` +7. Run `pana` to validate package quality + +### pdfrx-Specific Steps + +- Run tests: `dart test` in `packages/pdfrx_engine/` and `flutter test` in `packages/pdfrx/` +- Validate example app: `flutter build web --wasm` in `packages/pdfrx/example/viewer` +- Flag any WASM compatibility warnings from `pana` + +### pdfium_flutter-Specific Steps + +Update platform-specific build configurations if PDFium binaries changed: + +- `darwin/pdfium_flutter.podspec` for iOS/macOS (CocoaPods) +- `darwin/pdfium_flutter/Package.swift` for Swift Package Manager +- `android/CMakeLists.txt` for Android +- `windows/CMakeLists.txt` for Windows +- `linux/CMakeLists.txt` for Linux + +## Publishing + +### Windows (Claude Code) + +On Windows, use PowerShell wrapper for reliable path handling: + +```bash +# For Dart packages +pwsh.exe -Command "cd 'd:\pdfrx\packages\'; dart pub publish --force" + +# For Flutter packages +pwsh.exe -Command "cd 'd:\pdfrx\packages\'; flutter pub publish --force" +``` + +### Other Platforms + +```bash +# For Dart packages +cd packages/ +dart pub publish --force + +# For Flutter packages +cd packages/ +flutter pub publish --force +``` + +## Post-Release + +### Commit Changes + +Before tagging, commit all release changes: + +```bash +git add -A +git commit -m "Release pdfrx 2.2.19, pdfrx_engine 0.3.7, etc." +git push +``` + +### Git Tagging + +After publishing, create git tags for each released package and push them: + +```bash +# Tag format: -v +git tag pdfium_dart-v0.1.3 +git tag pdfium_flutter-v0.1.8 +git tag pdfrx_engine-v0.3.7 +git tag pdfrx-v2.2.19 +git tag pdfrx_coregraphics-v0.1.11 + +# Push all tags +git push --tags +``` + +### Notify Issues + +Comment on related GitHub issues/PRs once the release is live: + +```md +The FIX|UPDATE|SOMETHING for this issue has been released in v[x.y.z](https://pub.dev/packages/pdfrx/versions/x.y.z). + +...Fix/update summary... + +Written by [AGENT SIGNATURE] +``` + +Use `gh issue comment` or `gh pr comment` as appropriate. + +## Changelog Guidelines + +Follow [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) principles: + +- Focus on user-facing changes, new features, bug fixes, and breaking changes +- Do NOT include implementation details +- Link to issues/PRs: `[#NNN](https://github.com/espresso3389/pdfrx/issues/NNN)` +- Use bullet points for changes + +## Dependency Version Policy + +### pdfrx_engine + +Follows standard Dart package versioning practices. + +### pdfrx + +Intentionally does NOT specify version constraints for core Flutter-managed packages (collection, ffi, http, path, rxdart). This allows: + +- Flutter SDK to manage dependencies based on user's Flutter version +- Broader compatibility across different Flutter stable versions +- Avoiding version conflicts for users on older Flutter stable releases + +Warnings about missing version constraints during `flutter pub publish` can be safely ignored. diff --git a/doc/pdfrx-Initialization.md b/doc/pdfrx-Initialization.md new file mode 100644 index 00000000..4a268ebc --- /dev/null +++ b/doc/pdfrx-Initialization.md @@ -0,0 +1,41 @@ +# pdfrx Initialization + +If you use Flutter widgets like [PdfViewer](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer-class.html) or [PdfDocumentViewBuilder](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocumentViewBuilder-class.html), they implicitly initialize the library by calling [pdfrxFlutterInitialize](https://pub.dev/documentation/pdfrx/latest/pdfrx/pdfrxFlutterInitialize.html). + +But if you use [PdfDocument](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocument-class.html) directly, you should explicitly do either one of the following ways: + +- Call [pdfrxFlutterInitialize](https://pub.dev/documentation/pdfrx/latest/pdfrx/pdfrxFlutterInitialize.html) +- Call [pdfrxInitialize](https://pub.dev/documentation/pdfrx/latest/pdfrx/pdfrxInitialize.html) +- [Initialize things by yourself](https://github.com/espresso3389/pdfrx/wiki/pdfrx-Initialization#initialize-things-by-yourself) + +The first one is the recommended and the easiest way to initialize Flutter app. + +For pure Dart apps (or even some of Flutter apps), you can use [pdfrxInitialize](https://pub.dev/documentation/pdfrx/latest/pdfrx/pdfrxInitialize.html). + +## Initialize Things By Yourself + +Basically, these initialization functions do the following things: + +- Call [WidgetsFlutterBinding.ensureInitialized](https://api.flutter.dev/flutter/widgets/WidgetsFlutterBinding/ensureInitialized.html) (Flutter only) +- Set [Pdfrx.getCacheDirectory](https://pub.dev/documentation/pdfrx/latest/pdfrx/Pdfrx/getCacheDirectory.html) +- Map PdfDocument [factory/interop functions](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfrxEntryFunctions-class.html) to actual platform ones +- Set [Pdfrx.loadAsset](https://pub.dev/documentation/pdfrx/latest/pdfrx/Pdfrx/loadAsset.html) (Flutter only) +- Download PDFium binary on-demand ([pdfrxInitialize](https://pub.dev/documentation/pdfrx/latest/pdfrx/pdfrxInitialize.html) only) +- Call [PdfrxEntryFunctions.init](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfrxEntryFunctions/init.html) to initialize the PDFium library (internally calls `FPDF_InitLibraryWithConfig`) + +## Cache Directory + +The mechanism to locate cache directory is different between pure Dart apps and Flutter apps: + +Init. Func. | Underlying API | Notes +------------|----------------|------------------- +[pdfrxInitialize](https://pub.dev/documentation/pdfrx/latest/pdfrx/pdfrxInitialize.html) | [Directory.systemTemp](https://api.flutter.dev/flutter/dart-io/Directory/systemTemp.html) | May not be suitable for mobile apps. +[pdfrxFlutterInitialize](https://pub.dev/documentation/pdfrx/latest/pdfrx/pdfrxFlutterInitialize.html) | [path_provider.getTemporaryDirectory](https://pub.dev/documentation/path_provider/latest/path_provider/getTemporaryDirectory.html) | Always app local directory. + +## Download PDFium Binary On-Demand + +For pure Dart apps, because it is typically used on desktop environments, pdfrx downloads PDFium binary if your environment does not have it. + +- PDFium binaries are downloaded from +- By default, the binary is downloaded to `[TMP_DIR]/pdfrx.cache` +- You can explicitly specify `libpdfium` shared library file path/name by `PDFIUM_PATH` environment variable diff --git a/example/viewer/android/build.gradle b/example/viewer/android/build.gradle deleted file mode 100644 index bc157bd1..00000000 --- a/example/viewer/android/build.gradle +++ /dev/null @@ -1,18 +0,0 @@ -allprojects { - repositories { - google() - mavenCentral() - } -} - -rootProject.buildDir = '../build' -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { - project.evaluationDependsOn(':app') -} - -tasks.register("clean", Delete) { - delete rootProject.buildDir -} diff --git a/example/viewer/android/gradle.properties b/example/viewer/android/gradle.properties deleted file mode 100644 index 25971708..00000000 --- a/example/viewer/android/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError -android.useAndroidX=true -android.enableJetifier=true diff --git a/example/viewer/android/settings.gradle b/example/viewer/android/settings.gradle deleted file mode 100644 index dbf9ff3f..00000000 --- a/example/viewer/android/settings.gradle +++ /dev/null @@ -1,25 +0,0 @@ -pluginManagement { - def flutterSdkPath = { - def properties = new Properties() - file("local.properties").withInputStream { properties.load(it) } - def flutterSdkPath = properties.getProperty("flutter.sdk") - assert flutterSdkPath != null, "flutter.sdk not set in local.properties" - return flutterSdkPath - }() - - includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") - - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} - -plugins { - id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.7.0" apply false - id "org.jetbrains.kotlin.android" version "2.0.20" apply false -} - -include ":app" diff --git a/example/viewer/lib/main.dart b/example/viewer/lib/main.dart deleted file mode 100644 index d1fe7458..00000000 --- a/example/viewer/lib/main.dart +++ /dev/null @@ -1,628 +0,0 @@ -import 'package:file_selector/file_selector.dart' as fs; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:pdfrx/pdfrx.dart'; -import 'package:url_launcher/url_launcher.dart'; - -import 'markers_view.dart'; -import 'outline_view.dart'; -import 'password_dialog.dart'; -import 'search_view.dart'; -import 'thumbnails_view.dart'; - -void main() { - // NOTE: To enable bleeding-edge Pdfium WASM support on Flutter Web; - // 1. add pdfrx_wasm to your pubspec.yaml's dependencies. - // 2. uncomment the following line. - //Pdfrx.webRuntimeType = PdfrxWebRuntimeType.pdfiumWasm; - runApp(const MyApp()); -} - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - return const MaterialApp( - title: 'Pdfrx example', - home: MainPage(), - ); - } -} - -class MainPage extends StatefulWidget { - const MainPage({super.key}); - - @override - State createState() => _MainPageState(); -} - -class _MainPageState extends State with WidgetsBindingObserver { - final documentRef = ValueNotifier(null); - final controller = PdfViewerController(); - final showLeftPane = ValueNotifier(false); - final outline = ValueNotifier?>(null); - final textSearcher = ValueNotifier(null); - final _markers = >{}; - List? textSelections; - - void _update() { - if (mounted) { - setState(() {}); - } - } - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addObserver(this); - openDefaultAsset(); - } - - @override - void dispose() { - WidgetsBinding.instance.removeObserver(this); - textSearcher.value?.dispose(); - textSearcher.dispose(); - showLeftPane.dispose(); - outline.dispose(); - documentRef.dispose(); - super.dispose(); - } - - @override - void didChangeMetrics() { - super.didChangeMetrics(); - if (mounted) setState(() {}); - } - - static bool determineWhetherMobileDeviceOrNot() { - final data = MediaQueryData.fromView(WidgetsBinding.instance.platformDispatcher.views.single); - return data.size.shortestSide < 600; - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - leading: IconButton( - icon: const Icon(Icons.menu), - onPressed: () { - showLeftPane.value = !showLeftPane.value; - }, - ), - title: ValueListenableBuilder( - valueListenable: documentRef, - builder: (context, documentRef, child) { - final isMobileDevice = determineWhetherMobileDeviceOrNot(); - final visualDensity = isMobileDevice ? VisualDensity.compact : null; - return Row( - children: [ - if (!isMobileDevice) ...[ - Expanded( - child: Text(_fileName(documentRef?.sourceName) ?? 'No document loaded'), - ), - SizedBox(width: 10), - FilledButton(onPressed: () => openFile(), child: Text('Open File')), - SizedBox(width: 20), - FilledButton(onPressed: () => openUri(), child: Text('Open URL')), - Spacer(), - ], - IconButton( - visualDensity: visualDensity, - icon: const Icon( - Icons.circle, - color: Colors.red, - ), - onPressed: documentRef == null ? null : () => _addCurrentSelectionToMarkers(Colors.red), - ), - IconButton( - visualDensity: visualDensity, - icon: const Icon( - Icons.circle, - color: Colors.green, - ), - onPressed: documentRef == null ? null : () => _addCurrentSelectionToMarkers(Colors.green), - ), - IconButton( - visualDensity: visualDensity, - icon: const Icon( - Icons.circle, - color: Colors.orangeAccent, - ), - onPressed: documentRef == null ? null : () => _addCurrentSelectionToMarkers(Colors.orangeAccent), - ), - IconButton( - visualDensity: visualDensity, - icon: const Icon(Icons.zoom_in), - onPressed: documentRef == null - ? null - : () { - if (controller.isReady) controller.zoomUp(); - }, - ), - IconButton( - visualDensity: visualDensity, - icon: const Icon(Icons.zoom_out), - onPressed: documentRef == null - ? null - : () { - if (controller.isReady) controller.zoomDown(); - }, - ), - IconButton( - visualDensity: visualDensity, - icon: const Icon(Icons.first_page), - onPressed: documentRef == null - ? null - : () { - if (controller.isReady) controller.goToPage(pageNumber: 1); - }, - ), - IconButton( - visualDensity: visualDensity, - icon: const Icon(Icons.last_page), - onPressed: documentRef == null - ? null - : () { - if (controller.isReady) { - controller.goToPage(pageNumber: controller.pageCount); - } - }, - ), - ], - ); - }, - ), - ), - body: Row( - children: [ - AnimatedSize( - duration: const Duration(milliseconds: 300), - child: ValueListenableBuilder( - valueListenable: showLeftPane, - builder: (context, isLeftPaneShown, child) { - final isMobileDevice = determineWhetherMobileDeviceOrNot(); - return SizedBox( - width: isLeftPaneShown ? 300 : 0, - child: Padding( - padding: const EdgeInsets.fromLTRB(1, 0, 4, 0), - child: DefaultTabController( - length: 4, - child: Column( - children: [ - if (isMobileDevice) - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Row( - children: [ - ValueListenableBuilder( - valueListenable: documentRef, - builder: (context, documentRef, child) => Expanded( - child: Text( - _fileName(documentRef?.sourceName) ?? 'No document loaded', - softWrap: false, - ), - ), - ), - IconButton( - icon: const Icon(Icons.file_open), - onPressed: () { - showLeftPane.value = false; - openFile(); - }, - ), - IconButton( - icon: const Icon(Icons.http), - onPressed: () { - showLeftPane.value = false; - openUri(); - }, - ), - ], - ), - ), - ClipRect( - // NOTE: without ClipRect, TabBar shown even if the width is 0 - child: const TabBar(tabs: [ - Tab(icon: Icon(Icons.search), text: 'Search'), - Tab(icon: Icon(Icons.menu_book), text: 'TOC'), - Tab(icon: Icon(Icons.image), text: 'Pages'), - Tab(icon: Icon(Icons.bookmark), text: 'Markers'), - ]), - ), - Expanded( - child: TabBarView( - children: [ - ValueListenableBuilder( - valueListenable: textSearcher, - builder: (context, textSearcher, child) { - if (textSearcher == null) return SizedBox(); - return TextSearchView(textSearcher: textSearcher); - }, - ), - ValueListenableBuilder( - valueListenable: outline, - builder: (context, outline, child) => OutlineView( - outline: outline, - controller: controller, - ), - ), - ValueListenableBuilder( - valueListenable: documentRef, - builder: (context, documentRef, child) => ThumbnailsView( - documentRef: documentRef, - controller: controller, - ), - ), - MarkersView( - markers: _markers.values.expand((e) => e).toList(), - onTap: (marker) { - final rect = controller.calcRectForRectInsidePage( - pageNumber: marker.ranges.pageText.pageNumber, - rect: marker.ranges.bounds, - ); - controller.ensureVisible(rect); - }, - onDeleteTap: (marker) { - _markers[marker.ranges.pageNumber]!.remove(marker); - setState(() {}); - }, - ), - ], - ), - ), - ], - ), - ), - ), - ); - }, - ), - ), - Expanded( - child: Stack( - children: [ - ValueListenableBuilder( - valueListenable: documentRef, - builder: (context, docRef, child) { - if (docRef == null) { - return const Center( - child: Text( - 'No document loaded', - style: TextStyle(fontSize: 20), - ), - ); - } - return PdfViewer( - docRef, - // PdfViewer.asset( - // 'assets/hello.pdf', - // PdfViewer.file( - // r"D:\pdfrx\example\assets\hello.pdf", - // PdfViewer.uri( - // Uri.parse( - // 'https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf'), - // Set password provider to show password dialog - //passwordProvider: () => passwordDialog(context), - controller: controller, - params: PdfViewerParams( - enableTextSelection: true, - maxScale: 8, - // facing pages algorithm - // layoutPages: (pages, params) { - // // They should be moved outside function - // const isRightToLeftReadingOrder = false; - // const needCoverPage = true; - // final width = pages.fold( - // 0.0, (prev, page) => max(prev, page.width)); - - // final pageLayouts = []; - // double y = params.margin; - // for (int i = 0; i < pages.length; i++) { - // const offset = needCoverPage ? 1 : 0; - // final page = pages[i]; - // final pos = i + offset; - // final isLeft = isRightToLeftReadingOrder - // ? (pos & 1) == 1 - // : (pos & 1) == 0; - - // final otherSide = (pos ^ 1) - offset; - // final h = 0 <= otherSide && otherSide < pages.length - // ? max(page.height, pages[otherSide].height) - // : page.height; - - // pageLayouts.add( - // Rect.fromLTWH( - // isLeft - // ? width + params.margin - page.width - // : params.margin * 2 + width, - // y + (h - page.height) / 2, - // page.width, - // page.height, - // ), - // ); - // if (pos & 1 == 1 || i + 1 == pages.length) { - // y += h + params.margin; - // } - // } - // return PdfPageLayout( - // pageLayouts: pageLayouts, - // documentSize: Size( - // (params.margin + width) * 2 + params.margin, - // y, - // ), - // ); - // }, - // - onViewSizeChanged: (viewSize, oldViewSize, controller) { - if (oldViewSize != null) { - // - // Calculate the matrix to keep the center position during device - // screen rotation - // - // The most important thing here is that the transformation matrix - // is not changed on the view change. - final centerPosition = controller.value.calcPosition(oldViewSize); - final newMatrix = controller.calcMatrixFor(centerPosition); - // Don't change the matrix in sync; the callback might be called - // during widget-tree's build process. - Future.delayed( - const Duration(milliseconds: 200), - () => controller.goTo(newMatrix), - ); - } - }, - viewerOverlayBuilder: (context, size, handleLinkTap) => [ - // - // Example use of GestureDetector to handle custom gestures - // - // GestureDetector( - // behavior: HitTestBehavior.translucent, - // // If you use GestureDetector on viewerOverlayBuilder, it breaks link-tap handling - // // and you should manually handle it using onTapUp callback - // onTapUp: (details) { - // handleLinkTap(details.localPosition); - // }, - // onDoubleTap: () { - // controller.zoomUp(loop: true); - // }, - // // Make the GestureDetector covers all the viewer widget's area - // // but also make the event go through to the viewer. - // child: IgnorePointer( - // child: - // SizedBox(width: size.width, height: size.height), - // ), - // ), - // - // Scroll-thumbs example - // - // Show vertical scroll thumb on the right; it has page number on it - PdfViewerScrollThumb( - controller: controller, - orientation: ScrollbarOrientation.right, - thumbSize: const Size(40, 25), - thumbBuilder: (context, thumbSize, pageNumber, controller) => Container( - color: Colors.black, - child: Center( - child: Text( - pageNumber.toString(), - style: const TextStyle(color: Colors.white), - ), - ), - ), - ), - // Just a simple horizontal scroll thumb on the bottom - PdfViewerScrollThumb( - controller: controller, - orientation: ScrollbarOrientation.bottom, - thumbSize: const Size(80, 30), - thumbBuilder: (context, thumbSize, pageNumber, controller) => Container( - color: Colors.red, - ), - ), - ], - // - // Loading progress indicator example - // - loadingBannerBuilder: (context, bytesDownloaded, totalBytes) => Center( - child: CircularProgressIndicator( - value: totalBytes != null ? bytesDownloaded / totalBytes : null, - backgroundColor: Colors.grey, - ), - ), - // - // Link handling example - // - linkHandlerParams: PdfLinkHandlerParams( - onLinkTap: (link) { - if (link.url != null) { - navigateToUrl(link.url!); - } else if (link.dest != null) { - controller.goToDest(link.dest); - } - }, - ), - pagePaintCallbacks: [ - if (textSearcher.value != null) textSearcher.value!.pageTextMatchPaintCallback, - _paintMarkers, - ], - onDocumentChanged: (document) async { - if (document == null) { - textSearcher.value?.dispose(); - textSearcher.value = null; - outline.value = null; - textSelections = null; - _markers.clear(); - } - }, - onViewerReady: (document, controller) async { - outline.value = await document.loadOutline(); - textSearcher.value = PdfTextSearcher(controller)..addListener(_update); - }, - onTextSelectionChange: (selections) { - textSelections = selections; - }, - ), - ); - }), - ], - ), - ), - ], - ), - ); - } - - void _paintMarkers(Canvas canvas, Rect pageRect, PdfPage page) { - final markers = _markers[page.pageNumber]; - if (markers == null) { - return; - } - for (final marker in markers) { - final paint = Paint() - ..color = marker.color.withAlpha(100) - ..style = PaintingStyle.fill; - - for (final range in marker.ranges.ranges) { - final f = PdfTextRangeWithFragments.fromTextRange( - marker.ranges.pageText, - range.start, - range.end, - ); - if (f != null) { - canvas.drawRect( - f.bounds.toRectInPageRect(page: page, pageRect: pageRect), - paint, - ); - } - } - } - } - - void _addCurrentSelectionToMarkers(Color color) { - if (controller.isReady && textSelections != null) { - for (final selectedText in textSelections!) { - _markers.putIfAbsent(selectedText.pageNumber, () => []).add(Marker(color, selectedText)); - } - setState(() {}); - } - } - - Future navigateToUrl(Uri url) async { - if (await shouldOpenUrl(context, url)) { - await launchUrl(url); - } - } - - Future shouldOpenUrl(BuildContext context, Uri url) async { - final result = await showDialog( - context: context, - barrierDismissible: false, - builder: (context) { - return AlertDialog( - title: const Text('Navigate to URL?'), - content: SelectionArea( - child: Text.rich( - TextSpan( - children: [ - const TextSpan(text: 'Do you want to navigate to the following location?\n'), - TextSpan( - text: url.toString(), - style: const TextStyle(color: Colors.blue), - ), - ], - ), - ), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: const Text('Go'), - ), - ], - ); - }, - ); - return result ?? false; - } - - Future openDefaultAsset() async { - documentRef.value = PdfDocumentRefAsset('assets/hello.pdf'); - } - - Future openFile() async { - final file = await fs.openFile(acceptedTypeGroups: [ - fs.XTypeGroup( - label: 'PDF files', - extensions: ['pdf'], - uniformTypeIdentifiers: ['com.adobe.pdf'], - ) - ]); - if (file == null) return; - if (kIsWeb) { - final bytes = await file.readAsBytes(); - documentRef.value = PdfDocumentRefData( - bytes, - sourceName: file.name, - passwordProvider: () => passwordDialog(context), - ); - } else { - documentRef.value = PdfDocumentRefFile(file.path, passwordProvider: () => passwordDialog(context)); - } - } - - Future openUri() async { - final result = await showDialog( - context: context, - builder: (context) { - final controller = TextEditingController(); - controller.text = 'https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf'; - return AlertDialog( - title: const Text('Open URL'), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (kIsWeb) - const Text( - 'Note: The URL must be CORS-enabled.', - style: TextStyle(color: Colors.red), - ), - TextField( - controller: controller, - decoration: const InputDecoration(hintText: 'URL'), - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(controller.text), - child: const Text('Open'), - ), - ], - ); - }, - ); - if (result == null) return; - final uri = Uri.parse(result); - documentRef.value = PdfDocumentRefUri( - uri, - passwordProvider: () => passwordDialog(context), - ); - } - - static String? _fileName(String? path) { - if (path == null) return null; - final parts = path.split(RegExp(r'[\\/]')); - return parts.isEmpty ? path : parts.last; - } -} diff --git a/example/viewer/pubspec.lock b/example/viewer/pubspec.lock deleted file mode 100644 index 5a5fe49b..00000000 --- a/example/viewer/pubspec.lock +++ /dev/null @@ -1,510 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - async: - dependency: transitive - description: - name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 - url: "https://pub.dev" - source: hosted - version: "2.12.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - characters: - dependency: transitive - description: - name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - clock: - dependency: transitive - description: - name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b - url: "https://pub.dev" - source: hosted - version: "1.1.2" - collection: - dependency: transitive - description: - name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" - url: "https://pub.dev" - source: hosted - version: "1.19.1" - cross_file: - dependency: transitive - description: - name: cross_file - sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" - url: "https://pub.dev" - source: hosted - version: "0.3.4+2" - crypto: - dependency: transitive - description: - name: crypto - sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" - url: "https://pub.dev" - source: hosted - version: "3.0.6" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 - url: "https://pub.dev" - source: hosted - version: "1.0.8" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" - url: "https://pub.dev" - source: hosted - version: "1.3.2" - ffi: - dependency: transitive - description: - name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" - url: "https://pub.dev" - source: hosted - version: "2.1.3" - file_selector: - dependency: "direct main" - description: - name: file_selector - sha256: "5019692b593455127794d5718304ff1ae15447dea286cdda9f0db2a796a1b828" - url: "https://pub.dev" - source: hosted - version: "1.0.3" - file_selector_android: - dependency: transitive - description: - name: file_selector_android - sha256: "98ac58e878b05ea2fdb204e7f4fc4978d90406c9881874f901428e01d3b18fbc" - url: "https://pub.dev" - source: hosted - version: "0.5.1+12" - file_selector_ios: - dependency: transitive - description: - name: file_selector_ios - sha256: "94b98ad950b8d40d96fee8fa88640c2e4bd8afcdd4817993bd04e20310f45420" - url: "https://pub.dev" - source: hosted - version: "0.5.3+1" - file_selector_linux: - dependency: transitive - description: - name: file_selector_linux - sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" - url: "https://pub.dev" - source: hosted - version: "0.9.3+2" - file_selector_macos: - dependency: transitive - description: - name: file_selector_macos - sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" - url: "https://pub.dev" - source: hosted - version: "0.9.4+2" - file_selector_platform_interface: - dependency: transitive - description: - name: file_selector_platform_interface - sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b - url: "https://pub.dev" - source: hosted - version: "2.6.2" - file_selector_web: - dependency: transitive - description: - name: file_selector_web - sha256: c4c0ea4224d97a60a7067eca0c8fd419e708ff830e0c83b11a48faf566cec3e7 - url: "https://pub.dev" - source: hosted - version: "0.9.4+2" - file_selector_windows: - dependency: transitive - description: - name: file_selector_windows - sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4" - url: "https://pub.dev" - source: hosted - version: "0.9.3+3" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" - url: "https://pub.dev" - source: hosted - version: "5.0.0" - flutter_localizations: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - http: - dependency: transitive - description: - name: http - sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f - url: "https://pub.dev" - source: hosted - version: "1.3.0" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" - url: "https://pub.dev" - source: hosted - version: "4.1.2" - intl: - dependency: transitive - description: - name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf - url: "https://pub.dev" - source: hosted - version: "0.19.0" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec - url: "https://pub.dev" - source: hosted - version: "10.0.8" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 - url: "https://pub.dev" - source: hosted - version: "3.0.9" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" - url: "https://pub.dev" - source: hosted - version: "3.0.1" - lints: - dependency: transitive - description: - name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 - url: "https://pub.dev" - source: hosted - version: "5.1.1" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" - source: hosted - version: "0.12.17" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" - source: hosted - version: "0.11.1" - meta: - dependency: transitive - description: - name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c - url: "https://pub.dev" - source: hosted - version: "1.16.0" - path: - dependency: transitive - description: - name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" - url: "https://pub.dev" - source: hosted - version: "1.9.1" - path_provider: - dependency: transitive - description: - name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.dev" - source: hosted - version: "2.1.5" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" - url: "https://pub.dev" - source: hosted - version: "2.2.15" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" - source: hosted - version: "2.3.0" - pdfrx: - dependency: "direct main" - description: - path: "../.." - relative: true - source: path - version: "1.1.11" - platform: - dependency: transitive - description: - name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" - source: hosted - version: "3.1.6" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - rxdart: - dependency: "direct main" - description: - name: rxdart - sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" - url: "https://pub.dev" - source: hosted - version: "0.28.0" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" - url: "https://pub.dev" - source: hosted - version: "1.10.1" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" - url: "https://pub.dev" - source: hosted - version: "1.4.1" - synchronized: - dependency: "direct main" - description: - name: synchronized - sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" - url: "https://pub.dev" - source: hosted - version: "3.3.0+3" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" - url: "https://pub.dev" - source: hosted - version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd - url: "https://pub.dev" - source: hosted - version: "0.7.4" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" - source: hosted - version: "1.4.0" - url_launcher: - dependency: "direct main" - description: - name: url_launcher - sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" - url: "https://pub.dev" - source: hosted - version: "6.3.1" - url_launcher_android: - dependency: transitive - description: - name: url_launcher_android - sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" - url: "https://pub.dev" - source: hosted - version: "6.3.14" - url_launcher_ios: - dependency: transitive - description: - name: url_launcher_ios - sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" - url: "https://pub.dev" - source: hosted - version: "6.3.2" - url_launcher_linux: - dependency: transitive - description: - name: url_launcher_linux - sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" - url: "https://pub.dev" - source: hosted - version: "3.2.1" - url_launcher_macos: - dependency: transitive - description: - name: url_launcher_macos - sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" - url: "https://pub.dev" - source: hosted - version: "3.2.2" - url_launcher_platform_interface: - dependency: transitive - description: - name: url_launcher_platform_interface - sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - url_launcher_web: - dependency: transitive - description: - name: url_launcher_web - sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" - url: "https://pub.dev" - source: hosted - version: "2.4.0" - url_launcher_windows: - dependency: transitive - description: - name: url_launcher_windows - sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" - url: "https://pub.dev" - source: hosted - version: "3.1.4" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" - url: "https://pub.dev" - source: hosted - version: "14.3.1" - web: - dependency: transitive - description: - name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb - url: "https://pub.dev" - source: hosted - version: "1.1.0" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" - source: hosted - version: "1.1.0" -sdks: - dart: ">=3.7.0 <4.0.0" - flutter: ">=3.29.0" diff --git a/lib/src/pdf_api.dart b/lib/src/pdf_api.dart deleted file mode 100644 index 67787683..00000000 --- a/lib/src/pdf_api.dart +++ /dev/null @@ -1,980 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -import 'dart:async'; -import 'dart:ui' as ui; - -import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:http/http.dart' as http; - -// The trick to support Flutter Web is to use conditional import -// Both of the files define PdfDocumentFactoryImpl class but only one of them is imported. -import '../pdfrx.dart'; -import 'web/pdfrx_web.dart' if (dart.library.io) 'pdfium/pdfrx_pdfium.dart'; - -/// Class to provide Pdfrx's configuration. -/// The parameters should be set before calling any Pdfrx's functions. -abstract class Pdfrx { - /// Explicitly specify pdfium module path for special purpose. - /// - /// It is not supported on Flutter Web. - static String? pdfiumModulePath; - - /// Font paths scanned by pdfium if supported. - /// - /// It is not supported on Flutter Web. - static final fontPaths = []; - - /// Overriding the default HTTP client for PDF download. - /// - /// It is not supported on Flutter Web. - static http.Client Function()? createHttpClient; - - /// Select the Web runtime type. - static PdfrxWebRuntimeType webRuntimeType = PdfrxWebRuntimeType.pdfjs; - - /// To override the default pdfium WASM modules URL. - /// - /// It should be full - /// It is used only when on Flutter Web with [Pdfrx.webRuntimeType] is [PdfrxWebRuntimeType.pdfiumWasm]. - static String? pdfiumWasmModulesUrl; -} - -/// Web runtime type. -enum PdfrxWebRuntimeType { - /// Use PDF.js. - pdfjs, - - /// Use PDFium (WASM). - pdfiumWasm, -} - -/// For platform abstraction purpose; use [PdfDocument] instead. -abstract class PdfDocumentFactory { - /// See [PdfDocument.openAsset]. - Future openAsset( - String name, { - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - }); - - /// See [PdfDocument.openData]. - Future openData( - Uint8List data, { - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - String? sourceName, - bool allowDataOwnershipTransfer = false, - void Function()? onDispose, - }); - - /// See [PdfDocument.openFile]. - Future openFile( - String filePath, { - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - }); - - /// See [PdfDocument.openCustom]. - Future openCustom({ - required FutureOr Function(Uint8List buffer, int position, int size) read, - required int fileSize, - required String sourceName, - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - int? maxSizeToCacheOnMemory, - void Function()? onDispose, - }); - - /// See [PdfDocument.openUri]. - Future openUri( - Uri uri, { - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - PdfDownloadProgressCallback? progressCallback, - PdfDownloadReportCallback? reportCallback, - bool preferRangeAccess = false, - Map? headers, - bool withCredentials = false, - }); - - /// Singleton [PdfDocumentFactory] instance. - /// - /// It is used to switch PDFium/web implementation based on the running platform and of course, you can - /// override it to use your own implementation. - static PdfDocumentFactory instance = PdfDocumentFactoryImpl(); -} - -/// Callback function to notify download progress. -/// -/// [downloadedBytes] is the number of bytes downloaded so far. -/// [totalBytes] is the total number of bytes to download. It may be null if the total size is unknown. -typedef PdfDownloadProgressCallback = void Function(int downloadedBytes, [int? totalBytes]); - -/// Callback function to report download status on completion. -/// -/// [downloaded] is the number of bytes downloaded. -/// [total] is the total number of bytes downloaded. -/// [elapsedTime] is the time taken to download the file. -typedef PdfDownloadReportCallback = void Function(int downloaded, int total, Duration elapsedTime); - -/// Function to provide password for encrypted PDF. -/// -/// The function is called when PDF requires password. -/// It is repeatedly called until the function returns null or a valid password. -/// -/// [createSimplePasswordProvider] is a helper function to create [PdfPasswordProvider] that returns the password -/// only once. -typedef PdfPasswordProvider = FutureOr Function(); - -/// Create [PdfPasswordProvider] that returns the password only once. -/// -/// The returned [PdfPasswordProvider] returns the password only once and returns null afterwards. -/// If [password] is null, the returned [PdfPasswordProvider] returns null always. -PdfPasswordProvider createSimplePasswordProvider(String? password) { - return () { - final ret = password; - password = null; - return ret; - }; -} - -/// Handles PDF document loaded on memory. -abstract class PdfDocument { - PdfDocument({required this.sourceName}); - - /// File path, `asset:[ASSET_PATH]` or `memory:` depending on the content opened. - final String sourceName; - - /// Permission flags. - PdfPermissions? get permissions; - - /// Determine whether the PDF file is encrypted or not. - bool get isEncrypted; - - Future dispose(); - - /// Opening the specified file. - /// For Web, [filePath] can be relative path from `index.html` or any arbitrary URL but it may be restricted by CORS. - /// - /// [passwordProvider] is used to provide password for encrypted PDF. See [PdfPasswordProvider] for more info. - /// [firstAttemptByEmptyPassword] is used to determine whether the first attempt to open the PDF is by empty password - /// or not. For more info, see [PdfPasswordProvider]. - static Future openFile( - String filePath, { - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - }) => PdfDocumentFactory.instance.openFile( - filePath, - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - ); - - /// Opening the specified asset. - /// - /// [passwordProvider] is used to provide password for encrypted PDF. See [PdfPasswordProvider] for more info. - /// [firstAttemptByEmptyPassword] is used to determine whether the first attempt to open the PDF is by empty password - /// or not. For more info, see [PdfPasswordProvider]. - static Future openAsset( - String name, { - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - }) => PdfDocumentFactory.instance.openAsset( - name, - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - ); - - /// Opening the PDF on memory. - /// - /// [passwordProvider] is used to provide password for encrypted PDF. See [PdfPasswordProvider] for more info. - /// [firstAttemptByEmptyPassword] is used to determine whether the first attempt to open the PDF is by empty password - /// or not. For more info, see [PdfPasswordProvider]. - /// - /// [sourceName] must be some ID, e.g., file name or URL, to identify the source of the PDF. If [sourceName] is not - /// unique for each source, the viewer may not work correctly. - /// - /// Web only: [allowDataOwnershipTransfer] is used to determine if the data buffer can be transferred to - /// the worker thread. - static Future openData( - Uint8List data, { - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - String? sourceName, - bool allowDataOwnershipTransfer = false, - void Function()? onDispose, - }) => PdfDocumentFactory.instance.openData( - data, - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - sourceName: sourceName, - allowDataOwnershipTransfer: allowDataOwnershipTransfer, - onDispose: onDispose, - ); - - /// Opening the PDF from custom source. - /// - /// [maxSizeToCacheOnMemory] is the maximum size of the PDF to cache on memory in bytes; the custom loading process - /// may be heavy because of FFI overhead and it may be better to cache the PDF on memory if it's not too large. - /// The default size is 1MB. - /// - /// [passwordProvider] is used to provide password for encrypted PDF. See [PdfPasswordProvider] for more info. - /// [firstAttemptByEmptyPassword] is used to determine whether the first attempt to open the PDF is by empty password - /// or not. For more info, see [PdfPasswordProvider]. - /// - /// [sourceName] must be some ID, e.g., file name or URL, to identify the source of the PDF. If [sourceName] is not - /// unique for each source, the viewer may not work correctly. - static Future openCustom({ - required FutureOr Function(Uint8List buffer, int position, int size) read, - required int fileSize, - required String sourceName, - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - int? maxSizeToCacheOnMemory, - void Function()? onDispose, - }) => PdfDocumentFactory.instance.openCustom( - read: read, - fileSize: fileSize, - sourceName: sourceName, - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - maxSizeToCacheOnMemory: maxSizeToCacheOnMemory, - onDispose: onDispose, - ); - - /// Opening the PDF from URI. - /// - /// For Flutter Web, the implementation uses browser's function and restricted by CORS. - // ignore: comment_references - /// For other platforms, it uses [pdfDocumentFromUri] that uses HTTP's range request to download the file. - /// - /// [passwordProvider] is used to provide password for encrypted PDF. See [PdfPasswordProvider] for more info. - /// [firstAttemptByEmptyPassword] is used to determine whether the first attempt to open the PDF is by empty password - /// or not. For more info, see [PdfPasswordProvider]. - /// - /// [progressCallback] is called when the download progress is updated (Not supported on Web). - /// [reportCallback] is called when the download is completed (Not supported on Web). - /// [preferRangeAccess] to prefer range access to download the PDF (Not supported on Web). - /// [headers] is used to specify additional HTTP headers especially for authentication/authorization. - /// [withCredentials] is used to specify whether to include credentials in the request (Only supported on Web). - static Future openUri( - Uri uri, { - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - PdfDownloadProgressCallback? progressCallback, - PdfDownloadReportCallback? reportCallback, - bool preferRangeAccess = false, - Map? headers, - bool withCredentials = false, - }) => PdfDocumentFactory.instance.openUri( - uri, - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - progressCallback: progressCallback, - reportCallback: reportCallback, - preferRangeAccess: preferRangeAccess, - headers: headers, - withCredentials: withCredentials, - ); - - /// Pages. - List get pages; - - /// Load outline (a.k.a. bookmark). - Future> loadOutline(); - - /// Determine whether document handles are identical or not. - /// - /// It does not mean the document contents (or the document files) are identical. - bool isIdenticalDocumentHandle(Object? other); -} - -/// Handles a PDF page in [PdfDocument]. -/// -/// See [PdfDocument.pages]. -abstract class PdfPage { - /// PDF document. - PdfDocument get document; - - /// Page number. The first page is 1. - int get pageNumber; - - /// PDF page width in points (width in pixels at 72 dpi) (rotated). - double get width; - - /// PDF page height in points (height in pixels at 72 dpi) (rotated). - double get height; - - /// PDF page size in points (size in pixels at 72 dpi) (rotated). - Size get size => Size(width, height); - - /// PDF page rotation. - PdfPageRotation get rotation; - - /// Render a sub-area or full image of specified PDF file. - /// Returned image should be disposed after use. - /// [x], [y], [width], [height] specify sub-area to render in pixels. - /// [fullWidth], [fullHeight] specify virtual full size of the page to render in pixels. - /// - If [x], [y] are not specified, (0,0) is used. - /// - If [width], [height] is not specified, [fullWidth], [fullHeight] is used. - /// - If [fullWidth], [fullHeight] are not specified, [PdfPage.width] and [PdfPage.height] are used (it means rendered at 72-dpi). - /// [backgroundColor] is used to fill the background of the page. If no color is specified, [Colors.white] is used. - /// - [annotationRenderingMode] controls to render annotations or not. The default is [PdfAnnotationRenderingMode.annotationAndForms]. - /// - [cancellationToken] can be used to cancel the rendering process. It must be created by [createCancellationToken]. - /// - /// The following figure illustrates what each parameter means: - /// - /// ![image](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQwIiBoZWlnaHQ9IjM4MCIgdmlld0JveD0iMCAwIDMxMjMgMTg1MyIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBvdmVyZmxvdz0iaGlkZGVuIj48ZGVmcz48Y2xpcFBhdGggaWQ9InByZWZpeF9fYSI+PHBhdGggZD0iTTQ4MiAxNDhoMzEyM3YxODUzSDQ4MnoiLz48L2NsaXBQYXRoPjwvZGVmcz48ZyBjbGlwLXBhdGg9InVybCgjcHJlZml4X19hKSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTQ4MiAtMTQ4KSI+PHBhdGggZmlsbD0iI0ZGRiIgZD0iTTQ4MiAxNDhoMzEyM3YxODUzSDQ4MnoiLz48cGF0aCBkPSJNMTE5Ny41IDQ1MS41aDgwOC44TTY2NC41IDExODkuNWwxMzQxLjQgNTAzLjQ2IiBzdHJva2U9IiNCRkJGQkYiIHN0cm9rZS13aWR0aD0iMS4xNDYiIHN0cm9rZS1taXRlcmxpbWl0PSI4IiBzdHJva2UtZGFzaGFycmF5PSI0LjU4MyAzLjQzOCIgZmlsbD0ibm9uZSIvPjxwYXRoIHN0cm9rZT0iI0JGQkZCRiIgc3Ryb2tlLXdpZHRoPSI2Ljg3NSIgc3Ryb2tlLW1pdGVybGltaXQ9IjgiIGZpbGw9IiNEOUQ5RDkiIGQ9Ik0yMDA1LjUgNDUxLjVoMTMwNnYxMjQxaC0xMzA2eiIvPjxwYXRoIHN0cm9rZT0iI0JGQkZCRiIgc3Ryb2tlLXdpZHRoPSI2Ljg3NSIgc3Ryb2tlLW1pdGVybGltaXQ9IjgiIGZpbGw9IiNEOUQ5RDkiIGQ9Ik0yMzI2LjUgMTEzNi41aDYyMnY0MjNoLTYyMnoiLz48cGF0aCBkPSJNMjE0Ni41IDk3N2MwLTE5My4wMjQgMjIyLjUxLTM0OS41IDQ5Ny0zNDkuNXM0OTcgMTU2LjQ3NiA0OTcgMzQ5LjVjMCAxOTMuMDItMjIyLjUxIDM0OS41LTQ5NyAzNDkuNXMtNDk3LTE1Ni40OC00OTctMzQ5LjV6IiBmaWxsPSIjRjJGMkYyIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiLz48cGF0aCBkPSJNMjQzMi41MSA4NzIuNDczYzAtMjAuMTA2IDIzLjE3LTM2LjQwNiA1MS43Ny0zNi40MDYgMjguNTkgMCA1MS43NyAxNi4zIDUxLjc3IDM2LjQwNiAwIDIwLjEwNy0yMy4xOCAzNi40MDctNTEuNzcgMzYuNDA3LTI4LjYgMC01MS43Ny0xNi4zLTUxLjc3LTM2LjQwN20zMTguNDQgMGMwLTIwLjEwNiAyMy4xOC0zNi40MDYgNTEuNzctMzYuNDA2IDI4LjYgMCA1MS43NyAxNi4zIDUxLjc3IDM2LjQwNiAwIDIwLjEwNy0yMy4xNyAzNi40MDctNTEuNzcgMzYuNDA3LTI4LjU5IDAtNTEuNzctMTYuMy01MS43Ny0zNi40MDciIHN0cm9rZT0iI0JGQkZCRiIgc3Ryb2tlLXdpZHRoPSI2Ljg3NSIgc3Ryb2tlLW1pdGVybGltaXQ9IjgiIGZpbGw9IiNDM0MzQzMiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjxwYXRoIGQ9Ik0yMzc0LjEyIDExMjkuNDJjMTc5LjU5IDg2LjczIDM1OC45NiA4Ni43MyA1MzguMTMgME0yMTQ2LjUgOTc3YzAtMTkzLjAyNCAyMjIuNTEtMzQ5LjUgNDk3LTM0OS41czQ5NyAxNTYuNDc2IDQ5NyAzNDkuNWMwIDE5My4wMi0yMjIuNTEgMzQ5LjUtNDk3IDM0OS41cy00OTctMTU2LjQ4LTQ5Ny0zNDkuNXoiIHN0cm9rZT0iI0JGQkZCRiIgc3Ryb2tlLXdpZHRoPSI2Ljg3NSIgc3Ryb2tlLW1pdGVybGltaXQ9IjgiIGZpbGw9Im5vbmUiLz48cGF0aCBkPSJNMjAyOC40MiAxNzI5LjA2aDEyNTkuMzZ2Ni44OEgyMDI4LjQyem00LjU4IDE3LjE5bC0yNy41LTEzLjc1IDI3LjUtMTMuNzV6bTEyNTAuMTktMjcuNWwyNy41IDEzLjc1LTI3LjUgMTMuNzV6Ii8+PHRleHQgZm9udC1mYW1pbHk9IkNvbnNvbGFzLHNhbnMtc2VyaWYiIGZvbnQtd2VpZ2h0PSI0MDAiIGZvbnQtc2l6ZT0iODMiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDI0NTIgMTgwNSkiPmZ1bGxXaWR0aDwvdGV4dD48cGF0aCBkPSJNMzM2Mi45NCA0NzQuNDE2VjE2NzAuMzJoLTYuODhWNDc0LjQxNnpNMzM0NS43NSA0NzlsMTMuNzUtMjcuNSAxMy43NSAyNy41em0yNy41IDExODYuNzNsLTEzLjc1IDI3LjUtMTMuNzUtMjcuNXoiLz48dGV4dCBmb250LWZhbWlseT0iQ29uc29sYXMsc2Fucy1zZXJpZiIgZm9udC13ZWlnaHQ9IjQwMCIgZm9udC1zaXplPSI4MyIgdHJhbnNmb3JtPSJyb3RhdGUoOTAgMTI2Ni4xMDUgMjEwOS4xMDUpIj5mdWxsSGVpZ2h0PC90ZXh0PjxwYXRoIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSI2Ljg3NSIgc3Ryb2tlLW1pdGVybGltaXQ9IjgiIGZpbGw9IiNGRkYiIGZpbGwtb3BhY2l0eT0iLjI1OSIgZD0iTTIwNzguNSA1MzMuNWg3NTV2NjY2aC03NTV6Ii8+PHBhdGggZD0iTTIxMDEuNDIgMTIzMC4wNmg3MDkuMzZ2Ni44OGgtNzA5LjM2em00LjU4IDE3LjE5bC0yNy41LTEzLjc1IDI3LjUtMTMuNzV6bTcwMC4xOS0yNy41bDI3LjUgMTMuNzUtMjcuNSAxMy43NXpNMjg3My45NCA1NTYuNDE3djYyMC41MTNoLTYuODhWNTU2LjQxN3pNMjg1Ni43NSA1NjFsMTMuNzUtMjcuNSAxMy43NSAyNy41em0yNy41IDYxMS4zNWwtMTMuNzUgMjcuNS0xMy43NS0yNy41eiIvPjx0ZXh0IGZvbnQtZmFtaWx5PSJDb25zb2xhcyxzYW5zLXNlcmlmIiBmb250LXdlaWdodD0iNDAwIiBmb250LXNpemU9IjgzIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyMzQwLjk1IDEzMTApIj53aWR0aDwvdGV4dD48dGV4dCBmb250LWZhbWlseT0iQ29uc29sYXMsc2Fucy1zZXJpZiIgZm9udC13ZWlnaHQ9IjQwMCIgZm9udC1zaXplPSI4MyIgdHJhbnNmb3JtPSJyb3RhdGUoOTAgMTA2NS4zNTUgMTgyOS4zNTUpIj5oZWlnaHQ8L3RleHQ+PHBhdGggc3Ryb2tlPSIjQkZCRkJGIiBzdHJva2Utd2lkdGg9IjYuODc1IiBzdHJva2UtbWl0ZXJsaW1pdD0iOCIgZmlsbD0iI0Q5RDlEOSIgZD0iTTY2NC41IDQ1MS41aDUzM3Y3MzhoLTUzM3oiLz48cGF0aCBzdHJva2U9IiNCRkJGQkYiIHN0cm9rZS13aWR0aD0iNi44NzUiIHN0cm9rZS1taXRlcmxpbWl0PSI4IiBmaWxsPSIjRDlEOUQ5IiBkPSJNNzk1LjUgODU4LjVoMjU0djI1MmgtMjU0eiIvPjxwYXRoIGQ9Ik03MjEuNSA3NjRjMC0xMTQuNTk5IDkwLjg4Ni0yMDcuNSAyMDMtMjA3LjUgMTEyLjExIDAgMjAzIDkyLjkwMSAyMDMgMjA3LjVzLTkwLjg5IDIwNy41LTIwMyAyMDcuNWMtMTEyLjExNCAwLTIwMy05Mi45MDEtMjAzLTIwNy41eiIgZmlsbD0iI0YyRjJGMiIgZmlsbC1ydWxlPSJldmVub2RkIi8+PHBhdGggZD0iTTgzOC4zMTkgNzAxLjk0MmMwLTExLjkzNyA5LjQ2Ny0yMS42MTUgMjEuMTQ2LTIxLjYxNXMyMS4xNDYgOS42NzggMjEuMTQ2IDIxLjYxNWMwIDExLjkzNy05LjQ2NyAyMS42MTUtMjEuMTQ2IDIxLjYxNXMtMjEuMTQ2LTkuNjc4LTIxLjE0Ni0yMS42MTVtMTMwLjA3IDBjMC0xMS45MzcgOS40NjgtMjEuNjE1IDIxLjE0Ni0yMS42MTUgMTEuNjc1IDAgMjEuMTQ1IDkuNjc4IDIxLjE0NSAyMS42MTUgMCAxMS45MzctOS40NyAyMS42MTUtMjEuMTQ1IDIxLjYxNS0xMS42NzggMC0yMS4xNDYtOS42NzgtMjEuMTQ2LTIxLjYxNSIgc3Ryb2tlPSIjQkZCRkJGIiBzdHJva2Utd2lkdGg9IjYuODc1IiBzdHJva2UtbWl0ZXJsaW1pdD0iOCIgZmlsbD0iI0MzQzNDMyIgZmlsbC1ydWxlPSJldmVub2RkIi8+PHBhdGggZD0iTTgxNC40NzMgODU0LjQ5MmM3My4zNTEgNTEuNDkzIDE0Ni42MTcgNTEuNDkzIDIxOS43OTcgME03MjEuNSA3NjRjMC0xMTQuNTk5IDkwLjg4Ni0yMDcuNSAyMDMtMjA3LjUgMTEyLjExIDAgMjAzIDkyLjkwMSAyMDMgMjA3LjVzLTkwLjg5IDIwNy41LTIwMyAyMDcuNWMtMTEyLjExNCAwLTIwMy05Mi45MDEtMjAzLTIwNy41eiIgc3Ryb2tlPSIjQkZCRkJGIiBzdHJva2Utd2lkdGg9IjYuODc1IiBzdHJva2UtbWl0ZXJsaW1pdD0iOCIgZmlsbD0ibm9uZSIvPjx0ZXh0IGZvbnQtZmFtaWx5PSJDb25zb2xhcyxzYW5zLXNlcmlmIiBmb250LXdlaWdodD0iNDAwIiBmb250LXNpemU9IjgzIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyMDg4LjEzIDYwOCkiPig8L3RleHQ+PHRleHQgZm9udC1mYW1pbHk9IkNvbnNvbGFzLHNhbnMtc2VyaWYiIGZvbnQtd2VpZ2h0PSI0MDAiIGZvbnQtc2l6ZT0iODMiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIxMzMuMzkgNjA4KSI+eCx5PC90ZXh0Pjx0ZXh0IGZvbnQtZmFtaWx5PSJDb25zb2xhcyxzYW5zLXNlcmlmIiBmb250LXdlaWdodD0iNDAwIiBmb250LXNpemU9IjgzIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyMjY5LjE3IDYwOCkiPik8L3RleHQ+PHBhdGggZD0iTTIwNjEgNTM0YzAtOS45NDEgOC41MS0xOCAxOS0xOHMxOSA4LjA1OSAxOSAxOC04LjUxIDE4LTE5IDE4LTE5LTguMDU5LTE5LTE4eiIgZmlsbC1ydWxlPSJldmVub2RkIi8+PHBhdGggZD0iTTExOTcuNSAxMTg5LjVsMjExNCA1MDMuNDYiIHN0cm9rZT0iI0JGQkZCRiIgc3Ryb2tlLXdpZHRoPSIxLjE0NiIgc3Ryb2tlLW1pdGVybGltaXQ9IjgiIHN0cm9rZS1kYXNoYXJyYXk9IjQuNTgzIDMuNDM4IiBmaWxsPSJub25lIi8+PHRleHQgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtd2VpZ2h0PSI0MDAiIGZvbnQtc2l6ZT0iODMiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDc3NS4xMSA0MDkpIj5PcmlnaW5hbDwvdGV4dD48dGV4dCBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC13ZWlnaHQ9IjQwMCIgZm9udC1zaXplPSI4MyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjU0Ny42OSA0MDYpIj5TY2FsZWQ8L3RleHQ+PHBhdGggZD0iTTEyNDMuOTQgNDc0LjQxN3Y2OTIuNDMzaC02Ljg4VjQ3NC40MTd6TTEyMjYuNzUgNDc5bDEzLjc1LTI3LjUgMTMuNzUgMjcuNXptMjcuNSA2ODMuMjdsLTEzLjc1IDI3LjUtMTMuNzUtMjcuNXoiLz48dGV4dCBmb250LWZhbWlseT0iQ29uc29sYXMsc2Fucy1zZXJpZiIgZm9udC13ZWlnaHQ9IjQwMCIgZm9udC1zaXplPSI4MyIgdHJhbnNmb3JtPSJyb3RhdGUoOTAgMzQwLjQwNSA5MzQuNDA1KSI+cGFnZS5oZWlnaHQ8L3RleHQ+PHBhdGggZD0iTTY4Ny40MTcgMTIyNC4wNmg0ODYuNzYzdjYuODhINjg3LjQxN3ptNC41ODMgMTcuMTlsLTI3LjUtMTMuNzUgMjcuNS0xMy43NXptNDc3LjU5LTI3LjVsMjcuNSAxMy43NS0yNy41IDEzLjc1eiIvPjx0ZXh0IGZvbnQtZmFtaWx5PSJDb25zb2xhcyxzYW5zLXNlcmlmIiBmb250LXdlaWdodD0iNDAwIiBmb250LXNpemU9IjgzIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSg3MDEuNDUxIDEyOTYpIj5wYWdlLndpZHRoPC90ZXh0PjwvZz48L3N2Zz4=) - /// - /// The following code extract the area of (20,30)-(120,130) from the page image rendered at 1000x1500 pixels: - /// ```dart - /// final image = await page.render( - /// x: 20, - /// y: 30, - /// width: 100, - /// height: 100, - /// fullWidth: 1000, - /// fullHeight: 1500, - /// ); - /// ``` - Future render({ - int x = 0, - int y = 0, - int? width, - int? height, - double? fullWidth, - double? fullHeight, - Color? backgroundColor, - PdfAnnotationRenderingMode annotationRenderingMode = PdfAnnotationRenderingMode.annotationAndForms, - PdfPageRenderCancellationToken? cancellationToken, - }); - - /// Create [PdfPageRenderCancellationToken] to cancel the rendering process. - PdfPageRenderCancellationToken createCancellationToken(); - - /// Load text. - Future loadText(); - - /// Load links. - /// - /// if [compact] is true, it tries to reduce memory usage by compacting the link data. - /// See [PdfLink.compact] for more info. - Future> loadLinks({bool compact = false}); -} - -/// Page rotation. -enum PdfPageRotation { none, clockwise90, clockwise180, clockwise270 } - -/// Annotation rendering mode. -/// - [none]: Do not render annotations. -/// - [annotation]: Render annotations. -/// - [annotationAndForms]: Render annotations and forms. -enum PdfAnnotationRenderingMode { none, annotation, annotationAndForms } - -/// Token to try to cancel the rendering process. -abstract class PdfPageRenderCancellationToken { - /// Cancel the rendering process. - void cancel(); - - /// Determine whether the rendering process is canceled or not. - bool get isCanceled; -} - -/// PDF permissions defined on PDF 32000-1:2008, Table 22. -class PdfPermissions { - const PdfPermissions(this.permissions, this.securityHandlerRevision); - - /// User access permissions on on PDF 32000-1:2008, Table 22. - final int permissions; - - /// Security handler revision. - final int securityHandlerRevision; - - /// Determine whether the PDF file allows copying of the contents. - bool get allowsCopying => (permissions & 4) != 0; - - /// Determine whether the PDF file allows document assembly. - bool get allowsDocumentAssembly => (permissions & 8) != 0; - - /// Determine whether the PDF file allows printing of the pages. - bool get allowsPrinting => (permissions & 16) != 0; - - /// Determine whether the PDF file allows modifying annotations, form fields, and their associated - bool get allowsModifyAnnotations => (permissions & 32) != 0; -} - -/// Image rendered from PDF page. -/// -/// See [PdfPage.render]. -abstract class PdfImage { - /// Number of pixels in horizontal direction. - int get width; - - /// Number of pixels in vertical direction. - int get height; - - /// Pixel format in either [ui.PixelFormat.rgba8888] or [ui.PixelFormat.bgra8888]. - ui.PixelFormat get format; - - /// Raw pixel data. The actual format is platform dependent. - Uint8List get pixels; - - /// Dispose the image. - void dispose(); - - /// Create [ui.Image] from the rendered image. - Future createImage() { - final comp = Completer(); - ui.decodeImageFromPixels(pixels, width, height, format, (image) => comp.complete(image)); - return comp.future; - } -} - -/// Handles text extraction from PDF page. -/// -/// See [PdfPage.loadText]. -abstract class PdfPageText { - /// Page number. The first page is 1. - int get pageNumber; - - /// Full text of the page. - String get fullText; - - /// Get text fragments that organizes the full text structure. - /// - /// The [fullText] is the composed result of all fragments' text. - /// Any character in [fullText] must be included in one of the fragments. - List get fragments; - - /// Find text fragment index for the specified text index. - /// - /// If the specified text index is out of range, it returns -1. - int getFragmentIndexForTextIndex(int textIndex) { - final index = fragments.lowerBound(_PdfPageTextFragmentForSearch(textIndex), (a, b) => a.index - b.index); - if (index > fragments.length) { - return -1; // range error - } - if (index == fragments.length) { - final f = fragments.last; - if (textIndex >= f.index + f.length) { - return -1; // range error - } - return index - 1; - } - - final f = fragments[index]; - if (textIndex < f.index) { - return index - 1; - } - return index; - } - - /// Search text with [pattern]. - /// - /// Just work like [Pattern.allMatches] but it returns stream of [PdfTextRangeWithFragments]. - /// [caseInsensitive] is used to specify case-insensitive search only if [pattern] is [String]. - Stream allMatches(Pattern pattern, {bool caseInsensitive = true}) async* { - final String text; - if (pattern is RegExp) { - caseInsensitive = pattern.isCaseSensitive; - text = fullText; - } else if (pattern is String) { - pattern = caseInsensitive ? pattern.toLowerCase() : pattern; - text = caseInsensitive ? fullText.toLowerCase() : fullText; - } else { - throw ArgumentError.value(pattern, 'pattern'); - } - final matches = pattern.allMatches(text); - for (final match in matches) { - if (match.start == match.end) continue; - final m = PdfTextRangeWithFragments.fromTextRange(this, match.start, match.end); - if (m != null) { - yield m; - } - } - } -} - -/// Text fragment in PDF page. -abstract class PdfPageTextFragment { - /// Fragment's index on [PdfPageText.fullText]; [text] is the substring of [PdfPageText.fullText] at [index]. - int get index; - - /// Length of the text fragment. - int get length; - - /// End index of the text fragment on [PdfPageText.fullText]. - int get end => index + length; - - /// Bounds of the text fragment in PDF page coordinates. - PdfRect get bounds; - - /// Fragment's child character bounding boxes in PDF page coordinates if available. - List? get charRects; - - /// Text for the fragment. - String get text; - - @override - bool operator ==(covariant PdfPageTextFragment other) { - if (identical(this, other)) return true; - - return other.index == index && - other.bounds == bounds && - listEquals(other.charRects, charRects) && - other.text == text; - } - - @override - int get hashCode => index.hashCode ^ bounds.hashCode ^ text.hashCode; - - /// Create a [PdfPageTextFragment]. - static PdfPageTextFragment fromParams( - int index, - int length, - PdfRect bounds, - String text, { - List? charRects, - }) => _PdfPageTextFragment(index, length, bounds, text, charRects: charRects); -} - -class _PdfPageTextFragment extends PdfPageTextFragment { - _PdfPageTextFragment(this.index, this.length, this.bounds, this.text, {this.charRects}); - - @override - final int index; - @override - final int length; - @override - final PdfRect bounds; - @override - final List? charRects; - @override - final String text; -} - -/// Used only for searching fragments with [lowerBound]. -class _PdfPageTextFragmentForSearch extends PdfPageTextFragment { - _PdfPageTextFragmentForSearch(this.index); - @override - final int index; - @override - int get length => throw UnimplementedError(); - @override - PdfRect get bounds => throw UnimplementedError(); - @override - String get text => throw UnimplementedError(); - @override - List? get charRects => null; -} - -/// Simple text range in a PDF page. -/// -/// The text range is used to describe text selection in a page but it does not indicate the actual page text; -/// [PdfTextRanges] contains multiple [PdfTextRange]s and the actual [PdfPageText] the ranges are associated with. -class PdfTextRange { - const PdfTextRange({required this.start, required this.end}); - - /// Text start index in [PdfPageText.fullText]. - final int start; - - /// Text end index in [PdfPageText.fullText]. - final int end; - - PdfTextRange copyWith({int? start, int? end}) => PdfTextRange(start: start ?? this.start, end: end ?? this.end); - - @override - int get hashCode => start ^ end; - - @override - bool operator ==(Object other) { - return other is PdfTextRange && other.start == start && other.end == end; - } - - @override - String toString() => '[$start $end]'; - - /// Convert to [PdfTextRangeWithFragments]. - /// - /// The method is used to convert [PdfTextRange] to [PdfTextRangeWithFragments] using [PdfPageText]. - PdfTextRangeWithFragments? toTextRangeWithFragments(PdfPageText pageText) => - PdfTextRangeWithFragments.fromTextRange(pageText, start, end); -} - -/// Text ranges in a PDF page typically used to describe text selection. -class PdfTextRanges { - /// Create a [PdfTextRanges]. - const PdfTextRanges({required this.pageText, required this.ranges}); - - /// Create a [PdfTextRanges] with empty ranges. - PdfTextRanges.createEmpty(this.pageText) : ranges = []; - - /// The page text the text ranges are associated with. - final PdfPageText pageText; - - /// Text ranges. - final List ranges; - - /// Determine whether the text ranges are empty. - bool get isEmpty => ranges.isEmpty; - - /// Determine whether the text ranges are *NOT* empty. - bool get isNotEmpty => ranges.isNotEmpty; - - /// Page number of the text ranges. - int get pageNumber => pageText.pageNumber; - - /// Bounds of the text ranges. - PdfRect get bounds => ranges.map((r) => r.toTextRangeWithFragments(pageText)!.bounds).boundingRect(); - - /// The composed text of the text ranges. - String get text => ranges.map((r) => pageText.fullText.substring(r.start, r.end)).join(); -} - -/// For backward compatibility; [PdfTextRangeWithFragments] is previously named [PdfTextMatch]. -typedef PdfTextMatch = PdfTextRangeWithFragments; - -/// Text range (start/end index) in PDF page and it's associated text and bounding rectangle. -class PdfTextRangeWithFragments { - PdfTextRangeWithFragments(this.pageNumber, this.fragments, this.start, this.end, this.bounds); - - /// Page number of the page. - final int pageNumber; - - /// Fragments that contains the text. - final List fragments; - - /// In-fragment text start index on the first fragment. - final int start; - - /// In-fragment text end index on the last fragment. - final int end; - - /// Bounding rectangle of the text. - final PdfRect bounds; - - /// Create [PdfTextRangeWithFragments] from text range in [PdfPageText]. - /// - /// When you implement search-to-highlight feature, the most easiest way is to use [PdfTextSearcher] but you can - /// of course implement your own search algorithm and use this method to create [PdfTextRangeWithFragments]: - /// - /// ```dart - /// PdfPageText pageText = ...; - /// final searchPattern = 'search text'; - /// final textIndex = pageText.fullText.indexOf(searchPattern); - /// if (textIndex >= 0) { - /// final range = PdfTextRangeWithFragments.fromTextRange(pageText, textIndex, textIndex + searchPattern.length); - /// ... - /// } - /// ``` - /// - /// To paint text highlights on PDF pages, see [PdfViewerParams.pagePaintCallbacks] and [PdfViewerPagePaintCallback]. - static PdfTextRangeWithFragments? fromTextRange(PdfPageText pageText, int start, int end) { - if (start >= end) { - return null; - } - final s = pageText.getFragmentIndexForTextIndex(start); - final sf = pageText.fragments[s]; - if (start + 1 == end) { - return PdfTextRangeWithFragments( - pageText.pageNumber, - [pageText.fragments[s]], - start - sf.index, - end - sf.index, - sf.bounds, - ); - } - - final l = pageText.getFragmentIndexForTextIndex(end - 1); - if (s == l) { - if (sf.charRects == null) { - return PdfTextRangeWithFragments( - pageText.pageNumber, - [pageText.fragments[s]], - start - sf.index, - end - sf.index, - sf.bounds, - ); - } else { - return PdfTextRangeWithFragments( - pageText.pageNumber, - [pageText.fragments[s]], - start - sf.index, - end - sf.index, - sf.charRects!.skip(start - sf.index).take(end - start).boundingRect(), - ); - } - } - - var bounds = sf.charRects != null ? sf.charRects!.skip(start - sf.index).boundingRect() : sf.bounds; - for (int i = s + 1; i < l; i++) { - bounds = bounds.merge(pageText.fragments[i].bounds); - } - final lf = pageText.fragments[l]; - bounds = bounds.merge(lf.charRects != null ? lf.charRects!.take(end - lf.index).boundingRect() : lf.bounds); - - return PdfTextRangeWithFragments( - pageText.pageNumber, - pageText.fragments.sublist(s, l + 1), - start - sf.index, - end - lf.index, - bounds, - ); - } - - @override - int get hashCode => pageNumber ^ start ^ end; - - @override - bool operator ==(Object other) { - return other is PdfTextRangeWithFragments && - other.pageNumber == pageNumber && - other.start == start && - other.end == end && - other.bounds == bounds && - listEquals(other.fragments, fragments); - } -} - -/// Rectangle in PDF page coordinates. -/// -/// Please note that PDF page coordinates is different from Flutter's coordinate. -/// PDF page coordinates's origin is at the bottom-left corner and Y-axis is pointing upward; -/// [bottom] is generally smaller than [top]. -/// The unit is normally in points (1/72 inch). -@immutable -class PdfRect { - const PdfRect(this.left, this.top, this.right, this.bottom); - - /// Left coordinate. - final double left; - - /// Top coordinate (bigger than [bottom]). - final double top; - - /// Right coordinate. - final double right; - - /// Bottom coordinate (smaller than [top]). - final double bottom; - - /// Determine whether the rectangle is empty. - bool get isEmpty => left >= right || top <= bottom; - - /// Determine whether the rectangle is *NOT* empty. - bool get isNotEmpty => !isEmpty; - - /// Width of the rectangle. - double get width => right - left; - - /// Height of the rectangle. - double get height => top - bottom; - - /// Merge two rectangles. - PdfRect merge(PdfRect other) { - return PdfRect( - left < other.left ? left : other.left, - top > other.top ? top : other.top, - right > other.right ? right : other.right, - bottom < other.bottom ? bottom : other.bottom, - ); - } - - /// Determine whether the rectangle contains the specified point (in the PDF page coordinates). - bool contains(double x, double y) => x >= left && x <= right && y >= bottom && y <= top; - - /// Determine whether the rectangle contains the specified point (in the PDF page coordinates). - bool containsOffset(Offset offset) => contains(offset.dx, offset.dy); - - /// Empty rectangle. - static const empty = PdfRect(0, 0, 0, 0); - - /// Convert to [Rect] in Flutter coordinate. - /// [page] is the page to convert the rectangle. - /// [scaledPageSize] is the scaled page size to scale the rectangle. If not specified, [PdfPage.size] is used. - /// [rotation] is the rotation of the page. If not specified, [PdfPage.rotation] is used. - Rect toRect({required PdfPage page, Size? scaledPageSize, int? rotation}) { - final rotated = rotate(rotation ?? page.rotation.index, page); - final scale = scaledPageSize == null ? 1.0 : scaledPageSize.height / page.height; - return Rect.fromLTRB( - rotated.left * scale, - (page.height - rotated.top) * scale, - rotated.right * scale, - (page.height - rotated.bottom) * scale, - ); - } - - /// Convert to [Rect] in Flutter coordinate using [pageRect] as the page's bounding rectangle. - Rect toRectInPageRect({required PdfPage page, required Rect pageRect}) => - toRect(page: page, scaledPageSize: pageRect.size).translate(pageRect.left, pageRect.top); - - PdfRect rotate(int rotation, PdfPage page) { - final swap = (page.rotation.index & 1) == 1; - final width = swap ? page.height : page.width; - final height = swap ? page.width : page.height; - switch (rotation & 3) { - case 0: - return this; - case 1: - return PdfRect(bottom, width - left, top, width - right); - case 2: - return PdfRect(width - right, height - bottom, width - left, height - top); - case 3: - return PdfRect(height - top, right, height - bottom, left); - default: - throw ArgumentError.value(rotate, 'rotate'); - } - } - - PdfRect inflate(double dx, double dy) => PdfRect(left - dx, top + dy, right + dx, bottom - dy); - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is PdfRect && other.left == left && other.top == top && other.right == right && other.bottom == bottom; - } - - @override - int get hashCode => left.hashCode ^ top.hashCode ^ right.hashCode ^ bottom.hashCode; - - @override - String toString() { - return 'PdfRect(left: $left, top: $top, right: $right, bottom: $bottom)'; - } -} - -/// Extension methods for List of [PdfRect]. -extension PdfRectsExt on Iterable { - /// Merge all rectangles to calculate bounding rectangle. - PdfRect boundingRect() { - var left = double.infinity; - var top = double.negativeInfinity; - var right = double.negativeInfinity; - var bottom = double.infinity; - for (final r in this) { - if (r.left < left) { - left = r.left; - } - if (r.top > top) { - top = r.top; - } - if (r.right > right) { - right = r.right; - } - if (r.bottom < bottom) { - bottom = r.bottom; - } - } - if (left == double.infinity) { - // no rects - throw StateError('No rects'); - } - return PdfRect(left, top, right, bottom); - } -} - -/// PDF [Explicit Destination](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf#page=374) the page and inner-page location to jump to. -@immutable -class PdfDest { - const PdfDest(this.pageNumber, this.command, this.params); - - /// Page number to jump to. - final int pageNumber; - - /// Destination command. - final PdfDestCommand command; - - /// Destination parameters. For more info, see [PdfDestCommand]. - final List? params; - - @override - String toString() => 'PdfDest{pageNumber: $pageNumber, command: $command, params: $params}'; - - /// Compact the destination. - /// - /// The method is used to compact the destination to reduce memory usage. - /// [params] is typically growable and also modifiable. The method ensures that [params] is unmodifiable. - PdfDest compact() { - return params == null ? this : PdfDest(pageNumber, command, List.unmodifiable(params!)); - } -} - -/// [PDF 32000-1:2008, 12.3.2.2 Explicit Destinations, Table 151](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf#page=374) -enum PdfDestCommand { - unknown('unknown'), - xyz('xyz'), - fit('fit'), - fitH('fith'), - fitV('fitv'), - fitR('fitr'), - fitB('fitb'), - fitBH('fitbh'), - fitBV('fitbv'); - - const PdfDestCommand(this.name); - - final String name; - - factory PdfDestCommand.parse(String name) { - final nameLow = name.toLowerCase(); - return PdfDestCommand.values.firstWhere((e) => e.name == nameLow, orElse: () => PdfDestCommand.unknown); - } -} - -/// Link in PDF page. -/// -/// Either one of [url] or [dest] is valid (not null). -/// See [PdfPage.loadLinks]. -@immutable -class PdfLink { - const PdfLink(this.rects, {this.url, this.dest}); - - /// Link URL. - final Uri? url; - - /// Link destination. - /// - /// Link destination (link to page). - final PdfDest? dest; - - /// Link location. - final List rects; - - /// Compact the link. - /// - /// The method is used to compact the link to reduce memory usage. - /// [rects] is typically growable and also modifiable. The method ensures that [rects] is unmodifiable. - /// [dest] is also compacted by calling [PdfDest.compact]. - PdfLink compact() { - return PdfLink(List.unmodifiable(rects), url: url, dest: dest?.compact()); - } -} - -/// Outline (a.k.a. Bookmark) node in PDF document. -/// -/// See [PdfDocument.loadOutline]. -@immutable -class PdfOutlineNode { - const PdfOutlineNode({required this.title, required this.dest, required this.children}); - - /// Outline node title. - final String title; - - /// Outline node destination. - final PdfDest? dest; - - /// Outline child nodes. - final List children; -} - -class PdfException implements Exception { - const PdfException(this.message); - final String message; - @override - String toString() => 'PdfException: $message'; -} - -class PdfPasswordException extends PdfException { - const PdfPasswordException(super.message); -} diff --git a/lib/src/pdfium/pdfium_bindings.dart b/lib/src/pdfium/pdfium_bindings.dart deleted file mode 100644 index d9531c80..00000000 --- a/lib/src/pdfium/pdfium_bindings.dart +++ /dev/null @@ -1,7496 +0,0 @@ -// ignore_for_file: unused_field - -// AUTO GENERATED FILE, DO NOT EDIT. -// -// Generated by `package:ffigen`. -// ignore_for_file: type=lint -import 'dart:ffi' as ffi; - -class pdfium { - /// Holds the symbol lookup function. - final ffi.Pointer Function(String symbolName) - _lookup; - - /// The symbols are looked up in [dynamicLibrary]. - pdfium(ffi.DynamicLibrary dynamicLibrary) : _lookup = dynamicLibrary.lookup; - - /// The symbols are looked up with [lookup]. - pdfium.fromLookup( - ffi.Pointer Function(String symbolName) - lookup) - : _lookup = lookup; - - void FPDF_InitLibraryWithConfig( - ffi.Pointer config, - ) { - return _FPDF_InitLibraryWithConfig( - config, - ); - } - - late final _FPDF_InitLibraryWithConfigPtr = _lookup< - ffi - .NativeFunction)>>( - 'FPDF_InitLibraryWithConfig'); - late final _FPDF_InitLibraryWithConfig = _FPDF_InitLibraryWithConfigPtr - .asFunction)>(); - - void FPDF_InitLibrary() { - return _FPDF_InitLibrary(); - } - - late final _FPDF_InitLibraryPtr = - _lookup>('FPDF_InitLibrary'); - late final _FPDF_InitLibrary = - _FPDF_InitLibraryPtr.asFunction(); - - void FPDF_DestroyLibrary() { - return _FPDF_DestroyLibrary(); - } - - late final _FPDF_DestroyLibraryPtr = - _lookup>('FPDF_DestroyLibrary'); - late final _FPDF_DestroyLibrary = - _FPDF_DestroyLibraryPtr.asFunction(); - - void FPDF_SetSandBoxPolicy( - int policy, - int enable, - ) { - return _FPDF_SetSandBoxPolicy( - policy, - enable, - ); - } - - late final _FPDF_SetSandBoxPolicyPtr = - _lookup>( - 'FPDF_SetSandBoxPolicy'); - late final _FPDF_SetSandBoxPolicy = - _FPDF_SetSandBoxPolicyPtr.asFunction(); - - int FPDF_SetPrintMode( - int mode, - ) { - return _FPDF_SetPrintMode( - mode, - ); - } - - late final _FPDF_SetPrintModePtr = - _lookup>( - 'FPDF_SetPrintMode'); - late final _FPDF_SetPrintMode = - _FPDF_SetPrintModePtr.asFunction(); - - FPDF_DOCUMENT FPDF_LoadDocument( - FPDF_STRING file_path, - FPDF_BYTESTRING password, - ) { - return _FPDF_LoadDocument( - file_path, - password, - ); - } - - late final _FPDF_LoadDocumentPtr = _lookup< - ffi.NativeFunction< - FPDF_DOCUMENT Function( - FPDF_STRING, FPDF_BYTESTRING)>>('FPDF_LoadDocument'); - late final _FPDF_LoadDocument = _FPDF_LoadDocumentPtr.asFunction< - FPDF_DOCUMENT Function(FPDF_STRING, FPDF_BYTESTRING)>(); - - FPDF_DOCUMENT FPDF_LoadMemDocument( - ffi.Pointer data_buf, - int size, - FPDF_BYTESTRING password, - ) { - return _FPDF_LoadMemDocument( - data_buf, - size, - password, - ); - } - - late final _FPDF_LoadMemDocumentPtr = _lookup< - ffi.NativeFunction< - FPDF_DOCUMENT Function(ffi.Pointer, ffi.Int, - FPDF_BYTESTRING)>>('FPDF_LoadMemDocument'); - late final _FPDF_LoadMemDocument = _FPDF_LoadMemDocumentPtr.asFunction< - FPDF_DOCUMENT Function(ffi.Pointer, int, FPDF_BYTESTRING)>(); - - FPDF_DOCUMENT FPDF_LoadMemDocument64( - ffi.Pointer data_buf, - int size, - FPDF_BYTESTRING password, - ) { - return _FPDF_LoadMemDocument64( - data_buf, - size, - password, - ); - } - - late final _FPDF_LoadMemDocument64Ptr = _lookup< - ffi.NativeFunction< - FPDF_DOCUMENT Function(ffi.Pointer, ffi.Size, - FPDF_BYTESTRING)>>('FPDF_LoadMemDocument64'); - late final _FPDF_LoadMemDocument64 = _FPDF_LoadMemDocument64Ptr.asFunction< - FPDF_DOCUMENT Function(ffi.Pointer, int, FPDF_BYTESTRING)>(); - - FPDF_DOCUMENT FPDF_LoadCustomDocument( - ffi.Pointer pFileAccess, - FPDF_BYTESTRING password, - ) { - return _FPDF_LoadCustomDocument( - pFileAccess, - password, - ); - } - - late final _FPDF_LoadCustomDocumentPtr = _lookup< - ffi.NativeFunction< - FPDF_DOCUMENT Function(ffi.Pointer, - FPDF_BYTESTRING)>>('FPDF_LoadCustomDocument'); - late final _FPDF_LoadCustomDocument = _FPDF_LoadCustomDocumentPtr.asFunction< - FPDF_DOCUMENT Function(ffi.Pointer, FPDF_BYTESTRING)>(); - - int FPDF_GetFileVersion( - FPDF_DOCUMENT doc, - ffi.Pointer fileVersion, - ) { - return _FPDF_GetFileVersion( - doc, - fileVersion, - ); - } - - late final _FPDF_GetFileVersionPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_DOCUMENT, ffi.Pointer)>>('FPDF_GetFileVersion'); - late final _FPDF_GetFileVersion = _FPDF_GetFileVersionPtr.asFunction< - int Function(FPDF_DOCUMENT, ffi.Pointer)>(); - - int FPDF_GetLastError() { - return _FPDF_GetLastError(); - } - - late final _FPDF_GetLastErrorPtr = - _lookup>( - 'FPDF_GetLastError'); - late final _FPDF_GetLastError = - _FPDF_GetLastErrorPtr.asFunction(); - - int FPDF_DocumentHasValidCrossReferenceTable( - FPDF_DOCUMENT document, - ) { - return _FPDF_DocumentHasValidCrossReferenceTable( - document, - ); - } - - late final _FPDF_DocumentHasValidCrossReferenceTablePtr = - _lookup>( - 'FPDF_DocumentHasValidCrossReferenceTable'); - late final _FPDF_DocumentHasValidCrossReferenceTable = - _FPDF_DocumentHasValidCrossReferenceTablePtr.asFunction< - int Function(FPDF_DOCUMENT)>(); - - int FPDF_GetTrailerEnds( - FPDF_DOCUMENT document, - ffi.Pointer buffer, - int length, - ) { - return _FPDF_GetTrailerEnds( - document, - buffer, - length, - ); - } - - late final _FPDF_GetTrailerEndsPtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function(FPDF_DOCUMENT, ffi.Pointer, - ffi.UnsignedLong)>>('FPDF_GetTrailerEnds'); - late final _FPDF_GetTrailerEnds = _FPDF_GetTrailerEndsPtr.asFunction< - int Function(FPDF_DOCUMENT, ffi.Pointer, int)>(); - - int FPDF_GetDocPermissions( - FPDF_DOCUMENT document, - ) { - return _FPDF_GetDocPermissions( - document, - ); - } - - late final _FPDF_GetDocPermissionsPtr = - _lookup>( - 'FPDF_GetDocPermissions'); - late final _FPDF_GetDocPermissions = - _FPDF_GetDocPermissionsPtr.asFunction(); - - int FPDF_GetDocUserPermissions( - FPDF_DOCUMENT document, - ) { - return _FPDF_GetDocUserPermissions( - document, - ); - } - - late final _FPDF_GetDocUserPermissionsPtr = - _lookup>( - 'FPDF_GetDocUserPermissions'); - late final _FPDF_GetDocUserPermissions = - _FPDF_GetDocUserPermissionsPtr.asFunction(); - - int FPDF_GetSecurityHandlerRevision( - FPDF_DOCUMENT document, - ) { - return _FPDF_GetSecurityHandlerRevision( - document, - ); - } - - late final _FPDF_GetSecurityHandlerRevisionPtr = - _lookup>( - 'FPDF_GetSecurityHandlerRevision'); - late final _FPDF_GetSecurityHandlerRevision = - _FPDF_GetSecurityHandlerRevisionPtr.asFunction< - int Function(FPDF_DOCUMENT)>(); - - int FPDF_GetPageCount( - FPDF_DOCUMENT document, - ) { - return _FPDF_GetPageCount( - document, - ); - } - - late final _FPDF_GetPageCountPtr = - _lookup>( - 'FPDF_GetPageCount'); - late final _FPDF_GetPageCount = - _FPDF_GetPageCountPtr.asFunction(); - - FPDF_PAGE FPDF_LoadPage( - FPDF_DOCUMENT document, - int page_index, - ) { - return _FPDF_LoadPage( - document, - page_index, - ); - } - - late final _FPDF_LoadPagePtr = - _lookup>( - 'FPDF_LoadPage'); - late final _FPDF_LoadPage = - _FPDF_LoadPagePtr.asFunction(); - - double FPDF_GetPageWidthF( - FPDF_PAGE page, - ) { - return _FPDF_GetPageWidthF( - page, - ); - } - - late final _FPDF_GetPageWidthFPtr = - _lookup>( - 'FPDF_GetPageWidthF'); - late final _FPDF_GetPageWidthF = - _FPDF_GetPageWidthFPtr.asFunction(); - - double FPDF_GetPageWidth( - FPDF_PAGE page, - ) { - return _FPDF_GetPageWidth( - page, - ); - } - - late final _FPDF_GetPageWidthPtr = - _lookup>( - 'FPDF_GetPageWidth'); - late final _FPDF_GetPageWidth = - _FPDF_GetPageWidthPtr.asFunction(); - - double FPDF_GetPageHeightF( - FPDF_PAGE page, - ) { - return _FPDF_GetPageHeightF( - page, - ); - } - - late final _FPDF_GetPageHeightFPtr = - _lookup>( - 'FPDF_GetPageHeightF'); - late final _FPDF_GetPageHeightF = - _FPDF_GetPageHeightFPtr.asFunction(); - - double FPDF_GetPageHeight( - FPDF_PAGE page, - ) { - return _FPDF_GetPageHeight( - page, - ); - } - - late final _FPDF_GetPageHeightPtr = - _lookup>( - 'FPDF_GetPageHeight'); - late final _FPDF_GetPageHeight = - _FPDF_GetPageHeightPtr.asFunction(); - - int FPDF_GetPageBoundingBox( - FPDF_PAGE page, - ffi.Pointer rect, - ) { - return _FPDF_GetPageBoundingBox( - page, - rect, - ); - } - - late final _FPDF_GetPageBoundingBoxPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_PAGE, ffi.Pointer)>>('FPDF_GetPageBoundingBox'); - late final _FPDF_GetPageBoundingBox = _FPDF_GetPageBoundingBoxPtr.asFunction< - int Function(FPDF_PAGE, ffi.Pointer)>(); - - int FPDF_GetPageSizeByIndexF( - FPDF_DOCUMENT document, - int page_index, - ffi.Pointer size, - ) { - return _FPDF_GetPageSizeByIndexF( - document, - page_index, - size, - ); - } - - late final _FPDF_GetPageSizeByIndexFPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_DOCUMENT, ffi.Int, - ffi.Pointer)>>('FPDF_GetPageSizeByIndexF'); - late final _FPDF_GetPageSizeByIndexF = _FPDF_GetPageSizeByIndexFPtr - .asFunction)>(); - - int FPDF_GetPageSizeByIndex( - FPDF_DOCUMENT document, - int page_index, - ffi.Pointer width, - ffi.Pointer height, - ) { - return _FPDF_GetPageSizeByIndex( - document, - page_index, - width, - height, - ); - } - - late final _FPDF_GetPageSizeByIndexPtr = _lookup< - ffi.NativeFunction< - ffi.Int Function(FPDF_DOCUMENT, ffi.Int, ffi.Pointer, - ffi.Pointer)>>('FPDF_GetPageSizeByIndex'); - late final _FPDF_GetPageSizeByIndex = _FPDF_GetPageSizeByIndexPtr.asFunction< - int Function(FPDF_DOCUMENT, int, ffi.Pointer, - ffi.Pointer)>(); - - void FPDF_RenderPage( - HDC dc, - FPDF_PAGE page, - int start_x, - int start_y, - int size_x, - int size_y, - int rotate, - int flags, - ) { - return _FPDF_RenderPage( - dc, - page, - start_x, - start_y, - size_x, - size_y, - rotate, - flags, - ); - } - - late final _FPDF_RenderPagePtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(HDC, FPDF_PAGE, ffi.Int, ffi.Int, ffi.Int, ffi.Int, - ffi.Int, ffi.Int)>>('FPDF_RenderPage'); - late final _FPDF_RenderPage = _FPDF_RenderPagePtr.asFunction< - void Function(HDC, FPDF_PAGE, int, int, int, int, int, int)>(); - - void FPDF_RenderPageBitmap( - FPDF_BITMAP bitmap, - FPDF_PAGE page, - int start_x, - int start_y, - int size_x, - int size_y, - int rotate, - int flags, - ) { - return _FPDF_RenderPageBitmap( - bitmap, - page, - start_x, - start_y, - size_x, - size_y, - rotate, - flags, - ); - } - - late final _FPDF_RenderPageBitmapPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(FPDF_BITMAP, FPDF_PAGE, ffi.Int, ffi.Int, ffi.Int, - ffi.Int, ffi.Int, ffi.Int)>>('FPDF_RenderPageBitmap'); - late final _FPDF_RenderPageBitmap = _FPDF_RenderPageBitmapPtr.asFunction< - void Function(FPDF_BITMAP, FPDF_PAGE, int, int, int, int, int, int)>(); - - void FPDF_RenderPageBitmapWithMatrix( - FPDF_BITMAP bitmap, - FPDF_PAGE page, - ffi.Pointer matrix, - ffi.Pointer clipping, - int flags, - ) { - return _FPDF_RenderPageBitmapWithMatrix( - bitmap, - page, - matrix, - clipping, - flags, - ); - } - - late final _FPDF_RenderPageBitmapWithMatrixPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - FPDF_BITMAP, - FPDF_PAGE, - ffi.Pointer, - ffi.Pointer, - ffi.Int)>>('FPDF_RenderPageBitmapWithMatrix'); - late final _FPDF_RenderPageBitmapWithMatrix = - _FPDF_RenderPageBitmapWithMatrixPtr.asFunction< - void Function(FPDF_BITMAP, FPDF_PAGE, ffi.Pointer, - ffi.Pointer, int)>(); - - void FPDF_ClosePage( - FPDF_PAGE page, - ) { - return _FPDF_ClosePage( - page, - ); - } - - late final _FPDF_ClosePagePtr = - _lookup>( - 'FPDF_ClosePage'); - late final _FPDF_ClosePage = - _FPDF_ClosePagePtr.asFunction(); - - void FPDF_CloseDocument( - FPDF_DOCUMENT document, - ) { - return _FPDF_CloseDocument( - document, - ); - } - - late final _FPDF_CloseDocumentPtr = - _lookup>( - 'FPDF_CloseDocument'); - late final _FPDF_CloseDocument = - _FPDF_CloseDocumentPtr.asFunction(); - - int FPDF_DeviceToPage( - FPDF_PAGE page, - int start_x, - int start_y, - int size_x, - int size_y, - int rotate, - int device_x, - int device_y, - ffi.Pointer page_x, - ffi.Pointer page_y, - ) { - return _FPDF_DeviceToPage( - page, - start_x, - start_y, - size_x, - size_y, - rotate, - device_x, - device_y, - page_x, - page_y, - ); - } - - late final _FPDF_DeviceToPagePtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_PAGE, - ffi.Int, - ffi.Int, - ffi.Int, - ffi.Int, - ffi.Int, - ffi.Int, - ffi.Int, - ffi.Pointer, - ffi.Pointer)>>('FPDF_DeviceToPage'); - late final _FPDF_DeviceToPage = _FPDF_DeviceToPagePtr.asFunction< - int Function(FPDF_PAGE, int, int, int, int, int, int, int, - ffi.Pointer, ffi.Pointer)>(); - - int FPDF_PageToDevice( - FPDF_PAGE page, - int start_x, - int start_y, - int size_x, - int size_y, - int rotate, - double page_x, - double page_y, - ffi.Pointer device_x, - ffi.Pointer device_y, - ) { - return _FPDF_PageToDevice( - page, - start_x, - start_y, - size_x, - size_y, - rotate, - page_x, - page_y, - device_x, - device_y, - ); - } - - late final _FPDF_PageToDevicePtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_PAGE, - ffi.Int, - ffi.Int, - ffi.Int, - ffi.Int, - ffi.Int, - ffi.Double, - ffi.Double, - ffi.Pointer, - ffi.Pointer)>>('FPDF_PageToDevice'); - late final _FPDF_PageToDevice = _FPDF_PageToDevicePtr.asFunction< - int Function(FPDF_PAGE, int, int, int, int, int, double, double, - ffi.Pointer, ffi.Pointer)>(); - - FPDF_BITMAP FPDFBitmap_Create( - int width, - int height, - int alpha, - ) { - return _FPDFBitmap_Create( - width, - height, - alpha, - ); - } - - late final _FPDFBitmap_CreatePtr = _lookup< - ffi.NativeFunction>( - 'FPDFBitmap_Create'); - late final _FPDFBitmap_Create = - _FPDFBitmap_CreatePtr.asFunction(); - - FPDF_BITMAP FPDFBitmap_CreateEx( - int width, - int height, - int format, - ffi.Pointer first_scan, - int stride, - ) { - return _FPDFBitmap_CreateEx( - width, - height, - format, - first_scan, - stride, - ); - } - - late final _FPDFBitmap_CreateExPtr = _lookup< - ffi.NativeFunction< - FPDF_BITMAP Function(ffi.Int, ffi.Int, ffi.Int, ffi.Pointer, - ffi.Int)>>('FPDFBitmap_CreateEx'); - late final _FPDFBitmap_CreateEx = _FPDFBitmap_CreateExPtr.asFunction< - FPDF_BITMAP Function(int, int, int, ffi.Pointer, int)>(); - - int FPDFBitmap_GetFormat( - FPDF_BITMAP bitmap, - ) { - return _FPDFBitmap_GetFormat( - bitmap, - ); - } - - late final _FPDFBitmap_GetFormatPtr = - _lookup>( - 'FPDFBitmap_GetFormat'); - late final _FPDFBitmap_GetFormat = - _FPDFBitmap_GetFormatPtr.asFunction(); - - void FPDFBitmap_FillRect( - FPDF_BITMAP bitmap, - int left, - int top, - int width, - int height, - int color, - ) { - return _FPDFBitmap_FillRect( - bitmap, - left, - top, - width, - height, - color, - ); - } - - late final _FPDFBitmap_FillRectPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(FPDF_BITMAP, ffi.Int, ffi.Int, ffi.Int, ffi.Int, - FPDF_DWORD)>>('FPDFBitmap_FillRect'); - late final _FPDFBitmap_FillRect = _FPDFBitmap_FillRectPtr.asFunction< - void Function(FPDF_BITMAP, int, int, int, int, int)>(); - - ffi.Pointer FPDFBitmap_GetBuffer( - FPDF_BITMAP bitmap, - ) { - return _FPDFBitmap_GetBuffer( - bitmap, - ); - } - - late final _FPDFBitmap_GetBufferPtr = - _lookup Function(FPDF_BITMAP)>>( - 'FPDFBitmap_GetBuffer'); - late final _FPDFBitmap_GetBuffer = _FPDFBitmap_GetBufferPtr.asFunction< - ffi.Pointer Function(FPDF_BITMAP)>(); - - int FPDFBitmap_GetWidth( - FPDF_BITMAP bitmap, - ) { - return _FPDFBitmap_GetWidth( - bitmap, - ); - } - - late final _FPDFBitmap_GetWidthPtr = - _lookup>( - 'FPDFBitmap_GetWidth'); - late final _FPDFBitmap_GetWidth = - _FPDFBitmap_GetWidthPtr.asFunction(); - - int FPDFBitmap_GetHeight( - FPDF_BITMAP bitmap, - ) { - return _FPDFBitmap_GetHeight( - bitmap, - ); - } - - late final _FPDFBitmap_GetHeightPtr = - _lookup>( - 'FPDFBitmap_GetHeight'); - late final _FPDFBitmap_GetHeight = - _FPDFBitmap_GetHeightPtr.asFunction(); - - int FPDFBitmap_GetStride( - FPDF_BITMAP bitmap, - ) { - return _FPDFBitmap_GetStride( - bitmap, - ); - } - - late final _FPDFBitmap_GetStridePtr = - _lookup>( - 'FPDFBitmap_GetStride'); - late final _FPDFBitmap_GetStride = - _FPDFBitmap_GetStridePtr.asFunction(); - - void FPDFBitmap_Destroy( - FPDF_BITMAP bitmap, - ) { - return _FPDFBitmap_Destroy( - bitmap, - ); - } - - late final _FPDFBitmap_DestroyPtr = - _lookup>( - 'FPDFBitmap_Destroy'); - late final _FPDFBitmap_Destroy = - _FPDFBitmap_DestroyPtr.asFunction(); - - int FPDF_VIEWERREF_GetPrintScaling( - FPDF_DOCUMENT document, - ) { - return _FPDF_VIEWERREF_GetPrintScaling( - document, - ); - } - - late final _FPDF_VIEWERREF_GetPrintScalingPtr = - _lookup>( - 'FPDF_VIEWERREF_GetPrintScaling'); - late final _FPDF_VIEWERREF_GetPrintScaling = - _FPDF_VIEWERREF_GetPrintScalingPtr.asFunction< - int Function(FPDF_DOCUMENT)>(); - - int FPDF_VIEWERREF_GetNumCopies( - FPDF_DOCUMENT document, - ) { - return _FPDF_VIEWERREF_GetNumCopies( - document, - ); - } - - late final _FPDF_VIEWERREF_GetNumCopiesPtr = - _lookup>( - 'FPDF_VIEWERREF_GetNumCopies'); - late final _FPDF_VIEWERREF_GetNumCopies = - _FPDF_VIEWERREF_GetNumCopiesPtr.asFunction(); - - FPDF_PAGERANGE FPDF_VIEWERREF_GetPrintPageRange( - FPDF_DOCUMENT document, - ) { - return _FPDF_VIEWERREF_GetPrintPageRange( - document, - ); - } - - late final _FPDF_VIEWERREF_GetPrintPageRangePtr = - _lookup>( - 'FPDF_VIEWERREF_GetPrintPageRange'); - late final _FPDF_VIEWERREF_GetPrintPageRange = - _FPDF_VIEWERREF_GetPrintPageRangePtr.asFunction< - FPDF_PAGERANGE Function(FPDF_DOCUMENT)>(); - - int FPDF_VIEWERREF_GetPrintPageRangeCount( - FPDF_PAGERANGE pagerange, - ) { - return _FPDF_VIEWERREF_GetPrintPageRangeCount( - pagerange, - ); - } - - late final _FPDF_VIEWERREF_GetPrintPageRangeCountPtr = - _lookup>( - 'FPDF_VIEWERREF_GetPrintPageRangeCount'); - late final _FPDF_VIEWERREF_GetPrintPageRangeCount = - _FPDF_VIEWERREF_GetPrintPageRangeCountPtr.asFunction< - int Function(FPDF_PAGERANGE)>(); - - int FPDF_VIEWERREF_GetPrintPageRangeElement( - FPDF_PAGERANGE pagerange, - int index, - ) { - return _FPDF_VIEWERREF_GetPrintPageRangeElement( - pagerange, - index, - ); - } - - late final _FPDF_VIEWERREF_GetPrintPageRangeElementPtr = - _lookup>( - 'FPDF_VIEWERREF_GetPrintPageRangeElement'); - late final _FPDF_VIEWERREF_GetPrintPageRangeElement = - _FPDF_VIEWERREF_GetPrintPageRangeElementPtr.asFunction< - int Function(FPDF_PAGERANGE, int)>(); - - _FPDF_DUPLEXTYPE_ FPDF_VIEWERREF_GetDuplex( - FPDF_DOCUMENT document, - ) { - return _FPDF_DUPLEXTYPE_.fromValue(_FPDF_VIEWERREF_GetDuplex( - document, - )); - } - - late final _FPDF_VIEWERREF_GetDuplexPtr = - _lookup>( - 'FPDF_VIEWERREF_GetDuplex'); - late final _FPDF_VIEWERREF_GetDuplex = - _FPDF_VIEWERREF_GetDuplexPtr.asFunction(); - - int FPDF_VIEWERREF_GetName( - FPDF_DOCUMENT document, - FPDF_BYTESTRING key, - ffi.Pointer buffer, - int length, - ) { - return _FPDF_VIEWERREF_GetName( - document, - key, - buffer, - length, - ); - } - - late final _FPDF_VIEWERREF_GetNamePtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function( - FPDF_DOCUMENT, - FPDF_BYTESTRING, - ffi.Pointer, - ffi.UnsignedLong)>>('FPDF_VIEWERREF_GetName'); - late final _FPDF_VIEWERREF_GetName = _FPDF_VIEWERREF_GetNamePtr.asFunction< - int Function( - FPDF_DOCUMENT, FPDF_BYTESTRING, ffi.Pointer, int)>(); - - int FPDF_CountNamedDests( - FPDF_DOCUMENT document, - ) { - return _FPDF_CountNamedDests( - document, - ); - } - - late final _FPDF_CountNamedDestsPtr = - _lookup>( - 'FPDF_CountNamedDests'); - late final _FPDF_CountNamedDests = - _FPDF_CountNamedDestsPtr.asFunction(); - - FPDF_DEST FPDF_GetNamedDestByName( - FPDF_DOCUMENT document, - FPDF_BYTESTRING name, - ) { - return _FPDF_GetNamedDestByName( - document, - name, - ); - } - - late final _FPDF_GetNamedDestByNamePtr = _lookup< - ffi - .NativeFunction>( - 'FPDF_GetNamedDestByName'); - late final _FPDF_GetNamedDestByName = _FPDF_GetNamedDestByNamePtr.asFunction< - FPDF_DEST Function(FPDF_DOCUMENT, FPDF_BYTESTRING)>(); - - FPDF_DEST FPDF_GetNamedDest( - FPDF_DOCUMENT document, - int index, - ffi.Pointer buffer, - ffi.Pointer buflen, - ) { - return _FPDF_GetNamedDest( - document, - index, - buffer, - buflen, - ); - } - - late final _FPDF_GetNamedDestPtr = _lookup< - ffi.NativeFunction< - FPDF_DEST Function(FPDF_DOCUMENT, ffi.Int, ffi.Pointer, - ffi.Pointer)>>('FPDF_GetNamedDest'); - late final _FPDF_GetNamedDest = _FPDF_GetNamedDestPtr.asFunction< - FPDF_DEST Function( - FPDF_DOCUMENT, int, ffi.Pointer, ffi.Pointer)>(); - - int FPDF_GetXFAPacketCount( - FPDF_DOCUMENT document, - ) { - return _FPDF_GetXFAPacketCount( - document, - ); - } - - late final _FPDF_GetXFAPacketCountPtr = - _lookup>( - 'FPDF_GetXFAPacketCount'); - late final _FPDF_GetXFAPacketCount = - _FPDF_GetXFAPacketCountPtr.asFunction(); - - int FPDF_GetXFAPacketName( - FPDF_DOCUMENT document, - int index, - ffi.Pointer buffer, - int buflen, - ) { - return _FPDF_GetXFAPacketName( - document, - index, - buffer, - buflen, - ); - } - - late final _FPDF_GetXFAPacketNamePtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function( - FPDF_DOCUMENT, - ffi.Int, - ffi.Pointer, - ffi.UnsignedLong)>>('FPDF_GetXFAPacketName'); - late final _FPDF_GetXFAPacketName = _FPDF_GetXFAPacketNamePtr.asFunction< - int Function(FPDF_DOCUMENT, int, ffi.Pointer, int)>(); - - int FPDF_GetXFAPacketContent( - FPDF_DOCUMENT document, - int index, - ffi.Pointer buffer, - int buflen, - ffi.Pointer out_buflen, - ) { - return _FPDF_GetXFAPacketContent( - document, - index, - buffer, - buflen, - out_buflen, - ); - } - - late final _FPDF_GetXFAPacketContentPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_DOCUMENT, - ffi.Int, - ffi.Pointer, - ffi.UnsignedLong, - ffi.Pointer)>>('FPDF_GetXFAPacketContent'); - late final _FPDF_GetXFAPacketContent = - _FPDF_GetXFAPacketContentPtr.asFunction< - int Function(FPDF_DOCUMENT, int, ffi.Pointer, int, - ffi.Pointer)>(); - - FPDF_FORMHANDLE FPDFDOC_InitFormFillEnvironment( - FPDF_DOCUMENT document, - ffi.Pointer formInfo, - ) { - return _FPDFDOC_InitFormFillEnvironment( - document, - formInfo, - ); - } - - late final _FPDFDOC_InitFormFillEnvironmentPtr = _lookup< - ffi.NativeFunction< - FPDF_FORMHANDLE Function( - FPDF_DOCUMENT, ffi.Pointer)>>( - 'FPDFDOC_InitFormFillEnvironment'); - late final _FPDFDOC_InitFormFillEnvironment = - _FPDFDOC_InitFormFillEnvironmentPtr.asFunction< - FPDF_FORMHANDLE Function( - FPDF_DOCUMENT, ffi.Pointer)>(); - - void FPDFDOC_ExitFormFillEnvironment( - FPDF_FORMHANDLE hHandle, - ) { - return _FPDFDOC_ExitFormFillEnvironment( - hHandle, - ); - } - - late final _FPDFDOC_ExitFormFillEnvironmentPtr = - _lookup>( - 'FPDFDOC_ExitFormFillEnvironment'); - late final _FPDFDOC_ExitFormFillEnvironment = - _FPDFDOC_ExitFormFillEnvironmentPtr.asFunction< - void Function(FPDF_FORMHANDLE)>(); - - void FORM_OnAfterLoadPage( - FPDF_PAGE page, - FPDF_FORMHANDLE hHandle, - ) { - return _FORM_OnAfterLoadPage( - page, - hHandle, - ); - } - - late final _FORM_OnAfterLoadPagePtr = _lookup< - ffi.NativeFunction>( - 'FORM_OnAfterLoadPage'); - late final _FORM_OnAfterLoadPage = _FORM_OnAfterLoadPagePtr.asFunction< - void Function(FPDF_PAGE, FPDF_FORMHANDLE)>(); - - void FORM_OnBeforeClosePage( - FPDF_PAGE page, - FPDF_FORMHANDLE hHandle, - ) { - return _FORM_OnBeforeClosePage( - page, - hHandle, - ); - } - - late final _FORM_OnBeforeClosePagePtr = _lookup< - ffi.NativeFunction>( - 'FORM_OnBeforeClosePage'); - late final _FORM_OnBeforeClosePage = _FORM_OnBeforeClosePagePtr.asFunction< - void Function(FPDF_PAGE, FPDF_FORMHANDLE)>(); - - void FORM_DoDocumentJSAction( - FPDF_FORMHANDLE hHandle, - ) { - return _FORM_DoDocumentJSAction( - hHandle, - ); - } - - late final _FORM_DoDocumentJSActionPtr = - _lookup>( - 'FORM_DoDocumentJSAction'); - late final _FORM_DoDocumentJSAction = - _FORM_DoDocumentJSActionPtr.asFunction(); - - void FORM_DoDocumentOpenAction( - FPDF_FORMHANDLE hHandle, - ) { - return _FORM_DoDocumentOpenAction( - hHandle, - ); - } - - late final _FORM_DoDocumentOpenActionPtr = - _lookup>( - 'FORM_DoDocumentOpenAction'); - late final _FORM_DoDocumentOpenAction = _FORM_DoDocumentOpenActionPtr - .asFunction(); - - void FORM_DoDocumentAAction( - FPDF_FORMHANDLE hHandle, - int aaType, - ) { - return _FORM_DoDocumentAAction( - hHandle, - aaType, - ); - } - - late final _FORM_DoDocumentAActionPtr = - _lookup>( - 'FORM_DoDocumentAAction'); - late final _FORM_DoDocumentAAction = _FORM_DoDocumentAActionPtr.asFunction< - void Function(FPDF_FORMHANDLE, int)>(); - - void FORM_DoPageAAction( - FPDF_PAGE page, - FPDF_FORMHANDLE hHandle, - int aaType, - ) { - return _FORM_DoPageAAction( - page, - hHandle, - aaType, - ); - } - - late final _FORM_DoPageAActionPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function( - FPDF_PAGE, FPDF_FORMHANDLE, ffi.Int)>>('FORM_DoPageAAction'); - late final _FORM_DoPageAAction = _FORM_DoPageAActionPtr.asFunction< - void Function(FPDF_PAGE, FPDF_FORMHANDLE, int)>(); - - int FORM_OnMouseMove( - FPDF_FORMHANDLE hHandle, - FPDF_PAGE page, - int modifier, - double page_x, - double page_y, - ) { - return _FORM_OnMouseMove( - hHandle, - page, - modifier, - page_x, - page_y, - ); - } - - late final _FORM_OnMouseMovePtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_FORMHANDLE, FPDF_PAGE, ffi.Int, ffi.Double, - ffi.Double)>>('FORM_OnMouseMove'); - late final _FORM_OnMouseMove = _FORM_OnMouseMovePtr.asFunction< - int Function(FPDF_FORMHANDLE, FPDF_PAGE, int, double, double)>(); - - int FORM_OnMouseWheel( - FPDF_FORMHANDLE hHandle, - FPDF_PAGE page, - int modifier, - ffi.Pointer page_coord, - int delta_x, - int delta_y, - ) { - return _FORM_OnMouseWheel( - hHandle, - page, - modifier, - page_coord, - delta_x, - delta_y, - ); - } - - late final _FORM_OnMouseWheelPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_FORMHANDLE, FPDF_PAGE, ffi.Int, - ffi.Pointer, ffi.Int, ffi.Int)>>('FORM_OnMouseWheel'); - late final _FORM_OnMouseWheel = _FORM_OnMouseWheelPtr.asFunction< - int Function( - FPDF_FORMHANDLE, FPDF_PAGE, int, ffi.Pointer, int, int)>(); - - int FORM_OnFocus( - FPDF_FORMHANDLE hHandle, - FPDF_PAGE page, - int modifier, - double page_x, - double page_y, - ) { - return _FORM_OnFocus( - hHandle, - page, - modifier, - page_x, - page_y, - ); - } - - late final _FORM_OnFocusPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_FORMHANDLE, FPDF_PAGE, ffi.Int, ffi.Double, - ffi.Double)>>('FORM_OnFocus'); - late final _FORM_OnFocus = _FORM_OnFocusPtr.asFunction< - int Function(FPDF_FORMHANDLE, FPDF_PAGE, int, double, double)>(); - - int FORM_OnLButtonDown( - FPDF_FORMHANDLE hHandle, - FPDF_PAGE page, - int modifier, - double page_x, - double page_y, - ) { - return _FORM_OnLButtonDown( - hHandle, - page, - modifier, - page_x, - page_y, - ); - } - - late final _FORM_OnLButtonDownPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_FORMHANDLE, FPDF_PAGE, ffi.Int, ffi.Double, - ffi.Double)>>('FORM_OnLButtonDown'); - late final _FORM_OnLButtonDown = _FORM_OnLButtonDownPtr.asFunction< - int Function(FPDF_FORMHANDLE, FPDF_PAGE, int, double, double)>(); - - int FORM_OnRButtonDown( - FPDF_FORMHANDLE hHandle, - FPDF_PAGE page, - int modifier, - double page_x, - double page_y, - ) { - return _FORM_OnRButtonDown( - hHandle, - page, - modifier, - page_x, - page_y, - ); - } - - late final _FORM_OnRButtonDownPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_FORMHANDLE, FPDF_PAGE, ffi.Int, ffi.Double, - ffi.Double)>>('FORM_OnRButtonDown'); - late final _FORM_OnRButtonDown = _FORM_OnRButtonDownPtr.asFunction< - int Function(FPDF_FORMHANDLE, FPDF_PAGE, int, double, double)>(); - - int FORM_OnLButtonUp( - FPDF_FORMHANDLE hHandle, - FPDF_PAGE page, - int modifier, - double page_x, - double page_y, - ) { - return _FORM_OnLButtonUp( - hHandle, - page, - modifier, - page_x, - page_y, - ); - } - - late final _FORM_OnLButtonUpPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_FORMHANDLE, FPDF_PAGE, ffi.Int, ffi.Double, - ffi.Double)>>('FORM_OnLButtonUp'); - late final _FORM_OnLButtonUp = _FORM_OnLButtonUpPtr.asFunction< - int Function(FPDF_FORMHANDLE, FPDF_PAGE, int, double, double)>(); - - int FORM_OnRButtonUp( - FPDF_FORMHANDLE hHandle, - FPDF_PAGE page, - int modifier, - double page_x, - double page_y, - ) { - return _FORM_OnRButtonUp( - hHandle, - page, - modifier, - page_x, - page_y, - ); - } - - late final _FORM_OnRButtonUpPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_FORMHANDLE, FPDF_PAGE, ffi.Int, ffi.Double, - ffi.Double)>>('FORM_OnRButtonUp'); - late final _FORM_OnRButtonUp = _FORM_OnRButtonUpPtr.asFunction< - int Function(FPDF_FORMHANDLE, FPDF_PAGE, int, double, double)>(); - - int FORM_OnLButtonDoubleClick( - FPDF_FORMHANDLE hHandle, - FPDF_PAGE page, - int modifier, - double page_x, - double page_y, - ) { - return _FORM_OnLButtonDoubleClick( - hHandle, - page, - modifier, - page_x, - page_y, - ); - } - - late final _FORM_OnLButtonDoubleClickPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_FORMHANDLE, FPDF_PAGE, ffi.Int, ffi.Double, - ffi.Double)>>('FORM_OnLButtonDoubleClick'); - late final _FORM_OnLButtonDoubleClick = - _FORM_OnLButtonDoubleClickPtr.asFunction< - int Function(FPDF_FORMHANDLE, FPDF_PAGE, int, double, double)>(); - - int FORM_OnKeyDown( - FPDF_FORMHANDLE hHandle, - FPDF_PAGE page, - int nKeyCode, - int modifier, - ) { - return _FORM_OnKeyDown( - hHandle, - page, - nKeyCode, - modifier, - ); - } - - late final _FORM_OnKeyDownPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_FORMHANDLE, FPDF_PAGE, ffi.Int, ffi.Int)>>('FORM_OnKeyDown'); - late final _FORM_OnKeyDown = _FORM_OnKeyDownPtr.asFunction< - int Function(FPDF_FORMHANDLE, FPDF_PAGE, int, int)>(); - - int FORM_OnKeyUp( - FPDF_FORMHANDLE hHandle, - FPDF_PAGE page, - int nKeyCode, - int modifier, - ) { - return _FORM_OnKeyUp( - hHandle, - page, - nKeyCode, - modifier, - ); - } - - late final _FORM_OnKeyUpPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_FORMHANDLE, FPDF_PAGE, ffi.Int, ffi.Int)>>('FORM_OnKeyUp'); - late final _FORM_OnKeyUp = _FORM_OnKeyUpPtr.asFunction< - int Function(FPDF_FORMHANDLE, FPDF_PAGE, int, int)>(); - - int FORM_OnChar( - FPDF_FORMHANDLE hHandle, - FPDF_PAGE page, - int nChar, - int modifier, - ) { - return _FORM_OnChar( - hHandle, - page, - nChar, - modifier, - ); - } - - late final _FORM_OnCharPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_FORMHANDLE, FPDF_PAGE, ffi.Int, ffi.Int)>>('FORM_OnChar'); - late final _FORM_OnChar = _FORM_OnCharPtr.asFunction< - int Function(FPDF_FORMHANDLE, FPDF_PAGE, int, int)>(); - - int FORM_GetFocusedText( - FPDF_FORMHANDLE hHandle, - FPDF_PAGE page, - ffi.Pointer buffer, - int buflen, - ) { - return _FORM_GetFocusedText( - hHandle, - page, - buffer, - buflen, - ); - } - - late final _FORM_GetFocusedTextPtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function(FPDF_FORMHANDLE, FPDF_PAGE, - ffi.Pointer, ffi.UnsignedLong)>>('FORM_GetFocusedText'); - late final _FORM_GetFocusedText = _FORM_GetFocusedTextPtr.asFunction< - int Function(FPDF_FORMHANDLE, FPDF_PAGE, ffi.Pointer, int)>(); - - int FORM_GetSelectedText( - FPDF_FORMHANDLE hHandle, - FPDF_PAGE page, - ffi.Pointer buffer, - int buflen, - ) { - return _FORM_GetSelectedText( - hHandle, - page, - buffer, - buflen, - ); - } - - late final _FORM_GetSelectedTextPtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function( - FPDF_FORMHANDLE, - FPDF_PAGE, - ffi.Pointer, - ffi.UnsignedLong)>>('FORM_GetSelectedText'); - late final _FORM_GetSelectedText = _FORM_GetSelectedTextPtr.asFunction< - int Function(FPDF_FORMHANDLE, FPDF_PAGE, ffi.Pointer, int)>(); - - void FORM_ReplaceAndKeepSelection( - FPDF_FORMHANDLE hHandle, - FPDF_PAGE page, - FPDF_WIDESTRING wsText, - ) { - return _FORM_ReplaceAndKeepSelection( - hHandle, - page, - wsText, - ); - } - - late final _FORM_ReplaceAndKeepSelectionPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(FPDF_FORMHANDLE, FPDF_PAGE, - FPDF_WIDESTRING)>>('FORM_ReplaceAndKeepSelection'); - late final _FORM_ReplaceAndKeepSelection = _FORM_ReplaceAndKeepSelectionPtr - .asFunction(); - - void FORM_ReplaceSelection( - FPDF_FORMHANDLE hHandle, - FPDF_PAGE page, - FPDF_WIDESTRING wsText, - ) { - return _FORM_ReplaceSelection( - hHandle, - page, - wsText, - ); - } - - late final _FORM_ReplaceSelectionPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(FPDF_FORMHANDLE, FPDF_PAGE, - FPDF_WIDESTRING)>>('FORM_ReplaceSelection'); - late final _FORM_ReplaceSelection = _FORM_ReplaceSelectionPtr.asFunction< - void Function(FPDF_FORMHANDLE, FPDF_PAGE, FPDF_WIDESTRING)>(); - - int FORM_SelectAllText( - FPDF_FORMHANDLE hHandle, - FPDF_PAGE page, - ) { - return _FORM_SelectAllText( - hHandle, - page, - ); - } - - late final _FORM_SelectAllTextPtr = _lookup< - ffi.NativeFunction>( - 'FORM_SelectAllText'); - late final _FORM_SelectAllText = _FORM_SelectAllTextPtr.asFunction< - int Function(FPDF_FORMHANDLE, FPDF_PAGE)>(); - - int FORM_CanUndo( - FPDF_FORMHANDLE hHandle, - FPDF_PAGE page, - ) { - return _FORM_CanUndo( - hHandle, - page, - ); - } - - late final _FORM_CanUndoPtr = _lookup< - ffi.NativeFunction>( - 'FORM_CanUndo'); - late final _FORM_CanUndo = - _FORM_CanUndoPtr.asFunction(); - - int FORM_CanRedo( - FPDF_FORMHANDLE hHandle, - FPDF_PAGE page, - ) { - return _FORM_CanRedo( - hHandle, - page, - ); - } - - late final _FORM_CanRedoPtr = _lookup< - ffi.NativeFunction>( - 'FORM_CanRedo'); - late final _FORM_CanRedo = - _FORM_CanRedoPtr.asFunction(); - - int FORM_Undo( - FPDF_FORMHANDLE hHandle, - FPDF_PAGE page, - ) { - return _FORM_Undo( - hHandle, - page, - ); - } - - late final _FORM_UndoPtr = _lookup< - ffi.NativeFunction>( - 'FORM_Undo'); - late final _FORM_Undo = - _FORM_UndoPtr.asFunction(); - - int FORM_Redo( - FPDF_FORMHANDLE hHandle, - FPDF_PAGE page, - ) { - return _FORM_Redo( - hHandle, - page, - ); - } - - late final _FORM_RedoPtr = _lookup< - ffi.NativeFunction>( - 'FORM_Redo'); - late final _FORM_Redo = - _FORM_RedoPtr.asFunction(); - - int FORM_ForceToKillFocus( - FPDF_FORMHANDLE hHandle, - ) { - return _FORM_ForceToKillFocus( - hHandle, - ); - } - - late final _FORM_ForceToKillFocusPtr = - _lookup>( - 'FORM_ForceToKillFocus'); - late final _FORM_ForceToKillFocus = - _FORM_ForceToKillFocusPtr.asFunction(); - - int FORM_GetFocusedAnnot( - FPDF_FORMHANDLE handle, - ffi.Pointer page_index, - ffi.Pointer annot, - ) { - return _FORM_GetFocusedAnnot( - handle, - page_index, - annot, - ); - } - - late final _FORM_GetFocusedAnnotPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_FORMHANDLE, ffi.Pointer, - ffi.Pointer)>>('FORM_GetFocusedAnnot'); - late final _FORM_GetFocusedAnnot = _FORM_GetFocusedAnnotPtr.asFunction< - int Function(FPDF_FORMHANDLE, ffi.Pointer, - ffi.Pointer)>(); - - int FORM_SetFocusedAnnot( - FPDF_FORMHANDLE handle, - FPDF_ANNOTATION annot, - ) { - return _FORM_SetFocusedAnnot( - handle, - annot, - ); - } - - late final _FORM_SetFocusedAnnotPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_FORMHANDLE, FPDF_ANNOTATION)>>('FORM_SetFocusedAnnot'); - late final _FORM_SetFocusedAnnot = _FORM_SetFocusedAnnotPtr.asFunction< - int Function(FPDF_FORMHANDLE, FPDF_ANNOTATION)>(); - - int FPDFPage_HasFormFieldAtPoint( - FPDF_FORMHANDLE hHandle, - FPDF_PAGE page, - double page_x, - double page_y, - ) { - return _FPDFPage_HasFormFieldAtPoint( - hHandle, - page, - page_x, - page_y, - ); - } - - late final _FPDFPage_HasFormFieldAtPointPtr = _lookup< - ffi.NativeFunction< - ffi.Int Function(FPDF_FORMHANDLE, FPDF_PAGE, ffi.Double, - ffi.Double)>>('FPDFPage_HasFormFieldAtPoint'); - late final _FPDFPage_HasFormFieldAtPoint = _FPDFPage_HasFormFieldAtPointPtr - .asFunction(); - - int FPDFPage_FormFieldZOrderAtPoint( - FPDF_FORMHANDLE hHandle, - FPDF_PAGE page, - double page_x, - double page_y, - ) { - return _FPDFPage_FormFieldZOrderAtPoint( - hHandle, - page, - page_x, - page_y, - ); - } - - late final _FPDFPage_FormFieldZOrderAtPointPtr = _lookup< - ffi.NativeFunction< - ffi.Int Function(FPDF_FORMHANDLE, FPDF_PAGE, ffi.Double, - ffi.Double)>>('FPDFPage_FormFieldZOrderAtPoint'); - late final _FPDFPage_FormFieldZOrderAtPoint = - _FPDFPage_FormFieldZOrderAtPointPtr.asFunction< - int Function(FPDF_FORMHANDLE, FPDF_PAGE, double, double)>(); - - void FPDF_SetFormFieldHighlightColor( - FPDF_FORMHANDLE hHandle, - int fieldType, - int color, - ) { - return _FPDF_SetFormFieldHighlightColor( - hHandle, - fieldType, - color, - ); - } - - late final _FPDF_SetFormFieldHighlightColorPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(FPDF_FORMHANDLE, ffi.Int, - ffi.UnsignedLong)>>('FPDF_SetFormFieldHighlightColor'); - late final _FPDF_SetFormFieldHighlightColor = - _FPDF_SetFormFieldHighlightColorPtr.asFunction< - void Function(FPDF_FORMHANDLE, int, int)>(); - - void FPDF_SetFormFieldHighlightAlpha( - FPDF_FORMHANDLE hHandle, - int alpha, - ) { - return _FPDF_SetFormFieldHighlightAlpha( - hHandle, - alpha, - ); - } - - late final _FPDF_SetFormFieldHighlightAlphaPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(FPDF_FORMHANDLE, - ffi.UnsignedChar)>>('FPDF_SetFormFieldHighlightAlpha'); - late final _FPDF_SetFormFieldHighlightAlpha = - _FPDF_SetFormFieldHighlightAlphaPtr.asFunction< - void Function(FPDF_FORMHANDLE, int)>(); - - void FPDF_RemoveFormFieldHighlight( - FPDF_FORMHANDLE hHandle, - ) { - return _FPDF_RemoveFormFieldHighlight( - hHandle, - ); - } - - late final _FPDF_RemoveFormFieldHighlightPtr = - _lookup>( - 'FPDF_RemoveFormFieldHighlight'); - late final _FPDF_RemoveFormFieldHighlight = _FPDF_RemoveFormFieldHighlightPtr - .asFunction(); - - void FPDF_FFLDraw( - FPDF_FORMHANDLE hHandle, - FPDF_BITMAP bitmap, - FPDF_PAGE page, - int start_x, - int start_y, - int size_x, - int size_y, - int rotate, - int flags, - ) { - return _FPDF_FFLDraw( - hHandle, - bitmap, - page, - start_x, - start_y, - size_x, - size_y, - rotate, - flags, - ); - } - - late final _FPDF_FFLDrawPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(FPDF_FORMHANDLE, FPDF_BITMAP, FPDF_PAGE, ffi.Int, - ffi.Int, ffi.Int, ffi.Int, ffi.Int, ffi.Int)>>('FPDF_FFLDraw'); - late final _FPDF_FFLDraw = _FPDF_FFLDrawPtr.asFunction< - void Function(FPDF_FORMHANDLE, FPDF_BITMAP, FPDF_PAGE, int, int, int, int, - int, int)>(); - - int FPDF_GetFormType( - FPDF_DOCUMENT document, - ) { - return _FPDF_GetFormType( - document, - ); - } - - late final _FPDF_GetFormTypePtr = - _lookup>( - 'FPDF_GetFormType'); - late final _FPDF_GetFormType = - _FPDF_GetFormTypePtr.asFunction(); - - int FORM_SetIndexSelected( - FPDF_FORMHANDLE hHandle, - FPDF_PAGE page, - int index, - int selected, - ) { - return _FORM_SetIndexSelected( - hHandle, - page, - index, - selected, - ); - } - - late final _FORM_SetIndexSelectedPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_FORMHANDLE, FPDF_PAGE, ffi.Int, - FPDF_BOOL)>>('FORM_SetIndexSelected'); - late final _FORM_SetIndexSelected = _FORM_SetIndexSelectedPtr.asFunction< - int Function(FPDF_FORMHANDLE, FPDF_PAGE, int, int)>(); - - int FORM_IsIndexSelected( - FPDF_FORMHANDLE hHandle, - FPDF_PAGE page, - int index, - ) { - return _FORM_IsIndexSelected( - hHandle, - page, - index, - ); - } - - late final _FORM_IsIndexSelectedPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_FORMHANDLE, FPDF_PAGE, ffi.Int)>>('FORM_IsIndexSelected'); - late final _FORM_IsIndexSelected = _FORM_IsIndexSelectedPtr.asFunction< - int Function(FPDF_FORMHANDLE, FPDF_PAGE, int)>(); - - int FPDF_LoadXFA( - FPDF_DOCUMENT document, - ) { - return _FPDF_LoadXFA( - document, - ); - } - - late final _FPDF_LoadXFAPtr = - _lookup>( - 'FPDF_LoadXFA'); - late final _FPDF_LoadXFA = - _FPDF_LoadXFAPtr.asFunction(); - - int FPDFAnnot_IsSupportedSubtype( - int subtype, - ) { - return _FPDFAnnot_IsSupportedSubtype( - subtype, - ); - } - - late final _FPDFAnnot_IsSupportedSubtypePtr = - _lookup>( - 'FPDFAnnot_IsSupportedSubtype'); - late final _FPDFAnnot_IsSupportedSubtype = - _FPDFAnnot_IsSupportedSubtypePtr.asFunction(); - - FPDF_ANNOTATION FPDFPage_CreateAnnot( - FPDF_PAGE page, - int subtype, - ) { - return _FPDFPage_CreateAnnot( - page, - subtype, - ); - } - - late final _FPDFPage_CreateAnnotPtr = _lookup< - ffi.NativeFunction< - FPDF_ANNOTATION Function( - FPDF_PAGE, FPDF_ANNOTATION_SUBTYPE)>>('FPDFPage_CreateAnnot'); - late final _FPDFPage_CreateAnnot = _FPDFPage_CreateAnnotPtr.asFunction< - FPDF_ANNOTATION Function(FPDF_PAGE, int)>(); - - int FPDFPage_GetAnnotCount( - FPDF_PAGE page, - ) { - return _FPDFPage_GetAnnotCount( - page, - ); - } - - late final _FPDFPage_GetAnnotCountPtr = - _lookup>( - 'FPDFPage_GetAnnotCount'); - late final _FPDFPage_GetAnnotCount = - _FPDFPage_GetAnnotCountPtr.asFunction(); - - FPDF_ANNOTATION FPDFPage_GetAnnot( - FPDF_PAGE page, - int index, - ) { - return _FPDFPage_GetAnnot( - page, - index, - ); - } - - late final _FPDFPage_GetAnnotPtr = - _lookup>( - 'FPDFPage_GetAnnot'); - late final _FPDFPage_GetAnnot = _FPDFPage_GetAnnotPtr.asFunction< - FPDF_ANNOTATION Function(FPDF_PAGE, int)>(); - - int FPDFPage_GetAnnotIndex( - FPDF_PAGE page, - FPDF_ANNOTATION annot, - ) { - return _FPDFPage_GetAnnotIndex( - page, - annot, - ); - } - - late final _FPDFPage_GetAnnotIndexPtr = - _lookup>( - 'FPDFPage_GetAnnotIndex'); - late final _FPDFPage_GetAnnotIndex = _FPDFPage_GetAnnotIndexPtr.asFunction< - int Function(FPDF_PAGE, FPDF_ANNOTATION)>(); - - void FPDFPage_CloseAnnot( - FPDF_ANNOTATION annot, - ) { - return _FPDFPage_CloseAnnot( - annot, - ); - } - - late final _FPDFPage_CloseAnnotPtr = - _lookup>( - 'FPDFPage_CloseAnnot'); - late final _FPDFPage_CloseAnnot = - _FPDFPage_CloseAnnotPtr.asFunction(); - - int FPDFPage_RemoveAnnot( - FPDF_PAGE page, - int index, - ) { - return _FPDFPage_RemoveAnnot( - page, - index, - ); - } - - late final _FPDFPage_RemoveAnnotPtr = - _lookup>( - 'FPDFPage_RemoveAnnot'); - late final _FPDFPage_RemoveAnnot = - _FPDFPage_RemoveAnnotPtr.asFunction(); - - int FPDFAnnot_GetSubtype( - FPDF_ANNOTATION annot, - ) { - return _FPDFAnnot_GetSubtype( - annot, - ); - } - - late final _FPDFAnnot_GetSubtypePtr = _lookup< - ffi - .NativeFunction>( - 'FPDFAnnot_GetSubtype'); - late final _FPDFAnnot_GetSubtype = - _FPDFAnnot_GetSubtypePtr.asFunction(); - - int FPDFAnnot_IsObjectSupportedSubtype( - int subtype, - ) { - return _FPDFAnnot_IsObjectSupportedSubtype( - subtype, - ); - } - - late final _FPDFAnnot_IsObjectSupportedSubtypePtr = - _lookup>( - 'FPDFAnnot_IsObjectSupportedSubtype'); - late final _FPDFAnnot_IsObjectSupportedSubtype = - _FPDFAnnot_IsObjectSupportedSubtypePtr.asFunction(); - - int FPDFAnnot_UpdateObject( - FPDF_ANNOTATION annot, - FPDF_PAGEOBJECT obj, - ) { - return _FPDFAnnot_UpdateObject( - annot, - obj, - ); - } - - late final _FPDFAnnot_UpdateObjectPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_ANNOTATION, FPDF_PAGEOBJECT)>>('FPDFAnnot_UpdateObject'); - late final _FPDFAnnot_UpdateObject = _FPDFAnnot_UpdateObjectPtr.asFunction< - int Function(FPDF_ANNOTATION, FPDF_PAGEOBJECT)>(); - - int FPDFAnnot_AddInkStroke( - FPDF_ANNOTATION annot, - ffi.Pointer points, - int point_count, - ) { - return _FPDFAnnot_AddInkStroke( - annot, - points, - point_count, - ); - } - - late final _FPDFAnnot_AddInkStrokePtr = _lookup< - ffi.NativeFunction< - ffi.Int Function(FPDF_ANNOTATION, ffi.Pointer, - ffi.Size)>>('FPDFAnnot_AddInkStroke'); - late final _FPDFAnnot_AddInkStroke = _FPDFAnnot_AddInkStrokePtr.asFunction< - int Function(FPDF_ANNOTATION, ffi.Pointer, int)>(); - - int FPDFAnnot_RemoveInkList( - FPDF_ANNOTATION annot, - ) { - return _FPDFAnnot_RemoveInkList( - annot, - ); - } - - late final _FPDFAnnot_RemoveInkListPtr = - _lookup>( - 'FPDFAnnot_RemoveInkList'); - late final _FPDFAnnot_RemoveInkList = - _FPDFAnnot_RemoveInkListPtr.asFunction(); - - int FPDFAnnot_AppendObject( - FPDF_ANNOTATION annot, - FPDF_PAGEOBJECT obj, - ) { - return _FPDFAnnot_AppendObject( - annot, - obj, - ); - } - - late final _FPDFAnnot_AppendObjectPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_ANNOTATION, FPDF_PAGEOBJECT)>>('FPDFAnnot_AppendObject'); - late final _FPDFAnnot_AppendObject = _FPDFAnnot_AppendObjectPtr.asFunction< - int Function(FPDF_ANNOTATION, FPDF_PAGEOBJECT)>(); - - int FPDFAnnot_GetObjectCount( - FPDF_ANNOTATION annot, - ) { - return _FPDFAnnot_GetObjectCount( - annot, - ); - } - - late final _FPDFAnnot_GetObjectCountPtr = - _lookup>( - 'FPDFAnnot_GetObjectCount'); - late final _FPDFAnnot_GetObjectCount = - _FPDFAnnot_GetObjectCountPtr.asFunction(); - - FPDF_PAGEOBJECT FPDFAnnot_GetObject( - FPDF_ANNOTATION annot, - int index, - ) { - return _FPDFAnnot_GetObject( - annot, - index, - ); - } - - late final _FPDFAnnot_GetObjectPtr = _lookup< - ffi - .NativeFunction>( - 'FPDFAnnot_GetObject'); - late final _FPDFAnnot_GetObject = _FPDFAnnot_GetObjectPtr.asFunction< - FPDF_PAGEOBJECT Function(FPDF_ANNOTATION, int)>(); - - int FPDFAnnot_RemoveObject( - FPDF_ANNOTATION annot, - int index, - ) { - return _FPDFAnnot_RemoveObject( - annot, - index, - ); - } - - late final _FPDFAnnot_RemoveObjectPtr = - _lookup>( - 'FPDFAnnot_RemoveObject'); - late final _FPDFAnnot_RemoveObject = _FPDFAnnot_RemoveObjectPtr.asFunction< - int Function(FPDF_ANNOTATION, int)>(); - - DartFPDF_BOOL FPDFAnnot_SetColor( - FPDF_ANNOTATION annot, - FPDFANNOT_COLORTYPE type, - int R, - int G, - int B, - int A, - ) { - return _FPDFAnnot_SetColor( - annot, - type.value, - R, - G, - B, - A, - ); - } - - late final _FPDFAnnot_SetColorPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_ANNOTATION, - ffi.UnsignedInt, - ffi.UnsignedInt, - ffi.UnsignedInt, - ffi.UnsignedInt, - ffi.UnsignedInt)>>('FPDFAnnot_SetColor'); - late final _FPDFAnnot_SetColor = _FPDFAnnot_SetColorPtr.asFunction< - int Function(FPDF_ANNOTATION, int, int, int, int, int)>(); - - DartFPDF_BOOL FPDFAnnot_GetColor( - FPDF_ANNOTATION annot, - FPDFANNOT_COLORTYPE type, - ffi.Pointer R, - ffi.Pointer G, - ffi.Pointer B, - ffi.Pointer A, - ) { - return _FPDFAnnot_GetColor( - annot, - type.value, - R, - G, - B, - A, - ); - } - - late final _FPDFAnnot_GetColorPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_ANNOTATION, - ffi.UnsignedInt, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>>('FPDFAnnot_GetColor'); - late final _FPDFAnnot_GetColor = _FPDFAnnot_GetColorPtr.asFunction< - int Function( - FPDF_ANNOTATION, - int, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>(); - - int FPDFAnnot_HasAttachmentPoints( - FPDF_ANNOTATION annot, - ) { - return _FPDFAnnot_HasAttachmentPoints( - annot, - ); - } - - late final _FPDFAnnot_HasAttachmentPointsPtr = - _lookup>( - 'FPDFAnnot_HasAttachmentPoints'); - late final _FPDFAnnot_HasAttachmentPoints = _FPDFAnnot_HasAttachmentPointsPtr - .asFunction(); - - int FPDFAnnot_SetAttachmentPoints( - FPDF_ANNOTATION annot, - int quad_index, - ffi.Pointer quad_points, - ) { - return _FPDFAnnot_SetAttachmentPoints( - annot, - quad_index, - quad_points, - ); - } - - late final _FPDFAnnot_SetAttachmentPointsPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_ANNOTATION, ffi.Size, - ffi.Pointer)>>('FPDFAnnot_SetAttachmentPoints'); - late final _FPDFAnnot_SetAttachmentPoints = - _FPDFAnnot_SetAttachmentPointsPtr.asFunction< - int Function(FPDF_ANNOTATION, int, ffi.Pointer)>(); - - int FPDFAnnot_AppendAttachmentPoints( - FPDF_ANNOTATION annot, - ffi.Pointer quad_points, - ) { - return _FPDFAnnot_AppendAttachmentPoints( - annot, - quad_points, - ); - } - - late final _FPDFAnnot_AppendAttachmentPointsPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_ANNOTATION, ffi.Pointer)>>( - 'FPDFAnnot_AppendAttachmentPoints'); - late final _FPDFAnnot_AppendAttachmentPoints = - _FPDFAnnot_AppendAttachmentPointsPtr.asFunction< - int Function(FPDF_ANNOTATION, ffi.Pointer)>(); - - int FPDFAnnot_CountAttachmentPoints( - FPDF_ANNOTATION annot, - ) { - return _FPDFAnnot_CountAttachmentPoints( - annot, - ); - } - - late final _FPDFAnnot_CountAttachmentPointsPtr = - _lookup>( - 'FPDFAnnot_CountAttachmentPoints'); - late final _FPDFAnnot_CountAttachmentPoints = - _FPDFAnnot_CountAttachmentPointsPtr.asFunction< - int Function(FPDF_ANNOTATION)>(); - - int FPDFAnnot_GetAttachmentPoints( - FPDF_ANNOTATION annot, - int quad_index, - ffi.Pointer quad_points, - ) { - return _FPDFAnnot_GetAttachmentPoints( - annot, - quad_index, - quad_points, - ); - } - - late final _FPDFAnnot_GetAttachmentPointsPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_ANNOTATION, ffi.Size, - ffi.Pointer)>>('FPDFAnnot_GetAttachmentPoints'); - late final _FPDFAnnot_GetAttachmentPoints = - _FPDFAnnot_GetAttachmentPointsPtr.asFunction< - int Function(FPDF_ANNOTATION, int, ffi.Pointer)>(); - - int FPDFAnnot_SetRect( - FPDF_ANNOTATION annot, - ffi.Pointer rect, - ) { - return _FPDFAnnot_SetRect( - annot, - rect, - ); - } - - late final _FPDFAnnot_SetRectPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_ANNOTATION, ffi.Pointer)>>('FPDFAnnot_SetRect'); - late final _FPDFAnnot_SetRect = _FPDFAnnot_SetRectPtr.asFunction< - int Function(FPDF_ANNOTATION, ffi.Pointer)>(); - - int FPDFAnnot_GetRect( - FPDF_ANNOTATION annot, - ffi.Pointer rect, - ) { - return _FPDFAnnot_GetRect( - annot, - rect, - ); - } - - late final _FPDFAnnot_GetRectPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_ANNOTATION, ffi.Pointer)>>('FPDFAnnot_GetRect'); - late final _FPDFAnnot_GetRect = _FPDFAnnot_GetRectPtr.asFunction< - int Function(FPDF_ANNOTATION, ffi.Pointer)>(); - - int FPDFAnnot_GetVertices( - FPDF_ANNOTATION annot, - ffi.Pointer buffer, - int length, - ) { - return _FPDFAnnot_GetVertices( - annot, - buffer, - length, - ); - } - - late final _FPDFAnnot_GetVerticesPtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function(FPDF_ANNOTATION, ffi.Pointer, - ffi.UnsignedLong)>>('FPDFAnnot_GetVertices'); - late final _FPDFAnnot_GetVertices = _FPDFAnnot_GetVerticesPtr.asFunction< - int Function(FPDF_ANNOTATION, ffi.Pointer, int)>(); - - int FPDFAnnot_GetInkListCount( - FPDF_ANNOTATION annot, - ) { - return _FPDFAnnot_GetInkListCount( - annot, - ); - } - - late final _FPDFAnnot_GetInkListCountPtr = - _lookup>( - 'FPDFAnnot_GetInkListCount'); - late final _FPDFAnnot_GetInkListCount = - _FPDFAnnot_GetInkListCountPtr.asFunction(); - - int FPDFAnnot_GetInkListPath( - FPDF_ANNOTATION annot, - int path_index, - ffi.Pointer buffer, - int length, - ) { - return _FPDFAnnot_GetInkListPath( - annot, - path_index, - buffer, - length, - ); - } - - late final _FPDFAnnot_GetInkListPathPtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function( - FPDF_ANNOTATION, - ffi.UnsignedLong, - ffi.Pointer, - ffi.UnsignedLong)>>('FPDFAnnot_GetInkListPath'); - late final _FPDFAnnot_GetInkListPath = - _FPDFAnnot_GetInkListPathPtr.asFunction< - int Function(FPDF_ANNOTATION, int, ffi.Pointer, int)>(); - - int FPDFAnnot_GetLine( - FPDF_ANNOTATION annot, - ffi.Pointer start, - ffi.Pointer end, - ) { - return _FPDFAnnot_GetLine( - annot, - start, - end, - ); - } - - late final _FPDFAnnot_GetLinePtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_ANNOTATION, ffi.Pointer, - ffi.Pointer)>>('FPDFAnnot_GetLine'); - late final _FPDFAnnot_GetLine = _FPDFAnnot_GetLinePtr.asFunction< - int Function( - FPDF_ANNOTATION, ffi.Pointer, ffi.Pointer)>(); - - int FPDFAnnot_SetBorder( - FPDF_ANNOTATION annot, - double horizontal_radius, - double vertical_radius, - double border_width, - ) { - return _FPDFAnnot_SetBorder( - annot, - horizontal_radius, - vertical_radius, - border_width, - ); - } - - late final _FPDFAnnot_SetBorderPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_ANNOTATION, ffi.Float, ffi.Float, - ffi.Float)>>('FPDFAnnot_SetBorder'); - late final _FPDFAnnot_SetBorder = _FPDFAnnot_SetBorderPtr.asFunction< - int Function(FPDF_ANNOTATION, double, double, double)>(); - - int FPDFAnnot_GetBorder( - FPDF_ANNOTATION annot, - ffi.Pointer horizontal_radius, - ffi.Pointer vertical_radius, - ffi.Pointer border_width, - ) { - return _FPDFAnnot_GetBorder( - annot, - horizontal_radius, - vertical_radius, - border_width, - ); - } - - late final _FPDFAnnot_GetBorderPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_ANNOTATION, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>>('FPDFAnnot_GetBorder'); - late final _FPDFAnnot_GetBorder = _FPDFAnnot_GetBorderPtr.asFunction< - int Function(FPDF_ANNOTATION, ffi.Pointer, - ffi.Pointer, ffi.Pointer)>(); - - int FPDFAnnot_GetFormAdditionalActionJavaScript( - FPDF_FORMHANDLE hHandle, - FPDF_ANNOTATION annot, - int event, - ffi.Pointer buffer, - int buflen, - ) { - return _FPDFAnnot_GetFormAdditionalActionJavaScript( - hHandle, - annot, - event, - buffer, - buflen, - ); - } - - late final _FPDFAnnot_GetFormAdditionalActionJavaScriptPtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function(FPDF_FORMHANDLE, FPDF_ANNOTATION, - ffi.Int, ffi.Pointer, ffi.UnsignedLong)>>( - 'FPDFAnnot_GetFormAdditionalActionJavaScript'); - late final _FPDFAnnot_GetFormAdditionalActionJavaScript = - _FPDFAnnot_GetFormAdditionalActionJavaScriptPtr.asFunction< - int Function(FPDF_FORMHANDLE, FPDF_ANNOTATION, int, - ffi.Pointer, int)>(); - - int FPDFAnnot_HasKey( - FPDF_ANNOTATION annot, - FPDF_BYTESTRING key, - ) { - return _FPDFAnnot_HasKey( - annot, - key, - ); - } - - late final _FPDFAnnot_HasKeyPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_ANNOTATION, FPDF_BYTESTRING)>>('FPDFAnnot_HasKey'); - late final _FPDFAnnot_HasKey = _FPDFAnnot_HasKeyPtr.asFunction< - int Function(FPDF_ANNOTATION, FPDF_BYTESTRING)>(); - - int FPDFAnnot_GetValueType( - FPDF_ANNOTATION annot, - FPDF_BYTESTRING key, - ) { - return _FPDFAnnot_GetValueType( - annot, - key, - ); - } - - late final _FPDFAnnot_GetValueTypePtr = _lookup< - ffi.NativeFunction< - FPDF_OBJECT_TYPE Function( - FPDF_ANNOTATION, FPDF_BYTESTRING)>>('FPDFAnnot_GetValueType'); - late final _FPDFAnnot_GetValueType = _FPDFAnnot_GetValueTypePtr.asFunction< - int Function(FPDF_ANNOTATION, FPDF_BYTESTRING)>(); - - int FPDFAnnot_SetStringValue( - FPDF_ANNOTATION annot, - FPDF_BYTESTRING key, - FPDF_WIDESTRING value, - ) { - return _FPDFAnnot_SetStringValue( - annot, - key, - value, - ); - } - - late final _FPDFAnnot_SetStringValuePtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_ANNOTATION, FPDF_BYTESTRING, - FPDF_WIDESTRING)>>('FPDFAnnot_SetStringValue'); - late final _FPDFAnnot_SetStringValue = - _FPDFAnnot_SetStringValuePtr.asFunction< - int Function(FPDF_ANNOTATION, FPDF_BYTESTRING, FPDF_WIDESTRING)>(); - - int FPDFAnnot_GetStringValue( - FPDF_ANNOTATION annot, - FPDF_BYTESTRING key, - ffi.Pointer buffer, - int buflen, - ) { - return _FPDFAnnot_GetStringValue( - annot, - key, - buffer, - buflen, - ); - } - - late final _FPDFAnnot_GetStringValuePtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function( - FPDF_ANNOTATION, - FPDF_BYTESTRING, - ffi.Pointer, - ffi.UnsignedLong)>>('FPDFAnnot_GetStringValue'); - late final _FPDFAnnot_GetStringValue = - _FPDFAnnot_GetStringValuePtr.asFunction< - int Function(FPDF_ANNOTATION, FPDF_BYTESTRING, - ffi.Pointer, int)>(); - - int FPDFAnnot_GetNumberValue( - FPDF_ANNOTATION annot, - FPDF_BYTESTRING key, - ffi.Pointer value, - ) { - return _FPDFAnnot_GetNumberValue( - annot, - key, - value, - ); - } - - late final _FPDFAnnot_GetNumberValuePtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_ANNOTATION, FPDF_BYTESTRING, - ffi.Pointer)>>('FPDFAnnot_GetNumberValue'); - late final _FPDFAnnot_GetNumberValue = - _FPDFAnnot_GetNumberValuePtr.asFunction< - int Function( - FPDF_ANNOTATION, FPDF_BYTESTRING, ffi.Pointer)>(); - - int FPDFAnnot_SetAP( - FPDF_ANNOTATION annot, - int appearanceMode, - FPDF_WIDESTRING value, - ) { - return _FPDFAnnot_SetAP( - annot, - appearanceMode, - value, - ); - } - - late final _FPDFAnnot_SetAPPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_ANNOTATION, FPDF_ANNOT_APPEARANCEMODE, - FPDF_WIDESTRING)>>('FPDFAnnot_SetAP'); - late final _FPDFAnnot_SetAP = _FPDFAnnot_SetAPPtr.asFunction< - int Function(FPDF_ANNOTATION, int, FPDF_WIDESTRING)>(); - - int FPDFAnnot_GetAP( - FPDF_ANNOTATION annot, - int appearanceMode, - ffi.Pointer buffer, - int buflen, - ) { - return _FPDFAnnot_GetAP( - annot, - appearanceMode, - buffer, - buflen, - ); - } - - late final _FPDFAnnot_GetAPPtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function(FPDF_ANNOTATION, FPDF_ANNOT_APPEARANCEMODE, - ffi.Pointer, ffi.UnsignedLong)>>('FPDFAnnot_GetAP'); - late final _FPDFAnnot_GetAP = _FPDFAnnot_GetAPPtr.asFunction< - int Function(FPDF_ANNOTATION, int, ffi.Pointer, int)>(); - - FPDF_ANNOTATION FPDFAnnot_GetLinkedAnnot( - FPDF_ANNOTATION annot, - FPDF_BYTESTRING key, - ) { - return _FPDFAnnot_GetLinkedAnnot( - annot, - key, - ); - } - - late final _FPDFAnnot_GetLinkedAnnotPtr = _lookup< - ffi.NativeFunction< - FPDF_ANNOTATION Function( - FPDF_ANNOTATION, FPDF_BYTESTRING)>>('FPDFAnnot_GetLinkedAnnot'); - late final _FPDFAnnot_GetLinkedAnnot = _FPDFAnnot_GetLinkedAnnotPtr - .asFunction(); - - int FPDFAnnot_GetFlags( - FPDF_ANNOTATION annot, - ) { - return _FPDFAnnot_GetFlags( - annot, - ); - } - - late final _FPDFAnnot_GetFlagsPtr = - _lookup>( - 'FPDFAnnot_GetFlags'); - late final _FPDFAnnot_GetFlags = - _FPDFAnnot_GetFlagsPtr.asFunction(); - - int FPDFAnnot_SetFlags( - FPDF_ANNOTATION annot, - int flags, - ) { - return _FPDFAnnot_SetFlags( - annot, - flags, - ); - } - - late final _FPDFAnnot_SetFlagsPtr = - _lookup>( - 'FPDFAnnot_SetFlags'); - late final _FPDFAnnot_SetFlags = - _FPDFAnnot_SetFlagsPtr.asFunction(); - - int FPDFAnnot_GetFormFieldFlags( - FPDF_FORMHANDLE handle, - FPDF_ANNOTATION annot, - ) { - return _FPDFAnnot_GetFormFieldFlags( - handle, - annot, - ); - } - - late final _FPDFAnnot_GetFormFieldFlagsPtr = _lookup< - ffi - .NativeFunction>( - 'FPDFAnnot_GetFormFieldFlags'); - late final _FPDFAnnot_GetFormFieldFlags = _FPDFAnnot_GetFormFieldFlagsPtr - .asFunction(); - - FPDF_ANNOTATION FPDFAnnot_GetFormFieldAtPoint( - FPDF_FORMHANDLE hHandle, - FPDF_PAGE page, - ffi.Pointer point, - ) { - return _FPDFAnnot_GetFormFieldAtPoint( - hHandle, - page, - point, - ); - } - - late final _FPDFAnnot_GetFormFieldAtPointPtr = _lookup< - ffi.NativeFunction< - FPDF_ANNOTATION Function(FPDF_FORMHANDLE, FPDF_PAGE, - ffi.Pointer)>>('FPDFAnnot_GetFormFieldAtPoint'); - late final _FPDFAnnot_GetFormFieldAtPoint = - _FPDFAnnot_GetFormFieldAtPointPtr.asFunction< - FPDF_ANNOTATION Function( - FPDF_FORMHANDLE, FPDF_PAGE, ffi.Pointer)>(); - - int FPDFAnnot_GetFormFieldName( - FPDF_FORMHANDLE hHandle, - FPDF_ANNOTATION annot, - ffi.Pointer buffer, - int buflen, - ) { - return _FPDFAnnot_GetFormFieldName( - hHandle, - annot, - buffer, - buflen, - ); - } - - late final _FPDFAnnot_GetFormFieldNamePtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function( - FPDF_FORMHANDLE, - FPDF_ANNOTATION, - ffi.Pointer, - ffi.UnsignedLong)>>('FPDFAnnot_GetFormFieldName'); - late final _FPDFAnnot_GetFormFieldName = - _FPDFAnnot_GetFormFieldNamePtr.asFunction< - int Function(FPDF_FORMHANDLE, FPDF_ANNOTATION, - ffi.Pointer, int)>(); - - int FPDFAnnot_GetFormFieldAlternateName( - FPDF_FORMHANDLE hHandle, - FPDF_ANNOTATION annot, - ffi.Pointer buffer, - int buflen, - ) { - return _FPDFAnnot_GetFormFieldAlternateName( - hHandle, - annot, - buffer, - buflen, - ); - } - - late final _FPDFAnnot_GetFormFieldAlternateNamePtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function( - FPDF_FORMHANDLE, - FPDF_ANNOTATION, - ffi.Pointer, - ffi.UnsignedLong)>>('FPDFAnnot_GetFormFieldAlternateName'); - late final _FPDFAnnot_GetFormFieldAlternateName = - _FPDFAnnot_GetFormFieldAlternateNamePtr.asFunction< - int Function(FPDF_FORMHANDLE, FPDF_ANNOTATION, - ffi.Pointer, int)>(); - - int FPDFAnnot_GetFormFieldType( - FPDF_FORMHANDLE hHandle, - FPDF_ANNOTATION annot, - ) { - return _FPDFAnnot_GetFormFieldType( - hHandle, - annot, - ); - } - - late final _FPDFAnnot_GetFormFieldTypePtr = _lookup< - ffi - .NativeFunction>( - 'FPDFAnnot_GetFormFieldType'); - late final _FPDFAnnot_GetFormFieldType = _FPDFAnnot_GetFormFieldTypePtr - .asFunction(); - - int FPDFAnnot_GetFormFieldValue( - FPDF_FORMHANDLE hHandle, - FPDF_ANNOTATION annot, - ffi.Pointer buffer, - int buflen, - ) { - return _FPDFAnnot_GetFormFieldValue( - hHandle, - annot, - buffer, - buflen, - ); - } - - late final _FPDFAnnot_GetFormFieldValuePtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function( - FPDF_FORMHANDLE, - FPDF_ANNOTATION, - ffi.Pointer, - ffi.UnsignedLong)>>('FPDFAnnot_GetFormFieldValue'); - late final _FPDFAnnot_GetFormFieldValue = - _FPDFAnnot_GetFormFieldValuePtr.asFunction< - int Function(FPDF_FORMHANDLE, FPDF_ANNOTATION, - ffi.Pointer, int)>(); - - int FPDFAnnot_GetOptionCount( - FPDF_FORMHANDLE hHandle, - FPDF_ANNOTATION annot, - ) { - return _FPDFAnnot_GetOptionCount( - hHandle, - annot, - ); - } - - late final _FPDFAnnot_GetOptionCountPtr = _lookup< - ffi - .NativeFunction>( - 'FPDFAnnot_GetOptionCount'); - late final _FPDFAnnot_GetOptionCount = _FPDFAnnot_GetOptionCountPtr - .asFunction(); - - int FPDFAnnot_GetOptionLabel( - FPDF_FORMHANDLE hHandle, - FPDF_ANNOTATION annot, - int index, - ffi.Pointer buffer, - int buflen, - ) { - return _FPDFAnnot_GetOptionLabel( - hHandle, - annot, - index, - buffer, - buflen, - ); - } - - late final _FPDFAnnot_GetOptionLabelPtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function( - FPDF_FORMHANDLE, - FPDF_ANNOTATION, - ffi.Int, - ffi.Pointer, - ffi.UnsignedLong)>>('FPDFAnnot_GetOptionLabel'); - late final _FPDFAnnot_GetOptionLabel = - _FPDFAnnot_GetOptionLabelPtr.asFunction< - int Function(FPDF_FORMHANDLE, FPDF_ANNOTATION, int, - ffi.Pointer, int)>(); - - int FPDFAnnot_IsOptionSelected( - FPDF_FORMHANDLE handle, - FPDF_ANNOTATION annot, - int index, - ) { - return _FPDFAnnot_IsOptionSelected( - handle, - annot, - index, - ); - } - - late final _FPDFAnnot_IsOptionSelectedPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_FORMHANDLE, FPDF_ANNOTATION, - ffi.Int)>>('FPDFAnnot_IsOptionSelected'); - late final _FPDFAnnot_IsOptionSelected = _FPDFAnnot_IsOptionSelectedPtr - .asFunction(); - - int FPDFAnnot_GetFontSize( - FPDF_FORMHANDLE hHandle, - FPDF_ANNOTATION annot, - ffi.Pointer value, - ) { - return _FPDFAnnot_GetFontSize( - hHandle, - annot, - value, - ); - } - - late final _FPDFAnnot_GetFontSizePtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_FORMHANDLE, FPDF_ANNOTATION, - ffi.Pointer)>>('FPDFAnnot_GetFontSize'); - late final _FPDFAnnot_GetFontSize = _FPDFAnnot_GetFontSizePtr.asFunction< - int Function(FPDF_FORMHANDLE, FPDF_ANNOTATION, ffi.Pointer)>(); - - int FPDFAnnot_GetFontColor( - FPDF_FORMHANDLE hHandle, - FPDF_ANNOTATION annot, - ffi.Pointer R, - ffi.Pointer G, - ffi.Pointer B, - ) { - return _FPDFAnnot_GetFontColor( - hHandle, - annot, - R, - G, - B, - ); - } - - late final _FPDFAnnot_GetFontColorPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_FORMHANDLE, - FPDF_ANNOTATION, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>>('FPDFAnnot_GetFontColor'); - late final _FPDFAnnot_GetFontColor = _FPDFAnnot_GetFontColorPtr.asFunction< - int Function( - FPDF_FORMHANDLE, - FPDF_ANNOTATION, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>(); - - int FPDFAnnot_IsChecked( - FPDF_FORMHANDLE hHandle, - FPDF_ANNOTATION annot, - ) { - return _FPDFAnnot_IsChecked( - hHandle, - annot, - ); - } - - late final _FPDFAnnot_IsCheckedPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_FORMHANDLE, FPDF_ANNOTATION)>>('FPDFAnnot_IsChecked'); - late final _FPDFAnnot_IsChecked = _FPDFAnnot_IsCheckedPtr.asFunction< - int Function(FPDF_FORMHANDLE, FPDF_ANNOTATION)>(); - - int FPDFAnnot_SetFocusableSubtypes( - FPDF_FORMHANDLE hHandle, - ffi.Pointer subtypes, - int count, - ) { - return _FPDFAnnot_SetFocusableSubtypes( - hHandle, - subtypes, - count, - ); - } - - late final _FPDFAnnot_SetFocusableSubtypesPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_FORMHANDLE, - ffi.Pointer, - ffi.Size)>>('FPDFAnnot_SetFocusableSubtypes'); - late final _FPDFAnnot_SetFocusableSubtypes = - _FPDFAnnot_SetFocusableSubtypesPtr.asFunction< - int Function( - FPDF_FORMHANDLE, ffi.Pointer, int)>(); - - int FPDFAnnot_GetFocusableSubtypesCount( - FPDF_FORMHANDLE hHandle, - ) { - return _FPDFAnnot_GetFocusableSubtypesCount( - hHandle, - ); - } - - late final _FPDFAnnot_GetFocusableSubtypesCountPtr = - _lookup>( - 'FPDFAnnot_GetFocusableSubtypesCount'); - late final _FPDFAnnot_GetFocusableSubtypesCount = - _FPDFAnnot_GetFocusableSubtypesCountPtr.asFunction< - int Function(FPDF_FORMHANDLE)>(); - - int FPDFAnnot_GetFocusableSubtypes( - FPDF_FORMHANDLE hHandle, - ffi.Pointer subtypes, - int count, - ) { - return _FPDFAnnot_GetFocusableSubtypes( - hHandle, - subtypes, - count, - ); - } - - late final _FPDFAnnot_GetFocusableSubtypesPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_FORMHANDLE, - ffi.Pointer, - ffi.Size)>>('FPDFAnnot_GetFocusableSubtypes'); - late final _FPDFAnnot_GetFocusableSubtypes = - _FPDFAnnot_GetFocusableSubtypesPtr.asFunction< - int Function( - FPDF_FORMHANDLE, ffi.Pointer, int)>(); - - FPDF_LINK FPDFAnnot_GetLink( - FPDF_ANNOTATION annot, - ) { - return _FPDFAnnot_GetLink( - annot, - ); - } - - late final _FPDFAnnot_GetLinkPtr = - _lookup>( - 'FPDFAnnot_GetLink'); - late final _FPDFAnnot_GetLink = - _FPDFAnnot_GetLinkPtr.asFunction(); - - int FPDFAnnot_GetFormControlCount( - FPDF_FORMHANDLE hHandle, - FPDF_ANNOTATION annot, - ) { - return _FPDFAnnot_GetFormControlCount( - hHandle, - annot, - ); - } - - late final _FPDFAnnot_GetFormControlCountPtr = _lookup< - ffi - .NativeFunction>( - 'FPDFAnnot_GetFormControlCount'); - late final _FPDFAnnot_GetFormControlCount = _FPDFAnnot_GetFormControlCountPtr - .asFunction(); - - int FPDFAnnot_GetFormControlIndex( - FPDF_FORMHANDLE hHandle, - FPDF_ANNOTATION annot, - ) { - return _FPDFAnnot_GetFormControlIndex( - hHandle, - annot, - ); - } - - late final _FPDFAnnot_GetFormControlIndexPtr = _lookup< - ffi - .NativeFunction>( - 'FPDFAnnot_GetFormControlIndex'); - late final _FPDFAnnot_GetFormControlIndex = _FPDFAnnot_GetFormControlIndexPtr - .asFunction(); - - int FPDFAnnot_GetFormFieldExportValue( - FPDF_FORMHANDLE hHandle, - FPDF_ANNOTATION annot, - ffi.Pointer buffer, - int buflen, - ) { - return _FPDFAnnot_GetFormFieldExportValue( - hHandle, - annot, - buffer, - buflen, - ); - } - - late final _FPDFAnnot_GetFormFieldExportValuePtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function( - FPDF_FORMHANDLE, - FPDF_ANNOTATION, - ffi.Pointer, - ffi.UnsignedLong)>>('FPDFAnnot_GetFormFieldExportValue'); - late final _FPDFAnnot_GetFormFieldExportValue = - _FPDFAnnot_GetFormFieldExportValuePtr.asFunction< - int Function(FPDF_FORMHANDLE, FPDF_ANNOTATION, - ffi.Pointer, int)>(); - - int FPDFAnnot_SetURI( - FPDF_ANNOTATION annot, - ffi.Pointer uri, - ) { - return _FPDFAnnot_SetURI( - annot, - uri, - ); - } - - late final _FPDFAnnot_SetURIPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_ANNOTATION, ffi.Pointer)>>('FPDFAnnot_SetURI'); - late final _FPDFAnnot_SetURI = _FPDFAnnot_SetURIPtr.asFunction< - int Function(FPDF_ANNOTATION, ffi.Pointer)>(); - - FPDF_ATTACHMENT FPDFAnnot_GetFileAttachment( - FPDF_ANNOTATION annot, - ) { - return _FPDFAnnot_GetFileAttachment( - annot, - ); - } - - late final _FPDFAnnot_GetFileAttachmentPtr = - _lookup>( - 'FPDFAnnot_GetFileAttachment'); - late final _FPDFAnnot_GetFileAttachment = _FPDFAnnot_GetFileAttachmentPtr - .asFunction(); - - FPDF_ATTACHMENT FPDFAnnot_AddFileAttachment( - FPDF_ANNOTATION annot, - FPDF_WIDESTRING name, - ) { - return _FPDFAnnot_AddFileAttachment( - annot, - name, - ); - } - - late final _FPDFAnnot_AddFileAttachmentPtr = _lookup< - ffi.NativeFunction< - FPDF_ATTACHMENT Function(FPDF_ANNOTATION, - FPDF_WIDESTRING)>>('FPDFAnnot_AddFileAttachment'); - late final _FPDFAnnot_AddFileAttachment = _FPDFAnnot_AddFileAttachmentPtr - .asFunction(); - - FPDF_TEXTPAGE FPDFText_LoadPage( - FPDF_PAGE page, - ) { - return _FPDFText_LoadPage( - page, - ); - } - - late final _FPDFText_LoadPagePtr = - _lookup>( - 'FPDFText_LoadPage'); - late final _FPDFText_LoadPage = - _FPDFText_LoadPagePtr.asFunction(); - - void FPDFText_ClosePage( - FPDF_TEXTPAGE text_page, - ) { - return _FPDFText_ClosePage( - text_page, - ); - } - - late final _FPDFText_ClosePagePtr = - _lookup>( - 'FPDFText_ClosePage'); - late final _FPDFText_ClosePage = - _FPDFText_ClosePagePtr.asFunction(); - - int FPDFText_CountChars( - FPDF_TEXTPAGE text_page, - ) { - return _FPDFText_CountChars( - text_page, - ); - } - - late final _FPDFText_CountCharsPtr = - _lookup>( - 'FPDFText_CountChars'); - late final _FPDFText_CountChars = - _FPDFText_CountCharsPtr.asFunction(); - - int FPDFText_GetUnicode( - FPDF_TEXTPAGE text_page, - int index, - ) { - return _FPDFText_GetUnicode( - text_page, - index, - ); - } - - late final _FPDFText_GetUnicodePtr = _lookup< - ffi.NativeFunction>( - 'FPDFText_GetUnicode'); - late final _FPDFText_GetUnicode = - _FPDFText_GetUnicodePtr.asFunction(); - - int FPDFText_IsGenerated( - FPDF_TEXTPAGE text_page, - int index, - ) { - return _FPDFText_IsGenerated( - text_page, - index, - ); - } - - late final _FPDFText_IsGeneratedPtr = - _lookup>( - 'FPDFText_IsGenerated'); - late final _FPDFText_IsGenerated = - _FPDFText_IsGeneratedPtr.asFunction(); - - int FPDFText_IsHyphen( - FPDF_TEXTPAGE text_page, - int index, - ) { - return _FPDFText_IsHyphen( - text_page, - index, - ); - } - - late final _FPDFText_IsHyphenPtr = - _lookup>( - 'FPDFText_IsHyphen'); - late final _FPDFText_IsHyphen = - _FPDFText_IsHyphenPtr.asFunction(); - - int FPDFText_HasUnicodeMapError( - FPDF_TEXTPAGE text_page, - int index, - ) { - return _FPDFText_HasUnicodeMapError( - text_page, - index, - ); - } - - late final _FPDFText_HasUnicodeMapErrorPtr = - _lookup>( - 'FPDFText_HasUnicodeMapError'); - late final _FPDFText_HasUnicodeMapError = _FPDFText_HasUnicodeMapErrorPtr - .asFunction(); - - double FPDFText_GetFontSize( - FPDF_TEXTPAGE text_page, - int index, - ) { - return _FPDFText_GetFontSize( - text_page, - index, - ); - } - - late final _FPDFText_GetFontSizePtr = - _lookup>( - 'FPDFText_GetFontSize'); - late final _FPDFText_GetFontSize = _FPDFText_GetFontSizePtr.asFunction< - double Function(FPDF_TEXTPAGE, int)>(); - - int FPDFText_GetFontInfo( - FPDF_TEXTPAGE text_page, - int index, - ffi.Pointer buffer, - int buflen, - ffi.Pointer flags, - ) { - return _FPDFText_GetFontInfo( - text_page, - index, - buffer, - buflen, - flags, - ); - } - - late final _FPDFText_GetFontInfoPtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function( - FPDF_TEXTPAGE, - ffi.Int, - ffi.Pointer, - ffi.UnsignedLong, - ffi.Pointer)>>('FPDFText_GetFontInfo'); - late final _FPDFText_GetFontInfo = _FPDFText_GetFontInfoPtr.asFunction< - int Function(FPDF_TEXTPAGE, int, ffi.Pointer, int, - ffi.Pointer)>(); - - int FPDFText_GetFontWeight( - FPDF_TEXTPAGE text_page, - int index, - ) { - return _FPDFText_GetFontWeight( - text_page, - index, - ); - } - - late final _FPDFText_GetFontWeightPtr = - _lookup>( - 'FPDFText_GetFontWeight'); - late final _FPDFText_GetFontWeight = - _FPDFText_GetFontWeightPtr.asFunction(); - - FPDF_TEXT_RENDERMODE FPDFText_GetTextRenderMode( - FPDF_TEXTPAGE text_page, - int index, - ) { - return FPDF_TEXT_RENDERMODE.fromValue(_FPDFText_GetTextRenderMode( - text_page, - index, - )); - } - - late final _FPDFText_GetTextRenderModePtr = - _lookup>( - 'FPDFText_GetTextRenderMode'); - late final _FPDFText_GetTextRenderMode = _FPDFText_GetTextRenderModePtr - .asFunction(); - - int FPDFText_GetFillColor( - FPDF_TEXTPAGE text_page, - int index, - ffi.Pointer R, - ffi.Pointer G, - ffi.Pointer B, - ffi.Pointer A, - ) { - return _FPDFText_GetFillColor( - text_page, - index, - R, - G, - B, - A, - ); - } - - late final _FPDFText_GetFillColorPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_TEXTPAGE, - ffi.Int, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>>('FPDFText_GetFillColor'); - late final _FPDFText_GetFillColor = _FPDFText_GetFillColorPtr.asFunction< - int Function( - FPDF_TEXTPAGE, - int, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>(); - - int FPDFText_GetStrokeColor( - FPDF_TEXTPAGE text_page, - int index, - ffi.Pointer R, - ffi.Pointer G, - ffi.Pointer B, - ffi.Pointer A, - ) { - return _FPDFText_GetStrokeColor( - text_page, - index, - R, - G, - B, - A, - ); - } - - late final _FPDFText_GetStrokeColorPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_TEXTPAGE, - ffi.Int, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>>('FPDFText_GetStrokeColor'); - late final _FPDFText_GetStrokeColor = _FPDFText_GetStrokeColorPtr.asFunction< - int Function( - FPDF_TEXTPAGE, - int, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>(); - - double FPDFText_GetCharAngle( - FPDF_TEXTPAGE text_page, - int index, - ) { - return _FPDFText_GetCharAngle( - text_page, - index, - ); - } - - late final _FPDFText_GetCharAnglePtr = - _lookup>( - 'FPDFText_GetCharAngle'); - late final _FPDFText_GetCharAngle = _FPDFText_GetCharAnglePtr.asFunction< - double Function(FPDF_TEXTPAGE, int)>(); - - int FPDFText_GetCharBox( - FPDF_TEXTPAGE text_page, - int index, - ffi.Pointer left, - ffi.Pointer right, - ffi.Pointer bottom, - ffi.Pointer top, - ) { - return _FPDFText_GetCharBox( - text_page, - index, - left, - right, - bottom, - top, - ); - } - - late final _FPDFText_GetCharBoxPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_TEXTPAGE, - ffi.Int, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>>('FPDFText_GetCharBox'); - late final _FPDFText_GetCharBox = _FPDFText_GetCharBoxPtr.asFunction< - int Function( - FPDF_TEXTPAGE, - int, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>(); - - int FPDFText_GetLooseCharBox( - FPDF_TEXTPAGE text_page, - int index, - ffi.Pointer rect, - ) { - return _FPDFText_GetLooseCharBox( - text_page, - index, - rect, - ); - } - - late final _FPDFText_GetLooseCharBoxPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_TEXTPAGE, ffi.Int, - ffi.Pointer)>>('FPDFText_GetLooseCharBox'); - late final _FPDFText_GetLooseCharBox = _FPDFText_GetLooseCharBoxPtr - .asFunction)>(); - - int FPDFText_GetMatrix( - FPDF_TEXTPAGE text_page, - int index, - ffi.Pointer matrix, - ) { - return _FPDFText_GetMatrix( - text_page, - index, - matrix, - ); - } - - late final _FPDFText_GetMatrixPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_TEXTPAGE, ffi.Int, - ffi.Pointer)>>('FPDFText_GetMatrix'); - late final _FPDFText_GetMatrix = _FPDFText_GetMatrixPtr.asFunction< - int Function(FPDF_TEXTPAGE, int, ffi.Pointer)>(); - - int FPDFText_GetCharOrigin( - FPDF_TEXTPAGE text_page, - int index, - ffi.Pointer x, - ffi.Pointer y, - ) { - return _FPDFText_GetCharOrigin( - text_page, - index, - x, - y, - ); - } - - late final _FPDFText_GetCharOriginPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_TEXTPAGE, ffi.Int, ffi.Pointer, - ffi.Pointer)>>('FPDFText_GetCharOrigin'); - late final _FPDFText_GetCharOrigin = _FPDFText_GetCharOriginPtr.asFunction< - int Function(FPDF_TEXTPAGE, int, ffi.Pointer, - ffi.Pointer)>(); - - int FPDFText_GetCharIndexAtPos( - FPDF_TEXTPAGE text_page, - double x, - double y, - double xTolerance, - double yTolerance, - ) { - return _FPDFText_GetCharIndexAtPos( - text_page, - x, - y, - xTolerance, - yTolerance, - ); - } - - late final _FPDFText_GetCharIndexAtPosPtr = _lookup< - ffi.NativeFunction< - ffi.Int Function(FPDF_TEXTPAGE, ffi.Double, ffi.Double, ffi.Double, - ffi.Double)>>('FPDFText_GetCharIndexAtPos'); - late final _FPDFText_GetCharIndexAtPos = - _FPDFText_GetCharIndexAtPosPtr.asFunction< - int Function(FPDF_TEXTPAGE, double, double, double, double)>(); - - int FPDFText_GetText( - FPDF_TEXTPAGE text_page, - int start_index, - int count, - ffi.Pointer result, - ) { - return _FPDFText_GetText( - text_page, - start_index, - count, - result, - ); - } - - late final _FPDFText_GetTextPtr = _lookup< - ffi.NativeFunction< - ffi.Int Function(FPDF_TEXTPAGE, ffi.Int, ffi.Int, - ffi.Pointer)>>('FPDFText_GetText'); - late final _FPDFText_GetText = _FPDFText_GetTextPtr.asFunction< - int Function(FPDF_TEXTPAGE, int, int, ffi.Pointer)>(); - - int FPDFText_CountRects( - FPDF_TEXTPAGE text_page, - int start_index, - int count, - ) { - return _FPDFText_CountRects( - text_page, - start_index, - count, - ); - } - - late final _FPDFText_CountRectsPtr = _lookup< - ffi - .NativeFunction>( - 'FPDFText_CountRects'); - late final _FPDFText_CountRects = _FPDFText_CountRectsPtr.asFunction< - int Function(FPDF_TEXTPAGE, int, int)>(); - - int FPDFText_GetRect( - FPDF_TEXTPAGE text_page, - int rect_index, - ffi.Pointer left, - ffi.Pointer top, - ffi.Pointer right, - ffi.Pointer bottom, - ) { - return _FPDFText_GetRect( - text_page, - rect_index, - left, - top, - right, - bottom, - ); - } - - late final _FPDFText_GetRectPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_TEXTPAGE, - ffi.Int, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>>('FPDFText_GetRect'); - late final _FPDFText_GetRect = _FPDFText_GetRectPtr.asFunction< - int Function( - FPDF_TEXTPAGE, - int, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>(); - - int FPDFText_GetBoundedText( - FPDF_TEXTPAGE text_page, - double left, - double top, - double right, - double bottom, - ffi.Pointer buffer, - int buflen, - ) { - return _FPDFText_GetBoundedText( - text_page, - left, - top, - right, - bottom, - buffer, - buflen, - ); - } - - late final _FPDFText_GetBoundedTextPtr = _lookup< - ffi.NativeFunction< - ffi.Int Function( - FPDF_TEXTPAGE, - ffi.Double, - ffi.Double, - ffi.Double, - ffi.Double, - ffi.Pointer, - ffi.Int)>>('FPDFText_GetBoundedText'); - late final _FPDFText_GetBoundedText = _FPDFText_GetBoundedTextPtr.asFunction< - int Function(FPDF_TEXTPAGE, double, double, double, double, - ffi.Pointer, int)>(); - - FPDF_SCHHANDLE FPDFText_FindStart( - FPDF_TEXTPAGE text_page, - FPDF_WIDESTRING findwhat, - int flags, - int start_index, - ) { - return _FPDFText_FindStart( - text_page, - findwhat, - flags, - start_index, - ); - } - - late final _FPDFText_FindStartPtr = _lookup< - ffi.NativeFunction< - FPDF_SCHHANDLE Function(FPDF_TEXTPAGE, FPDF_WIDESTRING, - ffi.UnsignedLong, ffi.Int)>>('FPDFText_FindStart'); - late final _FPDFText_FindStart = _FPDFText_FindStartPtr.asFunction< - FPDF_SCHHANDLE Function(FPDF_TEXTPAGE, FPDF_WIDESTRING, int, int)>(); - - int FPDFText_FindNext( - FPDF_SCHHANDLE handle, - ) { - return _FPDFText_FindNext( - handle, - ); - } - - late final _FPDFText_FindNextPtr = - _lookup>( - 'FPDFText_FindNext'); - late final _FPDFText_FindNext = - _FPDFText_FindNextPtr.asFunction(); - - int FPDFText_FindPrev( - FPDF_SCHHANDLE handle, - ) { - return _FPDFText_FindPrev( - handle, - ); - } - - late final _FPDFText_FindPrevPtr = - _lookup>( - 'FPDFText_FindPrev'); - late final _FPDFText_FindPrev = - _FPDFText_FindPrevPtr.asFunction(); - - int FPDFText_GetSchResultIndex( - FPDF_SCHHANDLE handle, - ) { - return _FPDFText_GetSchResultIndex( - handle, - ); - } - - late final _FPDFText_GetSchResultIndexPtr = - _lookup>( - 'FPDFText_GetSchResultIndex'); - late final _FPDFText_GetSchResultIndex = - _FPDFText_GetSchResultIndexPtr.asFunction(); - - int FPDFText_GetSchCount( - FPDF_SCHHANDLE handle, - ) { - return _FPDFText_GetSchCount( - handle, - ); - } - - late final _FPDFText_GetSchCountPtr = - _lookup>( - 'FPDFText_GetSchCount'); - late final _FPDFText_GetSchCount = - _FPDFText_GetSchCountPtr.asFunction(); - - void FPDFText_FindClose( - FPDF_SCHHANDLE handle, - ) { - return _FPDFText_FindClose( - handle, - ); - } - - late final _FPDFText_FindClosePtr = - _lookup>( - 'FPDFText_FindClose'); - late final _FPDFText_FindClose = - _FPDFText_FindClosePtr.asFunction(); - - FPDF_PAGELINK FPDFLink_LoadWebLinks( - FPDF_TEXTPAGE text_page, - ) { - return _FPDFLink_LoadWebLinks( - text_page, - ); - } - - late final _FPDFLink_LoadWebLinksPtr = - _lookup>( - 'FPDFLink_LoadWebLinks'); - late final _FPDFLink_LoadWebLinks = _FPDFLink_LoadWebLinksPtr.asFunction< - FPDF_PAGELINK Function(FPDF_TEXTPAGE)>(); - - int FPDFLink_CountWebLinks( - FPDF_PAGELINK link_page, - ) { - return _FPDFLink_CountWebLinks( - link_page, - ); - } - - late final _FPDFLink_CountWebLinksPtr = - _lookup>( - 'FPDFLink_CountWebLinks'); - late final _FPDFLink_CountWebLinks = - _FPDFLink_CountWebLinksPtr.asFunction(); - - int FPDFLink_GetURL( - FPDF_PAGELINK link_page, - int link_index, - ffi.Pointer buffer, - int buflen, - ) { - return _FPDFLink_GetURL( - link_page, - link_index, - buffer, - buflen, - ); - } - - late final _FPDFLink_GetURLPtr = _lookup< - ffi.NativeFunction< - ffi.Int Function(FPDF_PAGELINK, ffi.Int, - ffi.Pointer, ffi.Int)>>('FPDFLink_GetURL'); - late final _FPDFLink_GetURL = _FPDFLink_GetURLPtr.asFunction< - int Function(FPDF_PAGELINK, int, ffi.Pointer, int)>(); - - int FPDFLink_CountRects( - FPDF_PAGELINK link_page, - int link_index, - ) { - return _FPDFLink_CountRects( - link_page, - link_index, - ); - } - - late final _FPDFLink_CountRectsPtr = - _lookup>( - 'FPDFLink_CountRects'); - late final _FPDFLink_CountRects = - _FPDFLink_CountRectsPtr.asFunction(); - - int FPDFLink_GetRect( - FPDF_PAGELINK link_page, - int link_index, - int rect_index, - ffi.Pointer left, - ffi.Pointer top, - ffi.Pointer right, - ffi.Pointer bottom, - ) { - return _FPDFLink_GetRect( - link_page, - link_index, - rect_index, - left, - top, - right, - bottom, - ); - } - - late final _FPDFLink_GetRectPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_PAGELINK, - ffi.Int, - ffi.Int, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>>('FPDFLink_GetRect'); - late final _FPDFLink_GetRect = _FPDFLink_GetRectPtr.asFunction< - int Function( - FPDF_PAGELINK, - int, - int, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>(); - - int FPDFLink_GetTextRange( - FPDF_PAGELINK link_page, - int link_index, - ffi.Pointer start_char_index, - ffi.Pointer char_count, - ) { - return _FPDFLink_GetTextRange( - link_page, - link_index, - start_char_index, - char_count, - ); - } - - late final _FPDFLink_GetTextRangePtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_PAGELINK, ffi.Int, ffi.Pointer, - ffi.Pointer)>>('FPDFLink_GetTextRange'); - late final _FPDFLink_GetTextRange = _FPDFLink_GetTextRangePtr.asFunction< - int Function( - FPDF_PAGELINK, int, ffi.Pointer, ffi.Pointer)>(); - - void FPDFLink_CloseWebLinks( - FPDF_PAGELINK link_page, - ) { - return _FPDFLink_CloseWebLinks( - link_page, - ); - } - - late final _FPDFLink_CloseWebLinksPtr = - _lookup>( - 'FPDFLink_CloseWebLinks'); - late final _FPDFLink_CloseWebLinks = - _FPDFLink_CloseWebLinksPtr.asFunction(); - - FPDF_BOOKMARK FPDFBookmark_GetFirstChild( - FPDF_DOCUMENT document, - FPDF_BOOKMARK bookmark, - ) { - return _FPDFBookmark_GetFirstChild( - document, - bookmark, - ); - } - - late final _FPDFBookmark_GetFirstChildPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOKMARK Function( - FPDF_DOCUMENT, FPDF_BOOKMARK)>>('FPDFBookmark_GetFirstChild'); - late final _FPDFBookmark_GetFirstChild = _FPDFBookmark_GetFirstChildPtr - .asFunction(); - - FPDF_BOOKMARK FPDFBookmark_GetNextSibling( - FPDF_DOCUMENT document, - FPDF_BOOKMARK bookmark, - ) { - return _FPDFBookmark_GetNextSibling( - document, - bookmark, - ); - } - - late final _FPDFBookmark_GetNextSiblingPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOKMARK Function( - FPDF_DOCUMENT, FPDF_BOOKMARK)>>('FPDFBookmark_GetNextSibling'); - late final _FPDFBookmark_GetNextSibling = _FPDFBookmark_GetNextSiblingPtr - .asFunction(); - - int FPDFBookmark_GetTitle( - FPDF_BOOKMARK bookmark, - ffi.Pointer buffer, - int buflen, - ) { - return _FPDFBookmark_GetTitle( - bookmark, - buffer, - buflen, - ); - } - - late final _FPDFBookmark_GetTitlePtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function(FPDF_BOOKMARK, ffi.Pointer, - ffi.UnsignedLong)>>('FPDFBookmark_GetTitle'); - late final _FPDFBookmark_GetTitle = _FPDFBookmark_GetTitlePtr.asFunction< - int Function(FPDF_BOOKMARK, ffi.Pointer, int)>(); - - int FPDFBookmark_GetCount( - FPDF_BOOKMARK bookmark, - ) { - return _FPDFBookmark_GetCount( - bookmark, - ); - } - - late final _FPDFBookmark_GetCountPtr = - _lookup>( - 'FPDFBookmark_GetCount'); - late final _FPDFBookmark_GetCount = - _FPDFBookmark_GetCountPtr.asFunction(); - - FPDF_BOOKMARK FPDFBookmark_Find( - FPDF_DOCUMENT document, - FPDF_WIDESTRING title, - ) { - return _FPDFBookmark_Find( - document, - title, - ); - } - - late final _FPDFBookmark_FindPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOKMARK Function( - FPDF_DOCUMENT, FPDF_WIDESTRING)>>('FPDFBookmark_Find'); - late final _FPDFBookmark_Find = _FPDFBookmark_FindPtr.asFunction< - FPDF_BOOKMARK Function(FPDF_DOCUMENT, FPDF_WIDESTRING)>(); - - FPDF_DEST FPDFBookmark_GetDest( - FPDF_DOCUMENT document, - FPDF_BOOKMARK bookmark, - ) { - return _FPDFBookmark_GetDest( - document, - bookmark, - ); - } - - late final _FPDFBookmark_GetDestPtr = _lookup< - ffi.NativeFunction>( - 'FPDFBookmark_GetDest'); - late final _FPDFBookmark_GetDest = _FPDFBookmark_GetDestPtr.asFunction< - FPDF_DEST Function(FPDF_DOCUMENT, FPDF_BOOKMARK)>(); - - FPDF_ACTION FPDFBookmark_GetAction( - FPDF_BOOKMARK bookmark, - ) { - return _FPDFBookmark_GetAction( - bookmark, - ); - } - - late final _FPDFBookmark_GetActionPtr = - _lookup>( - 'FPDFBookmark_GetAction'); - late final _FPDFBookmark_GetAction = _FPDFBookmark_GetActionPtr.asFunction< - FPDF_ACTION Function(FPDF_BOOKMARK)>(); - - int FPDFAction_GetType( - FPDF_ACTION action, - ) { - return _FPDFAction_GetType( - action, - ); - } - - late final _FPDFAction_GetTypePtr = - _lookup>( - 'FPDFAction_GetType'); - late final _FPDFAction_GetType = - _FPDFAction_GetTypePtr.asFunction(); - - FPDF_DEST FPDFAction_GetDest( - FPDF_DOCUMENT document, - FPDF_ACTION action, - ) { - return _FPDFAction_GetDest( - document, - action, - ); - } - - late final _FPDFAction_GetDestPtr = _lookup< - ffi.NativeFunction>( - 'FPDFAction_GetDest'); - late final _FPDFAction_GetDest = _FPDFAction_GetDestPtr.asFunction< - FPDF_DEST Function(FPDF_DOCUMENT, FPDF_ACTION)>(); - - int FPDFAction_GetFilePath( - FPDF_ACTION action, - ffi.Pointer buffer, - int buflen, - ) { - return _FPDFAction_GetFilePath( - action, - buffer, - buflen, - ); - } - - late final _FPDFAction_GetFilePathPtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function(FPDF_ACTION, ffi.Pointer, - ffi.UnsignedLong)>>('FPDFAction_GetFilePath'); - late final _FPDFAction_GetFilePath = _FPDFAction_GetFilePathPtr.asFunction< - int Function(FPDF_ACTION, ffi.Pointer, int)>(); - - int FPDFAction_GetURIPath( - FPDF_DOCUMENT document, - FPDF_ACTION action, - ffi.Pointer buffer, - int buflen, - ) { - return _FPDFAction_GetURIPath( - document, - action, - buffer, - buflen, - ); - } - - late final _FPDFAction_GetURIPathPtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function( - FPDF_DOCUMENT, - FPDF_ACTION, - ffi.Pointer, - ffi.UnsignedLong)>>('FPDFAction_GetURIPath'); - late final _FPDFAction_GetURIPath = _FPDFAction_GetURIPathPtr.asFunction< - int Function(FPDF_DOCUMENT, FPDF_ACTION, ffi.Pointer, int)>(); - - int FPDFDest_GetDestPageIndex( - FPDF_DOCUMENT document, - FPDF_DEST dest, - ) { - return _FPDFDest_GetDestPageIndex( - document, - dest, - ); - } - - late final _FPDFDest_GetDestPageIndexPtr = - _lookup>( - 'FPDFDest_GetDestPageIndex'); - late final _FPDFDest_GetDestPageIndex = _FPDFDest_GetDestPageIndexPtr - .asFunction(); - - int FPDFDest_GetView( - FPDF_DEST dest, - ffi.Pointer pNumParams, - ffi.Pointer pParams, - ) { - return _FPDFDest_GetView( - dest, - pNumParams, - pParams, - ); - } - - late final _FPDFDest_GetViewPtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function(FPDF_DEST, ffi.Pointer, - ffi.Pointer)>>('FPDFDest_GetView'); - late final _FPDFDest_GetView = _FPDFDest_GetViewPtr.asFunction< - int Function( - FPDF_DEST, ffi.Pointer, ffi.Pointer)>(); - - int FPDFDest_GetLocationInPage( - FPDF_DEST dest, - ffi.Pointer hasXVal, - ffi.Pointer hasYVal, - ffi.Pointer hasZoomVal, - ffi.Pointer x, - ffi.Pointer y, - ffi.Pointer zoom, - ) { - return _FPDFDest_GetLocationInPage( - dest, - hasXVal, - hasYVal, - hasZoomVal, - x, - y, - zoom, - ); - } - - late final _FPDFDest_GetLocationInPagePtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_DEST, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>>('FPDFDest_GetLocationInPage'); - late final _FPDFDest_GetLocationInPage = - _FPDFDest_GetLocationInPagePtr.asFunction< - int Function( - FPDF_DEST, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>(); - - FPDF_LINK FPDFLink_GetLinkAtPoint( - FPDF_PAGE page, - double x, - double y, - ) { - return _FPDFLink_GetLinkAtPoint( - page, - x, - y, - ); - } - - late final _FPDFLink_GetLinkAtPointPtr = _lookup< - ffi.NativeFunction< - FPDF_LINK Function( - FPDF_PAGE, ffi.Double, ffi.Double)>>('FPDFLink_GetLinkAtPoint'); - late final _FPDFLink_GetLinkAtPoint = _FPDFLink_GetLinkAtPointPtr.asFunction< - FPDF_LINK Function(FPDF_PAGE, double, double)>(); - - int FPDFLink_GetLinkZOrderAtPoint( - FPDF_PAGE page, - double x, - double y, - ) { - return _FPDFLink_GetLinkZOrderAtPoint( - page, - x, - y, - ); - } - - late final _FPDFLink_GetLinkZOrderAtPointPtr = _lookup< - ffi - .NativeFunction>( - 'FPDFLink_GetLinkZOrderAtPoint'); - late final _FPDFLink_GetLinkZOrderAtPoint = _FPDFLink_GetLinkZOrderAtPointPtr - .asFunction(); - - FPDF_DEST FPDFLink_GetDest( - FPDF_DOCUMENT document, - FPDF_LINK link, - ) { - return _FPDFLink_GetDest( - document, - link, - ); - } - - late final _FPDFLink_GetDestPtr = - _lookup>( - 'FPDFLink_GetDest'); - late final _FPDFLink_GetDest = _FPDFLink_GetDestPtr.asFunction< - FPDF_DEST Function(FPDF_DOCUMENT, FPDF_LINK)>(); - - FPDF_ACTION FPDFLink_GetAction( - FPDF_LINK link, - ) { - return _FPDFLink_GetAction( - link, - ); - } - - late final _FPDFLink_GetActionPtr = - _lookup>( - 'FPDFLink_GetAction'); - late final _FPDFLink_GetAction = - _FPDFLink_GetActionPtr.asFunction(); - - int FPDFLink_Enumerate( - FPDF_PAGE page, - ffi.Pointer start_pos, - ffi.Pointer link_annot, - ) { - return _FPDFLink_Enumerate( - page, - start_pos, - link_annot, - ); - } - - late final _FPDFLink_EnumeratePtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_PAGE, ffi.Pointer, - ffi.Pointer)>>('FPDFLink_Enumerate'); - late final _FPDFLink_Enumerate = _FPDFLink_EnumeratePtr.asFunction< - int Function(FPDF_PAGE, ffi.Pointer, ffi.Pointer)>(); - - FPDF_ANNOTATION FPDFLink_GetAnnot( - FPDF_PAGE page, - FPDF_LINK link_annot, - ) { - return _FPDFLink_GetAnnot( - page, - link_annot, - ); - } - - late final _FPDFLink_GetAnnotPtr = _lookup< - ffi.NativeFunction>( - 'FPDFLink_GetAnnot'); - late final _FPDFLink_GetAnnot = _FPDFLink_GetAnnotPtr.asFunction< - FPDF_ANNOTATION Function(FPDF_PAGE, FPDF_LINK)>(); - - int FPDFLink_GetAnnotRect( - FPDF_LINK link_annot, - ffi.Pointer rect, - ) { - return _FPDFLink_GetAnnotRect( - link_annot, - rect, - ); - } - - late final _FPDFLink_GetAnnotRectPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_LINK, ffi.Pointer)>>('FPDFLink_GetAnnotRect'); - late final _FPDFLink_GetAnnotRect = _FPDFLink_GetAnnotRectPtr.asFunction< - int Function(FPDF_LINK, ffi.Pointer)>(); - - int FPDFLink_CountQuadPoints( - FPDF_LINK link_annot, - ) { - return _FPDFLink_CountQuadPoints( - link_annot, - ); - } - - late final _FPDFLink_CountQuadPointsPtr = - _lookup>( - 'FPDFLink_CountQuadPoints'); - late final _FPDFLink_CountQuadPoints = - _FPDFLink_CountQuadPointsPtr.asFunction(); - - int FPDFLink_GetQuadPoints( - FPDF_LINK link_annot, - int quad_index, - ffi.Pointer quad_points, - ) { - return _FPDFLink_GetQuadPoints( - link_annot, - quad_index, - quad_points, - ); - } - - late final _FPDFLink_GetQuadPointsPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_LINK, ffi.Int, - ffi.Pointer)>>('FPDFLink_GetQuadPoints'); - late final _FPDFLink_GetQuadPoints = _FPDFLink_GetQuadPointsPtr.asFunction< - int Function(FPDF_LINK, int, ffi.Pointer)>(); - - FPDF_ACTION FPDF_GetPageAAction( - FPDF_PAGE page, - int aa_type, - ) { - return _FPDF_GetPageAAction( - page, - aa_type, - ); - } - - late final _FPDF_GetPageAActionPtr = - _lookup>( - 'FPDF_GetPageAAction'); - late final _FPDF_GetPageAAction = _FPDF_GetPageAActionPtr.asFunction< - FPDF_ACTION Function(FPDF_PAGE, int)>(); - - int FPDF_GetFileIdentifier( - FPDF_DOCUMENT document, - FPDF_FILEIDTYPE id_type, - ffi.Pointer buffer, - int buflen, - ) { - return _FPDF_GetFileIdentifier( - document, - id_type.value, - buffer, - buflen, - ); - } - - late final _FPDF_GetFileIdentifierPtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function( - FPDF_DOCUMENT, - ffi.UnsignedInt, - ffi.Pointer, - ffi.UnsignedLong)>>('FPDF_GetFileIdentifier'); - late final _FPDF_GetFileIdentifier = _FPDF_GetFileIdentifierPtr.asFunction< - int Function(FPDF_DOCUMENT, int, ffi.Pointer, int)>(); - - int FPDF_GetMetaText( - FPDF_DOCUMENT document, - FPDF_BYTESTRING tag, - ffi.Pointer buffer, - int buflen, - ) { - return _FPDF_GetMetaText( - document, - tag, - buffer, - buflen, - ); - } - - late final _FPDF_GetMetaTextPtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function(FPDF_DOCUMENT, FPDF_BYTESTRING, - ffi.Pointer, ffi.UnsignedLong)>>('FPDF_GetMetaText'); - late final _FPDF_GetMetaText = _FPDF_GetMetaTextPtr.asFunction< - int Function( - FPDF_DOCUMENT, FPDF_BYTESTRING, ffi.Pointer, int)>(); - - int FPDF_GetPageLabel( - FPDF_DOCUMENT document, - int page_index, - ffi.Pointer buffer, - int buflen, - ) { - return _FPDF_GetPageLabel( - document, - page_index, - buffer, - buflen, - ); - } - - late final _FPDF_GetPageLabelPtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function(FPDF_DOCUMENT, ffi.Int, - ffi.Pointer, ffi.UnsignedLong)>>('FPDF_GetPageLabel'); - late final _FPDF_GetPageLabel = _FPDF_GetPageLabelPtr.asFunction< - int Function(FPDF_DOCUMENT, int, ffi.Pointer, int)>(); - - FPDF_DOCUMENT FPDF_CreateNewDocument() { - return _FPDF_CreateNewDocument(); - } - - late final _FPDF_CreateNewDocumentPtr = - _lookup>( - 'FPDF_CreateNewDocument'); - late final _FPDF_CreateNewDocument = - _FPDF_CreateNewDocumentPtr.asFunction(); - - FPDF_PAGE FPDFPage_New( - FPDF_DOCUMENT document, - int page_index, - double width, - double height, - ) { - return _FPDFPage_New( - document, - page_index, - width, - height, - ); - } - - late final _FPDFPage_NewPtr = _lookup< - ffi.NativeFunction< - FPDF_PAGE Function( - FPDF_DOCUMENT, ffi.Int, ffi.Double, ffi.Double)>>('FPDFPage_New'); - late final _FPDFPage_New = _FPDFPage_NewPtr.asFunction< - FPDF_PAGE Function(FPDF_DOCUMENT, int, double, double)>(); - - void FPDFPage_Delete( - FPDF_DOCUMENT document, - int page_index, - ) { - return _FPDFPage_Delete( - document, - page_index, - ); - } - - late final _FPDFPage_DeletePtr = - _lookup>( - 'FPDFPage_Delete'); - late final _FPDFPage_Delete = - _FPDFPage_DeletePtr.asFunction(); - - int FPDF_MovePages( - FPDF_DOCUMENT document, - ffi.Pointer page_indices, - int page_indices_len, - int dest_page_index, - ) { - return _FPDF_MovePages( - document, - page_indices, - page_indices_len, - dest_page_index, - ); - } - - late final _FPDF_MovePagesPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_DOCUMENT, ffi.Pointer, - ffi.UnsignedLong, ffi.Int)>>('FPDF_MovePages'); - late final _FPDF_MovePages = _FPDF_MovePagesPtr.asFunction< - int Function(FPDF_DOCUMENT, ffi.Pointer, int, int)>(); - - int FPDFPage_GetRotation( - FPDF_PAGE page, - ) { - return _FPDFPage_GetRotation( - page, - ); - } - - late final _FPDFPage_GetRotationPtr = - _lookup>( - 'FPDFPage_GetRotation'); - late final _FPDFPage_GetRotation = - _FPDFPage_GetRotationPtr.asFunction(); - - void FPDFPage_SetRotation( - FPDF_PAGE page, - int rotate, - ) { - return _FPDFPage_SetRotation( - page, - rotate, - ); - } - - late final _FPDFPage_SetRotationPtr = - _lookup>( - 'FPDFPage_SetRotation'); - late final _FPDFPage_SetRotation = - _FPDFPage_SetRotationPtr.asFunction(); - - void FPDFPage_InsertObject( - FPDF_PAGE page, - FPDF_PAGEOBJECT page_object, - ) { - return _FPDFPage_InsertObject( - page, - page_object, - ); - } - - late final _FPDFPage_InsertObjectPtr = _lookup< - ffi.NativeFunction>( - 'FPDFPage_InsertObject'); - late final _FPDFPage_InsertObject = _FPDFPage_InsertObjectPtr.asFunction< - void Function(FPDF_PAGE, FPDF_PAGEOBJECT)>(); - - int FPDFPage_RemoveObject( - FPDF_PAGE page, - FPDF_PAGEOBJECT page_object, - ) { - return _FPDFPage_RemoveObject( - page, - page_object, - ); - } - - late final _FPDFPage_RemoveObjectPtr = _lookup< - ffi.NativeFunction>( - 'FPDFPage_RemoveObject'); - late final _FPDFPage_RemoveObject = _FPDFPage_RemoveObjectPtr.asFunction< - int Function(FPDF_PAGE, FPDF_PAGEOBJECT)>(); - - int FPDFPage_CountObjects( - FPDF_PAGE page, - ) { - return _FPDFPage_CountObjects( - page, - ); - } - - late final _FPDFPage_CountObjectsPtr = - _lookup>( - 'FPDFPage_CountObjects'); - late final _FPDFPage_CountObjects = - _FPDFPage_CountObjectsPtr.asFunction(); - - FPDF_PAGEOBJECT FPDFPage_GetObject( - FPDF_PAGE page, - int index, - ) { - return _FPDFPage_GetObject( - page, - index, - ); - } - - late final _FPDFPage_GetObjectPtr = - _lookup>( - 'FPDFPage_GetObject'); - late final _FPDFPage_GetObject = _FPDFPage_GetObjectPtr.asFunction< - FPDF_PAGEOBJECT Function(FPDF_PAGE, int)>(); - - int FPDFPage_HasTransparency( - FPDF_PAGE page, - ) { - return _FPDFPage_HasTransparency( - page, - ); - } - - late final _FPDFPage_HasTransparencyPtr = - _lookup>( - 'FPDFPage_HasTransparency'); - late final _FPDFPage_HasTransparency = - _FPDFPage_HasTransparencyPtr.asFunction(); - - int FPDFPage_GenerateContent( - FPDF_PAGE page, - ) { - return _FPDFPage_GenerateContent( - page, - ); - } - - late final _FPDFPage_GenerateContentPtr = - _lookup>( - 'FPDFPage_GenerateContent'); - late final _FPDFPage_GenerateContent = - _FPDFPage_GenerateContentPtr.asFunction(); - - void FPDFPageObj_Destroy( - FPDF_PAGEOBJECT page_object, - ) { - return _FPDFPageObj_Destroy( - page_object, - ); - } - - late final _FPDFPageObj_DestroyPtr = - _lookup>( - 'FPDFPageObj_Destroy'); - late final _FPDFPageObj_Destroy = - _FPDFPageObj_DestroyPtr.asFunction(); - - int FPDFPageObj_HasTransparency( - FPDF_PAGEOBJECT page_object, - ) { - return _FPDFPageObj_HasTransparency( - page_object, - ); - } - - late final _FPDFPageObj_HasTransparencyPtr = - _lookup>( - 'FPDFPageObj_HasTransparency'); - late final _FPDFPageObj_HasTransparency = _FPDFPageObj_HasTransparencyPtr - .asFunction(); - - int FPDFPageObj_GetType( - FPDF_PAGEOBJECT page_object, - ) { - return _FPDFPageObj_GetType( - page_object, - ); - } - - late final _FPDFPageObj_GetTypePtr = - _lookup>( - 'FPDFPageObj_GetType'); - late final _FPDFPageObj_GetType = - _FPDFPageObj_GetTypePtr.asFunction(); - - void FPDFPageObj_Transform( - FPDF_PAGEOBJECT page_object, - double a, - double b, - double c, - double d, - double e, - double f, - ) { - return _FPDFPageObj_Transform( - page_object, - a, - b, - c, - d, - e, - f, - ); - } - - late final _FPDFPageObj_TransformPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(FPDF_PAGEOBJECT, ffi.Double, ffi.Double, ffi.Double, - ffi.Double, ffi.Double, ffi.Double)>>('FPDFPageObj_Transform'); - late final _FPDFPageObj_Transform = _FPDFPageObj_TransformPtr.asFunction< - void Function( - FPDF_PAGEOBJECT, double, double, double, double, double, double)>(); - - int FPDFPageObj_GetMatrix( - FPDF_PAGEOBJECT page_object, - ffi.Pointer matrix, - ) { - return _FPDFPageObj_GetMatrix( - page_object, - matrix, - ); - } - - late final _FPDFPageObj_GetMatrixPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_PAGEOBJECT, - ffi.Pointer)>>('FPDFPageObj_GetMatrix'); - late final _FPDFPageObj_GetMatrix = _FPDFPageObj_GetMatrixPtr.asFunction< - int Function(FPDF_PAGEOBJECT, ffi.Pointer)>(); - - int FPDFPageObj_SetMatrix( - FPDF_PAGEOBJECT page_object, - ffi.Pointer matrix, - ) { - return _FPDFPageObj_SetMatrix( - page_object, - matrix, - ); - } - - late final _FPDFPageObj_SetMatrixPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_PAGEOBJECT, - ffi.Pointer)>>('FPDFPageObj_SetMatrix'); - late final _FPDFPageObj_SetMatrix = _FPDFPageObj_SetMatrixPtr.asFunction< - int Function(FPDF_PAGEOBJECT, ffi.Pointer)>(); - - void FPDFPage_TransformAnnots( - FPDF_PAGE page, - double a, - double b, - double c, - double d, - double e, - double f, - ) { - return _FPDFPage_TransformAnnots( - page, - a, - b, - c, - d, - e, - f, - ); - } - - late final _FPDFPage_TransformAnnotsPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(FPDF_PAGE, ffi.Double, ffi.Double, ffi.Double, - ffi.Double, ffi.Double, ffi.Double)>>('FPDFPage_TransformAnnots'); - late final _FPDFPage_TransformAnnots = - _FPDFPage_TransformAnnotsPtr.asFunction< - void Function( - FPDF_PAGE, double, double, double, double, double, double)>(); - - FPDF_PAGEOBJECT FPDFPageObj_NewImageObj( - FPDF_DOCUMENT document, - ) { - return _FPDFPageObj_NewImageObj( - document, - ); - } - - late final _FPDFPageObj_NewImageObjPtr = - _lookup>( - 'FPDFPageObj_NewImageObj'); - late final _FPDFPageObj_NewImageObj = _FPDFPageObj_NewImageObjPtr.asFunction< - FPDF_PAGEOBJECT Function(FPDF_DOCUMENT)>(); - - int FPDFPageObj_CountMarks( - FPDF_PAGEOBJECT page_object, - ) { - return _FPDFPageObj_CountMarks( - page_object, - ); - } - - late final _FPDFPageObj_CountMarksPtr = - _lookup>( - 'FPDFPageObj_CountMarks'); - late final _FPDFPageObj_CountMarks = - _FPDFPageObj_CountMarksPtr.asFunction(); - - FPDF_PAGEOBJECTMARK FPDFPageObj_GetMark( - FPDF_PAGEOBJECT page_object, - int index, - ) { - return _FPDFPageObj_GetMark( - page_object, - index, - ); - } - - late final _FPDFPageObj_GetMarkPtr = _lookup< - ffi.NativeFunction< - FPDF_PAGEOBJECTMARK Function( - FPDF_PAGEOBJECT, ffi.UnsignedLong)>>('FPDFPageObj_GetMark'); - late final _FPDFPageObj_GetMark = _FPDFPageObj_GetMarkPtr.asFunction< - FPDF_PAGEOBJECTMARK Function(FPDF_PAGEOBJECT, int)>(); - - FPDF_PAGEOBJECTMARK FPDFPageObj_AddMark( - FPDF_PAGEOBJECT page_object, - FPDF_BYTESTRING name, - ) { - return _FPDFPageObj_AddMark( - page_object, - name, - ); - } - - late final _FPDFPageObj_AddMarkPtr = _lookup< - ffi.NativeFunction< - FPDF_PAGEOBJECTMARK Function( - FPDF_PAGEOBJECT, FPDF_BYTESTRING)>>('FPDFPageObj_AddMark'); - late final _FPDFPageObj_AddMark = _FPDFPageObj_AddMarkPtr.asFunction< - FPDF_PAGEOBJECTMARK Function(FPDF_PAGEOBJECT, FPDF_BYTESTRING)>(); - - int FPDFPageObj_RemoveMark( - FPDF_PAGEOBJECT page_object, - FPDF_PAGEOBJECTMARK mark, - ) { - return _FPDFPageObj_RemoveMark( - page_object, - mark, - ); - } - - late final _FPDFPageObj_RemoveMarkPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_PAGEOBJECT, FPDF_PAGEOBJECTMARK)>>('FPDFPageObj_RemoveMark'); - late final _FPDFPageObj_RemoveMark = _FPDFPageObj_RemoveMarkPtr.asFunction< - int Function(FPDF_PAGEOBJECT, FPDF_PAGEOBJECTMARK)>(); - - int FPDFPageObjMark_GetName( - FPDF_PAGEOBJECTMARK mark, - ffi.Pointer buffer, - int buflen, - ffi.Pointer out_buflen, - ) { - return _FPDFPageObjMark_GetName( - mark, - buffer, - buflen, - out_buflen, - ); - } - - late final _FPDFPageObjMark_GetNamePtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_PAGEOBJECTMARK, - ffi.Pointer, - ffi.UnsignedLong, - ffi.Pointer)>>('FPDFPageObjMark_GetName'); - late final _FPDFPageObjMark_GetName = _FPDFPageObjMark_GetNamePtr.asFunction< - int Function(FPDF_PAGEOBJECTMARK, ffi.Pointer, int, - ffi.Pointer)>(); - - int FPDFPageObjMark_CountParams( - FPDF_PAGEOBJECTMARK mark, - ) { - return _FPDFPageObjMark_CountParams( - mark, - ); - } - - late final _FPDFPageObjMark_CountParamsPtr = - _lookup>( - 'FPDFPageObjMark_CountParams'); - late final _FPDFPageObjMark_CountParams = _FPDFPageObjMark_CountParamsPtr - .asFunction(); - - int FPDFPageObjMark_GetParamKey( - FPDF_PAGEOBJECTMARK mark, - int index, - ffi.Pointer buffer, - int buflen, - ffi.Pointer out_buflen, - ) { - return _FPDFPageObjMark_GetParamKey( - mark, - index, - buffer, - buflen, - out_buflen, - ); - } - - late final _FPDFPageObjMark_GetParamKeyPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_PAGEOBJECTMARK, - ffi.UnsignedLong, - ffi.Pointer, - ffi.UnsignedLong, - ffi.Pointer)>>('FPDFPageObjMark_GetParamKey'); - late final _FPDFPageObjMark_GetParamKey = - _FPDFPageObjMark_GetParamKeyPtr.asFunction< - int Function(FPDF_PAGEOBJECTMARK, int, ffi.Pointer, int, - ffi.Pointer)>(); - - int FPDFPageObjMark_GetParamValueType( - FPDF_PAGEOBJECTMARK mark, - FPDF_BYTESTRING key, - ) { - return _FPDFPageObjMark_GetParamValueType( - mark, - key, - ); - } - - late final _FPDFPageObjMark_GetParamValueTypePtr = _lookup< - ffi.NativeFunction< - FPDF_OBJECT_TYPE Function(FPDF_PAGEOBJECTMARK, - FPDF_BYTESTRING)>>('FPDFPageObjMark_GetParamValueType'); - late final _FPDFPageObjMark_GetParamValueType = - _FPDFPageObjMark_GetParamValueTypePtr.asFunction< - int Function(FPDF_PAGEOBJECTMARK, FPDF_BYTESTRING)>(); - - int FPDFPageObjMark_GetParamIntValue( - FPDF_PAGEOBJECTMARK mark, - FPDF_BYTESTRING key, - ffi.Pointer out_value, - ) { - return _FPDFPageObjMark_GetParamIntValue( - mark, - key, - out_value, - ); - } - - late final _FPDFPageObjMark_GetParamIntValuePtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_PAGEOBJECTMARK, FPDF_BYTESTRING, - ffi.Pointer)>>('FPDFPageObjMark_GetParamIntValue'); - late final _FPDFPageObjMark_GetParamIntValue = - _FPDFPageObjMark_GetParamIntValuePtr.asFunction< - int Function( - FPDF_PAGEOBJECTMARK, FPDF_BYTESTRING, ffi.Pointer)>(); - - int FPDFPageObjMark_GetParamStringValue( - FPDF_PAGEOBJECTMARK mark, - FPDF_BYTESTRING key, - ffi.Pointer buffer, - int buflen, - ffi.Pointer out_buflen, - ) { - return _FPDFPageObjMark_GetParamStringValue( - mark, - key, - buffer, - buflen, - out_buflen, - ); - } - - late final _FPDFPageObjMark_GetParamStringValuePtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_PAGEOBJECTMARK, - FPDF_BYTESTRING, - ffi.Pointer, - ffi.UnsignedLong, - ffi.Pointer)>>( - 'FPDFPageObjMark_GetParamStringValue'); - late final _FPDFPageObjMark_GetParamStringValue = - _FPDFPageObjMark_GetParamStringValuePtr.asFunction< - int Function(FPDF_PAGEOBJECTMARK, FPDF_BYTESTRING, - ffi.Pointer, int, ffi.Pointer)>(); - - int FPDFPageObjMark_GetParamBlobValue( - FPDF_PAGEOBJECTMARK mark, - FPDF_BYTESTRING key, - ffi.Pointer buffer, - int buflen, - ffi.Pointer out_buflen, - ) { - return _FPDFPageObjMark_GetParamBlobValue( - mark, - key, - buffer, - buflen, - out_buflen, - ); - } - - late final _FPDFPageObjMark_GetParamBlobValuePtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_PAGEOBJECTMARK, - FPDF_BYTESTRING, - ffi.Pointer, - ffi.UnsignedLong, - ffi.Pointer)>>( - 'FPDFPageObjMark_GetParamBlobValue'); - late final _FPDFPageObjMark_GetParamBlobValue = - _FPDFPageObjMark_GetParamBlobValuePtr.asFunction< - int Function(FPDF_PAGEOBJECTMARK, FPDF_BYTESTRING, - ffi.Pointer, int, ffi.Pointer)>(); - - int FPDFPageObjMark_SetIntParam( - FPDF_DOCUMENT document, - FPDF_PAGEOBJECT page_object, - FPDF_PAGEOBJECTMARK mark, - FPDF_BYTESTRING key, - int value, - ) { - return _FPDFPageObjMark_SetIntParam( - document, - page_object, - mark, - key, - value, - ); - } - - late final _FPDFPageObjMark_SetIntParamPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_DOCUMENT, - FPDF_PAGEOBJECT, - FPDF_PAGEOBJECTMARK, - FPDF_BYTESTRING, - ffi.Int)>>('FPDFPageObjMark_SetIntParam'); - late final _FPDFPageObjMark_SetIntParam = - _FPDFPageObjMark_SetIntParamPtr.asFunction< - int Function(FPDF_DOCUMENT, FPDF_PAGEOBJECT, FPDF_PAGEOBJECTMARK, - FPDF_BYTESTRING, int)>(); - - int FPDFPageObjMark_SetStringParam( - FPDF_DOCUMENT document, - FPDF_PAGEOBJECT page_object, - FPDF_PAGEOBJECTMARK mark, - FPDF_BYTESTRING key, - FPDF_BYTESTRING value, - ) { - return _FPDFPageObjMark_SetStringParam( - document, - page_object, - mark, - key, - value, - ); - } - - late final _FPDFPageObjMark_SetStringParamPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_DOCUMENT, - FPDF_PAGEOBJECT, - FPDF_PAGEOBJECTMARK, - FPDF_BYTESTRING, - FPDF_BYTESTRING)>>('FPDFPageObjMark_SetStringParam'); - late final _FPDFPageObjMark_SetStringParam = - _FPDFPageObjMark_SetStringParamPtr.asFunction< - int Function(FPDF_DOCUMENT, FPDF_PAGEOBJECT, FPDF_PAGEOBJECTMARK, - FPDF_BYTESTRING, FPDF_BYTESTRING)>(); - - int FPDFPageObjMark_SetBlobParam( - FPDF_DOCUMENT document, - FPDF_PAGEOBJECT page_object, - FPDF_PAGEOBJECTMARK mark, - FPDF_BYTESTRING key, - ffi.Pointer value, - int value_len, - ) { - return _FPDFPageObjMark_SetBlobParam( - document, - page_object, - mark, - key, - value, - value_len, - ); - } - - late final _FPDFPageObjMark_SetBlobParamPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_DOCUMENT, - FPDF_PAGEOBJECT, - FPDF_PAGEOBJECTMARK, - FPDF_BYTESTRING, - ffi.Pointer, - ffi.UnsignedLong)>>('FPDFPageObjMark_SetBlobParam'); - late final _FPDFPageObjMark_SetBlobParam = - _FPDFPageObjMark_SetBlobParamPtr.asFunction< - int Function(FPDF_DOCUMENT, FPDF_PAGEOBJECT, FPDF_PAGEOBJECTMARK, - FPDF_BYTESTRING, ffi.Pointer, int)>(); - - int FPDFPageObjMark_RemoveParam( - FPDF_PAGEOBJECT page_object, - FPDF_PAGEOBJECTMARK mark, - FPDF_BYTESTRING key, - ) { - return _FPDFPageObjMark_RemoveParam( - page_object, - mark, - key, - ); - } - - late final _FPDFPageObjMark_RemoveParamPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_PAGEOBJECT, FPDF_PAGEOBJECTMARK, - FPDF_BYTESTRING)>>('FPDFPageObjMark_RemoveParam'); - late final _FPDFPageObjMark_RemoveParam = - _FPDFPageObjMark_RemoveParamPtr.asFunction< - int Function( - FPDF_PAGEOBJECT, FPDF_PAGEOBJECTMARK, FPDF_BYTESTRING)>(); - - int FPDFImageObj_LoadJpegFile( - ffi.Pointer pages, - int count, - FPDF_PAGEOBJECT image_object, - ffi.Pointer file_access, - ) { - return _FPDFImageObj_LoadJpegFile( - pages, - count, - image_object, - file_access, - ); - } - - late final _FPDFImageObj_LoadJpegFilePtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(ffi.Pointer, ffi.Int, FPDF_PAGEOBJECT, - ffi.Pointer)>>('FPDFImageObj_LoadJpegFile'); - late final _FPDFImageObj_LoadJpegFile = - _FPDFImageObj_LoadJpegFilePtr.asFunction< - int Function(ffi.Pointer, int, FPDF_PAGEOBJECT, - ffi.Pointer)>(); - - int FPDFImageObj_LoadJpegFileInline( - ffi.Pointer pages, - int count, - FPDF_PAGEOBJECT image_object, - ffi.Pointer file_access, - ) { - return _FPDFImageObj_LoadJpegFileInline( - pages, - count, - image_object, - file_access, - ); - } - - late final _FPDFImageObj_LoadJpegFileInlinePtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(ffi.Pointer, ffi.Int, - FPDF_PAGEOBJECT, ffi.Pointer)>>( - 'FPDFImageObj_LoadJpegFileInline'); - late final _FPDFImageObj_LoadJpegFileInline = - _FPDFImageObj_LoadJpegFileInlinePtr.asFunction< - int Function(ffi.Pointer, int, FPDF_PAGEOBJECT, - ffi.Pointer)>(); - - int FPDFImageObj_SetMatrix( - FPDF_PAGEOBJECT image_object, - double a, - double b, - double c, - double d, - double e, - double f, - ) { - return _FPDFImageObj_SetMatrix( - image_object, - a, - b, - c, - d, - e, - f, - ); - } - - late final _FPDFImageObj_SetMatrixPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_PAGEOBJECT, - ffi.Double, - ffi.Double, - ffi.Double, - ffi.Double, - ffi.Double, - ffi.Double)>>('FPDFImageObj_SetMatrix'); - late final _FPDFImageObj_SetMatrix = _FPDFImageObj_SetMatrixPtr.asFunction< - int Function( - FPDF_PAGEOBJECT, double, double, double, double, double, double)>(); - - int FPDFImageObj_SetBitmap( - ffi.Pointer pages, - int count, - FPDF_PAGEOBJECT image_object, - FPDF_BITMAP bitmap, - ) { - return _FPDFImageObj_SetBitmap( - pages, - count, - image_object, - bitmap, - ); - } - - late final _FPDFImageObj_SetBitmapPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(ffi.Pointer, ffi.Int, FPDF_PAGEOBJECT, - FPDF_BITMAP)>>('FPDFImageObj_SetBitmap'); - late final _FPDFImageObj_SetBitmap = _FPDFImageObj_SetBitmapPtr.asFunction< - int Function( - ffi.Pointer, int, FPDF_PAGEOBJECT, FPDF_BITMAP)>(); - - FPDF_BITMAP FPDFImageObj_GetBitmap( - FPDF_PAGEOBJECT image_object, - ) { - return _FPDFImageObj_GetBitmap( - image_object, - ); - } - - late final _FPDFImageObj_GetBitmapPtr = - _lookup>( - 'FPDFImageObj_GetBitmap'); - late final _FPDFImageObj_GetBitmap = _FPDFImageObj_GetBitmapPtr.asFunction< - FPDF_BITMAP Function(FPDF_PAGEOBJECT)>(); - - FPDF_BITMAP FPDFImageObj_GetRenderedBitmap( - FPDF_DOCUMENT document, - FPDF_PAGE page, - FPDF_PAGEOBJECT image_object, - ) { - return _FPDFImageObj_GetRenderedBitmap( - document, - page, - image_object, - ); - } - - late final _FPDFImageObj_GetRenderedBitmapPtr = _lookup< - ffi.NativeFunction< - FPDF_BITMAP Function(FPDF_DOCUMENT, FPDF_PAGE, - FPDF_PAGEOBJECT)>>('FPDFImageObj_GetRenderedBitmap'); - late final _FPDFImageObj_GetRenderedBitmap = - _FPDFImageObj_GetRenderedBitmapPtr.asFunction< - FPDF_BITMAP Function(FPDF_DOCUMENT, FPDF_PAGE, FPDF_PAGEOBJECT)>(); - - int FPDFImageObj_GetImageDataDecoded( - FPDF_PAGEOBJECT image_object, - ffi.Pointer buffer, - int buflen, - ) { - return _FPDFImageObj_GetImageDataDecoded( - image_object, - buffer, - buflen, - ); - } - - late final _FPDFImageObj_GetImageDataDecodedPtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function(FPDF_PAGEOBJECT, ffi.Pointer, - ffi.UnsignedLong)>>('FPDFImageObj_GetImageDataDecoded'); - late final _FPDFImageObj_GetImageDataDecoded = - _FPDFImageObj_GetImageDataDecodedPtr.asFunction< - int Function(FPDF_PAGEOBJECT, ffi.Pointer, int)>(); - - int FPDFImageObj_GetImageDataRaw( - FPDF_PAGEOBJECT image_object, - ffi.Pointer buffer, - int buflen, - ) { - return _FPDFImageObj_GetImageDataRaw( - image_object, - buffer, - buflen, - ); - } - - late final _FPDFImageObj_GetImageDataRawPtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function(FPDF_PAGEOBJECT, ffi.Pointer, - ffi.UnsignedLong)>>('FPDFImageObj_GetImageDataRaw'); - late final _FPDFImageObj_GetImageDataRaw = _FPDFImageObj_GetImageDataRawPtr - .asFunction, int)>(); - - int FPDFImageObj_GetImageFilterCount( - FPDF_PAGEOBJECT image_object, - ) { - return _FPDFImageObj_GetImageFilterCount( - image_object, - ); - } - - late final _FPDFImageObj_GetImageFilterCountPtr = - _lookup>( - 'FPDFImageObj_GetImageFilterCount'); - late final _FPDFImageObj_GetImageFilterCount = - _FPDFImageObj_GetImageFilterCountPtr.asFunction< - int Function(FPDF_PAGEOBJECT)>(); - - int FPDFImageObj_GetImageFilter( - FPDF_PAGEOBJECT image_object, - int index, - ffi.Pointer buffer, - int buflen, - ) { - return _FPDFImageObj_GetImageFilter( - image_object, - index, - buffer, - buflen, - ); - } - - late final _FPDFImageObj_GetImageFilterPtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function( - FPDF_PAGEOBJECT, - ffi.Int, - ffi.Pointer, - ffi.UnsignedLong)>>('FPDFImageObj_GetImageFilter'); - late final _FPDFImageObj_GetImageFilter = - _FPDFImageObj_GetImageFilterPtr.asFunction< - int Function(FPDF_PAGEOBJECT, int, ffi.Pointer, int)>(); - - int FPDFImageObj_GetImageMetadata( - FPDF_PAGEOBJECT image_object, - FPDF_PAGE page, - ffi.Pointer metadata, - ) { - return _FPDFImageObj_GetImageMetadata( - image_object, - page, - metadata, - ); - } - - late final _FPDFImageObj_GetImageMetadataPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_PAGEOBJECT, FPDF_PAGE, - ffi.Pointer)>>( - 'FPDFImageObj_GetImageMetadata'); - late final _FPDFImageObj_GetImageMetadata = - _FPDFImageObj_GetImageMetadataPtr.asFunction< - int Function(FPDF_PAGEOBJECT, FPDF_PAGE, - ffi.Pointer)>(); - - int FPDFImageObj_GetImagePixelSize( - FPDF_PAGEOBJECT image_object, - ffi.Pointer width, - ffi.Pointer height, - ) { - return _FPDFImageObj_GetImagePixelSize( - image_object, - width, - height, - ); - } - - late final _FPDFImageObj_GetImagePixelSizePtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_PAGEOBJECT, ffi.Pointer, - ffi.Pointer)>>('FPDFImageObj_GetImagePixelSize'); - late final _FPDFImageObj_GetImagePixelSize = - _FPDFImageObj_GetImagePixelSizePtr.asFunction< - int Function(FPDF_PAGEOBJECT, ffi.Pointer, - ffi.Pointer)>(); - - FPDF_PAGEOBJECT FPDFPageObj_CreateNewPath( - double x, - double y, - ) { - return _FPDFPageObj_CreateNewPath( - x, - y, - ); - } - - late final _FPDFPageObj_CreateNewPathPtr = _lookup< - ffi.NativeFunction>( - 'FPDFPageObj_CreateNewPath'); - late final _FPDFPageObj_CreateNewPath = _FPDFPageObj_CreateNewPathPtr - .asFunction(); - - FPDF_PAGEOBJECT FPDFPageObj_CreateNewRect( - double x, - double y, - double w, - double h, - ) { - return _FPDFPageObj_CreateNewRect( - x, - y, - w, - h, - ); - } - - late final _FPDFPageObj_CreateNewRectPtr = _lookup< - ffi.NativeFunction< - FPDF_PAGEOBJECT Function(ffi.Float, ffi.Float, ffi.Float, - ffi.Float)>>('FPDFPageObj_CreateNewRect'); - late final _FPDFPageObj_CreateNewRect = _FPDFPageObj_CreateNewRectPtr - .asFunction(); - - int FPDFPageObj_GetBounds( - FPDF_PAGEOBJECT page_object, - ffi.Pointer left, - ffi.Pointer bottom, - ffi.Pointer right, - ffi.Pointer top, - ) { - return _FPDFPageObj_GetBounds( - page_object, - left, - bottom, - right, - top, - ); - } - - late final _FPDFPageObj_GetBoundsPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_PAGEOBJECT, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>>('FPDFPageObj_GetBounds'); - late final _FPDFPageObj_GetBounds = _FPDFPageObj_GetBoundsPtr.asFunction< - int Function( - FPDF_PAGEOBJECT, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>(); - - int FPDFPageObj_GetRotatedBounds( - FPDF_PAGEOBJECT page_object, - ffi.Pointer quad_points, - ) { - return _FPDFPageObj_GetRotatedBounds( - page_object, - quad_points, - ); - } - - late final _FPDFPageObj_GetRotatedBoundsPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_PAGEOBJECT, - ffi.Pointer)>>('FPDFPageObj_GetRotatedBounds'); - late final _FPDFPageObj_GetRotatedBounds = _FPDFPageObj_GetRotatedBoundsPtr - .asFunction)>(); - - void FPDFPageObj_SetBlendMode( - FPDF_PAGEOBJECT page_object, - FPDF_BYTESTRING blend_mode, - ) { - return _FPDFPageObj_SetBlendMode( - page_object, - blend_mode, - ); - } - - late final _FPDFPageObj_SetBlendModePtr = _lookup< - ffi - .NativeFunction>( - 'FPDFPageObj_SetBlendMode'); - late final _FPDFPageObj_SetBlendMode = _FPDFPageObj_SetBlendModePtr - .asFunction(); - - int FPDFPageObj_SetStrokeColor( - FPDF_PAGEOBJECT page_object, - int R, - int G, - int B, - int A, - ) { - return _FPDFPageObj_SetStrokeColor( - page_object, - R, - G, - B, - A, - ); - } - - late final _FPDFPageObj_SetStrokeColorPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_PAGEOBJECT, ffi.UnsignedInt, ffi.UnsignedInt, - ffi.UnsignedInt, ffi.UnsignedInt)>>('FPDFPageObj_SetStrokeColor'); - late final _FPDFPageObj_SetStrokeColor = _FPDFPageObj_SetStrokeColorPtr - .asFunction(); - - int FPDFPageObj_GetStrokeColor( - FPDF_PAGEOBJECT page_object, - ffi.Pointer R, - ffi.Pointer G, - ffi.Pointer B, - ffi.Pointer A, - ) { - return _FPDFPageObj_GetStrokeColor( - page_object, - R, - G, - B, - A, - ); - } - - late final _FPDFPageObj_GetStrokeColorPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_PAGEOBJECT, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>>('FPDFPageObj_GetStrokeColor'); - late final _FPDFPageObj_GetStrokeColor = - _FPDFPageObj_GetStrokeColorPtr.asFunction< - int Function( - FPDF_PAGEOBJECT, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>(); - - int FPDFPageObj_SetStrokeWidth( - FPDF_PAGEOBJECT page_object, - double width, - ) { - return _FPDFPageObj_SetStrokeWidth( - page_object, - width, - ); - } - - late final _FPDFPageObj_SetStrokeWidthPtr = _lookup< - ffi.NativeFunction>( - 'FPDFPageObj_SetStrokeWidth'); - late final _FPDFPageObj_SetStrokeWidth = _FPDFPageObj_SetStrokeWidthPtr - .asFunction(); - - int FPDFPageObj_GetStrokeWidth( - FPDF_PAGEOBJECT page_object, - ffi.Pointer width, - ) { - return _FPDFPageObj_GetStrokeWidth( - page_object, - width, - ); - } - - late final _FPDFPageObj_GetStrokeWidthPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_PAGEOBJECT, - ffi.Pointer)>>('FPDFPageObj_GetStrokeWidth'); - late final _FPDFPageObj_GetStrokeWidth = _FPDFPageObj_GetStrokeWidthPtr - .asFunction)>(); - - int FPDFPageObj_GetLineJoin( - FPDF_PAGEOBJECT page_object, - ) { - return _FPDFPageObj_GetLineJoin( - page_object, - ); - } - - late final _FPDFPageObj_GetLineJoinPtr = - _lookup>( - 'FPDFPageObj_GetLineJoin'); - late final _FPDFPageObj_GetLineJoin = - _FPDFPageObj_GetLineJoinPtr.asFunction(); - - int FPDFPageObj_SetLineJoin( - FPDF_PAGEOBJECT page_object, - int line_join, - ) { - return _FPDFPageObj_SetLineJoin( - page_object, - line_join, - ); - } - - late final _FPDFPageObj_SetLineJoinPtr = - _lookup>( - 'FPDFPageObj_SetLineJoin'); - late final _FPDFPageObj_SetLineJoin = _FPDFPageObj_SetLineJoinPtr.asFunction< - int Function(FPDF_PAGEOBJECT, int)>(); - - int FPDFPageObj_GetLineCap( - FPDF_PAGEOBJECT page_object, - ) { - return _FPDFPageObj_GetLineCap( - page_object, - ); - } - - late final _FPDFPageObj_GetLineCapPtr = - _lookup>( - 'FPDFPageObj_GetLineCap'); - late final _FPDFPageObj_GetLineCap = - _FPDFPageObj_GetLineCapPtr.asFunction(); - - int FPDFPageObj_SetLineCap( - FPDF_PAGEOBJECT page_object, - int line_cap, - ) { - return _FPDFPageObj_SetLineCap( - page_object, - line_cap, - ); - } - - late final _FPDFPageObj_SetLineCapPtr = - _lookup>( - 'FPDFPageObj_SetLineCap'); - late final _FPDFPageObj_SetLineCap = _FPDFPageObj_SetLineCapPtr.asFunction< - int Function(FPDF_PAGEOBJECT, int)>(); - - int FPDFPageObj_SetFillColor( - FPDF_PAGEOBJECT page_object, - int R, - int G, - int B, - int A, - ) { - return _FPDFPageObj_SetFillColor( - page_object, - R, - G, - B, - A, - ); - } - - late final _FPDFPageObj_SetFillColorPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_PAGEOBJECT, ffi.UnsignedInt, ffi.UnsignedInt, - ffi.UnsignedInt, ffi.UnsignedInt)>>('FPDFPageObj_SetFillColor'); - late final _FPDFPageObj_SetFillColor = _FPDFPageObj_SetFillColorPtr - .asFunction(); - - int FPDFPageObj_GetFillColor( - FPDF_PAGEOBJECT page_object, - ffi.Pointer R, - ffi.Pointer G, - ffi.Pointer B, - ffi.Pointer A, - ) { - return _FPDFPageObj_GetFillColor( - page_object, - R, - G, - B, - A, - ); - } - - late final _FPDFPageObj_GetFillColorPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_PAGEOBJECT, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>>('FPDFPageObj_GetFillColor'); - late final _FPDFPageObj_GetFillColor = - _FPDFPageObj_GetFillColorPtr.asFunction< - int Function( - FPDF_PAGEOBJECT, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer, - ffi.Pointer)>(); - - int FPDFPageObj_GetDashPhase( - FPDF_PAGEOBJECT page_object, - ffi.Pointer phase, - ) { - return _FPDFPageObj_GetDashPhase( - page_object, - phase, - ); - } - - late final _FPDFPageObj_GetDashPhasePtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_PAGEOBJECT, - ffi.Pointer)>>('FPDFPageObj_GetDashPhase'); - late final _FPDFPageObj_GetDashPhase = _FPDFPageObj_GetDashPhasePtr - .asFunction)>(); - - int FPDFPageObj_SetDashPhase( - FPDF_PAGEOBJECT page_object, - double phase, - ) { - return _FPDFPageObj_SetDashPhase( - page_object, - phase, - ); - } - - late final _FPDFPageObj_SetDashPhasePtr = _lookup< - ffi.NativeFunction>( - 'FPDFPageObj_SetDashPhase'); - late final _FPDFPageObj_SetDashPhase = _FPDFPageObj_SetDashPhasePtr - .asFunction(); - - int FPDFPageObj_GetDashCount( - FPDF_PAGEOBJECT page_object, - ) { - return _FPDFPageObj_GetDashCount( - page_object, - ); - } - - late final _FPDFPageObj_GetDashCountPtr = - _lookup>( - 'FPDFPageObj_GetDashCount'); - late final _FPDFPageObj_GetDashCount = - _FPDFPageObj_GetDashCountPtr.asFunction(); - - int FPDFPageObj_GetDashArray( - FPDF_PAGEOBJECT page_object, - ffi.Pointer dash_array, - int dash_count, - ) { - return _FPDFPageObj_GetDashArray( - page_object, - dash_array, - dash_count, - ); - } - - late final _FPDFPageObj_GetDashArrayPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_PAGEOBJECT, ffi.Pointer, - ffi.Size)>>('FPDFPageObj_GetDashArray'); - late final _FPDFPageObj_GetDashArray = _FPDFPageObj_GetDashArrayPtr - .asFunction, int)>(); - - int FPDFPageObj_SetDashArray( - FPDF_PAGEOBJECT page_object, - ffi.Pointer dash_array, - int dash_count, - double phase, - ) { - return _FPDFPageObj_SetDashArray( - page_object, - dash_array, - dash_count, - phase, - ); - } - - late final _FPDFPageObj_SetDashArrayPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_PAGEOBJECT, ffi.Pointer, ffi.Size, - ffi.Float)>>('FPDFPageObj_SetDashArray'); - late final _FPDFPageObj_SetDashArray = - _FPDFPageObj_SetDashArrayPtr.asFunction< - int Function(FPDF_PAGEOBJECT, ffi.Pointer, int, double)>(); - - int FPDFPath_CountSegments( - FPDF_PAGEOBJECT path, - ) { - return _FPDFPath_CountSegments( - path, - ); - } - - late final _FPDFPath_CountSegmentsPtr = - _lookup>( - 'FPDFPath_CountSegments'); - late final _FPDFPath_CountSegments = - _FPDFPath_CountSegmentsPtr.asFunction(); - - FPDF_PATHSEGMENT FPDFPath_GetPathSegment( - FPDF_PAGEOBJECT path, - int index, - ) { - return _FPDFPath_GetPathSegment( - path, - index, - ); - } - - late final _FPDFPath_GetPathSegmentPtr = _lookup< - ffi - .NativeFunction>( - 'FPDFPath_GetPathSegment'); - late final _FPDFPath_GetPathSegment = _FPDFPath_GetPathSegmentPtr.asFunction< - FPDF_PATHSEGMENT Function(FPDF_PAGEOBJECT, int)>(); - - int FPDFPathSegment_GetPoint( - FPDF_PATHSEGMENT segment, - ffi.Pointer x, - ffi.Pointer y, - ) { - return _FPDFPathSegment_GetPoint( - segment, - x, - y, - ); - } - - late final _FPDFPathSegment_GetPointPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_PATHSEGMENT, ffi.Pointer, - ffi.Pointer)>>('FPDFPathSegment_GetPoint'); - late final _FPDFPathSegment_GetPoint = - _FPDFPathSegment_GetPointPtr.asFunction< - int Function(FPDF_PATHSEGMENT, ffi.Pointer, - ffi.Pointer)>(); - - int FPDFPathSegment_GetType( - FPDF_PATHSEGMENT segment, - ) { - return _FPDFPathSegment_GetType( - segment, - ); - } - - late final _FPDFPathSegment_GetTypePtr = - _lookup>( - 'FPDFPathSegment_GetType'); - late final _FPDFPathSegment_GetType = - _FPDFPathSegment_GetTypePtr.asFunction(); - - int FPDFPathSegment_GetClose( - FPDF_PATHSEGMENT segment, - ) { - return _FPDFPathSegment_GetClose( - segment, - ); - } - - late final _FPDFPathSegment_GetClosePtr = - _lookup>( - 'FPDFPathSegment_GetClose'); - late final _FPDFPathSegment_GetClose = - _FPDFPathSegment_GetClosePtr.asFunction(); - - int FPDFPath_MoveTo( - FPDF_PAGEOBJECT path, - double x, - double y, - ) { - return _FPDFPath_MoveTo( - path, - x, - y, - ); - } - - late final _FPDFPath_MoveToPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_PAGEOBJECT, ffi.Float, ffi.Float)>>('FPDFPath_MoveTo'); - late final _FPDFPath_MoveTo = _FPDFPath_MoveToPtr.asFunction< - int Function(FPDF_PAGEOBJECT, double, double)>(); - - int FPDFPath_LineTo( - FPDF_PAGEOBJECT path, - double x, - double y, - ) { - return _FPDFPath_LineTo( - path, - x, - y, - ); - } - - late final _FPDFPath_LineToPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_PAGEOBJECT, ffi.Float, ffi.Float)>>('FPDFPath_LineTo'); - late final _FPDFPath_LineTo = _FPDFPath_LineToPtr.asFunction< - int Function(FPDF_PAGEOBJECT, double, double)>(); - - int FPDFPath_BezierTo( - FPDF_PAGEOBJECT path, - double x1, - double y1, - double x2, - double y2, - double x3, - double y3, - ) { - return _FPDFPath_BezierTo( - path, - x1, - y1, - x2, - y2, - x3, - y3, - ); - } - - late final _FPDFPath_BezierToPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_PAGEOBJECT, ffi.Float, ffi.Float, ffi.Float, - ffi.Float, ffi.Float, ffi.Float)>>('FPDFPath_BezierTo'); - late final _FPDFPath_BezierTo = _FPDFPath_BezierToPtr.asFunction< - int Function( - FPDF_PAGEOBJECT, double, double, double, double, double, double)>(); - - int FPDFPath_Close( - FPDF_PAGEOBJECT path, - ) { - return _FPDFPath_Close( - path, - ); - } - - late final _FPDFPath_ClosePtr = - _lookup>( - 'FPDFPath_Close'); - late final _FPDFPath_Close = - _FPDFPath_ClosePtr.asFunction(); - - int FPDFPath_SetDrawMode( - FPDF_PAGEOBJECT path, - int fillmode, - int stroke, - ) { - return _FPDFPath_SetDrawMode( - path, - fillmode, - stroke, - ); - } - - late final _FPDFPath_SetDrawModePtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_PAGEOBJECT, ffi.Int, FPDF_BOOL)>>('FPDFPath_SetDrawMode'); - late final _FPDFPath_SetDrawMode = _FPDFPath_SetDrawModePtr.asFunction< - int Function(FPDF_PAGEOBJECT, int, int)>(); - - int FPDFPath_GetDrawMode( - FPDF_PAGEOBJECT path, - ffi.Pointer fillmode, - ffi.Pointer stroke, - ) { - return _FPDFPath_GetDrawMode( - path, - fillmode, - stroke, - ); - } - - late final _FPDFPath_GetDrawModePtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_PAGEOBJECT, ffi.Pointer, - ffi.Pointer)>>('FPDFPath_GetDrawMode'); - late final _FPDFPath_GetDrawMode = _FPDFPath_GetDrawModePtr.asFunction< - int Function( - FPDF_PAGEOBJECT, ffi.Pointer, ffi.Pointer)>(); - - FPDF_PAGEOBJECT FPDFPageObj_NewTextObj( - FPDF_DOCUMENT document, - FPDF_BYTESTRING font, - double font_size, - ) { - return _FPDFPageObj_NewTextObj( - document, - font, - font_size, - ); - } - - late final _FPDFPageObj_NewTextObjPtr = _lookup< - ffi.NativeFunction< - FPDF_PAGEOBJECT Function(FPDF_DOCUMENT, FPDF_BYTESTRING, - ffi.Float)>>('FPDFPageObj_NewTextObj'); - late final _FPDFPageObj_NewTextObj = _FPDFPageObj_NewTextObjPtr.asFunction< - FPDF_PAGEOBJECT Function(FPDF_DOCUMENT, FPDF_BYTESTRING, double)>(); - - int FPDFText_SetText( - FPDF_PAGEOBJECT text_object, - FPDF_WIDESTRING text, - ) { - return _FPDFText_SetText( - text_object, - text, - ); - } - - late final _FPDFText_SetTextPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function( - FPDF_PAGEOBJECT, FPDF_WIDESTRING)>>('FPDFText_SetText'); - late final _FPDFText_SetText = _FPDFText_SetTextPtr.asFunction< - int Function(FPDF_PAGEOBJECT, FPDF_WIDESTRING)>(); - - int FPDFText_SetCharcodes( - FPDF_PAGEOBJECT text_object, - ffi.Pointer charcodes, - int count, - ) { - return _FPDFText_SetCharcodes( - text_object, - charcodes, - count, - ); - } - - late final _FPDFText_SetCharcodesPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_PAGEOBJECT, ffi.Pointer, - ffi.Size)>>('FPDFText_SetCharcodes'); - late final _FPDFText_SetCharcodes = _FPDFText_SetCharcodesPtr.asFunction< - int Function(FPDF_PAGEOBJECT, ffi.Pointer, int)>(); - - FPDF_FONT FPDFText_LoadFont( - FPDF_DOCUMENT document, - ffi.Pointer data, - int size, - int font_type, - int cid, - ) { - return _FPDFText_LoadFont( - document, - data, - size, - font_type, - cid, - ); - } - - late final _FPDFText_LoadFontPtr = _lookup< - ffi.NativeFunction< - FPDF_FONT Function(FPDF_DOCUMENT, ffi.Pointer, ffi.Uint32, - ffi.Int, FPDF_BOOL)>>('FPDFText_LoadFont'); - late final _FPDFText_LoadFont = _FPDFText_LoadFontPtr.asFunction< - FPDF_FONT Function( - FPDF_DOCUMENT, ffi.Pointer, int, int, int)>(); - - FPDF_FONT FPDFText_LoadStandardFont( - FPDF_DOCUMENT document, - FPDF_BYTESTRING font, - ) { - return _FPDFText_LoadStandardFont( - document, - font, - ); - } - - late final _FPDFText_LoadStandardFontPtr = _lookup< - ffi - .NativeFunction>( - 'FPDFText_LoadStandardFont'); - late final _FPDFText_LoadStandardFont = _FPDFText_LoadStandardFontPtr - .asFunction(); - - FPDF_FONT FPDFText_LoadCidType2Font( - FPDF_DOCUMENT document, - ffi.Pointer font_data, - int font_data_size, - FPDF_BYTESTRING to_unicode_cmap, - ffi.Pointer cid_to_gid_map_data, - int cid_to_gid_map_data_size, - ) { - return _FPDFText_LoadCidType2Font( - document, - font_data, - font_data_size, - to_unicode_cmap, - cid_to_gid_map_data, - cid_to_gid_map_data_size, - ); - } - - late final _FPDFText_LoadCidType2FontPtr = _lookup< - ffi.NativeFunction< - FPDF_FONT Function( - FPDF_DOCUMENT, - ffi.Pointer, - ffi.Uint32, - FPDF_BYTESTRING, - ffi.Pointer, - ffi.Uint32)>>('FPDFText_LoadCidType2Font'); - late final _FPDFText_LoadCidType2Font = - _FPDFText_LoadCidType2FontPtr.asFunction< - FPDF_FONT Function(FPDF_DOCUMENT, ffi.Pointer, int, - FPDF_BYTESTRING, ffi.Pointer, int)>(); - - int FPDFTextObj_GetFontSize( - FPDF_PAGEOBJECT text, - ffi.Pointer size, - ) { - return _FPDFTextObj_GetFontSize( - text, - size, - ); - } - - late final _FPDFTextObj_GetFontSizePtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_PAGEOBJECT, - ffi.Pointer)>>('FPDFTextObj_GetFontSize'); - late final _FPDFTextObj_GetFontSize = _FPDFTextObj_GetFontSizePtr.asFunction< - int Function(FPDF_PAGEOBJECT, ffi.Pointer)>(); - - void FPDFFont_Close( - FPDF_FONT font, - ) { - return _FPDFFont_Close( - font, - ); - } - - late final _FPDFFont_ClosePtr = - _lookup>( - 'FPDFFont_Close'); - late final _FPDFFont_Close = - _FPDFFont_ClosePtr.asFunction(); - - FPDF_PAGEOBJECT FPDFPageObj_CreateTextObj( - FPDF_DOCUMENT document, - FPDF_FONT font, - double font_size, - ) { - return _FPDFPageObj_CreateTextObj( - document, - font, - font_size, - ); - } - - late final _FPDFPageObj_CreateTextObjPtr = _lookup< - ffi.NativeFunction< - FPDF_PAGEOBJECT Function(FPDF_DOCUMENT, FPDF_FONT, - ffi.Float)>>('FPDFPageObj_CreateTextObj'); - late final _FPDFPageObj_CreateTextObj = _FPDFPageObj_CreateTextObjPtr - .asFunction(); - - FPDF_TEXT_RENDERMODE FPDFTextObj_GetTextRenderMode( - FPDF_PAGEOBJECT text, - ) { - return FPDF_TEXT_RENDERMODE.fromValue(_FPDFTextObj_GetTextRenderMode( - text, - )); - } - - late final _FPDFTextObj_GetTextRenderModePtr = - _lookup>( - 'FPDFTextObj_GetTextRenderMode'); - late final _FPDFTextObj_GetTextRenderMode = _FPDFTextObj_GetTextRenderModePtr - .asFunction(); - - DartFPDF_BOOL FPDFTextObj_SetTextRenderMode( - FPDF_PAGEOBJECT text, - FPDF_TEXT_RENDERMODE render_mode, - ) { - return _FPDFTextObj_SetTextRenderMode( - text, - render_mode.value, - ); - } - - late final _FPDFTextObj_SetTextRenderModePtr = - _lookup>( - 'FPDFTextObj_SetTextRenderMode'); - late final _FPDFTextObj_SetTextRenderMode = _FPDFTextObj_SetTextRenderModePtr - .asFunction(); - - int FPDFTextObj_GetText( - FPDF_PAGEOBJECT text_object, - FPDF_TEXTPAGE text_page, - ffi.Pointer buffer, - int length, - ) { - return _FPDFTextObj_GetText( - text_object, - text_page, - buffer, - length, - ); - } - - late final _FPDFTextObj_GetTextPtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function( - FPDF_PAGEOBJECT, - FPDF_TEXTPAGE, - ffi.Pointer, - ffi.UnsignedLong)>>('FPDFTextObj_GetText'); - late final _FPDFTextObj_GetText = _FPDFTextObj_GetTextPtr.asFunction< - int Function( - FPDF_PAGEOBJECT, FPDF_TEXTPAGE, ffi.Pointer, int)>(); - - FPDF_BITMAP FPDFTextObj_GetRenderedBitmap( - FPDF_DOCUMENT document, - FPDF_PAGE page, - FPDF_PAGEOBJECT text_object, - double scale, - ) { - return _FPDFTextObj_GetRenderedBitmap( - document, - page, - text_object, - scale, - ); - } - - late final _FPDFTextObj_GetRenderedBitmapPtr = _lookup< - ffi.NativeFunction< - FPDF_BITMAP Function(FPDF_DOCUMENT, FPDF_PAGE, FPDF_PAGEOBJECT, - ffi.Float)>>('FPDFTextObj_GetRenderedBitmap'); - late final _FPDFTextObj_GetRenderedBitmap = - _FPDFTextObj_GetRenderedBitmapPtr.asFunction< - FPDF_BITMAP Function( - FPDF_DOCUMENT, FPDF_PAGE, FPDF_PAGEOBJECT, double)>(); - - FPDF_FONT FPDFTextObj_GetFont( - FPDF_PAGEOBJECT text, - ) { - return _FPDFTextObj_GetFont( - text, - ); - } - - late final _FPDFTextObj_GetFontPtr = - _lookup>( - 'FPDFTextObj_GetFont'); - late final _FPDFTextObj_GetFont = - _FPDFTextObj_GetFontPtr.asFunction(); - - int FPDFFont_GetFontName( - FPDF_FONT font, - ffi.Pointer buffer, - int length, - ) { - return _FPDFFont_GetFontName( - font, - buffer, - length, - ); - } - - late final _FPDFFont_GetFontNamePtr = _lookup< - ffi.NativeFunction< - ffi.UnsignedLong Function(FPDF_FONT, ffi.Pointer, - ffi.UnsignedLong)>>('FPDFFont_GetFontName'); - late final _FPDFFont_GetFontName = _FPDFFont_GetFontNamePtr.asFunction< - int Function(FPDF_FONT, ffi.Pointer, int)>(); - - int FPDFFont_GetFontData( - FPDF_FONT font, - ffi.Pointer buffer, - int buflen, - ffi.Pointer out_buflen, - ) { - return _FPDFFont_GetFontData( - font, - buffer, - buflen, - out_buflen, - ); - } - - late final _FPDFFont_GetFontDataPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_FONT, ffi.Pointer, ffi.Size, - ffi.Pointer)>>('FPDFFont_GetFontData'); - late final _FPDFFont_GetFontData = _FPDFFont_GetFontDataPtr.asFunction< - int Function( - FPDF_FONT, ffi.Pointer, int, ffi.Pointer)>(); - - int FPDFFont_GetIsEmbedded( - FPDF_FONT font, - ) { - return _FPDFFont_GetIsEmbedded( - font, - ); - } - - late final _FPDFFont_GetIsEmbeddedPtr = - _lookup>( - 'FPDFFont_GetIsEmbedded'); - late final _FPDFFont_GetIsEmbedded = - _FPDFFont_GetIsEmbeddedPtr.asFunction(); - - int FPDFFont_GetFlags( - FPDF_FONT font, - ) { - return _FPDFFont_GetFlags( - font, - ); - } - - late final _FPDFFont_GetFlagsPtr = - _lookup>( - 'FPDFFont_GetFlags'); - late final _FPDFFont_GetFlags = - _FPDFFont_GetFlagsPtr.asFunction(); - - int FPDFFont_GetWeight( - FPDF_FONT font, - ) { - return _FPDFFont_GetWeight( - font, - ); - } - - late final _FPDFFont_GetWeightPtr = - _lookup>( - 'FPDFFont_GetWeight'); - late final _FPDFFont_GetWeight = - _FPDFFont_GetWeightPtr.asFunction(); - - int FPDFFont_GetItalicAngle( - FPDF_FONT font, - ffi.Pointer angle, - ) { - return _FPDFFont_GetItalicAngle( - font, - angle, - ); - } - - late final _FPDFFont_GetItalicAnglePtr = _lookup< - ffi - .NativeFunction)>>( - 'FPDFFont_GetItalicAngle'); - late final _FPDFFont_GetItalicAngle = _FPDFFont_GetItalicAnglePtr.asFunction< - int Function(FPDF_FONT, ffi.Pointer)>(); - - int FPDFFont_GetAscent( - FPDF_FONT font, - double font_size, - ffi.Pointer ascent, - ) { - return _FPDFFont_GetAscent( - font, - font_size, - ascent, - ); - } - - late final _FPDFFont_GetAscentPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_FONT, ffi.Float, - ffi.Pointer)>>('FPDFFont_GetAscent'); - late final _FPDFFont_GetAscent = _FPDFFont_GetAscentPtr.asFunction< - int Function(FPDF_FONT, double, ffi.Pointer)>(); - - int FPDFFont_GetDescent( - FPDF_FONT font, - double font_size, - ffi.Pointer descent, - ) { - return _FPDFFont_GetDescent( - font, - font_size, - descent, - ); - } - - late final _FPDFFont_GetDescentPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_FONT, ffi.Float, - ffi.Pointer)>>('FPDFFont_GetDescent'); - late final _FPDFFont_GetDescent = _FPDFFont_GetDescentPtr.asFunction< - int Function(FPDF_FONT, double, ffi.Pointer)>(); - - int FPDFFont_GetGlyphWidth( - FPDF_FONT font, - int glyph, - double font_size, - ffi.Pointer width, - ) { - return _FPDFFont_GetGlyphWidth( - font, - glyph, - font_size, - width, - ); - } - - late final _FPDFFont_GetGlyphWidthPtr = _lookup< - ffi.NativeFunction< - FPDF_BOOL Function(FPDF_FONT, ffi.Uint32, ffi.Float, - ffi.Pointer)>>('FPDFFont_GetGlyphWidth'); - late final _FPDFFont_GetGlyphWidth = _FPDFFont_GetGlyphWidthPtr.asFunction< - int Function(FPDF_FONT, int, double, ffi.Pointer)>(); - - FPDF_GLYPHPATH FPDFFont_GetGlyphPath( - FPDF_FONT font, - int glyph, - double font_size, - ) { - return _FPDFFont_GetGlyphPath( - font, - glyph, - font_size, - ); - } - - late final _FPDFFont_GetGlyphPathPtr = _lookup< - ffi.NativeFunction< - FPDF_GLYPHPATH Function( - FPDF_FONT, ffi.Uint32, ffi.Float)>>('FPDFFont_GetGlyphPath'); - late final _FPDFFont_GetGlyphPath = _FPDFFont_GetGlyphPathPtr.asFunction< - FPDF_GLYPHPATH Function(FPDF_FONT, int, double)>(); - - int FPDFGlyphPath_CountGlyphSegments( - FPDF_GLYPHPATH glyphpath, - ) { - return _FPDFGlyphPath_CountGlyphSegments( - glyphpath, - ); - } - - late final _FPDFGlyphPath_CountGlyphSegmentsPtr = - _lookup>( - 'FPDFGlyphPath_CountGlyphSegments'); - late final _FPDFGlyphPath_CountGlyphSegments = - _FPDFGlyphPath_CountGlyphSegmentsPtr.asFunction< - int Function(FPDF_GLYPHPATH)>(); - - FPDF_PATHSEGMENT FPDFGlyphPath_GetGlyphPathSegment( - FPDF_GLYPHPATH glyphpath, - int index, - ) { - return _FPDFGlyphPath_GetGlyphPathSegment( - glyphpath, - index, - ); - } - - late final _FPDFGlyphPath_GetGlyphPathSegmentPtr = _lookup< - ffi - .NativeFunction>( - 'FPDFGlyphPath_GetGlyphPathSegment'); - late final _FPDFGlyphPath_GetGlyphPathSegment = - _FPDFGlyphPath_GetGlyphPathSegmentPtr.asFunction< - FPDF_PATHSEGMENT Function(FPDF_GLYPHPATH, int)>(); - - int FPDFFormObj_CountObjects( - FPDF_PAGEOBJECT form_object, - ) { - return _FPDFFormObj_CountObjects( - form_object, - ); - } - - late final _FPDFFormObj_CountObjectsPtr = - _lookup>( - 'FPDFFormObj_CountObjects'); - late final _FPDFFormObj_CountObjects = - _FPDFFormObj_CountObjectsPtr.asFunction(); - - FPDF_PAGEOBJECT FPDFFormObj_GetObject( - FPDF_PAGEOBJECT form_object, - int index, - ) { - return _FPDFFormObj_GetObject( - form_object, - index, - ); - } - - late final _FPDFFormObj_GetObjectPtr = _lookup< - ffi.NativeFunction< - FPDF_PAGEOBJECT Function( - FPDF_PAGEOBJECT, ffi.UnsignedLong)>>('FPDFFormObj_GetObject'); - late final _FPDFFormObj_GetObject = _FPDFFormObj_GetObjectPtr.asFunction< - FPDF_PAGEOBJECT Function(FPDF_PAGEOBJECT, int)>(); -} - -enum FPDF_TEXT_RENDERMODE { - FPDF_TEXTRENDERMODE_UNKNOWN(-1), - FPDF_TEXTRENDERMODE_FILL(0), - FPDF_TEXTRENDERMODE_STROKE(1), - FPDF_TEXTRENDERMODE_FILL_STROKE(2), - FPDF_TEXTRENDERMODE_INVISIBLE(3), - FPDF_TEXTRENDERMODE_FILL_CLIP(4), - FPDF_TEXTRENDERMODE_STROKE_CLIP(5), - FPDF_TEXTRENDERMODE_FILL_STROKE_CLIP(6), - FPDF_TEXTRENDERMODE_CLIP(7); - - static const FPDF_TEXTRENDERMODE_LAST = FPDF_TEXTRENDERMODE_CLIP; - - final int value; - const FPDF_TEXT_RENDERMODE(this.value); - - static FPDF_TEXT_RENDERMODE fromValue(int value) => switch (value) { - -1 => FPDF_TEXTRENDERMODE_UNKNOWN, - 0 => FPDF_TEXTRENDERMODE_FILL, - 1 => FPDF_TEXTRENDERMODE_STROKE, - 2 => FPDF_TEXTRENDERMODE_FILL_STROKE, - 3 => FPDF_TEXTRENDERMODE_INVISIBLE, - 4 => FPDF_TEXTRENDERMODE_FILL_CLIP, - 5 => FPDF_TEXTRENDERMODE_STROKE_CLIP, - 6 => FPDF_TEXTRENDERMODE_FILL_STROKE_CLIP, - 7 => FPDF_TEXTRENDERMODE_CLIP, - _ => - throw ArgumentError("Unknown value for FPDF_TEXT_RENDERMODE: $value"), - }; - - @override - String toString() { - if (this == FPDF_TEXTRENDERMODE_CLIP) - return "FPDF_TEXT_RENDERMODE.FPDF_TEXTRENDERMODE_CLIP, FPDF_TEXT_RENDERMODE.FPDF_TEXTRENDERMODE_LAST"; - return super.toString(); - } -} - -final class fpdf_action_t__ extends ffi.Opaque {} - -typedef FPDF_ACTION = ffi.Pointer; - -final class fpdf_annotation_t__ extends ffi.Opaque {} - -typedef FPDF_ANNOTATION = ffi.Pointer; - -final class fpdf_attachment_t__ extends ffi.Opaque {} - -typedef FPDF_ATTACHMENT = ffi.Pointer; - -final class fpdf_avail_t__ extends ffi.Opaque {} - -typedef FPDF_AVAIL = ffi.Pointer; - -final class fpdf_bitmap_t__ extends ffi.Opaque {} - -typedef FPDF_BITMAP = ffi.Pointer; - -final class fpdf_bookmark_t__ extends ffi.Opaque {} - -typedef FPDF_BOOKMARK = ffi.Pointer; - -final class fpdf_clippath_t__ extends ffi.Opaque {} - -typedef FPDF_CLIPPATH = ffi.Pointer; - -final class fpdf_dest_t__ extends ffi.Opaque {} - -typedef FPDF_DEST = ffi.Pointer; - -final class fpdf_document_t__ extends ffi.Opaque {} - -typedef FPDF_DOCUMENT = ffi.Pointer; - -final class fpdf_font_t__ extends ffi.Opaque {} - -typedef FPDF_FONT = ffi.Pointer; - -final class fpdf_form_handle_t__ extends ffi.Opaque {} - -typedef FPDF_FORMHANDLE = ffi.Pointer; - -final class fpdf_glyphpath_t__ extends ffi.Opaque {} - -typedef FPDF_GLYPHPATH = ffi.Pointer; - -final class fpdf_javascript_action_t extends ffi.Opaque {} - -typedef FPDF_JAVASCRIPT_ACTION = ffi.Pointer; - -final class fpdf_link_t__ extends ffi.Opaque {} - -typedef FPDF_LINK = ffi.Pointer; - -final class fpdf_page_t__ extends ffi.Opaque {} - -typedef FPDF_PAGE = ffi.Pointer; - -final class fpdf_pagelink_t__ extends ffi.Opaque {} - -typedef FPDF_PAGELINK = ffi.Pointer; - -final class fpdf_pageobject_t__ extends ffi.Opaque {} - -typedef FPDF_PAGEOBJECT = ffi.Pointer; - -final class fpdf_pageobjectmark_t__ extends ffi.Opaque {} - -typedef FPDF_PAGEOBJECTMARK = ffi.Pointer; - -final class fpdf_pagerange_t__ extends ffi.Opaque {} - -typedef FPDF_PAGERANGE = ffi.Pointer; - -final class fpdf_pathsegment_t extends ffi.Opaque {} - -typedef FPDF_PATHSEGMENT = ffi.Pointer; - -final class fpdf_schhandle_t__ extends ffi.Opaque {} - -typedef FPDF_SCHHANDLE = ffi.Pointer; - -final class fpdf_signature_t__ extends ffi.Opaque {} - -typedef FPDF_SIGNATURE = ffi.Pointer; -typedef FPDF_SKIA_CANVAS = ffi.Pointer; - -final class fpdf_structelement_t__ extends ffi.Opaque {} - -typedef FPDF_STRUCTELEMENT = ffi.Pointer; - -final class fpdf_structelement_attr_t__ extends ffi.Opaque {} - -typedef FPDF_STRUCTELEMENT_ATTR = ffi.Pointer; - -final class fpdf_structelement_attr_value_t__ extends ffi.Opaque {} - -typedef FPDF_STRUCTELEMENT_ATTR_VALUE - = ffi.Pointer; - -final class fpdf_structtree_t__ extends ffi.Opaque {} - -typedef FPDF_STRUCTTREE = ffi.Pointer; - -final class fpdf_textpage_t__ extends ffi.Opaque {} - -typedef FPDF_TEXTPAGE = ffi.Pointer; - -final class fpdf_widget_t__ extends ffi.Opaque {} - -typedef FPDF_WIDGET = ffi.Pointer; - -final class fpdf_xobject_t__ extends ffi.Opaque {} - -typedef FPDF_XOBJECT = ffi.Pointer; -typedef FPDF_BOOL = ffi.Int; -typedef DartFPDF_BOOL = int; -typedef FPDF_RESULT = ffi.Int; -typedef DartFPDF_RESULT = int; -typedef FPDF_DWORD = ffi.UnsignedLong; -typedef DartFPDF_DWORD = int; -typedef FS_FLOAT = ffi.Float; -typedef DartFS_FLOAT = double; - -enum _FPDF_DUPLEXTYPE_ { - DuplexUndefined(0), - Simplex(1), - DuplexFlipShortEdge(2), - DuplexFlipLongEdge(3); - - final int value; - const _FPDF_DUPLEXTYPE_(this.value); - - static _FPDF_DUPLEXTYPE_ fromValue(int value) => switch (value) { - 0 => DuplexUndefined, - 1 => Simplex, - 2 => DuplexFlipShortEdge, - 3 => DuplexFlipLongEdge, - _ => throw ArgumentError("Unknown value for _FPDF_DUPLEXTYPE_: $value"), - }; -} - -typedef FPDF_WCHAR = ffi.UnsignedShort; -typedef DartFPDF_WCHAR = int; -typedef FPDF_BYTESTRING = ffi.Pointer; -typedef FPDF_WIDESTRING = ffi.Pointer; - -final class FPDF_BSTR_ extends ffi.Struct { - external ffi.Pointer str; - - @ffi.Int() - external int len; -} - -typedef FPDF_BSTR = FPDF_BSTR_; -typedef FPDF_STRING = ffi.Pointer; - -final class _FS_MATRIX_ extends ffi.Struct { - @ffi.Float() - external double a; - - @ffi.Float() - external double b; - - @ffi.Float() - external double c; - - @ffi.Float() - external double d; - - @ffi.Float() - external double e; - - @ffi.Float() - external double f; -} - -typedef FS_MATRIX = _FS_MATRIX_; - -final class _FS_RECTF_ extends ffi.Struct { - @ffi.Float() - external double left; - - @ffi.Float() - external double top; - - @ffi.Float() - external double right; - - @ffi.Float() - external double bottom; -} - -typedef FS_LPRECTF = ffi.Pointer<_FS_RECTF_>; -typedef FS_RECTF = _FS_RECTF_; -typedef FS_LPCRECTF = ffi.Pointer; - -final class FS_SIZEF_ extends ffi.Struct { - @ffi.Float() - external double width; - - @ffi.Float() - external double height; -} - -typedef FS_LPSIZEF = ffi.Pointer; -typedef FS_SIZEF = FS_SIZEF_; -typedef FS_LPCSIZEF = ffi.Pointer; - -final class FS_POINTF_ extends ffi.Struct { - @ffi.Float() - external double x; - - @ffi.Float() - external double y; -} - -typedef FS_LPPOINTF = ffi.Pointer; -typedef FS_POINTF = FS_POINTF_; -typedef FS_LPCPOINTF = ffi.Pointer; - -final class _FS_QUADPOINTSF extends ffi.Struct { - @FS_FLOAT() - external double x1; - - @FS_FLOAT() - external double y1; - - @FS_FLOAT() - external double x2; - - @FS_FLOAT() - external double y2; - - @FS_FLOAT() - external double x3; - - @FS_FLOAT() - external double y3; - - @FS_FLOAT() - external double x4; - - @FS_FLOAT() - external double y4; -} - -typedef FS_QUADPOINTSF = _FS_QUADPOINTSF; -typedef FPDF_ANNOTATION_SUBTYPE = ffi.Int; -typedef DartFPDF_ANNOTATION_SUBTYPE = int; -typedef FPDF_ANNOT_APPEARANCEMODE = ffi.Int; -typedef DartFPDF_ANNOT_APPEARANCEMODE = int; -typedef FPDF_OBJECT_TYPE = ffi.Int; -typedef DartFPDF_OBJECT_TYPE = int; - -enum FPDF_RENDERER_TYPE { - FPDF_RENDERERTYPE_AGG(0), - FPDF_RENDERERTYPE_SKIA(1); - - final int value; - const FPDF_RENDERER_TYPE(this.value); - - static FPDF_RENDERER_TYPE fromValue(int value) => switch (value) { - 0 => FPDF_RENDERERTYPE_AGG, - 1 => FPDF_RENDERERTYPE_SKIA, - _ => - throw ArgumentError("Unknown value for FPDF_RENDERER_TYPE: $value"), - }; -} - -final class FPDF_LIBRARY_CONFIG_ extends ffi.Struct { - @ffi.Int() - external int version; - - external ffi.Pointer> m_pUserFontPaths; - - external ffi.Pointer m_pIsolate; - - @ffi.UnsignedInt() - external int m_v8EmbedderSlot; - - external ffi.Pointer m_pPlatform; - - @ffi.UnsignedInt() - external int m_RendererTypeAsInt; - - FPDF_RENDERER_TYPE get m_RendererType => - FPDF_RENDERER_TYPE.fromValue(m_RendererTypeAsInt); -} - -typedef FPDF_LIBRARY_CONFIG = FPDF_LIBRARY_CONFIG_; - -final class FPDF_FILEACCESS extends ffi.Struct { - @ffi.UnsignedLong() - external int m_FileLen; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Int Function( - ffi.Pointer param, - ffi.UnsignedLong position, - ffi.Pointer pBuf, - ffi.UnsignedLong size)>> m_GetBlock; - - external ffi.Pointer m_Param; -} - -final class FPDF_FILEHANDLER_ extends ffi.Struct { - external ffi.Pointer clientData; - - external ffi.Pointer< - ffi - .NativeFunction clientData)>> - Release; - - external ffi.Pointer< - ffi.NativeFunction< - FPDF_DWORD Function(ffi.Pointer clientData)>> GetSize; - - external ffi.Pointer< - ffi.NativeFunction< - FPDF_RESULT Function( - ffi.Pointer clientData, - FPDF_DWORD offset, - ffi.Pointer buffer, - FPDF_DWORD size)>> ReadBlock; - - external ffi.Pointer< - ffi.NativeFunction< - FPDF_RESULT Function( - ffi.Pointer clientData, - FPDF_DWORD offset, - ffi.Pointer buffer, - FPDF_DWORD size)>> WriteBlock; - - external ffi.Pointer< - ffi.NativeFunction< - FPDF_RESULT Function(ffi.Pointer clientData)>> Flush; - - external ffi.Pointer< - ffi.NativeFunction< - FPDF_RESULT Function( - ffi.Pointer clientData, FPDF_DWORD size)>> Truncate; -} - -typedef FPDF_FILEHANDLER = FPDF_FILEHANDLER_; - -final class FPDF_COLORSCHEME_ extends ffi.Struct { - @FPDF_DWORD() - external int path_fill_color; - - @FPDF_DWORD() - external int path_stroke_color; - - @FPDF_DWORD() - external int text_fill_color; - - @FPDF_DWORD() - external int text_stroke_color; -} - -typedef FPDF_COLORSCHEME = FPDF_COLORSCHEME_; - -final class HDC__ extends ffi.Struct { - @ffi.Int() - external int unused; -} - -typedef HDC = ffi.Pointer; - -final class _IPDF_JsPlatform extends ffi.Struct { - @ffi.Int() - external int version; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Int Function( - ffi.Pointer<_IPDF_JsPlatform> pThis, - FPDF_WIDESTRING Msg, - FPDF_WIDESTRING Title, - ffi.Int Type, - ffi.Int Icon)>> app_alert; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer<_IPDF_JsPlatform> pThis, ffi.Int nType)>> app_beep; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Int Function( - ffi.Pointer<_IPDF_JsPlatform> pThis, - FPDF_WIDESTRING Question, - FPDF_WIDESTRING Title, - FPDF_WIDESTRING Default, - FPDF_WIDESTRING cLabel, - FPDF_BOOL bPassword, - ffi.Pointer response, - ffi.Int length)>> app_response; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Int Function(ffi.Pointer<_IPDF_JsPlatform> pThis, - ffi.Pointer filePath, ffi.Int length)>> Doc_getFilePath; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer<_IPDF_JsPlatform> pThis, - ffi.Pointer mailData, - ffi.Int length, - FPDF_BOOL bUI, - FPDF_WIDESTRING To, - FPDF_WIDESTRING Subject, - FPDF_WIDESTRING CC, - FPDF_WIDESTRING BCC, - FPDF_WIDESTRING Msg)>> Doc_mail; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer<_IPDF_JsPlatform> pThis, - FPDF_BOOL bUI, - ffi.Int nStart, - ffi.Int nEnd, - FPDF_BOOL bSilent, - FPDF_BOOL bShrinkToFit, - FPDF_BOOL bPrintAsImage, - FPDF_BOOL bReverse, - FPDF_BOOL bAnnotations)>> Doc_print; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer<_IPDF_JsPlatform> pThis, - ffi.Pointer formData, - ffi.Int length, - FPDF_WIDESTRING URL)>> Doc_submitForm; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer<_IPDF_JsPlatform> pThis, ffi.Int nPageNum)>> - Doc_gotoPage; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Int Function(ffi.Pointer<_IPDF_JsPlatform> pThis, - ffi.Pointer filePath, ffi.Int length)>> Field_browse; - - external ffi.Pointer m_pFormfillinfo; - - external ffi.Pointer m_isolate; - - @ffi.UnsignedInt() - external int m_v8EmbedderSlot; -} - -typedef IPDF_JSPLATFORM = _IPDF_JsPlatform; -typedef TimerCallbackFunction = ffi.Void Function(ffi.Int idEvent); -typedef DartTimerCallbackFunction = void Function(int idEvent); -typedef TimerCallback = ffi.Pointer>; - -final class _FPDF_SYSTEMTIME extends ffi.Struct { - @ffi.UnsignedShort() - external int wYear; - - @ffi.UnsignedShort() - external int wMonth; - - @ffi.UnsignedShort() - external int wDayOfWeek; - - @ffi.UnsignedShort() - external int wDay; - - @ffi.UnsignedShort() - external int wHour; - - @ffi.UnsignedShort() - external int wMinute; - - @ffi.UnsignedShort() - external int wSecond; - - @ffi.UnsignedShort() - external int wMilliseconds; -} - -typedef FPDF_SYSTEMTIME = _FPDF_SYSTEMTIME; - -final class _FPDF_FORMFILLINFO extends ffi.Struct { - @ffi.Int() - external int version; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer<_FPDF_FORMFILLINFO> pThis)>> Release; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer<_FPDF_FORMFILLINFO> pThis, - FPDF_PAGE page, - ffi.Double left, - ffi.Double top, - ffi.Double right, - ffi.Double bottom)>> FFI_Invalidate; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer<_FPDF_FORMFILLINFO> pThis, - FPDF_PAGE page, - ffi.Double left, - ffi.Double top, - ffi.Double right, - ffi.Double bottom)>> FFI_OutputSelectedRect; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer<_FPDF_FORMFILLINFO> pThis, ffi.Int nCursorType)>> - FFI_SetCursor; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Int Function(ffi.Pointer<_FPDF_FORMFILLINFO> pThis, - ffi.Int uElapse, TimerCallback lpTimerFunc)>> FFI_SetTimer; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer<_FPDF_FORMFILLINFO> pThis, ffi.Int nTimerID)>> - FFI_KillTimer; - - external ffi.Pointer< - ffi.NativeFunction< - FPDF_SYSTEMTIME Function(ffi.Pointer<_FPDF_FORMFILLINFO> pThis)>> - FFI_GetLocalTime; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer<_FPDF_FORMFILLINFO> pThis)>> - FFI_OnChange; - - external ffi.Pointer< - ffi.NativeFunction< - FPDF_PAGE Function(ffi.Pointer<_FPDF_FORMFILLINFO> pThis, - FPDF_DOCUMENT document, ffi.Int nPageIndex)>> FFI_GetPage; - - external ffi.Pointer< - ffi.NativeFunction< - FPDF_PAGE Function(ffi.Pointer<_FPDF_FORMFILLINFO> pThis, - FPDF_DOCUMENT document)>> FFI_GetCurrentPage; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Int Function( - ffi.Pointer<_FPDF_FORMFILLINFO> pThis, FPDF_PAGE page)>> - FFI_GetRotation; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer<_FPDF_FORMFILLINFO> pThis, - FPDF_BYTESTRING namedAction)>> FFI_ExecuteNamedAction; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer<_FPDF_FORMFILLINFO> pThis, - FPDF_WIDESTRING value, - FPDF_DWORD valueLen, - FPDF_BOOL is_focus)>> FFI_SetTextFieldFocus; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer<_FPDF_FORMFILLINFO> pThis, - FPDF_BYTESTRING bsURI)>> FFI_DoURIAction; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer<_FPDF_FORMFILLINFO> pThis, - ffi.Int nPageIndex, - ffi.Int zoomMode, - ffi.Pointer fPosArray, - ffi.Int sizeofArray)>> FFI_DoGoToAction; - - external ffi.Pointer m_pJsPlatform; - - @FPDF_BOOL() - external int xfa_disabled; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer<_FPDF_FORMFILLINFO> pThis, - FPDF_PAGE page, - FPDF_BOOL bVisible, - ffi.Double left, - ffi.Double top, - ffi.Double right, - ffi.Double bottom)>> FFI_DisplayCaret; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Int Function(ffi.Pointer<_FPDF_FORMFILLINFO> pThis, - FPDF_DOCUMENT document)>> FFI_GetCurrentPageIndex; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer<_FPDF_FORMFILLINFO> pThis, - FPDF_DOCUMENT document, ffi.Int iCurPage)>> FFI_SetCurrentPage; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer<_FPDF_FORMFILLINFO> pThis, - FPDF_DOCUMENT document, FPDF_WIDESTRING wsURL)>> FFI_GotoURL; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer<_FPDF_FORMFILLINFO> pThis, - FPDF_PAGE page, - ffi.Pointer left, - ffi.Pointer top, - ffi.Pointer right, - ffi.Pointer bottom)>> FFI_GetPageViewRect; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer<_FPDF_FORMFILLINFO> pThis, - ffi.Int page_count, FPDF_DWORD event_type)>> FFI_PageEvent; - - external ffi.Pointer< - ffi.NativeFunction< - FPDF_BOOL Function( - ffi.Pointer<_FPDF_FORMFILLINFO> pThis, - FPDF_PAGE page, - FPDF_WIDGET hWidget, - ffi.Int menuFlag, - ffi.Float x, - ffi.Float y)>> FFI_PopupMenu; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Pointer Function( - ffi.Pointer<_FPDF_FORMFILLINFO> pThis, - ffi.Int fileFlag, - FPDF_WIDESTRING wsURL, - ffi.Pointer mode)>> FFI_OpenFile; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer<_FPDF_FORMFILLINFO> pThis, - ffi.Pointer fileHandler, - FPDF_WIDESTRING pTo, - FPDF_WIDESTRING pSubject, - FPDF_WIDESTRING pCC, - FPDF_WIDESTRING pBcc, - FPDF_WIDESTRING pMsg)>> FFI_EmailTo; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer<_FPDF_FORMFILLINFO> pThis, - ffi.Pointer fileHandler, - ffi.Int fileFlag, - FPDF_WIDESTRING uploadTo)>> FFI_UploadTo; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Int Function(ffi.Pointer<_FPDF_FORMFILLINFO> pThis, - ffi.Pointer platform, ffi.Int length)>> FFI_GetPlatform; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Int Function(ffi.Pointer<_FPDF_FORMFILLINFO> pThis, - ffi.Pointer language, ffi.Int length)>> FFI_GetLanguage; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Pointer Function( - ffi.Pointer<_FPDF_FORMFILLINFO> pThis, FPDF_WIDESTRING URL)>> - FFI_DownloadFromURL; - - external ffi.Pointer< - ffi.NativeFunction< - FPDF_BOOL Function( - ffi.Pointer<_FPDF_FORMFILLINFO> pThis, - FPDF_WIDESTRING wsURL, - FPDF_WIDESTRING wsData, - FPDF_WIDESTRING wsContentType, - FPDF_WIDESTRING wsEncode, - FPDF_WIDESTRING wsHeader, - ffi.Pointer response)>> FFI_PostRequestURL; - - external ffi.Pointer< - ffi.NativeFunction< - FPDF_BOOL Function( - ffi.Pointer<_FPDF_FORMFILLINFO> pThis, - FPDF_WIDESTRING wsURL, - FPDF_WIDESTRING wsData, - FPDF_WIDESTRING wsEncode)>> FFI_PutRequestURL; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer<_FPDF_FORMFILLINFO> param, - FPDF_ANNOTATION annot, ffi.Int page_index)>> FFI_OnFocusChange; - - external ffi.Pointer< - ffi.NativeFunction< - ffi.Void Function( - ffi.Pointer<_FPDF_FORMFILLINFO> param, - FPDF_BYTESTRING uri, - ffi.Int modifiers)>> FFI_DoURIActionWithKeyboardModifier; -} - -typedef FPDF_FORMFILLINFO = _FPDF_FORMFILLINFO; - -enum FPDFANNOT_COLORTYPE { - FPDFANNOT_COLORTYPE_Color(0), - FPDFANNOT_COLORTYPE_InteriorColor(1); - - final int value; - const FPDFANNOT_COLORTYPE(this.value); - - static FPDFANNOT_COLORTYPE fromValue(int value) => switch (value) { - 0 => FPDFANNOT_COLORTYPE_Color, - 1 => FPDFANNOT_COLORTYPE_InteriorColor, - _ => - throw ArgumentError("Unknown value for FPDFANNOT_COLORTYPE: $value"), - }; -} - -enum FPDF_FILEIDTYPE { - FILEIDTYPE_PERMANENT(0), - FILEIDTYPE_CHANGING(1); - - final int value; - const FPDF_FILEIDTYPE(this.value); - - static FPDF_FILEIDTYPE fromValue(int value) => switch (value) { - 0 => FILEIDTYPE_PERMANENT, - 1 => FILEIDTYPE_CHANGING, - _ => throw ArgumentError("Unknown value for FPDF_FILEIDTYPE: $value"), - }; -} - -final class FPDF_IMAGEOBJ_METADATA extends ffi.Struct { - @ffi.UnsignedInt() - external int width; - - @ffi.UnsignedInt() - external int height; - - @ffi.Float() - external double horizontal_dpi; - - @ffi.Float() - external double vertical_dpi; - - @ffi.UnsignedInt() - external int bits_per_pixel; - - @ffi.Int() - external int colorspace; - - @ffi.Int() - external int marked_content_id; -} - -const int FPDF_OBJECT_UNKNOWN = 0; - -const int FPDF_OBJECT_BOOLEAN = 1; - -const int FPDF_OBJECT_NUMBER = 2; - -const int FPDF_OBJECT_STRING = 3; - -const int FPDF_OBJECT_NAME = 4; - -const int FPDF_OBJECT_ARRAY = 5; - -const int FPDF_OBJECT_DICTIONARY = 6; - -const int FPDF_OBJECT_STREAM = 7; - -const int FPDF_OBJECT_NULLOBJ = 8; - -const int FPDF_OBJECT_REFERENCE = 9; - -const int FPDF_POLICY_MACHINETIME_ACCESS = 0; - -const int FPDF_ERR_SUCCESS = 0; - -const int FPDF_ERR_UNKNOWN = 1; - -const int FPDF_ERR_FILE = 2; - -const int FPDF_ERR_FORMAT = 3; - -const int FPDF_ERR_PASSWORD = 4; - -const int FPDF_ERR_SECURITY = 5; - -const int FPDF_ERR_PAGE = 6; - -const int FPDF_ANNOT = 1; - -const int FPDF_LCD_TEXT = 2; - -const int FPDF_NO_NATIVETEXT = 4; - -const int FPDF_GRAYSCALE = 8; - -const int FPDF_DEBUG_INFO = 128; - -const int FPDF_NO_CATCH = 256; - -const int FPDF_RENDER_LIMITEDIMAGECACHE = 512; - -const int FPDF_RENDER_FORCEHALFTONE = 1024; - -const int FPDF_PRINTING = 2048; - -const int FPDF_RENDER_NO_SMOOTHTEXT = 4096; - -const int FPDF_RENDER_NO_SMOOTHIMAGE = 8192; - -const int FPDF_RENDER_NO_SMOOTHPATH = 16384; - -const int FPDF_REVERSE_BYTE_ORDER = 16; - -const int FPDF_CONVERT_FILL_TO_STROKE = 32; - -const int FPDFBitmap_Unknown = 0; - -const int FPDFBitmap_Gray = 1; - -const int FPDFBitmap_BGR = 2; - -const int FPDFBitmap_BGRx = 3; - -const int FPDFBitmap_BGRA = 4; - -const int FORMTYPE_NONE = 0; - -const int FORMTYPE_ACRO_FORM = 1; - -const int FORMTYPE_XFA_FULL = 2; - -const int FORMTYPE_XFA_FOREGROUND = 3; - -const int FORMTYPE_COUNT = 4; - -const int JSPLATFORM_ALERT_BUTTON_OK = 0; - -const int JSPLATFORM_ALERT_BUTTON_OKCANCEL = 1; - -const int JSPLATFORM_ALERT_BUTTON_YESNO = 2; - -const int JSPLATFORM_ALERT_BUTTON_YESNOCANCEL = 3; - -const int JSPLATFORM_ALERT_BUTTON_DEFAULT = 0; - -const int JSPLATFORM_ALERT_ICON_ERROR = 0; - -const int JSPLATFORM_ALERT_ICON_WARNING = 1; - -const int JSPLATFORM_ALERT_ICON_QUESTION = 2; - -const int JSPLATFORM_ALERT_ICON_STATUS = 3; - -const int JSPLATFORM_ALERT_ICON_ASTERISK = 4; - -const int JSPLATFORM_ALERT_ICON_DEFAULT = 0; - -const int JSPLATFORM_ALERT_RETURN_OK = 1; - -const int JSPLATFORM_ALERT_RETURN_CANCEL = 2; - -const int JSPLATFORM_ALERT_RETURN_NO = 3; - -const int JSPLATFORM_ALERT_RETURN_YES = 4; - -const int JSPLATFORM_BEEP_ERROR = 0; - -const int JSPLATFORM_BEEP_WARNING = 1; - -const int JSPLATFORM_BEEP_QUESTION = 2; - -const int JSPLATFORM_BEEP_STATUS = 3; - -const int JSPLATFORM_BEEP_DEFAULT = 4; - -const int FXCT_ARROW = 0; - -const int FXCT_NESW = 1; - -const int FXCT_NWSE = 2; - -const int FXCT_VBEAM = 3; - -const int FXCT_HBEAM = 4; - -const int FXCT_HAND = 5; - -const int FPDFDOC_AACTION_WC = 16; - -const int FPDFDOC_AACTION_WS = 17; - -const int FPDFDOC_AACTION_DS = 18; - -const int FPDFDOC_AACTION_WP = 19; - -const int FPDFDOC_AACTION_DP = 20; - -const int FPDFPAGE_AACTION_OPEN = 0; - -const int FPDFPAGE_AACTION_CLOSE = 1; - -const int FPDF_FORMFIELD_UNKNOWN = 0; - -const int FPDF_FORMFIELD_PUSHBUTTON = 1; - -const int FPDF_FORMFIELD_CHECKBOX = 2; - -const int FPDF_FORMFIELD_RADIOBUTTON = 3; - -const int FPDF_FORMFIELD_COMBOBOX = 4; - -const int FPDF_FORMFIELD_LISTBOX = 5; - -const int FPDF_FORMFIELD_TEXTFIELD = 6; - -const int FPDF_FORMFIELD_SIGNATURE = 7; - -const int FPDF_FORMFIELD_COUNT = 8; - -const int FPDF_ANNOT_UNKNOWN = 0; - -const int FPDF_ANNOT_TEXT = 1; - -const int FPDF_ANNOT_LINK = 2; - -const int FPDF_ANNOT_FREETEXT = 3; - -const int FPDF_ANNOT_LINE = 4; - -const int FPDF_ANNOT_SQUARE = 5; - -const int FPDF_ANNOT_CIRCLE = 6; - -const int FPDF_ANNOT_POLYGON = 7; - -const int FPDF_ANNOT_POLYLINE = 8; - -const int FPDF_ANNOT_HIGHLIGHT = 9; - -const int FPDF_ANNOT_UNDERLINE = 10; - -const int FPDF_ANNOT_SQUIGGLY = 11; - -const int FPDF_ANNOT_STRIKEOUT = 12; - -const int FPDF_ANNOT_STAMP = 13; - -const int FPDF_ANNOT_CARET = 14; - -const int FPDF_ANNOT_INK = 15; - -const int FPDF_ANNOT_POPUP = 16; - -const int FPDF_ANNOT_FILEATTACHMENT = 17; - -const int FPDF_ANNOT_SOUND = 18; - -const int FPDF_ANNOT_MOVIE = 19; - -const int FPDF_ANNOT_WIDGET = 20; - -const int FPDF_ANNOT_SCREEN = 21; - -const int FPDF_ANNOT_PRINTERMARK = 22; - -const int FPDF_ANNOT_TRAPNET = 23; - -const int FPDF_ANNOT_WATERMARK = 24; - -const int FPDF_ANNOT_THREED = 25; - -const int FPDF_ANNOT_RICHMEDIA = 26; - -const int FPDF_ANNOT_XFAWIDGET = 27; - -const int FPDF_ANNOT_REDACT = 28; - -const int FPDF_ANNOT_FLAG_NONE = 0; - -const int FPDF_ANNOT_FLAG_INVISIBLE = 1; - -const int FPDF_ANNOT_FLAG_HIDDEN = 2; - -const int FPDF_ANNOT_FLAG_PRINT = 4; - -const int FPDF_ANNOT_FLAG_NOZOOM = 8; - -const int FPDF_ANNOT_FLAG_NOROTATE = 16; - -const int FPDF_ANNOT_FLAG_NOVIEW = 32; - -const int FPDF_ANNOT_FLAG_READONLY = 64; - -const int FPDF_ANNOT_FLAG_LOCKED = 128; - -const int FPDF_ANNOT_FLAG_TOGGLENOVIEW = 256; - -const int FPDF_ANNOT_APPEARANCEMODE_NORMAL = 0; - -const int FPDF_ANNOT_APPEARANCEMODE_ROLLOVER = 1; - -const int FPDF_ANNOT_APPEARANCEMODE_DOWN = 2; - -const int FPDF_ANNOT_APPEARANCEMODE_COUNT = 3; - -const int FPDF_FORMFLAG_NONE = 0; - -const int FPDF_FORMFLAG_READONLY = 1; - -const int FPDF_FORMFLAG_REQUIRED = 2; - -const int FPDF_FORMFLAG_NOEXPORT = 4; - -const int FPDF_FORMFLAG_TEXT_MULTILINE = 4096; - -const int FPDF_FORMFLAG_TEXT_PASSWORD = 8192; - -const int FPDF_FORMFLAG_CHOICE_COMBO = 131072; - -const int FPDF_FORMFLAG_CHOICE_EDIT = 262144; - -const int FPDF_FORMFLAG_CHOICE_MULTI_SELECT = 2097152; - -const int FPDF_ANNOT_AACTION_KEY_STROKE = 12; - -const int FPDF_ANNOT_AACTION_FORMAT = 13; - -const int FPDF_ANNOT_AACTION_VALIDATE = 14; - -const int FPDF_ANNOT_AACTION_CALCULATE = 15; - -const int FPDF_MATCHCASE = 1; - -const int FPDF_MATCHWHOLEWORD = 2; - -const int FPDF_CONSECUTIVE = 4; - -const int PDFACTION_UNSUPPORTED = 0; - -const int PDFACTION_GOTO = 1; - -const int PDFACTION_REMOTEGOTO = 2; - -const int PDFACTION_URI = 3; - -const int PDFACTION_LAUNCH = 4; - -const int PDFACTION_EMBEDDEDGOTO = 5; - -const int PDFDEST_VIEW_UNKNOWN_MODE = 0; - -const int PDFDEST_VIEW_XYZ = 1; - -const int PDFDEST_VIEW_FIT = 2; - -const int PDFDEST_VIEW_FITH = 3; - -const int PDFDEST_VIEW_FITV = 4; - -const int PDFDEST_VIEW_FITR = 5; - -const int PDFDEST_VIEW_FITB = 6; - -const int PDFDEST_VIEW_FITBH = 7; - -const int PDFDEST_VIEW_FITBV = 8; - -const int FPDF_COLORSPACE_UNKNOWN = 0; - -const int FPDF_COLORSPACE_DEVICEGRAY = 1; - -const int FPDF_COLORSPACE_DEVICERGB = 2; - -const int FPDF_COLORSPACE_DEVICECMYK = 3; - -const int FPDF_COLORSPACE_CALGRAY = 4; - -const int FPDF_COLORSPACE_CALRGB = 5; - -const int FPDF_COLORSPACE_LAB = 6; - -const int FPDF_COLORSPACE_ICCBASED = 7; - -const int FPDF_COLORSPACE_SEPARATION = 8; - -const int FPDF_COLORSPACE_DEVICEN = 9; - -const int FPDF_COLORSPACE_INDEXED = 10; - -const int FPDF_COLORSPACE_PATTERN = 11; - -const int FPDF_PAGEOBJ_UNKNOWN = 0; - -const int FPDF_PAGEOBJ_TEXT = 1; - -const int FPDF_PAGEOBJ_PATH = 2; - -const int FPDF_PAGEOBJ_IMAGE = 3; - -const int FPDF_PAGEOBJ_SHADING = 4; - -const int FPDF_PAGEOBJ_FORM = 5; - -const int FPDF_SEGMENT_UNKNOWN = -1; - -const int FPDF_SEGMENT_LINETO = 0; - -const int FPDF_SEGMENT_BEZIERTO = 1; - -const int FPDF_SEGMENT_MOVETO = 2; - -const int FPDF_FILLMODE_NONE = 0; - -const int FPDF_FILLMODE_ALTERNATE = 1; - -const int FPDF_FILLMODE_WINDING = 2; - -const int FPDF_FONT_TYPE1 = 1; - -const int FPDF_FONT_TRUETYPE = 2; - -const int FPDF_LINECAP_BUTT = 0; - -const int FPDF_LINECAP_ROUND = 1; - -const int FPDF_LINECAP_PROJECTING_SQUARE = 2; - -const int FPDF_LINEJOIN_MITER = 0; - -const int FPDF_LINEJOIN_ROUND = 1; - -const int FPDF_LINEJOIN_BEVEL = 2; - -const int FPDF_PRINTMODE_EMF = 0; - -const int FPDF_PRINTMODE_TEXTONLY = 1; - -const int FPDF_PRINTMODE_POSTSCRIPT2 = 2; - -const int FPDF_PRINTMODE_POSTSCRIPT3 = 3; - -const int FPDF_PRINTMODE_POSTSCRIPT2_PASSTHROUGH = 4; - -const int FPDF_PRINTMODE_POSTSCRIPT3_PASSTHROUGH = 5; - -const int FPDF_PRINTMODE_EMF_IMAGE_MASKS = 6; - -const int FPDF_PRINTMODE_POSTSCRIPT3_TYPE42 = 7; - -const int FPDF_PRINTMODE_POSTSCRIPT3_TYPE42_PASSTHROUGH = 8; diff --git a/lib/src/pdfium/pdfium_interop.dart b/lib/src/pdfium/pdfium_interop.dart deleted file mode 100644 index 37686168..00000000 --- a/lib/src/pdfium/pdfium_interop.dart +++ /dev/null @@ -1,61 +0,0 @@ -// ignore_for_file: non_constant_identifier_names - -import 'dart:async'; -import 'dart:ffi'; -import 'dart:io'; -import 'dart:typed_data'; - -import 'pdfium_bindings.dart'; - -String _getModuleFileName() { - if (Platform.isAndroid) return 'libpdfrx.so'; - if (Platform.isIOS || Platform.isMacOS) return 'pdfrx.framework/pdfrx'; - if (Platform.isWindows) return 'pdfrx.dll'; - if (Platform.isLinux) { - return '${File(Platform.resolvedExecutable).parent.path}/lib/libpdfrx.so'; - } - throw UnsupportedError('Unsupported platform'); -} - -final interopLib = DynamicLibrary.open(_getModuleFileName()); - -final _pdfrx_file_access_create = interopLib - .lookupFunction( - 'pdfrx_file_access_create', - ); - -final _pdfrx_file_access_destroy = interopLib.lookupFunction( - 'pdfrx_file_access_destroy', -); - -final _pdfrx_file_access_set_value = interopLib.lookupFunction( - 'pdfrx_file_access_set_value', -); - -typedef _NativeFileReadCallable = NativeCallable, IntPtr)>; - -class FileAccess { - FileAccess(int fileSize, FutureOr Function(Uint8List buffer, int position, int size) read) { - void readNative(int param, int position, Pointer buffer, int size) async { - try { - final readSize = await read(buffer.asTypedList(size), position, size); - _pdfrx_file_access_set_value(_fileAccess, readSize); - } catch (e) { - _pdfrx_file_access_set_value(_fileAccess, -1); - } - } - - _nativeCallable = _NativeFileReadCallable.listener(readNative); - _fileAccess = _pdfrx_file_access_create(fileSize, _nativeCallable.nativeFunction.address, 0); - } - - void dispose() { - _pdfrx_file_access_destroy(_fileAccess); - _nativeCallable.close(); - } - - Pointer get fileAccess => Pointer.fromAddress(_fileAccess); - - late final int _fileAccess; - late final _NativeFileReadCallable _nativeCallable; -} diff --git a/lib/src/pdfium/pdfrx_pdfium.dart b/lib/src/pdfium/pdfrx_pdfium.dart deleted file mode 100644 index f532ab17..00000000 --- a/lib/src/pdfium/pdfrx_pdfium.dart +++ /dev/null @@ -1,1010 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -import 'dart:async'; -import 'dart:ffi'; -import 'dart:io'; -import 'dart:ui' as ui; - -import 'package:ffi/ffi.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import '../pdf_api.dart'; -import 'pdf_file_cache.dart'; -import 'pdfium_bindings.dart' as pdfium_bindings; -import 'pdfium_interop.dart'; -import 'worker.dart'; - -/// Get the module file name for pdfium. -String _getModuleFileName() { - if (Pdfrx.pdfiumModulePath != null) return Pdfrx.pdfiumModulePath!; - if (Platform.isAndroid) return 'libpdfium.so'; - if (Platform.isIOS || Platform.isMacOS) return 'pdfrx.framework/pdfrx'; - if (Platform.isWindows) return 'pdfium.dll'; - if (Platform.isLinux) { - return '${File(Platform.resolvedExecutable).parent.path}/lib/libpdfium.so'; - } - throw UnsupportedError('Unsupported platform'); -} - -/// Loaded PDFium module. -final pdfium = pdfium_bindings.pdfium(DynamicLibrary.open(_getModuleFileName())); - -bool _initialized = false; - -/// Initializes PDFium library. -void _init() { - if (_initialized) return; - using((arena) { - final config = arena.allocate(sizeOf()); - config.ref.version = 2; - - if (Pdfrx.fontPaths.isNotEmpty) { - final fontPathArray = arena.allocate>(sizeOf>() * (Pdfrx.fontPaths.length + 1)); - for (int i = 0; i < Pdfrx.fontPaths.length; i++) { - fontPathArray[i] = Pdfrx.fontPaths[i].toUtf8(arena); - } - fontPathArray[Pdfrx.fontPaths.length] = nullptr; - config.ref.m_pUserFontPaths = fontPathArray; - } else { - config.ref.m_pUserFontPaths = nullptr; - } - - config.ref.m_pIsolate = nullptr; - config.ref.m_v8EmbedderSlot = 0; - pdfium.FPDF_InitLibraryWithConfig(config); - }); - _initialized = true; -} - -final backgroundWorker = BackgroundWorker.create(); - -class PdfDocumentFactoryImpl extends PdfDocumentFactory { - @override - Future openAsset( - String name, { - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - }) async { - final data = await rootBundle.load(name); - return await _openData( - data.buffer.asUint8List(), - 'asset:$name', - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - ); - } - - @override - Future openData( - Uint8List data, { - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - String? sourceName, - bool allowDataOwnershipTransfer = false, // just ignored - void Function()? onDispose, - }) => _openData( - data, - sourceName ?? 'memory-${data.hashCode}', - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - onDispose: onDispose, - ); - - @override - Future openFile( - String filePath, { - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - }) { - _init(); - return _openByFunc( - (password) async => (await backgroundWorker).computeWithArena((arena, params) { - final doc = pdfium.FPDF_LoadDocument(params.filePath.toUtf8(arena), params.password?.toUtf8(arena) ?? nullptr); - return doc.address; - }, (filePath: filePath, password: password)), - sourceName: filePath, - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - ); - } - - Future _openData( - Uint8List data, - String sourceName, { - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - int? maxSizeToCacheOnMemory, - void Function()? onDispose, - }) { - return openCustom( - read: (buffer, position, size) { - if (position + size > data.length) { - size = data.length - position; - if (size < 0) return -1; - } - for (int i = 0; i < size; i++) { - buffer[i] = data[position + i]; - } - return size; - }, - fileSize: data.length, - sourceName: sourceName, - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - maxSizeToCacheOnMemory: maxSizeToCacheOnMemory, - onDispose: onDispose, - ); - } - - @override - Future openCustom({ - required FutureOr Function(Uint8List buffer, int position, int size) read, - required int fileSize, - required String sourceName, - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - int? maxSizeToCacheOnMemory, - void Function()? onDispose, - }) async { - _init(); - - maxSizeToCacheOnMemory ??= 1024 * 1024; // the default is 1MB - - // If the file size is smaller than the specified size, load the file on memory - if (fileSize <= maxSizeToCacheOnMemory) { - final buffer = malloc.allocate(fileSize); - try { - await read(buffer.asTypedList(fileSize), 0, fileSize); - return _openByFunc( - (password) async => (await backgroundWorker).computeWithArena( - (arena, params) => - pdfium.FPDF_LoadMemDocument( - Pointer.fromAddress(params.buffer), - params.fileSize, - params.password?.toUtf8(arena) ?? nullptr, - ).address, - (buffer: buffer.address, fileSize: fileSize, password: password), - ), - sourceName: sourceName, - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - disposeCallback: () { - try { - onDispose?.call(); - } finally { - malloc.free(buffer); - } - }, - ); - } catch (e) { - malloc.free(buffer); - rethrow; - } - } - - // Otherwise, load the file on demand - final fa = FileAccess(fileSize, read); - try { - return _openByFunc( - (password) async => (await backgroundWorker).computeWithArena( - (arena, params) => - pdfium.FPDF_LoadCustomDocument( - Pointer.fromAddress(params.fileAccess), - params.password?.toUtf8(arena) ?? nullptr, - ).address, - (fileAccess: fa.fileAccess.address, password: password), - ), - sourceName: sourceName, - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - disposeCallback: () { - try { - onDispose?.call(); - } finally { - fa.dispose(); - } - }, - ); - } catch (e) { - fa.dispose(); - rethrow; - } - } - - @override - Future openUri( - Uri uri, { - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - PdfDownloadProgressCallback? progressCallback, - PdfDownloadReportCallback? reportCallback, - bool preferRangeAccess = false, - Map? headers, - bool withCredentials = false, - }) => pdfDocumentFromUri( - uri, - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - progressCallback: progressCallback, - reportCallback: reportCallback, - useRangeAccess: preferRangeAccess, - headers: headers, - ); - - static bool _isPasswordError({int? error}) { - if (Platform.isWindows) { - // FIXME: Windows does not return error code correctly - // And we have to mimic every error is password error - return true; - } - error ??= pdfium.FPDF_GetLastError(); - return error == pdfium_bindings.FPDF_ERR_PASSWORD; - } - - static Future _openByFunc( - FutureOr Function(String? password) openPdfDocument, { - required String sourceName, - required PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - void Function()? disposeCallback, - }) async { - for (int i = 0; ; i++) { - final String? password; - if (firstAttemptByEmptyPassword && i == 0) { - password = null; - } else { - password = await passwordProvider?.call(); - if (password == null) { - throw const PdfPasswordException('No password supplied by PasswordProvider.'); - } - } - final doc = await openPdfDocument(password); - if (doc != 0) { - return PdfDocumentPdfium.fromPdfDocument( - pdfium_bindings.FPDF_DOCUMENT.fromAddress(doc), - sourceName: sourceName, - disposeCallback: disposeCallback, - ); - } - if (_isPasswordError()) { - continue; - } - throw PdfException('Failed to load PDF document (FPDF_GetLastError=${pdfium.FPDF_GetLastError()}).'); - } - } -} - -extension FpdfUtf8StringExt on String { - Pointer toUtf8(Arena arena) => Pointer.fromAddress(toNativeUtf8(allocator: arena).address); -} - -class PdfDocumentPdfium extends PdfDocument { - final pdfium_bindings.FPDF_DOCUMENT document; - final void Function()? disposeCallback; - final int securityHandlerRevision; - final pdfium_bindings.FPDF_FORMHANDLE formHandle; - final Pointer formInfo; - bool isDisposed = false; - - @override - bool get isEncrypted => securityHandlerRevision != -1; - @override - final PdfPermissions? permissions; - - PdfDocumentPdfium._( - this.document, { - required super.sourceName, - required this.securityHandlerRevision, - required this.permissions, - required this.formHandle, - required this.formInfo, - this.disposeCallback, - }); - - static Future fromPdfDocument( - pdfium_bindings.FPDF_DOCUMENT doc, { - required String sourceName, - void Function()? disposeCallback, - }) async { - if (doc == nullptr) { - throw const PdfException('Failed to load PDF document.'); - } - PdfDocumentPdfium? pdfDoc; - try { - final result = await (await backgroundWorker).compute((docAddress) { - final doc = pdfium_bindings.FPDF_DOCUMENT.fromAddress(docAddress); - return using((arena) { - Pointer formInfo = nullptr; - pdfium_bindings.FPDF_FORMHANDLE formHandle = nullptr; - try { - final pageCount = pdfium.FPDF_GetPageCount(doc); - final permissions = pdfium.FPDF_GetDocPermissions(doc); - final securityHandlerRevision = pdfium.FPDF_GetSecurityHandlerRevision(doc); - - formInfo = calloc.allocate(sizeOf()); - formInfo.ref.version = 1; - formHandle = pdfium.FPDFDOC_InitFormFillEnvironment(doc, formInfo); - - final pages = <({double width, double height, int rotation})>[]; - for (int i = 0; i < pageCount; i++) { - final page = pdfium.FPDF_LoadPage(doc, i); - try { - pages.add(( - width: pdfium.FPDF_GetPageWidthF(page), - height: pdfium.FPDF_GetPageHeightF(page), - rotation: pdfium.FPDFPage_GetRotation(page), - )); - } finally { - pdfium.FPDF_ClosePage(page); - } - } - - return ( - permissions: permissions, - securityHandlerRevision: securityHandlerRevision, - pages: pages, - formHandle: formHandle.address, - formInfo: formInfo.address, - ); - } catch (e) { - pdfium.FPDFDOC_ExitFormFillEnvironment(formHandle); - calloc.free(formInfo); - rethrow; - } - }); - }, doc.address); - - pdfDoc = PdfDocumentPdfium._( - doc, - sourceName: sourceName, - securityHandlerRevision: result.securityHandlerRevision, - permissions: - result.securityHandlerRevision != -1 - ? PdfPermissions(result.permissions, result.securityHandlerRevision) - : null, - formHandle: pdfium_bindings.FPDF_FORMHANDLE.fromAddress(result.formHandle), - formInfo: Pointer.fromAddress(result.formInfo), - disposeCallback: disposeCallback, - ); - - final pages = []; - for (int i = 0; i < result.pages.length; i++) { - final pageData = result.pages[i]; - pages.add( - PdfPagePdfium._( - document: pdfDoc, - pageNumber: i + 1, - width: pageData.width, - height: pageData.height, - rotation: PdfPageRotation.values[pageData.rotation], - ), - ); - } - pdfDoc.pages = List.unmodifiable(pages); - return pdfDoc; - } catch (e) { - pdfDoc?.dispose(); - rethrow; - } - } - - @override - late final List pages; - - @override - bool isIdenticalDocumentHandle(Object? other) => - other is PdfDocumentPdfium && document.address == other.document.address; - - @override - Future dispose() async { - if (!isDisposed) { - isDisposed = true; - await (await backgroundWorker).compute((params) { - final formHandle = pdfium_bindings.FPDF_FORMHANDLE.fromAddress(params.formHandle); - final formInfo = Pointer.fromAddress(params.formInfo); - pdfium.FPDFDOC_ExitFormFillEnvironment(formHandle); - calloc.free(formInfo); - - final doc = pdfium_bindings.FPDF_DOCUMENT.fromAddress(params.document); - pdfium.FPDF_CloseDocument(doc); - }, (formHandle: formHandle.address, formInfo: formInfo.address, document: document.address)); - - disposeCallback?.call(); - } - } - - @override - Future> loadOutline() async => - isDisposed - ? [] - : await (await backgroundWorker).compute( - (params) => using((arena) { - final document = pdfium_bindings.FPDF_DOCUMENT.fromAddress(params.document); - return _getOutlineNodeSiblings(pdfium.FPDFBookmark_GetFirstChild(document, nullptr), document, arena); - }), - (document: document.address), - ); - - static List _getOutlineNodeSiblings( - pdfium_bindings.FPDF_BOOKMARK bookmark, - pdfium_bindings.FPDF_DOCUMENT document, - Arena arena, - ) { - final siblings = []; - while (bookmark != nullptr) { - final titleBufSize = pdfium.FPDFBookmark_GetTitle(bookmark, nullptr, 0); - final titleBuf = arena.allocate(titleBufSize); - pdfium.FPDFBookmark_GetTitle(bookmark, titleBuf, titleBufSize); - siblings.add( - PdfOutlineNode( - title: titleBuf.cast().toDartString(), - dest: _pdfDestFromDest(pdfium.FPDFBookmark_GetDest(document, bookmark), document, arena), - children: _getOutlineNodeSiblings(pdfium.FPDFBookmark_GetFirstChild(document, bookmark), document, arena), - ), - ); - bookmark = pdfium.FPDFBookmark_GetNextSibling(document, bookmark); - } - return siblings; - } -} - -class PdfPagePdfium extends PdfPage { - @override - final PdfDocumentPdfium document; - @override - final int pageNumber; - @override - final double width; - @override - final double height; - - @override - final PdfPageRotation rotation; - - PdfPagePdfium._({ - required this.document, - required this.pageNumber, - required this.width, - required this.height, - required this.rotation, - }); - - @override - Future render({ - int x = 0, - int y = 0, - int? width, - int? height, - double? fullWidth, - double? fullHeight, - Color? backgroundColor, - PdfAnnotationRenderingMode annotationRenderingMode = PdfAnnotationRenderingMode.annotationAndForms, - PdfPageRenderCancellationToken? cancellationToken, - }) async { - if (cancellationToken != null && cancellationToken is! PdfPageRenderCancellationTokenPdfium) { - throw ArgumentError( - 'cancellationToken must be created by PdfPage.createCancellationToken().', - 'cancellationToken', - ); - } - final ct = cancellationToken as PdfPageRenderCancellationTokenPdfium?; - - fullWidth ??= this.width; - fullHeight ??= this.height; - width ??= fullWidth.toInt(); - height ??= fullHeight.toInt(); - backgroundColor ??= Colors.white; - const rgbaSize = 4; - Pointer buffer = nullptr; - try { - buffer = malloc.allocate(width * height * rgbaSize); - final isSucceeded = await using((arena) async { - final cancelFlag = arena.allocate(sizeOf()); - ct?.attach(cancelFlag); - - if (cancelFlag.value || document.isDisposed) return false; - return await (await backgroundWorker).compute( - (params) { - final cancelFlag = Pointer.fromAddress(params.cancelFlag); - if (cancelFlag.value) return false; - final bmp = pdfium.FPDFBitmap_CreateEx( - params.width, - params.height, - pdfium_bindings.FPDFBitmap_BGRA, - Pointer.fromAddress(params.buffer), - params.width * rgbaSize, - ); - if (bmp == nullptr) { - throw PdfException('FPDFBitmap_CreateEx(${params.width}, ${params.height}) failed.'); - } - pdfium_bindings.FPDF_PAGE page = nullptr; - try { - final doc = pdfium_bindings.FPDF_DOCUMENT.fromAddress(params.document); - page = pdfium.FPDF_LoadPage(doc, params.pageNumber - 1); - if (page == nullptr) { - throw PdfException('FPDF_LoadPage(${params.pageNumber}) failed.'); - } - pdfium.FPDFBitmap_FillRect(bmp, 0, 0, params.width, params.height, params.backgroundColor); - pdfium.FPDF_RenderPageBitmap( - bmp, - page, - -params.x, - -params.y, - params.fullWidth, - params.fullHeight, - 0, - params.annotationRenderingMode != PdfAnnotationRenderingMode.none ? pdfium_bindings.FPDF_ANNOT : 0, - ); - - if (params.formHandle != 0 && - params.annotationRenderingMode == PdfAnnotationRenderingMode.annotationAndForms) { - pdfium.FPDF_FFLDraw( - pdfium_bindings.FPDF_FORMHANDLE.fromAddress(params.formHandle), - bmp, - page, - -params.x, - -params.y, - params.fullWidth, - params.fullHeight, - 0, - 0, - ); - } - return true; - } finally { - pdfium.FPDF_ClosePage(page); - pdfium.FPDFBitmap_Destroy(bmp); - } - }, - ( - document: document.document.address, - pageNumber: pageNumber, - buffer: buffer.address, - x: x, - y: y, - width: width!, - height: height!, - fullWidth: fullWidth!.toInt(), - fullHeight: fullHeight!.toInt(), - backgroundColor: backgroundColor!.toARGB32(), - annotationRenderingMode: annotationRenderingMode, - formHandle: document.formHandle.address, - formInfo: document.formInfo.address, - cancelFlag: cancelFlag.address, - ), - ); - }); - - if (!isSucceeded) { - return null; - } - - final resultBuffer = buffer; - buffer = nullptr; - return PdfImagePdfium._(width: width, height: height, buffer: resultBuffer); - } catch (e) { - return null; - } finally { - malloc.free(buffer); - ct?.detach(); - } - } - - @override - PdfPageRenderCancellationTokenPdfium createCancellationToken() => PdfPageRenderCancellationTokenPdfium(this); - - @override - Future loadText() => PdfPageTextPdfium._loadText(this); - - @override - Future> loadLinks({bool compact = false}) async { - final links = await _loadAnnotLinks() + await _loadLinks(); - if (compact) { - for (int i = 0; i < links.length; i++) { - links[i] = links[i].compact(); - } - } - return List.unmodifiable(links); - } - - Future> _loadLinks() async => - document.isDisposed - ? [] - : await (await backgroundWorker).compute((params) { - pdfium_bindings.FPDF_PAGE page = nullptr; - pdfium_bindings.FPDF_TEXTPAGE textPage = nullptr; - pdfium_bindings.FPDF_PAGELINK linkPage = nullptr; - try { - final document = pdfium_bindings.FPDF_DOCUMENT.fromAddress(params.document); - page = pdfium.FPDF_LoadPage(document, params.pageNumber - 1); - textPage = pdfium.FPDFText_LoadPage(page); - if (textPage == nullptr) return []; - linkPage = pdfium.FPDFLink_LoadWebLinks(textPage); - if (linkPage == nullptr) return []; - - final doubleSize = sizeOf(); - return using((arena) { - final rectBuffer = arena.allocate(4 * doubleSize); - return List.generate(pdfium.FPDFLink_CountWebLinks(linkPage), (index) { - final rects = List.generate(pdfium.FPDFLink_CountRects(linkPage, index), (rectIndex) { - pdfium.FPDFLink_GetRect( - linkPage, - index, - rectIndex, - rectBuffer, - rectBuffer.offset(doubleSize), - rectBuffer.offset(doubleSize * 2), - rectBuffer.offset(doubleSize * 3), - ); - return _rectFromLTRBBuffer(rectBuffer); - }); - return PdfLink(rects, url: Uri.parse(_getLinkUrl(linkPage, index, arena))); - }); - }); - } finally { - pdfium.FPDFLink_CloseWebLinks(linkPage); - pdfium.FPDFText_ClosePage(textPage); - pdfium.FPDF_ClosePage(page); - } - }, (document: document.document.address, pageNumber: pageNumber)); - - static String _getLinkUrl(pdfium_bindings.FPDF_PAGELINK linkPage, int linkIndex, Arena arena) { - final urlLength = pdfium.FPDFLink_GetURL(linkPage, linkIndex, nullptr, 0); - final urlBuffer = arena.allocate(urlLength * sizeOf()); - pdfium.FPDFLink_GetURL(linkPage, linkIndex, urlBuffer, urlLength); - return urlBuffer.cast().toDartString(); - } - - Future> _loadAnnotLinks() async => - document.isDisposed - ? [] - : await (await backgroundWorker).compute( - (params) => using((arena) { - final document = pdfium_bindings.FPDF_DOCUMENT.fromAddress(params.document); - final page = pdfium.FPDF_LoadPage(document, params.pageNumber - 1); - try { - final count = pdfium.FPDFPage_GetAnnotCount(page); - final rectf = arena.allocate(sizeOf()); - final links = []; - for (int i = 0; i < count; i++) { - final annot = pdfium.FPDFPage_GetAnnot(page, i); - pdfium.FPDFAnnot_GetRect(annot, rectf); - final r = rectf.ref; - final rect = PdfRect( - r.left, - r.top > r.bottom ? r.top : r.bottom, - r.right, - r.top > r.bottom ? r.bottom : r.top, - ); - final dest = _processAnnotDest(annot, document, arena); - if (dest != nullptr) { - links.add(PdfLink([rect], dest: _pdfDestFromDest(dest, document, arena))); - } else { - final uri = _processAnnotLink(annot, document, arena); - if (uri != null) { - links.add(PdfLink([rect], url: uri)); - } - } - pdfium.FPDFPage_CloseAnnot(annot); - } - return links; - } finally { - pdfium.FPDF_ClosePage(page); - } - }), - (document: document.document.address, pageNumber: pageNumber), - ); - - static pdfium_bindings.FPDF_DEST _processAnnotDest( - pdfium_bindings.FPDF_ANNOTATION annot, - pdfium_bindings.FPDF_DOCUMENT document, - Arena arena, - ) { - final link = pdfium.FPDFAnnot_GetLink(annot); - - // firstly check the direct dest - final dest = pdfium.FPDFLink_GetDest(document, link); - if (dest != nullptr) return dest; - - final action = pdfium.FPDFLink_GetAction(link); - if (action == nullptr) return nullptr; - switch (pdfium.FPDFAction_GetType(action)) { - case pdfium_bindings.PDFACTION_GOTO: - return pdfium.FPDFAction_GetDest(document, action); - default: - return nullptr; - } - } - - static Uri? _processAnnotLink( - pdfium_bindings.FPDF_ANNOTATION annot, - pdfium_bindings.FPDF_DOCUMENT document, - Arena arena, - ) { - final link = pdfium.FPDFAnnot_GetLink(annot); - final action = pdfium.FPDFLink_GetAction(link); - if (action == nullptr) return null; - switch (pdfium.FPDFAction_GetType(action)) { - case pdfium_bindings.PDFACTION_URI: - final size = pdfium.FPDFAction_GetURIPath(document, action, nullptr, 0); - final buffer = arena.allocate(size); - pdfium.FPDFAction_GetURIPath(document, action, buffer.cast(), size); - try { - final String newBuffer = buffer.toDartString(); - return Uri.parse(newBuffer); - } catch (e) { - return null; - } - default: - return null; - } - } -} - -class PdfPageRenderCancellationTokenPdfium extends PdfPageRenderCancellationToken { - PdfPageRenderCancellationTokenPdfium(this.page); - final PdfPage page; - Pointer? _cancelFlag; - bool _canceled = false; - - @override - bool get isCanceled => _canceled; - - void attach(Pointer pointer) { - _cancelFlag = pointer; - if (_canceled) { - _cancelFlag!.value = true; - } - } - - void detach() { - _cancelFlag = null; - } - - @override - Future cancel() async { - _canceled = true; - _cancelFlag?.value = true; - } -} - -class PdfImagePdfium extends PdfImage { - @override - final int width; - @override - final int height; - @override - ui.PixelFormat get format => ui.PixelFormat.bgra8888; - @override - Uint8List get pixels => _buffer.asTypedList(width * height * 4); - - final Pointer _buffer; - - PdfImagePdfium._({required this.width, required this.height, required Pointer buffer}) : _buffer = buffer; - - @override - void dispose() { - malloc.free(_buffer); - } -} - -@immutable -class PdfPageTextFragmentPdfium implements PdfPageTextFragment { - const PdfPageTextFragmentPdfium(this.pageText, this.index, this.length, this.bounds, this.charRects); - - final PdfPageText pageText; - - @override - final int index; - @override - final int length; - @override - int get end => index + length; - @override - final PdfRect bounds; - @override - final List? charRects; - @override - String get text => pageText.fullText.substring(index, index + length); -} - -class PdfPageTextPdfium extends PdfPageText { - PdfPageTextPdfium({required this.pageNumber, required this.fullText, required this.fragments}); - - @override - final int pageNumber; - - @override - final String fullText; - @override - final List fragments; - - static Future _loadText(PdfPagePdfium page) async { - final result = await _load(page); - final pageText = PdfPageTextPdfium(pageNumber: page.pageNumber, fullText: result.fullText, fragments: []); - int pos = 0; - for (final fragment in result.fragments) { - final charRects = result.charRects.sublist(pos, pos + fragment); - pageText.fragments.add(PdfPageTextFragmentPdfium(pageText, pos, fragment, charRects.boundingRect(), charRects)); - pos += fragment; - } - return pageText; - } - - static Future<({String fullText, List charRects, List fragments})> _load(PdfPagePdfium page) async { - if (page.document.isDisposed) { - return (fullText: '', charRects: [], fragments: []); - } - return await (await backgroundWorker).compute( - (params) => using((arena) { - final doc = pdfium_bindings.FPDF_DOCUMENT.fromAddress(params.docHandle); - final pdfium_bindings.FPDF_PAGE page = pdfium.FPDF_LoadPage(doc, params.pageNumber - 1); - - final textPage = pdfium.FPDFText_LoadPage(page); - try { - final charCount = pdfium.FPDFText_CountChars(textPage); - final charRects = []; - final fragments = []; - final fullText = _loadInternal(textPage, 0, charCount, arena, charRects, fragments); - return (fullText: fullText, charRects: charRects, fragments: fragments); - } finally { - pdfium.FPDFText_ClosePage(textPage); - pdfium.FPDF_ClosePage(page); - } - }), - (docHandle: page.document.document.address, pageNumber: page.pageNumber), - ); - } - - static const _charLF = 10, _charCR = 13, _charSpace = 32; - - static String _loadInternal( - pdfium_bindings.FPDF_TEXTPAGE textPage, - int from, - int length, - Arena arena, - List charRects, - List fragments, - ) { - final fullText = _getText(textPage, from, length, arena); - final doubleSize = sizeOf(); - final buffer = arena.allocate(4 * doubleSize); - final sb = StringBuffer(); - int lineStart = 0, wordStart = 0; - int? lastChar; - for (int i = 0; i < length; i++) { - final char = fullText.codeUnitAt(from + i); - if (char == _charCR) { - if (i + 1 < length && fullText.codeUnitAt(from + i + 1) == _charLF) { - lastChar = char; - continue; - } - } - if (char == _charCR || char == _charLF) { - if (_makeLineFlat(charRects, lineStart, sb.length, sb)) { - sb.write('\r\n'); - charRects.appendDummy(); - charRects.appendDummy(); - fragments.add(sb.length - wordStart); - lineStart = wordStart = sb.length; - } - lastChar = char; - continue; - } - - pdfium.FPDFText_GetCharBox( - textPage, - from + i, - buffer, // L - buffer.offset(doubleSize * 2), // R - buffer.offset(doubleSize * 3), // B - buffer.offset(doubleSize), // T - ); - final rect = _rectFromLTRBBuffer(buffer); - if (char == _charSpace) { - if (lastChar == _charSpace) continue; - if (sb.length > wordStart) { - fragments.add(sb.length - wordStart); - } - sb.writeCharCode(char); - charRects.add(rect); - fragments.add(1); - wordStart = sb.length; - lastChar = char; - continue; - } - - if (sb.length > lineStart) { - const columnHeightThreshold = 72.0; // 1 inch - final prev = charRects.last; - if (prev.left > rect.left || prev.bottom + columnHeightThreshold < rect.bottom) { - if (_makeLineFlat(charRects, lineStart, sb.length, sb)) { - if (sb.length > wordStart) { - fragments.add(sb.length - wordStart); - } - lineStart = wordStart = sb.length; - } - } - } - - sb.writeCharCode(char); - charRects.add(rect); - lastChar = char; - } - - if (_makeLineFlat(charRects, lineStart, sb.length, sb)) { - if (sb.length > wordStart) { - fragments.add(sb.length - wordStart); - } - } - return sb.toString(); - } - - static String escapeString(String s) { - final sb = StringBuffer(); - for (int i = 0; i < s.length; i++) { - final char = s.codeUnitAt(i); - if (char >= 0x20 && char < 0x7f) { - sb.writeCharCode(char); - } else { - sb.write('\\u{${char.toRadixString(16).padLeft(4, '0')}}'); - } - } - return sb.toString(); - } - - /// return true if any meaningful characters in the line (start -> end) - static bool _makeLineFlat(List rects, int start, int end, StringBuffer sb) { - if (start >= end) return false; - final str = sb.toString(); - final bounds = rects.skip(start).take(end - start).boundingRect(); - double? prev; - for (int i = start; i < end; i++) { - final rect = rects[i]; - final char = str.codeUnitAt(i); - if (char == _charSpace) { - final next = i + 1 < end ? rects[i + 1].left : null; - rects[i] = PdfRect(prev ?? rect.left, bounds.top, next ?? rect.right, bounds.bottom); - prev = null; - } else { - rects[i] = PdfRect(prev ?? rect.left, bounds.top, rect.right, bounds.bottom); - prev = rect.right; - } - } - return true; - } - - static String _getText(pdfium_bindings.FPDF_TEXTPAGE textPage, int from, int length, Arena arena) { - final buffer = arena.allocate((length + 1) * sizeOf()); - pdfium.FPDFText_GetText(textPage, from, length, buffer.cast()); - return String.fromCharCodes(buffer.asTypedList(length)); - } -} - -PdfRect _rectFromLTRBBuffer(Pointer buffer) => PdfRect(buffer[0], buffer[1], buffer[2], buffer[3]); - -extension _PointerExt on Pointer { - Pointer offset(int offsetInBytes) => Pointer.fromAddress(address + offsetInBytes); -} - -extension _PdfRectsExt on List { - /// add dummy rect for control characters - void appendDummy({double width = 1}) { - if (isEmpty) return; - final prev = last; - add(PdfRect(prev.right, prev.top, prev.right + width, prev.bottom)); - } -} - -PdfDest? _pdfDestFromDest(pdfium_bindings.FPDF_DEST dest, pdfium_bindings.FPDF_DOCUMENT document, Arena arena) { - if (dest == nullptr) return null; - final pul = arena.allocate(sizeOf()); - final values = arena.allocate(sizeOf() * 4); - final pageIndex = pdfium.FPDFDest_GetDestPageIndex(document, dest); - final type = pdfium.FPDFDest_GetView(dest, pul, values); - if (type != 0) { - return PdfDest(pageIndex + 1, PdfDestCommand.values[type], List.generate(pul.value, (index) => values[index])); - } - return null; -} diff --git a/lib/src/pdfium/worker.dart b/lib/src/pdfium/worker.dart deleted file mode 100644 index 3dee429f..00000000 --- a/lib/src/pdfium/worker.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'dart:async'; -import 'dart:isolate'; - -import 'package:ffi/ffi.dart'; -import 'package:flutter/foundation.dart'; - -import '../../pdfrx.dart'; - -/// Background worker based on Dart [Isolate]. -class BackgroundWorker { - BackgroundWorker._(this._receivePort, this._sendPort); - final ReceivePort _receivePort; - final SendPort _sendPort; - bool _isDisposed = false; - - static Future create({String? debugName}) async { - final receivePort = ReceivePort(); - await Isolate.spawn(_workerEntry, receivePort.sendPort, debugName: debugName); - final worker = BackgroundWorker._(receivePort, await receivePort.first as SendPort); - - // propagate the pdfium module path to the worker - worker.compute((params) { - Pdfrx.pdfiumModulePath = params.modulePath; - }, (modulePath: Pdfrx.pdfiumModulePath)); - - return worker; - } - - static void _workerEntry(SendPort sendPort) { - final receivePort = ReceivePort(); - sendPort.send(receivePort.sendPort); - late final StreamSubscription sub; - sub = receivePort.listen((message) { - if (message is _ComputeParams) { - message.execute(); - } else { - sub.cancel(); - receivePort.close(); - return; - } - }); - } - - Future compute(ComputeCallback callback, M message) async { - if (_isDisposed) { - throw StateError('Worker is already disposed'); - } - final sendPort = ReceivePort(); - _sendPort.send(_ComputeParams(sendPort.sendPort, callback, message)); - return await sendPort.first as R; - } - - /// [compute] wrapper that also provides [Arena] for temporary memory allocation. - Future computeWithArena(R Function(Arena arena, M message) callback, M message) => - compute((message) => using((arena) => callback(arena, message)), message); - - void dispose() { - try { - _isDisposed = true; - _sendPort.send(null); - _receivePort.close(); - } catch (e) { - debugPrint('Failed to dispose worker (possible double-dispose?): $e'); - } - } -} - -class _ComputeParams { - _ComputeParams(this.sendPort, this.callback, this.message); - final SendPort sendPort; - final ComputeCallback callback; - final M message; - - void execute() => sendPort.send(callback(message)); -} diff --git a/lib/src/utils/native/native.dart b/lib/src/utils/native/native.dart deleted file mode 100644 index 5780cf08..00000000 --- a/lib/src/utils/native/native.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/services.dart'; - -final isApple = Platform.isMacOS || Platform.isIOS; -final isWindows = Platform.isWindows; - -/// Key pressing state of ⌘ or Control depending on the platform. -bool get isCommandKeyPressed => - isApple ? HardwareKeyboard.instance.isMetaPressed : HardwareKeyboard.instance.isControlPressed; diff --git a/lib/src/utils/web/web.dart b/lib/src/utils/web/web.dart deleted file mode 100644 index 5696510c..00000000 --- a/lib/src/utils/web/web.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:flutter/services.dart'; - -final isApple = false; -final isWindows = false; - -/// Key pressing state of ⌘ or Control depending on the platform. -bool get isCommandKeyPressed => HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed; diff --git a/lib/src/web/pdfjs.dart b/lib/src/web/pdfjs.dart deleted file mode 100644 index 27df102a..00000000 --- a/lib/src/web/pdfjs.dart +++ /dev/null @@ -1,386 +0,0 @@ -// ignore_for_file: avoid_web_libraries_in_flutter - -@JS() -library; - -import 'dart:async'; -import 'dart:js_interop'; -import 'dart:typed_data'; - -import 'package:flutter/foundation.dart'; -import 'package:synchronized/extension.dart'; -import 'package:web/web.dart' as web; - -import '../../pdfrx.dart'; -import 'js_utils.dart'; - -/// Default pdf.js version -const _pdfjsVersion = '4.10.38'; - -/// Default pdf.js URL -const _pdfjsUrl = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@$_pdfjsVersion/build/pdf.min.mjs'; - -/// Default pdf.worker.js URL -const _pdfjsWorkerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@$_pdfjsVersion/build/pdf.worker.min.mjs'; - -/// Default CMap URL -const _pdfjsCMapUrl = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@$_pdfjsVersion/cmaps/'; - -@JS('pdfjsLib') -external JSAny? get _pdfjsLib; - -bool get _isPdfjsLoaded => _pdfjsLib != null; - -@JS('pdfjsLib.getDocument') -external _PDFDocumentLoadingTask _pdfjsGetDocument(_PdfjsDocumentInitParameters data); - -extension type _PdfjsDocumentInitParameters._(JSObject _) implements JSObject { - external _PdfjsDocumentInitParameters({ - String? url, - JSArrayBuffer? data, - JSAny? httpHeaders, - bool? withCredentials, - String? password, - String? cMapUrl, - bool? cMapPacked, - bool? useSystemFonts, - String? standardFontDataUrl, - }); - - external String? get url; - external JSArrayBuffer? get data; - external JSAny? get httpHeaders; - external bool? get withCredentials; - external String? get password; - external String? get cMapUrl; - external bool? get cMapPacked; - external bool? get useSystemFonts; - external String? get standardFontDataUrl; -} - -@JS('pdfjsLib.GlobalWorkerOptions.workerSrc') -external set _pdfjsWorkerSrc(String src); - -extension type _PDFDocumentLoadingTask(JSObject _) implements JSObject { - external JSPromise get promise; -} - -Future pdfjsGetDocument( - String url, { - String? password, - Map? headers, - bool withCredentials = false, -}) => - _pdfjsGetDocument( - _PdfjsDocumentInitParameters( - url: url, - password: password, - httpHeaders: headers?.jsify(), - withCredentials: withCredentials, - cMapUrl: PdfJsConfiguration.configuration?.cMapUrl ?? _pdfjsCMapUrl, - cMapPacked: PdfJsConfiguration.configuration?.cMapPacked ?? true, - useSystemFonts: PdfJsConfiguration.configuration?.useSystemFonts, - standardFontDataUrl: PdfJsConfiguration.configuration?.standardFontDataUrl, - ), - ).promise.toDart; - -/// [allowDataOwnershipTransfer] is used to determine if the data buffer can be transferred to the worker thread. -Future pdfjsGetDocumentFromData( - ByteBuffer data, { - String? password, - bool allowDataOwnershipTransfer = false, -}) async { - if (!allowDataOwnershipTransfer) { - // We may need to duplicate the buffer if it is "technically transferrable". - if (data.isTechnicallyTransferrable) { - data = data.duplicate(); - } - } - - final result = - await _pdfjsGetDocument( - _PdfjsDocumentInitParameters( - data: data.toJS, - password: password, - cMapUrl: PdfJsConfiguration.configuration?.cMapUrl ?? _pdfjsCMapUrl, - cMapPacked: PdfJsConfiguration.configuration?.cMapPacked ?? true, - useSystemFonts: PdfJsConfiguration.configuration?.useSystemFonts, - standardFontDataUrl: PdfJsConfiguration.configuration?.standardFontDataUrl, - ), - ).promise.toDart; - return result; -} - -extension _ByteBufferExtensions on ByteBuffer { - bool get isTechnicallyTransferrable { - if (!kIsWeb) throw UnsupportedError('This method is only available on web'); - // if NOT running with Flutter WASM runtime, every ByteBuffer can be transferrable - if (!kIsWasm) return true; - // if running with Flutter WASM runtime, only JSArrayBufferImpl can be transferrable (I believe) - return runtimeType.toString() == 'JSArrayBufferImpl'; - } - - ByteBuffer duplicate() => Uint8List.fromList(asUint8List()).buffer; -} - -extension type PdfjsDocument._(JSObject _) implements JSObject { - external JSPromise getPage(int pageNumber); - external JSPromise?> getPermissions(); - external int get numPages; - external void destroy(); - - external JSPromise getPageIndex(PdfjsRef ref); - external JSPromise getDestination(String id); - external JSPromise?> getOutline(); -} - -extension type PdfjsPage._(JSObject _) implements JSObject { - external PdfjsViewport getViewport(PdfjsViewportParams params); - external PdfjsRender render(PdfjsRenderContext params); - external int get pageNumber; - external int get rotate; - external JSNumber get userUnit; - external JSArray get view; - - external JSPromise getTextContent(PdfjsGetTextContentParameters params); - external ReadableStream streamTextContent(PdfjsGetTextContentParameters params); - - external JSPromise> getAnnotations(PdfjsGetAnnotationsParameters params); -} - -extension type PdfjsAnnotation._(JSObject _) implements JSObject { - external String get subtype; - external int get annotationType; - external JSArray get rect; - external String? get url; - external String? get unsafeUrl; - external int get annotationFlags; - external JSAny? get dest; -} - -extension type PdfjsViewportParams._(JSObject _) implements JSObject { - external PdfjsViewportParams({ - double scale, - int rotation, // 0, 90, 180, 270 - double offsetX, - double offsetY, - bool dontFlip, - }); - - external double scale; - external int rotation; - external double offsetX; - external double offsetY; - external bool dontFlip; -} - -extension type PdfjsViewport(JSObject _) implements JSObject { - external JSArray viewBox; - - external double scale; - - /// 0, 90, 180, 270 - external int rotation; - external double offsetX; - external double offsetY; - external bool dontFlip; - - external double width; - external double height; - - external JSArray? transform; -} - -extension type PdfjsRenderContext._(JSObject _) implements JSObject { - external PdfjsRenderContext({ - required web.CanvasRenderingContext2D canvasContext, - required PdfjsViewport viewport, - String intent, - int annotationMode, - bool renderInteractiveForms, - JSArray? transform, - JSObject imageLayer, - JSObject canvasFactory, - JSObject background, - }); - - external web.CanvasRenderingContext2D canvasContext; - external PdfjsViewport viewport; - - /// `display` or `print` - external String intent; - - /// DISABLE=0, ENABLE=1, ENABLE_FORMS=2, ENABLE_STORAGE=3 - external int annotationMode; - external bool renderInteractiveForms; - external JSArray? transform; - external JSObject imageLayer; - external JSObject canvasFactory; - external JSObject background; -} - -extension type PdfjsRender._(JSObject _) implements JSObject { - external JSPromise get promise; -} - -extension type PdfjsGetTextContentParameters._(JSObject _) implements JSObject { - external PdfjsGetTextContentParameters({bool includeMarkedContent, bool disableNormalization}); - - external bool includeMarkedContent; - external bool disableNormalization; -} - -extension type PdfjsTextContent._(JSObject _) implements JSObject { - /// Either [PdfjsTextItem] or [PdfjsTextMarkedContent] - external JSArray get items; - external JSObject get styles; -} - -extension type PdfjsTextItem._(JSObject _) implements JSObject { - external String get str; - - /// Text direction: `ttb`, `ltr` or `rtl`. - external String get dir; - - /// Matrix for transformation, in the form `[a, b, c, d, e, f]`, equivalent to: - /// ``` - /// | a b 0 | - /// | c d 0 | - /// | e f 1 | - /// ``` - /// - /// Translation is performed with `[1, 0, 0, 1, tx, ty]`. - /// - /// Scaling is performed with `[sx, 0, 0, sy, 0, 0]`. - /// - /// See PDF Reference 1.7, 4.2.2 Common Transformations for more. - external JSArray get transform; - external num get width; - external num get height; - external String get fontName; - external bool get hasEOL; -} - -extension type PdfjsTextMarkedContent._(JSObject _) implements JSObject { - external String get type; - external String get id; -} - -extension type PdfjsTextStyle._(JSObject _) implements JSObject { - external num get ascent; - external num get descent; - external bool get vertical; - external String get fontFamily; -} - -extension type PdfjsBaseException._(JSObject _) implements JSObject { - external String get message; - external String get name; -} - -extension type PdfjsPasswordException._(JSObject _) implements JSObject { - external String get message; - external String get name; - external String get code; -} - -extension type PdfjsGetAnnotationsParameters._(JSObject _) implements JSObject { - external PdfjsGetAnnotationsParameters({String intent}); - - /// `display` or `print` or, `any` - external String get intent; -} - -extension type PdfjsRef._(JSObject _) implements JSObject { - external int get num; - external int get gen; -} - -extension type PdfjsAnnotationData._(JSObject _) implements JSObject { - external String get subtype; - external int get annotationType; - external JSArray get rect; - external String? get url; - external String? get unsafeUrl; - external int get annotationFlags; - external JSObject? get dest; -} - -extension type PdfjsOutlineNode._(JSObject _) implements JSObject { - external String get title; - external JSAny? get dest; - external JSArray get items; -} - -final _dummyJsSyncContext = {}; - -bool _pdfjsInitialized = false; - -Future ensurePdfjsInitialized() async { - if (_pdfjsInitialized) return; - await _dummyJsSyncContext.synchronized(() async { - if (_pdfjsInitialized) return; - if (_isPdfjsLoaded) { - _pdfjsInitialized = true; - return; - } - - debugPrint( - 'pdfrx Web status:\n' - '- Running WASM: $kIsWasm\n' - '- SharedArrayBuffer: $isSharedArrayBufferSupported', - ); - if (kIsWasm && !isSharedArrayBufferSupported) { - debugPrint( - 'WARNING: SharedArrayBuffer is not enabled and WASM is running in single thread mode. Enable SharedArrayBuffer by setting the following HTTP header on your server:\n' - ' Cross-Origin-Embedder-Policy: require-corp|credentialless\n' - ' Cross-Origin-Opener-Policy: same-origin\n', - ); - } - - final pdfJsSrc = PdfJsConfiguration.configuration?.pdfJsSrc ?? _pdfjsUrl; - - final script = - web.document.createElement('script') as web.HTMLScriptElement - ..type = 'text/javascript' - ..charset = 'utf-8' - ..async = true - ..type = 'module' - ..src = pdfJsSrc; - web.document.querySelector('head')!.appendChild(script); - final completer = Completer(); - final sub1 = script.onLoad.listen((_) => completer.complete()); - final sub2 = script.onError.listen((event) => completer.completeError(event)); - try { - await completer.future; - } catch (e) { - throw StateError('Failed to load pdf.js from $pdfJsSrc: $e'); - } finally { - await sub1.cancel(); - await sub2.cancel(); - } - - if (!_isPdfjsLoaded) { - throw StateError('Failed to load pdfjs'); - } - _pdfjsWorkerSrc = PdfJsConfiguration.configuration?.workerSrc ?? _pdfjsWorkerSrc; - - _pdfjsInitialized = true; - }); -} - -extension type ReadableStream._(JSObject _) implements JSObject { - external JSPromise cancel(); - external ReadableStreamDefaultReader getReader(JSObject options); -} - -extension type ReadableStreamDefaultReader._(JSObject _) implements JSObject { - external JSPromise cancel(JSObject reason); - external JSPromise read(); - external void releaseLock(); -} - -extension type ReadableStreamChunk._(JSObject _) implements JSObject { - external JSObject get value; - external bool get done; -} diff --git a/lib/src/web/pdfjs_configuration.dart b/lib/src/web/pdfjs_configuration.dart deleted file mode 100644 index 8dda4b4c..00000000 --- a/lib/src/web/pdfjs_configuration.dart +++ /dev/null @@ -1,45 +0,0 @@ -/// Configuration for the PDF.js library. -/// -/// Set [PdfJsConfiguration.configuration] before using any APIs. It can be typically set in the main function. -class PdfJsConfiguration { - const PdfJsConfiguration({ - required this.pdfJsSrc, - required this.workerSrc, - required this.cMapUrl, - required this.cMapPacked, - this.useSystemFonts = true, - this.standardFontDataUrl, - }); - - /// `psf.js` file URL such as https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.4.168/pdf.min.mjs - final String pdfJsSrc; - - /// `psf.worker.js` file URL such as https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.4.168/pdf.worker.min.mjs - final String workerSrc; - - /// `cmaps` directory URL such as https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.4.168/cmaps/ - final String cMapUrl; - - /// Whether to use the packed cmaps. The default is true. - final bool cMapPacked; - - /// When true, fonts that aren't embedded in the PDF document will fallback to a system font. - /// The default is true. - final bool useSystemFonts; - - /// The URL where the standard font files are located. Include the trailing slash. - final String? standardFontDataUrl; - - /// The current configuration. null to use the default. - /// - /// To customize the pdf.js download URLs, set this before using any APIs.: - /// - /// ```dart - /// PdfJsConfiguration.configuration = const PdfJsConfiguration( - /// pdfJsSrc: 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.4.168/pdf.min.mjs', - /// workerSrc: 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.4.168/pdf.worker.min.mjs', - /// cMapUrl: 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.4.168/cmaps/', - /// ); - /// ``` - static PdfJsConfiguration? configuration; -} diff --git a/lib/src/web/pdfrx_js.dart b/lib/src/web/pdfrx_js.dart deleted file mode 100644 index f1b0e6c6..00000000 --- a/lib/src/web/pdfrx_js.dart +++ /dev/null @@ -1,496 +0,0 @@ -import 'dart:async'; -import 'dart:js_interop'; -import 'dart:js_interop_unsafe'; -import 'dart:ui'; - -import 'package:flutter/services.dart'; -import 'package:web/web.dart' as web; - -import '../../pdfrx.dart'; -import 'pdfjs.dart'; -import 'pdfrx_web.dart'; - -class PdfDocumentFactoryJsImpl extends PdfDocumentFactoryImpl { - PdfDocumentFactoryJsImpl() : super.callMeIfYouWantToExtendMe(); - - @override - Future openAsset( - String name, { - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - }) => _openByFunc( - (password) async { - // NOTE: Moving the asset load outside the loop may cause: - // Uncaught TypeError: Cannot perform Construct on a detached ArrayBuffer - final bytes = await rootBundle.load(name); - return await pdfjsGetDocumentFromData(bytes.buffer, password: password, allowDataOwnershipTransfer: true); - }, - sourceName: 'asset:$name', - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - ); - - @override - Future openCustom({ - required FutureOr Function(Uint8List buffer, int position, int size) read, - required int fileSize, - required String sourceName, - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - int? maxSizeToCacheOnMemory, - void Function()? onDispose, - }) async { - final buffer = Uint8List(fileSize); - await read(buffer, 0, fileSize); - return _openByFunc( - (password) => pdfjsGetDocumentFromData(buffer.buffer, password: password), - sourceName: sourceName, - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - onDispose: onDispose, - ); - } - - @override - Future openData( - Uint8List data, { - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - String? sourceName, - bool allowDataOwnershipTransfer = false, - void Function()? onDispose, - }) async { - return _openByFunc( - (password) => pdfjsGetDocumentFromData( - data.buffer, - password: password, - allowDataOwnershipTransfer: allowDataOwnershipTransfer, - ), - sourceName: sourceName ?? 'memory-${data.hashCode}', - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - onDispose: onDispose, - ); - } - - @override - Future openFile( - String filePath, { - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - }) => _openByFunc( - (password) => pdfjsGetDocument(filePath, password: password), - sourceName: filePath, - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - ); - - @override - Future openUri( - Uri uri, { - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - PdfDownloadProgressCallback? progressCallback, - PdfDownloadReportCallback? reportCallback, - bool preferRangeAccess = false, - Map? headers, - bool withCredentials = false, - }) => _openByFunc( - (password) => - pdfjsGetDocument(uri.toString(), password: password, headers: headers, withCredentials: withCredentials), - sourceName: uri.toString(), - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - ); - - Future _openByFunc( - Future Function(String? password) openDocument, { - required String sourceName, - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - void Function()? onDispose, - }) async { - for (int i = 0; ; i++) { - final String? password; - if (firstAttemptByEmptyPassword && i == 0) { - password = null; - } else { - password = await passwordProvider?.call(); - if (password == null) { - throw const PdfPasswordException('No password supplied by PasswordProvider.'); - } - } - try { - await ensurePdfjsInitialized(); - - return PdfDocumentJs.fromDocument(await openDocument(password), sourceName: sourceName, onDispose: onDispose); - } catch (e) { - if (!_isPasswordError(e)) { - rethrow; - } - } - } - } - - static bool _isPasswordError(dynamic e) => e.toString().startsWith('PasswordException:'); -} - -class PdfDocumentJs extends PdfDocument { - PdfDocumentJs._( - this._document, { - required super.sourceName, - required this.isEncrypted, - required this.permissions, - this.onDispose, - }); - - @override - final bool isEncrypted; - @override - final PdfPermissions? permissions; - - final PdfjsDocument _document; - final void Function()? onDispose; - - static Future fromDocument( - PdfjsDocument document, { - required String sourceName, - void Function()? onDispose, - }) async { - final perms = (await document.getPermissions().toDart)?.toDart.map((v) => v.toDartInt).toList(); - final doc = PdfDocumentJs._( - document, - sourceName: sourceName, - isEncrypted: perms != null, - permissions: perms != null ? PdfPermissions(perms.fold(0, (p, e) => p | e), 2) : null, - onDispose: onDispose, - ); - final pageCount = document.numPages; - final pages = []; - for (int i = 0; i < pageCount; i++) { - pages.add(await doc._getPage(document, i + 1)); - } - doc.pages = List.unmodifiable(pages); - return doc; - } - - @override - Future dispose() async { - _document.destroy(); - onDispose?.call(); - } - - Future _getPage(PdfjsDocument document, int pageNumber) async { - final page = await _document.getPage(pageNumber).toDart; - final vp1 = page.getViewport(PdfjsViewportParams(scale: 1)); - return PdfPageJs._( - document: this, - pageNumber: pageNumber, - page: page, - width: vp1.width, - height: vp1.height, - rotation: PdfPageRotation.values[page.rotate ~/ 90], - ); - } - - @override - late final List pages; - - @override - bool isIdenticalDocumentHandle(Object? other) => other is PdfDocumentJs && _document == other._document; - - Future _getDestObject(JSAny? dest) async { - if (dest == null) return null; - if (dest.isA()) { - return await _document.getDestination((dest as JSString).toDart).toDart; - } else { - return dest as JSObject; - } - } - - @override - Future> loadOutline() async { - final outline = await _document.getOutline().toDart; - if (outline == null) return []; - final nodes = []; - for (final node in outline.toDart) { - nodes.add(await _pdfOutlineNodeFromOutline(node)); - } - return nodes; - } - - Future _pdfOutlineNodeFromOutline(PdfjsOutlineNode outline) async { - final children = []; - for (final item in outline.items.toDart) { - children.add(await _pdfOutlineNodeFromOutline(item)); - } - return PdfOutlineNode(title: outline.title, dest: await _getDestination(outline.dest), children: children); - } - - /// NOTE: The returned [PdfDest] is always compacted. - Future _getDestination(JSAny? dest) async { - final destObj = await _getDestObject(dest); - if (!destObj.isA()) return null; - final arr = (destObj as JSArray).toDart; - final ref = arr[0] as PdfjsRef; - final cmdStr = _getName(arr[1]); - final List? params; - if (arr.length < 3) { - params = null; - } else { - params = List.unmodifiable(arr.sublist(2).map((v) => (v as JSNumber?)?.toDartDouble)); - } - - return PdfDest((await _document.getPageIndex(ref).toDart).toDartInt + 1, PdfDestCommand.parse(cmdStr), params); - } - - static String _getName(JSAny? name) { - if (name == null) { - throw ArgumentError.notNull('name'); - } - if (name.isA()) { - return (name as JSString).toDart; - } - final nameValue = (name as JSObject).getProperty('name'.toJS)?.toString(); - if (nameValue == null) { - throw ArgumentError.value(name, 'name', 'Invalid name object.'); - } - return nameValue; - } -} - -class PdfPageJs extends PdfPage { - PdfPageJs._({ - required this.document, - required this.pageNumber, - required this.page, - required this.width, - required this.height, - required this.rotation, - }); - @override - final PdfDocumentJs document; - @override - final int pageNumber; - final PdfjsPage page; - @override - final double width; - @override - final double height; - @override - final PdfPageRotation rotation; - - @override - Future render({ - int x = 0, - int y = 0, - int? width, - int? height, - double? fullWidth, - double? fullHeight, - Color? backgroundColor, - PdfAnnotationRenderingMode annotationRenderingMode = PdfAnnotationRenderingMode.annotationAndForms, - PdfPageRenderCancellationToken? cancellationToken, - }) async { - if (cancellationToken == null) { - cancellationToken = PdfPageRenderCancellationTokenWeb(); - } else if (cancellationToken is! PdfPageRenderCancellationTokenWeb) { - throw ArgumentError( - 'cancellationToken must be created by PdfPage.createCancellationToken().', - 'cancellationToken', - ); - } - fullWidth ??= this.width; - fullHeight ??= this.height; - width ??= fullWidth.toInt(); - height ??= fullHeight.toInt(); - - return PdfImageWeb( - width: width, - height: height, - pixels: await _renderRaw( - x, - y, - width, - height, - fullWidth, - fullHeight, - backgroundColor, - false, - annotationRenderingMode, - cancellationToken as PdfPageRenderCancellationTokenWeb, - ), - ); - } - - @override - PdfPageRenderCancellationTokenWeb createCancellationToken() => PdfPageRenderCancellationTokenWeb(); - - Future _renderRaw( - int x, - int y, - int width, - int height, - double fullWidth, - double fullHeight, - Color? backgroundColor, - bool dontFlip, - PdfAnnotationRenderingMode annotationRenderingMode, - PdfPageRenderCancellationTokenWeb cancellationToken, - ) async { - final vp1 = page.getViewport(PdfjsViewportParams(scale: 1)); - final pageWidth = vp1.width; - if (width <= 0 || height <= 0) { - throw PdfException('Invalid PDF page rendering rectangle ($width x $height)'); - } - - final vp = page.getViewport( - PdfjsViewportParams( - scale: fullWidth / pageWidth, - offsetX: -x.toDouble(), - offsetY: -y.toDouble(), - dontFlip: dontFlip, - ), - ); - - final canvas = web.document.createElement('canvas') as web.HTMLCanvasElement; - canvas.width = width; - canvas.height = height; - - if (backgroundColor != null) { - canvas.context2D.fillStyle = '#${backgroundColor.toARGB32().toRadixString(16).padLeft(8, '0')}'.toJS; - canvas.context2D.fillRect(0, 0, width, height); - } - - await page - .render( - PdfjsRenderContext( - canvasContext: canvas.context2D, - viewport: vp, - annotationMode: annotationRenderingMode.index, - ), - ) - .promise - .toDart; - - return canvas.context2D.getImageData(0, 0, width, height).data.toDart.buffer.asUint8List(); - } - - @override - Future loadText() => PdfPageTextJs._loadText(this); - - @override - Future> loadLinks({bool compact = false}) async { - final annots = (await page.getAnnotations(PdfjsGetAnnotationsParameters()).toDart).toDart; - final links = []; - for (final annot in annots) { - if (annot.subtype != 'Link') { - continue; - } - final rects = List.unmodifiable([ - PdfRect( - // be careful with the order of the rect values - annot.rect[0].toDartDouble, // L - annot.rect[3].toDartDouble, // T - annot.rect[2].toDartDouble, // R - annot.rect[1].toDartDouble, // B - ), - ]); - if (annot.url != null) { - links.add(PdfLink(rects, url: Uri.parse(annot.url!))); - continue; - } - final dest = await document._getDestination(annot.dest); - if (dest != null) { - links.add(PdfLink(rects, dest: dest)); - continue; - } - } - return compact ? List.unmodifiable(links) : links; - } -} - -class PdfPageRenderCancellationTokenWeb extends PdfPageRenderCancellationToken { - bool _canceled = false; - @override - void cancel() => _canceled = true; - - @override - bool get isCanceled => _canceled; -} - -class PdfImageWeb extends PdfImage { - PdfImageWeb({required this.width, required this.height, required this.pixels, this.format = PixelFormat.rgba8888}); - - @override - final int width; - @override - final int height; - @override - final Uint8List pixels; - @override - final PixelFormat format; - @override - void dispose() {} -} - -class PdfPageTextFragmentWeb implements PdfPageTextFragment { - PdfPageTextFragmentWeb(this.index, this.bounds, this.text); - - @override - final int index; - @override - int get length => text.length; - @override - int get end => index + length; - @override - final PdfRect bounds; - @override - List? get charRects => null; - @override - final String text; -} - -class PdfPageTextJs extends PdfPageText { - PdfPageTextJs({required this.pageNumber, required this.fullText, required this.fragments}); - - @override - final int pageNumber; - - @override - final String fullText; - @override - final List fragments; - - static Future _loadText(PdfPageJs page) async { - final content = - await page.page - .getTextContent(PdfjsGetTextContentParameters(includeMarkedContent: false, disableNormalization: false)) - .toDart; - final sb = StringBuffer(); - final fragments = []; - for (final item in content.items.toDart) { - final x = item.transform[4].toDartDouble; - final y = item.transform[5].toDartDouble; - final str = item.hasEOL ? '${item.str}\n' : item.str; - if (str == '\n' && fragments.isNotEmpty) { - final prev = fragments.last; - fragments.add( - PdfPageTextFragmentWeb( - sb.length, - PdfRect(prev.bounds.right, prev.bounds.top, prev.bounds.right + item.width.toDouble(), prev.bounds.bottom), - str, - ), - ); - } else { - fragments.add( - PdfPageTextFragmentWeb(sb.length, PdfRect(x, y + item.height.toDouble(), x + item.width.toDouble(), y), str), - ); - } - - sb.write(str); - } - - return PdfPageTextJs(pageNumber: page.pageNumber, fullText: sb.toString(), fragments: fragments); - } -} diff --git a/lib/src/web/pdfrx_wasm.dart b/lib/src/web/pdfrx_wasm.dart deleted file mode 100644 index 470ee301..00000000 --- a/lib/src/web/pdfrx_wasm.dart +++ /dev/null @@ -1,423 +0,0 @@ -import 'dart:async'; -import 'dart:js_interop'; -import 'dart:typed_data'; -import 'dart:ui' as ui; - -import 'package:flutter/material.dart' show Colors, immutable; -import 'package:flutter/services.dart'; -import 'package:web/web.dart' as web; - -import '../pdf_api.dart'; -import 'pdfrx_js.dart'; -import 'pdfrx_web.dart'; - -/// Calls PDFium WASM worker with the given command and parameters. -@JS() -external JSPromise pdfiumWasmSendCommand([String command, JSAny? parameters, JSArray? transfer]); - -/// The URL of the PDFium WASM worker script; pdfium_client.js tries to load worker script from this URL.' -/// -/// [PdfDocumentFactoryWasmImpl._init] will initializes its value. -@JS() -external String pdfiumWasmWorkerUrl; - -/// [PdfDocumentFactory] for PDFium WASM implementation. -class PdfDocumentFactoryWasmImpl extends PdfDocumentFactoryImpl { - PdfDocumentFactoryWasmImpl() : super.callMeIfYouWantToExtendMe(); - - /// Default path to the WASM modules - /// - /// Normally, the WASM modules are provided by pdfrx_wasm package and this is the path to its assets. - static const defaultWasmModulePath = 'assets/packages/pdfrx_wasm/assets/'; - - Future _init() async { - pdfiumWasmWorkerUrl = _getWorkerUrl(); - final moduleUrl = Pdfrx.pdfiumWasmModulesUrl ?? defaultWasmModulePath; - final script = - web.document.createElement('script') as web.HTMLScriptElement - ..type = 'text/javascript' - ..charset = 'utf-8' - ..async = true - ..type = 'module' - ..src = '${moduleUrl}pdfium_client.js'; - web.document.querySelector('head')!.appendChild(script); - final completer = Completer(); - final sub1 = script.onLoad.listen((_) => completer.complete()); - final sub2 = script.onError.listen((event) => completer.completeError(event)); - try { - await completer.future; - } catch (e) { - throw StateError('Failed to load pdfium_client.js from $moduleUrl: $e'); - } finally { - await sub1.cancel(); - await sub2.cancel(); - } - } - - /// Ugly workaround for Cross-Origin-Embedder-Policy restriction on WASM enabled environments - String _getWorkerUrl() { - final moduleUrl = - Pdfrx.pdfiumWasmModulesUrl ?? '${_removeLastComponent(web.window.location.href)}$defaultWasmModulePath'; - final workerJsUrl = '${moduleUrl}pdfium_worker.js'; - final pdfiumWasmUrl = '${moduleUrl}pdfium.wasm'; - final content = 'const pdfiumWasmUrl="$pdfiumWasmUrl";importScripts("$workerJsUrl");'; - final blob = web.Blob( - [content].jsify() as JSArray, - web.BlobPropertyBag(type: 'application/javascript'), - ); - return web.URL.createObjectURL(blob); - } - - /// Removes the last component from the URL (e.g. the file name) and adds a trailing slash if necessary. - /// - /// This is necessary to ensure that the URL points to a directory, which is required by the WASM loader. - /// - `https://example.com/path/to/file.pdf` -> `https://example.com/path/to/` - /// - `https://example.com/path/to/` -> `https://example.com/path/to/` - /// - `https://example.com/` -> `https://example.com/` - /// - `https://example.com` -> `https://example.com/` - static String _removeLastComponent(String url) { - final lastSlash = url.lastIndexOf('/'); - if (lastSlash == -1) { - return '$url/'; - } - return url.substring(0, lastSlash + 1); - } - - Future> sendCommand(String command, {Map? parameters}) async { - final result = await pdfiumWasmSendCommand(command, parameters?.jsify()).toDart; - return (result.dartify()) as Map; - } - - @override - Future openAsset( - String name, { - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - }) async { - final asset = await rootBundle.load(name); - final data = asset.buffer.asUint8List(); - return await openData( - data, - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - sourceName: name, - allowDataOwnershipTransfer: true, - ); - } - - @override - Future openCustom({ - required FutureOr Function(Uint8List buffer, int position, int size) read, - required int fileSize, - required String sourceName, - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - int? maxSizeToCacheOnMemory, - void Function()? onDispose, - }) async { - throw UnimplementedError(); - } - - @override - Future openData( - Uint8List data, { - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - String? sourceName, - bool allowDataOwnershipTransfer = false, - void Function()? onDispose, - }) => _openByFunc( - (password) => sendCommand('loadDocumentFromData', parameters: {'data': data, 'password': password}), - sourceName: sourceName ?? 'data', - factory: this, - passwordProvider: passwordProvider, - onDispose: onDispose, - ); - - @override - Future openFile( - String filePath, { - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - }) => _openByFunc( - (password) => sendCommand('loadDocumentFromUrl', parameters: {'url': filePath, 'password': password}), - sourceName: filePath, - factory: this, - passwordProvider: passwordProvider, - ); - - @override - Future openUri( - Uri uri, { - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - PdfDownloadProgressCallback? progressCallback, - PdfDownloadReportCallback? reportCallback, - bool preferRangeAccess = false, - Map? headers, - bool withCredentials = false, - }) => _openByFunc( - (password) => sendCommand('loadDocumentFromUrl', parameters: {'url': uri.toString(), 'password': password}), - sourceName: uri.toString(), - factory: this, - passwordProvider: passwordProvider, - ); - - Future _openByFunc( - Future> Function(String? password) openDocument, { - required String sourceName, - required PdfDocumentFactoryWasmImpl factory, - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - void Function()? onDispose, - }) async { - for (int i = 0; ; i++) { - final String? password; - if (firstAttemptByEmptyPassword && i == 0) { - password = null; - } else { - password = await passwordProvider?.call(); - if (password == null) { - throw const PdfPasswordException('No password supplied by PasswordProvider.'); - } - } - try { - await _init(); - - return PdfDocumentWasm._( - await openDocument(password), - sourceName: sourceName, - disposeCallback: onDispose, - factory: factory, - ); - } catch (e) { - if (!_isPasswordError(e)) { - rethrow; - } - } - } - } - - static bool _isPasswordError(dynamic e) => e.toString().startsWith('PasswordException:'); -} - -class PdfDocumentWasm extends PdfDocument { - PdfDocumentWasm._(this.document, {required super.sourceName, required this.factory, this.disposeCallback}) - : permissions = parsePermissions(document) { - pages = parsePages(this, document); - } - - final Map document; - final PdfDocumentFactoryWasmImpl factory; - final void Function()? disposeCallback; - bool isDisposed = false; - - @override - final PdfPermissions? permissions; - - @override - bool get isEncrypted => permissions != null; - - @override - Future dispose() async { - if (!isDisposed) { - isDisposed = true; - await factory.sendCommand('closeDocument', parameters: document); - disposeCallback?.call(); - } - } - - @override - bool isIdenticalDocumentHandle(Object? other) { - return other is PdfDocumentWasm && other.document['docHandle'] == document['docHandle']; - } - - @override - Future> loadOutline() async { - return []; - } - - @override - late final List pages; - - static PdfPermissions? parsePermissions(Map document) { - final perms = (document['permissions'] as num).toInt(); - final securityHandlerRevision = (document['securityHandlerRevision'] as num).toInt(); - if (perms >= 0 && securityHandlerRevision >= 0) { - return PdfPermissions(perms, securityHandlerRevision); - } else { - return null; - } - } - - static List parsePages(PdfDocumentWasm doc, Map document) { - final pageList = document['pages'] as List; - return pageList - .map( - (page) => PdfPageWasm( - doc, - (page['pageIndex'] as num).toInt(), - page['width'], - page['height'], - (page['rotation'] as num).toInt(), - ), - ) - .toList(); - } -} - -class PdfPageRenderCancellationTokenWasm extends PdfPageRenderCancellationToken { - PdfPageRenderCancellationTokenWasm(); - - bool _isCanceled = false; - - @override - Future cancel() async { - _isCanceled = true; - } - - @override - bool get isCanceled => _isCanceled; -} - -class PdfPageWasm extends PdfPage { - PdfPageWasm(this.document, int pageIndex, this.width, this.height, int rotation) - : pageNumber = pageIndex + 1, - rotation = PdfPageRotation.values[rotation]; - - @override - PdfPageRenderCancellationToken createCancellationToken() { - return PdfPageRenderCancellationTokenWasm(); - } - - @override - final PdfDocumentWasm document; - - @override - Future> loadLinks({bool compact = false}) async { - final result = await document.factory.sendCommand( - 'loadLinks', - parameters: {'docHandle': document.document['docHandle'], 'pageIndex': pageNumber - 1}, - ); - return (result['links'] as List).map((link) { - if (link is! Map) { - throw FormatException('Unexpected link structure: $link'); - } - final rects = - (link['rects'] as List).map((r) { - final rect = r as List; - return PdfRect(rect[0] as double, rect[1] as double, rect[2] as double, rect[3] as double); - }).toList(); - final url = link['url']; - if (url is String) { - return PdfLink(rects, url: Uri.parse(url)); - } - final dest = link['dest']; - if (dest is! Map) { - throw FormatException('Unexpected link destination structure: $dest'); - } - final params = dest['params'] as List; - final pdfDest = PdfDest( - (dest['pageIndex'] as num).toInt() + 1, - PdfDestCommand.parse(dest['command'] as String), - params.map((p) => p as double).toList(), - ); - return PdfLink(rects, dest: pdfDest); - }).toList(); - } - - @override - Future loadText() async { - final result = await document.factory.sendCommand( - 'loadText', - parameters: {'docHandle': document.document['docHandle'], 'pageIndex': pageNumber - 1}, - ); - final pageText = PdfPageTextJs(pageNumber: pageNumber, fullText: result['fullText'], fragments: []); - final fragmentOffsets = result['fragments']; - final charRectsAll = result['charRects'] as List; - if (fragmentOffsets is List) { - int pos = 0; - for (final fragment in fragmentOffsets.map((n) => (n as num).toInt())) { - final charRects = - charRectsAll.sublist(pos, pos + fragment).map((rect) { - final r = rect as List; - return PdfRect(r[0] as double, r[1] as double, r[2] as double, r[3] as double); - }).toList(); - pageText.fragments.add(PdfPageTextFragmentPdfium(pageText, pos, fragment, charRects.boundingRect(), charRects)); - pos += fragment; - } - } - return pageText; - } - - @override - final int pageNumber; - - @override - final PdfPageRotation rotation; - - @override - final double width; - - @override - final double height; - - @override - Future render({ - int x = 0, - int y = 0, - int? width, - int? height, - double? fullWidth, - double? fullHeight, - Color? backgroundColor, - PdfAnnotationRenderingMode annotationRenderingMode = PdfAnnotationRenderingMode.annotationAndForms, - PdfPageRenderCancellationToken? cancellationToken, - }) async { - fullWidth ??= this.width; - fullHeight ??= this.height; - width ??= fullWidth.toInt(); - height ??= fullHeight.toInt(); - backgroundColor ??= Colors.white; - - final result = await document.factory.sendCommand( - 'renderPage', - parameters: { - 'docHandle': document.document['docHandle'], - 'pageIndex': pageNumber - 1, - 'x': x, - 'y': y, - 'width': width, - 'height': height, - 'fullWidth': fullWidth, - 'fullHeight': fullHeight, - 'backgroundColor': backgroundColor.toARGB32(), - 'annotationRenderingMode': annotationRenderingMode.index, - 'formHandle': document.document['formHandle'], - }, - ); - final bb = result['imageData'] as ByteBuffer; - final pixels = Uint8List.view(bb.asByteData().buffer, 0, bb.lengthInBytes); - return PdfImageWeb(width: width, height: height, pixels: pixels, format: ui.PixelFormat.bgra8888); - } -} - -@immutable -class PdfPageTextFragmentPdfium implements PdfPageTextFragment { - const PdfPageTextFragmentPdfium(this.pageText, this.index, this.length, this.bounds, this.charRects); - - final PdfPageText pageText; - - @override - final int index; - @override - final int length; - @override - int get end => index + length; - @override - final PdfRect bounds; - @override - final List? charRects; - @override - String get text => pageText.fullText.substring(index, index + length); -} diff --git a/lib/src/web/pdfrx_web.dart b/lib/src/web/pdfrx_web.dart deleted file mode 100644 index 201c9c2f..00000000 --- a/lib/src/web/pdfrx_web.dart +++ /dev/null @@ -1,17 +0,0 @@ -import '../../pdfrx.dart'; -import 'pdfrx_js.dart'; -import 'pdfrx_wasm.dart'; - -/// Ugly, but working solution to provide a factory that switches between JS and WASM implementations. -abstract class PdfDocumentFactoryImpl extends PdfDocumentFactory { - factory PdfDocumentFactoryImpl() { - if (Pdfrx.webRuntimeType == PdfrxWebRuntimeType.pdfiumWasm) { - return PdfDocumentFactoryWasmImpl(); - } else { - return PdfDocumentFactoryJsImpl(); - } - } - - /// Call this method to extend the factory like `super.callMeIfYouWantToExtendMe()` on its constructor implementation. - PdfDocumentFactoryImpl.callMeIfYouWantToExtendMe(); -} diff --git a/lib/src/widgets/interactive_viewer.dart b/lib/src/widgets/interactive_viewer.dart deleted file mode 100644 index 5cdba37d..00000000 --- a/lib/src/widgets/interactive_viewer.dart +++ /dev/null @@ -1,1223 +0,0 @@ -// ------------------------------------------------------------ -// FORKED FROM Flutter's original InteractiveViewer.dart -// ------------------------------------------------------------ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:math' as math; - -import 'package:flutter/foundation.dart' show clampDouble; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/physics.dart'; -import 'package:vector_math/vector_math_64.dart' show Matrix4, Quad, Vector3; - -// Examples can assume: -// late BuildContext context; -// late Offset? _childWasTappedAt; -// late TransformationController _transformationController; -// Widget child = const Placeholder(); - -/// A signature for widget builders that take a [Quad] of the current viewport. -/// -/// See also: -/// -/// * [InteractiveViewer.builder], whose builder is of this type. -/// * [WidgetBuilder], which is similar, but takes no viewport. -typedef InteractiveViewerWidgetBuilder = Widget Function(BuildContext context, Quad viewport); - -/// [**FORKED VERSION**] A widget that enables pan and zoom interactions with its child. -/// -/// {@youtube 560 315 https://www.youtube.com/watch?v=zrn7V3bMJvg} -/// -/// The user can transform the child by dragging to pan or pinching to zoom. -/// -/// By default, InteractiveViewer clips its child using [Clip.hardEdge]. -/// To prevent this behavior, consider setting [clipBehavior] to [Clip.none]. -/// When [clipBehavior] is [Clip.none], InteractiveViewer may draw outside of -/// its original area of the screen, such as when a child is zoomed in and -/// increases in size. However, it will not receive gestures outside of its original area. -/// To prevent dead areas where InteractiveViewer does not receive gestures, -/// don't set [clipBehavior] or be sure that the InteractiveViewer widget is the -/// size of the area that should be interactive. -/// -/// See also: -/// * The [Flutter Gallery's transformations demo](https://github.com/flutter/gallery/blob/main/lib/demos/reference/transformations_demo.dart), -/// which includes the use of InteractiveViewer. -/// * The [flutter-go demo](https://github.com/justinmc/flutter-go), which includes robust positioning of an InteractiveViewer child -/// that works for all screen sizes and child sizes. -/// * The [Lazy Flutter Performance Session](https://www.youtube.com/watch?v=qax_nOpgz7E), which includes the use of an InteractiveViewer to -/// performantly view subsets of a large set of widgets using the builder constructor. -/// -/// {@tool dartpad} -/// This example shows a simple Container that can be panned and zoomed. -/// -/// ** See code in examples/api/lib/widgets/interactive_viewer/interactive_viewer.0.dart ** -/// {@end-tool} -@immutable -class InteractiveViewer extends StatefulWidget { - /// Create an InteractiveViewer. - InteractiveViewer({ - required this.child, - super.key, - this.clipBehavior = Clip.hardEdge, - this.panAxis = PanAxis.free, - this.boundaryMargin = EdgeInsets.zero, - this.constrained = true, - // These default scale values were eyeballed as reasonable limits for common - // use cases. - this.maxScale = 8.0, - this.minScale = 0.8, - this.interactionEndFrictionCoefficient = _kDrag, - this.onInteractionEnd, - this.onInteractionStart, - this.onInteractionUpdate, - this.panEnabled = true, - this.scaleEnabled = true, - this.scaleFactor = kDefaultMouseScrollToScaleFactor, - this.transformationController, - this.alignment, - this.trackpadScrollCausesScale = false, - this.onWheelDelta, - }) : assert(minScale > 0), - assert(interactionEndFrictionCoefficient > 0), - assert(minScale.isFinite), - assert(maxScale > 0), - assert(!maxScale.isNaN), - assert(maxScale >= minScale), - // boundaryMargin must be either fully infinite or fully finite, but not - // a mix of both. - assert( - (boundaryMargin.horizontal.isInfinite && boundaryMargin.vertical.isInfinite) || - (boundaryMargin.top.isFinite && - boundaryMargin.right.isFinite && - boundaryMargin.bottom.isFinite && - boundaryMargin.left.isFinite), - ), - builder = null; - - /// Creates an InteractiveViewer for a child that is created on demand. - /// - /// Can be used to render a child that changes in response to the current - /// transformation. - /// - /// See the [builder] attribute docs for an example of using it to optimize a - /// large child. - InteractiveViewer.builder({ - required InteractiveViewerWidgetBuilder this.builder, - super.key, - this.clipBehavior = Clip.hardEdge, - this.panAxis = PanAxis.free, - this.boundaryMargin = EdgeInsets.zero, - // These default scale values were eyeballed as reasonable limits for common - // use cases. - this.maxScale = 8.0, - this.minScale = 0.8, - this.interactionEndFrictionCoefficient = _kDrag, - this.onInteractionEnd, - this.onInteractionStart, - this.onInteractionUpdate, - this.panEnabled = true, - this.scaleEnabled = true, - this.scaleFactor = 200.0, - this.transformationController, - this.alignment, - this.trackpadScrollCausesScale = false, - this.onWheelDelta, - }) : assert(minScale > 0), - assert(interactionEndFrictionCoefficient > 0), - assert(minScale.isFinite), - assert(maxScale > 0), - assert(!maxScale.isNaN), - assert(maxScale >= minScale), - // boundaryMargin must be either fully infinite or fully finite, but not - // a mix of both. - assert( - (boundaryMargin.horizontal.isInfinite && boundaryMargin.vertical.isInfinite) || - (boundaryMargin.top.isFinite && - boundaryMargin.right.isFinite && - boundaryMargin.bottom.isFinite && - boundaryMargin.left.isFinite), - ), - constrained = false, - child = null; - - /// The alignment of the child's origin, relative to the size of the box. - final Alignment? alignment; - - /// If set to [Clip.none], the child may extend beyond the size of the InteractiveViewer, - /// but it will not receive gestures in these areas. - /// Be sure that the InteractiveViewer is the desired size when using [Clip.none]. - /// - /// Defaults to [Clip.hardEdge]. - final Clip clipBehavior; - - /// When set to [PanAxis.aligned], panning is only allowed in the horizontal - /// axis or the vertical axis, diagonal panning is not allowed. - /// - /// When set to [PanAxis.vertical] or [PanAxis.horizontal] panning is only - /// allowed in the specified axis. For example, if set to [PanAxis.vertical], - /// panning will only be allowed in the vertical axis. And if set to [PanAxis.horizontal], - /// panning will only be allowed in the horizontal axis. - /// - /// When set to [PanAxis.free] panning is allowed in all directions. - /// - /// Defaults to [PanAxis.free]. - final PanAxis panAxis; - - /// A margin for the visible boundaries of the child. - /// - /// Any transformation that results in the viewport being able to view outside - /// of the boundaries will be stopped at the boundary. The boundaries do not - /// rotate with the rest of the scene, so they are always aligned with the - /// viewport. - /// - /// To produce no boundaries at all, pass infinite [EdgeInsets], such as - /// `EdgeInsets.all(double.infinity)`. - /// - /// No edge can be NaN. - /// - /// Defaults to [EdgeInsets.zero], which results in boundaries that are the - /// exact same size and position as the [child]. - final EdgeInsets boundaryMargin; - - /// Builds the child of this widget. - /// - /// Passed with the [InteractiveViewer.builder] constructor. Otherwise, the - /// [child] parameter must be passed directly, and this is null. - /// - /// {@tool dartpad} - /// This example shows how to use builder to create a [Table] whose cell - /// contents are only built when they are visible. Built and remove cells are - /// logged in the console for illustration. - /// - /// ** See code in examples/api/lib/widgets/interactive_viewer/interactive_viewer.builder.0.dart ** - /// {@end-tool} - /// - /// See also: - /// - /// * [ListView.builder], which follows a similar pattern. - final InteractiveViewerWidgetBuilder? builder; - - /// The child [Widget] that is transformed by InteractiveViewer. - /// - /// If the [InteractiveViewer.builder] constructor is used, then this will be - /// null, otherwise it is required. - final Widget? child; - - /// Whether the normal size constraints at this point in the widget tree are - /// applied to the child. - /// - /// If set to false, then the child will be given infinite constraints. This - /// is often useful when a child should be bigger than the InteractiveViewer. - /// - /// For example, for a child which is bigger than the viewport but can be - /// panned to reveal parts that were initially offscreen, [constrained] must - /// be set to false to allow it to size itself properly. If [constrained] is - /// true and the child can only size itself to the viewport, then areas - /// initially outside of the viewport will not be able to receive user - /// interaction events. If experiencing regions of the child that are not - /// receptive to user gestures, make sure [constrained] is false and the child - /// is sized properly. - /// - /// Defaults to true. - /// - /// {@tool dartpad} - /// This example shows how to create a pannable table. Because the table is - /// larger than the entire screen, setting [constrained] to false is necessary - /// to allow it to be drawn to its full size. The parts of the table that - /// exceed the screen size can then be panned into view. - /// - /// ** See code in examples/api/lib/widgets/interactive_viewer/interactive_viewer.constrained.0.dart ** - /// {@end-tool} - final bool constrained; - - /// If false, the user will be prevented from panning. - /// - /// Defaults to true. - /// - /// See also: - /// - /// * [scaleEnabled], which is similar but for scale. - final bool panEnabled; - - /// If false, the user will be prevented from scaling. - /// - /// Defaults to true. - /// - /// See also: - /// - /// * [panEnabled], which is similar but for panning. - final bool scaleEnabled; - - /// {@macro flutter.gestures.scale.trackpadScrollCausesScale} - final bool trackpadScrollCausesScale; - - /// Determines the amount of scale to be performed per pointer scroll. - /// - /// Defaults to [kDefaultMouseScrollToScaleFactor]. - /// - /// Increasing this value above the default causes scaling to feel slower, - /// while decreasing it causes scaling to feel faster. - /// - /// The amount of scale is calculated as the exponential function of the - /// [PointerScrollEvent.scrollDelta] to [scaleFactor] ratio. In the Flutter - /// engine, the mousewheel [PointerScrollEvent.scrollDelta] is hardcoded to 20 - /// per scroll, while a trackpad scroll can be any amount. - /// - /// Affects only pointer device scrolling, not pinch to zoom. - final double scaleFactor; - - /// The maximum allowed scale. - /// - /// The scale will be clamped between this and [minScale] inclusively. - /// - /// Defaults to 2.5. - /// - /// Must be greater than zero and greater than [minScale]. - final double maxScale; - - /// The minimum allowed scale. - /// - /// The scale will be clamped between this and [maxScale] inclusively. - /// - /// Scale is also affected by [boundaryMargin]. If the scale would result in - /// viewing beyond the boundary, then it will not be allowed. By default, - /// boundaryMargin is EdgeInsets.zero, so scaling below 1.0 will not be - /// allowed in most cases without first increasing the boundaryMargin. - /// - /// Defaults to 0.8. - /// - /// Must be a finite number greater than zero and less than [maxScale]. - final double minScale; - - /// Changes the deceleration behavior after a gesture. - /// - /// Defaults to 0.0000135. - /// - /// Must be a finite number greater than zero. - final double interactionEndFrictionCoefficient; - - /// Called when the user ends a pan or scale gesture on the widget. - /// - /// At the time this is called, the [TransformationController] will have - /// already been updated to reflect the change caused by the interaction, - /// though a pan may cause an inertia animation after this is called as well. - /// - /// {@template flutter.widgets.InteractiveViewer.onInteractionEnd} - /// Will be called even if the interaction is disabled with [panEnabled] or - /// [scaleEnabled] for both touch gestures and mouse interactions. - /// - /// A [GestureDetector] wrapping the InteractiveViewer will not respond to - /// [GestureDetector.onScaleStart], [GestureDetector.onScaleUpdate], and - /// [GestureDetector.onScaleEnd]. Use [onInteractionStart], - /// [onInteractionUpdate], and [onInteractionEnd] to respond to those - /// gestures. - /// {@endtemplate} - /// - /// See also: - /// - /// * [onInteractionStart], which handles the start of the same interaction. - /// * [onInteractionUpdate], which handles an update to the same interaction. - final GestureScaleEndCallback? onInteractionEnd; - - /// Called when the user begins a pan or scale gesture on the widget. - /// - /// At the time this is called, the [TransformationController] will not have - /// changed due to this interaction. - /// - /// {@macro flutter.widgets.InteractiveViewer.onInteractionEnd} - /// - /// The coordinates provided in the details' `focalPoint` and - /// `localFocalPoint` are normal Flutter event coordinates, not - /// InteractiveViewer scene coordinates. See - /// [TransformationController.toScene] for how to convert these coordinates to - /// scene coordinates relative to the child. - /// - /// See also: - /// - /// * [onInteractionUpdate], which handles an update to the same interaction. - /// * [onInteractionEnd], which handles the end of the same interaction. - final GestureScaleStartCallback? onInteractionStart; - - /// Called when the user updates a pan or scale gesture on the widget. - /// - /// At the time this is called, the [TransformationController] will have - /// already been updated to reflect the change caused by the interaction, if - /// the interaction caused the matrix to change. - /// - /// {@macro flutter.widgets.InteractiveViewer.onInteractionEnd} - /// - /// The coordinates provided in the details' `focalPoint` and - /// `localFocalPoint` are normal Flutter event coordinates, not - /// InteractiveViewer scene coordinates. See - /// [TransformationController.toScene] for how to convert these coordinates to - /// scene coordinates relative to the child. - /// - /// See also: - /// - /// * [onInteractionStart], which handles the start of the same interaction. - /// * [onInteractionEnd], which handles the end of the same interaction. - final GestureScaleUpdateCallback? onInteractionUpdate; - - /// A [TransformationController] for the transformation performed on the - /// child. - /// - /// Whenever the child is transformed, the [Matrix4] value is updated and all - /// listeners are notified. If the value is set, InteractiveViewer will update - /// to respect the new value. - /// - /// {@tool dartpad} - /// This example shows how transformationController can be used to animate the - /// transformation back to its starting position. - /// - /// ** See code in examples/api/lib/widgets/interactive_viewer/interactive_viewer.transformation_controller.0.dart ** - /// {@end-tool} - /// - /// See also: - /// - /// * [ValueNotifier], the parent class of TransformationController. - /// * [TextEditingController] for an example of another similar pattern. - final TransformationController? transformationController; - - /// To override the default mouse wheel behavior. - /// - final void Function(Offset scrollDelta)? onWheelDelta; - - // Used as the coefficient of friction in the inertial translation animation. - // This value was eyeballed to give a feel similar to Google Photos. - static const double _kDrag = 0.0000135; - - /// Returns the closest point to the given point on the given line segment. - @visibleForTesting - static Vector3 getNearestPointOnLine(Vector3 point, Vector3 l1, Vector3 l2) { - final double lengthSquared = math.pow(l2.x - l1.x, 2.0).toDouble() + math.pow(l2.y - l1.y, 2.0).toDouble(); - - // In this case, l1 == l2. - if (lengthSquared == 0) { - return l1; - } - - // Calculate how far down the line segment the closest point is and return - // the point. - final Vector3 l1P = point - l1; - final Vector3 l1L2 = l2 - l1; - final double fraction = clampDouble(l1P.dot(l1L2) / lengthSquared, 0.0, 1.0); - return l1 + l1L2 * fraction; - } - - /// Given a quad, return its axis aligned bounding box. - @visibleForTesting - static Quad getAxisAlignedBoundingBox(Quad quad) { - final double minX = math.min(quad.point0.x, math.min(quad.point1.x, math.min(quad.point2.x, quad.point3.x))); - final double minY = math.min(quad.point0.y, math.min(quad.point1.y, math.min(quad.point2.y, quad.point3.y))); - final double maxX = math.max(quad.point0.x, math.max(quad.point1.x, math.max(quad.point2.x, quad.point3.x))); - final double maxY = math.max(quad.point0.y, math.max(quad.point1.y, math.max(quad.point2.y, quad.point3.y))); - return Quad.points(Vector3(minX, minY, 0), Vector3(maxX, minY, 0), Vector3(maxX, maxY, 0), Vector3(minX, maxY, 0)); - } - - /// Returns true iff the point is inside the rectangle given by the Quad, - /// inclusively. - /// Algorithm from https://math.stackexchange.com/a/190373. - @visibleForTesting - static bool pointIsInside(Vector3 point, Quad quad) { - final Vector3 aM = point - quad.point0; - final Vector3 aB = quad.point1 - quad.point0; - final Vector3 aD = quad.point3 - quad.point0; - - final double aMAB = aM.dot(aB); - final double aBAB = aB.dot(aB); - final double aMAD = aM.dot(aD); - final double aDAD = aD.dot(aD); - - return 0 <= aMAB && aMAB <= aBAB && 0 <= aMAD && aMAD <= aDAD; - } - - /// Get the point inside (inclusively) the given Quad that is nearest to the - /// given Vector3. - @visibleForTesting - static Vector3 getNearestPointInside(Vector3 point, Quad quad) { - // If the point is inside the axis aligned bounding box, then it's ok where - // it is. - if (pointIsInside(point, quad)) { - return point; - } - - // Otherwise, return the nearest point on the quad. - final List closestPoints = [ - InteractiveViewer.getNearestPointOnLine(point, quad.point0, quad.point1), - InteractiveViewer.getNearestPointOnLine(point, quad.point1, quad.point2), - InteractiveViewer.getNearestPointOnLine(point, quad.point2, quad.point3), - InteractiveViewer.getNearestPointOnLine(point, quad.point3, quad.point0), - ]; - double minDistance = double.infinity; - late Vector3 closestOverall; - for (final Vector3 closePoint in closestPoints) { - final double distance = math.sqrt(math.pow(point.x - closePoint.x, 2) + math.pow(point.y - closePoint.y, 2)); - if (distance < minDistance) { - minDistance = distance; - closestOverall = closePoint; - } - } - return closestOverall; - } - - @override - State createState() => _InteractiveViewerState(); -} - -class _InteractiveViewerState extends State with TickerProviderStateMixin { - TransformationController? _transformationController; - - final GlobalKey _childKey = GlobalKey(); - final GlobalKey _parentKey = GlobalKey(); - Animation? _animation; - Animation? _scaleAnimation; - late Offset _scaleAnimationFocalPoint; - late AnimationController _controller; - late AnimationController _scaleController; - Axis? _currentAxis; // Used with panAxis. - Offset? _referenceFocalPoint; // Point where the current gesture began. - double? _scaleStart; // Scale value at start of scaling gesture. - double? _rotationStart = 0.0; // Rotation at start of rotation gesture. - double _currentRotation = 0.0; // Rotation of _transformationController.value. - _GestureType? _gestureType; - - // TODO(justinmc): Add rotateEnabled parameter to the widget and remove this - // hardcoded value when the rotation feature is implemented. - // https://github.com/flutter/flutter/issues/57698 - final bool _rotateEnabled = false; - - // The _boundaryRect is calculated by adding the boundaryMargin to the size of - // the child. - Rect get _boundaryRect { - assert(_childKey.currentContext != null); - assert(!widget.boundaryMargin.left.isNaN); - assert(!widget.boundaryMargin.right.isNaN); - assert(!widget.boundaryMargin.top.isNaN); - assert(!widget.boundaryMargin.bottom.isNaN); - - final RenderBox childRenderBox = _childKey.currentContext!.findRenderObject()! as RenderBox; - final Size childSize = childRenderBox.size; - final Rect boundaryRect = widget.boundaryMargin.inflateRect(Offset.zero & childSize); - assert(!boundaryRect.isEmpty, "InteractiveViewer's child must have nonzero dimensions."); - // Boundaries that are partially infinite are not allowed because Matrix4's - // rotation and translation methods don't handle infinites well. - assert( - boundaryRect.isFinite || - (boundaryRect.left.isInfinite && - boundaryRect.top.isInfinite && - boundaryRect.right.isInfinite && - boundaryRect.bottom.isInfinite), - 'boundaryRect must either be infinite in all directions or finite in all directions.', - ); - return boundaryRect; - } - - // The Rect representing the child's parent. - Rect get _viewport { - assert(_parentKey.currentContext != null); - final RenderBox parentRenderBox = _parentKey.currentContext!.findRenderObject()! as RenderBox; - return Offset.zero & parentRenderBox.size; - } - - // Return a new matrix representing the given matrix after applying the given - // translation. - Matrix4 _matrixTranslate(Matrix4 matrix, Offset translation) { - if (translation == Offset.zero) { - return matrix.clone(); - } - - final Offset alignedTranslation; - - if (_currentAxis != null) { - alignedTranslation = switch (widget.panAxis) { - PanAxis.horizontal => _alignAxis(translation, Axis.horizontal), - PanAxis.vertical => _alignAxis(translation, Axis.vertical), - PanAxis.aligned => _alignAxis(translation, _currentAxis!), - PanAxis.free => translation, - }; - } else { - alignedTranslation = translation; - } - - final Matrix4 nextMatrix = matrix.clone()..translate(alignedTranslation.dx, alignedTranslation.dy); - - // Transform the viewport to determine where its four corners will be after - // the child has been transformed. - final Quad nextViewport = _transformViewport(nextMatrix, _viewport); - - // If the boundaries are infinite, then no need to check if the translation - // fits within them. - if (_boundaryRect.isInfinite) { - return nextMatrix; - } - - // Expand the boundaries with rotation. This prevents the problem where a - // mismatch in orientation between the viewport and boundaries effectively - // limits translation. With this approach, all points that are visible with - // no rotation are visible after rotation. - final Quad boundariesAabbQuad = _getAxisAlignedBoundingBoxWithRotation(_boundaryRect, _currentRotation); - - // If the given translation fits completely within the boundaries, allow it. - final Offset offendingDistance = _exceedsBy(boundariesAabbQuad, nextViewport); - if (offendingDistance == Offset.zero) { - return nextMatrix; - } - - // Desired translation goes out of bounds, so translate to the nearest - // in-bounds point instead. - final Offset nextTotalTranslation = _getMatrixTranslation(nextMatrix); - final double currentScale = matrix.getMaxScaleOnAxis(); - final Offset correctedTotalTranslation = Offset( - nextTotalTranslation.dx - offendingDistance.dx * currentScale, - nextTotalTranslation.dy - offendingDistance.dy * currentScale, - ); - // TODO(justinmc): This needs some work to handle rotation properly. The - // idea is that the boundaries are axis aligned (boundariesAabbQuad), but - // calculating the translation to put the viewport inside that Quad is more - // complicated than this when rotated. - // https://github.com/flutter/flutter/issues/57698 - final Matrix4 correctedMatrix = - matrix.clone()..setTranslation(Vector3(correctedTotalTranslation.dx, correctedTotalTranslation.dy, 0.0)); - - // Double check that the corrected translation fits. - final Quad correctedViewport = _transformViewport(correctedMatrix, _viewport); - final Offset offendingCorrectedDistance = _exceedsBy(boundariesAabbQuad, correctedViewport); - if (offendingCorrectedDistance == Offset.zero) { - return correctedMatrix; - } - - // If the corrected translation doesn't fit in either direction, don't allow - // any translation at all. This happens when the viewport is larger than the - // entire boundary. - if (offendingCorrectedDistance.dx != 0.0 && offendingCorrectedDistance.dy != 0.0) { - return matrix.clone(); - } - - // Otherwise, allow translation in only the direction that fits. This - // happens when the viewport is larger than the boundary in one direction. - final Offset unidirectionalCorrectedTotalTranslation = Offset( - offendingCorrectedDistance.dx == 0.0 ? correctedTotalTranslation.dx : 0.0, - offendingCorrectedDistance.dy == 0.0 ? correctedTotalTranslation.dy : 0.0, - ); - return matrix.clone()..setTranslation( - Vector3(unidirectionalCorrectedTotalTranslation.dx, unidirectionalCorrectedTotalTranslation.dy, 0.0), - ); - } - - // Return a new matrix representing the given matrix after applying the given - // scale. - Matrix4 _matrixScale(Matrix4 matrix, double scale) { - if (scale == 1.0) { - return matrix.clone(); - } - assert(scale != 0.0); - - // Don't allow a scale that results in an overall scale beyond min/max - // scale. - final double currentScale = _transformationController!.value.getMaxScaleOnAxis(); - final double totalScale = math.max( - currentScale * scale, - // Ensure that the scale cannot make the child so big that it can't fit - // inside the boundaries (in either direction). - math.max(_viewport.width / _boundaryRect.width, _viewport.height / _boundaryRect.height), - ); - final double clampedTotalScale = clampDouble(totalScale, widget.minScale, widget.maxScale); - final double clampedScale = clampedTotalScale / currentScale; - return matrix.clone()..scale(clampedScale); - } - - // Return a new matrix representing the given matrix after applying the given - // rotation. - Matrix4 _matrixRotate(Matrix4 matrix, double rotation, Offset focalPoint) { - if (rotation == 0) { - return matrix.clone(); - } - final Offset focalPointScene = _transformationController!.toScene(focalPoint); - return matrix.clone() - ..translate(focalPointScene.dx, focalPointScene.dy) - ..rotateZ(-rotation) - ..translate(-focalPointScene.dx, -focalPointScene.dy); - } - - // Returns true iff the given _GestureType is enabled. - bool _gestureIsSupported(_GestureType? gestureType) { - return switch (gestureType) { - _GestureType.rotate => _rotateEnabled, - _GestureType.scale => widget.scaleEnabled, - _GestureType.pan || null => widget.panEnabled, - }; - } - - // Decide which type of gesture this is by comparing the amount of scale - // and rotation in the gesture, if any. Scale starts at 1 and rotation - // starts at 0. Pan will have no scale and no rotation because it uses only one - // finger. - _GestureType _getGestureType(ScaleUpdateDetails details) { - final double scale = !widget.scaleEnabled ? 1.0 : details.scale; - final double rotation = !_rotateEnabled ? 0.0 : details.rotation; - if ((scale - 1).abs() > rotation.abs()) { - return _GestureType.scale; - } else if (rotation != 0.0) { - return _GestureType.rotate; - } else { - return _GestureType.pan; - } - } - - // Handle the start of a gesture. All of pan, scale, and rotate are handled - // with GestureDetector's scale gesture. - void _onScaleStart(ScaleStartDetails details) { - widget.onInteractionStart?.call(details); - - if (_controller.isAnimating) { - _controller.stop(); - _controller.reset(); - _animation?.removeListener(_onAnimate); - _animation = null; - } - if (_scaleController.isAnimating) { - _scaleController.stop(); - _scaleController.reset(); - _scaleAnimation?.removeListener(_onScaleAnimate); - _scaleAnimation = null; - } - - _gestureType = null; - _currentAxis = null; - _scaleStart = _transformationController!.value.getMaxScaleOnAxis(); - _referenceFocalPoint = _transformationController!.toScene(details.localFocalPoint); - _rotationStart = _currentRotation; - } - - // Handle an update to an ongoing gesture. All of pan, scale, and rotate are - // handled with GestureDetector's scale gesture. - void _onScaleUpdate(ScaleUpdateDetails details) { - final double scale = _transformationController!.value.getMaxScaleOnAxis(); - _scaleAnimationFocalPoint = details.localFocalPoint; - final Offset focalPointScene = _transformationController!.toScene(details.localFocalPoint); - - if (_gestureType == _GestureType.pan) { - // When a gesture first starts, it sometimes has no change in scale and - // rotation despite being a two-finger gesture. Here the gesture is - // allowed to be reinterpreted as its correct type after originally - // being marked as a pan. - _gestureType = _getGestureType(details); - } else { - _gestureType ??= _getGestureType(details); - } - if (!_gestureIsSupported(_gestureType)) { - widget.onInteractionUpdate?.call(details); - return; - } - - switch (_gestureType!) { - case _GestureType.scale: - assert(_scaleStart != null); - // details.scale gives us the amount to change the scale as of the - // start of this gesture, so calculate the amount to scale as of the - // previous call to _onScaleUpdate. - final double desiredScale = _scaleStart! * details.scale; - final double scaleChange = desiredScale / scale; - _transformationController!.value = _matrixScale(_transformationController!.value, scaleChange); - - // While scaling, translate such that the user's two fingers stay on - // the same places in the scene. That means that the focal point of - // the scale should be on the same place in the scene before and after - // the scale. - final Offset focalPointSceneScaled = _transformationController!.toScene(details.localFocalPoint); - _transformationController!.value = _matrixTranslate( - _transformationController!.value, - focalPointSceneScaled - _referenceFocalPoint!, - ); - - // details.localFocalPoint should now be at the same location as the - // original _referenceFocalPoint point. If it's not, that's because - // the translate came in contact with a boundary. In that case, update - // _referenceFocalPoint so subsequent updates happen in relation to - // the new effective focal point. - final Offset focalPointSceneCheck = _transformationController!.toScene(details.localFocalPoint); - if (_round(_referenceFocalPoint!) != _round(focalPointSceneCheck)) { - _referenceFocalPoint = focalPointSceneCheck; - } - - case _GestureType.rotate: - if (details.rotation == 0.0) { - widget.onInteractionUpdate?.call(details); - return; - } - final double desiredRotation = _rotationStart! + details.rotation; - _transformationController!.value = _matrixRotate( - _transformationController!.value, - _currentRotation - desiredRotation, - details.localFocalPoint, - ); - _currentRotation = desiredRotation; - - case _GestureType.pan: - assert(_referenceFocalPoint != null); - // details may have a change in scale here when scaleEnabled is false. - // In an effort to keep the behavior similar whether or not scaleEnabled - // is true, these gestures are thrown away. - if (details.scale != 1.0) { - widget.onInteractionUpdate?.call(details); - return; - } - _currentAxis ??= _getPanAxis(_referenceFocalPoint!, focalPointScene); - // Translate so that the same point in the scene is underneath the - // focal point before and after the movement. - final Offset translationChange = focalPointScene - _referenceFocalPoint!; - _transformationController!.value = _matrixTranslate(_transformationController!.value, translationChange); - _referenceFocalPoint = _transformationController!.toScene(details.localFocalPoint); - } - widget.onInteractionUpdate?.call(details); - } - - // Handle the end of a gesture of _GestureType. All of pan, scale, and rotate - // are handled with GestureDetector's scale gesture. - void _onScaleEnd(ScaleEndDetails details) { - widget.onInteractionEnd?.call(details); - _scaleStart = null; - _rotationStart = null; - _referenceFocalPoint = null; - - _animation?.removeListener(_onAnimate); - _scaleAnimation?.removeListener(_onScaleAnimate); - _controller.reset(); - _scaleController.reset(); - - if (!_gestureIsSupported(_gestureType)) { - _currentAxis = null; - return; - } - - switch (_gestureType) { - case _GestureType.pan: - if (details.velocity.pixelsPerSecond.distance < kMinFlingVelocity) { - _currentAxis = null; - return; - } - final Vector3 translationVector = _transformationController!.value.getTranslation(); - final Offset translation = Offset(translationVector.x, translationVector.y); - final FrictionSimulation frictionSimulationX = FrictionSimulation( - widget.interactionEndFrictionCoefficient, - translation.dx, - details.velocity.pixelsPerSecond.dx, - ); - final FrictionSimulation frictionSimulationY = FrictionSimulation( - widget.interactionEndFrictionCoefficient, - translation.dy, - details.velocity.pixelsPerSecond.dy, - ); - final double tFinal = _getFinalTime( - details.velocity.pixelsPerSecond.distance, - widget.interactionEndFrictionCoefficient, - ); - _animation = Tween( - begin: translation, - end: Offset(frictionSimulationX.finalX, frictionSimulationY.finalX), - ).animate(CurvedAnimation(parent: _controller, curve: Curves.decelerate)); - _controller.duration = Duration(milliseconds: (tFinal * 1000).round()); - _animation!.addListener(_onAnimate); - _controller.forward(); - case _GestureType.scale: - if (details.scaleVelocity.abs() < 0.1) { - _currentAxis = null; - return; - } - final double scale = _transformationController!.value.getMaxScaleOnAxis(); - final FrictionSimulation frictionSimulation = FrictionSimulation( - widget.interactionEndFrictionCoefficient * widget.scaleFactor, - scale, - details.scaleVelocity / 10, - ); - final double tFinal = _getFinalTime( - details.scaleVelocity.abs(), - widget.interactionEndFrictionCoefficient, - effectivelyMotionless: 0.1, - ); - _scaleAnimation = Tween( - begin: scale, - end: frictionSimulation.x(tFinal), - ).animate(CurvedAnimation(parent: _scaleController, curve: Curves.decelerate)); - _scaleController.duration = Duration(milliseconds: (tFinal * 1000).round()); - _scaleAnimation!.addListener(_onScaleAnimate); - _scaleController.forward(); - case _GestureType.rotate || null: - break; - } - } - - // Handle mousewheel and web trackpad scroll events. - void _receivedPointerSignal(PointerSignalEvent event) { - final double scaleChange; - if (event is PointerScrollEvent) { - if (event.kind == PointerDeviceKind.trackpad && !widget.trackpadScrollCausesScale) { - // Trackpad scroll, so treat it as a pan. - widget.onInteractionStart?.call( - ScaleStartDetails(focalPoint: event.position, localFocalPoint: event.localPosition), - ); - - final Offset localDelta = PointerEvent.transformDeltaViaPositions( - untransformedEndPosition: event.position + event.scrollDelta, - untransformedDelta: event.scrollDelta, - transform: event.transform, - ); - - if (!_gestureIsSupported(_GestureType.pan)) { - widget.onInteractionUpdate?.call( - ScaleUpdateDetails( - focalPoint: event.position - event.scrollDelta, - localFocalPoint: event.localPosition - event.scrollDelta, - focalPointDelta: -localDelta, - ), - ); - widget.onInteractionEnd?.call(ScaleEndDetails()); - return; - } - - final Offset focalPointScene = _transformationController!.toScene(event.localPosition); - - final Offset newFocalPointScene = _transformationController!.toScene(event.localPosition - localDelta); - - _transformationController!.value = _matrixTranslate( - _transformationController!.value, - newFocalPointScene - focalPointScene, - ); - - widget.onInteractionUpdate?.call( - ScaleUpdateDetails( - focalPoint: event.position - event.scrollDelta, - localFocalPoint: event.localPosition - localDelta, - focalPointDelta: -localDelta, - ), - ); - widget.onInteractionEnd?.call(ScaleEndDetails()); - return; - } - - // We can handle mouse-wheel event here for our own purposes - if (widget.onWheelDelta != null) { - widget.onWheelDelta!(event.scrollDelta); - return; - } - - // Ignore left and right mouse wheel scroll. - if (event.scrollDelta.dy == 0.0) { - return; - } - scaleChange = math.exp(-event.scrollDelta.dy / widget.scaleFactor); - } else if (event is PointerScaleEvent) { - scaleChange = event.scale; - } else { - return; - } - widget.onInteractionStart?.call( - ScaleStartDetails(focalPoint: event.position, localFocalPoint: event.localPosition), - ); - - if (!_gestureIsSupported(_GestureType.scale)) { - widget.onInteractionUpdate?.call( - ScaleUpdateDetails(focalPoint: event.position, localFocalPoint: event.localPosition, scale: scaleChange), - ); - widget.onInteractionEnd?.call(ScaleEndDetails()); - return; - } - - final Offset focalPointScene = _transformationController!.toScene(event.localPosition); - - _transformationController!.value = _matrixScale(_transformationController!.value, scaleChange); - - // After scaling, translate such that the event's position is at the - // same scene point before and after the scale. - final Offset focalPointSceneScaled = _transformationController!.toScene(event.localPosition); - _transformationController!.value = _matrixTranslate( - _transformationController!.value, - focalPointSceneScaled - focalPointScene, - ); - - widget.onInteractionUpdate?.call( - ScaleUpdateDetails(focalPoint: event.position, localFocalPoint: event.localPosition, scale: scaleChange), - ); - widget.onInteractionEnd?.call(ScaleEndDetails()); - } - - // Handle inertia drag animation. - void _onAnimate() { - if (!_controller.isAnimating) { - _currentAxis = null; - _animation?.removeListener(_onAnimate); - _animation = null; - _controller.reset(); - return; - } - // Translate such that the resulting translation is _animation.value. - final Vector3 translationVector = _transformationController!.value.getTranslation(); - final Offset translation = Offset(translationVector.x, translationVector.y); - final Offset translationScene = _transformationController!.toScene(translation); - final Offset animationScene = _transformationController!.toScene(_animation!.value); - final Offset translationChangeScene = animationScene - translationScene; - _transformationController!.value = _matrixTranslate(_transformationController!.value, translationChangeScene); - } - - // Handle inertia scale animation. - void _onScaleAnimate() { - if (!_scaleController.isAnimating) { - _currentAxis = null; - _scaleAnimation?.removeListener(_onScaleAnimate); - _scaleAnimation = null; - _scaleController.reset(); - return; - } - final double desiredScale = _scaleAnimation!.value; - final double scaleChange = desiredScale / _transformationController!.value.getMaxScaleOnAxis(); - final Offset referenceFocalPoint = _transformationController!.toScene(_scaleAnimationFocalPoint); - _transformationController!.value = _matrixScale(_transformationController!.value, scaleChange); - - // While scaling, translate such that the user's two fingers stay on - // the same places in the scene. That means that the focal point of - // the scale should be on the same place in the scene before and after - // the scale. - final Offset focalPointSceneScaled = _transformationController!.toScene(_scaleAnimationFocalPoint); - _transformationController!.value = _matrixTranslate( - _transformationController!.value, - focalPointSceneScaled - referenceFocalPoint, - ); - } - - void _onTransformationControllerChange() { - // A change to the TransformationController's value is a change to the - // state. - setState(() {}); - } - - @override - void initState() { - super.initState(); - - _transformationController = widget.transformationController ?? TransformationController(); - _transformationController!.addListener(_onTransformationControllerChange); - _controller = AnimationController(vsync: this); - _scaleController = AnimationController(vsync: this); - } - - @override - void didUpdateWidget(InteractiveViewer oldWidget) { - super.didUpdateWidget(oldWidget); - // Handle all cases of needing to dispose and initialize - // transformationControllers. - if (oldWidget.transformationController == null) { - if (widget.transformationController != null) { - _transformationController!.removeListener(_onTransformationControllerChange); - _transformationController!.dispose(); - _transformationController = widget.transformationController; - _transformationController!.addListener(_onTransformationControllerChange); - } - } else { - if (widget.transformationController == null) { - _transformationController!.removeListener(_onTransformationControllerChange); - _transformationController = TransformationController(); - _transformationController!.addListener(_onTransformationControllerChange); - } else if (widget.transformationController != oldWidget.transformationController) { - _transformationController!.removeListener(_onTransformationControllerChange); - _transformationController = widget.transformationController; - _transformationController!.addListener(_onTransformationControllerChange); - } - } - } - - @override - void dispose() { - _controller.dispose(); - _scaleController.dispose(); - _transformationController!.removeListener(_onTransformationControllerChange); - if (widget.transformationController == null) { - _transformationController!.dispose(); - } - super.dispose(); - } - - @override - Widget build(BuildContext context) { - Widget child; - if (widget.child != null) { - child = _InteractiveViewerBuilt( - childKey: _childKey, - clipBehavior: widget.clipBehavior, - constrained: widget.constrained, - matrix: _transformationController!.value, - alignment: widget.alignment, - child: widget.child!, - ); - } else { - // When using InteractiveViewer.builder, then constrained is false and the - // viewport is the size of the constraints. - assert(widget.builder != null); - assert(!widget.constrained); - child = LayoutBuilder( - builder: (context, constraints) { - final Matrix4 matrix = _transformationController!.value; - return _InteractiveViewerBuilt( - childKey: _childKey, - clipBehavior: widget.clipBehavior, - constrained: widget.constrained, - alignment: widget.alignment, - matrix: matrix, - child: widget.builder!(context, _transformViewport(matrix, Offset.zero & constraints.biggest)), - ); - }, - ); - } - - return Listener( - key: _parentKey, - onPointerSignal: _receivedPointerSignal, - child: GestureDetector( - behavior: HitTestBehavior.opaque, // Necessary when panning off screen. - onScaleEnd: _onScaleEnd, - onScaleStart: _onScaleStart, - onScaleUpdate: _onScaleUpdate, - trackpadScrollCausesScale: widget.trackpadScrollCausesScale, - trackpadScrollToScaleFactor: Offset(0, -1 / widget.scaleFactor), - child: child, - ), - ); - } -} - -// This widget allows us to easily swap in and out the LayoutBuilder in -// InteractiveViewer's depending on if it's using a builder or a child. -class _InteractiveViewerBuilt extends StatelessWidget { - const _InteractiveViewerBuilt({ - required this.child, - required this.childKey, - required this.clipBehavior, - required this.constrained, - required this.matrix, - required this.alignment, - }); - - final Widget child; - final GlobalKey childKey; - final Clip clipBehavior; - final bool constrained; - final Matrix4 matrix; - final Alignment? alignment; - - @override - Widget build(BuildContext context) { - Widget child = Transform( - transform: matrix, - alignment: alignment, - child: KeyedSubtree(key: childKey, child: this.child), - ); - - if (!constrained) { - child = OverflowBox( - alignment: Alignment.topLeft, - minWidth: 0.0, - minHeight: 0.0, - maxWidth: double.infinity, - maxHeight: double.infinity, - child: child, - ); - } - - return ClipRect(clipBehavior: clipBehavior, child: child); - } -} - -// A classification of relevant user gestures. Each contiguous user gesture is -// represented by exactly one _GestureType. -enum _GestureType { pan, scale, rotate } - -// Given a velocity and drag, calculate the time at which motion will come to -// a stop, within the margin of effectivelyMotionless. -double _getFinalTime(double velocity, double drag, {double effectivelyMotionless = 10}) { - return math.log(effectivelyMotionless / velocity) / math.log(drag / 100); -} - -// Return the translation from the given Matrix4 as an Offset. -Offset _getMatrixTranslation(Matrix4 matrix) { - final Vector3 nextTranslation = matrix.getTranslation(); - return Offset(nextTranslation.x, nextTranslation.y); -} - -// Transform the four corners of the viewport by the inverse of the given -// matrix. This gives the viewport after the child has been transformed by the -// given matrix. The viewport transforms as the inverse of the child (i.e. -// moving the child left is equivalent to moving the viewport right). -Quad _transformViewport(Matrix4 matrix, Rect viewport) { - final Matrix4 inverseMatrix = matrix.clone()..invert(); - return Quad.points( - inverseMatrix.transform3(Vector3(viewport.topLeft.dx, viewport.topLeft.dy, 0.0)), - inverseMatrix.transform3(Vector3(viewport.topRight.dx, viewport.topRight.dy, 0.0)), - inverseMatrix.transform3(Vector3(viewport.bottomRight.dx, viewport.bottomRight.dy, 0.0)), - inverseMatrix.transform3(Vector3(viewport.bottomLeft.dx, viewport.bottomLeft.dy, 0.0)), - ); -} - -// Find the axis aligned bounding box for the rect rotated about its center by -// the given amount. -Quad _getAxisAlignedBoundingBoxWithRotation(Rect rect, double rotation) { - final Matrix4 rotationMatrix = - Matrix4.identity() - ..translate(rect.size.width / 2, rect.size.height / 2) - ..rotateZ(rotation) - ..translate(-rect.size.width / 2, -rect.size.height / 2); - final Quad boundariesRotated = Quad.points( - rotationMatrix.transform3(Vector3(rect.left, rect.top, 0.0)), - rotationMatrix.transform3(Vector3(rect.right, rect.top, 0.0)), - rotationMatrix.transform3(Vector3(rect.right, rect.bottom, 0.0)), - rotationMatrix.transform3(Vector3(rect.left, rect.bottom, 0.0)), - ); - return InteractiveViewer.getAxisAlignedBoundingBox(boundariesRotated); -} - -// Return the amount that viewport lies outside of boundary. If the viewport -// is completely contained within the boundary (inclusively), then returns -// Offset.zero. -Offset _exceedsBy(Quad boundary, Quad viewport) { - final List viewportPoints = [viewport.point0, viewport.point1, viewport.point2, viewport.point3]; - Offset largestExcess = Offset.zero; - for (final Vector3 point in viewportPoints) { - final Vector3 pointInside = InteractiveViewer.getNearestPointInside(point, boundary); - final Offset excess = Offset(pointInside.x - point.x, pointInside.y - point.y); - if (excess.dx.abs() > largestExcess.dx.abs()) { - largestExcess = Offset(excess.dx, largestExcess.dy); - } - if (excess.dy.abs() > largestExcess.dy.abs()) { - largestExcess = Offset(largestExcess.dx, excess.dy); - } - } - - return _round(largestExcess); -} - -// Round the output values. This works around a precision problem where -// values that should have been zero were given as within 10^-10 of zero. -Offset _round(Offset offset) { - return Offset(double.parse(offset.dx.toStringAsFixed(9)), double.parse(offset.dy.toStringAsFixed(9))); -} - -// Align the given offset to the given axis by allowing movement only in the -// axis direction. -Offset _alignAxis(Offset offset, Axis axis) { - return switch (axis) { - Axis.horizontal => Offset(offset.dx, 0.0), - Axis.vertical => Offset(0.0, offset.dy), - }; -} - -// Given two points, return the axis where the distance between the points is -// greatest. If they are equal, return null. -Axis? _getPanAxis(Offset point1, Offset point2) { - if (point1 == point2) { - return null; - } - final double x = point2.dx - point1.dx; - final double y = point2.dy - point1.dy; - return x.abs() > y.abs() ? Axis.horizontal : Axis.vertical; -} diff --git a/lib/src/widgets/pdf_page_text_overlay.dart b/lib/src/widgets/pdf_page_text_overlay.dart deleted file mode 100644 index 5a123904..00000000 --- a/lib/src/widgets/pdf_page_text_overlay.dart +++ /dev/null @@ -1,694 +0,0 @@ -import 'dart:collection'; - -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; - -import '../../pdfrx.dart'; -import '../utils/double_extensions.dart'; - -/// Function to be notified when the text selection is changed. -/// -/// [selection] is the selected text ranges. -/// If page selection is cleared on page dispose (it means, the page is scrolled out of the view), [selection] is null. -/// Otherwise, [selection] is the selected text ranges. If no selection is made, [selection] is an empty list. -typedef PdfViewerPageTextSelectionChangeCallback = void Function(PdfTextRanges selection); - -/// A widget that displays selectable text on a page. -/// -/// If [PdfDocument.permissions] does not allow copying, the widget does not show anything. -class PdfPageTextOverlay extends StatefulWidget { - const PdfPageTextOverlay({ - required this.selectables, - required this.page, - required this.pageRect, - required this.selectionColor, - required this.enabled, - this.textCursor = SystemMouseCursors.text, - this.onTextSelectionChange, - super.key, - }); - - final SplayTreeMap selectables; - final bool enabled; - final PdfPage page; - final Rect pageRect; - final PdfViewerPageTextSelectionChangeCallback? onTextSelectionChange; - final Color selectionColor; - final MouseCursor textCursor; - - @override - State createState() => _PdfPageTextOverlayState(); - - /// Whether to show debug information. - static bool isDebug = false; -} - -class _PdfPageTextOverlayState extends State { - PdfPageText? _pageText; - List? fragments; - bool selectionShouldBeEnabled = false; - - @override - void initState() { - super.initState(); - _initText(); - } - - @override - void didUpdateWidget(PdfPageTextOverlay oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.page != oldWidget.page) { - _initText(); - } - } - - @override - void dispose() { - _release(); - - super.dispose(); - } - - void _release() { - if (_pageText != null) { - _notifySelectionChange(PdfTextRanges.createEmpty(_pageText!)); - } - } - - void _notifySelectionChange(PdfTextRanges ranges) { - widget.onTextSelectionChange?.call(ranges); - } - - Future _initText() async { - _release(); - final pageText = _pageText = await widget.page.loadText(); - final fragments = []; - if (pageText.fragments.isNotEmpty) { - double y = pageText.fragments[0].bounds.bottom; - int start = 0; - for (int i = 1; i < pageText.fragments.length; i++) { - final fragment = pageText.fragments[i]; - if (!fragment.bounds.bottom.isAlmostIdentical(y, error: .25)) { - fragments.addAll(pageText.fragments.sublist(start, i)); - y = fragment.bounds.bottom; - start = i; - } - } - if (start < pageText.fragments.length) { - fragments.addAll(pageText.fragments.sublist(start, pageText.fragments.length)); - } - } - this.fragments = fragments; - if (mounted) { - setState(() {}); - } - } - - @override - Widget build(BuildContext context) { - if (fragments == null || fragments!.isEmpty || widget.page.document.permissions?.allowsCopying == false) { - return const SizedBox(); - } - final registrar = SelectionContainer.maybeOf(context); - return MouseRegion( - hitTestBehavior: HitTestBehavior.translucent, - cursor: selectionShouldBeEnabled ? widget.textCursor : MouseCursor.defer, - onHover: _onHover, - child: IgnorePointer( - ignoring: !(selectionShouldBeEnabled || _anySelections), - child: _PdfTextWidget(registrar, this), - ), - ); - } - - bool get _anySelections { - if (_pageText == null) return false; - final pageSelection = widget.selectables[_pageText!.pageNumber]; - return pageSelection != null && pageSelection.value.hasSelection; - } - - void _onHover(PointerHoverEvent event) { - final point = event.localPosition.toPdfPoint(widget.page, widget.pageRect); - - final selectionShouldBeEnabled = isPointOnText(point); - if (this.selectionShouldBeEnabled != selectionShouldBeEnabled) { - this.selectionShouldBeEnabled = selectionShouldBeEnabled; - if (mounted) { - setState(() {}); - } - } - } - - bool isPointOnText(Offset point, {double margin = 5}) { - for (final fragment in fragments!) { - if (pdfRectContains(fragment.bounds, point, margin)) { - return true; - } - } - return false; - } - - static bool pdfRectContains(PdfRect rect, Offset point, double margin) { - return rect.left - margin <= point.dx && - rect.right + margin >= point.dx && - rect.bottom - margin <= point.dy && - rect.top + margin >= point.dy; - } -} - -extension _OffsetExt on Offset { - Offset toPdfPoint(PdfPage page, Rect pageRect) { - final scale = page.height / pageRect.height; - return Offset(dx * scale, page.height - dy * scale); - } -} - -/// The code is based on the code on [Making a widget selectable](https://api.flutter.dev/flutter/widgets/SelectableRegion-class.html#widgets).SelectableRegion.2] -class _PdfTextWidget extends LeafRenderObjectWidget { - const _PdfTextWidget(this._registrar, this._state); - - final SelectionRegistrar? _registrar; - - final _PdfPageTextOverlayState _state; - - @override - RenderObject createRenderObject(BuildContext context) { - final selectable = _PdfTextRenderBox(_state.widget.selectionColor, this); - _state.widget.selectables[_state._pageText!.pageNumber] = selectable; - return selectable; - } - - @override - void updateRenderObject(BuildContext context, _PdfTextRenderBox renderObject) { - renderObject - ..selectionColor = _state.widget.selectionColor - ..registrar = _registrar; - _state.widget.selectables[_state._pageText!.pageNumber] = renderObject; - } -} - -mixin PdfPageTextSelectable implements Selectable { - PdfTextRanges get selectedRanges; -} - -/// The code is based on the code on [Making a widget selectable](https://api.flutter.dev/flutter/widgets/SelectableRegion-class.html#widgets).SelectableRegion.2] -class _PdfTextRenderBox extends RenderBox with PdfPageTextSelectable, Selectable, SelectionRegistrant { - _PdfTextRenderBox(this._selectionColor, this._textWidget) - : _geometry = ValueNotifier(_noSelection) { - registrar = _textWidget._registrar; - _geometry.addListener(markNeedsPaint); - } - - final _PdfTextWidget _textWidget; - - static const SelectionGeometry _noSelection = SelectionGeometry(status: SelectionStatus.none, hasContent: true); - - final ValueNotifier _geometry; - - Color _selectionColor; - Color get selectionColor => _selectionColor; - set selectionColor(Color value) { - if (_selectionColor == value) return; - _selectionColor = value; - markNeedsPaint(); - } - - @override - void dispose() { - _geometry.dispose(); - super.dispose(); - } - - Rect get _pageRect => _textWidget._state.widget.pageRect; - PdfPage get _page => _textWidget._state.widget.page; - List get _fragments => _textWidget._state.fragments!; - - @override - late final List boundingBoxes = _fragments - .map((f) => f.bounds.toRect(page: _page, scaledPageSize: size)) - .toList(growable: false); - - @override - bool hitTestSelf(Offset position) { - final point = position.toPdfPoint(_page, _pageRect); - return _textWidget._state.isPointOnText(point); - } - - @override - bool get sizedByParent => true; - @override - double computeMinIntrinsicWidth(double height) => _pageRect.size.width; - @override - double computeMaxIntrinsicWidth(double height) => _pageRect.size.width; - @override - double computeMinIntrinsicHeight(double width) => _pageRect.size.height; - @override - double computeMaxIntrinsicHeight(double width) => _pageRect.size.height; - @override - Size computeDryLayout(BoxConstraints constraints) => constraints.constrain(_pageRect.size); - - @override - void addListener(VoidCallback listener) => _geometry.addListener(listener); - - @override - void removeListener(VoidCallback listener) => _geometry.removeListener(listener); - - @override - SelectionGeometry get value => _geometry.value; - - Rect _getSelectionHighlightRect() => Offset.zero & size; - - Offset? _start; - Offset? _end; - String? _selectedText; - Rect? _selectedRect; - Size? _sizeOnSelection; - late PdfTextRanges _selectedRanges = PdfTextRanges.createEmpty(_textWidget._state._pageText!); - - @override - PdfTextRanges get selectedRanges => _selectedRanges; - - void _notifySelectionChange() { - _textWidget._state._notifySelectionChange(_selectedRanges); - } - - void _updateGeometry() { - _updateGeometryInternal(); - _notifySelectionChange(); - } - - void _updateGeometryInternal() { - _selectedText = null; - _selectedRect = null; - _selectedRanges = PdfTextRanges.createEmpty(_textWidget._state._pageText!); - - if (_start == null || _end == null) { - _geometry.value = _noSelection; - return; - } - - final renderObjectRect = Rect.fromLTWH(0, 0, size.width, size.height); - var selectionRect = Rect.fromPoints(_start!, _end!); - if (renderObjectRect.intersect(selectionRect).isEmpty) { - _geometry.value = _noSelection; - return; - } - selectionRect = !selectionRect.isEmpty ? selectionRect : _getSelectionHighlightRect(); - - final selectionRects = []; - final sb = StringBuffer(); - - int searchLineEnd(int start) { - final lastIndex = _fragments.length - 1; - var last = _fragments[start]; - for (int i = start; i < lastIndex; i++) { - final next = _fragments[i + 1]; - if (last.bounds.bottom != next.bounds.bottom) { - return i + 1; - } - last = next; - } - return _fragments.length; - } - - Iterable<({Rect rect, String text, PdfTextRange range})> enumerateCharRects(int start, int end) sync* { - for (int i = start; i < end; i++) { - final fragment = _fragments[i]; - if (fragment.charRects == null) { - yield ( - rect: fragment.bounds.toRect(page: _page, scaledPageSize: size), - text: fragment.text, - range: PdfTextRange(start: fragment.index, end: fragment.end), - ); - } else { - for (int j = 0; j < fragment.charRects!.length; j++) { - yield ( - rect: fragment.charRects![j].toRect(page: _page, scaledPageSize: size), - text: fragment.text.substring(j, j + 1), - range: PdfTextRange(start: fragment.index + j, end: fragment.index + j + 1), - ); - } - } - } - } - - ({Rect? rect, String text, List ranges}) selectChars(int start, int end, Rect lineSelectRect) { - Rect? rect; - final ranges = []; - final sb = StringBuffer(); - for (final r in enumerateCharRects(start, end)) { - if (!r.rect.intersect(lineSelectRect).isEmpty || r.rect.bottom < lineSelectRect.bottom) { - sb.write(r.text); - ranges.appendRange(r.range); - if (rect == null) { - rect = r.rect; - } else { - rect = rect.expandToInclude(r.rect); - } - } - } - return (rect: rect, text: sb.toString(), ranges: ranges); - } - - int? lastLineEnd; - Rect? lastLineStartRect; - for (int i = 0; i < _fragments.length;) { - final bounds = _fragments[i].bounds.toRect(page: _page, scaledPageSize: size); - final intersects = !selectionRect.intersect(bounds).isEmpty; - if (intersects) { - final lineEnd = searchLineEnd(i); - final chars = selectChars( - lastLineEnd ?? i, - lineEnd, - lastLineStartRect != null ? lastLineStartRect.expandToInclude(selectionRect) : selectionRect, - ); - lastLineStartRect = bounds; - lastLineEnd = i = lineEnd; - if (chars.rect == null) continue; - sb.write(chars.text); - selectionRects.add(chars.rect!); - _selectedRanges.ranges.appendAllRanges(chars.ranges); - } else { - i++; - } - } - if (selectionRects.isEmpty) { - _geometry.value = _noSelection; - return; - } - - final selectedBounds = selectionRects.reduce((a, b) => a.expandToInclude(b)); - _selectedRect = Rect.fromLTRB( - _start?.dx ?? selectedBounds.left, - _start?.dy ?? selectedBounds.top, - _end?.dx ?? selectedBounds.right, - _end?.dy ?? selectedBounds.bottom, - ); - _selectedText = sb.toString(); - - final first = selectionRects.first; - final firstSelectionPoint = SelectionPoint( - localPosition: first.bottomLeft, - lineHeight: first.height, - handleType: TextSelectionHandleType.left, - ); - final last = selectionRects.last; - final secondSelectionPoint = SelectionPoint( - localPosition: last.bottomRight, - lineHeight: last.height, - handleType: TextSelectionHandleType.right, - ); - final bool isReversed; - if (_start!.dy > _end!.dy) { - isReversed = true; - } else if (_start!.dy < _end!.dy) { - isReversed = false; - } else { - isReversed = _start!.dx > _end!.dx; - } - - _sizeOnSelection = size; - _geometry.value = SelectionGeometry( - status: _selectedText!.isNotEmpty ? SelectionStatus.uncollapsed : SelectionStatus.collapsed, - hasContent: true, - startSelectionPoint: isReversed ? secondSelectionPoint : firstSelectionPoint, - endSelectionPoint: isReversed ? firstSelectionPoint : secondSelectionPoint, - selectionRects: selectionRects, - ); - } - - void _selectFragment(Offset point) { - _selectedRanges = PdfTextRanges.createEmpty(_textWidget._state._pageText!); - for (final fragment in _fragments) { - final bounds = fragment.bounds.toRect(page: _page, scaledPageSize: size); - if (bounds.contains(point)) { - _start = bounds.topLeft; - _end = bounds.bottomRight; - _selectedRect = bounds; - _selectedText = fragment.text; - _sizeOnSelection = size; - _geometry.value = SelectionGeometry( - status: _selectedText!.isNotEmpty ? SelectionStatus.uncollapsed : SelectionStatus.collapsed, - hasContent: true, - startSelectionPoint: SelectionPoint( - localPosition: _selectedRect!.bottomLeft, - lineHeight: _selectedRect!.height, - handleType: TextSelectionHandleType.left, - ), - endSelectionPoint: SelectionPoint( - localPosition: _selectedRect!.bottomRight, - lineHeight: _selectedRect!.height, - handleType: TextSelectionHandleType.right, - ), - selectionRects: [bounds], - ); - _selectedRanges.ranges.appendRange(PdfTextRange(start: fragment.index, end: fragment.end)); - return; - } - } - _geometry.value = _noSelection; - } - - @override - SelectionResult dispatchSelectionEvent(SelectionEvent event) { - if (!_textWidget._state.widget.enabled) { - return SelectionResult.none; - } - - var result = SelectionResult.none; - switch (event.type) { - case SelectionEventType.startEdgeUpdate: - case SelectionEventType.endEdgeUpdate: - final renderObjectRect = Rect.fromLTWH(0, 0, size.width, size.height); - final point = globalToLocal((event as SelectionEdgeUpdateEvent).globalPosition); - final adjustedPoint = SelectionUtils.adjustDragOffset(renderObjectRect, point); - if (event.type == SelectionEventType.startEdgeUpdate) { - _start = adjustedPoint; - } else { - _end = adjustedPoint; - } - result = SelectionUtils.getResultBasedOnRect(renderObjectRect, point); - break; - case SelectionEventType.clear: - _start = _end = null; - case SelectionEventType.selectAll: - _start = Offset.zero; - _end = Offset.infinite; - case SelectionEventType.selectWord: - _selectFragment(globalToLocal((event as SelectWordSelectionEvent).globalPosition)); - _notifySelectionChange(); - return SelectionResult.none; - case SelectionEventType.granularlyExtendSelection: - result = SelectionResult.end; - final extendSelectionEvent = event as GranularlyExtendSelectionEvent; - // Initialize the offset it there is no ongoing selection. - if (_start == null || _end == null) { - if (extendSelectionEvent.forward) { - _start = _end = Offset.zero; - } else { - _start = _end = Offset.infinite; - } - } - // Move the corresponding selection edge. - final newOffset = extendSelectionEvent.forward ? Offset.infinite : Offset.zero; - if (extendSelectionEvent.isEnd) { - if (newOffset == _end) { - result = extendSelectionEvent.forward ? SelectionResult.next : SelectionResult.previous; - } - _end = newOffset; - } else { - if (newOffset == _start) { - result = extendSelectionEvent.forward ? SelectionResult.next : SelectionResult.previous; - } - _start = newOffset; - } - case SelectionEventType.directionallyExtendSelection: - result = SelectionResult.end; - final extendSelectionEvent = event as DirectionallyExtendSelectionEvent; - // Convert to local coordinates. - final horizontalBaseLine = globalToLocal(Offset(event.dx, 0)).dx; - final Offset newOffset; - final bool forward; - switch (extendSelectionEvent.direction) { - case SelectionExtendDirection.backward: - case SelectionExtendDirection.previousLine: - forward = false; - // Initialize the offset it there is no ongoing selection. - if (_start == null || _end == null) { - _start = _end = Offset.infinite; - } - // Move the corresponding selection edge. - if (extendSelectionEvent.direction == SelectionExtendDirection.previousLine || horizontalBaseLine < 0) { - newOffset = Offset.zero; - } else { - newOffset = Offset.infinite; - } - case SelectionExtendDirection.nextLine: - case SelectionExtendDirection.forward: - forward = true; - // Initialize the offset it there is no ongoing selection. - if (_start == null || _end == null) { - _start = _end = Offset.zero; - } - // Move the corresponding selection edge. - if (extendSelectionEvent.direction == SelectionExtendDirection.nextLine || - horizontalBaseLine > size.width) { - newOffset = Offset.infinite; - } else { - newOffset = Offset.zero; - } - } - if (extendSelectionEvent.isEnd) { - if (newOffset == _end) { - result = forward ? SelectionResult.next : SelectionResult.previous; - } - _end = newOffset; - } else { - if (newOffset == _start) { - result = forward ? SelectionResult.next : SelectionResult.previous; - } - _start = newOffset; - } - // FIXME: #156/#157 handle new SelectionEventType.selectParagraph (currently only in master channel) - default: // case SelectionEventType.selectParagraph: - _start = _end = null; - } - _updateGeometry(); - return result; - } - - @override - SelectedContent? getSelectedContent() => - value.hasSelection && _selectedText != null ? SelectedContent(plainText: _selectedText!) : null; - - @override - SelectedContentRange? getSelection() { - if (_selectedRanges.ranges.isEmpty) return null; - return SelectedContentRange( - startOffset: _selectedRanges.ranges.first.start, - endOffset: _selectedRanges.ranges.last.end, - ); - } - - @override - int get contentLength => _selectedRanges.pageText.fullText.length; - - LayerLink? _startHandle; - LayerLink? _endHandle; - - @override - void pushHandleLayers(LayerLink? startHandle, LayerLink? endHandle) { - if (_startHandle == startHandle && _endHandle == endHandle) { - return; - } - - _startHandle = startHandle; - _endHandle = endHandle; - // FIXME: pushHandleLayers sometimes called after dispose... - if (debugDisposed != true) { - markNeedsPaint(); - } - } - - @override - void paint(PaintingContext context, Offset offset) { - super.paint(context, offset); - - final scale = _sizeOnSelection != null ? size.width / _sizeOnSelection!.width : 1.0; - if (PdfPageTextOverlay.isDebug) { - for (int i = 0; i < _fragments.length; i++) { - final f = _fragments[i]; - final rect = f.bounds.toRect(page: _page, scaledPageSize: size); - context.canvas.drawRect( - rect.shift(offset), - Paint() - ..style = PaintingStyle.stroke - ..color = Colors.red - ..strokeWidth = 1, - ); - } - - if (_selectedRect != null) { - context.canvas.drawRect( - (_selectedRect! * scale).shift(offset), - Paint() - ..style = PaintingStyle.fill - ..color = Colors.blue.withAlpha(100), - ); - } - } - - if (!_geometry.value.hasSelection) { - return; - } - - for (final rect in _geometry.value.selectionRects) { - context.canvas.drawRect( - (rect * scale).shift(offset), - Paint() - ..style = PaintingStyle.fill - ..color = _selectionColor, - ); - } - - if (_startHandle != null) { - context.pushLayer( - LeaderLayer(link: _startHandle!, offset: offset + (value.startSelectionPoint!.localPosition * scale)) - ..applyTransform(null, Matrix4.diagonal3Values(scale, scale, 1.0)), - (context, offset) {}, - Offset.zero, - ); - } - if (_endHandle != null) { - context.pushLayer( - LeaderLayer(link: _endHandle!, offset: offset + (value.endSelectionPoint!.localPosition * scale)) - ..applyTransform(null, Matrix4.diagonal3Values(scale, scale, 1.0)), - (context, offset) {}, - Offset.zero, - ); - } - - // if (size != _sizeOnSelection) { - // Future.microtask( - // () { - // final sp = _geometry.value.startSelectionPoint; - // final ep = _geometry.value.endSelectionPoint; - // if (sp == null || ep == null) return; - // _sizeOnSelection = size; - // _selectedRect = _selectedRect! * scale; - // _geometry.value = _geometry.value.copyWith( - // startSelectionPoint: SelectionPoint( - // handleType: sp.handleType, - // lineHeight: sp.lineHeight * scale, - // localPosition: sp.localPosition * scale), - // endSelectionPoint: SelectionPoint( - // handleType: ep.handleType, - // lineHeight: ep.lineHeight * scale, - // localPosition: ep.localPosition * scale), - // selectionRects: - // _geometry.value.selectionRects.map((r) => r * scale).toList(), - // ); - // markNeedsPaint(); - // }, - // ); - // return; - // } - } -} - -extension _PdfTextRangeListExt on List { - void appendRange(PdfTextRange range) { - if (isNotEmpty && range.start >= last.start && range.start <= last.end) { - last = last.copyWith(end: range.end); - } else { - add(range); - } - } - - void appendAllRanges(Iterable ranges) { - for (final r in ranges) { - appendRange(r); - } - } -} diff --git a/lib/src/widgets/pdf_viewer.dart b/lib/src/widgets/pdf_viewer.dart deleted file mode 100644 index 26030a36..00000000 --- a/lib/src/widgets/pdf_viewer.dart +++ /dev/null @@ -1,2120 +0,0 @@ -// ignore_for_file: public_member_api_docs -import 'dart:async'; -import 'dart:collection'; -import 'dart:math'; -import 'dart:ui' as ui; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; -import 'package:rxdart/rxdart.dart'; -import 'package:synchronized/extension.dart'; -import 'package:vector_math/vector_math_64.dart' as vec; - -import '../../pdfrx.dart'; -import '../utils/platform.dart'; -import 'interactive_viewer.dart' as iv; -import 'pdf_error_widget.dart'; -import 'pdf_page_links_overlay.dart'; - -/// A widget to display PDF document. -/// -/// To create a [PdfViewer] widget, use one of the following constructors: -/// - [PdfDocument] with [PdfViewer.documentRef] -/// - [PdfViewer.asset] with an asset name -/// - [PdfViewer.file] with a file path -/// - [PdfViewer.uri] with a URI -/// -/// Or otherwise, you can pass [PdfDocumentRef] to [PdfViewer] constructor. -class PdfViewer extends StatefulWidget { - /// Create [PdfViewer] from a [PdfDocumentRef]. - /// - /// [documentRef] is the [PdfDocumentRef]. - /// [controller] is the controller to control the viewer. - /// [params] is the parameters to customize the viewer. - /// [initialPageNumber] is the page number to show initially. - const PdfViewer( - this.documentRef, { - super.key, - this.controller, - this.params = const PdfViewerParams(), - this.initialPageNumber = 1, - }); - - /// Create [PdfViewer] from an asset. - /// - /// [assetName] is the asset name. - /// [passwordProvider] is used to provide password for encrypted PDF. See [PdfPasswordProvider] for more info. - /// [firstAttemptByEmptyPassword] is used to determine whether the first attempt to open the PDF is by empty password - /// or not. For more info, see [PdfPasswordProvider]. - /// [controller] is the controller to control the viewer. - /// [params] is the parameters to customize the viewer. - /// [initialPageNumber] is the page number to show initially. - PdfViewer.asset( - String assetName, { - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - super.key, - this.controller, - this.params = const PdfViewerParams(), - this.initialPageNumber = 1, - }) : documentRef = PdfDocumentRefAsset( - assetName, - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - ); - - /// Create [PdfViewer] from a file. - /// - /// [path] is the file path. - /// [passwordProvider] is used to provide password for encrypted PDF. See [PdfPasswordProvider] for more info. - /// [firstAttemptByEmptyPassword] is used to determine whether the first attempt to open the PDF is by empty password - /// or not. For more info, see [PdfPasswordProvider]. - /// [controller] is the controller to control the viewer. - /// [params] is the parameters to customize the viewer. - /// [initialPageNumber] is the page number to show initially. - PdfViewer.file( - String path, { - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - super.key, - this.controller, - this.params = const PdfViewerParams(), - this.initialPageNumber = 1, - }) : documentRef = PdfDocumentRefFile( - path, - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - ); - - /// Create [PdfViewer] from a URI. - /// - /// [uri] is the URI. - /// [passwordProvider] is used to provide password for encrypted PDF. See [PdfPasswordProvider] for more info. - /// [firstAttemptByEmptyPassword] is used to determine whether the first attempt to open the PDF is by empty password - /// or not. For more info, see [PdfPasswordProvider]. - /// [controller] is the controller to control the viewer. - /// [params] is the parameters to customize the viewer. - /// [initialPageNumber] is the page number to show initially. - /// [preferRangeAccess] to prefer range access to download the PDF. The default is false. - /// [headers] is used to specify additional HTTP headers especially for authentication/authorization. - /// [withCredentials] is used to specify whether to include credentials in the request (Only supported on Web). - PdfViewer.uri( - Uri uri, { - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - super.key, - this.controller, - this.params = const PdfViewerParams(), - this.initialPageNumber = 1, - bool preferRangeAccess = false, - Map? headers, - bool withCredentials = false, - }) : documentRef = PdfDocumentRefUri( - uri, - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - preferRangeAccess: preferRangeAccess, - headers: headers, - withCredentials: withCredentials, - ); - - /// Create [PdfViewer] from a byte data. - /// - /// [data] is the byte data. - /// [sourceName] must be some ID, e.g., file name or URL, to identify the source of the PDF. If [sourceName] is not - /// unique for each source, the viewer may not work correctly. - /// [passwordProvider] is used to provide password for encrypted PDF. See [PdfPasswordProvider] for more info. - /// [firstAttemptByEmptyPassword] is used to determine whether the first attempt to open the PDF is by empty password - /// or not. For more info, see [PdfPasswordProvider]. - /// [controller] is the controller to control the viewer. - /// [params] is the parameters to customize the viewer. - /// [initialPageNumber] is the page number to show initially. - PdfViewer.data( - Uint8List data, { - required String sourceName, - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - super.key, - this.controller, - this.params = const PdfViewerParams(), - this.initialPageNumber = 1, - }) : documentRef = PdfDocumentRefData( - data, - sourceName: sourceName, - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - ); - - /// Create [PdfViewer] from a custom source. - /// - /// [fileSize] is the size of the PDF file. - /// [read] is the function to read the PDF file. - /// [sourceName] must be some ID, e.g., file name or URL, to identify the source of the PDF. If [sourceName] is not - /// unique for each source, the viewer may not work correctly. - /// [passwordProvider] is used to provide password for encrypted PDF. See [PdfPasswordProvider] for more info. - /// [firstAttemptByEmptyPassword] is used to determine whether the first attempt to open the PDF is by empty password - /// or not. For more info, see [PdfPasswordProvider]. - /// [controller] is the controller to control the viewer. - /// [params] is the parameters to customize the viewer. - /// [initialPageNumber] is the page number to show initially. - PdfViewer.custom({ - required int fileSize, - required FutureOr Function(Uint8List buffer, int position, int size) read, - required String sourceName, - PdfPasswordProvider? passwordProvider, - bool firstAttemptByEmptyPassword = true, - super.key, - this.controller, - this.params = const PdfViewerParams(), - this.initialPageNumber = 1, - }) : documentRef = PdfDocumentRefCustom( - fileSize: fileSize, - read: read, - sourceName: sourceName, - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - ); - - /// [PdfDocumentRef] that represents the PDF document. - final PdfDocumentRef documentRef; - - /// Controller to control the viewer. - final PdfViewerController? controller; - - /// Parameters to customize the display of the PDF document. - final PdfViewerParams params; - - /// Page number to show initially. - final int initialPageNumber; - - @override - State createState() => _PdfViewerState(); -} - -class _PdfViewerState extends State with SingleTickerProviderStateMixin { - PdfViewerController? _controller; - late final TransformationController _txController = _PdfViewerTransformationController(this); - late final AnimationController _animController; - Animation? _animGoTo; - int _animationResettingGuard = 0; - - PdfDocument? _document; - PdfPageLayout? _layout; - Size? _viewSize; - double? _coverScale; - double? _alternativeFitScale; - static const _defaultMinScale = 0.1; - double _minScale = _defaultMinScale; - int? _pageNumber; - bool _initialized = false; - - final List _zoomStops = [1.0]; - - final _pageImages = {}; - final _pageImageRenderingTimers = {}; - final _pageImagesPartial = {}; - final _cancellationTokens = >{}; - final _pageImagePartialRenderingRequests = {}; - - late final _canvasLinkPainter = _CanvasLinkPainter(this); - - // Changes to the stream rebuilds the viewer - final _updateStream = BehaviorSubject(); - - final _selectables = SplayTreeMap(); - Timer? _selectionChangedThrottleTimer; - - Timer? _interactionEndedTimer; - bool _isInteractionGoingOn = false; - - @override - void initState() { - super.initState(); - _animController = AnimationController(vsync: this, duration: const Duration(milliseconds: 200)); - _widgetUpdated(null); - } - - @override - void didUpdateWidget(covariant PdfViewer oldWidget) { - super.didUpdateWidget(oldWidget); - _widgetUpdated(oldWidget); - } - - Future _widgetUpdated(PdfViewer? oldWidget) async { - if (widget == oldWidget) { - return; - } - - if (oldWidget?.documentRef == widget.documentRef) { - if (widget.params.doChangesRequireReload(oldWidget?.params)) { - if (widget.params.annotationRenderingMode != oldWidget?.params.annotationRenderingMode) { - _releaseAllImages(); - } - _relayoutPages(); - - if (mounted) { - setState(() {}); - } - } - return; - } else { - final oldListenable = oldWidget?.documentRef.resolveListenable(); - oldListenable?.removeListener(_onDocumentChanged); - final listenable = widget.documentRef.resolveListenable(); - listenable.addListener(_onDocumentChanged); - listenable.load(); - } - - _onDocumentChanged(); - } - - void _releaseAllImages() { - for (final timer in _pageImageRenderingTimers.values) { - timer.cancel(); - } - _pageImageRenderingTimers.clear(); - for (final request in _pageImagePartialRenderingRequests.values) { - request.cancel(); - } - _pageImagePartialRenderingRequests.clear(); - for (final image in _pageImages.values) { - image.image.dispose(); - } - _pageImages.clear(); - for (final image in _pageImagesPartial.values) { - image.image.dispose(); - } - _pageImagesPartial.clear(); - } - - void _relayout() { - _relayoutPages(); - _releaseAllImages(); - if (mounted) { - setState(() {}); - } - } - - void _onDocumentChanged() async { - _layout = null; - - _selectionChangedThrottleTimer?.cancel(); - _stopInteraction(); - _releaseAllImages(); - _canvasLinkPainter.resetAll(); - _pageNumber = null; - _initialized = false; - _txController.removeListener(_onMatrixChanged); - _controller?._attach(null); - - final document = widget.documentRef.resolveListenable().document; - if (document == null) { - _document = null; - if (mounted) { - setState(() {}); - } - _notifyOnDocumentChanged(); - return; - } - - _document = document; - - _relayoutPages(); - - _controller ??= widget.controller ?? PdfViewerController(); - _controller!._attach(this); - _txController.addListener(_onMatrixChanged); - - if (mounted) { - setState(() {}); - } - - _notifyOnDocumentChanged(); - } - - void _notifyOnDocumentChanged() { - if (widget.params.onDocumentChanged != null) { - Future.microtask(() => widget.params.onDocumentChanged?.call(_document)); - } - } - - @override - void dispose() { - _selectionChangedThrottleTimer?.cancel(); - _interactionEndedTimer?.cancel(); - _cancelAllPendingRenderings(); - _animController.dispose(); - widget.documentRef.resolveListenable().removeListener(_onDocumentChanged); - _releaseAllImages(); - _canvasLinkPainter.resetAll(); - _txController.removeListener(_onMatrixChanged); - _controller?._attach(null); - _txController.dispose(); - super.dispose(); - } - - void _onMatrixChanged() { - _updateStream.add(_txController.value); - } - - @override - Widget build(BuildContext context) { - final listenable = widget.documentRef.resolveListenable(); - if (listenable.error != null) { - return Container( - color: widget.params.backgroundColor, - child: (widget.params.errorBannerBuilder ?? _defaultErrorBannerBuilder)( - context, - listenable.error!, - listenable.stackTrace, - widget.documentRef, - ), - ); - } - if (_document == null) { - return Container( - color: widget.params.backgroundColor, - child: widget.params.loadingBannerBuilder?.call(context, listenable.bytesDownloaded, listenable.totalBytes), - ); - } - return LayoutBuilder( - builder: (context, constraints) { - if (_updateViewSizeAndCoverScale(Size(constraints.maxWidth, constraints.maxHeight))) { - if (_initialized) { - Future.microtask(() { - if (_initialized && mounted) { - _goTo(_makeMatrixInSafeRange(_txController.value)); - } - }); - } - } - - if (!_initialized && _layout != null) { - _initialized = true; - Future.microtask(() async { - if (mounted) { - final initialPageNumber = - widget.params.calculateInitialPageNumber?.call(_document!, _controller!) ?? widget.initialPageNumber; - await _goToPage(pageNumber: initialPageNumber, duration: Duration.zero); - - if (mounted && _document != null && _controller != null) { - widget.params.onViewerReady?.call(_document!, _controller!); - } - } - }); - } - - Widget selectableRegionInjector(Widget child) => - widget.params.selectableRegionInjector?.call(context, child) ?? - (widget.params.enableTextSelection ? SelectionArea(child: child) : child); - - return Container( - color: widget.params.backgroundColor, - child: Focus( - onKeyEvent: _onKeyEvent, - child: StreamBuilder( - stream: _updateStream, - builder: (context, snapshot) { - _relayoutPages(); - _determineCurrentPage(); - _calcAlternativeFitScale(); - _calcZoomStopTable(); - return selectableRegionInjector( - Builder( - builder: (context) { - return Stack( - children: [ - iv.InteractiveViewer( - transformationController: _txController, - constrained: false, - boundaryMargin: widget.params.boundaryMargin ?? const EdgeInsets.all(double.infinity), - maxScale: widget.params.maxScale, - minScale: _alternativeFitScale != null ? _alternativeFitScale! / 2 : minScale, - panAxis: widget.params.panAxis, - panEnabled: widget.params.panEnabled, - scaleEnabled: widget.params.scaleEnabled, - onInteractionEnd: _onInteractionEnd, - onInteractionStart: _onInteractionStart, - onInteractionUpdate: widget.params.onInteractionUpdate, - interactionEndFrictionCoefficient: widget.params.interactionEndFrictionCoefficient, - onWheelDelta: widget.params.scrollByMouseWheel != null ? _onWheelDelta : null, - // PDF pages - child: CustomPaint( - foregroundPainter: _CustomPainter.fromFunctions(_customPaint), - size: _layout!.documentSize, - ), - ), - ..._buildPageOverlayWidgets(context), - if (_canvasLinkPainter.isEnabled) - SelectionContainer.disabled(child: _canvasLinkPainter.linkHandlingOverlay(_viewSize!)), - if (widget.params.viewerOverlayBuilder != null) - ...widget - .params - .viewerOverlayBuilder!(context, _viewSize!, _canvasLinkPainter._handleLinkTap) - .map((e) => SelectionContainer.disabled(child: e)), - ], - ); - }, - ), - ); - }, - ), - ), - ); - }, - ); - } - - void _startInteraction() { - _interactionEndedTimer?.cancel(); - _interactionEndedTimer = null; - _isInteractionGoingOn = true; - } - - void _stopInteraction() { - _interactionEndedTimer?.cancel(); - if (!mounted) return; - _interactionEndedTimer = Timer(const Duration(milliseconds: 300), () { - _isInteractionGoingOn = false; - _invalidate(); - }); - } - - void _onInteractionEnd(ScaleEndDetails details) { - widget.params.onInteractionEnd?.call(details); - _stopInteraction(); - } - - void _onInteractionStart(ScaleStartDetails details) { - _startInteraction(); - widget.params.onInteractionStart?.call(details); - } - - /// Last page number that is explicitly requested to go to. - int? _gotoTargetPageNumber; - - KeyEventResult _onKeyEvent(FocusNode node, KeyEvent event) { - final isDown = event is KeyDownEvent; - switch (event.logicalKey) { - case LogicalKeyboardKey.pageUp: - if (isDown) { - _goToPage(pageNumber: (_gotoTargetPageNumber ?? _pageNumber!) - 1); - } - return KeyEventResult.handled; - case LogicalKeyboardKey.pageDown: - if (isDown) { - _goToPage(pageNumber: (_gotoTargetPageNumber ?? _pageNumber!) + 1); - } - return KeyEventResult.handled; - case LogicalKeyboardKey.home: - if (isDown) { - _goToPage(pageNumber: 1); - } - return KeyEventResult.handled; - case LogicalKeyboardKey.end: - if (isDown) { - _goToPage(pageNumber: _document!.pages.length, anchor: widget.params.pageAnchorEnd); - } - return KeyEventResult.handled; - case LogicalKeyboardKey.equal: - if (isDown && isCommandKeyPressed) { - _zoomUp(); - } - return KeyEventResult.handled; - case LogicalKeyboardKey.minus: - if (isDown && isCommandKeyPressed) { - _zoomDown(); - } - return KeyEventResult.handled; - case LogicalKeyboardKey.arrowDown: - if (isDown) { - _goToManipulated((m) => m.translate(0.0, -widget.params.scrollByArrowKey)); - } - return KeyEventResult.handled; - case LogicalKeyboardKey.arrowUp: - if (isDown) { - _goToManipulated((m) => m.translate(0.0, widget.params.scrollByArrowKey)); - } - return KeyEventResult.handled; - case LogicalKeyboardKey.arrowLeft: - if (isDown) { - _goToManipulated((m) => m.translate(widget.params.scrollByArrowKey, 0.0)); - } - return KeyEventResult.handled; - case LogicalKeyboardKey.arrowRight: - if (isDown) { - _goToManipulated((m) => m.translate(-widget.params.scrollByArrowKey, 0.0)); - } - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - } - - Future _goToManipulated(void Function(Matrix4 m) manipulate) async { - final m = _txController.value.clone(); - manipulate(m); - _txController.value = m; - } - - bool _updateViewSizeAndCoverScale(Size viewSize) { - if (_viewSize != viewSize) { - final oldSize = _viewSize; - _viewSize = viewSize; - final s1 = viewSize.width / _layout!.documentSize.width; - final s2 = viewSize.height / _layout!.documentSize.height; - _coverScale = max(s1, s2); - if (_controller != null && widget.params.onViewSizeChanged != null) { - widget.params.onViewSizeChanged!(viewSize, oldSize, _controller!); - } - return true; - } - return false; - } - - Rect get _visibleRect => _txController.value.calcVisibleRect(_viewSize!); - - /// Set the current page number. - /// - /// Please note that the function does not scroll/zoom to the specified page but changes the current page number. - void _setCurrentPageNumber(int pageNumber) { - _gotoTargetPageNumber = pageNumber; - _setCurrentPageNumberInternal(_gotoTargetPageNumber, doSetState: true); - } - - void _determineCurrentPage() { - _setCurrentPageNumberInternal(_guessCurrentPage()); - } - - void _setCurrentPageNumberInternal(int? pageNumber, {bool doSetState = false}) { - if (pageNumber != null && _pageNumber != pageNumber) { - _pageNumber = pageNumber; - if (doSetState) { - _invalidate(); - } - if (widget.params.onPageChanged != null) { - Future.microtask(() => widget.params.onPageChanged?.call(_pageNumber)); - } - } - } - - int? _guessCurrentPage() { - if (widget.params.calculateCurrentPageNumber != null) { - return widget.params.calculateCurrentPageNumber!(_visibleRect, _layout!.pageLayouts, _controller!); - } - if (_layout == null) return null; - - final visibleRect = _visibleRect; - double calcIntersectionArea(int pageNumber) { - final rect = _layout!.pageLayouts[pageNumber - 1]; - final intersection = rect.intersect(visibleRect); - if (intersection.isEmpty) return 0; - final area = intersection.width * intersection.height; - return area / (rect.width * rect.height); - } - - if (_gotoTargetPageNumber != null) { - final ratio = calcIntersectionArea(_gotoTargetPageNumber!); - if (ratio > .2) return _gotoTargetPageNumber; - } - _gotoTargetPageNumber = null; - - int? pageNumber; - double maxRatio = 0; - for (int i = 1; i <= _document!.pages.length; i++) { - final ratio = calcIntersectionArea(i); - if (ratio == 0) continue; - if (ratio > maxRatio) { - maxRatio = ratio; - pageNumber = i; - } - } - return pageNumber; - } - - bool _calcAlternativeFitScale() { - if (_pageNumber != null) { - final params = widget.params; - final rect = _layout!.pageLayouts[_pageNumber! - 1]; - final m2 = params.margin * 2; - _alternativeFitScale = min((_viewSize!.width - m2) / rect.width, (_viewSize!.height - m2) / rect.height); - } else { - _alternativeFitScale = null; - } - if (_coverScale == null) { - _minScale = _defaultMinScale; - return false; - } - - _minScale = - !widget.params.useAlternativeFitScaleAsMinScale - ? widget.params.minScale - : _alternativeFitScale == null - ? _coverScale! - : min(_coverScale!, _alternativeFitScale!); - return _alternativeFitScale != null; - } - - void _calcZoomStopTable() { - _zoomStops.clear(); - double z; - if (_alternativeFitScale != null && !_areZoomsAlmostIdentical(_alternativeFitScale!, _coverScale!)) { - if (_alternativeFitScale! < _coverScale!) { - _zoomStops.add(_alternativeFitScale!); - z = _coverScale!; - } else { - _zoomStops.add(_coverScale!); - z = _alternativeFitScale!; - } - } else { - z = _coverScale!; - } - // in some case, z may be 0 and it causes infinite loop. - if (z < 1 / 128) { - _zoomStops.add(1.0); - return; - } - while (z < widget.params.maxScale) { - _zoomStops.add(z); - z *= 2; - } - if (!_areZoomsAlmostIdentical(z, widget.params.maxScale)) { - _zoomStops.add(widget.params.maxScale); - } - - if (!widget.params.useAlternativeFitScaleAsMinScale) { - z = _zoomStops.first; - while (z > widget.params.minScale) { - z /= 2; - _zoomStops.insert(0, z); - } - if (!_areZoomsAlmostIdentical(z, widget.params.minScale)) { - _zoomStops.insert(0, widget.params.minScale); - } - } - } - - double _findNextZoomStop(double zoom, {required bool zoomUp, bool loop = true}) { - if (zoomUp) { - for (final z in _zoomStops) { - if (z > zoom && !_areZoomsAlmostIdentical(z, zoom)) return z; - } - if (loop) { - return _zoomStops.first; - } else { - return _zoomStops.last; - } - } else { - for (int i = _zoomStops.length - 1; i >= 0; i--) { - final z = _zoomStops[i]; - if (z < zoom && !_areZoomsAlmostIdentical(z, zoom)) return z; - } - if (loop) { - return _zoomStops.last; - } else { - return _zoomStops.first; - } - } - } - - static bool _areZoomsAlmostIdentical(double z1, double z2) => (z1 - z2).abs() < 0.01; - - List _buildPageOverlayWidgets(BuildContext context) { - _selectables.clear(); - - final renderBox = context.findRenderObject(); - if (renderBox is! RenderBox) return []; - - final linkWidgets = []; - final textWidgets = []; - final overlayWidgets = []; - final targetRect = _getCacheExtentRect(); - final isTextSelectionEnabled = - (widget.params.enableTextSelection || - widget.params.selectableRegionInjector != null || - widget.params.perPageSelectableRegionInjector != null) && - _document!.permissions?.allowsCopying != false; - - for (int i = 0; i < _document!.pages.length; i++) { - final rect = _layout!.pageLayouts[i]; - final intersection = rect.intersect(targetRect); - if (intersection.isEmpty) continue; - - final page = _document!.pages[i]; - final rectExternal = _documentToRenderBox(rect, renderBox); - if (rectExternal != null) { - if (widget.params.linkHandlerParams == null && widget.params.linkWidgetBuilder != null) { - linkWidgets.add( - SelectionContainer.disabled( - child: PdfPageLinksOverlay( - key: Key('#__pageLinks__:${page.pageNumber}'), - page: page, - pageRect: rectExternal, - params: widget.params, - // FIXME: workaround for link widget eats wheel events. - wrapperBuilder: - (child) => Listener( - child: child, - onPointerSignal: (event) { - if (event is PointerScrollEvent) { - _onWheelDelta(event.scrollDelta); - } - }, - ), - ), - ), - ); - } - - Widget perPageSelectableRegionInjector(Widget child) => - widget.params.perPageSelectableRegionInjector?.call(context, child, page, rectExternal) ?? child; - - if (isTextSelectionEnabled && _document!.permissions?.allowsCopying != false) { - textWidgets.add( - Positioned( - key: Key('#__pageTextOverlay__:${page.pageNumber}'), - left: rectExternal.left, - top: rectExternal.top, - width: rectExternal.width, - height: rectExternal.height, - child: perPageSelectableRegionInjector( - PdfPageTextOverlay( - selectables: _selectables, - enabled: !_isInteractionGoingOn, - page: page, - pageRect: rectExternal, - onTextSelectionChange: _onSelectionChange, - selectionColor: DefaultSelectionStyle.of(context).selectionColor!, - ), - ), - ), - ); - } - - final overlay = widget.params.pageOverlaysBuilder?.call(context, rectExternal, page); - if (overlay != null && overlay.isNotEmpty) { - overlayWidgets.add( - Positioned( - key: Key('#__pageOverlay__:${page.pageNumber}'), - left: rectExternal.left, - top: rectExternal.top, - width: rectExternal.width, - height: rectExternal.height, - child: SelectionContainer.disabled(child: Stack(children: overlay)), - ), - ); - } - } - } - - Widget selectableRegionInjector(Widget child) => child; - - return [ - if (textWidgets.isNotEmpty) - selectableRegionInjector( - Listener( - behavior: HitTestBehavior.translucent, - // FIXME: Selectable absorbs wheel events. - onPointerSignal: (event) { - if (event is PointerScrollEvent) { - _onWheelDelta(event.scrollDelta); - } - }, - child: Stack(children: textWidgets), - ), - ), - ...linkWidgets, - ...overlayWidgets, - ]; - } - - void _clearAllTextSelections() { - for (final s in _selectables.values) { - s.dispatchSelectionEvent(const ClearSelectionEvent()); - } - } - - void _onSelectionChange(PdfTextRanges selection) { - _selectionChangedThrottleTimer?.cancel(); - _selectionChangedThrottleTimer = Timer(const Duration(milliseconds: 300), () { - if (!mounted || !_selectables.containsKey(selection.pageNumber)) return; - widget.params.onTextSelectionChange?.call( - _selectables.values.map((s) => s.selectedRanges).where((s) => s.isNotEmpty).toList(), - ); - }); - } - - Rect _getCacheExtentRect() { - final visibleRect = _visibleRect; - return visibleRect.inflateHV( - horizontal: visibleRect.width * widget.params.horizontalCacheExtent, - vertical: visibleRect.height * widget.params.verticalCacheExtent, - ); - } - - Rect? _documentToRenderBox(Rect rect, RenderBox renderBox) { - final tl = _documentToGlobal(rect.topLeft); - if (tl == null) return null; - final br = _documentToGlobal(rect.bottomRight); - if (br == null) return null; - return Rect.fromPoints(renderBox.globalToLocal(tl), renderBox.globalToLocal(br)); - } - - void _addCancellationToken(int pageNumber, PdfPageRenderCancellationToken token) { - var tokens = _cancellationTokens.putIfAbsent(pageNumber, () => []); - tokens.add(token); - } - - void _cancelPendingRenderings(int pageNumber) { - final tokens = _cancellationTokens[pageNumber]; - if (tokens != null) { - for (final token in tokens) { - token.cancel(); - } - tokens.clear(); - } - } - - void _cancelAllPendingRenderings() { - for (final pageNumber in _cancellationTokens.keys) { - _cancelPendingRenderings(pageNumber); - } - _cancellationTokens.clear(); - } - - /// [_CustomPainter] calls the function to paint PDF pages. - void _customPaint(ui.Canvas canvas, ui.Size size) { - final targetRect = _getCacheExtentRect(); - final scale = MediaQuery.of(context).devicePixelRatio * _currentZoom; - - final unusedPageList = []; - final dropShadowPaint = widget.params.pageDropShadow?.toPaint()?..style = PaintingStyle.fill; - - for (int i = 0; i < _document!.pages.length; i++) { - final rect = _layout!.pageLayouts[i]; - final intersection = rect.intersect(targetRect); - if (intersection.isEmpty) { - final page = _document!.pages[i]; - _cancelPendingRenderings(page.pageNumber); - if (_pageImages.containsKey(i + 1)) { - unusedPageList.add(i + 1); - } - continue; - } - - final page = _document!.pages[i]; - final realSize = _pageImages[page.pageNumber]; - final partial = _pageImagesPartial[page.pageNumber]; - - final scaleLimit = - widget.params.getPageRenderingScale?.call( - context, - page, - _controller!, - widget.params.onePassRenderingScaleThreshold, - ) ?? - widget.params.onePassRenderingScaleThreshold; - - if (dropShadowPaint != null) { - final offset = widget.params.pageDropShadow!.offset; - final spread = widget.params.pageDropShadow!.spreadRadius; - final shadowRect = rect.translate(offset.dx, offset.dy).inflateHV(horizontal: spread, vertical: spread); - canvas.drawRect(shadowRect, dropShadowPaint); - } - - if (widget.params.pageBackgroundPaintCallbacks != null) { - for (final callback in widget.params.pageBackgroundPaintCallbacks!) { - callback(canvas, rect, page); - } - } - - if (realSize != null) { - canvas.drawImageRect( - realSize.image, - Rect.fromLTWH(0, 0, realSize.image.width.toDouble(), realSize.image.height.toDouble()), - rect, - Paint()..filterQuality = FilterQuality.high, - ); - } else { - canvas.drawRect( - rect, - Paint() - ..color = Colors.white - ..style = PaintingStyle.fill, - ); - } - - if (realSize == null || realSize.scale != scaleLimit) { - _requestPageImageCached(page, scaleLimit); - } - - final pageScale = scale * max(rect.width / page.width, rect.height / page.height); - if (pageScale > scaleLimit) { - _requestPartialImage(page, scale); - } - - if (pageScale > scaleLimit && partial != null) { - canvas.drawImageRect( - partial.image, - Rect.fromLTWH(0, 0, partial.image.width.toDouble(), partial.image.height.toDouble()), - partial.rect, - Paint()..filterQuality = FilterQuality.high, - ); - } - - if (_canvasLinkPainter.isEnabled) { - _canvasLinkPainter.paintLinkHighlights(canvas, rect, page); - } - - if (widget.params.pagePaintCallbacks != null) { - for (final callback in widget.params.pagePaintCallbacks!) { - callback(canvas, rect, page); - } - } - - if (unusedPageList.isNotEmpty) { - final currentPageNumber = _pageNumber; - if (currentPageNumber != null && currentPageNumber > 0) { - final currentPage = _document!.pages[currentPageNumber - 1]; - _removeImagesIfCacheBytesExceedsLimit(unusedPageList, widget.params.maxImageBytesCachedOnMemory, currentPage); - } - } - } - } - - void _relayoutPages() { - if (_document == null) return; - _layout = (widget.params.layoutPages ?? _layoutPages)(_document!.pages, widget.params); - } - - PdfPageLayout _layoutPages(List pages, PdfViewerParams params) { - final width = pages.fold(0.0, (w, p) => max(w, p.width)) + params.margin * 2; - - final pageLayout = []; - var y = params.margin; - for (int i = 0; i < pages.length; i++) { - final page = pages[i]; - final rect = Rect.fromLTWH((width - page.width) / 2, y, page.width, page.height); - pageLayout.add(rect); - y += page.height + params.margin; - } - - return PdfPageLayout(pageLayouts: pageLayout, documentSize: Size(width, y)); - } - - void _invalidate() => _updateStream.add(_txController.value); - - Future _requestPageImageCached(PdfPage page, double scale) async { - final width = page.width * scale; - final height = page.height * scale; - if (width < 1 || height < 1) return; - - // if this is the first time to render the page, render it immediately - if (!_pageImages.containsKey(page.pageNumber)) { - _cachePageImage(page, width, height, scale); - return; - } - - _pageImageRenderingTimers[page.pageNumber]?.cancel(); - if (!mounted) return; - _pageImageRenderingTimers[page.pageNumber] = Timer( - const Duration(milliseconds: 50), - () => _cachePageImage(page, width, height, scale), - ); - } - - Future _cachePageImage(PdfPage page, double width, double height, double scale) async { - if (!mounted) return; - if (_pageImages[page.pageNumber]?.scale == scale) return; - final cancellationToken = page.createCancellationToken(); - _addCancellationToken(page.pageNumber, cancellationToken); - await synchronized(() async { - if (!mounted || cancellationToken.isCanceled) return; - if (_pageImages[page.pageNumber]?.scale == scale) return; - final img = await page.render( - fullWidth: width, - fullHeight: height, - backgroundColor: Colors.white, - annotationRenderingMode: widget.params.annotationRenderingMode, - cancellationToken: cancellationToken, - ); - if (img == null) return; - if (!mounted || cancellationToken.isCanceled) { - img.dispose(); - return; - } - final newImage = _PdfImageWithScale(await img.createImage(), scale); - if (!mounted || cancellationToken.isCanceled) { - img.dispose(); - newImage.dispose(); - return; - } - _pageImages[page.pageNumber]?.dispose(); - _pageImages[page.pageNumber] = newImage; - img.dispose(); - _invalidate(); - }); - } - - Future _requestPartialImage(PdfPage page, double scale) async { - _pageImagePartialRenderingRequests[page.pageNumber]?.cancel(); - final cancellationToken = page.createCancellationToken(); - _pageImagePartialRenderingRequests[page.pageNumber] = _PdfPartialImageRenderingRequest( - Timer(const Duration(milliseconds: 300), () async { - if (!mounted || cancellationToken.isCanceled) return; - final newImage = await _createPartialImage(page, scale, cancellationToken); - if (_pageImagesPartial[page.pageNumber] == newImage) return; - _pageImagesPartial.remove(page.pageNumber)?.dispose(); - if (newImage != null) { - _pageImagesPartial[page.pageNumber] = newImage; - } - _invalidate(); - }), - cancellationToken, - ); - } - - Future<_PdfImageWithScaleAndRect?> _createPartialImage( - PdfPage page, - double scale, - PdfPageRenderCancellationToken cancellationToken, - ) async { - final pageRect = _layout!.pageLayouts[page.pageNumber - 1]; - final rect = pageRect.intersect(_visibleRect); - final prev = _pageImagesPartial[page.pageNumber]; - if (prev?.rect == rect && prev?.scale == scale) return prev; - if (rect.width < 1 || rect.height < 1) return null; - final inPageRect = rect.translate(-pageRect.left, -pageRect.top); - - if (!mounted || cancellationToken.isCanceled) return null; - - final img = await page.render( - x: (inPageRect.left * scale).toInt(), - y: (inPageRect.top * scale).toInt(), - width: (inPageRect.width * scale).toInt(), - height: (inPageRect.height * scale).toInt(), - fullWidth: pageRect.width * scale, - fullHeight: pageRect.height * scale, - backgroundColor: Colors.white, - annotationRenderingMode: widget.params.annotationRenderingMode, - cancellationToken: cancellationToken, - ); - if (img == null) return null; - if (!mounted || cancellationToken.isCanceled) { - img.dispose(); - return null; - } - final result = _PdfImageWithScaleAndRect(await img.createImage(), scale, rect); - img.dispose(); - return result; - } - - void _removeImagesIfCacheBytesExceedsLimit(List pageNumbers, int acceptableBytes, PdfPage currentPage) { - double dist(int pageNumber) { - return (_layout!.pageLayouts[pageNumber - 1].center - _layout!.pageLayouts[currentPage.pageNumber - 1].center) - .distanceSquared; - } - - pageNumbers.sort((a, b) => dist(b).compareTo(dist(a))); - int getBytesConsumed(ui.Image? image) => image == null ? 0 : (image.width * image.height * 4).toInt(); - int bytesConsumed = - _pageImages.values.fold(0, (sum, e) => sum + getBytesConsumed(e.image)) + - _pageImagesPartial.values.fold(0, (sum, e) => sum + getBytesConsumed(e.image)); - for (final key in pageNumbers) { - final removed = _pageImages.remove(key); - if (removed != null) { - bytesConsumed -= getBytesConsumed(removed.image); - removed.image.dispose(); - } - final removedPartial = _pageImagesPartial.remove(key); - if (removedPartial != null) { - bytesConsumed -= getBytesConsumed(removedPartial.image); - removedPartial.image.dispose(); - } - if (bytesConsumed <= acceptableBytes) { - break; - } - } - } - - void _onWheelDelta(Offset delta) { - _startInteraction(); - final m = _txController.value.clone(); - m.translate(-delta.dx * widget.params.scrollByMouseWheel!, -delta.dy * widget.params.scrollByMouseWheel!); - _txController.value = m; - _stopInteraction(); - } - - /// Restrict matrix to the safe range. - Matrix4 _makeMatrixInSafeRange(Matrix4 newValue) { - _updateViewSizeAndCoverScale(_viewSize!); - if (widget.params.normalizeMatrix != null) { - return widget.params.normalizeMatrix!(newValue, _viewSize!, _layout!, _controller); - } - return _normalizeMatrix(newValue); - } - - Matrix4 _normalizeMatrix(Matrix4 newValue) { - final position = newValue.calcPosition(_viewSize!); - final newZoom = widget.params.boundaryMargin != null ? newValue.zoom : max(newValue.zoom, minScale); - final hw = _viewSize!.width / 2 / newZoom; - final hh = _viewSize!.height / 2 / newZoom; - final x = position.dx.range(hw, _layout!.documentSize.width - hw); - final y = position.dy.range(hh, _layout!.documentSize.height - hh); - - return _calcMatrixFor(Offset(x, y), zoom: newZoom, viewSize: _viewSize!); - } - - /// Calculate matrix to center the specified position. - Matrix4 _calcMatrixFor(Offset position, {required double zoom, required Size viewSize}) { - final hw = viewSize.width / 2; - final hh = viewSize.height / 2; - - return Matrix4.compose( - vec.Vector3(-position.dx * zoom + hw, -position.dy * zoom + hh, 0), - vec.Quaternion.identity(), - vec.Vector3(zoom, zoom, 1), - ); - } - - /// The minimum zoom ratio allowed. - double get minScale => _minScale; - - Matrix4 _calcMatrixForRect(Rect rect, {double? zoomMax, double? margin}) { - margin ??= 0; - var zoom = min((_viewSize!.width - margin * 2) / rect.width, (_viewSize!.height - margin * 2) / rect.height); - if (zoomMax != null && zoom > zoomMax) zoom = zoomMax; - return _calcMatrixFor(rect.center, zoom: zoom, viewSize: _viewSize!); - } - - Matrix4 _calcMatrixForArea({required Rect rect, PdfPageAnchor? anchor}) { - anchor ??= widget.params.pageAnchor; - final visibleRect = _visibleRect; - final w = min(rect.width, visibleRect.width); - final h = min(rect.height, visibleRect.height); - switch (anchor) { - case PdfPageAnchor.top: - return _calcMatrixForRect((rect.topLeft) & Size(rect.width, h)); - case PdfPageAnchor.left: - return _calcMatrixForRect((rect.topLeft) & Size(w, rect.height)); - case PdfPageAnchor.right: - return _calcMatrixForRect(Rect.fromLTWH(rect.right - w, rect.top, w, rect.height)); - case PdfPageAnchor.bottom: - return _calcMatrixForRect(Rect.fromLTWH(rect.left, rect.bottom - h, rect.width, h)); - case PdfPageAnchor.topLeft: - return _calcMatrixForRect((rect.topLeft) & visibleRect.size); - case PdfPageAnchor.topCenter: - return _calcMatrixForRect(rect.topCenter & visibleRect.size); - case PdfPageAnchor.topRight: - return _calcMatrixForRect((rect.topRight) & visibleRect.size); - case PdfPageAnchor.centerLeft: - return _calcMatrixForRect(rect.centerLeft & visibleRect.size); - case PdfPageAnchor.center: - return _calcMatrixForRect(rect.center & visibleRect.size); - case PdfPageAnchor.centerRight: - return _calcMatrixForRect(rect.centerRight & visibleRect.size); - case PdfPageAnchor.bottomLeft: - return _calcMatrixForRect((rect.bottomLeft) & visibleRect.size); - case PdfPageAnchor.bottomCenter: - return _calcMatrixForRect(rect.bottomCenter & visibleRect.size); - case PdfPageAnchor.bottomRight: - return _calcMatrixForRect((rect.bottomRight) & visibleRect.size); - case PdfPageAnchor.all: - return _calcMatrixForRect(rect); - } - } - - Matrix4 _calcMatrixForPage({required int pageNumber, PdfPageAnchor? anchor}) => - _calcMatrixForArea(rect: _layout!.pageLayouts[pageNumber - 1].inflate(widget.params.margin), anchor: anchor); - - Rect _calcRectForRectInsidePage({required int pageNumber, required PdfRect rect}) { - final page = _document!.pages[pageNumber - 1]; - final pageRect = _layout!.pageLayouts[pageNumber - 1]; - final area = rect.toRect(page: page, scaledPageSize: pageRect.size); - return area.translate(pageRect.left, pageRect.top); - } - - Matrix4 _calcMatrixForRectInsidePage({required int pageNumber, required PdfRect rect, PdfPageAnchor? anchor}) { - return _calcMatrixForArea(rect: _calcRectForRectInsidePage(pageNumber: pageNumber, rect: rect), anchor: anchor); - } - - Matrix4? _calcMatrixForDest(PdfDest? dest) { - if (dest == null) return null; - final page = _document!.pages[dest.pageNumber - 1]; - final pageRect = _layout!.pageLayouts[dest.pageNumber - 1]; - double calcX(double? x) => (x ?? 0) / page.width * pageRect.width; - double calcY(double? y) => (page.height - (y ?? 0)) / page.height * pageRect.height; - final params = dest.params; - switch (dest.command) { - case PdfDestCommand.xyz: - if (params != null && params.length >= 2) { - final zoom = - params.length >= 3 - ? params[2] != null && params[2] != 0.0 - ? params[2]! - : _currentZoom - : 1.0; - final hw = _viewSize!.width / 2 / zoom; - final hh = _viewSize!.height / 2 / zoom; - return _calcMatrixFor( - pageRect.topLeft.translate(calcX(params[0]) + hw, calcY(params[1]) + hh), - zoom: zoom, - viewSize: _viewSize!, - ); - } - break; - case PdfDestCommand.fit: - case PdfDestCommand.fitB: - return _calcMatrixForPage(pageNumber: dest.pageNumber, anchor: PdfPageAnchor.all); - - case PdfDestCommand.fitH: - case PdfDestCommand.fitBH: - if (params != null && params.length == 1) { - final hh = _viewSize!.height / 2 / _currentZoom; - return _calcMatrixFor( - pageRect.topLeft.translate(0, calcY(params[0]) + hh), - zoom: _currentZoom, - viewSize: _viewSize!, - ); - } - break; - case PdfDestCommand.fitV: - case PdfDestCommand.fitBV: - if (params != null && params.length == 1) { - final hw = _viewSize!.width / 2 / _currentZoom; - return _calcMatrixFor( - pageRect.topLeft.translate(calcX(params[0]) + hw, 0), - zoom: _currentZoom, - viewSize: _viewSize!, - ); - } - break; - case PdfDestCommand.fitR: - if (params != null && params.length == 4) { - // page /FitR left bottom right top - return _calcMatrixForArea( - rect: Rect.fromLTRB( - calcX(params[0]), - calcY(params[3]), - calcX(params[2]), - calcY(params[1]), - ).translate(pageRect.left, pageRect.top), - anchor: PdfPageAnchor.all, - ); - } - break; - default: - return null; - } - return null; - } - - Future _goTo(Matrix4? destination, {Duration duration = const Duration(milliseconds: 200)}) async { - void update() { - if (_animationResettingGuard != 0) return; - _txController.value = _animGoTo!.value; - } - - try { - if (destination == null) return; // do nothing - _animationResettingGuard++; - _animController.reset(); - _animationResettingGuard--; - _animGoTo = Matrix4Tween( - begin: _txController.value, - end: _makeMatrixInSafeRange(destination), - ).animate(_animController); - _animGoTo!.addListener(update); - await _animController.animateTo(1.0, duration: duration, curve: Curves.easeInOut).orCancel; - } on TickerCanceled { - // expected - } finally { - _animGoTo!.removeListener(update); - } - } - - Matrix4 _calcMatrixToEnsureRectVisible(Rect rect, {double margin = 0}) { - final restrictedRect = _txController.value.calcVisibleRect(_viewSize!, margin: margin); - if (restrictedRect.containsRect(rect)) { - return _txController.value; // keep the current position - } - if (rect.width <= restrictedRect.width && rect.height < restrictedRect.height) { - final intRect = Rect.fromLTWH( - rect.left < restrictedRect.left - ? rect.left - : rect.right < restrictedRect.right - ? restrictedRect.left - : restrictedRect.left + rect.right - restrictedRect.right, - rect.top < restrictedRect.top - ? rect.top - : rect.bottom < restrictedRect.bottom - ? restrictedRect.top - : restrictedRect.top + rect.bottom - restrictedRect.bottom, - restrictedRect.width, - restrictedRect.height, - ); - final newRect = intRect.inflate(margin / _currentZoom); - return _calcMatrixForRect(newRect); - } - return _calcMatrixForRect(rect, margin: margin); - } - - Future _ensureVisible(Rect rect, {Duration duration = const Duration(milliseconds: 200), double margin = 0}) => - _goTo(_calcMatrixToEnsureRectVisible(rect, margin: margin), duration: duration); - - Future _goToArea({ - required Rect rect, - PdfPageAnchor? anchor, - Duration duration = const Duration(milliseconds: 200), - }) => _goTo(_calcMatrixForArea(rect: rect, anchor: anchor), duration: duration); - - Future _goToPage({ - required int pageNumber, - PdfPageAnchor? anchor, - Duration duration = const Duration(milliseconds: 200), - }) async { - final pageCount = _document!.pages.length; - final int targetPageNumber; - if (pageNumber < 1) { - targetPageNumber = 1; - } else if (pageNumber != 1 && pageNumber >= pageCount) { - targetPageNumber = pageCount; - anchor ??= widget.params.pageAnchorEnd; - } else { - targetPageNumber = pageNumber; - } - await _goTo(_calcMatrixForPage(pageNumber: targetPageNumber, anchor: anchor), duration: duration); - _setCurrentPageNumber(targetPageNumber); - } - - Future _goToRectInsidePage({ - required int pageNumber, - required PdfRect rect, - PdfPageAnchor? anchor, - Duration duration = const Duration(milliseconds: 200), - }) async { - await _goTo(_calcMatrixForRectInsidePage(pageNumber: pageNumber, rect: rect, anchor: anchor), duration: duration); - _setCurrentPageNumber(pageNumber); - } - - Future _goToDest(PdfDest? dest, {Duration duration = const Duration(milliseconds: 200)}) async { - final m = _calcMatrixForDest(dest); - if (m == null) return false; - await _goTo(m, duration: duration); - if (dest != null) { - _setCurrentPageNumber(dest.pageNumber); - } - return true; - } - - double get _currentZoom => _txController.value.zoom; - - PdfPageHitTestResult? _getPdfPageHitTestResult(Offset offset, {required bool useDocumentLayoutCoordinates}) { - final pages = _document?.pages; - final pageLayouts = _layout?.pageLayouts; - if (pages == null || pageLayouts == null) return null; - if (!useDocumentLayoutCoordinates) { - final r = Matrix4.inverted(_txController.value); - offset = r.transformOffset(offset); - } - for (int i = 0; i < pages.length; i++) { - final page = pages[i]; - final pageRect = pageLayouts[i]; - if (pageRect.contains(offset)) { - return PdfPageHitTestResult( - page: page, - offset: Offset(offset.dx - pageRect.left, pageRect.bottom - offset.dy) * page.height / pageRect.height, - ); - } - } - return null; - } - - double _getNextZoom({bool loop = true}) => _findNextZoomStop(_currentZoom, zoomUp: true, loop: loop); - double _getPreviousZoom({bool loop = true}) => _findNextZoomStop(_currentZoom, zoomUp: false, loop: loop); - - Future _setZoom(Offset position, double zoom) => - _goTo(_calcMatrixFor(position, zoom: zoom, viewSize: _viewSize!)); - - Offset get _centerPosition => _txController.value.calcPosition(_viewSize!); - - Future _zoomUp({bool loop = false, Offset? zoomCenter}) => - _setZoom(zoomCenter ?? _centerPosition, _getNextZoom(loop: loop)); - - Future _zoomDown({bool loop = false, Offset? zoomCenter}) async { - await _setZoom(zoomCenter ?? _centerPosition, _getPreviousZoom(loop: loop)); - } - - RenderBox? get _renderBox { - final renderBox = context.findRenderObject(); - if (renderBox is! RenderBox) return null; - return renderBox; - } - - /// Converts the global position to the local position in the widget. - Offset? _globalToLocal(Offset global) { - try { - final renderBox = _renderBox; - if (renderBox == null) return null; - return renderBox.globalToLocal(global); - } catch (e) { - return null; - } - } - - /// Converts the local position to the global position in the widget. - Offset? _localToGlobal(Offset local) { - try { - final renderBox = _renderBox; - if (renderBox == null) return null; - return renderBox.localToGlobal(local); - } catch (e) { - return null; - } - } - - /// Converts the global position to the local position in the PDF document structure. - Offset? _globalToDocument(Offset global) { - final ratio = 1 / _currentZoom; - return _globalToLocal( - global, - )?.translate(-_txController.value.xZoomed, -_txController.value.yZoomed).scale(ratio, ratio); - } - - /// Converts the local position in the PDF document structure to the global position. - Offset? _documentToGlobal(Offset document) => _localToGlobal( - document.scale(_currentZoom, _currentZoom).translate(_txController.value.xZoomed, _txController.value.yZoomed), - ); -} - -class _PdfPartialImageRenderingRequest { - _PdfPartialImageRenderingRequest(this.timer, this.cancellationToken); - final Timer timer; - final PdfPageRenderCancellationToken cancellationToken; - - void cancel() { - timer.cancel(); - cancellationToken.cancel(); - } -} - -class _PdfImageWithScale { - _PdfImageWithScale(this.image, this.scale); - final ui.Image image; - final double scale; - - void dispose() { - image.dispose(); - } -} - -class _PdfImageWithScaleAndRect extends _PdfImageWithScale { - _PdfImageWithScaleAndRect(super.image, super.scale, this.rect); - final Rect rect; -} - -class _PdfViewerTransformationController extends TransformationController { - _PdfViewerTransformationController(this._state); - - final _PdfViewerState _state; - - @override - set value(Matrix4 newValue) { - super.value = _state._makeMatrixInSafeRange(newValue); - } -} - -/// Defines page layout. -class PdfPageLayout { - PdfPageLayout({required this.pageLayouts, required this.documentSize}); - final List pageLayouts; - final Size documentSize; -} - -/// Represents the result of the hit test on the page. -class PdfPageHitTestResult { - PdfPageHitTestResult({required this.page, required this.offset}); - - /// The page that was hit. - final PdfPage page; - - /// The offset in the PDF page coordinates; the origin is at the bottom-left corner. - final Offset offset; -} - -/// Controls associated [PdfViewer]. -/// -/// It's always your option to extend (inherit) the class to customize the [PdfViewer] behavior. -/// -/// Please note that almost all fields and functions are not working if the controller is not associated -/// to any [PdfViewer]. -/// You can check whether the controller is associated or not by checking [isReady] property. -class PdfViewerController extends ValueListenable { - _PdfViewerState? __state; - final _listeners = []; - - void _attach(_PdfViewerState? state) { - __state?._txController.removeListener(_notifyListeners); - __state = state; - __state?._txController.addListener(_notifyListeners); - } - - void _notifyListeners() { - for (final listener in _listeners) { - listener(); - } - } - - _PdfViewerState get _state => __state!; - - /// Get the associated [PdfViewer] widget. - PdfViewer get widget => _state.widget; - - /// Get the associated [PdfViewerParams] parameters. - PdfViewerParams get params => widget.params; - - /// Determine whether the document/pages are ready or not. - bool get isReady => __state?._document?.pages != null; - - /// The document layout size. - Size get documentSize => _state._layout!.documentSize; - - /// Page layout. - PdfPageLayout get layout => _state._layout!; - - /// The view port size (The widget's client area's size) - Size get viewSize => _state._viewSize!; - - /// The zoom ratio that fits the page's smaller side (either horizontal or vertical) to the view port. - double get coverScale => _state._coverScale!; - - /// The zoom ratio that fits whole the page to the view port. - double? get alternativeFitScale => _state._alternativeFitScale; - - /// The minimum zoom ratio allowed. - double get minScale => _state.minScale; - - /// The area of the document layout which is visible on the view port. - Rect get visibleRect => _state._visibleRect; - - /// Get the associated document. - /// - /// Please note that the field does not ensure that the [PdfDocument] is alive during long asynchronous operations. - /// If you want to do some time consuming asynchronous operation, use [useDocument] instead. - @Deprecated('Use useDocument instead') - PdfDocument get document => _state._document!; - - /// Get the associated pages. - /// - /// Please note that the field does not ensure that the associated [PdfDocument] is alive during long asynchronous - /// operations. If you want to do some time consuming asynchronous operation, use [useDocument] instead. - /// For page count, use [pageCount] instead. - @Deprecated('Use useDocument instead') - List get pages => _state._document!.pages; - - /// Get the page count of the document. - int get pageCount => _state._document!.pages.length; - - /// The current page number if available. - int? get pageNumber => _state._pageNumber; - - /// The document reference associated to the [PdfViewer]. - PdfDocumentRef get documentRef => _state.widget.documentRef; - - /// Within call to the function, it ensures that the [PdfDocument] is alive (not null and not disposed). - /// - /// If [ensureLoaded] is true, it tries to ensure that the document is loaded. - /// If the document is not loaded, the function does not call [task] and return null. - /// [cancelLoading] is used to cancel the loading process. - /// - /// The following fragment explains how to use [PdfDocument]: - /// - /// ```dart - /// await controller.useDocument( - /// (document) async { - /// // Use the document here - /// }, - /// ); - /// ``` - /// - /// This is just a shortcut for the combination of [PdfDocumentRef.resolveListenable] and [PdfDocumentListenable.useDocument]. - /// - /// For more information, see [PdfDocumentRef], [PdfDocumentRef.resolveListenable], and [PdfDocumentListenable.useDocument]. - FutureOr useDocument( - FutureOr Function(PdfDocument document) task, { - bool ensureLoaded = true, - Completer? cancelLoading, - }) => documentRef.resolveListenable().useDocument(task, ensureLoaded: ensureLoaded, cancelLoading: cancelLoading); - - @override - Matrix4 get value => _state._txController.value; - - set value(Matrix4 newValue) => _state._txController.value = makeMatrixInSafeRange(newValue); - - @override - void addListener(ui.VoidCallback listener) => _listeners.add(listener); - - @override - void removeListener(ui.VoidCallback listener) => _listeners.remove(listener); - - /// Restrict matrix to the safe range. - Matrix4 makeMatrixInSafeRange(Matrix4 newValue) => _state._makeMatrixInSafeRange(newValue); - - double getNextZoom({bool loop = true}) => _state._findNextZoomStop(currentZoom, zoomUp: true, loop: loop); - - double getPreviousZoom({bool loop = true}) => _state._findNextZoomStop(currentZoom, zoomUp: false, loop: loop); - - void notifyFirstChange(void Function() onFirstChange) { - void handler() { - removeListener(handler); - onFirstChange(); - } - - addListener(handler); - } - - /// Forcibly relayout the pages. - void relayout() => _state._relayout(); - - /// Go to the specified area. - /// - /// [anchor] specifies how the page is positioned if the page is larger than the view. - Future goToArea({ - required Rect rect, - PdfPageAnchor? anchor, - Duration duration = const Duration(milliseconds: 200), - }) => _state._goToArea(rect: rect, anchor: anchor, duration: duration); - - /// Go to the specified page. - /// - /// [anchor] specifies how the page is positioned if the page is larger than the view. - Future goToPage({ - required int pageNumber, - PdfPageAnchor? anchor, - Duration duration = const Duration(milliseconds: 200), - }) => _state._goToPage(pageNumber: pageNumber, anchor: anchor, duration: duration); - - /// Go to the specified area inside the page. - /// - /// [pageNumber] specifies the page number. - /// [rect] specifies the area to go in page coordinates. - /// [anchor] specifies how the page is positioned if the page is larger than the view. - Future goToRectInsidePage({ - required int pageNumber, - required PdfRect rect, - PdfPageAnchor? anchor, - Duration duration = const Duration(milliseconds: 200), - }) => _state._goToRectInsidePage(pageNumber: pageNumber, rect: rect, anchor: anchor, duration: duration); - - /// Calculate the rectangle for the specified area inside the page. - /// - /// [pageNumber] specifies the page number. - /// [rect] specifies the area to go in page coordinates. - Rect calcRectForRectInsidePage({required int pageNumber, required PdfRect rect}) => - _state._calcRectForRectInsidePage(pageNumber: pageNumber, rect: rect); - - /// Calculate the matrix for the specified area inside the page. - /// - /// [pageNumber] specifies the page number. - /// [rect] specifies the area to go in page coordinates. - /// [anchor] specifies how the page is positioned if the page is larger than the view. - Matrix4 calcMatrixForRectInsidePage({required int pageNumber, required PdfRect rect, PdfPageAnchor? anchor}) => - _state._calcMatrixForRectInsidePage(pageNumber: pageNumber, rect: rect, anchor: anchor); - - /// Go to the specified destination. - /// - /// [dest] specifies the destination. - /// [duration] specifies the duration of the animation. - Future goToDest(PdfDest? dest, {Duration duration = const Duration(milliseconds: 200)}) => - _state._goToDest(dest, duration: duration); - - /// Calculate the matrix for the specified destination. - /// - /// [dest] specifies the destination. - Matrix4? calcMatrixForDest(PdfDest? dest) => _state._calcMatrixForDest(dest); - - /// Calculate the matrix to fit the page into the view. - /// - /// `/Fit` command on [PDF 32000-1:2008, 12.3.2.2 Explicit Destinations, Table 151](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf#page=374) - Matrix4? calcMatrixForFit({required int pageNumber}) => - calcMatrixForDest(PdfDest(pageNumber, PdfDestCommand.fit, null)); - - /// Calculate the matrix to fit the specified page width into the view. - /// - Matrix4? calcMatrixFitWidthForPage({required int pageNumber}) { - final page = layout.pageLayouts[pageNumber - 1]; - final zoom = (viewSize.width - params.margin * 2) / page.width; - final y = (viewSize.height / 2 - params.margin) / zoom; - return calcMatrixFor(page.topCenter.translate(0, y), zoom: zoom, viewSize: viewSize); - } - - /// Calculate the matrix to fit the specified page height into the view. - /// - Matrix4? calcMatrixFitHeightForPage({required int pageNumber}) { - final page = layout.pageLayouts[pageNumber - 1]; - final zoom = (viewSize.height - params.margin * 2) / page.height; - return calcMatrixFor(page.center, zoom: zoom, viewSize: viewSize); - } - - /// Get list of possible matrices that fit some of the pages into the view. - /// - /// [sortInSuitableOrder] specifies whether the result is sorted in a suitable order. - /// - /// Because [PdfViewer] can show multiple pages at once, there are several possible - /// matrices to fit the pages into the view according to several criteria. - /// The method returns the list of such matrices. - /// - /// In theory, the method can be also used to determine the dominant pages in the view. - List calcFitZoomMatrices({bool sortInSuitableOrder = true}) { - final viewRect = visibleRect; - final result = []; - final pos = centerPosition; - for (int i = 0; i < layout.pageLayouts.length; i++) { - final page = layout.pageLayouts[i]; - if (page.intersect(viewRect).isEmpty) continue; - final zoom = (viewSize.width - params.margin * 2) / page.width; - // NOTE: keep the y-position but center the x-position - final newMatrix = calcMatrixFor(Offset(page.left + page.width / 2, pos.dy), zoom: zoom); - - final intersection = newMatrix.calcVisibleRect(viewSize).intersect(page); - // if the page is not visible after changing the zoom, ignore it - if (intersection.isEmpty) continue; - final intersectionRatio = intersection.width * intersection.height / (page.width * page.height); - result.add(PdfPageFitInfo(pageNumber: i + 1, matrix: newMatrix, visibleAreaRatio: intersectionRatio)); - } - if (sortInSuitableOrder) { - result.sort((a, b) => b.visibleAreaRatio.compareTo(a.visibleAreaRatio)); - } - return result; - } - - /// Calculate the matrix for the page. - /// - /// [pageNumber] specifies the page number. - /// [anchor] specifies how the page is positioned if the page is larger than the view. - Matrix4 calcMatrixForPage({required int pageNumber, PdfPageAnchor? anchor}) => - _state._calcMatrixForPage(pageNumber: pageNumber, anchor: anchor); - - /// Calculate the matrix for the specified area. - /// - /// [rect] specifies the area in document coordinates. - /// [anchor] specifies how the page is positioned if the page is larger than the view. - Matrix4 calcMatrixForArea({required Rect rect, PdfPageAnchor? anchor}) => - _state._calcMatrixForArea(rect: rect, anchor: anchor); - - /// Go to the specified position by the matrix. - /// - /// All the `goToXXX` functions internally calls the function. - /// So if you customize the behavior of the viewer, you can extend [PdfViewerController] and override the function: - /// - /// ```dart - /// class MyPdfViewerController extends PdfViewerController { - /// @override - /// Future goTo( - /// Matrix4? destination, { - /// Duration duration = const Duration(milliseconds: 200), - /// }) async { - /// print('goTo'); - /// super.goTo(destination, duration: duration); - /// } - /// } - /// ``` - Future goTo(Matrix4? destination, {Duration duration = const Duration(milliseconds: 200)}) => - _state._goTo(destination, duration: duration); - - /// Ensure the specified area is visible inside the view port. - /// - /// If the area is larger than the view port, the area is zoomed to fit the view port. - /// [margin] adds extra margin to the area. - Future ensureVisible(Rect rect, {Duration duration = const Duration(milliseconds: 200), double margin = 0}) => - _state._ensureVisible(rect, duration: duration, margin: margin); - - /// Calculate the matrix to center the specified position. - Matrix4 calcMatrixFor(Offset position, {double? zoom, Size? viewSize}) => - _state._calcMatrixFor(position, zoom: zoom ?? currentZoom, viewSize: viewSize ?? this.viewSize); - - Offset get centerPosition => value.calcPosition(viewSize); - - Matrix4 calcMatrixForRect(Rect rect, {double? zoomMax, double? margin}) => - _state._calcMatrixForRect(rect, zoomMax: zoomMax, margin: margin); - - Matrix4 calcMatrixToEnsureRectVisible(Rect rect, {double margin = 0}) => - _state._calcMatrixToEnsureRectVisible(rect, margin: margin); - - /// Do hit-test against laid out pages. - /// - /// Returns the hit-test result if the specified offset is inside a page; otherwise null. - /// - /// [useDocumentLayoutCoordinates] specifies whether the offset is in the document layout coordinates; - /// if true, the offset is in the document layout coordinates; otherwise, the offset is in the widget coordinates. - PdfPageHitTestResult? getPdfPageHitTestResult(Offset offset, {required bool useDocumentLayoutCoordinates}) => - _state._getPdfPageHitTestResult(offset, useDocumentLayoutCoordinates: useDocumentLayoutCoordinates); - - /// Set the current page number. - /// - /// This function does not scroll/zoom to the specified page but changes the current page number. - void setCurrentPageNumber(int pageNumber) => _state._setCurrentPageNumber(pageNumber); - - double get currentZoom => value.zoom; - - Future setZoom(Offset position, double zoom) => _state._setZoom(position, zoom); - - Future zoomUp({bool loop = false, Offset? zoomCenter}) => _state._zoomUp(loop: loop, zoomCenter: zoomCenter); - - Future zoomDown({bool loop = false, Offset? zoomCenter}) => - _state._zoomDown(loop: loop, zoomCenter: zoomCenter); - - RenderBox? get renderBox => _state._renderBox; - - /// Converts the global position to the local position in the widget. - Offset? globalToLocal(Offset global) => _state._globalToLocal(global); - - /// Converts the local position to the global position in the widget. - Offset? localToGlobal(Offset local) => _state._localToGlobal(local); - - /// Converts the global position to the local position in the PDF document structure. - Offset? globalToDocument(Offset global) => _state._globalToDocument(global); - - /// Converts the local position in the PDF document structure to the global position. - Offset? documentToGlobal(Offset document) => _state._documentToGlobal(document); - - /// Provided to workaround certain widgets eating wheel events. Use with [Listener.onPointerSignal]. - void handlePointerSignalEvent(PointerSignalEvent event) { - if (event is PointerScrollEvent) { - _state._onWheelDelta(event.scrollDelta); - } - } - - void invalidate() => _state._invalidate(); -} - -/// [PdfViewerController.calcFitZoomMatrices] returns the list of this class. -@immutable -class PdfPageFitInfo { - const PdfPageFitInfo({required this.pageNumber, required this.matrix, required this.visibleAreaRatio}); - - /// The page number of the target page. - final int pageNumber; - - /// The matrix to fit the page horizontally into the view. - final Matrix4 matrix; - - /// The ratio of the visible area of the page. 1 means the whole page is visible inside the view. - final double visibleAreaRatio; - - @override - String toString() => 'PdfPageFitInfo(pageNumber=$pageNumber, visibleAreaRatio=$visibleAreaRatio, matrix=$matrix)'; -} - -extension PdfMatrix4Ext on Matrix4 { - /// Zoom ratio of the matrix. - double get zoom => storage[0]; - - /// X position of the matrix. - double get xZoomed => storage[12]; - - /// X position of the matrix. - set xZoomed(double value) => storage[12] = value; - - /// Y position of the matrix. - double get yZoomed => storage[13]; - - /// Y position of the matrix. - set yZoomed(double value) => storage[13] = value; - - double get x => xZoomed / zoom; - - set x(double value) => xZoomed = value * zoom; - - double get y => yZoomed / zoom; - - set y(double value) => yZoomed = value * zoom; - - /// Calculate the position of the matrix based on the specified view size. - /// - /// Because [Matrix4] does not have the information of the view size, - /// this function calculates the position based on the specified view size. - Offset calcPosition(Size viewSize) => Offset((viewSize.width / 2 - xZoomed), (viewSize.height / 2 - yZoomed)) / zoom; - - /// Calculate the visible rectangle based on the specified view size. - /// - /// [margin] adds extra margin to the area. - /// Because [Matrix4] does not have the information of the view size, - /// this function calculates the visible rectangle based on the specified view size. - Rect calcVisibleRect(Size viewSize, {double margin = 0}) => Rect.fromCenter( - center: calcPosition(viewSize), - width: (viewSize.width - margin * 2) / zoom, - height: (viewSize.height - margin * 2) / zoom, - ); - - Offset transformOffset(Offset xy) { - final x = xy.dx; - final y = xy.dy; - final w = x * storage[3] + y * storage[7] + storage[15]; - return Offset( - (x * storage[0] + y * storage[4] + storage[12]) / w, - (x * storage[1] + y * storage[5] + storage[13]) / w, - ); - } -} - -extension _RangeDouble on T { - /// Identical to [num.clamp] but it does nothing if [a] is larger or equal to [b]. - T range(T a, T b) => a < b ? clamp(a, b) as T : (a + b) / 2 as T; -} - -extension RectExt on Rect { - Rect operator *(double operand) => Rect.fromLTRB(left * operand, top * operand, right * operand, bottom * operand); - - Rect operator /(double operand) => Rect.fromLTRB(left / operand, top / operand, right / operand, bottom / operand); - - bool containsRect(Rect other) => contains(other.topLeft) && contains(other.bottomRight); - - Rect inflateHV({required double horizontal, required double vertical}) => - Rect.fromLTRB(left - horizontal, top - vertical, right + horizontal, bottom + vertical); -} - -/// Create a [CustomPainter] from a paint function. -class _CustomPainter extends CustomPainter { - /// Create a [CustomPainter] from a paint function. - const _CustomPainter.fromFunctions(this.paintFunction); - final void Function(ui.Canvas canvas, ui.Size size) paintFunction; - @override - void paint(ui.Canvas canvas, ui.Size size) => paintFunction(canvas, size); - - @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => true; -} - -Widget _defaultErrorBannerBuilder( - BuildContext context, - Object error, - StackTrace? stackTrace, - PdfDocumentRef documentRef, -) { - return pdfErrorWidget(context, error, stackTrace: stackTrace); -} - -/// Handles the link painting and tap handling. -class _CanvasLinkPainter { - _CanvasLinkPainter(this._state); - final _PdfViewerState _state; - MouseCursor _cursor = MouseCursor.defer; - final _links = >{}; - - bool get isEnabled => _state.widget.params.linkHandlerParams != null; - - /// Reset all the internal data. - void resetAll() { - _cursor = MouseCursor.defer; - _links.clear(); - } - - /// Release the page data. - void releaseLinksForPage(int pageNumber) { - _links.remove(pageNumber); - } - - List? _ensureLinksLoaded(PdfPage page, {void Function()? onLoaded}) { - final links = _links[page.pageNumber]; - if (links != null) return links; - synchronized(() async { - final links = _links[page.pageNumber]; - if (links != null) return links; - _links[page.pageNumber] = await page.loadLinks(compact: true); - if (onLoaded != null) { - onLoaded(); - } else { - _state._invalidate(); - } - }); - return null; - } - - PdfLink? _findLinkAtPosition(Offset position) { - final hitResult = _state._getPdfPageHitTestResult(position, useDocumentLayoutCoordinates: false); - if (hitResult == null) return null; - final links = _ensureLinksLoaded(hitResult.page); - if (links == null) return null; - for (final link in links) { - for (final rect in link.rects) { - if (rect.containsOffset(hitResult.offset)) { - return link; - } - } - } - return null; - } - - bool _handleLinkTap(Offset tapPosition) { - _cursor = MouseCursor.defer; - final link = _findLinkAtPosition(tapPosition); - if (link != null) { - final onLinkTap = _state.widget.params.linkHandlerParams?.onLinkTap; - if (onLinkTap != null) { - onLinkTap(link); - return true; - } - } - _state._clearAllTextSelections(); - return false; - } - - void _handleLinkMouseCursor(Offset position, void Function(void Function()) setState) { - final link = _findLinkAtPosition(position); - final newCursor = link == null ? MouseCursor.defer : SystemMouseCursors.click; - if (newCursor != _cursor) { - _cursor = newCursor; - setState(() {}); - } - } - - /// Creates a [GestureDetector] for handling link taps and mouse cursor. - Widget linkHandlingOverlay(Size size) { - return GestureDetector( - behavior: HitTestBehavior.translucent, - // link taps - onTapUp: (details) => _handleLinkTap(details.localPosition), - child: StatefulBuilder( - builder: (context, setState) { - return MouseRegion( - hitTestBehavior: HitTestBehavior.translucent, - onHover: (event) => _handleLinkMouseCursor(event.localPosition, setState), - onExit: (event) { - _cursor = MouseCursor.defer; - setState(() {}); - }, - cursor: _cursor, - child: IgnorePointer(child: SizedBox(width: size.width, height: size.height)), - ); - }, - ), - ); - } - - /// Paints the link highlights. - void paintLinkHighlights(Canvas canvas, Rect pageRect, PdfPage page) { - final links = _ensureLinksLoaded(page); - if (links == null) return; - - final customPainter = _state.widget.params.linkHandlerParams?.customPainter; - - if (customPainter != null) { - customPainter.call(canvas, pageRect, page, links); - return; - } - - final paint = - Paint() - ..color = _state.widget.params.linkHandlerParams?.linkColor ?? Colors.blue.withAlpha(50) - ..style = PaintingStyle.fill; - for (final link in links) { - for (final rect in link.rects) { - final rectLink = rect.toRectInPageRect(page: page, pageRect: pageRect); - canvas.drawRect(rectLink, paint); - } - } - } -} diff --git a/lib/src/widgets/pdf_viewer_params.dart b/lib/src/widgets/pdf_viewer_params.dart deleted file mode 100644 index bc635c8e..00000000 --- a/lib/src/widgets/pdf_viewer_params.dart +++ /dev/null @@ -1,832 +0,0 @@ -import 'dart:ui' as ui; - -import 'package:flutter/material.dart'; - -import '../../pdfrx.dart'; - -/// Viewer customization parameters. -/// -/// Changes to several builder functions such as [layoutPages] does not -/// take effect until the viewer is re-layout-ed. You can relayout the viewer by calling [PdfViewerController.relayout]. -@immutable -class PdfViewerParams { - const PdfViewerParams({ - this.margin = 8.0, - this.backgroundColor = Colors.grey, - this.layoutPages, - this.normalizeMatrix, - this.maxScale = 8.0, - this.minScale = 0.1, - this.useAlternativeFitScaleAsMinScale = true, - this.panAxis = PanAxis.free, - this.boundaryMargin, - this.annotationRenderingMode = PdfAnnotationRenderingMode.annotationAndForms, - this.pageAnchor = PdfPageAnchor.top, - this.pageAnchorEnd = PdfPageAnchor.bottom, - this.onePassRenderingScaleThreshold = 200 / 72, - this.enableTextSelection = false, - this.matchTextColor, - this.activeMatchTextColor, - this.pageDropShadow = const BoxShadow(color: Colors.black54, blurRadius: 4, spreadRadius: 2, offset: Offset(2, 2)), - this.panEnabled = true, - this.scaleEnabled = true, - this.onInteractionEnd, - this.onInteractionStart, - this.onInteractionUpdate, - this.interactionEndFrictionCoefficient = _kDrag, - this.onDocumentChanged, - this.calculateInitialPageNumber, - this.calculateCurrentPageNumber, - this.onViewerReady, - this.onViewSizeChanged, - this.onPageChanged, - this.getPageRenderingScale, - this.scrollByMouseWheel = 0.2, - this.enableKeyboardNavigation = true, - this.scrollByArrowKey = 25.0, - this.maxImageBytesCachedOnMemory = 100 * 1024 * 1024, - this.horizontalCacheExtent = 1.0, - this.verticalCacheExtent = 1.0, - this.linkHandlerParams, - this.viewerOverlayBuilder, - this.pageOverlaysBuilder, - this.loadingBannerBuilder, - this.errorBannerBuilder, - this.linkWidgetBuilder, - this.pagePaintCallbacks, - this.pageBackgroundPaintCallbacks, - this.onTextSelectionChange, - this.selectableRegionInjector, - this.perPageSelectableRegionInjector, - this.forceReload = false, - }); - - /// Margin around the page. - final double margin; - - /// Background color of the viewer. - final Color backgroundColor; - - /// Function to customize the layout of the pages. - /// - /// Changes to this function does not take effect until the viewer is re-layout-ed. You can relayout the viewer by calling [PdfViewerController.relayout]. - /// - /// The following fragment is an example to layout pages horizontally with margin: - /// - /// ```dart - /// PdfViewerParams( - /// layoutPages: (pages, params) { - /// final height = pages.fold( - /// 0.0, (prev, page) => max(prev, page.height)) + params.margin * 2; - /// final pageLayouts = []; - /// double x = params.margin; - /// for (final page in pages) { - /// pageLayouts.add( - /// Rect.fromLTWH( - /// x, - /// (height - page.height) / 2, // center vertically - /// page.width, - /// page.height, - /// ), - /// ); - /// x += page.width + params.margin; - /// } - /// return PageLayout(pageLayouts: pageLayouts, documentSize: Size(x, height)); - /// }, - /// ), - /// ``` - final PdfPageLayoutFunction? layoutPages; - - /// Function to normalize the matrix. - /// - /// The function is called when the matrix is changed and normally used to restrict the matrix to certain range. - /// - /// The following fragment is an example to restrict the matrix to the document size, which is almost identical to - /// the default behavior: - /// - /// ```dart - /// PdfViewerParams( - /// normalizeMatrix: (matrix, viewSize, layout, controller) { - /// // If the controller is not ready, just return the input matrix. - /// if (controller == null || !controller.isReady) return matrix; - /// final position = newValue.calcPosition(viewSize); - /// final newZoom = controller.params.boundaryMargin != null - /// ? newValue.zoom - /// : max(newValue.zoom, controller.minScale); - /// final hw = viewSize.width / 2 / newZoom; - /// final hh = viewSize.height / 2 / newZoom; - /// final x = position.dx.range(hw, layout.documentSize.width - hw); - /// final y = position.dy.range(hh, layout.documentSize.height - hh); - /// return controller.calcMatrixFor(Offset(x, y), zoom: newZoom, viewSize: viewSize); - /// }, - /// ), - /// ``` - final PdfMatrixNormalizeFunction? normalizeMatrix; - - /// The maximum allowed scale. - /// - /// The default is 8.0. - final double maxScale; - - /// The minimum allowed scale. - /// - /// The default is 0.1. - /// - /// Please note that the value is not used if [useAlternativeFitScaleAsMinScale] is true. - /// See [useAlternativeFitScaleAsMinScale] for the details. - final double minScale; - - /// If true, the minimum scale is set to the calculated [PdfViewerController.alternativeFitScale]. - /// - /// If the minimum scale is small value, it makes many pages visible inside the view and it finally - /// renders many pages at once. It may make the viewer to be slow or even crash due to high memory consumption. - /// So, it is recommended to set this to false if you want to show PDF documents with many pages. - final bool useAlternativeFitScaleAsMinScale; - - /// See [InteractiveViewer.panAxis] for details. - final PanAxis panAxis; - - /// See [InteractiveViewer.boundaryMargin] for details. - /// - /// The default is `EdgeInsets.all(double.infinity)`. - final EdgeInsets? boundaryMargin; - - /// Annotation rendering mode. - final PdfAnnotationRenderingMode annotationRenderingMode; - - /// Anchor to position the page. - final PdfPageAnchor pageAnchor; - - /// Anchor to position the page at the end of the page. - final PdfPageAnchor pageAnchorEnd; - - /// If a page is rendered over the scale threshold, the page is rendered with the threshold scale - /// and actual resolution image is rendered after some delay (progressive rendering). - /// - /// Basically, if the value is larger, the viewer renders each page in one-pass rendering; it is - /// faster and looks better to the user. However, larger value may consume more memory. - /// So you may want to set the smaller value to reduce memory consumption. - /// - /// The default is 200 / 72, which implies rendering at 200 dpi. - /// If you want more granular control for each page, use [getPageRenderingScale]. - final double onePassRenderingScaleThreshold; - - /// Enable text selection on pages. - /// - /// The default is false. - /// If it is true, the text selection is enabled by injecting [SelectionArea] - /// internally. - /// - /// Basically, you can enable text selection by setting one (or more) of the following parameters: - /// - [enableTextSelection] to enable [SelectionArea] on the viewer - /// - [selectableRegionInjector] to inject your own [SelectableRegion] on the viewer - /// - [perPageSelectableRegionInjector] to inject your own [SelectableRegion] on each page - final bool enableTextSelection; - - /// Color for text search match. - /// - /// If null, the default color is `Colors.yellow.withOpacity(0.5)`. - final Color? matchTextColor; - - /// Color for active text search match. - /// - /// If null, the default color is `Colors.orange.withOpacity(0.5)`. - final Color? activeMatchTextColor; - - /// Drop shadow for the page. - /// - /// The default is: - /// ```dart - /// BoxShadow( - /// color: Colors.black54, - /// blurRadius: 4, - /// spreadRadius: 0, - /// offset: Offset(2, 2)) - /// ``` - /// - /// If you need to remove the shadow, set this to null. - /// To customize more of the shadow, you can use [pageBackgroundPaintCallbacks] to paint the shadow manually. - final BoxShadow? pageDropShadow; - - /// See [InteractiveViewer.panEnabled] for details. - final bool panEnabled; - - /// See [InteractiveViewer.scaleEnabled] for details. - final bool scaleEnabled; - - /// See [InteractiveViewer.onInteractionEnd] for details. - final GestureScaleEndCallback? onInteractionEnd; - - /// See [InteractiveViewer.onInteractionStart] for details. - final GestureScaleStartCallback? onInteractionStart; - - /// See [InteractiveViewer.onInteractionUpdate] for details. - final GestureScaleUpdateCallback? onInteractionUpdate; - - /// See [InteractiveViewer.interactionEndFrictionCoefficient] for details. - final double interactionEndFrictionCoefficient; - - // Used as the coefficient of friction in the inertial translation animation. - // This value was eyeballed to give a feel similar to Google Photos. - static const double _kDrag = 0.0000135; - - /// Function to notify that the document is loaded/changed. - /// - /// The function is called even if the document is null (it means the document is unloaded). - /// If you want to be notified when the viewer is ready to interact, use [onViewerReady] instead. - final PdfViewerDocumentChangedCallback? onDocumentChanged; - - /// Function called when the viewer is ready. - /// - /// Unlike [PdfViewerDocumentChangedCallback], this function is called after the viewer is ready to interact. - final PdfViewerReadyCallback? onViewerReady; - - /// Function to be notified when the viewer size is changed. - /// - /// Please note that the function might be called during widget build, - /// so you should not synchronously call functions that may cause rebuild; - /// instead, you can use [Future.microtask] or [Future.delayed] to schedule the function call after the build. - /// - /// The following code illustrates how to keep the center position during device screen rotation: - /// - /// ```dart - /// onViewSizeChanged: (viewSize, oldViewSize, controller) { - /// if (oldViewSize != null) { - /// // The most important thing here is that the transformation matrix - /// // is not changed on the view change. - /// final centerPosition = - /// controller.value.calcPosition(oldViewSize); - /// final newMatrix = - /// controller.calcMatrixFor(centerPosition); - /// // Don't change the matrix in sync; the callback might be called - /// // during widget-tree's build process. - /// Future.delayed( - /// const Duration(milliseconds: 200), - /// () => controller.goTo(newMatrix), - /// ); - /// } - /// }, - /// ``` - final PdfViewerViewSizeChanged? onViewSizeChanged; - - /// Function to calculate the initial page number. - /// - /// It is useful when you want to determine the initial page number based on the document content. - final PdfViewerCalculateInitialPageNumberFunction? calculateInitialPageNumber; - - /// Function to guess the current page number based on the visible rectangle and page layouts. - /// - /// The function is used to override the default behavior to calculate the current page number. - final PdfViewerCalculateCurrentPageNumberFunction? calculateCurrentPageNumber; - - /// Function called when the current page is changed. - final PdfPageChangedCallback? onPageChanged; - - /// Function to customize the rendering scale of the page. - /// - /// In some cases, if [maxScale]/[onePassRenderingScaleThreshold] is too large, - /// certain pages may not be rendered correctly due to memory limitation, - /// or anyway they may take too long to render. - /// In such cases, you can use this function to customize the rendering scales - /// for such pages. - /// - /// The following fragment is an example of rendering pages always on 300 dpi: - /// ```dart - /// PdfViewerParams( - /// getPageRenderingScale: (context, page, controller, estimatedScale) { - /// return 300 / 72; - /// }, - /// ), - /// ``` - /// - /// The following fragment is more realistic example to restrict the rendering - /// resolution to maximum to 6000 pixels: - /// ```dart - /// PdfViewerParams( - /// getPageRenderingScale: (context, page, controller, estimatedScale) { - /// final width = page.width * estimatedScale; - /// final height = page.height * estimatedScale; - /// if (width > 6000 || height > 6000) { - /// return min(6000 / page.width, 6000 / page.height); - /// } - /// return estimatedScale; - /// }, - /// ), - /// ``` - final PdfViewerGetPageRenderingScale? getPageRenderingScale; - - /// Set the scroll amount ratio by mouse wheel. The default is 0.2. - /// - /// Negative value to scroll opposite direction. - /// null to disable scroll-by-mouse-wheel. - final double? scrollByMouseWheel; - - /// Enable keyboard navigation. The default is true. - final bool enableKeyboardNavigation; - - /// Amount of pixels to scroll by arrow keys. The default is 25.0. - final double scrollByArrowKey; - - /// Restrict the total amount of image bytes to be cached on memory. The default is 100 MB. - /// - /// The internal cache mechanism tries to limit the actual memory usage under the value but it is not guaranteed. - final int maxImageBytesCachedOnMemory; - - /// The horizontal cache extent specified in ratio to the viewport width. The default is 1.0. - final double horizontalCacheExtent; - - /// The vertical cache extent specified in ratio to the viewport height. The default is 1.0. - final double verticalCacheExtent; - - /// Parameters for the built-in link handler. - /// - /// It is mutually exclusive with [linkWidgetBuilder]. - final PdfLinkHandlerParams? linkHandlerParams; - - /// Add overlays to the viewer. - /// - /// This function is to generate widgets on PDF viewer's overlay [Stack]. - /// The widgets can be layed out using layout widgets such as [Positioned] and [Align]. - /// - /// The most typical use case is to add scroll thumbs to the viewer. - /// The following fragment illustrates how to add vertical and horizontal scroll thumbs: - /// - /// ```dart - /// viewerOverlayBuilder: (context, size, handleLinkTap) => [ - /// PdfViewerScrollThumb( - /// controller: controller, - /// orientation: ScrollbarOrientation.right), - /// PdfViewerScrollThumb( - /// controller: controller, - /// orientation: ScrollbarOrientation.bottom), - /// ], - /// ``` - /// - /// For more information, see [PdfViewerScrollThumb]. - /// - /// ### Note for using [GestureDetector] inside [viewerOverlayBuilder]: - /// You may want to use [GestureDetector] inside [viewerOverlayBuilder] to handle certain gesture events. - /// In such cases, your [GestureDetector] eats the gestures and the viewer cannot handle them directly. - /// So, when you use [GestureDetector] inside [viewerOverlayBuilder], please ensure the following things: - /// - /// - [GestureDetector.behavior] should be [HitTestBehavior.translucent] - /// - [GestureDetector.onTapUp] (or such depending on your situation) should call `handleLinkTap` to handle link tap - /// - /// The following fragment illustrates how to handle link tap in [GestureDetector]: - /// ```dart - /// viewerOverlayBuilder: (context, size, handleLinkTap) => [ - /// GestureDetector( - /// behavior: HitTestBehavior.translucent, - /// onTapUp: (details) => handleLinkTap(details.localPosition), - /// // Make the GestureDetector covers all the viewer widget's area - /// // but also make the event go through to the viewer. - /// child: IgnorePointer(child: SizedBox(width: size.width, height: size.height)), - /// ... - /// ), - /// ... - /// ] - /// ``` - /// - final PdfViewerOverlaysBuilder? viewerOverlayBuilder; - - /// Add overlays to each page. - /// - /// This function is used to decorate each page with overlay widgets. - /// - /// The return value of the function is a list of widgets to be laid out on the page; - /// they are actually laid out on the page using [Stack]. - /// - /// There are many actual overlays on the page; the page overlays are; - /// - Page image - /// - Selectable page text - /// - Links (if [linkWidgetBuilder] is not null; otherwise links are handled by another logic) - /// - Overlay widgets returned by this function - /// - /// The most typical use case is to add page number footer to each page. - /// - /// The following fragment illustrates how to add page number footer to each page: - /// ```dart - /// pageOverlaysBuilder: (context, pageRect, page) { - /// return [ - /// Align( - /// alignment: Alignment.bottomCenter, - /// child: Text( - /// page.pageNumber.toString(), - /// style: const TextStyle(color: Colors.red), - /// ), - /// ), - /// ]; - /// }, - /// ``` - final PdfPageOverlaysBuilder? pageOverlaysBuilder; - - /// Build loading banner. - /// - /// Please note that the progress is only reported for [PdfViewer.uri] on non-Web platforms. - /// - /// The following fragment illustrates how to build loading banner that shows the download progress: - /// - /// ```dart - /// loadingBannerBuilder: (context, bytesDownloaded, totalBytes) { - /// return Center( - /// child: CircularProgressIndicator( - /// // totalBytes is null if the total bytes is unknown - /// value: totalBytes != null ? bytesDownloaded / totalBytes : null, - /// backgroundColor: Colors.grey, - /// ), - /// ); - /// }, - /// ``` - final PdfViewerLoadingBannerBuilder? loadingBannerBuilder; - - /// Build loading error banner. - final PdfViewerErrorBannerBuilder? errorBannerBuilder; - - /// Build link widget. - /// - /// If [linkHandlerParams] is specified, it is ignored. - /// - /// Basically, handling links with widgets are not recommended because it is not efficient. - /// [linkHandlerParams] is the recommended way to handle links. - final PdfLinkWidgetBuilder? linkWidgetBuilder; - - /// Callback to paint over the rendered page. - /// - /// For the detail usage, see [PdfViewerPagePaintCallback]. - final List? pagePaintCallbacks; - - /// Callback to paint on the background of the rendered page (called before painting the page content). - /// - /// It is useful to paint some background such as drop shadow of the page. - /// For the detail usage, see [PdfViewerPagePaintCallback]. - final List? pageBackgroundPaintCallbacks; - - /// Function to be notified when the text selection is changed. - final PdfViewerTextSelectionChangeCallback? onTextSelectionChange; - - /// Function to inject [SelectionArea] or [SelectableRegion] to customize text selection. - /// - /// It can be also used to "remove" the text selection feature by returning the child widget as it is. - /// Furthermore, it can be used to customize the text selection feature by returning a custom widget. - /// - /// Basically, you can enable text selection by setting one (or more) of the following parameters: - /// - [enableTextSelection] to enable [SelectionArea] on the viewer - /// - [selectableRegionInjector] to inject your own [SelectableRegion] on the viewer - /// - [perPageSelectableRegionInjector] to inject your own [SelectableRegion] on each page - /// - /// You can even enable both of [selectableRegionInjector] and [perPageSelectableRegionInjector] at the same time. - final PdfSelectableRegionInjector? selectableRegionInjector; - - /// Function to inject [SelectionArea] or [SelectableRegion] to customize text selection on each page. - /// - /// It can be also used to "remove" the text selection feature by returning the child widget as it is. - /// Furthermore, it can be used to customize the text selection feature by returning a custom widget. - /// - /// Basically, you can enable text selection by setting one (or more) of the following parameters: - /// - [enableTextSelection] to enable [SelectionArea] on the viewer - /// - [selectableRegionInjector] to inject your own [SelectableRegion] on the viewer - /// - [perPageSelectableRegionInjector] to inject your own [SelectableRegion] on each page - /// - /// You can even enable both of [selectableRegionInjector] and [perPageSelectableRegionInjector] at the same time. - final PdfPerPageSelectableRegionInjector? perPageSelectableRegionInjector; - - /// Force reload the viewer. - /// - /// Normally whether to reload the viewer is determined by the changes of the parameters but - /// if you want to force reload the viewer, set this to true. - /// - /// Because changing certain fields like functions on [PdfViewerParams] does not run hot-reload on Flutter, - /// sometimes it is useful to force reload the viewer by setting this to true. - final bool forceReload; - - /// Determine whether the viewer needs to be reloaded or not. - /// - bool doChangesRequireReload(PdfViewerParams? other) { - return other == null || - forceReload || - other.margin != margin || - other.backgroundColor != backgroundColor || - other.maxScale != maxScale || - other.minScale != minScale || - other.useAlternativeFitScaleAsMinScale != useAlternativeFitScaleAsMinScale || - other.panAxis != panAxis || - other.boundaryMargin != boundaryMargin || - other.annotationRenderingMode != annotationRenderingMode || - other.pageAnchor != pageAnchor || - other.pageAnchorEnd != pageAnchorEnd || - other.onePassRenderingScaleThreshold != onePassRenderingScaleThreshold || - other.enableTextSelection != enableTextSelection || - other.matchTextColor != matchTextColor || - other.activeMatchTextColor != activeMatchTextColor || - other.pageDropShadow != pageDropShadow || - other.panEnabled != panEnabled || - other.scaleEnabled != scaleEnabled || - other.interactionEndFrictionCoefficient != interactionEndFrictionCoefficient || - other.scrollByMouseWheel != scrollByMouseWheel || - other.enableKeyboardNavigation != enableKeyboardNavigation || - other.scrollByArrowKey != scrollByArrowKey || - other.horizontalCacheExtent != horizontalCacheExtent || - other.verticalCacheExtent != verticalCacheExtent || - other.linkHandlerParams != linkHandlerParams; - } - - @override - bool operator ==(covariant PdfViewerParams other) { - if (identical(this, other)) return true; - - return other.margin == margin && - other.backgroundColor == backgroundColor && - other.maxScale == maxScale && - other.minScale == minScale && - other.useAlternativeFitScaleAsMinScale == useAlternativeFitScaleAsMinScale && - other.panAxis == panAxis && - other.boundaryMargin == boundaryMargin && - other.annotationRenderingMode == annotationRenderingMode && - other.pageAnchor == pageAnchor && - other.pageAnchorEnd == pageAnchorEnd && - other.onePassRenderingScaleThreshold == onePassRenderingScaleThreshold && - other.enableTextSelection == enableTextSelection && - other.matchTextColor == matchTextColor && - other.activeMatchTextColor == activeMatchTextColor && - other.pageDropShadow == pageDropShadow && - other.panEnabled == panEnabled && - other.scaleEnabled == scaleEnabled && - other.onInteractionEnd == onInteractionEnd && - other.onInteractionStart == onInteractionStart && - other.onInteractionUpdate == onInteractionUpdate && - other.interactionEndFrictionCoefficient == interactionEndFrictionCoefficient && - other.onDocumentChanged == onDocumentChanged && - other.calculateInitialPageNumber == calculateInitialPageNumber && - other.calculateCurrentPageNumber == calculateCurrentPageNumber && - other.onViewerReady == onViewerReady && - other.onViewSizeChanged == onViewSizeChanged && - other.onPageChanged == onPageChanged && - other.getPageRenderingScale == getPageRenderingScale && - other.scrollByMouseWheel == scrollByMouseWheel && - other.enableKeyboardNavigation == enableKeyboardNavigation && - other.scrollByArrowKey == scrollByArrowKey && - other.horizontalCacheExtent == horizontalCacheExtent && - other.verticalCacheExtent == verticalCacheExtent && - other.linkHandlerParams == linkHandlerParams && - other.viewerOverlayBuilder == viewerOverlayBuilder && - other.pageOverlaysBuilder == pageOverlaysBuilder && - other.loadingBannerBuilder == loadingBannerBuilder && - other.errorBannerBuilder == errorBannerBuilder && - other.linkWidgetBuilder == linkWidgetBuilder && - other.pagePaintCallbacks == pagePaintCallbacks && - other.pageBackgroundPaintCallbacks == pageBackgroundPaintCallbacks && - other.onTextSelectionChange == onTextSelectionChange && - other.selectableRegionInjector == selectableRegionInjector && - other.perPageSelectableRegionInjector == perPageSelectableRegionInjector && - other.forceReload == forceReload; - } - - @override - int get hashCode { - return margin.hashCode ^ - backgroundColor.hashCode ^ - maxScale.hashCode ^ - minScale.hashCode ^ - useAlternativeFitScaleAsMinScale.hashCode ^ - panAxis.hashCode ^ - boundaryMargin.hashCode ^ - annotationRenderingMode.hashCode ^ - pageAnchor.hashCode ^ - pageAnchorEnd.hashCode ^ - onePassRenderingScaleThreshold.hashCode ^ - enableTextSelection.hashCode ^ - matchTextColor.hashCode ^ - activeMatchTextColor.hashCode ^ - pageDropShadow.hashCode ^ - panEnabled.hashCode ^ - scaleEnabled.hashCode ^ - onInteractionEnd.hashCode ^ - onInteractionStart.hashCode ^ - onInteractionUpdate.hashCode ^ - interactionEndFrictionCoefficient.hashCode ^ - onDocumentChanged.hashCode ^ - calculateInitialPageNumber.hashCode ^ - calculateCurrentPageNumber.hashCode ^ - onViewerReady.hashCode ^ - onViewSizeChanged.hashCode ^ - onPageChanged.hashCode ^ - getPageRenderingScale.hashCode ^ - scrollByMouseWheel.hashCode ^ - enableKeyboardNavigation.hashCode ^ - scrollByArrowKey.hashCode ^ - horizontalCacheExtent.hashCode ^ - verticalCacheExtent.hashCode ^ - linkHandlerParams.hashCode ^ - viewerOverlayBuilder.hashCode ^ - pageOverlaysBuilder.hashCode ^ - loadingBannerBuilder.hashCode ^ - errorBannerBuilder.hashCode ^ - linkWidgetBuilder.hashCode ^ - pagePaintCallbacks.hashCode ^ - pageBackgroundPaintCallbacks.hashCode ^ - onTextSelectionChange.hashCode ^ - selectableRegionInjector.hashCode ^ - perPageSelectableRegionInjector.hashCode ^ - forceReload.hashCode; - } -} - -/// Function to notify that the document is loaded/changed. -typedef PdfViewerDocumentChangedCallback = void Function(PdfDocument? document); - -/// Function to calculate the initial page number. -/// -/// If the function returns null, the viewer will show the page of [PdfViewer.initialPageNumber]. -typedef PdfViewerCalculateInitialPageNumberFunction = - int? Function(PdfDocument document, PdfViewerController controller); - -/// Function to guess the current page number based on the visible rectangle and page layouts. -typedef PdfViewerCalculateCurrentPageNumberFunction = - int? Function(Rect visibleRect, List pageRects, PdfViewerController controller); - -/// Function called when the viewer is ready. -/// -typedef PdfViewerReadyCallback = void Function(PdfDocument document, PdfViewerController controller); - -/// Function to be called when the viewer view size is changed. -/// -/// [viewSize] is the new view size. -/// [oldViewSize] is the previous view size. -typedef PdfViewerViewSizeChanged = void Function(Size viewSize, Size? oldViewSize, PdfViewerController controller); - -/// Function called when the current page is changed. -typedef PdfPageChangedCallback = void Function(int? pageNumber); - -/// Function to customize the rendering scale of the page. -/// -/// - [context] is normally used to call [MediaQuery.of] to get the device pixel ratio -/// - [page] can be used to determine the page dimensions -/// - [controller] can be used to get the current zoom by [PdfViewerController.currentZoom] -/// - [estimatedScale] is the precalculated scale for the page -typedef PdfViewerGetPageRenderingScale = - double? Function(BuildContext context, PdfPage page, PdfViewerController controller, double estimatedScale); - -/// Function to customize the layout of the pages. -/// -/// - [pages] is the list of pages. -/// This is just a copy of the first loaded page of the document. -/// - [params] is the viewer parameters. -typedef PdfPageLayoutFunction = PdfPageLayout Function(List pages, PdfViewerParams params); - -/// Function to normalize the matrix. -/// -/// The function is called when the matrix is changed and normally used to restrict the matrix to certain range. -/// -/// Another use case is to do something when the matrix is changed. -/// -/// If no actual matrix change is needed, just return the input matrix. -typedef PdfMatrixNormalizeFunction = - Matrix4 Function(Matrix4 matrix, Size viewSize, PdfPageLayout layout, PdfViewerController? controller); - -/// Function to build viewer overlays. -/// -/// [size] is the size of the viewer widget. -/// [handleLinkTap] is a function to handle link tap. For more details, see [PdfViewerParams.viewerOverlayBuilder]. -typedef PdfViewerOverlaysBuilder = - List Function(BuildContext context, Size size, PdfViewerHandleLinkTap handleLinkTap); - -/// Function to handle link tap. -/// -/// The function returns true if it processes the link on the specified position; otherwise, returns false. -/// [position] is the position of the tap in the viewer; -/// typically it is [GestureDetector.onTapUp]'s [TapUpDetails.localPosition]. -typedef PdfViewerHandleLinkTap = bool Function(Offset position); - -/// Function to build page overlays. -/// -/// [pageRect] is the rectangle of the page in the viewer. -/// [page] is the page. -typedef PdfPageOverlaysBuilder = List Function(BuildContext context, Rect pageRect, PdfPage page); - -/// Function to build loading banner. -/// -/// [bytesDownloaded] is the number of bytes downloaded so far. -/// [totalBytes] is the total number of bytes to be downloaded if available. -typedef PdfViewerLoadingBannerBuilder = Widget Function(BuildContext context, int bytesDownloaded, int? totalBytes); - -/// Function to build loading error banner. -typedef PdfViewerErrorBannerBuilder = - Widget Function(BuildContext context, Object error, StackTrace? stackTrace, PdfDocumentRef documentRef); - -/// Function to build link widget for [PdfLink]. -/// -/// [size] is the size of the link. -typedef PdfLinkWidgetBuilder = Widget? Function(BuildContext context, PdfLink link, Size size); - -/// Function to inject [SelectionArea] or [SelectableRegion] to customize text selection. -typedef PdfSelectableRegionInjector = Widget Function(BuildContext context, Widget child); - -/// Function to inject [SelectionArea] or [SelectableRegion] to customize text selection on each page. -/// -/// [pageRect] is the rectangle of the page in the viewer. -typedef PdfPerPageSelectableRegionInjector = - Widget Function(BuildContext context, Widget child, PdfPage page, Rect pageRect); - -/// Function to paint things on page. -/// -/// [canvas] is the canvas to paint on. -/// [pageRect] is the rectangle of the page in the viewer. -/// [page] is the page. -/// -/// If you have some [PdfRect] that describes something on the page, -/// you can use [PdfRect.toRect] to convert it to [Rect] and draw the rect on the canvas: -/// -/// ```dart -/// PdfRect pdfRect = ...; -/// canvas.drawRect( -/// pdfRect.toRectInPageRect(page: page, pageRect: pageRect), -/// Paint()..color = Colors.red); -/// ``` -typedef PdfViewerPagePaintCallback = void Function(ui.Canvas canvas, Rect pageRect, PdfPage page); - -/// Function to be notified when the text selection is changed. -/// -/// [selections] contains the selected text ranges on each page. -typedef PdfViewerTextSelectionChangeCallback = void Function(List selections); - -/// When [PdfViewerController.goToPage] is called, the page is aligned to the specified anchor. -/// -/// If the viewer area is smaller than the page, only some part of the page is shown in the viewer. -/// And the anchor determines which part of the page should be shown in the viewer when [PdfViewerController.goToPage] -/// is called. -/// -/// If you prefer to show the top of the page, [top] will do that. -/// -/// If you prefer to show whole the page even if the page will be zoomed down to fit into the viewer, -/// [all] will do that. -/// -/// Basically, [top], [left], [right], [bottom] anchors are used to make page edge line of that side visible inside -/// the view area. -/// -/// [topLeft], [topCenter], [topRight], [centerLeft], [center], [centerRight], [bottomLeft], [bottomCenter], -/// and [bottomRight] are used to make the "point" visible inside the view area. -/// -enum PdfPageAnchor { - top, - left, - right, - bottom, - topLeft, - topCenter, - topRight, - centerLeft, - center, - centerRight, - bottomLeft, - bottomCenter, - bottomRight, - all, -} - -/// Parameters to customize link handling/appearance. -class PdfLinkHandlerParams { - const PdfLinkHandlerParams({required this.onLinkTap, this.linkColor, this.customPainter}); - - /// Function to be called when the link is tapped. - /// - /// The functions should return true if it processes the link; otherwise, it should return false. - final void Function(PdfLink link) onLinkTap; - - /// Color for the link. If null, the default color is `Colors.blue.withOpacity(0.2)`. - /// - /// To fully customize the link appearance, use [customPainter]. - final Color? linkColor; - - /// Custom link painter for the page. - /// - /// The custom painter completely overrides the default link painter. - /// The following fragment is an example to draw a red rectangle on the link area: - /// - /// ```dart - /// customPainter: (canvas, pageRect, page, links) { - /// final paint = Paint() - /// ..color = Colors.red.withOpacity(0.2) - /// ..style = PaintingStyle.fill; - /// for (final link in links) { - /// final rect = link.rect.toRectInPageRect(page: page, pageRect: pageRect); - /// canvas.drawRect(rect, paint); - /// } - /// } - /// ``` - final PdfLinkCustomPagePainter? customPainter; - - @override - bool operator ==(covariant PdfLinkHandlerParams other) { - if (identical(this, other)) return true; - - return other.onLinkTap == onLinkTap && other.linkColor == linkColor && other.customPainter == customPainter; - } - - @override - int get hashCode { - return onLinkTap.hashCode ^ linkColor.hashCode ^ customPainter.hashCode; - } -} - -/// Custom painter for the page links. -typedef PdfLinkCustomPagePainter = void Function(ui.Canvas canvas, Rect pageRect, PdfPage page, List links); diff --git a/packages/pdfium_dart/.gitignore b/packages/pdfium_dart/.gitignore new file mode 100644 index 00000000..58a0dae1 --- /dev/null +++ b/packages/pdfium_dart/.gitignore @@ -0,0 +1,2 @@ +test/.tmp/ + diff --git a/packages/pdfium_dart/CHANGELOG.md b/packages/pdfium_dart/CHANGELOG.md new file mode 100644 index 00000000..8bacf531 --- /dev/null +++ b/packages/pdfium_dart/CHANGELOG.md @@ -0,0 +1,19 @@ +## 0.1.3 + +- Documentation updates. + +## 0.1.2 + +- Updated PDFium to version 144.0.7520.0. +- Improved cache directory structure to support multiple PDFium releases. +- Changed `tmpPath` parameter to `cacheRootPath` in `getPdfium()` for better clarity. +- Enhanced documentation for PDFium download and caching mechanism. + +## 0.1.1 + +- Add comments on PDFium class. +- Several PDFium capitalization fixes affecting API names. + +## 0.1.0 + +- First release. diff --git a/packages/pdfium_dart/LICENSE b/packages/pdfium_dart/LICENSE new file mode 100644 index 00000000..7de3ad69 --- /dev/null +++ b/packages/pdfium_dart/LICENSE @@ -0,0 +1,11 @@ + +The MIT License (MIT) +=============== + +Copyright (c) 2025 @espresso3389 (Takashi Kawasaki) + +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. diff --git a/packages/pdfium_dart/README.md b/packages/pdfium_dart/README.md new file mode 100644 index 00000000..ac1690bc --- /dev/null +++ b/packages/pdfium_dart/README.md @@ -0,0 +1,107 @@ +# pdfium_dart + +Dart FFI bindings for the PDFium library. This package provides low-level access to PDFium's C API from Dart. + +This package is part of the [pdfrx](https://github.com/espresso3389/pdfrx) project. + +## Overview + +This package contains auto-generated FFI bindings for PDFium using [ffigen](https://pub.dev/packages/ffigen). It is designed to be a minimal, pure Dart package that other packages can depend on to access PDFium functionality. + +**Key Features:** + +- Pure Dart package with no Flutter dependencies +- Auto-generated FFI bindings using [ffigen](https://pub.dev/packages/ffigen) +- Provides direct access to PDFium's C API +- Includes [getPdfium()](https://pub.dev/documentation/pdfium_dart/latest/pdfium_dart/getPdfium.html) function for on-demand PDFium binary downloads +- Supports Windows (x64), Linux (x64, ARM64), and macOS (x64, ARM64) + +## Usage + +### Basic Usage + +This package is primarily intended to be used as a dependency by higher-level packages like [pdfium_flutter](https://pub.dev/packages/pdfium_flutter) and [pdfrx_engine](https://pub.dev/packages/pdfrx_engine). Direct usage is possible but not recommended unless you need low-level PDFium access. + +```dart +import 'package:pdfium_dart/pdfium_dart.dart'; +import 'dart:ffi'; + +// If you already have PDFium loaded +final pdfium = PDFium(DynamicLibrary.open('/path/to/libpdfium.so')); +``` + +### On-Demand PDFium Downloads + +The [getPdfium](https://pub.dev/documentation/pdfium_dart/latest/pdfium_dart/getPdfium.html) function automatically downloads PDFium binaries on demand, making it easy to use PDFium in CLI applications or for testing without bundling binaries: + +```dart +import 'package:pdfium_dart/pdfium_dart.dart'; + +void main() async { + // Downloads PDFium binaries automatically if not cached + final pdfium = await getPdfium(); + + // Use PDFium API + // ... +} +``` + +**Note for macOS:** The downloaded library is not codesigned. If you encounter issues loading the library, you may need to manually codesign it: + +```bash +codesign --force --sign - +``` + +The binaries are downloaded from [bblanchon/pdfium-binaries](https://github.com/bblanchon/pdfium-binaries/releases) and cached in the system temp directory. + +## Generating Bindings + +### Prerequisites + +The [ffigen](https://pub.dev/packages/ffigen) process requires LLVM/Clang to be installed for parsing C headers: + +- **macOS**: Install via Homebrew + + ```bash + brew install llvm + ``` + +- **Linux**: Install via package manager + + ```bash + # Ubuntu/Debian + sudo apt-get install libclang-dev + + # Fedora + sudo dnf install clang-devel + ``` + +- **Windows**: Download and install LLVM from [llvm.org](https://releases.llvm.org/) + +### Regenerating Bindings + +To regenerate the FFI bindings: + +1. Run tests to download PDFium headers: + + ```bash + dart test + ``` + +2. Generate bindings: + + ```bash + dart run ffigen + ``` + +The bindings are generated from PDFium headers using the configuration in `ffigen.yaml`. + +## Platform Support + +| Platform | Architecture | Support | +|----------|-------------|---------| +| Windows | x64 | ✅ | +| Linux | x64, ARM64 | ✅ | +| macOS | x64, ARM64 | ✅ | + +**Note:** For Flutter applications with bundled PDFium binaries, use the [pdfium_flutter](https://pub.dev/packages/pdfium_flutter) package instead. diff --git a/packages/pdfium_dart/lib/pdfium_dart.dart b/packages/pdfium_dart/lib/pdfium_dart.dart new file mode 100644 index 00000000..e3a0d400 --- /dev/null +++ b/packages/pdfium_dart/lib/pdfium_dart.dart @@ -0,0 +1,9 @@ +/// Dart FFI bindings for PDFium library. +/// +/// This package provides low-level FFI bindings to the PDFium C API. +/// It is intended to be used by higher-level packages that provide +/// a more user-friendly API for working with PDF documents. +library pdfium_dart; + +export 'src/pdfium_bindings.dart'; +export 'src/pdfium_downloader.dart'; diff --git a/packages/pdfium_dart/lib/src/pdfium_bindings.dart b/packages/pdfium_dart/lib/src/pdfium_bindings.dart new file mode 100644 index 00000000..11ea8646 --- /dev/null +++ b/packages/pdfium_dart/lib/src/pdfium_bindings.dart @@ -0,0 +1,14020 @@ +// ignore_for_file: unused_field +// dart format off + +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. +// ignore_for_file: type=lint +import 'dart:ffi' as ffi; +/// Bindings for PDFium C API +class PDFium{ +/// Holds the symbol lookup function. +final ffi.Pointer Function(String symbolName) _lookup; + +/// The symbols are looked up in [dynamicLibrary]. +PDFium(ffi.DynamicLibrary dynamicLibrary): _lookup = dynamicLibrary.lookup; + +/// The symbols are looked up with [lookup]. +PDFium.fromLookup(ffi.Pointer Function(String symbolName) lookup): _lookup = lookup; + +/// Function: FPDF_InitLibraryWithConfig +/// Initialize the PDFium library and allocate global resources for it. +/// Parameters: +/// config - configuration information as above. +/// Return value: +/// None. +/// Comments: +/// You have to call this function before you can call any PDF +/// processing functions. +void FPDF_InitLibraryWithConfig(ffi.Pointer config, +) { + return _FPDF_InitLibraryWithConfig(config, +); +} + +late final _FPDF_InitLibraryWithConfigPtr = _lookup< + ffi.NativeFunction )>>('FPDF_InitLibraryWithConfig'); +late final _FPDF_InitLibraryWithConfig = _FPDF_InitLibraryWithConfigPtr.asFunction )>(); + +/// Function: FPDF_InitLibrary +/// Initialize the PDFium library (alternative form). +/// Parameters: +/// None +/// Return value: +/// None. +/// Comments: +/// Convenience function to call FPDF_InitLibraryWithConfig() with a +/// default configuration for backwards compatibility purposes. New +/// code should call FPDF_InitLibraryWithConfig() instead. This will +/// be deprecated in the future. +void FPDF_InitLibrary() { + return _FPDF_InitLibrary(); +} + +late final _FPDF_InitLibraryPtr = _lookup< + ffi.NativeFunction>('FPDF_InitLibrary'); +late final _FPDF_InitLibrary = _FPDF_InitLibraryPtr.asFunction(); + +/// Function: FPDF_DestroyLibrary +/// Release global resources allocated to the PDFium library by +/// FPDF_InitLibrary() or FPDF_InitLibraryWithConfig(). +/// Parameters: +/// None. +/// Return value: +/// None. +/// Comments: +/// After this function is called, you must not call any PDF +/// processing functions. +/// +/// Calling this function does not automatically close other +/// objects. It is recommended to close other objects before +/// closing the library with this function. +void FPDF_DestroyLibrary() { + return _FPDF_DestroyLibrary(); +} + +late final _FPDF_DestroyLibraryPtr = _lookup< + ffi.NativeFunction>('FPDF_DestroyLibrary'); +late final _FPDF_DestroyLibrary = _FPDF_DestroyLibraryPtr.asFunction(); + +/// Function: FPDF_SetSandBoxPolicy +/// Set the policy for the sandbox environment. +/// Parameters: +/// policy - The specified policy for setting, for example: +/// FPDF_POLICY_MACHINETIME_ACCESS. +/// enable - True to enable, false to disable the policy. +/// Return value: +/// None. +void FPDF_SetSandBoxPolicy(int policy, +int enable, +) { + return _FPDF_SetSandBoxPolicy(policy, +enable, +); +} + +late final _FPDF_SetSandBoxPolicyPtr = _lookup< + ffi.NativeFunction>('FPDF_SetSandBoxPolicy'); +late final _FPDF_SetSandBoxPolicy = _FPDF_SetSandBoxPolicyPtr.asFunction(); + +/// Function: FPDF_LoadDocument +/// Open and load a PDF document. +/// Parameters: +/// file_path - Path to the PDF file (including extension). +/// password - A string used as the password for the PDF file. +/// If no password is needed, empty or NULL can be used. +/// See comments below regarding the encoding. +/// Return value: +/// A handle to the loaded document, or NULL on failure. +/// Comments: +/// Loaded document can be closed by FPDF_CloseDocument(). +/// If this function fails, you can use FPDF_GetLastError() to retrieve +/// the reason why it failed. +/// +/// The encoding for |file_path| is UTF-8. +/// +/// The encoding for |password| can be either UTF-8 or Latin-1. PDFs, +/// depending on the security handler revision, will only accept one or +/// the other encoding. If |password|'s encoding and the PDF's expected +/// encoding do not match, FPDF_LoadDocument() will automatically +/// convert |password| to the other encoding. +FPDF_DOCUMENT FPDF_LoadDocument(FPDF_STRING file_path, +FPDF_BYTESTRING password, +) { + return _FPDF_LoadDocument(file_path, +password, +); +} + +late final _FPDF_LoadDocumentPtr = _lookup< + ffi.NativeFunction>('FPDF_LoadDocument'); +late final _FPDF_LoadDocument = _FPDF_LoadDocumentPtr.asFunction(); + +/// Function: FPDF_LoadMemDocument +/// Open and load a PDF document from memory. +/// Parameters: +/// data_buf - Pointer to a buffer containing the PDF document. +/// size - Number of bytes in the PDF document. +/// password - A string used as the password for the PDF file. +/// If no password is needed, empty or NULL can be used. +/// Return value: +/// A handle to the loaded document, or NULL on failure. +/// Comments: +/// The memory buffer must remain valid when the document is open. +/// The loaded document can be closed by FPDF_CloseDocument. +/// If this function fails, you can use FPDF_GetLastError() to retrieve +/// the reason why it failed. +/// +/// See the comments for FPDF_LoadDocument() regarding the encoding for +/// |password|. +/// Notes: +/// If PDFium is built with the XFA module, the application should call +/// FPDF_LoadXFA() function after the PDF document loaded to support XFA +/// fields defined in the fpdfformfill.h file. +FPDF_DOCUMENT FPDF_LoadMemDocument(ffi.Pointer data_buf, +int size, +FPDF_BYTESTRING password, +) { + return _FPDF_LoadMemDocument(data_buf, +size, +password, +); +} + +late final _FPDF_LoadMemDocumentPtr = _lookup< + ffi.NativeFunction , ffi.Int , FPDF_BYTESTRING )>>('FPDF_LoadMemDocument'); +late final _FPDF_LoadMemDocument = _FPDF_LoadMemDocumentPtr.asFunction , int , FPDF_BYTESTRING )>(); + +/// Experimental API. +/// Function: FPDF_LoadMemDocument64 +/// Open and load a PDF document from memory. +/// Parameters: +/// data_buf - Pointer to a buffer containing the PDF document. +/// size - Number of bytes in the PDF document. +/// password - A string used as the password for the PDF file. +/// If no password is needed, empty or NULL can be used. +/// Return value: +/// A handle to the loaded document, or NULL on failure. +/// Comments: +/// The memory buffer must remain valid when the document is open. +/// The loaded document can be closed by FPDF_CloseDocument. +/// If this function fails, you can use FPDF_GetLastError() to retrieve +/// the reason why it failed. +/// +/// See the comments for FPDF_LoadDocument() regarding the encoding for +/// |password|. +/// Notes: +/// If PDFium is built with the XFA module, the application should call +/// FPDF_LoadXFA() function after the PDF document loaded to support XFA +/// fields defined in the fpdfformfill.h file. +FPDF_DOCUMENT FPDF_LoadMemDocument64(ffi.Pointer data_buf, +int size, +FPDF_BYTESTRING password, +) { + return _FPDF_LoadMemDocument64(data_buf, +size, +password, +); +} + +late final _FPDF_LoadMemDocument64Ptr = _lookup< + ffi.NativeFunction , ffi.Size , FPDF_BYTESTRING )>>('FPDF_LoadMemDocument64'); +late final _FPDF_LoadMemDocument64 = _FPDF_LoadMemDocument64Ptr.asFunction , int , FPDF_BYTESTRING )>(); + +/// Function: FPDF_LoadCustomDocument +/// Load PDF document from a custom access descriptor. +/// Parameters: +/// pFileAccess - A structure for accessing the file. +/// password - Optional password for decrypting the PDF file. +/// Return value: +/// A handle to the loaded document, or NULL on failure. +/// Comments: +/// The application must keep the file resources |pFileAccess| points to +/// valid until the returned FPDF_DOCUMENT is closed. |pFileAccess| +/// itself does not need to outlive the FPDF_DOCUMENT. +/// +/// The loaded document can be closed with FPDF_CloseDocument(). +/// +/// See the comments for FPDF_LoadDocument() regarding the encoding for +/// |password|. +/// Notes: +/// If PDFium is built with the XFA module, the application should call +/// FPDF_LoadXFA() function after the PDF document loaded to support XFA +/// fields defined in the fpdfformfill.h file. +FPDF_DOCUMENT FPDF_LoadCustomDocument(ffi.Pointer pFileAccess, +FPDF_BYTESTRING password, +) { + return _FPDF_LoadCustomDocument(pFileAccess, +password, +); +} + +late final _FPDF_LoadCustomDocumentPtr = _lookup< + ffi.NativeFunction , FPDF_BYTESTRING )>>('FPDF_LoadCustomDocument'); +late final _FPDF_LoadCustomDocument = _FPDF_LoadCustomDocumentPtr.asFunction , FPDF_BYTESTRING )>(); + +/// Function: FPDF_GetFileVersion +/// Get the file version of the given PDF document. +/// Parameters: +/// doc - Handle to a document. +/// fileVersion - The PDF file version. File version: 14 for 1.4, 15 +/// for 1.5, ... +/// Return value: +/// True if succeeds, false otherwise. +/// Comments: +/// If the document was created by FPDF_CreateNewDocument, +/// then this function will always fail. +int FPDF_GetFileVersion(FPDF_DOCUMENT doc, +ffi.Pointer fileVersion, +) { + return _FPDF_GetFileVersion(doc, +fileVersion, +); +} + +late final _FPDF_GetFileVersionPtr = _lookup< + ffi.NativeFunction )>>('FPDF_GetFileVersion'); +late final _FPDF_GetFileVersion = _FPDF_GetFileVersionPtr.asFunction )>(); + +/// Function: FPDF_GetLastError +/// Get last error code when a function fails. +/// Parameters: +/// None. +/// Return value: +/// A 32-bit integer indicating error code as defined above. +/// Comments: +/// If the previous SDK call succeeded, the return value of this +/// function is not defined. This function only works in conjunction +/// with APIs that mention FPDF_GetLastError() in their documentation. +int FPDF_GetLastError() { + return _FPDF_GetLastError(); +} + +late final _FPDF_GetLastErrorPtr = _lookup< + ffi.NativeFunction>('FPDF_GetLastError'); +late final _FPDF_GetLastError = _FPDF_GetLastErrorPtr.asFunction(); + +/// Experimental API. +/// Function: FPDF_DocumentHasValidCrossReferenceTable +/// Whether the document's cross reference table is valid or not. +/// Parameters: +/// document - Handle to a document. Returned by FPDF_LoadDocument. +/// Return value: +/// True if the PDF parser did not encounter problems parsing the cross +/// reference table. False if the parser could not parse the cross +/// reference table and the table had to be rebuild from other data +/// within the document. +/// Comments: +/// The return value can change over time as the PDF parser evolves. +int FPDF_DocumentHasValidCrossReferenceTable(FPDF_DOCUMENT document, +) { + return _FPDF_DocumentHasValidCrossReferenceTable(document, +); +} + +late final _FPDF_DocumentHasValidCrossReferenceTablePtr = _lookup< + ffi.NativeFunction>('FPDF_DocumentHasValidCrossReferenceTable'); +late final _FPDF_DocumentHasValidCrossReferenceTable = _FPDF_DocumentHasValidCrossReferenceTablePtr.asFunction(); + +/// Experimental API. +/// Function: FPDF_GetTrailerEnds +/// Get the byte offsets of trailer ends. +/// Parameters: +/// document - Handle to document. Returned by FPDF_LoadDocument(). +/// buffer - The address of a buffer that receives the +/// byte offsets. +/// length - The size, in ints, of |buffer|. +/// Return value: +/// Returns the number of ints in the buffer on success, 0 on error. +/// +/// |buffer| is an array of integers that describes the exact byte offsets of the +/// trailer ends in the document. If |length| is less than the returned length, +/// or |document| or |buffer| is NULL, |buffer| will not be modified. +int FPDF_GetTrailerEnds(FPDF_DOCUMENT document, +ffi.Pointer buffer, +int length, +) { + return _FPDF_GetTrailerEnds(document, +buffer, +length, +); +} + +late final _FPDF_GetTrailerEndsPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDF_GetTrailerEnds'); +late final _FPDF_GetTrailerEnds = _FPDF_GetTrailerEndsPtr.asFunction , int )>(); + +/// Function: FPDF_GetDocPermissions +/// Get file permission flags of the document. +/// Parameters: +/// document - Handle to a document. Returned by FPDF_LoadDocument. +/// Return value: +/// A 32-bit integer indicating permission flags. Please refer to the +/// PDF Reference for detailed descriptions. If the document is not +/// protected or was unlocked by the owner, 0xffffffff will be returned. +int FPDF_GetDocPermissions(FPDF_DOCUMENT document, +) { + return _FPDF_GetDocPermissions(document, +); +} + +late final _FPDF_GetDocPermissionsPtr = _lookup< + ffi.NativeFunction>('FPDF_GetDocPermissions'); +late final _FPDF_GetDocPermissions = _FPDF_GetDocPermissionsPtr.asFunction(); + +/// Function: FPDF_GetDocUserPermissions +/// Get user file permission flags of the document. +/// Parameters: +/// document - Handle to a document. Returned by FPDF_LoadDocument. +/// Return value: +/// A 32-bit integer indicating permission flags. Please refer to the +/// PDF Reference for detailed descriptions. If the document is not +/// protected, 0xffffffff will be returned. Always returns user +/// permissions, even if the document was unlocked by the owner. +int FPDF_GetDocUserPermissions(FPDF_DOCUMENT document, +) { + return _FPDF_GetDocUserPermissions(document, +); +} + +late final _FPDF_GetDocUserPermissionsPtr = _lookup< + ffi.NativeFunction>('FPDF_GetDocUserPermissions'); +late final _FPDF_GetDocUserPermissions = _FPDF_GetDocUserPermissionsPtr.asFunction(); + +/// Function: FPDF_GetSecurityHandlerRevision +/// Get the revision for the security handler. +/// Parameters: +/// document - Handle to a document. Returned by FPDF_LoadDocument. +/// Return value: +/// The security handler revision number. Please refer to the PDF +/// Reference for a detailed description. If the document is not +/// protected, -1 will be returned. +int FPDF_GetSecurityHandlerRevision(FPDF_DOCUMENT document, +) { + return _FPDF_GetSecurityHandlerRevision(document, +); +} + +late final _FPDF_GetSecurityHandlerRevisionPtr = _lookup< + ffi.NativeFunction>('FPDF_GetSecurityHandlerRevision'); +late final _FPDF_GetSecurityHandlerRevision = _FPDF_GetSecurityHandlerRevisionPtr.asFunction(); + +/// Function: FPDF_GetPageCount +/// Get total number of pages in the document. +/// Parameters: +/// document - Handle to document. Returned by FPDF_LoadDocument. +/// Return value: +/// Total number of pages in the document. +int FPDF_GetPageCount(FPDF_DOCUMENT document, +) { + return _FPDF_GetPageCount(document, +); +} + +late final _FPDF_GetPageCountPtr = _lookup< + ffi.NativeFunction>('FPDF_GetPageCount'); +late final _FPDF_GetPageCount = _FPDF_GetPageCountPtr.asFunction(); + +/// Function: FPDF_LoadPage +/// Load a page inside the document. +/// Parameters: +/// document - Handle to document. Returned by FPDF_LoadDocument +/// page_index - Index number of the page. 0 for the first page. +/// Return value: +/// A handle to the loaded page, or NULL if page load fails. +/// Comments: +/// The loaded page can be rendered to devices using FPDF_RenderPage. +/// The loaded page can be closed using FPDF_ClosePage. +FPDF_PAGE FPDF_LoadPage(FPDF_DOCUMENT document, +int page_index, +) { + return _FPDF_LoadPage(document, +page_index, +); +} + +late final _FPDF_LoadPagePtr = _lookup< + ffi.NativeFunction>('FPDF_LoadPage'); +late final _FPDF_LoadPage = _FPDF_LoadPagePtr.asFunction(); + +/// Experimental API +/// Function: FPDF_GetPageWidthF +/// Get page width. +/// Parameters: +/// page - Handle to the page. Returned by FPDF_LoadPage(). +/// Return value: +/// Page width (excluding non-displayable area) measured in points. +/// One point is 1/72 inch (around 0.3528 mm). +/// Comments: +/// Changing the rotation of |page| affects the return value. +double FPDF_GetPageWidthF(FPDF_PAGE page, +) { + return _FPDF_GetPageWidthF(page, +); +} + +late final _FPDF_GetPageWidthFPtr = _lookup< + ffi.NativeFunction>('FPDF_GetPageWidthF'); +late final _FPDF_GetPageWidthF = _FPDF_GetPageWidthFPtr.asFunction(); + +/// Function: FPDF_GetPageWidth +/// Get page width. +/// Parameters: +/// page - Handle to the page. Returned by FPDF_LoadPage. +/// Return value: +/// Page width (excluding non-displayable area) measured in points. +/// One point is 1/72 inch (around 0.3528 mm). +/// Note: +/// Prefer FPDF_GetPageWidthF() above. This will be deprecated in the +/// future. +/// Comments: +/// Changing the rotation of |page| affects the return value. +double FPDF_GetPageWidth(FPDF_PAGE page, +) { + return _FPDF_GetPageWidth(page, +); +} + +late final _FPDF_GetPageWidthPtr = _lookup< + ffi.NativeFunction>('FPDF_GetPageWidth'); +late final _FPDF_GetPageWidth = _FPDF_GetPageWidthPtr.asFunction(); + +/// Experimental API +/// Function: FPDF_GetPageHeightF +/// Get page height. +/// Parameters: +/// page - Handle to the page. Returned by FPDF_LoadPage(). +/// Return value: +/// Page height (excluding non-displayable area) measured in points. +/// One point is 1/72 inch (around 0.3528 mm) +/// Comments: +/// Changing the rotation of |page| affects the return value. +double FPDF_GetPageHeightF(FPDF_PAGE page, +) { + return _FPDF_GetPageHeightF(page, +); +} + +late final _FPDF_GetPageHeightFPtr = _lookup< + ffi.NativeFunction>('FPDF_GetPageHeightF'); +late final _FPDF_GetPageHeightF = _FPDF_GetPageHeightFPtr.asFunction(); + +/// Function: FPDF_GetPageHeight +/// Get page height. +/// Parameters: +/// page - Handle to the page. Returned by FPDF_LoadPage. +/// Return value: +/// Page height (excluding non-displayable area) measured in points. +/// One point is 1/72 inch (around 0.3528 mm) +/// Note: +/// Prefer FPDF_GetPageHeightF() above. This will be deprecated in the +/// future. +/// Comments: +/// Changing the rotation of |page| affects the return value. +double FPDF_GetPageHeight(FPDF_PAGE page, +) { + return _FPDF_GetPageHeight(page, +); +} + +late final _FPDF_GetPageHeightPtr = _lookup< + ffi.NativeFunction>('FPDF_GetPageHeight'); +late final _FPDF_GetPageHeight = _FPDF_GetPageHeightPtr.asFunction(); + +/// Experimental API. +/// Function: FPDF_GetPageBoundingBox +/// Get the bounding box of the page. This is the intersection between +/// its media box and its crop box. +/// Parameters: +/// page - Handle to the page. Returned by FPDF_LoadPage. +/// rect - Pointer to a rect to receive the page bounding box. +/// On an error, |rect| won't be filled. +/// Return value: +/// True for success. +int FPDF_GetPageBoundingBox(FPDF_PAGE page, +ffi.Pointer rect, +) { + return _FPDF_GetPageBoundingBox(page, +rect, +); +} + +late final _FPDF_GetPageBoundingBoxPtr = _lookup< + ffi.NativeFunction )>>('FPDF_GetPageBoundingBox'); +late final _FPDF_GetPageBoundingBox = _FPDF_GetPageBoundingBoxPtr.asFunction )>(); + +/// Experimental API. +/// Function: FPDF_GetPageSizeByIndexF +/// Get the size of the page at the given index. +/// Parameters: +/// document - Handle to document. Returned by FPDF_LoadDocument(). +/// page_index - Page index, zero for the first page. +/// size - Pointer to a FS_SIZEF to receive the page size. +/// (in points). +/// Return value: +/// Non-zero for success. 0 for error (document or page not found). +int FPDF_GetPageSizeByIndexF(FPDF_DOCUMENT document, +int page_index, +ffi.Pointer size, +) { + return _FPDF_GetPageSizeByIndexF(document, +page_index, +size, +); +} + +late final _FPDF_GetPageSizeByIndexFPtr = _lookup< + ffi.NativeFunction )>>('FPDF_GetPageSizeByIndexF'); +late final _FPDF_GetPageSizeByIndexF = _FPDF_GetPageSizeByIndexFPtr.asFunction )>(); + +/// Function: FPDF_GetPageSizeByIndex +/// Get the size of the page at the given index. +/// Parameters: +/// document - Handle to document. Returned by FPDF_LoadDocument. +/// page_index - Page index, zero for the first page. +/// width - Pointer to a double to receive the page width +/// (in points). +/// height - Pointer to a double to receive the page height +/// (in points). +/// Return value: +/// Non-zero for success. 0 for error (document or page not found). +/// Note: +/// Prefer FPDF_GetPageSizeByIndexF() above. This will be deprecated in +/// the future. +int FPDF_GetPageSizeByIndex(FPDF_DOCUMENT document, +int page_index, +ffi.Pointer width, +ffi.Pointer height, +) { + return _FPDF_GetPageSizeByIndex(document, +page_index, +width, +height, +); +} + +late final _FPDF_GetPageSizeByIndexPtr = _lookup< + ffi.NativeFunction , ffi.Pointer )>>('FPDF_GetPageSizeByIndex'); +late final _FPDF_GetPageSizeByIndex = _FPDF_GetPageSizeByIndexPtr.asFunction , ffi.Pointer )>(); + +/// Function: FPDF_RenderPageBitmap +/// Render contents of a page to a device independent bitmap. +/// Parameters: +/// bitmap - Handle to the device independent bitmap (as the +/// output buffer). The bitmap handle can be created +/// by FPDFBitmap_Create or retrieved from an image +/// object by FPDFImageObj_GetBitmap. +/// page - Handle to the page. Returned by FPDF_LoadPage +/// start_x - Left pixel position of the display area in +/// bitmap coordinates. +/// start_y - Top pixel position of the display area in bitmap +/// coordinates. +/// size_x - Horizontal size (in pixels) for displaying the page. +/// size_y - Vertical size (in pixels) for displaying the page. +/// rotate - Page orientation: +/// 0 (normal) +/// 1 (rotated 90 degrees clockwise) +/// 2 (rotated 180 degrees) +/// 3 (rotated 90 degrees counter-clockwise) +/// flags - 0 for normal display, or combination of the Page +/// Rendering flags defined above. With the FPDF_ANNOT +/// flag, it renders all annotations that do not require +/// user-interaction, which are all annotations except +/// widget and popup annotations. +/// Return value: +/// None. +void FPDF_RenderPageBitmap(FPDF_BITMAP bitmap, +FPDF_PAGE page, +int start_x, +int start_y, +int size_x, +int size_y, +int rotate, +int flags, +) { + return _FPDF_RenderPageBitmap(bitmap, +page, +start_x, +start_y, +size_x, +size_y, +rotate, +flags, +); +} + +late final _FPDF_RenderPageBitmapPtr = _lookup< + ffi.NativeFunction>('FPDF_RenderPageBitmap'); +late final _FPDF_RenderPageBitmap = _FPDF_RenderPageBitmapPtr.asFunction(); + +/// Function: FPDF_RenderPageBitmapWithMatrix +/// Render contents of a page to a device independent bitmap. +/// Parameters: +/// bitmap - Handle to the device independent bitmap (as the +/// output buffer). The bitmap handle can be created +/// by FPDFBitmap_Create or retrieved by +/// FPDFImageObj_GetBitmap. +/// page - Handle to the page. Returned by FPDF_LoadPage. +/// matrix - The transform matrix, which must be invertible. +/// See PDF Reference 1.7, 4.2.2 Common Transformations. +/// clipping - The rect to clip to in device coords. +/// flags - 0 for normal display, or combination of the Page +/// Rendering flags defined above. With the FPDF_ANNOT +/// flag, it renders all annotations that do not require +/// user-interaction, which are all annotations except +/// widget and popup annotations. +/// Return value: +/// None. Note that behavior is undefined if det of |matrix| is 0. +void FPDF_RenderPageBitmapWithMatrix(FPDF_BITMAP bitmap, +FPDF_PAGE page, +ffi.Pointer matrix, +ffi.Pointer clipping, +int flags, +) { + return _FPDF_RenderPageBitmapWithMatrix(bitmap, +page, +matrix, +clipping, +flags, +); +} + +late final _FPDF_RenderPageBitmapWithMatrixPtr = _lookup< + ffi.NativeFunction , ffi.Pointer , ffi.Int )>>('FPDF_RenderPageBitmapWithMatrix'); +late final _FPDF_RenderPageBitmapWithMatrix = _FPDF_RenderPageBitmapWithMatrixPtr.asFunction , ffi.Pointer , int )>(); + +/// Function: FPDF_ClosePage +/// Close a loaded PDF page. +/// Parameters: +/// page - Handle to the loaded page. +/// Return value: +/// None. +void FPDF_ClosePage(FPDF_PAGE page, +) { + return _FPDF_ClosePage(page, +); +} + +late final _FPDF_ClosePagePtr = _lookup< + ffi.NativeFunction>('FPDF_ClosePage'); +late final _FPDF_ClosePage = _FPDF_ClosePagePtr.asFunction(); + +/// Function: FPDF_CloseDocument +/// Close a loaded PDF document. +/// Parameters: +/// document - Handle to the loaded document. +/// Return value: +/// None. +void FPDF_CloseDocument(FPDF_DOCUMENT document, +) { + return _FPDF_CloseDocument(document, +); +} + +late final _FPDF_CloseDocumentPtr = _lookup< + ffi.NativeFunction>('FPDF_CloseDocument'); +late final _FPDF_CloseDocument = _FPDF_CloseDocumentPtr.asFunction(); + +/// Function: FPDF_DeviceToPage +/// Convert the screen coordinates of a point to page coordinates. +/// Parameters: +/// page - Handle to the page. Returned by FPDF_LoadPage. +/// start_x - Left pixel position of the display area in +/// device coordinates. +/// start_y - Top pixel position of the display area in device +/// coordinates. +/// size_x - Horizontal size (in pixels) for displaying the page. +/// size_y - Vertical size (in pixels) for displaying the page. +/// rotate - Page orientation: +/// 0 (normal) +/// 1 (rotated 90 degrees clockwise) +/// 2 (rotated 180 degrees) +/// 3 (rotated 90 degrees counter-clockwise) +/// device_x - X value in device coordinates to be converted. +/// device_y - Y value in device coordinates to be converted. +/// page_x - A pointer to a double receiving the converted X +/// value in page coordinates. +/// page_y - A pointer to a double receiving the converted Y +/// value in page coordinates. +/// Return value: +/// Returns true if the conversion succeeds, and |page_x| and |page_y| +/// successfully receives the converted coordinates. +/// Comments: +/// The page coordinate system has its origin at the left-bottom corner +/// of the page, with the X-axis on the bottom going to the right, and +/// the Y-axis on the left side going up. +/// +/// NOTE: this coordinate system can be altered when you zoom, scroll, +/// or rotate a page, however, a point on the page should always have +/// the same coordinate values in the page coordinate system. +/// +/// The device coordinate system is device dependent. For screen device, +/// its origin is at the left-top corner of the window. However this +/// origin can be altered by the Windows coordinate transformation +/// utilities. +/// +/// You must make sure the start_x, start_y, size_x, size_y +/// and rotate parameters have exactly same values as you used in +/// the FPDF_RenderPage() function call. +int FPDF_DeviceToPage(FPDF_PAGE page, +int start_x, +int start_y, +int size_x, +int size_y, +int rotate, +int device_x, +int device_y, +ffi.Pointer page_x, +ffi.Pointer page_y, +) { + return _FPDF_DeviceToPage(page, +start_x, +start_y, +size_x, +size_y, +rotate, +device_x, +device_y, +page_x, +page_y, +); +} + +late final _FPDF_DeviceToPagePtr = _lookup< + ffi.NativeFunction , ffi.Pointer )>>('FPDF_DeviceToPage'); +late final _FPDF_DeviceToPage = _FPDF_DeviceToPagePtr.asFunction , ffi.Pointer )>(); + +/// Function: FPDF_PageToDevice +/// Convert the page coordinates of a point to screen coordinates. +/// Parameters: +/// page - Handle to the page. Returned by FPDF_LoadPage. +/// start_x - Left pixel position of the display area in +/// device coordinates. +/// start_y - Top pixel position of the display area in device +/// coordinates. +/// size_x - Horizontal size (in pixels) for displaying the page. +/// size_y - Vertical size (in pixels) for displaying the page. +/// rotate - Page orientation: +/// 0 (normal) +/// 1 (rotated 90 degrees clockwise) +/// 2 (rotated 180 degrees) +/// 3 (rotated 90 degrees counter-clockwise) +/// page_x - X value in page coordinates. +/// page_y - Y value in page coordinate. +/// device_x - A pointer to an integer receiving the result X +/// value in device coordinates. +/// device_y - A pointer to an integer receiving the result Y +/// value in device coordinates. +/// Return value: +/// Returns true if the conversion succeeds, and |device_x| and +/// |device_y| successfully receives the converted coordinates. +/// Comments: +/// See comments for FPDF_DeviceToPage(). +int FPDF_PageToDevice(FPDF_PAGE page, +int start_x, +int start_y, +int size_x, +int size_y, +int rotate, +double page_x, +double page_y, +ffi.Pointer device_x, +ffi.Pointer device_y, +) { + return _FPDF_PageToDevice(page, +start_x, +start_y, +size_x, +size_y, +rotate, +page_x, +page_y, +device_x, +device_y, +); +} + +late final _FPDF_PageToDevicePtr = _lookup< + ffi.NativeFunction , ffi.Pointer )>>('FPDF_PageToDevice'); +late final _FPDF_PageToDevice = _FPDF_PageToDevicePtr.asFunction , ffi.Pointer )>(); + +/// Function: FPDFBitmap_Create +/// Create a device independent bitmap (FXDIB). +/// Parameters: +/// width - The number of pixels in width for the bitmap. +/// Must be greater than 0. +/// height - The number of pixels in height for the bitmap. +/// Must be greater than 0. +/// alpha - A flag indicating whether the alpha channel is used. +/// Non-zero for using alpha, zero for not using. +/// Return value: +/// The created bitmap handle, or NULL if a parameter error or out of +/// memory. +/// Comments: +/// The bitmap always uses 4 bytes per pixel. The first byte is always +/// double word aligned. +/// +/// The byte order is BGRx (the last byte unused if no alpha channel) or +/// BGRA. +/// +/// The pixels in a horizontal line are stored side by side, with the +/// left most pixel stored first (with lower memory address). +/// Each line uses width * 4 bytes. +/// +/// Lines are stored one after another, with the top most line stored +/// first. There is no gap between adjacent lines. +/// +/// This function allocates enough memory for holding all pixels in the +/// bitmap, but it doesn't initialize the buffer. Applications can use +/// FPDFBitmap_FillRect() to fill the bitmap using any color. If the OS +/// allows it, this function can allocate up to 4 GB of memory. +FPDF_BITMAP FPDFBitmap_Create(int width, +int height, +int alpha, +) { + return _FPDFBitmap_Create(width, +height, +alpha, +); +} + +late final _FPDFBitmap_CreatePtr = _lookup< + ffi.NativeFunction>('FPDFBitmap_Create'); +late final _FPDFBitmap_Create = _FPDFBitmap_CreatePtr.asFunction(); + +/// Function: FPDFBitmap_CreateEx +/// Create a device independent bitmap (FXDIB) +/// Parameters: +/// width - The number of pixels in width for the bitmap. +/// Must be greater than 0. +/// height - The number of pixels in height for the bitmap. +/// Must be greater than 0. +/// format - A number indicating for bitmap format, as defined +/// above. +/// first_scan - A pointer to the first byte of the first line if +/// using an external buffer. If this parameter is NULL, +/// then a new buffer will be created. +/// stride - Number of bytes for each scan line. The value must +/// be 0 or greater. When the value is 0, +/// FPDFBitmap_CreateEx() will automatically calculate +/// the appropriate value using |width| and |format|. +/// When using an external buffer, it is recommended for +/// the caller to pass in the value. +/// When not using an external buffer, it is recommended +/// for the caller to pass in 0. +/// Return value: +/// The bitmap handle, or NULL if parameter error or out of memory. +/// Comments: +/// Similar to FPDFBitmap_Create function, but allows for more formats +/// and an external buffer is supported. The bitmap created by this +/// function can be used in any place that a FPDF_BITMAP handle is +/// required. +/// +/// If an external buffer is used, then the caller should destroy the +/// buffer. FPDFBitmap_Destroy() will not destroy the buffer. +/// +/// It is recommended to use FPDFBitmap_GetStride() to get the stride +/// value. +FPDF_BITMAP FPDFBitmap_CreateEx(int width, +int height, +int format, +ffi.Pointer first_scan, +int stride, +) { + return _FPDFBitmap_CreateEx(width, +height, +format, +first_scan, +stride, +); +} + +late final _FPDFBitmap_CreateExPtr = _lookup< + ffi.NativeFunction , ffi.Int )>>('FPDFBitmap_CreateEx'); +late final _FPDFBitmap_CreateEx = _FPDFBitmap_CreateExPtr.asFunction , int )>(); + +/// Function: FPDFBitmap_GetFormat +/// Get the format of the bitmap. +/// Parameters: +/// bitmap - Handle to the bitmap. Returned by FPDFBitmap_Create +/// or FPDFImageObj_GetBitmap. +/// Return value: +/// The format of the bitmap. +/// Comments: +/// Only formats supported by FPDFBitmap_CreateEx are supported by this +/// function; see the list of such formats above. +int FPDFBitmap_GetFormat(FPDF_BITMAP bitmap, +) { + return _FPDFBitmap_GetFormat(bitmap, +); +} + +late final _FPDFBitmap_GetFormatPtr = _lookup< + ffi.NativeFunction>('FPDFBitmap_GetFormat'); +late final _FPDFBitmap_GetFormat = _FPDFBitmap_GetFormatPtr.asFunction(); + +/// Function: FPDFBitmap_FillRect +/// Fill a rectangle in a bitmap. +/// Parameters: +/// bitmap - The handle to the bitmap. Returned by +/// FPDFBitmap_Create. +/// left - The left position. Starting from 0 at the +/// left-most pixel. +/// top - The top position. Starting from 0 at the +/// top-most line. +/// width - Width in pixels to be filled. +/// height - Height in pixels to be filled. +/// color - A 32-bit value specifing the color, in 8888 ARGB +/// format. +/// Return value: +/// Returns whether the operation succeeded or not. +/// Comments: +/// This function sets the color and (optionally) alpha value in the +/// specified region of the bitmap. +/// +/// NOTE: If the alpha channel is used, this function does NOT +/// composite the background with the source color, instead the +/// background will be replaced by the source color and the alpha. +/// +/// If the alpha channel is not used, the alpha parameter is ignored. +int FPDFBitmap_FillRect(FPDF_BITMAP bitmap, +int left, +int top, +int width, +int height, +int color, +) { + return _FPDFBitmap_FillRect(bitmap, +left, +top, +width, +height, +color, +); +} + +late final _FPDFBitmap_FillRectPtr = _lookup< + ffi.NativeFunction>('FPDFBitmap_FillRect'); +late final _FPDFBitmap_FillRect = _FPDFBitmap_FillRectPtr.asFunction(); + +/// Function: FPDFBitmap_GetBuffer +/// Get data buffer of a bitmap. +/// Parameters: +/// bitmap - Handle to the bitmap. Returned by FPDFBitmap_Create +/// or FPDFImageObj_GetBitmap. +/// Return value: +/// The pointer to the first byte of the bitmap buffer. +/// Comments: +/// The stride may be more than width * number of bytes per pixel +/// +/// Applications can use this function to get the bitmap buffer pointer, +/// then manipulate any color and/or alpha values for any pixels in the +/// bitmap. +/// +/// Use FPDFBitmap_GetFormat() to find out the format of the data. +ffi.Pointer FPDFBitmap_GetBuffer(FPDF_BITMAP bitmap, +) { + return _FPDFBitmap_GetBuffer(bitmap, +); +} + +late final _FPDFBitmap_GetBufferPtr = _lookup< + ffi.NativeFunction Function(FPDF_BITMAP )>>('FPDFBitmap_GetBuffer'); +late final _FPDFBitmap_GetBuffer = _FPDFBitmap_GetBufferPtr.asFunction Function(FPDF_BITMAP )>(); + +/// Function: FPDFBitmap_GetWidth +/// Get width of a bitmap. +/// Parameters: +/// bitmap - Handle to the bitmap. Returned by FPDFBitmap_Create +/// or FPDFImageObj_GetBitmap. +/// Return value: +/// The width of the bitmap in pixels. +int FPDFBitmap_GetWidth(FPDF_BITMAP bitmap, +) { + return _FPDFBitmap_GetWidth(bitmap, +); +} + +late final _FPDFBitmap_GetWidthPtr = _lookup< + ffi.NativeFunction>('FPDFBitmap_GetWidth'); +late final _FPDFBitmap_GetWidth = _FPDFBitmap_GetWidthPtr.asFunction(); + +/// Function: FPDFBitmap_GetHeight +/// Get height of a bitmap. +/// Parameters: +/// bitmap - Handle to the bitmap. Returned by FPDFBitmap_Create +/// or FPDFImageObj_GetBitmap. +/// Return value: +/// The height of the bitmap in pixels. +int FPDFBitmap_GetHeight(FPDF_BITMAP bitmap, +) { + return _FPDFBitmap_GetHeight(bitmap, +); +} + +late final _FPDFBitmap_GetHeightPtr = _lookup< + ffi.NativeFunction>('FPDFBitmap_GetHeight'); +late final _FPDFBitmap_GetHeight = _FPDFBitmap_GetHeightPtr.asFunction(); + +/// Function: FPDFBitmap_GetStride +/// Get number of bytes for each line in the bitmap buffer. +/// Parameters: +/// bitmap - Handle to the bitmap. Returned by FPDFBitmap_Create +/// or FPDFImageObj_GetBitmap. +/// Return value: +/// The number of bytes for each line in the bitmap buffer. +/// Comments: +/// The stride may be more than width * number of bytes per pixel. +int FPDFBitmap_GetStride(FPDF_BITMAP bitmap, +) { + return _FPDFBitmap_GetStride(bitmap, +); +} + +late final _FPDFBitmap_GetStridePtr = _lookup< + ffi.NativeFunction>('FPDFBitmap_GetStride'); +late final _FPDFBitmap_GetStride = _FPDFBitmap_GetStridePtr.asFunction(); + +/// Function: FPDFBitmap_Destroy +/// Destroy a bitmap and release all related buffers. +/// Parameters: +/// bitmap - Handle to the bitmap. Returned by FPDFBitmap_Create +/// or FPDFImageObj_GetBitmap. +/// Return value: +/// None. +/// Comments: +/// This function will not destroy any external buffers provided when +/// the bitmap was created. +void FPDFBitmap_Destroy(FPDF_BITMAP bitmap, +) { + return _FPDFBitmap_Destroy(bitmap, +); +} + +late final _FPDFBitmap_DestroyPtr = _lookup< + ffi.NativeFunction>('FPDFBitmap_Destroy'); +late final _FPDFBitmap_Destroy = _FPDFBitmap_DestroyPtr.asFunction(); + +/// Function: FPDF_VIEWERREF_GetPrintScaling +/// Whether the PDF document prefers to be scaled or not. +/// Parameters: +/// document - Handle to the loaded document. +/// Return value: +/// None. +int FPDF_VIEWERREF_GetPrintScaling(FPDF_DOCUMENT document, +) { + return _FPDF_VIEWERREF_GetPrintScaling(document, +); +} + +late final _FPDF_VIEWERREF_GetPrintScalingPtr = _lookup< + ffi.NativeFunction>('FPDF_VIEWERREF_GetPrintScaling'); +late final _FPDF_VIEWERREF_GetPrintScaling = _FPDF_VIEWERREF_GetPrintScalingPtr.asFunction(); + +/// Function: FPDF_VIEWERREF_GetNumCopies +/// Returns the number of copies to be printed. +/// Parameters: +/// document - Handle to the loaded document. +/// Return value: +/// The number of copies to be printed. +int FPDF_VIEWERREF_GetNumCopies(FPDF_DOCUMENT document, +) { + return _FPDF_VIEWERREF_GetNumCopies(document, +); +} + +late final _FPDF_VIEWERREF_GetNumCopiesPtr = _lookup< + ffi.NativeFunction>('FPDF_VIEWERREF_GetNumCopies'); +late final _FPDF_VIEWERREF_GetNumCopies = _FPDF_VIEWERREF_GetNumCopiesPtr.asFunction(); + +/// Function: FPDF_VIEWERREF_GetPrintPageRange +/// Page numbers to initialize print dialog box when file is printed. +/// Parameters: +/// document - Handle to the loaded document. +/// Return value: +/// The print page range to be used for printing. +FPDF_PAGERANGE FPDF_VIEWERREF_GetPrintPageRange(FPDF_DOCUMENT document, +) { + return _FPDF_VIEWERREF_GetPrintPageRange(document, +); +} + +late final _FPDF_VIEWERREF_GetPrintPageRangePtr = _lookup< + ffi.NativeFunction>('FPDF_VIEWERREF_GetPrintPageRange'); +late final _FPDF_VIEWERREF_GetPrintPageRange = _FPDF_VIEWERREF_GetPrintPageRangePtr.asFunction(); + +/// Experimental API. +/// Function: FPDF_VIEWERREF_GetPrintPageRangeCount +/// Returns the number of elements in a FPDF_PAGERANGE. +/// Parameters: +/// pagerange - Handle to the page range. +/// Return value: +/// The number of elements in the page range. Returns 0 on error. +int FPDF_VIEWERREF_GetPrintPageRangeCount(FPDF_PAGERANGE pagerange, +) { + return _FPDF_VIEWERREF_GetPrintPageRangeCount(pagerange, +); +} + +late final _FPDF_VIEWERREF_GetPrintPageRangeCountPtr = _lookup< + ffi.NativeFunction>('FPDF_VIEWERREF_GetPrintPageRangeCount'); +late final _FPDF_VIEWERREF_GetPrintPageRangeCount = _FPDF_VIEWERREF_GetPrintPageRangeCountPtr.asFunction(); + +/// Experimental API. +/// Function: FPDF_VIEWERREF_GetPrintPageRangeElement +/// Returns an element from a FPDF_PAGERANGE. +/// Parameters: +/// pagerange - Handle to the page range. +/// index - Index of the element. +/// Return value: +/// The value of the element in the page range at a given index. +/// Returns -1 on error. +int FPDF_VIEWERREF_GetPrintPageRangeElement(FPDF_PAGERANGE pagerange, +int index, +) { + return _FPDF_VIEWERREF_GetPrintPageRangeElement(pagerange, +index, +); +} + +late final _FPDF_VIEWERREF_GetPrintPageRangeElementPtr = _lookup< + ffi.NativeFunction>('FPDF_VIEWERREF_GetPrintPageRangeElement'); +late final _FPDF_VIEWERREF_GetPrintPageRangeElement = _FPDF_VIEWERREF_GetPrintPageRangeElementPtr.asFunction(); + +/// Function: FPDF_VIEWERREF_GetDuplex +/// Returns the paper handling option to be used when printing from +/// the print dialog. +/// Parameters: +/// document - Handle to the loaded document. +/// Return value: +/// The paper handling option to be used when printing. +_FPDF_DUPLEXTYPE_ FPDF_VIEWERREF_GetDuplex(FPDF_DOCUMENT document, +) { + return _FPDF_DUPLEXTYPE_.fromValue(_FPDF_VIEWERREF_GetDuplex(document, +)); +} + +late final _FPDF_VIEWERREF_GetDuplexPtr = _lookup< + ffi.NativeFunction>('FPDF_VIEWERREF_GetDuplex'); +late final _FPDF_VIEWERREF_GetDuplex = _FPDF_VIEWERREF_GetDuplexPtr.asFunction(); + +/// Function: FPDF_VIEWERREF_GetName +/// Gets the contents for a viewer ref, with a given key. The value must +/// be of type "name". +/// Parameters: +/// document - Handle to the loaded document. +/// key - Name of the key in the viewer pref dictionary, +/// encoded in UTF-8. +/// buffer - Caller-allocate buffer to receive the key, or NULL +/// - to query the required length. +/// length - Length of the buffer. +/// Return value: +/// The number of bytes in the contents, including the NULL terminator. +/// Thus if the return value is 0, then that indicates an error, such +/// as when |document| is invalid. If |length| is less than the required +/// length, or |buffer| is NULL, |buffer| will not be modified. +int FPDF_VIEWERREF_GetName(FPDF_DOCUMENT document, +FPDF_BYTESTRING key, +ffi.Pointer buffer, +int length, +) { + return _FPDF_VIEWERREF_GetName(document, +key, +buffer, +length, +); +} + +late final _FPDF_VIEWERREF_GetNamePtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDF_VIEWERREF_GetName'); +late final _FPDF_VIEWERREF_GetName = _FPDF_VIEWERREF_GetNamePtr.asFunction , int )>(); + +/// Function: FPDF_CountNamedDests +/// Get the count of named destinations in the PDF document. +/// Parameters: +/// document - Handle to a document +/// Return value: +/// The count of named destinations. +int FPDF_CountNamedDests(FPDF_DOCUMENT document, +) { + return _FPDF_CountNamedDests(document, +); +} + +late final _FPDF_CountNamedDestsPtr = _lookup< + ffi.NativeFunction>('FPDF_CountNamedDests'); +late final _FPDF_CountNamedDests = _FPDF_CountNamedDestsPtr.asFunction(); + +/// Function: FPDF_GetNamedDestByName +/// Get a the destination handle for the given name. +/// Parameters: +/// document - Handle to the loaded document. +/// name - The name of a destination. +/// Return value: +/// The handle to the destination. +FPDF_DEST FPDF_GetNamedDestByName(FPDF_DOCUMENT document, +FPDF_BYTESTRING name, +) { + return _FPDF_GetNamedDestByName(document, +name, +); +} + +late final _FPDF_GetNamedDestByNamePtr = _lookup< + ffi.NativeFunction>('FPDF_GetNamedDestByName'); +late final _FPDF_GetNamedDestByName = _FPDF_GetNamedDestByNamePtr.asFunction(); + +/// Function: FPDF_GetNamedDest +/// Get the named destination by index. +/// Parameters: +/// document - Handle to a document +/// index - The index of a named destination. +/// buffer - The buffer to store the destination name, +/// used as wchar_t*. +/// buflen [in/out] - Size of the buffer in bytes on input, +/// length of the result in bytes on output +/// or -1 if the buffer is too small. +/// Return value: +/// The destination handle for a given index, or NULL if there is no +/// named destination corresponding to |index|. +/// Comments: +/// Call this function twice to get the name of the named destination: +/// 1) First time pass in |buffer| as NULL and get buflen. +/// 2) Second time pass in allocated |buffer| and buflen to retrieve +/// |buffer|, which should be used as wchar_t*. +/// +/// If buflen is not sufficiently large, it will be set to -1 upon +/// return. +FPDF_DEST FPDF_GetNamedDest(FPDF_DOCUMENT document, +int index, +ffi.Pointer buffer, +ffi.Pointer buflen, +) { + return _FPDF_GetNamedDest(document, +index, +buffer, +buflen, +); +} + +late final _FPDF_GetNamedDestPtr = _lookup< + ffi.NativeFunction , ffi.Pointer )>>('FPDF_GetNamedDest'); +late final _FPDF_GetNamedDest = _FPDF_GetNamedDestPtr.asFunction , ffi.Pointer )>(); + +/// Experimental API. +/// Function: FPDF_GetXFAPacketCount +/// Get the number of valid packets in the XFA entry. +/// Parameters: +/// document - Handle to the document. +/// Return value: +/// The number of valid packets, or -1 on error. +int FPDF_GetXFAPacketCount(FPDF_DOCUMENT document, +) { + return _FPDF_GetXFAPacketCount(document, +); +} + +late final _FPDF_GetXFAPacketCountPtr = _lookup< + ffi.NativeFunction>('FPDF_GetXFAPacketCount'); +late final _FPDF_GetXFAPacketCount = _FPDF_GetXFAPacketCountPtr.asFunction(); + +/// Experimental API. +/// Function: FPDF_GetXFAPacketName +/// Get the name of a packet in the XFA array. +/// Parameters: +/// document - Handle to the document. +/// index - Index number of the packet. 0 for the first packet. +/// buffer - Buffer for holding the name of the XFA packet. +/// buflen - Length of |buffer| in bytes. +/// Return value: +/// The length of the packet name in bytes, or 0 on error. +/// +/// |document| must be valid and |index| must be in the range [0, N), where N is +/// the value returned by FPDF_GetXFAPacketCount(). +/// |buffer| is only modified if it is non-NULL and |buflen| is greater than or +/// equal to the length of the packet name. The packet name includes a +/// terminating NUL character. |buffer| is unmodified on error. +int FPDF_GetXFAPacketName(FPDF_DOCUMENT document, +int index, +ffi.Pointer buffer, +int buflen, +) { + return _FPDF_GetXFAPacketName(document, +index, +buffer, +buflen, +); +} + +late final _FPDF_GetXFAPacketNamePtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDF_GetXFAPacketName'); +late final _FPDF_GetXFAPacketName = _FPDF_GetXFAPacketNamePtr.asFunction , int )>(); + +/// Experimental API. +/// Function: FPDF_GetXFAPacketContent +/// Get the content of a packet in the XFA array. +/// Parameters: +/// document - Handle to the document. +/// index - Index number of the packet. 0 for the first packet. +/// buffer - Buffer for holding the content of the XFA packet. +/// buflen - Length of |buffer| in bytes. +/// out_buflen - Pointer to the variable that will receive the minimum +/// buffer size needed to contain the content of the XFA +/// packet. +/// Return value: +/// Whether the operation succeeded or not. +/// +/// |document| must be valid and |index| must be in the range [0, N), where N is +/// the value returned by FPDF_GetXFAPacketCount(). |out_buflen| must not be +/// NULL. When the aforementioned arguments are valid, the operation succeeds, +/// and |out_buflen| receives the content size. |buffer| is only modified if +/// |buffer| is non-null and long enough to contain the content. Callers must +/// check both the return value and the input |buflen| is no less than the +/// returned |out_buflen| before using the data in |buffer|. +int FPDF_GetXFAPacketContent(FPDF_DOCUMENT document, +int index, +ffi.Pointer buffer, +int buflen, +ffi.Pointer out_buflen, +) { + return _FPDF_GetXFAPacketContent(document, +index, +buffer, +buflen, +out_buflen, +); +} + +late final _FPDF_GetXFAPacketContentPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong , ffi.Pointer )>>('FPDF_GetXFAPacketContent'); +late final _FPDF_GetXFAPacketContent = _FPDF_GetXFAPacketContentPtr.asFunction , int , ffi.Pointer )>(); + +/// Experimental API. +/// Function: FPDF_GetSignatureCount +/// Get total number of signatures in the document. +/// Parameters: +/// document - Handle to document. Returned by FPDF_LoadDocument(). +/// Return value: +/// Total number of signatures in the document on success, -1 on error. +int FPDF_GetSignatureCount(FPDF_DOCUMENT document, +) { + return _FPDF_GetSignatureCount(document, +); +} + +late final _FPDF_GetSignatureCountPtr = _lookup< + ffi.NativeFunction>('FPDF_GetSignatureCount'); +late final _FPDF_GetSignatureCount = _FPDF_GetSignatureCountPtr.asFunction(); + +/// Experimental API. +/// Function: FPDF_GetSignatureObject +/// Get the Nth signature of the document. +/// Parameters: +/// document - Handle to document. Returned by FPDF_LoadDocument(). +/// index - Index into the array of signatures of the document. +/// Return value: +/// Returns the handle to the signature, or NULL on failure. The caller +/// does not take ownership of the returned FPDF_SIGNATURE. Instead, it +/// remains valid until FPDF_CloseDocument() is called for the document. +FPDF_SIGNATURE FPDF_GetSignatureObject(FPDF_DOCUMENT document, +int index, +) { + return _FPDF_GetSignatureObject(document, +index, +); +} + +late final _FPDF_GetSignatureObjectPtr = _lookup< + ffi.NativeFunction>('FPDF_GetSignatureObject'); +late final _FPDF_GetSignatureObject = _FPDF_GetSignatureObjectPtr.asFunction(); + +/// Experimental API. +/// Function: FPDFSignatureObj_GetContents +/// Get the contents of a signature object. +/// Parameters: +/// signature - Handle to the signature object. Returned by +/// FPDF_GetSignatureObject(). +/// buffer - The address of a buffer that receives the contents. +/// length - The size, in bytes, of |buffer|. +/// Return value: +/// Returns the number of bytes in the contents on success, 0 on error. +/// +/// For public-key signatures, |buffer| is either a DER-encoded PKCS#1 binary or +/// a DER-encoded PKCS#7 binary. If |length| is less than the returned length, or +/// |buffer| is NULL, |buffer| will not be modified. +int FPDFSignatureObj_GetContents(FPDF_SIGNATURE signature, +ffi.Pointer buffer, +int length, +) { + return _FPDFSignatureObj_GetContents(signature, +buffer, +length, +); +} + +late final _FPDFSignatureObj_GetContentsPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFSignatureObj_GetContents'); +late final _FPDFSignatureObj_GetContents = _FPDFSignatureObj_GetContentsPtr.asFunction , int )>(); + +/// Experimental API. +/// Function: FPDFSignatureObj_GetByteRange +/// Get the byte range of a signature object. +/// Parameters: +/// signature - Handle to the signature object. Returned by +/// FPDF_GetSignatureObject(). +/// buffer - The address of a buffer that receives the +/// byte range. +/// length - The size, in ints, of |buffer|. +/// Return value: +/// Returns the number of ints in the byte range on +/// success, 0 on error. +/// +/// |buffer| is an array of pairs of integers (starting byte offset, +/// length in bytes) that describes the exact byte range for the digest +/// calculation. If |length| is less than the returned length, or +/// |buffer| is NULL, |buffer| will not be modified. +int FPDFSignatureObj_GetByteRange(FPDF_SIGNATURE signature, +ffi.Pointer buffer, +int length, +) { + return _FPDFSignatureObj_GetByteRange(signature, +buffer, +length, +); +} + +late final _FPDFSignatureObj_GetByteRangePtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFSignatureObj_GetByteRange'); +late final _FPDFSignatureObj_GetByteRange = _FPDFSignatureObj_GetByteRangePtr.asFunction , int )>(); + +/// Experimental API. +/// Function: FPDFSignatureObj_GetSubFilter +/// Get the encoding of the value of a signature object. +/// Parameters: +/// signature - Handle to the signature object. Returned by +/// FPDF_GetSignatureObject(). +/// buffer - The address of a buffer that receives the encoding. +/// length - The size, in bytes, of |buffer|. +/// Return value: +/// Returns the number of bytes in the encoding name (including the +/// trailing NUL character) on success, 0 on error. +/// +/// The |buffer| is always encoded in 7-bit ASCII. If |length| is less than the +/// returned length, or |buffer| is NULL, |buffer| will not be modified. +int FPDFSignatureObj_GetSubFilter(FPDF_SIGNATURE signature, +ffi.Pointer buffer, +int length, +) { + return _FPDFSignatureObj_GetSubFilter(signature, +buffer, +length, +); +} + +late final _FPDFSignatureObj_GetSubFilterPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFSignatureObj_GetSubFilter'); +late final _FPDFSignatureObj_GetSubFilter = _FPDFSignatureObj_GetSubFilterPtr.asFunction , int )>(); + +/// Experimental API. +/// Function: FPDFSignatureObj_GetReason +/// Get the reason (comment) of the signature object. +/// Parameters: +/// signature - Handle to the signature object. Returned by +/// FPDF_GetSignatureObject(). +/// buffer - The address of a buffer that receives the reason. +/// length - The size, in bytes, of |buffer|. +/// Return value: +/// Returns the number of bytes in the reason on success, 0 on error. +/// +/// Regardless of the platform, the |buffer| is always in UTF-16LE encoding. The +/// string is terminated by a UTF16 NUL character. If |length| is less than the +/// returned length, or |buffer| is NULL, |buffer| will not be modified. +int FPDFSignatureObj_GetReason(FPDF_SIGNATURE signature, +ffi.Pointer buffer, +int length, +) { + return _FPDFSignatureObj_GetReason(signature, +buffer, +length, +); +} + +late final _FPDFSignatureObj_GetReasonPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFSignatureObj_GetReason'); +late final _FPDFSignatureObj_GetReason = _FPDFSignatureObj_GetReasonPtr.asFunction , int )>(); + +/// Experimental API. +/// Function: FPDFSignatureObj_GetTime +/// Get the time of signing of a signature object. +/// Parameters: +/// signature - Handle to the signature object. Returned by +/// FPDF_GetSignatureObject(). +/// buffer - The address of a buffer that receives the time. +/// length - The size, in bytes, of |buffer|. +/// Return value: +/// Returns the number of bytes in the encoding name (including the +/// trailing NUL character) on success, 0 on error. +/// +/// The |buffer| is always encoded in 7-bit ASCII. If |length| is less than the +/// returned length, or |buffer| is NULL, |buffer| will not be modified. +/// +/// The format of time is expected to be D:YYYYMMDDHHMMSS+XX'YY', i.e. it's +/// percision is seconds, with timezone information. This value should be used +/// only when the time of signing is not available in the (PKCS#7 binary) +/// signature. +int FPDFSignatureObj_GetTime(FPDF_SIGNATURE signature, +ffi.Pointer buffer, +int length, +) { + return _FPDFSignatureObj_GetTime(signature, +buffer, +length, +); +} + +late final _FPDFSignatureObj_GetTimePtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFSignatureObj_GetTime'); +late final _FPDFSignatureObj_GetTime = _FPDFSignatureObj_GetTimePtr.asFunction , int )>(); + +/// Experimental API. +/// Function: FPDFSignatureObj_GetDocMDPPermission +/// Get the DocMDP permission of a signature object. +/// Parameters: +/// signature - Handle to the signature object. Returned by +/// FPDF_GetSignatureObject(). +/// Return value: +/// Returns the permission (1, 2 or 3) on success, 0 on error. +int FPDFSignatureObj_GetDocMDPPermission(FPDF_SIGNATURE signature, +) { + return _FPDFSignatureObj_GetDocMDPPermission(signature, +); +} + +late final _FPDFSignatureObj_GetDocMDPPermissionPtr = _lookup< + ffi.NativeFunction>('FPDFSignatureObj_GetDocMDPPermission'); +late final _FPDFSignatureObj_GetDocMDPPermission = _FPDFSignatureObj_GetDocMDPPermissionPtr.asFunction(); + +/// Function: FPDF_GetDefaultTTFMap +/// Returns a pointer to the default character set to TT Font name map. The +/// map is an array of FPDF_CharsetFontMap structs, with its end indicated +/// by a { -1, NULL } entry. +/// Parameters: +/// None. +/// Return Value: +/// Pointer to the Charset Font Map. +/// Note: +/// Once FPDF_GetDefaultTTFMapCount() and FPDF_GetDefaultTTFMapEntry() are no +/// longer experimental, this API will be marked as deprecated. +/// See https://crbug.com/348468114 +ffi.Pointer FPDF_GetDefaultTTFMap() { + return _FPDF_GetDefaultTTFMap(); +} + +late final _FPDF_GetDefaultTTFMapPtr = _lookup< + ffi.NativeFunction Function()>>('FPDF_GetDefaultTTFMap'); +late final _FPDF_GetDefaultTTFMap = _FPDF_GetDefaultTTFMapPtr.asFunction Function()>(); + +/// Experimental API. +/// +/// Function: FPDF_GetDefaultTTFMapCount +/// Returns the number of entries in the default character set to TT Font name +/// map. +/// Parameters: +/// None. +/// Return Value: +/// The number of entries in the map. +int FPDF_GetDefaultTTFMapCount() { + return _FPDF_GetDefaultTTFMapCount(); +} + +late final _FPDF_GetDefaultTTFMapCountPtr = _lookup< + ffi.NativeFunction>('FPDF_GetDefaultTTFMapCount'); +late final _FPDF_GetDefaultTTFMapCount = _FPDF_GetDefaultTTFMapCountPtr.asFunction(); + +/// Experimental API. +/// +/// Function: FPDF_GetDefaultTTFMapEntry +/// Returns an entry in the default character set to TT Font name map. +/// Parameters: +/// index - The index to the entry in the map to retrieve. +/// Return Value: +/// A pointer to the entry, if it is in the map, or NULL if the index is out +/// of bounds. +ffi.Pointer FPDF_GetDefaultTTFMapEntry(int index, +) { + return _FPDF_GetDefaultTTFMapEntry(index, +); +} + +late final _FPDF_GetDefaultTTFMapEntryPtr = _lookup< + ffi.NativeFunction Function(ffi.Size )>>('FPDF_GetDefaultTTFMapEntry'); +late final _FPDF_GetDefaultTTFMapEntry = _FPDF_GetDefaultTTFMapEntryPtr.asFunction Function(int )>(); + +/// Function: FPDF_AddInstalledFont +/// Add a system font to the list in PDFium. +/// Comments: +/// This function is only called during the system font list building +/// process. +/// Parameters: +/// mapper - Opaque pointer to Foxit font mapper +/// face - The font face name +/// charset - Font character set. See above defined constants. +/// Return Value: +/// None. +void FPDF_AddInstalledFont(ffi.Pointer mapper, +ffi.Pointer face, +int charset, +) { + return _FPDF_AddInstalledFont(mapper, +face, +charset, +); +} + +late final _FPDF_AddInstalledFontPtr = _lookup< + ffi.NativeFunction , ffi.Pointer , ffi.Int )>>('FPDF_AddInstalledFont'); +late final _FPDF_AddInstalledFont = _FPDF_AddInstalledFontPtr.asFunction , ffi.Pointer , int )>(); + +/// Function: FPDF_SetSystemFontInfo +/// Set the system font info interface into PDFium +/// Parameters: +/// font_info - Pointer to a FPDF_SYSFONTINFO structure +/// Return Value: +/// None +/// Comments: +/// Platform support implementation should implement required methods of +/// FFDF_SYSFONTINFO interface, then call this function during PDFium +/// initialization process. +/// +/// Call this with NULL to tell PDFium to stop using a previously set +/// |FPDF_SYSFONTINFO|. +void FPDF_SetSystemFontInfo(ffi.Pointer font_info, +) { + return _FPDF_SetSystemFontInfo(font_info, +); +} + +late final _FPDF_SetSystemFontInfoPtr = _lookup< + ffi.NativeFunction )>>('FPDF_SetSystemFontInfo'); +late final _FPDF_SetSystemFontInfo = _FPDF_SetSystemFontInfoPtr.asFunction )>(); + +/// Function: FPDF_GetDefaultSystemFontInfo +/// Get default system font info interface for current platform +/// Parameters: +/// None +/// Return Value: +/// Pointer to a FPDF_SYSFONTINFO structure describing the default +/// interface, or NULL if the platform doesn't have a default interface. +/// Application should call FPDF_FreeDefaultSystemFontInfo to free the +/// returned pointer. +/// Comments: +/// For some platforms, PDFium implements a default version of system +/// font info interface. The default implementation can be passed to +/// FPDF_SetSystemFontInfo(). +ffi.Pointer FPDF_GetDefaultSystemFontInfo() { + return _FPDF_GetDefaultSystemFontInfo(); +} + +late final _FPDF_GetDefaultSystemFontInfoPtr = _lookup< + ffi.NativeFunction Function()>>('FPDF_GetDefaultSystemFontInfo'); +late final _FPDF_GetDefaultSystemFontInfo = _FPDF_GetDefaultSystemFontInfoPtr.asFunction Function()>(); + +/// Function: FPDF_FreeDefaultSystemFontInfo +/// Free a default system font info interface +/// Parameters: +/// font_info - Pointer to a FPDF_SYSFONTINFO structure +/// Return Value: +/// None +/// Comments: +/// This function should be called on the output from +/// FPDF_GetDefaultSystemFontInfo() once it is no longer needed. +void FPDF_FreeDefaultSystemFontInfo(ffi.Pointer font_info, +) { + return _FPDF_FreeDefaultSystemFontInfo(font_info, +); +} + +late final _FPDF_FreeDefaultSystemFontInfoPtr = _lookup< + ffi.NativeFunction )>>('FPDF_FreeDefaultSystemFontInfo'); +late final _FPDF_FreeDefaultSystemFontInfo = _FPDF_FreeDefaultSystemFontInfoPtr.asFunction )>(); + +/// Experimental API. +/// Get the number of JavaScript actions in |document|. +/// +/// document - handle to a document. +/// +/// Returns the number of JavaScript actions in |document| or -1 on error. +int FPDFDoc_GetJavaScriptActionCount(FPDF_DOCUMENT document, +) { + return _FPDFDoc_GetJavaScriptActionCount(document, +); +} + +late final _FPDFDoc_GetJavaScriptActionCountPtr = _lookup< + ffi.NativeFunction>('FPDFDoc_GetJavaScriptActionCount'); +late final _FPDFDoc_GetJavaScriptActionCount = _FPDFDoc_GetJavaScriptActionCountPtr.asFunction(); + +/// Experimental API. +/// Get the JavaScript action at |index| in |document|. +/// +/// document - handle to a document. +/// index - the index of the requested JavaScript action. +/// +/// Returns the handle to the JavaScript action, or NULL on failure. +/// Caller owns the returned handle and must close it with +/// FPDFDoc_CloseJavaScriptAction(). +FPDF_JAVASCRIPT_ACTION FPDFDoc_GetJavaScriptAction(FPDF_DOCUMENT document, +int index, +) { + return _FPDFDoc_GetJavaScriptAction(document, +index, +); +} + +late final _FPDFDoc_GetJavaScriptActionPtr = _lookup< + ffi.NativeFunction>('FPDFDoc_GetJavaScriptAction'); +late final _FPDFDoc_GetJavaScriptAction = _FPDFDoc_GetJavaScriptActionPtr.asFunction(); + +/// javascript - Handle to a JavaScript action. +void FPDFDoc_CloseJavaScriptAction(FPDF_JAVASCRIPT_ACTION javascript, +) { + return _FPDFDoc_CloseJavaScriptAction(javascript, +); +} + +late final _FPDFDoc_CloseJavaScriptActionPtr = _lookup< + ffi.NativeFunction>('FPDFDoc_CloseJavaScriptAction'); +late final _FPDFDoc_CloseJavaScriptAction = _FPDFDoc_CloseJavaScriptActionPtr.asFunction(); + +/// Experimental API. +/// Get the name from the |javascript| handle. |buffer| is only modified if +/// |buflen| is longer than the length of the name. On errors, |buffer| is +/// unmodified and the returned length is 0. +/// +/// javascript - handle to an JavaScript action. +/// buffer - buffer for holding the name, encoded in UTF-16LE. +/// buflen - length of the buffer in bytes. +/// +/// Returns the length of the JavaScript action name in bytes. +int FPDFJavaScriptAction_GetName(FPDF_JAVASCRIPT_ACTION javascript, +ffi.Pointer buffer, +int buflen, +) { + return _FPDFJavaScriptAction_GetName(javascript, +buffer, +buflen, +); +} + +late final _FPDFJavaScriptAction_GetNamePtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFJavaScriptAction_GetName'); +late final _FPDFJavaScriptAction_GetName = _FPDFJavaScriptAction_GetNamePtr.asFunction , int )>(); + +/// Experimental API. +/// Get the script from the |javascript| handle. |buffer| is only modified if +/// |buflen| is longer than the length of the script. On errors, |buffer| is +/// unmodified and the returned length is 0. +/// +/// javascript - handle to an JavaScript action. +/// buffer - buffer for holding the name, encoded in UTF-16LE. +/// buflen - length of the buffer in bytes. +/// +/// Returns the length of the JavaScript action name in bytes. +int FPDFJavaScriptAction_GetScript(FPDF_JAVASCRIPT_ACTION javascript, +ffi.Pointer buffer, +int buflen, +) { + return _FPDFJavaScriptAction_GetScript(javascript, +buffer, +buflen, +); +} + +late final _FPDFJavaScriptAction_GetScriptPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFJavaScriptAction_GetScript'); +late final _FPDFJavaScriptAction_GetScript = _FPDFJavaScriptAction_GetScriptPtr.asFunction , int )>(); + +/// Function: FPDFText_LoadPage +/// Prepare information about all characters in a page. +/// Parameters: +/// page - Handle to the page. Returned by FPDF_LoadPage function +/// (in FPDFVIEW module). +/// Return value: +/// A handle to the text page information structure. +/// NULL if something goes wrong. +/// Comments: +/// Application must call FPDFText_ClosePage to release the text page +/// information. +FPDF_TEXTPAGE FPDFText_LoadPage(FPDF_PAGE page, +) { + return _FPDFText_LoadPage(page, +); +} + +late final _FPDFText_LoadPagePtr = _lookup< + ffi.NativeFunction>('FPDFText_LoadPage'); +late final _FPDFText_LoadPage = _FPDFText_LoadPagePtr.asFunction(); + +/// Function: FPDFText_ClosePage +/// Release all resources allocated for a text page information +/// structure. +/// Parameters: +/// text_page - Handle to a text page information structure. +/// Returned by FPDFText_LoadPage function. +/// Return Value: +/// None. +void FPDFText_ClosePage(FPDF_TEXTPAGE text_page, +) { + return _FPDFText_ClosePage(text_page, +); +} + +late final _FPDFText_ClosePagePtr = _lookup< + ffi.NativeFunction>('FPDFText_ClosePage'); +late final _FPDFText_ClosePage = _FPDFText_ClosePagePtr.asFunction(); + +/// Function: FPDFText_CountChars +/// Get number of characters in a page. +/// Parameters: +/// text_page - Handle to a text page information structure. +/// Returned by FPDFText_LoadPage function. +/// Return value: +/// Number of characters in the page. Return -1 for error. +/// Generated characters, like additional space characters, new line +/// characters, are also counted. +/// Comments: +/// Characters in a page form a "stream", inside the stream, each +/// character has an index. +/// We will use the index parameters in many of FPDFTEXT functions. The +/// first character in the page +/// has an index value of zero. +int FPDFText_CountChars(FPDF_TEXTPAGE text_page, +) { + return _FPDFText_CountChars(text_page, +); +} + +late final _FPDFText_CountCharsPtr = _lookup< + ffi.NativeFunction>('FPDFText_CountChars'); +late final _FPDFText_CountChars = _FPDFText_CountCharsPtr.asFunction(); + +/// Function: FPDFText_GetUnicode +/// Get Unicode of a character in a page. +/// Parameters: +/// text_page - Handle to a text page information structure. +/// Returned by FPDFText_LoadPage function. +/// index - Zero-based index of the character. +/// Return value: +/// The Unicode of the particular character. +/// If a character is not encoded in Unicode and Foxit engine can't +/// convert to Unicode, +/// the return value will be zero. +int FPDFText_GetUnicode(FPDF_TEXTPAGE text_page, +int index, +) { + return _FPDFText_GetUnicode(text_page, +index, +); +} + +late final _FPDFText_GetUnicodePtr = _lookup< + ffi.NativeFunction>('FPDFText_GetUnicode'); +late final _FPDFText_GetUnicode = _FPDFText_GetUnicodePtr.asFunction(); + +/// Experimental API. +/// Function: FPDFText_GetTextObject +/// Get the FPDF_PAGEOBJECT associated with a given character. +/// Parameters: +/// text_page - Handle to a text page information structure. +/// Returned by FPDFText_LoadPage function. +/// index - Zero-based index of the character. +/// Return value: +/// The associated text object for the character at |index|, or NULL on +/// error. The returned text object, if non-null, is of type +/// |FPDF_PAGEOBJ_TEXT|. The caller does not own the returned object. +FPDF_PAGEOBJECT FPDFText_GetTextObject(FPDF_TEXTPAGE text_page, +int index, +) { + return _FPDFText_GetTextObject(text_page, +index, +); +} + +late final _FPDFText_GetTextObjectPtr = _lookup< + ffi.NativeFunction>('FPDFText_GetTextObject'); +late final _FPDFText_GetTextObject = _FPDFText_GetTextObjectPtr.asFunction(); + +/// Experimental API. +/// Function: FPDFText_IsGenerated +/// Get if a character in a page is generated by PDFium. +/// Parameters: +/// text_page - Handle to a text page information structure. +/// Returned by FPDFText_LoadPage function. +/// index - Zero-based index of the character. +/// Return value: +/// 1 if the character is generated by PDFium. +/// 0 if the character is not generated by PDFium. +/// -1 if there was an error. +int FPDFText_IsGenerated(FPDF_TEXTPAGE text_page, +int index, +) { + return _FPDFText_IsGenerated(text_page, +index, +); +} + +late final _FPDFText_IsGeneratedPtr = _lookup< + ffi.NativeFunction>('FPDFText_IsGenerated'); +late final _FPDFText_IsGenerated = _FPDFText_IsGeneratedPtr.asFunction(); + +/// Experimental API. +/// Function: FPDFText_IsHyphen +/// Get if a character in a page is a hyphen. +/// Parameters: +/// text_page - Handle to a text page information structure. +/// Returned by FPDFText_LoadPage function. +/// index - Zero-based index of the character. +/// Return value: +/// 1 if the character is a hyphen. +/// 0 if the character is not a hyphen. +/// -1 if there was an error. +int FPDFText_IsHyphen(FPDF_TEXTPAGE text_page, +int index, +) { + return _FPDFText_IsHyphen(text_page, +index, +); +} + +late final _FPDFText_IsHyphenPtr = _lookup< + ffi.NativeFunction>('FPDFText_IsHyphen'); +late final _FPDFText_IsHyphen = _FPDFText_IsHyphenPtr.asFunction(); + +/// Experimental API. +/// Function: FPDFText_HasUnicodeMapError +/// Get if a character in a page has an invalid unicode mapping. +/// Parameters: +/// text_page - Handle to a text page information structure. +/// Returned by FPDFText_LoadPage function. +/// index - Zero-based index of the character. +/// Return value: +/// 1 if the character has an invalid unicode mapping. +/// 0 if the character has no known unicode mapping issues. +/// -1 if there was an error. +int FPDFText_HasUnicodeMapError(FPDF_TEXTPAGE text_page, +int index, +) { + return _FPDFText_HasUnicodeMapError(text_page, +index, +); +} + +late final _FPDFText_HasUnicodeMapErrorPtr = _lookup< + ffi.NativeFunction>('FPDFText_HasUnicodeMapError'); +late final _FPDFText_HasUnicodeMapError = _FPDFText_HasUnicodeMapErrorPtr.asFunction(); + +/// Function: FPDFText_GetFontSize +/// Get the font size of a particular character. +/// Parameters: +/// text_page - Handle to a text page information structure. +/// Returned by FPDFText_LoadPage function. +/// index - Zero-based index of the character. +/// Return value: +/// The font size of the particular character, measured in points (about +/// 1/72 inch). This is the typographic size of the font (so called +/// "em size"). +double FPDFText_GetFontSize(FPDF_TEXTPAGE text_page, +int index, +) { + return _FPDFText_GetFontSize(text_page, +index, +); +} + +late final _FPDFText_GetFontSizePtr = _lookup< + ffi.NativeFunction>('FPDFText_GetFontSize'); +late final _FPDFText_GetFontSize = _FPDFText_GetFontSizePtr.asFunction(); + +/// Experimental API. +/// Function: FPDFText_GetFontInfo +/// Get the font name and flags of a particular character. +/// Parameters: +/// text_page - Handle to a text page information structure. +/// Returned by FPDFText_LoadPage function. +/// index - Zero-based index of the character. +/// buffer - A buffer receiving the font name. +/// buflen - The length of |buffer| in bytes. +/// flags - Optional pointer to an int receiving the font flags. +/// These flags should be interpreted per PDF spec 1.7 +/// Section 5.7.1 Font Descriptor Flags. +/// Return value: +/// On success, return the length of the font name, including the +/// trailing NUL character, in bytes. If this length is less than or +/// equal to |length|, |buffer| is set to the font name, |flags| is +/// set to the font flags. |buffer| is in UTF-8 encoding. Return 0 on +/// failure. +int FPDFText_GetFontInfo(FPDF_TEXTPAGE text_page, +int index, +ffi.Pointer buffer, +int buflen, +ffi.Pointer flags, +) { + return _FPDFText_GetFontInfo(text_page, +index, +buffer, +buflen, +flags, +); +} + +late final _FPDFText_GetFontInfoPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong , ffi.Pointer )>>('FPDFText_GetFontInfo'); +late final _FPDFText_GetFontInfo = _FPDFText_GetFontInfoPtr.asFunction , int , ffi.Pointer )>(); + +/// Experimental API. +/// Function: FPDFText_GetFontWeight +/// Get the font weight of a particular character. +/// Parameters: +/// text_page - Handle to a text page information structure. +/// Returned by FPDFText_LoadPage function. +/// index - Zero-based index of the character. +/// Return value: +/// On success, return the font weight of the particular character. If +/// |text_page| is invalid, if |index| is out of bounds, or if the +/// character's text object is undefined, return -1. +int FPDFText_GetFontWeight(FPDF_TEXTPAGE text_page, +int index, +) { + return _FPDFText_GetFontWeight(text_page, +index, +); +} + +late final _FPDFText_GetFontWeightPtr = _lookup< + ffi.NativeFunction>('FPDFText_GetFontWeight'); +late final _FPDFText_GetFontWeight = _FPDFText_GetFontWeightPtr.asFunction(); + +/// Experimental API. +/// Function: FPDFText_GetFillColor +/// Get the fill color of a particular character. +/// Parameters: +/// text_page - Handle to a text page information structure. +/// Returned by FPDFText_LoadPage function. +/// index - Zero-based index of the character. +/// R - Pointer to an unsigned int number receiving the +/// red value of the fill color. +/// G - Pointer to an unsigned int number receiving the +/// green value of the fill color. +/// B - Pointer to an unsigned int number receiving the +/// blue value of the fill color. +/// A - Pointer to an unsigned int number receiving the +/// alpha value of the fill color. +/// Return value: +/// Whether the call succeeded. If false, |R|, |G|, |B| and |A| are +/// unchanged. +int FPDFText_GetFillColor(FPDF_TEXTPAGE text_page, +int index, +ffi.Pointer R, +ffi.Pointer G, +ffi.Pointer B, +ffi.Pointer A, +) { + return _FPDFText_GetFillColor(text_page, +index, +R, +G, +B, +A, +); +} + +late final _FPDFText_GetFillColorPtr = _lookup< + ffi.NativeFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>>('FPDFText_GetFillColor'); +late final _FPDFText_GetFillColor = _FPDFText_GetFillColorPtr.asFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>(); + +/// Experimental API. +/// Function: FPDFText_GetStrokeColor +/// Get the stroke color of a particular character. +/// Parameters: +/// text_page - Handle to a text page information structure. +/// Returned by FPDFText_LoadPage function. +/// index - Zero-based index of the character. +/// R - Pointer to an unsigned int number receiving the +/// red value of the stroke color. +/// G - Pointer to an unsigned int number receiving the +/// green value of the stroke color. +/// B - Pointer to an unsigned int number receiving the +/// blue value of the stroke color. +/// A - Pointer to an unsigned int number receiving the +/// alpha value of the stroke color. +/// Return value: +/// Whether the call succeeded. If false, |R|, |G|, |B| and |A| are +/// unchanged. +int FPDFText_GetStrokeColor(FPDF_TEXTPAGE text_page, +int index, +ffi.Pointer R, +ffi.Pointer G, +ffi.Pointer B, +ffi.Pointer A, +) { + return _FPDFText_GetStrokeColor(text_page, +index, +R, +G, +B, +A, +); +} + +late final _FPDFText_GetStrokeColorPtr = _lookup< + ffi.NativeFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>>('FPDFText_GetStrokeColor'); +late final _FPDFText_GetStrokeColor = _FPDFText_GetStrokeColorPtr.asFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>(); + +/// Experimental API. +/// Function: FPDFText_GetCharAngle +/// Get character rotation angle. +/// Parameters: +/// text_page - Handle to a text page information structure. +/// Returned by FPDFText_LoadPage function. +/// index - Zero-based index of the character. +/// Return Value: +/// On success, return the angle value in radian. Value will always be +/// greater or equal to 0. If |text_page| is invalid, or if |index| is +/// out of bounds, then return -1. +double FPDFText_GetCharAngle(FPDF_TEXTPAGE text_page, +int index, +) { + return _FPDFText_GetCharAngle(text_page, +index, +); +} + +late final _FPDFText_GetCharAnglePtr = _lookup< + ffi.NativeFunction>('FPDFText_GetCharAngle'); +late final _FPDFText_GetCharAngle = _FPDFText_GetCharAnglePtr.asFunction(); + +/// Function: FPDFText_GetCharBox +/// Get bounding box of a particular character. +/// Parameters: +/// text_page - Handle to a text page information structure. +/// Returned by FPDFText_LoadPage function. +/// index - Zero-based index of the character. +/// left - Pointer to a double number receiving left position +/// of the character box. +/// right - Pointer to a double number receiving right position +/// of the character box. +/// bottom - Pointer to a double number receiving bottom position +/// of the character box. +/// top - Pointer to a double number receiving top position of +/// the character box. +/// Return Value: +/// On success, return TRUE and fill in |left|, |right|, |bottom|, and +/// |top|. If |text_page| is invalid, or if |index| is out of bounds, +/// then return FALSE, and the out parameters remain unmodified. +/// Comments: +/// All positions are measured in PDF "user space". +int FPDFText_GetCharBox(FPDF_TEXTPAGE text_page, +int index, +ffi.Pointer left, +ffi.Pointer right, +ffi.Pointer bottom, +ffi.Pointer top, +) { + return _FPDFText_GetCharBox(text_page, +index, +left, +right, +bottom, +top, +); +} + +late final _FPDFText_GetCharBoxPtr = _lookup< + ffi.NativeFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>>('FPDFText_GetCharBox'); +late final _FPDFText_GetCharBox = _FPDFText_GetCharBoxPtr.asFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>(); + +/// Experimental API. +/// Function: FPDFText_GetLooseCharBox +/// Get a "loose" bounding box of a particular character, i.e., covering +/// the entire glyph bounds, without taking the actual glyph shape into +/// account. +/// Parameters: +/// text_page - Handle to a text page information structure. +/// Returned by FPDFText_LoadPage function. +/// index - Zero-based index of the character. +/// rect - Pointer to a FS_RECTF receiving the character box. +/// Return Value: +/// On success, return TRUE and fill in |rect|. If |text_page| is +/// invalid, or if |index| is out of bounds, then return FALSE, and the +/// |rect| out parameter remains unmodified. +/// Comments: +/// All positions are measured in PDF "user space". +int FPDFText_GetLooseCharBox(FPDF_TEXTPAGE text_page, +int index, +ffi.Pointer rect, +) { + return _FPDFText_GetLooseCharBox(text_page, +index, +rect, +); +} + +late final _FPDFText_GetLooseCharBoxPtr = _lookup< + ffi.NativeFunction )>>('FPDFText_GetLooseCharBox'); +late final _FPDFText_GetLooseCharBox = _FPDFText_GetLooseCharBoxPtr.asFunction )>(); + +/// Experimental API. +/// Function: FPDFText_GetMatrix +/// Get the effective transformation matrix for a particular character. +/// Parameters: +/// text_page - Handle to a text page information structure. +/// Returned by FPDFText_LoadPage(). +/// index - Zero-based index of the character. +/// matrix - Pointer to a FS_MATRIX receiving the transformation +/// matrix. +/// Return Value: +/// On success, return TRUE and fill in |matrix|. If |text_page| is +/// invalid, or if |index| is out of bounds, or if |matrix| is NULL, +/// then return FALSE, and |matrix| remains unmodified. +int FPDFText_GetMatrix(FPDF_TEXTPAGE text_page, +int index, +ffi.Pointer matrix, +) { + return _FPDFText_GetMatrix(text_page, +index, +matrix, +); +} + +late final _FPDFText_GetMatrixPtr = _lookup< + ffi.NativeFunction )>>('FPDFText_GetMatrix'); +late final _FPDFText_GetMatrix = _FPDFText_GetMatrixPtr.asFunction )>(); + +/// Function: FPDFText_GetCharOrigin +/// Get origin of a particular character. +/// Parameters: +/// text_page - Handle to a text page information structure. +/// Returned by FPDFText_LoadPage function. +/// index - Zero-based index of the character. +/// x - Pointer to a double number receiving x coordinate of +/// the character origin. +/// y - Pointer to a double number receiving y coordinate of +/// the character origin. +/// Return Value: +/// Whether the call succeeded. If false, x and y are unchanged. +/// Comments: +/// All positions are measured in PDF "user space". +int FPDFText_GetCharOrigin(FPDF_TEXTPAGE text_page, +int index, +ffi.Pointer x, +ffi.Pointer y, +) { + return _FPDFText_GetCharOrigin(text_page, +index, +x, +y, +); +} + +late final _FPDFText_GetCharOriginPtr = _lookup< + ffi.NativeFunction , ffi.Pointer )>>('FPDFText_GetCharOrigin'); +late final _FPDFText_GetCharOrigin = _FPDFText_GetCharOriginPtr.asFunction , ffi.Pointer )>(); + +/// Function: FPDFText_GetCharIndexAtPos +/// Get the index of a character at or nearby a certain position on the +/// page. +/// Parameters: +/// text_page - Handle to a text page information structure. +/// Returned by FPDFText_LoadPage function. +/// x - X position in PDF "user space". +/// y - Y position in PDF "user space". +/// xTolerance - An x-axis tolerance value for character hit +/// detection, in point units. +/// yTolerance - A y-axis tolerance value for character hit +/// detection, in point units. +/// Return Value: +/// The zero-based index of the character at, or nearby the point (x,y). +/// If there is no character at or nearby the point, return value will +/// be -1. If an error occurs, -3 will be returned. +int FPDFText_GetCharIndexAtPos(FPDF_TEXTPAGE text_page, +double x, +double y, +double xTolerance, +double yTolerance, +) { + return _FPDFText_GetCharIndexAtPos(text_page, +x, +y, +xTolerance, +yTolerance, +); +} + +late final _FPDFText_GetCharIndexAtPosPtr = _lookup< + ffi.NativeFunction>('FPDFText_GetCharIndexAtPos'); +late final _FPDFText_GetCharIndexAtPos = _FPDFText_GetCharIndexAtPosPtr.asFunction(); + +/// Function: FPDFText_GetText +/// Extract unicode text string from the page. +/// Parameters: +/// text_page - Handle to a text page information structure. +/// Returned by FPDFText_LoadPage function. +/// start_index - Index for the start characters. +/// count - Number of UCS-2 values to be extracted. +/// result - A buffer (allocated by application) receiving the +/// extracted UCS-2 values. The buffer must be able to +/// hold `count` UCS-2 values plus a terminator. +/// Return Value: +/// Number of characters written into the result buffer, including the +/// trailing terminator. +/// Comments: +/// This function ignores characters without UCS-2 representations. +/// It considers all characters on the page, even those that are not +/// visible when the page has a cropbox. To filter out the characters +/// outside of the cropbox, use FPDF_GetPageBoundingBox() and +/// FPDFText_GetCharBox(). +int FPDFText_GetText(FPDF_TEXTPAGE text_page, +int start_index, +int count, +ffi.Pointer result, +) { + return _FPDFText_GetText(text_page, +start_index, +count, +result, +); +} + +late final _FPDFText_GetTextPtr = _lookup< + ffi.NativeFunction )>>('FPDFText_GetText'); +late final _FPDFText_GetText = _FPDFText_GetTextPtr.asFunction )>(); + +/// Function: FPDFText_CountRects +/// Counts number of rectangular areas occupied by a segment of text, +/// and caches the result for subsequent FPDFText_GetRect() calls. +/// Parameters: +/// text_page - Handle to a text page information structure. +/// Returned by FPDFText_LoadPage function. +/// start_index - Index for the start character. +/// count - Number of characters, or -1 for all remaining. +/// Return value: +/// Number of rectangles, 0 if text_page is null, or -1 on bad +/// start_index. +/// Comments: +/// This function, along with FPDFText_GetRect can be used by +/// applications to detect the position on the page for a text segment, +/// so proper areas can be highlighted. The FPDFText_* functions will +/// automatically merge small character boxes into bigger one if those +/// characters are on the same line and use same font settings. +int FPDFText_CountRects(FPDF_TEXTPAGE text_page, +int start_index, +int count, +) { + return _FPDFText_CountRects(text_page, +start_index, +count, +); +} + +late final _FPDFText_CountRectsPtr = _lookup< + ffi.NativeFunction>('FPDFText_CountRects'); +late final _FPDFText_CountRects = _FPDFText_CountRectsPtr.asFunction(); + +/// Function: FPDFText_GetRect +/// Get a rectangular area from the result generated by +/// FPDFText_CountRects. +/// Parameters: +/// text_page - Handle to a text page information structure. +/// Returned by FPDFText_LoadPage function. +/// rect_index - Zero-based index for the rectangle. +/// left - Pointer to a double value receiving the rectangle +/// left boundary. +/// top - Pointer to a double value receiving the rectangle +/// top boundary. +/// right - Pointer to a double value receiving the rectangle +/// right boundary. +/// bottom - Pointer to a double value receiving the rectangle +/// bottom boundary. +/// Return Value: +/// On success, return TRUE and fill in |left|, |top|, |right|, and +/// |bottom|. If |text_page| is invalid then return FALSE, and the out +/// parameters remain unmodified. If |text_page| is valid but +/// |rect_index| is out of bounds, then return FALSE and set the out +/// parameters to 0. +int FPDFText_GetRect(FPDF_TEXTPAGE text_page, +int rect_index, +ffi.Pointer left, +ffi.Pointer top, +ffi.Pointer right, +ffi.Pointer bottom, +) { + return _FPDFText_GetRect(text_page, +rect_index, +left, +top, +right, +bottom, +); +} + +late final _FPDFText_GetRectPtr = _lookup< + ffi.NativeFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>>('FPDFText_GetRect'); +late final _FPDFText_GetRect = _FPDFText_GetRectPtr.asFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>(); + +/// Function: FPDFText_GetBoundedText +/// Extract unicode text within a rectangular boundary on the page. +/// Parameters: +/// text_page - Handle to a text page information structure. +/// Returned by FPDFText_LoadPage function. +/// left - Left boundary. +/// top - Top boundary. +/// right - Right boundary. +/// bottom - Bottom boundary. +/// buffer - Caller-allocated buffer to receive UTF-16 values. +/// buflen - Number of UTF-16 values (not bytes) that `buffer` +/// is capable of holding. +/// Return Value: +/// If buffer is NULL or buflen is zero, return number of UTF-16 +/// values (not bytes) of text present within the rectangle, excluding +/// a terminating NUL. Generally you should pass a buffer at least one +/// larger than this if you want a terminating NUL, which will be +/// provided if space is available. Otherwise, return number of UTF-16 +/// values copied into the buffer, including the terminating NUL when +/// space for it is available. +/// Comment: +/// If the buffer is too small, as much text as will fit is copied into +/// it. May return a split surrogate in that case. +int FPDFText_GetBoundedText(FPDF_TEXTPAGE text_page, +double left, +double top, +double right, +double bottom, +ffi.Pointer buffer, +int buflen, +) { + return _FPDFText_GetBoundedText(text_page, +left, +top, +right, +bottom, +buffer, +buflen, +); +} + +late final _FPDFText_GetBoundedTextPtr = _lookup< + ffi.NativeFunction , ffi.Int )>>('FPDFText_GetBoundedText'); +late final _FPDFText_GetBoundedText = _FPDFText_GetBoundedTextPtr.asFunction , int )>(); + +/// Function: FPDFText_FindStart +/// Start a search. +/// Parameters: +/// text_page - Handle to a text page information structure. +/// Returned by FPDFText_LoadPage function. +/// findwhat - A unicode match pattern. +/// flags - Option flags. +/// start_index - Start from this character. -1 for end of the page. +/// Return Value: +/// A handle for the search context. FPDFText_FindClose must be called +/// to release this handle. +FPDF_SCHHANDLE FPDFText_FindStart(FPDF_TEXTPAGE text_page, +FPDF_WIDESTRING findwhat, +int flags, +int start_index, +) { + return _FPDFText_FindStart(text_page, +findwhat, +flags, +start_index, +); +} + +late final _FPDFText_FindStartPtr = _lookup< + ffi.NativeFunction>('FPDFText_FindStart'); +late final _FPDFText_FindStart = _FPDFText_FindStartPtr.asFunction(); + +/// Function: FPDFText_FindNext +/// Search in the direction from page start to end. +/// Parameters: +/// handle - A search context handle returned by +/// FPDFText_FindStart. +/// Return Value: +/// Whether a match is found. +int FPDFText_FindNext(FPDF_SCHHANDLE handle, +) { + return _FPDFText_FindNext(handle, +); +} + +late final _FPDFText_FindNextPtr = _lookup< + ffi.NativeFunction>('FPDFText_FindNext'); +late final _FPDFText_FindNext = _FPDFText_FindNextPtr.asFunction(); + +/// Function: FPDFText_FindPrev +/// Search in the direction from page end to start. +/// Parameters: +/// handle - A search context handle returned by +/// FPDFText_FindStart. +/// Return Value: +/// Whether a match is found. +int FPDFText_FindPrev(FPDF_SCHHANDLE handle, +) { + return _FPDFText_FindPrev(handle, +); +} + +late final _FPDFText_FindPrevPtr = _lookup< + ffi.NativeFunction>('FPDFText_FindPrev'); +late final _FPDFText_FindPrev = _FPDFText_FindPrevPtr.asFunction(); + +/// Function: FPDFText_GetSchResultIndex +/// Get the starting character index of the search result. +/// Parameters: +/// handle - A search context handle returned by +/// FPDFText_FindStart. +/// Return Value: +/// Index for the starting character. +int FPDFText_GetSchResultIndex(FPDF_SCHHANDLE handle, +) { + return _FPDFText_GetSchResultIndex(handle, +); +} + +late final _FPDFText_GetSchResultIndexPtr = _lookup< + ffi.NativeFunction>('FPDFText_GetSchResultIndex'); +late final _FPDFText_GetSchResultIndex = _FPDFText_GetSchResultIndexPtr.asFunction(); + +/// Function: FPDFText_GetSchCount +/// Get the number of matched characters in the search result. +/// Parameters: +/// handle - A search context handle returned by +/// FPDFText_FindStart. +/// Return Value: +/// Number of matched characters. +int FPDFText_GetSchCount(FPDF_SCHHANDLE handle, +) { + return _FPDFText_GetSchCount(handle, +); +} + +late final _FPDFText_GetSchCountPtr = _lookup< + ffi.NativeFunction>('FPDFText_GetSchCount'); +late final _FPDFText_GetSchCount = _FPDFText_GetSchCountPtr.asFunction(); + +/// Function: FPDFText_FindClose +/// Release a search context. +/// Parameters: +/// handle - A search context handle returned by +/// FPDFText_FindStart. +/// Return Value: +/// None. +void FPDFText_FindClose(FPDF_SCHHANDLE handle, +) { + return _FPDFText_FindClose(handle, +); +} + +late final _FPDFText_FindClosePtr = _lookup< + ffi.NativeFunction>('FPDFText_FindClose'); +late final _FPDFText_FindClose = _FPDFText_FindClosePtr.asFunction(); + +/// Function: FPDFLink_LoadWebLinks +/// Prepare information about weblinks in a page. +/// Parameters: +/// text_page - Handle to a text page information structure. +/// Returned by FPDFText_LoadPage function. +/// Return Value: +/// A handle to the page's links information structure, or +/// NULL if something goes wrong. +/// Comments: +/// Weblinks are those links implicitly embedded in PDF pages. PDF also +/// has a type of annotation called "link" (FPDFTEXT doesn't deal with +/// that kind of link). FPDFTEXT weblink feature is useful for +/// automatically detecting links in the page contents. For example, +/// things like "https://www.example.com" will be detected, so +/// applications can allow user to click on those characters to activate +/// the link, even the PDF doesn't come with link annotations. +/// +/// FPDFLink_CloseWebLinks must be called to release resources. +FPDF_PAGELINK FPDFLink_LoadWebLinks(FPDF_TEXTPAGE text_page, +) { + return _FPDFLink_LoadWebLinks(text_page, +); +} + +late final _FPDFLink_LoadWebLinksPtr = _lookup< + ffi.NativeFunction>('FPDFLink_LoadWebLinks'); +late final _FPDFLink_LoadWebLinks = _FPDFLink_LoadWebLinksPtr.asFunction(); + +/// Function: FPDFLink_CountWebLinks +/// Count number of detected web links. +/// Parameters: +/// link_page - Handle returned by FPDFLink_LoadWebLinks. +/// Return Value: +/// Number of detected web links. +int FPDFLink_CountWebLinks(FPDF_PAGELINK link_page, +) { + return _FPDFLink_CountWebLinks(link_page, +); +} + +late final _FPDFLink_CountWebLinksPtr = _lookup< + ffi.NativeFunction>('FPDFLink_CountWebLinks'); +late final _FPDFLink_CountWebLinks = _FPDFLink_CountWebLinksPtr.asFunction(); + +/// Function: FPDFLink_GetURL +/// Fetch the URL information for a detected web link. +/// Parameters: +/// link_page - Handle returned by FPDFLink_LoadWebLinks. +/// link_index - Zero-based index for the link. +/// buffer - A unicode buffer for the result. +/// buflen - Number of 16-bit code units (not bytes) for the +/// buffer, including an additional terminator. +/// Return Value: +/// If |buffer| is NULL or |buflen| is zero, return the number of 16-bit +/// code units (not bytes) needed to buffer the result (an additional +/// terminator is included in this count). +/// Otherwise, copy the result into |buffer|, truncating at |buflen| if +/// the result is too large to fit, and return the number of 16-bit code +/// units actually copied into the buffer (the additional terminator is +/// also included in this count). +/// If |link_index| does not correspond to a valid link, then the result +/// is an empty string. +int FPDFLink_GetURL(FPDF_PAGELINK link_page, +int link_index, +ffi.Pointer buffer, +int buflen, +) { + return _FPDFLink_GetURL(link_page, +link_index, +buffer, +buflen, +); +} + +late final _FPDFLink_GetURLPtr = _lookup< + ffi.NativeFunction , ffi.Int )>>('FPDFLink_GetURL'); +late final _FPDFLink_GetURL = _FPDFLink_GetURLPtr.asFunction , int )>(); + +/// Function: FPDFLink_CountRects +/// Count number of rectangular areas for the link. +/// Parameters: +/// link_page - Handle returned by FPDFLink_LoadWebLinks. +/// link_index - Zero-based index for the link. +/// Return Value: +/// Number of rectangular areas for the link. If |link_index| does +/// not correspond to a valid link, then 0 is returned. +int FPDFLink_CountRects(FPDF_PAGELINK link_page, +int link_index, +) { + return _FPDFLink_CountRects(link_page, +link_index, +); +} + +late final _FPDFLink_CountRectsPtr = _lookup< + ffi.NativeFunction>('FPDFLink_CountRects'); +late final _FPDFLink_CountRects = _FPDFLink_CountRectsPtr.asFunction(); + +/// Function: FPDFLink_GetRect +/// Fetch the boundaries of a rectangle for a link. +/// Parameters: +/// link_page - Handle returned by FPDFLink_LoadWebLinks. +/// link_index - Zero-based index for the link. +/// rect_index - Zero-based index for a rectangle. +/// left - Pointer to a double value receiving the rectangle +/// left boundary. +/// top - Pointer to a double value receiving the rectangle +/// top boundary. +/// right - Pointer to a double value receiving the rectangle +/// right boundary. +/// bottom - Pointer to a double value receiving the rectangle +/// bottom boundary. +/// Return Value: +/// On success, return TRUE and fill in |left|, |top|, |right|, and +/// |bottom|. If |link_page| is invalid or if |link_index| does not +/// correspond to a valid link, then return FALSE, and the out +/// parameters remain unmodified. +int FPDFLink_GetRect(FPDF_PAGELINK link_page, +int link_index, +int rect_index, +ffi.Pointer left, +ffi.Pointer top, +ffi.Pointer right, +ffi.Pointer bottom, +) { + return _FPDFLink_GetRect(link_page, +link_index, +rect_index, +left, +top, +right, +bottom, +); +} + +late final _FPDFLink_GetRectPtr = _lookup< + ffi.NativeFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>>('FPDFLink_GetRect'); +late final _FPDFLink_GetRect = _FPDFLink_GetRectPtr.asFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>(); + +/// Experimental API. +/// Function: FPDFLink_GetTextRange +/// Fetch the start char index and char count for a link. +/// Parameters: +/// link_page - Handle returned by FPDFLink_LoadWebLinks. +/// link_index - Zero-based index for the link. +/// start_char_index - pointer to int receiving the start char index +/// char_count - pointer to int receiving the char count +/// Return Value: +/// On success, return TRUE and fill in |start_char_index| and +/// |char_count|. if |link_page| is invalid or if |link_index| does +/// not correspond to a valid link, then return FALSE and the out +/// parameters remain unmodified. +int FPDFLink_GetTextRange(FPDF_PAGELINK link_page, +int link_index, +ffi.Pointer start_char_index, +ffi.Pointer char_count, +) { + return _FPDFLink_GetTextRange(link_page, +link_index, +start_char_index, +char_count, +); +} + +late final _FPDFLink_GetTextRangePtr = _lookup< + ffi.NativeFunction , ffi.Pointer )>>('FPDFLink_GetTextRange'); +late final _FPDFLink_GetTextRange = _FPDFLink_GetTextRangePtr.asFunction , ffi.Pointer )>(); + +/// Function: FPDFLink_CloseWebLinks +/// Release resources used by weblink feature. +/// Parameters: +/// link_page - Handle returned by FPDFLink_LoadWebLinks. +/// Return Value: +/// None. +void FPDFLink_CloseWebLinks(FPDF_PAGELINK link_page, +) { + return _FPDFLink_CloseWebLinks(link_page, +); +} + +late final _FPDFLink_CloseWebLinksPtr = _lookup< + ffi.NativeFunction>('FPDFLink_CloseWebLinks'); +late final _FPDFLink_CloseWebLinks = _FPDFLink_CloseWebLinksPtr.asFunction(); + +/// Get the character index in |text_page| internal character list. +/// +/// text_page - a text page information structure. +/// nTextIndex - index of the text returned from FPDFText_GetText(). +/// +/// Returns the index of the character in internal character list. -1 for error. +int FPDFText_GetCharIndexFromTextIndex(FPDF_TEXTPAGE text_page, +int nTextIndex, +) { + return _FPDFText_GetCharIndexFromTextIndex(text_page, +nTextIndex, +); +} + +late final _FPDFText_GetCharIndexFromTextIndexPtr = _lookup< + ffi.NativeFunction>('FPDFText_GetCharIndexFromTextIndex'); +late final _FPDFText_GetCharIndexFromTextIndex = _FPDFText_GetCharIndexFromTextIndexPtr.asFunction(); + +/// Get the text index in |text_page| internal character list. +/// +/// text_page - a text page information structure. +/// nCharIndex - index of the character in internal character list. +/// +/// Returns the index of the text returned from FPDFText_GetText(). -1 for error. +int FPDFText_GetTextIndexFromCharIndex(FPDF_TEXTPAGE text_page, +int nCharIndex, +) { + return _FPDFText_GetTextIndexFromCharIndex(text_page, +nCharIndex, +); +} + +late final _FPDFText_GetTextIndexFromCharIndexPtr = _lookup< + ffi.NativeFunction>('FPDFText_GetTextIndexFromCharIndex'); +late final _FPDFText_GetTextIndexFromCharIndex = _FPDFText_GetTextIndexFromCharIndexPtr.asFunction(); + +/// Experimental API. +/// Function: FPDF_RenderPageBitmapWithColorScheme_Start +/// Start to render page contents to a device independent bitmap +/// progressively with a specified color scheme for the content. +/// Parameters: +/// bitmap - Handle to the device independent bitmap (as the +/// output buffer). Bitmap handle can be created by +/// FPDFBitmap_Create function. +/// page - Handle to the page as returned by FPDF_LoadPage +/// function. +/// start_x - Left pixel position of the display area in the +/// bitmap coordinate. +/// start_y - Top pixel position of the display area in the +/// bitmap coordinate. +/// size_x - Horizontal size (in pixels) for displaying the +/// page. +/// size_y - Vertical size (in pixels) for displaying the page. +/// rotate - Page orientation: 0 (normal), 1 (rotated 90 +/// degrees clockwise), 2 (rotated 180 degrees), +/// 3 (rotated 90 degrees counter-clockwise). +/// flags - 0 for normal display, or combination of flags +/// defined in fpdfview.h. With FPDF_ANNOT flag, it +/// renders all annotations that does not require +/// user-interaction, which are all annotations except +/// widget and popup annotations. +/// color_scheme - Color scheme to be used in rendering the |page|. +/// If null, this function will work similar to +/// FPDF_RenderPageBitmap_Start(). +/// pause - The IFSDK_PAUSE interface. A callback mechanism +/// allowing the page rendering process. +/// Return value: +/// Rendering Status. See flags for progressive process status for the +/// details. +int FPDF_RenderPageBitmapWithColorScheme_Start(FPDF_BITMAP bitmap, +FPDF_PAGE page, +int start_x, +int start_y, +int size_x, +int size_y, +int rotate, +int flags, +ffi.Pointer color_scheme, +ffi.Pointer pause, +) { + return _FPDF_RenderPageBitmapWithColorScheme_Start(bitmap, +page, +start_x, +start_y, +size_x, +size_y, +rotate, +flags, +color_scheme, +pause, +); +} + +late final _FPDF_RenderPageBitmapWithColorScheme_StartPtr = _lookup< + ffi.NativeFunction , ffi.Pointer )>>('FPDF_RenderPageBitmapWithColorScheme_Start'); +late final _FPDF_RenderPageBitmapWithColorScheme_Start = _FPDF_RenderPageBitmapWithColorScheme_StartPtr.asFunction , ffi.Pointer )>(); + +/// Function: FPDF_RenderPageBitmap_Start +/// Start to render page contents to a device independent bitmap +/// progressively. +/// Parameters: +/// bitmap - Handle to the device independent bitmap (as the +/// output buffer). Bitmap handle can be created by +/// FPDFBitmap_Create(). +/// page - Handle to the page, as returned by FPDF_LoadPage(). +/// start_x - Left pixel position of the display area in the +/// bitmap coordinates. +/// start_y - Top pixel position of the display area in the bitmap +/// coordinates. +/// size_x - Horizontal size (in pixels) for displaying the page. +/// size_y - Vertical size (in pixels) for displaying the page. +/// rotate - Page orientation: 0 (normal), 1 (rotated 90 degrees +/// clockwise), 2 (rotated 180 degrees), 3 (rotated 90 +/// degrees counter-clockwise). +/// flags - 0 for normal display, or combination of flags +/// defined in fpdfview.h. With FPDF_ANNOT flag, it +/// renders all annotations that does not require +/// user-interaction, which are all annotations except +/// widget and popup annotations. +/// pause - The IFSDK_PAUSE interface.A callback mechanism +/// allowing the page rendering process +/// Return value: +/// Rendering Status. See flags for progressive process status for the +/// details. +int FPDF_RenderPageBitmap_Start(FPDF_BITMAP bitmap, +FPDF_PAGE page, +int start_x, +int start_y, +int size_x, +int size_y, +int rotate, +int flags, +ffi.Pointer pause, +) { + return _FPDF_RenderPageBitmap_Start(bitmap, +page, +start_x, +start_y, +size_x, +size_y, +rotate, +flags, +pause, +); +} + +late final _FPDF_RenderPageBitmap_StartPtr = _lookup< + ffi.NativeFunction )>>('FPDF_RenderPageBitmap_Start'); +late final _FPDF_RenderPageBitmap_Start = _FPDF_RenderPageBitmap_StartPtr.asFunction )>(); + +/// Function: FPDF_RenderPage_Continue +/// Continue rendering a PDF page. +/// Parameters: +/// page - Handle to the page, as returned by FPDF_LoadPage(). +/// pause - The IFSDK_PAUSE interface (a callback mechanism +/// allowing the page rendering process to be paused +/// before it's finished). This can be NULL if you +/// don't want to pause. +/// Return value: +/// The rendering status. See flags for progressive process status for +/// the details. +int FPDF_RenderPage_Continue(FPDF_PAGE page, +ffi.Pointer pause, +) { + return _FPDF_RenderPage_Continue(page, +pause, +); +} + +late final _FPDF_RenderPage_ContinuePtr = _lookup< + ffi.NativeFunction )>>('FPDF_RenderPage_Continue'); +late final _FPDF_RenderPage_Continue = _FPDF_RenderPage_ContinuePtr.asFunction )>(); + +/// Function: FPDF_RenderPage_Close +/// Release the resource allocate during page rendering. Need to be +/// called after finishing rendering or +/// cancel the rendering. +/// Parameters: +/// page - Handle to the page, as returned by FPDF_LoadPage(). +/// Return value: +/// None. +void FPDF_RenderPage_Close(FPDF_PAGE page, +) { + return _FPDF_RenderPage_Close(page, +); +} + +late final _FPDF_RenderPage_ClosePtr = _lookup< + ffi.NativeFunction>('FPDF_RenderPage_Close'); +late final _FPDF_RenderPage_Close = _FPDF_RenderPage_ClosePtr.asFunction(); + +/// Create a new PDF document. +/// +/// Returns a handle to a new document, or NULL on failure. +FPDF_DOCUMENT FPDF_CreateNewDocument() { + return _FPDF_CreateNewDocument(); +} + +late final _FPDF_CreateNewDocumentPtr = _lookup< + ffi.NativeFunction>('FPDF_CreateNewDocument'); +late final _FPDF_CreateNewDocument = _FPDF_CreateNewDocumentPtr.asFunction(); + +/// Create a new PDF page. +/// +/// document - handle to document. +/// page_index - suggested 0-based index of the page to create. If it is larger +/// than document's current last index(L), the created page index +/// is the next available index -- L+1. +/// width - the page width in points. +/// height - the page height in points. +/// +/// Returns the handle to the new page or NULL on failure. +/// +/// The page should be closed with FPDF_ClosePage() when finished as +/// with any other page in the document. +FPDF_PAGE FPDFPage_New(FPDF_DOCUMENT document, +int page_index, +double width, +double height, +) { + return _FPDFPage_New(document, +page_index, +width, +height, +); +} + +late final _FPDFPage_NewPtr = _lookup< + ffi.NativeFunction>('FPDFPage_New'); +late final _FPDFPage_New = _FPDFPage_NewPtr.asFunction(); + +/// Delete the page at |page_index|. +/// +/// document - handle to document. +/// page_index - the index of the page to delete. +void FPDFPage_Delete(FPDF_DOCUMENT document, +int page_index, +) { + return _FPDFPage_Delete(document, +page_index, +); +} + +late final _FPDFPage_DeletePtr = _lookup< + ffi.NativeFunction>('FPDFPage_Delete'); +late final _FPDFPage_Delete = _FPDFPage_DeletePtr.asFunction(); + +/// Experimental API. +/// Move the given pages to a new index position. +/// +/// page_indices - the ordered list of pages to move. No duplicates allowed. +/// page_indices_len - the number of elements in |page_indices| +/// dest_page_index - the new index position to which the pages in +/// |page_indices| are moved. +/// +/// Returns TRUE on success. If it returns FALSE, the document may be left in an +/// indeterminate state. +/// +/// Example: The PDF document starts out with pages [A, B, C, D], with indices +/// [0, 1, 2, 3]. +/// +/// > Move(doc, [3, 2], 2, 1); // returns true +/// > // The document has pages [A, D, C, B]. +/// > +/// > Move(doc, [0, 4, 3], 3, 1); // returns false +/// > // Returned false because index 4 is out of range. +/// > +/// > Move(doc, [0, 3, 1], 3, 2); // returns false +/// > // Returned false because index 2 is out of range for 3 page indices. +/// > +/// > Move(doc, [2, 2], 2, 0); // returns false +/// > // Returned false because [2, 2] contains duplicates. +int FPDF_MovePages(FPDF_DOCUMENT document, +ffi.Pointer page_indices, +int page_indices_len, +int dest_page_index, +) { + return _FPDF_MovePages(document, +page_indices, +page_indices_len, +dest_page_index, +); +} + +late final _FPDF_MovePagesPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong , ffi.Int )>>('FPDF_MovePages'); +late final _FPDF_MovePages = _FPDF_MovePagesPtr.asFunction , int , int )>(); + +/// Get the rotation of |page|. +/// +/// page - handle to a page +/// +/// Returns one of the following indicating the page rotation: +/// 0 - No rotation. +/// 1 - Rotated 90 degrees clockwise. +/// 2 - Rotated 180 degrees clockwise. +/// 3 - Rotated 270 degrees clockwise. +int FPDFPage_GetRotation(FPDF_PAGE page, +) { + return _FPDFPage_GetRotation(page, +); +} + +late final _FPDFPage_GetRotationPtr = _lookup< + ffi.NativeFunction>('FPDFPage_GetRotation'); +late final _FPDFPage_GetRotation = _FPDFPage_GetRotationPtr.asFunction(); + +/// Set rotation for |page|. +/// +/// page - handle to a page. +/// rotate - the rotation value, one of: +/// 0 - No rotation. +/// 1 - Rotated 90 degrees clockwise. +/// 2 - Rotated 180 degrees clockwise. +/// 3 - Rotated 270 degrees clockwise. +void FPDFPage_SetRotation(FPDF_PAGE page, +int rotate, +) { + return _FPDFPage_SetRotation(page, +rotate, +); +} + +late final _FPDFPage_SetRotationPtr = _lookup< + ffi.NativeFunction>('FPDFPage_SetRotation'); +late final _FPDFPage_SetRotation = _FPDFPage_SetRotationPtr.asFunction(); + +/// Insert |page_object| into |page|. +/// +/// page - handle to a page +/// page_object - handle to a page object. The |page_object| will be +/// automatically freed. +void FPDFPage_InsertObject(FPDF_PAGE page, +FPDF_PAGEOBJECT page_object, +) { + return _FPDFPage_InsertObject(page, +page_object, +); +} + +late final _FPDFPage_InsertObjectPtr = _lookup< + ffi.NativeFunction>('FPDFPage_InsertObject'); +late final _FPDFPage_InsertObject = _FPDFPage_InsertObjectPtr.asFunction(); + +/// Insert |page_object| into |page| at the specified |index|. +/// +/// page - handle to a page +/// page_object - handle to a page object as previously obtained by +/// FPDFPageObj_CreateNew{Path|Rect}() or +/// FPDFPageObj_New{Text|Image}Obj(). Ownership of the object +/// is transferred back to PDFium. +/// index - the index position to insert the object at. If index equals +/// the current object count, the object will be appended to the +/// end. If index is greater than the object count, the function +/// will fail and return false. +/// +/// Returns true if successful. +int FPDFPage_InsertObjectAtIndex(FPDF_PAGE page, +FPDF_PAGEOBJECT page_object, +int index, +) { + return _FPDFPage_InsertObjectAtIndex(page, +page_object, +index, +); +} + +late final _FPDFPage_InsertObjectAtIndexPtr = _lookup< + ffi.NativeFunction>('FPDFPage_InsertObjectAtIndex'); +late final _FPDFPage_InsertObjectAtIndex = _FPDFPage_InsertObjectAtIndexPtr.asFunction(); + +/// Experimental API. +/// Remove |page_object| from |page|. +/// +/// page - handle to a page +/// page_object - handle to a page object to be removed. +/// +/// Returns TRUE on success. +/// +/// Ownership is transferred to the caller. Call FPDFPageObj_Destroy() to free +/// it. +/// Note that when removing a |page_object| of type FPDF_PAGEOBJ_TEXT, all +/// FPDF_TEXTPAGE handles for |page| are no longer valid. +int FPDFPage_RemoveObject(FPDF_PAGE page, +FPDF_PAGEOBJECT page_object, +) { + return _FPDFPage_RemoveObject(page, +page_object, +); +} + +late final _FPDFPage_RemoveObjectPtr = _lookup< + ffi.NativeFunction>('FPDFPage_RemoveObject'); +late final _FPDFPage_RemoveObject = _FPDFPage_RemoveObjectPtr.asFunction(); + +/// Get number of page objects inside |page|. +/// +/// page - handle to a page. +/// +/// Returns the number of objects in |page|. +int FPDFPage_CountObjects(FPDF_PAGE page, +) { + return _FPDFPage_CountObjects(page, +); +} + +late final _FPDFPage_CountObjectsPtr = _lookup< + ffi.NativeFunction>('FPDFPage_CountObjects'); +late final _FPDFPage_CountObjects = _FPDFPage_CountObjectsPtr.asFunction(); + +/// Get object in |page| at |index|. +/// +/// page - handle to a page. +/// index - the index of a page object. +/// +/// Returns the handle to the page object, or NULL on failed. +FPDF_PAGEOBJECT FPDFPage_GetObject(FPDF_PAGE page, +int index, +) { + return _FPDFPage_GetObject(page, +index, +); +} + +late final _FPDFPage_GetObjectPtr = _lookup< + ffi.NativeFunction>('FPDFPage_GetObject'); +late final _FPDFPage_GetObject = _FPDFPage_GetObjectPtr.asFunction(); + +/// Checks if |page| contains transparency. +/// +/// page - handle to a page. +/// +/// Returns TRUE if |page| contains transparency. +int FPDFPage_HasTransparency(FPDF_PAGE page, +) { + return _FPDFPage_HasTransparency(page, +); +} + +late final _FPDFPage_HasTransparencyPtr = _lookup< + ffi.NativeFunction>('FPDFPage_HasTransparency'); +late final _FPDFPage_HasTransparency = _FPDFPage_HasTransparencyPtr.asFunction(); + +/// Generate the content of |page|. +/// +/// page - handle to a page. +/// +/// Returns TRUE on success. +/// +/// Before you save the page to a file, or reload the page, you must call +/// |FPDFPage_GenerateContent| or any changes to |page| will be lost. +int FPDFPage_GenerateContent(FPDF_PAGE page, +) { + return _FPDFPage_GenerateContent(page, +); +} + +late final _FPDFPage_GenerateContentPtr = _lookup< + ffi.NativeFunction>('FPDFPage_GenerateContent'); +late final _FPDFPage_GenerateContent = _FPDFPage_GenerateContentPtr.asFunction(); + +/// Destroy |page_object| by releasing its resources. |page_object| must have +/// been created by FPDFPageObj_CreateNew{Path|Rect}() or +/// FPDFPageObj_New{Text|Image}Obj(). This function must be called on +/// newly-created objects if they are not added to a page through +/// FPDFPage_InsertObject() or to an annotation through FPDFAnnot_AppendObject(). +/// +/// page_object - handle to a page object. +void FPDFPageObj_Destroy(FPDF_PAGEOBJECT page_object, +) { + return _FPDFPageObj_Destroy(page_object, +); +} + +late final _FPDFPageObj_DestroyPtr = _lookup< + ffi.NativeFunction>('FPDFPageObj_Destroy'); +late final _FPDFPageObj_Destroy = _FPDFPageObj_DestroyPtr.asFunction(); + +/// Checks if |page_object| contains transparency. +/// +/// page_object - handle to a page object. +/// +/// Returns TRUE if |page_object| contains transparency. +int FPDFPageObj_HasTransparency(FPDF_PAGEOBJECT page_object, +) { + return _FPDFPageObj_HasTransparency(page_object, +); +} + +late final _FPDFPageObj_HasTransparencyPtr = _lookup< + ffi.NativeFunction>('FPDFPageObj_HasTransparency'); +late final _FPDFPageObj_HasTransparency = _FPDFPageObj_HasTransparencyPtr.asFunction(); + +/// Get type of |page_object|. +/// +/// page_object - handle to a page object. +/// +/// Returns one of the FPDF_PAGEOBJ_* values on success, FPDF_PAGEOBJ_UNKNOWN on +/// error. +int FPDFPageObj_GetType(FPDF_PAGEOBJECT page_object, +) { + return _FPDFPageObj_GetType(page_object, +); +} + +late final _FPDFPageObj_GetTypePtr = _lookup< + ffi.NativeFunction>('FPDFPageObj_GetType'); +late final _FPDFPageObj_GetType = _FPDFPageObj_GetTypePtr.asFunction(); + +/// Experimental API. +/// Gets active state for |page_object| within page. +/// +/// page_object - handle to a page object. +/// active - pointer to variable that will receive if the page object is +/// active. This is a required parameter. Not filled if FALSE +/// is returned. +/// +/// For page objects where |active| is filled with FALSE, the |page_object| is +/// treated as if it wasn't in the document even though it is still held +/// internally. +/// +/// Returns TRUE if the operation succeeded, FALSE if it failed. +int FPDFPageObj_GetIsActive(FPDF_PAGEOBJECT page_object, +ffi.Pointer active, +) { + return _FPDFPageObj_GetIsActive(page_object, +active, +); +} + +late final _FPDFPageObj_GetIsActivePtr = _lookup< + ffi.NativeFunction )>>('FPDFPageObj_GetIsActive'); +late final _FPDFPageObj_GetIsActive = _FPDFPageObj_GetIsActivePtr.asFunction )>(); + +/// Experimental API. +/// Sets if |page_object| is active within page. +/// +/// page_object - handle to a page object. +/// active - a boolean specifying if the object is active. +/// +/// Returns TRUE on success. +/// +/// Page objects all start in the active state by default, and remain in that +/// state unless this function is called. +/// +/// When |active| is false, this makes the |page_object| be treated as if it +/// wasn't in the document even though it is still held internally. +int FPDFPageObj_SetIsActive(FPDF_PAGEOBJECT page_object, +int active, +) { + return _FPDFPageObj_SetIsActive(page_object, +active, +); +} + +late final _FPDFPageObj_SetIsActivePtr = _lookup< + ffi.NativeFunction>('FPDFPageObj_SetIsActive'); +late final _FPDFPageObj_SetIsActive = _FPDFPageObj_SetIsActivePtr.asFunction(); + +/// Transform |page_object| by the given matrix. +/// +/// page_object - handle to a page object. +/// a - matrix value. +/// b - matrix value. +/// c - matrix value. +/// d - matrix value. +/// e - matrix value. +/// f - matrix value. +/// +/// The matrix is composed as: +/// |a c e| +/// |b d f| +/// and can be used to scale, rotate, shear and translate the |page_object|. +void FPDFPageObj_Transform(FPDF_PAGEOBJECT page_object, +double a, +double b, +double c, +double d, +double e, +double f, +) { + return _FPDFPageObj_Transform(page_object, +a, +b, +c, +d, +e, +f, +); +} + +late final _FPDFPageObj_TransformPtr = _lookup< + ffi.NativeFunction>('FPDFPageObj_Transform'); +late final _FPDFPageObj_Transform = _FPDFPageObj_TransformPtr.asFunction(); + +/// Experimental API. +/// Transform |page_object| by the given matrix. +/// +/// page_object - handle to a page object. +/// matrix - the transform matrix. +/// +/// Returns TRUE on success. +/// +/// This can be used to scale, rotate, shear and translate the |page_object|. +/// It is an improved version of FPDFPageObj_Transform() that does not do +/// unnecessary double to float conversions, and only uses 1 parameter for the +/// matrix. It also returns whether the operation succeeded or not. +int FPDFPageObj_TransformF(FPDF_PAGEOBJECT page_object, +ffi.Pointer matrix, +) { + return _FPDFPageObj_TransformF(page_object, +matrix, +); +} + +late final _FPDFPageObj_TransformFPtr = _lookup< + ffi.NativeFunction )>>('FPDFPageObj_TransformF'); +late final _FPDFPageObj_TransformF = _FPDFPageObj_TransformFPtr.asFunction )>(); + +/// Experimental API. +/// Get the transform matrix of a page object. +/// +/// page_object - handle to a page object. +/// matrix - pointer to struct to receive the matrix value. +/// +/// The matrix is composed as: +/// |a c e| +/// |b d f| +/// and used to scale, rotate, shear and translate the page object. +/// +/// For page objects outside form objects, the matrix values are relative to the +/// page that contains it. +/// For page objects inside form objects, the matrix values are relative to the +/// form that contains it. +/// +/// Returns TRUE on success. +int FPDFPageObj_GetMatrix(FPDF_PAGEOBJECT page_object, +ffi.Pointer matrix, +) { + return _FPDFPageObj_GetMatrix(page_object, +matrix, +); +} + +late final _FPDFPageObj_GetMatrixPtr = _lookup< + ffi.NativeFunction )>>('FPDFPageObj_GetMatrix'); +late final _FPDFPageObj_GetMatrix = _FPDFPageObj_GetMatrixPtr.asFunction )>(); + +/// Experimental API. +/// Set the transform matrix of a page object. +/// +/// page_object - handle to a page object. +/// matrix - pointer to struct with the matrix value. +/// +/// The matrix is composed as: +/// |a c e| +/// |b d f| +/// and can be used to scale, rotate, shear and translate the page object. +/// +/// Returns TRUE on success. +int FPDFPageObj_SetMatrix(FPDF_PAGEOBJECT page_object, +ffi.Pointer matrix, +) { + return _FPDFPageObj_SetMatrix(page_object, +matrix, +); +} + +late final _FPDFPageObj_SetMatrixPtr = _lookup< + ffi.NativeFunction )>>('FPDFPageObj_SetMatrix'); +late final _FPDFPageObj_SetMatrix = _FPDFPageObj_SetMatrixPtr.asFunction )>(); + +/// Transform all annotations in |page|. +/// +/// page - handle to a page. +/// a - matrix value. +/// b - matrix value. +/// c - matrix value. +/// d - matrix value. +/// e - matrix value. +/// f - matrix value. +/// +/// The matrix is composed as: +/// |a c e| +/// |b d f| +/// and can be used to scale, rotate, shear and translate the |page| annotations. +void FPDFPage_TransformAnnots(FPDF_PAGE page, +double a, +double b, +double c, +double d, +double e, +double f, +) { + return _FPDFPage_TransformAnnots(page, +a, +b, +c, +d, +e, +f, +); +} + +late final _FPDFPage_TransformAnnotsPtr = _lookup< + ffi.NativeFunction>('FPDFPage_TransformAnnots'); +late final _FPDFPage_TransformAnnots = _FPDFPage_TransformAnnotsPtr.asFunction(); + +/// Create a new image object. +/// +/// document - handle to a document. +/// +/// Returns a handle to a new image object. +FPDF_PAGEOBJECT FPDFPageObj_NewImageObj(FPDF_DOCUMENT document, +) { + return _FPDFPageObj_NewImageObj(document, +); +} + +late final _FPDFPageObj_NewImageObjPtr = _lookup< + ffi.NativeFunction>('FPDFPageObj_NewImageObj'); +late final _FPDFPageObj_NewImageObj = _FPDFPageObj_NewImageObjPtr.asFunction(); + +/// Experimental API. +/// Get the marked content ID for the object. +/// +/// page_object - handle to a page object. +/// +/// Returns the page object's marked content ID, or -1 on error. +int FPDFPageObj_GetMarkedContentID(FPDF_PAGEOBJECT page_object, +) { + return _FPDFPageObj_GetMarkedContentID(page_object, +); +} + +late final _FPDFPageObj_GetMarkedContentIDPtr = _lookup< + ffi.NativeFunction>('FPDFPageObj_GetMarkedContentID'); +late final _FPDFPageObj_GetMarkedContentID = _FPDFPageObj_GetMarkedContentIDPtr.asFunction(); + +/// Experimental API. +/// Get number of content marks in |page_object|. +/// +/// page_object - handle to a page object. +/// +/// Returns the number of content marks in |page_object|, or -1 in case of +/// failure. +int FPDFPageObj_CountMarks(FPDF_PAGEOBJECT page_object, +) { + return _FPDFPageObj_CountMarks(page_object, +); +} + +late final _FPDFPageObj_CountMarksPtr = _lookup< + ffi.NativeFunction>('FPDFPageObj_CountMarks'); +late final _FPDFPageObj_CountMarks = _FPDFPageObj_CountMarksPtr.asFunction(); + +/// Experimental API. +/// Get content mark in |page_object| at |index|. +/// +/// page_object - handle to a page object. +/// index - the index of a page object. +/// +/// Returns the handle to the content mark, or NULL on failure. The handle is +/// still owned by the library, and it should not be freed directly. It becomes +/// invalid if the page object is destroyed, either directly or indirectly by +/// unloading the page. +FPDF_PAGEOBJECTMARK FPDFPageObj_GetMark(FPDF_PAGEOBJECT page_object, +int index, +) { + return _FPDFPageObj_GetMark(page_object, +index, +); +} + +late final _FPDFPageObj_GetMarkPtr = _lookup< + ffi.NativeFunction>('FPDFPageObj_GetMark'); +late final _FPDFPageObj_GetMark = _FPDFPageObj_GetMarkPtr.asFunction(); + +/// Experimental API. +/// Add a new content mark to a |page_object|. +/// +/// page_object - handle to a page object. +/// name - the name (tag) of the mark. +/// +/// Returns the handle to the content mark, or NULL on failure. The handle is +/// still owned by the library, and it should not be freed directly. It becomes +/// invalid if the page object is destroyed, either directly or indirectly by +/// unloading the page. +FPDF_PAGEOBJECTMARK FPDFPageObj_AddMark(FPDF_PAGEOBJECT page_object, +FPDF_BYTESTRING name, +) { + return _FPDFPageObj_AddMark(page_object, +name, +); +} + +late final _FPDFPageObj_AddMarkPtr = _lookup< + ffi.NativeFunction>('FPDFPageObj_AddMark'); +late final _FPDFPageObj_AddMark = _FPDFPageObj_AddMarkPtr.asFunction(); + +/// Experimental API. +/// Removes a content |mark| from a |page_object|. +/// The mark handle will be invalid after the removal. +/// +/// page_object - handle to a page object. +/// mark - handle to a content mark in that object to remove. +/// +/// Returns TRUE if the operation succeeded, FALSE if it failed. +int FPDFPageObj_RemoveMark(FPDF_PAGEOBJECT page_object, +FPDF_PAGEOBJECTMARK mark, +) { + return _FPDFPageObj_RemoveMark(page_object, +mark, +); +} + +late final _FPDFPageObj_RemoveMarkPtr = _lookup< + ffi.NativeFunction>('FPDFPageObj_RemoveMark'); +late final _FPDFPageObj_RemoveMark = _FPDFPageObj_RemoveMarkPtr.asFunction(); + +/// Experimental API. +/// Get the name of a content mark. +/// +/// mark - handle to a content mark. +/// buffer - buffer for holding the returned name in UTF-16LE. This is only +/// modified if |buflen| is large enough to store the name. +/// Optional, pass null to just retrieve the size of the buffer +/// needed. +/// buflen - length of the buffer in bytes. +/// out_buflen - pointer to variable that will receive the minimum buffer size +/// in bytes to contain the name. This is a required parameter. +/// Not filled if FALSE is returned. +/// +/// Returns TRUE if the operation succeeded, FALSE if it failed. +int FPDFPageObjMark_GetName(FPDF_PAGEOBJECTMARK mark, +ffi.Pointer buffer, +int buflen, +ffi.Pointer out_buflen, +) { + return _FPDFPageObjMark_GetName(mark, +buffer, +buflen, +out_buflen, +); +} + +late final _FPDFPageObjMark_GetNamePtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong , ffi.Pointer )>>('FPDFPageObjMark_GetName'); +late final _FPDFPageObjMark_GetName = _FPDFPageObjMark_GetNamePtr.asFunction , int , ffi.Pointer )>(); + +/// Experimental API. +/// Get the number of key/value pair parameters in |mark|. +/// +/// mark - handle to a content mark. +/// +/// Returns the number of key/value pair parameters |mark|, or -1 in case of +/// failure. +int FPDFPageObjMark_CountParams(FPDF_PAGEOBJECTMARK mark, +) { + return _FPDFPageObjMark_CountParams(mark, +); +} + +late final _FPDFPageObjMark_CountParamsPtr = _lookup< + ffi.NativeFunction>('FPDFPageObjMark_CountParams'); +late final _FPDFPageObjMark_CountParams = _FPDFPageObjMark_CountParamsPtr.asFunction(); + +/// Experimental API. +/// Get the key of a property in a content mark. +/// +/// mark - handle to a content mark. +/// index - index of the property. +/// buffer - buffer for holding the returned key in UTF-16LE. This is only +/// modified if |buflen| is large enough to store the key. +/// Optional, pass null to just retrieve the size of the buffer +/// needed. +/// buflen - length of the buffer in bytes. +/// out_buflen - pointer to variable that will receive the minimum buffer size +/// in bytes to contain the name. This is a required parameter. +/// Not filled if FALSE is returned. +/// +/// Returns TRUE if the operation was successful, FALSE otherwise. +int FPDFPageObjMark_GetParamKey(FPDF_PAGEOBJECTMARK mark, +int index, +ffi.Pointer buffer, +int buflen, +ffi.Pointer out_buflen, +) { + return _FPDFPageObjMark_GetParamKey(mark, +index, +buffer, +buflen, +out_buflen, +); +} + +late final _FPDFPageObjMark_GetParamKeyPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong , ffi.Pointer )>>('FPDFPageObjMark_GetParamKey'); +late final _FPDFPageObjMark_GetParamKey = _FPDFPageObjMark_GetParamKeyPtr.asFunction , int , ffi.Pointer )>(); + +/// Experimental API. +/// Get the type of the value of a property in a content mark by key. +/// +/// mark - handle to a content mark. +/// key - string key of the property. +/// +/// Returns the type of the value, or FPDF_OBJECT_UNKNOWN in case of failure. +int FPDFPageObjMark_GetParamValueType(FPDF_PAGEOBJECTMARK mark, +FPDF_BYTESTRING key, +) { + return _FPDFPageObjMark_GetParamValueType(mark, +key, +); +} + +late final _FPDFPageObjMark_GetParamValueTypePtr = _lookup< + ffi.NativeFunction>('FPDFPageObjMark_GetParamValueType'); +late final _FPDFPageObjMark_GetParamValueType = _FPDFPageObjMark_GetParamValueTypePtr.asFunction(); + +/// Experimental API. +/// Get the value of a number property in a content mark by key as int. +/// FPDFPageObjMark_GetParamValueType() should have returned FPDF_OBJECT_NUMBER +/// for this property. +/// +/// mark - handle to a content mark. +/// key - string key of the property. +/// out_value - pointer to variable that will receive the value. Not filled if +/// false is returned. +/// +/// Returns TRUE if the key maps to a number value, FALSE otherwise. +int FPDFPageObjMark_GetParamIntValue(FPDF_PAGEOBJECTMARK mark, +FPDF_BYTESTRING key, +ffi.Pointer out_value, +) { + return _FPDFPageObjMark_GetParamIntValue(mark, +key, +out_value, +); +} + +late final _FPDFPageObjMark_GetParamIntValuePtr = _lookup< + ffi.NativeFunction )>>('FPDFPageObjMark_GetParamIntValue'); +late final _FPDFPageObjMark_GetParamIntValue = _FPDFPageObjMark_GetParamIntValuePtr.asFunction )>(); + +/// Experimental API. +/// Get the value of a number property in a content mark by key as float. +/// FPDFPageObjMark_GetParamValueType() should have returned FPDF_OBJECT_NUMBER +/// for this property. +/// +/// mark - handle to a content mark. +/// key - string key of the property. +/// out_value - pointer to variable that will receive the value. Not filled if +/// false is returned. +/// +/// Returns TRUE if the key maps to a number value, FALSE otherwise. +int FPDFPageObjMark_GetParamFloatValue(FPDF_PAGEOBJECTMARK mark, +FPDF_BYTESTRING key, +ffi.Pointer out_value, +) { + return _FPDFPageObjMark_GetParamFloatValue(mark, +key, +out_value, +); +} + +late final _FPDFPageObjMark_GetParamFloatValuePtr = _lookup< + ffi.NativeFunction )>>('FPDFPageObjMark_GetParamFloatValue'); +late final _FPDFPageObjMark_GetParamFloatValue = _FPDFPageObjMark_GetParamFloatValuePtr.asFunction )>(); + +/// Experimental API. +/// Get the value of a string property in a content mark by key. +/// +/// mark - handle to a content mark. +/// key - string key of the property. +/// buffer - buffer for holding the returned value in UTF-16LE. This is +/// only modified if |buflen| is large enough to store the value. +/// Optional, pass null to just retrieve the size of the buffer +/// needed. +/// buflen - length of the buffer in bytes. +/// out_buflen - pointer to variable that will receive the minimum buffer size +/// in bytes to contain the name. This is a required parameter. +/// Not filled if FALSE is returned. +/// +/// Returns TRUE if the key maps to a string/blob value, FALSE otherwise. +int FPDFPageObjMark_GetParamStringValue(FPDF_PAGEOBJECTMARK mark, +FPDF_BYTESTRING key, +ffi.Pointer buffer, +int buflen, +ffi.Pointer out_buflen, +) { + return _FPDFPageObjMark_GetParamStringValue(mark, +key, +buffer, +buflen, +out_buflen, +); +} + +late final _FPDFPageObjMark_GetParamStringValuePtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong , ffi.Pointer )>>('FPDFPageObjMark_GetParamStringValue'); +late final _FPDFPageObjMark_GetParamStringValue = _FPDFPageObjMark_GetParamStringValuePtr.asFunction , int , ffi.Pointer )>(); + +/// Experimental API. +/// Get the value of a blob property in a content mark by key. +/// +/// mark - handle to a content mark. +/// key - string key of the property. +/// buffer - buffer for holding the returned value. This is only modified +/// if |buflen| is large enough to store the value. +/// Optional, pass null to just retrieve the size of the buffer +/// needed. +/// buflen - length of the buffer in bytes. +/// out_buflen - pointer to variable that will receive the minimum buffer size +/// in bytes to contain the name. This is a required parameter. +/// Not filled if FALSE is returned. +/// +/// Returns TRUE if the key maps to a string/blob value, FALSE otherwise. +int FPDFPageObjMark_GetParamBlobValue(FPDF_PAGEOBJECTMARK mark, +FPDF_BYTESTRING key, +ffi.Pointer buffer, +int buflen, +ffi.Pointer out_buflen, +) { + return _FPDFPageObjMark_GetParamBlobValue(mark, +key, +buffer, +buflen, +out_buflen, +); +} + +late final _FPDFPageObjMark_GetParamBlobValuePtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong , ffi.Pointer )>>('FPDFPageObjMark_GetParamBlobValue'); +late final _FPDFPageObjMark_GetParamBlobValue = _FPDFPageObjMark_GetParamBlobValuePtr.asFunction , int , ffi.Pointer )>(); + +/// Experimental API. +/// Set the value of an int property in a content mark by key. If a parameter +/// with key |key| exists, its value is set to |value|. Otherwise, it is added as +/// a new parameter. +/// +/// document - handle to the document. +/// page_object - handle to the page object with the mark. +/// mark - handle to a content mark. +/// key - string key of the property. +/// value - int value to set. +/// +/// Returns TRUE if the operation succeeded, FALSE otherwise. +int FPDFPageObjMark_SetIntParam(FPDF_DOCUMENT document, +FPDF_PAGEOBJECT page_object, +FPDF_PAGEOBJECTMARK mark, +FPDF_BYTESTRING key, +int value, +) { + return _FPDFPageObjMark_SetIntParam(document, +page_object, +mark, +key, +value, +); +} + +late final _FPDFPageObjMark_SetIntParamPtr = _lookup< + ffi.NativeFunction>('FPDFPageObjMark_SetIntParam'); +late final _FPDFPageObjMark_SetIntParam = _FPDFPageObjMark_SetIntParamPtr.asFunction(); + +/// Experimental API. +/// Set the value of a float property in a content mark by key. If a parameter +/// with key |key| exists, its value is set to |value|. Otherwise, it is added as +/// a new parameter. +/// +/// document - handle to the document. +/// page_object - handle to the page object with the mark. +/// mark - handle to a content mark. +/// key - string key of the property. +/// value - float value to set. +/// +/// Returns TRUE if the operation succeeded, FALSE otherwise. +int FPDFPageObjMark_SetFloatParam(FPDF_DOCUMENT document, +FPDF_PAGEOBJECT page_object, +FPDF_PAGEOBJECTMARK mark, +FPDF_BYTESTRING key, +double value, +) { + return _FPDFPageObjMark_SetFloatParam(document, +page_object, +mark, +key, +value, +); +} + +late final _FPDFPageObjMark_SetFloatParamPtr = _lookup< + ffi.NativeFunction>('FPDFPageObjMark_SetFloatParam'); +late final _FPDFPageObjMark_SetFloatParam = _FPDFPageObjMark_SetFloatParamPtr.asFunction(); + +/// Experimental API. +/// Set the value of a string property in a content mark by key. If a parameter +/// with key |key| exists, its value is set to |value|. Otherwise, it is added as +/// a new parameter. +/// +/// document - handle to the document. +/// page_object - handle to the page object with the mark. +/// mark - handle to a content mark. +/// key - string key of the property. +/// value - string value to set. +/// +/// Returns TRUE if the operation succeeded, FALSE otherwise. +int FPDFPageObjMark_SetStringParam(FPDF_DOCUMENT document, +FPDF_PAGEOBJECT page_object, +FPDF_PAGEOBJECTMARK mark, +FPDF_BYTESTRING key, +FPDF_BYTESTRING value, +) { + return _FPDFPageObjMark_SetStringParam(document, +page_object, +mark, +key, +value, +); +} + +late final _FPDFPageObjMark_SetStringParamPtr = _lookup< + ffi.NativeFunction>('FPDFPageObjMark_SetStringParam'); +late final _FPDFPageObjMark_SetStringParam = _FPDFPageObjMark_SetStringParamPtr.asFunction(); + +/// Experimental API. +/// Set the value of a blob property in a content mark by key. If a parameter +/// with key |key| exists, its value is set to |value|. Otherwise, it is added as +/// a new parameter. +/// +/// document - handle to the document. +/// page_object - handle to the page object with the mark. +/// mark - handle to a content mark. +/// key - string key of the property. +/// value - pointer to blob value to set. +/// value_len - size in bytes of |value|. +/// +/// Returns TRUE if the operation succeeded, FALSE otherwise. +int FPDFPageObjMark_SetBlobParam(FPDF_DOCUMENT document, +FPDF_PAGEOBJECT page_object, +FPDF_PAGEOBJECTMARK mark, +FPDF_BYTESTRING key, +ffi.Pointer value, +int value_len, +) { + return _FPDFPageObjMark_SetBlobParam(document, +page_object, +mark, +key, +value, +value_len, +); +} + +late final _FPDFPageObjMark_SetBlobParamPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFPageObjMark_SetBlobParam'); +late final _FPDFPageObjMark_SetBlobParam = _FPDFPageObjMark_SetBlobParamPtr.asFunction , int )>(); + +/// Experimental API. +/// Removes a property from a content mark by key. +/// +/// page_object - handle to the page object with the mark. +/// mark - handle to a content mark. +/// key - string key of the property. +/// +/// Returns TRUE if the operation succeeded, FALSE otherwise. +int FPDFPageObjMark_RemoveParam(FPDF_PAGEOBJECT page_object, +FPDF_PAGEOBJECTMARK mark, +FPDF_BYTESTRING key, +) { + return _FPDFPageObjMark_RemoveParam(page_object, +mark, +key, +); +} + +late final _FPDFPageObjMark_RemoveParamPtr = _lookup< + ffi.NativeFunction>('FPDFPageObjMark_RemoveParam'); +late final _FPDFPageObjMark_RemoveParam = _FPDFPageObjMark_RemoveParamPtr.asFunction(); + +/// Load an image from a JPEG image file and then set it into |image_object|. +/// +/// pages - pointer to the start of all loaded pages, may be NULL. +/// count - number of |pages|, may be 0. +/// image_object - handle to an image object. +/// file_access - file access handler which specifies the JPEG image file. +/// +/// Returns TRUE on success. +/// +/// The image object might already have an associated image, which is shared and +/// cached by the loaded pages. In that case, we need to clear the cached image +/// for all the loaded pages. Pass |pages| and page count (|count|) to this API +/// to clear the image cache. If the image is not previously shared, or NULL is a +/// valid |pages| value. +int FPDFImageObj_LoadJpegFile(ffi.Pointer pages, +int count, +FPDF_PAGEOBJECT image_object, +ffi.Pointer file_access, +) { + return _FPDFImageObj_LoadJpegFile(pages, +count, +image_object, +file_access, +); +} + +late final _FPDFImageObj_LoadJpegFilePtr = _lookup< + ffi.NativeFunction , ffi.Int , FPDF_PAGEOBJECT , ffi.Pointer )>>('FPDFImageObj_LoadJpegFile'); +late final _FPDFImageObj_LoadJpegFile = _FPDFImageObj_LoadJpegFilePtr.asFunction , int , FPDF_PAGEOBJECT , ffi.Pointer )>(); + +/// Load an image from a JPEG image file and then set it into |image_object|. +/// +/// pages - pointer to the start of all loaded pages, may be NULL. +/// count - number of |pages|, may be 0. +/// image_object - handle to an image object. +/// file_access - file access handler which specifies the JPEG image file. +/// +/// Returns TRUE on success. +/// +/// The image object might already have an associated image, which is shared and +/// cached by the loaded pages. In that case, we need to clear the cached image +/// for all the loaded pages. Pass |pages| and page count (|count|) to this API +/// to clear the image cache. If the image is not previously shared, or NULL is a +/// valid |pages| value. This function loads the JPEG image inline, so the image +/// content is copied to the file. This allows |file_access| and its associated +/// data to be deleted after this function returns. +int FPDFImageObj_LoadJpegFileInline(ffi.Pointer pages, +int count, +FPDF_PAGEOBJECT image_object, +ffi.Pointer file_access, +) { + return _FPDFImageObj_LoadJpegFileInline(pages, +count, +image_object, +file_access, +); +} + +late final _FPDFImageObj_LoadJpegFileInlinePtr = _lookup< + ffi.NativeFunction , ffi.Int , FPDF_PAGEOBJECT , ffi.Pointer )>>('FPDFImageObj_LoadJpegFileInline'); +late final _FPDFImageObj_LoadJpegFileInline = _FPDFImageObj_LoadJpegFileInlinePtr.asFunction , int , FPDF_PAGEOBJECT , ffi.Pointer )>(); + +/// TODO(thestig): Start deprecating this once FPDFPageObj_SetMatrix() is stable. +/// +/// Set the transform matrix of |image_object|. +/// +/// image_object - handle to an image object. +/// a - matrix value. +/// b - matrix value. +/// c - matrix value. +/// d - matrix value. +/// e - matrix value. +/// f - matrix value. +/// +/// The matrix is composed as: +/// |a c e| +/// |b d f| +/// and can be used to scale, rotate, shear and translate the |image_object|. +/// +/// Returns TRUE on success. +int FPDFImageObj_SetMatrix(FPDF_PAGEOBJECT image_object, +double a, +double b, +double c, +double d, +double e, +double f, +) { + return _FPDFImageObj_SetMatrix(image_object, +a, +b, +c, +d, +e, +f, +); +} + +late final _FPDFImageObj_SetMatrixPtr = _lookup< + ffi.NativeFunction>('FPDFImageObj_SetMatrix'); +late final _FPDFImageObj_SetMatrix = _FPDFImageObj_SetMatrixPtr.asFunction(); + +/// Set |bitmap| to |image_object|. +/// +/// pages - pointer to the start of all loaded pages, may be NULL. +/// count - number of |pages|, may be 0. +/// image_object - handle to an image object. +/// bitmap - handle of the bitmap. +/// +/// Returns TRUE on success. +int FPDFImageObj_SetBitmap(ffi.Pointer pages, +int count, +FPDF_PAGEOBJECT image_object, +FPDF_BITMAP bitmap, +) { + return _FPDFImageObj_SetBitmap(pages, +count, +image_object, +bitmap, +); +} + +late final _FPDFImageObj_SetBitmapPtr = _lookup< + ffi.NativeFunction , ffi.Int , FPDF_PAGEOBJECT , FPDF_BITMAP )>>('FPDFImageObj_SetBitmap'); +late final _FPDFImageObj_SetBitmap = _FPDFImageObj_SetBitmapPtr.asFunction , int , FPDF_PAGEOBJECT , FPDF_BITMAP )>(); + +/// Get a bitmap rasterization of |image_object|. FPDFImageObj_GetBitmap() only +/// operates on |image_object| and does not take the associated image mask into +/// account. It also ignores the matrix for |image_object|. +/// The returned bitmap will be owned by the caller, and FPDFBitmap_Destroy() +/// must be called on the returned bitmap when it is no longer needed. +/// +/// image_object - handle to an image object. +/// +/// Returns the bitmap. +FPDF_BITMAP FPDFImageObj_GetBitmap(FPDF_PAGEOBJECT image_object, +) { + return _FPDFImageObj_GetBitmap(image_object, +); +} + +late final _FPDFImageObj_GetBitmapPtr = _lookup< + ffi.NativeFunction>('FPDFImageObj_GetBitmap'); +late final _FPDFImageObj_GetBitmap = _FPDFImageObj_GetBitmapPtr.asFunction(); + +/// Experimental API. +/// Get a bitmap rasterization of |image_object| that takes the image mask and +/// image matrix into account. To render correctly, the caller must provide the +/// |document| associated with |image_object|. If there is a |page| associated +/// with |image_object|, the caller should provide that as well. +/// The returned bitmap will be owned by the caller, and FPDFBitmap_Destroy() +/// must be called on the returned bitmap when it is no longer needed. +/// +/// document - handle to a document associated with |image_object|. +/// page - handle to an optional page associated with |image_object|. +/// image_object - handle to an image object. +/// +/// Returns the bitmap or NULL on failure. +FPDF_BITMAP FPDFImageObj_GetRenderedBitmap(FPDF_DOCUMENT document, +FPDF_PAGE page, +FPDF_PAGEOBJECT image_object, +) { + return _FPDFImageObj_GetRenderedBitmap(document, +page, +image_object, +); +} + +late final _FPDFImageObj_GetRenderedBitmapPtr = _lookup< + ffi.NativeFunction>('FPDFImageObj_GetRenderedBitmap'); +late final _FPDFImageObj_GetRenderedBitmap = _FPDFImageObj_GetRenderedBitmapPtr.asFunction(); + +/// Get the decoded image data of |image_object|. The decoded data is the +/// uncompressed image data, i.e. the raw image data after having all filters +/// applied. |buffer| is only modified if |buflen| is longer than the length of +/// the decoded image data. +/// +/// image_object - handle to an image object. +/// buffer - buffer for holding the decoded image data. +/// buflen - length of the buffer in bytes. +/// +/// Returns the length of the decoded image data. +int FPDFImageObj_GetImageDataDecoded(FPDF_PAGEOBJECT image_object, +ffi.Pointer buffer, +int buflen, +) { + return _FPDFImageObj_GetImageDataDecoded(image_object, +buffer, +buflen, +); +} + +late final _FPDFImageObj_GetImageDataDecodedPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFImageObj_GetImageDataDecoded'); +late final _FPDFImageObj_GetImageDataDecoded = _FPDFImageObj_GetImageDataDecodedPtr.asFunction , int )>(); + +/// Get the raw image data of |image_object|. The raw data is the image data as +/// stored in the PDF without applying any filters. |buffer| is only modified if +/// |buflen| is longer than the length of the raw image data. +/// +/// image_object - handle to an image object. +/// buffer - buffer for holding the raw image data. +/// buflen - length of the buffer in bytes. +/// +/// Returns the length of the raw image data. +int FPDFImageObj_GetImageDataRaw(FPDF_PAGEOBJECT image_object, +ffi.Pointer buffer, +int buflen, +) { + return _FPDFImageObj_GetImageDataRaw(image_object, +buffer, +buflen, +); +} + +late final _FPDFImageObj_GetImageDataRawPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFImageObj_GetImageDataRaw'); +late final _FPDFImageObj_GetImageDataRaw = _FPDFImageObj_GetImageDataRawPtr.asFunction , int )>(); + +/// Get the number of filters (i.e. decoders) of the image in |image_object|. +/// +/// image_object - handle to an image object. +/// +/// Returns the number of |image_object|'s filters. +int FPDFImageObj_GetImageFilterCount(FPDF_PAGEOBJECT image_object, +) { + return _FPDFImageObj_GetImageFilterCount(image_object, +); +} + +late final _FPDFImageObj_GetImageFilterCountPtr = _lookup< + ffi.NativeFunction>('FPDFImageObj_GetImageFilterCount'); +late final _FPDFImageObj_GetImageFilterCount = _FPDFImageObj_GetImageFilterCountPtr.asFunction(); + +/// Get the filter at |index| of |image_object|'s list of filters. Note that the +/// filters need to be applied in order, i.e. the first filter should be applied +/// first, then the second, etc. |buffer| is only modified if |buflen| is longer +/// than the length of the filter string. +/// +/// image_object - handle to an image object. +/// index - the index of the filter requested. +/// buffer - buffer for holding filter string, encoded in UTF-8. +/// buflen - length of the buffer. +/// +/// Returns the length of the filter string. +int FPDFImageObj_GetImageFilter(FPDF_PAGEOBJECT image_object, +int index, +ffi.Pointer buffer, +int buflen, +) { + return _FPDFImageObj_GetImageFilter(image_object, +index, +buffer, +buflen, +); +} + +late final _FPDFImageObj_GetImageFilterPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFImageObj_GetImageFilter'); +late final _FPDFImageObj_GetImageFilter = _FPDFImageObj_GetImageFilterPtr.asFunction , int )>(); + +/// Get the image metadata of |image_object|, including dimension, DPI, bits per +/// pixel, and colorspace. If the |image_object| is not an image object or if it +/// does not have an image, then the return value will be false. Otherwise, +/// failure to retrieve any specific parameter would result in its value being 0. +/// +/// image_object - handle to an image object. +/// page - handle to the page that |image_object| is on. Required for +/// retrieving the image's bits per pixel and colorspace. +/// metadata - receives the image metadata; must not be NULL. +/// +/// Returns true if successful. +int FPDFImageObj_GetImageMetadata(FPDF_PAGEOBJECT image_object, +FPDF_PAGE page, +ffi.Pointer metadata, +) { + return _FPDFImageObj_GetImageMetadata(image_object, +page, +metadata, +); +} + +late final _FPDFImageObj_GetImageMetadataPtr = _lookup< + ffi.NativeFunction )>>('FPDFImageObj_GetImageMetadata'); +late final _FPDFImageObj_GetImageMetadata = _FPDFImageObj_GetImageMetadataPtr.asFunction )>(); + +/// Experimental API. +/// Get the image size in pixels. Faster method to get only image size. +/// +/// image_object - handle to an image object. +/// width - receives the image width in pixels; must not be NULL. +/// height - receives the image height in pixels; must not be NULL. +/// +/// Returns true if successful. +int FPDFImageObj_GetImagePixelSize(FPDF_PAGEOBJECT image_object, +ffi.Pointer width, +ffi.Pointer height, +) { + return _FPDFImageObj_GetImagePixelSize(image_object, +width, +height, +); +} + +late final _FPDFImageObj_GetImagePixelSizePtr = _lookup< + ffi.NativeFunction , ffi.Pointer )>>('FPDFImageObj_GetImagePixelSize'); +late final _FPDFImageObj_GetImagePixelSize = _FPDFImageObj_GetImagePixelSizePtr.asFunction , ffi.Pointer )>(); + +/// Experimental API. +/// Get ICC profile decoded data of |image_object|. If the |image_object| is not +/// an image object or if it does not have an image, then the return value will +/// be false. It also returns false if the |image_object| has no ICC profile. +/// |buffer| is only modified if ICC profile exists and |buflen| is longer than +/// the length of the ICC profile decoded data. +/// +/// image_object - handle to an image object; must not be NULL. +/// page - handle to the page containing |image_object|; must not be +/// NULL. Required for retrieving the image's colorspace. +/// buffer - Buffer to receive ICC profile data; may be NULL if querying +/// required size via |out_buflen|. +/// buflen - Length of the buffer in bytes. Ignored if |buffer| is NULL. +/// out_buflen - Pointer to receive the ICC profile data size in bytes; must +/// not be NULL. Will be set if this API returns true. +/// +/// Returns true if |out_buflen| is not null and an ICC profile exists for the +/// given |image_object|. +int FPDFImageObj_GetIccProfileDataDecoded(FPDF_PAGEOBJECT image_object, +FPDF_PAGE page, +ffi.Pointer buffer, +int buflen, +ffi.Pointer out_buflen, +) { + return _FPDFImageObj_GetIccProfileDataDecoded(image_object, +page, +buffer, +buflen, +out_buflen, +); +} + +late final _FPDFImageObj_GetIccProfileDataDecodedPtr = _lookup< + ffi.NativeFunction , ffi.Size , ffi.Pointer )>>('FPDFImageObj_GetIccProfileDataDecoded'); +late final _FPDFImageObj_GetIccProfileDataDecoded = _FPDFImageObj_GetIccProfileDataDecodedPtr.asFunction , int , ffi.Pointer )>(); + +/// Create a new path object at an initial position. +/// +/// x - initial horizontal position. +/// y - initial vertical position. +/// +/// Returns a handle to a new path object. +FPDF_PAGEOBJECT FPDFPageObj_CreateNewPath(double x, +double y, +) { + return _FPDFPageObj_CreateNewPath(x, +y, +); +} + +late final _FPDFPageObj_CreateNewPathPtr = _lookup< + ffi.NativeFunction>('FPDFPageObj_CreateNewPath'); +late final _FPDFPageObj_CreateNewPath = _FPDFPageObj_CreateNewPathPtr.asFunction(); + +/// Create a closed path consisting of a rectangle. +/// +/// x - horizontal position for the left boundary of the rectangle. +/// y - vertical position for the bottom boundary of the rectangle. +/// w - width of the rectangle. +/// h - height of the rectangle. +/// +/// Returns a handle to the new path object. +FPDF_PAGEOBJECT FPDFPageObj_CreateNewRect(double x, +double y, +double w, +double h, +) { + return _FPDFPageObj_CreateNewRect(x, +y, +w, +h, +); +} + +late final _FPDFPageObj_CreateNewRectPtr = _lookup< + ffi.NativeFunction>('FPDFPageObj_CreateNewRect'); +late final _FPDFPageObj_CreateNewRect = _FPDFPageObj_CreateNewRectPtr.asFunction(); + +/// Get the bounding box of |page_object|. +/// +/// page_object - handle to a page object. +/// left - pointer where the left coordinate will be stored +/// bottom - pointer where the bottom coordinate will be stored +/// right - pointer where the right coordinate will be stored +/// top - pointer where the top coordinate will be stored +/// +/// On success, returns TRUE and fills in the 4 coordinates. +int FPDFPageObj_GetBounds(FPDF_PAGEOBJECT page_object, +ffi.Pointer left, +ffi.Pointer bottom, +ffi.Pointer right, +ffi.Pointer top, +) { + return _FPDFPageObj_GetBounds(page_object, +left, +bottom, +right, +top, +); +} + +late final _FPDFPageObj_GetBoundsPtr = _lookup< + ffi.NativeFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>>('FPDFPageObj_GetBounds'); +late final _FPDFPageObj_GetBounds = _FPDFPageObj_GetBoundsPtr.asFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>(); + +/// Experimental API. +/// Get the quad points that bounds |page_object|. +/// +/// page_object - handle to a page object. +/// quad_points - pointer where the quadrilateral points will be stored. +/// +/// On success, returns TRUE and fills in |quad_points|. +/// +/// Similar to FPDFPageObj_GetBounds(), this returns the bounds of a page +/// object. When the object is rotated by a non-multiple of 90 degrees, this API +/// returns a tighter bound that cannot be represented with just the 4 sides of +/// a rectangle. +/// +/// Currently only works the following |page_object| types: FPDF_PAGEOBJ_TEXT and +/// FPDF_PAGEOBJ_IMAGE. +int FPDFPageObj_GetRotatedBounds(FPDF_PAGEOBJECT page_object, +ffi.Pointer quad_points, +) { + return _FPDFPageObj_GetRotatedBounds(page_object, +quad_points, +); +} + +late final _FPDFPageObj_GetRotatedBoundsPtr = _lookup< + ffi.NativeFunction )>>('FPDFPageObj_GetRotatedBounds'); +late final _FPDFPageObj_GetRotatedBounds = _FPDFPageObj_GetRotatedBoundsPtr.asFunction )>(); + +/// Set the blend mode of |page_object|. +/// +/// page_object - handle to a page object. +/// blend_mode - string containing the blend mode. +/// +/// Blend mode can be one of following: Color, ColorBurn, ColorDodge, Darken, +/// Difference, Exclusion, HardLight, Hue, Lighten, Luminosity, Multiply, Normal, +/// Overlay, Saturation, Screen, SoftLight +void FPDFPageObj_SetBlendMode(FPDF_PAGEOBJECT page_object, +FPDF_BYTESTRING blend_mode, +) { + return _FPDFPageObj_SetBlendMode(page_object, +blend_mode, +); +} + +late final _FPDFPageObj_SetBlendModePtr = _lookup< + ffi.NativeFunction>('FPDFPageObj_SetBlendMode'); +late final _FPDFPageObj_SetBlendMode = _FPDFPageObj_SetBlendModePtr.asFunction(); + +/// Set the stroke RGBA of a page object. Range of values: 0 - 255. +/// +/// page_object - the handle to the page object. +/// R - the red component for the object's stroke color. +/// G - the green component for the object's stroke color. +/// B - the blue component for the object's stroke color. +/// A - the stroke alpha for the object. +/// +/// Returns TRUE on success. +int FPDFPageObj_SetStrokeColor(FPDF_PAGEOBJECT page_object, +int R, +int G, +int B, +int A, +) { + return _FPDFPageObj_SetStrokeColor(page_object, +R, +G, +B, +A, +); +} + +late final _FPDFPageObj_SetStrokeColorPtr = _lookup< + ffi.NativeFunction>('FPDFPageObj_SetStrokeColor'); +late final _FPDFPageObj_SetStrokeColor = _FPDFPageObj_SetStrokeColorPtr.asFunction(); + +/// Get the stroke RGBA of a page object. Range of values: 0 - 255. +/// +/// page_object - the handle to the page object. +/// R - the red component of the path stroke color. +/// G - the green component of the object's stroke color. +/// B - the blue component of the object's stroke color. +/// A - the stroke alpha of the object. +/// +/// Returns TRUE on success. +int FPDFPageObj_GetStrokeColor(FPDF_PAGEOBJECT page_object, +ffi.Pointer R, +ffi.Pointer G, +ffi.Pointer B, +ffi.Pointer A, +) { + return _FPDFPageObj_GetStrokeColor(page_object, +R, +G, +B, +A, +); +} + +late final _FPDFPageObj_GetStrokeColorPtr = _lookup< + ffi.NativeFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>>('FPDFPageObj_GetStrokeColor'); +late final _FPDFPageObj_GetStrokeColor = _FPDFPageObj_GetStrokeColorPtr.asFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>(); + +/// Set the stroke width of a page object. +/// +/// path - the handle to the page object. +/// width - the width of the stroke. +/// +/// Returns TRUE on success +int FPDFPageObj_SetStrokeWidth(FPDF_PAGEOBJECT page_object, +double width, +) { + return _FPDFPageObj_SetStrokeWidth(page_object, +width, +); +} + +late final _FPDFPageObj_SetStrokeWidthPtr = _lookup< + ffi.NativeFunction>('FPDFPageObj_SetStrokeWidth'); +late final _FPDFPageObj_SetStrokeWidth = _FPDFPageObj_SetStrokeWidthPtr.asFunction(); + +/// Get the stroke width of a page object. +/// +/// path - the handle to the page object. +/// width - the width of the stroke. +/// +/// Returns TRUE on success +int FPDFPageObj_GetStrokeWidth(FPDF_PAGEOBJECT page_object, +ffi.Pointer width, +) { + return _FPDFPageObj_GetStrokeWidth(page_object, +width, +); +} + +late final _FPDFPageObj_GetStrokeWidthPtr = _lookup< + ffi.NativeFunction )>>('FPDFPageObj_GetStrokeWidth'); +late final _FPDFPageObj_GetStrokeWidth = _FPDFPageObj_GetStrokeWidthPtr.asFunction )>(); + +/// Get the line join of |page_object|. +/// +/// page_object - handle to a page object. +/// +/// Returns the line join, or -1 on failure. +/// Line join can be one of following: FPDF_LINEJOIN_MITER, FPDF_LINEJOIN_ROUND, +/// FPDF_LINEJOIN_BEVEL +int FPDFPageObj_GetLineJoin(FPDF_PAGEOBJECT page_object, +) { + return _FPDFPageObj_GetLineJoin(page_object, +); +} + +late final _FPDFPageObj_GetLineJoinPtr = _lookup< + ffi.NativeFunction>('FPDFPageObj_GetLineJoin'); +late final _FPDFPageObj_GetLineJoin = _FPDFPageObj_GetLineJoinPtr.asFunction(); + +/// Set the line join of |page_object|. +/// +/// page_object - handle to a page object. +/// line_join - line join +/// +/// Line join can be one of following: FPDF_LINEJOIN_MITER, FPDF_LINEJOIN_ROUND, +/// FPDF_LINEJOIN_BEVEL +int FPDFPageObj_SetLineJoin(FPDF_PAGEOBJECT page_object, +int line_join, +) { + return _FPDFPageObj_SetLineJoin(page_object, +line_join, +); +} + +late final _FPDFPageObj_SetLineJoinPtr = _lookup< + ffi.NativeFunction>('FPDFPageObj_SetLineJoin'); +late final _FPDFPageObj_SetLineJoin = _FPDFPageObj_SetLineJoinPtr.asFunction(); + +/// Get the line cap of |page_object|. +/// +/// page_object - handle to a page object. +/// +/// Returns the line cap, or -1 on failure. +/// Line cap can be one of following: FPDF_LINECAP_BUTT, FPDF_LINECAP_ROUND, +/// FPDF_LINECAP_PROJECTING_SQUARE +int FPDFPageObj_GetLineCap(FPDF_PAGEOBJECT page_object, +) { + return _FPDFPageObj_GetLineCap(page_object, +); +} + +late final _FPDFPageObj_GetLineCapPtr = _lookup< + ffi.NativeFunction>('FPDFPageObj_GetLineCap'); +late final _FPDFPageObj_GetLineCap = _FPDFPageObj_GetLineCapPtr.asFunction(); + +/// Set the line cap of |page_object|. +/// +/// page_object - handle to a page object. +/// line_cap - line cap +/// +/// Line cap can be one of following: FPDF_LINECAP_BUTT, FPDF_LINECAP_ROUND, +/// FPDF_LINECAP_PROJECTING_SQUARE +int FPDFPageObj_SetLineCap(FPDF_PAGEOBJECT page_object, +int line_cap, +) { + return _FPDFPageObj_SetLineCap(page_object, +line_cap, +); +} + +late final _FPDFPageObj_SetLineCapPtr = _lookup< + ffi.NativeFunction>('FPDFPageObj_SetLineCap'); +late final _FPDFPageObj_SetLineCap = _FPDFPageObj_SetLineCapPtr.asFunction(); + +/// Set the fill RGBA of a page object. Range of values: 0 - 255. +/// +/// page_object - the handle to the page object. +/// R - the red component for the object's fill color. +/// G - the green component for the object's fill color. +/// B - the blue component for the object's fill color. +/// A - the fill alpha for the object. +/// +/// Returns TRUE on success. +int FPDFPageObj_SetFillColor(FPDF_PAGEOBJECT page_object, +int R, +int G, +int B, +int A, +) { + return _FPDFPageObj_SetFillColor(page_object, +R, +G, +B, +A, +); +} + +late final _FPDFPageObj_SetFillColorPtr = _lookup< + ffi.NativeFunction>('FPDFPageObj_SetFillColor'); +late final _FPDFPageObj_SetFillColor = _FPDFPageObj_SetFillColorPtr.asFunction(); + +/// Get the fill RGBA of a page object. Range of values: 0 - 255. +/// +/// page_object - the handle to the page object. +/// R - the red component of the object's fill color. +/// G - the green component of the object's fill color. +/// B - the blue component of the object's fill color. +/// A - the fill alpha of the object. +/// +/// Returns TRUE on success. +int FPDFPageObj_GetFillColor(FPDF_PAGEOBJECT page_object, +ffi.Pointer R, +ffi.Pointer G, +ffi.Pointer B, +ffi.Pointer A, +) { + return _FPDFPageObj_GetFillColor(page_object, +R, +G, +B, +A, +); +} + +late final _FPDFPageObj_GetFillColorPtr = _lookup< + ffi.NativeFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>>('FPDFPageObj_GetFillColor'); +late final _FPDFPageObj_GetFillColor = _FPDFPageObj_GetFillColorPtr.asFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>(); + +/// Experimental API. +/// Get the line dash |phase| of |page_object|. +/// +/// page_object - handle to a page object. +/// phase - pointer where the dashing phase will be stored. +/// +/// Returns TRUE on success. +int FPDFPageObj_GetDashPhase(FPDF_PAGEOBJECT page_object, +ffi.Pointer phase, +) { + return _FPDFPageObj_GetDashPhase(page_object, +phase, +); +} + +late final _FPDFPageObj_GetDashPhasePtr = _lookup< + ffi.NativeFunction )>>('FPDFPageObj_GetDashPhase'); +late final _FPDFPageObj_GetDashPhase = _FPDFPageObj_GetDashPhasePtr.asFunction )>(); + +/// Experimental API. +/// Set the line dash phase of |page_object|. +/// +/// page_object - handle to a page object. +/// phase - line dash phase. +/// +/// Returns TRUE on success. +int FPDFPageObj_SetDashPhase(FPDF_PAGEOBJECT page_object, +double phase, +) { + return _FPDFPageObj_SetDashPhase(page_object, +phase, +); +} + +late final _FPDFPageObj_SetDashPhasePtr = _lookup< + ffi.NativeFunction>('FPDFPageObj_SetDashPhase'); +late final _FPDFPageObj_SetDashPhase = _FPDFPageObj_SetDashPhasePtr.asFunction(); + +/// Experimental API. +/// Get the line dash array of |page_object|. +/// +/// page_object - handle to a page object. +/// +/// Returns the line dash array size or -1 on failure. +int FPDFPageObj_GetDashCount(FPDF_PAGEOBJECT page_object, +) { + return _FPDFPageObj_GetDashCount(page_object, +); +} + +late final _FPDFPageObj_GetDashCountPtr = _lookup< + ffi.NativeFunction>('FPDFPageObj_GetDashCount'); +late final _FPDFPageObj_GetDashCount = _FPDFPageObj_GetDashCountPtr.asFunction(); + +/// Experimental API. +/// Get the line dash array of |page_object|. +/// +/// page_object - handle to a page object. +/// dash_array - pointer where the dashing array will be stored. +/// dash_count - number of elements in |dash_array|. +/// +/// Returns TRUE on success. +int FPDFPageObj_GetDashArray(FPDF_PAGEOBJECT page_object, +ffi.Pointer dash_array, +int dash_count, +) { + return _FPDFPageObj_GetDashArray(page_object, +dash_array, +dash_count, +); +} + +late final _FPDFPageObj_GetDashArrayPtr = _lookup< + ffi.NativeFunction , ffi.Size )>>('FPDFPageObj_GetDashArray'); +late final _FPDFPageObj_GetDashArray = _FPDFPageObj_GetDashArrayPtr.asFunction , int )>(); + +/// Experimental API. +/// Set the line dash array of |page_object|. +/// +/// page_object - handle to a page object. +/// dash_array - the dash array. +/// dash_count - number of elements in |dash_array|. +/// phase - the line dash phase. +/// +/// Returns TRUE on success. +int FPDFPageObj_SetDashArray(FPDF_PAGEOBJECT page_object, +ffi.Pointer dash_array, +int dash_count, +double phase, +) { + return _FPDFPageObj_SetDashArray(page_object, +dash_array, +dash_count, +phase, +); +} + +late final _FPDFPageObj_SetDashArrayPtr = _lookup< + ffi.NativeFunction , ffi.Size , ffi.Float )>>('FPDFPageObj_SetDashArray'); +late final _FPDFPageObj_SetDashArray = _FPDFPageObj_SetDashArrayPtr.asFunction , int , double )>(); + +/// Get number of segments inside |path|. +/// +/// path - handle to a path. +/// +/// A segment is a command, created by e.g. FPDFPath_MoveTo(), +/// FPDFPath_LineTo() or FPDFPath_BezierTo(). +/// +/// Returns the number of objects in |path| or -1 on failure. +int FPDFPath_CountSegments(FPDF_PAGEOBJECT path, +) { + return _FPDFPath_CountSegments(path, +); +} + +late final _FPDFPath_CountSegmentsPtr = _lookup< + ffi.NativeFunction>('FPDFPath_CountSegments'); +late final _FPDFPath_CountSegments = _FPDFPath_CountSegmentsPtr.asFunction(); + +/// Get segment in |path| at |index|. +/// +/// path - handle to a path. +/// index - the index of a segment. +/// +/// Returns the handle to the segment, or NULL on faiure. +FPDF_PATHSEGMENT FPDFPath_GetPathSegment(FPDF_PAGEOBJECT path, +int index, +) { + return _FPDFPath_GetPathSegment(path, +index, +); +} + +late final _FPDFPath_GetPathSegmentPtr = _lookup< + ffi.NativeFunction>('FPDFPath_GetPathSegment'); +late final _FPDFPath_GetPathSegment = _FPDFPath_GetPathSegmentPtr.asFunction(); + +/// Get coordinates of |segment|. +/// +/// segment - handle to a segment. +/// x - the horizontal position of the segment. +/// y - the vertical position of the segment. +/// +/// Returns TRUE on success, otherwise |x| and |y| is not set. +int FPDFPathSegment_GetPoint(FPDF_PATHSEGMENT segment, +ffi.Pointer x, +ffi.Pointer y, +) { + return _FPDFPathSegment_GetPoint(segment, +x, +y, +); +} + +late final _FPDFPathSegment_GetPointPtr = _lookup< + ffi.NativeFunction , ffi.Pointer )>>('FPDFPathSegment_GetPoint'); +late final _FPDFPathSegment_GetPoint = _FPDFPathSegment_GetPointPtr.asFunction , ffi.Pointer )>(); + +/// Get type of |segment|. +/// +/// segment - handle to a segment. +/// +/// Returns one of the FPDF_SEGMENT_* values on success, +/// FPDF_SEGMENT_UNKNOWN on error. +int FPDFPathSegment_GetType(FPDF_PATHSEGMENT segment, +) { + return _FPDFPathSegment_GetType(segment, +); +} + +late final _FPDFPathSegment_GetTypePtr = _lookup< + ffi.NativeFunction>('FPDFPathSegment_GetType'); +late final _FPDFPathSegment_GetType = _FPDFPathSegment_GetTypePtr.asFunction(); + +/// Gets if the |segment| closes the current subpath of a given path. +/// +/// segment - handle to a segment. +/// +/// Returns close flag for non-NULL segment, FALSE otherwise. +int FPDFPathSegment_GetClose(FPDF_PATHSEGMENT segment, +) { + return _FPDFPathSegment_GetClose(segment, +); +} + +late final _FPDFPathSegment_GetClosePtr = _lookup< + ffi.NativeFunction>('FPDFPathSegment_GetClose'); +late final _FPDFPathSegment_GetClose = _FPDFPathSegment_GetClosePtr.asFunction(); + +/// Move a path's current point. +/// +/// path - the handle to the path object. +/// x - the horizontal position of the new current point. +/// y - the vertical position of the new current point. +/// +/// Note that no line will be created between the previous current point and the +/// new one. +/// +/// Returns TRUE on success +int FPDFPath_MoveTo(FPDF_PAGEOBJECT path, +double x, +double y, +) { + return _FPDFPath_MoveTo(path, +x, +y, +); +} + +late final _FPDFPath_MoveToPtr = _lookup< + ffi.NativeFunction>('FPDFPath_MoveTo'); +late final _FPDFPath_MoveTo = _FPDFPath_MoveToPtr.asFunction(); + +/// Add a line between the current point and a new point in the path. +/// +/// path - the handle to the path object. +/// x - the horizontal position of the new point. +/// y - the vertical position of the new point. +/// +/// The path's current point is changed to (x, y). +/// +/// Returns TRUE on success +int FPDFPath_LineTo(FPDF_PAGEOBJECT path, +double x, +double y, +) { + return _FPDFPath_LineTo(path, +x, +y, +); +} + +late final _FPDFPath_LineToPtr = _lookup< + ffi.NativeFunction>('FPDFPath_LineTo'); +late final _FPDFPath_LineTo = _FPDFPath_LineToPtr.asFunction(); + +/// Add a cubic Bezier curve to the given path, starting at the current point. +/// +/// path - the handle to the path object. +/// x1 - the horizontal position of the first Bezier control point. +/// y1 - the vertical position of the first Bezier control point. +/// x2 - the horizontal position of the second Bezier control point. +/// y2 - the vertical position of the second Bezier control point. +/// x3 - the horizontal position of the ending point of the Bezier curve. +/// y3 - the vertical position of the ending point of the Bezier curve. +/// +/// Returns TRUE on success +int FPDFPath_BezierTo(FPDF_PAGEOBJECT path, +double x1, +double y1, +double x2, +double y2, +double x3, +double y3, +) { + return _FPDFPath_BezierTo(path, +x1, +y1, +x2, +y2, +x3, +y3, +); +} + +late final _FPDFPath_BezierToPtr = _lookup< + ffi.NativeFunction>('FPDFPath_BezierTo'); +late final _FPDFPath_BezierTo = _FPDFPath_BezierToPtr.asFunction(); + +/// Close the current subpath of a given path. +/// +/// path - the handle to the path object. +/// +/// This will add a line between the current point and the initial point of the +/// subpath, thus terminating the current subpath. +/// +/// Returns TRUE on success +int FPDFPath_Close(FPDF_PAGEOBJECT path, +) { + return _FPDFPath_Close(path, +); +} + +late final _FPDFPath_ClosePtr = _lookup< + ffi.NativeFunction>('FPDFPath_Close'); +late final _FPDFPath_Close = _FPDFPath_ClosePtr.asFunction(); + +/// Set the drawing mode of a path. +/// +/// path - the handle to the path object. +/// fillmode - the filling mode to be set: one of the FPDF_FILLMODE_* flags. +/// stroke - a boolean specifying if the path should be stroked or not. +/// +/// Returns TRUE on success +int FPDFPath_SetDrawMode(FPDF_PAGEOBJECT path, +int fillmode, +int stroke, +) { + return _FPDFPath_SetDrawMode(path, +fillmode, +stroke, +); +} + +late final _FPDFPath_SetDrawModePtr = _lookup< + ffi.NativeFunction>('FPDFPath_SetDrawMode'); +late final _FPDFPath_SetDrawMode = _FPDFPath_SetDrawModePtr.asFunction(); + +/// Get the drawing mode of a path. +/// +/// path - the handle to the path object. +/// fillmode - the filling mode of the path: one of the FPDF_FILLMODE_* flags. +/// stroke - a boolean specifying if the path is stroked or not. +/// +/// Returns TRUE on success +int FPDFPath_GetDrawMode(FPDF_PAGEOBJECT path, +ffi.Pointer fillmode, +ffi.Pointer stroke, +) { + return _FPDFPath_GetDrawMode(path, +fillmode, +stroke, +); +} + +late final _FPDFPath_GetDrawModePtr = _lookup< + ffi.NativeFunction , ffi.Pointer )>>('FPDFPath_GetDrawMode'); +late final _FPDFPath_GetDrawMode = _FPDFPath_GetDrawModePtr.asFunction , ffi.Pointer )>(); + +/// Create a new text object using one of the standard PDF fonts. +/// +/// document - handle to the document. +/// font - string containing the font name, without spaces. +/// font_size - the font size for the new text object. +/// +/// Returns a handle to a new text object, or NULL on failure +FPDF_PAGEOBJECT FPDFPageObj_NewTextObj(FPDF_DOCUMENT document, +FPDF_BYTESTRING font, +double font_size, +) { + return _FPDFPageObj_NewTextObj(document, +font, +font_size, +); +} + +late final _FPDFPageObj_NewTextObjPtr = _lookup< + ffi.NativeFunction>('FPDFPageObj_NewTextObj'); +late final _FPDFPageObj_NewTextObj = _FPDFPageObj_NewTextObjPtr.asFunction(); + +/// Set the text for a text object. If it had text, it will be replaced. +/// +/// text_object - handle to the text object. +/// text - the UTF-16LE encoded string containing the text to be added. +/// +/// Returns TRUE on success +int FPDFText_SetText(FPDF_PAGEOBJECT text_object, +FPDF_WIDESTRING text, +) { + return _FPDFText_SetText(text_object, +text, +); +} + +late final _FPDFText_SetTextPtr = _lookup< + ffi.NativeFunction>('FPDFText_SetText'); +late final _FPDFText_SetText = _FPDFText_SetTextPtr.asFunction(); + +/// Experimental API. +/// Set the text using charcodes for a text object. If it had text, it will be +/// replaced. +/// +/// text_object - handle to the text object. +/// charcodes - pointer to an array of charcodes to be added. +/// count - number of elements in |charcodes|. +/// +/// Returns TRUE on success +int FPDFText_SetCharcodes(FPDF_PAGEOBJECT text_object, +ffi.Pointer charcodes, +int count, +) { + return _FPDFText_SetCharcodes(text_object, +charcodes, +count, +); +} + +late final _FPDFText_SetCharcodesPtr = _lookup< + ffi.NativeFunction , ffi.Size )>>('FPDFText_SetCharcodes'); +late final _FPDFText_SetCharcodes = _FPDFText_SetCharcodesPtr.asFunction , int )>(); + +/// Returns a font object loaded from a stream of data. The font is loaded +/// into the document. Various font data structures, such as the ToUnicode data, +/// are auto-generated based on the inputs. +/// +/// document - handle to the document. +/// data - the stream of font data, which will be copied by the font object. +/// size - the size of the font data, in bytes. +/// font_type - FPDF_FONT_TYPE1 or FPDF_FONT_TRUETYPE depending on the font type. +/// cid - a boolean specifying if the font is a CID font or not. +/// +/// The loaded font can be closed using FPDFFont_Close(). +/// +/// Returns NULL on failure +FPDF_FONT FPDFText_LoadFont(FPDF_DOCUMENT document, +ffi.Pointer data, +int size, +int font_type, +int cid, +) { + return _FPDFText_LoadFont(document, +data, +size, +font_type, +cid, +); +} + +late final _FPDFText_LoadFontPtr = _lookup< + ffi.NativeFunction , ffi.Uint32 , ffi.Int , FPDF_BOOL )>>('FPDFText_LoadFont'); +late final _FPDFText_LoadFont = _FPDFText_LoadFontPtr.asFunction , int , int , int )>(); + +/// Experimental API. +/// Loads one of the standard 14 fonts per PDF spec 1.7 page 416. The preferred +/// way of using font style is using a dash to separate the name from the style, +/// for example 'Helvetica-BoldItalic'. +/// +/// document - handle to the document. +/// font - string containing the font name, without spaces. +/// +/// The loaded font can be closed using FPDFFont_Close(). +/// +/// Returns NULL on failure. +FPDF_FONT FPDFText_LoadStandardFont(FPDF_DOCUMENT document, +FPDF_BYTESTRING font, +) { + return _FPDFText_LoadStandardFont(document, +font, +); +} + +late final _FPDFText_LoadStandardFontPtr = _lookup< + ffi.NativeFunction>('FPDFText_LoadStandardFont'); +late final _FPDFText_LoadStandardFont = _FPDFText_LoadStandardFontPtr.asFunction(); + +/// Experimental API. +/// Returns a font object loaded from a stream of data for a type 2 CID font. The +/// font is loaded into the document. Unlike FPDFText_LoadFont(), the ToUnicode +/// data and the CIDToGIDMap data are caller provided, instead of auto-generated. +/// +/// document - handle to the document. +/// font_data - the stream of font data, which will be copied by +/// the font object. +/// font_data_size - the size of the font data, in bytes. +/// to_unicode_cmap - the ToUnicode data. +/// cid_to_gid_map_data - the stream of CIDToGIDMap data. +/// cid_to_gid_map_data_size - the size of the CIDToGIDMap data, in bytes. +/// +/// The loaded font can be closed using FPDFFont_Close(). +/// +/// Returns NULL on failure. +FPDF_FONT FPDFText_LoadCidType2Font(FPDF_DOCUMENT document, +ffi.Pointer font_data, +int font_data_size, +FPDF_BYTESTRING to_unicode_cmap, +ffi.Pointer cid_to_gid_map_data, +int cid_to_gid_map_data_size, +) { + return _FPDFText_LoadCidType2Font(document, +font_data, +font_data_size, +to_unicode_cmap, +cid_to_gid_map_data, +cid_to_gid_map_data_size, +); +} + +late final _FPDFText_LoadCidType2FontPtr = _lookup< + ffi.NativeFunction , ffi.Uint32 , FPDF_BYTESTRING , ffi.Pointer , ffi.Uint32 )>>('FPDFText_LoadCidType2Font'); +late final _FPDFText_LoadCidType2Font = _FPDFText_LoadCidType2FontPtr.asFunction , int , FPDF_BYTESTRING , ffi.Pointer , int )>(); + +/// Get the font size of a text object. +/// +/// text - handle to a text. +/// size - pointer to the font size of the text object, measured in points +/// (about 1/72 inch) +/// +/// Returns TRUE on success. +int FPDFTextObj_GetFontSize(FPDF_PAGEOBJECT text, +ffi.Pointer size, +) { + return _FPDFTextObj_GetFontSize(text, +size, +); +} + +late final _FPDFTextObj_GetFontSizePtr = _lookup< + ffi.NativeFunction )>>('FPDFTextObj_GetFontSize'); +late final _FPDFTextObj_GetFontSize = _FPDFTextObj_GetFontSizePtr.asFunction )>(); + +/// Close a loaded PDF font. +/// +/// font - Handle to the loaded font. +void FPDFFont_Close(FPDF_FONT font, +) { + return _FPDFFont_Close(font, +); +} + +late final _FPDFFont_ClosePtr = _lookup< + ffi.NativeFunction>('FPDFFont_Close'); +late final _FPDFFont_Close = _FPDFFont_ClosePtr.asFunction(); + +/// Create a new text object using a loaded font. +/// +/// document - handle to the document. +/// font - handle to the font object. +/// font_size - the font size for the new text object. +/// +/// Returns a handle to a new text object, or NULL on failure +FPDF_PAGEOBJECT FPDFPageObj_CreateTextObj(FPDF_DOCUMENT document, +FPDF_FONT font, +double font_size, +) { + return _FPDFPageObj_CreateTextObj(document, +font, +font_size, +); +} + +late final _FPDFPageObj_CreateTextObjPtr = _lookup< + ffi.NativeFunction>('FPDFPageObj_CreateTextObj'); +late final _FPDFPageObj_CreateTextObj = _FPDFPageObj_CreateTextObjPtr.asFunction(); + +/// Get the text rendering mode of a text object. +/// +/// text - the handle to the text object. +/// +/// Returns one of the known FPDF_TEXT_RENDERMODE enum values on success, +/// FPDF_TEXTRENDERMODE_UNKNOWN on error. +FPDF_TEXT_RENDERMODE FPDFTextObj_GetTextRenderMode(FPDF_PAGEOBJECT text, +) { + return FPDF_TEXT_RENDERMODE.fromValue(_FPDFTextObj_GetTextRenderMode(text, +)); +} + +late final _FPDFTextObj_GetTextRenderModePtr = _lookup< + ffi.NativeFunction>('FPDFTextObj_GetTextRenderMode'); +late final _FPDFTextObj_GetTextRenderMode = _FPDFTextObj_GetTextRenderModePtr.asFunction(); + +/// Experimental API. +/// Set the text rendering mode of a text object. +/// +/// text - the handle to the text object. +/// render_mode - the FPDF_TEXT_RENDERMODE enum value to be set (cannot set to +/// FPDF_TEXTRENDERMODE_UNKNOWN). +/// +/// Returns TRUE on success. +DartFPDF_BOOL FPDFTextObj_SetTextRenderMode(FPDF_PAGEOBJECT text, +FPDF_TEXT_RENDERMODE render_mode, +) { + return _FPDFTextObj_SetTextRenderMode(text, +render_mode.value, +); +} + +late final _FPDFTextObj_SetTextRenderModePtr = _lookup< + ffi.NativeFunction>('FPDFTextObj_SetTextRenderMode'); +late final _FPDFTextObj_SetTextRenderMode = _FPDFTextObj_SetTextRenderModePtr.asFunction(); + +/// Get the text of a text object. +/// +/// text_object - the handle to the text object. +/// text_page - the handle to the text page. +/// buffer - the address of a buffer that receives the text. +/// length - the size, in bytes, of |buffer|. +/// +/// Returns the number of bytes in the text (including the trailing NUL +/// character) on success, 0 on error. +/// +/// Regardless of the platform, the |buffer| is always in UTF-16LE encoding. +/// If |length| is less than the returned length, or |buffer| is NULL, |buffer| +/// will not be modified. +int FPDFTextObj_GetText(FPDF_PAGEOBJECT text_object, +FPDF_TEXTPAGE text_page, +ffi.Pointer buffer, +int length, +) { + return _FPDFTextObj_GetText(text_object, +text_page, +buffer, +length, +); +} + +late final _FPDFTextObj_GetTextPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFTextObj_GetText'); +late final _FPDFTextObj_GetText = _FPDFTextObj_GetTextPtr.asFunction , int )>(); + +/// Experimental API. +/// Get a bitmap rasterization of |text_object|. To render correctly, the caller +/// must provide the |document| associated with |text_object|. If there is a +/// |page| associated with |text_object|, the caller should provide that as well. +/// The returned bitmap will be owned by the caller, and FPDFBitmap_Destroy() +/// must be called on the returned bitmap when it is no longer needed. +/// +/// document - handle to a document associated with |text_object|. +/// page - handle to an optional page associated with |text_object|. +/// text_object - handle to a text object. +/// scale - the scaling factor, which must be greater than 0. +/// +/// Returns the bitmap or NULL on failure. +FPDF_BITMAP FPDFTextObj_GetRenderedBitmap(FPDF_DOCUMENT document, +FPDF_PAGE page, +FPDF_PAGEOBJECT text_object, +double scale, +) { + return _FPDFTextObj_GetRenderedBitmap(document, +page, +text_object, +scale, +); +} + +late final _FPDFTextObj_GetRenderedBitmapPtr = _lookup< + ffi.NativeFunction>('FPDFTextObj_GetRenderedBitmap'); +late final _FPDFTextObj_GetRenderedBitmap = _FPDFTextObj_GetRenderedBitmapPtr.asFunction(); + +/// Experimental API. +/// Get the font of a text object. +/// +/// text - the handle to the text object. +/// +/// Returns a handle to the font object held by |text| which retains ownership. +FPDF_FONT FPDFTextObj_GetFont(FPDF_PAGEOBJECT text, +) { + return _FPDFTextObj_GetFont(text, +); +} + +late final _FPDFTextObj_GetFontPtr = _lookup< + ffi.NativeFunction>('FPDFTextObj_GetFont'); +late final _FPDFTextObj_GetFont = _FPDFTextObj_GetFontPtr.asFunction(); + +/// Experimental API. +/// Get the base name of a font. +/// +/// font - the handle to the font object. +/// buffer - the address of a buffer that receives the base font name. +/// length - the size, in bytes, of |buffer|. +/// +/// Returns the number of bytes in the base name (including the trailing NUL +/// character) on success, 0 on error. The base name is typically the font's +/// PostScript name. See descriptions of "BaseFont" in ISO 32000-1:2008 spec. +/// +/// Regardless of the platform, the |buffer| is always in UTF-8 encoding. +/// If |length| is less than the returned length, or |buffer| is NULL, |buffer| +/// will not be modified. +int FPDFFont_GetBaseFontName(FPDF_FONT font, +ffi.Pointer buffer, +int length, +) { + return _FPDFFont_GetBaseFontName(font, +buffer, +length, +); +} + +late final _FPDFFont_GetBaseFontNamePtr = _lookup< + ffi.NativeFunction , ffi.Size )>>('FPDFFont_GetBaseFontName'); +late final _FPDFFont_GetBaseFontName = _FPDFFont_GetBaseFontNamePtr.asFunction , int )>(); + +/// Experimental API. +/// Get the family name of a font. +/// +/// font - the handle to the font object. +/// buffer - the address of a buffer that receives the font name. +/// length - the size, in bytes, of |buffer|. +/// +/// Returns the number of bytes in the family name (including the trailing NUL +/// character) on success, 0 on error. +/// +/// Regardless of the platform, the |buffer| is always in UTF-8 encoding. +/// If |length| is less than the returned length, or |buffer| is NULL, |buffer| +/// will not be modified. +int FPDFFont_GetFamilyName(FPDF_FONT font, +ffi.Pointer buffer, +int length, +) { + return _FPDFFont_GetFamilyName(font, +buffer, +length, +); +} + +late final _FPDFFont_GetFamilyNamePtr = _lookup< + ffi.NativeFunction , ffi.Size )>>('FPDFFont_GetFamilyName'); +late final _FPDFFont_GetFamilyName = _FPDFFont_GetFamilyNamePtr.asFunction , int )>(); + +/// Experimental API. +/// Get the decoded data from the |font| object. +/// +/// font - The handle to the font object. (Required) +/// buffer - The address of a buffer that receives the font data. +/// buflen - Length of the buffer. +/// out_buflen - Pointer to variable that will receive the minimum buffer size +/// to contain the font data. Not filled if the return value is +/// FALSE. (Required) +/// +/// Returns TRUE on success. In which case, |out_buflen| will be filled, and +/// |buffer| will be filled if it is large enough. Returns FALSE if any of the +/// required parameters are null. +/// +/// The decoded data is the uncompressed font data. i.e. the raw font data after +/// having all stream filters applied, when the data is embedded. +/// +/// If the font is not embedded, then this API will instead return the data for +/// the substitution font it is using. +int FPDFFont_GetFontData(FPDF_FONT font, +ffi.Pointer buffer, +int buflen, +ffi.Pointer out_buflen, +) { + return _FPDFFont_GetFontData(font, +buffer, +buflen, +out_buflen, +); +} + +late final _FPDFFont_GetFontDataPtr = _lookup< + ffi.NativeFunction , ffi.Size , ffi.Pointer )>>('FPDFFont_GetFontData'); +late final _FPDFFont_GetFontData = _FPDFFont_GetFontDataPtr.asFunction , int , ffi.Pointer )>(); + +/// Experimental API. +/// Get whether |font| is embedded or not. +/// +/// font - the handle to the font object. +/// +/// Returns 1 if the font is embedded, 0 if it not, and -1 on failure. +int FPDFFont_GetIsEmbedded(FPDF_FONT font, +) { + return _FPDFFont_GetIsEmbedded(font, +); +} + +late final _FPDFFont_GetIsEmbeddedPtr = _lookup< + ffi.NativeFunction>('FPDFFont_GetIsEmbedded'); +late final _FPDFFont_GetIsEmbedded = _FPDFFont_GetIsEmbeddedPtr.asFunction(); + +/// Experimental API. +/// Get the descriptor flags of a font. +/// +/// font - the handle to the font object. +/// +/// Returns the bit flags specifying various characteristics of the font as +/// defined in ISO 32000-1:2008, table 123, -1 on failure. +int FPDFFont_GetFlags(FPDF_FONT font, +) { + return _FPDFFont_GetFlags(font, +); +} + +late final _FPDFFont_GetFlagsPtr = _lookup< + ffi.NativeFunction>('FPDFFont_GetFlags'); +late final _FPDFFont_GetFlags = _FPDFFont_GetFlagsPtr.asFunction(); + +/// Experimental API. +/// Get the font weight of a font. +/// +/// font - the handle to the font object. +/// +/// Returns the font weight, -1 on failure. +/// Typical values are 400 (normal) and 700 (bold). +int FPDFFont_GetWeight(FPDF_FONT font, +) { + return _FPDFFont_GetWeight(font, +); +} + +late final _FPDFFont_GetWeightPtr = _lookup< + ffi.NativeFunction>('FPDFFont_GetWeight'); +late final _FPDFFont_GetWeight = _FPDFFont_GetWeightPtr.asFunction(); + +/// Experimental API. +/// Get the italic angle of a font. +/// +/// font - the handle to the font object. +/// angle - pointer where the italic angle will be stored +/// +/// The italic angle of a |font| is defined as degrees counterclockwise +/// from vertical. For a font that slopes to the right, this will be negative. +/// +/// Returns TRUE on success; |angle| unmodified on failure. +int FPDFFont_GetItalicAngle(FPDF_FONT font, +ffi.Pointer angle, +) { + return _FPDFFont_GetItalicAngle(font, +angle, +); +} + +late final _FPDFFont_GetItalicAnglePtr = _lookup< + ffi.NativeFunction )>>('FPDFFont_GetItalicAngle'); +late final _FPDFFont_GetItalicAngle = _FPDFFont_GetItalicAnglePtr.asFunction )>(); + +/// Experimental API. +/// Get ascent distance of a font. +/// +/// font - the handle to the font object. +/// font_size - the size of the |font|. +/// ascent - pointer where the font ascent will be stored +/// +/// Ascent is the maximum distance in points above the baseline reached by the +/// glyphs of the |font|. One point is 1/72 inch (around 0.3528 mm). +/// +/// Returns TRUE on success; |ascent| unmodified on failure. +int FPDFFont_GetAscent(FPDF_FONT font, +double font_size, +ffi.Pointer ascent, +) { + return _FPDFFont_GetAscent(font, +font_size, +ascent, +); +} + +late final _FPDFFont_GetAscentPtr = _lookup< + ffi.NativeFunction )>>('FPDFFont_GetAscent'); +late final _FPDFFont_GetAscent = _FPDFFont_GetAscentPtr.asFunction )>(); + +/// Experimental API. +/// Get descent distance of a font. +/// +/// font - the handle to the font object. +/// font_size - the size of the |font|. +/// descent - pointer where the font descent will be stored +/// +/// Descent is the maximum distance in points below the baseline reached by the +/// glyphs of the |font|. One point is 1/72 inch (around 0.3528 mm). +/// +/// Returns TRUE on success; |descent| unmodified on failure. +int FPDFFont_GetDescent(FPDF_FONT font, +double font_size, +ffi.Pointer descent, +) { + return _FPDFFont_GetDescent(font, +font_size, +descent, +); +} + +late final _FPDFFont_GetDescentPtr = _lookup< + ffi.NativeFunction )>>('FPDFFont_GetDescent'); +late final _FPDFFont_GetDescent = _FPDFFont_GetDescentPtr.asFunction )>(); + +/// Experimental API. +/// Get the width of a glyph in a font. +/// +/// font - the handle to the font object. +/// glyph - the glyph. +/// font_size - the size of the font. +/// width - pointer where the glyph width will be stored +/// +/// Glyph width is the distance from the end of the prior glyph to the next +/// glyph. This will be the vertical distance for vertical writing. +/// +/// Returns TRUE on success; |width| unmodified on failure. +int FPDFFont_GetGlyphWidth(FPDF_FONT font, +int glyph, +double font_size, +ffi.Pointer width, +) { + return _FPDFFont_GetGlyphWidth(font, +glyph, +font_size, +width, +); +} + +late final _FPDFFont_GetGlyphWidthPtr = _lookup< + ffi.NativeFunction )>>('FPDFFont_GetGlyphWidth'); +late final _FPDFFont_GetGlyphWidth = _FPDFFont_GetGlyphWidthPtr.asFunction )>(); + +/// Experimental API. +/// Get the glyphpath describing how to draw a font glyph. +/// +/// font - the handle to the font object. +/// glyph - the glyph being drawn. +/// font_size - the size of the font. +/// +/// Returns the handle to the segment, or NULL on faiure. +FPDF_GLYPHPATH FPDFFont_GetGlyphPath(FPDF_FONT font, +int glyph, +double font_size, +) { + return _FPDFFont_GetGlyphPath(font, +glyph, +font_size, +); +} + +late final _FPDFFont_GetGlyphPathPtr = _lookup< + ffi.NativeFunction>('FPDFFont_GetGlyphPath'); +late final _FPDFFont_GetGlyphPath = _FPDFFont_GetGlyphPathPtr.asFunction(); + +/// Experimental API. +/// Get number of segments inside glyphpath. +/// +/// glyphpath - handle to a glyph path. +/// +/// Returns the number of objects in |glyphpath| or -1 on failure. +int FPDFGlyphPath_CountGlyphSegments(FPDF_GLYPHPATH glyphpath, +) { + return _FPDFGlyphPath_CountGlyphSegments(glyphpath, +); +} + +late final _FPDFGlyphPath_CountGlyphSegmentsPtr = _lookup< + ffi.NativeFunction>('FPDFGlyphPath_CountGlyphSegments'); +late final _FPDFGlyphPath_CountGlyphSegments = _FPDFGlyphPath_CountGlyphSegmentsPtr.asFunction(); + +/// Experimental API. +/// Get segment in glyphpath at index. +/// +/// glyphpath - handle to a glyph path. +/// index - the index of a segment. +/// +/// Returns the handle to the segment, or NULL on faiure. +FPDF_PATHSEGMENT FPDFGlyphPath_GetGlyphPathSegment(FPDF_GLYPHPATH glyphpath, +int index, +) { + return _FPDFGlyphPath_GetGlyphPathSegment(glyphpath, +index, +); +} + +late final _FPDFGlyphPath_GetGlyphPathSegmentPtr = _lookup< + ffi.NativeFunction>('FPDFGlyphPath_GetGlyphPathSegment'); +late final _FPDFGlyphPath_GetGlyphPathSegment = _FPDFGlyphPath_GetGlyphPathSegmentPtr.asFunction(); + +/// Get number of page objects inside |form_object|. +/// +/// form_object - handle to a form object. +/// +/// Returns the number of objects in |form_object| on success, -1 on error. +int FPDFFormObj_CountObjects(FPDF_PAGEOBJECT form_object, +) { + return _FPDFFormObj_CountObjects(form_object, +); +} + +late final _FPDFFormObj_CountObjectsPtr = _lookup< + ffi.NativeFunction>('FPDFFormObj_CountObjects'); +late final _FPDFFormObj_CountObjects = _FPDFFormObj_CountObjectsPtr.asFunction(); + +/// Get page object in |form_object| at |index|. +/// +/// form_object - handle to a form object. +/// index - the 0-based index of a page object. +/// +/// Returns the handle to the page object, or NULL on error. +FPDF_PAGEOBJECT FPDFFormObj_GetObject(FPDF_PAGEOBJECT form_object, +int index, +) { + return _FPDFFormObj_GetObject(form_object, +index, +); +} + +late final _FPDFFormObj_GetObjectPtr = _lookup< + ffi.NativeFunction>('FPDFFormObj_GetObject'); +late final _FPDFFormObj_GetObject = _FPDFFormObj_GetObjectPtr.asFunction(); + +/// Experimental API. +/// +/// Remove |page_object| from |form_object|. +/// +/// form_object - handle to a form object. +/// page_object - handle to a page object to be removed from the form. +/// +/// Returns TRUE on success. +/// +/// Ownership of the removed |page_object| is transferred to the caller. +/// Call FPDFPageObj_Destroy() on the removed page_object to free it. +int FPDFFormObj_RemoveObject(FPDF_PAGEOBJECT form_object, +FPDF_PAGEOBJECT page_object, +) { + return _FPDFFormObj_RemoveObject(form_object, +page_object, +); +} + +late final _FPDFFormObj_RemoveObjectPtr = _lookup< + ffi.NativeFunction>('FPDFFormObj_RemoveObject'); +late final _FPDFFormObj_RemoveObject = _FPDFFormObj_RemoveObjectPtr.asFunction(); + +/// Experimental API. +/// Get the number of embedded files in |document|. +/// +/// document - handle to a document. +/// +/// Returns the number of embedded files in |document|. +int FPDFDoc_GetAttachmentCount(FPDF_DOCUMENT document, +) { + return _FPDFDoc_GetAttachmentCount(document, +); +} + +late final _FPDFDoc_GetAttachmentCountPtr = _lookup< + ffi.NativeFunction>('FPDFDoc_GetAttachmentCount'); +late final _FPDFDoc_GetAttachmentCount = _FPDFDoc_GetAttachmentCountPtr.asFunction(); + +/// Experimental API. +/// Add an embedded file with |name| in |document|. If |name| is empty, or if +/// |name| is the name of a existing embedded file in |document|, or if +/// |document|'s embedded file name tree is too deep (i.e. |document| has too +/// many embedded files already), then a new attachment will not be added. +/// +/// document - handle to a document. +/// name - name of the new attachment. +/// +/// Returns a handle to the new attachment object, or NULL on failure. +FPDF_ATTACHMENT FPDFDoc_AddAttachment(FPDF_DOCUMENT document, +FPDF_WIDESTRING name, +) { + return _FPDFDoc_AddAttachment(document, +name, +); +} + +late final _FPDFDoc_AddAttachmentPtr = _lookup< + ffi.NativeFunction>('FPDFDoc_AddAttachment'); +late final _FPDFDoc_AddAttachment = _FPDFDoc_AddAttachmentPtr.asFunction(); + +/// Experimental API. +/// Get the embedded attachment at |index| in |document|. Note that the returned +/// attachment handle is only valid while |document| is open. +/// +/// document - handle to a document. +/// index - the index of the requested embedded file. +/// +/// Returns the handle to the attachment object, or NULL on failure. +FPDF_ATTACHMENT FPDFDoc_GetAttachment(FPDF_DOCUMENT document, +int index, +) { + return _FPDFDoc_GetAttachment(document, +index, +); +} + +late final _FPDFDoc_GetAttachmentPtr = _lookup< + ffi.NativeFunction>('FPDFDoc_GetAttachment'); +late final _FPDFDoc_GetAttachment = _FPDFDoc_GetAttachmentPtr.asFunction(); + +/// Experimental API. +/// Delete the embedded attachment at |index| in |document|. Note that this does +/// not remove the attachment data from the PDF file; it simply removes the +/// file's entry in the embedded files name tree so that it does not appear in +/// the attachment list. This behavior may change in the future. +/// +/// document - handle to a document. +/// index - the index of the embedded file to be deleted. +/// +/// Returns true if successful. +int FPDFDoc_DeleteAttachment(FPDF_DOCUMENT document, +int index, +) { + return _FPDFDoc_DeleteAttachment(document, +index, +); +} + +late final _FPDFDoc_DeleteAttachmentPtr = _lookup< + ffi.NativeFunction>('FPDFDoc_DeleteAttachment'); +late final _FPDFDoc_DeleteAttachment = _FPDFDoc_DeleteAttachmentPtr.asFunction(); + +/// Experimental API. +/// Get the name of the |attachment| file. |buffer| is only modified if |buflen| +/// is longer than the length of the file name. On errors, |buffer| is unmodified +/// and the returned length is 0. +/// +/// attachment - handle to an attachment. +/// buffer - buffer for holding the file name, encoded in UTF-16LE. +/// buflen - length of the buffer in bytes. +/// +/// Returns the length of the file name in bytes. +int FPDFAttachment_GetName(FPDF_ATTACHMENT attachment, +ffi.Pointer buffer, +int buflen, +) { + return _FPDFAttachment_GetName(attachment, +buffer, +buflen, +); +} + +late final _FPDFAttachment_GetNamePtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFAttachment_GetName'); +late final _FPDFAttachment_GetName = _FPDFAttachment_GetNamePtr.asFunction , int )>(); + +/// Experimental API. +/// Check if the params dictionary of |attachment| has |key| as a key. +/// +/// attachment - handle to an attachment. +/// key - the key to look for, encoded in UTF-8. +/// +/// Returns true if |key| exists. +int FPDFAttachment_HasKey(FPDF_ATTACHMENT attachment, +FPDF_BYTESTRING key, +) { + return _FPDFAttachment_HasKey(attachment, +key, +); +} + +late final _FPDFAttachment_HasKeyPtr = _lookup< + ffi.NativeFunction>('FPDFAttachment_HasKey'); +late final _FPDFAttachment_HasKey = _FPDFAttachment_HasKeyPtr.asFunction(); + +/// Experimental API. +/// Get the type of the value corresponding to |key| in the params dictionary of +/// the embedded |attachment|. +/// +/// attachment - handle to an attachment. +/// key - the key to look for, encoded in UTF-8. +/// +/// Returns the type of the dictionary value. +int FPDFAttachment_GetValueType(FPDF_ATTACHMENT attachment, +FPDF_BYTESTRING key, +) { + return _FPDFAttachment_GetValueType(attachment, +key, +); +} + +late final _FPDFAttachment_GetValueTypePtr = _lookup< + ffi.NativeFunction>('FPDFAttachment_GetValueType'); +late final _FPDFAttachment_GetValueType = _FPDFAttachment_GetValueTypePtr.asFunction(); + +/// Experimental API. +/// Set the string value corresponding to |key| in the params dictionary of the +/// embedded file |attachment|, overwriting the existing value if any. The value +/// type should be FPDF_OBJECT_STRING after this function call succeeds. +/// +/// attachment - handle to an attachment. +/// key - the key to the dictionary entry, encoded in UTF-8. +/// value - the string value to be set, encoded in UTF-16LE. +/// +/// Returns true if successful. +int FPDFAttachment_SetStringValue(FPDF_ATTACHMENT attachment, +FPDF_BYTESTRING key, +FPDF_WIDESTRING value, +) { + return _FPDFAttachment_SetStringValue(attachment, +key, +value, +); +} + +late final _FPDFAttachment_SetStringValuePtr = _lookup< + ffi.NativeFunction>('FPDFAttachment_SetStringValue'); +late final _FPDFAttachment_SetStringValue = _FPDFAttachment_SetStringValuePtr.asFunction(); + +/// Experimental API. +/// Get the string value corresponding to |key| in the params dictionary of the +/// embedded file |attachment|. |buffer| is only modified if |buflen| is longer +/// than the length of the string value. Note that if |key| does not exist in the +/// dictionary or if |key|'s corresponding value in the dictionary is not a +/// string (i.e. the value is not of type FPDF_OBJECT_STRING or +/// FPDF_OBJECT_NAME), then an empty string would be copied to |buffer| and the +/// return value would be 2. On other errors, nothing would be added to |buffer| +/// and the return value would be 0. +/// +/// attachment - handle to an attachment. +/// key - the key to the requested string value, encoded in UTF-8. +/// buffer - buffer for holding the string value encoded in UTF-16LE. +/// buflen - length of the buffer in bytes. +/// +/// Returns the length of the dictionary value string in bytes. +int FPDFAttachment_GetStringValue(FPDF_ATTACHMENT attachment, +FPDF_BYTESTRING key, +ffi.Pointer buffer, +int buflen, +) { + return _FPDFAttachment_GetStringValue(attachment, +key, +buffer, +buflen, +); +} + +late final _FPDFAttachment_GetStringValuePtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFAttachment_GetStringValue'); +late final _FPDFAttachment_GetStringValue = _FPDFAttachment_GetStringValuePtr.asFunction , int )>(); + +/// Experimental API. +/// Set the file data of |attachment|, overwriting the existing file data if any. +/// The creation date and checksum will be updated, while all other dictionary +/// entries will be deleted. Note that only contents with |len| smaller than +/// INT_MAX is supported. +/// +/// attachment - handle to an attachment. +/// contents - buffer holding the file data to write to |attachment|. +/// len - length of file data in bytes. +/// +/// Returns true if successful. +int FPDFAttachment_SetFile(FPDF_ATTACHMENT attachment, +FPDF_DOCUMENT document, +ffi.Pointer contents, +int len, +) { + return _FPDFAttachment_SetFile(attachment, +document, +contents, +len, +); +} + +late final _FPDFAttachment_SetFilePtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFAttachment_SetFile'); +late final _FPDFAttachment_SetFile = _FPDFAttachment_SetFilePtr.asFunction , int )>(); + +/// Experimental API. +/// Get the file data of |attachment|. +/// When the attachment file data is readable, true is returned, and |out_buflen| +/// is updated to indicate the file data size. |buffer| is only modified if +/// |buflen| is non-null and long enough to contain the entire file data. Callers +/// must check both the return value and the input |buflen| is no less than the +/// returned |out_buflen| before using the data. +/// +/// Otherwise, when the attachment file data is unreadable or when |out_buflen| +/// is null, false is returned and |buffer| and |out_buflen| remain unmodified. +/// +/// attachment - handle to an attachment. +/// buffer - buffer for holding the file data from |attachment|. +/// buflen - length of the buffer in bytes. +/// out_buflen - pointer to the variable that will receive the minimum buffer +/// size to contain the file data of |attachment|. +/// +/// Returns true on success, false otherwise. +int FPDFAttachment_GetFile(FPDF_ATTACHMENT attachment, +ffi.Pointer buffer, +int buflen, +ffi.Pointer out_buflen, +) { + return _FPDFAttachment_GetFile(attachment, +buffer, +buflen, +out_buflen, +); +} + +late final _FPDFAttachment_GetFilePtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong , ffi.Pointer )>>('FPDFAttachment_GetFile'); +late final _FPDFAttachment_GetFile = _FPDFAttachment_GetFilePtr.asFunction , int , ffi.Pointer )>(); + +/// Experimental API. +/// Get the MIME type (Subtype) of the embedded file |attachment|. |buffer| is +/// only modified if |buflen| is longer than the length of the MIME type string. +/// If the Subtype is not found or if there is no file stream, an empty string +/// would be copied to |buffer| and the return value would be 2. On other errors, +/// nothing would be added to |buffer| and the return value would be 0. +/// +/// attachment - handle to an attachment. +/// buffer - buffer for holding the MIME type string encoded in UTF-16LE. +/// buflen - length of the buffer in bytes. +/// +/// Returns the length of the MIME type string in bytes. +int FPDFAttachment_GetSubtype(FPDF_ATTACHMENT attachment, +ffi.Pointer buffer, +int buflen, +) { + return _FPDFAttachment_GetSubtype(attachment, +buffer, +buflen, +); +} + +late final _FPDFAttachment_GetSubtypePtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFAttachment_GetSubtype'); +late final _FPDFAttachment_GetSubtype = _FPDFAttachment_GetSubtypePtr.asFunction , int )>(); + +/// Function: FPDFDOC_InitFormFillEnvironment +/// Initialize form fill environment. +/// Parameters: +/// document - Handle to document from FPDF_LoadDocument(). +/// formInfo - Pointer to a FPDF_FORMFILLINFO structure. +/// Return Value: +/// Handle to the form fill module, or NULL on failure. +/// Comments: +/// This function should be called before any form fill operation. +/// The FPDF_FORMFILLINFO passed in via |formInfo| must remain valid until +/// the returned FPDF_FORMHANDLE is closed. +FPDF_FORMHANDLE FPDFDOC_InitFormFillEnvironment(FPDF_DOCUMENT document, +ffi.Pointer formInfo, +) { + return _FPDFDOC_InitFormFillEnvironment(document, +formInfo, +); +} + +late final _FPDFDOC_InitFormFillEnvironmentPtr = _lookup< + ffi.NativeFunction )>>('FPDFDOC_InitFormFillEnvironment'); +late final _FPDFDOC_InitFormFillEnvironment = _FPDFDOC_InitFormFillEnvironmentPtr.asFunction )>(); + +/// Function: FPDFDOC_ExitFormFillEnvironment +/// Take ownership of |hHandle| and exit form fill environment. +/// Parameters: +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// Return Value: +/// None. +/// Comments: +/// This function is a no-op when |hHandle| is null. +void FPDFDOC_ExitFormFillEnvironment(FPDF_FORMHANDLE hHandle, +) { + return _FPDFDOC_ExitFormFillEnvironment(hHandle, +); +} + +late final _FPDFDOC_ExitFormFillEnvironmentPtr = _lookup< + ffi.NativeFunction>('FPDFDOC_ExitFormFillEnvironment'); +late final _FPDFDOC_ExitFormFillEnvironment = _FPDFDOC_ExitFormFillEnvironmentPtr.asFunction(); + +/// Function: FORM_OnAfterLoadPage +/// This method is required for implementing all the form related +/// functions. Should be invoked after user successfully loaded a +/// PDF page, and FPDFDOC_InitFormFillEnvironment() has been invoked. +/// Parameters: +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// Return Value: +/// None. +void FORM_OnAfterLoadPage(FPDF_PAGE page, +FPDF_FORMHANDLE hHandle, +) { + return _FORM_OnAfterLoadPage(page, +hHandle, +); +} + +late final _FORM_OnAfterLoadPagePtr = _lookup< + ffi.NativeFunction>('FORM_OnAfterLoadPage'); +late final _FORM_OnAfterLoadPage = _FORM_OnAfterLoadPagePtr.asFunction(); + +/// Function: FORM_OnBeforeClosePage +/// This method is required for implementing all the form related +/// functions. Should be invoked before user closes the PDF page. +/// Parameters: +/// page - Handle to the page, as returned by FPDF_LoadPage(). +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// Return Value: +/// None. +void FORM_OnBeforeClosePage(FPDF_PAGE page, +FPDF_FORMHANDLE hHandle, +) { + return _FORM_OnBeforeClosePage(page, +hHandle, +); +} + +late final _FORM_OnBeforeClosePagePtr = _lookup< + ffi.NativeFunction>('FORM_OnBeforeClosePage'); +late final _FORM_OnBeforeClosePage = _FORM_OnBeforeClosePagePtr.asFunction(); + +/// Function: FORM_DoDocumentJSAction +/// This method is required for performing document-level JavaScript +/// actions. It should be invoked after the PDF document has been loaded. +/// Parameters: +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// Return Value: +/// None. +/// Comments: +/// If there is document-level JavaScript action embedded in the +/// document, this method will execute the JavaScript action. Otherwise, +/// the method will do nothing. +void FORM_DoDocumentJSAction(FPDF_FORMHANDLE hHandle, +) { + return _FORM_DoDocumentJSAction(hHandle, +); +} + +late final _FORM_DoDocumentJSActionPtr = _lookup< + ffi.NativeFunction>('FORM_DoDocumentJSAction'); +late final _FORM_DoDocumentJSAction = _FORM_DoDocumentJSActionPtr.asFunction(); + +/// Function: FORM_DoDocumentOpenAction +/// This method is required for performing open-action when the document +/// is opened. +/// Parameters: +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// Return Value: +/// None. +/// Comments: +/// This method will do nothing if there are no open-actions embedded +/// in the document. +void FORM_DoDocumentOpenAction(FPDF_FORMHANDLE hHandle, +) { + return _FORM_DoDocumentOpenAction(hHandle, +); +} + +late final _FORM_DoDocumentOpenActionPtr = _lookup< + ffi.NativeFunction>('FORM_DoDocumentOpenAction'); +late final _FORM_DoDocumentOpenAction = _FORM_DoDocumentOpenActionPtr.asFunction(); + +/// Function: FORM_DoDocumentAAction +/// This method is required for performing the document's +/// additional-action. +/// Parameters: +/// hHandle - Handle to the form fill module. Returned by +/// FPDFDOC_InitFormFillEnvironment. +/// aaType - The type of the additional-actions which defined +/// above. +/// Return Value: +/// None. +/// Comments: +/// This method will do nothing if there is no document +/// additional-action corresponding to the specified |aaType|. +void FORM_DoDocumentAAction(FPDF_FORMHANDLE hHandle, +int aaType, +) { + return _FORM_DoDocumentAAction(hHandle, +aaType, +); +} + +late final _FORM_DoDocumentAActionPtr = _lookup< + ffi.NativeFunction>('FORM_DoDocumentAAction'); +late final _FORM_DoDocumentAAction = _FORM_DoDocumentAActionPtr.asFunction(); + +/// Function: FORM_DoPageAAction +/// This method is required for performing the page object's +/// additional-action when opened or closed. +/// Parameters: +/// page - Handle to the page, as returned by FPDF_LoadPage(). +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// aaType - The type of the page object's additional-actions +/// which defined above. +/// Return Value: +/// None. +/// Comments: +/// This method will do nothing if no additional-action corresponding +/// to the specified |aaType| exists. +void FORM_DoPageAAction(FPDF_PAGE page, +FPDF_FORMHANDLE hHandle, +int aaType, +) { + return _FORM_DoPageAAction(page, +hHandle, +aaType, +); +} + +late final _FORM_DoPageAActionPtr = _lookup< + ffi.NativeFunction>('FORM_DoPageAAction'); +late final _FORM_DoPageAAction = _FORM_DoPageAActionPtr.asFunction(); + +/// Function: FORM_OnMouseMove +/// Call this member function when the mouse cursor moves. +/// Parameters: +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// page - Handle to the page, as returned by FPDF_LoadPage(). +/// modifier - Indicates whether various virtual keys are down. +/// page_x - Specifies the x-coordinate of the cursor in PDF user +/// space. +/// page_y - Specifies the y-coordinate of the cursor in PDF user +/// space. +/// Return Value: +/// True indicates success; otherwise false. +int FORM_OnMouseMove(FPDF_FORMHANDLE hHandle, +FPDF_PAGE page, +int modifier, +double page_x, +double page_y, +) { + return _FORM_OnMouseMove(hHandle, +page, +modifier, +page_x, +page_y, +); +} + +late final _FORM_OnMouseMovePtr = _lookup< + ffi.NativeFunction>('FORM_OnMouseMove'); +late final _FORM_OnMouseMove = _FORM_OnMouseMovePtr.asFunction(); + +/// Experimental API +/// Function: FORM_OnMouseWheel +/// Call this member function when the user scrolls the mouse wheel. +/// Parameters: +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// page - Handle to the page, as returned by FPDF_LoadPage(). +/// modifier - Indicates whether various virtual keys are down. +/// page_coord - Specifies the coordinates of the cursor in PDF user +/// space. +/// delta_x - Specifies the amount of wheel movement on the x-axis, +/// in units of platform-agnostic wheel deltas. Negative +/// values mean left. +/// delta_y - Specifies the amount of wheel movement on the y-axis, +/// in units of platform-agnostic wheel deltas. Negative +/// values mean down. +/// Return Value: +/// True indicates success; otherwise false. +/// Comments: +/// For |delta_x| and |delta_y|, the caller must normalize +/// platform-specific wheel deltas. e.g. On Windows, a delta value of 240 +/// for a WM_MOUSEWHEEL event normalizes to 2, since Windows defines +/// WHEEL_DELTA as 120. +int FORM_OnMouseWheel(FPDF_FORMHANDLE hHandle, +FPDF_PAGE page, +int modifier, +ffi.Pointer page_coord, +int delta_x, +int delta_y, +) { + return _FORM_OnMouseWheel(hHandle, +page, +modifier, +page_coord, +delta_x, +delta_y, +); +} + +late final _FORM_OnMouseWheelPtr = _lookup< + ffi.NativeFunction , ffi.Int , ffi.Int )>>('FORM_OnMouseWheel'); +late final _FORM_OnMouseWheel = _FORM_OnMouseWheelPtr.asFunction , int , int )>(); + +/// Function: FORM_OnFocus +/// This function focuses the form annotation at a given point. If the +/// annotation at the point already has focus, nothing happens. If there +/// is no annotation at the point, removes form focus. +/// Parameters: +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// page - Handle to the page, as returned by FPDF_LoadPage(). +/// modifier - Indicates whether various virtual keys are down. +/// page_x - Specifies the x-coordinate of the cursor in PDF user +/// space. +/// page_y - Specifies the y-coordinate of the cursor in PDF user +/// space. +/// Return Value: +/// True if there is an annotation at the given point and it has focus. +int FORM_OnFocus(FPDF_FORMHANDLE hHandle, +FPDF_PAGE page, +int modifier, +double page_x, +double page_y, +) { + return _FORM_OnFocus(hHandle, +page, +modifier, +page_x, +page_y, +); +} + +late final _FORM_OnFocusPtr = _lookup< + ffi.NativeFunction>('FORM_OnFocus'); +late final _FORM_OnFocus = _FORM_OnFocusPtr.asFunction(); + +/// Function: FORM_OnLButtonDown +/// Call this member function when the user presses the left +/// mouse button. +/// Parameters: +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// page - Handle to the page, as returned by FPDF_LoadPage(). +/// modifier - Indicates whether various virtual keys are down. +/// page_x - Specifies the x-coordinate of the cursor in PDF user +/// space. +/// page_y - Specifies the y-coordinate of the cursor in PDF user +/// space. +/// Return Value: +/// True indicates success; otherwise false. +int FORM_OnLButtonDown(FPDF_FORMHANDLE hHandle, +FPDF_PAGE page, +int modifier, +double page_x, +double page_y, +) { + return _FORM_OnLButtonDown(hHandle, +page, +modifier, +page_x, +page_y, +); +} + +late final _FORM_OnLButtonDownPtr = _lookup< + ffi.NativeFunction>('FORM_OnLButtonDown'); +late final _FORM_OnLButtonDown = _FORM_OnLButtonDownPtr.asFunction(); + +/// Function: FORM_OnRButtonDown +/// Same as above, execpt for the right mouse button. +/// Comments: +/// At the present time, has no effect except in XFA builds, but is +/// included for the sake of symmetry. +int FORM_OnRButtonDown(FPDF_FORMHANDLE hHandle, +FPDF_PAGE page, +int modifier, +double page_x, +double page_y, +) { + return _FORM_OnRButtonDown(hHandle, +page, +modifier, +page_x, +page_y, +); +} + +late final _FORM_OnRButtonDownPtr = _lookup< + ffi.NativeFunction>('FORM_OnRButtonDown'); +late final _FORM_OnRButtonDown = _FORM_OnRButtonDownPtr.asFunction(); + +/// Function: FORM_OnLButtonUp +/// Call this member function when the user releases the left +/// mouse button. +/// Parameters: +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// page - Handle to the page, as returned by FPDF_LoadPage(). +/// modifier - Indicates whether various virtual keys are down. +/// page_x - Specifies the x-coordinate of the cursor in device. +/// page_y - Specifies the y-coordinate of the cursor in device. +/// Return Value: +/// True indicates success; otherwise false. +int FORM_OnLButtonUp(FPDF_FORMHANDLE hHandle, +FPDF_PAGE page, +int modifier, +double page_x, +double page_y, +) { + return _FORM_OnLButtonUp(hHandle, +page, +modifier, +page_x, +page_y, +); +} + +late final _FORM_OnLButtonUpPtr = _lookup< + ffi.NativeFunction>('FORM_OnLButtonUp'); +late final _FORM_OnLButtonUp = _FORM_OnLButtonUpPtr.asFunction(); + +/// Function: FORM_OnRButtonUp +/// Same as above, execpt for the right mouse button. +/// Comments: +/// At the present time, has no effect except in XFA builds, but is +/// included for the sake of symmetry. +int FORM_OnRButtonUp(FPDF_FORMHANDLE hHandle, +FPDF_PAGE page, +int modifier, +double page_x, +double page_y, +) { + return _FORM_OnRButtonUp(hHandle, +page, +modifier, +page_x, +page_y, +); +} + +late final _FORM_OnRButtonUpPtr = _lookup< + ffi.NativeFunction>('FORM_OnRButtonUp'); +late final _FORM_OnRButtonUp = _FORM_OnRButtonUpPtr.asFunction(); + +/// Function: FORM_OnLButtonDoubleClick +/// Call this member function when the user double clicks the +/// left mouse button. +/// Parameters: +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// page - Handle to the page, as returned by FPDF_LoadPage(). +/// modifier - Indicates whether various virtual keys are down. +/// page_x - Specifies the x-coordinate of the cursor in PDF user +/// space. +/// page_y - Specifies the y-coordinate of the cursor in PDF user +/// space. +/// Return Value: +/// True indicates success; otherwise false. +int FORM_OnLButtonDoubleClick(FPDF_FORMHANDLE hHandle, +FPDF_PAGE page, +int modifier, +double page_x, +double page_y, +) { + return _FORM_OnLButtonDoubleClick(hHandle, +page, +modifier, +page_x, +page_y, +); +} + +late final _FORM_OnLButtonDoubleClickPtr = _lookup< + ffi.NativeFunction>('FORM_OnLButtonDoubleClick'); +late final _FORM_OnLButtonDoubleClick = _FORM_OnLButtonDoubleClickPtr.asFunction(); + +/// Function: FORM_OnKeyDown +/// Call this member function when a nonsystem key is pressed. +/// Parameters: +/// hHandle - Handle to the form fill module, aseturned by +/// FPDFDOC_InitFormFillEnvironment(). +/// page - Handle to the page, as returned by FPDF_LoadPage(). +/// nKeyCode - The virtual-key code of the given key (see +/// fpdf_fwlevent.h for virtual key codes). +/// modifier - Mask of key flags (see fpdf_fwlevent.h for key +/// flag values). +/// Return Value: +/// True indicates success; otherwise false. +int FORM_OnKeyDown(FPDF_FORMHANDLE hHandle, +FPDF_PAGE page, +int nKeyCode, +int modifier, +) { + return _FORM_OnKeyDown(hHandle, +page, +nKeyCode, +modifier, +); +} + +late final _FORM_OnKeyDownPtr = _lookup< + ffi.NativeFunction>('FORM_OnKeyDown'); +late final _FORM_OnKeyDown = _FORM_OnKeyDownPtr.asFunction(); + +/// Function: FORM_OnKeyUp +/// Call this member function when a nonsystem key is released. +/// Parameters: +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// page - Handle to the page, as returned by FPDF_LoadPage(). +/// nKeyCode - The virtual-key code of the given key (see +/// fpdf_fwlevent.h for virtual key codes). +/// modifier - Mask of key flags (see fpdf_fwlevent.h for key +/// flag values). +/// Return Value: +/// True indicates success; otherwise false. +/// Comments: +/// Currently unimplemented and always returns false. PDFium reserves this +/// API and may implement it in the future on an as-needed basis. +int FORM_OnKeyUp(FPDF_FORMHANDLE hHandle, +FPDF_PAGE page, +int nKeyCode, +int modifier, +) { + return _FORM_OnKeyUp(hHandle, +page, +nKeyCode, +modifier, +); +} + +late final _FORM_OnKeyUpPtr = _lookup< + ffi.NativeFunction>('FORM_OnKeyUp'); +late final _FORM_OnKeyUp = _FORM_OnKeyUpPtr.asFunction(); + +/// Function: FORM_OnChar +/// Call this member function when a keystroke translates to a +/// nonsystem character. +/// Parameters: +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// page - Handle to the page, as returned by FPDF_LoadPage(). +/// nChar - The character code value itself. +/// modifier - Mask of key flags (see fpdf_fwlevent.h for key +/// flag values). +/// Return Value: +/// True indicates success; otherwise false. +int FORM_OnChar(FPDF_FORMHANDLE hHandle, +FPDF_PAGE page, +int nChar, +int modifier, +) { + return _FORM_OnChar(hHandle, +page, +nChar, +modifier, +); +} + +late final _FORM_OnCharPtr = _lookup< + ffi.NativeFunction>('FORM_OnChar'); +late final _FORM_OnChar = _FORM_OnCharPtr.asFunction(); + +/// Experimental API +/// Function: FORM_GetFocusedText +/// Call this function to obtain the text within the current focused +/// field, if any. +/// Parameters: +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// page - Handle to the page, as returned by FPDF_LoadPage(). +/// buffer - Buffer for holding the form text, encoded in +/// UTF-16LE. If NULL, |buffer| is not modified. +/// buflen - Length of |buffer| in bytes. If |buflen| is less +/// than the length of the form text string, |buffer| is +/// not modified. +/// Return Value: +/// Length in bytes for the text in the focused field. +int FORM_GetFocusedText(FPDF_FORMHANDLE hHandle, +FPDF_PAGE page, +ffi.Pointer buffer, +int buflen, +) { + return _FORM_GetFocusedText(hHandle, +page, +buffer, +buflen, +); +} + +late final _FORM_GetFocusedTextPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FORM_GetFocusedText'); +late final _FORM_GetFocusedText = _FORM_GetFocusedTextPtr.asFunction , int )>(); + +/// Function: FORM_GetSelectedText +/// Call this function to obtain selected text within a form text +/// field or form combobox text field. +/// Parameters: +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// page - Handle to the page, as returned by FPDF_LoadPage(). +/// buffer - Buffer for holding the selected text, encoded in +/// UTF-16LE. If NULL, |buffer| is not modified. +/// buflen - Length of |buffer| in bytes. If |buflen| is less +/// than the length of the selected text string, +/// |buffer| is not modified. +/// Return Value: +/// Length in bytes of selected text in form text field or form combobox +/// text field. +int FORM_GetSelectedText(FPDF_FORMHANDLE hHandle, +FPDF_PAGE page, +ffi.Pointer buffer, +int buflen, +) { + return _FORM_GetSelectedText(hHandle, +page, +buffer, +buflen, +); +} + +late final _FORM_GetSelectedTextPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FORM_GetSelectedText'); +late final _FORM_GetSelectedText = _FORM_GetSelectedTextPtr.asFunction , int )>(); + +/// Experimental API +/// Function: FORM_ReplaceAndKeepSelection +/// Call this function to replace the selected text in a form +/// text field or user-editable form combobox text field with another +/// text string (which can be empty or non-empty). If there is no +/// selected text, this function will append the replacement text after +/// the current caret position. After the insertion, the inserted text +/// will be selected. +/// Parameters: +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// page - Handle to the page, as Returned by FPDF_LoadPage(). +/// wsText - The text to be inserted, in UTF-16LE format. +/// Return Value: +/// None. +void FORM_ReplaceAndKeepSelection(FPDF_FORMHANDLE hHandle, +FPDF_PAGE page, +FPDF_WIDESTRING wsText, +) { + return _FORM_ReplaceAndKeepSelection(hHandle, +page, +wsText, +); +} + +late final _FORM_ReplaceAndKeepSelectionPtr = _lookup< + ffi.NativeFunction>('FORM_ReplaceAndKeepSelection'); +late final _FORM_ReplaceAndKeepSelection = _FORM_ReplaceAndKeepSelectionPtr.asFunction(); + +/// Function: FORM_ReplaceSelection +/// Call this function to replace the selected text in a form +/// text field or user-editable form combobox text field with another +/// text string (which can be empty or non-empty). If there is no +/// selected text, this function will append the replacement text after +/// the current caret position. After the insertion, the selection range +/// will be set to empty. +/// Parameters: +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// page - Handle to the page, as Returned by FPDF_LoadPage(). +/// wsText - The text to be inserted, in UTF-16LE format. +/// Return Value: +/// None. +void FORM_ReplaceSelection(FPDF_FORMHANDLE hHandle, +FPDF_PAGE page, +FPDF_WIDESTRING wsText, +) { + return _FORM_ReplaceSelection(hHandle, +page, +wsText, +); +} + +late final _FORM_ReplaceSelectionPtr = _lookup< + ffi.NativeFunction>('FORM_ReplaceSelection'); +late final _FORM_ReplaceSelection = _FORM_ReplaceSelectionPtr.asFunction(); + +/// Experimental API +/// Function: FORM_SelectAllText +/// Call this function to select all the text within the currently focused +/// form text field or form combobox text field. +/// Parameters: +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// page - Handle to the page, as returned by FPDF_LoadPage(). +/// Return Value: +/// Whether the operation succeeded or not. +int FORM_SelectAllText(FPDF_FORMHANDLE hHandle, +FPDF_PAGE page, +) { + return _FORM_SelectAllText(hHandle, +page, +); +} + +late final _FORM_SelectAllTextPtr = _lookup< + ffi.NativeFunction>('FORM_SelectAllText'); +late final _FORM_SelectAllText = _FORM_SelectAllTextPtr.asFunction(); + +/// Function: FORM_CanUndo +/// Find out if it is possible for the current focused widget in a given +/// form to perform an undo operation. +/// Parameters: +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// page - Handle to the page, as returned by FPDF_LoadPage(). +/// Return Value: +/// True if it is possible to undo. +int FORM_CanUndo(FPDF_FORMHANDLE hHandle, +FPDF_PAGE page, +) { + return _FORM_CanUndo(hHandle, +page, +); +} + +late final _FORM_CanUndoPtr = _lookup< + ffi.NativeFunction>('FORM_CanUndo'); +late final _FORM_CanUndo = _FORM_CanUndoPtr.asFunction(); + +/// Function: FORM_CanRedo +/// Find out if it is possible for the current focused widget in a given +/// form to perform a redo operation. +/// Parameters: +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// page - Handle to the page, as returned by FPDF_LoadPage(). +/// Return Value: +/// True if it is possible to redo. +int FORM_CanRedo(FPDF_FORMHANDLE hHandle, +FPDF_PAGE page, +) { + return _FORM_CanRedo(hHandle, +page, +); +} + +late final _FORM_CanRedoPtr = _lookup< + ffi.NativeFunction>('FORM_CanRedo'); +late final _FORM_CanRedo = _FORM_CanRedoPtr.asFunction(); + +/// Function: FORM_Undo +/// Make the current focused widget perform an undo operation. +/// Parameters: +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// page - Handle to the page, as returned by FPDF_LoadPage(). +/// Return Value: +/// True if the undo operation succeeded. +int FORM_Undo(FPDF_FORMHANDLE hHandle, +FPDF_PAGE page, +) { + return _FORM_Undo(hHandle, +page, +); +} + +late final _FORM_UndoPtr = _lookup< + ffi.NativeFunction>('FORM_Undo'); +late final _FORM_Undo = _FORM_UndoPtr.asFunction(); + +/// Function: FORM_Redo +/// Make the current focused widget perform a redo operation. +/// Parameters: +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// page - Handle to the page, as returned by FPDF_LoadPage(). +/// Return Value: +/// True if the redo operation succeeded. +int FORM_Redo(FPDF_FORMHANDLE hHandle, +FPDF_PAGE page, +) { + return _FORM_Redo(hHandle, +page, +); +} + +late final _FORM_RedoPtr = _lookup< + ffi.NativeFunction>('FORM_Redo'); +late final _FORM_Redo = _FORM_RedoPtr.asFunction(); + +/// Function: FORM_ForceToKillFocus. +/// Call this member function to force to kill the focus of the form +/// field which has focus. If it would kill the focus of a form field, +/// save the value of form field if was changed by theuser. +/// Parameters: +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// Return Value: +/// True indicates success; otherwise false. +int FORM_ForceToKillFocus(FPDF_FORMHANDLE hHandle, +) { + return _FORM_ForceToKillFocus(hHandle, +); +} + +late final _FORM_ForceToKillFocusPtr = _lookup< + ffi.NativeFunction>('FORM_ForceToKillFocus'); +late final _FORM_ForceToKillFocus = _FORM_ForceToKillFocusPtr.asFunction(); + +/// Experimental API. +/// Function: FORM_GetFocusedAnnot. +/// Call this member function to get the currently focused annotation. +/// Parameters: +/// handle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// page_index - Buffer to hold the index number of the page which +/// contains the focused annotation. 0 for the first page. +/// Can't be NULL. +/// annot - Buffer to hold the focused annotation. Can't be NULL. +/// Return Value: +/// On success, return true and write to the out parameters. Otherwise +/// return false and leave the out parameters unmodified. +/// Comments: +/// Not currently supported for XFA forms - will report no focused +/// annotation. +/// Must call FPDFPage_CloseAnnot() when the annotation returned in |annot| +/// by this function is no longer needed. +/// This will return true and set |page_index| to -1 and |annot| to NULL, +/// if there is no focused annotation. +int FORM_GetFocusedAnnot(FPDF_FORMHANDLE handle, +ffi.Pointer page_index, +ffi.Pointer annot, +) { + return _FORM_GetFocusedAnnot(handle, +page_index, +annot, +); +} + +late final _FORM_GetFocusedAnnotPtr = _lookup< + ffi.NativeFunction , ffi.Pointer )>>('FORM_GetFocusedAnnot'); +late final _FORM_GetFocusedAnnot = _FORM_GetFocusedAnnotPtr.asFunction , ffi.Pointer )>(); + +/// Experimental API. +/// Function: FORM_SetFocusedAnnot. +/// Call this member function to set the currently focused annotation. +/// Parameters: +/// handle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// annot - Handle to an annotation. +/// Return Value: +/// True indicates success; otherwise false. +/// Comments: +/// |annot| can't be NULL. To kill focus, use FORM_ForceToKillFocus() +/// instead. +int FORM_SetFocusedAnnot(FPDF_FORMHANDLE handle, +FPDF_ANNOTATION annot, +) { + return _FORM_SetFocusedAnnot(handle, +annot, +); +} + +late final _FORM_SetFocusedAnnotPtr = _lookup< + ffi.NativeFunction>('FORM_SetFocusedAnnot'); +late final _FORM_SetFocusedAnnot = _FORM_SetFocusedAnnotPtr.asFunction(); + +/// Function: FPDFPage_HasFormFieldAtPoint +/// Get the form field type by point. +/// Parameters: +/// hHandle - Handle to the form fill module. Returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// page - Handle to the page. Returned by FPDF_LoadPage(). +/// page_x - X position in PDF "user space". +/// page_y - Y position in PDF "user space". +/// Return Value: +/// Return the type of the form field; -1 indicates no field. +/// See field types above. +int FPDFPage_HasFormFieldAtPoint(FPDF_FORMHANDLE hHandle, +FPDF_PAGE page, +double page_x, +double page_y, +) { + return _FPDFPage_HasFormFieldAtPoint(hHandle, +page, +page_x, +page_y, +); +} + +late final _FPDFPage_HasFormFieldAtPointPtr = _lookup< + ffi.NativeFunction>('FPDFPage_HasFormFieldAtPoint'); +late final _FPDFPage_HasFormFieldAtPoint = _FPDFPage_HasFormFieldAtPointPtr.asFunction(); + +/// Function: FPDFPage_FormFieldZOrderAtPoint +/// Get the form field z-order by point. +/// Parameters: +/// hHandle - Handle to the form fill module. Returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// page - Handle to the page. Returned by FPDF_LoadPage(). +/// page_x - X position in PDF "user space". +/// page_y - Y position in PDF "user space". +/// Return Value: +/// Return the z-order of the form field; -1 indicates no field. +/// Higher numbers are closer to the front. +int FPDFPage_FormFieldZOrderAtPoint(FPDF_FORMHANDLE hHandle, +FPDF_PAGE page, +double page_x, +double page_y, +) { + return _FPDFPage_FormFieldZOrderAtPoint(hHandle, +page, +page_x, +page_y, +); +} + +late final _FPDFPage_FormFieldZOrderAtPointPtr = _lookup< + ffi.NativeFunction>('FPDFPage_FormFieldZOrderAtPoint'); +late final _FPDFPage_FormFieldZOrderAtPoint = _FPDFPage_FormFieldZOrderAtPointPtr.asFunction(); + +/// Function: FPDF_SetFormFieldHighlightColor +/// Set the highlight color of the specified (or all) form fields +/// in the document. +/// Parameters: +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// doc - Handle to the document, as returned by +/// FPDF_LoadDocument(). +/// fieldType - A 32-bit integer indicating the type of a form +/// field (defined above). +/// color - The highlight color of the form field. Constructed by +/// 0xxxrrggbb. +/// Return Value: +/// None. +/// Comments: +/// When the parameter fieldType is set to FPDF_FORMFIELD_UNKNOWN, the +/// highlight color will be applied to all the form fields in the +/// document. +/// Please refresh the client window to show the highlight immediately +/// if necessary. +void FPDF_SetFormFieldHighlightColor(FPDF_FORMHANDLE hHandle, +int fieldType, +int color, +) { + return _FPDF_SetFormFieldHighlightColor(hHandle, +fieldType, +color, +); +} + +late final _FPDF_SetFormFieldHighlightColorPtr = _lookup< + ffi.NativeFunction>('FPDF_SetFormFieldHighlightColor'); +late final _FPDF_SetFormFieldHighlightColor = _FPDF_SetFormFieldHighlightColorPtr.asFunction(); + +/// Function: FPDF_SetFormFieldHighlightAlpha +/// Set the transparency of the form field highlight color in the +/// document. +/// Parameters: +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// doc - Handle to the document, as returaned by +/// FPDF_LoadDocument(). +/// alpha - The transparency of the form field highlight color, +/// between 0-255. +/// Return Value: +/// None. +void FPDF_SetFormFieldHighlightAlpha(FPDF_FORMHANDLE hHandle, +int alpha, +) { + return _FPDF_SetFormFieldHighlightAlpha(hHandle, +alpha, +); +} + +late final _FPDF_SetFormFieldHighlightAlphaPtr = _lookup< + ffi.NativeFunction>('FPDF_SetFormFieldHighlightAlpha'); +late final _FPDF_SetFormFieldHighlightAlpha = _FPDF_SetFormFieldHighlightAlphaPtr.asFunction(); + +/// Function: FPDF_RemoveFormFieldHighlight +/// Remove the form field highlight color in the document. +/// Parameters: +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// Return Value: +/// None. +/// Comments: +/// Please refresh the client window to remove the highlight immediately +/// if necessary. +void FPDF_RemoveFormFieldHighlight(FPDF_FORMHANDLE hHandle, +) { + return _FPDF_RemoveFormFieldHighlight(hHandle, +); +} + +late final _FPDF_RemoveFormFieldHighlightPtr = _lookup< + ffi.NativeFunction>('FPDF_RemoveFormFieldHighlight'); +late final _FPDF_RemoveFormFieldHighlight = _FPDF_RemoveFormFieldHighlightPtr.asFunction(); + +/// Function: FPDF_FFLDraw +/// Render FormFields and popup window on a page to a device independent +/// bitmap. +/// Parameters: +/// hHandle - Handle to the form fill module, as returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// bitmap - Handle to the device independent bitmap (as the +/// output buffer). Bitmap handles can be created by +/// FPDFBitmap_Create(). +/// page - Handle to the page, as returned by FPDF_LoadPage(). +/// start_x - Left pixel position of the display area in the +/// device coordinates. +/// start_y - Top pixel position of the display area in the device +/// coordinates. +/// size_x - Horizontal size (in pixels) for displaying the page. +/// size_y - Vertical size (in pixels) for displaying the page. +/// rotate - Page orientation: 0 (normal), 1 (rotated 90 degrees +/// clockwise), 2 (rotated 180 degrees), 3 (rotated 90 +/// degrees counter-clockwise). +/// flags - 0 for normal display, or combination of flags +/// defined above. +/// Return Value: +/// None. +/// Comments: +/// This function is designed to render annotations that are +/// user-interactive, which are widget annotations (for FormFields) and +/// popup annotations. +/// With the FPDF_ANNOT flag, this function will render a popup annotation +/// when users mouse-hover on a non-widget annotation. Regardless of +/// FPDF_ANNOT flag, this function will always render widget annotations +/// for FormFields. +/// In order to implement the FormFill functions, implementation should +/// call this function after rendering functions, such as +/// FPDF_RenderPageBitmap() or FPDF_RenderPageBitmap_Start(), have +/// finished rendering the page contents. +void FPDF_FFLDraw(FPDF_FORMHANDLE hHandle, +FPDF_BITMAP bitmap, +FPDF_PAGE page, +int start_x, +int start_y, +int size_x, +int size_y, +int rotate, +int flags, +) { + return _FPDF_FFLDraw(hHandle, +bitmap, +page, +start_x, +start_y, +size_x, +size_y, +rotate, +flags, +); +} + +late final _FPDF_FFLDrawPtr = _lookup< + ffi.NativeFunction>('FPDF_FFLDraw'); +late final _FPDF_FFLDraw = _FPDF_FFLDrawPtr.asFunction(); + +/// Experimental API +/// Function: FPDF_GetFormType +/// Returns the type of form contained in the PDF document. +/// Parameters: +/// document - Handle to document. +/// Return Value: +/// Integer value representing one of the FORMTYPE_ values. +/// Comments: +/// If |document| is NULL, then the return value is FORMTYPE_NONE. +int FPDF_GetFormType(FPDF_DOCUMENT document, +) { + return _FPDF_GetFormType(document, +); +} + +late final _FPDF_GetFormTypePtr = _lookup< + ffi.NativeFunction>('FPDF_GetFormType'); +late final _FPDF_GetFormType = _FPDF_GetFormTypePtr.asFunction(); + +/// Experimental API +/// Function: FORM_SetIndexSelected +/// Selects/deselects the value at the given |index| of the focused +/// annotation. +/// Parameters: +/// hHandle - Handle to the form fill module. Returned by +/// FPDFDOC_InitFormFillEnvironment. +/// page - Handle to the page. Returned by FPDF_LoadPage +/// index - 0-based index of value to be set as +/// selected/unselected +/// selected - true to select, false to deselect +/// Return Value: +/// TRUE if the operation succeeded. +/// FALSE if the operation failed or widget is not a supported type. +/// Comments: +/// Intended for use with listbox/combobox widget types. Comboboxes +/// have at most a single value selected at a time which cannot be +/// deselected. Deselect on a combobox is a no-op that returns false. +/// Default implementation is a no-op that will return false for +/// other types. +/// Not currently supported for XFA forms - will return false. +int FORM_SetIndexSelected(FPDF_FORMHANDLE hHandle, +FPDF_PAGE page, +int index, +int selected, +) { + return _FORM_SetIndexSelected(hHandle, +page, +index, +selected, +); +} + +late final _FORM_SetIndexSelectedPtr = _lookup< + ffi.NativeFunction>('FORM_SetIndexSelected'); +late final _FORM_SetIndexSelected = _FORM_SetIndexSelectedPtr.asFunction(); + +/// Experimental API +/// Function: FORM_IsIndexSelected +/// Returns whether or not the value at |index| of the focused +/// annotation is currently selected. +/// Parameters: +/// hHandle - Handle to the form fill module. Returned by +/// FPDFDOC_InitFormFillEnvironment. +/// page - Handle to the page. Returned by FPDF_LoadPage +/// index - 0-based Index of value to check +/// Return Value: +/// TRUE if value at |index| is currently selected. +/// FALSE if value at |index| is not selected or widget is not a +/// supported type. +/// Comments: +/// Intended for use with listbox/combobox widget types. Default +/// implementation is a no-op that will return false for other types. +/// Not currently supported for XFA forms - will return false. +int FORM_IsIndexSelected(FPDF_FORMHANDLE hHandle, +FPDF_PAGE page, +int index, +) { + return _FORM_IsIndexSelected(hHandle, +page, +index, +); +} + +late final _FORM_IsIndexSelectedPtr = _lookup< + ffi.NativeFunction>('FORM_IsIndexSelected'); +late final _FORM_IsIndexSelected = _FORM_IsIndexSelectedPtr.asFunction(); + +/// Function: FPDF_LoadXFA +/// If the document consists of XFA fields, call this method to +/// attempt to load XFA fields. +/// Parameters: +/// document - Handle to document from FPDF_LoadDocument(). +/// Return Value: +/// TRUE upon success, otherwise FALSE. If XFA support is not built +/// into PDFium, performs no action and always returns FALSE. +int FPDF_LoadXFA(FPDF_DOCUMENT document, +) { + return _FPDF_LoadXFA(document, +); +} + +late final _FPDF_LoadXFAPtr = _lookup< + ffi.NativeFunction>('FPDF_LoadXFA'); +late final _FPDF_LoadXFA = _FPDF_LoadXFAPtr.asFunction(); + +/// Experimental API. +/// Check if an annotation subtype is currently supported for creation. +/// Currently supported subtypes: +/// - circle +/// - fileattachment +/// - freetext +/// - highlight +/// - ink +/// - link +/// - popup +/// - square, +/// - squiggly +/// - stamp +/// - strikeout +/// - text +/// - underline +/// +/// subtype - the subtype to be checked. +/// +/// Returns true if this subtype supported. +int FPDFAnnot_IsSupportedSubtype(int subtype, +) { + return _FPDFAnnot_IsSupportedSubtype(subtype, +); +} + +late final _FPDFAnnot_IsSupportedSubtypePtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_IsSupportedSubtype'); +late final _FPDFAnnot_IsSupportedSubtype = _FPDFAnnot_IsSupportedSubtypePtr.asFunction(); + +/// Experimental API. +/// Create an annotation in |page| of the subtype |subtype|. If the specified +/// subtype is illegal or unsupported, then a new annotation will not be created. +/// Must call FPDFPage_CloseAnnot() when the annotation returned by this +/// function is no longer needed. +/// +/// page - handle to a page. +/// subtype - the subtype of the new annotation. +/// +/// Returns a handle to the new annotation object, or NULL on failure. +FPDF_ANNOTATION FPDFPage_CreateAnnot(FPDF_PAGE page, +int subtype, +) { + return _FPDFPage_CreateAnnot(page, +subtype, +); +} + +late final _FPDFPage_CreateAnnotPtr = _lookup< + ffi.NativeFunction>('FPDFPage_CreateAnnot'); +late final _FPDFPage_CreateAnnot = _FPDFPage_CreateAnnotPtr.asFunction(); + +/// Experimental API. +/// Get the number of annotations in |page|. +/// +/// page - handle to a page. +/// +/// Returns the number of annotations in |page|. +int FPDFPage_GetAnnotCount(FPDF_PAGE page, +) { + return _FPDFPage_GetAnnotCount(page, +); +} + +late final _FPDFPage_GetAnnotCountPtr = _lookup< + ffi.NativeFunction>('FPDFPage_GetAnnotCount'); +late final _FPDFPage_GetAnnotCount = _FPDFPage_GetAnnotCountPtr.asFunction(); + +/// Experimental API. +/// Get annotation in |page| at |index|. Must call FPDFPage_CloseAnnot() when the +/// annotation returned by this function is no longer needed. +/// +/// page - handle to a page. +/// index - the index of the annotation. +/// +/// Returns a handle to the annotation object, or NULL on failure. +FPDF_ANNOTATION FPDFPage_GetAnnot(FPDF_PAGE page, +int index, +) { + return _FPDFPage_GetAnnot(page, +index, +); +} + +late final _FPDFPage_GetAnnotPtr = _lookup< + ffi.NativeFunction>('FPDFPage_GetAnnot'); +late final _FPDFPage_GetAnnot = _FPDFPage_GetAnnotPtr.asFunction(); + +/// Experimental API. +/// Get the index of |annot| in |page|. This is the opposite of +/// FPDFPage_GetAnnot(). +/// +/// page - handle to the page that the annotation is on. +/// annot - handle to an annotation. +/// +/// Returns the index of |annot|, or -1 on failure. +int FPDFPage_GetAnnotIndex(FPDF_PAGE page, +FPDF_ANNOTATION annot, +) { + return _FPDFPage_GetAnnotIndex(page, +annot, +); +} + +late final _FPDFPage_GetAnnotIndexPtr = _lookup< + ffi.NativeFunction>('FPDFPage_GetAnnotIndex'); +late final _FPDFPage_GetAnnotIndex = _FPDFPage_GetAnnotIndexPtr.asFunction(); + +/// Experimental API. +/// Close an annotation. Must be called when the annotation returned by +/// FPDFPage_CreateAnnot() or FPDFPage_GetAnnot() is no longer needed. This +/// function does not remove the annotation from the document. +/// +/// annot - handle to an annotation. +void FPDFPage_CloseAnnot(FPDF_ANNOTATION annot, +) { + return _FPDFPage_CloseAnnot(annot, +); +} + +late final _FPDFPage_CloseAnnotPtr = _lookup< + ffi.NativeFunction>('FPDFPage_CloseAnnot'); +late final _FPDFPage_CloseAnnot = _FPDFPage_CloseAnnotPtr.asFunction(); + +/// Experimental API. +/// Remove the annotation in |page| at |index|. +/// +/// page - handle to a page. +/// index - the index of the annotation. +/// +/// Returns true if successful. +int FPDFPage_RemoveAnnot(FPDF_PAGE page, +int index, +) { + return _FPDFPage_RemoveAnnot(page, +index, +); +} + +late final _FPDFPage_RemoveAnnotPtr = _lookup< + ffi.NativeFunction>('FPDFPage_RemoveAnnot'); +late final _FPDFPage_RemoveAnnot = _FPDFPage_RemoveAnnotPtr.asFunction(); + +/// Experimental API. +/// Get the subtype of an annotation. +/// +/// annot - handle to an annotation. +/// +/// Returns the annotation subtype. +int FPDFAnnot_GetSubtype(FPDF_ANNOTATION annot, +) { + return _FPDFAnnot_GetSubtype(annot, +); +} + +late final _FPDFAnnot_GetSubtypePtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_GetSubtype'); +late final _FPDFAnnot_GetSubtype = _FPDFAnnot_GetSubtypePtr.asFunction(); + +/// Experimental API. +/// Check if an annotation subtype is currently supported for object extraction, +/// update, and removal. +/// Currently supported subtypes: ink and stamp. +/// +/// subtype - the subtype to be checked. +/// +/// Returns true if this subtype supported. +int FPDFAnnot_IsObjectSupportedSubtype(int subtype, +) { + return _FPDFAnnot_IsObjectSupportedSubtype(subtype, +); +} + +late final _FPDFAnnot_IsObjectSupportedSubtypePtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_IsObjectSupportedSubtype'); +late final _FPDFAnnot_IsObjectSupportedSubtype = _FPDFAnnot_IsObjectSupportedSubtypePtr.asFunction(); + +/// Experimental API. +/// Update |obj| in |annot|. |obj| must be in |annot| already and must have +/// been retrieved by FPDFAnnot_GetObject(). Currently, only ink and stamp +/// annotations are supported by this API. Also note that only path, image, and +/// text objects have APIs for modification; see FPDFPath_*(), FPDFText_*(), and +/// FPDFImageObj_*(). +/// +/// annot - handle to an annotation. +/// obj - handle to the object that |annot| needs to update. +/// +/// Return true if successful. +int FPDFAnnot_UpdateObject(FPDF_ANNOTATION annot, +FPDF_PAGEOBJECT obj, +) { + return _FPDFAnnot_UpdateObject(annot, +obj, +); +} + +late final _FPDFAnnot_UpdateObjectPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_UpdateObject'); +late final _FPDFAnnot_UpdateObject = _FPDFAnnot_UpdateObjectPtr.asFunction(); + +/// Experimental API. +/// Add a new InkStroke, represented by an array of points, to the InkList of +/// |annot|. The API creates an InkList if one doesn't already exist in |annot|. +/// This API works only for ink annotations. Please refer to ISO 32000-1:2008 +/// spec, section 12.5.6.13. +/// +/// annot - handle to an annotation. +/// points - pointer to a FS_POINTF array representing input points. +/// point_count - number of elements in |points| array. This should not exceed +/// the maximum value that can be represented by an int32_t). +/// +/// Returns the 0-based index at which the new InkStroke is added in the InkList +/// of the |annot|. Returns -1 on failure. +int FPDFAnnot_AddInkStroke(FPDF_ANNOTATION annot, +ffi.Pointer points, +int point_count, +) { + return _FPDFAnnot_AddInkStroke(annot, +points, +point_count, +); +} + +late final _FPDFAnnot_AddInkStrokePtr = _lookup< + ffi.NativeFunction , ffi.Size )>>('FPDFAnnot_AddInkStroke'); +late final _FPDFAnnot_AddInkStroke = _FPDFAnnot_AddInkStrokePtr.asFunction , int )>(); + +/// Experimental API. +/// Removes an InkList in |annot|. +/// This API works only for ink annotations. +/// +/// annot - handle to an annotation. +/// +/// Return true on successful removal of /InkList entry from context of the +/// non-null ink |annot|. Returns false on failure. +int FPDFAnnot_RemoveInkList(FPDF_ANNOTATION annot, +) { + return _FPDFAnnot_RemoveInkList(annot, +); +} + +late final _FPDFAnnot_RemoveInkListPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_RemoveInkList'); +late final _FPDFAnnot_RemoveInkList = _FPDFAnnot_RemoveInkListPtr.asFunction(); + +/// Experimental API. +/// Add |obj| to |annot|. |obj| must have been created by +/// FPDFPageObj_CreateNew{Path|Rect}() or FPDFPageObj_New{Text|Image}Obj(), and +/// will be owned by |annot|. Note that an |obj| cannot belong to more than one +/// |annot|. Currently, only ink and stamp annotations are supported by this API. +/// Also note that only path, image, and text objects have APIs for creation. +/// +/// annot - handle to an annotation. +/// obj - handle to the object that is to be added to |annot|. +/// +/// Return true if successful. +int FPDFAnnot_AppendObject(FPDF_ANNOTATION annot, +FPDF_PAGEOBJECT obj, +) { + return _FPDFAnnot_AppendObject(annot, +obj, +); +} + +late final _FPDFAnnot_AppendObjectPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_AppendObject'); +late final _FPDFAnnot_AppendObject = _FPDFAnnot_AppendObjectPtr.asFunction(); + +/// Experimental API. +/// Get the total number of objects in |annot|, including path objects, text +/// objects, external objects, image objects, and shading objects. +/// +/// annot - handle to an annotation. +/// +/// Returns the number of objects in |annot|. +int FPDFAnnot_GetObjectCount(FPDF_ANNOTATION annot, +) { + return _FPDFAnnot_GetObjectCount(annot, +); +} + +late final _FPDFAnnot_GetObjectCountPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_GetObjectCount'); +late final _FPDFAnnot_GetObjectCount = _FPDFAnnot_GetObjectCountPtr.asFunction(); + +/// Experimental API. +/// Get the object in |annot| at |index|. +/// +/// annot - handle to an annotation. +/// index - the index of the object. +/// +/// Return a handle to the object, or NULL on failure. +FPDF_PAGEOBJECT FPDFAnnot_GetObject(FPDF_ANNOTATION annot, +int index, +) { + return _FPDFAnnot_GetObject(annot, +index, +); +} + +late final _FPDFAnnot_GetObjectPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_GetObject'); +late final _FPDFAnnot_GetObject = _FPDFAnnot_GetObjectPtr.asFunction(); + +/// Experimental API. +/// Remove the object in |annot| at |index|. +/// +/// annot - handle to an annotation. +/// index - the index of the object to be removed. +/// +/// Return true if successful. +int FPDFAnnot_RemoveObject(FPDF_ANNOTATION annot, +int index, +) { + return _FPDFAnnot_RemoveObject(annot, +index, +); +} + +late final _FPDFAnnot_RemoveObjectPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_RemoveObject'); +late final _FPDFAnnot_RemoveObject = _FPDFAnnot_RemoveObjectPtr.asFunction(); + +/// Experimental API. +/// Set the color of an annotation. Fails when called on annotations with +/// appearance streams already defined; instead use +/// FPDFPageObj_Set{Stroke|Fill}Color(). +/// +/// annot - handle to an annotation. +/// type - type of the color to be set. +/// R, G, B - buffer to hold the RGB value of the color. Ranges from 0 to 255. +/// A - buffer to hold the opacity. Ranges from 0 to 255. +/// +/// Returns true if successful. +DartFPDF_BOOL FPDFAnnot_SetColor(FPDF_ANNOTATION annot, +FPDFANNOT_COLORTYPE type, +int R, +int G, +int B, +int A, +) { + return _FPDFAnnot_SetColor(annot, +type.value, +R, +G, +B, +A, +); +} + +late final _FPDFAnnot_SetColorPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_SetColor'); +late final _FPDFAnnot_SetColor = _FPDFAnnot_SetColorPtr.asFunction(); + +/// Experimental API. +/// Get the color of an annotation. If no color is specified, default to yellow +/// for highlight annotation, black for all else. Fails when called on +/// annotations with appearance streams already defined; instead use +/// FPDFPageObj_Get{Stroke|Fill}Color(). +/// +/// annot - handle to an annotation. +/// type - type of the color requested. +/// R, G, B - buffer to hold the RGB value of the color. Ranges from 0 to 255. +/// A - buffer to hold the opacity. Ranges from 0 to 255. +/// +/// Returns true if successful. +DartFPDF_BOOL FPDFAnnot_GetColor(FPDF_ANNOTATION annot, +FPDFANNOT_COLORTYPE type, +ffi.Pointer R, +ffi.Pointer G, +ffi.Pointer B, +ffi.Pointer A, +) { + return _FPDFAnnot_GetColor(annot, +type.value, +R, +G, +B, +A, +); +} + +late final _FPDFAnnot_GetColorPtr = _lookup< + ffi.NativeFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>>('FPDFAnnot_GetColor'); +late final _FPDFAnnot_GetColor = _FPDFAnnot_GetColorPtr.asFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>(); + +/// Experimental API. +/// Check if the annotation is of a type that has attachment points +/// (i.e. quadpoints). Quadpoints are the vertices of the rectangle that +/// encompasses the texts affected by the annotation. They provide the +/// coordinates in the page where the annotation is attached. Only text markup +/// annotations (i.e. highlight, strikeout, squiggly, and underline) and link +/// annotations have quadpoints. +/// +/// annot - handle to an annotation. +/// +/// Returns true if the annotation is of a type that has quadpoints, false +/// otherwise. +int FPDFAnnot_HasAttachmentPoints(FPDF_ANNOTATION annot, +) { + return _FPDFAnnot_HasAttachmentPoints(annot, +); +} + +late final _FPDFAnnot_HasAttachmentPointsPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_HasAttachmentPoints'); +late final _FPDFAnnot_HasAttachmentPoints = _FPDFAnnot_HasAttachmentPointsPtr.asFunction(); + +/// Experimental API. +/// Replace the attachment points (i.e. quadpoints) set of an annotation at +/// |quad_index|. This index needs to be within the result of +/// FPDFAnnot_CountAttachmentPoints(). +/// If the annotation's appearance stream is defined and this annotation is of a +/// type with quadpoints, then update the bounding box too if the new quadpoints +/// define a bigger one. +/// +/// annot - handle to an annotation. +/// quad_index - index of the set of quadpoints. +/// quad_points - the quadpoints to be set. +/// +/// Returns true if successful. +int FPDFAnnot_SetAttachmentPoints(FPDF_ANNOTATION annot, +int quad_index, +ffi.Pointer quad_points, +) { + return _FPDFAnnot_SetAttachmentPoints(annot, +quad_index, +quad_points, +); +} + +late final _FPDFAnnot_SetAttachmentPointsPtr = _lookup< + ffi.NativeFunction )>>('FPDFAnnot_SetAttachmentPoints'); +late final _FPDFAnnot_SetAttachmentPoints = _FPDFAnnot_SetAttachmentPointsPtr.asFunction )>(); + +/// Experimental API. +/// Append to the list of attachment points (i.e. quadpoints) of an annotation. +/// If the annotation's appearance stream is defined and this annotation is of a +/// type with quadpoints, then update the bounding box too if the new quadpoints +/// define a bigger one. +/// +/// annot - handle to an annotation. +/// quad_points - the quadpoints to be set. +/// +/// Returns true if successful. +int FPDFAnnot_AppendAttachmentPoints(FPDF_ANNOTATION annot, +ffi.Pointer quad_points, +) { + return _FPDFAnnot_AppendAttachmentPoints(annot, +quad_points, +); +} + +late final _FPDFAnnot_AppendAttachmentPointsPtr = _lookup< + ffi.NativeFunction )>>('FPDFAnnot_AppendAttachmentPoints'); +late final _FPDFAnnot_AppendAttachmentPoints = _FPDFAnnot_AppendAttachmentPointsPtr.asFunction )>(); + +/// Experimental API. +/// Get the number of sets of quadpoints of an annotation. +/// +/// annot - handle to an annotation. +/// +/// Returns the number of sets of quadpoints, or 0 on failure. +int FPDFAnnot_CountAttachmentPoints(FPDF_ANNOTATION annot, +) { + return _FPDFAnnot_CountAttachmentPoints(annot, +); +} + +late final _FPDFAnnot_CountAttachmentPointsPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_CountAttachmentPoints'); +late final _FPDFAnnot_CountAttachmentPoints = _FPDFAnnot_CountAttachmentPointsPtr.asFunction(); + +/// Experimental API. +/// Get the attachment points (i.e. quadpoints) of an annotation. +/// +/// annot - handle to an annotation. +/// quad_index - index of the set of quadpoints. +/// quad_points - receives the quadpoints; must not be NULL. +/// +/// Returns true if successful. +int FPDFAnnot_GetAttachmentPoints(FPDF_ANNOTATION annot, +int quad_index, +ffi.Pointer quad_points, +) { + return _FPDFAnnot_GetAttachmentPoints(annot, +quad_index, +quad_points, +); +} + +late final _FPDFAnnot_GetAttachmentPointsPtr = _lookup< + ffi.NativeFunction )>>('FPDFAnnot_GetAttachmentPoints'); +late final _FPDFAnnot_GetAttachmentPoints = _FPDFAnnot_GetAttachmentPointsPtr.asFunction )>(); + +/// Experimental API. +/// Set the annotation rectangle defining the location of the annotation. If the +/// annotation's appearance stream is defined and this annotation is of a type +/// without quadpoints, then update the bounding box too if the new rectangle +/// defines a bigger one. +/// +/// annot - handle to an annotation. +/// rect - the annotation rectangle to be set. +/// +/// Returns true if successful. +int FPDFAnnot_SetRect(FPDF_ANNOTATION annot, +ffi.Pointer rect, +) { + return _FPDFAnnot_SetRect(annot, +rect, +); +} + +late final _FPDFAnnot_SetRectPtr = _lookup< + ffi.NativeFunction )>>('FPDFAnnot_SetRect'); +late final _FPDFAnnot_SetRect = _FPDFAnnot_SetRectPtr.asFunction )>(); + +/// Experimental API. +/// Get the annotation rectangle defining the location of the annotation. +/// +/// annot - handle to an annotation. +/// rect - receives the rectangle; must not be NULL. +/// +/// Returns true if successful. +int FPDFAnnot_GetRect(FPDF_ANNOTATION annot, +ffi.Pointer rect, +) { + return _FPDFAnnot_GetRect(annot, +rect, +); +} + +late final _FPDFAnnot_GetRectPtr = _lookup< + ffi.NativeFunction )>>('FPDFAnnot_GetRect'); +late final _FPDFAnnot_GetRect = _FPDFAnnot_GetRectPtr.asFunction )>(); + +/// Experimental API. +/// Get the vertices of a polygon or polyline annotation. |buffer| is an array of +/// points of the annotation. If |length| is less than the returned length, or +/// |annot| or |buffer| is NULL, |buffer| will not be modified. +/// +/// annot - handle to an annotation, as returned by e.g. FPDFPage_GetAnnot() +/// buffer - buffer for holding the points. +/// length - length of the buffer in points. +/// +/// Returns the number of points if the annotation is of type polygon or +/// polyline, 0 otherwise. +int FPDFAnnot_GetVertices(FPDF_ANNOTATION annot, +ffi.Pointer buffer, +int length, +) { + return _FPDFAnnot_GetVertices(annot, +buffer, +length, +); +} + +late final _FPDFAnnot_GetVerticesPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFAnnot_GetVertices'); +late final _FPDFAnnot_GetVertices = _FPDFAnnot_GetVerticesPtr.asFunction , int )>(); + +/// Experimental API. +/// Get the number of paths in the ink list of an ink annotation. +/// +/// annot - handle to an annotation, as returned by e.g. FPDFPage_GetAnnot() +/// +/// Returns the number of paths in the ink list if the annotation is of type ink, +/// 0 otherwise. +int FPDFAnnot_GetInkListCount(FPDF_ANNOTATION annot, +) { + return _FPDFAnnot_GetInkListCount(annot, +); +} + +late final _FPDFAnnot_GetInkListCountPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_GetInkListCount'); +late final _FPDFAnnot_GetInkListCount = _FPDFAnnot_GetInkListCountPtr.asFunction(); + +/// Experimental API. +/// Get a path in the ink list of an ink annotation. |buffer| is an array of +/// points of the path. If |length| is less than the returned length, or |annot| +/// or |buffer| is NULL, |buffer| will not be modified. +/// +/// annot - handle to an annotation, as returned by e.g. FPDFPage_GetAnnot() +/// path_index - index of the path +/// buffer - buffer for holding the points. +/// length - length of the buffer in points. +/// +/// Returns the number of points of the path if the annotation is of type ink, 0 +/// otherwise. +int FPDFAnnot_GetInkListPath(FPDF_ANNOTATION annot, +int path_index, +ffi.Pointer buffer, +int length, +) { + return _FPDFAnnot_GetInkListPath(annot, +path_index, +buffer, +length, +); +} + +late final _FPDFAnnot_GetInkListPathPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFAnnot_GetInkListPath'); +late final _FPDFAnnot_GetInkListPath = _FPDFAnnot_GetInkListPathPtr.asFunction , int )>(); + +/// Experimental API. +/// Get the starting and ending coordinates of a line annotation. +/// +/// annot - handle to an annotation, as returned by e.g. FPDFPage_GetAnnot() +/// start - starting point +/// end - ending point +/// +/// Returns true if the annotation is of type line, |start| and |end| are not +/// NULL, false otherwise. +int FPDFAnnot_GetLine(FPDF_ANNOTATION annot, +ffi.Pointer start, +ffi.Pointer end, +) { + return _FPDFAnnot_GetLine(annot, +start, +end, +); +} + +late final _FPDFAnnot_GetLinePtr = _lookup< + ffi.NativeFunction , ffi.Pointer )>>('FPDFAnnot_GetLine'); +late final _FPDFAnnot_GetLine = _FPDFAnnot_GetLinePtr.asFunction , ffi.Pointer )>(); + +/// Experimental API. +/// Set the characteristics of the annotation's border (rounded rectangle). +/// +/// annot - handle to an annotation +/// horizontal_radius - horizontal corner radius, in default user space units +/// vertical_radius - vertical corner radius, in default user space units +/// border_width - border width, in default user space units +/// +/// Returns true if setting the border for |annot| succeeds, false otherwise. +/// +/// If |annot| contains an appearance stream that overrides the border values, +/// then the appearance stream will be removed on success. +int FPDFAnnot_SetBorder(FPDF_ANNOTATION annot, +double horizontal_radius, +double vertical_radius, +double border_width, +) { + return _FPDFAnnot_SetBorder(annot, +horizontal_radius, +vertical_radius, +border_width, +); +} + +late final _FPDFAnnot_SetBorderPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_SetBorder'); +late final _FPDFAnnot_SetBorder = _FPDFAnnot_SetBorderPtr.asFunction(); + +/// Experimental API. +/// Get the characteristics of the annotation's border (rounded rectangle). +/// +/// annot - handle to an annotation +/// horizontal_radius - horizontal corner radius, in default user space units +/// vertical_radius - vertical corner radius, in default user space units +/// border_width - border width, in default user space units +/// +/// Returns true if |horizontal_radius|, |vertical_radius| and |border_width| are +/// not NULL, false otherwise. +int FPDFAnnot_GetBorder(FPDF_ANNOTATION annot, +ffi.Pointer horizontal_radius, +ffi.Pointer vertical_radius, +ffi.Pointer border_width, +) { + return _FPDFAnnot_GetBorder(annot, +horizontal_radius, +vertical_radius, +border_width, +); +} + +late final _FPDFAnnot_GetBorderPtr = _lookup< + ffi.NativeFunction , ffi.Pointer , ffi.Pointer )>>('FPDFAnnot_GetBorder'); +late final _FPDFAnnot_GetBorder = _FPDFAnnot_GetBorderPtr.asFunction , ffi.Pointer , ffi.Pointer )>(); + +/// Experimental API. +/// Get the JavaScript of an event of the annotation's additional actions. +/// |buffer| is only modified if |buflen| is large enough to hold the whole +/// JavaScript string. If |buflen| is smaller, the total size of the JavaScript +/// is still returned, but nothing is copied. If there is no JavaScript for +/// |event| in |annot|, an empty string is written to |buf| and 2 is returned, +/// denoting the size of the null terminator in the buffer. On other errors, +/// nothing is written to |buffer| and 0 is returned. +/// +/// hHandle - handle to the form fill module, returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// annot - handle to an interactive form annotation. +/// event - event type, one of the FPDF_ANNOT_AACTION_* values. +/// buffer - buffer for holding the value string, encoded in UTF-16LE. +/// buflen - length of the buffer in bytes. +/// +/// Returns the length of the string value in bytes, including the 2-byte +/// null terminator. +int FPDFAnnot_GetFormAdditionalActionJavaScript(FPDF_FORMHANDLE hHandle, +FPDF_ANNOTATION annot, +int event, +ffi.Pointer buffer, +int buflen, +) { + return _FPDFAnnot_GetFormAdditionalActionJavaScript(hHandle, +annot, +event, +buffer, +buflen, +); +} + +late final _FPDFAnnot_GetFormAdditionalActionJavaScriptPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFAnnot_GetFormAdditionalActionJavaScript'); +late final _FPDFAnnot_GetFormAdditionalActionJavaScript = _FPDFAnnot_GetFormAdditionalActionJavaScriptPtr.asFunction , int )>(); + +/// Experimental API. +/// Check if |annot|'s dictionary has |key| as a key. +/// +/// annot - handle to an annotation. +/// key - the key to look for, encoded in UTF-8. +/// +/// Returns true if |key| exists. +int FPDFAnnot_HasKey(FPDF_ANNOTATION annot, +FPDF_BYTESTRING key, +) { + return _FPDFAnnot_HasKey(annot, +key, +); +} + +late final _FPDFAnnot_HasKeyPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_HasKey'); +late final _FPDFAnnot_HasKey = _FPDFAnnot_HasKeyPtr.asFunction(); + +/// Experimental API. +/// Get the type of the value corresponding to |key| in |annot|'s dictionary. +/// +/// annot - handle to an annotation. +/// key - the key to look for, encoded in UTF-8. +/// +/// Returns the type of the dictionary value. +int FPDFAnnot_GetValueType(FPDF_ANNOTATION annot, +FPDF_BYTESTRING key, +) { + return _FPDFAnnot_GetValueType(annot, +key, +); +} + +late final _FPDFAnnot_GetValueTypePtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_GetValueType'); +late final _FPDFAnnot_GetValueType = _FPDFAnnot_GetValueTypePtr.asFunction(); + +/// Experimental API. +/// Set the string value corresponding to |key| in |annot|'s dictionary, +/// overwriting the existing value if any. The value type would be +/// FPDF_OBJECT_STRING after this function call succeeds. +/// +/// annot - handle to an annotation. +/// key - the key to the dictionary entry to be set, encoded in UTF-8. +/// value - the string value to be set, encoded in UTF-16LE. +/// +/// Returns true if successful. +int FPDFAnnot_SetStringValue(FPDF_ANNOTATION annot, +FPDF_BYTESTRING key, +FPDF_WIDESTRING value, +) { + return _FPDFAnnot_SetStringValue(annot, +key, +value, +); +} + +late final _FPDFAnnot_SetStringValuePtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_SetStringValue'); +late final _FPDFAnnot_SetStringValue = _FPDFAnnot_SetStringValuePtr.asFunction(); + +/// Experimental API. +/// Get the string value corresponding to |key| in |annot|'s dictionary. |buffer| +/// is only modified if |buflen| is longer than the length of contents. Note that +/// if |key| does not exist in the dictionary or if |key|'s corresponding value +/// in the dictionary is not a string (i.e. the value is not of type +/// FPDF_OBJECT_STRING or FPDF_OBJECT_NAME), then an empty string would be copied +/// to |buffer| and the return value would be 2. On other errors, nothing would +/// be added to |buffer| and the return value would be 0. +/// +/// annot - handle to an annotation. +/// key - the key to the requested dictionary entry, encoded in UTF-8. +/// buffer - buffer for holding the value string, encoded in UTF-16LE. +/// buflen - length of the buffer in bytes. +/// +/// Returns the length of the string value in bytes. +int FPDFAnnot_GetStringValue(FPDF_ANNOTATION annot, +FPDF_BYTESTRING key, +ffi.Pointer buffer, +int buflen, +) { + return _FPDFAnnot_GetStringValue(annot, +key, +buffer, +buflen, +); +} + +late final _FPDFAnnot_GetStringValuePtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFAnnot_GetStringValue'); +late final _FPDFAnnot_GetStringValue = _FPDFAnnot_GetStringValuePtr.asFunction , int )>(); + +/// Experimental API. +/// Get the float value corresponding to |key| in |annot|'s dictionary. Writes +/// value to |value| and returns True if |key| exists in the dictionary and +/// |key|'s corresponding value is a number (FPDF_OBJECT_NUMBER), False +/// otherwise. +/// +/// annot - handle to an annotation. +/// key - the key to the requested dictionary entry, encoded in UTF-8. +/// value - receives the value, must not be NULL. +/// +/// Returns True if value found, False otherwise. +int FPDFAnnot_GetNumberValue(FPDF_ANNOTATION annot, +FPDF_BYTESTRING key, +ffi.Pointer value, +) { + return _FPDFAnnot_GetNumberValue(annot, +key, +value, +); +} + +late final _FPDFAnnot_GetNumberValuePtr = _lookup< + ffi.NativeFunction )>>('FPDFAnnot_GetNumberValue'); +late final _FPDFAnnot_GetNumberValue = _FPDFAnnot_GetNumberValuePtr.asFunction )>(); + +/// Experimental API. +/// Set the AP (appearance string) in |annot|'s dictionary for a given +/// |appearanceMode|. +/// +/// annot - handle to an annotation. +/// appearanceMode - the appearance mode (normal, rollover or down) for which +/// to get the AP. +/// value - the string value to be set, encoded in UTF-16LE. If +/// nullptr is passed, the AP is cleared for that mode. If the +/// mode is Normal, APs for all modes are cleared. +/// +/// Returns true if successful. +int FPDFAnnot_SetAP(FPDF_ANNOTATION annot, +int appearanceMode, +FPDF_WIDESTRING value, +) { + return _FPDFAnnot_SetAP(annot, +appearanceMode, +value, +); +} + +late final _FPDFAnnot_SetAPPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_SetAP'); +late final _FPDFAnnot_SetAP = _FPDFAnnot_SetAPPtr.asFunction(); + +/// Experimental API. +/// Get the AP (appearance string) from |annot|'s dictionary for a given +/// |appearanceMode|. +/// |buffer| is only modified if |buflen| is large enough to hold the whole AP +/// string. If |buflen| is smaller, the total size of the AP is still returned, +/// but nothing is copied. +/// If there is no appearance stream for |annot| in |appearanceMode|, an empty +/// string is written to |buf| and 2 is returned. +/// On other errors, nothing is written to |buffer| and 0 is returned. +/// +/// annot - handle to an annotation. +/// appearanceMode - the appearance mode (normal, rollover or down) for which +/// to get the AP. +/// buffer - buffer for holding the value string, encoded in UTF-16LE. +/// buflen - length of the buffer in bytes. +/// +/// Returns the length of the string value in bytes. +int FPDFAnnot_GetAP(FPDF_ANNOTATION annot, +int appearanceMode, +ffi.Pointer buffer, +int buflen, +) { + return _FPDFAnnot_GetAP(annot, +appearanceMode, +buffer, +buflen, +); +} + +late final _FPDFAnnot_GetAPPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFAnnot_GetAP'); +late final _FPDFAnnot_GetAP = _FPDFAnnot_GetAPPtr.asFunction , int )>(); + +/// Experimental API. +/// Get the annotation corresponding to |key| in |annot|'s dictionary. Common +/// keys for linking annotations include "IRT" and "Popup". Must call +/// FPDFPage_CloseAnnot() when the annotation returned by this function is no +/// longer needed. +/// +/// annot - handle to an annotation. +/// key - the key to the requested dictionary entry, encoded in UTF-8. +/// +/// Returns a handle to the linked annotation object, or NULL on failure. +FPDF_ANNOTATION FPDFAnnot_GetLinkedAnnot(FPDF_ANNOTATION annot, +FPDF_BYTESTRING key, +) { + return _FPDFAnnot_GetLinkedAnnot(annot, +key, +); +} + +late final _FPDFAnnot_GetLinkedAnnotPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_GetLinkedAnnot'); +late final _FPDFAnnot_GetLinkedAnnot = _FPDFAnnot_GetLinkedAnnotPtr.asFunction(); + +/// Experimental API. +/// Get the annotation flags of |annot|. +/// +/// annot - handle to an annotation. +/// +/// Returns the annotation flags. +int FPDFAnnot_GetFlags(FPDF_ANNOTATION annot, +) { + return _FPDFAnnot_GetFlags(annot, +); +} + +late final _FPDFAnnot_GetFlagsPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_GetFlags'); +late final _FPDFAnnot_GetFlags = _FPDFAnnot_GetFlagsPtr.asFunction(); + +/// Experimental API. +/// Set the |annot|'s flags to be of the value |flags|. +/// +/// annot - handle to an annotation. +/// flags - the flag values to be set. +/// +/// Returns true if successful. +int FPDFAnnot_SetFlags(FPDF_ANNOTATION annot, +int flags, +) { + return _FPDFAnnot_SetFlags(annot, +flags, +); +} + +late final _FPDFAnnot_SetFlagsPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_SetFlags'); +late final _FPDFAnnot_SetFlags = _FPDFAnnot_SetFlagsPtr.asFunction(); + +/// Experimental API. +/// Get the annotation flags of |annot|. +/// +/// hHandle - handle to the form fill module, returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// annot - handle to an interactive form annotation. +/// +/// Returns the annotation flags specific to interactive forms. +int FPDFAnnot_GetFormFieldFlags(FPDF_FORMHANDLE handle, +FPDF_ANNOTATION annot, +) { + return _FPDFAnnot_GetFormFieldFlags(handle, +annot, +); +} + +late final _FPDFAnnot_GetFormFieldFlagsPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_GetFormFieldFlags'); +late final _FPDFAnnot_GetFormFieldFlags = _FPDFAnnot_GetFormFieldFlagsPtr.asFunction(); + +/// Experimental API. +/// Sets the form field flags for an interactive form annotation. +/// +/// handle - the handle to the form fill module, returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// annot - handle to an interactive form annotation. +/// flags - the form field flags to be set. +/// +/// Returns true if successful. +int FPDFAnnot_SetFormFieldFlags(FPDF_FORMHANDLE handle, +FPDF_ANNOTATION annot, +int flags, +) { + return _FPDFAnnot_SetFormFieldFlags(handle, +annot, +flags, +); +} + +late final _FPDFAnnot_SetFormFieldFlagsPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_SetFormFieldFlags'); +late final _FPDFAnnot_SetFormFieldFlags = _FPDFAnnot_SetFormFieldFlagsPtr.asFunction(); + +/// Experimental API. +/// Retrieves an interactive form annotation whose rectangle contains a given +/// point on a page. Must call FPDFPage_CloseAnnot() when the annotation returned +/// is no longer needed. +/// +/// +/// hHandle - handle to the form fill module, returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// page - handle to the page, returned by FPDF_LoadPage function. +/// point - position in PDF "user space". +/// +/// Returns the interactive form annotation whose rectangle contains the given +/// coordinates on the page. If there is no such annotation, return NULL. +FPDF_ANNOTATION FPDFAnnot_GetFormFieldAtPoint(FPDF_FORMHANDLE hHandle, +FPDF_PAGE page, +ffi.Pointer point, +) { + return _FPDFAnnot_GetFormFieldAtPoint(hHandle, +page, +point, +); +} + +late final _FPDFAnnot_GetFormFieldAtPointPtr = _lookup< + ffi.NativeFunction )>>('FPDFAnnot_GetFormFieldAtPoint'); +late final _FPDFAnnot_GetFormFieldAtPoint = _FPDFAnnot_GetFormFieldAtPointPtr.asFunction )>(); + +/// Experimental API. +/// Gets the name of |annot|, which is an interactive form annotation. +/// |buffer| is only modified if |buflen| is longer than the length of contents. +/// In case of error, nothing will be added to |buffer| and the return value will +/// be 0. Note that return value of empty string is 2 for "\0\0". +/// +/// hHandle - handle to the form fill module, returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// annot - handle to an interactive form annotation. +/// buffer - buffer for holding the name string, encoded in UTF-16LE. +/// buflen - length of the buffer in bytes. +/// +/// Returns the length of the string value in bytes. +int FPDFAnnot_GetFormFieldName(FPDF_FORMHANDLE hHandle, +FPDF_ANNOTATION annot, +ffi.Pointer buffer, +int buflen, +) { + return _FPDFAnnot_GetFormFieldName(hHandle, +annot, +buffer, +buflen, +); +} + +late final _FPDFAnnot_GetFormFieldNamePtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFAnnot_GetFormFieldName'); +late final _FPDFAnnot_GetFormFieldName = _FPDFAnnot_GetFormFieldNamePtr.asFunction , int )>(); + +/// Experimental API. +/// Gets the alternate name of |annot|, which is an interactive form annotation. +/// |buffer| is only modified if |buflen| is longer than the length of contents. +/// In case of error, nothing will be added to |buffer| and the return value will +/// be 0. Note that return value of empty string is 2 for "\0\0". +/// +/// hHandle - handle to the form fill module, returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// annot - handle to an interactive form annotation. +/// buffer - buffer for holding the alternate name string, encoded in +/// UTF-16LE. +/// buflen - length of the buffer in bytes. +/// +/// Returns the length of the string value in bytes. +int FPDFAnnot_GetFormFieldAlternateName(FPDF_FORMHANDLE hHandle, +FPDF_ANNOTATION annot, +ffi.Pointer buffer, +int buflen, +) { + return _FPDFAnnot_GetFormFieldAlternateName(hHandle, +annot, +buffer, +buflen, +); +} + +late final _FPDFAnnot_GetFormFieldAlternateNamePtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFAnnot_GetFormFieldAlternateName'); +late final _FPDFAnnot_GetFormFieldAlternateName = _FPDFAnnot_GetFormFieldAlternateNamePtr.asFunction , int )>(); + +/// Experimental API. +/// Gets the form field type of |annot|, which is an interactive form annotation. +/// +/// hHandle - handle to the form fill module, returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// annot - handle to an interactive form annotation. +/// +/// Returns the type of the form field (one of the FPDF_FORMFIELD_* values) on +/// success. Returns -1 on error. +/// See field types in fpdf_formfill.h. +int FPDFAnnot_GetFormFieldType(FPDF_FORMHANDLE hHandle, +FPDF_ANNOTATION annot, +) { + return _FPDFAnnot_GetFormFieldType(hHandle, +annot, +); +} + +late final _FPDFAnnot_GetFormFieldTypePtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_GetFormFieldType'); +late final _FPDFAnnot_GetFormFieldType = _FPDFAnnot_GetFormFieldTypePtr.asFunction(); + +/// Experimental API. +/// Gets the value of |annot|, which is an interactive form annotation. +/// |buffer| is only modified if |buflen| is longer than the length of contents. +/// In case of error, nothing will be added to |buffer| and the return value will +/// be 0. Note that return value of empty string is 2 for "\0\0". +/// +/// hHandle - handle to the form fill module, returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// annot - handle to an interactive form annotation. +/// buffer - buffer for holding the value string, encoded in UTF-16LE. +/// buflen - length of the buffer in bytes. +/// +/// Returns the length of the string value in bytes. +int FPDFAnnot_GetFormFieldValue(FPDF_FORMHANDLE hHandle, +FPDF_ANNOTATION annot, +ffi.Pointer buffer, +int buflen, +) { + return _FPDFAnnot_GetFormFieldValue(hHandle, +annot, +buffer, +buflen, +); +} + +late final _FPDFAnnot_GetFormFieldValuePtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFAnnot_GetFormFieldValue'); +late final _FPDFAnnot_GetFormFieldValue = _FPDFAnnot_GetFormFieldValuePtr.asFunction , int )>(); + +/// Experimental API. +/// Get the number of options in the |annot|'s "Opt" dictionary. Intended for +/// use with listbox and combobox widget annotations. +/// +/// hHandle - handle to the form fill module, returned by +/// FPDFDOC_InitFormFillEnvironment. +/// annot - handle to an annotation. +/// +/// Returns the number of options in "Opt" dictionary on success. Return value +/// will be -1 if annotation does not have an "Opt" dictionary or other error. +int FPDFAnnot_GetOptionCount(FPDF_FORMHANDLE hHandle, +FPDF_ANNOTATION annot, +) { + return _FPDFAnnot_GetOptionCount(hHandle, +annot, +); +} + +late final _FPDFAnnot_GetOptionCountPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_GetOptionCount'); +late final _FPDFAnnot_GetOptionCount = _FPDFAnnot_GetOptionCountPtr.asFunction(); + +/// Experimental API. +/// Get the string value for the label of the option at |index| in |annot|'s +/// "Opt" dictionary. Intended for use with listbox and combobox widget +/// annotations. |buffer| is only modified if |buflen| is longer than the length +/// of contents. If index is out of range or in case of other error, nothing +/// will be added to |buffer| and the return value will be 0. Note that +/// return value of empty string is 2 for "\0\0". +/// +/// hHandle - handle to the form fill module, returned by +/// FPDFDOC_InitFormFillEnvironment. +/// annot - handle to an annotation. +/// index - numeric index of the option in the "Opt" array +/// buffer - buffer for holding the value string, encoded in UTF-16LE. +/// buflen - length of the buffer in bytes. +/// +/// Returns the length of the string value in bytes. +/// If |annot| does not have an "Opt" array, |index| is out of range or if any +/// other error occurs, returns 0. +int FPDFAnnot_GetOptionLabel(FPDF_FORMHANDLE hHandle, +FPDF_ANNOTATION annot, +int index, +ffi.Pointer buffer, +int buflen, +) { + return _FPDFAnnot_GetOptionLabel(hHandle, +annot, +index, +buffer, +buflen, +); +} + +late final _FPDFAnnot_GetOptionLabelPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFAnnot_GetOptionLabel'); +late final _FPDFAnnot_GetOptionLabel = _FPDFAnnot_GetOptionLabelPtr.asFunction , int )>(); + +/// Experimental API. +/// Determine whether or not the option at |index| in |annot|'s "Opt" dictionary +/// is selected. Intended for use with listbox and combobox widget annotations. +/// +/// handle - handle to the form fill module, returned by +/// FPDFDOC_InitFormFillEnvironment. +/// annot - handle to an annotation. +/// index - numeric index of the option in the "Opt" array. +/// +/// Returns true if the option at |index| in |annot|'s "Opt" dictionary is +/// selected, false otherwise. +int FPDFAnnot_IsOptionSelected(FPDF_FORMHANDLE handle, +FPDF_ANNOTATION annot, +int index, +) { + return _FPDFAnnot_IsOptionSelected(handle, +annot, +index, +); +} + +late final _FPDFAnnot_IsOptionSelectedPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_IsOptionSelected'); +late final _FPDFAnnot_IsOptionSelected = _FPDFAnnot_IsOptionSelectedPtr.asFunction(); + +/// Experimental API. +/// Get the float value of the font size for an |annot| with variable text. +/// If 0, the font is to be auto-sized: its size is computed as a function of +/// the height of the annotation rectangle. +/// +/// hHandle - handle to the form fill module, returned by +/// FPDFDOC_InitFormFillEnvironment. +/// annot - handle to an annotation. +/// value - Required. Float which will be set to font size on success. +/// +/// Returns true if the font size was set in |value|, false on error or if +/// |value| not provided. +int FPDFAnnot_GetFontSize(FPDF_FORMHANDLE hHandle, +FPDF_ANNOTATION annot, +ffi.Pointer value, +) { + return _FPDFAnnot_GetFontSize(hHandle, +annot, +value, +); +} + +late final _FPDFAnnot_GetFontSizePtr = _lookup< + ffi.NativeFunction )>>('FPDFAnnot_GetFontSize'); +late final _FPDFAnnot_GetFontSize = _FPDFAnnot_GetFontSizePtr.asFunction )>(); + +/// Experimental API. +/// Set the text color of an annotation. +/// +/// handle - handle to the form fill module, returned by +/// FPDFDOC_InitFormFillEnvironment. +/// annot - handle to an annotation. +/// R - the red component for the text color. +/// G - the green component for the text color. +/// B - the blue component for the text color. +/// +/// Returns true if successful. +/// +/// Currently supported subtypes: freetext. +/// The range for the color components is 0 to 255. +int FPDFAnnot_SetFontColor(FPDF_FORMHANDLE handle, +FPDF_ANNOTATION annot, +int R, +int G, +int B, +) { + return _FPDFAnnot_SetFontColor(handle, +annot, +R, +G, +B, +); +} + +late final _FPDFAnnot_SetFontColorPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_SetFontColor'); +late final _FPDFAnnot_SetFontColor = _FPDFAnnot_SetFontColorPtr.asFunction(); + +/// Experimental API. +/// Get the RGB value of the font color for an |annot| with variable text. +/// +/// hHandle - handle to the form fill module, returned by +/// FPDFDOC_InitFormFillEnvironment. +/// annot - handle to an annotation. +/// R, G, B - buffer to hold the RGB value of the color. Ranges from 0 to 255. +/// +/// Returns true if the font color was set, false on error or if the font +/// color was not provided. +int FPDFAnnot_GetFontColor(FPDF_FORMHANDLE hHandle, +FPDF_ANNOTATION annot, +ffi.Pointer R, +ffi.Pointer G, +ffi.Pointer B, +) { + return _FPDFAnnot_GetFontColor(hHandle, +annot, +R, +G, +B, +); +} + +late final _FPDFAnnot_GetFontColorPtr = _lookup< + ffi.NativeFunction , ffi.Pointer , ffi.Pointer )>>('FPDFAnnot_GetFontColor'); +late final _FPDFAnnot_GetFontColor = _FPDFAnnot_GetFontColorPtr.asFunction , ffi.Pointer , ffi.Pointer )>(); + +/// Experimental API. +/// Determine if |annot| is a form widget that is checked. Intended for use with +/// checkbox and radio button widgets. +/// +/// hHandle - handle to the form fill module, returned by +/// FPDFDOC_InitFormFillEnvironment. +/// annot - handle to an annotation. +/// +/// Returns true if |annot| is a form widget and is checked, false otherwise. +int FPDFAnnot_IsChecked(FPDF_FORMHANDLE hHandle, +FPDF_ANNOTATION annot, +) { + return _FPDFAnnot_IsChecked(hHandle, +annot, +); +} + +late final _FPDFAnnot_IsCheckedPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_IsChecked'); +late final _FPDFAnnot_IsChecked = _FPDFAnnot_IsCheckedPtr.asFunction(); + +/// Experimental API. +/// Set the list of focusable annotation subtypes. Annotations of subtype +/// FPDF_ANNOT_WIDGET are by default focusable. New subtypes set using this API +/// will override the existing subtypes. +/// +/// hHandle - handle to the form fill module, returned by +/// FPDFDOC_InitFormFillEnvironment. +/// subtypes - list of annotation subtype which can be tabbed over. +/// count - total number of annotation subtype in list. +/// Returns true if list of annotation subtype is set successfully, false +/// otherwise. +int FPDFAnnot_SetFocusableSubtypes(FPDF_FORMHANDLE hHandle, +ffi.Pointer subtypes, +int count, +) { + return _FPDFAnnot_SetFocusableSubtypes(hHandle, +subtypes, +count, +); +} + +late final _FPDFAnnot_SetFocusableSubtypesPtr = _lookup< + ffi.NativeFunction , ffi.Size )>>('FPDFAnnot_SetFocusableSubtypes'); +late final _FPDFAnnot_SetFocusableSubtypes = _FPDFAnnot_SetFocusableSubtypesPtr.asFunction , int )>(); + +/// Experimental API. +/// Get the count of focusable annotation subtypes as set by host +/// for a |hHandle|. +/// +/// hHandle - handle to the form fill module, returned by +/// FPDFDOC_InitFormFillEnvironment. +/// Returns the count of focusable annotation subtypes or -1 on error. +/// Note : Annotations of type FPDF_ANNOT_WIDGET are by default focusable. +int FPDFAnnot_GetFocusableSubtypesCount(FPDF_FORMHANDLE hHandle, +) { + return _FPDFAnnot_GetFocusableSubtypesCount(hHandle, +); +} + +late final _FPDFAnnot_GetFocusableSubtypesCountPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_GetFocusableSubtypesCount'); +late final _FPDFAnnot_GetFocusableSubtypesCount = _FPDFAnnot_GetFocusableSubtypesCountPtr.asFunction(); + +/// Experimental API. +/// Get the list of focusable annotation subtype as set by host. +/// +/// hHandle - handle to the form fill module, returned by +/// FPDFDOC_InitFormFillEnvironment. +/// subtypes - receives the list of annotation subtype which can be tabbed +/// over. Caller must have allocated |subtypes| more than or +/// equal to the count obtained from +/// FPDFAnnot_GetFocusableSubtypesCount() API. +/// count - size of |subtypes|. +/// Returns true on success and set list of annotation subtype to |subtypes|, +/// false otherwise. +/// Note : Annotations of type FPDF_ANNOT_WIDGET are by default focusable. +int FPDFAnnot_GetFocusableSubtypes(FPDF_FORMHANDLE hHandle, +ffi.Pointer subtypes, +int count, +) { + return _FPDFAnnot_GetFocusableSubtypes(hHandle, +subtypes, +count, +); +} + +late final _FPDFAnnot_GetFocusableSubtypesPtr = _lookup< + ffi.NativeFunction , ffi.Size )>>('FPDFAnnot_GetFocusableSubtypes'); +late final _FPDFAnnot_GetFocusableSubtypes = _FPDFAnnot_GetFocusableSubtypesPtr.asFunction , int )>(); + +/// Experimental API. +/// Gets FPDF_LINK object for |annot|. Intended to use for link annotations. +/// +/// annot - handle to an annotation. +/// +/// Returns FPDF_LINK from the FPDF_ANNOTATION and NULL on failure, +/// if the input annot is NULL or input annot's subtype is not link. +FPDF_LINK FPDFAnnot_GetLink(FPDF_ANNOTATION annot, +) { + return _FPDFAnnot_GetLink(annot, +); +} + +late final _FPDFAnnot_GetLinkPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_GetLink'); +late final _FPDFAnnot_GetLink = _FPDFAnnot_GetLinkPtr.asFunction(); + +/// Experimental API. +/// Gets the count of annotations in the |annot|'s control group. +/// A group of interactive form annotations is collectively called a form +/// control group. Here, |annot|, an interactive form annotation, should be +/// either a radio button or a checkbox. +/// +/// hHandle - handle to the form fill module, returned by +/// FPDFDOC_InitFormFillEnvironment. +/// annot - handle to an annotation. +/// +/// Returns number of controls in its control group or -1 on error. +int FPDFAnnot_GetFormControlCount(FPDF_FORMHANDLE hHandle, +FPDF_ANNOTATION annot, +) { + return _FPDFAnnot_GetFormControlCount(hHandle, +annot, +); +} + +late final _FPDFAnnot_GetFormControlCountPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_GetFormControlCount'); +late final _FPDFAnnot_GetFormControlCount = _FPDFAnnot_GetFormControlCountPtr.asFunction(); + +/// Experimental API. +/// Gets the index of |annot| in |annot|'s control group. +/// A group of interactive form annotations is collectively called a form +/// control group. Here, |annot|, an interactive form annotation, should be +/// either a radio button or a checkbox. +/// +/// hHandle - handle to the form fill module, returned by +/// FPDFDOC_InitFormFillEnvironment. +/// annot - handle to an annotation. +/// +/// Returns index of a given |annot| in its control group or -1 on error. +int FPDFAnnot_GetFormControlIndex(FPDF_FORMHANDLE hHandle, +FPDF_ANNOTATION annot, +) { + return _FPDFAnnot_GetFormControlIndex(hHandle, +annot, +); +} + +late final _FPDFAnnot_GetFormControlIndexPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_GetFormControlIndex'); +late final _FPDFAnnot_GetFormControlIndex = _FPDFAnnot_GetFormControlIndexPtr.asFunction(); + +/// Experimental API. +/// Gets the export value of |annot| which is an interactive form annotation. +/// Intended for use with radio button and checkbox widget annotations. +/// |buffer| is only modified if |buflen| is longer than the length of contents. +/// In case of error, nothing will be added to |buffer| and the return value +/// will be 0. Note that return value of empty string is 2 for "\0\0". +/// +/// hHandle - handle to the form fill module, returned by +/// FPDFDOC_InitFormFillEnvironment(). +/// annot - handle to an interactive form annotation. +/// buffer - buffer for holding the value string, encoded in UTF-16LE. +/// buflen - length of the buffer in bytes. +/// +/// Returns the length of the string value in bytes. +int FPDFAnnot_GetFormFieldExportValue(FPDF_FORMHANDLE hHandle, +FPDF_ANNOTATION annot, +ffi.Pointer buffer, +int buflen, +) { + return _FPDFAnnot_GetFormFieldExportValue(hHandle, +annot, +buffer, +buflen, +); +} + +late final _FPDFAnnot_GetFormFieldExportValuePtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFAnnot_GetFormFieldExportValue'); +late final _FPDFAnnot_GetFormFieldExportValue = _FPDFAnnot_GetFormFieldExportValuePtr.asFunction , int )>(); + +/// Experimental API. +/// Add a URI action to |annot|, overwriting the existing action, if any. +/// +/// annot - handle to a link annotation. +/// uri - the URI to be set, encoded in 7-bit ASCII. +/// +/// Returns true if successful. +int FPDFAnnot_SetURI(FPDF_ANNOTATION annot, +ffi.Pointer uri, +) { + return _FPDFAnnot_SetURI(annot, +uri, +); +} + +late final _FPDFAnnot_SetURIPtr = _lookup< + ffi.NativeFunction )>>('FPDFAnnot_SetURI'); +late final _FPDFAnnot_SetURI = _FPDFAnnot_SetURIPtr.asFunction )>(); + +/// Experimental API. +/// Get the attachment from |annot|. +/// +/// annot - handle to a file annotation. +/// +/// Returns the handle to the attachment object, or NULL on failure. +FPDF_ATTACHMENT FPDFAnnot_GetFileAttachment(FPDF_ANNOTATION annot, +) { + return _FPDFAnnot_GetFileAttachment(annot, +); +} + +late final _FPDFAnnot_GetFileAttachmentPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_GetFileAttachment'); +late final _FPDFAnnot_GetFileAttachment = _FPDFAnnot_GetFileAttachmentPtr.asFunction(); + +/// Experimental API. +/// Add an embedded file with |name| to |annot|. +/// +/// annot - handle to a file annotation. +/// name - name of the new attachment. +/// +/// Returns a handle to the new attachment object, or NULL on failure. +FPDF_ATTACHMENT FPDFAnnot_AddFileAttachment(FPDF_ANNOTATION annot, +FPDF_WIDESTRING name, +) { + return _FPDFAnnot_AddFileAttachment(annot, +name, +); +} + +late final _FPDFAnnot_AddFileAttachmentPtr = _lookup< + ffi.NativeFunction>('FPDFAnnot_AddFileAttachment'); +late final _FPDFAnnot_AddFileAttachment = _FPDFAnnot_AddFileAttachmentPtr.asFunction(); + +/// Experimental API. +/// +/// Determine if |document| represents a tagged PDF. +/// +/// For the definition of tagged PDF, See (see 10.7 "Tagged PDF" in PDF +/// Reference 1.7). +/// +/// document - handle to a document. +/// +/// Returns |true| iff |document| is a tagged PDF. +int FPDFCatalog_IsTagged(FPDF_DOCUMENT document, +) { + return _FPDFCatalog_IsTagged(document, +); +} + +late final _FPDFCatalog_IsTaggedPtr = _lookup< + ffi.NativeFunction>('FPDFCatalog_IsTagged'); +late final _FPDFCatalog_IsTagged = _FPDFCatalog_IsTaggedPtr.asFunction(); + +/// Experimental API. +/// Sets the language of |document| to |language|. +/// +/// document - handle to a document. +/// language - the language to set to. +/// +/// Returns TRUE on success. +int FPDFCatalog_SetLanguage(FPDF_DOCUMENT document, +FPDF_BYTESTRING language, +) { + return _FPDFCatalog_SetLanguage(document, +language, +); +} + +late final _FPDFCatalog_SetLanguagePtr = _lookup< + ffi.NativeFunction>('FPDFCatalog_SetLanguage'); +late final _FPDFCatalog_SetLanguage = _FPDFCatalog_SetLanguagePtr.asFunction(); + +/// Experimental API. +/// Import pages to a FPDF_DOCUMENT. +/// +/// dest_doc - The destination document for the pages. +/// src_doc - The document to be imported. +/// page_indices - An array of page indices to be imported. The first page is +/// zero. If |page_indices| is NULL, all pages from |src_doc| +/// are imported. +/// length - The length of the |page_indices| array. +/// index - The page index at which to insert the first imported page +/// into |dest_doc|. The first page is zero. +/// +/// Returns TRUE on success. Returns FALSE if any pages in |page_indices| is +/// invalid. +int FPDF_ImportPagesByIndex(FPDF_DOCUMENT dest_doc, +FPDF_DOCUMENT src_doc, +ffi.Pointer page_indices, +int length, +int index, +) { + return _FPDF_ImportPagesByIndex(dest_doc, +src_doc, +page_indices, +length, +index, +); +} + +late final _FPDF_ImportPagesByIndexPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong , ffi.Int )>>('FPDF_ImportPagesByIndex'); +late final _FPDF_ImportPagesByIndex = _FPDF_ImportPagesByIndexPtr.asFunction , int , int )>(); + +/// Import pages to a FPDF_DOCUMENT. +/// +/// dest_doc - The destination document for the pages. +/// src_doc - The document to be imported. +/// pagerange - A page range string, Such as "1,3,5-7". The first page is one. +/// If |pagerange| is NULL, all pages from |src_doc| are imported. +/// index - The page index at which to insert the first imported page into +/// |dest_doc|. The first page is zero. +/// +/// Returns TRUE on success. Returns FALSE if any pages in |pagerange| is +/// invalid or if |pagerange| cannot be read. +int FPDF_ImportPages(FPDF_DOCUMENT dest_doc, +FPDF_DOCUMENT src_doc, +FPDF_BYTESTRING pagerange, +int index, +) { + return _FPDF_ImportPages(dest_doc, +src_doc, +pagerange, +index, +); +} + +late final _FPDF_ImportPagesPtr = _lookup< + ffi.NativeFunction>('FPDF_ImportPages'); +late final _FPDF_ImportPages = _FPDF_ImportPagesPtr.asFunction(); + +/// Experimental API. +/// Create a new document from |src_doc|. The pages of |src_doc| will be +/// combined to provide |num_pages_on_x_axis x num_pages_on_y_axis| pages per +/// |output_doc| page. +/// +/// src_doc - The document to be imported. +/// output_width - The output page width in PDF "user space" units. +/// output_height - The output page height in PDF "user space" units. +/// num_pages_on_x_axis - The number of pages on X Axis. +/// num_pages_on_y_axis - The number of pages on Y Axis. +/// +/// Return value: +/// A handle to the created document, or NULL on failure. +/// +/// Comments: +/// number of pages per page = num_pages_on_x_axis * num_pages_on_y_axis +FPDF_DOCUMENT FPDF_ImportNPagesToOne(FPDF_DOCUMENT src_doc, +double output_width, +double output_height, +int num_pages_on_x_axis, +int num_pages_on_y_axis, +) { + return _FPDF_ImportNPagesToOne(src_doc, +output_width, +output_height, +num_pages_on_x_axis, +num_pages_on_y_axis, +); +} + +late final _FPDF_ImportNPagesToOnePtr = _lookup< + ffi.NativeFunction>('FPDF_ImportNPagesToOne'); +late final _FPDF_ImportNPagesToOne = _FPDF_ImportNPagesToOnePtr.asFunction(); + +/// Experimental API. +/// Create a template to generate form xobjects from |src_doc|'s page at +/// |src_page_index|, for use in |dest_doc|. +/// +/// Returns a handle on success, or NULL on failure. Caller owns the newly +/// created object. +FPDF_XOBJECT FPDF_NewXObjectFromPage(FPDF_DOCUMENT dest_doc, +FPDF_DOCUMENT src_doc, +int src_page_index, +) { + return _FPDF_NewXObjectFromPage(dest_doc, +src_doc, +src_page_index, +); +} + +late final _FPDF_NewXObjectFromPagePtr = _lookup< + ffi.NativeFunction>('FPDF_NewXObjectFromPage'); +late final _FPDF_NewXObjectFromPage = _FPDF_NewXObjectFromPagePtr.asFunction(); + +/// Experimental API. +/// Close an FPDF_XOBJECT handle created by FPDF_NewXObjectFromPage(). +/// FPDF_PAGEOBJECTs created from the FPDF_XOBJECT handle are not affected. +void FPDF_CloseXObject(FPDF_XOBJECT xobject, +) { + return _FPDF_CloseXObject(xobject, +); +} + +late final _FPDF_CloseXObjectPtr = _lookup< + ffi.NativeFunction>('FPDF_CloseXObject'); +late final _FPDF_CloseXObject = _FPDF_CloseXObjectPtr.asFunction(); + +/// Experimental API. +/// Create a new form object from an FPDF_XOBJECT object. +/// +/// Returns a new form object on success, or NULL on failure. Caller owns the +/// newly created object. +FPDF_PAGEOBJECT FPDF_NewFormObjectFromXObject(FPDF_XOBJECT xobject, +) { + return _FPDF_NewFormObjectFromXObject(xobject, +); +} + +late final _FPDF_NewFormObjectFromXObjectPtr = _lookup< + ffi.NativeFunction>('FPDF_NewFormObjectFromXObject'); +late final _FPDF_NewFormObjectFromXObject = _FPDF_NewFormObjectFromXObjectPtr.asFunction(); + +/// Copy the viewer preferences from |src_doc| into |dest_doc|. +/// +/// dest_doc - Document to write the viewer preferences into. +/// src_doc - Document to read the viewer preferences from. +/// +/// Returns TRUE on success. +int FPDF_CopyViewerPreferences(FPDF_DOCUMENT dest_doc, +FPDF_DOCUMENT src_doc, +) { + return _FPDF_CopyViewerPreferences(dest_doc, +src_doc, +); +} + +late final _FPDF_CopyViewerPreferencesPtr = _lookup< + ffi.NativeFunction>('FPDF_CopyViewerPreferences'); +late final _FPDF_CopyViewerPreferences = _FPDF_CopyViewerPreferencesPtr.asFunction(); + +/// Function: FPDF_SaveAsCopy +/// Saves the copy of specified document in custom way. +/// Parameters: +/// document - Handle to document, as returned by +/// FPDF_LoadDocument() or FPDF_CreateNewDocument(). +/// pFileWrite - A pointer to a custom file write structure. +/// flags - Flags above that affect how the PDF gets saved. +/// Pass in 0 when there are no flags. +/// Return value: +/// TRUE for succeed, FALSE for failed. +int FPDF_SaveAsCopy(FPDF_DOCUMENT document, +ffi.Pointer pFileWrite, +int flags, +) { + return _FPDF_SaveAsCopy(document, +pFileWrite, +flags, +); +} + +late final _FPDF_SaveAsCopyPtr = _lookup< + ffi.NativeFunction , FPDF_DWORD )>>('FPDF_SaveAsCopy'); +late final _FPDF_SaveAsCopy = _FPDF_SaveAsCopyPtr.asFunction , int )>(); + +/// Function: FPDF_SaveWithVersion +/// Same as FPDF_SaveAsCopy(), except the file version of the +/// saved document can be specified by the caller. +/// Parameters: +/// document - Handle to document. +/// pFileWrite - A pointer to a custom file write structure. +/// flags - The creating flags. +/// fileVersion - The PDF file version. File version: 14 for 1.4, +/// 15 for 1.5, ... +/// Return value: +/// TRUE if succeed, FALSE if failed. +int FPDF_SaveWithVersion(FPDF_DOCUMENT document, +ffi.Pointer pFileWrite, +int flags, +int fileVersion, +) { + return _FPDF_SaveWithVersion(document, +pFileWrite, +flags, +fileVersion, +); +} + +late final _FPDF_SaveWithVersionPtr = _lookup< + ffi.NativeFunction , FPDF_DWORD , ffi.Int )>>('FPDF_SaveWithVersion'); +late final _FPDF_SaveWithVersion = _FPDF_SaveWithVersionPtr.asFunction , int , int )>(); + +/// Get the first child of |bookmark|, or the first top-level bookmark item. +/// +/// document - handle to the document. +/// bookmark - handle to the current bookmark. Pass NULL for the first top +/// level item. +/// +/// Returns a handle to the first child of |bookmark| or the first top-level +/// bookmark item. NULL if no child or top-level bookmark found. +/// Note that another name for the bookmarks is the document outline, as +/// described in ISO 32000-1:2008, section 12.3.3. +FPDF_BOOKMARK FPDFBookmark_GetFirstChild(FPDF_DOCUMENT document, +FPDF_BOOKMARK bookmark, +) { + return _FPDFBookmark_GetFirstChild(document, +bookmark, +); +} + +late final _FPDFBookmark_GetFirstChildPtr = _lookup< + ffi.NativeFunction>('FPDFBookmark_GetFirstChild'); +late final _FPDFBookmark_GetFirstChild = _FPDFBookmark_GetFirstChildPtr.asFunction(); + +/// Get the next sibling of |bookmark|. +/// +/// document - handle to the document. +/// bookmark - handle to the current bookmark. +/// +/// Returns a handle to the next sibling of |bookmark|, or NULL if this is the +/// last bookmark at this level. +/// +/// Note that the caller is responsible for handling circular bookmark +/// references, as may arise from malformed documents. +FPDF_BOOKMARK FPDFBookmark_GetNextSibling(FPDF_DOCUMENT document, +FPDF_BOOKMARK bookmark, +) { + return _FPDFBookmark_GetNextSibling(document, +bookmark, +); +} + +late final _FPDFBookmark_GetNextSiblingPtr = _lookup< + ffi.NativeFunction>('FPDFBookmark_GetNextSibling'); +late final _FPDFBookmark_GetNextSibling = _FPDFBookmark_GetNextSiblingPtr.asFunction(); + +/// Get the title of |bookmark|. +/// +/// bookmark - handle to the bookmark. +/// buffer - buffer for the title. May be NULL. +/// buflen - the length of the buffer in bytes. May be 0. +/// +/// Returns the number of bytes in the title, including the terminating NUL +/// character. The number of bytes is returned regardless of the |buffer| and +/// |buflen| parameters. +/// +/// Regardless of the platform, the |buffer| is always in UTF-16LE encoding. The +/// string is terminated by a UTF16 NUL character. If |buflen| is less than the +/// required length, or |buffer| is NULL, |buffer| will not be modified. +int FPDFBookmark_GetTitle(FPDF_BOOKMARK bookmark, +ffi.Pointer buffer, +int buflen, +) { + return _FPDFBookmark_GetTitle(bookmark, +buffer, +buflen, +); +} + +late final _FPDFBookmark_GetTitlePtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFBookmark_GetTitle'); +late final _FPDFBookmark_GetTitle = _FPDFBookmark_GetTitlePtr.asFunction , int )>(); + +/// Experimental API. +/// Get the number of chlidren of |bookmark|. +/// +/// bookmark - handle to the bookmark. +/// +/// Returns a signed integer that represents the number of sub-items the given +/// bookmark has. If the value is positive, child items shall be shown by default +/// (open state). If the value is negative, child items shall be hidden by +/// default (closed state). Please refer to PDF 32000-1:2008, Table 153. +/// Returns 0 if the bookmark has no children or is invalid. +int FPDFBookmark_GetCount(FPDF_BOOKMARK bookmark, +) { + return _FPDFBookmark_GetCount(bookmark, +); +} + +late final _FPDFBookmark_GetCountPtr = _lookup< + ffi.NativeFunction>('FPDFBookmark_GetCount'); +late final _FPDFBookmark_GetCount = _FPDFBookmark_GetCountPtr.asFunction(); + +/// Find the bookmark with |title| in |document|. +/// +/// document - handle to the document. +/// title - the UTF-16LE encoded Unicode title for which to search. +/// +/// Returns the handle to the bookmark, or NULL if |title| can't be found. +/// +/// FPDFBookmark_Find() will always return the first bookmark found even if +/// multiple bookmarks have the same |title|. +FPDF_BOOKMARK FPDFBookmark_Find(FPDF_DOCUMENT document, +FPDF_WIDESTRING title, +) { + return _FPDFBookmark_Find(document, +title, +); +} + +late final _FPDFBookmark_FindPtr = _lookup< + ffi.NativeFunction>('FPDFBookmark_Find'); +late final _FPDFBookmark_Find = _FPDFBookmark_FindPtr.asFunction(); + +/// Get the destination associated with |bookmark|. +/// +/// document - handle to the document. +/// bookmark - handle to the bookmark. +/// +/// Returns the handle to the destination data, or NULL if no destination is +/// associated with |bookmark|. +FPDF_DEST FPDFBookmark_GetDest(FPDF_DOCUMENT document, +FPDF_BOOKMARK bookmark, +) { + return _FPDFBookmark_GetDest(document, +bookmark, +); +} + +late final _FPDFBookmark_GetDestPtr = _lookup< + ffi.NativeFunction>('FPDFBookmark_GetDest'); +late final _FPDFBookmark_GetDest = _FPDFBookmark_GetDestPtr.asFunction(); + +/// Get the action associated with |bookmark|. +/// +/// bookmark - handle to the bookmark. +/// +/// Returns the handle to the action data, or NULL if no action is associated +/// with |bookmark|. +/// If this function returns a valid handle, it is valid as long as |bookmark| is +/// valid. +/// If this function returns NULL, FPDFBookmark_GetDest() should be called to get +/// the |bookmark| destination data. +FPDF_ACTION FPDFBookmark_GetAction(FPDF_BOOKMARK bookmark, +) { + return _FPDFBookmark_GetAction(bookmark, +); +} + +late final _FPDFBookmark_GetActionPtr = _lookup< + ffi.NativeFunction>('FPDFBookmark_GetAction'); +late final _FPDFBookmark_GetAction = _FPDFBookmark_GetActionPtr.asFunction(); + +/// Get the type of |action|. +/// +/// action - handle to the action. +/// +/// Returns one of: +/// PDFACTION_UNSUPPORTED +/// PDFACTION_GOTO +/// PDFACTION_REMOTEGOTO +/// PDFACTION_URI +/// PDFACTION_LAUNCH +int FPDFAction_GetType(FPDF_ACTION action, +) { + return _FPDFAction_GetType(action, +); +} + +late final _FPDFAction_GetTypePtr = _lookup< + ffi.NativeFunction>('FPDFAction_GetType'); +late final _FPDFAction_GetType = _FPDFAction_GetTypePtr.asFunction(); + +/// Get the destination of |action|. +/// +/// document - handle to the document. +/// action - handle to the action. |action| must be a |PDFACTION_GOTO| or +/// |PDFACTION_REMOTEGOTO|. +/// +/// Returns a handle to the destination data, or NULL on error, typically +/// because the arguments were bad or the action was of the wrong type. +/// +/// In the case of |PDFACTION_REMOTEGOTO|, you must first call +/// FPDFAction_GetFilePath(), then load the document at that path, then pass +/// the document handle from that document as |document| to FPDFAction_GetDest(). +FPDF_DEST FPDFAction_GetDest(FPDF_DOCUMENT document, +FPDF_ACTION action, +) { + return _FPDFAction_GetDest(document, +action, +); +} + +late final _FPDFAction_GetDestPtr = _lookup< + ffi.NativeFunction>('FPDFAction_GetDest'); +late final _FPDFAction_GetDest = _FPDFAction_GetDestPtr.asFunction(); + +/// Get the file path of |action|. +/// +/// action - handle to the action. |action| must be a |PDFACTION_LAUNCH| or +/// |PDFACTION_REMOTEGOTO|. +/// buffer - a buffer for output the path string. May be NULL. +/// buflen - the length of the buffer, in bytes. May be 0. +/// +/// Returns the number of bytes in the file path, including the trailing NUL +/// character, or 0 on error, typically because the arguments were bad or the +/// action was of the wrong type. +/// +/// Regardless of the platform, the |buffer| is always in UTF-8 encoding. +/// If |buflen| is less than the returned length, or |buffer| is NULL, |buffer| +/// will not be modified. +int FPDFAction_GetFilePath(FPDF_ACTION action, +ffi.Pointer buffer, +int buflen, +) { + return _FPDFAction_GetFilePath(action, +buffer, +buflen, +); +} + +late final _FPDFAction_GetFilePathPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFAction_GetFilePath'); +late final _FPDFAction_GetFilePath = _FPDFAction_GetFilePathPtr.asFunction , int )>(); + +/// Get the URI path of |action|. +/// +/// document - handle to the document. +/// action - handle to the action. Must be a |PDFACTION_URI|. +/// buffer - a buffer for the path string. May be NULL. +/// buflen - the length of the buffer, in bytes. May be 0. +/// +/// Returns the number of bytes in the URI path, including the trailing NUL +/// character, or 0 on error, typically because the arguments were bad or the +/// action was of the wrong type. +/// +/// The |buffer| may contain badly encoded data. The caller should validate the +/// output. e.g. Check to see if it is UTF-8. +/// +/// If |buflen| is less than the returned length, or |buffer| is NULL, |buffer| +/// will not be modified. +/// +/// Historically, the documentation for this API claimed |buffer| is always +/// encoded in 7-bit ASCII, but did not actually enforce it. +/// https://pdfium.googlesource.com/pdfium.git/+/d609e84cee2e14a18333247485af91df48a40592 +/// added that enforcement, but that did not work well for real world PDFs that +/// used UTF-8. As of this writing, this API reverted back to its original +/// behavior prior to commit d609e84cee. +int FPDFAction_GetURIPath(FPDF_DOCUMENT document, +FPDF_ACTION action, +ffi.Pointer buffer, +int buflen, +) { + return _FPDFAction_GetURIPath(document, +action, +buffer, +buflen, +); +} + +late final _FPDFAction_GetURIPathPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFAction_GetURIPath'); +late final _FPDFAction_GetURIPath = _FPDFAction_GetURIPathPtr.asFunction , int )>(); + +/// Get the page index of |dest|. +/// +/// document - handle to the document. +/// dest - handle to the destination. +/// +/// Returns the 0-based page index containing |dest|. Returns -1 on error. +int FPDFDest_GetDestPageIndex(FPDF_DOCUMENT document, +FPDF_DEST dest, +) { + return _FPDFDest_GetDestPageIndex(document, +dest, +); +} + +late final _FPDFDest_GetDestPageIndexPtr = _lookup< + ffi.NativeFunction>('FPDFDest_GetDestPageIndex'); +late final _FPDFDest_GetDestPageIndex = _FPDFDest_GetDestPageIndexPtr.asFunction(); + +/// Experimental API. +/// Get the view (fit type) specified by |dest|. +/// +/// dest - handle to the destination. +/// pNumParams - receives the number of view parameters, which is at most 4. +/// pParams - buffer to write the view parameters. Must be at least 4 +/// FS_FLOATs long. +/// Returns one of the PDFDEST_VIEW_* constants, PDFDEST_VIEW_UNKNOWN_MODE if +/// |dest| does not specify a view. +int FPDFDest_GetView(FPDF_DEST dest, +ffi.Pointer pNumParams, +ffi.Pointer pParams, +) { + return _FPDFDest_GetView(dest, +pNumParams, +pParams, +); +} + +late final _FPDFDest_GetViewPtr = _lookup< + ffi.NativeFunction , ffi.Pointer )>>('FPDFDest_GetView'); +late final _FPDFDest_GetView = _FPDFDest_GetViewPtr.asFunction , ffi.Pointer )>(); + +/// Get the (x, y, zoom) location of |dest| in the destination page, if the +/// destination is in [page /XYZ x y zoom] syntax. +/// +/// dest - handle to the destination. +/// hasXVal - out parameter; true if the x value is not null +/// hasYVal - out parameter; true if the y value is not null +/// hasZoomVal - out parameter; true if the zoom value is not null +/// x - out parameter; the x coordinate, in page coordinates. +/// y - out parameter; the y coordinate, in page coordinates. +/// zoom - out parameter; the zoom value. +/// Returns TRUE on successfully reading the /XYZ value. +/// +/// Note the [x, y, zoom] values are only set if the corresponding hasXVal, +/// hasYVal or hasZoomVal flags are true. +int FPDFDest_GetLocationInPage(FPDF_DEST dest, +ffi.Pointer hasXVal, +ffi.Pointer hasYVal, +ffi.Pointer hasZoomVal, +ffi.Pointer x, +ffi.Pointer y, +ffi.Pointer zoom, +) { + return _FPDFDest_GetLocationInPage(dest, +hasXVal, +hasYVal, +hasZoomVal, +x, +y, +zoom, +); +} + +late final _FPDFDest_GetLocationInPagePtr = _lookup< + ffi.NativeFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer , ffi.Pointer , ffi.Pointer )>>('FPDFDest_GetLocationInPage'); +late final _FPDFDest_GetLocationInPage = _FPDFDest_GetLocationInPagePtr.asFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer , ffi.Pointer , ffi.Pointer )>(); + +/// Find a link at point (|x|,|y|) on |page|. +/// +/// page - handle to the document page. +/// x - the x coordinate, in the page coordinate system. +/// y - the y coordinate, in the page coordinate system. +/// +/// Returns a handle to the link, or NULL if no link found at the given point. +/// +/// You can convert coordinates from screen coordinates to page coordinates using +/// FPDF_DeviceToPage(). +FPDF_LINK FPDFLink_GetLinkAtPoint(FPDF_PAGE page, +double x, +double y, +) { + return _FPDFLink_GetLinkAtPoint(page, +x, +y, +); +} + +late final _FPDFLink_GetLinkAtPointPtr = _lookup< + ffi.NativeFunction>('FPDFLink_GetLinkAtPoint'); +late final _FPDFLink_GetLinkAtPoint = _FPDFLink_GetLinkAtPointPtr.asFunction(); + +/// Find the Z-order of link at point (|x|,|y|) on |page|. +/// +/// page - handle to the document page. +/// x - the x coordinate, in the page coordinate system. +/// y - the y coordinate, in the page coordinate system. +/// +/// Returns the Z-order of the link, or -1 if no link found at the given point. +/// Larger Z-order numbers are closer to the front. +/// +/// You can convert coordinates from screen coordinates to page coordinates using +/// FPDF_DeviceToPage(). +int FPDFLink_GetLinkZOrderAtPoint(FPDF_PAGE page, +double x, +double y, +) { + return _FPDFLink_GetLinkZOrderAtPoint(page, +x, +y, +); +} + +late final _FPDFLink_GetLinkZOrderAtPointPtr = _lookup< + ffi.NativeFunction>('FPDFLink_GetLinkZOrderAtPoint'); +late final _FPDFLink_GetLinkZOrderAtPoint = _FPDFLink_GetLinkZOrderAtPointPtr.asFunction(); + +/// Get destination info for |link|. +/// +/// document - handle to the document. +/// link - handle to the link. +/// +/// Returns a handle to the destination, or NULL if there is no destination +/// associated with the link. In this case, you should call FPDFLink_GetAction() +/// to retrieve the action associated with |link|. +FPDF_DEST FPDFLink_GetDest(FPDF_DOCUMENT document, +FPDF_LINK link, +) { + return _FPDFLink_GetDest(document, +link, +); +} + +late final _FPDFLink_GetDestPtr = _lookup< + ffi.NativeFunction>('FPDFLink_GetDest'); +late final _FPDFLink_GetDest = _FPDFLink_GetDestPtr.asFunction(); + +/// Get action info for |link|. +/// +/// link - handle to the link. +/// +/// Returns a handle to the action associated to |link|, or NULL if no action. +/// If this function returns a valid handle, it is valid as long as |link| is +/// valid. +FPDF_ACTION FPDFLink_GetAction(FPDF_LINK link, +) { + return _FPDFLink_GetAction(link, +); +} + +late final _FPDFLink_GetActionPtr = _lookup< + ffi.NativeFunction>('FPDFLink_GetAction'); +late final _FPDFLink_GetAction = _FPDFLink_GetActionPtr.asFunction(); + +/// Enumerates all the link annotations in |page|. +/// +/// page - handle to the page. +/// start_pos - the start position, should initially be 0 and is updated with +/// the next start position on return. +/// link_annot - the link handle for |startPos|. +/// +/// Returns TRUE on success. +int FPDFLink_Enumerate(FPDF_PAGE page, +ffi.Pointer start_pos, +ffi.Pointer link_annot, +) { + return _FPDFLink_Enumerate(page, +start_pos, +link_annot, +); +} + +late final _FPDFLink_EnumeratePtr = _lookup< + ffi.NativeFunction , ffi.Pointer )>>('FPDFLink_Enumerate'); +late final _FPDFLink_Enumerate = _FPDFLink_EnumeratePtr.asFunction , ffi.Pointer )>(); + +/// Experimental API. +/// Gets FPDF_ANNOTATION object for |link_annot|. +/// +/// page - handle to the page in which FPDF_LINK object is present. +/// link_annot - handle to link annotation. +/// +/// Returns FPDF_ANNOTATION from the FPDF_LINK and NULL on failure, +/// if the input link annot or page is NULL. +FPDF_ANNOTATION FPDFLink_GetAnnot(FPDF_PAGE page, +FPDF_LINK link_annot, +) { + return _FPDFLink_GetAnnot(page, +link_annot, +); +} + +late final _FPDFLink_GetAnnotPtr = _lookup< + ffi.NativeFunction>('FPDFLink_GetAnnot'); +late final _FPDFLink_GetAnnot = _FPDFLink_GetAnnotPtr.asFunction(); + +/// Get the rectangle for |link_annot|. +/// +/// link_annot - handle to the link annotation. +/// rect - the annotation rectangle. +/// +/// Returns true on success. +int FPDFLink_GetAnnotRect(FPDF_LINK link_annot, +ffi.Pointer rect, +) { + return _FPDFLink_GetAnnotRect(link_annot, +rect, +); +} + +late final _FPDFLink_GetAnnotRectPtr = _lookup< + ffi.NativeFunction )>>('FPDFLink_GetAnnotRect'); +late final _FPDFLink_GetAnnotRect = _FPDFLink_GetAnnotRectPtr.asFunction )>(); + +/// Get the count of quadrilateral points to the |link_annot|. +/// +/// link_annot - handle to the link annotation. +/// +/// Returns the count of quadrilateral points. +int FPDFLink_CountQuadPoints(FPDF_LINK link_annot, +) { + return _FPDFLink_CountQuadPoints(link_annot, +); +} + +late final _FPDFLink_CountQuadPointsPtr = _lookup< + ffi.NativeFunction>('FPDFLink_CountQuadPoints'); +late final _FPDFLink_CountQuadPoints = _FPDFLink_CountQuadPointsPtr.asFunction(); + +/// Get the quadrilateral points for the specified |quad_index| in |link_annot|. +/// +/// link_annot - handle to the link annotation. +/// quad_index - the specified quad point index. +/// quad_points - receives the quadrilateral points. +/// +/// Returns true on success. +int FPDFLink_GetQuadPoints(FPDF_LINK link_annot, +int quad_index, +ffi.Pointer quad_points, +) { + return _FPDFLink_GetQuadPoints(link_annot, +quad_index, +quad_points, +); +} + +late final _FPDFLink_GetQuadPointsPtr = _lookup< + ffi.NativeFunction )>>('FPDFLink_GetQuadPoints'); +late final _FPDFLink_GetQuadPoints = _FPDFLink_GetQuadPointsPtr.asFunction )>(); + +/// Experimental API +/// Gets an additional-action from |page|. +/// +/// page - handle to the page, as returned by FPDF_LoadPage(). +/// aa_type - the type of the page object's addtional-action, defined +/// in public/fpdf_formfill.h +/// +/// Returns the handle to the action data, or NULL if there is no +/// additional-action of type |aa_type|. +/// If this function returns a valid handle, it is valid as long as |page| is +/// valid. +FPDF_ACTION FPDF_GetPageAAction(FPDF_PAGE page, +int aa_type, +) { + return _FPDF_GetPageAAction(page, +aa_type, +); +} + +late final _FPDF_GetPageAActionPtr = _lookup< + ffi.NativeFunction>('FPDF_GetPageAAction'); +late final _FPDF_GetPageAAction = _FPDF_GetPageAActionPtr.asFunction(); + +/// Experimental API. +/// Get the file identifer defined in the trailer of |document|. +/// +/// document - handle to the document. +/// id_type - the file identifier type to retrieve. +/// buffer - a buffer for the file identifier. May be NULL. +/// buflen - the length of the buffer, in bytes. May be 0. +/// +/// Returns the number of bytes in the file identifier, including the NUL +/// terminator. +/// +/// The |buffer| is always a byte string. The |buffer| is followed by a NUL +/// terminator. If |buflen| is less than the returned length, or |buffer| is +/// NULL, |buffer| will not be modified. +int FPDF_GetFileIdentifier(FPDF_DOCUMENT document, +FPDF_FILEIDTYPE id_type, +ffi.Pointer buffer, +int buflen, +) { + return _FPDF_GetFileIdentifier(document, +id_type.value, +buffer, +buflen, +); +} + +late final _FPDF_GetFileIdentifierPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDF_GetFileIdentifier'); +late final _FPDF_GetFileIdentifier = _FPDF_GetFileIdentifierPtr.asFunction , int )>(); + +/// Get meta-data |tag| content from |document|. +/// +/// document - handle to the document. +/// tag - the tag to retrieve. The tag can be one of: +/// Title, Author, Subject, Keywords, Creator, Producer, +/// CreationDate, or ModDate. +/// For detailed explanations of these tags and their respective +/// values, please refer to PDF Reference 1.6, section 10.2.1, +/// 'Document Information Dictionary'. +/// buffer - a buffer for the tag. May be NULL. +/// buflen - the length of the buffer, in bytes. May be 0. +/// +/// Returns the number of bytes in the tag, including trailing zeros. +/// +/// The |buffer| is always encoded in UTF-16LE. The |buffer| is followed by two +/// bytes of zeros indicating the end of the string. If |buflen| is less than +/// the returned length, or |buffer| is NULL, |buffer| will not be modified. +/// +/// For linearized files, FPDFAvail_IsFormAvail must be called before this, and +/// it must have returned PDF_FORM_AVAIL or PDF_FORM_NOTEXIST. Before that, there +/// is no guarantee the metadata has been loaded. +int FPDF_GetMetaText(FPDF_DOCUMENT document, +FPDF_BYTESTRING tag, +ffi.Pointer buffer, +int buflen, +) { + return _FPDF_GetMetaText(document, +tag, +buffer, +buflen, +); +} + +late final _FPDF_GetMetaTextPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDF_GetMetaText'); +late final _FPDF_GetMetaText = _FPDF_GetMetaTextPtr.asFunction , int )>(); + +/// Get the page label for |page_index| from |document|. +/// +/// document - handle to the document. +/// page_index - the 0-based index of the page. +/// buffer - a buffer for the page label. May be NULL. +/// buflen - the length of the buffer, in bytes. May be 0. +/// +/// Returns the number of bytes in the page label, including trailing zeros. +/// +/// The |buffer| is always encoded in UTF-16LE. The |buffer| is followed by two +/// bytes of zeros indicating the end of the string. If |buflen| is less than +/// the returned length, or |buffer| is NULL, |buffer| will not be modified. +int FPDF_GetPageLabel(FPDF_DOCUMENT document, +int page_index, +ffi.Pointer buffer, +int buflen, +) { + return _FPDF_GetPageLabel(document, +page_index, +buffer, +buflen, +); +} + +late final _FPDF_GetPageLabelPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDF_GetPageLabel'); +late final _FPDF_GetPageLabel = _FPDF_GetPageLabelPtr.asFunction , int )>(); + +/// Function: FPDF_StructTree_GetForPage +/// Get the structure tree for a page. +/// Parameters: +/// page - Handle to the page, as returned by FPDF_LoadPage(). +/// Return value: +/// A handle to the structure tree or NULL on error. The caller owns the +/// returned handle and must use FPDF_StructTree_Close() to release it. +/// The handle should be released before |page| gets released. +FPDF_STRUCTTREE FPDF_StructTree_GetForPage(FPDF_PAGE page, +) { + return _FPDF_StructTree_GetForPage(page, +); +} + +late final _FPDF_StructTree_GetForPagePtr = _lookup< + ffi.NativeFunction>('FPDF_StructTree_GetForPage'); +late final _FPDF_StructTree_GetForPage = _FPDF_StructTree_GetForPagePtr.asFunction(); + +/// Function: FPDF_StructTree_Close +/// Release a resource allocated by FPDF_StructTree_GetForPage(). +/// Parameters: +/// struct_tree - Handle to the structure tree, as returned by +/// FPDF_StructTree_LoadPage(). +/// Return value: +/// None. +void FPDF_StructTree_Close(FPDF_STRUCTTREE struct_tree, +) { + return _FPDF_StructTree_Close(struct_tree, +); +} + +late final _FPDF_StructTree_ClosePtr = _lookup< + ffi.NativeFunction>('FPDF_StructTree_Close'); +late final _FPDF_StructTree_Close = _FPDF_StructTree_ClosePtr.asFunction(); + +/// Function: FPDF_StructTree_CountChildren +/// Count the number of children for the structure tree. +/// Parameters: +/// struct_tree - Handle to the structure tree, as returned by +/// FPDF_StructTree_LoadPage(). +/// Return value: +/// The number of children, or -1 on error. +int FPDF_StructTree_CountChildren(FPDF_STRUCTTREE struct_tree, +) { + return _FPDF_StructTree_CountChildren(struct_tree, +); +} + +late final _FPDF_StructTree_CountChildrenPtr = _lookup< + ffi.NativeFunction>('FPDF_StructTree_CountChildren'); +late final _FPDF_StructTree_CountChildren = _FPDF_StructTree_CountChildrenPtr.asFunction(); + +/// Function: FPDF_StructTree_GetChildAtIndex +/// Get a child in the structure tree. +/// Parameters: +/// struct_tree - Handle to the structure tree, as returned by +/// FPDF_StructTree_LoadPage(). +/// index - The index for the child, 0-based. +/// Return value: +/// The child at the n-th index or NULL on error. The caller does not +/// own the handle. The handle remains valid as long as |struct_tree| +/// remains valid. +/// Comments: +/// The |index| must be less than the FPDF_StructTree_CountChildren() +/// return value. +FPDF_STRUCTELEMENT FPDF_StructTree_GetChildAtIndex(FPDF_STRUCTTREE struct_tree, +int index, +) { + return _FPDF_StructTree_GetChildAtIndex(struct_tree, +index, +); +} + +late final _FPDF_StructTree_GetChildAtIndexPtr = _lookup< + ffi.NativeFunction>('FPDF_StructTree_GetChildAtIndex'); +late final _FPDF_StructTree_GetChildAtIndex = _FPDF_StructTree_GetChildAtIndexPtr.asFunction(); + +/// Function: FPDF_StructElement_GetAltText +/// Get the alt text for a given element. +/// Parameters: +/// struct_element - Handle to the struct element. +/// buffer - A buffer for output the alt text. May be NULL. +/// buflen - The length of the buffer, in bytes. May be 0. +/// Return value: +/// The number of bytes in the alt text, including the terminating NUL +/// character. The number of bytes is returned regardless of the +/// |buffer| and |buflen| parameters. +/// Comments: +/// Regardless of the platform, the |buffer| is always in UTF-16LE +/// encoding. The string is terminated by a UTF16 NUL character. If +/// |buflen| is less than the required length, or |buffer| is NULL, +/// |buffer| will not be modified. +int FPDF_StructElement_GetAltText(FPDF_STRUCTELEMENT struct_element, +ffi.Pointer buffer, +int buflen, +) { + return _FPDF_StructElement_GetAltText(struct_element, +buffer, +buflen, +); +} + +late final _FPDF_StructElement_GetAltTextPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDF_StructElement_GetAltText'); +late final _FPDF_StructElement_GetAltText = _FPDF_StructElement_GetAltTextPtr.asFunction , int )>(); + +/// Experimental API. +/// Function: FPDF_StructElement_GetActualText +/// Get the actual text for a given element. +/// Parameters: +/// struct_element - Handle to the struct element. +/// buffer - A buffer for output the actual text. May be NULL. +/// buflen - The length of the buffer, in bytes. May be 0. +/// Return value: +/// The number of bytes in the actual text, including the terminating +/// NUL character. The number of bytes is returned regardless of the +/// |buffer| and |buflen| parameters. +/// Comments: +/// Regardless of the platform, the |buffer| is always in UTF-16LE +/// encoding. The string is terminated by a UTF16 NUL character. If +/// |buflen| is less than the required length, or |buffer| is NULL, +/// |buffer| will not be modified. +int FPDF_StructElement_GetActualText(FPDF_STRUCTELEMENT struct_element, +ffi.Pointer buffer, +int buflen, +) { + return _FPDF_StructElement_GetActualText(struct_element, +buffer, +buflen, +); +} + +late final _FPDF_StructElement_GetActualTextPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDF_StructElement_GetActualText'); +late final _FPDF_StructElement_GetActualText = _FPDF_StructElement_GetActualTextPtr.asFunction , int )>(); + +/// Function: FPDF_StructElement_GetID +/// Get the ID for a given element. +/// Parameters: +/// struct_element - Handle to the struct element. +/// buffer - A buffer for output the ID string. May be NULL. +/// buflen - The length of the buffer, in bytes. May be 0. +/// Return value: +/// The number of bytes in the ID string, including the terminating NUL +/// character. The number of bytes is returned regardless of the +/// |buffer| and |buflen| parameters. +/// Comments: +/// Regardless of the platform, the |buffer| is always in UTF-16LE +/// encoding. The string is terminated by a UTF16 NUL character. If +/// |buflen| is less than the required length, or |buffer| is NULL, +/// |buffer| will not be modified. +int FPDF_StructElement_GetID(FPDF_STRUCTELEMENT struct_element, +ffi.Pointer buffer, +int buflen, +) { + return _FPDF_StructElement_GetID(struct_element, +buffer, +buflen, +); +} + +late final _FPDF_StructElement_GetIDPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDF_StructElement_GetID'); +late final _FPDF_StructElement_GetID = _FPDF_StructElement_GetIDPtr.asFunction , int )>(); + +/// Experimental API. +/// Function: FPDF_StructElement_GetLang +/// Get the case-insensitive IETF BCP 47 language code for an element. +/// Parameters: +/// struct_element - Handle to the struct element. +/// buffer - A buffer for output the lang string. May be NULL. +/// buflen - The length of the buffer, in bytes. May be 0. +/// Return value: +/// The number of bytes in the ID string, including the terminating NUL +/// character. The number of bytes is returned regardless of the +/// |buffer| and |buflen| parameters. +/// Comments: +/// Regardless of the platform, the |buffer| is always in UTF-16LE +/// encoding. The string is terminated by a UTF16 NUL character. If +/// |buflen| is less than the required length, or |buffer| is NULL, +/// |buffer| will not be modified. +int FPDF_StructElement_GetLang(FPDF_STRUCTELEMENT struct_element, +ffi.Pointer buffer, +int buflen, +) { + return _FPDF_StructElement_GetLang(struct_element, +buffer, +buflen, +); +} + +late final _FPDF_StructElement_GetLangPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDF_StructElement_GetLang'); +late final _FPDF_StructElement_GetLang = _FPDF_StructElement_GetLangPtr.asFunction , int )>(); + +/// Experimental API. +/// Function: FPDF_StructElement_GetStringAttribute +/// Get a struct element attribute of type "name" or "string". +/// Parameters: +/// struct_element - Handle to the struct element. +/// attr_name - The name of the attribute to retrieve. +/// buffer - A buffer for output. May be NULL. +/// buflen - The length of the buffer, in bytes. May be 0. +/// Return value: +/// The number of bytes in the attribute value, including the +/// terminating NUL character. The number of bytes is returned +/// regardless of the |buffer| and |buflen| parameters. +/// Comments: +/// Regardless of the platform, the |buffer| is always in UTF-16LE +/// encoding. The string is terminated by a UTF16 NUL character. If +/// |buflen| is less than the required length, or |buffer| is NULL, +/// |buffer| will not be modified. +int FPDF_StructElement_GetStringAttribute(FPDF_STRUCTELEMENT struct_element, +FPDF_BYTESTRING attr_name, +ffi.Pointer buffer, +int buflen, +) { + return _FPDF_StructElement_GetStringAttribute(struct_element, +attr_name, +buffer, +buflen, +); +} + +late final _FPDF_StructElement_GetStringAttributePtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDF_StructElement_GetStringAttribute'); +late final _FPDF_StructElement_GetStringAttribute = _FPDF_StructElement_GetStringAttributePtr.asFunction , int )>(); + +/// Function: FPDF_StructElement_GetMarkedContentID +/// Get the marked content ID for a given element. +/// Parameters: +/// struct_element - Handle to the struct element. +/// Return value: +/// The marked content ID of the element. If no ID exists, returns +/// -1. +/// Comments: +/// FPDF_StructElement_GetMarkedContentIdAtIndex() may be able to +/// extract more marked content IDs out of |struct_element|. This API +/// may be deprecated in the future. +int FPDF_StructElement_GetMarkedContentID(FPDF_STRUCTELEMENT struct_element, +) { + return _FPDF_StructElement_GetMarkedContentID(struct_element, +); +} + +late final _FPDF_StructElement_GetMarkedContentIDPtr = _lookup< + ffi.NativeFunction>('FPDF_StructElement_GetMarkedContentID'); +late final _FPDF_StructElement_GetMarkedContentID = _FPDF_StructElement_GetMarkedContentIDPtr.asFunction(); + +/// Function: FPDF_StructElement_GetType +/// Get the type (/S) for a given element. +/// Parameters: +/// struct_element - Handle to the struct element. +/// buffer - A buffer for output. May be NULL. +/// buflen - The length of the buffer, in bytes. May be 0. +/// Return value: +/// The number of bytes in the type, including the terminating NUL +/// character. The number of bytes is returned regardless of the +/// |buffer| and |buflen| parameters. +/// Comments: +/// Regardless of the platform, the |buffer| is always in UTF-16LE +/// encoding. The string is terminated by a UTF16 NUL character. If +/// |buflen| is less than the required length, or |buffer| is NULL, +/// |buffer| will not be modified. +int FPDF_StructElement_GetType(FPDF_STRUCTELEMENT struct_element, +ffi.Pointer buffer, +int buflen, +) { + return _FPDF_StructElement_GetType(struct_element, +buffer, +buflen, +); +} + +late final _FPDF_StructElement_GetTypePtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDF_StructElement_GetType'); +late final _FPDF_StructElement_GetType = _FPDF_StructElement_GetTypePtr.asFunction , int )>(); + +/// Experimental API. +/// Function: FPDF_StructElement_GetObjType +/// Get the object type (/Type) for a given element. +/// Parameters: +/// struct_element - Handle to the struct element. +/// buffer - A buffer for output. May be NULL. +/// buflen - The length of the buffer, in bytes. May be 0. +/// Return value: +/// The number of bytes in the object type, including the terminating +/// NUL character. The number of bytes is returned regardless of the +/// |buffer| and |buflen| parameters. +/// Comments: +/// Regardless of the platform, the |buffer| is always in UTF-16LE +/// encoding. The string is terminated by a UTF16 NUL character. If +/// |buflen| is less than the required length, or |buffer| is NULL, +/// |buffer| will not be modified. +int FPDF_StructElement_GetObjType(FPDF_STRUCTELEMENT struct_element, +ffi.Pointer buffer, +int buflen, +) { + return _FPDF_StructElement_GetObjType(struct_element, +buffer, +buflen, +); +} + +late final _FPDF_StructElement_GetObjTypePtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDF_StructElement_GetObjType'); +late final _FPDF_StructElement_GetObjType = _FPDF_StructElement_GetObjTypePtr.asFunction , int )>(); + +/// Function: FPDF_StructElement_GetTitle +/// Get the title (/T) for a given element. +/// Parameters: +/// struct_element - Handle to the struct element. +/// buffer - A buffer for output. May be NULL. +/// buflen - The length of the buffer, in bytes. May be 0. +/// Return value: +/// The number of bytes in the title, including the terminating NUL +/// character. The number of bytes is returned regardless of the +/// |buffer| and |buflen| parameters. +/// Comments: +/// Regardless of the platform, the |buffer| is always in UTF-16LE +/// encoding. The string is terminated by a UTF16 NUL character. If +/// |buflen| is less than the required length, or |buffer| is NULL, +/// |buffer| will not be modified. +int FPDF_StructElement_GetTitle(FPDF_STRUCTELEMENT struct_element, +ffi.Pointer buffer, +int buflen, +) { + return _FPDF_StructElement_GetTitle(struct_element, +buffer, +buflen, +); +} + +late final _FPDF_StructElement_GetTitlePtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDF_StructElement_GetTitle'); +late final _FPDF_StructElement_GetTitle = _FPDF_StructElement_GetTitlePtr.asFunction , int )>(); + +/// Function: FPDF_StructElement_CountChildren +/// Count the number of children for the structure element. +/// Parameters: +/// struct_element - Handle to the struct element. +/// Return value: +/// The number of children, or -1 on error. +int FPDF_StructElement_CountChildren(FPDF_STRUCTELEMENT struct_element, +) { + return _FPDF_StructElement_CountChildren(struct_element, +); +} + +late final _FPDF_StructElement_CountChildrenPtr = _lookup< + ffi.NativeFunction>('FPDF_StructElement_CountChildren'); +late final _FPDF_StructElement_CountChildren = _FPDF_StructElement_CountChildrenPtr.asFunction(); + +/// Function: FPDF_StructElement_GetChildAtIndex +/// Get a child in the structure element. +/// Parameters: +/// struct_element - Handle to the struct element. +/// index - The index for the child, 0-based. +/// Return value: +/// The child at the n-th index or NULL on error. +/// Comments: +/// If the child exists but is not an element, then this function will +/// return NULL. This will also return NULL for out of bounds indices. +/// The |index| must be less than the FPDF_StructElement_CountChildren() +/// return value. +FPDF_STRUCTELEMENT FPDF_StructElement_GetChildAtIndex(FPDF_STRUCTELEMENT struct_element, +int index, +) { + return _FPDF_StructElement_GetChildAtIndex(struct_element, +index, +); +} + +late final _FPDF_StructElement_GetChildAtIndexPtr = _lookup< + ffi.NativeFunction>('FPDF_StructElement_GetChildAtIndex'); +late final _FPDF_StructElement_GetChildAtIndex = _FPDF_StructElement_GetChildAtIndexPtr.asFunction(); + +/// Experimental API. +/// Function: FPDF_StructElement_GetChildMarkedContentID +/// Get the child's content id +/// Parameters: +/// struct_element - Handle to the struct element. +/// index - The index for the child, 0-based. +/// Return value: +/// The marked content ID of the child. If no ID exists, returns -1. +/// Comments: +/// If the child exists but is not a stream or object, then this +/// function will return -1. This will also return -1 for out of bounds +/// indices. Compared to FPDF_StructElement_GetMarkedContentIdAtIndex, +/// it is scoped to the current page. +/// The |index| must be less than the FPDF_StructElement_CountChildren() +/// return value. +int FPDF_StructElement_GetChildMarkedContentID(FPDF_STRUCTELEMENT struct_element, +int index, +) { + return _FPDF_StructElement_GetChildMarkedContentID(struct_element, +index, +); +} + +late final _FPDF_StructElement_GetChildMarkedContentIDPtr = _lookup< + ffi.NativeFunction>('FPDF_StructElement_GetChildMarkedContentID'); +late final _FPDF_StructElement_GetChildMarkedContentID = _FPDF_StructElement_GetChildMarkedContentIDPtr.asFunction(); + +/// Experimental API. +/// Function: FPDF_StructElement_GetParent +/// Get the parent of the structure element. +/// Parameters: +/// struct_element - Handle to the struct element. +/// Return value: +/// The parent structure element or NULL on error. +/// Comments: +/// If structure element is StructTreeRoot, then this function will +/// return NULL. +FPDF_STRUCTELEMENT FPDF_StructElement_GetParent(FPDF_STRUCTELEMENT struct_element, +) { + return _FPDF_StructElement_GetParent(struct_element, +); +} + +late final _FPDF_StructElement_GetParentPtr = _lookup< + ffi.NativeFunction>('FPDF_StructElement_GetParent'); +late final _FPDF_StructElement_GetParent = _FPDF_StructElement_GetParentPtr.asFunction(); + +/// Function: FPDF_StructElement_GetAttributeCount +/// Count the number of attributes for the structure element. +/// Parameters: +/// struct_element - Handle to the struct element. +/// Return value: +/// The number of attributes, or -1 on error. +int FPDF_StructElement_GetAttributeCount(FPDF_STRUCTELEMENT struct_element, +) { + return _FPDF_StructElement_GetAttributeCount(struct_element, +); +} + +late final _FPDF_StructElement_GetAttributeCountPtr = _lookup< + ffi.NativeFunction>('FPDF_StructElement_GetAttributeCount'); +late final _FPDF_StructElement_GetAttributeCount = _FPDF_StructElement_GetAttributeCountPtr.asFunction(); + +/// Experimental API. +/// Function: FPDF_StructElement_GetAttributeAtIndex +/// Get an attribute object in the structure element. +/// Parameters: +/// struct_element - Handle to the struct element. +/// index - The index for the attribute object, 0-based. +/// Return value: +/// The attribute object at the n-th index or NULL on error. +/// Comments: +/// If the attribute object exists but is not a dict, then this +/// function will return NULL. This will also return NULL for out of +/// bounds indices. The caller does not own the handle. The handle +/// remains valid as long as |struct_element| remains valid. +/// The |index| must be less than the +/// FPDF_StructElement_GetAttributeCount() return value. +FPDF_STRUCTELEMENT_ATTR FPDF_StructElement_GetAttributeAtIndex(FPDF_STRUCTELEMENT struct_element, +int index, +) { + return _FPDF_StructElement_GetAttributeAtIndex(struct_element, +index, +); +} + +late final _FPDF_StructElement_GetAttributeAtIndexPtr = _lookup< + ffi.NativeFunction>('FPDF_StructElement_GetAttributeAtIndex'); +late final _FPDF_StructElement_GetAttributeAtIndex = _FPDF_StructElement_GetAttributeAtIndexPtr.asFunction(); + +/// Experimental API. +/// Function: FPDF_StructElement_Attr_GetCount +/// Count the number of attributes in a structure element attribute map. +/// Parameters: +/// struct_attribute - Handle to the struct element attribute. +/// Return value: +/// The number of attributes, or -1 on error. +int FPDF_StructElement_Attr_GetCount(FPDF_STRUCTELEMENT_ATTR struct_attribute, +) { + return _FPDF_StructElement_Attr_GetCount(struct_attribute, +); +} + +late final _FPDF_StructElement_Attr_GetCountPtr = _lookup< + ffi.NativeFunction>('FPDF_StructElement_Attr_GetCount'); +late final _FPDF_StructElement_Attr_GetCount = _FPDF_StructElement_Attr_GetCountPtr.asFunction(); + +/// Experimental API. +/// Function: FPDF_StructElement_Attr_GetName +/// Get the name of an attribute in a structure element attribute map. +/// Parameters: +/// struct_attribute - Handle to the struct element attribute. +/// index - The index of attribute in the map. +/// buffer - A buffer for output. May be NULL. This is only +/// modified if |buflen| is longer than the length +/// of the key. Optional, pass null to just +/// retrieve the size of the buffer needed. +/// buflen - The length of the buffer. +/// out_buflen - A pointer to variable that will receive the +/// minimum buffer size to contain the key. Not +/// filled if FALSE is returned. +/// Return value: +/// TRUE if the operation was successful, FALSE otherwise. +int FPDF_StructElement_Attr_GetName(FPDF_STRUCTELEMENT_ATTR struct_attribute, +int index, +ffi.Pointer buffer, +int buflen, +ffi.Pointer out_buflen, +) { + return _FPDF_StructElement_Attr_GetName(struct_attribute, +index, +buffer, +buflen, +out_buflen, +); +} + +late final _FPDF_StructElement_Attr_GetNamePtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong , ffi.Pointer )>>('FPDF_StructElement_Attr_GetName'); +late final _FPDF_StructElement_Attr_GetName = _FPDF_StructElement_Attr_GetNamePtr.asFunction , int , ffi.Pointer )>(); + +/// Experimental API. +/// Function: FPDF_StructElement_Attr_GetValue +/// Get a handle to a value for an attribute in a structure element +/// attribute map. +/// Parameters: +/// struct_attribute - Handle to the struct element attribute. +/// name - The attribute name. +/// Return value: +/// Returns a handle to the value associated with the input, if any. +/// Returns NULL on failure. The caller does not own the handle. +/// The handle remains valid as long as |struct_attribute| remains +/// valid. +FPDF_STRUCTELEMENT_ATTR_VALUE FPDF_StructElement_Attr_GetValue(FPDF_STRUCTELEMENT_ATTR struct_attribute, +FPDF_BYTESTRING name, +) { + return _FPDF_StructElement_Attr_GetValue(struct_attribute, +name, +); +} + +late final _FPDF_StructElement_Attr_GetValuePtr = _lookup< + ffi.NativeFunction>('FPDF_StructElement_Attr_GetValue'); +late final _FPDF_StructElement_Attr_GetValue = _FPDF_StructElement_Attr_GetValuePtr.asFunction(); + +/// Experimental API. +/// Function: FPDF_StructElement_Attr_GetType +/// Get the type of an attribute in a structure element attribute map. +/// Parameters: +/// value - Handle to the value. +/// Return value: +/// Returns the type of the value, or FPDF_OBJECT_UNKNOWN in case of +/// failure. Note that this will never return FPDF_OBJECT_REFERENCE, as +/// references are always dereferenced. +int FPDF_StructElement_Attr_GetType(FPDF_STRUCTELEMENT_ATTR_VALUE value, +) { + return _FPDF_StructElement_Attr_GetType(value, +); +} + +late final _FPDF_StructElement_Attr_GetTypePtr = _lookup< + ffi.NativeFunction>('FPDF_StructElement_Attr_GetType'); +late final _FPDF_StructElement_Attr_GetType = _FPDF_StructElement_Attr_GetTypePtr.asFunction(); + +/// Experimental API. +/// Function: FPDF_StructElement_Attr_GetBooleanValue +/// Get the value of a boolean attribute in an attribute map as +/// FPDF_BOOL. FPDF_StructElement_Attr_GetType() should have returned +/// FPDF_OBJECT_BOOLEAN for this property. +/// Parameters: +/// value - Handle to the value. +/// out_value - A pointer to variable that will receive the value. Not +/// filled if false is returned. +/// Return value: +/// Returns TRUE if the attribute maps to a boolean value, FALSE +/// otherwise. +int FPDF_StructElement_Attr_GetBooleanValue(FPDF_STRUCTELEMENT_ATTR_VALUE value, +ffi.Pointer out_value, +) { + return _FPDF_StructElement_Attr_GetBooleanValue(value, +out_value, +); +} + +late final _FPDF_StructElement_Attr_GetBooleanValuePtr = _lookup< + ffi.NativeFunction )>>('FPDF_StructElement_Attr_GetBooleanValue'); +late final _FPDF_StructElement_Attr_GetBooleanValue = _FPDF_StructElement_Attr_GetBooleanValuePtr.asFunction )>(); + +/// Experimental API. +/// Function: FPDF_StructElement_Attr_GetNumberValue +/// Get the value of a number attribute in an attribute map as float. +/// FPDF_StructElement_Attr_GetType() should have returned +/// FPDF_OBJECT_NUMBER for this property. +/// Parameters: +/// value - Handle to the value. +/// out_value - A pointer to variable that will receive the value. Not +/// filled if false is returned. +/// Return value: +/// Returns TRUE if the attribute maps to a number value, FALSE +/// otherwise. +int FPDF_StructElement_Attr_GetNumberValue(FPDF_STRUCTELEMENT_ATTR_VALUE value, +ffi.Pointer out_value, +) { + return _FPDF_StructElement_Attr_GetNumberValue(value, +out_value, +); +} + +late final _FPDF_StructElement_Attr_GetNumberValuePtr = _lookup< + ffi.NativeFunction )>>('FPDF_StructElement_Attr_GetNumberValue'); +late final _FPDF_StructElement_Attr_GetNumberValue = _FPDF_StructElement_Attr_GetNumberValuePtr.asFunction )>(); + +/// Experimental API. +/// Function: FPDF_StructElement_Attr_GetStringValue +/// Get the value of a string attribute in an attribute map as string. +/// FPDF_StructElement_Attr_GetType() should have returned +/// FPDF_OBJECT_STRING or FPDF_OBJECT_NAME for this property. +/// Parameters: +/// value - Handle to the value. +/// buffer - A buffer for holding the returned key in UTF-16LE. +/// This is only modified if |buflen| is longer than the +/// length of the key. Optional, pass null to just +/// retrieve the size of the buffer needed. +/// buflen - The length of the buffer. +/// out_buflen - A pointer to variable that will receive the minimum +/// buffer size to contain the key. Not filled if FALSE is +/// returned. +/// Return value: +/// Returns TRUE if the attribute maps to a string value, FALSE +/// otherwise. +int FPDF_StructElement_Attr_GetStringValue(FPDF_STRUCTELEMENT_ATTR_VALUE value, +ffi.Pointer buffer, +int buflen, +ffi.Pointer out_buflen, +) { + return _FPDF_StructElement_Attr_GetStringValue(value, +buffer, +buflen, +out_buflen, +); +} + +late final _FPDF_StructElement_Attr_GetStringValuePtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong , ffi.Pointer )>>('FPDF_StructElement_Attr_GetStringValue'); +late final _FPDF_StructElement_Attr_GetStringValue = _FPDF_StructElement_Attr_GetStringValuePtr.asFunction , int , ffi.Pointer )>(); + +/// Experimental API. +/// Function: FPDF_StructElement_Attr_GetBlobValue +/// Get the value of a blob attribute in an attribute map as string. +/// Parameters: +/// value - Handle to the value. +/// buffer - A buffer for holding the returned value. This is only +/// modified if |buflen| is at least as long as the length +/// of the value. Optional, pass null to just retrieve the +/// size of the buffer needed. +/// buflen - The length of the buffer. +/// out_buflen - A pointer to variable that will receive the minimum +/// buffer size to contain the key. Not filled if FALSE is +/// returned. +/// Return value: +/// Returns TRUE if the attribute maps to a string value, FALSE +/// otherwise. +int FPDF_StructElement_Attr_GetBlobValue(FPDF_STRUCTELEMENT_ATTR_VALUE value, +ffi.Pointer buffer, +int buflen, +ffi.Pointer out_buflen, +) { + return _FPDF_StructElement_Attr_GetBlobValue(value, +buffer, +buflen, +out_buflen, +); +} + +late final _FPDF_StructElement_Attr_GetBlobValuePtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong , ffi.Pointer )>>('FPDF_StructElement_Attr_GetBlobValue'); +late final _FPDF_StructElement_Attr_GetBlobValue = _FPDF_StructElement_Attr_GetBlobValuePtr.asFunction , int , ffi.Pointer )>(); + +/// Experimental API. +/// Function: FPDF_StructElement_Attr_CountChildren +/// Count the number of children values in an attribute. +/// Parameters: +/// value - Handle to the value. +/// Return value: +/// The number of children, or -1 on error. +int FPDF_StructElement_Attr_CountChildren(FPDF_STRUCTELEMENT_ATTR_VALUE value, +) { + return _FPDF_StructElement_Attr_CountChildren(value, +); +} + +late final _FPDF_StructElement_Attr_CountChildrenPtr = _lookup< + ffi.NativeFunction>('FPDF_StructElement_Attr_CountChildren'); +late final _FPDF_StructElement_Attr_CountChildren = _FPDF_StructElement_Attr_CountChildrenPtr.asFunction(); + +/// Experimental API. +/// Function: FPDF_StructElement_Attr_GetChildAtIndex +/// Get a child from an attribute. +/// Parameters: +/// value - Handle to the value. +/// index - The index for the child, 0-based. +/// Return value: +/// The child at the n-th index or NULL on error. +/// Comments: +/// The |index| must be less than the +/// FPDF_StructElement_Attr_CountChildren() return value. +FPDF_STRUCTELEMENT_ATTR_VALUE FPDF_StructElement_Attr_GetChildAtIndex(FPDF_STRUCTELEMENT_ATTR_VALUE value, +int index, +) { + return _FPDF_StructElement_Attr_GetChildAtIndex(value, +index, +); +} + +late final _FPDF_StructElement_Attr_GetChildAtIndexPtr = _lookup< + ffi.NativeFunction>('FPDF_StructElement_Attr_GetChildAtIndex'); +late final _FPDF_StructElement_Attr_GetChildAtIndex = _FPDF_StructElement_Attr_GetChildAtIndexPtr.asFunction(); + +/// Experimental API. +/// Function: FPDF_StructElement_GetMarkedContentIdCount +/// Get the count of marked content ids for a given element. +/// Parameters: +/// struct_element - Handle to the struct element. +/// Return value: +/// The count of marked content ids or -1 if none exists. +int FPDF_StructElement_GetMarkedContentIdCount(FPDF_STRUCTELEMENT struct_element, +) { + return _FPDF_StructElement_GetMarkedContentIdCount(struct_element, +); +} + +late final _FPDF_StructElement_GetMarkedContentIdCountPtr = _lookup< + ffi.NativeFunction>('FPDF_StructElement_GetMarkedContentIdCount'); +late final _FPDF_StructElement_GetMarkedContentIdCount = _FPDF_StructElement_GetMarkedContentIdCountPtr.asFunction(); + +/// Experimental API. +/// Function: FPDF_StructElement_GetMarkedContentIdAtIndex +/// Get the marked content id at a given index for a given element. +/// Parameters: +/// struct_element - Handle to the struct element. +/// index - The index of the marked content id, 0-based. +/// Return value: +/// The marked content ID of the element. If no ID exists, returns +/// -1. +/// Comments: +/// The |index| must be less than the +/// FPDF_StructElement_GetMarkedContentIdCount() return value. +/// This will likely supersede FPDF_StructElement_GetMarkedContentID(). +int FPDF_StructElement_GetMarkedContentIdAtIndex(FPDF_STRUCTELEMENT struct_element, +int index, +) { + return _FPDF_StructElement_GetMarkedContentIdAtIndex(struct_element, +index, +); +} + +late final _FPDF_StructElement_GetMarkedContentIdAtIndexPtr = _lookup< + ffi.NativeFunction>('FPDF_StructElement_GetMarkedContentIdAtIndex'); +late final _FPDF_StructElement_GetMarkedContentIdAtIndex = _FPDF_StructElement_GetMarkedContentIdAtIndexPtr.asFunction(); + +/// Create a document availability provider. +/// +/// file_avail - pointer to file availability interface. +/// file - pointer to a file access interface. +/// +/// Returns a handle to the document availability provider, or NULL on error. +/// +/// FPDFAvail_Destroy() must be called when done with the availability provider. +FPDF_AVAIL FPDFAvail_Create(ffi.Pointer file_avail, +ffi.Pointer file, +) { + return _FPDFAvail_Create(file_avail, +file, +); +} + +late final _FPDFAvail_CreatePtr = _lookup< + ffi.NativeFunction , ffi.Pointer )>>('FPDFAvail_Create'); +late final _FPDFAvail_Create = _FPDFAvail_CreatePtr.asFunction , ffi.Pointer )>(); + +/// Destroy the |avail| document availability provider. +/// +/// avail - handle to document availability provider to be destroyed. +void FPDFAvail_Destroy(FPDF_AVAIL avail, +) { + return _FPDFAvail_Destroy(avail, +); +} + +late final _FPDFAvail_DestroyPtr = _lookup< + ffi.NativeFunction>('FPDFAvail_Destroy'); +late final _FPDFAvail_Destroy = _FPDFAvail_DestroyPtr.asFunction(); + +/// Checks if the document is ready for loading, if not, gets download hints. +/// +/// avail - handle to document availability provider. +/// hints - pointer to a download hints interface. +/// +/// Returns one of: +/// PDF_DATA_ERROR: A common error is returned. Data availability unknown. +/// PDF_DATA_NOTAVAIL: Data not yet available. +/// PDF_DATA_AVAIL: Data available. +/// +/// Applications should call this function whenever new data arrives, and process +/// all the generated download hints, if any, until the function returns +/// |PDF_DATA_ERROR| or |PDF_DATA_AVAIL|. +/// if hints is nullptr, the function just check current document availability. +/// +/// Once all data is available, call FPDFAvail_GetDocument() to get a document +/// handle. +int FPDFAvail_IsDocAvail(FPDF_AVAIL avail, +ffi.Pointer hints, +) { + return _FPDFAvail_IsDocAvail(avail, +hints, +); +} + +late final _FPDFAvail_IsDocAvailPtr = _lookup< + ffi.NativeFunction )>>('FPDFAvail_IsDocAvail'); +late final _FPDFAvail_IsDocAvail = _FPDFAvail_IsDocAvailPtr.asFunction )>(); + +/// Get document from the availability provider. +/// +/// avail - handle to document availability provider. +/// password - password for decrypting the PDF file. Optional. +/// +/// Returns a handle to the document. +/// +/// When FPDFAvail_IsDocAvail() returns TRUE, call FPDFAvail_GetDocument() to +/// retrieve the document handle. +/// See the comments for FPDF_LoadDocument() regarding the encoding for +/// |password|. +FPDF_DOCUMENT FPDFAvail_GetDocument(FPDF_AVAIL avail, +FPDF_BYTESTRING password, +) { + return _FPDFAvail_GetDocument(avail, +password, +); +} + +late final _FPDFAvail_GetDocumentPtr = _lookup< + ffi.NativeFunction>('FPDFAvail_GetDocument'); +late final _FPDFAvail_GetDocument = _FPDFAvail_GetDocumentPtr.asFunction(); + +/// Get the page number for the first available page in a linearized PDF. +/// +/// doc - document handle. +/// +/// Returns the zero-based index for the first available page. +/// +/// For most linearized PDFs, the first available page will be the first page, +/// however, some PDFs might make another page the first available page. +/// For non-linearized PDFs, this function will always return zero. +int FPDFAvail_GetFirstPageNum(FPDF_DOCUMENT doc, +) { + return _FPDFAvail_GetFirstPageNum(doc, +); +} + +late final _FPDFAvail_GetFirstPageNumPtr = _lookup< + ffi.NativeFunction>('FPDFAvail_GetFirstPageNum'); +late final _FPDFAvail_GetFirstPageNum = _FPDFAvail_GetFirstPageNumPtr.asFunction(); + +/// Check if |page_index| is ready for loading, if not, get the +/// |FX_DOWNLOADHINTS|. +/// +/// avail - handle to document availability provider. +/// page_index - index number of the page. Zero for the first page. +/// hints - pointer to a download hints interface. Populated if +/// |page_index| is not available. +/// +/// Returns one of: +/// PDF_DATA_ERROR: A common error is returned. Data availability unknown. +/// PDF_DATA_NOTAVAIL: Data not yet available. +/// PDF_DATA_AVAIL: Data available. +/// +/// This function can be called only after FPDFAvail_GetDocument() is called. +/// Applications should call this function whenever new data arrives and process +/// all the generated download |hints|, if any, until this function returns +/// |PDF_DATA_ERROR| or |PDF_DATA_AVAIL|. Applications can then perform page +/// loading. +/// if hints is nullptr, the function just check current availability of +/// specified page. +int FPDFAvail_IsPageAvail(FPDF_AVAIL avail, +int page_index, +ffi.Pointer hints, +) { + return _FPDFAvail_IsPageAvail(avail, +page_index, +hints, +); +} + +late final _FPDFAvail_IsPageAvailPtr = _lookup< + ffi.NativeFunction )>>('FPDFAvail_IsPageAvail'); +late final _FPDFAvail_IsPageAvail = _FPDFAvail_IsPageAvailPtr.asFunction )>(); + +/// Check if form data is ready for initialization, if not, get the +/// |FX_DOWNLOADHINTS|. +/// +/// avail - handle to document availability provider. +/// hints - pointer to a download hints interface. Populated if form is not +/// ready for initialization. +/// +/// Returns one of: +/// PDF_FORM_ERROR: A common eror, in general incorrect parameters. +/// PDF_FORM_NOTAVAIL: Data not available. +/// PDF_FORM_AVAIL: Data available. +/// PDF_FORM_NOTEXIST: No form data. +/// +/// This function can be called only after FPDFAvail_GetDocument() is called. +/// The application should call this function whenever new data arrives and +/// process all the generated download |hints|, if any, until the function +/// |PDF_FORM_ERROR|, |PDF_FORM_AVAIL| or |PDF_FORM_NOTEXIST|. +/// if hints is nullptr, the function just check current form availability. +/// +/// Applications can then perform page loading. It is recommend to call +/// FPDFDOC_InitFormFillEnvironment() when |PDF_FORM_AVAIL| is returned. +int FPDFAvail_IsFormAvail(FPDF_AVAIL avail, +ffi.Pointer hints, +) { + return _FPDFAvail_IsFormAvail(avail, +hints, +); +} + +late final _FPDFAvail_IsFormAvailPtr = _lookup< + ffi.NativeFunction )>>('FPDFAvail_IsFormAvail'); +late final _FPDFAvail_IsFormAvail = _FPDFAvail_IsFormAvailPtr.asFunction )>(); + +/// Check whether a document is a linearized PDF. +/// +/// avail - handle to document availability provider. +/// +/// Returns one of: +/// PDF_LINEARIZED +/// PDF_NOT_LINEARIZED +/// PDF_LINEARIZATION_UNKNOWN +/// +/// FPDFAvail_IsLinearized() will return |PDF_LINEARIZED| or |PDF_NOT_LINEARIZED| +/// when we have 1k of data. If the files size less than 1k, it returns +/// |PDF_LINEARIZATION_UNKNOWN| as there is insufficient information to determine +/// if the PDF is linearlized. +int FPDFAvail_IsLinearized(FPDF_AVAIL avail, +) { + return _FPDFAvail_IsLinearized(avail, +); +} + +late final _FPDFAvail_IsLinearizedPtr = _lookup< + ffi.NativeFunction>('FPDFAvail_IsLinearized'); +late final _FPDFAvail_IsLinearized = _FPDFAvail_IsLinearizedPtr.asFunction(); + +/// Setup an unsupported object handler. +/// +/// unsp_info - Pointer to an UNSUPPORT_INFO structure. +/// +/// Returns TRUE on success. +int FSDK_SetUnSpObjProcessHandler(ffi.Pointer unsp_info, +) { + return _FSDK_SetUnSpObjProcessHandler(unsp_info, +); +} + +late final _FSDK_SetUnSpObjProcessHandlerPtr = _lookup< + ffi.NativeFunction )>>('FSDK_SetUnSpObjProcessHandler'); +late final _FSDK_SetUnSpObjProcessHandler = _FSDK_SetUnSpObjProcessHandlerPtr.asFunction )>(); + +/// Set replacement function for calls to time(). +/// +/// This API is intended to be used only for testing, thus may cause PDFium to +/// behave poorly in production environments. +/// +/// func - Function pointer to alternate implementation of time(), or +/// NULL to restore to actual time() call itself. +void FSDK_SetTimeFunction(ffi.Pointer> func, +) { + return _FSDK_SetTimeFunction(func, +); +} + +late final _FSDK_SetTimeFunctionPtr = _lookup< + ffi.NativeFunction> )>>('FSDK_SetTimeFunction'); +late final _FSDK_SetTimeFunction = _FSDK_SetTimeFunctionPtr.asFunction> )>(); + +/// Set replacement function for calls to localtime(). +/// +/// This API is intended to be used only for testing, thus may cause PDFium to +/// behave poorly in production environments. +/// +/// func - Function pointer to alternate implementation of localtime(), or +/// NULL to restore to actual localtime() call itself. +void FSDK_SetLocaltimeFunction(ffi.Pointer Function(ffi.Pointer )>> func, +) { + return _FSDK_SetLocaltimeFunction(func, +); +} + +late final _FSDK_SetLocaltimeFunctionPtr = _lookup< + ffi.NativeFunction Function(ffi.Pointer )>> )>>('FSDK_SetLocaltimeFunction'); +late final _FSDK_SetLocaltimeFunction = _FSDK_SetLocaltimeFunctionPtr.asFunction Function(ffi.Pointer )>> )>(); + +/// Get the document's PageMode. +/// +/// doc - Handle to document. +/// +/// Returns one of the |PAGEMODE_*| flags defined above. +/// +/// The page mode defines how the document should be initially displayed. +int FPDFDoc_GetPageMode(FPDF_DOCUMENT document, +) { + return _FPDFDoc_GetPageMode(document, +); +} + +late final _FPDFDoc_GetPageModePtr = _lookup< + ffi.NativeFunction>('FPDFDoc_GetPageMode'); +late final _FPDFDoc_GetPageMode = _FPDFDoc_GetPageModePtr.asFunction(); + +/// Set "MediaBox" entry to the page dictionary. +/// +/// page - Handle to a page. +/// left - The left of the rectangle. +/// bottom - The bottom of the rectangle. +/// right - The right of the rectangle. +/// top - The top of the rectangle. +void FPDFPage_SetMediaBox(FPDF_PAGE page, +double left, +double bottom, +double right, +double top, +) { + return _FPDFPage_SetMediaBox(page, +left, +bottom, +right, +top, +); +} + +late final _FPDFPage_SetMediaBoxPtr = _lookup< + ffi.NativeFunction>('FPDFPage_SetMediaBox'); +late final _FPDFPage_SetMediaBox = _FPDFPage_SetMediaBoxPtr.asFunction(); + +/// Set "CropBox" entry to the page dictionary. +/// +/// page - Handle to a page. +/// left - The left of the rectangle. +/// bottom - The bottom of the rectangle. +/// right - The right of the rectangle. +/// top - The top of the rectangle. +void FPDFPage_SetCropBox(FPDF_PAGE page, +double left, +double bottom, +double right, +double top, +) { + return _FPDFPage_SetCropBox(page, +left, +bottom, +right, +top, +); +} + +late final _FPDFPage_SetCropBoxPtr = _lookup< + ffi.NativeFunction>('FPDFPage_SetCropBox'); +late final _FPDFPage_SetCropBox = _FPDFPage_SetCropBoxPtr.asFunction(); + +/// Set "BleedBox" entry to the page dictionary. +/// +/// page - Handle to a page. +/// left - The left of the rectangle. +/// bottom - The bottom of the rectangle. +/// right - The right of the rectangle. +/// top - The top of the rectangle. +void FPDFPage_SetBleedBox(FPDF_PAGE page, +double left, +double bottom, +double right, +double top, +) { + return _FPDFPage_SetBleedBox(page, +left, +bottom, +right, +top, +); +} + +late final _FPDFPage_SetBleedBoxPtr = _lookup< + ffi.NativeFunction>('FPDFPage_SetBleedBox'); +late final _FPDFPage_SetBleedBox = _FPDFPage_SetBleedBoxPtr.asFunction(); + +/// Set "TrimBox" entry to the page dictionary. +/// +/// page - Handle to a page. +/// left - The left of the rectangle. +/// bottom - The bottom of the rectangle. +/// right - The right of the rectangle. +/// top - The top of the rectangle. +void FPDFPage_SetTrimBox(FPDF_PAGE page, +double left, +double bottom, +double right, +double top, +) { + return _FPDFPage_SetTrimBox(page, +left, +bottom, +right, +top, +); +} + +late final _FPDFPage_SetTrimBoxPtr = _lookup< + ffi.NativeFunction>('FPDFPage_SetTrimBox'); +late final _FPDFPage_SetTrimBox = _FPDFPage_SetTrimBoxPtr.asFunction(); + +/// Set "ArtBox" entry to the page dictionary. +/// +/// page - Handle to a page. +/// left - The left of the rectangle. +/// bottom - The bottom of the rectangle. +/// right - The right of the rectangle. +/// top - The top of the rectangle. +void FPDFPage_SetArtBox(FPDF_PAGE page, +double left, +double bottom, +double right, +double top, +) { + return _FPDFPage_SetArtBox(page, +left, +bottom, +right, +top, +); +} + +late final _FPDFPage_SetArtBoxPtr = _lookup< + ffi.NativeFunction>('FPDFPage_SetArtBox'); +late final _FPDFPage_SetArtBox = _FPDFPage_SetArtBoxPtr.asFunction(); + +/// Get "MediaBox" entry from the page dictionary. +/// +/// page - Handle to a page. +/// left - Pointer to a float value receiving the left of the rectangle. +/// bottom - Pointer to a float value receiving the bottom of the rectangle. +/// right - Pointer to a float value receiving the right of the rectangle. +/// top - Pointer to a float value receiving the top of the rectangle. +/// +/// On success, return true and write to the out parameters. Otherwise return +/// false and leave the out parameters unmodified. +int FPDFPage_GetMediaBox(FPDF_PAGE page, +ffi.Pointer left, +ffi.Pointer bottom, +ffi.Pointer right, +ffi.Pointer top, +) { + return _FPDFPage_GetMediaBox(page, +left, +bottom, +right, +top, +); +} + +late final _FPDFPage_GetMediaBoxPtr = _lookup< + ffi.NativeFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>>('FPDFPage_GetMediaBox'); +late final _FPDFPage_GetMediaBox = _FPDFPage_GetMediaBoxPtr.asFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>(); + +/// Get "CropBox" entry from the page dictionary. +/// +/// page - Handle to a page. +/// left - Pointer to a float value receiving the left of the rectangle. +/// bottom - Pointer to a float value receiving the bottom of the rectangle. +/// right - Pointer to a float value receiving the right of the rectangle. +/// top - Pointer to a float value receiving the top of the rectangle. +/// +/// On success, return true and write to the out parameters. Otherwise return +/// false and leave the out parameters unmodified. +int FPDFPage_GetCropBox(FPDF_PAGE page, +ffi.Pointer left, +ffi.Pointer bottom, +ffi.Pointer right, +ffi.Pointer top, +) { + return _FPDFPage_GetCropBox(page, +left, +bottom, +right, +top, +); +} + +late final _FPDFPage_GetCropBoxPtr = _lookup< + ffi.NativeFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>>('FPDFPage_GetCropBox'); +late final _FPDFPage_GetCropBox = _FPDFPage_GetCropBoxPtr.asFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>(); + +/// Get "BleedBox" entry from the page dictionary. +/// +/// page - Handle to a page. +/// left - Pointer to a float value receiving the left of the rectangle. +/// bottom - Pointer to a float value receiving the bottom of the rectangle. +/// right - Pointer to a float value receiving the right of the rectangle. +/// top - Pointer to a float value receiving the top of the rectangle. +/// +/// On success, return true and write to the out parameters. Otherwise return +/// false and leave the out parameters unmodified. +int FPDFPage_GetBleedBox(FPDF_PAGE page, +ffi.Pointer left, +ffi.Pointer bottom, +ffi.Pointer right, +ffi.Pointer top, +) { + return _FPDFPage_GetBleedBox(page, +left, +bottom, +right, +top, +); +} + +late final _FPDFPage_GetBleedBoxPtr = _lookup< + ffi.NativeFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>>('FPDFPage_GetBleedBox'); +late final _FPDFPage_GetBleedBox = _FPDFPage_GetBleedBoxPtr.asFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>(); + +/// Get "TrimBox" entry from the page dictionary. +/// +/// page - Handle to a page. +/// left - Pointer to a float value receiving the left of the rectangle. +/// bottom - Pointer to a float value receiving the bottom of the rectangle. +/// right - Pointer to a float value receiving the right of the rectangle. +/// top - Pointer to a float value receiving the top of the rectangle. +/// +/// On success, return true and write to the out parameters. Otherwise return +/// false and leave the out parameters unmodified. +int FPDFPage_GetTrimBox(FPDF_PAGE page, +ffi.Pointer left, +ffi.Pointer bottom, +ffi.Pointer right, +ffi.Pointer top, +) { + return _FPDFPage_GetTrimBox(page, +left, +bottom, +right, +top, +); +} + +late final _FPDFPage_GetTrimBoxPtr = _lookup< + ffi.NativeFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>>('FPDFPage_GetTrimBox'); +late final _FPDFPage_GetTrimBox = _FPDFPage_GetTrimBoxPtr.asFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>(); + +/// Get "ArtBox" entry from the page dictionary. +/// +/// page - Handle to a page. +/// left - Pointer to a float value receiving the left of the rectangle. +/// bottom - Pointer to a float value receiving the bottom of the rectangle. +/// right - Pointer to a float value receiving the right of the rectangle. +/// top - Pointer to a float value receiving the top of the rectangle. +/// +/// On success, return true and write to the out parameters. Otherwise return +/// false and leave the out parameters unmodified. +int FPDFPage_GetArtBox(FPDF_PAGE page, +ffi.Pointer left, +ffi.Pointer bottom, +ffi.Pointer right, +ffi.Pointer top, +) { + return _FPDFPage_GetArtBox(page, +left, +bottom, +right, +top, +); +} + +late final _FPDFPage_GetArtBoxPtr = _lookup< + ffi.NativeFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>>('FPDFPage_GetArtBox'); +late final _FPDFPage_GetArtBox = _FPDFPage_GetArtBoxPtr.asFunction , ffi.Pointer , ffi.Pointer , ffi.Pointer )>(); + +/// Apply transforms to |page|. +/// +/// If |matrix| is provided it will be applied to transform the page. +/// If |clipRect| is provided it will be used to clip the resulting page. +/// If neither |matrix| or |clipRect| are provided this method returns |false|. +/// Returns |true| if transforms are applied. +/// +/// This function will transform the whole page, and would take effect to all the +/// objects in the page. +/// +/// page - Page handle. +/// matrix - Transform matrix. +/// clipRect - Clipping rectangle. +int FPDFPage_TransFormWithClip(FPDF_PAGE page, +ffi.Pointer matrix, +ffi.Pointer clipRect, +) { + return _FPDFPage_TransFormWithClip(page, +matrix, +clipRect, +); +} + +late final _FPDFPage_TransFormWithClipPtr = _lookup< + ffi.NativeFunction , ffi.Pointer )>>('FPDFPage_TransFormWithClip'); +late final _FPDFPage_TransFormWithClip = _FPDFPage_TransFormWithClipPtr.asFunction , ffi.Pointer )>(); + +/// Transform (scale, rotate, shear, move) the clip path of page object. +/// page_object - Handle to a page object. Returned by +/// FPDFPageObj_NewImageObj(). +/// +/// a - The coefficient "a" of the matrix. +/// b - The coefficient "b" of the matrix. +/// c - The coefficient "c" of the matrix. +/// d - The coefficient "d" of the matrix. +/// e - The coefficient "e" of the matrix. +/// f - The coefficient "f" of the matrix. +void FPDFPageObj_TransformClipPath(FPDF_PAGEOBJECT page_object, +double a, +double b, +double c, +double d, +double e, +double f, +) { + return _FPDFPageObj_TransformClipPath(page_object, +a, +b, +c, +d, +e, +f, +); +} + +late final _FPDFPageObj_TransformClipPathPtr = _lookup< + ffi.NativeFunction>('FPDFPageObj_TransformClipPath'); +late final _FPDFPageObj_TransformClipPath = _FPDFPageObj_TransformClipPathPtr.asFunction(); + +/// Experimental API. +/// Get the clip path of the page object. +/// +/// page object - Handle to a page object. Returned by e.g. +/// FPDFPage_GetObject(). +/// +/// Returns the handle to the clip path, or NULL on failure. The caller does not +/// take ownership of the returned FPDF_CLIPPATH. Instead, it remains valid until +/// FPDF_ClosePage() is called for the page containing |page_object|. +FPDF_CLIPPATH FPDFPageObj_GetClipPath(FPDF_PAGEOBJECT page_object, +) { + return _FPDFPageObj_GetClipPath(page_object, +); +} + +late final _FPDFPageObj_GetClipPathPtr = _lookup< + ffi.NativeFunction>('FPDFPageObj_GetClipPath'); +late final _FPDFPageObj_GetClipPath = _FPDFPageObj_GetClipPathPtr.asFunction(); + +/// Experimental API. +/// Get number of paths inside |clip_path|. +/// +/// clip_path - handle to a clip_path. +/// +/// Returns the number of objects in |clip_path| or -1 on failure. +int FPDFClipPath_CountPaths(FPDF_CLIPPATH clip_path, +) { + return _FPDFClipPath_CountPaths(clip_path, +); +} + +late final _FPDFClipPath_CountPathsPtr = _lookup< + ffi.NativeFunction>('FPDFClipPath_CountPaths'); +late final _FPDFClipPath_CountPaths = _FPDFClipPath_CountPathsPtr.asFunction(); + +/// Experimental API. +/// Get number of segments inside one path of |clip_path|. +/// +/// clip_path - handle to a clip_path. +/// path_index - index into the array of paths of the clip path. +/// +/// Returns the number of segments or -1 on failure. +int FPDFClipPath_CountPathSegments(FPDF_CLIPPATH clip_path, +int path_index, +) { + return _FPDFClipPath_CountPathSegments(clip_path, +path_index, +); +} + +late final _FPDFClipPath_CountPathSegmentsPtr = _lookup< + ffi.NativeFunction>('FPDFClipPath_CountPathSegments'); +late final _FPDFClipPath_CountPathSegments = _FPDFClipPath_CountPathSegmentsPtr.asFunction(); + +/// Experimental API. +/// Get segment in one specific path of |clip_path| at index. +/// +/// clip_path - handle to a clip_path. +/// path_index - the index of a path. +/// segment_index - the index of a segment. +/// +/// Returns the handle to the segment, or NULL on failure. The caller does not +/// take ownership of the returned FPDF_PATHSEGMENT. Instead, it remains valid +/// until FPDF_ClosePage() is called for the page containing |clip_path|. +FPDF_PATHSEGMENT FPDFClipPath_GetPathSegment(FPDF_CLIPPATH clip_path, +int path_index, +int segment_index, +) { + return _FPDFClipPath_GetPathSegment(clip_path, +path_index, +segment_index, +); +} + +late final _FPDFClipPath_GetPathSegmentPtr = _lookup< + ffi.NativeFunction>('FPDFClipPath_GetPathSegment'); +late final _FPDFClipPath_GetPathSegment = _FPDFClipPath_GetPathSegmentPtr.asFunction(); + +/// Create a new clip path, with a rectangle inserted. +/// +/// Caller takes ownership of the returned FPDF_CLIPPATH. It should be freed with +/// FPDF_DestroyClipPath(). +/// +/// left - The left of the clip box. +/// bottom - The bottom of the clip box. +/// right - The right of the clip box. +/// top - The top of the clip box. +FPDF_CLIPPATH FPDF_CreateClipPath(double left, +double bottom, +double right, +double top, +) { + return _FPDF_CreateClipPath(left, +bottom, +right, +top, +); +} + +late final _FPDF_CreateClipPathPtr = _lookup< + ffi.NativeFunction>('FPDF_CreateClipPath'); +late final _FPDF_CreateClipPath = _FPDF_CreateClipPathPtr.asFunction(); + +/// Destroy the clip path. +/// +/// clipPath - A handle to the clip path. It will be invalid after this call. +void FPDF_DestroyClipPath(FPDF_CLIPPATH clipPath, +) { + return _FPDF_DestroyClipPath(clipPath, +); +} + +late final _FPDF_DestroyClipPathPtr = _lookup< + ffi.NativeFunction>('FPDF_DestroyClipPath'); +late final _FPDF_DestroyClipPath = _FPDF_DestroyClipPathPtr.asFunction(); + +/// Clip the page content, the page content that outside the clipping region +/// become invisible. +/// +/// A clip path will be inserted before the page content stream or content array. +/// In this way, the page content will be clipped by this clip path. +/// +/// page - A page handle. +/// clipPath - A handle to the clip path. (Does not take ownership.) +void FPDFPage_InsertClipPath(FPDF_PAGE page, +FPDF_CLIPPATH clipPath, +) { + return _FPDFPage_InsertClipPath(page, +clipPath, +); +} + +late final _FPDFPage_InsertClipPathPtr = _lookup< + ffi.NativeFunction>('FPDFPage_InsertClipPath'); +late final _FPDFPage_InsertClipPath = _FPDFPage_InsertClipPathPtr.asFunction(); + +/// Flatten annotations and form fields into the page contents. +/// +/// page - handle to the page. +/// nFlag - One of the |FLAT_*| values denoting the page usage. +/// +/// Returns one of the |FLATTEN_*| values. +/// +/// Currently, all failures return |FLATTEN_FAIL| with no indication of the +/// cause. +int FPDFPage_Flatten(FPDF_PAGE page, +int nFlag, +) { + return _FPDFPage_Flatten(page, +nFlag, +); +} + +late final _FPDFPage_FlattenPtr = _lookup< + ffi.NativeFunction>('FPDFPage_Flatten'); +late final _FPDFPage_Flatten = _FPDFPage_FlattenPtr.asFunction(); + +/// Experimental API. +/// Gets the decoded data from the thumbnail of |page| if it exists. +/// This only modifies |buffer| if |buflen| less than or equal to the +/// size of the decoded data. Returns the size of the decoded +/// data or 0 if thumbnail DNE. Optional, pass null to just retrieve +/// the size of the buffer needed. +/// +/// page - handle to a page. +/// buffer - buffer for holding the decoded image data. +/// buflen - length of the buffer in bytes. +int FPDFPage_GetDecodedThumbnailData(FPDF_PAGE page, +ffi.Pointer buffer, +int buflen, +) { + return _FPDFPage_GetDecodedThumbnailData(page, +buffer, +buflen, +); +} + +late final _FPDFPage_GetDecodedThumbnailDataPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFPage_GetDecodedThumbnailData'); +late final _FPDFPage_GetDecodedThumbnailData = _FPDFPage_GetDecodedThumbnailDataPtr.asFunction , int )>(); + +/// Experimental API. +/// Gets the raw data from the thumbnail of |page| if it exists. +/// This only modifies |buffer| if |buflen| is less than or equal to +/// the size of the raw data. Returns the size of the raw data or 0 +/// if thumbnail DNE. Optional, pass null to just retrieve the size +/// of the buffer needed. +/// +/// page - handle to a page. +/// buffer - buffer for holding the raw image data. +/// buflen - length of the buffer in bytes. +int FPDFPage_GetRawThumbnailData(FPDF_PAGE page, +ffi.Pointer buffer, +int buflen, +) { + return _FPDFPage_GetRawThumbnailData(page, +buffer, +buflen, +); +} + +late final _FPDFPage_GetRawThumbnailDataPtr = _lookup< + ffi.NativeFunction , ffi.UnsignedLong )>>('FPDFPage_GetRawThumbnailData'); +late final _FPDFPage_GetRawThumbnailData = _FPDFPage_GetRawThumbnailDataPtr.asFunction , int )>(); + +/// Experimental API. +/// Returns the thumbnail of |page| as a FPDF_BITMAP. Returns a nullptr +/// if unable to access the thumbnail's stream. +/// +/// page - handle to a page. +FPDF_BITMAP FPDFPage_GetThumbnailAsBitmap(FPDF_PAGE page, +) { + return _FPDFPage_GetThumbnailAsBitmap(page, +); +} + +late final _FPDFPage_GetThumbnailAsBitmapPtr = _lookup< + ffi.NativeFunction>('FPDFPage_GetThumbnailAsBitmap'); +late final _FPDFPage_GetThumbnailAsBitmap = _FPDFPage_GetThumbnailAsBitmapPtr.asFunction(); + +} + +/// PDF text rendering modes +enum FPDF_TEXT_RENDERMODE { + FPDF_TEXTRENDERMODE_UNKNOWN(-1), + FPDF_TEXTRENDERMODE_FILL(0), + FPDF_TEXTRENDERMODE_STROKE(1), + FPDF_TEXTRENDERMODE_FILL_STROKE(2), + FPDF_TEXTRENDERMODE_INVISIBLE(3), + FPDF_TEXTRENDERMODE_FILL_CLIP(4), + FPDF_TEXTRENDERMODE_STROKE_CLIP(5), + FPDF_TEXTRENDERMODE_FILL_STROKE_CLIP(6), + FPDF_TEXTRENDERMODE_CLIP(7); + + static const FPDF_TEXTRENDERMODE_LAST = FPDF_TEXTRENDERMODE_CLIP; + + final int value; + const FPDF_TEXT_RENDERMODE(this.value); + + static FPDF_TEXT_RENDERMODE fromValue(int value) => switch (value) { + -1 => FPDF_TEXTRENDERMODE_UNKNOWN, + 0 => FPDF_TEXTRENDERMODE_FILL, + 1 => FPDF_TEXTRENDERMODE_STROKE, + 2 => FPDF_TEXTRENDERMODE_FILL_STROKE, + 3 => FPDF_TEXTRENDERMODE_INVISIBLE, + 4 => FPDF_TEXTRENDERMODE_FILL_CLIP, + 5 => FPDF_TEXTRENDERMODE_STROKE_CLIP, + 6 => FPDF_TEXTRENDERMODE_FILL_STROKE_CLIP, + 7 => FPDF_TEXTRENDERMODE_CLIP, + _ => throw ArgumentError('Unknown value for FPDF_TEXT_RENDERMODE: $value'), + }; + + @override + String toString() { + if (this == FPDF_TEXTRENDERMODE_CLIP) return "FPDF_TEXT_RENDERMODE.FPDF_TEXTRENDERMODE_CLIP, FPDF_TEXT_RENDERMODE.FPDF_TEXTRENDERMODE_LAST"; + return super.toString(); + }} + +final class fpdf_action_t__ extends ffi.Opaque{ +} + +/// PDF types - use incomplete types (never completed) to force API type safety. +typedef FPDF_ACTION = ffi.Pointer; +final class fpdf_annotation_t__ extends ffi.Opaque{ +} + +typedef FPDF_ANNOTATION = ffi.Pointer; +final class fpdf_attachment_t__ extends ffi.Opaque{ +} + +typedef FPDF_ATTACHMENT = ffi.Pointer; +final class fpdf_avail_t__ extends ffi.Opaque{ +} + +typedef FPDF_AVAIL = ffi.Pointer; +final class fpdf_bitmap_t__ extends ffi.Opaque{ +} + +typedef FPDF_BITMAP = ffi.Pointer; +final class fpdf_bookmark_t__ extends ffi.Opaque{ +} + +typedef FPDF_BOOKMARK = ffi.Pointer; +final class fpdf_clippath_t__ extends ffi.Opaque{ +} + +typedef FPDF_CLIPPATH = ffi.Pointer; +final class fpdf_dest_t__ extends ffi.Opaque{ +} + +typedef FPDF_DEST = ffi.Pointer; +final class fpdf_document_t__ extends ffi.Opaque{ +} + +typedef FPDF_DOCUMENT = ffi.Pointer; +final class fpdf_font_t__ extends ffi.Opaque{ +} + +typedef FPDF_FONT = ffi.Pointer; +final class fpdf_form_handle_t__ extends ffi.Opaque{ +} + +typedef FPDF_FORMHANDLE = ffi.Pointer; +final class fpdf_glyphpath_t__ extends ffi.Opaque{ +} + +typedef FPDF_GLYPHPATH = ffi.Pointer; +final class fpdf_javascript_action_t extends ffi.Opaque{ +} + +typedef FPDF_JAVASCRIPT_ACTION = ffi.Pointer; +final class fpdf_link_t__ extends ffi.Opaque{ +} + +typedef FPDF_LINK = ffi.Pointer; +final class fpdf_page_t__ extends ffi.Opaque{ +} + +typedef FPDF_PAGE = ffi.Pointer; +final class fpdf_pagelink_t__ extends ffi.Opaque{ +} + +typedef FPDF_PAGELINK = ffi.Pointer; +final class fpdf_pageobject_t__ extends ffi.Opaque{ +} + +typedef FPDF_PAGEOBJECT = ffi.Pointer; +final class fpdf_pageobjectmark_t__ extends ffi.Opaque{ +} + +typedef FPDF_PAGEOBJECTMARK = ffi.Pointer; +final class fpdf_pagerange_t__ extends ffi.Opaque{ +} + +typedef FPDF_PAGERANGE = ffi.Pointer; +final class fpdf_pathsegment_t extends ffi.Opaque{ +} + +typedef FPDF_PATHSEGMENT = ffi.Pointer; +final class fpdf_schhandle_t__ extends ffi.Opaque{ +} + +typedef FPDF_SCHHANDLE = ffi.Pointer; +final class fpdf_signature_t__ extends ffi.Opaque{ +} + +typedef FPDF_SIGNATURE = ffi.Pointer; +typedef FPDF_SKIA_CANVAS = ffi.Pointer; +final class fpdf_structelement_t__ extends ffi.Opaque{ +} + +typedef FPDF_STRUCTELEMENT = ffi.Pointer; +final class fpdf_structelement_attr_t__ extends ffi.Opaque{ +} + +typedef FPDF_STRUCTELEMENT_ATTR = ffi.Pointer; +final class fpdf_structelement_attr_value_t__ extends ffi.Opaque{ +} + +typedef FPDF_STRUCTELEMENT_ATTR_VALUE = ffi.Pointer; +final class fpdf_structtree_t__ extends ffi.Opaque{ +} + +typedef FPDF_STRUCTTREE = ffi.Pointer; +final class fpdf_textpage_t__ extends ffi.Opaque{ +} + +typedef FPDF_TEXTPAGE = ffi.Pointer; +final class fpdf_widget_t__ extends ffi.Opaque{ +} + +typedef FPDF_WIDGET = ffi.Pointer; +final class fpdf_xobject_t__ extends ffi.Opaque{ +} + +typedef FPDF_XOBJECT = ffi.Pointer; +/// Basic data types +typedef FPDF_BOOL = ffi.Int; +typedef DartFPDF_BOOL = int; +typedef FPDF_RESULT = ffi.Int; +typedef DartFPDF_RESULT = int; +typedef FPDF_DWORD = ffi.UnsignedLong; +typedef DartFPDF_DWORD = int; +typedef FS_FLOAT = ffi.Float; +typedef DartFS_FLOAT = double; +/// Duplex types +enum _FPDF_DUPLEXTYPE_ { + DuplexUndefined(0), + Simplex(1), + DuplexFlipShortEdge(2), + DuplexFlipLongEdge(3); + + + final int value; + const _FPDF_DUPLEXTYPE_(this.value); + + static _FPDF_DUPLEXTYPE_ fromValue(int value) => switch (value) { + 0 => DuplexUndefined, + 1 => Simplex, + 2 => DuplexFlipShortEdge, + 3 => DuplexFlipLongEdge, + _ => throw ArgumentError('Unknown value for _FPDF_DUPLEXTYPE_: $value'), + }; + +} + +/// String types +typedef FPDF_WCHAR = ffi.UnsignedShort; +typedef DartFPDF_WCHAR = int; +/// Public PDFium API type for byte strings. +typedef FPDF_BYTESTRING = ffi.Pointer; +/// The public PDFium API always uses UTF-16LE encoded wide strings, each +/// character uses 2 bytes (except surrogation), with the low byte first. +typedef FPDF_WIDESTRING = ffi.Pointer; +/// Structure for persisting a string beyond the duration of a callback. +/// Note: although represented as a char*, string may be interpreted as +/// a UTF-16LE formated string. Used only by XFA callbacks. +final class FPDF_BSTR_ extends ffi.Struct{ + /// String buffer, manipulate only with FPDF_BStr_* methods. + external ffi.Pointer str; + + /// Length of the string, in bytes. + @ffi.Int() + external int len; + +} + +/// Structure for persisting a string beyond the duration of a callback. +/// Note: although represented as a char*, string may be interpreted as +/// a UTF-16LE formated string. Used only by XFA callbacks. +typedef FPDF_BSTR = FPDF_BSTR_; +/// For Windows programmers: In most cases it's OK to treat FPDF_WIDESTRING as a +/// Windows unicode string, however, special care needs to be taken if you +/// expect to process Unicode larger than 0xffff. +/// +/// For Linux/Unix programmers: most compiler/library environments use 4 bytes +/// for a Unicode character, and you have to convert between FPDF_WIDESTRING and +/// system wide string by yourself. +typedef FPDF_STRING = ffi.Pointer; +/// Matrix for transformation, in the form [a b c d e f], equivalent to: +/// | a b 0 | +/// | c d 0 | +/// | e f 1 | +/// +/// Translation is performed with [1 0 0 1 tx ty]. +/// Scaling is performed with [sx 0 0 sy 0 0]. +/// See PDF Reference 1.7, 4.2.2 Common Transformations for more. +final class _FS_MATRIX_ extends ffi.Struct{ + @ffi.Float() + external double a; + + @ffi.Float() + external double b; + + @ffi.Float() + external double c; + + @ffi.Float() + external double d; + + @ffi.Float() + external double e; + + @ffi.Float() + external double f; + +} + +/// Matrix for transformation, in the form [a b c d e f], equivalent to: +/// | a b 0 | +/// | c d 0 | +/// | e f 1 | +/// +/// Translation is performed with [1 0 0 1 tx ty]. +/// Scaling is performed with [sx 0 0 sy 0 0]. +/// See PDF Reference 1.7, 4.2.2 Common Transformations for more. +typedef FS_MATRIX = _FS_MATRIX_; +/// Rectangle area(float) in device or page coordinate system. +final class _FS_RECTF_ extends ffi.Struct{ + /// The x-coordinate of the left-top corner. + @ffi.Float() + external double left; + + /// The y-coordinate of the left-top corner. + @ffi.Float() + external double top; + + /// The x-coordinate of the right-bottom corner. + @ffi.Float() + external double right; + + /// The y-coordinate of the right-bottom corner. + @ffi.Float() + external double bottom; + +} + +/// Rectangle area(float) in device or page coordinate system. +typedef FS_LPRECTF = ffi.Pointer<_FS_RECTF_>; +typedef FS_RECTF = _FS_RECTF_; +/// Const Pointer to FS_RECTF structure. +typedef FS_LPCRECTF = ffi.Pointer; +/// Rectangle size. Coordinate system agnostic. +final class FS_SIZEF_ extends ffi.Struct{ + @ffi.Float() + external double width; + + @ffi.Float() + external double height; + +} + +/// Rectangle size. Coordinate system agnostic. +typedef FS_LPSIZEF = ffi.Pointer; +typedef FS_SIZEF = FS_SIZEF_; +/// Const Pointer to FS_SIZEF structure. +typedef FS_LPCSIZEF = ffi.Pointer; +/// 2D Point. Coordinate system agnostic. +final class FS_POINTF_ extends ffi.Struct{ + @ffi.Float() + external double x; + + @ffi.Float() + external double y; + +} + +/// 2D Point. Coordinate system agnostic. +typedef FS_LPPOINTF = ffi.Pointer; +typedef FS_POINTF = FS_POINTF_; +/// Const Pointer to FS_POINTF structure. +typedef FS_LPCPOINTF = ffi.Pointer; +final class _FS_QUADPOINTSF extends ffi.Struct{ + @FS_FLOAT() + external double x1; + + @FS_FLOAT() + external double y1; + + @FS_FLOAT() + external double x2; + + @FS_FLOAT() + external double y2; + + @FS_FLOAT() + external double x3; + + @FS_FLOAT() + external double y3; + + @FS_FLOAT() + external double x4; + + @FS_FLOAT() + external double y4; + +} + +typedef FS_QUADPOINTSF = _FS_QUADPOINTSF; +/// Annotation enums. +typedef FPDF_ANNOTATION_SUBTYPE = ffi.Int; +typedef DartFPDF_ANNOTATION_SUBTYPE = int; +typedef FPDF_ANNOT_APPEARANCEMODE = ffi.Int; +typedef DartFPDF_ANNOT_APPEARANCEMODE = int; +/// Dictionary value types. +typedef FPDF_OBJECT_TYPE = ffi.Int; +typedef DartFPDF_OBJECT_TYPE = int; +/// PDF renderer types - Experimental. +/// Selection of 2D graphics library to use for rendering to FPDF_BITMAPs. +enum FPDF_RENDERER_TYPE { + /// Anti-Grain Geometry - https://sourceforge.net/projects/agg/ + FPDF_RENDERERTYPE_AGG(0), + /// Skia - https://skia.org/ + FPDF_RENDERERTYPE_SKIA(1); + + + final int value; + const FPDF_RENDERER_TYPE(this.value); + + static FPDF_RENDERER_TYPE fromValue(int value) => switch (value) { + 0 => FPDF_RENDERERTYPE_AGG, + 1 => FPDF_RENDERERTYPE_SKIA, + _ => throw ArgumentError('Unknown value for FPDF_RENDERER_TYPE: $value'), + }; + +} + +/// Process-wide options for initializing the library. +final class FPDF_LIBRARY_CONFIG_ extends ffi.Struct{ + /// Version number of the interface. Currently must be 2. + /// Support for version 1 will be deprecated in the future. + @ffi.Int() + external int version; + + /// Array of paths to scan in place of the defaults when using built-in + /// FXGE font loading code. The array is terminated by a NULL pointer. + /// The Array may be NULL itself to use the default paths. May be ignored + /// entirely depending upon the platform. + external ffi.Pointer> m_pUserFontPaths; + + /// Pointer to the v8::Isolate to use, or NULL to force PDFium to create one. + external ffi.Pointer m_pIsolate; + + /// The embedder data slot to use in the v8::Isolate to store PDFium's + /// per-isolate data. The value needs to be in the range + /// [0, |v8::Internals::kNumIsolateDataLots|). Note that 0 is fine for most + /// embedders. + @ffi.UnsignedInt() + external int m_v8EmbedderSlot; + + /// Pointer to the V8::Platform to use. + external ffi.Pointer m_pPlatform; + + /// Explicit specification of core renderer to use. |m_RendererType| must be + /// a valid value for |FPDF_LIBRARY_CONFIG| versions of this level or higher, + /// or else the initialization will fail with an immediate crash. + /// Note that use of a specified |FPDF_RENDERER_TYPE| value for which the + /// corresponding render library is not included in the build will similarly + /// fail with an immediate crash. + @ffi.UnsignedInt() + external int m_RendererTypeAsInt; + +FPDF_RENDERER_TYPE get m_RendererType => FPDF_RENDERER_TYPE.fromValue(m_RendererTypeAsInt); + +} + +/// Process-wide options for initializing the library. +typedef FPDF_LIBRARY_CONFIG = FPDF_LIBRARY_CONFIG_; +/// Structure for custom file access. +final class FPDF_FILEACCESS extends ffi.Struct{ + /// File length, in bytes. + @ffi.UnsignedLong() + external int m_FileLen; + + /// A function pointer for getting a block of data from a specific position. + /// Position is specified by byte offset from the beginning of the file. + /// The pointer to the buffer is never NULL and the size is never 0. + /// The position and size will never go out of range of the file length. + /// It may be possible for PDFium to call this function multiple times for + /// the same position. + /// Return value: should be non-zero if successful, zero for error. + external ffi.Pointer param, ffi.UnsignedLong position, ffi.Pointer pBuf, ffi.UnsignedLong size)>> m_GetBlock; + + /// A custom pointer for all implementation specific data. This pointer will + /// be used as the first parameter to the m_GetBlock callback. + external ffi.Pointer m_Param; + +} + +/// Structure for file reading or writing (I/O). +/// +/// Note: This is a handler and should be implemented by callers, +/// and is only used from XFA. +final class FPDF_FILEHANDLER_ extends ffi.Struct{ + /// User-defined data. + /// Note: Callers can use this field to track controls. + external ffi.Pointer clientData; + + /// Callback function to release the current file stream object. + /// + /// Parameters: + /// clientData - Pointer to user-defined data. + /// Returns: + /// None. + external ffi.Pointer clientData)>> Release; + + /// Callback function to retrieve the current file stream size. + /// + /// Parameters: + /// clientData - Pointer to user-defined data. + /// Returns: + /// Size of file stream. + external ffi.Pointer clientData)>> GetSize; + + /// Callback function to read data from the current file stream. + /// + /// Parameters: + /// clientData - Pointer to user-defined data. + /// offset - Offset position starts from the beginning of file + /// stream. This parameter indicates reading position. + /// buffer - Memory buffer to store data which are read from + /// file stream. This parameter should not be NULL. + /// size - Size of data which should be read from file stream, + /// in bytes. The buffer indicated by |buffer| must be + /// large enough to store specified data. + /// Returns: + /// 0 for success, other value for failure. + external ffi.Pointer clientData, FPDF_DWORD offset, ffi.Pointer buffer, FPDF_DWORD size)>> ReadBlock; + + /// Callback function to write data into the current file stream. + /// + /// Parameters: + /// clientData - Pointer to user-defined data. + /// offset - Offset position starts from the beginning of file + /// stream. This parameter indicates writing position. + /// buffer - Memory buffer contains data which is written into + /// file stream. This parameter should not be NULL. + /// size - Size of data which should be written into file + /// stream, in bytes. + /// Returns: + /// 0 for success, other value for failure. + external ffi.Pointer clientData, FPDF_DWORD offset, ffi.Pointer buffer, FPDF_DWORD size)>> WriteBlock; + + /// Callback function to flush all internal accessing buffers. + /// + /// Parameters: + /// clientData - Pointer to user-defined data. + /// Returns: + /// 0 for success, other value for failure. + external ffi.Pointer clientData)>> Flush; + + /// Callback function to change file size. + /// + /// Description: + /// This function is called under writing mode usually. Implementer + /// can determine whether to realize it based on application requests. + /// Parameters: + /// clientData - Pointer to user-defined data. + /// size - New size of file stream, in bytes. + /// Returns: + /// 0 for success, other value for failure. + external ffi.Pointer clientData, FPDF_DWORD size)>> Truncate; + +} + +/// Structure for file reading or writing (I/O). +/// +/// Note: This is a handler and should be implemented by callers, +/// and is only used from XFA. +typedef FPDF_FILEHANDLER = FPDF_FILEHANDLER_; +/// Struct for color scheme. +/// Each should be a 32-bit value specifying the color, in 8888 ARGB format. +final class FPDF_COLORSCHEME_ extends ffi.Struct{ + @FPDF_DWORD() + external int path_fill_color; + + @FPDF_DWORD() + external int path_stroke_color; + + @FPDF_DWORD() + external int text_fill_color; + + @FPDF_DWORD() + external int text_stroke_color; + +} + +/// Struct for color scheme. +/// Each should be a 32-bit value specifying the color, in 8888 ARGB format. +typedef FPDF_COLORSCHEME = FPDF_COLORSCHEME_; +/// Interface: FPDF_SYSFONTINFO +/// Interface for getting system font information and font mapping +final class _FPDF_SYSFONTINFO extends ffi.Struct{ + /// Version number of the interface. Currently must be 1. + @ffi.Int() + external int version; + + /// Method: Release + /// Give implementation a chance to release any data after the + /// interface is no longer used. + /// Interface Version: + /// 1 + /// Implementation Required: + /// No + /// Parameters: + /// pThis - Pointer to the interface structure itself + /// Return Value: + /// None + /// Comments: + /// Called by PDFium during the final cleanup process. + external ffi.Pointer pThis)>> Release; + + /// Method: EnumFonts + /// Enumerate all fonts installed on the system + /// Interface Version: + /// 1 + /// Implementation Required: + /// No + /// Parameters: + /// pThis - Pointer to the interface structure itself + /// pMapper - An opaque pointer to internal font mapper, used + /// when calling FPDF_AddInstalledFont(). + /// Return Value: + /// None + /// Comments: + /// Implementations should call FPDF_AddInstalledFont() function for + /// each font found. Only TrueType/OpenType and Type1 fonts are + /// accepted by PDFium. + external ffi.Pointer pThis, ffi.Pointer pMapper)>> EnumFonts; + + /// Method: MapFont + /// Use the system font mapper to get a font handle from requested + /// parameters. + /// Interface Version: + /// 1 + /// Implementation Required: + /// Required if GetFont method is not implemented. + /// Parameters: + /// pThis - Pointer to the interface structure itself + /// weight - Weight of the requested font. 400 is normal and + /// 700 is bold. + /// bItalic - Italic option of the requested font, TRUE or + /// FALSE. + /// charset - Character set identifier for the requested font. + /// See above defined constants. + /// pitch_family - A combination of flags. See above defined + /// constants. + /// face - Typeface name. Currently use system local encoding + /// only. + /// bExact - Obsolete: this parameter is now ignored. + /// Return Value: + /// An opaque pointer for font handle, or NULL if system mapping is + /// not supported. + /// Comments: + /// If the system supports native font mapper (like Windows), + /// implementation can implement this method to get a font handle. + /// Otherwise, PDFium will do the mapping and then call GetFont + /// method. Only TrueType/OpenType and Type1 fonts are accepted + /// by PDFium. + external ffi.Pointer Function(ffi.Pointer<_FPDF_SYSFONTINFO> pThis, ffi.Int weight, FPDF_BOOL bItalic, ffi.Int charset, ffi.Int pitch_family, ffi.Pointer face, ffi.Pointer bExact)>> MapFont; + + /// Method: GetFont + /// Get a handle to a particular font by its internal ID + /// Interface Version: + /// 1 + /// Implementation Required: + /// Required if MapFont method is not implemented. + /// Return Value: + /// An opaque pointer for font handle. + /// Parameters: + /// pThis - Pointer to the interface structure itself + /// face - Typeface name in system local encoding. + /// Comments: + /// If the system mapping not supported, PDFium will do the font + /// mapping and use this method to get a font handle. + external ffi.Pointer Function(ffi.Pointer<_FPDF_SYSFONTINFO> pThis, ffi.Pointer face)>> GetFont; + + /// Method: GetFontData + /// Get font data from a font + /// Interface Version: + /// 1 + /// Implementation Required: + /// Yes + /// Parameters: + /// pThis - Pointer to the interface structure itself + /// hFont - Font handle returned by MapFont or GetFont method + /// table - TrueType/OpenType table identifier (refer to + /// TrueType specification), or 0 for the whole file. + /// buffer - The buffer receiving the font data. Can be NULL if + /// not provided. + /// buf_size - Buffer size, can be zero if not provided. + /// Return Value: + /// Number of bytes needed, if buffer not provided or not large + /// enough, or number of bytes written into buffer otherwise. + /// Comments: + /// Can read either the full font file, or a particular + /// TrueType/OpenType table. + external ffi.Pointer pThis, ffi.Pointer hFont, ffi.UnsignedInt table, ffi.Pointer buffer, ffi.UnsignedLong buf_size)>> GetFontData; + + /// Method: GetFaceName + /// Get face name from a font handle + /// Interface Version: + /// 1 + /// Implementation Required: + /// No + /// Parameters: + /// pThis - Pointer to the interface structure itself + /// hFont - Font handle returned by MapFont or GetFont method + /// buffer - The buffer receiving the face name. Can be NULL if + /// not provided + /// buf_size - Buffer size, can be zero if not provided + /// Return Value: + /// Number of bytes needed, if buffer not provided or not large + /// enough, or number of bytes written into buffer otherwise. + external ffi.Pointer pThis, ffi.Pointer hFont, ffi.Pointer buffer, ffi.UnsignedLong buf_size)>> GetFaceName; + + /// Method: GetFontCharset + /// Get character set information for a font handle + /// Interface Version: + /// 1 + /// Implementation Required: + /// No + /// Parameters: + /// pThis - Pointer to the interface structure itself + /// hFont - Font handle returned by MapFont or GetFont method + /// Return Value: + /// Character set identifier. See defined constants above. + external ffi.Pointer pThis, ffi.Pointer hFont)>> GetFontCharset; + + /// Method: DeleteFont + /// Delete a font handle + /// Interface Version: + /// 1 + /// Implementation Required: + /// Yes + /// Parameters: + /// pThis - Pointer to the interface structure itself + /// hFont - Font handle returned by MapFont or GetFont method + /// Return Value: + /// None + external ffi.Pointer pThis, ffi.Pointer hFont)>> DeleteFont; + +} + +/// Interface: FPDF_SYSFONTINFO +/// Interface for getting system font information and font mapping +typedef FPDF_SYSFONTINFO = _FPDF_SYSFONTINFO; +/// Struct: FPDF_CharsetFontMap +/// Provides the name of a font to use for a given charset value. +final class FPDF_CharsetFontMap_ extends ffi.Struct{ + /// Character Set Enum value, see FXFONT_*_CHARSET above. + @ffi.Int() + external int charset; + + /// Name of default font to use with that charset. + external ffi.Pointer fontname; + +} + +/// Struct: FPDF_CharsetFontMap +/// Provides the name of a font to use for a given charset value. +typedef FPDF_CharsetFontMap = FPDF_CharsetFontMap_; +/// IFPDF_RENDERINFO interface. +final class _IFSDK_PAUSE extends ffi.Struct{ + /// Version number of the interface. Currently must be 1. + @ffi.Int() + external int version; + + /// Method: NeedToPauseNow + /// Check if we need to pause a progressive process now. + /// Interface Version: + /// 1 + /// Implementation Required: + /// yes + /// Parameters: + /// pThis - Pointer to the interface structure itself + /// Return Value: + /// Non-zero for pause now, 0 for continue. + external ffi.Pointer pThis)>> NeedToPauseNow; + + /// A user defined data pointer, used by user's application. Can be NULL. + external ffi.Pointer user; + +} + +/// IFPDF_RENDERINFO interface. +typedef IFSDK_PAUSE = _IFSDK_PAUSE; +final class FPDF_IMAGEOBJ_METADATA extends ffi.Struct{ + /// The image width in pixels. + @ffi.UnsignedInt() + external int width; + + /// The image height in pixels. + @ffi.UnsignedInt() + external int height; + + /// The image's horizontal pixel-per-inch. + @ffi.Float() + external double horizontal_dpi; + + /// The image's vertical pixel-per-inch. + @ffi.Float() + external double vertical_dpi; + + /// The number of bits used to represent each pixel. + @ffi.UnsignedInt() + external int bits_per_pixel; + + /// The image's colorspace. See above for the list of FPDF_COLORSPACE_*. + @ffi.Int() + external int colorspace; + + /// The image's marked content ID. Useful for pairing with associated alt-text. + /// A value of -1 indicates no ID. + @ffi.Int() + external int marked_content_id; + +} + +final class _IPDF_JsPlatform extends ffi.Struct{ + /// Version number of the interface. Currently must be 2. + @ffi.Int() + external int version; + + /// Method: app_alert + /// Pop up a dialog to show warning or hint. + /// Interface Version: + /// 1 + /// Implementation Required: + /// yes + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// Msg - A string containing the message to be displayed. + /// Title - The title of the dialog. + /// Type - The type of button group, one of the + /// JSPLATFORM_ALERT_BUTTON_* values above. + /// nIcon - The type of the icon, one of the + /// JSPLATFORM_ALERT_ICON_* above. + /// Return Value: + /// Option selected by user in dialogue, one of the + /// JSPLATFORM_ALERT_RETURN_* values above. + external ffi.Pointer pThis, FPDF_WIDESTRING Msg, FPDF_WIDESTRING Title, ffi.Int Type, ffi.Int Icon)>> app_alert; + + /// Method: app_beep + /// Causes the system to play a sound. + /// Interface Version: + /// 1 + /// Implementation Required: + /// yes + /// Parameters: + /// pThis - Pointer to the interface structure itself + /// nType - The sound type, see JSPLATFORM_BEEP_TYPE_* + /// above. + /// Return Value: + /// None + external ffi.Pointer pThis, ffi.Int nType)>> app_beep; + + /// Method: app_response + /// Displays a dialog box containing a question and an entry field for + /// the user to reply to the question. + /// Interface Version: + /// 1 + /// Implementation Required: + /// yes + /// Parameters: + /// pThis - Pointer to the interface structure itself + /// Question - The question to be posed to the user. + /// Title - The title of the dialog box. + /// Default - A default value for the answer to the question. If + /// not specified, no default value is presented. + /// cLabel - A short string to appear in front of and on the + /// same line as the edit text field. + /// bPassword - If true, indicates that the user's response should + /// be shown as asterisks (*) or bullets (?) to mask + /// the response, which might be sensitive information. + /// response - A string buffer allocated by PDFium, to receive the + /// user's response. + /// length - The length of the buffer in bytes. Currently, it is + /// always 2048. + /// Return Value: + /// Number of bytes the complete user input would actually require, not + /// including trailing zeros, regardless of the value of the length + /// parameter or the presence of the response buffer. + /// Comments: + /// No matter on what platform, the response buffer should be always + /// written using UTF-16LE encoding. If a response buffer is + /// present and the size of the user input exceeds the capacity of the + /// buffer as specified by the length parameter, only the + /// first "length" bytes of the user input are to be written to the + /// buffer. + external ffi.Pointer pThis, FPDF_WIDESTRING Question, FPDF_WIDESTRING Title, FPDF_WIDESTRING Default, FPDF_WIDESTRING cLabel, FPDF_BOOL bPassword, ffi.Pointer response, ffi.Int length)>> app_response; + + /// Method: Doc_getFilePath + /// Get the file path of the current document. + /// Interface Version: + /// 1 + /// Implementation Required: + /// yes + /// Parameters: + /// pThis - Pointer to the interface structure itself + /// filePath - The string buffer to receive the file path. Can + /// be NULL. + /// length - The length of the buffer, number of bytes. Can + /// be 0. + /// Return Value: + /// Number of bytes the filePath consumes, including trailing zeros. + /// Comments: + /// The filePath should always be provided in the local encoding. + /// The return value always indicated number of bytes required for + /// the buffer, even when there is no buffer specified, or the buffer + /// size is less than required. In this case, the buffer will not + /// be modified. + external ffi.Pointer pThis, ffi.Pointer filePath, ffi.Int length)>> Doc_getFilePath; + + /// Method: Doc_mail + /// Mails the data buffer as an attachment to all recipients, with or + /// without user interaction. + /// Interface Version: + /// 1 + /// Implementation Required: + /// yes + /// Parameters: + /// pThis - Pointer to the interface structure itself + /// mailData - Pointer to the data buffer to be sent. Can be NULL. + /// length - The size,in bytes, of the buffer pointed by + /// mailData parameter. Can be 0. + /// bUI - If true, the rest of the parameters are used in a + /// compose-new-message window that is displayed to the + /// user. If false, the cTo parameter is required and + /// all others are optional. + /// To - A semicolon-delimited list of recipients for the + /// message. + /// Subject - The subject of the message. The length limit is + /// 64 KB. + /// CC - A semicolon-delimited list of CC recipients for + /// the message. + /// BCC - A semicolon-delimited list of BCC recipients for + /// the message. + /// Msg - The content of the message. The length limit is + /// 64 KB. + /// Return Value: + /// None. + /// Comments: + /// If the parameter mailData is NULL or length is 0, the current + /// document will be mailed as an attachment to all recipients. + external ffi.Pointer pThis, ffi.Pointer mailData, ffi.Int length, FPDF_BOOL bUI, FPDF_WIDESTRING To, FPDF_WIDESTRING Subject, FPDF_WIDESTRING CC, FPDF_WIDESTRING BCC, FPDF_WIDESTRING Msg)>> Doc_mail; + + /// Method: Doc_print + /// Prints all or a specific number of pages of the document. + /// Interface Version: + /// 1 + /// Implementation Required: + /// yes + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// bUI - If true, will cause a UI to be presented to the + /// user to obtain printing information and confirm + /// the action. + /// nStart - A 0-based index that defines the start of an + /// inclusive range of pages. + /// nEnd - A 0-based index that defines the end of an + /// inclusive page range. + /// bSilent - If true, suppresses the cancel dialog box while + /// the document is printing. The default is false. + /// bShrinkToFit - If true, the page is shrunk (if necessary) to + /// fit within the imageable area of the printed page. + /// bPrintAsImage - If true, print pages as an image. + /// bReverse - If true, print from nEnd to nStart. + /// bAnnotations - If true (the default), annotations are + /// printed. + /// Return Value: + /// None. + external ffi.Pointer pThis, FPDF_BOOL bUI, ffi.Int nStart, ffi.Int nEnd, FPDF_BOOL bSilent, FPDF_BOOL bShrinkToFit, FPDF_BOOL bPrintAsImage, FPDF_BOOL bReverse, FPDF_BOOL bAnnotations)>> Doc_print; + + /// Method: Doc_submitForm + /// Send the form data to a specified URL. + /// Interface Version: + /// 1 + /// Implementation Required: + /// yes + /// Parameters: + /// pThis - Pointer to the interface structure itself + /// formData - Pointer to the data buffer to be sent. + /// length - The size,in bytes, of the buffer pointed by + /// formData parameter. + /// URL - The URL to send to. + /// Return Value: + /// None. + external ffi.Pointer pThis, ffi.Pointer formData, ffi.Int length, FPDF_WIDESTRING URL)>> Doc_submitForm; + + /// Method: Doc_gotoPage + /// Jump to a specified page. + /// Interface Version: + /// 1 + /// Implementation Required: + /// yes + /// Parameters: + /// pThis - Pointer to the interface structure itself + /// nPageNum - The specified page number, zero for the first page. + /// Return Value: + /// None. + external ffi.Pointer pThis, ffi.Int nPageNum)>> Doc_gotoPage; + + /// Method: Field_browse + /// Show a file selection dialog, and return the selected file path. + /// Interface Version: + /// 1 + /// Implementation Required: + /// yes + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// filePath - Pointer to the data buffer to receive the file + /// path. Can be NULL. + /// length - The length of the buffer, in bytes. Can be 0. + /// Return Value: + /// Number of bytes the filePath consumes, including trailing zeros. + /// Comments: + /// The filePath should always be provided in local encoding. + external ffi.Pointer pThis, ffi.Pointer filePath, ffi.Int length)>> Field_browse; + + /// Pointer for embedder-specific data. Unused by PDFium, and despite + /// its name, can be any data the embedder desires, though traditionally + /// a FPDF_FORMFILLINFO interface. + external ffi.Pointer m_pFormfillinfo; + + /// Unused in v3, retain for compatibility. + external ffi.Pointer m_isolate; + + /// Unused in v3, retain for compatibility. + @ffi.UnsignedInt() + external int m_v8EmbedderSlot; + +} + +typedef IPDF_JSPLATFORM = _IPDF_JsPlatform; +typedef TimerCallbackFunction = ffi.Void Function(ffi.Int idEvent); +typedef DartTimerCallbackFunction = void Function(int idEvent); +/// Function signature for the callback function passed to the FFI_SetTimer +/// method. +/// Parameters: +/// idEvent - Identifier of the timer. +/// Return value: +/// None. +typedef TimerCallback = ffi.Pointer>; +/// Declares of a struct type to the local system time. +final class _FPDF_SYSTEMTIME extends ffi.Struct{ + /// years since 1900 + @ffi.UnsignedShort() + external int wYear; + + /// months since January - [0,11] + @ffi.UnsignedShort() + external int wMonth; + + /// days since Sunday - [0,6] + @ffi.UnsignedShort() + external int wDayOfWeek; + + /// day of the month - [1,31] + @ffi.UnsignedShort() + external int wDay; + + /// hours since midnight - [0,23] + @ffi.UnsignedShort() + external int wHour; + + /// minutes after the hour - [0,59] + @ffi.UnsignedShort() + external int wMinute; + + /// seconds after the minute - [0,59] + @ffi.UnsignedShort() + external int wSecond; + + /// milliseconds after the second - [0,999] + @ffi.UnsignedShort() + external int wMilliseconds; + +} + +/// Declares of a struct type to the local system time. +typedef FPDF_SYSTEMTIME = _FPDF_SYSTEMTIME; +final class _FPDF_FORMFILLINFO extends ffi.Struct{ + /// Version number of the interface. + /// Version 1 contains stable interfaces. Version 2 has additional + /// experimental interfaces. + /// When PDFium is built without the XFA module, version can be 1 or 2. + /// With version 1, only stable interfaces are called. With version 2, + /// additional experimental interfaces are also called. + /// When PDFium is built with the XFA module, version must be 2. + /// All the XFA related interfaces are experimental. If PDFium is built with + /// the XFA module and version 1 then none of the XFA related interfaces + /// would be called. When PDFium is built with XFA module then the version + /// must be 2. + @ffi.Int() + external int version; + + /// Method: Release + /// Give the implementation a chance to release any resources after the + /// interface is no longer used. + /// Interface Version: + /// 1 + /// Implementation Required: + /// No + /// Comments: + /// Called by PDFium during the final cleanup process. + /// Parameters: + /// pThis - Pointer to the interface structure itself + /// Return Value: + /// None + external ffi.Pointer pThis)>> Release; + + /// Method: FFI_Invalidate + /// Invalidate the client area within the specified rectangle. + /// Interface Version: + /// 1 + /// Implementation Required: + /// yes + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// page - Handle to the page. Returned by FPDF_LoadPage(). + /// left - Left position of the client area in PDF page + /// coordinates. + /// top - Top position of the client area in PDF page + /// coordinates. + /// right - Right position of the client area in PDF page + /// coordinates. + /// bottom - Bottom position of the client area in PDF page + /// coordinates. + /// Return Value: + /// None. + /// Comments: + /// All positions are measured in PDF "user space". + /// Implementation should call FPDF_RenderPageBitmap() for repainting + /// the specified page area. + external ffi.Pointer pThis, FPDF_PAGE page, ffi.Double left, ffi.Double top, ffi.Double right, ffi.Double bottom)>> FFI_Invalidate; + + /// Method: FFI_OutputSelectedRect + /// When the user selects text in form fields with the mouse, this + /// callback function will be invoked with the selected areas. + /// Interface Version: + /// 1 + /// Implementation Required: + /// No + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// page - Handle to the page. Returned by FPDF_LoadPage()/ + /// left - Left position of the client area in PDF page + /// coordinates. + /// top - Top position of the client area in PDF page + /// coordinates. + /// right - Right position of the client area in PDF page + /// coordinates. + /// bottom - Bottom position of the client area in PDF page + /// coordinates. + /// Return Value: + /// None. + /// Comments: + /// This callback function is useful for implementing special text + /// selection effects. An implementation should first record the + /// returned rectangles, then draw them one by one during the next + /// painting period. Lastly, it should remove all the recorded + /// rectangles when finished painting. + external ffi.Pointer pThis, FPDF_PAGE page, ffi.Double left, ffi.Double top, ffi.Double right, ffi.Double bottom)>> FFI_OutputSelectedRect; + + /// Method: FFI_SetCursor + /// Set the Cursor shape. + /// Interface Version: + /// 1 + /// Implementation Required: + /// yes + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// nCursorType - Cursor type, see Flags for Cursor type for details. + /// Return value: + /// None. + external ffi.Pointer pThis, ffi.Int nCursorType)>> FFI_SetCursor; + + /// Method: FFI_SetTimer + /// This method installs a system timer. An interval value is specified, + /// and every time that interval elapses, the system must call into the + /// callback function with the timer ID as returned by this function. + /// Interface Version: + /// 1 + /// Implementation Required: + /// yes + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// uElapse - Specifies the time-out value, in milliseconds. + /// lpTimerFunc - A pointer to the callback function-TimerCallback. + /// Return value: + /// The timer identifier of the new timer if the function is successful. + /// An application passes this value to the FFI_KillTimer method to kill + /// the timer. Nonzero if it is successful; otherwise, it is zero. + external ffi.Pointer pThis, ffi.Int uElapse, TimerCallback lpTimerFunc)>> FFI_SetTimer; + + /// Method: FFI_KillTimer + /// This method uninstalls a system timer, as set by an earlier call to + /// FFI_SetTimer. + /// Interface Version: + /// 1 + /// Implementation Required: + /// yes + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// nTimerID - The timer ID returned by FFI_SetTimer function. + /// Return value: + /// None. + external ffi.Pointer pThis, ffi.Int nTimerID)>> FFI_KillTimer; + + /// Method: FFI_GetLocalTime + /// This method receives the current local time on the system. + /// Interface Version: + /// 1 + /// Implementation Required: + /// yes + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// Return value: + /// The local time. See FPDF_SYSTEMTIME above for details. + /// Note: Unused. + external ffi.Pointer pThis)>> FFI_GetLocalTime; + + /// Method: FFI_OnChange + /// This method will be invoked to notify the implementation when the + /// value of any FormField on the document had been changed. + /// Interface Version: + /// 1 + /// Implementation Required: + /// no + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// Return value: + /// None. + external ffi.Pointer pThis)>> FFI_OnChange; + + /// Method: FFI_GetPage + /// This method receives the page handle associated with a specified + /// page index. + /// Interface Version: + /// 1 + /// Implementation Required: + /// yes + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// document - Handle to document. Returned by FPDF_LoadDocument(). + /// nPageIndex - Index number of the page. 0 for the first page. + /// Return value: + /// Handle to the page, as previously returned to the implementation by + /// FPDF_LoadPage(). + /// Comments: + /// The implementation is expected to keep track of the page handles it + /// receives from PDFium, and their mappings to page numbers. In some + /// cases, the document-level JavaScript action may refer to a page + /// which hadn't been loaded yet. To successfully run the Javascript + /// action, the implementation needs to load the page. + external ffi.Pointer pThis, FPDF_DOCUMENT document, ffi.Int nPageIndex)>> FFI_GetPage; + + /// Method: FFI_GetCurrentPage + /// This method receives the handle to the current page. + /// Interface Version: + /// 1 + /// Implementation Required: + /// Yes when V8 support is present, otherwise unused. + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// document - Handle to document. Returned by FPDF_LoadDocument(). + /// Return value: + /// Handle to the page. Returned by FPDF_LoadPage(). + /// Comments: + /// PDFium doesn't keep keep track of the "current page" (e.g. the one + /// that is most visible on screen), so it must ask the embedder for + /// this information. + external ffi.Pointer pThis, FPDF_DOCUMENT document)>> FFI_GetCurrentPage; + + /// Method: FFI_GetRotation + /// This method receives currently rotation of the page view. + /// Interface Version: + /// 1 + /// Implementation Required: + /// yes + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// page - Handle to page, as returned by FPDF_LoadPage(). + /// Return value: + /// A number to indicate the page rotation in 90 degree increments + /// in a clockwise direction: + /// 0 - 0 degrees + /// 1 - 90 degrees + /// 2 - 180 degrees + /// 3 - 270 degrees + /// Note: Unused. + external ffi.Pointer pThis, FPDF_PAGE page)>> FFI_GetRotation; + + /// Method: FFI_ExecuteNamedAction + /// This method will execute a named action. + /// Interface Version: + /// 1 + /// Implementation Required: + /// yes + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// namedAction - A byte string which indicates the named action, + /// terminated by 0. + /// Return value: + /// None. + /// Comments: + /// See ISO 32000-1:2008, section 12.6.4.11 for descriptions of the + /// standard named actions, but note that a document may supply any + /// name of its choosing. + external ffi.Pointer pThis, FPDF_BYTESTRING namedAction)>> FFI_ExecuteNamedAction; + + /// Method: FFI_SetTextFieldFocus + /// Called when a text field is getting or losing focus. + /// Interface Version: + /// 1 + /// Implementation Required: + /// no + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// value - The string value of the form field, in UTF-16LE + /// format. + /// valueLen - The length of the string value. This is the + /// number of characters, not bytes. + /// is_focus - True if the form field is getting focus, false + /// if the form field is losing focus. + /// Return value: + /// None. + /// Comments: + /// Only supports text fields and combobox fields. + external ffi.Pointer pThis, FPDF_WIDESTRING value, FPDF_DWORD valueLen, FPDF_BOOL is_focus)>> FFI_SetTextFieldFocus; + + /// Method: FFI_DoURIAction + /// Ask the implementation to navigate to a uniform resource identifier. + /// Interface Version: + /// 1 + /// Implementation Required: + /// No + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// bsURI - A byte string which indicates the uniform + /// resource identifier, terminated by 0. + /// Return value: + /// None. + /// Comments: + /// If the embedder is version 2 or higher and have implementation for + /// FFI_DoURIActionWithKeyboardModifier, then + /// FFI_DoURIActionWithKeyboardModifier takes precedence over + /// FFI_DoURIAction. + /// See the URI actions description of <> + /// for more details. + external ffi.Pointer pThis, FPDF_BYTESTRING bsURI)>> FFI_DoURIAction; + + /// Method: FFI_DoGoToAction + /// This action changes the view to a specified destination. + /// Interface Version: + /// 1 + /// Implementation Required: + /// No + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// nPageIndex - The index of the PDF page. + /// zoomMode - The zoom mode for viewing page. See below. + /// fPosArray - The float array which carries the position info. + /// sizeofArray - The size of float array. + /// PDFZoom values: + /// - XYZ = 1 + /// - FITPAGE = 2 + /// - FITHORZ = 3 + /// - FITVERT = 4 + /// - FITRECT = 5 + /// - FITBBOX = 6 + /// - FITBHORZ = 7 + /// - FITBVERT = 8 + /// Return value: + /// None. + /// Comments: + /// See the Destinations description of <> + /// in 8.2.1 for more details. + external ffi.Pointer pThis, ffi.Int nPageIndex, ffi.Int zoomMode, ffi.Pointer fPosArray, ffi.Int sizeofArray)>> FFI_DoGoToAction; + + /// Pointer to IPDF_JSPLATFORM interface. + /// Unused if PDFium is built without V8 support. Otherwise, if NULL, then + /// JavaScript will be prevented from executing while rendering the document. + external ffi.Pointer m_pJsPlatform; + + /// Whether the XFA module is disabled when built with the XFA module. + /// Interface Version: + /// Ignored if |version| < 2. + @FPDF_BOOL() + external int xfa_disabled; + + /// Method: FFI_DisplayCaret + /// This method will show the caret at specified position. + /// Interface Version: + /// Ignored if |version| < 2. + /// Implementation Required: + /// Required for XFA, otherwise set to NULL. + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// page - Handle to page. Returned by FPDF_LoadPage(). + /// left - Left position of the client area in PDF page + /// coordinates. + /// top - Top position of the client area in PDF page + /// coordinates. + /// right - Right position of the client area in PDF page + /// coordinates. + /// bottom - Bottom position of the client area in PDF page + /// coordinates. + /// Return value: + /// None. + external ffi.Pointer pThis, FPDF_PAGE page, FPDF_BOOL bVisible, ffi.Double left, ffi.Double top, ffi.Double right, ffi.Double bottom)>> FFI_DisplayCaret; + + /// Method: FFI_GetCurrentPageIndex + /// This method will get the current page index. + /// Interface Version: + /// Ignored if |version| < 2. + /// Implementation Required: + /// Required for XFA, otherwise set to NULL. + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// document - Handle to document from FPDF_LoadDocument(). + /// Return value: + /// The index of current page. + external ffi.Pointer pThis, FPDF_DOCUMENT document)>> FFI_GetCurrentPageIndex; + + /// Method: FFI_SetCurrentPage + /// This method will set the current page. + /// Interface Version: + /// Ignored if |version| < 2. + /// Implementation Required: + /// Required for XFA, otherwise set to NULL. + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// document - Handle to document from FPDF_LoadDocument(). + /// iCurPage - The index of the PDF page. + /// Return value: + /// None. + external ffi.Pointer pThis, FPDF_DOCUMENT document, ffi.Int iCurPage)>> FFI_SetCurrentPage; + + /// Method: FFI_GotoURL + /// This method will navigate to the specified URL. + /// Interface Version: + /// Ignored if |version| < 2. + /// Implementation Required: + /// Required for XFA, otherwise set to NULL. + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// document - Handle to document from FPDF_LoadDocument(). + /// wsURL - The string value of the URL, in UTF-16LE format. + /// Return value: + /// None. + external ffi.Pointer pThis, FPDF_DOCUMENT document, FPDF_WIDESTRING wsURL)>> FFI_GotoURL; + + /// Method: FFI_GetPageViewRect + /// This method will get the current page view rectangle. + /// Interface Version: + /// Ignored if |version| < 2. + /// Implementation Required: + /// Required for XFA, otherwise set to NULL. + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// page - Handle to page. Returned by FPDF_LoadPage(). + /// left - The pointer to receive left position of the page + /// view area in PDF page coordinates. + /// top - The pointer to receive top position of the page + /// view area in PDF page coordinates. + /// right - The pointer to receive right position of the + /// page view area in PDF page coordinates. + /// bottom - The pointer to receive bottom position of the + /// page view area in PDF page coordinates. + /// Return value: + /// None. + external ffi.Pointer pThis, FPDF_PAGE page, ffi.Pointer left, ffi.Pointer top, ffi.Pointer right, ffi.Pointer bottom)>> FFI_GetPageViewRect; + + /// Method: FFI_PageEvent + /// This method fires when pages have been added to or deleted from + /// the XFA document. + /// Interface Version: + /// Ignored if |version| < 2. + /// Implementation Required: + /// Required for XFA, otherwise set to NULL. + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// page_count - The number of pages to be added or deleted. + /// event_type - See FXFA_PAGEVIEWEVENT_* above. + /// Return value: + /// None. + /// Comments: + /// The pages to be added or deleted always start from the last page + /// of document. This means that if parameter page_count is 2 and + /// event type is FXFA_PAGEVIEWEVENT_POSTADDED, 2 new pages have been + /// appended to the tail of document; If page_count is 2 and + /// event type is FXFA_PAGEVIEWEVENT_POSTREMOVED, the last 2 pages + /// have been deleted. + external ffi.Pointer pThis, ffi.Int page_count, FPDF_DWORD event_type)>> FFI_PageEvent; + + /// Method: FFI_PopupMenu + /// This method will track the right context menu for XFA fields. + /// Interface Version: + /// Ignored if |version| < 2. + /// Implementation Required: + /// Required for XFA, otherwise set to NULL. + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// page - Handle to page. Returned by FPDF_LoadPage(). + /// hWidget - Always null, exists for compatibility. + /// menuFlag - The menu flags. Please refer to macro definition + /// of FXFA_MENU_XXX and this can be one or a + /// combination of these macros. + /// x - X position of the client area in PDF page + /// coordinates. + /// y - Y position of the client area in PDF page + /// coordinates. + /// Return value: + /// TRUE indicates success; otherwise false. + external ffi.Pointer pThis, FPDF_PAGE page, FPDF_WIDGET hWidget, ffi.Int menuFlag, ffi.Float x, ffi.Float y)>> FFI_PopupMenu; + + /// Method: FFI_OpenFile + /// This method will open the specified file with the specified mode. + /// Interface Version: + /// Ignored if |version| < 2. + /// Implementation Required: + /// Required for XFA, otherwise set to NULL. + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// fileFlag - The file flag. Please refer to macro definition + /// of FXFA_SAVEAS_XXX and use one of these macros. + /// wsURL - The string value of the file URL, in UTF-16LE + /// format. + /// mode - The mode for open file, e.g. "rb" or "wb". + /// Return value: + /// The handle to FPDF_FILEHANDLER. + external ffi.Pointer Function(ffi.Pointer<_FPDF_FORMFILLINFO> pThis, ffi.Int fileFlag, FPDF_WIDESTRING wsURL, ffi.Pointer mode)>> FFI_OpenFile; + + /// Method: FFI_EmailTo + /// This method will email the specified file stream to the specified + /// contact. + /// Interface Version: + /// Ignored if |version| < 2. + /// Implementation Required: + /// Required for XFA, otherwise set to NULL. + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// pFileHandler - Handle to the FPDF_FILEHANDLER. + /// pTo - A semicolon-delimited list of recipients for the + /// message,in UTF-16LE format. + /// pSubject - The subject of the message,in UTF-16LE format. + /// pCC - A semicolon-delimited list of CC recipients for + /// the message,in UTF-16LE format. + /// pBcc - A semicolon-delimited list of BCC recipients for + /// the message,in UTF-16LE format. + /// pMsg - Pointer to the data buffer to be sent.Can be + /// NULL,in UTF-16LE format. + /// Return value: + /// None. + external ffi.Pointer pThis, ffi.Pointer fileHandler, FPDF_WIDESTRING pTo, FPDF_WIDESTRING pSubject, FPDF_WIDESTRING pCC, FPDF_WIDESTRING pBcc, FPDF_WIDESTRING pMsg)>> FFI_EmailTo; + + /// Method: FFI_UploadTo + /// This method will upload the specified file stream to the + /// specified URL. + /// Interface Version: + /// Ignored if |version| < 2. + /// Implementation Required: + /// Required for XFA, otherwise set to NULL. + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// pFileHandler - Handle to the FPDF_FILEHANDLER. + /// fileFlag - The file flag. Please refer to macro definition + /// of FXFA_SAVEAS_XXX and use one of these macros. + /// uploadTo - Pointer to the URL path, in UTF-16LE format. + /// Return value: + /// None. + external ffi.Pointer pThis, ffi.Pointer fileHandler, ffi.Int fileFlag, FPDF_WIDESTRING uploadTo)>> FFI_UploadTo; + + /// Method: FFI_GetPlatform + /// This method will get the current platform. + /// Interface Version: + /// Ignored if |version| < 2. + /// Implementation Required: + /// Required for XFA, otherwise set to NULL. + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// platform - Pointer to the data buffer to receive the + /// platform,in UTF-16LE format. Can be NULL. + /// length - The length of the buffer in bytes. Can be + /// 0 to query the required size. + /// Return value: + /// The length of the buffer, number of bytes. + external ffi.Pointer pThis, ffi.Pointer platform, ffi.Int length)>> FFI_GetPlatform; + + /// Method: FFI_GetLanguage + /// This method will get the current language. + /// Interface Version: + /// Ignored if |version| < 2. + /// Implementation Required: + /// Required for XFA, otherwise set to NULL. + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// language - Pointer to the data buffer to receive the + /// current language. Can be NULL. + /// length - The length of the buffer in bytes. Can be + /// 0 to query the required size. + /// Return value: + /// The length of the buffer, number of bytes. + external ffi.Pointer pThis, ffi.Pointer language, ffi.Int length)>> FFI_GetLanguage; + + /// Method: FFI_DownloadFromURL + /// This method will download the specified file from the URL. + /// Interface Version: + /// Ignored if |version| < 2. + /// Implementation Required: + /// Required for XFA, otherwise set to NULL. + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// URL - The string value of the file URL, in UTF-16LE + /// format. + /// Return value: + /// The handle to FPDF_FILEHANDLER. + external ffi.Pointer Function(ffi.Pointer<_FPDF_FORMFILLINFO> pThis, FPDF_WIDESTRING URL)>> FFI_DownloadFromURL; + + /// Method: FFI_PostRequestURL + /// This method will post the request to the server URL. + /// Interface Version: + /// Ignored if |version| < 2. + /// Implementation Required: + /// Required for XFA, otherwise set to NULL. + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// wsURL - The string value of the server URL, in UTF-16LE + /// format. + /// wsData - The post data,in UTF-16LE format. + /// wsContentType - The content type of the request data, in + /// UTF-16LE format. + /// wsEncode - The encode type, in UTF-16LE format. + /// wsHeader - The request header,in UTF-16LE format. + /// response - Pointer to the FPDF_BSTR to receive the response + /// data from the server, in UTF-16LE format. + /// Return value: + /// TRUE indicates success, otherwise FALSE. + external ffi.Pointer pThis, FPDF_WIDESTRING wsURL, FPDF_WIDESTRING wsData, FPDF_WIDESTRING wsContentType, FPDF_WIDESTRING wsEncode, FPDF_WIDESTRING wsHeader, ffi.Pointer response)>> FFI_PostRequestURL; + + /// Method: FFI_PutRequestURL + /// This method will put the request to the server URL. + /// Interface Version: + /// Ignored if |version| < 2. + /// Implementation Required: + /// Required for XFA, otherwise set to NULL. + /// Parameters: + /// pThis - Pointer to the interface structure itself. + /// wsURL - The string value of the server URL, in UTF-16LE + /// format. + /// wsData - The put data, in UTF-16LE format. + /// wsEncode - The encode type, in UTR-16LE format. + /// Return value: + /// TRUE indicates success, otherwise FALSE. + external ffi.Pointer pThis, FPDF_WIDESTRING wsURL, FPDF_WIDESTRING wsData, FPDF_WIDESTRING wsEncode)>> FFI_PutRequestURL; + + /// Method: FFI_OnFocusChange + /// Called when the focused annotation is updated. + /// Interface Version: + /// Ignored if |version| < 2. + /// Implementation Required: + /// No + /// Parameters: + /// param - Pointer to the interface structure itself. + /// annot - The focused annotation. + /// page_index - Index number of the page which contains the + /// focused annotation. 0 for the first page. + /// Return value: + /// None. + /// Comments: + /// This callback function is useful for implementing any view based + /// action such as scrolling the annotation rect into view. The + /// embedder should not copy and store the annot as its scope is + /// limited to this call only. + external ffi.Pointer param, FPDF_ANNOTATION annot, ffi.Int page_index)>> FFI_OnFocusChange; + + /// Method: FFI_DoURIActionWithKeyboardModifier + /// Ask the implementation to navigate to a uniform resource identifier + /// with the specified modifiers. + /// Interface Version: + /// Ignored if |version| < 2. + /// Implementation Required: + /// No + /// Parameters: + /// param - Pointer to the interface structure itself. + /// uri - A byte string which indicates the uniform + /// resource identifier, terminated by 0. + /// modifiers - Keyboard modifier that indicates which of + /// the virtual keys are down, if any. + /// Return value: + /// None. + /// Comments: + /// If the embedder who is version 2 and does not implement this API, + /// then a call will be redirected to FFI_DoURIAction. + /// See the URI actions description of <> + /// for more details. + external ffi.Pointer param, FPDF_BYTESTRING uri, ffi.Int modifiers)>> FFI_DoURIActionWithKeyboardModifier; + +} + +typedef FPDF_FORMFILLINFO = _FPDF_FORMFILLINFO; +enum FPDFANNOT_COLORTYPE { + FPDFANNOT_COLORTYPE_Color(0), + FPDFANNOT_COLORTYPE_InteriorColor(1); + + + final int value; + const FPDFANNOT_COLORTYPE(this.value); + + static FPDFANNOT_COLORTYPE fromValue(int value) => switch (value) { + 0 => FPDFANNOT_COLORTYPE_Color, + 1 => FPDFANNOT_COLORTYPE_InteriorColor, + _ => throw ArgumentError('Unknown value for FPDFANNOT_COLORTYPE: $value'), + }; + +} + +/// Structure for custom file write +final class FPDF_FILEWRITE_ extends ffi.Struct{ + /// Version number of the interface. Currently must be 1. + @ffi.Int() + external int version; + + /// Method: WriteBlock + /// Output a block of data in your custom way. + /// Interface Version: + /// 1 + /// Implementation Required: + /// Yes + /// Comments: + /// Called by function FPDF_SaveDocument + /// Parameters: + /// pThis - Pointer to the structure itself + /// pData - Pointer to a buffer to output + /// size - The size of the buffer. + /// Return value: + /// Should be non-zero if successful, zero for error. + external ffi.Pointer pThis, ffi.Pointer pData, ffi.UnsignedLong size)>> WriteBlock; + +} + +/// Structure for custom file write +typedef FPDF_FILEWRITE = FPDF_FILEWRITE_; +/// The file identifier entry type. See section 14.4 "File Identifiers" of the +/// ISO 32000-1:2008 spec. +enum FPDF_FILEIDTYPE { + FILEIDTYPE_PERMANENT(0), + FILEIDTYPE_CHANGING(1); + + + final int value; + const FPDF_FILEIDTYPE(this.value); + + static FPDF_FILEIDTYPE fromValue(int value) => switch (value) { + 0 => FILEIDTYPE_PERMANENT, + 1 => FILEIDTYPE_CHANGING, + _ => throw ArgumentError('Unknown value for FPDF_FILEIDTYPE: $value'), + }; + +} + +/// Interface for checking whether sections of the file are available. +final class _FX_FILEAVAIL extends ffi.Struct{ + /// Version number of the interface. Must be 1. + @ffi.Int() + external int version; + + /// Reports if the specified data section is currently available. A section is + /// available if all bytes in the section are available. + /// + /// Interface Version: 1 + /// Implementation Required: Yes + /// + /// pThis - pointer to the interface structure. + /// offset - the offset of the data section in the file. + /// size - the size of the data section. + /// + /// Returns true if the specified data section at |offset| of |size| + /// is available. + external ffi.Pointer pThis, ffi.Size offset, ffi.Size size)>> IsDataAvail; + +} + +/// Interface for checking whether sections of the file are available. +typedef FX_FILEAVAIL = _FX_FILEAVAIL; +/// Download hints interface. Used to receive hints for further downloading. +final class _FX_DOWNLOADHINTS extends ffi.Struct{ + /// Version number of the interface. Must be 1. + @ffi.Int() + external int version; + + /// Add a section to be downloaded. + /// + /// Interface Version: 1 + /// Implementation Required: Yes + /// + /// pThis - pointer to the interface structure. + /// offset - the offset of the hint reported to be downloaded. + /// size - the size of the hint reported to be downloaded. + /// + /// The |offset| and |size| of the section may not be unique. Part of the + /// section might be already available. The download manager must deal with + /// overlapping sections. + external ffi.Pointer pThis, ffi.Size offset, ffi.Size size)>> AddSegment; + +} + +/// Download hints interface. Used to receive hints for further downloading. +typedef FX_DOWNLOADHINTS = _FX_DOWNLOADHINTS; +/// Key flags. +enum FWL_EVENTFLAG { + FWL_EVENTFLAG_ShiftKey(1), + FWL_EVENTFLAG_ControlKey(2), + FWL_EVENTFLAG_AltKey(4), + FWL_EVENTFLAG_MetaKey(8), + FWL_EVENTFLAG_KeyPad(16), + FWL_EVENTFLAG_AutoRepeat(32), + FWL_EVENTFLAG_LeftButtonDown(64), + FWL_EVENTFLAG_MiddleButtonDown(128), + FWL_EVENTFLAG_RightButtonDown(256); + + + final int value; + const FWL_EVENTFLAG(this.value); + + static FWL_EVENTFLAG fromValue(int value) => switch (value) { + 1 => FWL_EVENTFLAG_ShiftKey, + 2 => FWL_EVENTFLAG_ControlKey, + 4 => FWL_EVENTFLAG_AltKey, + 8 => FWL_EVENTFLAG_MetaKey, + 16 => FWL_EVENTFLAG_KeyPad, + 32 => FWL_EVENTFLAG_AutoRepeat, + 64 => FWL_EVENTFLAG_LeftButtonDown, + 128 => FWL_EVENTFLAG_MiddleButtonDown, + 256 => FWL_EVENTFLAG_RightButtonDown, + _ => throw ArgumentError('Unknown value for FWL_EVENTFLAG: $value'), + }; + +} + +/// Virtual keycodes. +enum FWL_VKEYCODE { + FWL_VKEY_Back(8), + FWL_VKEY_Tab(9), + FWL_VKEY_NewLine(10), + FWL_VKEY_Clear(12), + FWL_VKEY_Return(13), + FWL_VKEY_Shift(16), + FWL_VKEY_Control(17), + FWL_VKEY_Menu(18), + FWL_VKEY_Pause(19), + FWL_VKEY_Capital(20), + FWL_VKEY_Kana(21), + FWL_VKEY_Junja(23), + FWL_VKEY_Final(24), + FWL_VKEY_Hanja(25), + FWL_VKEY_Escape(27), + FWL_VKEY_Convert(28), + FWL_VKEY_NonConvert(29), + FWL_VKEY_Accept(30), + FWL_VKEY_ModeChange(31), + FWL_VKEY_Space(32), + FWL_VKEY_Prior(33), + FWL_VKEY_Next(34), + FWL_VKEY_End(35), + FWL_VKEY_Home(36), + FWL_VKEY_Left(37), + FWL_VKEY_Up(38), + FWL_VKEY_Right(39), + FWL_VKEY_Down(40), + FWL_VKEY_Select(41), + FWL_VKEY_Print(42), + FWL_VKEY_Execute(43), + FWL_VKEY_Snapshot(44), + FWL_VKEY_Insert(45), + FWL_VKEY_Delete(46), + FWL_VKEY_Help(47), + FWL_VKEY_0(48), + FWL_VKEY_1(49), + FWL_VKEY_2(50), + FWL_VKEY_3(51), + FWL_VKEY_4(52), + FWL_VKEY_5(53), + FWL_VKEY_6(54), + FWL_VKEY_7(55), + FWL_VKEY_8(56), + FWL_VKEY_9(57), + FWL_VKEY_A(65), + FWL_VKEY_B(66), + FWL_VKEY_C(67), + FWL_VKEY_D(68), + FWL_VKEY_E(69), + FWL_VKEY_F(70), + FWL_VKEY_G(71), + FWL_VKEY_H(72), + FWL_VKEY_I(73), + FWL_VKEY_J(74), + FWL_VKEY_K(75), + FWL_VKEY_L(76), + FWL_VKEY_M(77), + FWL_VKEY_N(78), + FWL_VKEY_O(79), + FWL_VKEY_P(80), + FWL_VKEY_Q(81), + FWL_VKEY_R(82), + FWL_VKEY_S(83), + FWL_VKEY_T(84), + FWL_VKEY_U(85), + FWL_VKEY_V(86), + FWL_VKEY_W(87), + FWL_VKEY_X(88), + FWL_VKEY_Y(89), + FWL_VKEY_Z(90), + FWL_VKEY_LWin(91), + FWL_VKEY_RWin(92), + FWL_VKEY_Apps(93), + FWL_VKEY_Sleep(95), + FWL_VKEY_NumPad0(96), + FWL_VKEY_NumPad1(97), + FWL_VKEY_NumPad2(98), + FWL_VKEY_NumPad3(99), + FWL_VKEY_NumPad4(100), + FWL_VKEY_NumPad5(101), + FWL_VKEY_NumPad6(102), + FWL_VKEY_NumPad7(103), + FWL_VKEY_NumPad8(104), + FWL_VKEY_NumPad9(105), + FWL_VKEY_Multiply(106), + FWL_VKEY_Add(107), + FWL_VKEY_Separator(108), + FWL_VKEY_Subtract(109), + FWL_VKEY_Decimal(110), + FWL_VKEY_Divide(111), + FWL_VKEY_F1(112), + FWL_VKEY_F2(113), + FWL_VKEY_F3(114), + FWL_VKEY_F4(115), + FWL_VKEY_F5(116), + FWL_VKEY_F6(117), + FWL_VKEY_F7(118), + FWL_VKEY_F8(119), + FWL_VKEY_F9(120), + FWL_VKEY_F10(121), + FWL_VKEY_F11(122), + FWL_VKEY_F12(123), + FWL_VKEY_F13(124), + FWL_VKEY_F14(125), + FWL_VKEY_F15(126), + FWL_VKEY_F16(127), + FWL_VKEY_F17(128), + FWL_VKEY_F18(129), + FWL_VKEY_F19(130), + FWL_VKEY_F20(131), + FWL_VKEY_F21(132), + FWL_VKEY_F22(133), + FWL_VKEY_F23(134), + FWL_VKEY_F24(135), + FWL_VKEY_NunLock(144), + FWL_VKEY_Scroll(145), + FWL_VKEY_LShift(160), + FWL_VKEY_RShift(161), + FWL_VKEY_LControl(162), + FWL_VKEY_RControl(163), + FWL_VKEY_LMenu(164), + FWL_VKEY_RMenu(165), + FWL_VKEY_BROWSER_Back(166), + FWL_VKEY_BROWSER_Forward(167), + FWL_VKEY_BROWSER_Refresh(168), + FWL_VKEY_BROWSER_Stop(169), + FWL_VKEY_BROWSER_Search(170), + FWL_VKEY_BROWSER_Favorites(171), + FWL_VKEY_BROWSER_Home(172), + FWL_VKEY_VOLUME_Mute(173), + FWL_VKEY_VOLUME_Down(174), + FWL_VKEY_VOLUME_Up(175), + FWL_VKEY_MEDIA_NEXT_Track(176), + FWL_VKEY_MEDIA_PREV_Track(177), + FWL_VKEY_MEDIA_Stop(178), + FWL_VKEY_MEDIA_PLAY_Pause(179), + FWL_VKEY_MEDIA_LAUNCH_Mail(180), + FWL_VKEY_MEDIA_LAUNCH_MEDIA_Select(181), + FWL_VKEY_MEDIA_LAUNCH_APP1(182), + FWL_VKEY_MEDIA_LAUNCH_APP2(183), + FWL_VKEY_OEM_1(186), + FWL_VKEY_OEM_Plus(187), + FWL_VKEY_OEM_Comma(188), + FWL_VKEY_OEM_Minus(189), + FWL_VKEY_OEM_Period(190), + FWL_VKEY_OEM_2(191), + FWL_VKEY_OEM_3(192), + FWL_VKEY_OEM_4(219), + FWL_VKEY_OEM_5(220), + FWL_VKEY_OEM_6(221), + FWL_VKEY_OEM_7(222), + FWL_VKEY_OEM_8(223), + FWL_VKEY_OEM_102(226), + FWL_VKEY_ProcessKey(229), + FWL_VKEY_Packet(231), + FWL_VKEY_Attn(246), + FWL_VKEY_Crsel(247), + FWL_VKEY_Exsel(248), + FWL_VKEY_Ereof(249), + FWL_VKEY_Play(250), + FWL_VKEY_Zoom(251), + FWL_VKEY_NoName(252), + FWL_VKEY_PA1(253), + FWL_VKEY_OEM_Clear(254), + FWL_VKEY_Unknown(0); + + static const FWL_VKEY_Hangul = FWL_VKEY_Kana; + static const FWL_VKEY_Kanji = FWL_VKEY_Hanja; + static const FWL_VKEY_Command = FWL_VKEY_LWin; + + final int value; + const FWL_VKEYCODE(this.value); + + static FWL_VKEYCODE fromValue(int value) => switch (value) { + 8 => FWL_VKEY_Back, + 9 => FWL_VKEY_Tab, + 10 => FWL_VKEY_NewLine, + 12 => FWL_VKEY_Clear, + 13 => FWL_VKEY_Return, + 16 => FWL_VKEY_Shift, + 17 => FWL_VKEY_Control, + 18 => FWL_VKEY_Menu, + 19 => FWL_VKEY_Pause, + 20 => FWL_VKEY_Capital, + 21 => FWL_VKEY_Kana, + 23 => FWL_VKEY_Junja, + 24 => FWL_VKEY_Final, + 25 => FWL_VKEY_Hanja, + 27 => FWL_VKEY_Escape, + 28 => FWL_VKEY_Convert, + 29 => FWL_VKEY_NonConvert, + 30 => FWL_VKEY_Accept, + 31 => FWL_VKEY_ModeChange, + 32 => FWL_VKEY_Space, + 33 => FWL_VKEY_Prior, + 34 => FWL_VKEY_Next, + 35 => FWL_VKEY_End, + 36 => FWL_VKEY_Home, + 37 => FWL_VKEY_Left, + 38 => FWL_VKEY_Up, + 39 => FWL_VKEY_Right, + 40 => FWL_VKEY_Down, + 41 => FWL_VKEY_Select, + 42 => FWL_VKEY_Print, + 43 => FWL_VKEY_Execute, + 44 => FWL_VKEY_Snapshot, + 45 => FWL_VKEY_Insert, + 46 => FWL_VKEY_Delete, + 47 => FWL_VKEY_Help, + 48 => FWL_VKEY_0, + 49 => FWL_VKEY_1, + 50 => FWL_VKEY_2, + 51 => FWL_VKEY_3, + 52 => FWL_VKEY_4, + 53 => FWL_VKEY_5, + 54 => FWL_VKEY_6, + 55 => FWL_VKEY_7, + 56 => FWL_VKEY_8, + 57 => FWL_VKEY_9, + 65 => FWL_VKEY_A, + 66 => FWL_VKEY_B, + 67 => FWL_VKEY_C, + 68 => FWL_VKEY_D, + 69 => FWL_VKEY_E, + 70 => FWL_VKEY_F, + 71 => FWL_VKEY_G, + 72 => FWL_VKEY_H, + 73 => FWL_VKEY_I, + 74 => FWL_VKEY_J, + 75 => FWL_VKEY_K, + 76 => FWL_VKEY_L, + 77 => FWL_VKEY_M, + 78 => FWL_VKEY_N, + 79 => FWL_VKEY_O, + 80 => FWL_VKEY_P, + 81 => FWL_VKEY_Q, + 82 => FWL_VKEY_R, + 83 => FWL_VKEY_S, + 84 => FWL_VKEY_T, + 85 => FWL_VKEY_U, + 86 => FWL_VKEY_V, + 87 => FWL_VKEY_W, + 88 => FWL_VKEY_X, + 89 => FWL_VKEY_Y, + 90 => FWL_VKEY_Z, + 91 => FWL_VKEY_LWin, + 92 => FWL_VKEY_RWin, + 93 => FWL_VKEY_Apps, + 95 => FWL_VKEY_Sleep, + 96 => FWL_VKEY_NumPad0, + 97 => FWL_VKEY_NumPad1, + 98 => FWL_VKEY_NumPad2, + 99 => FWL_VKEY_NumPad3, + 100 => FWL_VKEY_NumPad4, + 101 => FWL_VKEY_NumPad5, + 102 => FWL_VKEY_NumPad6, + 103 => FWL_VKEY_NumPad7, + 104 => FWL_VKEY_NumPad8, + 105 => FWL_VKEY_NumPad9, + 106 => FWL_VKEY_Multiply, + 107 => FWL_VKEY_Add, + 108 => FWL_VKEY_Separator, + 109 => FWL_VKEY_Subtract, + 110 => FWL_VKEY_Decimal, + 111 => FWL_VKEY_Divide, + 112 => FWL_VKEY_F1, + 113 => FWL_VKEY_F2, + 114 => FWL_VKEY_F3, + 115 => FWL_VKEY_F4, + 116 => FWL_VKEY_F5, + 117 => FWL_VKEY_F6, + 118 => FWL_VKEY_F7, + 119 => FWL_VKEY_F8, + 120 => FWL_VKEY_F9, + 121 => FWL_VKEY_F10, + 122 => FWL_VKEY_F11, + 123 => FWL_VKEY_F12, + 124 => FWL_VKEY_F13, + 125 => FWL_VKEY_F14, + 126 => FWL_VKEY_F15, + 127 => FWL_VKEY_F16, + 128 => FWL_VKEY_F17, + 129 => FWL_VKEY_F18, + 130 => FWL_VKEY_F19, + 131 => FWL_VKEY_F20, + 132 => FWL_VKEY_F21, + 133 => FWL_VKEY_F22, + 134 => FWL_VKEY_F23, + 135 => FWL_VKEY_F24, + 144 => FWL_VKEY_NunLock, + 145 => FWL_VKEY_Scroll, + 160 => FWL_VKEY_LShift, + 161 => FWL_VKEY_RShift, + 162 => FWL_VKEY_LControl, + 163 => FWL_VKEY_RControl, + 164 => FWL_VKEY_LMenu, + 165 => FWL_VKEY_RMenu, + 166 => FWL_VKEY_BROWSER_Back, + 167 => FWL_VKEY_BROWSER_Forward, + 168 => FWL_VKEY_BROWSER_Refresh, + 169 => FWL_VKEY_BROWSER_Stop, + 170 => FWL_VKEY_BROWSER_Search, + 171 => FWL_VKEY_BROWSER_Favorites, + 172 => FWL_VKEY_BROWSER_Home, + 173 => FWL_VKEY_VOLUME_Mute, + 174 => FWL_VKEY_VOLUME_Down, + 175 => FWL_VKEY_VOLUME_Up, + 176 => FWL_VKEY_MEDIA_NEXT_Track, + 177 => FWL_VKEY_MEDIA_PREV_Track, + 178 => FWL_VKEY_MEDIA_Stop, + 179 => FWL_VKEY_MEDIA_PLAY_Pause, + 180 => FWL_VKEY_MEDIA_LAUNCH_Mail, + 181 => FWL_VKEY_MEDIA_LAUNCH_MEDIA_Select, + 182 => FWL_VKEY_MEDIA_LAUNCH_APP1, + 183 => FWL_VKEY_MEDIA_LAUNCH_APP2, + 186 => FWL_VKEY_OEM_1, + 187 => FWL_VKEY_OEM_Plus, + 188 => FWL_VKEY_OEM_Comma, + 189 => FWL_VKEY_OEM_Minus, + 190 => FWL_VKEY_OEM_Period, + 191 => FWL_VKEY_OEM_2, + 192 => FWL_VKEY_OEM_3, + 219 => FWL_VKEY_OEM_4, + 220 => FWL_VKEY_OEM_5, + 221 => FWL_VKEY_OEM_6, + 222 => FWL_VKEY_OEM_7, + 223 => FWL_VKEY_OEM_8, + 226 => FWL_VKEY_OEM_102, + 229 => FWL_VKEY_ProcessKey, + 231 => FWL_VKEY_Packet, + 246 => FWL_VKEY_Attn, + 247 => FWL_VKEY_Crsel, + 248 => FWL_VKEY_Exsel, + 249 => FWL_VKEY_Ereof, + 250 => FWL_VKEY_Play, + 251 => FWL_VKEY_Zoom, + 252 => FWL_VKEY_NoName, + 253 => FWL_VKEY_PA1, + 254 => FWL_VKEY_OEM_Clear, + 0 => FWL_VKEY_Unknown, + _ => throw ArgumentError('Unknown value for FWL_VKEYCODE: $value'), + }; + + @override + String toString() { + if (this == FWL_VKEY_Kana) return "FWL_VKEYCODE.FWL_VKEY_Kana, FWL_VKEYCODE.FWL_VKEY_Hangul"; + if (this == FWL_VKEY_Hanja) return "FWL_VKEYCODE.FWL_VKEY_Hanja, FWL_VKEYCODE.FWL_VKEY_Kanji"; + if (this == FWL_VKEY_LWin) return "FWL_VKEYCODE.FWL_VKEY_LWin, FWL_VKEYCODE.FWL_VKEY_Command"; + return super.toString(); + }} + +/// Interface for unsupported feature notifications. +final class _UNSUPPORT_INFO extends ffi.Struct{ + /// Version number of the interface. Must be 1. + @ffi.Int() + external int version; + + /// Unsupported object notification function. + /// Interface Version: 1 + /// Implementation Required: Yes + /// + /// pThis - pointer to the interface structure. + /// nType - the type of unsupported object. One of the |FPDF_UNSP_*| entries. + external ffi.Pointer pThis, ffi.Int nType)>> FSDK_UnSupport_Handler; + +} + +/// Interface for unsupported feature notifications. +typedef UNSUPPORT_INFO = _UNSUPPORT_INFO; +typedef __darwin_time_t = ffi.Long; +typedef Dart__darwin_time_t = int; +typedef time_t = __darwin_time_t; +final class tm extends ffi.Struct{ + /// seconds after the minute [0-60] + @ffi.Int() + external int tm_sec; + + /// minutes after the hour [0-59] + @ffi.Int() + external int tm_min; + + /// hours since midnight [0-23] + @ffi.Int() + external int tm_hour; + + /// day of the month [1-31] + @ffi.Int() + external int tm_mday; + + /// months since January [0-11] + @ffi.Int() + external int tm_mon; + + /// years since 1900 + @ffi.Int() + external int tm_year; + + /// days since Sunday [0-6] + @ffi.Int() + external int tm_wday; + + /// days since January 1 [0-365] + @ffi.Int() + external int tm_yday; + + /// Daylight Savings Time flag + @ffi.Int() + external int tm_isdst; + + /// offset from UTC in seconds + @ffi.Long() + external int tm_gmtoff; + + /// timezone abbreviation + external ffi.Pointer tm_zone; + +} + + +const int FPDF_OBJECT_UNKNOWN = 0; + + +const int FPDF_OBJECT_BOOLEAN = 1; + + +const int FPDF_OBJECT_NUMBER = 2; + + +const int FPDF_OBJECT_STRING = 3; + + +const int FPDF_OBJECT_NAME = 4; + + +const int FPDF_OBJECT_ARRAY = 5; + + +const int FPDF_OBJECT_DICTIONARY = 6; + + +const int FPDF_OBJECT_STREAM = 7; + + +const int FPDF_OBJECT_NULLOBJ = 8; + + +const int FPDF_OBJECT_REFERENCE = 9; + + +const int FPDF_POLICY_MACHINETIME_ACCESS = 0; + + +const int FPDF_ERR_SUCCESS = 0; + + +const int FPDF_ERR_UNKNOWN = 1; + + +const int FPDF_ERR_FILE = 2; + + +const int FPDF_ERR_FORMAT = 3; + + +const int FPDF_ERR_PASSWORD = 4; + + +const int FPDF_ERR_SECURITY = 5; + + +const int FPDF_ERR_PAGE = 6; + + +const int FPDF_ANNOT = 1; + + +const int FPDF_LCD_TEXT = 2; + + +const int FPDF_NO_NATIVETEXT = 4; + + +const int FPDF_GRAYSCALE = 8; + + +const int FPDF_DEBUG_INFO = 128; + + +const int FPDF_NO_CATCH = 256; + + +const int FPDF_RENDER_LIMITEDIMAGECACHE = 512; + + +const int FPDF_RENDER_FORCEHALFTONE = 1024; + + +const int FPDF_PRINTING = 2048; + + +const int FPDF_RENDER_NO_SMOOTHTEXT = 4096; + + +const int FPDF_RENDER_NO_SMOOTHIMAGE = 8192; + + +const int FPDF_RENDER_NO_SMOOTHPATH = 16384; + + +const int FPDF_REVERSE_BYTE_ORDER = 16; + + +const int FPDF_CONVERT_FILL_TO_STROKE = 32; + + +const int FPDFBitmap_Unknown = 0; + + +const int FPDFBitmap_Gray = 1; + + +const int FPDFBitmap_BGR = 2; + + +const int FPDFBitmap_BGRx = 3; + + +const int FPDFBitmap_BGRA = 4; + + +const int FPDFBitmap_BGRA_Premul = 5; + + +const int FXFONT_ANSI_CHARSET = 0; + + +const int FXFONT_DEFAULT_CHARSET = 1; + + +const int FXFONT_SYMBOL_CHARSET = 2; + + +const int FXFONT_SHIFTJIS_CHARSET = 128; + + +const int FXFONT_HANGEUL_CHARSET = 129; + + +const int FXFONT_GB2312_CHARSET = 134; + + +const int FXFONT_CHINESEBIG5_CHARSET = 136; + + +const int FXFONT_GREEK_CHARSET = 161; + + +const int FXFONT_VIETNAMESE_CHARSET = 163; + + +const int FXFONT_HEBREW_CHARSET = 177; + + +const int FXFONT_ARABIC_CHARSET = 178; + + +const int FXFONT_CYRILLIC_CHARSET = 204; + + +const int FXFONT_THAI_CHARSET = 222; + + +const int FXFONT_EASTERNEUROPEAN_CHARSET = 238; + + +const int FXFONT_FF_FIXEDPITCH = 1; + + +const int FXFONT_FF_ROMAN = 16; + + +const int FXFONT_FF_SCRIPT = 64; + + +const int FXFONT_FW_NORMAL = 400; + + +const int FXFONT_FW_BOLD = 700; + + +const int FPDF_MATCHCASE = 1; + + +const int FPDF_MATCHWHOLEWORD = 2; + + +const int FPDF_CONSECUTIVE = 4; + + +const int FPDF_RENDER_READY = 0; + + +const int FPDF_RENDER_TOBECONTINUED = 1; + + +const int FPDF_RENDER_DONE = 2; + + +const int FPDF_RENDER_FAILED = 3; + + +const int FPDF_COLORSPACE_UNKNOWN = 0; + + +const int FPDF_COLORSPACE_DEVICEGRAY = 1; + + +const int FPDF_COLORSPACE_DEVICERGB = 2; + + +const int FPDF_COLORSPACE_DEVICECMYK = 3; + + +const int FPDF_COLORSPACE_CALGRAY = 4; + + +const int FPDF_COLORSPACE_CALRGB = 5; + + +const int FPDF_COLORSPACE_LAB = 6; + + +const int FPDF_COLORSPACE_ICCBASED = 7; + + +const int FPDF_COLORSPACE_SEPARATION = 8; + + +const int FPDF_COLORSPACE_DEVICEN = 9; + + +const int FPDF_COLORSPACE_INDEXED = 10; + + +const int FPDF_COLORSPACE_PATTERN = 11; + + +const int FPDF_PAGEOBJ_UNKNOWN = 0; + + +const int FPDF_PAGEOBJ_TEXT = 1; + + +const int FPDF_PAGEOBJ_PATH = 2; + + +const int FPDF_PAGEOBJ_IMAGE = 3; + + +const int FPDF_PAGEOBJ_SHADING = 4; + + +const int FPDF_PAGEOBJ_FORM = 5; + + +const int FPDF_SEGMENT_UNKNOWN = -1; + + +const int FPDF_SEGMENT_LINETO = 0; + + +const int FPDF_SEGMENT_BEZIERTO = 1; + + +const int FPDF_SEGMENT_MOVETO = 2; + + +const int FPDF_FILLMODE_NONE = 0; + + +const int FPDF_FILLMODE_ALTERNATE = 1; + + +const int FPDF_FILLMODE_WINDING = 2; + + +const int FPDF_FONT_TYPE1 = 1; + + +const int FPDF_FONT_TRUETYPE = 2; + + +const int FPDF_LINECAP_BUTT = 0; + + +const int FPDF_LINECAP_ROUND = 1; + + +const int FPDF_LINECAP_PROJECTING_SQUARE = 2; + + +const int FPDF_LINEJOIN_MITER = 0; + + +const int FPDF_LINEJOIN_ROUND = 1; + + +const int FPDF_LINEJOIN_BEVEL = 2; + + +const int FPDF_PRINTMODE_EMF = 0; + + +const int FPDF_PRINTMODE_TEXTONLY = 1; + + +const int FPDF_PRINTMODE_POSTSCRIPT2 = 2; + + +const int FPDF_PRINTMODE_POSTSCRIPT3 = 3; + + +const int FPDF_PRINTMODE_POSTSCRIPT2_PASSTHROUGH = 4; + + +const int FPDF_PRINTMODE_POSTSCRIPT3_PASSTHROUGH = 5; + + +const int FPDF_PRINTMODE_EMF_IMAGE_MASKS = 6; + + +const int FPDF_PRINTMODE_POSTSCRIPT3_TYPE42 = 7; + + +const int FPDF_PRINTMODE_POSTSCRIPT3_TYPE42_PASSTHROUGH = 8; + + +const int FORMTYPE_NONE = 0; + + +const int FORMTYPE_ACRO_FORM = 1; + + +const int FORMTYPE_XFA_FULL = 2; + + +const int FORMTYPE_XFA_FOREGROUND = 3; + + +const int FORMTYPE_COUNT = 4; + + +const int JSPLATFORM_ALERT_BUTTON_OK = 0; + + +const int JSPLATFORM_ALERT_BUTTON_OKCANCEL = 1; + + +const int JSPLATFORM_ALERT_BUTTON_YESNO = 2; + + +const int JSPLATFORM_ALERT_BUTTON_YESNOCANCEL = 3; + + +const int JSPLATFORM_ALERT_BUTTON_DEFAULT = 0; + + +const int JSPLATFORM_ALERT_ICON_ERROR = 0; + + +const int JSPLATFORM_ALERT_ICON_WARNING = 1; + + +const int JSPLATFORM_ALERT_ICON_QUESTION = 2; + + +const int JSPLATFORM_ALERT_ICON_STATUS = 3; + + +const int JSPLATFORM_ALERT_ICON_ASTERISK = 4; + + +const int JSPLATFORM_ALERT_ICON_DEFAULT = 0; + + +const int JSPLATFORM_ALERT_RETURN_OK = 1; + + +const int JSPLATFORM_ALERT_RETURN_CANCEL = 2; + + +const int JSPLATFORM_ALERT_RETURN_NO = 3; + + +const int JSPLATFORM_ALERT_RETURN_YES = 4; + + +const int JSPLATFORM_BEEP_ERROR = 0; + + +const int JSPLATFORM_BEEP_WARNING = 1; + + +const int JSPLATFORM_BEEP_QUESTION = 2; + + +const int JSPLATFORM_BEEP_STATUS = 3; + + +const int JSPLATFORM_BEEP_DEFAULT = 4; + + +const int FXCT_ARROW = 0; + + +const int FXCT_NESW = 1; + + +const int FXCT_NWSE = 2; + + +const int FXCT_VBEAM = 3; + + +const int FXCT_HBEAM = 4; + + +const int FXCT_HAND = 5; + + +const int FPDFDOC_AACTION_WC = 16; + + +const int FPDFDOC_AACTION_WS = 17; + + +const int FPDFDOC_AACTION_DS = 18; + + +const int FPDFDOC_AACTION_WP = 19; + + +const int FPDFDOC_AACTION_DP = 20; + + +const int FPDFPAGE_AACTION_OPEN = 0; + + +const int FPDFPAGE_AACTION_CLOSE = 1; + + +const int FPDF_FORMFIELD_UNKNOWN = 0; + + +const int FPDF_FORMFIELD_PUSHBUTTON = 1; + + +const int FPDF_FORMFIELD_CHECKBOX = 2; + + +const int FPDF_FORMFIELD_RADIOBUTTON = 3; + + +const int FPDF_FORMFIELD_COMBOBOX = 4; + + +const int FPDF_FORMFIELD_LISTBOX = 5; + + +const int FPDF_FORMFIELD_TEXTFIELD = 6; + + +const int FPDF_FORMFIELD_SIGNATURE = 7; + + +const int FPDF_FORMFIELD_COUNT = 8; + + +const int FPDF_ANNOT_UNKNOWN = 0; + + +const int FPDF_ANNOT_TEXT = 1; + + +const int FPDF_ANNOT_LINK = 2; + + +const int FPDF_ANNOT_FREETEXT = 3; + + +const int FPDF_ANNOT_LINE = 4; + + +const int FPDF_ANNOT_SQUARE = 5; + + +const int FPDF_ANNOT_CIRCLE = 6; + + +const int FPDF_ANNOT_POLYGON = 7; + + +const int FPDF_ANNOT_POLYLINE = 8; + + +const int FPDF_ANNOT_HIGHLIGHT = 9; + + +const int FPDF_ANNOT_UNDERLINE = 10; + + +const int FPDF_ANNOT_SQUIGGLY = 11; + + +const int FPDF_ANNOT_STRIKEOUT = 12; + + +const int FPDF_ANNOT_STAMP = 13; + + +const int FPDF_ANNOT_CARET = 14; + + +const int FPDF_ANNOT_INK = 15; + + +const int FPDF_ANNOT_POPUP = 16; + + +const int FPDF_ANNOT_FILEATTACHMENT = 17; + + +const int FPDF_ANNOT_SOUND = 18; + + +const int FPDF_ANNOT_MOVIE = 19; + + +const int FPDF_ANNOT_WIDGET = 20; + + +const int FPDF_ANNOT_SCREEN = 21; + + +const int FPDF_ANNOT_PRINTERMARK = 22; + + +const int FPDF_ANNOT_TRAPNET = 23; + + +const int FPDF_ANNOT_WATERMARK = 24; + + +const int FPDF_ANNOT_THREED = 25; + + +const int FPDF_ANNOT_RICHMEDIA = 26; + + +const int FPDF_ANNOT_XFAWIDGET = 27; + + +const int FPDF_ANNOT_REDACT = 28; + + +const int FPDF_ANNOT_FLAG_NONE = 0; + + +const int FPDF_ANNOT_FLAG_INVISIBLE = 1; + + +const int FPDF_ANNOT_FLAG_HIDDEN = 2; + + +const int FPDF_ANNOT_FLAG_PRINT = 4; + + +const int FPDF_ANNOT_FLAG_NOZOOM = 8; + + +const int FPDF_ANNOT_FLAG_NOROTATE = 16; + + +const int FPDF_ANNOT_FLAG_NOVIEW = 32; + + +const int FPDF_ANNOT_FLAG_READONLY = 64; + + +const int FPDF_ANNOT_FLAG_LOCKED = 128; + + +const int FPDF_ANNOT_FLAG_TOGGLENOVIEW = 256; + + +const int FPDF_ANNOT_APPEARANCEMODE_NORMAL = 0; + + +const int FPDF_ANNOT_APPEARANCEMODE_ROLLOVER = 1; + + +const int FPDF_ANNOT_APPEARANCEMODE_DOWN = 2; + + +const int FPDF_ANNOT_APPEARANCEMODE_COUNT = 3; + + +const int FPDF_FORMFLAG_NONE = 0; + + +const int FPDF_FORMFLAG_READONLY = 1; + + +const int FPDF_FORMFLAG_REQUIRED = 2; + + +const int FPDF_FORMFLAG_NOEXPORT = 4; + + +const int FPDF_FORMFLAG_TEXT_MULTILINE = 4096; + + +const int FPDF_FORMFLAG_TEXT_PASSWORD = 8192; + + +const int FPDF_FORMFLAG_CHOICE_COMBO = 131072; + + +const int FPDF_FORMFLAG_CHOICE_EDIT = 262144; + + +const int FPDF_FORMFLAG_CHOICE_MULTI_SELECT = 2097152; + + +const int FPDF_ANNOT_AACTION_KEY_STROKE = 12; + + +const int FPDF_ANNOT_AACTION_FORMAT = 13; + + +const int FPDF_ANNOT_AACTION_VALIDATE = 14; + + +const int FPDF_ANNOT_AACTION_CALCULATE = 15; + + +const int FPDF_INCREMENTAL = 1; + + +const int FPDF_NO_INCREMENTAL = 2; + + +const int FPDF_REMOVE_SECURITY = 3; + + +const int PDFACTION_UNSUPPORTED = 0; + + +const int PDFACTION_GOTO = 1; + + +const int PDFACTION_REMOTEGOTO = 2; + + +const int PDFACTION_URI = 3; + + +const int PDFACTION_LAUNCH = 4; + + +const int PDFACTION_EMBEDDEDGOTO = 5; + + +const int PDFDEST_VIEW_UNKNOWN_MODE = 0; + + +const int PDFDEST_VIEW_XYZ = 1; + + +const int PDFDEST_VIEW_FIT = 2; + + +const int PDFDEST_VIEW_FITH = 3; + + +const int PDFDEST_VIEW_FITV = 4; + + +const int PDFDEST_VIEW_FITR = 5; + + +const int PDFDEST_VIEW_FITB = 6; + + +const int PDFDEST_VIEW_FITBH = 7; + + +const int PDFDEST_VIEW_FITBV = 8; + + +const int PDF_LINEARIZATION_UNKNOWN = -1; + + +const int PDF_NOT_LINEARIZED = 0; + + +const int PDF_LINEARIZED = 1; + + +const int PDF_DATA_ERROR = -1; + + +const int PDF_DATA_NOTAVAIL = 0; + + +const int PDF_DATA_AVAIL = 1; + + +const int PDF_FORM_ERROR = -1; + + +const int PDF_FORM_NOTAVAIL = 0; + + +const int PDF_FORM_AVAIL = 1; + + +const int PDF_FORM_NOTEXIST = 2; + + +const int FPDF_UNSP_DOC_XFAFORM = 1; + + +const int FPDF_UNSP_DOC_PORTABLECOLLECTION = 2; + + +const int FPDF_UNSP_DOC_ATTACHMENT = 3; + + +const int FPDF_UNSP_DOC_SECURITY = 4; + + +const int FPDF_UNSP_DOC_SHAREDREVIEW = 5; + + +const int FPDF_UNSP_DOC_SHAREDFORM_ACROBAT = 6; + + +const int FPDF_UNSP_DOC_SHAREDFORM_FILESYSTEM = 7; + + +const int FPDF_UNSP_DOC_SHAREDFORM_EMAIL = 8; + + +const int FPDF_UNSP_ANNOT_3DANNOT = 11; + + +const int FPDF_UNSP_ANNOT_MOVIE = 12; + + +const int FPDF_UNSP_ANNOT_SOUND = 13; + + +const int FPDF_UNSP_ANNOT_SCREEN_MEDIA = 14; + + +const int FPDF_UNSP_ANNOT_SCREEN_RICHMEDIA = 15; + + +const int FPDF_UNSP_ANNOT_ATTACHMENT = 16; + + +const int FPDF_UNSP_ANNOT_SIG = 17; + + +const int PAGEMODE_UNKNOWN = -1; + + +const int PAGEMODE_USENONE = 0; + + +const int PAGEMODE_USEOUTLINES = 1; + + +const int PAGEMODE_USETHUMBS = 2; + + +const int PAGEMODE_FULLSCREEN = 3; + + +const int PAGEMODE_USEOC = 4; + + +const int PAGEMODE_USEATTACHMENTS = 5; + + +const int FLATTEN_FAIL = 0; + + +const int FLATTEN_SUCCESS = 1; + + +const int FLATTEN_NOTHINGTODO = 2; + + +const int FLAT_NORMALDISPLAY = 0; + + +const int FLAT_PRINT = 1; + diff --git a/packages/pdfium_dart/lib/src/pdfium_downloader.dart b/packages/pdfium_dart/lib/src/pdfium_downloader.dart new file mode 100644 index 00000000..8095dbf0 --- /dev/null +++ b/packages/pdfium_dart/lib/src/pdfium_downloader.dart @@ -0,0 +1,140 @@ +import 'dart:ffi'; +import 'dart:io'; + +import 'package:archive/archive_io.dart'; +import 'package:http/http.dart' as http; +import 'package:path/path.dart' as path; + +import 'pdfium_bindings.dart' as pdfium_bindings; + +/// The release of pdfium to download. +/// +/// The actual binaries are downloaded from https://github.com/bblanchon/pdfium-binaries. +const currentPDFiumRelease = 'chromium%2F7520'; + +/// Helper function to get PDFium instance. +/// +/// This function downloads the PDFium module if necessary. +/// +/// - [cacheRootPath]: The root directory to cache the downloaded PDFium module. +/// - [pdfiumRelease]: The release of PDFium to download. Defaults to [currentPDFiumRelease]. +/// +/// For macOS, the downloaded library is not codesigned. If you encounter issues loading the library, +/// you may need to manually codesign it using the following command: +/// +/// ``` +/// codesign --force --sign - +/// ``` +Future getPdfium({ + String? cacheRootPath, + String? pdfiumRelease = currentPDFiumRelease, +}) async { + cacheRootPath ??= path.join( + Directory.systemTemp.path, + 'pdfium_dart', + 'cache', + pdfiumRelease, + ); + + if (!await File(cacheRootPath).exists()) { + await Directory(cacheRootPath).create(recursive: true); + } + final modulePath = await PDFiumDownloader.downloadAndGetPDFiumModulePath( + cacheRootPath, + pdfiumRelease: pdfiumRelease, + ); + + try { + return pdfium_bindings.PDFium(DynamicLibrary.open(modulePath)); + } catch (e) { + throw Exception('Failed to load PDFium module at $modulePath: $e'); + } +} + +/// PdfiumDownloader is a utility class to download the PDFium module for various platforms. +class PDFiumDownloader { + PDFiumDownloader._(); + + /// Downloads the pdfium module for the current platform and architecture. + /// + /// Currently, the following platforms are supported: + /// - Windows x64 + /// - Linux x64, arm64 + /// - macOS x64, arm64 + /// + /// The binaries are downloaded from https://github.com/bblanchon/pdfium-binaries. + static Future downloadAndGetPDFiumModulePath( + String cacheRootPath, { + String? pdfiumRelease, + }) async { + final pa = RegExp(r'"([^_]+)_([^_]+)"').firstMatch(Platform.version)!; + final platform = pa[1]!; + final arch = pa[2]!; + if (platform == 'windows' && arch == 'x64') { + return await downloadPDFium( + cacheRootPath, + 'win', + arch, + 'bin/pdfium.dll', + pdfiumRelease, + ); + } + if (platform == 'linux' && (arch == 'x64' || arch == 'arm64')) { + return await downloadPDFium( + cacheRootPath, + platform, + arch, + 'lib/libpdfium.so', + pdfiumRelease, + ); + } + if (platform == 'macos') { + return await downloadPDFium( + cacheRootPath, + 'mac', + arch, + 'lib/libpdfium.dylib', + pdfiumRelease, + ); + } else { + throw Exception('Unsupported platform: $platform-$arch'); + } + } + + /// Downloads the pdfium module for the given platform and architecture. + static Future downloadPDFium( + String cacheRootPath, + String platform, + String arch, + String modulePath, + String? pdfiumRelease, + ) async { + pdfiumRelease ??= currentPDFiumRelease; + final pdfiumReleaseDirName = pdfiumRelease.replaceAll( + RegExp(r'[^A-Za-z0-9_]+'), + '_', + ); + final cacheDir = Directory( + '$cacheRootPath/$pdfiumReleaseDirName/$platform-$arch', + ); + final targetPath = '${cacheDir.path}/$modulePath'; + if (await File(targetPath).exists()) return targetPath; + + final uri = + 'https://github.com/bblanchon/pdfium-binaries/releases/download/$pdfiumRelease/pdfium-$platform-$arch.tgz'; + final tgz = await http.Client().get(Uri.parse(uri)); + if (tgz.statusCode != 200) { + throw Exception('Failed to download pdfium: $uri'); + } + + await cacheDir.create(recursive: true); + final archive = TarDecoder().decodeBytes( + GZipDecoder().decodeBytes(tgz.bodyBytes), + ); + try { + await cacheDir.delete(recursive: true); + } catch (_) {} + await extractArchiveToDisk(archive, cacheDir.path); + return targetPath; + } +} diff --git a/packages/pdfium_dart/pubspec.yaml b/packages/pdfium_dart/pubspec.yaml new file mode 100644 index 00000000..d6044342 --- /dev/null +++ b/packages/pdfium_dart/pubspec.yaml @@ -0,0 +1,63 @@ +name: pdfium_dart +description: Dart FFI bindings for PDFium library. Provides low-level access to PDFium's C API from Dart. +version: 0.1.3 +homepage: https://github.com/espresso3389/pdfrx +repository: https://github.com/espresso3389/pdfrx/tree/master/packages/pdfium_dart +issue_tracker: https://github.com/espresso3389/pdfrx/issues + +environment: + sdk: ^3.9.0 +resolution: workspace + +dependencies: + archive: ^4.0.7 + ffi: ^2.1.4 + http: ^1.5.0 + path: ^1.9.1 + +dev_dependencies: + ffigen: ^19.1.0 + lints: ^6.0.0 + test: ^1.26.2 + +# To generate the bindings, firstly you must run the test once to download PDFium headers: +# dart run test +# then run the following command: +# dart run ffigen +ffigen: + output: + bindings: 'lib/src/pdfium_bindings.dart' + description: 'Bindings for PDFium C API' + headers: + entry-points: + - 'test/.tmp/**/fpdf_signature.h' + - 'test/.tmp/**/fpdf_sysfontinfo.h' + - 'test/.tmp/**/fpdf_javascript.h' + - 'test/.tmp/**/fpdf_text.h' + - 'test/.tmp/**/fpdf_searchex.h' + - 'test/.tmp/**/fpdf_progressive.h' + - 'test/.tmp/**/fpdfview.h' + - 'test/.tmp/**/fpdf_edit.h' + - 'test/.tmp/**/fpdf_attachment.h' + - 'test/.tmp/**/fpdf_annot.h' + - 'test/.tmp/**/fpdf_catalog.h' + - 'test/.tmp/**/fpdf_ppo.h' + - 'test/.tmp/**/fpdf_formfill.h' + - 'test/.tmp/**/fpdf_save.h' + - 'test/.tmp/**/fpdf_doc.h' + - 'test/.tmp/**/fpdf_structtree.h' + - 'test/.tmp/**/fpdf_dataavail.h' + - 'test/.tmp/**/fpdf_fwlevent.h' + - 'test/.tmp/**/fpdf_ext.h' + - 'test/.tmp/**/fpdf_transformpage.h' + - 'test/.tmp/**/fpdf_flatten.h' + - 'test/.tmp/**/fpdf_thumbnail.h' + include-directives: + - 'test/.tmp/**/**' + preamble: | + // ignore_for_file: unused_field + // dart format off + name: 'PDFium' + comments: + style: any + length: full diff --git a/packages/pdfium_dart/test/pdfium_test.dart b/packages/pdfium_dart/test/pdfium_test.dart new file mode 100644 index 00000000..e148634c --- /dev/null +++ b/packages/pdfium_dart/test/pdfium_test.dart @@ -0,0 +1,20 @@ +import 'dart:io'; + +import 'package:pdfium_dart/pdfium_dart.dart'; +import 'package:test/test.dart'; + +final tmpRoot = Directory('${Directory.current.path}/test/.tmp'); +final testPdfFile = File('../pdfrx/example/viewer/assets/hello.pdf'); + +PDFium? _pdfium; + +void main() { + setUp(() async { + _pdfium = await getPdfium(cacheRootPath: tmpRoot.path); + }); + + test('PDFium Initialization', () { + _pdfium!.FPDF_InitLibrary(); + _pdfium!.FPDF_DestroyLibrary(); + }); +} diff --git a/packages/pdfium_flutter/.gitignore b/packages/pdfium_flutter/.gitignore new file mode 100644 index 00000000..463e519d --- /dev/null +++ b/packages/pdfium_flutter/.gitignore @@ -0,0 +1,4 @@ +.build/ +**.xcframework/ +*.zip +.pdfium_hash diff --git a/packages/pdfium_flutter/CHANGELOG.md b/packages/pdfium_flutter/CHANGELOG.md new file mode 100644 index 00000000..ca0a8cf1 --- /dev/null +++ b/packages/pdfium_flutter/CHANGELOG.md @@ -0,0 +1,41 @@ +## 0.1.9 + +- FIXED: Inconsistent environment constraints - Flutter version now correctly requires 3.35.1+ to match Dart 3.9.0 requirement ([#553](https://github.com/espresso3389/pdfrx/issues/553)) + +## 0.1.8 + +- Dependency configuration updates. + +## 0.1.7 + +- Documentation updates. + +## 0.1.6 + +- Updated PDFium to version 144.0.7520.0 for Android, Linux, and Windows platforms. +- Updated `pdfium_dart` dependency to ^0.1.2. + +## 0.1.5 + +- Updated PDFium to version 144.0.7520.0 (build 20251111-190355). + +## 0.1.4 + +- Updated PDFium to version 144.0.7520.0 (build 20251111-183119). + +## 0.1.3 + +- Updated PDFium to version 144.0.7520.0 (build 20251111-181257). + +## 0.1.2 + +- Updated PDFium to version 144.0.7520.0 (build 20251111-173323). + +## 0.1.1 + +- Fixed SwiftPM package name inconsistency. +- Several PDFium capitalization affecting API names + +## 0.1.0 + +- First release. diff --git a/packages/pdfium_flutter/LICENSE b/packages/pdfium_flutter/LICENSE new file mode 100644 index 00000000..7de3ad69 --- /dev/null +++ b/packages/pdfium_flutter/LICENSE @@ -0,0 +1,11 @@ + +The MIT License (MIT) +=============== + +Copyright (c) 2025 @espresso3389 (Takashi Kawasaki) + +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. diff --git a/packages/pdfium_flutter/README.md b/packages/pdfium_flutter/README.md new file mode 100644 index 00000000..9063e6f9 --- /dev/null +++ b/packages/pdfium_flutter/README.md @@ -0,0 +1,48 @@ +# pdfium_flutter + +Flutter FFI plugin for loading PDFium native libraries. This package bundles PDFium binaries for Android, iOS, Windows, macOS, and Linux. + +This package is part of the [pdfrx](https://github.com/espresso3389/pdfrx) project. + +## Overview + +This package provides: + +- Pre-built PDFium native libraries for all supported platforms +- Utilities for loading PDFium at runtime +- Re-exports of [pdfium_dart](https://pub.dev/packages/pdfium_dart) FFI bindings + +## Platform Support + +| Platform | Support | Notes | +|----------|---------|-------| +| Android | ✅ | ARM64, ARMv7, x86, x86_64 | +| iOS | ✅ | ARM64, Simulator | +| macOS | ✅ | ARM64, x86_64 | +| Windows | ✅ | x64, ARM64 | +| Linux | ✅ | x64, ARM64, ARM, x86 | +| Web | ❌ | FFI is not available for Web | + +## Usage + +This package is primarily intended to be used as a dependency by higher-level packages like [pdfrx](https://pub.dev/packages/pdfrx). Direct usage is possible but not recommended unless you need low-level PDFium access. + +```dart +import 'package:pdfium_flutter/pdfium_flutter.dart'; + +// Get PDFium bindings +final pdfium = pdfiumBindings; + +// Or load with custom path +final customPdfium = loadPdfium(modulePath: '/custom/path/to/pdfium.so'); +``` + +## Native Libraries + +### Android/Windows/Linux + +PDFium binaries are downloaded during build from [bblanchon/pdfium-binaries](https://github.com/bblanchon/pdfium-binaries/releases). + +### iOS/macOS + +PDFium XCFramework is downloaded using CocoaPods/SwiftPM install from [espresso3389/pdfium-xcframework](https://github.com/espresso3389/pdfium-xcframework/releases). diff --git a/android/.gitignore b/packages/pdfium_flutter/android/.gitignore similarity index 100% rename from android/.gitignore rename to packages/pdfium_flutter/android/.gitignore diff --git a/android/CMakeLists.txt b/packages/pdfium_flutter/android/CMakeLists.txt similarity index 76% rename from android/CMakeLists.txt rename to packages/pdfium_flutter/android/CMakeLists.txt index b616f573..6a17fa8f 100644 --- a/android/CMakeLists.txt +++ b/packages/pdfium_flutter/android/CMakeLists.txt @@ -1,18 +1,14 @@ cmake_minimum_required(VERSION 3.18.1) # Project-level configuration. -set(PROJECT_NAME "pdfrx") +set(PROJECT_NAME "pdfium_flutter") project(${PROJECT_NAME} LANGUAGES CXX) -# Invoke the build for native code shared with the other target platforms. -# This can be changed to accommodate different builds. -add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../src" "${CMAKE_CURRENT_BINARY_DIR}/shared") - # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. cmake_policy(VERSION 3.14...3.25) -set(PDFIUM_RELEASE chromium%2F6555) +set(PDFIUM_RELEASE chromium%2F7520) set(PDFIUM_DIR ${CMAKE_BINARY_DIR}/pdfium) set(PDFIUM_RELEASE_DIR ${PDFIUM_DIR}/${PDFIUM_RELEASE}) @@ -36,7 +32,7 @@ set(PDFIUM_DEST_LIB_FILENAME ${PDFIUM_LIBS_ARCH_DIR}/${PDFIUM_LIB_FILENAME}) set(PDFIUM_LATEST_DIR ${CMAKE_SOURCE_DIR}/.lib/latest) set(PDFIUM_LATEST_LIBS_ARCH_DIR ${PDFIUM_LATEST_DIR}/${ANDROID_ABI}) -set(PDFIUM_LASTEST_LIB_FILENAME ${PDFIUM_LATEST_LIBS_ARCH_DIR}/${PDFIUM_LIB_FILENAME}) +set(PDFIUM_LATEST_LIB_FILENAME ${PDFIUM_LATEST_LIBS_ARCH_DIR}/${PDFIUM_LIB_FILENAME}) if(NOT EXISTS ${PDFIUM_SRC_LIB_FILENAME}) message(STATUS "Download precompiled PDFium...") @@ -67,13 +63,7 @@ endif() file(REMOVE ${PDFIUM_LATEST_DIR}) file(CREATE_LINK ${PDFIUM_LIBS_DIR} ${PDFIUM_LATEST_DIR} SYMBOLIC) -set(pdfrx_bundled_libraries - # Defined in ../src/CMakeLists.txt. - # This can be changed to accommodate different builds. - $ - ${PDFIUM_LASTEST_LIB_FILENAME} +set(pdfium_flutter_bundled_libraries + ${PDFIUM_LATEST_LIB_FILENAME} PARENT_SCOPE ) - -target_include_directories(pdfrx PRIVATE ${PDFIUM_LATEST_DIR}/include) -target_link_libraries(pdfrx PRIVATE ${PDFIUM_LASTEST_LIB_FILENAME}) diff --git a/android/build.gradle b/packages/pdfium_flutter/android/build.gradle similarity index 79% rename from android/build.gradle rename to packages/pdfium_flutter/android/build.gradle index 2af45c04..61d21411 100644 --- a/android/build.gradle +++ b/packages/pdfium_flutter/android/build.gradle @@ -1,4 +1,4 @@ -group = "jp.espresso3389.pdfrx" +group = "jp.espresso3389.pdfium_flutter" version = "1.0-SNAPSHOT" buildscript { @@ -23,12 +23,15 @@ apply plugin: "com.android.library" android { if (project.android.hasProperty("namespace")) { - namespace = "jp.espresso3389.pdfrx" + namespace = "jp.espresso3389.pdfium_flutter" } - compileSdk = 34 + compileSdk = 35 - ndkVersion = android.ndkVersion + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } // Invoke the shared CMake build with the Android Gradle Plugin. externalNativeBuild { @@ -42,11 +45,6 @@ android { main.jniLibs.srcDirs += ".lib/latest" } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - defaultConfig { minSdk = 21 } diff --git a/packages/pdfium_flutter/android/settings.gradle b/packages/pdfium_flutter/android/settings.gradle new file mode 100644 index 00000000..3681ab45 --- /dev/null +++ b/packages/pdfium_flutter/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'pdfium_flutter' diff --git a/android/src/main/AndroidManifest.xml b/packages/pdfium_flutter/android/src/main/AndroidManifest.xml similarity index 64% rename from android/src/main/AndroidManifest.xml rename to packages/pdfium_flutter/android/src/main/AndroidManifest.xml index ec2aabfc..0469cbb3 100644 --- a/android/src/main/AndroidManifest.xml +++ b/packages/pdfium_flutter/android/src/main/AndroidManifest.xml @@ -1,3 +1,3 @@ + package="jp.espresso3389.pdfium_flutter"> diff --git a/packages/pdfium_flutter/darwin/pdfium_flutter.podspec b/packages/pdfium_flutter/darwin/pdfium_flutter.podspec new file mode 100644 index 00000000..00f4c02e --- /dev/null +++ b/packages/pdfium_flutter/darwin/pdfium_flutter.podspec @@ -0,0 +1,84 @@ +# PDFium xcframework configuration +# https://github.com/espresso3389/pdfium-xcframework/releases +PDFIUM_URL = "https://github.com/espresso3389/pdfium-xcframework/releases/download/v144.0.7520.0-20251111-190355/PDFium-chromium-7520-20251111-190355.xcframework.zip" +PDFIUM_HASH = "bd2a9542f13c78b06698c7907936091ceee2713285234cbda2e16bc03c64810b" + +Pod::Spec.new do |s| + s.name = 'pdfium_flutter' + s.version = '0.1.5' + s.summary = 'Flutter FFI plugin for loading PDFium native libraries.' + s.description = <<-DESC + Flutter FFI plugin for loading PDFium native libraries. Bundles PDFium binaries for iOS and macOS. + DESC + s.homepage = 'https://github.com/espresso3389/pdfrx' + s.license = { :type => 'MIT', :file => '../LICENSE' } + s.author = { 'Takashi Kawasaki' => 'espresso3389@gmail.com' } + s.source = { :path => '.' } + s.source_files = 'pdfium_flutter/Sources/**/*.swift' + s.preserve_paths = 'PDFium.xcframework/**/*' + + s.ios.deployment_target = '12.0' + s.ios.dependency 'Flutter' + s.ios.vendored_frameworks = 'PDFium.xcframework' + # Flutter.framework does not contain a i386 slice. + s.ios.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', + } + + s.osx.deployment_target = '10.13' + s.osx.dependency 'FlutterMacOS' + s.osx.vendored_frameworks = 'PDFium.xcframework' + s.osx.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + + s.swift_version = '5.0' + + s.prepare_command = <<-CMD + HASH_FILE=".pdfium_hash" + EXPECTED_HASH="#{PDFIUM_HASH}" + + # Check if we need to download/update + NEEDS_DOWNLOAD=false + if [ ! -d "PDFium.xcframework" ]; then + echo "PDFium xcframework not found" + NEEDS_DOWNLOAD=true + elif [ ! -f "$HASH_FILE" ]; then + echo "Hash file not found, will re-download" + NEEDS_DOWNLOAD=true + elif [ "$(cat $HASH_FILE)" != "$EXPECTED_HASH" ]; then + echo "PDFium version mismatch, will update" + NEEDS_DOWNLOAD=true + fi + + if [ "$NEEDS_DOWNLOAD" = true ]; then + # Clean up old version if exists + rm -rf PDFium.xcframework + rm -f "$HASH_FILE" + + echo "Downloading PDFium xcframework..." + curl -L -o pdfium.zip "#{PDFIUM_URL}" + + echo "Verifying ZIP file hash..." + ACTUAL_HASH=$(shasum -a 256 pdfium.zip | awk '{print $1}') + + if [ "$ACTUAL_HASH" != "$EXPECTED_HASH" ]; then + echo "Error: Hash mismatch!" + echo "Expected: $EXPECTED_HASH" + echo "Actual: $ACTUAL_HASH" + rm pdfium.zip + exit 1 + fi + echo "Hash verification successful" + + unzip -q pdfium.zip + rm pdfium.zip + + # Store hash for future version checks + echo "$EXPECTED_HASH" > "$HASH_FILE" + + echo "PDFium xcframework downloaded successfully" + else + echo "PDFium xcframework is up to date" + fi + CMD +end diff --git a/packages/pdfium_flutter/darwin/pdfium_flutter/Package.swift b/packages/pdfium_flutter/darwin/pdfium_flutter/Package.swift new file mode 100644 index 00000000..dc59a5e2 --- /dev/null +++ b/packages/pdfium_flutter/darwin/pdfium_flutter/Package.swift @@ -0,0 +1,30 @@ +// swift-tools-version:5.6 +import PackageDescription + +let package = Package( + name: "pdfium_flutter", + platforms: [ + .iOS(.v12), + .macOS(.v10_13), + ], + products: [ + .library( + name: "pdfium-flutter", + targets: ["pdfium_flutter"] + ), + ], + targets: [ + .target( + name: "pdfium_flutter", + dependencies: [ + .target(name: "PDFium"), + ], + path: "Sources/main" + ), + .binaryTarget( + name: "PDFium", + url: "https://github.com/espresso3389/pdfium-xcframework/releases/download/v144.0.7520.0-20251111-190355/PDFium-chromium-7520-20251111-190355.xcframework.zip", + checksum: "bd2a9542f13c78b06698c7907936091ceee2713285234cbda2e16bc03c64810b" + ), + ] +) diff --git a/packages/pdfium_flutter/darwin/pdfium_flutter/Sources/main/pdfium_flutter.swift b/packages/pdfium_flutter/darwin/pdfium_flutter/Sources/main/pdfium_flutter.swift new file mode 100644 index 00000000..25ec41ec --- /dev/null +++ b/packages/pdfium_flutter/darwin/pdfium_flutter/Sources/main/pdfium_flutter.swift @@ -0,0 +1,16 @@ +#if os(iOS) + import Flutter +#elseif os(macOS) + import FlutterMacOS +#endif + +class PDFiumFlutterPlugin: NSObject, FlutterPlugin { + static func register(with _: FlutterPluginRegistrar) { + // This is an FFI plugin - no platform channel needed + // The native code is accessed via FFI from pdfium_flutter + } + + func dummyMethodToPreventStripping() { + // This method prevents the linker from stripping the PDFium framework + } +} diff --git a/packages/pdfium_flutter/lib/pdfium_flutter.dart b/packages/pdfium_flutter/lib/pdfium_flutter.dart new file mode 100644 index 00000000..2556e381 --- /dev/null +++ b/packages/pdfium_flutter/lib/pdfium_flutter.dart @@ -0,0 +1,8 @@ +/// Flutter FFI plugin for loading PDFium native libraries. +/// +/// This package bundles PDFium binaries for Android, iOS, Windows, macOS, and Linux +/// and provides utilities for loading them at runtime. +library pdfium_flutter; + +export 'package:pdfium_dart/pdfium_dart.dart'; +export 'src/pdfium_loader.dart'; diff --git a/packages/pdfium_flutter/lib/src/pdfium_loader.dart b/packages/pdfium_flutter/lib/src/pdfium_loader.dart new file mode 100644 index 00000000..79baf593 --- /dev/null +++ b/packages/pdfium_flutter/lib/src/pdfium_loader.dart @@ -0,0 +1,79 @@ +import 'dart:ffi' as ffi; +import 'dart:ffi'; +import 'dart:io'; + +import 'package:pdfium_dart/pdfium_dart.dart'; + +/// Get the module file name for pdfium. +String _getModuleFileName() { + if (Platform.isAndroid) return 'libpdfium.so'; + if (Platform.isWindows) return 'pdfium.dll'; + if (Platform.isLinux) { + return '${File(Platform.resolvedExecutable).parent.path}/lib/libpdfium.so'; + } + throw UnsupportedError('Unsupported platform'); +} + +DynamicLibrary _getModule({String? explicitPath}) { + // If the module path is explicitly specified, use it. + if (explicitPath != null) { + return DynamicLibrary.open(explicitPath); + } + // For iOS/macOS, we assume pdfium is already loaded (or statically linked) in the process. + if (Platform.isIOS || Platform.isMacOS) { + return DynamicLibrary.process(); + } + return DynamicLibrary.open(_getModuleFileName()); +} + +PDFium? _pdfium; + +/// Loaded PDFium module. +/// +/// This getter lazily loads the PDFium library and returns the bindings. +PDFium get pdfiumBindings { + _pdfium ??= PDFium(_getModule()); + return _pdfium!; +} + +/// Set custom PDFium bindings (for testing or custom library paths). +set pdfiumBindings(PDFium value) { + _pdfium = value; +} + +/// Load PDFium with an explicit module path. +/// +/// This is useful for custom deployment scenarios or testing. +PDFium loadPdfium({String? modulePath}) { + final bindings = PDFium(_getModule(explicitPath: modulePath)); + _pdfium = bindings; + return bindings; +} + +/// Reset the PDFium bindings (useful for testing). +void resetPdfiumBindings() { + _pdfium = null; +} + +typedef PdfiumNativeFunctionLookup = + ffi.Pointer Function(String symbolName); + +/// Create a native function lookup for PDFium symbols. +/// +/// This is used for custom native bindings or mock implementations. +PdfiumNativeFunctionLookup? createPdfiumNativeFunctionLookup< + T extends ffi.NativeType +>(Map? nativeBindings) { + if (nativeBindings != null) { + ffi.Pointer lookup(String symbolName) { + final ptr = nativeBindings[symbolName]; + if (ptr == null) { + throw Exception('Failed to find binding for $symbolName'); + } + return ffi.Pointer.fromAddress(ptr); + } + + return lookup; + } + return null; +} diff --git a/linux/CMakeLists.txt b/packages/pdfium_flutter/linux/CMakeLists.txt similarity index 85% rename from linux/CMakeLists.txt rename to packages/pdfium_flutter/linux/CMakeLists.txt index 88d63b3d..e5ef65f8 100644 --- a/linux/CMakeLists.txt +++ b/packages/pdfium_flutter/linux/CMakeLists.txt @@ -4,7 +4,7 @@ cmake_minimum_required(VERSION 3.10) # Project-level configuration. -set(PROJECT_NAME "pdfrx") +set(PROJECT_NAME "pdfium_flutter") project(${PROJECT_NAME} LANGUAGES CXX) # Determine target processor name @@ -21,15 +21,11 @@ ELSE() ENDIF() message( STATUS "Target CPU Name: ${CPU_NAME}" ) -# Invoke the build for native code shared with the other target platforms. -# This can be changed to accommodate different builds. -add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../src" "${CMAKE_CURRENT_BINARY_DIR}/shared") - # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. cmake_policy(VERSION 3.14...3.25) -set(PDFIUM_RELEASE chromium%2F6555) +set(PDFIUM_RELEASE chromium%2F7520) set(PDFIUM_DIR ${CMAKE_BINARY_DIR}/pdfium) set(PDFIUM_RELEASE_DIR ${PDFIUM_DIR}/${PDFIUM_RELEASE}) @@ -84,13 +80,7 @@ file(CREATE_LINK ${PDFIUM_LIBS_DIR} ${PDFIUM_LATEST_DIR} SYMBOLIC) # List of absolute paths to libraries that should be bundled with the plugin. # This list could contain prebuilt libraries, or libraries created by an # external build triggered from this build file. -set(pdfrx_bundled_libraries - # Defined in ../src/CMakeLists.txt. - # This can be changed to accommodate different builds. - $ +set(pdfium_flutter_bundled_libraries ${PDFIUM_LATEST_LIB_FILENAME} PARENT_SCOPE ) - -target_include_directories(pdfrx INTERFACE PRIVATE ${PDFIUM_LATEST_DIR}/include) -target_link_libraries(pdfrx PRIVATE ${PDFIUM_LATEST_LIB_FILENAME}) diff --git a/packages/pdfium_flutter/pubspec.yaml b/packages/pdfium_flutter/pubspec.yaml new file mode 100644 index 00000000..80edad41 --- /dev/null +++ b/packages/pdfium_flutter/pubspec.yaml @@ -0,0 +1,39 @@ +name: pdfium_flutter +description: Flutter FFI plugin for loading PDFium native libraries. Bundles PDFium binaries for Android, iOS, Windows, macOS, and Linux. +version: 0.1.9 +homepage: https://github.com/espresso3389/pdfrx +repository: https://github.com/espresso3389/pdfrx/tree/master/packages/pdfium_flutter +issue_tracker: https://github.com/espresso3389/pdfrx/issues + +environment: + sdk: '>=3.9.0 <4.0.0' + flutter: '>=3.35.1' +resolution: workspace + +dependencies: + pdfium_dart: ^0.1.2 + ffi: ^2.1.4 + flutter: + sdk: flutter + path: ^1.9.1 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: + +flutter: + plugin: + platforms: + android: + ffiPlugin: true + ios: + ffiPlugin: true + sharedDarwinSource: true + linux: + ffiPlugin: true + macos: + ffiPlugin: true + sharedDarwinSource: true + windows: + ffiPlugin: true diff --git a/windows/.gitignore b/packages/pdfium_flutter/windows/.gitignore similarity index 100% rename from windows/.gitignore rename to packages/pdfium_flutter/windows/.gitignore diff --git a/windows/CMakeLists.txt b/packages/pdfium_flutter/windows/CMakeLists.txt similarity index 57% rename from windows/CMakeLists.txt rename to packages/pdfium_flutter/windows/CMakeLists.txt index 4c99c4bf..beade1ef 100644 --- a/windows/CMakeLists.txt +++ b/packages/pdfium_flutter/windows/CMakeLists.txt @@ -5,27 +5,33 @@ cmake_minimum_required(VERSION 3.14) # Project-level configuration. -set(PROJECT_NAME "pdfrx") +set(PROJECT_NAME "pdfium_flutter") project(${PROJECT_NAME} LANGUAGES CXX) -# Invoke the build for native code shared with the other target platforms. -# This can be changed to accommodate different builds. -add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../src" "${CMAKE_CURRENT_BINARY_DIR}/shared") - # Explicitly opt in to modern CMake behaviors to avoid warnings with recent # versions of CMake. cmake_policy(VERSION 3.14...3.25) -set(PDFIUM_RELEASE chromium%2F6555) +set(PDFIUM_RELEASE chromium%2F7520) set(PDFIUM_DIR ${CMAKE_BINARY_DIR}/pdfium) set(PDFIUM_RELEASE_DIR ${PDFIUM_DIR}/${PDFIUM_RELEASE}) file(MAKE_DIRECTORY ${PDFIUM_RELEASE_DIR}) +# Determine target processor name for Windows +IF(CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64") + SET(CPU_NAME "x64") +ELSEIF(CMAKE_SYSTEM_PROCESSOR STREQUAL "ARM64") + SET(CPU_NAME "arm64") +ELSE() + MESSAGE(FATAL_ERROR "Unsupported Windows architecture \"${CMAKE_SYSTEM_PROCESSOR}\". Only AMD64 and ARM64 are supported.") +ENDIF() +message( STATUS "Target Windows CPU Name: ${CPU_NAME}" ) + set(PDFIUM_PLATFORM "win") set(PDFIUM_LIB_FILENAME "pdfium.dll") set(PDFIUM_LIB_DIR "bin") -set(PDFIUM_WIN_ABI "x64") +set(PDFIUM_WIN_ABI ${CPU_NAME}) set(PDFIUM_ARCHIVE_NAME pdfium-${PDFIUM_PLATFORM}-${PDFIUM_WIN_ABI}) set(PDFIUM_SRC_LIB_FILENAME ${PDFIUM_RELEASE_DIR}/${PDFIUM_LIB_DIR}/${PDFIUM_LIB_FILENAME}) @@ -65,17 +71,44 @@ if (NOT EXISTS ${PDFIUM_LIBS_DIR}/include) endif() file(REMOVE ${PDFIUM_LATEST_DIR}) -file(CREATE_LINK ${PDFIUM_LIBS_DIR} ${PDFIUM_LATEST_DIR} SYMBOLIC) + +# Check if we can create symbolic links (requires Developer Mode on Windows) +execute_process( + COMMAND ${CMAKE_COMMAND} -E create_symlink ${PDFIUM_LIBS_DIR} ${PDFIUM_LATEST_DIR}_test + RESULT_VARIABLE SYMLINK_RESULT + ERROR_QUIET + OUTPUT_QUIET +) + +if(EXISTS ${PDFIUM_LATEST_DIR}_test) + file(REMOVE ${PDFIUM_LATEST_DIR}_test) +endif() + +if(SYMLINK_RESULT EQUAL 0) + # Developer Mode is enabled, use symbolic link + file(CREATE_LINK ${PDFIUM_LIBS_DIR} ${PDFIUM_LATEST_DIR} SYMBOLIC) +else() + # Developer Mode is not enabled, fail the build + message(FATAL_ERROR "\n" + "========================================================================\n" + " ERROR: Windows Developer Mode is not enabled!\n" + "------------------------------------------------------------------------\n" + " The pdfium_flutter build process requires Developer Mode to create symbolic\n" + " links. The build cannot continue without Developer Mode enabled.\n" + "\n" + " Please enable Developer Mode by following the instructions at:\n" + " https://learn.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development#activate-developer-mode\n" + "\n" + " After enabling Developer Mode, restart your computer and run the\n" + " build again.\n" + "========================================================================\n" + ) +endif() # List of absolute paths to libraries that should be bundled with the plugin. # This list could contain prebuilt libraries, or libraries created by an # external build triggered from this build file. -set(pdfrx_bundled_libraries - # Defined in ../src/CMakeLists.txt. - # This can be changed to accommodate different builds. - $ +set(pdfium_flutter_bundled_libraries ${PDFIUM_LATEST_LIB_FILENAME} PARENT_SCOPE ) - -target_include_directories(pdfrx PRIVATE ${PDFIUM_LATEST_DIR}/include) diff --git a/packages/pdfrx/.gitignore b/packages/pdfrx/.gitignore new file mode 100644 index 00000000..8f44ddfb --- /dev/null +++ b/packages/pdfrx/.gitignore @@ -0,0 +1,50 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# C++ +.cxx + +# For pdfium builds on macOS +.build/ +.swiftpm/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock +pubspec_overrides.yaml + +**/doc/api/ +.dart_tool/ +build/ + +/test/.tmp + +.claude/ + +# Build outputs and intermediate files +**.xcframework/ +*.zip + +doc/api/ + +.pdfium_hash diff --git a/.metadata b/packages/pdfrx/.metadata similarity index 100% rename from .metadata rename to packages/pdfrx/.metadata diff --git a/packages/pdfrx/.prettierrc b/packages/pdfrx/.prettierrc new file mode 100644 index 00000000..eb33b1aa --- /dev/null +++ b/packages/pdfrx/.prettierrc @@ -0,0 +1,8 @@ +{ + "printWidth": 120, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": true, + "trailingComma": "es5" +} \ No newline at end of file diff --git a/packages/pdfrx/.pubignore b/packages/pdfrx/.pubignore new file mode 100644 index 00000000..b51bf653 --- /dev/null +++ b/packages/pdfrx/.pubignore @@ -0,0 +1,2 @@ +example/viewer/ +build/ diff --git a/packages/pdfrx/CHANGELOG.md b/packages/pdfrx/CHANGELOG.md new file mode 100644 index 00000000..943c56a4 --- /dev/null +++ b/packages/pdfrx/CHANGELOG.md @@ -0,0 +1,1173 @@ +# 2.2.24 + +- Updated to pdfrx_engine 0.3.9 +- NEW: `PdfrxEntryFunctions.stopBackgroundWorker()` to stop the background worker thread ([#184](https://github.com/espresso3389/pdfrx/issues/184), [#430](https://github.com/espresso3389/pdfrx/issues/430)) + +# 2.2.23 + +- pdfrx_engine 0.3.8 +- NEW: `onDocumentLoadFinished` callback in `PdfViewerParams` to notify when document loading completes (or fails) +- Implemented `PdfDocumentLoadCompleteEvent` for WASM backend + +# 2.2.20 + +- FIXED: Inconsistent environment constraints - Flutter version now correctly requires 3.35.1+ to match Dart 3.9.0 requirement ([#553](https://github.com/espresso3389/pdfrx/issues/553)) + +# 2.2.19 + +- Updated to pdfrx_engine 0.3.7 and pdfium_flutter 0.1.8 +- IMPROVED: Add `isDirty` flag to page image cache to prevent cache removal before re-rendering page ([#567](https://github.com/espresso3389/pdfrx/issues/567)) +- FIXED: `round10BitFrac` should not process `Infinity` or `NaN` ([#550](https://github.com/espresso3389/pdfrx/issues/550)) +- Updated Gradle wrapper to version 8.12 and Android plugin to version 8.9.1 +- WIP: Adding `PdfDocument.useNativeDocumentHandle`/`reloadPages` + +# 2.2.18 + +- FIXED: Dependency conflicts with `dart_pubspec_licenses` causing version resolution failures ([#563](https://github.com/espresso3389/pdfrx/issues/563), [#570](https://github.com/espresso3389/pdfrx/issues/570)) + - Removed `dart_pubspec_licenses` dependency and reimplemented package path resolution internally + - This resolves conflicts with `pana`, `meta`, `lints`, and `analyzer` package versions + +# 2.2.17 + +- Updated to pdfrx_engine 0.3.6 +- FIXED: Trackpad and mouse wheel boundary issues on Web ([#547](https://github.com/espresso3389/pdfrx/issues/547), [#548](https://github.com/espresso3389/pdfrx/pull/548)) +- FIXED: `_setZoom` now properly sets `boundaryMargins` +- NEW: Ctrl+wheel zoom logic can be enabled on Web ([#538](https://github.com/espresso3389/pdfrx/issues/538)) +- NEW: [`PdfDateTime`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDateTime.html) extension type for PDF date string parsing +- NEW: [`PdfAnnotation`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfAnnotation-class.html) class for PDF annotation metadata extraction ([#546](https://github.com/espresso3389/pdfrx/pull/546)) + +# 2.2.16 + +- Updated to pdfrx_engine 0.3.4 +- NEW: Progressive loading helper functions - [`ensureLoaded()`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPageBaseExtensions/ensureLoaded.html) and [`waitForLoaded()`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPageBaseExtensions/waitForLoaded.html) +- NEW: [`PdfPageStatusChange.page`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPageStatusChange/page.html) property for easier access to updated page instances +- NEW: Added comprehensive [Progressive Loading documentation](https://github.com/espresso3389/pdfrx/blob/master/doc/Progressive-Loading.md) + +# 2.2.15 + +- FIXED: Focus context retrieval issue in [PdfViewerKeyHandler](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerKeyHandler-class.html) ([#518](https://github.com/espresso3389/pdfrx/issues/518)) + +# 2.2.14 + +- Minor changes. + +# 2.2.13 + +- FIXED: [PdfTextSearcher](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfTextSearcher-class.html) not in sync on [PdfDocument](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocument-class.html) update ([#515](https://github.com/espresso3389/pdfrx/issues/515)) +- FIXED: Focus.of -> Focus.maybeOf to prevent exceptions when FocusNode is not available ([#518](https://github.com/espresso3389/pdfrx/issues/518)) +- FIXED: Crash when opening an empty PDF - now treated as a valid PDF to keep consistency with existing editing feature ([#544](https://github.com/espresso3389/pdfrx/issues/544)) +- FIXED: [PdfViewerParams.onGeneralTap](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/onGeneralTap.html) one-click not working ([#540](https://github.com/espresso3389/pdfrx/issues/540)) +- IMPROVED: Document reference comparison now uses key for better consistency ([#543](https://github.com/espresso3389/pdfrx/pull/543)) + +# 2.2.12 + +- FIXED: Package incorrectly showing as web-only on pub.dev due to incorrect Flutter plugin platform declarations ([#535](https://github.com/espresso3389/pdfrx/issues/535)) + +# 2.2.11 + +- FIXED: Magnifier content location calculated incorrectly ([#532](https://github.com/espresso3389/pdfrx/issues/532)) +- NEW: Added `PdfViewerController.localToDocument()` and `PdfViewerController.documentToLocal()` methods for coordinate conversion +- Updated to pdfrx_engine 0.3.3 + +# 2.2.10 + +- Updated to pdfrx_engine 0.3.1 and pdfium_flutter 0.1.1 + +# 2.2.9 + +- Updated to pdfrx_engine 0.3.0 + +# 2.2.8 + +- NEW: `PdfDocument` now supports page re-arrangement and accepts `PdfPage` instances from other documents, enabling PDF combine/merge functionality +- Added `pdf_combine` app example demonstrating PDF merging capabilities +- Updated to pdfrx_engine 0.2.4 + +# 2.2.7 + +- `PdfViewer.uri` now supports timeout parameter ([#508](https://github.com/espresso3389/pdfrx/issues/508)) + +# 2.2.6 + +- Added configurable timeout parameter to `openUri` and `pdfDocumentFromUri` functions ([#509](https://github.com/espresso3389/pdfrx/pull/509)) +- Updated to pdfrx_engine 0.2.3 + +# 2.2.5 + +- Experimental iOS/macOS direct symbol lookup to address SwiftPM TestFlight/App Store symbol lookup issues ([#501](https://github.com/espresso3389/pdfrx/issues/501)) +- `pdfrxFlutterInitialize()` now internally calls `WidgetsFlutterBinding.ensureInitialized()` - no need to call it explicitly +- Updated to pdfrx_engine 0.2.2 + +# 2.2.4 + +- FIXED: SwiftPM/pod package structure updates for iOS/macOS ([#501](https://github.com/espresso3389/pdfrx/issues/501)) +- Updated to pdfrx_engine 0.2.1 + +# 2.2.3 + +- POSSIBLE FIX: Error on `openFile()` or `openAsset()` on iOS production builds installed from AppStore/TestFlight ([#501](https://github.com/espresso3389/pdfrx/issues/501)) + +# 2.2.2 + +- FIXED: InteractiveViewer ScrollPhysics pinch-zoom centering issues ([#502](https://github.com/espresso3389/pdfrx/issues/502)) + +# 2.2.1 + +- FIXED: PDF not visible initially if `_alternativeFitScale` is null ([#495](https://github.com/espresso3389/pdfrx/issues/495)) + +# 2.2.0 + +- **BREAKING**: Renamed `PdfrxEntryFunctions.initPdfium()` to `PdfrxEntryFunctions.init()` for consistency +- NEW: Added `dart run pdfrx:remove_darwin_pdfium_modules` command to remove PDFium dependencies from iOS/macOS when using alternative backends like pdfrx_coregraphics +- Updated to pdfrx_engine 0.2.0 + +# 2.1.26 + +- FIXED: PDF not visible initially when loading takes relatively long ([#495](https://github.com/espresso3389/pdfrx/issues/495)) + +# 2.1.25 + +- FIXED: Added ArrayBuffer fallback when `WebAssembly.instantiateStreaming` fails (e.g. missing `application/wasm` MIME type) ([#405](https://github.com/espresso3389/pdfrx/issues/405), [#493](https://github.com/espresso3389/pdfrx/issues/493)) + +# 2.1.24 + +- FIXED: Strange zooming out behavior ([#490](https://github.com/espresso3389/pdfrx/issues/490)) + +# 2.1.23 + +- FIXED: WASM+Safari StringBuffer issue with workaround ([#483](https://github.com/espresso3389/pdfrx/issues/483)) +- Introduces `PdfDocumentRefKey` for more flexible `PdfDocumentRef` identification +- Updated to pdfrx_engine 0.1.21 + +# 2.1.22 + +- NEW: Introducing [PdfViewerController.zoomOnLocalPosition](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerController/zoomOnLocalPosition.html) and its variants for consistent zooming on cursor/finger position ([#486](https://github.com/espresso3389/pdfrx/issues/486), [#462](https://github.com/espresso3389/pdfrx/issues/462)) +- PdfViewer now handles Ctrl+wheel to zoom up/down ([#486](https://github.com/espresso3389/pdfrx/issues/486)) + +# 2.1.21 + +- FIXED: Web compatibility issue where `dart:io` was imported in public-facing code + +# 2.1.20 + +- Added [PdfViewerParams.scrollPhysics](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/scrollPhysics.html) and [PdfViewerParams.scrollPhysicsScale](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/scrollPhysicsScale.html) so you can plug in custom [ScrollPhysics](https://api.flutter.dev/flutter/widgets/ScrollPhysics-class.html) for both panning and pinch-zoom interactions + - PR [#481](https://github.com/espresso3389/pdfrx/issues/481), [#482](https://github.com/espresso3389/pdfrx/issues/482), [#484](https://github.com/espresso3389/pdfrx/issues/484), [#485](https://github.com/espresso3389/pdfrx/issues/485) by [enhancient](https://github.com/enhancient) +- FIXED: regression where `dart run pdfrx:remove_wasm_modules` failed with dart_pubspec_licenses 3.0.12 ([#443](https://github.com/espresso3389/pdfrx/issues/443)). + +# 2.1.19 + +- Maintenance release: applied `dart format` to keep code integrity with Dart 3.9/Flutter 3.29 tooling. + +# 2.1.18 + +- Remove broken docImport not to crash dartdoc ([dart-lang/dartdoc#4106](https://github.com/dart-lang/dartdoc/issues/4106)) + +# 2.1.17 + +- FIXED: `dart run pdfrx:remove_wasm_modules` could fail with "Too many open files" during WASM cleanup ([#476](https://github.com/espresso3389/pdfrx/issues/476)) +- Updated dependencies, including pdfrx_engine 0.1.18 + +# 2.1.16 + +- [#474](https://github.com/espresso3389/pdfrx/issues/474) Add [PdfrxEntryFunctions.initPdfium](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfrxEntryFunctions/initPdfium.html) to explicitly call FPDF_InitLibraryWithConfig and [pdfrxInitialize](https://pub.dev/documentation/pdfrx/latest/pdfrx/pdfrxInitialize.html)/[pdfrxFlutterInitialize](https://pub.dev/documentation/pdfrx/latest/pdfrx/pdfrxFlutterInitialize.html) internally call it +- [#474](https://github.com/espresso3389/pdfrx/issues/474) Add [PdfrxEntryFunctions.suspendPdfiumWorkerDuringAction](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfrxEntryFunctions/suspendPdfiumWorkerDuringAction.html) +- Documentation improvements for low-level PDFium bindings access/PDFium interoperability and initialization +- Updated to pdfrx_engine 0.1.17 + +# 2.1.15 + +- FIXED: Package.swift does not point to pdfium-apple-v11 dependencies correctly +- More error handling logic for improved stability ([#468](https://github.com/espresso3389/pdfrx/issues/468)) +- Updated to pdfrx_engine 0.1.16 + +# 2.1.14 + +- Enhance podspec script to check for existing PDFium frameworks before downloading ([#396](https://github.com/espresso3389/pdfrx/issues/396), [#460](https://github.com/espresso3389/pdfrx/issues/460)) + - Improves build performance by avoiding redundant downloads of PDFium frameworks on iOS/macOS + +# 2.1.13 + +- FIXED: [#443](https://github.com/espresso3389/pdfrx/issues/443) `dart run pdfrx:remove_wasm_modules` failure + +# 2.1.12 + +- **BREAKING**: API changes in text loading methods - `loadText()` and `loadTextCharRects()` are now integrated into `loadText()` ([#434](https://github.com/espresso3389/pdfrx/issues/434)) +- FIXED: PDF link positioning errors that caused misplaced clickable areas ([#458](https://github.com/espresso3389/pdfrx/issues/458)) +- Updated to pdfrx_engine 0.1.15 + +# 2.1.11 + +- NEW: Experimental support for dynamic font installation on native platforms to handle missing fonts in PDFs ([#456](https://github.com/espresso3389/pdfrx/issues/456)) + - Example viewer has missing font support using Noto Sans/Serif on Google Fonts (**For production use, please take care of your license integrity**) +- iOS/macOS uses pdfium-apple-v11 (chromium/7390) +- Updated to pdfrx_engine 0.1.14 + +# 2.1.10 + +- NEW: Add `dismissPdfiumWasmWarnings` to `pdfrxFlutterInitialize` to optionally hide WASM warnings in debug ([#452](https://github.com/espresso3389/pdfrx/issues/452)) +- FIXED: Remove use of `.orCancel` on `animateTo` to prevent animation cancellation errors ([#454](https://github.com/espresso3389/pdfrx/issues/454)) + +# 2.1.9 + +- Replace deprecated Matrix4 methods (`translate` -> `translateByDouble`, `scaled` -> `scaledByDouble`) for improved numerical precision + +# 2.1.8 + +- FIXED: Unstoppable key-repeat on certain keys + +# 2.1.7 + +- Refactor InteractiveViewer to use Matrix4 double variants for better numerical precision + +# 2.1.6 + +- DOCS: Document initialization for Flutter (`pdfrxFlutterInitialize`) and link to non-Flutter initialization (`pdfrxInitialize`). Closes [#447](https://github.com/espresso3389/pdfrx/issues/447) +- No functional changes + +# 2.1.5 + +- FIXED: PdfDocumentViewBuilder was incorrectly reloading document on every widget change ([#439](https://github.com/espresso3389/pdfrx/issues/439)) + +# 2.1.4 + +- FIXED: Text coordinate calculation when CropBox/MediaBox has non-zero origin ([#441](https://github.com/espresso3389/pdfrx/issues/441)) +- Add support for dynamic font loading in WASM with `PdfDocumentMissingFontsEvent` +- Improve WASM stability and font handling mechanisms +- Update pdfrx_engine dependency to 0.1.13 + +# 2.1.3 + +- FIXED: UI distortion when selecting text ([#432](https://github.com/espresso3389/pdfrx/issues/432)) +- The list returned by `PdfPage.loadTextCharRects()` is now mutable for better flexibility +- Enhanced README documentation for text selection customization features +- Update pdfrx_engine dependency to 0.1.12 + +# 2.1.2 + +- FIXED: Prevent right-click context menu from showing on Flutter Web +- Update pdfrx_engine dependency to 0.1.11 + +# 2.1.1 + +- Update pdfrx_engine dependency to 0.1.10 for WASM compatibility improvements + +# 2.1.0 + +- BREAKING CHANGE: Text selection handles are now automatically shown/hidden based on the pointing device type used + - Touch input shows selection handles for precise control + - Non-touch input hides handles for cleaner interaction + - Removing some of text selection related parameters to simplify the API +- BREAKING CHANGE: Now context menu is not only for text selection but also for general tap events + - `PdfViewerParams.buildContextMenu` and `PdfViewerParams.customizeContextMenuItems` to customize context menu + - Introduces `PdfViewerContextMenuBuilderParams` (many context menu related parameters are moved to this class) +- BREAKING CHANGE: Tap handler functions are integrated into `PdfViewerParams.onGeneralTap` for better consistency + +# 2.0.4 + +- FIXED: GestureDetector for text selection now ignores touchpad events to prevent interference with touch-to-scroll ([#426](https://github.com/espresso3389/pdfrx/issues/426)) + +# 2.0.3 + +- IMPROVED: Enhanced text selection context menu API with better anchor positioning and adaptive toolbar support + +# 2.0.2 + +- BREAKING CHANGE: Renamed `PdfTextSelection.getSelectedTextRange()` to `getSelectedTextRanges()` for consistency +- NEW FEATURE: Added Shift+Space keyboard shortcut to navigate to previous page + +# 2.0.1 + +- FIXED: Added missing `PdfTextSelectionParams.enabled` property to control text selection functionality + +# 2.0.0 + +This is a major release that introduces significant architectural changes and new features. + +- BREAKING CHANGE: Extracted PDF rendering engine into a separate `pdfrx_engine` package that is platform-agnostic +- NEW FEATURE/BREAKING CHANGE: Text selection support with native platform UI including: + - Selection handles with drag support + - Magnifier for precise text selection + - Context menu with copy functionality + - Brand-new text selection/text extraction API (not compatible with previous versions) +- Enhanced focus management for better keyboard interaction support + +# 1.3.4 + +- FIXED: `PdfDocumentViewBuilder` did not properly handle progressive loading ([#419](https://github.com/espresso3389/pdfrx/pull/419)) + +# 1.3.3 + +- NEW FEATURE: Updated `bin/remove_wasm_modules.dart` to comment out assets line in pubspec.yaml instead of deleting files + - Added `--revert` option to restore commented assets line in `bin/remove_wasm_modules.dart` + +# 1.3.2 + +- NEW FEATURE: Added `useProgressiveLoading` parameter for all `PdfViewer` constructors to enable progressive page loading +- NEW FEATURE: Added `PdfDocument.changes` stream to notify page status changes and loading progress + +# 1.3.1 + +- NEW FEATURE: Added `PdfViewerParams.calculateInitialZoom` to customize initial zoom calculation ([#406](https://github.com/espresso3389/pdfrx/pull/406)) +- Removed deprecated `PdfrxWebRuntimeType` API + +# 1.3.0 + +- NEW FEATURE: Added support for disabling automatic web link detection from text content ([#403](https://github.com/espresso3389/pdfrx/pull/403)) + +# 1.2.9 + +- FIXED: Use document base href for resolving URLs in PDFium WASM ([#402](https://github.com/espresso3389/pdfrx/pull/402)) +- FIXED: PDFium WASM hot-restarting may call initializePdfium multiple times + +# 1.2.8 + +- FIXED: Ensure PDFium WASM is initialized before registering callbacks ([#399](https://github.com/espresso3389/pdfrx/issues/399)) + +# 1.2.7 + +- Enhanced PDFium initialization with optional authentication parameters for WASM + +# 1.2.6 + +- NEW FEATURE: Added `headers` and `withCredentials` support for PDFium WASM implementation ([#399](https://github.com/espresso3389/pdfrx/issues/399)) +- NEW FEATURE: Implemented progress callback support for PDFium WASM using a general callback mechanism +- Improved PDFium WASM worker-client communication architecture with `PdfiumWasmCommunicator` + +# 1.2.5 + +- FIXED: Flag handling in `PdfPagePdfium` for improved rendering ([#398](https://github.com/espresso3389/pdfrx/issues/398)) + +# 1.2.4 + +- FIXED: Reverted zoom ratio calculation change from 1.1.28 that was affecting pinch-to-zoom behavior ([#391](https://github.com/espresso3389/pdfrx/issues/391)) +- Added Windows Developer Mode requirement check in CMake build configuration + +# 1.2.3 + +- FIXED: Progressive loading support for PDF document references ([#397](https://github.com/espresso3389/pdfrx/issues/397)) +- Enhanced PDF document loading functions with better error handling +- Improved PDFium WASM worker implementation + +# 1.2.2 + +- FIXED: `_emscripten_throw_longjmp` error in PDFium WASM ([#354](https://github.com/espresso3389/pdfrx/issues/354)) +- Enhanced example viewer to support PDF file path/URL as a parameter +- Enabled build-test workflow on master commits and pull requests for better CI/CD + +# 1.2.1 + +- Temporarily disable Windows ARM64 architecture detection to maintain compatibility with Flutter stable ([#395](https://github.com/espresso3389/pdfrx/issues/395), [#388](https://github.com/espresso3389/pdfrx/issues/388)) + - Flutter stable doesn't support Windows ARM64 yet, so the build always targets x64 + +# 1.2.0 + +- BREAKING CHANGE: Removed PDF.js support - PDFium WASM is now the only web implementation +- BREAKING CHANGE: The separate `pdfrx_wasm` package is no longer needed; WASM assets are now included directly in the main `pdfrx` package +- NEW FEATURE: Implemented progressive/lazy page loading for PDFium WASM for better performance with large PDF files ([#319](https://github.com/espresso3389/pdfrx/issues/319)) +- Simplified web architecture by consolidating WASM assets into the main package + +# 1.1.35 + +- Add `limitRenderingCache` parameter to `PdfViewerParams` to control rendering cache behavior ([#394](https://github.com/espresso3389/pdfrx/pull/394)) +- Add rendering flags support to `PdfPage.render` method + +# 1.1.34 + +- Add `CLAUDE.md` for Claude Code integration +- FIXED: preserve null `max-age` in cache control ([#387](https://github.com/espresso3389/pdfrx/pull/387)) +- FIXED: `ArgumentError` parameter name in `PdfRect` ([#385](https://github.com/espresso3389/pdfrx/pull/385)) +- Windows ARM64 support ([#388](https://github.com/espresso3389/pdfrx/issues/388)) +- Documentation updates and improvements + +# 1.1.33 + +- Explicitly specify 16KB page size on Android rather than specifying specific NDK version + +# 1.1.32 + +- Minor fixes + +# 1.1.31 + +- SwiftPM support for iOS/macOS +- PDFium 138.0.7202.0 +- FIXED: null assertion exception when laying out view and `calculateCurrentPageNumber` is overridden ([#367](https://github.com/espresso3389/pdfrx/issues/367)) + +# 1.1.30 + +- MERGED: PR [#364](https://github.com/espresso3389/pdfrx/pull/364) fix: blank pdf on Windows when restore window from minimize +- Update example's `app/build.gradle` to support Android's 16KB page size + +# 1.1.29 + +- FIXED: [#363](https://github.com/espresso3389/pdfrx/issues/363) + - FIXED: `pdfium-wasm-module-url` on HTML meta tag overrides value explicitly set to `Pdfrx.pdfiumWasmModulesUrl` + - Improves `pdfium_worker.js`/`pdfium.wasm` loading path resolution logic to allow relative paths + +# 1.1.28 + +- WIP: zoom ratio calculation updates +- `goToPage` throws array index out of bounds error if the page number is out of range +- PDFium WASM 138.0.7162.0 +- Remove debug print + +# 1.1.27 + +- Apply a proposed fix for [#134](https://github.com/espresso3389/pdfrx/issues/134); but I'm not sure if it works well or not. Personally, I don't feel any difference... + +# 1.1.26 + +- Introduces `PdfPoint`, which work with `Offset` for conversion between PDF page coordinates and Flutter coordinates +- FIXED: [#352](https://github.com/espresso3389/pdfrx/issues/352) Link click/text selection are completely broken if PDF page is rotated + +# 1.1.25 + +- FIXED: [#350](https://github.com/espresso3389/pdfrx/issues/350) callback `onPageChanged` no longer called? + +# 1.1.24 + +- FIXED: [#336](https://github.com/espresso3389/pdfrx/issues/336) zoom out does not cover entire page after changing layout + - Updates to viewer example to support page layout switching + - Minor `goToPage` and other `goTo` functions behavior changes (`normalizeMatrix` and other) +- MERGED: PR [#349](https://github.com/espresso3389/pdfrx/pull/349) that fixes resource leaks on `PdfPageView` +- FIXED: [#215](https://github.com/espresso3389/pdfrx/issues/215) Wrong link highlight position on searching a word +- FIXED: [#344](https://github.com/espresso3389/pdfrx/issues/344) New "key event handling" feature in version 1.1.22 prevents `TextFormField` in page overlay from receiving key events + +# 1.1.23 + +- Minor internal change + +# 1.1.22 + +- `PdfDocumentFactory` refactoring to improve the code integrity + - Introduces `getDocumentFactory`/`getPdfjsDocumentFactory`/`getPdffiumDocumentFactory` to get the direct/internal document factory +- Introduces `PdfViewerParams.onKey`/`PdfViewerKeyHandlerParams` to handle key events on `PdfViewer` + +# 1.1.21 + +- FIXED: `loadOutline` is not implemented on PDFium WASM + +# 1.1.20 + +- Add HTML meta tags (`pdfrx-pdfium-wasm`/`pdfium-wasm-module-url`) to enable PDFium WASM support + +# 1.1.19 + +- `lib/src/pdfium/pdfium_bindings.dart` now keep Pdfium's original comments + +# 1.1.18 + +- Merge PR [#338](https://github.com/espresso3389/pdfrx/pull/338) from mtallenca/cache_expired_not_modified_fix + +# 1.1.17 + +- FIXED: example is not shown on pub.dev + +# 1.1.14 + +- Improve `pdfium_worker.js`/`pdfium.wasm` loading path resolution logic ([#331](https://github.com/espresso3389/pdfrx/issues/331)) + +# 1.1.13 + +- Fix indefinite stuck on loading PDF files from certain server; now it immediately return error (not actually fixed) ([#311](https://github.com/espresso3389/pdfrx/issues/311)) +- FIXED: 2nd time loading of certain URL fails due to some cache error ([#330](https://github.com/espresso3389/pdfrx/issues/330)) + +# 1.1.12 + +- FIXED: WASM: could not open PDF files smaller than 1MB ([#326](https://github.com/espresso3389/pdfrx/issues/326)) + +# 1.1.11 + +- `Color.withOpacity` -> `Color.withValues`, `Color.value` -> `Color.toARGB32()` + +# 1.1.10 + +- Update project structure to conform to [Package layout conventions](https://dart.dev/tools/pub/package-layout) +- revert: example code move on 1.1.9 + +# 1.1.9 + +- Move back the example viewer to example directory + +# 1.1.8 + +- Internal refactoring to improve the code integrity + +# 1.1.7 + +- Introducing `allowDataOwnershipTransfer` on `PdfDocument.openData` to allow transfer data ownership of the passed data; it is false by default to keep consistency with the previous behavior + - This actually fixes [#303](https://github.com/espresso3389/pdfrx/issues/303) but the drawback is that extra memory may be consumed on Flutter Web... + +# 1.1.6 + +- "Bleeding edge" PDFium WASM support (disabled by default) + +# 1.1.5 + +- Explicitly specify web support on `pubspec.yaml` + +# 1.1.4 + +- SDK constraint gets back to `>=3.7.0-323.0.dev` + +# 1.1.3 + +- Further WASM compatibility updates +- Demo page: CORS override for GitHub Pages using [gzuidhof/coi-serviceworker](https://github.com/gzuidhof/coi-serviceworker) + +# 1.1.2 + +- FIXED: if running with WASM enabled on Flutter Web, certain PDF file could not be loaded correctly +- Debug log to know WASM/SharedArrayBuffer status on Flutter Web + +# 1.1.1 + +- Supporting Flutter 3.29.0/Dart 3.7.0 (Stable) with workaround for breaking changes on Flutter 3.29.0 ([#295](https://github.com/espresso3389/pdfrx/issues/295)) + - It breaks compatibility with older stable Flutter versions :( + +# 1.0.103 + +- Change the default CDN for Pdf.js to `https://cdn.jsdelivr.net/npm/pdfjs-dist@/build/pdf.js` to deal with CORS error on loading CMAP files +- FIXED: `pdfjsGetDocumentFromData`, which is used by various `PdfDocument` open functions, does not propagate `cMapUrl`/`cMapPacked` to the Pdf.js + +# 1.0.102 + +- dart2wasm compatibility updates +- Pdf.js 4.10.38 +- `PdfTextSearcher` correctly releases its listeners on dispose +- Example viewer code updates + +# 1.0.101 + +- Revert commit d66fb3f that breaks consistency; `Color.withValues` -> `Color.withOpacity` +- Update pdfium ffi bindings + +# 1.0.100 + +- `PdfTextSearcher` introduces text caches ([#293](https://github.com/espresso3389/pdfrx/issues/293)) +- `PdfTextSearcher` search reset issue ([#291](https://github.com/espresso3389/pdfrx/issues/291)) +- collection's version spec. reverted to pre-1.0.95 + +# 1.0.99 + +- Introduces `Pdfrx.fontPaths` to set pdfium font loading path ([#140](https://github.com/espresso3389/pdfrx/issues/140)) + +# 1.0.98 + +- Introduces `PdfViewerController.calcFitZoomMatrices` to realize fit-to-width easier + +# 1.0.97 + +- Document updates + +# 1.0.96 + +- FIXED: [#260](https://github.com/espresso3389/pdfrx/issues/260) `onTextSelectionChange` callback cant be called + +# 1.0.95 + +- FIXED: [#273](https://github.com/espresso3389/pdfrx/issues/273); apart from the ream WASM support, it fixes several compilation issues with `--wasm` option + +# 1.0.94 + +- Merge PR [#272](https://github.com/espresso3389/pdfrx/pull/272); Fix `minScale` is not used + +# 1.0.93 + +- Merge PR [#264](https://github.com/espresso3389/pdfrx/pull/264); Check for non-existent zoom element in `PdfDest.params` in some PDFs +- FIXED: Widget tests starts to fail when using `PdfViewer` widget [#263](https://github.com/espresso3389/pdfrx/issues/263) + +# 1.0.92 + +- Merge PR [#262](https://github.com/espresso3389/pdfrx/pull/262); Remove redundant check that breaks building on some systems + +# 1.0.91 + +- Fixes selection issues caused by the changes on 1.0.90 + +# 1.0.90 + +- Introduces `selectableRegionInjector`/`perPageSelectableRegionInjector` ([#256](https://github.com/espresso3389/pdfrx/issues/256)) + +# 1.0.89 + +- web 1.1.0 support ([#254](https://github.com/espresso3389/pdfrx/issues/254)) + +# 1.0.88 + +- Merge PR [#251](https://github.com/espresso3389/pdfrx/pull/251) + +# 1.0.87 + +- BREAKING CHANGE: add more parameters to `PdfViewerParams.normalizeMatrix` to make it easier to handle more complex situations ([#239](https://github.com/espresso3389/pdfrx/issues/239)) + +# 1.0.86 + +- Add `PdfViewerParams.normalizeMatrix` to customize the transform matrix restriction; customizing existing logic on `_PdfViewerState._makeMatrixInSafeRange`; for issues like [#239](https://github.com/espresso3389/pdfrx/issues/239) + +# 1.0.85 + +- Fixes single-page layout issue on viewer start ([#247](https://github.com/espresso3389/pdfrx/issues/247)) +- Fixes blurry image issues ([#245](https://github.com/espresso3389/pdfrx/issues/245), [#232](https://github.com/espresso3389/pdfrx/issues/232)) + +# 1.0.84 + +- Merge PR [#230](https://github.com/espresso3389/pdfrx/pull/230) to add try-catch on UTF-8 decoding of URI path + +# 1.0.83 + +- Web related improvements + - Pdf.js 4.5.136 + - Remove dependency to `dart:js_interop_unsafe` + - Remove unnecessary synchronized call +- Improve text selection stability ([#4](https://github.com/espresso3389/pdfrx/issues/4), [#185](https://github.com/espresso3389/pdfrx/issues/185)) +- Add more mounted checks to improve `PdfViewer` stability and speed + +# 1.0.82 + +- collection/rxdart dependency workaround ([#211](https://github.com/espresso3389/pdfrx/issues/211)) + +# 1.0.81 + +- Introduces `PdfViewerController.useDocument` to make it easy to use `PdfDocument` safely +- Introduces `PdfViewerController.pageCount` to get page count without explicitly access `PdfViewerController.pages` +- `PdfViewerController.document`/`PdfViewerController.pages` are now deprecated + +# 1.0.80 + +- BREAKING CHANGE: `PdfViewerParams.viewerOverlayBuilder` introduces third parameter named `handleLinkTap`, which is used with `GestureDetector` to handle link-tap events on user code ([#175](https://github.com/espresso3389/pdfrx/issues/175)) +- Fix typos on `README.md` + +# 1.0.79 + +- FIXED: `RangeError` on `PdfViewer.uri` when missing "Expires" header ([#206](https://github.com/espresso3389/pdfrx/issues/206)) + +# 1.0.78 + +- Add `packagingOptions pickFirst` to workaround multiple `libpdfium.so` problem on Android build ([#8](https://github.com/espresso3389/pdfrx/issues/8)) +- FIXED: `_relayoutPages` may cause null access +- Update `README.md` to explain `PdfViewerParam.linkHandlerParams` for link handling + +# 1.0.77 + +- [#175](https://github.com/espresso3389/pdfrx/issues/175): Woops, just missing synchronized to call `loadLinks` causes multiple load invocations... + +# 1.0.76 + +- Add several tweaks to reduce `PdfLink`'s memory footprint (Related: [#175](https://github.com/espresso3389/pdfrx/issues/175)) +- Introduces `PdfViewerParam.linkHandlerParams` and `PdfLinkHandlerParams` to show/handle PDF links without using Flutter Widgets ([#175](https://github.com/espresso3389/pdfrx/issues/175)) + +# 1.0.75 + +- Pdf.js 4.4.168 + +# 1.0.74 + +- Introduces `PdfViewerController.getPdfPageHitTestResult` +- Introduces `PdfViewerController.layout` to get page layout + +# 1.0.73 + +- Introduces `PdfViewerParams.onViewSizeChanged`, which is called on view size change + - The feature can be used to keep the screen center on device screen rotation ([#194](https://github.com/espresso3389/pdfrx/issues/194)) + +# 1.0.72 + +- FIXED: Example code is not compilable +- FIXED: Marker could not be placed correctly on the example code ([#189](https://github.com/espresso3389/pdfrx/issues/189)) +- FIXED: Updated podspec file not to download the same archive again and again ([#154](https://github.com/espresso3389/pdfrx/issues/154)) +- Introduces chromium/6555 for all platforms + - Darwin uses pdfium-apple-v9 (chromium/6555) + - ~~Improves memory consumption by pdfium's internal caching feature ([#184](https://github.com/espresso3389/pdfrx/issues/184))~~ + +# 1.0.71 + +- Introduces `withCredentials` for Web to download PDF file using current session credentials (Cookie) ([#182](https://github.com/espresso3389/pdfrx/issues/182)) +- FIXED: Re-download logic error that causes 416 on certain web site ([#183](https://github.com/espresso3389/pdfrx/issues/183)) + +# 1.0.70 + +- `PdfViewer` calls re-layout logic on every zoom ratio changes ([#131](https://github.com/espresso3389/pdfrx/issues/131)) +- Add `PdfViewerParams.interactionEndFrictionCoefficient` ([#176](https://github.com/espresso3389/pdfrx/issues/176)) +- Minor fix for downloading cache +- `rxdart` gets back to 0.27.7 because 0.28.0 causes incompatibility with several other plugins... + +# 1.0.69 + +- FIXED: Small Page Size PDF Not Scaling to Fit Screen ([#174](https://github.com/espresso3389/pdfrx/issues/174)) + +# 1.0.68 + +- Introduces `PdfViewerController.setCurrentPageNumber` ([#152](https://github.com/espresso3389/pdfrx/issues/152)) +- BREAKING CHANGE: Current page number behavior change ([#152](https://github.com/espresso3389/pdfrx/issues/152)) +- BREAKING CHANGE: `PdfPageAnchor` behavior changes for existing `PdfPageAnchor` enumeration values. +- Introduces `PdfPageAnchor.top`/`left`/`right`/`bottom` +- Introduces `PdfViewerController.calcMatrixToEnsureRectVisible` + +# 1.0.67 + +- FIXED: `LateInitializationError`: Field `_cacheBlockCount@1436474497` has not been initialized ([#167](https://github.com/espresso3389/pdfrx/issues/167)) + +# 1.0.66 + +- FIXED: `PdfException`: Failed to load PDF document (`FPDF_GetLastError=3`) ([#166](https://github.com/espresso3389/pdfrx/issues/166)) +- Add explicit HTTP error handling code (to show the error detail) +- bblanchon/pdfium-binaries 127.0.6517.0 (chromium/6517) (iOS/macOS is still using 6406) + +# 1.0.65 + +- Remove dependency to `intl` ([#151](https://github.com/espresso3389/pdfrx/issues/151)) + +# 1.0.64 + +- Android: `minSdkVersion` to 21 (related [#158](https://github.com/espresso3389/pdfrx/issues/158)) + +# 1.0.63 + +- Workaround for `SelectionEventType.selectParagraph` that is introduced in master ([#156](https://github.com/espresso3389/pdfrx/issues/156)/PR [#157](https://github.com/espresso3389/pdfrx/pull/157)) + - The code uses `default` to handle the case but we should update it with the "right" code when it is introduced to the stable + +# 1.0.62 + +- iOS/macOS also uses bblanchon/pdfium-binaries 125.0.6406.0 (chromium/6406) +- Additional fix for [#147](https://github.com/espresso3389/pdfrx/issues/147) +- Additional implementation for [#132](https://github.com/espresso3389/pdfrx/issues/132) + +# 1.0.61 + +- Introduces `PdfViewerParams.pageDropShadow` +- Introduces `PdfViewerParams.pageBackgroundPaintCallbacks` + +# 1.0.60 + +- bblanchon/pdfium-binaries 125.0.6406.0 (chromium/6406) + - `default_min_sdk_version=21` to support lower API level devices ([#145](https://github.com/espresso3389/pdfrx/issues/145)) + +# 1.0.59 + +- Fixes concurrency issue on `PdfDocument` dispose ([#143](https://github.com/espresso3389/pdfrx/issues/143)) +- FIXED: Null check operator used on `_guessCurrentPage` ([#147](https://github.com/espresso3389/pdfrx/issues/147)) + +# 1.0.58 + +- Any API calls that wraps PDFium are now completely synchronized. They are run in an app-wide single worker isolate + - This is because PDFium does not support any kind of concurrency and even different `PdfDocument` instances could not be called concurrently + +# 1.0.57 + +- FIXED: possible double-dispose on race condition ([#136](https://github.com/espresso3389/pdfrx/issues/136)) +- Add mechanism to cancel partial real size rendering ([#137](https://github.com/espresso3389/pdfrx/issues/137)) +- WIP: Custom HTTP header for downloading PDF files ([#132](https://github.com/espresso3389/pdfrx/issues/132)) +- Text search match color customization ([#142](https://github.com/espresso3389/pdfrx/issues/142)) + +# 1.0.56 + +- Reduce total number of Isolates used when opening PDF documents +- Add `PdfViewerParams.calculateCurrentPageNumber` +- FIXED: Could not handle certain destination coordinates correctly ([#135](https://github.com/espresso3389/pdfrx/issues/135)) + +# 1.0.55 + +- Improve memory consumption by opening/closing page handle every time pdfrx need it (PR [#125](https://github.com/espresso3389/pdfrx/pull/125)) + +# 1.0.54 + +- Improves [End] button behavior to reach the actual end of document rather than the top of the last page + - `PdfViewerParams.pageAnchorEnd` for specifying anchor for the "virtual" page next to the last page +- `PdfViewerParams.onePassRenderingScaleThreshold` to specify maximum scale that is rendered in single rendering call + - If a page is scaled over the threshold scale, the page is once rendered in the threshold scale and after a some delay, the real scaled image is rendered partially that fits in the view port +- `PdfViewerParams.perPageSelectionAreaInjector` is introduced to customize text selection behavior + +# 1.0.53 + +- Fixes flicker on scrolling/zooming that was introduced on 1.0.52 +- Revival of high resolution partial rendering + +# 1.0.52 + +- Fixes memory consumption control issues (Related: [#121](https://github.com/espresso3389/pdfrx/issues/121)) + +# 1.0.51 + +- FIXED: memory leak on `_PdfPageViewState` ([#110](https://github.com/espresso3389/pdfrx/issues/110)) +- Remove dependency on `dart:js_util` ([#109](https://github.com/espresso3389/pdfrx/issues/109)) +- FIXED: Crash on `_PdfViewerScrollThumbState` ([#86](https://github.com/espresso3389/pdfrx/issues/86)) + +# 1.0.50 + +- Introduces `PdfViewerParams.useAlternativeFitScaleAsMinScale` but it's not recommended to set the value to false because it may degrade the viewer performance + +# 1.0.49 + +- iOS minimum deployment target 12.0 + +# 1.0.11 + +- `intl` 0.18.1 ([#87](https://github.com/espresso3389/pdfrx/issues/87)) + +# 1.0.10+1 + +- Add note for Flutter 3.19/Dart 3.3 support on 1.0.0+ + +# 1.0.10 + +- FIXED: `calcZoomStopTable` hangs app if zoom ratio is almost 0 ([#79](https://github.com/espresso3389/pdfrx/issues/79)) + +# 1.0.9 + +- `PdfRect.toRect`: `scaledTo` -> `scaledPageSize` +- FIXED: `PdfJsConfiguration.cMapUrl`/`cMapPacked` does not have correct default values + +# 1.0.8 + +- Condition analysis warnings on auto-generated `pdfium_bindings.dart` + +# 1.0.7 + +- Requires Flutter 3.19/Dart 3.3 again (pub.dev is upgraded to the stable🎉) +- `dart:js_interop` based Pdf.js interop implementation (remove dependency on `package:js`) + +# 1.0.6 + +- Due to the pub.dev version issues, the version introduces a "temporary workaround", which downgrades several packages: + - `sdk: '>=3.3.0-76.0.dev <4.0.0'` + - `flutter: '>=3.19.0-0.4.pre'` + - `web: ^0.4.2` + I'll update them as soon as [pub.dev upgrades their toolchains](https://github.com/dart-lang/pub-dev/issues/7484#issuecomment-1948206197) +- Pdf.js interop refactoring + +# 1.0.5 + +_NOTE: On pub.dev, 1.0.0+ versions gets [[ANALYSIS ISSUE]](https://pub.dev/packages/pdfrx/versions/1.0.5-testing-version-constraints-1/score). It does not affect your code consistency but API reference is not available until [pub.dev upgrades their toolchains](https://github.com/dart-lang/pub-dev/issues/7484#issuecomment-1948206197)._ + +- Requires Flutter 3.19/Dart 3.3 + +# 1.0.4 + +- Rollback version constraints to the older stable versions... + - I've created an issue for pub.dev: [dart-lang/pub-dev#7484](https://github.com/dart-lang/pub-dev/issues/7484) + +# 1.0.3 + +- Again, `flutter: '>=3.19.0-0.4.pre'` + +# 1.0.2 + +- To make the pub.dev analyzer work, we should use `sdk: '>=3.3.0-76.0.dev <4.0.0'` as version constraint... + +# 1.0.1 + +- `PdfViewerController.addListener`/`removeListener` independently has listener list on it to make it work regardless of `PdfViewer` attached or not ([#74](https://github.com/espresso3389/pdfrx/issues/74)) + +# 1.0.0 + +- Requires Flutter 3.19/Dart 3.3 +- Update Web code to use `package:web` (removing dependency to `dart:html`) + +# 0.4.44 + +- FIXED: `PdfViewerParams.boundaryMargin` does not work correctly. + +# 0.4.43 + +- Add note for dark/night mode support on `README.md`; the trick is originally introduced by [pckimlong](https://github.com/pckimlong) on [#46](https://github.com/espresso3389/pdfrx/issues/46). +- FIXED: wrong `PdfPageAnchor` behavior with landscape pages + +# 0.4.42 + +- FIXED: `PdfDocumentRefData`'s `operator==` is broken ([#66](https://github.com/espresso3389/pdfrx/issues/66)) + +# 0.4.41 + +- Marker example for `PdfViewerParams.onTextSelectionChange` ([#65](https://github.com/espresso3389/pdfrx/issues/65)) +- Add more explanation for `sourceName` ([#66](https://github.com/espresso3389/pdfrx/issues/66)) + +# 0.4.40 + +- Introduces `PdfViewerParams.onTextSelectionChange` ([#65](https://github.com/espresso3389/pdfrx/issues/65)) to know the last text selection + +# 0.4.39 + +- Minor updates on text selection (still experimental......) + +# 0.4.38 + +- Minor updates on text selection (still experimental...) +- Minor fix on `PdfPageView` + +# 0.4.37 + +- CMake version "3.18.1+" for [#48](https://github.com/espresso3389/pdfrx/issues/48), [#62](https://github.com/espresso3389/pdfrx/issues/62) + +# 0.4.36 + +- Introduces `PdfJsConfiguration` to configure Pdf.js download URLs + +# 0.4.35 + +- Download cache mechanism update ([#57](https://github.com/espresso3389/pdfrx/issues/57)/[#58](https://github.com/espresso3389/pdfrx/issues/58)) + +# 0.4.34 + +- Document update + +# 0.4.33 + +- Document update + +# 0.4.32 + +- Add `PdfViewerParams.calculateInitialPageNumber` to calculate the initial page number dynamically +- Add `PdfViewerParams.onViewerReady` to know when the viewer gets ready + +# 0.4.31 + +- Remove explicit CMake version spec 3.18.1 + +# 0.4.30 + +- FIXED: Link URI contains null-terminator +- Add support text/links on rotated pages +- Stability updates for `PdfTextSearcher` +- `README.md`/example updates +- Revival of `PdfViewer.data`/`PdfViewer.custom` + +# 0.4.29 + +- Minor fixes to `PdfTextSearcher` + +# 0.4.28 + +- `README.md`/example updates + +# 0.4.27 + +- Minor updates and `README.md` updates + +# 0.4.26 + +- Introduces `PdfTextSearcher` that helps you to implement search UI feature ([#47](https://github.com/espresso3389/pdfrx/issues/47)) +- Example code is vastly changed to explain more about the widget functions + +# 0.4.25 + +- FIXED: Able to scroll outside document area + +# 0.4.24 + +- Huge refactoring on `PdfViewerController`; it's no longer `TransformationController` but just a `ValueListenable` + - This fixes an "Unhandled Exception: Null check operator used on a null value" on widget state disposal ([#46](https://github.com/espresso3389/pdfrx/issues/46)) + +# 0.4.23 + +- Introduces `PdfDocumentViewBuilder`/`PdfPageView` widgets +- Example code is super updated with index and thumbnails. + +# 0.4.22 + +- Web: Now Pdf.js is loaded automatically and no modification to `index.html` is required! +- Default implementation for `PdfViewerParams.errorBannerBuilder` to show internally thrown errors +- `PdfPasswordException` is introduced to notify password error +- `PdfDocumentRef` now has `stackTrace` for error +- `PdfFileCache` now uses dedicated `http.Client` instance + +# 0.4.21 + +- Now `PdfDocumentRef` has const constructor and `PdfViewer.documentRef` is also const + +# 0.4.20 + +- Removes `PdfDocumentProvider` (Actually `PdfDocumentRef` does everything) +- Fixes breakage introduced by 0.4.18 + +# 0.4.19 + +- `firstAttemptByEmptyPassword` should be true by default + +# 0.4.18 + +- `PdfDocumentProvider` supercedes `PdfDocumentStore` ([#42](https://github.com/espresso3389/pdfrx/pull/42)) +- PDFium 6259 for Windows, Linux, and Android +- FIXED: Bug: Tests fail due to null operator check on `PdfViewerController` ([#44](https://github.com/espresso3389/pdfrx/issues/44)) + +# 0.4.17 + +- Additional fixes to text selection mechanism + +# 0.4.16 + +- Remove password parameters; use `passwordProvider` instead. +- Fixes several resource leak scenarios on `PdfDocument` open failures +- Restrict text selection if PDF permission does not allow copying +- Remove `PdfViewer.documentRef`; unnamed constructor is enough for the purpose + +# 0.4.15 + +- Introduces `PdfViewer.documentRef` ([#36](https://github.com/espresso3389/pdfrx/issues/36)) +- FIXED: `PdfViewer.uri` is broken on web for non relative paths ([#37](https://github.com/espresso3389/pdfrx/issues/37)) +- FIXED: Don't Animate to `initialPage` ([#39](https://github.com/espresso3389/pdfrx/issues/39)) + +# 0.4.14 + +- Introduces `PdfViewerParams.onDocumentChanged` event +- Introduces `PdfDocument.loadOutline` to load outline (a.k.a. bookmark) + +# 0.4.13 + +- Improves document password handling by async `PasswordProvider` ([#20](https://github.com/espresso3389/pdfrx/issues/20)) +- Introduces `PdfViewerParams.errorBannerBuilder` + +# 0.4.12 + +- Introduces `PdfViewerParams.maxImageBytesCachedOnMemory`, which restricts the maximum cache memory consumption + - Better than logic based on `maxThumbCacheCount` +- Remove the following parameters from `PdfViewerParams`: + - `maxThumbCacheCount` + - `maxRealSizeImageCount` + - `enableRealSizeRendering` + +# 0.4.11 + +- Add support for PDF Destination (Page links) + +# 0.4.10 + +- FIXED: `isEncrypted` property of document returns always true even the document is not encrypted ([#29](https://github.com/espresso3389/pdfrx/issues/29)) + +# 0.4.9 + +- FIXED: `SelectionArea` makes Web version almost unusable ([#31](https://github.com/espresso3389/pdfrx/issues/31)) + +# 0.4.8 + +- FIXED: Unhandled Exception: type 'Null' is not a subtype of type `PdfPageRenderCancellationTokenPdfium` in type cast ([#26](https://github.com/espresso3389/pdfrx/issues/26)) + +# 0.4.7 + +- FIXED: Android build broken? Cannot find `libpdfium.so` error ([#25](https://github.com/espresso3389/pdfrx/issues/25)) +- `PdfViewerParams.loadingBannerBuilder` to customize HTTP download progress +- `PdfViewerParams.linkWidgetBuilder` to support embedded links +- WIP: Updated text selection mechanism, which is faster and stable but still certain issues + - Pan-to-scroll does not work on Desktop/Web + - Selection does not work as expected on mobile devices +- Support Linux running on arm64 Raspberry PI ([#23](https://github.com/espresso3389/pdfrx/issues/23), [#24](https://github.com/espresso3389/pdfrx/issues/24)) + +# 0.4.6 + +- Introduces `PdfPage.render` cancellation mechanism + - `PdfPageRenderCancellationToken` to cancel the rendering process + - BREAKING CHANGE: `PdfPage.render` may return null if the rendering process is canceled +- `PdfPageRender.render` limits render resolution up to 300-dpi unless you use `getPageRenderingScale` + - Even with the restriction, image size may get large and you'd better implement `getPageRenderingScale` to restrict such large image rendering +- `PdfViewerParams` default changes: + - `scrollByMouseWheel` default is 0.2 + - `maxRealSizeImageCount` default is 3 +- `PdfViewerParams.scrollByArrowKey` to enable keyboard navigation + +# 0.4.5 + +- `PdfViewerParams` updates + - `PdfViewerParams.onPageChanged` replaces `onPageChanged` parameter on `PdfViewer` factories + - `PdfViewerParams.pageAnchor` replaces `anchor` parameter on `PdfViewer` factories +- `pdfDocumentFromUri`/`PdfFileCache` improves mechanism to cache downloaded PDF file + - ETag check to invalidate the existing cache + - Better downloaded region handling + +# 0.4.4 + +- `PdfPage.render` can render Annotations and FORMS +- `PdfFileCache`: More realistic file cache mechanism +- Introduces `PasswordProvider` to repeatedly test passwords (only API layer) + +# 0.4.3 + +- FIXED: cache mechanism is apparently broken ([#12](https://github.com/espresso3389/pdfrx/issues/12)) + +# 0.4.2 + +- `PdfViewerParams.pageOverlayBuilder` to customize PDF page ([#17](https://github.com/espresso3389/pdfrx/issues/17)) +- Updating `README.md` + +# 0.4.1 + +- Add `PdfViewerParams.enableRenderAnnotations` to enable annotations on rendering ([#18](https://github.com/espresso3389/pdfrx/issues/18), [#19](https://github.com/espresso3389/pdfrx/issues/19)) + +# 0.4.0 + +- Many breaking changes but they improve the code integrity: + - `PdfDocument.pages` supersedes `PdfDocument.getPage` + - `PdfDocument.pageCount` is removed + - `PdfViewerParams.devicePixelRatioOverride` is removed; use `getPageRenderingScale` instead +- Add `PdfPageAnchor.all` +- `PdfViewerParams.viewerOverlayBuilder`/`PdfViewerScrollThumb` to support scroll thumbs + +# 0.3.6 + +- `PageLayout` -> `PdfPageLayout` + +# 0.3.5 + +- `PageLayout` class change to ease page layout customization + - Add example use case in API document + +# 0.3.4 + +- Rewriting page rendering code + - Due to the internal structure change, page drawing customization parameters are once removed: + - `pageDecoration` + - `pageOverlaysBuilder` +- Example code does not enables `enableTextSelection`; it's still too experimental... + +# 0.3.3 + +- FIXED: Downloading of small PDF file causes internal loading error + +# 0.3.2 + +- Support mouse-wheel-to-scroll on Desktop platforms + +# 0.3.1 + +- Minor API changes +- Internal integrity updates that controls the viewer behaviors +- FIX: example code does not have `android.permission.INTERNET` on `AndroidManifest.xml` +- `PdfViewerParams.devicePixelRatioOverride` is deprecated and introduces `PdfViewerParams.getPageRenderingScale` + +# 0.3.0 + +- Many renaming of the APIs that potentially breaks existing apps + +# 0.2.4 + +- Now uses `plugin_ffi`. (Not containing any Flutter plugin stab) + +# 0.2.3 + +- FIXED: [#6](https://github.com/espresso3389/pdfrx/issues/6) `PdfPageWeb.render` behavior is different from `PdfPagePdfium.render` + +# 0.2.2 + +- Explicitly specify Flutter 3.16/Dart 3.2 as `NativeCallable.listener` does not accept non-static function ([#5](https://github.com/espresso3389/pdfrx/issues/5)) + +# 0.2.1 + +- Stabilizing API surface + - Introducing `PdfViewer.asset`/`file`/`uri`/`custom` + - `PdfViewer` has `documentLoader` to accept function to load `PdfDocument` +- Fixes minor issues on `PdfViewer` + +# 0.2.0 + +- Introducing `PdfDocument.openUri`/`PdfFileCache*` classes +- Introducing `PdfPermissions` +- `PdfPage.loadText`/`PdfPageText` for text extraction +- Android NDK CMake to 3.18.1 + +# 0.1.1 + +- Document updates +- Pdf.js 3.11.174 + +# 0.1.0 + +- First release (Documentation is not yet ready) diff --git a/wasm/pdfrx_wasm/LICENSE b/packages/pdfrx/LICENSE similarity index 100% rename from wasm/pdfrx_wasm/LICENSE rename to packages/pdfrx/LICENSE diff --git a/packages/pdfrx/README.md b/packages/pdfrx/README.md new file mode 100644 index 00000000..625ca05f --- /dev/null +++ b/packages/pdfrx/README.md @@ -0,0 +1,278 @@ +# pdfrx + +[![Build Test](https://github.com/espresso3389/pdfrx/actions/workflows/build-test.yml/badge.svg)](https://github.com/espresso3389/pdfrx/actions/workflows/build-test.yml) + +[pdfrx](https://pub.dartlang.org/packages/pdfrx) is a rich and fast PDF viewer and manipulation plugin for Flutter. It provides ready-to-use widgets for displaying and editing PDF documents, including page manipulation, document combining, and image import in your Flutter applications. + +This plugin is built on top of [pdfrx_engine](https://pub.dartlang.org/packages/pdfrx_engine), which handles the low-level PDF rendering and manipulation using [PDFium](https://pdfium.googlesource.com/pdfium/). The separation allows for a clean architecture where: + +- **pdfrx** (this package) - Provides Flutter widgets, UI components, platform integration, and PDF editing features (page manipulation, combining, image import) +- **pdfrx_engine** - Handles PDF parsing, rendering, and manipulation without Flutter dependencies + +The plugin supports Android, iOS, Windows, macOS, Linux, and Web. + +## Interactive Demo + +A [demo site](https://espresso3389.github.io/pdfrx/) using Flutter Web + +![pdfrx](https://github.com/espresso3389/pdfrx/assets/1311400/b076ac0b-e2cb-48f0-8772-9891537ade7b) + +## Multi-platform support + +- Android +- iOS +- Windows +- macOS +- Linux (even on Raspberry Pi) +- Web (WASM) + +## Example Code + +The following fragment illustrates the easiest way to show a PDF file in assets: + +```dart +import 'package:pdfrx/pdfrx.dart'; + +... + +class _MyAppState extends State { + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Pdfrx example'), + ), + body: PdfViewer.asset('assets/hello.pdf'), + ), + ); + } +} +``` + +Anyway, please follow the instructions below to install on your environment. + +## Getting Started + +### Installation + +Add this to your package's `pubspec.yaml` file and execute `flutter pub get`: + +```yaml +dependencies: + pdfrx: ^2.2.20 +``` + +**Note:** You only need to add `pdfrx` to your dependencies. The `pdfrx_engine` package is automatically included as a dependency of `pdfrx`. + +### Initialization + +If you access the document API directly (for example, opening a [PdfDocument](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocument-class.html) before any pdfrx widget is built), call [pdfrxFlutterInitialize](https://pub.dev/documentation/pdfrx/latest/pdfrx/pdfrxFlutterInitialize.html) once during app startup: + +```dart +import 'package:flutter/widgets.dart'; +import 'package:pdfrx/pdfrx.dart'; + +Future main() { + WidgetsFlutterBinding.ensureInitialized(); + pdfrxFlutterInitialize(); // Required when using engine APIs before widgets + runApp(const MyApp()); +} +``` + +For more information, see [pdfrx Initialization](https://github.com/espresso3389/pdfrx/blob/master/doc/pdfrx-Initialization.md) + +Tip: To silence debug-time WASM warnings, call `pdfrxFlutterInitialize(dismissPdfiumWasmWarnings: true)` during startup. + +### Note for Windows + +**REQUIRED: You must enable [Developer Mode](https://learn.microsoft.com/en-us/windows/apps/get-started/enable-your-device-for-development#activate-developer-mode) to build pdfrx on Windows.** + +The build process uses *symbolic links* which requires Developer Mode to be enabled. If Developer Mode is not enabled: + +- The build will fail with an error message +- You will see a link to Microsoft's official instructions +- You must enable Developer Mode and restart your computer before building + +Please follow Microsoft's official guide to enable Developer Mode as the exact steps may vary depending on your Windows version. + +## Note for Building Release Builds + +*Please note that the section is not applicable to Web.* + +Because the plugin contains WASM binaries as its assets and they increase the size of the app regardless of the platform. +This is normally OK for development or debugging but you may want to remove them when building release builds. + +To do this, do `dart run pdfrx:remove_wasm_modules` between `flutter pub get` and `flutter build ...` on your app project's root directory: + +```bash +flutter pub get +dart run pdfrx:remove_wasm_modules +flutter build ... +``` + +To restore the WASM binaries, run the following command: + +```bash +dart run pdfrx:remove_wasm_modules --revert +``` + +## Note for iOS/macOS: Using CoreGraphics Instead of PDFium + +For iOS and macOS apps, you can optionally use [pdfrx_coregraphics](https://pub.dev/packages/pdfrx_coregraphics) to render PDFs with Apple's native CoreGraphics/PDFKit instead of the bundled PDFium library. This can significantly reduce your app size by removing PDFium dependencies on Darwin platforms. + +**⚠️ Note: `pdfrx_coregraphics` is experimental and has some limitations. See the [package documentation](https://pub.dev/packages/pdfrx_coregraphics#limitations) for details.** + +To use CoreGraphics rendering: + +1. Add `pdfrx_coregraphics` to your dependencies +2. Set the CoreGraphics entry functions before initializing pdfrx +3. Optionally remove PDFium dependencies to reduce app size + +For complete installation instructions and app size reduction steps, see the [pdfrx_coregraphics README](https://pub.dev/packages/pdfrx_coregraphics). + +## PdfViewer constructors + +For opening PDF files from various sources, there are several constructors available in [PdfViewer](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer-class.html): + +- [PdfViewer.asset](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer/PdfViewer.asset.html) - Load from Flutter assets +- [PdfViewer.file](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer/PdfViewer.file.html) - Load from local file +- [PdfViewer.data](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer/PdfViewer.data.html) - Load from memory (Uint8List) +- [PdfViewer.uri](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer/PdfViewer.uri.html) - Load from network URL + +## Customizations/Features + +You can customize the behaviors and the viewer look and feel by configuring [PdfViewerParams](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams-class.html). + +```dart +PdfViewer.asset( + 'assets/test.pdf', + params: const PdfViewerParams( + scrollPhysics: FixedOverscrollPhysics(maxOverscroll: 120), + scrollPhysicsScale: BouncingScrollPhysics(), + ), +); +``` + +The `scrollPhysics` and `scrollPhysicsScale` hooks let you plug in your own [ScrollPhysics](https://api.flutter.dev/flutter/widgets/ScrollPhysics-class.html) (or the bundled [FixedOverscrollPhysics](https://pub.dev/documentation/pdfrx/latest/pdfrx/FixedOverscrollPhysics-class.html)) to tune drag and zoom behavior per platform. + +## Deal with Password Protected PDF Files + +```dart +PdfViewer.asset( + 'assets/test.pdf', + // The easiest way to supply a password + passwordProvider: () => createSimplePasswordProvider('password'), + + ... +), +``` + +See [Deal with Password Protected PDF Files using PasswordProvider](https://github.com/espresso3389/pdfrx/blob/master/doc/Deal-with-Password-Protected-PDF-Files-using-PasswordProvider.md) for more information. + +### Text Selection + +The text selection feature is enabled by default, allowing users to select text in the PDF viewer. You can customize the text selection behavior using [PdfTextSelectionParams](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfTextSelectionParams-class.html). + +The following example shows how to disable text selection in the PDF viewer: + +```dart +PdfViewer.asset( + 'assets/test.pdf', + params: PdfViewerParams( + textSelectionParams: PdfTextSelectionParams( + enabled: false, + ... + ), + ), + ... +), +``` + +The text selection feature supports various customizations, such as: + +- Context Menu Customization using [PdfViewerParams.buildContextMenu](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/buildContextMenu.html) +- Text Selection Magnifier Customization using [PdfTextSelectionParams.magnifier](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfTextSelectionParams/magnifier.html) + +For more text selection customization, see [Text Selection](https://github.com/espresso3389/pdfrx/blob/master/doc/Text-Selection.md). + +### PDF Feature Support + +- [PDF Link Handling](https://github.com/espresso3389/pdfrx/blob/master/doc/PDF-Link-Handling.md) +- [Document Outline (a.k.a Bookmarks)](https://github.com/espresso3389/pdfrx/blob/master/doc/Document-Outline-(a.k.a-Bookmarks).md) +- [Text Search](https://github.com/espresso3389/pdfrx/blob/master/doc/Text-Search.md) + +### Viewer Customization + +- [Page Layout (Horizontal Scroll View/Facing Pages)](https://github.com/espresso3389/pdfrx/blob/master/doc/Page-Layout-Customization.md) +- [Showing Scroll Thumbs](https://github.com/espresso3389/pdfrx/blob/master/doc/Showing-Scroll-Thumbs.md) +- [Dark/Night Mode Support](https://github.com/espresso3389/pdfrx/blob/master/doc/Dark-Night-Mode-Support.md) +- [Document Loading Indicator](https://github.com/espresso3389/pdfrx/blob/master/doc/Document-Loading-Indicator.md) +- [Viewer Customization using Widget Overlay](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/viewerOverlayBuilder.html) +- [Custom Scroll Physics for Drag/Zoom](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/scrollPhysics.html) + +### Additional Customizations + +- [Double-tap to Zoom](https://github.com/espresso3389/pdfrx/blob/master/doc/Double-tap-to-Zoom.md) +- [Adding Page Number on Page Bottom](https://github.com/espresso3389/pdfrx/blob/master/doc/Adding-Page-Number-on-Page-Bottom.md) +- [Per-page Customization using Widget Overlay](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/pageOverlaysBuilder.html) +- [Per-page Customization using Canvas](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewerParams/pagePaintCallbacks.html) + +## Additional Widgets + +### PdfDocumentViewBuilder/PdfPageView + +[PdfPageView](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfPageView-class.html) is just another PDF widget that shows only one page. It accepts [PdfDocument](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocument-class.html) and page number to show a page within the document. + +[PdfDocumentViewBuilder](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocumentViewBuilder-class.html) is used to safely manage [PdfDocument](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocument-class.html) inside widget tree and it accepts `builder` parameter that creates child widgets. + +The following fragment is a typical use of these widgets: + +```dart +PdfDocumentViewBuilder.asset( + 'asset/test.pdf', + builder: (context, document) => ListView.builder( + itemCount: document?.pages.length ?? 0, + itemBuilder: (context, index) { + return Container( + margin: const EdgeInsets.all(8), + height: 240, + child: Column( + children: [ + SizedBox( + height: 220, + child: PdfPageView( + document: document, + pageNumber: index + 1, + alignment: Alignment.center, + ), + ), + Text( + '${index + 1}', + ), + ], + ), + ); + }, + ), +), +``` + +## PdfDocument Management + +[PdfDocumentViewBuilder](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocumentViewBuilder-class.html) can accept [PdfDocumentRef](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocumentRef-class.html) from [PdfViewer](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer-class.html) to safely share the same [PdfDocument](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocument-class.html) instance. For more information, see [`example/viewer/lib/thumbnails_view.dart`](example/viewer/lib/thumbnails_view.dart). + +## API Documentation + +### Flutter Widgets (pdfrx) + +- [PdfViewer](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer-class.html) - Main PDF viewer widget +- [PdfDocumentViewBuilder](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocumentViewBuilder-class.html) - Builder for safe async document loading +- [PdfPageView](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfPageView-class.html) - Single page display widget + +### Low-Level PDF API (pdfrx_engine) + +For advanced use cases requiring direct PDF manipulation without Flutter widgets, see the [pdfrx_engine API reference](https://pub.dev/documentation/pdfrx_engine/latest/). This includes: + +- [PdfDocument](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDocument-class.html) - Core document interface +- [PdfPage](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPage-class.html) - Page rendering and manipulation diff --git a/analysis_options.yaml b/packages/pdfrx/analysis_options.yaml similarity index 94% rename from analysis_options.yaml rename to packages/pdfrx/analysis_options.yaml index 44ca9a1f..22448175 100644 --- a/analysis_options.yaml +++ b/packages/pdfrx/analysis_options.yaml @@ -34,14 +34,15 @@ linter: sort_constructors_first: true always_put_required_named_parameters_first: true invalid_runtime_check_with_js_interop_types: true + type_annotate_public_apis: true + omit_local_variable_types: true + omit_obvious_local_variable_types: true # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options analyzer: - exclude: - - lib/src/pdfium/pdfium_bindings.dart formatter: page_width: 120 diff --git a/wasm/pdfrx_wasm/assets/pdfium.wasm b/packages/pdfrx/assets/pdfium.wasm similarity index 61% rename from wasm/pdfrx_wasm/assets/pdfium.wasm rename to packages/pdfrx/assets/pdfium.wasm index edcf7c1a..f79da4df 100644 Binary files a/wasm/pdfrx_wasm/assets/pdfium.wasm and b/packages/pdfrx/assets/pdfium.wasm differ diff --git a/packages/pdfrx/assets/pdfium_client.js b/packages/pdfrx/assets/pdfium_client.js new file mode 100644 index 00000000..81fd2ce8 --- /dev/null +++ b/packages/pdfrx/assets/pdfium_client.js @@ -0,0 +1,65 @@ +globalThis.PdfiumWasmCommunicator = (function () { + const worker = new Worker(globalThis.pdfiumWasmWorkerUrl); + let requestId = 0; + let callbackId = 0; + const requestCallbacks = new Map(); + const registeredCallbacks = new Map(); + + worker.onmessage = (event) => { + const data = event.data; + if (data.type === 'ready') { + console.log('PDFium WASM worker is ready'); + return; + } + + // Handle callback invocations from the worker + if (data.type === 'callback') { + const callback = registeredCallbacks.get(data.callbackId); + if (callback) { + try { + callback(...data.args); + } catch (e) { + console.error('Error in callback:', e); + } + } + return; + } + + // For command responses, match using the request id. + if (data.id) { + const callback = requestCallbacks.get(data.id); + if (callback) { + if (data.status === 'success') { + callback.resolve(data.result); + } else { + callback.reject(new Error(data.error, data.cause != null ? { cause: data.cause } : undefined)); + } + requestCallbacks.delete(data.id); + } + } + }; + + worker.onerror = (err) => { + console.error('Worker error:', err); + }; + + return { + sendCommand: function (command, parameters = {}, transfer = []) { + return new Promise((resolve, reject) => { + const id = ++requestId; + requestCallbacks.set(id, { resolve, reject }); + worker.postMessage({ id, command, parameters }, transfer); + }); + }, + + registerCallback: function (callback) { + const id = ++callbackId; + registeredCallbacks.set(id, callback); + return id; + }, + + unregisterCallback: function (id) { + registeredCallbacks.delete(id); + } + }; +})(); diff --git a/packages/pdfrx/assets/pdfium_worker.js b/packages/pdfrx/assets/pdfium_worker.js new file mode 100644 index 00000000..f22c6aa2 --- /dev/null +++ b/packages/pdfrx/assets/pdfium_worker.js @@ -0,0 +1,2428 @@ +// +// A small implementation of a Web Worker that uses pdfium.wasm to render PDF files. +// + +/** + * PDFium WASM module imports + */ +const Pdfium = { + /** + * @param {WebAssembly.Exports} wasmExports + */ + initWith: function (wasmExports) { + Pdfium.wasmExports = wasmExports; + Pdfium.memory = Pdfium.wasmExports.memory; + Pdfium.wasmTable = Pdfium.wasmExports['__indirect_function_table']; + Pdfium.stackSave = Pdfium.wasmExports['emscripten_stack_get_current']; + Pdfium.stackRestore = Pdfium.wasmExports['_emscripten_stack_restore']; + Pdfium.setThrew = Pdfium.wasmExports['setThrew']; + Pdfium.__emscripten_stack_alloc = wasmExports['_emscripten_stack_alloc']; + }, + + /** + * @type {WebAssembly.Exports} + */ + wasmExports: null, + /** + * @type {WebAssembly.Memory} + */ + memory: null, + /** + * @type {WebAssembly.Table} + */ + wasmTable: null, + /** + * @type {WebAssembly.Table} + */ + wasmTableMirror: [], + /** + * @type {WeakMap} + */ + functionsInTableMap: null, + /** + * @type {number[]} + */ + freeTableIndexes: [], + /** + * @type {function():number} + */ + stackSave: null, + /** + * @type {function(number):void} + */ + stackRestore: null, + /** + * @type {function(number, number):void} + */ + setThrew: null, + /** + * @type {function(number):number} + */ + __emscripten_stack_alloc: null, + + /** + * Invoke a function from the WASM table + * @param {number} index Function index + * @param {function(function())} func Function to call + * @returns {*} Result of the function + */ + invokeFunc: function (index, func) { + const sp = Pdfium.stackSave(); + try { + return func(Pdfium.wasmTable.get(index)); + } catch (e) { + Pdfium.stackRestore(sp); + if (e !== e + 0) throw e; + Pdfium.setThrew(1, 0); + } + }, + + getCFunc: (ident) => Pdfium.wasmExports['_' + ident], + writeArrayToMemory: (array, buffer) => HEAP8.set(array, buffer), + stackAlloc: (sz) => Pdfium.__emscripten_stack_alloc(sz), + stringToUTF8OnStack: (str) => { + const size = StringUtils.lengthBytesUTF8(str) + 1; + const ret = Pdfium.stackAlloc(size); + StringUtils.stringToUtf8Bytes(str, ret); + return ret; + }, + ccall: (ident, returnType, argTypes, args, opts) => { + const toC = { + string: (str) => { + let ret = 0; + if (str !== null && str !== undefined && str !== 0) { + ret = Pdfium.stringToUTF8OnStack(str); + } + return ret; + }, + array: (arr) => { + const ret = Pdfium.stackAlloc(arr.length); + Pdfium.writeArrayToMemory(arr, ret); + return ret; + }, + }; + function convertReturnValue(ret) { + if (returnType === 'string') return UTF8ToString(ret); + if (returnType === 'boolean') return Boolean(ret); + return ret; + } + const func = Pdfium.getCFunc(ident); + const cArgs = []; + let stack = 0; + if (args) { + for (let i = 0; i < args.length; i++) { + const converter = toC[argTypes[i]]; + if (converter) { + if (stack === 0) stack = Pdfium.stackSave(); + cArgs[i] = converter(args[i]); + } else { + cArgs[i] = args[i]; + } + } + } + let ret = func(...cArgs); + function onDone(ret) { + if (stack !== 0) Pdfium.stackRestore(stack); + return convertReturnValue(ret); + } + ret = onDone(ret); + return ret; + }, + cwrap: (ident, returnType, argTypes, opts) => { + const numericArgs = !argTypes || argTypes.every((type) => type === 'number' || type === 'boolean'); + const numericRet = returnType !== 'string'; + if (numericRet && numericArgs && !opts) { + return Pdfium.getCFunc(ident); + } + return (...args) => Pdfium.ccall(ident, returnType, argTypes, args, opts); + }, + uleb128Encode: (n, target) => { + if (n < 128) { + target.push(n); + } else { + target.push(n % 128 | 128, n >> 7); + } + }, + sigToWasmTypes: (sig) => { + const typeNames = { + i: 'i32', + j: 'i64', + f: 'f32', + d: 'f64', + e: 'externref', + p: 'i32', + }; + const type = { + parameters: [], + results: sig[0] == 'v' ? [] : [typeNames[sig[0]]], + }; + for (let i = 1; i < sig.length; ++i) { + type.parameters.push(typeNames[sig[i]]); + } + return type; + }, + generateFuncType: (sig, target) => { + const sigRet = sig.slice(0, 1); + const sigParam = sig.slice(1); + const typeCodes = { i: 127, p: 127, j: 126, f: 125, d: 124, e: 111 }; + target.push(96); + Pdfium.uleb128Encode(sigParam.length, target); + for (let i = 0; i < sigParam.length; ++i) { + target.push(typeCodes[sigParam[i]]); + } + if (sigRet == 'v') { + target.push(0); + } else { + target.push(1, typeCodes[sigRet]); + } + }, + convertJsFunctionToWasm: (func, sig) => { + if (typeof WebAssembly.Function == 'function') { + return new WebAssembly.Function(Pdfium.sigToWasmTypes(sig), func); + } + const typeSectionBody = [1]; + Pdfium.generateFuncType(sig, typeSectionBody); + const bytes = [0, 97, 115, 109, 1, 0, 0, 0, 1]; + Pdfium.uleb128Encode(typeSectionBody.length, bytes); + bytes.push(...typeSectionBody); + bytes.push(2, 7, 1, 1, 101, 1, 102, 0, 0, 7, 5, 1, 1, 102, 0, 0); + const module = new WebAssembly.Module(new Uint8Array(bytes)); + const instance = new WebAssembly.Instance(module, { e: { f: func } }); + const wrappedFunc = instance.exports['f']; + return wrappedFunc; + }, + updateTableMap: (offset, count) => { + if (Pdfium.functionsInTableMap) { + for (let i = offset; i < offset + count; i++) { + const item = Pdfium.wasmTable.get(i); + if (item) { + Pdfium.functionsInTableMap.set(item, i); + } + } + } + }, + getFunctionAddress: (func) => { + if (!Pdfium.functionsInTableMap) { + Pdfium.functionsInTableMap = new WeakMap(); + Pdfium.updateTableMap(0, Pdfium.wasmTable.length); + } + return Pdfium.functionsInTableMap.get(func) || 0; + }, + getEmptyTableSlot: () => { + if (Pdfium.freeTableIndexes.length) return Pdfium.freeTableIndexes.pop(); + try { + Pdfium.wasmTable.grow(1); + } catch (err) { + if (!(err instanceof RangeError)) { + throw err; + } + throw 'Unable to grow wasm table. Set ALLOW_TABLE_GROWTH.'; + } + return Pdfium.wasmTable.length - 1; + }, + /** + * @param {function} func Function to add + * @param {string} sig Signature of the function + * @return {number} Function index in the table + */ + addFunction: (func, sig) => { + const rtn = Pdfium.getFunctionAddress(func); + if (rtn) { + return rtn; + } + const ret = Pdfium.getEmptyTableSlot(); + try { + Pdfium.wasmTable.set(ret, func); + } catch (err) { + if (!(err instanceof TypeError)) { + throw err; + } + const wrapped = Pdfium.convertJsFunctionToWasm(func, sig); + Pdfium.wasmTable.set(ret, wrapped); + } + Pdfium.functionsInTableMap.set(func, ret); + return ret; + }, + removeFunction: (index) => { + Pdfium.functionsInTableMap.delete(Pdfium.wasmTable.get(index)); + Pdfium.wasmTable.set(index, null); + Pdfium.freeTableIndexes.push(index); + }, +}; + +/** + * @typedef {Object} FileContext Defines I/O functions for a file + * @property {number} size File size + * @property {function(FileDescriptorContext, Uint8Array):number} read read(context, data) + * @property {function(FileDescriptorContext):void|undefined} close close(context) + * @property {function(FileDescriptorContext, Uint8Array):number|undefined} write write(context, data) + * @property {function(FileDescriptorContext):number|undefined} sync sync(context) + */ + +/** + * @typedef {Object} FileDescriptorContext Defines I/O functions for a file descriptor + * @property {number} size File size + * @property {function(FileDescriptorContext, Uint8Array):number} read read(context, data) + * @property {function(FileDescriptorContext):void|undefined} close close(context) + * @property {function(FileDescriptorContext, Uint8Array):number|undefined} write write(context, data) + * @property {function(FileDescriptorContext):number|undefined} sync sync(context) + * @property {string} fileName + * @property {number} fd + * @property {number} flags + * @property {number} mode + * @property {number} dirfd + * @property {number} position Current position + */ + +/** + * @typedef {Object} DirectoryContext Defines I/O functions for a directory file descriptor + * @property {string[]} entries Directory entries (For directories, the name should be terminated with /) + */ + +/** + * @typedef {Object} DirectoryFileDescriptorContext Defines I/O functions for a directory file descriptor + * @property {string[]} entries Directory entries (For directories, the name should be terminated with /) + * @property {string} fileName + * @property {number} fd + * @property {number} dirfd + * @property {number} position Current entry index + */ + +/** + * Emulate file system for PDFium + */ +class FileSystemEmulator { + constructor() { + /** + * Filename to I/O functions/data + * @type {Object} + */ + this.fn2context = {}; + /** + * File descriptor to I/O functions/data + * @type {Object} + */ + this.fd2context = {}; + /** + * Last assigned file descriptor + * @type {number} + */ + this.fdAssignedLast = 1000; + } + + /** + * Register file + * @param {string} fn Filename + * @param {FileContext|DirectoryContext} context I/O functions/data + */ + registerFile(fn, context) { + this.fn2context[fn] = context; + } + + /** + * Register file with ArrayBuffer + * @param {string} fn Filename + * @param {ArrayBuffer} data File data + */ + registerFileWithData(fn, data) { + data = data.buffer != null ? data.buffer : data; + this.registerFile(fn, { + size: data.byteLength, + read: function (context, buffer) { + try { + const size = Math.min(buffer.byteLength, data.byteLength - context.position); + const array = new Uint8Array(data, context.position, size); + buffer.set(array); + context.position += array.byteLength; + return array.length; + } catch (err) { + console.error(`read error: ${_error(err)}`); + return 0; + } + }, + }); + } + + /** + * Unregister file/directory context + * @param {string} fn Filename + */ + unregisterFile(fn) { + delete this.fn2context[fn]; + } + + /** + * Open a file + * @param {number} dirfd Directory file descriptor + * @param {number} fileNamePtr Pointer to buffer that contains filename + * @param {number} flags File open flags + * @param {number} mode File open mode + * @returns {number} File descriptor + */ + openFile(dirfd, fileNamePtr, flags, mode) { + const fn = StringUtils.utf8BytesToString(new Uint8Array(Pdfium.memory.buffer, fileNamePtr, 2048)); + const funcs = this.fn2context[fn]; + if (funcs) { + const fd = ++this.fdAssignedLast; + this.fd2context[fd] = { ...funcs, fd, flags, mode, dirfd, position: 0 }; + return fd; + } + console.error(`openFile: not found: ${dirfd}/${fn}`); + return -1; + } + + /** + * Close a file + * @param {number} fd File descriptor + */ + closeFile(fd) { + const context = this.fd2context[fd]; + context.close?.call(context); + delete this.fd2context[fd]; + return 0; + } + + /** + * Seek to a position in a file + * @param {number} fd File descriptor + * @returns {number} New offset + */ + seek(fd) { + let offset, whence, newOffset; + if (arguments.length == 4) { + // (fd: number, offset: BigInt, whence: number, newOffset: number) + offset = Number(arguments[1]); // BigInt to Number + whence = arguments[2]; + newOffset = arguments[3]; + } else if (arguments.length == 5) { + // (fd: number, offset_low: number, offset_high: number, whence: number, newOffset: number) + offset = arguments[1]; // offset_low; offset_high is ignored + whence = arguments[3]; + newOffset = arguments[4]; + } else { + throw new Error(`seek: invalid arguments count: ${arguments.length}`); + } + + const context = this.fd2context[fd]; + switch (whence) { + case 0: // SEEK_SET + context.position = offset; + break; + case 1: // SEEK_CUR + context.position += offset; + break; + case 2: // SEEK_END + context.position = context.size + offset; + break; + } + const offsetLowHigh = new Uint32Array(Pdfium.memory.buffer, newOffset, 2); + offsetLowHigh[0] = context.position; + offsetLowHigh[1] = 0; + return 0; + } + + /** + * fd__write + * @param {num} fd + * @param {num} iovs + * @param {num} iovs_len + * @param {num} ret_ptr + */ + write(fd, iovs, iovs_len, ret_ptr) { + const context = this.fd2context[fd]; + let total = 0; + for (let i = 0; i < iovs_len; i++) { + const iov = new Int32Array(Pdfium.memory.buffer, iovs + i * 8, 2); + const ptr = iov[0]; + const len = iov[1]; + const written = context.write(context, new Uint8Array(Pdfium.memory.buffer, ptr, len)); + total += written; + if (written < len) break; + } + const bytes_written = new Uint32Array(Pdfium.memory.buffer, ret_ptr, 1); + bytes_written[0] = written; + return 0; + } + + /** + * fd_read + * @param {num} fd + * @param {num} iovs + * @param {num} iovs_len + * @param {num} ret_ptr + */ + read(fd, iovs, iovs_len, ret_ptr) { + /** @type {FileDescriptorContext} */ + const context = this.fd2context[fd]; + let total = 0; + for (let i = 0; i < iovs_len; i++) { + const iov = new Int32Array(Pdfium.memory.buffer, iovs + i * 8, 2); + const ptr = iov[0]; + const len = iov[1]; + const read = context.read(context, new Uint8Array(Pdfium.memory.buffer, ptr, len)); + total += read; + if (read < len) break; + } + const bytes_read = new Uint32Array(Pdfium.memory.buffer, ret_ptr, 1); + bytes_read[0] = total; + return 0; + } + + sync(fd) { + const context = this.fd2context[fd]; + return context.sync(context); + } + + /** + * __syscall_fstat64 + * @param {num} fd + * @param {num} statbuf + * @returns {num} + */ + fstat(fd, statbuf) { + const context = this.fd2context[fd]; + const buffer = new Int32Array(Pdfium.memory.buffer, statbuf, 92); + buffer[6] = context.size; // st_size + buffer[7] = 0; + return 0; + } + + /** + * __syscall_stat64 + * @param {num} pathnamePtr + * @param {num} statbuf + * @returns {num} + */ + stat64(pathnamePtr, statbuf) { + const fn = StringUtils.utf8BytesToString(new Uint8Array(Pdfium.memory.buffer, pathnamePtr, 2048)); + const funcs = this.fn2context[fn]; + if (funcs) { + const buffer = new Int32Array(Pdfium.memory.buffer, statbuf, 92); + buffer[6] = funcs.size; // st_size + buffer[7] = 0; + return 0; + } + return -1; + } + + /** + * __syscall_getdents64 + * @param {num} fd + * @param {num} dirp struct linux_dirent64 + * @param {num} count + * @returns {num} + */ + getdents64(fd, dirp, count) { + /** @type {DirectoryFileDescriptorContext} */ + const context = this.fd2context[fd]; + const entries = context.entries; + if (entries == null) return -1; // not a directory + context.getdents_position = context.getdents_position || 0; + let written = 0; + const DT_REG = 8, + DT_DIR = 4; + _memset(dirp, 0, count); + for (; context.position < entries.length; context.position++) { + const i = context.position; + let d_type, d_name; + if (entries[i].endsWith('/')) { + d_type = DT_DIR; + d_name = entries[i].substring(0, entries[i].length - 1); + } else { + d_type = DT_REG; + d_name = entries[i]; + } + const d_nameLength = StringUtils.lengthBytesUTF8(d_name) + 1; + const size = 8 + 8 + 2 + 1 + d_nameLength; + if (written + size > count) break; + + const buffer = new Uint8Array(Pdfium.memory.buffer, dirp + written, size); + // d_off + const d_off = written + size; + buffer[8] = d_off & 255; + buffer[9] = (d_off >> 8) & 255; + buffer[10] = (d_off >> 16) & 255; + buffer[11] = (d_off >> 24) & 255; + // d_reclen + buffer[16] = size & 255; + buffer[17] = (size >> 8) & 255; + // d_type + buffer[18] = d_type; + // d_name + StringUtils.stringToUtf8Bytes(d_name, new Uint8Array(Pdfium.memory.buffer, dirp + written + 19, d_nameLength)); + written = d_off; + } + return written; + } +} + +function _error(e) { + return e.stack ? e.stack.toString() : e.toString(); +} + +function _notImplemented(name) { + throw new Error(`${name} is not implemented`); +} + +const fileSystem = new FileSystemEmulator(); + +const emEnv = { + __assert_fail: function (condition, filename, line, func) { + throw new Error(`Assertion failed: ${condition} at ${filename}:${line} (${func})`); + }, + _emscripten_memcpy_js: function (dest, src, num) { + new Uint8Array(Pdfium.memory.buffer).copyWithin(dest, src, src + num); + }, + __syscall_openat: fileSystem.openFile.bind(fileSystem), + __syscall_fstat64: fileSystem.fstat.bind(fileSystem), + __syscall_ftruncate64: function (fd, zero, zero2, zero3) { + _notImplemented('__syscall_ftruncate64'); + }, + __syscall_stat64: fileSystem.stat64.bind(fileSystem), + __syscall_newfstatat: function (dirfd, pathnamePtr, statbuf, flags) { + _notImplemented('__syscall_newfstatat'); + }, + __syscall_lstat64: function (pathnamePtr, statbuf) { + _notImplemented('__syscall_lstat64'); + }, + __syscall_fcntl64: function (fd, cmd, arg) { + _notImplemented('__syscall_fcntl64'); + }, + __syscall_ioctl: function (fd, request, arg) { + _notImplemented('__syscall_ioctl'); + }, + __syscall_getdents64: fileSystem.getdents64.bind(fileSystem), + __syscall_unlinkat: function (dirfd, pathnamePtr, flags) { + _notImplemented('__syscall_unlinkat'); + }, + __syscall_rmdir: function (pathnamePtr) { + _notImplemented('__syscall_rmdir'); + }, + _abort_js: function (what) { + throw new Error(what); + }, + _emscripten_throw_longjmp: function () { + throw Infinity; + }, + _gmtime_js: function (time, tmPtr) { + time = Number(time); + const date = new Date(time * 1000); + const tm = new Int32Array(Pdfium.memory.buffer, tmPtr, 9); + tm[0] = date.getUTCSeconds(); + tm[1] = date.getUTCMinutes(); + tm[2] = date.getUTCHours(); + tm[3] = date.getUTCDate(); + tm[4] = date.getUTCMonth(); + tm[5] = date.getUTCFullYear() - 1900; + tm[6] = date.getUTCDay(); + tm[7] = 0; // dst + tm[8] = 0; // gmtoff + }, + _localtime_js: function (time, tmPtr) { + time = Number(time); + const date = new Date(time * 1000); + const tm = new Int32Array(Pdfium.memory.buffer, tmPtr, 9); + tm[0] = date.getSeconds(); + tm[1] = date.getMinutes(); + tm[2] = date.getHours(); + tm[3] = date.getDate(); + tm[4] = date.getMonth(); + tm[5] = date.getFullYear() - 1900; + tm[6] = date.getDay(); + tm[7] = 0; // dst + tm[8] = 0; // gmtoff + }, + _tzset_js: function () {}, + emscripten_date_now: function () { + return Date.now(); + }, + emscripten_errn: function () { + _notImplemented('emscripten_errn'); + }, + emscripten_resize_heap: function (requestedSizeInBytes) { + const maxHeapSizeInBytes = 2 * 1024 * 1024 * 1024; // 2GB + if (requestedSizeInBytes > maxHeapSizeInBytes) { + console.error( + `emscripten_resize_heap: Cannot enlarge memory, asked for ${requestedPageCount} bytes but limit is ${maxHeapSizeInBytes}` + ); + return false; + } + + const pageSize = 65536; + const oldPageCount = ((Pdfium.memory.buffer.byteLength + pageSize - 1) / pageSize) | 0; + const requestedPageCount = ((requestedSizeInBytes + pageSize - 1) / pageSize) | 0; + const newPageCount = Math.max(oldPageCount * 1.5, requestedPageCount) | 0; + try { + Pdfium.memory.grow(newPageCount - oldPageCount); + console.log(`emscripten_resize_heap: ${oldPageCount} => ${newPageCount}`); + return true; + } catch (e) { + console.error(`emscripten_resize_heap: Failed to resize heap: ${_error(e)}`); + return false; + } + }, + exit: function (status) { + _notImplemented('exit'); + }, + invoke_ii: function (index, a) { + return Pdfium.invokeFunc(index, function (func) { + return func(a); + }); + }, + invoke_iii: function (index, a, b) { + return Pdfium.invokeFunc(index, function (func) { + return func(a, b); + }); + }, + invoke_iiii: function (index, a, b, c) { + return Pdfium.invokeFunc(index, function (func) { + return func(a, b, c); + }); + }, + invoke_iiiii: function (index, a, b, c, d) { + return Pdfium.invokeFunc(index, function (func) { + return func(a, b, c, d); + }); + }, + invoke_v: function (index) { + return Pdfium.invokeFunc(index, function (func) { + func(); + }); + }, + invoke_viii: function (index, a, b, c) { + Pdfium.invokeFunc(index, function (func) { + func(a, b, c); + }); + }, + invoke_viiii: function (index, a, b, c, d) { + Pdfium.invokeFunc(index, function (func) { + func(a, b, c, d); + }); + }, + print: function (text) { + console.log(text); + }, + printErr: function (text) { + console.error(text); + }, +}; + +const wasi = { + proc_exit: function (code) { + _notImplemented('proc_exit'); + }, + environ_sizes_get: function (environCount, environBufSize) { + _notImplemented('environ_sizes_get'); + }, + environ_get: function (environ, environBuf) { + _notImplemented('environ_get'); + }, + fd_close: fileSystem.closeFile.bind(fileSystem), + fd_seek: fileSystem.seek.bind(fileSystem), + fd_write: fileSystem.write.bind(fileSystem), + fd_read: fileSystem.read.bind(fileSystem), + fd_sync: fileSystem.sync.bind(fileSystem), +}; + +/** + * @param {{url: string, password: string|undefined, useProgressiveLoading: boolean|undefined, headers: Object.|undefined, withCredentials: boolean|undefined, progressCallbackId: number|undefined, preferRangeAccess: boolean|undefined}} params + */ +async function loadDocumentFromUrl(params) { + const url = params.url; + const password = params.password || ''; + const useProgressiveLoading = params.useProgressiveLoading || false; + const headers = params.headers || {}; + const withCredentials = params.withCredentials || false; + const progressCallbackId = params.progressCallbackId; + + const response = await fetch(url, { + headers: headers, + mode: 'cors', + credentials: withCredentials ? 'include' : 'same-origin', + redirect: 'follow', + }); + const contentLength = parseInt(response.headers.get('content-length') || '0', 10); + + // If we have progress callback and a valid content length, use streaming + if (progressCallbackId && contentLength > 0 && response.body) { + const reader = response.body.getReader(); + const chunks = []; + let receivedLength = 0; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(value); + receivedLength += value.length; + + // Send progress callback + invokeCallback(progressCallbackId, receivedLength, contentLength); + } + + // Combine chunks into single ArrayBuffer + const data = new Uint8Array(receivedLength); + let position = 0; + for (const chunk of chunks) { + data.set(chunk, position); + position += chunk.length; + } + + return loadDocumentFromData({ + data: data.buffer, + password, + useProgressiveLoading, + }); + } else { + // No progress callback or content-length, just get the data directly + return loadDocumentFromData({ + data: await response.arrayBuffer(), + password, + useProgressiveLoading, + }); + } +} + +/** + * @param {{data: ArrayBuffer, password: string|undefined, useProgressiveLoading: boolean|undefined}} params + */ +function loadDocumentFromData(params) { + const data = params.data; + const password = params.password || ''; + const useProgressiveLoading = params.useProgressiveLoading; + + const sizeThreshold = 1024 * 1024; // 1MB + if (data.byteLength < sizeThreshold) { + const buffer = Pdfium.wasmExports.malloc(data.byteLength); + if (buffer === 0) { + throw new Error('Failed to allocate memory for PDF data (${data.byteLength} bytes)'); + } + new Uint8Array(Pdfium.memory.buffer, buffer, data.byteLength).set(new Uint8Array(data)); + const passwordPtr = StringUtils.allocateUTF8(password); + const docHandle = Pdfium.wasmExports.FPDF_LoadMemDocument(buffer, data.byteLength, passwordPtr); + StringUtils.freeUTF8(passwordPtr); + return _loadDocument(docHandle, useProgressiveLoading, () => Pdfium.wasmExports.free(buffer)); + } + + const tempFileName = params.url ?? '/tmp/temp.pdf'; + fileSystem.registerFileWithData(tempFileName, data); + + const fileNamePtr = StringUtils.allocateUTF8(tempFileName); + const passwordPtr = StringUtils.allocateUTF8(password); + const docHandle = Pdfium.wasmExports.FPDF_LoadDocument(fileNamePtr, passwordPtr); + StringUtils.freeUTF8(passwordPtr); + StringUtils.freeUTF8(fileNamePtr); + return _loadDocument(docHandle, useProgressiveLoading, () => fileSystem.unregisterFile(tempFileName)); +} + +/** @type {Object} */ +const disposers = {}; + +/** @typedef {{face: string, weight: number, italic: boolean, charset: number, pitch_family: number}} FontQuery + * @typedef {Object} FontQueries + */ +/** @type {FontQueries} */ +let lastMissingFonts = {}; + +/** @type {Object} */ +let missingFonts = {}; + +/** + * + * @param {number} docHandle + * @returns {FontQueries} Missing fonts new found. + */ +function _updateMissingFonts(docHandle) { + if (Object.keys(lastMissingFonts).length === 0) return; + + const existing = missingFonts[docHandle] ?? {}; + missingFonts[docHandle] = { ...existing, ...lastMissingFonts }; + const result = lastMissingFonts; + lastMissingFonts = {}; + return result; +} + +function _resetMissingFonts() { + missingFonts = {}; +} + +/** + * @typedef {{docHandle: number,permissions: number, securityHandlerRevision: number, pages: PdfPage[], formHandle: number, formInfo: number, missingFonts: FontQueries}} PdfDocument + * @typedef {{pageIndex: number, width: number, height: number, rotation: number, isLoaded: boolean, bbLeft: number, bbBottom: number}} PdfPage + * @typedef {{errorCode: number, errorCodeStr: string|undefined, message: string}} PdfError + */ + +/** + * @param {number} docHandle + * @param {boolean} useProgressiveLoading + * @param {function():void} onDispose + * @returns {PdfDocument|PdfError} + */ +function _loadDocument(docHandle, useProgressiveLoading, onDispose) { + let formInfo = 0; + let formHandle = 0; + try { + if (!docHandle) { + const error = Pdfium.wasmExports.FPDF_GetLastError(); + const errorStr = _errorMappings[error]; + return { + errorCode: error, + errorCodeStr: _errorMappings[error], + message: `Failed to load document`, + }; + } + + missingFonts[docHandle] = {}; + lastMissingFonts = {}; + + const pageCount = Pdfium.wasmExports.FPDF_GetPageCount(docHandle); + const permissions = Pdfium.wasmExports.FPDF_GetDocPermissions(docHandle); + const securityHandlerRevision = Pdfium.wasmExports.FPDF_GetSecurityHandlerRevision(docHandle); + + const formInfoSize = 35 * 4; + formInfo = Pdfium.wasmExports.malloc(formInfoSize); + const uint32 = new Uint32Array(Pdfium.memory.buffer, formInfo, formInfoSize >> 2); + uint32[0] = 1; // version + formHandle = Pdfium.wasmExports.FPDFDOC_InitFormFillEnvironment(docHandle, formInfo); + + const pages = _loadPagesInLimitedTime(docHandle, 0, useProgressiveLoading ? 1 : null); + if (useProgressiveLoading) { + const firstPage = pages[0]; + for (let i = 1; i < pageCount; i++) { + pages.push({ + pageIndex: i, + width: firstPage.width, + height: firstPage.height, + rotation: firstPage.rotation, + isLoaded: false, + bbLeft: 0, + bbBottom: 0, + }); + } + } + disposers[docHandle] = onDispose; + _updateMissingFonts(docHandle); + + return { + docHandle: docHandle, + permissions: permissions, + securityHandlerRevision: securityHandlerRevision, + pages: pages, + formHandle: formHandle, + formInfo: formInfo, + missingFonts: missingFonts[docHandle], + }; + } catch (e) { + try { + if (formHandle !== 0) Pdfium.wasmExports.FPDFDOC_ExitFormFillEnvironment(formHandle); + } catch (e) {} + Pdfium.wasmExports.free(formInfo); + delete disposers[docHandle]; + onDispose(); + throw e; + } +} + +/** + * @param {number} docHandle + * @param {number} pagesLoadedCountSoFar + * @param {number|null} maxPageCountToLoadAdditionally + * @param {number} timeoutMs + * @returns {PdfPage[]} + */ +function _loadPagesInLimitedTime(docHandle, pagesLoadedCountSoFar, maxPageCountToLoadAdditionally, timeoutMs) { + const pageCount = Pdfium.wasmExports.FPDF_GetPageCount(docHandle); + const end = + maxPageCountToLoadAdditionally == null + ? pageCount + : Math.min(pageCount, pagesLoadedCountSoFar + maxPageCountToLoadAdditionally); + const t = timeoutMs != null ? Date.now() + timeoutMs : null; + /** @type {PdfPage[]} */ + const pages = []; + _resetMissingFonts(); + for (let i = pagesLoadedCountSoFar; i < end; i++) { + const pageHandle = Pdfium.wasmExports.FPDF_LoadPage(docHandle, i); + if (!pageHandle) { + const error = Pdfium.wasmExports.FPDF_GetLastError(); + throw new Error(`FPDF_LoadPage failed (${_getErrorMessage(error)})`); + } + + const rectBuffer = Pdfium.wasmExports.malloc(4 * 4); // FS_RECTF: float[4] + Pdfium.wasmExports.FPDF_GetPageBoundingBox(pageHandle, rectBuffer); + const rect = new Float32Array(Pdfium.memory.buffer, rectBuffer, 4); + const bbLeft = rect[0]; + const bbBottom = rect[3]; + Pdfium.wasmExports.free(rectBuffer); + + pages.push({ + pageIndex: i, + width: Pdfium.wasmExports.FPDF_GetPageWidthF(pageHandle), + height: Pdfium.wasmExports.FPDF_GetPageHeightF(pageHandle), + rotation: Pdfium.wasmExports.FPDFPage_GetRotation(pageHandle), + isLoaded: true, + bbLeft: bbLeft, + bbBottom: bbBottom, + }); + Pdfium.wasmExports.FPDF_ClosePage(pageHandle); + if (t != null && Date.now() > t) { + break; + } + } + _updateMissingFonts(docHandle); + return pages; +} + +/** + * @param {{docHandle: number, loadUnitDuration: number}} params + * @returns {{pages: PdfPage[], missingFonts: FontQueries}} + */ +function loadPagesProgressively(params) { + const { docHandle, firstPageIndex, loadUnitDuration } = params; + const pages = _loadPagesInLimitedTime(docHandle, firstPageIndex, null, loadUnitDuration); + return { pages, missingFonts: missingFonts[docHandle] }; +} + +/** + * + * @param {{docHandle: number, pageIndices: number[]|undefined, currentPagesCount: number}} params + * @returns {{pages: PdfPage[], missingFonts: FontQueries}} + */ +function reloadPages(params) { + const { docHandle, pageIndices } = params; + /** @type {PdfPage[]} */ + const pages = []; + const pageCount = Pdfium.wasmExports.FPDF_GetPageCount(docHandle); + /** @type {number[]} */ + var indicesToLoad = []; + if (pageIndices) { + for (const pageIndex of pageIndices) { + if (pageIndex < 0 || pageIndex >= pageCount) { + throw new Error(`Invalid page index ${pageIndex} (page count: ${pageCount})`); + } + if (pageIndex < currentPagesCount) { + indicesToLoad.push(pageIndex); + } + } + for (let i = currentPagesCount; i < pageCount; i++) { + indicesToLoad.push(i); + } + } else { + for (let i = 0; i < pageCount; i++) { + indicesToLoad.push(i); + } + } + + _resetMissingFonts(); + for (const pageIndex of indicesToLoad) { + const pageHandle = Pdfium.wasmExports.FPDF_LoadPage(docHandle, pageIndex); + if (!pageHandle) { + const error = Pdfium.wasmExports.FPDF_GetLastError(); + throw new Error(`FPDF_LoadPage failed (${_getErrorMessage(error)})`); + } + const rectBuffer = Pdfium.wasmExports.malloc(4 * 4); // FS_RECTF: float[4] + Pdfium.wasmExports.FPDF_GetPageBoundingBox(pageHandle, rectBuffer); + const rect = new Float32Array(Pdfium.memory.buffer, rectBuffer, 4); + const bbLeft = rect[0]; + const bbBottom = rect[3]; + Pdfium.wasmExports.free(rectBuffer); + pages.push({ + pageIndex: pageIndex, + width: Pdfium.wasmExports.FPDF_GetPageWidthF(pageHandle), + height: Pdfium.wasmExports.FPDF_GetPageHeightF(pageHandle), + rotation: Pdfium.wasmExports.FPDFPage_GetRotation(pageHandle), + isLoaded: true, + bbLeft: bbLeft, + bbBottom: bbBottom, + }); + Pdfium.wasmExports.FPDF_ClosePage(pageHandle); + } + return { pages, missingFonts: missingFonts[docHandle] }; +} + +/** + * @param {{formHandle: number, formInfo: number, docHandle: number}} params + */ +function closeDocument(params) { + if (params.formHandle) { + try { + Pdfium.wasmExports.FPDFDOC_ExitFormFillEnvironment(params.formHandle); + } catch (e) {} + } + Pdfium.wasmExports.free(params.formInfo); + Pdfium.wasmExports.FPDF_CloseDocument(params.docHandle); + disposers[params.docHandle](); + delete disposers[params.docHandle]; + delete missingFonts[params.docHandle]; + return { message: 'Document closed' }; +} + +/** + * @typedef {{pageIndex: number, command: string, params: number[]}} PdfDest + * @typedef {{title: string, dest: PdfDest, children: OutlineNode[]}} OutlineNode + */ + +/** + * @param {{docHandle: number}} params + * @return {OutlineNode[]} + */ +function loadOutline(params) { + return { + outline: _getOutlineNodeSiblings( + Pdfium.wasmExports.FPDFBookmark_GetFirstChild(params.docHandle, null), + params.docHandle + ), + }; +} + +/** + * @param {number} bookmark + * @param {number} docHandle + * @return {OutlineNode[]} + */ +function _getOutlineNodeSiblings(bookmark, docHandle) { + /** @type {OutlineNode[]} */ + const siblings = []; + while (bookmark) { + const titleBufSize = Pdfium.wasmExports.FPDFBookmark_GetTitle(bookmark, null, 0); + const titleBuf = Pdfium.wasmExports.malloc(titleBufSize); + Pdfium.wasmExports.FPDFBookmark_GetTitle(bookmark, titleBuf, titleBufSize); + const title = StringUtils.utf16BytesToString(new Uint8Array(Pdfium.memory.buffer, titleBuf, titleBufSize)); + Pdfium.wasmExports.free(titleBuf); + siblings.push({ + title: title, + dest: _pdfDestFromDest(Pdfium.wasmExports.FPDFBookmark_GetDest(docHandle, bookmark), docHandle), + children: _getOutlineNodeSiblings(Pdfium.wasmExports.FPDFBookmark_GetFirstChild(docHandle, bookmark), docHandle), + }); + bookmark = Pdfium.wasmExports.FPDFBookmark_GetNextSibling(docHandle, bookmark); + } + return siblings; +} + +/** + * @param {{docHandle: number, pageIndex: number}} params + * @return {number} Page handle + */ +function loadPage(params) { + const pageHandle = Pdfium.wasmExports.FPDF_LoadPage(params.docHandle, params.pageIndex); + if (!pageHandle) { + throw new Error(`Failed to load page ${params.pageIndex} from document ${params.docHandle}`); + } + return { pageHandle: pageHandle }; +} + +/** + * @param {{pageHandle: number}} params + */ +function closePage(params) { + Pdfium.wasmExports.FPDF_ClosePage(params.pageHandle); + return { message: 'Page closed' }; +} + +/** + * + * @param {{ + * docHandle: number, + * pageIndex: number, + * x: number, + * y: number, + * width: number, + * height: number, + * fullWidth: number, + * fullHeight: number, + * backgroundColor: number, + * rotation: number, + * annotationRenderingMode: number, + * flags: number, + * formHandle: number + * }} params + * @returns {{ + * imageData: ArrayBuffer, + * width: number, + * height: number, + * missingFonts: FontQueries + * }} + */ +function renderPage(params) { + const { + docHandle, + pageIndex, + x = 0, + y = 0, + width = 800, + height = 600, + fullWidth = width, + fullHeight = height, + backgroundColor, + rotation, + annotationRenderingMode = 0, + flags = 0, + formHandle, + } = params; + + let pageHandle = 0; + let bufferPtr = 0; + let bitmap = 0; + + try { + _resetMissingFonts(); + pageHandle = Pdfium.wasmExports.FPDF_LoadPage(docHandle, pageIndex); + if (!pageHandle) { + throw new Error(`Failed to load page ${pageIndex} from document ${docHandle}`); + } + + const bufferSize = width * height * 4; + bufferPtr = Pdfium.wasmExports.malloc(bufferSize); + if (!bufferPtr) { + throw new Error('Failed to allocate memory for rendering'); + } + const FPDFBitmap_BGRA = 4; + bitmap = Pdfium.wasmExports.FPDFBitmap_CreateEx(width, height, FPDFBitmap_BGRA, bufferPtr, width * 4); + if (!bitmap) { + throw new Error('Failed to create bitmap for rendering'); + } + + Pdfium.wasmExports.FPDFBitmap_FillRect(bitmap, 0, 0, width, height, backgroundColor); + + const FPDF_ANNOT = 1; + const PdfAnnotationRenderingMode_none = 0; + const PdfAnnotationRenderingMode_annotationAndForms = 2; + const premultipliedAlpha = 0x80000000; + + const pdfiumFlags = + (flags & 0xffff) | (annotationRenderingMode !== PdfAnnotationRenderingMode_none ? FPDF_ANNOT : 0); + Pdfium.wasmExports.FPDF_RenderPageBitmap(bitmap, pageHandle, -x, -y, fullWidth, fullHeight, rotation, pdfiumFlags); + + if (formHandle && annotationRenderingMode == PdfAnnotationRenderingMode_annotationAndForms) { + Pdfium.wasmExports.FPDF_FFLDraw(formHandle, bitmap, pageHandle, -x, -y, fullWidth, fullHeight, rotation, flags); + } + const src = new Uint8Array(Pdfium.memory.buffer, bufferPtr, bufferSize); + let copiedBuffer = new ArrayBuffer(bufferSize); + let dest = new Uint8Array(copiedBuffer); + if (flags & premultipliedAlpha) { + for (let i = 0; i < src.length; i += 4) { + const a = src[i + 3]; + dest[i] = (src[i] * a + 128) >> 8; + dest[i + 1] = (src[i + 1] * a + 128) >> 8; + dest[i + 2] = (src[i + 2] * a + 128) >> 8; + dest[i + 3] = a; + } + } else { + dest.set(src); + } + + _updateMissingFonts(docHandle); + + return { + result: { + imageData: copiedBuffer, + width: width, + height: height, + missingFonts: missingFonts[docHandle], + }, + transfer: [copiedBuffer], + }; + } finally { + Pdfium.wasmExports.FPDF_ClosePage(pageHandle); + Pdfium.wasmExports.FPDFBitmap_Destroy(bitmap); + Pdfium.wasmExports.free(bufferPtr); + } +} + +function _memset(ptr, value, num) { + const buffer = new Uint8Array(Pdfium.memory.buffer, ptr, num); + for (let i = 0; i < num; i++) { + buffer[i] = value; + } +} + +/** + * + * @param {{pageIndex: number, docHandle: number}} params + * @returns {{fullText: string, charRects: number[][], missingFonts: FontQueries}} + */ +function loadText(params) { + _resetMissingFonts(); + const { pageIndex, docHandle } = params; + const pageHandle = Pdfium.wasmExports.FPDF_LoadPage(docHandle, pageIndex); + const textPage = Pdfium.wasmExports.FPDFText_LoadPage(pageHandle); + if (textPage == null) return { fullText: '' }; + + const count = Pdfium.wasmExports.FPDFText_CountChars(textPage); + let fullText = ''; + + const rectBuffer = Pdfium.wasmExports.malloc(8 * 4); // double[4] + const rect = new Float64Array(Pdfium.memory.buffer, rectBuffer, 4); + let charRects = []; + for (let i = 0; i < count; i++) { + fullText += String.fromCodePoint(Pdfium.wasmExports.FPDFText_GetUnicode(textPage, i)); + Pdfium.wasmExports.FPDFText_GetCharBox( + textPage, + i, + rectBuffer, // L + rectBuffer + 8 * 2, // R + rectBuffer + 8 * 3, // B + rectBuffer + 8 // T + ); + charRects.push(Array.from(rect)); + } + Pdfium.wasmExports.free(rectBuffer); + + Pdfium.wasmExports.FPDFText_ClosePage(textPage); + Pdfium.wasmExports.FPDF_ClosePage(pageHandle); + + _updateMissingFonts(docHandle); + return { fullText, charRects, missingFonts: missingFonts[docHandle] }; +} + +/** + * @typedef {{rects: number[][], dest: url: string}} PdfUrlLink + * @typedef {{rects: number[][], dest: PdfDest}} PdfDestLink + */ + +/** + * @param {{docHandle: number, pageIndex: number, enableAutoLinkDetection: boolean}} params + * @returns {{links: Array}} + */ +function loadLinks(params) { + const links = [..._loadAnnotLinks(params), ...(params.enableAutoLinkDetection ? _loadWebLinks(params) : [])]; + return { + links: links, + }; +} + +/** + * @param {{docHandle: number, pageIndex: number, enableAutoLinkDetection: boolean}} params + * @returns {Array} + */ +function _loadWebLinks(params) { + const { pageIndex, docHandle } = params; + const pageHandle = Pdfium.wasmExports.FPDF_LoadPage(docHandle, pageIndex); + const textPage = Pdfium.wasmExports.FPDFText_LoadPage(pageHandle); + if (textPage == null) return []; + const linkPage = Pdfium.wasmExports.FPDFLink_LoadWebLinks(textPage); + if (linkPage == null) return []; + + const links = []; + const count = Pdfium.wasmExports.FPDFLink_CountWebLinks(linkPage); + const rectBuffer = Pdfium.wasmExports.malloc(8 * 4); // double[4] + for (let i = 0; i < count; i++) { + const rectCount = Pdfium.wasmExports.FPDFLink_CountRects(linkPage, i); + const rects = []; + for (let j = 0; j < rectCount; j++) { + Pdfium.wasmExports.FPDFLink_GetRect(linkPage, i, j, rectBuffer, rectBuffer + 8, rectBuffer + 16, rectBuffer + 24); + rects.push(Array.from(new Float64Array(Pdfium.memory.buffer, rectBuffer, 4))); + } + links.push({ + rects: rects, + url: _getLinkUrl(linkPage, i), + }); + } + Pdfium.wasmExports.free(rectBuffer); + Pdfium.wasmExports.FPDFLink_CloseWebLinks(linkPage); + Pdfium.wasmExports.FPDFText_ClosePage(textPage); + Pdfium.wasmExports.FPDF_ClosePage(pageHandle); + return links; +} + +/** + * @param {number} linkPage + * @param {number} linkIndex + * @returns {string} + */ +function _getLinkUrl(linkPage, linkIndex) { + const urlLength = Pdfium.wasmExports.FPDFLink_GetURL(linkPage, linkIndex, null, 0); + const urlBuffer = Pdfium.wasmExports.malloc(urlLength * 2); + Pdfium.wasmExports.FPDFLink_GetURL(linkPage, linkIndex, urlBuffer, urlLength); + const url = StringUtils.utf16BytesToString(new Uint8Array(Pdfium.memory.buffer, urlBuffer, urlLength * 2)); + Pdfium.wasmExports.free(urlBuffer); + return url; +} + +/** + * @param {{docHandle: number, pageIndex: number}} params + * @returns {Array} + */ +function _loadAnnotLinks(params) { + const { pageIndex, docHandle } = params; + const pageHandle = Pdfium.wasmExports.FPDF_LoadPage(docHandle, pageIndex); + const count = Pdfium.wasmExports.FPDFPage_GetAnnotCount(pageHandle); + const rectF = Pdfium.wasmExports.malloc(4 * 4); + const links = []; + for (let i = 0; i < count; i++) { + const annot = Pdfium.wasmExports.FPDFPage_GetAnnot(pageHandle, i); + Pdfium.wasmExports.FPDFAnnot_GetRect(annot, rectF); + const [l, t, r, b] = new Float32Array(Pdfium.memory.buffer, rectF, 4); + const rect = [l, t > b ? t : b, r, t > b ? b : t]; + + const annotation = _getAnnotationContent(annot); + + const dest = _processAnnotDest(annot, docHandle); + if (dest) { + links.push({ + rects: [rect], + dest: _pdfDestFromDest(dest, docHandle), + annotation: annotation, + }); + } else { + const url = _processAnnotLink(annot, docHandle); + if (url || annotation) { + links.push({ + rects: [rect], + url: url, + annotation: annotation, + }); + } + } + Pdfium.wasmExports.FPDFPage_CloseAnnot(annot); + } + Pdfium.wasmExports.free(rectF); + Pdfium.wasmExports.FPDF_ClosePage(pageHandle); + return links; +} + +/** + * @typedef {{title: string|null, content: string|null, modificationDate: string|null, creationDate: string|null, subject: string|null}} PdfAnnotationContent + */ + +/** + * Get annotation content with all metadata fields + * @param {number} annot Annotation handle + * @returns {PdfAnnotationContent|null} Annotation object or null if no content + */ +function _getAnnotationContent(annot) { + const title = _getAnnotField('T', annot); // Title (Author) + const content = _getAnnotField('Contents', annot); // Content + const modDate = _getAnnotField('M', annot); // Modification date + const creationDate = _getAnnotField('CreationDate', annot); // Creation date + const subject = _getAnnotField('Subj', annot); // Subject + if (!title && !content && !modDate && !creationDate && !subject) { + return null; + } + + return { + title: title, + content: content, + modificationDate: modDate, + creationDate: creationDate, + subject: subject, + }; +} + +/** + * Helper function to get annotation field value + * @param {string} fieldName PDF annotation field name + * @returns {string|null} + */ +function _getAnnotField(fieldName, annot) { + const key = StringUtils.allocateUTF8(fieldName); + try { + const length = Pdfium.wasmExports.FPDFAnnot_GetStringValue(annot, key, null, 0); + if (length <= 0) return null; + + const buffer = Pdfium.wasmExports.malloc(length * 2); + try { + Pdfium.wasmExports.FPDFAnnot_GetStringValue(annot, key, buffer, length); + const value = StringUtils.utf16BytesToString(new Uint8Array(Pdfium.memory.buffer, buffer, length * 2)); + return value && value.trim() !== '' ? value : null; + } finally { + Pdfium.wasmExports.free(buffer); + } + } finally { + StringUtils.freeUTF8(key); + } +} + +/** + * + * @param {number} annot + * @param {number} docHandle + * @returns {number|null} Dest + */ +function _processAnnotDest(annot, docHandle) { + const link = Pdfium.wasmExports.FPDFAnnot_GetLink(annot); + + // firstly check the direct dest + const dest = Pdfium.wasmExports.FPDFLink_GetDest(docHandle, link); + if (dest) return dest; + + const action = Pdfium.wasmExports.FPDFLink_GetAction(link); + if (!action) return null; + const PDFACTION_GOTO = 1; + switch (Pdfium.wasmExports.FPDFAction_GetType(action)) { + case PDFACTION_GOTO: + return Pdfium.wasmExports.FPDFAction_GetDest(docHandle, action); + default: + return null; + } +} + +/** + * @param {number} annot + * @param {number} docHandle + * @returns {string|null} URI + */ +function _processAnnotLink(annot, docHandle) { + const link = Pdfium.wasmExports.FPDFAnnot_GetLink(annot); + const action = Pdfium.wasmExports.FPDFLink_GetAction(link); + if (!action) return null; + const PDFACTION_URI = 3; + switch (Pdfium.wasmExports.FPDFAction_GetType(action)) { + case PDFACTION_URI: + const size = Pdfium.wasmExports.FPDFAction_GetURIPath(docHandle, action, null, 0); + const buf = Pdfium.wasmExports.malloc(size); + Pdfium.wasmExports.FPDFAction_GetURIPath(docHandle, action, buf, size); + const uri = StringUtils.utf8BytesToString(new Uint8Array(Pdfium.memory.buffer, buf, size)); + Pdfium.wasmExports.free(buf); + return uri; + default: + return null; + } +} + +/// [PDF 32000-1:2008, 12.3.2.2 Explicit Destinations, Table 151](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf#page=374) +const pdfDestCommands = ['unknown', 'xyz', 'fit', 'fitH', 'fitV', 'fitR', 'fitB', 'fitBH', 'fitBV']; + +/** + * @param {number} dest + * @param {number} docHandle + * @returns {PdfDest|null} + */ +function _pdfDestFromDest(dest, docHandle) { + if (dest === 0) return null; + const buf = Pdfium.wasmExports.malloc(40); + const pageIndex = Pdfium.wasmExports.FPDFDest_GetDestPageIndex(docHandle, dest); + const type = Pdfium.wasmExports.FPDFDest_GetView(dest, buf, buf + 4); + const [count] = new Int32Array(Pdfium.memory.buffer, buf, 1); + const params = Array.from(new Float32Array(Pdfium.memory.buffer, buf + 4, count)); + Pdfium.wasmExports.free(buf); + if (type !== 0) { + return { + pageIndex, + command: pdfDestCommands[type], + params, + }; + } + return null; +} + +/** + * Setup the system font info in PDFium. + */ +function _initializeFontEnvironment() { + // kBase14FontNames + const fontNamesToIgnore = { + Courier: true, + 'Courier-Bold': true, + 'Courier-BoldOblique': true, + 'Courier-Oblique': true, + Helvetica: true, + 'Helvetica-Bold': true, + 'Helvetica-BoldOblique': true, + 'Helvetica-Oblique': true, + 'Times-Roman': true, + 'Times-Bold': true, + 'Times-BoldItalic': true, + 'Times-Italic': true, + Symbol: true, + ZapfDingbats: true, + }; + + // load the default system font info and modify only MapFont (index=3) entry with our one, which + // wraps the original function and adds our custom logic + const sysFontInfoBuffer = Pdfium.wasmExports.FPDF_GetDefaultSystemFontInfo(); + const sysFontInfo = new Int32Array(Pdfium.memory.buffer, sysFontInfoBuffer, 9); // struct _FPDF_SYSFONTINFO + + // void* MapFont( + // struct _FPDF_SYSFONTINFO* pThis, + // int weight, + // FPDF_BOOL bItalic, + // int charset, + // int pitch_family, + // const char* face, + // FPDF_BOOL* bExact); + const mapFont = sysFontInfo[3]; + sysFontInfo[3] = Pdfium.addFunction((pThis, weight, bItalic, charset, pitchFamily, face, bExact) => { + const result = Pdfium.invokeFunc(mapFont, (func) => + func(sysFontInfoBuffer, weight, bItalic, charset, pitchFamily, face, bExact) + ); + if (!result) { + // the font face is missing + const faceName = StringUtils.utf8BytesToString(new Uint8Array(Pdfium.memory.buffer, face)); + if (fontNamesToIgnore[faceName] || lastMissingFonts[faceName]) return 0; + lastMissingFonts[faceName] = { + face: faceName, + weight: weight, + italic: !!bItalic, + charset: charset, + pitchFamily: pitchFamily, + }; + } + return result; + }, 'iiiiiiii'); + + // when registering a new SetSystemFontInfo, the previous one is automatically released + // and the only last one remains on memory + Pdfium.wasmExports.FPDF_SetSystemFontInfo(sysFontInfoBuffer); +} + +/** + * Reload fonts in PDFium. + * + * The function is based on the fact that PDFium reloads all the fonts when FPDF_SetSystemFontInfo is called. + */ +function reloadFonts() { + console.log('Reloading system fonts in PDFium...'); + _initializeFontEnvironment(); + return { message: 'Fonts reloaded' }; +} +/** + * @type {{[face: string]: string}} + */ +const fontFileNames = {}; +let fontFilesId = 0; + +/** + * Add font data to the file system. + * @param {{face: string, data: ArrayBuffer}} params + */ +function addFontData(params) { + console.log(`Adding font data for face: ${params.face}`); + const { face, data } = params; + fontFileNames[face] ??= `font_${++fontFilesId}.ttf`; + fileSystem.registerFileWithData(`/usr/share/fonts/${fontFileNames[face]}`, data); + fileSystem.registerFile('/usr/share/fonts', { entries: Object.values(fontFileNames) }); + return { message: `Font ${face} added`, face: face, fileName: fontFileNames[face] }; +} + +function clearAllFontData() { + console.log(`Clearing all font data`); + for (const face in fontFileNames) { + const fileName = fontFileNames[face]; + fileSystem.unregisterFile(`/usr/share/fonts/${fileName}`); + } + fileSystem.registerFile('/usr/share/fonts', { entries: [] }); + fontFileNames = {}; + return { message: 'All font data cleared' }; +} + +/** + * Assemble the document (apply page manipulations if any) + * @param {{docHandle: number, pageIndices: number[]|undefined, importedPages: Object.|undefined, rotations: (number|null)[]|undefined}} params + * @returns {{modified: boolean}} + */ +function assemble(params) { + const { docHandle, pageIndices, importedPages, rotations } = params; + + // If no page indices specified, no modifications needed + if (!pageIndices || pageIndices.length === 0) { + return { modified: false }; + } + + const originalLength = Pdfium.wasmExports.FPDF_GetPageCount(docHandle); + + // Check if there are any changes + let hasChanges = pageIndices.length !== originalLength; + if (!hasChanges) { + for (let i = 0; i < pageIndices.length; i++) { + if (pageIndices[i] !== i) { + hasChanges = true; + break; + } + } + } + + // Check for rotation changes + if (!hasChanges && rotations) { + for (let i = 0; i < rotations.length; i++) { + if (rotations[i] != null) { + hasChanges = true; + break; + } + } + } + + if (!hasChanges) { + return { modified: false }; + } + + // Perform the shuffle using the PDFium page manipulation functions + _shuffleInPlaceAccordingToIndices(docHandle, pageIndices, originalLength, importedPages); + + // Apply rotations if specified + if (rotations) { + for (let i = 0; i < rotations.length; i++) { + const rotation = rotations[i]; + if (rotation != null) { + const page = Pdfium.wasmExports.FPDF_LoadPage(docHandle, i); + Pdfium.wasmExports.FPDFPage_SetRotation(page, rotation); + Pdfium.wasmExports.FPDF_ClosePage(page); + } + } + } + + return { modified: true }; +} + +/** + * Internal class to track page tokens during shuffling + */ +class _ArrayOfItemsToken { + /** + * @param {number|null} originalIndex + * @param {boolean} isOriginal + */ + constructor(originalIndex, isOriginal) { + this.originalIndex = originalIndex; + this.isOriginal = isOriginal; + } +} + +/** + * Shuffle pages in place according to the given list of resulting item indices + * @param {number} docHandle Document handle + * @param {number[]} resultingItemIndices Array of page indices representing the desired order + * @param {number} originalLength Original number of pages + * @param {Object.|undefined} importedPages Map of negative indices to import info + */ +function _shuffleInPlaceAccordingToIndices(docHandle, resultingItemIndices, originalLength, importedPages) { + if (resultingItemIndices.length === 0) { + if (originalLength > 0) { + _removePages(docHandle, 0, originalLength); + } + return; + } + + const tokens = []; + for (let i = 0; i < originalLength; i++) { + tokens.push(new _ArrayOfItemsToken(i, true)); + } + + // Count usage of each original page + const usageCounts = new Array(originalLength).fill(0); + for (let i = 0; i < resultingItemIndices.length; i++) { + const index = resultingItemIndices[i]; + if (index >= 0) { + if (index >= originalLength) { + throw new Error(`resultingItemIndices[${i}] = ${index} is out of range for current length ${originalLength}`); + } + usageCounts[index]++; + } + } + + // Remove unused pages (from end to beginning to maintain indices) + for (let i = originalLength - 1; i >= 0; i--) { + if (usageCounts[i] === 0) { + _removePages(docHandle, i, 1); + tokens.splice(i, 1); + } + } + + const placedCounts = new Array(originalLength).fill(0); + let currentIndex = 0; + + while (currentIndex < resultingItemIndices.length) { + if (currentIndex > tokens.length) { + throw new Error(`Destination index ${currentIndex} is out of range for current length ${tokens.length}.`); + } + + const target = resultingItemIndices[currentIndex]; + if (target >= 0) { + const isFirst = placedCounts[target] === 0; + if (isFirst) { + // Find the original page + let fromIndex = -1; + for (let i = 0; i < tokens.length; i++) { + if (tokens[i].originalIndex === target && tokens[i].isOriginal) { + fromIndex = i; + break; + } + } + if (fromIndex === -1) { + throw new Error(`Item at index ${target} could not be found for initial placement.`); + } + + // Try to find consecutive pages to move as a chunk + let chunkLength = 1; + while (currentIndex + chunkLength < resultingItemIndices.length && fromIndex + chunkLength < tokens.length) { + const nextTarget = resultingItemIndices[currentIndex + chunkLength]; + if (nextTarget < 0 || placedCounts[nextTarget] > 0) break; + const nextToken = tokens[fromIndex + chunkLength]; + if (!nextToken.isOriginal || nextToken.originalIndex !== nextTarget) break; + chunkLength++; + } + + let placementIndex = currentIndex; + if (fromIndex !== currentIndex) { + const removalIndices = []; + for (let offset = 0; offset < chunkLength; offset++) { + removalIndices.push(fromIndex + offset); + } + + _movePages(docHandle, fromIndex, currentIndex, chunkLength); + + // Update tokens + const removedTokens = []; + for (let i = removalIndices.length - 1; i >= 0; i--) { + removedTokens.unshift(tokens.splice(removalIndices[i], 1)[0]); + } + + let insertIndex = currentIndex; + for (const index of removalIndices) { + if (index < currentIndex) { + insertIndex--; + } + } + if (insertIndex < 0) insertIndex = 0; + if (insertIndex > tokens.length) insertIndex = tokens.length; + tokens.splice(insertIndex, 0, ...removedTokens); + placementIndex = insertIndex; + } + + for (let offset = 0; offset < chunkLength; offset++) { + const token = tokens[placementIndex + offset]; + if (token.originalIndex !== null) { + placedCounts[token.originalIndex]++; + } + } + currentIndex += chunkLength; + continue; + } else { + // Duplicate page + let sourceIndex = -1; + for (let i = 0; i < tokens.length; i++) { + if (tokens[i].originalIndex === target) { + sourceIndex = i; + break; + } + } + if (sourceIndex === -1) { + throw new Error(`Item at index ${target} could not be found for duplication.`); + } + _duplicatePages(docHandle, sourceIndex, currentIndex, 1); + tokens.splice(currentIndex, 0, new _ArrayOfItemsToken(target, false)); + placedCounts[target]++; + } + } else { + // Negative index means importing from another document + if (!importedPages || !importedPages[target]) { + throw new Error(`Imported page info not found for negative index ${target}`); + } + const importInfo = importedPages[target]; + _insertImportedPage(docHandle, importInfo.docHandle, importInfo.pageNumber, currentIndex); + tokens.splice(currentIndex, 0, new _ArrayOfItemsToken(null, false)); + } + currentIndex++; + } + + const expectedLength = resultingItemIndices.length; + if (tokens.length > expectedLength) { + const extra = tokens.length - expectedLength; + _removePages(docHandle, expectedLength, extra); + tokens.splice(expectedLength, extra); + } else if (tokens.length < expectedLength) { + throw new Error(`Internal length mismatch after shuffling (expected ${expectedLength}, got ${tokens.length}).`); + } +} + +/** + * Move pages within a document + * @param {number} docHandle Document handle + * @param {number} fromIndex Starting index of pages to move + * @param {number} toIndex Destination index + * @param {number} count Number of pages to move + */ +function _movePages(docHandle, fromIndex, toIndex, count) { + const pageIndices = Pdfium.wasmExports.malloc(count * 4); // Int32 array + const pageIndicesView = new Int32Array(Pdfium.memory.buffer, pageIndices, count); + for (let i = 0; i < count; i++) { + pageIndicesView[i] = fromIndex + i; + } + Pdfium.wasmExports.FPDF_MovePages(docHandle, pageIndices, count, toIndex); + Pdfium.wasmExports.free(pageIndices); +} + +/** + * Remove pages from a document + * @param {number} docHandle Document handle + * @param {number} index Starting index + * @param {number} count Number of pages to remove + */ +function _removePages(docHandle, index, count) { + for (let i = count - 1; i >= 0; i--) { + Pdfium.wasmExports.FPDFPage_Delete(docHandle, index + i); + } +} + +/** + * Duplicate pages within a document + * @param {number} docHandle Document handle + * @param {number} fromIndex Index of page to duplicate + * @param {number} toIndex Destination index for the duplicate + * @param {number} count Number of pages to duplicate + */ +function _duplicatePages(docHandle, fromIndex, toIndex, count) { + const pageIndices = Pdfium.wasmExports.malloc(count * 4); // Int32 array + const pageIndicesView = new Int32Array(Pdfium.memory.buffer, pageIndices, count); + for (let i = 0; i < count; i++) { + pageIndicesView[i] = fromIndex + i; + } + Pdfium.wasmExports.FPDF_ImportPagesByIndex(docHandle, docHandle, pageIndices, count, toIndex); + Pdfium.wasmExports.free(pageIndices); +} + +/** + * Insert a page from another document + * @param {number} destDocHandle Destination document handle + * @param {number} srcDocHandle Source document handle + * @param {number} srcPageIndex Source page index (0-based) + * @param {number} destIndex Destination index + */ +function _insertImportedPage(destDocHandle, srcDocHandle, srcPageIndex, destIndex) { + const pageIndices = Pdfium.wasmExports.malloc(4); // Int32 for one page + const pageIndicesView = new Int32Array(Pdfium.memory.buffer, pageIndices, 1); + pageIndicesView[0] = srcPageIndex; + Pdfium.wasmExports.FPDF_ImportPagesByIndex(destDocHandle, srcDocHandle, pageIndices, 1, destIndex); + Pdfium.wasmExports.free(pageIndices); +} + +/** + * Encode PDF document to bytes + * @param {{docHandle: number, incremental: boolean, removeSecurity: boolean}} params + * @returns {{data: ArrayBuffer}} + */ +function encodePdf(params) { + const { docHandle, incremental = false, removeSecurity = false } = params; + + let buffer = new Uint8Array(1024 * 1024); // Start with 1MB buffer + let totalSize = 0; + + // Create a callback function that will be called by PDFium to write data + const writeCallback = Pdfium.addFunction((pThis, pData, size) => { + void pThis; // Suppress unused parameter warning + + // Grow buffer if needed + if (totalSize + size > buffer.length) { + const newSize = Math.max(buffer.length * 2, totalSize + size); + const newBuffer = new Uint8Array(newSize); + newBuffer.set(buffer.subarray(0, totalSize)); + buffer = newBuffer; + } + + // Copy data directly into buffer + const chunk = new Uint8Array(Pdfium.memory.buffer, pData, size); + buffer.set(chunk, totalSize); + totalSize += size; + + return size; + }, 'iiii'); + + try { + const fileWriteSize = 8; // sizeof(FPDF_FILEWRITE): version(4) + WriteBlock(4) + const fileWrite = Pdfium.wasmExports.malloc(fileWriteSize); + const fileWriteView = new Int32Array(Pdfium.memory.buffer, fileWrite, 2); + fileWriteView[0] = 1; // version + fileWriteView[1] = writeCallback; // WriteBlock function pointer + + // Determine flags based on parameters + let flags; + if (removeSecurity) { + flags = 3; // FPDF_SAVE_NO_SECURITY(3) + } else { + flags = incremental ? 1 : 2; // FPDF_INCREMENTAL(1) or FPDF_NO_INCREMENTAL(2) + } + + const result = Pdfium.wasmExports.FPDF_SaveAsCopy(docHandle, fileWrite, flags); + Pdfium.wasmExports.free(fileWrite); + + if (!result) { + throw new Error('FPDF_SaveAsCopy failed'); + } + + // Trim buffer to actual size + const combined = buffer.subarray(0, totalSize); + + return { + result: { data: combined.buffer }, + transfer: [combined.buffer], + }; + } finally { + Pdfium.removeFunction(writeCallback); + } +} + +/** + * Create a new empty PDF document + * @returns {PdfDocument|PdfError} + */ +function createNewDocument() { + const docHandle = Pdfium.wasmExports.FPDF_CreateNewDocument(); + return _loadDocument(docHandle, false, () => {}); +} + +/** + * Create a PDF document from JPEG data + * @param {Object} params Parameters object + * @param {ArrayBuffer} params.jpegData JPEG image data + * @param {number} params.width Page width in PDF units + * @param {number} params.height Page height in PDF units + * @returns {PdfDocument|PdfError} + */ +function createDocumentFromJpegData(params) { + const { jpegData, width, height } = params; + + if (!jpegData || !(jpegData instanceof ArrayBuffer)) { + return { errorCode: -1, errorCodeStr: 'Invalid JPEG data' }; + } + if (typeof width !== 'number' || width <= 0) { + return { errorCode: -1, errorCodeStr: 'Invalid width' }; + } + if (typeof height !== 'number' || height <= 0) { + return { errorCode: -1, errorCodeStr: 'Invalid height' }; + } + + // Create a new PDF document + const docHandle = Pdfium.wasmExports.FPDF_CreateNewDocument(); + if (!docHandle) { + return { errorCode: -1, errorCodeStr: 'Failed to create PDF document' }; + } + + // Create a new page + const pageHandle = Pdfium.wasmExports.FPDFPage_New(docHandle, 0, width, height); + if (!pageHandle) { + Pdfium.wasmExports.FPDF_CloseDocument(docHandle); + return { errorCode: -1, errorCodeStr: 'Failed to create PDF page' }; + } + + // Create an image object + const imageObj = Pdfium.wasmExports.FPDFPageObj_NewImageObj(docHandle); + if (!imageObj) { + Pdfium.wasmExports.FPDF_ClosePage(pageHandle); + Pdfium.wasmExports.FPDF_CloseDocument(docHandle); + return { errorCode: -1, errorCodeStr: 'Failed to create image object' }; + } + + // Create a FPDF_FILEACCESS structure in WASM memory + const fileAccessSize = 12; // sizeof(FPDF_FILEACCESS) - 3 pointers (each 4 bytes in wasm32) + const fileAccessPtr = Pdfium.wasmExports.malloc(fileAccessSize); + if (!fileAccessPtr) { + Pdfium.wasmExports.FPDF_ClosePage(pageHandle); + Pdfium.wasmExports.FPDF_CloseDocument(docHandle); + return { errorCode: -1, errorCodeStr: 'Failed to allocate file access structure' }; + } + + // Set up file access structure + const fa = new Uint32Array(Pdfium.memory.buffer, fileAccessPtr, fileAccessSize >> 2); + fa[0] = jpegData.byteLength; // m_FileLen + const getBlockCallback = (param, position, pBuf, size) => { + const toCopy = Math.min(size, jpegData.byteLength - position); + const src = new Uint8Array(jpegData, position, toCopy); + const dst = new Uint8Array(Pdfium.memory.buffer, pBuf, toCopy); + dst.set(src); + return toCopy; + }; + const callbackIndex = Pdfium.addFunction(getBlockCallback, 'iiiii'); + fa[1] = callbackIndex; // m_GetBlock function pointer + + // Allocate page array (pointer to single page handle) + const pageArrayPtr = Pdfium.wasmExports.malloc(4); + if (!pageArrayPtr) { + Pdfium.removeFunction(callbackIndex); + Pdfium.wasmExports.free(fileAccessPtr); + Pdfium.wasmExports.FPDF_ClosePage(pageHandle); + Pdfium.wasmExports.FPDF_CloseDocument(docHandle); + return { errorCode: -1, errorCodeStr: 'Failed to allocate page array' }; + } + new Int32Array(Pdfium.memory.buffer, pageArrayPtr, 1)[0] = pageHandle; + + // Load JPEG data into the image object + const loadResult = Pdfium.wasmExports.FPDFImageObj_LoadJpegFileInline(pageArrayPtr, 1, imageObj, fileAccessPtr); + Pdfium.wasmExports.free(pageArrayPtr); + Pdfium.removeFunction(callbackIndex); + Pdfium.wasmExports.free(fileAccessPtr); + + if (!loadResult) { + Pdfium.wasmExports.FPDF_ClosePage(pageHandle); + Pdfium.wasmExports.FPDF_CloseDocument(docHandle); + return { errorCode: -1, errorCodeStr: 'Failed to load JPEG data into image object' }; + } + + // Set image transformation matrix to fill the page + const setMatrixResult = Pdfium.wasmExports.FPDFImageObj_SetMatrix( + imageObj, + width, // a (horizontal scaling) + 0, // b (horizontal skewing) + 0, // c (vertical skewing) + height, // d (vertical scaling) + 0, // e (horizontal translation) + 0 // f (vertical translation) + ); + + if (!setMatrixResult) { + Pdfium.wasmExports.FPDF_ClosePage(pageHandle); + Pdfium.wasmExports.FPDF_CloseDocument(docHandle); + return { errorCode: -1, errorCodeStr: 'Failed to set image matrix' }; + } + + // Insert the image object into the page + Pdfium.wasmExports.FPDFPage_InsertObject(pageHandle, imageObj); + + // Generate page content + const generateResult = Pdfium.wasmExports.FPDFPage_GenerateContent(pageHandle); + if (!generateResult) { + Pdfium.wasmExports.FPDF_ClosePage(pageHandle); + Pdfium.wasmExports.FPDF_CloseDocument(docHandle); + return { errorCode: -1, errorCodeStr: 'Failed to generate page content' }; + } + + // Close the page (transfers ownership to document) + Pdfium.wasmExports.FPDF_ClosePage(pageHandle); + + // Load and return the document + return _loadDocument(docHandle, false, () => {}); +} + +/** + * Set pixel data for an image object + * @param {number} pageHandle Page handle + * @param {number} imageObj Image object handle + * @param {ArrayBuffer} pixels BGRA8888 pixel data + * @param {number} pixelWidth Image width in pixels + * @param {number} pixelHeight Image height in pixels + * @returns {PdfDocument|PdfError} + */ +function _setImageObjPixels(pageHandle, imageObj, pixels, pixelWidth, pixelHeight) { + const pixelDataPtr = Pdfium.wasmExports.malloc(pixels.byteLength); + if (!pixelDataPtr) throw new Error('Failed to allocate memory for image pixels'); + new Uint8Array(Pdfium.memory.buffer, pixelDataPtr, pixels.byteLength).set(new Uint8Array(pixels)); + const FPDFBitmap_BGRA = 4; + const bitmapHandle = Pdfium.wasmExports.FPDFBitmap_CreateEx( + pixelWidth, + pixelHeight, + FPDFBitmap_BGRA, + pixelDataPtr, + pixelWidth * 4 + ); + if (!bitmapHandle) { + Pdfium.wasmExports.free(pixelDataPtr); + throw new Error('Failed to create bitmap for image object'); + } + const pageArrayPtr = Pdfium.wasmExports.malloc(4); // Allocate space for one pointer + new Int32Array(Pdfium.memory.buffer, pageArrayPtr, 1)[0] = pageHandle; + const result = Pdfium.wasmExports.FPDFImageObj_SetBitmap(pageArrayPtr, 1, imageObj, bitmapHandle); + Pdfium.wasmExports.free(pageArrayPtr); + Pdfium.wasmExports.free(pixelDataPtr); + Pdfium.wasmExports.FPDFBitmap_Destroy(bitmapHandle); + if (!result) { + throw new Error('Failed to set bitmap for image object'); + } +} + +/** + * Functions that can be called from the main thread + */ +const functions = { + loadDocumentFromUrl, + loadDocumentFromData, + createNewDocument, + createDocumentFromJpegData, + loadPagesProgressively, + reloadPages, + closeDocument, + loadOutline, + loadPage, + closePage, + renderPage, + loadText, + loadLinks, + reloadFonts, + addFontData, + clearAllFontData, + assemble, + encodePdf, +}; + +/** + * Send a callback invocation message back to the client + * @param {number} callbackId The callback ID to invoke + * @param {*} args Arguments to pass to the callback + */ +function invokeCallback(callbackId, ...args) { + if (callbackId) { + postMessage({ + type: 'callback', + callbackId: callbackId, + args: args, + }); + } +} + +function handleRequest(data) { + const { id, command, parameters = {} } = data; + + try { + const result = functions[command](parameters); + if (result instanceof Promise) { + result + .then((finalResult) => { + if (finalResult.result != null && finalResult.transfer != null) { + postMessage({ id, status: 'success', result: finalResult.result }, finalResult.transfer); + } else { + postMessage({ id, status: 'success', result: finalResult }); + } + }) + .catch((err) => { + postMessage({ + id, + status: 'error', + error: _error(err), + }); + }); + } else { + if (result.result != null && result.transfer != null) { + postMessage({ id, status: 'success', result: result.result }, result.transfer); + } else { + postMessage({ id, status: 'success', result: result }); + } + } + } catch (err) { + postMessage({ + id, + status: 'error', + error: _error(err), + }); + } +} + +let messagesBeforeInitialized = []; +let pdfiumInitialized = false; + +console.log(`PDFium worker initialized: ${self.location.href}`); + +/** + * Initialize PDFium with optional authentication parameters + * @param {Object} params - Initialization parameters + * @param {boolean} params.withCredentials - Whether to include credentials in the fetch + * @param {Object} params.headers - Additional headers for the fetch request + */ +async function initializePdfium(params = {}) { + try { + if (pdfiumInitialized) { + // Hot-restart or such may call this multiple times, so we can skip re-initialization + return; + } + + console.log(`Loading PDFium WASM module from ${pdfiumWasmUrl}`); + + const fetchOptions = { + credentials: params.withCredentials ? 'include' : 'same-origin', + }; + + if (params.headers) { + fetchOptions.headers = params.headers; + } + + let result; + try { + result = await WebAssembly.instantiateStreaming(fetch(pdfiumWasmUrl, fetchOptions), { + env: emEnv, + wasi_snapshot_preview1: wasi, + }); + } catch (e) { + // Fallback for browsers that do not support instantiateStreaming + console.warn( + '%cWebAssembly.instantiateStreaming failed, falling back to ArrayBuffer instantiation. Consider to configure your server to serve wasm files as application/wasm', + 'background: red; color: white', + e + ); + const response = await fetch(pdfiumWasmUrl, fetchOptions); + const buffer = await response.arrayBuffer(); + result = await WebAssembly.instantiate(buffer, { + env: emEnv, + wasi_snapshot_preview1: wasi, + }); + } + + Pdfium.initWith(result.instance.exports); + Pdfium.wasmExports.FPDF_InitLibrary(); + _initializeFontEnvironment(); + + pdfiumInitialized = true; + + postMessage({ type: 'ready' }); + + // Process queued messages + messagesBeforeInitialized.forEach((event) => handleRequest(event.data)); + messagesBeforeInitialized = null; + } catch (err) { + console.error('Failed to load WASM module:', err); + postMessage({ type: 'error', error: _error(err) }); + throw err; + } +} + +onmessage = function (e) { + const data = e.data; + + // Handle init command + if (data && data.command === 'init') { + initializePdfium(data.parameters || {}) + .then(() => { + postMessage({ id: data.id, status: 'success', result: {} }); + }) + .catch((err) => { + postMessage({ id: data.id, status: 'error', error: _error(err) }); + }); + return; + } + + if (data && data.id && data.command) { + if (!pdfiumInitialized && messagesBeforeInitialized) { + messagesBeforeInitialized.push(e); + return; + } + handleRequest(data); + } else { + console.error('Received improperly formatted message:', data); + } +}; + +const _errorMappings = { + 0: 'FPDF_ERR_SUCCESS', + 1: 'FPDF_ERR_UNKNOWN', + 2: 'FPDF_ERR_FILE', + 3: 'FPDF_ERR_FORMAT', + 4: 'FPDF_ERR_PASSWORD', + 5: 'FPDF_ERR_SECURITY', + 6: 'FPDF_ERR_PAGE', + 7: 'FPDF_ERR_XFALOAD', + 8: 'FPDF_ERR_XFALAYOUT', +}; + +function _getErrorMessage(errorCode) { + const error = _errorMappings[errorCode]; + return error ? `${error} (${errorCode})` : `Unknown error (${errorCode})`; +} + +/** + * String utilities + */ +class StringUtils { + /** + * UTF-16 string to bytes + * @param {number[]} buffer + * @returns {string} Converted string + */ + static utf16BytesToString(buffer) { + let endPtr = 0; + while (buffer[endPtr] || buffer[endPtr + 1]) endPtr += 2; + const str = new TextDecoder('utf-16le').decode(new Uint8Array(buffer.buffer, buffer.byteOffset, endPtr)); + return str; + } + /** + * UTF-8 bytes to string + * @param {number[]} buffer + * @returns {string} Converted string + */ + static utf8BytesToString(buffer) { + let endPtr = 0; + while (buffer[endPtr] && !(endPtr >= buffer.length)) ++endPtr; + + let str = ''; + let idx = 0; + while (idx < endPtr) { + let u0 = buffer[idx++]; + if (!(u0 & 0x80)) { + str += String.fromCharCode(u0); + continue; + } + const u1 = buffer[idx++] & 63; + if ((u0 & 0xe0) == 0xc0) { + str += String.fromCharCode(((u0 & 31) << 6) | u1); + continue; + } + const u2 = buffer[idx++] & 63; + if ((u0 & 0xf0) == 0xe0) { + u0 = ((u0 & 15) << 12) | (u1 << 6) | u2; + } else { + u0 = ((u0 & 7) << 18) | (u1 << 12) | (u2 << 6) | (buffer[idx++] & 63); + } + if (u0 < 0x10000) { + str += String.fromCharCode(u0); + } else { + const ch = u0 - 0x10000; + str += String.fromCharCode(0xd800 | (ch >> 10), 0xdc00 | (ch & 0x3ff)); + } + } + return str; + } + /** + * String to UTF-8 bytes + * @param {string} str + * @param {number[]} buffer + * @returns {number} Number of bytes written to the buffer + */ + static stringToUtf8Bytes(str, buffer) { + let idx = 0; + for (let i = 0; i < str.length; ++i) { + let u = str.charCodeAt(i); + if (u >= 0xd800 && u <= 0xdfff) { + const u1 = str.charCodeAt(++i); + u = (0x10000 + ((u & 0x3ff) << 10)) | (u1 & 0x3ff); + } + if (u <= 0x7f) { + buffer[idx++] = u; + } else if (u <= 0x7ff) { + buffer[idx++] = 0xc0 | (u >> 6); + buffer[idx++] = 0x80 | (u & 63); + } else if (u <= 0xffff) { + buffer[idx++] = 0xe0 | (u >> 12); + buffer[idx++] = 0x80 | ((u >> 6) & 63); + buffer[idx++] = 0x80 | (u & 63); + } else { + buffer[idx++] = 0xf0 | (u >> 18); + buffer[idx++] = 0x80 | ((u >> 12) & 63); + buffer[idx++] = 0x80 | ((u >> 6) & 63); + buffer[idx++] = 0x80 | (u & 63); + } + } + buffer[idx++] = 0; + return idx; + } + /** + * Calculate length of UTF-8 string in bytes (it does not contain the terminating '\0' character) + * @param {string} str String to calculate length + * @returns {number} Number of bytes + */ + static lengthBytesUTF8(str) { + let len = 0; + for (let i = 0; i < str.length; ++i) { + let u = str.charCodeAt(i); + if (u >= 0xd800 && u <= 0xdfff) { + u = (0x10000 + ((u & 0x3ff) << 10)) | (str.charCodeAt(++i) & 0x3ff); + } + if (u <= 0x7f) len += 1; + else if (u <= 0x7ff) len += 2; + else if (u <= 0xffff) len += 3; + else len += 4; + } + return len; + } + /** + * Allocate memory for UTF-8 string + * @param {string} str + * @returns {number} Pointer to allocated buffer that contains UTF-8 string. The buffer should be released by calling [freeUTF8]. + */ + static allocateUTF8(str) { + if (str == null) return 0; + const size = this.lengthBytesUTF8(str) + 1; + const ptr = Pdfium.wasmExports.malloc(size); + this.stringToUtf8Bytes(str, new Uint8Array(Pdfium.memory.buffer, ptr, size)); + return ptr; + } + /** + * Release memory allocated for UTF-8 string + * @param {number} ptr Pointer to allocated buffer + */ + static freeUTF8(ptr) { + Pdfium.wasmExports.free(ptr); + } +} diff --git a/packages/pdfrx/bin/find_package_path.dart b/packages/pdfrx/bin/find_package_path.dart new file mode 100644 index 00000000..94092428 --- /dev/null +++ b/packages/pdfrx/bin/find_package_path.dart @@ -0,0 +1,170 @@ +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:yaml/yaml.dart'; + +/// Finds the pubspec.yaml path for a package by reading pubspec.lock. +/// +/// Searches for pubspec.lock starting from [projectRoot] and walking up +/// the directory tree (to support pub workspace). +/// +/// Returns null if the package is not found or pubspec.lock doesn't exist. +String? findPackagePubspecPath(String projectRoot, String packageName) { + final pubspecLockPath = _findPubspecLock(projectRoot); + if (pubspecLockPath == null) return null; + + final rootDir = p.dirname(pubspecLockPath); + + final pubspecLock = loadYaml(File(pubspecLockPath).readAsStringSync()); + final packages = pubspecLock['packages'] as YamlMap?; + + // First, check pubspec.lock for the package + if (packages != null) { + final package = packages[packageName] as YamlMap?; + if (package != null) { + final pubCacheDir = _guessPubCacheDir(); + if (pubCacheDir != null) { + final packageDir = _getPackageDirectory( + package: package, + pubCacheDir: pubCacheDir, + basePubspecLockPath: pubspecLockPath, + ); + if (packageDir != null) { + return p.join(packageDir, 'pubspec.yaml'); + } + } + } + } + + // If not found in pubspec.lock, check workspace packages + final rootPubspecYamlFile = File(p.join(rootDir, 'pubspec.yaml')); + if (rootPubspecYamlFile.existsSync()) { + final rootPubspecYaml = loadYaml(rootPubspecYamlFile.readAsStringSync()); + final workspace = rootPubspecYaml['workspace'] as YamlList?; + if (workspace != null) { + for (final entry in workspace.whereType()) { + final workspacePackageDir = p.normalize(p.join(rootDir, entry)); + final workspacePubspecPath = p.join(workspacePackageDir, 'pubspec.yaml'); + final workspacePubspecFile = File(workspacePubspecPath); + if (workspacePubspecFile.existsSync()) { + final workspacePubspec = loadYaml(workspacePubspecFile.readAsStringSync()); + if (workspacePubspec['name'] == packageName) { + return workspacePubspecPath; + } + } + } + } + } + + return null; +} + +/// Searches for pubspec.lock starting from [startDir] and walking up the directory tree. +String? _findPubspecLock(String startDir) { + var current = p.normalize(p.absolute(startDir)); + + while (true) { + final lockFile = File(p.join(current, 'pubspec.lock')); + if (lockFile.existsSync()) { + return lockFile.path; + } + + final parent = p.dirname(current); + if (parent == current) { + // Reached root + return null; + } + current = parent; + } +} + +/// Guesses the pub cache directory location. +String? _guessPubCacheDir() { + var pubCache = Platform.environment['PUB_CACHE']; + if (pubCache != null && Directory(pubCache).existsSync()) return pubCache; + + if (Platform.isWindows) { + final appData = Platform.environment['APPDATA']; + if (appData != null) { + pubCache = p.join(appData, 'Pub', 'Cache'); + if (Directory(pubCache).existsSync()) return pubCache; + } + final localAppData = Platform.environment['LOCALAPPDATA']; + if (localAppData != null) { + pubCache = p.join(localAppData, 'Pub', 'Cache'); + if (Directory(pubCache).existsSync()) return pubCache; + } + } + + final homeDir = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']; + if (homeDir != null) { + return p.join(homeDir, '.pub-cache'); + } + return null; +} + +/// Gets the package directory from a pubspec.lock package entry. +String? _getPackageDirectory({ + required YamlMap package, + required String pubCacheDir, + required String basePubspecLockPath, +}) { + final source = package['source'] as String?; + final desc = package['description']; + + if (source == 'hosted' && desc is YamlMap) { + final host = _removePrefix(desc['url'] as String? ?? 'pub.dev'); + final name = desc['name'] as String?; + final version = package['version'] as String?; + if (name == null || version == null) return null; + return p.join(pubCacheDir, 'hosted', host.replaceAll('/', '%47'), '$name-$version'); + } else if (source == 'git' && desc is YamlMap) { + final repo = _gitRepoName(desc['url'] as String? ?? ''); + final commit = desc['resolved-ref'] as String?; + final subPath = desc['path'] as String? ?? ''; + if (commit == null) return null; + return p.join(pubCacheDir, 'git', '$repo-$commit', subPath); + } else if (source == 'sdk') { + final flutterDir = Platform.environment['FLUTTER_ROOT']; + if (flutterDir == null) return null; + final sdkName = desc is YamlMap ? desc['sdk'] as String? : null; + if (sdkName == 'flutter') { + final name = package['description'] is YamlMap ? (package['description'] as YamlMap)['name'] : null; + if (name == null) return null; + return p.join(flutterDir, 'packages', name); + } + return null; + } else if (source == 'path' && desc is YamlMap) { + final relativePath = desc['path'] as String?; + if (relativePath == null) return null; + return p.normalize(p.join(p.dirname(basePubspecLockPath), relativePath)); + } + + return null; +} + +/// Removes the protocol prefix from a URL. +String _removePrefix(String url) { + if (url.startsWith('https://')) return url.substring(8); + if (url.startsWith('http://')) return url.substring(7); + return url; +} + +/// Extracts the repository name from a Git URL. +String _gitRepoName(String url) { + // Handle various Git URL formats + var name = url; + if (name.endsWith('.git')) { + name = name.substring(0, name.length - 4); + } + final lastSlash = name.lastIndexOf('/'); + if (lastSlash >= 0) { + name = name.substring(lastSlash + 1); + } + // Handle ssh URLs like git@github.com:user/repo + final colonIndex = name.lastIndexOf(':'); + if (colonIndex >= 0) { + name = name.substring(colonIndex + 1); + } + return name; +} diff --git a/packages/pdfrx/bin/remove_darwin_pdfium_modules.dart b/packages/pdfrx/bin/remove_darwin_pdfium_modules.dart new file mode 100644 index 00000000..cc9694ba --- /dev/null +++ b/packages/pdfrx/bin/remove_darwin_pdfium_modules.dart @@ -0,0 +1,121 @@ +// ignore_for_file: avoid_print + +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import 'find_package_path.dart'; + +Future main(List args) async { + try { + // Parse arguments + var revert = false; + var projectRoot = '.'; + + for (final arg in args) { + if (arg == '--revert' || arg == '-r') { + revert = true; + } else if (!arg.startsWith('-')) { + projectRoot = arg; + } else if (arg == '--help' || arg == '-h') { + _printUsage(); + return 0; + } + } + + final projectPubspecYaml = File(path.join(projectRoot, 'pubspec.yaml')); + if (!projectPubspecYaml.existsSync()) { + print('No pubspec.yaml found in $projectRoot'); + return 2; + } + + final pubspecPath = findPackagePubspecPath(projectRoot, 'pdfium_flutter'); + if (pubspecPath == null) { + print('pdfium_flutter package not found. Did you run "flutter pub get"?'); + return 3; + } + print('Found pdfium_flutter: $pubspecPath'); + + final pubspecFile = File(pubspecPath); + + if (!pubspecFile.existsSync()) { + print('pubspec.yaml not found at: $pubspecPath'); + return 3; + } + + // Read the pubspec.yaml content + var pubspecYaml = pubspecFile.readAsStringSync(); + + // Comment/uncomment iOS and macOS ffiPlugin configurations + final modifiedYaml = revert ? _uncommentPlatforms(pubspecYaml) : _commentPlatforms(pubspecYaml); + + if (modifiedYaml == pubspecYaml) { + print('No changes needed.'); + } else { + pubspecFile.writeAsStringSync(modifiedYaml); + print('Successfully ${revert ? "reverted" : "modified"}.'); + } + return 0; + } catch (e, s) { + print('Error: $e\n$s'); + return 1; + } +} + +String _commentPlatforms(String yaml) { + // Comment out iOS platform configuration + yaml = yaml.replaceAllMapped( + RegExp(r'^(\s*ios:\s*\n\s*ffiPlugin:\s*true\n\s*sharedDarwinSource:\s*true)', multiLine: true), + (match) => '# ${match[1]!.replaceAll('\n', '\n# ')}', + ); + + // Comment out macOS platform configuration + yaml = yaml.replaceAllMapped( + RegExp(r'^(\s*macos:\s*\n\s*ffiPlugin:\s*true\n\s*sharedDarwinSource:\s*true)', multiLine: true), + (match) => '# ${match[1]!.replaceAll('\n', '\n# ')}', + ); + + return yaml; +} + +String _uncommentPlatforms(String yaml) { + // Uncomment iOS platform configuration + yaml = yaml.replaceAllMapped( + RegExp(r'^# (\s*ios:\s*\n)# (\s*ffiPlugin:\s*true\n)# (\s*sharedDarwinSource:\s*true)', multiLine: true), + (match) => '${match[1]}${match[2]}${match[3]}', + ); + + // Uncomment macOS platform configuration + yaml = yaml.replaceAllMapped( + RegExp(r'^# (\s*macos:\s*\n)# (\s*ffiPlugin:\s*true\n)# (\s*sharedDarwinSource:\s*true)', multiLine: true), + (match) => '${match[1]}${match[2]}${match[3]}', + ); + + return yaml; +} + +void _printUsage() { + print(''' +Usage: dart run pdfrx:remove_darwin_pdfium_modules [options] [project_root] + +This tool comments out the iOS and macOS ffiPlugin configurations in pdfrx's +pubspec.yaml to remove PDFium dependencies when using pdfrx_coregraphics. + +Options: + -r, --revert Revert the changes (uncomment the platform configurations) + -h, --help Show this help message + +Arguments: + project_root Path to the project root (default: current directory) + +Examples: + # Comment out iOS/macOS PDFium dependencies + dart run pdfrx:remove_darwin_pdfium_modules + + # Revert changes (uncomment platform configurations) + dart run pdfrx:remove_darwin_pdfium_modules --revert + + # Specify a different project root + dart run pdfrx:remove_darwin_pdfium_modules ../my_project +'''); +} diff --git a/packages/pdfrx/bin/remove_wasm_modules.dart b/packages/pdfrx/bin/remove_wasm_modules.dart new file mode 100644 index 00000000..dc07c719 --- /dev/null +++ b/packages/pdfrx/bin/remove_wasm_modules.dart @@ -0,0 +1,88 @@ +// ignore_for_file: avoid_print + +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import 'find_package_path.dart'; + +Future main(List args) async { + try { + // Parse arguments + var revert = false; + var projectRoot = '.'; + + for (final arg in args) { + if (arg == '--revert' || arg == '-r') { + revert = true; + } else if (!arg.startsWith('-')) { + projectRoot = arg; + } else if (arg == '--help' || arg == '-h') { + _printUsage(); + return 0; + } + } + + final projectPubspecYaml = File(path.join(projectRoot, 'pubspec.yaml')); + if (!projectPubspecYaml.existsSync()) { + print('No pubspec.yaml found in $projectRoot'); + return 2; + } + + final pubspecPath = findPackagePubspecPath(projectRoot, 'pdfrx'); + if (pubspecPath == null) { + print('pdfrx package not found. Did you run "flutter pub get"?'); + return 3; + } + print('Found pdfrx: $pubspecPath'); + + final pubspecFile = File(pubspecPath); + + if (!pubspecFile.existsSync()) { + print('pubspec.yaml not found at: $pubspecPath'); + return 3; + } + + // Read the pubspec.yaml content + var pubspecYaml = pubspecFile.readAsStringSync(); + final modifiedYaml = revert + ? pubspecYaml.replaceAll('# - assets/', '- assets/') + : pubspecYaml.replaceAll('- assets/', '# - assets/'); + if (modifiedYaml == pubspecYaml) { + print('No changes needed.'); + } else { + pubspecFile.writeAsStringSync(modifiedYaml); + print('Successfully ${revert ? "reverted" : "modified"}.'); + } + return 0; + } catch (e, s) { + print('Error: $e\n$s'); + return 1; + } +} + +void _printUsage() { + print(''' +Usage: dart run bin/remove_wasm_modules.dart [options] [project_root] + +This tool comments out the '- assets/' line in pdfrx's pubspec.yaml to exclude +WASM modules from the package, reducing the package size. + +Options: + -r, --revert Revert the changes (uncomment the assets line) + -h, --help Show this help message + +Arguments: + project_root Path to the project root (default: current directory) + +Examples: + # Comment out assets line + dart run bin/remove_wasm_modules.dart + + # Revert changes (uncomment assets line) + dart run bin/remove_wasm_modules.dart --revert + + # Specify a different project root + dart run bin/remove_wasm_modules.dart ../my_project +'''); +} diff --git a/example/example.dart b/packages/pdfrx/example/README.md similarity index 83% rename from example/example.dart rename to packages/pdfrx/example/README.md index 31334920..29a0495b 100644 --- a/example/example.dart +++ b/packages/pdfrx/example/README.md @@ -1,3 +1,8 @@ +# Minimum Example + +The following code is a minimum example usage of pdfrx: + +```dart import 'package:flutter/material.dart'; import 'package:pdfrx/pdfrx.dart'; @@ -32,3 +37,6 @@ class MainPage extends StatelessWidget { ); } } +``` + +For more advanced usage, see [[viewer/lib/main.dart]]. diff --git a/example/viewer/.gitignore b/packages/pdfrx/example/pdf_combine/.gitignore similarity index 96% rename from example/viewer/.gitignore rename to packages/pdfrx/example/pdf_combine/.gitignore index 6ed65412..1735b245 100644 --- a/example/viewer/.gitignore +++ b/packages/pdfrx/example/pdf_combine/.gitignore @@ -45,4 +45,5 @@ app.*.map.json /android/app/release # never ever commit the local test example file... -assets/*.pdf + +pubspec_overrides.yaml diff --git a/packages/pdfrx/example/pdf_combine/.metadata b/packages/pdfrx/example/pdf_combine/.metadata new file mode 100644 index 00000000..fca9f99c --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + - platform: macos + create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/pdfrx/example/pdf_combine/README.md b/packages/pdfrx/example/pdf_combine/README.md new file mode 100644 index 00000000..99370e3f --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/README.md @@ -0,0 +1,47 @@ +# PDF Combine - Flutter Example + +A Flutter desktop application that demonstrates how to combine multiple PDF files and images using the pdfrx package with cross-document page import support. + +## Features + +- **Multi-file picker**: Select multiple PDF files and images to combine +- **Image import**: Import images (JPG, JPEG, PNG, BMP, GIF, TIFF, WebP) as PDF pages +- **Drag & drop support**: Drag and drop PDF files and images into the app +- **Page selection UI**: Visual grid view to select and reorder pages +- **Page rotation**: Rotate individual pages before combining +- **Thumbnail previews**: Preview of all pages for easy selection +- **Live output preview**: See the combined PDF in real-time using `PdfViewer` +- **Cross-document import**: Import pages from different PDF documents and images +- **Save combined PDF**: Export the merged PDF to local storage + +## How It Works + +The app uses the [`PdfDocument.createFromJpegData()`](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocument/createFromJpegData.html) and [`encodePdf()`](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocument/encodePdf.html) APIs from pdfrx_engine: + +### PDF Files + +1. **Open PDFs**: Load PDF documents using [`PdfDocument.openFile()`](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocument/openFile.html) or [`PdfDocument.openData()`](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocument/openData.html) +2. **Extract Pages**: Access pages from each document + +### Image Files + +1. **Load Images**: Read image bytes and decode/resize as needed +2. **Convert to JPEG**: Compress images to JPEG format with quality control and optional resizing +3. **Convert to PDF**: Use [`PdfDocument.createFromJpegData()`](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocument/createFromJpegData.html) to convert JPEG data to single-page PDF documents +4. **Size Control**: Images are resized to fit within A4 page dimensions while maintaining aspect ratio + +### Combining + +1. **Select & Reorder**: Choose pages and arrange them in desired order +2. **Apply Transformations**: Rotate pages as needed +3. **Combine**: Create a new PDF document and set [`document.pages`](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocument/pages.html) with all selected pages +4. **Encode**: Call [`document.encodePdf()`](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocument/encodePdf.html) to generate the combined PDF bytes +5. **Preview & Save**: Display the result and save to disk + +## Running the App + +```bash +cd packages/pdfrx/example/pdf_combine +flutter run -d linux # or macos, windows +flutter run -d chrome # for Web +``` diff --git a/example/viewer/analysis_options.yaml b/packages/pdfrx/example/pdf_combine/analysis_options.yaml similarity index 100% rename from example/viewer/analysis_options.yaml rename to packages/pdfrx/example/pdf_combine/analysis_options.yaml diff --git a/example/viewer/android/.gitignore b/packages/pdfrx/example/pdf_combine/android/.gitignore similarity index 69% rename from example/viewer/android/.gitignore rename to packages/pdfrx/example/pdf_combine/android/.gitignore index 6f568019..be3943c9 100644 --- a/example/viewer/android/.gitignore +++ b/packages/pdfrx/example/pdf_combine/android/.gitignore @@ -5,9 +5,10 @@ gradle-wrapper.jar /gradlew.bat /local.properties GeneratedPluginRegistrant.java +.cxx/ # Remember to never publicly share your keystore. -# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +# See https://flutter.dev/to/reference-keystore key.properties **/*.keystore **/*.jks diff --git a/example/viewer/android/app/build.gradle b/packages/pdfrx/example/pdf_combine/android/app/build.gradle.kts similarity index 63% rename from example/viewer/android/app/build.gradle rename to packages/pdfrx/example/pdf_combine/android/app/build.gradle.kts index b5d72326..dd6eaba9 100644 --- a/example/viewer/android/app/build.gradle +++ b/packages/pdfrx/example/pdf_combine/android/app/build.gradle.kts @@ -1,27 +1,27 @@ plugins { - id "com.android.application" - id "kotlin-android" + id("com.android.application") + id("kotlin-android") // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. - id "dev.flutter.flutter-gradle-plugin" + id("dev.flutter.flutter-gradle-plugin") } android { - namespace "jp.espresso3389.pdfrx_example" - compileSdkVersion flutter.compileSdkVersion - ndkVersion "27.0.12077973" + namespace = "com.example.pdfcombine" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion compileOptions { - sourceCompatibility JavaVersion.VERSION_18 - targetCompatibility JavaVersion.VERSION_18 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } kotlinOptions { - jvmTarget = 18 + jvmTarget = JavaVersion.VERSION_11.toString() } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "jp.espresso3389.pdfrx_example" + applicationId = "com.example.pdfcombine" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. minSdk = flutter.minSdkVersion @@ -34,11 +34,11 @@ android { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug + signingConfig = signingConfigs.getByName("debug") } } } flutter { - source '../..' + source = "../.." } diff --git a/example/viewer/android/app/src/debug/AndroidManifest.xml b/packages/pdfrx/example/pdf_combine/android/app/src/debug/AndroidManifest.xml similarity index 100% rename from example/viewer/android/app/src/debug/AndroidManifest.xml rename to packages/pdfrx/example/pdf_combine/android/app/src/debug/AndroidManifest.xml diff --git a/packages/pdfrx/example/pdf_combine/android/app/src/main/AndroidManifest.xml b/packages/pdfrx/example/pdf_combine/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..6af6aaa3 --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/pdfrx/example/pdf_combine/android/app/src/main/kotlin/com/example/pdfcombine/MainActivity.kt b/packages/pdfrx/example/pdf_combine/android/app/src/main/kotlin/com/example/pdfcombine/MainActivity.kt new file mode 100644 index 00000000..621dc8c3 --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/android/app/src/main/kotlin/com/example/pdfcombine/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.pdfcombine + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/example/viewer/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/pdfrx/example/pdf_combine/android/app/src/main/res/drawable-v21/launch_background.xml similarity index 100% rename from example/viewer/android/app/src/main/res/drawable-v21/launch_background.xml rename to packages/pdfrx/example/pdf_combine/android/app/src/main/res/drawable-v21/launch_background.xml diff --git a/example/viewer/android/app/src/main/res/drawable/launch_background.xml b/packages/pdfrx/example/pdf_combine/android/app/src/main/res/drawable/launch_background.xml similarity index 100% rename from example/viewer/android/app/src/main/res/drawable/launch_background.xml rename to packages/pdfrx/example/pdf_combine/android/app/src/main/res/drawable/launch_background.xml diff --git a/example/viewer/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/pdfrx/example/pdf_combine/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from example/viewer/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/pdfrx/example/pdf_combine/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/example/viewer/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/pdfrx/example/pdf_combine/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from example/viewer/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/pdfrx/example/pdf_combine/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/example/viewer/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/pdfrx/example/pdf_combine/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from example/viewer/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/pdfrx/example/pdf_combine/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/example/viewer/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/pdfrx/example/pdf_combine/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from example/viewer/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/pdfrx/example/pdf_combine/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/example/viewer/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/pdfrx/example/pdf_combine/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from example/viewer/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/pdfrx/example/pdf_combine/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/example/viewer/android/app/src/main/res/values-night/styles.xml b/packages/pdfrx/example/pdf_combine/android/app/src/main/res/values-night/styles.xml similarity index 100% rename from example/viewer/android/app/src/main/res/values-night/styles.xml rename to packages/pdfrx/example/pdf_combine/android/app/src/main/res/values-night/styles.xml diff --git a/example/viewer/android/app/src/main/res/values/styles.xml b/packages/pdfrx/example/pdf_combine/android/app/src/main/res/values/styles.xml similarity index 100% rename from example/viewer/android/app/src/main/res/values/styles.xml rename to packages/pdfrx/example/pdf_combine/android/app/src/main/res/values/styles.xml diff --git a/example/viewer/android/app/src/profile/AndroidManifest.xml b/packages/pdfrx/example/pdf_combine/android/app/src/profile/AndroidManifest.xml similarity index 100% rename from example/viewer/android/app/src/profile/AndroidManifest.xml rename to packages/pdfrx/example/pdf_combine/android/app/src/profile/AndroidManifest.xml diff --git a/packages/pdfrx/example/pdf_combine/android/build.gradle.kts b/packages/pdfrx/example/pdf_combine/android/build.gradle.kts new file mode 100644 index 00000000..dbee657b --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/packages/pdfrx/example/pdf_combine/android/gradle.properties b/packages/pdfrx/example/pdf_combine/android/gradle.properties new file mode 100644 index 00000000..fbee1d8c --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true diff --git a/example/viewer/android/gradle/wrapper/gradle-wrapper.properties b/packages/pdfrx/example/pdf_combine/android/gradle/wrapper/gradle-wrapper.properties similarity index 92% rename from example/viewer/android/gradle/wrapper/gradle-wrapper.properties rename to packages/pdfrx/example/pdf_combine/android/gradle/wrapper/gradle-wrapper.properties index 6cb8454c..e4ef43fb 100644 --- a/example/viewer/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/pdfrx/example/pdf_combine/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/packages/pdfrx/example/pdf_combine/android/settings.gradle.kts b/packages/pdfrx/example/pdf_combine/android/settings.gradle.kts new file mode 100644 index 00000000..ca7fe065 --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/example/viewer/ios/.gitignore b/packages/pdfrx/example/pdf_combine/ios/.gitignore similarity index 100% rename from example/viewer/ios/.gitignore rename to packages/pdfrx/example/pdf_combine/ios/.gitignore diff --git a/example/viewer/ios/Flutter/AppFrameworkInfo.plist b/packages/pdfrx/example/pdf_combine/ios/Flutter/AppFrameworkInfo.plist similarity index 96% rename from example/viewer/ios/Flutter/AppFrameworkInfo.plist rename to packages/pdfrx/example/pdf_combine/ios/Flutter/AppFrameworkInfo.plist index 7c569640..1dc6cf76 100644 --- a/example/viewer/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/pdfrx/example/pdf_combine/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/example/viewer/ios/Flutter/Debug.xcconfig b/packages/pdfrx/example/pdf_combine/ios/Flutter/Debug.xcconfig similarity index 100% rename from example/viewer/ios/Flutter/Debug.xcconfig rename to packages/pdfrx/example/pdf_combine/ios/Flutter/Debug.xcconfig diff --git a/example/viewer/ios/Flutter/Release.xcconfig b/packages/pdfrx/example/pdf_combine/ios/Flutter/Release.xcconfig similarity index 100% rename from example/viewer/ios/Flutter/Release.xcconfig rename to packages/pdfrx/example/pdf_combine/ios/Flutter/Release.xcconfig diff --git a/packages/pdfrx/example/pdf_combine/ios/Podfile b/packages/pdfrx/example/pdf_combine/ios/Podfile new file mode 100644 index 00000000..620e46eb --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/example/viewer/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/pdfrx/example/pdf_combine/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from example/viewer/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/pdfrx/example/pdf_combine/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/example/viewer/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/pdfrx/example/pdf_combine/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from example/viewer/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/pdfrx/example/pdf_combine/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/example/viewer/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/pdfrx/example/pdf_combine/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from example/viewer/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to packages/pdfrx/example/pdf_combine/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/example/viewer/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/pdfrx/example/pdf_combine/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 96% rename from example/viewer/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to packages/pdfrx/example/pdf_combine/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 15cada48..e3773d42 100644 --- a/example/viewer/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/pdfrx/example/pdf_combine/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png similarity index 100% rename from example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png rename to packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png diff --git a/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/example/viewer/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from example/viewer/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/example/viewer/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from example/viewer/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/example/viewer/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png similarity index 100% rename from example/viewer/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/example/viewer/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png similarity index 100% rename from example/viewer/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/example/viewer/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from example/viewer/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to packages/pdfrx/example/pdf_combine/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/example/viewer/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/pdfrx/example/pdf_combine/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from example/viewer/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/pdfrx/example/pdf_combine/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/example/viewer/ios/Runner/Base.lproj/Main.storyboard b/packages/pdfrx/example/pdf_combine/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from example/viewer/ios/Runner/Base.lproj/Main.storyboard rename to packages/pdfrx/example/pdf_combine/ios/Runner/Base.lproj/Main.storyboard diff --git a/packages/pdfrx/example/pdf_combine/ios/Runner/Info.plist b/packages/pdfrx/example/pdf_combine/ios/Runner/Info.plist new file mode 100644 index 00000000..fb55ebc8 --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Pdfcombine + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + pdfcombine + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/example/viewer/ios/Runner/Runner-Bridging-Header.h b/packages/pdfrx/example/pdf_combine/ios/Runner/Runner-Bridging-Header.h similarity index 100% rename from example/viewer/ios/Runner/Runner-Bridging-Header.h rename to packages/pdfrx/example/pdf_combine/ios/Runner/Runner-Bridging-Header.h diff --git a/example/viewer/ios/RunnerTests/RunnerTests.swift b/packages/pdfrx/example/pdf_combine/ios/RunnerTests/RunnerTests.swift similarity index 100% rename from example/viewer/ios/RunnerTests/RunnerTests.swift rename to packages/pdfrx/example/pdf_combine/ios/RunnerTests/RunnerTests.swift diff --git a/packages/pdfrx/example/pdf_combine/lib/helper_io.dart b/packages/pdfrx/example/pdf_combine/lib/helper_io.dart new file mode 100644 index 00000000..94ef4714 --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/lib/helper_io.dart @@ -0,0 +1,68 @@ +import 'dart:io'; +import 'dart:ui' as ui; + +import 'package:file_selector/file_selector.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:jpeg_encode/jpeg_encode.dart'; +import 'package:share_plus/share_plus.dart'; + +Future savePdf(Uint8List bytes, {String? suggestedName}) async { + if (Platform.isIOS || Platform.isAndroid) { + final params = ShareParams( + files: [XFile.fromData(bytes, name: suggestedName ?? 'document.pdf', mimeType: 'application/pdf')], + ); + await SharePlus.instance.share(params); + return; + } + final savePath = await getSaveLocation( + suggestedName: suggestedName, + acceptedTypeGroups: [ + const XTypeGroup(label: 'PDF', extensions: ['pdf']), + ], + ); + + if (savePath != null) { + final file = File(savePath.path); + await file.writeAsBytes(bytes); + } +} + +typedef CalculateTargetSize = ({int width, int height}) Function(int originalWidth, int originalHeight); + +class JpegData { + const JpegData(this.data, this.width, this.height); + final Uint8List data; + final int width; + final int height; +} + +Future compressImageToJpeg( + Uint8List imageData, { + CalculateTargetSize? calculateTargetSize, + int quality = 90, +}) async { + calculateTargetSize ??= (w, h) => (width: w, height: h); + final buffer = await ui.ImmutableBuffer.fromUint8List(imageData); + final codec = await PaintingBinding.instance.instantiateImageCodecWithSize( + buffer, + getTargetSize: (w, h) { + final size = calculateTargetSize!(w, h); + return ui.TargetImageSize(width: size.width, height: size.height); + }, + ); + final frameInfo = await codec.getNextFrame(); + final rgba = (await frameInfo.image.toByteData(format: ui.ImageByteFormat.rawRgba))!.buffer.asUint8List(); + final byteData = await _jpegEncodeAsync(rgba, frameInfo.image.width, frameInfo.image.height, quality); + codec.dispose(); + return JpegData(byteData.buffer.asUint8List(), frameInfo.image.width, frameInfo.image.height); +} + +Future _jpegEncodeAsync(Uint8List rgba, int width, int height, int quality) async { + return await compute((jpegParams) { + final (rgba, width, height, quality) = jpegParams; + return JpegEncoder().compress(rgba, width, height, quality); + }, (rgba, width, height, quality)); +} + +final isWindowsDesktop = Platform.isWindows; diff --git a/packages/pdfrx/example/pdf_combine/lib/helper_web.dart b/packages/pdfrx/example/pdf_combine/lib/helper_web.dart new file mode 100644 index 00000000..840df744 --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/lib/helper_web.dart @@ -0,0 +1,65 @@ +import 'dart:js_interop'; +import 'dart:typed_data'; + +import 'package:web/web.dart' as web; + +Future savePdf(Uint8List bytes, {String? suggestedName, bool openInNewTab = false}) async { + final blob = web.Blob([bytes].jsify() as JSArray, web.BlobPropertyBag(type: 'application/pdf')); + + final url = web.URL.createObjectURL(blob); + + if (openInNewTab) { + // Open in a new tab + web.window.open(url, '_blank'); + + Future.delayed(const Duration(seconds: 1), () { + web.URL.revokeObjectURL(url); + }); + } else { + // Download the file + final anchor = web.HTMLAnchorElement(); + anchor.href = url; + anchor.download = suggestedName ?? 'document.pdf'; + web.document.body?.append(anchor); + anchor.click(); + + web.URL.revokeObjectURL(url); + anchor.remove(); + } +} + +typedef CalculateTargetSize = ({int width, int height}) Function(int originalWidth, int originalHeight); + +class JpegData { + const JpegData(this.data, this.width, this.height); + final Uint8List data; + final int width; + final int height; +} + +Future compressImageToJpeg( + Uint8List imageData, { + CalculateTargetSize? calculateTargetSize, + int quality = 90, +}) async { + calculateTargetSize ??= (w, h) => (width: w, height: h); + final blob = web.Blob([imageData].jsify() as JSArray); + final webCodec = await web.window.createImageBitmap(blob).toDart; + + final size = calculateTargetSize(webCodec.width, webCodec.height); + + final offScreenCanvas = web.OffscreenCanvas(size.width, size.height); + final context = offScreenCanvas.getContext('2d') as web.CanvasRenderingContext2D; + context.drawImage(webCodec, 0, 0, size.width, size.height); + final encodedBlob = await offScreenCanvas + .convertToBlob( + web.ImageEncodeOptions() + ..type = 'image/jpeg' + ..quality = quality / 100, + ) + .toDart; + final arrayBuffer = await encodedBlob.arrayBuffer().toDart; + return JpegData(arrayBuffer.toDart.asUint8List(), size.width, size.height); +} + +const bool isWindowsDesktop = false; diff --git a/packages/pdfrx/example/pdf_combine/lib/main.dart b/packages/pdfrx/example/pdf_combine/lib/main.dart new file mode 100644 index 00000000..b334ad6e --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/lib/main.dart @@ -0,0 +1,753 @@ +import 'dart:async'; +import 'dart:ui' as ui; + +import 'package:animated_reorderable_list/animated_reorderable_list.dart'; +import 'package:desktop_drop/desktop_drop.dart'; +import 'package:file_selector/file_selector.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:pdfrx/pdfrx.dart'; + +import 'helper_web.dart' if (dart.library.io) 'helper_io.dart'; + +void main() { + pdfrxFlutterInitialize(); + runApp(const PdfCombineApp()); +} + +class PdfCombineApp extends StatelessWidget { + const PdfCombineApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'PDF Combine', + theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true), + home: const PdfCombinePage(), + ); + } +} + +abstract class PageItem { + String get id; +} + +class PageItemsTrailer extends PageItem { + @override + String get id => '##trailer##'; +} + +final _pageItemsTrailer = PageItemsTrailer(); + +class PdfPageItem extends PageItem { + PdfPageItem({ + required this.documentId, + required this.documentName, + required this.pageIndex, + required this.page, + this.rotationOverride, + }); + + /// Unique ID for the document + final int documentId; + + /// Name of the source document + final String documentName; + + /// Page index + final int pageIndex; + + /// The PDF page + final PdfPage page; + + /// Rotation override for the page + final PdfPageRotation? rotationOverride; + + @override + String get id => '${documentId}_$pageIndex'; + + PdfPageItem copyWith({PdfPage? page, PdfPageRotation? rotationOverride}) { + return PdfPageItem( + documentId: documentId, + documentName: documentName, + pageIndex: pageIndex, + page: page ?? this.page, + rotationOverride: rotationOverride ?? this.rotationOverride, + ); + } + + PdfPage createProxy() { + if (rotationOverride != null) { + return page.rotatedTo(rotationOverride!); + } + return page; + } +} + +/// Manages loaded PDF documents and tracks page usage +class DocumentManager { + DocumentManager(this.passwordProvider); + + final FutureOr Function(String name)? passwordProvider; + final Map _documents = {}; + final Map _pageRefCounts = {}; + int _nextDocId = 0; + + Future loadDocument(String name, String filePath) async { + final doc = await PdfDocument.openFile( + filePath, + passwordProvider: passwordProvider != null ? () => passwordProvider!(name) : null, + ); + final docId = _nextDocId++; + _documents[docId] = doc; + _pageRefCounts[docId] = 0; + return docId; + } + + /// Load image bytes as a PDF document + /// + /// The image will be placed on a PDF page sized to fit within [fitWidth] x [fitHeight] points, + /// maintaining the aspect ratio. The default page size is A4 (595 x 842 points). + /// [pixelSizeThreshold] specifies the maximum allowed pixel size (width or height) for the image; + /// if the image exceeds this size, it will be downscaled to fit within the threshold while maintaining the aspect ratio. + /// [jpegQuality] specifies the JPEG compression quality (1-100). + Future _loadImageAsPdf( + Uint8List bytes, + String name, { + double fitWidth = 595, + double fitHeight = 842, + int pixelSizeThreshold = 2000, + int jpegQuality = 90, + }) async { + final jpegData = await compressImageToJpeg( + bytes, + calculateTargetSize: (origWidth, origHeight) { + final aspectRatio = origWidth / origHeight; + if (origWidth <= pixelSizeThreshold && origHeight <= pixelSizeThreshold) { + return (width: origWidth, height: origHeight); + } else if (aspectRatio >= 1) { + final targetWidth = pixelSizeThreshold; + final targetHeight = (pixelSizeThreshold / aspectRatio).round(); + return (width: targetWidth, height: targetHeight); + } else { + final targetHeight = pixelSizeThreshold; + final targetWidth = (pixelSizeThreshold * aspectRatio).round(); + return (width: targetWidth, height: targetHeight); + } + }, + quality: jpegQuality, + ); + + final double width, height; + final aspectRatio = jpegData.width / jpegData.height; + if (jpegData.width <= fitWidth && jpegData.height <= fitHeight) { + width = jpegData.width.toDouble(); + height = jpegData.height.toDouble(); + } else if (aspectRatio >= fitWidth / fitHeight) { + width = fitWidth; + height = fitWidth / aspectRatio; + } else { + height = fitHeight; + width = fitHeight * aspectRatio; + } + return await PdfDocument.createFromJpegData(jpegData.data, width: width, height: height, sourceName: name); + } + + Future _loadPdf(Uint8List bytes, String name) async { + return await PdfDocument.openData( + bytes, + passwordProvider: passwordProvider != null ? () => passwordProvider!(name) : null, + ); + } + + Future loadDocumentFromBytes(String name, Uint8List bytes) async { + PdfDocument doc; + if (isWindowsDesktop) { + try { + /// NOTE: we should firstly try to open as image, because PDFium on Windows could not determine whether + /// the input bytes are PDF or not correctly in some cases. + doc = await _loadImageAsPdf(bytes, name); + } catch (e) { + doc = await _loadPdf(bytes, name); + } + } else { + try { + doc = await _loadPdf(bytes, name); + } catch (e) { + doc = await _loadImageAsPdf(bytes, name); + } + } + + final docId = _nextDocId++; + _documents[docId] = doc; + _pageRefCounts[docId] = 0; + return docId; + } + + PdfDocument? getDocument(int docId) => _documents[docId]; + + void addReference(int docId) { + _pageRefCounts[docId] = (_pageRefCounts[docId] ?? 0) + 1; + } + + void removeReference(int docId) { + final count = (_pageRefCounts[docId] ?? 1) - 1; + _pageRefCounts[docId] = count; + + if (count <= 0) { + _disposeDocument(docId); + } + } + + void _disposeDocument(int docId) { + _documents[docId]?.dispose(); + _documents.remove(docId); + _pageRefCounts.remove(docId); + } + + void disposeAll() { + for (final doc in _documents.values) { + doc.dispose(); + } + _documents.clear(); + _pageRefCounts.clear(); + } +} + +class PdfCombinePage extends StatefulWidget { + const PdfCombinePage({super.key}); + + @override + State createState() => _PdfCombinePageState(); +} + +class _PdfCombinePageState extends State { + late final _docManager = DocumentManager((name) => passwordDialog(name, context)); + final _pages = [_pageItemsTrailer]; + final _scrollController = ScrollController(); + bool _isLoading = false; + bool _disableDragging = false; + bool _isTouchDevice = true; + bool _isDraggingOver = false; + + @override + void dispose() { + _docManager.disposeAll(); + _scrollController.dispose(); + super.dispose(); + } + + Future _pickFiles() async { + final files = await openFiles( + acceptedTypeGroups: [ + XTypeGroup(label: 'PDFs', extensions: ['pdf'], uniformTypeIdentifiers: ['com.adobe.pdf']), + XTypeGroup( + label: 'Images', + extensions: ['jpg', 'jpeg', 'png', 'bmp', 'gif', 'webp'], + uniformTypeIdentifiers: [ + 'public.jpeg', + 'public.png', + 'com.microsoft.bmp', + 'com.compuserve.gif', + 'org.webmproject.webp', + ], + ), + ], + ); + if (files.isEmpty) return; + await _processFiles(files); + } + + int _fileId = 0; + + Future _processFiles(List files) async { + setState(() { + _isLoading = true; + }); + + try { + for (final file in files) { + try { + final filePath = file.path; + final int docId; + final String fileName; + if (filePath.toLowerCase().endsWith('.pdf')) { + docId = await _docManager.loadDocument(filePath, filePath); + fileName = filePath.split('/').last; + } else { + fileName = 'document_${++_fileId}'; + docId = await _docManager.loadDocumentFromBytes(fileName, await file.readAsBytes()); + } + + final doc = _docManager.getDocument(docId); + if (doc != null) { + for (var i = 0; i < doc.pages.length; i++) { + _docManager.addReference(docId); + _pages.insert( + _pages.length - 1, + PdfPageItem(documentId: docId, documentName: fileName, pageIndex: i, page: doc.pages[i]), + ); + if (mounted) { + setState(() {}); + } + } + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error loading PDF": $e'))); + } + } + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error loading PDF: $e'))); + } + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } + + void _removePage(int index) { + setState(() { + final page = _pages[index]; + if (page is! PdfPageItem) return; + _pages.removeAt(index); + _docManager.removeReference(page.documentId); + }); + } + + void _rotatePageLeft(int index) { + setState(() { + final page = _pages[index]; + if (page is! PdfPageItem) return; + _pages[index] = page.copyWith(rotationOverride: (page.rotationOverride ?? page.page.rotation).rotateCCW90); + }); + } + + Future _navigateToPreview() async { + if (_pages.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please add some pages first'))); + return; + } + + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) => OutputPreviewPage(pages: _pages.whereType().cast().toList()), + ), + ); + } + + Widget _disableDraggingOnChild(Widget child) { + return MouseRegion( + child: child, + onEnter: (_) { + setState(() { + _disableDragging = true; + }); + }, + onExit: (_) { + setState(() { + _disableDragging = false; + }); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('PDF Combine'), + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + actions: [ + const SizedBox(width: 8), + FilledButton.icon( + onPressed: _pages.isEmpty ? null : _navigateToPreview, + icon: const Icon(Icons.arrow_forward), + label: const Text('Preview & Save'), + ), + const SizedBox(width: 16), + ], + ), + body: DropTarget( + onDragEntered: (event) { + setState(() { + _isDraggingOver = true; + }); + }, + onDragExited: (event) { + setState(() { + _isDraggingOver = false; + }); + }, + onDragDone: (event) => _processFiles(event.files), + child: Stack( + children: [ + if (!_isDraggingOver && _pages.length == 1) + Container( + color: Colors.blue.withValues(alpha: 0.1), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.file_open, size: 64, color: Theme.of(context).colorScheme.primary), + const SizedBox(height: 16), + Text( + 'Or Drop PDF files here', + style: Theme.of( + context, + ).textTheme.headlineSmall?.copyWith(color: Theme.of(context).colorScheme.primary), + ), + ], + ), + ), + ), + LayoutBuilder( + builder: (context, constraints) { + final w = constraints.maxWidth; + final int crossAxisCount; + if (w < 120) { + crossAxisCount = 1; + } else if (w < 400) { + crossAxisCount = 2; + } else if (w < 800) { + crossAxisCount = 3; + } else if (w < 1200) { + crossAxisCount = 4; + } else { + crossAxisCount = w ~/ 300; + } + return Listener( + onPointerMove: (event) { + setState(() { + _isTouchDevice = event.kind == ui.PointerDeviceKind.touch; + }); + }, + onPointerHover: (event) { + setState(() { + _isTouchDevice = event.kind == ui.PointerDeviceKind.touch; + }); + }, + child: AnimatedReorderableGridView( + items: _pages, + nonDraggableItems: [_pageItemsTrailer], + lockedItems: [_pageItemsTrailer], + isSameItem: (a, b) => a.id == b.id, + itemBuilder: (context, index) { + final pageItem = _pages[index]; + if (pageItem is! PdfPageItem) { + return Card( + key: ValueKey(pageItem.id), + child: Center( + child: _isLoading + ? const CircularProgressIndicator() + : Column( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: Icon( + Icons.file_open, + size: 56, + color: Theme.of(context).colorScheme.primary, + ), + onPressed: () => _pickFiles(), + ), + const SizedBox(height: 16), + Text( + 'Add PDF files...', + style: Theme.of( + context, + ).textTheme.bodyMedium?.copyWith(color: Theme.of(context).colorScheme.primary), + ), + ], + ), + ), + ); + } + return _PageThumbnail( + key: ValueKey(pageItem.id), + page: pageItem.page, + rotationOverride: pageItem.rotationOverride, + onRemove: () => _removePage(index), + onRotateLeft: () => _rotatePageLeft(index), + currentIndex: index, + dragDisabler: _disableDraggingOnChild, + ); + }, + sliverGridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: crossAxisCount), + insertDuration: const Duration(milliseconds: 100), + removeDuration: const Duration(milliseconds: 300), + dragStartDelay: _isTouchDevice || _disableDragging + ? const Duration(milliseconds: 200) + : Duration.zero, + onReorder: (oldIndex, newIndex) { + setState(() { + final removed = _pages.removeAt(oldIndex); + _pages.insert(newIndex, removed); + }); + }, + ), + ); + }, + ), + if (_isDraggingOver) + Container( + color: Colors.blue.withValues(alpha: 0.1), + child: Center( + child: Container( + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Theme.of(context).colorScheme.primary, + width: 3, + strokeAlign: BorderSide.strokeAlignInside, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.file_download, size: 64, color: Theme.of(context).colorScheme.primary), + const SizedBox(height: 16), + Text( + 'Drop PDF files here', + style: Theme.of( + context, + ).textTheme.headlineSmall?.copyWith(color: Theme.of(context).colorScheme.primary), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ); + } +} + +/// Widget for displaying a page thumbnail in the grid +class _PageThumbnail extends StatelessWidget { + const _PageThumbnail({ + required this.page, + required this.onRemove, + required this.onRotateLeft, + required this.currentIndex, + required this.dragDisabler, + this.rotationOverride, + super.key, + }); + + final PdfPage page; + final PdfPageRotation? rotationOverride; + final VoidCallback onRemove; + final VoidCallback onRotateLeft; + final int currentIndex; + final Widget Function(Widget child) dragDisabler; + + @override + Widget build(BuildContext context) { + return Card( + clipBehavior: Clip.antiAlias, + child: Stack( + fit: StackFit.expand, + children: [ + Padding( + padding: const EdgeInsets.all(4.0), + child: PdfPageView( + document: page.document, + pageNumber: page.pageNumber, + rotationOverride: rotationOverride, + ), + ), + // Delete button + Positioned( + top: 4, + right: 4, + child: dragDisabler( + Material( + color: Colors.black54, + borderRadius: BorderRadius.circular(20), + child: InkWell( + onTap: onRemove, + borderRadius: BorderRadius.circular(20), + child: const Padding( + padding: EdgeInsets.all(4), + child: Icon(Icons.close, color: Colors.white, size: 20), + ), + ), + ), + ), + ), + // Rotate left button + Positioned( + top: 45, + right: 4, + child: dragDisabler( + Material( + color: Colors.black54, + borderRadius: BorderRadius.circular(20), + child: InkWell( + onTap: onRotateLeft, + borderRadius: BorderRadius.circular(20), + child: const Padding( + padding: EdgeInsets.all(4), + child: Icon(Icons.rotate_90_degrees_ccw, color: Colors.white, size: 20), + ), + ), + ), + ), + ), + ], + ), + ); + } +} + +class OutputPreviewPage extends StatefulWidget { + const OutputPreviewPage({required this.pages, super.key}); + + final List pages; + + @override + State createState() => _OutputPreviewPageState(); +} + +class _OutputPreviewPageState extends State { + Uint8List? _outputPdfBytes; + bool _isGenerating = false; + + @override + void initState() { + super.initState(); + _generatePdf(); + } + + Future _generatePdf() async { + setState(() { + _isGenerating = true; + }); + + try { + // Create a new PDF document + final combinedDoc = await PdfDocument.createNew(sourceName: 'combined.pdf'); + + // Set all selected pages + combinedDoc.pages = widget.pages.map((item) => item.createProxy()).toList(); + + // Encode to PDF + final bytes = await combinedDoc.encodePdf(); + + if (mounted) { + setState(() { + _outputPdfBytes = bytes; + _isGenerating = false; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _isGenerating = false; + }); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error generating PDF: $e'))); + } + } + } + + Future _savePdf() async { + if (_outputPdfBytes == null) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('PDF not ready yet'))); + return; + } + + try { + final timestamp = DateTime.now().millisecondsSinceEpoch; + await savePdf(_outputPdfBytes!, suggestedName: 'output_$timestamp.pdf'); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('File downloaded successfully.'))); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error saving PDF: $e'))); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Preview'), + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + actions: [ + FilledButton.icon( + onPressed: _outputPdfBytes == null ? null : _savePdf, + icon: const Icon(Icons.save), + label: const Text('Save PDF'), + ), + const SizedBox(width: 16), + ], + ), + body: _isGenerating + ? const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [CircularProgressIndicator(), SizedBox(height: 16), Text('Generating combined PDF...')], + ), + ) + : _outputPdfBytes == null + ? const Center(child: Text('Failed to generate PDF')) + : Column( + children: [ + Container( + padding: const EdgeInsets.all(16), + color: Theme.of(context).colorScheme.primaryContainer, + child: Row( + children: [ + const Icon(Icons.info_outline), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Combined ${widget.pages.length} pages. Review the PDF below, then save or go back to make changes.', + ), + ), + ], + ), + ), + Expanded(child: PdfViewer.data(_outputPdfBytes!, sourceName: 'combined.pdf')), + ], + ), + ); + } +} + +Future passwordDialog(String name, BuildContext context) async { + final textController = TextEditingController(); + return await showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + return AlertDialog( + title: Text('Enter password for "$name"'), + content: TextField( + controller: textController, + autofocus: true, + keyboardType: TextInputType.visiblePassword, + obscureText: true, + onSubmitted: (value) => Navigator.of(context).pop(value), + ), + actions: [ + TextButton(onPressed: () => Navigator.of(context).pop(null), child: const Text('Cancel')), + TextButton(onPressed: () => Navigator.of(context).pop(textController.text), child: const Text('OK')), + ], + ); + }, + ); +} diff --git a/example/viewer/linux/.gitignore b/packages/pdfrx/example/pdf_combine/linux/.gitignore similarity index 100% rename from example/viewer/linux/.gitignore rename to packages/pdfrx/example/pdf_combine/linux/.gitignore diff --git a/packages/pdfrx/example/pdf_combine/linux/CMakeLists.txt b/packages/pdfrx/example/pdf_combine/linux/CMakeLists.txt new file mode 100644 index 00000000..bd8938c9 --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "pdfcombine") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.pdfcombine") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/example/viewer/linux/flutter/CMakeLists.txt b/packages/pdfrx/example/pdf_combine/linux/flutter/CMakeLists.txt similarity index 100% rename from example/viewer/linux/flutter/CMakeLists.txt rename to packages/pdfrx/example/pdf_combine/linux/flutter/CMakeLists.txt diff --git a/packages/pdfrx/example/pdf_combine/linux/flutter/generated_plugin_registrant.cc b/packages/pdfrx/example/pdf_combine/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..54c79483 --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,23 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) desktop_drop_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin"); + desktop_drop_plugin_register_with_registrar(desktop_drop_registrar); + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/example/viewer/linux/flutter/generated_plugin_registrant.h b/packages/pdfrx/example/pdf_combine/linux/flutter/generated_plugin_registrant.h similarity index 100% rename from example/viewer/linux/flutter/generated_plugin_registrant.h rename to packages/pdfrx/example/pdf_combine/linux/flutter/generated_plugin_registrant.h diff --git a/packages/pdfrx/example/pdf_combine/linux/flutter/generated_plugins.cmake b/packages/pdfrx/example/pdf_combine/linux/flutter/generated_plugins.cmake new file mode 100644 index 00000000..0e00fb81 --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/linux/flutter/generated_plugins.cmake @@ -0,0 +1,27 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + desktop_drop + file_selector_linux + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + pdfium_flutter +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/packages/pdfrx/example/pdf_combine/linux/runner/CMakeLists.txt b/packages/pdfrx/example/pdf_combine/linux/runner/CMakeLists.txt new file mode 100644 index 00000000..e97dabc7 --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/example/viewer/linux/main.cc b/packages/pdfrx/example/pdf_combine/linux/runner/main.cc similarity index 100% rename from example/viewer/linux/main.cc rename to packages/pdfrx/example/pdf_combine/linux/runner/main.cc diff --git a/packages/pdfrx/example/pdf_combine/linux/runner/my_application.cc b/packages/pdfrx/example/pdf_combine/linux/runner/my_application.cc new file mode 100644 index 00000000..8fe0f684 --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/linux/runner/my_application.cc @@ -0,0 +1,144 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Called when first Flutter frame received. +static void first_frame_cb(MyApplication* self, FlView *view) +{ + gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); +} + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "pdfcombine"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "pdfcombine"); + } + + gtk_window_set_default_size(window, 1280, 720); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + GdkRGBA background_color; + // Background defaults to black, override it here if necessary, e.g. #00000000 for transparent. + gdk_rgba_parse(&background_color, "#000000"); + fl_view_set_background_color(view, &background_color); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + // Show the window when Flutter renders. + // Requires the view to be realized so we can start rendering. + g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), self); + gtk_widget_realize(GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/example/viewer/linux/my_application.h b/packages/pdfrx/example/pdf_combine/linux/runner/my_application.h similarity index 100% rename from example/viewer/linux/my_application.h rename to packages/pdfrx/example/pdf_combine/linux/runner/my_application.h diff --git a/example/viewer/macos/.gitignore b/packages/pdfrx/example/pdf_combine/macos/.gitignore similarity index 100% rename from example/viewer/macos/.gitignore rename to packages/pdfrx/example/pdf_combine/macos/.gitignore diff --git a/example/viewer/macos/Flutter/Flutter-Debug.xcconfig b/packages/pdfrx/example/pdf_combine/macos/Flutter/Flutter-Debug.xcconfig similarity index 100% rename from example/viewer/macos/Flutter/Flutter-Debug.xcconfig rename to packages/pdfrx/example/pdf_combine/macos/Flutter/Flutter-Debug.xcconfig diff --git a/example/viewer/macos/Flutter/Flutter-Release.xcconfig b/packages/pdfrx/example/pdf_combine/macos/Flutter/Flutter-Release.xcconfig similarity index 100% rename from example/viewer/macos/Flutter/Flutter-Release.xcconfig rename to packages/pdfrx/example/pdf_combine/macos/Flutter/Flutter-Release.xcconfig diff --git a/packages/pdfrx/example/pdf_combine/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/pdfrx/example/pdf_combine/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 00000000..d16846ad --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,20 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import desktop_drop +import file_selector_macos +import path_provider_foundation +import share_plus +import url_launcher_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin")) + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) +} diff --git a/packages/pdfrx/example/pdf_combine/macos/Podfile b/packages/pdfrx/example/pdf_combine/macos/Podfile new file mode 100644 index 00000000..ff5ddb3b --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/example/viewer/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/pdfrx/example/pdf_combine/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from example/viewer/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/pdfrx/example/pdf_combine/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/example/viewer/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/pdfrx/example/pdf_combine/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 94% rename from example/viewer/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to packages/pdfrx/example/pdf_combine/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 92f8073e..605a6a4b 100644 --- a/example/viewer/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/pdfrx/example/pdf_combine/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -15,7 +15,7 @@ @@ -31,7 +31,7 @@ @@ -59,13 +59,14 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> @@ -82,7 +83,7 @@ diff --git a/example/viewer/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/pdfrx/example/pdf_combine/macos/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from example/viewer/macos/Runner.xcworkspace/contents.xcworkspacedata rename to packages/pdfrx/example/pdf_combine/macos/Runner.xcworkspace/contents.xcworkspacedata diff --git a/example/viewer/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/pdfrx/example/pdf_combine/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from example/viewer/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/pdfrx/example/pdf_combine/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/example/viewer/macos/Runner/AppDelegate.swift b/packages/pdfrx/example/pdf_combine/macos/Runner/AppDelegate.swift similarity index 100% rename from example/viewer/macos/Runner/AppDelegate.swift rename to packages/pdfrx/example/pdf_combine/macos/Runner/AppDelegate.swift diff --git a/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/pdfrx/example/pdf_combine/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/pdfrx/example/pdf_combine/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/pdfrx/example/pdf_combine/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png similarity index 100% rename from example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png rename to packages/pdfrx/example/pdf_combine/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png diff --git a/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/pdfrx/example/pdf_combine/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png similarity index 100% rename from example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png rename to packages/pdfrx/example/pdf_combine/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png diff --git a/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/pdfrx/example/pdf_combine/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png similarity index 100% rename from example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png rename to packages/pdfrx/example/pdf_combine/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png diff --git a/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/pdfrx/example/pdf_combine/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png similarity index 100% rename from example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png rename to packages/pdfrx/example/pdf_combine/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png diff --git a/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/pdfrx/example/pdf_combine/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png similarity index 100% rename from example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png rename to packages/pdfrx/example/pdf_combine/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png diff --git a/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/pdfrx/example/pdf_combine/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png similarity index 100% rename from example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png rename to packages/pdfrx/example/pdf_combine/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png diff --git a/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/pdfrx/example/pdf_combine/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png similarity index 100% rename from example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png rename to packages/pdfrx/example/pdf_combine/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png diff --git a/example/viewer/macos/Runner/Base.lproj/MainMenu.xib b/packages/pdfrx/example/pdf_combine/macos/Runner/Base.lproj/MainMenu.xib similarity index 100% rename from example/viewer/macos/Runner/Base.lproj/MainMenu.xib rename to packages/pdfrx/example/pdf_combine/macos/Runner/Base.lproj/MainMenu.xib diff --git a/packages/pdfrx/example/pdf_combine/macos/Runner/Configs/AppInfo.xcconfig b/packages/pdfrx/example/pdf_combine/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 00000000..060ad21b --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = pdfcombine + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.pdfcombine + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/example/viewer/macos/Runner/Configs/Debug.xcconfig b/packages/pdfrx/example/pdf_combine/macos/Runner/Configs/Debug.xcconfig similarity index 100% rename from example/viewer/macos/Runner/Configs/Debug.xcconfig rename to packages/pdfrx/example/pdf_combine/macos/Runner/Configs/Debug.xcconfig diff --git a/example/viewer/macos/Runner/Configs/Release.xcconfig b/packages/pdfrx/example/pdf_combine/macos/Runner/Configs/Release.xcconfig similarity index 100% rename from example/viewer/macos/Runner/Configs/Release.xcconfig rename to packages/pdfrx/example/pdf_combine/macos/Runner/Configs/Release.xcconfig diff --git a/example/viewer/macos/Runner/Configs/Warnings.xcconfig b/packages/pdfrx/example/pdf_combine/macos/Runner/Configs/Warnings.xcconfig similarity index 100% rename from example/viewer/macos/Runner/Configs/Warnings.xcconfig rename to packages/pdfrx/example/pdf_combine/macos/Runner/Configs/Warnings.xcconfig diff --git a/packages/pdfrx/example/pdf_combine/macos/Runner/DebugProfile.entitlements b/packages/pdfrx/example/pdf_combine/macos/Runner/DebugProfile.entitlements new file mode 100644 index 00000000..d138bd5b --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.files.user-selected.read-write + + + diff --git a/example/viewer/macos/Runner/Info.plist b/packages/pdfrx/example/pdf_combine/macos/Runner/Info.plist similarity index 100% rename from example/viewer/macos/Runner/Info.plist rename to packages/pdfrx/example/pdf_combine/macos/Runner/Info.plist diff --git a/example/viewer/macos/Runner/MainFlutterWindow.swift b/packages/pdfrx/example/pdf_combine/macos/Runner/MainFlutterWindow.swift similarity index 100% rename from example/viewer/macos/Runner/MainFlutterWindow.swift rename to packages/pdfrx/example/pdf_combine/macos/Runner/MainFlutterWindow.swift diff --git a/packages/pdfrx/example/pdf_combine/macos/Runner/Release.entitlements b/packages/pdfrx/example/pdf_combine/macos/Runner/Release.entitlements new file mode 100644 index 00000000..19afff14 --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/macos/Runner/Release.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-write + + + diff --git a/packages/pdfrx/example/pdf_combine/macos/RunnerTests/RunnerTests.swift b/packages/pdfrx/example/pdf_combine/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..61f3bd1f --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/packages/pdfrx/example/pdf_combine/pubspec.yaml b/packages/pdfrx/example/pdf_combine/pubspec.yaml new file mode 100644 index 00000000..3a12b8a9 --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/pubspec.yaml @@ -0,0 +1,35 @@ +name: pdf_combine +description: "PDF Combine - Combine multiple PDF files into one with page selection" +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: ^3.9.2 +resolution: workspace + +dependencies: + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + + cupertino_icons: ^1.0.8 + + pdfrx: + path: ../../ + + file_selector: ^1.0.3 + share_plus: ^12.0.1 + path_provider: ^2.1.5 + animated_reorderable_list: ^1.3.0 + web: ^1.1.1 + desktop_drop: ^0.7.0 + jpeg_encode: ^1.0.1 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: + +flutter: + uses-material-design: true diff --git a/example/viewer/web/favicon.png b/packages/pdfrx/example/pdf_combine/web/favicon.png similarity index 100% rename from example/viewer/web/favicon.png rename to packages/pdfrx/example/pdf_combine/web/favicon.png diff --git a/example/viewer/web/icons/Icon-192.png b/packages/pdfrx/example/pdf_combine/web/icons/Icon-192.png similarity index 100% rename from example/viewer/web/icons/Icon-192.png rename to packages/pdfrx/example/pdf_combine/web/icons/Icon-192.png diff --git a/example/viewer/web/icons/Icon-512.png b/packages/pdfrx/example/pdf_combine/web/icons/Icon-512.png similarity index 100% rename from example/viewer/web/icons/Icon-512.png rename to packages/pdfrx/example/pdf_combine/web/icons/Icon-512.png diff --git a/example/viewer/web/icons/Icon-maskable-192.png b/packages/pdfrx/example/pdf_combine/web/icons/Icon-maskable-192.png similarity index 100% rename from example/viewer/web/icons/Icon-maskable-192.png rename to packages/pdfrx/example/pdf_combine/web/icons/Icon-maskable-192.png diff --git a/example/viewer/web/icons/Icon-maskable-512.png b/packages/pdfrx/example/pdf_combine/web/icons/Icon-maskable-512.png similarity index 100% rename from example/viewer/web/icons/Icon-maskable-512.png rename to packages/pdfrx/example/pdf_combine/web/icons/Icon-maskable-512.png diff --git a/packages/pdfrx/example/pdf_combine/web/index.html b/packages/pdfrx/example/pdf_combine/web/index.html new file mode 100644 index 00000000..d2c959b3 --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/web/index.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + pdf_combine + + + + + + diff --git a/packages/pdfrx/example/pdf_combine/web/manifest.json b/packages/pdfrx/example/pdf_combine/web/manifest.json new file mode 100644 index 00000000..207fecc9 --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "pdfcombine", + "short_name": "pdfcombine", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/example/viewer/windows/.gitignore b/packages/pdfrx/example/pdf_combine/windows/.gitignore similarity index 100% rename from example/viewer/windows/.gitignore rename to packages/pdfrx/example/pdf_combine/windows/.gitignore diff --git a/packages/pdfrx/example/pdf_combine/windows/CMakeLists.txt b/packages/pdfrx/example/pdf_combine/windows/CMakeLists.txt new file mode 100644 index 00000000..3f9f4183 --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(pdfcombine LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "pdfcombine") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/example/viewer/windows/flutter/CMakeLists.txt b/packages/pdfrx/example/pdf_combine/windows/flutter/CMakeLists.txt similarity index 100% rename from example/viewer/windows/flutter/CMakeLists.txt rename to packages/pdfrx/example/pdf_combine/windows/flutter/CMakeLists.txt diff --git a/packages/pdfrx/example/pdf_combine/windows/flutter/generated_plugin_registrant.cc b/packages/pdfrx/example/pdf_combine/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..2db4d5af --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,23 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + DesktopDropPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DesktopDropPlugin")); + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/example/viewer/windows/flutter/generated_plugin_registrant.h b/packages/pdfrx/example/pdf_combine/windows/flutter/generated_plugin_registrant.h similarity index 100% rename from example/viewer/windows/flutter/generated_plugin_registrant.h rename to packages/pdfrx/example/pdf_combine/windows/flutter/generated_plugin_registrant.h diff --git a/packages/pdfrx/example/pdf_combine/windows/flutter/generated_plugins.cmake b/packages/pdfrx/example/pdf_combine/windows/flutter/generated_plugins.cmake new file mode 100644 index 00000000..9a807d5f --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/windows/flutter/generated_plugins.cmake @@ -0,0 +1,28 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + desktop_drop + file_selector_windows + share_plus + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + pdfium_flutter +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/example/viewer/windows/runner/CMakeLists.txt b/packages/pdfrx/example/pdf_combine/windows/runner/CMakeLists.txt similarity index 100% rename from example/viewer/windows/runner/CMakeLists.txt rename to packages/pdfrx/example/pdf_combine/windows/runner/CMakeLists.txt diff --git a/packages/pdfrx/example/pdf_combine/windows/runner/Runner.rc b/packages/pdfrx/example/pdf_combine/windows/runner/Runner.rc new file mode 100644 index 00000000..1ae98029 --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "pdfcombine" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "pdfcombine" "\0" + VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "pdfcombine.exe" "\0" + VALUE "ProductName", "pdfcombine" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/example/viewer/windows/runner/flutter_window.cpp b/packages/pdfrx/example/pdf_combine/windows/runner/flutter_window.cpp similarity index 100% rename from example/viewer/windows/runner/flutter_window.cpp rename to packages/pdfrx/example/pdf_combine/windows/runner/flutter_window.cpp diff --git a/example/viewer/windows/runner/flutter_window.h b/packages/pdfrx/example/pdf_combine/windows/runner/flutter_window.h similarity index 100% rename from example/viewer/windows/runner/flutter_window.h rename to packages/pdfrx/example/pdf_combine/windows/runner/flutter_window.h diff --git a/packages/pdfrx/example/pdf_combine/windows/runner/main.cpp b/packages/pdfrx/example/pdf_combine/windows/runner/main.cpp new file mode 100644 index 00000000..b60381be --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"pdfcombine", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/example/viewer/windows/runner/resource.h b/packages/pdfrx/example/pdf_combine/windows/runner/resource.h similarity index 100% rename from example/viewer/windows/runner/resource.h rename to packages/pdfrx/example/pdf_combine/windows/runner/resource.h diff --git a/example/viewer/windows/runner/resources/app_icon.ico b/packages/pdfrx/example/pdf_combine/windows/runner/resources/app_icon.ico similarity index 100% rename from example/viewer/windows/runner/resources/app_icon.ico rename to packages/pdfrx/example/pdf_combine/windows/runner/resources/app_icon.ico diff --git a/packages/pdfrx/example/pdf_combine/windows/runner/runner.exe.manifest b/packages/pdfrx/example/pdf_combine/windows/runner/runner.exe.manifest new file mode 100644 index 00000000..153653e8 --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/packages/pdfrx/example/pdf_combine/windows/runner/utils.cpp b/packages/pdfrx/example/pdf_combine/windows/runner/utils.cpp new file mode 100644 index 00000000..3a0b4651 --- /dev/null +++ b/packages/pdfrx/example/pdf_combine/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/example/viewer/windows/runner/utils.h b/packages/pdfrx/example/pdf_combine/windows/runner/utils.h similarity index 100% rename from example/viewer/windows/runner/utils.h rename to packages/pdfrx/example/pdf_combine/windows/runner/utils.h diff --git a/example/viewer/windows/runner/win32_window.cpp b/packages/pdfrx/example/pdf_combine/windows/runner/win32_window.cpp similarity index 100% rename from example/viewer/windows/runner/win32_window.cpp rename to packages/pdfrx/example/pdf_combine/windows/runner/win32_window.cpp diff --git a/example/viewer/windows/runner/win32_window.h b/packages/pdfrx/example/pdf_combine/windows/runner/win32_window.h similarity index 100% rename from example/viewer/windows/runner/win32_window.h rename to packages/pdfrx/example/pdf_combine/windows/runner/win32_window.h diff --git a/packages/pdfrx/example/viewer/.gitignore b/packages/pdfrx/example/viewer/.gitignore new file mode 100644 index 00000000..1735b245 --- /dev/null +++ b/packages/pdfrx/example/viewer/.gitignore @@ -0,0 +1,49 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# never ever commit the local test example file... + +pubspec_overrides.yaml diff --git a/packages/pdfrx/example/viewer/.metadata b/packages/pdfrx/example/viewer/.metadata new file mode 100644 index 00000000..b1c3f10b --- /dev/null +++ b/packages/pdfrx/example/viewer/.metadata @@ -0,0 +1,27 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "d693b4b9dbac2acd4477aea4555ca6dcbea44ba2" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2 + base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/example/viewer/README.md b/packages/pdfrx/example/viewer/README.md similarity index 100% rename from example/viewer/README.md rename to packages/pdfrx/example/viewer/README.md diff --git a/wasm/pdfrx_wasm/analysis_options.yaml b/packages/pdfrx/example/viewer/analysis_options.yaml similarity index 97% rename from wasm/pdfrx_wasm/analysis_options.yaml rename to packages/pdfrx/example/viewer/analysis_options.yaml index 44ca9a1f..6be920c2 100644 --- a/wasm/pdfrx_wasm/analysis_options.yaml +++ b/packages/pdfrx/example/viewer/analysis_options.yaml @@ -40,8 +40,6 @@ linter: # https://dart.dev/guides/language/analysis-options analyzer: - exclude: - - lib/src/pdfium/pdfium_bindings.dart formatter: page_width: 120 diff --git a/packages/pdfrx/example/viewer/android/.gitignore b/packages/pdfrx/example/viewer/android/.gitignore new file mode 100644 index 00000000..be3943c9 --- /dev/null +++ b/packages/pdfrx/example/viewer/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/example/viewer/android/app/bin/main/jp/espresso3389/pdfrx_example/MainActivity.kt b/packages/pdfrx/example/viewer/android/app/bin/main/jp/espresso3389/pdfrx_example/MainActivity.kt similarity index 100% rename from example/viewer/android/app/bin/main/jp/espresso3389/pdfrx_example/MainActivity.kt rename to packages/pdfrx/example/viewer/android/app/bin/main/jp/espresso3389/pdfrx_example/MainActivity.kt diff --git a/packages/pdfrx/example/viewer/android/app/build.gradle.kts b/packages/pdfrx/example/viewer/android/app/build.gradle.kts new file mode 100644 index 00000000..0d97bbdc --- /dev/null +++ b/packages/pdfrx/example/viewer/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "jp.espresso3389.pdfrx_example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "jp.espresso3389.pdfrx_example" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/packages/pdfrx/example/viewer/android/app/src/debug/AndroidManifest.xml b/packages/pdfrx/example/viewer/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/packages/pdfrx/example/viewer/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/viewer/android/app/src/main/AndroidManifest.xml b/packages/pdfrx/example/viewer/android/app/src/main/AndroidManifest.xml similarity index 100% rename from example/viewer/android/app/src/main/AndroidManifest.xml rename to packages/pdfrx/example/viewer/android/app/src/main/AndroidManifest.xml diff --git a/example/viewer/android/app/src/main/kotlin/jp/espresso3389/pdfrx_example/MainActivity.kt b/packages/pdfrx/example/viewer/android/app/src/main/kotlin/jp/espresso3389/pdfrx_example/MainActivity.kt similarity index 100% rename from example/viewer/android/app/src/main/kotlin/jp/espresso3389/pdfrx_example/MainActivity.kt rename to packages/pdfrx/example/viewer/android/app/src/main/kotlin/jp/espresso3389/pdfrx_example/MainActivity.kt diff --git a/packages/pdfrx/example/viewer/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/pdfrx/example/viewer/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 00000000..f74085f3 --- /dev/null +++ b/packages/pdfrx/example/viewer/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/pdfrx/example/viewer/android/app/src/main/res/drawable/launch_background.xml b/packages/pdfrx/example/viewer/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 00000000..304732f8 --- /dev/null +++ b/packages/pdfrx/example/viewer/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/pdfrx/example/viewer/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/pdfrx/example/viewer/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..db77bb4b Binary files /dev/null and b/packages/pdfrx/example/viewer/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/pdfrx/example/viewer/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/pdfrx/example/viewer/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..17987b79 Binary files /dev/null and b/packages/pdfrx/example/viewer/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/pdfrx/example/viewer/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/pdfrx/example/viewer/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..09d43914 Binary files /dev/null and b/packages/pdfrx/example/viewer/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/pdfrx/example/viewer/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/pdfrx/example/viewer/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..d5f1c8d3 Binary files /dev/null and b/packages/pdfrx/example/viewer/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/pdfrx/example/viewer/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/pdfrx/example/viewer/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..4d6372ee Binary files /dev/null and b/packages/pdfrx/example/viewer/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/pdfrx/example/viewer/android/app/src/main/res/values-night/styles.xml b/packages/pdfrx/example/viewer/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 00000000..06952be7 --- /dev/null +++ b/packages/pdfrx/example/viewer/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/pdfrx/example/viewer/android/app/src/main/res/values/styles.xml b/packages/pdfrx/example/viewer/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..cb1ef880 --- /dev/null +++ b/packages/pdfrx/example/viewer/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/pdfrx/example/viewer/android/app/src/profile/AndroidManifest.xml b/packages/pdfrx/example/viewer/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 00000000..399f6981 --- /dev/null +++ b/packages/pdfrx/example/viewer/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/pdfrx/example/viewer/android/build.gradle.kts b/packages/pdfrx/example/viewer/android/build.gradle.kts new file mode 100644 index 00000000..dbee657b --- /dev/null +++ b/packages/pdfrx/example/viewer/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/packages/pdfrx/example/viewer/android/gradle.properties b/packages/pdfrx/example/viewer/android/gradle.properties new file mode 100644 index 00000000..fbee1d8c --- /dev/null +++ b/packages/pdfrx/example/viewer/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true diff --git a/packages/pdfrx/example/viewer/android/gradle/wrapper/gradle-wrapper.properties b/packages/pdfrx/example/viewer/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..e4ef43fb --- /dev/null +++ b/packages/pdfrx/example/viewer/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/packages/pdfrx/example/viewer/android/settings.gradle.kts b/packages/pdfrx/example/viewer/android/settings.gradle.kts new file mode 100644 index 00000000..ca7fe065 --- /dev/null +++ b/packages/pdfrx/example/viewer/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/example/viewer/assets/hello.pdf b/packages/pdfrx/example/viewer/assets/hello.pdf similarity index 100% rename from example/viewer/assets/hello.pdf rename to packages/pdfrx/example/viewer/assets/hello.pdf diff --git a/packages/pdfrx/example/viewer/ios/.gitignore b/packages/pdfrx/example/viewer/ios/.gitignore new file mode 100644 index 00000000..7a7f9873 --- /dev/null +++ b/packages/pdfrx/example/viewer/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/packages/pdfrx/example/viewer/ios/Flutter/AppFrameworkInfo.plist b/packages/pdfrx/example/viewer/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 00000000..1dc6cf76 --- /dev/null +++ b/packages/pdfrx/example/viewer/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 13.0 + + diff --git a/packages/pdfrx/example/viewer/ios/Flutter/Debug.xcconfig b/packages/pdfrx/example/viewer/ios/Flutter/Debug.xcconfig new file mode 100644 index 00000000..ec97fc6f --- /dev/null +++ b/packages/pdfrx/example/viewer/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/packages/pdfrx/example/viewer/ios/Flutter/Release.xcconfig b/packages/pdfrx/example/viewer/ios/Flutter/Release.xcconfig new file mode 100644 index 00000000..c4855bfe --- /dev/null +++ b/packages/pdfrx/example/viewer/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/example/viewer/ios/Podfile b/packages/pdfrx/example/viewer/ios/Podfile similarity index 98% rename from example/viewer/ios/Podfile rename to packages/pdfrx/example/viewer/ios/Podfile index d97f17e2..e51a31d9 100644 --- a/example/viewer/ios/Podfile +++ b/packages/pdfrx/example/viewer/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +# platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/viewer/ios/Podfile.lock b/packages/pdfrx/example/viewer/ios/Podfile.lock similarity index 60% rename from example/viewer/ios/Podfile.lock rename to packages/pdfrx/example/viewer/ios/Podfile.lock index 1777a260..d1bfe47e 100644 --- a/example/viewer/ios/Podfile.lock +++ b/packages/pdfrx/example/viewer/ios/Podfile.lock @@ -5,7 +5,7 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - pdfrx (0.0.3): + - pdfium_flutter (0.1.0): - Flutter - FlutterMacOS - url_launcher_ios (0.0.1): @@ -15,7 +15,7 @@ DEPENDENCIES: - file_selector_ios (from `.symlinks/plugins/file_selector_ios/ios`) - Flutter (from `Flutter`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - - pdfrx (from `.symlinks/plugins/pdfrx/darwin`) + - pdfium_flutter (from `.symlinks/plugins/pdfium_flutter/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) EXTERNAL SOURCES: @@ -25,18 +25,18 @@ EXTERNAL SOURCES: :path: Flutter path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" - pdfrx: - :path: ".symlinks/plugins/pdfrx/darwin" + pdfium_flutter: + :path: ".symlinks/plugins/pdfium_flutter/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: - file_selector_ios: f0670c1064a8c8450e38145d8043160105d0b97c - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - pdfrx: 07fc287c47ea8d027c4ed56457f8a1aa74d73594 - url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + file_selector_ios: ec57ec07954363dd730b642e765e58f199bb621a + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 + pdfium_flutter: 5dbf80fb3ce6d3a333a4b895a7b16844da6f9ac9 + url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b -PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 +PODFILE CHECKSUM: 4f1c12611da7338d21589c0b2ecd6bd20b109694 COCOAPODS: 1.16.2 diff --git a/example/viewer/ios/Runner.xcodeproj/project.pbxproj b/packages/pdfrx/example/viewer/ios/Runner.xcodeproj/project.pbxproj similarity index 87% rename from example/viewer/ios/Runner.xcodeproj/project.pbxproj rename to packages/pdfrx/example/viewer/ios/Runner.xcodeproj/project.pbxproj index 405dbda5..38c485fa 100644 --- a/example/viewer/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/pdfrx/example/viewer/ios/Runner.xcodeproj/project.pbxproj @@ -11,11 +11,12 @@ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - BE975C22D15CF1BBE8071C10 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 38B4320811C7EB4CB74C6133 /* Pods_Runner.framework */; }; - E99BBE0FBBBAAC964367C729 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E7BEC1FADF343CBFC5B51B1E /* Pods_RunnerTests.framework */; }; + B7491F1FA5813A72C5DB815E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A71E10111F6AE7ED2B3E748 /* Pods_Runner.framework */; }; + DDBE1588174F910263C39C4A /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4DB1D065852B0FEB77E57673 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -44,15 +45,20 @@ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3295B794FD54924048C1CD50 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 38B4320811C7EB4CB74C6133 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 36C1BD909E9326B5FE0920A8 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 3773DAD9E81D0A04DD41B8C0 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 4DB1D065852B0FEB77E57673 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5A71E10111F6AE7ED2B3E748 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 67B7D4760433250E4C3975AF /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7AB3913973095D5E19AA88E2 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 85C9F234D9DF12C65EB18206 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 896939C7E339590C9E807639 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -60,11 +66,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 9E247B55F4C64CE7CF913025 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - D5D2308B985CDB250A9F88B7 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - DEBC11A5C1305C8F354ABA41 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; - E7BEC1FADF343CBFC5B51B1E /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - EBE521B9258A2D46B34232C9 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 9A55819EB52450C0121C39BC /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -72,7 +74,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - E99BBE0FBBBAAC964367C729 /* Pods_RunnerTests.framework in Frameworks */, + DDBE1588174F910263C39C4A /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -80,7 +82,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - BE975C22D15CF1BBE8071C10 /* Pods_Runner.framework in Frameworks */, + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + B7491F1FA5813A72C5DB815E /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -90,12 +93,12 @@ 2C0A7AC090CFDFC89385173D /* Pods */ = { isa = PBXGroup; children = ( - 7AB3913973095D5E19AA88E2 /* Pods-Runner.debug.xcconfig */, - 9E247B55F4C64CE7CF913025 /* Pods-Runner.release.xcconfig */, - D5D2308B985CDB250A9F88B7 /* Pods-Runner.profile.xcconfig */, - DEBC11A5C1305C8F354ABA41 /* Pods-RunnerTests.debug.xcconfig */, - EBE521B9258A2D46B34232C9 /* Pods-RunnerTests.release.xcconfig */, - 85C9F234D9DF12C65EB18206 /* Pods-RunnerTests.profile.xcconfig */, + 896939C7E339590C9E807639 /* Pods-Runner.debug.xcconfig */, + 67B7D4760433250E4C3975AF /* Pods-Runner.release.xcconfig */, + 3773DAD9E81D0A04DD41B8C0 /* Pods-Runner.profile.xcconfig */, + 9A55819EB52450C0121C39BC /* Pods-RunnerTests.debug.xcconfig */, + 3295B794FD54924048C1CD50 /* Pods-RunnerTests.release.xcconfig */, + 36C1BD909E9326B5FE0920A8 /* Pods-RunnerTests.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -111,6 +114,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, @@ -127,7 +131,7 @@ 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, 2C0A7AC090CFDFC89385173D /* Pods */, - F3A93483760A6F9EFD891B53 /* Frameworks */, + ED9A0663962D5B9F096B4B34 /* Frameworks */, ); sourceTree = ""; }; @@ -155,11 +159,11 @@ path = Runner; sourceTree = ""; }; - F3A93483760A6F9EFD891B53 /* Frameworks */ = { + ED9A0663962D5B9F096B4B34 /* Frameworks */ = { isa = PBXGroup; children = ( - 38B4320811C7EB4CB74C6133 /* Pods_Runner.framework */, - E7BEC1FADF343CBFC5B51B1E /* Pods_RunnerTests.framework */, + 5A71E10111F6AE7ED2B3E748 /* Pods_Runner.framework */, + 4DB1D065852B0FEB77E57673 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -171,7 +175,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 938050D037D35BB79276ADD0 /* [CP] Check Pods Manifest.lock */, + 10CB03BA201C2D8083212310 /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, 8CA26D04F87237D4AA4DC2CF /* Frameworks */, @@ -190,20 +194,23 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 475BC99F98491F877D325517 /* [CP] Check Pods Manifest.lock */, + EF8B5360E6E6BF4A239F5F56 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - F6C1F0DECDC2C464FB14B588 /* [CP] Embed Pods Frameworks */, + 98169DAAFFC178DEC77A818A /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; @@ -237,6 +244,9 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; @@ -269,23 +279,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; - }; - 475BC99F98491F877D325517 /* [CP] Check Pods Manifest.lock */ = { + 10CB03BA201C2D8083212310 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -300,34 +294,28 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 938050D037D35BB79276ADD0 /* [CP] Check Pods Manifest.lock */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); + name = "Thin Binary"; outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; @@ -344,7 +332,7 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - F6C1F0DECDC2C464FB14B588 /* [CP] Embed Pods Frameworks */ = { + 98169DAAFFC178DEC77A818A /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -361,6 +349,28 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; + EF8B5360E6E6BF4A239F5F56 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -452,7 +462,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -470,10 +480,10 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = XRDM278W3T; + DEVELOPMENT_TEAM = ERC2H3WBFG; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -489,7 +499,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = DEBC11A5C1305C8F354ABA41 /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = 9A55819EB52450C0121C39BC /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -507,7 +517,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = EBE521B9258A2D46B34232C9 /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = 3295B794FD54924048C1CD50 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -523,7 +533,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 85C9F234D9DF12C65EB18206 /* Pods-RunnerTests.profile.xcconfig */; + baseConfigurationReference = 36C1BD909E9326B5FE0920A8 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -584,7 +594,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -633,7 +643,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -653,10 +663,10 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = XRDM278W3T; + DEVELOPMENT_TEAM = ERC2H3WBFG; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -680,10 +690,10 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = XRDM278W3T; + DEVELOPMENT_TEAM = ERC2H3WBFG; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -731,6 +741,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; } diff --git a/packages/pdfrx/example/viewer/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/pdfrx/example/viewer/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/packages/pdfrx/example/viewer/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/pdfrx/example/viewer/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/pdfrx/example/viewer/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/packages/pdfrx/example/viewer/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/pdfrx/example/viewer/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/pdfrx/example/viewer/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/packages/pdfrx/example/viewer/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/pdfrx/example/viewer/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/pdfrx/example/viewer/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..95d6e55f --- /dev/null +++ b/packages/pdfrx/example/viewer/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/pdfrx/example/viewer/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/pdfrx/example/viewer/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/packages/pdfrx/example/viewer/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/pdfrx/example/viewer/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/pdfrx/example/viewer/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/packages/pdfrx/example/viewer/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/pdfrx/example/viewer/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/pdfrx/example/viewer/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 00000000..f9b0d7c5 --- /dev/null +++ b/packages/pdfrx/example/viewer/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/viewer/ios/Runner/AppDelegate.swift b/packages/pdfrx/example/viewer/ios/Runner/AppDelegate.swift similarity index 100% rename from example/viewer/ios/Runner/AppDelegate.swift rename to packages/pdfrx/example/viewer/ios/Runner/AppDelegate.swift diff --git a/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d36b1fab --- /dev/null +++ b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 00000000..dc9ada47 Binary files /dev/null and b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 00000000..7353c41e Binary files /dev/null and b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 00000000..797d452e Binary files /dev/null and b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 00000000..6ed2d933 Binary files /dev/null and b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 00000000..4cd7b009 Binary files /dev/null and b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 00000000..fe730945 Binary files /dev/null and b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 00000000..321773cd Binary files /dev/null and b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 00000000..797d452e Binary files /dev/null and b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 00000000..502f463a Binary files /dev/null and b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 00000000..0ec30343 Binary files /dev/null and b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 00000000..0ec30343 Binary files /dev/null and b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 00000000..e9f5fea2 Binary files /dev/null and b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 00000000..84ac32ae Binary files /dev/null and b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 00000000..8953cba0 Binary files /dev/null and b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 00000000..0467bf12 Binary files /dev/null and b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 00000000..0bedcf2f --- /dev/null +++ b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 00000000..9da19eac Binary files /dev/null and b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 00000000..89c2725b --- /dev/null +++ b/packages/pdfrx/example/viewer/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/pdfrx/example/viewer/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/pdfrx/example/viewer/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000..f2e259c7 --- /dev/null +++ b/packages/pdfrx/example/viewer/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/pdfrx/example/viewer/ios/Runner/Base.lproj/Main.storyboard b/packages/pdfrx/example/viewer/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 00000000..f3c28516 --- /dev/null +++ b/packages/pdfrx/example/viewer/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/viewer/ios/Runner/Info.plist b/packages/pdfrx/example/viewer/ios/Runner/Info.plist similarity index 100% rename from example/viewer/ios/Runner/Info.plist rename to packages/pdfrx/example/viewer/ios/Runner/Info.plist diff --git a/packages/pdfrx/example/viewer/ios/Runner/Runner-Bridging-Header.h b/packages/pdfrx/example/viewer/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 00000000..308a2a56 --- /dev/null +++ b/packages/pdfrx/example/viewer/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/packages/pdfrx/example/viewer/ios/RunnerTests/RunnerTests.swift b/packages/pdfrx/example/viewer/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 00000000..86a7c3b1 --- /dev/null +++ b/packages/pdfrx/example/viewer/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/packages/pdfrx/example/viewer/lib/main.dart b/packages/pdfrx/example/viewer/lib/main.dart new file mode 100644 index 00000000..40b13c56 --- /dev/null +++ b/packages/pdfrx/example/viewer/lib/main.dart @@ -0,0 +1,790 @@ +import 'dart:math'; + +import 'package:file_selector/file_selector.dart' as fs; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:pdfrx/pdfrx.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import 'markers_view.dart'; +import 'noto_google_fonts.dart'; +import 'outline_view.dart'; +import 'password_dialog.dart'; +import 'search_view.dart'; +import 'thumbnails_view.dart'; + +void main(List args) { + runApp(MyApp(fileOrUri: args.isNotEmpty ? args[0] : null)); +} + +class MyApp extends StatelessWidget { + const MyApp({this.fileOrUri, super.key}); + + final String? fileOrUri; + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Pdfrx example', + home: MainPage(fileOrUri: fileOrUri), + ); + } +} + +class MainPage extends StatefulWidget { + const MainPage({this.fileOrUri, super.key}); + + final String? fileOrUri; + + @override + State createState() => _MainPageState(); +} + +class _MainPageState extends State with WidgetsBindingObserver, SingleTickerProviderStateMixin { + final documentRef = ValueNotifier(null); + final controller = PdfViewerController(); + final showLeftPane = ValueNotifier(false); + final outline = ValueNotifier?>(null); + final textSearcher = ValueNotifier(null); + final _markers = >{}; + List? textSelections; + + bool _isDraggingHandle = false; + // Magnifier animation controller + late final AnimationController _magnifierAnimController = AnimationController( + duration: const Duration(milliseconds: 250), + vsync: this, + ); + + void _update() { + if (mounted) { + setState(() {}); + } + } + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + openInitialFile(); + } + + @override + void dispose() { + _magnifierAnimController.dispose(); + WidgetsBinding.instance.removeObserver(this); + textSearcher.value?.dispose(); + textSearcher.dispose(); + showLeftPane.dispose(); + outline.dispose(); + documentRef.dispose(); + super.dispose(); + } + + @override + void didChangeMetrics() { + super.didChangeMetrics(); + if (mounted) setState(() {}); + } + + static bool determineWhetherMobileDeviceOrNot() { + final data = MediaQueryData.fromView(WidgetsBinding.instance.platformDispatcher.views.single); + return data.size.shortestSide < 600; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.menu), + onPressed: () { + showLeftPane.value = !showLeftPane.value; + }, + ), + title: ValueListenableBuilder( + valueListenable: documentRef, + builder: (context, documentRef, child) { + final isMobileDevice = determineWhetherMobileDeviceOrNot(); + final visualDensity = isMobileDevice ? VisualDensity.compact : null; + return Row( + children: [ + if (!isMobileDevice) ...[ + Expanded(child: Text(_fileName(documentRef?.key.sourceName) ?? 'No document loaded')), + SizedBox(width: 10), + FilledButton(onPressed: () => openFile(), child: Text('Open File')), + SizedBox(width: 20), + FilledButton(onPressed: () => openUri(), child: Text('Open URL')), + Spacer(), + ], + IconButton( + visualDensity: visualDensity, + onPressed: documentRef == null ? null : () => _changeLayoutType(), + icon: Icon(Icons.pages), + ), + IconButton( + visualDensity: visualDensity, + icon: const Icon(Icons.circle, color: Colors.red), + onPressed: documentRef == null ? null : () => _addCurrentSelectionToMarkers(Colors.red), + ), + IconButton( + visualDensity: visualDensity, + icon: const Icon(Icons.circle, color: Colors.green), + onPressed: documentRef == null ? null : () => _addCurrentSelectionToMarkers(Colors.green), + ), + IconButton( + visualDensity: visualDensity, + icon: const Icon(Icons.circle, color: Colors.orangeAccent), + onPressed: documentRef == null ? null : () => _addCurrentSelectionToMarkers(Colors.orangeAccent), + ), + IconButton( + visualDensity: visualDensity, + icon: const Icon(Icons.zoom_in), + onPressed: documentRef == null + ? null + : () { + if (controller.isReady) controller.zoomUp(); + }, + ), + IconButton( + visualDensity: visualDensity, + icon: const Icon(Icons.zoom_out), + onPressed: documentRef == null + ? null + : () { + if (controller.isReady) controller.zoomDown(); + }, + ), + IconButton( + visualDensity: visualDensity, + icon: const Icon(Icons.first_page), + onPressed: documentRef == null + ? null + : () { + if (controller.isReady) controller.goToPage(pageNumber: 1); + }, + ), + IconButton( + visualDensity: visualDensity, + icon: const Icon(Icons.last_page), + onPressed: documentRef == null + ? null + : () { + if (controller.isReady) { + controller.goToPage(pageNumber: controller.pageCount); + } + }, + ), + ], + ); + }, + ), + ), + body: Row( + children: [ + AnimatedSize( + duration: const Duration(milliseconds: 300), + child: ValueListenableBuilder( + valueListenable: showLeftPane, + builder: (context, isLeftPaneShown, child) { + final isMobileDevice = determineWhetherMobileDeviceOrNot(); + return SizedBox( + width: isLeftPaneShown ? 300 : 0, + child: Padding( + padding: const EdgeInsets.fromLTRB(1, 0, 4, 0), + child: DefaultTabController( + length: 4, + child: Column( + children: [ + if (isMobileDevice) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + children: [ + ValueListenableBuilder( + valueListenable: documentRef, + builder: (context, documentRef, child) => Expanded( + child: Text( + _fileName(documentRef?.key.sourceName) ?? 'No document loaded', + softWrap: false, + ), + ), + ), + IconButton( + icon: const Icon(Icons.file_open), + onPressed: () { + showLeftPane.value = false; + openFile(); + }, + ), + IconButton( + icon: const Icon(Icons.http), + onPressed: () { + showLeftPane.value = false; + openUri(); + }, + ), + ], + ), + ), + ClipRect( + // NOTE: without ClipRect, TabBar shown even if the width is 0 + child: const TabBar( + tabs: [ + Tab(icon: Icon(Icons.search), text: 'Search'), + Tab(icon: Icon(Icons.menu_book), text: 'TOC'), + Tab(icon: Icon(Icons.image), text: 'Pages'), + Tab(icon: Icon(Icons.bookmark), text: 'Markers'), + ], + ), + ), + Expanded( + child: TabBarView( + children: [ + ValueListenableBuilder( + valueListenable: textSearcher, + builder: (context, textSearcher, child) { + if (textSearcher == null) return SizedBox(); + return TextSearchView(textSearcher: textSearcher); + }, + ), + ValueListenableBuilder( + valueListenable: outline, + builder: (context, outline, child) => + OutlineView(outline: outline, controller: controller), + ), + ValueListenableBuilder( + valueListenable: documentRef, + builder: (context, documentRef, child) => + ThumbnailsView(documentRef: documentRef, controller: controller), + ), + MarkersView( + markers: _markers.values.expand((e) => e).toList(), + onTap: (marker) { + final rect = controller.calcRectForRectInsidePage( + pageNumber: marker.range.pageText.pageNumber, + rect: marker.range.bounds, + ); + controller.ensureVisible(rect); + }, + onDeleteTap: (marker) { + _markers[marker.range.pageNumber]!.remove(marker); + setState(() {}); + }, + ), + ], + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + Expanded( + child: Stack( + children: [ + ValueListenableBuilder( + valueListenable: documentRef, + builder: (context, docRef, child) { + if (docRef == null) { + return const Center(child: Text('No document loaded', style: TextStyle(fontSize: 20))); + } + return PdfViewer( + docRef, + // PdfViewer.asset( + // 'assets/hello.pdf', + // PdfViewer.file( + // r"D:\pdfrx\example\assets\hello.pdf", + // PdfViewer.uri( + // Uri.parse( + // 'https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf'), + // Set password provider to show password dialog + //passwordProvider: () => passwordDialog(context), + controller: controller, + params: PdfViewerParams( + layoutPages: _layoutPages[_layoutTypeIndex], + scrollHorizontallyByMouseWheel: isHorizontalLayout, + pageAnchor: isHorizontalLayout ? PdfPageAnchor.left : PdfPageAnchor.top, + pageAnchorEnd: isHorizontalLayout ? PdfPageAnchor.right : PdfPageAnchor.bottom, + textSelectionParams: PdfTextSelectionParams( + onTextSelectionChange: (textSelection) async { + textSelections = await textSelection.getSelectedTextRanges(); + }, + // magnifier: PdfViewerSelectionMagnifierParams( + // shouldShowMagnifierForAnchor: (textAnchor, controller, params) => true, + // getMagnifierRectForAnchor: (textAnchor, params, clampedPointerPosition) { + // final c = textAnchor.page.charRects[textAnchor.index]; + // final baseUnit = switch (textAnchor.direction) { + // PdfTextDirection.ltr || PdfTextDirection.rtl || PdfTextDirection.unknown => c.height, + // PdfTextDirection.vrtl => c.width, + // }; + + // // Convert clamped pointer position from viewport to document coordinates + // final pointerInDocument = controller.localToDocument(clampedPointerPosition); + // return Rect.fromLTRB( + // pointerInDocument.dx - baseUnit * 2.5, + // textAnchor.rect.top - baseUnit * 0.5, + // pointerInDocument.dx + baseUnit * 2.5, + // textAnchor.rect.bottom + baseUnit * 0.5, + // ); + // }, + // builder: + // ( + // context, + // textAnchor, + // params, + // magnifierContent, + // magnifierContentSize, + // pointerPosition, + // magnifierPosition, + // ) { + // // calculate the scale to fit the magnifier content fit into 80x80 box + // final contentScale = + // 80 / math.min(magnifierContentSize.width, magnifierContentSize.height); + + // // Calculate the actual magnifier widget size (with border radius padding) + // final magnifierWidgetSize = Size( + // magnifierContentSize.width * contentScale, + // magnifierContentSize.height * contentScale, + // ); + + // // Start animation when magnifier first appears and capture initial pointer position + // if (_magnifierAnimController.status == AnimationStatus.dismissed) { + // _magnifierAnimController.forward(); + // } + + // final centeredStartOffset = + // pointerPosition - + // Offset(magnifierWidgetSize.width / 2, magnifierWidgetSize.height / 2); + // final delta = centeredStartOffset - magnifierPosition; + + // return AnimatedBuilder( + // animation: _magnifierAnimController, + // builder: (context, child) { + // final currentProgress = _magnifierAnimController.value; + // return Transform.translate( + // offset: delta * (1 - currentProgress), + // child: Transform.scale( + // scale: currentProgress, + // alignment: Alignment.center, + // child: child!, + // ), + // ); + // }, + // child: Container( + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(25), + // boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 8, spreadRadius: 2)], + // ), + // child: ClipRRect( + // borderRadius: BorderRadius.circular(25), + // child: SizedBox( + // width: magnifierContentSize.width * contentScale, + // height: magnifierContentSize.height * contentScale, + // child: magnifierContent, + // ), + // ), + // ), + // ); + // }, + // calcPosition: + // ( + // widgetSize, + // anchorLocalRect, + // handleLocalRect, + // textAnchor, + // pointerPosition, { + // margin = 10.0, + // marginOnTop, + // marginOnBottom, + // }) { + // if (widgetSize == null) return null; + + // final viewSize = controller.viewSize; + + // // Center magnifier horizontally on pointer for smooth tracking + // var left = pointerPosition.dx - widgetSize.width / 2; + + // // Clamp to viewport bounds + // if (left < margin) { + // left = margin; + // } else if (left + widgetSize.width + margin > viewSize.width) { + // left = viewSize.width - widgetSize.width - margin; + // } + + // var top = anchorLocalRect.top - widgetSize.height - (marginOnTop ?? margin); + + // // If too close to top, place below instead + // if (top < margin) { + // top = anchorLocalRect.bottom + (marginOnBottom ?? margin); + // } + + // return Offset(left, top); + // }, + // shouldShowMagnifier: () => + // _isDraggingHandle || + // _magnifierAnimController.status == AnimationStatus.reverse || + // _magnifierAnimController.status == AnimationStatus.forward, + // animationDuration: Duration.zero, + // ), + onSelectionHandlePanStart: (anchor) { + setState(() { + _isDraggingHandle = true; + }); + }, + + onSelectionHandlePanEnd: (anchor) { + // Animate out, then reset for next drag + if (mounted) { + setState(() { + _isDraggingHandle = false; + }); + } + _magnifierAnimController.reverse().then((_) { + _magnifierAnimController.reset(); + }); + }, + ), + keyHandlerParams: PdfViewerKeyHandlerParams(autofocus: true), + maxScale: 8, + //scrollPhysics: PdfViewerParams.getScrollPhysics(context), + //pageTransition: PageTransition.discrete, + customizeContextMenuItems: (params, items) { + // Example: add custom menu item to search selected text on web + items.add( + ContextMenuButtonItem( + type: ContextMenuButtonType.searchWeb, + onPressed: () async { + final text = await controller.textSelectionDelegate.getSelectedText(); + if (text.isNotEmpty) { + final shortened = text.length > 100 ? text.substring(0, 100) : text; + await launchUrl( + Uri.parse('https://www.google.com/search?q=${Uri.encodeComponent(shortened)}'), + ); + } + }, + ), + ); + }, + //pageTransition: PageTransition.discrete, + viewerOverlayBuilder: (context, size, handleLinkTap) => [ + // + // Example use of GestureDetector to handle custom gestures + // + // GestureDetector( + // behavior: HitTestBehavior.translucent, + // // If you use GestureDetector on viewerOverlayBuilder, it breaks link-tap handling + // // and you should manually handle it using onTapUp callback + // onTapUp: (details) { + // handleLinkTap(details.localPosition); + // }, + // onDoubleTap: () { + // controller.zoomUp(loop: true); + // }, + // // Make the GestureDetector covers all the viewer widget's area + // // but also make the event go through to the viewer. + // child: IgnorePointer( + // child: + // SizedBox(width: size.width, height: size.height), + // ), + // ), + // + // Scroll-thumbs example + // + // Show vertical scroll thumb on the right; it has page number on it + PdfViewerScrollThumb( + controller: controller, + orientation: ScrollbarOrientation.right, + thumbSize: const Size(40, 25), + thumbBuilder: (context, thumbSize, showRange, pageRange, controller) => Container( + color: Colors.black, + child: isHorizontalLayout + ? null + : Center( + child: Text(pageRange?.label ?? '', style: const TextStyle(color: Colors.white)), + ), + ), + ), + // Just a simple horizontal scroll thumb on the bottom + PdfViewerScrollThumb( + controller: controller, + orientation: ScrollbarOrientation.bottom, + thumbSize: const Size(40, 25), + thumbBuilder: (context, thumbSize, showRange, pageRange, controller) => Container( + color: Colors.black, + child: !isHorizontalLayout + ? null + : Center( + child: Text(pageRange?.label ?? '', style: const TextStyle(color: Colors.white)), + ), + ), + ), + ], + // + // Loading progress indicator example + // + loadingBannerBuilder: (context, bytesDownloaded, totalBytes) => Center( + child: CircularProgressIndicator( + value: totalBytes != null ? bytesDownloaded / totalBytes : null, + backgroundColor: Colors.grey, + ), + ), + // + // Link handling example + // + linkHandlerParams: PdfLinkHandlerParams( + onLinkTap: (link) { + if (link.url != null) { + navigateToUrl(link.url!); + } else if (link.dest != null) { + controller.goToDest(link.dest); + } + }, + ), + pagePaintCallbacks: [ + if (textSearcher.value != null) textSearcher.value!.pageTextMatchPaintCallback, + _paintMarkers, + ], + onDocumentChanged: (document) async { + if (document == null) { + textSearcher.value?.dispose(); + textSearcher.value = null; + outline.value = null; + textSelections = null; + _markers.clear(); + } + }, + onViewerReady: (document, controller) async { + outline.value = await document.loadOutline(); + textSearcher.value = PdfTextSearcher(controller)..addListener(_update); + controller.requestFocus(); + controller.document.events.listen((event) { + if (event is PdfDocumentMissingFontsEvent) { + Future.microtask(() async { + // NOTE: This is just an example of downloading missing fonts from Google Fonts. + // In real-world use cases, you might want to have a more sophisticated + // mechanism to manage the fonts. + debugPrint('Missing fonts: ${event.missingFonts.map((f) => f.toString()).join(', ')}'); + int count = 0; + for (final font in event.missingFonts) { + final gf = getGoogleFontsUriFromFontQuery(font); + if (gf != null) { + debugPrint('Downloading font "${gf.faceName}" from ${gf.uri}...'); + final downloaded = (await http.get(gf.uri)).bodyBytes; + debugPrint(' Downloaded ${downloaded.length} bytes'); + await PdfrxEntryFunctions.instance.addFontData(face: font.face, data: downloaded); + count++; + } + } + if (count > 0) { + await PdfrxEntryFunctions.instance.reloadFonts(); + await controller.documentRef.resolveListenable().load(forceReload: true); + } + }); + } + }); + }, + ), + ); + }, + ), + ], + ), + ), + ], + ), + ); + } + + void _paintMarkers(Canvas canvas, Rect pageRect, PdfPage page) { + final markers = _markers[page.pageNumber]; + if (markers == null) { + return; + } + for (final marker in markers) { + final paint = Paint() + ..color = marker.color.withAlpha(100) + ..style = PaintingStyle.fill; + + canvas.drawRect(marker.range.bounds.toRectInDocument(page: page, pageRect: pageRect), paint); + } + } + + int _layoutTypeIndex = 0; + + /// Change the layout logic; see [_layoutPages] for the logics + void _changeLayoutType() { + setState(() { + _layoutTypeIndex = (_layoutTypeIndex + 1) % _layoutPages.length; + }); + } + + bool get isHorizontalLayout => _layoutTypeIndex == 1 || _layoutTypeIndex == 3; + + /// Page reading order; true to L-to-R that is commonly used by books like manga or such + var isRightToLeftReadingOrder = false; + + late final List _layoutPages = [ + null, + // Horizontal layout (using built-in layout class) + (pages, params, helper) => + SequentialPagesLayout.fromPages(pages, params, helper: helper, scrollDirection: Axis.horizontal), + + // Facing pages layout (using built-in layout class) + (pages, params, helper) => FacingPagesLayout.fromPages( + pages, + params, + helper: helper, + firstPageIsCoverPage: true, + isRightToLeftReadingOrder: isRightToLeftReadingOrder, + ), + + // Custom layout example - horizontal strip layout + (pages, params, helper) { + final height = pages.fold(0.0, (prev, page) => max(prev, page.height)) + params.margin * 2; + final pageLayouts = []; + double x = params.margin; + for (var page in pages) { + pageLayouts.add(Rect.fromLTWH(x, (height - page.height) / 2, page.width, page.height)); + x += page.width + params.margin; + } + return PdfPageLayout(pageLayouts: pageLayouts, documentSize: Size(x, height)); + }, + ]; + + void _addCurrentSelectionToMarkers(Color color) { + if (controller.isReady && textSelections != null) { + for (final selectedText in textSelections!) { + _markers.putIfAbsent(selectedText.pageNumber, () => []).add(Marker(color, selectedText)); + } + setState(() {}); + } + } + + Future navigateToUrl(Uri url) async { + if (await shouldOpenUrl(context, url)) { + await launchUrl(url); + } + } + + Future shouldOpenUrl(BuildContext context, Uri url) async { + final result = await showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + return AlertDialog( + title: const Text('Navigate to URL?'), + content: SelectionArea( + child: Text.rich( + TextSpan( + children: [ + const TextSpan(text: 'Do you want to navigate to the following location?\n'), + TextSpan( + text: url.toString(), + style: const TextStyle(color: Colors.blue), + ), + ], + ), + ), + ), + actions: [ + TextButton(onPressed: () => Navigator.of(context).pop(false), child: const Text('Cancel')), + TextButton(onPressed: () => Navigator.of(context).pop(true), child: const Text('Go')), + ], + ); + }, + ); + return result ?? false; + } + + Future openInitialFile({bool useProgressiveLoading = true}) async { + if (widget.fileOrUri != null) { + final fileOrUri = widget.fileOrUri!; + if (fileOrUri.startsWith('https://') || fileOrUri.startsWith('http://')) { + documentRef.value = PdfDocumentRefUri( + Uri.parse(fileOrUri), + passwordProvider: () => passwordDialog(context), + useProgressiveLoading: useProgressiveLoading, + ); + return; + } else { + documentRef.value = PdfDocumentRefFile( + fileOrUri, + passwordProvider: () => passwordDialog(context), + useProgressiveLoading: useProgressiveLoading, + ); + return; + } + } + documentRef.value = PdfDocumentRefAsset('assets/hello.pdf', useProgressiveLoading: useProgressiveLoading); + } + + Future openFile({bool useProgressiveLoading = true}) async { + final file = await fs.openFile( + acceptedTypeGroups: [ + fs.XTypeGroup(label: 'PDF files', extensions: ['pdf'], uniformTypeIdentifiers: ['com.adobe.pdf']), + ], + ); + if (file == null) return; + if (kIsWeb) { + final bytes = await file.readAsBytes(); + documentRef.value = PdfDocumentRefData( + bytes, + sourceName: file.name, + passwordProvider: () => passwordDialog(context), + useProgressiveLoading: useProgressiveLoading, + ); + } else { + documentRef.value = PdfDocumentRefFile( + file.path, + passwordProvider: () => passwordDialog(context), + useProgressiveLoading: useProgressiveLoading, + ); + } + } + + Future openUri({bool useProgressiveLoading = true}) async { + final result = await showDialog( + context: context, + builder: (context) { + final controller = TextEditingController(); + controller.text = 'https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf'; + return AlertDialog( + title: const Text('Open URL'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (kIsWeb) const Text('Note: The URL must be CORS-enabled.', style: TextStyle(color: Colors.red)), + TextField( + controller: controller, + decoration: const InputDecoration(hintText: 'URL'), + ), + ], + ), + actions: [ + TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Cancel')), + TextButton(onPressed: () => Navigator.of(context).pop(controller.text), child: const Text('Open')), + ], + ); + }, + ); + if (result == null) return; + final uri = Uri.parse(result); + documentRef.value = PdfDocumentRefUri( + uri, + passwordProvider: () => passwordDialog(context), + useProgressiveLoading: useProgressiveLoading, + ); + } + + static String? _fileName(String? path) { + if (path == null) return null; + final parts = path.split(RegExp(r'[\\/]')); + return parts.isEmpty ? path : parts.last; + } +} diff --git a/example/viewer/lib/markers_view.dart b/packages/pdfrx/example/viewer/lib/markers_view.dart similarity index 67% rename from example/viewer/lib/markers_view.dart rename to packages/pdfrx/example/viewer/lib/markers_view.dart index 51b8cd0c..0405f823 100644 --- a/example/viewer/lib/markers_view.dart +++ b/packages/pdfrx/example/viewer/lib/markers_view.dart @@ -2,22 +2,17 @@ import 'package:flutter/material.dart'; import 'package:pdfrx/pdfrx.dart'; class Marker { - Marker(this.color, this.ranges); + Marker(this.color, this.range); final Color color; - final PdfTextRanges ranges; + final PdfPageTextRange range; } class MarkersView extends StatefulWidget { - const MarkersView({ - required this.markers, - super.key, - this.onTap, - this.onDeleteTap, - }); + const MarkersView({required this.markers, super.key, this.onTap, this.onDeleteTap}); final List markers; - final void Function(Marker ranges)? onTap; - final void Function(Marker ranges)? onDeleteTap; + final void Function(Marker marker)? onTap; + final void Function(Marker marker)? onDeleteTap; @override State createState() => _MarkersViewState(); @@ -40,16 +35,13 @@ class _MarkersViewState extends State { child: SizedBox( width: double.infinity, height: 40, - child: Text('Page #${marker.ranges.pageNumber} - ${marker.ranges.text}'), + child: Text('Page #${marker.range.pageNumber} - ${marker.range.text}'), ), ), ), Align( alignment: Alignment.centerRight, - child: IconButton( - icon: const Icon(Icons.delete), - onPressed: () => widget.onDeleteTap?.call(marker), - ), + child: IconButton(icon: const Icon(Icons.delete), onPressed: () => widget.onDeleteTap?.call(marker)), ), ], ), diff --git a/packages/pdfrx/example/viewer/lib/noto_google_fonts.dart b/packages/pdfrx/example/viewer/lib/noto_google_fonts.dart new file mode 100644 index 00000000..8e415ac7 --- /dev/null +++ b/packages/pdfrx/example/viewer/lib/noto_google_fonts.dart @@ -0,0 +1,1139 @@ +// The code is based on the following code from Google Fonts: +// https://github.com/flutter/packages/blob/main/packages/google_fonts/lib/src/google_fonts_parts/part_n.g.dart +// +// Noto Sans/Serif is licensed under the SIL Open Font License, Version 1.1 . +// https://fonts.google.com/noto/specimen/Noto+Sans/license + +import 'package:flutter/foundation.dart'; +import 'package:pdfrx/pdfrx.dart'; + +abstract class GoogleFontsFile { + String get faceName; + Uri get uri; +} + +class GoogleFontsFileCJK implements GoogleFontsFile { + const GoogleFontsFileCJK(this.faceName, this.uri); + @override + final String faceName; + + @override + final Uri uri; +} + +class GoogleFontsFileSingle implements GoogleFontsFile { + const GoogleFontsFileSingle(this.faceName, this.weight, this.expectedFileHash, this.expectedLength); + @override + final String faceName; + final int weight; + final String expectedFileHash; + final int expectedLength; + + @override + Uri get uri => Uri.parse('https://fonts.gstatic.com/s/a/$expectedFileHash.ttf'); +} + +GoogleFontsFileSingle? _getNearestWeight(Map fonts, int weight) { + final weights = fonts.keys.toList(); + weights.sort((a, b) => (a - weight).abs().compareTo((b - weight).abs())); + return fonts[weights.first]; +} + +final notoSerifCJK = GoogleFontsFileCJK( + 'Noto Serif CJK', + Uri.parse('https://github.com/googlefonts/noto-cjk/raw/main/Serif/Variable/OTC/NotoSerifCJK-VF.otf.ttc'), +); +final notoSansCJK = GoogleFontsFileCJK( + 'Noto Sans CJK', + Uri.parse('https://github.com/googlefonts/noto-cjk/raw/main/Sans/Variable/OTC/NotoSansCJK-VF.otf.ttc'), +); + +GoogleFontsFile? getGoogleFontsUriFromFontQuery(PdfFontQuery query, {bool preferCJK = true}) { + // For CJK, prefer full CJK fonts (but not for Web because of CORS issues on GitHub) + if (!kIsWeb && + preferCJK && + (query.charset == PdfFontCharset.gb2312 || + query.charset == PdfFontCharset.chineseBig5 || + query.charset == PdfFontCharset.shiftJis || + query.charset == PdfFontCharset.hangul)) { + if (query.isRoman) return notoSerifCJK; + return notoSansCJK; + } + + final fontTable = switch (query.isRoman) { + true => switch (query.charset) { + PdfFontCharset.gb2312 => _notoSerifSc, + PdfFontCharset.chineseBig5 => _notoSerifTc, + PdfFontCharset.shiftJis => _notoSerifJp, + PdfFontCharset.hangul => _notoSerifKr, + PdfFontCharset.thai => _notoSerifThai, + PdfFontCharset.hebrew => _notoSerifHebrew, + PdfFontCharset.arabic => _notoNaskhArabic, + PdfFontCharset.greek || + PdfFontCharset.vietnamese || + PdfFontCharset.cyrillic || + PdfFontCharset.easternEuropean => query.isItalic ? _notoSerifItalic : _notoSerif, + PdfFontCharset.ansi || PdfFontCharset.default_ || PdfFontCharset.symbol => null, + }, + false => switch (query.charset) { + PdfFontCharset.gb2312 => _notoSansSc, + PdfFontCharset.chineseBig5 => _notoSansTc, + PdfFontCharset.shiftJis => _notoSansJp, + PdfFontCharset.hangul => _notoSansKr, + PdfFontCharset.thai => _notoSansThai, + PdfFontCharset.hebrew => _notoSansHebrew, + PdfFontCharset.arabic => _notoSansArabic, + PdfFontCharset.greek || + PdfFontCharset.vietnamese || + PdfFontCharset.cyrillic || + PdfFontCharset.easternEuropean => query.isItalic ? _notoSansItalic : _notoSans, + PdfFontCharset.ansi || PdfFontCharset.default_ || PdfFontCharset.symbol => null, + }, + }; + if (fontTable == null) return null; + return _getNearestWeight(fontTable, query.weight); +} + +/// Noto Sans (Latin, Greek, Cyrillic, Vietnamese, and more) +/// +/// See: +/// * https://fonts.google.com/specimen/Noto+Sans +final _notoSans = { + 100: GoogleFontsFileSingle( + 'NotoSans', + 100, + 'bc6ceb177561b27cfb9123c0dd372a54774cb6bcebe4ce18c12706bbb7ee902c', + 523812, + ), + 200: GoogleFontsFileSingle( + 'NotoSans', + 200, + '807ad06b65dbbaf657e4a7dcb6d2b0734c8831cd21a1f9172387ad0411cc396f', + 524708, + ), + 300: GoogleFontsFileSingle( + 'NotoSans', + 300, + '4e3e9bb50c6e6ade7e4a491bf0033d6b6ec3326a2621834201e735691cec4968', + 524492, + ), + 400: GoogleFontsFileSingle( + 'NotoSans', + 400, + '725edd9b341324f91a3859e24824c455d43c31be72ca6e710acd0f95920d61ee', + 523940, + ), + 500: GoogleFontsFileSingle( + 'NotoSans', + 500, + 'a77c7c7a4d75c23c5e68bcff3d44f71eb1ec0f80fe245457053ea43a4ce61bd4', + 524252, + ), + 600: GoogleFontsFileSingle( + 'NotoSans', + 600, + 'fc5b5ba2d400f44b0686c46db557e6b8067a97ade7337f14f823f524675c038c', + 524444, + ), + 700: GoogleFontsFileSingle( + 'NotoSans', + 700, + '222685dcf83610e3e88a0ecd4c602efde7a7b832832502649bfe2dcf1aa0bf15', + 523772, + ), + 800: GoogleFontsFileSingle( + 'NotoSans', + 800, + 'c6e87f6834db59a2a64ce43dce2fdc1aa3441f2a23afb0bfd667621403ed688c', + 524672, + ), + 900: GoogleFontsFileSingle( + 'NotoSans', + 900, + '7ead4fec44c3271cf7dc5d9f74795eb05fa9fb3cedc7bde3232eb10573d5f6cd', + 524708, + ), +}; + +/// Noto Sans Italic (Latin, Greek, Cyrillic, Vietnamese, and more) +/// +/// See: +/// * https://fonts.google.com/specimen/Noto+Sans +final _notoSansItalic = { + 100: GoogleFontsFileSingle( + 'NotoSans-Italic', + 100, + '8b32677abe42a47cdade4998d4124a3e1b44efa656c5badf27de546768c82f0d', + 541316, + ), + 200: GoogleFontsFileSingle( + 'NotoSans-Italic', + 200, + 'd64c291d542bb1211538aa1448a7f6bbaca4dbd170e78b8b8242be5c9ff28959', + 541752, + ), + 300: GoogleFontsFileSingle( + 'NotoSans-Italic', + 300, + '3a902e6bbe1ffba43428cb2981f1185ef529505836c311af5f6e5690bf9b44c8', + 541688, + ), + 400: GoogleFontsFileSingle( + 'NotoSans-Italic', + 400, + '3d23478749575c0febb6169fc3dba6cb8cdb4202e8fb47ae1867c71a21792295', + 539972, + ), + 500: GoogleFontsFileSingle( + 'NotoSans-Italic', + 500, + '085819a42ab67069f29329ae066ff8206a4b518bf6496dbf1193284f891fdbd1', + 540456, + ), + 600: GoogleFontsFileSingle( + 'NotoSans-Italic', + 600, + 'ecb66a73df07fac622c73fdc0e4972bd51f50165367807433d7fc620378f9577', + 540608, + ), + 700: GoogleFontsFileSingle( + 'NotoSans-Italic', + 700, + 'f72d0f7c9c7279b2762017fbafa2bcd9aaccdf7a79b8cf686f874e2eeb0e51ce', + 540016, + ), + 800: GoogleFontsFileSingle( + 'NotoSans-Italic', + 800, + '0ef3e94eb6875007204e41604898141fa5104f7e20b87cb5640509a8f10430b5', + 540812, + ), + 900: GoogleFontsFileSingle( + 'NotoSans-Italic', + 900, + 'b0e0148ef878a4ca6a295b6b56b1bfb4773400ff8ee0a31a1338285725dd514f', + 540396, + ), +}; + +/// Noto Sans SC (Simplified Chinese) +/// +/// See: +/// * https://fonts.google.com/specimen/Noto+Sans+SC +final _notoSansSc = { + 100: GoogleFontsFileSingle( + 'NotoSansSC', + 100, + 'f1b8c2a287d23095abd470376c60519c9ff650ae8744b82bf76434ac5438982a', + 10538940, + ), + 200: GoogleFontsFileSingle( + 'NotoSansSC', + 200, + 'cba9bb657b61103aeb3cd0f360e8d3958c66febf59fbf58a4762f61e52015d36', + 10544320, + ), + 300: GoogleFontsFileSingle( + 'NotoSansSC', + 300, + '4cdbb86a1d6eca92c7bcaa0c759593bc2600a153600532584a8016c24eaca56c', + 10545812, + ), + 400: GoogleFontsFileSingle( + 'NotoSansSC', + 400, + 'eacedb2999b6cd30457f3820f277842f0dfbb28152a246fca8161779a8945425', + 10540772, + ), + 500: GoogleFontsFileSingle( + 'NotoSansSC', + 500, + '5383032c8e54fc5fa09773ce16483f64d9cdb7d1f8e87073a556051eb60f8529', + 10533968, + ), + 600: GoogleFontsFileSingle( + 'NotoSansSC', + 600, + '85c00dac0627c2c0184c24669735fad5adbb4f150bcb320c05620d46ed086381', + 10530476, + ), + 700: GoogleFontsFileSingle( + 'NotoSansSC', + 700, + 'a7a29b6d611205bb39b9a1a5c2be5a48416fbcbcfd7e6de98976e73ecb48720b', + 10530536, + ), + 800: GoogleFontsFileSingle( + 'NotoSansSC', + 800, + '038de57b1dc5f6428317a8b0fc11984789c25f49a9c24d47d33d2c03e3491d28', + 10525556, + ), + 900: GoogleFontsFileSingle( + 'NotoSansSC', + 900, + '501582a5e956ab1f4d9f9b2d683cf1646463eea291b21f928419da5e0c5a26eb', + 10521812, + ), +}; + +/// Noto Sans TC (Traditional Chinese) +/// +/// See: +/// * https://fonts.google.com/specimen/Noto+Sans+TC +final _notoSansTc = { + 100: GoogleFontsFileSingle( + 'NotoSansTC', + 100, + '53debc0456f3a7d4bdb00e14704fc29ea129d38bd8a9f6565cf656ddc27abb91', + 7089040, + ), + 200: GoogleFontsFileSingle( + 'NotoSansTC', + 200, + '5ef06c341be841ab9e166a9cc7ebc0e39cfe695da81d819672f3d14b3fca56a8', + 7092508, + ), + 300: GoogleFontsFileSingle( + 'NotoSansTC', + 300, + '9e50ec0d5779016c848855daa73f8d866ef323f0431d5770f53b60a1506f1c4a', + 7092872, + ), + 400: GoogleFontsFileSingle( + 'NotoSansTC', + 400, + 'b4f9cfdee95b77d72fe945347c0b7457f1ffc0d5d05eaf6ff688e60a86067c95', + 7090948, + ), + 500: GoogleFontsFileSingle( + 'NotoSansTC', + 500, + '2011294f66de6692639ee00a9e74d67bc9134f251100feb5448ab6322a4a2a75', + 7087068, + ), + 600: GoogleFontsFileSingle( + 'NotoSansTC', + 600, + '440471acbbc2a3b33bf11befde184b2cafe5b0fcde243e2b832357044baa4aa1', + 7084432, + ), + 700: GoogleFontsFileSingle( + 'NotoSansTC', + 700, + '22779de66d31884014b0530df89e69d596018a486a84a57994209dff1dcb97cf', + 7085728, + ), + 800: GoogleFontsFileSingle( + 'NotoSansTC', + 800, + 'f5e8e3e746319570b0979bfa3a90b6ec6a84ec38fe9e41c45a395724c31db7b4', + 7082400, + ), + 900: GoogleFontsFileSingle( + 'NotoSansTC', + 900, + '2b1ab3d7db76aa94006fa19dc38b61e93578833d2e3f268a0a3b0b1321852af6', + 7079980, + ), +}; + +/// Noto Sans JP (Japanese) +/// +/// See: +/// * https://fonts.google.com/specimen/Noto+Sans+JP +final _notoSansJp = { + 100: GoogleFontsFileSingle( + 'NotoSansJP', + 100, + '78a1fa1d16c437fe5d97df787782b6098a750350b5913b9f80089dc81f512417', + 5706804, + ), + 200: GoogleFontsFileSingle( + 'NotoSansJP', + 200, + 'c0532e4abf0ca438ea0e56749a3106a5badb2f10a89c8ba217b43dae4ec6e590', + 5708144, + ), + 300: GoogleFontsFileSingle( + 'NotoSansJP', + 300, + '64f10b3b9e06c99b76b16e1441174fba6adf994fcd6b8036cef2fbfa38535a84', + 5707688, + ), + 400: GoogleFontsFileSingle( + 'NotoSansJP', + 400, + '209c70f533554d512ef0a417b70dfe2997aeec080d2fe41695c55b361643f9ba', + 5703748, + ), + 500: GoogleFontsFileSingle( + 'NotoSansJP', + 500, + 'c5233cdc5a2901be5503f0d95ff48b4b5170afff6a39f95a076520cb73f17860', + 5700280, + ), + 600: GoogleFontsFileSingle( + 'NotoSansJP', + 600, + '852ad9268beb7d467374ec5ff0d416a22102c52d984ec21913f6d886409b85c4', + 5697576, + ), + 700: GoogleFontsFileSingle( + 'NotoSansJP', + 700, + 'eee16e4913b766be0eb7b9a02cd6ec3daf27292ca0ddf194cae01279aac1c9d0', + 5698756, + ), + 800: GoogleFontsFileSingle( + 'NotoSansJP', + 800, + '68d3c7136501158a6cf7d15c1c13e4af995aa164e34d1c250c3eef259cda74dd', + 5696016, + ), + 900: GoogleFontsFileSingle( + 'NotoSansJP', + 900, + '6ff9b55a270592e78670f98a2f866f621d05b6e1c3a18a14301da455a36f6561', + 5693644, + ), +}; + +/// Noto Sans KR (Korean) +/// +/// See: +/// * https://fonts.google.com/specimen/Noto+Sans+KR +final _notoSansKr = { + 100: GoogleFontsFileSingle( + 'NotoSansKR', + 100, + '302d55d333b15473a5b4909964ad17885a53cb41c34e3b434471f22ea55faea1', + 6177560, + ), + 200: GoogleFontsFileSingle( + 'NotoSansKR', + 200, + '1b03f89eccef4f2931d49db437091de1b15ced57186990749350a2cec1f4feb8', + 6177360, + ), + 300: GoogleFontsFileSingle( + 'NotoSansKR', + 300, + 'f8ed45f767a44de83d969ea276c3b4419c41a291d8460c32379e95930eae878e', + 6175264, + ), + 400: GoogleFontsFileSingle( + 'NotoSansKR', + 400, + '82547e25c2011910dae0116ba57d3ab9abd63f4865405677bd6f79c64487ae31', + 6169044, + ), + 500: GoogleFontsFileSingle( + 'NotoSansKR', + 500, + 'f67bdb1581dbb91b1ce92bdf89a0f3a4ca2545d821d204b17c5443bcda6b3677', + 6166588, + ), + 600: GoogleFontsFileSingle( + 'NotoSansKR', + 600, + '922e269443119b1ffa72c9631d4c7dcb365ab29ba1587b96e715d29c9a66d1b4', + 6165240, + ), + 700: GoogleFontsFileSingle( + 'NotoSansKR', + 700, + 'ed93ef6659b28599d47e40d020b9f55d18a01d94fdd43c9c171e44a66ddc1d66', + 6165036, + ), + 800: GoogleFontsFileSingle( + 'NotoSansKR', + 800, + 'e7088e3dfcc13f400aa9433a4042fce57b3dbe41038040073e9b5909a9390048', + 6164096, + ), + 900: GoogleFontsFileSingle( + 'NotoSansKR', + 900, + '14c5cfe30331277d21fa0086e66e11a7c414d4a5ce403229bdb0f384d3376888', + 6163040, + ), +}; + +/// Noto Sans Thai (Thai) +/// +/// See: +/// * https://fonts.google.com/specimen/Noto+Sans+Thai +final _notoSansThai = { + 100: GoogleFontsFileSingle( + 'NotoSansThai', + 100, + '77e781c33ba38f872109864fcf2f7bab58c7f85d73baf213fbcf7df2a7ea6b3f', + 45684, + ), + 200: GoogleFontsFileSingle( + 'NotoSansThai', + 200, + 'c8dc3faea7ead6f573771d50e3d2cc84b49431295bde43af0bd5f6356a628f72', + 45792, + ), + 300: GoogleFontsFileSingle( + 'NotoSansThai', + 300, + '9a1ba366a64ee23d486f48f0a276d75baef6432da4db5efb92f7c9b35dd5198d', + 45728, + ), + 400: GoogleFontsFileSingle( + 'NotoSansThai', + 400, + '5f71b18a03432951e2bce4e74497752958bd8c9976be06201af5390d47922be3', + 45636, + ), + 500: GoogleFontsFileSingle( + 'NotoSansThai', + 500, + '4c82507facc222df924a0272cda2bfdddc629de12b5684816aea0eb5851a61a7', + 45720, + ), + 600: GoogleFontsFileSingle( + 'NotoSansThai', + 600, + 'e81c6d83f8a625690b1ecc5de4f6b7b66a4d2ee9cbaf5b4f9ede73359c1db064', + 45732, + ), + 700: GoogleFontsFileSingle( + 'NotoSansThai', + 700, + '81bba197f8c779233db14166526e226f68e60cd9e33f2046b80f8075158cb433', + 45640, + ), + 800: GoogleFontsFileSingle( + 'NotoSansThai', + 800, + '7ae7ca1dae7a3df8e839ae08364e14e8e015337bab7dc2842abfc3315e477404', + 45704, + ), + 900: GoogleFontsFileSingle( + 'NotoSansThai', + 900, + '689d439d52c795a225c7fe4657a1072151407a86cc2910a51280337b8b1f57a3', + 45584, + ), +}; + +/// Noto Sans Hebrew (Hebrew) +/// +/// See: +/// * https://fonts.google.com/specimen/Noto+Sans+Hebrew +final _notoSansHebrew = { + 100: GoogleFontsFileSingle( + 'NotoSansHebrew', + 100, + '724a57dd8003a31bad4428c37d10b2777cec5b5bfd20c6ed1be44d265989b599', + 46472, + ), + 200: GoogleFontsFileSingle( + 'NotoSansHebrew', + 200, + 'ee40f0088e4408bd36620fd1fa7290fa145bf8964d2368aa181794e5b17ad819', + 46532, + ), + 300: GoogleFontsFileSingle( + 'NotoSansHebrew', + 300, + '5686c511d470cd4e52afd09f7e1f004efe33549ff0d38cb23fe3621de1969cc9', + 46488, + ), + 400: GoogleFontsFileSingle( + 'NotoSansHebrew', + 400, + '95e23e29b8422a9a461300a8b8e97630d8a2b8de319a9decbf53dc51e880ac41', + 46476, + ), + 500: GoogleFontsFileSingle( + 'NotoSansHebrew', + 500, + '7fa6696c1d7d0d7f4ac63f1c5dafdc52bf0035a3d5b63a181b58e5515af338f6', + 46652, + ), + 600: GoogleFontsFileSingle( + 'NotoSansHebrew', + 600, + 'cc6deb0701c8034e8ca4eb52ad13770cbe6e494a2bedb91238ad5cb7c591f0ae', + 46648, + ), + 700: GoogleFontsFileSingle( + 'NotoSansHebrew', + 700, + 'fbb2c56fd00f54b81ecb4da7033e1729f1c3fd2b14f19a15db35d3f3dd5aadf9', + 46440, + ), + 800: GoogleFontsFileSingle( + 'NotoSansHebrew', + 800, + '0fb06ecce97f71320c91adf9be6369c8c12979ac65d229fa7fb123f2476726a1', + 46472, + ), + 900: GoogleFontsFileSingle( + 'NotoSansHebrew', + 900, + '8638b2f26a6e16bacf0b34c34d5b8a62efa912a3a90bfb93f0eb25a7b3f8705e', + 46372, + ), +}; + +/// Noto Sans Arabic (Arabic) +/// +/// See: +/// * https://fonts.google.com/specimen/Noto+Sans+Arabic +final _notoSansArabic = { + 100: GoogleFontsFileSingle( + 'NotoSansArabic', + 100, + '6cf2614bfc2885011fd9d47b2bcc7e5a576b3e35d379d4301d8247683a680245', + 162152, + ), + 200: GoogleFontsFileSingle( + 'NotoSansArabic', + 200, + 'cecf509869241973813ea04cf6c437ff1e571722fcd54e329880185baf750b19', + 162412, + ), + 300: GoogleFontsFileSingle( + 'NotoSansArabic', + 300, + 'c5219bd6425340861eb21a05d40d54da31875cb534dd128d5799b6b83674b9d1', + 162324, + ), + 400: GoogleFontsFileSingle( + 'NotoSansArabic', + 400, + '25c2bf5bc8222800e2d8887c3af985f61d5803177bd92b355cb8bffa09c48862', + 161592, + ), + 500: GoogleFontsFileSingle( + 'NotoSansArabic', + 500, + '47f226b1505792703ac273600be1dbce8c3cc83cd1981b3db5ef15e0f09bdd8a', + 162156, + ), + 600: GoogleFontsFileSingle( + 'NotoSansArabic', + 600, + '332c2d597ed4d1f4d1ed84ed493a341cf81515f5e4d392789a4764e084ff4f1f', + 162512, + ), + 700: GoogleFontsFileSingle( + 'NotoSansArabic', + 700, + '9235e0a73b449ef9a790df7bf5933644ede59c06099f7e96d8cda26c999641cd', + 162268, + ), + 800: GoogleFontsFileSingle( + 'NotoSansArabic', + 800, + '3614725eeafdb55d8eeabb81fb6fb294a807327fa01c2230b4e074f56922d0b5', + 162896, + ), + 900: GoogleFontsFileSingle( + 'NotoSansArabic', + 900, + 'cdbb85b809be063fb065f55b7226dc5161f4804795be56e007d7d3ce70208446', + 162668, + ), +}; + +/// Noto Serif (Serif) +/// +/// See: +/// * https://fonts.google.com/specimen/Noto+Serif +final _notoSerif = { + 100: GoogleFontsFileSingle( + 'NotoSerif', + 100, + '7fd15a02691cfb99c193341bbb082778b1f3ca27e15fdcb7076816591994b7c7', + 452700, + ), + 200: GoogleFontsFileSingle( + 'NotoSerif', + 200, + '9446cf19cd57af964054d0afd385b76f9dec5e3b927c74a2d955041f97fad39b', + 453240, + ), + 300: GoogleFontsFileSingle( + 'NotoSerif', + 300, + '384650b173fced05061be4249607b7caedbc6ba463724075c3ede879ee78d456', + 453240, + ), + 400: GoogleFontsFileSingle( + 'NotoSerif', + 400, + 'b7373b9f9dab0875961c5d214edef00a9384ab593cde30c6462d7b29935ef8b2', + 452276, + ), + 500: GoogleFontsFileSingle( + 'NotoSerif', + 500, + '105a9e9c9bb80bcf8f8c408ed3473f1d9baad881686ea4602ecebebf22bbed50', + 453160, + ), + 600: GoogleFontsFileSingle( + 'NotoSerif', + 600, + '30257a49c70dd2e8abe6cc6a904df863dbc6f9ccf85f4b28a5c858aaa258eab6', + 453104, + ), + 700: GoogleFontsFileSingle( + 'NotoSerif', + 700, + 'dad0f53be4da04bfb608c81cfb72441fba851b336b2bd867592698cfaa2a0c3c', + 452576, + ), + 800: GoogleFontsFileSingle( + 'NotoSerif', + 800, + '12c5c47e6810fc5ea4291b6948adfba87c366eb3c081d66c99f989efd2b55975', + 454040, + ), + 900: GoogleFontsFileSingle( + 'NotoSerif', + 900, + '16f59df53d64f8a896e3dcacadc5b78e8b5fb503318bf01d9ddbe00e90dcceea', + 453924, + ), +}; + +/// Noto Serif (Italic) +/// +/// See: +/// * https://fonts.google.com/specimen/Noto+Serif +final _notoSerifItalic = { + 100: GoogleFontsFileSingle( + 'NotoSerif-Italic', + 100, + '98c7bc89a0eca32e9045076dd4557dadf866820b3faf5dffe946614cd59bdbb8', + 479008, + ), + 200: GoogleFontsFileSingle( + 'NotoSerif-Italic', + 200, + '24a3e4603729024047e3af2a77e85fd3064c604b193add5b5ecb48fdeb630f4e', + 479532, + ), + 300: GoogleFontsFileSingle( + 'NotoSerif-Italic', + 300, + '940fb65bf51f2a2306bc12343c9661aa4309634ea15bf2b1a0c8da2d23e9e9f3', + 479180, + ), + 400: GoogleFontsFileSingle( + 'NotoSerif-Italic', + 400, + '65aae32ed0a63e3f6ce0fcde1cd5d04cd179699f7e1fef0d36a24948a3b17ce3', + 477448, + ), + 500: GoogleFontsFileSingle( + 'NotoSerif-Italic', + 500, + '322ec18ea04041aabc9f9b3529ff23e7d4e4e18d4330d39d4d422058c66ddded', + 478256, + ), + 600: GoogleFontsFileSingle( + 'NotoSerif-Italic', + 600, + '77e9996939afbc0723270879a0754de4374091b9b856f19790c098292992859c', + 478316, + ), + 700: GoogleFontsFileSingle( + 'NotoSerif-Italic', + 700, + 'b4cf981f0033c2e3d72585d84de3980bdfb87eaa4fe1d95392025ecd0fe0b83c', + 477644, + ), + 800: GoogleFontsFileSingle( + 'NotoSerif-Italic', + 800, + 'a9d0052ceaeea5a1962b7b1a23d995e39dd299ae59cfc288d3e9a68f1bf002e7', + 478924, + ), + 900: GoogleFontsFileSingle( + 'NotoSerif-Italic', + 900, + '99f429bfa3aea82cc9620a6242992534d8c7b10f75d0ec7ca15e1790ca315de7', + 478760, + ), +}; + +/// Noto Serif SC (Simplified Chinese) +/// +/// See: +/// * https://fonts.google.com/specimen/Noto+Serif+SC +final _notoSerifSc = { + 200: GoogleFontsFileSingle( + 'NotoSerifSC', + 200, + '288d1ce3098084328c59b62c0ee3ae79a41f2c29eef8c0b2ba9384c2c18f41ed', + 14778664, + ), + 300: GoogleFontsFileSingle( + 'NotoSerifSC', + 300, + '7725ad7c403a2d10fd0fe29ae5d50445057a3559c348d67f129d0c9b8521bce8', + 14780440, + ), + 400: GoogleFontsFileSingle( + 'NotoSerifSC', + 400, + 'a17a0dbf1d43a65b75ebd0222a6aa4e6a6fb68f8ecc982c05c9584717ed3567f', + 14781184, + ), + 500: GoogleFontsFileSingle( + 'NotoSerifSC', + 500, + '6a74a2bb8923bef7e34b0436f0edd9ab03e3369fdeabb41807b820e6127fa4e6', + 14781200, + ), + 600: GoogleFontsFileSingle( + 'NotoSerifSC', + 600, + 'ebbd878444e9c226709d1259352d9d821849ee8105b5191d44101889603e154b', + 14780624, + ), + 700: GoogleFontsFileSingle( + 'NotoSerifSC', + 700, + 'bf6e98a81314a396a59661bf892ac872a9338c1b252845bec5659af39ca2304f', + 14780140, + ), + 800: GoogleFontsFileSingle( + 'NotoSerifSC', + 800, + '13be96afae56fd632bbf58ec62eb7b295af62fb6c7b3e16eff73748f0e04daf9', + 14780920, + ), + 900: GoogleFontsFileSingle( + 'NotoSerifSC', + 900, + 'e50e6bffa405fcb45583a0f40f120e1c158b83b4a17fae29bbe2359d36a5b831', + 14780544, + ), +}; + +/// Noto Serif TC (Traditional Chinese) +/// +/// See: +/// * https://fonts.google.com/specimen/Noto+Serif+TC +final _notoSerifTc = { + 200: GoogleFontsFileSingle( + 'NotoSerifTC', + 200, + '7d21dcf9bae351366c21de7a554917af318fdf928b5f17a820b547584ebd3b03', + 9926428, + ), + 300: GoogleFontsFileSingle( + 'NotoSerifTC', + 300, + '2816a6528f03c7c7364da893e52ee3247622aa67efd5b96fac5c800af0cf7cfd', + 9928912, + ), + 400: GoogleFontsFileSingle( + 'NotoSerifTC', + 400, + '33247894b46a436114cb173a756d5f5a698f485c9cd88427a50c72301a81282f', + 9930576, + ), + 500: GoogleFontsFileSingle( + 'NotoSerifTC', + 500, + '3b3fa68244c613cee26f10dae75f702d5c61908973a763f2a87a4d3c9c14298a', + 9932116, + ), + 600: GoogleFontsFileSingle( + 'NotoSerifTC', + 600, + '1251e0304fa33bbf5c44cb361a0a969f998af22377a7b8e0bd9e862cf6c45d76', + 9932824, + ), + 700: GoogleFontsFileSingle( + 'NotoSerifTC', + 700, + 'db3ce7ba3443c00e9ff3ba87ebc51838598cb44bc25ea946480f2aebd290ad0e', + 9933360, + ), + 800: GoogleFontsFileSingle( + 'NotoSerifTC', + 800, + '96de55c76632a173cbb6ec9224dbd3040fa75234fadee1d7d03b081debbbdd37', + 9933988, + ), + 900: GoogleFontsFileSingle( + 'NotoSerifTC', + 900, + '2b58e95c7c7a35311152cb28da071dd10a156c30b1cfde117bac68cdca4984ea', + 9934072, + ), +}; + +/// Noto Serif JP (Japanese) +/// +/// See: +/// * https://fonts.google.com/specimen/Noto+Serif+JP +final _notoSerifJp = { + 200: GoogleFontsFileSingle( + 'NotoSerifJP', + 200, + '320e653bbc19e207ade23a39d4896aee4424d85e213f6c3f05584d1dc358eaf3', + 7999636, + ), + 300: GoogleFontsFileSingle( + 'NotoSerifJP', + 300, + 'b01bd95435bede8e6e55adde97d61d85cf3cad907a8e5e21df3fdee97436c972', + 8000752, + ), + 400: GoogleFontsFileSingle( + 'NotoSerifJP', + 400, + '100644e0b414be1c2b1f524e63cb888a8ca2a29c59bc685b1d3a1dccdb8bef3d', + 8000776, + ), + 500: GoogleFontsFileSingle( + 'NotoSerifJP', + 500, + '7f2c9f09930f9571d72946c4836178d99966b6e3dae4d0fb6a39d9278a1979e7', + 7999616, + ), + 600: GoogleFontsFileSingle( + 'NotoSerifJP', + 600, + '53bcadccd57b01926f9da05cb4c3edf4a572fe9918d463b16ce2c8e76adcc059', + 7997840, + ), + 700: GoogleFontsFileSingle( + 'NotoSerifJP', + 700, + 'afcb90bae847b37af92ad759d2ed65ab5691eb6f76180a9f3f3eae9121afc30c', + 7995008, + ), + 800: GoogleFontsFileSingle( + 'NotoSerifJP', + 800, + '6341d1d0229059ed23e9f8293d29052cdc869a8a358118109165e8979c395342', + 7994148, + ), + 900: GoogleFontsFileSingle( + 'NotoSerifJP', + 900, + 'cb22da84d7cef667d91b79672b6a6457bcb22c9354ad8e96184a558a1eeb5786', + 7992068, + ), +}; + +/// Noto Serif KR (Korean) +/// +/// See: +/// * https://fonts.google.com/specimen/Noto+Serif+KR +final _notoSerifKr = { + 200: GoogleFontsFileSingle( + 'NotoSerifKR', + 200, + '54ba0237db05724a034c17d539fb253d29059dcb908cfc953c93b3e0d9de8197', + 14020456, + ), + 300: GoogleFontsFileSingle( + 'NotoSerifKR', + 300, + 'ae26b0d843cb7966777c3b764139d0de052c62e4bf52e47e24b20da304b17101', + 14029668, + ), + 400: GoogleFontsFileSingle( + 'NotoSerifKR', + 400, + '558c8dac58a96ed9bd55c0e3b605699b9ca87545eaba6e887bbf5c07a4e77e61', + 14032260, + ), + 500: GoogleFontsFileSingle( + 'NotoSerifKR', + 500, + 'f9534728d53d16ffa1e8a1382d95495e5ba8779be7cc7c70d2d40fff283bae93', + 14041584, + ), + 600: GoogleFontsFileSingle( + 'NotoSerifKR', + 600, + 'c571b015c56cee39099f0aaeeece3b81c49a8b206dd2ab577c03ca6bd4e2a7bb', + 14040680, + ), + 700: GoogleFontsFileSingle( + 'NotoSerifKR', + 700, + 'f5397eff043cbe24929663e25ddb03a3b383195c8b877b6a4fcc48ecc8247002', + 14038616, + ), + 800: GoogleFontsFileSingle( + 'NotoSerifKR', + 800, + 'abb4439400202f9efd9863fad31138021b95a579acb4ae98516311da0bbae842', + 14036636, + ), + 900: GoogleFontsFileSingle( + 'NotoSerifKR', + 900, + '17b5842749bdec2f53cb3c0ccbe8292ddf025864e0466fad64ca7b96e9f7be06', + 14031812, + ), +}; + +/// Noto Serif Thai +/// +/// See: +/// * https://fonts.google.com/specimen/Noto+Serif+Thai +final _notoSerifThai = { + 100: GoogleFontsFileSingle( + 'NotoSerifThai', + 100, + '5eb35c0094128d7d01680b8843b2da94cc9dc4da0367bd73d9067287b48cc074', + 59812, + ), + 200: GoogleFontsFileSingle( + 'NotoSerifThai', + 200, + '48d9621d9f86d32d042924a1dca011561a6e12bb6577ecf77737d966721c6f96', + 59968, + ), + 300: GoogleFontsFileSingle( + 'NotoSerifThai', + 300, + 'd7e9e8ab36992509761cfbb52a8ccc910571ef167bd2cf9a15b7e393185aeadf', + 59908, + ), + 400: GoogleFontsFileSingle( + 'NotoSerifThai', + 400, + '3b677be028abaef2960675aa839310cf8b76eb02dd776b005e535ce8fd7b0dba', + 59668, + ), + 500: GoogleFontsFileSingle( + 'NotoSerifThai', + 500, + '269e49f943f4d5e3caebf7d381eca11ec24a3179713e9fc9594664d29f00638b', + 59904, + ), + 600: GoogleFontsFileSingle( + 'NotoSerifThai', + 600, + 'c2f95d912f539a2afb1a4fcaff25b3cfec88ff80bab99abc18e7e2b8a2ed0371', + 59844, + ), + 700: GoogleFontsFileSingle( + 'NotoSerifThai', + 700, + '26cc8f7b7d541cc050522a077448d3069e480d35edbd314748ab819fbce36b12', + 59760, + ), + 800: GoogleFontsFileSingle( + 'NotoSerifThai', + 800, + 'c7bcf386351f299d1a0440e23d14334dd32fcc736451a25721557bb13bf7ee9d', + 60072, + ), + 900: GoogleFontsFileSingle( + 'NotoSerifThai', + 900, + '3700c400ed31b5a182e21b6269e583e7dff8b8e16400504a9979684488574efa', + 60004, + ), +}; + +/// Noto Serif Hebrew +/// +/// See: +/// - https://fonts.google.com/specimen/Noto+Serif+Hebrew +final _notoSerifHebrew = { + 100: GoogleFontsFileSingle( + 'NotoSerifHebrew', + 100, + 'd53174aa0c8cd8df260a9004a3007e393160b062d50f775fecd519f057067cbd', + 54652, + ), + 200: GoogleFontsFileSingle( + 'NotoSerifHebrew', + 200, + 'd31e71918ab5ff0f0e030903449509e146010510779991a47d4a063373f14a7c', + 54720, + ), + 300: GoogleFontsFileSingle( + 'NotoSerifHebrew', + 300, + '7017169ff82520c5bf669e4ab770ca0804795609313ce54c8a29b66df36cd20a', + 54804, + ), + 400: GoogleFontsFileSingle( + 'NotoSerifHebrew', + 400, + '001e675f8528148912f3c8b4ce0f2e3d05c7d6ff0cbaa4c415df9301cfeec28e', + 54612, + ), + 500: GoogleFontsFileSingle( + 'NotoSerifHebrew', + 500, + '4927576763b95c2ed87e58dbef8ac565d8054f419a4641d2eb6bb59afd498e6c', + 54704, + ), + 600: GoogleFontsFileSingle( + 'NotoSerifHebrew', + 600, + 'fd86539b46574a35e1898c62c3e30ff092e1b6588a36660bcf1e91845be1e36a', + 54712, + ), + 700: GoogleFontsFileSingle( + 'NotoSerifHebrew', + 700, + 'eb9fd16284df252ac1e4c53c73617a8e027cf66425e197f39c4cc7e9773baf4a', + 54632, + ), + 800: GoogleFontsFileSingle( + 'NotoSerifHebrew', + 800, + 'cdbfc88d81100057725ac72b7b26cc125b718916102f9771adeeb1b8ab890c36', + 54816, + ), + 900: GoogleFontsFileSingle( + 'NotoSerifHebrew', + 900, + 'ec3cf5173830f6e5485ef7f012b9b8dd0603116b32021d000269bf3dd1f18324', + 54744, + ), +}; + +/// Noto Naskh Arabic +/// +/// See: +/// * https://fonts.google.com/specimen/Noto+Naskh+Arabic +final _notoNaskhArabic = { + 400: GoogleFontsFileSingle( + 'NotoNaskhArabic', + 400, + 'a19b33c4365bbd6e3f3ac85864fb134e44358ad188c30a9d67d606685d5261da', + 215356, + ), + 500: GoogleFontsFileSingle( + 'NotoNaskhArabic', + 500, + 'd8639b9c7c51cc662e5cf98ab913988835ca5cfde7fdd6db376c6f39f4ac8ea8', + 215768, + ), + 600: GoogleFontsFileSingle( + 'NotoNaskhArabic', + 600, + '76501d5ae7dea1d55ded66269abc936ece44353e17a70473c64f7072c61d7e89', + 215720, + ), + 700: GoogleFontsFileSingle( + 'NotoNaskhArabic', + 700, + 'bb9d4b9c041d13d8bc2c01fa6c5a4629bb4d19a158eec78a8249420a59418aa4', + 215344, + ), +}; diff --git a/example/viewer/lib/outline_view.dart b/packages/pdfrx/example/viewer/lib/outline_view.dart similarity index 74% rename from example/viewer/lib/outline_view.dart rename to packages/pdfrx/example/viewer/lib/outline_view.dart index f7dbee1a..39e4589b 100644 --- a/example/viewer/lib/outline_view.dart +++ b/packages/pdfrx/example/viewer/lib/outline_view.dart @@ -5,11 +5,7 @@ import 'package:flutter/material.dart'; import 'package:pdfrx/pdfrx.dart'; class OutlineView extends StatelessWidget { - const OutlineView({ - required this.outline, - required this.controller, - super.key, - }); + const OutlineView({required this.outline, required this.controller, super.key}); final List? outline; final PdfViewerController controller; @@ -26,15 +22,8 @@ class OutlineView extends StatelessWidget { return InkWell( onTap: () => controller.goToDest(item.node.dest), child: Container( - margin: EdgeInsets.only( - left: item.level * 16.0 + 8, - top: 8, - bottom: 8, - ), - child: Text( - item.node.title, - softWrap: false, - ), + margin: EdgeInsets.only(left: item.level * 16.0 + 8, top: 8, bottom: 8), + child: Text(item.node.title, softWrap: false), ), ); }, diff --git a/example/viewer/lib/password_dialog.dart b/packages/pdfrx/example/viewer/lib/password_dialog.dart similarity index 70% rename from example/viewer/lib/password_dialog.dart rename to packages/pdfrx/example/viewer/lib/password_dialog.dart index 611a79ef..23fdbfee 100644 --- a/example/viewer/lib/password_dialog.dart +++ b/packages/pdfrx/example/viewer/lib/password_dialog.dart @@ -19,14 +19,8 @@ Future passwordDialog(BuildContext context) async { onSubmitted: (value) => Navigator.of(context).pop(value), ), actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(null), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(textController.text), - child: const Text('OK'), - ), + TextButton(onPressed: () => Navigator.of(context).pop(null), child: const Text('Cancel')), + TextButton(onPressed: () => Navigator.of(context).pop(textController.text), child: const Text('OK')), ], ); }, diff --git a/example/viewer/lib/search_view.dart b/packages/pdfrx/example/viewer/lib/search_view.dart similarity index 81% rename from example/viewer/lib/search_view.dart rename to packages/pdfrx/example/viewer/lib/search_view.dart index a5658ad9..14363e8b 100644 --- a/example/viewer/lib/search_view.dart +++ b/packages/pdfrx/example/viewer/lib/search_view.dart @@ -7,10 +7,7 @@ import 'package:synchronized/extension.dart'; // Simple Text Search View // class TextSearchView extends StatefulWidget { - const TextSearchView({ - required this.textSearcher, - super.key, - }); + const TextSearchView({required this.textSearcher, super.key}); final PdfTextSearcher textSearcher; @@ -57,8 +54,9 @@ class _TextSearchViewState extends State { } for (int i = _matchIndexToListIndex.length; i < widget.textSearcher.matches.length; i++) { if (i == 0 || widget.textSearcher.matches[i - 1].pageNumber != widget.textSearcher.matches[i].pageNumber) { - _listIndexToMatchIndex - .add(-widget.textSearcher.matches[i].pageNumber); // negative index to indicate page header + _listIndexToMatchIndex.add( + -widget.textSearcher.matches[i].pageNumber, + ); // negative index to indicate page header } _matchIndexToListIndex.add(_listIndexToMatchIndex.length); _listIndexToMatchIndex.add(i); @@ -74,10 +72,7 @@ class _TextSearchViewState extends State { return Column( children: [ widget.textSearcher.isSearching - ? LinearProgressIndicator( - value: widget.textSearcher.searchProgress, - minHeight: 4, - ) + ? LinearProgressIndicator(value: widget.textSearcher.searchProgress, minHeight: 4) : const SizedBox(height: 4), Row( children: [ @@ -90,9 +85,7 @@ class _TextSearchViewState extends State { autofocus: true, focusNode: focusNode, controller: searchTextController, - decoration: const InputDecoration( - contentPadding: EdgeInsets.only(right: 50), - ), + decoration: const InputDecoration(contentPadding: EdgeInsets.only(right: 50)), textInputAction: TextInputAction.search, // onSubmitted: (value) { // // just focus back to the text field @@ -104,10 +97,7 @@ class _TextSearchViewState extends State { alignment: Alignment.centerRight, child: Text( '${widget.textSearcher.currentIndex! + 1} / ${widget.textSearcher.matches.length}', - style: const TextStyle( - fontSize: 12, - color: Colors.grey, - ), + style: const TextStyle(fontSize: 12, color: Colors.grey), ), ), ], @@ -173,13 +163,7 @@ class _TextSearchViewState extends State { height: itemHeight, alignment: Alignment.bottomLeft, padding: const EdgeInsets.only(bottom: 10), - child: Text( - 'Page ${-matchIndex}', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), + child: Text('Page ${-matchIndex}', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), ); } }, @@ -199,11 +183,7 @@ class _TextSearchViewState extends State { curve: Curves.decelerate, ); } else if (newPos < pos.pixels) { - scrollController.animateTo( - newPos, - duration: const Duration(milliseconds: 300), - curve: Curves.decelerate, - ); + scrollController.animateTo(newPos, duration: const Duration(milliseconds: 300), curve: Curves.decelerate); } if (mounted) setState(() {}); @@ -220,7 +200,7 @@ class SearchResultTile extends StatefulWidget { super.key, }); - final PdfTextRangeWithFragments match; + final PdfPageTextRange match; final void Function() onTap; final PdfPageTextCache pageTextStore; final double height; @@ -271,12 +251,7 @@ class _SearchResultTileState extends State { onTap: () => widget.onTap(), child: Container( decoration: const BoxDecoration( - border: Border( - bottom: BorderSide( - color: Colors.black12, - width: 0.5, - ), - ), + border: Border(bottom: BorderSide(color: Colors.black12, width: 0.5)), ), padding: const EdgeInsets.all(3), child: text, @@ -286,19 +261,14 @@ class _SearchResultTileState extends State { ); } - TextSpan createTextSpanForMatch(PdfPageText? pageText, PdfTextRangeWithFragments match, {TextStyle? style}) { - style ??= const TextStyle( - fontSize: 14, - ); + TextSpan createTextSpanForMatch(PdfPageText? pageText, PdfPageTextRange match, {TextStyle? style}) { + style ??= const TextStyle(fontSize: 14); if (pageText == null) { - return TextSpan( - text: match.fragments.map((f) => f.text).join(), - style: style, - ); + return TextSpan(text: match.text, style: style); } final fullText = pageText.fullText; int first = 0; - for (int i = match.fragments.first.index - 1; i >= 0;) { + for (int i = match.start - 1; i >= 0;) { if (fullText[i] == '\n') { first = i + 1; break; @@ -306,25 +276,23 @@ class _SearchResultTileState extends State { i--; } int last = fullText.length; - for (int i = match.fragments.last.end; i < fullText.length; i++) { + for (int i = match.end; i < fullText.length; i++) { if (fullText[i] == '\n') { last = i; break; } } - final header = fullText.substring(first, match.fragments.first.index + match.start); - final body = fullText.substring(match.fragments.first.index + match.start, match.fragments.last.index + match.end); - final footer = fullText.substring(match.fragments.last.index + match.end, last); + final header = fullText.substring(first, match.start); + final body = fullText.substring(match.start, match.end); + final footer = fullText.substring(match.end, last); return TextSpan( children: [ TextSpan(text: header), TextSpan( text: body, - style: const TextStyle( - backgroundColor: Colors.yellow, - ), + style: const TextStyle(backgroundColor: Colors.yellow), ), TextSpan(text: footer), ], @@ -336,9 +304,7 @@ class _SearchResultTileState extends State { /// A helper class to cache loaded page texts. class PdfPageTextCache { final PdfTextSearcher textSearcher; - PdfPageTextCache({ - required this.textSearcher, - }); + PdfPageTextCache({required this.textSearcher}); final _pageTextRefs = {}; diff --git a/example/viewer/lib/thumbnails_view.dart b/packages/pdfrx/example/viewer/lib/thumbnails_view.dart similarity index 64% rename from example/viewer/lib/thumbnails_view.dart rename to packages/pdfrx/example/viewer/lib/thumbnails_view.dart index c6360c58..d58ab664 100644 --- a/example/viewer/lib/thumbnails_view.dart +++ b/packages/pdfrx/example/viewer/lib/thumbnails_view.dart @@ -27,22 +27,15 @@ class ThumbnailsView extends StatelessWidget { child: Column( children: [ SizedBox( + key: ValueKey('thumb_${document!.hashCode}_$index'), height: 220, child: InkWell( - onTap: () => controller!.goToPage( - pageNumber: index + 1, - anchor: PdfPageAnchor.top, - ), - child: PdfPageView( - document: document, - pageNumber: index + 1, - alignment: Alignment.center, - ), + onTap: () => controller!.goToPage(pageNumber: index + 1, anchor: PdfPageAnchor.top), + onDoubleTap: () => onDoubleTap(document, index + 1), + child: PdfPageView(document: document, pageNumber: index + 1, alignment: Alignment.center), ), ), - Text( - '${index + 1}', - ), + Text('${index + 1}'), ], ), ); @@ -51,4 +44,10 @@ class ThumbnailsView extends StatelessWidget { ), ); } + + void onDoubleTap(PdfDocument document, int pageNumber) { + final pages = document.pages.toList(); + //pages[pageNumber - 1] = pages[pageNumber - 1].rotatedCCW90(); + document.pages = pages..removeAt(pageNumber - 1); + } } diff --git a/packages/pdfrx/example/viewer/linux/.gitignore b/packages/pdfrx/example/viewer/linux/.gitignore new file mode 100644 index 00000000..d3896c98 --- /dev/null +++ b/packages/pdfrx/example/viewer/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/example/viewer/linux/CMakeLists.txt b/packages/pdfrx/example/viewer/linux/CMakeLists.txt similarity index 100% rename from example/viewer/linux/CMakeLists.txt rename to packages/pdfrx/example/viewer/linux/CMakeLists.txt diff --git a/packages/pdfrx/example/viewer/linux/flutter/CMakeLists.txt b/packages/pdfrx/example/viewer/linux/flutter/CMakeLists.txt new file mode 100644 index 00000000..d5bd0164 --- /dev/null +++ b/packages/pdfrx/example/viewer/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/example/viewer/linux/flutter/generated_plugin_registrant.cc b/packages/pdfrx/example/viewer/linux/flutter/generated_plugin_registrant.cc similarity index 100% rename from example/viewer/linux/flutter/generated_plugin_registrant.cc rename to packages/pdfrx/example/viewer/linux/flutter/generated_plugin_registrant.cc diff --git a/packages/pdfrx/example/viewer/linux/flutter/generated_plugin_registrant.h b/packages/pdfrx/example/viewer/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..e0f0a47b --- /dev/null +++ b/packages/pdfrx/example/viewer/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/example/viewer/linux/flutter/generated_plugins.cmake b/packages/pdfrx/example/viewer/linux/flutter/generated_plugins.cmake similarity index 97% rename from example/viewer/linux/flutter/generated_plugins.cmake rename to packages/pdfrx/example/viewer/linux/flutter/generated_plugins.cmake index 409ae740..1647b76a 100644 --- a/example/viewer/linux/flutter/generated_plugins.cmake +++ b/packages/pdfrx/example/viewer/linux/flutter/generated_plugins.cmake @@ -8,7 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST - pdfrx + pdfium_flutter ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/packages/pdfrx/example/viewer/linux/main.cc b/packages/pdfrx/example/viewer/linux/main.cc new file mode 100644 index 00000000..e7c5c543 --- /dev/null +++ b/packages/pdfrx/example/viewer/linux/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/example/viewer/linux/my_application.cc b/packages/pdfrx/example/viewer/linux/my_application.cc similarity index 100% rename from example/viewer/linux/my_application.cc rename to packages/pdfrx/example/viewer/linux/my_application.cc diff --git a/packages/pdfrx/example/viewer/linux/my_application.h b/packages/pdfrx/example/viewer/linux/my_application.h new file mode 100644 index 00000000..72271d5e --- /dev/null +++ b/packages/pdfrx/example/viewer/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/packages/pdfrx/example/viewer/macos/.gitignore b/packages/pdfrx/example/viewer/macos/.gitignore new file mode 100644 index 00000000..746adbb6 --- /dev/null +++ b/packages/pdfrx/example/viewer/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/packages/pdfrx/example/viewer/macos/Flutter/Flutter-Debug.xcconfig b/packages/pdfrx/example/viewer/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 00000000..4b81f9b2 --- /dev/null +++ b/packages/pdfrx/example/viewer/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/pdfrx/example/viewer/macos/Flutter/Flutter-Release.xcconfig b/packages/pdfrx/example/viewer/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 00000000..5caa9d15 --- /dev/null +++ b/packages/pdfrx/example/viewer/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/example/viewer/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/pdfrx/example/viewer/macos/Flutter/GeneratedPluginRegistrant.swift similarity index 100% rename from example/viewer/macos/Flutter/GeneratedPluginRegistrant.swift rename to packages/pdfrx/example/viewer/macos/Flutter/GeneratedPluginRegistrant.swift diff --git a/example/viewer/macos/Podfile b/packages/pdfrx/example/viewer/macos/Podfile similarity index 98% rename from example/viewer/macos/Podfile rename to packages/pdfrx/example/viewer/macos/Podfile index c795730d..b52666a1 100644 --- a/example/viewer/macos/Podfile +++ b/packages/pdfrx/example/viewer/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.14' +platform :osx, '10.15' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/example/viewer/macos/Podfile.lock b/packages/pdfrx/example/viewer/macos/Podfile.lock similarity index 63% rename from example/viewer/macos/Podfile.lock rename to packages/pdfrx/example/viewer/macos/Podfile.lock index c8757c23..caf5cd15 100644 --- a/example/viewer/macos/Podfile.lock +++ b/packages/pdfrx/example/viewer/macos/Podfile.lock @@ -5,7 +5,7 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - pdfrx (0.0.3): + - pdfium_flutter (0.1.0): - Flutter - FlutterMacOS - url_launcher_macos (0.0.1): @@ -15,7 +15,7 @@ DEPENDENCIES: - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - - pdfrx (from `Flutter/ephemeral/.symlinks/plugins/pdfrx/darwin`) + - pdfium_flutter (from `Flutter/ephemeral/.symlinks/plugins/pdfium_flutter/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) EXTERNAL SOURCES: @@ -25,18 +25,18 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral path_provider_foundation: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin - pdfrx: - :path: Flutter/ephemeral/.symlinks/plugins/pdfrx/darwin + pdfium_flutter: + :path: Flutter/ephemeral/.symlinks/plugins/pdfium_flutter/darwin url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos SPEC CHECKSUMS: - file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d - FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - pdfrx: 07fc287c47ea8d027c4ed56457f8a1aa74d73594 - url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 + file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7 + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 + pdfium_flutter: 5dbf80fb3ce6d3a333a4b895a7b16844da6f9ac9 + url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd -PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 +PODFILE CHECKSUM: 9ebaf0ce3d369aaa26a9ea0e159195ed94724cf3 COCOAPODS: 1.16.2 diff --git a/example/viewer/macos/Runner.xcodeproj/project.pbxproj b/packages/pdfrx/example/viewer/macos/Runner.xcodeproj/project.pbxproj similarity index 79% rename from example/viewer/macos/Runner.xcodeproj/project.pbxproj rename to packages/pdfrx/example/viewer/macos/Runner.xcodeproj/project.pbxproj index 2dc79f6e..e8ff76d2 100644 --- a/example/viewer/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/pdfrx/example/viewer/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 60; objects = { /* Begin PBXAggregateTarget section */ @@ -21,14 +21,13 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ - 197F95E95681452C2C706E26 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E810B0C266EEE0A56DD759CF /* Pods_Runner.framework */; }; 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 7FC10F0346A1C9FEAEC7A56E /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A50F6EA4CAA1D0D6E368554 /* Pods_RunnerTests.framework */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -62,8 +61,6 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 1A50F6EA4CAA1D0D6E368554 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 2728056CB87CCEF7E7B9E3AD /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; @@ -80,14 +77,9 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 5F27827074D121E8149E1E36 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - A392714C36B02B18E76DEBDE /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; - D1F1A0FA3EB21E69A5CB6616 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - E4EF327FA5D50CED341F92A5 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; - E810B0C266EEE0A56DD759CF /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F1A3FB6C4BBE46ABE02C84CE /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -95,7 +87,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 7FC10F0346A1C9FEAEC7A56E /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -103,7 +94,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 197F95E95681452C2C706E26 /* Pods_Runner.framework in Frameworks */, + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -113,14 +104,7 @@ 2549777989FCB1E18D7F6133 /* Pods */ = { isa = PBXGroup; children = ( - 2728056CB87CCEF7E7B9E3AD /* Pods-Runner.debug.xcconfig */, - F1A3FB6C4BBE46ABE02C84CE /* Pods-Runner.release.xcconfig */, - D1F1A0FA3EB21E69A5CB6616 /* Pods-Runner.profile.xcconfig */, - 5F27827074D121E8149E1E36 /* Pods-RunnerTests.debug.xcconfig */, - A392714C36B02B18E76DEBDE /* Pods-RunnerTests.release.xcconfig */, - E4EF327FA5D50CED341F92A5 /* Pods-RunnerTests.profile.xcconfig */, - ); - name = Pods; + ); path = Pods; sourceTree = ""; }; @@ -150,7 +134,6 @@ 33CEB47122A05771004F2AC0 /* Flutter */, 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, - D73912EC22F37F3D000D13A0 /* Frameworks */, 2549777989FCB1E18D7F6133 /* Pods */, ); sourceTree = ""; @@ -178,6 +161,7 @@ 33CEB47122A05771004F2AC0 /* Flutter */ = { isa = PBXGroup; children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, @@ -199,15 +183,6 @@ path = Runner; sourceTree = ""; }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - E810B0C266EEE0A56DD759CF /* Pods_Runner.framework */, - 1A50F6EA4CAA1D0D6E368554 /* Pods_RunnerTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -215,7 +190,6 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 9C1586A419FF5DDDD0DFC35A /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -234,13 +208,11 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - AD9238A20634DDAB9D2BD9BF /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, - D724C1B933D3A4A79B1A27D3 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -248,6 +220,9 @@ 33CC11202044C79F0003C045 /* PBXTargetDependency */, ); name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); productName = Runner; productReference = 33CC10ED2044A3C60003C045 /* pdfrx_example.app */; productType = "com.apple.product-type.application"; @@ -269,7 +244,6 @@ 33CC10EC2044A3C60003C045 = { CreatedOnToolsVersion = 9.2; LastSwiftMigration = 1100; - ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.Sandbox = { enabled = 1; @@ -291,6 +265,9 @@ Base, ); mainGroup = 33CC10E42044A3C60003C045; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -360,67 +337,6 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; - 9C1586A419FF5DDDD0DFC35A /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - AD9238A20634DDAB9D2BD9BF /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - D724C1B933D3A4A79B1A27D3 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -472,7 +388,6 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 5F27827074D121E8149E1E36 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -487,7 +402,6 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = A392714C36B02B18E76DEBDE /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -502,7 +416,6 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = E4EF327FA5D50CED341F92A5 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -553,7 +466,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -568,13 +481,17 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = XRDM278W3T; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 12.4; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -632,7 +549,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -679,7 +596,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -694,13 +611,17 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = XRDM278W3T; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 12.4; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -714,13 +635,17 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + DEVELOPMENT_TEAM = XRDM278W3T; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 12.4; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -786,6 +711,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; } diff --git a/packages/pdfrx/example/viewer/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/pdfrx/example/viewer/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/packages/pdfrx/example/viewer/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/pdfrx/example/viewer/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/pdfrx/example/viewer/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..56aa5ca6 --- /dev/null +++ b/packages/pdfrx/example/viewer/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/pdfrx/example/viewer/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/pdfrx/example/viewer/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/packages/pdfrx/example/viewer/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/pdfrx/example/viewer/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/pdfrx/example/viewer/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/packages/pdfrx/example/viewer/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/pdfrx/example/viewer/macos/Runner/AppDelegate.swift b/packages/pdfrx/example/viewer/macos/Runner/AppDelegate.swift new file mode 100644 index 00000000..b3c17614 --- /dev/null +++ b/packages/pdfrx/example/viewer/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/packages/pdfrx/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/pdfrx/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..a2ec33f1 --- /dev/null +++ b/packages/pdfrx/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/pdfrx/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/pdfrx/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 00000000..82b6f9d9 Binary files /dev/null and b/packages/pdfrx/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/packages/pdfrx/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/pdfrx/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 00000000..13b35eba Binary files /dev/null and b/packages/pdfrx/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/packages/pdfrx/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/pdfrx/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 00000000..0a3f5fa4 Binary files /dev/null and b/packages/pdfrx/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/packages/pdfrx/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/pdfrx/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 00000000..bdb57226 Binary files /dev/null and b/packages/pdfrx/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/packages/pdfrx/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/pdfrx/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 00000000..f083318e Binary files /dev/null and b/packages/pdfrx/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/packages/pdfrx/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/pdfrx/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 00000000..326c0e72 Binary files /dev/null and b/packages/pdfrx/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/packages/pdfrx/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/pdfrx/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 00000000..2f1632cf Binary files /dev/null and b/packages/pdfrx/example/viewer/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/packages/pdfrx/example/viewer/macos/Runner/Base.lproj/MainMenu.xib b/packages/pdfrx/example/viewer/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 00000000..80e867a4 --- /dev/null +++ b/packages/pdfrx/example/viewer/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/viewer/macos/Runner/Configs/AppInfo.xcconfig b/packages/pdfrx/example/viewer/macos/Runner/Configs/AppInfo.xcconfig similarity index 100% rename from example/viewer/macos/Runner/Configs/AppInfo.xcconfig rename to packages/pdfrx/example/viewer/macos/Runner/Configs/AppInfo.xcconfig diff --git a/packages/pdfrx/example/viewer/macos/Runner/Configs/Debug.xcconfig b/packages/pdfrx/example/viewer/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 00000000..36b0fd94 --- /dev/null +++ b/packages/pdfrx/example/viewer/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/pdfrx/example/viewer/macos/Runner/Configs/Release.xcconfig b/packages/pdfrx/example/viewer/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 00000000..dff4f495 --- /dev/null +++ b/packages/pdfrx/example/viewer/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/packages/pdfrx/example/viewer/macos/Runner/Configs/Warnings.xcconfig b/packages/pdfrx/example/viewer/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 00000000..42bcbf47 --- /dev/null +++ b/packages/pdfrx/example/viewer/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/example/viewer/macos/Runner/DebugProfile.entitlements b/packages/pdfrx/example/viewer/macos/Runner/DebugProfile.entitlements similarity index 100% rename from example/viewer/macos/Runner/DebugProfile.entitlements rename to packages/pdfrx/example/viewer/macos/Runner/DebugProfile.entitlements diff --git a/packages/pdfrx/example/viewer/macos/Runner/Info.plist b/packages/pdfrx/example/viewer/macos/Runner/Info.plist new file mode 100644 index 00000000..4789daa6 --- /dev/null +++ b/packages/pdfrx/example/viewer/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/packages/pdfrx/example/viewer/macos/Runner/MainFlutterWindow.swift b/packages/pdfrx/example/viewer/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 00000000..3cc05eb2 --- /dev/null +++ b/packages/pdfrx/example/viewer/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/example/viewer/macos/Runner/Release.entitlements b/packages/pdfrx/example/viewer/macos/Runner/Release.entitlements similarity index 100% rename from example/viewer/macos/Runner/Release.entitlements rename to packages/pdfrx/example/viewer/macos/Runner/Release.entitlements diff --git a/example/viewer/macos/RunnerTests/RunnerTests.swift b/packages/pdfrx/example/viewer/macos/RunnerTests/RunnerTests.swift similarity index 100% rename from example/viewer/macos/RunnerTests/RunnerTests.swift rename to packages/pdfrx/example/viewer/macos/RunnerTests/RunnerTests.swift diff --git a/example/viewer/pubspec.yaml b/packages/pdfrx/example/viewer/pubspec.yaml similarity index 66% rename from example/viewer/pubspec.yaml rename to packages/pdfrx/example/viewer/pubspec.yaml index b19f2fe3..0b63a154 100644 --- a/example/viewer/pubspec.yaml +++ b/packages/pdfrx/example/viewer/pubspec.yaml @@ -3,8 +3,8 @@ description: "Demonstrates how to use the pdfrx plugin." publish_to: 'none' environment: - sdk: '>=3.3.0 <4.0.0' - flutter: '>=3.19.0' + sdk: ^3.9.2 +resolution: workspace dependencies: flutter: @@ -14,20 +14,18 @@ dependencies: pdfrx: path: ../../ - # pdfrx_wasm: - # path: ../../wasm/pdfrx_wasm/ cupertino_icons: ^1.0.8 rxdart: ^0.28.0 - url_launcher: ^6.3.1 - synchronized: ^3.3.0 - file_selector: ^1.0.3 + url_launcher: ^6.3.2 + synchronized: ^3.4.0 + file_selector: ^1.0.4 + http: ^1.5.0 dev_dependencies: flutter_test: sdk: flutter - - flutter_lints: ^5.0.0 + flutter_lints: flutter: uses-material-design: true diff --git a/packages/pdfrx/example/viewer/web/favicon.png b/packages/pdfrx/example/viewer/web/favicon.png new file mode 100644 index 00000000..8aaa46ac Binary files /dev/null and b/packages/pdfrx/example/viewer/web/favicon.png differ diff --git a/packages/pdfrx/example/viewer/web/icons/Icon-192.png b/packages/pdfrx/example/viewer/web/icons/Icon-192.png new file mode 100644 index 00000000..b749bfef Binary files /dev/null and b/packages/pdfrx/example/viewer/web/icons/Icon-192.png differ diff --git a/packages/pdfrx/example/viewer/web/icons/Icon-512.png b/packages/pdfrx/example/viewer/web/icons/Icon-512.png new file mode 100644 index 00000000..88cfd48d Binary files /dev/null and b/packages/pdfrx/example/viewer/web/icons/Icon-512.png differ diff --git a/packages/pdfrx/example/viewer/web/icons/Icon-maskable-192.png b/packages/pdfrx/example/viewer/web/icons/Icon-maskable-192.png new file mode 100644 index 00000000..eb9b4d76 Binary files /dev/null and b/packages/pdfrx/example/viewer/web/icons/Icon-maskable-192.png differ diff --git a/packages/pdfrx/example/viewer/web/icons/Icon-maskable-512.png b/packages/pdfrx/example/viewer/web/icons/Icon-maskable-512.png new file mode 100644 index 00000000..d69c5669 Binary files /dev/null and b/packages/pdfrx/example/viewer/web/icons/Icon-maskable-512.png differ diff --git a/example/viewer/web/index.html b/packages/pdfrx/example/viewer/web/index.html similarity index 79% rename from example/viewer/web/index.html rename to packages/pdfrx/example/viewer/web/index.html index 64196cf0..aa18d18f 100644 --- a/example/viewer/web/index.html +++ b/packages/pdfrx/example/viewer/web/index.html @@ -8,14 +8,17 @@ The path provided below has to start and end with a slash "/" in order for it to work correctly. - Fore more details: + For more details: * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base + + This is a placeholder for base href that will be replaced by the value of + the `--base-href` argument provided to `flutter build`. --> - + - + diff --git a/example/viewer/web/manifest.json b/packages/pdfrx/example/viewer/web/manifest.json similarity index 100% rename from example/viewer/web/manifest.json rename to packages/pdfrx/example/viewer/web/manifest.json diff --git a/packages/pdfrx/example/viewer/windows/.gitignore b/packages/pdfrx/example/viewer/windows/.gitignore new file mode 100644 index 00000000..d492d0d9 --- /dev/null +++ b/packages/pdfrx/example/viewer/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/example/viewer/windows/CMakeLists.txt b/packages/pdfrx/example/viewer/windows/CMakeLists.txt similarity index 100% rename from example/viewer/windows/CMakeLists.txt rename to packages/pdfrx/example/viewer/windows/CMakeLists.txt diff --git a/packages/pdfrx/example/viewer/windows/flutter/CMakeLists.txt b/packages/pdfrx/example/viewer/windows/flutter/CMakeLists.txt new file mode 100644 index 00000000..903f4899 --- /dev/null +++ b/packages/pdfrx/example/viewer/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/example/viewer/windows/flutter/generated_plugin_registrant.cc b/packages/pdfrx/example/viewer/windows/flutter/generated_plugin_registrant.cc similarity index 100% rename from example/viewer/windows/flutter/generated_plugin_registrant.cc rename to packages/pdfrx/example/viewer/windows/flutter/generated_plugin_registrant.cc diff --git a/packages/pdfrx/example/viewer/windows/flutter/generated_plugin_registrant.h b/packages/pdfrx/example/viewer/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..dc139d85 --- /dev/null +++ b/packages/pdfrx/example/viewer/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/example/viewer/windows/flutter/generated_plugins.cmake b/packages/pdfrx/example/viewer/windows/flutter/generated_plugins.cmake similarity index 97% rename from example/viewer/windows/flutter/generated_plugins.cmake rename to packages/pdfrx/example/viewer/windows/flutter/generated_plugins.cmake index 3f90e6e6..399e6936 100644 --- a/example/viewer/windows/flutter/generated_plugins.cmake +++ b/packages/pdfrx/example/viewer/windows/flutter/generated_plugins.cmake @@ -8,7 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST - pdfrx + pdfium_flutter ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/packages/pdfrx/example/viewer/windows/runner/CMakeLists.txt b/packages/pdfrx/example/viewer/windows/runner/CMakeLists.txt new file mode 100644 index 00000000..394917c0 --- /dev/null +++ b/packages/pdfrx/example/viewer/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/example/viewer/windows/runner/Runner.rc b/packages/pdfrx/example/viewer/windows/runner/Runner.rc similarity index 100% rename from example/viewer/windows/runner/Runner.rc rename to packages/pdfrx/example/viewer/windows/runner/Runner.rc diff --git a/packages/pdfrx/example/viewer/windows/runner/flutter_window.cpp b/packages/pdfrx/example/viewer/windows/runner/flutter_window.cpp new file mode 100644 index 00000000..955ee303 --- /dev/null +++ b/packages/pdfrx/example/viewer/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/packages/pdfrx/example/viewer/windows/runner/flutter_window.h b/packages/pdfrx/example/viewer/windows/runner/flutter_window.h new file mode 100644 index 00000000..6da0652f --- /dev/null +++ b/packages/pdfrx/example/viewer/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/example/viewer/windows/runner/main.cpp b/packages/pdfrx/example/viewer/windows/runner/main.cpp similarity index 100% rename from example/viewer/windows/runner/main.cpp rename to packages/pdfrx/example/viewer/windows/runner/main.cpp diff --git a/packages/pdfrx/example/viewer/windows/runner/resource.h b/packages/pdfrx/example/viewer/windows/runner/resource.h new file mode 100644 index 00000000..66a65d1e --- /dev/null +++ b/packages/pdfrx/example/viewer/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/packages/pdfrx/example/viewer/windows/runner/resources/app_icon.ico b/packages/pdfrx/example/viewer/windows/runner/resources/app_icon.ico new file mode 100644 index 00000000..c04e20ca Binary files /dev/null and b/packages/pdfrx/example/viewer/windows/runner/resources/app_icon.ico differ diff --git a/example/viewer/windows/runner/runner.exe.manifest b/packages/pdfrx/example/viewer/windows/runner/runner.exe.manifest similarity index 100% rename from example/viewer/windows/runner/runner.exe.manifest rename to packages/pdfrx/example/viewer/windows/runner/runner.exe.manifest diff --git a/example/viewer/windows/runner/utils.cpp b/packages/pdfrx/example/viewer/windows/runner/utils.cpp similarity index 100% rename from example/viewer/windows/runner/utils.cpp rename to packages/pdfrx/example/viewer/windows/runner/utils.cpp diff --git a/packages/pdfrx/example/viewer/windows/runner/utils.h b/packages/pdfrx/example/viewer/windows/runner/utils.h new file mode 100644 index 00000000..3879d547 --- /dev/null +++ b/packages/pdfrx/example/viewer/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/packages/pdfrx/example/viewer/windows/runner/win32_window.cpp b/packages/pdfrx/example/viewer/windows/runner/win32_window.cpp new file mode 100644 index 00000000..60608d0f --- /dev/null +++ b/packages/pdfrx/example/viewer/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/packages/pdfrx/example/viewer/windows/runner/win32_window.h b/packages/pdfrx/example/viewer/windows/runner/win32_window.h new file mode 100644 index 00000000..e901dde6 --- /dev/null +++ b/packages/pdfrx/example/viewer/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/lib/pdfrx.dart b/packages/pdfrx/lib/pdfrx.dart similarity index 50% rename from lib/pdfrx.dart rename to packages/pdfrx/lib/pdfrx.dart index b22ceea4..282c739b 100644 --- a/lib/pdfrx.dart +++ b/packages/pdfrx/lib/pdfrx.dart @@ -1,9 +1,12 @@ -export 'src/pdf_api.dart'; +// for backward compatibility, we'd better export pdfrx_engine APIs as well +export 'package:pdfrx_engine/pdfrx_engine.dart'; + export 'src/pdf_document_ref.dart'; -export 'src/web/pdfjs_configuration.dart'; -export 'src/widgets/pdf_page_text_overlay.dart'; +export 'src/pdfrx_flutter.dart'; +export 'src/widgets/pdf_page_layout.dart'; export 'src/widgets/pdf_text_searcher.dart'; export 'src/widgets/pdf_viewer.dart'; export 'src/widgets/pdf_viewer_params.dart'; export 'src/widgets/pdf_viewer_scroll_thumb.dart'; export 'src/widgets/pdf_widgets.dart'; +export 'src/utils/fixed_overscroll_physics.dart'; diff --git a/lib/src/pdf_document_ref.dart b/packages/pdfrx/lib/src/pdf_document_ref.dart similarity index 54% rename from lib/src/pdf_document_ref.dart rename to packages/pdfrx/lib/src/pdf_document_ref.dart index 6d575db5..148cc791 100644 --- a/lib/src/pdf_document_ref.dart +++ b/packages/pdfrx/lib/src/pdf_document_ref.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:synchronized/extension.dart'; @@ -11,14 +12,36 @@ import '../pdfrx.dart'; /// [totalBytes] is the total number of bytes to download. It may be null if the total size is unknown. typedef PdfDocumentLoaderProgressCallback = void Function(int downloadedBytes, [int? totalBytes]); -/// Callback function to report download status on completion. +/// A key that identifies the source of a [PdfDocumentRef]. /// -/// [downloaded] is the number of bytes downloaded. -/// [total] is the total number of bytes downloaded. -/// [elapsedTime] is the time taken to download the file. -typedef PdfDocumentLoaderReportCallback = void Function(int downloaded, int total, Duration elapsedTime); +/// It is used to cache and share [PdfDocumentListenable] instances for [PdfDocumentRef]s that refer to the same document. +/// +/// This class supercedes the previous [sourceName] property of [PdfDocumentRef] to provide a more flexible way to +/// identify the source. +class PdfDocumentRefKey { + PdfDocumentRefKey(this.sourceName, [Iterable parts = const []]) : parts = List.unmodifiable(parts); + + /// A name that identifies the source of the document. + final String sourceName; + + /// Additional parts to identify the source uniquely. + /// + /// For example, if the document is identified not only by the URI but also by some HTTP headers + /// (for example, authentication/authorization headers), you can include the headers in the parts. + final List parts; + + @override + bool operator ==(Object other) => + other is PdfDocumentRefKey && sourceName == other.sourceName && listEquals(parts, other.parts); + + @override + int get hashCode => Object.hash(sourceName, const ListEquality().hash(parts)); + + @override + String toString() => 'PdfDocumentRefKey($sourceName)'; +} -/// PdfDocumentRef controls loading of a [PdfDocument] and it also provide you with a way to use [PdfDocument] +/// PdfDocumentRef controls loading/caching of a [PdfDocument] and it also provide you with a way to use [PdfDocument] /// safely in your long running async operations. /// /// There are several types of [PdfDocumentRef]s predefined: @@ -42,15 +65,32 @@ typedef PdfDocumentLoaderReportCallback = void Function(int downloaded, int tota /// ``` /// abstract class PdfDocumentRef { - const PdfDocumentRef({this.autoDispose = true}); + /// Creates a new instance of [PdfDocumentRef]. + const PdfDocumentRef({required this.key, this.autoDispose = true}); /// Whether to dispose the document on reference dispose or not. final bool autoDispose; - /// Source name to identify the reference. - String get sourceName; + /// A name that identifies the source of the document. + /// + /// [PdfDocument] is cached based on the [PdfDocumentRefKey]. If you create multiple [PdfDocumentRef]s with the same + /// key, they share the same [PdfDocumentListenable] and thus the same [PdfDocument] instance. + /// + /// By default, the key is created from the source name (for example, file path or URI) of the document. But it may + /// be insufficient to identify the document uniquely in some cases. For example, if the document is not only + /// identified by the URI but also by some HTTP headers (for example, authentication/authorization headers), + /// you should create a custom key that includes the headers as well. + final PdfDocumentRefKey key; + + /// The name that identifies the source of the document. + /// + /// This is for compatibility. Use [key] instead. See [PdfDocumentRefKey] for more info. + @Deprecated('For compatibility. Use key for source identification') + String get sourceName => key.sourceName; - static final _listenables = {}; + static final _listenables = CanonicalizedMap( + (ref) => ref.key, + ); /// Resolve the [PdfDocumentListenable] for this reference. PdfDocumentListenable resolveListenable() => _listenables.putIfAbsent(this, () => PdfDocumentListenable._(this)); @@ -62,22 +102,8 @@ abstract class PdfDocumentRef { /// Classes that extends [PdfDocumentRef] should override this function to load the document. /// /// [progressCallback] should be called when the document is loaded from remote source to notify the progress. - Future loadDocument( - PdfDocumentLoaderProgressCallback progressCallback, - PdfDocumentLoaderReportCallback reportCallback, - ); - - /// Classes that extends [PdfDocumentRef] should override this function to compare the equality by [sourceName] - /// or such. - @override - bool operator ==(Object other) => throw UnimplementedError(); + Future loadDocument(PdfDocumentLoaderProgressCallback progressCallback); - /// Classes that extends [PdfDocumentRef] should override this function. - @override - int get hashCode => throw UnimplementedError(); -} - -mixin PdfDocumentRefPasswordMixin on PdfDocumentRef { /// [passwordProvider] is used to provide password for encrypted PDF. See [PdfPasswordProvider] for more info. PdfPasswordProvider? get passwordProvider; @@ -87,42 +113,40 @@ mixin PdfDocumentRefPasswordMixin on PdfDocumentRef { } /// A [PdfDocumentRef] that loads the document from asset. -class PdfDocumentRefAsset extends PdfDocumentRef with PdfDocumentRefPasswordMixin { - const PdfDocumentRefAsset( +class PdfDocumentRefAsset extends PdfDocumentRef { + PdfDocumentRefAsset( this.name, { this.passwordProvider, this.firstAttemptByEmptyPassword = true, super.autoDispose = true, - }); + this.useProgressiveLoading = true, + PdfDocumentRefKey? key, + }) : super(key: key ?? PdfDocumentRefKey(name)); final String name; @override final PdfPasswordProvider? passwordProvider; @override final bool firstAttemptByEmptyPassword; - @override - String get sourceName => name; - - @override - Future loadDocument( - PdfDocumentLoaderProgressCallback progressCallback, - PdfDocumentLoaderReportCallback reportCallback, - ) => PdfDocument.openAsset( - name, - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - ); - @override - bool operator ==(Object other) => other is PdfDocumentRefAsset && name == other.name; + /// Whether to use progressive loading or not. + final bool useProgressiveLoading; @override - int get hashCode => name.hashCode; + Future loadDocument(PdfDocumentLoaderProgressCallback progressCallback) async { + await pdfrxFlutterInitialize(); + return await PdfDocument.openAsset( + name, + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + ); + } } /// A [PdfDocumentRef] that loads the document from network. -class PdfDocumentRefUri extends PdfDocumentRef with PdfDocumentRefPasswordMixin { - const PdfDocumentRefUri( +class PdfDocumentRefUri extends PdfDocumentRef { + PdfDocumentRefUri( this.uri, { this.passwordProvider, this.firstAttemptByEmptyPassword = true, @@ -130,7 +154,10 @@ class PdfDocumentRefUri extends PdfDocumentRef with PdfDocumentRefPasswordMixin this.preferRangeAccess = false, this.headers, this.withCredentials = false, - }); + this.timeout, + this.useProgressiveLoading = true, + PdfDocumentRefKey? key, + }) : super(key: key ?? PdfDocumentRefKey(uri.toString())); /// The URI to load the document. final Uri uri; @@ -139,7 +166,7 @@ class PdfDocumentRefUri extends PdfDocumentRef with PdfDocumentRefPasswordMixin @override final bool firstAttemptByEmptyPassword; - /// Whether to prefer range access or not (Not supported on Web). + /// Whether to prefer range access or not. final bool preferRangeAccess; /// Additional HTTP headers especially for authentication/authorization. @@ -148,78 +175,76 @@ class PdfDocumentRefUri extends PdfDocumentRef with PdfDocumentRefPasswordMixin /// Whether to include credentials in the request (Only supported on Web). final bool withCredentials; - @override - String get sourceName => uri.toString(); - - @override - Future loadDocument( - PdfDocumentLoaderProgressCallback progressCallback, - PdfDocumentLoaderReportCallback reportCallback, - ) => PdfDocument.openUri( - uri, - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - progressCallback: progressCallback, - reportCallback: reportCallback, - preferRangeAccess: preferRangeAccess, - headers: headers, - withCredentials: withCredentials, - ); - - @override - bool operator ==(Object other) => other is PdfDocumentRefUri && uri == other.uri; - - @override - int get hashCode => uri.hashCode; + /// Timeout duration for loading the document. (Only supported on non-Web platforms). + final Duration? timeout; + + /// Whether to use progressive loading or not. + final bool useProgressiveLoading; + + @override + Future loadDocument(PdfDocumentLoaderProgressCallback progressCallback) async { + await pdfrxFlutterInitialize(); + return await PdfDocument.openUri( + uri, + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + progressCallback: progressCallback, + preferRangeAccess: preferRangeAccess, + headers: headers, + withCredentials: withCredentials, + timeout: timeout, + ); + } } /// A [PdfDocumentRef] that loads the document from file. -class PdfDocumentRefFile extends PdfDocumentRef with PdfDocumentRefPasswordMixin { - const PdfDocumentRefFile( +class PdfDocumentRefFile extends PdfDocumentRef { + PdfDocumentRefFile( this.file, { this.passwordProvider, this.firstAttemptByEmptyPassword = true, super.autoDispose = true, - }); + this.useProgressiveLoading = true, + PdfDocumentRefKey? key, + }) : super(key: key ?? PdfDocumentRefKey(file)); final String file; @override final PdfPasswordProvider? passwordProvider; @override final bool firstAttemptByEmptyPassword; - @override - String get sourceName => file; - @override - Future loadDocument( - PdfDocumentLoaderProgressCallback progressCallback, - PdfDocumentLoaderReportCallback reportCallback, - ) => PdfDocument.openFile( - file, - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - ); - - @override - bool operator ==(Object other) => other is PdfDocumentRefFile && file == other.file; + /// Whether to use progressive loading or not. + final bool useProgressiveLoading; @override - int get hashCode => file.hashCode; + Future loadDocument(PdfDocumentLoaderProgressCallback progressCallback) async { + await pdfrxFlutterInitialize(); + return await PdfDocument.openFile( + file, + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + ); + } } /// A [PdfDocumentRef] that loads the document from data. /// /// For [allowDataOwnershipTransfer], see [PdfDocument.openData]. -class PdfDocumentRefData extends PdfDocumentRef with PdfDocumentRefPasswordMixin { - const PdfDocumentRefData( +class PdfDocumentRefData extends PdfDocumentRef { + PdfDocumentRefData( this.data, { - required this.sourceName, + required String sourceName, this.passwordProvider, this.firstAttemptByEmptyPassword = true, super.autoDispose = true, this.allowDataOwnershipTransfer = false, this.onDispose, - }); + this.useProgressiveLoading = true, + PdfDocumentRefKey? key, + }) : super(key: key ?? PdfDocumentRefKey(sourceName)); final Uint8List data; @override @@ -229,41 +254,38 @@ class PdfDocumentRefData extends PdfDocumentRef with PdfDocumentRefPasswordMixin final bool allowDataOwnershipTransfer; final void Function()? onDispose; - @override - final String sourceName; - - @override - Future loadDocument( - PdfDocumentLoaderProgressCallback progressCallback, - PdfDocumentLoaderReportCallback reportCallback, - ) => PdfDocument.openData( - data, - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - sourceName: sourceName, - allowDataOwnershipTransfer: allowDataOwnershipTransfer, - onDispose: onDispose, - ); - - @override - bool operator ==(Object other) => other is PdfDocumentRefData && sourceName == other.sourceName; - - @override - int get hashCode => sourceName.hashCode; + /// Whether to use progressive loading or not. + final bool useProgressiveLoading; + + @override + Future loadDocument(PdfDocumentLoaderProgressCallback progressCallback) async { + await pdfrxFlutterInitialize(); + return await PdfDocument.openData( + data, + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + sourceName: key.sourceName, + allowDataOwnershipTransfer: allowDataOwnershipTransfer, + onDispose: onDispose, + ); + } } /// A [PdfDocumentRef] that loads the document from custom source. -class PdfDocumentRefCustom extends PdfDocumentRef with PdfDocumentRefPasswordMixin { - const PdfDocumentRefCustom({ +class PdfDocumentRefCustom extends PdfDocumentRef { + PdfDocumentRefCustom({ required this.fileSize, required this.read, - required this.sourceName, + required String sourceName, this.passwordProvider, this.firstAttemptByEmptyPassword = true, super.autoDispose = true, this.maxSizeToCacheOnMemory, this.onDispose, - }); + this.useProgressiveLoading = true, + PdfDocumentRefKey? key, + }) : super(key: key ?? PdfDocumentRefKey(sourceName)); final int fileSize; final FutureOr Function(Uint8List buffer, int position, int size) read; @@ -274,50 +296,64 @@ class PdfDocumentRefCustom extends PdfDocumentRef with PdfDocumentRefPasswordMix final int? maxSizeToCacheOnMemory; final void Function()? onDispose; - @override - final String sourceName; + /// Whether to use progressive loading or not. + final bool useProgressiveLoading; + + @override + Future loadDocument(PdfDocumentLoaderProgressCallback progressCallback) async { + await pdfrxFlutterInitialize(); + return await PdfDocument.openCustom( + read: read, + fileSize: fileSize, + sourceName: key.sourceName, + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + maxSizeToCacheOnMemory: maxSizeToCacheOnMemory, + onDispose: onDispose, + ); + } +} + +/// A [PdfDocumentRef] that directly contains [PdfDocument]. +/// +/// It's useful when you already have a [PdfDocument] instance and want to use it as a [PdfDocumentRef] +/// but sometimes it breaks the lifecycle management of [PdfDocument] on [PdfDocumentRef] and you had better +/// use [PdfDocumentRefByLoader] if possible. +class PdfDocumentRefDirect extends PdfDocumentRef { + PdfDocumentRefDirect(this.document, {super.autoDispose = true, PdfDocumentRefKey? key}) + : super(key: key ?? PdfDocumentRefKey(document.sourceName)); + + final PdfDocument document; @override - Future loadDocument( - PdfDocumentLoaderProgressCallback progressCallback, - PdfDocumentLoaderReportCallback reportCallback, - ) => PdfDocument.openCustom( - read: read, - fileSize: fileSize, - sourceName: sourceName, - passwordProvider: passwordProvider, - firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, - maxSizeToCacheOnMemory: maxSizeToCacheOnMemory, - onDispose: onDispose, - ); + Future loadDocument(PdfDocumentLoaderProgressCallback progressCallback) => Future.value(document); @override - bool operator ==(Object other) => other is PdfDocumentRefCustom && sourceName == other.sourceName; + bool get firstAttemptByEmptyPassword => throw UnimplementedError('Not applicable for PdfDocumentRefDirect'); @override - int get hashCode => sourceName.hashCode; + PdfPasswordProvider? get passwordProvider => throw UnimplementedError('Not applicable for PdfDocumentRefDirect'); } -/// A [PdfDocumentRef] that directly contains [PdfDocument]. -class PdfDocumentRefDirect extends PdfDocumentRef { - const PdfDocumentRefDirect(this.document, {super.autoDispose = true}); +/// A [PdfDocumentRef] that loads the document using a custom loader function. +/// +/// The loader function is called when the document is really needed to be loaded and the [PdfDocument] will be closed +/// automatically when the reference is disposed if [autoDispose] is true. +class PdfDocumentRefByLoader extends PdfDocumentRef { + PdfDocumentRefByLoader(this.loader, {required super.key, super.autoDispose = true}); - final PdfDocument document; + /// The loader function to load the document. + final Future Function(PdfDocumentLoaderProgressCallback progressCallback) loader; @override - String get sourceName => document.sourceName; + Future loadDocument(PdfDocumentLoaderProgressCallback progressCallback) => loader(progressCallback); @override - Future loadDocument( - PdfDocumentLoaderProgressCallback progressCallback, - PdfDocumentLoaderReportCallback reportCallback, - ) => Future.value(document); + bool get firstAttemptByEmptyPassword => throw UnimplementedError('Not applicable for PdfDocumentRefByLoader'); @override - bool operator ==(Object other) => other is PdfDocumentRefDirect && sourceName == other.sourceName; - - @override - int get hashCode => sourceName.hashCode; + PdfPasswordProvider? get passwordProvider => throw UnimplementedError('Not applicable for PdfDocumentRefByLoader'); } /// The class is used to load the referenced document and notify the listeners. @@ -379,22 +415,25 @@ class PdfDocumentListenable extends Listenable { if (!forceReload && loadAttempted) { return null; } + final stopwatch = Stopwatch()..start(); return await synchronized(() async { if (!forceReload && loadAttempted) return null; final PdfDocument document; PdfDownloadReport? report; try { - document = await ref.loadDocument( - _progress, - (downloaded, totalBytes, elapsed) => - report = PdfDownloadReport(downloaded: downloaded, total: totalBytes, elapsedTime: elapsed), - ); + document = await ref.loadDocument((cur, [total]) { + _progress(cur, total); + if (total != null) { + report = PdfDownloadReport(downloaded: cur, total: total, elapsedTime: stopwatch.elapsed); + } + }); + debugPrint('PdfDocument initial load: ${ref.key} (${stopwatch.elapsedMilliseconds} ms)'); } catch (err, stackTrace) { setError(err, stackTrace); - return report; + return report?.copyWith(elapsedTime: stopwatch.elapsed); } setDocument(document); - return report; + return report?.copyWith(elapsedTime: stopwatch.elapsed); }); } @@ -516,4 +555,25 @@ class PdfDownloadReport { final int downloaded; final int total; final Duration elapsedTime; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is PdfDownloadReport && + other.downloaded == downloaded && + other.total == total && + other.elapsedTime == elapsedTime; + } + + PdfDownloadReport copyWith({int? downloaded, int? total, Duration? elapsedTime}) { + return PdfDownloadReport( + downloaded: downloaded ?? this.downloaded, + total: total ?? this.total, + elapsedTime: elapsedTime ?? this.elapsedTime, + ); + } + + @override + int get hashCode => downloaded.hashCode ^ total.hashCode ^ elapsedTime.hashCode; } diff --git a/packages/pdfrx/lib/src/pdfrx_flutter.dart b/packages/pdfrx/lib/src/pdfrx_flutter.dart new file mode 100644 index 00000000..b00928b1 --- /dev/null +++ b/packages/pdfrx/lib/src/pdfrx_flutter.dart @@ -0,0 +1,182 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart' show WidgetsFlutterBinding; +import 'package:flutter/services.dart'; + +import '../pdfrx.dart'; +import 'utils/platform.dart'; + +bool _isInitialized = false; + +/// Explicitly initializes the Pdfrx library for Flutter. +/// +/// This function actually sets up the following functions: +/// - [Pdfrx.loadAsset]: Loads an asset by name and returns its byte data. +/// - [Pdfrx.getCacheDirectory]: Returns the path to the temporary directory for caching. +/// - Call [PdfrxEntryFunctions.init] to initialize the library. +/// +/// For Dart (non-Flutter) programs, you should call [pdfrxInitialize] instead. +/// +/// The function shows PDFium WASM module warnings in debug mode by default. +/// You can disable these warnings by setting [dismissPdfiumWasmWarnings] to true. +Future pdfrxFlutterInitialize({bool dismissPdfiumWasmWarnings = false}) async { + if (_isInitialized) return; + + WidgetsFlutterBinding.ensureInitialized(); + + if (pdfrxEntryFunctionsOverride != null) { + PdfrxEntryFunctions.instance = pdfrxEntryFunctionsOverride!; + } + + Pdfrx.loadAsset ??= (name) async { + final asset = await rootBundle.load(name); + return asset.buffer.asUint8List(); + }; + Pdfrx.getCacheDirectory ??= getCacheDirectory; + + // Checking pdfium.wasm availability for Web and debug builds. + if (kDebugMode && !dismissPdfiumWasmWarnings) { + () async { + try { + await Pdfrx.loadAsset!('packages/pdfrx/assets/pdfium.wasm'); + if (!kIsWeb) { + debugPrint( + '⚠️\u001b[37;41;1mDEBUG TIME WARNING: The app is bundling PDFium WASM module (about 4MB) as a part of the app.\u001b[0m\n' + '\u001b[91mFor production use (not for Web/Debug), you\'d better remove the PDFium WASM module.\u001b[0m\n' + '\u001b[91mSee https://github.com/espresso3389/pdfrx/tree/master/packages/pdfrx#note-for-building-release-builds for more details.\u001b[0m\n', + ); + } + } catch (e) { + if (kIsWeb) { + debugPrint( + '⚠️\u001b[37;41;1mDEBUG TIME WARNING: The app is running on Web, but the PDFium WASM module is not bundled with the app.\u001b[0m\n' + '\u001b[91mMake sure to include the PDFium WASM module in your web project.\u001b[0m\n' + '\u001b[91mIf you explicitly set Pdfrx.pdfiumWasmModulesUrl, you can ignore this warning.\u001b[0m\n' + '\u001b[91mSee https://github.com/espresso3389/pdfrx/tree/master/packages/pdfrx#note-for-building-release-builds for more details.\u001b[0m\n', + ); + } + } + }(); + } + + /// NOTE: it's actually async, but hopefully, it finishes quickly... + await platformInitialize(); + + _isInitialized = true; +} + +extension PdfPageExt on PdfPage { + /// PDF page size in points (size in pixels at 72 dpi) (rotated). + Size get size => Size(width, height); +} + +extension PdfImageExt on PdfImage { + /// Create [Image] from the rendered image. + /// + /// [pixelSizeThreshold] specifies the maximum allowed pixel size (width or height). + /// If the image exceeds this size, it will be downscaled to fit within the threshold + /// while maintaining the aspect ratio. + /// + /// The returned [Image] must be disposed of when no longer needed. + Future createImage({int? pixelSizeThreshold}) { + int? targetWidth; + int? targetHeight; + if (pixelSizeThreshold != null && (width > pixelSizeThreshold || height > pixelSizeThreshold)) { + final aspectRatio = width / height; + if (width >= height) { + targetWidth = pixelSizeThreshold; + targetHeight = (pixelSizeThreshold / aspectRatio).round(); + } else { + targetHeight = pixelSizeThreshold; + targetWidth = (pixelSizeThreshold * aspectRatio).round(); + } + } + + final comp = Completer(); + decodeImageFromPixels( + pixels, + width, + height, + PixelFormat.bgra8888, + (image) => comp.complete(image), + targetWidth: targetWidth, + targetHeight: targetHeight, + ); + return comp.future; + } +} + +extension ImageExt on Image { + /// Convert [Image] to [PdfImage]. + Future toPdfImage() async { + final rgba = (await toByteData(format: ImageByteFormat.rawRgba))!.buffer.asUint8List(); + for (var i = 0; i < rgba.length; i += 4) { + final r = rgba[i]; + rgba[i] = rgba[i + 2]; + rgba[i + 2] = r; + } + return PdfImage.createFromBgraData(rgba, width: width, height: height); + } +} + +extension PdfRectExt on PdfRect { + /// Convert to [Rect] in Flutter coordinate. + /// [page] is the page to convert the rectangle. + /// [scaledPageSize] is the scaled page size to scale the rectangle. If not specified, [PdfPage].size is used. + /// [rotation] is the rotation of the page. If not specified, [PdfPage.rotation] is used. + Rect toRect({required PdfPage page, Size? scaledPageSize, int? rotation}) { + final rotated = rotate(rotation ?? page.rotation.index, page); + final scale = scaledPageSize == null ? 1.0 : scaledPageSize.height / page.height; + return Rect.fromLTRB( + rotated.left * scale, + (page.height - rotated.top) * scale, + rotated.right * scale, + (page.height - rotated.bottom) * scale, + ); + } + + /// Convert to [Rect] in Flutter coordinate using [pageRect] as the page's bounding rectangle. + Rect toRectInDocument({required PdfPage page, required Rect pageRect}) => + toRect(page: page, scaledPageSize: pageRect.size).translate(pageRect.left, pageRect.top); +} + +extension RectPdfRectExt on Rect { + /// Convert to [PdfRect] in PDF page coordinates. + PdfRect toPdfRect({required PdfPage page, Size? scaledPageSize, int? rotation}) { + final scale = scaledPageSize == null ? 1.0 : scaledPageSize.height / page.height; + return PdfRect( + left / scale, + page.height - top / scale, + right / scale, + page.height - bottom / scale, + ).rotateReverse(rotation ?? page.rotation.index, page); + } +} + +extension PdfPointExt on PdfPoint { + /// Convert to [Offset] in Flutter coordinate. + /// [page] is the page to convert the rectangle. + /// [scaledPageSize] is the scaled page size to scale the rectangle. If not specified, [PdfPage].size is used. + /// [rotation] is the rotation of the page. If not specified, [PdfPage.rotation] is used. + Offset toOffset({required PdfPage page, Size? scaledPageSize, int? rotation}) { + final rotated = rotate(rotation ?? page.rotation.index, page); + final scale = scaledPageSize == null ? 1.0 : scaledPageSize.height / page.height; + return Offset(rotated.x * scale, (page.height - rotated.y) * scale); + } + + Offset toOffsetInDocument({required PdfPage page, required Rect pageRect}) { + final rotated = rotate(page.rotation.index, page); + final scale = pageRect.height / page.height; + return Offset(rotated.x * scale, (page.height - rotated.y) * scale).translate(pageRect.left, pageRect.top); + } +} + +extension OffsetPdfPointExt on Offset { + /// Convert to [PdfPoint] in PDF page coordinates. + PdfPoint toPdfPoint({required PdfPage page, Size? scaledPageSize, int? rotation}) { + final scale = scaledPageSize == null ? 1.0 : page.height / scaledPageSize.height; + return PdfPoint(dx * scale, page.height - dy * scale).rotateReverse(rotation ?? page.rotation.index, page); + } +} diff --git a/lib/src/utils/double_extensions.dart b/packages/pdfrx/lib/src/utils/double_extensions.dart similarity index 61% rename from lib/src/utils/double_extensions.dart rename to packages/pdfrx/lib/src/utils/double_extensions.dart index 48d51d86..6fb1ef98 100644 --- a/lib/src/utils/double_extensions.dart +++ b/packages/pdfrx/lib/src/utils/double_extensions.dart @@ -10,4 +10,14 @@ extension DoubleExtensions on double { final d = this > another ? this / another : another / this; return d - 1 < error; } + + /// Round the double to keep 10-bits of precision under the binary point. + /// + /// It's almost 3 decimal places (i.e. 1.23456789 => 1.234) but cleaner in binary representation. + double round10BitFrac() { + if (isInfinite || isNaN) { + return this; + } + return (this * 1024).round() / 1024; + } } diff --git a/packages/pdfrx/lib/src/utils/edge_insets_extensions.dart b/packages/pdfrx/lib/src/utils/edge_insets_extensions.dart new file mode 100644 index 00000000..50efdbb4 --- /dev/null +++ b/packages/pdfrx/lib/src/utils/edge_insets_extensions.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +/// Extensions for EdgeInsets to provide additional utility methods. +extension EdgeInsetsExtensions on EdgeInsets { + /// Returns true if any of the EdgeInsets values (left, top, right, bottom) are infinite. + bool get containsInfinite => left.isInfinite || right.isInfinite || top.isInfinite || bottom.isInfinite; + + /// Inflates a given Rect by the EdgeInsets values if all values are finite, otherwise returns the rect + Rect inflateRectIfFinite(Rect rect) { + if (containsInfinite) return rect; + return inflateRect(rect); + } +} diff --git a/packages/pdfrx/lib/src/utils/fixed_overscroll_physics.dart b/packages/pdfrx/lib/src/utils/fixed_overscroll_physics.dart new file mode 100644 index 00000000..97f9116a --- /dev/null +++ b/packages/pdfrx/lib/src/utils/fixed_overscroll_physics.dart @@ -0,0 +1,133 @@ +import 'package:flutter/widgets.dart'; + +/// A ScrollPhysics that lets you overscroll by up to [maxOverscroll] +/// and then springs back to the content bounds. +class FixedOverscrollPhysics extends ClampingScrollPhysics { + const FixedOverscrollPhysics({super.parent, this.maxOverscroll = 200.0}); + + /// How far (in logical pixels) the user can overscroll. + final double maxOverscroll; + + @override + FixedOverscrollPhysics applyTo(ScrollPhysics? ancestor) { + return FixedOverscrollPhysics(parent: buildParent(ancestor), maxOverscroll: maxOverscroll); + } + + @override + double applyBoundaryConditions(ScrollMetrics position, double value) { + // Overscroll allowed to a specific maximum [maxOverscroll] to replicate + // the behavior of popular PDF readers on Android. + + // If we're within the allowed overscroll zone, allow it (return 0). + if (value < position.minScrollExtent && value >= position.minScrollExtent - maxOverscroll) { + return 0.0; + } + if (value > position.maxScrollExtent && value <= position.maxScrollExtent + maxOverscroll) { + return 0.0; + } + + if (value <= position.minScrollExtent - maxOverscroll) { + return value - (position.minScrollExtent - maxOverscroll); + } else if (value >= position.maxScrollExtent + maxOverscroll) { + return value - (position.maxScrollExtent + maxOverscroll); + } + return 0.0; + } + + @override + Simulation? createBallisticSimulation(ScrollMetrics position, double velocity) { + final tolerance = toleranceFor(position); + + if (velocity.abs() >= tolerance.velocity || position.outOfRange) { + return _HybridScrollSimulation( + position: position, + velocity: velocity, + tolerance: tolerance, + maxOverscroll: maxOverscroll, + ); + } + return null; + } +} + +/// A simulation that behaves like ClampingScrollPhysics when within bounds +/// and switches to a custom BouncingScrollSimulation when exceeding bounds. +class _HybridScrollSimulation extends Simulation { + _HybridScrollSimulation({ + required ScrollMetrics position, + required double velocity, + required Tolerance tolerance, + required this.maxOverscroll, + }) : _minExtent = position.minScrollExtent, + _maxExtent = position.maxScrollExtent, + _tolerance = tolerance { + final currentPosition = position.pixels; + final isOutOfBounds = currentPosition < _minExtent || currentPosition > _maxExtent; + final isInOverscrollZone = + (currentPosition >= _minExtent - maxOverscroll && currentPosition < _minExtent) || + (currentPosition > _maxExtent && currentPosition <= _maxExtent + maxOverscroll); + + // If already out of bounds or in overscroll zone, start with bouncing simulation + if (isOutOfBounds || isInOverscrollZone) { + _createBouncingSimulation(currentPosition, velocity); + } else { + // Within content bounds - use clamping behavior + _currentSimulation = ClampingScrollSimulation(position: currentPosition, velocity: velocity); + } + } + + final double _minExtent; + final double _maxExtent; + final double maxOverscroll; + @override + Tolerance get tolerance => _tolerance; + final Tolerance _tolerance; + + late Simulation _currentSimulation; + bool _hasSwitchedToBouncing = false; + + @override + double x(double time) { + final position = _currentSimulation.x(time); + + // Check if we need to switch to bouncing simulation + if (!_hasSwitchedToBouncing) { + final wouldExceedBounds = position < _minExtent || position > _maxExtent; + + if (wouldExceedBounds) { + _switchToBouncingSimulation(position, dx(time)); + } + } + + return _currentSimulation.x(time); + } + + @override + double dx(double time) => _currentSimulation.dx(time); + + @override + bool isDone(double time) => _currentSimulation.isDone(time); + + void _switchToBouncingSimulation(double position, double velocity) { + _hasSwitchedToBouncing = true; + _createBouncingSimulation(position, velocity); + } + + void _createBouncingSimulation(double position, double velocity) { + // A spring with considerably less mass and higher stiffness than the default + // iOS BouncingScrollSimulation which results in hardly any overscroll on + // fling, and a quick snap back to the content bounds when dragged, to replicate + // similar behavior found in several popular PDF readers on Android. + + final spring = SpringDescription.withDampingRatio(mass: 0.3, stiffness: 1500.0, ratio: 3); + + _currentSimulation = BouncingScrollSimulation( + spring: spring, + position: position, + velocity: velocity, + leadingExtent: _minExtent, + trailingExtent: _maxExtent, + tolerance: tolerance, + ); + } +} diff --git a/packages/pdfrx/lib/src/utils/native/native.dart b/packages/pdfrx/lib/src/utils/native/native.dart new file mode 100644 index 00000000..d20b1092 --- /dev/null +++ b/packages/pdfrx/lib/src/utils/native/native.dart @@ -0,0 +1,42 @@ +import 'dart:io'; + +import 'package:flutter/services.dart'; +import 'package:path_provider/path_provider.dart'; + +import '../../../pdfrx.dart'; + +final isApple = Platform.isMacOS || Platform.isIOS; +final isAndroid = Platform.isAndroid; +final isWindows = Platform.isWindows; + +/// Whether the current platform is mobile (Android, iOS, or Fuchsia). +final isMobile = Platform.isAndroid || Platform.isIOS || Platform.isFuchsia; + +/// Key pressing state of ⌘ or Control depending on the platform. +bool get isCommandKeyPressed => + isApple ? HardwareKeyboard.instance.isMetaPressed : HardwareKeyboard.instance.isControlPressed; + +/// Sets the clipboard data with the provided text. +void setClipboardData(String text) { + Clipboard.setData(ClipboardData(text: text)); +} + +/// Gets the cache directory path for the current platform. +/// +/// For web, this function throws an [UnimplementedError] since there is no temporary directory available. +Future getCacheDirectory() async => (await getTemporaryDirectory()).path; + +/// Override for the [PdfrxEntryFunctions] for native platforms; it is null. +PdfrxEntryFunctions? get pdfrxEntryFunctionsOverride => null; + +/// Initializes the Pdfrx library for native platforms. +/// +/// This function is here to maintain a consistent API with web and other platforms. +Future platformInitialize() async { + await PdfrxEntryFunctions.instance.init(); +} + +/// Reports focus changes for the Web platform to handle right-click context menus. +/// +/// For native platforms, this function does nothing. +void focusReportForPreventingContextMenuWeb(Object viewer, bool hasFocus) {} diff --git a/lib/src/utils/platform.dart b/packages/pdfrx/lib/src/utils/platform.dart similarity index 100% rename from lib/src/utils/platform.dart rename to packages/pdfrx/lib/src/utils/platform.dart diff --git a/packages/pdfrx/lib/src/utils/web/web.dart b/packages/pdfrx/lib/src/utils/web/web.dart new file mode 100644 index 00000000..66b96fb3 --- /dev/null +++ b/packages/pdfrx/lib/src/utils/web/web.dart @@ -0,0 +1,66 @@ +import 'dart:js_interop'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:web/web.dart' as web; + +import '../../../pdfrx.dart'; +import '../../wasm/pdfrx_wasm.dart'; + +final isApple = false; +final isAndroid = false; +final isWindows = false; + +/// Whether the current platform is mobile (Android, iOS, or Fuchsia). +final isMobile = false; + +/// Key pressing state of ⌘ or Control depending on the platform. +bool get isCommandKeyPressed => HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed; + +/// Sets the clipboard data with the provided text. +void setClipboardData(String text) { + web.window.navigator.clipboard.writeText(text); +} + +/// Gets the cache directory path for the current platform. +/// +/// For web, this function throws an [UnimplementedError] since there is no temporary directory available. +Future getCacheDirectory() async => throw UnimplementedError('No temporary directory available for web.'); + +/// Override for the [PdfrxEntryFunctions] for web platforms to use WASM implementation. +PdfrxEntryFunctions? get pdfrxEntryFunctionsOverride => _factoryWasm; + +final _factoryWasm = PdfrxEntryFunctionsWasmImpl(); + +final _focusObject = {}; + +/// Initializes the Pdfrx library for Web. +/// +/// For Web, this function currently setup "contextmenu" event listener to prevent the default context menu from +/// appearing on right-click. +Future platformInitialize() async { + web.document.addEventListener( + 'contextmenu', + ((web.Event event) { + // Prevent the default context menu from appearing on right-click. + if (_focusObject.isNotEmpty) { + debugPrint('pdfrx: Context menu event prevented because PdfViewer has focus.'); + event.preventDefault(); + } else { + debugPrint('pdfrx: Context menu event allowed.'); + } + }).toJS, + ); + await PdfrxEntryFunctions.instance.init(); +} + +/// Reports focus changes for the Web platform to handle right-click context menus. +/// +/// For native platforms, this function does nothing. +void focusReportForPreventingContextMenuWeb(Object viewer, bool hasFocus) { + if (hasFocus) { + _focusObject.add(viewer); + } else { + _focusObject.remove(viewer); + } +} diff --git a/lib/src/web/js_utils.dart b/packages/pdfrx/lib/src/wasm/js_utils.dart similarity index 100% rename from lib/src/web/js_utils.dart rename to packages/pdfrx/lib/src/wasm/js_utils.dart diff --git a/packages/pdfrx/lib/src/wasm/pdfrx_wasm.dart b/packages/pdfrx/lib/src/wasm/pdfrx_wasm.dart new file mode 100644 index 00000000..326c1616 --- /dev/null +++ b/packages/pdfrx/lib/src/wasm/pdfrx_wasm.dart @@ -0,0 +1,879 @@ +import 'dart:async'; +import 'dart:js_interop'; +import 'dart:typed_data'; +import 'dart:ui_web' as ui_web; + +import 'package:crypto/crypto.dart'; +import 'package:pdfrx_engine/pdfrx_engine.dart'; +// ignore: implementation_imports +import 'package:pdfrx_engine/src/pdf_page_proxies.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:synchronized/extension.dart'; +import 'package:web/web.dart' as web; + +/// The PDFium WASM communicator object +@JS('PdfiumWasmCommunicator') +extension type _PdfiumWasmCommunicator(JSObject _) implements JSObject { + /// Sends a command to the worker and returns a promise + @JS('sendCommand') + external JSPromise sendCommand([String command, JSAny? parameters, JSArray? transfer]); + + /// Registers a callback function and returns its ID + @JS('registerCallback') + external int registerCallback(JSFunction callback); + + /// Unregisters a callback by its ID + @JS('unregisterCallback') + external void unregisterCallback(int callbackId); +} + +/// Get the global PdfiumWasmCommunicator instance +@JS('PdfiumWasmCommunicator') +external _PdfiumWasmCommunicator get _pdfiumWasmCommunicator; + +/// A handle to a registered callback that can be unregistered +class _PdfiumWasmCallback { + _PdfiumWasmCallback.register(JSFunction callback) + : id = _pdfiumWasmCommunicator.registerCallback(callback), + _communicator = _pdfiumWasmCommunicator; + + final int id; + final _PdfiumWasmCommunicator _communicator; + + void unregister() { + _communicator.unregisterCallback(id); + } +} + +Future> _sendCommand( + String command, { + Map? parameters, + JSArray? transfer, +}) async { + final result = await _pdfiumWasmCommunicator.sendCommand(command, parameters?.jsify(), transfer).toDart; + return (result.dartify()) as Map; +} + +/// The URL of the PDFium WASM worker script; pdfium_client.js tries to load worker script from this URL.' +@JS() +external String pdfiumWasmWorkerUrl; + +/// [PdfrxEntryFunctions] for PDFium WASM implementation. +class PdfrxEntryFunctionsWasmImpl extends PdfrxEntryFunctions { + /// Default path to the WASM modules + /// + /// Normally, the WASM modules are provided by pdfrx_wasm package and this is the path to its assets. + static const defaultWasmModulePath = 'assets/packages/pdfrx/assets/'; + + bool _initialized = false; + + @override + Future init() async { + if (_initialized) return; + await synchronized(() async { + if (_initialized) return; + Pdfrx.pdfiumWasmModulesUrl ??= _pdfiumWasmModulesUrlFromMetaTag(); + pdfiumWasmWorkerUrl = _getWorkerUrl(); + final moduleUrl = _resolveUrl(Pdfrx.pdfiumWasmModulesUrl ?? defaultWasmModulePath); + final script = web.document.createElement('script') as web.HTMLScriptElement + ..type = 'text/javascript' + ..charset = 'utf-8' + ..async = true + ..type = 'module' + ..src = _resolveUrl('pdfium_client.js', baseUrl: moduleUrl); + web.document.querySelector('head')!.appendChild(script); + final completer = Completer(); + final sub1 = script.onLoad.listen((_) => completer.complete()); + final sub2 = script.onError.listen((event) => completer.completeError(event)); + try { + await completer.future; + } catch (e) { + throw StateError('Failed to load pdfium_client.js from $moduleUrl: $e'); + } finally { + await sub1.cancel(); + await sub2.cancel(); + } + + // Send init command to worker with authentication options + await _sendCommand( + 'init', + parameters: { + if (Pdfrx.pdfiumWasmHeaders != null) 'headers': Pdfrx.pdfiumWasmHeaders, + 'withCredentials': Pdfrx.pdfiumWasmWithCredentials, + }, + ); + _initialized = true; + }); + } + + @override + Future suspendPdfiumWorkerDuringAction(FutureOr Function() action) async { + // We don't share PDFium wasm instance with other libraries, so no need to block calls anyway + return await action(); + } + + @override + Future compute(FutureOr Function(M message) callback, M message) async { + throw UnimplementedError('compute() is not implemented for WASM backend.'); + } + + @override + Future stopBackgroundWorker() async { + throw UnimplementedError('stopBackgroundWorker() is not implemented for WASM backend.'); + } + + static String? _pdfiumWasmModulesUrlFromMetaTag() { + final meta = web.document.querySelector('meta[name="pdfium-wasm-module-url"]') as web.HTMLMetaElement?; + return meta?.content; + } + + /// Workaround for Cross-Origin-Embedder-Policy restriction on WASM enabled environments + String _getWorkerUrl() { + final moduleUrl = _resolveUrl(Pdfrx.pdfiumWasmModulesUrl ?? defaultWasmModulePath); + final workerJsUrl = _resolveUrl('pdfium_worker.js', baseUrl: moduleUrl); + final pdfiumWasmUrl = _resolveUrl('pdfium.wasm', baseUrl: moduleUrl); + final content = 'const pdfiumWasmUrl="$pdfiumWasmUrl";importScripts("$workerJsUrl");'; + final blob = web.Blob( + [content].jsify() as JSArray, + web.BlobPropertyBag(type: 'application/javascript'), + ); + return web.URL.createObjectURL(blob); + } + + /// Resolves the given [relativeUrl] against a base URL to produce an absolute URL. + /// + /// The base URL is determined in the following order of preference: + /// 1. The explicitly provided [baseUrl] parameter. + /// 2. The `` tag of the current HTML document (obtained via `ui_web.BrowserPlatformLocation().getBaseHref()`). + /// 3. The current browser window's URL (`web.window.location.href`). + static String _resolveUrl(String relativeUrl, {String? baseUrl}) { + final baseHref = ui_web.BrowserPlatformLocation().getBaseHref(); + return Uri.parse(baseUrl ?? baseHref ?? web.window.location.href).resolveUri(Uri.parse(relativeUrl)).toString(); + } + + @override + Future openAsset( + String name, { + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, + }) async { + if (Pdfrx.loadAsset == null) { + throw StateError('Pdfrx.loadAsset is not set. Please set it to load assets.'); + } + final asset = await Pdfrx.loadAsset!(name); + return await openData( + asset.buffer.asUint8List(), + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + sourceName: 'asset%$name', + allowDataOwnershipTransfer: true, + ); + } + + @override + Future openCustom({ + required FutureOr Function(Uint8List buffer, int position, int size) read, + required int fileSize, + required String sourceName, + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, + int? maxSizeToCacheOnMemory, + void Function()? onDispose, + }) async { + throw UnimplementedError('PdfrxEntryFunctionsWasmImpl.openCustom is not implemented.'); + } + + @override + Future openData( + Uint8List data, { + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, + String? sourceName, + bool allowDataOwnershipTransfer = false, + void Function()? onDispose, + }) => _openByFunc( + (password) => _sendCommand( + 'loadDocumentFromData', + parameters: {'data': data, 'password': password, 'useProgressiveLoading': useProgressiveLoading}, + ), + sourceName: sourceName ?? _sourceNameFromData(data), + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + onDispose: onDispose, + ); + + /// Generates a pseudo-unique source name for the given data using its SHA-256 hash. + /// + /// This may be sometimes slow for large data, so it's better to provide a meaningful source name when possible. + static String _sourceNameFromData(Uint8List data) { + return 'data%${sha256.convert(data)}'; + } + + @override + Future openFile( + String filePath, { + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, + }) => _openByFunc( + (password) => _sendCommand( + 'loadDocumentFromUrl', + parameters: {'url': filePath, 'password': password, 'useProgressiveLoading': useProgressiveLoading}, + ), + sourceName: 'file%$filePath', + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + onDispose: null, + ); + + @override + Future openUri( + Uri uri, { + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, + PdfDownloadProgressCallback? progressCallback, + bool preferRangeAccess = false, + Map? headers, + bool withCredentials = false, + Duration? timeout, + }) async { + _PdfiumWasmCallback? progressCallbackReg; + void cleanupCallbacks() => progressCallbackReg?.unregister(); + + try { + if (progressCallback != null) { + await init(); + progressCallbackReg = _PdfiumWasmCallback.register( + ((int bytesReceived, int bytesTotal) => progressCallback(bytesReceived, bytesTotal)).toJS, + ); + } + + return _openByFunc( + (password) => _sendCommand( + 'loadDocumentFromUrl', + parameters: { + 'url': uri.toString(), + 'password': password, + 'useProgressiveLoading': useProgressiveLoading, + if (progressCallbackReg != null) 'progressCallbackId': progressCallbackReg.id, + 'preferRangeAccess': preferRangeAccess, + if (headers != null) 'headers': headers, + 'withCredentials': withCredentials, + }, + ), + sourceName: 'uri%$uri', + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + onDispose: cleanupCallbacks, + ); + } catch (e) { + cleanupCallbacks(); + rethrow; + } + } + + Future _openByFunc( + Future> Function(String? password) openDocument, { + required String sourceName, + required PdfPasswordProvider? passwordProvider, + required bool firstAttemptByEmptyPassword, + required bool useProgressiveLoading, + required void Function()? onDispose, + }) async { + await init(); + + for (var i = 0; ; i++) { + final String? password; + if (firstAttemptByEmptyPassword && i == 0) { + password = null; + } else { + password = await passwordProvider?.call(); + if (password == null) { + throw const PdfPasswordException('No password supplied by PasswordProvider.'); + } + } + + final result = await openDocument(password); + + const fpdfErrPassword = 4; + final errorCode = (result['errorCode'] as num?)?.toInt(); + if (errorCode != null) { + if (errorCode == fpdfErrPassword) { + continue; + } + throw StateError('Failed to open document: ${result['errorCodeStr']} ($errorCode)'); + } + + return _PdfDocumentWasm._( + result, + sourceName: sourceName, + disposeCallback: onDispose, + useProgressiveLoading: useProgressiveLoading, + ); + } + } + + @override + Future createNew({required String sourceName}) async { + await init(); + final result = await _sendCommand('createNewDocument'); + final errorCode = (result['errorCode'] as num?)?.toInt(); + if (errorCode != null) { + throw StateError('Failed to create new document: ${result['errorCodeStr']} ($errorCode)'); + } + return _PdfDocumentWasm._(result, sourceName: sourceName, disposeCallback: null, useProgressiveLoading: false); + } + + @override + Future createFromJpegData( + Uint8List jpegData, { + required double width, + required double height, + required String sourceName, + }) async { + await init(); + final jsData = jpegData.buffer.toJS; + final result = await _sendCommand( + 'createDocumentFromJpegData', + parameters: {'jpegData': jsData, 'width': width, 'height': height}, + transfer: [jsData].toJS, + ); + final errorCode = (result['errorCode'] as num?)?.toInt(); + if (errorCode != null) { + throw StateError('Failed to create document from JPEG data: ${result['errorCodeStr']} ($errorCode)'); + } + return _PdfDocumentWasm._(result, sourceName: sourceName, disposeCallback: null, useProgressiveLoading: false); + } + + @override + Future reloadFonts() async { + await init(); + await _sendCommand('reloadFonts', parameters: {'dummy': true}); + } + + @override + Future addFontData({required String face, required Uint8List data}) async { + await init(); + final jsData = data.buffer.toJS; + await _sendCommand('addFontData', parameters: {'face': face, 'data': jsData}, transfer: [jsData].toJS); + } + + @override + Future clearAllFontData() async { + await init(); + await _sendCommand('clearAllFontData', parameters: {'dummy': true}); + } + + @override + PdfrxBackendType get backendType => PdfrxBackendType.pdfiumWasm; +} + +class _PdfDocumentWasm extends PdfDocument { + _PdfDocumentWasm._( + this.document, { + required super.sourceName, + required bool useProgressiveLoading, + this.disposeCallback, + }) : permissions = parsePermissions(document) { + _pages = parsePages(this, document['pages'] as List); + updateMissingFonts(document['missingFonts']); + if (!useProgressiveLoading) { + _notifyDocumentLoadComplete(); + } + } + + void _notifyDocumentLoadComplete() { + subject.add(PdfDocumentLoadCompleteEvent(this)); + } + + final Map document; + final void Function()? disposeCallback; + bool isDisposed = false; + final subject = BehaviorSubject(); + + @override + final PdfPermissions? permissions; + + @override + bool get isEncrypted => permissions != null; + + @override + Stream get events => subject.stream; + + @override + Future dispose() async { + if (!isDisposed) { + isDisposed = true; + subject.close(); + await _sendCommand('closeDocument', parameters: document); + disposeCallback?.call(); + } + } + + @override + bool isIdenticalDocumentHandle(Object? other) { + return other is _PdfDocumentWasm && other.document['docHandle'] == document['docHandle']; + } + + @override + Future> loadOutline() async { + final result = await _sendCommand('loadOutline', parameters: {'docHandle': document['docHandle']}); + final outlineList = result['outline'] as List; + return outlineList.map((node) => _nodeFromMap(node)).toList(); + } + + static PdfOutlineNode _nodeFromMap(dynamic node) { + return PdfOutlineNode( + title: node['title'], + dest: _pdfDestFromMap(node['dest']), + children: (node['children'] as List).map((child) => _nodeFromMap(child)).toList(), + ); + } + + @override + Future loadPagesProgressively({ + PdfPageLoadingCallback? onPageLoadProgress, + T? data, + Duration loadUnitDuration = const Duration(milliseconds: 250), + }) async { + if (isDisposed) return; + await synchronized(() async { + var firstPageIndex = pages.indexWhere((page) => !page.isLoaded); + if (firstPageIndex < 0) return; // All pages are already loaded + + final newPages = pages.toList(growable: false); + for (; firstPageIndex < newPages.length;) { + if (isDisposed) return; + final result = await _sendCommand( + 'loadPagesProgressively', + parameters: { + 'docHandle': document['docHandle'], + 'firstPageIndex': firstPageIndex, + 'loadUnitDuration': loadUnitDuration.inMilliseconds, + }, + ); + final pagesLoaded = parsePages(this, result['pages'] as List); + firstPageIndex += pagesLoaded.length; + for (final page in pagesLoaded) { + newPages[page.pageNumber - 1] = page; // Update the existing page + } + pages = newPages; + + updateMissingFonts(result['missingFonts']); + + if (onPageLoadProgress != null) { + if (!await onPageLoadProgress(firstPageIndex, pages.length, data)) { + // If the callback returns false, stop loading more pages + break; + } + } + } + // All pages loaded + if (firstPageIndex >= pages.length) { + _notifyDocumentLoadComplete(); + } + }); + } + + @override + Future reloadPages({List? pageNumbersToReload}) async { + if (isDisposed) return; + await synchronized(() async { + final pageIndices = pageNumbersToReload?.map((n) => n - 1).toList(); + final result = await _sendCommand( + 'reloadPages', + parameters: { + 'docHandle': document['docHandle'], + if (pageIndices != null) 'pageIndices': pageIndices, + 'currentPagesCount': pages.length, + }, + ); + final reloadedPages = parsePages(this, result['pages'] as List); + final newPages = pages.toList(growable: false); + for (final page in reloadedPages) { + newPages[page.pageNumber - 1] = page; // Update the existing page + } + pages = newPages; + + updateMissingFonts(result['missingFonts']); + }); + } + + /// Don't handle [_pages] directly unless you really understand what you're doing; use [pages] getter/setter instead. + /// + /// [pages] automatically keeps consistency and also notifies page changes. + late List _pages; + + @override + List get pages => _pages; + + @override + set pages(Iterable newPages) { + final pages = []; + final changes = {}; + + for (final newPage in newPages) { + if (pages.length < _pages.length) { + final old = _pages[pages.length]; + if (identical(newPage, old)) { + pages.add(newPage); + continue; + } + } + + if (newPage.unwrap<_PdfPageWasm>() == null) { + throw ArgumentError('Unsupported PdfPage instances found at [${pages.length}]', 'newPages'); + } + + final newPageNumber = pages.length + 1; + final updated = newPage.withPageNumber(newPageNumber); + pages.add(updated); + + final oldPageIndex = _pages.indexWhere((p) => identical(p, newPage)); + if (oldPageIndex != -1) { + changes[newPageNumber] = PdfPageStatusChange.moved(page: updated, oldPageNumber: oldPageIndex + 1); + } else { + changes[newPageNumber] = PdfPageStatusChange.modified(page: updated); + } + } + + _pages = List.unmodifiable(pages); + subject.add(PdfDocumentPageStatusChangedEvent(this, changes: changes)); + } + + void updateMissingFonts(Map? missingFonts) { + if (missingFonts == null || missingFonts.isEmpty) { + return; + } + final fontQueries = []; + for (final entry in missingFonts.entries) { + final font = entry.value as Map; + fontQueries.add( + PdfFontQuery( + face: font['face'] as String, + weight: (font['weight'] as num).toInt(), + isItalic: (font['italic'] as bool), + charset: PdfFontCharset.fromPdfiumCharsetId((font['charset'] as num).toInt()), + pitchFamily: (font['pitchFamily'] as num).toInt(), + ), + ); + } + subject.add(PdfDocumentMissingFontsEvent(this, fontQueries)); + } + + static PdfPermissions? parsePermissions(Map document) { + final perms = (document['permissions'] as num).toInt(); + final securityHandlerRevision = (document['securityHandlerRevision'] as num).toInt(); + if (perms >= 0 && securityHandlerRevision >= 0) { + return PdfPermissions(perms, securityHandlerRevision); + } else { + return null; + } + } + + static List parsePages(_PdfDocumentWasm doc, List pageList) { + return pageList + .map( + (page) => _PdfPageWasm( + doc, + (page['pageIndex'] as num).toInt(), + page['width'], + page['height'], + (page['rotation'] as num).toInt(), + (page['isLoaded'] as bool?) ?? false, + (page['bbLeft'] as num).toDouble(), + (page['bbBottom'] as num).toDouble(), + ), + ) + .toList(); + } + + @override + Future assemble() async { + // Build the indices, imported pages map, and rotations + final indices = []; + final importedPages = >{}; + final rotations = []; + var modifiedCount = 0; + + for (var i = 0; i < pages.length; i++) { + final page = pages[i]; + final wasmPage = page.unwrap<_PdfPageWasm>()!; + // if rotation is different, we need to modify the page + if (page.rotation.index != wasmPage.rotation.index) { + rotations.add(page.rotation.index); + modifiedCount++; + } else { + rotations.add(null); + } + if (page.document != this) { + // the page is from another document; need to import + final importId = -(i + 1); + indices.add(importId); + importedPages[importId] = { + 'docHandle': wasmPage.document.document['docHandle'], + 'pageNumber': wasmPage.pageNumber - 1, // 0-based + }; + modifiedCount++; + } else { + indices.add(wasmPage.pageNumber - 1); + if (wasmPage.pageNumber - 1 != i) { + modifiedCount++; + } + } + } + if (modifiedCount == 0) { + // No changes + return false; + } + + final result = await _sendCommand( + 'assemble', + parameters: { + 'docHandle': document['docHandle'], + 'pageIndices': indices, + 'rotations': rotations, + if (importedPages.isNotEmpty) 'importedPages': importedPages, + }, + ); + + return result['modified'] as bool; + } + + @override + Future encodePdf({bool incremental = false, bool removeSecurity = false}) async { + await assemble(); + final result = await _sendCommand( + 'encodePdf', + parameters: {'docHandle': document['docHandle'], 'incremental': incremental, 'removeSecurity': removeSecurity}, + ); + final bb = result['data'] as ByteBuffer; + return Uint8List.view(bb.asByteData().buffer, 0, bb.lengthInBytes); + } + + @override + Future useNativeDocumentHandle(FutureOr Function(int nativeDocumentHandle) task) { + throw UnimplementedError('PdfDocument.useNativeDocumentHandle is not implemented for WASM backend.'); + } +} + +class _PdfPageRenderCancellationTokenWasm extends PdfPageRenderCancellationToken { + _PdfPageRenderCancellationTokenWasm(); + + bool _isCanceled = false; + + @override + Future cancel() async { + _isCanceled = true; + } + + @override + bool get isCanceled => _isCanceled; +} + +class _PdfPageWasm extends PdfPage { + _PdfPageWasm( + this.document, + int pageIndex, + this.width, + this.height, + int rotation, + this.isLoaded, + this.bbLeft, + this.bbBottom, + ) : pageNumber = pageIndex + 1, + rotation = PdfPageRotation.values[rotation]; + + @override + PdfPageRenderCancellationToken createCancellationToken() { + return _PdfPageRenderCancellationTokenWasm(); + } + + @override + final _PdfDocumentWasm document; + + @override + Future> loadLinks({bool compact = false, bool enableAutoLinkDetection = true}) async { + if (document.isDisposed || !isLoaded) return []; + final result = await _sendCommand( + 'loadLinks', + parameters: { + 'docHandle': document.document['docHandle'], + 'pageIndex': pageNumber - 1, + 'enableAutoLinkDetection': enableAutoLinkDetection, + }, + ); + return (result['links'] as List).map((link) { + if (link is! Map) { + throw FormatException('Unexpected link structure: $link'); + } + final rects = (link['rects'] as List).map((r) { + final rect = r as List; + return PdfRect( + (rect[0] as double) - bbLeft, + (rect[1] as double) - bbBottom, + (rect[2] as double) - bbLeft, + (rect[3] as double) - bbBottom, + ); + }).toList(); + + final url = link['url']; + final dest = link['dest']; + + final annotationData = link['annotation'] as Map?; + final annotation = annotationData != null + ? PdfAnnotation( + title: annotationData['title'] as String?, + content: annotationData['content'] as String?, + subject: annotationData['subject'] as String?, + modificationDate: PdfDateTime.fromPdfDateString(annotationData['modificationDate']), + creationDate: PdfDateTime.fromPdfDateString(annotationData['creationDate']), + ) + : null; + + if (url is String) { + return PdfLink(rects, url: Uri.tryParse(url), annotation: annotation); + } + + if (dest != null && dest is Map) { + return PdfLink(rects, dest: _pdfDestFromMap(dest), annotation: annotation); + } + + if (annotation != null) { + return PdfLink(rects, annotation: annotation); + } + + return PdfLink(rects); + }).toList(); + } + + @override + Future loadText() async { + if (document.isDisposed || !isLoaded) return null; + final result = await _sendCommand( + 'loadText', + parameters: {'docHandle': document.document['docHandle'], 'pageIndex': pageNumber - 1}, + ); + final charRectsAll = (result['charRects'] as List).map((rect) { + final r = rect as List; + return PdfRect( + (r[0] as double) - bbLeft, + (r[1] as double) - bbBottom, + (r[2] as double) - bbLeft, + (r[3] as double) - bbBottom, + ); + }).toList(); + document.updateMissingFonts(result['missingFonts']); + return PdfPageRawText(result['fullText'] as String, charRectsAll); + } + + @override + final int pageNumber; + + @override + final double width; + + @override + final double height; + + @override + final PdfPageRotation rotation; + + @override + final bool isLoaded; + + final double bbLeft; + + final double bbBottom; + + @override + Future render({ + int x = 0, + int y = 0, + int? width, + int? height, + double? fullWidth, + double? fullHeight, + int? backgroundColor, + PdfPageRotation? rotationOverride, + PdfAnnotationRenderingMode annotationRenderingMode = PdfAnnotationRenderingMode.annotationAndForms, + int flags = PdfPageRenderFlags.none, + PdfPageRenderCancellationToken? cancellationToken, + }) async { + if (document.isDisposed) return null; + fullWidth ??= this.width; + fullHeight ??= this.height; + width ??= fullWidth.toInt(); + height ??= fullHeight.toInt(); + backgroundColor ??= 0xffffffff; // white background + + final result = await _sendCommand( + 'renderPage', + parameters: { + 'docHandle': document.document['docHandle'], + 'pageIndex': pageNumber - 1, + 'x': x, + 'y': y, + 'width': width, + 'height': height, + 'fullWidth': fullWidth, + 'fullHeight': fullHeight, + 'backgroundColor': backgroundColor, + 'rotation': rotationOverride != null ? (rotationOverride.index - rotation.index + 4) & 3 : 0, + 'annotationRenderingMode': annotationRenderingMode.index, + 'flags': flags, + 'formHandle': document.document['formHandle'], + }, + ); + final bb = result['imageData'] as ByteBuffer; + final pixels = Uint8List.view(bb.asByteData().buffer, 0, bb.lengthInBytes); + + if ((flags & PdfPageRenderFlags.premultipliedAlpha) != 0) { + final count = width * height; + for (var i = 0; i < count; i++) { + final b = pixels[i * 4]; + final g = pixels[i * 4 + 1]; + final r = pixels[i * 4 + 2]; + final a = pixels[i * 4 + 3]; + pixels[i * 4] = b * a ~/ 255; + pixels[i * 4 + 1] = g * a ~/ 255; + pixels[i * 4 + 2] = r * a ~/ 255; + } + } + + document.updateMissingFonts(result['missingFonts']); + + return PdfImageWeb(width: width, height: height, pixels: pixels); + } +} + +class PdfImageWeb extends PdfImage { + PdfImageWeb({required this.width, required this.height, required this.pixels}); + + @override + final int width; + @override + final int height; + @override + final Uint8List pixels; + @override + void dispose() {} +} + +PdfDest? _pdfDestFromMap(dynamic dest) { + if (dest == null) return null; + final params = dest['params'] as List; + return PdfDest( + (dest['pageIndex'] as num).toInt() + 1, + PdfDestCommand.parse(dest['command'] as String), + params.map((p) => p as double).toList(), + ); +} diff --git a/packages/pdfrx/lib/src/widgets/interactive_viewer.dart b/packages/pdfrx/lib/src/widgets/interactive_viewer.dart new file mode 100644 index 00000000..01888ead --- /dev/null +++ b/packages/pdfrx/lib/src/widgets/interactive_viewer.dart @@ -0,0 +1,1863 @@ +// ------------------------------------------------------------ +// FORKED FROM Flutter's original InteractiveViewer.dart +// ------------------------------------------------------------ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math' as math; + +import 'package:flutter/foundation.dart' show clampDouble; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/physics.dart'; +import 'package:flutter/services.dart'; +import 'package:vector_math/vector_math_64.dart' show Matrix4, Quad, Vector3; + +import '../utils/double_extensions.dart'; + +// Examples can assume: +// late BuildContext context; +// late Offset? _childWasTappedAt; +// late TransformationController _transformationController; +// Widget child = const Placeholder(); + +/// A signature for widget builders that take a [Quad] of the current viewport. +/// +/// See also: +/// +/// * [InteractiveViewer.builder], whose builder is of this type. +/// * [WidgetBuilder], which is similar, but takes no viewport. +typedef InteractiveViewerWidgetBuilder = Widget Function(BuildContext context, Quad viewport); + +/// A signature for providing dynamic boundary rect based on the current visible rect. +/// +/// Returns the boundary rect that should be used for the given [visibleRect] and [childSize]. +/// This allows boundaries to change based on scroll position (e.g., per-page boundaries). +/// +/// The [visibleRect] is in document coordinates (the child's coordinate space). +/// The [childSize] is the full size of the child widget. +/// Return null to use the static [InteractiveViewer.boundaryMargin] instead. +typedef BoundaryProvider = Rect? Function(Rect visibleRect, Size childSize); + +/// [**FORKED VERSION**] A widget that enables pan and zoom interactions with its child. +/// +/// {@youtube 560 315 https://www.youtube.com/watch?v=zrn7V3bMJvg} +/// +/// The user can transform the child by dragging to pan or pinching to zoom. +/// +/// By default, InteractiveViewer clips its child using [Clip.hardEdge]. +/// To prevent this behavior, consider setting [clipBehavior] to [Clip.none]. +/// When [clipBehavior] is [Clip.none], InteractiveViewer may draw outside of +/// its original area of the screen, such as when a child is zoomed in and +/// increases in size. However, it will not receive gestures outside of its original area. +/// To prevent dead areas where InteractiveViewer does not receive gestures, +/// don't set [clipBehavior] or be sure that the InteractiveViewer widget is the +/// size of the area that should be interactive. +/// +/// See also: +/// * The [Flutter Gallery's transformations demo](https://github.com/flutter/gallery/blob/main/lib/demos/reference/transformations_demo.dart), +/// which includes the use of InteractiveViewer. +/// * The [flutter-go demo](https://github.com/justinmc/flutter-go), which includes robust positioning of an InteractiveViewer child +/// that works for all screen sizes and child sizes. +/// * The [Lazy Flutter Performance Session](https://www.youtube.com/watch?v=qax_nOpgz7E), which includes the use of an InteractiveViewer to +/// performantly view subsets of a large set of widgets using the builder constructor. +/// +/// {@tool dartpad} +/// This example shows a simple Container that can be panned and zoomed. +/// +/// ** See code in examples/api/lib/widgets/interactive_viewer/interactive_viewer.0.dart ** +/// {@end-tool} +@immutable +class InteractiveViewer extends StatefulWidget { + /// Create an InteractiveViewer. + InteractiveViewer({ + required this.child, + super.key, + this.clipBehavior = Clip.hardEdge, + this.panAxis = PanAxis.free, + this.boundaryMargin = EdgeInsets.zero, + this.constrained = true, + // These default scale values were eyeballed as reasonable limits for common + // use cases. + this.maxScale = 8.0, + this.minScale = 0.8, + this.interactionEndFrictionCoefficient = _kDrag, + this.onInteractionEnd, + this.onInteractionStart, + this.onInteractionUpdate, + this.onAnimationEnd, + this.panEnabled = true, + this.scaleEnabled = true, + this.scaleFactor = kDefaultMouseScrollToScaleFactor, + this.transformationController, + this.alignment, + this.trackpadScrollCausesScale = false, + this.onWheelDelta, + this.scrollPhysics, + this.scrollPhysicsScale, + this.scrollPhysicsAutoAdjustBoundaries = true, + this.boundaryProvider, + }) : assert(minScale > 0), + assert(interactionEndFrictionCoefficient > 0), + assert(minScale.isFinite), + assert(maxScale > 0), + assert(!maxScale.isNaN), + assert(maxScale >= minScale), + // boundaryMargin must be either fully infinite or fully finite, but not + // a mix of both. + assert( + (boundaryMargin.horizontal.isInfinite && boundaryMargin.vertical.isInfinite) || + (boundaryMargin.top.isFinite && + boundaryMargin.right.isFinite && + boundaryMargin.bottom.isFinite && + boundaryMargin.left.isFinite), + ), + builder = null; + + /// Creates an InteractiveViewer for a child that is created on demand. + /// + /// Can be used to render a child that changes in response to the current + /// transformation. + /// + /// See the [builder] attribute docs for an example of using it to optimize a + /// large child. + InteractiveViewer.builder({ + required InteractiveViewerWidgetBuilder this.builder, + super.key, + this.clipBehavior = Clip.hardEdge, + this.panAxis = PanAxis.free, + this.boundaryMargin = EdgeInsets.zero, + // These default scale values were eyeballed as reasonable limits for common + // use cases. + this.maxScale = 8.0, + this.minScale = 0.8, + this.interactionEndFrictionCoefficient = _kDrag, + this.onInteractionEnd, + this.onInteractionStart, + this.onInteractionUpdate, + this.onAnimationEnd, + this.panEnabled = true, + this.scaleEnabled = true, + this.scaleFactor = 200.0, + this.transformationController, + this.alignment, + this.trackpadScrollCausesScale = false, + this.onWheelDelta, + this.scrollPhysics, + this.scrollPhysicsScale, + this.scrollPhysicsAutoAdjustBoundaries = true, + this.boundaryProvider, + }) : assert(minScale > 0), + assert(interactionEndFrictionCoefficient > 0), + assert(minScale.isFinite), + assert(maxScale > 0), + assert(!maxScale.isNaN), + assert(maxScale >= minScale), + // boundaryMargin must be either fully infinite or fully finite, but not + // a mix of both. + assert( + (boundaryMargin.horizontal.isInfinite && boundaryMargin.vertical.isInfinite) || + (boundaryMargin.top.isFinite && + boundaryMargin.right.isFinite && + boundaryMargin.bottom.isFinite && + boundaryMargin.left.isFinite), + ), + constrained = false, + child = null; + + /// The alignment of the child's origin, relative to the size of the box. + final Alignment? alignment; + + /// If set to [Clip.none], the child may extend beyond the size of the InteractiveViewer, + /// but it will not receive gestures in these areas. + /// Be sure that the InteractiveViewer is the desired size when using [Clip.none]. + /// + /// Defaults to [Clip.hardEdge]. + final Clip clipBehavior; + + /// When set to [PanAxis.aligned], panning is only allowed in the horizontal + /// axis or the vertical axis, diagonal panning is not allowed. + /// + /// When set to [PanAxis.vertical] or [PanAxis.horizontal] panning is only + /// allowed in the specified axis. For example, if set to [PanAxis.vertical], + /// panning will only be allowed in the vertical axis. And if set to [PanAxis.horizontal], + /// panning will only be allowed in the horizontal axis. + /// + /// When set to [PanAxis.free] panning is allowed in all directions. + /// + /// Defaults to [PanAxis.free]. + final PanAxis panAxis; + + /// A margin for the visible boundaries of the child. + /// + /// Any transformation that results in the viewport being able to view outside + /// of the boundaries will be stopped at the boundary. The boundaries do not + /// rotate with the rest of the scene, so they are always aligned with the + /// viewport. + /// + /// To produce no boundaries at all, pass infinite [EdgeInsets], such as + /// `EdgeInsets.all(double.infinity)`. + /// + /// No edge can be NaN. + /// + /// Defaults to [EdgeInsets.zero], which results in boundaries that are the + /// exact same size and position as the [child]. + final EdgeInsets boundaryMargin; + + /// Builds the child of this widget. + /// + /// Passed with the [InteractiveViewer.builder] constructor. Otherwise, the + /// [child] parameter must be passed directly, and this is null. + /// + /// {@tool dartpad} + /// This example shows how to use builder to create a [Table] whose cell + /// contents are only built when they are visible. Built and remove cells are + /// logged in the console for illustration. + /// + /// ** See code in examples/api/lib/widgets/interactive_viewer/interactive_viewer.builder.0.dart ** + /// {@end-tool} + /// + /// See also: + /// + /// * [ListView.builder], which follows a similar pattern. + final InteractiveViewerWidgetBuilder? builder; + + /// The child [Widget] that is transformed by InteractiveViewer. + /// + /// If the [InteractiveViewer.builder] constructor is used, then this will be + /// null, otherwise it is required. + final Widget? child; + + /// Whether the normal size constraints at this point in the widget tree are + /// applied to the child. + /// + /// If set to false, then the child will be given infinite constraints. This + /// is often useful when a child should be bigger than the InteractiveViewer. + /// + /// For example, for a child which is bigger than the viewport but can be + /// panned to reveal parts that were initially offscreen, [constrained] must + /// be set to false to allow it to size itself properly. If [constrained] is + /// true and the child can only size itself to the viewport, then areas + /// initially outside of the viewport will not be able to receive user + /// interaction events. If experiencing regions of the child that are not + /// receptive to user gestures, make sure [constrained] is false and the child + /// is sized properly. + /// + /// Defaults to true. + /// + /// {@tool dartpad} + /// This example shows how to create a pannable table. Because the table is + /// larger than the entire screen, setting [constrained] to false is necessary + /// to allow it to be drawn to its full size. The parts of the table that + /// exceed the screen size can then be panned into view. + /// + /// ** See code in examples/api/lib/widgets/interactive_viewer/interactive_viewer.constrained.0.dart ** + /// {@end-tool} + final bool constrained; + + /// If false, the user will be prevented from panning. + /// + /// Defaults to true. + /// + /// See also: + /// + /// * [scaleEnabled], which is similar but for scale. + final bool panEnabled; + + /// If false, the user will be prevented from scaling. + /// + /// Defaults to true. + /// + /// See also: + /// + /// * [panEnabled], which is similar but for panning. + final bool scaleEnabled; + + /// {@macro flutter.gestures.scale.trackpadScrollCausesScale} + final bool trackpadScrollCausesScale; + + /// Determines the amount of scale to be performed per pointer scroll. + /// + /// Defaults to [kDefaultMouseScrollToScaleFactor]. + /// + /// Increasing this value above the default causes scaling to feel slower, + /// while decreasing it causes scaling to feel faster. + /// + /// The amount of scale is calculated as the exponential function of the + /// [PointerScrollEvent.scrollDelta] to [scaleFactor] ratio. In the Flutter + /// engine, the mousewheel [PointerScrollEvent.scrollDelta] is hardcoded to 20 + /// per scroll, while a trackpad scroll can be any amount. + /// + /// Affects only pointer device scrolling, not pinch to zoom. + final double scaleFactor; + + /// The maximum allowed scale. + /// + /// The scale will be clamped between this and [minScale] inclusively. + /// + /// Defaults to 2.5. + /// + /// Must be greater than zero and greater than [minScale]. + final double maxScale; + + /// The minimum allowed scale. + /// + /// The scale will be clamped between this and [maxScale] inclusively. + /// + /// Scale is also affected by [boundaryMargin]. If the scale would result in + /// viewing beyond the boundary, then it will not be allowed. By default, + /// boundaryMargin is EdgeInsets.zero, so scaling below 1.0 will not be + /// allowed in most cases without first increasing the boundaryMargin. + /// + /// Defaults to 0.8. + /// + /// Must be a finite number greater than zero and less than [maxScale]. + final double minScale; + + /// Changes the deceleration behavior after a gesture. + /// + /// Defaults to 0.0000135. + /// + /// Must be a finite number greater than zero. + final double interactionEndFrictionCoefficient; + + /// Called when the user ends a pan or scale gesture on the widget. + /// + /// At the time this is called, the [TransformationController] will have + /// already been updated to reflect the change caused by the interaction, + /// though a pan may cause an inertia animation after this is called as well. + /// + /// {@template flutter.widgets.InteractiveViewer.onInteractionEnd} + /// Will be called even if the interaction is disabled with [panEnabled] or + /// [scaleEnabled] for both touch gestures and mouse interactions. + /// + /// A [GestureDetector] wrapping the InteractiveViewer will not respond to + /// [GestureDetector.onScaleStart], [GestureDetector.onScaleUpdate], and + /// [GestureDetector.onScaleEnd]. Use [onInteractionStart], + /// [onInteractionUpdate], and [onInteractionEnd] to respond to those + /// gestures. + /// {@endtemplate} + /// + /// See also: + /// + /// * [onInteractionStart], which handles the start of the same interaction. + /// * [onInteractionUpdate], which handles an update to the same interaction. + final GestureScaleEndCallback? onInteractionEnd; + + /// Called when the user begins a pan or scale gesture on the widget. + /// + /// At the time this is called, the [TransformationController] will not have + /// changed due to this interaction. + /// + /// {@macro flutter.widgets.InteractiveViewer.onInteractionEnd} + /// + /// The coordinates provided in the details' `focalPoint` and + /// `localFocalPoint` are normal Flutter event coordinates, not + /// InteractiveViewer scene coordinates. See + /// [TransformationController.toScene] for how to convert these coordinates to + /// scene coordinates relative to the child. + /// + /// See also: + /// + /// * [onInteractionUpdate], which handles an update to the same interaction. + /// * [onInteractionEnd], which handles the end of the same interaction. + final GestureScaleStartCallback? onInteractionStart; + + /// Called when the user updates a pan or scale gesture on the widget. + /// + /// At the time this is called, the [TransformationController] will have + /// already been updated to reflect the change caused by the interaction, if + /// the interaction caused the matrix to change. + /// + /// {@macro flutter.widgets.InteractiveViewer.onInteractionEnd} + /// + /// The coordinates provided in the details' `focalPoint` and + /// `localFocalPoint` are normal Flutter event coordinates, not + /// InteractiveViewer scene coordinates. See + /// [TransformationController.toScene] for how to convert these coordinates to + /// scene coordinates relative to the child. + /// + /// See also: + /// + /// * [onInteractionStart], which handles the start of the same interaction. + /// * [onInteractionEnd], which handles the end of the same interaction. + final GestureScaleUpdateCallback? onInteractionUpdate; + + /// Called when all animations (inertia, scale, snap) have completed. + /// + /// This is useful for triggering UI updates after zoom or pan animations finish. + final VoidCallback? onAnimationEnd; + + /// A [TransformationController] for the transformation performed on the + /// child. + /// + /// Whenever the child is transformed, the [Matrix4] value is updated and all + /// listeners are notified. If the value is set, InteractiveViewer will update + /// to respect the new value. + /// + /// {@tool dartpad} + /// This example shows how transformationController can be used to animate the + /// transformation back to its starting position. + /// + /// ** See code in examples/api/lib/widgets/interactive_viewer/interactive_viewer.transformation_controller.0.dart ** + /// {@end-tool} + /// + /// See also: + /// + /// * [ValueNotifier], the parent class of TransformationController. + /// * [TextEditingController] for an example of another similar pattern. + final TransformationController? transformationController; + + /// To override the default mouse wheel behavior. + /// + final void Function(PointerScrollEvent event)? onWheelDelta; + + // Used as the coefficient of friction in the inertial translation animation. + // This value was eyeballed to give a feel similar to Google Photos. + static const double _kDrag = 0.0000135; + + /// ScrollPhysics to use for panning + final ScrollPhysics? scrollPhysics; + + /// ScrollPhysic to use for scaling + final ScrollPhysics? scrollPhysicsScale; + + /// Whether to automatically increase the ScrollPhysics boundaries when the + /// child size is smaller than the viewport size. + final bool scrollPhysicsAutoAdjustBoundaries; + + /// Provides dynamic boundary margins based on the current visible rect. + /// + /// When provided, this callback is called to determine the boundary margins + /// based on the current scroll position. This allows for per-page boundaries + /// in discrete scrolling modes. + /// + /// If null, [boundaryMargin] is used as a static boundary. + final BoundaryProvider? boundaryProvider; + + /// Returns the closest point to the given point on the given line segment. + @visibleForTesting + static Vector3 getNearestPointOnLine(Vector3 point, Vector3 l1, Vector3 l2) { + final lengthSquared = math.pow(l2.x - l1.x, 2.0).toDouble() + math.pow(l2.y - l1.y, 2.0).toDouble(); + + // In this case, l1 == l2. + if (lengthSquared == 0) { + return l1; + } + + // Calculate how far down the line segment the closest point is and return + // the point. + final l1P = point - l1; + final l1L2 = l2 - l1; + final fraction = clampDouble(l1P.dot(l1L2) / lengthSquared, 0.0, 1.0); + return l1 + l1L2 * fraction; + } + + /// Given a quad, return its axis aligned bounding box. + @visibleForTesting + static Quad getAxisAlignedBoundingBox(Quad quad) { + final double minX = math.min(quad.point0.x, math.min(quad.point1.x, math.min(quad.point2.x, quad.point3.x))); + final double minY = math.min(quad.point0.y, math.min(quad.point1.y, math.min(quad.point2.y, quad.point3.y))); + final double maxX = math.max(quad.point0.x, math.max(quad.point1.x, math.max(quad.point2.x, quad.point3.x))); + final double maxY = math.max(quad.point0.y, math.max(quad.point1.y, math.max(quad.point2.y, quad.point3.y))); + return Quad.points(Vector3(minX, minY, 0), Vector3(maxX, minY, 0), Vector3(maxX, maxY, 0), Vector3(minX, maxY, 0)); + } + + /// Returns true iff the point is inside the rectangle given by the Quad, + /// inclusively. + /// Algorithm from https://math.stackexchange.com/a/190373. + @visibleForTesting + static bool pointIsInside(Vector3 point, Quad quad) { + final aM = point - quad.point0; + final aB = quad.point1 - quad.point0; + final aD = quad.point3 - quad.point0; + + final aMAB = aM.dot(aB); + final aBAB = aB.dot(aB); + final aMAD = aM.dot(aD); + final aDAD = aD.dot(aD); + + return 0 <= aMAB && aMAB <= aBAB && 0 <= aMAD && aMAD <= aDAD; + } + + /// Get the point inside (inclusively) the given Quad that is nearest to the + /// given Vector3. + @visibleForTesting + static Vector3 getNearestPointInside(Vector3 point, Quad quad) { + // If the point is inside the axis aligned bounding box, then it's ok where + // it is. + if (pointIsInside(point, quad)) { + return point; + } + + // Otherwise, return the nearest point on the quad. + final closestPoints = [ + InteractiveViewer.getNearestPointOnLine(point, quad.point0, quad.point1), + InteractiveViewer.getNearestPointOnLine(point, quad.point1, quad.point2), + InteractiveViewer.getNearestPointOnLine(point, quad.point2, quad.point3), + InteractiveViewer.getNearestPointOnLine(point, quad.point3, quad.point0), + ]; + var minDistance = double.infinity; + late Vector3 closestOverall; + for (final closePoint in closestPoints) { + final distance = math.sqrt(math.pow(point.x - closePoint.x, 2) + math.pow(point.y - closePoint.y, 2)); + if (distance < minDistance) { + minDistance = distance; + closestOverall = closePoint; + } + } + return closestOverall; + } + + @override + State createState() => InteractiveViewerState(); +} + +class InteractiveViewerState extends State with TickerProviderStateMixin { + // Preserve the originally provided boundaryMargin for recalculation overrides. + late final EdgeInsets _originalBoundaryMargin = widget.boundaryMargin; + late TransformationController _transformer = widget.transformationController ?? TransformationController(); + + final GlobalKey _childKey = GlobalKey(); + final GlobalKey _parentKey = GlobalKey(); + Animation? _animation; + Animation? _scaleAnimation; + late Offset _scaleAnimationFocalPoint; + late AnimationController _controller; + late AnimationController _scaleController; + Axis? _currentAxis; // Used with panAxis. + Offset? _referenceFocalPoint; // Point where the current gesture began. + double? _scaleStart; // Scale value at start of scaling gesture. + double? _rotationStart = 0.0; // Rotation at start of rotation gesture. + double _currentRotation = 0.0; // Rotation of _transformationController.value. + _GestureType? _gestureType; + + // For ScrollPhysics + late AnimationController _snapController; // Snap-back animation controller and matrices/scales + late Matrix4 _snapStartMatrix; // Snap-back for matrix interpolation + Matrix4? _snapTargetMatrix; // Holds the transform at the exact moment the pinch ends + late Offset _snapFocalPoint; // Focal point for matrix snap-back interpolation + double _lastScale = 1.0; // to enable us to work in incremental scale changes for pinch zoom + Simulation? simulationX; // Simulations to use if scrollPhysics is specified + Simulation? simulationY; + Simulation? combinedSimulation; + Simulation? simulationScale; // Simulation for scale fling + // end ScrollPhysics + + // TODO(justinmc): Add rotateEnabled parameter to the widget and remove this + // hardcoded value when the rotation feature is implemented. + // https://github.com/flutter/flutter/issues/57698 + final bool _rotateEnabled = false; + + // The _boundaryRect is calculated by adding the boundaryMargin to the size of + // the child. If boundaryProvider is set, it's called to get dynamic boundaries. + Rect get _boundaryRect { + assert(_childKey.currentContext != null); + + final childRenderBox = _childKey.currentContext!.findRenderObject()! as RenderBox; + final childSize = childRenderBox.size; + + // Check if we should use dynamic boundaries from provider + if (widget.boundaryProvider != null) { + // Calculate current visible rect for provider + final translationVector = _transformer.value.getTranslation(); + final scale = _transformer.value.getMaxScaleOnAxis(); + final viewportSize = _viewport.size; + + final visibleRect = Rect.fromLTWH( + -translationVector.x / scale, + -translationVector.y / scale, + viewportSize.width / scale, + viewportSize.height / scale, + ); + + final dynamicBoundary = widget.boundaryProvider!(visibleRect, childSize); + if (dynamicBoundary != null) { + assert(!dynamicBoundary.isEmpty, "InteractiveViewer's boundary must have nonzero dimensions."); + assert( + dynamicBoundary.isFinite || + (dynamicBoundary.left.isInfinite && + dynamicBoundary.top.isInfinite && + dynamicBoundary.right.isInfinite && + dynamicBoundary.bottom.isInfinite), + 'boundaryRect must either be infinite in all directions or finite in all directions.', + ); + return dynamicBoundary; + } + } + + // Fall back to static boundary margin + final boundaryMargin = widget.boundaryMargin; + assert(!boundaryMargin.left.isNaN); + assert(!boundaryMargin.right.isNaN); + assert(!boundaryMargin.top.isNaN); + assert(!boundaryMargin.bottom.isNaN); + + final boundaryRect = boundaryMargin.inflateRect(Offset.zero & childSize); + assert(!boundaryRect.isEmpty, "InteractiveViewer's child must have nonzero dimensions."); + assert( + boundaryRect.isFinite || + (boundaryRect.left.isInfinite && + boundaryRect.top.isInfinite && + boundaryRect.right.isInfinite && + boundaryRect.bottom.isInfinite), + 'boundaryRect must either be infinite in all directions or finite in all directions.', + ); + return boundaryRect; + } + + // The Rect representing the child's parent. + Rect get _viewport { + assert(_parentKey.currentContext != null); + final parentRenderBox = _parentKey.currentContext!.findRenderObject()! as RenderBox; + return Offset.zero & parentRenderBox.size; + } + + // Return a new matrix representing the given matrix after applying the given + // translation. + Matrix4 _matrixTranslate(Matrix4 matrix, Offset translation) { + if (translation == Offset.zero) { + return matrix.clone(); + } + + final Offset alignedTranslation; + + if (_currentAxis != null && _gestureType == _GestureType.pan) { + alignedTranslation = switch (widget.panAxis) { + PanAxis.horizontal => _alignAxis(translation, Axis.horizontal), + PanAxis.vertical => _alignAxis(translation, Axis.vertical), + PanAxis.aligned => _alignAxis(translation, _currentAxis!), + PanAxis.free => translation, + }; + } else { + alignedTranslation = translation; + } + + final nextMatrix = matrix.clone()..translateByDouble(alignedTranslation.dx, alignedTranslation.dy, 0, 1); + + // Transform the viewport to determine where its four corners will be after + // the child has been transformed. + final nextViewport = _transformViewport(nextMatrix, _viewport); + + // If the boundaries are infinite, then no need to check if the translation + // fits within them. + if (_boundaryRect.isInfinite) { + return nextMatrix; + } + + /// ScrollPhysics + /// If the ScrollPhysics is defined we apply physics (bouncing or clamping) during pan. + if (widget.scrollPhysics != null) { + final physics = (_gestureType == _GestureType.scale) + ? (widget.scrollPhysicsScale ?? widget.scrollPhysics!) + : widget.scrollPhysics!; + // current translation in scene coordinates (negative because controller stores inverse) + final currentOffset = _getMatrixTranslation(_transformer.value) * -1; + // build scroll metrics + final metricsX = _calculateScrollMetrics(currentOffset.dx, AxisDirection.right); + final metricsY = _calculateScrollMetrics(currentOffset.dy, AxisDirection.down); + + final proposedX = currentOffset.dx - alignedTranslation.dx; + final proposedY = currentOffset.dy - alignedTranslation.dy; + + final overscrollX = proposedX == currentOffset.dx + ? 0 + : physics.applyBoundaryConditions(metricsX, proposedX); // : 0. + final overscrollY = proposedY == currentOffset.dy + ? 0 + : physics.applyBoundaryConditions(metricsY, proposedY); // : 0. + + // If the overscroll is zero, the ScrollPhysics (such as BouncingScrollPhysics) is + // enabling us to go out of boundaries, so we apply physics to the translation. + if (overscrollX == 0 && overscrollY == 0) { + if (_gestureType == _GestureType.scale) { + // TODO: better handle pan offsets when pinch zooming - for now, don't apply + // physics as it introduces issues around the snapback animation position + // due to an incorrect focal point, as well as causing undesired zoom behavior + // such as when zooming out at the bottom of a document + return nextMatrix; + } + // Check if the offset is accepted by the ScrollPhysics, and so apply it. + var dx = 0.0; + if (alignedTranslation.dx != 0 && physics.shouldAcceptUserOffset(_normalizeScrollMetrics(metricsX))) { + dx = physics.applyPhysicsToUserOffset(metricsX, alignedTranslation.dx); + } + var dy = 0.0; + if (alignedTranslation.dy != 0 && physics.shouldAcceptUserOffset(_normalizeScrollMetrics(metricsY))) { + dy = physics.applyPhysicsToUserOffset(metricsY, alignedTranslation.dy); + } + return matrix.clone()..translateByDouble(dx, dy, 0, 1); + } else { + // correct any overscroll + return matrix.clone() + ..translateByDouble(alignedTranslation.dx + overscrollX, alignedTranslation.dy + overscrollY, 0, 1); + } + } + + // Expand the boundaries with rotation. This prevents the problem where a + // mismatch in orientation between the viewport and boundaries effectively + // limits translation. With this approach, all points that are visible with + // no rotation are visible after rotation. + final boundariesAabbQuad = _getAxisAlignedBoundingBoxWithRotation(_boundaryRect, _currentRotation); + + // If the given translation fits completely within the boundaries, allow it. + final offendingDistance = _exceedsBy(boundariesAabbQuad, nextViewport); + if (offendingDistance == Offset.zero) { + return nextMatrix; + } + + // Desired translation goes out of bounds, so translate to the nearest + // in-bounds point instead. + final nextTotalTranslation = _getMatrixTranslation(nextMatrix); + final currentScale = matrix.getMaxScaleOnAxis(); + final correctedTotalTranslation = Offset( + nextTotalTranslation.dx - offendingDistance.dx * currentScale, + nextTotalTranslation.dy - offendingDistance.dy * currentScale, + ); + // TODO(justinmc): This needs some work to handle rotation properly. The + // idea is that the boundaries are axis aligned (boundariesAabbQuad), but + // calculating the translation to put the viewport inside that Quad is more + // complicated than this when rotated. + // https://github.com/flutter/flutter/issues/57698 + final correctedMatrix = matrix.clone() + ..setTranslation(Vector3(correctedTotalTranslation.dx, correctedTotalTranslation.dy, 0.0)); + + // Double check that the corrected translation fits. + final correctedViewport = _transformViewport(correctedMatrix, _viewport); + final offendingCorrectedDistance = _exceedsBy(boundariesAabbQuad, correctedViewport); + if (offendingCorrectedDistance == Offset.zero) { + return correctedMatrix; + } + + // If the corrected translation doesn't fit in either direction, don't allow + // any translation at all. This happens when the viewport is larger than the + // entire boundary. + if (offendingCorrectedDistance.dx != 0.0 && offendingCorrectedDistance.dy != 0.0) { + return matrix.clone(); + } + + // Otherwise, allow translation in only the direction that fits. This + // happens when the viewport is larger than the boundary in one direction. + final unidirectionalCorrectedTotalTranslation = Offset( + offendingCorrectedDistance.dx == 0.0 ? correctedTotalTranslation.dx : 0.0, + offendingCorrectedDistance.dy == 0.0 ? correctedTotalTranslation.dy : 0.0, + ); + return matrix.clone()..setTranslation( + Vector3(unidirectionalCorrectedTotalTranslation.dx, unidirectionalCorrectedTotalTranslation.dy, 0.0), + ); + } + + // Return a new matrix representing the given matrix after applying the given + // scale. + Matrix4 _matrixScale(Matrix4 matrix, double scale) { + // No-op for unity scale + if (scale == 1.0) { + return matrix.clone(); + } + assert(scale != 0.0); + + // fallback to widget.scrollPhysics if widget.scrollPhysicsScale not specified + final scrollPhysics = widget.scrollPhysicsScale ?? widget.scrollPhysics; + + if (scrollPhysics != null) { + // Compute current and desired scales + final currentScale = _transformer.value.getMaxScaleOnAxis(); + // scale provided is a desired change in scale between the current scale + // and the start of the gesture + final scaleChange = scale; + + // desired but not necessarily achieved if physics is applied + final desiredScale = currentScale * scale; + + final allowedScale = _getAllowedScale(desiredScale); + // Early return if not allowed to zoom outside bounds + if (allowedScale != desiredScale) { + return matrix.clone()..scaleByDouble(allowedScale, allowedScale, allowedScale, 1); + } + + // Compute ratio of this update's scale to the previous update + final scaleRatio = scaleChange / _lastScale; + // Store for next frame + _lastScale = scaleChange; + // Physics requires the incremental scale change since last update + final incrementalScale = currentScale * scaleRatio; + + // Content-space-based scrollPhysics for scale overscroll and undershoot + if (_gestureType == _GestureType.scale && + !_snapController.isAnimating && + ((desiredScale < widget.minScale) || (desiredScale > widget.maxScale))) { + final contentSize = _boundaryRect.isInfinite ? _childSize() : _boundaryRect.size; + + // Compute current and desired absolute scale + final contentWidth = contentSize.width * currentScale; + final desiredContentWidth = contentSize.width * incrementalScale; + final contentHeight = contentSize.height * currentScale; + final desiredContentHeight = contentSize.height * incrementalScale; + + // Build horizontal and vertical metrics + final ScrollMetrics metricsX = FixedScrollMetrics( + pixels: contentWidth, + minScrollExtent: contentSize.width * widget.minScale, + maxScrollExtent: contentSize.width * widget.maxScale, + viewportDimension: contentSize.width * widget.maxScale, + axisDirection: AxisDirection.right, + devicePixelRatio: 1.0, + ); + final ScrollMetrics metricsY = FixedScrollMetrics( + pixels: contentHeight, + minScrollExtent: contentSize.height * widget.minScale, + maxScrollExtent: contentSize.height * widget.maxScale, + viewportDimension: contentSize.height * widget.maxScale, + axisDirection: AxisDirection.down, + devicePixelRatio: 1.0, + ); + + // Compute content deltas + final deltaX = desiredContentWidth - contentWidth; + final deltaY = desiredContentHeight - contentHeight; + + // Apply scroll physics half the delta to simulate exceeding a boundary + // on one side + final adjustedX = scrollPhysics.applyPhysicsToUserOffset(metricsX, deltaX / 2) * 2; + final adjustedY = scrollPhysics.applyPhysicsToUserOffset(metricsY, deltaY / 2) * 2; + + // Convert back to scale factors + final newScaleX = (contentWidth + adjustedX) / contentWidth; + final newScaleY = (contentHeight + adjustedY) / contentHeight; + final factor = (newScaleX + newScaleY) / 2; + + return matrix.clone()..scaleByDouble(factor, factor, factor, 1); + } else { + final clampedTotalScale = clampDouble(desiredScale, widget.minScale, widget.maxScale); + final clampedScale = clampedTotalScale / currentScale; + + // Apply the scale factor to the matrix + return matrix.clone()..scaleByDouble(clampedScale, clampedScale, clampedScale, 1); + } + } else { + // Don't allow a scale that results in an overall scale beyond min/max + // scale. + final currentScale = _transformer.value.getMaxScaleOnAxis(); + final double totalScale = math.max( + currentScale * scale, + // Ensure that the scale cannot make the child so big that it can't fit + // inside the boundaries (in either direction). + math.max(_viewport.width / _boundaryRect.width, _viewport.height / _boundaryRect.height), + ); + final clampedTotalScale = clampDouble(totalScale, widget.minScale, widget.maxScale); + final clampedScale = clampedTotalScale / currentScale; + return matrix.clone()..scaleByDouble(clampedScale, clampedScale, clampedScale, 1); + } + } + + // Return a new matrix representing the given matrix after applying the given + // rotation. + Matrix4 _matrixRotate(Matrix4 matrix, double rotation, Offset focalPoint) { + if (rotation == 0) { + return matrix.clone(); + } + final focalPointScene = _transformer.toScene(focalPoint); + return matrix.clone() + ..translateByDouble(focalPointScene.dx, focalPointScene.dy, 0, 1) + ..rotateZ(-rotation) + ..translateByDouble(-focalPointScene.dx, -focalPointScene.dy, 0, 1); + } + + // Returns true iff the given _GestureType is enabled. + bool _gestureIsSupported(_GestureType? gestureType) { + return switch (gestureType) { + _GestureType.rotate => _rotateEnabled, + _GestureType.scale => widget.scaleEnabled, + _GestureType.pan || null => widget.panEnabled, + }; + } + + // Decide which type of gesture this is by comparing the amount of scale + // and rotation in the gesture, if any. Scale starts at 1 and rotation + // starts at 0. Pan will have no scale and no rotation because it uses only one + // finger. + _GestureType _getGestureType(ScaleUpdateDetails details) { + final scale = !widget.scaleEnabled ? 1.0 : details.scale; + final rotation = !_rotateEnabled ? 0.0 : details.rotation; + if ((scale - 1).abs() > rotation.abs()) { + return _GestureType.scale; + } else if (rotation != 0.0) { + return _GestureType.rotate; + } else { + return _GestureType.pan; + } + } + + // Handle the start of a gesture. All of pan, scale, and rotate are handled + // with GestureDetector's scale gesture. + void _onScaleStart(ScaleStartDetails details) { + widget.onInteractionStart?.call(details); + + if (_controller.isAnimating) { + _controller.stop(); + _controller.reset(); + _animation?.removeListener(_handleInertiaAnimation); + _animation = null; + } + + if (_scaleController.isAnimating) { + _scaleController.stop(); + _scaleController.reset(); + _scaleAnimation?.removeListener(_handleScaleAnimation); + _scaleAnimation = null; + } + + _gestureType = null; + _currentAxis = null; + _scaleStart = _transformer.value.getMaxScaleOnAxis(); + _lastScale = 1.0; // ScrollPhysics + _referenceFocalPoint = _transformer.toScene(details.localFocalPoint); + _snapFocalPoint = details.localFocalPoint; + _rotationStart = _currentRotation; + } + + // Handle an update to an ongoing gesture. All of pan, scale, and rotate are + // handled with GestureDetector's scale gesture. + void _onScaleUpdate(ScaleUpdateDetails details) { + final scale = _transformer.value.getMaxScaleOnAxis(); + _scaleAnimationFocalPoint = details.localFocalPoint; + final focalPointScene = _transformer.toScene(details.localFocalPoint); + + if (_gestureType == _GestureType.pan) { + // When a gesture first starts, it sometimes has no change in scale and + // rotation despite being a two-finger gesture. Here the gesture is + // allowed to be reinterpreted as its correct type after originally + // being marked as a pan. + _gestureType = _getGestureType(details); + } else { + _gestureType ??= _getGestureType(details); + } + if (!_gestureIsSupported(_gestureType)) { + widget.onInteractionUpdate?.call(details); + return; + } + + switch (_gestureType!) { + case _GestureType.scale: + assert(_scaleStart != null); + // details.scale gives us the amount to change the scale as of the + // start of this gesture, so calculate the amount to scale as of the + // previous call to _onScaleUpdate. + final desiredScale = _scaleStart! * details.scale; + final scaleChange = desiredScale / scale; + _snapFocalPoint = details.localFocalPoint; + _transformer.value = _matrixScale(_transformer.value, scaleChange); + + // While scaling, translate such that the user's two fingers stay on + // the same places in the scene. That means that the focal point of + // the scale should be on the same place in the scene before and after + // the scale. + final focalPointSceneScaled = _transformer.toScene(details.localFocalPoint); + _transformer.value = _matrixTranslate(_transformer.value, focalPointSceneScaled - _referenceFocalPoint!); + + // details.localFocalPoint should now be at the same location as the + // original _referenceFocalPoint point. If it's not, that's because + // the translate came in contact with a boundary. In that case, update + // _referenceFocalPoint so subsequent updates happen in relation to + // the new effective focal point. + final focalPointSceneCheck = _transformer.toScene(details.localFocalPoint); + if (_referenceFocalPoint!.round10BitFrac() != focalPointSceneCheck.round10BitFrac()) { + _referenceFocalPoint = focalPointSceneCheck; + } + + case _GestureType.rotate: + if (details.rotation == 0.0) { + widget.onInteractionUpdate?.call(details); + return; + } + final desiredRotation = _rotationStart! + details.rotation; + _transformer.value = _matrixRotate( + _transformer.value, + _currentRotation - desiredRotation, + details.localFocalPoint, + ); + _currentRotation = desiredRotation; + + case _GestureType.pan: + assert(_referenceFocalPoint != null); + // details may have a change in scale here when scaleEnabled is false. + // In an effort to keep the behavior similar whether or not scaleEnabled + // is true, these gestures are thrown away. + if (details.scale != 1.0) { + widget.onInteractionUpdate?.call(details); + return; + } + _currentAxis ??= _getPanAxis(_referenceFocalPoint!, focalPointScene); + // Translate so that the same point in the scene is underneath the + // focal point before and after the movement. + final translationChange = focalPointScene - _referenceFocalPoint!; + _transformer.value = _matrixTranslate(_transformer.value, translationChange); + _referenceFocalPoint = _transformer.toScene(details.localFocalPoint); + } + widget.onInteractionUpdate?.call(details); + } + + // Handle the end of a gesture of _GestureType. All of pan, scale, and rotate + // are handled with GestureDetector's scale gesture. + void _onScaleEnd(ScaleEndDetails details) { + widget.onInteractionEnd?.call(details); + _rotationStart = null; + _referenceFocalPoint = null; + + _animation?.removeListener(_handleInertiaAnimation); + _scaleAnimation?.removeListener(_handleScaleAnimation); + _controller.reset(); + _scaleController.reset(); + + if (!_gestureIsSupported(_gestureType)) { + _currentAxis = null; + return; + } + + switch (_gestureType) { + case _GestureType.pan: + if (widget.scrollPhysics != null) { + if (_snapController.isAnimating) return; + + final currentTranslation = _transformer.value.getTranslation(); + final currentOffset = Offset(currentTranslation.x, currentTranslation.y); + final adjustedOffset = currentOffset * -1; + + final flingVelocityX = + math.min(details.velocity.pixelsPerSecond.dx.abs(), widget.scrollPhysics!.maxFlingVelocity) * + details.velocity.pixelsPerSecond.dx.sign; + final flingVelocityY = + math.min(details.velocity.pixelsPerSecond.dy.abs(), widget.scrollPhysics!.maxFlingVelocity) * + details.velocity.pixelsPerSecond.dy.sign; + + final metricsX = _calculateScrollMetrics(adjustedOffset.dx, AxisDirection.right); + final metricsY = _calculateScrollMetrics(adjustedOffset.dy, AxisDirection.down); + + if (details.velocity.pixelsPerSecond.distance <= widget.scrollPhysics!.minFlingVelocity && + !metricsX.outOfRange && + !metricsY.outOfRange) { + return; + } + + simulationX = widget.scrollPhysics!.createBallisticSimulation(metricsX, -flingVelocityX); + simulationY = widget.scrollPhysics!.createBallisticSimulation(metricsY, -flingVelocityY); + combinedSimulation = _getCombinedSimulation(simulationX, simulationY); + + if (combinedSimulation == null) { + return; + } + + _controller.addListener(_handleInertiaAnimation); + _controller.animateWith(combinedSimulation!); + } else { + if (details.velocity.pixelsPerSecond.distance < kMinFlingVelocity) { + _currentAxis = null; + return; + } + final translationVector = _transformer.value.getTranslation(); + final translation = Offset(translationVector.x, translationVector.y); + // (Removed FrictionSimulation logic for scale; only pan uses it.) + final frictionSimulationX = FrictionSimulation( + widget.interactionEndFrictionCoefficient, + translation.dx, + details.velocity.pixelsPerSecond.dx, + ); + final frictionSimulationY = FrictionSimulation( + widget.interactionEndFrictionCoefficient, + translation.dy, + details.velocity.pixelsPerSecond.dy, + ); + final tFinal = _getFinalTime( + details.velocity.pixelsPerSecond.distance, + widget.interactionEndFrictionCoefficient, + ); + _animation = Tween( + begin: translation, + end: Offset(frictionSimulationX.finalX, frictionSimulationY.finalX), + ).animate(CurvedAnimation(parent: _controller, curve: Curves.decelerate)); + _controller.duration = Duration(milliseconds: (tFinal * 1000).round()); + _animation!.addListener(_handleInertiaAnimation); + _controller.forward(); + } + break; + case _GestureType.scale: + if (widget.scrollPhysics != null) { + final endScale = _transformer.value.getMaxScaleOnAxis(); + final clampedScale = endScale.clamp(widget.minScale, widget.maxScale); + + if (clampedScale != endScale) { + HapticFeedback.lightImpact(); + } + // even if the the scale doesn't change, we may be out of bounds, and + // want to animate the snap back to bounds + _snapStartMatrix = _transformer.value.clone(); + final pivotScene = _transformer.toScene(_snapFocalPoint); + final endMatrix = _snapStartMatrix.clone() + ..translateByDouble(pivotScene.dx, pivotScene.dy, 0, 1) + ..scaleByDouble(clampedScale / endScale, clampedScale / endScale, clampedScale / endScale, 1) + ..translateByDouble(-pivotScene.dx, -pivotScene.dy, 0, 1); + _snapTargetMatrix = _matrixClamp(endMatrix); + + _snapController + ..removeListener(_animateSnap) + ..addListener(_animateSnap) + ..forward(from: 0.0).then((_) { + _snapTargetMatrix = null; + _checkAndNotifyAnimationEnd(); + }); + break; + } else { + if (details.scaleVelocity.abs() < 0.1) { + _currentAxis = null; + return; + } + final scale = _transformer.value.getMaxScaleOnAxis(); + final frictionSimulation = FrictionSimulation( + widget.interactionEndFrictionCoefficient * widget.scaleFactor, + scale, + details.scaleVelocity / 10, + ); + final tFinal = _getFinalTime( + details.scaleVelocity.abs(), + widget.interactionEndFrictionCoefficient, + effectivelyMotionless: 0.1, + ); + _scaleAnimation = Tween( + begin: scale, + end: frictionSimulation.x(tFinal), + ).animate(CurvedAnimation(parent: _scaleController, curve: Curves.decelerate)); + _scaleController.duration = Duration(milliseconds: (tFinal * 1000).round()); + _scaleAnimation!.addListener(_handleScaleAnimation); + _scaleController.forward(); + } + case _GestureType.rotate: + case null: + break; + } + } + + // (Removed: _handleScaleEndAnimation) + + // Handle mousewheel and web trackpad scroll events. + void _receivedPointerSignal(PointerSignalEvent event) { + final local = event.localPosition; + final global = event.position; + final double scaleChange; + if (event is PointerScrollEvent) { + if (event.kind == PointerDeviceKind.trackpad && !widget.trackpadScrollCausesScale) { + // Trackpad scroll, so treat it as a pan. + widget.onInteractionStart?.call(ScaleStartDetails(focalPoint: global, localFocalPoint: local)); + + final localDelta = PointerEvent.transformDeltaViaPositions( + untransformedEndPosition: global + event.scrollDelta, + untransformedDelta: event.scrollDelta, + transform: event.transform, + ); + + if (!_gestureIsSupported(_GestureType.pan)) { + widget.onInteractionUpdate?.call( + ScaleUpdateDetails( + focalPoint: global - event.scrollDelta, + localFocalPoint: local - event.scrollDelta, + focalPointDelta: -localDelta, + ), + ); + widget.onInteractionEnd?.call(ScaleEndDetails()); + return; + } + + final focalPointScene = _transformer.toScene(local); + final newFocalPointScene = _transformer.toScene(local - localDelta); + + _transformer.value = _matrixClamp(_matrixTranslate(_transformer.value, newFocalPointScene - focalPointScene)); + + widget.onInteractionUpdate?.call( + ScaleUpdateDetails( + focalPoint: global - event.scrollDelta, + localFocalPoint: local - localDelta, + focalPointDelta: -localDelta, + ), + ); + widget.onInteractionEnd?.call(ScaleEndDetails()); + return; + } + + // We can handle mouse-wheel event here for our own purposes + if (widget.onWheelDelta != null) { + widget.onWheelDelta!(event); + return; + } + + // Ignore left and right mouse wheel scroll. + if (event.scrollDelta.dy == 0.0) { + return; + } + scaleChange = math.exp(-event.scrollDelta.dy / widget.scaleFactor); + } else if (event is PointerScaleEvent) { + scaleChange = event.scale; + } else { + return; + } + widget.onInteractionStart?.call(ScaleStartDetails(focalPoint: global, localFocalPoint: local)); + + if (!_gestureIsSupported(_GestureType.scale)) { + widget.onInteractionUpdate?.call( + ScaleUpdateDetails(focalPoint: global, localFocalPoint: local, scale: scaleChange), + ); + widget.onInteractionEnd?.call(ScaleEndDetails()); + return; + } + + final focalPointScene = _transformer.toScene(local); + _transformer.value = _matrixScale(_transformer.value, scaleChange); + + // After scaling, translate such that the event's position is at the + // same scene point before and after the scale. + final focalPointSceneScaled = _transformer.toScene(local); + _transformer.value = _matrixClamp(_matrixTranslate(_transformer.value, focalPointSceneScaled - focalPointScene)); + + widget.onInteractionUpdate?.call( + ScaleUpdateDetails(focalPoint: global, localFocalPoint: local, scale: scaleChange), + ); + widget.onInteractionEnd?.call(ScaleEndDetails()); + } + + void _handleInertiaAnimation() { + if (!_controller.isAnimating) { + if (widget.scrollPhysics != null) { + _controller.removeListener(_handleInertiaAnimation); + } else { + _animation?.removeListener(_handleInertiaAnimation); + _animation = null; + } + _currentAxis = null; + _controller.reset(); + _checkAndNotifyAnimationEnd(); + return; + } + // Translate such that the resulting translation is _animation.value. + final translationVector = _transformer.value.getTranslation(); + final translation = Offset(translationVector.x, translationVector.y); + final translationScene = _transformer.toScene(translation); + + Offset newTranslationVector; + if (widget.scrollPhysics != null) { + /// When using scrollPhysics, we apply a simulation rather than an animation to the offsets + final t = _controller.lastElapsedDuration!.inMilliseconds / 1000.0; + final simulationOffsetX = simulationX != null ? -simulationX!.x(t) : translationVector.x; + final simulationOffsetY = simulationY != null ? -simulationY!.x(t) : translationVector.y; + newTranslationVector = Offset(simulationOffsetX, simulationOffsetY); + } else { + // Translate such that the resulting translation is _animation.value. + newTranslationVector = _animation!.value; + } + + // Apply the translation + final newTranslationScene = _transformer.toScene(newTranslationVector); + final translationChangeScene = newTranslationScene - translationScene; + _transformer.value = _matrixTranslate(_transformer.value, translationChangeScene); + } + + /// ScrollPhysics helpers + /// ChildSize is the size of the child (without the boundary margin). + Size _childSize() { + final childRenderBox = _childKey.currentContext!.findRenderObject()! as RenderBox; + final childSize = childRenderBox.size; + return childSize; + } + + /// These are the boundaries for constructing a ScrollMetrics object. + Rect _computePanBoundaries({ + required Size viewportSize, + required double scale, + EdgeInsets? boundaryMargin, + bool overrideAutoAdjustBoundaries = false, + }) { + final baseMargin = + (overrideAutoAdjustBoundaries && !widget.scrollPhysicsAutoAdjustBoundaries) || boundaryMargin == null + ? _originalBoundaryMargin + : boundaryMargin; + + // If boundaries are infinite, provide very large finite extents to disable clamping + if (_boundaryRect.isInfinite) { + return const Rect.fromLTRB(-double.maxFinite, -double.maxFinite, double.maxFinite, double.maxFinite); + } + + // Compute the raw boundary rect - use boundaryProvider if provided, otherwise use baseMargin + final baseBoundaryRect = widget.boundaryProvider != null + ? widget.boundaryProvider!(_viewport, _childSize()) + : baseMargin.inflateRect(Offset.zero & _childSize()); + + // If boundaryProvider returned null, fall back to baseMargin + final effectiveBoundaryRect = baseBoundaryRect ?? baseMargin.inflateRect(Offset.zero & _childSize()); + final effectiveWidth = effectiveBoundaryRect.width * scale; + final effectiveHeight = effectiveBoundaryRect.height * scale; + + final extraWidth = effectiveWidth - viewportSize.width; + final extraHeight = effectiveHeight - viewportSize.height; + + // Always center when content is smaller than viewport, using a small tolerance for floating imprecision. + // Use a larger tolerance (1.0px) for boundaryProvider because: + // - Dynamic boundary rects go through more transformations (layout, spacing, margins, scale) + // - Floating-point errors accumulate through these operations + // - Sub-pixel differences create janky scrolling when content nearly fills viewport + // - 1px is imperceptible to users but prevents false "content overflows" detection + final kOverflowTolerance = widget.boundaryProvider != null ? 1.0 : 0.1; // logical pixels + final extraBoundaryHorizontal = extraWidth < -kOverflowTolerance ? (extraWidth.abs() / 2) : 0.0; + final extraBoundaryVertical = extraHeight < -kOverflowTolerance ? (extraHeight.abs() / 2) : 0.0; + + // Calculate pan boundaries differently based on whether boundaryProvider is used + final double minX, maxX, minY, maxY; + + if (widget.boundaryProvider != null) { + // boundaryProvider: rect represents absolute document region to keep visible + // We need to calculate boundaries relative to the entire child (document) size + // NOTE: Return NEGATIVE values to match the convention used by _matrixClamp + + // When content is smaller than or nearly equal to viewport, center the region + // Use larger tolerance to lock cross-axis when content almost fills viewport + if (extraWidth < kOverflowTolerance) { + // Center horizontally: position so the region center aligns with viewport center + final regionCenterX = effectiveBoundaryRect.center.dx * scale; + final viewportCenterX = viewportSize.width / 2; + final centerTranslation = viewportCenterX - regionCenterX; + minX = -centerTranslation; // Negate for convention + maxX = -centerTranslation; // Negate for convention + } else { + // Content is larger: allow panning to see all of the region + // Calculate actual translations (before negating for convention) + final rightEdgeTranslation = viewportSize.width - effectiveBoundaryRect.right * scale; + final leftEdgeTranslation = -effectiveBoundaryRect.left * scale; + // After negating, the order swaps: more negative becomes more positive + // So what was min becomes max and vice versa + minX = -leftEdgeTranslation; // Negate and swap + maxX = -rightEdgeTranslation; // Negate and swap + } + + if (extraHeight < kOverflowTolerance) { + // Center vertically: position so the region center aligns with viewport center + final regionCenterY = effectiveBoundaryRect.center.dy * scale; + final viewportCenterY = viewportSize.height / 2; + final centerTranslation = viewportCenterY - regionCenterY; + minY = -centerTranslation; // Negate for convention + maxY = -centerTranslation; // Negate for convention + } else { + // Content is larger: allow panning to see all of the region + // Calculate actual translations (before negating for convention) + final bottomEdgeTranslation = viewportSize.height - effectiveBoundaryRect.bottom * scale; + final topEdgeTranslation = -effectiveBoundaryRect.top * scale; + // After negating, the order swaps: more negative becomes more positive + // So what was min becomes max and vice versa + minY = -topEdgeTranslation; // Negate and swap + maxY = -bottomEdgeTranslation; // Negate and swap + } + } else { + // baseMargin: use traditional EdgeInsets-based calculation + // When content is smaller than viewport, center it by making min==max + minX = -((baseMargin.left * scale + extraBoundaryHorizontal)); + maxX = extraWidth < -kOverflowTolerance + ? minX // Force centering + : -((baseMargin.left * scale - extraBoundaryHorizontal)) + extraWidth; + minY = -((baseMargin.top * scale + extraBoundaryVertical)); + maxY = extraHeight < -kOverflowTolerance + ? minY // Force centering + : -((baseMargin.top * scale - extraBoundaryVertical)) + extraHeight; + } + + // Ensure bounds are valid (min <= max) to avoid scroll physics assertion errors + // This can happen due to floating point precision issues when content is centered + final safeMinX = math.min(minX, maxX); + final safeMaxX = math.max(minX, maxX); + final safeMinY = math.min(minY, maxY); + final safeMaxY = math.max(minY, maxY); + return Rect.fromLTRB(safeMinX, safeMinY, safeMaxX, safeMaxY).round10BitFrac(); + } + + // Normalize ScrollMetrics such that minScrollExtent = 0 and pixels shift accordingly. + // ScrollPhysics.shouldAcceptUserOffset() does not work where minScrollExtent and pixels + // are both the same value but not 0.0. + ScrollMetrics _normalizeScrollMetrics(ScrollMetrics scrollMetrics) { + var range = scrollMetrics.maxScrollExtent - scrollMetrics.minScrollExtent; + var pixels = scrollMetrics.pixels - scrollMetrics.minScrollExtent; + + // Define a small tolerance around zero to ignore tiny drifts + const kTolerance = 0.01; + range = range.abs() < kTolerance ? 0.0 : range; + pixels = pixels.abs() < kTolerance ? 0.0 : pixels; + + return FixedScrollMetrics( + pixels: pixels, + minScrollExtent: 0.0, + maxScrollExtent: range < 0.0 ? 0.0 : range, + viewportDimension: scrollMetrics.viewportDimension, + axisDirection: scrollMetrics.axisDirection, + devicePixelRatio: scrollMetrics.devicePixelRatio, + ); + } + + /// Creates a synthetic ScrollMetrics objects for the + /// InteractiveViewer so that we can use ScrollPhysics + ScrollMetrics _calculateScrollMetrics(double pixels, AxisDirection axisDirection) { + final panBoundaries = _computePanBoundaries( + viewportSize: _viewport.size, + scale: _transformer.value.getMaxScaleOnAxis(), + boundaryMargin: widget.boundaryMargin, + ); + + final axis = switch (axisDirection) { + AxisDirection.left => Axis.horizontal, + AxisDirection.right => Axis.horizontal, + AxisDirection.up => Axis.vertical, + AxisDirection.down => Axis.vertical, + }; + + final minX = panBoundaries.left; + final maxX = panBoundaries.right; + final minY = panBoundaries.top; + final maxY = panBoundaries.bottom; + + final scrollMetrics = FixedScrollMetrics( + pixels: pixels, + minScrollExtent: axis == Axis.horizontal ? minX : minY, + maxScrollExtent: axis == Axis.horizontal ? maxX : maxY, + viewportDimension: axis == Axis.horizontal ? _viewport.width : _viewport.height, + axisDirection: axisDirection, + devicePixelRatio: MediaQuery.of(context).devicePixelRatio, + ); + return scrollMetrics; + } + + /// ScrollPhysics + /// Clamp the given full transform matrix to the content boundaries by + /// directly clamping its translation component. + Matrix4 _matrixClamp(Matrix4 matrix) { + final totalTranslation = _getMatrixTranslation(matrix); + final scale = matrix.getMaxScaleOnAxis(); + final viewSize = _viewport.size; + final panBoundaries = _computePanBoundaries( + viewportSize: viewSize, + scale: scale, + boundaryMargin: widget.boundaryMargin, + overrideAutoAdjustBoundaries: false, // Use adjusted boundaries to respect centering logic + ); + + // Ensure bounds are ordered correctly for clamp. + final minX = math.min(-panBoundaries.left, -panBoundaries.right); + final maxX = math.max(-panBoundaries.left, -panBoundaries.right); + final minY = math.min(-panBoundaries.top, -panBoundaries.bottom); + final maxY = math.max(-panBoundaries.top, -panBoundaries.bottom); + final clampedX = totalTranslation.dx.clamp(minX, maxX); + final clampedY = totalTranslation.dy.clamp(minY, maxY); + + return matrix.clone()..setTranslation(Vector3(clampedX, clampedY, 0.0)); + } + + /// Animate snap-back by interpolating scale and translation in scene-space. + void _animateSnap() { + if (_snapTargetMatrix == null) { + return; + } + final t = Curves.ease.transform(_snapController.value); + final lerped = Matrix4Tween(begin: _snapStartMatrix, end: _snapTargetMatrix!).transform(t); + _transformer.value = lerped; + } + + /// Determines whether [proposedScale] can be applied without clamping, + /// by probing the widget.scrollPhysics. + double _getAllowedScale(double proposedScale) { + final scrollPhysics = widget.scrollPhysicsScale ?? widget.scrollPhysics; + if (scrollPhysics == null) { + return proposedScale.clamp(widget.minScale, widget.maxScale); + } + + final contentSize = _boundaryRect.isInfinite ? _childSize() : _boundaryRect.size; + + final currentScale = _transformer.value.getMaxScaleOnAxis(); + final contentWidth = contentSize.width * currentScale; + final desiredContentWidth = contentSize.width * proposedScale; + final contentHeight = contentSize.height * currentScale; + final desiredContentHeight = contentSize.height * proposedScale; + + final ScrollMetrics metricsX = FixedScrollMetrics( + pixels: contentWidth, + minScrollExtent: contentSize.width * widget.minScale, + maxScrollExtent: contentSize.width * widget.maxScale, + viewportDimension: _viewport.width, + axisDirection: AxisDirection.right, + devicePixelRatio: 1.0, + ); + final ScrollMetrics metricsY = FixedScrollMetrics( + pixels: contentHeight, + minScrollExtent: contentSize.height * widget.minScale, + maxScrollExtent: contentSize.height * widget.maxScale, + viewportDimension: _viewport.height, + axisDirection: AxisDirection.down, + devicePixelRatio: 1.0, + ); + + final adjustmentX = scrollPhysics.applyBoundaryConditions(metricsX, desiredContentWidth); + final adjustmentY = scrollPhysics.applyBoundaryConditions(metricsY, desiredContentHeight); + + if (adjustmentX == 0.0 && adjustmentY == 0.0) { + // No adjustment needed, so the proposed scale is allowed. + return proposedScale; + } else { + final allowedContentWidth = desiredContentWidth - adjustmentX; + final allowedContentHeight = desiredContentHeight - adjustmentY; + + if (proposedScale > widget.maxScale) { + return math.max(allowedContentWidth / contentWidth, allowedContentHeight / contentHeight); + } else { + return math.max(allowedContentWidth / contentWidth, allowedContentHeight / contentHeight); + } + } + } + + Simulation? _getCombinedSimulation(Simulation? simulationX, Simulation? simulationY) { + if (simulationX == null && simulationY == null) { + return null; + } + if (simulationX == null) { + return simulationY; + } + if (simulationY == null) { + return simulationX; + } + return CombinedSimulation(simulationX: simulationX, simulationY: simulationY); + } + + void _onPointerDown(PointerDownEvent event) { + if (widget.scrollPhysics != null && _controller.isAnimating) { + // ability to stop a in-progress pan fling is particularly important + // when scroll physics is enabled as the duration and distance of the + // pan can be considerable. + stopAllAnimations(); + } + } + + /// Check if any animations are currently active + bool get hasActiveAnimations => + _controller.isAnimating || _scaleController.isAnimating || _snapController.isAnimating; + + /// Check if all animations have completed and call onAnimationEnd if needed + void _checkAndNotifyAnimationEnd() { + if (!hasActiveAnimations) { + widget.onAnimationEnd?.call(); + } + } + + /// Stop all active animations without saving state + void stopAllAnimations() { + // Stop pan animations + if (_controller.isAnimating) { + _controller.stop(); + _controller.reset(); + _animation?.removeListener(_handleInertiaAnimation); + _animation = null; + } + + // Stop scale animations + if (_scaleController.isAnimating) { + _scaleController.stop(); + _scaleController.reset(); + _scaleAnimation?.removeListener(_handleScaleAnimation); + _scaleAnimation = null; + } + + // Stop snap animations + if (_snapController.isAnimating) { + _snapController.stop(); + _snapController.reset(); + _snapTargetMatrix = null; + } + + // Clear simulations + simulationX = null; + simulationY = null; + combinedSimulation = null; + } + + // end ScrollPhysics + + // Handle inertia scale animation. + void _handleScaleAnimation() { + if (!_scaleController.isAnimating) { + _currentAxis = null; + _scaleAnimation?.removeListener(_handleScaleAnimation); + _scaleAnimation = null; + _scaleController.reset(); + _checkAndNotifyAnimationEnd(); + return; + } + final desiredScale = _scaleAnimation!.value; + final scaleChange = desiredScale / _transformer.value.getMaxScaleOnAxis(); + final referenceFocalPoint = _transformer.toScene(_scaleAnimationFocalPoint); + _transformer.value = _matrixScale(_transformer.value, scaleChange); + + // While scaling, translate such that the user's two fingers stay on + // the same places in the scene. That means that the focal point of + // the scale should be on the same place in the scene before and after + // the scale. + final focalPointSceneScaled = _transformer.toScene(_scaleAnimationFocalPoint); + _transformer.value = _matrixTranslate(_transformer.value, focalPointSceneScaled - referenceFocalPoint); + } + + void _handleTransformation() { + // A change to the TransformationController's value is a change to the + // state. + setState(() {}); + } + + @override + void initState() { + super.initState(); + _controller = AnimationController(vsync: this); + _scaleController = AnimationController(vsync: this); + _snapController = AnimationController(vsync: this, duration: const Duration(milliseconds: 250)); + + _transformer.addListener(_handleTransformation); + } + + @override + void didUpdateWidget(InteractiveViewer oldWidget) { + super.didUpdateWidget(oldWidget); + + final newController = widget.transformationController; + if (newController == oldWidget.transformationController) { + return; + } + _transformer.removeListener(_handleTransformation); + if (oldWidget.transformationController == null) { + _transformer.dispose(); + } + _transformer = newController ?? TransformationController(); + _transformer.addListener(_handleTransformation); + } + + @override + void dispose() { + _controller.dispose(); + _scaleController.dispose(); + _snapController.dispose(); + _transformer.removeListener(_handleTransformation); + if (widget.transformationController == null) { + _transformer.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + Widget child; + if (widget.child != null) { + child = _InteractiveViewerBuilt( + childKey: _childKey, + clipBehavior: widget.clipBehavior, + constrained: widget.constrained, + matrix: _transformer.value, + alignment: widget.alignment, + child: widget.child!, + ); + } else { + // When using InteractiveViewer.builder, then constrained is false and the + // viewport is the size of the constraints. + assert(widget.builder != null); + assert(!widget.constrained); + child = LayoutBuilder( + builder: (context, constraints) { + final matrix = _transformer.value; + return _InteractiveViewerBuilt( + childKey: _childKey, + clipBehavior: widget.clipBehavior, + constrained: widget.constrained, + alignment: widget.alignment, + matrix: matrix, + child: widget.builder!(context, _transformViewport(matrix, Offset.zero & constraints.biggest)), + ); + }, + ); + } + + return Listener( + key: _parentKey, + onPointerSignal: _receivedPointerSignal, + onPointerDown: _onPointerDown, // ScrollPhysics + child: GestureDetector( + behavior: HitTestBehavior.opaque, + // Necessary when panning off screen. + onScaleEnd: _onScaleEnd, + onScaleStart: _onScaleStart, + onScaleUpdate: _onScaleUpdate, + trackpadScrollCausesScale: widget.trackpadScrollCausesScale, + trackpadScrollToScaleFactor: Offset(0, -1 / widget.scaleFactor), + child: child, + ), + ); + } +} + +// This widget allows us to easily swap in and out the LayoutBuilder in +// InteractiveViewer's depending on if it's using a builder or a child. +class _InteractiveViewerBuilt extends StatelessWidget { + const _InteractiveViewerBuilt({ + required this.child, + required this.childKey, + required this.clipBehavior, + required this.constrained, + required this.matrix, + required this.alignment, + }); + + final Widget child; + final GlobalKey childKey; + final Clip clipBehavior; + final bool constrained; + final Matrix4 matrix; + final Alignment? alignment; + + @override + Widget build(BuildContext context) { + Widget child = Transform( + transform: matrix, + alignment: alignment, + child: KeyedSubtree(key: childKey, child: this.child), + ); + + if (!constrained) { + child = OverflowBox( + alignment: Alignment.topLeft, + minWidth: 0.0, + minHeight: 0.0, + maxWidth: double.infinity, + maxHeight: double.infinity, + child: child, + ); + } + + return ClipRect(clipBehavior: clipBehavior, child: child); + } +} + +// A classification of relevant user gestures. Each contiguous user gesture is +// represented by exactly one _GestureType. +enum _GestureType { pan, scale, rotate } + +// Given a velocity and drag, calculate the time at which motion will come to +// a stop, within the margin of effectivelyMotionless. +double _getFinalTime(double velocity, double drag, {double effectivelyMotionless = 0.5}) { + return math.log(effectivelyMotionless / velocity) / math.log(drag / 100); +} + +// Return the translation from the given Matrix4 as an Offset. +Offset _getMatrixTranslation(Matrix4 matrix) { + final nextTranslation = matrix.getTranslation(); + return Offset(nextTranslation.x, nextTranslation.y); +} + +// Transform the four corners of the viewport by the inverse of the given +// matrix. This gives the viewport after the child has been transformed by the +// given matrix. The viewport transforms as the inverse of the child (i.e. +// moving the child left is equivalent to moving the viewport right). +Quad _transformViewport(Matrix4 matrix, Rect viewport) { + final inverseMatrix = matrix.clone()..invert(); + return Quad.points( + inverseMatrix.transform3(Vector3(viewport.topLeft.dx, viewport.topLeft.dy, 0.0)), + inverseMatrix.transform3(Vector3(viewport.topRight.dx, viewport.topRight.dy, 0.0)), + inverseMatrix.transform3(Vector3(viewport.bottomRight.dx, viewport.bottomRight.dy, 0.0)), + inverseMatrix.transform3(Vector3(viewport.bottomLeft.dx, viewport.bottomLeft.dy, 0.0)), + ); +} + +// Find the axis aligned bounding box for the rect rotated about its center by +// the given amount. +Quad _getAxisAlignedBoundingBoxWithRotation(Rect rect, double rotation) { + final rotationMatrix = Matrix4.identity() + ..translateByDouble(rect.size.width / 2, rect.size.height / 2, 0, 1) + ..rotateZ(rotation) + ..translateByDouble(-rect.size.width / 2, -rect.size.height / 2, 0, 1); + final boundariesRotated = Quad.points( + rotationMatrix.transform3(Vector3(rect.left, rect.top, 0.0)), + rotationMatrix.transform3(Vector3(rect.right, rect.top, 0.0)), + rotationMatrix.transform3(Vector3(rect.right, rect.bottom, 0.0)), + rotationMatrix.transform3(Vector3(rect.left, rect.bottom, 0.0)), + ); + return InteractiveViewer.getAxisAlignedBoundingBox(boundariesRotated); +} + +// Return the amount that viewport lies outside of boundary. If the viewport +// is completely contained within the boundary (inclusively), then returns +// Offset.zero. +Offset _exceedsBy(Quad boundary, Quad viewport) { + final viewportPoints = [viewport.point0, viewport.point1, viewport.point2, viewport.point3]; + var largestExcess = Offset.zero; + for (final point in viewportPoints) { + final pointInside = InteractiveViewer.getNearestPointInside(point, boundary); + final excess = Offset(pointInside.x - point.x, pointInside.y - point.y); + if (excess.dx.abs() > largestExcess.dx.abs()) { + largestExcess = Offset(excess.dx, largestExcess.dy); + } + if (excess.dy.abs() > largestExcess.dy.abs()) { + largestExcess = Offset(largestExcess.dx, excess.dy); + } + } + + return largestExcess.round10BitFrac(); +} + +// Align the given offset to the given axis by allowing movement only in the +// axis direction. +Offset _alignAxis(Offset offset, Axis axis) { + return switch (axis) { + Axis.horizontal => Offset(offset.dx, 0.0), + Axis.vertical => Offset(0.0, offset.dy), + }; +} + +// Given two points, return the axis where the distance between the points is +// greatest. If they are equal, return null. +Axis? _getPanAxis(Offset point1, Offset point2) { + if (point1 == point2) { + return null; + } + final x = point2.dx - point1.dx; + final y = point2.dy - point1.dy; + return x.abs() > y.abs() ? Axis.horizontal : Axis.vertical; +} + +/// A simulation that combines two one-dimensional simulations into one, +/// one for the x axis and one for the y axis. +class CombinedSimulation extends Simulation { + CombinedSimulation({required this.simulationX, required this.simulationY}); + final Simulation simulationX; + final Simulation simulationY; + + // For a combined simulation you don’t necessarily need to use x(t) directly. + // It is provided here so that animateWith() can drive a time value. + @override + double x(double time) => simulationX.x(time); + + // Returns the combined velocity magnitude of the two simulations. + @override + double dx(double time) { + final dxX = simulationX.dx(time); + final dxY = simulationY.dx(time); + return math.sqrt(dxX * dxX + dxY * dxY); + } + + @override + bool isDone(double time) { + return simulationX.isDone(time) && simulationY.isDone(time); + } +} + +extension _OffsetRounder on Offset { + /// Round the double to keep 10-bits of precision under the binary point. + Offset round10BitFrac() => Offset(dx.round10BitFrac(), dy.round10BitFrac()); +} + +extension _RectRounder on Rect { + /// Round the double to keep 10-bits of precision under the binary point. + Rect round10BitFrac() => + Rect.fromLTRB(left.round10BitFrac(), top.round10BitFrac(), right.round10BitFrac(), bottom.round10BitFrac()); +} diff --git a/lib/src/widgets/pdf_error_widget.dart b/packages/pdfrx/lib/src/widgets/internals/pdf_error_widget.dart similarity index 93% rename from lib/src/widgets/pdf_error_widget.dart rename to packages/pdfrx/lib/src/widgets/internals/pdf_error_widget.dart index 11844ba9..ae8b09f2 100644 --- a/lib/src/widgets/pdf_error_widget.dart +++ b/packages/pdfrx/lib/src/widgets/internals/pdf_error_widget.dart @@ -2,8 +2,8 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher_string.dart'; -import '../../pdfrx.dart'; -import '../utils/platform.dart'; +import '../../../pdfrx.dart'; +import '../../utils/platform.dart'; /// Show error widget when pdf viewer failed to load pdf. Widget pdfErrorWidget(BuildContext context, Object error, {StackTrace? stackTrace, bool bannerWarning = true}) { @@ -23,7 +23,10 @@ Widget pdfErrorWidget(BuildContext context, Object error, {StackTrace? stackTrac child: Icon(Icons.error, size: 50, color: Colors.yellow), alignment: PlaceholderAlignment.middle, ), - TextSpan(text: ' $error\n\n', style: const TextStyle(color: Colors.white)), + TextSpan( + text: ' $error\n\n', + style: const TextStyle(color: Colors.white), + ), if (stackTrace != null) TextSpan(text: stackTrace.toString(), style: const TextStyle(fontSize: 14)), if (error is PdfPasswordException && isWindows) diff --git a/packages/pdfrx/lib/src/widgets/internals/pdf_viewer_key_handler.dart b/packages/pdfrx/lib/src/widgets/internals/pdf_viewer_key_handler.dart new file mode 100644 index 00000000..c6994530 --- /dev/null +++ b/packages/pdfrx/lib/src/widgets/internals/pdf_viewer_key_handler.dart @@ -0,0 +1,60 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../../../pdfrx.dart'; + +/// A widget that handles key events for the PDF viewer. +class PdfViewerKeyHandler extends StatelessWidget { + const PdfViewerKeyHandler({ + required this.child, + required this.onKeyRepeat, + required this.params, + this.onFocusChange, + super.key, + }); + + /// Called on every key repeat. + /// + /// See [PdfViewerOnKeyCallback] for the parameters. + final bool Function(PdfViewerKeyHandlerParams, LogicalKeyboardKey, bool) onKeyRepeat; + final ValueChanged? onFocusChange; + final PdfViewerKeyHandlerParams params; + final Widget child; + + @override + Widget build(BuildContext context) { + final childBuilder = Builder( + builder: (context) { + final focusNode = Focus.maybeOf(context); + if (focusNode == null) { + return child; + } + return ListenableBuilder(listenable: focusNode, builder: (context, _) => child); + }, + ); + if (!params.enabled) { + return childBuilder; + } + + return Focus( + focusNode: params.focusNode, + parentNode: params.parentNode, + autofocus: params.autofocus, + canRequestFocus: params.canRequestFocus, + onFocusChange: onFocusChange, + onKeyEvent: (node, event) { + if (event is KeyDownEvent || event is KeyRepeatEvent) { + if (onKeyRepeat(params, event.logicalKey, event is KeyDownEvent)) { + return KeyEventResult.handled; + } + } else if (event is KeyUpEvent) { + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: childBuilder, + ); + } +} diff --git a/packages/pdfrx/lib/src/widgets/internals/widget_size_sniffer.dart b/packages/pdfrx/lib/src/widgets/internals/widget_size_sniffer.dart new file mode 100644 index 00000000..9939c958 --- /dev/null +++ b/packages/pdfrx/lib/src/widgets/internals/widget_size_sniffer.dart @@ -0,0 +1,56 @@ +// ignore_for_file: public_member_api_docs + +import 'dart:async'; + +import 'package:flutter/material.dart'; + +/// A widget that sniffs the size of its child and calls the callback when the size changes. +class WidgetSizeSniffer extends StatefulWidget { + const WidgetSizeSniffer({required this.child, this.onSizeChanged, super.key}); + + final Widget child; + final FutureOr Function(GlobalRect rect)? onSizeChanged; + + @override + State createState() => _WidgetSizeSnifferState(); +} + +class _WidgetSizeSnifferState extends State { + Rect? _rect; + + @override + Widget build(BuildContext context) { + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (!mounted) return; + final r = context.findRenderObject(); + if (r is! RenderBox) return; + final rect = Rect.fromPoints(r.localToGlobal(Offset.zero), r.localToGlobal(Offset(r.size.width, r.size.height))); + if (_rect != rect) { + _rect = rect; + await widget.onSizeChanged?.call(GlobalRect(_rect!)); + if (mounted) { + setState(() {}); + } + } + }); + return Offstage(offstage: _rect == null, child: widget.child); + } +} + +/// A class to hold the global rectangle and provide a method to convert it to local coordinates. +class GlobalRect { + const GlobalRect(this.globalRect); + + final Rect globalRect; + + Rect? toLocal(BuildContext context) { + final renderBox = context.findRenderObject(); + if (renderBox is RenderBox) { + return Rect.fromPoints( + renderBox.globalToLocal(globalRect.topLeft), + renderBox.globalToLocal(globalRect.bottomRight), + ); + } + return null; + } +} diff --git a/packages/pdfrx/lib/src/widgets/pdf_page_layout.dart b/packages/pdfrx/lib/src/widgets/pdf_page_layout.dart new file mode 100644 index 00000000..a86d4899 --- /dev/null +++ b/packages/pdfrx/lib/src/widgets/pdf_page_layout.dart @@ -0,0 +1,1078 @@ +// Copyright (c) 2024 Espresso Systems Inc. +// This file is part of pdfrx. + +import 'dart:math'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +import '../../pdfrx.dart'; + +/// Helper class to hold layout calculation results. +class LayoutResult { + LayoutResult({required this.pageLayouts, required this.documentSize}); + final List pageLayouts; + final Size documentSize; +} + +/// Helper class for viewport calculations with margins. +/// +/// Bundles viewport size with boundary and content margins, providing +/// convenient getters for calculating available space and inflating dimensions. +/// +/// ```dart +/// final helper = PdfLayoutHelper.fromParams(params, viewSize: viewSize); +/// +/// // Get available space for content +/// final width = helper.availableWidth; +/// final height = helper.availableHeight; +/// +/// // Add margins to content dimensions +/// final totalWidth = helper.widthWithMargins(contentWidth); +/// ``` +@immutable +class PdfLayoutHelper { + /// Creates a layout helper with specified parameters. + const PdfLayoutHelper({required this.viewSize, this.boundaryMargin, this.margin = 0.0}); + + /// Creates a layout helper from viewer parameters. + PdfLayoutHelper.fromParams(PdfViewerParams params, {required Size viewSize}) + : this(viewSize: viewSize, boundaryMargin: params.boundaryMargin, margin: params.margin); + + /// Viewport size. + final Size viewSize; + + /// Boundary margin around the viewport. + final EdgeInsets? boundaryMargin; + + /// Content margin around the document edges. + final double margin; + + /// Horizontal boundary margin + /// + /// 0 if infinite or null. + double get boundaryMarginHorizontal { + return boundaryMargin?.horizontal == double.infinity ? 0 : boundaryMargin?.horizontal ?? 0; + } + + /// Vertical boundary margin. + /// + /// 0 if infinite or null. + double get boundaryMarginVertical { + return boundaryMargin?.vertical == double.infinity ? 0 : boundaryMargin?.vertical ?? 0; + } + + /// Available width after subtracting boundary margins and content margins (`margin * 2`). + double get availableWidth { + return viewSize.width - boundaryMarginHorizontal - margin * 2; + } + + /// Available height after subtracting boundary margins and content margins (`margin * 2`). + double get availableHeight { + return viewSize.height - boundaryMarginVertical - margin * 2; + } + + /// Viewport width. + double get viewWidth => viewSize.width; + + /// Viewport height. + double get viewHeight => viewSize.height; + + /// Add horizontal boundary margin and content margins to a content width. + double getWidthWithMargins(double contentWidth) { + return contentWidth + boundaryMarginHorizontal + margin * 2; + } + + /// Add vertical boundary margin and content margins to a content height. + double getHeightWithMargins(double contentHeight) { + return contentHeight + boundaryMarginVertical + margin * 2; + } +} + +/// Base class for PDF page layouts. +/// +/// **Simple usage (backward compatible):** +/// Create instances directly with pre-computed page layouts: +/// ```dart +/// return PdfPageLayout( +/// pageLayouts: pageLayouts, // List of page positions +/// documentSize: Size(width, height), +/// ); +/// ``` +/// +/// **Advanced usage (subclassing):** +/// For dynamic layouts that respond to viewport changes or fit modes: +/// 1. Extend this class and override [layoutBuilder] for custom positioning logic +/// 2. Override [primaryAxis] if not vertical scrolling +/// 3. Override [calculateFitScale] for custom scaling logic +/// +/// **Document size and margin handling:** +/// Use [PdfLayoutHelper.getWidthWithMargins] and [PdfLayoutHelper.getHeightWithMargins] to properly +/// include both boundary margins and content margins in your document size calculations. +/// The helper handles the complexity of margin application so you don't have to. +/// +/// **Scaling considerations:** +/// - [calculateFitScale] returns the scale based on [FitMode] strategy +/// - This is typically used as the minimum scale for the InteractiveViewer, +/// unless an explicit [PdfViewerParams.minScale] parameter is set +/// - Override only if default implementation doesn't fit your layout's needs +class PdfPageLayout { + PdfPageLayout({required this.pageLayouts, required this.documentSize}) + : primaryAxis = documentSize.height >= documentSize.width ? Axis.vertical : Axis.horizontal { + _impliedMargin = _calcImpliedMargin(); + } + + /// Page rectangles positioned within the document coordinate space. + /// + /// Each rect represents a page's position and size. The rects include positioning + /// with spacing between pages, but the rect dimensions themselves are + /// the page sizes WITHOUT margins added to width/height. + final List pageLayouts; + + /// Total document size including content margins. + /// + /// This is the size of the scrollable content area and includes [_impliedMargin] spacing + /// on all sides. Does NOT include boundary margins - those are handled separately + /// at the viewport level. + final Size documentSize; + + /// The primary scroll axis for this layout. + /// + /// In the base class, derived from document dimensions: [Axis.vertical] if + /// height >= width, otherwise [Axis.horizontal]. + /// + /// Subclasses can override this to use explicit scroll direction instead. + /// For example, [SequentialPagesLayout] overrides to use its scroll direction parameter. + /// + /// Determines the direction of scrolling and page layout. + final Axis primaryAxis; + + /// Content margin around the document edges. + /// + /// Calculated automatically via [_calcImpliedMargin] based on the spacing + /// between page dimensions and document size. + late double _impliedMargin; + + /// Get the spread bounds for a given page number. + /// + /// For single page layouts, this simply returns the page bounds. + Rect getSpreadBounds(int pageNumber, {bool withMargins = false}) { + if (pageNumber < 1 || pageNumber > pageLayouts.length) { + throw RangeError('Invalid page number $pageNumber'); + } + return pageLayouts[pageNumber - 1].inflate(withMargins ? _impliedMargin : 0); + } + + /// Each layout implements its own calculation logic. + /// Optional [helper] can be used for fit mode calculations. + /// + /// The default implementation returns the existing layout, which supports + /// backward compatibility with pre-computed layouts. + LayoutResult layoutBuilder(List pages, PdfViewerParams params, {PdfLayoutHelper? helper}) { + return LayoutResult(pageLayouts: pageLayouts, documentSize: documentSize); + } + + /// Gets the maximum width across all layout units. + /// For single page layouts, this is the maximum page width. + /// For spread layouts, this can be overridden to return the maximum spread width. + double getMaxWidth({bool withMargins = false}) => + pageLayouts.fold(0.0, (maximum, rect) => max(maximum, rect.width)) + (withMargins ? _impliedMargin * 2 : 0); + + /// Gets the maximum height across all layout units. + /// For single page layouts, this is the maximum page height. + /// For spread layouts, this can be overridden to return the maximum spread height. + double getMaxHeight({bool withMargins = false}) => + pageLayouts.fold(0.0, (maximum, rect) => max(maximum, rect.height)) + (withMargins ? _impliedMargin * 2 : 0); + + /// Calculates the implied margin based on document size and max page dimensions. + /// This assumes pages are centered within the document size. + double _calcImpliedMargin() => primaryAxis == Axis.vertical + ? (documentSize.width - getMaxWidth()) / 2 + : (documentSize.height - getMaxHeight()) / 2; + + double get impliedMargin => _impliedMargin; + + bool _isPageNumberValid(int? pageNumber) { + return pageNumber != null && pageNumber >= 1 && pageNumber <= pageLayouts.length; + } + + /// Calculates page sizes based on fit mode and scroll direction, to enable independent + /// page scaling for documents with mixed page sizes to provide optimal viewing experience. + /// + /// **Scaling behavior:** + /// For each page, calculates a scale such that the page PLUS margins fits the viewport: + /// `scale = viewport / (pageSize + boundaryMargin + margin*2)` + /// + /// Returns the scaled page sizes WITHOUT margins included in the dimensions: + /// `scaledPageSize = pageSize * scale` + /// + /// The margins should then applied separately during layout positioning - for example + /// see [layoutSequentialPages]. + /// + /// **Normalization:** + /// For FitMode.fit and FitMode.fill with multiple pages, cross-axis dimensions are normalized + /// to ensure consistent visual spacing when pages have different aspect ratios. + /// + /// **Use this when:** You want standard PDF scaling behavior but custom positioning logic. + /// + /// **Example:** + /// ```dart + /// final sizes = calculatePageSizes( + /// pages: pages, + /// fitMode: FitMode.fill, + /// scrollAxis: Axis.vertical, + /// helper: helper, // Provides viewport and margin info + /// ); + /// // sizes[i] is the scaled page size (without margins included in the dimensions) + /// // Then use sizes for custom layout positioning, adding margins as needed + /// ``` + /// + /// See also: [calculateFitScale] for document-level scaling + static List calculatePageSizes({ + required List pages, + required FitMode fitMode, + required Axis scrollAxis, + required PdfLayoutHelper helper, + }) { + if (fitMode == FitMode.none || fitMode == FitMode.cover) { + // No scaling, use pdf document dimensions + return pages.map((page) => Size(page.width, page.height)).toList(); + } + + // Calculate initial scales for each page independently + final scales = pages.map((page) { + return calculateFitScaleForDimensions( + width: page.width, + height: page.height, + helper: helper, + mode: fitMode, + scrollAxis: scrollAxis, + ); + }).toList(); + + // For FitMode.fill and FitMode.fit, normalize scales to ensure uniform cross-axis dimensions + if ((fitMode == FitMode.fill || fitMode == FitMode.fit) && scales.length > 1) { + final normalizedSizes = _normalizePageSizes( + pages: pages, + scales: scales, + scrollAxis: scrollAxis, + fitMode: fitMode, + helper: helper, + ); + if (normalizedSizes != null) return normalizedSizes; + } + + // Apply calculated scales + return List.generate(pages.length, (i) => Size(pages[i].width * scales[i], pages[i].height * scales[i])); + } + + /// Normalizes page sizes to ensure uniform cross-axis dimensions for constrained pages. + /// Returns null if normalization is not needed or not applicable. + /// + /// For documents with mixed page sizes, independent page scaling results in varying margins + /// so we normalize cross-axis dimensions to be the same for constrained pages, so that the + /// margins are consistent between pages. + static List? _normalizePageSizes({ + required List pages, + required List scales, + required Axis scrollAxis, + required FitMode fitMode, + required PdfLayoutHelper helper, + }) { + // Find pages that are constrained by the cross-axis (not primary axis) + final crossAxisConstrainedPages = []; + + for (var i = 0; i < pages.length; i++) { + final page = pages[i]; + final widthWithMargins = helper.getWidthWithMargins(page.width); + final heightWithMargins = helper.getHeightWithMargins(page.height); + + final crossAxisScale = scrollAxis == Axis.vertical + ? helper.viewWidth / widthWithMargins + : helper.viewHeight / heightWithMargins; + final primaryAxisScale = scrollAxis == Axis.vertical + ? helper.viewHeight / heightWithMargins + : helper.viewWidth / widthWithMargins; + + // For fill mode: always constrained by cross-axis + // For fit mode: only if cross-axis is the limiting factor + if (fitMode == FitMode.fill || crossAxisScale <= primaryAxisScale) { + crossAxisConstrainedPages.add(i); + } + } + + if (crossAxisConstrainedPages.isEmpty) return null; + + // Calculate cross-axis sizes for constrained pages + final constrainedCrossAxisSizes = crossAxisConstrainedPages.map((i) { + final page = pages[i]; + return scrollAxis == Axis.vertical ? page.width * scales[i] : page.height * scales[i]; + }).toList(); + + final minSize = constrainedCrossAxisSizes.reduce(min); + final maxSize = constrainedCrossAxisSizes.reduce(max); + + // Only normalize if sizes differ by more than 0.5% + const similarityThreshold = 0.005; + if ((maxSize - minSize) / minSize <= similarityThreshold) return null; + + // Determine target cross-axis size + var targetSize = maxSize; + + // For fit mode, ensure no page exceeds its original fit scale + if (fitMode == FitMode.fit) { + for (var i in crossAxisConstrainedPages) { + final page = pages[i]; + final pageCrossAxisSize = scrollAxis == Axis.vertical ? page.width : page.height; + final requiredScale = targetSize / pageCrossAxisSize; + + if (requiredScale > scales[i]) { + targetSize = pageCrossAxisSize * scales[i]; + } + } + } + + // Apply normalization + return pages.asMap().entries.map((entry) { + final i = entry.key; + final page = entry.value; + + if (crossAxisConstrainedPages.contains(i)) { + // Normalize to target cross-axis size + final pageCrossAxisSize = scrollAxis == Axis.vertical ? page.width : page.height; + final newScale = targetSize / pageCrossAxisSize; + return Size(page.width * newScale, page.height * newScale); + } else { + // Keep original scale for primary-axis constrained pages + return Size(page.width * scales[i], page.height * scales[i]); + } + }).toList(); + } + + /// Calculate the scale factor for given dimensions based on fit mode. + /// This is the core logic used by both [calculatePageSizes] and [calculateFitScale]. + /// + /// **Important:** Margins are in PDF points and scale with content. The calculation accounts + /// for this by dividing viewport by (width + margin*2), ensuring the scaled page + scaled margins + /// fit within the viewport. + /// + /// **Note:** [FitMode.cover] is not supported here as it requires document-level dimensions. + /// Use [calculateFitScale] instead for cover mode. + static double calculateFitScaleForDimensions({ + required double width, + required double height, + required PdfLayoutHelper helper, + required FitMode mode, + required Axis scrollAxis, + }) { + assert(mode != FitMode.cover, 'FitMode.cover requires document-level calculation. Use calculateFitScale instead.'); + + // Both margins and boundaryMargins are in PDF points and scale with the content + // So we need: scale = viewport / (pageSize + margin*2 + boundaryMargin) + // This ensures: (pageSize + margin*2 + boundaryMargin) * scale = viewport + final widthWithMargins = helper.getWidthWithMargins(width); + final heightWithMargins = helper.getHeightWithMargins(height); + + switch (mode) { + case FitMode.fit: + // Scale to fit viewport (letterbox) - content + all margins must fit + return min(helper.viewWidth / widthWithMargins, helper.viewHeight / heightWithMargins); + + case FitMode.fill: + // Scale to fill cross-axis - content + all margins on cross-axis must fit + return scrollAxis == Axis.vertical + ? helper.viewWidth / widthWithMargins + : helper.viewHeight / heightWithMargins; + + case FitMode.cover: + // Cover mode not supported at page level - requires full document dimensions + // This should be caught by the assertion above + throw UnsupportedError('FitMode.cover requires document-level calculation'); + + case FitMode.none: + return 1.0; + } + } + + /// Positions pre-sized pages sequentially along a scroll axis. + /// + /// This is a building block for creating simple scrolling layouts. It handles the + /// geometry of positioning pages one after another, with optional centering + /// perpendicular to the scroll direction. + /// + /// **Use this when:** You have pre-calculated page sizes and need to position them + /// sequentially (vertical or horizontal scrolling). + /// + /// **Parameters:** + /// - [pageSizes]: Pre-calculated size for each page + /// - [scrollAxis]: Direction of scrolling (vertical or horizontal) + /// - [margin]: Margin around pages + /// - [centerPerpendicular]: Whether to center pages perpendicular to scroll axis + /// + /// **Example:** + /// ```dart + /// final sizes = pages.map((p) => Size(p.width, p.height)).toList(); + /// return layoutSequentialPages( + /// pageSizes: sizes, + /// scrollAxis: Axis.vertical, + /// margin: 8.0, + /// centerPerpendicular: true, + /// ); + /// ``` + LayoutResult layoutSequentialPages({ + required List pageSizes, + required Axis scrollAxis, + required double margin, + bool centerPerpendicular = false, + }) { + final isVertical = scrollAxis == Axis.vertical; + final pageLayouts = []; + var scrollPosition = margin; + var maxCrossAxis = 0.0; + + // Track max cross-axis dimension (needed for centering and document size) + for (var size in pageSizes) { + maxCrossAxis = max(maxCrossAxis, isVertical ? size.width : size.height); + } + + // Layout pages along scroll axis + for (var size in pageSizes) { + final rect = isVertical + ? Rect.fromLTWH(margin, scrollPosition, size.width, size.height) + : Rect.fromLTWH(scrollPosition, margin, size.width, size.height); + pageLayouts.add(rect); + scrollPosition += (isVertical ? size.height : size.width) + margin; + } + + // Center perpendicular to scroll if requested + final finalLayouts = centerPerpendicular + ? pageLayouts.map((rect) { + if (isVertical) { + final xOffset = (maxCrossAxis - rect.width) / 2; + return rect.translate(xOffset, 0); + } else { + final yOffset = (maxCrossAxis - rect.height) / 2; + return rect.translate(0, yOffset); + } + }).toList() + : pageLayouts; + + final docSize = isVertical + ? Size(maxCrossAxis + margin * 2, scrollPosition) + : Size(scrollPosition, maxCrossAxis + margin * 2); + + return LayoutResult(pageLayouts: finalLayouts, documentSize: docSize); + } + + /// Calculates the scale to display content according to the [FitMode] strategy. + /// + /// This value determines how content should be scaled based on the fit mode and is + /// typically used as the minimum scale for the InteractiveViewer (though an explicit + /// [PdfViewerParams.minScale] parameter may override this). + /// + /// **Calculation:** + /// Uses the maximum page dimensions from [getMaxWidth] and [getMaxHeight], then calculates: + /// `scale = viewport / (maxPageDimension + boundaryMargin + margin*2)` + /// + /// This ensures the largest page plus all margins fits within the viewport when scaled. + /// + /// **Return value behavior by mode:** + /// - [FitMode.fit]: Scale to fit largest page + margins within viewport (both axes) + /// - [FitMode.fill]: Scale to fill viewport on cross-axis (width for vertical scroll) + /// - [FitMode.cover]: Scale to fill viewport on largest axis (may crop content) + /// - [FitMode.none]: Returns 1.0 (no scaling) + /// + /// **Custom layouts:** + /// Override this method for custom scaling logic, or override [getMaxWidth] and [getMaxHeight] + /// to change the dimensions used (e.g., spread width instead of page width). + /// + /// See also: [calculatePageSizes] for page-level scaling + double calculateFitScale( + PdfLayoutHelper helper, + FitMode mode, { + PageTransition pageTransition = PageTransition.continuous, + int? pageNumber, + }) { + // FitMode.cover is special - it uses different logic + if (mode == FitMode.cover) { + // In discrete mode, calculate cover scale for the specific page + // In continuous mode, use document dimensions to match legacy _coverScale calculation + final double width; + final double height; + + if (pageTransition == PageTransition.discrete && _isPageNumberValid(pageNumber)) { + final dimensions = getSpreadBounds(pageNumber!).size; + width = dimensions.width; + height = dimensions.height; + // For discrete, add margins since page dimensions don't include them + final widthWithMargins = width + helper.margin * 2 + helper.boundaryMarginHorizontal; + final heightWithMargins = height + helper.margin * 2 + helper.boundaryMarginVertical; + return max(helper.viewWidth / widthWithMargins, helper.viewHeight / heightWithMargins); + } else { + // Continuous mode uses document dimensions (which already include margin * 2) + // This matches legacy _coverScale calculation: viewport / (documentSize + boundaryMargin) + width = documentSize.width; + height = documentSize.height; + final widthWithMargins = width + helper.boundaryMarginHorizontal; + final heightWithMargins = height + helper.boundaryMarginVertical; + return max(helper.viewWidth / widthWithMargins, helper.viewHeight / heightWithMargins); + } + } + + // For discrete mode with a specific page, calculate scale for that page + // Otherwise, use the maximum dimensions across all pages + final double width; + final double height; + + if (pageTransition == PageTransition.discrete && _isPageNumberValid(pageNumber)) { + final dimensions = getSpreadBounds(pageNumber!).size; + width = dimensions.width; + height = dimensions.height; + } else { + width = getMaxWidth(); + height = getMaxHeight(); + } + + // Use the core calculation logic for fit/fill/none + return calculateFitScaleForDimensions( + width: width, + height: height, + helper: helper, + mode: mode, + scrollAxis: primaryAxis, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! PdfPageLayout) return false; + // Use runtimeType to ensure subclasses with additional fields aren't considered equal + if (runtimeType != other.runtimeType) return false; + return listEquals(pageLayouts, other.pageLayouts) && documentSize == other.documentSize; + } + + @override + int get hashCode => Object.hash(runtimeType, Object.hashAll(pageLayouts), documentSize); +} + +/// Spread-aware layout base class. +/// +/// This class extends [PdfPageLayout] to support layouts that group multiple pages +/// into "spreads" (e.g., facing pages). It provides spread-specific functionality +/// while maintaining compatibility with the base page layout system. +/// +/// **Key concepts:** +/// - A "spread" is a layout unit that may contain one or more pages +/// - [spreadLayouts] contains the bounds of each spread (indexed by spread index, 0-based) +/// - [pageToSpreadIndex] maps page numbers (1-based) to spread indices (0-based) +/// +/// **Example: Facing pages layout** +/// - Page 1 (cover): spread 0 +/// - Pages 2-3: spread 1 +/// - Pages 4-5: spread 2 +/// - `pageToSpreadIndex = [0, 1, 1, 2, 2, ...]` (0-based: index 0 = page 1) +/// - `spreadLayouts = [Rect(cover bounds), Rect(pages 2-3 bounds), Rect(pages 4-5 bounds), ...]` +class PdfSpreadLayout extends PdfPageLayout { + PdfSpreadLayout({ + required super.pageLayouts, + required super.documentSize, + required this.spreadLayouts, + required this.pageToSpreadIndex, + }); + + /// List of spread bounds, indexed by spread index + /// + /// Each Rect represents the bounds of one spread in document coordinates. + /// Use [getSpreadBounds] to get the spread for a specific page number. + final List spreadLayouts; + + /// Maps page number to spread index. + /// + /// - Example: `pageToSpreadIndex[0]` = spread index for page 1 + final List pageToSpreadIndex; + + /// Get the spread bounds for a given page number. + @override + Rect getSpreadBounds(int pageNumber, {bool withMargins = false}) { + if (pageNumber < 1 || pageNumber > pageLayouts.length) { + throw RangeError('Invalid page number $pageNumber'); + } + return spreadLayouts[pageToSpreadIndex[pageNumber - 1]].inflate(withMargins ? _impliedMargin : 0); + } + + /// Get the page range for the spread containing pageNumber. + PdfPageRange getPageRange(int pageNumber) { + final spreadIndex = pageToSpreadIndex[pageNumber - 1]; + var first = -1; + var last = -1; + for (var i = 0; i < pageToSpreadIndex.length; i++) { + if (pageToSpreadIndex[i] == spreadIndex) { + if (first == -1) first = i + 1; // Convert to 1-based + last = i + 1; // Convert to 1-based + } + } + return PdfPageRange(first, last); + } + + /// Get the first page number of the spread containing pageNumber. + int getSpreadFirstPage(int pageNumber) => getPageRange(pageNumber).firstPageNumber; + + /// Get the last page number of the spread containing pageNumber. + int getSpreadLastPage(int pageNumber) => getPageRange(pageNumber).lastPageNumber; + + /// Get the first page number of a spread by its index. + int? getFirstPageOfSpread(int spreadIndex) { + if (spreadIndex < 0 || spreadIndex >= spreadLayouts.length) { + return null; + } + for (var i = 0; i < pageToSpreadIndex.length; i++) { + if (pageToSpreadIndex[i] == spreadIndex) { + return i + 1; // Convert to 1-based page number + } + } + return null; + } + + /// Gets the maximum spread width across all spreads. + @override + double getMaxWidth({bool withMargins = false}) { + final maxWidthNoMargins = spreadLayouts.fold(0.0, (maximum, rect) => max(maximum, rect.width)); + return maxWidthNoMargins + (withMargins ? _impliedMargin * 2 : 0); + } + + /// Gets the maximum spread height across all spreads. + @override + double getMaxHeight({bool withMargins = false}) { + final maxHeightNoMargins = spreadLayouts.fold(0.0, (maximum, rect) => max(maximum, rect.height)); + return maxHeightNoMargins + (withMargins ? _impliedMargin * 2 : 0); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! PdfSpreadLayout) return false; + return super == other && + listEquals(spreadLayouts, other.spreadLayouts) && + listEquals(pageToSpreadIndex, other.pageToSpreadIndex); + } + + @override + int get hashCode => Object.hash(super.hashCode, Object.hashAll(spreadLayouts), Object.hashAll(pageToSpreadIndex)); +} + +/// Sequential pages layout implementation supporting both vertical and horizontal scrolling. +/// +/// This layout displays pages one after another in either a vertical or horizontal +/// scrolling direction. The scroll direction is specified when creating the layout. +/// +/// Example usage: +/// ```dart +/// // Vertical scrolling (default) +/// layoutPages: (pages, params, {viewport}) => +/// SequentialPagesLayout.fromPages(pages, params, helper: helper), +/// +/// // Horizontal scrolling +/// layoutPages: (pages, params, {viewport}) => +/// SequentialPagesLayout.fromPages( +/// pages, +/// params, +/// helper: helper, +/// scrollDirection: Axis.horizontal, +/// ), +/// ``` +class SequentialPagesLayout extends PdfPageLayout { + SequentialPagesLayout({required super.pageLayouts, required super.documentSize, required this.scrollDirection}); + + /// Create a sequential pages layout from pages and parameters. + /// + /// The [scrollDirection] parameter determines whether pages scroll vertically (default) + /// or horizontally. + factory SequentialPagesLayout.fromPages( + List pages, + PdfViewerParams params, { + PdfLayoutHelper? helper, + Axis scrollDirection = Axis.vertical, + }) { + final layout = SequentialPagesLayout(pageLayouts: [], documentSize: Size.zero, scrollDirection: scrollDirection); + final result = layout.layoutBuilder(pages, params, helper: helper); + return SequentialPagesLayout( + pageLayouts: result.pageLayouts, + documentSize: result.documentSize, + scrollDirection: scrollDirection, + ); + } + + final Axis scrollDirection; + + @override + Axis get primaryAxis => scrollDirection; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! SequentialPagesLayout) return false; + return super == other && scrollDirection == other.scrollDirection; + } + + @override + int get hashCode => Object.hash(super.hashCode, scrollDirection); + + @override + LayoutResult layoutBuilder(List pages, PdfViewerParams params, {PdfLayoutHelper? helper}) { + assert(helper != null, 'SequentialPagesLayout requires PdfLayoutHelper for fit modes other than none or cover'); + final pageSizes = PdfPageLayout.calculatePageSizes( + pages: pages, + fitMode: params.fitMode, + scrollAxis: scrollDirection, + helper: helper!, + ); + + final centerPerpendicular = + params.fitMode == FitMode.fit || params.fitMode == FitMode.none || params.fitMode == FitMode.cover; + + return layoutSequentialPages( + pageSizes: pageSizes, + scrollAxis: scrollDirection, + margin: params.margin, + centerPerpendicular: centerPerpendicular, + ); + } +} + +/// Facing pages layout implementation. +class FacingPagesLayout extends PdfSpreadLayout { + FacingPagesLayout({ + required super.pageLayouts, + required super.documentSize, + required super.spreadLayouts, + required super.pageToSpreadIndex, + }); + + /// Create a facing pages layout from pages and parameters. + factory FacingPagesLayout.fromPages( + List pages, + PdfViewerParams params, { + PdfLayoutHelper? helper, + bool firstPageIsCoverPage = false, + bool isRightToLeftReadingOrder = false, + double? gutter, // gap between left/right pages + bool singlePagesFillAvailableWidth = true, + bool independentPageScaling = true, + }) { + final effectiveGutter = gutter ?? params.margin; + + if (pages.isEmpty) { + return FacingPagesLayout(pageLayouts: [], documentSize: Size.zero, spreadLayouts: [], pageToSpreadIndex: []); + } + + assert( + !independentPageScaling || helper != null, + 'FitMode.${params.fitMode.name} requires PdfLayoutHelper for FacingPagesLayout.', + ); + if (independentPageScaling && helper == null) { + return FacingPagesLayout(pageLayouts: [], documentSize: Size.zero, spreadLayouts: [], pageToSpreadIndex: []); + } + + final pageLayouts = []; + final spreadLayouts = []; + final pageToSpreadIndex = List.filled(pages.length, 0); + var y = params.margin; + var spreadIndex = 0; + var pageIndex = 0; + + // Available space for content + final double availableWidth; + final double availableHeight; + final double maxPageWidth; // Max width of any page (for non-independent scaling) + + if (independentPageScaling) { + availableWidth = helper!.availableWidth; + availableHeight = helper.availableHeight; + maxPageWidth = 0.0; // Not used when scaling independently + } else { + // For non-independent scaling, find the maximum page width + maxPageWidth = pages.fold(0.0, (prev, page) => max(prev, page.width)); + availableWidth = 0.0; // Not used when not scaling independently + availableHeight = 0.0; // Not used when not scaling independently + } + + // Handle cover page if needed + if (firstPageIsCoverPage && pages.isNotEmpty) { + final coverPage = pages[0]; + + // Determine target width based on singlePagesFillAvailableWidth setting + final coverTargetWidth = singlePagesFillAvailableWidth ? availableWidth : (availableWidth - effectiveGutter) / 2; + + final coverSize = independentPageScaling + ? _calculatePageSize( + page: coverPage, + targetWidth: coverTargetWidth, + availableHeight: availableHeight, + fitMode: params.fitMode, + ) + : Size(coverPage.width, coverPage.height); + + // Position cover page based on RTL and fill setting + // RTL: cover on left, LTR: cover on right (when not filling full width) + final double coverX; + if (independentPageScaling) { + if (singlePagesFillAvailableWidth) { + // Center the page in available width + coverX = params.margin + (availableWidth - coverSize.width) / 2; + } else { + // Position on left (RTL) or right (LTR) + if (isRightToLeftReadingOrder) { + // RTL: cover page on left + coverX = params.margin; + } else { + // LTR: cover page on right + coverX = params.margin + (availableWidth - coverSize.width); + } + } + } else { + // Legacy mode: position cover page using maxPageWidth centering + if (singlePagesFillAvailableWidth) { + // Center in document width + coverX = params.margin + (maxPageWidth * 2 + params.margin - coverSize.width) / 2; + } else { + // Position in one half based on RTL + if (isRightToLeftReadingOrder) { + // RTL: cover page on left side (right-aligned within left half) + coverX = params.margin + (maxPageWidth - coverSize.width); + } else { + // LTR: cover page on right side (left-aligned within right half) + coverX = params.margin * 2 + maxPageWidth; + } + } + } + + pageLayouts.add(Rect.fromLTWH(coverX, y, coverSize.width, coverSize.height)); + pageToSpreadIndex[0] = spreadIndex; // Page 1 is at index 0 + spreadLayouts.add(pageLayouts.last); + + spreadIndex++; + y += coverSize.height + params.margin; + pageIndex = 1; + } + + // Process remaining pages as spreads (pairs) + while (pageIndex < pages.length) { + final leftPage = pages[pageIndex]; + final rightPageIndex = pageIndex + 1; + final rightPage = rightPageIndex < pages.length ? pages[rightPageIndex] : null; + + // Determine if this is the last page and it's a single page + final isLastPageSingle = rightPage == null; + + // Calculate page dimensions + // For last single page, use singlePagesFillAvailableWidth to determine width + final leftTargetWidth = independentPageScaling + ? (isLastPageSingle && singlePagesFillAvailableWidth + ? availableWidth + : (availableWidth - effectiveGutter) / 2) + : 0.0; + + final leftSize = independentPageScaling + ? _calculatePageSize( + page: leftPage, + targetWidth: leftTargetWidth, + availableHeight: availableHeight, + fitMode: params.fitMode, + ) + : Size(leftPage.width, leftPage.height); + + final rightSize = rightPage != null && independentPageScaling + ? _calculatePageSize( + page: rightPage, + targetWidth: (availableWidth - effectiveGutter) / 2, + availableHeight: availableHeight, + fitMode: params.fitMode, + ) + : Size(rightPage?.width ?? 0.0, rightPage?.height ?? 0.0); + + // Calculate spread dimensions and positioning + final double spreadWidth; + final spreadHeight = max(leftSize.height, rightSize.height); + + // Determine spread X position + final double spreadX; + if (independentPageScaling) { + spreadWidth = rightPage != null ? leftSize.width + effectiveGutter + rightSize.width : leftSize.width; + if (rightPage != null) { + // Two-page spread: center it + spreadX = params.margin + (availableWidth - spreadWidth) / 2; + } else { + // Single last page + if (singlePagesFillAvailableWidth) { + // Fill full width: center the page + spreadX = params.margin + (availableWidth - spreadWidth) / 2; + } else { + // Don't fill full width: position based on RTL + if (isRightToLeftReadingOrder) { + // RTL: last page on right + spreadX = params.margin + (availableWidth - spreadWidth); + } else { + // LTR: last page on left + spreadX = params.margin; + } + } + } + } else { + // Legacy mode: spread width is based on two max-width slots + spreadWidth = rightPage != null ? maxPageWidth * 2 + params.margin : leftSize.width; + // Legacy mode: spread starts at left margin + // The individual page positions will be calculated relative to maxPageWidth + spreadX = params.margin; + } + + spreadLayouts.add(Rect.fromLTWH(spreadX, y, spreadWidth, spreadHeight)); + + // Layout pages within spread (RTL vs LTR) + final double leftX; + final double rightX; + + if (!independentPageScaling) { + // Legacy facing pages layout: pages are centered relative to maxPageWidth + // Document width = (margin + maxPageWidth) * 2 + margin + if (rightPage != null) { + // Two-page spread + // Left pages: maxPageWidth + margin - page.width (right-aligned within left half) + // Right pages: margin * 2 + maxPageWidth (left-aligned within right half) + if (isRightToLeftReadingOrder) { + // RTL: right page on left (right-aligned), left page on right (left-aligned) + rightX = params.margin + (maxPageWidth - rightSize.width); + leftX = params.margin * 2 + maxPageWidth; + } else { + // LTR: left page on left (right-aligned), right page on right (left-aligned) + leftX = params.margin + (maxPageWidth - leftSize.width); + rightX = params.margin * 2 + maxPageWidth; + } + } else { + // Single last page + if (singlePagesFillAvailableWidth) { + // Center in document width + leftX = params.margin + (maxPageWidth * 2 + params.margin - leftSize.width) / 2; + } else { + // Position in one half based on RTL + if (isRightToLeftReadingOrder) { + // RTL: last page on right side (left-aligned within right half) + leftX = params.margin * 2 + maxPageWidth; + } else { + // LTR: last page on left side (right-aligned within left half) + leftX = params.margin + (maxPageWidth - leftSize.width); + } + } + rightX = 0; // Not used for single pages + } + } else { + // Original behavior for independentPageScaling = true + leftX = isRightToLeftReadingOrder && rightPage != null ? spreadX + rightSize.width + effectiveGutter : spreadX; + rightX = isRightToLeftReadingOrder ? spreadX : spreadX + leftSize.width + effectiveGutter; + } + + pageLayouts.add(Rect.fromLTWH(leftX, y + (spreadHeight - leftSize.height) / 2, leftSize.width, leftSize.height)); + pageToSpreadIndex[pageIndex] = spreadIndex; // 0-based indexing + + if (rightPage != null) { + pageLayouts.add( + Rect.fromLTWH(rightX, y + (spreadHeight - rightSize.height) / 2, rightSize.width, rightSize.height), + ); + pageToSpreadIndex[rightPageIndex] = spreadIndex; // 0-based indexing + } + + spreadIndex++; + y += spreadHeight + params.margin; + pageIndex += rightPage != null ? 2 : 1; + } + + // Calculate document width based on content + final double documentWidth; + if (independentPageScaling) { + final maxSpreadRight = spreadLayouts.fold(0.0, (maximum, rect) => max(maximum, rect.right)); + documentWidth = maxSpreadRight + params.margin; + } else { + // Legacy mode: document width = (margin + maxPageWidth) * 2 + margin + documentWidth = (params.margin + maxPageWidth) * 2 + params.margin; + } + + return FacingPagesLayout( + pageLayouts: pageLayouts, + documentSize: Size(documentWidth, y), + spreadLayouts: spreadLayouts, + pageToSpreadIndex: pageToSpreadIndex, + ); + } + + @override + Axis get primaryAxis => Axis.vertical; // Typically vertical for facing pages + + @override + LayoutResult layoutBuilder(List pages, PdfViewerParams params, {PdfLayoutHelper? helper}) { + // For the base layoutBuilder, use default parameters (no cover, LTR, no gutter) + final layout = FacingPagesLayout.fromPages( + pages, + params, + helper: helper, + firstPageIsCoverPage: false, + isRightToLeftReadingOrder: false, + gutter: 0.0, + ); + return LayoutResult(pageLayouts: layout.pageLayouts, documentSize: layout.documentSize); + } +} + +/// FacingPagesLayout Helper function to calculate page dimensions for a single page +Size _calculatePageSize({ + required PdfPage page, + required double targetWidth, + required double availableHeight, + required FitMode fitMode, +}) { + if (fitMode == FitMode.fit) { + final scale = min(targetWidth / page.width, availableHeight / page.height); + return Size(page.width * scale, page.height * scale); + } else { + final scale = targetWidth / page.width; + return Size(targetWidth, page.height * scale); + } +} + +/// Represents a range of pages in the PDF document. +@immutable +class PdfPageRange { + /// Creates a page range from [firstPageNumber] to [lastPageNumber], inclusive. + const PdfPageRange(this.firstPageNumber, this.lastPageNumber); + + /// Creates a page range representing a single page. + const PdfPageRange.single(int pageNumber) : firstPageNumber = pageNumber, lastPageNumber = pageNumber; + + /// 1-based page number of first page in range. + /// + /// [firstPageNumber] is always <= [lastPageNumber]. They can be equal for a single-page range. + final int firstPageNumber; + + /// 1-based page number of last page in range. + /// + /// [lastPageNumber] is always >= [firstPageNumber]. They can be equal for a single-page range. + /// As "last" implies, this is inclusive. + final int lastPageNumber; + + /// Returns a string representation of the page range. + /// + /// If the range is a single page, returns just that page number (e.g., "5"). + /// If the range spans multiple pages, returns in "start-last" format (e.g., "3-7"). + String get label => firstPageNumber == lastPageNumber ? '$firstPageNumber' : '$firstPageNumber-$lastPageNumber'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is PdfPageRange && firstPageNumber == other.firstPageNumber && lastPageNumber == other.lastPageNumber; + } + + @override + int get hashCode => Object.hash(firstPageNumber, lastPageNumber); + + @override + String toString() => 'PdfPageRange($label)'; +} diff --git a/lib/src/widgets/pdf_page_links_overlay.dart b/packages/pdfrx/lib/src/widgets/pdf_page_links_overlay.dart similarity index 100% rename from lib/src/widgets/pdf_page_links_overlay.dart rename to packages/pdfrx/lib/src/widgets/pdf_page_links_overlay.dart diff --git a/lib/src/widgets/pdf_text_searcher.dart b/packages/pdfrx/lib/src/widgets/pdf_text_searcher.dart similarity index 71% rename from lib/src/widgets/pdf_text_searcher.dart rename to packages/pdfrx/lib/src/widgets/pdf_text_searcher.dart index 396c34b1..15c587bf 100644 --- a/lib/src/widgets/pdf_text_searcher.dart +++ b/packages/pdfrx/lib/src/widgets/pdf_text_searcher.dart @@ -10,7 +10,9 @@ import '../../pdfrx.dart'; /// To be notified when the search status change, use [addListener]. class PdfTextSearcher extends Listenable { /// Creates a new instance of [PdfTextSearcher]. - PdfTextSearcher(this._controller); + PdfTextSearcher(this._controller) { + _registerForDocumentChanges(); + } final PdfViewerController _controller; @@ -19,21 +21,22 @@ class PdfTextSearcher extends Listenable { Timer? _searchTextTimer; // timer to start search int _searchSession = 0; // current search session - List _matches = const []; + List _matches = const []; List _matchesPageStartIndices = const []; - Pattern? _lastSearchPattern; + _SearchCondition? _lastSearchCondition; int? _currentIndex; - PdfTextRangeWithFragments? _currentMatch; + PdfPageTextRange? _currentMatch; int? _searchingPageNumber; int? _totalPageCount; bool _isSearching = false; final _cachedText = {}; + StreamSubscription? _documentEventSubscription; /// The current match index in [matches] if available. int? get currentIndex => _currentIndex; /// Get the current matches. - List get matches => _matches; + List get matches => _matches; /// Whether there are any matches or not (so far). bool get hasMatches => _currentIndex != null && matches.isNotEmpty; @@ -51,7 +54,7 @@ class PdfTextSearcher extends Listenable { return _searchingPageNumber! / _totalPageCount!; } - Pattern? get pattern => _lastSearchPattern; + Pattern? get pattern => _lastSearchCondition?.pattern; int get searchSession => _searchSession; @@ -81,13 +84,17 @@ class PdfTextSearcher extends Listenable { final searchSession = ++_searchSession; void search() { - if (_isIdenticalPattern(_lastSearchPattern, pattern)) return; - _lastSearchPattern = pattern; + if (_isIdenticalPattern(_lastSearchCondition?.pattern, pattern)) return; + _lastSearchCondition = _SearchCondition( + pattern: pattern, + caseInsensitive: caseInsensitive, + goToFirstMatch: goToFirstMatch, + ); if (pattern.isEmpty) { _resetTextSearch(); return; } - _startTextSearchInternal(pattern, searchSession, caseInsensitive, goToFirstMatch); + _startTextSearchInternal(_lastSearchCondition!, searchSession); } if (searchImmediately) { @@ -97,34 +104,18 @@ class PdfTextSearcher extends Listenable { } } - bool _isIdenticalPattern(Pattern? a, Pattern? b) { - if (a is String && b is String) { - return a == b; - } - if (a is RegExp && b is RegExp) { - return a.pattern == b.pattern && - a.isCaseSensitive == b.isCaseSensitive && - a.isMultiLine == b.isMultiLine && - a.isUnicode == b.isUnicode && - a.isDotAll == b.isDotAll; - } - if (a == null && b == null) { - return true; - } - return false; - } - /// Reset the current search. void resetTextSearch() => _resetTextSearch(); /// Almost identical to [resetTextSearch], but does not notify listeners. void dispose() { + _documentEventSubscription?.cancel(); _listeners.clear(); _cachedText.clear(); _resetTextSearch(notify: false); } - void _resetTextSearch({bool notify = true}) { + void _resetTextSearch({bool notify = true, bool clearSearchCondition = true}) { _cancelTextSearch(); _matches = const []; _matchesPageStartIndices = const []; @@ -132,7 +123,9 @@ class PdfTextSearcher extends Listenable { _currentIndex = null; _currentMatch = null; _isSearching = false; - _lastSearchPattern = null; + if (clearSearchCondition) { + _lastSearchCondition = null; + } if (notify) { notifyListeners(); } @@ -143,25 +136,21 @@ class PdfTextSearcher extends Listenable { ++_searchSession; } - Future _startTextSearchInternal( - Pattern text, - int searchSession, - bool caseInsensitive, - bool goToFirstMatch, - ) async { + Future _startTextSearchInternal(_SearchCondition condition, int searchSession) async { await controller?.useDocument((document) async { - final textMatches = []; + final textMatches = []; final textMatchesPageStartIndex = []; - bool first = true; + var first = true; _isSearching = true; _totalPageCount = document.pages.length; for (final page in document.pages) { _searchingPageNumber = page.pageNumber; if (searchSession != _searchSession) return; final pageText = await loadText(pageNumber: page.pageNumber); + if (searchSession != _searchSession) return; if (pageText == null) continue; textMatchesPageStartIndex.add(textMatches.length); - await for (final f in pageText.allMatches(text, caseInsensitive: caseInsensitive)) { + await for (final f in pageText.allMatches(condition.pattern, caseInsensitive: condition.caseInsensitive)) { if (searchSession != _searchSession) return; textMatches.add(f); } @@ -172,7 +161,7 @@ class PdfTextSearcher extends Listenable { if (_matches.isNotEmpty && first) { first = false; - if (goToFirstMatch) { + if (condition.goToFirstMatch) { _currentIndex = 0; _currentMatch = null; goToMatchOfIndex(_currentIndex!); @@ -182,12 +171,37 @@ class PdfTextSearcher extends Listenable { }); } + void _registerForDocumentChanges() { + _documentEventSubscription?.cancel(); + _documentEventSubscription = controller!.document.events.listen((event) { + if (event is PdfDocumentPageStatusChangedEvent) { + final changedPages = event.changes.keys.toSet(); + final needRestart = _matches.any((m) => changedPages.contains(m.pageNumber)); + if (needRestart) { + _restartSearch(); + } + } + }); + } + + void _restartSearch() { + _resetTextSearch(clearSearchCondition: false); + _cachedText.clear(); + if (_lastSearchCondition != null) { + startTextSearch( + _lastSearchCondition!.pattern, + caseInsensitive: _lastSearchCondition!.caseInsensitive, + goToFirstMatch: _lastSearchCondition!.goToFirstMatch, + ); + } + } + /// Just a helper function to load the text of a page. Future loadText({required int pageNumber}) async { final cached = _cachedText[pageNumber]; if (cached != null) return cached; return await controller?.useDocument((document) async { - return _cachedText[pageNumber] ??= await document.pages[pageNumber - 1].loadText(); + return _cachedText[pageNumber] ??= await document.pages[pageNumber - 1].loadStructuredText(); }); } @@ -218,7 +232,7 @@ class PdfTextSearcher extends Listenable { } /// Go to the given match. - Future goToMatch(PdfTextRangeWithFragments match) async { + Future goToMatch(PdfPageTextRange match) async { _currentMatch = match; _currentIndex = _matches.indexOf(match); await controller?.ensureVisible( @@ -247,7 +261,7 @@ class PdfTextSearcher extends Listenable { /// Paint callback to highlight the matches. /// - /// Use this with [PdfViewerParams.pagePaintCallback] to highlight the matches. + /// Use this with [PdfViewerParams.pagePaintCallbacks] to highlight the matches. void pageTextMatchPaintCallback(ui.Canvas canvas, Rect pageRect, PdfPage page) { final range = getMatchesRangeForPage(page.pageNumber); if (range == null) return; @@ -255,7 +269,7 @@ class PdfTextSearcher extends Listenable { final matchTextColor = controller?.params.matchTextColor ?? Colors.yellow.withAlpha(127); final activeMatchTextColor = controller?.params.activeMatchTextColor ?? Colors.orange.withAlpha(127); - for (int i = range.start; i < range.end; i++) { + for (var i = range.start; i < range.end; i++) { final m = _matches[i]; final rect = m.bounds.toRect(page: page, scaledPageSize: pageRect.size).translate(pageRect.left, pageRect.top); canvas.drawRect(rect, Paint()..color = m == _currentMatch ? activeMatchTextColor : matchTextColor); @@ -272,7 +286,7 @@ class PdfTextSearcher extends Listenable { void removeListener(VoidCallback listener) => _listeners.remove(listener); } -extension PatternExts on Pattern { +extension _PatternExts on Pattern { bool get isEmpty { switch (this) { case String s: @@ -284,3 +298,27 @@ extension PatternExts on Pattern { } } } + +class _SearchCondition { + const _SearchCondition({required this.pattern, required this.caseInsensitive, required this.goToFirstMatch}); + final Pattern pattern; + final bool caseInsensitive; + final bool goToFirstMatch; +} + +bool _isIdenticalPattern(Pattern? a, Pattern? b) { + if (a is String && b is String) { + return a == b; + } + if (a is RegExp && b is RegExp) { + return a.pattern == b.pattern && + a.isCaseSensitive == b.isCaseSensitive && + a.isMultiLine == b.isMultiLine && + a.isUnicode == b.isUnicode && + a.isDotAll == b.isDotAll; + } + if (a == null && b == null) { + return true; + } + return false; +} diff --git a/packages/pdfrx/lib/src/widgets/pdf_viewer.dart b/packages/pdfrx/lib/src/widgets/pdf_viewer.dart new file mode 100644 index 00000000..c53e7025 --- /dev/null +++ b/packages/pdfrx/lib/src/widgets/pdf_viewer.dart @@ -0,0 +1,5543 @@ +// ignore_for_file: public_member_api_docs +import 'dart:async'; +import 'dart:math'; +import 'dart:ui' as ui; + +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:rxdart/rxdart.dart'; +import 'package:synchronized/extension.dart'; +import 'package:vector_math/vector_math_64.dart' as vec; + +import '../../pdfrx.dart'; +import '../utils/edge_insets_extensions.dart'; +import '../utils/platform.dart'; +import 'interactive_viewer.dart' as iv; +import 'internals/pdf_error_widget.dart'; +import 'internals/pdf_viewer_key_handler.dart'; +import 'internals/widget_size_sniffer.dart'; +import 'pdf_page_links_overlay.dart'; + +/// A widget to display PDF document. +/// +/// To create a [PdfViewer] widget, use one of the following constructors: +/// - [PdfDocument] with [PdfViewer.documentRef] +/// - [PdfViewer.asset] with an asset name +/// - [PdfViewer.file] with a file path +/// - [PdfViewer.uri] with a URI +/// +/// Or otherwise, you can pass [PdfDocumentRef] to [PdfViewer] constructor. +class PdfViewer extends StatefulWidget { + /// Create [PdfViewer] from a [PdfDocumentRef]. + /// + /// - [documentRef] is the [PdfDocumentRef]. + /// - [controller] is the controller to control the viewer. + /// - [params] is the parameters to customize the viewer. + /// - [initialPageNumber] is the page number to show initially. + const PdfViewer( + this.documentRef, { + super.key, + this.controller, + this.params = const PdfViewerParams(), + this.initialPageNumber = 1, + }); + + /// Create [PdfViewer] from an asset. + /// + /// - [assetName] is the asset name. + /// - [passwordProvider] is used to provide password for encrypted PDF. See [PdfPasswordProvider] for more info. + /// - [firstAttemptByEmptyPassword] is used to determine whether the first attempt to open the PDF is by empty password + /// or not. For more info, see [PdfPasswordProvider]. + /// - [controller] is the controller to control the viewer. + /// - [params] is the parameters to customize the viewer. + /// - [initialPageNumber] is the page number to show initially. + PdfViewer.asset( + String assetName, { + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = true, + super.key, + this.controller, + this.params = const PdfViewerParams(), + this.initialPageNumber = 1, + }) : documentRef = PdfDocumentRefAsset( + assetName, + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + ); + + /// Create [PdfViewer] from a file. + /// + /// - [path] is the file path. + /// - [passwordProvider] is used to provide password for encrypted PDF. See [PdfPasswordProvider] for more info. + /// - [firstAttemptByEmptyPassword] is used to determine whether the first attempt to open the PDF is by empty password + /// or not. For more info, see [PdfPasswordProvider]. + /// - [controller] is the controller to control the viewer. + /// - [params] is the parameters to customize the viewer. + /// - [initialPageNumber] is the page number to show initially. + PdfViewer.file( + String path, { + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = true, + super.key, + this.controller, + this.params = const PdfViewerParams(), + this.initialPageNumber = 1, + }) : documentRef = PdfDocumentRefFile( + path, + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + ); + + /// Create [PdfViewer] from a URI. + /// + /// - [uri] is the URI. + /// - [passwordProvider] is used to provide password for encrypted PDF. See [PdfPasswordProvider] for more info. + /// - [firstAttemptByEmptyPassword] is used to determine whether the first attempt to open the PDF is by empty password + /// or not. For more info, see [PdfPasswordProvider]. + /// - [controller] is the controller to control the viewer. + /// - [params] is the parameters to customize the viewer. + /// - [initialPageNumber] is the page number to show initially. + /// - [preferRangeAccess] to prefer range access to download the PDF. The default is false. (Not supported on Web). + /// - [headers] is used to specify additional HTTP headers especially for authentication/authorization. + /// - [withCredentials] is used to specify whether to include credentials in the request (Only supported on Web). + /// - [timeout] is the timeout duration for loading the document. (Only supported on non-Web platforms). + PdfViewer.uri( + Uri uri, { + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = true, + super.key, + this.controller, + this.params = const PdfViewerParams(), + this.initialPageNumber = 1, + bool preferRangeAccess = false, + Map? headers, + bool withCredentials = false, + Duration? timeout, + }) : documentRef = PdfDocumentRefUri( + uri, + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + preferRangeAccess: preferRangeAccess, + headers: headers, + withCredentials: withCredentials, + timeout: timeout, + ); + + /// Create [PdfViewer] from a byte data. + /// + /// - [data] is the byte data. + /// - [sourceName] must be some ID, e.g., file name or URL, to identify the source of the PDF. If [sourceName] is not + /// unique for each source, the viewer may not work correctly. + /// - [passwordProvider] is used to provide password for encrypted PDF. See [PdfPasswordProvider] for more info. + /// - [firstAttemptByEmptyPassword] is used to determine whether the first attempt to open the PDF is by empty password + /// or not. For more info, see [PdfPasswordProvider]. + /// - [controller] is the controller to control the viewer. + /// - [params] is the parameters to customize the viewer. + /// - [initialPageNumber] is the page number to show initially. + PdfViewer.data( + Uint8List data, { + required String sourceName, + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = true, + super.key, + this.controller, + this.params = const PdfViewerParams(), + this.initialPageNumber = 1, + }) : documentRef = PdfDocumentRefData( + data, + sourceName: sourceName, + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + ); + + /// Create [PdfViewer] from a custom source. + /// + /// - [fileSize] is the size of the PDF file. + /// - [read] is the function to read the PDF file. + /// - [sourceName] must be some ID, e.g., file name or URL, to identify the source of the PDF. If [sourceName] is not + /// unique for each source, the viewer may not work correctly. + /// - [passwordProvider] is used to provide password for encrypted PDF. See [PdfPasswordProvider] for more info. + /// - [firstAttemptByEmptyPassword] is used to determine whether the first attempt to open the PDF is by empty password + /// or not. For more info, see [PdfPasswordProvider]. + /// - [controller] is the controller to control the viewer. + /// - [params] is the parameters to customize the viewer. + /// - [initialPageNumber] is the page number to show initially. + PdfViewer.custom({ + required int fileSize, + required FutureOr Function(Uint8List buffer, int position, int size) read, + required String sourceName, + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = true, + super.key, + this.controller, + this.params = const PdfViewerParams(), + this.initialPageNumber = 1, + }) : documentRef = PdfDocumentRefCustom( + fileSize: fileSize, + read: read, + sourceName: sourceName, + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + ); + + /// [PdfDocumentRef] that represents the PDF document. + final PdfDocumentRef documentRef; + + /// Controller to control the viewer. + final PdfViewerController? controller; + + /// Parameters to customize the display of the PDF document. + final PdfViewerParams params; + + /// Page number to show initially. + final int initialPageNumber; + + @override + State createState() => _PdfViewerState(); +} + +class _PdfViewerState extends State + with SingleTickerProviderStateMixin + implements PdfTextSelectionDelegate, PdfViewerCoordinateConverter { + PdfViewerController? _controller; + late final _txController = _PdfViewerTransformationController(this); + late final AnimationController _animController; + Animation? _animGoTo; + int _animationResettingGuard = 0; + + PdfDocument? _document; + PdfPageLayout? _layout; + Size? _viewSize; + static const _defaultMinScale = 0.1; + double _fitScale = _defaultMinScale; // Scale calculated based on fitMode for page positioning + int? _pageNumber; + PdfPageRange? _visiblePageRange; + bool _initialized = false; + bool _usingScrollPercentageMode = false; + + StreamSubscription? _documentSubscription; + final _interactiveViewerKey = GlobalKey(); + + final List _zoomStops = [1.0]; + + final _imageCache = _PdfPageImageCache(); + final _magnifierImageCache = _PdfPageImageCache(); + + late final _canvasLinkPainter = _CanvasLinkPainter(this); + + // Changes to the stream rebuilds the viewer + final _updateStream = BehaviorSubject(); + + /// page-number keyed cache of extracted text + final _textCache = {}; + Timer? _textSelectionChangedDebounceTimer; + final double _hitTestMargin = 3.0; + + /// The starting/ending point of the text selection. + PdfTextSelectionPoint? _selA, _selB; + Offset? _textSelectAnchor; + + /// [_textSelA] is the rectangle of the first character in the selected paragraph and + PdfTextSelectionAnchor? _textSelA; + + /// [_textSelB] is the rectangle of the last character in the selected paragraph. + PdfTextSelectionAnchor? _textSelB; + + _TextSelectionPart _selPartMoving = _TextSelectionPart.none; + + _TextSelectionPart _selPartLastMoved = _TextSelectionPart.none; + + bool _isSelectingAllText = false; + PointerDeviceKind? _selectionPointerDeviceKind; + + Offset? _contextMenuDocumentPosition; + PdfViewerPart _contextMenuFor = PdfViewerPart.background; + + Timer? _interactionEndedTimer; + bool _isInteractionGoingOn = false; + bool _isActiveGesture = false; // True during pan/scale gestures + bool _isActivelyZooming = false; // True only during active pinch-zoom gesture + bool _hasActiveAnimations = false; // True when InteractiveViewer has active animations + + BuildContext? _contextForFocusNode; + + /// last pointer location in viewer local coordinates + Offset _pointerOffset = Offset.zero; + + /// last pointer device kind to differentiate between mouse and touch + PointerDeviceKind? _pointerDeviceKind; + + // boundary margins adjusted to center content that's smaller than + // the viewport + EdgeInsets _adjustedBoundaryMargins = EdgeInsets.zero; + + @override + void initState() { + super.initState(); + pdfrxFlutterInitialize(); + _animController = AnimationController(vsync: this, duration: const Duration(milliseconds: 200)); + _widgetUpdated(null); + } + + @override + void didUpdateWidget(covariant PdfViewer oldWidget) { + super.didUpdateWidget(oldWidget); + _widgetUpdated(oldWidget); + } + + Future _widgetUpdated(PdfViewer? oldWidget) async { + if (widget == oldWidget) { + return; + } + + if (oldWidget?.documentRef.key == widget.documentRef.key) { + if (widget.params.doChangesRequireReload(oldWidget?.params)) { + if (widget.params.annotationRenderingMode != oldWidget?.params.annotationRenderingMode) { + _imageCache.releaseAllImages(); + _magnifierImageCache.releaseAllImages(); + } + } + return; + } else { + oldWidget?.documentRef.resolveListenable().removeListener(_onDocumentChanged); + widget.documentRef.resolveListenable() + ..addListener(_onDocumentChanged) + ..load(); + } + + _onDocumentChanged(); + } + + void _onDocumentChanged() async { + _layout = null; + _documentSubscription?.cancel(); + _documentSubscription = null; + _textSelectionChangedDebounceTimer?.cancel(); + _stopInteraction(); + _imageCache.releaseAllImages(); + _magnifierImageCache.releaseAllImages(); + _canvasLinkPainter.resetAll(); + _textCache.clear(); + _clearTextSelections(invalidate: false); + _pageNumber = null; + _gotoTargetPageNumber = null; + _initialized = false; + _txController.removeListener(_onMatrixChanged); + _controller?._attach(null); + + final listenable = widget.documentRef.resolveListenable(); + final document = listenable.document; + if (document == null) { + _document = null; + if (mounted) { + setState(() {}); + } + _notifyOnDocumentChanged(); + if (listenable.error != null) { + _notifyDocumentLoadFinished(succeeded: false); + } + return; + } + + _document = document; + + _controller ??= widget.controller ?? PdfViewerController(); + _controller!._attach(this); + _txController.addListener(_onMatrixChanged); + _documentSubscription = document.events.listen(_onDocumentEvent); + + if (mounted) { + setState(() {}); + } + + _notifyOnDocumentChanged(); + _loadDelayed(); + } + + Future _loadDelayed() async { + // To make the page image loading more smooth, delay the loading of pages + await Future.delayed(widget.params.behaviorControlParams.trailingPageLoadingDelay); + + final stopwatch = Stopwatch()..start(); + await _document?.loadPagesProgressively( + onPageLoadProgress: (pageNumber, totalPageCount, document) { + if (document == _document && mounted) { + debugPrint('PdfViewer: Loaded page $pageNumber of $totalPageCount in ${stopwatch.elapsedMilliseconds} ms'); + return true; + } + return false; + }, + data: _document, + ); + } + + void _notifyOnDocumentChanged() { + if (widget.params.onDocumentChanged != null) { + Future.microtask(() => widget.params.onDocumentChanged?.call(_document)); + } + } + + @override + void dispose() { + focusReportForPreventingContextMenuWeb(this, false); + _documentSubscription?.cancel(); + _textSelectionChangedDebounceTimer?.cancel(); + _interactionEndedTimer?.cancel(); + _imageCache.cancelAllPendingRenderings(); + _magnifierImageCache.cancelAllPendingRenderings(); + _animController.dispose(); + widget.documentRef.resolveListenable().removeListener(_onDocumentChanged); + _imageCache.releaseAllImages(); + _magnifierImageCache.releaseAllImages(); + _canvasLinkPainter.resetAll(); + _txController.removeListener(_onMatrixChanged); + _controller?._attach(null); + _txController.dispose(); + super.dispose(); + } + + void _onMatrixChanged() => _invalidate(); + + void _onDocumentEvent(PdfDocumentEvent event) { + if (event is PdfDocumentPageStatusChangedEvent) { + // FIXME: We can handle the event more efficiently by only updating the affected pages. + for (final change in event.changes.entries) { + _imageCache.makeCacheImageForPageDirty(change.key); + _magnifierImageCache.makeCacheImageForPageDirty(change.key); + _canvasLinkPainter.releaseLinksForPage(change.key); + _textCache.remove(change.key); + } + _clearTextSelections(invalidate: false); + _invalidate(); + } else if (event is PdfDocumentLoadCompleteEvent) { + _notifyDocumentLoadFinished(succeeded: true); + } + } + + Future _notifyDocumentLoadFinished({required bool succeeded}) async { + if (succeeded) { + // FIXME: This is a temporary workaround to wait until the initial page is loaded. + while (mounted) { + if (_imageCache.pageImages.containsKey(widget.initialPageNumber)) break; + await Future.delayed(const Duration(milliseconds: 100)); + } + } + + Future.microtask(() async { + if (mounted) { + widget.params.onDocumentLoadFinished?.call(widget.documentRef, succeeded); + } + }); + } + + @override + Widget build(BuildContext context) { + final listenable = widget.documentRef.resolveListenable(); + if (listenable.error != null) { + return Container( + color: widget.params.backgroundColor, + child: (widget.params.errorBannerBuilder ?? _defaultErrorBannerBuilder)( + context, + listenable.error!, + listenable.stackTrace, + widget.documentRef, + ), + ); + } + if (_document == null) { + return Container( + color: widget.params.backgroundColor, + child: widget.params.loadingBannerBuilder?.call(context, listenable.bytesDownloaded, listenable.totalBytes), + ); + } + + return Container( + color: widget.params.backgroundColor, + child: PdfViewerKeyHandler( + onKeyRepeat: _onKey, + // NOTE: When the PdfViewer gets focus, we report it to prevent the default context menu on Web browser. + onFocusChange: (hasFocus) => focusReportForPreventingContextMenuWeb(this, hasFocus), + params: widget.params.keyHandlerParams, + child: StreamBuilder( + stream: _updateStream, + builder: (context, snapshot) { + _contextForFocusNode = context; + return LayoutBuilder( + builder: (context, constraints) { + final isCopyTextEnabled = _document!.permissions?.allowsCopying != false; + final viewSize = Size(constraints.maxWidth, constraints.maxHeight); + + _updateLayout(viewSize); + + return Listener( + onPointerDown: (details) => _handlePointerEvent(details, details.localPosition, details.kind), + onPointerMove: (details) => _handlePointerEvent(details, details.localPosition, details.kind), + onPointerUp: (details) => _handlePointerEvent(details, details.localPosition, details.kind), + onPointerHover: (event) => _handlePointerEvent(event, event.localPosition, event.kind), + child: Stack( + children: [ + iv.InteractiveViewer( + key: _interactiveViewerKey, + transformationController: _txController, + constrained: false, + boundaryMargin: widget.params.scrollPhysics == null + ? const EdgeInsets.all(double.infinity) + : widget.params.pageTransition == PageTransition.discrete + ? EdgeInsets + .zero // Discrete mode uses boundaryProvider + : _adjustedBoundaryMargins, + boundaryProvider: widget.params.pageTransition == PageTransition.discrete + ? _getDiscreteBoundaryRect + : null, + maxScale: widget.params.maxScale, + minScale: minScale, + panAxis: widget.params.panAxis, + panEnabled: widget.params.panEnabled, + scaleEnabled: widget.params.scaleEnabled, + onInteractionEnd: _onInteractionEnd, + onInteractionStart: _onInteractionStart, + onInteractionUpdate: _onInteractionUpdate, + onAnimationEnd: _onAnimationEnd, + interactionEndFrictionCoefficient: widget.params.interactionEndFrictionCoefficient, + onWheelDelta: widget.params.scrollByMouseWheel != null ? _onWheelDelta : null, + scrollPhysics: widget.params.scrollPhysics, + scrollPhysicsScale: widget.params.scrollPhysicsScale, + scrollPhysicsAutoAdjustBoundaries: false, + // PDF pages + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTapUp: (d) => _handleGeneralTap(d.globalPosition, PdfViewerGeneralTapType.tap), + onDoubleTapDown: (d) => + _handleGeneralTap(d.globalPosition, PdfViewerGeneralTapType.doubleTap), + onLongPressStart: (d) => + _handleGeneralTap(d.globalPosition, PdfViewerGeneralTapType.longPress), + onSecondaryTapUp: (d) => + _handleGeneralTap(d.globalPosition, PdfViewerGeneralTapType.secondaryTap), + child: !isTextSelectionEnabled + // show PDF pages without text selection + ? CustomPaint( + foregroundPainter: _CustomPainter.fromFunctions(_paintPages), + size: _layout!.documentSize, + ) + // show PDF pages with text selection + : MouseRegion( + cursor: SystemMouseCursors.text, + hitTestBehavior: HitTestBehavior.deferToChild, + child: GestureDetector( + onPanStart: enableSelectionHandles ? null : _onTextPanStart, + onPanUpdate: enableSelectionHandles ? null : _onTextPanUpdate, + onPanEnd: enableSelectionHandles ? null : _onTextPanEnd, + supportedDevices: { + // PointerDeviceKind.trackpad is intentionally not included here + PointerDeviceKind.mouse, + PointerDeviceKind.stylus, + PointerDeviceKind.touch, + PointerDeviceKind.invertedStylus, + }, + child: CustomPaint( + painter: _CustomPainter.fromFunctions( + _paintPages, + hitTestFunction: _hitTestForTextSelection, + ), + size: _layout!.documentSize, + ), + ), + ), + ), + ), + if (_initialized && _canvasLinkPainter.isLaidUnderPageOverlays) + _canvasLinkPainter.linkHandlingOverlay(viewSize), + if (_initialized) ..._buildPageOverlayWidgets(context), + if (_initialized && _canvasLinkPainter.isLaidOverPageOverlays) + _canvasLinkPainter.linkHandlingOverlay(viewSize), + if (_initialized && widget.params.viewerOverlayBuilder != null) + ...widget.params.viewerOverlayBuilder!(context, viewSize, _canvasLinkPainter._handleTapUp), + if (_initialized) ..._placeTextSelectionWidgets(context, viewSize, isCopyTextEnabled), + ], + ), + ); + }, + ); + }, + ), + ), + ); + } + + Offset _calcOverscroll(Matrix4 m, {required Size viewSize, bool allowExtendedBoundaries = true}) { + final layout = _layout!; + final visible = m.calcVisibleRect(viewSize); + var dxDoc = 0.0; + var dyDoc = 0.0; + + // Check for invalid rect (happens when matrix has incorrect values) + if (!visible.isFinite || visible.isEmpty || visible.left > visible.right || visible.top > visible.bottom) { + return Offset.zero; + } + + // In discrete mode, use the page/spread bounds directly + if (widget.params.pageTransition == PageTransition.discrete) { + final discreteBounds = _getDiscreteBoundaryRect( + visible, + Size(layout.documentSize.width, layout.documentSize.height), + zoom: m.zoom, + ); + if (discreteBounds == null) { + return Offset.zero; + } + + // Calculate overscroll: how much to adjust to keep visible rect within discrete bounds + if (visible.left < discreteBounds.left) { + dxDoc = discreteBounds.left - visible.left; + } else if (visible.right > discreteBounds.right) { + dxDoc = discreteBounds.right - visible.right; + } + + if (visible.top < discreteBounds.top) { + dyDoc = discreteBounds.top - visible.top; + } else if (visible.bottom > discreteBounds.bottom) { + dyDoc = discreteBounds.bottom - visible.bottom; + } + + // Special case: if the discrete bounds are smaller than the viewport, center them + // Note: both discreteBounds and visible are in document space, so we need to compare + // with viewSize converted to document space (viewSize / zoom = visible.size) + if (discreteBounds.width < visible.width) { + final desiredCenter = (discreteBounds.left + discreteBounds.right) / 2; + final currentCenter = (visible.left + visible.right) / 2; + dxDoc = desiredCenter - currentCenter; + } + if (discreteBounds.height < visible.height) { + final desiredCenter = (discreteBounds.top + discreteBounds.bottom) / 2; + final currentCenter = (visible.top + visible.bottom) / 2; + dyDoc = desiredCenter - currentCenter; + } + + return Offset(dxDoc, dyDoc); + } + + // Continuous mode: use EdgeInsets-based boundaries + final boundaryMargin = _adjustedBoundaryMargins; + if (boundaryMargin.containsInfinite) { + return Offset.zero; + } + + final leftBoundary = -boundaryMargin.left; // negative margin reduces allowed leftward scroll + final rightBoundary = + layout.documentSize.width + boundaryMargin.right; // negative margin reduces allowed rightward scroll + final topBoundary = -boundaryMargin.top; // negative margin reduces allowed upward scroll + final bottomBoundary = + layout.documentSize.height + boundaryMargin.bottom; // negative margin reduces allowed downward scroll + + if (visible.left < leftBoundary) { + dxDoc = leftBoundary - visible.left; + } else if (visible.right > rightBoundary) { + dxDoc = rightBoundary - visible.right; + } + + if (visible.top < topBoundary) { + dyDoc = topBoundary - visible.top; + } else if (visible.bottom > bottomBoundary) { + dyDoc = bottomBoundary - visible.bottom; + } + return Offset(dxDoc, dyDoc); + } + + Matrix4 _calcMatrixForClampedToNearestBoundary(Matrix4 candidate, {required Size viewSize}) { + _adjustBoundaryMargins(viewSize, candidate.zoom); + final overScroll = _calcOverscroll(candidate, viewSize: viewSize); + if (overScroll == Offset.zero) { + return candidate; + } + return candidate.clone()..translateByDouble(-overScroll.dx, -overScroll.dy, 0, 1); + } + + /// Snaps the viewport to effective bounds when positioned between document origin and bounds. + /// Used after layout changes to ensure proper boundary alignment. + Matrix4 _calcMatrixForMarginSnappedToNearestBoundary( + Matrix4 candidate, { + required int pageNumber, + required Size viewSize, + }) { + final layout = _layout; + if (layout == null) return candidate; + + _adjustBoundaryMargins(viewSize, candidate.zoom); + + // Get the effective page bounds (includes margins and boundary margins) + final effectiveBounds = _getEffectivePageBounds(pageNumber, layout); + // Use spread bounds (without layout-applied margins) for comparison + final pageRect = layout.getSpreadBounds(pageNumber); + + // Calculate visible rect from candidate matrix + final visible = candidate.calcVisibleRect(viewSize); + + var dxDoc = 0.0; + var dyDoc = 0.0; + + // Snap threshold in screen pixels, converted to document space + const snapThresholdPx = 5.0; // 5 pixels on screen + final snapThreshold = snapThresholdPx / candidate.zoom; + + if (visible.left > effectiveBounds.left && visible.left < pageRect.left + snapThreshold) { + dxDoc = effectiveBounds.left - visible.left; + } + // Top edge: if visible is between effectiveBounds.top and just before page origin, snap to effectiveBounds.top + if (visible.top > effectiveBounds.top && visible.top < pageRect.top + snapThreshold) { + dyDoc = effectiveBounds.top - visible.top; + } + + if (dxDoc == 0.0 && dyDoc == 0.0) { + return candidate; + } + + return candidate.clone()..translateByDouble(-dxDoc, -dyDoc, 0, 1); + } + + void _updateLayout(Size viewSize) { + if (viewSize.height <= 0) return; // For fix blank pdf when restore window from minimize on Windows + final currentPageNumber = _guessCurrentPageNumber(); + final oldVisibleRect = _initialized ? _visibleRect : Rect.zero; + final oldLayout = _layout; + final oldFitScale = _fitScale; + final oldSize = _viewSize; + final isViewSizeChanged = oldSize != viewSize; + _viewSize = viewSize; + + // Clear active gesture state before relayout to prevent extended boundaries + // from being baked into the layout + if (isViewSizeChanged) { + _isActiveGesture = false; + } + + final isLayoutChanged = _relayoutPages(); + + _calcFitScale(); + _calcZoomStopTable(); + // Use max of current zoom and minScale for boundary calculations + // minScale getter returns widget.params.minScale ?? _fitScale, matching InteractiveViewer + final boundaryScale = max(_currentZoom, minScale); + _adjustBoundaryMargins(viewSize, boundaryScale); + + void callOnViewerSizeChanged() { + if (isViewSizeChanged) { + if (_controller != null && widget.params.onViewSizeChanged != null) { + widget.params.onViewSizeChanged!(viewSize, oldSize, _controller!); + } + } + } + + if (!_initialized && _layout != null && _fitScale != _defaultMinScale) { + _initialized = true; + Future.microtask(() async { + // forcibly calculate fit scale for the initial page + _pageNumber = _gotoTargetPageNumber = _calcInitialPageNumber(); + _calcFitScale(); + _calcZoomStopTable(); + final zoom = + widget.params.calculateInitialZoom?.call( + _document!, + _controller!, + _calculateScaleForMode(FitMode.fit), // fitZoom (was _alternativeFitScale) + _calculateScaleForMode(FitMode.fill), // coverZoom (was _coverScale)** + ) ?? + _getInitialZoom(); + await _setZoom(Offset.zero, zoom, duration: Duration.zero); + // Recalculate boundary margins with the correct initial zoom + _adjustBoundaryMargins(_viewSize!, zoom); + + // Determine initial anchor for discrete mode + var discreteAnchor = PdfPageAnchor.center; + if (widget.params.pageTransition == PageTransition.discrete) { + // Check if page fits in viewport on primary axis + final layout = _layout!; + final isPrimaryVertical = layout.primaryAxis == Axis.vertical; + final pageRect = layout.getSpreadBounds(_pageNumber!); + final pageSize = isPrimaryVertical ? pageRect.height : pageRect.width; + final viewportSize = isPrimaryVertical ? _viewSize!.height : _viewSize!.width; + final pageFitsInViewport = pageSize * zoom <= viewportSize; + + // If page fits, center it; otherwise use appropriate edge anchor + if (!pageFitsInViewport) { + discreteAnchor = widget.params.pageAnchor; + } + } + + await _goToPage( + pageNumber: _pageNumber!, + duration: Duration.zero, + maintainCurrentZoom: true, + anchor: widget.params.pageTransition == PageTransition.discrete ? discreteAnchor : PdfPageAnchor.topLeft, + ); + if (mounted && _document != null && _controller != null) { + widget.params.onViewerReady?.call(_document!, _controller!); + } + callOnViewerSizeChanged(); + }); + } else if (isLayoutChanged || isViewSizeChanged) { + // Handle layout/size changes synchronously to prevent flash + // Preserve the visual page size by comparing actual page dimensions in layouts + double zoomTo; + if (_currentZoom < _fitScale || _currentZoom == oldFitScale) { + // User was at fit scale or below minimum - use new fit scale + zoomTo = _fitScale; + } else if (oldLayout != null && currentPageNumber != null && _layout != null) { + // Calculate zoom to maintain same visual page size + // Visual size = pageRect.size * zoom, so we scale zoom by page size ratio + final oldPageRect = oldLayout.pageLayouts[currentPageNumber - 1]; + final newPageRect = _layout!.pageLayouts[currentPageNumber - 1]; + + // Use the primary axis size to determine scaling + final isPrimaryVertical = _layout!.primaryAxis == Axis.vertical; + final oldPageSize = isPrimaryVertical ? oldPageRect.height : oldPageRect.width; + final newPageSize = isPrimaryVertical ? newPageRect.height : newPageRect.width; + + if (newPageSize > 0) { + final calculatedZoom = _currentZoom * (oldPageSize / newPageSize); + // Clamp to min/max scale constraints + zoomTo = calculatedZoom.clamp(minScale, widget.params.maxScale); + } else { + zoomTo = _currentZoom; + } + } else { + zoomTo = _currentZoom; + } + + if (isLayoutChanged) { + _clearTextSelections(); + // if the layout changed, calculate the top-left position in the document + // before the layout change and go to that position in the new layout + + if (oldLayout != null && currentPageNumber != null) { + // Get the hit point in PDF page coordinates (stable across layout changes) + + final hit = _getClosestPageHit(currentPageNumber, oldLayout, oldVisibleRect); + + Offset newOffset; + if (hit == null) { + // Hit is null - top left was in margin area + // Use the page's top-left as the reference point + newOffset = _layout!.pageLayouts[currentPageNumber - 1].topLeft; + } else { + // Got a valid hit - convert PDF coordinates to new layout + newOffset = hit.offset.toOffsetInDocument( + page: hit.page, + pageRect: _layout!.pageLayouts[hit.page.pageNumber - 1], + ); + } + + // preserve the position after a layout change + // Call _goToPosition without await - with Duration.zero it completes synchronously + // This ensures the matrix is updated before the widget rebuilds, preventing flash + _goToPosition(documentOffset: newOffset, zoom: zoomTo, pageNumber: currentPageNumber); + } + } else { + if (zoomTo != _currentZoom) { + // layout hasn't changed, but size and zoom has + final zoomChange = zoomTo / _currentZoom; + final pivot = vec.Vector3(_txController.value.x, _txController.value.y, 0); + + final pivotScale = Matrix4.identity() + ..translateByVector3(pivot) + ..scaleByDouble(zoomChange, zoomChange, zoomChange, 1) + ..translateByVector3(-pivot / zoomChange); + + final Matrix4 zoomPivoted = pivotScale * _txController.value; + _adjustBoundaryMargins(viewSize, zoomTo); + _clampToNearestBoundary(zoomPivoted, viewSize: viewSize); + } else { + // size changes (e.g. rotation) can still cause out-of-bounds matrices + // so clamp here + _clampToNearestBoundary(_txController.value, viewSize: viewSize); + } + callOnViewerSizeChanged(); + } + } else if (currentPageNumber != null && _pageNumber != currentPageNumber) { + // In discrete mode, only allow page changes via _snapToPage (not guessed page number) + // Exception: during initialization when _pageNumber is null + // In continuous mode, always update page based on what's most visible + if (widget.params.pageTransition != PageTransition.discrete || _pageNumber == null) { + _setCurrentPageNumber(currentPageNumber); + } + } + } + + /// Stop InteractiveViewer animations and apply boundary clamping + void _clampToNearestBoundary(Matrix4 candidate, {required Size viewSize}) { + if (_isInteractionGoingOn) return; + + _stopInteractiveViewerAnimation(); + + // Apply the clamped matrix + _txController.value = _calcMatrixForClampedToNearestBoundary(candidate, viewSize: viewSize); + } + + /// Get the state of the internal [iv.InteractiveViewer]. + iv.InteractiveViewerState? get _interactiveViewerState => _interactiveViewerKey.currentState; + + /// Stop any active animations + void _stopInteractiveViewerAnimation() { + if (_interactiveViewerState?.hasActiveAnimations == true) { + _interactiveViewerState?.stopAllAnimations(); + } + } + + int _calcInitialPageNumber() { + return widget.params.calculateInitialPageNumber?.call(_document!, _controller!) ?? widget.initialPageNumber; + } + + PdfPageHitTestResult? _getClosestPageHit(int currentPageNumber, PdfPageLayout oldLayout, ui.Rect oldVisibleRect) { + for (final pageIndex in [currentPageNumber - 1, currentPageNumber - 2, currentPageNumber]) { + if (pageIndex >= 0 && pageIndex < oldLayout.pageLayouts.length) { + final rec = _nudgeHitTest(oldVisibleRect.topLeft, layout: oldLayout, pageIndex: pageIndex); + if (rec != null) { + return rec.hit; + } + } + } + return null; + } + + /// Hit-tests a point against a given layout and optional page number. + PdfPageHitTestResult? _hitTestWithLayout({ + required Offset point, + required PdfPageLayout layout, + required int pageIndex, + }) { + final pages = _document?.pages; + if (pages == null) return null; + if (pageIndex >= layout.pageLayouts.length) { + return null; + } + + final rect = layout.pageLayouts[pageIndex]; + if (rect.contains(point)) { + final page = pages[pageIndex]; + final local = point - rect.topLeft; + final pdfOffset = local.toPdfPoint(page: page, scaledPageSize: rect.size); + return PdfPageHitTestResult(page: page, offset: pdfOffset); + } else { + return null; + } + } + + // Attempts to nudge the point to find the nearest page content. + // Intelligently determines nudge direction based on page layout and visible rect. + ({Offset point, PdfPageHitTestResult hit})? _nudgeHitTest(Offset start, {PdfPageLayout? layout, int? pageIndex}) { + const epsViewPx = 1.0; + final epsDoc = epsViewPx / _currentZoom; + + final useLayout = layout ?? _layout; + if (useLayout == null) return null; + + // Try the original point first + final initialResult = pageIndex != null + ? _hitTestWithLayout(point: start, layout: useLayout, pageIndex: pageIndex) + : _getPdfPageHitTestResult(start, useDocumentLayoutCoordinates: true); + if (initialResult != null) { + return (point: Offset.zero, hit: initialResult); + } + + // Find the nearest page by checking which page rect is closest to the start point + Rect? nearestPageRect; + int? nearestPageIndex; + var minDistance = double.infinity; + + for (var i = 0; i < useLayout.pageLayouts.length; i++) { + final pageRect = useLayout.pageLayouts[i]; + + // Calculate distance from point to page rect + final dx = start.dx < pageRect.left + ? pageRect.left - start.dx + : start.dx > pageRect.right + ? start.dx - pageRect.right + : 0.0; + final dy = start.dy < pageRect.top + ? pageRect.top - start.dy + : start.dy > pageRect.bottom + ? start.dy - pageRect.bottom + : 0.0; + final distance = dx * dx + dy * dy; // Squared distance (no need for sqrt for comparison) + + if (distance < minDistance) { + minDistance = distance; + nearestPageRect = pageRect; + nearestPageIndex = i; + } + } + + if (nearestPageRect == null || nearestPageIndex == null) return null; + + // Determine the direction to nudge based on where start is relative to the nearest page + double nudgeDx = 0; + double nudgeDy = 0; + + if (start.dx < nearestPageRect.left) { + // Point is to the left of the page - nudge right + nudgeDx = nearestPageRect.left - start.dx + epsDoc; + } else if (start.dx > nearestPageRect.right) { + // Point is to the right of the page - nudge left + nudgeDx = nearestPageRect.right - start.dx - epsDoc; + } /*else { + // Point is horizontally within page bounds - nudge slightly right to ensure we're inside + nudgeDx = epsDoc; + } */ + + if (start.dy < nearestPageRect.top) { + // Point is above the page - nudge down + nudgeDy = nearestPageRect.top - start.dy + epsDoc; + } else if (start.dy > nearestPageRect.bottom) { + // Point is below the page - nudge up + nudgeDy = nearestPageRect.bottom - start.dy - epsDoc; + } /* else { + // Point is vertically within page bounds - nudge slightly down to ensure we're inside + nudgeDy = epsDoc; + } */ + + final nudgeOffset = Offset(nudgeDx, nudgeDy); + final tryPoint = start.translate(nudgeDx, nudgeDy); + + final result = _hitTestWithLayout(point: tryPoint, layout: useLayout, pageIndex: nearestPageIndex); + if (result != null) { + return (point: nudgeOffset, hit: result); + } + + /* // If that didn't work, try nudging to the top-left corner of the nearest page + final cornerOffset = Offset( + nearestPageRect.left + epsDoc - start.dx, + nearestPageRect.top + epsDoc - start.dy, + ); + final cornerPoint = Offset(nearestPageRect.left + epsDoc, nearestPageRect.top + epsDoc); + + final cornerResult = _hitTestWithLayout(point: cornerPoint, layout: useLayout, pageIndex: nearestPageIndex); + if (cornerResult != null) { + return (point: cornerOffset, hit: cornerResult); + } */ + + return null; + } + + void _startInteraction() { + _interactionEndedTimer?.cancel(); + _interactionEndedTimer = null; + _isInteractionGoingOn = true; + } + + void _stopInteraction() { + _interactionEndedTimer?.cancel(); + if (!mounted) return; + _interactionEndedTimer = Timer(const Duration(milliseconds: 300), () { + _isInteractionGoingOn = false; + _invalidate(); + }); + } + + // State for discrete page transitions + Matrix4? _interactionStartMatrix; + double? _interactionStartScale; + int? _interactionStartPage; + bool _hadScaleChangeInInteraction = false; + Offset _lastPanDelta = Offset.zero; + + Future _onInteractionEnd(ScaleEndDetails details) async { + _isActiveGesture = false; + _isActivelyZooming = false; + + widget.params.onInteractionEnd?.call(details); + + final shouldHandleDiscrete = + (widget.params.pageTransition == PageTransition.discrete && + (!_hadScaleChangeInInteraction && !_hasActiveAnimations)); + + if (shouldHandleDiscrete) { + await _handleDiscretePageTransition(details); + } + + _interactionStartMatrix = null; + _interactionStartScale = null; + _stopInteraction(); + } + + void _onInteractionStart(ScaleStartDetails details) { + _startInteraction(); + _requestFocus(); + _isActiveGesture = true; // User is now actively dragging + if (widget.params.pageTransition == PageTransition.discrete) { + _interactionStartMatrix = _txController.value.clone(); + _interactionStartScale = _currentZoom; + _interactionStartPage = _pageNumber; + _hadScaleChangeInInteraction = false; // Reset for new interaction + _lastPanDelta = Offset.zero; // Reset pan delta for new interaction + } + widget.params.onInteractionStart?.call(details); + } + + void _onInteractionUpdate(ScaleUpdateDetails details) { + // Track pan delta for boundary extension logic + _lastPanDelta = details.focalPointDelta; + + // Track if scale changed during the interaction + if (widget.params.pageTransition == PageTransition.discrete && _interactionStartScale != null) { + final currentScale = _currentZoom; + final scaleChanged = (_interactionStartScale! - currentScale).abs() > 0.01; + if (scaleChanged) { + _hadScaleChangeInInteraction = true; + _isActivelyZooming = true; + _hasActiveAnimations = true; + } + } + widget.params.onInteractionUpdate?.call(details); + } + + void _onAnimationEnd() { + // Check if all animations have completed + final hasAnimations = (_interactiveViewerState?.hasActiveAnimations == true) || _animController.isAnimating; + + if (!hasAnimations) { + // Animations complete, clear flags + _hasActiveAnimations = false; + _hadScaleChangeInInteraction = false; + setState(() {}); // Trigger repaint + } + } + + /// Last page number that is explicitly requested to go to. + int? _gotoTargetPageNumber; + + bool _onKey(PdfViewerKeyHandlerParams params, LogicalKeyboardKey key, bool isRealKeyPress) { + final result = widget.params.onKey?.call(params, key, isRealKeyPress); + if (result != null) { + return result; + } + // NOTE: repeatInterval should be shorter than the actual key repeat interval of the platform + // because the animation should finish before the next key event. + const repeatInterval = Duration(milliseconds: 100); + switch (key) { + case LogicalKeyboardKey.pageUp: + _goToPage(pageNumber: (_gotoTargetPageNumber ?? _pageNumber!) - 1, duration: repeatInterval); + return true; + case LogicalKeyboardKey.pageDown: + _goToPage(pageNumber: (_gotoTargetPageNumber ?? _pageNumber!) + 1, duration: repeatInterval); + return true; + case LogicalKeyboardKey.space: + final move = HardwareKeyboard.instance.isShiftPressed ? -1 : 1; + _goToPage(pageNumber: (_gotoTargetPageNumber ?? _pageNumber!) + move, duration: repeatInterval); + return true; + case LogicalKeyboardKey.home: + _goToPage(pageNumber: 1, duration: repeatInterval); + return true; + case LogicalKeyboardKey.end: + _goToPage(pageNumber: _document!.pages.length, anchor: widget.params.pageAnchorEnd, duration: repeatInterval); + return true; + case LogicalKeyboardKey.equal: + if (isCommandKeyPressed) { + _zoomUp(duration: repeatInterval); + return true; + } + case LogicalKeyboardKey.minus: + if (isCommandKeyPressed) { + _zoomDown(duration: repeatInterval); + return true; + } + case LogicalKeyboardKey.arrowDown: + _goToManipulated((m) => m.translateByDouble(0.0, -widget.params.scrollByArrowKey, 0, 1)); + return true; + case LogicalKeyboardKey.arrowUp: + _goToManipulated((m) => m.translateByDouble(0.0, widget.params.scrollByArrowKey, 0, 1)); + return true; + case LogicalKeyboardKey.arrowLeft: + _goToManipulated((m) => m.translateByDouble(widget.params.scrollByArrowKey, 0.0, 0, 1)); + return true; + case LogicalKeyboardKey.arrowRight: + _goToManipulated((m) => m.translateByDouble(-widget.params.scrollByArrowKey, 0.0, 0, 1)); + return true; + case LogicalKeyboardKey.keyA: + if (isCommandKeyPressed) { + selectAllText(); + return true; + } + case LogicalKeyboardKey.keyC: + if (isCommandKeyPressed) { + _copyTextSelection(); + return true; + } + } + return false; + } + + void _goToManipulated(void Function(Matrix4 m) manipulate) { + final m = _txController.value.clone(); + manipulate(m); + _txController.value = _makeMatrixInSafeRange(m, forceClamp: true); + } + + /// Handles discrete page transition logic when interaction ends. + /// + /// Determines whether to snap back to current page/spread or advance to next/previous + /// based on swipe velocity or drag threshold (50% of page/spread width). + Future _handleDiscretePageTransition(ScaleEndDetails details) async { + final startMatrix = _interactionStartMatrix; + if (startMatrix == null || _layout == null || _pageNumber == null || _interactionStartPage == null) { + return; + } + + final currentPage = _interactionStartPage!; + final layout = _layout!; + final isPrimaryVertical = layout.primaryAxis == Axis.vertical; + + // Calculate viewport movement for threshold-based page transitions + final startRect = startMatrix.calcVisibleRect(_viewSize!); + final endRect = _txController.value.calcVisibleRect(_viewSize!); + final verticalDelta = endRect.top - startRect.top; + final horizontalDelta = endRect.left - startRect.left; + final scrollDelta = isPrimaryVertical ? verticalDelta : horizontalDelta; + + // Extract velocity components + final scrollVelocity = isPrimaryVertical + ? details.velocity.pixelsPerSecond.dy + : details.velocity.pixelsPerSecond.dx; + final crossAxisVelocity = isPrimaryVertical + ? details.velocity.pixelsPerSecond.dx + : details.velocity.pixelsPerSecond.dy; + + // Ignore cross-axis dominant movements, unless there's significant velocity on primary axis + // Use velocity (not delta) to detect user's intent at release time + const minFlingVelocity = 50.0; + final hasSignificantVelocity = scrollVelocity.abs() > minFlingVelocity; + if (!hasSignificantVelocity && crossAxisVelocity.abs() > scrollVelocity.abs() * 3) { + return; + } + + // Stop animation before making snap decision + _stopInteractiveViewerAnimation(); + await Future.delayed(const Duration(milliseconds: 5)); + + // Determine target page based on fling velocity or drag threshold + int targetPage; + if (hasSignificantVelocity) { + // Check if velocity direction matches drag direction to detect snapback + // scrollDelta: positive = viewport moved down/right, negative = viewport moved up/left + // scrollVelocity: positive = finger moving down/right → viewport moves opposite direction + // So we need to INVERT velocity to get viewport movement direction + final dragDirection = scrollDelta > 0 ? 1 : (scrollDelta < 0 ? -1 : 0); + final velocityDirection = scrollVelocity > 0 + ? -1 + : 1; // INVERTED: positive velocity = viewport moves up (previous page) + + // If velocity contradicts drag direction, this is likely a snapback - ignore velocity + if (dragDirection != 0 && dragDirection != velocityDirection) { + targetPage = _getTargetPageBasedOnThreshold(currentPage, scrollDelta, isPrimaryVertical); + } else { + // Velocity matches drag direction - use velocity for page transition + // Only advance if at boundary when fling started + if (_isAtBoundary(startRect, currentPage, layout, isPrimaryVertical, velocityDirection)) { + targetPage = _getAdjacentPage(currentPage, layout, velocityDirection); + } else { + return; // Not at boundary - let InteractiveViewer handle fling + } + } + } else { + // No fling - use visible page area threshold + targetPage = _getTargetPageBasedOnThreshold(currentPage, scrollDelta, isPrimaryVertical); + } + + final snapAnchor = _getSnapAnchor( + targetPageNumber: targetPage, + currentPageNumber: currentPage, + layout: layout, + isPrimaryVertical: isPrimaryVertical, + endRect: endRect, + ); + + await _snapToPage(targetPage, anchor: snapAnchor, currentPageNumber: currentPage); + } + + /// Checks if viewport is at a page boundary in the given direction. + bool _isAtBoundary(Rect visibleRect, int pageNumber, PdfPageLayout layout, bool isPrimaryVertical, int direction) { + final isMovingForward = direction > 0; + + final pageRect = _getEffectivePageBounds(pageNumber, layout); + final currentScale = _txController.value.getMaxScaleOnAxis(); + // Increase tolerance for clamping physics - user may not be able to get exactly to the boundary + const baseTolerance = 10; // pixels in screen space + final tolerance = baseTolerance * currentScale; // scale to document coordinates + + // Check appropriate boundary based on direction + if (isMovingForward) { + return isPrimaryVertical + ? visibleRect.bottom >= pageRect.bottom - tolerance + : visibleRect.right >= pageRect.right - tolerance; + } else { + return isPrimaryVertical + ? visibleRect.top <= pageRect.top + tolerance + : visibleRect.left <= pageRect.left + tolerance; + } + } + + /// Gets the adjacent page/spread in the given direction. + int _getAdjacentPage(int currentPage, PdfPageLayout layout, int direction) { + if (layout is PdfSpreadLayout) { + final currentSpreadIndex = layout.pageToSpreadIndex[currentPage - 1]; // Convert to 0-based + final nextSpreadIndex = currentSpreadIndex + direction; + + return layout.getFirstPageOfSpread(nextSpreadIndex) ?? currentPage; + } else { + final candidatePage = currentPage + direction; + return candidatePage.clamp(1, _document!.pages.length); + } + } + + /// Determines the snap anchor based on target page and current position. + PdfPageAnchor _getSnapAnchor({ + required int targetPageNumber, + required int currentPageNumber, + required PdfPageLayout layout, + required bool isPrimaryVertical, + required Rect endRect, + }) { + // Check if page/spread overflows on primary and cross axes at current zoom + final pageRect = layout.getSpreadBounds(targetPageNumber); + + final pagePrimarySize = isPrimaryVertical ? pageRect.height : pageRect.width; + final pageCrossSize = isPrimaryVertical ? pageRect.width : pageRect.height; + final viewportPrimarySize = isPrimaryVertical ? _viewSize!.height : _viewSize!.width; + final viewportCrossSize = isPrimaryVertical ? _viewSize!.width : _viewSize!.height; + + final primaryOverflows = pagePrimarySize * _currentZoom > viewportPrimarySize; + final crossOverflows = pageCrossSize * _currentZoom > viewportCrossSize; + + if (!primaryOverflows && !crossOverflows) { + // Page fits entirely - center it + return PdfPageAnchor.center; + } + + // Page overflows on at least one axis + if (targetPageNumber == currentPageNumber) { + // Staying on current page - snap to nearest edge + final pageCenter = isPrimaryVertical + ? (pageRect.top + pageRect.bottom) / 2 + : (pageRect.left + pageRect.right) / 2; + final visibleCenter = isPrimaryVertical ? (endRect.top + endRect.bottom) / 2 : (endRect.left + endRect.right) / 2; + + if (isPrimaryVertical) { + // If cross overflows, anchor to top-left or bottom-left instead of centering + return crossOverflows + ? (visibleCenter < pageCenter ? PdfPageAnchor.topLeft : PdfPageAnchor.bottomLeft) + : (visibleCenter < pageCenter ? PdfPageAnchor.topCenter : PdfPageAnchor.bottomCenter); + } else { + // If cross overflows, anchor to top-left or top-right instead of centering + return crossOverflows + ? (visibleCenter < pageCenter ? PdfPageAnchor.topLeft : PdfPageAnchor.topRight) + : (visibleCenter < pageCenter ? PdfPageAnchor.centerLeft : PdfPageAnchor.centerRight); + } + } else { + // Advancing to different page - snap to entry edge + if (isPrimaryVertical) { + // If cross overflows, anchor to top-left/bottom-left instead of top-center/bottom-center + return crossOverflows + ? (targetPageNumber > currentPageNumber ? PdfPageAnchor.topLeft : PdfPageAnchor.bottomLeft) + : (targetPageNumber > currentPageNumber ? PdfPageAnchor.topCenter : PdfPageAnchor.bottomCenter); + } else { + // If cross overflows, anchor to top-left/top-right instead of center-left/center-right + return crossOverflows + ? (targetPageNumber > currentPageNumber ? PdfPageAnchor.topLeft : PdfPageAnchor.topRight) + : (targetPageNumber > currentPageNumber ? PdfPageAnchor.centerLeft : PdfPageAnchor.centerRight); + } + } + } + + /// Determines target page based on visible area of pages. + /// + /// Transitions to whichever page has the most visible area on the primary axis. + /// This works correctly at any zoom level, including when zoomed in. + int _getTargetPageBasedOnThreshold(int currentPageNumber, double scrollDelta, bool isPrimaryVertical) { + final layout = _layout!; + final visibleRect = _txController.value.calcVisibleRect(_viewSize!); + + // Calculate visible area for current page and adjacent pages + final currentPageBounds = layout.getSpreadBounds(currentPageNumber); + final currentPageVisibleArea = _calcPageIntersectionArea(visibleRect, currentPageBounds, isPrimaryVertical); + + // Check previous page (if exists) + double prevPageVisibleArea = 0; + if (currentPageNumber >= 2) { + final prevPageBounds = layout.pageLayouts[currentPageNumber - 2]; + prevPageVisibleArea = _calcPageIntersectionArea(visibleRect, prevPageBounds, isPrimaryVertical); + } + + // Check next page (if exists) + double nextPageVisibleArea = 0; + if (currentPageNumber < layout.pageLayouts.length) { + final nextPageBounds = layout.pageLayouts[currentPageNumber]; + nextPageVisibleArea = _calcPageIntersectionArea(visibleRect, nextPageBounds, isPrimaryVertical); + } + + // Transition to the page with the most visible area + if (prevPageVisibleArea > currentPageVisibleArea && prevPageVisibleArea > nextPageVisibleArea) { + return _getAdjacentPage(currentPageNumber, layout, -1); + } else if (nextPageVisibleArea > currentPageVisibleArea && nextPageVisibleArea > prevPageVisibleArea) { + return _getAdjacentPage(currentPageNumber, layout, 1); + } + + // Current page has most visible area - snap back to current page + return currentPageNumber; + } + + /// Calculate the visible intersection area on the primary axis between visible rect and page bounds. + double _calcPageIntersectionArea(Rect visibleRect, Rect pageBounds, bool isPrimaryVertical) { + final intersection = visibleRect.intersect(pageBounds); + if (intersection.isEmpty) { + return 0; + } + // Return the primary axis length of intersection (not full area) + // This gives us how much of the page is visible on the scroll axis + return isPrimaryVertical ? intersection.height : intersection.width; + } + + /// Snaps to the target page/spread with animation. + Future _snapToPage(int targetPageNumber, {required PdfPageAnchor anchor, int? currentPageNumber}) async { + final duration = const Duration(milliseconds: 400); + + // Only reset scale when advancing to a different page, not when snapping back to current page + final isAdvancingToNewPage = currentPageNumber != null && targetPageNumber != currentPageNumber; + + if (!isAdvancingToNewPage) { + // let InteractiveViewer's scroll physics handle snap back to current page + return; + } + + if (isAdvancingToNewPage && _viewSize != null) { + // Calculate fit scale for the target page + _calcFitScale(targetPageNumber); + _adjustBoundaryMargins(_viewSize!, _fitScale); + } + + // Check if the target page fits in viewport and use centered anchor if so + var effectiveAnchor = anchor; + final layout = _layout; + if (layout != null && _viewSize != null && anchor != PdfPageAnchor.center) { + // Calculate what the scale will be for this page + final targetScale = _fitScale; + final isPrimaryVertical = layout.primaryAxis == Axis.vertical; + final pageRect = layout.getSpreadBounds(targetPageNumber); + + final pagePrimarySize = isPrimaryVertical ? pageRect.height : pageRect.width; + final pageCrossSize = isPrimaryVertical ? pageRect.width : pageRect.height; + final viewportPrimarySize = isPrimaryVertical ? _viewSize!.height : _viewSize!.width; + final viewportCrossSize = isPrimaryVertical ? _viewSize!.width : _viewSize!.height; + + final primaryFits = pagePrimarySize * targetScale <= viewportPrimarySize; + final crossFits = pageCrossSize * targetScale <= viewportCrossSize; + + // Only use center anchor if page fits on BOTH axes + if (primaryFits && crossFits) { + effectiveAnchor = PdfPageAnchor.center; + } + } + + final targetZoom = _fitScale; + + _setCurrentPageNumber(targetPageNumber, targetZoom: targetZoom, doSetState: true); + + final targetMatrix = _calcMatrixForPage( + pageNumber: targetPageNumber, + anchor: effectiveAnchor, + forceScale: targetZoom, + ); + + await _goTo(targetMatrix, duration: duration, curve: Curves.easeInOutCubic); + + _onAnimationEnd(); + } + + Rect get _visibleRect => _txController.value.calcVisibleRect(_viewSize!); + + /// Set the current page number. + /// + /// Please note that the function does not scroll/zoom to the specified page but changes the current page number. + void _setCurrentPageNumber(int? pageNumber, {bool doSetState = false, double? targetZoom}) { + if (pageNumber != null && _pageNumber != pageNumber) { + _pageNumber = pageNumber; + // Update boundary margins for the new page (for discrete mode with overflow) + // Use targetZoom if provided (for transitions), otherwise use current zoom + if (_viewSize != null) { + _adjustBoundaryMargins(_viewSize!, targetZoom ?? _currentZoom); + } + if (doSetState) { + _invalidate(); + } + if (widget.params.onPageChanged != null) { + Future.microtask(() => widget.params.onPageChanged?.call(_pageNumber)); + } + } + } + + double _calcPrimaryAxisVisibility(Rect pageRect, Rect viewportRect, bool isHorizontal) { + if (isHorizontal) { + if (pageRect.right <= viewportRect.left || pageRect.left >= viewportRect.right) return 0.0; + final visibleLeft = pageRect.left < viewportRect.left ? viewportRect.left : pageRect.left; + final visibleRight = pageRect.right > viewportRect.right ? viewportRect.right : pageRect.right; + return ((visibleRight - visibleLeft) / pageRect.width).clamp(0.0, 1.0); + } else { + if (pageRect.bottom <= viewportRect.top || pageRect.top >= viewportRect.bottom) return 0.0; + final visibleTop = pageRect.top < viewportRect.top ? viewportRect.top : pageRect.top; + final visibleBottom = pageRect.bottom > viewportRect.bottom ? viewportRect.bottom : pageRect.bottom; + return ((visibleBottom - visibleTop) / pageRect.height).clamp(0.0, 1.0); + } + } + + double _calcPageIntersectionPercentage(int pageNumber, Rect visibleRect) { + final rect = _layout!.pageLayouts[pageNumber - 1]; + final intersection = rect.intersect(visibleRect); + if (intersection.isEmpty) return 0; + final area = intersection.width * intersection.height; + return area / (rect.width * rect.height); + } + + static const double _kPageIntersectionThreshold = 0.2; + + int? _guessCurrentPageNumber() { + if (_layout == null || _viewSize == null) { + _visiblePageRange = null; + return null; + } + final visibleRect = _visibleRect; + _updateVisiblePageRange(visibleRect); + if (widget.params.calculateCurrentPageNumber != null) { + final pageNumber = widget.params.calculateCurrentPageNumber!(visibleRect, _layout!.pageLayouts, _controller!); + return pageNumber; + } + + final layout = _layout!; + final isHorizontal = + layout.primaryAxis == Axis.horizontal || layout.documentSize.width > layout.documentSize.height; + final isSimple = _isSimpleLayout(layout, isHorizontal); + + // Calculate primary axis visibility for all pages (map: page number -> visibility %) + final visible = {}; + for (var i = 0; i < layout.pageLayouts.length; i++) { + final pct = _calcPrimaryAxisVisibility(layout.pageLayouts[i], visibleRect, isHorizontal); + if (pct > 0) visible[i + 1] = pct; + } + + if (visible.isEmpty) return _pageNumber ?? 1; + + final current = _pageNumber; + final fullyVisiblePages = visible.entries.where((e) => e.value >= 1.0).map((e) => e.key).toList(); + + // 3+ fully visible pages to enter scroll percentage mode, <2 to exit + // to stop flapping between modes + if (_usingScrollPercentageMode) { + if (fullyVisiblePages.length < 2 && isSimple) _usingScrollPercentageMode = false; + } else { + if (fullyVisiblePages.length >= 3 || !isSimple) _usingScrollPercentageMode = true; + } + + // Check goto target + final gotoVisibility = visible[_gotoTargetPageNumber] ?? 0.0; + if (gotoVisibility >= 0.5) return _gotoTargetPageNumber; + if (_gotoTargetPageNumber != null && !_animController.isAnimating) _gotoTargetPageNumber = null; + + // Scroll percentage mode + if (_usingScrollPercentageMode) { + final scrollPosition = isHorizontal ? visibleRect.left : visibleRect.top; + final scrollLength = + (isHorizontal ? layout.documentSize.width : layout.documentSize.height) - + (isHorizontal ? visibleRect.width : visibleRect.height); + final scrollPercentage = scrollLength > 0 ? (scrollPosition / scrollLength).clamp(0.0, 1.0) : 0.0; + return ((scrollPercentage * layout.pageLayouts.length).floor() + 1).clamp(1, layout.pageLayouts.length); + } + + // Sticky mode - prefer current page if it is fully visible as long + // as the first or last page is not fully visible also + if (current != null) { + final currentPercentage = visible[current] ?? 0.0; + if (currentPercentage >= 1.0) { + // Edge detection: prefer first/last page if also fully visible + if (fullyVisiblePages.contains(1) && current != 1) return 1; + if (fullyVisiblePages.contains(layout.pageLayouts.length) && current != layout.pageLayouts.length) { + return layout.pageLayouts.length; + } + return current; + } + // Most visible with proximity tie-break + var maxPercentage = 0.0, maxPage = visible.keys.first; + for (final entry in visible.entries) { + if (entry.value > maxPercentage || + (entry.value == maxPercentage && (current - entry.key).abs() < (current - maxPage).abs())) { + maxPercentage = entry.value; + maxPage = entry.key; + } + } + return maxPage; + } + + // Should not reach here, but fallback to most visible + return visible.entries.reduce((a, b) => a.value > b.value ? a : b).key; + } + + /// Detect if layout is "simple" (sequential pages) vs "complex" (facing pages) + /// In facing pages, multiple pages can share the same PRIMARY SCROLL axis coordinate + bool _isSimpleLayout(PdfPageLayout layout, bool isHorizontal) { + final pageLayouts = layout.pageLayouts; + if (layout is PdfSpreadLayout) { + // Spread layouts are always complex + return false; + } + if (layout is SequentialPagesLayout) { + // Sequential layouts are always simple + return true; + } + + if (pageLayouts.length <= 1) return true; + + // Check if any two consecutive pages overlap along the PRIMARY SCROLL axis + // For horizontal layouts (scroll left-right), check if pages overlap horizontally (same x-range = facing) + // For vertical layouts (scroll up-down), check if pages overlap vertically (same y-range = facing) + for (var i = 0; i < pageLayouts.length - 1; i++) { + final page1 = pageLayouts[i]; + final page2 = pageLayouts[i + 1]; + + if (isHorizontal) { + // Horizontal scroll: pages are "complex" if they overlap horizontally (facing pages side-by-side) + // In sequential horizontal, page2.left should be >= page1.right (no overlap) + if (page2.left < page1.right - 1.0) { + return false; // Overlap detected = facing pages + } + } else { + // Vertical scroll: pages are "complex" if they overlap vertically (facing pages top-bottom) + // In sequential vertical, page2.top should be >= page1.bottom (no overlap) + if (page2.top < page1.bottom - 1.0) { + return false; // Overlap detected = facing pages + } + } + } + + return true; + } + + /// Calculate the range of all pages that have any intersection with the visible viewport. + void _updateVisiblePageRange(Rect visibleRect) { + if (_layout == null || _document == null) { + _visiblePageRange = null; + return; + } + + int? firstVisible; + int? lastVisible; + for (var i = 1; i <= _document!.pages.length; i++) { + final ratio = _calcPageIntersectionPercentage(i, visibleRect); + if (ratio > _kPageIntersectionThreshold) { + firstVisible ??= i; + lastVisible = i; + } + } + + if (firstVisible != null && lastVisible != null) { + _visiblePageRange = PdfPageRange(firstVisible, lastVisible); + } else { + _visiblePageRange = null; + } + } + + /// Returns true if page layouts are changed. + bool _relayoutPages() { + if (_document == null) { + _layout = null; + return false; + } + + final helper = PdfLayoutHelper.fromParams(widget.params, viewSize: _viewSize ?? Size.zero); + var newLayout = (widget.params.layoutPages ?? _layoutPages)(_document!.pages, widget.params, helper); + + // In discrete mode, add spacing between pages to fill viewport and prevent neighboring pages from showing + if (widget.params.pageTransition == PageTransition.discrete) { + newLayout = _addDiscreteSpacing(newLayout, helper); + } + + // Only update if layout actually changed + if (_layout == newLayout) { + return false; + } + + _layout = newLayout; + return true; + } + + double _getInitialZoom() { + if (_viewSize != null && _layout != null) { + final params = widget.params; + + // In discrete mode, pages are already scaled in the layout, so zoom should be 1.0 + if (params.pageTransition == PageTransition.discrete) { + return 1.0; + } + + // For continuous mode, calculate fit scale for the current fitMode + return _calculateScaleForMode(params.fitMode); + } + // Fall back to fitScale + return _fitScale; + } + + /// Calculate scale for a specific fit mode on-demand. + /// Used for backward compatibility with deprecated APIs. + double _calculateScaleForMode(FitMode mode) { + if (_viewSize == null || _layout == null) { + return _defaultMinScale; + } + + final params = widget.params; + final helper = PdfLayoutHelper.fromParams(params, viewSize: _viewSize!); + + return _layout!.calculateFitScale(helper, mode); + } + + void _calcFitScale([int? pageNumber]) { + if (_viewSize == null || _layout == null) { + _fitScale = _defaultMinScale; + return; + } + + final helper = PdfLayoutHelper.fromParams(widget.params, viewSize: _viewSize!); + final effectivePageNumber = pageNumber ?? _pageNumber ?? _gotoTargetPageNumber; + if (widget.params.useAlternativeFitScaleAsMinScale) { + // Legacy useAlternativeFitScaleAsMinScale behavior (deprecated) + // This maps to FitMode.fit (show whole page) + _fitScale = _layout!.calculateFitScale( + helper, + FitMode.fit, + pageTransition: widget.params.pageTransition, + pageNumber: effectivePageNumber, + ); + } else { + // Calculate fit scale based on fitMode for page positioning + // In discrete mode, calculate fit scale for the current/target page + // In continuous mode, calculate for the entire document + _fitScale = _layout!.calculateFitScale( + helper, + widget.params.fitMode, + pageTransition: widget.params.pageTransition, + pageNumber: effectivePageNumber, + ); + } + } + + void _calcZoomStopTable() { + _zoomStops.clear(); + double z; + + // Calculate both fit and cover scales to provide good zoom stops + // even when only one fitMode is selected + final fitScale = _calculateScaleForMode(FitMode.fit); + final coverScale = _calculateScaleForMode(FitMode.fill); + + // Use the primary scale based on current fitMode (or legacy behavior) + final primaryScale = _fitScale; + + // Add both scales to zoom stops if they're significantly different + if (!_areZoomsAlmostIdentical(fitScale, coverScale)) { + if (fitScale < coverScale) { + _zoomStops.add(fitScale); + z = coverScale; + } else { + _zoomStops.add(coverScale); + z = fitScale; + } + } else { + z = primaryScale; + } + + // in some case, z may be 0 and it causes infinite loop. + if (z < 1 / 128) { + _zoomStops.add(1.0); + return; + } + while (z < widget.params.maxScale) { + _zoomStops.add(z); + z *= 2; + } + if (!_areZoomsAlmostIdentical(z, widget.params.maxScale)) { + _zoomStops.add(widget.params.maxScale); + } + + // Add smaller zoom stops down to the minimum scale + if (widget.params.minScale != null || !widget.params.useAlternativeFitScaleAsMinScale) { + z = _zoomStops.first; + + while (z > minScale) { + z /= 2; + _zoomStops.insert(0, z); + } + if (!_areZoomsAlmostIdentical(z, minScale)) { + _zoomStops.insert(0, minScale); + } + } + } + + double _findNextZoomStop(double zoom, {required bool zoomUp, bool loop = true}) { + if (zoomUp) { + for (final z in _zoomStops) { + if (z > zoom && !_areZoomsAlmostIdentical(z, zoom)) return z; + } + if (loop) { + return _zoomStops.first; + } else { + return _zoomStops.last; + } + } else { + for (var i = _zoomStops.length - 1; i >= 0; i--) { + final z = _zoomStops[i]; + if (z < zoom && !_areZoomsAlmostIdentical(z, zoom)) return z; + } + if (loop) { + return _zoomStops.last; + } else { + return _zoomStops.first; + } + } + } + + static bool _areZoomsAlmostIdentical(double z1, double z2) => (z1 - z2).abs() < 0.01; + + /// Adds spacing between pages in discrete mode to fill viewport and prevent neighboring pages from showing. + /// This modifies the page positions to add viewport-sized gaps on the primary scroll axis. + /// For spread layouts, positions spreads as units rather than individual pages. + PdfPageLayout _addDiscreteSpacing(PdfPageLayout layout, PdfLayoutHelper helper) { + final isPrimaryVertical = layout.primaryAxis == Axis.vertical; + final viewportSize = isPrimaryVertical ? helper.viewHeight : helper.viewWidth; + final margin = widget.params.margin; + + final newPageLayouts = []; + var offset = 0.0; + + // For spread layouts, we need to iterate through spreads, not individual pages + if (layout is PdfSpreadLayout) { + // Track which pages we've already positioned + final processedPages = {}; + + for (var pageNum = 1; pageNum <= layout.pageLayouts.length; pageNum++) { + if (processedPages.contains(pageNum)) continue; + + // Get all pages in this spread + final range = layout.getPageRange(pageNum); + final spreadBounds = layout.getSpreadBounds(pageNum); + + // Calculate scale for this spread + final spreadScale = layout.calculateFitScale( + helper, + widget.params.fitMode, + pageTransition: PageTransition.discrete, + pageNumber: pageNum, + ); + + // Get spread size on primary axis, scaled to final rendered size + final scaledSpreadWidthWithMargins = (spreadBounds.width + margin * 2) * spreadScale; + final scaledSpreadHeightWithMargins = (spreadBounds.height + margin * 2) * spreadScale; + final spreadSizeWithMargins = isPrimaryVertical ? scaledSpreadHeightWithMargins : scaledSpreadWidthWithMargins; + + // Calculate slot size: if spread+margins fits in viewport, use viewport to add centering padding + final slotSize = spreadSizeWithMargins < viewportSize ? viewportSize : spreadSizeWithMargins; + + // Calculate viewport padding to center the spread in its slot on primary axis + final viewportPaddingPrimary = (slotSize - spreadSizeWithMargins) / 2; + + // The spread content (without margins) starts at: offset + padding + scaled margin + final spreadContentStart = offset + viewportPaddingPrimary + (margin * spreadScale); + + // Center the spread on cross-axis + final crossAxisSpreadSizeWithMargins = isPrimaryVertical + ? scaledSpreadWidthWithMargins + : scaledSpreadHeightWithMargins; + final crossAxisViewport = isPrimaryVertical ? helper.viewWidth : helper.viewHeight; + + // Calculate viewport padding on cross-axis + final viewportPaddingCross = max(0.0, (crossAxisViewport - crossAxisSpreadSizeWithMargins) / 2); + + // Spread content starts at: padding + scaled margin + final crossAxisSpreadContentStart = viewportPaddingCross + (margin * spreadScale); + + // Position all pages in this spread + for (var p = range.firstPageNumber; p <= range.lastPageNumber; p++) { + final pageRect = layout.pageLayouts[p - 1]; + final scaledPageWidth = pageRect.width * spreadScale; + final scaledPageHeight = pageRect.height * spreadScale; + + // Calculate page position relative to spread bounds on both axes + final pageOffsetInSpreadPrimary = isPrimaryVertical + ? pageRect.top - spreadBounds.top + : pageRect.left - spreadBounds.left; + final pageOffsetInSpreadCross = isPrimaryVertical + ? pageRect.left - spreadBounds.left + : pageRect.top - spreadBounds.top; + + final scaledPageOffsetInSpreadPrimary = pageOffsetInSpreadPrimary * spreadScale; + final scaledPageOffsetInSpreadCross = pageOffsetInSpreadCross * spreadScale; + + // Position pages relative to the centered spread content position on both axes + final newRect = isPrimaryVertical + ? Rect.fromLTWH( + crossAxisSpreadContentStart + + scaledPageOffsetInSpreadCross, // Horizontal: relative to centered spread content + spreadContentStart + scaledPageOffsetInSpreadPrimary, // Vertical: relative to centered spread content + scaledPageWidth, + scaledPageHeight, + ) + : Rect.fromLTWH( + spreadContentStart + + scaledPageOffsetInSpreadPrimary, // Horizontal: relative to centered spread content + crossAxisSpreadContentStart + + scaledPageOffsetInSpreadCross, // Vertical: relative to centered spread content + scaledPageWidth, + scaledPageHeight, + ); + + newPageLayouts.add(newRect); + processedPages.add(p); + } + + // Move offset for next spread + offset += slotSize; + } + } else { + // Single page layout - original logic + for (var i = 0; i < layout.pageLayouts.length; i++) { + final pageRect = layout.pageLayouts[i]; + + // Calculate the scale that will be applied to this page in discrete mode + final pageScale = layout.calculateFitScale( + helper, + widget.params.fitMode, + pageTransition: PageTransition.discrete, + pageNumber: i + 1, + ); + + // Get page size on primary axis, scaled to final rendered size + final scaledPageWidthWithMargins = (helper.getWidthWithMargins(pageRect.width)) * pageScale; + final scaledPageHeightWithMargins = (helper.getHeightWithMargins(pageRect.height)) * pageScale; + final pageSizeWithMargins = isPrimaryVertical ? scaledPageHeightWithMargins : scaledPageWidthWithMargins; + + // Calculate slot size: if page+margins fits in viewport, use viewport to add centering padding + final slotSize = pageSizeWithMargins < viewportSize ? viewportSize : pageSizeWithMargins; + + // Calculate viewport padding to center the page in its slot on primary axis + final viewportPaddingPrimary = (slotSize - pageSizeWithMargins) / 2; + + // The page content (without margins) starts at: offset + padding + scaled margin + final scaledPageWidth = pageRect.width * pageScale; + final scaledPageHeight = pageRect.height * pageScale; + final pageContentStart = offset + viewportPaddingPrimary /*+ (margin * pageScale) */; + + // Position page on cross-axis + final crossAxisPageSizeWithMargins = isPrimaryVertical + ? scaledPageWidthWithMargins + : scaledPageHeightWithMargins; + final crossAxisViewport = isPrimaryVertical ? helper.viewWidth : helper.viewHeight; + + // Calculate viewport padding on cross-axis + final viewportPaddingCross = max(0.0, (crossAxisViewport - crossAxisPageSizeWithMargins) / 2); + + // Page content starts at: padding + scaled margin + final crossAxisPageContentStart = viewportPaddingCross /*+ (margin * pageScale) */; + final newRect = isPrimaryVertical + ? Rect.fromLTWH(crossAxisPageContentStart, pageContentStart, scaledPageWidth, scaledPageHeight) + : Rect.fromLTWH(pageContentStart, crossAxisPageContentStart, scaledPageWidth, scaledPageHeight); + + newPageLayouts.add(newRect); + + offset += slotSize; + } + } + + // offset now represents the total document size on primary axis (including all padding and margins) + + // Calculate new document size + // On cross-axis, use viewport size to accommodate centered pages with different aspect ratios + final crossAxisViewportSize = isPrimaryVertical ? helper.viewWidth : helper.viewHeight; + + // Also consider the maximum page extent on cross-axis to ensure no clipping + final maxCrossAxisExtent = newPageLayouts.fold(0.0, (max, rect) { + final extent = isPrimaryVertical ? rect.right : rect.bottom; + return extent > max ? extent : max; + }); + + final crossAxisSize = max(crossAxisViewportSize, maxCrossAxisExtent); + + final newDocSize = isPrimaryVertical ? Size(crossAxisSize, offset) : Size(offset, crossAxisSize); + + // Preserve spread layout information if present + if (layout is PdfSpreadLayout) { + // Recalculate spread bounds based on new page positions + final newSpreadLayouts = []; + for (var spreadIndex = 0; spreadIndex < layout.spreadLayouts.length; spreadIndex++) { + var minLeft = double.infinity; + var minTop = double.infinity; + var maxRight = 0.0; + var maxBottom = 0.0; + + for (var pageIndex = 0; pageIndex < layout.pageToSpreadIndex.length; pageIndex++) { + if (layout.pageToSpreadIndex[pageIndex] == spreadIndex) { + final pageRect = newPageLayouts[pageIndex]; + minLeft = min(minLeft, pageRect.left); + minTop = min(minTop, pageRect.top); + maxRight = max(maxRight, pageRect.right); + maxBottom = max(maxBottom, pageRect.bottom); + } + } + + newSpreadLayouts.add(Rect.fromLTRB(minLeft, minTop, maxRight, maxBottom)); + } + + return PdfSpreadLayout( + pageLayouts: newPageLayouts, + documentSize: newDocSize, + spreadLayouts: newSpreadLayouts, + pageToSpreadIndex: layout.pageToSpreadIndex, + ); + } + + return PdfPageLayout(pageLayouts: newPageLayouts, documentSize: newDocSize); + } + + /// Returns the boundary rect for discrete mode (current page/spread bounds). + /// Used by boundaryProvider to restrict scrolling to current page. + Rect? _getDiscreteBoundaryRect(Rect visibleRect, Size childSize, {double? zoom}) { + final layout = _layout; + final currentPageNumber = _gotoTargetPageNumber ?? _pageNumber; + + if (layout == null || + currentPageNumber == null || + currentPageNumber < 1 || + currentPageNumber > layout.pageLayouts.length) { + return null; + } + + // Get base page/spread bounds + final baseBounds = layout.getSpreadBounds(currentPageNumber); + + // Add margin + var result = baseBounds.inflate(widget.params.margin); + + // Add boundary margins for discrete mode + final userBoundaryMargin = (widget.params.boundaryMargin ?? EdgeInsets.zero); + if (!userBoundaryMargin.containsInfinite && _viewSize != null) { + final isPrimaryVertical = layout.primaryAxis == Axis.vertical; + final currentZoom = zoom ?? _currentZoom; + // result = (userBoundaryMargin * currentZoom).inflateRect(baseBounds); + // Check if page content extends beyond viewport on each axis at current zoom + final primaryAxisSize = isPrimaryVertical ? result.height : result.width; + final crossAxisSize = isPrimaryVertical ? result.width : result.height; + final primaryAxisViewport = isPrimaryVertical ? _viewSize!.height : _viewSize!.width; + final crossAxisViewport = isPrimaryVertical ? _viewSize!.width : _viewSize!.height; + final scaledPrimaryAxisSize = primaryAxisSize * currentZoom; + final scaledCrossAxisSize = crossAxisSize * currentZoom; + + // For positive margins: only apply when content exceeds viewport to prevent unwanted scrolling + // For negative margins: always apply (they're meant to restrict boundaries regardless of zoom) + final needsPrimaryAxisMargin = + scaledPrimaryAxisSize >= primaryAxisViewport || + (isPrimaryVertical + ? (userBoundaryMargin.top < 0 || userBoundaryMargin.bottom < 0) + : (userBoundaryMargin.left < 0 || userBoundaryMargin.right < 0)); + final needsCrossAxisMargin = + scaledCrossAxisSize >= crossAxisViewport || + (isPrimaryVertical + ? (userBoundaryMargin.left < 0 || userBoundaryMargin.right < 0) + : (userBoundaryMargin.top < 0 || userBoundaryMargin.bottom < 0)); + + if (isPrimaryVertical) { + // Vertical layout: add top/bottom margins only if content exceeds viewport height + // Add left/right margins only if content exceeds viewport width + result = Rect.fromLTRB( + needsCrossAxisMargin ? result.left - userBoundaryMargin.left : result.left, + needsPrimaryAxisMargin ? result.top - userBoundaryMargin.top : result.top, + needsCrossAxisMargin ? result.right + userBoundaryMargin.right : result.right, + needsPrimaryAxisMargin ? result.bottom + userBoundaryMargin.bottom : result.bottom, + ); + } else { + // Horizontal layout: add left/right margins only if content exceeds viewport width + // Add top/bottom margins only if content exceeds viewport height + result = Rect.fromLTRB( + needsPrimaryAxisMargin ? result.left - userBoundaryMargin.left : result.left, + needsCrossAxisMargin ? result.top - userBoundaryMargin.top : result.top, + needsPrimaryAxisMargin ? result.right + userBoundaryMargin.right : result.right, + needsCrossAxisMargin ? result.bottom + userBoundaryMargin.bottom : result.bottom, + ); + } + } + + // Extend boundaries into adjacent pages during pan gestures + // This allows smooth page transitions when panning on the primary axis + final shouldExtendBoundaries = + _isActiveGesture && !_hadScaleChangeInInteraction && !_isActivelyZooming && _viewSize != null; + + if (shouldExtendBoundaries) { + final isPrimaryVertical = layout.primaryAxis == Axis.vertical; + const extensionRatio = 0.5; // Extend 50% into adjacent pages + + final viewportPrimarySize = isPrimaryVertical ? _viewSize!.height : _viewSize!.width; + + // Calculate extension distance based on viewport size (not page size) + // This ensures proper extension even when viewport is much larger than page (discrete mode with spacing) + final extensionDistance = viewportPrimarySize * extensionRatio; + + // Determine swipe direction to only extend boundaries in the direction being swiped + // This prevents unwanted scrolling beyond document boundaries on first/last pages + var shouldExtendToPrev = true; + var shouldExtendToNext = true; + + final panDelta = isPrimaryVertical ? _lastPanDelta.dy : _lastPanDelta.dx; + + if (currentPageNumber == 1) { + // First page: only extend upward/leftward if user is panning down/right + // This allows smooth transition toward page 2 but prevents scrolling before page 1 + shouldExtendToPrev = panDelta < 0; + } else if (currentPageNumber == layout.pageLayouts.length) { + // Last page: only extend downward/rightward if user is panning up/left + // This allows smooth transition toward previous page but prevents scrolling after last page + shouldExtendToNext = panDelta > 0; + } + + // Extend to previous page (if exists and should extend) + if (shouldExtendToPrev) { + if (isPrimaryVertical) { + final newTop = result.top - extensionDistance; + result = Rect.fromLTRB(result.left, newTop, result.right, result.bottom); + } else { + final newLeft = result.left - extensionDistance; + result = Rect.fromLTRB(newLeft, result.top, result.right, result.bottom); + } + } + + // Extend to next page (if exists and should extend) + if (shouldExtendToNext) { + if (isPrimaryVertical) { + final newBottom = result.bottom + extensionDistance; + result = Rect.fromLTRB(result.left, result.top, result.right, newBottom); + } else { + final newRight = result.right + extensionDistance; + result = Rect.fromLTRB(result.left, result.top, newRight, result.bottom); + } + } + } + return result; + } + + // Auto-adjust boundaries when content is smaller than the view, centering + // the content and ensuring InteractiveViewer's scrollPhysics works when specified + void _adjustBoundaryMargins(Size viewSize, double zoom) { + final boundaryMargin = widget.params.boundaryMargin ?? EdgeInsets.zero; + + // Discrete mode: restrict scrolling to current page/spread only + if (widget.params.pageTransition == PageTransition.discrete) { + final layout = _layout; + final currentPageNumber = _pageNumber; + + if (layout == null || + currentPageNumber == null || + currentPageNumber < 1 || + currentPageNumber > layout.pageLayouts.length) { + _adjustedBoundaryMargins = boundaryMargin; + return; + } + + // TODO: should we need _implicit margins if not SpreadLayout? + // If we don't need it, I once remove getPageRectWithMargins function. + // Rect pageBounds; + // if (layout is PdfSpreadLayout) { + // pageBounds = layout.getSpreadBounds(currentPageNumber); + // } else { + // pageBounds = layout.getPageRectWithMargins(currentPageNumber); + // } + final pageBounds = layout.getSpreadBounds(currentPageNumber); + + var left = -pageBounds.left; + var top = -pageBounds.top; + var right = pageBounds.right - layout.documentSize.width; + var bottom = pageBounds.bottom - layout.documentSize.height; + + _adjustedBoundaryMargins = EdgeInsets.fromLTRB(left, top, right, bottom); + return; + } + + // Continuous mode: add extra boundary margin to center content when zoomed out + if (boundaryMargin.containsInfinite) { + _adjustedBoundaryMargins = boundaryMargin; + return; + } + + final currentDocumentSize = boundaryMargin.inflateSize(_layout!.documentSize); + final effectiveWidth = currentDocumentSize.width * zoom; + final effectiveHeight = currentDocumentSize.height * zoom; + final extraWidth = effectiveWidth - viewSize.width; + final extraHeight = effectiveHeight - viewSize.height; + + final extraBoundaryHorizontal = extraWidth < 0 ? (-extraWidth / 2) / zoom : 0.0; + final extraBoundaryVertical = extraHeight < 0 ? (-extraHeight / 2) / zoom : 0.0; + + _adjustedBoundaryMargins = + boundaryMargin + + EdgeInsets.fromLTRB( + extraBoundaryHorizontal, + extraBoundaryVertical, + extraBoundaryHorizontal, + extraBoundaryVertical, + ); + } + + List _buildPageOverlayWidgets(BuildContext context) { + final renderBox = context.findRenderObject(); + if (renderBox is! RenderBox) return []; + + final linkWidgets = []; + final overlayWidgets = []; + final targetRect = _getCacheExtentRect(); + + for (var i = 0; i < _document!.pages.length; i++) { + final rect = _layout!.pageLayouts[i]; + final intersection = rect.intersect(targetRect); + if (intersection.isEmpty) continue; + + final page = _document!.pages[i]; + final rectExternal = _documentToRenderBox(rect, renderBox); + if (rectExternal != null) { + if (widget.params.linkHandlerParams == null && widget.params.linkWidgetBuilder != null) { + linkWidgets.add( + PdfPageLinksOverlay( + key: Key('#__pageLinks__:${page.pageNumber}'), + page: page, + pageRect: rectExternal, + params: widget.params, + // FIXME: workaround for link widget eats wheel events. + wrapperBuilder: (child) => Listener( + child: child, + onPointerSignal: (event) { + if (event is PointerScrollEvent) { + _onWheelDelta(event); + } + }, + ), + ), + ); + } + + final overlay = widget.params.pageOverlaysBuilder?.call(context, rectExternal, page); + if (overlay != null && overlay.isNotEmpty) { + overlayWidgets.add( + Positioned( + key: Key('#__pageOverlay__:${page.pageNumber}'), + left: rectExternal.left, + top: rectExternal.top, + width: rectExternal.width, + height: rectExternal.height, + child: Stack(children: overlay), + ), + ); + } + } + } + + return [...linkWidgets, ...overlayWidgets]; + } + + void _onSelectionChange() { + _textSelectionChangedDebounceTimer?.cancel(); + _textSelectionChangedDebounceTimer = Timer(const Duration(milliseconds: 300), () { + if (!mounted) return; + widget.params.textSelectionParams?.onTextSelectionChange?.call(this); + }); + } + + Rect _getCacheExtentRect() { + final visibleRect = _visibleRect; + return visibleRect.inflateHV( + horizontal: visibleRect.width * widget.params.horizontalCacheExtent, + vertical: visibleRect.height * widget.params.verticalCacheExtent, + ); + } + + Rect? _documentToRenderBox(Rect rect, RenderBox renderBox) { + final tl = _documentToGlobal(rect.topLeft); + if (tl == null) return null; + final br = _documentToGlobal(rect.bottomRight); + if (br == null) return null; + return Rect.fromPoints(renderBox.globalToLocal(tl), renderBox.globalToLocal(br)); + } + + /// [_CustomPainter] calls the function to paint PDF pages. + void _paintPages(ui.Canvas canvas, ui.Size size) { + if (!_initialized) return; + _paintPagesCustom( + canvas, + cache: _imageCache, + maxImageCacheBytes: widget.params.maxImageBytesCachedOnMemory, + targetRect: _visibleRect, + cacheTargetRect: _getCacheExtentRect(), + scale: _currentZoom * MediaQuery.of(context).devicePixelRatio, + enableLowResolutionPagePreview: widget.params.behaviorControlParams.enableLowResolutionPagePreview, + filterQuality: FilterQuality.low, + ); + } + + void _paintPagesCustom( + ui.Canvas canvas, { + required _PdfPageImageCache cache, + required int maxImageCacheBytes, + required double scale, + required Rect targetRect, + Rect? cacheTargetRect, + bool enableLowResolutionPagePreview = true, + FilterQuality filterQuality = FilterQuality.high, + }) { + final unusedPageList = []; + final dropShadowPaint = widget.params.pageDropShadow?.toPaint()?..style = PaintingStyle.fill; + cacheTargetRect ??= targetRect; + + for (var i = 0; i < _document!.pages.length; i++) { + final rect = _layout!.pageLayouts[i]; + final intersection = rect.intersect(cacheTargetRect); + + // In discrete mode, only render current page(s)/spread during zoom and its animations + // This ensures that pinch zooming out doesn't show neighboring pages + var shouldSkipForDiscrete = false; + if (widget.params.pageTransition == PageTransition.discrete && + _pageNumber != null && + (_isActivelyZooming || _hasActiveAnimations)) { + final layout = _layout; + if (layout is PdfSpreadLayout) { + final range = layout.getPageRange(_pageNumber!); + shouldSkipForDiscrete = i + 1 < range.firstPageNumber || i + 1 > range.lastPageNumber; + } else { + shouldSkipForDiscrete = i + 1 != _pageNumber; + } + } + + if (intersection.isEmpty || shouldSkipForDiscrete) { + final page = _document!.pages[i]; + cache.cancelPendingRenderings(page.pageNumber); + if (cache.pageImages.containsKey(i + 1)) { + unusedPageList.add(i + 1); + } + continue; + } + + final page = _document!.pages[i]; + final previewImage = cache.pageImages[page.pageNumber]; + final partial = cache.pageImagesPartial[page.pageNumber]; + + final getPageRenderingScale = + widget.params.getPageRenderingScale ?? + (context, page, controller, estimatedScale) { + final max = widget.params.onePassRenderingSizeThreshold; + if (page.width > max || page.height > max) { + return min(max / page.width, max / page.height); + } + return estimatedScale; + }; + + final previewScaleLimit = getPageRenderingScale( + context, + page, + _controller!, + widget.params.onePassRenderingScaleThreshold, + ); + + if (dropShadowPaint != null) { + final offset = widget.params.pageDropShadow!.offset; + final spread = widget.params.pageDropShadow!.spreadRadius; + final shadowRect = rect.translate(offset.dx, offset.dy).inflateHV(horizontal: spread, vertical: spread); + canvas.drawRect(shadowRect, dropShadowPaint); + } + + if (widget.params.pageBackgroundPaintCallbacks != null) { + for (final callback in widget.params.pageBackgroundPaintCallbacks!) { + callback(canvas, rect, page); + } + } + + if (enableLowResolutionPagePreview && previewImage != null) { + canvas.drawImageRect( + previewImage.image, + Rect.fromLTWH(0, 0, previewImage.image.width.toDouble(), previewImage.image.height.toDouble()), + rect, + Paint()..filterQuality = filterQuality, + ); + } else { + canvas.drawRect( + rect, + Paint() + ..color = Colors.white + ..style = PaintingStyle.fill, + ); + } + + if (enableLowResolutionPagePreview && + (previewImage == null || previewImage.isDirty || previewImage.scale != previewScaleLimit)) { + _requestPagePreviewImageCached(cache, page, previewScaleLimit); + } + + final pageScale = scale * max(rect.width / page.width, rect.height / page.height); + if (!enableLowResolutionPagePreview || pageScale > previewScaleLimit) { + _requestRealSizePartialImage(cache, page, pageScale, targetRect); + } + + if ((!enableLowResolutionPagePreview || pageScale > previewScaleLimit) && partial != null) { + partial.draw(canvas, filterQuality); + } + + final selectionColor = + Theme.of(context).textSelectionTheme.selectionColor ?? DefaultSelectionStyle.of(context).selectionColor!; + final text = _getCachedTextOrDelayLoadText(page.pageNumber); + if (text != null) { + final selectionInPage = _loadTextSelectionForPageNumber(page.pageNumber); + if (selectionInPage != null) { + for (final r in selectionInPage.enumerateFragmentBoundingRects()) { + canvas.drawRect( + r.bounds.toRectInDocument(page: page, pageRect: rect), + Paint() + ..color = selectionColor + ..style = PaintingStyle.fill, + ); + } + } + } + + if (_canvasLinkPainter.isEnabled) { + _canvasLinkPainter.paintLinkHighlights(canvas, rect, page); + } + + if (widget.params.pagePaintCallbacks != null) { + for (final callback in widget.params.pagePaintCallbacks!) { + callback(canvas, rect, page); + } + } + } + + if (unusedPageList.isNotEmpty) { + final currentPageNumber = _pageNumber; + if (currentPageNumber != null && currentPageNumber > 0) { + final currentPage = _document!.pages[currentPageNumber - 1]; + cache.removeCacheImagesIfCacheBytesExceedsLimit( + unusedPageList, + maxImageCacheBytes, + currentPage, + dist: (pageNumber) => + (_layout!.pageLayouts[pageNumber - 1].center - _layout!.pageLayouts[currentPage.pageNumber - 1].center) + .distanceSquared, + ); + } + } + } + + /// Loads text for the specified page number. + /// + /// If the text is not loaded yet, it will be loaded asynchronously + /// and [onTextLoaded] callback will be called when the text is loaded. + /// If [onTextLoaded] is not provided and [invalidate] is true, the widget will be rebuilt when the text is loaded. + PdfPageText? _getCachedTextOrDelayLoadText(int pageNumber, {void Function()? onTextLoaded, bool invalidate = true}) { + final page = _document!.pages[pageNumber - 1]; + if (!page.isLoaded) return null; + if (_textCache.containsKey(pageNumber)) return _textCache[pageNumber]; + if (onTextLoaded == null && invalidate) { + onTextLoaded = _invalidate; + } + _loadTextAsync(pageNumber, onTextLoaded: onTextLoaded); + return null; + } + + Future _loadTextAsync(int pageNumber, {void Function()? onTextLoaded}) async { + final page = _document!.pages[pageNumber - 1]; + if (!page.isLoaded) return null; + if (_textCache.containsKey(pageNumber)) return _textCache[pageNumber]!; + return await synchronized(() async { + if (_textCache.containsKey(pageNumber)) return _textCache[pageNumber]!; + final page = _document!.pages[pageNumber - 1]; + if (!page.isLoaded) return null; + final text = await page.loadStructuredText(); + _textCache[pageNumber] = text; + if (onTextLoaded != null) { + onTextLoaded(); + } + return text; + }); + } + + bool _hitTestForTextSelection(ui.Offset position) { + if (_selPartMoving != _TextSelectionPart.free && enableSelectionHandles) return false; + if (_document == null || _layout == null) return false; + for (var i = 0; i < _document!.pages.length; i++) { + final pageRect = _layout!.pageLayouts[i]; + if (!pageRect.contains(position)) continue; + final page = _document!.pages[i]; + final text = _getCachedTextOrDelayLoadText( + page.pageNumber, + invalidate: false, + ); // the routine may be called multiple times, we can ignore the chance + if (text == null) continue; + for (final f in text.fragments) { + final rect = f.bounds.toRectInDocument(page: page, pageRect: pageRect).inflate(_hitTestMargin); + if (rect.contains(position)) { + return true; + } + } + } + return false; + } + + PdfPageLayout _layoutPages(List pages, PdfViewerParams params, PdfLayoutHelper helper) { + return SequentialPagesLayout.fromPages(pages, params, helper: helper); + } + + void _invalidate() => _updateStream.add(_txController.value); + + Future _requestPagePreviewImageCached(_PdfPageImageCache cache, PdfPage page, double scale) async { + final width = page.width * scale; + final height = page.height * scale; + if (width < 1 || height < 1) return; + + // if this is the first time to render the page, render it immediately + if (!cache.pageImages.containsKey(page.pageNumber)) { + _cachePagePreviewImage(cache, page, width, height, scale); + return; + } + + cache.pageImageRenderingTimers[page.pageNumber]?.cancel(); + if (!mounted) return; + cache.pageImageRenderingTimers[page.pageNumber] = Timer( + widget.params.behaviorControlParams.pageImageCachingDelay, + () => _cachePagePreviewImage(cache, page, width, height, scale), + ); + } + + Future _cachePagePreviewImage( + _PdfPageImageCache cache, + PdfPage page, + double width, + double height, + double scale, + ) async { + if (!mounted) return; + final prev = cache.pageImages[page.pageNumber]; + if (prev != null && !prev.isDirty && prev.scale == scale) return; + final cancellationToken = page.createCancellationToken(); + + cache.addCancellationToken(page.pageNumber, cancellationToken); + await cache.synchronized(() async { + if (!mounted || cancellationToken.isCanceled) return; + final prev = cache.pageImages[page.pageNumber]; + if (prev != null && !prev.isDirty && prev.scale == scale) return; + PdfImage? img; + try { + img = await page.render( + fullWidth: width, + fullHeight: height, + backgroundColor: 0xffffffff, + annotationRenderingMode: widget.params.annotationRenderingMode, + flags: widget.params.limitRenderingCache ? PdfPageRenderFlags.limitedImageCache : PdfPageRenderFlags.none, + cancellationToken: cancellationToken, + ); + if (img == null || !mounted || cancellationToken.isCanceled) return; + + final newImage = _PdfImageWithScale(await img.createImage(), scale); + cache.pageImages[page.pageNumber]?.dispose(); + cache.pageImages[page.pageNumber] = newImage; + _invalidate(); + } catch (e) { + return; // ignore error + } finally { + img?.dispose(); + } + }); + } + + Future _requestRealSizePartialImage( + _PdfPageImageCache cache, + PdfPage page, + double scale, + Rect targetRect, + ) async { + final pageRect = _layout!.pageLayouts[page.pageNumber - 1]; + final rect = pageRect.intersect(targetRect); + final prev = cache.pageImagesPartial[page.pageNumber]; + if (prev != null && !prev.isDirty && prev.rect == rect && prev.scale == scale) return; + if (rect.width < 1 || rect.height < 1) return; + + cache.pageImagePartialRenderingRequests[page.pageNumber]?.cancel(); + + final cancellationToken = page.createCancellationToken(); + cache.pageImagePartialRenderingRequests[page.pageNumber] = _PdfPartialImageRenderingRequest( + Timer(widget.params.behaviorControlParams.partialImageLoadingDelay, () async { + if (!mounted || cancellationToken.isCanceled) return; + final newImage = await _createRealSizePartialImage(cache, page, scale, rect, cancellationToken); + if (newImage != null) { + cache.pageImagesPartial.remove(page.pageNumber)?.dispose(); + cache.pageImagesPartial[page.pageNumber] = newImage; + _invalidate(); + } + }), + cancellationToken, + ); + } + + Future<_PdfImageWithScaleAndRect?> _createRealSizePartialImage( + _PdfPageImageCache cache, + PdfPage page, + double scale, + Rect rect, + PdfPageRenderCancellationToken cancellationToken, + ) async { + if (!mounted || cancellationToken.isCanceled) return null; + final pageRect = _layout!.pageLayouts[page.pageNumber - 1]; + final inPageRect = rect.translate(-pageRect.left, -pageRect.top); + final x = (inPageRect.left * scale).toInt(); + final y = (inPageRect.top * scale).toInt(); + final width = (inPageRect.width * scale).toInt(); + final height = (inPageRect.height * scale).toInt(); + if (width < 1 || height < 1) return null; + + var flags = 0; + if (widget.params.limitRenderingCache) flags |= PdfPageRenderFlags.limitedImageCache; + + PdfImage? img; + try { + img = await page.render( + x: x, + y: y, + width: width, + height: height, + fullWidth: pageRect.width * scale, + fullHeight: pageRect.height * scale, + backgroundColor: 0xffffffff, + annotationRenderingMode: widget.params.annotationRenderingMode, + flags: flags, + cancellationToken: cancellationToken, + ); + if (img == null || !mounted || cancellationToken.isCanceled) return null; + return _PdfImageWithScaleAndRect(await img.createImage(), scale, rect, x, y); + } catch (e) { + return null; // ignore error + } finally { + img?.dispose(); + } + } + + void _onWheelDelta(PointerScrollEvent event) { + _startInteraction(); + try { + // Handle Ctrl+wheel for zooming + // NOTE: On Flutter Web on Windows, Ctrl+wheel is handled by Flutter engine and it never gets here; and if + // you set scrollPhysics to non-null, it causes layout issue on zooming out (see #547). + if (HardwareKeyboard.instance.isControlPressed) { + // NOTE: I believe that either only dx or dy is set, but I don't know which one is guaranteed to be set. + // So, I just add both values. + var zoomFactor = -(event.scrollDelta.dx + event.scrollDelta.dy) / 120.0; + final newZoom = (_currentZoom * (pow(1.2, zoomFactor))).clamp(minScale, widget.params.maxScale); + if (_areZoomsAlmostIdentical(newZoom, _currentZoom)) return; + // NOTE: _onWheelDelta may be called from other widget's context and localPosition may be incorrect. + _controller!.zoomOnLocalPosition( + localPosition: _controller!.globalToLocal(event.position)!, + newZoom: newZoom, + duration: Duration.zero, + ); + return; + } + + final dx = -event.scrollDelta.dx * widget.params.scrollByMouseWheel! / _currentZoom; + final dy = -event.scrollDelta.dy * widget.params.scrollByMouseWheel! / _currentZoom; + final m = _txController.value.clone(); + if (widget.params.scrollHorizontallyByMouseWheel) { + m.translateByDouble(dy, dx, 0, 1); + } else { + m.translateByDouble(dx, dy, 0, 1); + } + _txController.value = _makeMatrixInSafeRange(m, forceClamp: true); + } finally { + _stopInteraction(); + } + } + + /// Restrict matrix to the safe range. + Matrix4 _makeMatrixInSafeRange(Matrix4 newValue, {bool forceClamp = false}) { + if (!forceClamp && (_layout == null || _viewSize == null || widget.params.scrollPhysics != null)) return newValue; + if (widget.params.normalizeMatrix != null) { + return widget.params.normalizeMatrix!(newValue, _viewSize!, _layout!, _controller); + } + return _calcMatrixForClampedToNearestBoundary(newValue, viewSize: _viewSize!); + } + + /// Calculate matrix to center the specified position. + Matrix4 _calcMatrixFor(Offset position, {required double zoom, required Size viewSize}) { + final hw = viewSize.width / 2; + final hh = viewSize.height / 2; + return Matrix4.compose( + vec.Vector3(-position.dx * zoom + hw, -position.dy * zoom + hh, 0), + vec.Quaternion.identity(), + vec.Vector3( + zoom, + zoom, + zoom, + ), // setting zoom of 1 on z caused a call to matrix.maxScaleOnAxis() to return 1 even when x and y are < 1 + ); + } + + /// The minimum zoom ratio allowed. + double get minScale { + // In discrete mode, prevent zooming out below fit scale + if (widget.params.pageTransition == PageTransition.discrete) { + return _fitScale; + } + + if (widget.params.minScale != null) { + return widget.params.minScale!; + } + + return _fitScale; + } + + Matrix4 _calcMatrixForRect(Rect rect, {double? zoomMax, double? margin}) { + margin ??= 0; + + // Calculate zoom to fit rect in viewport with margins + var calculatedZoom = min( + (_viewSize!.width - margin * 2) / rect.width, + (_viewSize!.height - margin * 2) / rect.height, + ); + + // Clamp to zoomMax if provided + if (zoomMax != null && calculatedZoom > zoomMax) { + calculatedZoom = zoomMax; + } + + return _calcMatrixFor(rect.center, zoom: calculatedZoom, viewSize: _viewSize!); + } + + Matrix4 _calcMatrixForArea({required Rect rect, double? zoomMax, double? margin, PdfPageAnchor? anchor}) => + _calcMatrixForRect( + _calcRectForArea(rect: rect, anchor: anchor ?? widget.params.pageAnchor), + zoomMax: zoomMax, + margin: margin, + ); + + /// The function calculate the rectangle which should be shown in the view. + /// + /// If the rect is smaller than the view size, it will + Rect _calcRectForArea({required Rect rect, required PdfPageAnchor anchor}) { + // Use physical viewport size, not current visible rect + // _visibleRect.size varies with zoom, but we want consistent anchor behavior + final viewSize = _viewSize!; + final w = min(rect.width, viewSize.width); + final h = min(rect.height, viewSize.height); + + switch (anchor) { + case PdfPageAnchor.top: + return Rect.fromLTWH(rect.left, rect.top, rect.width, h); + case PdfPageAnchor.left: + return Rect.fromLTWH(rect.left, rect.top, w, rect.height); + case PdfPageAnchor.right: + return Rect.fromLTWH(rect.right - w, rect.top, w, rect.height); + case PdfPageAnchor.bottom: + return Rect.fromLTWH(rect.left, rect.bottom - h, rect.width, h); + case PdfPageAnchor.topLeft: + return Rect.fromLTWH(rect.left, rect.top, viewSize.width, viewSize.height); + case PdfPageAnchor.topCenter: + return Rect.fromLTWH(rect.topCenter.dx - w / 2, rect.top, viewSize.width, viewSize.height); + case PdfPageAnchor.topRight: + return Rect.fromLTWH(rect.topRight.dx - w, rect.top, viewSize.width, viewSize.height); + case PdfPageAnchor.centerLeft: + return Rect.fromLTWH(rect.left, rect.center.dy - h / 2, viewSize.width, viewSize.height); + case PdfPageAnchor.center: + return Rect.fromLTWH(rect.center.dx - w / 2, rect.center.dy - h / 2, w, h); + case PdfPageAnchor.centerRight: + return Rect.fromLTWH(rect.right - w, rect.center.dy - h / 2, viewSize.width, viewSize.height); + case PdfPageAnchor.bottomLeft: + return Rect.fromLTWH(rect.left, rect.bottom - h, viewSize.width, viewSize.height); + case PdfPageAnchor.bottomCenter: + return Rect.fromLTWH(rect.center.dx - w / 2, rect.bottom - h, viewSize.width, viewSize.height); + case PdfPageAnchor.bottomRight: + return Rect.fromLTWH(rect.right - w, rect.bottom - h, viewSize.width, viewSize.height); + case PdfPageAnchor.all: + return rect; + } + } + + /// Gets the effective page bounds for a given page, including margins. + /// Optionally includes boundary margins for positioning purposes. + Rect _getEffectivePageBounds(int pageNumber, PdfPageLayout layout, {bool includingBoundaryMargins = true}) { + final baseRect = layout.getSpreadBounds(pageNumber); + + var result = baseRect.inflate(widget.params.margin); + + // Add boundary margins for positioning when appropriate + if (includingBoundaryMargins) { + if (widget.params.pageTransition == PageTransition.continuous) { + // Continuous mode: apply boundary margins on cross-axis throughout, + // and on primary axis only at document ends + final margins = _adjustedBoundaryMargins; + final isPrimaryVertical = layout.primaryAxis == Axis.vertical; + final isFirstPage = pageNumber == 1; + final isLastPage = pageNumber == layout.pageLayouts.length; + + if (isPrimaryVertical) { + // Vertical scrolling: margins on left/right (cross-axis), top/bottom only at ends + result = Rect.fromLTRB( + result.left - margins.left, // Cross-axis: left margin + isFirstPage ? result.top - margins.top : result.top, // Primary: top margin only on first page + result.right + margins.right, // Cross-axis: right margin + isLastPage ? result.bottom + margins.bottom : result.bottom, // Primary: bottom margin only on last page + ); + } else { + // Horizontal scrolling: margins on top/bottom (cross-axis), left/right only at ends + result = Rect.fromLTRB( + isFirstPage ? result.left - margins.left : result.left, // Primary: left margin only on first page + result.top - margins.top, // Cross-axis: top margin + isLastPage ? result.right + margins.right : result.right, // Primary: right margin only on last page + result.bottom + margins.bottom, // Cross-axis: bottom margin + ); + } + } else { + // Discrete mode: always use the user's boundary margins for positioning + final userBoundaryMargin = widget.params.boundaryMargin ?? EdgeInsets.zero; + result = userBoundaryMargin.inflateRectIfFinite(result); + } + } + return result; + } + + Matrix4 _calcMatrixForPage({ + required int pageNumber, + PdfPageAnchor? anchor, + double? forceScale, + bool maintainCurrentZoom = false, + }) { + final layout = _layout!; + final targetRect = _getEffectivePageBounds(pageNumber, layout); + + // Simple priority: forceScale > maintainCurrentZoom > calculate fit + final double zoom; + if (forceScale != null) { + zoom = forceScale; + } else if (maintainCurrentZoom) { + zoom = _currentZoom; + } else { + // Calculate zoom to fit page in viewport + zoom = min(_viewSize!.width / targetRect.width, _viewSize!.height / targetRect.height); + } + + return _calcMatrixForArea(rect: targetRect, anchor: anchor, zoomMax: zoom); + } + + Rect _calcRectForRectInsidePage({required int pageNumber, required PdfRect rect}) { + final page = _document!.pages[pageNumber - 1]; + final pageRect = _layout!.pageLayouts[pageNumber - 1]; + final area = rect.toRect(page: page, scaledPageSize: pageRect.size); + return area.translate(pageRect.left, pageRect.top); + } + + Matrix4 _calcMatrixForRectInsidePage({required int pageNumber, required PdfRect rect, PdfPageAnchor? anchor}) { + return _calcMatrixForArea( + rect: _calcRectForRectInsidePage(pageNumber: pageNumber, rect: rect), + anchor: anchor, + ); + } + + Matrix4? _calcMatrixForDest(PdfDest? dest) { + if (dest == null) return null; + final page = _document!.pages[dest.pageNumber - 1]; + final pageRect = _layout!.pageLayouts[dest.pageNumber - 1]; + double calcX(double? x) => (x ?? 0) / page.width * pageRect.width; + double calcY(double? y) => (page.height - (y ?? 0)) / page.height * pageRect.height; + final params = dest.params; + switch (dest.command) { + case PdfDestCommand.xyz: + if (params != null && params.length >= 2) { + final zoom = params.length >= 3 + ? params[2] != null && params[2] != 0.0 + ? params[2]! + : _currentZoom + : 1.0; + final hw = _viewSize!.width / 2 / zoom; + final hh = _viewSize!.height / 2 / zoom; + return _calcMatrixFor( + pageRect.topLeft.translate(calcX(params[0]) + hw, calcY(params[1]) + hh), + zoom: zoom, + viewSize: _viewSize!, + ); + } + break; + case PdfDestCommand.fit: + case PdfDestCommand.fitB: + return _calcMatrixForPage(pageNumber: dest.pageNumber, anchor: PdfPageAnchor.all); + + case PdfDestCommand.fitH: + case PdfDestCommand.fitBH: + if (params != null && params.length == 1) { + final hh = _viewSize!.height / 2 / _currentZoom; + return _calcMatrixFor( + pageRect.topLeft.translate(0, calcY(params[0]) + hh), + zoom: _currentZoom, + viewSize: _viewSize!, + ); + } + break; + case PdfDestCommand.fitV: + case PdfDestCommand.fitBV: + if (params != null && params.length == 1) { + final hw = _viewSize!.width / 2 / _currentZoom; + return _calcMatrixFor( + pageRect.topLeft.translate(calcX(params[0]) + hw, 0), + zoom: _currentZoom, + viewSize: _viewSize!, + ); + } + break; + case PdfDestCommand.fitR: + if (params != null && params.length == 4) { + // page /FitR left bottom right top + return _calcMatrixForArea( + rect: Rect.fromLTRB( + calcX(params[0]), + calcY(params[3]), + calcX(params[2]), + calcY(params[1]), + ).translate(pageRect.left, pageRect.top), + anchor: PdfPageAnchor.all, + ); + } + break; + default: + return null; + } + return null; + } + + Future _goTo( + Matrix4? destination, { + Duration duration = const Duration(milliseconds: 200), + Curve curve = Curves.easeInOut, + }) async { + void update() { + if (_animationResettingGuard != 0) return; + _txController.value = _animGoTo!.value; + } + + try { + if (destination == null) { + return; // do nothing + } + + final safeDestination = _makeMatrixInSafeRange(destination, forceClamp: true); + + _stopInteractiveViewerAnimation(); + _animationResettingGuard++; + _animController.reset(); + _animationResettingGuard--; + _animGoTo = Matrix4Tween(begin: _txController.value, end: safeDestination).animate(_animController); + _animGoTo!.addListener(update); + await _animController.animateTo(1.0, duration: duration, curve: curve); + } finally { + _animGoTo?.removeListener(update); + } + } + + Matrix4 _calcMatrixToEnsureRectVisible(Rect rect, {double margin = 0}) { + final restrictedRect = _txController.value.calcVisibleRect(_viewSize!, margin: margin); + if (restrictedRect.containsRect(rect)) { + return _txController.value; // keep the current position + } + if (rect.width <= restrictedRect.width && rect.height < restrictedRect.height) { + final intRect = Rect.fromLTWH( + rect.left < restrictedRect.left + ? rect.left + : rect.right < restrictedRect.right + ? restrictedRect.left + : restrictedRect.left + rect.right - restrictedRect.right, + rect.top < restrictedRect.top + ? rect.top + : rect.bottom < restrictedRect.bottom + ? restrictedRect.top + : restrictedRect.top + rect.bottom - restrictedRect.bottom, + restrictedRect.width, + restrictedRect.height, + ); + final newRect = intRect.inflate(margin / _currentZoom); + return _calcMatrixForRect(newRect); + } + return _calcMatrixForRect(rect, margin: margin); + } + + Future _ensureVisible(Rect rect, {Duration duration = const Duration(milliseconds: 200), double margin = 0}) => + _goTo(_calcMatrixToEnsureRectVisible(rect, margin: margin), duration: duration); + + Future _goToArea({ + required Rect rect, + PdfPageAnchor? anchor, + Duration duration = const Duration(milliseconds: 200), + }) => _goTo( + _calcMatrixForArea(rect: rect, anchor: anchor), + duration: duration, + ); + + Future _goToPage({ + required int pageNumber, + PdfPageAnchor? anchor, + Duration duration = const Duration(milliseconds: 200), + bool maintainCurrentZoom = true, + double? forceScale, + }) async { + final pageCount = _document!.pages.length; + final int targetPageNumber; + if (pageNumber < 1) { + targetPageNumber = 1; + } else if (pageNumber != 1 && pageNumber >= pageCount) { + targetPageNumber = pageNumber = pageCount; + anchor ??= widget.params.pageAnchorEnd; + } else { + targetPageNumber = pageNumber; + } + _gotoTargetPageNumber = pageNumber; + + await _goTo( + _calcMatrixForClampedToNearestBoundary( + _calcMatrixForPage( + pageNumber: targetPageNumber, + anchor: anchor, + maintainCurrentZoom: maintainCurrentZoom, + forceScale: forceScale, + ), + viewSize: _viewSize!, + ), + duration: duration, + ); + _setCurrentPageNumber(targetPageNumber); + } + + /// Scrolls/zooms so that the specified PDF document coordinate appears at + /// the top-left corner of the viewport. + /// + /// If [pageNumber] and [wasAtBoundaries] are provided, applies margin snapping + /// to ensure points that were at boundaries are positioned at the correct boundaries + /// with new margins applied. + Future _goToPosition({ + required Offset documentOffset, + Duration duration = const Duration(milliseconds: 0), + double? zoom, + int? pageNumber, + }) async { + // Clear any cached partial images to avoid stale tiles after + // going to the new matrix + _imageCache.releasePartialImages(); + + zoom = zoom ?? _currentZoom; + final tx = -documentOffset.dx * zoom; + final ty = -documentOffset.dy * zoom; + final m = Matrix4.compose(vec.Vector3(tx, ty, 0), vec.Quaternion.identity(), vec.Vector3(zoom, zoom, zoom)); + + _adjustBoundaryMargins(_viewSize!, zoom); + + // Apply margin snapping if page number and boundary info are provided + final marginAdjusted = pageNumber != null + ? _calcMatrixForMarginSnappedToNearestBoundary(m, pageNumber: pageNumber, viewSize: _viewSize!) + : m; + + // Then clamp to nearest boundary to handle out-of-bounds cases + // When preserving position (wasAtBoundaries provided), don't use extended boundaries + final clamped = _calcMatrixForClampedToNearestBoundary(marginAdjusted, viewSize: _viewSize!); + + await _goTo(clamped, duration: duration); + } + + Future _goToRectInsidePage({ + required int pageNumber, + required PdfRect rect, + PdfPageAnchor? anchor, + Duration duration = const Duration(milliseconds: 200), + }) async { + _gotoTargetPageNumber = pageNumber; + await _goTo( + _calcMatrixForRectInsidePage(pageNumber: pageNumber, rect: rect, anchor: anchor), + duration: duration, + ); + _setCurrentPageNumber(pageNumber); + } + + Future _goToDest(PdfDest? dest, {Duration duration = const Duration(milliseconds: 200)}) async { + final m = _calcMatrixForDest(dest); + if (m == null) return false; + if (dest != null) { + _gotoTargetPageNumber = dest.pageNumber; + } + await _goTo(m, duration: duration); + if (dest != null) { + _setCurrentPageNumber(dest.pageNumber); + } + return true; + } + + double get _currentZoom => _txController.value.zoom; + + PdfPageHitTestResult? _getPdfPageHitTestResult(Offset offset, {required bool useDocumentLayoutCoordinates}) { + final pages = _document?.pages; + final pageLayouts = _layout?.pageLayouts; + if (pages == null || pageLayouts == null) return null; + if (!useDocumentLayoutCoordinates) { + final r = Matrix4.inverted(_txController.value); + offset = r.transformOffset(offset); + } + for (var i = 0; i < pages.length; i++) { + final page = pages[i]; + final pageRect = pageLayouts[i]; + if (pageRect.contains(offset)) { + return PdfPageHitTestResult( + page: page, + offset: offset.translate(-pageRect.left, -pageRect.top).toPdfPoint(page: page, scaledPageSize: pageRect.size), + ); + } + } + return null; + } + + double _getNextZoom({bool loop = true}) => _findNextZoomStop(_currentZoom, zoomUp: true, loop: loop); + double _getPreviousZoom({bool loop = true}) => _findNextZoomStop(_currentZoom, zoomUp: false, loop: loop); + + Future _setZoom(Offset position, double zoom, {Duration duration = const Duration(milliseconds: 200)}) { + _adjustBoundaryMargins(_viewSize!, zoom); + return _goTo( + _calcMatrixFor(position, zoom: zoom, viewSize: _viewSize!), + duration: duration, + ); + } + + Offset _localPositionToZoomCenter(Offset localPosition, double newZoom) { + final toCenter = (_viewSize!.center(Offset.zero) - localPosition) / newZoom; + final zoomPosition = _controller!.globalToDocument(_controller!.localToGlobal(localPosition)!)!; + return zoomPosition.translate(toCenter.dx, toCenter.dy); + } + + Offset get _centerPosition => _txController.value.calcPosition(_viewSize!); + + Future _zoomUp({ + bool loop = false, + Offset? zoomCenter, + Duration duration = const Duration(milliseconds: 200), + }) async { + final newZoom = _getNextZoom(loop: loop); + + // In discrete mode, zoom to the current page to avoid jumping to other pages + if (widget.params.pageTransition == PageTransition.discrete && zoomCenter == null && _pageNumber != null) { + await _goToPage(pageNumber: _pageNumber!, duration: duration, anchor: PdfPageAnchor.center, forceScale: newZoom); + } else { + await _setZoom(zoomCenter ?? _centerPosition, newZoom, duration: duration); + } + } + + Future _zoomDown({ + bool loop = false, + Offset? zoomCenter, + Duration duration = const Duration(milliseconds: 200), + }) async { + final newZoom = _getPreviousZoom(loop: loop); + + // In discrete mode, zoom to the current page to avoid jumping to other pages + if (widget.params.pageTransition == PageTransition.discrete && zoomCenter == null && _pageNumber != null) { + await _goToPage(pageNumber: _pageNumber!, duration: duration, anchor: PdfPageAnchor.center, forceScale: newZoom); + } else { + await _setZoom(zoomCenter ?? _centerPosition, newZoom, duration: duration); + } + } + + RenderBox? get _renderBox { + final renderBox = context.findRenderObject(); + if (renderBox is! RenderBox) return null; + return renderBox; + } + + /// Converts the global position to the local position in the widget. + Offset? _globalToLocal(Offset global) { + try { + final renderBox = _renderBox; + if (renderBox == null) return null; + return renderBox.globalToLocal(global); + } catch (e) { + return null; + } + } + + /// Converts the local position to the global position in the widget. + Offset? _localToGlobal(Offset local) { + try { + final renderBox = _renderBox; + if (renderBox == null) return null; + return renderBox.localToGlobal(local); + } catch (e) { + return null; + } + } + + /// Converts the global position to the local position in the PDF document structure. + Offset? _globalToDocument(Offset global) { + final local = _globalToLocal(global); + if (local == null) return null; + return _localToDocument(local); + } + + /// Converts the local position in the PDF document structure to the global position. + Offset? _documentToGlobal(Offset document) => _localToGlobal((_documentToLocal(document))); + + /// Converts the local position in the widget to the local position in the PDF document structure. + Offset _localToDocument(Offset local) { + final ratio = 1 / _currentZoom; + return local.translate(-_txController.value.xZoomed, -_txController.value.yZoomed).scale(ratio, ratio); + } + + /// Converts the local position in the PDF document structure to the local position in the widget. + Offset _documentToLocal(Offset document) { + return document + .scale(_currentZoom, _currentZoom) + .translate(_txController.value.xZoomed, _txController.value.yZoomed); + } + + FocusNode? _getFocusNode() { + return _contextForFocusNode != null ? Focus.maybeOf(_contextForFocusNode!) : null; + } + + void _requestFocus() { + _getFocusNode()?.requestFocus(); + } + + void _handlePointerEvent(PointerEvent event, Offset localPosition, PointerDeviceKind? deviceKind) { + _pointerOffset = localPosition; + if (_pointerDeviceKind != deviceKind) { + _pointerDeviceKind = deviceKind; + _invalidate(); + } + } + + PdfViewerPart _onWhat(Offset localPosition) { + final p = _findTextAndIndexForPoint(localPosition); + if (p != null) { + if (_selA != null && _selB != null && _selA! <= p && p <= _selB!) { + return PdfViewerPart.selectedText; + } + return PdfViewerPart.nonSelectedText; + } + return PdfViewerPart.background; + } + + void _handleGeneralTap(Offset globalPosition, PdfViewerGeneralTapType type) { + final docPosition = _globalToDocument(globalPosition); + final what = _onWhat(docPosition!); + _requestFocus(); + + if (widget.params.onGeneralTap != null) { + final localPosition = doc2local.offsetToLocal(context, docPosition)!; + if (widget.params.onGeneralTap!( + _contextForFocusNode!, + _controller!, + PdfViewerGeneralTapHandlerDetails( + type: type, + localPosition: localPosition, + documentPosition: docPosition, + tapOn: what, + ), + )) { + return; // the tap was handled + } + } + switch (type) { + case PdfViewerGeneralTapType.tap: + _clearTextSelections(); + case PdfViewerGeneralTapType.longPress: + if (what == PdfViewerPart.nonSelectedText && isTextSelectionEnabled) { + selectWord(docPosition, deviceKind: _pointerDeviceKind); + } else { + showContextMenu(docPosition, forPart: what); + } + case PdfViewerGeneralTapType.secondaryTap: + showContextMenu(docPosition, forPart: what); + default: + } + } + + void showContextMenu(Offset docPosition, {PdfViewerPart? forPart}) { + _contextMenuDocumentPosition = docPosition; + _contextMenuFor = forPart ?? _onWhat(docPosition); + _invalidate(); + } + + void _onTextPanStart(DragStartDetails details) { + if (_isInteractionGoingOn) return; + _selPartMoving = _TextSelectionPart.free; + _isSelectingAllText = false; + _contextMenuDocumentPosition = null; + _selA = _findTextAndIndexForPoint(details.localPosition); + _textSelectAnchor = Offset(_txController.value.x, _txController.value.y); + _selB = null; + _updateTextSelection(); + _requestFocus(); + } + + void _onTextPanUpdate(DragUpdateDetails details) { + _updateTextSelectRectTo(details.localPosition); + _selectionPointerDeviceKind = _pointerDeviceKind; + } + + void _onTextPanEnd(DragEndDetails details) { + _updateTextSelectRectTo(details.localPosition); + _selPartMoving = _TextSelectionPart.none; + _isSelectingAllText = false; + _invalidate(); + } + + void _updateTextSelectRectTo(Offset panTo) { + if (_selPartMoving != _TextSelectionPart.free) return; + final to = _findTextAndIndexForPoint( + panTo + _textSelectAnchor! - Offset(_txController.value.x, _txController.value.y), + ); + if (to != null) { + _selB = to; + _updateTextSelection(); + } + } + + void _updateTextSelection({bool invalidate = true}) { + if (isTextSelectionEnabled) { + final a = _selA; + final b = _selB; + if (a == null || b == null) { + _textSelA = _textSelB = null; + } else if (a.text.pageNumber == b.text.pageNumber) { + final page = _document!.pages[a.text.pageNumber - 1]; + final pageRect = _layout!.pageLayouts[a.text.pageNumber - 1]; + final range = a.text.getRangeFromAB(a.index, b.index); + _textSelA = PdfTextSelectionAnchor( + a.text.charRects[range.start].toRectInDocument(page: page, pageRect: pageRect), + range.firstFragment?.direction ?? PdfTextDirection.ltr, + PdfTextSelectionAnchorType.a, + a.text, + a.index, + ); + _textSelB = PdfTextSelectionAnchor( + a.text.charRects[range.end - 1].toRectInDocument(page: page, pageRect: pageRect), + range.lastFragment?.direction ?? PdfTextDirection.ltr, + PdfTextSelectionAnchorType.b, + a.text, + b.index, + ); + } else { + final first = a.text.pageNumber < b.text.pageNumber ? a : b; + final second = a.text.pageNumber < b.text.pageNumber ? b : a; + final rangeA = PdfPageTextRange(pageText: first.text, start: first.index, end: first.text.charRects.length); + _textSelA = PdfTextSelectionAnchor( + first.text.charRects[first.index].toRectInDocument( + page: _document!.pages[first.text.pageNumber - 1], + pageRect: _layout!.pageLayouts[first.text.pageNumber - 1], + ), + rangeA.firstFragment?.direction ?? PdfTextDirection.ltr, + PdfTextSelectionAnchorType.a, + first.text, + first.index, + ); + final rangeB = PdfPageTextRange(pageText: second.text, start: 0, end: second.index + 1); + _textSelB = PdfTextSelectionAnchor( + second.text.charRects[second.index].toRectInDocument( + page: _document!.pages[second.text.pageNumber - 1], + pageRect: _layout!.pageLayouts[second.text.pageNumber - 1], + ), + rangeB.lastFragment?.direction ?? PdfTextDirection.ltr, + PdfTextSelectionAnchorType.b, + second.text, + second.index, + ); + } + } else { + _selA = _selB = null; + _textSelA = _textSelB = null; + _contextMenuDocumentPosition = null; + _isSelectingAllText = false; + } + + if (invalidate) { + _notifyTextSelectionChange(); + } + } + + /// [point] is in the document coordinates. + PdfTextSelectionPoint? _findTextAndIndexForPoint(Offset? point, {double hitTestMargin = 8}) { + if (point == null) return null; + for (var pageIndex = 0; pageIndex < _document!.pages.length; pageIndex++) { + final pageRect = _layout!.pageLayouts[pageIndex]; + if (!pageRect.contains(point)) { + continue; + } + final page = _document!.pages[pageIndex]; + final text = _getCachedTextOrDelayLoadText(pageIndex + 1, onTextLoaded: () => _updateTextSelection()); + if (text == null) continue; + final pt = point.translate(-pageRect.left, -pageRect.top).toPdfPoint(page: page, scaledPageSize: pageRect.size); + var d2Min = double.infinity; + int? closestIndex; + for (var i = 0; i < text.charRects.length; i++) { + final charRect = text.charRects[i]; + if (charRect.containsPoint(pt)) { + return PdfTextSelectionPoint(text, i); + } + final d2 = charRect.distanceSquaredTo(pt); + if (d2 < d2Min) { + d2Min = d2; + closestIndex = i; + } + } + if (closestIndex != null && d2Min <= hitTestMargin * hitTestMargin) { + return PdfTextSelectionPoint(text, closestIndex); + } + } + return null; + } + + void _notifyTextSelectionChange() { + _onSelectionChange(); + _invalidate(); + } + + Rect? _anchorARect; + Rect? _anchorBRect; + Rect? _magnifierRect; + Rect? _previousMagnifierRect; + Rect? _contextMenuRect; + _TextSelectionPart _hoverOn = _TextSelectionPart.none; + + bool get enableSelectionHandles => + widget.params.textSelectionParams?.enableSelectionHandles ?? _pointerDeviceKind == PointerDeviceKind.touch; + + List _placeTextSelectionWidgets(BuildContext context, Size viewSize, bool isCopyTextEnabled) { + Widget? createContextMenu(Offset? a, Offset? b, PdfViewerPart contextMenuFor) { + if (a == null) return null; + final ctxMenuBuilder = widget.params.buildContextMenu ?? _buildContextMenu; + return ctxMenuBuilder( + context, + PdfViewerContextMenuBuilderParams( + isTextSelectionEnabled: isTextSelectionEnabled, + anchorA: a, + anchorB: b, + a: _textSelA, + b: _textSelB, + contextMenuFor: contextMenuFor, + textSelectionDelegate: this, + dismissContextMenu: () { + _contextMenuDocumentPosition = null; + _invalidate(); + }, + ), + ); + } + + List contextMenuIfNeeded() { + final contextMenu = createContextMenu( + offsetToLocal(context, _contextMenuDocumentPosition), + null, + _contextMenuFor, + ); + return [if (contextMenu != null) contextMenu]; + } + + final renderBox = _renderBox; + if (!isTextSelectionEnabled || renderBox == null || _textSelA == null || _textSelB == null) { + return contextMenuIfNeeded(); + } + + final rectA = _documentToRenderBox(_textSelA!.rect, renderBox); + final rectB = _documentToRenderBox(_textSelB!.rect, renderBox); + if (rectA == null || rectB == null) { + return contextMenuIfNeeded(); + } + + double? aLeft, aRight, aBottom; + double? bLeft, bTop, bRight; + Widget? anchorA, anchorB; + + if ((enableSelectionHandles || _selectionPointerDeviceKind == PointerDeviceKind.touch) && + _selPartMoving != _TextSelectionPart.free) { + final builder = widget.params.textSelectionParams?.buildSelectionHandle ?? _buildDefaultSelectionHandle; + + if (_textSelA != null) { + final state = _selPartMoving == _TextSelectionPart.a + ? PdfViewerTextSelectionAnchorHandleState.dragging + : _hoverOn == _TextSelectionPart.a + ? PdfViewerTextSelectionAnchorHandleState.hover + : PdfViewerTextSelectionAnchorHandleState.normal; + final offset = + widget.params.textSelectionParams?.calcSelectionHandleOffset?.call(context, _textSelA!, state) ?? + Offset.zero; + switch (_textSelA!.direction) { + case PdfTextDirection.ltr: + case PdfTextDirection.unknown: + aRight = viewSize.width - rectA.left - offset.dx; + aBottom = viewSize.height - rectA.top - offset.dy; + case PdfTextDirection.rtl: + aLeft = rectA.right + offset.dx; + aBottom = viewSize.height - rectA.top - offset.dy; + case PdfTextDirection.vrtl: + aLeft = rectA.right + offset.dx; + aBottom = viewSize.height - rectA.top - offset.dy; + } + anchorA = builder(context, _textSelA!, state); + } + if (_textSelB != null) { + final state = _selPartMoving == _TextSelectionPart.b + ? PdfViewerTextSelectionAnchorHandleState.dragging + : _hoverOn == _TextSelectionPart.b + ? PdfViewerTextSelectionAnchorHandleState.hover + : PdfViewerTextSelectionAnchorHandleState.normal; + final offset = + widget.params.textSelectionParams?.calcSelectionHandleOffset?.call(context, _textSelB!, state) ?? + Offset.zero; + switch (_textSelB!.direction) { + case PdfTextDirection.ltr: + case PdfTextDirection.unknown: + bLeft = rectB.right + offset.dx; + bTop = rectB.bottom + offset.dy; + case PdfTextDirection.rtl: + bRight = viewSize.width - rectB.left - offset.dx; + bTop = rectB.bottom + offset.dy; + case PdfTextDirection.vrtl: + bRight = viewSize.width - rectB.left - offset.dx; + bTop = rectB.bottom + offset.dy; + } + anchorB = builder(context, _textSelB!, state); + } + } else { + _anchorARect = _anchorBRect = null; + } + + final textAnchorMoving = switch (_selPartMoving) { + _TextSelectionPart.a => _selA! < _selB! ? _TextSelectionPart.a : _TextSelectionPart.b, + _TextSelectionPart.b => _selA! < _selB! ? _TextSelectionPart.b : _TextSelectionPart.a, + _ => _selPartMoving, + }; + + // Determines whether the widget is [Positioned] or [Align] to avoid unnecessary wrapping. + bool isPositionalWidget(Widget? widget) => widget != null && (widget is Positioned || widget is Align); + + const defMargin = 8.0; + + Offset normalizeWidgetPosition(Offset pos, Size? widgetSize, {double margin = defMargin}) { + if (widgetSize == null) return pos; + var left = pos.dx; + var top = pos.dy; + if (left + widgetSize.width + margin > viewSize.width) { + left = viewSize.width - widgetSize.width - margin; + } + if (left < margin) { + left = margin; + } + if (top + widgetSize.height + margin > viewSize.height) { + top = viewSize.height - widgetSize.height - margin; + } + if (top < margin) { + top = margin; + } + return Offset(left, top); + } + + Offset? calcPosition( + Size? widgetSize, + Rect anchorLocalRect, + Rect? handleLocalRect, + PdfTextSelectionAnchor? textAnchor, + Offset pointerPosition, { + double margin = defMargin, + double? marginOnTop, + double? marginOnBottom, + }) { + if (widgetSize == null || textAnchor == null) { + return null; + } + + late double left, top; + final rect0 = anchorLocalRect; + final rect1 = handleLocalRect; + final pt = rect0.center; + final rectTop = rect1 == null ? rect0.top : min(rect0.top, rect1.top); + final rectBottom = rect1 == null ? rect0.bottom : max(rect0.bottom, rect1.bottom); + final rectLeft = rect1 == null ? rect0.left : min(rect0.left, rect1.left); + final rectRight = rect1 == null ? rect0.right : max(rect0.right, rect1.right); + switch (textAnchor.direction) { + case PdfTextDirection.ltr: + case PdfTextDirection.rtl: + case PdfTextDirection.unknown: + left = pt.dx - widgetSize.width / 2 + margin; + if (left < margin) { + left = margin; + } else if (left + widgetSize.width + margin > viewSize.width) { + // If the anchor is too close to the right, place the magnifier to the left of it. + left = viewSize.width - widgetSize.width - margin; + } + top = rectTop - widgetSize.height - (marginOnTop ?? margin); + if (top < margin) { + // If the anchor is too close to the top, place the magnifier below it. + top = rectBottom + (marginOnBottom ?? margin); + } + break; + case PdfTextDirection.vrtl: + if (textAnchor.type == PdfTextSelectionAnchorType.a) { + left = rectRight + margin; + if (left + widgetSize.width + margin > viewSize.width) { + left = rectLeft - widgetSize.width - margin; + } + } else { + left = rectLeft - widgetSize.width - margin; + if (left < margin) { + left = rectRight + margin; + } + } + top = pt.dy - widgetSize.height / 2; + if (top < margin) { + top = margin; + } else if (top + widgetSize.height + margin > viewSize.height) { + // If the anchor is too close to the bottom, place the magnifier above it. + top = viewSize.height - widgetSize.height - margin; + } + } + return normalizeWidgetPosition(Offset(left, top), widgetSize, margin: margin); + } + + Widget? magnifier; + + final shouldShowMagnifier = widget.params.textSelectionParams?.magnifier?.shouldShowMagnifier?.call(); + // Show magnifier if dragging + if (((textAnchorMoving == _TextSelectionPart.a || textAnchorMoving == _TextSelectionPart.b) && + shouldShowMagnifier != false) || + shouldShowMagnifier == true) { + final textAnchor = textAnchorMoving == _TextSelectionPart.a + ? _textSelA! + : textAnchorMoving == _TextSelectionPart.b + ? _textSelB! + : (_selPartLastMoved == _TextSelectionPart.a ? _textSelA! : _textSelB!); + final magnifierParams = widget.params.textSelectionParams?.magnifier ?? const PdfViewerSelectionMagnifierParams(); + + final magnifierEnabled = + (magnifierParams.enabled ?? _selectionPointerDeviceKind == PointerDeviceKind.touch) && + (magnifierParams.shouldShowMagnifierForAnchor ?? _shouldShowMagnifierForAnchor)( + textAnchor, + _controller!, + magnifierParams, + ); + if (magnifierEnabled) { + final anchorLocalRect = textAnchorMoving == _TextSelectionPart.a ? rectA : rectB; + final handleLocalRect = textAnchorMoving == _TextSelectionPart.a ? _anchorARect! : _anchorBRect!; + // Calculate final magnifier position before calling builder + final magnifierPosition = + (magnifierParams.calcPosition ?? calcPosition)( + _magnifierRect?.size, + anchorLocalRect, + handleLocalRect, + textAnchor, + _pointerOffset, + margin: 10, + marginOnTop: 20, + marginOnBottom: 80, + ) ?? + Offset.zero; + + // Calculate clamped pointer position for magnifier content + final clampedPointerPosition = _calcClampedPointerPosition( + _pointerOffset, + magnifierPosition, + _magnifierRect?.size, + textAnchor, + ); + + final magRect = (magnifierParams.getMagnifierRectForAnchor ?? _getMagnifierRect)( + textAnchor, + magnifierParams, + clampedPointerPosition, + ); + final magnifierMain = _buildMagnifier(context, magRect, magnifierParams); + final builder = magnifierParams.builder ?? _buildMagnifierDecoration; + magnifier = builder( + context, + textAnchor, + magnifierParams, + magnifierMain, + magRect.size, + _pointerOffset, + magnifierPosition, + ); + if (magnifier != null && !isPositionalWidget(magnifier)) { + final offset = magnifierPosition; + magnifier = AnimatedPositioned( + duration: _previousMagnifierRect != null ? magnifierParams.animationDuration : Duration.zero, + left: offset.dx, + top: offset.dy, + child: WidgetSizeSniffer( + key: Key('magnifier'), + child: magnifier, + onSizeChanged: (rect) { + _magnifierRect = rect.toLocal(context); + _invalidate(); + }, + ), + ); + _previousMagnifierRect = _magnifierRect; + } + } else { + _magnifierRect = _previousMagnifierRect = null; + } + } + + final showContextMenuAutomatically = + widget.params.textSelectionParams?.showContextMenuAutomatically ?? + _selectionPointerDeviceKind == PointerDeviceKind.touch; + var showContextMenu = false; + if (_contextMenuDocumentPosition != null) { + showContextMenu = true; + } else if (showContextMenuAutomatically && + _textSelA != null && + _textSelB != null && + _selPartMoving == _TextSelectionPart.none) { + // Show context menu on mobile when selection is not moving. + showContextMenu = true; + _contextMenuFor = PdfViewerPart.selectedText; + } + + Widget? contextMenu; + if (showContextMenu && + _selPartMoving == _TextSelectionPart.none && + (_contextMenuDocumentPosition != null || + _selPartLastMoved == _TextSelectionPart.a || + _selPartLastMoved == _TextSelectionPart.b || + _isSelectingAllText) && + isCopyTextEnabled) { + final localOffset = _contextMenuDocumentPosition != null + ? offsetToLocal(context, _contextMenuDocumentPosition!) + : null; + + Offset? a, b; + switch (Theme.of(context).platform) { + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + case TargetPlatform.macOS: + a = _pointerOffset; + if (_anchorARect != null && _anchorBRect != null) { + switch (_textSelA?.direction) { + case PdfTextDirection.ltr: + if (_anchorARect!.inflate(16).contains(a)) { + a = rectA.bottomLeft.translate(0, 8); + } + final selRect = rectA.expandToInclude(rectB); + if (selRect.height < 60 && selRect.width < 250) { + a = _anchorBRect!.bottomRight; + } + if (_anchorBRect!.inflate(16).contains(a)) { + a = _anchorBRect!.bottomRight; + } + case PdfTextDirection.rtl: + case PdfTextDirection.vrtl: + final distA = (_pointerOffset - _anchorARect!.center).distanceSquared; + final distB = (_pointerOffset - _anchorBRect!.center).distanceSquared; + if (distA < distB) { + a = _anchorARect!.bottomLeft.translate(8, 8); + } else { + a = _anchorBRect!.topRight.translate(8, 8); + } + default: + } + } + default: + a = localOffset; + switch (_textSelA?.direction) { + case PdfTextDirection.ltr: + a ??= _anchorARect?.topLeft; + b = localOffset == null ? _anchorBRect?.bottomLeft : null; + case PdfTextDirection.rtl: + case PdfTextDirection.vrtl: + a ??= _anchorARect?.topRight; + b = localOffset == null ? _anchorBRect?.bottomRight : null; + default: + } + } + + contextMenu = createContextMenu(a, b, _contextMenuFor); + if (contextMenu != null && !isPositionalWidget(contextMenu)) { + final textAnchor = _selPartLastMoved == _TextSelectionPart.a + ? _textSelA + : _selPartLastMoved == _TextSelectionPart.b + ? _textSelB + : null; + final anchorLocalRect = _selPartLastMoved == _TextSelectionPart.a ? rectA : rectB; + final handleLocalRect = _selPartLastMoved == _TextSelectionPart.a ? _anchorARect : _anchorBRect; + final offset = localOffset != null + ? normalizeWidgetPosition(localOffset, _contextMenuRect?.size) + : (calcPosition(_contextMenuRect?.size, anchorLocalRect, handleLocalRect, textAnchor, _pointerOffset) ?? + Offset.zero); + + contextMenu = Positioned( + left: offset.dx, + top: offset.dy, + child: WidgetSizeSniffer( + key: Key('contextMenu'), + child: contextMenu, + onSizeChanged: (rect) { + _contextMenuRect = rect.toLocal(context); + _invalidate(); + }, + ), + ); + } else { + _contextMenuRect = null; + } + } + + if (textAnchorMoving == _TextSelectionPart.a || textAnchorMoving == _TextSelectionPart.b) { + _selPartLastMoved = textAnchorMoving; + } + + return [ + if (anchorA != null) + Positioned( + left: aLeft, + right: aRight, + bottom: aBottom, + child: MouseRegion( + cursor: _selPartMoving == _TextSelectionPart.a ? SystemMouseCursors.none : SystemMouseCursors.move, + onEnter: (details) => _onSelectionHandleEnter(_TextSelectionPart.a, details), + onExit: (details) => _onSelectionHandleExit(_TextSelectionPart.a, details), + onHover: (details) => _onSelectionHandleHover(_TextSelectionPart.a, details), + child: GestureDetector( + onPanStart: (details) => _onSelectionHandlePanStart(_TextSelectionPart.a, details), + onPanUpdate: (details) => _onSelectionHandlePanUpdate(_TextSelectionPart.a, details), + onPanEnd: (details) => _onSelectionHandlePanEnd(_TextSelectionPart.a, details), + child: WidgetSizeSniffer( + key: Key('anchorA'), + child: anchorA, + onSizeChanged: (rect) { + _anchorARect = rect.toLocal(context); + _invalidate(); + }, + ), + ), + ), + ), + if (anchorB != null) + Positioned( + left: bLeft, + top: bTop, + right: bRight, + child: MouseRegion( + cursor: _selPartMoving == _TextSelectionPart.b ? SystemMouseCursors.none : SystemMouseCursors.move, + onEnter: (details) => _onSelectionHandleEnter(_TextSelectionPart.b, details), + onExit: (details) => _onSelectionHandleExit(_TextSelectionPart.b, details), + onHover: (details) => _onSelectionHandleHover(_TextSelectionPart.b, details), + + child: GestureDetector( + onPanStart: (details) => _onSelectionHandlePanStart(_TextSelectionPart.b, details), + onPanUpdate: (details) => _onSelectionHandlePanUpdate(_TextSelectionPart.b, details), + onPanEnd: (details) => _onSelectionHandlePanEnd(_TextSelectionPart.b, details), + child: WidgetSizeSniffer( + key: Key('anchorB'), + child: anchorB, + onSizeChanged: (rect) { + _anchorBRect = rect.toLocal(context); + _invalidate(); + }, + ), + ), + ), + ), + if (magnifier != null) magnifier, + if (contextMenu != null) contextMenu, + ]; + } + + /// Calculate the clamped pointer position for the magnifier content. + /// + /// When the magnifier widget is clamped to stay within viewport bounds (e.g., near screen edges), + /// we adjust the pointer position by the same amount. This enables [PdfViewerGetMagnifierRectForAnchor] + /// and [PdfViewerMagnifierBuilder] to use the clamped pointer position to effectively "freeze" the + /// magnifier content, preventing it from sliding inside the magnifier. + /// + /// Returns the clamped pointer position in viewport coordinates. + Offset _calcClampedPointerPosition( + Offset pointerOffset, + Offset magnifierPosition, + Size? magnifierSize, + PdfTextSelectionAnchor textAnchor, + ) { + var clampedPointerOffset = pointerOffset; + + if (magnifierSize != null) { + // What the magnifier X position would be without clamping (centered on pointer) + final unclampedLeft = pointerOffset.dx - magnifierSize.width / 2; + // How much it was actually clamped by calcPosition + final clampAmount = magnifierPosition.dx - unclampedLeft; + // Adjust pointer position by the same clamp amount to freeze content + clampedPointerOffset = Offset(pointerOffset.dx + clampAmount, pointerOffset.dy); + } + return clampedPointerOffset; + } + + bool _shouldShowMagnifierForAnchor( + PdfTextSelectionAnchor textAnchor, + PdfViewerController controller, + PdfViewerSelectionMagnifierParams params, + ) { + final h = textAnchor.direction == PdfTextDirection.vrtl ? textAnchor.rect.size.width : textAnchor.rect.size.height; + return h * _currentZoom < params.magnifierSizeThreshold; + } + + Widget _buildHandle(BuildContext context, Path path, PdfViewerTextSelectionAnchorHandleState state) { + final baseColor = + Theme.of(context).textSelectionTheme.selectionColor ?? DefaultSelectionStyle.of(context).selectionColor!; + final (selectionColor, shadow) = switch (state) { + PdfViewerTextSelectionAnchorHandleState.normal => (baseColor.withValues(alpha: .7), true), + PdfViewerTextSelectionAnchorHandleState.dragging => (baseColor.withValues(alpha: 1), false), + PdfViewerTextSelectionAnchorHandleState.hover => (baseColor.withValues(alpha: 1), true), + }; + return CustomPaint( + painter: _CustomPainter.fromFunctions((canvas, size) { + if (shadow) { + canvas.drawShadow(path, Colors.black, 4, true); + } + canvas.drawPath( + path, + Paint() + ..color = selectionColor + ..style = PaintingStyle.fill, + ); + }, hitTestFunction: (position) => position.dx >= 0 && position.dx <= 30 && position.dy >= 0 && position.dy <= 30), + size: Size(30, 30), + ); + } + + Widget? _buildDefaultSelectionHandle( + BuildContext context, + PdfTextSelectionAnchor anchor, + PdfViewerTextSelectionAnchorHandleState state, + ) { + switch (anchor.direction) { + case PdfTextDirection.ltr: + if (anchor.type == PdfTextSelectionAnchorType.a) { + return _buildHandle( + context, + Path() + ..moveTo(30, 0) + ..lineTo(30, 30) + ..lineTo(0, 30) + ..close(), + state, + ); + } else { + return _buildHandle( + context, + Path() + ..moveTo(0, 0) + ..lineTo(30, 0) + ..lineTo(0, 30) + ..close(), + state, + ); + } + case PdfTextDirection.rtl: + case PdfTextDirection.vrtl: + if (anchor.type == PdfTextSelectionAnchorType.a) { + return _buildHandle( + context, + Path() + ..moveTo(0, 30) + ..lineTo(30, 30) + ..lineTo(0, 0) + ..close(), + state, + ); + } else { + return _buildHandle( + context, + Path() + ..moveTo(0, 0) + ..lineTo(30, 0) + ..lineTo(30, 30) + ..close(), + state, + ); + } + + case PdfTextDirection.unknown: + return _buildHandle( + context, + Path() + ..moveTo(0, 0) + ..lineTo(30, 0) + ..lineTo(30, 30) + ..lineTo(0, 30) + ..close(), + state, + ); + } + } + + Rect _getMagnifierRect( + PdfTextSelectionAnchor textAnchor, + PdfViewerSelectionMagnifierParams params, + Offset clampedPointerPosition, + ) { + final c = textAnchor.page.charRects[textAnchor.index]; + + final (width, height) = switch (_document!.pages[textAnchor.page.pageNumber - 1].rotation.index & 1) { + 0 => (c.width, c.height), + _ => (c.height, c.width), + }; + + final (baseUnit, v, h) = switch (textAnchor.direction) { + PdfTextDirection.ltr || PdfTextDirection.rtl || PdfTextDirection.unknown => (height, 2.0, 0.2), + PdfTextDirection.vrtl => (width, 0.2, 2.0), + }; + return Rect.fromLTRB( + textAnchor.rect.left - baseUnit * v, + textAnchor.rect.top - baseUnit * h, + textAnchor.rect.right + baseUnit * v, + textAnchor.rect.bottom + baseUnit * h, + ); + } + + Widget _buildMagnifier(BuildContext context, Rect rectToDraw, PdfViewerSelectionMagnifierParams magnifierParams) { + return LayoutBuilder( + builder: (context, constraints) { + return ClipRect( + clipBehavior: Clip.antiAlias, + child: CustomPaint( + painter: _CustomPainter.fromFunctions((canvas, size) { + final magScale = max(size.width / rectToDraw.width, size.height / rectToDraw.height); + canvas.save(); + canvas.scale(magScale); + canvas.translate(-rectToDraw.left, -rectToDraw.top); + _paintPagesCustom( + canvas, + cache: _magnifierImageCache, + maxImageCacheBytes: + widget.params.textSelectionParams?.magnifier?.maxImageBytesCachedOnMemory ?? + PdfViewerSelectionMagnifierParams.defaultMaxImageBytesCachedOnMemory, + targetRect: rectToDraw, + scale: magScale * MediaQuery.of(context).devicePixelRatio, + enableLowResolutionPagePreview: true, + filterQuality: FilterQuality.low, + ); + canvas.restore(); + }), + size: Size(constraints.maxWidth, constraints.maxHeight), + ), + ); + }, + ); + } + + Widget _buildMagnifierDecoration( + BuildContext context, + PdfTextSelectionAnchor textAnchor, + PdfViewerSelectionMagnifierParams params, + Widget child, + Size childSize, + Offset pointerPosition, + Offset magnifierPosition, + ) { + final scale = 80 / min(childSize.width, childSize.height); + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 8, spreadRadius: 2)], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(30), + child: SizedBox(width: childSize.width * scale, height: childSize.height * scale, child: child), + ), + ); + } + + Widget? _buildContextMenu(BuildContext context, PdfViewerContextMenuBuilderParams params) { + final items = [ + if (params.isTextSelectionEnabled && + params.textSelectionDelegate.isCopyAllowed && + params.textSelectionDelegate.hasSelectedText) + ContextMenuButtonItem( + onPressed: () => params.textSelectionDelegate.copyTextSelection(), + type: ContextMenuButtonType.copy, + ), + if (params.isTextSelectionEnabled && !params.textSelectionDelegate.isSelectingAllText) + ContextMenuButtonItem( + onPressed: () => params.textSelectionDelegate.selectAllText(), + type: ContextMenuButtonType.selectAll, + ), + ]; + + widget.params.customizeContextMenuItems?.call(params, items); + + if (items.isEmpty) { + return null; + } + + return Align( + alignment: Alignment.topLeft, + child: AdaptiveTextSelectionToolbar.buttonItems( + anchors: TextSelectionToolbarAnchors(primaryAnchor: params.anchorA, secondaryAnchor: params.anchorB), + buttonItems: items, + ), + ); + } + + void _onSelectionHandlePanStart(_TextSelectionPart handle, DragStartDetails details) { + if (_isInteractionGoingOn) return; + _selPartMoving = handle; + _isSelectingAllText = false; + final position = _globalToDocument(details.globalPosition); + final anchor = Offset(_txController.value.x, _txController.value.y); + if (_selPartMoving == _TextSelectionPart.a) { + _textSelectAnchor = anchor + _textSelA!.rect.topLeft - position!; + final a = _findTextAndIndexForPoint(_textSelA!.rect.center); + if (a == null) return; + _selA = a; + // Notify drag start callback + widget.params.textSelectionParams?.onSelectionHandlePanStart?.call(_textSelA!); + } else if (_selPartMoving == _TextSelectionPart.b) { + _textSelectAnchor = anchor + _textSelB!.rect.bottomRight - position!; + final b = _findTextAndIndexForPoint(_textSelB!.rect.center); + if (b == null) return; + _selB = b; + // Notify drag start callback + widget.params.textSelectionParams?.onSelectionHandlePanStart?.call(_textSelB!); + } else { + return; + } + _updateTextSelection(); + _requestFocus(); + } + + bool _updateSelectionHandlesPan(Offset? panTo) { + if (panTo == null) { + return false; + } + if (_selPartMoving == _TextSelectionPart.a) { + final a = _findTextAndIndexForPoint( + panTo + _textSelectAnchor! - Offset(_txController.value.x, _txController.value.y), + ); + if (a == null) { + return false; + } + _selA = a; + } else if (_selPartMoving == _TextSelectionPart.b) { + final b = _findTextAndIndexForPoint( + panTo + _textSelectAnchor! - Offset(_txController.value.x, _txController.value.y), + ); + if (b == null) { + return false; + } + _selB = b; + } else { + return false; + } + _updateTextSelection(); + return true; + } + + void _onSelectionHandlePanUpdate(_TextSelectionPart handle, DragUpdateDetails details) { + if (_isInteractionGoingOn) return; + _contextMenuDocumentPosition = null; + _updateSelectionHandlesPan(_globalToDocument(details.globalPosition)); + // Notify drag update callback + final anchor = handle == _TextSelectionPart.a ? _textSelA : _textSelB; + if (anchor != null) { + widget.params.textSelectionParams?.onSelectionHandlePanUpdate?.call(anchor, details.delta); + } + } + + void _onSelectionHandlePanEnd(_TextSelectionPart handle, DragEndDetails details) { + if (_isInteractionGoingOn) return; + final result = _updateSelectionHandlesPan(_globalToDocument(details.globalPosition)); + // Notify drag end callback before clearing state + final anchor = handle == _TextSelectionPart.a ? _textSelA : _textSelB; + if (anchor != null) { + widget.params.textSelectionParams?.onSelectionHandlePanEnd?.call(anchor); + } + + _selPartMoving = _TextSelectionPart.none; + _isSelectingAllText = false; + if (!result) { + _updateTextSelection(); + } + } + + void _onSelectionHandleEnter(_TextSelectionPart handle, PointerEnterEvent details) { + _hoverOn = handle; + _invalidate(); + } + + void _onSelectionHandleExit(_TextSelectionPart handle, PointerExitEvent details) { + _hoverOn = _TextSelectionPart.none; + _invalidate(); + } + + void _onSelectionHandleHover(_TextSelectionPart handle, PointerHoverEvent details) { + _hoverOn = handle; + _invalidate(); + } + + void _clearTextSelections({bool invalidate = true}) { + _selA = _selB = null; + _textSelA = _textSelB = null; + _contextMenuDocumentPosition = null; + _isSelectingAllText = false; + _updateTextSelection(invalidate: invalidate); + } + + @override + Future clearTextSelection() async => _clearTextSelections(); + + @override + PdfTextSelectionRange? get textSelectionPointRange { + final a = _selA; + final b = _selB; + if (a == null || b == null) { + return null; + } + return PdfTextSelectionRange.fromPoints(a, b); + } + + @override + Future setTextSelectionPointRange(PdfTextSelectionRange range) async { + if (_selA == range.start && _selB == range.end) { + return; + } + _selA = range.start; + _selB = range.end; + if (_selA! > _selB!) { + final temp = _selA; + _selA = _selB; + _selB = temp; + } + _textSelA = _textSelB = null; + _contextMenuDocumentPosition = null; + _isSelectingAllText = false; + _updateTextSelection(); + } + + PdfPageTextRange? _loadTextSelectionForPageNumber(int pageNumber) { + final a = _selA; + final b = _selB; + if (a == null || b == null) { + return null; + } + final first = a.text.pageNumber < b.text.pageNumber ? a : b; + final second = a.text.pageNumber < b.text.pageNumber ? b : a; + if (first.text.pageNumber == second.text.pageNumber && first.text.pageNumber == pageNumber) { + return a.text.getRangeFromAB(a.index, b.index); + } + if (first.text.pageNumber == pageNumber) { + return PdfPageTextRange(pageText: first.text, start: a.index, end: first.text.charRects.length); + } + if (second.text.pageNumber == pageNumber) { + return PdfPageTextRange(pageText: second.text, start: 0, end: b.index + 1); + } + if (first.text.pageNumber < pageNumber && pageNumber < second.text.pageNumber) { + final text = _getCachedTextOrDelayLoadText(pageNumber, onTextLoaded: () => _invalidate()); + if (text == null) return null; + return PdfPageTextRange(pageText: text, start: 0, end: text.fullText.length); + } + return null; + } + + @override + bool get hasSelectedText => _selA != null && _selB != null; + + @override + Future> getSelectedTextRanges() async { + final a = _selA; + final b = _selB; + if (a == null || b == null) { + return []; + } + final first = a.text.pageNumber < b.text.pageNumber ? a : b; + final second = a.text.pageNumber < b.text.pageNumber ? b : a; + if (first.text.pageNumber == second.text.pageNumber) { + return [a.text.getRangeFromAB(a.index, b.index)]; + } + final selections = [a.text.getRangeFromAB(a.index, a.text.charRects.length - 1)]; + + for (var i = first.text.pageNumber + 1; i < second.text.pageNumber; i++) { + final text = await _loadTextAsync(i); + if (text == null || text.fullText.isEmpty) continue; + selections.add(text.getRangeFromAB(0, text.charRects.length - 1)); + } + + selections.add(second.text.getRangeFromAB(0, b.index)); + return selections; + } + + @override + Future getSelectedText() async { + final selections = await getSelectedTextRanges(); + if (selections.isEmpty) return ''; + return selections.map((e) => e.text).join(); + } + + @override + bool get isTextSelectionEnabled => widget.params.textSelectionParams?.enabled ?? true; + + @override + bool get isCopyAllowed => _document!.permissions?.allowsCopying != false; + + @override + bool get isSelectingAllText => _isSelectingAllText; + + @override + Future selectAllText() async { + if (_document!.pages.isEmpty && _layout != null) return; + PdfPageText? first; + for (var i = 1; i <= _document!.pages.length; i++) { + final text = await _loadTextAsync(i); + if (text == null || text.fullText.isEmpty) continue; + first = text; + break; + } + PdfPageText? last; + for (var i = _document!.pages.length; i >= 1; i--) { + final text = await _loadTextAsync(i); + if (text == null || text.fullText.isEmpty) continue; + last = text; + break; + } + + if (first != null && last != null) { + _selA = _findTextAndIndexForPoint( + first.charRects.first.center.toOffsetInDocument( + page: _document!.pages[first.pageNumber - 1], + pageRect: _layout!.pageLayouts[first.pageNumber - 1], + ), + ); + _selB = _findTextAndIndexForPoint( + last.charRects.last.center.toOffsetInDocument( + page: _document!.pages[last.pageNumber - 1], + pageRect: _layout!.pageLayouts[last.pageNumber - 1], + ), + ); + } else { + _selA = _selB = null; + } + _textSelA = _textSelB = null; + _contextMenuDocumentPosition = null; + _selPartLastMoved = _TextSelectionPart.none; + _isSelectingAllText = true; + _updateTextSelection(); + } + + @override + Future selectWord(Offset offset, {PointerDeviceKind? deviceKind}) async { + for (var i = 0; i < _document!.pages.length; i++) { + final pageRect = _layout!.pageLayouts[i]; + if (!pageRect.contains(offset)) { + continue; + } + + final text = await _loadTextAsync(i + 1); + if (text == null || text.fullText.isEmpty) { + continue; + } + final page = _document!.pages[i]; + final point = offset + .translate(-pageRect.left, -pageRect.top) + .toPdfPoint(page: page, scaledPageSize: pageRect.size); + final f = text.fragments.firstWhereOrNull((f) => f.bounds.containsPoint(point)); + if (f == null) { + continue; + } + final range = PdfPageTextRange(pageText: text, start: f.index, end: f.end); + final selectionRect = f.bounds.toRectInDocument(page: page, pageRect: pageRect); + _selA = PdfTextSelectionPoint(text, f.index); + _selB = PdfTextSelectionPoint(text, f.end - 1); + _textSelA = PdfTextSelectionAnchor( + selectionRect, + range.pageText.getFragmentForTextIndex(range.start)?.direction ?? PdfTextDirection.ltr, + PdfTextSelectionAnchorType.a, + text, + _selA!.index, + ); + _textSelB = _textSelA!.copyWith(type: PdfTextSelectionAnchorType.b, index: _selB!.index); + _textSelectAnchor = Offset(_txController.value.x, _txController.value.y); + break; + } + + _selPartMoving = _TextSelectionPart.none; + _selPartLastMoved = _TextSelectionPart.a; + _isSelectingAllText = false; + _selectionPointerDeviceKind = deviceKind; + _notifyTextSelectionChange(); + } + + Future _copyTextSelection() async { + if (_document!.permissions?.allowsCopying == false) return false; + setClipboardData(await getSelectedText()); + return true; + } + + @override + Future copyTextSelection() async { + final result = await _copyTextSelection(); + _clearTextSelections(); + return result; + } + + @override + Offset? offsetToLocal(BuildContext context, Offset? position) { + if (position == null) return null; + final renderBox = context.findRenderObject() as RenderBox?; + if (renderBox == null) return null; + final global = _documentToGlobal(position); + if (global == null) return null; + return renderBox.globalToLocal(global); + } + + @override + Rect? rectToLocal(BuildContext context, Rect? rect) { + if (rect == null) return null; + final renderBox = context.findRenderObject() as RenderBox?; + if (renderBox == null) return null; + final globalTopLeft = _documentToGlobal(rect.topLeft); + final globalBottomRight = _documentToGlobal(rect.bottomRight); + if (globalTopLeft == null || globalBottomRight == null) return null; + return Rect.fromPoints(renderBox.globalToLocal(globalTopLeft), renderBox.globalToLocal(globalBottomRight)); + } + + @override + Offset? offsetToDocument(BuildContext context, Offset? position) { + if (position == null) return null; + final renderBox = context.findRenderObject() as RenderBox?; + if (renderBox == null) return null; + final global = renderBox.localToGlobal(position); + return _globalToDocument(global); + } + + @override + Rect? rectToDocument(BuildContext context, Rect? rect) { + if (rect == null) return null; + final renderBox = context.findRenderObject() as RenderBox?; + if (renderBox == null) return null; + final globalTopLeft = renderBox.localToGlobal(rect.topLeft); + final globalBottomRight = renderBox.localToGlobal(rect.bottomRight); + final docTopLeft = _globalToDocument(globalTopLeft); + final docBottomRight = _globalToDocument(globalBottomRight); + if (docTopLeft == null || docBottomRight == null) return null; + return Rect.fromPoints(docTopLeft, docBottomRight); + } + + @override + PdfViewerCoordinateConverter get doc2local => this; + + void forceRepaintAllPageImages() { + _imageCache.cancelAllPendingRenderings(); + _magnifierImageCache.cancelAllPendingRenderings(); + _imageCache.releaseAllImages(); + _magnifierImageCache.releaseAllImages(); + _invalidate(); + } +} + +class _PdfPageImageCache { + final pageImages = {}; + final pageImageRenderingTimers = {}; + final pageImagesPartial = {}; + final cancellationTokens = >{}; + final pageImagePartialRenderingRequests = {}; + + void addCancellationToken(int pageNumber, PdfPageRenderCancellationToken token) { + var tokens = cancellationTokens.putIfAbsent(pageNumber, () => []); + tokens.add(token); + } + + void releasePartialImages() { + for (final request in pageImagePartialRenderingRequests.values) { + request.cancel(); + } + pageImagePartialRenderingRequests.clear(); + for (final image in pageImagesPartial.values) { + image.image.dispose(); + } + pageImagesPartial.clear(); + } + + void releaseAllImages() { + for (final timer in pageImageRenderingTimers.values) { + timer.cancel(); + } + pageImageRenderingTimers.clear(); + for (final request in pageImagePartialRenderingRequests.values) { + request.cancel(); + } + pageImagePartialRenderingRequests.clear(); + for (final image in pageImages.values) { + image.image.dispose(); + } + pageImages.clear(); + for (final image in pageImagesPartial.values) { + image.image.dispose(); + } + pageImagesPartial.clear(); + } + + void cancelPendingRenderings(int pageNumber) { + final tokens = cancellationTokens[pageNumber]; + if (tokens != null) { + for (final token in tokens) { + token.cancel(); + } + tokens.clear(); + } + } + + void cancelAllPendingRenderings() { + for (final pageNumber in cancellationTokens.keys) { + cancelPendingRenderings(pageNumber); + } + cancellationTokens.clear(); + } + + void makeCacheImageForPageDirty(int pageNumber) { + final image = pageImages[pageNumber]; + if (image != null) { + image.isDirty = true; + } + final imagePartial = pageImagesPartial[pageNumber]; + if (imagePartial != null) { + imagePartial.isDirty = true; + } + } + + void removeCacheImagesForPage(int pageNumber) { + final removed = pageImages.remove(pageNumber); + if (removed != null) { + removed.image.dispose(); + } + pageImagesPartial.remove(pageNumber)?.dispose(); + } + + void removeCacheImagesIfCacheBytesExceedsLimit( + List pageNumbers, + int acceptableBytes, + PdfPage currentPage, { + required double Function(int pageNumber) dist, + }) { + pageNumbers.sort((a, b) => dist(b).compareTo(dist(a))); + int getBytesConsumed(ui.Image? image) => image == null ? 0 : (image.width * image.height * 4).toInt(); + var bytesConsumed = + pageImages.values.fold(0, (sum, e) => sum + getBytesConsumed(e.image)) + + pageImagesPartial.values.fold(0, (sum, e) => sum + getBytesConsumed(e.image)); + for (final key in pageNumbers) { + final removed = pageImages.remove(key); + if (removed != null) { + bytesConsumed -= getBytesConsumed(removed.image); + removed.image.dispose(); + } + final removedPartial = pageImagesPartial.remove(key); + if (removedPartial != null) { + bytesConsumed -= getBytesConsumed(removedPartial.image); + removedPartial.dispose(); + } + if (bytesConsumed <= acceptableBytes) { + break; + } + } + } +} + +class _PdfPartialImageRenderingRequest { + _PdfPartialImageRenderingRequest(this.timer, this.cancellationToken); + final Timer timer; + final PdfPageRenderCancellationToken cancellationToken; + + void cancel() { + timer.cancel(); + cancellationToken.cancel(); + } +} + +class _PdfImageWithScale { + _PdfImageWithScale(this.image, this.scale); + final ui.Image image; + final double scale; + + int get width => image.width; + int get height => image.height; + + bool isDirty = false; + + void dispose() { + image.dispose(); + } +} + +class _PdfImageWithScaleAndRect extends _PdfImageWithScale { + _PdfImageWithScaleAndRect(super.image, super.scale, this.rect, this.left, this.top); + final Rect rect; + final int left; + final int top; + + int get bottom => top + height; + int get right => left + width; + + void draw(Canvas canvas, [FilterQuality filterQuality = FilterQuality.low]) { + canvas.drawImageRect( + image, + Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble()), + rect, + Paint()..filterQuality = filterQuality, + ); + } + + void drawNoScale(Canvas canvas, int x, int y, [FilterQuality filterQuality = FilterQuality.low]) { + canvas.drawImage( + image, + Offset((left - x).toDouble(), (top - y).toDouble()), + Paint()..filterQuality = filterQuality, + ); + } +} + +class _PdfViewerTransformationController extends TransformationController { + _PdfViewerTransformationController(this._state); + + final _PdfViewerState _state; + + @override + set value(Matrix4 newValue) { + super.value = _state._makeMatrixInSafeRange(newValue); + } +} + +/// What selection part is moving by mouse-dragging/finger-panning. +enum _TextSelectionPart { none, free, a, b } + +/// Represents a point (combination of page and character index) in the text selection. +/// It contains the [PdfPageText] and the index of the character in that text. +@immutable +class PdfTextSelectionPoint { + const PdfTextSelectionPoint(this.text, this.index); + + /// The page text associated with this selection point. + final PdfPageText text; + + /// The index of the character in the [text]. + /// + /// Similar to [PdfPageText.getRangeFromAB], this index is inclusive; even for the end point of the selection. + /// In other words, for the end of the selection, the index points to the last selected character. + final int index; + + /// Whether the index is valid in the [text]. + bool get isValid => index >= 0 && index < text.charRects.length; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! PdfTextSelectionPoint) return false; + return text == other.text && index == other.index; + } + + bool operator <(PdfTextSelectionPoint other) { + if (text.pageNumber != other.text.pageNumber) { + return text.pageNumber < other.text.pageNumber; + } + return index < other.index; + } + + bool operator >(PdfTextSelectionPoint other) => !(this <= other); + + bool operator <=(PdfTextSelectionPoint other) { + if (text.pageNumber != other.text.pageNumber) { + return text.pageNumber < other.text.pageNumber; + } + return index <= other.index; + } + + bool operator >=(PdfTextSelectionPoint other) => !(this < other); + + @override + int get hashCode => text.hashCode ^ index.hashCode; + + @override + String toString() => '$PdfTextSelectionPoint(text: $text, index: $index)'; +} + +/// Represents a range of text selection between two points. +class PdfTextSelectionRange { + /// Creates a [PdfTextSelectionRange] from two selection points. + /// + /// The points can be in any order; the constructor will ensure that [start] is less than or equal to [end]. + PdfTextSelectionRange.fromPoints(PdfTextSelectionPoint a, PdfTextSelectionPoint b) + : start = a <= b ? a : b, + end = a <= b ? b : a; + + /// The start point of the text selection. + final PdfTextSelectionPoint start; + + /// The end point of the text selection. + /// + /// Please note that the index of this point is inclusive; it points to the last selected character. + final PdfTextSelectionPoint end; +} + +/// Represents the anchor point of the text selection. +/// +/// It contains the rectangle of the anchor point, the text direction, and the type of the anchor (A or B). +@immutable +class PdfTextSelectionAnchor { + const PdfTextSelectionAnchor(this.rect, this.direction, this.type, this.page, this.index); + + /// The rectangle of the character, on which the anchor is associated to. + /// + /// This rectangle is in the document coordinates. + final Rect rect; + + /// The text direction of the anchored character. + final PdfTextDirection direction; + + /// The type of the anchor, either [PdfTextSelectionAnchorType.a] or [PdfTextSelectionAnchorType.b]. + final PdfTextSelectionAnchorType type; + + /// The page text on which the anchor is associated to. + final PdfPageText page; + + /// The index of the character in [page]. + /// + /// Please note that the index is always inclusive, even for the end anchor (B); + /// + /// When selecting `"Selection"` in `"This is a Selection."`, the index of the start anchor (A) is 10, + /// and the index of the end anchor (B) is 19 (not 18). + /// + /// ``` + /// A B + /// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + /// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + /// | T | h | i | s | | i | s | | a | | S | e | l | e | c | t | i | o | n | . | + /// +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ + /// ``` + final int index; + + /// The point of the anchor in the document coordinates, which is an apex of [rect] depending on + /// the [direction] and [type]. + Offset get anchorPoint { + switch (direction) { + case PdfTextDirection.ltr: + case PdfTextDirection.unknown: + return type == PdfTextSelectionAnchorType.a ? rect.topLeft : rect.bottomRight; + case PdfTextDirection.rtl: + return type == PdfTextSelectionAnchorType.a ? rect.topRight : rect.bottomLeft; + case PdfTextDirection.vrtl: + return type == PdfTextSelectionAnchorType.a ? rect.topRight : rect.bottomLeft; + } + } + + /// Copies the current instance with the given parameters. + PdfTextSelectionAnchor copyWith({ + Rect? rect, + PdfTextDirection? direction, + PdfTextSelectionAnchorType? type, + PdfPageText? page, + int? index, + }) { + return PdfTextSelectionAnchor( + rect ?? this.rect, + direction ?? this.direction, + type ?? this.type, + page ?? this.page, + index ?? this.index, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! PdfTextSelectionAnchor) return false; + return rect == other.rect && + direction == other.direction && + type == other.type && + page == other.page && + index == other.index; + } + + @override + int get hashCode { + return rect.hashCode ^ direction.hashCode ^ type.hashCode ^ page.hashCode ^ index.hashCode; + } +} + +/// Defines the type of the text selection anchor. +/// +/// It can be either [a] or [b], which represents the start and end of the selection respectively. +enum PdfTextSelectionAnchorType { a, b } + +/// Defines how the PDF pages should fit within the viewport. +enum FitMode { + /// Entire page/spread visible (may have letterboxing). + fit, + + /// Fill viewport along the cross axis (may crop content). + fill, + + /// Legacy cover mode - ensures the whole document fills the viewport (may crop content). + cover, + + /// No scaling applied. + none, +} + +/// Defines how pages transition when navigating through the document. +enum PageTransition { + /// Pages flow continuously in an uninterrupted scrollable view. + /// Similar to browsing a webpage - all pages are laid out sequentially. + continuous, + + /// Pages transition discretely, one page (or spread for facing pages) at a time. + /// + /// When a pan gesture ends at fit zoom, the viewer snaps to either: + /// - The current page/spread (if insufficient movement) + /// - The next/previous page/spread (if swipe velocity > 300 px/s or dragged > 50% threshold) + /// + /// Important behaviors: + /// - Only applies to pan-only gestures (zoom/pinch gestures work normally) + /// - Only active when at or near fit zoom level (free panning when zoomed in) + /// - Provides a controlled, book-like reading experience + discrete, +} + +/// Represents the result of the hit test on the page. +class PdfPageHitTestResult { + PdfPageHitTestResult({required this.page, required this.offset}); + + /// The page that was hit. + final PdfPage page; + + /// The offset in the PDF page coordinates; the origin is at the bottom-left corner. + final PdfPoint offset; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is PdfPageHitTestResult && other.page == page && other.offset == offset; + } + + @override + int get hashCode => page.hashCode ^ offset.hashCode; +} + +/// Controls associated [PdfViewer]. +/// +/// It's always your option to extend (inherit) the class to customize the [PdfViewer] behavior. +/// +/// Please note that almost all fields and functions are not working if the controller is not associated +/// to any [PdfViewer]. +/// You can check whether the controller is associated or not by checking [isReady] property. +class PdfViewerController extends ValueListenable { + _PdfViewerState? __state; + final _listeners = []; + + void _attach(_PdfViewerState? state) { + __state?._txController.removeListener(_notifyListeners); + __state = state; + __state?._txController.addListener(_notifyListeners); + } + + void _notifyListeners() { + for (final listener in _listeners) { + listener(); + } + } + + _PdfViewerState get _state => __state!; + + /// Get the associated [PdfViewer] widget. + PdfViewer get widget => _state.widget; + + /// Get the associated [PdfViewerParams] parameters. + PdfViewerParams get params => widget.params; + + /// Determine whether the document/pages are ready or not. + bool get isReady => __state?._document?.pages != null; + + /// The document layout size. + Size get documentSize => _state._layout!.documentSize; + + /// Page layout. + PdfPageLayout get layout => _state._layout!; + + /// The view port size (The widget's client area's size) + Size get viewSize => _state._viewSize!; + + /// **DEPRECATED:** The zoom ratio for FitMode.cover (fills viewport, may crop content). + /// + /// This getter calculates the cover scale on-demand. Consider using `PdfViewerParams(fitMode: FitMode.cover)` instead. + /// This API will be removed in a future version. + @Deprecated('Use PdfViewerParams(fitMode: FitMode.cover) to set cover mode. This getter will be removed.') + double get coverScale => _state._calculateScaleForMode(FitMode.cover); + + /// **DEPRECATED:** The zoom ratio for FitMode.fit (shows whole page in viewport). + /// + /// This getter calculates the fit scale on-demand. Consider using `PdfViewerParams(fitMode: FitMode.fit)` instead. + /// This API will be removed in a future version. + @Deprecated('Use PdfViewerParams(fitMode: FitMode.fit) to set fit mode. This getter will be removed.') + double? get alternativeFitScale => _state._calculateScaleForMode(FitMode.fit); + + /// The minimum zoom ratio allowed. + double get minScale => _state.minScale; + + /// The area of the document layout which is visible on the view port. + Rect get visibleRect => _state._visibleRect; + + /// Get the associated document. + /// + /// Please note that the field does not ensure that the [PdfDocument] is alive during long asynchronous operations. + /// + /// **If you want to do some time consuming asynchronous operation, consider to use [useDocument] instead.** + PdfDocument get document => _state._document!; + + /// Get the associated pages. + /// + /// Please note that the field does not ensure that the associated [PdfDocument] is alive during long asynchronous + /// operations. For page count, use [pageCount] instead. + /// + /// **If you want to do some time consuming asynchronous operation, consider to use [useDocument] instead.** + List get pages => _state._document!.pages; + + /// Get the page count of the document. + int get pageCount => _state._document!.pages.length; + + /// The current page number if available. + int? get pageNumber => _state._pageNumber; + + /// The range of all visible pages (any page with any intersection with the viewport). + /// Returns null if no pages are visible. + /// This is useful for displaying page ranges in UI elements like scroll thumbs, + /// especially when zoomed out or using spread layouts where multiple pages are visible. + PdfPageRange? get visiblePageRange => _state._visiblePageRange; + + /// The document reference associated to the [PdfViewer]. + PdfDocumentRef get documentRef => _state.widget.documentRef; + + /// Within call to the function, it ensures that the [PdfDocument] is alive (not null and not disposed). + /// + /// If [ensureLoaded] is true, it tries to ensure that the document is loaded. + /// If the document is not loaded, the function does not call [task] and return null. + /// [cancelLoading] is used to cancel the loading process. + /// + /// The following fragment explains how to use [PdfDocument]: + /// + /// ```dart + /// await controller.useDocument( + /// (document) async { + /// // Use the document here + /// }, + /// ); + /// ``` + /// + /// This is just a shortcut for the combination of [PdfDocumentRef.resolveListenable] and [PdfDocumentListenable.useDocument]. + /// + /// For more information, see [PdfDocumentRef], [PdfDocumentRef.resolveListenable], and [PdfDocumentListenable.useDocument]. + FutureOr useDocument( + FutureOr Function(PdfDocument document) task, { + bool ensureLoaded = true, + Completer? cancelLoading, + }) => documentRef.resolveListenable().useDocument(task, ensureLoaded: ensureLoaded, cancelLoading: cancelLoading); + + @override + Matrix4 get value => _state._txController.value; + + set value(Matrix4 newValue) { + _state._txController.value = makeMatrixInSafeRange(newValue, forceClamp: true); + } + + @override + void addListener(ui.VoidCallback listener) => _listeners.add(listener); + + @override + void removeListener(ui.VoidCallback listener) => _listeners.remove(listener); + + /// Restrict matrix to the safe range. + Matrix4 makeMatrixInSafeRange(Matrix4 newValue, {bool forceClamp = false}) => + _state._makeMatrixInSafeRange(newValue, forceClamp: forceClamp); + + double getNextZoom({bool loop = true}) => _state._findNextZoomStop(currentZoom, zoomUp: true, loop: loop); + + double getPreviousZoom({bool loop = true}) => _state._findNextZoomStop(currentZoom, zoomUp: false, loop: loop); + + void notifyFirstChange(void Function() onFirstChange) { + void handler() { + removeListener(handler); + onFirstChange(); + } + + addListener(handler); + } + + /// Go to the specified area. + /// + /// [anchor] specifies how the page is positioned if the page is larger than the view. + Future goToArea({ + required Rect rect, + PdfPageAnchor? anchor, + Duration duration = const Duration(milliseconds: 200), + }) => _state._goToArea(rect: rect, anchor: anchor, duration: duration); + + /// Go to the specified page. + /// + /// [anchor] specifies how the page is positioned if the page is larger than the view. + Future goToPage({ + required int pageNumber, + PdfPageAnchor? anchor, + Duration duration = const Duration(milliseconds: 200), + }) => _state._goToPage(pageNumber: pageNumber, anchor: anchor, duration: duration); + + /// Go to the specified area inside the page. + /// + /// [pageNumber] specifies the page number. + /// [rect] specifies the area to go in page coordinates. + /// [anchor] specifies how the page is positioned if the page is larger than the view. + Future goToRectInsidePage({ + required int pageNumber, + required PdfRect rect, + PdfPageAnchor? anchor, + Duration duration = const Duration(milliseconds: 200), + }) => _state._goToRectInsidePage(pageNumber: pageNumber, rect: rect, anchor: anchor, duration: duration); + + /// Calculate the rectangle for the specified area inside the page. + /// + /// [pageNumber] specifies the page number. + /// [rect] specifies the area to go in page coordinates. + Rect calcRectForRectInsidePage({required int pageNumber, required PdfRect rect}) => + _state._calcRectForRectInsidePage(pageNumber: pageNumber, rect: rect); + + /// Calculate the matrix for the specified area inside the page. + /// + /// [pageNumber] specifies the page number. + /// [rect] specifies the area to go in page coordinates. + /// [anchor] specifies how the page is positioned if the page is larger than the view. + Matrix4 calcMatrixForRectInsidePage({required int pageNumber, required PdfRect rect, PdfPageAnchor? anchor}) => + _state._calcMatrixForRectInsidePage(pageNumber: pageNumber, rect: rect, anchor: anchor); + + /// Go to the specified destination. + /// + /// [dest] specifies the destination. + /// [duration] specifies the duration of the animation. + Future goToDest(PdfDest? dest, {Duration duration = const Duration(milliseconds: 200)}) => + _state._goToDest(dest, duration: duration); + + /// Calculate the matrix for the specified destination. + /// + /// [dest] specifies the destination. + Matrix4? calcMatrixForDest(PdfDest? dest) => _state._calcMatrixForDest(dest); + + /// Calculate the matrix to fit the page into the view. + /// + /// `/Fit` command on [PDF 32000-1:2008, 12.3.2.2 Explicit Destinations, Table 151](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf#page=374) + Matrix4? calcMatrixForFit({required int pageNumber}) => + calcMatrixForDest(PdfDest(pageNumber, PdfDestCommand.fit, null)); + + /// Calculate the matrix to fit the specified page width into the view. + /// + Matrix4? calcMatrixFitWidthForPage({required int pageNumber}) { + final page = layout.pageLayouts[pageNumber - 1]; + final zoom = (viewSize.width - params.margin * 2) / page.width; + final y = (viewSize.height / 2 - params.margin) / zoom; + return calcMatrixFor(page.topCenter.translate(0, y), zoom: zoom, viewSize: viewSize); + } + + /// Calculate the matrix to fit the specified page height into the view. + /// + Matrix4? calcMatrixFitHeightForPage({required int pageNumber}) { + final page = layout.pageLayouts[pageNumber - 1]; + final zoom = (viewSize.height - params.margin * 2) / page.height; + return calcMatrixFor(page.center, zoom: zoom, viewSize: viewSize); + } + + /// Get list of possible matrices that fit some of the pages into the view. + /// + /// [sortInSuitableOrder] specifies whether the result is sorted in a suitable order. + /// + /// Because [PdfViewer] can show multiple pages at once, there are several possible + /// matrices to fit the pages into the view according to several criteria. + /// The method returns the list of such matrices. + /// + /// In theory, the method can be also used to determine the dominant pages in the view. + List calcFitZoomMatrices({bool sortInSuitableOrder = true}) { + final viewRect = visibleRect; + final result = []; + final pos = centerPosition; + for (var i = 0; i < layout.pageLayouts.length; i++) { + final page = layout.pageLayouts[i]; + if (page.intersect(viewRect).isEmpty) continue; + final boundaryMargin = _state._adjustedBoundaryMargins; + final zoom = viewSize.width / (page.width + (params.margin * 2) + boundaryMargin.horizontal); + + // NOTE: keep the y-position but center the x-position + final newMatrix = calcMatrixFor(Offset(page.left + page.width / 2, pos.dy), zoom: zoom); + + final intersection = newMatrix.calcVisibleRect(viewSize).intersect(page); + // if the page is not visible after changing the zoom, ignore it + if (intersection.isEmpty) continue; + final intersectionRatio = intersection.width * intersection.height / (page.width * page.height); + result.add(PdfPageFitInfo(pageNumber: i + 1, matrix: newMatrix, visibleAreaRatio: intersectionRatio)); + } + if (sortInSuitableOrder) { + result.sort((a, b) => b.visibleAreaRatio.compareTo(a.visibleAreaRatio)); + } + return result; + } + + /// Calculate the matrix for the page. + /// + /// [pageNumber] specifies the page number. + /// [anchor] specifies how the page is positioned if the page is larger than the view. + Matrix4 calcMatrixForPage({required int pageNumber, PdfPageAnchor? anchor}) => + _state._calcMatrixForPage(pageNumber: pageNumber, anchor: anchor); + + /// Calculate the matrix for the specified area. + /// + /// [rect] specifies the area in document coordinates. + /// [anchor] specifies how the page is positioned if the page is larger than the view. + Matrix4 calcMatrixForArea({required Rect rect, PdfPageAnchor? anchor}) => + _state._calcMatrixForArea(rect: rect, anchor: anchor); + + /// Go to the specified position by the matrix. + /// + /// All the `goToXXX` functions internally calls the function. + /// So if you customize the behavior of the viewer, you can extend [PdfViewerController] and override the function: + /// + /// ```dart + /// class MyPdfViewerController extends PdfViewerController { + /// @override + /// Future goTo( + /// Matrix4? destination, { + /// Duration duration = const Duration(milliseconds: 200), + /// }) async { + /// print('goTo'); + /// super.goTo(destination, duration: duration); + /// } + /// } + /// ``` + Future goTo(Matrix4? destination, {Duration duration = const Duration(milliseconds: 200)}) => + _state._goTo(destination, duration: duration); + + /// Ensure the specified area is visible inside the view port. + /// + /// If the area is larger than the view port, the area is zoomed to fit the view port. + /// [margin] adds extra margin to the area. + Future ensureVisible(Rect rect, {Duration duration = const Duration(milliseconds: 200), double margin = 0}) => + _state._ensureVisible(rect, duration: duration, margin: margin); + + /// Calculate the matrix to center the specified position. + Matrix4 calcMatrixFor(Offset position, {double? zoom, Size? viewSize}) => + _state._calcMatrixFor(position, zoom: zoom ?? currentZoom, viewSize: viewSize ?? this.viewSize); + + Offset get centerPosition => value.calcPosition(viewSize); + + Matrix4 calcMatrixForRect(Rect rect, {double? zoomMax, double? margin}) => + _state._calcMatrixForRect(rect, zoomMax: zoomMax, margin: margin); + + Matrix4 calcMatrixToEnsureRectVisible(Rect rect, {double margin = 0}) => + _state._calcMatrixToEnsureRectVisible(rect, margin: margin); + + /// Do hit-test against laid out pages. + /// + /// Returns the hit-test result if the specified offset is inside a page; otherwise null. + /// + /// [useDocumentLayoutCoordinates] specifies whether the offset is in the document layout coordinates; + /// if true, the offset is in the document layout coordinates; otherwise, the offset is in the widget's local coordinates. + PdfPageHitTestResult? getPdfPageHitTestResult(Offset offset, {required bool useDocumentLayoutCoordinates}) => + _state._getPdfPageHitTestResult(offset, useDocumentLayoutCoordinates: useDocumentLayoutCoordinates); + + /// Set the current page number. + /// + /// This function does not scroll/zoom to the specified page but changes the current page number. + void setCurrentPageNumber(int pageNumber) => _state._setCurrentPageNumber(pageNumber); + + /// The current zoom ratio. + double get currentZoom => value.zoom; + + /// Set the zoom ratio with the specified position as the zoom center. + /// + /// [position] specifies the zoom center in the document coordinates. + /// [zoom] specifies the new zoom ratio. + /// [duration] specifies the duration of the animation. + Future setZoom(Offset position, double zoom, {Duration duration = const Duration(milliseconds: 200)}) => + _state._setZoom(position, zoom, duration: duration); + + /// Zoom in with the specified position as the zoom center. + /// + /// [zoomCenter] specifies the zoom center in the document coordinates; if null, the center of the view is used. + /// [loop] specifies whether to loop the zoom stops. + /// [duration] specifies the duration of the animation. + Future zoomUp({bool loop = false, Offset? zoomCenter, Duration duration = const Duration(milliseconds: 200)}) => + _state._zoomUp(loop: loop, zoomCenter: zoomCenter, duration: duration); + + /// Zoom out with the specified position as the zoom center. + /// + /// [zoomCenter] specifies the zoom center in the document coordinates; if null, the center of the view is used. + /// [loop] specifies whether to loop the zoom stops. + /// [duration] specifies the duration of the animation. + Future zoomDown({ + bool loop = false, + Offset? zoomCenter, + Duration duration = const Duration(milliseconds: 200), + }) => _state._zoomDown(loop: loop, zoomCenter: zoomCenter, duration: duration); + + /// Set the zoom ratio with the document point corresponding to the specified local position is kept unmoved + /// on the view. + /// + /// [localPosition] specifies the position in the widget's local coordinates. + /// [newZoom] specifies the new zoom ratio. + /// [duration] specifies the duration of the animation. + Future zoomOnLocalPosition({ + required Offset localPosition, + required double newZoom, + Duration duration = const Duration(milliseconds: 200), + }) async { + final center = _state._localPositionToZoomCenter(localPosition, newZoom); + await _state._setZoom(center, newZoom, duration: duration); + } + + /// Zoom in with the document point corresponding to the specified local position is kept unmoved + /// on the view. + /// + /// [localPosition] specifies the position in the widget's local coordinates. + /// [loop] specifies whether to loop the zoom stops. + /// [duration] specifies the duration of the animation. + Future zoomUpOnLocalPosition({ + required Offset localPosition, + bool loop = false, + Duration duration = const Duration(milliseconds: 200), + }) async { + final newZoom = _state._findNextZoomStop(currentZoom, zoomUp: true, loop: loop); + final center = _state._localPositionToZoomCenter(localPosition, newZoom); + await _state._setZoom(center, newZoom, duration: duration); + } + + /// Zoom out with the document point corresponding to the specified local position is kept unmoved + /// on the view. + /// + /// [localPosition] specifies the position in the widget's local coordinates. + /// [loop] specifies whether to loop the zoom stops. + /// [duration] specifies the duration of the animation. + Future zoomDownOnLocalPosition({ + required Offset localPosition, + bool loop = false, + Duration duration = const Duration(milliseconds: 200), + }) async { + final newZoom = _state._findNextZoomStop(currentZoom, zoomUp: false, loop: loop); + final center = _state._localPositionToZoomCenter(localPosition, newZoom); + await _state._setZoom(center, newZoom, duration: duration); + } + + RenderBox? get renderBox => _state._renderBox; + + /// Converts the global position to the local position in the widget. + Offset? globalToLocal(Offset global) => _state._globalToLocal(global); + + /// Converts the local position to the global position in the widget. + Offset? localToGlobal(Offset local) => _state._localToGlobal(local); + + /// Converts the global position to the local position in the PDF document structure. + Offset? globalToDocument(Offset global) => _state._globalToDocument(global); + + /// Converts the local position in the PDF document structure to the global position. + Offset? documentToGlobal(Offset document) => _state._documentToGlobal(document); + + /// Converts local coordinates to document coordinates. + Offset localToDocument(Offset local) => _state._localToDocument(local); + + /// Converts document coordinates to local coordinates. + Offset documentToLocal(Offset document) => _state._documentToLocal(document); + + /// Converts document coordinates to local coordinates. + PdfViewerCoordinateConverter get doc2local => _state; + + /// Provided to workaround certain widgets eating wheel events. Use with [Listener.onPointerSignal]. + void handlePointerSignalEvent(PointerSignalEvent event) { + if (event is PointerScrollEvent) { + _state._onWheelDelta(event); + } + } + + /// Invalidates the current Widget display state. + /// + /// Almost identical to `setState` but can be called outside the state. + void invalidate() => _state._invalidate(); + + /// The text selection delegate. + PdfTextSelectionDelegate get textSelectionDelegate => _state; + + /// [FocusNode] associated to the [PdfViewer] if available. + FocusNode? get focusNode => _state._getFocusNode(); + + /// Request focus to the [PdfViewer]. + void requestFocus() => _state._requestFocus(); + + /// Force redraw all the page images. + void forceRepaintAllPageImages() => _state.forceRepaintAllPageImages(); +} + +/// [PdfViewerController.calcFitZoomMatrices] returns the list of this class. +@immutable +class PdfPageFitInfo { + const PdfPageFitInfo({required this.pageNumber, required this.matrix, required this.visibleAreaRatio}); + + /// The page number of the target page. + final int pageNumber; + + /// The matrix to fit the page horizontally into the view. + final Matrix4 matrix; + + /// The ratio of the visible area of the page. 1 means the whole page is visible inside the view. + final double visibleAreaRatio; + + @override + String toString() => 'PdfPageFitInfo(pageNumber=$pageNumber, visibleAreaRatio=$visibleAreaRatio, matrix=$matrix)'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is PdfPageFitInfo && + other.pageNumber == pageNumber && + other.matrix == matrix && + other.visibleAreaRatio == visibleAreaRatio; + } + + @override + int get hashCode => pageNumber.hashCode ^ matrix.hashCode ^ visibleAreaRatio.hashCode; +} + +extension PdfMatrix4Ext on Matrix4 { + /// Zoom ratio of the matrix. + double get zoom => storage[0]; + + /// X position of the matrix. + double get xZoomed => storage[12]; + + /// X position of the matrix. + set xZoomed(double value) => storage[12] = value; + + /// Y position of the matrix. + double get yZoomed => storage[13]; + + /// Y position of the matrix. + set yZoomed(double value) => storage[13] = value; + + double get x => xZoomed / zoom; + + set x(double value) => xZoomed = value * zoom; + + double get y => yZoomed / zoom; + + set y(double value) => yZoomed = value * zoom; + + /// Calculate the position of the matrix based on the specified view size. + /// + /// Because [Matrix4] does not have the information of the view size, + /// this function calculates the position based on the specified view size. + Offset calcPosition(Size viewSize) => Offset((viewSize.width / 2 - xZoomed), (viewSize.height / 2 - yZoomed)) / zoom; + + /// Calculate the visible rectangle based on the specified view size. + /// + /// [margin] adds extra margin to the area. + /// Because [Matrix4] does not have the information of the view size, + /// this function calculates the visible rectangle based on the specified view size. + Rect calcVisibleRect(Size viewSize, {double margin = 0}) => Rect.fromCenter( + center: calcPosition(viewSize), + width: (viewSize.width - margin * 2) / zoom, + height: (viewSize.height - margin * 2) / zoom, + ); + + Offset transformOffset(Offset xy) { + final x = xy.dx; + final y = xy.dy; + final w = x * storage[3] + y * storage[7] + storage[15]; + return Offset( + (x * storage[0] + y * storage[4] + storage[12]) / w, + (x * storage[1] + y * storage[5] + storage[13]) / w, + ); + } +} + +extension RectExt on Rect { + Rect operator *(double operand) => Rect.fromLTRB(left * operand, top * operand, right * operand, bottom * operand); + + Rect operator /(double operand) => Rect.fromLTRB(left / operand, top / operand, right / operand, bottom / operand); + + bool containsRect(Rect other) => contains(other.topLeft) && contains(other.bottomRight); + + Rect inflateHV({required double horizontal, required double vertical}) => + Rect.fromLTRB(left - horizontal, top - vertical, right + horizontal, bottom + vertical); +} + +/// Create a [CustomPainter] from a paint function. +class _CustomPainter extends CustomPainter { + /// Create a [CustomPainter] from a paint function. + const _CustomPainter.fromFunctions(this.paintFunction, {this.hitTestFunction}); + final void Function(ui.Canvas canvas, ui.Size size) paintFunction; + final bool Function(ui.Offset position)? hitTestFunction; + @override + void paint(ui.Canvas canvas, ui.Size size) => paintFunction(canvas, size); + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; + + @override + bool hitTest(ui.Offset position) { + if (hitTestFunction == null) return false; + return hitTestFunction!(position); + } +} + +Widget _defaultErrorBannerBuilder( + BuildContext context, + Object error, + StackTrace? stackTrace, + PdfDocumentRef documentRef, +) { + return pdfErrorWidget(context, error, stackTrace: stackTrace); +} + +/// Handles the link painting and tap handling. +class _CanvasLinkPainter { + _CanvasLinkPainter(this._state); + final _PdfViewerState _state; + MouseCursor _cursor = MouseCursor.defer; + final _links = >{}; + + bool get isEnabled => _state.widget.params.linkHandlerParams != null; + + bool get isLaidOverPageOverlays => + _state.widget.params.linkHandlerParams != null && _state.widget.params.linkHandlerParams!.laidOverPageOverlays; + + bool get isLaidUnderPageOverlays => + _state.widget.params.linkHandlerParams != null && !_state.widget.params.linkHandlerParams!.laidOverPageOverlays; + + /// Reset all the internal data. + void resetAll() { + _cursor = MouseCursor.defer; + _links.clear(); + } + + /// Release the page data. + void releaseLinksForPage(int pageNumber) { + _links.remove(pageNumber); + } + + List? _ensureLinksLoaded(PdfPage page, {void Function()? onLoaded}) { + if (!page.isLoaded) return null; + final links = _links[page.pageNumber]; + if (links != null) return links; + synchronized(() async { + final links = _links[page.pageNumber]; + if (links != null) return links; + final enableAutoLinkDetection = _state.widget.params.linkHandlerParams?.enableAutoLinkDetection ?? true; + _links[page.pageNumber] = await page.loadLinks(compact: true, enableAutoLinkDetection: enableAutoLinkDetection); + if (onLoaded != null) { + onLoaded(); + } else { + _state._invalidate(); + } + }); + return null; + } + + PdfLink? _findLinkAtPosition(Offset position) { + final hitResult = _state._getPdfPageHitTestResult(position, useDocumentLayoutCoordinates: false); + if (hitResult == null) return null; + final links = _ensureLinksLoaded(hitResult.page); + if (links == null) return null; + for (final link in links) { + for (final rect in link.rects) { + if (rect.containsPoint(hitResult.offset)) { + return link; + } + } + } + return null; + } + + bool _handleTapUp(Offset tapPosition) { + _state._requestFocus(); + _cursor = MouseCursor.defer; + final link = _findLinkAtPosition(tapPosition); + if (link != null) { + final onLinkTap = _state.widget.params.linkHandlerParams?.onLinkTap; + if (onLinkTap != null) { + onLinkTap(link); + return true; + } + } + final globalPosition = _state._localToGlobal(tapPosition)!; + _state._handleGeneralTap(globalPosition, PdfViewerGeneralTapType.tap); + return false; + } + + /// Creates a [GestureDetector] for handling link taps and mouse cursor. + Widget linkHandlingOverlay(Size size) { + return GestureDetector( + behavior: HitTestBehavior.translucent, + // link taps + onTapUp: (details) => _handleTapUp(details.localPosition), + child: StatefulBuilder( + builder: (context, setState) { + return MouseRegion( + hitTestBehavior: HitTestBehavior.translucent, + onHover: (event) { + final link = _findLinkAtPosition(event.localPosition); + final newCursor = link == null ? MouseCursor.defer : SystemMouseCursors.click; + if (newCursor != _cursor) { + _cursor = newCursor; + setState(() {}); + } + }, + onExit: (event) { + _cursor = MouseCursor.defer; + setState(() {}); + }, + cursor: _cursor, + ); + }, + ), + ); + } + + /// Paints the link highlights. + void paintLinkHighlights(Canvas canvas, Rect pageRect, PdfPage page) { + final links = _ensureLinksLoaded(page); + if (links == null) return; + + final customPainter = _state.widget.params.linkHandlerParams?.customPainter; + + if (customPainter != null) { + customPainter.call(canvas, pageRect, page, links); + return; + } + + final paint = Paint() + ..color = _state.widget.params.linkHandlerParams?.linkColor ?? Colors.blue.withAlpha(50) + ..style = PaintingStyle.fill; + for (final link in links) { + for (final rect in link.rects) { + final rectLink = rect.toRectInDocument(page: page, pageRect: pageRect); + canvas.drawRect(rectLink, paint); + } + } + } +} diff --git a/packages/pdfrx/lib/src/widgets/pdf_viewer_params.dart b/packages/pdfrx/lib/src/widgets/pdf_viewer_params.dart new file mode 100644 index 00000000..fffda50f --- /dev/null +++ b/packages/pdfrx/lib/src/widgets/pdf_viewer_params.dart @@ -0,0 +1,1811 @@ +import 'dart:ui' as ui; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../../pdfrx.dart'; +import '../utils/platform.dart'; + +/// Viewer customization parameters. +/// +/// Changes to several functions such as [layoutPages] does not +/// take effect until the viewer is re-layout-ed. You can relayout the viewer by calling [PdfViewerController.invalidate]. +@immutable +class PdfViewerParams { + const PdfViewerParams({ + this.margin = 8.0, + this.backgroundColor = Colors.grey, + this.layoutPages, + this.normalizeMatrix, + this.fitMode = FitMode.fit, + this.pageTransition = PageTransition.continuous, + this.maxScale = 8.0, + this.minScale, + this.useAlternativeFitScaleAsMinScale = false, + this.panAxis = PanAxis.free, + this.boundaryMargin, + this.annotationRenderingMode = PdfAnnotationRenderingMode.annotationAndForms, + this.limitRenderingCache = true, + this.pageAnchor = PdfPageAnchor.top, + this.pageAnchorEnd = PdfPageAnchor.bottom, + this.onePassRenderingScaleThreshold = 200 / 72, + this.onePassRenderingSizeThreshold = 2000, + this.textSelectionParams, + this.matchTextColor, + this.activeMatchTextColor, + this.pageDropShadow = const BoxShadow(color: Colors.black54, blurRadius: 4, spreadRadius: 2, offset: Offset(2, 2)), + this.panEnabled = true, + this.scaleEnabled = true, + this.onInteractionEnd, + this.onInteractionStart, + this.onInteractionUpdate, + this.interactionEndFrictionCoefficient = _kDrag, + this.onSecondaryTapUp, + this.onLongPressStart, + this.onDocumentChanged, + this.onDocumentLoadFinished, + this.calculateInitialPageNumber, + this.calculateInitialZoom, + this.calculateCurrentPageNumber, + this.onViewerReady, + this.onViewSizeChanged, + this.onPageChanged, + this.getPageRenderingScale, + this.scrollByMouseWheel = 0.2, + this.scrollHorizontallyByMouseWheel = false, + this.enableKeyboardNavigation = true, + this.scrollByArrowKey = 25.0, + this.maxImageBytesCachedOnMemory = 100 * 1024 * 1024, + this.horizontalCacheExtent = 1.0, + this.verticalCacheExtent = 1.0, + this.linkHandlerParams, + this.viewerOverlayBuilder, + this.pageOverlaysBuilder, + this.loadingBannerBuilder, + this.errorBannerBuilder, + this.linkWidgetBuilder, + this.pagePaintCallbacks, + this.pageBackgroundPaintCallbacks, + this.onGeneralTap, + this.buildContextMenu, + this.customizeContextMenuItems, + this.onKey, + this.keyHandlerParams = const PdfViewerKeyHandlerParams(), + this.behaviorControlParams = const PdfViewerBehaviorControlParams(), + this.forceReload = false, + ScrollPhysics? scrollPhysics, + this.scrollPhysicsScale, + }) : scrollPhysics = + scrollPhysics ?? (pageTransition == PageTransition.discrete ? const ClampingScrollPhysics() : null), + assert( + !useAlternativeFitScaleAsMinScale || fitMode == FitMode.fit, + 'useAlternativeFitScaleAsMinScale is deprecated and forces FitMode.fit behavior, ' + 'making the fitMode parameter ($fitMode) ineffective. ' + 'Remove the useAlternativeFitScaleAsMinScale parameter to use fitMode as intended.', + ); + + /// Margin around the page. + final double margin; + + /// Background color of the viewer. + final Color backgroundColor; + + /// Function to customize the layout of the pages. + /// + /// Changes to this function does not take effect until the viewer is re-layout-ed. You can relayout the viewer by calling [PdfViewerController.invalidate]. + /// + /// The following fragment is an example to layout pages horizontally with margin: + /// + /// ```dart + /// PdfViewerParams( + /// layoutPages: (pages, params) { + /// final height = pages.fold( + /// 0.0, (prev, page) => max(prev, page.height)) + params.margin * 2; + /// final pageLayouts = []; + /// double x = params.margin; + /// for (final page in pages) { + /// pageLayouts.add( + /// Rect.fromLTWH( + /// x, + /// (height - page.height) / 2, // center vertically + /// page.width, + /// page.height, + /// ), + /// ); + /// x += page.width + params.margin; + /// } + /// return PageLayout(pageLayouts: pageLayouts, documentSize: Size(x, height)); + /// }, + /// ), + /// ``` + final PdfPageLayoutFunction? layoutPages; + + /// Function to normalize the matrix. + /// + /// The function is called when the matrix is changed and normally used to restrict the matrix to certain range. + /// + /// If [scrollPhysics] is non-null, this function is ignored. + /// + /// The following fragment is an example to restrict the matrix to the document size, which is almost identical to + /// the default behavior: + /// + /// ```dart + /// PdfViewerParams( + /// normalizeMatrix: (matrix, viewSize, layout, controller) { + /// // If the controller is not ready, just return the input matrix. + /// if (controller == null || !controller.isReady) return matrix; + /// final position = newValue.calcPosition(viewSize); + /// final newZoom = controller.params.boundaryMargin != null + /// ? newValue.zoom + /// : max(newValue.zoom, controller.minScale); + /// final hw = viewSize.width / 2 / newZoom; + /// final hh = viewSize.height / 2 / newZoom; + /// final x = position.dx.range(hw, layout.documentSize.width - hw); + /// final y = position.dy.range(hh, layout.documentSize.height - hh); + /// return controller.calcMatrixFor(Offset(x, y), zoom: newZoom, viewSize: viewSize); + /// }, + /// ), + /// ``` + final PdfMatrixNormalizeFunction? normalizeMatrix; + + /// How pages should be fitted within the viewport. + /// + /// - [FitMode.fit]: Entire page/spread visible (may have letterboxing) + /// - [FitMode.fill]: Fill viewport (may crop content perpendicular to scroll direction) + /// + /// The default is [FitMode.fit]. + final FitMode fitMode; + + /// Defines how pages transition when navigating through the document. + /// + /// - [PageTransition.continuous]: Pages flow continuously in an uninterrupted scrollable view + /// - [PageTransition.discrete]: Pages transition discretely, one page (or spread) at a time + /// + /// When using [PageTransition.discrete]: + /// - Swipe gestures (velocity > 300 px/s) advance to next/previous page + /// - Drag gestures snap based on 50% threshold + /// - Only applies to pan-only gestures (zoom/pinch work normally) + /// - Only active at fit zoom level (free panning when zoomed in) + /// - Works with all layout types (single pages and facing pages) + /// - Provides a book-like reading experience + /// + /// Example: + /// ```dart + /// PdfViewer.asset( + /// 'assets/sample.pdf', + /// params: PdfViewerParams( + /// pageTransition: PageTransition.discrete, + /// ), + /// ) + /// ``` + /// + /// The default is [PageTransition.continuous]. + final PageTransition pageTransition; + + /// The maximum allowed scale. + /// + /// The default is 8.0. + final double maxScale; + + /// The minimum allowed scale for zooming. + /// + /// - If `null` (default): The minimum scale is automatically calculated using the layout's + /// `calculateFitScale()` method with the current [fitMode], ensuring content fits appropriately. + /// - If a value is provided: That value is used as the explicit minimum scale. + /// + /// **Note:** When [useAlternativeFitScaleAsMinScale] is `true` (deprecated), it overrides this setting. + /// + /// **Examples:** + /// ```dart + /// // Automatic calculation (recommended): + /// PdfViewerParams(minScale: null) // or omit entirely + /// + /// // Explicit minimum scale: + /// PdfViewerParams(minScale: 0.5) + /// ``` + final double? minScale; + + /// **DEPRECATED:** Use `fitMode` and `minScale` parameters instead. + /// + /// This legacy parameter controlled whether to force `FitMode.fit` behavior for fit scale calculation. + /// When `true`, the fit scale is always calculated as `FitMode.fit` regardless of the `fitMode` parameter. + /// When `false` (now default as of v2.3.0), the fit scale respects the `fitMode` parameter. + /// + /// **Breaking change in v2.3.0:** Default changed from `true` to `false`. + /// If you were relying on the old default, explicitly set this to `true`. + /// + /// **Important:** Explicit `minScale` values are now always honored regardless of this flag (fixed in v2.3.0). + /// Previously, when this flag was `true`, explicit `minScale` values were ignored. + /// + /// **Migration:** + /// - If you want the old behavior: Set `useAlternativeFitScaleAsMinScale: true` explicitly. + /// - If you want to allow zooming out beyond the fit scale: Set `minScale: 0.1` (or desired value). + /// - If you want different fit modes to work correctly: Remove this parameter or set to `false` (default). + /// + /// **Example:** + /// ```dart + /// // Old code (pre-v2.3.0): + /// PdfViewerParams(fitMode: FitMode.fill) // Didn't work, behaved like FitMode.fit + /// + /// // New code (v2.3.0+): + /// PdfViewerParams(fitMode: FitMode.fill) // Works correctly now + /// + /// // To keep old behavior: + /// PdfViewerParams(fitMode: FitMode.fill, useAlternativeFitScaleAsMinScale: true) + /// ``` + @Deprecated('Use fitMode parameter instead. See documentation for migration guide.') + final bool useAlternativeFitScaleAsMinScale; + + /// See [InteractiveViewer.panAxis] for details. + final PanAxis panAxis; + + /// See [InteractiveViewer.boundaryMargin] for details. + /// + /// The default is `EdgeInsets.all(double.infinity)`. + final EdgeInsets? boundaryMargin; + + /// Annotation rendering mode. + final PdfAnnotationRenderingMode annotationRenderingMode; + + /// If true, the viewer limits the rendering cache to reduce memory consumption. + /// + /// For PDFium, it internally enables `FPDF_RENDER_LIMITEDIMAGECACHE` flag on rendering + /// to reduce the memory consumption by image caching. + final bool limitRenderingCache; + + /// Anchor to position the page. + final PdfPageAnchor pageAnchor; + + /// Anchor to position the page at the end of the page. + final PdfPageAnchor pageAnchorEnd; + + /// If a page is rendered over the scale threshold, the page is rendered with the threshold scale + /// and actual resolution image is rendered after some delay (progressive rendering). + /// + /// Basically, if the value is larger, the viewer renders each page in one-pass rendering; it is + /// faster and looks better to the user. However, larger value may consume more memory. + /// So you may want to set the smaller value to reduce memory consumption. + /// + /// The default is 200 / 72, which implies rendering at 200 dpi. + /// If you want more granular control for each page, use [getPageRenderingScale]. + final double onePassRenderingScaleThreshold; + + /// If a page is too large, the page is rendered with the size which fits within the threshold size (in pixels). + /// + /// The default is 2000, which implies the maximum size of the page is 2000 pixels in width or height. + final double onePassRenderingSizeThreshold; + + /// Parameters for text selection. + final PdfTextSelectionParams? textSelectionParams; + + /// Color for text search match. + /// + /// If null, the default color is `Colors.yellow.withValue(alpha: 0.5)`. + final Color? matchTextColor; + + /// Color for active text search match. + /// + /// If null, the default color is `Colors.orange.withValue(alpha: 0.5)`. + final Color? activeMatchTextColor; + + /// Drop shadow for the page. + /// + /// The default is: + /// ```dart + /// BoxShadow( + /// color: Colors.black54, + /// blurRadius: 4, + /// spreadRadius: 0, + /// offset: Offset(2, 2)) + /// ``` + /// + /// If you need to remove the shadow, set this to null. + /// To customize more of the shadow, you can use [pageBackgroundPaintCallbacks] to paint the shadow manually. + final BoxShadow? pageDropShadow; + + /// See [InteractiveViewer.panEnabled] for details. + final bool panEnabled; + + /// See [InteractiveViewer.scaleEnabled] for details. + final bool scaleEnabled; + + /// See [InteractiveViewer.onInteractionEnd] for details. + final GestureScaleEndCallback? onInteractionEnd; + + /// See [InteractiveViewer.onInteractionStart] for details. + final GestureScaleStartCallback? onInteractionStart; + + /// See [InteractiveViewer.onInteractionUpdate] for details. + final GestureScaleUpdateCallback? onInteractionUpdate; + + /// See [InteractiveViewer.interactionEndFrictionCoefficient] for details. + final double interactionEndFrictionCoefficient; + + /// Function to call when the text is secondary tapped (right-click). + /// + /// By default, secondary tap on non-text area to open text context menu. + final void Function(TapUpDetails details)? onSecondaryTapUp; + + /// Function to call when the text is long pressed. + /// + /// By default, long press on non-text area to open text context menu. + final void Function(LongPressStartDetails details)? onLongPressStart; + + // Used as the coefficient of friction in the inertial translation animation. + // This value was eyeballed to give a feel similar to Google Photos. + static const double _kDrag = 0.0000135; + + /// Function to notify that the document is loaded/changed. + /// + /// The function is called even if the document is null (it means the document is unloaded). + /// If you want to be notified when the viewer is ready to interact, use [onViewerReady] instead. + final PdfViewerDocumentChangedCallback? onDocumentChanged; + + /// Function to notify that the document loading is finished regardless of success or failure. + /// + /// For the function usage, see [PdfDocumentLoadFinished]. + final PdfDocumentLoadFinished? onDocumentLoadFinished; + + /// Function called when the viewer is ready. + /// + /// Unlike [PdfViewerDocumentChangedCallback], this function is called after the viewer is ready to interact. + final PdfViewerReadyCallback? onViewerReady; + + /// Function to be notified when the viewer size is changed. + /// + /// Please note that the function might be called during widget build, + /// so you should not synchronously call functions that may cause rebuild; + /// instead, you can use [Future.microtask] or [Future.delayed] to schedule the function call after the build. + /// + /// The following code illustrates how to keep the center position during device screen rotation: + /// + /// ```dart + /// onViewSizeChanged: (viewSize, oldViewSize, controller) { + /// if (oldViewSize != null) { + /// // The most important thing here is that the transformation matrix + /// // is not changed on the view change. + /// final centerPosition = + /// controller.value.calcPosition(oldViewSize); + /// final newMatrix = + /// controller.calcMatrixFor(centerPosition); + /// // Don't change the matrix in sync; the callback might be called + /// // during widget-tree's build process. + /// Future.delayed( + /// const Duration(milliseconds: 200), + /// () => controller.goTo(newMatrix), + /// ); + /// } + /// }, + /// ``` + final PdfViewerViewSizeChanged? onViewSizeChanged; + + /// Function to calculate the initial page number. + /// + /// It is useful when you want to determine the initial page number based on the document content. + final PdfViewerCalculateInitialPageNumberFunction? calculateInitialPageNumber; + + /// Function to calculate the initial zoom level. + final PdfViewerCalculateZoomFunction? calculateInitialZoom; + + /// Function to guess the current page number based on the visible rectangle and page layouts. + /// + /// The function is used to override the default behavior to calculate the current page number. + final PdfViewerCalculateCurrentPageNumberFunction? calculateCurrentPageNumber; + + /// Function called when the current page is changed. + final PdfPageChangedCallback? onPageChanged; + + /// Function to customize the rendering scale of the page. + /// + /// In some cases, if [maxScale]/[onePassRenderingScaleThreshold] is too large, + /// certain pages may not be rendered correctly due to memory limitation, + /// or anyway they may take too long to render. + /// In such cases, you can use this function to customize the rendering scales + /// for such pages. + /// + /// The following fragment is an example of rendering pages always on 300 dpi: + /// ```dart + /// PdfViewerParams( + /// getPageRenderingScale: (context, page, controller, estimatedScale) { + /// return 300 / 72; + /// }, + /// ), + /// ``` + /// + /// The following fragment is more realistic example to restrict the rendering + /// resolution to maximum to 6000 pixels: + /// ```dart + /// PdfViewerParams( + /// getPageRenderingScale: (context, page, controller, estimatedScale) { + /// final width = page.width * estimatedScale; + /// final height = page.height * estimatedScale; + /// if (width > 6000 || height > 6000) { + /// return min(6000 / page.width, 6000 / page.height); + /// } + /// return estimatedScale; + /// }, + /// ), + /// ``` + final PdfViewerGetPageRenderingScale? getPageRenderingScale; + + /// Set the scroll amount ratio by mouse wheel. The default is 0.2. + /// + /// Negative value to scroll opposite direction. + /// null to disable scroll-by-mouse-wheel. + final double? scrollByMouseWheel; + + /// If true, the scroll direction is horizontal when the mouse wheel is scrolled in primary direction. + final bool scrollHorizontallyByMouseWheel; + + /// Enable keyboard navigation. The default is true. + final bool enableKeyboardNavigation; + + /// Amount of pixels to scroll by arrow keys. The default is 25.0. + final double scrollByArrowKey; + + /// Restrict the total amount of image bytes to be cached on memory. The default is 100 MB. + /// + /// The internal cache mechanism tries to limit the actual memory usage under the value but it is not guaranteed. + final int maxImageBytesCachedOnMemory; + + /// The horizontal cache extent specified in ratio to the viewport width. The default is 1.0. + final double horizontalCacheExtent; + + /// The vertical cache extent specified in ratio to the viewport height. The default is 1.0. + final double verticalCacheExtent; + + /// Parameters for the built-in link handler. + /// + /// It is mutually exclusive with [linkWidgetBuilder]. + final PdfLinkHandlerParams? linkHandlerParams; + + /// Add overlays to the viewer. + /// + /// This function is to generate widgets on PDF viewer's overlay [Stack]. + /// The widgets can be laid out using layout widgets such as [Positioned] and [Align]. + /// + /// The most typical use case is to add scroll thumbs to the viewer. + /// The following fragment illustrates how to add vertical and horizontal scroll thumbs: + /// + /// ```dart + /// viewerOverlayBuilder: (context, size, handleLinkTap) => [ + /// PdfViewerScrollThumb( + /// controller: controller, + /// orientation: ScrollbarOrientation.right), + /// PdfViewerScrollThumb( + /// controller: controller, + /// orientation: ScrollbarOrientation.bottom), + /// ], + /// ``` + /// + /// For more information, see [PdfViewerScrollThumb]. + /// + /// ### Note for using [GestureDetector] inside [viewerOverlayBuilder]: + /// You may want to use [GestureDetector] inside [viewerOverlayBuilder] to handle certain gesture events. + /// In such cases, your [GestureDetector] eats the gestures and the viewer cannot handle them directly. + /// So, when you use [GestureDetector] inside [viewerOverlayBuilder], please ensure the following things: + /// + /// - [GestureDetector.behavior] should be [HitTestBehavior.translucent] + /// - [GestureDetector.onTapUp] (or such depending on your situation) should call `handleLinkTap` to handle link tap + /// + /// The following fragment illustrates how to handle link tap in [GestureDetector]: + /// ```dart + /// viewerOverlayBuilder: (context, size, handleLinkTap) => [ + /// GestureDetector( + /// behavior: HitTestBehavior.translucent, + /// onTapUp: (details) => handleLinkTap(details.localPosition), + /// // Make the GestureDetector covers all the viewer widget's area + /// // but also make the event go through to the viewer. + /// child: IgnorePointer(child: SizedBox(width: size.width, height: size.height)), + /// ... + /// ), + /// ... + /// ] + /// ``` + /// + final PdfViewerOverlaysBuilder? viewerOverlayBuilder; + + /// Add overlays to each page. + /// + /// This function is used to decorate each page with overlay widgets. + /// + /// But placing widgets over the page may make the viewer heavier, especially when + /// the document has many pages. So please use this function with care. + /// To draw simple decorations such as page number footer, consider using [pageBackgroundPaintCallbacks]. + /// + /// The return value of the function is a list of widgets to be laid out on the page; + /// they are actually laid out on the page using [Stack]. + /// + /// There are many actual overlays on the page; the page overlays are; + /// - Page image + /// - Selectable page text + /// - Links (if [linkWidgetBuilder] is not null; otherwise links are handled by another logic) + /// - Overlay widgets returned by this function + /// + /// The most typical use case is to add page number footer to each page. + /// + /// The following fragment illustrates how to add page number footer to each page: + /// ```dart + /// pageOverlaysBuilder: (context, pageRect, page) { + /// return [ + /// Align( + /// alignment: Alignment.bottomCenter, + /// child: Text( + /// page.pageNumber.toString(), + /// style: const TextStyle(color: Colors.red), + /// ), + /// ), + /// ]; + /// }, + /// ``` + final PdfPageOverlaysBuilder? pageOverlaysBuilder; + + /// Build loading banner. + /// + /// Please note that the progress is only reported for [PdfViewer.uri] on non-Web platforms. + /// + /// The following fragment illustrates how to build loading banner that shows the download progress: + /// + /// ```dart + /// loadingBannerBuilder: (context, bytesDownloaded, totalBytes) { + /// return Center( + /// child: CircularProgressIndicator( + /// // totalBytes is null if the total bytes is unknown + /// value: totalBytes != null ? bytesDownloaded / totalBytes : null, + /// backgroundColor: Colors.grey, + /// ), + /// ); + /// }, + /// ``` + final PdfViewerLoadingBannerBuilder? loadingBannerBuilder; + + /// Build loading error banner. + final PdfViewerErrorBannerBuilder? errorBannerBuilder; + + /// Build link widget. + /// + /// If [linkHandlerParams] is specified, it is ignored. + /// + /// Basically, handling links with widgets are not recommended because it makes the viewer heavier. + /// If you just handle simple link taps or want to customize link visuals, use [linkHandlerParams]. + final PdfLinkWidgetBuilder? linkWidgetBuilder; + + /// Callback to paint over the rendered page. + /// + /// For the detail usage, see [PdfViewerPagePaintCallback]. + final List? pagePaintCallbacks; + + /// Callback to paint on the background of the rendered page (called before painting the page content). + /// + /// It is useful to paint some background such as drop shadow of the page. + /// For the detail usage, see [PdfViewerPagePaintCallback]. + final List? pageBackgroundPaintCallbacks; + + /// Function to handle general tap events. + /// + /// This function is called when the user taps on the viewer. + /// It can be used to handle general tap events such as single tap, double tap, long press, etc. + /// The function returns true if it processes the tap; otherwise, returns false. + /// + /// When the function returns true, the tap is considered handled and the viewer does not process it further. + final PdfViewerGeneralTapHandler? onGeneralTap; + + /// Function to build context menu. + /// + /// - If the function returns null, no context menu is shown. + /// - If the function is null, the default context menu will be used. + /// + /// When you implement the function, you should consider whether to call [customizeContextMenuItems] internally + /// or not according to your use case. + final PdfViewerContextMenuBuilder? buildContextMenu; + + /// Function to customize the context menu items. + /// + /// This function is called when the context menu is built and can be used to customize the context menu items. + /// This function may not be called if the context menu is build using [buildContextMenu]. [buildContextMenu] is + /// responsible for building the context menu items (i.e. it should decide whether to call this function internally or not) + final PdfViewerContextMenuUpdateMenuItemsFunction? customizeContextMenuItems; + + /// Function to handle key events. + /// + /// See [PdfViewerOnKeyCallback] for the details. + final PdfViewerOnKeyCallback? onKey; + + /// Parameters to customize key handling. + final PdfViewerKeyHandlerParams keyHandlerParams; + + /// Parameters to control viewer behaviors. + final PdfViewerBehaviorControlParams behaviorControlParams; + + /// Force reload the viewer. + /// + /// Normally whether to reload the viewer is determined by the changes of the parameters but + /// if you want to force reload the viewer, set this to true. + /// + /// Because changing certain fields like functions on [PdfViewerParams] does not run hot-reload on Flutter, + /// sometimes it is useful to force reload the viewer by setting this to true. + final bool forceReload; + + /// Scroll physics for the viewer. + /// + /// If null, default InteractiveViewer physics is used on all platforms. This physics clamps to boundaries, + /// does not allow zooming beyond the min/max scale, and flings on panning come to rest quickly relative to + /// Scrollables in Flutter (such as [SingleChildScrollView]). + /// + /// **Important for discrete mode:** When [pageTransition] is [PageTransition.discrete], scroll physics + /// are required for proper boundary snapping and settling behavior. If null in discrete mode, + /// [ClampingScrollPhysics] is automatically used as a fallback. + /// + /// A convenience function [getScrollPhysics] is provided to get platform-specific default scroll physics. + /// If you want no overscroll, but still want the physics for panning to be similar to other Scrollables, + /// you can use [ClampingScrollPhysics]. + /// + /// If the value is set non-null, it disables [normalizeMatrix]. + /// + /// If you set [boundaryMargin] to `EdgeInsets.all(double.infinity)`, this will enable scrolling + /// beyond the boundaries regardless of which [ScrollPhysics] is used. + final ScrollPhysics? scrollPhysics; + + /// Scroll physics for scaling within the viewer. If null, it uses the same value as [scrollPhysics]. + final ScrollPhysics? scrollPhysicsScale; + + /// A convenience function to get platform-specific default scroll physics. + /// + /// On iOS/MacOS this is [BouncingScrollPhysics], and on Android this is [FixedOverscrollPhysics], a + /// custom [ScrollPhysics] that allows fixed overscroll on pan/zoom and snapback. + static ScrollPhysics getScrollPhysics(BuildContext context) { + if (isAndroid) { + return FixedOverscrollPhysics(); + } else { + return ScrollConfiguration.of(context).getScrollPhysics(context); + } + } + + /// Determine whether the viewer needs to be reloaded or not. + /// + bool doChangesRequireReload(PdfViewerParams? other) { + return other == null || + forceReload || + other.margin != margin || + other.backgroundColor != backgroundColor || + other.fitMode != fitMode || + other.maxScale != maxScale || + other.minScale != minScale || + other.useAlternativeFitScaleAsMinScale != useAlternativeFitScaleAsMinScale || + other.panAxis != panAxis || + other.boundaryMargin != boundaryMargin || + other.annotationRenderingMode != annotationRenderingMode || + other.limitRenderingCache != limitRenderingCache || + other.pageAnchor != pageAnchor || + other.pageAnchorEnd != pageAnchorEnd || + other.onePassRenderingScaleThreshold != onePassRenderingScaleThreshold || + other.onePassRenderingSizeThreshold != onePassRenderingSizeThreshold || + other.textSelectionParams != textSelectionParams || + other.matchTextColor != matchTextColor || + other.activeMatchTextColor != activeMatchTextColor || + other.pageDropShadow != pageDropShadow || + other.panEnabled != panEnabled || + other.scaleEnabled != scaleEnabled || + other.interactionEndFrictionCoefficient != interactionEndFrictionCoefficient || + other.scrollByMouseWheel != scrollByMouseWheel || + other.scrollHorizontallyByMouseWheel != scrollHorizontallyByMouseWheel || + other.enableKeyboardNavigation != enableKeyboardNavigation || + other.scrollByArrowKey != scrollByArrowKey || + other.horizontalCacheExtent != horizontalCacheExtent || + other.verticalCacheExtent != verticalCacheExtent || + other.linkHandlerParams != linkHandlerParams || + other.scrollPhysics != scrollPhysics; + } + + @override + bool operator ==(covariant PdfViewerParams other) { + if (identical(this, other)) return true; + + return other.margin == margin && + other.backgroundColor == backgroundColor && + other.fitMode == fitMode && + other.maxScale == maxScale && + other.minScale == minScale && + other.useAlternativeFitScaleAsMinScale == useAlternativeFitScaleAsMinScale && + other.panAxis == panAxis && + other.boundaryMargin == boundaryMargin && + other.annotationRenderingMode == annotationRenderingMode && + other.limitRenderingCache == limitRenderingCache && + other.pageAnchor == pageAnchor && + other.pageAnchorEnd == pageAnchorEnd && + other.onePassRenderingScaleThreshold == onePassRenderingScaleThreshold && + other.onePassRenderingSizeThreshold == onePassRenderingSizeThreshold && + other.textSelectionParams == textSelectionParams && + other.matchTextColor == matchTextColor && + other.activeMatchTextColor == activeMatchTextColor && + other.pageDropShadow == pageDropShadow && + other.panEnabled == panEnabled && + other.scaleEnabled == scaleEnabled && + other.onInteractionEnd == onInteractionEnd && + other.onInteractionStart == onInteractionStart && + other.onInteractionUpdate == onInteractionUpdate && + other.interactionEndFrictionCoefficient == interactionEndFrictionCoefficient && + other.onSecondaryTapUp == onSecondaryTapUp && + other.onLongPressStart == onLongPressStart && + other.onDocumentChanged == onDocumentChanged && + other.onDocumentLoadFinished == onDocumentLoadFinished && + other.calculateInitialPageNumber == calculateInitialPageNumber && + other.calculateInitialZoom == calculateInitialZoom && + other.calculateCurrentPageNumber == calculateCurrentPageNumber && + other.onViewerReady == onViewerReady && + other.onViewSizeChanged == onViewSizeChanged && + other.onPageChanged == onPageChanged && + other.getPageRenderingScale == getPageRenderingScale && + other.scrollByMouseWheel == scrollByMouseWheel && + other.scrollHorizontallyByMouseWheel == scrollHorizontallyByMouseWheel && + other.enableKeyboardNavigation == enableKeyboardNavigation && + other.scrollByArrowKey == scrollByArrowKey && + other.horizontalCacheExtent == horizontalCacheExtent && + other.verticalCacheExtent == verticalCacheExtent && + other.linkHandlerParams == linkHandlerParams && + other.viewerOverlayBuilder == viewerOverlayBuilder && + other.pageOverlaysBuilder == pageOverlaysBuilder && + other.loadingBannerBuilder == loadingBannerBuilder && + other.errorBannerBuilder == errorBannerBuilder && + other.linkWidgetBuilder == linkWidgetBuilder && + other.pagePaintCallbacks == pagePaintCallbacks && + other.pageBackgroundPaintCallbacks == pageBackgroundPaintCallbacks && + other.onGeneralTap == onGeneralTap && + other.buildContextMenu == buildContextMenu && + other.customizeContextMenuItems == customizeContextMenuItems && + other.onKey == onKey && + other.keyHandlerParams == keyHandlerParams && + other.behaviorControlParams == behaviorControlParams && + other.forceReload == forceReload && + other.scrollPhysics == scrollPhysics; + } + + @override + int get hashCode { + return margin.hashCode ^ + backgroundColor.hashCode ^ + fitMode.hashCode ^ + maxScale.hashCode ^ + minScale.hashCode ^ + useAlternativeFitScaleAsMinScale.hashCode ^ + panAxis.hashCode ^ + boundaryMargin.hashCode ^ + annotationRenderingMode.hashCode ^ + limitRenderingCache.hashCode ^ + pageAnchor.hashCode ^ + pageAnchorEnd.hashCode ^ + onePassRenderingScaleThreshold.hashCode ^ + onePassRenderingSizeThreshold.hashCode ^ + textSelectionParams.hashCode ^ + matchTextColor.hashCode ^ + activeMatchTextColor.hashCode ^ + pageDropShadow.hashCode ^ + panEnabled.hashCode ^ + scaleEnabled.hashCode ^ + onInteractionEnd.hashCode ^ + onInteractionStart.hashCode ^ + onInteractionUpdate.hashCode ^ + interactionEndFrictionCoefficient.hashCode ^ + onSecondaryTapUp.hashCode ^ + onLongPressStart.hashCode ^ + onDocumentChanged.hashCode ^ + onDocumentLoadFinished.hashCode ^ + calculateInitialPageNumber.hashCode ^ + calculateInitialZoom.hashCode ^ + calculateCurrentPageNumber.hashCode ^ + onViewerReady.hashCode ^ + onViewSizeChanged.hashCode ^ + onPageChanged.hashCode ^ + getPageRenderingScale.hashCode ^ + scrollByMouseWheel.hashCode ^ + scrollHorizontallyByMouseWheel.hashCode ^ + enableKeyboardNavigation.hashCode ^ + scrollByArrowKey.hashCode ^ + horizontalCacheExtent.hashCode ^ + verticalCacheExtent.hashCode ^ + linkHandlerParams.hashCode ^ + viewerOverlayBuilder.hashCode ^ + pageOverlaysBuilder.hashCode ^ + loadingBannerBuilder.hashCode ^ + errorBannerBuilder.hashCode ^ + linkWidgetBuilder.hashCode ^ + pagePaintCallbacks.hashCode ^ + pageBackgroundPaintCallbacks.hashCode ^ + onGeneralTap.hashCode ^ + buildContextMenu.hashCode ^ + customizeContextMenuItems.hashCode ^ + onKey.hashCode ^ + keyHandlerParams.hashCode ^ + behaviorControlParams.hashCode ^ + forceReload.hashCode ^ + scrollPhysics.hashCode; + } +} + +/// Parameters for text selection. +@immutable +class PdfTextSelectionParams { + const PdfTextSelectionParams({ + this.enabled = true, + this.enableSelectionHandles, + this.showContextMenuAutomatically, + this.buildSelectionHandle, + this.calcSelectionHandleOffset, + this.onTextSelectionChange, + this.onSelectionHandlePanStart, + this.onSelectionHandlePanUpdate, + this.onSelectionHandlePanEnd, + this.magnifier, + }); + + /// Whether text selection is enabled. + final bool enabled; + + /// Whether to use selection handles or not. + /// + /// If true, drag-to-select is disabled and only the selection handles are used to select text. + /// null to determine the behavior based on pointing device. + final bool? enableSelectionHandles; + + /// Whether to automatically show context menu on text selection. + /// + /// Normally, on desktop, the context menu is shown on right-click. + /// If this is true, the context menu is shown on selection handle. + /// null to determine the behavior based on pointing device. + final bool? showContextMenuAutomatically; + + /// Function to build anchor handle for text selection. + /// + /// - If the function returns null, no anchor handle is shown. + /// - If the function is null, the default anchor handle will be used. + final PdfViewerTextSelectionAnchorHandleBuilder? buildSelectionHandle; + + /// Optional callback to calculate the offset for the anchor handles. + /// + /// This callback is called for each anchor handle to determine the offset + /// to apply to the handle's default position. If null, defaults to [Offset.zero]. + final PdfViewerCalcSelectionAnchorHandleOffsetFunction? calcSelectionHandleOffset; + + /// Function to be notified when the text selection is changed. + final PdfViewerTextSelectionChangeCallback? onTextSelectionChange; + + /// Callback for when a selection handle pan starts. + final PdfViewerSelectionHandlePanStartCallback? onSelectionHandlePanStart; + + /// Callback for when a selection handle is being panned. + final PdfViewerSelectionHandlePanUpdateCallback? onSelectionHandlePanUpdate; + + /// Callback for when a selection handle pan ends. + final PdfViewerSelectionHandlePanEndCallback? onSelectionHandlePanEnd; + + /// Parameters for the magnifier. + final PdfViewerSelectionMagnifierParams? magnifier; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is PdfTextSelectionParams && + other.enabled == enabled && + other.buildSelectionHandle == buildSelectionHandle && + other.calcSelectionHandleOffset == calcSelectionHandleOffset && + other.onTextSelectionChange == onTextSelectionChange && + other.onSelectionHandlePanStart == onSelectionHandlePanStart && + other.onSelectionHandlePanUpdate == onSelectionHandlePanUpdate && + other.onSelectionHandlePanEnd == onSelectionHandlePanEnd && + other.enableSelectionHandles == enableSelectionHandles && + other.showContextMenuAutomatically == showContextMenuAutomatically && + other.magnifier == magnifier; + } + + @override + int get hashCode => + enabled.hashCode ^ + buildSelectionHandle.hashCode ^ + calcSelectionHandleOffset.hashCode ^ + onTextSelectionChange.hashCode ^ + onSelectionHandlePanStart.hashCode ^ + onSelectionHandlePanUpdate.hashCode ^ + onSelectionHandlePanEnd.hashCode ^ + enableSelectionHandles.hashCode ^ + showContextMenuAutomatically.hashCode ^ + magnifier.hashCode; +} + +/// Function to build the text selection context menu. +/// +/// The following fragment is a simple example to build a context menu with "Copy" and "Select All" actions: +/// +/// ```dart +/// Widget? _buildTextSelectionContextMenu( +/// BuildContext context, +/// PdfViewerTextSelectionContextMenuBuilderParams params, +/// ) { +/// +/// final items = [ +/// if (params.isTextSelectionEnabled && +/// params.textSelectionDelegate.isCopyAllowed && +/// params.textSelectionDelegate.hasSelectedText) +/// ContextMenuButtonItem( +/// onPressed: () => params.textSelectionDelegate.copyTextSelection(), +/// type: ContextMenuButtonType.copy, +/// ), +/// if (params.isTextSelectionEnabled && !params.textSelectionDelegate.isSelectingAllText) +/// ContextMenuButtonItem( +/// onPressed: () => params.textSelectionDelegate.selectAllText(), +/// type: ContextMenuButtonType.selectAll, +/// ), +/// ]; +/// +/// widget.params.customizeContextMenuItems?.call(params, items); +/// +/// if (items.isEmpty) { +/// return null; +/// } +/// +/// return Align( +/// alignment: Alignment.topLeft, +/// child: AdaptiveTextSelectionToolbar.buttonItems( +/// anchors: TextSelectionToolbarAnchors(primaryAnchor: params.anchorA, secondaryAnchor: params.anchorB), +/// buttonItems: items, +/// ), +/// ); +/// } +/// ``` +/// +/// See [PdfViewerParams.customizeContextMenuItems] for more. +typedef PdfViewerContextMenuBuilder = Widget? Function(BuildContext context, PdfViewerContextMenuBuilderParams params); + +/// Function to customize the context menu items. +/// +/// This function is called when the context menu is built and can be used to customize the context menu items. +/// This function may not be called if the context menu is build using [PdfViewerContextMenuBuilder]. +/// [PdfViewerContextMenuBuilder] is responsible for building the context menu items +/// (i.e. it should decide whether to call this function internally or not). +/// +/// - [params] contains the parameters for building the context menu. +/// - [items] is the list of context menu items to be customized. You can add, remove, or modify the items in this list. +typedef PdfViewerContextMenuUpdateMenuItemsFunction = + void Function(PdfViewerContextMenuBuilderParams params, List items); + +/// Parameters for the text selection context menu builder. +/// +/// [anchorA], [anchorB] are the offsets of the text selection anchors in the local coordinates, which are normally +/// directly corresponding to the `primaryAnchor` and `secondaryAnchor` of [TextSelectionToolbarAnchors] if you use +/// [AdaptiveTextSelectionToolbar.buttonItems]. +/// +/// [a], [b] are the text selection anchors that represent the selected text range. +/// +/// [textSelectionDelegate] provides access to the text selection actions such as copy and clear selection. +/// Please note that the function does not copy the text if [PdfTextSelectionDelegate.isCopyAllowed] is false and +/// use of [PdfTextSelectionDelegate.getSelectedText]/[PdfTextSelectionDelegate.getSelectedTextRanges] is also restricted by the same condition. +/// +/// [dismissContextMenu] is the function to dismiss the context menu. +class PdfViewerContextMenuBuilderParams { + const PdfViewerContextMenuBuilderParams({ + required this.isTextSelectionEnabled, + required this.anchorA, + required this.textSelectionDelegate, + required this.dismissContextMenu, + required this.contextMenuFor, + this.anchorB, + this.a, + this.b, + }); + + final bool isTextSelectionEnabled; + + /// The primary anchor offset in the local coordinates. + final Offset anchorA; + + /// The secondary anchor offset in the local coordinates. + final Offset? anchorB; + + /// The primary text selection anchor. + final PdfTextSelectionAnchor? a; + + /// The secondary text selection anchor. + final PdfTextSelectionAnchor? b; + + /// The text selection delegate to access text selection actions. + final PdfTextSelectionDelegate textSelectionDelegate; + + /// For what target part the context menu will be built. + final PdfViewerPart contextMenuFor; + + /// Function to dismiss the context menu. + final void Function() dismissContextMenu; +} + +/// Where the user taps on. +enum PdfViewerPart { + /// Selected text. + selectedText, + + /// Non-selected text. + nonSelectedText, + + /// Background (it means either page area or outside of page area). + background, +} + +/// State of the text selection anchor handle. +enum PdfViewerTextSelectionAnchorHandleState { normal, hover, dragging } + +/// Function to build the text selection anchor handle. +typedef PdfViewerTextSelectionAnchorHandleBuilder = + Widget? Function( + BuildContext context, + PdfTextSelectionAnchor anchor, + PdfViewerTextSelectionAnchorHandleState state, + ); + +/// Function to calculate the offset for an anchor handle. +/// +/// This callback is called for each anchor handle to determine the offset +/// to apply to the handle's default position. +/// +/// The callback receives the [PdfTextSelectionAnchor] and should return an [Offset] +/// that positions the handle widget relative to the anchor point: +/// - For anchor A (LTR): default anchor point is text's top-left, widget's bottom-right +/// - For anchor B (LTR): default anchor point is text's bottom-right, widget's top-left +typedef PdfViewerCalcSelectionAnchorHandleOffsetFunction = + Offset Function(BuildContext context, PdfTextSelectionAnchor anchor, PdfViewerTextSelectionAnchorHandleState state); + +/// Function to be notified when the text selection is changed. +/// +/// [textSelection] contains the selected text range on each page. +typedef PdfViewerTextSelectionChangeCallback = void Function(PdfTextSelection textSelection); + +/// Callback for when a selection handle pan starts +typedef PdfViewerSelectionHandlePanStartCallback = void Function(PdfTextSelectionAnchor anchor); + +/// Callback for when a selection handle is being panned +typedef PdfViewerSelectionHandlePanUpdateCallback = void Function(PdfTextSelectionAnchor anchor, Offset delta); + +/// Callback for when a selection handle pan ends +typedef PdfViewerSelectionHandlePanEndCallback = void Function(PdfTextSelectionAnchor anchor); + +/// Interface for text selection information. +/// +/// To perform text selection actions, use [PdfTextSelectionDelegate]. +abstract class PdfTextSelection { + /// Whether the text selection is enabled by the configuration. + /// + /// See [PdfTextSelectionParams.enabled]. + bool get isTextSelectionEnabled; + + /// Whether the copy action is allowed. + bool get isCopyAllowed; + + /// Whether the viewer has selected text. + bool get hasSelectedText; + + /// Whether the viewer is currently selecting all text. + bool get isSelectingAllText; + + /// Get the text selection point range. + /// + /// null if there is no text selected. + PdfTextSelectionRange? get textSelectionPointRange; + + /// Get the selected text. + /// + /// Although the use of this property is not restricted by [isCopyAllowed] + /// but you have to ensure that your use of the text does not violate [isCopyAllowed] condition. + Future getSelectedText(); + + /// Get the selected text ranges. + /// + /// Although the use of this property is not restricted by [isCopyAllowed] + /// but you have to ensure that your use of the text does not violate [isCopyAllowed] condition. + Future> getSelectedTextRanges(); +} + +/// Delegate for text selection actions. +/// +/// You can obtain the instance via [PdfViewerController.textSelectionDelegate]. +abstract class PdfTextSelectionDelegate implements PdfTextSelection { + /// Copy the selected text. + /// + /// Please note that the function does not copy the text if [isCopyAllowed] is false. + /// The function returns true if the copy action is successful. + Future copyTextSelection(); + + /// Clear the text selection. + /// + /// By clearing the text selection, the text context menu will be dismissed. + Future clearTextSelection(); + + /// Select all text. + /// + /// The function may take some time to complete if the document is large. + Future selectAllText(); + + /// Select a word at the given position. + /// + /// Please note that [position] is in document coordinates. + Future selectWord(Offset position); + + /// Set the text selection point range. + /// + /// This function will update the current text selection to the specified range. + /// + /// See also [textSelectionPointRange]. + Future setTextSelectionPointRange(PdfTextSelectionRange range); + + /// Convert document coordinates to local coordinates and vice versa. + PdfViewerCoordinateConverter get doc2local; +} + +/// Utility class to convert document coordinates to local coordinates and vice versa. +abstract class PdfViewerCoordinateConverter { + /// Convert a document position to a local position in the specified [context]. + Offset? offsetToLocal(BuildContext context, Offset? position); + + /// Convert a document rectangle to a local rectangle in the specified [context]. + Rect? rectToLocal(BuildContext context, Rect? rect); + + /// Convert a local position in the specified [context] to a document position. + Offset? offsetToDocument(BuildContext context, Offset? position); + + /// Convert a local rectangle in the specified [context] to a document rectangle. + Rect? rectToDocument(BuildContext context, Rect? rect); +} + +/// Parameters for the text selection magnifier. +/// +/// The text selection magnifier is used with text selection handles to help users select text precisely, +/// especially on touch devices. It shows a magnified view of the text around the selection handle +/// as the user drags the handle. +/// +/// Because of this, the magnifier is typically enabled only on touch devices by default and if you want to +/// show the magnifier on non-touch devices, you need to set [enabled] to true and also set +/// [PdfTextSelectionParams.enableSelectionHandles] to true to enable selection handles. +@immutable +class PdfViewerSelectionMagnifierParams { + const PdfViewerSelectionMagnifierParams({ + this.enabled, + this.magnifierSizeThreshold = 72, + this.getMagnifierRectForAnchor, + this.builder, + this.shouldShowMagnifier, + this.calcPosition, + this.animationDuration = const Duration(milliseconds: 100), + this.shouldShowMagnifierForAnchor, + this.maxImageBytesCachedOnMemory = defaultMaxImageBytesCachedOnMemory, + }); + + /// The default maximum image bytes cached on memory is 256 KB. + static const defaultMaxImageBytesCachedOnMemory = 256 * 1024; + + /// Whether the magnifier is enabled. + /// + /// null to determine the behavior based on pointing device. + /// + /// To show magnifier on non-touch devices, set this and [PdfTextSelectionParams.enableSelectionHandles] to true. + final bool? enabled; + + /// If the character size (in pt.) is smaller than this value, the magnifier will be shown. + /// + /// The default is 72 pt. + final double magnifierSizeThreshold; + + /// Function to get the magnifier rectangle for the anchor. + final PdfViewerGetMagnifierRectForAnchor? getMagnifierRectForAnchor; + + /// Function to build the magnifier widget. + final PdfViewerMagnifierBuilder? builder; + + /// Function to control magnifier visibility. + /// + /// This allows for fine grained control of when the magnifier should be shown during text selection, for example + /// to coordinate with custom animations. + /// If null, the magnifier is shown whenever a selection handle is being dragged. + /// + /// Return true to show the magnifier, false to hide it. + /// + /// Even if this function returns true, the magnifier may not be shown if other conditions are not met + /// (e.g., if [enabled] is false or if the character height is above [magnifierSizeThreshold], + /// or [shouldShowMagnifierForAnchor] returns false). + final bool Function()? shouldShowMagnifier; + + /// Function to calculate the magnifier widget position. + /// + /// When provided, this function will be used to determine where to place + /// the magnifier widget in the viewport. If null, pdfrx uses its default + /// positioning logic. + /// + /// This can also be used for context menu positioning or other overlay widgets. + final PdfViewerCalcMagnifierPositionFunction? calcPosition; + + /// Duration for the magnifier position animation. + /// + /// This controls the animation duration when the magnifier position changes + /// as the user drags the selection handle. Set to [Duration.zero] to disable + /// the position animation. + /// + /// Default is 100 milliseconds. + final Duration animationDuration; + + /// Function to determine whether the magnifier should be shown based on conditions such as zoom level. + /// + /// If [enabled] is false, this function is not called. + /// + /// If the function is null, the magnifier is shown if the character height is smaller than + /// [magnifierSizeThreshold]. + /// + /// Please note that the function is called after evaluating [enabled] and [shouldShowMagnifier]. + final PdfViewerMagnifierShouldBeShownFunction? shouldShowMagnifierForAnchor; + + /// The maximum number of image bytes to be cached on memory. + /// + /// The default is 256 * 1024 bytes (256 KB). + final int maxImageBytesCachedOnMemory; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is PdfViewerSelectionMagnifierParams && + other.enabled == enabled && + other.magnifierSizeThreshold == magnifierSizeThreshold && + other.getMagnifierRectForAnchor == getMagnifierRectForAnchor && + other.builder == builder && + other.shouldShowMagnifierForAnchor == shouldShowMagnifierForAnchor && + other.maxImageBytesCachedOnMemory == maxImageBytesCachedOnMemory && + other.calcPosition == calcPosition && + other.shouldShowMagnifier == shouldShowMagnifier && + other.animationDuration == animationDuration; + } + + @override + int get hashCode => + enabled.hashCode ^ + magnifierSizeThreshold.hashCode ^ + getMagnifierRectForAnchor.hashCode ^ + builder.hashCode ^ + shouldShowMagnifierForAnchor.hashCode ^ + maxImageBytesCachedOnMemory.hashCode ^ + calcPosition.hashCode ^ + shouldShowMagnifier.hashCode ^ + animationDuration.hashCode; +} + +/// Function to get the magnifier rectangle for the anchor. +/// +/// This function determines what part of the PDF document to show in the magnifier. +/// +/// Parameters: +/// - [anchor]: The text selection anchor with character information +/// - [params]: Magnifier parameters +/// - [clampedPointerPosition]: The clamped pointer position in viewport coordinates. +/// This is the raw pointer position adjusted for viewport edge clamping to prevent +/// content sliding when the magnifier widget itself is clamped at the viewport edge. +/// Use [PdfViewerController.globalToDocument] to convert to document coordinates if needed. +/// +/// Returns a [Rect] in document coordinates representing the area to magnify. +/// +/// Example: +///```dart +/// getMagnifierRectForAnchor: (textAnchor, params, clampedPointerPosition) { +/// final c = textAnchor.page.charRects[textAnchor.index]; +/// final baseUnit = switch (textAnchor.direction) { +/// PdfTextDirection.ltr || PdfTextDirection.rtl || PdfTextDirection.unknown => c.height, +/// PdfTextDirection.vrtl => c.width, +/// }; +/// // Convert to document coordinates for positioning +/// final pointerInDocument = controller.globalToDocument(clampedPointerPosition) ?? textAnchor.anchorPoint; +/// return Rect.fromLTRB( +/// pointerInDocument.dx - baseUnit * 2.5, +/// textAnchor.rect.top - baseUnit * 0.5, +/// pointerInDocument.dx + baseUnit * 2.5, +/// textAnchor.rect.bottom + baseUnit * 0.5, +/// ); +/// } +///``` +typedef PdfViewerGetMagnifierRectForAnchor = + Rect Function( + PdfTextSelectionAnchor anchor, + PdfViewerSelectionMagnifierParams params, + Offset clampedPointerPosition, + ); + +/// Function to build the magnifier widget. +/// +/// The function can be used to decorate the magnifier widget with additional widgets such as [Container] or [Size]. +/// +/// If the function returns null, the magnifier is not shown. +/// If the function is null, the magnifier is shown with the default magnifier widget. +/// +/// If the function returns a widget of [Positioned] or [Align], the magnifier content is laid out as +/// specified. Otherwise, the widget is laid out automatically. +/// +/// [magnifierContent] is the widget that contains the magnified content. And you can embed it into your widget tree. +/// [magnifierContentSize] is the size of the magnified content in document coordinates; you can use the size to know +/// the aspect ratio of the magnified content. +/// [pointerPosition] is the pointer/finger position in viewport coordinates. +/// [magnifierPosition] is the calculated position for the magnifier widget in viewport coordinates. +/// +/// The following fragment illustrates how to build a magnifier widget with a border and rounded corners: +/// +/// ```dart +/// builder: (context, textAnchor, params, magnifierContent, magnifierContentSize, pointerPosition, magnifierPosition) { +/// // calculate the scale to fit the magnifier content fit into 80x80 box +/// final scale = 80 / min(magnifierContentSize.width, magnifierContentSize.height); +/// return Container( +/// decoration: BoxDecoration( +/// borderRadius: BorderRadius.circular(16), +/// boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 8, spreadRadius: 2)], +/// ), +/// child: ClipRRect(borderRadius: BorderRadius.circular(15), +/// child: SizedBox( +/// width: magnifierContentSize.width * scale, +/// height: magnifierContentSize.height * scale, +/// child: magnifierContent +/// ), +/// ), +/// ); +/// } +/// ``` +typedef PdfViewerMagnifierBuilder = + Widget? Function( + BuildContext context, + PdfTextSelectionAnchor textAnchor, + PdfViewerSelectionMagnifierParams params, + Widget magnifierContent, + Size magnifierContentSize, + Offset pointerPosition, + Offset magnifierPosition, + ); + +/// Function to determine whether the magnifier should be shown or not. +/// +/// Determine whether the magnifier should be shown for the text anchor, [textAnchor], +/// which points to a character in the text. +/// +/// The following fragment illustrates how to determine whether the magnifier should be shown based on the zoom level: +/// +/// ```dart +/// shouldBeShownForAnchor: (textAnchor, controller, params) { +/// final h = textAnchor.direction == PdfTextDirection.vrtl ? textAnchor.rect.size.width : textAnchor.rect.size.height; +/// return h * _currentZoom < params.magnifierSizeThreshold; +/// ``` +typedef PdfViewerMagnifierShouldBeShownFunction = + bool Function( + PdfTextSelectionAnchor textAnchor, + PdfViewerController controller, + PdfViewerSelectionMagnifierParams params, + ); + +/// Function to calculate the position of the magnifier widget in viewport coordinates. +/// +/// This callback allows custom positioning logic for the magnifier. +/// If null, pdfrx uses its default positioning algorithm that handles different text +/// directions (LTR, RTL, VRTL) and viewport edge cases. +/// +/// Parameters: +/// - [widgetSize]: The size of the magnifier widget (null if not yet measured) +/// - [anchorLocalRect]: The anchor's character rectangle in viewport coordinates +/// - [handleLocalRect]: The selection handle rectangle in viewport coordinates (may be null) +/// - [textAnchor]: The text selection anchor with character information (may be null) +/// - [pointerPosition]: The pointer/finger position in viewport coordinates +/// - [margin]: Default margin from viewport edges +/// - [marginOnTop]: Optional custom margin when magnifier is positioned above text +/// - [marginOnBottom]: Optional custom margin when magnifier is positioned below text +typedef PdfViewerCalcMagnifierPositionFunction = + Offset? Function( + Size? widgetSize, + Rect anchorLocalRect, + Rect? handleLocalRect, + PdfTextSelectionAnchor textAnchor, + Offset pointerPosition, { + double margin, + double? marginOnTop, + double? marginOnBottom, + }); + +/// Function to notify that the document is loaded/changed. +typedef PdfViewerDocumentChangedCallback = void Function(PdfDocument? document); + +/// Function to calculate the initial page number. +/// +/// If the function returns null, the viewer will show the page of [PdfViewer.initialPageNumber]. +typedef PdfViewerCalculateInitialPageNumberFunction = + int? Function(PdfDocument document, PdfViewerController controller); + +/// Function to calculate the initial zoom level. +/// +/// If the function returns null, the viewer will use the default zoom level. +/// You can use the following parameters to calculate the zoom level: +/// - [fitZoom] is the zoom level to fit the "initial" page into the viewer. +/// - [coverZoom] is the zoom level to cover the entire viewer with the "initial" page. +typedef PdfViewerCalculateZoomFunction = + double? Function(PdfDocument document, PdfViewerController controller, double fitZoom, double coverZoom); + +/// Function to guess the current page number based on the visible rectangle and page layouts. +typedef PdfViewerCalculateCurrentPageNumberFunction = + int? Function(Rect visibleRect, List pageRects, PdfViewerController controller); + +/// Function called when the viewer is ready. +/// +typedef PdfViewerReadyCallback = void Function(PdfDocument document, PdfViewerController controller); + +/// Function to called when the document loading is finished regardless of success or failure. +/// +/// [documentRef] is the reference to the loaded document. +/// [loadSucceeded] indicates whether the document was loaded successfully. +/// +/// The following fragment illustrates how to use the function: +/// ```dart +/// onDocumentLoadFinished: (documentRef, succeeded) { +/// if (succeeded) { +/// // all the pages are loaded successfully +/// debugPrint('Document loaded successfully.'); +/// } else { +/// // there was an error loading the document +/// final listenable = widget.documentRef.resolveListenable(); +/// final error = listenable.error; +/// final stackTrace = listenable.stackTrace; +/// debugPrint('Document load failed: $error\n$stackTrace'); +/// } +/// } +/// ``` +typedef PdfDocumentLoadFinished = void Function(PdfDocumentRef documentRef, bool loadSucceeded); + +/// Function to be called when the viewer view size is changed. +/// +/// [viewSize] is the new view size. +/// [oldViewSize] is the previous view size. +typedef PdfViewerViewSizeChanged = void Function(Size viewSize, Size? oldViewSize, PdfViewerController controller); + +/// Function called when the current page is changed. +typedef PdfPageChangedCallback = void Function(int? pageNumber); + +/// Function to customize the rendering scale of the page. +/// +/// - [context] is normally used to call [MediaQuery.of] to get the device pixel ratio +/// - [page] can be used to determine the page dimensions +/// - [controller] can be used to get the current zoom by [PdfViewerController.currentZoom] +/// - [estimatedScale] is the precalculated scale for the page +typedef PdfViewerGetPageRenderingScale = + double Function(BuildContext context, PdfPage page, PdfViewerController controller, double estimatedScale); + +/// Function to customize the layout of the pages. +/// +/// **Parameters:** +/// - [pages] - List of pages from the PDF document +/// - [params] - Viewer parameters +/// - [helper] - Layout helper with viewport and margin information +/// +/// **Example:** +/// ```dart +/// layoutPages: (pages, params, helper) { +/// // Use helper for viewport-aware layouts +/// return SequentialPagesLayout.fromPages(pages, params, helper: helper); +/// } +/// ``` +/// +/// If you have custom layout functions, add `helper` parameter: +/// - Old: `(pages, params) => ...` +/// - New: `(pages, params, helper) => ...` +/// +/// The helper provides viewport size and margins for dynamic layouts. +typedef PdfPageLayoutFunction = + PdfPageLayout Function(List pages, PdfViewerParams params, PdfLayoutHelper helper); + +/// Function to normalize the matrix. +/// +/// The function is called when the matrix is changed and normally used to restrict the matrix to certain range. +/// +/// Another use case is to do something when the matrix is changed. +/// +/// If no actual matrix change is needed, just return the input matrix. +typedef PdfMatrixNormalizeFunction = + Matrix4 Function(Matrix4 matrix, Size viewSize, PdfPageLayout layout, PdfViewerController? controller); + +/// Function to build viewer overlays. +/// +/// [size] is the size of the viewer widget. +/// [handleLinkTap] is a function to handle link tap. For more details, see [PdfViewerParams.viewerOverlayBuilder]. +typedef PdfViewerOverlaysBuilder = + List Function(BuildContext context, Size size, PdfViewerHandleLinkTap handleLinkTap); + +/// Function to handle link tap. +/// +/// The function returns true if it processes the link on the specified position; otherwise, returns false. +/// [position] is the position of the tap in the viewer; +/// typically it is [GestureDetector.onTapUp]'s [TapUpDetails.localPosition]. +typedef PdfViewerHandleLinkTap = bool Function(Offset position); + +/// Function to handle tap events. +/// +/// This function is called when the user taps on the viewer. +/// It can be used to handle general tap events such as single tap, double tap, long press, etc. +/// The function returns true if it processes the tap; otherwise, returns false. +/// +/// When the function returns true, the tap is considered handled and the viewer does not process it further. +typedef PdfViewerGeneralTapHandler = + bool Function(BuildContext context, PdfViewerController controller, PdfViewerGeneralTapHandlerDetails details); + +/// Describes the type of the tap. +class PdfViewerGeneralTapHandlerDetails { + const PdfViewerGeneralTapHandlerDetails({ + required this.localPosition, + required this.documentPosition, + required this.type, + required this.tapOn, + }); + + /// The global position of the tap. + final Offset localPosition; + + /// The document position of the tap. + final Offset documentPosition; + + /// The type of the tap. + /// + /// This is used to determine the type of the tap, such as single tap, double tap, long press, etc. + final PdfViewerGeneralTapType type; + + /// Where the tap is occurred on. + /// + /// This is used to determine where the tap is occurred, such as on selected text, non-selected text, or background. + final PdfViewerPart tapOn; +} + +/// Function to build page overlays. +/// +/// [pageRectInViewer] is the rectangle of the page in the viewer; it represents where the page is drawn in the viewer and +/// not the page size in the document. +/// [page] is the page. +typedef PdfPageOverlaysBuilder = List Function(BuildContext context, Rect pageRectInViewer, PdfPage page); + +/// Function to build loading banner. +/// +/// [bytesDownloaded] is the number of bytes downloaded so far. +/// [totalBytes] is the total number of bytes to be downloaded if available. +typedef PdfViewerLoadingBannerBuilder = Widget Function(BuildContext context, int bytesDownloaded, int? totalBytes); + +/// Function to build loading error banner. +typedef PdfViewerErrorBannerBuilder = + Widget Function(BuildContext context, Object error, StackTrace? stackTrace, PdfDocumentRef documentRef); + +/// Function to build link widget for [PdfLink]. +/// +/// [size] is the size of the link. +typedef PdfLinkWidgetBuilder = Widget? Function(BuildContext context, PdfLink link, Size size); + +/// Function to paint things on page. +/// +/// [canvas] is the canvas to paint on. +/// [pageRect] is the rectangle of the page in the viewer. +/// [page] is the page. +/// +/// If you have some [PdfRect] that describes something on the page, +/// you can use [PdfRect].toRect to convert it to [Rect] and draw the rect on the canvas: +/// +/// ```dart +/// PdfRect pdfRect = ...; +/// canvas.drawRect( +/// pdfRect.toRectInDocument(page: page, pageRect: pageRect), +/// Paint()..color = Colors.red); +/// ``` +typedef PdfViewerPagePaintCallback = void Function(ui.Canvas canvas, Rect pageRect, PdfPage page); + +/// When [PdfViewerController.goToPage] is called, the page is aligned to the specified anchor. +/// +/// If the viewer area is smaller than the page, only some part of the page is shown in the viewer. +/// And the anchor determines which part of the page should be shown in the viewer when [PdfViewerController.goToPage] +/// is called. +/// +/// If you prefer to show the top of the page, [top] will do that. +/// +/// If you prefer to show whole the page even if the page will be zoomed down to fit into the viewer, +/// [all] will do that. +/// +/// Basically, [top], [left], [right], [bottom] anchors are used to make page edge line of that side visible inside +/// the view area. +/// +/// [topLeft], [topCenter], [topRight], [centerLeft], [center], [centerRight], [bottomLeft], [bottomCenter], +/// and [bottomRight] are used to make the "point" visible inside the view area. +/// +enum PdfPageAnchor { + top, + left, + right, + bottom, + topLeft, + topCenter, + topRight, + centerLeft, + center, + centerRight, + bottomLeft, + bottomCenter, + bottomRight, + all, +} + +/// Parameters to customize link handling/appearance. +class PdfLinkHandlerParams { + const PdfLinkHandlerParams({ + required this.onLinkTap, + this.linkColor, + this.customPainter, + this.enableAutoLinkDetection = true, + this.laidOverPageOverlays = true, + }); + + /// Function to be called when the link is tapped. + /// + /// The functions should return true if it processes the link; otherwise, it should return false. + final void Function(PdfLink link) onLinkTap; + + /// Color for the link. If null, the default color is `Colors.blue.withValue(alpha: 0.2)`. + /// + /// To fully customize the link appearance, use [customPainter]. + final Color? linkColor; + + /// Custom link painter for the page. + /// + /// The custom painter completely overrides the default link painter. + /// The following fragment is an example to draw a red rectangle on the link area: + /// + /// ```dart + /// customPainter: (canvas, pageRect, page, links) { + /// final paint = Paint() + /// ..color = Colors.red.withValue(alpha: 0.2) + /// ..style = PaintingStyle.fill; + /// for (final link in links) { + /// final rect = link.rect.toRectInDocument(page: page, pageRect: pageRect); + /// canvas.drawRect(rect, paint); + /// } + /// } + /// ``` + final PdfLinkCustomPagePainter? customPainter; + + /// Whether to try to detect Web links automatically or not. + /// This is useful if the PDF file contains text that looks like Web links but not defined as links in the PDF. + /// The default is true. + final bool enableAutoLinkDetection; + + /// Whether the link widgets are laid over page overlays or not. + /// + /// If true, the link widgets are laid over page overlays built by [PdfViewerParams.pageOverlaysBuilder]. + /// If false, the link widgets are laid under page overlays. + /// The default is true. + final bool laidOverPageOverlays; + + @override + bool operator ==(covariant PdfLinkHandlerParams other) { + if (identical(this, other)) return true; + + return other.onLinkTap == onLinkTap && + other.linkColor == linkColor && + other.customPainter == customPainter && + other.enableAutoLinkDetection == enableAutoLinkDetection && + other.laidOverPageOverlays == laidOverPageOverlays; + } + + @override + int get hashCode { + return onLinkTap.hashCode ^ + linkColor.hashCode ^ + customPainter.hashCode ^ + enableAutoLinkDetection.hashCode ^ + laidOverPageOverlays.hashCode; + } +} + +/// Custom painter for the page links. +typedef PdfLinkCustomPagePainter = void Function(ui.Canvas canvas, Rect pageRect, PdfPage page, List links); + +/// Function to handle key events. +/// +/// The function can return one of the following values: +/// Returned value | Description +/// -------------- | ----------- +/// true | The key event is not handled by any other handlers. +/// false | The key event is ignored and propagated to other handlers. +/// null | The key event is handled by the default handler which handles several key events such as arrow keys and page up/down keys. The other keys are just ignored and propagated to other handlers. +/// +/// [params] is the key handler parameters. +/// [key] is the key event. +/// [isRealKeyPress] is true if the key event is the actual key press event. It is false if the key event is generated +/// by key repeat feature. +typedef PdfViewerOnKeyCallback = + bool? Function(PdfViewerKeyHandlerParams params, LogicalKeyboardKey key, bool isRealKeyPress); + +/// Parameters for the built-in key handler. +/// +/// For [autofocus], [canRequestFocus], [focusNode], and [parentNode], +/// please refer to the documentation of [Focus] widget. +class PdfViewerKeyHandlerParams { + const PdfViewerKeyHandlerParams({ + this.enabled = true, + this.autofocus = false, + this.canRequestFocus = true, + this.focusNode, + this.parentNode, + }); + + final bool enabled; + final bool autofocus; + final bool canRequestFocus; + final FocusNode? focusNode; + final FocusNode? parentNode; + + @override + bool operator ==(covariant PdfViewerKeyHandlerParams other) { + if (identical(this, other)) return true; + + return other.enabled == enabled && + other.autofocus == autofocus && + other.canRequestFocus == canRequestFocus && + other.focusNode == focusNode && + other.parentNode == parentNode; + } + + @override + int get hashCode => + enabled.hashCode ^ autofocus.hashCode ^ canRequestFocus.hashCode ^ focusNode.hashCode ^ parentNode.hashCode; +} + +enum PdfViewerGeneralTapType { + /// Tap gesture. + tap, + + /// Double tap gesture. + doubleTap, + + /// Long press gesture. + longPress, + + /// Secondary tap gesture. + secondaryTap, +} + +/// Parameters to customize the behavior of the PDF viewer. +/// +/// These parameters are to tune the behavior/performance of the PDF viewer. +class PdfViewerBehaviorControlParams { + const PdfViewerBehaviorControlParams({ + this.trailingPageLoadingDelay = const Duration(milliseconds: kIsWeb ? 200 : 100), + this.enableLowResolutionPagePreview = true, + this.pageImageCachingDelay = const Duration(milliseconds: kIsWeb ? 20 : 20), + this.partialImageLoadingDelay = const Duration(milliseconds: kIsWeb ? 100 : 0), + }); + + /// How long to wait before loading the trailing pages after the initial page load. + /// + /// This is to ensure that the initial page is displayed quickly, and the trailing pages are loaded in the background. + final Duration trailingPageLoadingDelay; + + /// Whether to enable low resolution page preview. + final bool enableLowResolutionPagePreview; + + /// How long to wait before loading the page image. + final Duration pageImageCachingDelay; + + /// How long to wait before loading the partial real size image. + final Duration partialImageLoadingDelay; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is PdfViewerBehaviorControlParams && + other.trailingPageLoadingDelay == trailingPageLoadingDelay && + other.enableLowResolutionPagePreview == enableLowResolutionPagePreview && + other.pageImageCachingDelay == pageImageCachingDelay && + other.partialImageLoadingDelay == partialImageLoadingDelay; + } + + @override + int get hashCode => + trailingPageLoadingDelay.hashCode ^ + enableLowResolutionPagePreview.hashCode ^ + pageImageCachingDelay.hashCode ^ + partialImageLoadingDelay.hashCode; +} diff --git a/lib/src/widgets/pdf_viewer_scroll_thumb.dart b/packages/pdfrx/lib/src/widgets/pdf_viewer_scroll_thumb.dart similarity index 51% rename from lib/src/widgets/pdf_viewer_scroll_thumb.dart rename to packages/pdfrx/lib/src/widgets/pdf_viewer_scroll_thumb.dart index e41a0b1d..7fc8f04f 100644 --- a/lib/src/widgets/pdf_viewer_scroll_thumb.dart +++ b/packages/pdfrx/lib/src/widgets/pdf_viewer_scroll_thumb.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'pdf_page_layout.dart'; import 'pdf_viewer.dart'; import 'pdf_viewer_params.dart'; @@ -13,6 +14,7 @@ class PdfViewerScrollThumb extends StatefulWidget { this.thumbSize, this.margin = 2, this.thumbBuilder, + this.showVisiblePageRange, super.key, }); @@ -28,8 +30,20 @@ class PdfViewerScrollThumb extends StatefulWidget { /// Margin from the viewer's edge. final double margin; + /// Whether to show the visible page range (all pages with any intersection with the viewport). + /// If null, automatically set to true for spread layouts and false otherwise. + /// When true, the default thumb shows the range of all visible pages (e.g., "1-4"). + /// When false, only shows the current page number. + final bool? showVisiblePageRange; + /// Function to customize the thumb widget. - final Widget? Function(BuildContext context, Size thumbSize, int? pageNumber, PdfViewerController controller)? + final Widget? Function( + BuildContext context, + Size thumbSize, + bool showVisiblePageRange, + PdfPageRange? visiblePageRange, + PdfViewerController controller, + )? thumbBuilder; /// Determine whether the orientation is vertical or not. @@ -49,12 +63,50 @@ class _PdfViewerScrollThumbState extends State { return widget.isVertical ? _buildVertical(context) : _buildHorizontal(context); } + /// Build the thumb widget with visible page range awareness. + Widget _buildThumbWidget(BuildContext context, Size thumbSize) { + final showVisiblePageRange = widget.showVisiblePageRange ?? widget.controller.layout is PdfSpreadLayout; + final pageNumber = widget.controller.pageNumber; + final range = showVisiblePageRange + ? widget.controller.visiblePageRange + : pageNumber != null + ? PdfPageRange.single(pageNumber) + : null; + return widget.thumbBuilder?.call(context, thumbSize, showVisiblePageRange, range, widget.controller) ?? + _buildDefaultThumb(thumbSize, showVisiblePageRange, range); + } + + /// Build default thumb widget with visible page range awareness. + Widget _buildDefaultThumb(Size thumbSize, bool showVisiblePageRange, PdfPageRange? visibleRange) { + final label = visibleRange?.label; + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(5), + boxShadow: [ + BoxShadow(color: Colors.black.withAlpha(127), spreadRadius: 1, blurRadius: 1, offset: const Offset(1, 1)), + ], + ), + child: label != null ? Center(child: Text(label)) : const SizedBox(), + ); + } + Widget _buildVertical(BuildContext context) { final thumbSize = widget.thumbSize ?? const Size(25, 40); final view = widget.controller.visibleRect; final all = widget.controller.documentSize; - if (all.height <= view.height) return const SizedBox(); - final y = -widget.controller.value.y / (all.height - view.height); + final boundaryMargin = widget.controller.params.boundaryMargin; + + final effectiveDocHeight = boundaryMargin == null || boundaryMargin.vertical.isInfinite + ? all.height + : all.height + boundaryMargin.vertical; + + if (effectiveDocHeight <= view.height) return const SizedBox(); + + final scrollRange = effectiveDocHeight - view.height; + final minScrollY = boundaryMargin == null || boundaryMargin.vertical.isInfinite ? 0.0 : -boundaryMargin.top; + + final y = (-widget.controller.value.y - minScrollY) / scrollRange; final vh = view.height * widget.controller.currentZoom - thumbSize.height; final top = y * vh; return Positioned( @@ -64,30 +116,14 @@ class _PdfViewerScrollThumbState extends State { width: thumbSize.width, height: thumbSize.height, child: GestureDetector( - child: - widget.thumbBuilder?.call(context, thumbSize, widget.controller.pageNumber, widget.controller) ?? - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(5), - boxShadow: [ - BoxShadow( - color: Colors.black.withAlpha(127), - spreadRadius: 1, - blurRadius: 1, - offset: const Offset(1, 1), - ), - ], - ), - child: Center(child: Text(widget.controller.pageNumber.toString())), - ), + child: _buildThumbWidget(context, thumbSize), onPanStart: (details) { _panStartOffset = top - details.localPosition.dy; }, onPanUpdate: (details) { final y = (_panStartOffset + details.localPosition.dy) / vh; final m = widget.controller.value.clone(); - m.y = -y * (all.height - view.height); + m.y = -(y * scrollRange + minScrollY); widget.controller.value = m; }, ), @@ -98,9 +134,20 @@ class _PdfViewerScrollThumbState extends State { final thumbSize = widget.thumbSize ?? const Size(40, 25); final view = widget.controller.visibleRect; final all = widget.controller.documentSize; - if (all.width <= view.width) return const SizedBox(); - final x = -widget.controller.value.x / (all.width - view.width); + final boundaryMargin = widget.controller.params.boundaryMargin; + + final effectiveDocWidth = boundaryMargin == null || boundaryMargin.horizontal.isInfinite + ? all.width + : all.width + boundaryMargin.horizontal; + + if (effectiveDocWidth <= view.width) return const SizedBox(); + + final scrollRange = effectiveDocWidth - view.width; + final minScrollX = boundaryMargin == null || boundaryMargin.horizontal.isInfinite ? 0.0 : -boundaryMargin.left; + + final x = (-widget.controller.value.x - minScrollX) / scrollRange; final vw = view.width * widget.controller.currentZoom - thumbSize.width; + final left = x * vw; return Positioned( top: widget.orientation == ScrollbarOrientation.top ? widget.margin : null, @@ -109,30 +156,14 @@ class _PdfViewerScrollThumbState extends State { width: thumbSize.width, height: thumbSize.height, child: GestureDetector( - child: - widget.thumbBuilder?.call(context, thumbSize, widget.controller.pageNumber, widget.controller) ?? - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(5), - boxShadow: [ - BoxShadow( - color: Colors.black.withAlpha(127), - spreadRadius: 1, - blurRadius: 1, - offset: const Offset(1, 1), - ), - ], - ), - child: Center(child: Text(widget.controller.pageNumber.toString())), - ), + child: _buildThumbWidget(context, thumbSize), onPanStart: (details) { _panStartOffset = left - details.localPosition.dx; }, onPanUpdate: (details) { final x = (_panStartOffset + details.localPosition.dx) / vw; final m = widget.controller.value.clone(); - m.x = -x * (all.width - view.width); + m.x = -(x * scrollRange + minScrollX); widget.controller.value = m; }, ), diff --git a/lib/src/widgets/pdf_widgets.dart b/packages/pdfrx/lib/src/widgets/pdf_widgets.dart similarity index 54% rename from lib/src/widgets/pdf_widgets.dart rename to packages/pdfrx/lib/src/widgets/pdf_widgets.dart index d1060cc8..8021a8fe 100644 --- a/lib/src/widgets/pdf_widgets.dart +++ b/packages/pdfrx/lib/src/widgets/pdf_widgets.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:developer' as developer; import 'dart:math'; import 'dart:ui' as ui; @@ -37,42 +38,96 @@ import '../../pdfrx.dart'; /// ), /// ``` class PdfDocumentViewBuilder extends StatefulWidget { - const PdfDocumentViewBuilder({required this.documentRef, required this.builder, super.key}); + /// Creates a widget that loads PDF document. + const PdfDocumentViewBuilder({ + required this.documentRef, + required this.builder, + this.loadingBuilder, + this.errorBuilder, + super.key, + }); + /// Creates a widget that loads PDF document from an asset. + /// + /// - [assetName] is the name of the asset. + /// - [builder] is the builder that builds the widget tree with the PDF document. + /// - [loadingBuilder] is the builder that builds the loading widget. + /// - [errorBuilder] is the builder that builds the error widget on error. + /// - [passwordProvider] is the provider for the password of the PDF document. + /// - [firstAttemptByEmptyPassword] indicates whether to try to open the document with an empty password first. + /// - [useProgressiveLoading] indicates whether to use progressive loading. + /// - [autoDispose] indicates whether to automatically dispose the document when the widget is disposed. + /// + /// Returns the created widget. PdfDocumentViewBuilder.asset( String assetName, { required this.builder, + this.loadingBuilder, + this.errorBuilder, super.key, PdfPasswordProvider? passwordProvider, bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, bool autoDispose = true, }) : documentRef = PdfDocumentRefAsset( assetName, passwordProvider: passwordProvider, firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, autoDispose: autoDispose, ); + /// Creates a widget that loads PDF document from a file. + /// + /// - [filePath] is the path of the file. + /// - [builder] is the builder that builds the widget tree with the PDF document. + /// - [loadingBuilder] is the builder that builds the loading widget. + /// - [errorBuilder] is the builder that builds the error widget on error. + /// - [passwordProvider] is the provider for the password of the PDF document. + /// - [firstAttemptByEmptyPassword] indicates whether to try to open the document with an empty password first. + /// - [useProgressiveLoading] indicates whether to use progressive loading. + /// - [autoDispose] indicates whether to automatically dispose the document when the widget is disposed. + /// + /// Returns the created widget. PdfDocumentViewBuilder.file( String filePath, { required this.builder, + this.loadingBuilder, + this.errorBuilder, super.key, PdfPasswordProvider? passwordProvider, bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, bool autoDispose = true, }) : documentRef = PdfDocumentRefFile( filePath, passwordProvider: passwordProvider, firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, autoDispose: autoDispose, ); + /// Creates a widget that loads PDF document from a URI. + /// + /// - [uri] is the URI of the PDF document. + /// - [builder] is the builder that builds the widget tree with the PDF document. + /// - [loadingBuilder] is the builder that builds the loading widget. + /// - [errorBuilder] is the builder that builds the error widget on error. + /// - [passwordProvider] is the provider for the password of the PDF document. + /// - [firstAttemptByEmptyPassword] indicates whether to try to open the document with an empty password first. + /// - [useProgressiveLoading] indicates whether to use progressive loading. + /// - [autoDispose] indicates whether to automatically dispose the document when the widget is disposed. + /// + /// Returns the created widget. PdfDocumentViewBuilder.uri( Uri uri, { required this.builder, + this.loadingBuilder, + this.errorBuilder, super.key, PdfPasswordProvider? passwordProvider, bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, bool autoDispose = true, bool preferRangeAccess = false, Map? headers, @@ -81,50 +136,98 @@ class PdfDocumentViewBuilder extends StatefulWidget { uri, passwordProvider: passwordProvider, firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, autoDispose: autoDispose, preferRangeAccess: preferRangeAccess, headers: headers, withCredentials: withCredentials, ); - /// A reference to the PDF document. + /// The reference to the PDF document. final PdfDocumentRef documentRef; - /// A builder that builds a widget tree with the PDF document. + /// The builder that builds the widget tree with the PDF document. final PdfDocumentViewBuilderFunction builder; + /// The builder that builds the loading widget. + final WidgetBuilder? loadingBuilder; + + /// The builder that builds the error widget on error. + final PdfDocumentViewBuilderErrorBuilder? errorBuilder; + @override State createState() => _PdfDocumentViewBuilderState(); - - static PdfDocumentViewBuilder? maybeOf(BuildContext context) { - return context.findAncestorWidgetOfExactType(); - } } +/// A function that builds a widget tree when an error occurs while loading the PDF document. +/// +/// [context] is the build context. +/// [error] is the error that occurred. +/// [stackTrace] is the stack trace of the error, which may be null. +typedef PdfDocumentViewBuilderErrorBuilder = + Widget Function(BuildContext context, Object error, StackTrace? stackTrace); + class _PdfDocumentViewBuilderState extends State { + StreamSubscription? _updateSubscription; + @override void initState() { super.initState(); + pdfrxFlutterInitialize(); widget.documentRef.resolveListenable() ..addListener(_onDocumentChanged) ..load(); + _onDocumentChanged(); + } + + @override + void didUpdateWidget(covariant PdfDocumentViewBuilder oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.documentRef != oldWidget.documentRef) { + oldWidget.documentRef.resolveListenable().removeListener(_onDocumentChanged); + widget.documentRef.resolveListenable() + ..addListener(_onDocumentChanged) + ..load(); + _onDocumentChanged(); + } } @override void dispose() { + _updateSubscription?.cancel(); widget.documentRef.resolveListenable().removeListener(_onDocumentChanged); super.dispose(); } void _onDocumentChanged() { if (mounted) { - setState(() {}); + _updateSubscription?.cancel(); + final document = widget.documentRef.resolveListenable().document; + _updateSubscription = document?.events.listen((event) { + if (mounted && event.type == PdfDocumentEventType.pageStatusChanged) { + setState(() {}); + } + }); + document?.loadPagesProgressively(); + if (mounted) setState(() {}); } } @override Widget build(BuildContext context) { final listenable = widget.documentRef.resolveListenable(); + + // Handle error + if (listenable.error != null && widget.errorBuilder != null) { + return widget.errorBuilder!(context, listenable.error!, listenable.stackTrace); + } + + // Handle loading + if (listenable.document == null && widget.loadingBuilder != null) { + return widget.loadingBuilder!(context); + } + + // Render document return widget.builder(context, listenable.document); } } @@ -136,9 +239,10 @@ typedef PdfDocumentViewBuilderFunction = Widget Function(BuildContext context, P /// /// [biggestSize] is the size of the widget. /// [page] is the page to be displayed. +/// [rotationOverride] is the rotation to override the page rotation. /// /// The function returns the size of the page. -typedef PdfPageViewSizeCallback = Size Function(Size biggestSize, PdfPage page); +typedef PdfPageViewSizeCallback = Size Function(Size biggestSize, PdfPage page, PdfPageRotation? rotationOverride); /// Function to build a widget that wraps the page image. /// @@ -159,6 +263,7 @@ class PdfPageView extends StatefulWidget { const PdfPageView({ required this.document, required this.pageNumber, + this.rotationOverride, this.maximumDpi = 300, this.alignment = Alignment.center, this.decoration, @@ -174,6 +279,9 @@ class PdfPageView extends StatefulWidget { /// The page number to be displayed. (The first page is 1). final int pageNumber; + /// The rotation to override the page rotation. + final PdfPageRotation? rotationOverride; + /// The maximum DPI of the page image. The default value is 300. /// /// The value is used to limit the actual image size to avoid excessive memory usage. @@ -207,12 +315,52 @@ class _PdfPageViewState extends State { ui.Image? _image; Size? _pageSize; PdfPageRenderCancellationToken? _cancellationToken; + StreamSubscription? _eventSubscription; + + @override + void initState() { + super.initState(); + _subscribeToDocumentEvents(); + } @override void dispose() { + _eventSubscription?.cancel(); + _clearCache(refresh: false); + super.dispose(); + } + + @override + void didUpdateWidget(covariant PdfPageView oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.document != oldWidget.document || + widget.pageNumber != oldWidget.pageNumber || + widget.rotationOverride != oldWidget.rotationOverride) { + _clearCache(); + _subscribeToDocumentEvents(); + } + } + + void _clearCache({bool refresh = true}) { _image?.dispose(); + _image = null; + _pageSize = null; _cancellationToken?.cancel(); - super.dispose(); + _cancellationToken = null; + if (refresh && mounted) { + setState(() {}); + } + } + + void _subscribeToDocumentEvents() { + _eventSubscription?.cancel(); + _eventSubscription = widget.document?.events.listen((event) { + if (event is PdfDocumentPageStatusChangedEvent) { + if (event.changes.keys.contains(widget.pageNumber)) { + _clearCache(); + } + } + }); } Widget _defaultDecorationBuilder(BuildContext context, Size pageSize, PdfPage page, RawImage? pageImage) { @@ -253,11 +401,11 @@ class _PdfPageViewState extends State { widget.document!.pages[widget.pageNumber - 1], _image != null ? RawImage( - image: _image, - width: _pageSize!.width * scale, - height: _pageSize!.height * scale, - fit: BoxFit.fill, - ) + image: _image, + width: _pageSize!.width * scale, + height: _pageSize!.height * scale, + fit: BoxFit.fill, + ) : null, ); } @@ -275,10 +423,14 @@ class _PdfPageViewState extends State { final Size pageSize; if (widget.pageSizeCallback != null) { - pageSize = widget.pageSizeCallback!(size, page); + pageSize = widget.pageSizeCallback!(size, page, widget.rotationOverride); } else { - final scale = min(widget.maximumDpi / 72, min(size.width / page.width, size.height / page.height)); - pageSize = Size(page.width * scale, page.height * scale); + final swapWH = ((widget.rotationOverride ?? page.rotation).index - page.rotation.index) & 1 == 1; + final w = swapWH ? page.height : page.width; + final h = swapWH ? page.width : page.height; + + final scale = min(widget.maximumDpi / 72, min(size.width / w, size.height / h)); + pageSize = Size(w * scale, h * scale); } if (pageSize == _pageSize) return; @@ -289,16 +441,20 @@ class _PdfPageViewState extends State { final pageImage = await page.render( fullWidth: pageSize.width, fullHeight: pageSize.height, + rotationOverride: widget.rotationOverride, cancellationToken: _cancellationToken, ); if (pageImage == null) return; - final newImage = await pageImage.createImage(); - pageImage.dispose(); - final oldImage = _image; - _image = newImage; - oldImage?.dispose(); - if (mounted) { - setState(() {}); + try { + final newImage = await pageImage.createImage(); + pageImage.dispose(); + _image = newImage; + if (mounted) { + setState(() {}); + } + } catch (e) { + developer.log('Error creating image: $e'); + pageImage.dispose(); } } } diff --git a/packages/pdfrx/pubspec.yaml b/packages/pdfrx/pubspec.yaml new file mode 100644 index 00000000..6d1a0e82 --- /dev/null +++ b/packages/pdfrx/pubspec.yaml @@ -0,0 +1,47 @@ +name: pdfrx +description: pdfrx is a rich and fast PDF viewer and manipulation plugin built on the top of PDFium. Supports viewing, editing, combining PDFs on Android, iOS, Windows, macOS, Linux, and Web. +version: 2.2.24 +homepage: https://github.com/espresso3389/pdfrx +repository: https://github.com/espresso3389/pdfrx/tree/master/packages/pdfrx +issue_tracker: https://github.com/espresso3389/pdfrx/issues +screenshots: + - description: 'Viewer example on Web' + path: screenshot.jpg + +environment: + sdk: '>=3.9.0 <4.0.0' + flutter: '>=3.35.1' +resolution: workspace + +dependencies: + pdfrx_engine: ^0.3.9 + pdfium_flutter: ^0.1.8 + collection: + crypto: ^3.0.6 + ffi: + flutter: + sdk: flutter + http: + path: + path_provider: ^2.1.5 + rxdart: + synchronized: ^3.4.0 + url_launcher: ^6.3.2 + vector_math: ^2.2.0 + web: ^1.1.1 + yaml: + +dev_dependencies: + ffigen: ^19.1.0 + flutter_test: + sdk: flutter + flutter_lints: + archive: ^4.0.7 + +executables: + remove_wasm_modules: + remove_darwin_pdfium_modules: + +flutter: + assets: + - assets/ diff --git a/packages/pdfrx/screenshot.jpg b/packages/pdfrx/screenshot.jpg new file mode 100644 index 00000000..408faeab Binary files /dev/null and b/packages/pdfrx/screenshot.jpg differ diff --git a/packages/pdfrx/test/pdf_viewer_test.dart b/packages/pdfrx/test/pdf_viewer_test.dart new file mode 100644 index 00000000..761ce138 --- /dev/null +++ b/packages/pdfrx/test/pdf_viewer_test.dart @@ -0,0 +1,33 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:pdfrx/pdfrx.dart'; + +final testPdfFile = File('example/viewer/assets/hello.pdf'); +final binding = TestWidgetsFlutterBinding.ensureInitialized(); + +void main() { + // For testing purpose, we should run on the command line + // and pdfrxInitialize is a better way to initialize the library. + setUp(() => pdfrxInitialize()); + Pdfrx.createHttpClient = () => MockClient((request) async { + return http.Response.bytes(await testPdfFile.readAsBytes(), 200); + }); + + testWidgets('PdfViewer.uri', (tester) async { + await binding.setSurfaceSize(Size(1080, 1920)); + await tester.pumpWidget( + MaterialApp( + // FIXME: Just a workaround for "A RenderFlex overflowed..." + home: SingleChildScrollView(child: PdfViewer.uri(Uri.parse('https://example.com/hello.pdf'))), + ), + ); + + await tester.pumpAndSettle(); + + expect(find.byType(PdfViewer), findsOneWidget); + }); +} diff --git a/wasm/pdfrx_wasm/.gitignore b/packages/pdfrx_coregraphics/.gitignore similarity index 95% rename from wasm/pdfrx_wasm/.gitignore rename to packages/pdfrx_coregraphics/.gitignore index e7d347d9..b9d7f25b 100644 --- a/wasm/pdfrx_wasm/.gitignore +++ b/packages/pdfrx_coregraphics/.gitignore @@ -28,6 +28,6 @@ migrate_working_dir/ /pubspec.lock **/doc/api/ .dart_tool/ -.flutter-plugins .flutter-plugins-dependencies -build/ +/build/ +/coverage/ diff --git a/packages/pdfrx_coregraphics/.metadata b/packages/pdfrx_coregraphics/.metadata new file mode 100644 index 00000000..7b98f660 --- /dev/null +++ b/packages/pdfrx_coregraphics/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "9f455d2486bcb28cad87b062475f42edc959f636" + channel: "stable" + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + base_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + - platform: ios + create_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + base_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + - platform: macos + create_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + base_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packages/pdfrx_coregraphics/.swiftformat b/packages/pdfrx_coregraphics/.swiftformat new file mode 100644 index 00000000..838bc469 --- /dev/null +++ b/packages/pdfrx_coregraphics/.swiftformat @@ -0,0 +1,37 @@ +# SwiftFormat configuration for pdfrx_coregraphics +--swiftversion 5.0 + +# Indentation +--indent 2 +--tabwidth 2 +--xcodeindentation enabled + +# Spacing +--trimwhitespace always +--stripunusedargs closure-only + +# Wrapping +--maxwidth 120 +--wraparguments before-first +--wrapparameters before-first +--wrapcollections before-first + +# Line breaks +--elseposition next-line +--guardelse next-line + +# Other formatting +--self remove +--importgrouping testable-bottom +--commas inline +--decimalgrouping 3,4 +--binarygrouping 4,8 +--octalgrouping 4,8 +--hexgrouping 4,8 +--fractiongrouping disabled +--exponentgrouping disabled +--hexliteralcase uppercase +--ifdef no-indent + +# Disabled rules (if any specific rules need to be disabled) +# --disable diff --git a/packages/pdfrx_coregraphics/CHANGELOG.md b/packages/pdfrx_coregraphics/CHANGELOG.md new file mode 100644 index 00000000..36b72ffe --- /dev/null +++ b/packages/pdfrx_coregraphics/CHANGELOG.md @@ -0,0 +1,68 @@ +## 0.1.16 + +- Updated to pdfrx_engine 0.3.9 +- NEW: `PdfrxEntryFunctions.stopBackgroundWorker()` support ([#184](https://github.com/espresso3389/pdfrx/issues/184), [#430](https://github.com/espresso3389/pdfrx/issues/430)) + +## 0.1.15 + +- Updated to pdfrx_engine 0.3.8 +- Implemented `PdfDocumentLoadCompleteEvent` for CoreGraphics backend + +## 0.1.12 + +- FIXED: Inconsistent environment constraints - Flutter version now correctly requires 3.35.1+ to match Dart 3.9.0 requirement ([#553](https://github.com/espresso3389/pdfrx/issues/553)) + +## 0.1.11 + +- Updated to pdfrx_engine 0.3.7 +- Dependency configuration updates +- WIP: Adding `PdfDocument.useNativeDocumentHandle`/`reloadPages` + +## 0.1.10 + +- Updated to pdfrx_engine 0.3.6 +- NEW: [`PdfDateTime`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDateTime.html) extension type for PDF date string parsing +- NEW: [`PdfAnnotation`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfAnnotation-class.html) class for PDF annotation metadata extraction ([#546](https://github.com/espresso3389/pdfrx/pull/546)) + +## 0.1.9 + +- Updated to pdfrx_engine 0.3.4 +- NEW: Progressive loading helper functions - [`ensureLoaded()`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPageBaseExtensions/ensureLoaded.html) and [`waitForLoaded()`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPageBaseExtensions/waitForLoaded.html) +- NEW: [`PdfPageStatusChange.page`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPageStatusChange/page.html) property for easier access to updated page instances + +## 0.1.8 + +- Updated to pdfrx_engine 0.3.0 + +## 0.1.7 + +- Updated to pdfrx_engine 0.2.4 + +## 0.1.6 + +- Updated to pdfrx_engine 0.2.3 + +## 0.1.5 + +- Fixed destination handling issues where some PDF destinations could not be processed correctly + +## 0.1.4 + +- Updated to pdfrx_engine 0.2.2 +- Updated README example to remove explicit `WidgetsFlutterBinding.ensureInitialized()` call (now handled internally by `pdfrxFlutterInitialize()`) +- Implemented `PdfrxBackend` enum support + +## 0.1.3 + +- **BREAKING**: Renamed `PdfrxEntryFunctions.initPdfium()` to `PdfrxEntryFunctions.init()` for consistency +- Updated README with documentation for `dart run pdfrx:remove_darwin_pdfium_modules` command to reduce app size +- Updated to pdfrx_engine 0.2.0 + +## 0.1.2 + +- Added Swift Package Manager (SwiftPM) support for easier integration +- Internal code structure reorganization and formatting improvements + +## 0.1.1 + +- Initial CoreGraphics-backed Pdfrx entry implementation for iOS/macOS diff --git a/packages/pdfrx_coregraphics/LICENSE b/packages/pdfrx_coregraphics/LICENSE new file mode 100644 index 00000000..575416cb --- /dev/null +++ b/packages/pdfrx_coregraphics/LICENSE @@ -0,0 +1,11 @@ + +The MIT License (MIT) +=============== + +Copyright (c) 2018 @espresso3389 (Takashi Kawasaki) + +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. diff --git a/packages/pdfrx_coregraphics/README.md b/packages/pdfrx_coregraphics/README.md new file mode 100644 index 00000000..3cf22f6b --- /dev/null +++ b/packages/pdfrx_coregraphics/README.md @@ -0,0 +1,67 @@ +# pdfrx_coregraphics + +CoreGraphics-backed renderer for [pdfrx](https://pub.dev/packages/pdfrx) on iOS and macOS. + +**⚠️ EXPERIMENTAL: This package is in very early experimental stage. APIs and functionality may change significantly.** + +This plugin provides a [`PdfrxEntryFunctions`](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfrxEntryFunctions-class.html) implementation that uses PDFKit/CoreGraphics instead of the bundled PDFium +runtime. It is intended for teams that prefer the system PDF stack on Apple platforms while keeping the pdfrx widget +API. + +## Installation + +Add the package to your Flutter app: + +```yaml +dependencies: + pdfrx: ^2.2.23 + pdfrx_coregraphics: ^0.1.15 +``` + +Set the CoreGraphics entry functions before initializing pdfrx: + +```dart +import 'package:flutter/material.dart'; +import 'package:pdfrx/pdfrx.dart'; +import 'package:pdfrx_coregraphics/pdfrx_coregraphics.dart'; + +void main() { + PdfrxEntryFunctions.instance = PdfrxCoreGraphicsEntryFunctions(); + pdfrxFlutterInitialize(); + runApp(const MyApp()); +} +``` + +After installation, use pdfrx as usual. All [`PdfDocument`](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfDocument-class.html) and widget APIs continue to work, but rendering is routed +through CoreGraphics. + +## Removing PDFium Dependencies (Reducing App Size) + +By default, pdfrx bundles PDFium shared libraries for iOS and macOS even when using `pdfrx_coregraphics`. If you're only using the CoreGraphics backend, you can remove these PDFium dependencies to reduce your app size. + +Run this command from your project root: + +```bash +flutter clean # if the environment is not clean +flutter pub get +dart run pdfrx:remove_darwin_pdfium_modules +``` + +This will comment out the iOS and macOS ffiPlugin configurations in pdfrx's `pubspec.yaml`, preventing PDFium binaries from being bundled with your app. + +To revert the changes (restore PDFium dependencies): + +```bash +flutter clean # if the environment is not clean +flutter pub get +dart run pdfrx:remove_darwin_pdfium_modules --revert +``` + +After executing it, you can run `flutter build` or `flutter run` for iOS/macOS. + +## Limitations + +- Incremental/custom stream loading is converted to in-memory loading +- Custom font registration is not yet supported +- In document links are always `xyz` and zoom is not reliable (or omitted) in certain situations +- Text extraction does not fully cover certain scenarios like vertical texts or R-to-L texts so far diff --git a/packages/pdfrx_coregraphics/analysis_options.yaml b/packages/pdfrx_coregraphics/analysis_options.yaml new file mode 100644 index 00000000..a5744c1c --- /dev/null +++ b/packages/pdfrx_coregraphics/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/pdfrx_coregraphics/darwin/Assets/.gitkeep b/packages/pdfrx_coregraphics/darwin/Assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/pdfrx_coregraphics/darwin/Resources/PrivacyInfo.xcprivacy b/packages/pdfrx_coregraphics/darwin/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 00000000..a34b7e2e --- /dev/null +++ b/packages/pdfrx_coregraphics/darwin/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyTrackingDomains + + NSPrivacyAccessedAPITypes + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/packages/pdfrx_coregraphics/darwin/pdfrx_coregraphics.podspec b/packages/pdfrx_coregraphics/darwin/pdfrx_coregraphics.podspec new file mode 100644 index 00000000..be7247f6 --- /dev/null +++ b/packages/pdfrx_coregraphics/darwin/pdfrx_coregraphics.podspec @@ -0,0 +1,37 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint pdfrx_coregraphics.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'pdfrx_coregraphics' + s.version = '0.1.2' + s.summary = 'CoreGraphics-backed renderer for pdfrx on Apple platforms.' + s.description = <<-DESC +Provides a PdfrxEntryFunctions implementation that uses PDFKit/CoreGraphics instead of PDFium. + DESC + s.homepage = 'https://github.com/espresso3389/pdfrx' + s.license = { :type => 'BSD', :file => '../LICENSE' } + s.author = { 'Takashi Kawasaki' => 'espresso3389@gmail.com' } + s.source = { :path => '.' } + s.source_files = [ 'pdfrx_coregraphics/Sources/PdfrxCoregraphicsPlugin.swift' ] + + s.ios.deployment_target = '13.0' + s.ios.dependency 'Flutter' + # Flutter.framework does not contain a i386 slice. + s.ios.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', + } + + s.osx.deployment_target = '10.13' + s.osx.dependency 'FlutterMacOS' + s.osx.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + + s.swift_version = '5.0' + + # If your plugin requires a privacy manifest, for example if it uses any + # required reason APIs, update the PrivacyInfo.xcprivacy file to describe your + # plugin's privacy impact, and then uncomment this line. For more information, + # see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files + # s.resource_bundles = {'pdfrx_coregraphics_privacy' => ['Resources/PrivacyInfo.xcprivacy']} +end diff --git a/packages/pdfrx_coregraphics/darwin/pdfrx_coregraphics/Package.swift b/packages/pdfrx_coregraphics/darwin/pdfrx_coregraphics/Package.swift new file mode 100644 index 00000000..a0f99113 --- /dev/null +++ b/packages/pdfrx_coregraphics/darwin/pdfrx_coregraphics/Package.swift @@ -0,0 +1,24 @@ +// swift-tools-version:5.6 +import PackageDescription + +let package = Package( + name: "pdfrx_coregraphics", + platforms: [ + .iOS(.v13), + .macOS(.v10_13) + ], + products: [ + .library( + name: "pdfrx-coregraphics", + targets: ["pdfrx_coregraphics"] + ) + ], + targets: [ + .target( + name: "pdfrx_coregraphics", + dependencies: [], + path: ".", + sources: ["Sources"] + ) + ] +) diff --git a/packages/pdfrx_coregraphics/darwin/pdfrx_coregraphics/Sources/PdfrxCoregraphicsPlugin.swift b/packages/pdfrx_coregraphics/darwin/pdfrx_coregraphics/Sources/PdfrxCoregraphicsPlugin.swift new file mode 100644 index 00000000..253d861a --- /dev/null +++ b/packages/pdfrx_coregraphics/darwin/pdfrx_coregraphics/Sources/PdfrxCoregraphicsPlugin.swift @@ -0,0 +1,1613 @@ +import Foundation +import PDFKit + +#if os(iOS) +import Flutter +#elseif os(macOS) +import FlutterMacOS +#endif + +/// Flutter-side bridge that mirrors the PdfRx engine API by combining PDFKit conveniences with +/// CoreGraphics access. +public class PdfrxCoregraphicsPlugin: NSObject, FlutterPlugin { + private var nextHandle: Int64 = 1 + private var documents: [Int64: PDFDocument] = [:] + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel( + name: "pdfrx_coregraphics", + binaryMessenger: registrar.pdfrxCoreGraphicsMessenger + ) + let instance = PdfrxCoregraphicsPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "initialize": + result(nil) + case "openDocument": + openDocument(arguments: call.arguments, result: result) + case "createNewDocument": + createNewDocument(arguments: call.arguments, result: result) + case "createDocumentFromJpegData": + createDocumentFromJpegData(arguments: call.arguments, result: result) + case "renderPage": + renderPage(arguments: call.arguments, result: result) + case "loadPageText": + loadPageText(arguments: call.arguments, result: result) + case "closeDocument": + closeDocument(arguments: call.arguments, result: result) + case "loadOutline": + loadOutline(arguments: call.arguments, result: result) + case "loadPageLinks": + loadPageLinks(arguments: call.arguments, result: result) + default: + result(FlutterMethodNotImplemented) + } + } + + /// Opens a PDF document from file, bytes, or custom providers and registers it under a handle. + private func openDocument(arguments: Any?, result: @escaping FlutterResult) { + guard let args = arguments as? [String: Any] else { + result( + FlutterError( + code: "bad-arguments", message: "Invalid arguments for openDocument.", details: nil + )) + return + } + guard let sourceType = args["sourceType"] as? String else { + result(FlutterError(code: "missing-source", message: "sourceType is required.", details: nil)) + return + } + let password = args["password"] as? String + + let document: PDFDocument? + switch sourceType { + case "file": + guard let path = args["path"] as? String else { + result( + FlutterError( + code: "missing-path", message: "File path is required for openDocument.", details: nil + )) + return + } + document = PDFDocument(url: URL(fileURLWithPath: path)) + case "bytes": + guard let data = args["bytes"] as? FlutterStandardTypedData else { + result( + FlutterError( + code: "missing-bytes", message: "PDF bytes are required for openDocument.", details: nil + )) + return + } + document = PDFDocument(data: data.data) + default: + result( + FlutterError( + code: "unsupported-source", message: "Unsupported sourceType \(sourceType).", details: nil + )) + return + } + + guard let pdfDocument = document else { + result( + FlutterError(code: "open-failed", message: "Failed to open PDF document.", details: nil)) + return + } + + if pdfDocument.isLocked { + let candidatePassword = password ?? "" + if !pdfDocument.unlock(withPassword: candidatePassword) || pdfDocument.isLocked { + result( + FlutterError( + code: "wrong-password", message: "Password is required or incorrect.", details: nil + )) + return + } + } + + guard pdfDocument.pageCount > 0 else { + result( + FlutterError( + code: "empty-document", message: "PDF document does not contain any pages.", details: nil + ) + ) + return + } + + let handle = nextHandle + nextHandle += 1 + documents[handle] = pdfDocument + + var pageInfos: [[String: Any]] = [] + for index in 0 ..< pdfDocument.pageCount { + guard let page = pdfDocument.page(at: index) else { + continue + } + let bounds = page.bounds(for: .mediaBox) + pageInfos.append([ + "width": Double(bounds.width), + "height": Double(bounds.height), + "rotation": page.rotation + ]) + } + + result([ + "handle": handle, + "isEncrypted": pdfDocument.isEncrypted, + "pages": pageInfos + ]) + } + + /// Creates a new empty PDF document + private func createNewDocument(arguments: Any?, result: @escaping FlutterResult) { + // Create a truly empty PDF document with no pages + // PDFDocument() creates an empty document directly + guard let pdfDocument = PDFDocument() else { + result( + FlutterError( + code: "pdf-document-failure", message: "Failed to create empty PDFDocument.", details: nil + )) + return + } + + // Register the document + let handle = nextHandle + nextHandle += 1 + documents[handle] = pdfDocument + + result([ + "handle": handle, + "isEncrypted": false, + "pages": [] + ]) + } + + /// Creates a PDF document from JPEG data + private func createDocumentFromJpegData(arguments: Any?, result: @escaping FlutterResult) { + guard let args = arguments as? [String: Any] else { + result( + FlutterError( + code: "bad-arguments", message: "Invalid arguments for createDocumentFromJpegData.", details: nil + )) + return + } + + guard let jpegData = args["jpegData"] as? FlutterStandardTypedData else { + result( + FlutterError( + code: "missing-jpeg-data", message: "JPEG data is required.", details: nil + )) + return + } + + guard let width = args["width"] as? Double, width > 0 else { + result( + FlutterError( + code: "invalid-width", message: "Valid width is required.", details: nil + )) + return + } + + guard let height = args["height"] as? Double, height > 0 else { + result( + FlutterError( + code: "invalid-height", message: "Valid height is required.", details: nil + )) + return + } + + // Create an empty PDF document + guard let pdfDocument = PDFDocument() else { + result( + FlutterError( + code: "pdf-document-failure", message: "Failed to create PDFDocument.", details: nil + )) + return + } + + // Create a PDF page with the specified dimensions + let pageRect = CGRect(x: 0, y: 0, width: width, height: height) + guard let pdfPage = PDFPage() else { + result( + FlutterError( + code: "pdf-page-failure", message: "Failed to create PDF page.", details: nil + )) + return + } + pdfPage.setBounds(pageRect, for: .mediaBox) + + // Create image from JPEG data + #if os(iOS) + guard let image = UIImage(data: jpegData.data) else { + result( + FlutterError( + code: "invalid-jpeg", message: "Failed to decode JPEG data.", details: nil + )) + return + } + guard let cgImage = image.cgImage else { + result( + FlutterError( + code: "invalid-image", message: "Failed to get CGImage from JPEG.", details: nil + )) + return + } + #elseif os(macOS) + guard let image = NSImage(data: jpegData.data) else { + result( + FlutterError( + code: "invalid-jpeg", message: "Failed to decode JPEG data.", details: nil + )) + return + } + guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { + result( + FlutterError( + code: "invalid-image", message: "Failed to get CGImage from JPEG.", details: nil + )) + return + } + #endif + + // Draw the image on the page using CoreGraphics + let context = CGContext( + data: nil, + width: Int(width), + height: Int(height), + bitsPerComponent: 8, + bytesPerRow: 0, + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + ) + + guard let ctx = context else { + result( + FlutterError( + code: "context-failure", message: "Failed to create graphics context.", details: nil + )) + return + } + + // Create a mutable data to hold the PDF content + let pdfData = NSMutableData() + guard let pdfConsumer = CGDataConsumer(data: pdfData) else { + result( + FlutterError( + code: "consumer-failure", message: "Failed to create PDF data consumer.", details: nil + )) + return + } + + // Create a PDF context to generate the page + var mediaBox = pageRect + guard let pdfContext = CGContext(consumer: pdfConsumer, mediaBox: &mediaBox, nil) else { + result( + FlutterError( + code: "pdf-context-failure", message: "Failed to create PDF context.", details: nil + )) + return + } + + pdfContext.beginPDFPage(nil) + pdfContext.draw(cgImage, in: pageRect) + pdfContext.endPDFPage() + pdfContext.closePDF() + + // Create PDFDocument from generated data + guard let resultDocument = PDFDocument(data: pdfData as Data) else { + result( + FlutterError( + code: "pdf-creation-failure", message: "Failed to create PDF document from image.", details: nil + )) + return + } + + // Register the document + let handle = nextHandle + nextHandle += 1 + documents[handle] = resultDocument + + // Build page info + var pageInfos: [[String: Any]] = [] + if let page = resultDocument.page(at: 0) { + let bounds = page.bounds(for: .mediaBox) + pageInfos.append([ + "width": Double(bounds.width), + "height": Double(bounds.height), + "rotation": page.rotation + ]) + } + + result([ + "handle": handle, + "isEncrypted": false, + "pages": pageInfos + ]) + } + + /// Renders the requested page using a CoreGraphics bitmap context and returns ARGB pixels. + private func renderPage(arguments: Any?, result: @escaping FlutterResult) { + guard + let args = arguments as? [String: Any], + let handleValue = args["handle"], + let pageIndex = args["pageIndex"] as? Int, + let width = args["width"] as? Int, + let height = args["height"] as? Int, + let fullWidth = args["fullWidth"] as? Int, + let fullHeight = args["fullHeight"] as? Int, + let rotation = args["rotation"] as? Int + else { + result( + FlutterError( + code: "bad-arguments", message: "Invalid arguments for renderPage.", details: nil + )) + return + } + let handle = (handleValue as? Int64) ?? Int64((handleValue as? Int) ?? -1) + guard handle >= 0, let document = documents[handle], let page = document.page(at: pageIndex) else { + result( + FlutterError( + code: "unknown-document", message: "Document not found for handle \(handle).", + details: nil + )) + return + } + + let x = args["x"] as? Int ?? 0 + let y = args["y"] as? Int ?? 0 + let backgroundColor = args["backgroundColor"] as? Int ?? 0xFFFF_FFFF + let renderAnnotations = args["renderAnnotations"] as? Bool ?? true + + guard width > 0, height > 0, fullWidth > 0, fullHeight > 0 else { + result( + FlutterError(code: "invalid-size", message: "Invalid render dimensions.", details: nil)) + return + } + + let bytesPerPixel = 4 + let bytesPerRow = width * bytesPerPixel + let dataSize = bytesPerRow * height + + guard + let context = CGContext( + data: nil, + width: width, + height: height, + bitsPerComponent: 8, + bytesPerRow: bytesPerRow, + space: CGColorSpaceCreateDeviceRGB(), + bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue + | CGBitmapInfo.byteOrder32Little.rawValue + ) + else { + result( + FlutterError( + code: "context-failure", message: "Failed to create bitmap context.", details: nil + )) + return + } + + context.setBlendMode(.normal) + context.interpolationQuality = .high + let components = colorComponents(from: backgroundColor) + context.setFillColor( + red: components.red, green: components.green, blue: components.blue, alpha: components.alpha + ) + context.fill(CGRect(x: 0, y: 0, width: width, height: height)) + + let bounds = page.bounds(for: .mediaBox) + let scaleX = CGFloat(fullWidth) / bounds.width + let scaleY = CGFloat(fullHeight) / bounds.height + let pdfX = CGFloat(x) + let pdfBottom = CGFloat(fullHeight - (y + height)) + + // FIXME: We should handle page rotation here properly + + context.translateBy(x: -pdfX, y: -pdfBottom) + context.scaleBy(x: scaleX, y: scaleY) + + let originalDisplaysAnnotations = page.displaysAnnotations + page.displaysAnnotations = renderAnnotations + page.draw(with: .mediaBox, to: context) + page.displaysAnnotations = originalDisplaysAnnotations + + guard let contextData = context.data else { + result( + FlutterError( + code: "render-failure", message: "Failed to access rendered bitmap.", details: nil + )) + return + } + + let buffer = Data(bytes: contextData, count: dataSize) + result([ + "width": width, + "height": height, + "pixels": FlutterStandardTypedData(bytes: buffer) + ]) + } + + /// Builds the document outline using CoreGraphics for accurate zoom values. + private func loadOutline(arguments: Any?, result: @escaping FlutterResult) { + guard + let args = arguments as? [String: Any], + let handleValue = args["handle"] + else { + result( + FlutterError( + code: "bad-arguments", message: "Invalid arguments for loadOutline.", details: nil + )) + return + } + let handle = (handleValue as? Int64) ?? Int64((handleValue as? Int) ?? -1) + guard handle >= 0, let document = documents[handle] else { + result( + FlutterError( + code: "unknown-document", message: "Document not found for handle \(handle).", + details: nil + )) + return + } + + guard let cgDocument = document.documentRef else { + result([]) + return + } + + guard let catalog = cgDocument.catalog else { + result([]) + return + } + + var outlinesDict: CGPDFDictionaryRef? + guard CGPDFDictionaryGetDictionary(catalog, "Outlines", &outlinesDict), + let outlines = outlinesDict + else { + result([]) + return + } + + var firstOutline: CGPDFDictionaryRef? + guard CGPDFDictionaryGetDictionary(outlines, "First", &firstOutline), + let first = firstOutline + else { + result([]) + return + } + + result(parseCGOutlineNodes(first, document: document)) + } + + /// Loads page links using CoreGraphics PDF parsing and optional text-based auto-detection. + /// Duplicate links are filtered via a stable hash. + private func loadPageLinks(arguments: Any?, result: @escaping FlutterResult) { + guard + let args = arguments as? [String: Any], + let handleValue = args["handle"], + let pageIndex = args["pageIndex"] as? Int + else { + result( + FlutterError( + code: "bad-arguments", message: "Invalid arguments for loadPageLinks.", details: nil + )) + return + } + let handle = (handleValue as? Int64) ?? Int64((handleValue as? Int) ?? -1) + guard handle >= 0, let document = documents[handle], + let page = document.page(at: pageIndex) + else { + result( + FlutterError( + code: "unknown-document", message: "Document not found for handle \(handle).", + details: nil + )) + return + } + + var links: [[String: Any]] = [] + var occupiedRects: [CGRect] = [] + var seenKeys = Set() + + // Parse annotations using CoreGraphics for reliable, spec-compliant extraction + if let cgDocument = document.documentRef { + let (cgLinks, cgRects) = cgAnnotationLinks( + cgDocument: cgDocument, + pageIndex: pageIndex, + document: document + ) + for link in cgLinks { + let key = linkKey(link) + if seenKeys.insert(key).inserted { + links.append(link) + } + } + occupiedRects.append(contentsOf: cgRects) + } + + // Optionally detect links from text content (URLs in plain text) + let enableAutoLinkDetection = args["enableAutoLinkDetection"] as? Bool ?? true + if enableAutoLinkDetection { + for link in autodetectedLinks(on: page, excluding: occupiedRects) { + let key = linkKey(link) + if seenKeys.insert(key).inserted { + links.append(link) + } + } + } + result(links) + } + + private func autodetectedLinks(on page: PDFPage, excluding occupiedRects: [CGRect]) -> [[String: Any]] { + guard let text = page.string, !text.isEmpty else { return [] } + guard let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else { + return [] + } + + var links: [[String: Any]] = [] + var occupied = occupiedRects + let fullRange = NSRange(location: 0, length: (text as NSString).length) + let matches = detector.matches(in: text, options: [], range: fullRange) + + for match in matches { + guard let url = match.url else { continue } + guard let selection = page.selection(for: match.range) else { continue } + let selectionsByLine = selection.selectionsByLine() + let lineSelections = selectionsByLine.isEmpty ? [selection] : selectionsByLine + var rectDictionaries: [[String: Double]] = [] + var rectsForMatch: [CGRect] = [] + for lineSelection in lineSelections { + let bounds = lineSelection.bounds(for: page) + if bounds.isNull || bounds.isEmpty { + continue + } + if intersects(bounds, with: occupied) { + rectDictionaries.removeAll() + break + } + rectDictionaries.append(rectDictionary(bounds)) + rectsForMatch.append(bounds) + } + guard !rectDictionaries.isEmpty else { continue } + links.append([ + "rects": rectDictionaries, + "url": url.absoluteString + ]) + occupied.append(contentsOf: rectsForMatch) + } + return links + } + + /// Extracts raw page text along with bounding boxes for each character so the engine can run its + /// own text-selection heuristics. + private func loadPageText(arguments: Any?, result: @escaping FlutterResult) { + guard + let args = arguments as? [String: Any], + let handleValue = args["handle"], + let pageIndex = args["pageIndex"] as? Int + else { + result( + FlutterError( + code: "bad-arguments", message: "Invalid arguments for loadPageText.", details: nil + )) + return + } + let handle = (handleValue as? Int64) ?? Int64((handleValue as? Int) ?? -1) + guard handle >= 0, let document = documents[handle], let page = document.page(at: pageIndex) else { + result( + FlutterError( + code: "unknown-document", message: "Document not found for handle \(handle).", + details: nil + )) + return + } + + guard let fullText = page.string else { + result(["text": "", "rects": []]) + return + } + + let nsText = fullText as NSString + let length = nsText.length + if length <= 0 { + result(["text": "", "rects": []]) + return + } + + let mediaBounds = page.bounds(for: .mediaBox) + let offsetX = mediaBounds.minX + let offsetY = mediaBounds.minY + let zeroRect: [String: Double] = [ + "left": 0.0, + "top": 0.0, + "right": 0.0, + "bottom": 0.0 + ] + + var rects: [[String: Double]] = [] + rects.reserveCapacity(length) + var boundsIndex = 0 + for charIndex in 0 ..< length { + let charCode = nsText.character(at: charIndex) + if let scalar = UnicodeScalar(charCode), CharacterSet.newlines.contains(scalar) { + rects.append(zeroRect) + continue + } + + let bounds = page.characterBounds(at: boundsIndex) + boundsIndex += 1 + + if bounds.isNull { + rects.append(zeroRect) + continue + } + rects.append( + [ + "left": bounds.minX - offsetX, + "top": bounds.maxY - offsetY, + "right": bounds.maxX - offsetX, + "bottom": bounds.minY - offsetY + ] + ) + } + + result([ + "text": fullText, + "rects": rects + ]) + } + + /// Parses outline nodes using CoreGraphics for accurate destination data including zoom values. + private func parseCGOutlineNodes(_ outlineDict: CGPDFDictionaryRef, document: PDFDocument) -> [[String: Any]] { + var result: [[String: Any]] = [] + var currentDict: CGPDFDictionaryRef? = outlineDict + + while let current = currentDict { + if let node = parseCGOutlineNode(current, document: document) { + result.append(node) + } + + var nextDict: CGPDFDictionaryRef? + if CGPDFDictionaryGetDictionary(current, "Next", &nextDict), let next = nextDict { + currentDict = next + } + else { + break + } + } + + return result + } + + /// Parses a single outline node from CoreGraphics dictionary. + private func parseCGOutlineNode(_ outlineDict: CGPDFDictionaryRef, document: PDFDocument) -> [String: Any]? { + // Extract title + var titleString: CGPDFStringRef? + let title: String + if CGPDFDictionaryGetString(outlineDict, "Title", &titleString), let titleStr = titleString { + if let cfString = CGPDFStringCopyTextString(titleStr) { + title = cfString as String + } + else { + title = "" + } + } + else { + title = "" + } + + var node: [String: Any] = ["title": title] + + // Extract destination using CoreGraphics to get accurate zoom values + if let dest = parseCGDestination(outlineDict, document: document) { + node["dest"] = dest + } + + // Extract children (First child) + var firstChild: CGPDFDictionaryRef? + if CGPDFDictionaryGetDictionary(outlineDict, "First", &firstChild), let first = firstChild { + node["children"] = parseCGOutlineNodes(first, document: document) + } + else { + node["children"] = [] + } + + return node + } + + private func parseCGDestination(_ dict: CGPDFDictionaryRef, document: PDFDocument) -> [String: Any]? { + // Check for "Dest" key (explicit destination) + var destObject: CGPDFObjectRef? + if CGPDFDictionaryGetObject(dict, "Dest", &destObject), let dest = destObject { + return parseCGDestinationObject(dest, document: document) + } + + // Check for "A" key (action dictionary) + var actionDict: CGPDFDictionaryRef? + if CGPDFDictionaryGetDictionary(dict, "A", &actionDict), let action = actionDict { + if let actionType = actionType(from: action) { + let normalizedType = actionType.lowercased() + + // Handle GoTo action (internal navigation) + if normalizedType == "goto" { + if let dest = parseCGDestinationFromAction(action, document: document) { + return dest + } + } + + // Handle GoToR action (remote goto) - extract destination from current document + // Note: Remote file reference is ignored as we only handle current document + else if normalizedType == "gotor" { + if let dest = parseCGDestinationFromAction(action, document: document) { + return dest + } + } + + // Handle GoToE action (embedded goto) + else if normalizedType == "gotoe" { + if let dest = parseCGDestinationFromAction(action, document: document) { + return dest + } + } + + // Thread actions are not supported for destination extraction + // URI, Launch, Named, and other actions don't have destinations + } + } + + return nil + } + + private func actionType(from action: CGPDFDictionaryRef) -> String? { + var actionTypeString: CGPDFStringRef? + if CGPDFDictionaryGetString(action, "S", &actionTypeString), let typeStr = actionTypeString { + if let cfString = CGPDFStringCopyTextString(typeStr) { + return (cfString as String).trimmingCharacters(in: CharacterSet(charactersIn: "/")) + } + } + var actionTypeName: UnsafePointer? + if CGPDFDictionaryGetName(action, "S", &actionTypeName), let name = actionTypeName { + return String(cString: name).trimmingCharacters(in: CharacterSet(charactersIn: "/")) + } + return nil + } + + private func parseCGDestinationFromAction(_ action: CGPDFDictionaryRef, document: PDFDocument) -> [String: Any]? { + var destObject: CGPDFObjectRef? + if CGPDFDictionaryGetObject(action, "D", &destObject), let dest = destObject { + return parseCGDestinationObject(dest, document: document) + } + return nil + } + + private func parseCGDestinationObject(_ destObject: CGPDFObjectRef, document: PDFDocument) -> [String: Any]? { + // Try array-type destination first (explicit destination) + var destArray: CGPDFArrayRef? + if CGPDFObjectGetValue(destObject, .array, &destArray), let array = destArray { + return parseDestinationArray(array, document: document) + } + + // Try dictionary-type destination (indirect destination reference with /D key) + var destDict: CGPDFDictionaryRef? + if CGPDFObjectGetValue(destObject, .dictionary, &destDict), let dict = destDict { + // The dictionary should contain a /D key with the actual destination array + var actualDestObject: CGPDFObjectRef? + if CGPDFDictionaryGetObject(dict, "D", &actualDestObject), let actualDest = actualDestObject { + return parseCGDestinationObject(actualDest, document: document) + } + } + + // Try string-type destination (named destination) + var destString: CGPDFStringRef? + if CGPDFObjectGetValue(destObject, .string, &destString), let string = destString { + if let cfString = CGPDFStringCopyTextString(string) { + let destName = cfString as String + let result = lookupNamedDestination(destName, document: document) + #if DEBUG + if result == nil { + print("Warning: Named destination '\(destName)' not found in document") + } + #endif + return result + } + } + + // Try name-type destination (named destination) + var destName: UnsafePointer? + if CGPDFObjectGetValue(destObject, .name, &destName), let name = destName { + let destNameStr = String(cString: name).trimmingCharacters(in: CharacterSet(charactersIn: "/")) + let result = lookupNamedDestination(destNameStr, document: document) + #if DEBUG + if result == nil { + print("Warning: Named destination '\(destNameStr)' not found in document") + } + #endif + return result + } + + #if DEBUG + print("Warning: Unable to parse destination object - unrecognized type") + #endif + return nil + } + + private func parseDestinationArray(_ array: CGPDFArrayRef, document: PDFDocument) -> [String: Any]? { + let count = CGPDFArrayGetCount(array) + guard count >= 1 else { return nil } + + // First element should be a page reference (dictionary or integer) + var pageIndex: Int? + + // Try to get the page reference as a dictionary (most common case - indirect reference to page) + var pageRef: CGPDFDictionaryRef? + if CGPDFArrayGetDictionary(array, 0, &pageRef), let pageDict = pageRef { + // Use improved page index finding with object number comparison + pageIndex = findPageIndexByObjectNumber(pageDict, document: document) + } + + // Fallback: try direct integer (rare, but some PDFs use 0-based page numbers directly) + if pageIndex == nil { + var rawIndex: CGPDFInteger = 0 + if CGPDFArrayGetInteger(array, 0, &rawIndex) { + // Treat as 0-based page index + let idx = Int(rawIndex) + if idx >= 0, idx < document.pageCount { + pageIndex = idx + } + } + } + + guard let pageIndex, pageIndex >= 0, pageIndex < document.pageCount else { + #if DEBUG + print("Warning: Failed to resolve page reference in destination array") + #endif + return nil + } + + // Extract command type (XYZ, Fit, FitH, etc.) + var commandRaw = "xyz" + if count >= 2 { + var commandName: UnsafePointer? + if CGPDFArrayGetName(array, 1, &commandName), let name = commandName { + commandRaw = String(cString: name) + } + else { + var commandString: CGPDFStringRef? + if CGPDFArrayGetString(array, 1, &commandString), let str = commandString, + let cfString = CGPDFStringCopyTextString(str) + { + commandRaw = cfString as String + } + } + } + let command = commandRaw + .trimmingCharacters(in: CharacterSet(charactersIn: "/")) + .lowercased() + + // Extract parameters (x, y, zoom) + var params: [Any] = [] + for i in 2 ..< count { + var object: CGPDFObjectRef? + guard CGPDFArrayGetObject(array, i, &object), let obj = object else { + params.append(NSNull()) + continue + } + switch CGPDFObjectGetType(obj) { + case .null: + params.append(NSNull()) + case .integer: + var intValue: CGPDFInteger = 0 + params.append( + CGPDFObjectGetValue(obj, .integer, &intValue) ? Double(intValue) : NSNull() + ) + case .real: + var realValue: CGPDFReal = 0 + params.append( + CGPDFObjectGetValue(obj, .real, &realValue) ? Double(realValue) : NSNull() + ) + default: + params.append(NSNull()) + } + } + + return [ + "page": pageIndex + 1, // Convert to 1-based + "command": command, + "params": params + ] + } + + private func lookupNamedDestination(_ name: String, document: PDFDocument) -> [String: Any]? { + guard let cgDocument = document.documentRef else { + return nil + } + + guard let catalog = cgDocument.catalog else { + return nil + } + + // Try to get Dests dictionary (old-style named destinations) + var destsDict: CGPDFDictionaryRef? + if CGPDFDictionaryGetDictionary(catalog, "Dests", &destsDict), let dests = destsDict { + var destObject: CGPDFObjectRef? + let found = name.withCString { cName -> Bool in + CGPDFDictionaryGetObject(dests, cName, &destObject) + } + if found, let dest = destObject { + return parseCGDestinationObject(dest, document: document) + } + } + + // Try to get Names dictionary (new-style named destinations) + var namesDict: CGPDFDictionaryRef? + if CGPDFDictionaryGetDictionary(catalog, "Names", &namesDict), let names = namesDict { + var destsNameTreeDict: CGPDFDictionaryRef? + if CGPDFDictionaryGetDictionary(names, "Dests", &destsNameTreeDict), let destsTree = destsNameTreeDict { + if let dest = lookupInNameTree(name, nameTree: destsTree) { + return parseCGDestinationObject(dest, document: document) + } + } + } + + return nil + } + + private func lookupInNameTree(_ name: String, nameTree: CGPDFDictionaryRef) -> CGPDFObjectRef? { + // Check if this node has a Names array (leaf node) + var namesArray: CGPDFArrayRef? + if CGPDFDictionaryGetArray(nameTree, "Names", &namesArray), let names = namesArray { + let count = CGPDFArrayGetCount(names) + // Names array contains pairs: [name1, value1, name2, value2, ...] + var i: size_t = 0 + while i + 1 < count { + var nameString: CGPDFStringRef? + if CGPDFArrayGetString(names, i, &nameString), let str = nameString { + // Try CGPDFStringCopyTextString first (for text strings with BOM) + if let cfString = CGPDFStringCopyTextString(str), (cfString as String) == name { + var valueObj: CGPDFObjectRef? + if CGPDFArrayGetObject(names, i + 1, &valueObj) { + return valueObj + } + } + // Also try raw byte string comparison with multiple encodings + else if let bytePtr = CGPDFStringGetBytePtr(str) { + let length = CGPDFStringGetLength(str) + // Try UTF-8 + if let byteString = String( + bytesNoCopy: UnsafeMutableRawPointer(mutating: bytePtr), + length: length, + encoding: .utf8, + freeWhenDone: false + ), + byteString == name + { + var valueObj: CGPDFObjectRef? + if CGPDFArrayGetObject(names, i + 1, &valueObj) { + return valueObj + } + } + // Try ISO-Latin-1 as fallback (common in older PDFs) + else if let latin1String = String( + bytesNoCopy: UnsafeMutableRawPointer(mutating: bytePtr), + length: length, + encoding: .isoLatin1, + freeWhenDone: false + ), + latin1String == name + { + var valueObj: CGPDFObjectRef? + if CGPDFArrayGetObject(names, i + 1, &valueObj) { + return valueObj + } + } + } + } + i += 2 + } + } + + // Check if this node has Kids array (intermediate node) + var kidsArray: CGPDFArrayRef? + if CGPDFDictionaryGetArray(nameTree, "Kids", &kidsArray), let kids = kidsArray { + let count = CGPDFArrayGetCount(kids) + + // Use Limits to optimize search if available + for i in 0 ..< count { + var kidDict: CGPDFDictionaryRef? + if CGPDFArrayGetDictionary(kids, i, &kidDict), let kid = kidDict { + // Check if name falls within this kid's limits + if nameInLimits(name, limits: kid) { + if let result = lookupInNameTree(name, nameTree: kid) { + return result + } + } + } + } + } + + return nil + } + + /// Checks if a name falls within the Limits of a name tree node + private func nameInLimits(_ name: String, limits dict: CGPDFDictionaryRef) -> Bool { + var limitsArray: CGPDFArrayRef? + guard CGPDFDictionaryGetArray(dict, "Limits", &limitsArray), let limits = limitsArray else { + // No limits means we should search this node + return true + } + + guard CGPDFArrayGetCount(limits) >= 2 else { + return true + } + + // Extract lower limit - try text strings and byte strings with multiple encodings + var lowerString: CGPDFStringRef? + if CGPDFArrayGetString(limits, 0, &lowerString), let lower = lowerString { + var lowerName: String? + // Try text string first (UTF-16 with BOM) + if let cfString = CGPDFStringCopyTextString(lower) as String? { + lowerName = cfString + } + // Try raw byte strings with multiple encodings + else if let bytePtr = CGPDFStringGetBytePtr(lower) { + let length = CGPDFStringGetLength(lower) + // Try UTF-8 + if let utf8String = String( + bytesNoCopy: UnsafeMutableRawPointer(mutating: bytePtr), + length: length, + encoding: .utf8, + freeWhenDone: false + ) { + lowerName = utf8String + } + // Try ISO-Latin-1 as fallback + else if let latin1String = String( + bytesNoCopy: UnsafeMutableRawPointer(mutating: bytePtr), + length: length, + encoding: .isoLatin1, + freeWhenDone: false + ) { + lowerName = latin1String + } + } + + if let lowerName, name < lowerName { + return false + } + } + + // Extract upper limit - try text strings and byte strings with multiple encodings + var upperString: CGPDFStringRef? + if CGPDFArrayGetString(limits, 1, &upperString), let upper = upperString { + var upperName: String? + // Try text string first (UTF-16 with BOM) + if let cfString = CGPDFStringCopyTextString(upper) as String? { + upperName = cfString + } + // Try raw byte strings with multiple encodings + else if let bytePtr = CGPDFStringGetBytePtr(upper) { + let length = CGPDFStringGetLength(upper) + // Try UTF-8 + if let utf8String = String( + bytesNoCopy: UnsafeMutableRawPointer(mutating: bytePtr), + length: length, + encoding: .utf8, + freeWhenDone: false + ) { + upperName = utf8String + } + // Try ISO-Latin-1 as fallback + else if let latin1String = String( + bytesNoCopy: UnsafeMutableRawPointer(mutating: bytePtr), + length: length, + encoding: .isoLatin1, + freeWhenDone: false + ) { + upperName = latin1String + } + } + + if let upperName, name > upperName { + return false + } + } + + return true + } + + /// Finds page index by comparing object identifiers in the PDF structure. + /// This is more robust than pointer comparison for indirect references. + private func findPageIndexByObjectNumber(_ pageDict: CGPDFDictionaryRef, document: PDFDocument) -> Int? { + guard let cgDocument = document.documentRef else { + return nil + } + + // First try: Direct pointer comparison (works for direct references) + for i in 0 ..< document.pageCount { + guard let cgPage = cgDocument.page(at: i + 1) else { continue } + guard let currentPageDict = cgPage.dictionary else { continue } + + if currentPageDict == pageDict { + return i + } + } + + // Second try: Compare by extracting unique page properties + // This works when indirect references point to the same logical page + // We create a fingerprint based on MediaBox, CropBox, Rotate, and Resources reference + let targetFingerprint = createPageFingerprint(pageDict) + + for i in 0 ..< document.pageCount { + guard let cgPage = cgDocument.page(at: i + 1) else { continue } + guard let currentPageDict = cgPage.dictionary else { continue } + + let currentFingerprint = createPageFingerprint(currentPageDict) + if targetFingerprint == currentFingerprint { + return i + } + } + + #if DEBUG + print("Warning: Could not match page dictionary to any page in document") + #endif + return nil + } + + /// Creates a fingerprint for a page dictionary based on its key properties + private func createPageFingerprint(_ pageDict: CGPDFDictionaryRef) -> String { + var components: [String] = [] + + // MediaBox + var mediaBox: CGPDFArrayRef? + if CGPDFDictionaryGetArray(pageDict, "MediaBox", &mediaBox), let mb = mediaBox { + components.append("MB:\(arrayToString(mb))") + } + + // CropBox (if present) + var cropBox: CGPDFArrayRef? + if CGPDFDictionaryGetArray(pageDict, "CropBox", &cropBox), let cb = cropBox { + components.append("CB:\(arrayToString(cb))") + } + + // Rotate + var rotate: CGPDFInteger = 0 + if CGPDFDictionaryGetInteger(pageDict, "Rotate", &rotate) { + components.append("R:\(rotate)") + } + + // Contents reference (if available) - helps distinguish pages + var contents: CGPDFObjectRef? + if CGPDFDictionaryGetObject(pageDict, "Contents", &contents) { + components.append("C:exists") + } + + return components.joined(separator: "|") + } + + /// Converts a CGPDFArray to a string representation + private func arrayToString(_ array: CGPDFArrayRef) -> String { + let count = CGPDFArrayGetCount(array) + var values: [String] = [] + + for i in 0 ..< count { + var num: CGPDFReal = 0 + if CGPDFArrayGetNumber(array, i, &num) { + values.append(String(format: "%.2f", num)) + } + } + + return values.joined(separator: ",") + } + + private func cgAnnotationLinks( + cgDocument: CGPDFDocument, + pageIndex: Int, + document: PDFDocument + ) -> ([[String: Any]], [CGRect]) { + var links: [[String: Any]] = [] + var rects: [CGRect] = [] + + guard let cgPage = cgDocument.page(at: pageIndex + 1) else { + return (links, rects) + } + + guard let pageDict = cgPage.dictionary else { + return (links, rects) + } + + // Get the Annots array + var annotsArray: CGPDFArrayRef? + guard CGPDFDictionaryGetArray(pageDict, "Annots", &annotsArray), let annots = annotsArray else { + return (links, rects) + } + + let count = CGPDFArrayGetCount(annots) + for i in 0 ..< count { + var annotObj: CGPDFObjectRef? + guard CGPDFArrayGetObject(annots, i, &annotObj), let obj = annotObj else { + continue + } + + var annotDict: CGPDFDictionaryRef? + if CGPDFObjectGetValue(obj, .dictionary, &annotDict), let annot = annotDict { + if let link = parseCGAnnotation(annot, document: document) { + links.append(link) + if let linkRects = link["rects"] as? [[String: Double]] { + for rectDict in linkRects { + if let rect = cgRectFromDict(rectDict) { + rects.append(rect) + } + } + } + } + } + } + + return (links, rects) + } + + private func parseCGAnnotation(_ annotDict: CGPDFDictionaryRef, document: PDFDocument) -> [String: Any]? { + // Check if this is a link annotation + var subtypeString: CGPDFStringRef? + var subtypeName: UnsafePointer? + var isLink = false + + if CGPDFDictionaryGetString(annotDict, "Subtype", &subtypeString), let subtype = subtypeString { + if let cfString = CGPDFStringCopyTextString(subtype) { + let subtypeStr = (cfString as String).trimmingCharacters(in: CharacterSet(charactersIn: "/")).lowercased() + isLink = subtypeStr == "link" + } + } + else if CGPDFDictionaryGetName(annotDict, "Subtype", &subtypeName), let name = subtypeName { + let subtypeStr = String(cString: name).trimmingCharacters(in: CharacterSet(charactersIn: "/")).lowercased() + isLink = subtypeStr == "link" + } + + guard isLink else { return nil } + + // Extract rectangle + var rectArray: CGPDFArrayRef? + var annotRects: [[String: Double]] = [] + if CGPDFDictionaryGetArray(annotDict, "Rect", &rectArray), let rect = rectArray { + if let rectDict = parseCGRect(rect) { + annotRects.append(rectDict) + } + } + + // Try to get QuadPoints for more accurate rectangles + var quadPointsArray: CGPDFArrayRef? + if CGPDFDictionaryGetArray(annotDict, "QuadPoints", &quadPointsArray), let quadPoints = quadPointsArray { + let quadRects = parseCGQuadPoints(quadPoints) + if !quadRects.isEmpty { + annotRects = quadRects + } + } + + guard !annotRects.isEmpty else { return nil } + + var linkEntry: [String: Any] = ["rects": annotRects] + + var annotation: [String: String] = [:] + var hasAnnotation = false + + // Extract URL from action + var actionDict: CGPDFDictionaryRef? + if CGPDFDictionaryGetDictionary(annotDict, "A", &actionDict), let action = actionDict { + if let actionType = actionType(from: action)?.lowercased() { + switch actionType { + case "uri": + var uriString: CGPDFStringRef? + if CGPDFDictionaryGetString(action, "URI", &uriString), let uri = uriString, + let cfString = CGPDFStringCopyTextString(uri) + { + linkEntry["url"] = cfString as String + } + case "goto": + if let dest = parseCGDestinationFromAction(action, document: document) { + linkEntry["dest"] = dest + } + default: + break + } + } + } + + // Extract destination directly from annotation (without action) + if linkEntry["dest"] == nil, linkEntry["url"] == nil { + if let dest = parseCGDestination(annotDict, document: document) { + linkEntry["dest"] = dest + } + } + + // Author (T field) + var titleString: CGPDFStringRef? + if CGPDFDictionaryGetString(annotDict, "T", &titleString), let title = titleString { + if let cfString = CGPDFStringCopyTextString(title) { + annotation["title"] = cfString as String + hasAnnotation = true + } + } + + // Contents + var contentsString: CGPDFStringRef? + if CGPDFDictionaryGetString(annotDict, "Contents", &contentsString), let contents = contentsString { + if let cfString = CGPDFStringCopyTextString(contents) { + annotation["content"] = cfString as String + hasAnnotation = true + } + } + + // Subject + var subjString: CGPDFStringRef? + if CGPDFDictionaryGetString(annotDict, "Subj", &subjString), let subj = subjString { + if let cfString = CGPDFStringCopyTextString(subj) { + annotation["subject"] = cfString as String + hasAnnotation = true + } + } + + // Modification date (M) + var modDateString: CGPDFStringRef? + if CGPDFDictionaryGetString(annotDict, "M", &modDateString), let modDate = modDateString { + if let cfString = CGPDFStringCopyTextString(modDate) { + annotation["modificationDate"] = cfString as String + hasAnnotation = true + } + } + + // Creation date + var createDateString: CGPDFStringRef? + if CGPDFDictionaryGetString(annotDict, "CreationDate", &createDateString), let createDate = createDateString { + if let cfString = CGPDFStringCopyTextString(createDate) { + annotation["creationDate"] = cfString as String + hasAnnotation = true + } + } + + if hasAnnotation { + linkEntry["annotation"] = annotation + } + + // Only return if we have a URL, destination, or annotation + guard linkEntry["url"] != nil || linkEntry["dest"] != nil || linkEntry["annotation"] != nil else { + return nil + } + + return linkEntry +} + + private func parseCGRect(_ rectArray: CGPDFArrayRef) -> [String: Double]? { + let count = CGPDFArrayGetCount(rectArray) + guard count >= 4 else { return nil } + + var values: [CGFloat] = [] + for i in 0 ..< 4 { + var num: CGPDFReal = 0 + var intNum: CGPDFInteger = 0 + if CGPDFArrayGetNumber(rectArray, i, &num) { + values.append(CGFloat(num)) + } + else if CGPDFArrayGetInteger(rectArray, i, &intNum) { + values.append(CGFloat(intNum)) + } + else { + return nil + } + } + + return [ + "left": Double(values[0]), + "bottom": Double(values[1]), + "right": Double(values[2]), + "top": Double(values[3]) + ] + } + + private func parseCGQuadPoints(_ quadPointsArray: CGPDFArrayRef) -> [[String: Double]] { + let count = CGPDFArrayGetCount(quadPointsArray) + guard count >= 8, count % 8 == 0 else { return [] } + + var rects: [[String: Double]] = [] + + for i in stride(from: 0, to: count, by: 8) { + var points: [CGFloat] = [] + for j in 0 ..< 8 { + var num: CGPDFReal = 0 + var intNum: CGPDFInteger = 0 + if CGPDFArrayGetNumber(quadPointsArray, i + j, &num) { + points.append(CGFloat(num)) + } + else if CGPDFArrayGetInteger(quadPointsArray, i + j, &intNum) { + points.append(CGFloat(intNum)) + } + else { + break + } + } + + if points.count == 8 { + // QuadPoints are in order: (x1,y1), (x2,y2), (x3,y3), (x4,y4) + // Usually represents corners of a quadrilateral + let minX = min(points[0], points[2], points[4], points[6]) + let maxX = max(points[0], points[2], points[4], points[6]) + let minY = min(points[1], points[3], points[5], points[7]) + let maxY = max(points[1], points[3], points[5], points[7]) + + rects.append([ + "left": Double(minX), + "bottom": Double(minY), + "right": Double(maxX), + "top": Double(maxY) + ]) + } + } + + return rects + } + + private func cgRectFromDict(_ dict: [String: Double]) -> CGRect? { + guard let left = dict["left"], + let bottom = dict["bottom"], + let right = dict["right"], + let top = dict["top"] + else { + return nil + } + + return CGRect( + x: left, + y: bottom, + width: right - left, + height: top - bottom + ) + } + + private func intersects(_ rect: CGRect, with others: [CGRect]) -> Bool { + for other in others where rect.intersects(other) { + return true + } + return false + } + + private func rectDictionary(_ rect: CGRect) -> [String: Double] { + let left = Double(rect.minX) + let right = Double(rect.maxX) + let bottom = Double(rect.minY) + let top = Double(rect.maxY) + return [ + "left": left, + "top": top, + "right": right, + "bottom": bottom + ] + } + + private func linkKey(_ link: [String: Any]) -> String { + if let dest = link["dest"] as? [String: Any] { + let page = dest["page"] as? Int ?? -1 + let command = (dest["command"] as? String ?? "").lowercased() + let paramsKey = paramsKeyString(from: dest["params"]) + return "dest:\(page):\(command):\(paramsKey)" + } + if let url = link["url"] as? String { + return "url:\(url.lowercased())" + } + if let rects = link["rects"] as? [[String: Double]] { + let rectKey = rects + .map { rect -> String in + let left = rect["left"] ?? 0 + let top = rect["top"] ?? 0 + let right = rect["right"] ?? 0 + let bottom = rect["bottom"] ?? 0 + return [ + numberKey(left), + numberKey(top), + numberKey(right), + numberKey(bottom) + ].joined(separator: ",") + } + .joined(separator: "|") + return "rect:\(rectKey)" + } + + // Use annotation fields for key generation + if let annotation = link["annotation"] as? [String: String] { + var components: [String] = [] + + if let author = annotation["author"], !author.isEmpty { + components.append("author:\(author)") + } + if let content = annotation["content"], !content.isEmpty { + components.append("content:\(content)") + } + if let subject = annotation["subject"], !subject.isEmpty { + components.append("subject:\(subject)") + } + + if !components.isEmpty { + return components.joined(separator: "|") + } + } + + return UUID().uuidString + } + + private func paramsKeyString(from value: Any?) -> String { + guard let value else { return "" } + if let doubles = value as? [Double] { + return doubles.map(numberKey).joined(separator: ",") + } + if let optionals = value as? [Double?] { + return optionals.map { $0.map(numberKey) ?? "null" }.joined(separator: ",") + } + if let numbers = value as? [NSNumber] { + return numbers.map { numberKey($0.doubleValue) }.joined(separator: ",") + } + if let anys = value as? [Any] { + return anys.map { valueKey($0) }.joined(separator: ",") + } + return valueKey(value) + } + + private func valueKey(_ value: Any?) -> String { + guard let value else { return "null" } + if value is NSNull { + return "null" + } + if let number = value as? NSNumber { + return numberKey(number.doubleValue) + } + if let doubleValue = value as? Double { + return numberKey(doubleValue) + } + if let intValue = value as? Int { + return numberKey(Double(intValue)) + } + return "null" + } + + private func numberKey(_ value: Double) -> String { + return String(format: "%.4f", value) + } + + private func closeDocument(arguments: Any?, result: @escaping FlutterResult) { + guard + let args = arguments as? [String: Any], + let handleValue = args["handle"] + else { + result( + FlutterError( + code: "bad-arguments", message: "Invalid arguments for closeDocument.", details: nil + )) + return + } + let handle = (handleValue as? Int64) ?? Int64((handleValue as? Int) ?? -1) + documents.removeValue(forKey: handle) + result(nil) + } + + private func colorComponents(from argb: Int) -> ( + red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat + ) { + let value = UInt32(bitPattern: Int32(truncatingIfNeeded: argb)) + let alpha = CGFloat((value >> 24) & 0xFF) / 255.0 + let red = CGFloat((value >> 16) & 0xFF) / 255.0 + let green = CGFloat((value >> 8) & 0xFF) / 255.0 + let blue = CGFloat(value & 0xFF) / 255.0 + return (red, green, blue, alpha) + } +} + +private extension FlutterPluginRegistrar { + #if os(iOS) + var pdfrxCoreGraphicsMessenger: FlutterBinaryMessenger { + messenger() + } + + #elseif os(macOS) + var pdfrxCoreGraphicsMessenger: FlutterBinaryMessenger { + messenger + } + #endif +} diff --git a/packages/pdfrx_coregraphics/lib/pdfrx_coregraphics.dart b/packages/pdfrx_coregraphics/lib/pdfrx_coregraphics.dart new file mode 100644 index 00000000..ffe78c3e --- /dev/null +++ b/packages/pdfrx_coregraphics/lib/pdfrx_coregraphics.dart @@ -0,0 +1,838 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/services.dart'; +import 'package:pdfrx_engine/pdfrx_engine.dart'; +// ignore: implementation_imports +import 'package:pdfrx_engine/src/native/pdf_file_cache.dart'; +// ignore: implementation_imports +import 'package:pdfrx_engine/src/pdf_page_proxies.dart'; + +const _kPasswordErrorCode = 'wrong-password'; + +/// CoreGraphics backed implementation of [PdfrxEntryFunctions]. +class PdfrxCoreGraphicsEntryFunctions implements PdfrxEntryFunctions { + static final _channel = MethodChannel('pdfrx_coregraphics'); + static bool _initialized = false; + + PdfrxCoreGraphicsEntryFunctions(); + + @override + Future init() async { + if (_initialized) { + return; + } + try { + await _channel.invokeMethod('initialize'); + } on MissingPluginException { + // Older platform code may not implement an explicit initializer; that's fine. + } + _initialized = true; + } + + @override + Future suspendPdfiumWorkerDuringAction( + FutureOr Function() action, + ) async { + return await Future.sync(action); + } + + @override + Future compute( + FutureOr Function(M message) callback, + M message, + ) async { + throw UnimplementedError( + 'compute() is not implemented for CoreGraphics backend.', + ); + } + + @override + Future stopBackgroundWorker() async { + throw UnimplementedError( + 'stopBackgroundWorker() is not implemented for CoreGraphics backend.', + ); + } + + @override + Future openAsset( + String name, { + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, + }) async { + if (Pdfrx.loadAsset == null) { + throw StateError( + 'Pdfrx.loadAsset is not set. Please set it before calling openAsset.', + ); + } + await init(); + final data = await Pdfrx.loadAsset!(name); + return openData( + data, + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + sourceName: 'asset:$name', + ); + } + + @override + Future openData( + Uint8List data, { + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + String? sourceName, + bool allowDataOwnershipTransfer = false, + bool useProgressiveLoading = false, + void Function()? onDispose, + }) async { + await init(); + sourceName ??= _sourceNameFromData(data); + return _openWithPassword( + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + sourceName: sourceName, + useProgressiveLoading: useProgressiveLoading, + onDispose: onDispose, + opener: (password) async { + final result = await _channel + .invokeMapMethod('openDocument', { + 'sourceType': 'bytes', + 'bytes': data, + 'password': password, + 'sourceName': sourceName, + }); + return result; + }, + ); + } + + static String _sourceNameFromData(Uint8List data) { + final hash = data.fold( + 0, + (value, element) => ((value * 31) ^ element) & 0xFFFFFFFF, + ); + return 'memory:$hash'; + } + + @override + Future openFile( + String filePath, { + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, + }) async { + await init(); + return _openWithPassword( + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + sourceName: 'file:$filePath', + useProgressiveLoading: useProgressiveLoading, + opener: (password) => + _channel.invokeMapMethod('openDocument', { + 'sourceType': 'file', + 'path': filePath, + 'password': password, + 'sourceName': 'file:$filePath', + }), + ); + } + + @override + Future openCustom({ + required FutureOr Function(Uint8List buffer, int position, int size) + read, + required int fileSize, + required String sourceName, + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, + int? maxSizeToCacheOnMemory, + void Function()? onDispose, + }) async { + await init(); + final buffer = Uint8List(fileSize); + final chunk = Uint8List(min(fileSize, 128 * 1024)); + var offset = 0; + while (offset < fileSize) { + final currentSize = min(chunk.length, fileSize - offset); + final readCount = await read(chunk, offset, currentSize); + if (readCount <= 0) { + throw PdfException( + 'Unexpected end of custom stream. Expected $currentSize bytes at offset $offset.', + ); + } + buffer.setRange(offset, offset + readCount, chunk); + offset += readCount; + } + return openData( + buffer, + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + sourceName: sourceName, + onDispose: onDispose, + ); + } + + @override + Future openUri( + Uri uri, { + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, + PdfDownloadProgressCallback? progressCallback, + bool preferRangeAccess = false, + Map? headers, + bool withCredentials = false, + Duration? timeout, + }) => pdfDocumentFromUri( + uri, + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + progressCallback: progressCallback, + useRangeAccess: preferRangeAccess, + headers: headers, + timeout: timeout, + entryFunctions: this, + ); + + @override + Future createNew({required String sourceName}) async { + await init(); + final result = await _channel.invokeMapMethod( + 'createNewDocument', + {'sourceName': sourceName}, + ); + if (result == null) { + throw const PdfException('Failed to create empty PDF document.'); + } + return _CoreGraphicsPdfDocument.fromPlatformMap( + channel: _channel, + result: result, + sourceName: sourceName, + useProgressiveLoading: false, + onDispose: null, + ); + } + + @override + Future createFromJpegData( + Uint8List jpegData, { + required double width, + required double height, + required String sourceName, + }) async { + await init(); + final result = await _channel.invokeMapMethod( + 'createDocumentFromJpegData', + { + 'jpegData': jpegData, + 'width': width, + 'height': height, + 'sourceName': sourceName, + }, + ); + if (result == null) { + throw const PdfException('Failed to create PDF document from JPEG data.'); + } + return _CoreGraphicsPdfDocument.fromPlatformMap( + channel: _channel, + result: result, + sourceName: sourceName, + useProgressiveLoading: false, + onDispose: null, + ); + } + + @override + Future reloadFonts() async { + // CoreGraphics reuses system font registrations; nothing to do. + } + + @override + Future addFontData({ + required String face, + required Uint8List data, + }) async { + // Custom font registration is not currently supported by the CoreGraphics bridge. + } + + @override + Future clearAllFontData() async { + // Custom font registration is not currently supported by the CoreGraphics bridge. + } + + @override + PdfrxBackendType get backendType => PdfrxBackendType.pdfKit; + + Future _openWithPassword({ + required PdfPasswordProvider? passwordProvider, + required bool firstAttemptByEmptyPassword, + required String sourceName, + required bool useProgressiveLoading, + required Future?> Function(String? password) opener, + void Function()? onDispose, + }) async { + for (var attempt = 0; ; attempt++) { + final String? password; + if (firstAttemptByEmptyPassword && attempt == 0) { + password = null; + } else { + password = await passwordProvider?.call(); + if (password == null) { + throw const PdfPasswordException( + 'No password supplied by PasswordProvider.', + ); + } + } + try { + final result = await opener(password); + if (result == null) { + throw const PdfException('Failed to open PDF document.'); + } + return _CoreGraphicsPdfDocument.fromPlatformMap( + channel: _channel, + result: result, + sourceName: sourceName, + useProgressiveLoading: useProgressiveLoading, + onDispose: onDispose, + ); + } on PlatformException catch (e) { + if (e.code == _kPasswordErrorCode) { + // try again with the next password + continue; + } + throw PdfException(e.message ?? 'Platform error: ${e.code}'); + } + } + } +} + +class _CoreGraphicsPdfDocument extends PdfDocument { + _CoreGraphicsPdfDocument._({ + required this.channel, + required this.handle, + required super.sourceName, + required this.isEncrypted, + required this.permissions, + required this.useProgressiveLoading, + this.onDispose, + }) : subject = StreamController.broadcast(); + + factory _CoreGraphicsPdfDocument.fromPlatformMap({ + required MethodChannel channel, + required Map result, + required String sourceName, + required bool useProgressiveLoading, + void Function()? onDispose, + }) { + final map = result.cast(); + final handle = + map['handle'] as int? ?? + (throw const PdfException( + 'Platform response missing document handle.', + )); + final isEncrypted = map['isEncrypted'] as bool? ?? false; + final permissionsValue = map['permissions'] as Map?; + final permissions = permissionsValue == null + ? null + : PdfPermissions( + (permissionsValue['flags'] as int?) ?? 0, + (permissionsValue['revision'] as int?) ?? -1, + ); + final doc = _CoreGraphicsPdfDocument._( + channel: channel, + handle: handle, + sourceName: sourceName, + isEncrypted: isEncrypted, + permissions: permissions, + useProgressiveLoading: useProgressiveLoading, + onDispose: onDispose, + ); + final pageInfos = (map['pages'] as List? ?? const []) + .map((e) => (e as Map).cast()) + .toList(growable: false); + doc._pages = List.unmodifiable([ + for (var i = 0; i < pageInfos.length; i++) + _CoreGraphicsPdfPage( + document: doc, + index: i, + width: (pageInfos[i]['width'] as num?)?.toDouble() ?? 0.0, + height: (pageInfos[i]['height'] as num?)?.toDouble() ?? 0.0, + rotation: _rotationFromDegrees(pageInfos[i]['rotation'] as int? ?? 0), + ), + ]); + // CoreGraphics loads all pages immediately, so notify load complete + if (!useProgressiveLoading) { + doc._notifyDocumentLoadComplete(); + } + return doc; + } + + void _notifyDocumentLoadComplete() { + subject.add(PdfDocumentLoadCompleteEvent(this)); + } + + static PdfPageRotation _rotationFromDegrees(int degrees) { + switch (degrees % 360) { + case 90: + return PdfPageRotation.clockwise90; + case 180: + return PdfPageRotation.clockwise180; + case 270: + return PdfPageRotation.clockwise270; + default: + return PdfPageRotation.none; + } + } + + final MethodChannel channel; + final int handle; + final bool useProgressiveLoading; + final void Function()? onDispose; + final StreamController subject; + bool _disposed = false; + + List _pages = const []; + + @override + final bool isEncrypted; + + @override + final PdfPermissions? permissions; + + @override + Stream get events => subject.stream; + + @override + List get pages => _pages; + + @override + Future dispose() async { + if (_disposed) { + return; + } + _disposed = true; + subject.close(); + onDispose?.call(); + try { + await channel.invokeMethod('closeDocument', {'handle': handle}); + } on MissingPluginException { + // Ignore if the platform does not provide explicit disposal. + } + } + + @override + Future loadPagesProgressively({ + PdfPageLoadingCallback? onPageLoadProgress, + T? data, + Duration loadUnitDuration = const Duration(milliseconds: 250), + }) async { + // CoreGraphics loads all pages immediately; nothing to do. + } + + @override + Future> loadOutline() async { + try { + final result = await channel.invokeListMethod('loadOutline', { + 'handle': handle, + }); + if (result == null) { + return const []; + } + return _parseOutline(result); + } on MissingPluginException { + return const []; + } + } + + List _parseOutline(List nodes) { + return List.unmodifiable( + nodes.map((node) { + final map = (node as Map).cast(); + return PdfOutlineNode( + title: map['title'] as String? ?? '', + dest: _parseDest(map['dest'] as Map?), + children: _parseOutline( + (map['children'] as List?) ?? const [], + ), + ); + }), + ); + } + + @override + bool isIdenticalDocumentHandle(Object? other) { + return other is _CoreGraphicsPdfDocument && other.handle == handle; + } + + @override + set pages(List newPages) { + final pages = []; + final changes = {}; + for (final newPage in newPages) { + if (pages.length < _pages.length) { + final old = _pages[pages.length]; + if (identical(newPage, old)) { + pages.add(newPage); + continue; + } + } + + if (newPage.unwrap<_CoreGraphicsPdfPage>() == null) { + throw ArgumentError( + 'Unsupported PdfPage instances found at [${pages.length}]', + 'newPages', + ); + } + + final newPageNumber = pages.length + 1; + final updated = newPage.withPageNumber(newPageNumber); + pages.add(updated); + + final oldPageIndex = _pages.indexWhere((p) => identical(p, newPage)); + if (oldPageIndex != -1) { + changes[newPageNumber] = PdfPageStatusChange.moved( + page: updated, + oldPageNumber: oldPageIndex + 1, + ); + } else { + changes[newPageNumber] = PdfPageStatusChange.modified(page: updated); + } + } + + _pages = pages; + subject.add(PdfDocumentPageStatusChangedEvent(this, changes: changes)); + } + + @override + Future assemble() async { + throw UnimplementedError( + 'assemble() is not implemented for CoreGraphics backend.', + ); + } + + @override + Future encodePdf({ + bool incremental = false, + bool removeSecurity = false, + }) async { + throw UnimplementedError( + 'encodePdf() is not implemented for CoreGraphics backend.', + ); + } + + @override + Future useNativeDocumentHandle( + FutureOr Function(int nativeDocumentHandle) task, + ) { + throw UnimplementedError( + 'useNativeDocumentHandle() is not implemented for CoreGraphics backend.', + ); + } + + @override + Future reloadPages({List? pageNumbersToReload}) { + throw UnimplementedError( + 'reloadPages() is not implemented for CoreGraphics backend.', + ); + } +} + +class _CoreGraphicsPdfPage extends PdfPage { + _CoreGraphicsPdfPage({ + required _CoreGraphicsPdfDocument document, + required this.index, + required double width, + required double height, + required PdfPageRotation rotation, + }) : _document = document, + _width = width, + _height = height, + _rotation = rotation; + + final _CoreGraphicsPdfDocument _document; + final int index; + final double _width; + final double _height; + final PdfPageRotation _rotation; + + @override + PdfDocument get document => _document; + + @override + double get width => _width; + + @override + double get height => _height; + + @override + PdfPageRotation get rotation => _rotation; + + @override + int get pageNumber => index + 1; + + @override + bool get isLoaded => true; + + @override + Future render({ + int x = 0, + int y = 0, + int? width, + int? height, + double? fullWidth, + double? fullHeight, + int? backgroundColor, + PdfPageRotation? rotationOverride, + PdfAnnotationRenderingMode annotationRenderingMode = + PdfAnnotationRenderingMode.annotationAndForms, + int flags = PdfPageRenderFlags.none, + PdfPageRenderCancellationToken? cancellationToken, + }) async { + if (_document._disposed) { + return null; + } + final token = cancellationToken as _CoreGraphicsCancellationToken?; + if (token?.isCanceled == true) { + return null; + } + + final targetFullWidth = max(1, (fullWidth ?? width ?? this.width).round()); + final targetFullHeight = max( + 1, + (fullHeight ?? height ?? this.height).round(), + ); + final targetWidth = max(1, (width ?? targetFullWidth)); + final targetHeight = max(1, (height ?? targetFullHeight)); + + final Map? result; + try { + result = await _document.channel + .invokeMapMethod('renderPage', { + 'handle': _document.handle, + 'pageIndex': index, + 'x': x, + 'y': y, + 'width': targetWidth, + 'height': targetHeight, + 'fullWidth': targetFullWidth, + 'fullHeight': targetFullHeight, + 'backgroundColor': backgroundColor ?? 0xffffffff, + 'rotation': rotationOverride != null + ? (rotationOverride.index - rotation.index + 4) & 3 + : 0, + 'flags': flags, + 'renderAnnotations': + annotationRenderingMode != PdfAnnotationRenderingMode.none, + 'renderForms': + annotationRenderingMode == + PdfAnnotationRenderingMode.annotationAndForms, + }); + } on MissingPluginException { + return null; + } + if (token?.isCanceled == true) { + return null; + } + if (result == null) { + return null; + } + final map = result.cast(); + final pixels = map['pixels']; + if (pixels is! Uint8List) { + throw const PdfException('renderPage did not return pixel data.'); + } + final renderedWidth = map['width'] as int? ?? targetWidth; + final renderedHeight = map['height'] as int? ?? targetHeight; + return _CoreGraphicsPdfImage( + width: renderedWidth, + height: renderedHeight, + pixels: pixels, + ); + } + + @override + PdfPageRenderCancellationToken createCancellationToken() => + _CoreGraphicsCancellationToken._(); + + @override + Future loadText() async { + try { + final result = await _document.channel.invokeMapMethod( + 'loadPageText', + {'handle': _document.handle, 'pageIndex': index}, + ); + if (result == null) { + return null; + } + final map = result.cast(); + final text = map['text'] as String?; + final rects = (map['rects'] as List?) + ?.map((e) => (e as Map).cast()) + .map( + (rect) => PdfRect( + (rect['left'] as num).toDouble(), + (rect['top'] as num).toDouble(), + (rect['right'] as num).toDouble(), + (rect['bottom'] as num).toDouble(), + ), + ) + .toList(growable: false); + if (text == null || rects == null) { + return null; + } + return PdfPageRawText(text, rects); + } on MissingPluginException { + return null; + } + } + + @override + Future> loadLinks({ + bool compact = false, + bool enableAutoLinkDetection = true, + }) async { + try { + final result = await _document.channel + .invokeListMethod('loadPageLinks', { + 'handle': _document.handle, + 'pageIndex': index, + 'enableAutoLinkDetection': enableAutoLinkDetection, + }); + if (result == null) { + return const []; + } + final links = result + .map((entry) { + final map = (entry as Map) + .cast(); + final rects = + (map['rects'] as List?) + ?.map( + (rect) => (rect as Map) + .cast(), + ) + .map( + (rect) => PdfRect( + (rect['left'] as num).toDouble(), + (rect['top'] as num).toDouble(), + (rect['right'] as num).toDouble(), + (rect['bottom'] as num).toDouble(), + ), + ) + .toList(growable: false) ?? + const []; + final url = map['url'] as String?; + final destMap = map['dest'] as Map?; + + // Parse annotation from Swift + final annotationData = map['annotation'] as Map?; + final annotation = annotationData != null + ? PdfAnnotation( + title: annotationData['title'] as String?, + content: annotationData['content'] as String?, + subject: annotationData['subject'] as String?, + modificationDate: PdfDateTime.fromPdfDateString( + annotationData['modificationDate'] as String?, + ), + creationDate: PdfDateTime.fromPdfDateString( + annotationData['creationDate'] as String?, + ), + ) + : null; + + final link = PdfLink( + rects, + url: url == null ? null : Uri.tryParse(url), + dest: _parseDest(destMap, defaultPageNumber: pageNumber), + annotation: annotation, + ); + return compact ? link.compact() : link; + }) + .toList(growable: false); + return List.unmodifiable(links); + } on MissingPluginException { + return const []; + } + } +} + +class _CoreGraphicsPdfImage implements PdfImage { + _CoreGraphicsPdfImage({ + required int width, + required int height, + required Uint8List pixels, + }) : _width = width, + _height = height, + _pixels = pixels; + + final int _width; + final int _height; + Uint8List _pixels; + bool _disposed = false; + + @override + int get width => _width; + + @override + int get height => _height; + + @override + Uint8List get pixels { + if (_disposed) { + throw StateError('PdfImage has been disposed.'); + } + return _pixels; + } + + @override + void dispose() { + _disposed = true; + _pixels = Uint8List(0); + } +} + +class _CoreGraphicsCancellationToken implements PdfPageRenderCancellationToken { + _CoreGraphicsCancellationToken._(); + + bool _isCanceled = false; + + @override + void cancel() { + _isCanceled = true; + } + + @override + bool get isCanceled => _isCanceled; +} + +PdfDest? _parseDest(Map? dest, {int defaultPageNumber = 1}) { + if (dest == null) return null; + final map = dest.cast(); + return PdfDest( + map['page'] as int? ?? defaultPageNumber, + _tryParseDestCommand(map['command'] as String?), + (map['params'] as List?) + ?.map((value) { + if (value == null) return null; + if (value is num) return value.toDouble(); + return null; + }) + .toList(growable: false), + ); +} + +PdfDestCommand _tryParseDestCommand(String? commandName) { + try { + if (commandName == null) { + return PdfDestCommand.unknown; + } + return PdfDestCommand.parse(commandName); + } catch (e) { + return PdfDestCommand.unknown; + } +} diff --git a/packages/pdfrx_coregraphics/pubspec.yaml b/packages/pdfrx_coregraphics/pubspec.yaml new file mode 100644 index 00000000..75b006d1 --- /dev/null +++ b/packages/pdfrx_coregraphics/pubspec.yaml @@ -0,0 +1,32 @@ +name: pdfrx_coregraphics +description: CoreGraphics-backed Pdfrx engine layer for Apple platforms. +version: 0.1.16 +homepage: https://github.com/espresso3389/pdfrx +repository: https://github.com/espresso3389/pdfrx/tree/master/packages/pdfrx +issue_tracker: https://github.com/espresso3389/pdfrx/issues + +environment: + sdk: '>=3.9.0 <4.0.0' + flutter: '>=3.35.1' +resolution: workspace + +dependencies: + flutter: + sdk: flutter + pdfrx_engine: ^0.3.9 + http: + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: + +flutter: + plugin: + platforms: + ios: + pluginClass: PdfrxCoregraphicsPlugin + sharedDarwinSource: true + macos: + pluginClass: PdfrxCoregraphicsPlugin + sharedDarwinSource: true diff --git a/packages/pdfrx_engine/.gitignore b/packages/pdfrx_engine/.gitignore new file mode 100644 index 00000000..79e6782a --- /dev/null +++ b/packages/pdfrx_engine/.gitignore @@ -0,0 +1,17 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock +pubspec_overrides.yaml + +/build/ +/test/.tmp/ + +/tmp/ +**/*.exe + +doc/api/ + diff --git a/packages/pdfrx_engine/CHANGELOG.md b/packages/pdfrx_engine/CHANGELOG.md new file mode 100644 index 00000000..40ad6106 --- /dev/null +++ b/packages/pdfrx_engine/CHANGELOG.md @@ -0,0 +1,177 @@ +## 0.3.9 + +- NEW: `PdfrxEntryFunctions.stopBackgroundWorker()` to stop the background worker thread ([#184](https://github.com/espresso3389/pdfrx/issues/184), [#430](https://github.com/espresso3389/pdfrx/issues/430)) +- Code cleanup: removed unused native function lookup + +## 0.3.8 + +- NEW: `PdfDocumentLoadCompleteEvent` for document load completion notification + +## 0.3.7 + +- IMPROVED: Add `isDirty` flag to page image cache to prevent cache removal before re-rendering page ([#567](https://github.com/espresso3389/pdfrx/issues/567)) +- FIXED: `round10BitFrac` should not process `Infinity` or `NaN` ([#550](https://github.com/espresso3389/pdfrx/issues/550)) +- WIP: Adding `PdfDocument.useNativeDocumentHandle`/`reloadPages` + +## 0.3.6 + +- NEW: [`PdfDateTime`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDateTime.html) extension type for PDF date string parsing ([PDF 32000-1:2008, 7.9.4 Dates](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf#page=95)) +- NEW: [`PdfAnnotation`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfAnnotation-class.html) class for PDF annotation metadata extraction ([#546](https://github.com/espresso3389/pdfrx/pull/546)) + +## 0.3.5 + +- Documentation updates. + +## 0.3.4 + +- NEW: Added [`PdfPageBaseExtensions.ensureLoaded()`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPageBaseExtensions/ensureLoaded.html) - Wait for page to load (waits indefinitely, never returns null) +- NEW: Added [`PdfPageBaseExtensions.waitForLoaded()`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPageBaseExtensions/waitForLoaded.html) - Wait for page to load with optional timeout (may return null on timeout) +- NEW: [`PdfPageStatusChange.page`](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPageStatusChange/page.html) property now provides the newest page instance directly +- NEW: Added comprehensive [Progressive Loading documentation](https://github.com/espresso3389/pdfrx/blob/master/doc/Progressive-Loading.md) +- IMPROVED: Better API for progressive loading - `ensureLoaded()` simplifies common use cases, `waitForLoaded()` for timeout scenarios + +## 0.3.3 + +- Code refactoring and maintenance updates. +- Use shortened syntax for `Allocator.allocate`. + +## 0.3.2 + +- Updated to `pdfium_dart` 0.1.2. +- Improved cache directory management with support for `PDFRX_CACHE_DIR` environment variable. +- Better default cache locations: `~/.pdfrx` on Unix-like systems, `%LOCALAPPDATA%\pdfrx` on Windows. +- Fallback to system temp directory for backward compatibility. + +## 0.3.1 + +- Updated to pdfium_dart 0.1.1 + +## 0.3.0 + +- NEW: `PdfDocument.createFromJpegData()` - Create PDF documents from JPEG image data +- CHANGED: Now uses `pdfium_dart` package for PDFium FFI bindings instead of bundled bindings +- CHANGED: File structure refactoring - moved from monolithic API file to separate files for better organization +- Dependency updates + +## 0.2.4 + +- NEW: `PdfDocument` now supports page re-arrangement and accepts `PdfPage` instances from other documents, enabling PDF combine/merge functionality +- Added additional PDFium functions for page manipulation +- FIXED: Type parameter 'T' shadowing issue in pdfium.dart + +## 0.2.3 + +- Added configurable timeout parameter to `openUri` and `pdfDocumentFromUri` functions ([#509](https://github.com/espresso3389/pdfrx/pull/509)) + +## 0.2.2 + +- Experimental support for Apple platforms direct symbol lookup to address TestFlight/App Store symbol lookup issues ([#501](https://github.com/espresso3389/pdfrx/issues/501)) +- Added `PdfrxBackend` enum to identify which PDF backend is being used +- Internal refactoring to support lookup-based function loading on iOS/macOS + +## 0.2.1 + +- FIXED: Handle servers that return 200 instead of 206 for content-range requests ([#468](https://github.com/espresso3389/pdfrx/issues/468)) + +## 0.2.0 + +- **BREAKING**: Renamed `PdfrxEntryFunctions.initPdfium()` to `PdfrxEntryFunctions.init()` for consistency + +## 0.1.21 + +- FIXED: WASM+Safari StringBuffer issue with workaround ([#483](https://github.com/espresso3389/pdfrx/issues/483)) +- Introduces `PdfDocumentRefKey` for more flexible `PdfDocumentRef` identification + +## 0.1.20 + +- Maintenance release to keep version alignment and ensure code integrity alongside pdfrx 2.1.19. + +## 0.1.19 + +- Remove broken docImport not to crash dartdoc ([dart-lang/dartdoc#4106](https://github.com/dart-lang/dartdoc/issues/4106)) + +## 0.1.18 + +- FIXED: `dart run pdfrx:remove_wasm_modules` could hit "Too many open files" on some platforms ([#476](https://github.com/espresso3389/pdfrx/issues/476)) +- Dependency updates + +## 0.1.17 + +- [#474](https://github.com/espresso3389/pdfrx/issues/474) Add PdfrxEntryFunctions.initPdfium to explicitly call FPDF_InitLibraryWithConfig and pdfrxInitialize/pdfrxFlutterInitialize internally call it +- [#474](https://github.com/espresso3389/pdfrx/issues/474) Add PdfrxEntryFunctions.suspendPdfiumWorkerDuringAction +- Documentation improvements for low-level PDFium bindings access/PDFium interoperability and initialization + +## 0.1.16 + +- More error handling logic for improved stability ([#468](https://github.com/espresso3389/pdfrx/issues/468)) + +## 0.1.15 + +- **BREAKING**: Integrated `loadText()` and `loadTextCharRects()` into a single function `loadText()` to fix crash issue ([#434](https://github.com/espresso3389/pdfrx/issues/434)) +- FIXED: Coordinate calculation errors when loading annotation links ([#458](https://github.com/espresso3389/pdfrx/issues/458)) + +## 0.1.14 + +- Experimental support for dynamic font installation on native platforms ([#456](https://github.com/espresso3389/pdfrx/issues/456)) + +## 0.1.13 + +- Add font loading APIs for WASM: `reloadFonts()` and `addFontData()` methods +- Add `PdfDocumentMissingFontsEvent` to notify about missing fonts in PDF documents +- FIXED: Text coordinate calculation when CropBox/MediaBox has non-zero origin ([#441](https://github.com/espresso3389/pdfrx/issues/441)) +- Improve WASM stability and font handling + +## 0.1.12 + +- Fix text character rectangle rotation handling in `loadTextCharRects()` and related methods + +## 0.1.11 + +- Make PdfiumDownloader class private to native implementation + +## 0.1.10 + +- Add mock pdfrxInitialize implementation for WASM compatibility to address pub.dev analyzer complaints + +## 0.1.9 + +- Update example to include text extraction functionality + +## 0.1.8 + +- More consistent behavior on disposed PdfDocument + +## 0.1.7 + +- Improve `loadPagesProgressively` API by making `onPageLoadProgress` a named parameter +- Fix parentheses in premultiplied alpha flag check +- Improve documentation for enums + +## 0.1.6 + +- Add premultiplied alpha support with new flag `PdfPageRenderFlags.premultipliedAlpha` + +## 0.1.5 + +- New text extraction API. + +## 0.1.4 + +- Minor updates. + +## 0.1.3 + +- Add an example for converting PDF pages to images. +- Add PdfImage.createImageNF() extension method to create Image (of image package) from PdfImage. + +## 0.1.2 + +- Introduces new PDF text extraction API. + +## 0.1.1 + +- Minor fixes. + +## 0.1.0 + +- Initial version. diff --git a/packages/pdfrx_engine/LICENSE b/packages/pdfrx_engine/LICENSE new file mode 100644 index 00000000..575416cb --- /dev/null +++ b/packages/pdfrx_engine/LICENSE @@ -0,0 +1,11 @@ + +The MIT License (MIT) +=============== + +Copyright (c) 2018 @espresso3389 (Takashi Kawasaki) + +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. diff --git a/packages/pdfrx_engine/README.md b/packages/pdfrx_engine/README.md new file mode 100644 index 00000000..88c87214 --- /dev/null +++ b/packages/pdfrx_engine/README.md @@ -0,0 +1,149 @@ +# pdfrx_engine + +[![Build Test](https://github.com/espresso3389/pdfrx/actions/workflows/build-test.yml/badge.svg)](https://github.com/espresso3389/pdfrx/actions/workflows/build-test.yml) + +[pdfrx_engine](https://pub.dartlang.org/packages/pdfrx_engine) is a platform-agnostic PDF rendering and manipulation engine built on top of [PDFium](https://pdfium.googlesource.com/pdfium/). It provides low-level PDF document APIs for viewing, editing, combining PDF documents, and importing images without any Flutter dependencies, making it suitable for use in pure Dart applications, CLI tools, or server-side PDF processing. + +This package depends on [pdfium_dart](https://pub.dartlang.org/packages/pdfium_dart) for PDFium FFI bindings and is a part of [pdfrx](https://pub.dartlang.org/packages/pdfrx) Flutter plugin, which adds UI widgets and Flutter-specific features on top of this engine. + +## Multi-platform support + +- Android +- iOS +- Windows +- macOS +- Linux (even on Raspberry Pi) +- Web (WASM) supported only on Flutter by [pdfrx](https://pub.dartlang.org/packages/pdfrx) + +## Example Codes + +### Page Image Export + +The following fragment illustrates how to use the PDF engine to load and render a PDF file: + +```dart +import 'dart:io'; +import 'package:image/image.dart' as img; +import 'package:pdfrx_engine/pdfrx_engine.dart'; + +void main() async { + await pdfrxInitialize(); + + final document = await PdfDocument.openFile('test.pdf'); + final page = document.pages[0]; // first page + final pageImage = await page.render( + width: page.width * 200 / 72, + height: page.height * 200 / 72, + ); + final image = pageImage!.createImageNF(); + await File('output.png').writeAsBytes(img.encodePng(image)); + pageImage.dispose(); + document.close(); +} +``` + +You should call [pdfrxInitialize()](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/pdfrxInitialize.html) before using any PDF engine APIs to ensure the native PDFium library is properly loaded. For more information, see [pdfrx Initialization](https://github.com/espresso3389/pdfrx/blob/master/doc/pdfrx-Initialization.md) + +### Page Manipulation Example + +The following example demonstrates how to combine pages from multiple PDF documents: + +```dart +import 'dart:io'; +import 'package:pdfrx_engine/pdfrx_engine.dart'; + +void main() async { + await pdfrxInitialize(); + + // Open source PDF documents + final doc1 = await PdfDocument.openFile('document1.pdf'); + final doc2 = await PdfDocument.openFile('document2.pdf'); + + // Create a new PDF document + final outputDoc = await PdfDocument.createNew(sourceName: 'combined.pdf'); + + // Combine pages: first 3 pages from doc1, all pages from doc2, last page from doc1 + outputDoc.pages = [ + ...doc1.pages.sublist(0, 3), + ...doc2.pages, + doc1.pages.last, + ]; + + // Save the combined PDF + final pdfData = await outputDoc.encodePdf(); + await File('combined.pdf').writeAsBytes(pdfData); + + // Clean up + doc1.dispose(); + doc2.dispose(); + outputDoc.dispose(); +} +``` + +### Image-to-PDF Conversion Example + +The following example shows how to convert JPEG images to PDF: + +```dart +import 'dart:io'; +import 'package:pdfrx_engine/pdfrx_engine.dart'; + +void main() async { + await pdfrxInitialize(); + + // Load JPEG image data + final jpegData = await File('photo.jpg').readAsBytes(); + + // Create PDF from JPEG (A4 size: 595 x 842 points) + final doc = await PdfDocument.createFromJpegData( + jpegData, + width: 595, + height: 842, + sourceName: 'photo.pdf', + ); + + // Save to file + final pdfData = await doc.encodePdf(); + await File('output.pdf').writeAsBytes(pdfData); + + doc.dispose(); +} +``` + +For more complex examples including selective page combining with range specifications, see [pdfcombine.dart](https://github.com/espresso3389/pdfrx/blob/master/packages/pdfrx_engine/example/pdfcombine.dart). + +For detailed guides and tutorials, see the [documentation](https://github.com/espresso3389/pdfrx/tree/master/doc): + +- [PDF Page Manipulation](https://github.com/espresso3389/pdfrx/blob/master/doc/PDF-Page-Manipulation.md) - Re-arrange, combine, and extract PDF pages +- [Importing Images to PDF](https://github.com/espresso3389/pdfrx/blob/master/doc/Importing-Images-to-PDF.md) - Convert images to PDF and insert images into PDFs + +## PDF API + +- Easy to use PDF APIs + - [PdfDocument](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDocument-class.html) - Main document interface + - [PdfDocument.openFile](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDocument/openFile.html) - Open PDF from file path + - [PdfDocument.openData](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDocument/openData.html) - Open PDF from memory (Uint8List) + - [PdfDocument.openUri](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDocument/openUri.html) - Open PDF from stream (advanced use case) + - [PdfDocument.openAsset](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDocument/openAsset.html) - Open PDF from Flutter asset + - [PdfDocument.createNew](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDocument/createNew.html) - Create new empty PDF document + - [PdfDocument.createFromJpegData](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfDocument/createFromJpegData.html) - Create PDF from JPEG data + - [PdfPage](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPage-class.html) - Page representation and rendering + - [PdfPage.render](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPage/render.html) - Render page to bitmap + - [PdfPage.loadText](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPage/loadText.html) - Extract text content from page + - [PdfPage.loadLinks](https://pub.dev/documentation/pdfrx_engine/latest/pdfrx_engine/PdfPage/loadLinks.html) - Extract links from page + +## When to Use pdfrx_engine vs. pdfrx + +**Use pdfrx_engine when:** + +- Building CLI tools or server applications +- You need PDF rendering without Flutter UI +- Creating custom PDF processing pipelines +- Working in pure Dart environments + +**Use pdfrx when:** + +- Building Flutter applications +- You need ready-to-use [PDF viewer widgets](https://pub.dev/documentation/pdfrx/latest/pdfrx/PdfViewer-class.html) +- You want features like text selection, search, and zoom controls +- You prefer high-level APIs with Flutter integration diff --git a/packages/pdfrx_engine/analysis_options.yaml b/packages/pdfrx_engine/analysis_options.yaml new file mode 100644 index 00000000..ab72b708 --- /dev/null +++ b/packages/pdfrx_engine/analysis_options.yaml @@ -0,0 +1,51 @@ +# This file configures the static analysis results for your project (errors, +# warnings, and lints). +# +# This enables the 'recommended' set of lints from `package:lints`. +# This set helps identify many issues that may lead to problems when running +# or consuming Dart code, and enforces writing Dart using a single, idiomatic +# style and format. +# +# If you want a smaller set of lints you can change this to specify +# 'package:lints/core.yaml'. These are just the most critical lints +# (the recommended set includes the core lints). +# The core lints are also what is used by pub.dev for scoring packages. + +include: package:lints/recommended.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + prefer_single_quotes: true + comment_references: true + prefer_relative_imports: true + use_key_in_widget_constructors: true + avoid_return_types_on_setters: true + avoid_types_on_closure_parameters: true + eol_at_end_of_file: true + sort_child_properties_last: true + sort_unnamed_constructors_first: true + sort_constructors_first: true + always_put_required_named_parameters_first: true + invalid_runtime_check_with_js_interop_types: true + type_annotate_public_apis: true + omit_local_variable_types: true + omit_obvious_local_variable_types: true + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options + +analyzer: + +formatter: + page_width: 120 diff --git a/packages/pdfrx_engine/example/README.md b/packages/pdfrx_engine/example/README.md new file mode 100644 index 00000000..e0b43c52 --- /dev/null +++ b/packages/pdfrx_engine/example/README.md @@ -0,0 +1,117 @@ +# pdfrx_engine Examples + +This directory contains example applications demonstrating the capabilities of `pdfrx_engine`. + +## Examples + +### pdf2image.dart + +Converts PDF pages to PNG images and extracts text from each page. + +**Usage:** + +```bash +dart run example/main.dart [output_dir] +``` + +**Example:** + +```bash +dart run example/main.dart document.pdf ./output +``` + +### pdfcombine.dart + +Combines multiple PDF files into a single PDF, with flexible page selection and ordering. + +**Usage:** + +```bash +dart run example/pdfcombine.dart [...] -o [...] -- ... +``` + +Input PDF files are automatically assigned IDs: a, b, c, etc. in order. +The `-o` flag can appear anywhere before the `--` separator. + +**Arguments:** + +- `-o ` - Output PDF file path (can appear anywhere before `--`) +- `...` - Input PDF file(s) (assigned IDs a, b, c, ... in order) +- `--` - Separator between input files and page specifications +- `...` - Page specification (e.g., `a`, `b[1-3]`, `c[1,2,3]`) + +**Page specification formats:** + +- `a` - All pages from file a +- `a[1-10]` - Pages 1-10 from file a +- `a[1,2,3,4]` - Pages 1,2,3,4 from file a +- `a[1-3,5,6,7,10]` - Pages 1,2,3,5,6,7,10 from file a (hybrid of ranges and individual pages) + +**Examples:** + +Combine all pages from three PDFs in order (files are assigned IDs a, b, c): + +```bash +dart run example/pdfcombine.dart -o output.pdf doc1.pdf doc2.pdf doc3.pdf -- a b c +``` + +Combine specific pages from multiple PDFs (-o at the beginning): + +```bash +dart run example/pdfcombine.dart -o output.pdf doc1.pdf doc2.pdf doc3.pdf -- a b[1-3] c b[4,5,6] +``` + +Same command with -o in the middle (files can be split around -o flag): + +```bash +dart run example/pdfcombine.dart doc1.pdf doc2.pdf -o output.pdf doc3.pdf -- a b[1-3] c b[4,5,6] +``` + +This will: +1. `doc1.pdf` is assigned ID `a`, `doc2.pdf` is assigned ID `b`, `doc3.pdf` is assigned ID `c` +2. Add all pages from `a` (doc1.pdf) +3. Add pages 1-3 from `b` (doc2.pdf) +4. Add all pages from `c` (doc3.pdf) +5. Add pages 4,5,6 from `b` (doc2.pdf) + +Split and reorder pages from a single file: + +```bash +dart run example/pdfcombine.dart -o output.pdf input.pdf -- a[1-10] a[20-30] a[11-19] +``` + +Merge two PDFs with custom ordering: + +```bash +dart run example/pdfcombine.dart -o merged.pdf input1.pdf input2.pdf -- a[1-10] b a[11-20] +``` + +Use hybrid page specifications (ranges and individual pages): + +```bash +dart run example/pdfcombine.dart -o output.pdf doc.pdf -- a[1-3,5,6,7,10] +``` + +This extracts pages 1,2,3,5,6,7,10 from `doc.pdf`. + +## Running Examples + +From the repository root: + +```bash +# Run pdf2image example +dart run packages/pdfrx_engine/example/main.dart [output_dir] + +# Run pdfcombine example +dart run packages/pdfrx_engine/example/pdfcombine.dart +``` + +From the `packages/pdfrx_engine` directory: + +```bash +# Run pdf2image example +dart run example/main.dart [output_dir] + +# Run pdfcombine example +dart run example/pdfcombine.dart +``` diff --git a/packages/pdfrx_engine/example/pdf2image.dart b/packages/pdfrx_engine/example/pdf2image.dart new file mode 100644 index 00000000..b95e8c6b --- /dev/null +++ b/packages/pdfrx_engine/example/pdf2image.dart @@ -0,0 +1,71 @@ +import 'dart:io'; + +import 'package:image/image.dart' as img; +import 'package:pdfrx_engine/pdfrx_engine.dart'; + +Future main(List args) async { + if (args.isEmpty) { + print('Usage: dart pdf2image.dart [output_dir]'); + print('Example: dart pdf2image.dart document.pdf ./output'); + return 1; + } + + final pdfFile = args[0]; + final outputDir = args.length > 1 ? args[1] : './output'; + + // Create output directory if it doesn't exist + final outputDirectory = Directory(outputDir); + if (!outputDirectory.existsSync()) { + outputDirectory.createSync(recursive: true); + } + + print('Converting PDF: $pdfFile'); + print('Output directory: $outputDir'); + + try { + await pdfrxInitialize(); + + // Open the PDF document + final document = await PdfDocument.openFile(pdfFile); + + print('PDF opened successfully. Pages: ${document.pages.length}'); + + // Process each page + for (var i = 0; i < document.pages.length; i++) { + final pageNumber = i + 1; + print('Processing page $pageNumber/${document.pages.length}...'); + + final page = document.pages[i]; + + // Render at 200 DPI + const scale = 200.0 / 72; + final pageImage = await page.render(fullWidth: page.width * scale, fullHeight: page.height * scale); + if (pageImage == null) { + print('Failed to render page $pageNumber'); + continue; + } + + // Convert to image format using the createImageNF extension + final image = pageImage.createImageNF(); + + pageImage.dispose(); + + // Save as PNG + final outputImageFile = File('$outputDir/page_$pageNumber.png'); + await outputImageFile.writeAsBytes(img.encodePng(image)); + + final outputTextFile = File('$outputDir/page_$pageNumber.txt'); + await outputTextFile.writeAsString((await page.loadText())?.fullText ?? ''); + } + + // Clean up + document.dispose(); + + print('\nConversion completed successfully!'); + print('Output files saved in: $outputDir'); + return 0; + } catch (e) { + print('Error: $e'); + return 1; + } +} diff --git a/packages/pdfrx_engine/example/pdfcombine.dart b/packages/pdfrx_engine/example/pdfcombine.dart new file mode 100644 index 00000000..3fd7acc0 --- /dev/null +++ b/packages/pdfrx_engine/example/pdfcombine.dart @@ -0,0 +1,239 @@ +import 'dart:io'; + +import 'package:pdfrx_engine/pdfrx_engine.dart'; + +/// Represents a page specification for a PDF file. +/// +/// Examples: +/// - `a` - all pages from file 'a' +/// - `a[1-10]` - pages 1-10 from file 'a' +/// - `a[1,2,3,4]` - pages 1,2,3,4 from file 'a' +/// - `a[1-3,5,6,7,10]` - pages 1,2,3,5,6,7,10 from file 'a' (hybrid) +class PageSpec { + PageSpec(this.fileId, this.pages); + + final String fileId; + final List? pages; // null means all pages + + /// Parses a page specification string like 'a', 'a[1-10]', 'a[1,2,3,4]', or 'a[1-3,5,6,7,10]' + static PageSpec parse(String spec) { + final match = RegExp(r'^([a-zA-Z0-9_-]+)(?:\[([0-9,\-\s]+)\])?$').firstMatch(spec.trim()); + if (match == null) { + throw ArgumentError('Invalid page specification: $spec'); + } + + final fileId = match.group(1)!; + final pageRange = match.group(2); + + if (pageRange == null) { + return PageSpec(fileId, null); // All pages + } + + final pages = []; + for (final part in pageRange.split(',')) { + final rangePart = part.trim(); + if (rangePart.contains('-')) { + final rangeParts = rangePart.split('-').map((s) => s.trim()).toList(); + if (rangeParts.length != 2) { + throw ArgumentError('Invalid page range: $rangePart'); + } + final start = int.parse(rangeParts[0]); + final end = int.parse(rangeParts[1]); + if (start > end) { + throw ArgumentError('Invalid page range: $rangePart (start > end)'); + } + for (var i = start; i <= end; i++) { + pages.add(i); + } + } else { + pages.add(int.parse(rangePart)); + } + } + + return PageSpec(fileId, pages); + } + + @override + String toString() => pages == null ? fileId : '$fileId[${pages!.join(',')}]'; +} + +Future main(List args) async { + if (args.length < 4) { + print('Usage: dart pdfcombine.dart [...] -o [...] -- ...'); + print(''); + print('Input PDF files are automatically assigned IDs: a, b, c, etc.'); + print('The -o flag can appear anywhere before the -- separator.'); + print(''); + print('Examples:'); + print(' dart pdfcombine.dart -o output.pdf doc1.pdf doc2.pdf doc3.pdf -- a b[1-3] c b[4,5,6]'); + print(' dart pdfcombine.dart doc1.pdf doc2.pdf -o output.pdf doc3.pdf -- a b[1-3] c b[4,5,6]'); + print(' dart pdfcombine.dart input1.pdf input2.pdf -o merged.pdf -- a[1-10] b a[11-20]'); + print(''); + print('Arguments:'); + print(' -o - Output PDF file path (can appear anywhere before --)'); + print(' ... - Input PDF file(s) (assigned IDs a, b, c, ... in order)'); + print(' -- - Separator between input files and page specifications'); + print(' ... - Page specification (e.g., a, b[1-3], c[1,2,3])'); + print(''); + print('Page specification formats:'); + print(' a - All pages from file a'); + print(' a[1-10] - Pages 1-10 from file a'); + print(' a[1,2,3,4] - Pages 1,2,3,4 from file a'); + print(' a[1-3,5,6,7,10] - Pages 1,2,3,5,6,7,10 from file a (hybrid)'); + return 1; + } + + try { + await pdfrxInitialize(); + + // Parse arguments + String? outputFile; + final inputFiles = []; + final pageSpecArgs = []; + + var i = 0; + var foundSeparator = false; + + // Parse input files and -o flag until we hit -- + while (i < args.length) { + if (args[i] == '--') { + foundSeparator = true; + i++; + break; + } else if (args[i] == '-o') { + if (i + 1 >= args.length) { + print('Error: -o flag requires an output file path'); + return 1; + } + if (outputFile != null) { + print('Error: Multiple -o flags specified'); + return 1; + } + outputFile = args[i + 1]; + i += 2; + } else { + inputFiles.add(args[i]); + i++; + } + } + + if (!foundSeparator) { + print('Error: Missing -- separator between input files and page specifications'); + return 1; + } + + if (outputFile == null) { + print('Error: Missing -o flag for output file'); + return 1; + } + + // Remaining arguments are page specifications + while (i < args.length) { + pageSpecArgs.add(args[i]); + i++; + } + + // Validate inputs + if (inputFiles.isEmpty) { + print('Error: No input PDF files specified'); + return 1; + } + + if (pageSpecArgs.isEmpty) { + print('Error: No page specifications provided'); + return 1; + } + + // Assign file IDs (a, b, c, etc.) to input files + final fileMap = {}; + for (var i = 0; i < inputFiles.length; i++) { + final filePath = inputFiles[i]; + if (!File(filePath).existsSync()) { + print('Error: File not found: $filePath'); + return 1; + } + final fileId = String.fromCharCode(97 + i); // 'a' + i + fileMap[fileId] = filePath; + } + + // Parse page specifications + final pageSpecs = []; + for (final arg in pageSpecArgs) { + try { + pageSpecs.add(PageSpec.parse(arg)); + } catch (e) { + print('Error parsing page specification "$arg": $e'); + return 1; + } + } + + // Validate all file IDs in page specs exist + for (final spec in pageSpecs) { + if (!fileMap.containsKey(spec.fileId)) { + print('Error: Unknown file ID "${spec.fileId}" in page specification'); + print('Available file IDs: ${fileMap.keys.join(', ')}'); + return 1; + } + } + + print('Input files:'); + fileMap.forEach((id, path) => print(' $id = $path')); + print('Output file: $outputFile'); + print('Page specifications: ${pageSpecs.join(' ')}'); + print(''); + + // Open all PDF documents + final documents = {}; + PdfDocument? outputDoc; + try { + for (final entry in fileMap.entries) { + print('Opening ${entry.value}...'); + documents[entry.key] = await PdfDocument.openFile(entry.value); + } + + // Combine pages from all specifications + print('Combining pages...'); + final combinedPages = []; + + for (final spec in pageSpecs) { + final doc = documents[spec.fileId]!; + final pages = spec.pages ?? List.generate(doc.pages.length, (i) => i + 1); + + // Validate page numbers + for (final pageNum in pages) { + if (pageNum < 1 || pageNum > doc.pages.length) { + print('Error: Page $pageNum out of range for file ${spec.fileId} (has ${doc.pages.length} pages)'); + return 1; + } + } + + for (final pageNum in pages) { + combinedPages.add(doc.pages[pageNum - 1]); + print(' Adding page $pageNum from ${spec.fileId}'); + } + } + + // Encode and save the combined PDF + print(''); + print('Saving to $outputFile...'); + outputDoc = await PdfDocument.createNew(sourceName: outputFile); + outputDoc.pages = combinedPages; + final pdfData = await outputDoc.encodePdf(); + await File(outputFile).writeAsBytes(pdfData); + + print(''); + print('Successfully combined ${combinedPages.length} pages into $outputFile'); + return 0; + } finally { + // Clean up - close all documents + for (final doc in documents.values) { + doc.dispose(); + } + outputDoc?.dispose(); + } + } catch (e, stackTrace) { + print('Error: $e'); + print(stackTrace); + return 1; + } +} diff --git a/packages/pdfrx_engine/lib/pdfrx_engine.dart b/packages/pdfrx_engine/lib/pdfrx_engine.dart new file mode 100644 index 00000000..3446469b --- /dev/null +++ b/packages/pdfrx_engine/lib/pdfrx_engine.dart @@ -0,0 +1,22 @@ +library; + +export 'src/mock/pdfrx_initialize_mock.dart' if (dart.library.io) 'src/pdfrx_initialize_dart.dart'; +export 'src/pdf_annotation.dart'; +export 'src/pdf_datetime.dart'; +export 'src/pdf_dest.dart'; +export 'src/pdf_document.dart'; +export 'src/pdf_document_event.dart'; +export 'src/pdf_exception.dart'; +export 'src/pdf_font_query.dart'; +export 'src/pdf_image.dart'; +export 'src/pdf_link.dart'; +export 'src/pdf_outline_node.dart'; +export 'src/pdf_page.dart'; +export 'src/pdf_page_status_change.dart'; +export 'src/pdf_permissions.dart'; +export 'src/pdf_point.dart'; +export 'src/pdf_rect.dart'; +export 'src/pdf_text.dart'; +export 'src/pdfrx.dart'; +export 'src/pdfrx_dart.dart'; +export 'src/pdfrx_entry_functions.dart'; diff --git a/packages/pdfrx_engine/lib/src/mock/pdfrx_initialize_mock.dart b/packages/pdfrx_engine/lib/src/mock/pdfrx_initialize_mock.dart new file mode 100644 index 00000000..253e1a10 --- /dev/null +++ b/packages/pdfrx_engine/lib/src/mock/pdfrx_initialize_mock.dart @@ -0,0 +1,23 @@ +library; + +import '../pdfrx.dart'; +import '../pdfrx_entry_functions.dart'; + +/// Initializes the Pdfrx library for Dart. +/// +/// This function sets up the following: +/// +/// - [Pdfrx.getCacheDirectory] is set to return the system temporary directory. +/// - [Pdfrx.pdfiumModulePath] is configured to point to the pdfium module. +/// - The function checks for the `PDFIUM_PATH` environment variable to find an existing pdfium module. +/// - If Pdfium module is not found, it will be downloaded from the internet. +/// - [Pdfrx.loadAsset] is set to throw an error by default (Dart does not support assets like Flutter does). +/// - Calls [PdfrxEntryFunctions.init] to initialize the library. +/// +/// For Flutter, you should call `pdfrxFlutterInitialize` instead of the function. +Future pdfrxInitialize({String? tmpPath, String? pdfiumRelease}) async { + throw UnimplementedError( + 'Wow, this is not supposed to be called.\n' + 'For WASM support, use Flutter and initialize with pdfrxFlutterInitialize function.', + ); +} diff --git a/packages/pdfrx_engine/lib/src/mock/pdfrx_mock.dart b/packages/pdfrx_engine/lib/src/mock/pdfrx_mock.dart new file mode 100644 index 00000000..9af2db25 --- /dev/null +++ b/packages/pdfrx_engine/lib/src/mock/pdfrx_mock.dart @@ -0,0 +1,119 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import '../pdf_document.dart'; +import '../pdfrx_entry_functions.dart'; + +/// This is an empty implementation of [PdfrxEntryFunctions] that just throws [UnimplementedError]. +/// +/// This is used to indicate that the factory is not initialized. +class PdfrxEntryFunctionsImpl implements PdfrxEntryFunctions { + PdfrxEntryFunctionsImpl(); + + Future unimplemented() { + throw UnimplementedError( + 'PdfrxEntryFunctions.instance is not initialized. ' + 'Please call pdfrxInitialize()/pdfrxFlutterInitialize() or explicitly set PdfrxEntryFunctions.instance.', + ); + } + + @override + Future init() => unimplemented(); + + @override + Future suspendPdfiumWorkerDuringAction(FutureOr Function() action) async { + unimplemented(); // actually never returns + return await action(); + } + + @override + Future compute(FutureOr Function(M message) callback, M message) async { + throw UnimplementedError( + 'compute() is not implemented because PdfrxEntryFunctions.instance is not initialized. ' + 'Please call pdfrxInitialize()/pdfrxFlutterInitialize() or explicitly set PdfrxEntryFunctions.instance.', + ); + } + + @override + Future stopBackgroundWorker() async { + throw UnimplementedError( + 'stopBackgroundWorker() is not implemented because PdfrxEntryFunctions.instance is not initialized. ' + 'Please call pdfrxInitialize()/pdfrxFlutterInitialize() or explicitly set PdfrxEntryFunctions.instance.', + ); + } + + @override + Future openAsset( + String name, { + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, + }) => unimplemented(); + + @override + Future openCustom({ + required FutureOr Function(Uint8List buffer, int position, int size) read, + required int fileSize, + required String sourceName, + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, + int? maxSizeToCacheOnMemory, + void Function()? onDispose, + }) => unimplemented(); + + @override + Future openData( + Uint8List data, { + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + String? sourceName, + bool allowDataOwnershipTransfer = false, + bool useProgressiveLoading = false, + void Function()? onDispose, + }) => unimplemented(); + + @override + Future openFile( + String filePath, { + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, + }) => unimplemented(); + + @override + Future openUri( + Uri uri, { + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, + PdfDownloadProgressCallback? progressCallback, + bool preferRangeAccess = false, + Map? headers, + bool withCredentials = false, + Duration? timeout, + }) => unimplemented(); + + @override + Future createNew({required String sourceName}) => unimplemented(); + + @override + Future createFromJpegData( + Uint8List jpegData, { + required double width, + required double height, + required String sourceName, + }) => unimplemented(); + + @override + Future reloadFonts() => unimplemented(); + + @override + Future addFontData({required String face, required Uint8List data}) => unimplemented(); + + @override + Future clearAllFontData() => unimplemented(); + + @override + PdfrxBackendType get backendType => PdfrxBackendType.mock; +} diff --git a/packages/pdfrx_engine/lib/src/mock/string_buffer_wrapper.dart b/packages/pdfrx_engine/lib/src/mock/string_buffer_wrapper.dart new file mode 100644 index 00000000..21024905 --- /dev/null +++ b/packages/pdfrx_engine/lib/src/mock/string_buffer_wrapper.dart @@ -0,0 +1,22 @@ +/// A workaround for WASM+Safari StringBuffer issue. +class StringBufferWrapper { + String buffer = ''; + + void write(Object? str) { + buffer += str?.toString() ?? ''; + } + + int get length => buffer.length; + + @override + String toString() => buffer; +} + +/// This is a workaround for WASM+Safari StringBuffer issue (#483). +/// +/// - for native code, use [StringBuffer] directly +/// - for Flutter Web, use this [StringBufferWrapper] that internally uses [String] instead. +StringBufferWrapper createStringBufferForWorkaroundSafariWasm() { + // FIXME: we need some kind of kIsWeb && kIsSafari check here but it's not easy to do that correctly on pdfrx_engine. + return StringBufferWrapper(); +} diff --git a/lib/src/pdfium/http_cache_control.dart b/packages/pdfrx_engine/lib/src/native/http_cache_control.dart similarity index 81% rename from lib/src/pdfium/http_cache_control.dart rename to packages/pdfrx_engine/lib/src/native/http_cache_control.dart index 21f2dda2..232e8cfb 100644 --- a/lib/src/pdfium/http_cache_control.dart +++ b/packages/pdfrx_engine/lib/src/native/http_cache_control.dart @@ -54,20 +54,20 @@ class HttpCacheControl { @override String toString() { - final sb = StringBuffer(); - if (noCache) sb.write('no-cache,'); - if (mustRevalidate) sb.write('must-revalidate,'); - if (noStore) sb.write('no-store,'); - if (private) sb.write('private,'); - if (public) sb.write('public,'); - if (mustUnderstand) sb.write('must-understand,'); - if (noTransform) sb.write('no-transform,'); - if (immutable) sb.write('immutable,'); - if (staleWhileRevalidate) sb.write('stale-while-revalidate,'); - if (staleIfError) sb.write('stale-if-error,'); - if (maxAge != null) sb.write('max-age=$maxAge,'); - if (sMaxAge != null) sb.write('s-maxage=$sMaxAge,'); - return sb.toString(); + final sb = []; + if (noCache) sb.add('no-cache'); + if (mustRevalidate) sb.add('must-revalidate'); + if (noStore) sb.add('no-store'); + if (private) sb.add('private'); + if (public) sb.add('public'); + if (mustUnderstand) sb.add('must-understand'); + if (noTransform) sb.add('no-transform'); + if (immutable) sb.add('immutable'); + if (staleWhileRevalidate) sb.add('stale-while-revalidate'); + if (staleIfError) sb.add('stale-if-error'); + if (maxAge != null) sb.add('max-age=$maxAge'); + if (sMaxAge != null) sb.add('s-maxage=$sMaxAge'); + return sb.join(','); } @override @@ -96,8 +96,8 @@ class HttpCacheControlState { final expires = _parseHttpDateTime(headers['expires']); final etag = headers['etag']; final lastModified = _parseHttpDateTime(headers['last-modified']); - var noCache = cacheControl?.contains('no-cache') == true; - var noStore = cacheControl?.contains('no-store') == true; + var noCache = cacheControl?.contains('no-cache') ?? false; + var noStore = cacheControl?.contains('no-store') ?? false; var maxAge = int.tryParse( cacheControl?.firstWhere((e) => e.startsWith('max-age='), orElse: () => '********').substring(8) ?? '', ); @@ -107,22 +107,21 @@ class HttpCacheControlState { maxAge = maxAgeForNoStore.inSeconds; } return HttpCacheControlState( - cacheControl: - cacheControl != null - ? HttpCacheControl.fromDirectives( - noCache: noCache, - mustRevalidate: cacheControl.contains('must-revalidate'), - noStore: noStore, - private: cacheControl.contains('private'), - public: cacheControl.contains('public'), - mustUnderstand: cacheControl.contains('must-understand'), - noTransform: cacheControl.contains('no-transform'), - immutable: cacheControl.contains('immutable'), - staleWhileRevalidate: cacheControl.contains('stale-while-revalidate'), - staleIfError: cacheControl.contains('stale-if-error'), - maxAge: maxAge, - ) - : const HttpCacheControl(directives: 0), + cacheControl: cacheControl != null + ? HttpCacheControl.fromDirectives( + noCache: noCache, + mustRevalidate: cacheControl.contains('must-revalidate'), + noStore: noStore, + private: cacheControl.contains('private'), + public: cacheControl.contains('public'), + mustUnderstand: cacheControl.contains('must-understand'), + noTransform: cacheControl.contains('no-transform'), + immutable: cacheControl.contains('immutable'), + staleWhileRevalidate: cacheControl.contains('stale-while-revalidate'), + staleIfError: cacheControl.contains('stale-if-error'), + maxAge: maxAge, + ) + : const HttpCacheControl(directives: 0), date: date, expires: expires, etag: etag, @@ -195,7 +194,7 @@ class HttpCacheControlState { int get hashCode => cacheControl.hashCode ^ date.hashCode ^ expires.hashCode ^ etag.hashCode ^ lastModified.hashCode; } -int _parseInt(String s) => s == 'null' ? 0 : int.parse(s); +int? _parseInt(String s) => s == 'null' ? null : int.parse(s); DateTime? _parseDateTime(String s) => s == 'null' ? null : DateTime.fromMillisecondsSinceEpoch(int.parse(s) * 1000); diff --git a/packages/pdfrx_engine/lib/src/native/native_utils.dart b/packages/pdfrx_engine/lib/src/native/native_utils.dart new file mode 100644 index 00000000..ad4a02c5 --- /dev/null +++ b/packages/pdfrx_engine/lib/src/native/native_utils.dart @@ -0,0 +1,50 @@ +import 'dart:io'; + +import 'package:path/path.dart' as path; + +import '../pdfrx.dart'; + +/// Helper function to get the cache directory for a specific purpose and name. +Future getCacheDirectory( + String part1, [ + String? part2, + String? part3, + String? part4, + String? part5, + String? part6, + String? part7, + String? part8, + String? part9, + String? part10, + String? part11, + String? part12, + String? part13, + String? part14, + String? part15, +]) async { + if (Pdfrx.getCacheDirectory == null) { + throw StateError('Pdfrx.getCacheDirectory is not set. Please set it to get cache directory.'); + } + final dir = Directory( + path.join( + await Pdfrx.getCacheDirectory!(), + part1, + part2, + part3, + part4, + part5, + part6, + part7, + part8, + part9, + part10, + part11, + part12, + part13, + part14, + part15, + ), + ); + await dir.create(recursive: true); + return dir; +} diff --git a/lib/src/pdfium/pdf_file_cache.dart b/packages/pdfrx_engine/lib/src/native/pdf_file_cache.dart similarity index 70% rename from lib/src/pdfium/pdf_file_cache.dart rename to packages/pdfrx_engine/lib/src/native/pdf_file_cache.dart index 0cb8bab8..e8bed9f0 100644 --- a/lib/src/pdfium/pdf_file_cache.dart +++ b/packages/pdfrx_engine/lib/src/native/pdf_file_cache.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; @@ -6,17 +7,39 @@ import 'dart:typed_data'; import 'package:crypto/crypto.dart'; import 'package:http/http.dart' as http; import 'package:path/path.dart' as path; -import 'package:path_provider/path_provider.dart'; +import 'package:synchronized/extension.dart'; -import '../../pdfrx.dart'; +import '../pdf_document.dart'; +import '../pdf_exception.dart'; +import '../pdfrx.dart'; +import '../pdfrx_entry_functions.dart'; +import '../pdfrx_initialize_dart.dart'; import 'http_cache_control.dart'; +import 'native_utils.dart'; +import 'package:pdfium_dart/pdfium_dart.dart' as pdfium_bindings; + +final _rafFinalizer = Finalizer((raf) { + // Attempt to close the file if it hasn't been closed explicitly. + // Use try-catch as close might fail or already be closed. + try { + raf.close(); + // Consider adding logging here if needed for debugging finalization. + // print('PdfFileCache: Finalizer closed RandomAccessFile.'); + } catch (_) { + // Ignore errors during finalization. + } +}); /// PDF file cache backed by a file. /// +/// The cache directory used by this class is obtained using [Pdfrx.getCacheDirectory]. +/// +/// For Flutter, `pdfrxFlutterInitialize` should be called explicitly or implicitly before using this class. +/// For Dart only, call [pdfrxInitialize] or explicitly set [Pdfrx.getCacheDirectory]. class PdfFileCache { PdfFileCache(this.file); - /// Default cache block size is 32KB. + /// Default cache block size is 1MB. static const defaultBlockSize = 1024 * 1024; /// Cache file. @@ -46,7 +69,7 @@ class PdfFileCache { int get cachedBytes { if (!isInitialized) return 0; var countCached = 0; - for (int i = 0; i < totalBlocks; i++) { + for (var i = 0; i < totalBlocks; i++) { if (isCached(i)) { countCached++; } @@ -57,12 +80,27 @@ class PdfFileCache { bool get isInitialized => _initialized; Future close() async { - await _raf?.close(); - _raf = null; + final raf = _raf; + if (raf != null) { + _rafFinalizer.detach(this); // Detach from finalizer since we are closing explicitly + _raf = null; + await raf.close(); + } + } + + Future deleteCacheFile() async { + await close(); + try { + await file.delete(); + } catch (_) {} } Future _ensureFileOpen() async { - _raf ??= await file.open(mode: FileMode.append); + if (_raf == null) { + _raf = await file.open(mode: FileMode.append); + // Attach the file handle to the finalizer, associated with 'this' cache instance. + _rafFinalizer.attach(this, _raf!, detach: this); + } } Future _read(List buffer, int bufferPosition, int position, int size) async { @@ -91,14 +129,14 @@ class PdfFileCache { Future setCached(int startBlock, {int? lastBlock}) async { lastBlock ??= startBlock; - for (int i = startBlock; i <= lastBlock; i++) { + for (var i = startBlock; i <= lastBlock; i++) { _cacheState[i >> 3] |= 1 << (i & 7); } await _saveCacheState(); } static const header1Size = 16; - static const headerMagic = 23456; + static const headerMagic = 34567; static const dataStrSizeMax = 128; Future _saveCacheState() => _write(_cacheStatePosition!, _cacheState); @@ -146,7 +184,7 @@ class PdfFileCache { _fileSize = await _getSize() - _headerSize!; } _initialized = true; - } catch (e) { + } catch (_) { _initialized = false; } } @@ -214,25 +252,37 @@ class PdfFileCache { } static Future getCacheFilePathForUri(Uri uri) async { - final cacheDir = await getCacheDirectory(); - final fnHash = - sha1.convert(utf8.encode(uri.toString())).bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); + if (Pdfrx.getCacheDirectory == null) { + throw StateError('Pdfrx.getCacheDirectory is not set. Please set it to get cache directory.'); + } + final fnHash = sha1 + .convert(utf8.encode(uri.toString())) + .bytes + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(); final dir1 = fnHash.substring(0, 2); final dir2 = fnHash.substring(2, 4); final body = fnHash.substring(4); - final dir = Directory(path.join(cacheDir.path, 'pdfrx.cache', dir1, dir2)); - await dir.create(recursive: true); + final dir = await getCacheDirectory('pdfrx.cache', dir1, dir2); return File(path.join(dir.path, '$body.pdf')); } static Future fromUri(Uri uri) async { return await fromFile(await getCacheFilePathForUri(uri)); } +} - /// Function to determine the cache directory. - /// - /// You can override the default cache directory by setting this variable. - static Future Function() getCacheDirectory = getApplicationCacheDirectory; +class _HttpClientWrapper { + _HttpClientWrapper(this.createHttpClient); + final http.Client Function() createHttpClient; + + http.Client? _client; + http.Client get client => _client ??= createHttpClient(); + + void reset() { + _client?.close(); + _client = null; + } } /// Open PDF file from [uri]. @@ -242,41 +292,39 @@ Future pdfDocumentFromUri( Uri uri, { PdfPasswordProvider? passwordProvider, bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, int? blockSize, PdfFileCache? cache, PdfDownloadProgressCallback? progressCallback, - PdfDownloadReportCallback? reportCallback, bool useRangeAccess = true, Map? headers, + Duration? timeout, + PdfrxEntryFunctions? entryFunctions, }) async { - final startTime = reportCallback != null ? DateTime.now() : null; - void report() { - if (reportCallback != null && cache?.isInitialized == true) { - reportCallback(cache?.cachedBytes ?? 0, cache?.fileSize ?? 0, DateTime.now().difference(startTime!)); - } - } - + entryFunctions ??= PdfrxEntryFunctions.instance; progressCallback?.call(0); cache ??= await PdfFileCache.fromUri(uri); - final httpClient = Pdfrx.createHttpClient?.call() ?? http.Client(); + final httpClientWrapper = _HttpClientWrapper(Pdfrx.createHttpClient ?? () => http.Client()); try { if (!cache.isInitialized) { cache.setBlockSize(blockSize ?? PdfFileCache.defaultBlockSize); final result = await _downloadBlock( - httpClient, + httpClientWrapper, uri, cache, progressCallback, 0, useRangeAccess: useRangeAccess, headers: headers, + timeout: timeout, ); if (result.isFullDownload) { - return await PdfDocument.openFile( + return await entryFunctions.openFile( cache.filePath, passwordProvider: passwordProvider, firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, ); } } else { @@ -285,7 +333,7 @@ Future pdfDocumentFromUri( // cache is valid; no need to download. } else { final result = await _downloadBlock( - httpClient, + httpClientWrapper, uri, cache, progressCallback, @@ -293,29 +341,41 @@ Future pdfDocumentFromUri( addCacheControlHeaders: true, useRangeAccess: useRangeAccess, headers: headers, + timeout: timeout, ); - if (result.isFullDownload) { + // cached file has expired + // if the file has fully downloaded again or has not been modified + if (result.isFullDownload || result.notModified) { cache.close(); // close the cache file before opening it. - httpClient.close(); - return await PdfDocument.openFile( + httpClientWrapper.reset(); + return await entryFunctions.openFile( cache.filePath, passwordProvider: passwordProvider, firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, ); } } } - return await PdfDocument.openCustom( + return await entryFunctions.openCustom( read: (buffer, position, size) async { final totalSize = size; final end = position + size; - int bufferPosition = 0; - for (int p = position; p < end;) { + var bufferPosition = 0; + for (var p = position; p < end;) { final blockId = p ~/ cache!.blockSize; final isAvailable = cache.isCached(blockId); if (!isAvailable) { - await _downloadBlock(httpClient, uri, cache, progressCallback, blockId, headers: headers); + await _downloadBlock( + httpClientWrapper, + uri, + cache, + progressCallback, + blockId, + headers: headers, + timeout: timeout, + ); } final readEnd = min(p + size, (blockId + 1) * cache.blockSize); final sizeToRead = readEnd - p; @@ -328,19 +388,23 @@ Future pdfDocumentFromUri( }, passwordProvider: passwordProvider, firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, fileSize: cache.fileSize, sourceName: uri.toString(), onDispose: () { cache!.close(); - httpClient.close(); + httpClientWrapper.reset(); }, ); } catch (e) { + if (e is PdfException && e.errorCode == pdfium_bindings.FPDF_ERR_FORMAT) { + // the file seems broken; delete the cache file. + // NOTE: the trick does not work on Windows :( + await cache.deleteCacheFile(); + } cache.close(); - httpClient.close(); + httpClientWrapper.reset(); rethrow; - } finally { - report(); } } @@ -363,7 +427,7 @@ class _DownloadResult { // Download blocks of the file and cache the data to file. Future<_DownloadResult> _downloadBlock( - http.Client httpClient, + _HttpClientWrapper httpClientWrapper, Uri uri, PdfFileCache cache, PdfDownloadProgressCallback? progressCallback, @@ -372,18 +436,27 @@ Future<_DownloadResult> _downloadBlock( bool addCacheControlHeaders = false, bool useRangeAccess = true, Map? headers, -}) async { + Duration? timeout, +}) => httpClientWrapper.synchronized(() async { int? fileSize; final blockOffset = blockId * cache.blockSize; final end = blockOffset + cache.blockSize * blockCount; - final request = http.Request('GET', uri) ..headers.addAll({ if (useRangeAccess) 'Range': 'bytes=$blockOffset-${end - 1}', if (addCacheControlHeaders) ...cache.cacheControlState.getHeadersForFetch(), if (headers != null) ...headers, }); - final response = await httpClient.send(request); + late final http.StreamedResponse response; + try { + response = await httpClientWrapper.client.send(request).timeout(timeout ?? const Duration(seconds: 5)); + } on TimeoutException { + httpClientWrapper.reset(); + rethrow; + } catch (e) { + httpClientWrapper.reset(); + throw PdfException('Failed to download PDF file: $e'); + } if (response.statusCode == 304) { return _DownloadResult(cache.fileSize, false, true); } @@ -397,8 +470,8 @@ Future<_DownloadResult> _downloadBlock( } final contentRange = response.headers['content-range']; - bool isFullDownload = false; - if (response.statusCode == 206 && contentRange != null) { + var isFullDownload = false; + if (contentRange != null) { final m = RegExp(r'bytes (\d+)-(\d+)/(\d+)').firstMatch(contentRange); fileSize = int.parse(m!.group(3)!); } else { @@ -421,11 +494,18 @@ Future<_DownloadResult> _downloadBlock( } if (isFullDownload) { - fileSize ??= cachedBytesSoFar; + if (fileSize != null) { + if (fileSize != cache.fileSize) { + throw PdfException('File size mismatch after full download: expected $fileSize, got ${cache.fileSize}'); + } + } else { + fileSize = cachedBytesSoFar; + } + await cache.initializeWithFileSize(fileSize, truncateExistingContent: false); await cache.setCached(0, lastBlock: cache.totalBlocks - 1); } else { await cache.setCached(blockId, lastBlock: blockId + blockCount - 1); } return _DownloadResult(fileSize!, isFullDownload, false); -} +}); diff --git a/packages/pdfrx_engine/lib/src/native/pdfium.dart b/packages/pdfrx_engine/lib/src/native/pdfium.dart new file mode 100644 index 00000000..a1cf803c --- /dev/null +++ b/packages/pdfrx_engine/lib/src/native/pdfium.dart @@ -0,0 +1,41 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:ffi'; +import 'dart:io'; + +import 'package:pdfium_dart/pdfium_dart.dart' as pdfium_bindings; + +import '../pdfrx.dart'; + +/// Get the module file name for pdfium. +String _getModuleFileName() { + if (Platform.isAndroid) return 'libpdfium.so'; + if (Platform.isWindows) return 'pdfium.dll'; + if (Platform.isLinux) { + return '${File(Platform.resolvedExecutable).parent.path}/lib/libpdfium.so'; + } + throw UnsupportedError('Unsupported platform'); +} + +DynamicLibrary _getModule() { + // If the module path is explicitly specified, use it. + if (Pdfrx.pdfiumModulePath != null) { + return DynamicLibrary.open(Pdfrx.pdfiumModulePath!); + } + // For iOS/macOS, we assume pdfium is already loaded (or statically linked) in the process. + if (Platform.isIOS || Platform.isMacOS) { + return DynamicLibrary.process(); + } + return DynamicLibrary.open(_getModuleFileName()); +} + +pdfium_bindings.PDFium? _pdfium; + +/// Loaded PDFium module. +pdfium_bindings.PDFium get pdfium { + _pdfium ??= pdfium_bindings.PDFium(_getModule()); + return _pdfium!; +} + +set pdfium(pdfium_bindings.PDFium value) { + _pdfium = value; +} diff --git a/packages/pdfrx_engine/lib/src/native/pdfium_file_access.dart b/packages/pdfrx_engine/lib/src/native/pdfium_file_access.dart new file mode 100644 index 00000000..8e6eb769 --- /dev/null +++ b/packages/pdfrx_engine/lib/src/native/pdfium_file_access.dart @@ -0,0 +1,92 @@ +import 'dart:async'; +import 'dart:ffi'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'pthread/file_access.dart'; +import 'win32/file_access.dart'; + +class PdfiumFileAccess { + PdfiumFileAccess._(); + + /// Creates a file access structure for PDFium with the provided read function. + static Future create( + int fileSize, + FutureOr Function(Uint8List buffer, int position, int size) read, + ) async { + final fa = PdfiumFileAccess._(); + void readAndSignal(int position, Pointer buffer, int size) async { + try { + final readSize = await read(buffer.asTypedList(size), position, size); + PdfiumFileAccessHelper.instance.setValue(fa.fileAccess, readSize); + } catch (e) { + PdfiumFileAccessHelper.instance.setValue(fa.fileAccess, -1); + } + } + + fa._nativeCallable = _NativeFileReadCallable.listener(readAndSignal); + fa.fileAccess = await PdfiumFileAccessHelper.instance.create(fileSize, fa._nativeCallable.nativeFunction.address); + return fa; + } + + /// Disposes the file access structure and associated resources. + void dispose() { + PdfiumFileAccessHelper.instance.destroy(fileAccess); + _nativeCallable.close(); + } + + /// Address of `FPDF_FILEACCESS` structure. + late final int fileAccess; + late final _NativeFileReadCallable _nativeCallable; +} + +typedef _NativeFileReadCallable = NativeCallable, IntPtr)>; + +/// Abstract interface for platform-specific file access implementations. +/// +/// This provides a bridge between Dart and native code for PDF file access operations, +/// using platform-specific synchronization primitives (pthread on Unix-like systems, +/// Windows synchronization objects on Windows). +abstract class PdfiumFileAccessHelper { + /// Creates a file access structure for PDFium. + /// + /// Parameters: + /// - [fileSize]: Total size of the file in bytes + /// - [readBlock]: Function pointer to the read callback + /// + /// Returns the address of the allocated structure. + Future create(int fileSize, int readBlock); + + /// Destroys a file access structure and frees associated resources. + /// + /// Parameters: + /// - [faAddress]: Address of the file access structure to destroy + void destroy(int faAddress); + + /// Sets the return value and signals the waiting thread. + /// + /// This is called from Dart after completing an async read operation + /// to unblock the native thread waiting for data. + /// + /// Parameters: + /// - [faAddress]: Address of the file access structure + /// - [value]: Return value to set (typically 1 for success, 0 for failure) + void setValue(int faAddress, int value); + + /// Gets the singleton instance for the current platform. + static PdfiumFileAccessHelper get instance { + // ignore: prefer_conditional_expression + if (_instance == null) { + if (Platform.isWindows) { + _instance = PdfiumFileAccessHelperWin32(); + } else if (Platform.isAndroid || Platform.isLinux || Platform.isIOS || Platform.isMacOS) { + _instance = PdfiumFileAccessHelperPthread(); + } else { + throw UnsupportedError('PdfiumFileAccessHelper is not implemented for this platform.'); + } + } + return _instance!; + } + + static PdfiumFileAccessHelper? _instance; +} diff --git a/packages/pdfrx_engine/lib/src/native/pdfium_file_write.dart b/packages/pdfrx_engine/lib/src/native/pdfium_file_write.dart new file mode 100644 index 00000000..9f12321a --- /dev/null +++ b/packages/pdfrx_engine/lib/src/native/pdfium_file_write.dart @@ -0,0 +1,65 @@ +import 'dart:async'; +import 'dart:ffi'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'pthread/file_write.dart'; +import 'win32/file_write.dart'; + +class PdfiumFileWrite { + PdfiumFileWrite._(); + + /// Creates a file write structure for PDFium with the provided write function. + static Future create(FutureOr Function(Uint8List buffer, int position, int size) write) async { + final fw = PdfiumFileWrite._(); + void writeAndSignal(Pointer buffer, int position, int size) async { + try { + final writtenSize = await write(buffer.asTypedList(size), position, size); + PdfiumFileWriteHelper.instance.setValue(fw.fileWrite, writtenSize); + } catch (e) { + PdfiumFileWriteHelper.instance.setValue(fw.fileWrite, -1); + } + } + + fw._nativeCallable = _NativeFileWriteCallable.listener(writeAndSignal); + fw.fileWrite = await PdfiumFileWriteHelper.instance.create(fw._nativeCallable.nativeFunction.address); + return fw; + } + + /// Disposes the file write structure and associated resources. + void dispose() { + PdfiumFileWriteHelper.instance.destroy(fileWrite); + _nativeCallable.close(); + } + + /// Address of `FPDF_FILEWRITE` structure. + late final int fileWrite; + late final _NativeFileWriteCallable _nativeCallable; +} + +typedef _NativeFileWriteCallable = NativeCallable, IntPtr, IntPtr)>; + +/// Abstract interface for platform-specific file write implementations. +abstract class PdfiumFileWriteHelper { + Future create(int writeBlock); + + void destroy(int fwAddress); + + void setValue(int fwAddress, int value); + + static PdfiumFileWriteHelper get instance { + // ignore: prefer_conditional_expression + if (_instance == null) { + if (Platform.isWindows) { + _instance = PdfiumFileWriteHelperWin32(); + } else if (Platform.isAndroid || Platform.isLinux || Platform.isIOS || Platform.isMacOS) { + _instance = PdfiumFileWriteHelperPthread(); + } else { + throw UnsupportedError('PdfiumFileWriteHelper is not implemented for this platform.'); + } + } + return _instance!; + } + + static PdfiumFileWriteHelper? _instance; +} diff --git a/packages/pdfrx_engine/lib/src/native/pdfrx_pdfium.dart b/packages/pdfrx_engine/lib/src/native/pdfrx_pdfium.dart new file mode 100644 index 00000000..af1a420b --- /dev/null +++ b/packages/pdfrx_engine/lib/src/native/pdfrx_pdfium.dart @@ -0,0 +1,1628 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:async'; +import 'dart:collection'; +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:io'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:crypto/crypto.dart'; +import 'package:ffi/ffi.dart'; +import 'package:pdfium_dart/pdfium_dart.dart' as pdfium_bindings; +import 'package:rxdart/rxdart.dart'; +import 'package:synchronized/extension.dart'; + +import '../pdf_annotation.dart'; +import '../pdf_datetime.dart'; +import '../pdf_dest.dart'; +import '../pdf_document.dart'; +import '../pdf_document_event.dart'; +import '../pdf_exception.dart'; +import '../pdf_font_query.dart'; +import '../pdf_image.dart'; +import '../pdf_link.dart'; +import '../pdf_outline_node.dart'; +import '../pdf_page.dart'; +import '../pdf_page_proxies.dart'; +import '../pdf_page_status_change.dart'; +import '../pdf_permissions.dart'; +import '../pdf_rect.dart'; +import '../pdf_text.dart'; +import '../pdfrx.dart'; +import '../pdfrx_entry_functions.dart'; +import '../utils/shuffle_in_place.dart'; +import 'native_utils.dart'; +import 'pdf_file_cache.dart'; +import 'pdfium.dart'; +import 'pdfium_file_access.dart'; +import 'worker.dart'; + +Directory? _appLocalFontPath; + +bool _initialized = false; +final _initSync = Object(); + +/// Initializes PDFium library. +Future _init() async { + if (_initialized) return; + await _initSync.synchronized(() async { + if (_initialized) return; + + _appLocalFontPath = await getCacheDirectory('pdfrx.fonts'); + + BackgroundWorker.computeWithArena((arena, params) { + final config = arena(); + config.ref.version = 2; + + final fontPaths = [?params.appLocalFontPath?.path, ...params.fontPaths]; + if (fontPaths.isNotEmpty) { + // NOTE: m_pUserFontPaths must not be freed until FPDF_DestroyLibrary is called; on pdfrx, it's never freed. + final fontPathArray = malloc>(fontPaths.length + 1); + for (var i = 0; i < fontPaths.length; i++) { + fontPathArray[i] = fontPaths[i] + .toNativeUtf8() + .cast(); // NOTE: the block allocated by toNativeUtf8 never released + } + fontPathArray[fontPaths.length] = nullptr; + config.ref.m_pUserFontPaths = fontPathArray; + } else { + config.ref.m_pUserFontPaths = nullptr; + } + + config.ref.m_pIsolate = nullptr; + config.ref.m_v8EmbedderSlot = 0; + pdfium.FPDF_InitLibraryWithConfig(config); + _initialized = true; + }, (appLocalFontPath: _appLocalFontPath, fontPaths: Pdfrx.fontPaths)); + }); + + await _initializeFontEnvironment(); +} + +Future _deinit() async { + await BackgroundWorker.compute((params) { + pdfium.FPDF_DestroyLibrary(); + }, {}); + await BackgroundWorker.stop(); + _mapFont?.close(); + _mapFont = null; + _lastMissingFonts.clear(); + _initialized = false; +} + +/// Stores the fonts that were not found during mapping. +/// NOTE: This is used by [BackgroundWorker] and should not be used directly; use [_getAndClearMissingFonts] instead. +final _lastMissingFonts = {}; + +/// MapFont function used by PDFium to map font requests to system fonts. +/// NOTE: This is used by [BackgroundWorker] and should not be used directly. +NativeCallable< + Pointer Function( + Pointer, + Int, + pdfium_bindings.FPDF_BOOL, + Int, + Int, + Pointer, + Pointer, + ) +>? +_mapFont; + +/// Setup the system font info in PDFium. +Future _initializeFontEnvironment() async { + await BackgroundWorker.computeWithArena((arena, params) { + // kBase14FontNames + const fontNamesToIgnore = { + 'Courier': true, + 'Courier-Bold': true, + 'Courier-BoldOblique': true, + 'Courier-Oblique': true, + 'Helvetica': true, + 'Helvetica-Bold': true, + 'Helvetica-BoldOblique': true, + 'Helvetica-Oblique': true, + 'Times-Roman': true, + 'Times-Bold': true, + 'Times-BoldItalic': true, + 'Times-Italic': true, + 'Symbol': true, + 'ZapfDingbats': true, + }; + + final sysFontInfoBuffer = pdfium.FPDF_GetDefaultSystemFontInfo(); + final mapFontOriginal = sysFontInfoBuffer.ref.MapFont + .asFunction< + Pointer Function( + Pointer, + int, + int, + int, + int, + Pointer, + Pointer, + ) + >(); + + _mapFont?.close(); + _mapFont = + NativeCallable< + Pointer Function( + Pointer, + Int, + pdfium_bindings.FPDF_BOOL, + Int, + Int, + Pointer, + Pointer, + ) + >.isolateLocal(( + Pointer sysFontInfo, + int weight, + int italic, + int charset, + int pitchFamily, + Pointer face, + Pointer bExact, + ) { + final result = mapFontOriginal(sysFontInfo, weight, italic, charset, pitchFamily, face, bExact); + if (result.address == 0) { + final faceName = face.cast().toDartString(); + if (!fontNamesToIgnore.containsKey(faceName)) { + _lastMissingFonts[faceName] = PdfFontQuery( + face: faceName, + weight: weight, + isItalic: italic != 0, + charset: PdfFontCharset.fromPdfiumCharsetId(charset), + pitchFamily: pitchFamily, + ); + } + } + return result; + }); + + sysFontInfoBuffer.ref.MapFont = _mapFont!.nativeFunction; + + // when registering a new SetSystemFontInfo, the previous one is automatically released + // and the only last one remains on memory + pdfium.FPDF_SetSystemFontInfo(sysFontInfoBuffer); + }, {}); +} + +/// Retrieve and clear the last missing fonts from [_lastMissingFonts] in a thread-safe manner. +Future> _getAndClearMissingFonts() async { + return await BackgroundWorker.compute((params) { + final fonts = _lastMissingFonts.values.toList(); + _lastMissingFonts.clear(); + return fonts; + }, null); +} + +class PdfrxEntryFunctionsImpl implements PdfrxEntryFunctions { + PdfrxEntryFunctionsImpl(); + + @override + Future init() => _init(); + + @override + Future suspendPdfiumWorkerDuringAction(FutureOr Function() action) async { + return await BackgroundWorker.suspendDuringAction(action); + } + + @override + Future compute(FutureOr Function(M message) callback, M message) async { + return await BackgroundWorker.compute(callback, message); + } + + @override + Future stopBackgroundWorker() => _deinit(); + + @override + Future openAsset( + String name, { + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, + }) async { + if (Pdfrx.loadAsset == null) { + throw StateError('Pdfrx.loadAsset is not set. Please set it to load assets.'); + } + final asset = await Pdfrx.loadAsset!(name); + return await _openData( + asset.buffer.asUint8List(), + 'asset:$name', + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + maxSizeToCacheOnMemory: null, + onDispose: null, + ); + } + + @override + Future openData( + Uint8List data, { + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + String? sourceName, + bool allowDataOwnershipTransfer = false, // just ignored + bool useProgressiveLoading = false, + void Function()? onDispose, + }) => _openData( + data, + sourceName ?? _sourceNameFromData(data), + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + maxSizeToCacheOnMemory: null, + onDispose: onDispose, + ); + + /// Generates a pseudo-unique source name for the given data using its SHA-256 hash. + /// + /// This may be sometimes slow for large data, so it's better to provide a meaningful source name when possible. + static String _sourceNameFromData(Uint8List data) { + return 'data%${sha256.convert(data)}'; + } + + @override + Future openFile( + String filePath, { + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, + }) async { + await _init(); + return _openByFunc( + (password) async => BackgroundWorker.computeWithArena((arena, params) { + final doc = pdfium.FPDF_LoadDocument(params.filePath.toUtf8(arena), params.password?.toUtf8(arena) ?? nullptr); + return doc.address; + }, (filePath: filePath, password: password)), + sourceName: 'file%$filePath', + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + ); + } + + Future _openData( + Uint8List data, + String sourceName, { + required PdfPasswordProvider? passwordProvider, + required bool firstAttemptByEmptyPassword, + required bool useProgressiveLoading, + required int? maxSizeToCacheOnMemory, + required void Function()? onDispose, + }) { + return openCustom( + read: (buffer, position, size) { + if (position + size > data.length) { + size = data.length - position; + if (size < 0) return -1; + } + for (var i = 0; i < size; i++) { + buffer[i] = data[position + i]; + } + return size; + }, + fileSize: data.length, + sourceName: sourceName, + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + maxSizeToCacheOnMemory: maxSizeToCacheOnMemory, + onDispose: onDispose, + ); + } + + @override + Future openCustom({ + required FutureOr Function(Uint8List buffer, int position, int size) read, + required int fileSize, + required String sourceName, + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, + int? maxSizeToCacheOnMemory, + void Function()? onDispose, + }) async { + await _init(); + + maxSizeToCacheOnMemory ??= 1024 * 1024; // the default is 1MB + + // If the file size is smaller than the specified size, load the file on memory + if (fileSize <= maxSizeToCacheOnMemory) { + final buffer = malloc(fileSize); + try { + await read(buffer.asTypedList(fileSize), 0, fileSize); + return _openByFunc( + (password) async => BackgroundWorker.computeWithArena( + (arena, params) => pdfium.FPDF_LoadMemDocument( + Pointer.fromAddress(params.buffer), + params.fileSize, + params.password?.toUtf8(arena) ?? nullptr, + ).address, + (buffer: buffer.address, fileSize: fileSize, password: password), + ), + sourceName: sourceName, + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + disposeCallback: () { + try { + onDispose?.call(); + } finally { + malloc.free(buffer); + } + }, + ); + } catch (e) { + malloc.free(buffer); + rethrow; + } + } + + // Otherwise, load the file on demand + final fa = await PdfiumFileAccess.create(fileSize, read); + try { + return _openByFunc( + (password) async => BackgroundWorker.computeWithArena( + (arena, params) => pdfium.FPDF_LoadCustomDocument( + Pointer.fromAddress(params.fileAccess), + params.password?.toUtf8(arena) ?? nullptr, + ).address, + (fileAccess: fa.fileAccess, password: password), + ), + sourceName: sourceName, + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + disposeCallback: () { + try { + onDispose?.call(); + } finally { + fa.dispose(); + } + }, + ); + } catch (e) { + fa.dispose(); + rethrow; + } + } + + @override + Future openUri( + Uri uri, { + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, + PdfDownloadProgressCallback? progressCallback, + bool preferRangeAccess = false, + Map? headers, + bool withCredentials = false, + Duration? timeout, + }) => pdfDocumentFromUri( + uri, + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + progressCallback: progressCallback, + useRangeAccess: preferRangeAccess, + headers: headers, + timeout: timeout, + entryFunctions: this, + ); + + static Future _openByFunc( + FutureOr Function(String? password) openPdfDocument, { + required String sourceName, + required PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, + void Function()? disposeCallback, + }) async { + for (var i = 0; ; i++) { + final String? password; + if (firstAttemptByEmptyPassword && i == 0) { + password = null; + } else { + password = await passwordProvider?.call(); + if (password == null) { + throw const PdfPasswordException('No password supplied by PasswordProvider.'); + } + } + final doc = await openPdfDocument(password); + if (doc != 0) { + return _PdfDocumentPdfium.fromPdfDocument( + pdfium_bindings.FPDF_DOCUMENT.fromAddress(doc), + sourceName: sourceName, + useProgressiveLoading: useProgressiveLoading, + disposeCallback: disposeCallback, + ); + } + final error = pdfium.FPDF_GetLastError(); + if (Platform.isWindows || error == pdfium_bindings.FPDF_ERR_PASSWORD) { + // FIXME: Windows does not return error code correctly; we have to mimic every error is password error + continue; + } + throw PdfException('Failed to load PDF document ${_getPdfiumErrorString()}.', error); + } + } + + @override + Future createNew({required String sourceName}) async { + await _init(); + final doc = await BackgroundWorker.compute((params) { + return pdfium.FPDF_CreateNewDocument().address; + }, null); + return _PdfDocumentPdfium.fromPdfDocument( + pdfium_bindings.FPDF_DOCUMENT.fromAddress(doc), + sourceName: sourceName, + useProgressiveLoading: false, + disposeCallback: null, + ); + } + + @override + Future createFromJpegData( + Uint8List jpegData, { + required double width, + required double height, + required String sourceName, + }) async { + await _init(); + final dataBuffer = malloc(jpegData.length); + try { + dataBuffer.asTypedList(jpegData.length).setAll(0, jpegData); + final doc = await BackgroundWorker.computeWithArena( + (arena, params) { + final document = pdfium.FPDF_CreateNewDocument(); + final newPage = pdfium.FPDFPage_New(document, 0, params.width, params.height); + final newPages = arena(); + newPages.value = newPage; + + final imageObj = pdfium.FPDFPageObj_NewImageObj(document); + + final fa = _FileAccess.fromDataBuffer(Pointer.fromAddress(dataBuffer.address), jpegData.length); + pdfium.FPDFImageObj_LoadJpegFileInline(newPages, 1, imageObj, fa.fileAccess); + fa.dispose(); + + pdfium.FPDFImageObj_SetMatrix(imageObj, params.width, 0, 0, params.height, 0, 0); + pdfium.FPDFPage_InsertObject(newPage, imageObj); // image is now owned by the page + + pdfium.FPDFPage_GenerateContent(newPage); + pdfium.FPDF_ClosePage(newPage); + return document.address; + }, + ( + dataBuffer: dataBuffer.address, + dataLength: jpegData.length, + width: width, + height: height, + sourceName: sourceName, + ), + ); + return _PdfDocumentPdfium.fromPdfDocument( + pdfium_bindings.FPDF_DOCUMENT.fromAddress(doc), + sourceName: sourceName, + useProgressiveLoading: false, + disposeCallback: null, + ); + } finally { + malloc.free(dataBuffer); + } + } + + static String _getPdfiumErrorString([int? error]) { + error ??= pdfium.FPDF_GetLastError(); + final errStr = _errorMappings[error]; + if (errStr != null) { + return '($errStr: $error)'; + } + return '(FPDF_GetLastError=$error)'; + } + + static final _errorMappings = { + 0: 'FPDF_ERR_SUCCESS', + 1: 'FPDF_ERR_UNKNOWN', + 2: 'FPDF_ERR_FILE', + 3: 'FPDF_ERR_FORMAT', + 4: 'FPDF_ERR_PASSWORD', + 5: 'FPDF_ERR_SECURITY', + 6: 'FPDF_ERR_PAGE', + 7: 'FPDF_ERR_XFALOAD', + 8: 'FPDF_ERR_XFALAYOUT', + }; + + @override + Future reloadFonts() async { + await _initializeFontEnvironment(); + } + + @override + Future addFontData({required String face, required Uint8List data}) async { + await _appLocalFontPath!.create(recursive: true); + final name = base64Encode(utf8.encode(face)); + final file = File('${_appLocalFontPath!.path}/$name.ttf'); + await file.writeAsBytes(data); + stderr.writeln('Added font data: $face (${data.length} bytes) at ${file.path}'); + } + + @override + Future clearAllFontData() async { + try { + await _appLocalFontPath!.delete(recursive: true); + } catch (e) { + // ignored + } + } + + @override + PdfrxBackendType get backendType => PdfrxBackendType.pdfium; +} + +extension _FpdfUtf8StringExt on String { + Pointer toUtf8(Arena arena) => Pointer.fromAddress(toNativeUtf8(allocator: arena).address); +} + +class _PdfDocumentPdfium extends PdfDocument { + final pdfium_bindings.FPDF_DOCUMENT document; + final void Function()? disposeCallback; + final int securityHandlerRevision; + final pdfium_bindings.FPDF_FORMHANDLE formHandle; + final Pointer formInfo; + bool isDisposed = false; + final subject = BehaviorSubject(); + + @override + bool get isEncrypted => securityHandlerRevision != -1; + @override + final PdfPermissions? permissions; + + @override + Stream get events => subject.stream; + + _PdfDocumentPdfium._( + this.document, { + required super.sourceName, + required this.securityHandlerRevision, + required this.permissions, + required this.formHandle, + required this.formInfo, + this.disposeCallback, + }); + + static Future fromPdfDocument( + pdfium_bindings.FPDF_DOCUMENT doc, { + required String sourceName, + required bool useProgressiveLoading, + required void Function()? disposeCallback, + }) async { + if (doc == nullptr) { + throw const PdfException('Failed to load PDF document.'); + } + _PdfDocumentPdfium? pdfDoc; + try { + final result = await BackgroundWorker.computeWithArena((arena, docAddress) { + final doc = pdfium_bindings.FPDF_DOCUMENT.fromAddress(docAddress); + Pointer formInfo = nullptr; + pdfium_bindings.FPDF_FORMHANDLE formHandle = nullptr; + try { + final permissions = pdfium.FPDF_GetDocPermissions(doc); + final securityHandlerRevision = pdfium.FPDF_GetSecurityHandlerRevision(doc); + + formInfo = calloc(); + formInfo.ref.version = 1; + formHandle = pdfium.FPDFDOC_InitFormFillEnvironment(doc, formInfo); + return ( + permissions: permissions, + securityHandlerRevision: securityHandlerRevision, + formHandle: formHandle.address, + formInfo: formInfo.address, + ); + } catch (e) { + pdfium.FPDFDOC_ExitFormFillEnvironment(formHandle); + calloc.free(formInfo); + rethrow; + } + }, doc.address); + + pdfDoc = _PdfDocumentPdfium._( + doc, + sourceName: sourceName, + securityHandlerRevision: result.securityHandlerRevision, + permissions: result.securityHandlerRevision != -1 + ? PdfPermissions(result.permissions, result.securityHandlerRevision) + : null, + formHandle: pdfium_bindings.FPDF_FORMHANDLE.fromAddress(result.formHandle), + formInfo: Pointer.fromAddress(result.formInfo), + disposeCallback: disposeCallback, + ); + + final pages = await pdfDoc._loadPagesInLimitedTime( + maxPageCountToLoadAdditionally: useProgressiveLoading ? 1 : null, + ); + pdfDoc._pages = List.unmodifiable(pages.pages); + if (!useProgressiveLoading) { + pdfDoc._notifyDocumentLoadComplete(); + } + pdfDoc._notifyMissingFonts(); + return pdfDoc; + } catch (e) { + pdfDoc?.dispose(); + rethrow; + } + } + + /// Notify missing fonts by sending [PdfDocumentMissingFontsEvent]. + Future _notifyMissingFonts() async { + final lastMissingFonts = await _getAndClearMissingFonts(); + if (lastMissingFonts.isNotEmpty) { + subject.add(PdfDocumentMissingFontsEvent(this, lastMissingFonts)); + } + } + + @override + Future loadPagesProgressively({ + PdfPageLoadingCallback? onPageLoadProgress, + T? data, + Duration loadUnitDuration = const Duration(milliseconds: 250), + }) async { + for (;;) { + if (isDisposed) return; + + final firstUnloadedPageIndex = _pages.indexWhere((p) => !p.isLoaded); + if (firstUnloadedPageIndex == -1) { + // All pages are already loaded + return; + } + + final loaded = await _loadPagesInLimitedTime( + pagesLoadedSoFar: _pages.sublist(0, firstUnloadedPageIndex).toList(), + timeout: loadUnitDuration, + ); + if (isDisposed) return; + pages = loaded.pages; + + if (onPageLoadProgress != null) { + final result = await onPageLoadProgress(loaded.pageCountLoadedTotal, loaded.pages.length, data); + if (result == false) { + // If the callback returns false, stop loading pages + return; + } + } + if (loaded.pageCountLoadedTotal == loaded.pages.length) { + _notifyDocumentLoadComplete(); + return; + } + if (isDisposed) { + return; + } + } + } + + void _notifyDocumentLoadComplete() { + subject.add(PdfDocumentLoadCompleteEvent(this)); + } + + /// Loads pages in the document in a time-limited manner. + Future<({List pages, int pageCountLoadedTotal})> _loadPagesInLimitedTime({ + List pagesLoadedSoFar = const [], + int? maxPageCountToLoadAdditionally, + Duration? timeout, + }) async { + try { + final results = await BackgroundWorker.computeWithArena( + (arena, params) { + final doc = pdfium_bindings.FPDF_DOCUMENT.fromAddress(params.docAddress); + final pageCount = pdfium.FPDF_GetPageCount(doc); + final end = maxPageCountToLoadAdditionally == null + ? pageCount + : min(pageCount, params.pagesCountLoadedSoFar + params.maxPageCountToLoadAdditionally!); + final t = params.timeoutUs != null ? (Stopwatch()..start()) : null; + final pages = <({double width, double height, int rotation, double bbLeft, double bbBottom})>[]; + for (var i = params.pagesCountLoadedSoFar; i < end; i++) { + final page = pdfium.FPDF_LoadPage(doc, i); + try { + final rect = arena(); + pdfium.FPDF_GetPageBoundingBox(page, rect); + pages.add(( + width: pdfium.FPDF_GetPageWidthF(page), + height: pdfium.FPDF_GetPageHeightF(page), + rotation: pdfium.FPDFPage_GetRotation(page), + bbLeft: rect.ref.left.toDouble(), + bbBottom: rect.ref.bottom.toDouble(), + )); + } finally { + pdfium.FPDF_ClosePage(page); + } + if (t != null && t.elapsedMicroseconds > params.timeoutUs!) { + break; + } + } + return (pages: pages, totalPageCount: pageCount); + }, + ( + docAddress: document.address, + pagesCountLoadedSoFar: pagesLoadedSoFar.length, + maxPageCountToLoadAdditionally: maxPageCountToLoadAdditionally, + timeoutUs: timeout?.inMicroseconds, + ), + ); + + final pages = [...pagesLoadedSoFar]; + for (var i = 0; i < results.pages.length; i++) { + final pageData = results.pages[i]; + pages.add( + _PdfPagePdfium._( + document: this, + pageNumber: pages.length + 1, + width: pageData.width, + height: pageData.height, + rotation: PdfPageRotation.values[pageData.rotation], + bbLeft: pageData.bbLeft, + bbBottom: pageData.bbBottom, + isLoaded: true, + ), + ); + } + final pageCountLoadedTotal = pages.length; + if (pageCountLoadedTotal > 0) { + final last = pages.last; + for (var i = pages.length; i < results.totalPageCount; i++) { + pages.add( + _PdfPagePdfium._( + document: this, + pageNumber: pages.length + 1, + width: last.width, + height: last.height, + rotation: last.rotation, + bbLeft: 0, + bbBottom: 0, + isLoaded: false, + ), + ); + } + } + return (pages: pages, pageCountLoadedTotal: pageCountLoadedTotal); + } catch (e) { + rethrow; + } + } + + @override + Future reloadPages({List? pageNumbersToReload}) async { + try { + final results = await BackgroundWorker.computeWithArena((arena, params) { + final doc = pdfium_bindings.FPDF_DOCUMENT.fromAddress(params.docAddress); + final pageCount = pdfium.FPDF_GetPageCount(doc); + if (params.pageNumbersToReload != null) { + for (final pageNumber in params.pageNumbersToReload!) { + if (pageNumber < 1 || pageNumber > pageCount) { + throw ArgumentError('Invalid page number to reload: $pageNumber', 'pageNumbersToReload'); + } + } + } + + final pageNumbersToLoad = SplayTreeSet.from(params.pageNumbersToReload ?? []); + pageNumbersToLoad.addAll( + Iterable.generate(pageCount - params.currentPageCount, (index) => params.currentPageCount + index + 1), + ); + + final pages = <({int pageIndex, double width, double height, int rotation, double bbLeft, double bbBottom})>[]; + for (final pageNumber in pageNumbersToLoad) { + final page = pdfium.FPDF_LoadPage(doc, pageNumber - 1); + try { + final rect = arena(); + pdfium.FPDF_GetPageBoundingBox(page, rect); + pages.add(( + pageIndex: pageNumber - 1, + width: pdfium.FPDF_GetPageWidthF(page), + height: pdfium.FPDF_GetPageHeightF(page), + rotation: pdfium.FPDFPage_GetRotation(page), + bbLeft: rect.ref.left.toDouble(), + bbBottom: rect.ref.bottom.toDouble(), + )); + } finally { + pdfium.FPDF_ClosePage(page); + } + } + return (pages: pages); + }, (docAddress: document.address, pageNumbersToReload: pageNumbersToReload, currentPageCount: _pages.length)); + + final newPages = [..._pages]; + for (var i = 0; i < results.pages.length; i++) { + final pageData = results.pages[i]; + final newPage = _PdfPagePdfium._( + document: this, + pageNumber: i + 1, + width: pageData.width, + height: pageData.height, + rotation: PdfPageRotation.values[pageData.rotation], + bbLeft: pageData.bbLeft, + bbBottom: pageData.bbBottom, + isLoaded: true, + ); + if (i < newPages.length) { + newPages[i] = newPage; + } else { + newPages.add(newPage); + } + } + pages = newPages; + } catch (e) { + rethrow; + } + } + + @override + List get pages => _pages; + + @override + set pages(Iterable newPages) { + final pages = []; + final changes = {}; + for (final newPage in newPages) { + if (pages.length < _pages.length) { + final old = _pages[pages.length]; + if (identical(newPage, old)) { + pages.add(newPage); + continue; + } + } + + if (newPage.unwrap<_PdfPagePdfium>() == null) { + throw ArgumentError('Unsupported PdfPage instances found at [${pages.length}]', 'newPages'); + } + + final newPageNumber = pages.length + 1; + final updated = newPage.withPageNumber(newPageNumber); + pages.add(updated); + + final oldPageIndex = _pages.indexWhere((p) => identical(p, newPage)); + if (oldPageIndex != -1) { + changes[newPageNumber] = PdfPageStatusChange.moved(page: updated, oldPageNumber: oldPageIndex + 1); + } else { + changes[newPageNumber] = PdfPageStatusChange.modified(page: updated); + } + } + + _pages = List.unmodifiable(pages); + subject.add(PdfDocumentPageStatusChangedEvent(this, changes: changes)); + } + + /// Don't handle [_pages] directly unless you really understand what you're doing; use [pages] getter/setter instead. + /// + /// [pages] automatically keeps consistency and also notifies page changes. + List _pages = []; + + @override + bool isIdenticalDocumentHandle(Object? other) => + other is _PdfDocumentPdfium && document.address == other.document.address; + + @override + Future dispose() async { + if (!isDisposed) { + isDisposed = true; + subject.close(); + await BackgroundWorker.compute((params) { + final formHandle = pdfium_bindings.FPDF_FORMHANDLE.fromAddress(params.formHandle); + final formInfo = Pointer.fromAddress(params.formInfo); + pdfium.FPDFDOC_ExitFormFillEnvironment(formHandle); + calloc.free(formInfo); + + final doc = pdfium_bindings.FPDF_DOCUMENT.fromAddress(params.document); + pdfium.FPDF_CloseDocument(doc); + }, (formHandle: formHandle.address, formInfo: formInfo.address, document: document.address)); + + disposeCallback?.call(); + } + } + + @override + Future> loadOutline() async => isDisposed + ? [] + : await BackgroundWorker.computeWithArena((arena, params) { + final document = pdfium_bindings.FPDF_DOCUMENT.fromAddress(params.document); + return _getOutlineNodeSiblings(pdfium.FPDFBookmark_GetFirstChild(document, nullptr), document, arena); + }, (document: document.address)); + + static List _getOutlineNodeSiblings( + pdfium_bindings.FPDF_BOOKMARK bookmark, + pdfium_bindings.FPDF_DOCUMENT document, + Arena arena, + ) { + final siblings = []; + while (bookmark != nullptr) { + final titleBufSize = pdfium.FPDFBookmark_GetTitle(bookmark, nullptr, 0); + final titleBuf = arena.allocate(titleBufSize); + pdfium.FPDFBookmark_GetTitle(bookmark, titleBuf, titleBufSize); + siblings.add( + PdfOutlineNode( + title: titleBuf.cast().toDartString(), + dest: _pdfDestFromDest(pdfium.FPDFBookmark_GetDest(document, bookmark), document, arena), + children: _getOutlineNodeSiblings(pdfium.FPDFBookmark_GetFirstChild(document, bookmark), document, arena), + ), + ); + bookmark = pdfium.FPDFBookmark_GetNextSibling(document, bookmark); + } + return siblings; + } + + @override + Future assemble() => _DocumentPageArranger.doShufflePagesInPlace(this); + + @override + Future encodePdf({bool incremental = false, bool removeSecurity = false}) async { + await assemble(); + final byteBuffer = BytesBuilder(); + return await BackgroundWorker.computeWithArena((arena, params) { + int write(Pointer pThis, Pointer pData, int size) { + byteBuffer.add(Pointer.fromAddress(pData.address).asTypedList(size)); + return size; + } + + final nativeWriteCallable = _NativeFileWriteCallable.isolateLocal(write, exceptionalReturn: 0); + try { + final document = pdfium_bindings.FPDF_DOCUMENT.fromAddress(params.document); + final fw = arena(); + fw.ref.version = 1; + fw.ref.WriteBlock = nativeWriteCallable.nativeFunction; + final int flags; + if (params.removeSecurity) { + flags = 3; // FPDF_SAVE_NO_SECURITY(3) + } else { + flags = params.incremental ? 1 : 2; // FPDF_INCREMENTAL(1) or FPDF_NO_INCREMENTAL(2) + } + pdfium.FPDF_SaveAsCopy(document, fw, flags); + return byteBuffer.toBytes(); + } finally { + nativeWriteCallable.close(); + } + }, (document: document.address, incremental: incremental, removeSecurity: removeSecurity)); + } + + @override + Future useNativeDocumentHandle(FutureOr Function(int nativeDocumentHandle) task) async { + if (isDisposed) { + throw StateError('Document is already disposed.'); + } + return await PdfrxEntryFunctions.instance.suspendPdfiumWorkerDuringAction(() { + return task(document.address); + }); + } +} + +typedef _NativeFileWriteCallable = + NativeCallable, Pointer, UnsignedLong)>; + +class _DocumentPageArranger with ShuffleItemsInPlaceMixin { + /// Shuffle pages in place according to the current order of pages in [document]. + /// Returns true if the pages was modified. + static Future doShufflePagesInPlace(_PdfDocumentPdfium document) async { + final indices = []; + final rotations = []; + final items = {}; + var modifiedCount = 0; + for (var i = 0; i < document.pages.length; i++) { + final page = document.pages[i]; + final pdfiumPage = page.unwrap<_PdfPagePdfium>()!; + // if rotation is different, we need to modify the page + if (page.rotation.index != pdfiumPage.rotation.index) { + rotations.add(page.rotation.index); + modifiedCount++; + } else { + rotations.add(null); + } + if (page.document != document) { + // the page is from another document; need to import + final importId = -(i + 1); + indices.add(importId); + items[importId] = (document: pdfiumPage.document.document.address, pageNumber: pdfiumPage.pageNumber); + modifiedCount++; + } else { + indices.add(page.pageNumber - 1); + if (page.pageNumber - 1 != i) { + modifiedCount++; + } + } + } + if (modifiedCount == 0) { + // No changes + return false; + } + + await BackgroundWorker.computeWithArena( + (arena, params) { + final arranger = _DocumentPageArranger._( + pdfium_bindings.FPDF_DOCUMENT.fromAddress(params.document), + params.items, + ); + arranger.shuffleInPlaceAccordingToIndices(indices); + + for (var i = 0; i < params.length; i++) { + final rotation = params.rotations[i]; + if (rotation == null) continue; + final page = pdfium.FPDF_LoadPage(arranger.document, i); + pdfium.FPDFPage_SetRotation(page, rotation); + pdfium.FPDF_ClosePage(page); + } + }, + ( + document: document.document.address, + indices: indices, + rotations: rotations, + items: items, + length: document.pages.length, + ), + ); + return true; + } + + _DocumentPageArranger._(this.document, this.items); + final pdfium_bindings.FPDF_DOCUMENT document; + final Map items; + + @override + int get length => pdfium.FPDF_GetPageCount(document); + + @override + void move(int fromIndex, int toIndex, int count) { + using((arena) { + final pageIndices = arena(count); + for (var i = 0; i < count; i++) { + pageIndices[i] = fromIndex + i; + } + pdfium.FPDF_MovePages(document, pageIndices, count, toIndex); + }); + } + + @override + void remove(int index, int count) { + for (var i = count - 1; i >= 0; i--) { + pdfium.FPDFPage_Delete(document, index + i); + } + } + + @override + void duplicate(int fromIndex, int toIndex, int count) { + using((arena) { + final pageIndices = arena(count); + for (var i = 0; i < count; i++) { + pageIndices[i] = fromIndex + i; + } + pdfium.FPDF_ImportPagesByIndex(document, document, pageIndices, count, toIndex); + }); + } + + @override + void insertNew(int index, int negativeItemIndex) async { + final page = items[negativeItemIndex]!; + final src = pdfium_bindings.FPDF_DOCUMENT.fromAddress(page.document); + using((arena) { + final pageIndices = arena(); + pageIndices.value = page.pageNumber - 1; + pdfium.FPDF_ImportPagesByIndex(document, src, pageIndices, 1, index); + }); + } +} + +class _PdfPagePdfium extends PdfPage { + @override + final _PdfDocumentPdfium document; + @override + final int pageNumber; + @override + final double width; + @override + final double height; + + /// Bounding box left + final double bbLeft; + + /// Bounding box bottom + final double bbBottom; + + @override + final PdfPageRotation rotation; + + @override + final bool isLoaded; + + _PdfPagePdfium._({ + required this.document, + required this.pageNumber, + required this.width, + required this.height, + required this.rotation, + required this.bbLeft, + required this.bbBottom, + required this.isLoaded, + }); + + @override + Future render({ + int x = 0, + int y = 0, + int? width, + int? height, + double? fullWidth, + double? fullHeight, + int? backgroundColor, + PdfPageRotation? rotationOverride, + PdfAnnotationRenderingMode annotationRenderingMode = PdfAnnotationRenderingMode.annotationAndForms, + int flags = PdfPageRenderFlags.none, + PdfPageRenderCancellationToken? cancellationToken, + }) async { + if (cancellationToken != null && cancellationToken is! PdfPageRenderCancellationTokenPdfium) { + throw ArgumentError( + 'cancellationToken must be created by PdfPage.createCancellationToken().', + 'cancellationToken', + ); + } + final ct = cancellationToken as PdfPageRenderCancellationTokenPdfium?; + + fullWidth ??= this.width; + fullHeight ??= this.height; + width ??= fullWidth.toInt(); + height ??= fullHeight.toInt(); + backgroundColor ??= 0xffffffff; // white background + const rgbaSize = 4; + Pointer buffer = nullptr; + try { + buffer = malloc(width * height * rgbaSize); + final isSucceeded = await using((arena) async { + final cancelFlag = arena(); + ct?.attach(cancelFlag); + + if (cancelFlag.value || document.isDisposed) return false; + return await BackgroundWorker.compute( + (params) { + final cancelFlag = Pointer.fromAddress(params.cancelFlag); + if (cancelFlag.value) return false; + final bmp = pdfium.FPDFBitmap_CreateEx( + params.width, + params.height, + pdfium_bindings.FPDFBitmap_BGRA, + Pointer.fromAddress(params.buffer), + params.width * rgbaSize, + ); + if (bmp == nullptr) { + throw PdfException('FPDFBitmap_CreateEx(${params.width}, ${params.height}) failed.'); + } + pdfium_bindings.FPDF_PAGE page = nullptr; + try { + final doc = pdfium_bindings.FPDF_DOCUMENT.fromAddress(params.document); + page = pdfium.FPDF_LoadPage(doc, params.pageNumber - 1); + if (page == nullptr) { + throw PdfException('FPDF_LoadPage(${params.pageNumber}) failed.'); + } + pdfium.FPDFBitmap_FillRect(bmp, 0, 0, params.width, params.height, params.backgroundColor!); + + pdfium.FPDF_RenderPageBitmap( + bmp, + page, + -params.x, + -params.y, + params.fullWidth, + params.fullHeight, + params.rotation, + params.flags | + (params.annotationRenderingMode != PdfAnnotationRenderingMode.none + ? pdfium_bindings.FPDF_ANNOT + : 0), + ); + + if (params.formHandle != 0 && + params.annotationRenderingMode == PdfAnnotationRenderingMode.annotationAndForms) { + pdfium.FPDF_FFLDraw( + pdfium_bindings.FPDF_FORMHANDLE.fromAddress(params.formHandle), + bmp, + page, + -params.x, + -params.y, + params.fullWidth, + params.fullHeight, + params.rotation, + params.flags, + ); + } + return true; + } finally { + pdfium.FPDF_ClosePage(page); + pdfium.FPDFBitmap_Destroy(bmp); + } + }, + ( + document: document.document.address, + pageNumber: pageNumber, + buffer: buffer.address, + x: x, + y: y, + width: width!, + height: height!, + fullWidth: fullWidth!.toInt(), + fullHeight: fullHeight!.toInt(), + backgroundColor: backgroundColor, + rotation: rotationOverride != null ? ((rotationOverride.index - rotation.index + 4) & 3) : 0, + annotationRenderingMode: annotationRenderingMode, + flags: flags & 0xffff, // Ensure flags are within 16-bit range + formHandle: document.formHandle.address, + formInfo: document.formInfo.address, + cancelFlag: cancelFlag.address, + ), + ); + }); + + document._notifyMissingFonts(); + + if (!isSucceeded) { + return null; + } + + final resultBuffer = buffer; + buffer = nullptr; + + if ((flags & PdfPageRenderFlags.premultipliedAlpha) != 0) { + final count = width * height; + for (var i = 0; i < count; i++) { + final b = resultBuffer[i * rgbaSize]; + final g = resultBuffer[i * rgbaSize + 1]; + final r = resultBuffer[i * rgbaSize + 2]; + final a = resultBuffer[i * rgbaSize + 3]; + resultBuffer[i * rgbaSize] = b * a ~/ 255; + resultBuffer[i * rgbaSize + 1] = g * a ~/ 255; + resultBuffer[i * rgbaSize + 2] = r * a ~/ 255; + } + } + + return _PdfImagePdfium._(width: width, height: height, buffer: resultBuffer); + } catch (e) { + return null; + } finally { + malloc.free(buffer); + ct?.detach(); + } + } + + @override + PdfPageRenderCancellationTokenPdfium createCancellationToken() => PdfPageRenderCancellationTokenPdfium(this); + + @override + Future loadText() async { + if (document.isDisposed || !isLoaded) return null; + return await BackgroundWorker.computeWithArena((arena, params) { + final doubleSize = sizeOf(); + final rectBuffer = arena(4); + final doc = pdfium_bindings.FPDF_DOCUMENT.fromAddress(params.docHandle); + final page = pdfium.FPDF_LoadPage(doc, params.pageNumber - 1); + final textPage = pdfium.FPDFText_LoadPage(page); + try { + final charCount = pdfium.FPDFText_CountChars(textPage); + final sb = StringBuffer(); + final charRects = []; + for (var i = 0; i < charCount; i++) { + sb.writeCharCode(pdfium.FPDFText_GetUnicode(textPage, i)); + pdfium.FPDFText_GetCharBox( + textPage, + i, + rectBuffer, // L + rectBuffer.offset(doubleSize * 2), // R + rectBuffer.offset(doubleSize * 3), // B + rectBuffer.offset(doubleSize), // T + ); + charRects.add(_rectFromLTRBBuffer(rectBuffer, params.bbLeft, params.bbBottom)); + } + return PdfPageRawText(sb.toString(), charRects); + } finally { + pdfium.FPDFText_ClosePage(textPage); + pdfium.FPDF_ClosePage(page); + } + }, (docHandle: document.document.address, pageNumber: pageNumber, bbLeft: bbLeft, bbBottom: bbBottom)); + } + + @override + Future> loadLinks({bool compact = false, bool enableAutoLinkDetection = true}) async { + if (document.isDisposed || !isLoaded) return []; + final links = await _loadAnnotLinks(); + if (enableAutoLinkDetection) { + links.addAll(await _loadWebLinks()); + } + if (compact) { + for (var i = 0; i < links.length; i++) { + links[i] = links[i].compact(); + } + } + return List.unmodifiable(links); + } + + Future> _loadWebLinks() async => document.isDisposed + ? [] + : await BackgroundWorker.computeWithArena((arena, params) { + pdfium_bindings.FPDF_PAGE page = nullptr; + pdfium_bindings.FPDF_TEXTPAGE textPage = nullptr; + pdfium_bindings.FPDF_PAGELINK linkPage = nullptr; + try { + final document = pdfium_bindings.FPDF_DOCUMENT.fromAddress(params.document); + page = pdfium.FPDF_LoadPage(document, params.pageNumber - 1); + textPage = pdfium.FPDFText_LoadPage(page); + if (textPage == nullptr) return []; + linkPage = pdfium.FPDFLink_LoadWebLinks(textPage); + if (linkPage == nullptr) return []; + + final doubleSize = sizeOf(); + final rectBuffer = arena(4); + return List.generate(pdfium.FPDFLink_CountWebLinks(linkPage), (index) { + final rects = List.generate(pdfium.FPDFLink_CountRects(linkPage, index), (rectIndex) { + pdfium.FPDFLink_GetRect( + linkPage, + index, + rectIndex, + rectBuffer, + rectBuffer.offset(doubleSize), + rectBuffer.offset(doubleSize * 2), + rectBuffer.offset(doubleSize * 3), + ); + return _rectFromLTRBBuffer(rectBuffer, params.bbLeft, params.bbBottom); + }); + return PdfLink(rects, url: Uri.tryParse(_getLinkUrl(linkPage, index, arena))); + }); + } finally { + pdfium.FPDFLink_CloseWebLinks(linkPage); + pdfium.FPDFText_ClosePage(textPage); + pdfium.FPDF_ClosePage(page); + } + }, (document: document.document.address, pageNumber: pageNumber, bbLeft: bbLeft, bbBottom: bbBottom)); + + static String _getLinkUrl(pdfium_bindings.FPDF_PAGELINK linkPage, int linkIndex, Arena arena) { + final urlLength = pdfium.FPDFLink_GetURL(linkPage, linkIndex, nullptr, 0); + final urlBuffer = arena(urlLength); + pdfium.FPDFLink_GetURL(linkPage, linkIndex, urlBuffer, urlLength); + return urlBuffer.cast().toDartString(); + } + + static String? _getAnnotField(String fieldName, pdfium_bindings.FPDF_ANNOTATION annot, Arena arena) { + final length = pdfium.FPDFAnnot_GetStringValue( + annot, + fieldName.toNativeUtf8(allocator: arena).cast(), + nullptr, + 0, + ); + if (length <= 0) return null; + + final buffer = arena.allocate(length); + pdfium.FPDFAnnot_GetStringValue(annot, fieldName.toNativeUtf8(allocator: arena).cast(), buffer, length); + final value = buffer.cast().toDartString(); + return value.isEmpty ? null : value; + } + + static PdfAnnotation? _getAnnotationContent(pdfium_bindings.FPDF_ANNOTATION annot, Arena arena) { + final title = _getAnnotField('T', annot, arena); + final content = _getAnnotField('Contents', annot, arena); + final modDate = _getAnnotField('M', annot, arena); + final creationDate = _getAnnotField('CreationDate', annot, arena); + final subject = _getAnnotField('Subj', annot, arena); + if (title == null && content == null && modDate == null && creationDate == null && subject == null) { + return null; + } + + return PdfAnnotation( + title: title, + content: content, + modificationDate: PdfDateTime.fromPdfDateString(modDate), + creationDate: PdfDateTime.fromPdfDateString(creationDate), + subject: subject, + ); + } + + Future> _loadAnnotLinks() async => document.isDisposed + ? [] + : await BackgroundWorker.computeWithArena((arena, params) { + final document = pdfium_bindings.FPDF_DOCUMENT.fromAddress(params.document); + final page = pdfium.FPDF_LoadPage(document, params.pageNumber - 1); + try { + final count = pdfium.FPDFPage_GetAnnotCount(page); + final rectf = arena(); + final links = []; + for (var i = 0; i < count; i++) { + final annot = pdfium.FPDFPage_GetAnnot(page, i); + pdfium.FPDFAnnot_GetRect(annot, rectf); + final r = rectf.ref; + final rect = PdfRect( + r.left, + r.top > r.bottom ? r.top : r.bottom, + r.right, + r.top > r.bottom ? r.bottom : r.top, + ).translate(-params.bbLeft, -params.bbBottom); + + final annotation = _getAnnotationContent(annot, arena); + + final dest = _processAnnotDest(annot, document, arena); + if (dest != nullptr) { + links.add(PdfLink([rect], dest: _pdfDestFromDest(dest, document, arena), annotation: annotation)); + } else { + final uri = _processAnnotLink(annot, document, arena); + if (uri != null || annotation != null) { + links.add(PdfLink([rect], url: uri, annotation: annotation)); + } + } + pdfium.FPDFPage_CloseAnnot(annot); + } + return links; + } finally { + pdfium.FPDF_ClosePage(page); + } + }, (document: document.document.address, pageNumber: pageNumber, bbLeft: bbLeft, bbBottom: bbBottom)); + + static pdfium_bindings.FPDF_DEST _processAnnotDest( + pdfium_bindings.FPDF_ANNOTATION annot, + pdfium_bindings.FPDF_DOCUMENT document, + Arena arena, + ) { + final link = pdfium.FPDFAnnot_GetLink(annot); + + // firstly check the direct dest + final dest = pdfium.FPDFLink_GetDest(document, link); + if (dest != nullptr) return dest; + + final action = pdfium.FPDFLink_GetAction(link); + if (action == nullptr) return nullptr; + switch (pdfium.FPDFAction_GetType(action)) { + case pdfium_bindings.PDFACTION_GOTO: + return pdfium.FPDFAction_GetDest(document, action); + default: + return nullptr; + } + } + + static Uri? _processAnnotLink( + pdfium_bindings.FPDF_ANNOTATION annot, + pdfium_bindings.FPDF_DOCUMENT document, + Arena arena, + ) { + final link = pdfium.FPDFAnnot_GetLink(annot); + final action = pdfium.FPDFLink_GetAction(link); + if (action == nullptr) return null; + switch (pdfium.FPDFAction_GetType(action)) { + case pdfium_bindings.PDFACTION_URI: + final size = pdfium.FPDFAction_GetURIPath(document, action, nullptr, 0); + final buffer = arena.allocate(size); + pdfium.FPDFAction_GetURIPath(document, action, buffer.cast(), size); + try { + final newBuffer = buffer.toDartString(); + return Uri.tryParse(newBuffer); + } catch (e) { + return null; + } + default: + return null; + } + } + + static PdfRect _rectFromLTRBBuffer(Pointer buffer, double bbLeft, double bbBottom) { + final left = buffer[0] - bbLeft; + final top = buffer[1] - bbBottom; + final right = buffer[2] - bbLeft; + final bottom = buffer[3] - bbBottom; + return PdfRect(left, top, right, bottom); + } +} + +class PdfPageRenderCancellationTokenPdfium extends PdfPageRenderCancellationToken { + PdfPageRenderCancellationTokenPdfium(this.page); + final PdfPage page; + Pointer? _cancelFlag; + bool _canceled = false; + + @override + bool get isCanceled => _canceled; + + void attach(Pointer pointer) { + _cancelFlag = pointer; + if (_canceled) { + _cancelFlag!.value = true; + } + } + + void detach() { + _cancelFlag = null; + } + + @override + Future cancel() async { + _canceled = true; + _cancelFlag?.value = true; + } +} + +class _PdfImagePdfium extends PdfImage { + @override + final int width; + @override + final int height; + @override + Uint8List get pixels => _buffer.asTypedList(width * height * 4); + + final Pointer _buffer; + + _PdfImagePdfium._({required this.width, required this.height, required Pointer buffer}) : _buffer = buffer; + + @override + void dispose() { + malloc.free(_buffer); + } +} + +extension _PointerExt on Pointer { + Pointer offset(int offsetInBytes) => Pointer.fromAddress(address + offsetInBytes); +} + +PdfDest? _pdfDestFromDest(pdfium_bindings.FPDF_DEST dest, pdfium_bindings.FPDF_DOCUMENT document, Arena arena) { + if (dest == nullptr) return null; + final pul = arena(); + final values = arena(4); + final pageIndex = pdfium.FPDFDest_GetDestPageIndex(document, dest); + final type = pdfium.FPDFDest_GetView(dest, pul, values); + if (type != 0) { + return PdfDest(pageIndex + 1, PdfDestCommand.values[type], List.generate(pul.value, (index) => values[index])); + } + return null; +} + +/// Native callable type for `FPDF_FILEACCESS.m_GetBlock` +typedef _NativeFileReadCallable = + NativeCallable< + Int Function(Pointer param, UnsignedLong position, Pointer pBuf, UnsignedLong size) + >; + +/// Manages `FPDF_FILEACCESS` structure and its associated native callable. +class _FileAccess { + _FileAccess._(this.fileAccess, this._nativeReadCallable); + + final Pointer fileAccess; + final _NativeFileReadCallable? _nativeReadCallable; + + static _FileAccess fromDataBuffer(Pointer bufferPtr, int length) { + _NativeFileReadCallable? nativeReadCallable; + Pointer? fileAccessToRelease; + try { + final fileAccess = fileAccessToRelease = malloc(); + fileAccess.ref.m_FileLen = length; + + nativeReadCallable = _NativeFileReadCallable.isolateLocal(( + Pointer param, + int position, + Pointer pBuf, + int size, + ) { + final dataPtr = bufferPtr.offset(position); + final toCopy = min(size, length - position); + if (toCopy <= 0) { + return 0; + } + pBuf.cast().asTypedList(toCopy).setAll(0, dataPtr.cast().asTypedList(toCopy)); + return toCopy; + }, exceptionalReturn: 0); + + fileAccess.ref.m_GetBlock = nativeReadCallable.nativeFunction; + final result = _FileAccess._(fileAccess, nativeReadCallable); + nativeReadCallable = null; + fileAccessToRelease = null; + return result; + } catch (e) { + rethrow; + } finally { + nativeReadCallable?.close(); + if (fileAccessToRelease != null) { + malloc.free(fileAccessToRelease); + } + } + } + + void dispose() { + malloc.free(fileAccess); + _nativeReadCallable?.close(); + } +} diff --git a/packages/pdfrx_engine/lib/src/native/pthread/file_access.dart b/packages/pdfrx_engine/lib/src/native/pthread/file_access.dart new file mode 100644 index 00000000..8d03b2e7 --- /dev/null +++ b/packages/pdfrx_engine/lib/src/native/pthread/file_access.dart @@ -0,0 +1,89 @@ +// ignore_for_file: non_constant_identifier_names, camel_case_types, constant_identifier_names + +import 'dart:ffi'; + +import 'package:ffi/ffi.dart'; +import 'package:pdfium_dart/pdfium_dart.dart' as pdfium_bindings; + +import '../pdfium_file_access.dart'; +import '../worker.dart'; +import 'pthread.dart'; + +class PdfiumFileAccessHelperPthread implements PdfiumFileAccessHelper { + @override + Future create(int fileSize, int readBlock) async { + final buffer = malloc( + sizeOf() + sizeOfPthreadMutex + sizeOfPthreadCond + sizeOf() * 2, + ); + final fa = buffer.cast(); + fa.ref.m_FileLen = fileSize; + fa.ref.m_Param = Pointer.fromAddress(buffer.address); + fa.ref.m_GetBlock = Pointer.fromAddress(await _getReadFuncOnBackgroundWorker()); + + final readFuncPtr = Pointer.fromAddress(buffer.address + _readFuncOffset); + readFuncPtr.value = readBlock; + + pthread_mutex_init(buffer.address + _mutexOffset, 0); + pthread_cond_init(buffer.address + _condOffset, 0); + + return buffer.address; + } + + @override + void destroy(int faAddress) { + pthread_mutex_destroy(faAddress + _mutexOffset); + pthread_cond_destroy(faAddress + _condOffset); + malloc.free(Pointer.fromAddress(faAddress)); + } + + @override + void setValue(int faAddress, int value) { + pthread_mutex_lock(faAddress + _mutexOffset); + final returnValue = Pointer.fromAddress(faAddress + _retValueOffset); + returnValue.value = value; + pthread_cond_signal(faAddress + _condOffset); + pthread_mutex_unlock(faAddress + _mutexOffset); + } +} + +typedef _NativeFileReadCallable = + NativeCallable, UnsignedLong, Pointer, UnsignedLong)>; + +/// NOTE: Don't read the value of this variable directly, use [_getReadFuncOnBackgroundWorker] instead. +final _readFuncPtr = _NativeFileReadCallable.isolateLocal(_read, exceptionalReturn: 0).nativeFunction; + +/// Gets the read function pointer address on the background worker isolate. +Future _getReadFuncOnBackgroundWorker() async { + return await BackgroundWorker.compute((m) => _readFuncPtr.address, {}); +} + +int _read(Pointer param, int position, Pointer buffer, int size) { + final faAddress = param.address; + final cs = faAddress + _mutexOffset; + final cv = faAddress + _condOffset; + final readFuncPtr = Pointer.fromAddress(faAddress + _readFuncOffset); + final readFunc = Pointer, IntPtr)>>.fromAddress( + readFuncPtr.value, + ).asFunction, int)>(); + + pthread_mutex_lock(cs); + // Call Dart side read function. The call is returned immediately (it runs asynchronously) + readFunc(position, buffer, size); + // So, we should wait for Dart to signal completion + pthread_cond_wait(cv, cs); + final returnValue = Pointer.fromAddress(faAddress + _retValueOffset).value; + pthread_mutex_unlock(cs); + return returnValue; +} + +/// pthread_mutex_t offset within FPDF_FILEACCESS +final _mutexOffset = sizeOf(); + +/// pthread_cond_t offset within FPDF_FILEACCESS +final _condOffset = _mutexOffset + sizeOfPthreadMutex; + +/// read function pointer offset within FPDF_FILEACCESS +final _readFuncOffset = _condOffset + sizeOfPthreadCond; + +/// return-value offset within FPDF_FILEACCESS +final _retValueOffset = _readFuncOffset + sizeOf(); diff --git a/packages/pdfrx_engine/lib/src/native/pthread/file_write.dart b/packages/pdfrx_engine/lib/src/native/pthread/file_write.dart new file mode 100644 index 00000000..76f2ca83 --- /dev/null +++ b/packages/pdfrx_engine/lib/src/native/pthread/file_write.dart @@ -0,0 +1,87 @@ +// ignore_for_file: non_constant_identifier_names, camel_case_types, constant_identifier_names + +import 'dart:ffi'; + +import 'package:ffi/ffi.dart'; +import 'package:pdfium_dart/pdfium_dart.dart' as pdfium_bindings; + +import '../pdfium_file_write.dart'; +import '../worker.dart'; +import 'pthread.dart'; + +class PdfiumFileWriteHelperPthread implements PdfiumFileWriteHelper { + @override + Future create(int writeBlock) async { + final buffer = malloc( + sizeOf() + sizeOfPthreadMutex + sizeOfPthreadCond + sizeOf() * 2, + ); + final fw = buffer.cast(); + fw.ref.version = 1; + fw.ref.WriteBlock = Pointer.fromAddress(await _getWriteFuncOnBackgroundWorker()); + + final writeFuncPtr = Pointer.fromAddress(buffer.address + _writeFuncOffset); + writeFuncPtr.value = writeBlock; + + pthread_mutex_init(buffer.address + _mutexOffsetWrite, 0); + pthread_cond_init(buffer.address + _condOffsetWrite, 0); + + return buffer.address; + } + + @override + void destroy(int fwAddress) { + pthread_mutex_destroy(fwAddress + _mutexOffsetWrite); + pthread_cond_destroy(fwAddress + _condOffsetWrite); + malloc.free(Pointer.fromAddress(fwAddress)); + } + + @override + void setValue(int fwAddress, int value) { + pthread_mutex_lock(fwAddress + _mutexOffsetWrite); + final returnValue = Pointer.fromAddress(fwAddress + _retValueOffsetWrite); + returnValue.value = value; + pthread_cond_signal(fwAddress + _condOffsetWrite); + pthread_mutex_unlock(fwAddress + _mutexOffsetWrite); + } +} + +typedef _NativeFileWriteCallable = NativeCallable, Pointer, UnsignedLong)>; + +/// NOTE: Don't read the value of this variable directly, use [_getWriteFuncOnBackgroundWorker] instead. +final _writeFuncPtr = _NativeFileWriteCallable.isolateLocal(_write, exceptionalReturn: 0).nativeFunction; + +/// Gets the write function pointer address on the background worker isolate. +Future _getWriteFuncOnBackgroundWorker() async { + return await BackgroundWorker.compute((m) => _writeFuncPtr.address, {}); +} + +int _write(Pointer pThis, Pointer pData, int size) { + final fwAddress = pThis.address; + final cs = fwAddress + _mutexOffsetWrite; + final cv = fwAddress + _condOffsetWrite; + final writeFuncPtr = Pointer.fromAddress(fwAddress + _writeFuncOffset); + final writeFunc = Pointer, IntPtr)>>.fromAddress( + writeFuncPtr.value, + ).asFunction, int)>(); + + pthread_mutex_lock(cs); + // Call Dart side write function. The call is returned immediately (it runs asynchronously) + writeFunc(pData, size); + // So, we should wait for Dart to signal completion + pthread_cond_wait(cv, cs); + final returnValue = Pointer.fromAddress(fwAddress + _retValueOffsetWrite).value; + pthread_mutex_unlock(cs); + return returnValue; +} + +/// pthread_mutex_t offset within FPDF_FILEWRITE +final _mutexOffsetWrite = sizeOf(); + +/// pthread_cond_t offset within FPDF_FILEWRITE +final _condOffsetWrite = _mutexOffsetWrite + sizeOfPthreadMutex; + +/// write function pointer offset within FPDF_FILEWRITE +final _writeFuncOffset = _condOffsetWrite + sizeOfPthreadCond; + +/// return-value offset within FPDF_FILEWRITE +final _retValueOffsetWrite = _writeFuncOffset + sizeOf(); diff --git a/packages/pdfrx_engine/lib/src/native/pthread/pthread.dart b/packages/pdfrx_engine/lib/src/native/pthread/pthread.dart new file mode 100644 index 00000000..04dfc5c2 --- /dev/null +++ b/packages/pdfrx_engine/lib/src/native/pthread/pthread.dart @@ -0,0 +1,48 @@ +// ignore_for_file: non_constant_identifier_names + +import 'dart:ffi'; +import 'dart:io'; + +/// We hope pthread is always available in the process +final DynamicLibrary _pthread = DynamicLibrary.process(); + +final pthread_mutex_init = _pthread.lookupFunction( + 'pthread_mutex_init', +); +final pthread_mutex_destroy = _pthread.lookupFunction( + 'pthread_mutex_destroy', +); +final pthread_mutex_lock = _pthread.lookupFunction('pthread_mutex_lock'); +final pthread_mutex_unlock = _pthread.lookupFunction('pthread_mutex_unlock'); +final pthread_cond_init = _pthread.lookupFunction( + 'pthread_cond_init', +); +final pthread_cond_wait = _pthread.lookupFunction( + 'pthread_cond_wait', +); +final pthread_cond_signal = _pthread.lookupFunction('pthread_cond_signal'); +final pthread_cond_destroy = _pthread.lookupFunction('pthread_cond_destroy'); + +/// Size of pthread_mutex_t varies by platform +int get sizeOfPthreadMutex { + if (Platform.isAndroid) { + return 40; // Android uses 40 bytes for pthread_mutex_t on 64-bit + } else if (Platform.isLinux) { + return 40; // Linux uses 40 bytes for pthread_mutex_t on 64-bit + } else if (Platform.isIOS || Platform.isMacOS) { + return 64; // Darwin (iOS/macOS) uses 64 bytes for pthread_mutex_t on 64-bit + } + throw UnsupportedError('Unsupported platform for pthread mutex size'); +} + +/// Size of pthread_cond_t varies by platform +int get sizeOfPthreadCond { + if (Platform.isAndroid) { + return 48; // Android uses 48 bytes for pthread_cond_t on 64-bit + } else if (Platform.isLinux) { + return 48; // Linux uses 48 bytes for pthread_cond_t on 64-bit + } else if (Platform.isIOS || Platform.isMacOS) { + return 48; // Darwin (iOS/macOS) uses 48 bytes for pthread_cond_t on 64-bit + } + throw UnsupportedError('Unsupported platform for pthread cond size'); +} diff --git a/packages/pdfrx_engine/lib/src/native/string_buffer_wrapper.dart b/packages/pdfrx_engine/lib/src/native/string_buffer_wrapper.dart new file mode 100644 index 00000000..2006005a --- /dev/null +++ b/packages/pdfrx_engine/lib/src/native/string_buffer_wrapper.dart @@ -0,0 +1,8 @@ +/// This is a workaround for WASM+Safari StringBuffer issue. For native code, use StringBuffer directly. +typedef StringBufferWrapper = StringBuffer; + +/// This is a workaround for WASM+Safari StringBuffer issue (#483). +/// +/// - for native code, use [StringBuffer] directly +/// - for Flutter Web, use this [StringBufferWrapper] that internally uses [String] instead. +StringBufferWrapper createStringBufferForWorkaroundSafariWasm() => StringBuffer(); diff --git a/packages/pdfrx_engine/lib/src/native/win32/file_access.dart b/packages/pdfrx_engine/lib/src/native/win32/file_access.dart new file mode 100644 index 00000000..cc54ad3b --- /dev/null +++ b/packages/pdfrx_engine/lib/src/native/win32/file_access.dart @@ -0,0 +1,93 @@ +// ignore_for_file: non_constant_identifier_names, camel_case_types, constant_identifier_names + +import 'dart:ffi'; + +import 'package:ffi/ffi.dart'; +import 'package:pdfium_dart/pdfium_dart.dart' as pdfium_bindings; + +import '../pdfium_file_access.dart'; +import '../worker.dart'; +import 'kernel32.dart'; + +class PdfiumFileAccessHelperWin32 implements PdfiumFileAccessHelper { + @override + Future create(int fileSize, int readBlock) async { + final buffer = malloc( + sizeOf() + + sizeOfCriticalSection + + sizeOfConditionVariable + + sizeOf() * 2, + ); + final fa = buffer.cast(); + fa.ref.m_FileLen = fileSize; + fa.ref.m_Param = Pointer.fromAddress(buffer.address); + fa.ref.m_GetBlock = Pointer.fromAddress(await _getReadFuncOnBackgroundWorker()); + + final readFuncPtr = Pointer.fromAddress(buffer.address + _readFuncOffset); + readFuncPtr.value = readBlock; + + InitializeCriticalSection(buffer.address + _csOffset); + InitializeConditionVariable(buffer.address + _cvOffset); + + return buffer.address; + } + + @override + void destroy(int faAddress) { + DeleteCriticalSection(faAddress + _csOffset); + malloc.free(Pointer.fromAddress(faAddress)); + } + + @override + void setValue(int faAddress, int value) { + final returnValue = Pointer.fromAddress(faAddress + _retValueOffset); + returnValue.value = value; + WakeConditionVariable(faAddress + _cvOffset); + } +} + +typedef _NativeFileReadCallable = + NativeCallable, UnsignedLong, Pointer, UnsignedLong)>; + +/// NOTE: Don't read the value of this variable directly, use [_getReadFuncOnBackgroundWorker] instead. +/// +/// The value will be leaked, but it's acceptable since the value is singleton per isolate. +final _readFuncPtr = _NativeFileReadCallable.isolateLocal(_read, exceptionalReturn: 0).nativeFunction; + +/// Gets the read function pointer address on the background worker isolate. +Future _getReadFuncOnBackgroundWorker() async { + return await BackgroundWorker.compute((m) => _readFuncPtr.address, {}); +} + +int _read(Pointer param, int position, Pointer buffer, int size) { + final faAddress = param.address; + final cs = faAddress + _csOffset; + final cv = faAddress + _cvOffset; + final readFuncPtr = Pointer.fromAddress(faAddress + _readFuncOffset); + final readFunc = Pointer, IntPtr)>>.fromAddress( + readFuncPtr.value, + ).asFunction, int)>(); + + EnterCriticalSection(cs); + + // Call Dart side read function. The call is returned immediately (it runs asynchronously) + readFunc(position, buffer, size); + + // So, we should wait for Dart to signal completion + SleepConditionVariableCS(cv, cs, INFINITE); + final returnValue = Pointer.fromAddress(faAddress + _retValueOffset).value; + LeaveCriticalSection(cs); + return returnValue; +} + +/// CRITICAL_SECTION offset within FPDF_FILEACCESS +final _csOffset = sizeOf(); + +/// CONDITION_VARIABLE offset within FPDF_FILEACCESS +final _cvOffset = _csOffset + sizeOfCriticalSection; + +/// read function pointer offset within FPDF_FILEACCESS +final _readFuncOffset = _cvOffset + sizeOfConditionVariable; + +/// return-value offset within FPDF_FILEACCESS +final _retValueOffset = _readFuncOffset + sizeOf(); diff --git a/packages/pdfrx_engine/lib/src/native/win32/file_write.dart b/packages/pdfrx_engine/lib/src/native/win32/file_write.dart new file mode 100644 index 00000000..375da15b --- /dev/null +++ b/packages/pdfrx_engine/lib/src/native/win32/file_write.dart @@ -0,0 +1,88 @@ +// ignore_for_file: non_constant_identifier_names, camel_case_types, constant_identifier_names + +import 'dart:ffi'; + +import 'package:ffi/ffi.dart'; +import 'package:pdfium_dart/pdfium_dart.dart' as pdfium_bindings; + +import '../pdfium_file_write.dart'; +import '../worker.dart'; +import 'kernel32.dart'; + +class PdfiumFileWriteHelperWin32 implements PdfiumFileWriteHelper { + @override + Future create(int writeBlock) async { + final buffer = calloc( + sizeOf() + sizeOfCriticalSection + sizeOfConditionVariable + sizeOf() * 2, + ); + final fw = buffer.cast(); + fw.ref.version = 1; + fw.ref.WriteBlock = Pointer.fromAddress(await _getWriteFuncOnBackgroundWorker()); + + final writeFuncPtr = Pointer.fromAddress(buffer.address + _writeFuncOffset); + writeFuncPtr.value = writeBlock; + + InitializeCriticalSection(buffer.address + _csOffsetWrite); + InitializeConditionVariable(buffer.address + _cvOffsetWrite); + + return buffer.address; + } + + @override + void destroy(int fwAddress) { + DeleteCriticalSection(fwAddress + _csOffsetWrite); + malloc.free(Pointer.fromAddress(fwAddress)); + } + + @override + void setValue(int fwAddress, int value) { + final returnValue = Pointer.fromAddress(fwAddress + _retValueOffsetWrite); + returnValue.value = value; + WakeConditionVariable(fwAddress + _cvOffsetWrite); + } +} + +typedef _NativeFileWriteCallable = NativeCallable, Pointer, UnsignedLong)>; + +/// NOTE: Don't read the value of this variable directly, use [_getWriteFuncOnBackgroundWorker] instead. +/// +/// The value will be leaked, but it's acceptable since the value is singleton per isolate. +final _writeFuncPtr = _NativeFileWriteCallable.isolateLocal(_write, exceptionalReturn: 0).nativeFunction; + +/// Gets the write function pointer address on the background worker isolate. +Future _getWriteFuncOnBackgroundWorker() async { + return await BackgroundWorker.compute((m) => _writeFuncPtr.address, {}); +} + +int _write(Pointer pThis, Pointer pData, int size) { + final fwAddress = pThis.address; + final cs = fwAddress + _csOffsetWrite; + final cv = fwAddress + _cvOffsetWrite; + final writeFuncPtr = Pointer.fromAddress(fwAddress + _writeFuncOffset); + final writeFunc = Pointer, IntPtr)>>.fromAddress( + writeFuncPtr.value, + ).asFunction, int)>(); + + EnterCriticalSection(cs); + + // Call Dart side write function. The call is returned immediately (it runs asynchronously) + writeFunc(pData, size); + + // So, we should wait for Dart to signal completion + SleepConditionVariableCS(cv, cs, INFINITE); + final returnValue = Pointer.fromAddress(fwAddress + _retValueOffsetWrite).value; + LeaveCriticalSection(cs); + return returnValue; +} + +/// CRITICAL_SECTION offset within FPDF_FILEWRITE +final _csOffsetWrite = sizeOf(); + +/// CONDITION_VARIABLE offset within FPDF_FILEWRITE +final _cvOffsetWrite = _csOffsetWrite + sizeOfCriticalSection; + +/// write function pointer offset within FPDF_FILEWRITE +final _writeFuncOffset = _cvOffsetWrite + sizeOfConditionVariable; + +/// return-value offset within FPDF_FILEWRITE +final _retValueOffsetWrite = _writeFuncOffset + sizeOf(); diff --git a/packages/pdfrx_engine/lib/src/native/win32/kernel32.dart b/packages/pdfrx_engine/lib/src/native/win32/kernel32.dart new file mode 100644 index 00000000..fe6a95da --- /dev/null +++ b/packages/pdfrx_engine/lib/src/native/win32/kernel32.dart @@ -0,0 +1,34 @@ +// ignore_for_file: non_constant_identifier_names, constant_identifier_names + +import 'dart:ffi'; + +final _kernel32 = DynamicLibrary.open('kernel32.dll'); + +final InitializeCriticalSection = _kernel32.lookupFunction( + 'InitializeCriticalSection', +); +final InitializeConditionVariable = _kernel32.lookupFunction( + 'InitializeConditionVariable', +); +final DeleteCriticalSection = _kernel32.lookupFunction( + 'DeleteCriticalSection', +); +final EnterCriticalSection = _kernel32.lookupFunction( + 'EnterCriticalSection', +); +final SleepConditionVariableCS = _kernel32 + .lookupFunction('SleepConditionVariableCS'); +final LeaveCriticalSection = _kernel32.lookupFunction( + 'LeaveCriticalSection', +); +final WakeConditionVariable = _kernel32.lookupFunction( + 'WakeConditionVariable', +); + +const int INFINITE = 0xFFFFFFFF; + +/// CRITICAL_SECTION size is 40 bytes on Windows x64 +const int sizeOfCriticalSection = 40; + +/// CONDITION_VARIABLE size is 8 bytes on Windows x64 +const int sizeOfConditionVariable = 8; diff --git a/packages/pdfrx_engine/lib/src/native/worker.dart b/packages/pdfrx_engine/lib/src/native/worker.dart new file mode 100644 index 00000000..7ac5ed0d --- /dev/null +++ b/packages/pdfrx_engine/lib/src/native/worker.dart @@ -0,0 +1,179 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:developer' as developer; +import 'dart:isolate'; + +import 'package:ffi/ffi.dart'; +import 'package:synchronized/extension.dart'; + +import '../pdfrx.dart'; + +typedef PdfrxComputeCallback = FutureOr Function(M message); + +/// Background worker based on Dart [Isolate]. +class BackgroundWorker { + BackgroundWorker._(this.debugName); + + static final _instance = BackgroundWorker._('PdfrxEngineWorker'); + + final String debugName; + SendPort? _sendPort; + Isolate? _isolate; + + /// Ensures that the worker isolate is initialized, and returns its [SendPort]. + Future _ensureInit() async { + if (_sendPort != null) return _sendPort!; + await synchronized(() async { + if (_sendPort != null) return; + final receivePort = ReceivePort(); + _isolate = await Isolate.spawn(_workerEntry, receivePort.sendPort, debugName: debugName); + _sendPort = await receivePort.first as SendPort; + + // propagate the pdfium module path to the worker + _compute((params) { + Pdfrx.pdfiumModulePath = params.modulePath; + Pdfrx.pdfiumNativeBindings = params.bindings; + }, (modulePath: Pdfrx.pdfiumModulePath, bindings: Pdfrx.pdfiumNativeBindings)); + }); + return _sendPort!; + } + + /// Stops the worker isolate. + Future _stop() async { + if (_sendPort == null) return; + await synchronized(() async { + try { + if (_sendPort == null) return; + await _sendComputeParamsNoInit(_sendPort!, (sendPort) => _StopRequest._(sendPort)); + _sendPort = null; + } catch (e) { + developer.log('Failed to dispose worker (possible double-dispose?): $e'); + } + }); + _isolate?.kill(priority: Isolate.immediate); + _isolate = null; + } + + /// Entry point for the worker isolate. + static void _workerEntry(SendPort sendPort) { + final receivePort = ReceivePort(); + sendPort.send(receivePort.sendPort); + late StreamSubscription? sub; + final suspendingQueue = Queue<_ComputeParams>(); + var suspendingLevel = 0; + sub = receivePort.listen((message) { + if (message is _SuspendRequest) { + suspendingLevel++; + message.execute(); + } else if (message is _ResumeRequest) { + if (suspendingLevel > 0) { + suspendingLevel--; + while (suspendingQueue.isNotEmpty) { + suspendingQueue.removeFirst().execute(); + } + } + message.execute(); + } else if (message is _ComputeParams) { + if (suspendingLevel > 0) { + suspendingQueue.add(message); + } else { + message.execute(); + } + } else if (message is _StopRequest) { + developer.log('Stopping worker isolate.'); + message.execute(); + sub?.cancel(); + sub = null; + receivePort.close(); + } + }); + } + + static Future _sendComputeParamsNoInit( + SendPort sendPort, + T Function(SendPort) createParams, + ) async { + final receivePort = ReceivePort(); + sendPort.send(createParams(receivePort.sendPort)); + return await receivePort.first; + } + + Future _sendComputeParams(T Function(SendPort) createParams) async { + return _sendComputeParamsNoInit(await _ensureInit(), createParams); + } + + /// Runs [callback] in the worker isolate with [message]. + /// + /// [callback] can be any function that takes a single argument of type [M] and returns a value of type [R] or + /// a [Future]. + /// Inside [callback], you can only use passed message and create new objects. + /// You cannot access any variables from the outer scope, otherwise, it will throw an error. + Future _compute(PdfrxComputeCallback callback, M message) async { + return await _sendComputeParams((sendPort) => _ExecuteParams(sendPort, callback, message)) as R; + } + + /// Runs [callback] in the worker isolate with a new [Arena]. + /// + /// [callback] can be any function that takes a single argument of type [M] and returns a value of type [R] or + /// a [Future]. + /// Inside [callback], you can only use passed message and create new objects. + /// You cannot access any variables from the outer scope, otherwise, it will throw an error. + static Future compute(PdfrxComputeCallback callback, M message) async => + await _instance._compute(callback, message); + + /// Suspends the worker isolate during the execution of [action]. + static Future suspendDuringAction(FutureOr Function() action) async { + await _instance._sendComputeParams((sendPort) => _SuspendRequest._(sendPort)); + try { + return await action(); + } finally { + await _instance._sendComputeParams((sendPort) => _ResumeRequest._(sendPort)); + } + } + + /// [compute] wrapper that also provides [Arena] for temporary memory allocation. + /// + /// [callback] can be any function that takes a single argument of type [M] and returns a value of type [R] or + /// a [Future]. + /// Inside [callback], you can only use passed message and create new objects. + /// You cannot access any variables from the outer scope, otherwise, it will throw an error. + /// + /// [Arena] is provided as the first argument to [callback] for temporary memory allocation; the memory block + /// allocated using the [Arena] within the [callback] will be automatically released after the [callback] execution. + static Future computeWithArena(R Function(Arena arena, M message) callback, M message) => + compute((message) => using((arena) => callback(arena, message)), message); + + /// Stop the background worker isolate. + /// + /// This will release all resources associated with the worker. But you can still call [compute], [computeWithArena], + /// and [suspendDuringAction] afterwards, which will recreate the worker isolate. + static Future stop() => _instance._stop(); +} + +class _ComputeParams { + _ComputeParams(this.sendPort); + final SendPort sendPort; + + void execute() => sendPort.send(null); +} + +class _ExecuteParams extends _ComputeParams { + _ExecuteParams(super.sendPort, this.callback, this.message); + final PdfrxComputeCallback callback; + final M message; + + @override + void execute() => sendPort.send(callback(message)); +} + +class _SuspendRequest extends _ComputeParams { + _SuspendRequest._(super.sendPort); +} + +class _ResumeRequest extends _ComputeParams { + _ResumeRequest._(super.sendPort); +} + +class _StopRequest extends _ComputeParams { + _StopRequest._(super.sendPort); +} diff --git a/packages/pdfrx_engine/lib/src/pdf_annotation.dart b/packages/pdfrx_engine/lib/src/pdf_annotation.dart new file mode 100644 index 00000000..6032604b --- /dev/null +++ b/packages/pdfrx_engine/lib/src/pdf_annotation.dart @@ -0,0 +1,53 @@ +import 'pdf_datetime.dart'; + +/// PDF annotation information extracted from PDF links. +/// +/// Contains metadata about PDF annotations such as author, content, and dates. +class PdfAnnotation { + PdfAnnotation({this.title, this.content, this.subject, this.modificationDate, this.creationDate}); + + /// The author/creator of the annotation (PDF field: `T` - Title). + final String? title; + + /// The content/text of the annotation (PDF field: `Contents`). + final String? content; + + /// The subject of the annotation (PDF field: `Subj` - Subject). + final String? subject; + + /// The modification date of the annotation (PDF field: `M` - ModificationDate). + final PdfDateTime? modificationDate; + + /// The creation date of the annotation (PDF field: `CreationDate`). + final PdfDateTime? creationDate; + + /// Returns true if all fields are null. + bool get isEmpty => + title == null && content == null && subject == null && modificationDate == null && creationDate == null; + + /// Returns true if at least one field is not null. + bool get isNotEmpty => !isEmpty; + + @override + String toString() { + return 'PdfAnnotation{title: $title, content: $content, subject: $subject, ' + 'modificationDate: $modificationDate, creationDate: $creationDate}'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is PdfAnnotation && + other.title == title && + other.content == content && + other.subject == subject && + other.modificationDate == modificationDate && + other.creationDate == creationDate; + } + + @override + int get hashCode { + return title.hashCode ^ content.hashCode ^ subject.hashCode ^ modificationDate.hashCode ^ creationDate.hashCode; + } +} diff --git a/packages/pdfrx_engine/lib/src/pdf_datetime.dart b/packages/pdfrx_engine/lib/src/pdf_datetime.dart new file mode 100644 index 00000000..354d75cd --- /dev/null +++ b/packages/pdfrx_engine/lib/src/pdf_datetime.dart @@ -0,0 +1,132 @@ +/// Represents a PDF date/time string defined in [PDF 32000-1:2008, 7.9.4 Dates](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf#page=95) +/// +/// [pdfDateString] is a PDF date string. +/// The date string should be in `D:YYYYMMDDHHmmSSOHH'mm'` format as specified in the PDF standard. +/// But this class do permissive parsing and allows missing some of the components. +/// To validate the format, use [isValidFormat]. +extension type const PdfDateTime(String pdfDateString) { + /// Creates a [PdfDateTime] from a [DateTime] object. + PdfDateTime.fromDateTime(DateTime dateTime) + : pdfDateString = _pdfDateStringFromYMDHMS( + dateTime.year, + dateTime.month, + dateTime.day, + dateTime.hour, + dateTime.minute, + dateTime.second, + timeZoneOffset: dateTime.timeZoneOffset.inMinutes, + ); + + /// Creates a [PdfDateTime] from individual date and time components. + /// + /// - [year] (e.g., 2025) + /// - [month] (1-12) + /// - [day] (1-31) + /// - [hour] (0-23) + /// - [minute] (0-59) + /// - [second] (0-59) + /// - [timeZoneOffset] is in minutes and defaults to 0 (UTC) + PdfDateTime.fromYMDHMS(int year, int month, int day, int hour, int minute, int second, {int timeZoneOffset = 0}) + : pdfDateString = _pdfDateStringFromYMDHMS(year, month, day, hour, minute, second, timeZoneOffset: timeZoneOffset); + + /// Creates a [PdfDateTime] from a nullable PDF date string. + /// + /// Returns null if [pdfDateString] is null. Otherwise, creates a [PdfDateTime] instance. + static PdfDateTime? fromPdfDateString(String? pdfDateString) => + pdfDateString != null ? PdfDateTime(pdfDateString) : null; + + /// Generates a PDF date string from individual date and time components. + /// + /// - [year] (e.g., 2025) + /// - [month] (1-12) + /// - [day] (1-31) + /// - [hour] (0-23) + /// - [minute] (0-59) + /// - [second] (0-59) + /// - [timeZoneOffset] is in minutes and defaults to 0 (UTC) + static String _pdfDateStringFromYMDHMS( + int year, + int month, + int day, + int hour, + int minute, + int second, { + int timeZoneOffset = 0, + }) { + final y = year.toString().padLeft(4, '0'); + final m = month.toString().padLeft(2, '0'); + final d = day.toString().padLeft(2, '0'); + final h = hour.toString().padLeft(2, '0'); + final min = minute.toString().padLeft(2, '0'); + final s = second.toString().padLeft(2, '0'); + final String tz; + if (timeZoneOffset == 0) { + tz = 'Z'; + } else { + final sign = timeZoneOffset > 0 ? '+' : '-'; + final absOffset = timeZoneOffset.abs(); + final hours = (absOffset ~/ 60).toString().padLeft(2, '0'); + final minutes = (absOffset % 60).toString().padLeft(2, '0'); + tz = "$sign$hours'$minutes'"; + } + return 'D:$y$m$d$h$min$s$tz'; + } + + /// Year (e.g., 2025) + int get year => _getNumber(2, 4); + + /// Month (1-12) + int get month => _getNumber(6, 2, 1); + + /// Day (1-31) + int get day => _getNumber(8, 2, 1); + + /// Hour (0-23). + int get hour => _getNumber(10, 2); + + /// Minute (0-59). + int get minute => _getNumber(12, 2); + + /// Second (0-59). + int get second => _getNumber(14, 2); + + int get timezoneOffset { + if (pdfDateString.length >= 17) { + final sign = pdfDateString[16]; + if (sign == 'Z') return 0; + final hours = _getNumber(17, 2); + final minutes = _getNumber(20, 2); + final offsetInMinutes = hours * 60 + minutes; + return sign == '+' + ? offsetInMinutes + : sign == '-' + ? -offsetInMinutes + : 0; + } + return 0; + } + + int _getNumber(int start, int length, [int defaultValue = 0]) { + if (pdfDateString.length >= start + length) { + return int.tryParse(pdfDateString.substring(start, start + length)) ?? defaultValue; + } + return 0; + } + + /// UTC [DateTime] representation of the PDF date/time string. + /// + /// The date/time is adjusted according to the timezone offset specified in the PDF date string so that the resulting + /// [DateTime] is in UTC. + DateTime toDateTime() => + DateTime.utc(year, month, day, hour, minute, second).subtract(Duration(minutes: timezoneOffset)); + + /// Checks if the PDF date/time string is in a valid format or not. + bool get isValidFormat => _dtRegex.hasMatch(pdfDateString); + + /// Regular expression to validate PDF date/time string format. + static final _dtRegex = RegExp(r"^D:\d{4}(\d{2}(\d{2}(\d{2}(\d{2}(\d{2}(Z|[+\-]\d{2}'\d{2}'?)?)?)?)?)?)?$"); + + /// Returns a canonicalized [PdfDateTime] with all components filled in. + PdfDateTime canonicalize() => + PdfDateTime.fromYMDHMS(year, month, day, hour, minute, second, timeZoneOffset: timezoneOffset); +} diff --git a/packages/pdfrx_engine/lib/src/pdf_dest.dart b/packages/pdfrx_engine/lib/src/pdf_dest.dart new file mode 100644 index 00000000..60a22ae6 --- /dev/null +++ b/packages/pdfrx_engine/lib/src/pdf_dest.dart @@ -0,0 +1,65 @@ +import 'utils/list_equals.dart'; + +/// PDF [Explicit Destination](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf#page=374) the page and inner-page location to jump to. +class PdfDest { + /// Create a [PdfDest]. + const PdfDest(this.pageNumber, this.command, this.params); + + /// Page number to jump to. + final int pageNumber; + + /// Destination command. + final PdfDestCommand command; + + /// Destination parameters. For more info, see [PdfDestCommand]. + final List? params; + + @override + String toString() => 'PdfDest{pageNumber: $pageNumber, command: $command, params: $params}'; + + /// Compact the destination. + /// + /// The method is used to compact the destination to reduce memory usage. + /// [params] is typically growable and also modifiable. The method ensures that [params] is unmodifiable. + PdfDest compact() { + return params == null ? this : PdfDest(pageNumber, command, List.unmodifiable(params!)); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is PdfDest && + other.pageNumber == pageNumber && + other.command == command && + listEquals(other.params, params); + } + + @override + int get hashCode => pageNumber.hashCode ^ command.hashCode ^ params.hashCode; +} + +/// [PDF 32000-1:2008, 12.3.2.2 Explicit Destinations, Table 151](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf#page=374) +enum PdfDestCommand { + unknown('unknown'), + xyz('xyz'), + fit('fit'), + fitH('fith'), + fitV('fitv'), + fitR('fitr'), + fitB('fitb'), + fitBH('fitbh'), + fitBV('fitbv'); + + /// Create a [PdfDestCommand] with the specified command name. + const PdfDestCommand(this.name); + + /// Parse the command name to [PdfDestCommand]. + factory PdfDestCommand.parse(String name) { + final nameLow = name.toLowerCase(); + return PdfDestCommand.values.firstWhere((e) => e.name == nameLow, orElse: () => PdfDestCommand.unknown); + } + + /// Command name. + final String name; +} diff --git a/packages/pdfrx_engine/lib/src/pdf_document.dart b/packages/pdfrx_engine/lib/src/pdf_document.dart new file mode 100644 index 00000000..a43dd52f --- /dev/null +++ b/packages/pdfrx_engine/lib/src/pdf_document.dart @@ -0,0 +1,321 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +/// @docImport 'native/pdfrx_pdfium.dart'; + +/// Pdfrx API +library; + +import 'dart:async'; +import 'dart:typed_data'; + +import 'pdf_document_event.dart'; +import 'pdf_outline_node.dart'; +import 'pdf_page.dart'; +import 'pdf_permissions.dart'; +import 'pdfrx_entry_functions.dart'; + +/// Handles PDF document loaded on memory. +abstract class PdfDocument { + /// Constructor to force initialization of sourceName. + PdfDocument({required this.sourceName}); + + /// File path, `asset:[ASSET_PATH]` or `memory:` depending on the content opened. + final String sourceName; + + /// Permission flags. + PdfPermissions? get permissions; + + /// Determine whether the PDF file is encrypted or not. + bool get isEncrypted; + + /// PdfDocument must have [dispose] function. + Future dispose(); + + /// Stream to notify change events in the document. + Stream get events; + + /// Opening the specified file. + /// For Web, [filePath] can be relative path from `index.html` or any arbitrary URL but it may be restricted by CORS. + /// + /// [passwordProvider] is used to provide password for encrypted PDF. See [PdfPasswordProvider] for more info. + /// + /// [firstAttemptByEmptyPassword] is used to determine whether the first attempt to open the PDF is by empty + /// password or not. For more info, see [PdfPasswordProvider]. + /// + /// If [useProgressiveLoading] is true, only the first page is loaded initially and the rest of the pages + /// are loaded progressively when [PdfDocument.loadPagesProgressively] is called explicitly. + static Future openFile( + String filePath, { + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, + }) => PdfrxEntryFunctions.instance.openFile( + filePath, + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + ); + + /// Opening the specified asset. + /// + /// [passwordProvider] is used to provide password for encrypted PDF. See [PdfPasswordProvider] for more info. + /// + /// [firstAttemptByEmptyPassword] is used to determine whether the first attempt to open the PDF is by empty + /// password or not. For more info, see [PdfPasswordProvider]. + /// + /// If [useProgressiveLoading] is true, only the first page is loaded initially and the rest of the pages + /// are loaded progressively when [PdfDocument.loadPagesProgressively] is called explicitly. + static Future openAsset( + String name, { + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, + }) => PdfrxEntryFunctions.instance.openAsset( + name, + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + ); + + /// Opening the PDF on memory. + /// + /// [passwordProvider] is used to provide password for encrypted PDF. See [PdfPasswordProvider] for more info. + /// + /// [firstAttemptByEmptyPassword] is used to determine whether the first attempt to open the PDF is by empty password + /// or not. For more info, see [PdfPasswordProvider]. + /// + /// If [useProgressiveLoading] is true, only the first page is loaded initially and the rest of the pages + /// are loaded progressively when [PdfDocument.loadPagesProgressively] is called explicitly. + /// + /// [sourceName] must be some ID, e.g., file name or URL, to identify the source of the PDF. If [sourceName] is not + /// unique for each source, the viewer may not work correctly. + /// + /// Web only: [allowDataOwnershipTransfer] is used to determine if the data buffer can be transferred to + /// the worker thread. + static Future openData( + Uint8List data, { + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, + String? sourceName, + bool allowDataOwnershipTransfer = false, + void Function()? onDispose, + }) => PdfrxEntryFunctions.instance.openData( + data, + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + sourceName: sourceName, + allowDataOwnershipTransfer: allowDataOwnershipTransfer, + onDispose: onDispose, + ); + + /// Creating a new empty PDF document. + /// + /// [sourceName] must be some ID, e.g., file name or URL, to identify the source of the PDF. If [sourceName] is not + /// unique for each source, the viewer may not work correctly. + static Future createNew({required String sourceName}) => + PdfrxEntryFunctions.instance.createNew(sourceName: sourceName); + + /// Creating a PDF document from JPEG data. + /// + /// [jpegData] is the JPEG encoded image data. + /// [width] and [height] are the dimensions of the image in PDF units (1/72 inch). + /// [sourceName] must be some ID, e.g., file name or URL, to identify the source of the PDF. If [sourceName] is not + /// unique for each source, the viewer may not work correctly. + static Future createFromJpegData( + Uint8List jpegData, { + required double width, + required double height, + required String sourceName, + }) => PdfrxEntryFunctions.instance.createFromJpegData(jpegData, width: width, height: height, sourceName: sourceName); + + /// Opening the PDF from custom source. + /// + /// On Flutter Web, this function is not supported and throws an exception. + /// It is also not supported if pdfrx is running without libpdfrx (**typically on Dart only**). + /// + /// [maxSizeToCacheOnMemory] is the maximum size of the PDF to cache on memory in bytes; the custom loading process + /// may be heavy because of FFI overhead and it may be better to cache the PDF on memory if it's not too large. + /// The default size is 1MB. + /// + /// [passwordProvider] is used to provide password for encrypted PDF. See [PdfPasswordProvider] for more info. + /// + /// [firstAttemptByEmptyPassword] is used to determine whether the first attempt to open the PDF is by empty + /// password or not. For more info, see [PdfPasswordProvider]. + /// + /// If [useProgressiveLoading] is true, only the first page is loaded initially and the rest of the pages + /// are loaded progressively when [PdfDocument.loadPagesProgressively] is called explicitly. + /// + /// [sourceName] must be some ID, e.g., file name or URL, to identify the source of the PDF. If [sourceName] is not + /// unique for each source, the viewer may not work correctly. + static Future openCustom({ + required FutureOr Function(Uint8List buffer, int position, int size) read, + required int fileSize, + required String sourceName, + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, + int? maxSizeToCacheOnMemory, + void Function()? onDispose, + }) => PdfrxEntryFunctions.instance.openCustom( + read: read, + fileSize: fileSize, + sourceName: sourceName, + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + maxSizeToCacheOnMemory: maxSizeToCacheOnMemory, + onDispose: onDispose, + ); + + /// Opening the PDF from URI. + /// + /// For Flutter Web, the implementation uses browser's function and restricted by CORS. + // ignore: comment_references + /// For other platforms, it uses [pdfDocumentFromUri] that uses HTTP's range request to download the file. + /// + /// [passwordProvider] is used to provide password for encrypted PDF. See [PdfPasswordProvider] for more info. + /// + /// [firstAttemptByEmptyPassword] is used to determine whether the first attempt to open the PDF is by empty + /// password or not. For more info, see [PdfPasswordProvider]. + /// + /// If [useProgressiveLoading] is true, only the first page is loaded initially and the rest of the pages + /// are loaded progressively when [PdfDocument.loadPagesProgressively] is called explicitly. + /// + /// [progressCallback] is called when the download progress is updated. + /// + /// [preferRangeAccess] to prefer range access to download the PDF. The default is false (Not supported on Web). + /// It is not supported if pdfrx is running without libpdfrx (**typically on Dart only**). + /// + /// [headers] is used to specify additional HTTP headers especially for authentication/authorization. + /// + /// [withCredentials] is used to specify whether to include credentials in the request (Only supported on Web). + /// + /// [timeout] is used to specify the timeout duration for each HTTP request (Only supported on non-Web platforms). + static Future openUri( + Uri uri, { + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, + PdfDownloadProgressCallback? progressCallback, + bool preferRangeAccess = false, + Map? headers, + bool withCredentials = false, + Duration? timeout, + }) => PdfrxEntryFunctions.instance.openUri( + uri, + passwordProvider: passwordProvider, + firstAttemptByEmptyPassword: firstAttemptByEmptyPassword, + useProgressiveLoading: useProgressiveLoading, + progressCallback: progressCallback, + preferRangeAccess: preferRangeAccess, + headers: headers, + withCredentials: withCredentials, + timeout: timeout, + ); + + /// Load pages progressively. + /// + /// This function loads pages progressively if the pages are not loaded yet. + /// It calls [onPageLoadProgress] for each [loadUnitDuration] duration until all pages are loaded or the loading + /// is cancelled. + /// When [onPageLoadProgress] is called, it should return true to continue loading process or false to stop loading. + /// [data] is an optional data that can be used to pass additional information to the callback. + /// + /// It's always safe to call this function even if the pages are already loaded. + Future loadPagesProgressively({ + PdfPageLoadingCallback? onPageLoadProgress, + T? data, + Duration loadUnitDuration = const Duration(milliseconds: 250), + }); + + /// Pages. + /// + /// The list is unmodifiable; you cannot add, remove, or replace pages directly. + /// To modify the pages, use [pages] setter to set a new list of pages. + List get pages; + + /// Set pages. + /// + /// You can add [PdfPage] instances from any [PdfDocument] instances and the resulting document works correctly + /// if the referenced [PdfDocument] instances are alive; it's your responsibility to manage the lifetime of those + /// instances. To make the document independent from the source documents, you should call [assemble] after setting + /// the pages. + set pages(List value); + + /// Load outline (a.k.a. bookmark). + Future> loadOutline(); + + /// Determine whether document handles are identical or not. + /// + /// It does not mean the document contents (or the document files) are identical. + bool isIdenticalDocumentHandle(Object? other); + + /// Assemble the document after modifying pages. + /// + /// You should call this function after modifying [pages] to make the document consistent and independent from + /// the other source documents. If [pages] contains pages from other documents, those documents must be alive + /// until this function returns. + Future assemble(); + + /// Save the PDF document. + /// + /// This function internally calls [assemble] before encoding the PDF. + Future encodePdf({bool incremental = false, bool removeSecurity = false}); + + /// Execute native document task with the native document handle. + /// + /// Only supported for native backends (e.g., PDFium). + /// + /// Please note that this function suspends pdfrx internal PDFium worker during the execution of [task]. + /// + /// If you modify the document inside [task], make sure to keep consistency of the document after the + /// modification; e.g., call [reloadPages] if necessary. + /// + /// `nativeDocumentHandle` is a pointer to the native PDF document handle (e.g., FPDF_DOCUMENT in PDFium) but + /// represented as an integer to avoid direct dependency to PDFium bindings. + /// + /// ```dart + /// final result = await pdfDocument.useNativeDocumentHandle((nativeDocumentHandle) { + /// // Convert nativeDocumentHandle to FPDF_DOCUMENT handle. + /// final pdfDocument = pdfium_bindings.FPDF_DOCUMENT.fromAddress(nativeDocumentHandle); + /// // <> + /// return someResult; + /// }); + /// ``` + Future useNativeDocumentHandle(FutureOr Function(int nativeDocumentHandle) task); + + /// Reload specified pages. + /// + /// [pageNumbersToReload] is the list of page numbers (1-based) to reload. If null, all pages are reloaded. + Future reloadPages({List? pageNumbersToReload}); +} + +typedef PdfPageLoadingCallback = FutureOr Function(int currentPageNumber, int totalPageCount, T? data); + +/// Callback function to notify download progress. +/// +/// [downloadedBytes] is the number of bytes downloaded so far. +/// [totalBytes] is the total number of bytes to download. It may be null if the total size is unknown. +typedef PdfDownloadProgressCallback = void Function(int downloadedBytes, [int? totalBytes]); + +/// Function to provide password for encrypted PDF. +/// +/// The function is called when PDF requires password. +/// It is repeatedly called until the function returns null or a valid password. +/// +/// [createSimplePasswordProvider] is a helper function to create [PdfPasswordProvider] that returns the password +/// only once. +typedef PdfPasswordProvider = FutureOr Function(); + +/// Create [PdfPasswordProvider] that returns the password only once. +/// +/// The returned [PdfPasswordProvider] returns the password only once and returns null afterwards. +/// If [password] is null, the returned [PdfPasswordProvider] returns null always. +PdfPasswordProvider createSimplePasswordProvider(String? password) { + return () { + final ret = password; + password = null; + return ret; + }; +} diff --git a/packages/pdfrx_engine/lib/src/pdf_document_event.dart b/packages/pdfrx_engine/lib/src/pdf_document_event.dart new file mode 100644 index 00000000..c8494942 --- /dev/null +++ b/packages/pdfrx_engine/lib/src/pdf_document_event.dart @@ -0,0 +1,68 @@ +import 'pdf_document.dart'; +import 'pdf_font_query.dart'; +import 'pdf_page_status_change.dart'; + +/// PDF document event types. +enum PdfDocumentEventType { + /// [PdfDocumentLoadCompleteEvent]: Document's loading is complete; i.e., all pages are loaded. + documentLoadComplete, + + /// [PdfDocumentPageStatusChangedEvent]: Page status changed. + pageStatusChanged, + + /// [PdfDocumentMissingFontsEvent]: Missing fonts changed. + missingFonts, +} + +/// Base class for PDF document events. +abstract class PdfDocumentEvent { + /// Event type. + PdfDocumentEventType get type; + + /// Document that this event is related to. + PdfDocument get document; +} + +/// Event that is triggered when the PDF document has finished loading. +class PdfDocumentLoadCompleteEvent implements PdfDocumentEvent { + const PdfDocumentLoadCompleteEvent(this.document); + + @override + PdfDocumentEventType get type => PdfDocumentEventType.documentLoadComplete; + + @override + final PdfDocument document; +} + +/// Event that is triggered when the status of PDF document pages has changed. +class PdfDocumentPageStatusChangedEvent implements PdfDocumentEvent { + const PdfDocumentPageStatusChangedEvent(this.document, {required this.changes}); + + @override + PdfDocumentEventType get type => PdfDocumentEventType.pageStatusChanged; + + @override + final PdfDocument document; + + /// The pages that have changed. + /// + /// The map is from page number (1-based) to it's status change. + /// + /// You can assume that the keys in this map are sorted in ascending order. + final Map changes; +} + +/// Event that is triggered when the list of missing fonts in the PDF document has changed. +class PdfDocumentMissingFontsEvent implements PdfDocumentEvent { + /// Create a [PdfDocumentMissingFontsEvent]. + const PdfDocumentMissingFontsEvent(this.document, this.missingFonts); + + @override + PdfDocumentEventType get type => PdfDocumentEventType.missingFonts; + + @override + final PdfDocument document; + + /// The list of missing fonts. + final List missingFonts; +} diff --git a/packages/pdfrx_engine/lib/src/pdf_exception.dart b/packages/pdfrx_engine/lib/src/pdf_exception.dart new file mode 100644 index 00000000..0ec84ea7 --- /dev/null +++ b/packages/pdfrx_engine/lib/src/pdf_exception.dart @@ -0,0 +1,19 @@ +/// PDF exception class. +class PdfException implements Exception { + /// Creates a new [PdfException]. + const PdfException(this.message, [this.errorCode]); + + /// Exception message. + final String message; + + /// Optional error code. + final int? errorCode; + @override + String toString() => 'PdfException: $message'; +} + +/// PDF exception for password related errors. +class PdfPasswordException extends PdfException { + /// Creates a new [PdfPasswordException]. + const PdfPasswordException(super.message); +} diff --git a/packages/pdfrx_engine/lib/src/pdf_font_query.dart b/packages/pdfrx_engine/lib/src/pdf_font_query.dart new file mode 100644 index 00000000..2f9a6ed9 --- /dev/null +++ b/packages/pdfrx_engine/lib/src/pdf_font_query.dart @@ -0,0 +1,82 @@ +/// PDF font query parameters. +class PdfFontQuery { + const PdfFontQuery({ + required this.face, + required this.weight, + required this.isItalic, + required this.charset, + required this.pitchFamily, + }); + + /// Font face name. + final String face; + + /// Font weight. + final int weight; + + /// Whether the font is italic. + final bool isItalic; + + /// PDFium's charset ID. + final PdfFontCharset charset; + + /// Pitch family flags. + /// + /// It can be any combination of the following values: + /// - `fixed` = 1 + /// - `roman` = 16 + /// - `script` = 64 + final int pitchFamily; + + bool get isFixed => (pitchFamily & 1) != 0; + bool get isRoman => (pitchFamily & 16) != 0; + bool get isScript => (pitchFamily & 64) != 0; + + String _getPitchFamily() { + return [if (isFixed) 'fixed', if (isRoman) 'roman', if (isScript) 'script'].join(','); + } + + @override + String toString() => + 'PdfFontQuery(face: "$face", weight: $weight, italic: $isItalic, charset: $charset, pitchFamily: $pitchFamily=[${_getPitchFamily()}])'; +} + +/// PDFium font charset ID. +/// +enum PdfFontCharset { + ansi(0), + default_(1), + symbol(2), + + /// Japanese + shiftJis(128), + + /// Korean + hangul(129), + + /// Chinese Simplified + gb2312(134), + + /// Chinese Traditional + chineseBig5(136), + greek(161), + vietnamese(163), + hebrew(177), + arabic(178), + cyrillic(204), + thai(222), + easternEuropean(238); + + const PdfFontCharset(this.pdfiumCharsetId); + + /// PDFium's charset ID. + final int pdfiumCharsetId; + + static final _value2Enum = {for (final e in PdfFontCharset.values) e.pdfiumCharsetId: e}; + + /// Convert PDFium's charset ID to [PdfFontCharset]. + static PdfFontCharset fromPdfiumCharsetId(int id) => _value2Enum[id]!; + + @override + String toString() => '$name($pdfiumCharsetId)'; +} diff --git a/packages/pdfrx_engine/lib/src/pdf_image.dart b/packages/pdfrx_engine/lib/src/pdf_image.dart new file mode 100644 index 00000000..86a80070 --- /dev/null +++ b/packages/pdfrx_engine/lib/src/pdf_image.dart @@ -0,0 +1,48 @@ +import 'dart:typed_data'; + +import 'pdf_page.dart'; + +/// Image rendered from PDF page. +/// +/// Please note that the image created must be disposed after use by calling [dispose]. +/// See [PdfPage.render]. +abstract class PdfImage { + /// Number of pixels in horizontal direction. + int get width; + + /// Number of pixels in vertical direction. + int get height; + + /// BGRA8888 Raw pixel data. + Uint8List get pixels; + + /// Dispose the image. + void dispose(); + + /// Create [PdfImage] from BGRA pixel data. + /// + /// [bgraPixels] is the raw pixel data in BGRA8888 format. + /// [width] and [height] specify the dimensions of the image. + /// + /// The size of [bgraPixels] must be equal to `width * height * 4`. + /// Returns the created [PdfImage]. + static PdfImage createFromBgraData(Uint8List bgraPixels, {required int width, required int height}) { + return _PdfImageBgraRaw(width, height, bgraPixels); + } +} + +class _PdfImageBgraRaw implements PdfImage { + _PdfImageBgraRaw(this.width, this.height, this.pixels); + + @override + final int width; + + @override + final int height; + + @override + final Uint8List pixels; + + @override + void dispose() {} +} diff --git a/packages/pdfrx_engine/lib/src/pdf_link.dart b/packages/pdfrx_engine/lib/src/pdf_link.dart new file mode 100644 index 00000000..e5263632 --- /dev/null +++ b/packages/pdfrx_engine/lib/src/pdf_link.dart @@ -0,0 +1,55 @@ +import 'pdf_annotation.dart'; +import 'pdf_dest.dart'; +import 'pdf_page.dart'; +import 'pdf_rect.dart'; +import 'utils/list_equals.dart'; + +/// Link in PDF page. +/// +/// Either one of [url] or [dest] is valid (not null). +/// See [PdfPage.loadLinks]. +class PdfLink { + const PdfLink(this.rects, {this.url, this.dest, this.annotation}); + + /// Link URL. + final Uri? url; + + /// Link destination (link to page). + final PdfDest? dest; + + /// Link location(s) inside the associated PDF page. + /// + /// Sometimes a link can span multiple rectangles, e.g., a link across multiple lines. + final List rects; + + /// Annotation information if available. + final PdfAnnotation? annotation; + + /// Compact the link. + /// + /// The method is used to compact the link to reduce memory usage. + /// [rects] is typically growable and also modifiable. The method ensures that [rects] is unmodifiable. + /// [dest] is also compacted by calling [PdfDest.compact]. + PdfLink compact() { + return PdfLink(List.unmodifiable(rects), url: url, dest: dest?.compact(), annotation: annotation); + } + + @override + String toString() { + return 'PdfLink{${url?.toString() ?? dest?.toString()}, rects: $rects, annotation: $annotation}'; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is PdfLink && + other.url == url && + other.dest == dest && + listEquals(other.rects, rects) && + other.annotation == annotation; + } + + @override + int get hashCode => url.hashCode ^ dest.hashCode ^ rects.hashCode ^ annotation.hashCode; +} diff --git a/packages/pdfrx_engine/lib/src/pdf_outline_node.dart b/packages/pdfrx_engine/lib/src/pdf_outline_node.dart new file mode 100644 index 00000000..5e22c0d3 --- /dev/null +++ b/packages/pdfrx_engine/lib/src/pdf_outline_node.dart @@ -0,0 +1,32 @@ +import 'pdf_dest.dart'; +import 'pdf_document.dart'; +import 'utils/list_equals.dart'; + +/// Outline (a.k.a. Bookmark) node in PDF document. +/// +/// See [PdfDocument.loadOutline]. +class PdfOutlineNode { + const PdfOutlineNode({required this.title, required this.dest, required this.children}); + + /// Outline node title. + final String title; + + /// Outline node destination. + final PdfDest? dest; + + /// Outline child nodes. + final List children; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is PdfOutlineNode && + other.title == title && + other.dest == dest && + listEquals(other.children, children); + } + + @override + int get hashCode => title.hashCode ^ dest.hashCode ^ children.hashCode; +} diff --git a/packages/pdfrx_engine/lib/src/pdf_page.dart b/packages/pdfrx_engine/lib/src/pdf_page.dart new file mode 100644 index 00000000..92714722 --- /dev/null +++ b/packages/pdfrx_engine/lib/src/pdf_page.dart @@ -0,0 +1,324 @@ +import 'dart:async'; + +import 'package:rxdart/rxdart.dart'; + +import 'pdf_document.dart'; +import 'pdf_document_event.dart'; +import 'pdf_image.dart'; +import 'pdf_link.dart'; +import 'pdf_page_proxies.dart'; +import 'pdf_page_status_change.dart'; +import 'pdf_text.dart'; +import 'pdf_text_formatter.dart'; + +/// Handles a PDF page in [PdfDocument]. +/// +/// See [PdfDocument.pages]. +abstract class PdfPage { + /// PDF document. + PdfDocument get document; + + /// Page number. The first page is 1. + int get pageNumber; + + /// PDF page width in points (width in pixels at 72 dpi) (rotated). + double get width; + + /// PDF page height in points (height in pixels at 72 dpi) (rotated). + double get height; + + /// PDF page rotation. + PdfPageRotation get rotation; + + /// Whether the page is really loaded or not. + /// + /// If the value is false, the page's [width], [height], and [rotation] are just guessed values and + /// will be updated when the page is really loaded (progressive loading case only). + /// + /// If you want to wait until the page is really loaded, use [PdfPageBaseExtensions.ensureLoaded] or + /// [PdfPageBaseExtensions.waitForLoaded]. + bool get isLoaded; + + /// Render a sub-area or full image of specified PDF file. + /// Returned image should be disposed after use. + /// [x], [y], [width], [height] specify sub-area to render in pixels. + /// [fullWidth], [fullHeight] specify virtual full size of the page to render in pixels. + /// - If [x], [y] are not specified, (0,0) is used. + /// - If [width], [height] are not specified, [fullWidth], [fullHeight] are used. + /// - If [fullWidth], [fullHeight] are not specified, [PdfPage.width] and [PdfPage.height] are used (it means rendered at 72-dpi). + /// [backgroundColor] is `AARRGGBB` integer color notation used to fill the background of the page. If no color is specified, 0xffffffff (white) is used. + /// - [annotationRenderingMode] controls to render annotations or not. The default is [PdfAnnotationRenderingMode.annotationAndForms]. + /// - [flags] is used to specify additional rendering flags. The default is [PdfPageRenderFlags.none]. + /// - [cancellationToken] can be used to cancel the rendering process. It must be created by [createCancellationToken]. + /// + /// If the page is not loaded yet (progressive loading case only), the function renders empty page with specified + /// background color. + /// + /// The following code extract the area of (20,30)-(120,130) from the page image rendered at 1000x1500 pixels: + /// ```dart + /// final image = await page.render( + /// x: 20, + /// y: 30, + /// width: 100, + /// height: 100, + /// fullWidth: 1000, + /// fullHeight: 1500, + /// ); + /// ``` + Future render({ + int x = 0, + int y = 0, + int? width, + int? height, + double? fullWidth, + double? fullHeight, + int? backgroundColor, + PdfPageRotation? rotationOverride, + PdfAnnotationRenderingMode annotationRenderingMode = PdfAnnotationRenderingMode.annotationAndForms, + int flags = PdfPageRenderFlags.none, + PdfPageRenderCancellationToken? cancellationToken, + }); + + /// Create [PdfPageRenderCancellationToken] to cancel the rendering process. + PdfPageRenderCancellationToken createCancellationToken(); + + /// Load plain text for the page. + /// + /// For text with character bounding boxes, use [PdfPageBaseExtensions.loadStructuredText]. + /// + /// If the page is not loaded yet (progressive loading case only), this function returns null. + Future loadText(); + + /// Load links. + /// + /// If [compact] is true, it tries to reduce memory usage by compacting the link data. + /// See [PdfLink.compact] for more info. + /// + /// If [enableAutoLinkDetection] is true, the function tries to detect Web links automatically. + /// This is useful if the PDF file contains text that looks like Web links but not defined as links in the PDF. + /// The default is true. + /// + /// If the page is not loaded yet (progressive loading case only), this function returns an empty list. + Future> loadLinks({bool compact = false, bool enableAutoLinkDetection = true}); +} + +/// Extension methods for [PdfPage]. +extension PdfPageBaseExtensions on PdfPage { + /// Load structured text with character bounding boxes. + /// + /// The function internally does test flow analysis (reading order) and line segmentation to detect + /// text direction and line breaks. + /// + /// To access the raw text, use [loadText]. + /// + /// If the page is not loaded yet (progressive loading case only), this function returns null. + Future loadStructuredText({bool ensureLoaded = true}) => + PdfTextFormatter.loadStructuredText(this, pageNumberOverride: pageNumber); + + /// Stream of page status change events for this page. + /// + /// The event is based on the page position (page number), so the page instance identity may change if the page is + /// replaced by another instance with the same page number. + Stream get events { + return document.events + .where((event) => event is PdfDocumentPageStatusChangedEvent && event.changes.containsKey(pageNumber)) + .map((event) => (event as PdfDocumentPageStatusChangedEvent).changes[pageNumber]!); + } + + /// Stream of latest page instances when the page status changes. + /// + /// The page instance identity may change if the page is replaced by another instance with the same page number. + /// For example, when the page is loaded, the instance may be replaced with a fully loaded page instance. + /// This stream emits the latest instance of the page whenever a status change event occurs for this page. + /// Note that this stream may emit the same instance multiple times if the page is not replaced. + Stream get latestPageStream => + Stream.value(this).concatWith([events.map((event) => document.pages[pageNumber - 1])]); + + /// Ensure the page is really loaded. + /// + /// Returns the latest instance of the page once it is loaded. + /// + /// If you want to specify a timeout, use [waitForLoaded] instead. + Future ensureLoaded() async { + return (await waitForLoaded())!; + } + + /// Wait until the page is really loaded. + /// + /// Returns the latest instance of the page once it is loaded. + /// If [timeout] is specified, it returns null if the page is not loaded within the duration. otherwise, + /// it waits indefinitely and never returns null. + Future waitForLoaded({Duration? timeout}) async { + final newPage = document.pages[pageNumber - 1]; + if (newPage.isLoaded) { + return newPage; + } + final completer = Completer(); + late StreamSubscription subscription; + subscription = events.listen((event) { + if (event.page.isLoaded) { + subscription.cancel(); + completer.complete(event.page); // get the latest instance + } + }); + if (timeout != null) { + return completer.future.timeout( + timeout, + onTimeout: () { + subscription.cancel(); + return null; + }, + ); + } + return completer.future; + } +} + +/// Extension to add rotation capability to [PdfPage]. +/// +/// Use these functions to create rotated pages when reorganizing or combining PDFs. +/// +/// The following example shows how to fix page orientations: +/// +/// ```dart +/// final doc = await PdfDocument.openFile('document.pdf'); +/// doc.pages = [ +/// doc.pages[0], +/// doc.pages[1].rotatedTo(PdfPageRotation.clockwise90), +/// doc.pages[2].rotatedBy(PdfPageRotation.clockwise90), +/// doc.pages[3].rotatedCW90(), +/// ]; +/// await File('fixed.pdf').writeAsBytes(await doc.encodePdf()); +/// ``` +extension PdfPageWithRotationExtension on PdfPage { + /// Rotates a page with the specified rotation. + /// + /// The method returns a new page with rotation equal to [rotation]. + /// + /// See usage example in [PdfPageWithRotationExtension]. + PdfPage rotatedTo(PdfPageRotation rotation) { + if (rotation == this.rotation) { + return this; // No rotation change needed + } + return PdfPageRotated(this, rotation); + } + + /// Rotates a page with rotation added to the current rotation. + /// + /// The method returns a new page with rotation equal to (current rotation + [delta]). + /// + /// See usage example in [PdfPageWithRotationExtension]. + PdfPage rotatedBy(PdfPageRotation delta) { + final newRotation = PdfPageRotation.values[(rotation.index + delta.index) & 3]; + return rotatedTo(newRotation); + } + + /// Rotates a page clockwise by 90 degrees. + /// + /// This method returns a new page with rotation equal to (current rotation + clockwise90). + /// + /// See usage example in [PdfPageWithRotationExtension]. + PdfPage rotatedCW90() => rotatedBy(PdfPageRotation.clockwise90); + + /// Rotates a page counter-clockwise by 90 degrees. + /// + /// This method returns a new page with rotation equal to (current rotation + clockwise270). + /// + /// See usage example in [PdfPageWithRotationExtension]. + PdfPage rotatedCCW90() => rotatedBy(PdfPageRotation.clockwise270); + + /// Rotates a page clockwise by 180 degrees. + /// + /// This method returns a new page with rotation equal to (current rotation + clockwise180). + /// + /// See usage example in [PdfPageWithRotationExtension]. + PdfPage rotated180() => rotatedBy(PdfPageRotation.clockwise180); +} + +/// Extension to add page renumbering capability to [PdfPage]. +/// +/// This is used internally when assembling documents, but can also be used manually. +extension PdfPageRenumberedExtension on PdfPage { + /// Renumbers a page with the specified page number. + /// + /// See usage example in [PdfPageRenumberedExtension]. + PdfPage withPageNumber(int pageNumber) { + if (pageNumber == this.pageNumber) { + return this; // No page number change needed + } + return PdfPageRenumbered(this, pageNumber: pageNumber); + } +} + +/// Page rotation. +enum PdfPageRotation { none, clockwise90, clockwise180, clockwise270 } + +extension PdfPageRotationEnumExtension on PdfPageRotation { + /// Get counter-clockwise 90 degree rotation value from the current rotation. + PdfPageRotation get rotateCCW90 => PdfPageRotation.values[(index + 3) % 4]; + + /// Get clockwise 90 degree rotation value from the current rotation. + PdfPageRotation get rotateCW90 => PdfPageRotation.values[(index + 1) % 4]; + + /// Get 180 degree rotation value from the current rotation. + PdfPageRotation get rotate180 => PdfPageRotation.values[(index + 2) % 4]; + + /// Add two rotations. + PdfPageRotation operator +(PdfPageRotation other) => PdfPageRotation.values[(index + other.index) % 4]; +} + +/// Annotation rendering mode. +enum PdfAnnotationRenderingMode { + /// Do not render annotations. + none, + + /// Render annotations. + annotation, + + /// Render annotations and forms. + annotationAndForms, +} + +/// Flags for [PdfPage.render]. +/// +/// Basically, they are PDFium's `FPDF_RENDER_*` flags and not supported on PDF.js. +abstract class PdfPageRenderFlags { + /// None. + static const none = 0; + + /// `FPDF_LCD_TEXT` flag. + static const lcdText = 0x0002; + + /// `FPDF_GRAYSCALE` flag. + static const grayscale = 0x0008; + + /// `FPDF_RENDER_LIMITEDIMAGECACHE` flag. + static const limitedImageCache = 0x0200; + + /// `FPDF_RENDER_FORCEHALFTONE` flag. + static const forceHalftone = 0x0400; + + /// `FPDF_PRINTING` flag. + static const printing = 0x0800; + + /// `FPDF_RENDER_NO_SMOOTHTEXT` flag. + static const noSmoothText = 0x1000; + + /// `FPDF_RENDER_NO_SMOOTHIMAGE` flag. + static const noSmoothImage = 0x2000; + + /// `FPDF_RENDER_NO_SMOOTHPATH` flag. + static const noSmoothPath = 0x4000; + + /// Output image is in premultiplied alpha format. + static const premultipliedAlpha = 0x80000000; +} + +/// Token to try to cancel the rendering process. +abstract class PdfPageRenderCancellationToken { + /// Cancel the rendering process. + void cancel(); + + /// Determine whether the rendering process is canceled or not. + bool get isCanceled; +} diff --git a/packages/pdfrx_engine/lib/src/pdf_page_proxies.dart b/packages/pdfrx_engine/lib/src/pdf_page_proxies.dart new file mode 100644 index 00000000..eb0fa5d6 --- /dev/null +++ b/packages/pdfrx_engine/lib/src/pdf_page_proxies.dart @@ -0,0 +1,182 @@ +import 'pdf_document.dart'; +import 'pdf_image.dart'; +import 'pdf_link.dart'; +import 'pdf_page.dart'; +import 'pdf_text.dart'; + +/// Proxy interface for [PdfPage]. +/// +/// Used for creating proxy pages that modify behavior of the base page. +/// +/// For implementation, see [PdfPageRenumbered] and [PdfPageRotated]. +abstract class PdfPageProxy implements PdfPage { + PdfPage get basePage; +} + +/// Extension to unwrap [PdfPageProxy] on [PdfPage]. +extension PdfPageProxyExtension on PdfPage { + /// Unwrap the page to get the base page of type [T]. + /// + /// If the base page of type [T] is not found, returns null. + T? unwrap() { + final pThis = this; + if (pThis is T) return pThis; + if (pThis is PdfPageProxy) return pThis.basePage.unwrap(); + return null; + } + + /// Unwrap the page until [stopCondition] is met. + /// + /// If the condition is met, returns null. + PdfPage? unwrapUntil(bool Function(PdfPage page) stopCondition) { + var current = this; + while (true) { + if (stopCondition(current)) return current; + if (current is PdfPageProxy) { + current = current.basePage; + } else { + return null; + } + } + } +} + +/// PDF page wrapper that renumbers the page number. +class PdfPageRenumbered implements PdfPageProxy { + PdfPageRenumbered(PdfPage basePage, {required this.pageNumber}) + : basePage = basePage.unwrapUntil((p) => p is! PdfPageRenumbered)!; + + @override + final PdfPage basePage; + + @override + final int pageNumber; + + @override + PdfPageRenderCancellationToken createCancellationToken() => basePage.createCancellationToken(); + + @override + PdfDocument get document => basePage.document; + + @override + PdfPageRotation get rotation => basePage.rotation; + + @override + double get width => basePage.width; + + @override + double get height => basePage.height; + + @override + bool get isLoaded => basePage.isLoaded; + + @override + Future> loadLinks({bool compact = false, bool enableAutoLinkDetection = true}) => + basePage.loadLinks(compact: compact, enableAutoLinkDetection: enableAutoLinkDetection); + + @override + Future loadText() => basePage.loadText(); + + @override + Future render({ + int x = 0, + int y = 0, + int? width, + int? height, + double? fullWidth, + double? fullHeight, + int? backgroundColor, + PdfPageRotation? rotationOverride, + PdfAnnotationRenderingMode annotationRenderingMode = PdfAnnotationRenderingMode.annotationAndForms, + int flags = PdfPageRenderFlags.none, + PdfPageRenderCancellationToken? cancellationToken, + }) => basePage.render( + x: x, + y: y, + width: width, + height: height, + fullWidth: fullWidth, + fullHeight: fullHeight, + backgroundColor: backgroundColor, + annotationRenderingMode: annotationRenderingMode, + flags: flags, + cancellationToken: cancellationToken, + rotationOverride: rotationOverride, + ); +} + +/// PDF page wrapper that applies an absolute rotation to the base page. +class PdfPageRotated implements PdfPageProxy { + PdfPageRotated(PdfPage basePage, this.rotation) : basePage = basePage.unwrapUntil((p) => p is! PdfPageRotated)!; + + @override + final PdfPage basePage; + + // Override rotation to return the effective rotation + @override + final PdfPageRotation rotation; + + /// Check if dimensions need to be swapped (for 90° or 270° rotations). + /// This is relative to the source page's rotation. + bool get _swapWH => shouldSwapWH(rotation); + + bool shouldSwapWH(PdfPageRotation rotation) => ((rotation.index - basePage.rotation.index) & 1) == 1; + + // Delegate basic properties + @override + PdfDocument get document => basePage.document; + + @override + int get pageNumber => basePage.pageNumber; + + @override + bool get isLoaded => basePage.isLoaded; + + // Swap width/height if additional rotation is 90° or 270° + @override + double get width => _swapWH ? basePage.height : basePage.width; + + @override + double get height => _swapWH ? basePage.width : basePage.height; + + // Override render to pass the effective rotation as rotationOverride + @override + Future render({ + int x = 0, + int y = 0, + int? width, + int? height, + double? fullWidth, + double? fullHeight, + int? backgroundColor, + PdfPageRotation? rotationOverride, + PdfAnnotationRenderingMode annotationRenderingMode = PdfAnnotationRenderingMode.annotationAndForms, + int flags = PdfPageRenderFlags.none, + PdfPageRenderCancellationToken? cancellationToken, + }) { + return basePage.render( + x: x, + y: y, + width: width, + height: height, + fullWidth: fullWidth, + fullHeight: fullHeight, + backgroundColor: backgroundColor, + rotationOverride: rotationOverride ?? rotation, + annotationRenderingMode: annotationRenderingMode, + flags: flags, + cancellationToken: cancellationToken, + ); + } + + // All other methods just delegate - text/links work correctly because they use `rotation` property + @override + PdfPageRenderCancellationToken createCancellationToken() => basePage.createCancellationToken(); + + @override + Future loadText() => basePage.loadText(); + + @override + Future> loadLinks({bool compact = false, bool enableAutoLinkDetection = true}) => + basePage.loadLinks(compact: compact, enableAutoLinkDetection: enableAutoLinkDetection); +} diff --git a/packages/pdfrx_engine/lib/src/pdf_page_status_change.dart b/packages/pdfrx_engine/lib/src/pdf_page_status_change.dart new file mode 100644 index 00000000..40f2d3d1 --- /dev/null +++ b/packages/pdfrx_engine/lib/src/pdf_page_status_change.dart @@ -0,0 +1,78 @@ +import 'pdf_page.dart'; + +/// Enum representing the type of PDF page status change. +enum PdfPageStatusChangeType { + /// Page has been moved inside the same document. + moved, + + /// Page has been newly added or modified. + modified, +} + +/// Base class for PDF page status change. +abstract class PdfPageStatusChange { + const PdfPageStatusChange(); + + /// Type of the status change. + PdfPageStatusChangeType get type; + + /// The page that has changed. + /// + /// This is a new instance of the page after the change. + PdfPage get page; + + /// Create [PdfPageStatusMoved]. + static PdfPageStatusChange moved({required PdfPage page, required int oldPageNumber}) => + PdfPageStatusMoved(page: page, oldPageNumber: oldPageNumber); + + /// Return [PdfPageStatusModified]. + static PdfPageStatusChange modified({required PdfPage page}) => PdfPageStatusModified(page: page); +} + +/// Event that is triggered when a PDF page is moved inside the same document. +class PdfPageStatusMoved extends PdfPageStatusChange { + const PdfPageStatusMoved({required this.page, required this.oldPageNumber}); + + @override + final PdfPage page; + + final int oldPageNumber; + + @override + PdfPageStatusChangeType get type => PdfPageStatusChangeType.moved; + + @override + int get hashCode => oldPageNumber.hashCode; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is PdfPageStatusMoved && other.oldPageNumber == oldPageNumber; + } + + @override + String toString() => 'PdfPageStatusMoved(oldPageNumber: $oldPageNumber)'; +} + +/// Event that is triggered when a PDF page is modified or newly added. +class PdfPageStatusModified extends PdfPageStatusChange { + const PdfPageStatusModified({required this.page}); + + @override + final PdfPage page; + + @override + PdfPageStatusChangeType get type => PdfPageStatusChangeType.modified; + + @override + int get hashCode => 0; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is PdfPageStatusModified; + } + + @override + String toString() => 'PdfPageStatusModified()'; +} diff --git a/packages/pdfrx_engine/lib/src/pdf_permissions.dart b/packages/pdfrx_engine/lib/src/pdf_permissions.dart new file mode 100644 index 00000000..8be23988 --- /dev/null +++ b/packages/pdfrx_engine/lib/src/pdf_permissions.dart @@ -0,0 +1,34 @@ +/// PDF permissions defined on PDF 32000-1:2008, Table 22. +class PdfPermissions { + const PdfPermissions(this.permissions, this.securityHandlerRevision); + + /// User access permissions on on PDF 32000-1:2008, Table 22. + final int permissions; + + /// Security handler revision. + final int securityHandlerRevision; + + /// Determine whether the PDF file allows copying of the contents. + bool get allowsCopying => (permissions & 4) != 0; + + /// Determine whether the PDF file allows document assembly. + bool get allowsDocumentAssembly => (permissions & 8) != 0; + + /// Determine whether the PDF file allows printing of the pages. + bool get allowsPrinting => (permissions & 16) != 0; + + /// Determine whether the PDF file allows modifying annotations, form fields, and their associated + bool get allowsModifyAnnotations => (permissions & 32) != 0; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is PdfPermissions && + other.permissions == permissions && + other.securityHandlerRevision == securityHandlerRevision; + } + + @override + int get hashCode => permissions.hashCode ^ securityHandlerRevision.hashCode; +} diff --git a/packages/pdfrx_engine/lib/src/pdf_point.dart b/packages/pdfrx_engine/lib/src/pdf_point.dart new file mode 100644 index 00000000..104c9918 --- /dev/null +++ b/packages/pdfrx_engine/lib/src/pdf_point.dart @@ -0,0 +1,82 @@ +import 'package:vector_math/vector_math_64.dart'; + +import 'pdf_page.dart'; + +/// PDF page coordinates point. +/// +/// In Pdf page coordinates, the origin is at the bottom-left corner and Y-axis is pointing upward. +/// The unit is normally in points (1/72 inch). +class PdfPoint { + const PdfPoint(this.x, this.y); + + /// X coordinate. + final double x; + + /// Y coordinate. + final double y; + + /// Calculate the vector to another point. + Vector2 differenceTo(PdfPoint other) => Vector2(other.x - x, other.y - y); + + @override + String toString() => 'PdfOffset($x, $y)'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is PdfPoint && other.x == x && other.y == y; + } + + @override + int get hashCode => x.hashCode ^ y.hashCode; + + double distanceSquaredTo(PdfPoint other) { + final dx = x - other.x; + final dy = y - other.y; + return dx * dx + dy * dy; + } + + /// Rotate the point. + PdfPoint rotate(int rotation, PdfPage page) { + final swap = (page.rotation.index & 1) == 1; + final width = swap ? page.height : page.width; + final height = swap ? page.width : page.height; + switch (rotation & 3) { + case 0: + return this; + case 1: + return PdfPoint(y, width - x); + case 2: + return PdfPoint(width - x, height - y); + case 3: + return PdfPoint(height - y, x); + default: + throw ArgumentError.value(rotate, 'rotate'); + } + } + + /// Rotate the point in reverse direction. + PdfPoint rotateReverse(int rotation, PdfPage page) { + final swap = (page.rotation.index & 1) == 1; + final width = swap ? page.height : page.width; + final height = swap ? page.width : page.height; + switch (rotation & 3) { + case 0: + return this; + case 1: + return PdfPoint(width - y, x); + case 2: + return PdfPoint(width - x, height - y); + case 3: + return PdfPoint(y, height - x); + default: + throw ArgumentError.value(rotate, 'rotate'); + } + } + + /// Translate the point. + /// + /// [dx] is added to x, and [dy] is added to y. + PdfPoint translate(double dx, double dy) => PdfPoint(x + dx, y + dy); +} diff --git a/packages/pdfrx_engine/lib/src/pdf_rect.dart b/packages/pdfrx_engine/lib/src/pdf_rect.dart new file mode 100644 index 00000000..15afd534 --- /dev/null +++ b/packages/pdfrx_engine/lib/src/pdf_rect.dart @@ -0,0 +1,185 @@ +import 'pdf_page.dart'; +import 'pdf_point.dart'; + +/// Rectangle in PDF page coordinates. +/// +/// Please note that PDF page coordinates is different from Flutter's coordinate. +/// PDF page coordinates's origin is at the bottom-left corner and Y-axis is pointing upward; +/// [bottom] is generally smaller than [top]. +/// The unit is normally in points (1/72 inch). +class PdfRect { + const PdfRect(this.left, this.top, this.right, this.bottom) + : assert(left <= right, 'Left coordinate must be less than or equal to right coordinate.'), + assert(top >= bottom, 'Top coordinate must be greater than or equal to bottom coordinate.'); + + /// Left coordinate. + final double left; + + /// Top coordinate (bigger than [bottom]). + final double top; + + /// Right coordinate. + final double right; + + /// Bottom coordinate (smaller than [top]). + final double bottom; + + /// Determine whether the rectangle is empty. + bool get isEmpty => left >= right || top <= bottom; + + /// Determine whether the rectangle is *NOT* empty. + bool get isNotEmpty => !isEmpty; + + /// Width of the rectangle. + double get width => right - left; + + /// Height of the rectangle. + double get height => top - bottom; + + /// Top-left point of the rectangle. + PdfPoint get topLeft => PdfPoint(left, top); + + /// Top-right point of the rectangle. + PdfPoint get topRight => PdfPoint(right, top); + + /// Bottom-left point of the rectangle. + PdfPoint get bottomLeft => PdfPoint(left, bottom); + + /// Bottom-right point of the rectangle. + PdfPoint get bottomRight => PdfPoint(right, bottom); + + /// Center point of the rectangle. + PdfPoint get center => PdfPoint((left + right) / 2, (top + bottom) / 2); + + /// Merge two rectangles. + PdfRect merge(PdfRect other) { + return PdfRect( + left < other.left ? left : other.left, + top > other.top ? top : other.top, + right > other.right ? right : other.right, + bottom < other.bottom ? bottom : other.bottom, + ); + } + + /// Determine whether the rectangle contains the specified point (in the PDF page coordinates). + bool containsXy(double x, double y, {double margin = 0}) => + x >= left - margin && x <= right + margin && y >= bottom - margin && y <= top + margin; + + /// Determine whether the rectangle contains the specified point (in the PDF page coordinates). + bool containsPoint(PdfPoint offset, {double margin = 0}) => containsXy(offset.x, offset.y, margin: margin); + + double distanceSquaredTo(PdfPoint point) { + if (containsPoint(point)) { + return 0.0; // inside the rectangle + } + final dx = point.x.clamp(left, right) - point.x; + final dy = point.y.clamp(bottom, top) - point.y; + return dx * dx + dy * dy; + } + + /// Determine whether the rectangle overlaps the specified rectangle (in the PDF page coordinates). + bool overlaps(PdfRect other) { + return left < other.right && + right > other.left && + top > other.bottom && + bottom < other.top; // PDF page coordinates: top is bigger than bottom + } + + /// Empty rectangle. + static const empty = PdfRect(0, 0, 0, 0); + + /// Rotate the rectangle. + PdfRect rotate(int rotation, PdfPage page) { + final swap = (page.rotation.index & 1) == 1; + final width = swap ? page.height : page.width; + final height = swap ? page.width : page.height; + switch (rotation & 3) { + case 0: + return this; + case 1: + return PdfRect(bottom, width - left, top, width - right); + case 2: + return PdfRect(width - right, height - bottom, width - left, height - top); + case 3: + return PdfRect(height - top, right, height - bottom, left); + default: + throw ArgumentError.value(rotation, 'rotation'); + } + } + + /// Rotate the rectangle in reverse direction. + PdfRect rotateReverse(int rotation, PdfPage page) { + final swap = (page.rotation.index & 1) == 1; + final width = swap ? page.height : page.width; + final height = swap ? page.width : page.height; + switch (rotation & 3) { + case 0: + return this; + case 1: + return PdfRect(width - top, right, width - bottom, left); + case 2: + return PdfRect(width - right, height - bottom, width - left, height - top); + case 3: + return PdfRect(bottom, height - left, top, height - right); + default: + throw ArgumentError.value(rotation, 'rotation'); + } + } + + /// Inflate (or deflate) the rectangle. + /// + /// [dx] is added to left and right, and [dy] is added to top and bottom. + PdfRect inflate(double dx, double dy) => PdfRect(left - dx, top + dy, right + dx, bottom - dy); + + /// Translate the rectangle. + /// + /// [dx] is added to left and right, and [dy] is added to top and bottom. + PdfRect translate(double dx, double dy) => PdfRect(left + dx, top + dy, right + dx, bottom + dy); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is PdfRect && other.left == left && other.top == top && other.right == right && other.bottom == bottom; + } + + @override + int get hashCode => left.hashCode ^ top.hashCode ^ right.hashCode ^ bottom.hashCode; + + @override + String toString() { + return 'PdfRect(left: $left, top: $top, right: $right, bottom: $bottom)'; + } +} + +/// Extension methods for List of [PdfRect]. +extension PdfRectsExt on Iterable { + /// Calculate the bounding rectangle of the list of rectangles. + PdfRect boundingRect({int? start, int? end}) { + start ??= 0; + end ??= length; + var left = double.infinity; + var top = double.negativeInfinity; + var right = double.negativeInfinity; + var bottom = double.infinity; + for (final r in skip(start).take(end - start)) { + if (r.left < left) { + left = r.left; + } + if (r.top > top) { + top = r.top; + } + if (r.right > right) { + right = r.right; + } + if (r.bottom < bottom) { + bottom = r.bottom; + } + } + if (left == double.infinity) { + // no rects + throw StateError('No rects'); + } + return PdfRect(left, top, right, bottom); + } +} diff --git a/packages/pdfrx_engine/lib/src/pdf_text.dart b/packages/pdfrx_engine/lib/src/pdf_text.dart new file mode 100644 index 00000000..94eb9db3 --- /dev/null +++ b/packages/pdfrx_engine/lib/src/pdf_text.dart @@ -0,0 +1,335 @@ +import 'dart:math'; + +import 'package:collection/collection.dart'; + +import 'pdf_page.dart'; +import 'pdf_rect.dart'; +import 'utils/list_equals.dart'; + +/// PDF's raw text and its associated character bounding boxes. +class PdfPageRawText { + PdfPageRawText(this.fullText, this.charRects); + + /// Full text of the page. + final String fullText; + + /// Bounds corresponding to characters in the full text. + final List charRects; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is PdfPageRawText && other.fullText == fullText && listEquals(other.charRects, charRects); + } + + @override + int get hashCode => fullText.hashCode ^ charRects.hashCode; +} + +/// Handles text extraction from PDF page. +/// +/// See [PdfPage.loadText]. +class PdfPageText { + const PdfPageText({ + required this.pageNumber, + required this.fullText, + required this.charRects, + required this.fragments, + }); + + /// Page number. The first page is 1. + final int pageNumber; + + /// Full text of the page. + final String fullText; + + /// Bounds corresponding to characters in the full text. + final List charRects; + + /// Get text fragments that organizes the full text structure. + /// + /// The [fullText] is the composed result of all fragments' text. + /// Any character in [fullText] must be included in one of the fragments. + final List fragments; + + /// Find text fragment index for the specified text index. + /// + /// If the specified text index is out of range, it returns -1; + /// only the exception is [textIndex] is equal to [fullText].length, + /// which means the end of the text and it returns [fragments].length. + int getFragmentIndexForTextIndex(int textIndex) { + if (textIndex == fullText.length) { + return fragments.length; // the end of the text + } + final searchIndex = PdfPageTextFragment( + pageText: this, + index: textIndex, + length: 0, + bounds: PdfRect.empty, + charRects: const [], + direction: PdfTextDirection.unknown, + ); + final index = fragments.lowerBound(searchIndex, (a, b) => a.index - b.index); + if (index > fragments.length) { + return -1; // range error + } + if (index == fragments.length) { + final f = fragments.last; + if (textIndex >= f.index + f.length) { + return -1; // range error + } + return index - 1; + } + + final f = fragments[index]; + if (textIndex < f.index) { + return index - 1; + } + return index; + } + + /// Get text fragment for the specified text index. + /// + /// If the specified text index is out of range, it returns null. + PdfPageTextFragment? getFragmentForTextIndex(int textIndex) { + final index = getFragmentIndexForTextIndex(textIndex); + if (index < 0 || index >= fragments.length) { + return null; // range error + } + return fragments[index]; + } + + /// Search text with [pattern]. + /// + /// Just work like [Pattern.allMatches] but it returns stream of [PdfPageTextRange]. + /// [caseInsensitive] is used to specify case-insensitive search only if [pattern] is [String]. + Stream allMatches(Pattern pattern, {bool caseInsensitive = true}) async* { + final String text; + if (pattern is RegExp) { + caseInsensitive = pattern.isCaseSensitive; + text = fullText; + } else if (pattern is String) { + pattern = caseInsensitive ? pattern.toLowerCase() : pattern; + text = caseInsensitive ? fullText.toLowerCase() : fullText; + } else { + throw ArgumentError.value(pattern, 'pattern'); + } + final matches = pattern.allMatches(text); + for (final match in matches) { + if (match.start == match.end) continue; + final m = PdfPageTextRange(pageText: this, start: match.start, end: match.end); + yield m; + } + } + + /// Create a [PdfPageTextRange] from two character indices. + /// + /// Unlike [PdfPageTextRange.end], both [a] and [b] are inclusive character indices in [fullText] and + /// [a] and [b] can be in any order (e.g., [a] can be greater than [b]). + PdfPageTextRange getRangeFromAB(int a, int b) { + final min = a < b ? a : b; + final max = a < b ? b : a; + if (min < 0 || max > fullText.length) { + throw RangeError('Indices out of range: $min, $max for fullText length ${fullText.length}.'); + } + return PdfPageTextRange(pageText: this, start: min, end: max + 1); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is PdfPageText && + other.pageNumber == pageNumber && + other.fullText == fullText && + listEquals(other.charRects, charRects) && + listEquals(other.fragments, fragments); + } + + @override + int get hashCode => pageNumber.hashCode ^ fullText.hashCode ^ charRects.hashCode ^ fragments.hashCode; +} + +/// Text direction in PDF page. +enum PdfTextDirection { + /// Left to Right + ltr, + + /// Right to Left + rtl, + + /// Vertical (top to bottom), Right to Left. + vrtl, + + /// Unknown direction, e.g., no text or no text direction can be determined. + unknown, +} + +/// Text fragment in PDF page. +class PdfPageTextFragment { + const PdfPageTextFragment({ + required this.pageText, + required this.index, + required this.length, + required this.bounds, + required this.charRects, + required this.direction, + }); + + /// Owner of the fragment. + final PdfPageText pageText; + + /// Fragment's index on [PdfPageText.fullText]; [text] is the substring of [PdfPageText.fullText] at [index]. + final int index; + + /// Length of the text fragment. + final int length; + + /// End index of the text fragment on [PdfPageText.fullText]. + int get end => index + length; + + /// Bounds of the text fragment in PDF page coordinates. + final PdfRect bounds; + + /// The fragment's child character bounding boxes in PDF page coordinates. + final List charRects; + + /// Text direction of the fragment. + final PdfTextDirection direction; + + /// Text for the fragment. + String get text => pageText.fullText.substring(index, index + length); + + @override + bool operator ==(covariant PdfPageTextFragment other) { + if (identical(this, other)) return true; + + return other.index == index && + other.bounds == bounds && + listEquals(other.charRects, charRects) && + other.text == text; + } + + @override + int get hashCode => index.hashCode ^ bounds.hashCode ^ text.hashCode; +} + +/// Text range in a PDF page, which is typically used to describe text selection. +class PdfPageTextRange { + /// Create a [PdfPageTextRange]. + /// + /// [start] is inclusive and [end] is exclusive. + const PdfPageTextRange({required this.pageText, required this.start, required this.end}); + + /// The page text the text range are associated with. + final PdfPageText pageText; + + /// Text start index in [PdfPageText.fullText]. + final int start; + + /// Text end index in [PdfPageText.fullText]. + final int end; + + /// Page number of the text range. + int get pageNumber => pageText.pageNumber; + + /// The composed text of the text range. + String get text => pageText.fullText.substring(start, end); + + /// The bounding rectangle of the text range in PDF page coordinates. + PdfRect get bounds => pageText.charRects.boundingRect(start: start, end: end); + + /// Get the first text fragment index corresponding to the text range. + /// + /// It can be used with [PdfPageText.fragments] to get the first text fragment in the range. + int get firstFragmentIndex => pageText.getFragmentIndexForTextIndex(start); + + /// Get the last text fragment index corresponding to the text range. + /// + /// It can be used with [PdfPageText.fragments] to get the last text fragment in the range. + int get lastFragmentIndex => pageText.getFragmentIndexForTextIndex(end - 1); + + /// Get the first text fragment in the range. + PdfPageTextFragment? get firstFragment { + final index = firstFragmentIndex; + if (index < 0 || index >= pageText.fragments.length) { + return null; // range error + } + return pageText.fragments[index]; + } + + /// Get the last text fragment in the range. + PdfPageTextFragment? get lastFragment { + final index = lastFragmentIndex; + if (index < 0 || index >= pageText.fragments.length) { + return null; // range error + } + return pageText.fragments[index]; + } + + /// Enumerate all the fragment bounding rectangles for the text range. + /// + /// The function is useful when you implement text selection algorithm or such. + Iterable enumerateFragmentBoundingRects() sync* { + final fStart = firstFragmentIndex; + final fEnd = lastFragmentIndex; + for (var i = fStart; i <= fEnd; i++) { + final f = pageText.fragments[i]; + if (f.end <= start || end <= f.index) continue; + yield PdfTextFragmentBoundingRect(f, max(start - f.index, 0), min(end - f.index, f.length)); + } + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is PdfPageTextRange && other.pageText == pageText && other.start == start && other.end == end; + } + + @override + int get hashCode => pageText.hashCode ^ start.hashCode ^ end.hashCode; +} + +/// Bounding rectangle for a text range in a PDF page. +class PdfTextFragmentBoundingRect { + const PdfTextFragmentBoundingRect(this.fragment, this.sif, this.eif); + + /// Associated text fragment. + final PdfPageTextFragment fragment; + + /// In fragment text start index (Start-In-Fragment) + /// + /// It is the character index in the [PdfPageTextFragment.charRects]/[PdfPageTextFragment.text] + /// of the associated [fragment]. + final int sif; + + /// In fragment text end index (End-In-Fragment). + /// + /// It is the end character index in the [PdfPageTextFragment.charRects]/[PdfPageTextFragment.text] + /// of the associated [fragment]. + final int eif; + + /// Rectangle in PDF page coordinates. + PdfRect get bounds => fragment.pageText.charRects.boundingRect(start: start, end: end); + + /// Start index of the text range in page's full text. + int get start => fragment.index + sif; + + /// End index of the text range in page's full text. + int get end => fragment.index + eif; + + /// Text direction of the text range. + PdfTextDirection get direction => fragment.direction; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is PdfTextFragmentBoundingRect && other.fragment == fragment && other.sif == sif && other.eif == eif; + } + + @override + int get hashCode => fragment.hashCode ^ sif.hashCode ^ eif.hashCode; +} diff --git a/packages/pdfrx_engine/lib/src/pdf_text_formatter.dart b/packages/pdfrx_engine/lib/src/pdf_text_formatter.dart new file mode 100644 index 00000000..7eccbf60 --- /dev/null +++ b/packages/pdfrx_engine/lib/src/pdf_text_formatter.dart @@ -0,0 +1,279 @@ +import 'dart:collection'; + +import 'package:vector_math/vector_math_64.dart'; + +import './mock/string_buffer_wrapper.dart' if (dart.library.io) './native/string_buffer_wrapper.dart'; +import 'pdf_page.dart'; +import 'pdf_rect.dart'; +import 'pdf_text.dart'; +import 'utils/unmodifiable_list.dart'; + +/// Text formatter to load structured text from PDF page. +/// +/// The class provides functions to load structured text with character bounding boxes. +final class PdfTextFormatter { + static final _reSpaces = RegExp(r'(\s+)', unicode: true); + static final _reNewLine = RegExp(r'\r?\n', unicode: true); + + /// Load structured text with character bounding boxes for the page. + /// + /// The function internally does test flow analysis (reading order) and line segmentation to detect + /// text direction and line breaks. + /// + /// To access the raw text, use [PdfPage.loadText]. + /// + /// This implementation is shared among multiple [PdfPage] and [PdfPage] proxy implementations. + static Future loadStructuredText(PdfPage page, {required int? pageNumberOverride}) async { + pageNumberOverride ??= page.pageNumber; + final raw = await _loadFormattedText(page); + if (raw == null) { + return PdfPageText(pageNumber: pageNumberOverride, fullText: '', charRects: [], fragments: []); + } + final inputCharRects = raw.charRects; + final inputFullText = raw.fullText; + + final fragmentsTmp = <({int length, PdfTextDirection direction})>[]; + + /// Ugly workaround for WASM+Safari StringBuffer issue (#483). + final outputText = createStringBufferForWorkaroundSafariWasm(); + final outputCharRects = []; + + PdfTextDirection vector2direction(Vector2 v) { + if (v.x.abs() > v.y.abs()) { + return v.x > 0 ? PdfTextDirection.ltr : PdfTextDirection.rtl; + } else { + return PdfTextDirection.vrtl; + } + } + + PdfTextDirection getLineDirection(int start, int end) { + if (start == end || start + 1 == end) return PdfTextDirection.unknown; + return vector2direction(inputCharRects[start].center.differenceTo(inputCharRects[end - 1].center)); + } + + void addWord( + int wordStart, + int wordEnd, + PdfTextDirection dir, + PdfRect bounds, { + bool isSpace = false, + bool isNewLine = false, + }) { + if (wordStart < wordEnd) { + final pos = outputText.length; + if (isSpace) { + if (wordStart > 0 && wordEnd < inputCharRects.length) { + // combine several spaces into one space + final a = inputCharRects[wordStart - 1]; + final b = inputCharRects[wordEnd]; + switch (dir) { + case PdfTextDirection.ltr: + case PdfTextDirection.unknown: + outputCharRects.add(PdfRect(a.right, bounds.top, a.right < b.left ? b.left : a.right, bounds.bottom)); + case PdfTextDirection.rtl: + outputCharRects.add(PdfRect(b.right, bounds.top, b.right < a.left ? a.left : b.right, bounds.bottom)); + case PdfTextDirection.vrtl: + outputCharRects.add(PdfRect(bounds.left, a.bottom, bounds.right, a.bottom > b.top ? b.top : a.bottom)); + } + outputText.write(' '); + } + } else if (isNewLine) { + if (wordStart > 0) { + // new line (\n) + switch (dir) { + case PdfTextDirection.ltr: + case PdfTextDirection.unknown: + outputCharRects.add(PdfRect(bounds.right, bounds.top, bounds.right, bounds.bottom)); + case PdfTextDirection.rtl: + outputCharRects.add(PdfRect(bounds.left, bounds.top, bounds.left, bounds.bottom)); + case PdfTextDirection.vrtl: + outputCharRects.add(PdfRect(bounds.left, bounds.bottom, bounds.right, bounds.bottom)); + } + outputText.write('\n'); + } + } else { + // Adjust character bounding box based on text direction. + switch (dir) { + case PdfTextDirection.ltr: + case PdfTextDirection.rtl: + case PdfTextDirection.unknown: + for (var i = wordStart; i < wordEnd; i++) { + final r = inputCharRects[i]; + outputCharRects.add(PdfRect(r.left, bounds.top, r.right, bounds.bottom)); + } + case PdfTextDirection.vrtl: + for (var i = wordStart; i < wordEnd; i++) { + final r = inputCharRects[i]; + outputCharRects.add(PdfRect(bounds.left, r.top, bounds.right, r.bottom)); + } + } + outputText.write(inputFullText.substring(wordStart, wordEnd)); + } + if (outputText.length > pos) fragmentsTmp.add((length: outputText.length - pos, direction: dir)); + } + } + + int addWords(int start, int end, PdfTextDirection dir, PdfRect bounds) { + final firstIndex = fragmentsTmp.length; + final matches = _reSpaces.allMatches(inputFullText.substring(start, end)); + var wordStart = start; + for (final match in matches) { + final spaceStart = start + match.start; + addWord(wordStart, spaceStart, dir, bounds); + wordStart = start + match.end; + addWord(spaceStart, wordStart, dir, bounds, isSpace: true); + } + addWord(wordStart, end, dir, bounds); + return fragmentsTmp.length - firstIndex; + } + + Vector2 charVec(int index, Vector2 prev) { + if (index + 1 >= inputCharRects.length) { + return prev; + } + final next = inputCharRects[index + 1]; + if (next.isEmpty) { + return prev; + } + final cur = inputCharRects[index]; + return cur.center.differenceTo(next.center); + } + + List<({int start, int end, PdfTextDirection dir})> splitLine(int start, int end) { + final list = <({int start, int end, PdfTextDirection dir})>[]; + final lineThreshold = 1.5; // radians + final last = end - 1; + var curStart = start; + var curVec = charVec(start, Vector2(1, 0)); + for (var next = start + 1; next < last;) { + final nextVec = charVec(next, curVec); + if (curVec.angleTo(nextVec) > lineThreshold) { + list.add((start: curStart, end: next + 1, dir: vector2direction(curVec))); + curStart = next + 1; + if (next + 2 == end) break; + curVec = charVec(next + 1, nextVec); + next += 2; + continue; + } + curVec += nextVec; + next++; + } + if (curStart < end) { + list.add((start: curStart, end: end, dir: vector2direction(curVec))); + } + return list; + } + + void handleLine(int start, int end, {int? newLineEnd}) { + final dir = getLineDirection(start, end); + final segments = splitLine(start, end).toList(); + if (segments.length >= 2) { + for (var i = 0; i < segments.length; i++) { + final seg = segments[i]; + final bounds = inputCharRects.boundingRect(start: seg.start, end: seg.end); + addWords(seg.start, seg.end, seg.dir, bounds); + if (i + 1 == segments.length && newLineEnd != null) { + addWord(seg.end, newLineEnd, seg.dir, bounds, isNewLine: true); + } + } + } else { + final bounds = inputCharRects.boundingRect(start: start, end: end); + addWords(start, end, dir, bounds); + if (newLineEnd != null) { + addWord(end, newLineEnd, dir, bounds, isNewLine: true); + } + } + } + + var lineStart = 0; + for (final match in _reNewLine.allMatches(inputFullText)) { + if (lineStart < match.start) { + handleLine(lineStart, match.start, newLineEnd: match.end); + } else { + final lastRect = outputCharRects.last; + outputCharRects.add(PdfRect(lastRect.left, lastRect.top, lastRect.left, lastRect.bottom)); + outputText.write('\n'); + } + lineStart = match.end; + } + if (lineStart < inputFullText.length) { + handleLine(lineStart, inputFullText.length); + } + + final fragments = []; + final text = PdfPageText( + pageNumber: pageNumberOverride, + fullText: outputText.toString(), + charRects: outputCharRects, + fragments: UnmodifiableListView(fragments), + ); + + var start = 0; + for (var i = 0; i < fragmentsTmp.length; i++) { + final length = fragmentsTmp[i].length; + final direction = fragmentsTmp[i].direction; + final end = start + length; + final fragmentRects = UnmodifiableSublist(outputCharRects, start: start, end: end); + fragments.add( + PdfPageTextFragment( + pageText: text, + index: start, + length: length, + charRects: fragmentRects, + bounds: fragmentRects.boundingRect(), + direction: direction, + ), + ); + start = end; + } + + return text; + } + + static Future _loadFormattedText(PdfPage page) async { + final input = await page.loadText(); + if (input == null) { + return null; + } + + final fullText = StringBuffer(); + final charRects = []; + + // Process the whole text + final lnMatches = _reNewLine.allMatches(input.fullText).toList(); + var lineStart = 0; + var prevEnd = 0; + for (var i = 0; i < lnMatches.length; i++) { + lineStart = prevEnd; + final match = lnMatches[i]; + fullText.write(input.fullText.substring(lineStart, match.start)); + charRects.addAll(input.charRects.sublist(lineStart, match.start)); + prevEnd = match.end; + + // Microsoft Word sometimes outputs vertical text like this: "縦\n書\nき\nの\nテ\nキ\nス\nト\nで\nす\n。\n" + // And, we want to remove these line-feeds. + if (i + 1 < lnMatches.length) { + final next = lnMatches[i + 1]; + final len = match.start - lineStart; + final nextLen = next.start - match.end; + if (len == 1 && nextLen == 1) { + final rect = input.charRects[lineStart]; + final nextRect = input.charRects[match.end]; + final nextCenterX = nextRect.center.x; + if (rect.left < nextCenterX && nextCenterX < rect.right && rect.top > nextRect.top) { + // The line is vertical, and the line-feed is virtual + continue; + } + } + } + fullText.write(input.fullText.substring(match.start, match.end)); + charRects.addAll(input.charRects.sublist(match.start, match.end)); + } + if (prevEnd < input.fullText.length) { + fullText.write(input.fullText.substring(prevEnd)); + charRects.addAll(input.charRects.sublist(prevEnd)); + } + + return PdfPageRawText(fullText.toString(), charRects); + } +} diff --git a/packages/pdfrx_engine/lib/src/pdfrx.dart b/packages/pdfrx_engine/lib/src/pdfrx.dart new file mode 100644 index 00000000..c7140dcf --- /dev/null +++ b/packages/pdfrx_engine/lib/src/pdfrx.dart @@ -0,0 +1,60 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:http/http.dart' as http; + +/// Class to provide Pdfrx's configuration. +/// The parameters should be set before calling any Pdfrx's functions. +/// +class Pdfrx { + Pdfrx._(); + + /// Explicitly specify pdfium module path for special purpose. + /// + /// It is not supported on Flutter Web. + static String? pdfiumModulePath; + + /// Font paths scanned by pdfium if supported. + /// + /// It should be set before calling any Pdfrx's functions. + /// + /// It is not supported on Flutter Web. + static final fontPaths = []; + + /// Overriding the default HTTP client for PDF download. + /// + /// It is not supported on Flutter Web. + static http.Client Function()? createHttpClient; + + /// To override the default pdfium WASM modules directory URL. It must be terminated by '/'. + static String? pdfiumWasmModulesUrl; + + /// HTTP headers to use when fetching the PDFium WASM module. + /// This is useful for authentication on protected servers. + /// Only supported on Flutter Web. + static Map? pdfiumWasmHeaders; + + /// Whether to include credentials (cookies) when fetching the PDFium WASM module. + /// This is useful for authentication on protected servers. + /// Only supported on Flutter Web. + static bool pdfiumWasmWithCredentials = false; + + /// Function to load asset data. + /// + /// This function is used to load PDF files from assets. + /// It is used to isolate pdfrx API implementation from Flutter framework. + /// + /// For Flutter, `pdfrxFlutterInitialize` should be called explicitly or implicitly before using this class. + /// For Dart only, you can set this function to load assets from your own asset management system. + static Future Function(String name)? loadAsset; + + /// Function to determine the cache directory. + /// + /// You can override the default cache directory by setting this variable. + /// + /// For Flutter, `pdfrxFlutterInitialize` should be called explicitly or implicitly before using this class. + /// For Dart only, you can set this function to obtain the cache directory from your own file system. + static FutureOr Function()? getCacheDirectory; + + static Map? pdfiumNativeBindings; +} diff --git a/packages/pdfrx_engine/lib/src/pdfrx_dart.dart b/packages/pdfrx_engine/lib/src/pdfrx_dart.dart new file mode 100644 index 00000000..f6455c33 --- /dev/null +++ b/packages/pdfrx_engine/lib/src/pdfrx_dart.dart @@ -0,0 +1,60 @@ +import 'package:image/image.dart'; + +import 'pdf_image.dart'; + +extension PdfImageDartExt on PdfImage { + /// Create [Image] (of [image package](https://pub.dev/packages/image)) from the rendered image. + /// + /// [pixelSizeThreshold] specifies the maximum allowed pixel size (width or height). + /// If the image exceeds this size, it will be downscaled to fit within the threshold + /// while maintaining the aspect ratio. + /// [interpolation] specifies the interpolation method to use when resizing images. + /// + /// **NF**: This method does not require Flutter and can be used in pure Dart applications. + Image createImageNF({int? pixelSizeThreshold, Interpolation interpolation = Interpolation.linear}) { + final image = Image.fromBytes( + width: width, + height: height, + bytes: pixels.buffer, + numChannels: 4, + order: ChannelOrder.bgra, + ); + + if (pixelSizeThreshold != null && (width > pixelSizeThreshold || height > pixelSizeThreshold)) { + final aspectRatio = width / height; + int targetWidth; + int targetHeight; + if (width >= height) { + targetWidth = pixelSizeThreshold; + targetHeight = (pixelSizeThreshold / aspectRatio).round(); + } else { + targetHeight = pixelSizeThreshold; + targetWidth = (pixelSizeThreshold * aspectRatio).round(); + } + return copyResize(image, width: targetWidth, height: targetHeight, interpolation: interpolation); + } + return image; + } +} + +extension ImageDartExt on Image { + /// Create [PdfImage] from the rendered image. + /// + /// **NF**: This method does not require Flutter and can be used in pure Dart applications. + /// + /// By default, the function assumes that the image data is in RGBA format and performs conversion to BGRA. + /// - If the image data is already in BGRA format, set [order] to [ChannelOrder.bgra] to prevent unnecessary conversion. + /// - If [bgraConversionInPlace] is set to true and conversion is needed, the conversion will be done in place + /// modifying the original image data. This can save memory but will alter the original image. + PdfImage toPdfImageNF({ChannelOrder order = ChannelOrder.rgba, bool bgraConversionInPlace = false}) { + if (data == null) { + throw StateError('The image has no pixel data.'); + } + final needsConversion = order != ChannelOrder.bgra; + return PdfImage.createFromBgraData( + needsConversion ? data!.getBytes(order: ChannelOrder.bgra, inPlace: bgraConversionInPlace) : getBytes(), + width: width, + height: height, + ); + } +} diff --git a/packages/pdfrx_engine/lib/src/pdfrx_entry_functions.dart b/packages/pdfrx_engine/lib/src/pdfrx_entry_functions.dart new file mode 100644 index 00000000..8924fa84 --- /dev/null +++ b/packages/pdfrx_engine/lib/src/pdfrx_entry_functions.dart @@ -0,0 +1,159 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import './mock/pdfrx_mock.dart' if (dart.library.io) './native/pdfrx_pdfium.dart'; +import 'pdf_document.dart'; +import 'pdfrx.dart'; + +/// The class is used to implement Pdfrx's backend functions. +/// +/// In normal usage, you should use [PdfDocument]'s static functions to open PDF files instead of using this class directly. +/// +/// [pdfrx_coregraphics](https://pub.dev/packages/pdfrx_coregraphics) provide an alternative implementation of this +/// class for Apple platforms. +abstract class PdfrxEntryFunctions { + /// Singleton instance of [PdfrxEntryFunctions]. + /// + /// [PdfDocument] internally calls this instance to open PDF files. + static PdfrxEntryFunctions instance = PdfrxEntryFunctionsImpl(); + + /// Call `FPDF_InitLibraryWithConfig` to initialize the PDFium library. + /// + /// For actual apps, call `pdfrxFlutterInitialize` (for Flutter) or `pdfrxInitialize` (for Dart only) instead of this function. + Future init(); + + /// This function blocks pdfrx internally calls PDFium functions during the execution of [action]. + /// + /// Because PDFium is not thread-safe, if your app is calling some other libraries that potentially calls PDFium + /// functions, pdfrx may interfere with those calls and cause crashes or data corruption. + /// To avoid such problems, you can wrap the code that calls those libraries with this function. + Future suspendPdfiumWorkerDuringAction(FutureOr Function() action); + + /// Perform a computation in the background worker isolate. + /// + /// The [callback] function is executed in the background isolate with [message] as its argument. + /// The result of the [callback] function is returned as a [Future]. + /// + /// The background worker isolate is same to the one used by pdfrx internally to call PDFium + /// functions. + /// + /// This function is only available for native PDFium backend; for other backends, calling this function + /// will throw an [UnimplementedError]. + Future compute(FutureOr Function(M message) callback, M message); + + /// **Experimental** + /// Stop the background worker isolate. + /// + /// This function can be called anytime to stop the background worker isolate. + /// If you call [compute] after calling this function, the background worker isolate will be recreated automatically. + /// + /// The function internally calls `FPDF_DestroyLibrary` and then stops the isolate. + /// You should ensure any PDFium-related resources are properly released before calling this function. + /// + /// This function is only available for native PDFium backend; for other backends, calling this function + /// will throw an [UnimplementedError]. + Future stopBackgroundWorker(); + + /// See [PdfDocument.openAsset]. + Future openAsset( + String name, { + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, + }); + + /// See [PdfDocument.openData]. + Future openData( + Uint8List data, { + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + String? sourceName, + bool allowDataOwnershipTransfer = false, // only for Web + bool useProgressiveLoading = false, + void Function()? onDispose, + }); + + /// See [PdfDocument.openFile]. + Future openFile( + String filePath, { + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, + }); + + /// See [PdfDocument.openCustom]. + Future openCustom({ + required FutureOr Function(Uint8List buffer, int position, int size) read, + required int fileSize, + required String sourceName, + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, + int? maxSizeToCacheOnMemory, + void Function()? onDispose, + }); + + /// See [PdfDocument.openUri]. + Future openUri( + Uri uri, { + PdfPasswordProvider? passwordProvider, + bool firstAttemptByEmptyPassword = true, + bool useProgressiveLoading = false, + PdfDownloadProgressCallback? progressCallback, + bool preferRangeAccess = false, + Map? headers, + bool withCredentials = false, + Duration? timeout, + }); + + /// See [PdfDocument.createNew]. + Future createNew({required String sourceName}); + + /// See [PdfDocument.createFromJpegData]. + Future createFromJpegData( + Uint8List jpegData, { + required double width, + required double height, + required String sourceName, + }); + + /// Reload the fonts. + Future reloadFonts(); + + /// Add font data to font cache. + /// + /// For Web platform, this is the only way to add custom fonts (the fonts are cached on memory). + /// + /// For other platforms, the font data is cached on temporary files in the cache directory; if you want to keep + /// the font data permanently, you should save the font data to some other persistent storage and set its path + /// to [Pdfrx.fontPaths]. + Future addFontData({required String face, required Uint8List data}); + + /// Clear all font data added by [addFontData]. + Future clearAllFontData(); + + /// Backend in use. + PdfrxBackendType get backendType; +} + +/// Pdfrx backend types. +enum PdfrxBackendType { + /// PDFium backend. + pdfium, + + /// PDFium WebAssembly backend for Web platform. + /// + /// The implementation for this is provided by [pdfrx](https://pub.dev/packages/pdfrx) package. + pdfiumWasm, + + /// pdfKit (CoreGraphics) backend for Apple platforms. + /// + /// The implementation for this is provided by [pdfrx_coregraphics](https://pub.dev/packages/pdfrx_coregraphics) package. + pdfKit, + + /// Mock backend for internal consistency. + mock, + + /// Unknown backend. + unknown, +} diff --git a/packages/pdfrx_engine/lib/src/pdfrx_initialize_dart.dart b/packages/pdfrx_engine/lib/src/pdfrx_initialize_dart.dart new file mode 100644 index 00000000..d9f576ad --- /dev/null +++ b/packages/pdfrx_engine/lib/src/pdfrx_initialize_dart.dart @@ -0,0 +1,76 @@ +import 'dart:io'; + +import 'package:path/path.dart' as path; +import 'package:pdfium_dart/pdfium_dart.dart' as pdfium_dart; + +import 'pdfrx.dart'; +import 'pdfrx_entry_functions.dart'; + +bool _isInitialized = false; + +/// Initializes the Pdfrx library for Dart. +/// +/// This function sets up the following: +/// +/// - [Pdfrx.getCacheDirectory] is set to return the system temporary directory. +/// - [Pdfrx.pdfiumModulePath] is configured to point to the pdfium module. +/// - The function checks for the `PDFIUM_PATH` environment variable to find an existing pdfium module. +/// - If Pdfium module is not found, it will be downloaded from the internet. +/// - [Pdfrx.loadAsset] is set to throw an error by default (Dart does not support assets like Flutter does). +/// - Calls [PdfrxEntryFunctions.init] to initialize the PDFium library. +/// +/// For Flutter, you should call `pdfrxFlutterInitialize` instead of the function. +Future pdfrxInitialize({String? tmpPath, String? pdfiumRelease}) async { + if (_isInitialized) return; + + Pdfrx.loadAsset ??= (name) async { + throw UnimplementedError('By default, Pdfrx.loadAsset is not implemented for Dart.'); + }; + + final tmpDir = tmpPath != null ? Directory(tmpPath) : _getPdfrxCacheDirectory(); + Pdfrx.getCacheDirectory ??= () => tmpDir.path; + + final pdfiumPath = Platform.environment['PDFIUM_PATH']; + if (pdfiumPath != null && await File(pdfiumPath).exists()) { + Pdfrx.pdfiumModulePath ??= pdfiumPath; + await PdfrxEntryFunctions.instance.init(); + _isInitialized = true; + return; + } else { + final moduleDir = Directory(tmpDir.path); + await moduleDir.create(recursive: true); + Pdfrx.pdfiumModulePath = await pdfium_dart.PDFiumDownloader.downloadAndGetPDFiumModulePath( + moduleDir.path, + pdfiumRelease: pdfiumRelease, + ); + } + + await PdfrxEntryFunctions.instance.init(); + + _isInitialized = true; +} + +/// Gets the Pdfrx cache directory. +/// +/// If the environment variable `PDFRX_CACHE_DIR` is set, it uses that directory. +/// Otherwise, it defaults to: +/// - On Windows: `%LOCALAPPDATA%\pdfrx` +/// - On other platforms: `~/.pdfrx` +Directory _getPdfrxCacheDirectory() { + final pdfrxCacheDir = Platform.environment['PDFRX_CACHE_DIR']; + if (pdfrxCacheDir != null && pdfrxCacheDir.isNotEmpty) { + return Directory(pdfrxCacheDir); + } + + final tmp = path.join(Directory.systemTemp.path, 'pdfrx.cache'); + final tmpDir = Directory(tmp); + if (tmpDir.existsSync()) { + return tmpDir; + } + + if (Platform.isWindows) { + return Directory(path.join(Platform.environment['LOCALAPPDATA']!, 'pdfrx')); + } else { + return Directory(path.join(Platform.environment['HOME']!, '.pdfrx')); + } +} diff --git a/packages/pdfrx_engine/lib/src/utils/list_equals.dart b/packages/pdfrx_engine/lib/src/utils/list_equals.dart new file mode 100644 index 00000000..d3d502eb --- /dev/null +++ b/packages/pdfrx_engine/lib/src/utils/list_equals.dart @@ -0,0 +1,20 @@ +/// Compares two lists for element-by-element equality. +/// +/// **NOTE: This function is copied from flutter's `foundation` library to remove dependency to Flutter** +bool listEquals(List? a, List? b) { + if (a == null) { + return b == null; + } + if (b == null || a.length != b.length) { + return false; + } + if (identical(a, b)) { + return true; + } + for (var index = 0; index < a.length; index += 1) { + if (a[index] != b[index]) { + return false; + } + } + return true; +} diff --git a/packages/pdfrx_engine/lib/src/utils/shuffle_in_place.dart b/packages/pdfrx_engine/lib/src/utils/shuffle_in_place.dart new file mode 100644 index 00000000..424aa4f1 --- /dev/null +++ b/packages/pdfrx_engine/lib/src/utils/shuffle_in_place.dart @@ -0,0 +1,147 @@ +/// Mixin that provides in-place shuffling capabilities for an array-like collection of items. +mixin ShuffleItemsInPlaceMixin { + /// The current number of items. + int get length; + + /// Moves [count] consecutive item(s) starting at [fromIndex] to [toIndex]. + void move(int fromIndex, int toIndex, int count); + + /// Removes [count] item(s) starting from the given [index]. + void remove(int index, int count); + + /// Duplicates the item(s) at the given [fromIndex] and inserts them at [toIndex]. + void duplicate(int fromIndex, int toIndex, int count); + + /// Inserts a new item at the given index. The [negativeItemIndex] indicates + /// which new item, which is identified by the negative index. + void insertNew(int index, int negativeItemIndex); + + /// Shuffles the items in place according to the given list of resulting item indices. + /// + /// For example, if the current items are [A, B, C, D] and the resultingItemIndices is [2, 0, 1], + /// the resulting items will be [B, C, A]. + /// + /// If the index is negative, a new item (of the negative index) is inserted at that position using [insertNew]. + /// + /// If same index appears multiple times in resultingItemIndices, the item at the index should be duplicated + /// accordingly; only one item can be moved, but the others are created using [duplicate]. + void shuffleInPlaceAccordingToIndices(List resultingItemIndices) { + final originalLength = length; + if (resultingItemIndices.isEmpty) { + if (originalLength > 0) { + remove(0, originalLength); + } + return; + } + + final tokens = <_ArrayOfItemsToken>[ + for (var i = 0; i < originalLength; i++) _ArrayOfItemsToken(originalIndex: i, isOriginal: true), + ]; + + final usageCounts = List.filled(originalLength, 0); + for (var i = 0; i < resultingItemIndices.length; i++) { + final index = resultingItemIndices[i]; + if (index >= 0) { + if (index >= originalLength) { + throw RangeError('resultingItemIndices[$i] = $index is out of range for current length $originalLength'); + } + usageCounts[index]++; + } + } + + for (var i = originalLength - 1; i >= 0; i--) { + if (usageCounts[i] == 0) { + remove(i, 1); + tokens.removeAt(i); + } + } + + final placedCounts = List.filled(originalLength, 0); + var currentIndex = 0; + + while (currentIndex < resultingItemIndices.length) { + if (currentIndex > tokens.length) { + throw StateError('Destination index $currentIndex is out of range for current length ${tokens.length}.'); + } + + final target = resultingItemIndices[currentIndex]; + if (target >= 0) { + final isFirst = placedCounts[target] == 0; + if (isFirst) { + final fromIndex = tokens.indexWhere((token) => token.originalIndex == target && token.isOriginal); + if (fromIndex == -1) { + throw StateError('Item at index $target could not be found for initial placement.'); + } + + var chunkLength = 1; + while (currentIndex + chunkLength < resultingItemIndices.length && fromIndex + chunkLength < tokens.length) { + final nextTarget = resultingItemIndices[currentIndex + chunkLength]; + if (nextTarget < 0 || placedCounts[nextTarget] > 0) break; + final nextToken = tokens[fromIndex + chunkLength]; + if (!nextToken.isOriginal || nextToken.originalIndex != nextTarget) break; + chunkLength++; + } + + var placementIndex = currentIndex; + if (fromIndex != currentIndex) { + final removalIndices = List.generate(chunkLength, (offset) => fromIndex + offset); + move(fromIndex, currentIndex, chunkLength); + final removedTokens = <_ArrayOfItemsToken>[]; + for (var i = removalIndices.length - 1; i >= 0; i--) { + removedTokens.insert(0, tokens.removeAt(removalIndices[i])); + } + var insertIndex = currentIndex; + for (final index in removalIndices) { + if (index < currentIndex) { + insertIndex--; + } + } + if (insertIndex < 0) insertIndex = 0; + if (insertIndex > tokens.length) insertIndex = tokens.length; + tokens.insertAll(insertIndex, removedTokens); + placementIndex = insertIndex; + } + + for (var offset = 0; offset < chunkLength; offset++) { + final token = tokens[placementIndex + offset]; + final originalIndex = token.originalIndex; + if (originalIndex != null) { + placedCounts[originalIndex]++; + } + } + currentIndex += chunkLength; + continue; + } else { + final sourceIndex = tokens.indexWhere((token) => token.originalIndex == target); + if (sourceIndex == -1) { + throw StateError('Item at index $target could not be found for duplication.'); + } + duplicate(sourceIndex, currentIndex, 1); + final newToken = _ArrayOfItemsToken(originalIndex: target, isOriginal: false); + tokens.insert(currentIndex, newToken); + placedCounts[target]++; + } + } else { + insertNew(currentIndex, target); + tokens.insert(currentIndex, const _ArrayOfItemsToken(originalIndex: null, isOriginal: false)); + } + currentIndex++; + } + + final expectedLength = resultingItemIndices.length; + if (tokens.length > expectedLength) { + final extra = tokens.length - expectedLength; + remove(expectedLength, extra); + tokens.removeRange(expectedLength, tokens.length); + } else if (tokens.length < expectedLength) { + throw StateError('Internal length mismatch after shuffling (expected $expectedLength, got ${tokens.length}).'); + } + } +} + +class _ArrayOfItemsToken { + const _ArrayOfItemsToken({required this.originalIndex, required this.isOriginal}); + + final int? originalIndex; + final bool isOriginal; +} diff --git a/packages/pdfrx_engine/lib/src/utils/unmodifiable_list.dart b/packages/pdfrx_engine/lib/src/utils/unmodifiable_list.dart new file mode 100644 index 00000000..42f4f7ed --- /dev/null +++ b/packages/pdfrx_engine/lib/src/utils/unmodifiable_list.dart @@ -0,0 +1,30 @@ +import 'dart:collection'; + +/// An unmodifiable sublist view of a list. +class UnmodifiableSublist extends ListBase { + /// Creates an unmodifiable sublist view of the provided list. + /// + /// Please note that the class assumes the underlying list is also unmodifiable. + /// If the underlying list is modified, the behavior of this class is undefined. + UnmodifiableSublist(this._list, {int start = 0, int? end}) + : assert(start >= 0 && start <= _list.length && (end == null || end >= start)), + _start = start, + length = (end ?? _list.length) - start; + final List _list; + final int _start; + + @override + final int length; + + @override + set length(int newLength) => throw UnsupportedError('Cannot modify the length of an unmodifiable list'); + + @override + T operator [](int index) => _list[index + _start]; + + @override + void operator []=(int index, T value) => throw UnsupportedError('Cannot modify an unmodifiable list'); + + @override + Iterator get iterator => _list.getRange(_start, _start + length).iterator; +} diff --git a/packages/pdfrx_engine/pubspec.yaml b/packages/pdfrx_engine/pubspec.yaml new file mode 100644 index 00000000..ca00097a --- /dev/null +++ b/packages/pdfrx_engine/pubspec.yaml @@ -0,0 +1,31 @@ +name: pdfrx_engine +description: pdfrx_engine is a PDF rendering and manipulation API built on top of PDFium, designed to be used with the pdfrx plugin. Supports viewing, editing, and combining PDF documents. +version: 0.3.9 +homepage: https://github.com/espresso3389/pdfrx +repository: https://github.com/espresso3389/pdfrx/tree/master/packages/pdfrx_engine +issue_tracker: https://github.com/espresso3389/pdfrx/issues + +environment: + sdk: ^3.9.0 +resolution: workspace + +# Some of the dependencies are intentionally not pinned to a specific version +# to allow for more flexibility in resolving versions when used in larger projects. +dependencies: + pdfium_dart: ^0.1.2 + collection: + crypto: ^3.0.6 + ffi: + http: + path: + rxdart: + synchronized: ^3.4.0 + vector_math: ^2.2.0 + archive: ^4.0.7 + image: ^4.5.4 + +dev_dependencies: + lints: ^6.0.0 + test: ^1.26.2 + + diff --git a/packages/pdfrx_engine/test/pdf_document_test.dart b/packages/pdfrx_engine/test/pdf_document_test.dart new file mode 100644 index 00000000..ab017b62 --- /dev/null +++ b/packages/pdfrx_engine/test/pdf_document_test.dart @@ -0,0 +1,212 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:pdfrx_engine/pdfrx_engine.dart'; +import 'package:test/test.dart'; + +import 'utils.dart'; + +final testPdfFile = File('../pdfrx/example/viewer/assets/hello.pdf'); + +void main() { + setUp(() => pdfrxInitialize(tmpPath: tmpRoot.path)); + + test('PdfDocument.openFile', () async => await testDocument(await PdfDocument.openFile(testPdfFile.path))); + test('PdfDocument.openData', () async { + final data = await testPdfFile.readAsBytes(); + await testDocument(await PdfDocument.openData(data)); + }); + test('PdfDocument.openUri', () async { + Pdfrx.createHttpClient = () => + MockClient((request) async => http.Response.bytes(await testPdfFile.readAsBytes(), 200)); + await testDocument(await PdfDocument.openUri(Uri.parse('https://example.com/hello.pdf'))); + }); + + group('PdfDocument.openCustom with maxSizeToCacheOnMemory=0', () { + test('opens PDF with custom read function', () async { + final data = await testPdfFile.readAsBytes(); + + // Custom read function that reads from the data buffer + int readFunc(Uint8List buffer, int position, int size) { + if (position >= data.length) return 0; + final actualSize = (position + size > data.length) ? data.length - position : size; + buffer.setRange(0, actualSize, data, position); + return actualSize; + } + + final doc = await PdfDocument.openCustom( + read: readFunc, + fileSize: data.length, + sourceName: 'custom:test.pdf', + maxSizeToCacheOnMemory: 0, + ); + + await testDocument(doc); + }); + + test('handles multiple concurrent reads', () async { + final data = await testPdfFile.readAsBytes(); + var readCount = 0; + + int readFunc(Uint8List buffer, int position, int size) { + readCount++; + if (position >= data.length) return 0; + final actualSize = (position + size > data.length) ? data.length - position : size; + buffer.setRange(0, actualSize, data, position); + return actualSize; + } + + final doc = await PdfDocument.openCustom( + read: readFunc, + fileSize: data.length, + sourceName: 'custom:concurrent.pdf', + maxSizeToCacheOnMemory: 0, + ); + + await testDocument(doc); + expect(readCount, greaterThan(0), reason: 'Read function should be called at least once'); + }); + + test('handles async read function', () async { + final data = await testPdfFile.readAsBytes(); + + Future asyncReadFunc(Uint8List buffer, int position, int size) async { + // Simulate async delay + await Future.delayed(Duration(milliseconds: 1)); + + if (position >= data.length) return 0; + final actualSize = (position + size > data.length) ? data.length - position : size; + buffer.setRange(0, actualSize, data, position); + return actualSize; + } + + final doc = await PdfDocument.openCustom( + read: asyncReadFunc, + fileSize: data.length, + sourceName: 'custom:async.pdf', + maxSizeToCacheOnMemory: 0, + ); + + await testDocument(doc); + }); + + test('handles read at various positions', () async { + final data = await testPdfFile.readAsBytes(); + final readPositions = []; + + int readFunc(Uint8List buffer, int position, int size) { + readPositions.add(position); + if (position >= data.length) return 0; + final actualSize = (position + size > data.length) ? data.length - position : size; + buffer.setRange(0, actualSize, data, position); + return actualSize; + } + + final doc = await PdfDocument.openCustom( + read: readFunc, + fileSize: data.length, + sourceName: 'custom:positions.pdf', + maxSizeToCacheOnMemory: 0, + ); + + await testDocument(doc); + + // Verify that reads occurred at different positions (random access) + expect(readPositions.isNotEmpty, true, reason: 'Should have read positions recorded'); + // PDFium typically reads from multiple positions for PDF structure + expect(readPositions.toSet().length, greaterThan(1), reason: 'Should read from multiple positions'); + }); + + test('handles read errors gracefully', () async { + int readFunc(Uint8List buffer, int position, int size) { + // Return 0 to indicate EOF/error - no valid PDF data + return 0; + } + + // This should fail because we're not providing valid PDF data + expect( + () async => await PdfDocument.openCustom( + read: readFunc, + fileSize: 1000, + sourceName: 'custom:error.pdf', + maxSizeToCacheOnMemory: 0, + ), + throwsA(isA()), + ); + }); + + test('calls onDispose callback when document is disposed', () async { + final data = await testPdfFile.readAsBytes(); + var disposeCalled = false; + + int readFunc(Uint8List buffer, int position, int size) { + if (position >= data.length) return 0; + final actualSize = (position + size > data.length) ? data.length - position : size; + buffer.setRange(0, actualSize, data, position); + return actualSize; + } + + final doc = await PdfDocument.openCustom( + read: readFunc, + fileSize: data.length, + sourceName: 'custom:dispose.pdf', + maxSizeToCacheOnMemory: 0, + onDispose: () { + disposeCalled = true; + }, + ); + + expect(disposeCalled, false, reason: 'onDispose should not be called yet'); + await doc.dispose(); + expect(disposeCalled, true, reason: 'onDispose should be called after dispose'); + }); + + test('handles large file sizes correctly', () async { + final data = await testPdfFile.readAsBytes(); + final largeFileSize = data.length; + + int readFunc(Uint8List buffer, int position, int size) { + if (position >= data.length) return 0; + final actualSize = (position + size > data.length) ? data.length - position : size; + buffer.setRange(0, actualSize, data, position); + return actualSize; + } + + final doc = await PdfDocument.openCustom( + read: readFunc, + fileSize: largeFileSize, + sourceName: 'custom:large.pdf', + maxSizeToCacheOnMemory: 0, + ); + + await testDocument(doc); + }); + + test('handles partial reads correctly', () async { + final data = await testPdfFile.readAsBytes(); + final readSizes = []; + + int readFunc(Uint8List buffer, int position, int size) { + readSizes.add(size); + + if (position >= data.length) return 0; + final actualSize = (position + size > data.length) ? data.length - position : size; + buffer.setRange(0, actualSize, data, position); + return actualSize; + } + + final doc = await PdfDocument.openCustom( + read: readFunc, + fileSize: data.length, + sourceName: 'custom:partial.pdf', + maxSizeToCacheOnMemory: 0, + ); + + await testDocument(doc); + // Verify that reads occurred with various sizes + expect(readSizes.isNotEmpty, true, reason: 'Should have read sizes recorded'); + }); + }); +} diff --git a/test/utils.dart b/packages/pdfrx_engine/test/utils.dart similarity index 72% rename from test/utils.dart rename to packages/pdfrx_engine/test/utils.dart index 0a690204..3af1e21d 100644 --- a/test/utils.dart +++ b/packages/pdfrx_engine/test/utils.dart @@ -1,10 +1,7 @@ import 'dart:io'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:pdfrx/pdfrx.dart'; - -/// The release of pdfium to download. -const pdfiumRelease = 'chromium%2F6555'; +import 'package:pdfrx_engine/pdfrx_engine.dart'; +import 'package:test/test.dart'; /// Temporary directory for testing. final tmpRoot = Directory('${Directory.current.path}/test/.tmp'); @@ -28,9 +25,7 @@ Future testPage(PdfDocument doc, int pageNumber) async { expect(pageImage, isNotNull); expect(pageImage!.width, page.width.toInt(), reason: 'pageImage.width'); expect(pageImage.height, page.height.toInt(), reason: 'pageImage.height'); - final image = await pageImage.createImage(); - expect(image.width, page.width.toInt(), reason: 'image.width'); - expect(image.height, page.height.toInt(), reason: 'image.height'); - image.dispose(); + expect(pageImage.width, page.width.toInt(), reason: 'image.width'); + expect(pageImage.height, page.height.toInt(), reason: 'image.height'); pageImage.dispose(); } diff --git a/pubspec.yaml b/pubspec.yaml index 791fcd56..86fdc087 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,65 +1,11 @@ -name: pdfrx -description: pdfrx is a rich and fast PDF viewer implementation built on the top of PDFium. The plugin supports Android, iOS, Windows, macOS, Linux, and Web. -version: 1.1.11 -homepage: https://github.com/espresso3389/pdfrx - +name: pdfrx_workspace environment: - sdk: '>=3.7.0 <4.0.0' - flutter: ">=3.29.0" - -dependencies: - collection: '>=1.18.0 <1.20.0' - crypto: ^3.0.6 - ffi: ^2.1.3 - flutter: - sdk: flutter - http: ^1.2.2 - path: ^1.9.0 - path_provider: ^2.1.5 - rxdart: '>=0.27.0 <0.29.0' - synchronized: ^3.3.0 - url_launcher: ^6.3.1 - vector_math: ^2.1.4 - web: ^1.1.0 - -dev_dependencies: - ffigen: ^16.1.0 - flutter_test: - sdk: flutter - flutter_lints: ^5.0.0 - archive: ^4.0.2 - -flutter: - plugin: - platforms: - android: - ffiPlugin: true - ios: - ffiPlugin: true - sharedDarwinSource: true - linux: - ffiPlugin: true - macos: - ffiPlugin: true - sharedDarwinSource: true - windows: - ffiPlugin: true - web: - -# dart run ffigen -ffigen: - output: - bindings: "lib/src/pdfium/pdfium_bindings.dart" - headers: - entry-points: - - "example/viewer/build/windows/x64/.lib/latest/include/fpdfview.h" - - "example/viewer/build/windows/x64/.lib/latest/include/fpdf_annot.h" - - "example/viewer/build/windows/x64/.lib/latest/include/fpdf_text.h" - - "example/viewer/build/windows/x64/.lib/latest/include/fpdf_doc.h" - - "example/viewer/build/windows/x64/.lib/latest/include/fpdf_edit.h" - - "example/viewer/build/windows/x64/.lib/latest/include/fpdf_formfill.h" - include-directives: - - "example/viewer/build/windows/x64/.lib/latest/include/**" - preamble: | - // ignore_for_file: unused_field - name: "pdfium" + sdk: ">=3.9.0 <4.0.0" +workspace: + - packages/pdfium_dart + - packages/pdfium_flutter + - packages/pdfrx + - packages/pdfrx_engine + - packages/pdfrx_coregraphics + - packages/pdfrx/example/viewer + - packages/pdfrx/example/pdf_combine diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt deleted file mode 100644 index 5385169e..00000000 --- a/src/CMakeLists.txt +++ /dev/null @@ -1,16 +0,0 @@ -# The Flutter tooling requires that developers have CMake 3.10 or later -# installed. You should not increase this version, as doing so will cause -# the plugin to fail to compile for some customers of the plugin. -cmake_minimum_required(VERSION 3.10) - -project(pdfrx_library VERSION 0.0.1 LANGUAGES CXX) - -add_library(pdfrx SHARED - "pdfium_interop.cpp" -) - -set_target_properties(pdfrx PROPERTIES - OUTPUT_NAME "pdfrx" -) - -target_compile_definitions(pdfrx PUBLIC DART_SHARED_LIB) diff --git a/src/pdfium_interop.cpp b/src/pdfium_interop.cpp deleted file mode 100644 index 58e71491..00000000 --- a/src/pdfium_interop.cpp +++ /dev/null @@ -1,367 +0,0 @@ -#include -#include -#include -#include -#include - -#if defined(_WIN32) -#define PDFRX_EXPORT __declspec(dllexport) -#define PDFRX_INTEROP_API __stdcall -#else -#define PDFRX_EXPORT __attribute__((visibility("default"))) __attribute__((used)) -#define PDFRX_INTEROP_API -#endif - -struct pdfrx_file_access; - -typedef void(PDFRX_INTEROP_API *pdfrx_read_function)(void *param, - size_t position, - unsigned char *pBuf, - size_t size); - -struct pdfrx_file_access -{ - FPDF_FILEACCESS fileAccess; - int retValue; - pdfrx_read_function readBlock; - void *param; - std::mutex mutex; - std::condition_variable cond; -}; - -static int PDFRX_INTEROP_API read(void *param, - unsigned long position, - unsigned char *pBuf, - unsigned long size) -{ - auto fileAccess = reinterpret_cast(param); - std::unique_lock lock(fileAccess->mutex); - fileAccess->readBlock(fileAccess->param, position, pBuf, size); - fileAccess->cond.wait(lock); - return fileAccess->retValue; -} - -extern "C" PDFRX_EXPORT pdfrx_file_access *PDFRX_INTEROP_API pdfrx_file_access_create(unsigned long fileSize, pdfrx_read_function readBlock, void *param) -{ - auto fileAccess = new pdfrx_file_access(); - fileAccess->fileAccess.m_FileLen = fileSize; - fileAccess->fileAccess.m_GetBlock = read; - fileAccess->fileAccess.m_Param = fileAccess; - fileAccess->retValue = 0; - fileAccess->readBlock = readBlock; - fileAccess->param = param; - return fileAccess; -} - -extern "C" PDFRX_EXPORT void PDFRX_INTEROP_API pdfrx_file_access_destroy(pdfrx_file_access *fileAccess) -{ - delete fileAccess; -} - -extern "C" PDFRX_EXPORT void PDFRX_INTEROP_API pdfrx_file_access_set_value(pdfrx_file_access *fileAccess, int retValue) -{ - std::unique_lock lock(fileAccess->mutex); - fileAccess->retValue = retValue; - fileAccess->cond.notify_one(); -} - -#if defined(__APPLE__) -#include -#include -#include -#include -#include - -extern "C" PDFRX_EXPORT void const *const *PDFRX_INTEROP_API pdfrx_binding() -{ - static const void *bindings[] = { - reinterpret_cast(FPDF_InitLibraryWithConfig), - reinterpret_cast(FPDF_InitLibrary), - reinterpret_cast(FPDF_DestroyLibrary), - reinterpret_cast(FPDF_SetSandBoxPolicy), - // reinterpret_cast(FPDF_SetPrintMode), - reinterpret_cast(FPDF_LoadDocument), - reinterpret_cast(FPDF_LoadMemDocument), - reinterpret_cast(FPDF_LoadMemDocument64), - reinterpret_cast(FPDF_LoadCustomDocument), - reinterpret_cast(FPDF_GetFileVersion), - reinterpret_cast(FPDF_GetLastError), - reinterpret_cast(FPDF_DocumentHasValidCrossReferenceTable), - reinterpret_cast(FPDF_GetTrailerEnds), - reinterpret_cast(FPDF_GetDocPermissions), - // reinterpret_cast(FPDF_GetDocUserPermissions), - reinterpret_cast(FPDF_GetSecurityHandlerRevision), - reinterpret_cast(FPDF_GetPageCount), - reinterpret_cast(FPDF_LoadPage), - reinterpret_cast(FPDF_GetPageWidthF), - reinterpret_cast(FPDF_GetPageWidth), - reinterpret_cast(FPDF_GetPageHeightF), - reinterpret_cast(FPDF_GetPageHeight), - reinterpret_cast(FPDF_GetPageBoundingBox), - reinterpret_cast(FPDF_GetPageSizeByIndexF), - reinterpret_cast(FPDF_GetPageSizeByIndex), - // reinterpret_cast(FPDF_RenderPage), - reinterpret_cast(FPDF_RenderPageBitmap), - reinterpret_cast(FPDF_RenderPageBitmapWithMatrix), - reinterpret_cast(FPDF_ClosePage), - reinterpret_cast(FPDF_CloseDocument), - reinterpret_cast(FPDF_DeviceToPage), - reinterpret_cast(FPDF_PageToDevice), - reinterpret_cast(FPDFBitmap_Create), - reinterpret_cast(FPDFBitmap_CreateEx), - reinterpret_cast(FPDFBitmap_GetFormat), - reinterpret_cast(FPDFBitmap_FillRect), - reinterpret_cast(FPDFBitmap_GetBuffer), - reinterpret_cast(FPDFBitmap_GetWidth), - reinterpret_cast(FPDFBitmap_GetHeight), - reinterpret_cast(FPDFBitmap_GetStride), - reinterpret_cast(FPDFBitmap_Destroy), - reinterpret_cast(FPDF_VIEWERREF_GetPrintScaling), - reinterpret_cast(FPDF_VIEWERREF_GetNumCopies), - reinterpret_cast(FPDF_VIEWERREF_GetPrintPageRange), - reinterpret_cast(FPDF_VIEWERREF_GetPrintPageRangeCount), - reinterpret_cast(FPDF_VIEWERREF_GetPrintPageRangeElement), - reinterpret_cast(FPDF_VIEWERREF_GetDuplex), - reinterpret_cast(FPDF_VIEWERREF_GetName), - reinterpret_cast(FPDF_CountNamedDests), - reinterpret_cast(FPDF_GetNamedDestByName), - reinterpret_cast(FPDF_GetNamedDest), - reinterpret_cast(FPDF_GetXFAPacketCount), - reinterpret_cast(FPDF_GetXFAPacketName), - reinterpret_cast(FPDF_GetXFAPacketContent), - reinterpret_cast(FPDFAnnot_IsSupportedSubtype), - reinterpret_cast(FPDFPage_CreateAnnot), - reinterpret_cast(FPDFPage_GetAnnotCount), - reinterpret_cast(FPDFPage_GetAnnot), - reinterpret_cast(FPDFPage_GetAnnotIndex), - reinterpret_cast(FPDFPage_CloseAnnot), - reinterpret_cast(FPDFPage_RemoveAnnot), - reinterpret_cast(FPDFAnnot_GetSubtype), - reinterpret_cast(FPDFAnnot_IsObjectSupportedSubtype), - reinterpret_cast(FPDFAnnot_UpdateObject), - reinterpret_cast(FPDFAnnot_AddInkStroke), - reinterpret_cast(FPDFAnnot_RemoveInkList), - reinterpret_cast(FPDFAnnot_AppendObject), - reinterpret_cast(FPDFAnnot_GetObjectCount), - reinterpret_cast(FPDFAnnot_GetObject), - reinterpret_cast(FPDFAnnot_RemoveObject), - reinterpret_cast(FPDFAnnot_SetColor), - reinterpret_cast(FPDFAnnot_GetColor), - reinterpret_cast(FPDFAnnot_HasAttachmentPoints), - reinterpret_cast(FPDFAnnot_SetAttachmentPoints), - reinterpret_cast(FPDFAnnot_AppendAttachmentPoints), - reinterpret_cast(FPDFAnnot_CountAttachmentPoints), - reinterpret_cast(FPDFAnnot_GetAttachmentPoints), - reinterpret_cast(FPDFAnnot_SetRect), - reinterpret_cast(FPDFAnnot_GetRect), - reinterpret_cast(FPDFAnnot_GetVertices), - reinterpret_cast(FPDFAnnot_GetInkListCount), - reinterpret_cast(FPDFAnnot_GetInkListPath), - reinterpret_cast(FPDFAnnot_GetLine), - reinterpret_cast(FPDFAnnot_SetBorder), - reinterpret_cast(FPDFAnnot_GetBorder), - reinterpret_cast(FPDFAnnot_GetFormAdditionalActionJavaScript), - reinterpret_cast(FPDFAnnot_HasKey), - reinterpret_cast(FPDFAnnot_GetValueType), - reinterpret_cast(FPDFAnnot_SetStringValue), - reinterpret_cast(FPDFAnnot_GetStringValue), - reinterpret_cast(FPDFAnnot_GetNumberValue), - reinterpret_cast(FPDFAnnot_SetAP), - reinterpret_cast(FPDFAnnot_GetAP), - reinterpret_cast(FPDFAnnot_GetLinkedAnnot), - reinterpret_cast(FPDFAnnot_GetFlags), - reinterpret_cast(FPDFAnnot_SetFlags), - reinterpret_cast(FPDFAnnot_GetFormFieldFlags), - reinterpret_cast(FPDFAnnot_GetFormFieldAtPoint), - reinterpret_cast(FPDFAnnot_GetFormFieldName), - reinterpret_cast(FPDFAnnot_GetFormFieldAlternateName), - reinterpret_cast(FPDFAnnot_GetFormFieldType), - reinterpret_cast(FPDFAnnot_GetFormFieldValue), - reinterpret_cast(FPDFAnnot_GetOptionCount), - reinterpret_cast(FPDFAnnot_GetOptionLabel), - reinterpret_cast(FPDFAnnot_IsOptionSelected), - reinterpret_cast(FPDFAnnot_GetFontSize), - reinterpret_cast(FPDFAnnot_IsChecked), - reinterpret_cast(FPDFAnnot_SetFocusableSubtypes), - reinterpret_cast(FPDFAnnot_GetFocusableSubtypesCount), - reinterpret_cast(FPDFAnnot_GetFocusableSubtypes), - reinterpret_cast(FPDFAnnot_GetLink), - reinterpret_cast(FPDFAnnot_GetFormControlCount), - reinterpret_cast(FPDFAnnot_GetFormControlIndex), - reinterpret_cast(FPDFAnnot_GetFormFieldExportValue), - reinterpret_cast(FPDFAnnot_SetURI), - reinterpret_cast(FPDFText_LoadPage), - reinterpret_cast(FPDFText_ClosePage), - reinterpret_cast(FPDFText_CountChars), - reinterpret_cast(FPDFText_GetUnicode), - reinterpret_cast(FPDFText_IsGenerated), - reinterpret_cast(FPDFText_IsHyphen), - reinterpret_cast(FPDFText_HasUnicodeMapError), - reinterpret_cast(FPDFText_GetFontSize), - reinterpret_cast(FPDFText_GetFontInfo), - reinterpret_cast(FPDFText_GetFontWeight), - reinterpret_cast(FPDFText_GetTextRenderMode), - reinterpret_cast(FPDFText_GetFillColor), - reinterpret_cast(FPDFText_GetStrokeColor), - reinterpret_cast(FPDFText_GetCharAngle), - reinterpret_cast(FPDFText_GetCharBox), - reinterpret_cast(FPDFText_GetLooseCharBox), - reinterpret_cast(FPDFText_GetMatrix), - reinterpret_cast(FPDFText_GetCharOrigin), - reinterpret_cast(FPDFText_GetCharIndexAtPos), - reinterpret_cast(FPDFText_GetText), - reinterpret_cast(FPDFText_CountRects), - reinterpret_cast(FPDFText_GetRect), - reinterpret_cast(FPDFText_GetBoundedText), - reinterpret_cast(FPDFText_FindStart), - reinterpret_cast(FPDFText_FindNext), - reinterpret_cast(FPDFText_FindPrev), - reinterpret_cast(FPDFText_GetSchResultIndex), - reinterpret_cast(FPDFText_GetSchCount), - reinterpret_cast(FPDFText_FindClose), - reinterpret_cast(FPDFLink_LoadWebLinks), - reinterpret_cast(FPDFLink_CountWebLinks), - reinterpret_cast(FPDFLink_LoadWebLinks), - reinterpret_cast(FPDFLink_CountWebLinks), - reinterpret_cast(FPDFLink_GetURL), - reinterpret_cast(FPDFLink_CountRects), - reinterpret_cast(FPDFLink_GetRect), - reinterpret_cast(FPDFLink_GetTextRange), - reinterpret_cast(FPDFLink_CloseWebLinks), - reinterpret_cast(FPDFLink_GetLinkAtPoint), - reinterpret_cast(FPDFLink_GetLinkZOrderAtPoint), - reinterpret_cast(FPDFLink_GetDest), - reinterpret_cast(FPDFLink_GetAction), - reinterpret_cast(FPDFLink_Enumerate), - reinterpret_cast(FPDFLink_GetAnnot), - reinterpret_cast(FPDFLink_GetAnnotRect), - reinterpret_cast(FPDFLink_CountQuadPoints), - reinterpret_cast(FPDFLink_GetQuadPoints), - reinterpret_cast(FPDFLink_CloseWebLinks), - reinterpret_cast(FPDFAction_GetType), - reinterpret_cast(FPDFAction_GetDest), - reinterpret_cast(FPDFAction_GetFilePath), - reinterpret_cast(FPDFAction_GetURIPath), - reinterpret_cast(FPDFDest_GetDestPageIndex), - reinterpret_cast(FPDFDest_GetView), - reinterpret_cast(FPDFDest_GetLocationInPage), - reinterpret_cast(FPDFBookmark_GetFirstChild), - reinterpret_cast(FPDFBookmark_GetNextSibling), - reinterpret_cast(FPDFBookmark_GetTitle), - reinterpret_cast(FPDFBookmark_GetCount), - reinterpret_cast(FPDFBookmark_Find), - reinterpret_cast(FPDFBookmark_GetDest), - reinterpret_cast(FPDFBookmark_GetAction), - reinterpret_cast(FPDFDOC_InitFormFillEnvironment), - reinterpret_cast(FPDFDOC_ExitFormFillEnvironment), - reinterpret_cast(FPDF_FFLDraw), - reinterpret_cast(FPDF_GetFormType), - reinterpret_cast(FPDF_CreateNewDocument), - reinterpret_cast(FPDFPage_New), - reinterpret_cast(FPDFPage_Delete), - reinterpret_cast(FPDF_MovePages), - reinterpret_cast(FPDFPage_GetRotation), - reinterpret_cast(FPDFPage_SetRotation), - reinterpret_cast(FPDFPage_InsertObject), - reinterpret_cast(FPDFPage_RemoveObject), - reinterpret_cast(FPDFPage_CountObjects), - reinterpret_cast(FPDFPage_GetObject), - reinterpret_cast(FPDFPage_HasTransparency), - reinterpret_cast(FPDFPage_GenerateContent), - reinterpret_cast(FPDFPageObj_Destroy), - reinterpret_cast(FPDFPageObj_HasTransparency), - reinterpret_cast(FPDFPageObj_GetType), - reinterpret_cast(FPDFPageObj_Transform), - reinterpret_cast(FPDFPageObj_GetMatrix), - reinterpret_cast(FPDFPageObj_SetMatrix), - reinterpret_cast(FPDFPage_TransformAnnots), - reinterpret_cast(FPDFPageObj_NewImageObj), - reinterpret_cast(FPDFPageObj_CountMarks), - reinterpret_cast(FPDFPageObj_GetMark), - reinterpret_cast(FPDFPageObj_AddMark), - reinterpret_cast(FPDFPageObj_RemoveMark), - reinterpret_cast(FPDFPageObjMark_GetName), - reinterpret_cast(FPDFPageObjMark_CountParams), - reinterpret_cast(FPDFPageObjMark_GetParamKey), - reinterpret_cast(FPDFPageObjMark_GetParamValueType), - reinterpret_cast(FPDFPageObjMark_GetParamIntValue), - reinterpret_cast(FPDFPageObjMark_GetParamStringValue), - reinterpret_cast(FPDFPageObjMark_GetParamBlobValue), - reinterpret_cast(FPDFPageObjMark_SetIntParam), - reinterpret_cast(FPDFPageObjMark_SetStringParam), - reinterpret_cast(FPDFPageObjMark_SetBlobParam), - reinterpret_cast(FPDFPageObjMark_RemoveParam), - reinterpret_cast(FPDFImageObj_LoadJpegFile), - reinterpret_cast(FPDFImageObj_LoadJpegFileInline), - reinterpret_cast(FPDFImageObj_SetMatrix), - reinterpret_cast(FPDFImageObj_SetBitmap), - reinterpret_cast(FPDFImageObj_GetBitmap), - reinterpret_cast(FPDFImageObj_GetRenderedBitmap), - reinterpret_cast(FPDFImageObj_GetImageDataDecoded), - reinterpret_cast(FPDFImageObj_GetImageDataRaw), - reinterpret_cast(FPDFImageObj_GetImageFilterCount), - reinterpret_cast(FPDFImageObj_GetImageFilter), - reinterpret_cast(FPDFImageObj_GetImageMetadata), - reinterpret_cast(FPDFImageObj_GetImagePixelSize), - reinterpret_cast(FPDFPageObj_CreateNewPath), - reinterpret_cast(FPDFPageObj_CreateNewRect), - reinterpret_cast(FPDFPageObj_GetBounds), - reinterpret_cast(FPDFPageObj_GetRotatedBounds), - reinterpret_cast(FPDFPageObj_SetBlendMode), - reinterpret_cast(FPDFPageObj_SetStrokeColor), - reinterpret_cast(FPDFPageObj_GetStrokeColor), - reinterpret_cast(FPDFPageObj_SetStrokeWidth), - reinterpret_cast(FPDFPageObj_GetStrokeWidth), - reinterpret_cast(FPDFPageObj_GetLineJoin), - reinterpret_cast(FPDFPageObj_SetLineJoin), - reinterpret_cast(FPDFPageObj_GetLineCap), - reinterpret_cast(FPDFPageObj_SetLineCap), - reinterpret_cast(FPDFPageObj_SetFillColor), - reinterpret_cast(FPDFPageObj_GetFillColor), - reinterpret_cast(FPDFPageObj_GetDashPhase), - reinterpret_cast(FPDFPageObj_SetDashPhase), - reinterpret_cast(FPDFPageObj_GetDashCount), - reinterpret_cast(FPDFPageObj_GetDashArray), - reinterpret_cast(FPDFPageObj_SetDashArray), - reinterpret_cast(FPDFPath_CountSegments), - reinterpret_cast(FPDFPath_GetPathSegment), - reinterpret_cast(FPDFPathSegment_GetPoint), - reinterpret_cast(FPDFPathSegment_GetType), - reinterpret_cast(FPDFPathSegment_GetClose), - reinterpret_cast(FPDFPath_MoveTo), - reinterpret_cast(FPDFPath_LineTo), - reinterpret_cast(FPDFPath_BezierTo), - reinterpret_cast(FPDFPath_Close), - reinterpret_cast(FPDFPath_SetDrawMode), - reinterpret_cast(FPDFPath_GetDrawMode), - reinterpret_cast(FPDFPageObj_NewTextObj), - reinterpret_cast(FPDFText_SetText), - reinterpret_cast(FPDFText_SetCharcodes), - reinterpret_cast(FPDFText_LoadFont), - reinterpret_cast(FPDFText_LoadStandardFont), - reinterpret_cast(FPDFTextObj_GetFontSize), - reinterpret_cast(FPDFFont_Close), - reinterpret_cast(FPDFPageObj_CreateTextObj), - reinterpret_cast(FPDFTextObj_GetTextRenderMode), - reinterpret_cast(FPDFTextObj_SetTextRenderMode), - reinterpret_cast(FPDFTextObj_GetText), - reinterpret_cast(FPDFTextObj_GetRenderedBitmap), - reinterpret_cast(FPDFTextObj_GetFont), - reinterpret_cast(FPDFFont_GetFontName), - reinterpret_cast(FPDFFont_GetFontData), - reinterpret_cast(FPDFFont_GetIsEmbedded), - reinterpret_cast(FPDFFont_GetFlags), - reinterpret_cast(FPDFFont_GetWeight), - reinterpret_cast(FPDFFont_GetItalicAngle), - reinterpret_cast(FPDFFont_GetAscent), - reinterpret_cast(FPDFFont_GetDescent), - reinterpret_cast(FPDFFont_GetGlyphWidth), - reinterpret_cast(FPDFFont_GetGlyphPath), - reinterpret_cast(FPDFGlyphPath_CountGlyphSegments), - reinterpret_cast(FPDFGlyphPath_GetGlyphPathSegment), - reinterpret_cast(FPDFFormObj_CountObjects), - reinterpret_cast(FPDFFormObj_GetObject), - }; - return bindings; -} -#endif diff --git a/test/pdf_document_test.dart b/test/pdf_document_test.dart deleted file mode 100644 index d27ede35..00000000 --- a/test/pdf_document_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'dart:io'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:http/http.dart' as http; -import 'package:http/testing.dart'; -import 'package:pdfrx/pdfrx.dart'; - -import 'setup.dart'; -import 'utils.dart'; - -final testPdfFile = File('example/viewer/assets/hello.pdf'); - -void main() { - setUp(() => setup()); - - test( - 'PdfDocument.openFile', - () async => - await testDocument(await PdfDocument.openFile(testPdfFile.path))); - test('PdfDocument.openData', () async { - final data = await testPdfFile.readAsBytes(); - await testDocument(await PdfDocument.openData(data)); - }); - test('PdfDocument.openUri', () async { - Pdfrx.createHttpClient = () => MockClient((request) async => - http.Response.bytes(await testPdfFile.readAsBytes(), 200)); - await testDocument( - await PdfDocument.openUri(Uri.parse('https://example.com/hello.pdf'))); - }); -} diff --git a/test/pdf_viewer_test.dart b/test/pdf_viewer_test.dart deleted file mode 100644 index 5ec84678..00000000 --- a/test/pdf_viewer_test.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:http/http.dart' as http; -import 'package:http/testing.dart'; -import 'package:pdfrx/pdfrx.dart'; - -import 'setup.dart'; - -final testPdfFile = File('example/viewer/assets/hello.pdf'); -final binding = TestWidgetsFlutterBinding.ensureInitialized(); - -void main() { - setUp(() => setup()); - Pdfrx.createHttpClient = () => MockClient( - (request) async { - return http.Response.bytes(await testPdfFile.readAsBytes(), 200); - }, - ); - - testWidgets( - 'PdfViewer.uri', - (tester) async { - await binding.setSurfaceSize(Size(1080, 1920)); - await tester.pumpWidget( - MaterialApp( - // FIXME: Just a workaround for "A RenderFlex overflowed..." - home: SingleChildScrollView( - child: PdfViewer.uri( - Uri.parse('https://example.com/hello.pdf'), - ), - ), - ), - ); - - await tester.pumpAndSettle(); - - expect(find.byType(PdfViewer), findsOneWidget); - }, - ); -} diff --git a/test/setup.dart b/test/setup.dart deleted file mode 100644 index cbdd6e25..00000000 --- a/test/setup.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'dart:io'; - -import 'package:archive/archive_io.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:http/http.dart' as http; -import 'package:pdfrx/pdfrx.dart'; - -import 'utils.dart'; - -final cacheRoot = Directory('${tmpRoot.path}/cache'); - -/// Sets up the test environment. -Future setup() async { - Pdfrx.pdfiumModulePath = await downloadAndGetPdfiumModulePath(); - - TestWidgetsFlutterBinding.ensureInitialized(); - - const channel = MethodChannel('plugins.flutter.io/path_provider'); - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler(channel, (methodCall) async { - return cacheRoot.path; - }); - try { - await cacheRoot.delete(recursive: true); - } catch (e) {/**/} -} - -/// Downloads the pdfium module for the current platform and architecture. -/// -/// Currently, the following platforms are supported: -/// - Windows x64 -/// - Linux x64, arm64 -/// - macOS x64, arm64 -Future downloadAndGetPdfiumModulePath() async { - final pa = RegExp(r'"([^_]+)_([^_]+)"').firstMatch(Platform.version)!; - final platform = pa[1]!; - final arch = pa[2]!; - if (platform == 'windows' && arch == 'x64') { - return await _downloadPdfium('win', arch, 'bin/pdfium.dll'); - } - if (platform == 'linux' && (arch == 'x64' || arch == 'arm64')) { - return await _downloadPdfium(platform, arch, 'lib/libpdfium.so'); - } - if (platform == 'macos') { - return await _downloadPdfium('mac', arch, 'lib/libpdfium.dylib'); - } else { - throw Exception('Unsupported platform: $platform-$arch'); - } -} - -/// Downloads the pdfium module for the given platform and architecture. -Future _downloadPdfium( - String platform, String arch, String modulePath) async { - final tmpDir = Directory('${tmpRoot.path}/$platform-$arch'); - final targetPath = '${tmpDir.path}/$modulePath'; - if (await File(targetPath).exists()) return targetPath; - - final uri = - 'https://github.com/bblanchon/pdfium-binaries/releases/download/$pdfiumRelease/pdfium-$platform-$arch.tgz'; - final tgz = await http.Client().get(Uri.parse(uri)); - if (tgz.statusCode != 200) { - throw Exception('Failed to download pdfium: $uri'); - } - final archive = - TarDecoder().decodeBytes(GZipDecoder().decodeBytes(tgz.bodyBytes)); - try { - await tmpDir.delete(recursive: true); - } catch (_) {} - await extractArchiveToDisk(archive, tmpDir.path); - return targetPath; -} diff --git a/wasm/pdfrx_wasm/CHANGELOG.md b/wasm/pdfrx_wasm/CHANGELOG.md deleted file mode 100644 index 29f648fd..00000000 --- a/wasm/pdfrx_wasm/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -## 0.0.1 - -* Initial release diff --git a/wasm/pdfrx_wasm/README.md b/wasm/pdfrx_wasm/README.md deleted file mode 100644 index 1ed071a2..00000000 --- a/wasm/pdfrx_wasm/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# pdfrx_wasm - -This is a satellite plugin for [pdfrx](https://pub.dev/packages/pdfrx) that allows you to use WASM version of Pdfium. - -## Usage - -See [pdfrx](https://pub.dev/packages/pdfrx)'s documentation for usage. diff --git a/wasm/pdfrx_wasm/assets/pdfium_client.js b/wasm/pdfrx_wasm/assets/pdfium_client.js deleted file mode 100644 index 2636dcec..00000000 --- a/wasm/pdfrx_wasm/assets/pdfium_client.js +++ /dev/null @@ -1,36 +0,0 @@ -globalThis.pdfiumWasmSendCommand = function() { - const worker = new Worker(globalThis.pdfiumWasmWorkerUrl); - let requestId = 0; - const callbacks = new Map(); - - worker.onmessage = (event) => { - const data = event.data; - if (data.type === "ready") { - console.log("PDFium WASM worker is ready"); - return; - } - // For command responses, match using the request id. - if (data.id) { - const callback = callbacks.get(data.id); - if (callback) { - if (data.status === "success") { - callback.resolve(data.result); - } else { - callback.reject(new Error(data.error)); - } - callbacks.delete(data.id); - } - } - }; - - worker.onerror = (err) => { - console.error("Worker error:", err); - }; - - return function(command, parameters = {}, transfer = []) { - return new Promise((resolve, reject) => { - const id = ++requestId; - callbacks.set(id, { resolve, reject }); - worker.postMessage({ id, command, parameters }, transfer); - });}; -}(); diff --git a/wasm/pdfrx_wasm/assets/pdfium_worker.js b/wasm/pdfrx_wasm/assets/pdfium_worker.js deleted file mode 100644 index 77e0204c..00000000 --- a/wasm/pdfrx_wasm/assets/pdfium_worker.js +++ /dev/null @@ -1,1316 +0,0 @@ -// -// A small implementation of a Web Worker that uses pdfium.wasm to render PDF files. -// - -/** - * PDFium WASM module imports - */ -const Pdfium = { - /** - * @param {WebAssembly.Exports} wasmExports - */ - initWith: function(wasmExports) { - Pdfium.wasmExports = wasmExports; - Pdfium.memory = Pdfium.wasmExports.memory; - Pdfium.wasmTable = Pdfium.wasmExports["__indirect_function_table"]; - Pdfium.stackSave = Pdfium.wasmExports["emscripten_stack_get_current"]; - Pdfium.stackRestore = Pdfium.wasmExports["_emscripten_stack_restore"]; - Pdfium.setThrew = Pdfium.wasmExports["setThrew"]; - }, - - /** - * @type {WebAssembly.Exports} - */ - wasmExports: null, - /** - * @type {WebAssembly.Memory} - */ - memory: null, - /** - * @type {WebAssembly.Table} - */ - wasmTable: null, - /** - * @type {function():number} - */ - stackSave: null, - /** - * @type {function(number):void} - */ - stackRestore: null, - /** - * @type {function(number, number):void} - */ - setThrew: null, - - /** - * Invoke a function from the WASM table - * @param {number} index Function index - * @param {function(function())} func Function to call - * @returns {*} Result of the function - */ - invokeFunc: function(index, func) { - const sp = Pdfium.stackSave(); - try { - return func(Pdfium.wasmTable.get(index)); - } catch (e) { - Pdfium.stackRestore(sp); - if (e !== e + 0) throw e; - Pdfium.setThrew(1, 0); - } - } -} - -/** - * @typedef {Object} FileContext Defines I/O functions for a file - * @property {number} size File size - * @property {function(FileDescriptorContext, Uint8Array):number} read read(context, data) - * @property {function(FileDescriptorContext):void|undefined} close close(context) - * @property {function(FileDescriptorContext, Uint8Array):number|undefined} write write(context, data) - * @property {function(FileDescriptorContext):number|undefined} sync sync(context) - */ - -/** - * @typedef {Object} FileDescriptorContext Defines I/O functions for a file descriptor - * @property {number} size File size - * @property {function(FileDescriptorContext, Uint8Array):number} read read(context, data) - * @property {function(FileDescriptorContext):void|undefined} close close(context) - * @property {function(FileDescriptorContext, Uint8Array):number|undefined} write write(context, data) - * @property {function(FileDescriptorContext):number|undefined} sync sync(context) - * @property {string} fileName - * @property {number} fd - * @property {number} flags - * @property {number} mode - * @property {number} dirfd - * @property {number} position Current position - */ - -/** - * @typedef {Object} DirectoryContext Defines I/O functions for a directory file descriptor - * @property {string[]} entries Directory entries (For directories, the name should be terminated with /) - */ - -/** - * @typedef {Object} DirectoryFileDescriptorContext Defines I/O functions for a directory file descriptor - * @property {string[]} entries Directory entries (For directories, the name should be terminated with /) - * @property {string} fileName - * @property {number} fd - * @property {number} dirfd - * @property {number} position Current entry index - */ - -/** - * Emulate file system for PDFium - */ -class FileSystemEmulator { - constructor() { - /** - * Filename to I/O functions/data - * @type {Object} - */ - this.fn2context = {}; - /** - * File descriptor to I/O functions/data - * @type {Object} - */ - this.fd2context = {}; - /** - * Last assigned file descriptor - * @type {number} - */ - this.fdAssignedLast = 1000; - } - - /** - * Register file - * @param {string} fn Filename - * @param {FileContext} context I/O functions/data - */ - registerFile(fn, context) { - this.fn2context[fn] = context; - } - - /** - * Register file with ArrayBuffer - * @param {string} fn Filename - * @param {ArrayBuffer} data File data - */ - registerFileWithData(fn, data) { - data = data.buffer != null ? data.buffer : data; - this.registerFile(fn, { - size: data.byteLength, - read: function(context, buffer) { - try { - const size = Math.min(buffer.byteLength, data.byteLength - context.position); - const array = new Uint8Array(data, context.position, size); - buffer.set(array); - context.position += array.byteLength; - return array.length; - } catch (err) { - console.error(`read error: ${_error(err)}`); - return 0; - } - }, - }); - } - - /** - * Register directory - * @param {string} fn Filename - * @param {*} entries Directory entries (For directories, the name should be terminated with /) - */ - registerDirectoryWithEntries(fn, entries) { - this.registerFile(fn, { entries }); - } - - /** - * Unregister file/directory context - * @param {string} fn Filename - */ - unregisterFile(fn) { - delete this.fn2context[fn]; - } - - /** - * Open a file - * @param {number} dirfd Directory file descriptor - * @param {number} fileNamePtr Pointer to buffer that contains filename - * @param {number} flags File open flags - * @param {number} mode File open mode - * @returns {number} File descriptor - */ - openFile(dirfd, fileNamePtr, flags, mode) { - const fn = StringUtils.utf8BytesToString(new Uint8Array(Pdfium.memory.buffer, fileNamePtr, 2048)); - const funcs = this.fn2context[fn]; - if (funcs) { - const fd = ++this.fdAssignedLast; - this.fd2context[fd] = { ...funcs, fd, flags, mode, dirfd, position: 0 }; - return fd; - } - console.error(`openFile: not found: ${dirfd}/${fn}`); - return -1; - } - - /** - * Close a file - * @param {number} fd File descriptor - */ - closeFile(fd) { - const context = this.fd2context[fd]; - context.close?.call(context); - delete this.fd2context[fd]; - } - - /** - * Seek to a position in a file - * @param {number} fd File descriptor - * @param {number} offset_low Offset low - * @param {number} offset_high Offset high - * @param {number} whence Whence - * @param {number} newOffset New offset - * @returns {number} New offset - */ - seek(fd, offset_low, offset_high, whence, newOffset) { - const context = this.fd2context[fd]; - if (offset_high !== 0) { - throw new Error('seek: offset_high is not supported'); - } - switch (whence) { - case 0: // SEEK_SET - context.position = offset_low; - break; - case 1: // SEEK_CUR - context.position += offset_low; - break; - case 2: // SEEK_END - context.position = context.size + offset_low; - break; - } - const offsetLowHigh = new Uint32Array(Pdfium.memory.buffer, newOffset, 2); - offsetLowHigh[0] = context.position; - offsetLowHigh[1] = 0; - } - - /** - * fd__write - * @param {num} fd - * @param {num} iovs - * @param {num} iovs_len - * @param {num} ret_ptr - */ - write(fd, iovs, iovs_len, ret_ptr) { - const context = this.fd2context[fd]; - let total = 0; - for (let i = 0; i < iovs_len; i++) { - const iov = new Int32Array(Pdfium.memory.buffer, iovs + i * 8, 2); - const ptr = iov[0]; - const len = iov[1]; - const written = context.write(context, new Uint8Array(Pdfium.memory.buffer, ptr, len)); - total += written; - if (written < len) break; - } - const bytes_written = new Uint32Array(Pdfium.memory.buffer, ret_ptr, 1); - bytes_written[0] = written; - } - - /** - * fd_read - * @param {num} fd - * @param {num} iovs - * @param {num} iovs_len - * @param {num} ret_ptr - */ - read(fd,iovs, iovs_len, ret_ptr) { - /** @type {FileDescriptorContext} */ - const context = this.fd2context[fd]; - let total = 0; - for (let i = 0; i < iovs_len; i++) { - const iov = new Int32Array(Pdfium.memory.buffer, iovs + i * 8, 2); - const ptr = iov[0]; - const len = iov[1]; - const read = context.read(context, new Uint8Array(Pdfium.memory.buffer, ptr, len)); - total += read; - if (read < len) break; - } - const bytes_read = new Uint32Array(Pdfium.memory.buffer, ret_ptr, 1); - bytes_read[0] = total; - } - - sync(fd) { - const context = this.fd2context[fd]; - return context.sync(context); - } - - /** - * __syscall_fstat64 - * @param {num} fd - * @param {num} statbuf - * @returns {num} - */ - fstat(fd, statbuf) { - const context = this.fd2context[fd]; - const buffer = new Int32Array(Pdfium.memory.buffer, statbuf, 92); - buffer[6] = context.size; // st_size - buffer[7] = 0; - return 0; - } - - /** - * __syscall_stat64 - * @param {num} pathnamePtr - * @param {num} statbuf - * @returns {num} - */ - stat64(pathnamePtr, statbuf) { - const fn = StringUtils.utf8BytesToString(new Uint8Array(Pdfium.memory.buffer, pathnamePtr, 2048)); - const funcs = this.fn2context[fn]; - if (funcs) { - const buffer = new Int32Array(Pdfium.memory.buffer, statbuf, 92); - buffer[6] = funcs.size; // st_size - buffer[7] = 0; - return 0; - } - return -1; - } - - /** - * __syscall_getdents64 - * @param {num} fd - * @param {num} dirp - * @param {num} count - * @returns {num} - */ - getdents64(fd, dirp, count) { - /** @type {DirectoryFileDescriptorContext} */ - const context = this.fd2context[fd]; - const entries = context.entries; - if (entries == null) return 0; - let written = 0; - const DT_REG = 8, DT_DIR = 4; - _memset(dirp, 0, count); - for (let i = context.position; i < entries.length; i++) { - let d_type, d_name; - if (entries[i].endsWith('/')) { - d_type = DT_DIR; - d_name = entries[i].substring(0, entries[i].length - 1); - } else { - d_type = DT_REG; - d_name = entries[i]; - } - const d_nameLength = StringUtils.lengthBytesUTF8(d_name) + 1; - const size = 8 + 8 + 2 + 1 + d_nameLength; - - if (written + size > count) break; - const buffer = new Uint8Array(Pdfium.memory.buffer, dirp + written, size); - // d_off - const d_off = written + size; - buffer[8] = d_off & 255; - buffer[9] = (d_off >> 8) & 255; - // d_reclen - buffer[16] = size & 255; - buffer[17] = (size >> 8) & 255; - // d_type - buffer[18] = d_type; - // d_name - StringUtils.stringToUtf8Bytes(d_name, new Uint8Array(Pdfium.memory.buffer, dirp + written + 19, d_nameLength)); - written = d_off; - } - return written; - } -}; - -function _error(e) { return e.stack ? e.stack.toString() : e.toString(); } - -function _notImplemented(name) { throw new Error(`${name} is not implemented`); } - -const fileSystem = new FileSystemEmulator(); - -const emEnv = { - __assert_fail: function(condition, filename, line, func) { throw new Error(`Assertion failed: ${condition} at ${filename}:${line} (${func})`); }, - _emscripten_memcpy_js: function(dest, src, num) { new Uint8Array(Pdfium.memory.buffer).copyWithin(dest, src, src + num); }, - __syscall_openat: fileSystem.openFile.bind(fileSystem), - __syscall_fstat64: fileSystem.fstat.bind(fileSystem), - __syscall_ftruncate64: function(fd, zero, zero2, zero3) { _notImplemented('__syscall_ftruncate64'); }, - __syscall_stat64: fileSystem.stat64.bind(fileSystem), - __syscall_newfstatat: function(dirfd, pathnamePtr, statbuf, flags) { _notImplemented('__syscall_newfstatat'); }, - __syscall_lstat64: function(pathnamePtr, statbuf) { _notImplemented('__syscall_lstat64'); }, - __syscall_fcntl64: function(fd, cmd, arg) { _notImplemented('__syscall_fcntl64'); }, - __syscall_ioctl: function(fd, request, arg) { _notImplemented('__syscall_ioctl'); }, - __syscall_getdents64: fileSystem.getdents64.bind(fileSystem), - __syscall_unlinkat: function(dirfd, pathnamePtr, flags) { _notImplemented('__syscall_unlinkat'); }, - __syscall_rmdir: function(pathnamePtr) { _notImplemented('__syscall_rmdir'); }, - _abort_js: function(what) { throw new Error(what); }, - _emscripten_throw_longjmp: function() { _notImplemented('longjmp'); }, - _gmtime_js: function(time, tmPtr) { - const date = new Date(time * 1000); - const tm = new Int32Array(Pdfium.memory.buffer, tmPtr, 9); - tm[0] = date.getUTCSeconds(); - tm[1] = date.getUTCMinutes(); - tm[2] = date.getUTCHours(); - tm[3] = date.getUTCDate(); - tm[4] = date.getUTCMonth(); - tm[5] = date.getUTCFullYear() - 1900; - tm[6] = date.getUTCDay(); - tm[7] = 0; // dst - tm[8] = 0; // gmtoff - }, - _localtime_js: function(time, tmPtr) { _notImplemented('_localtime_js'); }, - _tzset_js: function() { }, - emscripten_date_now: function() { return Date.now(); }, - emscripten_errn: function() { _notImplemented('emscripten_errn'); }, - emscripten_resize_heap: function(requestedSize) { - const oldSize = Pdfium.memory.buffer.byteLength; - const maxHeapSize = 2 * 1024 * 1024 * 1024; // 2GB - const pageSize = 65536; - if (requestedSize > maxHeapSize) { - console.error(`emscripten_resize_heap: Cannot enlarge memory, asked for ${requestedSize} bytes but limit is ${maxHeapSize}`); - return false; - } - let newSize = Math.max(oldSize * 1.5, requestedSize); - newSize = (newSize + pageSize - 1) & ~pageSize; - try { - Pdfium.memory.grow((newSize - oldSize) / pageSize); - console.log(`emscripten_resize_heap: ${oldSize} => ${newSize}`); - return true; - } catch (e) { - console.error(`emscripten_resize_heap: Failed to resize heap: ${_error(e)}`); - return false; - } - }, - exit: function(status) { _notImplemented('exit'); }, - invoke_ii: function(index, a) { return Pdfium.invokeFunc(index, function(func) { return func(a); });}, - invoke_iii: function(index, a, b) { return Pdfium.invokeFunc(index, function(func) { return func(a, b); });}, - invoke_iiii: function(index, a, b, c) { return Pdfium.invokeFunc(index, function(func) { return func(a, b, c); });}, - invoke_iiiii: function(index, a, b, c, d) { return Pdfium.invokeFunc(index, function(func) { return func(a, b, c, d); });}, - invoke_v: function(index) { return Pdfium.invokeFunc(index, function(func) { func(); });}, - invoke_viii: function(index, a, b, c) { Pdfium.invokeFunc(index, function(func) { func(a, b, c); });}, - invoke_viiii: function(index, a, b, c, d) { Pdfium.invokeFunc(index, function(func) { func(a, b, c, d); });}, - print: function(text) { console.log(text); }, - printErr: function(text) { console.error(text); }, -}; - -const wasi = { - proc_exit: function(code) { _notImplemented('proc_exit'); }, - environ_sizes_get: function(environCount, environBufSize) { _notImplemented('environ_sizes_get'); }, - environ_get: function(environ, environBuf) { _notImplemented('environ_get'); }, - fd_close: fileSystem.closeFile.bind(fileSystem), - fd_seek: fileSystem.seek.bind(fileSystem), - fd_write: fileSystem.write.bind(fileSystem), - fd_read: fileSystem.read.bind(fileSystem), - fd_sync: fileSystem.sync.bind(fileSystem), -}; - - -/** @type {string[]} */ -const fontNames = []; - -/** - * @param {{data: ArrayBuffer, name: string}} params - */ -function registerFont(params) { - const {name, data} = params; - const fileDir = '/usr/share/fonts'; - fontNames.push(fileDir + name); - fileSystem.registerDirectoryWithEntries(fileDir, name); - - fileSystem.registerFileWithData(name, data); -} - -/** - * @param {{url: string, name: string}} params - */ -async function registerFontFromUrl(params) { - const {name, url} = params; - const response = await fetch(url); - if (!response.ok) { - throw new Error("Failed to fetch font from URL: " + fontUrl); - } - const data = await response.arrayBuffer(); - registerFont({name, data}); -} - -/** - * @param {{url: string, password: string|undefined}} params - */ -async function loadDocumentFromUrl(params) { - const url = params.url; - const password = params.password || ""; - - const response = await fetch(url); - if (!response.ok) { - throw new Error("Failed to fetch PDF from URL: " + url); - } - - return loadDocumentFromData({data: await response.arrayBuffer(), url: url, password}); -} - -/** - * @param {{data: ArrayBuffer, password: string|undefined}} params - */ -function loadDocumentFromData(params) { - const data = params.data; - const password = params.password || ""; - - const sizeThreshold = 1024 * 1024; // 1MB - if (data.byteLength < sizeThreshold) { - const passwordPtr = StringUtils.allocateUTF8(password); - const buffer = Pdfium.wasmExports.malloc(data.byteLength); - if (buffer === 0) { - throw new Error("Failed to allocate memory for PDF data (${data.byteLength} bytes)"); - } - new Uint8Array(Pdfium.memory.buffer, buffer, data.byteLength).set(data); - const docHandle = Pdfium.wasmExports.FPDF_LoadMemDocument( - buffer, - data.byteLength, - passwordPtr, - ); - StringUtils.freeUTF8(passwordPtr); - return _loadDocument(docHandle, () => Pdfium.wasmExports.free(buffer)); - } - - const tempFileName = params.url ?? '/tmp/temp.pdf'; - fileSystem.registerFileWithData(tempFileName, data); - - const fileNamePtr = StringUtils.allocateUTF8(tempFileName); - const passwordPtr = StringUtils.allocateUTF8(password); - const docHandle = Pdfium.wasmExports.FPDF_LoadDocument(fileNamePtr, passwordPtr); - StringUtils.freeUTF8(fileNamePtr); - StringUtils.freeUTF8(passwordPtr); - return _loadDocument(docHandle, () => fileSystem.unregisterFile(tempFileName)); -} - -/** @type {Object} */ -const disposers = {}; - -/** - * @param {number} docHandle - * @param {function():void} onDispose - */ -function _loadDocument(docHandle, onDispose) { - try { - if (!docHandle) { - const error = Pdfium.wasmExports.FPDF_GetLastError(); - throw new Error(`Failed to load document from data (${_getErrorMessage(error)})`); - } - - const pageCount = Pdfium.wasmExports.FPDF_GetPageCount(docHandle); - const permissions = Pdfium.wasmExports.FPDF_GetDocPermissions(docHandle); - const securityHandlerRevision = Pdfium.wasmExports.FPDF_GetSecurityHandlerRevision(docHandle); - - const formInfoSize = 35 * 4; - let formInfo = Pdfium.wasmExports.malloc(formInfoSize); - const uint32 = new Uint32Array(Pdfium.memory.buffer, formInfo, formInfoSize >> 2); - uint32[0] = 1; // version - const formHandle = Pdfium.wasmExports.FPDFDOC_InitFormFillEnvironment(docHandle, formInfo); - if (formHandle === 0) { - Pdfium.wasmExports.free(formInfo); - formInfo = 0; - } - - const pages = []; - for (let i = 0; i < pageCount; i++) { - const pageHandle = Pdfium.wasmExports.FPDF_LoadPage(docHandle, i); - if (!pageHandle) { - const error = Pdfium.wasmExports.FPDF_GetLastError(); - throw new Error(`FPDF_LoadPage failed (${_getErrorMessage(error)})`); - } - - pages.push({ - pageIndex: i, - width: Pdfium.wasmExports.FPDF_GetPageWidth(pageHandle), - height: Pdfium.wasmExports.FPDF_GetPageHeight(pageHandle), - rotation: Pdfium.wasmExports.FPDFPage_GetRotation(pageHandle) - }); - Pdfium.wasmExports.FPDF_ClosePage(pageHandle); - } - - disposers[docHandle] = onDispose; - return { - docHandle: docHandle, - permissions: permissions, - securityHandlerRevision: securityHandlerRevision, - pages: pages, - formHandle: formHandle, - formInfo: formInfo, - }; - } catch (e) { - delete disposers[docHandle]; - onDispose(); - throw e; - } -} - -/** - * @param {{formHandle: number, formInfo: number, docHandle: number}} params - */ -function closeDocument(params) { - if (params.formHandle) { - try { - Pdfium.wasmExports.FPDFDOC_ExitFormFillEnvironment(params.formHandle); - } catch (e) {} - } - Pdfium.wasmExports.free(params.formInfo); - Pdfium.wasmExports.FPDF_CloseDocument(params.docHandle); - disposers[params.docHandle](); - delete disposers[params.docHandle]; - return { message: "Document closed" }; -} - -/** - * @param {{docHandle: number}} params - */ -function loadOutline(params) { - return _getOutlineNodeSiblings(Pdfium.wasmExports.FPDFBookmark_GetFirstChild(params.docHandle, null), params.docHandle); -} - -/** - * @param {number} bookmark - * @param {number} docHandle - */ -function _getOutlineNodeSiblings(bookmark, docHandle) { - const siblings = []; - while (bookmark) { - const titleBufSize = Pdfium.wasmExports.FPDFBookmark_GetTitle(bookmark, null, 0); - const titleBuf = Pdfium.wasmExports.malloc(titleBufSize); - Pdfium.wasmExports.FPDFBookmark_GetTitle(bookmark, titleBuf, titleBufSize); - const title = StringUtils.utf16BytesToString(new Uint8Array(Pdfium.memory.buffer, titleBuf, titleBufSize)); - Pdfium.wasmExports.free(titleBuf); - siblings.push({ - title: title, - dest: Pdfium.wasmExports.FPDFBookmark_GetDest(docHandle, bookmark), - children: _getOutlineNodeSiblings(Pdfium.wasmExports.FPDFBookmark_GetFirstChild(docHandle, bookmark), docHandle), - }); - bookmark = Pdfium.wasmExports.FPDFBookmark_GetNextSibling(docHandle, bookmark); - } - return siblings; -} - -/** - * @param {{docHandle: number, pageIndex: number}} params - * @return {number} Page handle - */ -function loadPage(params) { - const pageHandle = Pdfium.wasmExports.FPDF_LoadPage(params.docHandle, params.pageIndex); - if (!pageHandle) { - throw new Error(`Failed to load page ${params.pageIndex} from document ${params.docHandle}`); - } - return { pageHandle: pageHandle }; -} - -/** - * @param {{pageHandle: number}} params -*/ -function closePage(params) { - Pdfium.wasmExports.FPDF_ClosePage(params.pageHandle); - return { message: "Page closed" }; -} - -/** - * - * @param {{ - * docHandle: number, - * pageIndex: number, - * x: number, - * y: number, - * width: number, - * height: number, - * fullWidth: number, - * fullHeight: number, - * backgroundColor: number, - * annotationRenderingMode: number, - * formHandle: number - * }} params - * @returns - */ -function renderPage(params) { - const { - docHandle, - pageIndex, - x = 0, - y = 0, - width = 800, - height = 600, - fullWidth = width, - fullHeight = height, - backgroundColor, - annotationRenderingMode = 0, - formHandle, - } = params; - - let pageHandle = 0; - let bufferPtr = 0; - let bitmap = 0; - - try { - pageHandle = Pdfium.wasmExports.FPDF_LoadPage(docHandle, pageIndex); - if (!pageHandle) { - throw new Error(`Failed to load page ${pageIndex} from document ${docHandle}`); - } - - const bufferSize = width * height * 4; - bufferPtr = Pdfium.wasmExports.malloc(bufferSize); - if (!bufferPtr) { - throw new Error("Failed to allocate memory for rendering"); - } - const FPDFBitmap_BGRA = 4; - bitmap = Pdfium.wasmExports.FPDFBitmap_CreateEx( - width, - height, - FPDFBitmap_BGRA, - bufferPtr, - width * 4 - ); - - if (!bitmap) { - throw new Error("Failed to create bitmap for rendering"); - } - - Pdfium.wasmExports.FPDFBitmap_FillRect(bitmap, 0, 0, width, height, backgroundColor); - - const FPDF_ANNOT = 1; - const PdfAnnotationRenderingMode_none = 0; - const PdfAnnotationRenderingMode_annotationAndForms = 2; - Pdfium.wasmExports.FPDF_RenderPageBitmap( - bitmap, - pageHandle, - -x, - -y, - fullWidth, - fullHeight, - 0, - annotationRenderingMode !== PdfAnnotationRenderingMode_none ? FPDF_ANNOT : 0, - ); - - if (formHandle && annotationRenderingMode == PdfAnnotationRenderingMode_annotationAndForms) { - Pdfium.wasmExports.FPDF_FFLDraw(formHandle, bitmap, pageHandle, -x, -y, fullWidth, fullHeight, 0, 0); - } - - let copiedBuffer = new ArrayBuffer(bufferSize); - let b = new Uint8Array(copiedBuffer); - b.set(new Uint8Array(Pdfium.memory.buffer, bufferPtr, bufferSize)); - - return { - result: { - imageData: copiedBuffer, - width: width, - height: height - }, - transfer: [copiedBuffer], - }; - } finally { - Pdfium.wasmExports.FPDF_ClosePage(pageHandle); - Pdfium.wasmExports.FPDFBitmap_Destroy(bitmap); - Pdfium.wasmExports.free(bufferPtr); - } -} - -function _memset(ptr, value, num) { - const buffer = new Uint8Array(Pdfium.memory.buffer, ptr, num); - for (let i = 0; i < num; i++) { - buffer[i] = value; - } -} - -const CR = 0x0D, LF = 0x0A, SPC = 0x20; - -/** - * - * @param {{pageIndex: number, docHandle: number}} params - * @returns {{fullText: string, charRects: number[][], fragments: number[]}} - */ -function loadText(params) { - const { pageIndex, docHandle } = params; - const pageHandle = Pdfium.wasmExports.FPDF_LoadPage(docHandle, pageIndex); - const textPage = Pdfium.wasmExports.FPDFText_LoadPage(pageHandle); - if (textPage == null) return {fullText: '', charRects: [], fragments: []}; - const charCount = Pdfium.wasmExports.FPDFText_CountChars(textPage); - /** @type {number[][]} */ - const charRects = []; - /** @type {number[]} */ - const fragments = []; - const fullText = _loadTextInternal(textPage, 0, charCount, charRects, fragments); - Pdfium.wasmExports.FPDFText_ClosePage(textPage); - Pdfium.wasmExports.FPDF_ClosePage(pageHandle); - return {fullText, charRects, fragments}; -} - -/** - * @param {number} textPage - * @param {number} from - * @param {number} length - * @param {number[][]} charRects - * @param {number[]} fragments - * @returns - */ -function _loadTextInternal(textPage, from, length, charRects, fragments) { - const fullText = _getText(textPage, from, length); - const rectBuffer = Pdfium.wasmExports.malloc(8 * 4); // double[4] - const sb = { - str: '', - push(text) { this.str += text; }, - get length() { return this.str.length; }, - }; - let lineStart = 0, wordStart = 0; - let lastChar; - for (let i = 0; i < length; i++) { - const char = fullText.charCodeAt(from + i); - if (char == CR) { - if (i + 1 < length && fullText.codePointAt(from + i + 1) == LF) { - lastChar = char; - continue; - } - } - if (char === CR || char === LF) { - if (_makeLineFlat(charRects, lineStart, sb.length, sb)) { - sb.push('\r\n'); - _appendDummy(charRects); - _appendDummy(charRects); - fragments.push(sb.length - wordStart); - lineStart = wordStart = sb.length; - } - lastChar = char; - continue; - } - - Pdfium.wasmExports.FPDFText_GetCharBox(textPage, from + i, - rectBuffer, // L - rectBuffer + 8 * 2, // R - rectBuffer + 8 * 3, // B - rectBuffer + 8); // T - const rect = Array.from(new Float64Array(Pdfium.memory.buffer, rectBuffer, 4)); - if (char === SPC) { - if (lastChar == SPC) continue; - if (sb.length > wordStart) { - fragments.push(sb.length - wordStart); - } - sb.push(String.fromCharCode(char)); - charRects.push(rect); - fragments.push(1); - wordStart = sb.length; - lastChar = char; - continue; - } - - if (sb.length > lineStart) { - const columnHeightThreshold = 72.0; // 1 inch - const prev = charRects[charRects.length - 1]; - if (prev[0] > rect[0] || prev[3] + columnHeightThreshold < rect[3]) { - if (_makeLineFlat(charRects, lineStart, sb.length, sb)) { - if (sb.length > wordStart) { - fragments.push(sb.length - wordStart); - } - lineStart = wordStart = sb.length; - } - } - } - - sb.push(String.fromCharCode(char)); - charRects.push(rect); - lastChar = char; - } - - if (_makeLineFlat(charRects, lineStart, sb.length, sb)) { - if (sb.length > wordStart) { - fragments.push(sb.length - wordStart); - } - } - - Pdfium.wasmExports.free(rectBuffer); - return sb.str; -} - -function _appendDummy(rects, width = 1) { - if (rects.length === 0) return; - const last = rects[rects.length - 1]; - rects.push([last[0], last[1], last[2] + width, last[3]]); -} - -/// return true if any meaningful characters in the line (start -> end) -function _makeLineFlat(rects, start, end, sb) { - if (start >= end) return false; - const str = sb.str; - const bounds = _boundingRect(rects, start, end); - let prev; - for (let i = start; i < end; i++) { - const rect = rects[i]; - const char = str.codePointAt(i); - if (char === SPC) { - const next = i + 1 < end ? rects[i + 1][0] : null; - rects[i] = [prev != null ? prev : rect[0], bounds[1], next != null ? next : rect[2], bounds[3]]; - prev = null; - } else { - rects[i] = [prev != null ? prev : rect[0], bounds[1], rect[2], bounds[3]]; - prev = rect[2]; // right - } - } - return true; -} - -function _boundingRect(rects, start, end) { - let l = Number.MAX_VALUE, t = 0, r = 0, b = Number.MAX_VALUE; - for (let i = start; i < end; i++) { - const rect = rects[i]; - l = Math.min(l, rect[0]); - t = Math.max(t, rect[1]); - r = Math.max(r, rect[2]); - b = Math.min(b, rect[3]); - } - return [l, t, r, b]; -} - -function _getText(textPage, from, length) { - const textBuffer = Pdfium.wasmExports.malloc(length * 2 + 2); - const count = Pdfium.wasmExports.FPDFText_GetText(textPage, from, length, textBuffer); - const text = StringUtils.utf16BytesToString(new Uint8Array(Pdfium.memory.buffer, textBuffer, count * 2)); - Pdfium.wasmExports.free(textBuffer); - return text; -} - - -/** - * @typedef {{pageIndex: number, command: string, params: number[]}} PdfDest - * @typedef {{rects: number[][], dest: url: string}} PdfUrlLink - * @typedef {{rects: number[][], dest: PdfDest}} PdfDestLink - */ - -/** - * @param {{docHandle: number, pageIndex: number}} params - * @returns {{links: Array}} - */ -function loadLinks(params) { - const links = [..._loadAnnotLinks(params), ..._loadLinks(params)]; - return { - links: links, - }; -} - -/** - * @param {{docHandle: number, pageIndex: number}} params - * @returns {Array} - */ -function _loadLinks(params) { - const { pageIndex, docHandle } = params; - const pageHandle = Pdfium.wasmExports.FPDF_LoadPage(docHandle, pageIndex); - const textPage = Pdfium.wasmExports.FPDFText_LoadPage(pageHandle); - if (textPage == null) return []; - const linkPage = Pdfium.wasmExports.FPDFLink_LoadWebLinks(textPage); - if (linkPage == null) return []; - - const links = []; - const count = Pdfium.wasmExports.FPDFLink_CountWebLinks(linkPage); - const rectBuffer = Pdfium.wasmExports.malloc(8 * 4); // double[4] - for (let i = 0; i < count; i++) { - const rectCount = Pdfium.wasmExports.FPDFLink_CountRects(linkPage, i); - const rects = []; - for (let j = 0; j < rectCount; j++) { - Pdfium.wasmExports.FPDFLink_GetRect(linkPage, i, j, rectBuffer, rectBuffer + 8, rectBuffer + 16, rectBuffer + 24); - rects.push(Array.from(new Float64Array(Pdfium.memory.buffer, rectBuffer, 4))); - } - links.push({ - rects: rects, - url: _getLinkUrl(linkPage, i), - }); - } - Pdfium.wasmExports.free(rectBuffer); - Pdfium.wasmExports.FPDFLink_CloseWebLinks(linkPage); - Pdfium.wasmExports.FPDFText_ClosePage(textPage); - Pdfium.wasmExports.FPDF_ClosePage(pageHandle); - return links; -} - -/** - * @param {number} linkPage - * @param {number} linkIndex - * @returns {string} - */ -function _getLinkUrl(linkPage, linkIndex) { - const urlLength = Pdfium.wasmExports.FPDFLink_GetURL(linkPage, linkIndex, null, 0); - const urlBuffer = Pdfium.wasmExports.malloc(urlLength * 2); - Pdfium.wasmExports.FPDFLink_GetURL(linkPage, linkIndex, urlBuffer, urlLength); - const url = StringUtils.utf16BytesToString(new Uint8Array(Pdfium.memory.buffer, urlBuffer, urlLength * 2)); - Pdfium.wasmExports.free(urlBuffer); - return url; -} - -/** - * @param {{docHandle: number, pageIndex: number}} params - * @returns {Array} - */ -function _loadAnnotLinks(params) { - const { pageIndex, docHandle } = params; - const pageHandle = Pdfium.wasmExports.FPDF_LoadPage(docHandle, pageIndex); - const count = Pdfium.wasmExports.FPDFPage_GetAnnotCount(pageHandle); - const rectF = Pdfium.wasmExports.malloc(4 * 4); // float[4] - const links = []; - for (let i = 0; i < count; i++) { - const annot = Pdfium.wasmExports.FPDFPage_GetAnnot(pageHandle, i); - Pdfium.wasmExports.FPDFAnnot_GetRect(annot, rectF); - const [l, t, r, b] = new Float32Array(Pdfium.memory.buffer, rectF, 4); - const rect = [ - l, - t > b ? t : b, - r, - t > b ? b : t, - ]; - const dest = _processAnnotDest(annot, docHandle); - if (dest) { - links.push({rects: [rect], dest: _pdfDestFromDest(dest, docHandle)}); - } else { - const url = _processAnnotLink(annot, docHandle); - if (url) { - links.push({rects: [rect], url: url}); - } - } - Pdfium.wasmExports.FPDFPage_CloseAnnot(annot); - } - Pdfium.wasmExports.free(rectF); - Pdfium.wasmExports.FPDF_ClosePage(pageHandle); - return links; -} - -/** - * - * @param {number} annot - * @param {number} docHandle - * @returns {number|null} Dest - */ -function _processAnnotDest(annot, docHandle) { - const link = Pdfium.wasmExports.FPDFAnnot_GetLink(annot); - - // firstly check the direct dest - const dest = Pdfium.wasmExports.FPDFLink_GetDest(docHandle, link); - if (dest) return dest; - - const action = Pdfium.wasmExports.FPDFLink_GetAction(link); - if (!action) return null; - const PDFACTION_GOTO = 1; - switch (Pdfium.wasmExports.FPDFAction_GetType(action)) { - case PDFACTION_GOTO: - return Pdfium.wasmExports.FPDFAction_GetDest(docHandle, action); - default: - return null; - } -} - -/** - * @param {number} annot - * @param {number} docHandle - * @returns {string|null} URI - */ -function _processAnnotLink(annot, docHandle) { - const link = Pdfium.wasmExports.FPDFAnnot_GetLink(annot); - const action = Pdfium.wasmExports.FPDFLink_GetAction(link); - if (!action) return null; - const PDFACTION_URI = 3; - switch (Pdfium.wasmExports.FPDFAction_GetType(action)) { - case PDFACTION_URI: - const size = Pdfium.wasmExports.FPDFAction_GetURIPath(docHandle, action, null, 0); - const buf = Pdfium.wasmExports.malloc(size); - Pdfium.wasmExports.FPDFAction_GetURIPath(docHandle, action, buf, size); - const uri = StringUtils.utf8BytesToString(new Uint8Array(Pdfium.memory.buffer, buf, size)); - Pdfium.wasmExports.free(buf); - return uri; - default: - return null; - } -} - -/// [PDF 32000-1:2008, 12.3.2.2 Explicit Destinations, Table 151](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf#page=374) -const pdfDestCommands = ['unknown', 'xyz', 'fit', 'fitH', 'fitV', 'fitR', 'fitB', 'fitBH', 'fitBV']; - -/** - * @param {number} dest - * @param {number} docHandle - * @returns {PdfDest|null} - */ -function _pdfDestFromDest(dest, docHandle) { - if (dest == null) return null; - const buf = Pdfium.wasmExports.malloc(40); - const pageIndex = Pdfium.wasmExports.FPDFDest_GetDestPageIndex(docHandle, dest); - const type = Pdfium.wasmExports.FPDFDest_GetView(dest, buf, buf + 4); - const [count] = new Int32Array(Pdfium.memory.buffer, buf, 1); - const params = Array.from(new Float32Array(Pdfium.memory.buffer, buf + 4, count)); - Pdfium.wasmExports.free(buf); - if (type !== 0) { - return { - pageIndex, - command: pdfDestCommands[type], - params, - }; - } - return null; -} - -/** - * Functions that can be called from the main thread - */ -const functions = { - registerFont, - registerFontFromUrl, - loadDocumentFromUrl, - loadDocumentFromData, - closeDocument, - loadOutline, - loadPage, - closePage, - renderPage, - loadText, - loadLinks, -}; - -function handleRequest(data) { - const { id, command, parameters = {} } = data; - - try { - const result = functions[command](parameters); - if (result instanceof Promise) { - result - .then(finalResult => { - if (finalResult.result != null && finalResult.transfer != null) { - postMessage({ id, status: "success", result: finalResult.result }, finalResult.transfer); - } else { - postMessage({ id, status: "success", result: finalResult }); - } - }) - .catch(err => { - postMessage({ - id, - status: "error", - error: _error(err) - }); - }); - } else { - if (result.result != null && result.transfer != null) { - postMessage({ id, status: "success", result: result.result }, result.transfer); - } else { - postMessage({ id, status: "success", result: result }); - } - } - } catch (err) { - postMessage({ - id, - status: "error", - error: _error(err) - }); - } -} - -let messagesBeforeInitialized = []; - -console.log(`PDFium worker initialized: ${self.location.href}`); - - -/** - * Entrypoint - */ -console.log(`Loading PDFium WASM module from ${pdfiumWasmUrl}`); -WebAssembly.instantiateStreaming(fetch(pdfiumWasmUrl), { - env: emEnv, - wasi_snapshot_preview1: wasi -}).then(result => { - Pdfium.initWith(result.instance.exports); - - Pdfium.wasmExports.FPDF_InitLibrary(); - postMessage({ type: "ready" }); - - messagesBeforeInitialized.forEach(event => handleRequest(event.data)); - messagesBeforeInitialized = null; -}).catch(err => { - console.error('Failed to load WASM module:', err); - postMessage({ type: "error", error: _error(err) }); -}); - -onmessage = function(e) { - const data = e.data; - if (data && data.id && data.command) { - if (messagesBeforeInitialized) { - messagesBeforeInitialized.push(e); - return; - } - handleRequest(data); - } else { - console.error("Received improperly formatted message:", data); - } -}; - -const _errorMappings = { - 0: "FPDF_ERR_SUCCESS", - 1: "FPDF_ERR_UNKNOWN", - 2: "FPDF_ERR_FILE", - 3: "FPDF_ERR_FORMAT", - 4: "FPDF_ERR_PASSWORD", - 5: "FPDF_ERR_SECURITY", - 6: "FPDF_ERR_PAGE", - 7: "FPDF_ERR_XFALOAD", - 8: "FPDF_ERR_XFALAYOUT", -}; - -function _getErrorMessage(errorCode) { - const error = _errorMappings[errorCode]; - return error ? `${error} (${errorCode})` : `Unknown error (${errorCode})`; -} - -/** - * String utilities - */ -class StringUtils { - /** - * UTF-16 string to bytes - * @param {number[]} buffer - * @returns {string} Converted string - */ - static utf16BytesToString(buffer) { - let endPtr = 0; - while (buffer[endPtr] || buffer[endPtr + 1]) endPtr += 2; - const str = new TextDecoder('utf-16le').decode(new Uint8Array(buffer.buffer, buffer.byteOffset, endPtr)); - return str; - } - /** - * UTF-8 bytes to string - * @param {number[]} buffer - * @returns {string} Converted string - */ - static utf8BytesToString(buffer) { - let endPtr = 0; - while (buffer[endPtr] && !(endPtr >= buffer.length)) ++endPtr; - - let str = ''; - let idx = 0; - while (idx < endPtr) { - let u0 = buffer[idx++]; - if (!(u0 & 0x80)) { - str += String.fromCharCode(u0); - continue; - } - const u1 = buffer[idx++] & 63; - if ((u0 & 0xE0) == 0xC0) { - str += String.fromCharCode(((u0 & 31) << 6) | u1); - continue; - } - const u2 = buffer[idx++] & 63; - if ((u0 & 0xF0) == 0xE0) { - u0 = ((u0 & 15) << 12) | (u1 << 6) | u2; - } else { - u0 = ((u0 & 7) << 18) | (u1 << 12) | (u2 << 6) | (buffer[idx++] & 63); - } - if (u0 < 0x10000) { - str += String.fromCharCode(u0); - } else { - const ch = u0 - 0x10000; - str += String.fromCharCode(0xD800 | (ch >> 10), 0xDC00 | (ch & 0x3FF)); - } - } - return str; - } - /** - * String to UTF-8 bytes - * @param {string} str - * @param {number[]} buffer - * @returns {number} Number of bytes written to the buffer - */ - static stringToUtf8Bytes(str, buffer) { - let idx = 0; - for(let i = 0; i < str.length; ++i) { - let u = str.charCodeAt(i); - if(u >= 0xD800 && u <= 0xDFFF) { - const u1 = str.charCodeAt(++i); - u = 0x10000 + ((u & 0x3FF) << 10) | (u1 & 0x3FF); - } - if(u <= 0x7F) { - buffer[idx++] = u; - } else if(u <= 0x7FF) { - buffer[idx++] = 0xC0 | (u >> 6); - buffer[idx++] = 0x80 | (u & 63); - } else if(u <= 0xFFFF) { - buffer[idx++] = 0xE0 | (u >> 12); - buffer[idx++] = 0x80 | ((u >> 6) & 63); - buffer[idx++] = 0x80 | (u & 63); - } else { - buffer[idx++] = 0xF0 | (u >> 18); - buffer[idx++] = 0x80 | ((u >> 12) & 63); - buffer[idx++] = 0x80 | ((u >> 6) & 63); - buffer[idx++] = 0x80 | (u & 63); - } - } - buffer[idx++] = 0; - return idx; - } - /** - * Calculate length of UTF-8 string in bytes (it does not contain the terminating '\0' character) - * @param {string} str String to calculate length - * @returns {number} Number of bytes - */ - static lengthBytesUTF8(str) { - let len = 0; - for(let i = 0; i < str.length; ++i) { - let u = str.charCodeAt(i); - if(u >= 0xD800 && u <= 0xDFFF) { - u = 0x10000 + ((u & 0x3FF) << 10) | (str.charCodeAt(++i) & 0x3FF); - } - if(u <= 0x7F) len += 1; - else if(u <= 0x7FF) len += 2; - else if(u <= 0xFFFF) len += 3; - else len += 4; - } - return len; - } - /** - * Allocate memory for UTF-8 string - * @param {string} str - * @returns {number} Pointer to allocated buffer that contains UTF-8 string. The buffer should be released by calling [freeUTF8]. - */ - static allocateUTF8(str) { - if (str == null) return 0; - const size = this.lengthBytesUTF8(str) + 1; - const ptr = Pdfium.wasmExports.malloc(size); - this.stringToUtf8Bytes(str, new Uint8Array(Pdfium.memory.buffer, ptr, size)); - return ptr; - } - /** - * Release memory allocated for UTF-8 string - * @param {number} ptr Pointer to allocated buffer - */ - static freeUTF8(ptr) { - Pdfium.wasmExports.free(ptr); - } -}; diff --git a/wasm/pdfrx_wasm/pubspec.yaml b/wasm/pdfrx_wasm/pubspec.yaml deleted file mode 100644 index f92e1dca..00000000 --- a/wasm/pdfrx_wasm/pubspec.yaml +++ /dev/null @@ -1,20 +0,0 @@ -name: pdfrx_wasm -description: This is a satellite plugin for pdfrx that allows you to use WASM version of Pdfium. -version: 1.1.6 -homepage: https://github.com/espresso3389/pdfrx - -environment: - sdk: '>=3.7.0 <4.0.0' - flutter: ">=3.29.0" - -dependencies: - flutter: - sdk: flutter - -flutter: - plugin: - platforms: - web: - - assets: - - assets/