2025.01.17
STAFF BLOG
スタッフブログ
TECHNICAL
テクログ

はじめに
お久しぶりです!
のりさんです。
前回はFlutterのCanvasを使って
ペイントアプリを作ってみました。
お絵描きしたものが前回は保存できなかったので
今回は画像の保存機能を作ってみようと思います。
前回の記事はこちら
追加した機能
①floatingActionButtonとして保存ボタンを配置
②canvas+pictureRecorderを使ってpng画像を作成
③htmlのアンカー要素にpng画像(base64変換)を埋め込んでダウンロードを実行

chromeで実行してみましたが、ちゃんと画像がダウンロードできました!
それではソースコードをみていきたいと思います。
ソースコード
前回作成のファイル「lib/main.dart」を少し修正をしています。
import 'dart:convert';
import 'dart:ui' as ui;
import 'dart:html' as html;
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
enum Mode {
pen,box, line, circle, text;
}
enum PaintWidth {
normal, wide, full;
double get strokeWidth {
return switch (this) {
normal => 2,
wide => 5,
full => 10,
};
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
var mode = Mode.pen;
var paintWidth = PaintWidth.normal;
var startOffset = const Offset(0, 0);
var isPreview = false;
List<void Function(ui.Canvas canvas, Size size)> drawHistory = [];
List<void Function(ui.Canvas canvas, Size size)> drawPreview = [];
List<ui.Offset> penPoints = [];
var imageWidth = 1000;
var imageHeight = 1000;
late List<Color> palette = createPalette();
late Color selectedColor = palette.first;
late Paint paint = createPaint();
@override
void initState() {
drawHistory.add((canvas, size) {
canvas.drawRect(
Rect.fromLTWH(0, 0, size.width, size.height),
Paint()..color = Colors.white
);
});
super.initState();
}
Paint createPaint() {
return Paint()..color = selectedColor
..strokeCap = StrokeCap.round
..style = PaintingStyle.stroke
..strokeWidth = paintWidth.strokeWidth;
}
List<Color> createPalette() {
return [
Colors.black,
Colors.grey,
Colors.brown,
Colors.red,
Colors.orange,
Colors.yellow,
Colors.green,
Colors.blue,
Colors.purple,
Colors.purpleAccent,
Colors.white,
Colors.grey.shade300,
Colors.brown.shade300,
Colors.pink.shade200,
Colors.orange.shade300,
Colors.yellow.shade300,
Colors.lime,
Colors.lightBlue.shade300,
Colors.purple.shade200,
Colors.purpleAccent.shade100,
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onPanStart: (details) {
startOffset = details.localPosition;
drawPreview = [...drawHistory];
if (mode == Mode.pen) {
penPoints = [Offset(startOffset.dx, startOffset.dy)];
var drawPaint = Paint.from(paint);
drawPreview.add((canvas, size) {
canvas.drawPoints(ui.PointMode.polygon, penPoints, drawPaint);
});
}
setState(() {
isPreview = true;
});
},
onPanUpdate: (details) {
var prevStartOffset = Offset(startOffset.dx, startOffset.dy);
var currentOffset = details.localPosition;
var drawPaint = Paint.from(paint);
var currentColor = Color(selectedColor.value);
// 範囲外判定
if (currentOffset.dx < imageWidth && currentOffset.dx >= 0 && currentOffset.dy < imageHeight && currentOffset.dy >= 0 ){
if (mode == Mode.pen) {
// 鉛筆プレビュー
penPoints.add(currentOffset);
} else if (mode == Mode.line) {
// 直線プレビュー
drawPreview = [
...drawHistory,
(canvas, size) {
canvas.drawLine(prevStartOffset, currentOffset, drawPaint);
}
];
} else if (mode == Mode.box) {
// 四角形プレビュー
drawPreview = [
...drawHistory,
(canvas, size) {
canvas.drawRect(Rect.fromLTRB(prevStartOffset.dx, prevStartOffset.dy, currentOffset.dx, currentOffset.dy) , drawPaint);
}
];
} else if (mode == Mode.circle) {
// 円形プレビュー
drawPreview = [
...drawHistory,
(canvas, size) {
var circlePath = Path()
..addOval(Rect.fromCircle(
center: Offset((prevStartOffset.dx + currentOffset.dx) / 2, (prevStartOffset.dy + currentOffset.dy) /2),
radius: currentOffset.dx - prevStartOffset.dx,
));
canvas.drawPath(circlePath, drawPaint);
}
];
} else if (mode == Mode.text) {
// テキストプレビュー
drawPreview = [
...drawHistory,
(canvas, size) {
var tp = TextPainter(
text: TextSpan(
text: "Flutter",
style: TextStyle(color: currentColor, fontSize: 10 + (currentOffset.dy - prevStartOffset.dy)),
),
textDirection: TextDirection.ltr,
);
tp.layout(minWidth: 0);
tp.paint(canvas, prevStartOffset);
}
];
}
setState(() {});
}
},
onPanEnd: (details) {
var drawPaint = Paint.from(paint);
setState(() {
if (mode == Mode.pen) {
var points = [...penPoints];
drawHistory = [...drawHistory, (canvas, size) {
canvas.drawPoints(ui.PointMode.polygon, points, drawPaint);
}];
} else {
drawHistory = [...drawPreview];
}
isPreview = false;
});
},
child: SizedBox(
width: imageWidth.toDouble(),
height: imageHeight.toDouble(),
child: CustomPaint(
painter: DrawHistoryPainter(
drawHistory:
isPreview ? drawPreview : drawHistory)))),
Container(
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
IconButton(
onPressed: ()=>setState((){
mode = Mode.pen;
}),
icon: const Icon(Icons.edit_outlined, color: Colors.black87),
style: IconButton.styleFrom(backgroundColor: mode == Mode.pen ? Colors.blue[100] : Colors.transparent),
),
const SizedBox(height: 10),
IconButton(
onPressed: ()=>setState((){
mode = Mode.line;
}),
icon: const Icon(Icons.border_color_outlined, color: Colors.black87),
style: IconButton.styleFrom(backgroundColor: mode == Mode.line ? Colors.blue[100] : Colors.transparent),
),
const SizedBox(height: 10),
IconButton(
onPressed: ()=>setState((){
mode = Mode.box;
}),
icon: const Icon(Icons.rectangle_outlined, color: Colors.black87),
style: IconButton.styleFrom(backgroundColor: mode == Mode.box ? Colors.blue[100] : Colors.transparent),
),
const SizedBox(height: 10),
IconButton(
onPressed: ()=>setState((){
mode = Mode.circle;
}),
icon: const Icon(Icons.circle_outlined, color: Colors.black87),
style: IconButton.styleFrom(backgroundColor: mode == Mode.circle ? Colors.blue[100] : Colors.transparent),
),
const SizedBox(height: 10),
IconButton(
onPressed: ()=>setState((){
mode = Mode.text;
}),
icon: const Icon(Icons.font_download_outlined, color: Colors.black87),
style: IconButton.styleFrom(backgroundColor: mode == Mode.text ? Colors.blue[100] : Colors.transparent),
)
],
),
),
Container(
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
IconButton(
onPressed: ()=>setState((){
paintWidth = PaintWidth.normal;
paint = createPaint();
}),
icon: const Icon(Icons.width_normal, color: Colors.black87),
style: IconButton.styleFrom(backgroundColor: paintWidth == PaintWidth.normal ? Colors.green[100] : Colors.transparent),
),
const SizedBox(height: 10),
IconButton(
onPressed: ()=>setState((){
paintWidth = PaintWidth.wide;
paint = createPaint();
}),
icon: const Icon(Icons.width_wide, color: Colors.black87),
style: IconButton.styleFrom(backgroundColor: paintWidth == PaintWidth.wide ? Colors.green[100] : Colors.transparent),
),
const SizedBox(height: 10),
IconButton(
onPressed: ()=>setState((){
paintWidth = PaintWidth.full;
paint = createPaint();
}),
icon: const Icon(Icons.width_full, color: Colors.black87),
style: IconButton.styleFrom(backgroundColor: paintWidth == PaintWidth.full ? Colors.green[100] : Colors.transparent),
),
],
),
),
Container(
width: 300,
padding: const EdgeInsets.all(10),
child: GridView.builder(
itemCount: palette.length,
itemBuilder: (context, index) {
return GestureDetector(
child: Container(
color: selectedColor == palette[index] ? Colors.blue.shade200 : Colors.transparent,
padding: const EdgeInsets.all(3),
child: ColoredBox(
color: palette[index],
),
),
onTap: () {
setState(() {
selectedColor = palette[index];
paint = createPaint();
});
},
);
},
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 10,
mainAxisSpacing: 0,
crossAxisSpacing: 2,
),
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: download,
child: const Icon(Icons.save),
),
);
}
void download() async {
var painter = DrawHistoryPainter(drawHistory: drawHistory);
var recorder = ui.PictureRecorder();
var canvas = ui.Canvas(recorder, ui.Rect.fromLTWH(0, 0, imageWidth.toDouble(), imageHeight.toDouble()));
painter.paint(canvas, Size(imageWidth.toDouble(), imageHeight.toDouble()));
var img = await recorder.endRecording().toImage(imageWidth, imageHeight);
var byteData = await img.toByteData(format: ui.ImageByteFormat.png);
var base64 = base64Encode(byteData!.buffer.asUint8List().toList());
var anchor = html.AnchorElement(href: "data:image/png;base64,$base64");
anchor.download = "ペイント画像.png";
anchor.click();
}
}
class DrawHistoryPainter extends CustomPainter {
final List<void Function(ui.Canvas canvas, Size size)> drawHistory;
DrawHistoryPainter({required this.drawHistory});
@override
void paint(Canvas canvas, Size size) {
for (var draw in drawHistory) {
draw(canvas, size);
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
おわりに
Canvasを使った画像保存を実装してみました。
今回は画像の保存のみですが
画像を編集できるパッケージもあるようなので
そのあたりも今後みてみたいと思います。
それではまた!