initial commit

This commit is contained in:
2026-09-21 13:41:40 +09:00
commit 855c7328df
411 changed files with 85352 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
BasedOnStyle: LLVM
IndentWidth: 4
ColumnLimit: 110
AllowShortFunctionsOnASingleLine: Empty
AllowShortIfStatementsOnASingleLine: Never
BreakBeforeBraces: Attach
+45
View File
@@ -0,0 +1,45 @@
# Out-of-source builds and downloaded dependencies
/build/
/build-*/
/cmake-build-*/
/out/
/.deps/
# CMake / CTest files from accidental in-source builds
CMakeFiles/
CMakeCache.txt
cmake_install.cmake
CTestTestfile.cmake
DartConfiguration.tcl
Testing/
/Makefile
/build.ninja
/.ninja_deps
/.ninja_log
/compile_commands.json
/install_manifest*.txt
/CMakeUserPresets.json
# Generated validation reports (keep tests/fixtures under version control)
/tests/results/
# Python caches and local virtual environments
__pycache__/
*.py[cod]
.pytest_cache/
.venv/
venv/
# Local Qt Creator / IDE settings and editor temporary files
*.user
*.user.*
/.vs/
/.idea/
*.swp
*.swo
*~
# OS metadata
.DS_Store
Thumbs.db
Desktop.ini
+195
View File
@@ -0,0 +1,195 @@
cmake_minimum_required(VERSION 3.24)
project(DocView VERSION 0.1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
include(CTest)
if(MSVC)
add_compile_options(/utf-8 /permissive- /Zc:__cplusplus)
endif()
if(WIN32 AND (NOT MSVC OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8))
message(FATAL_ERROR "DocView on Windows requires a 64-bit MSVC-compatible toolchain and matching Qt WebEngine.")
endif()
find_package(Qt6 6.11.2 EXACT REQUIRED COMPONENTS Core Gui Network Qml Quick QuickControls2 WebEngineQuick Xml LinguistTools)
if(WIN32)
find_package(libzip 1.11 CONFIG REQUIRED)
find_package(tomlplusplus 3.4 CONFIG REQUIRED)
set(DOCVIEW_ZIP_TARGET libzip::zip)
set(DOCVIEW_TOML_TARGET tomlplusplus::tomlplusplus)
else()
find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBZIP REQUIRED IMPORTED_TARGET libzip>=1.11)
pkg_check_modules(TOML REQUIRED IMPORTED_TARGET tomlplusplus>=3.4)
pkg_check_modules(SECCOMP REQUIRED IMPORTED_TARGET libseccomp>=2.5)
pkg_check_modules(FONTCONFIG REQUIRED IMPORTED_TARGET fontconfig>=2.13)
set(DOCVIEW_ZIP_TARGET PkgConfig::LIBZIP)
set(DOCVIEW_TOML_TARGET PkgConfig::TOML)
endif()
include(cmake/Pdfium.cmake)
find_package(qpdf 12.4.1 EXACT CONFIG REQUIRED)
add_library(docview_common
src/common/ipc.cpp src/common/contracts.cpp src/common/sandbox.cpp
src/common/worker_process.cpp src/common/session_storage.cpp src/common/single_instance.cpp
src/common/windows_sandbox.cpp src/common/windows_pipe.cpp src/common/windows_runtime_staging.cpp
src/common/worker_runtime.cpp
src/common/font_contract.cpp src/common/worker_font_client.cpp
src/web/resource_policy.cpp src/web/renderer_watchdog.cpp)
target_include_directories(docview_common PUBLIC src)
target_link_libraries(docview_common PUBLIC Qt6::Core Qt6::Network)
if(WIN32)
target_compile_definitions(docview_common PUBLIC _WIN32_WINNT=0x0A00 NOMINMAX WIN32_LEAN_AND_MEAN)
target_link_libraries(docview_common PRIVATE userenv advapi32 kernel32 shell32 ole32 psapi)
else()
target_link_libraries(docview_common PRIVATE PkgConfig::SECCOMP)
endif()
add_library(docview_core src/core/config.cpp src/core/input.cpp src/core/state.cpp src/core/types.cpp
src/common/memory_pressure.cpp)
target_include_directories(docview_core PUBLIC src)
target_link_libraries(docview_core PUBLIC Qt6::Core ${DOCVIEW_TOML_TARGET})
if(WIN32)
target_compile_definitions(docview_core PRIVATE _WIN32_WINNT=0x0A00 NOMINMAX WIN32_LEAN_AND_MEAN)
target_link_libraries(docview_core PRIVATE shell32 ole32)
endif()
add_library(docview_pdf src/pdf/pdf_document.cpp src/pdf/link_border_reader.cpp src/pdf/pdf_metadata_context.cpp src/pdf/structure_reader.cpp)
target_sources(docview_pdf PRIVATE src/pdf/system_font_adapter.cpp)
target_include_directories(docview_pdf PUBLIC src)
target_link_libraries(docview_pdf PUBLIC Qt6::Core PDFium::PDFium docview_common)
target_link_libraries(docview_pdf PRIVATE qpdf::libqpdf)
add_library(docview_font_broker src/broker/font_broker.cpp src/broker/font_backend.cpp)
target_include_directories(docview_font_broker PUBLIC src)
target_link_libraries(docview_font_broker PUBLIC docview_common Qt6::Core)
if(WIN32)
target_sources(docview_font_broker PRIVATE src/broker/font_backend_win.cpp)
target_link_libraries(docview_font_broker PRIVATE gdi32)
else()
target_sources(docview_font_broker PRIVATE src/broker/font_backend_linux.cpp)
target_link_libraries(docview_font_broker PRIVATE PkgConfig::FONTCONFIG)
endif()
add_executable(docview-pdf-worker src/pdf/main.cpp)
target_link_libraries(docview-pdf-worker PRIVATE docview_pdf docview_common)
add_library(docview_archive src/archive/archive.cpp)
target_include_directories(docview_archive PUBLIC src)
target_link_libraries(docview_archive PUBLIC Qt6::Core Qt6::Xml ${DOCVIEW_ZIP_TARGET})
add_executable(docview-archive-worker src/archive/main.cpp)
target_link_libraries(docview-archive-worker PRIVATE docview_archive docview_common)
set(DOCVIEW_SHELL_SOURCES src/app/controller.cpp src/app/pdf_canvas.cpp src/web/web_profile.cpp resources.qrc)
qt_add_executable(docview src/app/main.cpp src/app/benchmark.cpp src/app/translations.cpp ${DOCVIEW_SHELL_SOURCES})
target_link_libraries(docview PRIVATE docview_core docview_common docview_font_broker Qt6::Gui Qt6::Quick Qt6::Qml Qt6::QuickControls2 Qt6::WebEngineQuick)
add_dependencies(docview docview-pdf-worker docview-archive-worker)
qt_add_translations(TARGETS docview
SOURCE_TARGETS docview docview_core docview_common docview_pdf docview_archive
docview_font_broker docview-pdf-worker docview-archive-worker
SOURCES qml/Main.qml qml/WebPane.qml
TS_FILES resources/i18n/docview_ja.ts
LUPDATE_OPTIONS -locations relative -no-obsolete
LUPDATE_TARGET docview_lupdate
LRELEASE_TARGET docview_lrelease
RESOURCE_PREFIX "/i18n")
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
# Register candidate revalidation before any install rule copies a binary.
include(cmake/PdfiumSupplementalNotices.cmake)
endif()
if(UNIX)
set_target_properties(docview-pdf-worker PROPERTIES INSTALL_RPATH "$ORIGIN/../lib")
install(FILES "${DOCVIEW_PDFIUM_LIBRARY}" DESTINATION lib)
endif()
install(TARGETS docview docview-pdf-worker docview-archive-worker RUNTIME DESTINATION bin)
install(FILES resources/config.example.toml DESTINATION share/doc/docview)
install(DIRECTORY resources/licenses/qpdf DESTINATION share/doc/docview/licenses)
install(FILES "${DOCVIEW_PDFIUM_ROOT}/LICENSE" DESTINATION share/doc/docview/pdfium)
install(DIRECTORY "${DOCVIEW_PDFIUM_ROOT}/licenses/" DESTINATION share/doc/docview/pdfium/licenses)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
install(FILES docs/LINUX-DEVELOPMENT-PACKAGE.md DESTINATION share/doc/docview)
install(FILES resources/licenses/README.md DESTINATION share/doc/docview RENAME THIRD-PARTY-SCOPE.md)
include(cmake/LinuxDevelopmentPackage.cmake)
endif()
if(WIN32)
include(cmake/WindowsDeployment.cmake)
foreach(worker docview-pdf-worker docview-archive-worker)
docview_configure_windows_worker(${worker})
docview_install_windows_worker(${worker})
endforeach()
docview_install_windows_gui(docview)
endif()
if(BUILD_TESTING)
find_package(Qt6 REQUIRED COMPONENTS Test QuickTest)
find_package(Python3 REQUIRED COMPONENTS Interpreter)
qt_add_executable(test_translations tests/test_translations.cpp src/app/translations.cpp)
target_link_libraries(test_translations PRIVATE docview_core Qt6::Test Qt6::Quick Qt6::Qml Qt6::QuickControls2)
qt_add_resources(test_translations test_translations_catalog
PREFIX "/i18n" BASE "${CMAKE_CURRENT_BINARY_DIR}"
FILES "${CMAKE_CURRENT_BINARY_DIR}/docview_ja.qm")
add_dependencies(test_translations docview_lrelease)
add_test(NAME translations COMMAND test_translations)
set_tests_properties(translations PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen;QT_QUICK_BACKEND=software;LANG=C.UTF-8")
add_test(NAME runtime_manifest COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_runtime_manifest.py" -v)
add_test(NAME benchmark_runner COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_benchmark_runner.py" -v)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
add_test(NAME linux_package COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_linux_package.py" -v)
add_test(NAME dependency_provenance COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_dependency_provenance_qt.py" -v)
add_test(NAME ubuntu_runtime_inventory COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_ubuntu_validation_runtime.py" -v)
add_test(NAME pdfium_candidate_staging COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_stage_pdfium_candidate.py" -v)
add_test(NAME pdfium_candidate_integration COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_pdfium_candidate_integration.py" -v)
endif()
foreach(name core archive pdf security instance renderer_watchdog worker_process runtime_staging font_contract font_broker system_font_adapter memory_pressure)
add_executable(test_${name} tests/test_${name}.cpp)
target_link_libraries(test_${name} PRIVATE docview_common Qt6::Test)
if(TARGET docview_${name})
target_link_libraries(test_${name} PRIVATE docview_${name})
endif()
add_test(NAME ${name} COMMAND test_${name})
endforeach()
target_link_libraries(test_memory_pressure PRIVATE docview_core)
target_link_libraries(test_pdf PRIVATE docview_font_broker qpdf::libqpdf)
target_link_libraries(test_system_font_adapter PRIVATE docview_pdf)
add_executable(pdf-worker-evidence tests/render_pdf_worker.cpp)
target_link_libraries(pdf-worker-evidence PRIVATE docview_common docview_font_broker Qt6::Gui)
add_dependencies(pdf-worker-evidence docview-pdf-worker)
if(WIN32)
foreach(name windows_pipe windows_resources windows_worker)
add_executable(test_${name} tests/test_${name}.cpp)
target_link_libraries(test_${name} PRIVATE docview_common Qt6::Test)
add_test(NAME ${name} COMMAND test_${name})
endforeach()
add_executable(docview-windows-worker-probe tests/windows_worker_probe.cpp)
target_link_libraries(docview-windows-worker-probe PRIVATE docview_common ws2_32 userenv advapi32 ole32)
docview_configure_windows_worker(docview-windows-worker-probe)
add_dependencies(test_windows_worker docview-windows-worker-probe)
set_tests_properties(windows_worker PROPERTIES TIMEOUT 120)
endif()
add_dependencies(test_pdf docview-pdf-worker)
add_dependencies(test_archive docview-archive-worker)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
# Exercises the actual 10/30/120-second timers without shortening the
# production budgets. Run explicitly, or opt in to the long CTest case.
add_executable(test_worker_deadlines tests/test_worker_deadlines.cpp)
target_link_libraries(test_worker_deadlines PRIVATE docview_common Qt6::Test)
option(DOCVIEW_ENABLE_LONG_TESTS "Register real-time worker watchdog tests" OFF)
if(DOCVIEW_ENABLE_LONG_TESTS)
add_test(NAME worker_deadlines COMMAND test_worker_deadlines)
set_tests_properties(worker_deadlines PROPERTIES TIMEOUT 150 LABELS "long")
endif()
endif()
add_executable(test_pdf_canvas tests/test_pdf_canvas.cpp src/app/pdf_canvas.cpp)
target_link_libraries(test_pdf_canvas PRIVATE docview_pdf docview_common docview_font_broker Qt6::Test Qt6::Quick Qt6::Gui)
add_dependencies(test_pdf_canvas docview-pdf-worker)
add_test(NAME pdf_canvas COMMAND test_pdf_canvas)
set_tests_properties(pdf_canvas PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
add_test(NAME pdf_canvas_hidpi COMMAND test_pdf_canvas annotationFlagsHitTargetsMatchRenderedPixels sceneGraphPresentsPdfPixelsAndReplacesOldPage)
set_tests_properties(pdf_canvas_hidpi PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen;QT_SCALE_FACTOR=2")
qt_add_executable(test_app tests/test_app.cpp ${DOCVIEW_SHELL_SOURCES})
target_link_libraries(test_app PRIVATE docview_core docview_common docview_font_broker Qt6::Gui Qt6::Quick Qt6::Qml Qt6::QuickControls2 Qt6::WebEngineQuick Qt6::Test ${DOCVIEW_ZIP_TARGET})
target_compile_definitions(test_app PRIVATE DOCVIEW_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
add_dependencies(test_app docview-pdf-worker docview-archive-worker)
add_test(NAME app COMMAND test_app)
qt_add_executable(test_web_qml tests/test_web_qml.cpp src/web/web_profile.cpp resources.qrc)
target_link_libraries(test_web_qml PRIVATE docview_common Qt6::Gui Qt6::Quick Qt6::Qml Qt6::WebEngineQuick Qt6::QuickTest Qt6::Test)
add_test(NAME web COMMAND test_web_qml)
set_tests_properties(app web PROPERTIES TIMEOUT 90)
endif()
+152
View File
@@ -0,0 +1,152 @@
# DocView
PDF、ローカルHTML、HTML ZIP、DRMなしのEPUB 2/3を読む、Qt Quick製の文書ビューアーです。日本語UIとVim風のキーボード操作を備え、PDF原本の編集・保存は行いません。
**動作を確認したのはLinux開発版です。Windows向けの起動・保護・配布コードは実装しましたが、Windowsでのコンパイルと実行、対象OSでの配布確認、基準機での性能受入は未完了です。** 実施範囲と残作業は [検証記録](docs/VALIDATION.md)、設計基準は [設計書](docs/design/00-guide.md) を参照してください。
[PDFiumの第2修正候補](tests/results/pdfium-intent-context/README.md)を専用ビルドとUbuntu検証用debへ組み込みました。ICC変換指定、パターンの状態継承、タイルの線色を修正し、Arch・Ubuntuで各972点の描画試験とDocView全24群、上流1,979試験が成功しました。PDF性能4条件と約1GB・5,000ページの負荷試験も成功しています。
比較用に残した元の依存版には[ICC変換指定による描画差](tests/results/cmyk-lut/README.md)があります。修正版の試験では以前のパターン期待値も訂正しています。生成資料の成功だけで色の互換性全体を保証するものではなく、人による描画承認を含む最終受入は未完了です。
## ビルドと起動
この作業環境の修正版は `./build-context/docview` で起動します。[修正版PDFiumの配置・再ビルド手順](docs/PDFIUM-CANDIDATE.md)を参照してください。以下の `.deps/pdfium` を使う手順は比較用に保持した元の依存版です。
確認済み環境はArch Linux x64(GCC 16.2.1)と専用Ubuntu 24.04仮想環境(GCC 13.3.0)、Qt 6.11.2です。C++20、CMake 3.24以上、Ninja、pkg-config(Archではpkgconf)、Qt Core/Gui/Network/Qml/Quick/QuickControls2/WebEngineQuick/Xml/LinguistTools、libzip 1.11以上、toml++ 3.4以上、qpdf 12.4.1、libseccomp 2.5以上、Fontconfig 2.13以上が必要です。Qt標準部品の日本語表示にはQtの日本語翻訳資源も必要です(Archのqt6-translations)。試験にはQt Test/QuickTestとXvfbを使います。PDFiumは固定版の非V8ビルドを取得します。
```sh
python3 cmake/fetch_pdfium.py
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build -j 6
./build/docview "/path/to/book.pdf"
```
引数なしでも起動でき、`Ctrl+O`、ドラッグ&ドロップ、`:open "パス"`で文書を開けます。`--page 42`でPDFの実ページを指定できます。独自設定を使う場合は`--config /path/to/config.toml`を指定します。通常起動時は同一ユーザーの既存ウィンドウへ文書を引き渡します。
Linuxワーカーの隔離にはLandlock ABI 3以上、seccomp、利用可能なQt WebEngineのサンドボックスが必要です。隔離できない環境では文書処理を開始しません。`QTWEBENGINE_DISABLE_SANDBOX`、`--no-sandbox`などによる回避はサポートしません。
Ubuntuの公式Qt SDKを用いた[ビルド・検証手順](tests/ubuntu_vm/README.md)、[ローカル検証用debの手順](docs/UBUNTU-PACKAGE.md)、[URL比較修正後の試験記録](tests/results/portal/README.md)、[Qt告知補足版の検証](tests/results/qt-notice-supplement/README.md)も保存しています。
Windowsでのビルド手順、必要な依存物、未検証項目は [Windowsの準備手順](docs/WINDOWS-BUILD.md) にまとめています。
日本語を初期表示にし、翻訳可能な文言を[翻訳カタログ](resources/i18n/docview_ja.ts)へ分離しています。
文言を変更したときは[更新手順](docs/TRANSLATIONS.md)に従ってカタログを更新します。
## 主な操作
| キー | 動作 |
|---|---|
| `j` / `k` / `h` / `l` | 下・上・左・右へスクロール |
| `Ctrl+D` / `Ctrl+U` | 半画面進む・戻る |
| `Ctrl+F` / `Ctrl+B` | 一画面進む・戻る |
| `gg` / `G` | 文書の先頭・末尾 |
| `42G` / `:page 42` | PDFの42ページ目へ移動 |
| `J` / `K` | 次・前のPDFページ |
| `]]` / `[[` | 次・前の見出し |
| `]c` / `[c` | 次・前のEPUB章 |
| `t` / `p` / `zs` | 目次・ページ一覧・ステータスの個別切替 |
| `Ctrl+W` → `w` | 本文・目次・ページ一覧の操作先を切替 |
| `Enter` / `Esc` | 項目・リンク・PDF注釈の確定 / 取消・本文へ戻る |
| `+` / `-` / `=` | 拡大・縮小・幅に合わせる |
| `:rotate 90` | PDFを90度回転 |
| `Tab` / `Shift+Tab` | 次・前のリンクを選択(PDFでは注釈も対象) |
| `/` / `n` / `N` | 本文検索・次・前の結果 |
| `Alt+Left` / `Alt+Right` | 移動履歴を戻る・進む |
| `?` / `:help` | 現在の設定に対応する操作一覧 |
| `:info` / `:reload` / `:q` | 文書情報・設定再読込・終了 |
目次・ページ一覧では`j`/`k`で選択し、`Enter`で移動します。目次の`>`は現在読んでいる節、枠はキー操作で選択中の項目を示します。`h`/`l`は階層の折りたたみ・展開です。入力欄やダイアログでは閲覧キーを停止します。操作一覧・コマンド入力・ダイアログを閉じると元の操作先へ戻ります。元のパネルが非表示になった場合は本文へ戻り、文書の切替などで操作先が指定された場合はその指定を優先します。PDFのステータスにはページラベルと実ページ番号を表示します。
目次の準備中に`t`を押すと読み込み中の表示を開き、準備できた項目を表示します。PDFの構造見出しがない場合、文書内の移動先を持つ目次項目を見出し移動に使います。外部リンクや禁止されたアクションは代替候補に含めません。Webの選択リンクは`Esc`で解除できます。
HTML・EPUBで欠落した資源や遮断された参照がある場合は、`:info`で理由と検出件数を確認できます。同じ資源の再試行も件数に含まれます。URLや本文を警告履歴に保存しません。資源提供側の読込み失敗は、不在・アクセス拒否・破損・上限・保存や読取りの失敗などを理由別に区別します。
すべての形式でステータスに倍率を表示します。HTML・HTML ZIP・流込EPUBの倍率は25–500%です。固定レイアウトEPUBでは、ページ全体または幅に合わせた大きさを100%とし、倍率に「ページ合わせ」「幅合わせ」を添えます。`privacy.remember_position`が有効なら、読書位置と倍率を保存して同じ原本を開き直したときに復元します。固定EPUBは合わせ方も保存するため、ウィンドウの大きさが変わってもその基準で表示します。
ファイル選択や`:open`で読込みに失敗した場合は、対象名、原因の分類、利用者ができる対処を表示します。切替前の文書は表示を続け、`:info`で最後に開こうとした文書の失敗を確認できます。この情報は表示中の文書の警告と分けて扱い、ディスクには保存せず、次に文書を開く試行で更新します。
固定レイアウトEPUBは著者指定に応じた見開きに対応し、RTL、中央の単独ページ、項目別指定を反映します。複数入口のHTML ZIPで選んだページは、履歴または位置保存が有効なら、同じ原本を開き直したときに再利用します。`:history clear`でこの選択も消去できます。
PDF検索は既存の文字情報、HTML/ZIP検索は表示中のHTML、EPUB検索は表示中の章を対象にします。EPUBの`gg`/`G`は通常の読書順序に含まれる先頭章の先頭/最終章の末尾へ移動します。
## 設定と保存先
[設定例](resources/config.example.toml)を配置して編集し、`:reload`で再読込します。検証に失敗した設定は適用されず、現在の有効な設定が維持されます。既定のLinux保存先は次のとおりです。
| 用途 | パス |
|---|---|
| 設定 | `$XDG_CONFIG_HOME/docview/config.toml`(未指定時`~/.config/docview/config.toml`) |
| 読書状態 | `$XDG_STATE_HOME/docview/state.json`(未指定時`~/.local/state/docview/state.json`) |
| 一時資源 | `$XDG_CACHE_HOME/docview/sessions`(未指定時`~/.cache/docview/sessions`) |
`privacy.remember_position`と`privacy.recent_documents`で位置・履歴の保存を制御します。`:history clear`は件数を確認して履歴と保存位置を消去します。本文引用の保存は既定で無効です。パスワードは保存しません。
開いた原本の識別情報を保存・復元時に再確認します。閲覧中に原本が変更・置換された場合、以前の文書の位置を新しい原本へ保存しません。保存済み位置との同一性を確認できない原本を明示的に開いた場合は、復元できないことを通知して先頭から表示します。
履歴を有効にしている場合、文書を開く前の画面に最近使った文書を表示します。矢印または`j`/`k`で選び、`Enter`で開けます。削除・変更・置換された原本は自動では開かず、ファイル選択から開き直すよう通知します。
単独HTMLは原則として同じフォルダー配下の資源を参照します。親フォルダーにある資源が必要な場合は`:root`で許可する文書フォルダーを明示します。文書のJavaScript、外部資源の自動取得、フォーム送信は無効です。利用者が選んだ外部リンクは宛先を確認して外部ブラウザーで開きます。
## 試験
試験資料はリポジトリー内の生成スクリプトから作成します。基本PDFの再生成にはqpdf、追加PDFの再生成にはReportLab、pypdf、Pillowと指定フォントが必要です。生成済み資料とハッシュも含めています。
```sh
python3 tests/fixtures/pdf/generate.py
python3 tests/generate_web_fixtures.py
QT_QPA_PLATFORM=xcb QT_QUICK_BACKEND=software \
QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu \
xvfb-run -a ctest --test-dir build --output-on-failure --output-junit tests.xml
```
通常のデスクトップセッションではXvfbを省略できます。試験でもアプリ内部の隔離は有効です。外側の実行環境がローカルソケットや名前空間を禁止する場合、その環境ではGUI・ワーカー試験を実施できません。
Linuxの実時間watchdog試験は`./build/test_worker_deadlines`で別途実行できます。PDFの30秒通知・120秒停止、展開の10秒通知・30秒停止、待機中の取消を検証します。[現行結果と再現手順](tests/results/completion-final/worker-deadlines/README.md)を参照してください。
```sh
python3 tests/compare_pdf_renderers.py
python3 tests/compare_pdf_extended.py
python3 tests/compare_pdf_fonts.py
python3 tests/compare_font_collection.py
python3 tests/compare_pdf_icc.py
QT_QPA_PLATFORM=xcb QT_QUICK_BACKEND=software \
QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu \
xvfb-run -a python3 tests/benchmark.py --operations 125 --cold-process-runs 30
```
描画比較には独立したPopplerを使います。実TTCと個別TTFを比べる追加試験にはfontToolsとOFLフォントを使い、一時フォントは試験後に削除します。性能測定は新プロセス・空のアプリキャッシュで30回、同一プロセスの初回と再openを30回、同条件の29開閉、対応するページ・見出し・章系列を各125操作、継続scrollを240入力で記録します。PDFの125操作はページ移動101回と倍率変更24回です。OSファイルキャッシュは消去しません。Qtのフレーム通知は物理ディスプレイへの提示時刻ではなく、この開発機の結果で基準機・両OSの合格とはしません。
PDFの比較は本番の隔離ワーカーとフォント供給処理を経由します。非埋込日本語の横書き・縦書き資料は`tests/fixtures/pdf/generate_fonts.py`で、タグ付きPDFの境界資料は`tests/fixtures/pdf/generate_headings.py`で再生成できます。使用した代替フォントと制約は[PDFの検証記録](tests/PDF-VALIDATION.md)にまとめています。
500ページのPDF、500章のEPUB、400見出しのHTMLを使う測定も実行できます。これらは小さな画像を再利用した生成資料であり、1 GiBのスキャン資料ではありません。
```sh
python3 tests/generate_performance_corpus.py
QT_QPA_PLATFORM=xcb QT_QUICK_BACKEND=software \
QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu \
xvfb-run -a python3 tests/benchmark.py --corpus standard --operations 125 --cold-process-runs 30 --output tests/results/performance-standard
```
長押し、連続する遠距離ジャンプ、リサイズ、文書切替、取消の計測は別に実行します。出力先は新規ディレクトリーを指定してください。通常のCTestではこの計測用slotだけを意図的にskipします。
```sh
QT_QPA_PLATFORM=xcb QT_QUICK_BACKEND=software \
QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu \
xvfb-run -a -s '-screen 0 1100x760x24' python3 tests/measure_interaction.py \
--binary build/test_app --output tests/results/performance-interaction/new-run
```
約1GB・5,000ページの画像PDFも生成し、100操作と末尾への移動を検証しました。[負荷試験記録](tests/STRESS-PDF-VALIDATION.md)に数値と再現手順があります。大容量資料は試験後に自動削除します。
## ローカルインストール
```sh
cmake --install build --prefix "$PWD/build/install"
./build/install/bin/docview
```
本体、ワーカー、PDFiumとそのライセンスを配置します。Qtとその他のライブラリはシステムの動的ライブラリを利用します。単体で配布できるパッケージではありません。依存関係は [DEPENDENCIES.md](docs/DEPENDENCIES.md)、追加形式の接続方法は [adapter-contract.md](docs/adapter-contract.md) を参照してください。
Arch Linux向けの開発用tar.gzと、依存物の版・ハッシュ・告知をまとめる手順は [ローカルパッケージ](docs/LINUX-DEVELOPMENT-PACKAGE.md) にあります。生成物は同じ開発環境のシステムライブラリーを必要とします。
Ubuntu 24.04 amd64向けには、修正版PDFiumとQt SDK等を同梱した[validation4 deb](docs/UBUNTU-PACKAGE.md)を作成しました。ビルド環境とSDKのない既存試験VMで、それぞれX11・Wayland合計12条件の起動、install/remove/purgeを確認しています。新規OSからの試験は旧validation1の履歴です。公開リリースの受入は未完了です。
+95
View File
@@ -0,0 +1,95 @@
# Verify the complete inventory before installation and again at the destination.
include("${CMAKE_CURRENT_LIST_DIR}/WriteWindowsRuntimeManifest.cmake")
function(docview_verify_windows_worker executable output_files)
docview_runtime_file_info("${executable}" EXE worker_name worker_size)
get_filename_component(worker_directory "${executable}" DIRECTORY)
set(manifest "${executable}.runtime.json")
if(NOT EXISTS "${manifest}" OR IS_SYMLINK "${manifest}" OR IS_DIRECTORY "${manifest}")
message(FATAL_ERROR "Build the worker runtime manifest before installing: ${manifest}")
endif()
file(SIZE "${manifest}" manifest_size)
if(manifest_size LESS 1 OR manifest_size GREATER 131072)
message(FATAL_ERROR "Worker runtime manifest exceeds its size limit or is empty")
endif()
file(READ "${manifest}" inventory)
string(JSON root_type TYPE "${inventory}")
string(JSON fields LENGTH "${inventory}")
string(JSON version_type TYPE "${inventory}" schemaVersion)
string(JSON executable_type TYPE "${inventory}" executable)
string(JSON files_type TYPE "${inventory}" files)
string(JSON version GET "${inventory}" schemaVersion)
string(JSON declared_executable GET "${inventory}" executable)
string(JSON count LENGTH "${inventory}" files)
if(NOT root_type STREQUAL "OBJECT" OR NOT fields EQUAL 3 OR
NOT version_type STREQUAL "NUMBER" OR NOT version STREQUAL "1" OR
NOT executable_type STREQUAL "STRING" OR NOT declared_executable STREQUAL worker_name OR
NOT files_type STREQUAL "ARRAY" OR count LESS 1 OR count GREATER 128)
message(FATAL_ERROR "Invalid worker runtime inventory schema")
endif()
math(EXPR last "${count} - 1")
set(files)
set(names)
set(total 0)
set(found_executable FALSE)
foreach(index RANGE 0 ${last})
string(JSON entry_type TYPE "${inventory}" files ${index})
string(JSON fields LENGTH "${inventory}" files ${index})
string(JSON name_type TYPE "${inventory}" files ${index} path)
string(JSON hash_type TYPE "${inventory}" files ${index} sha256)
string(JSON size_type TYPE "${inventory}" files ${index} size)
string(JSON name GET "${inventory}" files ${index} path)
string(JSON expected_hash GET "${inventory}" files ${index} sha256)
string(JSON expected_size GET "${inventory}" files ${index} size)
string(LENGTH "${expected_hash}" hash_length)
if(NOT entry_type STREQUAL "OBJECT" OR NOT fields EQUAL 3 OR
NOT name_type STREQUAL "STRING" OR NOT hash_type STREQUAL "STRING" OR
NOT size_type STREQUAL "NUMBER" OR NOT expected_size MATCHES "^[1-9][0-9]*$" OR
NOT hash_length EQUAL 64 OR NOT expected_hash MATCHES "^[0-9a-f]+$")
message(FATAL_ERROR "Invalid worker runtime file record")
endif()
if(name STREQUAL worker_name)
set(kind EXE)
set(found_executable TRUE)
else()
set(kind DLL)
endif()
get_filename_component(basename "${name}" NAME)
if(NOT name STREQUAL basename)
message(FATAL_ERROR "Runtime manifest contains a non-basename path")
endif()
set(path "${worker_directory}/${name}")
docview_runtime_file_info("${path}" "${kind}" checked_name size)
string(TOLOWER "${name}" folded)
if(folded IN_LIST names)
message(FATAL_ERROR "Runtime manifest has a duplicate name")
endif()
list(APPEND names "${folded}")
math(EXPR total "${total} + ${size}")
if(NOT size EQUAL expected_size OR total GREATER 536870912)
message(FATAL_ERROR "Runtime file size no longer matches its build inventory: ${name}")
endif()
file(SHA256 "${path}" actual_hash)
if(NOT actual_hash STREQUAL expected_hash)
message(FATAL_ERROR "Runtime file no longer matches its build inventory: ${name}")
endif()
list(APPEND files "${path}")
endforeach()
if(NOT found_executable)
message(FATAL_ERROR "Runtime manifest is missing its worker executable")
endif()
set(${output_files} "${files}" PARENT_SCOPE)
endfunction()
docview_verify_windows_worker("${WORKER_EXECUTABLE}" verified_files)
if(NOT WORKER_VERIFY_ONLY)
if(NOT IS_ABSOLUTE "${WORKER_INSTALL_DIRECTORY}")
message(FATAL_ERROR "An absolute worker installation directory is required")
endif()
# file(INSTALL) applies DESTDIR itself. Its input must retain the unstaged
# install prefix; native tools and the verification below need the real path.
file(INSTALL DESTINATION "${WORKER_INSTALL_DIRECTORY}" TYPE FILE
FILES ${verified_files} "${WORKER_EXECUTABLE}.runtime.json")
get_filename_component(worker_name "${WORKER_EXECUTABLE}" NAME)
docview_verify_windows_worker("$ENV{DESTDIR}${WORKER_INSTALL_DIRECTORY}/${worker_name}" installed_files)
endif()
+16
View File
@@ -0,0 +1,16 @@
# Local Arch development packaging is explicit; normal builds do not package.
include_guard(GLOBAL)
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
return()
endif()
find_package(Python3 3.11 COMPONENTS Interpreter QUIET)
if(Python3_Interpreter_FOUND)
set(DOCVIEW_LINUX_PACKAGE_OUTPUT "${CMAKE_BINARY_DIR}/linux-development-package" CACHE PATH
"New output directory for the local Arch development package")
add_custom_target(linux-development-package
COMMAND "${Python3_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/tools/package_linux_development.py"
--build-dir "${CMAKE_BINARY_DIR}" --output "${DOCVIEW_LINUX_PACKAGE_OUTPUT}"
DEPENDS docview docview-pdf-worker docview-archive-worker
USES_TERMINAL VERBATIM
COMMENT "Packaging the local Arch development build with dependency notices")
endif()
+10
View File
@@ -0,0 +1,10 @@
# The non-V8 build never executes PDF JavaScript or dynamic XFA.
set(DOCVIEW_PDFIUM_ROOT "${CMAKE_SOURCE_DIR}/.deps/pdfium" CACHE PATH "Pinned PDFium installation")
find_path(DOCVIEW_PDFIUM_INCLUDE_DIR fpdfview.h HINTS "${DOCVIEW_PDFIUM_ROOT}/include" REQUIRED)
find_library(DOCVIEW_PDFIUM_LIBRARY NAMES pdfium pdfium.dll HINTS "${DOCVIEW_PDFIUM_ROOT}/lib" REQUIRED)
add_library(PDFium::PDFium SHARED IMPORTED GLOBAL)
set_target_properties(PDFium::PDFium PROPERTIES IMPORTED_LOCATION "${DOCVIEW_PDFIUM_LIBRARY}" INTERFACE_INCLUDE_DIRECTORIES "${DOCVIEW_PDFIUM_INCLUDE_DIR}")
if(WIN32)
find_file(DOCVIEW_PDFIUM_RUNTIME pdfium.dll HINTS "${DOCVIEW_PDFIUM_ROOT}/bin" NO_DEFAULT_PATH REQUIRED)
set_target_properties(PDFium::PDFium PROPERTIES IMPORTED_IMPLIB "${DOCVIEW_PDFIUM_LIBRARY}" IMPORTED_LOCATION "${DOCVIEW_PDFIUM_RUNTIME}")
endif()
+83
View File
@@ -0,0 +1,83 @@
# Extra notices tied to the exact inspected Linux PDFium library and source pin.
# These are install data; no application code or PDF rendering option changes.
set(_notice_root "${CMAKE_CURRENT_SOURCE_DIR}/resources/licenses/pdfium-supplemental")
file(READ "${_notice_root}/sources.json" _notice_manifest)
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/cmake/pdfium.lock.json" _pdfium_lock)
foreach(_pair "pdfiumVersion;version" "pdfiumUpstreamCommit;upstreamCommit")
list(GET _pair 0 _notice_key)
list(GET _pair 1 _lock_key)
string(JSON _notice_value GET "${_notice_manifest}" "${_notice_key}")
string(JSON _lock_value GET "${_pdfium_lock}" "${_lock_key}")
if(NOT _notice_value STREQUAL _lock_value)
message(FATAL_ERROR "PDFium supplemental notices need review for this source pin")
endif()
endforeach()
string(JSON _notice_binary GET "${_notice_manifest}" pdfiumLibrarySha256)
file(SHA256 "${DOCVIEW_PDFIUM_LIBRARY}" _actual_binary)
if(NOT _notice_binary STREQUAL _actual_binary OR EXISTS "${DOCVIEW_PDFIUM_ROOT}/BUILDINFO.json")
# Only the separately reviewed v2 tuple may differ from the original
# provider. There is deliberately no user-supplied trust/hash override.
find_package(Python3 3.11 REQUIRED COMPONENTS Interpreter)
set(_candidate_verifier "${CMAKE_CURRENT_SOURCE_DIR}/tools/verify_pdfium_candidate.py")
set(_candidate_notice "${CMAKE_CURRENT_BINARY_DIR}/pdfium-candidate-notices/sources.json")
execute_process(COMMAND "${Python3_EXECUTABLE}" "${_candidate_verifier}"
--prefix "${DOCVIEW_PDFIUM_ROOT}" --library "${DOCVIEW_PDFIUM_LIBRARY}"
--include-dir "${DOCVIEW_PDFIUM_INCLUDE_DIR}" --notice-output "${_candidate_notice}"
RESULT_VARIABLE _candidate_result OUTPUT_VARIABLE _candidate_record ERROR_VARIABLE _candidate_error)
if(NOT _candidate_result EQUAL 0)
message(FATAL_ERROR "PDFium supplemental notices need review for this binary: ${_candidate_error}")
endif()
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
"${DOCVIEW_PDFIUM_ROOT}/BUILDINFO.json"
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/pdfium-candidate-v2.lock.json"
"${_candidate_verifier}")
# Recheck at install as well: configure success is not permission to install
# a prefix whose library, headers, or correspondence subsequently changed.
install(CODE "
execute_process(COMMAND \"${Python3_EXECUTABLE}\" \"${_candidate_verifier}\"
--prefix \"${DOCVIEW_PDFIUM_ROOT}\" --library \"${DOCVIEW_PDFIUM_LIBRARY}\"
--include-dir \"${DOCVIEW_PDFIUM_INCLUDE_DIR}\" --notice-output \"${_candidate_notice}\"
RESULT_VARIABLE candidate_result OUTPUT_QUIET ERROR_VARIABLE candidate_error)
if(NOT candidate_result EQUAL 0)
message(FATAL_ERROR \"PDFium candidate changed before install: \${candidate_error}\")
endif()")
string(JSON _candidate_files LENGTH "${_candidate_record}" installFiles)
math(EXPR _candidate_last "${_candidate_files} - 1")
foreach(_index RANGE 0 ${_candidate_last})
string(JSON _source GET "${_candidate_record}" installFiles ${_index} source)
string(JSON _destination GET "${_candidate_record}" installFiles ${_index} destination)
get_filename_component(_directory "${_destination}" DIRECTORY)
install(FILES "${DOCVIEW_PDFIUM_ROOT}/${_source}" DESTINATION "${_directory}")
endforeach()
install(FILES "${_candidate_notice}" DESTINATION share/doc/docview/pdfium/supplemental)
install(FILES "${DOCVIEW_PDFIUM_ROOT}/licenses/libcxx-LICENSE.txt"
"${DOCVIEW_PDFIUM_ROOT}/licenses/libcxxabi-LICENSE.txt"
DESTINATION share/doc/docview/pdfium/supplemental)
return()
endif()
string(JSON _notice_count LENGTH "${_notice_manifest}" files)
string(JSON _notice_schema GET "${_notice_manifest}" schemaVersion)
if(NOT _notice_schema EQUAL 1)
message(FATAL_ERROR "Unsupported PDFium supplemental notice schema")
endif()
if(NOT _notice_count EQUAL 2)
message(FATAL_ERROR "Expected the two audited PDFium supplemental notices")
endif()
set(_notice_names)
foreach(_index RANGE 0 1)
string(JSON _name GET "${_notice_manifest}" files ${_index} name)
string(JSON _hash GET "${_notice_manifest}" files ${_index} sha256)
if(NOT _name MATCHES "^(libcxx|libcxxabi)-LICENSE[.]txt$")
message(FATAL_ERROR "Unexpected PDFium supplemental notice filename")
endif()
if(_name IN_LIST _notice_names)
message(FATAL_ERROR "Duplicate PDFium supplemental notice filename")
endif()
list(APPEND _notice_names "${_name}")
file(SHA256 "${_notice_root}/${_name}" _actual)
if(NOT _actual STREQUAL _hash)
message(FATAL_ERROR "PDFium supplemental notice digest mismatch")
endif()
install(FILES "${_notice_root}/${_name}" DESTINATION share/doc/docview/pdfium/supplemental)
endforeach()
install(FILES "${_notice_root}/sources.json" DESTINATION share/doc/docview/pdfium/supplemental)
+255
View File
@@ -0,0 +1,255 @@
# Native script/include inputs: WORKER_EXECUTABLE, RUNTIME_LIBRARIES,
# SEARCH_DIRECTORIES and EXTRA_RUNTIME_LIBRARIES (explicit MSVC redistributables).
# No input is resolved through PATH. CMake's PE scanner searches the importing
# binary's directory, OS directories and the explicit DIRECTORIES list.
cmake_minimum_required(VERSION 3.24)
include("${CMAKE_CURRENT_LIST_DIR}/WriteWindowsRuntimeManifest.cmake")
function(docview_runtime_actual_path input output)
if(NOT IS_ABSOLUTE "${input}" OR NOT EXISTS "${input}" OR IS_SYMLINK "${input}")
message(FATAL_ERROR "Invalid worker runtime source: ${input}")
endif()
set(actual "${input}")
if(WIN32)
# CMake before 3.27 lowercases DLL result names. Recover on-disk casing
# before comparing an imported target against the scanner's result.
get_filename_component(parent "${input}" DIRECTORY)
get_filename_component(name "${input}" NAME)
string(TOLOWER "${name}" wanted)
file(GLOB siblings LIST_DIRECTORIES FALSE "${parent}/*")
set(matches)
foreach(sibling IN LISTS siblings)
get_filename_component(candidate "${sibling}" NAME)
string(TOLOWER "${candidate}" folded)
if(folded STREQUAL wanted)
list(APPEND matches "${sibling}")
endif()
endforeach()
list(LENGTH matches count)
if(NOT count EQUAL 1)
message(FATAL_ERROR "Ambiguous case-insensitive runtime source: ${input}")
endif()
list(GET matches 0 actual)
endif()
set(${output} "${actual}" PARENT_SCOPE)
endfunction()
# Pure package-copy step is independently testable with synthetic MZ fixtures.
# It does not claim that these files passed the native PE dependency scanner.
function(docview_stage_runtime_files executable sources)
docview_runtime_file_info("${executable}" EXE executable_name executable_size)
get_filename_component(directory "${executable}" DIRECTORY)
file(LOCK "${directory}/.docview-worker-runtime.lock" GUARD FUNCTION TIMEOUT 60 RESULT_VARIABLE locked)
if(NOT locked STREQUAL "0")
message(FATAL_ERROR "Cannot lock the worker runtime package directory: ${locked}")
endif()
set(names)
set(paths)
set(digests)
set(sizes)
set(total "${executable_size}")
foreach(input IN LISTS sources)
docview_runtime_actual_path("${input}" source)
docview_runtime_file_info("${source}" DLL name size)
string(TOLOWER "${name}" key)
list(FIND names "${key}" existing)
file(SHA256 "${source}" digest)
if(NOT existing EQUAL -1)
list(GET paths ${existing} previous)
get_filename_component(previous_name "${previous}" NAME)
list(GET digests ${existing} previous_digest)
if(NOT name STREQUAL previous_name OR NOT digest STREQUAL previous_digest)
message(FATAL_ERROR "Conflicting worker runtime DLL sources: ${previous};${source}")
endif()
continue()
endif()
list(APPEND names "${key}")
list(APPEND paths "${source}")
list(APPEND digests "${digest}")
list(APPEND sizes "${size}")
list(LENGTH names count)
math(EXPR total "${total} + ${size}")
if(count GREATER 127 OR total GREATER 536870912)
message(FATAL_ERROR "Worker runtime exceeds 128 files or 512 MiB")
endif()
endforeach()
# Inspect existing names case-insensitively, including on case-sensitive
# build hosts. Never overwrite a different DLL shared by another worker.
file(GLOB existing_entries LIST_DIRECTORIES TRUE "${directory}/*")
set(runtime_files "${executable}")
list(LENGTH paths count)
if(count GREATER 0)
math(EXPR last "${count} - 1")
foreach(index RANGE 0 ${last})
list(GET paths ${index} source)
list(GET names ${index} key)
list(GET digests ${index} digest)
list(GET sizes ${index} size)
get_filename_component(name "${source}" NAME)
set(destination "${directory}/${name}")
foreach(entry IN LISTS existing_entries)
get_filename_component(existing_name "${entry}" NAME)
string(TOLOWER "${existing_name}" existing_key)
if(existing_key STREQUAL key AND NOT existing_name STREQUAL name)
message(FATAL_ERROR "Case-insensitive runtime destination collision: ${name}")
endif()
endforeach()
if(EXISTS "${destination}" OR IS_SYMLINK "${destination}")
docview_runtime_file_info("${destination}" DLL old_name old_size)
file(SHA256 "${destination}" old_digest)
if(NOT old_size EQUAL size OR NOT old_digest STREQUAL digest)
message(FATAL_ERROR "Refusing to replace a different runtime DLL: ${destination}")
endif()
else()
string(RANDOM LENGTH 24 ALPHABET 0123456789abcdef nonce)
set(temporary "${destination}.${nonce}.tmp")
if(EXISTS "${temporary}" OR IS_SYMLINK "${temporary}")
message(FATAL_ERROR "Runtime copy temporary path already exists")
endif()
file(COPY_FILE "${source}" "${temporary}" RESULT copied)
if(NOT copied STREQUAL "0")
file(REMOVE "${temporary}")
message(FATAL_ERROR "Cannot copy runtime DLL: ${copied}")
endif()
file(SHA256 "${temporary}" copied_digest)
file(SIZE "${temporary}" copied_size)
if(NOT copied_digest STREQUAL digest OR NOT copied_size EQUAL size)
file(REMOVE "${temporary}")
message(FATAL_ERROR "Runtime DLL changed while copying: ${source}")
endif()
file(RENAME "${temporary}" "${destination}" NO_REPLACE RESULT renamed)
if(NOT renamed STREQUAL "0")
file(REMOVE "${temporary}")
message(FATAL_ERROR "Cannot publish runtime DLL: ${renamed}")
endif()
# Older CMake on POSIX can publish NO_REPLACE with a hard link
# while leaving the source name behind. Remove only our own
# temporary name after successful publication, preserving the
# destination and NO_REPLACE's collision protection.
file(REMOVE "${temporary}")
if(EXISTS "${temporary}" OR IS_SYMLINK "${temporary}")
message(FATAL_ERROR "Cannot remove the runtime copy temporary path")
endif()
endif()
list(APPEND runtime_files "${destination}")
endforeach()
endif()
docview_write_windows_runtime_manifest("${executable}" "${runtime_files}")
endfunction()
function(docview_regex_case_path path output)
string(REPLACE "\\" "/" normalized "${path}")
string(LENGTH "${normalized}" length)
math(EXPR last "${length} - 1")
set(regex)
set(metacharacters "." "[" "]" "(" ")" "+" "*" "?" "^" "$" "|" "{" "}")
foreach(index RANGE 0 ${last})
string(SUBSTRING "${normalized}" ${index} 1 character)
if(character MATCHES "[A-Za-z]")
string(TOUPPER "${character}" upper)
string(TOLOWER "${character}" lower)
string(APPEND regex "[${upper}${lower}]")
elseif(character IN_LIST metacharacters)
string(APPEND regex "\\${character}")
else()
string(APPEND regex "${character}")
endif()
endforeach()
set(${output} "${regex}" PARENT_SCOPE)
endfunction()
function(docview_is_windows_os_runtime path system_root output)
string(REPLACE "\\" "/" normalized "${path}")
string(REPLACE "\\" "/" root "${system_root}")
string(REGEX REPLACE "/+$" "" root "${root}")
string(TOLOWER "${normalized}" normalized)
string(TOLOWER "${root}/" root)
string(FIND "${normalized}" "${root}" prefix)
set(result FALSE)
if(prefix EQUAL 0)
string(LENGTH "${root}" length)
string(SUBSTRING "${normalized}" ${length} -1 relative)
if(relative MATCHES "^(system32|syswow64)/" OR relative MATCHES "^[^/]+[.]dll$")
set(result TRUE)
endif()
endif()
set(${output} "${result}" PARENT_SCOPE)
endfunction()
function(docview_stage_windows_worker)
if(NOT CMAKE_HOST_WIN32)
message(FATAL_ERROR "Native Windows PE runtime dependency scanning requires a Windows host")
endif()
docview_runtime_file_info("${WORKER_EXECUTABLE}" EXE executable_name executable_size)
file(TO_CMAKE_PATH "$ENV{SystemRoot}" system_root)
if(NOT IS_ABSOLUTE "${system_root}" OR NOT IS_DIRECTORY "${system_root}/System32")
message(FATAL_ERROR "Cannot locate the Windows OS runtime directories")
endif()
docview_regex_case_path("${system_root}" os_regex)
set(os_exclusions "^${os_regex}/[Ss][Yy][Ss][Tt][Ee][Mm]32/"
"^${os_regex}/[Ss][Yy][Ss][Ww][Oo][Ww]64/"
"^${os_regex}/[^/]+[.][Dd][Ll][Ll]$")
set(explicit_libraries)
set(directories ${SEARCH_DIRECTORIES})
foreach(input IN LISTS RUNTIME_LIBRARIES EXTRA_RUNTIME_LIBRARIES)
docview_runtime_actual_path("${input}" source)
docview_runtime_file_info("${source}" DLL name size)
string(TOLOWER "${name}" folded)
if(folded MATCHES "^(api|ext)-ms-win-.*[.]dll$")
message(FATAL_ERROR "API-set DLLs belong to Windows and must not be copied: ${name}")
endif()
docview_is_windows_os_runtime("${source}" "${system_root}" os_file)
if(os_file)
message(FATAL_ERROR "Do not package DLLs from Windows OS directories: ${source}")
endif()
list(APPEND explicit_libraries "${source}")
get_filename_component(parent "${source}" DIRECTORY)
list(APPEND directories "${parent}")
endforeach()
list(REMOVE_DUPLICATES explicit_libraries)
list(REMOVE_DUPLICATES directories)
foreach(directory IN LISTS directories)
if(NOT IS_ABSOLUTE "${directory}" OR NOT IS_DIRECTORY "${directory}")
message(FATAL_ERROR "DLL search directories must be explicit existing absolute paths: ${directory}")
endif()
endforeach()
set(CMAKE_GET_RUNTIME_DEPENDENCIES_PLATFORM "windows+pe")
if(DEFINED CMAKE_GET_RUNTIME_DEPENDENCIES_COMMAND AND
(NOT IS_ABSOLUTE "${CMAKE_GET_RUNTIME_DEPENDENCIES_COMMAND}" OR
NOT EXISTS "${CMAKE_GET_RUNTIME_DEPENDENCIES_COMMAND}"))
message(FATAL_ERROR "The PE inspection tool must have an existing absolute path")
endif()
file(GET_RUNTIME_DEPENDENCIES
EXECUTABLES "${WORKER_EXECUTABLE}"
LIBRARIES ${explicit_libraries}
DIRECTORIES ${directories}
RESOLVED_DEPENDENCIES_VAR resolved
UNRESOLVED_DEPENDENCIES_VAR unresolved
CONFLICTING_DEPENDENCIES_PREFIX conflicting
PRE_EXCLUDE_REGEXES "^api-ms-win-.*[.]dll$" "^ext-ms-win-.*[.]dll$"
POST_EXCLUDE_REGEXES ${os_exclusions})
if(unresolved)
message(FATAL_ERROR "Unresolved worker runtime DLL dependencies: ${unresolved}")
endif()
if(conflicting_FILENAMES)
message(FATAL_ERROR "Conflicting worker runtime DLL dependencies: ${conflicting_FILENAMES}")
endif()
set(package_dependencies)
foreach(dependency IN LISTS resolved)
docview_runtime_actual_path("${dependency}" dependency)
docview_is_windows_os_runtime("${dependency}" "${system_root}" os_file)
get_filename_component(name "${dependency}" NAME)
string(TOLOWER "${name}" folded)
if(NOT os_file AND NOT folded MATCHES "^(api|ext)-ms-win-.*[.]dll$")
list(APPEND package_dependencies "${dependency}")
endif()
endforeach()
# Explicit redistributables are included even if a matching import resolved
# to a Windows OS DLL that the scanner correctly excluded.
set(package_sources ${explicit_libraries} ${package_dependencies})
docview_stage_runtime_files("${WORKER_EXECUTABLE}" "${package_sources}")
endfunction()
if(DEFINED WORKER_EXECUTABLE)
docview_stage_windows_worker()
endif()
+61
View File
@@ -0,0 +1,61 @@
# Windows SDK tools and matching Qt binaries are required on the build host.
include_guard(GLOBAL)
if(NOT WIN32)
return()
endif()
set(DOCVIEW_EXTRA_WORKER_RUNTIME_DLLS "" CACHE STRING
"Explicit additional redistributable DLLs approved for worker packaging")
find_program(DOCVIEW_DUMPBIN_EXECUTABLE NAMES dumpbin REQUIRED)
find_program(DOCVIEW_WINDEPLOYQT_EXECUTABLE NAMES windeployqt
HINTS "${Qt6_DIR}/../../../bin" REQUIRED)
function(docview_configure_windows_worker target)
set(script "${CMAKE_CURRENT_BINARY_DIR}/stage-${target}-$<CONFIG>.cmake")
file(GENERATE OUTPUT "${script}" CONTENT "
set(WORKER_EXECUTABLE [==[$<TARGET_FILE:${target}>]==])
set(RUNTIME_LIBRARIES [==[$<TARGET_RUNTIME_DLLS:${target}>]==])
set(SEARCH_DIRECTORIES [==[$<TARGET_FILE_DIR:Qt6::Core>;$<TARGET_FILE_DIR:${DOCVIEW_ZIP_TARGET}>;$<TARGET_FILE_DIR:qpdf::libqpdf>;${DOCVIEW_PDFIUM_ROOT}/bin]==])
set(EXTRA_RUNTIME_LIBRARIES [==[${DOCVIEW_EXTRA_WORKER_RUNTIME_DLLS}]==])
set(CMAKE_GET_RUNTIME_DEPENDENCIES_TOOL dumpbin)
set(CMAKE_GET_RUNTIME_DEPENDENCIES_COMMAND [==[${DOCVIEW_DUMPBIN_EXECUTABLE}]==])
include([==[${CMAKE_CURRENT_FUNCTION_LIST_DIR}/StageWindowsWorker.cmake]==])
")
add_custom_command(TARGET ${target} POST_BUILD
COMMAND "${CMAKE_COMMAND}" -P "${script}" VERBATIM
COMMENT "Collecting and hashing the restricted runtime for ${target}")
endfunction()
function(docview_install_windows_worker target)
set(script "${CMAKE_CURRENT_BINARY_DIR}/install-${target}-$<CONFIG>.cmake")
file(GENERATE OUTPUT "${script}" CONTENT "
set(WORKER_EXECUTABLE [==[$<TARGET_FILE:${target}>]==])
set(WORKER_INSTALL_DIRECTORY \"\${CMAKE_INSTALL_PREFIX}/bin\")
set(WORKER_VERIFY_ONLY FALSE)
include([==[${CMAKE_CURRENT_FUNCTION_LIST_DIR}/InstallWindowsWorker.cmake]==])
")
install(SCRIPT "${script}")
endfunction()
function(docview_install_windows_gui target)
set(script "${CMAKE_CURRENT_BINARY_DIR}/deploy-${target}-$<CONFIG>.cmake")
file(GENERATE OUTPUT "${script}" CONTENT "
set(destination \"\$ENV{DESTDIR}\${CMAKE_INSTALL_PREFIX}/bin\")
execute_process(COMMAND [==[${DOCVIEW_WINDEPLOYQT_EXECUTABLE}]==]
--$<IF:$<CONFIG:Debug>,debug,release> --no-compiler-runtime --nopatchqt
--dir \"\${destination}\" --plugindir \"\${destination}/plugins\"
--qml-deploy-dir \"\${destination}/qml\" --translationdir \"\${destination}/translations\"
--qmldir [==[${CMAKE_SOURCE_DIR}/qml]==] \"\${destination}/$<TARGET_FILE_NAME:${target}>\"
RESULT_VARIABLE deployed)
if(NOT deployed EQUAL 0)
message(FATAL_ERROR \"windeployqt failed: \${deployed}\")
endif()
file(WRITE \"\${destination}/qt.conf\" \"[Paths]\\nPrefix=.\\nPlugins=plugins\\nQmlImports=qml\\nLibraryExecutables=.\\nData=.\\nTranslations=translations\\n\")
# windeployqt must not patch or replace any hashed worker dependency.
set(WORKER_VERIFY_ONLY TRUE)
foreach(name docview-pdf-worker.exe docview-archive-worker.exe)
set(WORKER_EXECUTABLE \"\${destination}/\${name}\")
include([==[${CMAKE_CURRENT_FUNCTION_LIST_DIR}/InstallWindowsWorker.cmake]==])
endforeach()
")
install(SCRIPT "${script}")
endfunction()
+123
View File
@@ -0,0 +1,123 @@
# Script API: -DWORKER_EXECUTABLE=<absolute exe> -DRUNTIME_FILES=<absolute list>
# Include API: docview_write_windows_runtime_manifest(executable "${files}").
# This writer validates the package inventory, not the PE import table. The
# native dependency scanner is StageWindowsWorker.cmake.
cmake_minimum_required(VERSION 3.24)
include_guard(GLOBAL)
function(docview_runtime_file_info path kind out_name out_size)
if(NOT IS_ABSOLUTE "${path}" OR NOT EXISTS "${path}" OR IS_DIRECTORY "${path}" OR IS_SYMLINK "${path}")
message(FATAL_ERROR "Worker runtime requires an absolute regular file: ${path}")
endif()
get_filename_component(name "${path}" NAME)
string(LENGTH "${name}" length)
string(TOLOWER "${name}" folded)
if(length GREATER 128 OR NOT name MATCHES "^[A-Za-z0-9][A-Za-z0-9._-]*$" OR
name MATCHES "[.]$" OR folded MATCHES "^(con|prn|aux|nul|com[1-9]|lpt[1-9])([.]|$)")
message(FATAL_ERROR "Invalid Windows runtime filename: ${name}")
endif()
if((kind STREQUAL "EXE" AND NOT folded MATCHES "[.]exe$") OR
(kind STREQUAL "DLL" AND NOT folded MATCHES "[.]dll$"))
message(FATAL_ERROR "Only the worker executable and DLLs may enter its runtime: ${name}")
endif()
file(SIZE "${path}" size)
if(size LESS 2 OR size GREATER 268435456)
message(FATAL_ERROR "Worker runtime file exceeds the 256 MiB limit or is empty: ${name}")
endif()
file(READ "${path}" signature LIMIT 2 HEX)
if(NOT signature STREQUAL "4d5a")
message(FATAL_ERROR "Worker runtime file has no MZ signature: ${name}")
endif()
set(${out_name} "${name}" PARENT_SCOPE)
set(${out_size} "${size}" PARENT_SCOPE)
endfunction()
function(docview_write_windows_runtime_manifest executable files)
docview_runtime_file_info("${executable}" EXE executable_name executable_size)
file(REAL_PATH "${executable}" executable_real)
get_filename_component(directory "${executable_real}" DIRECTORY)
if(WIN32)
string(TOLOWER "${directory}" directory)
endif()
list(LENGTH files count)
if(count LESS 1 OR count GREATER 128)
message(FATAL_ERROR "Worker runtime inventory must contain 1..128 files")
endif()
set(seen)
set(total 0)
set(has_executable FALSE)
# Check every limit before hashing potentially large files or changing an
# existing manifest. File order does not affect the serialized inventory.
foreach(path IN LISTS files)
get_filename_component(name "${path}" NAME)
if(name STREQUAL executable_name)
set(kind EXE)
else()
set(kind DLL)
endif()
docview_runtime_file_info("${path}" "${kind}" name size)
file(REAL_PATH "${path}" real)
get_filename_component(parent "${real}" DIRECTORY)
if(WIN32)
string(TOLOWER "${parent}" parent)
endif()
if(NOT parent STREQUAL directory)
message(FATAL_ERROR "Manifest entries must already be beside the worker: ${name}")
endif()
string(TOLOWER "${name}" key)
if(key IN_LIST seen)
message(FATAL_ERROR "Case-insensitive duplicate worker runtime filename: ${name}")
endif()
list(APPEND seen "${key}")
if(name STREQUAL executable_name)
if(NOT real STREQUAL executable_real)
message(FATAL_ERROR "Manifest executable does not match its worker")
endif()
set(has_executable TRUE)
endif()
math(EXPR total "${total} + ${size}")
if(total GREATER 536870912)
message(FATAL_ERROR "Worker runtime exceeds the 512 MiB aggregate limit")
endif()
endforeach()
if(NOT has_executable)
message(FATAL_ERROR "Worker runtime manifest must include its executable")
endif()
list(SORT files CASE INSENSITIVE)
set(entries)
foreach(path IN LISTS files)
get_filename_component(name "${path}" NAME)
file(SIZE "${path}" size)
file(SHA256 "${path}" digest)
string(TOLOWER "${digest}" digest)
list(APPEND entries " {\"path\":\"${name}\",\"sha256\":\"${digest}\",\"size\":${size}}")
endforeach()
list(JOIN entries ",\n" entries_json)
set(json "{\n \"schemaVersion\":1,\n \"executable\":\"${executable_name}\",\n \"files\":[\n${entries_json}\n ]\n}\n")
string(LENGTH "${json}" json_size)
if(json_size GREATER 131072)
message(FATAL_ERROR "Worker runtime manifest exceeds its 128 KiB limit")
endif()
set(output "${executable}.runtime.json")
if(IS_SYMLINK "${output}" OR IS_DIRECTORY "${output}")
message(FATAL_ERROR "Refusing a non-regular worker runtime manifest destination")
endif()
string(RANDOM LENGTH 24 ALPHABET 0123456789abcdef nonce)
set(temporary "${output}.${nonce}.tmp")
if(EXISTS "${temporary}" OR IS_SYMLINK "${temporary}")
message(FATAL_ERROR "Worker runtime manifest temporary path already exists")
endif()
file(WRITE "${temporary}" "${json}")
file(RENAME "${temporary}" "${output}" RESULT renamed)
if(NOT renamed STREQUAL "0")
file(REMOVE "${temporary}")
message(FATAL_ERROR "Cannot atomically publish worker runtime manifest: ${renamed}")
endif()
endfunction()
if(CMAKE_SCRIPT_MODE_FILE STREQUAL CMAKE_CURRENT_LIST_FILE)
if(NOT DEFINED WORKER_EXECUTABLE OR NOT DEFINED RUNTIME_FILES)
message(FATAL_ERROR "WORKER_EXECUTABLE and RUNTIME_FILES are required")
endif()
docview_write_windows_runtime_manifest("${WORKER_EXECUTABLE}" "${RUNTIME_FILES}")
endif()
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env python3
"""Fetch the hash-pinned non-V8 PDFium dependency for Linux or Windows x64."""
from pathlib import Path
import argparse
import hashlib
import json
import platform
import tarfile
import tempfile
import urllib.request
root = Path(__file__).resolve().parent.parent
lock = json.loads((root / 'cmake/pdfium.lock.json').read_text())
host = {'Linux': 'linux-x64', 'Windows': 'windows-x64'}.get(platform.system())
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--platform', choices=['linux-x64', 'windows-x64'], default=host)
parser.add_argument('--destination', type=Path)
args = parser.parse_args()
if args.platform is None or (args.platform == host and platform.machine() not in ('x86_64', 'AMD64')):
raise SystemExit('A supported x64 platform is required; specify --platform for a cross-platform download.')
prefix = 'windowsX64' if args.platform == 'windows-x64' else 'linuxX64'
destination = args.destination or root / '.deps' / ('pdfium' if args.platform == host else 'pdfium-' + args.platform.split('-')[0])
with tempfile.TemporaryDirectory(prefix='docview-pdfium-') as temporary:
archive = Path(temporary) / 'pdfium.tgz'
urllib.request.urlretrieve(lock[prefix + 'Archive'], archive)
if hashlib.sha256(archive.read_bytes()).hexdigest() != lock[prefix + 'Sha256']:
raise SystemExit('PDFium archive hash mismatch; dependency not installed.')
destination.mkdir(parents=True, exist_ok=True)
with tarfile.open(archive) as bundle:
bundle.extractall(destination, filter='data')
print(f'Installed pinned PDFium {lock["version"]} in {destination}')
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+12
View File
@@ -0,0 +1,12 @@
{
"schemaVersion": 1,
"candidateId": "reviewed-v2",
"sourceRevision": "a5a7089234f121990b336b3841008009dca143bf",
"librarySha256": "cb049fe434f4c911b5eb047c0aca572d4dd9f5e405dde329b76c05a6aa771403",
"buildInfoSha256": "6fbb6f24a7ca241150f8abf8d9ad031c76d9cfffc809026a61068c357d47ae18",
"anchorRecordPath": "provenance/master6.json",
"anchorRecordSha256": "80af72b06d5c9a6a3ab8b9311373e6a23e8df28668ce575e1124350e69aa160b",
"patchSha256": "470ca6dd5c83e06ee82c788d1e34b19766f7c592473b80607f0d7bb09d761616",
"gnArgsSha256": "5a2e04e1c0bb3eb05dc415dfa005a917804be8f63ade2365016c148e8efd8197",
"supplementalSourceSha256": "97d75f0b50e5ad8114229ec4e5cece147272563dfed4fd434087b70d871a114a"
}
+28
View File
@@ -0,0 +1,28 @@
{
"version": "155.0.8057.0",
"branch": "chromium/8057",
"upstreamCommit": "a5a7089234f121990b336b3841008009dca143bf",
"upstream": "https://pdfium.googlesource.com/pdfium/+/a5a7089234f121990b336b3841008009dca143bf",
"binaryProvider": "https://github.com/bblanchon/pdfium-binaries",
"linuxX64Archive": "https://github.com/bblanchon/pdfium-binaries/releases/download/chromium%2F8057/pdfium-linux-x64.tgz",
"linuxX64Sha256": "7788fa57a2996ebd21afc4090eb0a42b9f95ef3a72e7ef4c0756f1ea703c0415",
"windowsX64Archive": "https://github.com/bblanchon/pdfium-binaries/releases/download/chromium%2F8057/pdfium-win-x64.tgz",
"windowsX64Sha256": "e307d519e42f2e69b1b531f0c2a32dffcdf3891ec0eba60328ba51a57cec01ed",
"windowsBuildOptions": {
"pdf_enable_v8": false,
"pdf_enable_xfa": false,
"target_cpu": "x64",
"target_os": "win"
},
"buildOptions": {
"pdf_enable_v8": false,
"pdf_enable_xfa": false,
"pdf_is_standalone": true,
"is_debug": false,
"pdf_use_partition_alloc": false,
"target_cpu": "x64",
"target_os": "linux"
},
"licenses": [".deps/pdfium/LICENSE", ".deps/pdfium/licenses/"],
"apiNotes": "Structure tree, MCID and page-object access use experimental public APIs, compiled against these exact headers. No private Qt or PDFium API is used."
}
+89
View File
@@ -0,0 +1,89 @@
# 原設計との受入照合
2026-09-20。基準は変更していない [設計書11件](design/00-guide.md)。個別試験の成功と製品全体の完成を区別する。実行結果とビルドの対応は [検証記録](VALIDATION.md) に置く。
## 要件の追跡
2026-09-20追記:[第2PDFium候補](../tests/results/pdfium-intent-context/README.md)を専用ビルドとUbuntu validation4へ統合した。旧試験の `shading-pattern-inherit` 8点の期待値は誤りだったため、旧648点の成功をその規則の根拠にしない。訂正版600点・元48点・追加324点はArch・Ubuntuで成功し、新ビルド各24群、上流1,979試験も成功した。SoftMask/tiling専用試験、Ubuntu配布境界13試験、2回のdeb生成一致、SDKなし既存VMの12条件起動・削除、PDF性能4条件と約1GBの負荷試験まで実施済み。以下の第1候補・元依存版の未完了記述は当時の履歴である。T01/G-RENDER全体の承認と対象実機・Windows・公開配布の最終受入は未完了のまま。
2026-09-20追記:[ICC変換指定の修正候補](../tests/results/pdfium-intent-build/README.md)では、固定ソースへのパッチを再適用確認し、隔離ビルドの648点・上流1,976試験・DocView全22群が成功した。製品依存とUbuntuパッケージは未変更で、T01/G-RENDERの完成判定は進めない。比較先のshading pattern RI指定の差、候補のUbuntu・配布統合、SoftMask/tiling専用の画素試験、人による承認は残る。
2026-09-20追記:[ICC CMYK追加比較](../tests/results/cmyk-lut/README.md)で、文書の相対的測色指定に対して固定PDFiumが知覚的変換を使う差を確認した。Arch・Ubuntu各48点中30点が許容差を超えた。原因は照合済みだが、T01/G-RENDERのこの色一致条件は未達である。変換指定への対応または明示的な互換性判断、検出可能な制約の通知を未完了事項に追加する。
2026-09-20追記:[Qt告知補足版](../tests/results/qt-notice-supplement/README.md)はWebM/WebPのPatent本文を固定ソースと照合してUbuntu debへ追加した。梱包8試験×2環境、deb再現性と全2,016ファイル検証、SDKなしVMでの起動12条件・install/remove/purgeが成功。本体と同梱ランタイムは同一である。全Chromium告知・対応ソースの完成、Windows/物理環境/人による描画承認の受入を済ませたとはしない。
2026-09-20追記:[実ファイル選択の追加試験](../tests/results/portal/README.md)で特殊なファイル名のHTMLが読込中になる問題を発見・修正した。新しい本体ではArch・Ubuntuの全22群が成功。以下の過去の性能・GPU・IME等の証拠は固定した旧版の範囲に限り、現行本体への再実行は別に扱う。
修正後の[性能10条件と追加負荷](../tests/results/portal/performance-suite/README.md)も再実行した。新規プロセス300回・同一プロセス内300回・移動2,500操作、約1GB/5,000ページPDFが成功し、測定した参考予算の超過は0だった。基準機・対象OS・実フレーム提示の受入条件は変更しない。
| 原要件 | 現在の証拠 | 最終判定に残るもの |
|---|---|---|
| R01 PDF描画、R14 原本を編集しない | 本番の隔離PDFWorker、独立レンダラー比較、原本ハッシュ、注釈・フォーム・日本語・幾何試験 | G-RENDERの承認画像、対象OS、代表制作ソフトの互換性判断 |
| R02 HTML、R03 HTML ZIP、R04 EPUB | 本番WebEngine/ArchiveWorker、相対資源・spine・RTL・縦書き・固定見開き・入口選択試験 | 両OSのG-WEB・実ディスプレイ。遅延レイアウトと実読取量の回帰はLinuxで確認 |
| R05 キー操作、R08 最小UI、R15 GUIのTUI風操作 | InputRouter、設定、QMLの実キーイベント試験、パネル表示/フォーカス試験 | X11 US/JP XKBとX11/Wayland IBus/Mozcは追加検証済み。物理入力・Windows IME・対象ディスプレイ |
| R06 見出し、R07 目次 | PDF構造/内部宛先outline代替・Form所有元、DOM/ARIA、EPUB nav/NCX、抽出とGUI移動、索引loadingの待機表示・失敗した見出し章境界の破棄 | 特殊構造の既知制約、対象OSのG-HEADINGS |
| R09 拡張性 | [接続契約の採用記録](adapter-contract.md)、能力スキーマ、操作と解析プロセスの分離 | 形式追加の実装時は同契約と取消・位置のレビューを適用 |
| R10 ファイル設定 | TOMLの有限スキーマ、一括適用、未知キー/型/衝突/不正時の保持、再読込試験、日本語の行・原因案内 | 対象OSの実入力系 |
| R11 書籍の応答性 | 冷起動/再open各30回・各125操作、500ページ/章、約1GB/5,000ページ、cache上限・旧応答破棄・メモリ圧迫対処 | 基準機・GPU/60Hzと長時間負荷 |
| R12 Linux/Windows、R13 OS配置 | ArchとUbuntu 24.04専用VMのX11/Wayland、ArchのAMD OpenGL、XDG・専用保存先、Windows Known Folders/LPAC/pipe等の実装 | Windows nativeビルドと試験、対象OSのGPU/IME、製品配布の実機検証 |
補助機能T-P01〜04、安全試験T-S01〜05、通常受入T01〜21の個別範囲は [検証記録の対応表](VALIDATION.md#受入idとの対応) を参照する。外部から追加環境は提供されていないが、このPC内にUbuntu 24.04専用KVM環境を作り、全22群・Wayland GUI・配置後起動を[検証した](../tests/results/ui-final/ubuntu/README.md)。生成文書を使うことは利用者が指定済みであり、実書籍がないことを理由にローカル試験を止めない。
## 今回の照合で見つけたローカル項目
| 項目 | 原文 | 対応・必要な証拠 |
|---|---|---|
| 現在位置と目次選択を別表示 | [04 §2.2](design/04-ui-navigation.md#22-閲覧画面) | 現在節の記号と選択枠を分け、選択のみで本文が動かないGUI試験。折りたたみ・文書切替・古い応答も確認する |
| 現在PDFページとズーム中心 | [04 §5.1](design/04-ui-navigation.md#51-pdf) | 中央線のページ/余白の距離/同距離の前ページと、保存用先頭位置を分離。ズーム中心のPDF座標保持を検証する |
| ページ精度・回転見出しの反復移動 | [04 §5.4](design/04-ui-navigation.md#54-見出し移動の共通定義) | `navigationY`で表示上の前後を判定。ページ先頭を前操作で再選択しないこと、回転時の実GUI移動を試験 |
| ICC入力 | [G-RENDER](design/09-decisions-roadmap.md#4-実装前の技術ゲート) | 自作RGB ICCの54点と独立比較。元依存版のCMYK CLUTで48点中30点の差を再現し、第2候補で48点・訂正済み状態600点・SoftMask/tiling等324点が各OSで成功。校正済み印刷色や人による描画承認は未完了 |
| Web rendererの応答監視 | [07 §3.2](design/07-security-errors.md#32-上限の初期提案) | RSSだけでなく有限deadlineの応答監視、実renderer停止、取消/世代/対象identity/2viewを検証する |
| メモリ圧迫の段階的縮退 | [02 資源管理](design/02-architecture.md#キャッシュと資源管理)、[06 設定](design/06-config-storage.md#設定項目) | OSの空き監視、先読み停止→不可視cache回収→解像度抑制通知→処理停止と回復。[契約](memory-pressure.md)と注入試験 |
| 依存告知・配布アーカイブ | [09 配布](design/09-decisions-roadmap.md) | 同梱とシステム依存、実版・由来・license・対応sourceを区別したmanifest。再現可能な開発版パッケージ・サイズ・解凍起動の証跡 |
| XHTMLの章内見出しと目次 | [03 EPUB](design/03-formats.md)、[04 見出し移動](design/04-ui-navigation.md#54-見出し移動の共通定義) | 実XHTMLでnative見出しの小文字タグ名と名前空間付きepub:typeを扱う。著者目次と章内見出しを区別し、測定時も実索引の種類・件数を記録する |
| 原本の識別と位置保存 | [06 読書状態と履歴](design/06-config-storage.md#読書状態と履歴) | 全形式のopen時identityを保存時に照合し、閲覧中に置換された新原本へ旧位置を転用しない。通常openでの復元不能通知も実ファイルで検査 |
| 目次の準備中表示 | [02 開く処理](design/02-architecture.md) | DOM索引が空でも能力loadingなら要求を保持し、読み込み中のパネルを表示。索引確定後の表示までGUIで検査 |
| PDF見出し代替の内部宛先 | [04 §5.1](design/04-ui-navigation.md#51-pdf) | 外部・禁止アクションを代替見出し候補から除外。外部だけの場合の能力と、混在時の次/前移動をGUIで検査 |
| Webリンクと章境界の取消 | [04 入力・見出し移動](design/04-ui-navigation.md) | EscでDOMリンク選択とfocusを解除。成立した章移動にだけ最初/最後の見出し指示を付け、失敗後の通常章移動へ残さない |
| PDF情報の省略通知 | [03 PDF](design/03-formats.md)、[07 エラー](design/07-security-errors.md) | 注釈本文長・ページ内件数・CBOR情報予算超過を通知し、取得済みprefixを保持。本文省略でも注釈を一覧から消さない |
| 一時入力とダイアログの焦点復帰 | [04 入力状態](design/04-ui-navigation.md) | 目次・ページ一覧からのコマンド取消、help/file dialog終了後は有効な元領域へ戻す。明示的な本文移動と非表示領域へのfallbackを検査 |
| Web倍率の表示と位置保存 | [04 ステータス](design/04-ui-navigation.md)、[06 状態](design/06-config-storage.md) | HTML/ZIP/EPUBの倍率を表示・保存・再復元。fixedはfitの基準と論理倍率を分け、25〜500%の境界を検査 |
| 開く操作の失敗案内 | [04 失敗表示](design/04-ui-navigation.md) | 失敗した対象名・有限の分類・対処を表示し、表示中の旧文書と区別して文書情報へ試行内容を保持する |
| 日本語初期表示と翻訳資源 | [01 対象環境](design/01-requirements.md) | tr/qsTrのTS抽出・QM埋込・QTranslatorとQt標準部品の日本語資源を接続。英語localeの実Qt Quick Dialogを検査。OSネイティブdialogはOS言語に従う |
この表のうちXHTML修正までの履歴はLinux統合19試験群(2026-09-19、162.00秒)と、その時点のPDFWorkerに対応する6組の比較で確認した。後から加えた原本同一性・目次待機・PDF候補・Web取消等は[追加回帰](../tests/results/completion-regressions/controller-state.md)で確認した。翻訳試験を含む[前回20試験群](../tests/results/completion-final/ctest/tests.xml)はすべて成功した(165.61秒)。[描画6系列・実時間watchdog・再現可能な開発パッケージと6条件起動](../tests/results/completion-final/README.md)も再検証した。T20の[10条件と負荷の再測定](../tests/results/completion-final/performance-summary.md)も完了し、[検証記録](VALIDATION.md)の現行版と履歴の区別に従う。旧結果を現行バイナリーへ読み替えない。
今回のUI追加修正は[最新の22群のX11統合](../tests/results/ui-final/README.md)で検証した。Waylandの6条件起動と全GUIも成功した。見開きresizeの初回2失敗は試験側の確定待ちを修正し、製品の描画処理は変更していない。修正前の本体の[性能10条件と操作負荷](../tests/results/ui-final/performance-summary.md)、[約1GB・5,000ページ](../tests/results/ui-final/stress/README.md)も成功した。以下の20群・従来性能測定は修正前に固定したビルドの結果であり、最新本体へ読み替えない。
## 追加測定と個別の検証結果
1. **T20の測定範囲** — [08 §3.3](design/08-performance-tests.md#33-再現可能な測定手順) は開始時・最大・文書close後の本体/プロセス群RSS、同一文書10開閉、見出し/章の固定100操作、連続scrollフレーム間隔、描画待ち数を要求する。測定経路を拡充し、アプリキャッシュなし30回、同一プロセス30回、29同条件開閉と最後のclose、各125操作系列、240scroll、描画待ち/旧要求破棄を記録する。別の固定操作列で長押し・遠距離ジャンプ・resize・異文書切替・取消を確認する。実scanoutとの結合と基準機判定は未実施。
2. **遅延フォント・画像による再配置** — [03 EPUB](design/03-formats.md) の「レイアウト確定後に同じ論理位置へ再配置」について、load成功時/設定時/resizeの一度の復元後にフォントが届く場合を実DOMで再現し、位置保持と取消を確認する。遅延資源の実読込み、横/縦の文字位置保持、新しい移動を優先する回帰を実装した。連続scroll、native anchoring、poll先行、クリック後の画像到着も追加し、Web全34ケースが成功した。
3. **圧縮率の実入力量** — [07 §3.2](design/07-security-errors.md#32-上限の初期提案) の200倍判定は入力消費量/出力量を指定している。ZIP索引のcompressed sizeだけの判定を補い、source callbackの実read量を各展開に計数する修正を実装した。5圧縮方式のpadding資料、正規6方式、本番sandbox workerを含むArchive全76ケースが成功した。実出力256MiB/文書2GiBの別上限があることを、この比率の検証の代用にしない。
4. **メモリ圧迫** — 検出/遷移27ケース、Canvas21ケースとDPR2、GUIでの段階縮退・停止・回復・原本保持を検証。実RAMを枯渇させず、同じ本番経路へ有限sampleを注入した。
5. **初期画面サイズ** — 配置後の実測で200%時の画面超過を検出し、利用可能な画面へ収める修正を実施。配置後/別展開先の6条件で寸法と表示を確認した。
6. **XHTMLの見出し** — 小型EPUBの性能試験を契機に、HTMLとXHTMLのタグ名表現の違いでnative見出しが欠落する問題を実DOMで再現した。見出しと目次を修正し、計測側の著者目次による代用も除いた。[修正前の失敗記録](../tests/results/performance-before-xhtml-fix/README.md)は保持し、旧EPUBの見出し系列は最終版の証拠に使わない。
7. **章末の重複移動先** — 章末で同じスクロール位置へ収まる2見出しを交互に再選択する問題を横書き・縦書きで再現した。実移動がない候補をまとめ、EPUBの次章境界へ進めるよう修正した。元の500章資料を保持して章を跨ぐ往復を検証し、性能用には全13見出しの移動先が異なる別版を生成した。[回帰記録](../tests/results/xhtml-headings/README.md)
8. **PDFiumの追加告知** — 固定providerの収集処理と実libraryを調べ、libc++/libc++abiの2告知が従来の15件に含まれないことを確認した。固定ソースの全文と出典をinstallへ追加し、版・library・本文の不一致を拒否する。[新パッケージ](../tests/results/linux-development-package/README.md)は553ファイルを照合し、独立生成2回のbyte一致・6条件の別展開先起動を確認。packaging 12件とprovenance 23件の限定試験も成功した。この告知追加時点では全3実行ファイルとPDFiumのバイト列は従来版と同一だった。後続の製品修正後のバイナリー一致を示す記録ではない。[現行パッケージ](../tests/results/completion-final/linux-development-package/README.md)は別のhashで2回生成一致・6条件の起動を再確認し、[由来台帳](../tests/results/completion-final/dependency-provenance/README.md)も更新した。
9. **高解像度画像・高密度ベクターの取消** — [追加GUI試験](../tests/results/heavy-pdf/README.md)では600dpi相当のRGB画像と60,000個の半透明パスを生成し、独立復号と実ウィンドウ画素を照合した。実open/render RPCが未完の状態で取消・文書切替を行い、原本不変、旧session回収、通常解像度とGUI応答の維持をArch/Ubuntuで確認した。単一生成資料での機能検証であり、長時間・基準機の性能受入ではない。
10. **実IMEと日英配列** — [Ubuntu X11のIBus/Mozc試験](../tests/results/ime/README.md)で、検索・コマンド入力の変換確定、変換候補/未確定入力のEsc取消、j/k・数字の入力保護、通常操作への復帰を検証した。US pc105/JP jp106のXKBとXTestキー番号で記号操作を確認。[WaylandのQt+IBus D-Bus経路](../tests/results/ime/WAYLAND.md)も実変換と入力保護が成功。物理キーボード・Windowsは未実施。
11. **固定PDFiumのソースと配布ヘッダー** — [Git snapshot収集](../tests/results/source-archives/PDFIUM.md)で、本体と27依存・追加gitlinkの計29件を保管。Git objectと全アーカイブ内容を検査し、先行原文31件・公開ヘッダー24件・告知15件が一致した。Linux3件/Windows4件のproviderパッチは専用コピーに適用成功。ビルドの再現、Windows実行、全ビルド入力の回収とは区別する。
12. **Ubuntu SDK内のChromium告知** — [SBOM本文抽出](../tests/results/source-archives/QT-SDK-NOTICES.md)で、129entry・105本文をQt source原文と照合した。元SDK archiveのファイルhashをSBOM67件・Ubuntu実行時83件と一致確認。元SBOMの参照ID不整合1件と本文なし2部品を記録し、生成器のskipも保持した。SDKの全告知やArchの実GN構成の完全性を示すものではない。
13. **Ubuntu用debの配置と回収** — [ローカル検証パッケージ](../tests/results/ubuntu-package/README.md)は2回生成でbyte一致し、全2,009ファイルを照合した。元SDK・build/installを隠した通常ユーザーでX11/Waylandの12条件がReadyとなり、install/reinstall/remove/purge、AppArmorの登録・解除、設定と文書の保持probeが成功した。さらに[新しいUbuntu OS](../tests/results/ubuntu-package/clean-vm/README.md)でも宣言依存の解決、12条件起動、remove/purgeが成功した。公開配布条件と実機desktopの受入は残る。
## 外部条件を要する受入
UbuntuのOSファイル選択は、[実XDG/GTK portal](../tests/results/portal/README.md)のX11/Wayland計10ケースで追加検証した。生成資料の日本語・空白・予約文字を含む名前を用い、取消・4形式の選択・焦点復帰を確認。仮想入力とheadless描画の証拠であり、以下の物理環境の項目は残る。
- Windows 11 native、物理入力・Windows IME・対象OSのGPU・物理表示、公開配布条件と実機desktopの検証。Ubuntu 24.04は専用VMのXvfb/Swayで全22群、100%・200%を含む起動、配置後起動を確認済み。Archでは[AMD OpenGLのGUIと6条件起動](../tests/results/ui-final/wayland/GPU-VALIDATION.md)も成功したが、対象OSのGPU・物理表示の受入とは区別する。
- 本番期限の[現行実時間watchdog](../tests/results/completion-final/worker-deadlines/README.md)は成功しているが、基準機の性能・メモリ・実フレーム提示は未判定。仮想X11の開発機数値は参考値に限定する。
- 実際の配布範囲に対する対応source・告知・ライセンス条件の最終照合。[台帳](DEPENDENCY-PROVENANCE.md)と[ソース対応記録](../tests/results/source-correspondence/README.md)で、告知追加版の553ファイル、PDFiumの15+2告知、toml++組込コード、Qtの動的リンク、146件の同版キャッシュmetadata、主要4 recipeと1,632実ファイルを照合した。[現行アーカイブの由来台帳](../tests/results/completion-final/dependency-provenance/README.md)も別に生成し、2回一致を確認した。[Qt一式とtoml++のsource取得・照合](../tests/results/source-archives/README.md)は後続で完了した。独立PDFium等を含む完全な対応source・全linked入力・全Chromium告知は未完了。収集状況と実際の配布条件から生じる必要性の判断を区別する。DocView自身の公開ライセンスは未選定であり、ローカルアーカイブ生成は公開配布を意味しない。
- G-RENDERで指定された正解画像の人による承認。[現行21組の比較画面](../tests/results/completion-final/render-review/index.html)に確認項目・既知の差分・原本/画像ハッシュをまとめた。独立Popplerとの19組と同一PDFiumのTTC/TTF比較2組を区別する。生成時の期待値やエージェントの画像確認だけで承認済みとは記録しない。
保存外観のない非Text注釈のNoZoomとannotation所有OBJR/StmOwnは検出時に制約を示す。PDFの情報上限による省略も黙って欠如として扱わず通知する。原設計は検証済み互換範囲と明示制約の管理を許すため、これらを任意PDFへの無制限な対応義務とは読み替えない。一方、対象OSや承認ゲートの不足を「既知制約」で合格に変更しない。
+86
View File
@@ -0,0 +1,86 @@
# Arch recipe とローカル依存版の対応
最終開発版の[依存由来台帳](DEPENDENCY-PROVENANCE.md)に記録した 4 件について、
キャッシュ `.BUILDINFO` の `pkgbuild_sha256sum` と**完全一致する**公式 Arch packaging
Git の固定 commit を確認した。latest recipe で代用していない。
全 SHA-256、URL、Git blob ID、取得ファイル一覧は
[correspondence.json](../tests/results/source-correspondence/arch/correspondence.json)に保存した。
| パッケージ版 | 一致した公式 Arch commit | recipe SHA-256 の先頭 |
|---|---|---|
| qt6-base 6.11.2-3 | [dc5728701868](https://gitlab.archlinux.org/archlinux/packaging/packages/qt6-base/-/commit/dc5728701868792b88f1a3e7f9b56aa3362c5474) | `217bd5ea50a3c721` |
| qt6-declarative 6.11.2-2 | [9eac2c716283](https://gitlab.archlinux.org/archlinux/packaging/packages/qt6-declarative/-/commit/9eac2c716283905721fdcbc5bb18494f0a486325) | `f955fe1f3f93ca32` |
| qt6-webengine 6.11.2-1 | [f701f0aa989dc](https://gitlab.archlinux.org/archlinux/packaging/packages/qt6-webengine/-/commit/f701f0aa989dcbc653805d696118f48d6a70dd0d) | `e4395367f03fd6ca` |
| tomlplusplus 3.4.0-2 | [69b0e478ef49](https://gitlab.archlinux.org/archlinux/packaging/packages/tomlplusplus/-/commit/69b0e478ef49b2a5636346d8c9b64c1effc39cfa) | `08383384ca64c9cd` |
同 commit の packaging ファイル 30 件、25,299 bytes を取得した。PKGBUILD、展開済み
参照一覧 `.SRCINFO`、local patch、packaging 自体の LICENSE/REUSE 等を含む。
全件の SHA-256 と Git blob ID を照合した。recipe のコードは実行していない。
元の本体・ワーカー・PDFium lock・最終 tar は変更していない。
## 元ソースと適用差分
- **Qt base**: `qtbase` の `v6.11.2` を参照。Qt 公式 tag の現在の解決先は
`ef55f427f2c8b410d34f8a7681020a3000cf6866`。Arch の
`qt6-base-cflags.patch` と `qt6-base-nostrip.patch` は取得し、recipe 内 SHA-256 と一致。
さらに upstream commit `e80e3f0cebae9c3a45a1b7ce81d6454c699d89c6` を
`cherry-pick -n` する。対応する公式 patch 本文も取得した。
- **Qt declarative**: `qtdeclarative` の `v6.11.2` を参照し、現在の解決先は
`4e3399c26ec57246c08de019cfcbda8d23604cfa`。
`2efb7c60ef45952cc8e04b9c9a07965d14c30446` の cherry-pick patch 本文を取得した。
- **Qt WebEngine**: `qtwebengine` の `v6.11.2` と別取得する `qtwebengine-chromium` を参照。
Qt tag の現在の解決先は `a33fa2a897e5ee58e385b3f88dc247d99fca56db`、その
`src/3rdparty` gitlink は `5170777d28bee1ce92cc693a0dbf2ad01492e5cf`。
この exact recipe の `_chromium` は空なので、prepare は `git submodule update` の
gitlink を使い、別 commit の checkout は行わない。packaging にある
`bump-chromium-commit` は保守用 helper で、この recipe からは実行されない。
Chromium source 行の checksum は `SKIP`。これを検証済み source hash と扱っていない。
- **toml++**: GitHub の公式 `v3.4.0.tar.gz` を `tomlplusplus-3.4.0.tar.gz` として参照。
recipe の SHA-512 / BLAKE2b を記録した。当時は元 archive 未取得だった。後続の[取得・検査](../tests/results/source-archives/README.md)で両checksumと現在の51ヘッダー・LICENSEの一致を確認した。tag の Git commit 解決はこの追加作業には含まない。
上の Qt tag 解決は今回の公式 metadata の観測であり、当時の builder が受け取った
source bytes を再現した証明とは異なる。Git source に宣言された SHA-256 も台帳へ保持したが、
全 checkout を取得してその checksum を計算したとは扱わない。
## Chromium の版表記
固定 gitlink の `chromium/chrome/VERSION` は `140.0.7339.225`。
Qt の `src/core/configure/BUILD.root.gn.in` は `build/util/version.py` へ
`//chrome/VERSION` を渡し、その結果を `CHROMIUM_VERSION` macro にする。
`QtGnGen.cmake` が指定する GN root は submodule の `chromium` ディレクトリーである。
`web_engine_context.cpp` の `qWebEngineChromiumVersion()` はこの macro を文字列化する。
したがって、この source から得られる版は[実ライブラリで別途観測した
`140.0.7339.225`](../tests/results/source-correspondence/qt-credits/resource-extraction.json) と整合する。
[取得した固定 source とハッシュ](../tests/results/source-correspondence/arch/upstream-metadata/version-sources.json)
一方、Qt repository 上位の説明ファイル `CHROMIUM_VERSION` は
base `140.0.7339.264` / security patches `151.0.7922.71` を記載する。
この説明ファイルは上記 API の生成元ではない。
`qWebEngineChromiumSecurityPatchVersion()` の source は `151.0.7922.71` の固定文字列を返す。
base API、security patch API、説明 metadata の値を混ぜて単一の runtime 版としない。
個々の security patch の完全性を、この版表記だけから保証するものでもない。
## 再検証と未完了範囲
[collect.py](../tests/results/source-correspondence/arch/collect.py) は既定で保存済み証拠を
オフライン検証する。`--fetch --output NEW_DIRECTORY` は固定 commit の小ファイルと metadata
だけを再取得する。1 ファイル 512 KiB、保存対象合計 4 MiB、packaging 64 ファイルを上限とし、
対象は公式 Arch/Qt の HTTPS に限定する。source archive、clone、build は実施しない。
HTML metadata には生成時刻が入り得るため、再取得時は semantic commit を照合し、
recipe・patch・固定 source 本文は byte hash を照合する。
```sh
python3 tests/results/source-correspondence/arch/collect.py
python3 tests/results/source-correspondence/arch/test_collect.py
python3 tests/results/source-correspondence/arch/collect.py --fetch \
--output tests/results/source-correspondence/arch/new-fetch
```
正式な G-DISTRIBUTION は引き続き未達。この作業で、4 recipe の版・commit・付属 patch・source
参照の対応は進んだが、全対応ソース、build toolchain、生成物の再ビルド再現、署名の真正性、
全第三者告知や適用条件の判定、対象 OS のクリーン配布検証まで完了したものではない。
DocView の公開ライセンスや製品版の同梱方針も選択していない。
## 公式リリースソース取得の追加結果
[Qt 6.11.2一式の公式archive](../tests/results/source-archives/README.md)を公開SHA-256と照合し、全390,976通常ファイルを検査した。上記WebEngineの固定source 8件が一致し、base/declarativeの4差分は専用コピーへ適用できた。CFLAGS patchは元recipeのGNU patch既定許容範囲でfuzz 1を必要とし、upstream 2差分はfuzz 0で確認した。適用後のmkspec 2件は現在のArchファイルと一致した。実ビルド、全コンパイル入力・全告知の照合、G-DISTRIBUTIONの完了は示さない。
+35
View File
@@ -0,0 +1,35 @@
# 依存関係とビルド記録
2026-09-19のLinux検証で使用した構成。依存ライブラリは動的リンク、DocView内部のライブラリは静的リンクで本体・ワーカーに組み込む。
| 要素 | 検証版 | 固定・取得方法 |
|---|---|---|
| Qt Base | 6.11.2-3(Arch) | CMakeで6.11.2 EXACT |
| Qt Declarative | 6.11.2-2(Arch) | 同上 |
| Qt WebEngine | 6.11.2-1(Arch) | 同上。文書JavaScript・通信をアプリ側で制限 |
| PDFium | 155.0.8057.0 / chromium/8057 | [lock](../cmake/pdfium.lock.json)の取得URL・SHA-256を照合 |
| libzip | 1.11.4-1 | システムライブラリ、CMake下限1.11 |
| toml++ | 3.4.0-2 | システムライブラリ、CMake下限3.4 |
| libseccomp | 2.6.0-1 | Linuxのみ、CMake下限2.5 |
| Fontconfig | 2.18.3 | LinuxのMain側FontBrokerのみ、CMake下限2.13。OS font索引・選択に利用 |
| GCC | 16.2.1+r23+gd564253eb6c8-1 | C++20、Releaseビルド |
| CMake / Ninja | 4.4.3 / 1.13.2 | ビルド・CTest |
| Poppler | 26.08.0-1 | 試験のみ。PDFiumと独立した比較器 |
| libqpdf / qpdf | 12.4.1-1 | CMakeで12.4.1 EXACT。PDFWorker内でリンク注釈辞書・構造見出しの共有メタデータ読取り。CLIは試験資料の暗号化生成にも利用 |
| Xvfb | 21.1.24-1 | GUI自動試験の仮想X11画面 |
PDFiumの上流コミットは`a5a7089234f121990b336b3841008009dca143bf`。配布元はbblanchon/pdfium-binariesのLinux x64成果物で、V8・XFAは無効。アーカイブSHA-256は`7788fa57a2996ebd21afc4090eb0a42b9f95ef3a72e7ef4c0756f1ea703c0415`。公開APIだけを利用するが、構造タグのAPIと`FPDF_SYSFONTINFO` version 2にはExperimental指定があるため、更新時にタグ・MCID・座標に加え、TTCのface選択とshort-buffer callbackの回帰試験を行う。
FontBrokerはPDFiumをMainにリンクせず、LinuxではFontconfig、WindowsではOSのGDI(`gdi32`)を専用threadで利用する。選択済みfontは上限付きのバイト列としてPDFWorkerへ転送し、Workerにはfontディレクトリーの読取り権限を与えない。static TTF/OpenType/collectionを初期対象とし、候補順と実際の選択情報を[font broker契約](font-broker-contract.md)に記録する。WindowsのGDI実装はソースと補助コンパイルまでで、native実行は未検証である。
Linuxの実行にはシステムのQt WebEngineプロセス、資源、ロケール、Qtプラグインも必要。Linuxでの`cmake --install`はPDFium本体とLICENSE/licensesを同梱し、Qtその他はシステムから利用する開発用配置である。クリーンOS向けの自己完結した配布物ではない。
libqpdfはPDFWorkerにだけリンクし、MainにはPDFパーサーを追加しない。同じ継承済みの読み取り専用ファイルから、PDFium公開APIでは取得できないリンク枠のdash配列・Border Style辞書を読む。ファイル名による再openや書込みは行わない。CMakeパッケージとtargetは[公式のライブラリー利用方法](https://qpdf.readthedocs.io/en/stable/library.html)に従う。使用したLinuxパッケージのApache-2.0ライセンス本文を[resources/licenses/qpdf](../resources/licenses/qpdf/README.txt)に複写し、installへ含める。各配布物の推移依存と告知の最終確認は残る。
Windows x64の同一版PDFiumアーカイブも取得し、SHA-256 `e307d519e42f2e69b1b531f0c2a32dffcdf3891ec0eba60328ba51a57cec01ed`、非V8・非XFAのビルド引数、DLLとimport library、Linux版と公開ヘッダー22件の一致を確認した。Windows上でのロード・実行は未検証。WindowsではCMakeパッケージのlibzip/toml++とqpdf 12.4.1 EXACT、libqpdfの推移依存DLL、MSVC x64、対応する公式Visual C++ Redistributableを必要とし、Qt資源は`windeployqt`を呼ぶ配置コードを用意した。[Windowsの準備手順](WINDOWS-BUILD.md)
PDFiumのBSD系ライセンスと同梱第三者ライセンス、Qtの利用するモジュールごとのライセンス、libzip、toml++、libseccomp、Fontconfigのライセンスを配布方式に応じて収集・表示する工程が残る。DocView自身の公開ライセンスはこの作業では指定していない。
試験文書の本文・図形は本リポジトリーの生成スクリプトで作成した。追加日本語PDFはBIZ UDPGothic Regularをsubset埋込し、[OFL本文](../tests/fixtures/pdf/extended/FONT-OFL.txt)と元フォントSHA-256を[manifest](../tests/fixtures/pdf/extended/manifest.json)に保存している。フォント自体をアプリのランタイム依存として追加してはいない。
セキュリティ修正版の導入時は、Qt、PDFium、libqpdf、libzipを独立に更新せず、版・ハッシュを更新して全CTest、PDF独立比較、WebEngine隔離試験を再実施する。記載版の将来のサポート継続や無脆弱性を保証するものではない。
+119
View File
@@ -0,0 +1,119 @@
# 開発版パッケージの依存由来台帳
URL比較修正前のArch開発版について、[ui-final台帳](../tests/results/ui-final/dependency-provenance/README.md)は、焦点・Web倍率・失敗案内の修正後のArch開発版を対象にする。561ファイル、圧縮6,378,914 bytes、展開内容17,504,986 bytes。tarは2回生成してSHA-256 `83e69c6c6354f99a8666c94cbdd669a7d4e1a86ee87d956f566132a0ea6f1153`が一致し、台帳も2回生成して`be6ed689820ce414ff23831fee2266e08bbdb3e2bf617b2324c4a924b21cad35`が一致した。
146件の同版cache metadata、system告知530件、PDFium告知17件を照合した。追加したQtの7本文は13資源・2 DataPack・固定版/hash/元全文との対応を検証した部分集合であり、`completeChromiumNotices=false`、runtimeはsystem依存である。この保存台帳のsource欄は当時の未収集状態を保持する。後続の[ソース取得](../tests/results/source-archives/README.md)ではQt 6.11.2一式とtoml++を収集・検査し、現在の収集状況は部分完了となった。[抽出の証拠](../tests/results/source-correspondence/qt-embedded-notices/README.md)と[境界試験](../tests/results/ui-final/dependency-provenance/tests.log)を保存した。Qt資源調査の旧「credits文字列の検索結果」と、後から抽出した完全な告知コメント7件は区別する。
[告知補足版](../tests/results/qt-notice-supplement/README.md)では、固定ソースのWebM/WebPメタデータが指定するPatentファイル2件をUbuntu用debへ追加した。本文は同一だが元の2パスを保持する。IDのみのmini_chromium/libdrmは、上流メタデータが非同梱と記すことを保存し、実SDKのリンク構成未確認という制約を分けて記録した。全告知の完全性判定は引き続き未完了。
## Ubuntu専用環境の配置確認
[Ubuntuの別台帳](../tests/results/ui-final/ubuntu/installed-runtime-final/runtime.json)は公式Qt SDK 6.11.2、専用prefixのqpdf/libzip、Ubuntu 24.04の85 packageを対象にし、1,740ファイルの依存解決を確認した。告知104件、SDK SBOM43件、PDFium出典metadata1件を保存した。Qtの選択位置の告知本文とqpdf/libzipのinstall先告知directoryの欠落3件を明示し、後者2件は選択sourceから各告知を別途採取した。完全なChromium告知・対応sourceとはしない。Archの固定告知pinは流用していない。この台帳の採取後に、[Ubuntu用deb](UBUNTU-PACKAGE.md)を生成して配置・起動・削除を検証した。[範囲と実起動の記録](../tests/results/ui-final/ubuntu/README.md)を併読する。
後続の[SDK告知抽出](../tests/results/source-archives/QT-SDK-NOTICES.md)で、保存済みSBOMに含まれていたChromium告知129entry・105本文を採取し、Qt source原文・元SDK archive・Ubuntu実行時inventoryと照合した。元SBOMの外部package参照不整合1件と本文のない2部品、生成器の除外を明示しており、完全性は未判定。旧台帳の採取時点の本文件数は変更していない。
## 日本語UI修正時の前回台帳
Arch Linux 開発版 tar に何が含まれ、どの資料まで照合できたかを記録する。
前回の[台帳](../tests/results/completion-final/dependency-provenance/first/provenance.json)は、
日本語UI・Web操作等の修正後に生成した `completion-final` パッケージを対象にする。
前回tarのSHA-256は`e384c8c21da11343f845a5ca7310ee6ca944e51c9585de4b2b5c7c73ca352e0e`。
553ファイル、圧縮後6,367,967 bytes、展開内容17,460,237 bytesを全照合した。
2回の台帳生成はbyte一致し、台帳SHA-256は
`9d43b6ae79bb8e7d4ced790ef25f399b4c616ad0d02cf286f59ae93d29d3220b`。
146件のcache metadata、PDFium告知17件とsystem告知523件を照合し、限定23試験が成功した。
Qtの日本語QMはsystem依存の `qt6-translations 6.11.2-1` に対応する。
DocView自身の日本語catalogは本体に含むが、QtのQMをtarへコピーしたとは扱わない。
限定試験・再生成の結果は[前回結果 README](../tests/results/completion-final/dependency-provenance/README.md)、
従来版は[履歴 README](../tests/results/dependency-provenance/README.md)、
固定recipe・告知原文と実ファイルの照合は[別の記録](../tests/results/source-correspondence/README.md)にある。
## 追加告知を収録した従来版
[従来の台帳](../tests/results/dependency-provenance/notices-final/provenance.json)は、
固定ソースとの照合で見つかったPDFiumの2告知を補った時点のパッケージを対象とする。
そのtarのSHA-256は`df9c0cc5396d7d952804a4611c85e5e2130fad821311e76f880a17e78be49c06`。
553ファイル、圧縮後6,342,588 bytes、展開内容17,418,141 bytesを照合した。
追加はlibc++/libc++abi告知と出典metadataの3ファイル。変更は説明文・告知一覧・manifestの5ファイルで、
当時の本体・2ワーカー・PDFiumの全バイナリーは、下記550ファイル版と同一だった。
今回の現行バイナリーとの同一性を主張する記録ではない。
追加告知はソースrevision・版・実ライブラリー・本文hashを結び付けて検証する。
2回の独立パッケージ生成はbyte一致し、別展開先から6条件のGUI起動が成功した。
## 元のローカル採取で確認した範囲
対象は `docview-0.1.0-arch-x86_64-development.tar.gz`、SHA-256 は
`fe1cbeb7af6bf53c82dbee3a571b369454115b5c0f1561674cdb09e26c089c4d`。
圧縮後 6,338,459 bytes、展開ファイル合計 17,369,299 bytes、550 ファイルを
tar 内の payload manifest と照合した。manifest 自身は自己ハッシュから除外されるが、
そのハッシュも台帳へ保存する。元の dependency manifest のハッシュも記録する。
| 対象 | 実際の含有・参照 | 追加で採取した証拠 |
|---|---|---|
| DocView と 2 ワーカー | tar 内の実行ファイル 3 本 | 全ファイル SHA-256 と `readelf` の直接 `DT_NEEDED` |
| 独立 PDFium 155.0.8057.0 | `lib/libpdfium.so` を同梱 | ローカルのライブラリ・15 告知文が tar と一致。`VERSION`、全 10 項目の `args.gn`、固定 lock のハッシュと値を照合 |
| Qt 6.11.2 | Qt/WebEngine の runtime 本体はシステムから動的に参照 | tar 内の 3 実行ファイルから Qt の直接リンクを確認。元の依存 inventory の Qt 共有ライブラリ 42 行を保持 |
| toml++ 3.4.0-2 | 共有ライブラリはシステム側。ヘッダー由来コードは DocView 本体に含む | 本体に定義された `toml::` シンボル 27 件、`toml++/toml.h` の include、同梱 MIT 告知文のハッシュ |
| システム依存候補 146 パッケージ | 元 inventory が記録した共有ライブラリ・Qt 資源など。runtime 本体をこの tar へコピーしていない | 全 146 件で同版・同 architecture・同 package base の cache `.PKGINFO` / `.BUILDINFO` を照合。各 raw metadata と `PKGBUILD` の SHA-256 |
システム共有ライブラリの非同梱は、ヘッダー・テンプレート由来コードが実行ファイルに
含まれないという意味ではない。toml++ は両方を実物で確認した。Qt を含むその他全依存の
ヘッダー由来コードの列挙までは実施していない。また、146 件の一覧は元 inventory の
依存候補集合であり、各資料を開くたびに全件を実際にロードしたとの主張ではない。
PDFium の先頭 `LICENSE` は binary provider の MIT 文である。
PDFium upstream の BSD 条件は `licenses/pdfium.txt` にあり、残りのライセンス文も別々に
対応させた。上位の `LICENSE` だけを PDFium 全体の条件として扱っていない。
lock 記載の provider archive SHA-256 は既存の固定値として記録し、今回その取得 archive を
再ダウンロード・再検証したとは扱わない。
## キャッシュ証拠の意味
Qt base/declarative/webengine と toml++ の 4 archive は全体の SHA-256 も記録した。
他の 142 archive は展開を進めず、最大 8 MiB の先頭領域にある metadata のみ採取する。
raw metadata はハッシュだけを保存し、本文からは package 名・版・architecture・license label・
build date・build tool・`PKGBUILD` hash を選択する。氏名、メール、build directory、
builder の全 installed-package 一覧は保存しない。利用者のホーム内データも探索しない。
このcollector単独では、recipe本文やcacheのruntime payloadを検査しない。
後続の[照合](../tests/results/source-correspondence/README.md)では主要4パッケージの
実行時ファイル1,581件と現在のtoml++ヘッダー51件をcache payloadと照合し、全て一致した。
4つの`PKGBUILD`本文もBUILDINFOのhashに一致する公式commitへ対応させ、patchを取得した。
残り142パッケージのpayload比較、cacheの署名認証、過去のコンパイル時点のヘッダー証明、
システム全体のソース回収を行ったという意味ではない。
## 再実行
Python 3.14 以上の zstd 対応 `tarfile`、GNU `nm` / `readelf`、既存の最終 tar、
`.deps/pdfium`、ローカル Arch package cache を使う。既存結果は上書きしないため、
`--output` は新しいディレクトリーにする。
```sh
python3 tools/record_dependency_provenance.py \
--archive tests/results/ui-final/linux-development-package/package/docview-0.1.0-arch-x86_64-development.tar.gz \
--output tests/results/ui-final/dependency-provenance/new-run
python3 tests/test_dependency_provenance_qt.py -v
```
別配置の入力は `--archive`、`--pdfium-root`、`--package-cache` で指定できる。
同じ tar・ローカル PDFium・cache metadata・調査ツール・source include から同じ JSON を生成する。
時刻や作業ディレクトリーを出力に含めない。cache がない項目は明示的に未採取と記録し、
版不一致や壊れた metadata は成功へ変換しない。本体やワーカー、`ldd` は実行しない。
## 原設計に対して残る作業
[設計 09 §4 G-DISTRIBUTION と §5](design/09-decisions-roadmap.md) は、
実配布物と対応ソース・告知・依存一覧の対応、および対象 OS での配布確認を求める。
この台帳だけで合格とはしない。`sourceCollectionStatus: not-collected` は資料収集状況であり、
全 146 システム依存のソースを一律に再配布する法的義務が確定したという判定でもない。
1. **対応ソースと告知の整理**: 主要4つのArch recipe/patch、PDFium providerの固定recipe・
依存pin・既存15告知の原文対応は確認した。見つかったlibc++/libc++abiの告知不足は現行tarへ
反映した。Qt一式とtoml++のsource取得・固定hash照合、Archの4差分の適用確認は[追加記録](../tests/results/source-archives/README.md)で完了した。[PDFiumのGitソース収集](../tests/results/source-archives/PDFIUM.md)では本体と27依存・追加gitlinkを保管し、公開ヘッダー24件と告知15件の一致、Linux/Windows用パッチの適用を確認した。全linked component・compiler runtime/sysroot・完全な対応ソース、
Qt WebEngineの実GN構成に対応する全Chromium告知は未完了。
[追加の資源調査](../tests/results/source-correspondence/qt-embedded-notices/README.md)で7件の完全な告知本文を収集したが、全Chromium告知は取得できておらず、
当該buildの生成物または同じ構成による生成が必要になる。これらを対象OS試験の不在だけで説明しない。
2. **公開方針の決定を伴う整理**: DocView 自身の公開ライセンス、採用する Qt の条件、
製品で実際に同梱する範囲・形式を決め、その具体的な配布に適用する告知・ソース提供等を
確認する。この台帳は条件の選択や法的適合の断定を行わない。
3. **追加環境が必要な確認**: Windows 11 x64 の署名済み配布・削除、対象OSの物理GPU・表示・入力の受入は残る。Ubuntu 24.04については[新しいOSから作成した専用VM](../tests/results/ubuntu-package/clean-vm/README.md)で依存解決・sandbox・X11/Waylandの起動・削除を検証し、[URL修正版](../tests/results/portal/README.md)では更新と実ファイル選択も確認した。これらをWindowsや物理環境の代わりとはしない。
+32
View File
@@ -0,0 +1,32 @@
# 実装時の判断記録
設計書は[docs/design](design/00-guide.md)へそのまま複写し、[SHA-256](design/sha256.json)を保存した。本書は実装時の追加判断であり、原設計の受入条件を書き換えない。
| 判断 | 実装と理由 | 検証・影響 |
|---|---|---|
| フォントを選択したバイト列だけで供給 | MainのFontconfig/GDIでOS索引から選択し、64 KiBずつ既存IPCでWorkerへ渡す。SFNT/TTC解析とPDFiumはWorker内 | Linuxのフォントディレクトリー許可を撤去。日本語CID横/縦と転送・取消・上限を試験。Windows実行は未確認。[契約](font-broker-contract.md) |
| Linuxから実装 | Arch Linux x64に加え、専用KVMのUbuntu 24.04でもQt 6.11.2と固定PDFiumをビルド・検証 | UbuntuはXvfb/Swayのsoftware描画、ArchはAMD OpenGLも確認。Windows native、対象OSの物理表示・IME、製品配布の受入は未完了 |
| PDFタイルのオフスクリーン余白16px | 設計案の2pxでは、タイル境界を横切る文字のアンチエイリアスに差が出た。16px描画して要求矩形だけを返す | タイルと全ページの切り出しが試験資料でbyte一致。転送タイルは最大514px、通常512pxのまま |
| アーカイブワーカーを読み取り専用にする | ZIP解析・CRC確認・フォント難読化解除はワーカー、最大64KiBずつの展開結果の保存はメイン側 | パーサー侵害で一時領域を無制限に書き込む経路を除去。メイン側で資源256MiB・文書2GiBを確認し、完了後だけ公開 |
| 索引を分割する | PDF目次最大20,000件、初回64件/128KiB、継続最大256件/512KiB。ページ情報は最大256件/256KiB。アーカイブ目次等も件数と実バイト数で分割 | IPC制御フレーム1MiBを保ち、大きなラベルで一括応答が溢れる問題を防止。メイン側でも累積32MiBの索引上限 |
| 先読みの予算と優先度 | 可視タイルと通常操作が完了した後、設定された隣接ページの可視域相当の帯を1件ずつ読む。前ページは末尾、後ページは先頭を対象にする | 0〜3ページ、隣接1ページ最大8タイル。余白がなければ先読みせず、先読みで既存キャッシュを追い出さない |
| メモリ圧迫の段階的対応 | OSの利用可能物理メモリを1秒ごとに読み、先読み停止→不可視タイル回収→描画解像度抑制と通知→文書処理停止の順で進む。LinuxはMemAvailable、WindowsはGlobalMemoryStatusEx | 閾値・ヒステリシス・位置保持・回復と停止後の再openは [契約](memory-pressure.md)。設定のキャッシュ上限・隔離の資源上限は維持する。Windows側の実行は未確認 |
| 実際に提示した位置を保存 | PDFは可視タイル完成後のframeSwapped、Webは提示時の論理位置を状態へ渡す | 未表示のジャンプ先を終了時に保存しない。2秒debounce/10秒最大間隔はStateStoreへ集約 |
| 保存先の原本をセッション開始時と照合 | 全形式のopen時に取得した正規パス・サイズ・更新時刻・物理ファイルIDを保存時にも照合する | 閲覧中の変更・置換で古い位置を新原本へ転用しない。通常openでも履歴との同一性不一致を通知。[実ファイル置換の回帰](../tests/results/completion-regressions/controller-state.md) |
| 目次の準備中を欠如と区別 | 能力がloadingの間は空の索引でも表示要求を保持し、パネルに読み込み中と示す | 本文のReadyを待たせず、DOM索引到着後に同じパネルへ項目を表示する |
| 見出しの代替と章境界を限定 | PDFのoutline代替は内部宛先だけを採用。EPUBの最初/最後の見出しへの指示は成立した章移動にだけ結び付ける | 外部だけの目次を見出しありと判定しない。失敗した章境界操作の指示が後の通常移動へ残らない |
| 日本語を初期表示し翻訳資源を分離 | tr/qsTr/固定contextからTSを生成し、QMを本体へ埋め込む。Qt標準部品の日本語資源も読み込む | 英語localeでの設定・コマンドの案内とQt Quick Dialogを検査。OSネイティブdialogはOS言語に従う。[更新・配置条件](TRANSLATIONS.md) |
| Webの内部URLを履歴から除外 | Mainで位置の型・上限・originを検証し、resourcePath/CFI/進捗とEPUB package/spineで復元する。旧state/backupのlocation.urlも書込み可能時に除去 | 旧href形式の復元を維持。読み取り専用・forceReadOnlyではファイルを変えず、本文引用中のURLは削除しない |
| ZIP入口は読書位置と別に保存 | 選んだ相対HTMLを同じ原本identityと現在の候補で再確認し、表示成功後だけ記憶する | recent-only/position-only、別GUIプロセス再起動、取消・消去・原本置換を試験。[記録](../tests/results/zip-entry-choice/README.md) |
| 固定EPUBの見開きは最大2ビュー | 同じ文書profileのprimary/companionへ左右を配置し、author viewportと共通倍率を維持。renderer監視は2枠に限定 | LTR/RTL、両向き、単独中央、混在、取消を確認。章loadの同期通知は現在章のみ更新し、読書位置保存は提示後のまま |
| 資源拒否を分類・件数だけ集約 | broker、interceptor、ブラウザーCSPの検出を文書内でまとめ、`:info`へ表示する | URL・本文を記録しない。合成イベント・古いセッションを拒否。[契約](resource-warning-contract.md) |
| PDFリンク枠の辞書をWorker内で補完 | PDFium公開APIで不足するBorder/BSをlibqpdfで読み、保存外観がないリンクにだけ数値から一時外観を生成する | 原本は書かず、既存外観を維持。通常枠の欠落を解消。NoZoom/NoRotateは描画中だけRect/回転を補正し、原本座標と保存APを保持する。[比較](../tests/results/link-borders/README.md) |
| PDF注釈へキーボードで到達 | 既存のリンク順の後に本文付き注釈を加え、Tab/Shift+Tabで選びEnterで読み取り専用の本文を表示する | CropBox外の注釈も選択可能。通常モードのEscで選択解除し、Web文書へ切り替えた後はPDFの選択と画像を回収する |
| 生成資料を使用 | 利用者から既存コーパスなしとの回答。本文・図形・座標・色の期待値を持つ資料を生成 | PDFiumとは独立したPopplerで比較。実書籍全般・全描画機能の互換性を保証する試験には置き換えない |
| OSの保護が利用できないときは停止 | LinuxはLandlock ABI3以上・seccomp・資源上限。WindowsはLPAC・Job・明示ハンドル継承・子側の実token検証を本番経路へ接続 | Windows固有部分は未ビルド・未実行。保護なしで起動するフォールバックを設けない |
| Windowsワーカーの依存物を個別コピー | ビルド時に有限のDLL一覧とハッシュを生成し、起動時に検証して専用領域へコピーする | インストール先や原本のACLを変更しない。実機でのPE解析・初期表示速度・配布確認は残る |
| Windowsのprofileを読み取り専用にする | AppContainer作成時にできる固有filesystem/registryの書込権限も制限し、子側で検証する | 任意Tempと自身のprofileへの書込拒否を試すnative試験を追加。Windowsでの保護成立は未確認 |
PDF見出しはWorker内の共有qpdf contextでページ/Formの所有元、ParentTree、RoleMapと構造順を読み、文字・座標は公開PDFium APIから取得する。FormだけのStructParentsにも対応し、入れ子Form・名前付きproperty・複数ページの子要素順を試験した。複数呼出や一意に対応できないFormは理由付きページ精度へ下げ、別のstreamの同じMCIDから文字や座標を借用しない。跨ページ見出しはページ別の断片として扱い、フォントサイズから見出しを推測しない。[補完アダプター](pdf-structure-adapter-plan.md)。
本文の検索・保存位置・最近使った文書のローカル記録を実装した。これら補助機能の合格範囲は検証記録のT-P系列で区別する。OCR、PDF入力・編集・保存、外部通信、文書スクリプト実行を追加しない。
+122
View File
@@ -0,0 +1,122 @@
# Linux開発版のローカルパッケージ
この手順は、現在の **Arch Linux x86_64・Qt 6.11.2** 環境向けの開発版を
tar.gzにまとめる。公開・アップロード・署名は行わない。Ubuntu 24.04、Windows、
任意Linuxでの互換性やクリーンインストールの合格を意味しない。
## 形式と対象範囲
| 候補 | この段階での判断 |
|---|---|
| tar.gz + システム依存 | ローカル開発版に採用。展開先を移して検証でき、元に戻す操作も明確。配布先のパッケージ管理やABIを変更しない |
| Ubuntu向けdeb | 初期対象の製品版候補。Ubuntu上で依存解決・インストール/削除の検証が必要なため未採用 |
| AppImage等のランタイム同梱方式 | Qt/WebEngine/GPU・sandbox・ライセンス集合を含む別検証が必要。今回の開発版には採用しない |
製品版の初期対象ごとの形式決定は未完了。Windowsの既存配置コードをこの判断で変更しない。
同梱するランタイムは本体、PDF/アーカイブワーカー、固定版の独立PDFium共有ライブラリ。
Qt、WebEngine helper/資源/翻訳/QML/plugin、libqpdf、libzip、libseccomp、
Fontconfig等はシステムから読み込む。Qt共有ライブラリとpluginの配置は別の依存条件である。
[QtのLinux配布資料](https://doc.qt.io/qt-6/linux-deployment.html)
toml++はシステムパッケージのヘッダーから実行ファイルへ組み込むため、
実行時だけのシステム依存とは区別する。MIT本文は同梱済みである。
実際の含有範囲とローカルビルド由来は[依存物の証跡台帳](DEPENDENCY-PROVENANCE.md)を参照する。
`third-party/dependency-manifest.json`に実際のArchパッケージ版、共有ライブラリや
Qt資源のハッシュ、実行ファイルが要求するGLIBC/GLIBCXX/CXXABIのsymbol versionを記録する。
Archのrolling-release ABIに依存し、単に同じCPUやQtのmajor版であれば動くという主張はしない。
X11/Wayland、IME、GPU driver、フォント、Landlock ABI 3以上とseccompが別途必要。
隔離できない環境ではワーカーを起動せず、sandboxを無効にして実行しない。
## 生成
Python 3.11以上、CMake、Archのpacmanデータベース、`ldd`、`readelf`、
`/usr/lib/qt6/bin/qtpaths`とビルド済み実行ファイルを必要とする。
自分でビルドした信頼できる実行ファイルだけを入力にする。
初回は[READMEのビルド手順](../README.md#ビルドと起動)に従い、依存物と固定版PDFiumを準備する。
```sh
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
python3 tools/package_linux_development.py --build-dir build --output build/package-local
```
Pythonが見つかったLinuxビルドには`linux-development-package` targetも登録する。
その既定出力先は`build/linux-development-package`で、必要ならCMakeの
`DOCVIEW_LINUX_PACKAGE_OUTPUT`で変更する。`BUILD_TESTING`時は軽量の`linux_package`試験も登録する。
生成スクリプトは一時領域に`cmake --install`し、実依存を調査して告知を収集する。
既に存在する出力アーカイブは上書きしない。別の出力ディレクトリーを指定して再生成する。
既存の新規install treeを使う場合は`--build-dir`の代わりに`--prefix`を指定できる。
利用者の設定・履歴・文書や、system library/fontの実体はパッケージに含めない。
LinuxのinstallにはPDFium providerの15告知に加えて、固定ソースから補った
libc++/libc++abiの2告知と出典metadataを含める。版・commit・ライブラリー・告知の
ハッシュをconfigure時とpackage生成時に照合する。PDFiumを更新した場合は再調査が必要。
追加の根拠は[PDFiumのソース対応記録](PDFIUM-SOURCE-CORRESPONDENCE.md)を参照する。
Linux packagerはさらに、QtWebEngineの実DataPack内13資源から得た7種類の完全な告知本文と
出典metadataを収録する。固定Qt版・Arch package版・DataPackのpath/hash・本文のsize/hashを
現在のsystem inventoryへ照合し、不明な版・variantや欠落・改変は新たなレビューまで拒否する。
本文とmetadataだけをコピーし、QtのruntimeとDataPackはsystem依存のままとする。
これは全Chromium告知の完成ではない。根拠と比較範囲は
[資源内告知の抽出記録](../tests/results/source-correspondence/qt-embedded-notices/README.md)にある。
- `docview-0.1.0-arch-x86_64-development.tar.gz`: ローカル開発版。
- 同名`.json`: アーカイブSHA-256、圧縮後サイズ、展開後ファイルサイズ合計、
ファイル数、本体/workerハッシュ、含めていないsystem runtime候補の合計サイズ。
- アーカイブ内`share/doc/docview/package-manifest.json`: 全payloadの相対パス・mode・
サイズ・SHA-256。自己ハッシュは含めない。
- 同`third-party/`: 依存版/出典/対応ソースの収集状況・NOTICE・収集したlicense本文。
順序、uid/gid、所有者名、mode、mtimeとgzip headerを固定する。
`SOURCE_DATE_EPOCH`は未指定時0。必要なら`--epoch`で指定できる。
同じinstall内容・ホスト依存/告知・スクリプト・Python/zlib・epochから同じバイト列を得る。
コンパイラーや実行ファイル自体の再現可能ビルドを証明する手順ではない。
生成時に展開して全hashを確認し、その展開物から再圧縮したhashが一致することも要求する。
## 展開、起動、削除
```sh
mkdir -p /tmp/docview-local-check
tar -xzf build/package-local/docview-0.1.0-arch-x86_64-development.tar.gz -C /tmp/docview-local-check
/tmp/docview-local-check/docview-0.1.0-arch-x86_64-development/bin/docview /absolute/path/to/book.pdf
```
PDFiumの検索先はworkerからの相対RPATHを使う。`LD_LIBRARY_PATH`の追加は不要。
実行すると通常は利用者のXDG設定・履歴を使う。試験は次の専用スクリプトで行い、
一時XDG領域へ隔離する。
試験結果を区別するため、`--output`には毎回新しいディレクトリーを指定する。
```sh
python3 tests/test_linux_package.py -v
xvfb-run -a -s '-screen 0 1100x760x24' \
env QT_QPA_PLATFORM=xcb QT_QUICK_BACKEND=software QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu \
python3 tests/smoke_linux_package.py \
--archive build/package-local/docview-0.1.0-arch-x86_64-development.tar.gz \
--output tests/results/linux-development-package/new-run
```
後者は全payloadを検証して空の一時ディレクトリーへ展開し、6条件の本番GUI smokeを実行する。
元のinstall pathと異なる位置からPDF/HTML/ZIP/EPUBを開く。Chromiumとworkerのsandboxは有効。
成功時も失敗時も一時展開先を回収する。同一開発機での検証なのでclean-OS試験ではない。
同じ出力先を再利用した場合も、開始時に前回の`results.json`と`package-smoke.json`を除去する。
途中失敗では成功集計を残さず、古い画像だけでは当該実行の成功を示さない。
この配置はpackage managerやsystem directoryに登録しない。不要になった展開ディレクトリーを
削除すれば実行ファイルの配置を取り除ける。通常起動で保存した設定・履歴は別に残る。
原本文書を削除対象に含めない。
## 告知と未完了事項
DocView自身の公開ライセンスは未指定。第三者のlicense本文やpackage labelsを収録しても、
DocViewのライセンスを選択したことにはならない。主要4つのArch recipeとpatch、PDFium
providerと固定依存ソースの対応は[別の調査記録](../tests/results/source-correspondence/README.md)に
保存した。各依存の対応ソース一式・完全なbuild materialはパッケージに含めず、Qt WebEngineの
build固有の全Chromium告知も未取得。今回の7本文は有限の部分告知集として記録し、
`completeChromiumNotices = false`を維持する。manifestの`source.status = not-collected`は
パッケージ側の完全なソース収集が未完了であることを表す。ソースURLだけを履行済みの証拠にしない。
今回計測する圧縮サイズはWebEngine込みの自己完結した配布サイズではない。
system runtime候補の合計は必要ディスク量の参考であり、共有・既存install・任意追加providerを
含むため、追加インストール容量や製品配布サイズと同一視しない。
G-DISTRIBUTION/T-S05の正式合格には、対象OSの実配布・依存/告知/対応ソース整理が引き続き必要。
+54
View File
@@ -0,0 +1,54 @@
# 修正版PDFiumの配置とビルド
現在の候補は `v2` です。ICC変換指定、パターンの初期状態、未着色タイルの線色を修正しています。[検証記録](../tests/results/pdfium-intent-context/README.md)に、元ソース・パッチ・バイナリーと試験結果の対応を保存しています。
## この作業環境での起動
配置済みの `.deps/pdfium-context` を選択した `build-context` を使用します。
```sh
./build-context/docview "/path/to/book.pdf"
```
新しいprefixを作る場合は、まだ存在しないパスを指定します。既存prefixを上書きすることはできません。
```sh
python3 tools/stage_pdfium_candidate.py --candidate v2 --verify-only
python3 tools/stage_pdfium_candidate.py --candidate v2 --output "$PWD/pdfium-context-prefix"
cmake -S . -B build-context-new -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DDOCVIEW_PDFIUM_ROOT="$PWD/pdfium-context-prefix"
cmake --build build-context-new -j 6
```
この配置ツールは、検証済みソース・ツール・結果が保存された本作業環境を入力にします。ネットワーク取得やPDFiumの再ビルドは行いません。CMakeには新しいビルドディレクトリーを使い、別版のライブラリーが残るキャッシュを避けます。
## 固定値と照合
| 項目 | 値 |
|---|---|
| upstream revision | `a5a7089234f121990b336b3841008009dca143bf` |
| 候補library SHA-256 | `cb049fe434f4c911b5eb047c0aca572d4dd9f5e405dde329b76c05a6aa771403` |
| 固定ビルド記録 | `tests/results/pdfium-intent-context/record.json` |
| 記録SHA-256 | `80af72b06d5c9a6a3ab8b9311373e6a23e8df28668ce575e1124350e69aa160b` |
| 配置検証の固定値 | `cmake/pdfium-candidate-v2.lock.json` |
配置時は固定記録、パッチ、変更ソース60件、ツールアーカイブ、実際のGN引数、公開ヘッダー24件、告知17件を照合します。ヘッダーはproviderパッチを逆適用して元Git inventoryとの一致も確認します。告知は元ソースとの対応記録に結び付けます。
`include/`、`lib/`、告知、`VERSION`、`args.gn`、`provenance/`、`BUILDINFO.json`を配置します。入力のリンクや特殊ファイルを拒否し、検証したバイト列から一時prefixを作成して、既存出力を置換しないrenameで公開します。
CMakeは候補libraryとヘッダーの選択先を確認し、BUILDINFO・原記録・パッチ・告知を固定lockに照合します。配置先へコピーする前に再検証します。配置後とUbuntu deb検証でも同じ対応を確認します。配置済みの告知・由来記録は `share/doc/docview/pdfium/` に含まれます。
## 回帰試験
LinuxのCTestには配置ツールと配布対応の試験を含めています。個別にも実行できます。
```sh
python3 -m unittest discover -s tests -p test_stage_pdfium_candidate.py -v
python3 -m unittest discover -s tests -p test_pdfium_candidate_integration.py -v
```
合成入力の改変、ヘッダーと原本の同時改変、既存出力・リンク・特殊ファイル、書込み途中の失敗、公開時の競合、配置前の改変、自己整合だけを装ったdebを検査します。実debの境界試験には `dpkg-deb` が必要です。
第1候補は `--candidate v1` で履歴再現用に保持しています。パターン状態の問題があり、現在のCMake配置先としては受理しません。CLIの既定値は以前の再現手順を保つv1なので、現在の候補には `--candidate v2` を指定してください。
この成果物はLinux x64の検証用です。Windows、物理表示・基準機、人による正解画像承認、公開配布の最終判断は別の未完了条件です。
+119
View File
@@ -0,0 +1,119 @@
# PDFium配布物と固定ソースの対応
2026-09-19、DocViewが固定するPDFium **155.0.8057.0**について、配布元・ビルド手順・依存pin・同梱告知の対応を調査した。これは技術的な出所確認であり、法的適合性の認定、全対応ソースの収集完了、再現ビルドの成功を意味しない。製品ソース、PDFium lock、配布アーカイブは変更していない。
**後続の収集状況:** [固定Gitソースの追加記録](../tests/results/source-archives/PDFIUM.md)で、本体と27依存、FreeType内の追加参照dlgを取得した。Git objectとアーカイブ全payloadを検査し、先行取得原文31件、パッチ後の全公開ヘッダー24件、同梱告知15件の一致を確認した。Linux3件・Windows4件のパッチを専用コピーへ適用済み。以下の「未取得・未適用」は先行調査時点の範囲を記録したもので、現在の取得状況は追加記録を参照する。コンパイラー/sysroot等の全入力と再現ビルドは引き続き未完了。
機械可読の結果は[report.json](../tests/results/source-correspondence/pdfium/report.json)、取得URL・時刻・元応答と保存内容のSHA-256は各`*.retrieval.json`、全保存物の一覧は[manifest.json](../tests/results/source-correspondence/pdfium/manifest.json)にある。保存物は小規模なメタデータ・レシピ・patch・告知原文であり、巨大ソースarchive、依存checkout、ビルドは取得・実行していない。
## 配布物からレシピへの対応
| 対象 | 確認した固定値 |
|---|---|
| PDFium upstream commit | `a5a7089234f121990b336b3841008009dca143bf` |
| provider tag | `bblanchon/pdfium-binaries`の`chromium/8057` |
| provider recipe commit | `f2e9a1c45bb17b85b540abf1af30146ef65416ac` |
| GitHub release ID | `388577209`、公開APIの`immutable=true` |
| build workflow run | `34853011437`、attempt 1、`workflow_dispatch`、`success` |
| Linux x64 archive SHA-256 | `7788fa57a2996ebd21afc4090eb0a42b9f95ef3a72e7ef4c0756f1ea703c0415` |
| Windows x64 archive SHA-256 | `e307d519e42f2e69b1b531f0c2a32dffcdf3891ec0eba60328ba51a57cec01ed` |
上のarchive digestは、既存[lock](../cmake/pdfium.lock.json)、[GitHub release API](https://api.github.com/repos/bblanchon/pdfium-binaries/releases/tags/chromium/8057)、releaseに同梱されたattestationのsubjectで一致した。今回archive本体は再取得していない。
[タグAPI](https://api.github.com/repos/bblanchon/pdfium-binaries/git/ref/tags/chromium/8057)、releaseの`target_commitish`、[workflow run](https://github.com/bblanchon/pdfium-binaries/actions/runs/34853011437)、attestation payloadの`resolvedDependencies`は、同じprovider commitを示す。Linux x64 / Build(job `104005424482`)とWindows x64 / Build(job `104005424089`)の成功も公開job metadataで確認した。
[attestation原文](../tests/results/source-correspondence/pdfium/provider/attestation.json)のSHA-256は`3928dc9e52ebda1b200d9cb2dcbd971aee57b08190a49ea3e6cd7c31ee445719`で、release assetのdigestと一致する。DSSE payloadは[読取り用JSON](../tests/results/source-correspondence/pdfium/provider/attestation-payload.json)へbase64展開した。**Sigstore署名・証明書・透明性ログの信頼検証は未実施**であり、payloadを読めたことを署名検証済みとは扱わない。payloadが固定するソースはprovider repositoryであり、PDFium本体のcheckout SHAや全依存は列挙していない。
PDFiumの[固定commit](https://pdfium.googlesource.com/pdfium/+/a5a7089234f121990b336b3841008009dca143bf)と調査時点の`refs/heads/chromium/8057`は一致した。ただしproviderは`gclient sync -r origin/chromium/8057`で取得するため、現在のbranch参照とversionだけで当時の全checkout状態を暗号学的に証明したことにはならない。
## レシピと適用patch
固定commitの[レシピ一式](https://github.com/bblanchon/pdfium-binaries/tree/f2e9a1c45bb17b85b540abf1af30146ef65416ac)から、6 workflow、`build.sh`、11段階の`steps`、全`patches`、README、LICENSEの計44ファイルを保存した。全ファイルのGit blob SHA-1を固定commitのtreeと照合し、個別SHA-256も記録している。別OS用・static版用を含むpatch集合全体を保持し、対象に適用する集合とは区別した。
| 対象 | `steps/03-patch.sh`の分岐が選ぶpatch |
|---|---|
| Linux x64、shared、V8無効 | `shared_library.patch`、`public_headers.patch`、`clang_rt.patch` |
| Windows x64、shared、V8無効 | 上記3件+`win/build.patch`。別途`win/resources.rc`をversion/yearで展開 |
`shared_library.patch`はPDFium targetを共有library化し、Windows resourcesを追加する。`public_headers.patch`はexport条件と公開C++ headerの相対includeを調整する。`clang_rt.patch`はChromium build側の未対応platform分岐を調整し、`win/build.patch`はresource compilerの呼出しを変更する。これらは公開元のpatchをそのまま保存したもので、今回新規に適用してビルドしたものではない。
[checkout手順](../tests/results/source-correspondence/pdfium/provider/recipe/steps/02-checkout.sh)はV8無効時に`checkout_configuration=minimal`を使う。[configure手順](../tests/results/source-correspondence/pdfium/provider/recipe/steps/05-configure.sh)と既存Linux配布物の[args.gn](../tests/results/source-correspondence/pdfium/packaged/args.gn)は、Release、standalone、shared、x64、V8/XFA/partition allocator無効と整合する。配布物の`args.gn`には指定した値のみがあり、全GN既定値の展開結果ではない。
## 依存ソースのpin
[固定PDFium DEPS](https://pdfium.googlesource.com/pdfium/+/a5a7089234f121990b336b3841008009dca143bf/DEPS)はSHA-256 `55e7f4ecba645a748d99b91df6ff8ed9133faa08a07d39a6c0324a717926b44e`。Pythonとして実行せず、定数・文字列連結・`Var`参照だけを読取り、56依存のURL/revision、条件、CIPD version、GCS object hash/generationを[dependency-pins.json](../tests/results/source-correspondence/pdfium/dependency-pins.json)へ保存した。
明示された`recursedeps`は3件で、いずれもその固定revisionのDEPSを取得した。そこからさらに`recursedeps`は宣言されていない。
| recursive dependency | 固定revision | 宣言された追加依存 |
|---|---|---:|
| Chromium build | `2cd94c0abeada712aeea6906d99021913c9fc28d` | 9 sysroot |
| Chromium buildtools | `6f6a5dbf04b734214f3b1f386567d101ec9d607e` | 4 formatter package |
| instrumented_libraries | `d15c278eed5d38d9acf2d8054cf37baba93cef8e` | 1条件付きbinary集合 |
これはgclientが宣言する取得グラフであり、全てが今回のlibraryへリンクされるという意味ではない。条件は評価せず原文を保持する。固定`build_overrides/pdfium.gni`ではBrotli、Skia、Fontations、Rust BMP/JPEG/PNGが無効。V8無効のminimal checkout、partition allocator無効というrecipeの設定も別に記録した。CIPDのversion文字列からinstance IDへの解決、toolchain/sysroot archiveの取得、実際の最終`build.ninja`との照合は未実施。
## 同梱15告知とソース原文
対象は既存Linux配布物のトップレベル`LICENSE`と`licenses/`の14ファイル。providerの[収集手順](../tests/results/source-correspondence/pdfium/provider/recipe/steps/08-licenses.sh)とstage手順の指定を、データ変換として再現した。**15件全てが同梱ファイルとbyte単位で一致**した。各原文URL・固定revision・原文hash・変換後hash・同梱hashは[license-correspondence.json](../tests/results/source-correspondence/pdfium/license-correspondence.json)にある。
| 同梱ファイル | ソース所在/固定revision | 対応方法 |
|---|---|---|
| `LICENSE` | provider `f2e9a1c45bb17b85b540abf1af30146ef65416ac` | LICENSEへ第三者告知の案内を追記 |
| `pdfium.txt` | PDFium `a5a7089234f121990b336b3841008009dca143bf` | LICENSEの先頭`//`を除去 |
| `agg23.txt` | 同PDFium内`third_party/agg23` | `agg_array.h`冒頭の告知を抽出 |
| `lcms.txt` | 同PDFium内`third_party/lcms` | `include/lcms2.h`冒頭の告知を抽出 |
| `libopenjpeg.txt` | 同PDFium内`third_party/libopenjpeg` | `openjpeg.c`冒頭の告知を抽出 |
| `freetype.txt` | PDFium内FTL.TXT。実ソースpin `4800589f76dc62fbe523a74a59e0d4850a5dd7df` | 原文と一致 |
| `abseil.txt` | `435e7d977fb36fb47854a4c552c0706dad0bd7cf` | LICENSEと一致 |
| `fast_float.txt` | `34164f547b7df3f5d794ff67e9f885c36819ebfc` | LICENSE-MITと一致 |
| `icu.txt` | `8cc91d9b6ab9991802fd208ee03a69714fd0251c` | LICENSEと一致 |
| `libjpeg_turbo.md` | `640f254ad0fa03f6b1f29f89b7dd9366f2f6e533` | LICENSE.mdと一致 |
| `libjpeg_turbo.ijg` | 同libjpeg-turbo revision | README.ijgと一致 |
| `libpng.txt` | `6d5341764ef4e38cbc07d35514a8aa73de50de8a` | LICENSEと一致 |
| `llvm-libc.txt` | `320824188c37e5c28738b9652a0ca8087c934bc9` | LICENSE.TXTと一致 |
| `simdutf.txt` | `f7356eed293f8208c40b3c1b344a50bd70971983` | LICENSEと一致 |
| `zlib.txt` | `285e94b8fa95ad3b7d16b80798ec8dce6febb8c8` | zlib.h冒頭の告知を抽出 |
AGG、Little CMS、OpenJPEGはPDFium内へ取り込まれた変更済みソースである。保存したREADMEにはAGG 2.3 hard fork、Little CMS 2.19の原revision `b76633e60c8387a77268fb3359277ca25b5fd75c`、OpenJPEG 2.5.4の原revision `6c4a29b00211eb0430fa0e5e890f1ce5c80f409f`とローカル変更が記載される。対応ソースとしては元のreleaseだけでなく、上表の固定PDFium treeが必要になる。
fast_floatのREADMEはversion 7.0.0/`cb1d42aaa1e14b09e1452cfdef373d051b8c02a4`を記載する一方、DEPSのcheckout pinは`34164f5…`である。この差を推測で統一せず、checkoutの特定にはDEPSの完全なrevisionを採用した。
## 追加告知の技術的根拠
15件の一致は告知一式の網羅性を証明しない。providerの収集処理は`build.ninja`の`third_party`名を英小文字・数字・`_`・`-`の範囲で抽出するため、`libc++`と`libc++abi`は`libc`へ切り詰められ、そのcaseは収集対象から除かれる。
固定Chromiumの`config/c++/c++.gni`は`use_custom_libcxx=true`を既定にし、配布Linux `args.gn`にはその無効化がない。加えて、実`libpdfium.so`(SHA-256 `08f6ea53a64f6fbb9f5ba8d9c02e0051987c6a9175f3fb5ba25f219174731aaa`)から次の小規模な証拠を保存した。
- [DT_NEEDED](../tests/results/source-correspondence/pdfium/local-readelf-dynamic.txt)は`libpthread`、`libm`、`libgcc_s`、`libc`、dynamic loaderで、外部`libstdc++`/`libc++`依存はない。この観測だけで標準libraryの実装までは決定しない。
- [公開symbol](../tests/results/source-correspondence/pdfium/local-readelf-cxx-symbols.txt)には、定義済みの`std::__Cr::__libcpp_verbose_abort`がある。
- [読取り文字列](../tests/results/source-correspondence/pdfium/local-cxx-string-indicators.txt)には`third_party/libc++abi/src/src/private_typeinfo.cpp`、`fallback_malloc.cpp`と`libc++abi:`がある。
これらを合わせると、libc++/libc++abiの告知を追加する技術的根拠がある。取得済みの固定原文は次の通り。`llvm-libc`とは別のcomponentである。
| 追加候補 | DEPS固定revision | 原文SHA-256 |
|---|---|---|
| [libc++ LICENSE.TXT](../tests/results/source-correspondence/pdfium/dependencies/third_party/libc++/src/LICENSE.TXT) | `97b436da4c33663581d394f4ee0a5977fc38c2f4` | `539dd7aed86e8a4f12cbdd0e6c50c189c7d74847e4fecc64ce2c6ee3a01da38b` |
| [libc++abi LICENSE.TXT](../tests/results/source-correspondence/pdfium/dependencies/third_party/libc++abi/src/LICENSE.TXT) | `fc1897a2c12aa27e703c3ed48b62eba8abf4ce19` | `e2b35be49f7284a45b7baca8fc7b3ab7440e7902392b2528a457816b5bb2a15c` |
調査後、固定原文を`resources/licenses/pdfium-supplemental/`へ追加し、Linuxのinstallと
開発パッケージへ反映した。[配布検証記録](../tests/results/linux-development-package/source-notices-record.json)は
2件の告知・出典metadataの追加、全553ファイルの整合、従来版とのバイナリー一致を記録する。
configureとpackagerはPDFium版・commit・library hash・告知hashを照合する。
新しい[provenance](../tests/results/dependency-provenance/notices-final/provenance.json)は、
repository原本・package内metadata・実告知の対応も検査する。
## 未証明の範囲と再確認
現段階で得たのは固定レシピ、patch集合、宣言された依存pin、既存15告知の原文対応である。全ソースの保管、全linked componentの一覧、compiler runtimeとsysrootを含む対応ソース、再現ビルド、Windows archiveの告知照合は未完了。providerの初期`depot_tools`取得は無指定のcloneであり、GitHub Actionsも`@v6`等の可変tagを使う。root DEPS内の`depot_tools_revision`を、その前に使用したbootstrap cloneのrevisionと同一視しない。
レシピの解釈と公開API metadataだけから、全入力のbit単位再現性や配布条件の充足を宣言しない。G-DISTRIBUTIONの完了判定には、これらの不足を対象に追加確認が必要である。
オフラインで再確認する場合はrepository rootから以下を実行できる。取得済みmetadataだけを読み、元PDFiumのビルドスクリプトは実行しない。
```sh
python3 tests/results/source-correspondence/pdfium/check_correspondence.py
python3 tests/results/source-correspondence/pdfium/summarize.py
```
再取得URLは`provider-plan.json`、`upstream-plan.json`、`supplement-plan.json`と各retrieval sidecarに保持する。1件の`README.pdfium` HTTP 404は[失敗記録](../tests/results/source-correspondence/pdfium/retrieval-failures.json)に残し、固定treeで確認した`README.md`を取得した。全source cloneや巨大archive取得へ置き換えていない。
+44
View File
@@ -0,0 +1,44 @@
# 日本語表示と翻訳用資源
設計01の初期UI言語は日本語。文書の言語やOSの数値・日付localeを変更せず、
本体の翻訳カタログとQt標準部品の日本語カタログを起動時に読み込む。
言語切替UIや外部から任意の翻訳ファイルを読み込む機能は提供しない。
本体・ワーカーの利用者向け文字列は`tr`、`qsTr`、または固定contextを持つ
`QCoreApplication::translate`で抽出する。コマンドID、設定キー、IPCキー、
エラーコード、文書の本文・見出し・パスは翻訳しない。
構文解析器が返す英語の詳細は、日本語の原因・行番号案内と区別して表示する。
## 更新とビルド
Qt 6.11.2のLinguistToolsを必要とする。
```sh
cmake --build build --target docview_lupdate
# resources/i18n/docview_ja.ts の新規・変更文言と訳文を確認する
cmake --build build --target docview_lrelease
cmake --build build
```
`docview_lupdate`は本番C++ targetとQMLから文言・相対ソース位置を収集する。
日本語の原文に同じ日本語の訳文を登録したTSを基準にし、追加時も訳文を確認する。
生成QMは本体へ埋め込み、通常ビルドではTSを書き換えない。新しい言語への翻訳には
TSを別言語として作成し、読み込む言語を選ぶ設計とワーカーの運用を別途追加する。
現状の多言語動作を保証するものではない。
Qt標準部品には、Qtの`TranslationsPath`にある`qtbase_ja.qm`と
`qtdeclarative_ja.qm`を使用する。配置時に統合される`qt_ja.qm`も読込み対象にする。
Linuxではシステム依存として明示し、開発版packagerが
存在とhashを検査する。Windowsの配置コードは`windeployqt`のtranslations出力を使う。
資源が欠けても文書処理を終了させず、Qtの原文へ戻るが、その配置を日本語表示の
合格とはしない。OS自身が提供するネイティブダイアログはOS側の言語設定に従う。
`translations`試験は英語localeのプロセスで、埋込QM、設定の行・原因、コマンド入力エラー、
実Qt Quick Dialogの「開く」「キャンセル」を検査する。統合実行の4ケースは成功し、最新の533件のTS文言を完了状態で収録した。
[CTestログ](../tests/results/ui-final/ctest-font-final/LastTest.log)と
[対象ビルドのhash](../tests/results/ui-final/build-record.json)に記録した。
実装はQt公式の[翻訳ビルド手順](https://doc.qt.io/qt-6/qtlinguist-cmake-qt-add-translations.html)と
[QTranslator](https://doc.qt.io/qt-6/qtranslator.html)の公開APIに基づく。
配置時の統合カタログは[Qtの翻訳配置資料](https://doc.qt.io/qt-6/qt-deploy-translations.html)を参照する。
+87
View File
@@ -0,0 +1,87 @@
# Ubuntu 24.04用ローカル検証パッケージ
Ubuntu 24.04 amd64向けのDebianパッケージを作成した。Qt SDK、PDFium、qpdf、libzipを`/opt/docview`へ配置し、`/usr/bin/docview`が必要な実行時パスを設定する。呼び出す側で環境変数を設定する必要はない。現在の検証版は、修正版PDFiumを含む[validation4](../tests/results/pdfium-intent-context/ubuntu-package/README.md)。一般公開用のリリースではない。
[debファイル](../tests/results/pdfium-intent-context/ubuntu-package/guest/package/package/docview_0.1.0~validation4_amd64.deb)は123,417,928バイト、SHA-256は `bd4f00912078d406cb466cd6910ed01c0c9fdd32b76cb2f47b74cfb9719f1bd9`。
## インストールと起動
Ubuntu 24.04 amd64のデスクトップ環境で実行する。
```sh
sudo apt install ./docview_0.1.0~validation4_amd64.deb
docview
docview '/path/to/日本語の文書.pdf'
```
PDF、HTML、HTML ZIP、EPUBを開ける。アプリケーションメニューにはDocViewを登録する。基本操作と設定は[README](../README.md)を参照する。
共有システムライブラリーはDebian依存情報に記載した版以上を要求する。日本語代替フォントとして`fonts-noto-cjk`、ラテン文字用に`fonts-dejavu-core`を要求する。同梱ライブラリーだけでOSの依存をすべて代替する構成ではない。
`/etc/apparmor.d/docview`は同梱`QtWebEngineProcess`の正確な実行パスに対するプロファイルで、Ubuntuのuser namespace制限下でChromiumの隔離を起動するために使う。インストール時に読み込み、削除時に解除する。カーネルのuser namespace制限を無効にせず、アプリのsandbox無効化オプションも設定しない。
## 削除
```sh
sudo apt remove docview
sudo apt purge docview
```
`remove`は実行ファイル・同梱ランタイム・メニュー登録を削除し、システム設定ファイルを保持する。`purge`は保持されたAppArmor設定も削除する。どちらも利用者の文書・設定・読書履歴を削除しない。
## 生成と照合
収集器と梱包処理の限定試験は次のコマンドで実行できる。通常の本体CTestとは別の結果として保存する。
```sh
python3 tests/test_ubuntu_validation_runtime.py -v
python3 tests/test_ubuntu_package.py -v
```
生成はUbuntu 24.04 amd64で行う。ビルドとSDKの準備は[専用VM手順](../tests/ubuntu_vm/README.md)、候補prefixの選択は[修正版PDFiumの手順](PDFIUM-CANDIDATE.md)を参照する。以下は候補を選択して配置した `$HOME/docview-context-install` を入力にする。入力するSDK告知とQt LICENSESは、[元SDK・ソースとの照合済み資料](../tests/results/source-archives/QT-SDK-NOTICES.md)を使う。
```sh
python3 tools/package_ubuntu.py \
--prefix "$HOME/docview-context-install" \
--version '0.1.0~validation4' \
--qtpaths /opt/docview-qt/6.11.2/gcc_64/bin/qtpaths \
--dependency-prefix /opt/docview-deps \
--source-root "$HOME/dependency-sources/qpdf-12.4.1" \
--source-root "$HOME/dependency-sources/libzip-1.11.4" \
--input-manifest "$HOME/incoming/sdk-downloads.json" \
--sdk-notices tests/results/source-archives/qt-sdk-notices-final \
--sdk-supplement tests/results/qt-notice-supplement/collection \
--qt-licenses tests/results/ubuntu-package/inputs/qt-licenses \
--output "$HOME/validation/new-ubuntu-package"
python3 tools/verify_ubuntu_package.py \
--archive "$HOME/validation/new-ubuntu-package/docview_0.1.0~validation4_amd64.deb" \
--output "$HOME/validation/new-ubuntu-package/verification.json"
```
生成先は毎回新しいディレクトリーを指定する。生成器は固定入力のハッシュを確認し、ランタイムの通常ファイルだけをコピーする。シンボリックリンクは参照先のバイト列を所定のロード名へ配置する。全ファイルのサイズ・モード・SHA-256をmanifestへ記録し、独立した検証器が`.deb`のdata/controlアーカイブを読み戻す。`SOURCE_DATE_EPOCH=0`と所有者・権限を固定し、同じ入力から2回生成した結果が一致した。
Qt SDKの抽出告知129entry、対応する105本文、QtのLICENSES 64ファイルと実行時依存の部分台帳を含む。validation3はさらに、生成器が除外していたWebM/WebPのPatentファイル2件(同一本文)と出典メタデータ4件を補う。補足台帳・元台帳・実行時ファイル・全補足原文のハッシュを確認し、一致しない入力では生成を拒否する。元SBOMの不整合や除外範囲、全対応ソースと公開ライセンスの未確定事項は解消済みとはしない。告知文書内のArch用開発パッケージ説明は従来の別方式を説明するものであり、このUbuntuパッケージの起動手順は本書に従う。
## validation4の検証
候補を明示的にリンクしたUbuntu新ビルドは全24試験群、実debを含む候補境界13試験、色probe972点が成功した。生成2回のdebが一致し、archive内の2,100ファイルを検証した。候補library、ヘッダー、BUILDINFO、固定ビルド記録、パッチ、補足告知の対応も検証する。参照CMMは色probe専用で、配布物には追加していない。
ビルド環境ではSDK・依存・旧新のbuild/install/prefixを隠し、配置後2,095ファイルと131 ELFを確認した。X11・Wayland各6条件の起動とinstall/remove/purgeが成功した。[SDKのない既存試験VM](../tests/results/pdfium-intent-context/ubuntu-package/clean-vm/README.md)でも同じ12条件、配置の照合、remove/purgeと4つの利用者データprobe保持が成功した。このVMには過去の試験ツールが残っており、新規OSでの試験とは扱わない。2台とも試験後に正常終了した。
## validation3の検証履歴
validation3は補足告知を追加した版で、本体・両ワーカー・同梱ランタイムはvalidation2と同じバイト列である。[今回の記録](../tests/results/qt-notice-supplement/README.md)で、Arch/Ubuntu各8試験、独立したdeb生成2回の一致、全2,016ファイルのarchive検証を確認した。
SDKのない既存試験VMでも、インストール後2,011ファイルと131 ELFの依存、X11/Waylandの起動12条件、remove/purge・ユーザーデータ保持が成功した。このVMには前回の試験ツールが残っている。新しいOSでの検証と、validation2で行った実portal試験の範囲は以下の履歴に分ける。
## validation2の検証履歴
validation2は日本語・空白等を含むHTML名のURL比較を修正した版である。[修正後の全22群と実portal](../tests/results/portal/README.md)はArch・UbuntuおよびUbuntu X11/Waylandで確認した。debは約123 MB、2回生成が同一で、SDKのない試験VMの旧版からの更新・配置ファイル/依存の照合も成功した。
X11/Waylandで通常起動12条件、実OSファイル選択の取消と4形式の表示10ケース、remove/purgeと4つのユーザーデータprobe保持を確認した。全Chromium告知・対応ソース・公開ライセンス、Windowsと物理環境の受入は未完了である。
## 旧版validation1での配置検証
専用Ubuntu VMでインストール・再インストール・削除・完全削除を確認した。元のQt SDK、依存ライブラリー、build/install先を私有マウント名前空間内で隠し、通常ユーザーの空の実行時環境からX11とWaylandの各6条件を起動した。131個のELFの依存解決、配置された2,004ファイルの所有者・権限・バイト列も確認した。
さらに、元のUbuntu cloud imageから作った[新しいVM](../tests/results/ubuntu-package/clean-vm/README.md)でも、SDKやコンパイラーを別途導入せずにインストール・依存解決・同じ12条件の起動・remove/purgeが成功した。共有ライブラリーの解決はGUI試験ツール追加前に確認した。この節の結果は旧版validation1の記録であり、修正版の実OSファイル選択は別記録で検査する。実GPU・物理表示、Windows、署名・公開配布条件の受入は残る。パッケージの再現性は梱包工程の一致であり、依存ライブラリーのソースからの再現ビルドを意味しない。
+297
View File
@@ -0,0 +1,297 @@
# 実装・検証記録
2026-09-20追加:[PDFium第2候補の統合検証](../tests/results/pdfium-intent-context/README.md)。ICC変換指定・パターン状態継承・タイル線色を修正し、Arch・Ubuntu各972描画点、新ビルド各24試験群、上流1,979試験が成功した。旧パターン期待値の誤りと比較先の差は別に記録する。
対象日: 2026-09-20。対象はこのリポジトリーのLinux開発版であり、設計書一式の完成版受入は未完了である。Windows向けワーカー保護・通信・資源アクセス・フォント供給・renderer監視・配布コードはソース上で統合したが、Windows版は未ビルド・未実行。Windows 11での実行、製品としての配布、基準機の性能判定が残っている。外部の追加試験環境は提供されていないため、このPC内にUbuntu 24.04の専用KVM環境を作成し、X11・Waylandで追加検証した。
## 現在の修正版と検証範囲
起動先は `build-context/docview`。専用prefix `.deps/pdfium-context` を明示的に選択し、元の依存版・旧ビルド・旧パッケージは保持した。候補の由来、ライブラリ、ヘッダー、告知の固定値をCMake・配置・deb検証で照合する。配置の再検査は実行物のコピー前に行い、不正入力で既存配置が変わらない回帰を含む。[ビルド手順](PDFIUM-CANDIDATE.md)
[Ubuntu validation4](../tests/results/pdfium-intent-context/ubuntu-package/README.md)は全24群と配布境界13試験が成功し、2回生成した123,417,928バイトのdebが完全一致した。ビルド環境のSDK等を隠した試験と、SDKのない既存VMで、それぞれX11・Wayland各6条件の起動、install/remove/purge、利用者データ保持が成功した。後者は過去の試験ツールが残る再利用環境である。参考色の計算用CMMは配布物に追加していない。
[PDF性能4条件](../tests/results/pdfium-intent-context/performance/README.md)は、新規プロセス120回・同一プロセス内120回・750操作・960スクロール入力が成功した。約1GB・5,000ページPDFの3回open・100操作・末尾確認も成功した。測定した暫定予算の超過は0。Xvfb・software描画の参考値で、物理表示や基準機の受入ではない。今回の変更はPDFiumと配布対応であり、HTML・EPUBの性能を今回再測定したとはしない。
[21組の比較画面](../tests/results/pdfium-intent-context/render-review/index.html)を新ワーカーで再生成した。83枚の復号RGBAは前回と完全一致し、既知の比較差を保持している。人によるG-RENDER承認は未実施。[現在の統合JSON](validation-record.json)は固定ビルドアンカーと最終成果物・結果を結び付け、以下の旧結果を履歴として参照する。
残る最終受入はWindows 11でのビルド・実行・配布、対象OSの物理GPU・表示・入力と基準機・長時間負荷、人による描画承認、公開配布に必要な対応ソース・告知・ライセンスの最終確認である。
## 第1候補の履歴:2026-09-20のPDFium色変換修正
[固定ソースへの修正候補](../tests/results/pdfium-intent-build/README.md)を隔離先でビルドし、元のCMYK試験48点と4種類のintent・継承・画像キャッシュ・Type3等の600点が一致した。許容差4を維持し、最大差はそれぞれ0と2。上流の単体1,104件・結合872件、既存DocViewの全22試験群も候補を使って成功した。パッチを元ソースへfuzzなしで再適用し、変更後の全51ファイルが一致することを確認した。
比較先Popplerにはshading patternのRI指定で18点の不一致が残るため、比較全体の受入は未合格のまま保存した。候補は既存依存ライブラリ・Ubuntu validation3パッケージへ未導入で、以下の製品の失敗記録を上書きしない。Ubuntuでの候補試験・梱包、SoftMask/tiling専用の画素試験、Windows、人による描画受入は残る。[統合JSON](validation-record.json)は製品と候補の識別・結果を分けて保持する。
## 元の依存版の履歴:2026-09-20のICC CMYK追加比較
[4次元ICC CLUTの比較](../tests/results/cmyk-lut/README.md)で、文書の相対的測色指定に対してPDFiumが知覚的変換を使う差を確認した。Arch・Ubuntu各48測定点のうち30点が許容差を超え、最大channel差は74。独立Popplerは全点が数式の期待値に一致し、PDFiumの画素は固定ソースの知覚的変換と照合できた。原因が分かったことを描画受入の合格とはしない。
資料の構造・プロファイル16頂点・埋込バイト列・原本不変・3倍率の寸法を検証し、[追加比較画面](../tests/results/cmyk-lut/index.html)を用意した。製品ソースとバイナリーは不変で、全22群や配布試験の以前の結果と、今回の色一致の失敗は別の範囲である。ICCの変換指定への対応または明示的な互換性判断と、検出可能な制約の通知は未完了。[現在の統合JSON](validation-record.json)にも失敗を記録した。
## 2026-09-20のQt告知補足
[Ubuntu validation3](../tests/results/qt-notice-supplement/README.md)に、SDKのSBOM生成器が省いたWebM/WebPのPatentファイル2件(同一本文)と元メタデータを追加した。Arch/Ubuntu各8件の梱包試験、2回のdeb生成一致、全2,016ファイルのarchive検証、SDKのない既存VMでのinstall・起動12条件・remove/purgeとデータ保持が成功した。元SBOMの不整合・全告知と対応ソースの未確定範囲は残る。
本体・ワーカー・同梱ランタイムと全コンパイル対象ソースはvalidation2から不変である。以下の全22群・性能・実portalは元の実行結果を保持し、今回の再実行とは数えない。[告知補足時点の統合JSON](../tests/results/qt-notice-supplement/record.json)と[その直前のJSON](../tests/results/qt-notice-supplement/prior-validation-record.json)も変更せず保存する。
## 2026-09-20のURL比較修正
実OSファイル選択の追加試験から、日本語・空白・予約文字を含むHTMLの読込みが停止する不具合を発見し、URLの表現をそろえて比較するよう修正した。[追加記録](../tests/results/portal/README.md)に再現・変更・新しいビルドとの対応を保存する。修正後の全22試験群はArchで171.56秒、Ubuntuで184.71秒、いずれも失敗0だった。
この変更で本体バイナリーは変わった。以下の`ui-final`等による性能・GPU・IME・重負荷・旧パッケージの記録は、各記録で固定した修正前の版の履歴である。「現行」と記した従来節の語も当時の版を指す。新しい本体へ実行結果を読み替えない。旧統合JSONも[履歴として保存](../tests/results/portal/prior-validation-record.json)した。
新しい本体の[性能10条件](../tests/results/portal/performance-suite/README.md)を再測定し、新規プロセス300回・同一プロセス内300回・移動2,500操作、操作負荷と約1GB/5,000ページPDFが成功した。open p95は条件間最大266.85ms、応答p95は最大23.74ms、群RSS最大830.83MiBで、測定した暫定予算の超過は0。開発機の仮想表示・software描画の値である。Ubuntu修正版debは、実portal選択10ケース、通常起動12条件、旧版からの更新・remove/purgeも成功した。[現在の統合JSON](validation-record.json)と[履歴・制約](../tests/results/portal/README.md)を併記する。
## URL比較修正前のUI実装と検証
焦点復帰、Web倍率の表示・保存、開く操作の失敗対象と対処案内を修正した。詳細は[最新の統合記録](../tests/results/ui-final/README.md)と[変更前後の回帰](../tests/results/final-ui-regressions/README.md)へ保存する。ArchのX11全22試験群が166.00秒で成功し、WaylandではApp110・Web36と6条件起動が成功した。同行した旧Canvas21件は内部でoffscreenを強制していたため、Wayland描画の証拠から除外する。初回の見開き2件の失敗は、非同期resizeの確定前に次の要求を送った試験側の問題と診断し、サーバーとQt双方の確定寸法を待つ手順へ修正した。製品の描画処理は同一である。
[AMD GPUのOpenGL経路](../tests/results/ui-final/wayland/GPU-VALIDATION.md)でもApp110・Web36・Canvas22と本体6条件起動が成功した。Canvasは実platformの検査とQQuickWindowの取得画素によるページ表示・切替・画像消去の試験を追加し、Wayland/softwareとoffscreen/200%でも成功した。追加試験後も製品バイナリーは同一で、既存の全22群と後続の限定試験を別記録にしている。実モニターのscanout・物理入力・基準機の性能受入は未実施である。実IMEの追加範囲は以下に分けて記録する。
以下の`completion-final`の20試験群・性能10条件・大容量PDFは、今回のUI修正前に固定した前回の結果である。PDF/Archive worker、core試験、実時間watchdogとcommon libraryの5バイナリーは今回も同一であり、[変更されていない部品の対応記録](../tests/results/ui-final/unchanged-component-evidence.json)に既存比較との一致を残した。全アプリ測定を新本体へ読み替えない。新しい561ファイルの開発パッケージは[2回生成一致・6条件起動](../tests/results/ui-final/linux-development-package/README.md)、部分告知台帳は[2回一致](../tests/results/ui-final/dependency-provenance/README.md)を確認した。
[URL比較修正前の性能10条件](../tests/results/ui-final/performance-summary.md)は、新規プロセス300回・同一プロセス300回、対応する移動系列の計2500操作を完了した。Xvfb 1100×760・100%・software描画で、起動p95は最大271.03ms、初期応答p95は最大23.82ms、群RSS最大831.23MiBとなり、測定した暫定予算の超過はなかった。[別の操作負荷](../tests/results/ui-final/performance-interaction/record.json)と[約1GB・5,000ページの追加負荷](../tests/results/ui-final/stress/README.md)も同じ修正前の本体で成功した。後者の3回open p95は92.31ms、独立標本の群RSS最大は721.07MiBだった。実GPU・基準機の判定ではない。初回にホストの画面設定を引き継いだ[条件不一致の失敗](../tests/results/ui-final/performance-display-mismatch/README.md)は別保存し、採用していない。
[600dpi相当画像と60,000個の半透明ベクター](../tests/results/heavy-pdf/README.md)もArchとUbuntuで表示でき、実open処理中のEsc取消と実render処理中の文書切替が成功した。画像の独立復号hashと取得画素を検査し、原本不変、旧session回収、メモリ圧迫なしを確認した。既存の本番GUI部品・ライブラリ・ワーカーを使う独立試験で、通常の22群や本体の性能10条件とは別に記録する。単一試験のheartbeat最大間隔31/38msは視覚応答p95の代用ではない。
[実IBus/MozcとUS pc105・JP jp106配列の追加試験](../tests/results/ime/README.md)もUbuntuのX11で成功した。検索・コマンド欄の日本語変換、Enter/Escの保護、物理キー番号からの記号操作、入力後のj/k復帰を確認し、原本と本体は不変だった。2ケース・setup/cleanupを含む4件が12.329秒で成功。合成IMEイベントの既存試験とは別であり、後続の[Wayland試験](../tests/results/ime/WAYLAND.md)でもQt Wayland+IBusのD-Bus経路が1ケース・setup/cleanup込み3件、7.474秒で成功した。同一補助バイナリーのX11 US/JP回帰は4件・12.438秒で成功。物理入力・Windowsは未検証で、Waylandのkeysym入力を日英物理配列の証拠へ読み替えない。
## Ubuntu 24.04の追加検証
[専用KVM環境の記録](../tests/results/ui-final/ubuntu/README.md)で、X11全22試験群が177.63秒で成功した。WaylandでもApp110・Web36、6条件の本体起動が成功し、配置済み本体は別install prefixからX11の6条件でReadyとなった。Landlock ABI 4、seccomp、Chromium sandboxとUbuntuのuserns制限を維持した。公式Qt SDK 6.11.2とGCC 13.3.0によるUbuntu用ビルドであり、Archの実行ファイルを流用していない。
Canvas試験のplatform強制を除いた後、[通常/200%の2群](../tests/results/ui-final/ubuntu/canvas-ctest/tests.xml)と[実Waylandの22件](../tests/results/ui-final/ubuntu/canvas-wayland/report.json)も成功した。追加の実ウィンドウ画素検査を含み、本体・ワーカーのhashは全22群の実行時から同一である。
CMake 3.28の配置一時名の残留とPython 3.12のzstd非対応に対応した。provenanceの5件はUbuntuだけ明示skipし、30件が成功している。非埋込日本語PDFの試験は、RPCを待つ実イベントループへ変更した後、単独3回と全体実行で成功した。製品コードと15秒の期限は同じである。Archでも修正後の全22群が成功した。
配置の依存検査は1,740ファイル・85システムpackageで解決し、追加したSDK metadata上限の20試験も成功した。告知104件とmetadata44件は部分収集であり、Qt/Chromiumの告知と対応source全体は未完了。公式SDKを別配置した専用環境での検査であり、自己完結したUbuntu配布物や実GPU/IMEの受入を意味しない。
## Ubuntuパッケージの追加検証
[Ubuntu 24.04用deb](../tests/results/ubuntu-package/README.md)を約123 MBで作成し、2回生成のbyte一致、全2,009ファイルのarchive検証、配置後2,004ファイルと131 ELFの依存を確認した。元のSDK・依存・build/install先を私有マウント名前空間で隠し、呼出側のQt/LD環境変数なしでX11/Wayland各6条件がReadyとなった。install/reinstall/remove/purge、AppArmorの登録・解除、ユーザーデータ保持probeも成功した。本体・workerは既存のUbuntu配置済みバイナリーと一致し、全22群を今回再実行したものではない。さらに[新しいUbuntu OS](../tests/results/ubuntu-package/clean-vm/README.md)でも、SDKなしで宣言依存を導入し、131 ELF解決・12条件起動・remove/purgeが成功した。実GPU・native portalと公開配布条件は別の受入とする。[使用手順](UBUNTU-PACKAGE.md)
## 対応ソース収集の追加
[Qt 6.11.2一式とtoml++ 3.4.0](../tests/results/source-archives/README.md)を固定した公開hashと照合して取得した。Qtの390,976通常ファイル・約4.74GBを検査し、告知等3,309件の所在を記録。固定WebEngine source 8件、現在のtoml++ヘッダー51件、Arch差分4件の適用とmkspec 2件の一致を確認した。本体・ワーカーは変更していない。実GN構成に対する全Chromium告知、独立PDFium等を含む全対応ソース、実配布条件の最終確認は残る。
[独立PDFiumの固定Gitソース](../tests/results/source-archives/PDFIUM.md)では、本体と27依存、追加gitlink 1件を保管し、通常46,430ファイル・リンク4件を検査した。先行Gitiles原文31件、パッチ後の公開ヘッダー24件、同梱告知15件が一致し、Linux3件/Windows4件のパッチ適用を確認した。検査の再実行結果もbyte一致した。Windowsのコンパイル・実行、compiler/sysroot等の全入力、再現ビルドはこの結果に含まない。
[Ubuntu用Qt SDKのChromium告知](../tests/results/source-archives/QT-SDK-NOTICES.md)は、実ビルドのSBOMから129entry・105本文を取り出し、Qt sourceの原文と全件一致した。SBOM記載67ファイルとUbuntu実行時83ファイルが元SDK archiveと一致。外部package参照IDの不整合1件、本文を持たない2部品と生成器の除外処理を記録し、全告知の網羅性とは区別する。
## 環境と資料
Arch Linux x64、Linux 7.2.2-zen1-1-zen、AMD Ryzen 9 9955HX(16コア/32スレッド)、約62 GiB RAM。Qt 6.11.2、GCC 16.2.1、Fontconfig 2.18.3、固定PDFium 155.0.8057.0をReleaseビルドした。全体CTestと性能測定はXvfb上のX11、Qt Quick software backend、WebEngine GPU無効で実行し、Chromium・Linuxワーカーの隔離は有効のままとした。Waylandは専用Sway headless/pixmanとAMD OpenGLの2経路を別々に記録する。実ディスプレイの60 Hzと実IMEの試験は含めない。
文書は生成した。PDFの本文、MCID、注釈、リンク、回転、切り抜き座標を生成元で指定し、追加日本語PDFはOFLフォントをsubset埋込した。さらにフォント非埋込のUniJIS-UCS2-H/V資料と、変換Form・回転・跨ページ構造を持つtagged PDF資料を生成した。HTML/ZIP/EPUBはローカルCSS・SVG・見出し・spineを持つ資料、破損資料、隔離攻撃用資料を生成した。
- [基本PDFの期待値と比較](../tests/PDF-VALIDATION.md)
- [21組のPDF比較レビュー画面](../tests/results/completion-final/render-review/index.html)・[資料と確認方法](../tests/results/completion-final/render-review/README.md)(人による承認は未実施)
- [追加PDFの原版・制約・ハッシュ](../tests/fixtures/pdf/extended/manifest.json)
- [非埋込日本語CID横/縦の前回比較](../tests/results/completion-final/pdf-fonts/comparison.json)・[手順と旧結果](../tests/results/pdf-fonts/README.md)・[フォント供給契約](font-broker-contract.md)
- [実TTCのface選択とTTF比較](../tests/results/completion-final/font-collection/comparison.json)・[手順と旧結果](../tests/results/font-collection/README.md)
- [保存外観のないPDFリンク枠の前回比較](../tests/results/completion-final/link-borders/comparison.json)・[補完の制約と旧結果](../tests/results/link-borders/README.md)
- [EPUB見開き](../tests/results/epub-spreads/README.md)・[ZIP入口の再利用](../tests/results/zip-entry-choice/README.md)・[資源警告](../tests/results/resource-warnings/README.md)
- [タグ付きPDFの境界資料](../tests/fixtures/pdf/headings/README.md)・[構造解析の検証](../tests/results/pdf-structure/README.md)
- [注釈の倍率・回転・DPI](../tests/results/pdf-annotation-flags/README.md)・[コメントのキー操作](../tests/results/pdf-comments/README.md)
- [資源エラーの詳細分類](../tests/results/resource-classification/README.md)
- [ICCプロファイルの3倍率・54点比較](../tests/results/completion-final/pdf-icc/comparison.json)・[手順と旧結果](../tests/results/pdf-icc/README.md)
- [ナビゲーションと遅延レイアウト](../tests/results/navigation-reflow/README.md)
- [Web応答監視](../tests/results/renderer-response/README.md)・[アーカイブ実読取量](../tests/results/archive-ratio/README.md)
- [メモリ圧迫への段階的対応](memory-pressure.md)
- [XHTML見出し・目次と章末の重複移動先](../tests/results/xhtml-headings/README.md)
- [前回Linux開発パッケージの証跡](../tests/results/completion-final/linux-development-package/README.md)・[作成手順](LINUX-DEVELOPMENT-PACKAGE.md)
- [同梱コード・システム依存・ローカルビルド由来の台帳](DEPENDENCY-PROVENANCE.md)
- [固定recipe・ソース参照・実行時ファイルの照合](../tests/results/source-correspondence/README.md)
- [原設計との受入照合](ACCEPTANCE-AUDIT.md)
- [原本同一性・目次待機・見出しとリンク取消の追加回帰](../tests/results/completion-regressions/controller-state.md)
- [日本語の初期表示・翻訳資源](TRANSLATIONS.md)
- [追加PDFの前回独立比較](../tests/results/completion-final/pdf-extended/comparison.json)・[手順と旧結果](../tests/results/pdf-extended/README.md)
- [HTML/EPUB資料のハッシュ](../tests/fixtures/web/manifest.json)
- [性能資料のハッシュ](../tests/fixtures/performance/manifest.json)
- [約1GB・5,000ページの画像PDF負荷試験](../tests/STRESS-PDF-VALIDATION.md)
- [ビルド依存](DEPENDENCIES.md)・[実装判断](IMPLEMENTATION-DECISIONS.md)
前回ビルドの[6系列の比較](../tests/results/completion-final/comparison-run.json)はすべて完了し、原本とバイナリーは前後で不変だった。TTCの初回は選択したPythonにfontToolsがなく失敗し、失敗ログを残してsystem Pythonで再実行した。旧比較の対応83画像とは[RGBA画素が一致](../tests/results/completion-final/comparison-pixel-regression.json)した。これは人による正解画像の承認ではない。
追加PDFは5文書・9ページ。独立したsystem Poppler 26.08.0とproduction PDFiumを144 DPIで比較し、全9ページの寸法、CropBox/Rotateの16色位置、透明合成3点を確認した。日本語の抽出・検索も成功し、全ページの比較画像を確認した。RGB平均絶対差は0.5207〜2.5135/255だが、校正済みの互換性合格閾値には使わない。基本PDFの保存外観のない標準リンク枠は補完し、比較領域8,448画素がPopplerと一致した。コメントアイコンと文字のアンチエイリアスには比較器間の差が残る。
フォント選択はMainの専用処理で行い、選択済みバイト列だけを既存IPCでPDFWorkerへ渡す。SFNT/TTC解析はWorker内に残し、Linuxのfontディレクトリー許可を撤去した。非埋込日本語資料ではIPAexMincho/BIZ UDPGothicを選択し、横/縦2ページと検索を確認した。Popplerとは代替書体が異なり、その差を記録した。任意フォント名・範囲・応答・時間・キャッシュ上限も試験した。実際の2書体TTCでも通常/太字のface 0/1を選択し、個別TTF条件と2資料・892,800画素が一致した。試験用フォントは保存・配布せず回収した。
## 自動試験の範囲
今回の追加修正より前の統合結果は **19試験群すべて成功(失敗0、未実行0)**、所要162.00秒。記録は[旧CTest JUnit](../tests/results/completion-regressions/prior-19-tests.xml)、[旧CTestログ](../tests/results/completion-regressions/prior-19-LastTest.log)、[旧集計とバイナリーハッシュ](../tests/results/completion-regressions/prior-validation-record.json)へ保存した。通常CTestのapp群で計測専用slotを1件意図的にskipし、別runnerで実行する運用は継続する。Windows固有試験はLinuxの成功件数に含めない。
その後、原本の同一性・目次待機・PDF見出し候補・Web操作取消・PDF情報省略通知・翻訳資源を修正した。独立した`build-worker`では[追加回帰と全GUI](../tests/results/completion-regressions/controller-state.md)を実行し、core 111成功、GUI 96成功・0失敗・計測専用1skipを確認した。[Web全36ケース](../tests/results/completion-regressions/docview-web-navigation-regression.txt)も成功した。これらは最終の統合実行とは区別する。
前回ソースの統合実行は、`translations`を加えた **20試験群すべて成功(失敗0、未実行0)**、165.61秒。[前回CTest JUnit](../tests/results/completion-final/ctest/tests.xml)、[前回CTestログ](../tests/results/completion-final/ctest/LastTest.log)、[対象ビルドのhash](../tests/results/completion-final/build-record.json)を保存した。内訳はcore 111、PDF 75、Web 36、translations 4、app 96成功・計測専用1skipを含む。後から追加したpackaging境界2件は[限定14件の再実行](../tests/results/completion-final/ctest/linux-package-focused.txt)で成功し、全体CTest内の12件とは区別する。試験中も本番と同じLandlock・seccompの保護を使用した。
PDFiumのlibc++/libc++abi告知を追加した時点のpackaging 12件・provenance 23件と、[553ファイルの整合・生成一致・6条件起動](../tests/results/linux-development-package/README.md)は修正前バイナリーの配布履歴である。告知追加時には主3実行ファイルのhashが不変だったが、今回の製品変更後まで同一と主張しない。前回版は[553ファイルのアーカイブを独立に2回生成](../tests/results/completion-final/linux-development-package/README.md)し、hash一致と別展開先6条件のReadyを確認した。配布物・6系列の描画比較・実時間watchdogは[前回記録](../tests/results/completion-final/README.md)へ保存済みで、同じビルドの性能再測定も完了した。
| 試験群 | 主な確認 |
|---|---|
| core | 開いた原本identityと保存時の再照合、変更と履歴なしの区別、能力スキーマ、TOMLの一括適用、型・未知キー・衝突、論理キー正規化、数値G、期限・長押し・入力状態、XDG、位置・privacy・ロック・バックアップ |
| archive | ZIP名・型・重複・展開上限・CRC、source実read量による膨張率制限、5codecのpadding攻撃と正規6方式、XML制限、入口選択、EPUB 2/3・NCX/nav/spine・nonlinear・固定レイアウト・RTL・難読化フォント |
| pdf | コメント本文・ページ内件数・CBOR応答予算の省略通知とprefix保持、PDFium metadata・ラベル・タグ/RoleMap/MCID・リンク・注釈・フォーム外観・パスワード・検索・座標・タイル一致・破損・目次/ページ分割 |
| security | 資源の不在・安全拒否・権限・サイズ・読取/保存I/Oの分類、パス・シンボリックリンク・IPCフレーム・索引総量、展開途中の非公開、原本保護、一時領域回収、OSの読取/書込/通信/子プロセス/シグナル拒否、font grantなしで実fontファイル読取拒否 |
| instance | 同一ユーザーの二重起動、ロック、引渡し、ACK、異常要求、ソケット所有・リンク、読み取り専用時の回復 |
| memory_pressure | OSサンプルの有限解析、段階順序、閾値境界、回復ヒステリシス、無効値保持、1秒timerと注入 |
| renderer_watchdog | Chromium rendererの識別、親子関係とPID再利用防止、RSS上限、30秒の応答期限、保持pidfd/handleによる対象終了、取消・旧probe失効 |
| worker_process | 実IPCで成功・エラー、要求ID/世代/操作不一致、過大応答、異常終了、一度だけの失敗通知、キュー取消、展開ストリームの順序、font逆RPCの親・副ID・範囲・遅い応答・取消 |
| benchmark_runner | 起動失敗・timeoutで古い成功証跡を破棄、冷起動失敗と測定検査失敗を最終successへ反映 |
| linux_package | 相対パス・リンク・型・重複・未登録ファイル・サイズ/hashの境界、告知補完の版/hash、固定epochのtar/gzip再現性、失敗時の旧成功記録除去、Qt日本語資源の存在・hash検査(統合12件、追加後の限定実行14件) |
| runtime_manifest | 合成MZファイルによる有限の依存一覧、名前・型・件数・サイズ、SHA-256、衝突拒否、コピー・配置後の改変検知。native PE解析の証拠ではない |
| runtime_staging | 一覧の厳密なスキーマ、hash/size検証、未登録ファイルの除外、リンク拒否、原本不変、一時コピーの回収 |
| font_contract | font逆RPCの厳密な型・ID・上限、切断時の即時終了 |
| font_broker | OS索引の限定選択、16参照・256選択・512MiB累積転送、revoke、2thread上限、GUIで待たない破棄、文書間LRU回収と使用中参照保護 |
| system_font_adapter | SFNT/TTCの表・face境界、短いbuffer、hash、再利用、128MiB上限と使用中cacheの保持、エラー時の代替成功抑止 |
| pdf_canvas | 中央ページと保存用先頭位置の分離・余白同距離・ズーム中心保持、CropBox/回転の双方向変換、PDF座標の復元、古い移動の取消、先読みの優先度・件数・上限、メモリ圧迫での先読み停止・不可視cache回収・解像度抑制・回復、旧応答破棄、キャッシュ予算、先読み/旧倍率のfont致命エラー・旧文書応答の破棄 |
| pdf_canvas_hidpi | DPR 2で192条件の通常/抑制解像度の注釈の実描画画素とクリック・選択領域を照合 |
| translations | 英語localeで埋込QM・設定/コマンドの日本語案内・実Qt Quick Dialogの「開く」「キャンセル」。OSネイティブdialogの言語は対象外 |
| app | 全4形式の閲覧中原本置換と復元不能通知、DOM目次loadingパネル、内部宛先だけのPDF見出し代替、失敗したEPUB境界指示の破棄、WebリンクのEsc取消、本番QML/Controller、コメントのTab/Enter・CropBox外・極小ページ・解除・文書切替、検索完了/取消時の能力状態、実展開worker終了時の資源分類、初期パネル表示/480・1100px幅/表示切替、現在節と選択の分離・折りたたみ・IDなし見出しの履歴復元、PDFラベル/実ページ、明示closeと旧応答破棄、メモリ圧迫での処理停止・新open拒否・回復後の明示再open、能力境界、5,000ページの遠距離移動と取消、特殊キー、IMEイベント、パネル、全形式の文書切替、元資料ハッシュ、履歴消去・未提示位置、font失敗後の表示維持・新workerでの再open・警告の保存、文書切替の失敗/取消とRecoveringの整合 |
| web | リンク選択解除と作者outline復元、同一資源の見出し境界・新navigationによる旧指示失効、本番WebEngine、HTML/XHTMLのnative/ARIA見出し・名前空間付き目次・入れ子階層、章末重複移動先・内部scroll、MainWorldとApplicationWorldの分離、実通信拒否、meta refresh/フォーム拒否、CSS/SVG、縦書き/RTL、CFI・リフロー復元、遅延image/fontの横/縦・連続scroll・native anchoring・poll先行、生成見出しのbase解決、固定viewport、検索、実renderer SIGSTOPと取消・再open |
5,000ページGUI試験は全ページ描画を待たず末尾へ移動し、異なる描画世代を連続要求して最後の緑色ページの画素を確認する。元の青いページの混入、置換後の旧画像、staging文書の取消、10ms周期のUI heartbeatも確認する。この試験の最大停止1.5秒という閾値は回帰検出用であり、T20のp95予算を緩和するものではない。
追加のGUI回帰では、[最近使った文書の初期画面](../tests/results/recent-documents/README.md)と[EPUBの文書端移動・Web検索取消](../tests/results/web-epub-boundaries/README.md)を確認した。Web位置はMainで型・上限・originを確認して論理位置だけを採用し、EPUBのpackage/spineを照合して復元する。履歴とバックアップに残る旧`location.url`は、書込み可能な状態だけで原子的に除去する。CFI・進捗・本文引用を保持し、lock取得失敗や`forceReadOnly()`ではファイルを変更しない。
## 受入IDとの対応
「Linux限定」は記載した生成資料・開発環境の個別試験が成功した範囲を指す。対象OS全体の最終合格を意味しない。「一部」は記載以外の確認が残る。
| ID | 判定範囲 | 実結果・残る確認 |
|---|---|---|
| T01 | 一部 | 文字・画像・ベクター・透明・回転・CropBox、埋込日本語を比較。非埋込UniJIS-UCS2-H/Vの横組・縦組も描画/検索/目視確認。保存appearanceなしの通常リンク枠を補完し、NoZoom/NoRotateは生成リンク枠・保存リンク/コメント外観・Textアイコンで検証。任意CID/CMap、校正済みICC/CMYK、特殊合成、両OS・全倍率の広いコーパスは未実施 |
| T02 | Linux限定 | ページ移動・倍率・回転・保存座標・旧tile破棄を自動確認 |
| T03 | Linux限定 | 5,000ページの直接末尾移動・応答性・取消・文書置換をGUI確認。約1GB(0.940 GiB)の実画像ストリームを持つ5,000ページ資料でも100操作と末尾移動を確認 |
| T04 | Linux限定 | ローカルHTMLのCSS/SVG/別HTML/アンカーを本番WebEngineで確認 |
| T05 | Linux限定 | ZIP相対資源・別HTML・入口不在/複数、ストリーム展開を確認。手動入口を原本identityへ結び付け、別GUIプロセスで再利用。変更・消去・取消・privacyを検証 |
| T06 | 一部 | EPUB 2/3・spine/目次順相違・nonlinear・RTL・固定viewportを確認。gg/Gの通常読書対象の先頭/末尾と章内端を横書きLTR/RTL・縦書きで確認。見開き・package/item上書き・左右/RTL・中央単独・混在・resize・zoom・取消を生成資料で確認。実XHTMLの章内見出し、章末の重複移動先と次章/前章への往復、失敗した境界移動が後の通常章移動へ残らないことも検証。両OSは未判定 |
| T07 | Linux限定 | 開く・読む・数値ページ・目次・パネル・文書置換のキーイベント試験 |
| T08 | Linux限定 | 内容・パネル・入力欄の分岐、文字入力中の閲覧停止 |
| T09 | 一部 | IME preedit/commitイベントと論理キーは確認。Ubuntu X11では実IBus/Mozc・US/JP XKBの変換と取消も成功。Waylandも実Qt+IBus/MozcのD-Bus経路が成功。物理入力・Windowsは未実施 |
| T10 | Linux限定 | 期限・Esc・操作先変更・長押し・数値列・設定再読込時の取消 |
| T11 | 一部 | 構造タグ/アウトライン、RoleMap、複数MCID、同一ページの異なる見出し、推測しないfallbackを確認。外部/禁止アクションのみと内部宛先混在の目次で能力・移動を検証。変換Form、回転CropBox、直接/子MCRの跨ページ順、vector衝突のfallbackを追加。FormだけのStructParentsは共有qpdfアダプターで取得し、実位置への移動もGUIで確認。annotation所有構造は明示的制約 |
| T12 | Linux限定 | 階層・現在位置印/選択の分離・折りたたみ・確定・不正宛先、更新後の選択保持、IDなし見出しの履歴と同HTML内復元、20,000件上限と分割目次、DOM索引のloadingと欠如の区別・表示要求の保持を確認 |
| T13 | Linux限定 | 3パネルの独立切替・隠した操作領域から本文への復帰 |
| T14 | 一部 | 暗/明テーマとPDF原色保持をGUI確認。表示スケールの証跡は下記。両OSは未実施 |
| T15 | Linux限定 | 有効設定を一括適用、キー一覧更新、文字設定変更と論理位置維持 |
| T16 | Linux限定 | 型・範囲・未知キー・重複・接頭辞衝突・別表記衝突を拒否して前設定を保持 |
| T17 | 一部 | XDG・非ASCII/空白パス・状態書込ロック・設定失敗を確認。Windows Known Folders実行・対象OS初回起動は未実施 |
| T18 | 一部 | 静的注釈・コメント・widget保存外観、欠損外観の通知、通常リンク枠の表示補完、入力不能・原本ハッシュ不変。NoZoom/NoRotateは生成リンク・保存AP・Textアイコンで確認。DPI、実画素とhit領域、Tab/Enterでのコメント閲覧を確認。APなしの他形式注釈はNoZoomの制約を通知。本文長・1,000件・512KiB情報予算での省略を明示し、取得済みprefixと注釈の存在を保持 |
| T19 | レビュー採用 | [接続契約のレビュー](adapter-contract.md)。解析をUIから分離し、能力・位置・ナビゲーション・取消しの既存経路と追加手順を追跡。全8能力・4状態・未対応理由を共通境界で検証。同名の抽象基底や動的登録は論理契約の必須条件としない |
| T20 | 一部 | 生成資料の新プロセス冷起動30回と同一プロセスopen30回、各系列125操作、29同条件開閉、連続scroll・操作集中時の開発機測定を実施。約1GB画像PDFでも3回open・100操作を追加。対象OS・基準機・GPU/60 Hzの受入は未実施 |
| T21 | Linux限定 | 破損・欠損資源・パスワード誤り・失敗時の旧文書維持・Esc・旧世代破棄。broker/interceptor/CSPの検出をURLなしで集計し、文書情報に分類と件数を保持。broker側も不在・安全拒否・アクセス拒否・CRC・上限・保存/読取失敗を有限分類で区別する |
| T-P01 | Linux限定 | PDF/HTMLの日本語/英語検索・n/N・文字情報なしを確認。04 §7の表示テキスト検索としてWebは現在の章/HTMLを対象にする。全spine一括検索は提供せず、入力欄とREADMEに範囲を表示 |
| T-P02 | Linux限定 | 入力モードの隔離、PDF/Webの検索revision取消、同文書内取消と文書切替後の旧callback破棄 |
| T-P03 | Linux限定 | PDF表示済み座標、Web CFI/DOM位置、package/spine解決、受信位置の型・範囲・origin検証、font/幅変更と遅延image/fontの横/縦同一文字維持、連続scroll・native anchoring・poll先行、全形式のopen時identityを保存時にも照合し閲覧中の置換先へ旧位置を転用しない。通常openの同一性不一致も通知。近似復元通知。内部URLを新規保存せず、旧履歴/backupも移行 |
| T-P04 | Linux限定 | 初期画面の一覧・キーで再表示・保存上限・無効化・履歴消去・終了時に消去位置が復活しないことを確認。削除・変更・置換された原本は以前の項目から開かず、明示的な再選択を案内 |
| T-S01 | Linux限定 | 絶対/親/リンク/重複名・実展開量・CRC・一時ファイル公開の境界を確認。実readを各streamで計数し、paddingされた5圧縮方式を途中で停止(codec内部の先読み量を含む) |
| T-S02 | Linux限定 | 実WebEngineで文書JS・外部通信・フォーム・meta/任意遷移・外部ファイル・ダウンロードを制限 |
| T-S03 | 一部 | OS隔離、資源上限、異常終了・不正返信・取消を確認。実時間でPDFの30秒通知/120秒停止、展開の10秒通知/30秒停止、途中応答で期限が延長されないことと取消を追加確認。Web rendererの実停止・応答期限・再open・取消も確認。Windows native攻撃試験は未実施 |
| T-S04 | Linux限定 | 現在/backup状態消去、原本不変、自分の未使用一時領域だけ回収 |
| T-S05 | 未完了 | Arch開発版tar.gzの依存/告知manifest、再現可能な生成、移動先からの6条件起動を確認。Ubuntu用debは同梱SDKで12条件起動・install/remove/purgeを確認。新しいUbuntu OSでも同じ12条件とremove/purgeが成功。Windows配布、対応sourceと全Chromium告知を含む公開パッケージは未完了 |
## 性能・表示・配置の証跡
前回ビルドでは [10条件の再測定](../tests/results/completion-final/performance-summary.md)が完了した。新プロセスopen計300回、同一プロセスopen計300回、各適用系列125操作、各240scroll入力で測定検査は成功し、10条件の暫定予算の参考超過はなかった。原本・バイナリーのhashも照合した。500章EPUBを含む群RSS最大は830.75 MiB。OSファイルキャッシュは保持した。
[操作集中時の再測定](../tests/results/completion-final/performance-interaction/README.md)は3成功、長押し120入力・24ジャンプ・10resize・4切替を確認した。最大heartbeat超過145.82 msは独立して報告し、各入力の視覚応答や物理scanoutと同一視しない。[大容量PDF](../tests/results/completion-final/stress/README.md)も3回open・100操作・5,000ページ目を確認した。原本不変・一時資料回収済みで、最終品質p95は24.40 ms、独立監視の群RSS最大は720.46 MiBだった。これらは対象OS・基準機の受入を代替しない。
[前回の実時間watchdog試験](../tests/results/completion-final/worker-deadlines/README.md)では本番の待機時間を変更せず、隔離済みの試験用IPC peerが応答しない場合の停止を確認する。Mainのイベントループと別要求の取消も確認し、結果は通常CTestとは分けて記録する。PDFium/libzip自体を停止させた試験や、GUIフレーム性能の測定としては扱わない。前回実行は3成功・120.172秒、PDF通知/停止30,007/120,006ms、archive通知/停止10,008/30,008ms。実行ファイルとcommon libraryは統合ビルドのhashに一致する。[旧結果と再現手順](../tests/results/worker-deadlines/README.md)も保持する。
以下の数値とJSONは今回の原本同一性・翻訳等の修正前のビルドによる履歴である。前回版の再計測結果は[別の集計](../tests/results/completion-final/performance-summary.md)へ記録しており、履歴値を前回版の測定と扱わない。履歴の性能JSONは[小型資料](../tests/results/performance/)、[500ページ/500章資料](../tests/results/performance-standard/)、[非埋込日本語PDF](../tests/results/performance-fonts/)に保存する。記録したビルドの各条件を、新プロセス・空のXDG設定/状態/キャッシュで30回開く。OSファイルキャッシュとシステムフォントは保持するため、完全コールドとは呼ばない。計測の起点はアプリ起動後のファイルopenであり、プロセス起動時間は含めない。同一プロセスの計30回は、初回1回と明示的な再open29回に分けて集計する。
各資料で同条件のopen→最初の表示→closeを29回、close後1秒待ちで観測する。その後のナビゲーション/scrollを含む最後のcloseは別に記録する。対応するページ・見出し・章の系列は各125操作。PDFのページ系列は実ページ移動101回と倍率変更24回で、同じ位置へのno-opをcache hitへ算入しない。見出しがない原500ページPDFは対象不足を明示し、500件の著者outlineを持つ別資料で見出し系列を確認する。
Webの見出し系列は実DOMの`html-heading`索引だけを使い、EPUBの著者目次では代用しない。各系列には対象数・対象の種類・往復範囲を記録する。XHTMLのnative見出しが欠落していた修正前の測定は[別記録](../tests/results/performance-before-xhtml-fix/README.md)へ保存し、最終版の見出し性能の証拠から除外した。
500章EPUBの測定資料は、章末の複数見出しが同じスクロール位置に収まらないよう最後の節に1画面分の最小高さを指定し、13見出しに異なる到達点を持たせた。元資料は`standard-500-clamped.epub`として同じバイト列で保持する。元資料で見つかった同位置の見出しを交互に再選択する不具合は製品側で修正し、横書き・縦書きの実DOMとEPUB章境界の回帰試験を追加した。[資料の版と目的](../tests/fixtures/performance/README.md)を区別する。
入力はフォーカス中の本番ウィンドウへの合成QKeyEvent。`firstResponseFrameMs`は操作成立後の最初のQtフレーム通知までの時間で、PDFでは可視tileの完成前にも記録する。Webのスクロールは操作ID付きACK、見出しはACKと論理位置変化、章移動は検証済み`resourcePath`の変化を成立条件とする。`finalQualityFrameMs`は2回以上の通知を待ち、PDFでは対象の可視tileがそろった後の通知に限定する。以前のフレームの遅延callbackは除外する。
QtのframeSwappedは提示キューの通知であり、物理的なscanoutの時刻ではない。WebはChromium compositorのframe IDと結合していないため、両値とも対象画素の提示時刻を直接検証した値ではない。`success`は要求回数・移動・close・原本不変等の計測検査の成功であり、暫定性能予算の合格ではない。50 msとの参考比較は全操作系列をまとめた`firstResponseFrameMs`のp95を使い、原設計の視覚応答を直接測定した値とは区別する。
本体RSS、および本体+全子孫のRSS合計を100msごとに採取し、開始・最大・close後・反復差分を分ける。表の群ピークと1 GiBの参考比較は同一プロセスの開閉・操作系列を対象とし、新プロセス起動30回のRSSは各生JSONへ別に記録する。close後は1秒待ってEmptyと描画cacheゼロを確認するが、全補助プロセスの消滅を条件にはしない。RSS合計は共有ページの重複を含み、採取の間に生じたピークは捕捉できない。保持RSSが直ちに戻らないことだけでリークと断定しない。実QWindow/QScreenの寸法・DPR・報告refresh、pressure段階も保存し、メモリ圧迫中の測定を通常品質の成功値へ混ぜない。描画cacheの課金量はRSSとは別指標とする。
| 資料 | 形式 | 空のアプリcacheからopen p95 (ms) | 入力→初回Qt通知 p95 (ms) | 最終品質指標 p95 (ms) | 連続scrollフレーム p95 (ms) | 群ピーク RSS (MiB) |
|---|---|---:|---:|---:|---:|---:|
| 小型 | pdf | 63.34 | 7.95 | 13.12 | 19.14 | 174.45 |
| 小型 | html | 211.38 | 11.37 | 17.01 | 19.25 | 635.30 |
| 小型 | zip | 216.61 | 6.68 | 17.07 | 18.19 | 645.33 |
| 小型 | epub | 213.54 | 13.00 | 21.20 | 18.19 | 712.08 |
| 500ページ/章等 | pdf | 63.75 | 7.93 | 15.84 | 18.25 | 433.72 |
| 500ページ/章等 | pdf-headings | 66.44 | 8.04 | 16.67 | 20.33 | 451.60 |
| 500ページ/章等 | html | 259.46 | 6.50 | 12.82 | 17.06 | 701.00 |
| 500ページ/章等 | zip | 267.92 | 10.84 | 17.07 | 17.50 | 703.34 |
| 500ページ/章等 | epub | 270.86 | 22.75 | 31.07 | 19.91 | 826.52 |
| 非埋込日本語 | pdf | 87.28 | 7.80 | 12.93 | 18.56 | 211.64 |
10条件すべて冷起動30回・原本不変・測定条件検査が成功した。OSファイルキャッシュは保持する。上記は開発機上のQt通知時刻による参考値であり、両OS・基準機の性能合格ではない。
| 資料 | PDF | cache hit 件数 / p95 (ms) | miss 件数 / p95 (ms) | 同位置移動の除外件数 |
|---|---|---:|---:|---:|
| 小型 | pdf | 101 / 13.02 | 0 / — | 0 |
| 500ページ/章等 | pdf | 81 / 12.14 | 20 / 13.26 | 0 |
| 500ページ/章等 | pdf-headings | 81 / 15.87 | 20 / 28.13 | 0 |
| 非埋込日本語 | pdf | 101 / 12.93 | 0 / — | 0 |
メモリは本体RSSと、本体+全子孫の群RSSを分けて記録する。以下の3値は開始 / 最大 / 最終close後(MiB)。反復差分は、同条件29開閉の最初と最後のclose後の差(MiB)である。
| 資料 | 形式 | 本体 開始 / 最大 / close後 | 群 開始 / 最大 / close後 | 29開閉の群差分 |
|---|---|---:|---:|---:|
| 小型 | pdf | 103.38 / 141.95 / 125.04 | 103.38 / 174.45 / 125.04 | +7.75 |
| 小型 | html | 101.76 / 352.82 / 349.98 | 101.76 / 635.30 / 501.49 | +9.09 |
| 小型 | zip | 100.92 / 339.68 / 336.52 | 100.92 / 645.33 / 490.67 | +9.69 |
| 小型 | epub | 101.75 / 342.21 / 339.50 | 101.75 / 712.08 / 491.68 | +9.71 |
| 500ページ/章等 | pdf | 103.19 / 393.89 / 387.09 | 103.19 / 433.72 / 387.09 | +5.79 |
| 500ページ/章等 | pdf-headings | 101.93 / 409.69 / 346.12 | 101.93 / 451.60 / 346.12 | +2.02 |
| 500ページ/章等 | html | 102.21 / 373.73 / 353.61 | 102.21 / 701.00 / 513.59 | +8.68 |
| 500ページ/章等 | zip | 101.51 / 363.29 / 343.16 | 101.51 / 703.34 / 493.57 | +12.73 |
| 500ページ/章等 | epub | 101.55 / 359.94 / 343.18 | 101.55 / 826.52 / 504.09 | +11.85 |
| 非埋込日本語 | pdf | 103.57 / 154.39 / 154.39 | 103.57 / 211.64 / 154.39 | +18.98 |
暫定予算から外れた参考値: なし。予算値は変更していない。継続scrollの物理60 Hz判定、完全OS-cold、長期リーク判定はこの計測では行わない。
[操作集中時の専用測定](../tests/results/performance-interaction/README.md)は長押し120入力、遠距離ジャンプ24回、resize10回、staging取消、異なる文書への4切替を含む。QtTestの本番Controller/QML/workerを使い、最後の5,000ページ目は緑色の実画素も検査する。各入力の移動有無、描画待ち/実破棄数、10ms heartbeatを記録する。通常CTestの計測専用slotのskipは、この独立実行結果と分けて報告する。
大容量PDFは5,000ページ・1,009,828,104 bytes(0.940 GiB)。1,280個の異なる画像を全て実際に使い、ページ間で再利用する。修正前の記録では3回openのp95は97.70 ms、100操作の最終品質p95は24.12 ms、50 ms間隔の独立監視によるプロセス群ピークRSSは約720 MiBだった。30回openの受入を置き換える値ではない。原本ハッシュ不変と5,000ページ目の画面を確認し、大容量一時資料は回収した。[詳細と再現手順](../tests/STRESS-PDF-VALIDATION.md)
## 前回の配置と記録の対応
前回の`cmake --install`と開発版tar.gzの別展開先で本体・ワーカー・PDFiumとライセンスを配置した。同じ開発機のシステムQtを使用し、クリーンOSへの独立配布の合格ではない。[前回配布記録](../tests/results/completion-final/linux-development-package/README.md)では2回のアーカイブ生成がbyte一致し、圧縮後6,367,967 bytes、553ファイル、SHA-256は`e384c8c21da11343f845a5ca7310ee6ca944e51c9585de4b2b5c7c73ca352e0e`。原本と別の一時展開先から、PDF・非埋込日本語PDF・HTML・ZIP・EPUB、100%/200%を含む[6条件すべてReady](../tests/results/completion-final/linux-development-package/smoke/package-smoke.json)を確認して一時展開先を回収した。100%は1100×760、200%は550×380論理pxの仮想画面内に収まり、worker/Chromiumのsandboxは有効だった。配置後PDFWorkerはRPATH変更でbuild直後とhashが異なるため、それぞれを記録する。[従来の配置結果](../tests/results/smoke/results.json)と[旧配布記録](../tests/results/linux-development-package/README.md)は履歴として保持する。
ソース、実行ファイル、試験、証跡のSHA-256と集計は[validation-record.json](validation-record.json)に記録する。今回の追加修正前の集計は[prior-validation-record.json](../tests/results/completion-regressions/prior-validation-record.json)に保存した。前回版の全試験・比較・配置・性能の結果は[completion-final](../tests/results/completion-final/README.md)にそろえ、旧結果を新しいバイナリーへ読み替えない。最終性能は10条件、PDF比較は6組を記録し、それぞれ対象バイナリーを照合する。前回の実時間watchdogは再実行済みで、専用試験実行ファイルとcommon libraryのhashを個別reportとbuild-recordで確認できる。最終集計の`workerDeadlinesBuildMatches`へも反映する。旧結果は従来の各フォルダーと旧集計へ保持する。記録の`matchesRecordedBuild`と`pdfComparisonBuildMatches`で対象を区別する。約1GBの負荷試験も最終ビルドで再実行し、主3実行ファイルの一致を`stressPdfBuildMatches`に記録する。旧ビルドの測定は`stressPdfPrior`として保持する。
## Windows向けに追加したコード
LPAC・Job・明示したハンドルだけの継承、子側での実token検証、元文書のread-onlyハンドル入力、非同期pipe通信を本番ワーカー経路へ接続した。個別runtimeの一覧・ハッシュ照合、profileのfilesystem/registry書込拒否、handle相対の資源アクセス、rendererのプロセス識別・監視、volume+128bit file IDによる保存位置の照合を追加した。Windows PDFiumの固定アーカイブと公開ヘッダーも確認した。
Windows限定の`windows_pipe`、`windows_resources`、`windows_worker`試験をCMakeへ登録した。これらは未実行であり、Linuxでの合成MZ試験やWineヘッダーによる構文確認では代用しない。Windows GUI試験は一時保存先を明示し、実利用の履歴を消去しない構成とした。[準備手順](WINDOWS-BUILD.md)・[隔離の詳細とフォント代替の未確認点](windows-sandbox-port.md)
## リリースの残作業
### 追加で解消した実装上の不足
- 全形式でopen時の原本identityを保存時に照合し、閲覧中の変更・置換へ旧位置を転用しない。復元不能と履歴なしを区別して案内する。DOM目次のloadingは欠如と区別し、表示要求を保持する。[修正前6失敗と修正後のGUI/core](../tests/results/completion-regressions/controller-state.md)。
- PDFの構造見出しがない場合の代替候補は内部宛先だけを採用する。EPUBの見出し章境界は成立したnavigationへ結び付け、失敗した操作の指示を次の章移動へ残さない。WebリンクのEsc取消はDOMのフォーカスと選択表示も解除する。
- PDFのコメント本文長・ページ内1,000件・512KiB情報応答予算の省略を明示し、取得できたprefixを保持する。本文が長すぎる注釈は固定代替文で存在を示す。安全上限を緩めず、原本も変更しない。
- 日本語sourceをQtの翻訳contextで抽出し、TS/QMと起動時のQTranslatorを接続した。英語localeでの日本語案内とQt Quick標準ボタンの4ケースを確認した。Qtの日本語資源の配置は必要条件であり、OSネイティブdialogや多言語切替の完成は主張しない。[翻訳・配置手順](TRANSLATIONS.md)。
- PDFのNoZoom/NoRotateは、保存APのバイト列を変更せず、描画中だけ矩形・回転を補正する。DPRを描画要求とキャッシュ識別へ渡し、Mainの選択枠・マウス判定も同じ表示座標へ合わせる。生成リンク枠、保存リンク/コメント外観、ネイティブTextアイコン、非表示指定、重なり、CropBox境界を生成資料で検証した。APなしの非Text注釈のNoZoomは制約を通知し、不正確なクリック補正を行わない。
- 資源提供側の詳細原因を表示集計まで渡す。実在しない資源、安全検査拒否、権限、MIME、上限、CRC、容量不足、保存/読取I/O、Worker異常を区別し、取消は通知しない。[追加契約](resource-warning-contract.md)。
- FormだけにStructParentsを持つ構造見出しは、[共有qpdfアダプター](pdf-structure-adapter-plan.md)で取得する。所有元と呼出を検証できる文字だけをexactにし、複数呼出・曖昧な対応は理由付きpage precisionへ下げる。循環・破損・quota超過は制約を返す。
- PDF検索の能力は、走査完了の実結果でsupported/unavailableへ更新する。取消・失敗・旧応答では未確認状態を保持する。
- PDFのコメント本文はTab/Shift+Tab、Enterで開ける。CropBox外の注釈も対象とし、通常モードのEscで選択を解除する。Web文書への切替後はPDFの選択・画像を回収する。
これらの資料に対する成功を、任意のPDF・全OSの互換性合格としては扱わない。
### 対象環境・配布・性能の残作業
1. 統合済みWindowsコードをMSVC・Windows Qtでビルドし、本番LPAC下のPDF/ZIP/EPUB、pipe・resource・worker試験を実行する。日本語を含む非埋込フォントの代替、profile回収、renderer監視、PE依存解析と配置を検証する。現状は利用可能と確認できたWindows版ではない。[詳細](windows-sandbox-port.md)
2. Ubuntu 24.04ではXvfb/Sway・software描画の22試験群、100%/200%を含む起動と配置済みアプリを確認済み。X11の実IBus/Mozc・US/JP XKBは補助試験で確認済み。WaylandのQt+IBus D-Bus経路も確認済み。ローカルdebのinstall/remove/purgeは既存VMと新しいUbuntu OSで確認済み。物理入力、GPU、native desktop portalと公開配布条件を確認する。Windows 11ではnativeビルドとこれら一式が未検証。
3. 基準機と拡張コーパスでT20のp95、継続scrollのフレーム間隔、長時間稼働、様々なスキャン文書の取消と上限を測定する。600dpi相当画像と高密度ベクターの単一生成資料はArch/Ubuntuで検証済みだが、長い実スキャン群を網羅していない。
4. PDF互換性の未検証項目と既知の外観差を評価し、依存ライセンス・告知・配布方式を確定する。
以上が残るため、G-RENDER/G-HEADINGS/G-SANDBOX/G-WEBは確認済みの範囲だけを記録し、G-DISTRIBUTIONと全要件の完成判定を合格にしない。
+45
View File
@@ -0,0 +1,45 @@
# Windowsビルドと未検証項目
Windows向けコードと試験を用意したが、Windows SDK・MSVC・Windows版Qtを利用できる環境は今回提供されていない。以下は実装したビルド経路であり、実行済みの手順や動作保証ではない。Windows 11でのコンパイル、実行、隔離攻撃試験、クリーン環境への配置が必要である。
環境を新規作成する場合、[MicrosoftのWindows 11 Enterprise評価版](https://www.microsoft.com/en-us/evalcenter/evaluate-windows-11-enterprise)は利用登録後の90日評価を案内している(2026-09-19確認)。利用可能なISO・利用条件・必要なアカウントを確認してから専用VMへ導入する。旧Developer VMの英語URLは確認時点で[開発環境案内](https://learn.microsoft.com/en-us/windows/dev-environment/)へ転送され、既成VMの現行配布元としては確認できなかった。Windows VMはまだ作成していない。
## 必要なもの
- Windows 11 x64、対応するMSVC x64コンパイラとWindows SDK。`dumpbin`を実行できるDeveloper PowerShellを使用する。
- 同じツールチェーン向けのQt 6.11.2。Core、Gui、Network、Qml、Quick、QuickControls2、WebEngineQuick、Xml、LinguistTools、Test、QuickTestと`windeployqt`が必要。Qtの日本語翻訳資源(qtbase_ja.qm、qtdeclarative_ja.qm、または配置ツールが統合したqt_ja.qm)も配置する。
- libzip 1.11以上、toml++ 3.4以上、qpdf 12.4.1のCMakeパッケージ。libzip/libqpdfとその依存DLLは同じアーキテクチャ・ランタイムで用意する。
- CMake 3.24以上、Ninja、Python 3.12以上。
- ビルドしたアプリに対応する公式Microsoft Visual C++ Redistributable。開発機のCRT DLLを自動で収集・再配布する構成にはしていない。
PDFiumはLinux版と同じ固定コミットのWindows x64成果物を取得する。V8とXFAは無効で、`bin/pdfium.dll`と`lib/pdfium.dll.lib`を使う。取得元とハッシュは [lock](../cmake/pdfium.lock.json) に固定した。
このWindows providerには、Linuxで検証した[第2候補の描画修正](PDFIUM-CANDIDATE.md)をまだ組み込んでいない。以下は元のproviderでビルド経路を確認する手順であり、修正版の完成手順ではない。Windows用PDFiumへの同修正の適用・ビルド・色とパターンの回帰試験も残っている。Linux用の共有ライブラリーと固定lockをWindowsへ流用しない。
## 構成・試験・配置
以下のQtと依存ライブラリの場所を実際の配置先へ置き換える。
```powershell
$env:CMAKE_PREFIX_PATH = "C:/Qt/6.11.2/msvc2022_64;C:/docview-dependencies"
python cmake/fetch_pdfium.py --platform windows-x64 --destination .deps/pdfium-windows
cmake -S . -B build-win -G Ninja -DCMAKE_BUILD_TYPE=Release -DDOCVIEW_PDFIUM_ROOT="$PWD/.deps/pdfium-windows"
cmake --build build-win --parallel 6
ctest --test-dir build-win --output-on-failure --output-junit tests.xml
cmake --install build-win --prefix "$PWD/build-win/install"
& ./build-win/install/bin/docview.exe ./tests/fixtures/pdf/navigation.pdf
```
実機ではQtが使用するGPU・表示環境とChromium sandboxを有効にする。保護を無効にする引数や環境変数で試験を通さない。基礎試験資料は同梱し、追加生成手順は [README](../README.md) に記載した。
ワーカーのリンク後、`dumpbin`とCMakeのPE依存解析を使い、必要なDLLを実行ファイルの隣へ収集する。未解決依存、異なる内容の同名DLL、大小文字だけが異なる名前はビルド失敗とする。Windows API-setとOSのDLLは収集対象から除外する。特殊な再配布DLLを追加する場合のみ、`DOCVIEW_EXTRA_WORKER_RUNTIME_DLLS`に再配布可否を確認した絶対パスを指定する。
各`*.exe.runtime.json`は、ワーカー本体と有限個のDLLの名前・サイズ・SHA-256を記録する。起動時には全て照合して個別の一時領域へコピーし、そのコピーにだけAppContainerアクセス権を設定する。DLL更新時に古いDLLとの衝突が出る場合は、依存構成を確認して新しいビルドディレクトリーを用意する。既存の異なるDLLを無条件に上書きしない。
`cmake --install`はワーカーの一覧とハッシュを再検証し、GUI用Qtプラグイン・QML・WebEngine資源を`windeployqt`で配置する。`--nopatchqt`でQt DLLの変更を止め、明示した`qt.conf`で配置先を指定する。配置後もワーカー依存のハッシュを検証する。これはインストーラー作成、署名、アンインストール、ライセンス告知を完了する処理ではない。[QtのWindows配布手順](https://doc.qt.io/qt-6/windows-deployment.html)
## Windows固有の試験
`windows_pipe`は非同期通信、上限、切断と取消、`windows_resources`はhardlink/reparse・名前変更・原子的公開、`windows_worker`は本番LPAC内の要求順序、不正応答、異常終了、原本・兄弟ファイル・設定・通信・子プロセス・余分な継承ハンドル・自身のprofile書込拒否を試す。追加試験用ワーカーは製品installの対象に含めない。GUI試験の履歴と資源は明示した一時保存先を使用する。
これらはWindowsではまだ実行していない。Linuxでのmanifest生成・コピー試験は合成MZファイルを使うため、PE依存解析やLPACの成立を証明しない。Windowsのフォント代替、profile回収、WebEngine renderer監視、Known Folders、IME、表示倍率、配布については [隔離の実装と残作業](windows-sandbox-port.md) と [受入対応表](VALIDATION.md) を参照する。
+106
View File
@@ -0,0 +1,106 @@
# 文書アダプターの実装契約と拡張手順
対象は現在の C++20 / Qt 6 実装である。設計書 01 の R09、02 の処理境界、05 の論理契約との対応を記録する。設計書にある `FormatAdapter` は操作の意図を定義したもので、現在のコードに同名の基底クラスや動的プラグイン機構はない。
## 現在の境界
| 責務 | 実装 | 境界で受け渡すもの |
|---|---|---|
| 開く、取消し、文書切替、履歴、操作の振分け | `src/app/controller.*` | セッショントークン、generation、検証済みの QVariant データ |
| キー・入力欄から有限コマンドへの変換 | `src/core/input.*` | command ID、引数。シェル展開は行わない |
| PDF 解析・描画・構造情報 | `src/pdf/pdf_document.*`、`src/pdf/main.cpp` | CBOR。PDFium ハンドルはワーカーの外へ渡さない |
| PDF 表示・位置解決・タイルキャッシュ | `src/app/pdf_canvas.*` | PDF ポイント座標、画像タイル、renderRevision |
| ZIP/EPUB 解析 | `src/archive/archive.*`、`src/archive/main.cpp` | メタデータ、資源一覧、上限付きの展開ストリーム |
| HTML/EPUB 表示と資源提供 | `src/web/*`、`qml/WebPane.qml`、`resources/reader.js` | 文書別 `doc://` origin、アプリ所有の isolated-world コマンド |
| ワーカー輸送と応答検証 | `src/common/worker_process.*`、`ipc.*`、`contracts.*` | 有限 CBOR、要求 ID、世代、用途別の受信検証 |
| ローカル保存 | `src/core/state.*`、`config.*` | 検証した JSON 状態と TOML 設定 |
PDFium と libzip は GUI 実行ファイルにリンクしない。PDF タイルと WebEngine の DOM を共通画像へ変換する層もない。共有するのは文書操作の意味、位置・世代・能力の扱いである。
## 論理操作と実際の呼出し
| 設計書 05 の操作 | 現在の入口 | 結果・制約 |
|---|---|---|
| probe | Controller の先頭バイト判定、ArchiveReader の内部判定 | 複雑な ZIP/XML の解析はワーカー側。独立した FormatAdapter::probe はない |
| open | WorkerProcess `open`、ローカル HTML の profile 作成 | PDF は初期 metadata と初回描画の成功を確認して切替。HTML は staging view の load 成功を確認 |
| getOutline | PDF `outline(cursor,limit)`、archive `metadata`、Web `index` | PDF は安定 ID と parentId/depth、nextOutlineCursor。Web は読み込んだ資源の索引 |
| getHeadings | PDF `headings(page)`、Web `index` | PDF はページごとに抽出し、exact/page と source を保持。独立した節一覧の cursor はない |
| resolveLocation / navigate | PdfCanvas::goToLocation、Web `location.restore` / `navigate` | PDF のページ情報待ちを含む。後続の明示移動・倍率変更・取消しは未完了の復元を失効させる |
| renderPdfTiles | PDF `render` → PdfCanvas | page、物理scale、devicePixelRatio、rotation、clip、renderRevision。返却は BGRA8888 |
| setViewport | Canvas の geometry/DPR/fit 計算、WebPane の layout | PDF の描画 revision と Web view の documentSerial は別管理 |
| findText | PDF `search`、WebEngineView::findText | PDF は query/page/start/limit、矩形と nextCursor。OCR はない |
| close | WorkerProcess::stop、profile revoke、view dispose | GUI は終了期限を伴うプロセス終了を使う。PDF の close RPC は handle 解放後ワーカーを終了する |
PDF の `pages` は outline を再送しない。初期応答に全ページ・全目次を入れず、ページは最大 256 件かつ 256 KiB、目次は最大 256 件かつ 512 KiB 以下に分割する。目次全体の走査上限は 20,000 件・深さ 64 で、省略を warning と truncated で通知する。GUI はページ範囲・連続性を再検証し、索引は許可したフィールドだけを残してセッション合計 32 MiB に制限する。
## 能力情報
`src/core/types.h` の `DocumentCapabilities` は Qt 非依存で、次の 8 個の `Capability` を持つ。PDF の metadata 生成はこの表現を実際に使用する。通信上の既存表現は `capabilities: {name: state}`、理由コードは `capabilityReasons: {name: reasonCode}` である。
| 項目 | PDF open 成功時 |
|---|---|
| pageNavigation | supported |
| chapterNavigation | unsupported / E_CHAPTER_NAVIGATION_UNSUPPORTED |
| outline | supported または unavailable |
| headings | loading。解析結果と文書内宛先を持つ outline fallback の確定は Controller の責務。外部・禁止アクションだけの目次を supported の根拠にしない |
| textSearch | loading。現在の検索応答で文字を確認した時点で supported、全ページの探索が完了しても文字がなければ unavailable。取消し・旧セッション/旧検索の応答・エラーでは未確定の能力を確定しない |
| textSelection | unsupported / E_TEXT_SELECTION_UNSUPPORTED |
| reflow | unsupported / E_REFLOW_UNSUPPORTED |
| password | unavailable。要求中は open の E_PASSWORD_REQUIRED / E_PASSWORD_INVALID とセッション状態で通知 |
`supported` はアダプターが機能を提供できること、`unavailable` はその文書で対象がないこと、`loading` は未確定、`unsupported` は実装範囲外を表す。未知の能力名・状態を成功扱いしてはいけない。`CommandRegistry::requiredCapability()` がコマンドと能力の対応を返す。
`contracts.cpp::validateCapabilities()` は 8 項目が全て存在すること、状態が上記 4 種の文字列であること、`unsupported` に理由があることを検証する。理由は既知の能力に対応する 128 文字以下の `E_` で始まる英大文字・数字・アンダースコアのコードに限る。PDF の open 応答受信時と全形式の commit 前に検証し、不正な新文書では切替を中止して以前の文書を保持する。Web の能力と理由は Controller が既定の描画ポートから構築する。能力を必要とするコマンドは、検索入力・目次表示を含めて同じ検証と可否判定を通り、未知・欠落を実行許可に変換しない。`loading` は非同期探索を開始できる状態として扱い、`unsupported` の理由は一時通知に表示する。
HTMLのDOM索引がまだ届いていない場合、空の配列と`outline=loading`を目次の欠如へ変換しない。利用者の表示要求を保持し、読み込み中のパネルから確定した索引へ移行する。
PDF metadata の `isTagged` は公開 `FPDFCatalog_IsTagged` の結果であり、タグ付き文書の宣言を示す。構造木の完全性や見出しの存在を保証する値ではない。タグ宣言がある場合でも、個々の構造と位置は `headings` の結果で判定する。参考: [PDFium の catalog API 実装](https://pdfium.googlesource.com/pdfium/%2B/e8b8183f4d7a410b42bc4af6c31dcacb3b365685/fpdfsdk/fpdf_catalog.cpp)。
## 位置・応答の規約
- 内部ページ番号は 0 始まり。画面の物理番号は 1 始まり。ページラベルは文字列として保持し、数値計算に使わない。
- PDF の保存位置は `kind=pdf`、pageIndex、pageLabel、xPt/yPt、viewRotation、fitMode、zoom。xPt/yPt は元 PDF 空間であり、Canvas が CropBox・元回転・利用者回転・DPR を適用する。読書位置は表示上端を基準にする。
- Web の保存位置には token を含む URL を残さず、検証済みの資源相対パス、CFI/fragment、progression等を保存する。EPUBではpackagePath/spineIdrefも照合する。`zoom`は有限数の25–500で、HTML/HTML ZIP/流込EPUBではWebEngineのzoomFactor×100、固定EPUBではページ/幅合わせを基準にするspreadZoom×100を表す。固定EPUBの`fitWidth`はbool(falseはページ合わせ)、`panX`/`panY`は表示可能なパン範囲に対する有限数の0–1。画面サイズから計算する固定ページの最終ピクセル係数やDPRをこの倍率へ混ぜない。
- WebPaneはアプリ側の表示倍率と合わせ方を`location`の結果に加える。Controllerの`logicalWebLocation()`が表示通知・保存位置の復元の両方で型と範囲を検証し、表示済み位置の`zoom`をStateStoreの位置と倍率へ保存する。復元ではレイアウトを確定して表示倍率を適用してからDOM位置を解決する。旧位置に`zoom`がなく別保存の倍率がある場合はそれを同じ境界で検証する。両方になければ既定表示を維持する。後続コマンドは未完了のWeb位置復元を取り消す。
- PDF outline/link の wire target は平坦な page/x/y、HTML/EPUB は href 等を持つ。設計書の再帰的 TocNode と DocumentLocation variant に全面変換する実装ではない。
- 各 IPC メッセージは protocolVersion=1、requestId、sessionId、generation、operation と、payload または result/error のどちらかを持つ。送信元の stdout を命令として扱わない。
- 制御は 1 MiB、画像データは 8 MiB 以下。Linuxはローカルソケット、Windowsは継承した双方向pipeを使う。どちらも同じIpcChannelのバイトストリーム内で、長さヘッダーの上位 bit により画像と制御のフレームを識別する。
- GUI は画像の幅・高さ・stride・形式・バイト数・ページ・clip・renderRevision を再検証する。Canvas の文書番号と revision も照合して、古い画像を採用しない。
- `discardQueued(operation)` は未送信要求だけを除去する。実行中の PDFium 呼出しは中断されず、応答破棄またはワーカー終了で取消しを成立させる。
- `frameReady` は画像の受信であり、画面提示完了ではない。保存・性能測定で表示済み位置が必要な場合は、window の frameSwapped と viewportReady を併用する。
- 保存する位置は全形式でopen時の原本識別情報と結び付ける。保存時にパスを再照合して変更・置換があれば拒否し、復元時は保存済み位置なしと同一性不一致を区別して通知する。完全な内容ハッシュの常時計算は行わない。
## フォーカスと開く失敗の受渡し
一時的な入力欄・操作一覧・ダイアログを開くときは、Main.qmlが元の操作領域を保持する。閉じるときは有効な目次/ページ一覧へ戻し、非表示・空になった領域は本文へ戻す。正常に完了したコマンドが`focus.content`等でフォーカスを指定した場合は、保留していた復帰先を破棄する。形式のcommitや新たなUI操作を追加するときも、後から閉じる入力欄でその指定を上書きしない。
`Controller::openPath()`の直接失敗とstagingセッションの失敗は、`recordOpenFailure()`がbasename・有限の原因分類・利用者対処へ変換する。認識しないコードは一般のopen失敗として扱い、任意のworkerエラー本文・内部例外を利用者説明へ流用しない。新しい失敗コードを追加する場合はこの対応も追加する。既存文書の描画やwarningsは維持し、最後の失敗はController内の`lastOpenFailure_`にだけ保持する。`:info`の別項目で表示し、次のopenPath試行で消去する。StateStoreや表示中セッションのmetadataへ保存しない。最近使った文書の欠落・置換検査など、openPath到達前の拒否は各入口の通知で扱う。
## 形式を追加する手順
1. 新形式の source、位置、能力、必要な読み取り権限、上限を定義する。既存の PDF・Web の描画ポートで扱えるかを判断する。
2. パーサーと依存ライブラリを専用ターゲットに分離し、未信頼解析はワーカーに置く。ワーカーは許可された入力を開き、OS sandbox の成立後に解析する。保護失敗時の通常権限への再試行を作らない。
3. IPC 操作を追加する場合は `ipc.cpp` の whitelist、ワーカーの入力スキーマ、GUI の結果スキーマを同時に更新する。ページング、総量、文字列長、数値範囲、取消し時の破棄条件を決める。
4. Controller の型判定、staging/commit、機能振分け、位置保存を接続する。現在は中央の format 分岐の変更が必要であり、アダプター登録だけでは追加できない。
5. 新コマンドが必要なら CommandRegistry の ID・引数・能力対応、InputRouter の既定割当、設定例、UI ハンドラーを追加する。解析済み文書からコマンド名や実行コードを注入しない。
6. Web 資源を使う形式は文書別 origin と ResourceHandler を利用し、外部通信・文書スクリプト・ダウンロードの既定を維持する。展開先は broker 側の ResourceWriter が所有する。
7. 形式単体、実ワーカー IPC、破損/巨大入力、取消し・文書切替、位置復元、実表示の回帰試験を追加する。依存の固定版と配布告知も更新する。
## R09 / 設計書 05 の評価
R09 の分離基盤は、ワーカー単位の解析、有限コマンド、能力表現、専用描画ポートとして成立している。一方で、形式追加の登録機構、Qt 非依存モデルを端から端まで通す adapter interface、全操作の統一取消しトークンは未実装である。`DocumentIdentity`、`DocumentLocation`、`ResponseStamp` 等は補助モデルと単体試験にとどまり、runtime の保存・通信は QVariant/QCbor と個別検証を使う。`DocumentCapabilities` の採用だけでこの差分を解消したとは扱わない。
UI のモデルは一部が format 分岐に依存する。利用者向け説明はQtの翻訳contextと日本語sourceへ分離したが、IPCは翻訳キーだけを送る統一エラー型ではなく、固定コードと説明文を使用する。Web の再配置には view ごとの serial と JavaScript callback の生存確認があり、設計書の単一 `layoutRevision` モデルとは異なる。これらを拡張時の変更点として扱う。不要な抽象基底を追加して runtime と別の契約を増やすより、次形式の追加時に既存 2 経路をその契約へ移すのが検証可能な手順となる。
証跡は `tests/test_pdf.cpp`、`test_pdf_canvas.cpp`、`test_core.cpp`、`test_archive.cpp`、`test_security.cpp`、`test_app.cpp`、`test_web_qml.cpp` と `tests/PDF-VALIDATION.md` にある。合格した個別試験と設計全体の受入完了は区別する。
## T19 設計レビュー記録(2026-09-19)
**判定: 現在の静的組込み方式による接続手順を採用する。** T19 の受入条件である「UI 固有処理にパーサーを埋め込まず、能力・位置・ナビゲーション契約で接続できる」ことは、上記の責務表・操作表・追加手順から追跡できる。第五の形式の実装や、特定の名前を持つ C++ 抽象基底の存在を合否条件にはしない。
- 設計書 05 は `FormatAdapter` を言語非依存の論理契約と定義し、PDF タイルと Web 可視域には別の描画ポートを認めている。IPC、専用 Canvas、Web コマンドの組合せでこの操作境界を実現することは実装上の選択である。設計書 02 の初期拡張方式も配布元が組み込むモジュールに限定しているため、動的プラグイン機構は必須ではない。
- PDFium と libzip は各ワーカー専用ターゲットへ分離され、QML へ形式パーサーを追加する必要はない。新形式は既存の二つの描画ポートを選ぶか、別の専用ポートを定義し、Controller で正規化済みメタデータ、位置復元、ナビゲーションを接続する。
- 能力は厳密な境界検証と有限コマンドに接続する。位置は PDF 座標または Web の資源・CFI・fragment・progression、ナビゲーションは上限付き索引、非同期応答は要求 ID とセッション世代で区別する。これらの更新が形式追加時の必須作業である。`test_core` の能力スキーマ試験と `test_app` の不正能力での操作拒否試験が、未知の能力を許可扱いしないことを検証する。
- 設計書 02 が目指す「登録だけで追加する」依存方向には差分が残る。現在は Controller の中央分岐と一部 UI モデルを変更する必要があり、Qt 非依存の位置型を実行時全経路で利用しているわけではない。この差分は拡張時の変更箇所として明示する。T19 の接続可能性の採用を、R09 配下の全ての理想的な構造が実装済みという意味にはしない。
以上から、同名基底の不在は単独の未合格理由としない。必須なのは既存文書の挙動、隔離、能力・位置・ナビゲーションの検証を保つ接続経路であり、形式追加時には上記 7 手順と同等の適合試験で再評価する。
+52
View File
@@ -0,0 +1,52 @@
# DocView アプリケーション設計書
設計基準版 1.0 / 2026年9月19日
## 目的と設計の要点
DocView(仮称)は、PDFを中心にHTML・HTML ZIP・EPUBを読むための、Vim風操作を備えたデスクトップ文書リーダーである。本文を主役にした静かな画面と、キーボードだけで完結する移動を提供する。
表示基盤にはQt Quick、PDF描画には独立プロセス内のPDFium、HTML・EPUB表示にはQt WebEngineを採用する設計とする。PDFは紙面を保持し、HTML・EPUBはそれぞれの構造を使って移動する。表示形式の違いを無理に「ページ番号」だけへ統一しない。
本版は実装の入力となる設計基準である。動作・性能・互換性の実測結果は含まない。性能数値は受入目標、技術選定は検証ゲート付きの設計判断として示す。プログラム、ビルド環境、試作品は成果物に含まれない。
## 文書構成
| 文書 | 主な内容 | 主な読者 |
|---|---|---|
| [01 要件・スコープ](01-requirements.md) | 要件ID、優先順位、対応範囲、受入追跡 | 企画・開発・QA |
| [02 全体アーキテクチャ](02-architecture.md) | 構成、責務、処理フロー、拡張点 | 開発 |
| [03 形式別設計](03-formats.md) | PDF、HTML、ZIP、EPUBの取込みと表示 | 開発・QA |
| [04 画面・操作設計](04-ui-navigation.md) | 画面、モード、キー、フォーカス、見出し | 企画・開発・QA |
| [05 データ・インターフェース設計](05-data-contracts.md) | モデル、位置表現、コマンド、IPC、状態遷移 | 開発 |
| [06 設定・保存設計](06-config-storage.md) | TOML、キーバインド、OS別配置、永続化 | 開発・運用 |
| [07 セキュリティ・障害設計](07-security-errors.md) | 信頼境界、入力制限、障害復旧 | 開発・QA |
| [08 性能・試験設計](08-performance-tests.md) | 目標、測定条件、互換性、受入ケース | 開発・QA |
| [09 技術判断・開発計画](09-decisions-roadmap.md) | 選定理由、代替案、検証ゲート、残課題 | 技術責任者 |
| [10 参考資料](10-references.md) | 公式仕様・API・配布資料 | 開発 |
初読は01→02→04→09の順、実装着手時は03・05・06・07・08を併読する。
## 設計の確度
| 区分 | 意味 |
|---|---|
| 確定要件 | 原要件、またはデスクトップGUIであるという補足合意 |
| 設計判断 | この版で採用する構成・振る舞い。変更は関連文書と試験を同時に更新する |
| 提案値 | 原要件に数値指定がないため設定した測定可能な初期値 |
| 検証ゲート | 実装開始後に実機・代表文書で確認し、未達なら選定を見直す条件 |
PDFの再現性、構造タグからの見出し移動、隔離環境の動作、縦書きEPUB、Qt依存物を含めた配布を最初に検証する。これらが未確認の段階で、全PDFへの完全互換や全Linux環境への対応を宣言しない。
## 用語
- 文書: 開いたPDF、単独HTML、HTML ZIP、またはEPUB。HTMLの内部リンク先も同じ許可ルート内なら同一文書セッションに属する。
- 目次: PDFアウトライン、EPUB nav/NCX、HTML見出しから構成したナビゲーションツリー。
- 見出し: 文書構造に記録された節の起点。PDFアウトラインとPDF構造タグは出典を区別する。
- ページ一覧: PDFの物理ページ、固定レイアウトEPUBのspine項目。流し込み文書では代わりに章一覧を使う。
- 論理位置: 再描画やウィンドウサイズ変更後も可能な限り再発見できる読書位置。
- 許可ルート: 文書セッションが参照できるファイル集合の境界。端末全体のファイル権限とは異なる。
## 文書管理
要件をR01〜R15、設計判断をADR、受入ケースをT、技術検証をGで識別する。要求の変更時は01の対応表から影響をたどる。補助的な機能は原要件と区別し、リリース範囲に入れる判断を記録する。外部資料は確認時点の情報であり、採用時にバージョンとライセンスを固定する。
+77
View File
@@ -0,0 +1,77 @@
# 01 要件・スコープ
## 製品の目的
PDFの紙面を正確に表示し、HTML・HTML ZIP・EPUBも同じ操作体系で読み進められる、Linux・Windows向けデスクトップGUIを提供する。TUI風とは、本文中心の簡潔な画面、必要時だけ現れる補助表示、Vim風のキー操作を指す。端末エミュレーター内での表示は対象としない。
## 要件一覧と設計への対応
「必須」は完成版の受入条件であり、段階的な開発順とは区別する。PDF表示を最初に成立させる。
| ID | 要件 | 優先度 | 主な設計文書 | 主な受入観点 |
|---|---|---|---|---|
| R01 | PDFを正確にレンダリングする | 最優先・必須 | 02・03・08 | 文字・画像・配置・透明・回転・注釈外観の再現 |
| R02 | ローカルHTMLを表示する | 必須 | 03・07 | HTML・CSS・画像・フォントの参照とリンク移動 |
| R03 | ZIP化HTMLを内部展開しindex.htmlと依存要素を読む | 必須 | 03・07 | 入口解決、安全な展開、相対パス・リンク |
| R04 | EPUBを表示する | 必須 | 03・08 | package/spine/nav、縦書き・RTL・固定レイアウト |
| R05 | キーボード主体・Vim風に操作する | 必須 | 04・05・06 | 開く・読む・移動・設定再読込・終了をキーで完結 |
| R06 | 見出し情報があれば見出しへジャンプする | 必須 | 03・04・05 | 元形式の構造情報から移動、情報の有無を正しく表示 |
| R07 | 目次があれば表示し、項目へジャンプする | 必須 | 03・04 | 階層・現在位置・移動先の整合 |
| R08 | 目次・ページ一覧等を選択表示し、余分なボタンやラベルを置かない | 必須 | 04・06 | 初期の最小表示、表示切替、フォーカス復帰 |
| R09 | 将来の拡張を容易にする | 必須 | 02・05・09 | 形式アダプター・コマンド・能力情報による分離 |
| R10 | キーバインドや表示設定をファイルで変更する | 必須 | 06 | スキーマ、上書き、再読込、エラー時の回復 |
| R11 | 書籍を読み込み滑らかに操作できる | 必須 | 02・08 | 初期表示、キー応答、ページ遷移、メモリの測定 |
| R12 | 少なくともLinux・Windowsで利用する | 必須 | 06・08・09 | 両OS実機で表示・入力・配布を検証 |
| R13 | 設定等をOSごとの慣習に沿って配置する | 必須 | 06 | XDG、Windows Known Folders、非ASCIIパス |
| R14 | PDFの編集・マークアップ・フォーム記入を含めない | 除外 | 03・04・07 | 保存操作やフォーム入力で原本を変更しない |
| R15 | デスクトップGUIで見た目とキー操作をTUI風にする | 確定補足 | 02・04 | GUIで正確な紙面と簡潔な操作画面を両立 |
R01〜R14の根拠は提供要件「Vim/TUI風文書リーダー」(docview.md)、R15は製品形態の補足合意である。試験ケースへの詳細対応は08に置く。
## 初期版の範囲
| 項目 | 設計範囲 |
|---|---|
| 実行環境 | Windows 11 x64、Ubuntu 24.04 LTS x64を初期の試験対象とする提案。LinuxのX11・Wayland双方で確認 |
| 文書の開き方 | OSファイル選択、コマンドラインのローカルパス、ドラッグ&ドロップ。1ウィンドウ・1アクティブ文書 |
| PDF | 静的ページ、内部・外部リンク、アウトライン、構造見出し、ページラベル、拡大縮小、回転、暗号化文書のパスワード入力 |
| HTML | ローカル静的HTMLと、同じ許可ルート内の関連HTML・CSS・画像・フォント |
| HTML ZIP | 検証済み一時領域への内部展開、index.html選択、関連資源・複数ページ |
| EPUB | DRMなしのEPUB 2/3。流し込み、固定レイアウト、日本語縦書き、RTL。詳細は03 |
| ナビゲーション | スクロール、ページ/章移動、見出し、目次、リンク、戻る/進む |
| 設定 | TOMLによるキー・配色・補助パネル・文字サイズ・性能上限 |
## 原要件を補完する設計提案
本文検索、読書位置の再開、少数の最近使った文書、エラー詳細表示、簡易ヘルプを提案する。長い書籍をキーボードで読むための補助手段として初期設計に含めるが、R01〜R15を満たすうえで独立した追加要件であり、開発計画上は削減可能である。本文検索は既存の文字情報だけを対象とし、OCRを暗黙に追加しない。
アプリの日本語UIを初期値とし、文字列を翻訳可能な資源へ分離する。読書状態・履歴はローカルに保存する。ネットワークアカウントやサーバーは必要としない。
## 表示互換性の境界
「PDFを正確に表示する」は、対応対象の文書について文字、画像、図形、クリッピング、透明度、ページ寸法・向きが読書に影響する欠落なく再現されることを意味する。代表コーパスと目視による判定を行う。異なる表示器間のアンチエイリアス差をそのまま欠陥と判定しない。
電子署名の検証、添付ファイルの実行、動画・3D・JavaScript、動的XFA、DRM解除、印刷色校正、OCRは追加機能として扱い、初期版に含めない。既存の注釈・フォームの静的外観は読書内容なので描画対象とする。検出した未対応機能や読込失敗には警告または開けない理由を示す。検出可能なXFA等は事前分類する。一方、描画APIだけですべての視覚的欠落を実行時検出できるとは想定せず、検出不能な差は代表文書の比較試験と既知制約で管理する。
HTML・EPUBの文書スクリプトと外部通信は初期版では無効とする。ローカルにそろった静的コンテンツを対応の中心とし、スクリプトや遠隔資源が必須の資料は互換性制限を表示する。EPUB規格のすべての機能への適合を宣言する設計ではない。
## 主要ユースケース
1. 文書を開く。型と権限を確認し、処理中も画面操作と取消しを受け付け、最初の本文を表示する。
2. j/k、ページ移動、倍率変更で読む。描画中のページは場所と進捗を示し、操作受付を止めない。
3. 目次を出し、階層をたどって項目に移動し、本文へフォーカスを戻す。
4. 見出し単位で進む。目次と本文見出しの出典を区別し、情報がない場合は短く通知する。
5. 設定ファイルを編集して再読込する。不正なら現在の有効設定を維持し、行・項目・理由を表示する。
6. 終了して同じ文書を再度開く。提案機能の位置復元が有効なら最後の位置へ戻る。
## 設計上の未確定事項
| 論点 | 本版の既定案 | 確定時点 |
|---|---|---|
| 配布形態・ライセンス | オープンソース依存の条件を満たす動的リンク配布を候補とする | G0・配布方式決定時 |
| 実際の書籍の種類・最大容量 | 08の代表・負荷コーパスで開始 | G0で実利用文書を追加 |
| PDF構造タグの位置精度 | 公開PDFium APIで見出し→ページ・矩形を抽出 | G0で採否判断 |
| Linuxの配布先拡大・ARM64 | 初期のx64対象とは別の検証枠 | 初期版の品質成立後 |
| 初期画面の配色 | 暗色UI・PDF原色の既定、設定で変更 | 操作レビュー時 |
これらは設計書作成を妨げる未回答事項ではない。実装時の判断を再現できるよう、既定案・判断時点・影響を記録する。
+99
View File
@@ -0,0 +1,99 @@
# 02 全体アーキテクチャ
## 採用構成
Qt 6 / C++20 / Qt Quickをアプリケーション基盤とし、PDFは独立したPDFiumワーカー、HTML・EPUBはQt WebEngineで描画する。CMakeで構成し、両OSの依存バージョンを同じリリース単位で固定する。具体的なQtパッチ版・PDFiumコミットは実装開始時に選ぶ。
Qt PDFはアプリのPDF処理には使わない。PDFiumの公開APIにアクセスする窓口を一つにし、構造タグの読取りも同じワーカーに閉じ込める。Qt WebEngine内蔵のPDFビューアは無効化する。Qt WebEngine自身の内部依存に含まれるPDFiumまで配布物から除去できるとは想定しない。
```mermaid
flowchart TB
UI[Qt Quick Reader Shell] --> APP[Application Services]
APP --> BROKER[Main Process / File Broker]
BROKER --> PDF[Sandboxed PDFWorker / PDFium]
BROKER --> ARCHIVE[Sandboxed ArchiveWorker]
BROKER --> WEB[Qt WebEngine / Isolated Content]
APP --> STORE[Config and State Store]
PDF --> UI
ARCHIVE --> BROKER
BROKER --> ROOT[Document Scoped Resources]
ROOT --> WEB
```
矢印は論理的な要求・応答を表す。PDFWorker・ArchiveWorkerは別OSプロセスであり、Qt WebEngineは独自のマルチプロセス構成を持つ。主プロセスから子プロセスを起動するだけではOSによるアクセス制限は成立しないため、07の隔離条件を別途満たす。
## 責務分割
| 層・要素 | 責務 | 保有しない責務 |
|---|---|---|
| Reader Shell | 本文領域、目次、一覧、ステータス、コマンド欄、アクセシブル名 | 形式固有の解析、任意ファイル読取り |
| Input Router | モード・フォーカス・IMEを考慮したキー解釈、コマンド生成 | PDFページ番号への直接変換 |
| Document Controller | セッション、遷移、位置、戻る/進む、エラーの統括 | 描画エンジンの内部API露出 |
| Navigation Service | アダプターの目次・見出し・リンクを共通モデルへ変換 | 見た目だけによる見出しの推測 |
| Render Scheduler | 可視域優先、先読み、取消し、キャッシュ、世代管理 | ファイルの権限判定 |
| Format Adapter | PDF/HTML/EPUB固有の能力・位置・描画の変換 | 他形式の特例をUIへ要求すること |
| File Broker | 開く操作で許可されたファイル、範囲読取り、仮想資源の提供 | PDF/XML/HTML/画像の主プロセス内解析 |
| PDFWorker | PDFium初期化、文書解析、ページ描画、文字・構造・リンク抽出 | 外部通信、原本文書の書込み |
| ArchiveWorker | ZIP検証・展開、EPUB XML解析、文書資源索引 | 任意パスへの書込み、外部実体参照 |
| Web Content Host | docスキーム、通信遮断、DOMナビゲーション、本文表示 | アプリ全体の権限や設定へのアクセス |
| Settings / State Store | 検証済み設定とローカル読書状態の保存 | 文書由来スクリプトの評価 |
依存の向きはUI→アプリケーション→抽象インターフェース。形式固有コードは抽象インターフェースを実装する。PDFium、WebEngine、OS APIはそれぞれの境界内へ閉じ込める。
## 文書を開く流れ
1. UIがOSファイル選択または明示されたローカルパスをFile Brokerへ渡す。Brokerは通常ファイル・権限・実体パス・容量を確認する。
2. 拡張子と先頭の限定的なシグネチャを照合する。ZIPとEPUBの詳細判定はArchiveWorkerで行う。HTMLは拡張子/MIMEと文字コード情報から扱い、文書本文を主プロセスで解釈しない。
3. 新しいsessionIdとgenerationを発行する。既存の文書は新文書の入口検証が済むまで保持し、失敗時に元の位置へ戻れるようにする。
4. PDFは読取り専用ハンドルまたは範囲読取りチャネルをPDFWorkerへ渡す。HTMLは選択ファイルの親ディレクトリを許可ルート候補にし、ZIP/EPUBは検証済みのセッション用資源集合を構築する。
5. 最初に必要な本文と最低限のメタデータを要求する。全ページのサムネイル、全文字抽出、全ファイルのハッシュ計算が終わるまで本文表示を待たせない。
6. 初期本文が表示できた時点でReadyへ移行し、目次・見出し・先読みを低優先で進める。準備途中の目次は「読込中」とし、「目次なし」と区別する。
7. 新文書のコミット後に旧セッションを破棄し、旧要求・共有メモリ・一時資源を解放する。旧generationの応答は採用しない。
複数文書を常駐させない。切替中の旧文書保持は一時的な例外とし、メモリ圧迫時は最後の表示画像と復帰位置だけを保つ。
## 描画・スケジューリング
PDFiumのAPI呼出しはワーカー内で直列化する。複数スレッドから無保護で呼び出さない。最初はアクティブ文書につきPDFWorker一つとし、追加並列化は性能実測後の変更とする。PDFiumのスレッド条件は[公開API](https://pdfium.googlesource.com/pdfium/+/refs/heads/main/public/fpdfview.h)を参照する。
優先度は「新しい可視ページ→可視ページの高解像度化→移動方向の隣接ページ→逆方向→サムネイル→検索索引/構造抽出」。先読みはまず前後1ページまでとし、キャッシュ余裕とキー操作から調整する。解析中にUIスレッドを同期的に待たせない。
拡大したページは512×512デバイスピクセルのタイルを基本とし、外周に2ピクセルの描画余白を持たせて表示時に切り落とす。CropBox、回転、倍率、DPRを含む変換行列をタイルとリンク・文字の当たり判定で共有する。タイルの継ぎ目・端の欠落は08の必須試験で確認する。
倍率変更中は既存画像を暫定拡大し、新しい倍率で再描画する。旧倍率の応答が新しい画面を上書きしないようgenerationとrenderRevisionを検証する。リサイズだけでは全文書を再解析しない。
PDFの段階描画を利用できる処理は短い区間で取消しを確認する。中断不能なデコードや解析はワーカー監視の期限で扱い、UI側の取消し完了とワーカーの実処理終了を同一視しない。
## キャッシュと資源管理
| 資源 | 方針 | 初期提案 |
|---|---|---|
| PDF画像タイル | バイト数基準LRU、可視タイル優先 | 256 MiB、設定可能 |
| PDFページオブジェクト | 現在・前後を保持し不要分を閉じる | 前後1ページ |
| サムネイル | パネル表示時に可視行から作る | 64 MiB以内で画像予算と調整 |
| WebEngine | 文書セッション用off-the-record profile | 永続Cookie・遠隔cacheなし |
| ZIP展開 | 安全検証済み専用ディレクトリ、一時資源 | 上限は03・07 |
| 読書状態 | 小さなJSON、画像や本文は保存しない | 06を参照 |
PDFの画像キャッシュ予算はアプリ全体のRSS上限を意味しない。WebEngine、GPU、パーサー、一時バッファは別に消費するため、08で子プロセス込みの総使用量を測る。メモリ圧迫時は先読み停止→不可視キャッシュ解放→解像度抑制の通知→文書処理停止の順で対応する。
## 拡張の契約
新形式の追加はFormatAdapterの実装、型判定の登録、能力情報、位置型の追加、共通適合試験への登録で行う。UIは能力情報を見てページ一覧・見出し・検索の可否を決める。画面内へ形式名による条件分岐を散在させない。
キーはCommand Registry内の安定したcommandIdに対応する。新しい操作は同じ入口へ登録し、既定キーがない操作もコマンド欄から実行できる。設定ファイルはスキーマに定義された値だけを受け付ける。
初期版の拡張は配布元がビルド時に組み込むモジュールに限る。第三者の任意コードを読み込むプラグイン機構は将来設計とする。将来導入する場合も、文書がプラグインや設定を自動的に選択・実行することは許可しない。
## 主な依存とビルド境界
| 部分 | 採用案 | 管理単位 |
|---|---|---|
| UI・共通基盤 | Qt Core / Gui / Quick / Qml / Quick Controls、C++20 | 同一Qtリリース |
| HTML・EPUB本文 | Qt WebEngine / WebEngineQuick | Qtと整合したChromium依存 |
| PDF | PDFium公開C API、V8/XFA無効ビルドを基本 | 固定コミット・ビルドオプション |
| ZIP・XML | 維持管理されているライブラリをArchiveWorkerへ組込み | G0でZIP64・文字コード・制限設定を確認し固定 |
| 設定 | TOML 1.0互換パーサー、独自スキーマ検証 | parser版とschema_versionを別管理 |
| IPC | 長さ付きCBORメッセージと制御された共有バッファ | protocolVersion |
ZIP・TOML等のライブラリ名は、制限付き処理・ライセンス・更新頻度を確認してG0で固定する。ライブラリ選定未了でも、受け入れるデータと制限は03・05・06・07で固定する。
+159
View File
@@ -0,0 +1,159 @@
# 03 フォーマット・取込み設計
DocView アプリケーション設計書 | 版 1.0 | 2026-09-19
対象要件: R01-R04、R06、R07、R09、R11、R14。
## 1. 対応方針
PDFを最優先とし、PDFiumを呼ぶ専用PDFWorkerで描画する。HTMLとEPUBの本文はQt WebEngineで描画する。TUI風の配色・枠線・文字中心の操作UIはアプリの領域に適用し、PDFや著者指定の本文レイアウトを変更しない。
| 形式 | 初期リリースの対応範囲 | 制約の伝え方 |
|---|---|---|
| PDF | 読取り専用表示、拡大縮小、回転、ページ一覧、outline、リンク | 対応限界・破損を文書情報とエラーで明示 |
| 単独HTML | ローカルHTMLと許可された文書フォルダー内のCSS・画像・font・リンク | 外部依存資源とスクリプト依存部分は表示できない旨を通知 |
| ZIP化HTML | `index.html` と相対参照資源を内部展開して表示 | 入口不明時は候補選択。危険なアーカイブは取込み中止 |
| EPUB 2 / 3 | DRMのない静的本文、NCX/nav、spine、縦書き、RTL、リフロー、固定レイアウト | 音声・動画・media overlays・動的対話は初期版対象外 |
静的閲覧、DRM非対応、音声・動画非対応は設計上の初期範囲であり、原要件から直接指定された制限ではない。EPUBの全機能適合を宣言するものではない。PDFの編集、注釈追加、フォーム入力、保存による文書変更は行わない。既存の可視要素の表示は、編集機能とは分けて扱う。
## 2. 共通の取込み手順
1. MainのFileBrokerが利用者の選んだファイルを読取り専用で開く。拡張子と固定長のシグネチャを候補判定に利用するが、複雑な内容解析は行わない。
2. PDFはPDFWorker、ZIP/EPUBはArchiveWorkerへ読取り専用handleを渡す。単独HTMLは許可rootと資源IDを登録する。形式の確定は担当の解析器で行う。
3. Workerが返すメタデータは、Mainでも型・長さ・件数・参照IDを検証する。Workerは信頼境界の内側ではない。
4. 新しい文書セッションに世代番号と推測困難なtokenを割り当てる。1ウィンドウのアクティブ文書は1つとする。切替候補の入口検証中は旧文書を維持し、新文書のコミット時に旧tokenと旧要求を失効させる。
5. 最初の表示位置に必要な情報・資源を優先して読み込む。目次とページ一覧は段階的に更新する。取込み・描画は取消可能とする。
6. 正常終了、文書切替、取消、失敗のすべてでhandle、共有画像、仮想URL、展開資源を解放する。画面に残っている旧文書の結果を新文書へ混ぜない。
位置とナビゲーションの形式非依存モデルは [データ・内部契約設計](05-data-contracts.md)、隔離と入力制限は [セキュリティ・異常系設計](07-security-errors.md) に定義する。
## 3. PDFの表示契約
### 3.1 正確性
PDFのページ寸法、CropBox/MediaBox、ページ回転、font、文字配置、画像、透明度、クリッピング、注釈外観を尊重する。座標はPDF文書座標を基準に保持し、device pixel ratio、ズーム、表示回転を一つの変換で適用する。CropBox左下が原点でないページ、文書のRotateと利用者の回転が重なるページも同じ変換行列で扱う。リンク矩形と文字選択の当たり判定はその逆行列を利用する。ページ本文をHTMLへ変換して表示しない。
画面は必要解像度のページ/タイル画像を使用する。倍率は論理画面上で計算し、DPRを一度だけ掛けて描画バッファ寸法を得る。低解像度の暫定画像を拡大表示している間は置換用の再描画を要求する。文字・図形の欠落、既存注釈の欠落、透明度の破綻は「表示できた」と判定しない。注釈の静的外観とフォームwidget外観は必要な描画経路を分けて確認し、編集を無効にしただけで外観も消える状態を避ける。具体的な評価方法は試験設計とPDF技術判断に従う。
未埋込みfontにはOS差が残り得る。代替fontの集合と探索順を管理し、両OSの比較対象に含める。任意PDFに対する完全一致を保証せず、検証済みの互換性範囲をリリースごとに記録する。
### 3.2 注釈・フォーム外観・フォント
通常のページ描画へFPDF_ANNOTを付けるだけではwidgetとpopupは描画されない。保存済みフォーム外観は、PDFiumのform environmentを用いるFPDF_FFLDraw系の描画経路を別途用意し、ページ本文と同じ回転・縮尺・タイル原点へ合わせて合成する。form environmentの初期化、ページ読込/破棄通知、終了処理はワーカー内で管理する。フォーム入力、action実行、JavaScript、保存APIは公開しない。タイル描画とフォーム描画の座標が一致しない場合は、可視領域単位の合成方式へ修正する。[PDFium描画API](https://pdfium.googlesource.com/pdfium/+/main/public/fpdfview.h)、[フォーム描画API](https://pdfium.googlesource.com/pdfium/+/main/public/fpdf_formfill.h)
既存外観が欠落したフォームは、暗黙にユーザー入力や文書保存で補修せず、表示できない可能性を通知する。popup内の注釈本文は選択時の読取り専用補助表示として扱い、紙面外に隠れたコメントの存在を見落とさない。静的注釈、widget、popup、appearance欠落をT18とG-RENDERで個別確認する。
埋込fontを優先し、未埋込みfontは許諾を確認した同梱候補と明示的なOS font索引を用いる。OS font索引はBrokerが管理し、PDFWorkerへは選択済みfontの読取り専用handleまたはバイト列だけを渡す。font探索目的でWorkerの任意パスアクセスを許可しない。共通font集合を固定しても、元fontが欠けた文書の完全一致を保証するものではない。
### 3.3 ナビゲーション
- PDF outlineは目次として保持し、明示destinationsとnamed destinationsをページ位置へ解決する。
- タグ構造に有効なH/H1-H6があれば見出しジャンプを提供する。矩形まで解決できれば見出し位置、ページだけ解決できる場合はページ先頭へ移動し精度を表示する。見出しからページへの対応も解決不能なら該当項目に理由を付け、全体を「見出しなし」としない。座標が得られずページだけ分かる場合はprecision=pageで移動する。有効な構造見出しがない場合は文書内宛先を持つoutlineを代用し、「目次項目移動」と表示する。目次そのものはoutlineを使用する。文字サイズだけから推測した見出しは初期版で生成しない。
- ページ番号は内部の0始まりindexと画面の1始まり表示を分ける。文書のPageLabelsがあれば併記し、`iv` や `1` と物理ページ番号を混同しない。
- ページ内リンクは現在の文書へ移動する。外部HTTP(S)リンクは宛先を提示し、利用者の明示操作で既定ブラウザーへ渡す。Launch action、JavaScript action、自動外部起動は実行しない。
- パスワードが必要な文書は専用入力を求め、値はメモリーだけに保持する。DRM等の未対応保護や未対応機能は理由を返す。
## 4. 単独HTMLとZIP化HTML
### 4.1 入口の選択
単独HTMLは選んだファイルを入口、その親ディレクトリーを初期rootとする。`../assets` 等でroot外の資源が必要な場合、許可範囲を自動拡大しない。「文書フォルダーを指定」で利用者が共通親を指定した場合だけrootを変更する。ブラウザーには絶対ローカルパスを渡さない。
ZIPの入口は次の順序で決める。
| 条件 | 動作 |
|---|---|
| root直下に完全一致の `index.html` がある | 採用 |
| rootにはなく、共通トップディレクトリー直下に `index.html` が一つある | 採用 |
| 上記以外でHTML候補がある | 相対パス一覧からキーボードで選択 |
| HTML候補がない | `E_HTML_ENTRY_MISSING` |
`index.htm`、大小文字違いの名称、複数の深いディレクトリー内のindexは候補一覧で扱い、勝手に最初の候補へ決めない。選択結果は文書fingerprintと関連付ける。ZIPでは入口が深い場所にあってもZIP全体をrootとし、正当な相対参照を維持する。
### 4.2 内部展開
ArchiveWorkerがZIP索引を解析し、安全検査後に必要資源をストリーム展開する。Mainはエントリー記述を再検証し、専用領域へのhandle単位の書込みと実バイト数の上限監視だけを担当する。ZIP/XMLパーサーをMainへ持ち込まない。
最初に入口HTMLを、その後はWebEngineから要求されたCSS、画像、font等を展開する。CRCと上限の検査を終えた資源だけを確定して公開する。展開中の不完全ファイルを描画エンジンへ渡さない。全アーカイブを一括でメモリーへ展開せず、内側のZIPも自動展開しない。
### 4.3 URLと依存資源
資源URLは `doc://<session-token>/<relative-path>` とする。`QWebEngineUrlScheme::Syntax::Host` を使い、文書ごとのhostをoriginとする。URLは当該文書のResourceBrokerだけが解決できる。
| 入力 | 解決規則 |
|---|---|
| HTMLのsrc/href/srcset | 当該HTMLのURLを基準 |
| CSSの `url()` / `@import` | 当該CSSのURLを基準 |
| font-face、SVG資源 | 対応する資源の基準URLと同じ規則 |
| `#fragment` | 現在の資源内の移動先 |
| 相対URLの `..` | URL解決後にroot内なら許可 |
| query | URL情報として保持し、OSファイル名へ直接連結しない |
| 別origin、file URL、UNC、未知のスキーム | 読込みを拒否 |
percent-encodingは一箇所で一度だけ復号する。符号化された区切り・NUL・二重復号に依存するパスを拒否し、ZIPエントリー名の `%` を索引作成時にURL復号しない。HTMLの `base` は同一文書root内に解決できるものだけ許可する。外部baseはCSPと資源gateで拒否し、影響する依存資源の欠落を通知する。黙って別の場所へ読み替えない。
HTML、CSS、SVGの解析はQt WebEngineの描画側で行う。Mainの資源ハンドラーは内容を書き換えるためのHTML/CSSパーサーを持たない。文書タイトルや目次ラベルをアプリUIへ渡す際は文字列として表示し、HTMLとして再解釈しない。
### 4.4 本文の静的閲覧
文書JavaScript、form送信、meta refreshによる自動移動、外部iframe、外部画像・CSS・fontを無効にする。ローカルの許可資源は表示する。欠落画像・fontは代替表示、CSS欠落は本文を維持した警告とする。制限の詳細は文書情報から確認できる。
一般のHTMLメニューバーを自動で目次と決め付けない。`role=doc-toc` 等の意味が明確なnavを目次として読み、それ以外はh1-h6と有効なARIA見出しから「見出し一覧」を作る。見出しがないときは情報なしを示す。文書にIDを恒久追加せず、索引側に一時識別子を持つ。
## 5. EPUB
### 5.1 packageと読書順
ZIPの安全検査後、ArchiveWorkerが `mimetype`、`META-INF/container.xml`、OPF、manifest、spineを解析する。XMLは非検証パーサーとし、外部DTD・外部実体・実体の膨張を禁止する。先頭rootfileを既定packageとし、他のrenditionの存在もメタデータに記録する。
通常の次/前移動は `linear != no` のspine順で行う。`linear=no` は本文リンクや目次から到達でき、ジャンプ履歴で元へ戻れる。manifestの未知媒体には定義済みfallbackを辿り、循環参照と深さを制限する。利用可能なfallbackがなければ該当章をエラーにし、他の章へ移動可能にする。
### 5.2 目次・章・ページの区別
| 優先順/情報 | 使用方法 |
|---|---|
| EPUB 3 navのtoc | 著者の目次として使用 |
| EPUB 2 NCX、またはEPUB 3 nav破損時のNCX | 目次として使用。破損fallback時は通知 |
| spineから生成する章一覧 | 目次がない場合の補助。「章一覧」と明示 |
| XHTMLの見出し | 見出しジャンプ用。章を開く際に段階的索引化 |
| navのpage-list | 出版物のページラベルとして保持 |
| landmarks | 補助ナビゲーションとして保持 |
著者のページラベル、固定レイアウトのページ、リフロー後の画面単位は異なる。リフロー表示では章内進捗を標準とし、画面番号を本の確定ページ番号として保存しない。navとspineの順序が違っても書き換えず、目次操作はnav、通常読書はspineに従う。
### 5.3 縦書き・RTL・固定レイアウト
CSSの `writing-mode`、`text-orientation`、`dir`、rubyを尊重する。`page-progression-direction` はページの進行方向であり、本文の文字方向と別に扱う。「次の読書位置」は論理方向で進め、h/j/k/lによる物理スクロールと分ける。
リフロー型は表示幅・文字サイズ変更後に再レイアウトし、保存した論理位置へ戻す。固定レイアウト型はXHTML viewportまたはSVG viewBoxを基準に拡縮し、本文fontサイズや行幅を強制しない。spine一項目を一ページとして扱い、見開き・RTL・項目ごとのrendition上書きを反映する。混在書籍は章の切替時に表示方式も切り替える。
### 5.4 位置の復元
保存情報はDocumentIdentityの内容識別情報と `packagePath / spineIdref / href / fragment / cfi / progression / textQuote` を基礎とする。対象packageとspineを確定した後の復元順はCFI、ID/fragment、textQuote、章内progression。CFIが壊れていても他の情報から近似復元し、近似であることを通知する。文書fingerprintが変わった場合は一致を確認してから古い位置を使う。
CFI解決と見出し抽出は、原文のDOM構造を基準にする。読書補助の要素を本文DOMへ挿入してCFIの番地を変えない。アプリが管理するUIはQt Quick側へ置く。fontや画像の読込みで位置が動く場合は、レイアウト確定後に同じ論理位置へ再配置する。縦書き・RTLのprogressionは論理読書方向へ正規化する。
### 5.5 保護と破損
`encryption.xml` があるだけでDRMと断定しない。IDPFの標準font難読化を認識して描画用に復元する。それ以外の暗号化方式、保護された本文、パスワード付きZIPは初期版非対応とし、`E_DRM_UNSUPPORTED` または `E_ARCHIVE_ENCRYPTED` を返す。
container/OPFが読めず読書順を確定できないときは開かない。nav破損はNCX・章一覧へfallbackする。spineの一部欠落は欠落章を明示して他の章を読めるようにする。正式な解析に成功していないEPUBを、ZIP内の最初のHTMLを開くだけで成功扱いにしない。
## 6. Qt WebEngineによる制御
`JavascriptEnabled=false` でMainWorldの文書スクリプトを停止する。アプリ所有の固定コードは `ApplicationWorld` に置き、DOMの見出し取得、CFI解決、表示位置測定、スクロールを行う。Qtのworld分離ではDOMへアクセスできる一方、JavaScript変数はworld間で共有されない。[QWebEngineSettings](https://doc.qt.io/qt-6/qwebenginesettings.html)、[QWebEngineScript](https://doc.qt.io/qt-6/qwebenginescript.html)
Qt Quickの `WebEngineView.runJavaScript(script, worldId, callback)` でworldを必ず指定する。文書文字列をコードへ連結せず、検証済みの型付きデータとして渡す。非同期callbackには要求ID・文書世代番号を関連付け、古い結果を捨てる。MainWorld無効化とApplicationWorld実行の組合せは、採用するQt版のG0検証で確認する。[WebEngineView](https://doc.qt.io/qt-6/qml-qtwebengine-webengineview.html)
本文WebEngineViewには汎用QWebChannelやOS APIを公開しない。設定ファイルから任意JavaScriptを実行する機能も初期版には設けない。位置・見出しの測定結果も未信頼データとしてMainで検査する。
## 7. 参照資料
確認日: 2026-09-19。規格の要点を基に、DocView独自の静的閲覧プロファイル・制限・回復動作を設計した。
- [EPUB 3.3](https://www.w3.org/TR/epub-33/) - package、spine、nav、rendition、font難読化。
- [EPUB Reading Systems 3.3](https://www.w3.org/TR/epub-rs-33/) - rootfile選択、file URL、XML処理、script fallback。
- [EPUB CFI 1.1](https://idpf.org/epub/linking/cfi/epub-cfi.html) - EPUB内の論理位置の表現。
- [QWebEngineUrlScheme](https://doc.qt.io/qt-6/qwebengineurlscheme.html) - Host構文とorigin。
- [QWebEngineUrlSchemeHandler](https://doc.qt.io/qt-6/qwebengineurlschemehandler.html) - 独自スキームの資源応答。
+232
View File
@@ -0,0 +1,232 @@
# 04 画面・操作設計
## 1. 設計方針
本文を中心にしたデスクトップGUIとし、アプリの周辺表示に等幅文字・細い区切り線・キーボード中心の操作を使う。PDFの紙面は文字端末に置き換えず、原文の色・文字・図形を保持して描画する。
R01〜R14は提供要件、R15は「デスクトップGUIで、見た目とキーボード操作をTUI風にする」という確定補足に対応する。本書の具体的なキー割当、初期レイアウト、タイムアウトは設計提案であり、利用者から指定された固定値ではない。要件一覧は01、位置・能力・コマンドのデータ契約は05、設定スキーマは06に置く。
初期版は1ウィンドウ・1アクティブ文書とする。ファイルはOSの選択画面、起動引数のローカルパス、ドラッグ&ドロップから開ける。Qt Quick側がUIとキーモードを管理し、PDFiumの表示結果とQt WebEngineの文書内容へ操作を振り分ける。文書内スクリプトにアプリのキー操作を管理させない。
## 2. 画面構成
### 2.1 初期画面
文書未選択時は、中央に「Ctrl+O: 文書を開く」「?: 操作一覧」の2項目を表示する。メニューバー、ツールバー、常時表示の装飾ボタンは置かない。OSのウィンドウ枠は通常どおり使用する。画面の余白、文字、区切り線は設定テーマの色を使う。
### 2.2 閲覧画面
```text
┌─ 目次 ─────────┬──────────────────────────────────────┐
│ 第1章 │ │
│ > 第2章 │ 原文どおりの文書描画領域 │
│ 2.1 │ │
│ 2.2 │ │
├─ ページ一覧 ────┤ │
│ 12 │ │
│ >13 │ │
│ 14 │ │
├────────────────┴──────────────────────────────────────┤
│ book.pdf 13 / 248 125% 内容 │
└───────────────────────────────────────────────────────┘
```
図はすべての領域を表示した例。初期値では**文書領域と1行のステータス行だけ**を表示し、目次とページ一覧は隠す。目次とページ一覧は独立に表示・非表示を切り替えられる。両方を表示した場合は左側の上下に配置し、片方だけなら左側の高さ全体を使う。各幅・高さの下限を確保し、小さいウィンドウではパネルを自動的に増やさない。
- **文書領域**: PDFのページは連続縦スクロールを初期値とする。ページの背景色や埋込フォントは文書どおりに描く。アプリのテーマを変更しても文書の色を勝手に反転しない。
- **目次パネル**: 項目を階層表示する。現在位置を含む最も近い項目を示す「現在位置」と、キーボードで選択中の行を示す「選択位置」を区別する。選択を動かすだけで本文を移動させない。
- **ページ一覧パネル**: PDFは実ページ番号の一覧を基本とする。ページラベルがある場合は「付録-3(実ページ 213)」のように表示できる。サムネイルは必須にせず、将来追加候補とする。HTMLには表示しない。固定レイアウトEPUBではspine項目のページ一覧、リフローEPUBではpage-listがあれば出版物由来のページ参照、なければspine由来の章一覧を表示する。見出しは「ページ一覧」「出版物のページ」「章一覧」と内容に合わせる。
- **ステータス行**: ファイル名、位置、ズーム倍率、フォーカス領域を簡潔に表示する。PDFは実ページ番号/総ページ数、HTMLは見出し名または「文書」、EPUBは章位置を表示する。HTMLやリフローEPUBに固定総ページ数を捏造しない。非表示にできる。
- **通知**: 文書読込中・エラー・利用できない操作は短い通知として表示する。通常の閲覧時に通知領域の高さを占有しない。自動消去される通知と、明示的な対処を要するエラーを区別する。
ページ一覧・目次が提供されない文書で対応キーを押した場合は「この文書にはページ一覧がありません」等を表示する。空のパネルを常設しない。マウスによるスクロール、リンク選択、パネル操作も可能とするが、主要操作はすべてキーボードで到達できる。
### 2.3 読込と異常時
1. 文書を開く操作から、OS標準のファイル選択を表示する。
2. ファイル確定後、読込状態を即座に表示する。目次や総ページ数の確定を待ってから初めて画面を更新する実装にしない。
3. 最初に必要なページ・章を表示し、他ページや目次は準備できた時点で反映する。
4. 読込が失敗した場合は、対象名、原因の分類、利用者が可能な対処を示す。内部例外・スタックトレースは画面に出さない。
5. 既存文書から別文書への切替では、新文書の最低限の表示準備が完了するまで旧文書を保持する。失敗時は旧文書へ戻る。
PDFパスワード入力等の文字入力が必要な画面は、閲覧キーを奪わない専用ダイアログにする。対応暗号方式・パスワード保持方針は03・07に従う。
## 3. 操作状態とフォーカス
Vimの通常モードに相当する閲覧操作を採用するが、文書編集用の挿入モード・ビジュアルモードは作らない。
| 状態 | 入力の扱い | 遷移・終了 |
|---|---|---|
| 内容閲覧 | 文書用キー、リンクのTab移動、マウススクロール | パネルへフォーカス移動、ファイル選択等 |
| 目次フォーカス | 行移動・階層開閉・選択確定 | Enterで本文へ移動して内容に戻る、Escで内容へ戻る |
| ページ一覧フォーカス | 行移動・選択確定 | Enterで対象へ移動して内容に戻る、Escで内容へ戻る |
| キー列待機 | `g`、`z`、`[`、`]`、`Ctrl+W`、数値カウントの後続を待つ | 完成で実行、Escで破棄、期限切れで破棄 |
| 操作一覧表示 | スクロール・閉じる操作のみ | Escまたは?で閉じて元のフォーカスへ戻る |
| コマンド入力 | `:`から入力欄を表示し、文字入力・IME・左右移動・削除を優先 | Enterで検証して実行、Escで取消して元のフォーカスへ戻る |
| ダイアログ/文字入力 | OS標準の編集操作とIMEを優先 | 確定・取消で元のフォーカスへ戻る |
| 読込中 | UIは応答し、不要になった読込の取消または別文書選択を可能にする | 成功、失敗、取消 |
### 3.1 キー処理の優先順位
1. OSまたはネイティブダイアログが占有する操作。
2. IMEの変換・確定、ダイアログ内の文字入力と標準編集操作。
3. 最前面にある操作一覧等の一時表示。
4. 現在のフォーカス領域に限定された割当。
5. アプリ共通割当。
`Ctrl+O`等の共通操作も、文字入力中・IME変換中に無条件で横取りしない。少なくとも `isComposing` 相当の状態にあるキーイベントは閲覧コマンドへ配送しない。コマンド入力、ファイル選択、パスワード入力、OSの標準ダイアログでは、`j`、`k`、数字、`?`等を通常の文字として入力できる。
### 3.2 フォーカス規則
- 文書を開き終えたときは内容にフォーカスする。
- パネルを表示しても直ちに本文位置やフォーカスを変更しない。
- フォーカス中のパネルを隠した場合は内容に戻す。
- `Ctrl+W`の後に`w`を押すと「内容→目次→ページ一覧→内容」の順に循環する。非表示領域は飛ばす。
- リンクの `Tab` / `Shift+Tab` は内容領域内のリンクを順に選択する。リンクがない場合は本文位置を変えず、内容にフォーカスを残す。パネル間の移動は専用キーで行う。
- PDF内・HTML内・EPUB内の読取専用文書要素は、アプリの通常操作モードを勝手に切り替えない。フォーム記入や文書内スクリプトによるキーフックは設けない。
- フォーカス枠、選択行、現在位置は色だけに依存せず、線・記号・濃淡で区別する。
## 4. 既定キーバインド案
キー表記は論理キー名を使う。Shiftを要する大文字は別のキーとして扱い、日本語配列/英語配列で文字を入力する物理キーが異なっても、設定上は `?`、`+` 等の文字で識別する。OSが予約するキーは上書きできない場合があるため、設定チェックと操作一覧で知らせる。
### 4.1 内容領域
| キー | 動作 | 境界・備考 |
|---|---|---|
| `j` / `k` | 小さく下/上へスクロール | 設定可能な移動量。キーリピートに対応 |
| `h` / `l` | 横方向にスクロール | 横の余りがない場合は何もしない |
| `Ctrl+D` / `Ctrl+U` | 表示領域の半分だけ進む/戻る | PDFは下/上。HTML/EPUBは書字方向に沿った読書軸を使う |
| `Ctrl+F` / `Ctrl+B` | 表示領域のほぼ1画面分進む/戻る | 読書軸に沿って移動し、少量の重なりを残す |
| `gg` / `G` | 文書の先頭/末尾へ | EPUBは読書順序の先頭/末尾。パネルでは先頭/末行 |
| `数字G` | PDFの指定実ページへ | 例: `42G`。HTML/EPUBでは利用不可と通知 |
| `J` / `K` | 次/前のPDF実ページへ | PDFのみ。移動先ページの先頭へ |
| `]]` / `[[` | 次/前の見出しへ | 見出し情報がある場合のみ |
| `]c` / `[c` | 次/前のEPUB章へ | EPUBのspine順に移動。非linear項目は通常順送りから除外 |
| `+` / `-` | 拡大/縮小 | PDFは表示倍率、HTML/リフローEPUBはズーム。原文は変更しない |
| `=` | 幅に合わせる | PDFまたは固定レイアウトに適用。リフロー形式では標準倍率へ戻す |
| `Tab` / `Shift+Tab` | 次/前のリンクを選ぶ | リンク数が多い場合もスクロール位置を選択先へ追従 |
| `Enter` | 選択リンクを実行 | 文書内移動・別文書/外部リンクの扱いは基盤仕様に従う |
| `Esc` | 未完成のキー列またはリンク選択を解除。読込・索引待機中は要求を取り消す | 文字入力/IMEの取消を優先し、文書自体は閉じない |
このキー案では `Ctrl+F` をVim流の画面送りに使う。追加提案である本文検索は `/` で開始する。原要件を満たす基本操作と、検索・位置復元等の補助機能を区別する。
### 4.2 目次・ページ一覧
| キー | 目次 | ページ一覧 |
|---|---|---|
| `j` / `k` | 次/前の可視行 | 次/前の行 |
| `h` | 展開中なら折りたたみ、そうでなければ親へ | 何もしない |
| `l` | 子があれば展開、展開済みなら最初の子へ | 何もしない |
| `gg` / `G` | 先頭/末尾の可視行 | 先頭/末尾の行 |
| `Ctrl+D` / `Ctrl+U` | 半画面分の行を移動 | 同左 |
| `Enter` | 選択項目へ移動 | 選択ページへ移動 |
| `Esc` | 内容へフォーカスを戻す | 同左 |
### 4.3 共通操作
| キー | 動作 |
|---|---|
| `Ctrl+O` | 文書を開く |
| `t` | 目次の表示/非表示を切り替える |
| `p` | ページ一覧の表示/非表示を切り替える |
| `zs` | ステータス行の表示/非表示を切り替える |
| `Ctrl+W` `w` | 表示中の領域間でフォーカスを移す |
| `:` | コマンド入力欄を表示する |
| `?` | 現在の設定を反映した操作一覧を表示する |
`t`、`p`、`J`、`K`には文書リーダー独自の役割を割り当てる。Vimの完全互換ではないことを操作一覧で示す。`q`単押しで終了する操作は誤操作を避けて既定に含めず、OS標準のウィンドウ終了を使う。
### 4.4 キー列と設定検証
- 接頭辞待機の既定値は800 msとする設計提案。06に従い200〜3,000 msへ変更でき、0は無期限待機とする。
- 単一キーと、そのキーを接頭辞とする長いキー列を同じ有効コンテキストへ割り当てた場合はエラーにする。例: `g` と `gg` の併存は不可。
- 同じ有効コンテキストで同じキーを複数動作へ割り当てる設定は拒否する。共通割当と領域別割当で同じキー列がある場合は領域別を優先する。利用者設定は(mode, context, keys)が一致する既定定義を置換する。異なる長さの接頭辞衝突は、同時に有効なcontext間でも拒否する。
- 設定変更はファイル全体の構文・値・キー衝突検証が成功した後、一括で適用する。失敗時は最後に有効だった設定を維持し、行番号と修正可能な理由を表示する。
- 数字は内容領域のPDFページ指定にのみ使い、先行ゼロを除き1以上の整数に制限する。入力中の数字は必要時だけ通知する。末尾が `G` 以外または期限切れなら入力列を破棄して、直前キーを別のコマンドとして勝手に実行しない。
- フォーカス変更、文書切替、ダイアログ表示時は未完成キー列を破棄する。
- リピートイベントで接頭辞待機を何重にも作らない。`j`等の連続スクロールはリピートを受け付けるが、パネル表示切替は長押しで点滅しない。
## 5. 形式別の位置と移動
### 5.1 PDF
- 内部のページ番号は0始まり、利用者向けの実ページ番号は1始まりとし、変換箇所を位置型の境界へ限定する。
- `42G` はラベル「42」ではなく42枚目を開く。ページラベルと実ページが異なる場合は、ステータスと一覧に両方を表示する。
- 現在ページは、文書表示領域の中央線に交わるページを基本とする。中央線がページ間の余白にある場合は近いページ、同距離なら前のページとする。これにより、ページ端で次/前操作が不安定になることを避ける。
- 次/前ページは現在ページ番号に±1を適用し、移動先ページの上端へ移動する。末尾・先頭ではその位置にとどまり、短く境界を通知する。
- PDFの目次は文書内のアウトライン情報を利用する。構造タグのH/H1〜H6から有効なページ位置を取得できる場合は、これを見出しジャンプへ用いる。構造タグの有効な見出しがない場合は、文書内宛先を持つアウトラインを代わりに用い、表示名を「目次項目移動」とする。構造タグ由来の見出しとアウトライン由来の項目は共通位置型へ変換しても、情報源の種別を保存する。両方を単純に併合して同一の項目へ二度移動させない。文字サイズ推定・OCRによる見出し生成は行わない。
- ズーム変更時は、表示領域の中心にある文書上の点をできる限り保持する。異なる大きさや回転を持つページへ移動しても、そのページ固有の幾何情報を尊重する。
### 5.2 HTMLとZIP HTML
- 通常HTMLはそのファイルを入口とする。ZIP HTMLは現在表示するHTMLを基準に相対URLを解決する。CSS内の参照はそのCSS自身の場所を基準とする。入口が複数ある場合・存在しない場合の選択規則は基盤仕様に従う。
- `h1`〜`h6` と有効なARIA見出しをDOM順で収集して見出し移動を提供する。`display:none`等で表示されない要素は対象外とする。見出し文言が空の場合は「見出し(レベルN)」と表示する。
- HTMLの目次は、明示的な文書内目次のリンク構造が認識可能な場合にそれを採用する。認識不能でも見出しから作成した**見出し一覧**を利用できる。この一覧を著者作成の目次と混同しないようにラベルを区別する。
- ZIP内の別HTMLへのリンクは同じ出版物コンテキストで開く。ページ一覧をファイル一覧として流用しない。HTMLを開いた順を「章順」として推定しない。
- `gg` / `G` は現在のHTMLリソースの先頭/末尾である。ZIP全体の先頭/末尾とは解釈しない。
- リフロー・ズーム変更後は、先頭可視要素とその要素内の相対位置を基準に位置を回復する。DOMに安定した位置がない場合はスクロール比率へフォールバックする。
### 5.3 EPUB
- 章の通常移動はパッケージのspine順に従う。目次の表示順から章の読書順を推定しない。
- リフロー形式は表示領域と作者の書字方向に従って組版する。横書きの通常文書は縦スクロール、日本語縦書き等はwriting-modeとdirectionに合った読書軸を使う。`j`/`k`と`h`/`l`は物理方向、半画面/1画面移動は読書方向に対応する。次/前章は次/前の通常読書対象spine項目の先頭へ移動する。
- 目次はEPUB 3のnavigation document、EPUB 2はNCXを利用する。両方ある場合の優先順位はEPUB基盤仕様で固定する。
- EPUB内の `h1`〜`h6` と有効なARIA見出しを見出しジャンプの対象とする。章境界を越える見出し移動は、次/前の通常読書対象spine項目の最初/最後の有効見出しへ移る。先読みは必要最小限とし、全章を描画してから読書開始する構成にしない。
- 章単位の位置と章内アンカーを持ち、画面幅の変更でページ数が変わっても同じ箇所に留まる。EPUB page-listのページ番号は書籍が提供した参照であり、現在の画面分割数ではない。
- 固定レイアウトはそのviewportと読み方向を尊重する。日本語縦書き・RTLを含め、作者のCSSと出版物の方向情報を使う。固定レイアウトとリフロー文書では倍率・幅合わせの意味を分ける。
### 5.4 見出し移動の共通定義
「次の見出し」は現在の読書位置より後ろにある最初の有効な見出し、「前の見出し」は現在の読書位置より前にある最後の有効な見出しとする。同一の目的地へ重複する見出しは一度にまとめる。見出しの先頭ぴったりにいる場合、その見出しを再選択しない。PDFで見出しのページだけが確定し座標が確定しない場合は、`precision=page`としてページ先頭へ移動し、その精度を表示する。確定していない位置を正確な見出し位置と扱わない。目的地が不正な目次項目は選択時に理由を示し、他の目次項目まで使えなくしない。
## 6. コマンド契約と文字入力
設定ファイル、キー入力、コマンド入力は同じコマンドIDへ解決する。コマンド入力欄は必要時だけ最下部に現れ、ステータス行が非表示でも利用できる。入力中は等幅文字で `:` と文字列、エラーがあればその理由を示す。
| ID | 既定キー/文字列 | 引数・適用対象 |
|---|---|---|
| `file.open` | `Ctrl+O`、`:open` | 引数なしはファイル選択。`:open "path"` は指定ローカルパスを開く |
| `app.quit` | `:q` | 終了。OS標準の終了操作も使用可 |
| `operation.cancel` | 読込/索引待機中の`Esc`、`:cancel` | 現在の待機要求を取り消す。新文書のコミット前なら旧文書へ戻る。文字入力/IMEの取消が優先 |
| `document.root.choose` | `:root` | 単独HTMLの許可フォルダーをOSの選択画面で指定。新root内へ資源を再解決 |
| `document.info` | `:info` | 形式、検出した制限、欠落資源、エラー詳細を表示 |
| `history.clear` | `:history clear` | 保存した履歴・位置を件数確認後に消去。補助機能 |
| `scroll.down` / `scroll.up` | `j` / `k` | 内容領域の縦スクロール |
| `scroll.left` / `scroll.right` | `h` / `l` | 内容領域の横スクロール |
| `scroll.half_down` / `scroll.half_up` | `Ctrl+D` / `Ctrl+U` | 表示領域の半分 |
| `scroll.page_down` / `scroll.page_up` | `Ctrl+F` / `Ctrl+B` | 表示領域のほぼ全体 |
| `nav.document_start` / `nav.document_end` | `gg` / `G` | 形式別に定める文書範囲 |
| `nav.page` | `数字G`、`:page N` | 1始まりのPDF実ページ番号。範囲外はエラー、暗黙に末尾へ丸めない |
| `nav.page_next` / `nav.page_previous` | `J` / `K` | PDFのみ |
| `nav.heading_next` / `nav.heading_previous` | `]]` / `[[` | 有効な見出しまたは代替目次項目がある文書 |
| `nav.chapter_next` / `nav.chapter_previous` | `]c` / `[c` | EPUBのみ |
| `panel.toc.toggle` | `t`、`:toctoggle` | 目次または見出し一覧の表示を切り替える |
| `panel.pages.toggle` | `p`、`:pagestoggle` | 利用可能なページ/章一覧の表示を切り替える |
| `panel.status.toggle` | `zs`、`:statusbar` | ステータス行の表示を切り替える |
| `focus.next` | `Ctrl+W` `w` | 表示中の領域を循環 |
| `zoom.in` / `zoom.out` | `+` / `-` | 現在位置を保持して拡大/縮小 |
| `zoom.fit_width` | `=` | 固定ページでは幅合わせ、リフローでは標準倍率 |
| `view.rotate` | `:rotate 90`、`:rotate -90` | PDFの表示回転のみ。原本へ保存しない |
| `command.open` | `:` | コマンド入力状態へ移行 |
| `config.reload` | `:reload` | 06で定義する検証と一括再読込 |
| `help.toggle` | `?`、`:help` | 現在のキー設定に対応する操作一覧 |
| `nav.back` / `nav.forward` | `Alt+Left` / `Alt+Right` | 同一文書コンテキスト内の明示的なジャンプ履歴 |
| `search.open` | `/` | 本文検索の入力欄。補助機能の提案 |
| `search.next` / `search.previous` | `n` / `N` | 検索結果。補助機能の提案 |
`nav.back` / `nav.forward` は目次・リンク・指定ページ等のジャンプを戻す。小さなスクロールのすべてを履歴へ積まない。新しい文書を開くことと、同じZIP内HTMLへの移動は、文書コンテキストの境界によって区別する。
コマンドの引数はアプリ内パーサーで処理し、シェルへ渡さない。パスは空白を含む場合に二重引用符で囲み、Windowsのバックスラッシュは文字どおり扱う。シェル変数、ワイルドカード、`!`による外部コマンド実行は展開しない。未知のコマンド、引数不足、余分な引数は入力欄に理由を表示し、文書状態を変更しない。IME確定のEnterはコマンド実行と兼用せず、変換確定後のEnterで実行する。
## 7. 補助機能の提案
本文検索、読書位置の再開、最近使った文書は原要件の独立した追加提案である。実装順の判断により削減できる。これらを省略してもR01〜R15の試験を省略しない。
- **本文検索**: `/`で検索欄を開く。入力中はIMEと文字編集を優先する。Enterで検索し、`n`/`N`で結果を移動する。PDFの既存テキストとHTML/EPUBの表示テキストを対象とし、画像へOCRを適用しない。検索処理は取消可能にし、結果なしと文字情報なしを区別する。
- **読書位置の再開**: 文書を閉じる前の論理位置・倍率をローカル保存し、同じ文書を開いたときに回復する。原文の変更やウィンドウ幅の違いによる復元精度の低下を考慮し、保存先と識別方式は05・06に従う。HTML/リフローEPUBの画面上のページ数を永続キーにしない。
- **最近使った文書**: 初期画面の追加表示として設定で有効化できる。初期画面を履歴の大量表示で埋めない。消去機能と保存上限を持ち、原本を変更しない。
## 8. UI実装前の確認項目
Qt QuickとQt WebEngineの間のフォーカス、IMEのcompositionイベント、日本語/英語キーボード配列、Wayland/X11、Windowsの表示スケール差を実機で確認する。PDFのアウトラインと構造タグ由来の見出し位置は出典を区別して保持する。これらは実装開始時の技術検証対象とする。
+128
View File
@@ -0,0 +1,128 @@
# 05 データ・インターフェース設計
## 設計原則
文書の位置は形式ごとの意味を保持した直和型で表現する。内部のページ番号は0始まり、画面の物理ページ番号は1始まりとする。ページラベルは文字列であり数値として計算しない。ドメインモデルはQtやPDFiumのオブジェクトを直接含めず、非同期操作の全応答に文書の世代を付ける。
## 主要モデル
| モデル | 主なフィールド | 制約・意味 |
|---|---|---|
| DocumentIdentity | documentId、canonicalPath、fileIdentity、size、mtime、contentHash? | documentIdは端末内UUID。物理ファイルIDは利用可能時のみ。ハッシュ未計算を許容 |
| DocumentSession | sessionId、generation、format、state、capabilities | sessionIdは起動ごとの予測困難な識別子。保存IDとは別 |
| DocumentMetadata | title、author?、language?、pageCount?、spineCount? | 未判明と0を区別。文字列長を制限しUIへプレーンテキスト表示 |
| PageInfo | pageIndex、label?、widthPt、heightPt、cropBox、rotation | PDFの元座標と表示変換を保持 |
| TocNode | id、parentId?、title、target?、source、children | sourceはpdf-outline / epub-nav / epub-ncx / html-toc / html-heading / spine |
| Heading | id、level?、title、target、source、precision | level不明可。precisionはexact / page / approximate |
| Link | region?、target、label?、kind | kindはinternal / external / blocked。文字列をコマンドとして実行しない |
| ViewState | location、zoomMode、zoomValue、rotation、panels、focus | ズームはpercent / fit-width / fit-page。本文フォントサイズとは別 |
| ReaderState | documentId、savedLocation、updatedAt、formatVersion | 文書が変わった場合は位置の再解決が必要 |
配列は必要に応じページングして応答する。巨大な目次や数万項目を一度にUIへ投入しない。titleの空白・制御文字を整理する際も原本は変更しない。
## 位置の表現
| 型 | 保存する値 | 再解決 |
|---|---|---|
| PdfLocation | pageIndex、pageLabel?、xPt、yPt、viewRotation、fitMode | PDFユーザー空間の位置をCropBox・回転・DPRで画面へ変換。変更文書ではページ範囲を検証 |
| HtmlLocation | resourcePath、fragment?、domPath?、textQuote?、progression | fragment→DOMと文字列照合→資源内進捗の順。progressionは0〜1 |
| EpubLocation | packagePath、spineIdref、href、cfi?、fragment?、textQuote?、progression | パッケージ内のspine→CFI→fragment→文字列→進捗の順で解決 |
PDFのxPt/yPtは元PDF座標系を明示して保存し、画面左上基準のCSS座標とは混ぜない。常に変換行列と逆行列を使う。CropBox外の指定は最寄りの可視領域へ調整し、精度をpageまたはapproximateとして通知する。
HTML/EPUBのスクロール軸はwriting-modeとdirectionから決める。progressionは読み進めた割合であり、scrollTop/scrollHeightだけでは定義しない。書体や幅が変わっても文字列アンカーを先に試す。レイアウト完了前の仮サイズから復元位置を確定しない。
textQuoteは短い前後文脈を持つが保存を無効化できる。ローカル状態には文書パスと短い本文断片が含まれ得るため、06の履歴消去対象に含める。スクリプト、DOMオブジェクト、任意XPath評価式は保存しない。
## 能力情報
能力はsupported / unavailable / loading / unsupportedの4状態を基本とし、単純な真偽値で「未取得」と「存在しない」を同一視しない。
| 能力 | 意味 |
|---|---|
| pageNavigation | 安定した物理ページに移動できる |
| chapterNavigation | spineや複数HTML資源に沿った移動がある |
| outline | 文書に目次として使える構造がある |
| headings | 本文または構造情報から節の起点を得られる |
| textSearch | 既存文字情報を検索できる。画像PDFではunavailableになる場合がある |
| textSelection | 内容が文字選択を許容し、テキストを取得できる |
| reflow | 本文の文字サイズ・行幅変更を反映できる |
| password | 現在パスワードを要求している |
unsupportedには理由コードを付ける。UIは理由をヘルプ/一時通知に出し、無効ボタンを常時並べない。
## FormatAdapterの論理契約
以下は実装言語に依存しないインターフェース定義であり、アプリケーションコードではない。操作は非同期、sessionIdと取消しトークンを受け取る。
| 操作 | 入力 | 結果 | 特記事項 |
|---|---|---|---|
| probe | 制限付きファイル情報・先頭バイト | format、confidence、理由 | ZIP内部の詳しい判定は隔離側 |
| open | DocumentSource、policy、password? | session、metadata、capabilities | パスワード再入力と破損を区別 |
| getOutline | cursor、limit | TocNodeの部分集合、nextCursor | 同一文書で安定したID |
| getHeadings | resource/page範囲、cursor | Headingの部分集合、完了状態 | 抽出途中のジャンプ要求は待機/取消し可能 |
| resolveLocation | 保存位置、復元方針 | 解決済み位置、precision | 失敗時は先頭へ黙って移さず通知 |
| navigate | target、alignment | 到達位置、renderRevision | jump履歴への記録はControllerが担当 |
| renderPdfTiles | page、matrix、clip、revision | TileBatch | PDF専用描画ポート。WebEngineは可視域通知ポートを使う |
| setViewport | size、DPR、readingStyle | layoutRevision | reflow後に位置再解決 |
| findText | query、direction、cursor、limit | match位置、nextCursor | 提案機能。平文検索が既定 |
| close | sessionId | released | 繰返し呼出しは無害 |
共通契約は操作の意図をそろえるためのもので、PDFの画像タイルとHTMLのDOMを同一の描画データへ変換しない。
## コマンドと入力の境界
Command RegistryはcommandId、引数スキーマ、実行可能モード、必要な能力、取消し可否を持つ。Input Routerからはキー名そのものではなく検証済みCommandを受け取る。マウス操作も同じCommandを経由する。
コマンド入力欄は定義済み文法だけを解釈する。シェル、外部コマンド、JavaScriptの実行機能は持たない。パスの引用符は構文として扱うが、変数展開・コマンド置換・グロブ実行はしない。`:open`のパスは明示操作による新しい許可対象としてBrokerで検証する。
## プロセス間メッセージ
IPCは長さ付きCBORフレームをローカル専用チャネルで送る。外部待受ポートを開かない。子プロセスの標準出力を信頼済みのコマンドストリームとして解釈しない。実装時にパイプ/ローカルソケットを選び、OSごとのアクセス制御を適用する。
| フィールド | 内容 |
|---|---|
| protocolVersion | 整数。未知版は接続を拒否し依存不整合を報告 |
| requestId | セッション内一意、応答・取消しの対応づけ |
| sessionId / generation | 文書と世代。旧文書の応答を破棄 |
| operation | ホワイトリスト列挙。任意メソッド名を実行しない |
| payload | 操作別の有限スキーマ。整数範囲・文字列長・配列数を検証 |
| result / error | どちらか一方。errorはコード、再試行可否、利用者向け説明キー |
制御フレームの上限は1 MiB、PDFタイル/資源データは別の分割チャネルで1片8 MiB以下を提案する。1タイルの最大寸法・stride・ピクセル形式・バッファ長を受信側で再検証し、乗算の桁あふれを拒否する。数値は08で測定し、07の入力制限と合わせて変更する。
共有メモリを使う場合はBrokerが割り当てた短命のバッファIDだけをワーカーへ渡し、サイズ上限・所有セッション・読書き方向を管理する。ワーカーが示す任意パスやハンドル値を主プロセスの権限で開かない。close/取消し/クラッシュ時に参照数を解放し、解放済み応答を表示しない。
## 状態と遷移
```mermaid
stateDiagram-v2
[*] --> Empty
Empty --> Opening: open
Ready --> Opening: open another
Opening --> PasswordRequired: encrypted
PasswordRequired --> Opening: submit
Opening --> Ready: initial content
Opening --> Failed: invalid or unsupported
Ready --> Recovering: worker crash
Recovering --> Ready: explicit retry
Recovering --> Failed: retry fails
Ready --> Closing: close
Closing --> Empty: resources released
```
OpeningとPasswordRequiredの取消しは、旧文書があればReadyへ戻し、なければEmptyへ戻す。図のFailedはエラー状態を表し、元文書を保持している場合はエラー表示後に旧Readyへ復帰できる。終了要求はどの状態からでも受け付ける。
ReadyにはnavigationIndex=loadingという独立した副状態を持てる。スクロール・ズームのたびにOpeningへ戻さない。パネル表示は文書状態と独立し、読み込み途中に目次を開いても競合しない。
## 整合性と取消し
- 文書切替でgeneration、倍率や向きの変更でrenderRevision、HTML/EPUBの再配置でlayoutRevisionを進める。
- PDFium処理は直列キューとし、取消し要求は予約済み処理を除去する。実行中の処理は安全な区切りで止める。
- closeは読取チャネルの失効→新規要求停止→実行処理取消し→ワーカー終了→一時資源回収の順。停止不能時は期限付きで強制終了する。
- 読書位置の保存は最後に画面へ反映された位置だけを対象とする。未到達のジャンプ要求で保存位置を更新しない。
- 戻る/進む履歴は明示ジャンプとリンク移動を単位に記録する。連続スクロールの全フレームは記録しない。
## エラーの責務
ワーカーは技術的な原因と位置を返し、Controllerは回復操作へ翻訳する。UIへOSの例外やスタックトレースをそのまま表示しない。利用者が必要とするファイル名・ページ・理由は表示し、ログ側ではパスや本文を原則省く。コード体系と回復策は07を正とする。
+150
View File
@@ -0,0 +1,150 @@
# 06 設定・保存設計
## 設定方式
利用者が編集する設定はUTF-8のTOMLとし、`config.toml`一つを入口にする。初期設定の全項目を記載しなくても、差分だけで上書きできる。`schema_version = 1`で構造の版を識別する。設定から任意のコードやシェルを実行しない。
優先順位は「組込み既定値 < 利用者設定 < 起動引数で明示した一時上書き」。`--config PATH`は読む利用者設定ファイルを置き換え、二つの利用者設定を暗黙に合成しない。起動引数はファイルパス、設定パス、初期ページ等の限定された項目だけを許可する。読書状態は表示位置の復元にのみ使い、キーや配色の設定より優先させない。
文書と同じディレクトリにある設定やZIP内部の設定は自動読込みしない。ファイルを開くだけでキーや許可範囲が変更されることを防ぐ。
## OSごとの配置
| 種類 | Linux | Windows |
|---|---|---|
| 設定 | `$XDG_CONFIG_HOME/docview/config.toml` | `%APPDATA%\DocView\config.toml` |
| 読書状態・履歴 | `$XDG_STATE_HOME/docview/state.json` | `%LOCALAPPDATA%\DocView\state\state.json` |
| キャッシュ | `$XDG_CACHE_HOME/docview/` | `%LOCALAPPDATA%\DocView\cache\` |
| ログ | `$XDG_STATE_HOME/docview/logs/` | `%LOCALAPPDATA%\DocView\logs\` |
| ZIP等の作業領域 | cache配下の`sessions/<sessionId>/` | cache配下の`sessions\<sessionId>\` |
Linuxの未設定/空のXDG変数は順に`~/.config`、`~/.local/state`、`~/.cache`へフォールバックする。相対パスのXDG値は仕様に従い無効とする。Windowsは文字列置換した環境変数だけに頼らず、Known Folders APIでRoamingAppDataとLocalAppDataを解決する。QtのQStandardPathsは共通の経路解決を補助するが、AppConfigLocationのWindows既定はLocal側であるため、上記Roaming側設定と混同しない。[XDG仕様](https://specifications.freedesktop.org/basedir/latest/)、[QStandardPaths](https://doc.qt.io/qt-6/qstandardpaths.html)、[Windows Known Folders](https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid)を参照。
すべてユーザー専用領域とし、Linuxではディレクトリ0700・状態ファイル0600相当、Windowsでは現在ユーザー中心のACLを継承・確認する。インストール先、実行ファイル隣、現在ディレクトリへ暗黙に設定を書かない。
## 設定項目
数値は初期提案であり、08の性能測定に合わせて調整する。セキュリティの絶対上限・隔離の有効化は利用者設定で解除できない。
| キー | 型・既定値 | 範囲/効果 |
|---|---|---|
| schema_version | integer = 1 | 未知の将来版は適用しない |
| ui.theme | string = "dark" | dark / light / system |
| ui.status_bar | boolean = true | 状態1行を表示。zsで一時切替 |
| ui.toc_visible | boolean = false | 初期の目次表示 |
| ui.pages_visible | boolean = false | 初期のページ/章一覧表示 |
| ui.font_size | number = 14 | 10〜28論理px、補助UIの文字サイズ |
| ui.panel_width | integer = 280 | 160〜600論理px、画面幅でも上限を制約 |
| pdf.zoom_mode | string = "fit-width" | fit-width / fit-page / percent |
| pdf.zoom_percent | integer = 100 | 25〜800、percent時のみ使用 |
| pdf.preserve_colors | boolean = true | 本文の原色を維持。falseは表示フィルターを使う追加提案 |
| reading.font_size | number = 18 | 12〜40論理px、流し込みHTML/EPUBの利用者表示設定 |
| reading.line_height | number = 1.6 | 1.0〜2.5。著者指定維持との切替を持つ |
| reading.use_publisher_style | boolean = true | 既定は著者スタイル。利用者上書き時もwriting-mode等を保持 |
| keys.sequence_timeout_ms | integer = 800 | 0(無期限)または200〜3000、複数打鍵の待機。IME合成中は解釈しない |
| performance.tile_cache_mib | integer = 256 | 64〜512、PDFタイル予算。総RSS制限ではない |
| performance.prefetch_pages | integer = 1 | 0〜3、前後ページ。メモリ圧迫時は自動抑制 |
| privacy.remember_position | boolean = true | 読書位置を保存する提案機能 |
| privacy.recent_documents | integer = 20 | 0〜100、0で最近使った一覧を無効化 |
| privacy.store_text_anchor | boolean = false | 本文断片の永続保存。無効でもCFI/fragment等で復元可能 |
`pdf.preserve_colors=false`の表示フィルターは原色を変えるため、正確な再現性の受入はtrueで行う。初期実装でフィルターを採用しない場合、falseを未対応として検証エラーにし、黙って受け付けない。
## 設定例
以下は設計上の設定例であり、アプリケーションのプログラムではない。通常は変更した項目だけを記述する。
```toml
schema_version = 1
[ui]
theme = "dark"
status_bar = true
toc_visible = false
pages_visible = false
font_size = 14
panel_width = 280
[pdf]
zoom_mode = "fit-width"
zoom_percent = 100
preserve_colors = true
[reading]
use_publisher_style = true
font_size = 18
line_height = 1.6
[keys]
sequence_timeout_ms = 800
[performance]
tile_cache_mib = 256
prefetch_pages = 1
[privacy]
remember_position = true
recent_documents = 20
store_text_anchor = false
[[keybindings]]
mode = "normal"
context = "content"
keys = ["j"]
command = "scroll.down"
[[keybindings]]
mode = "normal"
context = "global"
keys = ["t"]
command = "panel.toc.toggle"
[[keybindings]]
mode = "normal"
context = "content"
keys = ["]", "]"]
command = "nav.heading_next"
```
## キー設定の仕様
1. 一つのキーは`j`、`J`、`Ctrl+D`、`Alt+Left`、`Esc`のような標準表記とする。大文字と小文字を区別し、印字キーはキーボード配列上の文字として解釈する。物理スキャンコードへの依存は初期版で導入しない。
2. `keys`配列は順番に押すキーを表す。`["g", "g"]`、`["Ctrl+W", "w"]`のように記述する。修飾キーの同時押しは一要素に含める。
3. `mode`はnormal / command / search / dialogとし、入力欄で合成中のIMEイベントは対応付けより優先する。`context`はglobal / content / toc / pagesとする。commandとsearchの文字入力はユーザーキー設定で奪わない。
4. 適用キーは(mode, context, keys)で同定する。利用者設定の同じ組は既定定義を一つ置換する。既定を無効化する場合は`command = "unbound"`とする。同じファイルに同じ組が二つあれば検証エラーとする。
5. 現在のパネルcontextを先に、globalを次に評価する。通常モードのtはどのパネルからも目次を切り替えるが、文字入力中のtは入力文字になる。
6. `g`と`gg`のように実行コマンドが互いに前方一致する定義は、同時に有効になるcontext間も含めて拒否する。既定のgは未実行の接頭辞なのでggと共存できる。部分列の待機中に不一致のキーが来たら接頭辞を破棄し、新しいキーを一度だけ再評価する。ただし数字Gの入力中は不一致キーも消費し、意図しない操作を起こさない。
7. Escは複数打鍵・検索・コマンド・一時モードを抜ける回復手段として予約し、再割当て不可とする。OSが予約するキーやファイル選択ダイアログの標準操作はアプリが横取りしない。
8. コマンドID、引数、必要能力を検証する。存在しないIDは保存成功として扱わない。現在の文書で能力がない操作は理由を短く表示する。
数値付きページ移動`42G`はInput Routerの限定文法として扱い、数字全般の自由なキー再割当てとは衝突させない。詳細なキー一覧・文法・フォーカス動作は04を正とする。
## 再読込みとエラー
設定は起動時と`:reload`時に読み込む。初期版はファイル監視による自動適用を行わず、保存途中の内容による揺れを防ぐ。最大設定サイズは1 MiB、入れ子深度やキー数にも上限を設ける。
読込み→TOML解析→型と値域→キー衝突→コマンド能力→適用計画の順に検証する。全検証が成功した場合だけ設定スナップショットを一括交換する。失敗した場合は直前の有効設定を保持し、行番号・キー・修正理由を表示する。初回起動なら組込み既定値を使う。
未知のキーは綴り間違いを見逃さないためエラーとする。将来版のschema_versionは読取り専用の説明を表示し、利用者ファイルを自動で書き換えない。移行が必要な場合は変換案とバックアップを用意し、明示操作で保存する。
配色・パネル・キー・キャッシュ予算は即時反映し、再配置を伴う文字サイズは現在の論理位置を保持して反映する。適用中はキー列を取消し、二重実行を防ぐ。再起動が必要な項目を将来追加するときは、効果発生時点をスキーマに明示する。
## 読書状態と履歴
状態は`state.json`へ保存し、`state_version`、文書ID、ファイル識別情報、最終位置、倍率、更新時刻を持つ。アプリ設定は自動で書き換えない。パネルの一時切替はセッション内状態であり、次回起動はconfigの既定へ戻す。
元ファイルの完全ハッシュは初期表示を妨げない低優先処理にする。最初は正規化したパス・サイズ・更新時刻・利用可能なファイルIDで候補を探す。文書の同一性が確認できない場合は「以前の位置を復元できない」とし、似たファイルへ位置を自動転用しない。ファイルが変更された場合は保存アンカーを再検証し、曖昧なら位置選択を提示する。
位置は停止後2秒のdebounceと最大10秒間隔を提案し、正常終了時にも保存する。急な終了では直前10秒程度の位置更新を失う可能性がある。保存先と同じディレクトリ内の一時ファイルへ書き、flush後に原子的な置換を使う。前回正常版を1世代だけ残し、破損時はバックアップへ戻す。
アプリはユーザーごとに一つの状態Writerを持つ。二重起動は既存インスタンスへファイルオープン要求を送る。IPCは同じユーザーのプロセスだけに制限する。ロックが得られない場合は読書専用モードで起動し、設定・状態を競合上書きしない。
`:history clear`は最近使った一覧、保存位置、textQuote、状態バックアップを消去する提案コマンドとする。実行前に対象件数を表示し、文書原本は含めない。`privacy.remember_position=false`は将来の保存を止める設定であり、既存データの消去とは分ける。
## 一時データとログ
ZIP/EPUB作業領域はsessionIdごとに分離し、正常終了で削除する。起動時は自分の未使用セッション領域だけを回収し、他インスタンスが使用中の領域を削除しない。削除対象の実体・所有者・ロックを確認し、シンボリックリンクを追跡しない。
ログは日時、エラーコード、形式、処理時間、匿名の要求IDを中心とし、原則として本文・パスワード・完全パスを記録しない。ローカルに5 MiB×3世代を提案する。外部送信は行わない。詳細診断を保存する機能を将来設ける場合も、利用者が内容を確認して書き出す方式とする。
キャッシュは削除しても原本文書と設定に影響しない。履歴や設定の破損で本文が開けなくならないよう、保存処理のエラーは表示処理から切り離す。
+166
View File
@@ -0,0 +1,166 @@
# 07 セキュリティ・異常系設計
DocView アプリケーション設計書 | 版 1.0 | 2026-09-19
対象要件: R01-R04、R09、R11、R12、R14。文書を開く操作が、原本の変更、文書外のファイル読取り、外部送信、任意プログラム実行へ広がらないようにする。
## 1. 信頼境界
ローカルにあるPDF、HTML、ZIP、EPUBも未信頼入力として扱う。文書の名前・本文・メタデータに書かれた指示をアプリの命令や設定として解釈しない。文書処理から設定ファイルを読み替えたり、拡張コードを自動ロードしたりしない。
| 領域 | 許可する処理 | 禁止する処理 |
|---|---|---|
| Main / FileBroker | UI、検証済み設定、利用者が開いたファイルのhandle管理、限定範囲のIO、IPC検証 | PDF、ZIP、EPUB XML、HTML、CSS、画像の内容解析 |
| PDFWorker | 渡されたPDFの解析・描画、限定的なfont資源利用 | ネットワーク、任意ファイル、原本への書込み、外部起動 |
| ArchiveWorker | 渡されたZIPの索引・展開、EPUB構造XML、font難読化の処理 | 任意パス作成、ネットワーク、XML外部実体、外部起動 |
| WebEngine文書側 | 文書セッション内資源の表示、アプリ管理のDOM位置操作 | 設定・履歴・任意OS API、別文書、遠隔通信、文書JavaScript |
MainがWorkerから受けた索引・ラベル・URL・画像を無条件に信用すると境界が失われる。型、長さ、整数範囲、参照先、文書世代番号を検査し、描画画像についてもstride、幅、高さ、共有バッファ長を照合する。制御IPCの上限は1 MiB、データチャネルは1チャンク8 MiBを初期値とし、詳細は [データ・内部契約設計](05-data-contracts.md) に従う。
## 2. OSによる隔離と配布条件
別プロセスに分けただけではファイルやネットワークへのアクセスを防げない。PDFWorkerとArchiveWorkerにはOSによる制限を設定する。実装前のG0では起動可能性だけでなく、境界の外にある試験ファイルへのアクセス拒否、通信拒否、子プロセス起動拒否、資源上限超過時の停止を実証する。未達の対象OS版は配布しない。
| OS/実行系 | 採用方針 | G0で確かめること |
|---|---|---|
| Linux Worker | seccompとLandlockを組み合わせる。要件を満たせない環境は同等の隔離launcherを利用 | 必要ABI・syscall、font読込み、handle受渡し、全threadへの制限、socket・process作成・root外IOの拒否 |
| Windows Worker | AppContainerを権限境界とし、Job Objectで寿命・資源・子プロセスを管理 | network capabilityなし、必要handleだけの継承、font依存、DACL、停止・再起動 |
| Qt WebEngine | Qt/Chromiumのsandboxを有効のまま利用 | 両OSの配布形態、実際のrenderer隔離、必要なLinux kernel機能、無効化フラグの検知 |
Landlock単独で全種類の通信制限が揃うとは想定せず、socket等の操作はseccomp側でも制限する。Job Object単独もファイル権限の代替にしない。Workerの許可資源は読取り専用文書handle、限定IPC、必要なfont/runtimeのみとし、ホーム全体や任意ディレクトリーを許可しない。[Linux Landlock](https://docs.kernel.org/userspace-api/landlock.html)、[AppContainerの起動](https://learn.microsoft.com/en-us/windows/win32/secauthz/implementing-an-appcontainer)
`--no-sandbox`、`QTWEBENGINE_DISABLE_SANDBOX` 等で保護を外す運用を製品の回避策にしない。sandboxが必要条件を満たさない場合は `E_SANDBOX_UNAVAILABLE` を表示し、該当形式を開かない。Qt WebEngineはrendererのsandboxを提供するが、独立したPDFWorkerやArchiveWorkerの隔離までは提供しない。[Qt WebEngine Platform Notes](https://doc.qt.io/qt-6/qtwebengine-platform-notes.html)
## 3. ZIP・一時資源
### 3.1 検証と書込み
ArchiveWorkerは展開前に索引を検査し、Mainも公開対象entryを再検証する。Mainは検証済みの相対名を文書rootのdirectory handleに結び付け、OSのNOFOLLOW相当の手段でファイルを開く。文字列の前方一致だけでroot内と判定しない。Workerはディスク上の任意展開先を指定できない。
- 絶対パス、ドライブ名、UNC、NUL、`..` セグメント、バックスラッシュ、ADSを作るコロン、Windowsデバイス名を拒否する。
- symlink、hardlink、junction/reparse point、device、FIFOを拒否する。通常ファイルとディレクトリーだけを受け入れる。
- 正規化後の重複、大小文字・Unicode正規化による衝突、ファイルとディレクトリーの衝突、末尾のドット/空白で曖昧になる名前を拒否する。
- アーカイブの所有権、実行権限、ACLを再現しない。ユーザー専用の新しい一時ディレクトリーを作り、他セッションの内容を混ぜない。
- 申告サイズだけでなく、Mainへ届いた実展開バイト数を計測する。各entryのCRC・長さ・完了状態が正常になるまで仮想URLから公開しない。
- 内側のZIPは再帰展開しない。エラー時に展開途中ファイルを残して再利用しない。
これらはZIP Slip、リンクを使う脱出、解凍による資源枯渇を別々に防ぐための方針である。[OWASPアーカイブ試験](https://wstg.owasp.org/latest/4-Web_Application_Security_Testing/10-Business_Logic/09-Upload_of_Malicious_Files/)
### 3.2 上限の初期提案
数値は原要件にはなく、代表書籍と負荷試験で妥当性を確認する設計値である。初期版では06の利用者設定による緩和を提供せず、ビルド時の有限上限とする。値の変更時は安全試験を再実施し、パス・リンクの検査そのものは無効化できない。
| 上限対象 | 初期値 | 判定する場所 |
|---|---:|---|
| ZIP entry数 | 20,000 | Worker索引、Main索引受信 |
| 1資源の実展開量 | 256 MiB | WorkerとMainストリーム受信 |
| 1文書の実展開量 | 2 GiB | WorkerとMainセッション合計 |
| path階層 | 64 | WorkerとMain |
| pathのUTF-8長 | 1,024 bytes | WorkerとMain |
| 膨張率 | 200倍。実展開量32 MiB超のentryに適用 | Workerの入力消費量/出力量 |
| EPUB構造XML 1資源 | 16 MiB | Worker |
| XML深さ/要素数 | 128/200,000 | Worker |
| 同時資源展開 | 2 | スケジューラー |
| 1資源の展開時間 | 10秒で継続表示、30秒で取消 | Main watchdog |
| PDFの1要求 | 30秒で長時間処理を通知、120秒でワーカー停止 | Main watchdog。操作取消は待たずに受理 |
| PDF/Archiveワーカーのメモリー | 1プロセスあたり1.5 GiBを初期の停止上限とする | OS制限とMain監視。通常時の全体1 GiB目標とは別 |
絶対サイズを主要防御とし、圧縮率だけで判定しない。CPU・メモリーはWorkerおよびWebEngineの資源監視でも制限する。WebEngineでの画像デコード前に全ピクセル数を安全に判定できるとは想定せず、rendererの応答監視とメモリー超過時の停止を併用する。WebEngineの停止閾値は初期1.5 GiB/rendererを提案し、監視間隔による瞬間的な超過はあり得る。展開先の空き容量不足は上限超過と別のエラーにする。上限を変更した版での再試行時も入口から同じ検査を行う。
終了時はそのセッションの管理領域だけを削除する。次回起動時の残骸清掃は、所有マーカーと管理下の実体を確認して行う。文書由来のパスを再帰削除の起点にしない。
## 4. HTML・EPUBの隔離
### 4.1 独自URLと資源提供
`doc://<session-token>/...` を `Syntax::Host` で起動初期に登録し、QQuickWebEngineProfileへ専用scheme handlerを入れる。文書ごとのoff-the-record profileを使い、Cookie、localStorage、WebEngine履歴を文書間で継承しない。文書を閉じるとprofileとtokenを破棄する。
`LocalAccessAllowed`、`ContentSecurityPolicyIgnored`、`ServiceWorkersAllowed`、`CorsEnabled`、`FetchApiAllowed` は付けない。`NoAccessAllowed` によって全資源が別originになる構成も使わず、同一文書内のCSS/font等の互換性を保つ。安全なschemeフラグだけに依存せず、handlerはセッションtoken・initiator・method・相対パス・resource kindを照合する。
許可methodは読取要求のみ。空initiatorはアプリが発行した既知のトップレベル要求に限定して受け付ける。文書から別tokenを当てられても、tokenの一致と許可資源集合の両方で拒否する。文書URLのtokenを長期履歴やログへ残さない。
### 4.2 3箇所の制御
| 制御点 | 役割 |
|---|---|
| QWebEngineUrlRequestInterceptor | Chromiumのネットワーク層に届く前に、同一文書の許可資源以外を拒否 |
| QWebEngineUrlSchemeHandler | 実体root・許可entry・MIME・サイズ・寿命を検査し、読取専用資源を応答 |
| WebEngineView.navigationRequested / newWindowRequested | トップレベル遷移、自動遷移、外部ウィンドウ作成を制御 |
request interceptor内で展開完了を同期的に待たない。許可判定を短時間で終え、資源の非同期応答をhandlerへ委ねる。欠落や取消ではrequest jobを失敗として終了し、jobの破棄に合わせてQIODeviceの寿命も終える。
外部HTTP(S)リンクはユーザーが選択したときだけMainへ通知し、宛先を表示したうえで「外部ブラウザーで開く」操作によりOSへ渡す。WebEngine内で外部サイトへ遷移しない。未知のスキーム、OSコマンド、メール送信、file URLを一般のURLとして起動しない。
### 4.3 本文スクリプトとCSP
MainWorldのJavaScriptを無効にし、ApplicationWorldのアプリ固定コードだけで読書位置・見出しを制御する。汎用WebChannelを公開せず、結果は限定されたcallbackから返す。document内の文字列をJavaScriptソースへ埋め込む方法は使わない。
scheme handlerの追加応答ヘッダーでCSPを設定する。基本方針は `default-src 'none'`、`script-src 'none'`、`connect-src 'none'`、`object-src 'none'`、`frame-src 'none'`、`form-action 'none'`。画像・fontは同一文書内、CSSは同一文書内と著者のinline styleを許可する。画像のdata URLはimage用途に限定して許可し、top-level documentやSVG scriptの抜け道にしない。`base-uri 'self'` とナビゲーションgateを組み合わせる。
応答ヘッダーのAPIはQt 6.6以降に存在する。CSPが独自schemeとApplicationWorldで意図どおり動くことをG0で確認し、CSPを迂回するフラグを付けない。CSPを差し込むためにMainで本文HTMLを解析・改変する設計は採らない。[QWebEngineUrlRequestJob](https://doc.qt.io/qt-6/qwebengineurlrequestjob.html)
カメラ、マイク、位置、通知、screen capture、clipboard、file picker、download、protocol登録は文書からの要求を拒否する。DNS prefetch、link auditing、local-file access、local-to-remote access、WebGL、plugins、WebEngine内蔵PDF viewerを無効にする。WebEngineが要求する権限は既定拒否とし、document内の設定で上書きしない。
## 5. PDFに固有の制約
PDFiumはV8とXFAを無効にした構成を基本とする。JavaScript action、Launch action、添付実行、外部自動起動は提供しない。ファイルは読取り専用で開き、PDF保存APIをアプリの操作として公開しない。既存の注釈・フォーム値の静的外観は描画するが、文書へ入力しない。
暗号化PDFのパスワードはその場の入力だけで受け取り、設定・履歴・ログ・クラッシュ情報に残さない。明示的な再入力を許し、誤りを破損ファイルと混同しない。署名が見えるPDFでも、署名検証を実施したと表示しない。
ワーカーへ渡すfontは許可集合に限定する。未埋込みfontに対応するためホーム全体を読めるようにしない。悪意あるfont、巨大画像、再帰的なPDF構造もPDFWorkerの時間・メモリー制限で扱う。
## 6. エラーの共通モデル
エラーは `code / severity / operation / documentGeneration / resourceId / retryable / userMessage / diagnosticId` を持つ。本文や秘密を含む生データをmessageに連結しない。短い説明と復帰操作を主表示とし、詳細は必要時に開く。繰返し発生する欠落資源は件数をまとめ、キー操作を通知で奪わない。
| ID | 表示と復帰 |
|---|---|
| E_OPEN_FAILED | ファイルを開けない。場所・権限を確認し別ファイルを選択 |
| E_FORMAT_UNSUPPORTED | 対応形式でない、またはシグネチャ不一致。元文書へ戻る |
| E_PASSWORD_REQUIRED | PDFパスワードを入力。取消で元文書へ戻る |
| E_PASSWORD_INVALID | パスワードが一致しない。再入力可能 |
| E_PDF_CORRUPT | PDFを解析できない。元文書へ戻る |
| E_PDF_FEATURE_UNSUPPORTED | 動的XFA等の未対応内容。静的部分が読める場合も制限を通知 |
| E_SANDBOX_UNAVAILABLE | 安全な文書処理環境を作れない。該当形式の読込みを停止 |
| E_ARCHIVE_UNSAFE_PATH | root外へ出るパス・リンク・衝突を検出。取込みを停止 |
| E_ARCHIVE_LIMIT | 件数・実展開量・深さ等の上限。項目と上限を提示 |
| E_ARCHIVE_CORRUPT | 索引・長さ・CRCが破損。取込みを停止 |
| E_ARCHIVE_ENCRYPTED | パスワード付きZIPは非対応 |
| E_HTML_ENTRY_MISSING | HTMLの入口がない。入口候補があれば選択 |
| E_RESOURCE_MISSING | 一部資源がない。代替表示で読書を継続 |
| E_RESOURCE_BLOCKED | root外・遠隔資源等を遮断。読める部分を維持 |
| E_EPUB_PACKAGE_INVALID | package/読書順が確定できない。読込みを停止 |
| E_EPUB_NAV_INVALID | nav破損。NCXまたは章一覧へfallback |
| E_EPUB_SPINE_MISSING | 該当章が欠落。他の章へ移動可能 |
| E_DRM_UNSUPPORTED | 未対応の暗号化・保護方式。解除処理は行わない |
| E_LOCATOR_UNRESOLVED | 正確な位置に戻れない。近い位置へ戻ったことを通知 |
| E_WORKER_TIMEOUT | 処理期限超過。取消/再読込み/別文書 |
| E_WORKER_CRASH | Worker停止。UIを維持し、利用者の明示操作で再試行。繰返す停止ではセッションを閉じる |
| E_STORAGE_FULL | 一時領域・状態保存先の空き不足。展開を止めて清掃 |
| E_CONFIG_INVALID | 設定エラー。現在の有効設定を維持 |
| E_STATE_SAVE | 読書状態を保存できない。閲覧を継続し保存失敗を通知 |
ファイル切替中の失敗は、コミット前なら元文書と元位置を維持する。操作取消は通常結果として扱い、エラー通知を出さない。深刻な境界違反や繰返すクラッシュでは再試行ループに入らず、その文書セッションを閉じる。
## 7. 診断情報・更新
ログは時刻、エラーID、操作、件数、処理時間、依存版を中心とする。本文、パスワード、URL token、任意のローカル絶対パスを既定ログへ記録しない。必要なファイル名も利用者の診断操作で確認できる範囲に留める。クラッシュレポートや利用統計を自動送信しない。
Qt、Chromium、PDFium、ZIP/XMLライブラリーの依存一覧と版をリリースごとに残す。脆弱性修正時は内容解析器を優先して更新し、PDF描画の回帰と文書隔離試験を行う。更新で無効になった保護を性能改善の名目で省略しない。
## 8. 必須の境界試験
受入試験に、path traversal、symlink、大小文字衝突、ZIP bomb、偽の申告サイズ、CRC不一致、XML実体、CSS外部import、外部font、script、iframe、form、meta refresh、file URL、別token、二重percent、Worker偽応答、サイズ不正画像を含める。
期待結果はUIが落ちないことに加えて、許可外ファイルの読取り・書込み・送信・プログラム起動が発生しないこと。G0では監視用ファイル・通信先を用いて観測する。通常のCSS相対参照やIDPF難読化fontも同時に試験し、防御が正当な書籍を不必要に壊さないことを確認する。
## 9. 参照資料
確認日: 2026-09-19。制限値と配布ゲートはDocView独自の設計判断である。
- [QWebEngineUrlRequestInterceptor](https://doc.qt.io/qt-6/qwebengineurlrequestinterceptor.html) - ネットワーク層より前の要求制御。
- [QQuickWebEngineProfile](https://doc.qt.io/qt-6/qquickwebengineprofile.html) - 文書用profileとhandler登録。
- [QWebEngineUrlScheme](https://doc.qt.io/qt-6/qwebengineurlscheme.html) - originと権限フラグ。
- [QWebEngineSettings](https://doc.qt.io/qt-6/qwebenginesettings.html) - 文書スクリプト・local資源等の設定。
- [WebEngineView](https://doc.qt.io/qt-6/qml-qtwebengine-webengineview.html) - 遷移・権限・renderer停止の通知。
- [OWASP File Upload Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html) - 展開後サイズの制限。
+128
View File
@@ -0,0 +1,128 @@
# 08 性能・受入試験設計
## 1. 検証方針
PDFの正確な表示を最初の品質判断とし、その後にキーボード操作、HTML/ZIP/EPUB、設定、Linux/Windowsでの動作を確認する。要件IDは01に対応する。本書は試験計画であり、アプリケーションの実装、性能計測、各試験の合格を報告するものではない。
数値は未測定の性能予算である。最低環境、実利用文書、PDFium・Qt WebEngineの組合せを固定して測定し、目標変更が必要な場合は09の意思決定記録へ理由と影響を残す。
## 2. 受入試験
### 2.1 試験の読み方
以下は試験の**設計**であり、合格済みの結果ではない。PDFを優先して実装・検証するが、全要件に対応したリリースの完了判断にはHTML/ZIP/EPUBと両OSの試験が必要となる。
試験データは再配布権を確認し、ファイルのハッシュ、出典、フォント条件、期待されるページ数・リンク先を固定する。PDF描画の正解画像は評価対象と同じ描画エンジンから作らず、独立した既知の表示環境または原版から用意し、その作成環境を記録する。
### 2.2 要件対応試験表
| 試験ID | 要件 | 入力・操作 | 合格条件 |
|---|---|---|---|
| T01 | R01 | 埋込フォントあり/なし、日本語・縦書き、ベクター、透明合成、画像、各種ページ寸法、回転、CropBoxを含むPDFを開く | 期待する文字・図形・画像が欠落/置換/不正配置されず、ページ寸法・回転・可視範囲が正しい。指定倍率・両OSで確認 |
| T02 | R01 | PDFの1ページ目、途中、最終ページへ移動し、ズームを変更する | 正しいページを描画し、古いページの描画結果が現在ページへ混入しない。位置保持規則を満たす |
| T03 | R01, R11 | 数千ページのPDFを開いて末尾へ直接移動する | 全ページの描画を待たずに要求ページを処理する。UIが応答し、不要な要求を取り消せる |
| T04 | R02 | 相対CSS、画像、文書内アンカー、別HTMLリンクを含むローカルHTMLを開く | 仕様で対応対象の資源を表示し、相対参照とアンカーが正しく解決される |
| T05 | R03 | ディレクトリ構造を持つZIP内index.htmlからCSS・画像・別HTMLへ進む | アーカイブ内で参照が解決し、リンク先が正しく表示される。入口不在/複数時は定義した結果になる |
| T06 | R04 | EPUB 2/3の目次順とspine順が異なる本、縦書き/RTL、固定レイアウトを開く | 次/前章はspine、目次は目次宛先に従う。章境界、nonlinear項目、書字方向、固定viewportが仕様どおり |
| T07 | R05, R15 | マウスなしで開く、スクロール、指定PDFページ移動、目次選択、パネル切替、別文書を開く | すべて実施でき、入力待ちとフォーカスが分かる |
| T08 | R05 | 同じキーを内容、目次、ページ一覧、文字入力の各状態で入力する | 状態表どおりに振り分けられ、文字入力中に閲覧位置が変わらない |
| T09 | R05, R12 | 日本語IME変換中にj/k、数字、Enter、Escを入力し、日英配列で記号キーを操作する | IME操作が閲覧コマンドにならない。日英配列で設定された論理キーを入力できる |
| T10 | R05, R10 | キー列の期限切れ、Esc、フォーカス変更、接頭辞の長押しを試す | 未完のキー列が残らず、予期せぬ操作・連続した表示切替が起きない |
| T11 | R06 | PDFの構造見出しのみ/アウトラインのみ/双方あり/双方なし、HTML/EPUBの見出し、同宛先の重複見出しで次/前を操作する | 出典と位置を区別し、04の優先順位で移動する。見出しなしは明示し、推測の見出しを生成しない |
| T12 | R07 | 多階層目次を展開/折りたたみ、項目を選択する | 選択だけでは本文を動かさず、確定時に正しい宛先へ移動する。不正な宛先でも他項目を操作できる |
| T13 | R08 | 目次・ページ一覧・ステータスを個別に切り替える | 個別に反映し、フォーカス中のパネルを隠すと内容へ戻る。隠れた領域が空白として残らない |
| T14 | R08, R15 | 明暗テーマ、100%/200%表示スケールでPDFを表示する | アプリ外観のみテーマが変わり、PDF原文の色を変更しない。主要操作・文字が欠けない |
| T15 | R10 | 有効なキー設定と表示設定を書き、再読込する | 全体の検証後に一括適用し、操作一覧も新設定になる |
| T16 | R10 | 構文誤り、型誤り、範囲外値、重複/接頭辞衝突のキー設定を読む | 行/項目と理由を知らせ、直前の有効な設定を維持する。部分適用しない |
| T17 | R12, R13 | LinuxとWindowsの初回起動、非ASCIIユーザー名、パスに空白、読み取り専用設定ディレクトリで起動する | 各OSの配置規則を満たし、パスを破損しない。書けない場合でも利用可能な既定設定で開始し、対処可能な通知を示す |
| T18 | R14 | 静的注釈・popup・widget・保存済みフォーム外観・外観欠落を持つPDFを表示して操作し、終了する | 文書に既にある内容は対応描画範囲に従って表示するが、編集・入力・保存を提供しない。原本ハッシュが変わらない |
| T19 | R09 | 設計レビューで別形式アダプターの追加手順と依存関係を追跡する | UI固有処理へパーサーを埋め込まず、文書能力・位置・ナビゲーション契約で接続できる。採否は設計レビュー記録で残す |
| T20 | R11 | 次節の代表文書と操作列を両OSで測定する | 合意された測定条件と性能予算を満たす。実測値・分位値・失敗数を記録する |
| T21 | R01-R04, R05 | 破損、読めないファイル、必要資源欠損、読込途中でEscを押す、または別文書を開く | 明確なエラーまたは部分表示の状態を示す。クラッシュせず、旧要求の完了が新文書を上書きしない |
### 2.3 PDF描画の判定
PDFの正確性を単一の画素一致率だけで判定しない。アンチエイリアスや色管理の違いがあるため、少なくとも以下を確認する。
1. ページ数、ページ寸法、回転、切り抜き範囲の一致。
2. 文字・グリフの欠落、豆腐表示、順序違い、画像欠損、合成順序違いがないこと。
3. 正解画像との重ね合わせによる図形・文字位置と可視領域の一致。
4. 透明・マスク・グラデーション・色空間など、PDFエンジン依存箇所の目視比較。
5. 低倍率・標準倍率・高倍率および表示スケール差の確認。
差分画像は検出補助として保存し、許容できる縁の差と本文の欠落を分けて判定する。画素差の閾値は代表コーパスで校正してから確定する。現時点で「99%一致なら正確」等の数値を保証値として置かない。
## 3. 性能測定設計
### 3.1 指標と暫定目標
以下の数値は、実測前の**提案する性能予算**であり、現在の達成値ではない。基盤選定時の試作測定で達成可能性を確認し、製品の最低動作環境とともに確定する。
| 指標 | 起点と終点 | 標準コーパスでの暫定予算 |
|---|---|---|
| 入力への視覚応答 | キーイベント受領→移動/選択/読込中表示が画面へ提示される | p95 ≤ 50 ms |
| 開く操作後の最初の内容表示 | ファイル選択確定→最初に読む内容を判読できるフレーム提示 | アプリキャッシュなしp95: PDF 2 s、HTML 2 s、ZIP HTML 5 s、EPUB 5 s |
| キャッシュ済みPDFページ移動 | ページ移動キー受領→対象ページの最終品質フレーム提示 | p95 ≤ 100 ms |
| 未キャッシュPDFページ移動 | ページ移動キー受領→対象ページの最終品質フレーム提示 | 標準コーパスp95 ≤ 500 ms |
| 継続スクロール | 同一の操作列のフレーム間隔を取得する | 60 Hz環境でp95 ≤ 33 msを目安とする |
| プロセス群のメモリ | アプリ・PDFワーカー・WebEngine等を含むRSS合計 | 標準コーパスの通常閲覧で1 GiB以下を目標とする。厳密なハード上限ではない |
| キャッシュメモリ | 文書描画キャッシュの実使用量 | 設定上限を超えない。プロセス群のRSSとは別に記録 |
PDFを開く際に小さいプレビューだけを先に描く設計なら、「最初に判読可能な表示」と「最終品質到達」を別々に測る。HTML/EPUBでは最初の文字だけを出してスタイル適用を遅らせた状態を完了にしない。必要なCSSと表示範囲内の主要な内容が反映された時点を記録する。
### 3.2 コーパスの構成
| 区分 | 代表例 | 目的 |
|---|---|---|
| 標準PDF | 100 MiB以下/500ページ以下の代表群、文字中心、画像混在、埋込日本語フォント | 日常の読書操作と開く時間 |
| 重いPDF | 1 GiB/5,000ページ規模の代表群、大きいスキャン画像、複雑な透明/ベクター | 長時間停止を防ぐ処理、キャッシュと取消 |
| HTML | 100 MiB以内の参照資源群を持つ長い見出し構造、CSS、ローカル画像 | レイアウト変更と見出し移動 |
| ZIP HTML | 圧縮時100 MiB以下、展開後500 MiB以下、複数階層の相対リンク、画像資源が多い書籍 | 展開・入口解決・内部リンク |
| EPUB | 100 MiB以下/500 spine項目以下、画像混在、日本語、章数が多い例 | spine移動、章の切替、位置保持 |
これらのページ数や容量は試験セットの選定目安であり、それだけで入力上限を決定しない。アーカイブ展開量等の安全上限はセキュリティ仕様と整合させる。極端な文書には標準コーパスと同じ表示時間を一律に保証せず、応答・取消・資源上限の維持を優先して別枠で評価する。実際に採用した各ファイルのハッシュと特性を試験報告に固定する。
### 3.3 再現可能な測定手順
1. 暫定の基準機は物理4コア相当のCPU、メモリ8 GiB、SSD、内蔵GPU、1,920×1,080/60 Hzとする。LinuxとWindowsの対象バージョン、CPU型番、コア数、メモリ、ストレージ、GPU、ドライバー、表示解像度/倍率、リフレッシュレート、アプリと描画エンジンのバージョンを記録する。同じ構成の比較を基本にする。
2. 冷起動はアプリのキャッシュなしで測る。OSファイルキャッシュを消していない場合は、その事実を明記して「完全コールド」と呼ばない。アプリを開き直すだけの試験と区別する。
3. 読込試験は文書ごと30回を目安とし、p50/p95、最小/最大、失敗数を報告する。p95の安定性が不足する場合は追加測定する。
4. 操作試験は固定したページ/見出し/章の移動列を最低100操作実施し、キャッシュヒットとミスを分離する。入力時刻と実際の画面提示時刻を計測し、要求を送っただけの時刻で完了にしない。
5. キー長押し、遠いページへの連続ジャンプ、ウィンドウのリサイズ、別文書への切替を含め、古い仕事を破棄した回数、描画待ちの数、UI停止時間も記録する。
6. メモリはアプリ単体と補助プロセスを含むプロセス群を分けて測る。開始時、最大、文書閉鎖後の使用量を記録し、同じ文書を10回開閉して無制限増加しないか確認する。OSのRSSが直ちに基準値へ戻らないことだけでリークと断定しない。
7. 最低動作環境の正式決定後、その環境で受入判定する。高性能な開発機だけの達成を製品保証としない。
## 4. 補助機能の試験
以下は01・04の追加提案を採用した場合の受入条件である。必須要件の合否と区別する。
| 試験ID | 対象 | 合格条件 |
|---|---|---|
| T-P01 | 本文検索 | 日本語/英語で検索でき、`n`/`N`で正しい位置へ移動する。画像PDFは文字情報なしを示し、勝手にOCRを開始しない |
| T-P02 | 検索中の入力 | IME変換・Enter/Escが閲覧操作にならない。検索取消後に古い結果が新しい文書へ反映されない |
| T-P03 | 位置復元 | 同じ文書の再開位置が復元される。リフロー時は論理位置を使い、原本が変化した場合は不正な位置を適用しない |
| T-P04 | 最近使った文書 | 保存上限・無効化・履歴消去が機能する。削除済みの原本は理由を示し、別ファイルとして誤って開かない |
## 5. 異常系・非機能の追加確認
07の安全境界の設計が実際に有効であることを、通常文書の試験とは独立して確認する。以下の拒否は仕様に基づく挙動であり、アプリケーションのクラッシュを合格としない。
| 試験ID | 対象 | 合格条件 |
|---|---|---|
| T-S01 | ZIPの絶対パス・親ディレクトリ参照・シンボリックリンク・展開量超過 | 07の規則で拒否し、許可ルート外へファイルを書かない。中断した一時領域を回収する |
| T-S02 | HTML/EPUBの外部URL・文書スクリプト・フォーム・ローカルルート外参照 | 07の通信/参照規則を守る。文書から任意のファイルやOS機能へ到達できない |
| T-S03 | PDFワーカーの異常終了・固まった読込・不正な応答 | UIは応答し、対象文書の失敗を示す。古いワーカー応答を新文書へ適用しない |
| T-S04 | データ消去・一時ファイル回収 | 読書履歴等の対象を区別して消去し、原本を消さない。異常終了後の一時領域も起動時の規則で回収する |
| T-S05 | 配布物からの初回起動 | WindowsとLinuxの対象環境で依存ライブラリ、サンドボックス、WebEngine資源、ライセンス同梱が成立する |
## 6. 合否の記録と完了条件
試験報告には、試験ID、要件ID、対象ビルド、OS/表示環境、文書ハッシュ、手順、期待結果、実結果、証拠画像または計測記録、判定、既知制限、再試験条件を記載する。未実施と失敗を区別し、将来機能の保留を必須要件の合格に置き換えない。
完成版の受入条件は次のとおり。
1. R01〜R15に対応するT01〜T21が、対象OSで合格している。PDF描画の対応範囲は03と一致し、検出可能な未対応要素を通知する。実行時に検出できない描画差はコーパスの比較試験で判定する。
2. T-S01〜T-S05の隔離・入出力・回復・配布確認が完了している。
3. 合意された基準機・コーパスで性能予算を達成している。未達項目があれば、制限と判断を記録して受入判断を行う。黙って目標値を実績値へ書き換えない。
4. 補助機能を採用した場合は対応するT-P系列の試験が完了している。
5. 残っている問題が、原文の欠落、データ破損、キー操作不能、原本書換え、UI長時間停止を引き起こさない。
+95
View File
@@ -0,0 +1,95 @@
# 09 技術選定・設計判断・実装ロードマップ
対象: DocView(仮称)/設計日: 2026-09-19/状態: 設計基準案
## 1. 採用構成
**Qt 6 / C++20 / Qt Quick を用いたデスクトップ GUI とし、PDF は独立プロセスの PDFium、HTML と EPUB は Qt WebEngine で表示する。** UI は TUI 風の簡潔な外観と Vim 風キーボード操作を備える。今回の成果物は設計書であり、アプリケーションの実装・試作・性能測定は含まない。
Qt、PDFium、WebEngine、ZIP ライブラリ、フォント、ビルドツールの具体的な版は、実装開始時にサポート状況と検証結果を確認して固定する。PDFium は公開 C API を使用し、取得元、コミット、ビルドオプション、成果物のハッシュを記録する。最新版へ無条件に追従せず、修正版の評価と回帰試験を伴って更新する。
## 2. 候補比較
下表の評価は本アプリケーションの要件に対する設計判断であり、各製品の公式な優劣評価ではない。描画精度の順位を示す比較試験は未実施である。
| 候補 | 適合する点 | 本件で負担となる点 | 判断 |
|---|---|---|---|
| Electron + PDF.js | Web 技術による UI、HTML、EPUB の統合が容易。PDF.js を個別更新できる | Electron / Chromium / Node.js / PDF.js の更新と権限境界を管理する。PDF 互換性は別途検証が必要 | 代替候補として保存 |
| Qt Quick + Qt PDF + Qt WebEngine | Qt PDF の目次、リンク、検索、ビューア部品を利用できる | 確認した Qt PDF 公開 API に PDF 構造タグの取得 API がなく、アウトラインなしの tagged PDF の見出し対応が不足する | 不採用 |
| Qt Quick + 直接 PDFium + Qt WebEngine | PDF 描画、構造、座標の制御を一つのアダプターに集約できる。PDF 処理の障害を GUI から分離できる | ワーカー通信、キャッシュ、表示部品、ビルドと更新管理が必要。構造関連 API の一部は Experimental | 採用設計 |
| Qt Quick + MuPDF + Qt WebEngine | PDFium と異なる描画系として比較できる | AGPL または商用ライセンスと配布方針の整合を検討する必要がある。別途統合と検証が必要 | 採用再検討時の候補 |
公式の機能・ライセンス・API 根拠は [参考資料](10-references.md) の S01-S14 を参照する。PDFium が PDF.js より常に正確、または Qt を使うだけで軽量になるとは判断していない。Qt WebEngine を含めた配布サイズとメモリ量を測定対象にする。
## 3. 設計判断記録
| ID | 決定 | 根拠と結果 | 要件 |
|---|---|---|---|
| ADR-001 | デスクトップ GUI と TUI 風 UI を採用 | 利用者が GUI を選択済み。PDF は本来のレイアウトを保持して描画し、周囲の UI を簡素化する | R01, R05, R08, R15 |
| ADR-002 | PDFium の公開 C API を独立ワーカーに集約 | tagged PDF の構造情報と描画を同じ PDF バックエンドで扱う。Qt PDF の private API は利用しない | R01, R06, R07, R09 |
| ADR-003 | PDF ワーカー内の PDFium 呼び出しを直列化 | PDFium API は thread-safe ではない。UI と独立したキューで実行し、採用版で確認した中断方法とプロセス終了による回復を設計する | R11, R12 |
| ADR-004 | HTML / EPUB に Qt WebEngine を使用 | 現代的な CSS、縦書き、フォント等をブラウザ描画系に委ねる。EPUB のコンテナー、目次、読書順序はアプリ側で扱う | R02, R03, R04, R06, R07 |
| ADR-005 | PDF 描画ルートを一本化 | Qt WebEngine 内蔵 PDF ビューアを無効にし、PDF リンクはアプリの PDF オープン処理へ渡す。Qt PDF と直接 PDFium を併用しない | R01, R05, R09 |
| ADR-006 | 文書内容とアプリ操作の権限を分離 | 文書を未信頼入力として扱い、文書のスクリプトや設定が任意ファイル・外部通信・OS コマンドへ到達しない境界を設ける | R02, R03, R04, R09, R10 |
| ADR-007 | 既存の見出し情報を使う | PDF outline と構造タグ、HTML 見出し、EPUB の目次を区別する。文字サイズからの見出し推定を必須機能にしない | R06, R07 |
| ADR-008 | PDF は閲覧専用とする | 元ファイルを書き換えない。既存の注釈や保存済みフォーム appearance の表示と、編集操作の提供を分ける | R01, R14 |
| ADR-009 | 設定と文書アダプターを分離 | キー入力はコマンドへ変換し、フォーマット固有処理はアダプターへ委譲する。設定ファイルは OS の慣習に沿って配置する | R05, R09, R10, R13 |
| ADR-010 | 初期サポート OS を明示する | Linux 全般という無限定な互換性保証を避け、実機検証済みの組合せをサポート表で公開する | R12 |
Qt WebEngine 自体にも PDFium 由来コードが含まれ得るため、ADR-005 は配布物全体から PDFium の重複がなくなることを意味しない。独自 PDF ワーカーと WebEngine は、それぞれの依存部品と更新責任を記録する。
## 4. 実装前の技術ゲート
G0は以下の技術ゲート一式を実施する段階の総称である。以下は後続実装で採用構成を確定するための条件である。本設計時点で合格を確認したものではない。画質判定と性能の定量基準は品質・試験設計に定義する。
| ゲート | 検証内容 | 合格条件・証跡 | 不合格時の判断 |
|---|---|---|---|
| G-RENDER | 代表 PDF の描画。日本語横組み・縦組み、埋込/非埋込フォント、CID/CMap、透明、クリップ、画像、回転/CropBox、ICC/CMYK、既存注釈、保存済みフォーム appearance | 人が承認した正解画像との比較と差分レビューを完了。文字・図の欠落、誤配置、読めない描画が残っていない | PDFium の設定・版を見直す。要件を満たせなければ別エンジンを比較し ADR を更新 |
| G-HEADINGS | outline なしの H/H1..H6、RoleMap、複数 MCID、Form XObject、回転、ページにまたがる構造 | 見出し列と順序が妥当で、正しいページ・位置へ移動する。座標が得られない場合のページ単位移動を区別して報告できる | 必要な公開 API の不足を特定。黙ってタグ対応を省略せず、アダプター/エンジンの再選定へ戻る |
| G-SANDBOX | Linux と Windows の PDF ワーカー権限制限、クラッシュ回復、IPC 境界 | 許可された入力と通信のみ成功。任意パスの読取・書込、外部通信、子プロセス生成を拒否する試験の記録。GUI が継続しワーカーを再作成できる | 配布対象ごとに制限方式を再設計。sandbox を無効化する起動オプションを通常運用の解決策にしない |
| G-WEB | 文書別 origin、文書スクリプト抑止、アプリ所有の isolated world 操作、読み取り範囲、ページ遷移、リンク、ダウンロード、通信遮断、内蔵 PDF ビューア無効 | HTML / EPUB の表示と見出し操作が機能し、書籍からアプリ権限や許可外リソースへ到達しない。通常 HTML と ZIP / EPUB の双方で確認 | Qt WebEngine 設定・scheme handler・操作経路を修正。文書 JavaScript を広く許可して回避しない |
| G-DISTRIBUTION | クリーン OS 環境での配布と依存・ライセンス確認 | ランタイム、プラグイン、WebEngine 補助プロセス、フォント、PDFium 等の不足なし。配布物と対応ソース・告知・依存一覧の対応が確認済み | パッケージと採用条件を見直して再試験 |
PDF 正解画像は一つのビューアの出力だけで無条件に決めない。仕様に基づく期待、複数の独立した描画系、元文書の作成条件、人による確認を使う。アンチエイリアスの軽微な差と内容の欠落を同じ問題として扱わない。特殊な印刷表現や壊れた文書は、対応範囲・既知制約を明記する。
PDFium の構造タグ API には Experimental な項目がある。必要な API の一覧、固定コミット、文字列長・寿命・エラー処理、MCID と座標の対応を検証記録に残す。見出しのラベルを得ても、正しい位置へ移動できたとは判定しない。
## 5. OS と配布方針
| 環境 | 初期方針 | 必須確認 |
|---|---|---|
| Windows 11 x64 | 正式サポートの提案対象 | クリーン環境、表示倍率、IME、非 ASCII パス、ワーカー権限、署名済み配布物、アンインストール |
| Ubuntu 24.04 x64 | 正式サポートの提案対象。X11 / Wayland を個別に確認 | GPU と software rendering、フォント、IME、ファイル選択、sandbox、Qt / WebEngine の共有ライブラリ依存 |
| Fedora x64 | 追加検証対象 | 対象版を実装時に選定。Wayland、SELinux、ライブラリ、sandbox、パッケージ形式を検証してからサポートを宣言 |
| その他 Linux、Windows on ARM、旧 Windows、macOS | 初期の正式サポートに含めない | 要望と検証コストに基づき別途追加 |
Qt の一般的なサポート対象と、本アプリケーションで検証した対象は区別する。Qt WebEngine には独自のビルド制約があり、一般的な Qt 構成のすべてを利用できるわけではない。確認した資料では Windows の MinGW ビルドは Qt WebEngine に適合せず、静的ビルドも非対応である。[Qt WebEngine Platform Notes](https://doc.qt.io/qt-6/qtwebengine-platform-notes.html)
Windows は MSVC 系の Qt と互換性のあるツールチェーンを使い、PDFium のビルドは upstream の Clang 系要件と整合させる。Linux では対象環境に合う共有ライブラリと sandbox を梱包・依存解決する。具体的なパッケージ形式は G-DISTRIBUTION で比較し、初期対象ごとに一つを決める。単一実行ファイル化や任意 Linux での起動を前提にしない。
ライセンスはライブラリ名だけで結論を出さず、実際に同梱する版・モジュール・ビルド方式で確認する。Qt 商用ライセンスの採用だけで Chromium 等の第三者条件が消えるとは扱わない。MuPDF は代替評価対象にとどめ、ライセンス整理を行わず自動フォールバックとして同梱しない。
## 6. 後続の実装順序
| 段階 | 完了させるもの | 次へ進む条件 |
|---|---|---|
| 0. 設計基準の固定 | 対応範囲、テスト文書、参照画像、初期 OS、設定形式、依存版、ライセンス方針 | 未決事項の担当と期限が決まり、上記ゲートの評価環境が用意されている |
| 1. PDF 基盤の検証 | 描画、構造見出し、座標、ワーカー制御、権限制限、最小配布経路 | G-RENDER / G-HEADINGS / G-SANDBOX を満たし、配布成立の見通しがある |
| 2. PDF 閲覧機能 | 通常表示、移動、拡大縮小、目次、見出し、ページ一覧、設定、状態復元 | PDF の機能・性能・操作性の受入試験に合格 |
| 3. HTML / ZIP HTML / EPUB | パッケージ読込、リソース解決、目次、見出し、読書順序、位置復元 | G-WEB と各形式の受入試験に合格 |
| 4. 製品化 | OS 別配布、クリーンインストール、更新、障害回復、ヘルプ、告知文書 | 全対象 OS の回帰試験と G-DISTRIBUTION に合格 |
PDF 最優先は開発・検証順序を意味する。最終的な初期リリースが要件を満たすためには、HTML、ZIP HTML、EPUB を含む全必須要件を受け入れる必要がある。PDF のみの中間成果物を要件一式の完成とは扱わない。
## 7. 継続管理する不確実性
| 項目 | 設計時点の扱い | 解消時点 |
|---|---|---|
| 実際の書籍での PDF 描画互換性 | 代表コーパスは後続で収集・承認。完全一致は未保証 | G-RENDER |
| tagged PDF の全構造と座標 | 公開 API による経路を採用し、特殊構造は要検証 | G-HEADINGS |
| PDFium の固定版と更新頻度 | 実装開始時にコミットを固定し、脆弱性・互換性を継続評価 | 段階 0 以降 |
| ワーカーの OS 別権限制限 | 別プロセス化と sandbox を別の要件として扱う | G-SANDBOX |
| WebEngine の文書操作と無権限化の両立 | 文書の JavaScript とアプリ所有の操作を区別して検証 | G-WEB |
| 配布ライセンスとパッケージ方式 | 実際の依存集合に基づいて固定 | G-DISTRIBUTION |
| 性能の成立 | 品質・試験設計の測定条件と合格値を使用。未測定 | 各段階の受入試験 |
+64
View File
@@ -0,0 +1,64 @@
# 10 参考資料
確認日: 2026-09-19
一次資料を用いて API、機能、ライセンス表記、サポート条件を確認した。Web 上の最新資料は変更されるため、実装開始時には採用版に対応した資料・ソースコミットも固定する。以下の資料は設計の根拠であり、本アプリケーションの描画品質、性能、ライセンス適合性を実証するものではない。
## 1. PDF と技術選定
| ID | 資料 | 本設計で確認した事項 |
|---|---|---|
| S01 | [PDFium README](https://pdfium.googlesource.com/pdfium/+/refs/heads/main/README.md) | Linux / Windows のビルド経路、JavaScript / XFA のビルド選択、公開 API の範囲、ピクセル試験の存在 |
| S02 | [PDFium fpdfview.h](https://pdfium.googlesource.com/pdfium/+/main/public/fpdfview.h) | 公開 C API、読込と描画、API が thread-safe ではないこと |
| S03 | [PDFium fpdf_structtree.h](https://pdfium.googlesource.com/pdfium/+/main/public/fpdf_structtree.h) | ページの構造ツリー、要素型、子要素、MCID の取得。一部 API が Experimental であること |
| S04 | [PDFium fpdf_edit.h](https://pdfium.googlesource.com/pdfium/+/main/public/fpdf_edit.h) | ページオブジェクトの読取、MCID と境界情報へのアクセス。ファイル名に edit を含むが、読取 API の参照に使う |
| S05 | [PDFium LICENSE](https://pdfium.googlesource.com/pdfium/+/refs/heads/main/LICENSE) | PDFium のライセンス本文。依存部品の条件を含めて確認する起点 |
| S06 | [Qt PDF](https://doc.qt.io/qt-6/qtpdf-index.html) | Qt PDF の描画、ビューア、目次、リンク、検索等の機能 |
| S07 | [QPdfDocument](https://doc.qt.io/qt-6/qpdfdocument.html) | 読込、描画、ページラベル、テキスト、エラーの公開 API。構造タグ API の有無を評価する対象 |
| S08 | [Qt PDF Licensing](https://doc.qt.io/qt-6/qtpdf-licensing.html) | Qt PDF の商用 / LGPLv3 / GPLv2 表記と、PDFium スナップショット・第三者ライセンス |
| S09 | [PDF.js 公式サイト](https://mozilla.github.io/pdf.js/) | PDF.js の位置づけと Apache 2.0 の表記 |
| S10 | [PDF.js FAQ](https://github.com/mozilla/pdf.js/wiki/Frequently-Asked-Questions) | ブラウザ環境による対応差、Worker と本体のバージョン整合、可視ページを優先するメモリ上の考え方 |
| S11 | [Electron Security](https://www.electronjs.org/docs/latest/tutorial/security) | Node.js 能力の分離、context isolation、sandbox、IPC 検証、依存更新の必要性 |
| S12 | [Electron Releases](https://www.electronjs.org/docs/latest/tutorial/electron-timelines) | 直近三つの stable major release というサポート方針と更新周期 |
| S13 | [What is MuPDF?](https://mupdf.readthedocs.io/en/latest/guide/what-is-mupdf.html) | PDF 等の閲覧・変換・操作のためのライブラリとツールという位置づけ |
| S14 | [MuPDF License](https://mupdf.readthedocs.io/en/latest/license.html) | AGPL または商用ライセンスという提供形態 |
## 2. Qt WebEngine と配布
| ID | 資料 | 本設計で確認した事項 |
|---|---|---|
| S15 | [QWebEngineSettings](https://doc.qt.io/qt-6/qwebenginesettings.html) | MainWorld の JavaScript、ローカルファイル/外部 URL、DNS prefetch、内蔵 PDF ビューア等の設定 |
| S16 | [Qt WebEngine Platform Notes](https://doc.qt.io/qt-6/qtwebengine-platform-notes.html) | Chromium renderer の sandbox、Linux の実行条件、C++20、Windows ビルド制約、静的ビルド非対応 |
| S17 | [Qt WebEngine Licensing](https://doc.qt.io/qt-6/qtwebengine-licensing.html) | Qt 部分と Chromium 側の両方の条件が関係すること、第三者ライセンスの一覧 |
| S18 | [Deploying Qt WebEngine Applications](https://doc.qt.io/qt-6/qtwebengine-deploying.html) | 補助プロセス、リソース、翻訳等を含む配布の検討事項 |
| S19 | [Qt Supported Platforms](https://doc.qt.io/qt-6/supported-platforms.html) | Windows 11 / Ubuntu 24.04 の掲載、版ごとのサポート条件、モジュール固有の例外 |
## 3. 文書形式・隔離・保存
| ID | 資料 | 本設計で確認した事項 |
|---|---|---|
| S20 | [PDFium fpdf_formfill.h](https://pdfium.googlesource.com/pdfium/+/main/public/fpdf_formfill.h) | フォーム環境とwidget描画。通常ページ描画との違い |
| S21 | [EPUB 3.3](https://www.w3.org/TR/epub-33/) | package、spine、nav、rendition、font難読化 |
| S22 | [EPUB Reading Systems 3.3](https://www.w3.org/TR/epub-rs-33/) | rootfile、XML処理、スクリプトfallback、ローカル参照の制約 |
| S23 | [EPUB CFI 1.1](https://idpf.org/epub/linking/cfi/epub-cfi.html) | 文書の論理位置表現 |
| S24 | [QWebEngineScript](https://doc.qt.io/qt-6/qwebenginescript.html) | ApplicationWorld、DOMアクセスと変数の分離 |
| S25 | [WebEngineView](https://doc.qt.io/qt-6/qml-qtwebengine-webengineview.html) | runJavaScriptのworld、ナビゲーション・権限の通知 |
| S26 | [QWebEngineUrlScheme](https://doc.qt.io/qt-6/qwebengineurlscheme.html) | Host構文によるoriginとscheme権限 |
| S27 | [QWebEngineUrlSchemeHandler](https://doc.qt.io/qt-6/qwebengineurlschemehandler.html) | 文書資源の応答 |
| S28 | [QWebEngineUrlRequestJob](https://doc.qt.io/qt-6/qwebengineurlrequestjob.html) | Qt 6.6以降の追加応答ヘッダー |
| S29 | [QWebEngineUrlRequestInterceptor](https://doc.qt.io/qt-6/qwebengineurlrequestinterceptor.html) | 要求の事前検査 |
| S30 | [QQuickWebEngineProfile](https://doc.qt.io/qt-6/qquickwebengineprofile.html) | 文書ごとのprofileとhandler |
| S31 | [Linux Landlock](https://docs.kernel.org/userspace-api/landlock.html) | OSアクセス制限とABIによる機能差 |
| S32 | [Windows AppContainer](https://learn.microsoft.com/en-us/windows/win32/secauthz/implementing-an-appcontainer) | 制限されたプロセス環境の構成 |
| S33 | [OWASP Archive Testing](https://wstg.owasp.org/latest/4-Web_Application_Security_Testing/10-Business_Logic/09-Upload_of_Malicious_Files/) | 不正アーカイブの試験観点 |
| S34 | [OWASP File Upload Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html) | 解凍後サイズの制限 |
| S35 | [XDG Base Directory](https://specifications.freedesktop.org/basedir/latest/) | 設定・状態・キャッシュの配置と既定値 |
| S36 | [QStandardPaths](https://doc.qt.io/qt-6/qstandardpaths.html) | OS別保存経路、AppConfigLocationとAppDataLocationの差 |
| S37 | [Windows Known Folders](https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid) | RoamingAppDataとLocalAppData |
## 4. 参照の読み方
- **資料の事実:** 公開 API の存在、スレッド安全性の注意、ライセンス表記、サポート対象等。上表に対応する一次資料で確認する。
- **設計判断:** Qt Quick と直接 PDFium の組合せ、ワーカー分離、初期 OS、PDF 描画ルートの一本化等。[技術選定・設計判断](09-decisions-roadmap.md) の ADR に記録する。
- **未検証事項:** 対象書籍の描画互換性、特殊な構造見出しの座標、実機性能、OS 別 sandbox、最終配布物。公式資料に機能があることだけで合格とせず、受入試験で確認する。
+13
View File
@@ -0,0 +1,13 @@
{
"00-guide.md": "179fa4a28b5ba994d3d71ffca310da78f071520f9fc8b18f529e5e5f2210ac38",
"01-requirements.md": "ee35438cf3c749b7410db2b1e4b941dd3a5fc1e3b4bb1cd6c0e3cf6056c34961",
"02-architecture.md": "afbc10d91dc6ddcd9727f3af1b861535c6d9e6da977ac19a9e2ad55c49bf83c3",
"03-formats.md": "5590019c96f8935175670b9d1a4876d5f365303e63c53953de006cde105eaa53",
"04-ui-navigation.md": "9ccb90ad0549c8a8dc41b2d883842ebcf28358796398cf62c36e218980387456",
"05-data-contracts.md": "2b9d28523e68950b48d57db363c663416c957145ad3f4e460774ac3740287a71",
"06-config-storage.md": "c7e00e61f686c36288a87f01dc00429a68174001984def78b75446b1e6c954ed",
"07-security-errors.md": "855450a9791dce0e6afb9f0e18f2f5bbd7b6cecfe6cf8c5ad677ac2af5608c8a",
"08-performance-tests.md": "eb852e6b6016e732c715ed061ea25ed1a911c092bb6de6e71aabbf4acb4f6483",
"09-decisions-roadmap.md": "55c176637d017acc66b46ba1780100854f671442d465c329b5d258f4c8fb2ebb",
"10-references.md": "4d0614abd7104a1e68c3b061fc797c3e1576d1127907a08c75c6e008da2ca0da"
}
+107
View File
@@ -0,0 +1,107 @@
# Font broker contract
This implementation supplements design 03 §3.2 and 05. The selected font is
transferred as an immutable, bounded byte snapshot through the existing IPC
connection. No filesystem path or OS font handle crosses the boundary. The
main process selects fonts through Fontconfig on Linux and GDI on Windows;
PDFium and SFNT/TTC table parsing remain inside the sandboxed PDF worker.
See [the API investigation](font-broker-plan.md) for pinned upstream evidence.
Windows implementation and native Windows execution are separate claims;
consult [the validation record](VALIDATION.md) for actual execution coverage.
## Request envelope and identity
Only a `WorkerProcess` with an explicitly registered font handler accepts
`font.map`, `font.read`, or `font.close` requests. Production registers the
handler only for PDF workers. Requests use the current parent operation's
`sessionId`, `generation`, and `requestId`. The payload contains a positive,
monotonically increasing `fontRequestId`; the result or error repeats it.
Only one font request may be outstanding. These are control frames, with no
streaming `more` field and the existing 1 MiB encoded control-frame limit.
Unknown fields, wrong types, stale identities and unissued handles are rejected.
The main process routes font requests before ordinary PDF responses. It never
finishes or replaces the active PDF operation or extends its deadline as a
side effect. The callback API removes `fontRequestId` from request/result maps;
the transport layer owns that field. Broker completion returns either the
inner result map or `{error: {code, message}}`. Cancellation discards pending
completion and invalidates issued handles. A parent timeout error can end a
pending font request without interpreting it as successful font acquisition.
| Operation | Additional payload fields | Additional result fields |
| --- | --- | --- |
| `font.map` | `faceLocal` bytes, `weight` integer, `italic` boolean, `charset` integer, `pitchFamily` integer | `found` boolean. If true: `fontId`, `actualFaceLocal`, `charset`, `size`, `sha256`, `substituted`, and exactly one of `faceIndex` / `faceOffset` |
| `font.read` | `fontId`, `offset`, `length` | Same `fontId` / `offset`, and exactly `length` bytes in `data` |
| `font.close` | `fontId` | `released: true` |
Names use the system local encoding as required by the pinned PDFium public
API, contain no NUL, and are at most 256 bytes. Requested names may be empty;
selected names must not be empty. Weight is 0–1000. Charset is one of
0, 1, 2, 128, 129, 134, 136, 161, 163, 177, 178, 204, 222, 238.
Pitch/family accepts family values 0x00–0x50 in steps of 0x10 with pitch 0–2.
Names are typed family values, never fontconfig pattern expressions or paths.
The font ID is 32 lowercase hexadecimal characters, scoped to one service.
SHA-256 is 32 raw bytes. Face index is 0–65535; face offset is inside the snapshot.
## Limits and errors
| Resource | Bound |
| --- | --- |
| Snapshot | 64 MiB per font/collection |
| Read | 1–65536 bytes; checked against the issued snapshot's actual size |
| Open font handles | 16 per session |
| Different selection requests | 256 per session, including misses |
| Total transferred font bytes | 512 MiB per session |
| Worker snapshot cache | 128 MiB; live callback references cannot be evicted |
| Broker snapshots | 256 MiB across active, staging and revoked services; global LRU of unreferenced snapshots |
| OS font threads | Two per application process; excess services fail explicitly |
| Broker task queue | Eight entries per service (production wire permits only one pending request) |
| One reply / complete font fetch | 5 seconds / 15 seconds, within the unchanged parent deadline |
`E_FONT_LIMIT` is a new registered error for these resource bounds. It is not
reported as a missing font. `E_FONT_FAILED` is a new registered error for OS
font acquisition, unsupported/invalid font data. A snapshot hash or wire mismatch uses the protocol-failure path.
Existing `E_WORKER_TIMEOUT` and `E_WORKER_CRASH` represent transport/deadline
failure. All four font error maps contain `fontRequestId`, `code`, and a
nonempty message of at most 4096 characters. Malformed reverse RPC uses the
existing worker protocol-failure path. A genuine absent match is the success
result `found: false`; PDFium may use its built-in fallback and the adapter
records a warning. A communication/resource error must fail the PDF operation.
Fatal font errors remain sticky for the adapter lifetime, because PDFium may
have cached an internal substitute during the failed call. Retry requires a
new worker/document; clearing only the error would hide that failed selection.
## Execution and font data
Normal PDF operations run from a queued invocation after the transport's
receive stack unwinds. Font responses are routed during the bounded nested
wait without reentering PDFium or starting another PDF operation. Main-thread
GUI processing never waits for Fontconfig/GDI. Revocation prevents new work
and drops late callbacks, while an already running OS call retains its bounded
thread and snapshot charges until cleanup. Snapshot eviction considers unused
entries across services, so an old document cannot monopolize reclaimable
cache space while a new document opens. In-process test/low-memory constructor
arguments may lower the cache bound; they cannot raise the production cap.
The worker always installs `FPDF_SYSFONTINFO` version 2. A missing provider
does not reactivate PDFium's default filesystem font mapper. The production
Linux worker receives no font-directory grants. Downloaded chunks must match
declared size and SHA-256 before the immutable snapshot reaches PDFium.
For a TTC, `ttcf` returns the complete collection and table zero returns the
bytes from the selected face offset to the end. A normal table tag resolves
through that face's checked directory. A short non-null caller buffer is
zero-initialized and filled with the available prefix; the required size is
returned. This also supports the pinned PDFium mapper's 1024-byte checksum
read. Font parsing rejects out-of-range tables, invalid collection indices and
duplicate table tags. Static TTF/OpenType/collections are the initial scope;
named variable instances are not silently mapped to face zero. A collection
may contain at most 256 faces and each face at most 4096 table entries. These
parser bounds are separate from the wire field range for an OS face index.
Font substitution depends on installed candidates. Fixed aliases and family
priorities are maintained in `src/broker/font_backend.cpp`; selected family,
charset, face metadata and substitution status describe the actual result.
The synthetic unembedded Japanese H/V corpus exercises native CID writing
modes. Its independent-renderer comparison is additional evidence, not a
human-approved golden image or a claim about every PDF CMap.
+300
View File
@@ -0,0 +1,300 @@
# PDF font broker 実装計画
調査日: 2026-09-19。対象: PDFium 155.0.8057.0、固定 commit
`a5a7089234f121990b336b3841008009dca143bf`、Qt 6.11.2。
この文書は実装前の調査・計画を保存したものである。現在の実装契約は[font-broker-contract.md](font-broker-contract.md)、実行結果は[VALIDATION.md](VALIDATION.md)を参照。以下の「現行」「未実装」は調査時点を指し、Windowsの実行確認を示すものではない。
## 結論
最小の推奨構成は、Main 側の FontBroker が OS font 索引と選択を管理し、
PDFWorker の公開 `FPDF_SYSFONTINFO` version 2 callback から、既存 IPC を使う
限定的な逆方向 RPC で選択済み font のバイト列を取得する方式である。
font の表・glyph の解析は PDFWorker 内に残す。Main に PDFium をリンクしない。
新しい socket、継承 handle、任意パス指定、font ディレクトリーの Worker 読取り権限は追加しない。
バイト列方式なら Linux の SCM_RIGHTS と Windows の起動後 handle 複製を別実装にせず、
現在の socket/匿名 pipe で共通化できる。全 font の先行転送ではなく、要求された font のみを
64 KiB ごとの明示的な read 要求で取得する。巨大な TTC の一括 IPC frame も避けられる。
現行 `src/pdf/main.cpp:16` は `/usr/share/fonts`、`/usr/local/share/fonts`、
`/etc/fonts`、`/var/cache/fontconfig` を Worker に許可している。これは削除対象である。
固定版 Linux PDFium の既定実装は fontconfig ではなく、フォルダー列挙と独自の
CJK 代替候補を使う。したがって Broker を fontconfig に替えるだけで、同一 Linux 上でも
未埋込み font の選択が変化し得る。
[固定版 Linux 実装](https://pdfium.googlesource.com/pdfium/+/a5a7089234f121990b336b3841008009dca143bf/core/fxge/linux/fx_linux_impl.cpp)
## PDFium 公開 API の適用
ローカル `.deps/pdfium/include/fpdf_sysfontinfo.h` の 49–52 行に version 2 の
per-request matching があり、86–87 行では EnumFonts を呼ばないことが明記されている。
version 2 は experimental のため、この固定版への依存と更新時の回帰試験を明記する。
private header の include や private symbol の利用は不要である。
| Callback/公開関数 | 推奨実装 |
| --- | --- |
| `FPDF_SetSystemFontInfo` | `FPDF_InitLibraryWithConfig` 後、文書読込み前に Worker 専用 adapter を登録 |
| `MapFont` | weight/italic/charset/pitch-family/face を Broker へ送り、選択済みバイト列を得て Worker 内 `FontBlob` を返す |
| `GetFont` | version 2 では主経路でないが、同じ選択処理への限定 wrapper として用意 |
| `GetFontData` | Worker の immutable blob から全体または選択 face の table を返す。パスを開かない |
| `GetFaceName`/`GetFontCharset` | Broker の選択結果を返す。要求名を実際の選択名として偽装しない |
| `DeleteFont` | Worker の callback handle を破棄。PDFium が保持するコピーとは寿命を分離 |
| `Release` | `FPDF_DestroyLibrary` まで adapter 自体を生存させる。二重解放しない |
`MapFont` の face は公開 header 上「system local encoding」であり、無条件に UTF-8 と
解釈しない。callback 側で最大 256 bytes まで NUL を探し、raw bytes を RPC に載せる。
Broker が当該 OS の同一規約で解釈し、別名表に照合する。埋込み font の処理は既存 PDFium
のまま維持する。custom adapter が欠けた状態で既定 filesystem font mapper に戻す経路は設けない。
[公開 API と固定版 wrapper](https://pdfium.googlesource.com/pdfium/+/a5a7089234f121990b336b3841008009dca143bf/fpdfsdk/fpdf_sysfontinfo.cpp)
想定する内部 API は次の程度に限定する。名前は実装時に確定する。
```cpp
struct FontRequest {
QByteArray faceLocal; // <= 256 bytes, no embedded NUL
int weight; // 0..1000
bool italic;
int charset; // supported FXFONT_* values
int pitchFamily; // accepted pitch/family bits only
};
struct SelectedFont {
QByteArray fontId; // opaque, scoped to this worker session/generation
QByteArray actualFaceLocal;
int charset;
quint64 size;
QByteArray sha256;
int faceIndex; // Linux FC_INDEX for a supported static face
quint64 faceOffset; // Windows GDI-derived TTC face offset; otherwise unset
bool substituted;
};
// Broker-owned asynchronous service; no PDFium dependency.
// select(request), read(fontId, offset, length), release(fontId), revoke(session)
// Worker-owned synchronous callback adapter over the routed async transport.
// fetchSelected(request) -> immutable FontBlob
```
## RPC 契約
新 operation は `font.map`、`font.read`、`font.close` の三つとする。
既存 envelope の `protocolVersion/sessionId/generation/requestId/operation` と
`payload` XOR `result/error` を維持する。`requestId` は現在実行中の親 PDF 要求の ID、
各 payload と result/error の `fontRequestId` は Worker が単調増加させる副要求 ID とする。
同時に保留する副要求は一つだけである。
| Operation | Worker → Broker payload | Broker → Worker result | 主な照合 |
| --- | --- | --- | --- |
| `font.map` | `fontRequestId, faceLocal, weight, italic, charset, pitchFamily` | `fontRequestId, found`。found=true のとき `fontId, actualFaceLocal, charset, size, sha256, faceIndex/faceOffset, substituted` | PDFWorker にだけ有効。現在の親 requestId/session/generation 一致。文字列をパスや fontconfig pattern 構文として解釈しない |
| `font.read` | `fontRequestId, fontId, offset, length` | `fontRequestId, fontId, offset, data` | ID は同セッションに発行済み、`1 <= length <= 65536`、加算 overflow と `offset+length <= size` を確認。返却サイズは要求どおり |
| `font.close` | `fontRequestId, fontId` | `fontRequestId, released:true` | 発行済み ID を一度だけ解放。revoke 後の ID は無効 |
全応答は control frame のまま 1 MiB 以下とし、64 KiB を超える data は拒否する。
`more` は使わず、一回の read に一回の応答を返す。最終 size と SHA-256 が一致してから
blob を PDFium に公開する。CRC 等の font 内容検証を Main で行う意味ではなく、転送を
途中で使わないための完整性確認である。
Broker の受信側は通常 PDF 応答を照合する前に、`payload` を持つ三つの font operation を
専用分岐へ送る。通常の `active_` を完了させず、キューを進めず、親 deadline を延長しない。
不正な role/ID/offset/連続要求は既存の protocol failure として Worker を停止する。
ArchiveWorker と試験用一般 worker には、このサービスを明示的に登録しない。
ファイル名や fontconfig の `FC_FILE` は応答に含めない。
font が存在しない場合は `found:false` として PDFium 内部代替と UI の代替警告を許す。
通信破損、資源上限超過、timeout は font 不在と区別し、当該 PDF 操作を失敗させる。
新規 error code を増やす場合は data contract に登録し、未定義の code を先行実装しない。
## 同期 callback、deadlock、取消
現在の `src/pdf/main.cpp` は `IpcChannel::messageReceived` の direct slot 内で PDFium
を呼ぶ。そのまま callback 内で `QEventLoop` を回して font を待ってはいけない。
Qt の `readyRead` は再帰的に再送されず、Windows の現 `WindowsPipeDevice::completeRead`
も `emit readyRead()` の後に次の `beginRead()` を置いている。いずれも受信処理の
stack を抜ける前に次の受信を待つ構成が停止の原因になる。
[QIODevice の通知規約](https://doc.qt.io/qt-6/qiodevice.html#readyRead)
1. Worker の受信 slot は envelope と状態を検証する router にする。通常の PDF 要求は
一件だけ保存し、`Qt::QueuedConnection` または queued invocation で後から実行する。
queued 実行が始まる前から reserved 状態とし、二件目の通常要求を通さない。
2. PDFium 処理は受信通知の stack を抜けた後、引き続き同じ Worker thread で実行する。
PDFium の並列呼出しや新 Worker thread は導入しない。
3. callback の同期待ちは、該当 font 応答・transport failure・副要求 deadline だけで解決する。
router は font 応答を通常要求の handling guard より先に処理する。待機中に他の PDF
操作を実行しない。callback から C++ exception を C ABI 越しに送出しない。
4. Main の選択/ファイル読取りは専用 Broker thread に非同期で渡す。HDC と fontconfig
config はその thread が所有する。Main UI thread で同期待ちをせず、font 選択処理から
Worker の応答を待たない。送信時に Worker の生存・session・generation を再確認する。
5. Worker 停止/新 generation/staging 取消で Broker の ID と待機処理を revoke する。
進行中 OS API の終了を GUI が待つ必要はない。遅れて来た結果は捨てる。既存の process
kill と親 deadline が最終的な中断手段になる。単なる検索キュー破棄では、現在の render
に必要な font を revoke しない。
QueuedConnection は受信側 event loop へ制御が戻ってから slot を実行する。
同じ thread で `BlockingQueuedConnection` を使う方式は採らない。
[Qt connection type](https://doc.qt.io/qt-6/qt.html#ConnectionType-enum)
## OS ごとの Broker 選択
### Linux
Broker が `FcInitLoadConfigAndFonts`/`FcFontList` または `FcConfigGetFonts` で許可索引を作り、
font のファミリー・style・charset coverage・`FC_FILE`・`FC_INDEX` を内部に保存する。
Worker の値は `FcPatternAddString` 等の typed API の値として渡し、`FcNameParse` に流さない。
固定 alias と優先順位を先に適用し、`FcConfigSubstitute`、`FcDefaultSubstitute`、
`FcFontMatch`/必要なら限定した `FcFontSort` の順で選ぶ。OpenType weight は
`FcWeightFromOpenType` で変換し、固定幅/serif/italic と charset に対応する言語を保持する。
`FcFontMatch` は事前 substitution が必要であり、`FC_INDEX` はファイル内の face index である。
[Fontconfig 一次リファレンス](https://fontconfig.pages.freedesktop.org/fontconfig/fontconfig-devel/)
結果は Broker が構築した索引内の候補に再照合する。Worker からファイル、ディレクトリー、
fontconfig config、`FC_FT_FACE`、任意 pattern オブジェクトを受け取らない。選ばれた索引の
ファイルを Broker が read-only で開き、通常ファイルとサイズを確認して bounded snapshot
を作る。索引作成時と file identity が変わっていれば、その候補を再索引または拒否する。
font 更新との競合でバイト列が壊れても、font 内容を解析するのは隔離 Worker である。
索引の入力は OS/アプリの font 設定だけであり、PDF 内の添付 font を fontconfig に登録しない。
単に `sans:lang=ja` に委ねることは固定順序の代わりにならない。今回の環境では
`fc-match 'sans:lang=ja'` と `serif:lang=ja` が両方 `FORM UDPGothic` を返した。
これは環境の観測であり一般的な fontconfig の保証ではない。
### Windows
Broker が `EnumFontFamiliesExW` で名前・style・charset の索引を作る。候補を
`LOGFONTW` と `CreateFontIndirectW` で作成し、Broker 専有 HDC へ `SelectObject` して
`GetTextFaceW`/`GetTextMetricsW` で実際の選択を検証する。要求名がそのまま選ばれる保証は
ないので、許可索引と alias 規則に照合する。HDC と HFONT は Broker thread だけで使い、
元の選択 object を戻してから `DeleteObject`/`DeleteDC` する。
[列挙](https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-enumfontfamiliesexw)、
[論理 font の選択](https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-createfontindirectw)、
[LOGFONTW](https://learn.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-logfontw)
`GetFontData` で選択済み font をバイト列にする。collection は GDI の `'ttcf'`
`0x66637474` で全体を取得し、table 0 のサイズとの差から選択 face の offset を求める。
通常の TTF/OTF は table 0。`GDI_ERROR`、0、上限超過を失敗として扱う。Worker に HFONT
や HDC は渡さない。font ファイルパスの探索も Worker へ移さない。必要追加 library は
Broker target の `gdi32` だけである。
[GetFontData の全体・collection・サイズ取得](https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-getfontdata)
OS font のバイト列はプロセス内の表示用途とし、文書への再埋込みや配布用ファイル生成を
この機能に含めない。同梱 font は別途、配布許諾・LICENSE・hash を固定する。
## TTC、table、short buffer の具体的な注意
公開 API に faceIndex を直接 PDFium へ渡す欄はない。固定版 `GetCachedTTCFace` は
`ttc_size - data_size` を face offset とし、それを collection 内の face index に戻している。
そのため Linux の `FC_INDEX` も Windows の選択済み HFONT も、下表の callback の振舞いへ
変換しなければならない。全ファイルだけを table 0 として返す実装では collection の
選択 face が失われる。
[固定版 mapper の TTC 経路](https://pdfium.googlesource.com/pdfium/+/a5a7089234f121990b336b3841008009dca143bf/core/fxge/cfx_fontmapper.cpp#784)
| 入力/状況 | Worker adapter の契約 |
| --- | --- |
| 単独 TTF/OTF、table 0 | ファイル全体の size/bytes |
| 単独 TTF/OTF、`0x74746366` (`ttcf`) | 0、collection ではない |
| TTC、`ttcf` | collection 全体の size/bytes |
| TTC、table 0 | `collectionSize - selectedFaceOffset`。必要な bytes は selectedFaceOffset から末尾まで |
| 通常の table tag | 選択 face の directory を参照。tag は PDFium 側の big-endian 数値、offset/length は checked arithmetic で blob 内に制限 |
| `buffer == nullptr` または size 0 | 必要サイズだけ返す |
| buffer が足りる | 全対象 bytes をコピーし、コピーしたサイズを返す |
| 小さい非 NULL buffer | 最大 buf_size だけ prefix を埋め、公開 header に従い必要サイズを返す。後述の固定版 1024-byte 読取りを試験する |
TTC header の offsets と各 SFNT table directory を Worker の bounded reader で解析する。
offset は collection の先頭を基準とする。face count、table count、offset + length、重複 tag、
選択 index を検証し、Main にこの font parser を置かない。
[OpenType collection と table directory](https://learn.microsoft.com/en-us/typography/opentype/spec/otff#font-collections)
固定版 `GetChecksumFromTT` は 1024 bytes の buffer を渡し、返却サイズを使わず、その
buffer から checksum を計算する。したがって「短い buffer では何も書かず必要サイズだけ
返す」という解釈では未初期化 bytes が残る。adapter は buffer 範囲を初期化し、存在する
prefix を必ず埋める。短い collection の場合の余りはゼロにする。この挙動は公開 API
記述だけから推定せず、固定版実装を対象とする回帰試験にする。
[固定版 checksum 呼出し](https://pdfium.googlesource.com/pdfium/+/a5a7089234f121990b336b3841008009dca143bf/core/fxge/cfx_fontmapper.cpp#333)
Windows GDI は tag の整数 byte order が異なる。PDFium の既定 Windows adapter も
`FromBE32(table)` で変換してから GDI に渡している。Worker の SFNT reader と GDI の
`GetFontData` を同じ tag 値で無条件に呼ばない。
[固定版 Windows adapter](https://pdfium.googlesource.com/pdfium/+/a5a7089234f121990b336b3841008009dca143bf/core/fxge/win32/cwin32_platform.cpp#456)
初期対応は static TTF/OpenType/TTC/OTC とする案を推奨する。variable font の named
instance、色付き font、Type1 の扱いを実装せずに対応済みとはしない。索引にはあっても
未対応なら別の静的候補を選び、代替を記録する。variable instance を黙って face 0 として
渡す方式は採らない。これらは既存 mapper に対する潜在的な互換性縮小である。
## 上限と lifetime の初期案
以下は設計書に既存の値ではなく、font broker 向けの提案値である。CJK collection の実測と
性能試験で妥当性を確認してから contract に固定する。
| 対象 | 初期上限案/扱い |
| --- | --- |
| face 名 | raw bytes 256、embedded NUL 拒否 |
| 1 font/collection snapshot | 64 MiB。超過時は別候補、なければ明示的制限 |
| 1 read | 64 KiB、同時副要求 1、write queue に全体を先積みしない |
| font 選択 handle | session 内 16、通常は map→read 全体→close で短く保持 |
| Broker snapshot cache | 全体 256 MiB、参照中を避ける LRU。複数 session 合計で計測 |
| Worker font blob cache | 128 MiB。callback handle が参照中の blob を捨てない |
| session の異なる選択数/転送累計 | 256/512 MiB。repeated malicious names による無制限列挙を防ぐ |
| RPC 待機 | 1応答 5秒、1 font 取得全体 15秒。成功 chunk ごとに全体期限を延長しない |
| 全体の処理 | 既存親要求期限と 1.5 GiB Worker 制限を維持。font 待機もその中に含める |
64 MiB は未検証の大きな CJK collection を切り捨て得るため、上限到達を「font が無い」と
黙って扱わない。PDFium 自身の font cache も bytes を複製するので、adapter cache のみを
数えて 1.5 GiB を守れたと判断しない。Broker の snapshot と OS index は Worker から
呼び出せる任意ファイル読み取りサービスに拡張しない。
## 固定 alias と回帰 corpus
fontconfig の全ホスト設定を置き換えるのではなく、アプリが管理する候補順を先に試す。
例として Base14 の Courier/Helvetica/Times と各 bold/italic を個別の entry にし、
承認した同梱書体、既知の metrically compatible OS 書体、最後に charset/family に沿う
OS 選択という順を固定する。Symbol/ZapfDingbats は generic sans に置換せず、既存の
PDFium 内蔵経路または専用の承認候補を維持する。
日本語は Gothic/Mincho、等幅/比例、weight/italic、英語名/ローカル名を分ける。
`@` で始まる縦書き face、Shift-JIS に由来する face 名、CJK の charset を別ケースとして
試験する。alias の具体的な font を決める前に、対象 OS に存在することと配布条件を確認する。
試験用 BIZ UDPGothic の埋込み許諾が確認済みでも、それだけで製品の全 fallback 資産が揃った
ことにはならない。
| Corpus/試験 | 固定する観測と合格条件 |
| --- | --- |
| 既存埋込み日本語/画像/透明合成/CropBox PDF | font broker を入れる前後で選択・glyph・配置が変わらない。埋込み font を system font に置換しない |
| 既存 `unembedded-font.pdf` | 現在は Standard-14 の限定 corpus。選択 family/file hash/style を記録し、Poppler 対照と文字位置・幅を比較 |
| 新規未埋込み日本語 | 正しい CID encoding を持つ horizontal/vertical の原本を生成。単に subset font stream を削除する方式は glyph ID が変わるため用いない |
| alias/weight/italic/symbol | exact installed name、Base14 alias、不在名、日本語ローカル名、記号をそれぞれ検証。Broker が選んだ実名と hash も期待値に含める |
| TTC の複数 face | 同一 collection の異なる faceIndex を描画し、face 0 固定にならないこと。table offset/collection read/1024-byte prefix を単体試験 |
| malformed font bytes | truncated directory、過大 table count、範囲外 offset、加算 overflow、重複 tag、短い buffer を Worker adapter 単体で拒否 |
| sandbox 攻撃 | font broker 正常動作中でも Worker から font directory/設定/無関係ファイルを open できない。path 風 face 値を使っても任意パスを読まない |
| RPC/取消 | 偽 fontId、別 generation、重複応答、途中切断、読取り範囲超過、response timeout、font 取得中の close/kill、遅い応答を投入。UI が待ち合わせで停止しない |
| lifecycle/メモリー | 一覧にない要求の連打、font 名の大量変化、TTC 再利用、長時間読書を測定。Broker と Worker 両方のメモリー、送信 queue、UI 応答を記録 |
| 両 OS | alias policy、選択結果、PDFium adapter が同じでも OS font の bytes が違う点を記録。Windows は native 環境で実施するまで未検証 |
この検証は固定 corpus に対する互換性範囲を増やすもので、欠けた元 font の完全再現や
任意 PDF の一致を保証しない。既存 Linux/Poppler 比較は embedded 日本語と Standard-14 を
中心にしており、未埋込み日本語・TTC・variable instance の新経路の合格証拠にはならない。
## 必要変更箇所と実装順
| 場所 | 変更 |
| --- | --- |
| 新 `src/common/font_contract.*` | font RPC の型・値・サイズ・role・ID 検証。QtCore のみ |
| 新 `src/broker/font_broker.*` | OS index、alias、選択、snapshot と quota。PDFium 非依存。OS backend は別ファイルに分割 |
| 新 `src/pdf/system_font_adapter.*` | 公開 SYSFONTINFO、immutable blob、checked SFNT/TTC reader。PDFWorker/adapter test にだけリンク |
| `src/common/ipc.*` | 三 operation の許可と envelope の限定拡張。control/data 上限は増やさない |
| `src/common/worker_process.*` | font request の role 分岐、親 active 保持、応答送信、deadline、stop 時 revoke |
| `src/pdf/main.cpp` | router と queued PDF 実行、font response 待機、font directory 権限の全削除 |
| `src/pdf/pdf_document.*` | adapter を init 時登録し destroy まで保持。テストは explicit in-memory font provider を注入可能にする |
| `src/app/controller.*` | PDF session だけに Broker を登録。代替・制限警告と寿命管理 |
| `CMakeLists.txt` | Broker Linux backend に fontconfig、Windows backend に gdi32。Main に `docview_pdf`/PDFium を追加しない |
| tests/fixtures/results | 契約、callback、実 IPC、sandbox denial、両 OS 描画比較を追加 |
実装順は、(1) memory-only callback と TTC 単体試験、(2) bounded font 契約+偽 provider で
同一 transport の応答/取消/deadlock 試験、(3) Linux index と alias、(4) font directory
権限を外した PDFWorker で回帰、(5) Windows GDI backend と native gate とする。
最初の callback 実装と IPC 試験は独立して進められるが、権限削除だけを先行して
既定 font mapper の失敗を内部 fallback で隠さない。
## 調査の検証範囲
公開 local header、固定 commit の PDFium 公式ソース、Fontconfig・Qt・Microsoft の一次資料を
読んで確認した。Linux の fontconfig 2.18.3 と現在の `fc-match` 選択を読み取りで確認した。
この調査では production code を変更せず、callback prototype の実行、逆方向 RPC、GDI の
native 実行は行っていない。上限値・alias の最終集合・描画閾値は、実装と corpus 計測で
確定する必要がある。
+44
View File
@@ -0,0 +1,44 @@
# メモリ圧迫への対応
原設計02「キャッシュと資源管理」と06「先読みの自動抑制」を実装する。
描画キャッシュの設定値とは別に、OSが報告する利用可能な物理メモリを監視する。
Linuxでは `/proc/meminfo` の `MemTotal` / `MemAvailable`、Windowsでは
`GlobalMemoryStatusEx` の物理メモリ値を用いる。1秒間隔で読み、圧迫状態の変化を
Controllerへ通知する。パーサーワーカーにはこの監視を追加しない。
## 閾値と遷移
閾値は実装時に決めた暫定値であり、原設計08の性能予算を変更しない。
- 空きが総物理メモリの10%未満、かつ1 GiB未満になったら先読みを止める。
- 同条件が続くと、5サンプルごとに不可視キャッシュ回収、解像度抑制、文書処理停止へ進む。
- 空きが3%未満、かつ256 MiB未満の場合は、毎サンプル1段階ずつ進む。処置の順を飛ばさない。
- 空きが15%以上、または1.5 GiB以上の状態が5サンプル続いたら通常状態へ戻す。
- 取得失敗、不正な数値、欠損、重複、オーバーフローは状態を維持し、連続判定をリセットする。
不明な値を空きゼロや回復として扱わない。
監視はOSの物理メモリ報告に基づく。cgroup固有の割当量、GPUメモリ、仮想アドレス空間の
残量をこのサンプルから推測しない。これらを含む全種類の割当失敗を予知する保証ではない。
既存のworker資源上限、renderer監視、キャッシュ予算と併用する。
## 文書処理
PDFの先読み停止は設定値を書き換えず、既存の描画世代を失効させる。
不可視キャッシュは位置が変わった後も回収し、表示中の画像を優先する。
解像度抑制では論理倍率とページ・注釈座標を変えず、描画時の画素密度を原則半分にする。
workerが受け付ける最小倍率・DPRを下回らないよう制限する。読書位置を維持し、通知する。
低解像度画像が揃った後に旧画像を回収する。回復時は通常の解像度で再描画する。
HTML/ZIP/EPUBのWebEngine内部キャッシュや描画解像度を外部から操作したとは扱わない。
これらは圧迫継続の通知と、最終段階での資源提供・renderer・展開処理の停止を適用する。
最終段階では読込中の新文書を取り消し、現在の文書の処理を止め、PDF画像も回収する。
状態は `Recovering` とし、原本へ書き込まない。圧迫中の新規openを拒否する。
メモリが回復しても停止した文書は自動で再読込せず、利用者が明示的に開き直す。
## 検証方法
メモリを実際に枯渇させず、同じ監視・Controller・Canvas経路へ有限のサンプルを注入する。
状態遷移、閾値境界、破損サンプル、ヒステリシス、先読み・キャッシュ・解像度・座標の保持、
取消、原本ハッシュ、停止後の明示的再openを試験する。Linuxの実サンプル取得も別に確認する。
この方法をOS全体のOOM耐性試験やWindows実行の証拠とはしない。結果は
[検証記録](VALIDATION.md)へ記録する。
+64
View File
@@ -0,0 +1,64 @@
# PDF Form構造見出しアダプターの実装契約
状態: **実装済み、Linux回帰確認済み**(2026-09-19)。当初のForm-only構造を取得できない制約は、Worker内の共有qpdfメタデータ読取で解消した。本書は実装範囲を記録し、全OS・任意の実文書に対するG-HEADINGS合格を宣言するものではない。[試験記録](../tests/results/pdf-structure/README.md)、[合成資料の期待値](../tests/fixtures/pdf/headings/manifest.json)を参照。
## 構成と所有権
`PdfDocument`は一つの`PdfMetadataContext`を作り、`LinkBorderReader`と`StructureReader`で共有する。qpdf 12.4.1はPDFWorkerだけのprivate依存であり、MainにはPDF解析オブジェクトを渡さない。元の読み取り専用QFileとパスワードをPDFium/qpdfの双方が使用し、パスからの再openや原本への保存を行わない。qpdf入力の論理オフセットは独立し、各読取でQFileをseekする。終了時は両reader、context、PDFium document、入力の順で破棄する。
構造解析は`headings(page)`時だけ行う。open時に全構造・全ページを走査しない。共有contextは生のページ木を順番に検証し、読んだページ参照を保持する。探索の例外後はそのcontextのページ探索を停止し、次要求で壊れたノードを飛ばしてページ番号を詰めることを禁止する。
qpdf 12.4.1の`setMaxWarnings(nonzero)`には、初期parseでページ木を走査・修復する副作用があるため使用しない。公開`QPDFLogger::setWarn`の破棄Pipelineで、初期解析を含め警告32行・64KiBを制限する。sinkは警告本文を保持・出力せず、超過はstickyにして以降の入力読取でも停止する。qpdfはsinkを呼ぶ直前に警告を内部へ追加するため、境界を越えた最後の1件は内部に存在し得る。xref回復は無効である。この選択はnull Kid保持、32/33警告、初期警告bytes超過の回帰で検証した。[qpdf 12.4.1 Objects::parse](https://github.com/qpdf/qpdf/blob/v12.4.1/libqpdf/QPDF_objects.cc)、[Pages::cache](https://github.com/qpdf/qpdf/blob/v12.4.1/libqpdf/QPDF_pages.cc)
## 構造の所有元と順序
対象ページと実際に`Do`で呼ばれたFormの`StructParents`から、`ParentTree`のNums/Kidsを解決する。リソース辞書にあるだけの未使用Formは見出し候補にしない。候補の`P`をたどってH/H1–H6を得て、各祖先の順序付き`K`内の位置から表示順を決める。RoleMapは循環と深さを検証する。文字の大きさから見出しを推測しない。
`K`の整数MCID、MCR、配列、子StructElemを同じ順序で展開する。`Pg`は最寄りの構造祖先から継承し、局所指定を優先する。`Stm`付きMCRの所有元はそのForm object/generation、なしならページである。**MCID単独をキーにせず、所有元object/generationと組にする。** 各参照をParentTreeへ逆照合し、その所有元のMCIDが同じ見出しの祖先経路に属することを確認する。子要素のP/K不一致も拒否する。
跨ページのHはページ別fragmentとして返す。同一ページ内では構造オブジェクト同一性で重複を除き、同名の別見出しは統合しない。これにより、ページ、別Form、別ページに同じ数値MCIDがあっても文字や位置を借用しない。
## PDFiumオブジェクトとの対応
固定PDFium公開APIはFormの元stream object番号を返さない。このためqpdfの公開tokenizerで、ページと使用Formの`q/Q/cm/Do`、`BDC/BMC/EMC`、名前付きProperties、Form MatrixとResourcesを解析する。画像Doは構造Formに数えず、inline imageは画素を復号しない。未知演算子は対応の信頼性を下げ、壊れたgraphics stackは制約応答にする。
PDFium側は公開Form/PageObject APIで親子関係、局所行列、直接のMCID集合を集める。親ごとに行列とMCID集合が一意に合うFormだけを対応させ、列挙順で決めない。固定版ではFormオブジェクトのGetMatrixは呼出元CTMを示し、そのForm自身のMatrixは子オブジェクトの行列に含まれる。非identity Matrixを持つ入れ子の資料で、この扱いとページ座標を検証している。
`FPDFText_GetTextObject`から文字の所有Formを得て、対応済みの所有元付きMCIDへ文字とbboxを集める。`FPDFText_GetCharBox`はページuser spaceなのでForm行列を再適用しない。exactのx/y/rectは従来のPDF-point契約を保つ。位置の往復変換はCropBox、ページRotate、ユーザー回転0/90/180/270度で0.5pt以内を検証する。
## 応答とfallback
| 条件 | 応答 |
|---|---|
| 構造上のH、ページ、所有元、文字bboxを一意に確認 | `source=pdf-tag, precision=exact`、x/y/rect |
| Hとページを確認したがForm対応が曖昧、同じFormを複数回呼出、文字bboxなし | `precision=page`と具体的reason。確認済みの文字、ActualText/T/Alt、名称なしの順でtitleを選ぶ。推測した座標は付けない |
| ページ/所有元を確認できない、ParentTree不整合、P/K/RoleMap/Form循環 | 空見出しと`structureLimited=true`、固定の`unavailableReason`。未確認を正常な「見出しなし」にしない |
| 解析quota超過 | `structureLimited=true, truncated=true, complete=false` |
| 応答件数/CBOR bytes超過 | 検証済み先頭項目を残し、上と同じ未完了フラグ |
| OBJRまたはannotation-owned StmOwn | 明示的制約。今回の通常Form対応に含めない |
同じFormの複数描画先を大きな矩形にunionしてexactとはしない。同一行列・MCID集合の別Formも列挙順で解決せずpage precisionにする。見出しの存在まで確定できない場合はpage移動先も作らない。
## 上限
| 対象 | 上限 |
|---|---|
| 共有qpdf入力 | 32MiB/操作、256MiB/文書、10秒/操作 |
| 警告 | 32行かつ64KiB/文書。初期解析から適用、超過後継続不可 |
| ページ木 | 深さ64、訪問200,000、未処理ノード100,000 |
| 内容展開 | 16MiB/stream、32MiB/要求ページ。展開中のPipelineで停止 |
| content token | 1,000,000/ページ、通常token64KiB、複合operand1MiB、operand256件、入れ子64 |
| Form呼出 | 10,000/ページ、深さ64 |
| PDFiumオブジェクト/文字 | 100,000オブジェクト、深さ64、1,000,000文字/ページ |
| 構造探索 | 共通step10,000、ParentTree未処理ノード10,000、深さ64 |
| 見出し応答 | 1,000項目、title4,096 UTF-16単位、CBOR512KiB(envelope余裕込み) |
streamの復号は公開`pipeStreamData`からbounded Pipelineへ行い、無制限のgetStreamData後検査をしない。復号済みstream・対応表・ParentTree参照のキャッシュは一要求の寿命だけで、別の文書全体構造キャッシュを設けない。OS側Workerメモリ制限、監視、取消時終了も維持する。Windowsは継承handleを包む同じQFile契約を使用するが、native実行の証拠は本記録に含まない。
## 検証範囲と残る制限
26個の構造資料で、元の6資料、Form-only、RoleMap/名前付きproperty、非identity入れ子、別Form同MCID、構造順、回転/CropBox、跨ページfragment、vector fallbackを検証した。陰性資料は循環、MCR/Pg/Stm不正、ParentTree所有矛盾、graphics stack破損、16MiB直前/超過、CBOR部分応答を含む。共有context専用資料4個と既存null Kid資料で、ページ番号保持と診断上限も確認した。
実Worker/FontBroker/IPCはexact、曖昧・繰返しpage fallback、quota、部分応答、取消を検証した。暗号化Formはqpdf CLIで試験時に作り、誤password拒否・正passwordの同じ借用QFileによるexact抽出・原本不変を確認した。画面上のForm-only heading移動はApp試験が別に担う。全体のrenderer比較、性能、配置、OS別受入記録は総合検証文書を参照する。
OBJR/StmOwnは未対応で、曖昧な描画instanceはpage fallbackに留まる。未検証の制作ソフト由来構造やnative Windowsの結果を、この合成資料の成功から推定しない。より広いexact対応には、元stream IDを公開するPDFium API等の別評価が必要である。
+33
View File
@@ -0,0 +1,33 @@
# 資源の警告集計
HTML・HTML ZIP・EPUBで読み込めない資源があっても、表示可能な本文を維持する。最初の検出時に一度だけ通知し、`:info`で理由別の検出件数を確認できる。件数は同じ資源の再試行も含み、固有URLの数ではない。
検出は三つの経路を使う。
- ResourceHandler: `ResourceFailure` の有限分類を使い、不在、危険な参照、OSアクセス拒否、許可外の資源形式、サイズ上限、アーカイブの展開上限・CRC/長さ破損、容量不足、保存失敗、読取I/O失敗、Worker停止・期限超過を区別する。OSやWorkerの生のmessage・パスは受け渡さない。
- DocumentInterceptor: 通信・文書外参照の拒否。Chromiumの割込み処理では上限付きatomicカウンターだけを更新し、250msごとにMainで集約する。
- ApplicationWorldの固定スクリプト: ブラウザー自身が発生させた`securitypolicyviolation`のうち、強制適用されたもの。画像、CSS、フォント等のdirective分類だけを取り出す。
集計は文書セッションのメモリー内だけに置く。URL、ファイル名、blockedURI、sourceFile、policy本文、sampleは収集しない。理由は固定24種類(broker 12、network 1、CSP 11)、各件数は999,999で飽和し、上限では「以上」と表示する。ブラウザーからの報告は固定11キー、有限の正整数、上限を全件検証してから反映する。不正な一項目があれば報告全体を捨てる。
| 検出 | 表示コード |
|---|---|
| 索引にない資源、OSが返す存在しないファイル・ディレクトリー | `E_RESOURCE_MISSING` |
| root/型/リンク等の安全検査拒否、OSの権限・共有拒否、許可外MIME(理由は別集計) | `E_RESOURCE_BLOCKED` |
| 1資源256 MiBまたは1書込64 KiBの上限 | `E_RESOURCE_LIMIT` |
| アーカイブの累積・資源展開上限 | `E_ARCHIVE_LIMIT` |
| CRC・圧縮データ・長さの不整合 | `E_ARCHIVE_CORRUPT` |
| OSが明示するディスク容量・割当量不足 | `E_STORAGE_FULL` |
| その他の一時資源の作成・書込・同期・確定失敗 | `E_STORAGE_FAILED` |
| その他の資源open・read失敗 | `E_RESOURCE_IO` |
| 不正/未知のworker失敗、期限超過 | `E_WORKER_CRASH` / `E_WORKER_TIMEOUT` |
`E_RESOURCE_LIMIT`、`E_STORAGE_FAILED`、`E_RESOURCE_IO` はこの追加契約のコードである。未知のOS失敗を欠落や容量不足と推測しない。WindowsではNTSTATUSをOSの変換APIで分類し、reparse/type/link/sizeは開いたhandleの情報から判定する。QFileは両OSとも所有済みfdを採用する。open後のread失敗は一度だけqueued signalでMainへ伝え、破棄・失効済みセッションには反映しない。
展開のcompletionは成功・取消・原因分類を保持する。WorkerProcessが実際に停止してRPC callbackを破棄した場合も、failed通知で保留中の資源jobへ停止理由を渡してからセッションを失効する。明示的な取消・文書切替の失効はCancelledのままとし、件数を追加しない。CRC・展開上限・不正worker応答では後続展開を停止し、既に表示した本文を保持する。失敗した途中ファイルは公開しない。保存容量不足とCRC不整合を同じコードへまとめない。汎用の章load失敗は`E_OPEN_FAILED`とし、資源不在と決め付けない。
取り消した文書、終了した文書、失効したセッションの報告は反映しない。固定EPUBの見開きでは現在表示する両ページから同じ文書へ集約する。履歴・保存位置には件数を保存しない。すべてのブラウザー内拒否が必ずCSPイベントを発生させるとは限らないため、一覧は実際に検出した事象を表す。
[Qtのinterceptor契約](https://doc.qt.io/qt-6/qwebengineurlrequestinterceptor.html)に従い、interceptRequest内でprofileのAPIやUIを呼ばない。[CSP仕様](https://www.w3.org/TR/CSP3/#securitypolicyviolationevent)のイベントを利用し、合成イベントは`isTrusted`で除外する。Qt 6.11.2での実際のイベント配送と通信拒否は[試験記録](../tests/results/resource-warnings/README.md)に残す。
分類の追加回帰は[resource-classification](../tests/results/resource-classification/README.md)に記録する。Windows nativeのアクセス拒否・容量不足・I/O配送は未検証であり、補助コンパイルはその代わりにならない。
File diff suppressed because it is too large Load Diff
+53
View File
@@ -0,0 +1,53 @@
# Windows worker sandbox port
Status: the Windows broker, bounded handle transport, and PDF/archive worker bootstrap are connected in source. Native Windows execution and an MSVC/Windows Qt build have **not** been performed. Successful Linux regression tests and a compile-only check against Wine declarations are not Windows security or compatibility evidence. R12, G-SANDBOX on Windows, and distribution acceptance remain open.
## Launch and verification
`WorkerProcess` uses `WindowsRuntimeStaging`, `createWindowsPipePair`, and `launchWindowsWorker` on Windows. There is no unprotected `QProcess` fallback. The broker opens the original document with read access, checks the file type and reparse attributes, and supplies only duplicated handles to the worker. The shared installation tree and original document ACL are never changed.
The launcher creates a unique temporary AppContainer profile and requires the **LPAC** process attribute, zero capability SIDs, low integrity, a one-process Job, a 1.5 GiB per-process committed-memory limit, kill-on-close, no Job breakaway, and desktop/clipboard restrictions. The child-process policy attribute also blocks child creation. The process starts suspended; token identity and Job limits are checked before it resumes. Environment variables are reduced to the private runtime/System32 search path and SystemRoot. Arguments use Windows C runtime quoting without a shell.
Handle inheritance is limited to a read-only document handle, two directional overlapped pipes, and one inert `NUL` handle for standard streams. The child receives `--document-handle`, `--ipc-read-handle`, `--ipc-write-handle`, and `--sandbox-sid`; it rejects `--source` and `--socket` on Windows. `startWorkerRuntime` verifies the actual kernel token, LPAC state, SID, capability count, integrity level, immediate Job membership and limits, and private-profile access restrictions. A flag or SID argument alone cannot establish protection. A failed bootstrap exits with code 5 before parsing.
The document handle is duplicated again with only read rights and the inherited original is closed. `_open_osfhandle` transfers the restricted handle to a CRT descriptor; `QFile` owns that descriptor. PDFium's file-access callback and libzip's seekable source callback use this already-open `QFile`. They never reopen the original path. Source callbacks retain bounded index and entry checks; archive output is streamed as provisional chunks to the Main broker, which alone verifies and commits files.
`WindowsPipeDevice` uses overlapped I/O, `QWinEventNotifier`, a 256 KiB transport read buffer and a 16 MiB write queue. CBOR framing retains the 1 MiB control / 8 MiB render limits. Worker extraction drains pending transport writes above 128 KiB, with a five-second timeout; synchronous parser callbacks reject reentrant commands during this drain. `WorkerProcess` waits for a protected bootstrap ping before reporting the Windows worker ready.
## Private profile and runtime storage
Creating an AppContainer also creates writable private storage. That default is insufficient for a read-only parser. The launcher replaces DACLs only on its newly generated profile and broker-created runtime copies: the owner retains full access, the package receives read/execute access, and explicit package deny entries block file creation, writes, deletion, ACL changes, and ownership changes. Existing descendant entries are treated similarly. Reparse points and files with multiple hard links are rejected. The profile filesystem is checked again while the child is suspended.
The broker briefly impersonates the suspended child's token to obtain its own profile registry HKEY with `GetAppContainerRegistryLocation`, then returns to the broker identity before applying protected DACLs recursively. Package registry access is limited to read; creating keys/links, setting values, deleting keys and changing ACLs/ownership are explicitly denied. Unexpected registry links, oversized trees, and API failures stop launch. The child independently attempts to acquire each write permission on its profile directory and registry root and requires access-denied results. These checks do not create files or values.
The launcher exposes no writable worker output directory. The profile is deleted on normal `NativeProcess` destruction after Job termination and handle closure; recovery after interrupted broker shutdown still needs native validation. Runtime staging uses a finite DLL/executable manifest with hashes and private owner permissions. It does not copy user font directories or grant access to document parents, user configuration/state, or shared installation directories.
## Font behavior and remaining validation
The PDF worker now installs the public `FPDF_SYSFONTINFO` version 2 adapter and requests selected font bytes through `font.map`, `font.read`, and `font.close` on its existing protected IPC transport. It does not reactivate PDFium's default filesystem font provider. Main does not link PDFium or parse SFNT/TTC tables. See the [font broker contract](font-broker-contract.md).
The source Windows backend builds an OS font index with `EnumFontFamiliesExW`, selects an indexed family using `CreateFontIndirectW` and a thread-owned HDC, verifies `GetTextFaceW`/`GetTextMetricsW`, and takes a bounded snapshot through `GetFontData`. A TTC includes its collection bytes and the selected face offset derived from GDI's collection and table-zero sizes. Only an opaque session ID, selected metadata, and 64 KiB byte chunks reach the worker; HDC/HFONT and paths stay in Main. GDI calls run on dedicated background threads, limited to two across active and revoked services, with 256 MiB of total snapshot charges. A shared LRU can reclaim unused snapshots from either service while protecting issued handles. Revocation drops late replies without joining an in-progress OS call. Variable-font instances are skipped and subsequent static candidates are tried. No extra LPAC font capability or font-directory ACL grant was added.
This Windows backend passed only a supplementary object-compilation check against Wine declarations and Linux Qt headers. An MSVC/Windows Qt build, actual GDI selection and TTC behavior, font RPC through LPAC, Japanese fallback, glyph metrics, and non-ASCII family names remain **unverified on native Windows**. Linux broker and drawing results are not Windows fallback-font evidence. The implementation follows the pinned [PDFium Windows adapter](https://pdfium.googlesource.com/pdfium/+/a5a7089234f121990b336b3841008009dca143bf/core/fxge/win32/cwin32_platform.cpp) and Microsoft's [GetFontData contract](https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-getfontdata); their semantics still need native execution tests.
Native acceptance must first prove successful PDF/ZIP/EPUB operation, then reject source writes and path reopen, sibling/configuration/state reads, external and loopback connections, child creation, unrelated inherited handles, runtime mutation, and writes to **own profile, its Temp descendants, own registry storage, and arbitrary temporary directories**. Tests must check actual API error codes and distinguish denied access from malformed test setup. Also verify memory enforcement, broker termination, watchdog/restart, profile cleanup, non-ASCII paths, resource reparse protection, renderer restrictions, and clean packaged installs. Each protection setup failure must leave the worker stopped.
## Evidence recorded on the Linux development host
After the shared bootstrap and libzip handle callback changes: Archive **57 passed**, including reading an unlinked-but-open source, rejecting writable/null sources, source-lifetime failure, and unchanged source hashes; PDF **15 passed** through the production `WorkerProcess` route; Canvas **11 passed** with real sandboxed PDF worker IPC. These exercise the Linux path and platform-independent callback behavior only.
`windows_sandbox.cpp` also passed a C++ syntax-only check using Linux Qt declarations and Wine Win32 headers. The local Wine headers lack current AppContainer declarations/constants, so documented Microsoft declarations were supplied in a temporary compile-only prefix. This is not a link test, ABI check, Windows Qt build, or native execution, and is not an acceptance gate.
## API references
- [Microsoft: Launch an AppContainer and LPAC](https://learn.microsoft.com/en-us/windows/win32/secauthz/implementing-an-appcontainer)
- [Microsoft: UpdateProcThreadAttribute](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-updateprocthreadattribute)
- [Microsoft: GetTokenInformation](https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-gettokeninformation)
- [Microsoft: QueryInformationJobObject](https://learn.microsoft.com/en-us/windows/win32/api/jobapi2/nf-jobapi2-queryinformationjobobject)
- [Microsoft: Job Object extended limits](https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-jobobject_extended_limit_information)
- [Microsoft: AppContainer profile folder](https://learn.microsoft.com/en-us/windows/win32/api/userenv/nf-userenv-getappcontainerfolderpath)
- [Microsoft: AppContainer registry location](https://learn.microsoft.com/en-us/windows/win32/api/userenv/nf-userenv-getappcontainerregistrylocation)
- [Microsoft: ImpersonateLoggedOnUser](https://learn.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-impersonateloggedonuser)
- [Microsoft: SetSecurityInfo](https://learn.microsoft.com/en-us/windows/win32/api/aclapi/nf-aclapi-setsecurityinfo)
- [Microsoft: `_open_osfhandle` ownership](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/open-osfhandle?view=msvc-170)
- [libzip: seekable source callback](https://libzip.org/documentation/zip_source_function/)
+403
View File
@@ -0,0 +1,403 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import DocView 1.0
ApplicationWindow {
id: window
width: 1100; height: 800; minimumWidth: 480; minimumHeight: 360
visible: true
title: reader.title + (reader.title === "DocView" ? "" : " — DocView")
property bool dark: reader.settings.ui.theme === "system"
? Qt.styleHints.colorScheme !== Qt.ColorScheme.Light : reader.settings.ui.theme !== "light"
property color backgroundColor: dark ? "#181c20" : "#f1f3f4"
property color surfaceColor: dark ? "#22282e" : "#ffffff"
property color textColor: dark ? "#d8e0e8" : "#24303b"
property color mutedColor: dark ? "#97a4b1" : "#586775"
property color accentColor: dark ? "#abc8de" : "#215f8e"
property bool tocVisible: reader.settings.ui.toc_visible
property bool pagesVisible: reader.settings.ui.pages_visible
property bool statusVisible: reader.settings.ui.status_bar
property var collapsed: ({})
property var tocRows: []
readonly property int visibleCurrentOutline: {
var current = reader.currentOutline, visible = -1
for (var i = 0; i < tocRows.length; ++i) {
if (tocRows[i].originalIndex === current) return i
// A collapsed ancestor represents the current section below it.
if (tocRows[i].originalIndex < current && collapsed[tocRows[i].originalIndex]) {
var depth = tocRows[i].depth || 0, within = true
for (var j = tocRows[i].originalIndex + 1; j <= current; ++j)
if ((reader.outline[j].depth || 0) <= depth) { within = false; break }
if (within) visible = i
}
}
return visible
}
property bool searchInput: false
property string previousContext: ""
property var commandNames: ({
"file.open": qsTr("文書を開く"), "app.quit": qsTr("終了"),
"operation.cancel": qsTr("処理を取り消す"), "document.root.choose": qsTr("文書フォルダーを指定"),
"document.info": qsTr("文書情報"), "history.clear": qsTr("履歴を消去"),
"scroll.down": qsTr("下へスクロール"), "scroll.up": qsTr("上へスクロール"),
"scroll.left": qsTr("左へスクロール"), "scroll.right": qsTr("右へスクロール"),
"scroll.half_down": qsTr("半画面進む"), "scroll.half_up": qsTr("半画面戻る"),
"scroll.page_down": qsTr("一画面進む"), "scroll.page_up": qsTr("一画面戻る"),
"nav.document_start": qsTr("文書の先頭"), "nav.document_end": qsTr("文書の末尾"),
"nav.page": qsTr("指定ページへ移動"), "nav.page_next": qsTr("次のPDFページ"),
"nav.page_previous": qsTr("前のPDFページ"), "nav.heading_next": qsTr("次の見出し"),
"nav.heading_previous": qsTr("前の見出し"), "nav.chapter_next": qsTr("次の章"),
"nav.chapter_previous": qsTr("前の章"), "panel.toc.toggle": qsTr("目次の表示切替"),
"panel.pages.toggle": qsTr("ページ一覧の表示切替"), "panel.status.toggle": qsTr("ステータスの表示切替"),
"focus.next": qsTr("操作する領域を切替"), "focus.content": qsTr("本文に戻る"),
"zoom.in": qsTr("拡大"), "zoom.out": qsTr("縮小"), "zoom.fit_width": qsTr("幅に合わせる"),
"view.rotate": qsTr("PDFを回転"), "command.open": qsTr("コマンドを入力"),
"config.reload": qsTr("設定を再読込"), "help.toggle": qsTr("操作一覧"),
"nav.back": qsTr("移動履歴を戻る"), "nav.forward": qsTr("移動履歴を進む"),
"search.open": qsTr("本文を検索"), "search.next": qsTr("次の検索結果"),
"search.previous": qsTr("前の検索結果"), "panel.next": qsTr("次の項目を選択"),
"panel.previous": qsTr("前の項目を選択"), "panel.collapse": qsTr("階層を折りたたむ"),
"panel.expand": qsTr("階層を展開"), "panel.first": qsTr("先頭項目を選択"),
"panel.last": qsTr("末尾項目を選択"), "panel.half_down": qsTr("項目を半画面進む"),
"panel.half_up": qsTr("項目を半画面戻る"), "panel.activate": qsTr("選択項目へ移動"),
"link.next": qsTr("次のリンク・PDF注釈を選択"), "link.previous": qsTr("前のリンク・PDF注釈を選択"),
"link.activate": qsTr("選択リンク・PDF注釈を開く"), "unbound": qsTr("割り当てなし")
})
function contextName(value) {
return value === "content" ? qsTr("本文") : value === "toc" ? qsTr("目次")
: value === "pages" ? qsTr("ページ一覧") : qsTr("共通")
}
color: backgroundColor
font.family: "monospace"
font.pixelSize: reader.settings.ui.font_size
function focusContent() {
previousContext = "" // Explicit navigation takes precedence over temporary UI restoration.
reader.context = "content"
if (reader.format === "pdf") pdf.forceActiveFocus()
else if (reader.format !== "") web.focusContent()
else if (recentList.count > 0) recentList.forceActiveFocus()
else content.forceActiveFocus()
}
function beginTemporary() {
if (!previousContext) previousContext = reader.context
}
function endTemporary() {
reader.mode = "normal"
var target = previousContext
previousContext = ""
if (!target) return // A successful action already selected its own focus.
if (target === "toc" && tocVisible && tocList.count > 0) {
reader.context = "toc"; tocList.forceActiveFocus()
} else if (target === "pages" && pagesVisible && pageList.count > 0) {
reader.context = "pages"; pageList.forceActiveFocus()
} else focusContent()
}
function setInputMode(mode) { reader.mode = mode }
function updateToc(resetSelection) {
var previous = resetSelection ? null : tocRows[tocList.currentIndex]
let list = [], skipDepth = -1
for (let i = 0; i < reader.outline.length; ++i) {
let row = reader.outline[i], depth = row.depth || 0
if (skipDepth >= 0 && depth > skipDepth) continue
skipDepth = -1
let copy = Object.assign({}, row)
copy.originalIndex = i
copy.hasChildren = i + 1 < reader.outline.length && (reader.outline[i+1].depth || 0) > depth
list.push(copy)
if (collapsed[i]) skipDepth = depth
}
tocRows = list
var selected = 0
if (previous) {
for (var row = 0; row < list.length; ++row)
if (list[row].originalIndex === previous.originalIndex && list[row].title === previous.title &&
list[row].href === previous.href && list[row].page === previous.page) { selected = row; break }
}
tocList.currentIndex = list.length ? selected : -1
}
function panelCommand(id) {
let list = reader.context === "toc" ? tocList : pageList
if (list.count === 0) return
let next = list.currentIndex
if (id === "panel.next") next++
if (id === "panel.previous") next--
if (id === "panel.first") next = 0
if (id === "panel.last") next = list.count - 1
if (id === "panel.half_down") next += Math.max(1, Math.floor(list.height / 56))
if (id === "panel.half_up") next -= Math.max(1, Math.floor(list.height / 56))
if (id === "panel.activate") { reader.activateItem(list.model[list.currentIndex]); return }
if (reader.context === "toc" && (id === "panel.collapse" || id === "panel.expand")) {
let row = tocRows[list.currentIndex]
let c = Object.assign({}, collapsed)
if (id === "panel.collapse") {
if (row.hasChildren && !c[row.originalIndex]) c[row.originalIndex] = true
else for(let i = list.currentIndex-1; i >= 0; --i) if ((tocRows[i].depth || 0) < (row.depth || 0)) { next = i; break }
} else {
if (c[row.originalIndex]) delete c[row.originalIndex]
else if (row.hasChildren) next++
}
collapsed = c; updateToc()
}
list.currentIndex = Math.max(0, Math.min(list.count-1,next))
list.positionViewAtIndex(list.currentIndex,ListView.Contain)
}
function beginInput(search) {
beginTemporary(); searchInput = search
reader.mode = search ? "search" : "command"
commandField.text = ""; commandField.forceActiveFocus()
}
function recentCommand(id) {
if (!recentList.count || reader.state !== "Empty") return
if (id === "link.next" || id === "link.previous") {
if (clearRecent.activeFocus) recentList.forceActiveFocus()
else clearRecent.forceActiveFocus()
return
}
if (id === "link.activate") {
if (clearRecent.activeFocus) reader.execute("history.clear")
else if (recentList.currentIndex >= 0) reader.openRecent(recentList.model[recentList.currentIndex].documentId)
return
}
let next = recentList.currentIndex
if (id === "scroll.down") next++
if (id === "scroll.up") next--
if (id === "nav.document_start") next = 0
if (id === "nav.document_end") next = recentList.count - 1
if (id === "scroll.half_down" || id === "scroll.page_down") next += Math.max(1, Math.floor(recentList.height / 52))
if (id === "scroll.half_up" || id === "scroll.page_up") next -= Math.max(1, Math.floor(recentList.height / 52))
recentList.currentIndex = Math.max(0, Math.min(recentList.count - 1, next))
recentList.positionViewAtIndex(recentList.currentIndex, ListView.Contain)
recentList.forceActiveFocus()
}
function endInput() { endTemporary() }
Connections {
target: reader
function onNavigationChanged() { window.updateToc() }
function onDocumentChanged() { window.collapsed = ({}); window.updateToc(true) }
function onRecentDocumentsChanged() {
Qt.callLater(function() { if (reader.format === "" && reader.mode === "normal") window.focusContent() })
}
function onUiCommand(id,args) {
if(id === "file.open") { beginTemporary(); reader.mode = "dialog"; fileDialog.open() }
else if(id === "panel.toc.toggle") { tocVisible = !tocVisible; if(!tocVisible && reader.context === "toc") focusContent() }
else if(id === "panel.pages.toggle") { pagesVisible = !pagesVisible; if(!pagesVisible && reader.context === "pages") focusContent() }
else if(id === "panel.status.toggle") statusVisible = !statusVisible
else if(id.startsWith("panel.")) panelCommand(id)
else if(id === "focus.next") {
previousContext = ""; let areas=["content"]; if(tocVisible && reader.outline.length)areas.push("toc");if(pagesVisible && reader.pages.length)areas.push("pages")
reader.context=areas[(areas.indexOf(reader.context)+1)%areas.length]
if(reader.context === "toc")tocList.forceActiveFocus();else if(reader.context === "pages")pageList.forceActiveFocus();else focusContent()
}
else if(id === "focus.content") focusContent()
else if(id === "command.open") beginInput(false)
else if(id === "search.open") beginInput(true)
else if(id === "help.toggle") { beginTemporary(); reader.mode="dialog"; helpDialog.open() }
else if(id === "document.info") { beginTemporary(); infoText.text=args.text; infoDialog.title=args.title || qsTr("文書情報"); reader.mode="dialog"; infoDialog.open() }
else if(id === "document.root.choose") { beginTemporary(); reader.mode="dialog"; folderDialog.open() }
else if(id === "history.clear") { beginTemporary(); reader.mode="dialog"; historyDialog.open() }
else if(id === "recent.navigate") recentCommand(args.command)
}
function onPasswordNeeded(name) { beginTemporary(); reader.mode="dialog"; passwordDialog.title=name; passwordField.text="";passwordDialog.open();passwordField.forceActiveFocus() }
function onEntryNeeded(candidates) { beginTemporary(); candidateList.model=candidates; reader.mode="dialog"; entryDialog.open();candidateList.forceActiveFocus() }
function onExternalLinkRequested(url) { beginTemporary(); externalDialog.destination=url;reader.mode="dialog";externalDialog.open() }
function onSettingsChanged() {
tocVisible=reader.settings.ui.toc_visible;pagesVisible=reader.settings.ui.pages_visible;statusVisible=reader.settings.ui.status_bar
if ((!tocVisible && reader.context === "toc") || (!pagesVisible && reader.context === "pages")) focusContent()
}
}
ColumnLayout {
anchors.fill: parent; spacing: 0
RowLayout {
Layout.fillWidth: true; Layout.fillHeight: true; spacing: 1
ColumnLayout {
objectName: "navigationSidebar"
visible: (tocVisible && (reader.outline.length > 0 || reader.outlineLoading)) || (pagesVisible && reader.pages.length > 0)
readonly property real panelWidth: Math.min(reader.settings.ui.panel_width,window.width*0.4)
Layout.preferredWidth: panelWidth
Layout.minimumWidth: panelWidth
Layout.maximumWidth: panelWidth
Layout.fillWidth: false
Layout.fillHeight: true; spacing: 1
Rectangle {
visible: tocVisible && (reader.outline.length > 0 || reader.outlineLoading)
Layout.fillWidth:true;Layout.fillHeight:true;color:surfaceColor
border.color:reader.context === "toc" ? accentColor : surfaceColor
ColumnLayout {
anchors.fill:parent;anchors.margins:8;spacing:6
Label { objectName:"outlineTitle";text:reader.outlineTitle;color:mutedColor;font.pixelSize:12 }
Label { objectName:"outlineLoading";visible:reader.outlineLoading;text:qsTr("目次を読み込んでいます…");color:mutedColor;wrapMode:Text.Wrap;Layout.fillWidth:true;textFormat:Text.PlainText }
ListView {
id:tocList;objectName:"tocList";Layout.fillWidth:true;Layout.fillHeight:true;clip:true;model:window.tocRows;currentIndex:0
delegate: Rectangle {
required property var modelData;required property int index
objectName:"tocRow";property bool readingCurrent:window.visibleCurrentOutline === index
property bool keyboardSelected:tocList.currentIndex === index
width:ListView.view.width;height:32;color:tocList.currentIndex === index ? (dark ? "#35414b" : "#dce6ee") : "transparent"
border.width:keyboardSelected ? 1 : 0;border.color:mutedColor
Label { anchors.fill:parent;anchors.leftMargin:4+(modelData.depth||0)*14;verticalAlignment:Text.AlignVCenter;color:textColor;elide:Text.ElideRight;text:(readingCurrent ? "> " : " ")+(modelData.hasChildren ? (window.collapsed[modelData.originalIndex] ? "+ " : "− ") : " ")+(modelData.title||qsTr("見出し"));textFormat:Text.PlainText }
MouseArea { anchors.fill:parent;onClicked:{tocList.currentIndex=index;reader.context="toc";reader.activateItem(modelData)} }
}
ScrollBar.vertical:ScrollBar{}
Accessible.name:qsTr("目次一覧")
}
}
}
Rectangle {
visible:pagesVisible && reader.pages.length > 0
Layout.fillWidth:true;Layout.fillHeight:true;color:surfaceColor
border.color:reader.context === "pages" ? accentColor : surfaceColor
ColumnLayout {
anchors.fill:parent;anchors.margins:8;spacing:6
Label{text:reader.listTitle;color:mutedColor;font.pixelSize:12}
ListView {
id:pageList;Layout.fillWidth:true;Layout.fillHeight:true;clip:true;model:reader.pages;currentIndex:0
delegate:Rectangle {
required property var modelData;required property int index
width:ListView.view.width;height:32;color:pageList.currentIndex === index ? (dark ? "#35414b" : "#dce6ee") : "transparent"
Label{anchors.fill:parent;anchors.leftMargin:4;verticalAlignment:Text.AlignVCenter;color:textColor;elide:Text.ElideRight;text:(reader.format === "pdf" && pdf.currentPage === index ? "> " : " ")+(modelData.title||modelData.href||String(index+1));textFormat:Text.PlainText}
MouseArea{anchors.fill:parent;onClicked:{pageList.currentIndex=index;reader.context="pages";reader.activateItem(modelData)}}
}
ScrollBar.vertical:ScrollBar{}
Accessible.name:reader.listTitle
}
}
}
}
Item {
id:content;objectName:"documentContent";Layout.fillWidth:true;Layout.fillHeight:true;focus:true
PdfCanvas { id:pdf;anchors.fill:parent;visible:reader.format === "pdf";backgroundColor:window.backgroundColor;Component.onCompleted:reader.attachCanvas(pdf);Accessible.name:qsTr("PDF本文") }
WebPane { id:web;anchors.fill:parent;visible:reader.format !== "pdf" && reader.format !== "" }
ColumnLayout {
id:initialScreen;objectName:"initialScreen"
readonly property real availableHeight:content.height-(noticeBanner.visible ? noticeBanner.height : 0)
anchors.centerIn:parent;width:Math.min(580,parent.width-48);spacing:8;visible:reader.format === ""
anchors.verticalCenterOffset:noticeBanner.visible ? -noticeBanner.height/2 : 0
Label { text:qsTr("Ctrl+O: 文書を開く");color:textColor;Layout.fillWidth:true }
Label { text:qsTr("?: 操作一覧");color:mutedColor;Layout.fillWidth:true }
Label { visible:recentList.count>0;text:qsTr("最近使った文書");color:textColor;Layout.topMargin:12;Layout.fillWidth:true }
ListView {
id:recentList;objectName:"recentDocumentsList"
readonly property real rowHeight: Math.max(52, window.font.pixelSize+34)
Layout.fillWidth:true;Layout.preferredHeight:Math.min(260,Math.max(rowHeight,initialScreen.availableHeight-4*window.font.pixelSize-154),count*rowHeight)
visible:count>0;enabled:reader.state === "Empty";clip:true
model:reader.recentDocuments;currentIndex:0;keyNavigationEnabled:true
onCountChanged:currentIndex=count>0 ? Math.max(0,Math.min(currentIndex,count-1)) : -1
Keys.onPressed:event=>{
if (event.key === Qt.Key_Home) currentIndex=0
else if (event.key === Qt.Key_End) currentIndex=count-1
else return
positionViewAtIndex(currentIndex,ListView.Contain);event.accepted=true
}
delegate:ItemDelegate {
required property var modelData;required property int index
width:ListView.view.width;height:recentList.rowHeight;padding:6;focusPolicy:Qt.NoFocus
highlighted:recentList.currentIndex===index
background:Rectangle {
color:recentList.currentIndex===index ? (dark ? "#35414b" : "#dce6ee") : surfaceColor
border.color:recentList.activeFocus && recentList.currentIndex===index ? accentColor : "transparent"
}
contentItem:Column {
Label { width:parent.width;text:modelData.title;textFormat:Text.PlainText;color:textColor;elide:Text.ElideRight }
Label { width:parent.width;text:modelData.path;textFormat:Text.PlainText;color:mutedColor;font.pixelSize:12;elide:Text.ElideMiddle }
}
Accessible.name:modelData.title+" — "+modelData.path
onClicked: { recentList.currentIndex=index;recentList.forceActiveFocus();reader.openRecent(modelData.documentId) }
}
ScrollBar.vertical:ScrollBar{}
Accessible.name:qsTr("最近使った文書。上下キーで選択し、Enterで開きます")
}
RowLayout {
visible:recentList.count>0;Layout.fillWidth:true
Label { text:qsTr("↑↓: 選択 Enter: 開く");color:mutedColor;font.pixelSize:12;Layout.fillWidth:true }
Button { id:clearRecent;objectName:"clearRecentDocuments";text:qsTr("履歴を消去…");enabled:reader.state === "Empty";onClicked:reader.execute("history.clear") }
}
}
Rectangle {
id:noticeBanner;objectName:"noticeBanner"
anchors.left:parent.left;anchors.right:parent.right;anchors.bottom:parent.bottom
visible:reader.notice.length > 0;height:noticeLabel.implicitHeight+20;color:surfaceColor
Label { id:noticeLabel;anchors.left:parent.left;anchors.right:parent.right;anchors.margins:12;anchors.verticalCenter:parent.verticalCenter;text:reader.notice;textFormat:Text.PlainText;wrapMode:Text.Wrap;color:textColor;Accessible.role:Accessible.AlertMessage }
}
DropArea { anchors.fill:parent;onDropped:drop=>{if(drop.hasUrls){reader.open(drop.urls[0]);drop.acceptProposedAction()}} }
}
}
Rectangle {
visible:statusVisible;Layout.fillWidth:true;Layout.preferredHeight:28;color:surfaceColor
RowLayout {
anchors.fill:parent;anchors.leftMargin:10;anchors.rightMargin:10
Label { Layout.fillWidth:true;text:reader.title;elide:Text.ElideMiddle;color:mutedColor;textFormat:Text.PlainText }
Label { text:reader.state === "Opening" ? qsTr("読込中") : reader.position;color:textColor }
Label { text:reader.pending;color:accentColor }
Label { text:reader.context === "content" ? qsTr("内容") : reader.context === "toc" ? qsTr("目次") : reader.listTitle;color:mutedColor;leftPadding:18 }
}
}
RowLayout {
visible:reader.mode === "command" || reader.mode === "search"
Layout.fillWidth:true;spacing:0
Label { text:window.searchInput ? "/" : ":";color:textColor;leftPadding:10 }
TextField {
id:commandField;Layout.fillWidth:true;selectByMouse:true;color:textColor;background:Rectangle{color:surfaceColor}Accessible.name:window.searchInput ? qsTr("本文検索") : qsTr("コマンド")
placeholderText:window.searchInput ? (reader.format === "epub" ? qsTr("表示中の章を検索") : reader.format === "pdf" ? qsTr("本文を検索") : qsTr("表示中のHTMLを検索")) : ""
onAccepted:{if(inputMethodComposing)return;if(window.searchInput){reader.search(text);endInput()}else if(reader.command(text)){if(reader.mode === "command")endInput();else text=""}}
Keys.onEscapePressed:event=>{if(!inputMethodComposing){endInput();event.accepted=true}}
}
}
}
FileDialog { id:fileDialog;title:qsTr("文書を開く");nameFilters:[qsTr("文書 (*.pdf *.html *.htm *.xhtml *.zip *.epub)"),qsTr("すべて (*)")];onAccepted:{reader.mode="normal";reader.open(selectedFile);endTemporary()}onRejected:endTemporary() }
FolderDialog { id:folderDialog;title:qsTr("文書フォルダーを指定");onAccepted:{reader.mode="normal";reader.setHtmlRoot(selectedFolder);endTemporary()}onRejected:endTemporary() }
Dialog {
id:passwordDialog;anchors.centerIn:parent;modal:true;standardButtons:Dialog.Ok|Dialog.Cancel
TextField{id:passwordField;width:300;echoMode:TextInput.Password;Accessible.name:qsTr("PDFパスワード");onAccepted:if(!inputMethodComposing)passwordDialog.accept()}
onAccepted:{reader.mode="normal";let value=passwordField.text;passwordField.text="";reader.password(value);endTemporary()}
onRejected:{passwordField.text="";reader.mode="normal";reader.cancel();endTemporary()}
}
Dialog {
id:entryDialog;objectName:"entryDialog";anchors.centerIn:parent;modal:true;title:qsTr("入口HTMLを選択");width:Math.min(600,window.width-48);height:Math.min(440,window.height-60);standardButtons:Dialog.Ok|Dialog.Cancel
ListView{id:candidateList;objectName:"entryCandidates";anchors.fill:parent;clip:true;currentIndex:0;delegate:ItemDelegate{required property string modelData;required property int index;width:ListView.view.width;text:modelData;highlighted:candidateList.currentIndex===index;onClicked:{candidateList.currentIndex=index;entryDialog.accept()}}Keys.onReturnPressed:entryDialog.accept()}
onAccepted:{reader.mode="normal";if(candidateList.currentIndex>=0)reader.chooseEntry(candidateList.model[candidateList.currentIndex]);endTemporary()}
onRejected:{reader.cancel();endTemporary()}
}
Dialog {
id:externalDialog;property url destination;anchors.centerIn:parent;modal:true;title:qsTr("外部ブラウザーで開く");width:Math.min(640,window.width-48);standardButtons:Dialog.Open|Dialog.Cancel
Label{width:parent.width;text:externalDialog.destination.toString();textFormat:Text.PlainText;wrapMode:Text.WrapAnywhere}
onAccepted:{reader.openExternal(destination);endTemporary()}
onRejected:endTemporary()
}
Dialog {
id:infoDialog;objectName:"infoDialog";anchors.centerIn:parent;modal:true;title:qsTr("文書情報")
width:Math.min(640,window.width-48);height:Math.min(implicitHeight,window.height-60)
standardButtons:Dialog.Close
contentItem:ScrollView {
id:infoScroll;objectName:"infoScroll";clip:true
contentWidth:availableWidth;implicitHeight:infoText.implicitHeight
Label{id:infoText;objectName:"infoText";width:infoScroll.availableWidth;wrapMode:Text.WrapAnywhere;textFormat:Text.PlainText}
}
onClosed:endTemporary()
}
Dialog {
id:historyDialog;objectName:"historyDialog";anchors.centerIn:parent;modal:true
title:qsTr("履歴と保存位置を消去");width:Math.min(580,window.width-48);standardButtons:Dialog.Ok|Dialog.Cancel
Label { id:historyConfirmation;objectName:"historyConfirmation";width:parent.width;wrapMode:Text.Wrap }
onAboutToShow:historyConfirmation.text=qsTr("%1 件の履歴と読書位置を消去します。原本は変更されません。").arg(reader.historyCount())
onAccepted:{reader.clearHistory();endTemporary()}
onRejected:endTemporary()
}
Dialog {
id:helpDialog;anchors.centerIn:parent;modal:true;title:qsTr("操作一覧");width:Math.min(760,window.width-48);height:Math.min(620,window.height-60);standardButtons:Dialog.Close
ColumnLayout {
anchors.fill:parent
Label{text:qsTr("Vim風の閲覧操作です。q単押しでは終了しません。:q で終了します。");wrapMode:Text.Wrap;Layout.fillWidth:true}
ListView {
Layout.fillWidth:true;Layout.fillHeight:true;clip:true;model:reader.settings.keybindings
delegate:Label{required property var modelData;width:ListView.view.width;text:modelData.keys.join(" ")+" "+(window.commandNames[modelData.command] || modelData.command)+" ["+window.contextName(modelData.context)+"]";textFormat:Text.PlainText;font.family:"monospace";height:26;elide:Text.ElideRight}
ScrollBar.vertical:ScrollBar{}
}
}
onClosed:endTemporary()
}
Shortcut { sequence:"?";enabled:helpDialog.visible;onActivated:helpDialog.close() }
Component.onCompleted:{updateToc();focusContent()}
}
+594
View File
@@ -0,0 +1,594 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtWebEngine
Item {
id: pane
clip: true
property var views: ({})
property var activeView: null
property var pendingView: null
property string searchQuery: ""
property var companionView: null
property var spreadSelection: ({})
property string failedCompanionUrl: ""
property bool arrangingPages: false
property real spreadZoom: 1
property bool spreadFitWidth: false
property real pagePanX: 0
property real pagePanY: 0
property real maxPagePanX: 0
property real maxPagePanY: 0
// Navigation requests expose a display string, while signal arguments and
// QML url properties can use percent encoding. Compare the same URL type.
function canonicalUrl(value) { return String(Qt.url(value)) }
// A spread always follows spine order; CSS direction does not reorder pages.
// Unspecified first pages occupy the trailing slot (the conventional cover).
function spreadFor(spine, current, rtl, landscape) {
var leading = rtl ? "right" : "left", trailing = rtl ? "left" : "right"
var groups = [], pending = null, first = true
function flush() { if (pending) { groups.push(pending); pending = null } }
for (var i = 0; i < spine.length; ++i) {
var row = spine[i]
if (row.linear === false) continue
if (row.missing || !row.href) { flush(); continue }
var policy = row.renditionSpread || "auto"
var eligible = row.layout === "fixed" && policy !== "none" && (policy === "both" || landscape)
if (!eligible || row.spread === "center") {
flush(); groups.push({center:i}); first = false; continue
}
var side = row.spread === "left" || row.spread === "right" ? row.spread : (pending ? trailing : (first ? trailing : leading))
first = false
if (side === leading) { flush(); pending = {}; pending[side] = i }
else { var group = pending || {}; group[side] = i; groups.push(group); pending = null }
}
flush()
for (var j = 0; j < groups.length; ++j)
if (groups[j].left === current || groups[j].right === current || groups[j].center === current) return groups[j]
return {center:current} // nonlinear items are independently navigable.
}
function resourcePath(url) {
return decodeURIComponent(String(url).replace(/^doc:\/\/[^/]+\//, "").split("#")[0])
}
function retireView(view) {
if (!view) return
resetHeartbeat(view)
view.alive = false
view.stop()
view.visible = false
if (typeof reader.registerRenderer === "function") reader.registerRenderer(view.documentToken, 0, view.rendererSlot)
view.destroy()
}
function closeCompanion() {
var old = companionView
companionView = null
if (old) retireView(old)
}
function synchronizeSpread() {
var view = activeView
if (!view || !view.alive || !view.documentReady) return
var opts = optionsFor(view), spine = opts.spine || [], current = -1
for (var i = 0; i < spine.length; ++i)
if (resourcePath(view.url) === decodeURIComponent(String(spine[i].href || "").split("#")[0])) { current = i; break }
spreadSelection = current >= 0 ? spreadFor(spine, current, !!opts.rtl, width > height) : ({center:-1})
var other = spreadSelection.left === current ? spreadSelection.right : spreadSelection.left
if (other === undefined || other === current || !view.fixedLayout || pendingView) {
closeCompanion(); arrangePages(); return
}
var url = "doc://" + view.documentToken + "/" + spine[other].href
if (canonicalUrl(url) === canonicalUrl(failedCompanionUrl) || !reader.navigationAllowed(url, WebEngineView.TypedNavigation, view.documentToken)) {
closeCompanion(); arrangePages(); return
}
if (!companionView) {
companionView = webComponent.createObject(pane, {profile:view.profile, documentToken:view.documentToken,
rendererSlot:1, isCompanion:true, permittedUrl:url})
if (!companionView) { arrangePages(); return }
companionView.url = url
} else if (canonicalUrl(companionView.url) !== canonicalUrl(url)) {
companionView.documentReady = false
companionView.pagePlaced = false
companionView.fixedViewport = ({})
companionView.permittedUrl = url
companionView.url = url
}
arrangePages()
}
function arrangePages() {
if (arrangingPages || !activeView) return
arrangingPages = true
var view = activeView, other = companionView
var a = view.fixedViewport, b = other ? other.fixedViewport : ({})
if (!view.fixedLayout || !(a.width > 0) || !(a.height > 0)) {
view.x = 0; view.y = 0; view.width = width; view.height = height; view.scale = 1
} else {
var currentLeft = spreadSelection.left !== undefined && resourcePath(view.url) === decodeURIComponent(String((optionsFor(view).spine || [])[spreadSelection.left].href).split("#")[0])
var paired = other && other.documentReady && b.width > 0 && b.height > 0
var slots = spreadSelection.center === undefined
var leftSize = paired && !currentLeft ? b : a, rightSize = paired && currentLeft ? b : a
var totalWidth = slots ? leftSize.width + rightSize.width : a.width
var totalHeight = paired ? Math.max(a.height, b.height) : a.height
var factor = (spreadFitWidth ? width / totalWidth : Math.min(width / totalWidth, height / totalHeight)) * spreadZoom
maxPagePanX = Math.max(0, totalWidth * factor - width)
maxPagePanY = Math.max(0, totalHeight * factor - height)
pagePanX = Math.max(0, Math.min(maxPagePanX, pagePanX))
pagePanY = Math.max(0, Math.min(maxPagePanY, pagePanY))
var origin = Math.max(0, (width - totalWidth * factor) / 2) - pagePanX
function place(page, size, left) {
page.zoomFactor = Math.max(0.25, Math.min(5, factor))
page.scale = factor / page.zoomFactor
page.width = size.width * page.zoomFactor; page.height = size.height * page.zoomFactor
page.x = origin + left * factor; page.y = Math.max(0, (height - size.height * factor) / 2) - pagePanY
page.pagePlaced = true
}
place(view, a, slots && !currentLeft ? leftSize.width : 0)
if (paired) place(other, b, currentLeft ? leftSize.width : 0)
}
arrangingPages = false
}
function collectResourceIssues(view) {
if (typeof reader.webResourceIssues !== "function") return
run(view, "resource.issues", {}, function(value) { if (value) reader.webResourceIssues(view.documentToken, value) })
}
WheelHandler {
target: null
enabled: !!pane.activeView && pane.activeView.fixedLayout
onWheel: function(event) {
pane.pagePanX -= event.angleDelta.x / 2
pane.pagePanY -= event.angleDelta.y / 2
pane.arrangePages()
}
}
onWidthChanged: Qt.callLater(synchronizeSpread)
onHeightChanged: Qt.callLater(synchronizeSpread)
function focusContent() {
if (activeView) activeView.forceActiveFocus()
}
function optionsFor(view) {
var opts = typeof reader.webOptions === "function" ? reader.webOptions(view.documentToken) : ({})
var reading = opts.reading || reader.settings.reading || ({})
var currentPath = decodeURIComponent(String(view.url).replace(/^doc:\/\/[^/]+\//, "").split("#")[0])
var spine = opts.spine || []
for (var i = 0; i < spine.length; ++i) {
if (decodeURIComponent(String(spine[i].href || "").split("#")[0]) === currentPath) { opts.fixed = spine[i].layout === "fixed"; break }
}
opts.publisherStyle = reading.use_publisher_style !== false
opts.fontSize = reading.font_size || 18
opts.lineHeight = reading.line_height || 1.6
return opts
}
function resetHeartbeat(view) {
if (view.heartbeatPending && typeof reader.finishRendererProbe === "function")
reader.finishRendererProbe(view.documentToken, view.rendererSlot, view.heartbeatPending)
view.heartbeatPending = 0
}
function trackRenderer(view, pid) {
if (!view || !view.alive || view.registeredRendererPid === pid || typeof reader.registerRenderer !== "function") return
resetHeartbeat(view)
view.registeredRendererPid = pid
reader.registerRenderer(view.documentToken, pid, view.rendererSlot)
}
function heartbeat(view) {
if (!view || !view.alive) return
// Qt can expose the initial process through the getter before delivering
// a PID notification. Reconcile once, without resetting healthy probes.
trackRenderer(view, view.renderProcessPid)
if (!view || !view.alive || view.heartbeatPending || view.renderProcessPid <= 0 ||
typeof reader.beginRendererProbe !== "function") return
var probe = reader.beginRendererProbe(view.documentToken, view.rendererSlot)
if (!probe) return
view.heartbeatPending = probe
var serial = view.documentSerial, token = view.documentToken
// The response requires the renderer main thread, including while the
// document is loading. Document JavaScript remains disabled.
view.runJavaScript(String(probe), WebEngineScript.ApplicationWorld, function(result) {
if (!view || !view.alive || view.documentSerial !== serial || view.documentToken !== token ||
view.heartbeatPending !== probe || result !== probe) return
reader.finishRendererProbe(token, view.rendererSlot, probe)
view.heartbeatPending = 0
// Polling work is queued only after the previous heartbeat returns;
// a stalled renderer cannot accumulate periodic DOM operations.
if (view === activeView) updateLocation(view)
collectResourceIssues(view)
})
}
function run(view, id, args, callback) {
if (!view || !view.alive || !view.documentReady) return
var serial = view.documentSerial
var token = view.documentToken
// Only JSON data is interpolated; all executable source is app-owned.
var data = JSON.stringify({id: id, args: args || ({})})
var script = "(function(data){return globalThis.__docviewReader ? globalThis.__docviewReader.command(data.id,data.args) : null;})(" + data + ")"
view.runJavaScript(script, WebEngineScript.ApplicationWorld, function(result) {
if (!view || !view.alive || view.documentSerial !== serial || view.documentToken !== token) return
if (callback) callback(result)
})
}
function updateLocation(view) {
run(view, "location", {}, function(value) {
if (!value || typeof value.url !== "string") return
value.zoom = (view.fixedLayout ? spreadZoom : view.zoomFactor) * 100
if (view.fixedLayout) {
value.fitWidth = spreadFitWidth
value.panX = maxPagePanX > 0 ? pagePanX / maxPagePanX : 0
value.panY = maxPagePanY > 0 ? pagePanY / maxPagePanY : 0
}
view.lastLocation = value
if (view === activeView) reader.webLocation(view.documentToken, value)
})
}
function configureNavigation(view) {
if (!view || view !== activeView || typeof reader.webNavigationTargets !== "function") return
var args = reader.webNavigationTargets(view.documentToken, view.url)
run(view, "navigation.configure", args, function() { updateLocation(view) })
}
function collectIndex(view) {
run(view, "index", {}, function(value) {
if (!value) return
if (view === activeView) reader.webIndex(view.documentToken, value)
if (view === activeView && value.truncated) reader.notify(qsTr("見出し一覧は先頭2,000件を表示しています"))
view.fixedViewport = value.fixedViewport || ({})
if (view.fixedLayout) arrangePages()
if (view === activeView) configureNavigation(view)
})
}
function fitFixed(view, widthOnly) {
var size = view.fixedViewport
if (!view.fixedLayout || !(size.width > 0) || !(size.height > 0)) return
var factor = widthOnly ? view.width / size.width : Math.min(view.width / size.width, view.height / size.height)
view.zoomFactor = Math.max(0.25, Math.min(5, factor))
}
function applyLayout(view, callback) {
if (!view || !view.documentReady) return
var opts = optionsFor(view)
var wasFixed = view.fixedLayout
view.fixedLayout = !!opts.fixed
if (wasFixed && !view.fixedLayout) view.zoomFactor = 1
run(view, "layout", opts, function(result) {
if (!result) return
view.fixedViewport = result.fixedViewport || ({})
if (view === activeView) synchronizeSpread()
else if (view.isCompanion) arrangePages()
if (callback) callback()
})
}
function restoreAfterLayout(view, saved, restoreDisplay, applied) {
if (!saved || !view || !view.alive) return
if (restoreDisplay) {
if (typeof saved.zoom === "number" && isFinite(saved.zoom) && saved.zoom >= 25 && saved.zoom <= 500) {
if (view.fixedLayout) spreadZoom = saved.zoom / 100
else view.zoomFactor = saved.zoom / 100
}
if (view.fixedLayout) {
spreadFitWidth = saved.fitWidth === true
arrangePages()
pagePanX = (saved.panX || 0) * maxPagePanX
pagePanY = (saved.panY || 0) * maxPagePanY
arrangePages()
}
}
run(view, "location.restore", saved, function(result) {
if (result && result.approximate) reader.notify(qsTr("E_LOCATOR_UNRESOLVED: 近い読書位置へ戻りました"))
if (applied) applied()
updateLocation(view)
})
}
function applyBoundary(view) {
if (!view || !view.documentReady || !view.pendingBoundary) return
var boundary = view.pendingBoundary
var request = view.pendingBoundaryRequest
view.pendingBoundary = ""
if (view !== activeView || request !== view.commandSerial) return
run(view, boundary, {}, function() {
if (view !== activeView || request !== view.commandSerial) return
reader.webApplied(view.documentToken, boundary, request)
updateLocation(view)
})
}
function command(id, args) {
var view = activeView
if (!view) return
var request = ++view.commandSerial
var operation = id === "navigate" && args.boundary ? args.boundary : id
reader.webStarted(view.documentToken, operation, request)
view.cancelResizeRestore()
view.restorePending = null // A newer user command supersedes a deferred restoration.
view.pendingBoundary = ""
if (id === "navigate") {
view.pendingBoundary = ["nav.document_start", "nav.document_end", "heading.first", "heading.last"].indexOf(args.boundary) >= 0 ? args.boundary : ""
view.pendingBoundaryRequest = request
view.permittedUrl = String(args.url)
var sameResource = canonicalUrl(view.url).split("#")[0] === canonicalUrl(args.url).split("#")[0]
// A following CFI restoration supplies the exact position. Avoid
// an identical-URL reload or fragment scroll overwriting that jump.
if (sameResource && args.preserveLocation) { view.permittedUrl = ""; return }
// Assigning an identical URL may reload the page. Keep an endpoint
// jump inside this DOM so a new load cannot consume its callback.
if (sameResource && view.documentReady && view.pendingBoundary) {
view.permittedUrl = ""
applyLayout(view, function() { applyBoundary(view) })
return
}
if (!sameResource) { view.documentReady = false; failedCompanionUrl = ""; pagePanX = 0; pagePanY = 0; closeCompanion() }
view.url = args.url
return
}
if (view.fixedLayout && id.indexOf("scroll.") === 0) {
var distance = id.indexOf("half_") >= 0 ? height / 2 : id.indexOf("page_") >= 0 ? height * 0.9 : 48
if (id === "scroll.left") pagePanX -= 48
else if (id === "scroll.right") pagePanX += 48
else if (id.indexOf("up") >= 0) pagePanY -= distance
else if (id.indexOf("down") >= 0) pagePanY += distance
arrangePages()
return
}
if (id === "location.restore") {
if (!view.documentReady) view.restorePending = args
else applyLayout(view, function() {
if (view === activeView && request === view.commandSerial)
restoreAfterLayout(view, args, true, function() {
if (view === activeView && request === view.commandSerial) reader.webApplied(view.documentToken, id, request)
})
})
return
}
if (id === "layout") { applyLayout(view); return }
if (id === "zoom.in" || id === "zoom.out" || id === "zoom.fit_width") {
if (view.fixedLayout) {
spreadFitWidth = id === "zoom.fit_width"
spreadZoom = spreadFitWidth ? 1 : Math.max(0.25, Math.min(5, spreadZoom * (id === "zoom.in" ? 1.15 : 1 / 1.15)))
arrangePages()
updateLocation(view)
return
}
var saved = view.lastLocation
if (id === "zoom.fit_width") {
if (view.fixedLayout) fitFixed(view, true)
else view.zoomFactor = 1
} else view.zoomFactor = Math.max(0.25, Math.min(5, view.zoomFactor * (id === "zoom.in" ? 1.15 : 1 / 1.15)))
Qt.callLater(function() {
if (view !== activeView || !view.alive || request !== view.commandSerial) return
if (saved) restoreAfterLayout(view, saved)
else updateLocation(view)
})
return
}
if (id === "search.cancel") {
++view.searchRevision
view.findText("")
return
}
if (id === "search" || id === "search.next" || id === "search.previous") {
if (id === "search") searchQuery = String(args.query || "").slice(0, 4096)
var serial = view.documentSerial
var revision = ++view.searchRevision
view.findText(searchQuery, id === "search.previous" ? WebEngineView.FindBackward : 0, function(found) {
if (!view || !view.alive || view !== activeView || view.documentSerial !== serial || view.searchRevision !== revision) return
if (!found) reader.notify(qsTr("一致する文字がありません"))
updateLocation(view)
})
return
}
run(view, id, args, function(result) {
if (view !== activeView || request !== view.commandSerial) return
if (typeof reader.webApplied === "function") reader.webApplied(view.documentToken, id, request)
if (id === "link.activate" && result) {
if (result.url && reader.navigationAllowed(result.url, WebEngineView.LinkClickedNavigation, view.documentToken))
pane.command("navigate", {url: result.url})
else if (result.blocked) reader.notify(qsTr("E_RESOURCE_BLOCKED: このリンクは開けません"))
else if (result.empty) reader.notify(qsTr("Tabでリンクを選択してください"))
} else if ((id === "link.next" || id === "link.previous") && result && result.empty)
reader.notify(qsTr("この文書にはリンクがありません"))
if (result && result.boundary) {
if (reader.format === "epub") {
reader.execute(result.boundary > 0 ? "nav.chapter_next" : "nav.chapter_previous",
{headingBoundary: result.boundary > 0 ? "heading.first" : "heading.last"})
} else reader.notify(result.empty ? qsTr("この文書には見出しがありません") : qsTr("見出しの端です"))
}
if (id === "location.restore" && result && result.approximate) reader.notify(qsTr("E_LOCATOR_UNRESOLVED: 近い読書位置へ戻りました"))
updateLocation(view)
})
}
Connections {
target: reader
function onStageWeb(profile, url, token) {
closeCompanion()
var view = webComponent.createObject(pane, {profile: profile, documentToken: token, permittedUrl: String(url)})
if (!view) { reader.webLoaded(token, false); return }
views[token] = view
pendingView = view
view.url = url
}
function onActivateWeb(token) {
if (activeView) { activeView.isCurrent = false; activeView.enabled = false }
activeView = views[token] || null
if (activeView) {
spreadZoom = 1; spreadFitWidth = false; pagePanX = 0; pagePanY = 0
activeView.isCurrent = true
activeView.enabled = true
if (pendingView === activeView) pendingView = null
Qt.callLater(function() { if (activeView && activeView.documentToken === token) { applyLayout(activeView); collectIndex(activeView) } })
}
}
function onDisposeWeb(token) {
var view = views[token]
if (!view) return
if (companionView && companionView.documentToken === token) closeCompanion()
if (activeView === view) activeView = null
if (pendingView === view) pendingView = null
delete views[token]
retireView(view)
if (activeView && !pendingView) Qt.callLater(synchronizeSpread)
}
function onWebCommand(id, args) { pane.command(id, args) }
function onNavigationChanged() { pane.configureNavigation(pane.activeView) }
function onSettingsChanged() {
if (!activeView) return
var view = activeView
run(view, "location", {}, function(saved) {
applyLayout(view, function() { restoreAfterLayout(view, saved) })
})
}
}
Timer {
interval: 400
running: !!pane.activeView || !!pane.pendingView || !!pane.companionView
repeat: true
onTriggered: {
pane.heartbeat(pane.activeView)
pane.heartbeat(pane.pendingView)
pane.heartbeat(pane.companionView)
}
}
Component {
id: webComponent
WebEngineView {
id: web
width: pane.width
height: pane.height
transformOrigin: Item.TopLeft
property int rendererSlot: 0
property bool isCompanion: false
property bool pagePlaced: false
property string documentToken: ""
property string permittedUrl: ""
property bool alive: true
property bool isCurrent: false
property bool documentReady: false
property int documentSerial: 0
property double heartbeatPending: 0
property double registeredRendererPid: 0
property int searchRevision: 0
property double commandSerial: 0
property double pendingBoundaryRequest: 0
property var lastLocation: null
property var resizeLocation: null
property var restorePending: null
property var fixedViewport: ({})
property bool fixedLayout: false
property string pendingBoundary: ""
opacity: isCurrent || (isCompanion && documentReady && pagePlaced) ? 1 : 0
z: isCurrent || isCompanion ? 1 : 0
enabled: isCurrent || (isCompanion && pagePlaced)
audioMuted: true
backgroundColor: "white"
webChannel: null
activeFocusOnPress: true
settings.javascriptEnabled: false
settings.javascriptCanOpenWindows: false
settings.javascriptCanAccessClipboard: false
settings.javascriptCanPaste: false
settings.localStorageEnabled: false
settings.localContentCanAccessRemoteUrls: false
settings.localContentCanAccessFileUrls: false
settings.hyperlinkAuditingEnabled: false
settings.pluginsEnabled: false
settings.fullScreenSupportEnabled: false
settings.screenCaptureEnabled: false
settings.webGLEnabled: false
settings.accelerated2dCanvasEnabled: false
settings.autoLoadIconsForPage: false
settings.touchIconsEnabled: false
settings.allowRunningInsecureContent: false
settings.allowGeolocationOnInsecureOrigins: false
settings.allowWindowActivationFromJavaScript: false
settings.dnsPrefetchEnabled: false
settings.pdfViewerEnabled: false
settings.navigateOnDropEnabled: false
settings.playbackRequiresUserGesture: true
settings.webRTCPublicInterfacesOnly: true
settings.unknownUrlSchemePolicy: WebEngineSettings.DisallowUnknownUrlSchemes
settings.focusOnNavigationEnabled: false
userScripts.collection: [{
name: "DocView Reader",
injectionPoint: WebEngineScript.DocumentCreation,
worldId: WebEngineScript.ApplicationWorld,
runsOnSubFrames: false,
sourceCode: reader.script("reader")
}]
onActiveFocusChanged: if (activeFocus) reader.context = "content"
onNavigationRequested: function(request) {
var type = request.navigationType
var known = type === WebEngineView.LinkClickedNavigation ||
((type === WebEngineView.OtherNavigation || type === WebEngineView.TypedNavigation) && pane.canonicalUrl(request.url) === pane.canonicalUrl(permittedUrl))
if (isCompanion && type === WebEngineView.LinkClickedNavigation) {
request.action = WebEngineView.IgnoreRequest
if (reader.navigationAllowed(request.url, type, documentToken)) pane.command("navigate", {url:request.url})
return
}
request.action = known && reader.navigationAllowed(request.url, type, documentToken)
? WebEngineView.AcceptRequest : WebEngineView.IgnoreRequest
if (known && type !== WebEngineView.LinkClickedNavigation) permittedUrl = ""
}
onLoadingChanged: function(info) {
if (info.status === WebEngineView.LoadStartedStatus) { pane.resetHeartbeat(web); ++documentSerial; documentReady = false }
else if (info.status === WebEngineView.LoadSucceededStatus) {
documentReady = true
if (!isCompanion) {
reader.webLoaded(documentToken, true)
if (typeof reader.webDocumentLoaded === "function") reader.webDocumentLoaded(documentToken, url)
}
pane.applyLayout(web, function() {
pane.applyBoundary(web)
if (restorePending) { var saved = restorePending; restorePending = null; pane.restoreAfterLayout(web, saved, true) }
})
pane.collectIndex(web)
pane.collectResourceIssues(web)
} else if (info.status === WebEngineView.LoadFailedStatus && alive) {
if (isCompanion) { pane.failedCompanionUrl = String(url); pane.closeCompanion(); pane.arrangePages() }
else reader.webLoaded(documentToken, false)
}
}
onNewWindowRequested: function(request) {
if (request.userInitiated && reader.navigationAllowed(request.requestedUrl, WebEngineView.LinkClickedNavigation, documentToken)) {
pane.command("navigate", {url:request.requestedUrl})
}
}
onJavaScriptConsoleMessage: function(level, message, lineNumber, sourceId) { /* Document text is not written to application logs. */ }
onPermissionRequested: function(permissionRequest) { permissionRequest.deny() }
onFeaturePermissionRequested: function(origin, feature) { grantFeaturePermission(origin, feature, false) }
onFileDialogRequested: function(request) { request.accepted = true; request.dialogReject() }
onColorDialogRequested: function(request) { request.accepted = true; request.dialogReject() }
onAuthenticationDialogRequested: function(request) { request.accepted = true; request.dialogReject() }
onJavaScriptDialogRequested: function(request) { request.accepted = true; request.dialogReject() }
onContextMenuRequested: function(request) { request.accepted = true }
onFullScreenRequested: function(request) { request.reject() }
onCertificateError: function(error) { error.rejectCertificate() }
onQuotaRequested: function(request) { request.reject() }
onRegisterProtocolHandlerRequested: function(request) { request.reject() }
onFileSystemAccessRequested: function(request) { request.reject() }
onSelectClientCertificate: function(selection) { selection.selectNone() }
onDesktopMediaRequested: function(request) { request.cancel() }
onWebAuthUxRequested: function(request) { request.cancel() }
onRenderProcessTerminated: function(status, code) {
if (!alive) return
documentReady = false
if (isCurrent || isCompanion) reader.renderStopped()
else reader.webLoaded(documentToken, false)
}
onRenderProcessPidChanged: function(pid) {
pane.trackRenderer(web, pid)
}
Timer {
id: resizeDelay
interval: 150
onTriggered: {
if (web.fixedLayout) pane.arrangePages()
else pane.restoreAfterLayout(web, web.resizeLocation)
web.resizeLocation = null
}
}
function preserveResizeLocation() {
if (!documentReady || pane.arrangingPages) return
if (!resizeDelay.running) resizeLocation = lastLocation
resizeDelay.restart()
}
function cancelResizeRestore() { resizeDelay.stop(); resizeLocation = null }
onWidthChanged: preserveResizeLocation()
onHeightChanged: preserveResizeLocation()
}
}
}
+1
View File
@@ -0,0 +1 @@
<RCC><qresource prefix="/"><file>qml/Main.qml</file><file>qml/WebPane.qml</file><file alias="scripts/reader.js">resources/reader.js</file></qresource></RCC>
+38
View File
@@ -0,0 +1,38 @@
schema_version = 1
[ui]
theme = "dark"
status_bar = true
toc_visible = false
pages_visible = false
font_size = 14
panel_width = 280
[pdf]
zoom_mode = "fit-width"
zoom_percent = 100
preserve_colors = true
[reading]
use_publisher_style = true
font_size = 18
line_height = 1.6
[keys]
sequence_timeout_ms = 800
[performance]
tile_cache_mib = 256
prefetch_pages = 1
[privacy]
remember_position = true
recent_documents = 20
store_text_anchor = false
# Example: replace a binding. Remove the leading # characters to enable it.
# [[keybindings]]
# mode = "normal"
# context = "content"
# keys = ["j"]
# command = "scroll.down"
File diff suppressed because it is too large Load Diff
+73
View File
@@ -0,0 +1,73 @@
# Third-party material in the Linux development package
DocView's public license has not been selected. These notices do not grant a
license to DocView, declare a license choice for a dependency, or authorize a
public release.
`cmake --install` includes the independently distributed PDFium binary's
`LICENSE` and `licenses/` directory, plus the locally verified qpdf license text.
The Linux install also includes `pdfium-supplemental/`'s full libc++ and
libc++abi notices. The provider's collection script omits these names, while
the fixed build settings and inspected library identify their use. The
supplement's `sources.json` binds each text to its exact upstream revision and
hash, and to the inspected PDFium library hash. Configure and packaging reject
a changed PDFium version, source pin, binary or notice pending a new review.
This adds two texts to the provider's 15; it does not prove all component
attribution is complete.
`tools/package_linux_development.py` additionally creates
`share/doc/docview/third-party/NOTICE.txt`, `dependency-manifest.json`, and a
`licenses/` tree from the actual Arch Linux packages on the packaging host.
Every copied text has a source path or resource attribution and SHA-256 in the manifest. Package license
labels are recorded verbatim; a list of alternatives is not a selected license.
For the current e2fsprogs 1.47.4, libidn2 2.3.8 and opencore-amr 0.1.6 packages,
the installed package has no specific license-text directory. The
`linux-supplemental/` tree supplies top-level notice/license texts from the
versioned upstream sources. `sources.json` records the URLs, downloaded-object
hashes and each text's hash. Packaging rejects a changed upstream version or
modified text until this supplement is reviewed. Downloaded source archives
were used only to obtain these texts and are not included as corresponding
source. This does not establish complete component-specific attribution.
The package contains DocView's three executables and the independent PDFium
shared library. Qt, Qt Quick, Qt WebEngine, libqpdf, libzip, toml++, libseccomp,
Fontconfig and their resolved system libraries remain system dependencies.
Compiled-in toml++ code is also recorded. Copying a system dependency's notice
does not mean that its runtime binary is bundled.
The manifest separates pinned PDFium source/binary provenance from Arch package
version, upstream home and packaging-repository pointers. Corresponding source
trees and complete build materials are **not included in this package**.
Separate repository evidence records selected exact Arch recipes/patches,
PDFium provider metadata and fixed-source notice comparisons. That bounded
collection is not a complete corresponding-source archive. A source URL or an
installed license text does not complete source-delivery requirements.
Each component therefore retains `source.status = not-collected` for the
package's complete-source collection.
Qt WebEngine includes Chromium components whose terms are separate from the Qt
module's terms. The installed Arch `LICENSE.chromium` is copied, but it is a
top-level license, not the complete build-specific Chromium attribution set.
The Linux packager also copies the seven complete notice comments in
`qt-embedded-notices/`, extracted from 13 inspected QtWebEngine DataPack entries.
The reviewed `sources.json` binds the texts and exact resource spans to Qt
6.11.2, Arch qt6-webengine 6.11.2-1, the two DataPack paths/hashes and the
extraction evidence. Packaging checks the fixed metadata digest, Qt/package
versions, actual system-file inventory, and every notice's size/hash. A changed
variant or missing/modified text fails packaging pending a new review.
Only the texts and source metadata are copied; Qt's runtime/resources remain
system dependencies. These seven texts do not complete Chromium attribution
or corresponding sources. That collection and review remain open. See the
[Qt WebEngine licensing documentation](https://doc.qt.io/qt-6/qtwebengine-licensing.html).
The bounded inventory includes the executables' ELF closure, Qt WebEngine helper
and resources, selected QML modules, and candidate styles/platform/image plugins.
It records system files without copying them into the runtime. It is not a
dynamic-load trace or an inventory of every optional IME, GPU, theme or system
font provider. No system font files are bundled.
The generated package is for local development on the recorded Arch Linux ABI.
It does not satisfy the clean Ubuntu/Windows distribution gate. Before public
distribution, settle the application license and target packaging policy,
resolve all actual bundled-component conditions, complete required notices and
source arrangements, and test the resulting target-OS package.
@@ -0,0 +1,849 @@
This package, the EXT2 filesystem utilities, are made available under
the GNU Public License version 2, with the exception of the lib/ext2fs
and lib/e2p libraries, which are made available under the GNU Library
General Public License Version 2, the lib/uuid library which is made
available under a BSD-style license and the lib/et and lib/ss
libraries which are made available under an MIT-style license. Please
see lib/uuid/COPYING for more details for the license for the files
comprising the libuuid library, and the source file headers of the
libet and libss libraries for more information.
The most recent officially distributed version can be found at
http://e2fsprogs.sourceforge.net. If you need to make a distribution,
that's the one you should use. If there is some reason why you'd like
a more recent version that is still in ALPHA testing (i.e., either
using the "WIP" test distributions or one from the hg or git
repository from the development branch, please contact me
([email protected]) before you ship. The release schedules for this
package are flexible, if you give me enough lead time.
Theodore Ts'o
23-June-2007
----------------------------------------------------------------------
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Library General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Library General
Public License instead of this License.
----------------------------------------------------------------------
GNU LIBRARY GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1991 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the library GPL. It is
numbered 2 because it goes with version 2 of the ordinary GPL.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Library General Public License, applies to some
specially designated Free Software Foundation software, and to any
other libraries whose authors decide to use it. You can use it for
your libraries, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if
you distribute copies of the library, or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link a program with the library, you must provide
complete object files to the recipients so that they can relink them
with the library, after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
Our method of protecting your rights has two steps: (1) copyright
the library, and (2) offer you this license which gives you legal
permission to copy, distribute and/or modify the library.
Also, for each distributor's protection, we want to make certain
that everyone understands that there is no warranty for this free
library. If the library is modified by someone else and passed on, we
want its recipients to know that what they have is not the original
version, so that any problems introduced by others will not reflect on
the original authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that companies distributing free
software will individually obtain patent licenses, thus in effect
transforming the program into proprietary software. To prevent this,
we have made it clear that any patent must be licensed for everyone's
free use or not licensed at all.
Most GNU software, including some libraries, is covered by the ordinary
GNU General Public License, which was designed for utility programs. This
license, the GNU Library General Public License, applies to certain
designated libraries. This license is quite different from the ordinary
one; be sure to read it in full, and don't assume that anything in it is
the same as in the ordinary license.
The reason we have a separate public license for some libraries is that
they blur the distinction we usually make between modifying or adding to a
program and simply using it. Linking a program with a library, without
changing the library, is in some sense simply using the library, and is
analogous to running a utility program or application program. However, in
a textual and legal sense, the linked executable is a combined work, a
derivative of the original library, and the ordinary General Public License
treats it as such.
Because of this blurred distinction, using the ordinary General
Public License for libraries did not effectively promote software
sharing, because most developers did not use the libraries. We
concluded that weaker conditions might promote sharing better.
However, unrestricted linking of non-free programs would deprive the
users of those programs of all benefit from the free status of the
libraries themselves. This Library General Public License is intended to
permit developers of non-free programs to use free libraries, while
preserving your freedom as a user of such programs to change the free
libraries that are incorporated in them. (We have not seen how to achieve
this as regards changes in header files, but we have achieved it as regards
changes in the actual functions of the Library.) The hope is that this
will lead to faster development of free libraries.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, while the latter only
works together with the library.
Note that it is possible for a library to be covered by the ordinary
General Public License rather than by this special one.
GNU LIBRARY GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library which
contains a notice placed by the copyright holder or other authorized
party saying it may be distributed under the terms of this Library
General Public License (also called "this License"). Each licensee is
addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also compile or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
c) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
d) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the source code distributed need not include anything that is normally
distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Library General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change. You can do so by permitting
redistribution under these terms (or, alternatively, under the terms of the
ordinary General Public License).
To apply these terms, attach the following notices to the library. It is
safest to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the library's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the library, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
<signature of Ty Coon>, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
@@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
@@ -0,0 +1,91 @@
A. Unicode Copyright.
Copyright © 1991-2016 Unicode, Inc. All rights reserved.
Certain documents and files on this website contain a legend indicating that "Modification is permitted." Any person is hereby authorized, without fee, to modify such documents and files to create derivative works conforming to the Unicode® Standard, subject to Terms and Conditions herein.
Any person is hereby authorized, without fee, to view, use, reproduce, and distribute all documents and files solely for informational purposes and in the creation of products supporting the Unicode Standard, subject to the Terms and Conditions herein.
Further specifications of rights and restrictions pertaining to the use of the particular set of data files known as the "Unicode Character Database" can be found in the License.
Each version of the Unicode Standard has further specifications of rights and restrictions of use. For the book editions (Unicode 5.0 and earlier), these are found on the back of the title page. The online code charts carry specific restrictions. All other files, including online documentation of the core specification for Unicode 6.0 and later, are covered under these general Terms of Use.
No license is granted to "mirror" the Unicode website where a fee is charged for access to the "mirror" site.
Modification is not permitted with respect to this document. All copies of this document must be verbatim.
B. Restricted Rights Legend.
Any technical data or software which is licensed to the United States of America, its agencies and/or instrumentalities under this Agreement is commercial technical data or commercial computer software developed exclusively at private expense as defined in FAR 2.101, or DFARS 252.227-7014 (June 1995), as applicable. For technical data, use, duplication, or disclosure by the Government is subject to restrictions as set forth in DFARS 202.227-7015 Technical Data, Commercial and Items (Nov 1995) and this Agreement. For Software, in accordance with FAR 12-212 or DFARS 227-7202, as applicable, use, duplication or disclosure by the Government is subject to the restrictions set forth in this Agreement.
C. Warranties and Disclaimers.
This publication and/or website may include technical or typographical errors or other inaccuracies . Changes are periodically added to the information herein; these changes will be incorporated in new editions of the publication and/or website. Unicode may make improvements and/or changes in the product(s) and/or program(s) described in this publication and/or website at any time.
If this file has been purchased on magnetic or optical media from Unicode, Inc. the sole and exclusive remedy for any claim will be exchange of the defective media within ninety (90) days of original purchase.
EXCEPT AS PROVIDED IN SECTION C.2, THIS PUBLICATION AND/OR SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND EITHER EXPRESS, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. UNICODE AND ITS LICENSORS ASSUME NO RESPONSIBILITY FOR ERRORS OR OMISSIONS IN THIS PUBLICATION AND/OR SOFTWARE OR OTHER DOCUMENTS WHICH ARE REFERENCED BY OR LINKED TO THIS PUBLICATION OR THE UNICODE WEBSITE.
D. Waiver of Damages.
In no event shall Unicode or its licensors be liable for any special, incidental, indirect or consequential damages of any kind, or any damages whatsoever, whether or not Unicode was advised of the possibility of the damage, including, without limitation, those resulting from the following: loss of use, data or profits, in connection with the use, modification or distribution of this information or its derivatives.
E. Trademarks & Logos.
The Unicode Word Mark and the Unicode Logo are trademarks of Unicode, Inc. “The Unicode Consortium” and “Unicode, Inc.” are trade names of Unicode, Inc. Use of the information and materials found on this website indicates your acknowledgement of Unicode, Inc.’s exclusive worldwide rights in the Unicode Word Mark, the Unicode Logo, and the Unicode trade names.
The Unicode Consortium Name and Trademark Usage Policy (“Trademark Policy”) are incorporated herein by reference and you agree to abide by the provisions of the Trademark Policy, which may be changed from time to time in the sole discretion of Unicode, Inc.
All third party trademarks referenced herein are the property of their respective owners.
F. Miscellaneous.
Jurisdiction and Venue. This server is operated from a location in the State of California, United States of America. Unicode makes no representation that the materials are appropriate for use in other locations. If you access this server from other locations, you are responsible for compliance with local laws. This Agreement, all use of this site and any claims and damages resulting from use of this site are governed solely by the laws of the State of California without regard to any principles which would apply the laws of a different jurisdiction. The user agrees that any disputes regarding this site shall be resolved solely in the courts located in Santa Clara County, California. The user agrees said courts have personal jurisdiction and agree to waive any right to transfer the dispute to any other forum.
Modification by Unicode Unicode shall have the right to modify this Agreement at any time by posting it to this site. The user may not assign any part of this Agreement without Unicode’s prior written consent.
Taxes. The user agrees to pay any taxes arising from access to this website or use of the information herein, except for those based on Unicode’s net income.
Severability. If any provision of this Agreement is declared invalid or unenforceable, the remaining provisions of this Agreement shall remain in effect.
Entire Agreement. This Agreement constitutes the entire agreement between the parties.
EXHIBIT 1
Unicode Data Files include all data files under the directories
http://www.unicode.org/Public/, http://www.unicode.org/reports/,
http://www.unicode.org/cldr/data/, http://source.icu-project.org/repos/icu/, and
http://www.unicode.org/utility/trac/browser/.
Unicode Data Files do not include PDF online code charts under the
directory http://www.unicode.org/Public/.
Software includes any source code published in the Unicode Standard
or under the directories
http://www.unicode.org/Public/, http://www.unicode.org/reports/,
http://www.unicode.org/cldr/data/, http://source.icu-project.org/repos/icu/, and
http://www.unicode.org/utility/trac/browser/.
NOTICE TO USER: Carefully read the following legal agreement.
BY DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING UNICODE INC.'S
DATA FILES ("DATA FILES"), AND/OR SOFTWARE ("SOFTWARE"),
YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE
TERMS AND CONDITIONS OF THIS AGREEMENT.
IF YOU DO NOT AGREE, DO NOT DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE
THE DATA FILES OR SOFTWARE.
COPYRIGHT AND PERMISSION NOTICE
Copyright © 1991-2016 Unicode, Inc. All rights reserved.
Distributed under the Terms of Use in http://www.unicode.org/copyright.html.
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Unicode data files and any associated documentation
(the "Data Files") or Unicode software and any associated documentation
(the "Software") to deal in the Data Files or Software
without restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, and/or sell copies of
the Data Files or Software, and to permit persons to whom the Data Files
or Software are furnished to do so, provided that either
(a) this copyright and permission notice appear with all copies
of the Data Files or Software, or
(b) this copyright and permission notice appear in associated
Documentation.
THE DATA FILES AND SOFTWARE ARE 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 OF THIRD PARTY RIGHTS.
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS
NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL
DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THE DATA FILES OR SOFTWARE.
Except as contained in this notice, the name of a copyright holder
shall not be used in advertising or otherwise to promote the sale,
use or other dealings in these Data Files or Software without prior
written authorization of the copyright holder.
@@ -0,0 +1,338 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
<https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Moe Ghoul>, 1 April 1989
Moe Ghoul, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
@@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the
copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other
entities that control, are controlled by, or are under common control with
that entity. For the purposes of this definition, "control" means (i) the
power, direct or indirect, to cause the direction or management of such
entity, whether by contract or otherwise, or (ii) ownership of fifty
percent (50%) or more of the outstanding shares, or (iii) beneficial
ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation source,
and configuration files.
"Object" form shall mean any form resulting from mechanical transformation
or translation of a Source form, including but not limited to compiled
object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form,
made available under the License, as indicated by a copyright notice that
is included in or attached to the work (an example is provided in the
Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form,
that is based on (or derived from) the Work and for which the editorial
revisions, annotations, elaborations, or other modifications represent, as
a whole, an original work of authorship. For the purposes of this License,
Derivative Works shall not include works that remain separable from, or
merely link (or bind by name) to the interfaces of, the Work and Derivative
Works thereof.
"Contribution" shall mean any work of authorship, including the original
version of the Work and any modifications or additions to that Work or
Derivative Works thereof, that is intentionally submitted to Licensor for
inclusion in the Work by the copyright owner or by an individual or Legal
Entity authorized to submit on behalf of the copyright owner. For the
purposes of this definition, "submitted" means any form of electronic,
verbal, or written communication sent to the Licensor or its
representatives, including but not limited to communication on electronic
mailing lists, source code control systems, and issue tracking systems that
are managed by, or on behalf of, the Licensor for the purpose of discussing
and improving the Work, but excluding communication that is conspicuously
marked or otherwise designated in writing by the copyright owner as "Not a
Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on
behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this
License, each Contributor hereby grants to You a perpetual, worldwide,
non-exclusive, no-charge, royalty-free, irrevocable copyright license to
reproduce, prepare Derivative Works of, publicly display, publicly perform,
sublicense, and distribute the Work and such Derivative Works in Source or
Object form.
3. Grant of Patent License. Subject to the terms and conditions of this
License, each Contributor hereby grants to You a perpetual, worldwide,
non-exclusive, no-charge, royalty-free, irrevocable (except as stated in
this section) patent license to make, have made, use, offer to sell, sell,
import, and otherwise transfer the Work, where such license applies only to
those patent claims licensable by such Contributor that are necessarily
infringed by their Contribution(s) alone or by combination of their
Contribution(s) with the Work to which such Contribution(s) was submitted.
If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or
contributory patent infringement, then any patent licenses granted to You
under this License for that Work shall terminate as of the date such
litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or
Derivative Works thereof in any medium, with or without modifications, and
in Source or Object form, provided that You meet the following conditions:
1. You must give any other recipients of the Work or Derivative Works a
copy of this License; and
2. You must cause any modified files to carry prominent notices stating
that You changed the files; and
3. You must retain, in the Source form of any Derivative Works that You
distribute, all copyright, patent, trademark, and attribution notices from
the Source form of the Work, excluding those notices that do not pertain to
any part of the Derivative Works; and
4. If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must include a
readable copy of the attribution notices contained within such NOTICE file,
excluding those notices that do not pertain to any part of the Derivative
Works, in at least one of the following places: within a NOTICE text file
distributed as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or, within a
display generated by the Derivative Works, if and wherever such third-party
notices normally appear. The contents of the NOTICE file are for
informational purposes only and do not modify the License. You may add Your
own attribution notices within Derivative Works that You distribute,
alongside or as an addendum to the NOTICE text from the Work, provided that
such additional attribution notices cannot be construed as modifying the
License.
You may add Your own copyright statement to Your modifications and may
provide additional or different license terms and conditions for use,
reproduction, or distribution of Your modifications, or for any such
Derivative Works as a whole, provided Your use, reproduction, and
distribution of the Work otherwise complies with the conditions stated in
this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any
Contribution intentionally submitted for inclusion in the Work by You to
the Licensor shall be under the terms and conditions of this License,
without any additional terms or conditions. Notwithstanding the above,
nothing herein shall supersede or modify the terms of any separate license
agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor, except
as required for reasonable and customary use in describing the origin of
the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to
in writing, Licensor provides the Work (and each Contributor provides its
Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied, including, without limitation, any
warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or
FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for
determining the appropriateness of using or redistributing the Work and
assume any risks associated with Your exercise of permissions under this
License.
8. Limitation of Liability. In no event and under no legal theory, whether
in tort (including negligence), contract, or otherwise, unless required by
applicable law (such as deliberate and grossly negligent acts) or agreed to
in writing, shall any Contributor be liable to You for damages, including
any direct, indirect, special, incidental, or consequential damages of any
character arising as a result of this License or out of the use or
inability to use the Work (including but not limited to damages for loss of
goodwill, work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor has been
advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the
Work or Derivative Works thereof, You may choose to offer, and charge a fee
for, acceptance of support, warranty, indemnity, or other liability
obligations and/or rights consistent with this License. However, in
accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if
You agree to indemnify, defend, and hold each Contributor harmless for any
liability incurred by, or claims asserted against, such Contributor by
reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "[]" replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included
on the same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain a
copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable
law or agreed to in writing, software distributed under the License is
distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.
@@ -0,0 +1,32 @@
[
{
"package": "e2fsprogs",
"version": "1.47.4",
"url": "https://raw.githubusercontent.com/tytso/e2fsprogs/v1.47.4/NOTICE",
"downloadSha256": "5da5ef153e559c1d990d4c3eedbedd4442db892d37eae1f35fff069de8ec9020",
"files": {
"NOTICE": "5da5ef153e559c1d990d4c3eedbedd4442db892d37eae1f35fff069de8ec9020"
}
},
{
"package": "libidn2",
"version": "2.3.8",
"url": "https://ftp.gnu.org/gnu/libidn/libidn2-2.3.8.tar.gz",
"downloadSha256": "f557911bf6171621e1f72ff35f5b1825bb35b52ed45325dcdee931e5d3c0787a",
"files": {
"COPYING": "3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986",
"COPYING.LESSERv3": "e3a994d82e644b03a792a930f574002658412f62407f5fee083f2555c5f23118",
"COPYING.unicode": "01d621eef165cf4d3d3dbb737aa0699178d94c6f18cf87e9dde6db3ca7790f46",
"COPYINGv2": "edaef632cbb643e4e7a221717a6c441a4c1a7c918e6e4d56debc3d8739b233f6"
}
},
{
"package": "opencore-amr",
"version": "0.1.6",
"url": "https://downloads.sourceforge.net/project/opencore-amr/opencore-amr/opencore-amr-0.1.6.tar.gz",
"downloadSha256": "483eb4061088e2b34b358e47540b5d495a96cd468e361050fae615b1809dc4a1",
"files": {
"LICENSE": "8b3f1762349248d444ab9acbafe73941254e36e1064954da56bb9ddbd5873ddb"
}
}
]
@@ -0,0 +1,311 @@
==============================================================================
The LLVM Project is under the Apache License v2.0 with LLVM Exceptions:
==============================================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
---- LLVM Exceptions to the Apache 2.0 License ----
As an exception, if, as a result of your compiling your source code, portions
of this Software are embedded into an Object form of such source code, you
may redistribute such embedded portions in such Object form without complying
with the conditions of Sections 4(a), 4(b) and 4(d) of the License.
In addition, if you combine or link compiled forms of this Software with
software that is licensed under the GPLv2 ("Combined Software") and if a
court of competent jurisdiction determines that the patent provision (Section
3), the indemnity provision (Section 9) or other Section of the License
conflicts with the conditions of the GPLv2, you may retroactively and
prospectively choose to deem waived or otherwise exclude such Section(s) of
the License, but only in their entirety and only with respect to the Combined
Software.
==============================================================================
Software from third parties included in the LLVM Project:
==============================================================================
The LLVM Project contains third party software which is under different license
terms. All such code will be identified clearly using at least one of two
mechanisms:
1) It will be in a separate directory tree with its own `LICENSE.txt` or
`LICENSE` file at the top containing the specific license and restrictions
which apply to that software, or
2) It will contain specific license and restriction terms at the top of every
file.
==============================================================================
Legacy LLVM License (https://llvm.org/docs/DeveloperPolicy.html#legacy):
==============================================================================
The libc++ library is dual licensed under both the University of Illinois
"BSD-Like" license and the MIT license. As a user of this code you may choose
to use it under either license. As a contributor, you agree to allow your code
to be used under both.
Full text of the relevant licenses is included below.
==============================================================================
University of Illinois/NCSA
Open Source License
Copyright (c) 2009-2019 by the contributors listed in CREDITS.TXT
All rights reserved.
Developed by:
LLVM Team
University of Illinois at Urbana-Champaign
http://llvm.org
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal with
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:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimers.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimers in the
documentation and/or other materials provided with the distribution.
* Neither the names of the LLVM Team, University of Illinois at
Urbana-Champaign, nor the names of its contributors may be used to
endorse or promote products derived from this Software without specific
prior written permission.
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
CONTRIBUTORS 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 WITH THE
SOFTWARE.
==============================================================================
Copyright (c) 2009-2014 by the contributors listed in CREDITS.TXT
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.
@@ -0,0 +1,311 @@
==============================================================================
The LLVM Project is under the Apache License v2.0 with LLVM Exceptions:
==============================================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
---- LLVM Exceptions to the Apache 2.0 License ----
As an exception, if, as a result of your compiling your source code, portions
of this Software are embedded into an Object form of such source code, you
may redistribute such embedded portions in such Object form without complying
with the conditions of Sections 4(a), 4(b) and 4(d) of the License.
In addition, if you combine or link compiled forms of this Software with
software that is licensed under the GPLv2 ("Combined Software") and if a
court of competent jurisdiction determines that the patent provision (Section
3), the indemnity provision (Section 9) or other Section of the License
conflicts with the conditions of the GPLv2, you may retroactively and
prospectively choose to deem waived or otherwise exclude such Section(s) of
the License, but only in their entirety and only with respect to the Combined
Software.
==============================================================================
Software from third parties included in the LLVM Project:
==============================================================================
The LLVM Project contains third party software which is under different license
terms. All such code will be identified clearly using at least one of two
mechanisms:
1) It will be in a separate directory tree with its own `LICENSE.txt` or
`LICENSE` file at the top containing the specific license and restrictions
which apply to that software, or
2) It will contain specific license and restriction terms at the top of every
file.
==============================================================================
Legacy LLVM License (https://llvm.org/docs/DeveloperPolicy.html#legacy):
==============================================================================
The libc++abi library is dual licensed under both the University of Illinois
"BSD-Like" license and the MIT license. As a user of this code you may choose
to use it under either license. As a contributor, you agree to allow your code
to be used under both.
Full text of the relevant licenses is included below.
==============================================================================
University of Illinois/NCSA
Open Source License
Copyright (c) 2009-2019 by the contributors listed in CREDITS.TXT
All rights reserved.
Developed by:
LLVM Team
University of Illinois at Urbana-Champaign
http://llvm.org
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal with
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:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimers.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimers in the
documentation and/or other materials provided with the distribution.
* Neither the names of the LLVM Team, University of Illinois at
Urbana-Champaign, nor the names of its contributors may be used to
endorse or promote products derived from this Software without specific
prior written permission.
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
CONTRIBUTORS 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 WITH THE
SOFTWARE.
==============================================================================
Copyright (c) 2009-2014 by the contributors listed in CREDITS.TXT
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.
@@ -0,0 +1,27 @@
{
"schemaVersion": 1,
"scope": "Additional fixed-source notices for the inspected Linux x64 PDFium binary; not a complete dependency or legal-compliance assessment",
"pdfiumVersion": "155.0.8057.0",
"pdfiumUpstreamCommit": "a5a7089234f121990b336b3841008009dca143bf",
"pdfiumLibrarySha256": "08f6ea53a64f6fbb9f5ba8d9c02e0051987c6a9175f3fb5ba25f219174731aaa",
"providerRecipeCommit": "f2e9a1c45bb17b85b540abf1af30146ef65416ac",
"reason": "Provider notice collection ignores libc; the pinned build selects custom libc++ and the inspected binary contains libc++/libc++abi evidence. Retain both full source license files.",
"files": [
{
"name": "libcxx-LICENSE.txt",
"component": "libc++",
"sourceRevision": "97b436da4c33663581d394f4ee0a5977fc38c2f4",
"sourceUrl": "https://chromium.googlesource.com/external/github.com/llvm/llvm-project/libcxx.git/+/97b436da4c33663581d394f4ee0a5977fc38c2f4/LICENSE.TXT?format=TEXT",
"sha256": "539dd7aed86e8a4f12cbdd0e6c50c189c7d74847e4fecc64ce2c6ee3a01da38b",
"size": 16703
},
{
"name": "libcxxabi-LICENSE.txt",
"component": "libc++abi",
"sourceRevision": "fc1897a2c12aa27e703c3ed48b62eba8abf4ce19",
"sourceUrl": "https://chromium.googlesource.com/external/github.com/llvm/llvm-project/libcxxabi.git/+/fc1897a2c12aa27e703c3ed48b62eba8abf4ce19/LICENSE.TXT?format=TEXT",
"sha256": "e2b35be49f7284a45b7baca8fc7b3ab7440e7902392b2528a457816b5bb2a15c",
"size": 16706
}
]
}
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+12
View File
@@ -0,0 +1,12 @@
qpdf 12.4.1
Upstream: https://github.com/qpdf/qpdf/tree/v12.4.1
Project licensing information: https://qpdf.readthedocs.io/en/stable/license.html
LICENSE.txt is an unmodified copy from the qpdf 12.4.1-1 package used for
the local Linux build (/usr/share/licenses/qpdf/LICENSE.txt).
DocView uses the public libqpdf API inside PDFWorker to read annotation
border dictionaries. It does not modify or embed qpdf source code.
The local Linux install uses the system libqpdf shared library. A Windows
distribution must include the notices and licenses for the actual qpdf
binary and its linked dependencies as part of the release packaging review.
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2010 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2009 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
@@ -0,0 +1,24 @@
/*
* Copyright (C) 2012 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
* DAMAGE.
*/
@@ -0,0 +1,23 @@
/*
* Copyright (C) 2006 Apple Computer, Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2012 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
@@ -0,0 +1,26 @@
/*
* The default style sheet used to render SVG.
*
* Copyright (C) 2005, 2006 Apple Computer, Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
@@ -0,0 +1,26 @@
/*
* The default style sheet used to render MathML.
*
* Copyright (C) 2014 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
@@ -0,0 +1,198 @@
{
"comparisonScope": "These seven complete legal texts have no byte-identical, normalized-identical, or normalized-contained match among the 523 system and 17 PDFium notices in the previously inspected package; similar terms and legal sufficiency were not assessed.",
"completeChromiumNotices": false,
"dataPacks": [
{
"package": "qt6-webengine",
"packageVersion": "6.11.2-1",
"path": "/usr/share/qt6/resources/qtwebengine_resources.pak",
"sha256": "e51fbb20ff31cd4407ab34338f9f9f1afc7eadd270ce6e3cf9901f3622647b96",
"size": 2452411
},
{
"package": "qt6-webengine",
"packageVersion": "6.11.2-1",
"path": "/usr/share/qt6/resources/qtwebengine_devtools_resources.pak",
"sha256": "a27b04ae3810f73c6f9a5aea02bb0b35fac626b4b94572109e423bab797b1bc0",
"size": 11698477
}
],
"extractionEvidence": {
"path": "tests/results/source-correspondence/qt-embedded-notices/first/report.json",
"sha256": "0c14b488c230465958dfcb34bfcc46ce5a4c1fc20f37fe2e585859c1100e8792"
},
"files": [
{
"name": "20a973990b2f574890615dcda764a32f1087dfb0c93eeab041f8edbb1ee461c0.txt",
"sha256": "20a973990b2f574890615dcda764a32f1087dfb0c93eeab041f8edbb1ee461c0",
"size": 1562,
"sourceResources": [
{
"compression": "brotli",
"dataPackPath": "/usr/share/qt6/resources/qtwebengine_devtools_resources.pak",
"endByteExclusive": 1562,
"entrySha256": "f558156d8ac09451f5666e3ffafa8e2dad3707f9a85b0b4c76c521aedb4edd4a",
"entrySize": 61820,
"resourceId": 50485,
"startByte": 0
}
]
},
{
"name": "23ed0ee0cf026ef6601ba5c92c1189f225dad76068d3cc1902ed43391bb07d44.txt",
"sha256": "23ed0ee0cf026ef6601ba5c92c1189f225dad76068d3cc1902ed43391bb07d44",
"size": 1562,
"sourceResources": [
{
"compression": "brotli",
"dataPackPath": "/usr/share/qt6/resources/qtwebengine_resources.pak",
"endByteExclusive": 1562,
"entrySha256": "b5b4aecf18d7f38c6a2167460c76af23a6d0e71d778127ae6709e610237aee3b",
"entrySize": 3136,
"resourceId": 44825,
"startByte": 0
}
]
},
{
"name": "3050e356873d44a733913871ab7f15267af5898d58f2d6ab272371adb4c95282.txt",
"sha256": "3050e356873d44a733913871ab7f15267af5898d58f2d6ab272371adb4c95282",
"size": 1355,
"sourceResources": [
{
"compression": "brotli",
"dataPackPath": "/usr/share/qt6/resources/qtwebengine_resources.pak",
"endByteExclusive": 1369,
"entrySha256": "2c90178ecfefc9fa6421a0038b9d41477eec3cecbf09ff29ca2cf211ec9d97b8",
"entrySize": 12904,
"resourceId": 44844,
"startByte": 14
},
{
"compression": "brotli",
"dataPackPath": "/usr/share/qt6/resources/qtwebengine_resources.pak",
"endByteExclusive": 1355,
"entrySha256": "e3eb8a1d4f20b4409996047cda8d59943bf4d42b4dec4262556e887b21253e74",
"entrySize": 1518,
"resourceId": 44845,
"startByte": 0
},
{
"compression": "brotli",
"dataPackPath": "/usr/share/qt6/resources/qtwebengine_resources.pak",
"endByteExclusive": 1369,
"entrySha256": "87a5e63aad7f15d1ca9ab0f26f82b4e260f635edbb5429a2cf29f7ab0e3278c3",
"entrySize": 13506,
"resourceId": 44853,
"startByte": 14
},
{
"compression": "brotli",
"dataPackPath": "/usr/share/qt6/resources/qtwebengine_resources.pak",
"endByteExclusive": 1355,
"entrySha256": "9c421afcc6128a04e75c27e38cef7479deb1846bff47487d7c1fe171a282003c",
"entrySize": 4100,
"resourceId": 44855,
"startByte": 0
},
{
"compression": "brotli",
"dataPackPath": "/usr/share/qt6/resources/qtwebengine_resources.pak",
"endByteExclusive": 1369,
"entrySha256": "b9b6201609c8c37c624858108bad546e5def00898f3c5cb4c2f380df5c2eaf9b",
"entrySize": 7877,
"resourceId": 44856,
"startByte": 14
}
]
},
{
"name": "4cfb67cb718c860a0fbfb311917d358380cdae89647a03b5d1553635a2b5eaed.txt",
"sha256": "4cfb67cb718c860a0fbfb311917d358380cdae89647a03b5d1553635a2b5eaed",
"size": 1291,
"sourceResources": [
{
"compression": "brotli",
"dataPackPath": "/usr/share/qt6/resources/qtwebengine_resources.pak",
"endByteExclusive": 1291,
"entrySha256": "f53caf157c449529b882b3ffe1fe218b8f7f39974735a8876445c92a8a61a21e",
"entrySize": 4437,
"resourceId": 44822,
"startByte": 0
}
]
},
{
"name": "7faa9d1d99e7c8104fb2c1241051905186376aa0d5275d812c9cd24861cc458c.txt",
"sha256": "7faa9d1d99e7c8104fb2c1241051905186376aa0d5275d812c9cd24861cc458c",
"size": 1562,
"sourceResources": [
{
"compression": "brotli",
"dataPackPath": "/usr/share/qt6/resources/qtwebengine_resources.pak",
"endByteExclusive": 1562,
"entrySha256": "68b477e85e06e84b92c4cb956b6072fc44c77e0418718ae0732645d659ae98d4",
"entrySize": 2687,
"resourceId": 44823,
"startByte": 0
},
{
"compression": "brotli",
"dataPackPath": "/usr/share/qt6/resources/qtwebengine_resources.pak",
"endByteExclusive": 1562,
"entrySha256": "730d9a36ff0539952fb6d3300c7b27a6bb1586d5b36d7d10f1b0050ac10157d5",
"entrySize": 18530,
"resourceId": 44846,
"startByte": 0
},
{
"compression": "brotli",
"dataPackPath": "/usr/share/qt6/resources/qtwebengine_resources.pak",
"endByteExclusive": 1576,
"entrySha256": "084c7fe91b2f3198862918884a47043690ed8d6eaee6b97564b580bcc754b255",
"entrySize": 140644,
"resourceId": 44847,
"startByte": 14
}
]
},
{
"name": "9a33e7247adcc6a4a8e5277c1ed1a5108ac4171799e6c691d928fc574467b528.txt",
"sha256": "9a33e7247adcc6a4a8e5277c1ed1a5108ac4171799e6c691d928fc574467b528",
"size": 1418,
"sourceResources": [
{
"compression": "brotli",
"dataPackPath": "/usr/share/qt6/resources/qtwebengine_resources.pak",
"endByteExclusive": 1418,
"entrySha256": "bcf80bc68a433419b2d9024f8cae288399582b796a61a67bfc53c4c9f6b25240",
"entrySize": 3723,
"resourceId": 44832,
"startByte": 0
}
]
},
{
"name": "c727e9f69804169e68131c4a8d09b3b059a6f6484a34fa3a0342e601354bbf49.txt",
"sha256": "c727e9f69804169e68131c4a8d09b3b059a6f6484a34fa3a0342e601354bbf49",
"size": 1404,
"sourceResources": [
{
"compression": "brotli",
"dataPackPath": "/usr/share/qt6/resources/qtwebengine_resources.pak",
"endByteExclusive": 1404,
"entrySha256": "569cb8621956590633cc5bdc69c274d319abf18a15ecfaebbb3bc6992e9240f1",
"entrySize": 3859,
"resourceId": 44834,
"startByte": 0
}
]
}
],
"package": "qt6-webengine",
"packageVersion": "6.11.2-1",
"qtVersion": "6.11.2",
"runtimeDistribution": "system-not-bundled",
"schemaVersion": 1,
"scope": "Partial notice texts extracted from 13 resources in the inspected QtWebEngine DataPacks; not complete Chromium attributions or corresponding sources"
}
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh
set -e
case "$1" in
configure)
if [ -d /sys/module/apparmor ]; then
/usr/sbin/apparmor_parser -r /etc/apparmor.d/docview
fi
;;
esac
exit 0
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh
set -e
case "$1" in
remove|deconfigure)
if [ -d /sys/module/apparmor ]; then
/usr/sbin/apparmor_parser -R /etc/apparmor.d/docview
fi
;;
esac
exit 0
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
set -eu
docview_root=/opt/docview
export LD_LIBRARY_PATH="$docview_root/lib:$docview_root/qt/lib"
export QT_PLUGIN_PATH="$docview_root/qt/plugins"
export QT_QPA_PLATFORM_PLUGIN_PATH="$docview_root/qt/plugins/platforms"
export QML_IMPORT_PATH="$docview_root/qt/qml"
export QML2_IMPORT_PATH="$docview_root/qt/qml"
export QTWEBENGINEPROCESS_PATH="$docview_root/qt/libexec/QtWebEngineProcess"
export QTWEBENGINE_RESOURCES_PATH="$docview_root/qt/resources"
export QTWEBENGINE_LOCALES_PATH="$docview_root/qt/translations/qtwebengine_locales"
exec "$docview_root/bin/docview" "$@"
+7
View File
@@ -0,0 +1,7 @@
abi <abi/4.0>,
include <tunables/global>
profile docview-bundled-qtwebengine /opt/docview/qt/libexec/QtWebEngineProcess flags=(unconfined) {
userns,
include if exists <local/docview>
}
+10
View File
@@ -0,0 +1,10 @@
[Desktop Entry]
Type=Application
Name=DocView
Comment=Read PDF, HTML and EPUB documents
Comment[ja]=PDF・HTML・EPUB文書を閲覧
Exec=/usr/bin/docview %f
Icon=text-x-generic
Terminal=false
Categories=Office;Viewer;
MimeType=application/pdf;text/html;application/xhtml+xml;application/epub+zip;application/zip;
+516
View File
@@ -0,0 +1,516 @@
(function () {
"use strict";
// This fixed program runs only in Qt's isolated ApplicationWorld. It adds
// no DOM nodes, IDs, bridges, document scripts, or host capabilities.
if (globalThis.__docviewReader) return;
const MAX_INDEX = 2000;
let headings = [];
let options = { fixed: false, rtl: false, publisherStyle: true };
let originalTypography = null;
let focusedLink = null;
let linkOutline = null;
let restoredRange = null;
let layoutAnchor = null, resourceRestoreFrame = 0;
let userScrollPending = false, userScrollFrame = 0;
let navigationTargets = [], navigationRevision = "";
let resourceIssues = Object.create(null);
// Only aggregate directive classes. Never copy blockedURI, sourceFile,
// originalPolicy or sample into the application or its saved state.
document.addEventListener("securitypolicyviolation", event => {
if (!event.isTrusted || event.disposition !== "enforce") return;
const directive = String(event.effectiveDirective || "").split("-")[0];
const category = ({img:"image", style:"style", font:"font", script:"script", frame:"frame",
child:"frame", connect:"connect", form:"form", base:"base", object:"object", media:"media"})[directive] || "other";
const key = "csp." + category;
resourceIssues[key] = Math.min(999999, (resourceIssues[key] || 0) + 1);
});
function text(node, limit = 512) {
return String(node ? node.textContent || "" : "").replace(/\s+/g, " ").trim().slice(0, limit);
}
function visible(element) {
if (!element || !element.isConnected || element.hidden || element.closest("[hidden],[aria-hidden='true']")) return false;
const style = getComputedStyle(element);
return style.display !== "none" && style.visibility !== "hidden" && style.visibility !== "collapse" && element.getClientRects().length > 0;
}
function axis() {
const style = getComputedStyle(document.body || document.documentElement);
const vertical = /^(vertical|sideways)/.test(style.writingMode);
return { vertical, reverse: vertical && /-rl$/.test(style.writingMode), writingMode: style.writingMode };
}
function progression() {
const root = document.scrollingElement || document.documentElement;
const a = axis();
const total = a.vertical ? root.scrollWidth - innerWidth : root.scrollHeight - innerHeight;
const position = a.vertical ? (a.reverse ? -scrollX : scrollX) : scrollY;
return total > 0 ? Math.max(0, Math.min(1, position / total)) : 0;
}
function setProgression(value) {
const root = document.scrollingElement || document.documentElement;
const a = axis();
const p = Math.max(0, Math.min(1, Number(value) || 0));
const total = a.vertical ? root.scrollWidth - innerWidth : root.scrollHeight - innerHeight;
if (a.vertical) scrollTo({ left: Math.max(0, total) * p * (a.reverse ? -1 : 1), top: 0, behavior: "instant" });
else scrollTo({ left: 0, top: Math.max(0, total) * p, behavior: "instant" });
}
function hrefFor(reference, base = document.baseURI) {
try {
const url = new URL(reference, base);
if (url.protocol !== "doc:" || url.host !== location.host || url.username || url.password) return "";
return url.pathname.replace(/^\//, "") + url.search + url.hash;
} catch (_) { return ""; }
}
function headingIndex() {
headings = [];
const nodes = document.querySelectorAll("h1,h2,h3,h4,h5,h6,[role='heading']");
let truncated = false;
for (const node of nodes) {
if (!visible(node)) continue;
const name = node.localName.toLowerCase();
const nativeLevel = /^h[1-6]$/.test(name) ? Number(name[1]) : 0;
const ariaLevel = Number(node.getAttribute("aria-level"));
const level = nativeLevel || (Number.isInteger(ariaLevel) && ariaLevel > 0 && ariaLevel <= 6 ? ariaLevel : 0);
if (!level) continue;
if (headings.length === MAX_INDEX) { truncated = true; break; }
headings.push({ element: node, level, title: text(node) || "見出し(レベル" + level + ")" });
}
return { truncated, rows: headings.map((h, index) => ({ index, title: h.title, level: h.level, depth: h.level - 1,
href: h.element.id ? hrefFor("#" + encodeURIComponent(h.element.id), location.href) : "", source: "html-heading", precision: "exact" })) };
}
function index() {
const result = headingIndex();
const outline = [];
const navs = document.querySelectorAll("[role='doc-toc'],nav");
for (const nav of navs) {
// XHTML keeps epub:type in its XML namespace; HTML retains the
// prefixed attribute name. CSS attribute selectors do not match both.
const epubType = nav.getAttributeNS("http://www.idpf.org/2007/ops", "type") || nav.getAttribute("epub:type") || "";
if (nav.getAttribute("role") !== "doc-toc" && !epubType.split(/\s+/).includes("toc")) continue;
for (const link of nav.querySelectorAll("a[href]")) {
if (!visible(link) || outline.length >= MAX_INDEX) continue;
let depth = 0;
for (let parent = link.parentElement; parent && parent !== nav; parent = parent.parentElement)
if (parent.localName.toLowerCase() === "ol" || parent.localName.toLowerCase() === "ul") ++depth;
const href = hrefFor(link.getAttribute("href"));
outline.push({ title: text(link) || "目次項目", href, depth: Math.max(0, depth - 1), missing: !href, source: "html-toc" });
}
}
return { title: String(document.title || "").slice(0, 4096), headings: result.rows, outline,
truncated: result.truncated, scriptCount: document.scripts.length,
fixedViewport: viewport(), writingMode: axis().writingMode };
}
function viewport() {
const element = document.querySelector("meta[name='viewport']");
const value = element ? element.getAttribute("content") || "" : "";
const w = /(?:^|[,;\s])width\s*=\s*([0-9.]+)/i.exec(value);
const h = /(?:^|[,;\s])height\s*=\s*([0-9.]+)/i.exec(value);
let width = w ? Number(w[1]) : 0, height = h ? Number(h[1]) : 0;
if ((!width || !height) && document.documentElement.localName === "svg") {
const box = document.documentElement.viewBox && document.documentElement.viewBox.baseVal;
if (box) { width = box.width; height = box.height; }
}
return width > 0 && height > 0 && width <= 100000 && height <= 100000 ? { width, height } : {};
}
function cfiFor(node, offset = 0) {
const steps = [];
let target = node;
if (!target) return "";
if (target.nodeType === Node.TEXT_NODE) {
let elements = 0, previousText = 0;
for (const sibling of target.parentNode.childNodes) {
if (sibling === target) break;
if (sibling.nodeType === Node.ELEMENT_NODE) { ++elements; previousText = 0; }
if (sibling.nodeType === Node.TEXT_NODE) previousText += sibling.length;
}
steps.unshift(elements * 2 + 1);
offset += previousText;
target = target.parentElement;
}
while (target && target !== document.documentElement && steps.length < 128) {
let order = 1;
for (let sibling = target.previousElementSibling; sibling; sibling = sibling.previousElementSibling) ++order;
steps.unshift(order * 2);
target = target.parentElement;
}
return target === document.documentElement ? "epubcfi(/" + steps.join("/") + ":" + Math.max(0, offset) + ")" : "";
}
function resolveCfi(cfi) {
if (typeof cfi !== "string" || cfi.length > 4096 || !/^epubcfi\(.*\)$/.test(cfi)) return null;
let value = cfi.slice(8, -1);
if (value.includes("!")) value = value.slice(value.lastIndexOf("!") + 1);
value = value.replace(/\[(?:\^.|[^\]])*\]/g, "");
if (!/^\/(?:[1-9][0-9]*\/)*[1-9][0-9]*(?::[0-9]+)?$/.test(value)) return null;
const parts = value.split(":");
const steps = parts[0].slice(1).split("/").map(Number);
if (steps.length > 128) return null;
let node = document.documentElement;
let offset = Number(parts[1] || 0);
for (let i = 0; i < steps.length; ++i) {
const step = steps[i];
if (step % 2 === 0) node = node && node.children && node.children[step / 2 - 1];
else {
if (!node || i !== steps.length - 1) return null;
const slot = (step - 1) / 2;
let elements = 0, found = null;
for (const child of node.childNodes) {
if (child.nodeType === Node.ELEMENT_NODE) ++elements;
else if (child.nodeType === Node.TEXT_NODE && elements === slot) {
if (offset <= child.length) { found = child; break; }
offset -= child.length;
}
}
node = found;
}
if (!node) return null;
}
const range = document.createRange();
if (node.nodeType === Node.TEXT_NODE) range.setStart(node, Math.min(offset, node.length));
else range.setStart(node, Math.min(offset, node.childNodes.length));
range.collapse(true);
return range;
}
function visibleRange() {
const a = axis();
const x = Math.max(1, Math.min(innerWidth - 1, a.vertical && a.reverse ? innerWidth - 20 : 20));
const y = Math.max(1, Math.min(innerHeight - 1, 20));
let range = document.caretRangeFromPoint ? document.caretRangeFromPoint(x, y) : null;
if (range && range.startContainer.nodeType === Node.TEXT_NODE) return range;
const element = document.elementFromPoint(x, y) || document.body || document.documentElement;
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
let node, inspected = 0;
while ((node = walker.nextNode()) && ++inspected < 1000) {
if (!node.textContent.trim() || !visible(node.parentElement)) continue;
range = document.createRange(); range.setStart(node, 0); range.collapse(true); return range;
}
range = document.createRange(); range.selectNodeContents(element); range.collapse(true); return range;
}
function locate() {
// A font can change layout before its loadingdone event, and a location
// poll can run before the image's animation-frame restoration. Retain
// the pre-layout anchor until that restoration has used it.
if (layoutAnchor && layoutMoved(layoutAnchor)) resourcesSettled();
// Reflow can put a different character at the viewport sampling point.
// Keep the restored logical character until the reader actually scrolls.
const keep = restoredRange && document.documentElement.contains(restoredRange.range.startContainer) &&
Math.abs(scrollX - restoredRange.x) < 1 && Math.abs(scrollY - restoredRange.y) < 1;
if (!keep) restoredRange = null;
const range = keep ? restoredRange.range : visibleRange();
const node = range.startContainer;
const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
const rect = range.getBoundingClientRect();
const result = { url: location.href, cfi: cfiFor(node, range.startOffset), fragment: element && element.id || "",
progression: progression(), textQuote: node.nodeType === Node.TEXT_NODE ? node.textContent.slice(range.startOffset, range.startOffset + 160) : text(element, 160),
offsetX: rect.left / Math.max(1, innerWidth), offsetY: rect.top / Math.max(1, innerHeight), writingMode: axis().writingMode,
currentOutline: currentNavigation(range), navigationRevision };
if (!resourceRestoreFrame) layoutAnchor = { range: range.cloneRange(), saved: result,
x:scrollX, y:scrollY, documentX:rect.left + scrollX, documentY:rect.top + scrollY,
width:innerWidth, height:innerHeight };
return result;
}
function layoutMoved(anchor) {
if (!anchor.range.startContainer.isConnected || anchor.width !== innerWidth || anchor.height !== innerHeight) return false;
const rect = anchor.range.getBoundingClientRect();
return Math.abs(rect.left + scrollX - anchor.documentX) > 1 || Math.abs(rect.top + scrollY - anchor.documentY) > 1;
}
function cancelResourceRestore() {
if (resourceRestoreFrame) cancelAnimationFrame(resourceRestoreFrame);
resourceRestoreFrame = 0;
layoutAnchor = null;
}
function resourcesSettled() {
if (resourceRestoreFrame || !layoutAnchor || options.fixed) return;
const anchor = layoutAnchor;
resourceRestoreFrame = requestAnimationFrame(() => {
resourceRestoreFrame = 0;
// Retain the character and its viewport fraction across a decoded
// image or font swap. Never insert helper nodes into the source DOM.
// Clear the old snapshot before locate(), otherwise it would see
// the same completed layout change and schedule another frame.
const current = layoutAnchor === anchor;
if (current) layoutAnchor = null;
if (current && anchor.range.startContainer.isConnected)
positionRange(anchor.range, anchor.saved);
userScrollPending = false;
locate();
});
}
document.addEventListener("load", event => {
if (event.isTrusted && event.target instanceof HTMLImageElement) resourcesSettled();
}, true);
document.addEventListener("error", event => {
if (event.isTrusted && event.target instanceof HTMLImageElement) resourcesSettled();
}, true);
if (document.fonts) {
document.fonts.addEventListener("loadingdone", resourcesSettled);
document.fonts.addEventListener("loadingerror", resourcesSettled);
}
for (const type of ["wheel", "touchstart", "touchmove", "pointerdown", "pointermove", "keydown"])
document.addEventListener(type, event => {
if (!event.isTrusted || (type === "pointermove" && !event.buttons)) return;
cancelResourceRestore();
// Clicking without scrolling must not leave a 400 ms gap without
// any anchor. A following input scroll still takes precedence.
if (document.body) locate();
userScrollPending = true;
// Input intent applies to this frame, not to an unrelated native
// anchor adjustment long after a click that did not scroll.
if (userScrollFrame) cancelAnimationFrame(userScrollFrame);
userScrollFrame = requestAnimationFrame(() => { userScrollFrame = 0; userScrollPending = false; });
}, {capture:true, passive:true});
document.addEventListener("scroll", () => {
if (layoutAnchor && Math.abs(scrollX - layoutAnchor.x) < 1 && Math.abs(scrollY - layoutAnchor.y) < 1) return;
if (!userScrollPending && layoutAnchor && layoutMoved(layoutAnchor)) {
// Native overflow-anchor can move the scroll offset as resources
// reflow. This is not a new reader navigation.
resourcesSettled();
return;
}
userScrollPending = false;
cancelResourceRestore();
restoredRange = null;
locate();
}, {passive:true});
window.addEventListener("hashchange", () => { cancelResourceRestore(); locate(); });
function configureNavigation(args) {
navigationTargets = []; navigationRevision = "";
if (!args || typeof args.revision !== "string" || !/^[0-9]{1,19}$/.test(args.revision) ||
!Array.isArray(args.targets) || args.targets.length > 20000) return false;
const targets = [];
for (const item of args.targets) {
if (!item || !Number.isInteger(item.row) || item.row < 0 || item.row >= 20000) return false;
let element;
if (Number.isInteger(item.heading) && item.heading >= 0 && item.heading < headings.length)
element = headings[item.heading].element;
else if (typeof item.fragment === "string" && item.fragment.length <= 2048)
element = item.fragment ? document.getElementById(item.fragment) : document.body || document.documentElement;
if (!visible(element)) continue;
const range = document.createRange(); range.selectNodeContents(element); range.collapse(true);
targets.push({ row:item.row, element, range });
}
targets.sort((a, b) => a.range.compareBoundaryPoints(Range.START_TO_START, b.range) || a.row - b.row);
navigationTargets = targets; navigationRevision = args.revision;
return true;
}
function currentNavigation(position) {
// DOM order is stable across zoom/reflow. Avoid measuring every target's
// layout at each 400 ms location poll, even for a 20,000-item EPUB nav.
let low = 0, high = navigationTargets.length;
while (low < high) {
const mid = (low + high) >>> 1;
if (navigationTargets[mid].range.compareBoundaryPoints(Range.START_TO_START, position) <= 0) low = mid + 1;
else high = mid;
}
for (let i = low - 1; i >= 0; --i)
if (visible(navigationTargets[i].element)) return navigationTargets[i].row;
return -1;
}
function positionRange(range, saved) {
const node = range.startContainer;
const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
if (!element || !visible(element)) return false;
element.scrollIntoView({ block: "start", inline: "start", behavior: "instant" });
const rect = range.getBoundingClientRect();
const a = axis();
const offset = a.vertical ? Number(saved.offsetX) : Number(saved.offsetY);
const fraction = Number.isFinite(offset) && Math.abs(offset) <= 2 ? offset : 0;
if (a.vertical) scrollBy({ left: rect.left - fraction * innerWidth, behavior: "instant" });
else scrollBy({ top: rect.top - fraction * innerHeight, behavior: "instant" });
restoredRange = { range: range.cloneRange(), x: scrollX, y: scrollY };
return true;
}
function restore(saved) {
if (!saved || typeof saved !== "object") return { restored: false, approximate: true };
let range = resolveCfi(saved.cfi);
if (range && positionRange(range, saved)) return { restored: true, approximate: false };
const fragment = typeof saved.fragment === "string" ? saved.fragment.slice(0, 1024) : "";
const element = fragment ? document.getElementById(fragment) : null;
if (element && visible(element)) {
restoredRange = null;
element.scrollIntoView({ block: "start", inline: "start", behavior: "instant" });
// A failed character-level CFI resolved only to its containing ID.
return { restored: true, approximate: Boolean(saved.cfi) };
}
const quote = typeof saved.textQuote === "string" ? saved.textQuote.slice(0, 160) : "";
if (quote) {
const walker = document.createTreeWalker(document.body || document.documentElement, NodeFilter.SHOW_TEXT);
let node, inspected = 0;
while ((node = walker.nextNode()) && ++inspected < 100000) {
const offset = node.textContent.indexOf(quote);
if (offset >= 0 && visible(node.parentElement)) {
range = document.createRange(); range.setStart(node, offset); range.collapse(true);
if (positionRange(range, saved)) return { restored: true, approximate: true };
}
}
}
setProgression(saved.progression);
return { restored: true, approximate: true };
}
function heading(direction) {
if (!headings.length) headingIndex();
const a = axis();
const candidates = direction > 0 ? headings : headings.slice().reverse();
const seen = new Set();
for (const h of candidates) {
const rect = h.element.getBoundingClientRect();
const distance = a.vertical ? (a.reverse ? innerWidth - rect.right : rect.left) : rect.top;
const position = Math.round(distance);
if (seen.has(position)) continue;
seen.add(position);
if ((direction > 0 && distance > 3) || (direction < 0 && distance < -3)) {
const x = scrollX, y = scrollY;
h.element.scrollIntoView({ block: "start", inline: "start", behavior: "instant" });
// Several final headings can clamp to the same destination.
// Check the current layout's actual result, so a repeated
// destination cannot masquerade as movement or block a boundary.
const after = h.element.getBoundingClientRect();
if (Math.abs(scrollX - x) < 1 && Math.abs(scrollY - y) < 1 &&
Math.abs(after.left - rect.left) < 1 && Math.abs(after.top - rect.top) < 1) continue;
return { moved: true, title: h.title };
}
}
return { moved: false, boundary: direction, empty: !headings.length };
}
function applyOptions(next) {
options = Object.assign({}, options, next || {});
if (!options.fixed && options.publisherStyle === false) {
const size = Number(options.fontSize), height = Number(options.lineHeight);
if (!originalTypography) {
const body = document.body || document.documentElement;
const base = parseFloat(getComputedStyle(body).fontSize) || 16;
const nodes = [document.documentElement, body];
// Inline priorities override authored fixed-pixel declarations.
// Relative heading/ruby sizes survive; DOM nodes and CFI paths do not change.
for (const node of body.querySelectorAll("*")) {
if (node.namespaceURI !== "http://www.w3.org/1999/xhtml") continue;
if (Array.from(node.childNodes).some(child => child.nodeType === Node.TEXT_NODE && child.textContent.trim())) nodes.push(node);
}
originalTypography = Array.from(new Set(nodes)).map(node => ({ node,
hadStyle: node.hasAttribute("style"),
font: node.style.getPropertyValue("font-size"), fontPriority: node.style.getPropertyPriority("font-size"),
line: node.style.getPropertyValue("line-height"), linePriority: node.style.getPropertyPriority("line-height"),
ratio: node === document.documentElement || node === body ? 1 : (parseFloat(getComputedStyle(node).fontSize) || base) / base }));
}
for (const entry of originalTypography) {
if (size >= 12 && size <= 40) entry.node.style.setProperty("font-size", size * entry.ratio + "px", "important");
if (height >= 1 && height <= 2.5) entry.node.style.setProperty("line-height", String(height), "important");
}
} else if (originalTypography) {
for (const entry of originalTypography) {
if (entry.font) entry.node.style.setProperty("font-size", entry.font, entry.fontPriority);
else entry.node.style.removeProperty("font-size");
if (entry.line) entry.node.style.setProperty("line-height", entry.line, entry.linePriority);
else entry.node.style.removeProperty("line-height");
if (!entry.hadStyle && !entry.node.getAttribute("style")) entry.node.removeAttribute("style");
}
originalTypography = null;
}
return { fixed: !!options.fixed, fixedViewport: viewport() };
}
function restoreLinkOutline() {
if (!focusedLink || !linkOutline) return;
for (const entry of linkOutline.properties) {
if (entry.value) focusedLink.style.setProperty(entry.name, entry.value, entry.priority);
else focusedLink.style.removeProperty(entry.name);
}
if (!linkOutline.hadStyle && !focusedLink.getAttribute("style")) focusedLink.removeAttribute("style");
linkOutline = null;
}
function clearLinkSelection() {
restoreLinkOutline();
focusedLink = null;
// selectedLink also supports a natively focused anchor. Clear both
// routes so Enter cannot follow a link after selection is cancelled.
const active = document.activeElement;
if (active && active.localName === "a") active.blur();
return { cleared: true };
}
function focusLink(direction) {
const links = Array.from(document.querySelectorAll("a[href]")).filter(visible).slice(0, 20000);
if (!links.length) return { empty: true };
const current = links.indexOf(focusedLink || document.activeElement);
const index = current < 0 ? (direction > 0 ? 0 : links.length - 1) : (current + direction + links.length) % links.length;
restoreLinkOutline();
focusedLink = links[index];
linkOutline = { hadStyle: focusedLink.hasAttribute("style"),
properties: ["outline-color", "outline-style", "outline-width", "outline-offset"].map(name => ({
name, value: focusedLink.style.getPropertyValue(name), priority: focusedLink.style.getPropertyPriority(name)
})) };
focusedLink.style.outline = "2px solid #008577";
focusedLink.style.outlineOffset = "3px";
focusedLink.focus({ preventScroll: false });
return { focused: true, title: text(focusedLink), index, count: links.length };
}
function selectedLink() {
const element = focusedLink && focusedLink.isConnected ? focusedLink : document.activeElement;
if (!element || element.localName !== "a" || !element.hasAttribute("href")) return { empty: true };
try {
const url = new URL(element.getAttribute("href"), document.baseURI);
if (!["doc:", "http:", "https:"].includes(url.protocol) || url.username || url.password) return { blocked: true };
if (url.protocol === "doc:" && url.host !== location.host) return { blocked: true };
return { url: url.href.slice(0, 8192), title: text(element) };
} catch (_) { return { blocked: true }; }
}
function executeCommand(id, args) {
args = args && typeof args === "object" ? args : {};
const a = axis();
const step = Math.max(1, Math.min(100, Number(args.count) || 1));
switch (id) {
case "resource.issues": {
const result = resourceIssues;
resourceIssues = Object.create(null);
return result;
}
case "index": return index();
case "location": return locate();
case "navigation.configure": return configureNavigation(args);
case "location.restore": return restore(args);
case "layout": return applyOptions(args);
case "link.next": return focusLink(1);
case "link.previous": return focusLink(-1);
case "link.clear": return clearLinkSelection();
case "link.activate": return selectedLink();
case "scroll.down": scrollBy({ top: 48 * step, behavior: "instant" }); break;
case "scroll.up": scrollBy({ top: -48 * step, behavior: "instant" }); break;
case "scroll.left": scrollBy({ left: -48 * step, behavior: "instant" }); break;
case "scroll.right": scrollBy({ left: 48 * step, behavior: "instant" }); break;
case "scroll.half_down": case "scroll.half_up": case "scroll.page_down": case "scroll.page_up": {
const forward = id.endsWith("down") ? 1 : -1;
const size = id.includes("half") ? 0.5 : 0.9;
scrollBy(a.vertical ? { left: innerWidth * size * forward * (a.reverse ? -1 : 1), behavior: "instant" }
: { top: innerHeight * size * forward, behavior: "instant" }); break;
}
case "nav.document_start": setProgression(0); break;
case "nav.document_end": setProgression(1); break;
case "nav.heading_next": return heading(1);
case "nav.heading_previous": return heading(-1);
case "heading.first": if (!headings.length) headingIndex(); if (headings.length) headings[0].element.scrollIntoView({block:"start",inline:"start"}); return {moved:!!headings.length};
case "heading.last": if (!headings.length) headingIndex(); if (headings.length) headings[headings.length-1].element.scrollIntoView({block:"start",inline:"start"}); return {moved:!!headings.length};
case "heading.index": {
const index = Number(args.index);
if (Number.isInteger(index) && index >= 0 && index < headings.length) headings[index].element.scrollIntoView({ block: "start", inline: "start", behavior: "instant" });
break;
}
default: return { handled: false };
}
return { handled: true };
}
function command(id, args) {
const passive = ["location", "index", "navigation.configure", "resource.issues"].includes(id);
if (!passive) { cancelResourceRestore(); userScrollPending = false; }
try { return executeCommand(id, args); }
finally { if (!passive && document.body) locate(); }
}
function block(event) { event.preventDefault(); event.stopImmediatePropagation(); }
document.addEventListener("submit", block, true);
document.addEventListener("reset", block, true);
document.addEventListener("beforeinput", block, true);
document.addEventListener("drop", block, true);
document.addEventListener("click", event => {
if (event.target.closest && event.target.closest("button,input,select,textarea,[contenteditable]")) block(event);
}, true);
document.addEventListener("keydown", event => {
if (event.target.closest && event.target.closest("button,input,select,textarea,[contenteditable]") && !(event.ctrlKey && /^(a|c)$/i.test(event.key))) block(event);
}, true);
document.addEventListener("play", event => { if (event.target.pause) event.target.pause(); }, true);
globalThis.__docviewReader = Object.freeze({ command, index, location: locate, restore });
})();
+386
View File
@@ -0,0 +1,386 @@
#include "benchmark.h"
#include <QCoreApplication>
#include <QCryptographicHash>
#include <QDir>
#include <QFile>
#include <QGuiApplication>
#include <QJsonDocument>
#include <QKeyEvent>
#include <QSaveFile>
#include <QScreen>
#include <QSysInfo>
#include <chrono>
#ifdef Q_OS_LINUX
#include <unistd.h>
#endif
namespace docview {
static qint64 monotonicNs() {
return std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
}
static QJsonObject residentSnapshot() {
#ifdef Q_OS_LINUX
// PPid traversal includes children created by ANY application thread. The
// main-thread /proc/PID/task/PID/children file misses Chromium descendants.
QHash<qint64, RendererProcessInfo> processes;
const auto pageSize = quint64(sysconf(_SC_PAGESIZE));
for (const auto &name : QDir("/proc").entryList(QDir::Dirs | QDir::NoDotAndDotDot)) {
bool ok = false; const auto pid = name.toLongLong(&ok); if (!ok) continue;
QFile file("/proc/" + name + "/stat"); RendererProcessInfo info;
if (file.open(QIODevice::ReadOnly) && parseRendererProcStat(file.readAll(), pageSize, &info)) processes[pid] = info;
}
const auto root = QCoreApplication::applicationPid();
if (!processes.contains(root)) return {{"available", false}};
QMultiHash<qint64, qint64> children;
for (auto it = processes.begin(); it != processes.end(); ++it) children.insert(it->parentPid, it.key());
QSet<qint64> seen; QList<qint64> pending{root}; qint64 total = 0;
while (!pending.isEmpty()) {
const auto pid = pending.takeLast(); if (seen.contains(pid)) continue;
seen.insert(pid); total += qint64(processes[pid].residentBytes);
for (auto child : children.values(pid)) pending.append(child);
}
return {{"available", true}, {"applicationBytes", qint64(processes[root].residentBytes)},
{"processGroupBytes", total}, {"processCount", seen.size()}};
#else
// Unsupported platforms are not reported as a fictitious zero-byte sample.
return {{"available", false}, {"reason", "RSS sampling is currently implemented on Linux only"}};
#endif
}
Benchmark::Benchmark(Controller *r, QQuickWindow *w, PdfCanvas *c, QString source, QString output,
int runs, int operations, int closeCycles, int scrollSteps, QObject *parent)
: QObject(parent), reader_(r), window_(w), canvas_(c), source_(std::move(source)), output_(std::move(output)),
runs_(runs), operations_(operations), closeCycles_(closeCycles), scrollSteps_(scrollSteps) {
connect(w, &QQuickWindow::frameSwapped, this, [this] {
const auto timestamp = monotonicNs();
QMetaObject::invokeMethod(this, [this, timestamp] { frame(timestamp); }, Qt::QueuedConnection);
}, Qt::DirectConnection);
connect(r, &Controller::positionChanged, this, &Benchmark::observePosition);
connect(r, &Controller::webActionStarted, this, [this](const QString &command, quint64 request) {
if (phase_ == Phase::Scrolling && (command == "scroll.down" || command == "scroll.up")) scrollRequests_.insert(request);
if ((phase_ == Phase::Preparing || phase_ == Phase::Action) && command == awaitedCommand_) awaitedRequest_ = request;
});
connect(r, &Controller::webActionAcknowledged, this, [this](const QString &command, quint64 request) {
if ((phase_ == Phase::Scrolling || phase_ == Phase::Draining) && scrollRequests_.remove(request)) ++scrollApplied_;
if (request != awaitedRequest_ || command != awaitedCommand_ || !request) return;
if (phase_ == Phase::Preparing || phase_ == Phase::Action) commandApplied_ = true;
if (phase_ == Phase::Action && series_ == "page-scroll") {
applied_ = true; appliedNs_ = monotonicNs(); frames_ = 0; window_->update();
}
});
connect(r, &Controller::documentChanged, this, [this] { frames_ = 0; window_->update(); });
connect(c, &PdfCanvas::tileRequested, this, [this](int, bool prefetch, qint64) {
if (phase_ == Phase::Action && !prefetch) ++visibleRequests_;
pendingPeak_ = qMax(pendingPeak_, qint64(canvas_->pendingRenderCount()));
});
connect(c, &PdfCanvas::frameReady, this, [this] {
if (canvas_->viewportReady()) {
qualityReadyNs_ = monotonicNs();
if (phase_ == Phase::Opening) { applied_ = true; appliedNs_ = qualityReadyNs_; }
}
});
deadline_.setSingleShot(true);
connect(&deadline_, &QTimer::timeout, this, [this] { finish(false, "phase deadline exceeded: " + series_); });
memoryTimer_.setInterval(100);
connect(&memoryTimer_, &QTimer::timeout, this, [this] { sampleMemory(); });
uiTimer_.setInterval(16); uiTimer_.setTimerType(Qt::PreciseTimer);
connect(&uiTimer_, &QTimer::timeout, this, [this] {
const auto now = monotonicNs();
if (lastUiNs_ && uiIntervals_.size() < 100000) uiIntervals_.append((now - lastUiNs_) / 1e6);
lastUiNs_ = now;
const auto state = reader_->benchmarkSnapshot();
queuedRenderPeak_ = qMax(queuedRenderPeak_, state.value("queuedRenders").toLongLong());
activeRenderPeak_ = qMax(activeRenderPeak_, state.value("activeRenders").toLongLong());
if (canvas_) pendingPeak_ = qMax(pendingPeak_, qint64(canvas_->pendingRenderCount()));
});
scrollTimer_.setInterval(16); scrollTimer_.setTimerType(Qt::PreciseTimer);
connect(&scrollTimer_, &QTimer::timeout, this, &Benchmark::scrollStep);
}
void Benchmark::sampleMemory(const QString &label) {
lastMemory_ = residentSnapshot();
const int pressure = canvas_ ? canvas_->memoryPressureLevel() : 0;
maximumMemoryPressureLevel_ = qMax(maximumMemoryPressureLevel_, pressure);
lastMemory_["memoryPressureLevel"] = pressure;
lastMemory_["elapsedMs"] = (monotonicNs() - benchmarkStartNs_) / 1e6;
lastMemory_["phase"] = label.isEmpty() ? QString::number(int(phase_)) : label;
if (lastMemory_.value("available").toBool()) {
rssPeak_ = qMax(rssPeak_, lastMemory_.value("processGroupBytes").toInteger());
appRssPeak_ = qMax(appRssPeak_, lastMemory_.value("applicationBytes").toInteger());
}
if (canvas_) cachePeak_ = qMax(cachePeak_, canvas_->cacheCostBytes());
if (memorySamples_.size() < 10000) memorySamples_.append(lastMemory_);
}
void Benchmark::start() {
benchmarkStartNs_ = lastUiNs_ = monotonicNs();
window_->requestActivate(); sampleMemory("start"); startMemory_ = lastMemory_;
memoryTimer_.start(); uiTimer_.start(); nextOpen();
}
void Benchmark::nextOpen() {
phase_ = Phase::Opening; frames_ = 0; visibleRequests_ = 0;
applied_ = false; qualityReadyNs_ = 0;
operationStartNs_ = appliedNs_ = monotonicNs(); deadline_.start(30000);
reader_->openPath(source_); window_->update();
}
void Benchmark::closeCurrent(bool final) {
deadline_.stop(); finalClose_ = final; phase_ = Phase::Closing;
sampleMemory("before-close"); openingMemory_ = lastMemory_;
const auto snapshot = reader_->benchmarkSnapshot();
discardedTotal_ += snapshot.value("discardedQueuedRequests").toLongLong() + snapshot.value("queuedRequests").toLongLong();
closeStartNs_ = monotonicNs(); reader_->closeDocument();
// Deferred WebEngine/profile/process teardown must run before the post-close
// sample. Allocator/browser caches may remain; their retention is measured.
QTimer::singleShot(1000, this, [this] {
if (finished_ || phase_ != Phase::Closing) return;
sampleMemory("after-close");
closeRecords_.append(QJsonObject{{"cycle", openCount_}, {"beforeClose", openingMemory_},
{"afterClose", lastMemory_}, {"settleMs", (monotonicNs() - closeStartNs_) / 1e6},
{"workload", finalClose_ ? "navigation-and-scroll" : "open-first-visible-only"},
{"stateEmpty", reader_->state() == "Empty" && reader_->format().isEmpty()},
{"cacheBytesAfterClose", canvas_ ? canvas_->cacheCostBytes() : 0}});
if (reader_->state() != "Empty" || !reader_->format().isEmpty()) finish(false, "explicit close did not reach Empty");
else if (finalClose_) finish(true);
else nextOpen();
});
}
QVariantMap Benchmark::locator() const { return reader_->benchmarkSnapshot().value("location").toMap(); }
bool Benchmark::samePosition(const QVariantMap &a, const QVariantMap &b) {
for (const auto *key : {"pageIndex", "xPt", "yPt", "resourcePath", "cfi", "progression"})
if (a.value(key) != b.value(key)) return false;
return true;
}
bool Benchmark::readyForPresentation() const {
return reader_->state() == "Ready" && (format_ == "pdf" ? canvas_ && canvas_->viewportReady() : !locator().isEmpty());
}
void Benchmark::observePosition() {
if (phase_ == Phase::Opening && reader_->format() != "pdf" && readyForPresentation() && !applied_) {
applied_ = true; appliedNs_ = monotonicNs(); frames_ = 0; window_->update();
}
if (phase_ == Phase::Preparing && commandApplied_) {
applied_ = true; appliedNs_ = monotonicNs(); frames_ = 0; window_->update();
}
if (phase_ != Phase::Action || (series_ != "heading" && series_ != "chapter")) return;
if (format_ != "pdf" && series_ == "heading" && !commandApplied_) return;
const auto after = locator();
const bool moved = series_ == "chapter" ? after.value("resourcePath") != before_.value("resourcePath")
: !samePosition(before_, after);
if (!after.isEmpty() && moved && !applied_) {
applied_ = true; appliedNs_ = monotonicNs(); frames_ = 0; window_->update();
}
}
void Benchmark::prepareSeries() {
++seriesIndex_; seriesCount_ = 0; seriesTargets_ = 0; seriesTargetSource_.clear();
const QStringList families{"page-scroll", "heading", "chapter"};
if (seriesIndex_ >= families.size()) { startScroll(); return; }
series_ = families[seriesIndex_];
if (!operations_ || (series_ == "chapter" && format_ != "epub")) {
seriesRecords_.append(QJsonObject{{"series", series_}, {"status", "not-requested-or-not-applicable"}, {"count", 0}});
prepareSeries(); return;
}
phase_ = Phase::Preparing; frames_ = 0; preparationAligned_ = false; commandApplied_ = false; applied_ = false;
awaitedCommand_ = "nav.document_start"; awaitedRequest_ = 0;
if (format_ == "pdf") applied_ = true;
operationStartNs_ = monotonicNs(); deadline_.start(30000);
appliedNs_ = operationStartNs_;
reader_->execute("nav.document_start"); window_->update();
}
void Benchmark::dispatch(const QString &id, int page, bool repeat) {
auto key = [this, repeat](Qt::Key code, Qt::KeyboardModifiers modifiers, const QString &text) {
QKeyEvent press(QEvent::KeyPress, code, modifiers, text, repeat);
QCoreApplication::sendEvent(window_, &press);
QKeyEvent release(QEvent::KeyRelease, code, modifiers, text);
QCoreApplication::sendEvent(window_, &release);
};
if (id == "nav.page") {
for (const QChar digit : QString::number(page)) key(Qt::Key(Qt::Key_0 + digit.digitValue()), {}, QString(digit));
key(Qt::Key_G, Qt::ShiftModifier, "G");
} else if (id == "zoom.in") key(Qt::Key_Plus, {}, "+");
else if (id == "zoom.fit_width") key(Qt::Key_Equal, {}, "=");
else if (id.startsWith("nav.heading") || id.startsWith("nav.chapter")) {
const bool forward = id.endsWith("next");
key(forward ? Qt::Key_BracketRight : Qt::Key_BracketLeft, {}, forward ? "]" : "[");
if (id.startsWith("nav.chapter")) key(Qt::Key_C, {}, "c");
else key(forward ? Qt::Key_BracketRight : Qt::Key_BracketLeft, {}, forward ? "]" : "[");
} else if (id == "scroll.down" || id == "scroll.up") key(id.endsWith("down") ? Qt::Key_J : Qt::Key_K, {}, id.endsWith("down") ? "j" : "k");
else key(id == "scroll.page_down" ? Qt::Key_F : Qt::Key_B, Qt::ControlModifier, {});
}
void Benchmark::nextOperation() {
if (seriesCount_ >= operations_) {
seriesRecords_.append(QJsonObject{{"series", series_}, {"status", "measured"}, {"count", seriesCount_},
{"availableTargets", seriesTargets_}, {"targetSource", seriesTargetSource_}, {"traversalSpan", span_}});
prepareSeries(); return;
}
if (QGuiApplication::focusWindow() != window_) { finish(false, "benchmark window lost focus"); return; }
phase_ = Phase::Action; frames_ = visibleRequests_ = 0; responseRecorded_ = false;
commandApplied_ = false;
before_ = locator(); applied_ = format_ == "pdf" && series_ == "page-scroll";
const int n = seriesCount_; QString id; int page = 0;
if (series_ == "heading" || series_ == "chapter")
id = "nav." + series_ + ((n / span_) % 2 ? "_previous" : "_next");
else if (format_ == "pdf") {
const int count = qMax(1, int(reader_->pages().size()));
if (n % 10 == 0) {
pageA_ = ((n / 10 * 97) + 1) % count;
if (pageA_ == before_.value("pageIndex").toInt() && count > 1) pageA_ = (pageA_ + 1) % count;
pageB_ = (pageA_ + 1) % count;
}
id = "nav.page"; page = 1 + (n % 2 ? pageB_ : pageA_);
if (n % 10 == 8) id = "zoom.in";
if (n % 10 == 9) id = "zoom.fit_width";
} else id = n % 2 ? "scroll.page_up" : "scroll.page_down";
operation_ = {{"series", series_}, {"command", id}, {"index", n}};
if (series_ == "heading" || series_ == "chapter") {
operation_["availableTargets"] = seriesTargets_;
operation_["targetSource"] = seriesTargetSource_;
operation_["traversalSpan"] = span_;
}
awaitedCommand_ = id; awaitedRequest_ = 0;
if (page) operation_["requestedPhysicalPage"] = page;
operationStartNs_ = appliedNs_ = monotonicNs(); deadline_.start(30000);
qualityReadyNs_ = 0;
dispatch(id, page); observePosition();
if (format_ == "pdf") {
if (applied_) appliedNs_ = monotonicNs();
if (canvas_->viewportReady()) qualityReadyNs_ = monotonicNs();
}
window_->update();
}
void Benchmark::startScroll() {
if (!scrollSteps_) { closeCurrent(true); return; }
phase_ = Phase::Scrolling; series_ = "continuous-scroll";
scrollSent_ = scrollApplied_ = 0; lastScrollFrameNs_ = lastScrollDispatchNs_ = 0;
scrollRequests_.clear();
operationStartNs_ = monotonicNs(); deadline_.start(qMax(30000, scrollSteps_ * 50)); scrollTimer_.start();
}
void Benchmark::scrollStep() {
if (phase_ != Phase::Scrolling) return;
const auto now = monotonicNs();
if (lastScrollDispatchNs_) scrollDispatch_.append((now - lastScrollDispatchNs_) / 1e6);
lastScrollDispatchNs_ = now;
dispatch((scrollSent_ / 30) % 2 ? "scroll.up" : "scroll.down", 0, scrollSent_ != 0);
++scrollSent_; window_->update();
if (scrollSent_ >= scrollSteps_) { scrollTimer_.stop(); phase_ = Phase::Draining; frames_ = 0; }
}
void Benchmark::frame(qint64 presentedNs) {
if (finished_ || presentedNs < operationStartNs_) return;
if (phase_ == Phase::Scrolling || phase_ == Phase::Draining) {
if (lastScrollFrameNs_) scrollFrames_.append((presentedNs - lastScrollFrameNs_) / 1e6);
lastScrollFrameNs_ = presentedNs;
if (phase_ == Phase::Draining && readyForPresentation() && (format_ == "pdf" || scrollApplied_ >= scrollSent_)) {
if (++frames_ >= 2) { deadline_.stop(); closeCurrent(true); } else window_->update();
}
return;
}
if (phase_ == Phase::Opening) {
format_ = reader_->format();
if (!readyForPresentation() || !applied_ || presentedNs < appliedNs_) { window_->update(); return; }
if (++frames_ < 2) { window_->update(); return; }
++openCount_; if (opens_.size() < runs_) opens_.append((presentedNs - operationStartNs_) / 1e6);
sampleMemory("after-open"); rssAfterOpen_.append(lastMemory_.value("processGroupBytes"));
openRecords_.append(QJsonObject{{"cycle", openCount_}, {"openFrameMs", (presentedNs - operationStartNs_) / 1e6}, {"memory", lastMemory_}});
deadline_.stop();
if (openCount_ < qMax(runs_, closeCycles_ + 1)) closeCurrent(false);
else prepareSeries();
return;
}
if (phase_ == Phase::Preparing) {
const auto snapshot = reader_->benchmarkSnapshot();
if (!readyForPresentation() || (series_ == "heading" && !snapshot.value("headingsReady").toBool())) { window_->update(); return; }
if (!applied_ || presentedNs < appliedNs_) { window_->update(); return; }
const auto location = locator();
if (!preparationAligned_ && ((format_ == "pdf" && location.value("pageIndex").toInt() != 0) ||
(format_ != "pdf" && (location.value("progression").toDouble() > .001 ||
(format_ == "epub" && snapshot.value("chapterIndex").toInt() != 0))))) { window_->update(); return; }
if (++frames_ < 2) { window_->update(); return; }
int targets = series_ == "chapter" ? snapshot.value("chapterCount").toInt()
: series_ == "heading" ? snapshot.value("headingCount").toInt()
: format_ == "pdf" ? int(reader_->pages().size()) : 0;
const auto firstHeading = snapshot.value("firstHeading").toMap();
const QString targetSource = series_ == "chapter" ? QStringLiteral("epub-linear-spine")
: series_ == "heading" ? firstHeading.value("source").toString()
: format_ == "pdf" ? QStringLiteral("pdf-physical-pages") : QStringLiteral("viewport-scroll");
// A Web table of contents is not the DOM heading index. Otherwise
// heading commands can merely cross chapter boundaries while falsely
// being counted as measured heading navigation.
if (series_ == "heading" && format_ != "pdf" && targetSource != "html-heading") targets = 0;
if (series_ != "page-scroll" && targets < 2) {
seriesRecords_.append(QJsonObject{{"series", series_}, {"status", "insufficient-corpus-targets"},
{"count", 0}, {"availableTargets", targets}, {"targetSource", targetSource}});
deadline_.stop(); prepareSeries(); return;
}
if (series_ == "heading" && !preparationAligned_) {
preparationAligned_ = true; frames_ = 0; commandApplied_ = false; applied_ = format_ == "pdf";
appliedNs_ = monotonicNs();
awaitedCommand_ = "heading.index"; awaitedRequest_ = 0;
auto first = firstHeading;
// Use the known DOM heading index for setup, obtaining the normal
// command completion callback instead of an unacknowledged hash URL.
if (format_ != "pdf") first.remove("href");
reader_->activateItem(first); window_->update(); return;
}
if (series_ == "heading" && !applied_) { window_->update(); return; }
span_ = qMax(1, qMin(50, targets - 1));
seriesTargets_ = targets; seriesTargetSource_ = targetSource;
deadline_.stop(); QTimer::singleShot(20, this, [this] { nextOperation(); }); phase_ = Phase::Idle;
return;
}
if (phase_ != Phase::Action || !applied_ || presentedNs < appliedNs_ || reader_->state() != "Ready") return;
if (!responseRecorded_) { operation_["firstResponseFrameMs"] = (presentedNs - operationStartNs_) / 1e6; responseRecorded_ = true; }
if (++frames_ < 2 || !readyForPresentation() || (format_ == "pdf" && (!qualityReadyNs_ || presentedNs < qualityReadyNs_))) { window_->update(); return; }
operation_["finalQualityFrameMs"] = (presentedNs - operationStartNs_) / 1e6;
operation_["positionChanged"] = !samePosition(before_, locator());
if (format_ == "pdf") { operation_["visibleTileRequests"] = visibleRequests_; operation_["cacheHit"] = visibleRequests_ == 0; }
actions_.append(operation_); ++seriesCount_; phase_ = Phase::Idle; deadline_.stop();
QTimer::singleShot(20, this, [this] { nextOperation(); });
}
void Benchmark::finish(bool success, const QString &reason) {
if (finished_) return;
if (!success) {
const auto diagnosticPosition = [](const QVariantMap &location) {
QJsonObject result;
for (const auto *field : {"resourcePath", "pageIndex", "progression", "xPt", "yPt"})
if (location.contains(field)) result.insert(field, QJsonValue::fromVariant(location.value(field)));
return result;
};
operation_["beforePosition"] = diagnosticPosition(before_);
operation_["observedPosition"] = diagnosticPosition(locator());
operation_["commandAcknowledged"] = commandApplied_;
}
finished_ = true; phase_ = Phase::Done; deadline_.stop(); memoryTimer_.stop(); uiTimer_.stop(); scrollTimer_.stop();
sampleMemory("finish"); QFile input(source_); QByteArray hash;
if (input.open(QIODevice::ReadOnly)) { QCryptographicHash sha(QCryptographicHash::Sha256); sha.addData(&input); hash = sha.result().toHex(); }
QJsonObject display{{"windowLogicalWidth", window_->width()}, {"windowLogicalHeight", window_->height()},
{"windowDevicePixelRatio", window_->devicePixelRatio()}};
if (const auto screen = window_->screen()) {
display.insert("screenLogicalWidth", screen->geometry().width());
display.insert("screenLogicalHeight", screen->geometry().height());
display.insert("screenDevicePixelRatio", screen->devicePixelRatio());
display.insert("screenReportedRefreshRateHz", screen->refreshRate());
}
QJsonObject record{{"schemaVersion", 2}, {"success", success}, {"failureReason", reason}, {"qt", qVersion()},
{"display", display}, {"maximumMemoryPressureLevel", maximumMemoryPressureLevel_},
{"qtPlatform", QGuiApplication::platformName()}, {"sceneGraphBackend", QQuickWindow::sceneGraphBackend()},
{"requestedQtQuickBackend", qEnvironmentVariable("QT_QUICK_BACKEND")},
{"requestedRhiBackend", qEnvironmentVariable("QSG_RHI_BACKEND")},
{"os", QSysInfo::prettyProductName()}, {"kernel", QSysInfo::kernelVersion()}, {"cpuArchitecture", QSysInfo::currentCpuArchitecture()},
{"documentSha256", QString::fromLatin1(hash)}, {"format", format_}, {"openFrameMs", opens_}, {"operations", actions_},
{"operationSeries", seriesRecords_}, {"openCount", openCount_}, {"requestedOpenTimings", runs_}, {"requestedCloseCycles", closeCycles_},
{"openRecords", openRecords_},
{"memoryStart", startMemory_}, {"memoryAfterFinalClose", finalClose_ && reader_->state() == "Empty" ? QJsonValue(lastMemory_) : QJsonValue()}, {"openCloseCycles", closeRecords_},
{"failedOperation", success ? QJsonValue() : QJsonValue(operation_)},
{"notMeasured", QJsonArray{"rapid overlapping distant-page jumps", "resize latency", "replacement by a different document", "OS-cache-cold startup", "reference-hardware acceptance"}},
{"timingSemantics", "Qt frameSwapped is queued-for-presentation, not physical scanout. Web command/sequence ACK plus validated logical location and two subsequent Qt frames do not identify a Chromium compositor frame or prove target-pixel presentation. Xvfb results are reference measurements only."},
{"processGroupRssPeakBytes", rssPeak_}, {"applicationRssPeakBytes", appRssPeak_}, {"processGroupRssAfterEachOpenBytes", rssAfterOpen_},
{"memorySamples", memorySamples_}, {"rssSampleIntervalMs", 100}, {"closeSettleIntervalMs", 1000},
{"uiTimerExpectedIntervalMs", 16}, {"uiTimerIntervalMs", uiIntervals_},
{"continuousScroll", QJsonObject{{"requestedInputs", scrollSteps_}, {"dispatchedInputs", scrollSent_}, {"webAppliedCallbacks", scrollApplied_},
{"inputIntervalMs", 16}, {"frameIntervalMs", scrollFrames_}, {"dispatchIntervalMs", scrollDispatch_}}},
{"pendingRenderPeak", pendingPeak_}, {"queuedRenderPeak", queuedRenderPeak_}, {"activeRenderPeak", activeRenderPeak_},
{"discardedQueuedRequests", discardedTotal_}, {"tileCachePeakChargedBytes", cachePeak_},
{"tileCacheBudgetBytes", canvas_ ? canvas_->cacheBudgetBytes() : 0},
{"visibleTileRequests", canvas_ ? qint64(canvas_->visibleRenderRequestCount()) : 0},
{"prefetchTileRequests", canvas_ ? qint64(canvas_->prefetchRenderRequestCount()) : 0},
{"staleRenderResponses", canvas_ ? qint64(canvas_->staleRenderResponseCount()) : 0},
{"method", "Focused-window synthetic keys; subsequent frameSwapped timestamps. PDF requires every visible final tile. Heading/chapter operations require a changed validated logical position before presentation. Web page-scroll waits for its command callback. Every open is explicitly closed; post-close RSS waits 1 s. Linux RSS snapshots traverse all PPids, sampled every 100 ms, so peaks are sampled lower bounds. Continuous scroll injects repeat keys every 16 ms without waiting for rendering. OS file cache retained. Runtime success is not performance-budget or reference-hardware acceptance."}};
QSaveFile file(output_); const auto bytes = QJsonDocument(record).toJson();
const bool saved = file.open(QIODevice::WriteOnly) && file.write(bytes) == bytes.size() && file.commit();
QCoreApplication::exit(success && saved ? 0 : 4);
}
} // namespace docview
+56
View File
@@ -0,0 +1,56 @@
#pragma once
#include "controller.h"
#include <QJsonArray>
#include <QJsonObject>
#include <QTimer>
namespace docview {
// Developer harness. Timings use readiness-qualified Qt frameSwapped events;
// these are presentation-queue timestamps, not physical scanout timestamps.
class Benchmark : public QObject {
public:
Benchmark(Controller *, QQuickWindow *, PdfCanvas *, QString source, QString output,
int runs, int operations, int closeCycles, int scrollSteps, QObject *parent = nullptr);
void start();
private:
enum class Phase { Idle, Opening, Closing, Preparing, Action, Scrolling, Draining, Done };
void frame(qint64);
void nextOpen();
void closeCurrent(bool final);
void prepareSeries();
void nextOperation();
void startScroll();
void scrollStep();
void sampleMemory(const QString &label = {});
void observePosition();
void dispatch(const QString &, int page = 0, bool repeat = false);
void finish(bool success, const QString &reason = {});
bool readyForPresentation() const;
QVariantMap locator() const;
static bool samePosition(const QVariantMap &, const QVariantMap &);
Controller *reader_;
QQuickWindow *window_;
PdfCanvas *canvas_;
QString source_, output_, format_, series_;
QString awaitedCommand_;
quint64 awaitedRequest_ = 0;
QSet<quint64> scrollRequests_;
int pageA_ = 0, pageB_ = 0;
int seriesTargets_ = 0;
QString seriesTargetSource_;
int runs_, operations_, closeCycles_, scrollSteps_, openCount_ = 0, seriesIndex_ = -1, seriesCount_ = 0;
int frames_ = 0, visibleRequests_ = 0, span_ = 1, scrollSent_ = 0, scrollApplied_ = 0;
bool responseRecorded_ = false, applied_ = false, finalClose_ = false, finished_ = false, preparationAligned_ = false, commandApplied_ = false;
Phase phase_ = Phase::Idle;
QJsonArray opens_, actions_, rssAfterOpen_, memorySamples_, closeRecords_, seriesRecords_, openRecords_;
QJsonArray scrollFrames_, scrollDispatch_, uiIntervals_;
QJsonObject operation_, openingMemory_, startMemory_, lastMemory_;
QVariantMap before_;
QTimer deadline_, memoryTimer_, uiTimer_, scrollTimer_;
qint64 rssPeak_ = -1, appRssPeak_ = -1, cachePeak_ = 0, operationStartNs_ = 0, appliedNs_ = 0;
qint64 lastUiNs_ = 0, lastScrollFrameNs_ = 0, lastScrollDispatchNs_ = 0, closeStartNs_ = 0;
qint64 benchmarkStartNs_ = 0;
qint64 qualityReadyNs_ = 0;
qint64 pendingPeak_ = 0, queuedRenderPeak_ = 0, activeRenderPeak_ = 0, discardedTotal_ = 0;
int maximumMemoryPressureLevel_ = 0;
};
} // namespace docview
File diff suppressed because it is too large Load Diff
+227
View File
@@ -0,0 +1,227 @@
#pragma once
#include "common/worker_process.h"
#include "common/memory_pressure.h"
#include "core/config.h"
#include "core/input.h"
#include "core/state.h"
#include "pdf_canvas.h"
#include "web/renderer_watchdog.h"
#include "web/web_profile.h"
#include <QObject>
#include <QQuickWebEngineProfile>
#include <QQuickWindow>
#include <QTemporaryDir>
#include <QUrl>
#include <QVariantList>
namespace docview {
class FontBroker;
class Session : public QObject {
public:
using QObject::QObject;
QString token, source, format, root, entry;
qint64 generation = 0, navigationBytes = 0;
int initialPage = 0;
QVariantMap meta, lastPresented, sourceIdentity;
QHash<QString, int> resourceIssues;
QSet<QString> entries, extracted, headingKeys;
QHash<QString, QList<ResourceHandler::Completion>> extractWaiters;
qint64 expandedBytes = 0;
bool stateCleared = false;
bool stateIdentityRejected = false;
bool entrySelected = false;
bool resourcesStopped = false;
QHash<QString, QByteArray> mimeTypes;
WorkerProcess *worker = nullptr;
FontBroker *fontBroker = nullptr;
QQuickWebEngineProfile *profile = nullptr;
ResourceHandler *handler = nullptr;
std::unique_ptr<QTemporaryDir> temporary;
std::unique_ptr<QLockFile> sessionLock;
};
class Controller : public QObject {
Q_OBJECT
Q_PROPERTY(QString title READ title NOTIFY documentChanged)
Q_PROPERTY(QString format READ format NOTIFY documentChanged)
Q_PROPERTY(QString state READ state NOTIFY stateChanged)
Q_PROPERTY(QString notice READ notice NOTIFY noticeChanged)
Q_PROPERTY(QString context READ context WRITE setContext NOTIFY contextChanged)
Q_PROPERTY(QString mode READ mode WRITE setMode NOTIFY modeChanged)
Q_PROPERTY(QString pending READ pending NOTIFY pendingChanged)
Q_PROPERTY(QVariantMap settings READ settings NOTIFY settingsChanged)
Q_PROPERTY(QVariantList outline READ outline NOTIFY navigationChanged)
Q_PROPERTY(QVariantList pages READ pages NOTIFY navigationChanged)
Q_PROPERTY(QString listTitle READ listTitle NOTIFY navigationChanged)
Q_PROPERTY(QString outlineTitle READ outlineTitle NOTIFY navigationChanged)
Q_PROPERTY(bool outlineLoading READ outlineLoading NOTIFY navigationChanged)
Q_PROPERTY(int currentOutline READ currentOutline NOTIFY currentOutlineChanged)
Q_PROPERTY(QString position READ position NOTIFY positionChanged)
Q_PROPERTY(QString webToken READ webToken NOTIFY documentChanged)
Q_PROPERTY(QVariantList recentDocuments READ recentDocuments NOTIFY recentDocumentsChanged)
public:
Controller(QObject *parent = nullptr);
~Controller() override;
void initialize(const QString &config, bool readOnly = false,
const StoragePaths &paths = StoragePaths::forCurrentUser());
void attachWindow(QQuickWindow *w);
Q_INVOKABLE void attachCanvas(PdfCanvas *c);
QString title() const;
QString format() const;
QString state() const {
return state_;
}
QString notice() const {
return notice_;
}
QString context() const {
return context_;
}
QString mode() const {
return mode_;
}
QString pending() const {
return input_.pending();
}
QVariantMap settings() const {
return config_.snapshot();
}
QVariantList outline() const {
return outline_;
}
QVariantList pages() const {
return pages_;
}
QString listTitle() const;
QString outlineTitle() const;
bool outlineLoading() const;
int currentOutline() const { return currentOutline_; }
QString position() const;
QString webToken() const {
return active_ ? active_->token : QString{};
}
void setContext(const QString &);
void setMode(const QString &);
Q_INVOKABLE void open(const QUrl &url);
Q_INVOKABLE void openPath(const QString &path, const QString &password = {}, int initialPage = 0);
Q_INVOKABLE void cancel();
// Internal lifecycle and measurement API; not a document command or QML slot.
void closeDocument();
QVariantMap benchmarkSnapshot() const;
Q_INVOKABLE void execute(const QString &id, const QVariantMap &args = {});
Q_INVOKABLE bool command(const QString &text);
Q_INVOKABLE void password(const QString &text);
Q_INVOKABLE void chooseEntry(const QString &path);
Q_INVOKABLE bool navigationAllowed(const QUrl &url, int navigationType, const QString &token);
Q_INVOKABLE void webLoaded(const QString &token, bool ok);
Q_INVOKABLE void webIndex(const QString &token, const QVariantMap &data);
Q_INVOKABLE void webResourceIssues(const QString &token, const QVariantMap &counts);
Q_INVOKABLE void webLocation(const QString &token, const QVariantMap &location);
Q_INVOKABLE void activateItem(const QVariantMap &item);
Q_INVOKABLE void openExternal(const QUrl &url);
Q_INVOKABLE void setHtmlRoot(const QUrl &url);
Q_INVOKABLE void notify(const QString &message);
Q_INVOKABLE void search(const QString &text);
Q_INVOKABLE void renderStopped();
Q_INVOKABLE void webStarted(const QString &token, const QString &command, quint64 request);
Q_INVOKABLE void webApplied(const QString &token, const QString &command = {}, quint64 request = 0);
Q_INVOKABLE void webDocumentLoaded(const QString &token, const QUrl &url);
Q_INVOKABLE void registerRenderer(const QString &token, qint64 pid, int slot = 0);
Q_INVOKABLE quint32 beginRendererProbe(const QString &token, int slot);
Q_INVOKABLE void finishRendererProbe(const QString &token, int slot, quint32 probe);
Q_INVOKABLE void clearHistory();
Q_INVOKABLE int historyCount() const;
QVariantList recentDocuments() const;
Q_INVOKABLE void openRecent(const QString &documentId);
Q_INVOKABLE QVariantMap webOptions(const QString &token) const;
Q_INVOKABLE QVariantMap webNavigationTargets(const QString &token, const QUrl &url) const;
Q_INVOKABLE QString script(const QString &name) const;
signals:
void documentChanged();
void stateChanged();
void noticeChanged();
void contextChanged();
void modeChanged();
void pendingChanged();
void settingsChanged();
void navigationChanged();
void currentOutlineChanged();
void positionChanged();
void recentDocumentsChanged();
void uiCommand(QString id, QVariantMap args);
void webCommand(QString id, QVariantMap args);
void stageWeb(QQuickWebEngineProfile *profile, QUrl url, QString token);
void activateWeb(QString token);
void disposeWeb(QString token);
void externalLinkRequested(QUrl url);
void passwordNeeded(QString name);
void entryNeeded(QStringList candidates);
void framePresented();
void webActionApplied();
void webActionStarted(QString command, quint64 request);
void webActionAcknowledged(QString command, quint64 request);
protected:
bool eventFilter(QObject *, QEvent *) override;
private:
void fail(Session *s, const QString &code, const QString &message);
void recordOpenFailure(const QString &path, const QString &code);
bool failPdfWorker(Session *s, const QCborMap &error);
void commit(Session *s);
void dispose(Session *s);
void stopResources(Session *s);
void recordWarnings(Session *s, const QStringList &warnings);
void recordResourceIssue(Session *s, const QString &key, int count);
void prepareWeb(Session *s);
void archiveEntries(Session *s, int offset);
void archiveMetadata(Session *s);
void loadPdfPages(int page);
void updatePageLabels(const QVariantList &pages);
void publishNavigation();
void updateCurrentOutline();
void pdfHeading(int direction);
QVariantList pdfHeadingCandidates() const;
void indexPdfHeadings();
void findPdf(int direction, bool reset = false);
void searchPdfPage(int page, int cursor, int before, int direction, int remaining, qint64 revision,
QVariantMap candidate = {});
void navigateWeb(const QString &href, const QString &boundary = {}, bool preserveLocation = false);
void rememberJump();
void restoreLocation(const QVariantMap &location);
QVariantMap currentLocation() const;
void savePosition();
void applyMemoryPressure(MemoryPressureLevel level);
ConfigManager config_;
StoragePaths storagePaths_;
InputRouter input_;
RendererWatchdog rendererWatchdog_;
MemoryPressureMonitor memoryPressureMonitor_;
MemoryPressureLevel memoryPressureLevel_ = MemoryPressureLevel::Normal;
Session *active_ = nullptr, *staging_ = nullptr;
QPointer<PdfCanvas> canvas_;
QPointer<QQuickWindow> window_;
QString state_ = "Empty", notice_, context_ = "content", mode_ = "normal", passwordPath_, query_;
QVariantList outline_, pages_, headings_;
int currentOutline_ = -1, webCurrentOutline_ = -1;
qint64 outlineRevision_ = 0;
mutable qint64 cachedNavigationRevision_ = -1;
mutable QString cachedNavigationPath_;
mutable QVariantMap cachedNavigationTargets_;
mutable QSet<int> cachedNavigationRows_;
int chapter_ = 0;
qint64 generation_ = 0;
QVariantMap webLocation_;
QVector<QVariantMap> back_, forward_;
QTimer noticeTimer_, indexTimer_;
int indexPage_ = 0, headingDirection_ = 0;
bool headingsComplete_ = false;
int searchPage_ = -1, searchStart_ = -1;
qint64 searchRevision_ = 0;
bool searchHasText_ = false;
bool composing_ = false;
int passwordInitialPage_ = 0;
StateStore *store_ = nullptr;
QVariantMap restorePending_;
QString lastOpenFailure_; // The last attempt belongs to the app, never the displayed document.
QSet<int> pageRequests_;
};
} // namespace docview
+149
View File
@@ -0,0 +1,149 @@
#include "benchmark.h"
#include "common/sandbox.h"
#include "common/single_instance.h"
#include "controller.h"
#include "pdf_canvas.h"
#include "translations.h"
#include "web/web_profile.h"
#include <QCommandLineParser>
#include <QDir>
#include <QFileInfo>
#include <QGuiApplication>
#include <QImage>
#include <QJsonDocument>
#include <QJsonObject>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QQuickWindow>
#include <QScreen>
#include <QTimer>
#include <QtWebEngineQuick>
using namespace docview;
int main(int argc, char **argv) {
registerDocumentScheme();
QtWebEngineQuick::initialize();
QGuiApplication app(argc, argv);
app.setApplicationName("DocView");
app.setOrganizationName("DocView");
app.setApplicationVersion("0.1.0");
ApplicationTranslations translations;
QCommandLineParser parser;
parser.setApplicationDescription(QObject::tr("Vim風の読み取り専用文書リーダー"));
parser.addHelpOption();
parser.addVersionOption();
parser.addOption({"config", QObject::tr("設定ファイル"), "path"});
parser.addOption({"page", QObject::tr("PDFの初期実ページ番号"), "number"});
parser.addOption({"smoke-output", QObject::tr("試験用: 画面をPNG保存して終了"), "directory"});
parser.addOption({"benchmark-output", QObject::tr("試験用: フレーム提示とRSSをJSONへ記録"), "path"});
parser.addOption({"benchmark-runs", QObject::tr("試験用: 開く回数"), "count", "30"});
parser.addOption({"benchmark-operations", QObject::tr("試験用: 各ページ・見出し・章系列の操作回数"), "count", "100"});
parser.addOption({"benchmark-close-cycles", QObject::tr("試験用: 最小の明示的な開閉回数"), "count", "10"});
parser.addOption({"benchmark-scroll-steps", QObject::tr("試験用: 16ms間隔の継続スクロール入力数"), "count", "240"});
parser.addPositionalArgument("document", QObject::tr("ローカル文書のパス"), "[document]");
parser.process(app);
if (parser.positionalArguments().size() > 1)
parser.showHelp(2);
int initialPage = 0;
if (parser.isSet("page")) {
bool ok = false;
initialPage = parser.value("page").toInt(&ok);
if (!ok || initialPage < 1 || initialPage > 100000 || parser.positionalArguments().isEmpty())
return 2;
}
SingleInstance instance;
auto instanceResult = SingleInstance::StartResult::Primary;
QStringList openPaths;
for (const auto &path : parser.positionalArguments())
openPaths << QFileInfo(path).absoluteFilePath();
if (!parser.isSet("benchmark-output") && !parser.isSet("smoke-output")) {
instanceResult = instance.start(QFileInfo(StoragePaths::forCurrentUser().stateFile).absolutePath(),
openPaths, 750, initialPage);
if (instanceResult == SingleInstance::StartResult::Forwarded)
return 0;
}
qmlRegisterType<PdfCanvas>("DocView", 1, 0, "PdfCanvas");
Controller controller;
controller.initialize(parser.value("config"), instanceResult == SingleInstance::StartResult::ReadOnly);
QQmlApplicationEngine engine;
engine.rootContext()->setContextProperty("reader", &controller);
engine.load(QUrl("qrc:/qml/Main.qml"));
if (engine.rootObjects().isEmpty())
return 1;
auto *window = qobject_cast<QQuickWindow *>(engine.rootObjects().first());
if (const auto *screen = window->screen()) {
const auto available = screen->availableGeometry();
if (available.isValid()) {
window->resize(qMax(window->minimumWidth(), qMin(window->width(), available.width())),
qMax(window->minimumHeight(), qMin(window->height(), available.height())));
window->setPosition(available.topLeft() + QPoint(qMax(0, (available.width() - window->width()) / 2),
qMax(0, (available.height() - window->height()) / 2)));
}
}
controller.attachWindow(window);
QObject::connect(&instance, &SingleInstance::openRequested, &controller, [&](const QStringList &paths) {
if (!paths.isEmpty())
controller.openPath(paths.first());
window->show();
window->raise();
window->requestActivate();
});
QObject::connect(&instance, &SingleInstance::openAtPageRequested, &controller,
[&](const QString &path, int page) {
controller.openPath(path, {}, page);
window->show();
window->raise();
window->requestActivate();
});
if (instanceResult == SingleInstance::StartResult::ReadOnly)
controller.notify(QObject::tr("状態保存を読み取り専用で開始しました"));
if (parser.isSet("benchmark-output")) {
if (parser.positionalArguments().isEmpty())
return 2;
bool runsOk = false, opsOk = false, cyclesOk = false, stepsOk = false;
int runs = parser.value("benchmark-runs").toInt(&runsOk), ops = parser.value("benchmark-operations").toInt(&opsOk);
int cycles = parser.value("benchmark-close-cycles").toInt(&cyclesOk), steps = parser.value("benchmark-scroll-steps").toInt(&stepsOk);
if (!runsOk || !opsOk || !cyclesOk || !stepsOk || runs < 1 || runs > 100 || ops < 0 || ops > 1000 || cycles < 0 || cycles > 100 || steps < 0 || steps > 3000)
return 2;
auto canvas = window->findChild<PdfCanvas *>();
auto monitor = new Benchmark(&controller, window, canvas,
QFileInfo(parser.positionalArguments().first()).absoluteFilePath(),
parser.value("benchmark-output"), runs, ops, cycles, steps, &app);
QTimer::singleShot(0, monitor, [monitor] { monitor->start(); });
} else if (!parser.positionalArguments().isEmpty())
QTimer::singleShot(0, &controller, [&] {
controller.openPath(QFileInfo(parser.positionalArguments().first()).absoluteFilePath(), {},
initialPage);
});
if (parser.isSet("smoke-output")) {
const QString out = parser.value("smoke-output");
QDir().mkpath(out);
QTimer::singleShot(5000, &app, [&, out] {
const bool imageSaved = window->grabWindow().save(out + "/screen.png");
const auto *screen = window->screen();
const QJsonObject runtime{
{"qtVersion", qVersion()}, {"qtPlatform", QGuiApplication::platformName()},
{"requestedQtQuickBackend", qEnvironmentVariable("QT_QUICK_BACKEND")},
{"sceneGraphBackend", QQuickWindow::sceneGraphBackend()},
{"windowWidth", window->width()}, {"windowHeight", window->height()},
{"devicePixelRatio", window->devicePixelRatio()},
{"screenWidth", screen ? screen->size().width() : 0},
{"screenHeight", screen ? screen->size().height() : 0}};
QFile runtimeFile(out + "/runtime.json");
const auto runtimeBytes = QJsonDocument(runtime).toJson();
const bool runtimeSaved = runtimeFile.open(QIODevice::WriteOnly) &&
runtimeFile.write(runtimeBytes) == runtimeBytes.size();
QFile f(out + "/state.txt");
if (f.open(QIODevice::WriteOnly))
f.write((controller.state() + "\n" + controller.format() + "\n" + controller.notice() + "\n")
.toUtf8());
const auto *canvas = window->findChild<PdfCanvas *>();
const bool pdfPresented = controller.format() != "pdf" ||
(canvas && canvas->width() > 0 && canvas->height() > 0 &&
canvas->viewportReady());
const bool ready = (controller.state() == "Ready" && pdfPresented) ||
(controller.state() == "Empty" && parser.positionalArguments().isEmpty());
app.exit(imageSaved && runtimeSaved && ready ? 0 : 3);
});
}
return app.exec();
}
+584
View File
@@ -0,0 +1,584 @@
#include "pdf_canvas.h"
#include <QPainter>
#include <QQuickWindow>
#include <QWheelEvent>
#include <QMouseEvent>
#include <QCborArray>
#include <cmath>
#include <algorithm>
#include <limits>
namespace docview {
namespace {
constexpr int TileSize = 512;
bool finitePoint(const QPointF &p) { return std::isfinite(p.x()) && std::isfinite(p.y()); }
QRectF pdfRect(const QVariant &value) {
const auto a = value.toList();
if (a.size() != 4) return {};
const QPointF lower(a[0].toDouble(), a[1].toDouble()), upper(a[2].toDouble(), a[3].toDouble());
if (!finitePoint(lower) || !finitePoint(upper)) return {};
return QRectF(lower, upper).normalized();
}
}
PdfCanvas::PdfCanvas(QQuickItem *parent) : QQuickPaintedItem(parent) {
setAcceptedMouseButtons(Qt::LeftButton); setAntialiasing(false);
cache_.setMaxCost(256 * 1024); timer_.setSingleShot(true);
connect(&timer_, &QTimer::timeout, this, &PdfCanvas::requestVisible);
connect(this, &QQuickItem::windowChanged, this, [this](QQuickWindow *w) {
disconnect(windowDprConnection_);
if (w) windowDprConnection_ = connect(w, &QQuickWindow::devicePixelRatioChanged, this, [this] {
if (worker_) worker_->discardQueued(QStringLiteral("render"));
++revision_; pending_.clear(); schedule();
});
});
}
void PdfCanvas::setCacheBudget(int mib) { cache_.setMaxCost(qBound(64, mib, 512) * 1024); schedule(); }
void PdfCanvas::setBackgroundColor(const QColor &color) {
if (!color.isValid() || color == backgroundColor_) return;
backgroundColor_ = color; update(); emit backgroundColorChanged();
}
void PdfCanvas::setPrefetchPages(int pages) {
const int next = qBound(0, pages, 3);
if (prefetchPages_ == next) return;
prefetchPages_ = next;
// Only one prefetch request can be active, and no prefetch is queued.
// An in-flight response is checked again before it can consume cache.
schedule();
}
double PdfCanvas::rasterResolutionScale() const {
if (memoryPressureLevel_ < 3) return 1.;
const double dpr = window() ? window()->devicePixelRatio() : 1.;
// Keep the worker's public scale/DPR bounds. Lowering both together keeps
// NoZoom appearances the same logical size even in the reduced raster.
return qBound(.5, std::max(.05 / (scale_ * dpr), .5 / dpr), 1.);
}
void PdfCanvas::setMemoryPressureLevel(int level) {
const int next = qBound(0, level, 4);
if (memoryPressureLevel_ == next) return;
memoryPressureLevel_ = next;
// Queue cancellation is operation-scoped. Retire this render revision so
// active speculative/old-quality work cannot repopulate trimmed caches;
// the still-visible misses are resubmitted before speculative work.
if (worker_) worker_->discardQueued(QStringLiteral("render"));
++revision_; pending_.clear(); prefetchMetadata_.clear(); timer_.stop();
if (next >= 2) trimCache();
schedule();
}
void PdfCanvas::trimCache(bool currentResolutionOnly) {
QSet<QString> current;
if (currentResolutionOnly) for (const auto &tile : visibleTiles()) current.insert(tile.key);
for (auto it = cachedTiles_.begin(); it != cachedTiles_.end();) {
const auto &tile = it.value();
bool keep = cache_.contains(it.key()) && tile.rotation == rotation_ && tile.page >= 0 && tile.page < rects_.size();
if (keep && currentResolutionOnly) keep = current.contains(it.key());
else if (keep) {
const auto &page = rects_[tile.page]; const double ratio = scale_ / tile.scale;
const QRectF target(page.x() - pan_ + tile.x * ratio, page.y() - offset_ + tile.y * ratio,
tile.w * ratio, tile.h * ratio);
keep = target.intersects(boundingRect());
}
if (!keep) { cache_.remove(it.key()); it = cachedTiles_.erase(it); }
else ++it;
}
}
void PdfCanvas::setDocument(WorkerProcess *worker, const QVariantList &items, int count) {
workerFailed_ = false;
worker_ = worker; pages_.fill(Page{}, qBound(0, count, 100000)); rects_.clear();
cache_.clear(); cachedTiles_.clear(); searchResult_.clear(); prefetchMetadata_.clear(); pending_.clear(); links_.clear(); annotations_.clear(); linksPending_.clear(); pendingLocation_.clear();
++document_; ++revision_; offset_ = pan_ = 0; currentPage_ = readingPage_ = rotation_ = 0;
selectedLink_ = selectedLinkPage_ = -1;
updatePages(items); relayout(false);
}
void PdfCanvas::updatePages(const QVariantList &items) {
bool changed = false;
for (const auto &item : items) {
const auto m = item.toMap(); const qint64 index = m.value("pageIndex", -1).toLongLong();
const double width = m.value("width").toDouble(), height = m.value("height").toDouble();
const int rotation = m.value("rotation").toInt(); const QRectF crop = pdfRect(m.value("cropBox"));
if (index < 0 || index >= pages_.size() || !std::isfinite(width) || !std::isfinite(height) || width <= 0 || height <= 0 || width > 100000 || height > 100000 || rotation < 0 || rotation > 270 || rotation % 90) continue;
Page page; page.width = width; page.height = height; page.label = m.value("label").toString().left(4096); page.known = true; page.rotation = rotation;
page.crop = crop.isEmpty() ? QRectF(0, 0, rotation % 180 ? height : width, rotation % 180 ? width : height) : crop;
pages_[int(index)] = page; prefetchMetadata_.remove(int(index)); changed = true;
}
if (!changed) return;
relayout();
if (!pendingLocation_.isEmpty()) {
const int page = pendingLocation_.value("pageIndex", pendingLocation_.value("page", -1)).toInt();
if (page >= 0 && page < pages_.size() && pages_[page].known) { const auto location = pendingLocation_; pendingLocation_.clear(); applyLocation(location); }
}
}
void PdfCanvas::geometryChange(const QRectF &now, const QRectF &before) {
QQuickPaintedItem::geometryChange(now, before); if (now.size() != before.size()) relayout();
}
QPointF PdfCanvas::pageToCanvas(int index, const QPointF &point) const {
if (index < 0 || index >= pages_.size() || index >= rects_.size()) return {};
const auto &page = pages_[index]; const auto &rect = rects_[index];
double x = (point.x() - page.crop.left()) / page.crop.width();
double y = (point.y() - page.crop.top()) / page.crop.height();
QPointF normalized;
switch ((page.rotation + rotation_) % 360) {
case 90: normalized = {y, x}; break;
case 180: normalized = {1 - x, y}; break;
case 270: normalized = {1 - y, 1 - x}; break;
default: normalized = {x, 1 - y}; break;
}
return rect.topLeft() + QPointF(normalized.x() * rect.width() - pan_, normalized.y() * rect.height() - offset_);
}
QPointF PdfCanvas::canvasToPage(int index, const QPointF &point) const {
if (index < 0 || index >= pages_.size() || index >= rects_.size()) return {};
const auto &page = pages_[index]; const auto &rect = rects_[index];
double x = (point.x() + pan_ - rect.left()) / rect.width();
double y = (point.y() + offset_ - rect.top()) / rect.height();
QPointF normalized;
switch ((page.rotation + rotation_) % 360) {
case 90: normalized = {y, x}; break;
case 180: normalized = {1 - x, y}; break;
case 270: normalized = {1 - y, 1 - x}; break;
default: normalized = {x, 1 - y}; break;
}
return page.crop.topLeft() + QPointF(normalized.x() * page.crop.width(), normalized.y() * page.crop.height());
}
QRectF PdfCanvas::annotationRectToCanvas(int page, const QVariantMap &annotation) const {
if (page < 0 || page >= pages_.size() || page >= rects_.size()) return {};
const auto rect = pdfRect(annotation.value("rect")); if (rect.isEmpty()) return {};
const int flags = annotation.value("flags").toInt();
const QPointF anchor = pageToCanvas(page, {rect.left(), rect.bottom()});
const double zoom = flags & 8 ? 1. : scale_;
if (flags & 16) return {anchor, QSizeF(rect.width() * zoom, rect.height() * zoom)};
auto first = pageToCanvas(page, rect.topLeft()), last = pageToCanvas(page, rect.bottomRight());
if (flags & 8) { first = anchor + (first - anchor) / scale_; last = anchor + (last - anchor) / scale_; }
return QRectF(first, last).normalized();
}
double PdfCanvas::navigationY(int page, const QVariantMap &target) const {
if (page < 0 || page >= rects_.size() || !pages_[page].known)
return std::numeric_limits<double>::quiet_NaN();
if (!target.contains("yPt") && !target.contains("y")) return rects_[page].top() - offset_;
const auto &crop = pages_[page].crop;
const double x = target.value("xPt", target.value("x", crop.left())).toDouble();
const double y = target.value("yPt", target.value("y")).toDouble();
if (!std::isfinite(x) || !std::isfinite(y)) return std::numeric_limits<double>::quiet_NaN();
return pageToCanvas(page, {qBound(crop.left(), x, crop.right()), qBound(crop.top(), y, crop.bottom())}).y();
}
void PdfCanvas::relayout(bool preserve) {
const int anchor = qBound(0, currentPage_, qMax(0, int(pages_.size()) - 1));
const bool hadAnchor = preserve && anchor < rects_.size();
const QPointF oldAnchor = hadAnchor ? canvasToPage(anchor, QPointF(width() / 2, height() / 2)) : QPointF();
if (fitMode_ != "percent" && !pages_.isEmpty()) {
const double pageWidth = rotation_ % 180 ? pages_[anchor].height : pages_[anchor].width;
const double pageHeight = rotation_ % 180 ? pages_[anchor].width : pages_[anchor].height;
double fit = (width() - 32) / pageWidth;
if (fitMode_ == "fit-page") fit = std::min(fit, (height() - 32) / pageHeight);
scale_ = qBound(.05, fit, 8.0);
}
rects_.clear(); double y = 16;
for (const auto &page : pages_) {
double w = page.width, h = page.height; if (rotation_ % 180) std::swap(w, h);
w *= scale_; h *= scale_; rects_.append(QRectF(qMax(16., (width() - w) / 2), y, w, h)); y += h + 16;
}
total_ = y;
if (hadAnchor) {
const QPointF next = pageToCanvas(anchor, oldAnchor); offset_ += next.y() - height() / 2; pan_ += next.x() - width() / 2;
}
offset_ = qBound(0., offset_, qMax(0., total_ - height()));
double maxPan = 0; for (const auto &r : rects_) maxPan = qMax(maxPan, r.width() + 32 - width()); pan_ = qBound(0., pan_, maxPan);
if (worker_) worker_->discardQueued(QStringLiteral("render"));
++revision_; pending_.clear(); updateCurrent(); emit layoutChanged(); emit zoomChanged(); schedule();
}
void PdfCanvas::schedule() { update(); if (memoryPressureLevel_ < 4 && !timer_.isActive()) timer_.start(0); }
void PdfCanvas::scroll(double x, double y) {
if (!std::isfinite(x) || !std::isfinite(y)) return;
pendingLocation_.clear();
offset_ = qBound(0., offset_ + y, qMax(0., total_ - height()));
double maxPan = 0; for (const auto &r : rects_) maxPan = qMax(maxPan, r.width() + 32 - width()); pan_ = qBound(0., pan_ + x, maxPan);
updateCurrent(); schedule();
}
void PdfCanvas::goTo(int page, double fraction) {
if (page < 0 || page >= rects_.size() || !std::isfinite(fraction)) return;
pendingLocation_.clear();
offset_ = qBound(0., rects_[page].y() + qBound(0., fraction, 1.) * rects_[page].height() - 16, qMax(0., total_ - height()));
currentPage_ = page; if (!pages_[page].known) emit pageNeeded(page); updateCurrent(); schedule();
}
void PdfCanvas::goToLocation(const QVariantMap &location) {
const qint64 page = location.value("pageIndex", location.value("page", -1)).toLongLong();
if (page < 0 || page >= pages_.size()) { emit renderFailed(tr("E_LOCATION_INVALID: ページ番号が範囲外です")); return; }
pendingLocation_.clear();
if (!pages_[int(page)].known) {
goTo(int(page));
// pageNeeded may synchronously provide the metadata.
if (pages_[int(page)].known) applyLocation(location);
else { pendingLocation_ = location; emit pageNeeded(int(page)); }
return;
}
applyLocation(location);
}
void PdfCanvas::applyLocation(const QVariantMap &location) {
const int page = location.value("pageIndex", location.value("page", -1)).toInt();
if (page < 0 || page >= pages_.size()) return;
if (location.contains("viewRotation") || location.contains("rotation")) {
const int value = location.value("viewRotation", location.value("rotation")).toInt();
if (value >= 0 && value <= 270 && value % 90 == 0) rotation_ = value;
}
if (location.contains("zoom")) {
const double zoom = location.value("zoom").toDouble(); if (std::isfinite(zoom)) scale_ = qBound(.05, zoom / 100., 8.);
}
if (location.contains("fitMode")) {
const auto fit = location.value("fitMode").toString(); if (fit == "fit-width" || fit == "fit-page" || fit == "percent") fitMode_ = fit;
}
currentPage_ = page; relayout(false);
const bool hasY = location.contains("yPt") || location.contains("y");
if (!hasY) { goTo(page, location.value("progression").toDouble()); return; }
const auto &crop = pages_[page].crop;
const QPointF original(location.value("xPt", location.value("x", crop.left())).toDouble(), location.value("yPt", location.value("y", crop.bottom())).toDouble());
if (!finitePoint(original)) { emit renderFailed(tr("E_LOCATION_INVALID: PDFの座標が無効です")); return; }
const QPointF clipped(qBound(crop.left(), original.x(), crop.right()), qBound(crop.top(), original.y(), crop.bottom()));
const QPointF device = pageToCanvas(page, clipped); offset_ += device.y() - 16;
if (location.contains("xPt")) pan_ += device.x() - 16;
else if (device.x() < 16) pan_ += device.x() - 16; else if (device.x() > width() - 16) pan_ += device.x() - width() + 16;
scroll(0, 0);
if (clipped != original) emit renderFailed(tr("保存位置をページの可視範囲内へ近似復元しました"));
}
void PdfCanvas::setZoom(double percent) { if (!std::isfinite(percent)) return; pendingLocation_.clear(); fitMode_ = "percent"; scale_ = qBound(.25, percent / 100, 8.); relayout(); }
void PdfCanvas::fitWidth() { pendingLocation_.clear(); fitMode_ = "fit-width"; relayout(); }
void PdfCanvas::fitPage() { pendingLocation_.clear(); fitMode_ = "fit-page"; relayout(); }
void PdfCanvas::rotate(int degrees) {
if (degrees % 90) return;
pendingLocation_.clear();
auto saved = location();
rotation_ = int((qint64(rotation_) + degrees) % 360 + 360) % 360;
if (saved.isEmpty()) { relayout(false); return; }
saved.insert("rotation", rotation_); saved.insert("viewRotation", rotation_); applyLocation(saved);
}
void PdfCanvas::end() { scroll(0, total_); }
void PdfCanvas::updateCurrent() {
if (rects_.isEmpty()) return;
// Status/page commands use the viewport centre; saved reading positions
// and heading traversal use its leading edge, so short pages are not skipped.
const double anchor = offset_ + 17; int index = int(rects_.size()) - 1;
for (int i = 0; i < rects_.size(); ++i) if (rects_[i].bottom() > anchor) { index = i; break; }
readingPage_ = index;
const double centre = offset_ + height() / 2;
double nearest = std::numeric_limits<double>::infinity();
int centralPage = 0;
for (int i = 0; i < rects_.size(); ++i) {
const double distance = std::max({rects_[i].top() - centre, centre - rects_[i].bottom(), 0.});
if (distance < nearest) { nearest = distance; centralPage = i; }
if (rects_[i].top() > centre) break;
}
const bool changed = centralPage != currentPage_; currentPage_ = centralPage; emit positionChanged();
if (!pages_[readingPage_].known) emit pageNeeded(readingPage_);
if (currentPage_ != readingPage_ && !pages_[currentPage_].known) emit pageNeeded(currentPage_);
if (changed) { selectedLink_ = selectedLinkPage_ = -1; }
}
QVariantMap PdfCanvas::location() const {
if (readingPage_ < 0 || readingPage_ >= rects_.size()) return {};
const auto &page = pages_[readingPage_]; const auto &rect = rects_[readingPage_];
QPointF point = canvasToPage(readingPage_, QPointF(qBound(16., rect.left() - pan_, qMax(16., width() - 16)), 16));
point.setX(qBound(page.crop.left(), point.x(), page.crop.right())); point.setY(qBound(page.crop.top(), point.y(), page.crop.bottom()));
const double fraction = (offset_ + 16 - rect.y()) / rect.height();
return {{"kind", "pdf"}, {"pageIndex", readingPage_}, {"pageLabel", page.label}, {"xPt", point.x()}, {"yPt", point.y()},
{"progression", qBound(0., fraction, 1.)}, {"rotation", rotation_}, {"viewRotation", rotation_}, {"zoom", zoom()}, {"fitMode", fitMode_}};
}
QList<PdfCanvas::Tile> PdfCanvas::visibleTiles() const {
QList<Tile> result; if (!worker_ || width() <= 0 || height() <= 0) return result;
const double dpr = (window() ? window()->devicePixelRatio() : 1.) * rasterResolutionScale();
QRectF viewport(pan_, offset_, width(), height());
for (int page = 0; page < rects_.size(); ++page) {
const auto rect = rects_[page]; if (!rect.intersects(viewport) || !pages_[page].known) continue;
const auto clip = rect.intersected(viewport);
const int firstX = qMax(0, int(std::floor((clip.left() - rect.left()) * dpr / TileSize)) * TileSize);
const int firstY = qMax(0, int(std::floor((clip.top() - rect.top()) * dpr / TileSize)) * TileSize);
const int pixelWidth = int(std::ceil(rect.width() * dpr)), pixelHeight = int(std::ceil(rect.height() * dpr));
for (int y = firstY; y < qMin(pixelHeight, int(std::ceil((clip.bottom() - rect.top()) * dpr))); y += TileSize)
for (int x = firstX; x < qMin(pixelWidth, int(std::ceil((clip.right() - rect.left()) * dpr))); x += TileSize) {
const int w = qMin(TileSize, pixelWidth - x), h = qMin(TileSize, pixelHeight - y);
const QString key = QString("%1:%2:%3:%4:%5:%6:%7:%8:%9").arg(document_).arg(page).arg(scale_ * dpr, 0, 'g', 17).arg(rotation_).arg(x).arg(y).arg(w).arg(h).arg(dpr, 0, 'g', 17);
result.append({key, page, x, y, w, h, rotation_, scale_ * dpr, dpr, QRectF(rect.x() - pan_ + x / dpr, rect.y() - offset_ + y / dpr, w / dpr, h / dpr)});
}
}
return result;
}
void PdfCanvas::paint(QPainter *painter) {
painter->fillRect(boundingRect(), backgroundColor_);
for (int i = 0; i < rects_.size(); ++i) {
const auto rect = rects_[i].translated(-pan_, -offset_); if (!rect.intersects(boundingRect())) continue;
painter->fillRect(rect, Qt::white); painter->setPen(QColor("#69737e"));
painter->drawText(rect.adjusted(16, 16, -16, -16), Qt::AlignTop | Qt::AlignLeft, tr("ページ %1 · 読込中").arg(i + 1));
}
// Cached earlier resolutions remain as provisional pixels during zoom.
// New tiles are composited over them; only current-resolution tiles satisfy
// viewportReady(), so a stretched preview is never counted as completion.
painter->setRenderHint(QPainter::SmoothPixmapTransform, true);
for (auto it = cachedTiles_.cbegin(); it != cachedTiles_.cend(); ++it) {
const auto &tile = it.value();
if (tile.rotation != rotation_ || tile.page < 0 || tile.page >= rects_.size()) continue;
const auto &page = rects_[tile.page]; const double ratio = scale_ / tile.scale;
const QRectF target(page.x() - pan_ + tile.x * ratio, page.y() - offset_ + tile.y * ratio, tile.w * ratio, tile.h * ratio);
if (target.intersects(boundingRect())) if (auto image = cache_.object(tile.key)) painter->drawImage(target, *image);
}
painter->setRenderHint(QPainter::SmoothPixmapTransform, false);
for (const auto &tile : visibleTiles()) if (auto image = cache_.object(tile.key)) painter->drawImage(tile.target, *image, QRectF(0, 0, tile.w, tile.h));
const int searchPage = searchResult_.value("page", -1).toInt();
if (searchPage >= 0 && searchPage < rects_.size()) {
painter->setPen(QPen(QColor("#bb8b00"), 1)); painter->setBrush(QColor(255, 212, 71, 95));
for (const auto &value : searchResult_.value("rects").toList()) {
const auto rect = pdfRect(value); if (rect.isEmpty()) continue;
const QRectF device(pageToCanvas(searchPage, rect.topLeft()), pageToCanvas(searchPage, rect.bottomRight())); painter->drawRect(device.normalized());
}
}
if (const auto selected = selectedTarget(); !selected.isEmpty()) {
const auto device = annotationRectToCanvas(selectedLinkPage_, selected)
.intersected(rects_[selectedLinkPage_].translated(-pan_, -offset_));
painter->setPen(QPen(QColor("#63dec4"), 2)); painter->setBrush(QColor(99, 222, 196, 35));
if (!device.isEmpty()) painter->drawRect(device);
}
}
QList<PdfCanvas::Tile> PdfCanvas::prefetchTiles() const {
QList<Tile> result;
if (!worker_ || memoryPressureLevel_ >= 1 || prefetchPages_ == 0 || width() <= 0 || height() <= 0) return result;
const QRectF viewport(pan_, offset_, width(), height());
int first = -1, last = -1;
for (int page = 0; page < rects_.size(); ++page) if (rects_[page].intersects(viewport)) {
if (first < 0) first = page; last = page;
}
if (first < 0) return result;
const double dpr = window() ? window()->devicePixelRatio() : 1.;
// Each adjacent page contributes at most eight tiles from a single
// viewport-sized strip. A very large page never expands into a full grid.
for (int distance = 1; distance <= prefetchPages_; ++distance) {
for (const int page : {last + distance, first - distance}) {
if (page < 0 || page >= pages_.size() || !pages_[page].known) continue;
const auto &rect = rects_[page];
const double left = qBound(0., pan_ - rect.x(), qMax(0., rect.width() - width()));
const double top = page < first ? qMax(0., rect.height() - height()) : 0.;
const int startX = qMax(0, int(std::floor(left * dpr / TileSize)) * TileSize);
const int startY = qMax(0, int(std::floor(top * dpr / TileSize)) * TileSize);
const int pw = int(std::ceil(rect.width() * dpr)), ph = int(std::ceil(rect.height() * dpr));
const int endX = qMin(pw, int(std::ceil((left + width()) * dpr)));
const int endY = qMin(ph, int(std::ceil((top + height()) * dpr)));
int count = 0;
for (int y = startY; y < endY && count < 8; y += TileSize)
for (int x = startX; x < endX && count < 8; x += TileSize, ++count) {
const int w = qMin(TileSize, pw - x), h = qMin(TileSize, ph - y);
const QString key = QString("%1:%2:%3:%4:%5:%6:%7:%8:%9").arg(document_).arg(page).arg(scale_ * dpr, 0, 'g', 17).arg(rotation_).arg(x).arg(y).arg(w).arg(h).arg(dpr, 0, 'g', 17);
result.append({key, page, x, y, w, h, rotation_, scale_ * dpr, dpr, QRectF(rect.x() - pan_ + x / dpr, rect.y() - offset_ + y / dpr, w / dpr, h / dpr)});
}
}
}
return result;
}
qint64 PdfCanvas::visibleMissingCost() const {
qint64 cost = 0;
for (const auto &tile : visibleTiles()) if (!cache_.contains(tile.key)) cost += (qint64(tile.w) * tile.h * 4 + 1023) / 1024;
return cost;
}
void PdfCanvas::requestVisible() {
if (!worker_ || workerFailed_ || memoryPressureLevel_ >= 4) return;
if (memoryPressureLevel_ >= 2) trimCache(memoryPressureLevel_ >= 3 && viewportReady());
for (auto it = cachedTiles_.begin(); it != cachedTiles_.end();) {
if (!cache_.contains(it.key())) it = cachedTiles_.erase(it); else ++it;
}
const auto visible = visibleTiles();
for (const auto &tile : visible) {
requestLinks(tile.page);
if (cache_.contains(tile.key) || pending_.contains(tile.key)) continue;
if (pending_.size() >= 4) break;
requestTile(tile, false);
}
// Rendering for the viewport and all already queued interactive work win.
// There is at most one low-priority tile in flight, never a prefetch queue.
if (memoryPressureLevel_ >= 1 || prefetchPages_ == 0 || !pending_.isEmpty() || !viewportReady()) return;
if (!worker_->idle()) { if (!timer_.isActive()) timer_.start(60); return; }
for (const auto &tile : prefetchTiles()) {
if (cache_.contains(tile.key)) continue;
const qint64 cost = (qint64(tile.w) * tile.h * 4 + 1023) / 1024;
if (cache_.totalCost() + cost + visibleMissingCost() > cache_.maxCost()) return;
requestTile(tile, true); return;
}
// Initial metadata is paged. Fetch only the adjacent pages for which the
// configured prefetch radius actually asks; the controller coalesces them.
if (!visible.isEmpty()) {
int first = visible.first().page, last = visible.last().page;
for (int distance = 1; distance <= prefetchPages_; ++distance)
for (int page : {last + distance, first - distance}) {
if (page < 0 || page >= pages_.size() || pages_[page].known || prefetchMetadata_.contains(page)) continue;
prefetchMetadata_.insert(page); emit pageNeeded(page); return;
}
}
}
void PdfCanvas::requestTile(const Tile &tile, bool prefetch) {
pending_.insert(tile.key); const auto rev = revision_, doc = document_; QPointer<PdfCanvas> self(this);
QCborMap request{{"page", tile.page}, {"scale", tile.scale}, {"devicePixelRatio", tile.dpr}, {"rotation", rotation_}, {"x", tile.x}, {"y", tile.y}, {"width", tile.w}, {"height", tile.h}, {"renderRevision", rev}};
emit tileRequested(tile.page, prefetch, rev);
if (!self || !worker_ || doc != document_ || rev != revision_) return;
if (prefetch) ++prefetchRenderRequests_; else ++visibleRenderRequests_;
worker_->request("render", request, [self, tile, prefetch, rev, doc](const QCborMap &message) {
if (!self) return;
if (doc != self->document_) { ++self->staleRenderResponses_; return; }
// A font failure poisons this worker's PDFium cache. It remains fatal
// across zoom revisions and speculative tiles, but never documents.
if (message.contains(QStringLiteral("error")) && self->handleWorkerError(message.value("error").toMap())) return;
// A newer revision can have the same tile key; do not remove its pending entry.
if (rev != self->revision_) { ++self->staleRenderResponses_; self->schedule(); return; }
self->pending_.remove(tile.key);
if (message.contains(QStringLiteral("error"))) {
if (!prefetch) emit self->renderFailed(message.value("error").toMap().value("message").toString());
return;
}
const auto result = message.value("result").toMap(); const auto bytes = result.value("data").toByteArray();
self->reportWarnings(result.value("warnings"));
if (!self) return;
const qint64 w = result.value("width").toInteger(), h = result.value("height").toInteger(), stride = result.value("stride").toInteger();
if (w != tile.w || h != tile.h || stride != w * 4 || bytes.size() != stride * h || result.value("page").toInteger(-1) != tile.page ||
result.value("pixelFormat").toString() != "BGRA8888" || result.value("renderRevision").toInteger(-1) != rev ||
result.value("x").toInteger(-1) != tile.x || result.value("y").toInteger(-1) != tile.y) { self->handleWorkerError({{"code", "E_WORKER_CRASH"}, {"message", tr("不正な描画画像")}}); return; }
const qint64 cost = (w * h * 4 + 1023) / 1024;
const auto visibleTiles = self->visibleTiles(); bool visibleNow = false;
for (const auto &visible : visibleTiles) if (visible.key == tile.key) { visibleNow = true; break; }
if (self->memoryPressureLevel_ >= 2 && !visibleNow) { self->schedule(); return; }
if (prefetch && !visibleNow) {
// Never evict anything for speculative pixels. Visible cache and
// any new viewport misses reserve capacity before prefetch does.
const auto candidates = self->prefetchTiles(); bool stillWanted = false;
for (const auto &candidate : candidates) if (candidate.key == tile.key) { stillWanted = true; break; }
if (!stillWanted || self->cache_.totalCost() + cost + self->visibleMissingCost() > self->cache_.maxCost()) {
++self->discardedPrefetchResponses_; self->schedule(); return;
}
}
for (const auto &visible : visibleTiles) self->cache_.object(visible.key); // protect displayed LRU entries
QImage image = QImage(reinterpret_cast<const uchar *>(bytes.constData()), int(w), int(h), int(stride), QImage::Format_ARGB32).copy();
self->cache_.insert(tile.key, new QImage(image), qMax(1, int(cost)));
self->cachedTiles_.insert(tile.key, tile);
if (self->memoryPressureLevel_ >= 2) self->trimCache(self->memoryPressureLevel_ >= 3 && self->viewportReady());
self->schedule(); if (!prefetch || visibleNow) emit self->frameReady();
});
}
bool PdfCanvas::handleWorkerError(const QCborMap &error) {
const auto code = error.value("code").toString();
if (code != "E_FONT_FAILED" && code != "E_FONT_LIMIT" && code != "E_WORKER_TIMEOUT" && code != "E_WORKER_CRASH") return false;
if (!workerFailed_) {
workerFailed_ = true; timer_.stop(); pending_.clear();
if (worker_) worker_->discardQueued();
emit fatalWorkerError(code, error.value("message").toString().left(4096));
}
return true;
}
void PdfCanvas::reportWarnings(const QCborValue &value) {
if (!value.isArray()) return;
QPointer<PdfCanvas> self(this);
const auto warnings = value.toArray();
for (qsizetype i = 0; i < std::min<qsizetype>(32, warnings.size()); ++i) {
if (!warnings[i].isString()) continue;
const auto warning = warnings[i].toString().left(1024);
if (warning.isEmpty() || warning.contains(QChar::Null)) continue;
emit documentWarning(warning);
if (!self) return;
}
}
bool PdfCanvas::viewportReady() const {
if (memoryPressureLevel_ >= 4) return false;
const auto tiles = visibleTiles();
if (tiles.isEmpty()) return false;
for (const auto &tile : tiles) if (!cache_.contains(tile.key)) return false;
// Unknown visible pages still need metadata and are not a completed frame.
const QRectF viewport(pan_, offset_, width(), height());
for (int page = 0; page < rects_.size(); ++page) if (rects_[page].intersects(viewport) && !pages_[page].known) return false;
return true;
}
void PdfCanvas::showSearchResult(const QVariantMap &match) {
searchResult_.clear();
if (match.isEmpty()) { update(); return; }
const qint64 page = match.value("page", -1).toLongLong(); const auto rects = match.value("rects").toList();
if (page < 0 || page >= pages_.size() || rects.isEmpty() || rects.size() > 100) return;
for (const auto &value : rects) if (pdfRect(value).isEmpty()) return;
searchResult_ = match; const auto first = pdfRect(rects.first());
goToLocation({{"page", page}, {"x", first.left()}, {"y", first.bottom()}}); update();
}
void PdfCanvas::requestLinks(int page) {
if (!worker_ || memoryPressureLevel_ >= 4 || links_.contains(page) || linksPending_.contains(page)) return;
linksPending_.insert(page); const auto doc = document_; QPointer<PdfCanvas> self(this);
worker_->request("links", {{"page", page}}, [self, page, doc](const QCborMap &message) {
if (!self || doc != self->document_) return; self->linksPending_.remove(page);
if (message.contains(QStringLiteral("error"))) { self->handleWorkerError(message.value("error").toMap()); return; }
const auto result = message.value("result").toMap();
if (result.value("page").toInteger(-1) != page) return;
QVariantList links, notes;
const auto entries = result.value("links").toArray(); if (entries.size() > 1000) return;
for (const auto &entry : entries) { const auto link = entry.toMap().toVariantMap(); if (!pdfRect(link.value("rect")).isEmpty()) links.append(link); }
const auto annotations = result.value("annotations").toArray(); if (annotations.size() > 1000) return;
for (const auto &entry : annotations) {
const auto map = entry.toMap(); const auto text = map.value("text");
if (!text.isString() || text.toString().isEmpty() || text.toString().size() > 4096 || text.toString().contains(QChar::Null)) continue;
const auto note = map.toVariantMap(); if (!pdfRect(note.value("rect")).isEmpty()) notes.append(note);
}
if (self->links_.size() >= 32) { const int victim = self->links_.constBegin().key(); self->links_.remove(victim); self->annotations_.remove(victim); }
self->links_.insert(page, links); self->annotations_.insert(page, notes);
self->reportWarnings(result.value("warnings"));
});
}
void PdfCanvas::activateTarget(const QVariantMap &target) {
const auto kind = target.value("kind").toString();
if (kind == "internal") {
const qint64 page = target.value("page", -1).toLongLong(); if (page >= 0 && page < pages_.size()) emit internalLinkRequested(target);
} else if (kind == "external") {
const QUrl url(target.value("url").toString(), QUrl::StrictMode);
if (url.isValid() && !url.host().isEmpty() && (url.scheme() == "http" || url.scheme() == "https")) emit externalLinkRequested(url);
} else emit renderFailed(tr("E_LINK_BLOCKED: このリンクの操作は許可されていません"));
}
QVariantMap PdfCanvas::selectedTarget() const {
if (selectedLinkPage_ < 0 || selectedLinkPage_ >= pages_.size() || selectedLink_ < 0) return {};
const auto links = links_.value(selectedLinkPage_);
if (selectedLink_ < links.size()) return links[selectedLink_].toMap();
const auto notes = annotations_.value(selectedLinkPage_);
const int index = selectedLink_ - int(links.size());
return index < notes.size() ? notes[index].toMap() : QVariantMap{};
}
void PdfCanvas::focusLink(int direction) {
if (currentPage_ < 0 || currentPage_ >= pages_.size()) return;
if (!links_.contains(currentPage_)) { requestLinks(currentPage_); emit renderFailed(tr("リンクと注釈を読込中です。もう一度操作してください")); return; }
const int page = currentPage_, linkCount = int(links_.value(page).size());
const int noteCount = int(annotations_.value(page).size()), count = linkCount + noteCount;
if (!count) { emit renderFailed(tr("このページにはリンクや注釈がありません")); return; }
if (selectedLinkPage_ != page) { selectedLink_ = direction < 0 ? 0 : -1; selectedLinkPage_ = page; }
const int index = (selectedLink_ + (direction < 0 ? -1 : 1) + count) % count;
selectedLink_ = index;
const QRectF rect = pdfRect(selectedTarget().value("rect"));
const auto &crop = pages_[page].crop;
// A comment outside CropBox must still be readable from the keyboard.
// Keep navigation on its owning page instead of following its off-page rect.
const auto center = rect.center();
const auto visibleRect = annotationRectToCanvas(page, selectedTarget()).intersected(rects_[page].translated(-pan_, -offset_));
const auto point = visibleRect.isEmpty()
? pageToCanvas(page, {qBound(crop.left(), center.x(), crop.right()), qBound(crop.top(), center.y(), crop.bottom())})
: visibleRect.center();
double dx = 0, dy = 0;
if (point.x() < 16 || point.x() > width() - 16) dx = point.x() - width() / 2;
if (point.y() < 16 || point.y() > height() - 16) dy = point.y() - height() / 2;
const double lower = rects_[page].top() - 16;
const double nextOffset = qBound(lower, offset_ + dy, qMax(lower, rects_[page].bottom() - 17));
scroll(dx, nextOffset - offset_);
selectedLinkPage_ = page; selectedLink_ = index;
if (index >= linkCount) emit renderFailed((rect.intersects(crop)
? tr("注釈 %1 / %2 · Enterで本文を開きます") : tr("ページ外の注釈 %1 / %2 · Enterで本文を開きます"))
.arg(index - linkCount + 1).arg(noteCount));
schedule();
}
void PdfCanvas::activateLink() {
const auto target = selectedTarget();
if (target.isEmpty()) { emit renderFailed(tr("リンクまたは注釈を選択してください")); return; }
if (selectedLink_ < links_.value(selectedLinkPage_).size()) activateTarget(target);
else emit annotationRequested(target.value("text").toString());
}
void PdfCanvas::mouseReleaseEvent(QMouseEvent *event) {
if (event->button() != Qt::LeftButton) return;
for (int page = 0; page < rects_.size(); ++page) {
if (!rects_[page].translated(-pan_, -offset_).contains(event->position())) continue;
for (const auto &value : links_.value(page)) { const auto link = value.toMap(); if (annotationRectToCanvas(page, link).contains(event->position())) { activateTarget(link); event->accept(); return; } }
for (const auto &value : annotations_.value(page)) { const auto note = value.toMap(); if (annotationRectToCanvas(page, note).contains(event->position())) { emit annotationRequested(note.value("text").toString()); event->accept(); return; } }
break;
}
event->accept();
}
void PdfCanvas::wheelEvent(QWheelEvent *event) {
QPoint delta = event->pixelDelta(); if (delta.isNull()) delta = event->angleDelta() / 3;
scroll(-delta.x(), -delta.y()); event->accept();
}
}
+120
View File
@@ -0,0 +1,120 @@
#pragma once
#include <QQuickPaintedItem>
#include <QCache>
#include <QImage>
#include <QPointer>
#include <QSet>
#include <QTimer>
#include <QUrl>
#include "common/worker_process.h"
namespace docview {
class PdfCanvas : public QQuickPaintedItem {
Q_OBJECT
Q_PROPERTY(int currentPage READ currentPage NOTIFY positionChanged)
Q_PROPERTY(double zoom READ zoom NOTIFY zoomChanged)
Q_PROPERTY(double offset READ offset NOTIFY positionChanged)
Q_PROPERTY(double totalHeight READ totalHeight NOTIFY layoutChanged)
Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor NOTIFY backgroundColorChanged)
public:
explicit PdfCanvas(QQuickItem *parent = nullptr);
void paint(QPainter *) override;
void setDocument(WorkerProcess *worker, const QVariantList &pages, int count);
void updatePages(const QVariantList &pages);
void setCacheBudget(int mib);
void setPrefetchPages(int pages);
void setMemoryPressureLevel(int level);
int memoryPressureLevel() const { return memoryPressureLevel_; }
double rasterResolutionScale() const;
void setBackgroundColor(const QColor &color);
QColor backgroundColor() const { return backgroundColor_; }
void cancelPendingNavigation() { pendingLocation_.clear(); }
void clearSelection() { selectedLink_ = selectedLinkPage_ = -1; update(); }
int currentPage() const { return currentPage_; }
double zoom() const { return scale_ * 100; }
double offset() const { return offset_; }
double totalHeight() const { return total_; }
// Cache cost is QCache's KiB-rounded image charge, not process RSS.
qint64 cacheCostBytes() const { return qint64(cache_.totalCost()) * 1024; }
qint64 cacheBudgetBytes() const { return qint64(cache_.maxCost()) * 1024; }
qsizetype cacheEntryCount() const { return cache_.size(); }
// Lifetime counters: sample before/after an operation. Repaint and
// readiness checks do not count as hits or additional render requests.
quint64 visibleRenderRequestCount() const { return visibleRenderRequests_; }
quint64 prefetchRenderRequestCount() const { return prefetchRenderRequests_; }
quint64 staleRenderResponseCount() const { return staleRenderResponses_; }
quint64 discardedPrefetchResponseCount() const { return discardedPrefetchResponses_; }
qsizetype pendingRenderCount() const { return pending_.size(); }
Q_INVOKABLE void scroll(double x, double y);
Q_INVOKABLE void goTo(int page, double fraction = 0);
Q_INVOKABLE void goToLocation(const QVariantMap &location);
Q_INVOKABLE void setZoom(double percent);
Q_INVOKABLE void fitWidth();
Q_INVOKABLE void fitPage();
Q_INVOKABLE void rotate(int degrees);
Q_INVOKABLE void end();
Q_INVOKABLE void focusLink(int direction);
Q_INVOKABLE void activateLink();
QVariantMap location() const;
bool viewportReady() const;
Q_INVOKABLE void showSearchResult(const QVariantMap &match);
// Shared forward/inverse PDF-space transform, also used by link hit tests.
QPointF pageToCanvas(int page, const QPointF &point) const;
QPointF canvasToPage(int page, const QPointF &point) const;
double navigationY(int page, const QVariantMap &target) const;
QRectF annotationRectToCanvas(int page, const QVariantMap &annotation) const;
signals:
void positionChanged(); void zoomChanged(); void layoutChanged();
void renderFailed(QString message); void frameReady(); void pageNeeded(int page);
void fatalWorkerError(QString code, QString message);
void internalLinkRequested(QVariantMap target);
void externalLinkRequested(QUrl url);
void annotationRequested(QString text);
void documentWarning(QString message);
void tileRequested(int page, bool prefetch, qint64 revision);
void backgroundColorChanged();
protected:
void geometryChange(const QRectF &, const QRectF &) override;
void wheelEvent(QWheelEvent *) override;
void mouseReleaseEvent(QMouseEvent *) override;
private:
void relayout(bool preserve = true);
void schedule(); void requestVisible(); void updateCurrent();
void requestLinks(int page); void activateTarget(const QVariantMap &target);
QVariantMap selectedTarget() const;
bool handleWorkerError(const QCborMap &error);
void reportWarnings(const QCborValue &warnings);
void applyLocation(const QVariantMap &location);
struct Page {
double width = 595, height = 842;
QString label;
bool known = false;
QRectF crop{0, 0, 595, 842}; // PDF coordinates: left,bottom,width,height
int rotation = 0;
};
struct Tile { QString key; int page, x, y, w, h, rotation; double scale, dpr; QRectF target; };
QList<Tile> visibleTiles() const;
QList<Tile> prefetchTiles() const;
void requestTile(const Tile &tile, bool prefetch);
qint64 visibleMissingCost() const;
void trimCache(bool currentResolutionOnly = false);
QPointer<WorkerProcess> worker_;
QMetaObject::Connection windowDprConnection_;
QVector<Page> pages_; QVector<QRectF> rects_;
QCache<QString, QImage> cache_; QSet<QString> pending_;
QHash<QString, Tile> cachedTiles_;
QVariantMap searchResult_;
QHash<int, QVariantList> links_, annotations_; QSet<int> linksPending_;
QTimer timer_; double offset_ = 0, pan_ = 0, scale_ = 1, total_ = 0;
int currentPage_ = 0, readingPage_ = 0, rotation_ = 0, selectedLink_ = -1, selectedLinkPage_ = -1;
int prefetchPages_ = 0;
int memoryPressureLevel_ = 0;
bool workerFailed_ = false;
QSet<int> prefetchMetadata_;
QString fitMode_ = QStringLiteral("fit-width");
qint64 revision_ = 0, document_ = 0;
quint64 visibleRenderRequests_ = 0, prefetchRenderRequests_ = 0;
quint64 staleRenderResponses_ = 0, discardedPrefetchResponses_ = 0;
QVariantMap pendingLocation_;
QColor backgroundColor_{QStringLiteral("#181c20")};
};
}
+21
View File
@@ -0,0 +1,21 @@
#include "translations.h"
#include <QCoreApplication>
#include <QLibraryInfo>
namespace docview {
ApplicationTranslations::ApplicationTranslations()
{
const auto directory = QLibraryInfo::path(QLibraryInfo::TranslationsPath);
// Qt deployment tools may merge module catalogs into qt_ja.qm.
if (qtBase_.load(QStringLiteral("qtbase_ja"), directory)
|| qtBase_.load(QStringLiteral("qt_ja"), directory))
QCoreApplication::installTranslator(&qtBase_);
if (qtDeclarative_.load(QStringLiteral("qtdeclarative_ja"), directory))
QCoreApplication::installTranslator(&qtDeclarative_);
if (application_.load(QStringLiteral(":/i18n/docview_ja.qm")))
QCoreApplication::installTranslator(&application_);
}
} // namespace docview
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include <QTranslator>
namespace docview {
// Japanese is the product's initial UI language, independent of the document's
// language or the host's numeric/date locale. Keep translators alive for the app.
class ApplicationTranslations {
public:
ApplicationTranslations();
private:
QTranslator qtBase_;
QTranslator qtDeclarative_;
QTranslator application_;
};
} // namespace docview
+684
View File
@@ -0,0 +1,684 @@
#include "archive.h"
#include <QCoreApplication>
#include <QCryptographicHash>
#include <QDir>
#include <QDomDocument>
#include <QElapsedTimer>
#include <QFile>
#include <QFileInfo>
#include <QPointer>
#include <QRegularExpression>
#include <QSaveFile>
#include <QSet>
#include <QUrl>
#include <QXmlStreamReader>
#include <QXmlStreamWriter>
#include <QtEndian>
#include <zip.h>
#include <algorithm>
#include <limits>
#ifdef Q_OS_UNIX
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#endif
namespace docview {
struct ArchiveInputMeter {
static constexpr quint64 MaximumRead = 64 * 1024;
ArchiveStreamStatistics statistics;
QElapsedTimer timer;
bool active = false;
bool expired = false;
};
namespace {
bool fail(ArchiveError *error, const char *code, const QString &message) {
if (error) *error = {QString::fromLatin1(code), message};
return false;
}
QString mimeFor(const QString &path) {
const QString suffix = QFileInfo(path).suffix().toLower();
static const QHash<QString, QString> types = {
{"html", "text/html"}, {"htm", "text/html"}, {"xhtml", "application/xhtml+xml"},
{"css", "text/css"}, {"svg", "image/svg+xml"}, {"png", "image/png"},
{"jpg", "image/jpeg"}, {"jpeg", "image/jpeg"}, {"gif", "image/gif"},
{"webp", "image/webp"}, {"avif", "image/avif"}, {"woff", "font/woff"},
{"woff2", "font/woff2"}, {"ttf", "font/ttf"}, {"otf", "font/otf"},
{"xml", "application/xml"}, {"opf", "application/oebps-package+xml"},
{"ncx", "application/x-dtbncx+xml"}
};
return types.value(suffix, "application/octet-stream");
}
QString collisionKey(const QString &path) {
return path.normalized(QString::NormalizationForm_C).toCaseFolded().normalized(QString::NormalizationForm_C);
}
quint16 u16(const QByteArray &data, int offset) {
return qFromLittleEndian<quint16>(reinterpret_cast<const uchar *>(data.constData() + offset));
}
quint32 u32(const QByteArray &data, int offset) {
return qFromLittleEndian<quint32>(reinterpret_cast<const uchar *>(data.constData() + offset));
}
quint64 u64(const QByteArray &data, int offset) {
return qFromLittleEndian<quint64>(reinterpret_cast<const uchar *>(data.constData() + offset));
}
// libzip's string API cannot report embedded NUL in the original filename.
// Inspect the bounded central directory before asking libzip to interpret it.
bool checkRawDirectory(QFile &file, const ArchiveLimits &limits, ArchiveError *error) {
const qint64 length = file.size();
if (length < 22) return fail(error, "E_ARCHIVE_CORRUPT", QCoreApplication::translate("Archive", "ZIPの索引が見つかりません。"));
const qint64 tailStart = std::max<qint64>(0, length - 65557);
file.seek(tailStart);
const QByteArray tail = file.read(65557);
int eocd = -1;
for (int i = int(tail.size()) - 22; i >= 0; --i) {
if (u32(tail, i) == 0x06054b50 && i + 22 + u16(tail, i + 20) == tail.size()) {
eocd = i;
break;
}
}
if (eocd < 0 || u16(tail, eocd + 4) || u16(tail, eocd + 6))
return fail(error, "E_ARCHIVE_CORRUPT", QCoreApplication::translate("Archive", "ZIPの索引が不正、または分割アーカイブです。"));
quint64 count = u16(tail, eocd + 10);
quint64 directorySize = u32(tail, eocd + 12);
quint64 offset = u32(tail, eocd + 16);
if (count == 65535 || directorySize == 0xffffffff || offset == 0xffffffff) {
const qint64 locator = tailStart + eocd - 20;
if (locator < 0 || !file.seek(locator)) return fail(error, "E_ARCHIVE_CORRUPT", QCoreApplication::translate("Archive", "ZIP64の索引位置が不正です。"));
const auto loc = file.read(20);
if (loc.size() != 20 || u32(loc, 0) != 0x07064b50 || u32(loc, 4) || u32(loc, 16) != 1)
return fail(error, "E_ARCHIVE_CORRUPT", QCoreApplication::translate("Archive", "ZIP64の索引位置が不正です。"));
const quint64 pos = u64(loc, 8);
if (pos > quint64(length - 56) || !file.seek(qint64(pos))) return fail(error, "E_ARCHIVE_CORRUPT", QCoreApplication::translate("Archive", "ZIP64の索引が不正です。"));
const auto end = file.read(56);
if (end.size() != 56 || u32(end, 0) != 0x06064b50 || u32(end, 16) || u32(end, 20))
return fail(error, "E_ARCHIVE_CORRUPT", QCoreApplication::translate("Archive", "ZIP64の索引が不正です。"));
count = u64(end, 32);
directorySize = u64(end, 40);
offset = u64(end, 48);
}
if (count > limits.maxEntries) return fail(error, "E_ARCHIVE_LIMIT", QCoreApplication::translate("Archive", "アーカイブ内の項目数が上限を超えています。"));
if (offset > quint64(length) || directorySize > quint64(length) - offset || !file.seek(qint64(offset)))
return fail(error, "E_ARCHIVE_CORRUPT", QCoreApplication::translate("Archive", "ZIPの索引がファイルの範囲を超えています。"));
quint64 consumed = 0;
for (quint64 i = 0; i < count; ++i) {
const auto header = file.read(46);
if (header.size() != 46 || u32(header, 0) != 0x02014b50)
return fail(error, "E_ARCHIVE_CORRUPT", QCoreApplication::translate("Archive", "ZIPの索引が途中で切れています。"));
const quint16 nameSize = u16(header, 28), extraSize = u16(header, 30), commentSize = u16(header, 32);
if (!nameSize || nameSize > limits.maxPathBytes + 1)
return fail(error, "E_ARCHIVE_UNSAFE_PATH", QCoreApplication::translate("Archive", "アーカイブ内のパスが長さの上限を超えています。"));
const auto name = file.read(nameSize);
if (name.size() != nameSize || name.contains('\0'))
return fail(error, "E_ARCHIVE_UNSAFE_PATH", QCoreApplication::translate("Archive", "アーカイブ内のパスにNUL文字が含まれています。"));
const auto extra = file.read(extraSize);
if (extra.size() != extraSize) return fail(error, "E_ARCHIVE_CORRUPT", QCoreApplication::translate("Archive", "ZIPの拡張フィールドが途中で切れています。"));
for (qsizetype p = 0; p < extra.size();) {
if (extra.size() - p < 4) return fail(error, "E_ARCHIVE_CORRUPT", QCoreApplication::translate("Archive", "ZIPの拡張フィールドが不正です。"));
const quint16 tag = u16(extra, int(p)), size = u16(extra, int(p + 2));
p += 4;
if (size > extra.size() - p) return fail(error, "E_ARCHIVE_CORRUPT", QCoreApplication::translate("Archive", "ZIPの拡張フィールドが不正です。"));
// Unix and ASi Unix extensions can carry links beyond external attrs.
if ((tag == 0x000d && size > 12) || (tag == 0x756e && size > 14))
return fail(error, "E_ARCHIVE_UNSAFE_PATH", QCoreApplication::translate("Archive", "アーカイブ内のリンクは許可されていません。"));
p += size;
}
consumed += 46ull + nameSize + extraSize + commentSize;
if (consumed > directorySize || !file.seek(file.pos() + commentSize))
return fail(error, "E_ARCHIVE_CORRUPT", QCoreApplication::translate("Archive", "ZIPの索引の長さが不正です。"));
}
return true;
}
struct FileSource {
QPointer<QFile> file;
zip_error_t error;
zip_uint64_t length = 0;
zip_uint64_t offset = 0;
std::shared_ptr<ArchiveInputMeter> meter;
FileSource(QFile *input, std::shared_ptr<ArchiveInputMeter> accounting)
: file(input), length(zip_uint64_t(input->size())), meter(std::move(accounting)) { zip_error_init(&error); }
~FileSource() { zip_error_fini(&error); }
};
zip_int64_t fileSourceCallback(void *context, void *data, zip_uint64_t length, zip_source_cmd_t command) {
auto &source = *static_cast<FileSource *>(context);
const auto invalid = [&source](int code) -> zip_int64_t { zip_error_set(&source.error, code, 0); return -1; };
switch (command) {
case ZIP_SOURCE_OPEN:
if (!source.file || !source.file->isOpen() || !source.file->isReadable()
|| source.file->isWritable() || source.file->isSequential()) return invalid(ZIP_ER_READ);
source.offset = 0;
return 0;
case ZIP_SOURCE_READ: {
if (source.meter->active && source.meter->timer.elapsed() > 30000) {
source.meter->expired = true;
return invalid(ZIP_ER_READ);
}
if (!source.file || !source.file->isOpen() || source.offset > source.length
|| !source.file->seek(qint64(source.offset))) return invalid(ZIP_ER_READ);
const auto amount = std::min<zip_uint64_t>({length, source.length - source.offset,
source.meter->active ? ArchiveInputMeter::MaximumRead : zip_uint64_t(std::numeric_limits<qint64>::max())});
const auto count = source.file->read(static_cast<char *>(data), qint64(amount));
if (count < 0) return invalid(ZIP_ER_READ);
source.offset += zip_uint64_t(count);
if (source.meter->active) {
if (quint64(count) > std::numeric_limits<quint64>::max() - source.meter->statistics.inputBytesRead) return invalid(ZIP_ER_INVAL);
source.meter->statistics.inputBytesRead += quint64(count);
source.meter->statistics.largestInputRead = std::max(source.meter->statistics.largestInputRead, quint64(count));
}
return count;
}
case ZIP_SOURCE_STAT: {
if (length < sizeof(zip_stat_t)) return invalid(ZIP_ER_INVAL);
auto *stat = static_cast<zip_stat_t *>(data);
zip_stat_init(stat); stat->size = source.length; stat->valid = ZIP_STAT_SIZE;
return sizeof(zip_stat_t);
}
case ZIP_SOURCE_SEEK: {
const auto position = zip_source_seek_compute_offset(source.offset, source.length, data, length, &source.error);
if (position < 0) return -1;
source.offset = zip_uint64_t(position);
return 0;
}
case ZIP_SOURCE_TELL: return zip_int64_t(source.offset);
case ZIP_SOURCE_CLOSE: return 0;
case ZIP_SOURCE_ERROR: return zip_error_to_data(&source.error, data, length);
case ZIP_SOURCE_FREE: delete &source; return 0;
case ZIP_SOURCE_SUPPORTS:
return ZIP_SOURCE_SUPPORTS_SEEKABLE | ZIP_SOURCE_MAKE_COMMAND_BITMASK(ZIP_SOURCE_SUPPORTS_REOPEN)
| ZIP_SOURCE_MAKE_COMMAND_BITMASK(ZIP_SOURCE_ACCEPT_EMPTY);
case ZIP_SOURCE_ACCEPT_EMPTY: return 0;
default: return invalid(ZIP_ER_OPNOTSUPP);
}
}
QString tag(const QDomElement &e) { return e.localName().isEmpty() ? e.tagName().section(':', -1) : e.localName(); }
QList<QDomElement> children(const QDomElement &e, const QString &name = {}) {
QList<QDomElement> result;
for (auto n = e.firstChildElement(); !n.isNull(); n = n.nextSiblingElement())
if (name.isEmpty() || tag(n) == name) result.push_back(n);
return result;
}
QList<QDomElement> descendants(const QDomElement &e, const QString &name) {
QList<QDomElement> result;
if (tag(e) == name) result.push_back(e);
for (const auto &c : children(e)) result.append(descendants(c, name));
return result;
}
bool parseXml(const QByteArray &bytes, const ArchiveLimits &limits, QDomDocument *document, ArchiveError *error) {
if (quint64(bytes.size()) > limits.maxXmlBytes) return fail(error, "E_ARCHIVE_LIMIT", QCoreApplication::translate("Archive", "文書構造のXMLがサイズの上限を超えています。"));
QXmlStreamReader reader(bytes);
QByteArray safeXml;
QXmlStreamWriter writer(&safeXml);
int depth = 0, elements = 0;
reader.setEntityExpansionLimit(1);
while (!reader.atEnd()) {
const auto token = reader.readNext();
if (token == QXmlStreamReader::EntityReference)
return fail(error, "E_EPUB_PACKAGE_INVALID", QCoreApplication::translate("Archive", "XMLの実体参照は許可されていません。"));
if (token == QXmlStreamReader::DTD) {
if (!reader.entityDeclarations().isEmpty() || !reader.notationDeclarations().isEmpty())
return fail(error, "E_EPUB_PACKAGE_INVALID", QCoreApplication::translate("Archive", "XMLの実体宣言は許可されていません。"));
// EPUB 2 NCX commonly declares an external DOCTYPE. The stream
// parser has no resolver and never retrieves it; omit the DTD
// before the DOM pass so no external subsets can be consulted.
continue;
}
if (token == QXmlStreamReader::StartElement && (++depth > limits.maxXmlDepth || ++elements > limits.maxXmlElements))
return fail(error, "E_ARCHIVE_LIMIT", QCoreApplication::translate("Archive", "文書構造のXMLが階層数または要素数の上限を超えています。"));
if (token == QXmlStreamReader::EndElement) --depth;
if (token != QXmlStreamReader::Invalid) writer.writeCurrentToken(reader);
}
if (reader.hasError() || writer.hasError() || !document->setContent(safeXml, QDomDocument::ParseOption::UseNamespaceProcessing))
return fail(error, "E_EPUB_PACKAGE_INVALID", QCoreApplication::translate("Archive", "文書構造のXMLが不正です。"));
return true;
}
// Manifest/nav references are URLs, whereas ZIP entry names are never decoded.
QString resolveHref(const QString &base, const QString &reference) {
if (reference.isEmpty() || reference.size() > 4096 || reference.contains('\\')) return {};
static const QRegularExpression badEscapes("%(?:00|2f|5c|25)", QRegularExpression::CaseInsensitiveOption);
if (badEscapes.match(reference).hasMatch()) return {};
const QUrl ref(reference, QUrl::StrictMode);
if (!ref.isValid() || !ref.scheme().isEmpty() || !ref.host().isEmpty() || reference.startsWith('/')) return {};
// Resolve dot segments explicitly: QUrl normalizes traversal above the root.
const auto relativePath = ref.path(QUrl::FullyDecoded);
QStringList parts = base.section('/', 0, -2).split('/', Qt::SkipEmptyParts);
if (!base.contains('/')) parts.clear();
if (relativePath.isEmpty()) parts = base.split('/');
else for (const QString &part : relativePath.split('/')) {
if (part.isEmpty() || part == ".") continue;
if (part == "..") { if (parts.isEmpty()) return {}; parts.removeLast(); }
else parts.append(part);
}
const auto path = parts.join('/');
if (!ArchiveReader::validPath(path, false)) return {};
return path + (ref.hasFragment() ? "#" + ref.fragment(QUrl::FullyDecoded) : QString());
}
QString resourcePath(const QString &href) { return href.section('#', 0, 0); }
QString label(const QDomElement &e) { return e.text().simplified().left(4096); }
bool tokensContain(const QString &tokens, const QString &token) {
return tokens.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts).contains(token);
}
}
QVariantMap ArchiveEntry::toVariant() const {
return {{"path", path}, {"size", QVariant::fromValue(size)}, {"mime", mime}, {"directory", directory}};
}
QVariantMap ArchiveDocument::toVariant() const {
return {{"format", format}, {"title", title}, {"entry", entry}, {"packagePath", packagePath},
{"candidates", candidates}, {"warnings", warnings}, {"renditions", renditions},
{"spine", spine}, {"outline", outline}, {"pageList", pageList}, {"landmarks", landmarks},
{"rtl", rtl}, {"fixed", fixed}, {"spread", spread}};
}
ArchiveReader::ArchiveReader(ArchiveLimits limits) : limits_(limits) {}
ArchiveReader::~ArchiveReader() { if (archive_) zip_discard(archive_); }
ArchiveStreamStatistics ArchiveReader::lastStreamStatistics() const {
return inputMeter_ ? inputMeter_->statistics : ArchiveStreamStatistics{};
}
bool ArchiveReader::validPath(const QString &path, bool directory, const ArchiveLimits &limits) {
QString name = path;
if (directory && name.endsWith('/')) name.chop(1);
if (name.isEmpty() || name.startsWith('/') || name.contains('\\') || name.contains(':') ||
name.contains(QChar::Null) || name.toUtf8().size() > limits.maxPathBytes) return false;
const auto parts = name.split('/');
if (parts.size() > limits.maxDepth) return false;
static const QRegularExpression devices("^(CON|PRN|AUX|NUL|COM[1-9¹²³]|LPT[1-9¹²³])(?:\\.|$)", QRegularExpression::CaseInsensitiveOption);
for (const auto &p : parts) {
if (p.isEmpty() || p == "." || p == ".." || p.endsWith('.') || p.endsWith(' ') || devices.match(p).hasMatch()) return false;
for (const QChar c : p) if (c.unicode() < 32 || c.unicode() == 127 || c == '<' || c == '>' || c == '"' || c == '|' || c == '?' || c == '*') return false;
}
return true;
}
bool ArchiveReader::open(const QString &source, ArchiveError *error) {
auto file = std::make_unique<QFile>(source);
if (!file->open(QIODevice::ReadOnly)) return fail(error, "E_OPEN_FAILED", QCoreApplication::translate("Archive", "アーカイブを読み取れません。"));
if (!openFile(file.get(), error)) return false;
ownedSource_ = std::move(file);
return true;
}
bool ArchiveReader::openFile(QFile *source, ArchiveError *error) {
if (archive_) zip_discard(archive_);
archive_ = nullptr;
ownedSource_.reset();
inputMeter_ = std::make_shared<ArchiveInputMeter>();
entries_.clear(); paths_.clear(); fontKeys_.clear(); extracted_.clear(); totalExtracted_ = 0;
if (!source || !source->isOpen() || !source->isReadable() || source->isWritable() || source->isSequential() || source->size() < 0)
return fail(error, "E_OPEN_FAILED", QCoreApplication::translate("Archive", "アーカイブには位置を指定して読み取れる読取り専用ファイルが必要です。"));
if (!checkRawDirectory(*source, limits_, error)) return false;
zip_error_t zipError; zip_error_init(&zipError);
auto *context = new FileSource(source, inputMeter_);
auto *zipSource = zip_source_function_create(fileSourceCallback, context, &zipError);
if (!zipSource) delete context;
else {
archive_ = zip_open_from_source(zipSource, ZIP_RDONLY | ZIP_CHECKCONS, &zipError);
if (!archive_) zip_source_free(zipSource);
}
zip_error_fini(&zipError);
if (!archive_) return fail(error, "E_ARCHIVE_CORRUPT", QCoreApplication::translate("Archive", "ZIPの索引に不整合があります。"));
const auto count = zip_get_num_entries(archive_, 0);
if (count < 0 || quint64(count) > limits_.maxEntries) return fail(error, "E_ARCHIVE_LIMIT", QCoreApplication::translate("Archive", "アーカイブ内の項目数が上限を超えています。"));
QHash<QString, QString> allNames;
QSet<QString> files, explicitNames;
quint64 total = 0;
for (zip_int64_t i = 0; i < count; ++i) {
zip_stat_t stat;
zip_stat_init(&stat);
if (zip_stat_index(archive_, zip_uint64_t(i), ZIP_FL_ENC_GUESS, &stat) < 0 || !stat.name ||
!(stat.valid & ZIP_STAT_SIZE) || !(stat.valid & ZIP_STAT_COMP_SIZE) || !(stat.valid & ZIP_STAT_ENCRYPTION_METHOD))
return fail(error, "E_ARCHIVE_CORRUPT", QCoreApplication::translate("Archive", "アーカイブ内の項目情報が不正です。"));
const QByteArray utf8(stat.name);
const QString path = QString::fromUtf8(utf8);
const bool directory = path.endsWith('/');
if (path.toUtf8() != utf8 || !validPath(path, directory, limits_))
return fail(error, "E_ARCHIVE_UNSAFE_PATH", QCoreApplication::translate("Archive", "アーカイブ内の項目のパスが安全ではありません。"));
zip_uint8_t system = 0; zip_uint32_t attrs = 0;
if (zip_file_get_external_attributes(archive_, zip_uint64_t(i), 0, &system, &attrs) < 0)
return fail(error, "E_ARCHIVE_CORRUPT", QCoreApplication::translate("Archive", "アーカイブ内の属性を読み取れません。"));
const auto kind = (attrs >> 16) & 0170000;
if ((kind && kind != 0100000 && kind != 0040000) || (kind == 0040000 && !directory) ||
(kind == 0100000 && directory) || ((attrs & 0x10) && !directory) || (attrs & 0x400))
return fail(error, "E_ARCHIVE_UNSAFE_PATH", QCoreApplication::translate("Archive", "アーカイブ内のリンクや特殊ファイルは許可されていません。"));
if (stat.encryption_method != ZIP_EM_NONE)
return fail(error, "E_ARCHIVE_ENCRYPTED", QCoreApplication::translate("Archive", "パスワードで保護されたアーカイブには対応していません。"));
if (stat.size > limits_.maxEntryBytes || total > limits_.maxTotalBytes || stat.size > limits_.maxTotalBytes - total ||
(stat.size > limits_.ratioThreshold && (stat.comp_size == 0 || stat.size / stat.comp_size > limits_.maxRatio ||
(stat.size / stat.comp_size == limits_.maxRatio && stat.size % stat.comp_size != 0))))
return fail(error, "E_ARCHIVE_LIMIT", QCoreApplication::translate("Archive", "アーカイブの展開量が上限を超えています。"));
if (directory && stat.size) return fail(error, "E_ARCHIVE_CORRUPT", QCoreApplication::translate("Archive", "アーカイブ内のディレクトリーにファイルデータがあります。"));
total += stat.size;
const QString clean = directory ? path.left(path.size() - 1) : path;
const auto key = collisionKey(clean);
if (explicitNames.contains(key) || (!directory && allNames.contains(key)))
return fail(error, "E_ARCHIVE_UNSAFE_PATH", QCoreApplication::translate("Archive", "アーカイブ内の項目名が重複しています。"));
explicitNames.insert(key);
const auto parts = clean.split('/');
QString prefix;
for (qsizetype n = 0; n < parts.size(); ++n) {
if (!prefix.isEmpty()) prefix += '/';
prefix += parts[n];
const auto prefixKey = collisionKey(prefix);
if ((allNames.contains(prefixKey) && allNames[prefixKey] != prefix) ||
(n + 1 < parts.size() && files.contains(prefixKey)) || (directory && files.contains(prefixKey)))
return fail(error, "E_ARCHIVE_UNSAFE_PATH", QCoreApplication::translate("Archive", "アーカイブ内のファイル名とディレクトリー名が重複しています。"));
allNames[prefixKey] = prefix;
}
if (!directory) files.insert(key);
paths_[path] = entries_.size();
entries_.append({path, stat.size, stat.comp_size, quint64(i), mimeFor(path), directory});
}
return true;
}
bool ArchiveReader::stream(const ArchiveEntry &entry, quint64 limit,
const std::function<bool(const char *, qsizetype)> &write, ArchiveError *error) {
if (!archive_ || entry.directory) return fail(error, "E_RESOURCE_MISSING", QCoreApplication::translate("Archive", "アーカイブ内の資源を利用できません。"));
if (entry.size > limit) return fail(error, "E_ARCHIVE_LIMIT", QCoreApplication::translate("Archive", "アーカイブ内の資源がサイズの上限を超えています。"));
if (!inputMeter_ || inputMeter_->active) return fail(error, "E_ARCHIVE_CORRUPT", QCoreApplication::translate("Archive", "アーカイブ内の資源は同時に複数展開できません。"));
struct Measurement {
ArchiveInputMeter &meter;
explicit Measurement(ArchiveInputMeter &m) : meter(m) {
meter.statistics = {}; meter.expired = false; meter.timer.start(); meter.active = true;
}
~Measurement() { meter.active = false; }
} measurement(*inputMeter_);
// Start before open: some codecs can read their headers/payload on open.
// Keep all earlier reads when output later crosses the 32 MiB threshold.
zip_file_t *file = zip_fopen_index(archive_, entry.index, 0);
inputMeter_->statistics.inputBytesDuringOpen = inputMeter_->statistics.inputBytesRead;
if (!file) return inputMeter_->expired
? fail(error, "E_WORKER_TIMEOUT", QCoreApplication::translate("Archive", "資源の展開が時間制限を超えました。"))
: fail(error, "E_ARCHIVE_CORRUPT", QCoreApplication::translate("Archive", "アーカイブ内の資源を展開できません。"));
QByteArray buffer(64 * 1024, Qt::Uninitialized);
quint64 actual = 0;
bool ok = true;
for (;;) {
const auto n = zip_fread(file, buffer.data(), zip_uint64_t(buffer.size()));
if (inputMeter_->expired || inputMeter_->timer.elapsed() > 30000) {
ok = fail(error, "E_WORKER_TIMEOUT", QCoreApplication::translate("Archive", "資源の展開が時間制限を超えました。")); break;
}
if (n < 0) { ok = fail(error, "E_ARCHIVE_CORRUPT", QCoreApplication::translate("Archive", "アーカイブ内の資源のCRCまたは展開結果の検証に失敗しました。")); break; }
if (!n) break;
if (quint64(n) > limit - actual || quint64(n) > entry.size - std::min(entry.size, actual)) {
ok = fail(error, "E_ARCHIVE_LIMIT", QCoreApplication::translate("Archive", "展開した資源が宣言されたサイズまたは上限を超えています。")); break;
}
const auto output = actual + quint64(n);
const auto input = inputMeter_->statistics.inputBytesRead;
inputMeter_->statistics.outputBytes = output;
if (output > limits_.ratioThreshold && (!input || output / input > limits_.maxRatio ||
(output / input == limits_.maxRatio && output % input))) {
ok = fail(error, "E_ARCHIVE_LIMIT", QCoreApplication::translate("Archive", "資源の実読取量に対する展開量が倍率の上限を超えています。")); break;
}
const auto key = fontKeys_.value(entry.path);
if (!key.isEmpty() && actual < 1040) {
for (quint64 k = 0; k < quint64(n) && actual + k < 1040; ++k)
buffer[qsizetype(k)] = char(uchar(buffer[qsizetype(k)]) ^ uchar(key[qsizetype((actual + k) % quint64(key.size()))]));
}
if (!write(buffer.constData(), qsizetype(n))) { ok = fail(error, "E_STORAGE_FULL", QCoreApplication::translate("Archive", "資源を一時保存先へ書き込めません。")); break; }
actual += quint64(n);
}
if (zip_fclose(file) < 0 && ok) ok = fail(error, "E_ARCHIVE_CORRUPT", QCoreApplication::translate("Archive", "アーカイブ内の資源の検証に失敗しました。"));
if (ok && actual != entry.size) return fail(error, "E_ARCHIVE_CORRUPT", QCoreApplication::translate("Archive", "展開した資源の長さが一致しません。"));
return ok;
}
bool ArchiveReader::read(const QString &path, quint64 limit, QByteArray *bytes, ArchiveError *error) {
bytes->clear();
const auto found = paths_.constFind(path);
if (found == paths_.cend()) return fail(error, "E_RESOURCE_MISSING", QCoreApplication::translate("Archive", "アーカイブ内の資源が見つかりません。"));
const auto &entry = entries_[*found];
const bool ok = stream(entry, std::min(limit, limits_.maxEntryBytes), [bytes](const char *p, qsizetype n) { bytes->append(p, n); return true; }, error);
if (!ok) bytes->clear();
return ok;
}
bool ArchiveReader::extract(const QString &path, const QString &outputRoot, ArchiveError *error) {
const auto found = paths_.constFind(path);
if (found == paths_.cend() || entries_[*found].directory) return fail(error, "E_RESOURCE_MISSING", QCoreApplication::translate("Archive", "アーカイブ内の資源が見つかりません。"));
if (extracted_.contains(path)) return true;
const auto &entry = entries_[*found];
if (entry.size > limits_.maxTotalBytes - totalExtracted_) return fail(error, "E_ARCHIVE_LIMIT", QCoreApplication::translate("Archive", "文書全体の展開量が上限を超えています。"));
#ifdef Q_OS_UNIX
int directory = ::open(QFile::encodeName(outputRoot).constData(), O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
if (directory < 0) return fail(error, "E_ARCHIVE_UNSAFE_PATH", QCoreApplication::translate("Archive", "一時資源の保存ルートを利用できません。"));
const auto components = path.split('/');
for (qsizetype i = 0; i + 1 < components.size(); ++i) {
const auto name = components[i].toUtf8();
if (::mkdirat(directory, name.constData(), 0700) < 0 && errno != EEXIST) {
::close(directory); return fail(error, "E_STORAGE_FULL", QCoreApplication::translate("Archive", "一時資源のディレクトリーを作成できません。"));
}
const int next = ::openat(directory, name.constData(), O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
::close(directory); directory = next;
if (directory < 0) return fail(error, "E_ARCHIVE_UNSAFE_PATH", QCoreApplication::translate("Archive", "一時資源のディレクトリーが安全ではありません。"));
}
const auto target = components.last().toUtf8();
// The random private session root is never exposed to document code. O_EXCL
// and NOFOLLOW stop pre-existing names, links, and cross-session overwrites.
const int fd = ::openat(directory, target.constData(), O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC | O_NOFOLLOW, 0600);
if (fd < 0) { ::close(directory); return fail(error, "E_ARCHIVE_UNSAFE_PATH", QCoreApplication::translate("Archive", "一時資源の名前が使用済み、またはファイルを作成できません。")); }
const bool ok = stream(entry, limits_.maxEntryBytes, [fd](const char *data, qsizetype size) {
qsizetype offset = 0;
while (offset < size) { const auto n = ::write(fd, data + offset, size_t(size - offset)); if (n < 0 && errno == EINTR) continue; if (n <= 0) return false; offset += n; }
return true;
}, error);
::close(fd);
if (!ok) ::unlinkat(directory, target.constData(), 0);
::close(directory);
if (!ok) return false;
#else
Q_UNUSED(outputRoot);
return fail(error, "E_SANDBOX_UNAVAILABLE", QCoreApplication::translate("Archive", "この環境では安全なアーカイブ展開を利用できません。"));
#endif
totalExtracted_ += entry.size;
extracted_.insert(path, entry.size);
return true;
}
bool ArchiveReader::extractTo(const QString &path, const std::function<bool(const char *, qsizetype)> &sink, ArchiveError *error) {
const auto found = paths_.constFind(path);
if (found == paths_.cend() || entries_[*found].directory) return fail(error, "E_RESOURCE_MISSING", QCoreApplication::translate("Archive", "アーカイブ内の資源が見つかりません。"));
const auto &entry = entries_[*found];
if (entry.size > limits_.maxTotalBytes - totalExtracted_) return fail(error, "E_ARCHIVE_LIMIT", QCoreApplication::translate("Archive", "文書全体の展開量が上限を超えています。"));
if (!stream(entry, limits_.maxEntryBytes, sink, error)) return false;
totalExtracted_ += entry.size;
return true;
}
bool ArchiveReader::describe(bool epubExpected, ArchiveDocument *document, ArchiveError *error) {
*document = {};
if (!archive_) return fail(error, "E_OPEN_FAILED", QCoreApplication::translate("Archive", "アーカイブが開かれていません。"));
QByteArray mimetype;
if (paths_.contains("mimetype") && !read("mimetype", 256, &mimetype, error)) return false;
if (epubExpected || mimetype == "application/epub+zip" || paths_.contains("META-INF/container.xml"))
return describeEpub(document, error);
document->format = "htmlzip";
QStringList filePaths;
for (const auto &entry : entries_) if (!entry.directory) {
filePaths.append(entry.path);
if (entry.mime == "text/html" || entry.mime == "application/xhtml+xml") document->candidates.append(entry.path);
}
if (document->candidates.isEmpty()) return fail(error, "E_HTML_ENTRY_MISSING", QCoreApplication::translate("Archive", "アーカイブ内に入口となるHTMLがありません。"));
if (document->candidates.contains("index.html")) document->entry = "index.html";
else {
const auto top = filePaths.first().section('/', 0, 0);
const bool common = std::all_of(filePaths.cbegin(), filePaths.cend(), [&top](const auto &path) { return path.startsWith(top + '/'); });
if (common && document->candidates.contains(top + "/index.html")) document->entry = top + "/index.html";
}
document->title = document->entry.isEmpty() ? "HTML archive" : QFileInfo(document->entry).fileName();
return true;
}
bool ArchiveReader::describeEpub(ArchiveDocument *document, ArchiveError *error) {
QByteArray bytes;
if (!read("mimetype", 256, &bytes, error) || bytes != "application/epub+zip")
return fail(error, "E_EPUB_PACKAGE_INVALID", QCoreApplication::translate("Archive", "EPUBのメディア型が不明、または不正です。"));
auto xml = [&](const QString &path, QDomDocument *dom) {
return read(path, limits_.maxXmlBytes, &bytes, error) && parseXml(bytes, limits_, dom, error);
};
QDomDocument container;
if (!xml("META-INF/container.xml", &container)) return false;
if (tag(container.documentElement()) != "container") return fail(error, "E_EPUB_PACKAGE_INVALID", QCoreApplication::translate("Archive", "EPUBのコンテナーのルートが不正です。"));
const auto rootfiles = descendants(container.documentElement(), "rootfile");
if (rootfiles.isEmpty()) return fail(error, "E_EPUB_PACKAGE_INVALID", QCoreApplication::translate("Archive", "EPUBのコンテナーにパッケージがありません。"));
if (quint64(rootfiles.size()) > limits_.maxEntries) return fail(error, "E_ARCHIVE_LIMIT", QCoreApplication::translate("Archive", "EPUBの表示版の数が上限を超えています。"));
for (const auto &r : rootfiles) {
const QString p = r.attribute("full-path");
if (!validPath(p, false, limits_)) return fail(error, "E_EPUB_PACKAGE_INVALID", QCoreApplication::translate("Archive", "EPUBのパッケージのパスが不正です。"));
document->renditions.append(p);
}
document->packagePath = document->renditions.first();
QDomDocument package;
if (!xml(document->packagePath, &package)) return false;
const auto root = package.documentElement();
const QString version = root.attribute("version");
if (tag(root) != "package" || (!version.startsWith("2.") && !version.startsWith("3.")))
return fail(error, "E_EPUB_PACKAGE_INVALID", QCoreApplication::translate("Archive", "この版のEPUBパッケージには対応していません。"));
const auto manifests = children(root, "manifest"), spines = children(root, "spine"), metadata = children(root, "metadata");
if (manifests.size() != 1 || spines.size() != 1 || metadata.size() != 1)
return fail(error, "E_EPUB_PACKAGE_INVALID", QCoreApplication::translate("Archive", "EPUBのパッケージ構造に不足があります。"));
struct Item { QString id, href, type, properties, fallback; };
QHash<QString, Item> items;
QString navPath, uniqueIdentifier;
for (const auto &m : children(metadata.first())) {
if (tag(m) == "title" && document->title.isEmpty()) document->title = label(m);
if (tag(m) == "identifier" && m.attribute("id") == root.attribute("unique-identifier")) uniqueIdentifier = m.text();
if (tag(m) == "meta" && m.attribute("property") == "rendition:layout") document->fixed = label(m) == "pre-paginated";
if (tag(m) == "meta" && m.attribute("property") == "rendition:spread") {
const auto value = label(m);
if (QStringList{"auto", "both", "none", "landscape"}.contains(value)) document->spread = value;
else if (value == "portrait") document->spread = "both"; // EPUB 3.0 deprecated value.
}
}
if (document->title.isEmpty()) document->title = "EPUB";
for (const auto &m : children(manifests.first(), "item")) {
Item item{m.attribute("id"), resolveHref(document->packagePath, m.attribute("href")), m.attribute("media-type"), m.attribute("properties"), m.attribute("fallback")};
if (item.id.isEmpty() || item.id.size() > 1024 || items.contains(item.id))
return fail(error, "E_EPUB_PACKAGE_INVALID", QCoreApplication::translate("Archive", "EPUBのmanifestに重複または不正な識別子があります。"));
items.insert(item.id, item);
if (tokensContain(item.properties, "nav") && navPath.isEmpty()) navPath = resourcePath(item.href);
if (!item.href.isEmpty() && paths_.contains(resourcePath(item.href))) {
// EPUB media types, rather than filename extensions, identify
// extensionless resources. Only the static rendering whitelist is
// exported; no arbitrary type is trusted by the resource broker.
static const QSet<QString> supportedTypes{"application/xhtml+xml", "text/html", "text/css", "image/svg+xml", "image/png", "image/jpeg", "image/gif", "image/webp", "image/avif", "font/ttf", "font/otf", "font/woff", "font/woff2"};
QString mime = item.type;
if (mime == "application/vnd.ms-opentype" || mime == "application/font-sfnt") mime = "font/otf";
if (mime == "application/font-woff") mime = "font/woff";
if (supportedTypes.contains(mime)) entries_[paths_[resourcePath(item.href)]].mime = mime;
}
if (m.hasAttribute("media-overlay") || tokensContain(item.properties, "scripted") || item.type.startsWith("audio/") || item.type.startsWith("video/"))
if (!document->warnings.contains("E_EPUB_STATIC_ONLY")) document->warnings.append("E_EPUB_STATIC_ONLY");
}
document->rtl = spines.first().attribute("page-progression-direction") == "rtl";
bool readable = false;
const auto itemrefs = children(spines.first(), "itemref");
if (quint64(itemrefs.size()) > limits_.maxEntries) return fail(error, "E_ARCHIVE_LIMIT", QCoreApplication::translate("Archive", "EPUBの読書順序の項目数が上限を超えています。"));
for (const auto &reference : itemrefs) {
const QString idref = reference.attribute("idref");
if (idref.isEmpty() || idref.size() > 1024) return fail(error, "E_EPUB_PACKAGE_INVALID", QCoreApplication::translate("Archive", "EPUBの読書順序の識別子が不正です。"));
QString id = idref;
QSet<QString> visited;
Item item;
bool supported = false;
while (!id.isEmpty() && !visited.contains(id) && visited.size() < 64 && items.contains(id)) {
visited.insert(id); item = items[id];
if ((item.type == "application/xhtml+xml" || item.type == "image/svg+xml" || item.type == "text/html") &&
!item.href.isEmpty() && paths_.contains(resourcePath(item.href))) { supported = true; break; }
id = item.fallback;
}
bool fixed = document->fixed;
const auto properties = reference.attribute("properties");
if (tokensContain(properties, "rendition:layout-pre-paginated")) fixed = true;
if (tokensContain(properties, "rendition:layout-reflowable")) fixed = false;
const bool linear = reference.attribute("linear") != "no";
QString spread;
if ((tokensContain(properties, "page-spread-left") || tokensContain(properties, "rendition:page-spread-left"))) spread = "left";
if ((tokensContain(properties, "page-spread-right") || tokensContain(properties, "rendition:page-spread-right"))) spread = "right";
if (tokensContain(properties, "rendition:page-spread-center")) spread = "center";
QString renditionSpread = document->spread;
for (const auto &value : QStringList{"auto", "both", "landscape", "none"})
if (tokensContain(properties, "rendition:spread-" + value)) renditionSpread = value;
if (tokensContain(properties, "rendition:spread-portrait")) renditionSpread = "both";
document->spine.append(QVariantMap{{"idref", idref}, {"href", supported ? item.href : QString()}, {"title", idref.left(4096)},
{"linear", linear}, {"layout", fixed ? "fixed" : "reflowable"}, {"spread", spread},
{"renditionSpread", renditionSpread}, {"missing", !supported}});
if (supported) {
readable = true;
if (document->entry.isEmpty() && linear) document->entry = resourcePath(item.href);
} else document->warnings.append("E_EPUB_SPINE_MISSING");
}
if (document->spine.isEmpty() || !readable) return fail(error, "E_EPUB_PACKAGE_INVALID", QCoreApplication::translate("Archive", "EPUBの読書順序に表示できる本文がありません。"));
if (document->entry.isEmpty()) for (const auto &s : document->spine) if (!s.toMap()["missing"].toBool()) { document->entry = resourcePath(s.toMap()["href"].toString()); break; }
if (paths_.contains("META-INF/encryption.xml")) {
QDomDocument encryption;
if (!xml("META-INF/encryption.xml", &encryption)) return false;
for (const auto &encrypted : descendants(encryption.documentElement(), "EncryptedData")) {
const auto methods = descendants(encrypted, "EncryptionMethod"), references = descendants(encrypted, "CipherReference");
if (methods.size() != 1 || methods.first().attribute("Algorithm") != "http://www.idpf.org/2008/embedding" || references.size() != 1)
return fail(error, "E_DRM_UNSUPPORTED", QCoreApplication::translate("Archive", "このEPUBの暗号化方式には対応していません。"));
const QString path = resourcePath(resolveHref("encryption.xml", references.first().attribute("URI")));
bool font = false;
for (const auto &item : items) if (resourcePath(item.href) == path && (item.type.startsWith("font/") || item.type == "application/vnd.ms-opentype" || item.type == "application/font-sfnt" || item.type == "application/font-woff")) font = true;
if (!font || !paths_.contains(path) || uniqueIdentifier.isEmpty()) return fail(error, "E_DRM_UNSUPPORTED", QCoreApplication::translate("Archive", "EPUBの標準的なフォント難読化のみ対応しています。"));
uniqueIdentifier.remove(QRegularExpression("[\\x20\\x09\\x0d\\x0a]"));
fontKeys_[path] = QCryptographicHash::hash(uniqueIdentifier.toUtf8(), QCryptographicHash::Sha1);
}
}
auto outlineNode = [&](const QString &title, const QString &href, int depth, const QString &source) -> QVariant {
const QString resolved = resolveHref(source, href);
const bool valid = !resolved.isEmpty() && paths_.contains(resourcePath(resolved));
return QVariantMap{{"title", title.left(4096)}, {"href", valid ? resolved : QString()}, {"depth", depth}, {"missing", !valid}};
};
if (!navPath.isEmpty()) {
QDomDocument nav;
ArchiveError saved;
if (error) saved = *error;
if (xml(navPath, &nav)) {
for (const auto &navigation : descendants(nav.documentElement(), "nav")) {
QString type = navigation.attributeNS("http://www.idpf.org/2007/ops", "type");
if (type.isEmpty()) type = navigation.attribute("epub:type");
QVariantList *list = tokensContain(type, "toc") ? &document->outline : tokensContain(type, "page-list") ? &document->pageList : tokensContain(type, "landmarks") ? &document->landmarks : nullptr;
if (!list) continue;
std::function<void(QDomElement, int)> traverse = [&](QDomElement element, int depth) {
for (const auto &c : children(element)) {
const auto name = tag(c);
if (name == "a" || (name == "span" && tag(element) == "li")) list->append(outlineNode(label(c), c.attribute("href"), std::max(0, depth - 1), navPath));
else traverse(c, depth + (name == "ol" ? 1 : 0));
}
};
traverse(navigation, 0);
}
} else if (error && error->code == "E_ARCHIVE_LIMIT") return false;
if (document->outline.isEmpty()) document->warnings.append("E_EPUB_NAV_INVALID");
if (error) *error = saved;
}
if (document->outline.isEmpty()) {
QString ncxPath = resourcePath(items.value(spines.first().attribute("toc")).href);
if (ncxPath.isEmpty()) for (const auto &item : items) if (item.type == "application/x-dtbncx+xml") { ncxPath = resourcePath(item.href); break; }
if (!ncxPath.isEmpty()) {
QDomDocument ncx;
ArchiveError saved; if (error) saved = *error;
if (xml(ncxPath, &ncx)) {
std::function<void(QDomElement, int)> traverse = [&](QDomElement e, int depth) {
for (const auto &point : children(e, "navPoint")) {
const auto labels = children(point, "navLabel"), content = children(point, "content");
document->outline.append(outlineNode(labels.isEmpty() ? QString() : label(labels.first()), content.isEmpty() ? QString() : content.first().attribute("src"), depth, ncxPath));
traverse(point, depth + 1);
}
};
for (const auto &map : descendants(ncx.documentElement(), "navMap")) traverse(map, 0);
} else if (error && error->code == "E_ARCHIVE_LIMIT") return false;
if (error) *error = saved;
}
}
if (document->outline.isEmpty()) {
document->warnings.append("E_EPUB_TOC_GENERATED");
for (const auto &s : document->spine) { auto m = s.toMap(); m["depth"] = 0; m["source"] = "spine"; document->outline.append(m); }
}
document->format = "epub";
if (quint64(document->outline.size()) > limits_.maxEntries || quint64(document->pageList.size()) > limits_.maxEntries || quint64(document->landmarks.size()) > limits_.maxEntries)
return fail(error, "E_ARCHIVE_LIMIT", QCoreApplication::translate("Archive", "EPUBのナビゲーション索引が項目数の上限を超えています。"));
document->warnings.removeDuplicates();
return true;
}
}
+100
View File
@@ -0,0 +1,100 @@
#pragma once
#include <QByteArray>
#include <QHash>
#include <QStringList>
#include <QVariant>
#include <functional>
#include <memory>
struct zip;
class QFile;
namespace docview {
struct ArchiveLimits {
quint64 maxEntries = 20000;
quint64 maxEntryBytes = 256ull * 1024 * 1024;
quint64 maxTotalBytes = 2ull * 1024 * 1024 * 1024;
quint64 maxXmlBytes = 16ull * 1024 * 1024;
int maxPathBytes = 1024;
int maxDepth = 64;
int maxXmlDepth = 128;
int maxXmlElements = 200000;
quint64 ratioThreshold = 32ull * 1024 * 1024;
quint64 maxRatio = 200;
};
struct ArchiveError { QString code; QString message; };
// Counts bytes returned by the archive's bounded source, including any reads
// during zip_fopen. This is not a codec-internal consumed-bit counter.
struct ArchiveStreamStatistics {
quint64 inputBytesRead = 0;
quint64 inputBytesDuringOpen = 0;
quint64 largestInputRead = 0;
quint64 outputBytes = 0;
};
struct ArchiveInputMeter;
struct ArchiveEntry {
QString path;
quint64 size = 0;
quint64 compressedSize = 0;
quint64 index = 0;
QString mime;
bool directory = false;
QVariantMap toVariant() const;
};
struct ArchiveDocument {
QString format;
QString title;
QString entry;
QString packagePath;
QString spread = "auto";
QStringList candidates;
QStringList warnings;
QStringList renditions;
QVariantList spine;
QVariantList outline;
QVariantList pageList;
QVariantList landmarks;
bool rtl = false;
bool fixed = false;
QVariantMap toVariant() const;
};
// This parser is linked only into ArchiveWorker and isolated unit tests.
class ArchiveReader {
public:
explicit ArchiveReader(ArchiveLimits limits = {});
~ArchiveReader();
ArchiveReader(const ArchiveReader &) = delete;
ArchiveReader &operator=(const ArchiveReader &) = delete;
bool open(const QString &source, ArchiveError *error);
// Borrows a seekable, read-only QFile. Keep it open for this reader's
// lifetime; neither index validation nor libzip reopens its file name.
bool openFile(QFile *source, ArchiveError *error);
bool describe(bool epubExpected, ArchiveDocument *document, ArchiveError *error);
bool extract(const QString &path, const QString &outputRoot, ArchiveError *error);
// The sink receives provisional bytes. Publish only after this returns
// true: libzip validates the unmodified archive CRC at end of stream.
bool extractTo(const QString &path, const std::function<bool(const char *, qsizetype)> &sink, ArchiveError *error);
bool read(const QString &path, quint64 limit, QByteArray *bytes, ArchiveError *error);
const QList<ArchiveEntry> &entries() const { return entries_; }
ArchiveStreamStatistics lastStreamStatistics() const;
static bool validPath(const QString &path, bool directory, const ArchiveLimits &limits = {});
private:
bool stream(const ArchiveEntry &entry, quint64 limit,
const std::function<bool(const char *, qsizetype)> &write, ArchiveError *error);
bool describeEpub(ArchiveDocument *document, ArchiveError *error);
ArchiveLimits limits_;
zip *archive_ = nullptr;
std::unique_ptr<QFile> ownedSource_;
std::shared_ptr<ArchiveInputMeter> inputMeter_;
QList<ArchiveEntry> entries_;
QHash<QString, qsizetype> paths_;
QHash<QString, QByteArray> fontKeys_;
QHash<QString, quint64> extracted_;
quint64 totalExtracted_ = 0;
};
}
+151
View File
@@ -0,0 +1,151 @@
#include "archive.h"
#include "common/ipc.h"
#include "common/worker_runtime.h"
#include <QCommandLineParser>
#include <QCoreApplication>
#include <QCborArray>
#include <QScopedValueRollback>
#include <QTimer>
using namespace docview;
namespace {
QCborMap replyFor(const QCborMap &request) {
QCborMap reply;
for (const auto *field : {"protocolVersion", "requestId", "sessionId", "generation", "operation"})
reply.insert(QString::fromLatin1(field), request.value(QString::fromLatin1(field)));
return reply;
}
QCborArray entryPage(const ArchiveReader &reader, qsizetype offset, qsizetype count) {
QCborArray page;
for (qsizetype i = offset; i < std::min(reader.entries().size(), offset + count); ++i)
page.append(QCborValue::fromVariant(reader.entries()[i].toVariant()));
return page;
}
QCborArray metadataPage(const QVariantList &items, qsizetype offset, qsizetype count, qsizetype byteBudget) {
QCborArray page;
qsizetype bytes = 8;
for (qsizetype i = offset; i < std::min(items.size(), offset + count); ++i) {
const auto value = QCborValue::fromVariant(items[i]);
const auto size = value.toCbor().size();
if (size > byteBudget - bytes) break;
page.append(value); bytes += size;
}
return page;
}
}
int main(int argc, char **argv) {
QCoreApplication app(argc, argv);
QCommandLineParser parser;
addWorkerOptions(parser);
parser.addOption(QCommandLineOption("output", "Private resource root", "path"));
parser.process(app);
const auto source = parser.value("source");
QString sandboxError;
// The parser has no filesystem write authority. The Main broker receives
// bounded provisional chunks and alone commits verified resources.
auto runtime = startWorkerRuntime(parser, {}, &sandboxError);
if (!runtime) return 5;
IpcChannel channel(runtime->transport.get());
QObject::connect(&channel, &IpcChannel::protocolError, &app, [&app](const QString &) { app.exit(6); });
ArchiveReader reader;
ArchiveDocument document;
bool opened = false;
bool handling = false;
QString activeSession;
qint64 activeGeneration = -1;
QObject::connect(&channel, &IpcChannel::messageReceived, &app, [&](const QCborMap &request) {
if (handling) { app.exit(6); return; }
QScopedValueRollback<bool> guard(handling, true);
auto reply = replyFor(request);
const auto operation = request.value(QStringLiteral("operation")).toString();
const auto payload = request.value(QStringLiteral("payload")).toMap();
ArchiveError error;
QCborMap result;
bool ok = true;
if (opened && (request.value(QStringLiteral("sessionId")).toString() != activeSession || request.value(QStringLiteral("generation")).toInteger() != activeGeneration)) {
ok = false; error = {"E_PROTOCOL_INVALID", QCoreApplication::translate("ArchiveWorker", "アーカイブの要求が別の文書に属しています。")};
} else if (operation == "open" && !opened) {
const bool epub = source.endsWith(".epub", Qt::CaseInsensitive) || payload.value(QStringLiteral("format")).toString() == "epub"
|| payload.value(QStringLiteral("epub")).toBool();
ok = reader.openFile(&runtime->source, &error) && reader.describe(epub, &document, &error);
if (ok) {
opened = true;
activeSession = request.value(QStringLiteral("sessionId")).toString();
activeGeneration = request.value(QStringLiteral("generation")).toInteger();
auto metadata = document.toVariant();
QVariantMap counts;
for (const auto *name : {"spine", "outline", "pageList", "landmarks", "candidates", "renditions"}) {
const QString key = QString::fromLatin1(name);
const auto list = metadata[key].toList();
counts[key] = list.size();
metadata[key] = metadataPage(list, 0, 32, 48 * 1024).toVariantList();
}
metadata["metadataCounts"] = counts;
result = QCborValue::fromVariant(metadata).toMap();
result.insert(QStringLiteral("entries"), entryPage(reader, 0, 64));
result.insert(QStringLiteral("entryCount"), reader.entries().size());
result.insert(QStringLiteral("nextOffset"), std::min<qsizetype>(64, reader.entries().size()));
}
} else if (operation == "entries" && opened) {
const auto offset = payload.value(QStringLiteral("offset")).toInteger(-1);
const auto count = payload.value(QStringLiteral("count")).toInteger(128);
if (offset < 0 || offset > reader.entries().size() || count < 1 || count > 128) {
ok = false; error = {"E_PROTOCOL_INVALID", QCoreApplication::translate("ArchiveWorker", "アーカイブの索引範囲が不正です。")};
} else {
result.insert(QStringLiteral("entries"), entryPage(reader, offset, count));
result.insert(QStringLiteral("nextOffset"), std::min<qsizetype>(offset + count, reader.entries().size()));
result.insert(QStringLiteral("total"), reader.entries().size());
}
} else if (operation == "metadata" && opened) {
const auto kind = payload.value(QStringLiteral("kind")).toString();
const auto offset = payload.value(QStringLiteral("offset")).toInteger(-1);
const auto count = payload.value(QStringLiteral("count")).toInteger(128);
const QStringList allowed{"spine", "outline", "pageList", "landmarks", "candidates", "renditions"};
const auto values = document.toVariant()[kind].toList();
if (!allowed.contains(kind) || offset < 0 || offset > values.size() || count < 1 || count > 128) {
ok = false; error = {"E_PROTOCOL_INVALID", QCoreApplication::translate("ArchiveWorker", "アーカイブの項目情報の範囲が不正です。")};
} else {
result.insert(QStringLiteral("kind"), kind);
const auto page = metadataPage(values, offset, count, 512 * 1024);
result.insert(QStringLiteral("items"), page);
result.insert(QStringLiteral("nextOffset"), offset + page.size());
result.insert(QStringLiteral("total"), values.size());
}
} else if (operation == "extract" && opened) {
const auto path = payload.value(QStringLiteral("path")).toString();
quint64 actual = 0;
bool transportFailed = false;
ok = reader.extractTo(path, [&](const char *data, qsizetype length) {
auto chunk = replyFor(request);
chunk.insert(QStringLiteral("result"), QCborMap{{QStringLiteral("path"), path},
{QStringLiteral("data"), QByteArray(data, length)}, {QStringLiteral("more"), true}});
if (!channel.send(chunk)) { transportFailed = true; return false; }
if (!runtime->flushWrites(128 * 1024)) { transportFailed = true; return false; }
actual += quint64(length);
return true;
}, &error);
if (transportFailed) { app.exit(7); return; }
if (ok) {
result.insert(QStringLiteral("path"), path);
result.insert(QStringLiteral("size"), qint64(actual));
result.insert(QStringLiteral("more"), false);
}
} else if (operation == "ping") {
result.insert(QStringLiteral("ready"), true);
} else if (operation == "close") {
result.insert(QStringLiteral("closed"), true);
} else {
ok = false; error = {"E_PROTOCOL_INVALID", QCoreApplication::translate("ArchiveWorker", "このアーカイブ操作は利用できません。")};
}
if (ok) reply.insert(QStringLiteral("result"), result);
else reply.insert(QStringLiteral("error"), QCborMap{{QStringLiteral("code"), error.code}, {QStringLiteral("message"), error.message}});
if (!channel.send(reply)) app.exit(7);
if (operation == "close") {
if (!runtime->flushWrites()) app.exit(7);
else QTimer::singleShot(0, &app, &QCoreApplication::quit);
}
});
return app.exec();
}
+55
View File
@@ -0,0 +1,55 @@
#include "font_backend.h"
namespace docview::fontbackend {
QString normalizedFamily(QString value) {
value = value.toCaseFolded();
value.remove(' '); value.remove('-'); value.remove('_');
if (value.startsWith('@')) value.remove(0, 1);
return value;
}
QString charsetLanguage(int charset) {
switch (charset) {
case 0: return "en";
case 128: return "ja";
case 129: return "ko";
case 134: return "zh-cn";
case 136: return "zh-tw";
case 161: return "el";
case 163: return "vi";
case 177: return "he";
case 178: return "ar";
case 204: return "ru";
case 222: return "th";
case 238: return "pl";
default: return {};
}
}
QStringList familyCandidates(const QString &requested, int charset, int pitchFamily) {
const auto name = normalizedFamily(requested);
QStringList result;
if (!requested.isEmpty()) result << requested;
// Symbol fonts require their own cmap. Never turn them into generic sans.
if (charset == 2 || name == "symbol" || name == "zapfdingbats") return result;
const bool fixed = (pitchFamily & 3) == 1 || name.startsWith("courier");
const bool serif = (pitchFamily & 0xf0) == 0x10 || name.startsWith("times") ||
name.contains("mincho") || name.contains(QString::fromUtf8("明朝"));
if (charset == 128) {
if (serif) result << "Yu Mincho" << "MS PMincho" << "Noto Serif CJK JP" << "IPAexMincho" << "IPAMincho";
else result << "Yu Gothic" << "Meiryo" << "MS PGothic" << "Noto Sans CJK JP" << "BIZ UDPGothic" << "IPAexGothic";
} else if (charset == 134) {
result << (serif ? "SimSun" : "Microsoft YaHei") << (serif ? "Noto Serif CJK SC" : "Noto Sans CJK SC");
} else if (charset == 136) {
result << (serif ? "MingLiU" : "Microsoft JhengHei") << (serif ? "Noto Serif CJK TC" : "Noto Sans CJK TC");
} else if (charset == 129) {
result << (serif ? "Batang" : "Malgun Gothic") << (serif ? "Noto Serif CJK KR" : "Noto Sans CJK KR");
} else if (fixed) {
result << "Courier New" << "Liberation Mono" << "Nimbus Mono PS" << "DejaVu Sans Mono";
} else if (serif) {
result << "Times New Roman" << "Liberation Serif" << "Nimbus Roman" << "DejaVu Serif";
} else {
result << "Arial" << "Liberation Sans" << "Nimbus Sans" << "DejaVu Sans";
}
result.removeDuplicates();
return result;
}
}
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include <QByteArray>
#include <QStringList>
#include <atomic>
#include <functional>
#include <memory>
namespace docview::fontbackend {
inline constexpr qint64 MaxFontBytes = 64ll * 1024 * 1024;
struct Request {
QByteArray faceLocal;
int weight = 400;
bool italic = false;
int charset = 1;
int pitchFamily = 0;
};
struct Selection {
QString cacheKey;
QByteArray actualFaceLocal;
int charset = 1;
qint64 size = 0;
int faceIndex = 0;
qint64 faceOffset = -1;
bool substituted = false;
// Called on this backend's owning thread, before another select().
// Must deliver only the already selected font; never use worker input as a path.
std::function<QByteArray(QString *)> read;
};
class Backend {
public:
virtual ~Backend() = default;
virtual bool select(const Request &, Selection *, QString *error) = 0;
};
std::unique_ptr<Backend> createBackend(const std::atomic_bool &cancelled);
QString normalizedFamily(QString value);
QStringList familyCandidates(const QString &requested, int charset, int pitchFamily);
QString charsetLanguage(int charset);
}
+149
View File
@@ -0,0 +1,149 @@
#include "font_backend.h"
#include <QFile>
#include <QFileInfo>
#include <QHash>
#include <QSet>
#include <fontconfig/fontconfig.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
namespace docview::fontbackend {
namespace {
struct ConfigDelete { void operator()(FcConfig *p) const { if (p) FcConfigDestroy(p); } };
struct SetDelete { void operator()(FcFontSet *p) const { if (p) FcFontSetDestroy(p); } };
struct PatternDelete { void operator()(FcPattern *p) const { if (p) FcPatternDestroy(p); } };
struct Fd {
explicit Fd(int descriptor) : value(descriptor) {}
Fd(const Fd &) = delete;
int value;
~Fd() { if (value >= 0) ::close(value); }
};
using Pattern = std::unique_ptr<FcPattern, PatternDelete>;
QString string(FcPattern *pattern, const char *key, int index = 0) {
FcChar8 *text = nullptr;
return FcPatternGetString(pattern, key, index, &text) == FcResultMatch ? QString::fromUtf8(reinterpret_cast<char *>(text)) : QString();
}
int number(FcPattern *pattern, const char *key, int fallback = 0) {
int value = fallback; FcPatternGetInteger(pattern, key, 0, &value); return value;
}
bool flag(FcPattern *pattern, const char *key, bool fallback = false) {
FcBool value = fallback; FcPatternGetBool(pattern, key, 0, &value); return value;
}
QString identity(const struct stat &s) {
return QString("%1:%2:%3:%4:%5").arg(quint64(s.st_dev)).arg(quint64(s.st_ino))
.arg(qint64(s.st_size)).arg(qint64(s.st_mtim.tv_sec)).arg(qint64(s.st_mtim.tv_nsec));
}
QString key(FcPattern *pattern) { return string(pattern, FC_FILE) + QChar::Null + QString::number(number(pattern, FC_INDEX)); }
class LinuxBackend final : public Backend {
struct Entry { QString path, identity; FcPattern *pattern; };
const std::atomic_bool &cancelled_;
std::unique_ptr<FcConfig, ConfigDelete> config_;
std::unique_ptr<FcFontSet, SetDelete> fonts_;
QHash<QString, Entry> entries_;
QSet<QString> families_;
bool initialized_ = false;
bool initialize(QString *error) {
if (initialized_) return bool(config_ && fonts_);
initialized_ = true;
config_.reset(FcInitLoadConfigAndFonts());
fonts_.reset(FcFontSetCreate());
if (!config_ || !fonts_) { *error = "index"; return false; }
for (auto setName : {FcSetSystem, FcSetApplication}) {
const auto *set = FcConfigGetFonts(config_.get(), setName);
if (!set) continue;
for (int i = 0; i < set->nfont && entries_.size() < 32768 && !cancelled_; ++i) {
auto *pattern = set->fonts[i];
const auto source = string(pattern, FC_FILE);
const int index = number(pattern, FC_INDEX, -1);
const auto suffix = QFileInfo(source).suffix().toLower();
if (index < 0 || index > 65535 || !flag(pattern, FC_OUTLINE) || !flag(pattern, FC_SCALABLE) ||
flag(pattern, FC_VARIABLE) || flag(pattern, "namedinstance") || flag(pattern, FC_COLOR) ||
!QStringList{"ttf", "otf", "ttc", "otc"}.contains(suffix)) continue;
const auto canonical = QFileInfo(source).canonicalFilePath();
if (canonical.isEmpty()) continue;
struct stat info{};
if (::lstat(QFile::encodeName(canonical).constData(), &info) || !S_ISREG(info.st_mode) || info.st_size <= 0) continue;
const auto id = key(pattern);
if (entries_.contains(id)) continue;
FcPatternReference(pattern);
if (!FcFontSetAdd(fonts_.get(), pattern)) { FcPatternDestroy(pattern); *error = "index"; return false; }
entries_.insert(id, {canonical, identity(info), pattern});
for (int n = 0;; ++n) {
auto family = string(pattern, FC_FAMILY, n);
if (family.isEmpty()) break;
families_.insert(normalizedFamily(family));
}
}
}
return !cancelled_;
}
public:
explicit LinuxBackend(const std::atomic_bool &cancelled) : cancelled_(cancelled) {}
bool select(const Request &request, Selection *selected, QString *error) override {
error->clear();
if (!initialize(error) || cancelled_) return false;
const auto requested = QString::fromLocal8Bit(request.faceLocal);
auto candidates = familyCandidates(requested, request.charset, request.pitchFamily);
Pattern pattern(FcPatternCreate());
if (!pattern) { *error = "allocation"; return false; }
bool named = false;
for (const auto &family : candidates) {
if (!families_.contains(normalizedFamily(family))) continue;
const auto utf8 = family.toUtf8();
if (!FcPatternAddString(pattern.get(), FC_FAMILY, reinterpret_cast<const FcChar8 *>(utf8.constData()))) { *error = "pattern"; return false; }
named = true;
}
const auto normalized = normalizedFamily(requested);
if (!named && (request.charset == 2 || normalized == "symbol" || normalized == "zapfdingbats")) return false;
if (!named) {
const char *generic = (request.pitchFamily & 3) == 1 ? "monospace" : (request.pitchFamily & 0xf0) == 0x10 ? "serif" : "sans-serif";
FcPatternAddString(pattern.get(), FC_FAMILY, reinterpret_cast<const FcChar8 *>(generic));
}
FcPatternAddInteger(pattern.get(), FC_WEIGHT, FcWeightFromOpenType(request.weight ? request.weight : 400));
FcPatternAddInteger(pattern.get(), FC_SLANT, request.italic ? FC_SLANT_ITALIC : FC_SLANT_ROMAN);
if ((request.pitchFamily & 3) == 1) FcPatternAddInteger(pattern.get(), FC_SPACING, FC_MONO);
const auto language = charsetLanguage(request.charset).toUtf8();
if (!language.isEmpty()) FcPatternAddString(pattern.get(), FC_LANG, reinterpret_cast<const FcChar8 *>(language.constData()));
if (!FcConfigSubstitute(config_.get(), pattern.get(), FcMatchPattern)) { *error = "pattern"; return false; }
FcDefaultSubstitute(pattern.get());
FcResult status{}; FcFontSet *set = fonts_.get();
Pattern match(FcFontSetMatch(config_.get(), &set, 1, pattern.get(), &status));
if (!match || cancelled_) return false;
const auto entry = entries_.constFind(key(match.get()));
if (entry == entries_.cend()) { *error = "index"; return false; }
if (!language.isEmpty()) {
FcLangSet *languages = nullptr;
if (FcPatternGetLangSet(entry->pattern, FC_LANG, 0, &languages) != FcResultMatch ||
FcLangSetHasLang(languages, reinterpret_cast<const FcChar8 *>(language.constData())) == FcLangDifferentLang) return false;
}
if (request.charset == 2 && !flag(entry->pattern, FC_SYMBOL)) return false;
auto fd = std::make_shared<Fd>(::open(QFile::encodeName(entry->path).constData(), O_RDONLY | O_CLOEXEC | O_NOFOLLOW));
struct stat info{};
if (fd->value < 0 || ::fstat(fd->value, &info) || !S_ISREG(info.st_mode) || identity(info) != entry->identity) { *error = "changed"; return false; }
if (info.st_size <= 0 || info.st_size > MaxFontBytes) { *error = "limit"; return false; }
const auto family = string(entry->pattern, FC_FAMILY);
selected->actualFaceLocal = family.toLocal8Bit();
if (selected->actualFaceLocal.isEmpty() || selected->actualFaceLocal.size() > 256 || selected->actualFaceLocal.contains('\0')) return false;
selected->cacheKey = entry->identity;
selected->charset = request.charset == 1 ? 0 : request.charset;
selected->size = info.st_size; selected->faceIndex = number(match.get(), FC_INDEX);
selected->substituted = normalizedFamily(family) != normalized;
selected->read = [this, fd, size = selected->size, expected = entry->identity](QString *reason) {
QByteArray result(size, Qt::Uninitialized);
qint64 position = 0;
while (position < size && !cancelled_) {
const auto count = ::pread(fd->value, result.data() + position, size_t(qMin<qint64>(65536, size - position)), position);
if (count <= 0) { *reason = "read"; return QByteArray(); }
position += count;
}
struct stat after{};
if (cancelled_ || ::fstat(fd->value, &after) || identity(after) != expected) { *reason = "changed"; return QByteArray(); }
return result;
};
return true;
}
};
}
std::unique_ptr<Backend> createBackend(const std::atomic_bool &cancelled) { return std::make_unique<LinuxBackend>(cancelled); }
}
+161
View File
@@ -0,0 +1,161 @@
#include "font_backend.h"
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <QHash>
#include <QSet>
#include <cstring>
namespace docview::fontbackend {
namespace {
QString decode(const QByteArray &bytes) {
if (bytes.isEmpty()) return {};
const int size = MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS, bytes.constData(), int(bytes.size()), nullptr, 0);
if (size <= 0 || size > 256) return {};
QString result(size, Qt::Uninitialized);
if (MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS, bytes.constData(), int(bytes.size()),
reinterpret_cast<wchar_t *>(result.data()), size) != size) return {};
return result;
}
QByteArray encode(const QString &text) {
const auto wide = text.toStdWString();
// CP_ACP can be UTF-8 on modern Windows; WC_NO_BEST_FIT_CHARS is invalid
// for that code page. Round-trip validation below rejects lossy conversion.
const int size = WideCharToMultiByte(CP_ACP, 0, wide.data(), int(wide.size()), nullptr, 0, nullptr, nullptr);
if (size <= 0 || size > 256) return {};
QByteArray result(size, Qt::Uninitialized);
if (WideCharToMultiByte(CP_ACP, 0, wide.data(), int(wide.size()), result.data(), size, nullptr, nullptr) != size || decode(result) != text) return {};
return result;
}
struct Font {
HDC dc = nullptr;
HFONT font = nullptr;
HGDIOBJ previous = nullptr;
~Font() {
if (dc && previous && previous != HGDI_ERROR) SelectObject(dc, previous);
if (font) DeleteObject(font);
if (dc) DeleteDC(dc);
}
};
QString indexedFamily(const QString &family) {
auto result = normalizedFamily(family);
// GDI enumerates vertical '@' faces separately. Preserve that distinction
// in the index so enumeration order cannot select one for horizontal text.
if (family.startsWith('@')) result.prepend('@');
return result;
}
class WindowsBackend final : public Backend {
const std::atomic_bool &cancelled_;
QHash<QString, QString> families_;
bool initialized_ = false;
static int CALLBACK enumerate(const LOGFONTW *logical, const TEXTMETRICW *, DWORD type, LPARAM parameter) {
auto *self = reinterpret_cast<WindowsBackend *>(parameter);
if (self->cancelled_ || self->families_.size() >= 32768) return 0;
if (!(type & TRUETYPE_FONTTYPE)) return 1;
int length = 0;
while (length < LF_FACESIZE && logical->lfFaceName[length]) ++length;
const auto family = QString::fromWCharArray(logical->lfFaceName, length);
if (!family.isEmpty()) self->families_.insert(indexedFamily(family), family);
return 1;
}
bool initialize(QString *error) {
if (initialized_) return !families_.isEmpty();
initialized_ = true;
HDC dc = CreateCompatibleDC(nullptr);
if (!dc) { *error = "index"; return false; }
LOGFONTW logical{}; logical.lfCharSet = DEFAULT_CHARSET;
EnumFontFamiliesExW(dc, &logical, &enumerate, reinterpret_cast<LPARAM>(this), 0);
DeleteDC(dc);
if (families_.isEmpty() && !cancelled_) *error = "index";
return !families_.isEmpty() && !cancelled_;
}
bool selectFamily(const Request &request, const QString &requested, const QString &family,
Selection *selected, QString *error) {
auto font = std::make_shared<Font>();
font->dc = CreateCompatibleDC(nullptr);
if (!font->dc) { *error = "dc"; return false; }
LOGFONTW logical{};
logical.lfHeight = -16;
logical.lfWeight = request.weight ? request.weight : 400;
logical.lfItalic = request.italic;
logical.lfCharSet = BYTE(request.charset);
logical.lfPitchAndFamily = BYTE(request.pitchFamily);
logical.lfOutPrecision = OUT_TT_ONLY_PRECIS;
const auto wide = family.toStdWString();
if (wide.size() >= LF_FACESIZE) return false;
std::memcpy(logical.lfFaceName, wide.c_str(), (wide.size() + 1) * sizeof(wchar_t));
font->font = CreateFontIndirectW(&logical);
if (!font->font) { *error = "select"; return false; }
font->previous = SelectObject(font->dc, font->font);
if (!font->previous || font->previous == HGDI_ERROR) { *error = "select"; return false; }
wchar_t actual[256]{}; TEXTMETRICW metrics{};
if (!GetTextFaceW(font->dc, 256, actual) || !GetTextMetricsW(font->dc, &metrics)) { *error = "metrics"; return false; }
const auto actualFamily = QString::fromWCharArray(actual);
if (!families_.contains(indexedFamily(actualFamily))) { *error = "index"; return false; }
// A charset mismatch must not quietly return Latin for an unembedded CJK font.
if (request.charset != DEFAULT_CHARSET && metrics.tmCharSet != request.charset) return false;
// GDI may synthesize a variable-font instance without returning those
// instance coordinates in the SFNT blob. Do not advertise that instance
// as faithfully represented by default bytes.
constexpr DWORD VariationTag = 0x72617666; // little-endian 'fvar'
if (GetFontData(font->dc, VariationTag, 0, nullptr, 0) != GDI_ERROR) return false;
const DWORD plainSize = GetFontData(font->dc, 0, 0, nullptr, 0);
constexpr DWORD CollectionTag = 0x66637474; // GDI's little-endian 'ttcf'.
const DWORD collectionSize = GetFontData(font->dc, CollectionTag, 0, nullptr, 0);
const bool collection = collectionSize != GDI_ERROR && collectionSize != 0;
const DWORD size = collection ? collectionSize : plainSize;
if (plainSize == GDI_ERROR || !plainSize || size == GDI_ERROR || !size || (collection && plainSize > size)) { *error = "data"; return false; }
if (size > MaxFontBytes) { *error = "limit"; return false; }
selected->actualFaceLocal = encode(actualFamily);
if (selected->actualFaceLocal.isEmpty()) return false;
selected->charset = metrics.tmCharSet;
selected->size = size; selected->faceOffset = collection ? size - plainSize : 0;
selected->substituted = indexedFamily(actualFamily) != indexedFamily(requested);
// The selected physical font is pinned by the HFONT through snapshot
// acquisition. No filename, registry path or PDF bytes are parsed here.
selected->cacheKey = QString("win:%1:%2:%3:%4:%5:%6").arg(actualFamily)
.arg(logical.lfWeight).arg(logical.lfItalic).arg(metrics.tmCharSet).arg(size).arg(selected->faceOffset);
selected->read = [this, font, size, table = collection ? CollectionTag : 0u](QString *reason) {
QByteArray bytes(size, Qt::Uninitialized);
DWORD position = 0;
while (position < size && !cancelled_) {
const DWORD count = qMin<DWORD>(65536, size - position);
if (GetFontData(font->dc, table, position, bytes.data() + position, count) != count) { *reason = "read"; return QByteArray(); }
position += count;
}
if (cancelled_) { *reason = "cancelled"; return QByteArray(); }
return bytes;
};
return true;
}
public:
explicit WindowsBackend(const std::atomic_bool &cancelled) : cancelled_(cancelled) {}
bool select(const Request &request, Selection *selected, QString *error) override {
error->clear();
if (!initialize(error) || cancelled_) return false;
const auto requested = decode(request.faceLocal);
if (!request.faceLocal.isEmpty() && requested.isEmpty()) return false;
QSet<QString> tried;
QString failure;
for (const auto &candidate : familyCandidates(requested, request.charset, request.pitchFamily)) {
if (cancelled_) return false;
const auto family = families_.value(indexedFamily(candidate));
if (family.isEmpty() || tried.contains(family)) continue;
tried.insert(family);
Selection next; QString reason;
if (selectFamily(request, requested, family, &next, &reason)) {
*selected = std::move(next); return true;
}
// A present but unsupported variable face must not prevent a later
// static alias from supplying the requested charset. Preserve a
// real OS/quota failure only when no usable candidate remains.
if (!reason.isEmpty() && (failure.isEmpty() || reason == "limit")) failure = reason;
}
*error = failure;
return false;
}
};
}
std::unique_ptr<Backend> createBackend(const std::atomic_bool &cancelled) { return std::make_unique<WindowsBackend>(cancelled); }
}
+227
View File
@@ -0,0 +1,227 @@
#include "font_broker.h"
#include <QCoreApplication>
#include "font_backend.h"
#include "common/font_contract.h"
#include <QCryptographicHash>
#include <QHash>
#include <QMetaObject>
#include <QSet>
#include <QThread>
#include <QUuid>
#include <algorithm>
#include <atomic>
#include <condition_variable>
#include <deque>
#include <mutex>
#include <thread>
namespace docview {
namespace {
QCborMap error(const char *code, const QString &message) {
return {{"error", QCborMap{{"code", QString::fromLatin1(code)}, {"message", message}}}};
}
constexpr qint64 CacheLimit = MaxBrokerFontCacheBytes, TransferLimit = MaxFontTransferBytes;
struct Blob {
QByteArray bytes, hash;
qint64 charge = 0;
~Blob();
};
struct SharedBudget {
struct Entry { QString owner; std::shared_ptr<Blob> blob; quint64 lastUse; };
std::atomic_int threads{0};
std::atomic<qint64> snapshots{0};
std::mutex mutex;
QHash<QString, Entry> cache;
quint64 clock = 0;
std::shared_ptr<Blob> find(const QString &key) {
std::lock_guard lock(mutex);
const auto it = cache.find(key);
if (it == cache.end()) return {};
it->lastUse = ++clock;
return it->blob;
}
void touch(const QString &key) {
std::lock_guard lock(mutex);
const auto it = cache.find(key);
if (it != cache.end()) it->lastUse = ++clock;
}
bool reserve(qint64 bytes, qint64 loweredLimit) {
std::lock_guard lock(mutex);
const qint64 limit = std::min(loweredLimit, CacheLimit);
// Snapshots being read are charged but not published in this cache.
// Allocation can proceed only after enough globally unused entries are
// evicted, including entries created by another active/staging service.
while (bytes > limit - snapshots.load()) {
auto victim = cache.end();
for (auto it = cache.begin(); it != cache.end(); ++it)
if (it->blob.use_count() == 1 && (victim == cache.end() || it->lastUse < victim->lastUse)) victim = it;
if (victim == cache.end()) return false;
cache.erase(victim);
}
snapshots.fetch_add(bytes);
return true;
}
void publish(const QString &owner, const QString &key, const std::shared_ptr<Blob> &blob) {
std::lock_guard lock(mutex);
cache.insert(key, {owner, blob, ++clock});
}
void removeOwner(const QString &owner) {
std::lock_guard lock(mutex);
for (auto it = cache.begin(); it != cache.end();)
if (it->owner == owner) it = cache.erase(it); else ++it;
}
};
// Shared by active, staging, and revoked-but-still-returning services. The
// registry outlives detached OS calls. Each service removes its snapshots on its
// background thread; cached entries own bytes only, never OS objects or paths.
SharedBudget &budget() { static auto *value = new SharedBudget; return *value; }
Blob::~Blob() {
// Release bytes before making their charge available to another reader.
// IPC chunks have separately bounded transport queues.
bytes = {}; hash = {};
if (charge) budget().snapshots.fetch_sub(charge);
}
}
struct FontBroker::State {
struct Task { QString operation; QCborMap payload; std::function<void(QCborMap)> completion; };
struct Issued { std::shared_ptr<Blob> blob; QString cacheKey; };
std::atomic_bool revoked{false};
std::mutex mutex;
std::condition_variable available;
std::deque<Task> tasks;
QObject *receiver = nullptr; // Guarded by mutex through queued event posting.
bool running = true;
std::unique_ptr<fontbackend::Backend> backend;
QHash<QString, Issued> issued;
QSet<QByteArray> selections;
const QString owner = QUuid::createUuid().toString(QUuid::Id128);
qint64 cacheLimit = CacheLimit, transferred = 0;
QCborMap execute(const QString &operation, const QCborMap &payload) {
auto wire = payload; wire.insert(QStringLiteral("fontRequestId"), 1);
QString reason;
if (!validateFontPayload(operation, wire, &reason))
return error("E_FONT_FAILED", QCoreApplication::translate("FontBroker", "フォント要求が無効です。"));
if (operation == "font.close") {
if (!issued.remove(payload.value("fontId").toString()))
return error("E_FONT_FAILED", QCoreApplication::translate("FontBroker", "フォントの参照が無効です。"));
return {{"released", true}};
}
if (operation == "font.read") {
const auto id = payload.value("fontId").toString();
const auto it = issued.constFind(id);
if (it == issued.cend()) return error("E_FONT_FAILED", QCoreApplication::translate("FontBroker", "フォントの参照が無効です。"));
const auto offset = payload.value("offset").toInteger(), length = payload.value("length").toInteger();
const auto &bytes = it->blob->bytes;
if (offset > bytes.size() || length > bytes.size() - offset)
return error("E_FONT_FAILED", QCoreApplication::translate("FontBroker", "フォントの読取り範囲が無効です。"));
if (length > TransferLimit - transferred)
return error("E_FONT_LIMIT", QCoreApplication::translate("FontBroker", "フォント転送量が上限を超えました。"));
transferred += length; budget().touch(it->cacheKey);
return {{"fontId", id}, {"offset", offset}, {"data", bytes.mid(offset, length)}};
}
if (issued.size() >= MaxFontHandles)
return error("E_FONT_LIMIT", QCoreApplication::translate("FontBroker", "フォント参照数が上限を超えました。"));
const auto fingerprint = QCborValue(payload).toCbor();
if (!selections.contains(fingerprint)) {
if (selections.size() >= MaxFontSelections)
return error("E_FONT_LIMIT", QCoreApplication::translate("FontBroker", "フォント選択数が上限を超えました。"));
selections.insert(fingerprint); // Negative matches also consume a distinct selection.
}
if (!backend) backend = fontbackend::createBackend(revoked);
if (!backend) return error("E_FONT_FAILED", QCoreApplication::translate("FontBroker", "OSフォント索引を利用できません。"));
fontbackend::Request request{payload.value("faceLocal").toByteArray(), int(payload.value("weight").toInteger()),
payload.value("italic").toBool(), int(payload.value("charset").toInteger()), int(payload.value("pitchFamily").toInteger())};
fontbackend::Selection selected;
if (!backend->select(request, &selected, &reason)) {
if (!reason.isEmpty()) return error(reason == "limit" ? "E_FONT_LIMIT" : "E_FONT_FAILED", QCoreApplication::translate("FontBroker", "OSフォントを取得できません。"));
return {{"found", false}};
}
if (revoked) return {};
if (selected.size <= 0 || selected.size > fontbackend::MaxFontBytes)
return error("E_FONT_LIMIT", QCoreApplication::translate("FontBroker", "フォントのサイズが上限を超えました。"));
// Cache ownership is session-specific, while eviction is global. This
// also keeps independent OS-index snapshots from aliasing each other.
const QString cacheKey = owner + ':' + selected.cacheKey;
auto blob = budget().find(cacheKey);
if (!blob) {
if (!selected.read) return error("E_FONT_FAILED", QCoreApplication::translate("FontBroker", "フォントを読み取れません。"));
blob = std::make_shared<Blob>();
if (!budget().reserve(selected.size, cacheLimit))
return error("E_FONT_LIMIT", QCoreApplication::translate("FontBroker", "フォントキャッシュが上限に達しました。"));
// Charge before reading so two OS threads cannot both allocate past
// the process-wide snapshot limit. RAII releases on every failure.
blob->charge = selected.size;
auto bytes = selected.read(&reason);
if (revoked) return {};
if (bytes.size() != selected.size) return error("E_FONT_FAILED", QCoreApplication::translate("FontBroker", "フォントの読取りが完了しませんでした。"));
blob->bytes = std::move(bytes);
blob->hash = QCryptographicHash::hash(blob->bytes, QCryptographicHash::Sha256);
budget().publish(owner, cacheKey, blob);
}
const auto id = QUuid::createUuid().toString(QUuid::Id128);
QCborMap result{{"found", true}, {"fontId", id}, {"actualFaceLocal", selected.actualFaceLocal},
{"charset", selected.charset}, {"size", blob->bytes.size()}, {"sha256", blob->hash}, {"substituted", selected.substituted}};
if (selected.faceOffset >= 0) result.insert(QStringLiteral("faceOffset"), selected.faceOffset);
else result.insert(QStringLiteral("faceIndex"), selected.faceIndex);
auto wireResult = result; wireResult.insert(QStringLiteral("fontRequestId"), 1);
if (!validateFontResult(operation, wire, wireResult, &reason))
return error("E_FONT_FAILED", QCoreApplication::translate("FontBroker", "OSフォント情報が無効です。"));
issued.insert(id, {std::move(blob), cacheKey});
return result;
}
static void run(std::shared_ptr<State> state) {
for (;;) {
Task task;
{
std::unique_lock lock(state->mutex);
state->available.wait(lock, [&] { return state->revoked || !state->tasks.empty(); });
if (state->revoked) break;
task = std::move(state->tasks.front()); state->tasks.pop_front();
}
QCborMap result;
try { result = state->execute(task.operation, task.payload); }
catch (...) { result = error("E_FONT_FAILED", QCoreApplication::translate("FontBroker", "OSフォント処理に失敗しました。")); }
std::lock_guard lock(state->mutex);
if (!state->revoked && state->receiver && task.completion) {
QMetaObject::invokeMethod(state->receiver, [state, completion = std::move(task.completion), result = std::move(result)]() mutable {
if (!state->revoked) completion(std::move(result));
}, Qt::QueuedConnection);
}
}
// These objects are destroyed on their owning background thread even
// if the GUI facade was destroyed while an OS call was still running.
state->issued.clear(); budget().removeOwner(state->owner); state->backend.reset();
budget().threads.fetch_sub(1);
}
};
FontBroker::FontBroker(QObject *parent) : FontBroker(parent, CacheLimit) {}
FontBroker::FontBroker(QObject *parent, qint64 snapshotCacheLimit) : QObject(parent), state_(std::make_shared<State>()) {
state_->cacheLimit = std::clamp<qint64>(snapshotCacheLimit, 1, CacheLimit);
state_->receiver = this;
int count = budget().threads.load();
while (count < 2 && !budget().threads.compare_exchange_weak(count, count + 1)) {}
if (count >= 2) { state_->running = false; return; }
try { std::thread(&State::run, state_).detach(); }
catch (...) { state_->running = false; budget().threads.fetch_sub(1); }
}
FontBroker::~FontBroker() { revoke(); }
void FontBroker::revoke() {
std::lock_guard lock(state_->mutex);
state_->revoked = true; state_->receiver = nullptr; state_->tasks.clear();
state_->available.notify_one();
}
void FontBroker::request(const QString &operation, const QCborMap &payload, std::function<void(QCborMap)> completion) {
Q_ASSERT(QThread::currentThread() == thread());
std::lock_guard lock(state_->mutex);
if (state_->revoked) return;
if (!state_->running || state_->tasks.size() >= 8) {
QMetaObject::invokeMethod(this, [state = state_, completion = std::move(completion)] {
if (!state->revoked && completion) completion(error("E_FONT_LIMIT", QCoreApplication::translate("FontBroker", "フォント処理の待機数が上限を超えました。")));
}, Qt::QueuedConnection);
return;
}
state_->tasks.push_back({operation, payload, std::move(completion)});
state_->available.notify_one();
}
}
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <QCborMap>
#include <QObject>
#include <functional>
#include <memory>
namespace docview {
// One facade per PDF worker/session. This class has no PDFium dependency.
// Calls and completion callbacks belong to the facade's QObject thread.
class FontBroker final : public QObject {
public:
explicit FontBroker(QObject *parent = nullptr);
// Lowers the admission limit against the shared cache; cannot exceed the
// production 256 MiB cap. Useful for constrained deployments and tests.
FontBroker(QObject *parent, qint64 snapshotCacheLimit);
~FontBroker() override;
void request(const QString &operation, const QCborMap &payload,
std::function<void(QCborMap)> completion);
// Permanently invalidates this service and drops queued/late completions.
// Does not wait for an in-progress OS font API or file read.
void revoke();
private:
struct State;
std::shared_ptr<State> state_;
};
}
+151
View File
@@ -0,0 +1,151 @@
#include "contracts.h"
#include <QCoreApplication>
#include <QJsonDocument>
#include <QJsonObject>
#include <QRegularExpression>
#include <QSet>
#include <cmath>
namespace docview {
bool validateCapabilities(const QVariant &capabilities, const QVariant &reasons, QString *error) {
auto bad = [&] {
if (error)
*error = QCoreApplication::translate("Contracts", "E_WORKER_CRASH: 文書の能力情報が不正です");
return false;
};
if (capabilities.metaType().id() != QMetaType::QVariantMap ||
reasons.metaType().id() != QMetaType::QVariantMap)
return bad();
static const QSet<QString> names = {"pageNavigation", "chapterNavigation", "outline", "headings",
"textSearch", "textSelection", "reflow", "password"};
static const QSet<QString> states = {"supported", "unavailable", "loading", "unsupported"};
static const QRegularExpression code(QStringLiteral("\\AE_[A-Z0-9_]{1,126}\\z"));
const auto declared = capabilities.toMap();
const auto declaredReasons = reasons.toMap();
if (declared.size() != names.size() || declaredReasons.size() > names.size())
return bad();
for (auto it = declaredReasons.begin(); it != declaredReasons.end(); ++it) {
if (!names.contains(it.key()) || it.value().metaType().id() != QMetaType::QString ||
!code.match(it.value().toString()).hasMatch())
return bad();
}
for (auto it = declared.begin(); it != declared.end(); ++it) {
if (!names.contains(it.key()) || it.value().metaType().id() != QMetaType::QString ||
!states.contains(it.value().toString()) ||
(it.value().toString() == "unsupported" && !declaredReasons.contains(it.key())))
return bad();
}
if (error)
error->clear();
return true;
}
bool sanitizeNavigation(const QVariantList &input, QVariantList *out, qint64 *used, QString *error) {
auto bad = [&] {
if (error)
*error = QCoreApplication::translate("Contracts", "E_WORKER_CRASH: ナビゲーションの応答が不正です");
return false;
};
if (input.size() > 20000 || !out || !used)
return bad();
QVariantList clean;
qint64 bytes = *used;
const QHash<QString, int> strings = {{"parentId", 128}, {"title", 4096}, {"href", 2048}, {"id", 128},
{"idref", 1024}, {"source", 32}, {"precision", 32}, {"kind", 32},
{"layout", 32}, {"spread", 64}, {"reason", 128}, {"renditionSpread", 32}};
const QSet<QString> numbers = {"page", "pageIndex", "index", "level", "depth"};
const QSet<QString> real = {"x", "y", "xPt", "yPt"};
const QSet<QString> booleans = {"missing", "linear"};
for (const auto &v : input) {
if (v.metaType().id() != QMetaType::QVariantMap)
return bad();
const auto row = v.toMap();
QVariantMap next;
for (auto it = row.begin(); it != row.end(); ++it) {
const auto key = it.key();
if (strings.contains(key)) {
if (it.value().metaType().id() != QMetaType::QString ||
it.value().toString().size() > strings[key])
return bad();
next[key] = it.value();
} else if (numbers.contains(key)) {
bool ok = false;
const auto n = it.value().toLongLong(&ok);
if (!ok || it.value().toDouble() != double(n) || n < 0 ||
n > (key == "depth" ? 128
: key == "level" ? 6
: 100000))
return bad();
next[key] = n;
} else if (real.contains(key)) {
bool ok = false;
double n = it.value().toDouble(&ok);
if (!ok || !std::isfinite(n) || std::abs(n) > 1e7)
return bad();
next[key] = n;
} else if (booleans.contains(key)) {
if (it.value().metaType().id() != QMetaType::Bool)
return bad();
next[key] = it.value();
} else if (key == "rect") {
const auto rect = it.value().toList();
if (rect.size() != 4)
return bad();
for (const auto &p : rect) {
bool ok = false;
double n = p.toDouble(&ok);
if (!ok || !std::isfinite(n) || std::abs(n) > 1e7)
return bad();
}
next[key] = rect;
}
}
if (next.isEmpty())
return bad();
bytes += QJsonDocument(QJsonObject::fromVariantMap(next)).toJson(QJsonDocument::Compact).size();
if (bytes > MaxNavigationBytes) {
if (error)
*error = "E_NAVIGATION_LIMIT";
return false;
}
clean << next;
}
*out = clean;
*used = bytes;
return true;
}
bool validatePageMetadata(const QVariantList &pages, int count, int first) {
if (count < 1 || count > 100000 || pages.isEmpty() || pages.size() > 256)
return false;
QSet<int> seen;
int expected = first;
for (const auto &v : pages) {
const auto p = v.toMap();
bool ok = false;
const auto i = p.value("pageIndex").toLongLong(&ok);
if (!ok || i < 0 || i >= count || seen.contains(int(i)))
return false;
if (first >= 0 && i != expected++)
return false;
seen.insert(int(i));
for (const auto &key : {"width", "height"}) {
double n = p.value(key).toDouble(&ok);
if (!ok || !std::isfinite(n) || n <= 0 || n > 100000)
return false;
}
if (p.value("label").toString().size() > 4096)
return false;
const auto rotation = p.value("rotation").toInt(&ok);
if (!ok || (rotation != 0 && rotation != 90 && rotation != 180 && rotation != 270))
return false;
const auto box = p.value("cropBox").toList();
if (box.size() != 4 || box[2].toDouble() <= box[0].toDouble() ||
box[3].toDouble() <= box[1].toDouble())
return false;
for (const auto &v : box) {
double n = v.toDouble(&ok);
if (!ok || !std::isfinite(n) || std::abs(n) > 1e7)
return false;
}
}
return true;
}
} // namespace docview
+10
View File
@@ -0,0 +1,10 @@
#pragma once
#include <QVariant>
namespace docview {
inline constexpr qint64 MaxNavigationBytes = 32ll * 1024 * 1024;
// Version 1 requires all eight capabilities. Unknown declarations never grant an operation.
bool validateCapabilities(const QVariant &capabilities, const QVariant &reasons, QString *error = nullptr);
bool sanitizeNavigation(const QVariantList &input, QVariantList *output, qint64 *usedBytes,
QString *error = nullptr);
bool validatePageMetadata(const QVariantList &pages, int pageCount, int firstPage = -1);
} // namespace docview

Some files were not shown because too many files have changed in this diff Show More