隐私模糊

一款跨平台应用程序,用于模糊图像中的敏感数据,面向 iOS 和 Android 设备。主要使用 Flutter SDK 的 Dart 语言编写。

该应用现已在 Google Play 商店和 App Store 上架!

Get it on Google Play PrivacyBlur
## 该应用做什么?

该项目旨在为用户提供一个免费、简洁、简单的图像数据处理解决方案。
无应用内购买。无广告。无水印。无烦恼。永久免费,因为隐私不应收费。免费,因为我们关心!

代码风格

我们对该项目的代码风格有一些限制。
您可以在 CONTRIBUTING 文件中找到所有要求。

功能

  • 人脸检测
  • 模糊/像素化效果
  • 精细/粗糙颗粒效果
  • 圆形/方形区域
  • 导出到相机胶卷

Flutter SDK 设置

该应用运行在 Flutter SDK 2.2 和 Dart 2.13 上。
在处理项目之前,请确保运行 flutter upgrade

Windows

点击此处
Windows 安装指南

macOS

点击此处
macOS 安装指南

构建

该应用面向手机和平板电脑上的 iOS 和 Android。
桌面和 Web 平台可能会导致问题,目前未计划支持。

建议运行

flutter clean
flutter pub get

一旦出现构建问题。

iOS 构建

Flutter iOS 构建指南

您需要一台 macOS 机器才能运行 Flutter iOS 应用程序。
请同时确保您的机器上已正确安装了 CocoaPods 和 Xcode 的版本。

有关部署信息,请访问:iOS 部署指南

Android 构建

Flutter Android 构建指南

构建 Android 版本无需其他要求。

有关部署信息,请访问:Android 部署指南

结构

lib/--+--main.dart (entry point)
      |
      +--resources/--- images, fonts, strings, etc...
      |
      +--------src/--+----- app.dart (some inital code)
                     +---router.dart (navigation handling)
                     +-------di.dart (dependency injection) 
                     |
                     |
                     +--screens/--screen_name/
                     |                  +--helpers/-- (garbage place, like many events and states)
                     |                  +--widgets/-- (internal widgets for this screen)
                     |                  |
                     |                  +--repo.dart
                     |                  +--bloc.dart
                     |                  +--view.dart                                  
                     |                 
                     |
                     +--widgets/-- (common widgets for application)
                     |
                     |
                     +----utils/-- (some utils for application if necessary)

架构

为了构建一个可维护、可扩展和可测试的项目,该应用采用了架构模式和文件结构。
为了可读性,我们将文件大小限制在 300 行

导航使用 Flutter Navigation v1 保持相对简单。
router.dart 文件包含 2 个类中所有的路由和导航逻辑。

  • ScreenNavigator:实现并提供启用路由的方法
  • AppRouter:声明所有路由及其配置

为了能够单独测试每个路由,AppRouter 具有多个构造函数。每个路由也必须定义一个构造函数。
该构造函数会覆盖实际的初始路由,并在指定位置启动。

每个路由接收依赖注入实例和 AppRouter 实例本身。可选参数作为第三个参数。

依赖注入

依赖注入类用于按正确顺序注入应用程序的所有可测试部分。
它持有不同 bloc 和 repository 的 provider。该类的实例必须传递给视图,以便它们可以在 Provider/Consumer 结构中访问所有依赖项。

BLoC

在 Dart 中,使用 Streams 和 Provider 模式非常有用。因此,我们选择了流行的 BLoC 模式。
为每个屏幕实现一个 bloc 是有意义的,但前提是它需要持有和管理数据。通常,bloc 从依赖注入中接收相应的 repository。

BLoC 类文件位于屏幕视图的同一目录中。
每个视图都使用 events 来触发 bloc 中的 state 更新,而视图又会重新消费这些更新。
事件和状态位于屏幕文件夹下的 helpers 目录中。

为了能够决定发出哪个状态,bloc 会消费事件并根据事件处理决策。
每个事件后始终产生一些输出很重要,否则 UI 不会被更新并且会卡住。

Stream<SampleState> mapEventToState(SampleEvent event) async* {
  if (event is EventOne) {
    yield* handleStateOneChange(event);
  } else if (event is EventTwo) {
    yield* handleStateTwoChange(event);
  } ...
}

bloc 作为屏幕的逻辑组件,将所有逻辑与 UI 分开。因此,bloc 也不应处理任何 UI 部分。

视图

