11 changed files with 490 additions and 9 deletions
@ -0,0 +1,157 @@
@@ -0,0 +1,157 @@
|
||||
import 'package:file_picker/file_picker.dart'; |
||||
import 'package:win_text_editor/framework/controllers/logger.dart'; |
||||
import 'package:win_text_editor/modules/outline/models/outline_node.dart'; |
||||
import 'package:win_text_editor/shared/base/base_content_controller.dart'; |
||||
|
||||
// Import the PDFium bindings |
||||
import 'pdfium_bindings.dart'; |
||||
|
||||
class PdfParseController extends BaseContentController { |
||||
String _filePath = ''; |
||||
String? _errorMessage; |
||||
PdfDocument? _pdfDocument; |
||||
|
||||
Map<String, String> _contentSections = {}; |
||||
List<List<List<String>>> _tables = []; // 修正为三维列表(多个表格,每个表格是行列表) |
||||
List<String> _nonTableContent = []; |
||||
|
||||
String get filePath => _filePath; |
||||
String? get errorMessage => _errorMessage; |
||||
Map<String, String> get contentSections => _contentSections; |
||||
List<List<List<String>>> get tables => _tables; |
||||
|
||||
@override |
||||
void dispose() { |
||||
_pdfDocument?.dispose(); |
||||
super.dispose(); |
||||
} |
||||
|
||||
Future<void> pickFile() async { |
||||
final result = await FilePicker.platform.pickFiles( |
||||
type: FileType.custom, |
||||
allowedExtensions: ['pdf'], |
||||
); |
||||
if (result != null) { |
||||
_filePath = result.files.single.path!; |
||||
notifyListeners(); |
||||
await _loadPdfContent(); |
||||
} |
||||
} |
||||
|
||||
Future<void> setFilePath(String path) async { |
||||
_filePath = path; |
||||
notifyListeners(); |
||||
await _loadPdfContent(); |
||||
} |
||||
|
||||
Future<void> _loadPdfContent() async { |
||||
try { |
||||
// Dispose of previous document if exists |
||||
_pdfDocument?.dispose(); |
||||
_pdfDocument = null; |
||||
|
||||
// Load new document |
||||
_pdfDocument = PdfDocument.fromFile(_filePath); |
||||
|
||||
// Extract text from all pages |
||||
String allText = ''; |
||||
for (var i = 0; i < _pdfDocument!.pageCount; i++) { |
||||
allText += _pdfDocument!.getPageText(i) + '\n'; |
||||
} |
||||
|
||||
await _extractDocumentSections(allText); |
||||
notifyListeners(); |
||||
} catch (e) { |
||||
_errorMessage = 'Failed to load PDF: ${e.toString()}'; |
||||
notifyListeners(); |
||||
Logger().error(_errorMessage!); |
||||
} |
||||
} |
||||
|
||||
// Rest of the methods remain the same... |
||||
Future<void> _extractDocumentSections(String allText) async { |
||||
_contentSections.clear(); |
||||
_tables.clear(); |
||||
_nonTableContent.clear(); |
||||
|
||||
// 分割文本为行(处理可能的空白行) |
||||
final lines = |
||||
allText.split('\n').map((line) => line.trim()).where((line) => line.isNotEmpty).toList(); |
||||
|
||||
// 提取表格和非表格内容 |
||||
_extractTablesAndNonTables(lines); |
||||
|
||||
// 存储非表格内容 |
||||
_contentSections['表格外'] = _nonTableContent.join('\n'); |
||||
} |
||||
|
||||
void _extractTablesAndNonTables(List<String> lines) { |
||||
List<List<String>>? currentTable; |
||||
bool inTable = false; |
||||
|
||||
for (final line in lines) { |
||||
// 检测表格行(至少2列,通过制表符或2+空格分隔) |
||||
final columns = line.split(RegExp(r'\t|\s{2,}')).where((e) => e.isNotEmpty).toList(); |
||||
|
||||
if (columns.length >= 2) { |
||||
// 表格行处理 |
||||
inTable = true; |
||||
currentTable ??= []; |
||||
currentTable.add(columns); |
||||
} else { |
||||
// 非表格行处理 |
||||
if (inTable) { |
||||
// 表格结束,保存当前表格(至少2行才视为有效表格) |
||||
if (currentTable != null && currentTable.length >= 2) { |
||||
_tables.add(currentTable); |
||||
} |
||||
currentTable = null; |
||||
inTable = false; |
||||
} |
||||
// 添加到非表格内容 |
||||
_nonTableContent.add(line); |
||||
} |
||||
} |
||||
|
||||
// 处理文档末尾的表格 |
||||
if (currentTable != null && currentTable.length >= 2) { |
||||
_tables.add(currentTable); |
||||
} |
||||
} |
||||
|
||||
String? genContentString(List<String> sections) { |
||||
final buffer = StringBuffer(); |
||||
|
||||
for (final section in sections) { |
||||
if (section == '表格' && _tables.isNotEmpty) { |
||||
buffer.writeln('===== 表格内容 ====='); |
||||
for (var tableIndex = 0; tableIndex < _tables.length; tableIndex++) { |
||||
buffer.writeln('----- 表格 ${tableIndex + 1} -----'); |
||||
final table = _tables[tableIndex]; |
||||
for (var rowIndex = 0; rowIndex < table.length; rowIndex++) { |
||||
buffer.writeln('行 ${rowIndex + 1}: ${table[rowIndex].join(' | ')}'); |
||||
} |
||||
buffer.writeln(); |
||||
} |
||||
buffer.writeln(); |
||||
} else if (_contentSections.containsKey(section)) { |
||||
buffer.writeln('===== $section ====='); |
||||
buffer.writeln(_contentSections[section]); |
||||
buffer.writeln(); |
||||
} |
||||
} |
||||
|
||||
return buffer.isEmpty ? null : buffer.toString(); |
||||
} |
||||
|
||||
@override |
||||
Future<void> onOpenFile(String filePath, {dynamic appendArg}) async { |
||||
await setFilePath(filePath); |
||||
} |
||||
|
||||
@override |
||||
void onOpenFolder(String folderPath) {} |
||||
|
||||
@override |
||||
void onDropOutlineNode(OutlineNode node) {} |
||||
} |
@ -0,0 +1,162 @@
@@ -0,0 +1,162 @@
|
||||
import 'dart:ffi'; |
||||
import 'dart:io'; |
||||
|
||||
import 'package:ffi/ffi.dart'; |
||||
|
||||
// PDFium FFI Bindings |
||||
final DynamicLibrary pdfiumLib = _loadPdfiumLibrary(); |
||||
|
||||
DynamicLibrary _loadPdfiumLibrary() { |
||||
if (Platform.isWindows) { |
||||
return DynamicLibrary.open('pdfium.dll'); |
||||
} else if (Platform.isMacOS) { |
||||
return DynamicLibrary.open('libpdfium.dylib'); |
||||
} else if (Platform.isLinux) { |
||||
return DynamicLibrary.open('libpdfium.so'); |
||||
} |
||||
throw UnsupportedError('Unsupported platform'); |
||||
} |
||||
|
||||
// PDFium Function Bindings |
||||
class PDFium { |
||||
static final FPDF_InitLibrary = pdfiumLib.lookupFunction<Void Function(), void Function()>( |
||||
'FPDF_InitLibrary', |
||||
); |
||||
static final FPDF_DestroyLibrary = pdfiumLib.lookupFunction<Void Function(), void Function()>( |
||||
'FPDF_DestroyLibrary', |
||||
); |
||||
static final FPDF_LoadDocument = pdfiumLib.lookupFunction< |
||||
Pointer<Void> Function(Pointer<Utf8>, Pointer<Utf8>), |
||||
Pointer<Void> Function(Pointer<Utf8>, Pointer<Utf8>) |
||||
>('FPDF_LoadDocument'); |
||||
static final FPDF_CloseDocument = pdfiumLib |
||||
.lookupFunction<Void Function(Pointer<Void>), void Function(Pointer<Void>)>( |
||||
'FPDF_CloseDocument', |
||||
); |
||||
static final FPDF_GetPageCount = pdfiumLib |
||||
.lookupFunction<Int32 Function(Pointer<Void>), int Function(Pointer<Void>)>( |
||||
'FPDF_GetPageCount', |
||||
); |
||||
static final FPDF_LoadPage = pdfiumLib.lookupFunction< |
||||
Pointer<Void> Function(Pointer<Void>, Int32), |
||||
Pointer<Void> Function(Pointer<Void>, int) |
||||
>('FPDF_LoadPage'); |
||||
static final FPDF_ClosePage = pdfiumLib |
||||
.lookupFunction<Void Function(Pointer<Void>), void Function(Pointer<Void>)>('FPDF_ClosePage'); |
||||
static final FPDFText_LoadPage = pdfiumLib |
||||
.lookupFunction<Pointer<Void> Function(Pointer<Void>), Pointer<Void> Function(Pointer<Void>)>( |
||||
'FPDFText_LoadPage', |
||||
); |
||||
static final FPDFText_ClosePage = pdfiumLib |
||||
.lookupFunction<Void Function(Pointer<Void>), void Function(Pointer<Void>)>( |
||||
'FPDFText_ClosePage', |
||||
); |
||||
static final FPDFText_CountChars = pdfiumLib |
||||
.lookupFunction<Int32 Function(Pointer<Void>), int Function(Pointer<Void>)>( |
||||
'FPDFText_CountChars', |
||||
); |
||||
|
||||
// Corrected FPDFText_GetText binding - uses Uint16 for Unicode text |
||||
static final FPDFText_GetText = pdfiumLib.lookupFunction< |
||||
Int32 Function(Pointer<Void>, Int32, Int32, Pointer<Uint16>), |
||||
int Function(Pointer<Void>, int, int, Pointer<Uint16>) |
||||
>('FPDFText_GetText'); |
||||
|
||||
static final FPDF_GetLastError = pdfiumLib.lookupFunction<Uint32 Function(), int Function()>( |
||||
'FPDF_GetLastError', |
||||
); |
||||
} |
||||
|
||||
// Helper class to manage PDFium resources |
||||
class PdfDocument { |
||||
final Pointer<Void> _docPtr; |
||||
|
||||
PdfDocument._(this._docPtr); |
||||
|
||||
factory PdfDocument.fromFile(String filePath) { |
||||
PDFium.FPDF_InitLibrary(); |
||||
|
||||
final filePathPtr = filePath.toNativeUtf8(); |
||||
final passwordPtr = ''.toNativeUtf8(); |
||||
|
||||
final docPtr = PDFium.FPDF_LoadDocument(filePathPtr, passwordPtr); |
||||
|
||||
calloc.free(filePathPtr); |
||||
calloc.free(passwordPtr); |
||||
|
||||
if (docPtr == nullptr) { |
||||
final error = PDFium.FPDF_GetLastError(); |
||||
throw Exception('Failed to load PDF: ${_getErrorDescription(error)}'); |
||||
} |
||||
|
||||
return PdfDocument._(docPtr); |
||||
} |
||||
|
||||
int get pageCount => PDFium.FPDF_GetPageCount(_docPtr); |
||||
|
||||
String getPageText(int pageIndex) { |
||||
final pagePtr = PDFium.FPDF_LoadPage(_docPtr, pageIndex); |
||||
if (pagePtr == nullptr) { |
||||
throw Exception('Failed to load page $pageIndex'); |
||||
} |
||||
|
||||
try { |
||||
final textPagePtr = PDFium.FPDFText_LoadPage(pagePtr); |
||||
if (textPagePtr == nullptr) { |
||||
throw Exception('Failed to load text for page $pageIndex'); |
||||
} |
||||
|
||||
try { |
||||
final charCount = PDFium.FPDFText_CountChars(textPagePtr); |
||||
if (charCount == 0) { |
||||
return ''; |
||||
} |
||||
|
||||
// Allocate buffer for UTF-16 characters (+1 for null terminator) |
||||
final buffer = calloc<Uint16>(charCount + 1); |
||||
try { |
||||
final copied = PDFium.FPDFText_GetText(textPagePtr, 0, charCount, buffer); |
||||
if (copied <= 0) { |
||||
return ''; |
||||
} |
||||
|
||||
// Convert UTF-16 to Dart string |
||||
return _utf16PointerToString(buffer, copied); |
||||
} finally { |
||||
calloc.free(buffer); |
||||
} |
||||
} finally { |
||||
PDFium.FPDFText_ClosePage(textPagePtr); |
||||
} |
||||
} finally { |
||||
PDFium.FPDF_ClosePage(pagePtr); |
||||
} |
||||
} |
||||
|
||||
static String _utf16PointerToString(Pointer<Uint16> pointer, int length) { |
||||
final units = pointer.asTypedList(length); |
||||
return String.fromCharCodes(units); |
||||
} |
||||
|
||||
void dispose() { |
||||
PDFium.FPDF_CloseDocument(_docPtr); |
||||
PDFium.FPDF_DestroyLibrary(); |
||||
} |
||||
|
||||
static String _getErrorDescription(int errorCode) { |
||||
switch (errorCode) { |
||||
case 1: |
||||
return 'File not found or could not be opened'; |
||||
case 2: |
||||
return 'File not in PDF format or corrupted'; |
||||
case 3: |
||||
return 'Password required or incorrect password'; |
||||
case 4: |
||||
return 'Unsupported security scheme'; |
||||
case 5: |
||||
return 'Page not found or content error'; |
||||
default: |
||||
return 'Unknown error'; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,61 @@
@@ -0,0 +1,61 @@
|
||||
// uft_component_right_side.dart |
||||
import 'package:flutter/material.dart'; |
||||
import 'package:win_text_editor/modules/pdf_parse/controllers/pdf_parse_controller.dart'; |
||||
import 'package:win_text_editor/shared/components/code_generation_components.dart'; |
||||
|
||||
class PdfParseOutput extends StatefulWidget { |
||||
final PdfParseController controller; |
||||
final TextEditingController codeController; |
||||
|
||||
const PdfParseOutput({super.key, required this.controller, required this.codeController}); |
||||
|
||||
@override |
||||
State<PdfParseOutput> createState() => _PdfParseOutputState(); |
||||
} |
||||
|
||||
class _PdfParseOutputState extends State<PdfParseOutput> { |
||||
String? _selectedOperation; |
||||
|
||||
@override |
||||
void initState() { |
||||
super.initState(); |
||||
widget.controller.addListener(_updateDisplay); |
||||
} |
||||
|
||||
@override |
||||
void dispose() { |
||||
widget.controller.removeListener(_updateDisplay); |
||||
super.dispose(); |
||||
} |
||||
|
||||
void _updateDisplay() { |
||||
if (_selectedOperation != null) { |
||||
widget.codeController.text = widget.controller.genContentString([_selectedOperation!]) ?? ''; |
||||
} else { |
||||
widget.codeController.text = ''; |
||||
} |
||||
} |
||||
|
||||
void _selectOperation(String? operation) { |
||||
setState(() { |
||||
_selectedOperation = operation; |
||||
_updateDisplay(); |
||||
}); |
||||
} |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
final operations = ['表格内', '表格外']; |
||||
|
||||
return CodeGenerationSection( |
||||
title: '生成内容', |
||||
codeController: widget.codeController, |
||||
onNodeDropped: (node) => widget.controller.onDropOutlineNode(node), |
||||
child: OperationRadioSection( |
||||
operations: operations, |
||||
selectedOperation: _selectedOperation, |
||||
onOperationSelected: _selectOperation, |
||||
), |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,83 @@
@@ -0,0 +1,83 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:provider/provider.dart'; |
||||
import 'package:win_text_editor/framework/controllers/tab_items_controller.dart'; |
||||
import 'package:win_text_editor/modules/pdf_parse/controllers/pdf_parse_controller.dart'; |
||||
|
||||
import 'pdf_parse_output.dart'; |
||||
|
||||
class PdfParseView extends StatefulWidget { |
||||
final String tabId; |
||||
const PdfParseView({super.key, required this.tabId}); |
||||
|
||||
@override |
||||
State<PdfParseView> createState() => _PdfParseViewState(); |
||||
} |
||||
|
||||
class _PdfParseViewState extends State<PdfParseView> { |
||||
late final PdfParseController _controller; |
||||
final TextEditingController _contentController = TextEditingController(); |
||||
bool _isControllerFromTabManager = false; |
||||
|
||||
get tabManager => Provider.of<TabItemsController>(context, listen: false); |
||||
|
||||
@override |
||||
void initState() { |
||||
super.initState(); |
||||
|
||||
final controllerFromManager = tabManager.getController(widget.tabId); |
||||
if (controllerFromManager != null) { |
||||
_controller = controllerFromManager; |
||||
_isControllerFromTabManager = true; |
||||
} else { |
||||
_controller = PdfParseController(); |
||||
_isControllerFromTabManager = false; |
||||
tabManager.registerController(widget.tabId, _controller); |
||||
} |
||||
} |
||||
|
||||
@override |
||||
void dispose() { |
||||
if (!_isControllerFromTabManager) { |
||||
_controller.dispose(); |
||||
} |
||||
super.dispose(); |
||||
} |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return ChangeNotifierProvider.value( |
||||
value: _controller, |
||||
child: Consumer<PdfParseController>( |
||||
builder: (context, controller, child) { |
||||
return Padding( |
||||
padding: const EdgeInsets.all(8.0), |
||||
child: Column( |
||||
crossAxisAlignment: CrossAxisAlignment.start, |
||||
children: [ |
||||
// 上方的文件路径输入框 |
||||
TextField( |
||||
decoration: InputDecoration( |
||||
labelText: 'PDF File', |
||||
hintText: 'Select an PDF file', |
||||
suffixIcon: IconButton( |
||||
icon: const Icon(Icons.folder_open), |
||||
onPressed: _controller.pickFile, |
||||
), |
||||
border: const OutlineInputBorder(), |
||||
errorText: controller.errorMessage, |
||||
), |
||||
controller: TextEditingController(text: _controller.filePath), |
||||
readOnly: true, |
||||
), |
||||
const SizedBox(height: 4), |
||||
Expanded( |
||||
child: PdfParseOutput(codeController: _contentController, controller: controller), |
||||
), |
||||
], |
||||
), |
||||
); |
||||
}, |
||||
), |
||||
); |
||||
} |
||||
} |
Loading…
Reference in new issue