tex_document 0.1.0
tex_document: ^0.1.0 copied to clipboard
A Flutter package for rendering TeX documents as native widgets. Supports academic papers with math, figures, tables, citations, and bibliography.
import 'dart:io';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:tex_document/tex_document.dart';
import 'package:pdfrx/pdfrx.dart';
void main(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
pdfrxFlutterInitialize();
final autoCapture = args.contains('--capture');
var paper = 'attention';
for (final arg in args) {
if (arg.startsWith('--paper=')) {
paper = arg.substring('--paper='.length);
}
}
runApp(TexReaderApp(autoCapture: autoCapture, initialPaper: paper));
}
class TexReaderApp extends StatelessWidget {
final bool autoCapture;
final String initialPaper;
const TexReaderApp({super.key, this.autoCapture = false, this.initialPaper = 'attention'});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter TeX Example',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.indigo,
brightness: Brightness.light,
),
scaffoldBackgroundColor: Colors.white,
),
home: PaperView(autoCapture: autoCapture, initialPaper: initialPaper),
);
}
}
class PaperView extends StatefulWidget {
final bool autoCapture;
final String initialPaper;
const PaperView({super.key, this.autoCapture = false, this.initialPaper = 'attention'});
@override
State<PaperView> createState() => _PaperViewState();
}
class _PaperViewState extends State<PaperView> {
late Future<TexDocument> _document;
final _scrollController = ScrollController();
final _repaintKey = GlobalKey();
bool _capturing = false;
late String _currentPaper;
static const _papers = {
'attention': (
base: 'assets/papers/attention',
main: 'ms.tex',
subFiles: [
'introduction.tex',
'background.tex',
'model_architecture.tex',
'why_self_attention.tex',
'training.tex',
'results.tex',
'visualizations.tex',
],
),
'ntk': (
base: 'assets/papers/ntk',
main: 'main_Arxiv2.tex',
subFiles: ['main_Arxiv2.bbl'],
),
'vae': (
base: 'assets/papers/vae',
main: 'iclr14_sva.tex',
subFiles: ['iclr14_sva_appendix.tex', 'shared/commands.tex'],
),
};
@override
void initState() {
super.initState();
_currentPaper = widget.initialPaper;
_document = _loadPaper(_currentPaper);
}
void _switchPaper(String paper) {
setState(() {
_currentPaper = paper;
_document = _loadPaper(paper);
});
}
Future<TexDocument> _loadPaper(String paper) async {
final config = _papers[paper]!;
final base = config.base;
final fileCache = <String, String>{};
final bblFiles = <String>[];
for (final name in config.subFiles) {
try {
fileCache[name] = await rootBundle.loadString('$base/$name');
if (name.endsWith('.bbl')) bblFiles.add(name);
} catch (_) {}
}
final source = await rootBundle.loadString('$base/${config.main}');
final parser = TexParser((filename) => fileCache[filename], bblFiles: bblFiles);
final doc = parser.parse(source);
if (widget.autoCapture) {
WidgetsBinding.instance.addPostFrameCallback((_) {
Future.delayed(const Duration(seconds: 2), _captureScreenshots);
});
}
return doc;
}
Future<void> _captureScreenshots() async {
setState(() => _capturing = true);
final basePath = Platform.environment['SCREENSHOT_DIR'] ?? '${Directory.current.path}/screenshots';
final dir = Directory('$basePath/$_currentPaper');
if (!dir.existsSync()) dir.createSync(recursive: true);
debugPrint('Saving screenshots to: ${dir.path}');
var index = 0;
_scrollController.jumpTo(0);
await Future.delayed(const Duration(milliseconds: 500));
while (true) {
await Future.delayed(const Duration(milliseconds: 200));
final boundary = _repaintKey.currentContext?.findRenderObject() as RenderRepaintBoundary?;
if (boundary != null) {
final image = await boundary.toImage(pixelRatio: 2.0);
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
if (byteData != null) {
final file = File('${dir.path}/${_currentPaper}_${index.toString().padLeft(2, '0')}.png');
file.writeAsBytesSync(byteData.buffer.asUint8List());
debugPrint('Captured screenshot $index');
}
}
final maxScroll = _scrollController.position.maxScrollExtent;
final currentScroll = _scrollController.offset;
if (currentScroll >= maxScroll - 1) break;
final viewportHeight = _scrollController.position.viewportDimension;
final nextScroll = (currentScroll + viewportHeight * 0.8).clamp(0.0, maxScroll);
_scrollController.jumpTo(nextScroll);
index++;
}
setState(() => _capturing = false);
debugPrint('Captured ${index + 1} screenshots total');
if (widget.autoCapture) {
exit(0);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: DropdownButton<String>(
value: _currentPaper,
underline: const SizedBox(),
items: _papers.keys.map((p) => DropdownMenuItem(value: p, child: Text(p.toUpperCase()))).toList(),
onChanged: (v) => v != null ? _switchPaper(v) : null,
),
elevation: 1,
actions: [
if (_capturing)
const Padding(
padding: EdgeInsets.all(16),
child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)),
)
else
IconButton(
icon: const Icon(Icons.camera_alt),
onPressed: _captureScreenshots,
tooltip: 'Capture screenshots',
),
],
),
body: RepaintBoundary(
key: _repaintKey,
child: FutureBuilder<TexDocument>(
future: _document,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(32),
child: Text(
'Error: ${snapshot.error}\n\n${snapshot.stackTrace}',
style: const TextStyle(color: Colors.red, fontFamily: 'monospace', fontSize: 12),
),
),
);
}
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
return DocumentRenderer(
document: snapshot.data!,
scrollController: _scrollController,
assetBasePath: _papers[_currentPaper]!.base,
textTheme: const TexTextTheme(
body: TextStyle(fontFamily: 'NotoSerif', fontSize: 15, height: 1.7),
title: TextStyle(fontFamily: 'NotoSerif', fontSize: 24, fontWeight: FontWeight.bold, height: 1.3),
h1: TextStyle(fontFamily: 'NotoSerif', fontSize: 20, fontWeight: FontWeight.bold, height: 1.3),
h2: TextStyle(fontFamily: 'NotoSerif', fontSize: 17, fontWeight: FontWeight.bold, height: 1.3),
h3: TextStyle(fontFamily: 'NotoSerif', fontSize: 15, fontWeight: FontWeight.bold, height: 1.3),
h4: TextStyle(fontFamily: 'NotoSerif', fontSize: 14, fontWeight: FontWeight.bold, height: 1.5),
caption: TextStyle(fontFamily: 'NotoSerif', fontSize: 12, height: 1.5),
bibliography: TextStyle(fontFamily: 'NotoSerif', fontSize: 11, height: 1.4),
),
);
},
),
),
);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
}