PdfTextExtractor.extractText() crashes with a TypeError (Null check operator used on a null value) when a page's content stream invokes, via the Do operator, an XObject whose /Subtype is neither /Form nor /Image (e.g. the spec-legal /Subtype /PS). This also crashes SfPdfViewer on load, because SfPdfViewer._checkVisiblePages calls extractText() automatically for every visible page (even with enableTextSelection: false).
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:syncfusion_flutter_pdf/pdf.dart';
void main() {
test('PdfTextExtractor.extractText throws on a non-Form/-Image XObject', () {
final Uint8List bytes = _buildPdfWithNonFormXObject();
// ignore: avoid_print
print('PDF size: ${bytes.length} bytes');
// ignore: avoid_print
print('base64 (attach to the Syncfusion ticket):\n${base64.encode(bytes)}');
final PdfDocument document = PdfDocument(inputBytes: bytes);
addTearDown(document.dispose);
Object? thrown;
StackTrace? stack;
try {
PdfTextExtractor(document).extractText();
} catch (e, st) {
thrown = e;
stack = st;
}
// ignore: avoid_print
print('extractText threw: $thrown');
// ignore: avoid_print
print('Crash origin (expect _getXObject at pdf_text_extractor.dart:2291):');
// ignore: avoid_print
print(stack);
// Documents the current (buggy) behaviour: extractText throws a null-check
// TypeError instead of skipping the unsupported XObject. When Syncfusion
// ships the guard, this expectation flips to returnsNormally.
expect(thrown, isA<TypeError>());
expect(
stack.toString(),
contains('pdf_text_extractor.dart'),
reason: 'crash should originate in the text extractor',
);
});
}
/// Hand-builds a minimal, valid single-page PDF whose only content operator is
/// `/X0 Do`, where `/X0` is a `/Subtype /PS` XObject. xref byte offsets are
/// computed so the file parses cleanly.
Uint8List _buildPdfWithNonFormXObject() {
const String psStream = '0 0 moveto';
const String contentStream = 'q /X0 Do Q';
final List<String> objects = <String>[
'<< /Type /Catalog /Pages 2 0 R >>',
'<< /Type /Pages /Kids [3 0 R] /Count 1 >>',
'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] '
'/Resources << /XObject << /X0 4 0 R >> >> /Contents 5 0 R >>',
// Non-Form, non-Image XObject — render() returns null for this, and the
// unguarded `!` in _getXObject then throws.
'<< /Type /XObject /Subtype /PS /Length ${psStream.length} >>\n'
'stream\n$psStream\nendstream',
'<< /Length ${contentStream.length} >>\nstream\n$contentStream\nendstream',
];
final StringBuffer sb = StringBuffer('%PDF-1.4\n');
final List<int> offsets = <int>[];
for (int i = 0; i < objects.length; i++) {
offsets.add(sb.length);
sb.write('${i + 1} 0 obj\n${objects[i]}\nendobj\n');
}
final int xrefOffset = sb.length;
final int size = objects.length + 1;
sb.write('xref\n0 $size\n');
sb.write('0000000000 65535 f \n');
for (final int off in offsets) {
sb.write('${off.toString().padLeft(10, '0')} 00000 n \n');
}
sb.write('trailer\n<< /Size $size /Root 1 0 R >>\n');
sb.write('startxref\n$xrefOffset\n%%EOF');
return Uint8List.fromList(latin1.encode(sb.toString()));
}
Bug description
PdfTextExtractor.extractText() crashes with a TypeError (Null check operator used on a null value) when a page's content stream invokes, via the Do operator, an XObject whose /Subtype is neither /Form nor /Image (e.g. the spec-legal /Subtype /PS). This also crashes SfPdfViewer on load, because SfPdfViewer._checkVisiblePages calls extractText() automatically for every visible page (even with enableTextSelection: false).
Code sample
On which target platforms have you observed this bug?
Web