WIP HWP 고침 (관리자 지시 10/20, 28 지적): ① 라이선스 - .so 에 실제로 링크되는 crate 167개 (Android 대상 · 일반 의존 · proc-macro 뺌) 마다 이름 · 판 · 라이선스 · 원문을 tool/licenses/rhwp_crates.py 가 RHWP_LICENSES.txt 로 (빌드 스크립트가 부름, npm 쪽 THIRD_PARTY_LICENSES.md 는 뺌), licenses_test 가 Cargo.lock 과 견줌 ③ 진짜 멈추기 - jj_rhwp_cancel (AtomicBool, 쪽마다 확인), 시간 제한 · 닫을 때 부름 ④ 오류 종류 코드 (JJ_RHWP_E_*) → 번역 글, 원문은 [자세히] · 작업 기록 (.so 다시 빌드 전, 검토 대기)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>hwp2
parent
c43637b9cc
commit
d18b3b02ad
|
|
@ -211,6 +211,7 @@
|
|||
"GitHub 의 최신 버전과 비교해, 새 버전이 있으면 받아서 설치합니다": "Compares with the latest version on GitHub and downloads and installs it if newer",
|
||||
"HTML 파일로 내보내기": "Export to HTML file",
|
||||
"HTML 파일에서 가져오기": "Import from HTML file",
|
||||
"HWP · HWPX 문서로 읽을 수 없습니다 (깨졌거나 지원하지 않는 형식)": "Can't read it as an HWP / HWPX document (damaged or unsupported format)",
|
||||
"IPv4 · IPv6 주소를 넣으세요 (예: 8.8.4.4)": "Enter an IPv4 or IPv6 address (e.g. 8.8.4.4)",
|
||||
"IPv4 · IPv6 주소를 넣으세요 (예: 8.8.8.8)": "Enter an IPv4 or IPv6 address (e.g. 8.8.8.8)",
|
||||
"JJ_MKVMaker - 다운로드 {0}개": "JJ_MKVMaker - {0} downloads",
|
||||
|
|
@ -806,6 +807,7 @@
|
|||
"그림을 읽을 수 없습니다": "Cannot read the image",
|
||||
"그림을 페이지로 넣기 (이 페이지 뒤)": "Add images as pages (after this page)",
|
||||
"그림이 거의 같고 마지막 단계가 10배쯤 빠릅니다 (받아 두었을 때)": "Almost the same image, and the last step is about 10× faster (when downloaded)",
|
||||
"그만두었습니다": "Stopped",
|
||||
"그중 {0}개는 받는 쪽이 더 새것입니다 - 더 새 파일을 예전 것으로 덮어씁니다": "{0} of them are newer on the receiving side - the newer files will be overwritten with older ones",
|
||||
"그중 폴더 {0}개 - 안의 파일 · 폴더도 모두 함께": "Including {0} folders - everything inside them too",
|
||||
"글 → 그림 · 그림 → 그림 (기기 안에서, 모델은 환경 설정에서 받기)": "Text → image · image → image (on this device; download models in Settings)",
|
||||
|
|
@ -1295,6 +1297,7 @@
|
|||
"바꾸기 ({0}개)": "Rename ({0})",
|
||||
"바꾸는 정도 {0}%": "Change amount {0}%",
|
||||
"바꾸면 바로 저장됩니다": "Saved automatically",
|
||||
"바꾼 PDF 를 저장하지 못했습니다 (저장 공간을 확인해 주세요)": "Couldn't save the converted PDF (check storage space)",
|
||||
"바꾼 것이 없습니다 (원본 그대로)": "Nothing changed (original as is)",
|
||||
"바꿀 글": "Replace with",
|
||||
"바꿀 글 (비우면 지움)": "Replace with (empty = remove)",
|
||||
|
|
@ -1397,6 +1400,7 @@
|
|||
"벵골어": "Bengali",
|
||||
"변경 내용을 저장하지 않고 나갈까요?": "Leave without saving the changes?",
|
||||
"변환": "Convert",
|
||||
"변환기 안에서 오류가 났습니다": "An error occurred inside the converter",
|
||||
"보관한 설정으로 되살리고 다시 시작": "Restore kept settings and restart",
|
||||
"보관해 둔 설정을 되살렸습니다: {0}": "Restored the kept settings: {0}",
|
||||
"보기": "View",
|
||||
|
|
@ -1756,6 +1760,8 @@
|
|||
"알리지 않음": "Don't notify",
|
||||
"알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)": "Notifications show as JJ_MKVMaker and open the app when clicked (current user only, no admin rights)",
|
||||
"암호 ZIP 으로 묶는 중 · {0}": "Making password ZIP · {0}",
|
||||
"암호가 걸린 문서입니다": "This document is password-protected",
|
||||
"암호가 맞지 않습니다": "The password is incorrect",
|
||||
"암호로 묶기": "Make password ZIP",
|
||||
"암호화": "Encryption",
|
||||
"암호화 (TLS) 연결을 맺지 못했습니다: {0}": "Couldn't establish an encrypted (TLS) connection: {0}",
|
||||
|
|
@ -2073,6 +2079,7 @@
|
|||
"이 위치 (네트워크 드라이브 · 네트워크 공유 · USB 메모리 등) 에는 휴지통이 없어 바로 지워집니다.": "This location (network drive, network share, USB memory etc.) has no Recycle Bin, so items are deleted right away.",
|
||||
"이 이동 버튼 지우기": "Delete this move button",
|
||||
"이 이동을 다시 할 때 이 옵션을 씁니다. 지금 새 옵션으로 실행할까요?": "These options will be used the next time this move runs. Run it now with the new options?",
|
||||
"이 쪽을 그리지 못했습니다": "Couldn't render this page",
|
||||
"이 파일에 맞는 자막 우선 (영상 해시)": "Prefer subtitles that match this file (video hash)",
|
||||
"이 파일용": "For this file",
|
||||
"이 파일을 열 수 있는 앱이 없습니다.": "No app can open this file.",
|
||||
|
|
@ -2494,6 +2501,7 @@
|
|||
"진행 패널 보기": "Show progress panel",
|
||||
"집 NAS 처럼 자체 서명 인증서면 [서버 설정 고치기] 에서 \"인증서 확인 안 함\" 을 켜세요.": "If it uses a self-signed certificate (like a home NAS), turn on \"Don't verify certificate\" in [Edit server settings].",
|
||||
"짧을수록 빨리 맞추지만 배터리를 더 씁니다.": "Shorter means faster syncing but more battery use.",
|
||||
"쪽 범위가 맞지 않습니다": "The page range is invalid",
|
||||
"차례 기다림": "Waiting in queue",
|
||||
"창": "Panes",
|
||||
"창 · 작업 표시줄 · 트레이와 앱 안 왼쪽 위 버튼의 아이콘입니다 (실행 파일 · 바탕 화면 바로 가기 아이콘은 그대로)": "Icon of the window, taskbar, tray and the top-left button in the app (the program file and desktop shortcut icons stay the same)",
|
||||
|
|
@ -2661,6 +2669,7 @@
|
|||
"파일만 골랐을 때": "When only files are selected",
|
||||
"파일을 복사하지 못했습니다": "Could not copy the file",
|
||||
"파일을 분석할 수 없습니다: {0}": "Cannot analyze the file: {0}",
|
||||
"파일을 읽을 수 없습니다": "Can't read the file",
|
||||
"파일을 읽지 못했습니다": "Could not read the file",
|
||||
"파일이 깨졌습니다": "The file is damaged",
|
||||
"파일이 깨졌습니다 · [다시 받기] 를 눌러 주세요": "The file is damaged · tap [Download again]",
|
||||
|
|
|
|||
|
|
@ -211,6 +211,7 @@
|
|||
"GitHub 의 최신 버전과 비교해, 새 버전이 있으면 받아서 설치합니다": "GitHub の最新版と比べ、新しい版があればダウンロードしてインストールします",
|
||||
"HTML 파일로 내보내기": "HTML ファイルに書き出す",
|
||||
"HTML 파일에서 가져오기": "HTML ファイルから取り込む",
|
||||
"HWP · HWPX 문서로 읽을 수 없습니다 (깨졌거나 지원하지 않는 형식)": "HWP・HWPX 文書として読み込めません (破損しているか未対応の形式)",
|
||||
"IPv4 · IPv6 주소를 넣으세요 (예: 8.8.4.4)": "IPv4・IPv6 アドレスを入力してください (例: 8.8.4.4)",
|
||||
"IPv4 · IPv6 주소를 넣으세요 (예: 8.8.8.8)": "IPv4・IPv6 アドレスを入力してください (例: 8.8.8.8)",
|
||||
"JJ_MKVMaker - 다운로드 {0}개": "JJ_MKVMaker - ダウンロード {0}個",
|
||||
|
|
@ -806,6 +807,7 @@
|
|||
"그림을 읽을 수 없습니다": "画像を読み込めません",
|
||||
"그림을 페이지로 넣기 (이 페이지 뒤)": "画像をページとして追加 (このページの後)",
|
||||
"그림이 거의 같고 마지막 단계가 10배쯤 빠릅니다 (받아 두었을 때)": "画像はほぼ同じで、最後の段階が約 10 倍速くなります (ダウンロード済みの場合)",
|
||||
"그만두었습니다": "中止しました",
|
||||
"그중 {0}개는 받는 쪽이 더 새것입니다 - 더 새 파일을 예전 것으로 덮어씁니다": "そのうち {0} 件は受け取る側の方が新しいです - 新しいファイルを古いもので上書きします",
|
||||
"그중 폴더 {0}개 - 안의 파일 · 폴더도 모두 함께": "うちフォルダー {0} 件 - 中のファイル・フォルダーもすべて一緒に",
|
||||
"글 → 그림 · 그림 → 그림 (기기 안에서, 모델은 환경 설정에서 받기)": "テキスト → 画像 · 画像 → 画像 (端末内で。モデルは設定でダウンロード)",
|
||||
|
|
@ -1295,6 +1297,7 @@
|
|||
"바꾸기 ({0}개)": "変更 ({0} 件)",
|
||||
"바꾸는 정도 {0}%": "変える度合い {0}%",
|
||||
"바꾸면 바로 저장됩니다": "変更するとすぐ保存されます",
|
||||
"바꾼 PDF 를 저장하지 못했습니다 (저장 공간을 확인해 주세요)": "変換した PDF を保存できませんでした (空き容量を確認してください)",
|
||||
"바꾼 것이 없습니다 (원본 그대로)": "変更なし(元のまま)",
|
||||
"바꿀 글": "置き換える文字",
|
||||
"바꿀 글 (비우면 지움)": "置き換える文字 (空なら削除)",
|
||||
|
|
@ -1397,6 +1400,7 @@
|
|||
"벵골어": "ベンガル語",
|
||||
"변경 내용을 저장하지 않고 나갈까요?": "変更を保存せずに終了しますか?",
|
||||
"변환": "変換",
|
||||
"변환기 안에서 오류가 났습니다": "変換エンジン内でエラーが発生しました",
|
||||
"보관한 설정으로 되살리고 다시 시작": "保管した設定で復元して再起動",
|
||||
"보관해 둔 설정을 되살렸습니다: {0}": "保管した設定を復元しました: {0}",
|
||||
"보기": "表示",
|
||||
|
|
@ -1756,6 +1760,8 @@
|
|||
"알리지 않음": "知らせない",
|
||||
"알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)": "通知が JJ_MKVMaker の名前で表示され、クリックするとアプリが開きます (管理者権限なしで現在のユーザーのみ)",
|
||||
"암호 ZIP 으로 묶는 중 · {0}": "パスワード付き ZIP にまとめています · {0}",
|
||||
"암호가 걸린 문서입니다": "パスワードで保護された文書です",
|
||||
"암호가 맞지 않습니다": "パスワードが正しくありません",
|
||||
"암호로 묶기": "パスワード付きでまとめる",
|
||||
"암호화": "暗号化",
|
||||
"암호화 (TLS) 연결을 맺지 못했습니다: {0}": "暗号化 (TLS) 接続を確立できませんでした: {0}",
|
||||
|
|
@ -2073,6 +2079,7 @@
|
|||
"이 위치 (네트워크 드라이브 · 네트워크 공유 · USB 메모리 등) 에는 휴지통이 없어 바로 지워집니다.": "この場所 (ネットワークドライブ・ネットワーク共有・USB メモリなど) にはごみ箱がないため、すぐに削除されます。",
|
||||
"이 이동 버튼 지우기": "この移動ボタンを削除",
|
||||
"이 이동을 다시 할 때 이 옵션을 씁니다. 지금 새 옵션으로 실행할까요?": "この移動を次に行うときにこのオプションを使います。今すぐ新しいオプションで実行しますか?",
|
||||
"이 쪽을 그리지 못했습니다": "このページを描画できませんでした",
|
||||
"이 파일에 맞는 자막 우선 (영상 해시)": "このファイルに合う字幕を優先(動画ハッシュ)",
|
||||
"이 파일용": "このファイル用",
|
||||
"이 파일을 열 수 있는 앱이 없습니다.": "このファイルを開けるアプリがありません。",
|
||||
|
|
@ -2494,6 +2501,7 @@
|
|||
"진행 패널 보기": "進行パネルを表示",
|
||||
"집 NAS 처럼 자체 서명 인증서면 [서버 설정 고치기] 에서 \"인증서 확인 안 함\" 을 켜세요.": "自宅 NAS のように自己署名証明書なら、[サーバー設定を修正] で「証明書を検証しない」をオンにしてください。",
|
||||
"짧을수록 빨리 맞추지만 배터리를 더 씁니다.": "短いほど早く同期しますが、バッテリーをより使います。",
|
||||
"쪽 범위가 맞지 않습니다": "ページ範囲が正しくありません",
|
||||
"차례 기다림": "順番待ち",
|
||||
"창": "ペイン",
|
||||
"창 · 작업 표시줄 · 트레이와 앱 안 왼쪽 위 버튼의 아이콘입니다 (실행 파일 · 바탕 화면 바로 가기 아이콘은 그대로)": "ウィンドウ・タスクバー・トレイとアプリ内左上のボタンのアイコンです(実行ファイル・デスクトップのショートカットのアイコンはそのまま)",
|
||||
|
|
@ -2661,6 +2669,7 @@
|
|||
"파일만 골랐을 때": "ファイルだけを選んだとき",
|
||||
"파일을 복사하지 못했습니다": "ファイルをコピーできませんでした",
|
||||
"파일을 분석할 수 없습니다: {0}": "ファイルを分析できません: {0}",
|
||||
"파일을 읽을 수 없습니다": "ファイルを読み込めません",
|
||||
"파일을 읽지 못했습니다": "ファイルを読み取れませんでした",
|
||||
"파일이 깨졌습니다": "ファイルが壊れています",
|
||||
"파일이 깨졌습니다 · [다시 받기] 를 눌러 주세요": "ファイルが壊れています・[再ダウンロード] を押してください",
|
||||
|
|
|
|||
|
|
@ -211,6 +211,7 @@
|
|||
"GitHub 의 최신 버전과 비교해, 새 버전이 있으면 받아서 설치합니다": "与 GitHub 上的最新版本比较,如有新版本则下载并安装",
|
||||
"HTML 파일로 내보내기": "导出为 HTML 文件",
|
||||
"HTML 파일에서 가져오기": "从 HTML 文件导入",
|
||||
"HWP · HWPX 문서로 읽을 수 없습니다 (깨졌거나 지원하지 않는 형식)": "无法作为 HWP · HWPX 文档读取 (已损坏或格式不受支持)",
|
||||
"IPv4 · IPv6 주소를 넣으세요 (예: 8.8.4.4)": "请输入 IPv4 · IPv6 地址 (例: 8.8.4.4)",
|
||||
"IPv4 · IPv6 주소를 넣으세요 (예: 8.8.8.8)": "请输入 IPv4 · IPv6 地址 (例: 8.8.8.8)",
|
||||
"JJ_MKVMaker - 다운로드 {0}개": "JJ_MKVMaker - 下载 {0} 个",
|
||||
|
|
@ -806,6 +807,7 @@
|
|||
"그림을 읽을 수 없습니다": "无法读取图片",
|
||||
"그림을 페이지로 넣기 (이 페이지 뒤)": "将图片作为页面加入 (本页之后)",
|
||||
"그림이 거의 같고 마지막 단계가 10배쯤 빠릅니다 (받아 두었을 때)": "图片几乎相同,最后一步快约 10 倍 (已下载时)",
|
||||
"그만두었습니다": "已停止",
|
||||
"그중 {0}개는 받는 쪽이 더 새것입니다 - 더 새 파일을 예전 것으로 덮어씁니다": "其中 {0} 个在接收端较新 - 将用旧文件覆盖较新的文件",
|
||||
"그중 폴더 {0}개 - 안의 파일 · 폴더도 모두 함께": "其中 {0} 个文件夹 - 其中的文件和文件夹也一并处理",
|
||||
"글 → 그림 · 그림 → 그림 (기기 안에서, 모델은 환경 설정에서 받기)": "文字 → 图片 · 图片 → 图片(在本设备上;模型在设置中下载)",
|
||||
|
|
@ -1295,6 +1297,7 @@
|
|||
"바꾸기 ({0}개)": "重命名 ({0} 个)",
|
||||
"바꾸는 정도 {0}%": "改变程度 {0}%",
|
||||
"바꾸면 바로 저장됩니다": "更改后立即保存",
|
||||
"바꾼 PDF 를 저장하지 못했습니다 (저장 공간을 확인해 주세요)": "无法保存转换后的 PDF (请检查存储空间)",
|
||||
"바꾼 것이 없습니다 (원본 그대로)": "未做更改(保持原样)",
|
||||
"바꿀 글": "替换为",
|
||||
"바꿀 글 (비우면 지움)": "替换为 (留空则删除)",
|
||||
|
|
@ -1397,6 +1400,7 @@
|
|||
"벵골어": "孟加拉语",
|
||||
"변경 내용을 저장하지 않고 나갈까요?": "不保存更改就离开吗?",
|
||||
"변환": "转换",
|
||||
"변환기 안에서 오류가 났습니다": "转换器内部出错",
|
||||
"보관한 설정으로 되살리고 다시 시작": "用保存的设置恢复并重新启动",
|
||||
"보관해 둔 설정을 되살렸습니다: {0}": "已恢复保存的设置: {0}",
|
||||
"보기": "查看",
|
||||
|
|
@ -1756,6 +1760,8 @@
|
|||
"알리지 않음": "不提醒",
|
||||
"알림이 JJ_MKVMaker 이름으로 보이고, 누르면 앱이 열립니다 (관리자 권한 없이 지금 사용자에게만)": "通知以 JJ_MKVMaker 名义显示,点击即可打开应用 (仅当前用户,无需管理员权限)",
|
||||
"암호 ZIP 으로 묶는 중 · {0}": "正在创建加密 ZIP · {0}",
|
||||
"암호가 걸린 문서입니다": "这是受密码保护的文档",
|
||||
"암호가 맞지 않습니다": "密码不正确",
|
||||
"암호로 묶기": "加密压缩",
|
||||
"암호화": "加密",
|
||||
"암호화 (TLS) 연결을 맺지 못했습니다: {0}": "无法建立加密 (TLS) 连接:{0}",
|
||||
|
|
@ -2073,6 +2079,7 @@
|
|||
"이 위치 (네트워크 드라이브 · 네트워크 공유 · USB 메모리 등) 에는 휴지통이 없어 바로 지워집니다.": "此位置 (网络驱动器 · 网络共享 · U 盘等) 没有回收站,会直接删除。",
|
||||
"이 이동 버튼 지우기": "删除此移动按钮",
|
||||
"이 이동을 다시 할 때 이 옵션을 씁니다. 지금 새 옵션으로 실행할까요?": "下次执行此移动时将使用这些选项。现在用新选项执行吗?",
|
||||
"이 쪽을 그리지 못했습니다": "无法渲染此页",
|
||||
"이 파일에 맞는 자막 우선 (영상 해시)": "优先匹配此文件的字幕(视频哈希)",
|
||||
"이 파일용": "适用于此文件",
|
||||
"이 파일을 열 수 있는 앱이 없습니다.": "没有可以打开此文件的应用。",
|
||||
|
|
@ -2494,6 +2501,7 @@
|
|||
"진행 패널 보기": "显示进度面板",
|
||||
"집 NAS 처럼 자체 서명 인증서면 [서버 설정 고치기] 에서 \"인증서 확인 안 함\" 을 켜세요.": "如果是自签名证书 (如家用 NAS),请在 [修改服务器设置] 中开启“不验证证书”。",
|
||||
"짧을수록 빨리 맞추지만 배터리를 더 씁니다.": "间隔越短同步越快,但更耗电。",
|
||||
"쪽 범위가 맞지 않습니다": "页面范围无效",
|
||||
"차례 기다림": "排队等待",
|
||||
"창": "窗格",
|
||||
"창 · 작업 표시줄 · 트레이와 앱 안 왼쪽 위 버튼의 아이콘입니다 (실행 파일 · 바탕 화면 바로 가기 아이콘은 그대로)": "窗口、任务栏、托盘以及应用内左上角按钮的图标(程序文件和桌面快捷方式的图标不变)",
|
||||
|
|
@ -2661,6 +2669,7 @@
|
|||
"파일만 골랐을 때": "只选择文件时",
|
||||
"파일을 복사하지 못했습니다": "无法复制文件",
|
||||
"파일을 분석할 수 없습니다: {0}": "无法分析文件: {0}",
|
||||
"파일을 읽을 수 없습니다": "无法读取文件",
|
||||
"파일을 읽지 못했습니다": "无法读取文件",
|
||||
"파일이 깨졌습니다": "文件已损坏",
|
||||
"파일이 깨졌습니다 · [다시 받기] 를 눌러 주세요": "文件已损坏 · 请点按 [重新下载]",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -22,7 +22,7 @@ class _FakeEngine implements HwpPdfEngine {
|
|||
|
||||
@override
|
||||
Future<HwpPdfDoc> open(String path, {String? password}) async {
|
||||
if (password == 'bad') throw const HwpPdfException('암호가 틀림');
|
||||
if (password == 'bad') throw const HwpPdfException(HwpErrorKind.passwordWrong, 'CryptoError WrongPassword');
|
||||
return _FakeDoc(this);
|
||||
}
|
||||
}
|
||||
|
|
@ -40,11 +40,11 @@ class _FakeDoc implements HwpPdfDoc {
|
|||
// 끝나지 않는 변환 (시간 제한 시험): 풀려도 아무것도 쓰지 않는다 (시험이 끝난 뒤 폴더에 쓰지 않게)
|
||||
if (e.hold != null) {
|
||||
await e.hold!.future;
|
||||
throw const HwpPdfException('늦게 끝남');
|
||||
throw const HwpPdfException(HwpErrorKind.cancelled, 'late');
|
||||
}
|
||||
if (e.failNext > 0) {
|
||||
e.failNext--;
|
||||
throw const HwpPdfException('렌더 실패');
|
||||
throw const HwpPdfException(HwpErrorKind.render, 'page 1: boom');
|
||||
}
|
||||
final docs = <PdfDocument>[];
|
||||
for (var i = from; i < to; i++) {
|
||||
|
|
@ -62,6 +62,9 @@ class _FakeDoc implements HwpPdfDoc {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void cancel() {}
|
||||
|
||||
@override
|
||||
Future<void> close() async => closed = true;
|
||||
}
|
||||
|
|
@ -106,7 +109,7 @@ void main() {
|
|||
await tester.runAsync(() => src!.load(0, maxWidth: 200).then((r) => r.dispose(), onError: (Object e) {
|
||||
err = e;
|
||||
}));
|
||||
expect('$err', contains('렌더 실패'));
|
||||
expect('$err', contains('이 쪽을 그리지 못했습니다'));
|
||||
final ok = await tester.runAsync(() => src!.load(0, maxWidth: 200));
|
||||
expect(ok!.image, isNotNull, reason: '실패한 묶음은 다시');
|
||||
ok.dispose();
|
||||
|
|
@ -129,6 +132,6 @@ void main() {
|
|||
await tester.runAsync(() => HwpSource.open(doc.path, engine: engine, tempDir: dir.path, password: 'bad').then((_) {}, onError: (Object e) {
|
||||
err3 = e;
|
||||
}));
|
||||
expect('$err3', contains('암호'));
|
||||
expect('$err3', contains('암호가 맞지 않습니다'));
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,12 +43,45 @@ abstract class HwpPdfDoc {
|
|||
|
||||
/// [from] 부터 [to] 앞까지 (0 부터) 를 PDF 파일 [out] 으로. [fontDirs] 는 글꼴을 찾을 폴더들
|
||||
Future<void> exportPdf(int from, int to, String out, {List<String> fontDirs = const []});
|
||||
|
||||
/// 지금 바꾸는 것을 멈춘다 (시간 제한을 넘겼을 때 · 닫을 때 - 관리자 지시 10/20: 기다림만 끊으면 뒤에서 계속 돌아
|
||||
/// 다음 묶음이 줄을 서서 연달아 실패했다). 멈춘 바꾸기는 [HwpErrorKind.cancelled] 로 끝난다
|
||||
void cancel();
|
||||
Future<void> close();
|
||||
}
|
||||
|
||||
/// 관리자 지시 (10/20, 28 지적): 변환기 오류의 종류 - 화면에는 종류에 맞는 번역 글, 원문은 [HwpPdfException.detail] ([자세히] · 작업 기록).
|
||||
/// 앞 9개는 tool/rhwp_ffi 의 JJ_RHWP_E_* 와 같은 순서 (1 부터)
|
||||
enum HwpErrorKind { cancelled, read, invalid, passwordRequired, passwordWrong, range, render, write, internal, timeout, closed }
|
||||
|
||||
/// tool/rhwp_ffi 의 오류 코드 (JJ_RHWP_E_*) → 종류 (모르는 코드는 내부 오류)
|
||||
HwpErrorKind hwpErrorKindOf(int code) => code >= 1 && code <= 9 ? HwpErrorKind.values[code - 1] : HwpErrorKind.internal;
|
||||
|
||||
class HwpPdfException implements Exception {
|
||||
final String message;
|
||||
const HwpPdfException(this.message);
|
||||
final HwpErrorKind kind;
|
||||
|
||||
/// 변환기 원문 (화면에는 보이지 않고 [자세히] · 작업 기록에)
|
||||
final String detail;
|
||||
|
||||
/// 글에 넣을 값 (시간 제한: 첫 쪽 · 끝 쪽 · 초)
|
||||
final List<Object> args;
|
||||
const HwpPdfException(this.kind, [this.detail = '', this.args = const []]);
|
||||
|
||||
/// 화면에 보일 글 (화면 언어)
|
||||
String get message => switch (kind) {
|
||||
HwpErrorKind.cancelled => tr('그만두었습니다'),
|
||||
HwpErrorKind.read => tr('파일을 읽을 수 없습니다'),
|
||||
HwpErrorKind.invalid => tr('HWP · HWPX 문서로 읽을 수 없습니다 (깨졌거나 지원하지 않는 형식)'),
|
||||
HwpErrorKind.passwordRequired => tr('암호가 걸린 문서입니다'),
|
||||
HwpErrorKind.passwordWrong => tr('암호가 맞지 않습니다'),
|
||||
HwpErrorKind.range => tr('쪽 범위가 맞지 않습니다'),
|
||||
HwpErrorKind.render => tr('이 쪽을 그리지 못했습니다'),
|
||||
HwpErrorKind.write => tr('바꾼 PDF 를 저장하지 못했습니다 (저장 공간을 확인해 주세요)'),
|
||||
HwpErrorKind.internal => tr('변환기 안에서 오류가 났습니다'),
|
||||
HwpErrorKind.timeout => trf('{0}~{1}쪽을 {2}초 안에 바꾸지 못했습니다', args),
|
||||
HwpErrorKind.closed => tr('닫힌 문서입니다'),
|
||||
};
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
|
@ -121,7 +154,10 @@ class HwpSource extends ReaderSource {
|
|||
var _disposed = false;
|
||||
|
||||
HwpSource._(this.path, this.doc, this.work, this.title,
|
||||
{required this.fontDirs, required this.chunk, required this.timeout, required this.maxOpenChunks, required this.opener});
|
||||
{required this.fontDirs, required this.chunk, required this.timeout, required this.maxOpenChunks, required this.opener, this.log});
|
||||
|
||||
/// 실패의 원문을 남길 곳 (작업 기록 - 파일 탐색기가 AppController.note 를 준다)
|
||||
final void Function(String line)? log;
|
||||
|
||||
/// 문서를 연다 (WebDAV 등은 [tempDir] 로 받은 뒤). 열지 못하면 [HwpPdfException]
|
||||
static Future<HwpSource> open(String path,
|
||||
|
|
@ -132,12 +168,19 @@ class HwpSource extends ReaderSource {
|
|||
int chunk = 5,
|
||||
Duration timeout = const Duration(minutes: 3),
|
||||
int maxOpenChunks = 3,
|
||||
HwpChunkOpener opener = pdfChunkOpener}) async {
|
||||
HwpChunkOpener opener = pdfChunkOpener,
|
||||
void Function(String line)? log}) async {
|
||||
final local = await vLocalCopy(path, tempDir);
|
||||
final doc = await engine.open(local, password: password);
|
||||
final HwpPdfDoc doc;
|
||||
try {
|
||||
doc = await engine.open(local, password: password);
|
||||
} on HwpPdfException catch (e) {
|
||||
log?.call('HWP ${vBasename(path)}: ${e.kind.name} ${e.detail}');
|
||||
rethrow;
|
||||
}
|
||||
final work = Directory(p.join(tempDir, 'jj_hwp_${DateTime.now().microsecondsSinceEpoch}'))..createSync(recursive: true);
|
||||
return HwpSource._(path, doc, work, vBasename(path),
|
||||
fontDirs: fontDirs, chunk: chunk, timeout: timeout, maxOpenChunks: math.max(2, maxOpenChunks), opener: opener);
|
||||
fontDirs: fontDirs, chunk: chunk, timeout: timeout, maxOpenChunks: math.max(2, maxOpenChunks), opener: opener, log: log);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -176,10 +219,14 @@ class HwpSource extends ReaderSource {
|
|||
try {
|
||||
await doc.exportPdf(from, to, out, fontDirs: fontDirs).timeout(timeout);
|
||||
} on TimeoutException {
|
||||
throw HwpPdfException(trf('{0}~{1}쪽을 {2}초 안에 바꾸지 못했습니다', [from + 1, to, timeout.inSeconds]));
|
||||
} catch (_) {
|
||||
// 닫은 뒤에 끝난 변환은 (임시 폴더를 이미 지워) 실패한다 - 그 이유 대신 "닫힌 문서"
|
||||
// 관리자 지시 (10/20): 기다림만 끊지 않고 변환도 멈춘다 (다음 묶음이 줄 서서 실패하지 않게)
|
||||
doc.cancel();
|
||||
log?.call('HWP ${vBasename(path)}: timeout ${from + 1}~$to');
|
||||
throw HwpPdfException(HwpErrorKind.timeout, 'timeout ${timeout.inSeconds}s', [from + 1, to, timeout.inSeconds]);
|
||||
} catch (e) {
|
||||
// 닫은 뒤에 끝난 변환은 (임시 폴더를 이미 지워 · 멈춰) 실패한다 - 그 이유 대신 "닫힌 문서"
|
||||
if (_disposed) throw StateError(tr('닫힌 문서입니다'));
|
||||
if (e is HwpPdfException) log?.call('HWP ${vBasename(path)} ${from + 1}~$to: ${e.kind.name} ${e.detail}');
|
||||
rethrow;
|
||||
}
|
||||
_made.add(k);
|
||||
|
|
@ -232,7 +279,8 @@ class HwpSource extends ReaderSource {
|
|||
for (final f in open) {
|
||||
unawaited(f.then((c) => c.dispose(), onError: (Object _) {}));
|
||||
}
|
||||
await doc.close(); // 바꾸는 중이면 그것이 끝난 뒤에 닫힌다 (엔진이 한 번에 하나씩)
|
||||
doc.cancel(); // 바꾸는 중이면 다음 쪽에서 멈춘다
|
||||
await doc.close(); // 멈춘 바꾸기가 끝난 뒤에 닫힌다 (엔진이 한 번에 하나씩)
|
||||
try {
|
||||
if (work.existsSync()) work.deleteSync(recursive: true);
|
||||
} catch (_) {}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import '../../core/hwp_source.dart';
|
|||
|
||||
/// 관리자 지시 (10/20): Android 의 HWP · HWPX → PDF - APK 에 든 libjj_rhwp.so (rhwp v0.8.7, MIT - tool/build_rhwp_android.sh ·
|
||||
/// tool/rhwp_ffi) 를 dart:ffi 로 부른다. 여는 것 · 바꾸는 것은 다른 isolate 에서 (화면을 막지 않게), 한 문서는 한 번에 하나씩.
|
||||
/// 라이브러리는 HWP 를 처음 열 때 읽는다 (앱을 켤 때 읽지 않음).
|
||||
class RhwpEngine implements HwpPdfEngine {
|
||||
static const libName = 'libjj_rhwp.so';
|
||||
|
||||
|
|
@ -27,8 +28,8 @@ class RhwpEngine implements HwpPdfEngine {
|
|||
|
||||
@override
|
||||
Future<HwpPdfDoc> open(String path, {String? password}) async {
|
||||
final (handle, err) = await Isolate.run(() => _open(path, password ?? ''));
|
||||
if (handle == 0) throw HwpPdfException(err ?? 'rhwp');
|
||||
final (handle, code, err) = await Isolate.run(() => _open(path, password ?? ''));
|
||||
if (handle == 0) throw HwpPdfException(hwpErrorKindOf(code), err ?? '');
|
||||
final pages = await Isolate.run(() => _pageCount(handle));
|
||||
return _RhwpDoc(handle, pages);
|
||||
}
|
||||
|
|
@ -40,7 +41,7 @@ class _RhwpDoc implements HwpPdfDoc {
|
|||
final int pageCount;
|
||||
var _closed = false;
|
||||
|
||||
/// 한 번에 하나씩 (rhwp 문서는 여러 스레드에서 함께 쓰지 않는다)
|
||||
/// 한 번에 하나씩 (rhwp 문서는 여러 스레드에서 함께 바꾸지 않는다)
|
||||
Future<void> _queue = Future.value();
|
||||
|
||||
_RhwpDoc(this._handle, this.pageCount);
|
||||
|
|
@ -53,12 +54,18 @@ class _RhwpDoc implements HwpPdfDoc {
|
|||
|
||||
@override
|
||||
Future<void> exportPdf(int from, int to, String out, {List<String> fontDirs = const []}) => _serial(() async {
|
||||
if (_closed) throw const HwpPdfException('closed');
|
||||
if (_closed) throw const HwpPdfException(HwpErrorKind.closed);
|
||||
final h = _handle, dirs = fontDirs.join('\n');
|
||||
final err = await Isolate.run(() => _export(h, from, to, dirs, out));
|
||||
if (err != null) throw HwpPdfException(err);
|
||||
final (code, err) = await Isolate.run(() => _export(h, from, to, dirs, out));
|
||||
if (code != 0) throw HwpPdfException(hwpErrorKindOf(code), err ?? '');
|
||||
});
|
||||
|
||||
/// 지금 바꾸는 것을 멈춘다 (줄을 서지 않고 바로 - 다른 isolate 의 바꾸기가 다음 쪽에서 끝난다)
|
||||
@override
|
||||
void cancel() {
|
||||
if (!_closed) _cancel(_handle);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() => _serial(() async {
|
||||
if (_closed) return;
|
||||
|
|
@ -69,13 +76,13 @@ class _RhwpDoc implements HwpPdfDoc {
|
|||
|
||||
// ───────── C 함수 (isolate 안에서 부른다 - 핸들은 주소 숫자로 넘긴다) ─────────
|
||||
|
||||
typedef _OpenC = Pointer<Void> Function(Pointer<Utf8>, Pointer<Utf8>, Pointer<Pointer<Utf8>>);
|
||||
typedef _OpenC = Pointer<Void> Function(Pointer<Utf8>, Pointer<Utf8>, Pointer<Int32>, Pointer<Pointer<Utf8>>);
|
||||
typedef _CountC = Int32 Function(Pointer<Void>);
|
||||
typedef _CountD = int Function(Pointer<Void>);
|
||||
typedef _ExportC = Int32 Function(Pointer<Void>, Int32, Int32, Pointer<Utf8>, Pointer<Utf8>, Pointer<Pointer<Utf8>>);
|
||||
typedef _ExportD = int Function(Pointer<Void>, int, int, Pointer<Utf8>, Pointer<Utf8>, Pointer<Pointer<Utf8>>);
|
||||
typedef _CloseC = Void Function(Pointer<Void>);
|
||||
typedef _CloseD = void Function(Pointer<Void>);
|
||||
typedef _ExportC = Int32 Function(Pointer<Void>, Int32, Int32, Pointer<Utf8>, Pointer<Utf8>, Pointer<Int32>, Pointer<Pointer<Utf8>>);
|
||||
typedef _ExportD = int Function(Pointer<Void>, int, int, Pointer<Utf8>, Pointer<Utf8>, Pointer<Int32>, Pointer<Pointer<Utf8>>);
|
||||
typedef _VoidC = Void Function(Pointer<Void>);
|
||||
typedef _VoidD = void Function(Pointer<Void>);
|
||||
typedef _FreeC = Void Function(Pointer<Utf8>);
|
||||
typedef _FreeD = void Function(Pointer<Utf8>);
|
||||
|
||||
|
|
@ -90,37 +97,43 @@ String? _takeError(DynamicLibrary lib, Pointer<Pointer<Utf8>> err) {
|
|||
return s;
|
||||
}
|
||||
|
||||
(int, String?) _open(String path, String password) {
|
||||
(int, int, String?) _open(String path, String password) {
|
||||
final lib = _lib();
|
||||
final open = lib.lookupFunction<_OpenC, _OpenC>('jj_rhwp_open');
|
||||
final a = path.toNativeUtf8(), b = password.toNativeUtf8();
|
||||
final code = calloc<Int32>();
|
||||
final err = calloc<Pointer<Utf8>>();
|
||||
try {
|
||||
final doc = open(a, b, err);
|
||||
return (doc.address, _takeError(lib, err));
|
||||
final doc = open(a, b, code, err);
|
||||
return (doc.address, code.value, _takeError(lib, err));
|
||||
} finally {
|
||||
calloc.free(a);
|
||||
calloc.free(b);
|
||||
calloc.free(code);
|
||||
calloc.free(err);
|
||||
}
|
||||
}
|
||||
|
||||
int _pageCount(int handle) => _lib().lookupFunction<_CountC, _CountD>('jj_rhwp_page_count')(Pointer<Void>.fromAddress(handle));
|
||||
|
||||
String? _export(int handle, int from, int to, String fontDirs, String out) {
|
||||
(int, String?) _export(int handle, int from, int to, String fontDirs, String out) {
|
||||
final lib = _lib();
|
||||
final export = lib.lookupFunction<_ExportC, _ExportD>('jj_rhwp_export_pdf');
|
||||
final d = fontDirs.toNativeUtf8(), o = out.toNativeUtf8();
|
||||
final code = calloc<Int32>();
|
||||
final err = calloc<Pointer<Utf8>>();
|
||||
try {
|
||||
final r = export(Pointer<Void>.fromAddress(handle), from, to, d, o, err);
|
||||
final r = export(Pointer<Void>.fromAddress(handle), from, to, d, o, code, err);
|
||||
final e = _takeError(lib, err);
|
||||
return r == 0 ? null : (e ?? 'rhwp');
|
||||
return r == 0 ? (0, null) : (code.value == 0 ? 9 : code.value, e);
|
||||
} finally {
|
||||
calloc.free(d);
|
||||
calloc.free(o);
|
||||
calloc.free(code);
|
||||
calloc.free(err);
|
||||
}
|
||||
}
|
||||
|
||||
void _close(int handle) => _lib().lookupFunction<_CloseC, _CloseD>('jj_rhwp_close')(Pointer<Void>.fromAddress(handle));
|
||||
void _cancel(int handle) => _lib().lookupFunction<_VoidC, _VoidD>('jj_rhwp_cancel')(Pointer<Void>.fromAddress(handle));
|
||||
|
||||
void _close(int handle) => _lib().lookupFunction<_VoidC, _VoidD>('jj_rhwp_close')(Pointer<Void>.fromAddress(handle));
|
||||
|
|
|
|||
|
|
@ -1805,7 +1805,7 @@ class _ExplorerPageState extends State<ExplorerPage> {
|
|||
HwpSource? src;
|
||||
Object? error;
|
||||
try {
|
||||
src = await HwpSource.open(path, engine: RhwpEngine(), tempDir: temp, fontDirs: RhwpEngine.fontDirs);
|
||||
src = await HwpSource.open(path, engine: RhwpEngine(), tempDir: temp, fontDirs: RhwpEngine.fontDirs, log: c.note);
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import 'package:wakelock_plus/wakelock_plus.dart';
|
|||
|
||||
import '../app/app_controller.dart';
|
||||
import '../core/reader_sources.dart';
|
||||
import '../core/hwp_source.dart' show HwpSource;
|
||||
import '../core/hwp_source.dart' show HwpPdfException, HwpSource;
|
||||
import '../core/vfs.dart';
|
||||
import '../l10n/tr.dart';
|
||||
import 'ai_image_page.dart';
|
||||
|
|
@ -448,6 +448,18 @@ class _ReaderPageState extends State<ReaderPage> {
|
|||
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Text(trf('열 수 없습니다: {0}', [e]), textAlign: TextAlign.center, style: const TextStyle(color: Colors.white70)),
|
||||
const SizedBox(height: 8),
|
||||
// 관리자 지시 (10/20, 28 지적): 변환기 원문은 화면 글 대신 [자세히] 에
|
||||
if (e is HwpPdfException && e.detail.isNotEmpty)
|
||||
TextButton(
|
||||
onPressed: () => showDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
content: SelectableText(e.detail, style: const TextStyle(fontSize: 12)),
|
||||
actions: [TextButton(onPressed: () => Navigator.of(ctx).pop(), child: Text(tr('닫기')))],
|
||||
),
|
||||
),
|
||||
child: Text(tr('자세히')),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () => setState(() {
|
||||
_cache.remove(slot.page);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ class _Engine implements HwpPdfEngine {
|
|||
final calls = <(int, int)>[];
|
||||
Completer<void>? hold;
|
||||
var closed = false;
|
||||
var cancels = 0;
|
||||
_Engine(this.pages);
|
||||
|
||||
@override
|
||||
|
|
@ -32,6 +33,9 @@ class _Doc implements HwpPdfDoc {
|
|||
File(out).writeAsStringSync('$from-$to');
|
||||
}
|
||||
|
||||
@override
|
||||
void cancel() => e.cancels++;
|
||||
|
||||
@override
|
||||
Future<void> close() async => e.closed = true;
|
||||
}
|
||||
|
|
@ -145,7 +149,9 @@ void main() {
|
|||
err = x;
|
||||
}
|
||||
expect(err, isA<HwpPdfException>());
|
||||
expect((err! as HwpPdfException).kind, HwpErrorKind.timeout);
|
||||
expect('$err', allOf(contains('1~5'), contains('0초')));
|
||||
expect(e.cancels, 1, reason: '시간 제한을 넘기면 변환도 멈춘다 (기다림만 끊지 않음)');
|
||||
expect(s.openChunks, isNot(contains(0)), reason: '실패한 묶음은 기억하지 않음 - [다시 시도] 가 다시 만든다');
|
||||
e.hold!.complete();
|
||||
e.hold = null;
|
||||
|
|
@ -161,6 +167,7 @@ void main() {
|
|||
final pending = s.load(0, maxWidth: 100).then<Object?>((_) => null, onError: (Object x) => x);
|
||||
await settle();
|
||||
await s.dispose(); // 기다리지 않고 바로 끝난다
|
||||
expect(e.cancels, 1, reason: '닫으면 바꾸던 것을 멈춘다');
|
||||
e.hold!.complete();
|
||||
final r = await pending;
|
||||
expect(r, isA<StateError>(), reason: '닫힌 문서');
|
||||
|
|
@ -181,4 +188,22 @@ void main() {
|
|||
expect(other.existsSync(), isTrue, reason: '이름이 jj_hwp_ 로 시작하지 않음 (jj_hwpx_…)');
|
||||
expect(file.existsSync(), isTrue, reason: '폴더만');
|
||||
});
|
||||
|
||||
test('변환기 오류 코드 → 종류 · 화면 글 (tool/rhwp_ffi 의 JJ_RHWP_E_* 순서), 원문은 detail 에만', () {
|
||||
expect([for (var c = 1; c <= 9; c++) hwpErrorKindOf(c)], [
|
||||
HwpErrorKind.cancelled, HwpErrorKind.read, HwpErrorKind.invalid, HwpErrorKind.passwordRequired, HwpErrorKind.passwordWrong,
|
||||
HwpErrorKind.range, HwpErrorKind.render, HwpErrorKind.write, HwpErrorKind.internal, //
|
||||
]);
|
||||
expect(hwpErrorKindOf(0), HwpErrorKind.internal);
|
||||
expect(hwpErrorKindOf(42), HwpErrorKind.internal);
|
||||
const e = HwpPdfException(HwpErrorKind.passwordRequired, '비밀번호가 필요한 암호 문서입니다 (parse_document_with_password …)');
|
||||
expect('$e', '암호가 걸린 문서입니다', reason: '화면에는 원문 대신 번역 글');
|
||||
expect(e.detail, contains('parse_document_with_password'));
|
||||
// 원본 lib.rs 의 코드 값과 같은지 (순서가 바뀌면 화면 글이 틀린다)
|
||||
final rs = File('tool/rhwp_ffi/src/lib.rs').readAsStringSync();
|
||||
const names = ['CANCELLED', 'READ', 'INVALID', 'PASSWORD_REQUIRED', 'PASSWORD_WRONG', 'RANGE', 'RENDER', 'WRITE', 'INTERNAL'];
|
||||
for (final (i, n) in names.indexed) {
|
||||
expect(rs, contains('pub const JJ_RHWP_E_$n: c_int = ${i + 1};'), reason: n);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -165,4 +165,30 @@ void main() {
|
|||
expect(a, contains('[패키지 라이선스]'));
|
||||
expect(a, contains('GPLv3'));
|
||||
});
|
||||
|
||||
// 관리자 지시 (10/20, 28 지적): Android HWP 보기 (libjj_rhwp.so) - 실제로 링크되는 Rust crate 마다 라이선스 · 원문
|
||||
// (tool/licenses/rhwp_crates.py 를 tool/build_rhwp_android.sh 가 부름). Cargo.lock 이 바뀌었는데 다시 만들지 않으면 여기서 잡힌다
|
||||
test('rhwp: Cargo.lock 의 모든 crate 가 "linked" 나 "not linked" 에 있고, 링크되는 crate 마다 원문 절', () {
|
||||
final t = File('assets/licenses/RHWP_LICENSES.txt').readAsStringSync().replaceAll('\r\n', '\n');
|
||||
final lock = File('tool/rhwp_ffi/Cargo.lock').readAsStringSync().replaceAll('\r\n', '\n');
|
||||
final locked = {
|
||||
for (final m in RegExp(r'\[\[package\]\]\s*\nname = "([^"]+)"\s*\nversion = "([^"]+)"').allMatches(lock)) '${m[1]} ${m[2]}'
|
||||
};
|
||||
final linked = {for (final m in RegExp(r'^linked: (\S+) (\S+) ', multiLine: true).allMatches(t)) '${m[1]} ${m[2]}'};
|
||||
final notLinked = {for (final m in RegExp(r'^not linked: (\S+) (\S+)$', multiLine: true).allMatches(t)) '${m[1]} ${m[2]}'};
|
||||
expect(linked, isNotEmpty);
|
||||
expect(linked.intersection(notLinked), isEmpty);
|
||||
expect({...linked, ...notLinked, 'jj_rhwp 0.1.0'}, locked, reason: 'Cargo.lock 이 바뀌면 sh tool/build_rhwp_android.sh 로 다시 만든다');
|
||||
for (final c in linked) {
|
||||
expect(t, contains('=== $c ('), reason: '$c 의 라이선스 원문 절');
|
||||
}
|
||||
// 28 이 짚은 것들 (Cargo.lock 에 있으면 어느 한쪽 목록에 - 링크되는지는 cargo tree 가 정함)
|
||||
for (final n in ['tiny-skia', 'zlib-rs', 'curve25519-dalek', 'unicode-ident', 'rustybuzz', 'skrifa', 'miniz_oxide', 'fontdb']) {
|
||||
for (final c in locked.where((c) => c.startsWith('$n '))) {
|
||||
expect(linked.contains(c) || notLinked.contains(c), isTrue, reason: c);
|
||||
}
|
||||
}
|
||||
expect(t, contains('rhwp v0.8.7'));
|
||||
expect(t, contains('1a76570e833917d15817415a53c09ad61ab3203f'));
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@
|
|||
# APK 의 jniLibs 로 들어가 앱이 dart:ffi 로 읽는다).
|
||||
# 소스: https://github.com/edwardkim/rhwp v0.8.7 = 커밋 1a76570 (고정 - git 커밋 해시가 내용의 SHA-1 지문이다, 고치지 않음).
|
||||
# 저장소가 크므로 (예제 문서 · PDF 로 4GB 넘음) 빌드에 쓰는 폴더만 받는다 (git sparse, 깊이 1).
|
||||
# 라이선스: rhwp MIT (LICENSE) · 의존 crate 들 (THIRD_PARTY_LICENSES.md) - 둘 다 third_party/rhwp 에 함께 둔다.
|
||||
# 라이선스: rhwp MIT (LICENSE → third_party/rhwp) · .so 에 실제로 링크되는 crate 마다 이름 · 판 · 라이선스 · 원문은
|
||||
# tool/licenses/rhwp_crates.py 가 assets/licenses/RHWP_LICENSES.txt 로 만든다 (관리자 지시 10/20 - 저장소의 THIRD_PARTY_LICENSES.md 는
|
||||
# npm 쪽이 섞여 있고 링크되는 crate 가 빠져 쓰지 않는다). Cargo.lock 이 바뀌면 test/licenses_test.dart 가 다시 만들라고 잡는다.
|
||||
# 도구: Rust 1.93.1 (rhwp 의 rust-toolchain.toml 과 같음, tools/rust - RUSTUP_HOME · CARGO_HOME) + Android NDK clang (링커).
|
||||
# Windows 에서는 호스트 쪽 (빌드 스크립트) 을 MSVC 로 링크하므로 tools/VSBuildTools 의 vcvars64 환경을 읽어 온다.
|
||||
# 16KB 쪽 기기 (Android 15+) 에서도 읽히게 -z max-page-size=16384, 다시 만들어도 같은 파일이 되게 경로를 /jj 로 바꿔 적는다.
|
||||
|
|
@ -84,7 +86,9 @@ mkdir -p "$OUT"
|
|||
build "$WORK/target" "$OUT/libjj_rhwp.so"
|
||||
[ -n "$LOCKED" ] || cp "$WORK/ffi/Cargo.lock" "$APP/tool/rhwp_ffi/Cargo.lock"
|
||||
cp "$SRC/LICENSE" "$OUT/../LICENSE.txt"
|
||||
cp "$SRC/THIRD_PARTY_LICENSES.md" "$OUT/../THIRD_PARTY_LICENSES.md"
|
||||
rm -f "$OUT/../THIRD_PARTY_LICENSES.md"
|
||||
# 링크되는 crate 의 라이선스 · 원문 (cargo tree · metadata - 의존은 위 빌드가 받아 둠)
|
||||
python "$APP/tool/licenses/rhwp_crates.py" "$WORK/ffi/Cargo.toml" "$SRC" "$VER" "$REV" "$APP/assets/licenses/RHWP_LICENSES.txt"
|
||||
if [ -n "$JJ_RHWP_CHECK" ]; then
|
||||
rm -rf "$WORK/target2"
|
||||
build "$WORK/target2" "$WORK/again.so"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,140 @@
|
|||
"""관리자 지시 (10/20, 28 지적): libjj_rhwp.so 에 실제로 링크되는 Rust crate 마다 이름 · 판 · 라이선스와 원문 · 저작권 고지를 모아
|
||||
assets/licenses/RHWP_LICENSES.txt 를 만든다 (tool/build_rhwp_android.sh 가 빌드 뒤에 부른다).
|
||||
|
||||
- 링크되는 것: cargo tree --target aarch64-linux-android -e normal,no-proc-macro (빌드 전용 · 개발 · proc-macro 는 .so 에 들어가지 않음)
|
||||
- 원문: crate 폴더 맨 위의 LICENSE* · LICENCE* · COPYING* · NOTICE* · COPYRIGHT* · UNLICENSE* (cargo metadata 의 manifest_path)
|
||||
원문 파일이 없는 crate 는 같은 라이선스의 원문을 맨 뒤 "라이선스 원문" 에 한 번씩 넣는다.
|
||||
- Cargo.lock 의 나머지는 "링크되지 않음" 목록에 적는다 - test/licenses_test.dart 가 두 목록의 합이 Cargo.lock 과 같은지 본다
|
||||
(Cargo.lock 이 바뀌었는데 이 파일을 다시 만들지 않으면 시험이 잡는다).
|
||||
- rhwp 저장소의 THIRD_PARTY_LICENSES.md (npm 쪽 포함) 는 쓰지 않는다.
|
||||
|
||||
사용: python tool/licenses/rhwp_crates.py <ffi Cargo.toml> <rhwp 소스 폴더> <rhwp 판> <rhwp 커밋> <출력 파일>
|
||||
(cargo 는 PATH · CARGO_HOME 의 것, 의존은 이미 받아 둔 것 - --offline --locked)
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
manifest, rhwp_src, version, commit, out = sys.argv[1:6]
|
||||
TARGET = 'aarch64-linux-android'
|
||||
|
||||
|
||||
def cargo(*args):
|
||||
r = subprocess.run(['cargo', *args, '--manifest-path', manifest, '--locked', '--offline'],
|
||||
capture_output=True, text=True, encoding='utf-8')
|
||||
if r.returncode != 0:
|
||||
sys.exit('cargo %s 실패:\n%s' % (args[0], r.stderr))
|
||||
return r.stdout
|
||||
|
||||
|
||||
# 1. 링크되는 crate (이름, 판)
|
||||
linked = set()
|
||||
for line in cargo('tree', '--target', TARGET, '-e', 'normal,no-proc-macro', '--prefix', 'none', '--format', '{p}').splitlines():
|
||||
m = re.match(r'^(\S+) v(\S+)', line.strip())
|
||||
if m:
|
||||
linked.add((m.group(1), m.group(2)))
|
||||
|
||||
# 2. 모든 crate 의 폴더 · 라이선스
|
||||
meta = json.loads(cargo('metadata', '--format-version', '1', '--filter-platform', TARGET))
|
||||
pkgs = {(p['name'], p['version']): p for p in meta['packages']}
|
||||
|
||||
# 3. Cargo.lock 의 모든 crate
|
||||
lock = open(os.path.join(os.path.dirname(manifest), 'Cargo.lock'), encoding='utf-8').read()
|
||||
locked = set(re.findall(r'\[\[package\]\]\s*\nname = "([^"]+)"\s*\nversion = "([^"]+)"', lock))
|
||||
missing = linked - locked
|
||||
if missing:
|
||||
sys.exit('Cargo.lock 에 없는 crate: %s' % sorted(missing))
|
||||
|
||||
ours = {'jj_rhwp'}
|
||||
NAMES = re.compile(r'^(LICEN[CS]E|COPYING|NOTICE|COPYRIGHT|UNLICENSE)', re.I)
|
||||
|
||||
|
||||
def license_files(pkg):
|
||||
d = os.path.dirname(pkg['manifest_path'])
|
||||
files = []
|
||||
for n in sorted(os.listdir(d)):
|
||||
p = os.path.join(d, n)
|
||||
if os.path.isfile(p) and NAMES.match(n):
|
||||
files.append((n, open(p, encoding='utf-8', errors='replace').read().strip()))
|
||||
if pkg.get('license_file'):
|
||||
p = os.path.join(d, pkg['license_file'])
|
||||
if os.path.isfile(p) and os.path.basename(p) not in [f for f, _ in files]:
|
||||
files.append((os.path.basename(p), open(p, encoding='utf-8', errors='replace').read().strip()))
|
||||
return files
|
||||
|
||||
|
||||
# 원문이 없는 crate 를 위해 라이선스 이름 → 원문 하나 (같은 이름의 원문 파일 하나만 가진 crate 에서)
|
||||
SPDX_FILE = {'MIT': r'LICEN[CS]E-?MIT', 'Apache-2.0': r'LICEN[CS]E-?APACHE', 'BSD-3-Clause': r'LICEN[CS]E-?BSD',
|
||||
'Zlib': r'LICEN[CS]E-?ZLIB', 'BSL-1.0': r'LICEN[CS]E-?BOOST', 'Unicode-3.0': r'LICEN[CS]E-?UNICODE'}
|
||||
spdx_text = {}
|
||||
sections = []
|
||||
seen_text = {}
|
||||
no_file = []
|
||||
for key in sorted(linked, key=lambda k: (k[0].lower(), k[1])):
|
||||
name, ver = key
|
||||
pkg = pkgs[key]
|
||||
lic = pkg.get('license') or ('see ' + pkg['license_file'] if pkg.get('license_file') else '?')
|
||||
if name in ours:
|
||||
continue
|
||||
files = license_files(pkg)
|
||||
for fname, text in files:
|
||||
for spdx, pat in SPDX_FILE.items():
|
||||
if re.fullmatch(pat + r'(\.(txt|md))?', fname, re.I) and spdx not in spdx_text:
|
||||
spdx_text[spdx] = (text, '%s %s' % (name, ver))
|
||||
body = []
|
||||
for fname, text in files:
|
||||
h = hashlib.sha256(text.encode()).hexdigest()
|
||||
if h in seen_text:
|
||||
body.append('[%s] (%s 의 %s 와 같은 글)' % (fname, *seen_text[h]))
|
||||
else:
|
||||
seen_text[h] = ('%s %s' % (name, ver), fname)
|
||||
body.append('[%s]\n%s' % (fname, text))
|
||||
if not files:
|
||||
no_file.append((name, ver, lic))
|
||||
body.append('(crate 에 원문 파일이 없음 - 아래 "라이선스 원문" 의 %s)' % lic)
|
||||
authors = ', '.join(pkg.get('authors') or [])
|
||||
sections.append('=== %s %s (%s) ===\n%s%s\n%s' % (
|
||||
name, ver, lic, 'Authors: %s\n' % authors if authors else '', pkg.get('repository') or '', '\n\n'.join(body)))
|
||||
|
||||
not_linked = sorted(locked - linked, key=lambda k: (k[0].lower(), k[1]))
|
||||
lines = [
|
||||
'rhwp v%s (https://github.com/edwardkim/rhwp, commit %s) - MIT' % (version, commit),
|
||||
'Android: libjj_rhwp.so (HWP / HWPX -> PDF), built by tool/build_rhwp_android.sh with tool/rhwp_ffi (MIT)',
|
||||
'This file lists every Rust crate linked into libjj_rhwp.so (target %s, normal dependencies,' % TARGET,
|
||||
'build-only / dev / proc-macro crates excluded) with its license and the license / notice files it ships.',
|
||||
'',
|
||||
'---------------------------------------------------------------------',
|
||||
'rhwp LICENSE',
|
||||
'---------------------------------------------------------------------',
|
||||
open(os.path.join(rhwp_src, 'LICENSE'), encoding='utf-8').read().strip(),
|
||||
'',
|
||||
'---------------------------------------------------------------------',
|
||||
'Linked crates (%d)' % len(linked - {(n, v) for n, v in linked if n in ours}),
|
||||
'---------------------------------------------------------------------',
|
||||
]
|
||||
for name, ver in sorted(linked, key=lambda k: (k[0].lower(), k[1])):
|
||||
if name not in ours:
|
||||
lines.append('linked: %s %s (%s)' % (name, ver, pkgs[(name, ver)].get('license') or '?'))
|
||||
lines += ['', '---------------------------------------------------------------------',
|
||||
'Not linked into libjj_rhwp.so (build tools, proc-macros, other platforms) - listed for completeness',
|
||||
'---------------------------------------------------------------------']
|
||||
lines += ['not linked: %s %s' % k for k in not_linked]
|
||||
lines += ['', '---------------------------------------------------------------------', 'License and notice files',
|
||||
'---------------------------------------------------------------------', '']
|
||||
lines += [s + '\n' for s in sections]
|
||||
need = sorted({l for _, _, l in no_file})
|
||||
if need:
|
||||
lines += ['---------------------------------------------------------------------', '라이선스 원문 (원문 파일이 없는 crate 용)',
|
||||
'---------------------------------------------------------------------', '']
|
||||
for lic in need:
|
||||
for spdx in re.findall(r'[A-Za-z0-9.\-]+', lic):
|
||||
if spdx in spdx_text:
|
||||
text, src = spdx_text.pop(spdx)
|
||||
lines += ['=== %s (원문: %s 의 파일) ===' % (spdx, src), text, '']
|
||||
text = '\n'.join(lines).rstrip() + '\n'
|
||||
open(out, 'wb').write(text.replace('\n', '\r\n').encode('utf-8'))
|
||||
print('linked %d · not linked %d · 원문 파일 없음 %d %s' % (len(linked) - 1, len(not_linked), len(no_file),
|
||||
[f'{n} {v}' for n, v, _ in no_file]))
|
||||
|
|
@ -1,21 +1,49 @@
|
|||
//! JJ_MKVMaker: HWP · HWPX 를 PDF 로 바꾸는 얇은 C ABI (rhwp, MIT - https://github.com/edwardkim/rhwp).
|
||||
//! 관리자 지시 (10/20): Android 에서 HWP · HWPX 를 앱의 보기 화면 (PDF) 으로 연다 - 문서를 한 번 열고, 쪽 범위마다 PDF 를 만든다
|
||||
//! (큰 문서는 앞쪽부터 몇 쪽씩 보이면서 뒤를 이어 만든다). Dart 는 dart:ffi 로 부른다 (lib/platform/android/hwp_pdf.dart).
|
||||
//! (큰 문서는 앞쪽부터 몇 쪽씩 보이면서 뒤를 이어 만든다). Dart 는 dart:ffi 로 부른다 (lib/platform/android/hwp_pdf_ffi.dart).
|
||||
//! - 오류는 종류 코드 (JJ_RHWP_E_*) 로 돌려주고, 원문 (rhwp 의 글) 은 err 로 (화면은 코드로 고른 번역 글, 원문은 [자세히] · 작업 기록).
|
||||
//! - jj_rhwp_cancel: 다른 스레드에서 지금 바꾸는 것을 멈춘다 (쪽마다 확인 - 관리자 지시 10/20, 시간 제한 뒤에도 계속 돌지 않게).
|
||||
//! 오류 글 · 돌려준 글은 jj_rhwp_free 로 푼다. 어느 함수도 panic 을 C 쪽으로 넘기지 않는다.
|
||||
|
||||
use std::ffi::{c_char, c_int, CStr, CString};
|
||||
use std::panic::{catch_unwind, AssertUnwindSafe};
|
||||
use std::path::PathBuf;
|
||||
use std::ptr::null_mut;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use rhwp::document_core::DocumentCore;
|
||||
use rhwp::renderer::pdf::PdfExportOptions;
|
||||
use rhwp::paint::RenderProfile;
|
||||
use rhwp::renderer::pdf::{svgs_to_pdf_with_options, PdfExportOptions};
|
||||
|
||||
/// 성공
|
||||
pub const JJ_RHWP_OK: c_int = 0;
|
||||
/// jj_rhwp_cancel 로 멈춤
|
||||
pub const JJ_RHWP_E_CANCELLED: c_int = 1;
|
||||
/// 파일을 읽지 못함
|
||||
pub const JJ_RHWP_E_READ: c_int = 2;
|
||||
/// HWP · HWPX 로 읽지 못함 (깨짐 · 지원하지 않는 형식)
|
||||
pub const JJ_RHWP_E_INVALID: c_int = 3;
|
||||
/// 비밀번호가 필요한 문서
|
||||
pub const JJ_RHWP_E_PASSWORD_REQUIRED: c_int = 4;
|
||||
/// 비밀번호가 틀림
|
||||
pub const JJ_RHWP_E_PASSWORD_WRONG: c_int = 5;
|
||||
/// 쪽 범위가 맞지 않음 (앱 잘못)
|
||||
pub const JJ_RHWP_E_RANGE: c_int = 6;
|
||||
/// 쪽을 그리지 못함
|
||||
pub const JJ_RHWP_E_RENDER: c_int = 7;
|
||||
/// PDF 를 저장하지 못함
|
||||
pub const JJ_RHWP_E_WRITE: c_int = 8;
|
||||
/// 변환기 안의 예기치 않은 오류 (panic)
|
||||
pub const JJ_RHWP_E_INTERNAL: c_int = 9;
|
||||
|
||||
/// 연 문서
|
||||
pub struct JjRhwpDoc {
|
||||
core: DocumentCore,
|
||||
cancel: AtomicBool,
|
||||
}
|
||||
|
||||
struct Fail(c_int, String);
|
||||
|
||||
fn text(p: *const c_char) -> Option<String> {
|
||||
if p.is_null() {
|
||||
return None;
|
||||
|
|
@ -23,19 +51,26 @@ fn text(p: *const c_char) -> Option<String> {
|
|||
Some(unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
fn set_err(err: *mut *mut c_char, msg: &str) {
|
||||
if err.is_null() {
|
||||
return;
|
||||
fn set_out(code: *mut c_int, err: *mut *mut c_char, c: c_int, msg: &str) {
|
||||
if !code.is_null() {
|
||||
unsafe { *code = c };
|
||||
}
|
||||
if !err.is_null() {
|
||||
let s = CString::new(msg.replace('\0', " ")).unwrap_or_default();
|
||||
unsafe { *err = s.into_raw() };
|
||||
}
|
||||
let c = CString::new(msg.replace('\0', " ")).unwrap_or_default();
|
||||
unsafe { *err = c.into_raw() };
|
||||
}
|
||||
|
||||
fn guarded<T>(err: *mut *mut c_char, fail: T, f: impl FnOnce() -> Result<T, String>) -> T {
|
||||
fn guarded<T>(code: *mut c_int, err: *mut *mut c_char, fail: T, f: impl FnOnce() -> Result<T, Fail>) -> T {
|
||||
match catch_unwind(AssertUnwindSafe(f)) {
|
||||
Ok(Ok(v)) => v,
|
||||
Ok(Err(e)) => {
|
||||
set_err(err, &e);
|
||||
Ok(Ok(v)) => {
|
||||
if !code.is_null() {
|
||||
unsafe { *code = JJ_RHWP_OK };
|
||||
}
|
||||
v
|
||||
}
|
||||
Ok(Err(Fail(c, msg))) => {
|
||||
set_out(code, err, c, &msg);
|
||||
fail
|
||||
}
|
||||
Err(p) => {
|
||||
|
|
@ -44,30 +79,54 @@ fn guarded<T>(err: *mut *mut c_char, fail: T, f: impl FnOnce() -> Result<T, Stri
|
|||
.map(|s| s.to_string())
|
||||
.or_else(|| p.downcast_ref::<String>().cloned())
|
||||
.unwrap_or_else(|| "panic".to_string());
|
||||
set_err(err, &format!("rhwp panic: {msg}"));
|
||||
set_out(code, err, JJ_RHWP_E_INTERNAL, &format!("rhwp panic: {msg}"));
|
||||
fail
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 여는 오류의 종류: rhwp v0.8.7 의 오류 글 (ParseError 의 Display) 로 암호 문서를 가린다
|
||||
fn open_kind(msg: &str, with_password: bool) -> c_int {
|
||||
if msg.contains("비밀번호가 필요한 암호 문서") {
|
||||
JJ_RHWP_E_PASSWORD_REQUIRED
|
||||
} else if msg.contains("암호 오류") {
|
||||
if with_password {
|
||||
JJ_RHWP_E_PASSWORD_WRONG
|
||||
} else {
|
||||
JJ_RHWP_E_PASSWORD_REQUIRED
|
||||
}
|
||||
} else {
|
||||
JJ_RHWP_E_INVALID
|
||||
}
|
||||
}
|
||||
|
||||
/// 판 (예: "0.8.7") - 라이선스 화면 · 작업 기록용. jj_rhwp_free 로 푼다
|
||||
#[no_mangle]
|
||||
pub extern "C" fn jj_rhwp_version() -> *mut c_char {
|
||||
CString::new(option_env!("JJ_RHWP_VERSION").unwrap_or("?")).unwrap_or_default().into_raw()
|
||||
}
|
||||
|
||||
/// 문서 열기 (password 는 null 이나 "" 이면 없음). 실패하면 null 과 err
|
||||
/// 문서 열기 (password 는 null 이나 "" 이면 없음). 실패하면 null 과 code · err
|
||||
#[no_mangle]
|
||||
pub extern "C" fn jj_rhwp_open(path: *const c_char, password: *const c_char, err: *mut *mut c_char) -> *mut JjRhwpDoc {
|
||||
guarded(err, null_mut(), || {
|
||||
let path = text(path).ok_or("경로 없음")?;
|
||||
let data = std::fs::read(&path).map_err(|e| e.to_string())?;
|
||||
let core = match text(password).filter(|p| !p.is_empty()) {
|
||||
pub extern "C" fn jj_rhwp_open(
|
||||
path: *const c_char,
|
||||
password: *const c_char,
|
||||
code: *mut c_int,
|
||||
err: *mut *mut c_char,
|
||||
) -> *mut JjRhwpDoc {
|
||||
guarded(code, err, null_mut(), || {
|
||||
let path = text(path).ok_or_else(|| Fail(JJ_RHWP_E_READ, "no path".into()))?;
|
||||
let data = std::fs::read(&path).map_err(|e| Fail(JJ_RHWP_E_READ, e.to_string()))?;
|
||||
let pw = text(password).filter(|p| !p.is_empty());
|
||||
let core = match &pw {
|
||||
Some(p) => DocumentCore::from_bytes_with_password(&data, p.as_bytes()),
|
||||
None => DocumentCore::from_bytes(&data),
|
||||
}
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(Box::into_raw(Box::new(JjRhwpDoc { core })))
|
||||
.map_err(|e| {
|
||||
let msg = e.to_string();
|
||||
Fail(open_kind(&msg, pw.is_some()), msg)
|
||||
})?;
|
||||
Ok(Box::into_raw(Box::new(JjRhwpDoc { core, cancel: AtomicBool::new(false) })))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -81,8 +140,19 @@ pub extern "C" fn jj_rhwp_page_count(doc: *const JjRhwpDoc) -> c_int {
|
|||
catch_unwind(AssertUnwindSafe(|| doc.core.page_count() as c_int)).unwrap_or(-1)
|
||||
}
|
||||
|
||||
/// 지금 바꾸는 것을 멈춘다 (다른 스레드에서 불러도 됨 - 다음 쪽에서 JJ_RHWP_E_CANCELLED 로 끝난다).
|
||||
/// 다음 jj_rhwp_export_pdf 는 표시를 지우고 시작한다
|
||||
#[no_mangle]
|
||||
pub extern "C" fn jj_rhwp_cancel(doc: *const JjRhwpDoc) {
|
||||
if !doc.is_null() {
|
||||
unsafe { &*doc }.cancel.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
/// [from, to) 쪽 (0 부터) 을 PDF 파일 out 으로. font_dirs 는 줄바꿈 (\n) 으로 나눈 글꼴 폴더들 (Android 는 시스템 글꼴을
|
||||
/// 스스로 찾지 않으므로 /system/fonts 와 앱이 받은 한글 글꼴 폴더를 넘긴다). 성공 0, 실패 -1 과 err
|
||||
/// 스스로 찾지 않으므로 /system/fonts 와 앱이 받은 한글 글꼴 폴더를 넘긴다). 성공 0, 실패 -1 과 code · err.
|
||||
/// rhwp 의 render_pages_pdf_native_with_options 와 같게 (Print 로 쪽마다 SVG → 한 PDF) 하되, 쪽마다 멈춤 표시를 본다
|
||||
/// (하이퍼링크는 넣지 않는다 - 보기용)
|
||||
#[no_mangle]
|
||||
pub extern "C" fn jj_rhwp_export_pdf(
|
||||
doc: *const JjRhwpDoc,
|
||||
|
|
@ -90,18 +160,20 @@ pub extern "C" fn jj_rhwp_export_pdf(
|
|||
to: c_int,
|
||||
font_dirs: *const c_char,
|
||||
out: *const c_char,
|
||||
code: *mut c_int,
|
||||
err: *mut *mut c_char,
|
||||
) -> c_int {
|
||||
guarded(err, -1, || {
|
||||
guarded(code, err, -1, || {
|
||||
if doc.is_null() {
|
||||
return Err("문서 없음".into());
|
||||
return Err(Fail(JJ_RHWP_E_RANGE, "no document".into()));
|
||||
}
|
||||
let doc = unsafe { &*doc };
|
||||
doc.cancel.store(false, Ordering::SeqCst);
|
||||
let count = doc.core.page_count() as c_int;
|
||||
if from < 0 || to > count || from >= to {
|
||||
return Err(format!("쪽 범위가 맞지 않음: {from}..{to} / {count}"));
|
||||
return Err(Fail(JJ_RHWP_E_RANGE, format!("pages {from}..{to} / {count}")));
|
||||
}
|
||||
let out = text(out).ok_or("저장할 곳 없음")?;
|
||||
let out = text(out).ok_or_else(|| Fail(JJ_RHWP_E_WRITE, "no output path".into()))?;
|
||||
let mut options = PdfExportOptions::default();
|
||||
options.font_paths = text(font_dirs)
|
||||
.unwrap_or_default()
|
||||
|
|
@ -110,12 +182,29 @@ pub extern "C" fn jj_rhwp_export_pdf(
|
|||
.filter(|s| !s.is_empty())
|
||||
.map(PathBuf::from)
|
||||
.collect();
|
||||
let pages: Vec<u32> = (from as u32..to as u32).collect();
|
||||
let bytes = doc.core.render_pages_pdf_native_with_options(&pages, &options).map_err(|e| e.to_string())?;
|
||||
let cancelled = || doc.cancel.load(Ordering::SeqCst);
|
||||
let mut svgs = Vec::with_capacity((to - from) as usize);
|
||||
for page in from as u32..to as u32 {
|
||||
if cancelled() {
|
||||
return Err(Fail(JJ_RHWP_E_CANCELLED, "cancelled".into()));
|
||||
}
|
||||
svgs.push(
|
||||
doc.core
|
||||
.render_page_svg_layer_with_profile_native(page, RenderProfile::Print)
|
||||
.map_err(|e| Fail(JJ_RHWP_E_RENDER, format!("page {}: {e}", page + 1)))?,
|
||||
);
|
||||
}
|
||||
if cancelled() {
|
||||
return Err(Fail(JJ_RHWP_E_CANCELLED, "cancelled".into()));
|
||||
}
|
||||
let bytes = svgs_to_pdf_with_options(&svgs, &options).map_err(|e| Fail(JJ_RHWP_E_RENDER, e))?;
|
||||
if cancelled() {
|
||||
return Err(Fail(JJ_RHWP_E_CANCELLED, "cancelled".into()));
|
||||
}
|
||||
// 다 쓴 뒤에 이름을 바꾼다 (보기 화면이 반쪽 파일을 읽지 않게)
|
||||
let part = format!("{out}.part");
|
||||
std::fs::write(&part, &bytes).map_err(|e| e.to_string())?;
|
||||
std::fs::rename(&part, &out).map_err(|e| e.to_string())?;
|
||||
std::fs::write(&part, &bytes).map_err(|e| Fail(JJ_RHWP_E_WRITE, e.to_string()))?;
|
||||
std::fs::rename(&part, &out).map_err(|e| Fail(JJ_RHWP_E_WRITE, e.to_string()))?;
|
||||
Ok(0)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue