flutter_freetype 1.0.1
flutter_freetype: ^1.0.1 copied to clipboard
Cross-platform FreeType bindings for Flutter, with native FFI support and bundled WebAssembly for the web.
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;
}