Files
StackChan/app/lib/util/extension.dart
T
2026-04-27 12:16:53 +08:00

240 lines
7.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
import 'dart:convert';
import 'dart:ui' as ui;
import 'package:flex_color_picker/flex_color_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:image/image.dart' as img;
extension HexExtension on Uint8List {
String toHexString() {
return map(
(byte) => byte.toRadixString(16).padLeft(2, '0'),
).join().toUpperCase();
}
}
// 定义NeedReplaceKeyValueFor
final projectStringReplacement = {
"小智": "Xiaozhi",
"Qwen3 实时": "Qwen3 235B (Fast)",
"DeepSeek V3.1": "DeepSeek V3.1 (Powerful)",
"DouBao Seed 1.6": "Doubao Seed 1.6 (Delayed)",
"GLM 4.7(内测)": "GLM 4.7Internal Test",
"Kimi-K2(内测)": "Kimi-K2Internal Test",
"Doubao 2.0(内测)": "Doubao 2.0Internal Test",
"Qwen3.5 397B(内测)": "Qwen3.5 397BInternal Test",
};
extension StringTool on String? {
/// Regex批量ReplaceString
/// 按照 projectStringReplacement 定义规ThenReplaceAllMatchContent
String? regularExpressionSubstitution() {
// 1. NullValueDirectlyReturns null
if (this == null) {
return null;
}
// 2. 拿到Non-NullString
String result = this!;
// 3. IterateReplaceDictionary,逐个ReplaceAllMatchItem
for (final entry in projectStringReplacement.entries) {
// Escape特殊字符,AvoidRegex报错(比如括号,点号等)
final pattern = RegExp.escape(entry.key);
// GlobalReplaceAllMatchContent
result = result.replaceAll(RegExp(pattern), entry.value);
}
return result;
}
}
extension StringToUint8List on String? {
///Convert String? to Uint8List
Uint8List toUint8List() {
if (this == null || this!.isEmpty) {
return Uint8List(0);
}
return Uint8List.fromList(utf8.encode(this!));
}
///Convert Hex string to Color object
///Supported formats: "0xFFFFFFFF", "#FFFFFF", "FFFFFF"
Color hex() {
if (this == null || this!.isEmpty) return CupertinoColors.transparent;
String hexString = this!.toUpperCase().replaceAll("#", "");
if (hexString.startsWith("0X")) {
hexString = hexString.substring(2);
}
if (hexString.length == 6) {
hexString = "FF$hexString";
}
final intValue = int.tryParse(hexString, radix: 16);
return Color(intValue ?? 0x00000000);
}
}
extension ColorExtension on Color? {
///Convert Color to hex string (e.g., #RRGGBB)
String hexString() {
if (this == null) return "#000000";
//Extract RGB channels and convert to hex, ignore Alpha to match standard color codes
String r = this!.red8bit.toRadixString(16).padLeft(2, '0');
String g = this!.green8bit.toRadixString(16).padLeft(2, '0');
String b = this!.blue8bit.toRadixString(16).padLeft(2, '0');
return "#$r$g$b".toUpperCase();
}
}
extension ImageExtension on Uint8List {
Future<Uint8List?> compress({
ui.Size? resolutionSize,
double? memorySize,
bool cropCenter = false,
}) async {
//Use compute isolation to avoid blocking UI thread when processing large images
return compute(
_compressImage,
_CompressParams(
bytes: this,
resolutionSize: resolutionSize,
memorySize: memorySize,
cropCenter: cropCenter,
),
);
}
Future<Uint8List?> compressToMemorySize(double memorySize) async {
return compress(
resolutionSize: null,
memorySize: memorySize,
cropCenter: false,
);
}
}
//Compression parameter wrapper (for compute isolation)
class _CompressParams {
final Uint8List bytes;
final ui.Size? resolutionSize;
final double? memorySize;
final bool cropCenter;
_CompressParams({
required this.bytes,
this.resolutionSize,
this.memorySize,
required this.cropCenter,
});
}
//Core compression logic (top-level function for compute isolation)
Future<Uint8List?> _compressImage(_CompressParams params) async {
try {
//1. Decode original image
img.Image? originalImage = img.decodeImage(params.bytes);
if (originalImage == null) return null; //Return null on decode failure
img.Image processedImage = originalImage;
//2. Handle resolution scaling/cropping (align with iOS logic)
if (params.resolutionSize != null) {
final targetWidth = params.resolutionSize!.width.toInt();
final targetHeight = params.resolutionSize!.height.toInt();
if (params.cropCenter) {
//CropCenter=true: Scale to cover target size then center crop (Aspect-Fill)
final scaleX = targetWidth / originalImage.width;
final scaleY = targetHeight / originalImage.height;
final scale = scaleX > scaleY
? scaleX
: scaleY; //Take larger scale ratio
//Scale image to cover target size
final scaledWidth = (originalImage.width * scale).toInt();
final scaledHeight = (originalImage.height * scale).toInt();
final scaledImage = img.copyResize(
originalImage,
width: scaledWidth,
height: scaledHeight,
);
//Calculate center crop offset
final cropX = (scaledWidth - targetWidth) ~/ 2;
final cropY = (scaledHeight - targetHeight) ~/ 2;
//Execute center crop
processedImage = img.copyCrop(
scaledImage,
x: cropX,
y: cropY,
width: targetWidth,
height: targetHeight,
);
} else {
//CropCenter=false: Aspect-Fit scaling, draw to target size canvas
final scaleX = targetWidth / originalImage.width;
final scaleY = targetHeight / originalImage.height;
final scale = scaleX < scaleY
? scaleX
: scaleY; //Take smaller scale ratio
//Scale image to fit target size
final newWidth = (originalImage.width * scale).toInt();
final newHeight = (originalImage.height * scale).toInt();
final scaledImage = img.copyResize(
originalImage,
width: newWidth,
height: newHeight,
);
//Create target size canvas, draw scaled image at top-left (align with iOS draw logic)
final canvas = img.Image(width: targetWidth, height: targetHeight);
//Critical fix: Use direct blend mode (normal overlay, no color blending)
img.compositeImage(
canvas,
scaledImage,
dstX: 0,
dstY: 0,
blend: img.BlendMode.direct,
);
processedImage = canvas;
}
}
//3. Handle memory size compression (JPEG quality adjustment)
if (params.memorySize == null) {
//No memory limit, return 100% quality JPEG
return img.encodeJpg(processedImage, quality: 100);
}
//Calculate max bytes (MB → Bytes)
final maxBytes = (params.memorySize! * 1024 * 1024).toInt();
int quality = 100; //Corresponds to iOS compressionQuality=1.0
//Null safety fix: encodeJpg may return null, need ? and handling
Uint8List compressedData = img.encodeJpg(processedImage, quality: quality);
//Gradually reduce quality (×0.7 each time) until size limit met or quality below 1%
while (compressedData.length > maxBytes && quality > 1) {
quality = (quality * 0.7).round();
if (quality < 1) quality = 1; //Minimum quality limit is 1%
compressedData = img.encodeJpg(processedImage, quality: quality);
}
return compressedData;
} catch (e) {
debugPrint('图片压缩Failed:$e');
return null;
}
}