From 2ce3f01f9938fd8e9f930ccde34b692273342053 Mon Sep 17 00:00:00 2001 From: mym0404 Date: Sat, 4 May 2024 17:00:03 +0900 Subject: [PATCH] feat: getBase64PngData, getBase64JpegData --- README.md | 26 +- example/lib/main.dart | 628 ++++++++++++++++++---------------- ios/Classes/FLPencilKit.swift | 51 ++- lib/src/pencil_kit.dart | 21 ++ package.json | 3 +- 5 files changed, 421 insertions(+), 308 deletions(-) diff --git a/README.md b/README.md index c2c4469..01fe0fb 100644 --- a/README.md +++ b/README.md @@ -45,18 +45,20 @@ flutter pub add pencil_kit Methods available for `PencilKitController`. -| Method | Description | Throws | -|------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------|--------| -| clear() | Clear canvas | X | -| show() | Show Palette | X | -| hide() | Hide Palette | X | -| redo() | Redo last drawing action | X | -| undo() | Undo last drawing action | X | -| save(): Future | Save drawing data into file system, can return base 64 data if `withBase64Data` is true | O | -| load(): Future | Load drawing data from file system, can return base 64 data if `withBase64Data` is true | O | -| getBase64Data(): Future | Get current drawing data as base64 string form | O | -| loadBase64Data(String base64Data): Future | Load base64 drawing data into canvas | O | -| setPKTool({required ToolType toolType, double? width, Color? color}): Future | Set `PKTool` type with width and color | X | +| Method | Description | Throws | Etc | +|------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------|--------|------------------------------------------------------------------------------| +| clear() | Clear canvas | X | | +| show() | Show Palette | X | | +| hide() | Hide Palette | X | | +| redo() | Redo last drawing action | X | | +| undo() | Undo last drawing action | X | | +| save(): Future | Save drawing data into file system, can return base 64 data if `withBase64Data` is true | O | | +| load(): Future | Load drawing data from file system, can return base 64 data if `withBase64Data` is true | O | | +| getBase64Data(): Future | Get current drawing data as base64 string form | O | | +| loadBase64Data(String base64Data): Future | Load base64 drawing data into canvas | O | | +| getBase64PngData(): Future | Get current drawing data as png base64 string form | O | scale = 0 means use default UIScreen.main.scale | +| getBase64JpegData(): Future | Get current drawing data as jpeg base64 string form | O | scale = 0 means use default UIScreen.main.scale. default compression is 0.93 | +| setPKTool({required ToolType toolType, double? width, Color? color}): Future | Set `PKTool` type with width and color | X | | ## Caution for `setPKTool` diff --git a/example/lib/main.dart b/example/lib/main.dart index 68fc8e4..cb5bc3a 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,6 @@ // ignore_for_file: avoid_print +import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; @@ -24,6 +25,7 @@ class _MyAppState extends State { ToolType currentToolType = ToolType.pen; double currentWidth = 1; Color currentColor = Colors.black; + String base64Image = ''; @override Widget build(BuildContext context) { @@ -40,7 +42,7 @@ class _MyAppState extends State { visualDensity: VisualDensity.compact), home: Scaffold( appBar: AppBar( - title: const Text('PencilKit Example'), + title: const Text('PencilKit'), actions: [ IconButton( icon: const Icon(Icons.palette), @@ -64,304 +66,348 @@ class _MyAppState extends State { ), ], ), - body: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + body: Stack( children: [ - SingleChildScrollView( - child: Row( - children: [ - IconButton( - icon: const Icon(Icons.save), - onPressed: () async { - final Directory documentDir = - await getApplicationDocumentsDirectory(); - final String pathToSave = '${documentDir.path}/drawing'; - try { - final data = await controller.save( - uri: pathToSave, withBase64Data: true); - if (kDebugMode) { - print(data); - } - Fluttertoast.showToast( - msg: "Save Success to [$pathToSave]", - toastLength: Toast.LENGTH_LONG, - gravity: ToastGravity.CENTER, - timeInSecForIosWeb: 1, - backgroundColor: Colors.blueAccent, - textColor: Colors.white, - fontSize: 12.0); - } catch (e) { - Fluttertoast.showToast( - msg: "Save Failed to [$pathToSave]", - toastLength: Toast.LENGTH_LONG, - gravity: ToastGravity.CENTER, - timeInSecForIosWeb: 1, - backgroundColor: Colors.redAccent, - textColor: Colors.white, - fontSize: 12.0); - } - }, - tooltip: "Save", - ), - IconButton( - icon: const Icon(Icons.download), - onPressed: () async { - final Directory documentDir = - await getApplicationDocumentsDirectory(); - final String pathToLoad = '${documentDir.path}/drawing'; - try { - final data = await controller.load( - uri: pathToLoad, withBase64Data: true); - if (kDebugMode) { - print(data); - } - Fluttertoast.showToast( - msg: "Load Success from [$pathToLoad]", - toastLength: Toast.LENGTH_LONG, - gravity: ToastGravity.CENTER, - timeInSecForIosWeb: 1, - backgroundColor: Colors.blueAccent, - textColor: Colors.white, - fontSize: 12.0); - } catch (e) { - Fluttertoast.showToast( - msg: "Load Failed from [$pathToLoad]", - toastLength: Toast.LENGTH_LONG, - gravity: ToastGravity.CENTER, - timeInSecForIosWeb: 1, - backgroundColor: Colors.redAccent, - textColor: Colors.white, - fontSize: 12.0); - } - }, - tooltip: "Load", - ), - IconButton( - icon: const Icon(Icons.print), - onPressed: () async { - final data = await controller.getBase64Data(); - Fluttertoast.showToast( - msg: data, - toastLength: Toast.LENGTH_LONG, - gravity: ToastGravity.CENTER, - timeInSecForIosWeb: 1, - backgroundColor: Colors.blueAccent, - textColor: Colors.white, - fontSize: 12.0); - }, - tooltip: "Get base64 data", - ), - ], - ), - ), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SingleChildScrollView( + child: Row( children: [ - ToolType.pen, - ToolType.pencil, - ToolType.marker, - ToolType.monoline, - ToolType.fountainPen, - ToolType.watercolor, - ToolType.crayon - ] - .map( - (e) => TextButton( - onPressed: () { - setState(() { - currentToolType = e; - controller.setPKTool( - toolType: e, - width: currentWidth, - color: currentColor, - ); - }); - }, - child: Text( - '${e.name}${e.isAvailableFromIos17 ? ' (iOS17)' : ''}'), - ), - ) - .toList())), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - TextButton( - onPressed: () { - setState(() { - currentToolType = ToolType.eraserVector; - controller.setPKTool( - toolType: currentToolType, - width: currentWidth, - color: currentColor, - ); - }); - }, - child: const Text('Vector Eraser'), - ), - TextButton( - onPressed: () { - setState(() { - currentToolType = ToolType.eraserBitmap; - controller.setPKTool( - toolType: currentToolType, - width: currentWidth, - color: currentColor, - ); - }); - }, - child: const Text('Bitmap Eraser'), - ), - TextButton( - onPressed: () { - setState(() { - currentToolType = ToolType.eraserFixedWidthBitmap; - controller.setPKTool( - toolType: currentToolType, - width: currentWidth, - color: currentColor, - ); - }); - }, - child: const Text('FixedWidthBitmap Eraser(iOS 16.4)'), - ), - ], - )), - SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - IconButton( - icon: Container( - color: Colors.black, - width: 12, - height: 1, - ), - onPressed: () { - setState(() { - currentWidth = 1; - controller.setPKTool( - toolType: currentToolType, - width: currentWidth, - color: currentColor, - ); - }); - }, - ), - IconButton( - icon: Container( - color: Colors.black, - width: 12, - height: 3, + IconButton( + icon: const Icon(Icons.save), + onPressed: () async { + final Directory documentDir = + await getApplicationDocumentsDirectory(); + final String pathToSave = + '${documentDir.path}/drawing'; + try { + final data = await controller.save( + uri: pathToSave, withBase64Data: true); + if (kDebugMode) { + print(data); + } + Fluttertoast.showToast( + msg: "Save Success to [$pathToSave]", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.CENTER, + timeInSecForIosWeb: 1, + backgroundColor: Colors.blueAccent, + textColor: Colors.white, + fontSize: 12.0); + } catch (e) { + Fluttertoast.showToast( + msg: "Save Failed to [$pathToSave]", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.CENTER, + timeInSecForIosWeb: 1, + backgroundColor: Colors.redAccent, + textColor: Colors.white, + fontSize: 12.0); + } + }, + tooltip: "Save", ), - onPressed: () { - setState(() { - currentWidth = 3; - controller.setPKTool( - toolType: currentToolType, - width: currentWidth, - color: currentColor, - ); - }); - }, - ), - IconButton( - icon: Container( - color: Colors.black, - width: 12, - height: 5, + IconButton( + icon: const Icon(Icons.download), + onPressed: () async { + final Directory documentDir = + await getApplicationDocumentsDirectory(); + final String pathToLoad = + '${documentDir.path}/drawing'; + try { + final data = await controller.load( + uri: pathToLoad, withBase64Data: true); + if (kDebugMode) { + print(data); + } + Fluttertoast.showToast( + msg: "Load Success from [$pathToLoad]", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.CENTER, + timeInSecForIosWeb: 1, + backgroundColor: Colors.blueAccent, + textColor: Colors.white, + fontSize: 12.0); + } catch (e) { + Fluttertoast.showToast( + msg: "Load Failed from [$pathToLoad]", + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.CENTER, + timeInSecForIosWeb: 1, + backgroundColor: Colors.redAccent, + textColor: Colors.white, + fontSize: 12.0); + } + }, + tooltip: "Load", ), - onPressed: () { - setState(() { - currentWidth = 5; - controller.setPKTool( - toolType: currentToolType, - width: currentWidth, - color: currentColor, - ); - }); - }, - ), - const VerticalDivider(), - IconButton( - icon: const Icon( - Icons.lens, - color: Colors.orange, + IconButton( + icon: const Icon(Icons.print), + onPressed: () async { + final data = await controller.getBase64Data(); + Fluttertoast.showToast( + msg: data, + toastLength: Toast.LENGTH_LONG, + gravity: ToastGravity.CENTER, + timeInSecForIosWeb: 1, + backgroundColor: Colors.blueAccent, + textColor: Colors.white, + fontSize: 12.0); + }, + tooltip: "Get base64 data", ), - onPressed: () { - setState(() { - currentColor = Colors.orange; - controller.setPKTool( - toolType: currentToolType, - width: currentWidth, - color: currentColor, - ); - }); - }, - ), - IconButton( - icon: const Icon( - Icons.lens, - color: Colors.purpleAccent, + IconButton( + icon: const Icon(Icons.image), + onPressed: () async { + final data = await controller.getBase64PngData(); + setState(() { + base64Image = data; + }); + }, + tooltip: "Get base64 png data", ), - onPressed: () { - setState(() { - currentColor = Colors.purpleAccent; - controller.setPKTool( - toolType: currentToolType, - width: currentWidth, - color: currentColor, - ); - }); - }, - ), - IconButton( - icon: const Icon( - Icons.lens, - color: Colors.greenAccent, + IconButton( + icon: const Icon(Icons.image), + onPressed: () async { + final data = await controller.getBase64JpegData(); + setState(() { + base64Image = data; + }); + }, + tooltip: "Get base64 jpeg data", ), - onPressed: () { - setState(() { - currentColor = Colors.greenAccent; - controller.setPKTool( - toolType: currentToolType, - width: currentWidth, - color: currentColor, - ); - }); - }, - ), - ], - )), - Expanded( - child: PencilKit( - onPencilKitViewCreated: (controller) => - this.controller = controller, - alwaysBounceVertical: false, - alwaysBounceHorizontal: true, - isRulerActive: false, - drawingPolicy: PencilKitIos14DrawingPolicy.anyInput, - backgroundColor: Colors.yellow.withOpacity(0.1), - isOpaque: false, - toolPickerVisibilityDidChange: (isVisible) => - print('toolPickerVisibilityDidChange $isVisible'), - toolPickerIsRulerActiveDidChange: (isRulerActive) => - print('toolPickerIsRulerActiveDidChange $isRulerActive'), - toolPickerFramesObscuredDidChange: () => - print('toolPickerFramesObscuredDidChange'), - toolPickerSelectedToolDidChange: () => - print('toolPickerSelectedToolDidChange'), - canvasViewDidBeginUsingTool: () => - print('canvasViewDidBeginUsingTool'), - canvasViewDidEndUsingTool: () => - print('canvasViewDidEndUsingTool'), - canvasViewDrawingDidChange: () => - print('canvasViewDrawingDidChange'), - canvasViewDidFinishRendering: () => - print('canvasViewDidFinishRendering'), - ), + ], + ), + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + ToolType.pen, + ToolType.pencil, + ToolType.marker, + ToolType.monoline, + ToolType.fountainPen, + ToolType.watercolor, + ToolType.crayon + ] + .map( + (e) => TextButton( + onPressed: () { + setState(() { + currentToolType = e; + controller.setPKTool( + toolType: e, + width: currentWidth, + color: currentColor, + ); + }); + }, + child: Text( + '${e.name}${e.isAvailableFromIos17 ? ' (iOS17)' : ''}'), + ), + ) + .toList())), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + TextButton( + onPressed: () { + setState(() { + currentToolType = ToolType.eraserVector; + controller.setPKTool( + toolType: currentToolType, + width: currentWidth, + color: currentColor, + ); + }); + }, + child: const Text('Vector Eraser'), + ), + TextButton( + onPressed: () { + setState(() { + currentToolType = ToolType.eraserBitmap; + controller.setPKTool( + toolType: currentToolType, + width: currentWidth, + color: currentColor, + ); + }); + }, + child: const Text('Bitmap Eraser'), + ), + TextButton( + onPressed: () { + setState(() { + currentToolType = ToolType.eraserFixedWidthBitmap; + controller.setPKTool( + toolType: currentToolType, + width: currentWidth, + color: currentColor, + ); + }); + }, + child: + const Text('FixedWidthBitmap Eraser(iOS 16.4)'), + ), + ], + )), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + IconButton( + icon: Container( + color: Colors.black, + width: 12, + height: 1, + ), + onPressed: () { + setState(() { + currentWidth = 1; + controller.setPKTool( + toolType: currentToolType, + width: currentWidth, + color: currentColor, + ); + }); + }, + ), + IconButton( + icon: Container( + color: Colors.black, + width: 12, + height: 3, + ), + onPressed: () { + setState(() { + currentWidth = 3; + controller.setPKTool( + toolType: currentToolType, + width: currentWidth, + color: currentColor, + ); + }); + }, + ), + IconButton( + icon: Container( + color: Colors.black, + width: 12, + height: 5, + ), + onPressed: () { + setState(() { + currentWidth = 5; + controller.setPKTool( + toolType: currentToolType, + width: currentWidth, + color: currentColor, + ); + }); + }, + ), + const VerticalDivider(), + IconButton( + icon: const Icon( + Icons.lens, + color: Colors.orange, + ), + onPressed: () { + setState(() { + currentColor = Colors.orange; + controller.setPKTool( + toolType: currentToolType, + width: currentWidth, + color: currentColor, + ); + }); + }, + ), + IconButton( + icon: const Icon( + Icons.lens, + color: Colors.purpleAccent, + ), + onPressed: () { + setState(() { + currentColor = Colors.purpleAccent; + controller.setPKTool( + toolType: currentToolType, + width: currentWidth, + color: currentColor, + ); + }); + }, + ), + IconButton( + icon: const Icon( + Icons.lens, + color: Colors.greenAccent, + ), + onPressed: () { + setState(() { + currentColor = Colors.greenAccent; + controller.setPKTool( + toolType: currentToolType, + width: currentWidth, + color: currentColor, + ); + }); + }, + ), + ], + )), + Expanded( + child: PencilKit( + onPencilKitViewCreated: (controller) => + this.controller = controller, + alwaysBounceVertical: false, + alwaysBounceHorizontal: true, + isRulerActive: false, + drawingPolicy: PencilKitIos14DrawingPolicy.anyInput, + backgroundColor: Colors.yellow.withOpacity(0.1), + isOpaque: false, + toolPickerVisibilityDidChange: (isVisible) => + print('toolPickerVisibilityDidChange $isVisible'), + toolPickerIsRulerActiveDidChange: (isRulerActive) => print( + 'toolPickerIsRulerActiveDidChange $isRulerActive'), + toolPickerFramesObscuredDidChange: () => + print('toolPickerFramesObscuredDidChange'), + toolPickerSelectedToolDidChange: () => + print('toolPickerSelectedToolDidChange'), + canvasViewDidBeginUsingTool: () => + print('canvasViewDidBeginUsingTool'), + canvasViewDidEndUsingTool: () => + print('canvasViewDidEndUsingTool'), + canvasViewDrawingDidChange: () => + print('canvasViewDrawingDidChange'), + canvasViewDidFinishRendering: () => + print('canvasViewDidFinishRendering'), + ), + ), + ], ), + if (base64Image.isNotEmpty) + Positioned( + bottom: 128, + right: 24, + child: Container( + width: 160, + height: 160, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: const BorderRadius.all( + Radius.circular(12), + ), + border: Border.all(color: Colors.black12)), + child: Image.memory( + base64Decode(base64Image), + ), + )), ], ), ), diff --git a/ios/Classes/FLPencilKit.swift b/ios/Classes/FLPencilKit.swift index ff4c93b..b98fc80 100644 --- a/ios/Classes/FLPencilKit.swift +++ b/ios/Classes/FLPencilKit.swift @@ -84,6 +84,10 @@ class FLPencilKit: NSObject, FlutterPlatformView { load(pencilKitView: pencilKitView, call: call, result: result) case "getBase64Data": getBase64Data(pencilKitView: pencilKitView, call: call, result: result) + case "getBase64PngData": + getBase64PngData(pencilKitView: pencilKitView, call: call, result: result) + case "getBase64JpegData": + getBase64JpegData(pencilKitView: pencilKitView, call: call, result: result) case "loadBase64Data": loadBase64Data(pencilKitView: pencilKitView, call: call, result: result) case "applyProperties": @@ -127,6 +131,35 @@ class FLPencilKit: NSObject, FlutterPlatformView { result(base64Data) } + @available(iOS 13, *) + private func getBase64PngData( + pencilKitView: PencilKitView, + call: FlutterMethodCall, + result: FlutterResult + ) { + if let base64Data = pencilKitView.getBase64PngData(scale: (call.arguments as! [Double])[0]) { + result(base64Data) + } else { + result(FlutterError(code: "NATIVE_ERROR", message: "getBase64PngData failed", details: nil)) + } + } + + @available(iOS 13, *) + private func getBase64JpegData( + pencilKitView: PencilKitView, + call: FlutterMethodCall, + result: FlutterResult + ) { + if let base64Data = pencilKitView.getBase64JpegData( + scale: (call.arguments as! [Double])[0], + compression: (call.arguments as! [Double])[1] + ) { + result(base64Data) + } else { + result(FlutterError(code: "NATIVE_ERROR", message: "getBase64JpegData failed", details: nil)) + } + } + @available(iOS 13, *) private func loadBase64Data( pencilKitView: PencilKitView, @@ -201,7 +234,7 @@ private class PencilKitView: UIView { toolPicker?.addObserver(self) toolPicker?.setVisible(true, forFirstResponder: canvasView) } - + private func layoutCanvasView() { addSubview(canvasView) NSLayoutConstraint.activate([ @@ -329,6 +362,16 @@ private class PencilKitView: UIView { canvasView.drawing.dataRepresentation().base64EncodedString() } + func getBase64PngData(scale: Double) -> String? { + let image = canvasView.drawing.image(from: canvasView.bounds, scale: scale) + return image.pngData()?.base64EncodedString() + } + + func getBase64JpegData(scale: Double, compression: Double) -> String? { + let image = canvasView.drawing.image(from: canvasView.bounds, scale: scale) + return image.jpegData(compressionQuality: compression)?.base64EncodedString() + } + func loadBase64Data(base64Data: String) throws { let data = Data(base64Encoded: base64Data)! let drawing = try PKDrawing(data: data) @@ -409,15 +452,15 @@ extension PencilKitView: PKToolPickerObserver { func toolPickerVisibilityDidChange(_ toolPicker: PKToolPicker) { channel.invokeMethod("toolPickerVisibilityDidChange", arguments: toolPicker.isVisible) } - + func toolPickerIsRulerActiveDidChange(_ toolPicker: PKToolPicker) { channel.invokeMethod("toolPickerIsRulerActiveDidChange", arguments: toolPicker.isRulerActive) } - + func toolPickerFramesObscuredDidChange(_ toolPicker: PKToolPicker) { channel.invokeMethod("toolPickerFramesObscuredDidChange", arguments: nil) } - + func toolPickerSelectedToolDidChange(_ toolPicker: PKToolPicker) { channel.invokeMethod("toolPickerSelectedToolDidChange", arguments: nil) } diff --git a/lib/src/pencil_kit.dart b/lib/src/pencil_kit.dart index 13fabad..5367e75 100644 --- a/lib/src/pencil_kit.dart +++ b/lib/src/pencil_kit.dart @@ -349,6 +349,27 @@ class PencilKitController { return await _channel.invokeMethod('getBase64Data') as String; } + /// Get current drawing data as png base 64 encoded form. + /// + /// Throws an [Error] if failed + /// ``` + Future getBase64PngData({double scale = 0}) async { + return await _channel.invokeMethod('getBase64PngData', [scale]) + as String; + } + + /// Get current drawing data as jpeg base 64 encoded form. + /// + /// Throws an [Error] if failed + /// ``` + Future getBase64JpegData( + {double scale = 0, double compression = 0.93}) async { + return await _channel.invokeMethod('getBase64JpegData', [ + scale, + compression, + ]) as String; + } + /// Load drawing data from base 64 encoded form. /// ``` /// Throws an [Error] if failed diff --git a/package.json b/package.json index f0ea857..e7e6d1e 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "scripts": { "prepare": "husky", - "check:all": "dart format --set-exit-if-changed . && flutter analyze" + "check:all": "dart format --set-exit-if-changed . && flutter analyze", + "t": "yarn check:all" } }