0

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.

4
  • @AkshayGupta No, this is only about Flutter PDF border alignment. Please keep comments technical and on-topic. Commented Sep 9, 2025 at 7:21
  • "For output see the attached image," there is no attached image Commented Sep 9, 2025 at 8:23
  • @pskink Please check i have added screenshot through link. Commented Sep 9, 2025 at 9:24
  • Thanks for your comment, is there any free library to render the json or edit the scanned pdf into flutter or web? Commented Sep 9, 2025 at 11:30

0

Know someone who can answer? Share a link to this question via email, Twitter, or Facebook.

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.