每个视图代表用作导航路由中屏幕的小部件。它应该只定义 UI 并消费/使用状态。
主要而言,最好将屏幕实现为 StatelessWidget,并且只在状态更新后重新渲染真正需要重新渲染的部分。

Bloc-Screen 被 MultiBlocProvider 包裹,后者从依赖注入中获取相应的 bloc,并通过 context 使其可用于嵌套的 BlocConsumer。

return MultiBlocProvider(
    providers: [_di.getSampleBloc()],
    child: BlocConsumer<SampleBloc, SampleState>(...)

对于简单的 UI 数据更改,例如显示 Toast 消息,我们使用了 bloc consumer 的 buildWhen 条件来防止整个重新渲染。只有 MessageBar 会通过监听器绘制在屏幕上方。

BlocConsumer<SampleBloc, SampleState>(
  listenWhen: (_, curState) => (curState is SimpleState),
  buildWhen: (_, curState) => !(curState is SimpleState),
  listener: (_, state) {
    showMessageOnSimpleState(...)
  }
  ...

视图的大部分内容都分成了单独的方法。这提高了可读性和模块化。一旦文件大小超过 300 行或小部件变得非常大,就有必要将它们移动到另一个文件。

小部件

小部件存在于通用和屏幕作用域级别。通用小部件位于 src/widgets 中,屏幕小部件位于 src/screen_name/widgets 中。
通用级别的小部件不应实现屏幕特定的配置,而应通过最小化设置使其可重用。
屏幕作用域中的小部件要么仅在此处使用,要么其用法非常具体于屏幕。

有状态小部件

持有状态的小部件的最佳实践是也内部管理它。例如

class WidgetState extends State<WidgetWithState> {
  late double _initialValue;

  @override
  void didUpdateWidget(covariant DeferredSlider oldWidget) {
    _initialValue = widget._initialValue;
    super.didUpdateWidget(oldWidget);
  }

  @override
  void initState() {
    _initialValue = widget._initialValue;
    super.initState();
  }
  
  ... Widget build ...
  
  void onChanged(doubleValue) {
    setState(() {
      _initialValue = doubleValue;
    });
    this.widget.onChanged(doubleValue);
  }
}

自适应小部件

该应用面向 iOS 和 Android,但在这两个平台上需要看起来不同。
这就是为什么我们将特定于平台(iOS/Android)的代码拆分到 src/widgets/adaptive_widgets_builder.dart 中。
我们注意到,Flutter 正在开始构建小部件的自适应版本。
当他们完成这些工作后,我们可以移除并用 Flutter 小部件的自适应版本替换此文件中的“构建器”。

国际化

该应用已翻译成

  • :us: 英语(默认语言)
  • :de: 德语

翻译文件位于 lib/resources/i18n 目录中。
要添加新语言,您需要执行以下操作:

  • 将语言代码添加到支持的语言中
  var delegate = await LocalizationDelegate.create(
      ...
      supportedLocales: [..., '<language_code>']);
  • lib/resources/i18n/<lang_code>.json 中为语言代码添加 json 文件
  • 从现有的 .json 文件复制结构
  • 一旦添加了新文本,就需要生成新的键。运行:flutter pub run build_runner build --delete-conflicting-outputs

主题

主题根据不同平台拆分,此外,Android 还有主题模式。
theme_provider.dart

  • iosTheme(提供主题模式拆分的 iOS ThemeData)
  • light(提供浅色 Android ThemeData)
  • dark(提供深色 Android ThemeData)

支持深色和浅色模式,具体取决于系统偏好设置。

// AppBuilder widget
themeMode: ThemeMode.system,

资产/资源

所有资产/资源都可以在 lib/resouces 中找到。
图片、图标和其他通用资源应放置在此处。

图标

目前,该应用使用自定义图标字体,其中包含用于工具栏选择中图像编辑工具的 4 个图标。
字体可以在此处生成:fluttericon.com

仅包含项目中使用的图标!

此外,Flutter 还提供了默认图标,可以从 CupertinoIconsIcons 类使用。
为了从一个地方处理图标,项目中有 icons_provider 类。
这是定义图标名称的地方,它可以有额外的逻辑来区分平台。

图片

图片可以添加到此处。为了处理不同屏幕分辨率的图像尺寸,可以将图像以 x2 和 x3 的尺寸添加到相关的屏幕。
在 Dart 端,像这样加载它就足够了:lib/resources/images/<image_name>。Dart 会处理分辨率之间的拆分。

目前,该应用仅使用 App Logo,它在启动/闪屏和主屏幕上至关重要。还建议限制静态图像文件的数量,因为它们会大大增加项目的大小。

字符串

此处包含应用程序中使用的所有文本字符串。计划在项目的后期阶段添加 i18n 和 i10n。
此处的所有字符串都需要移至翻译 .aab 文件。

ImageFilter 库描述

此库是单例模式。它在工作时使用大量内存,因此创建该类的多个实例是个非常糟糕的主意。

该类可以加载和过滤图像。

支持的过滤器

  • 像素化
  • 线性模糊(非高斯,太慢)

类特性

  • 提交前的更改预览
  • 撤销特定区域的更改(橡皮擦工具)
  • 处理方形和圆形区域
  • 一次处理多个区域
  • 将所有已处理的像素替换为选定的过滤器

如何使用此类

   import 'dart:ui' as img_tools;                           
           final file = File(filename);  
           final imageFilter = ImageAppFilter();
           var completer = Completer<img_tools.Image>();
           img_tools.decodeImageFromList(file.readAsBytesSync(), (result) {
             completer.complete(result);
           });
           var image = await completer.future;
           
           /// VERY IMPORTANT TO USE AWAIT HERE!!!
           var filteredImage = await imageFilter.setImage(_blocState.image);
           imageFilter.transactionStart();
           imageFilter.setMatrix(MatrixAppPixelate(20));
           imageFilter.apply2CircleArea(200, 200, 150);
           /// for preview before saving (only changed area of image will be updated)
           filteredImage = await imageFilter.getImage();

           imageFilter.setMatrix(MatrixAppBlur(20));
           imageFilter.apply2CircleArea(400, 400, 250);

           /// save to cached image, after that you can not cancel changes
           /// only if you load image again.
           imageFilter.transactionCommit();

           /// get saved image after transaction completed (complete merged image without changed part)
           filteredImage = await imageFilter.getImage();

方法

  • static void setMaxProcessedWidth(int) - 默认值为 1000,为了模糊速度优化,我们需要知道最大处理区域宽度。
  • Future<ImageFilterResult> setImage(dart:ui.Image) - 设置要过滤的图像。
  • Future<ImageFilterResult> getImage() - 如果事务处于活动状态,则仅返回已更改区域且不包含背景图像。如果事务已关闭,则返回完整的更新图像。
  • void transactionStart() - 为更改开启事务
  • void transactionCancel() - 重置更改并关闭事务
  • void transactionCommit() - 应用事务中的更改并关闭事务
  • void setFilter(ImageAppMatrix newMatrix) - 设置当前过滤器
  • void apply2SquareArea(int x1, int y1, double radius) - 将选定的过滤器应用于以 x1, y1 为中心、半径为 radius 的方形区域。
  • void apply2CircleArea(int x1, int y1, double radius) - 将选定的过滤器应用于以 x1, y1 为中心、半径为 radius 的圆形区域。
  • void cancelCurrent() - 取消当前事务中的所有更改
  • void cancelSquare(int, int, int, int) - 取消方形区域内的更改
  • void cancelCircle(int, int, int) - 取消圆形区域内的更改

过滤器

  • MatrixAppPixelate(int blockSize)
  • MatrixAppBlur(int blockSize) - 线性模糊

测试

在项目根目录或测试文件夹中运行 flutter test 命令。所有测试文件必须以 _test.dart 后缀结尾。

依赖项

只添加空安全依赖项!
该应用程序运行在健全的空安全模式下。因此,所有添加的依赖项都需要遵循此限制。

  • flutter_styled_toast: 用于警报
  • image_size_getter: 获取图像尺寸以决定是否应旋转图像
  • flutter_exif_rotation: 用于图像加载时的初始旋转
  • url_launcher: 用于在主屏幕打开链接
  • image_picker: 用于访问图像库
  • image: 用于图像文件处理
  • image_gallery_saver: 用于将图像保存到库
  • permission_handler: 用于访问操作系统权限状态
  • path_provider: 用于读取操作系统图像路径
  • bloc: 架构模式类的库
  • flutter_bloc: Dart bloc 库的 Flutter 附加功能
  • mockito: 用于测试中的模拟
  • bloc_test: 用于测试 bloc
  • flutter_translate: 为应用添加 i18n
  • flutter_translate_gen: 用于生成用于静态使用翻译的变量

GitHub

https://github.com/MATHEMA-GmbH/privacyblur