tex_document 0.1.0 copy "tex_document: ^0.1.0" to clipboard
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.

example/lib/main.dart

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();
  }
}
1
likes
150
points
9
downloads

Documentation

API reference

Publisher

verified publisherfinereli.com

Weekly Downloads

A Flutter package for rendering TeX documents as native widgets. Supports academic papers with math, figures, tables, citations, and bibliography.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, flutter_math_fork, image, pdfrx

More

Packages that depend on tex_document