为了复用flutter能力,提高代码维护性,所以将原生生成水印的操作移动到flutter层面处理,减少了原生代码的维护复杂程度。(当然在flutter层处理图片,效率没有原生层面高)
考虑:
1.首先考虑到代码侵入性,所以决定继承ImageProvider。
2.考虑到缓存的问题,如果需要每次生成新的缓存,就需要将cacheKey所相关的逻辑进行改动。
如果水印的内容如果不同,就需要每次修改缓存键,来生成不同的水印图片。
这里我将cacheKey+url作为水印图片的缓存,防止只通过url获取到的缓存图片是同一张。
Future<ui.Codec> _loadImageAsync(
String url,
String? cacheKey,
StreamController<ImageChunkEvent> chunkEvents,
DecoderCallback decode,
BaseCacheManager cacheManager,
int? maxHeight,
int? maxWidth,
Map<String, String>? headers,
Function()? errorListener,
Function() evictImage,
) async {
...
final cacheDefine = "$url/cache=$cacheKey";
Completer<ui.Codec> completer = Completer();
/// if cacheKey is not null,first to get File from cache
if (cacheKey != null) {
FileInfo? cacheFile = await cacheManager.getFileFromCache(cacheDefine,
ignoreMemCache: true);
if (cacheFile != null && cacheFile.validTill.isAfter(DateTime.now())) {
var bytes = await cacheFile.file.readAsBytes();
if (!completer.isCompleted) completer.complete(await decode(bytes));
}
}
...
}
主要分成2个步骤:
1.首先将文件流转换成ui.Image类型的数据,是通过any2Image()来实现。
2.将需要绘制的内容绘制到合成图片的文件流,然后返回,这个是通过generateWaterMaskData()实现。
绘制好的内容可以根据新生成的缓存规则,存在缓存中以便后用。
var stream = cacheManager is ImageCacheManager
? cacheManager.getImageFile(url,
maxHeight: maxHeight,
maxWidth: maxWidth,
withProgress: true,
headers: headers)
: cacheManager.getFileStream(url,
withProgress: true, headers: headers);
streamSubscription = stream.listen((result) async {
if (result is DownloadProgress) {
chunkEvents.add(ImageChunkEvent(
cumulativeBytesLoaded: result.downloaded,
expectedTotalBytes: result.totalSize,
));
}
if (result is FileInfo) {
var file = result.file;
var bytes = await file.readAsBytes();
if (settingModel != null) {
var image = await any2Image(Convert2ImageType.DATA, data: bytes);
var waterCacheData =
await generateWaterMaskData(image, settingModel!);
var cacheBytes = waterCacheData!.buffer.asUint8List();
cacheManager.putFile(cacheDefine, cacheBytes,
maxAge: const Duration(days: 7),
fileExtension: fileName?.getMine() ?? 'file');
if (!completer.isCompleted) {
await chunkEvents.close();
completer.complete(await decode(cacheBytes));
}
} else {
if (!completer.isCompleted) {
await chunkEvents.close();
completer.complete(await decode(bytes));
}
}
streamSubscription?.cancel();
}
});
首先将文件流转换成ui.Image,这个过程很简单,根据类型来判定传入的数据是文件流还是图片,如果是图片路径则需要先转换成文件流。主要是要注意生成的图片宽高,最好可以按比例缩小一部分,否则可能会影响到水印生成的速度,这里我设置了0.85的缩放系数。
Future<ui.Image> any2Image(Convert2ImageType type,
{String? path,
Uint8List? data,
int? width,
int? height,
double scaleRatio = 0.85}) async {
late Uint8List codecData;
if (type == Convert2ImageType.DATA) {
codecData = data!;
} else {
codecData = File(path!).readAsBytesSync();
}
late ui.Codec codec;
if (width != null && width != 0 && height != null && height != 0) {
codec = await ui.instantiateImageCodec(codecData,
targetWidth: width * scaleRatio ~/ 1,
targetHeight: height * scaleRatio ~/ 1);
} else {
codec = await ui.instantiateImageCodec(codecData);
}
ui.FrameInfo fi =
await codec.getNextFrame().whenComplete(() => codec.dispose());
return fi.image;
}
generateWaterMaskData()实现逻辑:
1.考虑到水印不能和图片的整体颜色相近导致看不清,所以这时使用palette_generator库,截取了一部分的图片区域作为样本,取得样本中出现次数最多的色号,然后判断RGB值是偏明还是偏暗,如果偏暗则水印的颜色使用白色,反之亦然。
2.原文件图片作为背景,绘制到一个新的Canvas中。
3.为了水印生成的层次清晰,需要将图片根据宽度分成N等份,然后根据行数进行交错渲染。同时绘制时,会将Canvas进行一定角度的旋转,然后将图片绘制上。结束绘制后Canvas必须要按照原来的角度旋转回来,坐标系会根据根据当前的角度进行累加,导致绘制效果异常。
4.将叠加绘制的Canvas转换成文件流,此时可以选择根据原文件流的尺寸进行部分缩放,否则文件流大小将影响整体的处理速度。最后将图片流占用的内存释放即可。
Future<ByteData?> generateWaterMaskData(
ui.Image originImage, ImageWaterMarkSettingModel settingModel) async {
PaletteGenerator? res = await PaletteGenerator.fromImage(originImage,
region: Rect.fromCenter(
center: Offset((originImage.width / 4), (originImage.height / 4)),
width: (originImage.width / 4),
height: (originImage.height / 4)));
PaletteColor? paletteColor = res.dominantColor;
var i = 0;
if ((paletteColor?.color.red ?? 0) < 128) {
i++;
}
if ((paletteColor?.color.blue ?? 0) < 128) {
i++;
}
if ((paletteColor?.color.green ?? 0) < 128) {
i++;
}
Color renderColor =
i >= 2 ? settingModel.lightColor : settingModel.darkColor;
///首先根据图片宽度分为四等份
settingModel.textWidth = originImage.width / 4;
///根据配置第一项生成水印(为了获取水印渲染高度)
var paragraph = _generateText(settingModel!, renderColor);
///startPosition 水印第一个元素与x、y轴间距
///verticalDistance 水印行y轴间距
double startPosition = 25, verticalDistance = 200;
late int height;
///获取图片高度
height = paragraph.height ~/ 1;
verticalDistance += (height / 600).ceil() * 50;
///计算图片可以配置多少行水印(当配置的角度非0时,由于倾斜的问题,可能会导致边角出现空余,所以角度不为0时,需要多渲染一行)
var heightNumber = settingModel.radians != 0
? (originImage.height - height) ~/ (height + verticalDistance) + 2
: (originImage.height - height) ~/ (height + verticalDistance) + 1;
ImageWaterMarkSettingModel tempSetting = settingModel;
double widthUnit = originImage.width / tempSetting.divided;
double heightUnit = originImage.height / tempSetting.divided;
var scaleFont = (originImage.width / 800) * settingModel.fontSize;
var textWidth = (originImage.width - startPosition) / 4;
var templateParagraph = _generateText(
ImageWaterMarkSettingModel(
fontSize: scaleFont,
textStyle: tempSetting.textStyle,
text: tempSetting.text,
textWidth: textWidth),
renderColor);
///再创建canvas
ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
ui.Canvas canvas = ui.Canvas(pictureRecorder);
ui.Paint paint = ui.Paint();
///原图大小绘制在最底层
canvas.drawImage(originImage, ui.Offset.zero, paint);
///一行默认渲染4个
for (var i = -1; i < 5; i++) {
for (var j = 0; j < heightNumber; j++) {
ImageWaterMarkSettingModel settings = ImageWaterMarkSettingModel();
settings.asset = tempSetting.asset;
settings.divided = tempSetting.divided;
settings.radians = tempSetting.radians;
settings.text = tempSetting.text;
settings.fontSize = scaleFont;
settings.textStyle = tempSetting.textStyle;
settings.textWidth = textWidth;
settings.type = tempSetting.type;
///由于要形成交错效果,所以每行的间距都会出现一段距离
settings.dx = (startPosition +
i * ((originImage.width - startPosition) / 2) +
(j % 2 == 0 ? 0 : 0 - (originImage.width - startPosition) / 2) /
2) /
widthUnit;
settings.dy =
(startPosition + j * (height + verticalDistance)) / heightUnit;
///如果是隔行,且为第一个元素,且角度为0,则该元素不需要渲染
if (j % 2 != 0 && i == 0 && settings.radians == 0) continue;
canvas.rotate(-settings.radians);
// if (waterMarkList[i].runtimeType == ui.Paragraph) {
///canvas以左上角点为原点建立坐标系
//以下出现2次rotate,因为旋转canvas后,整个画布坐标系会一起旋转,如不重置回原来,则下一次旋转的角度将根据此次角度叠加
canvas.drawParagraph(templateParagraph,
Offset(widthUnit * settings.dx, heightUnit * settings.dy));
// }
canvas.rotate(settings.radians);
}
}
///生成与原图尺寸一样的合成图片
ui.Image generateImage = await pictureRecorder
.endRecording()
.toImage(originImage.width * 0.9 ~/ 1, originImage.height * 0.9 ~/ 1);
ByteData? data = await generateImage
.toByteData(format: ui.ImageByteFormat.png)
.whenComplete(() {
originImage.dispose();
generateImage.dispose();
});
return Future.value(data);
}
最后该情况还有2点问题暂时没有解决:
1.如何加快整个过程的速度,目前通过断点和日志,可以发现大部分的耗时都是在图片转文件流的处理上,目前的解决的方案:
1.1通过缩小图片的尺寸,获取到更小的文件流大小来达到效果。
1.2通过isolate来分批处理不同逻辑。但是该方法发现最耗时的部分,无法放在子isolate中处 理。
2.另一个问题是,渲染的水印如果是文字,文字的大小似乎与系统的缩放比无关,导致渲染出来的字体大小不太完美。