For output see the attached imageattached image, the output borders are not good enough, can someone tell how to optimise this for more accurate borders and text alignment??
For output see the attached image, the output borders are not good enough, can someone tell how to optimise this for more accurate borders and text alignment??
For output see the attached image, the output borders are not good enough, can someone tell how to optimise this for more accurate borders and text alignment??
` const params = new ExtractPDFParams({ elementsToExtract: [ExtractElementType.TEXT, ExtractElementType.TABLES], addCharInfo: true,
const params = new ExtractPDFParams({
elementsToExtract: [ExtractElementType.TEXT, ExtractElementType.TABLES],
addCharInfo: true,
});`;
` import 'dart:convert';
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:http/http.dart' as http;
import 'package:newflutter/utils/constants.dart';
import 'package:open_file/open_file.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
class PdfEditorScreen extends StatefulWidget {
const PdfEditorScreen({super.key});
@override
State<PdfEditorScreen> createState() => _PdfEditorScreenState();
}
class _PdfEditorScreenState extends State<PdfEditorScreen> {
Map<String, dynamic>? _jsonData;
bool _loading = false;
String? _error;
final Map<int, TextEditingController> _controllers = {};
late final pw.Font robotoRegular;
late final pw.Font robotoBold;
@override
void initState() {
super.initState();
_loadFonts();
}
Future<void> _loadFonts() async {
robotoRegular =
pw.Font.ttf(await rootBundle.load("assets/fonts/Roboto-Regular.ttf"));
robotoBold =
pw.Font.ttf(await rootBundle.load("assets/fonts/Roboto-Bold.ttf"));
}
// Load JSON from assets
Future<void> _loadFromAssets() async {
setState(() {
_loading = true;
_error = null;
_jsonData = null;
_controllers.clear();
});
try {
final raw = await rootBundle.loadString('assets/structuredDataM.json');
final jsonMap = json.decode(raw) as Map<String, dynamic>;
_initData(jsonMap);
} catch (e) {
setState(() => _error = 'Error loading assets: $e');
} finally {
setState(() => _loading = false);
}
}
// Upload PDF → API → JSON
Future<void> _pickAndUploadPdf() async {
setState(() {
_loading = true;
_error = null;
_jsonData = null;
_controllers.clear();
});
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['pdf'],
);
if (result == null) {
setState(() => _loading = false);
return;
}
final file = result.files.first;
final uri = Uri.parse("${Constants().baseUrl}/test/convert");
final request = http.MultipartRequest('POST', uri)
..files.add(await http.MultipartFile.fromPath('file', file.path!,
filename: file.name));
final streamed = await request.send();
final response = await http.Response.fromStream(streamed);
if (response.statusCode == 200 || response.statusCode == 201) {
final jsonMap = json.decode(response.body) as Map<String, dynamic>;
_initData(jsonMap);
} else {
setState(
() => _error = 'Something went wrong! Please try after some time ');
}
} catch (e) {
setState(
() => _error = 'Something went wrong! Please try after some time:');
} finally {
setState(() => _loading = false);
}
}
void _initData(Map<String, dynamic> parsed) {
final elements = parsed['elements'] as List;
for (int i = 0; i < elements.length; i++) {
final e = elements[i];
if ((e['Text'] ?? '').toString().isNotEmpty) {
_controllers[i] = TextEditingController(text: e['Text']);
}
}
setState(() => _jsonData = parsed);
}
@override
void dispose() {
for (final c in _controllers.values) {
c.dispose();
}
super.dispose();
}
// ---------- UI ----------
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("PDF Editor"),
actions: [
IconButton(
onPressed: _loadFromAssets,
icon: const Icon(Icons.folder_open),
tooltip: 'Load JSON from assets',
),
IconButton(
onPressed: _pickAndUploadPdf,
icon: const Icon(Icons.cloud_upload),
tooltip: 'Upload PDF → Convert',
),
],
),
body: _loading
? const Center(child: CircularProgressIndicator())
: _error != null
? Center(child: Text(_error!))
: _jsonData == null
? const Center(child: Text("Load or upload PDF JSON"))
: _buildEditor(),
floatingActionButton: _jsonData == null
? null
: FloatingActionButton.extended(
onPressed: _makePdfFromEditedContent,
label: const Text("Make PDF"),
icon: const Icon(Icons.picture_as_pdf),
),
);
}
Widget _buildEditor() {
final page = _jsonData!['pages'][0];
final pageWidth = (page['width'] as num).toDouble();
final pageHeight = (page['height'] as num).toDouble();
final elements = _jsonData!['elements'] as List;
// 1) Precompute table BBoxes
final List<Map<String, double>> tableBboxes = [];
for (final e in elements) {
final path = e['Path'] ?? '';
if (path.toString().contains('Table') &&
e['attributes']?['BBox'] != null) {
final bbox = e['attributes']['BBox'] as List;
tableBboxes.add({
'left': (bbox[0] as num).toDouble(),
'bottom': (bbox[1] as num).toDouble(),
'right': (bbox[2] as num).toDouble(),
'top': (bbox[3] as num).toDouble(),
});
}
}
bool _lineInsideAnyTable(List<num> bounds) {
// bounds: [left, bottom, right, top]
final left = (bounds[0] as num).toDouble();
final bottom = (bounds[1] as num).toDouble();
final right = (bounds[2] as num).toDouble();
final top = (bounds[3] as num).toDouble();
// small tolerance for float inaccuracies
const tol = 0.8;
for (final t in tableBboxes) {
if (left >= t['left']! - tol &&
right <= t['right']! + tol &&
bottom >= t['bottom']! - tol &&
top <= t['top']! + tol) {
return true;
}
// Also consider lines that sit exactly on the bbox edges:
// if line overlaps bbox left/right edge or top/bottom edge
final onLeftOrRightEdge = ((left - t['left']!).abs() < tol) ||
((right - t['right']!).abs() < tol);
final onTopOrBottomEdge = ((top - t['top']!).abs() < tol) ||
((bottom - t['bottom']!).abs() < tol);
if ((onLeftOrRightEdge &&
top <= t['top']! + tol &&
bottom >= t['bottom']! - tol) ||
(onTopOrBottomEdge &&
right <= t['right']! + tol &&
left >= t['left']! - tol)) {
return true;
}
}
return false;
}
return InteractiveViewer(
minScale: 0.5,
maxScale: 5.0,
constrained: false,
child: Container(
color: Colors.white,
width: pageWidth,
height: pageHeight,
child: Stack(
children: elements.asMap().entries.map((entry) {
final i = entry.key;
final element = entry.value;
final path = element['Path'] ?? '';
// ---------- TEXT ----------
if (element.containsKey('Text')) {
// prefer Bounds; fallback to CharBounds if available
List boundsList = element['Bounds'] ??
element['CharBounds'] ??
[0, 0, pageWidth, 20];
final left = (boundsList[0] as num).toDouble();
final bottom = (boundsList[1] as num).toDouble();
final right = (boundsList[2] as num).toDouble();
final top = (boundsList[3] as num).toDouble();
final y = pageHeight - top;
final x = left;
final ctl = _controllers[i];
final fontSize = (element['TextSize'] ?? 12).toDouble();
final font = element['Font'];
final fontFamily = font?['family_name'] ?? 'Arial';
final fontWeight = (font?['weight'] ?? 400) >= 600
? FontWeight.bold
: FontWeight.normal;
final fontStyle = (font?['italic'] ?? false)
? FontStyle.italic
: FontStyle.normal;
Color textColor = Colors.black;
if (element['attributes']?['rgb_color'] != null) {
final rgb = element['attributes']['rgb_color'];
textColor = Color.fromRGBO(
((rgb[0] ?? 0) * 255).toInt(),
((rgb[1] ?? 0) * 255).toInt(),
((rgb[2] ?? 0) * 255).toInt(),
1,
);
}
return Positioned(
left: x,
top: y + 20,
width: (right - left) + 30,
height: (top - bottom) + 50,
child: Container(
decoration: element['attributes']?['BBox'] != null
? BoxDecoration(
border: Border.all(color: Colors.black54, width: 1))
: null,
padding: const EdgeInsets.all(2),
child: TextFormField(
controller: ctl,
style: TextStyle(
fontSize: fontSize,
fontFamily: fontFamily,
fontWeight: fontWeight,
fontStyle: fontStyle,
color: textColor,
),
maxLines: null,
expands: true,
decoration: const InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
),
),
);
}
// ---------- LINES ----------
// if (path.toString().contains('Line')) {
// final bounds = element['Bounds'] as List;
// // if this line sits inside any table bbox, skip drawing it (prevents double borders)
// // if (_lineInsideAnyTable(bounds)) {
// // return const SizedBox.shrink();
// // }
// final left = (bounds[0] as num).toDouble();
// final bottom = (bounds[1] as num).toDouble();
// final right = (bounds[2] as num).toDouble();
// final top = (bounds[3] as num).toDouble();
// final y = pageHeight - top;
// final x = left;
// return Positioned(
// left: x,
// top: y,
// width: right - left,
// height: (top - bottom) == 0 ? 1 : (top - bottom),
// child: Container(color: Colors.black),
// );
// }
// ---------- TABLES (one rectangle per table) ----------
if (path.toString().contains('Table') &&
element['attributes']?['BBox'] != null) {
final bbox = element['attributes']['BBox'] as List;
final left = (bbox[0] as num).toDouble();
final bottom = (bbox[1] as num).toDouble();
final right = (bbox[2] as num).toDouble();
final top = (bbox[3] as num).toDouble();
final y = pageHeight - top;
final x = left;
return Positioned(
left: x,
top: y,
width: (right - left),
height: top - bottom,
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.black54, width: 1),
),
),
);
}
// ---------- FIGURES ----------
if (path.toString().contains('Figure') &&
element['filePaths'] != null) {
final bounds = element['Bounds'];
final left = (bounds[0] as num).toDouble();
final bottom = (bounds[1] as num).toDouble();
final right = (bounds[2] as num).toDouble();
final top = (bounds[3] as num).toDouble();
final y = pageHeight - top;
final x = left;
final filePath = element['filePaths'][0];
return Positioned(
left: x,
top: y,
width: (right - left),
height: (top - bottom),
child: Image.asset(filePath, fit: BoxFit.contain),
);
}
return const SizedBox.shrink();
}).toList(),
),
),
);
}
// ---------- MAKE PDF FROM EDITED CONTENT ----------
Future<void> _makePdfFromEditedContent() async {
if (_jsonData == null) return;
final pdf = pw.Document();
final page = _jsonData!['pages'][0];
final pageWidth = (page['width'] as num).toDouble();
final pageHeight = (page['height'] as num).toDouble();
final elements = _jsonData!['elements'] as List;
// Precompute table BBoxes (same logic as editor)
final List<Map<String, double>> tableBboxes = [];
for (final e in elements) {
final path = e['Path'] ?? '';
if (path.toString().contains('Table') &&
e['attributes']?['BBox'] != null) {
final bbox = e['attributes']['BBox'] as List;
tableBboxes.add({
'left': (bbox[0] as num).toDouble(),
'bottom': (bbox[1] as num).toDouble(),
'right': (bbox[2] as num).toDouble(),
'top': (bbox[3] as num).toDouble(),
});
}
}
bool _lineInsideAnyTable(List<num> bounds) {
final left = (bounds[0] as num).toDouble();
final bottom = (bounds[1] as num).toDouble();
final right = (bounds[2] as num).toDouble();
final top = (bounds[3] as num).toDouble();
const tol = 0.8;
for (final t in tableBboxes) {
if (left >= t['left']! - tol &&
right <= t['right']! + tol &&
bottom >= t['bottom']! - tol &&
top <= t['top']! + tol) {
return true;
}
final onLeftOrRightEdge = ((left - t['left']!).abs() < tol) ||
((right - t['right']!).abs() < tol);
final onTopOrBottomEdge = ((top - t['top']!).abs() < tol) ||
((bottom - t['bottom']!).abs() < tol);
if ((onLeftOrRightEdge &&
top <= t['top']! + tol &&
bottom >= t['bottom']! - tol) ||
(onTopOrBottomEdge &&
right <= t['right']! + tol &&
left >= t['left']! - tol)) {
return true;
}
}
return false;
}
pdf.addPage(
pw.Page(
pageFormat: PdfPageFormat(pageWidth, pageHeight),
build: (context) {
final widgets = <pw.Widget>[];
for (int i = 0; i < elements.length; i++) {
final element = elements[i];
final path = element['Path'] ?? '';
// ---------- TEXT ----------
if (element.containsKey('Text')) {
final boundsList = element['Bounds'] ??
element['CharBounds'] ??
[0, 0, pageWidth, 20];
final left = (boundsList[0] as num).toDouble();
final bottom = (boundsList[1] as num).toDouble();
final right = (boundsList[2] as num).toDouble();
final top = (boundsList[3] as num).toDouble();
final y = pageHeight - top;
final x = left;
final ctl = _controllers[i];
final text = ctl?.text ?? (element['Text'] ?? '');
final fontSize = (element['TextSize'] ?? 12).toDouble();
final font = element['Font'];
final fontWeight = (font?['weight'] ?? 400) >= 600
? pw.FontWeight.bold
: pw.FontWeight.normal;
final fontStyle = (font?['italic'] ?? false)
? pw.FontStyle.italic
: pw.FontStyle.normal;
PdfColor textColor = PdfColors.black;
final rgb = element['attributes']?['rgb_color'];
if (rgb != null && rgb is List && rgb.length >= 3) {
textColor = PdfColor.fromInt(
(0xFF << 24) |
(((((rgb[0] ?? 0) as num) * 255).toInt()) << 16) |
(((((rgb[1] ?? 0) as num) * 255).toInt()) << 8) |
((((rgb[2] ?? 0) as num) * 255).toInt()),
);
}
final isBold = (font?['weight'] ?? 400) >= 600;
widgets.add(
pw.Positioned(
left: x,
top: y + 25,
child: pw.Container(
width: (right - left) + 25,
height: (top - bottom),
child: pw.Text(
text,
style: pw.TextStyle(
font: isBold ? robotoBold : robotoRegular,
fontSize: fontSize,
fontWeight: fontWeight,
fontStyle: fontStyle,
color: textColor,
),
maxLines: null,
softWrap: true,
),
),
),
);
}
// ---------- LINES ----------
// if (path.toString().contains('Line')) {
// final bounds = element['Bounds'] as List;
// // if (_lineInsideAnyTable(bounds)) {
// // // skip if this line is part of a table bbox (prevents double borders)
// // continue;
// // }
// final left = (bounds[0] as num).toDouble();
// final bottom = (bounds[1] as num).toDouble();
// final right = (bounds[2] as num).toDouble();
// final top = (bounds[3] as num).toDouble();
// final y = pageHeight - top;
// final x = left;
// widgets.add(
// pw.Positioned(
// left: x,
// top: y,
// child: pw.Container(
// width: right - left,
// height: (top - bottom) == 0 ? 1 : (top - bottom),
// color: PdfColors.black,
// ),
// ),
// );
// }
// ---------- TABLES (draw single bbox per table) ----------
if (path.toString().contains('Table') &&
element['attributes']?['BBox'] != null) {
final bbox = element['attributes']['BBox'] as List;
final left = (bbox[0] as num).toDouble();
final bottom = (bbox[1] as num).toDouble();
final right = (bbox[2] as num).toDouble();
final top = (bbox[3] as num).toDouble();
final x = left;
final y = pageHeight - top;
final strokeWidth =
((element['attributes']?['line_width'] ?? 1) as num)
.toDouble();
widgets.add(
pw.Positioned(
left: x,
top: y,
child: pw.Container(
width: (right - left),
height: top - bottom,
decoration: pw.BoxDecoration(
border: pw.Border.all(
color: PdfColors.black,
width: strokeWidth,
),
),
),
),
);
}
// ---------- FIGURES ----------
if (path.toString().contains('Figure') &&
element['filePaths'] != null) {
final bounds = element['Bounds'];
final left = (bounds[0] as num).toDouble();
final bottom = (bounds[1] as num).toDouble();
final right = (bounds[2] as num).toDouble();
final top = (bounds[3] as num).toDouble();
final y = pageHeight - top;
final x = left;
final filePath = element['filePaths'][0];
widgets.add(
pw.Positioned(
left: x,
top: y,
child: pw.Image(
pw.MemoryImage(File(filePath).readAsBytesSync()),
width: (right - left),
height: (top - bottom),
fit: pw.BoxFit.contain),
),
);
}
}
return pw.Stack(children: widgets);
},
),
);
final dir = await getTemporaryDirectory();
final file = File('${dir.path}/edited_output.pdf');
await file.writeAsBytes(await pdf.save());
await OpenFile.open(file.path);
}
}
`
` const params = new ExtractPDFParams({ elementsToExtract: [ExtractElementType.TEXT, ExtractElementType.TABLES], addCharInfo: true,
});`
` import 'dart:convert';
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:http/http.dart' as http;
import 'package:newflutter/utils/constants.dart';
import 'package:open_file/open_file.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
class PdfEditorScreen extends StatefulWidget {
const PdfEditorScreen({super.key});
@override
State<PdfEditorScreen> createState() => _PdfEditorScreenState();
}
class _PdfEditorScreenState extends State<PdfEditorScreen> {
Map<String, dynamic>? _jsonData;
bool _loading = false;
String? _error;
final Map<int, TextEditingController> _controllers = {};
late final pw.Font robotoRegular;
late final pw.Font robotoBold;
@override
void initState() {
super.initState();
_loadFonts();
}
Future<void> _loadFonts() async {
robotoRegular =
pw.Font.ttf(await rootBundle.load("assets/fonts/Roboto-Regular.ttf"));
robotoBold =
pw.Font.ttf(await rootBundle.load("assets/fonts/Roboto-Bold.ttf"));
}
// Load JSON from assets
Future<void> _loadFromAssets() async {
setState(() {
_loading = true;
_error = null;
_jsonData = null;
_controllers.clear();
});
try {
final raw = await rootBundle.loadString('assets/structuredDataM.json');
final jsonMap = json.decode(raw) as Map<String, dynamic>;
_initData(jsonMap);
} catch (e) {
setState(() => _error = 'Error loading assets: $e');
} finally {
setState(() => _loading = false);
}
}
// Upload PDF → API → JSON
Future<void> _pickAndUploadPdf() async {
setState(() {
_loading = true;
_error = null;
_jsonData = null;
_controllers.clear();
});
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['pdf'],
);
if (result == null) {
setState(() => _loading = false);
return;
}
final file = result.files.first;
final uri = Uri.parse("${Constants().baseUrl}/test/convert");
final request = http.MultipartRequest('POST', uri)
..files.add(await http.MultipartFile.fromPath('file', file.path!,
filename: file.name));
final streamed = await request.send();
final response = await http.Response.fromStream(streamed);
if (response.statusCode == 200 || response.statusCode == 201) {
final jsonMap = json.decode(response.body) as Map<String, dynamic>;
_initData(jsonMap);
} else {
setState(
() => _error = 'Something went wrong! Please try after some time ');
}
} catch (e) {
setState(
() => _error = 'Something went wrong! Please try after some time:');
} finally {
setState(() => _loading = false);
}
}
void _initData(Map<String, dynamic> parsed) {
final elements = parsed['elements'] as List;
for (int i = 0; i < elements.length; i++) {
final e = elements[i];
if ((e['Text'] ?? '').toString().isNotEmpty) {
_controllers[i] = TextEditingController(text: e['Text']);
}
}
setState(() => _jsonData = parsed);
}
@override
void dispose() {
for (final c in _controllers.values) {
c.dispose();
}
super.dispose();
}
// ---------- UI ----------
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("PDF Editor"),
actions: [
IconButton(
onPressed: _loadFromAssets,
icon: const Icon(Icons.folder_open),
tooltip: 'Load JSON from assets',
),
IconButton(
onPressed: _pickAndUploadPdf,
icon: const Icon(Icons.cloud_upload),
tooltip: 'Upload PDF → Convert',
),
],
),
body: _loading
? const Center(child: CircularProgressIndicator())
: _error != null
? Center(child: Text(_error!))
: _jsonData == null
? const Center(child: Text("Load or upload PDF JSON"))
: _buildEditor(),
floatingActionButton: _jsonData == null
? null
: FloatingActionButton.extended(
onPressed: _makePdfFromEditedContent,
label: const Text("Make PDF"),
icon: const Icon(Icons.picture_as_pdf),
),
);
}
Widget _buildEditor() {
final page = _jsonData!['pages'][0];
final pageWidth = (page['width'] as num).toDouble();
final pageHeight = (page['height'] as num).toDouble();
final elements = _jsonData!['elements'] as List;
// 1) Precompute table BBoxes
final List<Map<String, double>> tableBboxes = [];
for (final e in elements) {
final path = e['Path'] ?? '';
if (path.toString().contains('Table') &&
e['attributes']?['BBox'] != null) {
final bbox = e['attributes']['BBox'] as List;
tableBboxes.add({
'left': (bbox[0] as num).toDouble(),
'bottom': (bbox[1] as num).toDouble(),
'right': (bbox[2] as num).toDouble(),
'top': (bbox[3] as num).toDouble(),
});
}
}
bool _lineInsideAnyTable(List<num> bounds) {
// bounds: [left, bottom, right, top]
final left = (bounds[0] as num).toDouble();
final bottom = (bounds[1] as num).toDouble();
final right = (bounds[2] as num).toDouble();
final top = (bounds[3] as num).toDouble();
// small tolerance for float inaccuracies
const tol = 0.8;
for (final t in tableBboxes) {
if (left >= t['left']! - tol &&
right <= t['right']! + tol &&
bottom >= t['bottom']! - tol &&
top <= t['top']! + tol) {
return true;
}
// Also consider lines that sit exactly on the bbox edges:
// if line overlaps bbox left/right edge or top/bottom edge
final onLeftOrRightEdge = ((left - t['left']!).abs() < tol) ||
((right - t['right']!).abs() < tol);
final onTopOrBottomEdge = ((top - t['top']!).abs() < tol) ||
((bottom - t['bottom']!).abs() < tol);
if ((onLeftOrRightEdge &&
top <= t['top']! + tol &&
bottom >= t['bottom']! - tol) ||
(onTopOrBottomEdge &&
right <= t['right']! + tol &&
left >= t['left']! - tol)) {
return true;
}
}
return false;
}
return InteractiveViewer(
minScale: 0.5,
maxScale: 5.0,
constrained: false,
child: Container(
color: Colors.white,
width: pageWidth,
height: pageHeight,
child: Stack(
children: elements.asMap().entries.map((entry) {
final i = entry.key;
final element = entry.value;
final path = element['Path'] ?? '';
// ---------- TEXT ----------
if (element.containsKey('Text')) {
// prefer Bounds; fallback to CharBounds if available
List boundsList = element['Bounds'] ??
element['CharBounds'] ??
[0, 0, pageWidth, 20];
final left = (boundsList[0] as num).toDouble();
final bottom = (boundsList[1] as num).toDouble();
final right = (boundsList[2] as num).toDouble();
final top = (boundsList[3] as num).toDouble();
final y = pageHeight - top;
final x = left;
final ctl = _controllers[i];
final fontSize = (element['TextSize'] ?? 12).toDouble();
final font = element['Font'];
final fontFamily = font?['family_name'] ?? 'Arial';
final fontWeight = (font?['weight'] ?? 400) >= 600
? FontWeight.bold
: FontWeight.normal;
final fontStyle = (font?['italic'] ?? false)
? FontStyle.italic
: FontStyle.normal;
Color textColor = Colors.black;
if (element['attributes']?['rgb_color'] != null) {
final rgb = element['attributes']['rgb_color'];
textColor = Color.fromRGBO(
((rgb[0] ?? 0) * 255).toInt(),
((rgb[1] ?? 0) * 255).toInt(),
((rgb[2] ?? 0) * 255).toInt(),
1,
);
}
return Positioned(
left: x,
top: y + 20,
width: (right - left) + 30,
height: (top - bottom) + 50,
child: Container(
decoration: element['attributes']?['BBox'] != null
? BoxDecoration(
border: Border.all(color: Colors.black54, width: 1))
: null,
padding: const EdgeInsets.all(2),
child: TextFormField(
controller: ctl,
style: TextStyle(
fontSize: fontSize,
fontFamily: fontFamily,
fontWeight: fontWeight,
fontStyle: fontStyle,
color: textColor,
),
maxLines: null,
expands: true,
decoration: const InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
),
),
);
}
// ---------- LINES ----------
// if (path.toString().contains('Line')) {
// final bounds = element['Bounds'] as List;
// // if this line sits inside any table bbox, skip drawing it (prevents double borders)
// // if (_lineInsideAnyTable(bounds)) {
// // return const SizedBox.shrink();
// // }
// final left = (bounds[0] as num).toDouble();
// final bottom = (bounds[1] as num).toDouble();
// final right = (bounds[2] as num).toDouble();
// final top = (bounds[3] as num).toDouble();
// final y = pageHeight - top;
// final x = left;
// return Positioned(
// left: x,
// top: y,
// width: right - left,
// height: (top - bottom) == 0 ? 1 : (top - bottom),
// child: Container(color: Colors.black),
// );
// }
// ---------- TABLES (one rectangle per table) ----------
if (path.toString().contains('Table') &&
element['attributes']?['BBox'] != null) {
final bbox = element['attributes']['BBox'] as List;
final left = (bbox[0] as num).toDouble();
final bottom = (bbox[1] as num).toDouble();
final right = (bbox[2] as num).toDouble();
final top = (bbox[3] as num).toDouble();
final y = pageHeight - top;
final x = left;
return Positioned(
left: x,
top: y,
width: (right - left),
height: top - bottom,
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.black54, width: 1),
),
),
);
}
// ---------- FIGURES ----------
if (path.toString().contains('Figure') &&
element['filePaths'] != null) {
final bounds = element['Bounds'];
final left = (bounds[0] as num).toDouble();
final bottom = (bounds[1] as num).toDouble();
final right = (bounds[2] as num).toDouble();
final top = (bounds[3] as num).toDouble();
final y = pageHeight - top;
final x = left;
final filePath = element['filePaths'][0];
return Positioned(
left: x,
top: y,
width: (right - left),
height: (top - bottom),
child: Image.asset(filePath, fit: BoxFit.contain),
);
}
return const SizedBox.shrink();
}).toList(),
),
),
);
}
// ---------- MAKE PDF FROM EDITED CONTENT ----------
Future<void> _makePdfFromEditedContent() async {
if (_jsonData == null) return;
final pdf = pw.Document();
final page = _jsonData!['pages'][0];
final pageWidth = (page['width'] as num).toDouble();
final pageHeight = (page['height'] as num).toDouble();
final elements = _jsonData!['elements'] as List;
// Precompute table BBoxes (same logic as editor)
final List<Map<String, double>> tableBboxes = [];
for (final e in elements) {
final path = e['Path'] ?? '';
if (path.toString().contains('Table') &&
e['attributes']?['BBox'] != null) {
final bbox = e['attributes']['BBox'] as List;
tableBboxes.add({
'left': (bbox[0] as num).toDouble(),
'bottom': (bbox[1] as num).toDouble(),
'right': (bbox[2] as num).toDouble(),
'top': (bbox[3] as num).toDouble(),
});
}
}
bool _lineInsideAnyTable(List<num> bounds) {
final left = (bounds[0] as num).toDouble();
final bottom = (bounds[1] as num).toDouble();
final right = (bounds[2] as num).toDouble();
final top = (bounds[3] as num).toDouble();
const tol = 0.8;
for (final t in tableBboxes) {
if (left >= t['left']! - tol &&
right <= t['right']! + tol &&
bottom >= t['bottom']! - tol &&
top <= t['top']! + tol) {
return true;
}
final onLeftOrRightEdge = ((left - t['left']!).abs() < tol) ||
((right - t['right']!).abs() < tol);
final onTopOrBottomEdge = ((top - t['top']!).abs() < tol) ||
((bottom - t['bottom']!).abs() < tol);
if ((onLeftOrRightEdge &&
top <= t['top']! + tol &&
bottom >= t['bottom']! - tol) ||
(onTopOrBottomEdge &&
right <= t['right']! + tol &&
left >= t['left']! - tol)) {
return true;
}
}
return false;
}
pdf.addPage(
pw.Page(
pageFormat: PdfPageFormat(pageWidth, pageHeight),
build: (context) {
final widgets = <pw.Widget>[];
for (int i = 0; i < elements.length; i++) {
final element = elements[i];
final path = element['Path'] ?? '';
// ---------- TEXT ----------
if (element.containsKey('Text')) {
final boundsList = element['Bounds'] ??
element['CharBounds'] ??
[0, 0, pageWidth, 20];
final left = (boundsList[0] as num).toDouble();
final bottom = (boundsList[1] as num).toDouble();
final right = (boundsList[2] as num).toDouble();
final top = (boundsList[3] as num).toDouble();
final y = pageHeight - top;
final x = left;
final ctl = _controllers[i];
final text = ctl?.text ?? (element['Text'] ?? '');
final fontSize = (element['TextSize'] ?? 12).toDouble();
final font = element['Font'];
final fontWeight = (font?['weight'] ?? 400) >= 600
? pw.FontWeight.bold
: pw.FontWeight.normal;
final fontStyle = (font?['italic'] ?? false)
? pw.FontStyle.italic
: pw.FontStyle.normal;
PdfColor textColor = PdfColors.black;
final rgb = element['attributes']?['rgb_color'];
if (rgb != null && rgb is List && rgb.length >= 3) {
textColor = PdfColor.fromInt(
(0xFF << 24) |
(((((rgb[0] ?? 0) as num) * 255).toInt()) << 16) |
(((((rgb[1] ?? 0) as num) * 255).toInt()) << 8) |
((((rgb[2] ?? 0) as num) * 255).toInt()),
);
}
final isBold = (font?['weight'] ?? 400) >= 600;
widgets.add(
pw.Positioned(
left: x,
top: y + 25,
child: pw.Container(
width: (right - left) + 25,
height: (top - bottom),
child: pw.Text(
text,
style: pw.TextStyle(
font: isBold ? robotoBold : robotoRegular,
fontSize: fontSize,
fontWeight: fontWeight,
fontStyle: fontStyle,
color: textColor,
),
maxLines: null,
softWrap: true,
),
),
),
);
}
// ---------- LINES ----------
// if (path.toString().contains('Line')) {
// final bounds = element['Bounds'] as List;
// // if (_lineInsideAnyTable(bounds)) {
// // // skip if this line is part of a table bbox (prevents double borders)
// // continue;
// // }
// final left = (bounds[0] as num).toDouble();
// final bottom = (bounds[1] as num).toDouble();
// final right = (bounds[2] as num).toDouble();
// final top = (bounds[3] as num).toDouble();
// final y = pageHeight - top;
// final x = left;
// widgets.add(
// pw.Positioned(
// left: x,
// top: y,
// child: pw.Container(
// width: right - left,
// height: (top - bottom) == 0 ? 1 : (top - bottom),
// color: PdfColors.black,
// ),
// ),
// );
// }
// ---------- TABLES (draw single bbox per table) ----------
if (path.toString().contains('Table') &&
element['attributes']?['BBox'] != null) {
final bbox = element['attributes']['BBox'] as List;
final left = (bbox[0] as num).toDouble();
final bottom = (bbox[1] as num).toDouble();
final right = (bbox[2] as num).toDouble();
final top = (bbox[3] as num).toDouble();
final x = left;
final y = pageHeight - top;
final strokeWidth =
((element['attributes']?['line_width'] ?? 1) as num)
.toDouble();
widgets.add(
pw.Positioned(
left: x,
top: y,
child: pw.Container(
width: (right - left),
height: top - bottom,
decoration: pw.BoxDecoration(
border: pw.Border.all(
color: PdfColors.black,
width: strokeWidth,
),
),
),
),
);
}
// ---------- FIGURES ----------
if (path.toString().contains('Figure') &&
element['filePaths'] != null) {
final bounds = element['Bounds'];
final left = (bounds[0] as num).toDouble();
final bottom = (bounds[1] as num).toDouble();
final right = (bounds[2] as num).toDouble();
final top = (bounds[3] as num).toDouble();
final y = pageHeight - top;
final x = left;
final filePath = element['filePaths'][0];
widgets.add(
pw.Positioned(
left: x,
top: y,
child: pw.Image(
pw.MemoryImage(File(filePath).readAsBytesSync()),
width: (right - left),
height: (top - bottom),
fit: pw.BoxFit.contain),
),
);
}
}
return pw.Stack(children: widgets);
},
),
);
final dir = await getTemporaryDirectory();
final file = File('${dir.path}/edited_output.pdf');
await file.writeAsBytes(await pdf.save());
await OpenFile.open(file.path);
}
}
`
const params = new ExtractPDFParams({
elementsToExtract: [ExtractElementType.TEXT, ExtractElementType.TABLES],
addCharInfo: true,
});
import 'dart:convert';
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:http/http.dart' as http;
import 'package:newflutter/utils/constants.dart';
import 'package:open_file/open_file.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
class PdfEditorScreen extends StatefulWidget {
const PdfEditorScreen({super.key});
@override
State<PdfEditorScreen> createState() => _PdfEditorScreenState();
}
class _PdfEditorScreenState extends State<PdfEditorScreen> {
Map<String, dynamic>? _jsonData;
bool _loading = false;
String? _error;
final Map<int, TextEditingController> _controllers = {};
late final pw.Font robotoRegular;
late final pw.Font robotoBold;
@override
void initState() {
super.initState();
_loadFonts();
}
Future<void> _loadFonts() async {
robotoRegular =
pw.Font.ttf(await rootBundle.load("assets/fonts/Roboto-Regular.ttf"));
robotoBold =
pw.Font.ttf(await rootBundle.load("assets/fonts/Roboto-Bold.ttf"));
}
// Load JSON from assets
Future<void> _loadFromAssets() async {
setState(() {
_loading = true;
_error = null;
_jsonData = null;
_controllers.clear();
});
try {
final raw = await rootBundle.loadString('assets/structuredDataM.json');
final jsonMap = json.decode(raw) as Map<String, dynamic>;
_initData(jsonMap);
} catch (e) {
setState(() => _error = 'Error loading assets: $e');
} finally {
setState(() => _loading = false);
}
}
// Upload PDF → API → JSON
Future<void> _pickAndUploadPdf() async {
setState(() {
_loading = true;
_error = null;
_jsonData = null;
_controllers.clear();
});
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['pdf'],
);
if (result == null) {
setState(() => _loading = false);
return;
}
final file = result.files.first;
final uri = Uri.parse("${Constants().baseUrl}/test/convert");
final request = http.MultipartRequest('POST', uri)
..files.add(await http.MultipartFile.fromPath('file', file.path!,
filename: file.name));
final streamed = await request.send();
final response = await http.Response.fromStream(streamed);
if (response.statusCode == 200 || response.statusCode == 201) {
final jsonMap = json.decode(response.body) as Map<String, dynamic>;
_initData(jsonMap);
} else {
setState(
() => _error = 'Something went wrong! Please try after some time ');
}
} catch (e) {
setState(
() => _error = 'Something went wrong! Please try after some time:');
} finally {
setState(() => _loading = false);
}
}
void _initData(Map<String, dynamic> parsed) {
final elements = parsed['elements'] as List;
for (int i = 0; i < elements.length; i++) {
final e = elements[i];
if ((e['Text'] ?? '').toString().isNotEmpty) {
_controllers[i] = TextEditingController(text: e['Text']);
}
}
setState(() => _jsonData = parsed);
}
@override
void dispose() {
for (final c in _controllers.values) {
c.dispose();
}
super.dispose();
}
// ---------- UI ----------
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("PDF Editor"),
actions: [
IconButton(
onPressed: _loadFromAssets,
icon: const Icon(Icons.folder_open),
tooltip: 'Load JSON from assets',
),
IconButton(
onPressed: _pickAndUploadPdf,
icon: const Icon(Icons.cloud_upload),
tooltip: 'Upload PDF → Convert',
),
],
),
body: _loading
? const Center(child: CircularProgressIndicator())
: _error != null
? Center(child: Text(_error!))
: _jsonData == null
? const Center(child: Text("Load or upload PDF JSON"))
: _buildEditor(),
floatingActionButton: _jsonData == null
? null
: FloatingActionButton.extended(
onPressed: _makePdfFromEditedContent,
label: const Text("Make PDF"),
icon: const Icon(Icons.picture_as_pdf),
),
);
}
Widget _buildEditor() {
final page = _jsonData!['pages'][0];
final pageWidth = (page['width'] as num).toDouble();
final pageHeight = (page['height'] as num).toDouble();
final elements = _jsonData!['elements'] as List;
// 1) Precompute table BBoxes
final List<Map<String, double>> tableBboxes = [];
for (final e in elements) {
final path = e['Path'] ?? '';
if (path.toString().contains('Table') &&
e['attributes']?['BBox'] != null) {
final bbox = e['attributes']['BBox'] as List;
tableBboxes.add({
'left': (bbox[0] as num).toDouble(),
'bottom': (bbox[1] as num).toDouble(),
'right': (bbox[2] as num).toDouble(),
'top': (bbox[3] as num).toDouble(),
});
}
}
bool _lineInsideAnyTable(List<num> bounds) {
// bounds: [left, bottom, right, top]
final left = (bounds[0] as num).toDouble();
final bottom = (bounds[1] as num).toDouble();
final right = (bounds[2] as num).toDouble();
final top = (bounds[3] as num).toDouble();
// small tolerance for float inaccuracies
const tol = 0.8;
for (final t in tableBboxes) {
if (left >= t['left']! - tol &&
right <= t['right']! + tol &&
bottom >= t['bottom']! - tol &&
top <= t['top']! + tol) {
return true;
}
// Also consider lines that sit exactly on the bbox edges:
// if line overlaps bbox left/right edge or top/bottom edge
final onLeftOrRightEdge = ((left - t['left']!).abs() < tol) ||
((right - t['right']!).abs() < tol);
final onTopOrBottomEdge = ((top - t['top']!).abs() < tol) ||
((bottom - t['bottom']!).abs() < tol);
if ((onLeftOrRightEdge &&
top <= t['top']! + tol &&
bottom >= t['bottom']! - tol) ||
(onTopOrBottomEdge &&
right <= t['right']! + tol &&
left >= t['left']! - tol)) {
return true;
}
}
return false;
}
return InteractiveViewer(
minScale: 0.5,
maxScale: 5.0,
constrained: false,
child: Container(
color: Colors.white,
width: pageWidth,
height: pageHeight,
child: Stack(
children: elements.asMap().entries.map((entry) {
final i = entry.key;
final element = entry.value;
final path = element['Path'] ?? '';
// ---------- TEXT ----------
if (element.containsKey('Text')) {
// prefer Bounds; fallback to CharBounds if available
List boundsList = element['Bounds'] ??
element['CharBounds'] ??
[0, 0, pageWidth, 20];
final left = (boundsList[0] as num).toDouble();
final bottom = (boundsList[1] as num).toDouble();
final right = (boundsList[2] as num).toDouble();
final top = (boundsList[3] as num).toDouble();
final y = pageHeight - top;
final x = left;
final ctl = _controllers[i];
final fontSize = (element['TextSize'] ?? 12).toDouble();
final font = element['Font'];
final fontFamily = font?['family_name'] ?? 'Arial';
final fontWeight = (font?['weight'] ?? 400) >= 600
? FontWeight.bold
: FontWeight.normal;
final fontStyle = (font?['italic'] ?? false)
? FontStyle.italic
: FontStyle.normal;
Color textColor = Colors.black;
if (element['attributes']?['rgb_color'] != null) {
final rgb = element['attributes']['rgb_color'];
textColor = Color.fromRGBO(
((rgb[0] ?? 0) * 255).toInt(),
((rgb[1] ?? 0) * 255).toInt(),
((rgb[2] ?? 0) * 255).toInt(),
1,
);
}
return Positioned(
left: x,
top: y + 20,
width: (right - left) + 30,
height: (top - bottom) + 50,
child: Container(
decoration: element['attributes']?['BBox'] != null
? BoxDecoration(
border: Border.all(color: Colors.black54, width: 1))
: null,
padding: const EdgeInsets.all(2),
child: TextFormField(
controller: ctl,
style: TextStyle(
fontSize: fontSize,
fontFamily: fontFamily,
fontWeight: fontWeight,
fontStyle: fontStyle,
color: textColor,
),
maxLines: null,
expands: true,
decoration: const InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
),
),
);
}
// ---------- LINES ----------
// if (path.toString().contains('Line')) {
// final bounds = element['Bounds'] as List;
// // if this line sits inside any table bbox, skip drawing it (prevents double borders)
// // if (_lineInsideAnyTable(bounds)) {
// // return const SizedBox.shrink();
// // }
// final left = (bounds[0] as num).toDouble();
// final bottom = (bounds[1] as num).toDouble();
// final right = (bounds[2] as num).toDouble();
// final top = (bounds[3] as num).toDouble();
// final y = pageHeight - top;
// final x = left;
// return Positioned(
// left: x,
// top: y,
// width: right - left,
// height: (top - bottom) == 0 ? 1 : (top - bottom),
// child: Container(color: Colors.black),
// );
// }
// ---------- TABLES (one rectangle per table) ----------
if (path.toString().contains('Table') &&
element['attributes']?['BBox'] != null) {
final bbox = element['attributes']['BBox'] as List;
final left = (bbox[0] as num).toDouble();
final bottom = (bbox[1] as num).toDouble();
final right = (bbox[2] as num).toDouble();
final top = (bbox[3] as num).toDouble();
final y = pageHeight - top;
final x = left;
return Positioned(
left: x,
top: y,
width: (right - left),
height: top - bottom,
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.black54, width: 1),
),
),
);
}
// ---------- FIGURES ----------
if (path.toString().contains('Figure') &&
element['filePaths'] != null) {
final bounds = element['Bounds'];
final left = (bounds[0] as num).toDouble();
final bottom = (bounds[1] as num).toDouble();
final right = (bounds[2] as num).toDouble();
final top = (bounds[3] as num).toDouble();
final y = pageHeight - top;
final x = left;
final filePath = element['filePaths'][0];
return Positioned(
left: x,
top: y,
width: (right - left),
height: (top - bottom),
child: Image.asset(filePath, fit: BoxFit.contain),
);
}
return const SizedBox.shrink();
}).toList(),
),
),
);
}
// ---------- MAKE PDF FROM EDITED CONTENT ----------
Future<void> _makePdfFromEditedContent() async {
if (_jsonData == null) return;
final pdf = pw.Document();
final page = _jsonData!['pages'][0];
final pageWidth = (page['width'] as num).toDouble();
final pageHeight = (page['height'] as num).toDouble();
final elements = _jsonData!['elements'] as List;
// Precompute table BBoxes (same logic as editor)
final List<Map<String, double>> tableBboxes = [];
for (final e in elements) {
final path = e['Path'] ?? '';
if (path.toString().contains('Table') &&
e['attributes']?['BBox'] != null) {
final bbox = e['attributes']['BBox'] as List;
tableBboxes.add({
'left': (bbox[0] as num).toDouble(),
'bottom': (bbox[1] as num).toDouble(),
'right': (bbox[2] as num).toDouble(),
'top': (bbox[3] as num).toDouble(),
});
}
}
bool _lineInsideAnyTable(List<num> bounds) {
final left = (bounds[0] as num).toDouble();
final bottom = (bounds[1] as num).toDouble();
final right = (bounds[2] as num).toDouble();
final top = (bounds[3] as num).toDouble();
const tol = 0.8;
for (final t in tableBboxes) {
if (left >= t['left']! - tol &&
right <= t['right']! + tol &&
bottom >= t['bottom']! - tol &&
top <= t['top']! + tol) {
return true;
}
final onLeftOrRightEdge = ((left - t['left']!).abs() < tol) ||
((right - t['right']!).abs() < tol);
final onTopOrBottomEdge = ((top - t['top']!).abs() < tol) ||
((bottom - t['bottom']!).abs() < tol);
if ((onLeftOrRightEdge &&
top <= t['top']! + tol &&
bottom >= t['bottom']! - tol) ||
(onTopOrBottomEdge &&
right <= t['right']! + tol &&
left >= t['left']! - tol)) {
return true;
}
}
return false;
}
pdf.addPage(
pw.Page(
pageFormat: PdfPageFormat(pageWidth, pageHeight),
build: (context) {
final widgets = <pw.Widget>[];
for (int i = 0; i < elements.length; i++) {
final element = elements[i];
final path = element['Path'] ?? '';
// ---------- TEXT ----------
if (element.containsKey('Text')) {
final boundsList = element['Bounds'] ??
element['CharBounds'] ??
[0, 0, pageWidth, 20];
final left = (boundsList[0] as num).toDouble();
final bottom = (boundsList[1] as num).toDouble();
final right = (boundsList[2] as num).toDouble();
final top = (boundsList[3] as num).toDouble();
final y = pageHeight - top;
final x = left;
final ctl = _controllers[i];
final text = ctl?.text ?? (element['Text'] ?? '');
final fontSize = (element['TextSize'] ?? 12).toDouble();
final font = element['Font'];
final fontWeight = (font?['weight'] ?? 400) >= 600
? pw.FontWeight.bold
: pw.FontWeight.normal;
final fontStyle = (font?['italic'] ?? false)
? pw.FontStyle.italic
: pw.FontStyle.normal;
PdfColor textColor = PdfColors.black;
final rgb = element['attributes']?['rgb_color'];
if (rgb != null && rgb is List && rgb.length >= 3) {
textColor = PdfColor.fromInt(
(0xFF << 24) |
(((((rgb[0] ?? 0) as num) * 255).toInt()) << 16) |
(((((rgb[1] ?? 0) as num) * 255).toInt()) << 8) |
((((rgb[2] ?? 0) as num) * 255).toInt()),
);
}
final isBold = (font?['weight'] ?? 400) >= 600;
widgets.add(
pw.Positioned(
left: x,
top: y + 25,
child: pw.Container(
width: (right - left) + 25,
height: (top - bottom),
child: pw.Text(
text,
style: pw.TextStyle(
font: isBold ? robotoBold : robotoRegular,
fontSize: fontSize,
fontWeight: fontWeight,
fontStyle: fontStyle,
color: textColor,
),
maxLines: null,
softWrap: true,
),
),
),
);
}
// ---------- LINES ----------
// if (path.toString().contains('Line')) {
// final bounds = element['Bounds'] as List;
// // if (_lineInsideAnyTable(bounds)) {
// // // skip if this line is part of a table bbox (prevents double borders)
// // continue;
// // }
// final left = (bounds[0] as num).toDouble();
// final bottom = (bounds[1] as num).toDouble();
// final right = (bounds[2] as num).toDouble();
// final top = (bounds[3] as num).toDouble();
// final y = pageHeight - top;
// final x = left;
// widgets.add(
// pw.Positioned(
// left: x,
// top: y,
// child: pw.Container(
// width: right - left,
// height: (top - bottom) == 0 ? 1 : (top - bottom),
// color: PdfColors.black,
// ),
// ),
// );
// }
// ---------- TABLES (draw single bbox per table) ----------
if (path.toString().contains('Table') &&
element['attributes']?['BBox'] != null) {
final bbox = element['attributes']['BBox'] as List;
final left = (bbox[0] as num).toDouble();
final bottom = (bbox[1] as num).toDouble();
final right = (bbox[2] as num).toDouble();
final top = (bbox[3] as num).toDouble();
final x = left;
final y = pageHeight - top;
final strokeWidth =
((element['attributes']?['line_width'] ?? 1) as num)
.toDouble();
widgets.add(
pw.Positioned(
left: x,
top: y,
child: pw.Container(
width: (right - left),
height: top - bottom,
decoration: pw.BoxDecoration(
border: pw.Border.all(
color: PdfColors.black,
width: strokeWidth,
),
),
),
),
);
}
// ---------- FIGURES ----------
if (path.toString().contains('Figure') &&
element['filePaths'] != null) {
final bounds = element['Bounds'];
final left = (bounds[0] as num).toDouble();
final bottom = (bounds[1] as num).toDouble();
final right = (bounds[2] as num).toDouble();
final top = (bounds[3] as num).toDouble();
final y = pageHeight - top;
final x = left;
final filePath = element['filePaths'][0];
widgets.add(
pw.Positioned(
left: x,
top: y,
child: pw.Image(
pw.MemoryImage(File(filePath).readAsBytesSync()),
width: (right - left),
height: (top - bottom),
fit: pw.BoxFit.contain),
),
);
}
}
return pw.Stack(children: widgets);
},
),
);
final dir = await getTemporaryDirectory();
final file = File('${dir.path}/edited_output.pdf');
await file.writeAsBytes(await pdf.save());
await OpenFile.open(file.path);
}
}
How to draw the border boxes in flutter using pdf json?
I have a scanned pdf and want to edit this into Flutter with JSON. So what I am doing:
I have used Adobe apis to first convert the Scanned pdf to searchable pdf using OCR and after that extracted the JSON with Style which has data with styles and other format (see attached json link) .
Backend code to extract the JSON
` const params = new ExtractPDFParams({ elementsToExtract: [ExtractElementType.TEXT, ExtractElementType.TABLES], addCharInfo: true,
});`
and see this flutter code
` import 'dart:convert';
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:http/http.dart' as http;
import 'package:newflutter/utils/constants.dart';
import 'package:open_file/open_file.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw;
class PdfEditorScreen extends StatefulWidget {
const PdfEditorScreen({super.key});
@override
State<PdfEditorScreen> createState() => _PdfEditorScreenState();
}
class _PdfEditorScreenState extends State<PdfEditorScreen> {
Map<String, dynamic>? _jsonData;
bool _loading = false;
String? _error;
final Map<int, TextEditingController> _controllers = {};
late final pw.Font robotoRegular;
late final pw.Font robotoBold;
@override
void initState() {
super.initState();
_loadFonts();
}
Future<void> _loadFonts() async {
robotoRegular =
pw.Font.ttf(await rootBundle.load("assets/fonts/Roboto-Regular.ttf"));
robotoBold =
pw.Font.ttf(await rootBundle.load("assets/fonts/Roboto-Bold.ttf"));
}
// Load JSON from assets
Future<void> _loadFromAssets() async {
setState(() {
_loading = true;
_error = null;
_jsonData = null;
_controllers.clear();
});
try {
final raw = await rootBundle.loadString('assets/structuredDataM.json');
final jsonMap = json.decode(raw) as Map<String, dynamic>;
_initData(jsonMap);
} catch (e) {
setState(() => _error = 'Error loading assets: $e');
} finally {
setState(() => _loading = false);
}
}
// Upload PDF → API → JSON
Future<void> _pickAndUploadPdf() async {
setState(() {
_loading = true;
_error = null;
_jsonData = null;
_controllers.clear();
});
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['pdf'],
);
if (result == null) {
setState(() => _loading = false);
return;
}
final file = result.files.first;
final uri = Uri.parse("${Constants().baseUrl}/test/convert");
final request = http.MultipartRequest('POST', uri)
..files.add(await http.MultipartFile.fromPath('file', file.path!,
filename: file.name));
final streamed = await request.send();
final response = await http.Response.fromStream(streamed);
if (response.statusCode == 200 || response.statusCode == 201) {
final jsonMap = json.decode(response.body) as Map<String, dynamic>;
_initData(jsonMap);
} else {
setState(
() => _error = 'Something went wrong! Please try after some time ');
}
} catch (e) {
setState(
() => _error = 'Something went wrong! Please try after some time:');
} finally {
setState(() => _loading = false);
}
}
void _initData(Map<String, dynamic> parsed) {
final elements = parsed['elements'] as List;
for (int i = 0; i < elements.length; i++) {
final e = elements[i];
if ((e['Text'] ?? '').toString().isNotEmpty) {
_controllers[i] = TextEditingController(text: e['Text']);
}
}
setState(() => _jsonData = parsed);
}
@override
void dispose() {
for (final c in _controllers.values) {
c.dispose();
}
super.dispose();
}
// ---------- UI ----------
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("PDF Editor"),
actions: [
IconButton(
onPressed: _loadFromAssets,
icon: const Icon(Icons.folder_open),
tooltip: 'Load JSON from assets',
),
IconButton(
onPressed: _pickAndUploadPdf,
icon: const Icon(Icons.cloud_upload),
tooltip: 'Upload PDF → Convert',
),
],
),
body: _loading
? const Center(child: CircularProgressIndicator())
: _error != null
? Center(child: Text(_error!))
: _jsonData == null
? const Center(child: Text("Load or upload PDF JSON"))
: _buildEditor(),
floatingActionButton: _jsonData == null
? null
: FloatingActionButton.extended(
onPressed: _makePdfFromEditedContent,
label: const Text("Make PDF"),
icon: const Icon(Icons.picture_as_pdf),
),
);
}
Widget _buildEditor() {
final page = _jsonData!['pages'][0];
final pageWidth = (page['width'] as num).toDouble();
final pageHeight = (page['height'] as num).toDouble();
final elements = _jsonData!['elements'] as List;
// 1) Precompute table BBoxes
final List<Map<String, double>> tableBboxes = [];
for (final e in elements) {
final path = e['Path'] ?? '';
if (path.toString().contains('Table') &&
e['attributes']?['BBox'] != null) {
final bbox = e['attributes']['BBox'] as List;
tableBboxes.add({
'left': (bbox[0] as num).toDouble(),
'bottom': (bbox[1] as num).toDouble(),
'right': (bbox[2] as num).toDouble(),
'top': (bbox[3] as num).toDouble(),
});
}
}
bool _lineInsideAnyTable(List<num> bounds) {
// bounds: [left, bottom, right, top]
final left = (bounds[0] as num).toDouble();
final bottom = (bounds[1] as num).toDouble();
final right = (bounds[2] as num).toDouble();
final top = (bounds[3] as num).toDouble();
// small tolerance for float inaccuracies
const tol = 0.8;
for (final t in tableBboxes) {
if (left >= t['left']! - tol &&
right <= t['right']! + tol &&
bottom >= t['bottom']! - tol &&
top <= t['top']! + tol) {
return true;
}
// Also consider lines that sit exactly on the bbox edges:
// if line overlaps bbox left/right edge or top/bottom edge
final onLeftOrRightEdge = ((left - t['left']!).abs() < tol) ||
((right - t['right']!).abs() < tol);
final onTopOrBottomEdge = ((top - t['top']!).abs() < tol) ||
((bottom - t['bottom']!).abs() < tol);
if ((onLeftOrRightEdge &&
top <= t['top']! + tol &&
bottom >= t['bottom']! - tol) ||
(onTopOrBottomEdge &&
right <= t['right']! + tol &&
left >= t['left']! - tol)) {
return true;
}
}
return false;
}
return InteractiveViewer(
minScale: 0.5,
maxScale: 5.0,
constrained: false,
child: Container(
color: Colors.white,
width: pageWidth,
height: pageHeight,
child: Stack(
children: elements.asMap().entries.map((entry) {
final i = entry.key;
final element = entry.value;
final path = element['Path'] ?? '';
// ---------- TEXT ----------
if (element.containsKey('Text')) {
// prefer Bounds; fallback to CharBounds if available
List boundsList = element['Bounds'] ??
element['CharBounds'] ??
[0, 0, pageWidth, 20];
final left = (boundsList[0] as num).toDouble();
final bottom = (boundsList[1] as num).toDouble();
final right = (boundsList[2] as num).toDouble();
final top = (boundsList[3] as num).toDouble();
final y = pageHeight - top;
final x = left;
final ctl = _controllers[i];
final fontSize = (element['TextSize'] ?? 12).toDouble();
final font = element['Font'];
final fontFamily = font?['family_name'] ?? 'Arial';
final fontWeight = (font?['weight'] ?? 400) >= 600
? FontWeight.bold
: FontWeight.normal;
final fontStyle = (font?['italic'] ?? false)
? FontStyle.italic
: FontStyle.normal;
Color textColor = Colors.black;
if (element['attributes']?['rgb_color'] != null) {
final rgb = element['attributes']['rgb_color'];
textColor = Color.fromRGBO(
((rgb[0] ?? 0) * 255).toInt(),
((rgb[1] ?? 0) * 255).toInt(),
((rgb[2] ?? 0) * 255).toInt(),
1,
);
}
return Positioned(
left: x,
top: y + 20,
width: (right - left) + 30,
height: (top - bottom) + 50,
child: Container(
decoration: element['attributes']?['BBox'] != null
? BoxDecoration(
border: Border.all(color: Colors.black54, width: 1))
: null,
padding: const EdgeInsets.all(2),
child: TextFormField(
controller: ctl,
style: TextStyle(
fontSize: fontSize,
fontFamily: fontFamily,
fontWeight: fontWeight,
fontStyle: fontStyle,
color: textColor,
),
maxLines: null,
expands: true,
decoration: const InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.zero,
),
),
),
);
}
// ---------- LINES ----------
// if (path.toString().contains('Line')) {
// final bounds = element['Bounds'] as List;
// // if this line sits inside any table bbox, skip drawing it (prevents double borders)
// // if (_lineInsideAnyTable(bounds)) {
// // return const SizedBox.shrink();
// // }
// final left = (bounds[0] as num).toDouble();
// final bottom = (bounds[1] as num).toDouble();
// final right = (bounds[2] as num).toDouble();
// final top = (bounds[3] as num).toDouble();
// final y = pageHeight - top;
// final x = left;
// return Positioned(
// left: x,
// top: y,
// width: right - left,
// height: (top - bottom) == 0 ? 1 : (top - bottom),
// child: Container(color: Colors.black),
// );
// }
// ---------- TABLES (one rectangle per table) ----------
if (path.toString().contains('Table') &&
element['attributes']?['BBox'] != null) {
final bbox = element['attributes']['BBox'] as List;
final left = (bbox[0] as num).toDouble();
final bottom = (bbox[1] as num).toDouble();
final right = (bbox[2] as num).toDouble();
final top = (bbox[3] as num).toDouble();
final y = pageHeight - top;
final x = left;
return Positioned(
left: x,
top: y,
width: (right - left),
height: top - bottom,
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.black54, width: 1),
),
),
);
}
// ---------- FIGURES ----------
if (path.toString().contains('Figure') &&
element['filePaths'] != null) {
final bounds = element['Bounds'];
final left = (bounds[0] as num).toDouble();
final bottom = (bounds[1] as num).toDouble();
final right = (bounds[2] as num).toDouble();
final top = (bounds[3] as num).toDouble();
final y = pageHeight - top;
final x = left;
final filePath = element['filePaths'][0];
return Positioned(
left: x,
top: y,
width: (right - left),
height: (top - bottom),
child: Image.asset(filePath, fit: BoxFit.contain),
);
}
return const SizedBox.shrink();
}).toList(),
),
),
);
}
// ---------- MAKE PDF FROM EDITED CONTENT ----------
Future<void> _makePdfFromEditedContent() async {
if (_jsonData == null) return;
final pdf = pw.Document();
final page = _jsonData!['pages'][0];
final pageWidth = (page['width'] as num).toDouble();
final pageHeight = (page['height'] as num).toDouble();
final elements = _jsonData!['elements'] as List;
// Precompute table BBoxes (same logic as editor)
final List<Map<String, double>> tableBboxes = [];
for (final e in elements) {
final path = e['Path'] ?? '';
if (path.toString().contains('Table') &&
e['attributes']?['BBox'] != null) {
final bbox = e['attributes']['BBox'] as List;
tableBboxes.add({
'left': (bbox[0] as num).toDouble(),
'bottom': (bbox[1] as num).toDouble(),
'right': (bbox[2] as num).toDouble(),
'top': (bbox[3] as num).toDouble(),
});
}
}
bool _lineInsideAnyTable(List<num> bounds) {
final left = (bounds[0] as num).toDouble();
final bottom = (bounds[1] as num).toDouble();
final right = (bounds[2] as num).toDouble();
final top = (bounds[3] as num).toDouble();
const tol = 0.8;
for (final t in tableBboxes) {
if (left >= t['left']! - tol &&
right <= t['right']! + tol &&
bottom >= t['bottom']! - tol &&
top <= t['top']! + tol) {
return true;
}
final onLeftOrRightEdge = ((left - t['left']!).abs() < tol) ||
((right - t['right']!).abs() < tol);
final onTopOrBottomEdge = ((top - t['top']!).abs() < tol) ||
((bottom - t['bottom']!).abs() < tol);
if ((onLeftOrRightEdge &&
top <= t['top']! + tol &&
bottom >= t['bottom']! - tol) ||
(onTopOrBottomEdge &&
right <= t['right']! + tol &&
left >= t['left']! - tol)) {
return true;
}
}
return false;
}
pdf.addPage(
pw.Page(
pageFormat: PdfPageFormat(pageWidth, pageHeight),
build: (context) {
final widgets = <pw.Widget>[];
for (int i = 0; i < elements.length; i++) {
final element = elements[i];
final path = element['Path'] ?? '';
// ---------- TEXT ----------
if (element.containsKey('Text')) {
final boundsList = element['Bounds'] ??
element['CharBounds'] ??
[0, 0, pageWidth, 20];
final left = (boundsList[0] as num).toDouble();
final bottom = (boundsList[1] as num).toDouble();
final right = (boundsList[2] as num).toDouble();
final top = (boundsList[3] as num).toDouble();
final y = pageHeight - top;
final x = left;
final ctl = _controllers[i];
final text = ctl?.text ?? (element['Text'] ?? '');
final fontSize = (element['TextSize'] ?? 12).toDouble();
final font = element['Font'];
final fontWeight = (font?['weight'] ?? 400) >= 600
? pw.FontWeight.bold
: pw.FontWeight.normal;
final fontStyle = (font?['italic'] ?? false)
? pw.FontStyle.italic
: pw.FontStyle.normal;
PdfColor textColor = PdfColors.black;
final rgb = element['attributes']?['rgb_color'];
if (rgb != null && rgb is List && rgb.length >= 3) {
textColor = PdfColor.fromInt(
(0xFF << 24) |
(((((rgb[0] ?? 0) as num) * 255).toInt()) << 16) |
(((((rgb[1] ?? 0) as num) * 255).toInt()) << 8) |
((((rgb[2] ?? 0) as num) * 255).toInt()),
);
}
final isBold = (font?['weight'] ?? 400) >= 600;
widgets.add(
pw.Positioned(
left: x,
top: y + 25,
child: pw.Container(
width: (right - left) + 25,
height: (top - bottom),
child: pw.Text(
text,
style: pw.TextStyle(
font: isBold ? robotoBold : robotoRegular,
fontSize: fontSize,
fontWeight: fontWeight,
fontStyle: fontStyle,
color: textColor,
),
maxLines: null,
softWrap: true,
),
),
),
);
}
// ---------- LINES ----------
// if (path.toString().contains('Line')) {
// final bounds = element['Bounds'] as List;
// // if (_lineInsideAnyTable(bounds)) {
// // // skip if this line is part of a table bbox (prevents double borders)
// // continue;
// // }
// final left = (bounds[0] as num).toDouble();
// final bottom = (bounds[1] as num).toDouble();
// final right = (bounds[2] as num).toDouble();
// final top = (bounds[3] as num).toDouble();
// final y = pageHeight - top;
// final x = left;
// widgets.add(
// pw.Positioned(
// left: x,
// top: y,
// child: pw.Container(
// width: right - left,
// height: (top - bottom) == 0 ? 1 : (top - bottom),
// color: PdfColors.black,
// ),
// ),
// );
// }
// ---------- TABLES (draw single bbox per table) ----------
if (path.toString().contains('Table') &&
element['attributes']?['BBox'] != null) {
final bbox = element['attributes']['BBox'] as List;
final left = (bbox[0] as num).toDouble();
final bottom = (bbox[1] as num).toDouble();
final right = (bbox[2] as num).toDouble();
final top = (bbox[3] as num).toDouble();
final x = left;
final y = pageHeight - top;
final strokeWidth =
((element['attributes']?['line_width'] ?? 1) as num)
.toDouble();
widgets.add(
pw.Positioned(
left: x,
top: y,
child: pw.Container(
width: (right - left),
height: top - bottom,
decoration: pw.BoxDecoration(
border: pw.Border.all(
color: PdfColors.black,
width: strokeWidth,
),
),
),
),
);
}
// ---------- FIGURES ----------
if (path.toString().contains('Figure') &&
element['filePaths'] != null) {
final bounds = element['Bounds'];
final left = (bounds[0] as num).toDouble();
final bottom = (bounds[1] as num).toDouble();
final right = (bounds[2] as num).toDouble();
final top = (bounds[3] as num).toDouble();
final y = pageHeight - top;
final x = left;
final filePath = element['filePaths'][0];
widgets.add(
pw.Positioned(
left: x,
top: y,
child: pw.Image(
pw.MemoryImage(File(filePath).readAsBytesSync()),
width: (right - left),
height: (top - bottom),
fit: pw.BoxFit.contain),
),
);
}
}
return pw.Stack(children: widgets);
},
),
);
final dir = await getTemporaryDirectory();
final file = File('${dir.path}/edited_output.pdf');
await file.writeAsBytes(await pdf.save());
await OpenFile.open(file.path);
}
}
`
For output see the attached image, the output borders are not good enough, can someone tell how to optimise this for more accurate borders and text alignment??
I tried many pdf files but the accurate borders, boxes and alignment is not adjusting.