import 'dart:async'; import 'dart:ffi'; import 'dart:isolate'; import 'package:ffi/ffi.dart'; 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'; /// 글꼴을 찾을 폴더 (Android 는 rhwp 가 시스템 글꼴을 스스로 찾지 않는다 - 한글은 Noto Sans/Serif CJK KR 로 대신 그림). /// 관리자 판단 (10/20): 한글 글꼴 컴포넌트를 받으면 그 폴더를 앞에 더한다 (아직 없음) static const fontDirs = ['/system/fonts']; /// 이 Dart 가 기대하는 C ABI 의 판 (tool/rhwp_ffi 의 JJ_RHWP_ABI_VERSION 과 같아야 함) static const abiVersion = 2; /// 꼭 있어야 하는 C 함수 static const requiredSymbols = [ 'jj_rhwp_abi_version', 'jj_rhwp_open', 'jj_rhwp_page_count', 'jj_rhwp_export_pdf', 'jj_rhwp_cancel', 'jj_rhwp_close', 'jj_rhwp_free', // ]; static bool? _available; /// 라이브러리를 읽을 수 있고 ABI 가 맞는지 (아니면 다른 앱으로 연다). 처음 HWP 를 열 때 한 번 본다. /// 관리자 지시 (10/20, 28 지적): 옛 .so (판 1 - code 칸 · cancel 없음) 에 새 인수를 넘기면 힙이 망가지므로, 기호와 판이 맞을 때만 static bool get available => _available ??= () { try { final lib = DynamicLibrary.open(libName); return abiCompatible( has: lib.providesSymbol, abi: () => lib.lookupFunction('jj_rhwp_abi_version')()); } catch (_) { return false; } }(); /// [requiredSymbols] 가 모두 있고 [abi] 가 [abiVersion] 인지 (시험용으로 따로 - 기호를 찾다 · 부르다 실패하면 false) static bool abiCompatible({required bool Function(String name) has, required int Function() abi}) { try { if (!requiredSymbols.every(has)) return false; return abi() == abiVersion; } catch (_) { return false; } } @override Future open(String path, {String? password}) async { 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); } } class _RhwpDoc implements HwpPdfDoc { final int _handle; @override final int pageCount; var _closed = false; /// 한 번에 하나씩 (rhwp 문서는 여러 스레드에서 함께 바꾸지 않는다) Future _queue = Future.value(); _RhwpDoc(this._handle, this.pageCount); Future _serial(Future Function() f) { final run = _queue.then((_) => f()); _queue = run.then((_) {}, onError: (Object _) {}); return run; } @override Future exportPdf(int from, int to, String out, {List fontDirs = const []}) => _serial(() async { if (_closed) throw const HwpPdfException(HwpErrorKind.closed); final h = _handle, dirs = fontDirs.join('\n'); 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 close() => _serial(() async { if (_closed) return; _closed = true; _close(_handle); }); } // ───────── C 함수 (isolate 안에서 부른다 - 핸들은 주소 숫자로 넘긴다) ───────── typedef _OpenC = Pointer Function(Pointer, Pointer, Pointer, Pointer>); typedef _CountC = Int32 Function(Pointer); typedef _CountD = int Function(Pointer); typedef _ExportC = Int32 Function(Pointer, Int32, Int32, Pointer, Pointer, Pointer, Pointer>); typedef _ExportD = int Function(Pointer, int, int, Pointer, Pointer, Pointer, Pointer>); typedef _VoidC = Void Function(Pointer); typedef _VoidD = void Function(Pointer); typedef _FreeC = Void Function(Pointer); typedef _FreeD = void Function(Pointer); DynamicLibrary _lib() => DynamicLibrary.open(RhwpEngine.libName); /// 돌려받은 오류 글을 읽고 푼다 String? _takeError(DynamicLibrary lib, Pointer> err) { final p = err.value; if (p == nullptr) return null; final s = p.toDartString(); lib.lookupFunction<_FreeC, _FreeD>('jj_rhwp_free')(p); return s; } (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(); final err = calloc>(); try { 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.fromAddress(handle)); (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(); final err = calloc>(); try { final r = export(Pointer.fromAddress(handle), from, to, d, o, code, err); final e = _takeError(lib, err); 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 _cancel(int handle) => _lib().lookupFunction<_VoidC, _VoidD>('jj_rhwp_cancel')(Pointer.fromAddress(handle)); void _close(int handle) => _lib().lookupFunction<_VoidC, _VoidD>('jj_rhwp_close')(Pointer.fromAddress(handle));