flutter_freetype 1.0.1 copy "flutter_freetype: ^1.0.1" to clipboard
flutter_freetype: ^1.0.1 copied to clipboard

Cross-platform FreeType bindings for Flutter, with native FFI support and bundled WebAssembly for the web.

example/lib/main.dart

import 'dart:typed_data';

import 'package:flutter_freetype/flutter_freetype.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(const FreetypeExampleApp());
}

class FreetypeExampleApp extends StatelessWidget {
  const FreetypeExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'flutter_freetype example',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xff2563eb)),
        useMaterial3: true,
      ),
      home: const FreetypeExamplePage(),
    );
  }
}

class FreetypeExamplePage extends StatefulWidget {
  const FreetypeExamplePage({super.key});

  @override
  State<FreetypeExamplePage> createState() => _FreetypeExamplePageState();
}

class _FreetypeExamplePageState extends State<FreetypeExamplePage> {
  late final Future<RenderedGlyph> _glyph;

  @override
  void initState() {
    super.initState();
    _glyph = _renderGlyph();
  }

  Future<RenderedGlyph> _renderGlyph() async {
    final fontData = await rootBundle.load('assets/fonts/Basic-Regular.ttf');
    final bytes = fontData.buffer.asUint8List();
    final ft = await Freetype.create();

    try {
      final face = ft.newFaceFromBytes(bytes);
      try {
        face.setPixelSizes(0, 72);
        final loaded = face.loadChar('A'.codeUnitAt(0), LoadFlag.RENDER);
        if (!loaded) {
          throw StateError('FT_Load_Char failed for "A".');
        }

        final bitmap = face.glyph.bitmap();
        return RenderedGlyph(
          bytes: Uint8List.fromList(
            bitmap.buffer.map((value) => value & 0xff).toList(),
          ),
          width: bitmap.width,
          rows: bitmap.rows,
          pitch: bitmap.pitch.abs(),
          bitmapLeft: face.glyph.bitmapLeft,
          bitmapTop: face.glyph.bitmapTop,
        );
      } finally {
        face.free();
      }
    } finally {
      ft.free();
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('flutter_freetype')),
      body: SafeArea(
        child: Center(
          child: ConstrainedBox(
            constraints: const BoxConstraints(maxWidth: 520),
            child: Padding(
              padding: const EdgeInsets.all(24),
              child: FutureBuilder<RenderedGlyph>(
                future: _glyph,
                builder: (context, snapshot) {
                  if (snapshot.connectionState != ConnectionState.done) {
                    return const Column(
                      mainAxisSize: MainAxisSize.min,
                      children: [
                        CircularProgressIndicator(),
                        SizedBox(height: 16),
                        Text('Initializing FreeType...'),
                      ],
                    );
                  }

                  if (snapshot.hasError) {
                    return _StatusPanel(
                      icon: Icons.error_outline,
                      title: 'FreeType initialization failed',
                      message: snapshot.error.toString(),
                    );
                  }

                  final glyph = snapshot.requireData;
                  return Column(
                    mainAxisSize: MainAxisSize.min,
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: [
                      Text(
                        'FreeType is running on this platform',
                        style: Theme.of(context).textTheme.headlineSmall,
                        textAlign: TextAlign.center,
                      ),
                      const SizedBox(height: 20),
                      Center(
                        child: CustomPaint(
                          size: const Size.square(220),
                          painter: GlyphPainter(glyph),
                        ),
                      ),
                      const SizedBox(height: 20),
                      Text(
                        'Glyph "A" rendered from '
                        'assets/fonts/Basic-Regular.ttf',
                        style: Theme.of(context).textTheme.bodyLarge,
                        textAlign: TextAlign.center,
                      ),
                      const SizedBox(height: 8),
                      Text(
                        '${glyph.width}x${glyph.rows}, pitch ${glyph.pitch}, '
                        'bearing ${glyph.bitmapLeft}/${glyph.bitmapTop}',
                        style: Theme.of(context).textTheme.bodyMedium,
                        textAlign: TextAlign.center,
                      ),
                    ],
                  );
                },
              ),
            ),
          ),
        ),
      ),
    );
  }
}

class _StatusPanel extends StatelessWidget {
  const _StatusPanel({
    required this.icon,
    required this.title,
    required this.message,
  });

  final IconData icon;
  final String title;
  final String message;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Icon(icon, size: 48, color: Theme.of(context).colorScheme.error),
        const SizedBox(height: 16),
        Text(title, style: Theme.of(context).textTheme.headlineSmall),
        const SizedBox(height: 8),
        SelectableText(message, textAlign: TextAlign.center),
      ],
    );
  }
}

class RenderedGlyph {
  const RenderedGlyph({
    required this.bytes,
    required this.width,
    required this.rows,
    required this.pitch,
    required this.bitmapLeft,
    required this.bitmapTop,
  });

  final Uint8List bytes;
  final int width;
  final int rows;
  final int pitch;
  final int bitmapLeft;
  final int bitmapTop;
}

class GlyphPainter extends CustomPainter {
  const GlyphPainter(this.glyph);

  final RenderedGlyph glyph;

  @override
  void paint(Canvas canvas, Size size) {
    final background = Paint()..color = const Color(0xfff8fafc);
    canvas.drawRRect(
      RRect.fromRectAndRadius(Offset.zero & size, const Radius.circular(8)),
      background,
    );

    final cell = (size.shortestSide * 0.72) / glyph.rows.clamp(1, 9999);
    final glyphWidth = glyph.width * cell;
    final glyphHeight = glyph.rows * cell;
    final origin = Offset(
      (size.width - glyphWidth) / 2,
      (size.height - glyphHeight) / 2,
    );
    final paint = Paint()..color = const Color(0xff111827);

    for (var y = 0; y < glyph.rows; y++) {
      for (var x = 0; x < glyph.width; x++) {
        final index = y * glyph.pitch + x;
        if (index >= glyph.bytes.length) continue;

        final alpha = glyph.bytes[index];
        if (alpha == 0) continue;

        paint.color = Color.fromARGB(alpha, 17, 24, 39);
        canvas.drawRect(
          Rect.fromLTWH(origin.dx + x * cell, origin.dy + y * cell, cell, cell),
          paint,
        );
      }
    }
  }

  @override
  bool shouldRepaint(GlyphPainter oldDelegate) => oldDelegate.glyph != glyph;
}
1
likes
130
points
105
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Cross-platform FreeType bindings for Flutter, with native FFI support and bundled WebAssembly for the web.

Repository (GitHub)
View/report issues

Topics

#freetype #ffi #fonts #typography #wasm

License

MIT (license)

Dependencies

ffi, flutter, flutter_web_plugins

More

Packages that depend on flutter_freetype

Packages that implement flutter_freetype