JJ_MKVMaker/lib/core/hwp_source.dart

132 lines
4.9 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'dart:math' as math;
import 'package:path/path.dart' as p;
import 'package:pdfrx/pdfrx.dart';
import 'reader_sources.dart';
import 'vfs.dart';
import '../l10n/tr.dart';
/// HWP · HWPX 확장자 (앱 안에서 PDF 로 바꿔 보기 - Android, rhwp)
const hwpExtensions = ['hwp', 'hwpx'];
/// HWP · HWPX → PDF 엔진 (Android: APK 의 libjj_rhwp.so - lib/platform/android/hwp_pdf_ffi.dart, 시험: 가짜)
abstract class HwpPdfEngine {
/// 문서 열기 (암호가 있으면 [password]). 실패하면 [HwpPdfException]
Future<HwpPdfDoc> open(String path, {String? password});
}
/// 연 문서: 쪽 수와 쪽 범위 → PDF
abstract class HwpPdfDoc {
int get pageCount;
/// [from] 부터 [to] 앞까지 (0 부터) 를 PDF 파일 [out] 으로. [fontDirs] 는 글꼴을 찾을 폴더들
Future<void> exportPdf(int from, int to, String out, {List<String> fontDirs = const []});
Future<void> close();
}
class HwpPdfException implements Exception {
final String message;
const HwpPdfException(this.message);
@override
String toString() => message;
}
/// 관리자 지시 (10/20): HWP · HWPX 를 보기 화면으로 - 큰 문서 (편람 383쪽은 PC 에서도 14분) 를 통째로 바꾸지 않고,
/// 보는 쪽이 든 [chunk] 쪽 묶음만 PDF 로 바꿔 보이면서 다음 묶음을 뒤에서 미리 만든다. 묶음마다 [timeout],
/// 닫으면 (dispose) 더 만들지 않는다. 바꾸는 중의 쪽은 보기 화면이 기다리는 표시를 보이고, 실패한 묶음은 그 쪽에 이유가 보인다.
class HwpSource extends ReaderSource {
final String path;
final HwpPdfDoc doc;
final Directory work;
final List<String> fontDirs;
final int chunk;
final Duration timeout;
@override
final String title;
final _chunks = <int, Future<PdfDocument>>{};
final _opened = <PdfDocument>[];
var _disposed = false;
HwpSource._(this.path, this.doc, this.work, this.title, {required this.fontDirs, required this.chunk, required this.timeout});
/// 문서를 연다 (WebDAV 등은 [tempDir] 로 받은 뒤). 열지 못하면 [HwpPdfException]
static Future<HwpSource> open(String path,
{required HwpPdfEngine engine,
required String tempDir,
String? password,
List<String> fontDirs = const [],
int chunk = 5,
Duration timeout = const Duration(minutes: 3)}) async {
final local = await vLocalCopy(path, tempDir);
final doc = await engine.open(local, password: password);
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);
}
@override
int get length => doc.pageCount;
@override
String pageName(int i) => '${i + 1} / $length';
/// [k] 번째 묶음의 PDF (한 번만 만든다 - 실패하면 다음에 다시)
Future<PdfDocument> _chunk(int k) => _chunks[k] ??= _make(k).onError<Object>((e, st) {
_chunks.remove(k);
Error.throwWithStackTrace(e, st);
});
Future<PdfDocument> _make(int k) async {
if (_disposed) throw StateError(tr('닫힌 문서입니다'));
final from = k * chunk, to = math.min(from + chunk, length);
final out = p.join(work.path, 'p$k.pdf');
try {
await doc.exportPdf(from, to, out, fontDirs: fontDirs).timeout(timeout);
} on TimeoutException {
throw HwpPdfException(trf('{0}~{1}쪽을 {2}초 안에 바꾸지 못했습니다', [from + 1, to, timeout.inSeconds]));
}
if (_disposed) throw StateError(tr('닫힌 문서입니다'));
await pdfrxFlutterInitialize();
final pdf = await PdfDocument.openFile(out);
if (_disposed) {
await pdf.dispose();
throw StateError(tr('닫힌 문서입니다'));
}
_opened.add(pdf);
return pdf;
}
@override
Future<ReaderImage> load(int i, {required int maxWidth}) async {
final k = i ~/ chunk;
final pdf = await _chunk(k);
// 다음 묶음을 미리 (넘겨 볼 때 기다리지 않게) - 실패해도 여기서는 알리지 않는다 (그 쪽을 볼 때 보임)
if ((k + 1) * chunk < length) unawaited(_chunk(k + 1).then((_) {}, onError: (Object _) {}));
final page = pdf.pages[i - k * chunk];
final w = maxWidth.toDouble();
final h = w * page.height / page.width;
final image = await page.render(fullWidth: w, fullHeight: h, backgroundColor: 0xffffffff);
if (image == null) return const ReaderImage();
try {
return ReaderImage(image: await image.createImage());
} finally {
image.dispose();
}
}
@override
Future<void> dispose() async {
_disposed = true;
for (final d in _opened) {
await d.dispose();
}
_opened.clear();
await doc.close(); // 바꾸는 중이면 그것이 끝난 뒤에 닫힌다 (엔진이 한 번에 하나씩)
try {
if (work.existsSync()) work.deleteSync(recursive: true);
} catch (_) {}
}
}