11 changed files with 490 additions and 9 deletions
@ -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 @@ |
|||||||
|
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 @@ |
|||||||
|
// 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 @@ |
|||||||
|
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