JJ_MKVMaker/tool/rhwp_ffi/src/lib.rs

270 lines
11 KiB
Rust

//! 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<HashSet<u64>>,
/// 닫는 중 - 모든 요청을 멈춘다 (되돌리지 않는다)
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<String> {
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<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)) => {
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::<String>().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) });
}
}