//! 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_ffi.dart). //! - 오류는 종류 코드 (JJ_RHWP_E_*) 로 돌려주고, 원문 (rhwp 의 글) 은 err 로 (화면은 코드로 고른 번역 글, 원문은 [자세히] · 작업 기록). //! - jj_rhwp_cancel(요청 번호): 다른 스레드에서 그 요청만 멈춘다 (쪽마다 확인 - 관리자 지시 10/20, 시간 제한 뒤에도 계속 돌지 않게). //! jj_rhwp_cancel_all: 닫을 때 - 도는 것 · 앞으로 올 것 모두 멈춘다 (ABI 3: 예전에는 문서 하나의 깃발이라 한 요청의 시간 제한이 //! 앞 묶음까지 멈췄고, 바꾸기를 시작할 때 깃발을 지워 닫기 요청이 사라졌다 - 28 지적). //! 오류 글 · 돌려준 글은 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::collections::HashSet; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Mutex; use rhwp::document_core::DocumentCore; 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, /// 멈추라고 한 요청 번호 (끝나면 지운다) cancelled: Mutex>, /// 닫는 중 - 모든 요청을 멈춘다 (되돌리지 않는다) closing: AtomicBool, } impl JjRhwpDoc { fn stopped(&self, req: u64) -> bool { self.closing.load(Ordering::SeqCst) || self.cancelled.lock().map(|s| s.contains(&req)).unwrap_or(false) } } struct Fail(c_int, String); fn text(p: *const c_char) -> Option { if p.is_null() { return None; } Some(unsafe { CStr::from_ptr(p) }.to_string_lossy().into_owned()) } 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() }; } } fn guarded(code: *mut c_int, err: *mut *mut c_char, fail: T, f: impl FnOnce() -> Result) -> T { match catch_unwind(AssertUnwindSafe(f)) { 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) => { let msg = p .downcast_ref::<&str>() .map(|s| s.to_string()) .or_else(|| p.downcast_ref::().cloned()) .unwrap_or_else(|| "panic".to_string()); 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 } } /// 이 C ABI 의 판 - Dart (lib/platform/android/hwp_pdf_ffi.dart 의 RhwpEngine.abiVersion) 와 같아야 앱 안 보기를 쓴다 /// (관리자 지시 10/20, 28 지적: 옛 .so (판 1 - code 칸 · cancel 없음) 에 새 인수를 넘기면 힙이 망가진다). /// 함수의 인수 · 오류 코드를 바꾸면 올린다 pub const JJ_RHWP_ABI_VERSION: c_int = 3; #[no_mangle] pub extern "C" fn jj_rhwp_abi_version() -> c_int { JJ_RHWP_ABI_VERSION } /// 판 (예: "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 과 code · err #[no_mangle] 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| { let msg = e.to_string(); Fail(open_kind(&msg, pw.is_some()), msg) })?; Ok(Box::into_raw(Box::new(JjRhwpDoc { core, cancelled: Mutex::new(HashSet::new()), closing: AtomicBool::new(false) }))) }) } /// 쪽 수 (doc 이 null 이면 -1) #[no_mangle] pub extern "C" fn jj_rhwp_page_count(doc: *const JjRhwpDoc) -> c_int { if doc.is_null() { return -1; } let doc = unsafe { &*doc }; catch_unwind(AssertUnwindSafe(|| doc.core.page_count() as c_int)).unwrap_or(-1) } /// 요청 [req] 만 멈춘다 (다른 스레드에서 불러도 됨 - 도는 중이면 다음 쪽에서, 아직 시작 전이면 시작하자마자 /// JJ_RHWP_E_CANCELLED 로 끝난다). 다른 요청은 그대로 #[no_mangle] pub extern "C" fn jj_rhwp_cancel(doc: *const JjRhwpDoc, req: u64) { if !doc.is_null() { if let Ok(mut s) = unsafe { &*doc }.cancelled.lock() { s.insert(req); } } } /// 닫기 전에: 도는 요청 · 앞으로 올 요청 모두 멈춘다 (되돌리지 않음) #[no_mangle] pub extern "C" fn jj_rhwp_cancel_all(doc: *const JjRhwpDoc) { if !doc.is_null() { unsafe { &*doc }.closing.store(true, Ordering::SeqCst); } } /// 요청 [req] (부르는 쪽이 정한 번호 - jj_rhwp_cancel 로 멈출 때 씀): [from, to) 쪽 (0 부터) 을 PDF 파일 out 으로. font_dirs 는 줄바꿈 (\n) 으로 나눈 글꼴 폴더들 (Android 는 시스템 글꼴을 /// 스스로 찾지 않으므로 /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, req: u64, from: c_int, to: c_int, font_dirs: *const c_char, out: *const c_char, code: *mut c_int, err: *mut *mut c_char, ) -> c_int { guarded(code, err, -1, || { if doc.is_null() { return Err(Fail(JJ_RHWP_E_RANGE, "no document".into())); } let doc = unsafe { &*doc }; // 끝나면 (어떻게 끝나든) 이 요청의 멈춤 표시를 지운다 - 시작할 때는 지우지 않는다 (먼저 온 멈춤 · 닫기를 지키려고) struct Forget<'a>(&'a JjRhwpDoc, u64); impl Drop for Forget<'_> { fn drop(&mut self) { if let Ok(mut s) = self.0.cancelled.lock() { s.remove(&self.1); } } } let _forget = Forget(doc, req); let count = doc.core.page_count() as c_int; if from < 0 || to > count || from >= to { return Err(Fail(JJ_RHWP_E_RANGE, format!("pages {from}..{to} / {count}"))); } 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() .split('\n') .map(str::trim) .filter(|s| !s.is_empty()) .map(PathBuf::from) .collect(); let cancelled = || doc.stopped(req); 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| 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) }) } /// 문서 닫기 #[no_mangle] pub extern "C" fn jj_rhwp_close(doc: *mut JjRhwpDoc) { if !doc.is_null() { let _ = catch_unwind(AssertUnwindSafe(|| drop(unsafe { Box::from_raw(doc) }))); } } /// 이 라이브러리가 돌려준 글 (err · 판) 풀기 #[no_mangle] pub extern "C" fn jj_rhwp_free(s: *mut c_char) { if !s.is_null() { drop(unsafe { CString::from_raw(s) }); } }