mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
Reduce app size
This commit is contained in:
@@ -1,33 +1,28 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:pdf/widgets.dart';
|
||||
import 'package:venera/foundation/app.dart';
|
||||
import 'package:venera/foundation/local.dart';
|
||||
import 'package:venera/utils/image.dart';
|
||||
import 'package:venera/utils/io.dart';
|
||||
import 'package:zip_flutter/zip_flutter.dart';
|
||||
|
||||
typedef DecodeImage = Future<Image> Function(Uint8List data);
|
||||
|
||||
Future<void> _createPdfFromComic({
|
||||
required LocalComic comic,
|
||||
required String savePath,
|
||||
required String localPath,
|
||||
required DecodeImage decodeImage,
|
||||
}) async {
|
||||
final pdf = Document(
|
||||
title: comic.title,
|
||||
author: comic.subTitle ?? "",
|
||||
producer: "Venera",
|
||||
);
|
||||
|
||||
pdf.document.outline;
|
||||
var images = <String>[];
|
||||
|
||||
var baseDir = comic.directory.contains('/') || comic.directory.contains('\\')
|
||||
? comic.directory
|
||||
: FilePath.join(localPath, comic.directory);
|
||||
|
||||
// add cover
|
||||
var imageData = File(FilePath.join(baseDir, comic.cover)).readAsBytesSync();
|
||||
pdf.addPage(Page(
|
||||
build: (Context context) {
|
||||
return Image(MemoryImage(imageData), fit: BoxFit.contain);
|
||||
},
|
||||
));
|
||||
images.add(FilePath.join(baseDir, comic.cover));
|
||||
|
||||
bool multiChapters = comic.chapters != null;
|
||||
|
||||
@@ -51,42 +46,360 @@ Future<void> _createPdfFromComic({
|
||||
reorderFiles(files);
|
||||
|
||||
for (var file in files) {
|
||||
var imageData = (file as File).readAsBytesSync();
|
||||
pdf.addPage(Page(
|
||||
build: (Context context) {
|
||||
return Image(MemoryImage(imageData), fit: BoxFit.contain);
|
||||
},
|
||||
));
|
||||
images.add(file.path);
|
||||
}
|
||||
} else {
|
||||
for (var chapter in comic.chapters!.keys) {
|
||||
var files = Directory(FilePath.join(baseDir, chapter)).listSync();
|
||||
reorderFiles(files);
|
||||
for (var file in files) {
|
||||
var imageData = (file as File).readAsBytesSync();
|
||||
pdf.addPage(Page(
|
||||
build: (Context context) {
|
||||
return Image(MemoryImage(imageData), fit: BoxFit.contain);
|
||||
},
|
||||
));
|
||||
images.add(file.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final file = File(savePath);
|
||||
file.writeAsBytesSync(await pdf.save());
|
||||
var generator = PdfGenerator(
|
||||
title: comic.title,
|
||||
author: comic.subtitle,
|
||||
imagePaths: images,
|
||||
outputPath: savePath,
|
||||
decodeImage: decodeImage,
|
||||
);
|
||||
await generator.generate();
|
||||
}
|
||||
|
||||
Future<Isolate> _runIsolate(
|
||||
LocalComic comic, String savePath, SendPort sendPort) {
|
||||
var localPath = LocalManager().path;
|
||||
return Isolate.spawn<SendPort>(
|
||||
(sendPort) => overrideIO(
|
||||
() async {
|
||||
var receivePort = ReceivePort();
|
||||
sendPort.send(receivePort.sendPort);
|
||||
|
||||
Completer<Image>? completer;
|
||||
|
||||
Future<Image> decodeImage(Uint8List data) async {
|
||||
if (completer != null) {
|
||||
throw Exception('Another image is being decoded');
|
||||
}
|
||||
sendPort.send(data);
|
||||
completer = Completer();
|
||||
return completer!.future;
|
||||
}
|
||||
|
||||
receivePort.listen((message) {
|
||||
if (message is Image) {
|
||||
if (completer == null) {
|
||||
throw Exception('No image is being decoded');
|
||||
}
|
||||
completer!.complete(message);
|
||||
completer = null;
|
||||
}
|
||||
});
|
||||
|
||||
await _createPdfFromComic(
|
||||
comic: comic,
|
||||
savePath: savePath,
|
||||
localPath: localPath,
|
||||
decodeImage: decodeImage,
|
||||
);
|
||||
|
||||
sendPort.send(null);
|
||||
},
|
||||
),
|
||||
sendPort,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> createPdfFromComicIsolate({
|
||||
required LocalComic comic,
|
||||
required String savePath,
|
||||
}) async {
|
||||
var localPath = LocalManager().path;
|
||||
return Isolate.run(() => overrideIO(() async {
|
||||
return await _createPdfFromComic(
|
||||
comic: comic,
|
||||
savePath: savePath,
|
||||
localPath: localPath,
|
||||
);
|
||||
}));
|
||||
var receivePort = ReceivePort();
|
||||
SendPort? sendPort;
|
||||
Isolate? isolate;
|
||||
var completer = Completer<void>();
|
||||
receivePort.listen((message) {
|
||||
if (message is SendPort) {
|
||||
sendPort = message;
|
||||
} else if (message is Uint8List) {
|
||||
Image.decodeImage(message).then((image) {
|
||||
sendPort!.send(image);
|
||||
});
|
||||
} else if (message == null) {
|
||||
receivePort.close();
|
||||
completer.complete();
|
||||
isolate!.kill();
|
||||
}
|
||||
});
|
||||
isolate = await _runIsolate(comic, savePath, receivePort.sendPort);
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
class PdfGenerator {
|
||||
final String title;
|
||||
final String author;
|
||||
final List<String> imagePaths;
|
||||
final String outputPath;
|
||||
final DecodeImage decodeImage;
|
||||
|
||||
// PDF文件的对象ID计数器
|
||||
int _objectId = 1;
|
||||
|
||||
// 存储每个对象在PDF中的字节位置
|
||||
final Map<int, int> _objectOffsets = {};
|
||||
|
||||
static const double a4Width = 595.0; // points
|
||||
static const double a4Height = 842.0; // points
|
||||
|
||||
PdfGenerator({
|
||||
required this.title,
|
||||
required this.author,
|
||||
required this.imagePaths,
|
||||
required this.outputPath,
|
||||
required this.decodeImage,
|
||||
});
|
||||
|
||||
Future<void> generate() async {
|
||||
var file = File(outputPath);
|
||||
final output = file.openWrite();
|
||||
|
||||
int length = 0;
|
||||
|
||||
void write(String str) {
|
||||
var data = utf8.encode(str);
|
||||
output.add(data);
|
||||
length += data.length;
|
||||
}
|
||||
|
||||
void writeData(Uint8List data) {
|
||||
output.add(data);
|
||||
length += data.length;
|
||||
}
|
||||
|
||||
int getCurrentLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
// 1. 写入PDF头部
|
||||
write('%PDF-1.7\n%\xFF\xFF\xFF\xFF\n\n');
|
||||
|
||||
// 2. 写入Catalog对象
|
||||
_objectOffsets[_objectId] = getCurrentLength();
|
||||
write('$_objectId 0 obj\n');
|
||||
write('<<\n');
|
||||
write('/Type /Catalog\n');
|
||||
write('/Pages ${_objectId + 1} 0 R\n');
|
||||
write('>>\nendobj\n\n');
|
||||
|
||||
final catalogId = _objectId++;
|
||||
|
||||
// 3. 写入Pages对象
|
||||
_objectOffsets[_objectId] = getCurrentLength();
|
||||
write('$_objectId 0 obj\n');
|
||||
write('<<\n');
|
||||
write('/Type /Pages\n');
|
||||
write('/Kids [');
|
||||
final pageIds = <int>[];
|
||||
for (var i = 0; i < imagePaths.length; i++) {
|
||||
pageIds.add(_objectId + 1 + i * 3);
|
||||
write('${_objectId + 1 + i * 3} 0 R ');
|
||||
}
|
||||
write(']\n');
|
||||
write('/Count ${imagePaths.length}\n');
|
||||
write('>>\nendobj\n\n');
|
||||
|
||||
final pagesId = _objectId++;
|
||||
|
||||
// 4. 为每个图片创建Page和Image对象
|
||||
for (var i = 0; i < imagePaths.length; i++) {
|
||||
final imagePath = imagePaths[i];
|
||||
final image = await _getImage(imagePath);
|
||||
|
||||
// 写入Page对象
|
||||
_objectOffsets[_objectId] = getCurrentLength();
|
||||
write('$_objectId 0 obj\n');
|
||||
write('<<\n');
|
||||
write('/Type /Page\n');
|
||||
write('/Parent $pagesId 0 R\n');
|
||||
write('/Resources <<\n');
|
||||
write('/XObject << /Im${i + 1} ${_objectId + 1} 0 R >>\n');
|
||||
write('>>\n');
|
||||
write('/MediaBox [0 0 $a4Width $a4Height]\n');
|
||||
write('/Contents ${_objectId + 2} 0 R\n');
|
||||
write('>>\nendobj\n\n');
|
||||
|
||||
_objectId++;
|
||||
|
||||
// 写入Image对象
|
||||
_objectOffsets[_objectId] = getCurrentLength();
|
||||
write('$_objectId 0 obj\n');
|
||||
write('<<\n');
|
||||
write('/Type /XObject\n');
|
||||
write('/Subtype /Image\n');
|
||||
write('/Width ${image.width}\n');
|
||||
write('/Height ${image.height}\n');
|
||||
write('/ColorSpace /DeviceRGB\n');
|
||||
write('/BitsPerComponent 8\n');
|
||||
write('/Filter /FlateDecode\n');
|
||||
write('/Length ${image.data.length}\n');
|
||||
write('>>\nstream\n');
|
||||
writeData(image.data);
|
||||
write('\nendstream\nendobj\n\n');
|
||||
|
||||
_objectId++;
|
||||
|
||||
// 写入Contents对象(绘制图片的指令)
|
||||
_objectOffsets[_objectId] = getCurrentLength();
|
||||
write('$_objectId 0 obj\n');
|
||||
write('<<\n');
|
||||
var stream = '';
|
||||
stream += 'q\n';
|
||||
// Calculate scaling factors
|
||||
var scaleX = a4Width / image.width;
|
||||
var scaleY = a4Height / image.height;
|
||||
var scale = scaleX < scaleY ? scaleX : scaleY;
|
||||
// Calculate centering offsets
|
||||
var offsetX = (a4Width - (image.width * scale)) / 2;
|
||||
var offsetY = (a4Height - (image.height * scale)) / 2;
|
||||
// Apply transformation matrix
|
||||
stream += '1 0 0 1 $offsetX $offsetY cm\n'; // Translate
|
||||
stream += '${scale * image.width} 0 0 ${scale * image.height} 0 0 cm\n';
|
||||
stream += '/Im${i + 1} Do\n';
|
||||
stream += 'Q\n';
|
||||
var streamData = utf8.encode(stream);
|
||||
write('/Length ${streamData.length}\n');
|
||||
write('>>\nstream\n');
|
||||
writeData(streamData);
|
||||
write('endstream\nendobj\n\n');
|
||||
|
||||
_objectId++;
|
||||
}
|
||||
|
||||
// 5. 写入Info对象(元数据)
|
||||
final infoId = _objectId;
|
||||
_objectOffsets[_objectId] = getCurrentLength();
|
||||
write('$_objectId 0 obj\n');
|
||||
write('<<\n');
|
||||
write('/Title <');
|
||||
writeData(_toPdfString(title));
|
||||
write('>\n');
|
||||
write('/Author <');
|
||||
writeData(_toPdfString(author));
|
||||
write('>\n');
|
||||
write('/Producer (venera v${App.version})\n');
|
||||
write('/CreationDate (D:${_formatDateTime(DateTime.now())})\n');
|
||||
write('>>\nendobj\n\n');
|
||||
|
||||
_objectId++;
|
||||
|
||||
// 6. 写入交叉引用表
|
||||
final xrefOffset = getCurrentLength();
|
||||
write('xref\n');
|
||||
write('0 $_objectId\n');
|
||||
write('0000000000 65535 f\r\n');
|
||||
|
||||
for (var i = 1; i < _objectId; i++) {
|
||||
final offset = _objectOffsets[i]!;
|
||||
write('${offset.toString().padLeft(10, '0')} 00000 n\r\n'); // 使用\r\n
|
||||
}
|
||||
|
||||
// 7. 写入文件尾部
|
||||
write('trailer\n');
|
||||
write('<<\n');
|
||||
write('/Size $_objectId\n');
|
||||
write('/Root $catalogId 0 R\n');
|
||||
write('/Info $infoId 0 R\n');
|
||||
write('>>\n');
|
||||
write('startxref\n');
|
||||
write('$xrefOffset\n');
|
||||
write('%%EOF\n');
|
||||
|
||||
await output.close();
|
||||
}
|
||||
|
||||
int _codeUnitForDigit(int digit) =>
|
||||
digit < 10 ? digit + 0x30 : digit + 0x61 - 10;
|
||||
|
||||
Uint8List _toPdfString(String str) {
|
||||
Uint8List data;
|
||||
try {
|
||||
data = latin1.encode(str);
|
||||
} catch (e) {
|
||||
data = Uint8List.fromList(<int>[0xfe, 0xff] + _encodeUtf16be(str));
|
||||
}
|
||||
var result = <int>[];
|
||||
for (final byte in data) {
|
||||
result.add(_codeUnitForDigit((byte & 0xF0) >> 4));
|
||||
result.add(_codeUnitForDigit(byte & 0x0F));
|
||||
}
|
||||
return Uint8List.fromList(result);
|
||||
}
|
||||
|
||||
List<int> _encodeUtf16be(String str) {
|
||||
const unicodeReplacementCharacterCodePoint = 0xfffd;
|
||||
const unicodeByteZeroMask = 0xff;
|
||||
const unicodeByteOneMask = 0xff00;
|
||||
const unicodeValidRangeMax = 0x10ffff;
|
||||
const unicodePlaneOneMax = 0xffff;
|
||||
const unicodeUtf16ReservedLo = 0xd800;
|
||||
const unicodeUtf16ReservedHi = 0xdfff;
|
||||
const unicodeUtf16Offset = 0x10000;
|
||||
const unicodeUtf16SurrogateUnit0Base = 0xd800;
|
||||
const unicodeUtf16SurrogateUnit1Base = 0xdc00;
|
||||
const unicodeUtf16HiMask = 0xffc00;
|
||||
const unicodeUtf16LoMask = 0x3ff;
|
||||
|
||||
final encoding = <int>[];
|
||||
|
||||
void add(int unit) {
|
||||
encoding.add((unit & unicodeByteOneMask) >> 8);
|
||||
encoding.add(unit & unicodeByteZeroMask);
|
||||
}
|
||||
|
||||
for (final unit in str.codeUnits) {
|
||||
if ((unit >= 0 && unit < unicodeUtf16ReservedLo) ||
|
||||
(unit > unicodeUtf16ReservedHi && unit <= unicodePlaneOneMax)) {
|
||||
add(unit);
|
||||
} else if (unit > unicodePlaneOneMax && unit <= unicodeValidRangeMax) {
|
||||
final base = unit - unicodeUtf16Offset;
|
||||
add(unicodeUtf16SurrogateUnit0Base +
|
||||
((base & unicodeUtf16HiMask) >> 10));
|
||||
add(unicodeUtf16SurrogateUnit1Base + (base & unicodeUtf16LoMask));
|
||||
} else {
|
||||
add(unicodeReplacementCharacterCodePoint);
|
||||
}
|
||||
}
|
||||
return encoding;
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
String _formatDateTime(DateTime dt) {
|
||||
return dt
|
||||
.toUtc()
|
||||
.toString()
|
||||
.replaceAll('-', '')
|
||||
.replaceAll(':', '')
|
||||
.replaceAll(' ', '')
|
||||
.replaceAll('.', '')
|
||||
.substring(0, 14);
|
||||
}
|
||||
|
||||
Future<({int width, int height, Uint8List data})> _getImage(
|
||||
String imagePath) async {
|
||||
var data = await File(imagePath).readAsBytes();
|
||||
var image = await decodeImage(data);
|
||||
var width = image.width;
|
||||
var height = image.height;
|
||||
data = Uint8List(width * height * 3);
|
||||
for (var i = 0; i < width * height; i++) {
|
||||
var pixel = image.getPixelAtIndex(i); // RGBA
|
||||
data[i * 3] = pixel & 0xFF; // R
|
||||
data[i * 3 + 1] = (pixel >> 8) & 0xFF; // G
|
||||
data[i * 3 + 2] = (pixel >> 16) & 0xFF; // B
|
||||
}
|
||||
data = tdeflCompressData(data, true, true, 9);
|
||||
return (width: width, height: height, data: data);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user