270 lines
11 KiB
Rust
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) });
|
|
}
|
|
}
|