Add watermark to Flutter images, and Flutter saves widgets as images
Inscription
- Holding the sword in the world, starting from your accumulation, wherever you go, you must strive for perfection, that is, toss every day.
important news
There are two implementation ideas for adding a watermark. One is to decode the picture and the watermark, and then mix and re-encode, and the other is to synthesize by Widget.
The implementation idea adopted here is to use the stacked layout Stack to load the image and the watermark part. The watermark part may be a ready-made image or a text and other style components, and then wrap the Stack with the RepaintBoundary component, and then generate it through Widget The function of the picture can achieve the effect of saving the picture as a watermark.
As shown in the figure below, for a picture in the loaded local resource directory, click the check mark in the upper right corner and then combine the picture with the text in the lower right corner to generate a new picture and save it in the mobile phone directory storage.
Then the log entered in the Android Studio console is as follows:
flutter : The saved image address is / Users / androidlongs / Library / Developer / CoreSimulator / Devices / DD736D17 - AB7D - 47 CA - 81 C5 - 2D 5283944F 6 B / data / Containers / Data / Application / AE08A4F2 - 9D FE - 4 A23 - 80 A7 - 7 BEB153A30B0/Documents/723.png _ _ _ _
- 1
Because the ios simulator is used here, it is saved in the memory space used by the simulator. Open the saved path and view it.
From the saved picture, you can see that the watermark has been added.
Here, the display will encapsulate all the above-mentioned UI functions of display and operation in a StatefulWidget. Here, you can use a new way to open this page. When the displayed picture does not fill the screen, the surrounding area will be dark and translucent, that is, The background of the opened Widet page is transparent, the code is as follows:
/// Jump
Navigator . of ( context )
. push ( PageRouteBuilder (
opaque : false ,
pageBuilder : ( context , animation , secondaryAnimation ) {
return ImageWatermarkPage ( ) ;
} ) )
. then ( ( value ) {
if ( value != null ) {
///
print( "The saved image address is $value" ) ;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
For the page ImageWatermarkPage, it realizes the function of displaying pictures and watermarks. The code is as follows:
class ImageWatermarkPage extends StatefulWidget {
@override
_RawImageState createState() => _RawImageState();
}
class _RawImageState extends State < ImageWatermarkPage > {
///Generate the primary key of the stacked layout Stack of the image
GlobalKey _globalKey = GlobalKey ( ) ;
///Saving
bool isSaving = false ;
@override
Widget build ( BuildContext context ) {
return Scaffold (
///The page background is a semi-transparent gray
backgroundColor : Color ( 0x50cdcdcd ) ,
/// Fill the layout
body : Stack (
/// Constraint in the sub-Widget play where the position is not set Alignment
alignment : Alignment . center ,
children : [
///Generate the image and watermark part of the target image
buildWaterImageWidget ( ) ,
/// The operation part of saving the watermark image
buildSaveWidget ( ) ,
///Part of canceling the operation
buildCancleWidget ( ) ,
/// Progress displayed while the image is being
saved
buildLoadingWidget ( ) ,
] ,
) ,
) ;
}
. . .omit }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
The picture and watermark part of the generated target image are encapsulated in the buildWaterImageWidget method. Here is the UI construction that combines the picture and the watermark part through the cascade layout through the RepaintBoundary component. The code is as follows:
///Generate the picture and watermark part of the target image
Widget buildWaterImageWidget ( ) {
///You can create a separate subtree for its child elements
///It is equivalent to a small branch on the total tree Widgets
return RepaintBoundary (
key : _globalKey ,
/// Widget for generating images
child : Container (
/// full screen
width : MediaQuery . of ( context ) . size . width ,
height : MediaQuery . of ( context ) . size .height,
child: Stack(
children: [
Image.asset(
"assets/images/2.0x/s03.jpeg",
fit: BoxFit.fill,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
),
/// The watermark part in the lower right corner
Positioned (
bottom : 20 , right : 20 ,
child : Container (
padding : EdgeInsets . only ( left : 8 , right : 8 , top : 2 , bottom : 2 ) ,
decoration :
BoxDecoration ( border : Border . all (
color : Colors . red ,
width : 1.0
) ) ,
child : Text ( "This is the watermark" , style : TextStyle ( fontSize : 14 , color : Colors . white ) , ) ,
) ,
)
] ,
) ,
) ,
) ;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
For the method buildSaveWidget, it is a method that encapsulates the build save checkmark and function. The code is as follows:
///The operation part of saving the watermark image
Widget buildSaveWidget ( ) {
///The small check button is displayed in the upper right corner
return Positioned (
top : 40 ,
right : 20 ,
child : IconButton (
icon : Icon ( Icons . check_circle , color : Colors . blue , size : 33 , ) ,
onPressed : ( ) async {
///Update page display
setState ( ( ) {
isSaving = true ;
} ) ;
/// Save
Widget as ui.Image ui.Image _image = await ImageLoaderUtils.imageLoader.getImageFromWidget ( _globalKey
) through globalkey ;
///Asynchronously save this image in the internal storage directory of the phone
String localImagePath = await ImageUtils . imageUtils
. saveImageByUIImage ( _image , isEncode : false ) ;
/// After saving, close the current page and return the saved image path to the above A Page
Navigator . of ( context ) . pop ( localImagePath ) ;
} ,
) ,
) ;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
The part that cancels the operation is the close button in the upper left corner of the page. The function is relatively simple to build in the buildCancleWidget method. The code is as follows:
///Part of the
Widget that cancels the operation buildCancleWidget ( ) {
return Positioned (
top : 40 ,
left : 20 ,
child : IconButton (
icon : Icon ( Icons . clear , color : Colors . red , size : 33 , ) ,
onPressed : ( ) {
Navigator . of (context).pop();
},
),
);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
The buildLoadingWidget method encapsulates that when the user clicks save, the value of isSaving is updated to true, and then a small circle is displayed on the page to give the user a prompt feedback about the operation. The code is as follows:
///Progress displayed while the image is being saved
///A small circle
widget buildLoadingWidget ( ) {
return isSaving ? CircularProgressIndicator ( ) : Container ( ) ;
}
- 1
- 2
- 3
- 4
- 5
Here, the method of saving the Widget as an image, the getImageFromWidget method of ImageLoaderUtils, and the method saveImageByUIImage of saving the Image to the phone storage are as follows:
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';
/// Image loading tool
class ImageLoaderUtils {
//Privately construct
ImageLoaderUtils . _ ( ) ;
// singleton mode creates
static final ImageLoaderUtils imageLoader = ImageLoaderUtils . _ ( ) ;
// Convert a Widget to an image.Image object
Future < ui . Image > getImageFromWidget ( GlobalKey globalKey ) async {
// globalKey is the key of the widget that needs to be imaged
RenderRepaintBoundary boundary = globalKey . currentContext . findRenderObject ( ) ;
// Convert to image
ui . Image img = await boundary . toImage ( ) ;
return img ;
}
///Save the specified file to the directory space.
///[image] Here is the Image under the ui package used
///[picName] Save to the local file (picture) file name, such as test_image
///[endFormat] Save to the local file (picture) file format , such as png,
///[isReplace] When a file (picture) with the same name exists locally, true is to replace
///[isEncode] to encode the saved file (picture)
/// Finally save it to the local file (picture) ) is named picName.endFormat
Future < String > saveImageByUIImage ( ui . Image image ,
{ String picName ,
String endFormat = "png" ,
bool isReplace = true ,
bool isEncode = true } ) async{
///Get the local disk path
/*
* Obtained in the Android platform is /data/user/0/com.studyyoun.flutterbookcode/app_flutter
* This method gets the Documents path on the iOS platform
*/
Directory appDocDir = await getApplicationDocumentsDirectory();
String appDocPath = appDocDir.path;
///Splicing directory
if ( picName == null || picName . trim ( ) . length == 0 ) {
/// When the user does not specify picName, take the current time name
picName = "${DateTime.now() .millisecond.toString()}.$endFormat" ;
} else {
picName = "$picName.$endFormat" ;
}
if ( isEncode ) {
///Encrypt the saved image name
picName = md5 . convert ( utf8 . encode ( picName ) ) . toString ( ) ;
}
appDocPath = "$appDocPath/$picName";
///Check if the image exists
var file = File ( appDocPath ) ;
bool exist = await file . exists ( ) ;
if ( exist ) {
if ( isReplace ) {
///If the image exists, delete and replace
///If If the new image fails to load, the old image is also deleted
await file . delete ( ) ;
} else {
///If the image exists, it will not be downloaded
return null ;
}
}
ByteData byteData = await image. toByteData ( format : ImageByteFormat . png ) ;
Uint8List pngBytes = byteData . buffer . asUint8List ( ) ;
print ( "Saved image path $appDocPath" ) ;
///Save the data format of Uint8List
await File ( appDocPath ) . writeAsBytes ( pngBytes ) ;
return appDocPath;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
The path_provider is used here to get the storage directory of the phone, so you need to add dependencies:
#Get the phone storage directory
path_provider: ^1.6.9
- 1
- 2
The latest version of path_provider can be viewed in the pub China warehouse. Click here to view