diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..d2ff20141ceed86d87c0ea5d99481973005bab2b --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +/node_modules +/oh_modules +/local.properties +/.idea +**/build +/.hvigor +.cxx +/.clangd +/.clang-format +/.clang-tidy +**/.test +/.appanalyzer \ No newline at end of file diff --git a/AppScope/app.json5 b/AppScope/app.json5 new file mode 100644 index 0000000000000000000000000000000000000000..a3e4ee7e35029d3d67822914c6e4c4e8988a098c --- /dev/null +++ b/AppScope/app.json5 @@ -0,0 +1,10 @@ +{ + "app": { + "bundleName": "com.example.pixelmapimageedit", + "vendor": "example", + "versionCode": 1000000, + "versionName": "1.0.0", + "icon": "$media:app_layered_image", + "label": "$string:app_name" + } +} diff --git a/AppScope/resources/base/element/string.json b/AppScope/resources/base/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..3f2c87a81b1c5ea7fa8b850b87c9594d5752f84f --- /dev/null +++ b/AppScope/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "PixelMapImageEdit" + } + ] +} diff --git a/AppScope/resources/base/media/app_background.png b/AppScope/resources/base/media/app_background.png new file mode 100644 index 0000000000000000000000000000000000000000..923f2b3f27e915d6871871deea0420eb45ce102f Binary files /dev/null and b/AppScope/resources/base/media/app_background.png differ diff --git a/AppScope/resources/base/media/app_foreground.png b/AppScope/resources/base/media/app_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..eb9427585b36d14b12477435b6419d1f07b3e0bb Binary files /dev/null and b/AppScope/resources/base/media/app_foreground.png differ diff --git a/AppScope/resources/base/media/app_layered_image.json b/AppScope/resources/base/media/app_layered_image.json new file mode 100644 index 0000000000000000000000000000000000000000..b71c16122943f248ace0f5fa26da38b71dbdbdde --- /dev/null +++ b/AppScope/resources/base/media/app_layered_image.json @@ -0,0 +1,7 @@ +{ + "layered-image": + { + "background" : "$media:app_background", + "foreground" : "$media:app_foreground" + } +} \ No newline at end of file diff --git a/README.en.md b/README.en.md index f3bb16ff982943076dd468e2380da1413e044d7c..58b1fcc663cda8b180b671308d200673d5f8750d 100644 --- a/README.en.md +++ b/README.en.md @@ -1,36 +1,89 @@ -# ImageCanvasEdit +# Image Edit -#### Description -构建Surface输入输出的图片编辑 +Learn how to edit images based on image encoding and decoding, including cropping, rotation, adjustment (brightness, transparency, saturation), translation, scaling, filters, watermarks, and more. -#### Software Architecture -Software architecture description +| ![dome1.png](screenshots%2Fdevice%2Fdome1.png) | ![dome2.png](screenshots%2Fdevice%2Fdome2.png) | ![dome3.png](screenshots%2Fdevice%2Fdome3.png) | +| ---------------------------------------------- | ---------------------------------------------- | ---------------------------------------------- | -#### Installation +| ![dome4.png](screenshots%2Fdevice%2Fdome4.png) | ![dome5.png](screenshots%2Fdevice%2Fdome5.png) | ![dome6.png](screenshots%2Fdevice%2Fdome6.png) | +| ---------------------------------------------- | ---------------------------------------------- | ---------------------------------------------- | -1. xxxx -2. xxxx -3. xxxx +### Concepts -#### Instructions +- Image decoding: refers to the process of decoding an archived image in supported formats into a unified pixel map for image display or processing in applications or systems. +- PixelMap: provides APIs to read or write image data and obtain image information. +- Image encoding: refers to the process of encoding a pixel map into an archived image in different formats (only JPEG, WebP, and PNG currently) for subsequent processing, such as storage and transmission. +- Canvas: a canvas component for custom drawing of graphics, images, and other content. +- OffscreenCanvas: an off-screen canvas for background image processing and rendering. -1. xxxx -2. xxxx -3. xxxx +### Features -#### Contribution +- **Basic Editing**: Crop, rotate, mirror +- **Image Adjustment**: Brightness, transparency, saturation adjustment +- **Image Transformation**: Translation, scaling +- **Filter Effects**: Grayscale, blur, sharpen, and various other filters +- **Watermark**: Support for adding text and image watermarks +- **Real-time Preview**: All editing operations support real-time preview +- **Save Function**: Support for saving edited images -1. Fork the repository -2. Create Feat_xxx branch -3. Commit your code -4. Create Pull Request +### 工程目录 +``` +├──ets +│ ├──common +│ │ └──constant +│ │ └──constant +│ │ └──CommonConstants.ts // Constants +│ ├──entryability +│ │ └──EntryAbility.ets +│ ├──pages +│ │ └──PictureEdit.ets // Main edit page +│ ├──utils +│ │ ├──AdjustUtil.ets // Adjust utilities +│ │ ├──CropUtil.ets // Crop utilities +│ │ ├──DecodeUtil.ets // Decode utilities +│ │ ├──EncodeUtil.ets // Encode utilities +│ │ ├──LoggerUtil.ets // Logger utilities +│ │ └──OpacityUtil.ets // Opacity utilities +│ ├──view +│ │ ├──AdjustContentView.ets // Adjust component +│ │ ├──ApplyFilterView.ets // Filter component +│ │ ├──PhotoPicker.ets // Photo picker component +│ │ └──WatermarkView.ets // Watermark component +│ ├──viewModel +│ │ ├──IconListViewModel.ets // Icon list model +│ │ ├──MessageItem.ets // Message item model +│ │ ├──OptionViewModel.ets // Option model +│ │ └──RegionItem.ets // Region item model +│ └──workers +│ ├──AdjustBrightnessWork.ets // Brightness adjustment worker +│ └──AdjustSaturationWork.ets // Saturation adjustment worker +└──resources +``` +### Permissions -#### Gitee Feature +- ohos.permission.MEDIA_LOCATION +- ohos.permission.READ_MEDIA +- ohos.permission.WRITE_MEDIA -1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md -2. Gitee blog [blog.gitee.com](https://blog.gitee.com) -3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore) -4. The most valuable open source project [GVP](https://gitee.com/gvp) -5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help) -6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) +### How to Use + +1. Launch the app and select an image to edit. +2. Use the bottom tab bar to switch between different editing functions: + - **Crop**: Support for various aspect ratio cropping and free cropping + - **Adjust**: Adjust image brightness, transparency, and saturation + - **Filter**: Apply various filter effects + - **Translate**: Move the image horizontally and vertically + - **Scale**: Zoom in or out of the image + - **Watermark**: Add text or image watermarks +3. Preview editing effects in real-time. +4. Tap the save button to save the edited image. + +### Constraints + +1. The sample is only supported on Huawei phones with standard systems. +2. HarmonyOS: HarmonyOS 5.0.5 Release or later. +3. DevEco Studio: DevEco Studio 5.0.5 Release or later. +4. HarmonyOS SDK: HarmonyOS 5.0.5 Release SDK or later. +5. Supported image formats: JPEG, PNG, WebP. +6. Recommended image size should not exceed 10MB for optimal performance. \ No newline at end of file diff --git a/README.md b/README.md index 759781263216184471e034203841fe9cce606bdb..faaee2793de071ab418e7a948135c826d1f35e8a 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,89 @@ -# ImageCanvasEdit +# 基于图片编解码实现图片编辑功能 -#### 介绍 -构建Surface输入输出的图片编辑 +基于图片编解码,实现图片编辑,包含裁剪、旋转、色域调节(本章介绍亮度、镜像、透明度、饱和度)等功能。 -#### 软件架构 -软件架构说明 +|![dome1.png](screenshots%2Fdevice%2Fdome1.png)|![dome2.png](screenshots%2Fdevice%2Fdome2.png)|![dome3.png](screenshots%2Fdevice%2Fdome3.png)| +|---|---|---| +| ![dome4.png](screenshots%2Fdevice%2Fdome4.png) | ![dome5.png](screenshots%2Fdevice%2Fdome5.png) | ![dome6.png](screenshots%2Fdevice%2Fdome6.png) | +| ---------------------------------------------- | ---------------------------------------------- | ---------------------------------------------- | -#### 安装教程 +### 相关概念 -1. xxxx -2. xxxx -3. xxxx +- 图片解码:图片解码指将所支持格式的存档图片解码成统一的PixelMap,以便在应用或系统中进行图片显示或图片处理。 +- PixelMap:图像像素类,用于读取或写入图像数据以及获取图像信息。 +- 图片编码:图片编码指将PixelMap编码成不同格式的存档图片(当前仅支持打包为JPEG、WebP 和 png 格式),用于后续处理,如保存、传输等。 +- Canvas:画布组件,用于自定义绘制图形、图片等内容。 +- OffscreenCanvas:离屏画布,用于在后台进行图像处理和渲染。 -#### 使用说明 +### 功能特性 -1. xxxx -2. xxxx -3. xxxx +- **基础编辑**:裁剪、旋转、镜像 +- **图像调节**:亮度、透明度、饱和度调节 +- **图像变换**:平移、缩放 +- **滤镜效果**:灰度、模糊、锐化等多种滤镜 +- **水印功能**:支持添加文字和图片水印 +- **实时预览**:所有编辑操作支持实时预览 +- **保存功能**:支持保存编辑后的图片 -#### 参与贡献 +### 工程目录 +``` +├──ets +│ ├──common +│ │ └──constant +│ │ └──constant +│ │ └──CommonConstants.ts // 常量 +│ ├──entryability +│ │ └──EntryAbility.ets +│ ├──pages +│ │ └──PictureEdit.ets // 主编辑页面 +│ ├──utils +│ │ ├──AdjustUtil.ets // 调节工具类 +│ │ ├──CropUtil.ets // 裁剪工具类 +│ │ ├──DecodeUtil.ets // 解码工具类 +│ │ ├──EncodeUtil.ets // 编码工具类 +│ │ ├──LoggerUtil.ets // 日志工具类 +│ │ └──OpacityUtil.ets // 透明度工具类 +│ ├──view +│ │ ├──AdjustContentView.ets // 调节功能组件 +│ │ ├──ApplyFilterView.ets // 滤镜功能组件 +│ │ ├──PhotoPicker.ets // 选图组件 +│ │ └──WatermarkView.ets // 水印功能组件 +│ ├──viewModel +│ │ ├──IconListViewModel.ets // 图标列表模型 +│ │ ├──MessageItem.ets // 消息项模型 +│ │ ├──OptionViewModel.ets // 操作选项枚举 +│ │ └──RegionItem.ets // 区域项模型 +│ └──workers +│ ├──AdjustBrightnessWork.ets // 亮度调节工作线程 +│ └──AdjustSaturationWork.ets // 饱和度调节工作线程 +└──resources +``` -1. Fork 本仓库 -2. 新建 Feat_xxx 分支 -3. 提交代码 -4. 新建 Pull Request +### 相关权限 +- ohos.permission.MEDIA_LOCATION +- ohos.permission.READ_MEDIA +- ohos.permission.WRITE_MEDIA -#### 特技 +### 使用说明 -1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md -2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com) -3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目 -4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目 -5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help) -6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) +1. 启动应用后选择要编辑的图片。 +2. 使用底部标签页切换不同的编辑功能: + - **裁剪**:支持多种比例裁剪和自由裁剪 + - **调节**:调整图片亮度、透明度、饱和度 + - **滤镜**:应用各种滤镜效果 + - **平移**:水平和垂直移动图片 + - **缩放**:放大或缩小图片 + - **水印**:添加文字或图片水印 +3. 实时预览编辑效果。 +4. 点击保存按钮保存编辑后的图片。 + +### 约束与限制 + +1. 本示例仅支持标准系统上运行,支持设备:华为手机。 +2. HarmonyOS系统:HarmonyOS 5.0.5 Release及以上。 +3. DevEco Studio版本:DevEco Studio 5.0.5 Release及以上。 +4. HarmonyOS SDK版本:HarmonyOS 5.0.5 Release SDK及以上。 +5. 支持的图片格式:JPEG、PNG、WebP。 +6. 建议图片大小不超过10MB以确保最佳性能。 diff --git a/build-profile.json5 b/build-profile.json5 new file mode 100644 index 0000000000000000000000000000000000000000..1bd73409985f5de44c0bd0676de6d46fd5e14cbb --- /dev/null +++ b/build-profile.json5 @@ -0,0 +1,43 @@ +{ + "app": { + "signingConfigs": [ + ], + "products": [ + { + "name": "default", + "signingConfig": "default", + "targetSdkVersion": "5.1.0(18)", + "compatibleSdkVersion": "5.0.5(17)", + "runtimeOS": "HarmonyOS", + "buildOption": { + "strictMode": { + "caseSensitiveCheck": true, + "useNormalizedOHMUrl": true + } + } + } + ], + "buildModeSet": [ + { + "name": "debug", + }, + { + "name": "release" + } + ] + }, + "modules": [ + { + "name": "entry", + "srcPath": "./entry", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/code-linter.json5 b/code-linter.json5 new file mode 100644 index 0000000000000000000000000000000000000000..073990fa45394e1f8e85d85418ee60a8953f9b99 --- /dev/null +++ b/code-linter.json5 @@ -0,0 +1,32 @@ +{ + "files": [ + "**/*.ets" + ], + "ignore": [ + "**/src/ohosTest/**/*", + "**/src/test/**/*", + "**/src/mock/**/*", + "**/node_modules/**/*", + "**/oh_modules/**/*", + "**/build/**/*", + "**/.preview/**/*" + ], + "ruleSet": [ + "plugin:@performance/recommended", + "plugin:@typescript-eslint/recommended" + ], + "rules": { + "@security/no-unsafe-aes": "error", + "@security/no-unsafe-hash": "error", + "@security/no-unsafe-mac": "warn", + "@security/no-unsafe-dh": "error", + "@security/no-unsafe-dsa": "error", + "@security/no-unsafe-ecdsa": "error", + "@security/no-unsafe-rsa-encrypt": "error", + "@security/no-unsafe-rsa-sign": "error", + "@security/no-unsafe-rsa-key": "error", + "@security/no-unsafe-dsa-key": "error", + "@security/no-unsafe-dh-key": "error", + "@security/no-unsafe-3des": "error" + } +} \ No newline at end of file diff --git a/entry/.gitignore b/entry/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..e2713a2779c5a3e0eb879efe6115455592caeea5 --- /dev/null +++ b/entry/.gitignore @@ -0,0 +1,6 @@ +/node_modules +/oh_modules +/.preview +/build +/.cxx +/.test \ No newline at end of file diff --git a/entry/build-profile.json5 b/entry/build-profile.json5 new file mode 100644 index 0000000000000000000000000000000000000000..4d611879c7913fb0610c686e2399258ab3a6dad1 --- /dev/null +++ b/entry/build-profile.json5 @@ -0,0 +1,28 @@ +{ + "apiType": "stageMode", + "buildOption": { + }, + "buildOptionSet": [ + { + "name": "release", + "arkOptions": { + "obfuscation": { + "ruleOptions": { + "enable": false, + "files": [ + "./obfuscation-rules.txt" + ] + } + } + } + }, + ], + "targets": [ + { + "name": "default" + }, + { + "name": "ohosTest", + } + ] +} \ No newline at end of file diff --git a/entry/hvigorfile.ts b/entry/hvigorfile.ts new file mode 100644 index 0000000000000000000000000000000000000000..c6edcd90486dd5a853cf7d34c8647f08414ca7a3 --- /dev/null +++ b/entry/hvigorfile.ts @@ -0,0 +1,6 @@ +import { hapTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins:[] /* Custom plugin to extend the functionality of Hvigor. */ +} diff --git a/entry/obfuscation-rules.txt b/entry/obfuscation-rules.txt new file mode 100644 index 0000000000000000000000000000000000000000..272efb6ca3f240859091bbbfc7c5802d52793b0b --- /dev/null +++ b/entry/obfuscation-rules.txt @@ -0,0 +1,23 @@ +# Define project specific obfuscation rules here. +# You can include the obfuscation configuration files in the current module's build-profile.json5. +# +# For more details, see +# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5 + +# Obfuscation options: +# -disable-obfuscation: disable all obfuscations +# -enable-property-obfuscation: obfuscate the property names +# -enable-toplevel-obfuscation: obfuscate the names in the global scope +# -compact: remove unnecessary blank spaces and all line feeds +# -remove-log: remove all console.* statements +# -print-namecache: print the name cache that contains the mapping from the old names to new names +# -apply-namecache: reuse the given cache file + +# Keep options: +# -keep-property-name: specifies property names that you want to keep +# -keep-global-name: specifies names that you want to keep in the global scope + +-enable-property-obfuscation +-enable-toplevel-obfuscation +-enable-filename-obfuscation +-enable-export-obfuscation \ No newline at end of file diff --git a/entry/oh-package.json5 b/entry/oh-package.json5 new file mode 100644 index 0000000000000000000000000000000000000000..248c3b7541a589682a250f86a6d3ecf7414d2d6a --- /dev/null +++ b/entry/oh-package.json5 @@ -0,0 +1,10 @@ +{ + "name": "entry", + "version": "1.0.0", + "description": "Please describe the basic information.", + "main": "", + "author": "", + "license": "", + "dependencies": {} +} + diff --git a/entry/src/main/ets/common/constant/CommonConstants.ts b/entry/src/main/ets/common/constant/CommonConstants.ts new file mode 100644 index 0000000000000000000000000000000000000000..29344cdf0ea45b17091b0d6233aca1a64e88adbf --- /dev/null +++ b/entry/src/main/ets/common/constant/CommonConstants.ts @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License,Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { image } from "@kit.ImageKit"; + +export class CommonConstants { + /** + * Title row width. + */ + static readonly TITLE_ROW_WEIGHT: string = '50%'; + + /** + * Layout full screen. + */ + static readonly LAYOUT_FULL_SCREEN: string = '100%'; + + /** + * Edit page height. + */ + static readonly EDIT_PAGE_HEIGHT: string = '30%'; + + /** + * Image show height. + */ + static readonly IMAGE_SHOW_HEIGHT: string = '70%'; + + /** + * Slider width. + */ + static readonly SLIDER_WIDTH: string = '70%'; + + /** + * Loading width and height. + */ + static readonly LOADING_WH: string = '30%'; + + /** + * Clock wise. + */ + static readonly CLOCK_WISE: number = 90; + + /** + * Anti clock. + */ + static readonly ANTI_CLOCK: number = -90; + + /** + * Tab menu width. + */ + static readonly TAB_MENU_WIDTH: number = 24; + + /** + * Navigation height. + */ + static readonly NAVIGATION_HEIGHT: number = 56; + + /** + * Adjust slider value. + */ + static readonly ADJUST_SLIDER_VALUE: number[] = [100, 100, 100]; + + /** + * Slider min. + */ + static readonly SLIDER_MIN: number = 1; + + /** + * Slider step. + */ + static readonly SLIDER_MAX: number = 100; + + /** + * Slider step. + */ + static readonly SLIDER_STEP: number = 10; + + /** + * Pixel step. + */ + static readonly PIXEL_STEP: number = 4; + + /** + * Decimal two. + */ + static readonly DECIMAL_TWO: number = 2; + + /** + * Color level max. + */ + static readonly COLOR_LEVEL_MAX: number = 255; + + /** + * Convert int. + */ + static readonly CONVERT_INT: number = 100; + + /** + * Angle 60. + */ + static readonly ANGLE_60: number = 60; + + /** + * Angle 120. + */ + static readonly ANGLE_120: number = 120; + + /** + * Angle 240. + */ + static readonly ANGLE_240: number = 240; + + /** + * Angle 300. + */ + static readonly ANGLE_360: number = 360; + + /** + * Angle 360. + */ + static readonly MOD_2: number = 2; + + /** + * Average height and width. + */ + static readonly AVERAGE_WEIGHT_WIDTH: number = 2; + + /** + * Crop rate 4:3. + */ + static readonly CROP_RATE_4_3: number = 0.75; + + /** + * Crop rate 16:9. + */ + static readonly CROP_RATE_9_16: number = 9 / 16; + + /** + * Encode quality. + */ + static readonly ENCODE_QUALITY: number = 100; + + /** + * Title space. + */ + static readonly TITLE_SPACE: number = 0; + + /** + * Slider mode click. + */ + static readonly SLIDER_CLICK_MODE: number = 3; + + /** + * Encode format. + */ + static readonly ENCODE_FORMAT: string = 'image/jpeg'; + + /** + * Encode file permission. + */ + static readonly ENCODE_FILE_PERMISSION: string = 'rw'; + + /** + * Brightness worker file. + */ + static readonly BRIGHTNESS_WORKER_FILE = 'entry/ets/workers/AdjustBrightnessWork.ts'; + + /** + * Brightness worker file. + */ + static readonly SATURATION_WORKER_FILE = 'entry/ets/workers/AdjustSaturationWork.ts'; + + /** + * Image name. + */ + static readonly IMAGE_PREFIX = 'image'; + + /** + * Image format. + */ + static readonly IMAGE_FORMAT = '.jpg'; + + /** + * Rawfile name. + */ + static readonly RAW_FILE_NAME = 'low.jpg'; + + static readonly IMG_PROPERTIES_KEYS = [ + image.PropertyKey.ORIENTATION, + image.PropertyKey.BITS_PER_SAMPLE, + image.PropertyKey.IMAGE_LENGTH, + image.PropertyKey.IMAGE_WIDTH, + image.PropertyKey.SCENE_TYPE, + image.PropertyKey.ISO_SPEED_RATINGS, + image.PropertyKey.F_NUMBER, + image.PropertyKey.DATE_TIME, + image.PropertyKey.IMAGE_DESCRIPTION, + image.PropertyKey.MAKE, + image.PropertyKey.MODEL, + image.PropertyKey.PHOTO_MODE, + image.PropertyKey.SENSITIVITY_TYPE, + image.PropertyKey.ISO_SPEED, + image.PropertyKey.APERTURE_VALUE, + image.PropertyKey.METERING_MODE, + image.PropertyKey.LIGHT_SOURCE, + image.PropertyKey.FLASH, + image.PropertyKey.FOCAL_LENGTH, + image.PropertyKey.PIXEL_X_DIMENSION, + image.PropertyKey.EXPOSURE_TIME + ]; +} \ No newline at end of file diff --git a/entry/src/main/ets/entryability/EntryAbility.ets b/entry/src/main/ets/entryability/EntryAbility.ets new file mode 100644 index 0000000000000000000000000000000000000000..62e215f56df498956904b086cbdc222cb69b0d4c --- /dev/null +++ b/entry/src/main/ets/entryability/EntryAbility.ets @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License,Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Permissions, + abilityAccessCtrl, + AbilityConstant, + ConfigurationConstant, + UIAbility, + Want +} from '@kit.AbilityKit'; +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { window } from '@kit.ArkUI'; +import { ContextStorageUtil } from '../utils/ContextStorageUtil' + +const DOMAIN = 0x0000; + +export default class EntryAbility extends UIAbility { + onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { + this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET); + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate'); + } + + onDestroy(): void { + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy'); + } + + onWindowStageCreate(windowStage: window.WindowStage): void { + // Main window is created, set main page for this ability + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate'); + const permissions: Array = [ + 'ohos.permission.WRITE_IMAGEVIDEO', + 'ohos.permission.WRITE_MEDIA', + 'ohos.permission.READ_MEDIA', + 'ohos.permission.MEDIA_LOCATION' + ]; + const atManager = abilityAccessCtrl.createAtManager(); + atManager.requestPermissionsFromUser(this.context, permissions, (err, data) => { + if (err) { + hilog.error(0x0000, 'testTag', 'Failed to requestPermission. Cause: %{public}s', + JSON.stringify(err) ?? ''); + } else { + hilog.info(0x0000, 'testTag', 'Succeeded in requestPermission. Data: %{public}s', + JSON.stringify(data) ?? ''); + } + }); + + windowStage.getMainWindow((_err, windowClass) => { + windowClass.setWindowLayoutFullScreen(true); + }); + + windowStage.loadContent('pages/PictureEdit', (err) => { + if (err.code) { + hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err)); + return; + } + let uiContext = windowStage.getMainWindowSync().getUIContext() + ContextStorageUtil.getInstance().init(uiContext); + + hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.'); + }); + } + + onWindowStageDestroy(): void { + // Main window is destroyed, release UI related resources + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy'); + } + + onForeground(): void { + // Ability has brought to foreground + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground'); + } + + onBackground(): void { + // Ability has back to background + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground'); + } +} \ No newline at end of file diff --git a/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets b/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets new file mode 100644 index 0000000000000000000000000000000000000000..634207b7b9e5681548295482bc4fbde36c643a72 --- /dev/null +++ b/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License,Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { BackupExtensionAbility, BundleVersion } from '@kit.CoreFileKit'; + +const DOMAIN = 0x0000; + +export default class EntryBackupAbility extends BackupExtensionAbility { + async onBackup() { + hilog.info(DOMAIN, 'testTag', 'onBackup ok'); + await Promise.resolve(); + } + + async onRestore(bundleVersion: BundleVersion) { + hilog.info(DOMAIN, 'testTag', 'onRestore ok %{public}s', JSON.stringify(bundleVersion)); + await Promise.resolve(); + } +} \ No newline at end of file diff --git a/entry/src/main/ets/pages/PictureEdit.ets b/entry/src/main/ets/pages/PictureEdit.ets new file mode 100644 index 0000000000000000000000000000000000000000..a1d15792ea32dc4cd2d6b08afe7846ea7de10ae8 --- /dev/null +++ b/entry/src/main/ets/pages/PictureEdit.ets @@ -0,0 +1,1058 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License,Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { image } from '@kit.ImageKit'; +import { effectKit } from "@kit.ArkGraphics2D"; +import { IconStatus } from '../viewModel/IconListViewModel'; +import AdjustContentView from '../view/AdjustContentView'; +import ApplyFilterView from '../view/ApplyFilterView'; +import { cropIconChangeList } from '../viewModel/IconListViewModel'; +import Logger from '../utils/LoggerUtil'; +import { MirrorType, CropType, MainTabId } from '../viewModel/OptionViewModel'; +import { square, banner, rectangle } from '../utils/CropUtil'; +import { encode } from '../utils/EncodeUtil'; +import { CommonConstants } from '../common/constant/CommonConstants'; +import { getPixelMap, getResourceFd } from '../utils/DecodeUtil'; +import { ContextStorageUtil } from '../utils/ContextStorageUtil'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { WatermarkView } from '../view/WatermarkView'; + +const TAG: string = 'imageEdit'; + +interface translateListType { + icon: string, + title: Resource | string, + color: string, + selectColor: string +} + +interface initDataArrType { + label: '', + value: '' +} + +interface paramsType { + selectUris: string +} + +interface ImageDisplaySize { + x: number; + y: number; + width: number; + height: number; +} + +@CustomDialog +@Component +struct MyCustomDialog { + controller?: CustomDialogController; + cancel: () => void = () => { + }; + confirm: () => void = () => { + }; + + build() { + Column() { + Text($r('app.string.save_crop')) + .textAlign(TextAlign.Center) + .fontSize('16vp') + .fontColor('rgba(0, 0, 0, 0.9)') + .lineHeight('21vp') + + Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center, justifyContent: FlexAlign.SpaceBetween }) { + Button($r('app.string.cancel'), { buttonStyle: ButtonStyleMode.EMPHASIZED, role: ButtonRole.NORMAL }) + .flexGrow(1) + .backgroundColor(Color.Transparent) + .fontColor('#0A59F7') + .fontSize('16vp') + .fontWeight(500) + .onClick(() => { + this.cancel(); + }) + Divider() + .vertical(true) + .strokeWidth('0.5vp') + .height(24) + .color('rgba(0, 0, 0, 0.05)') + .margin({ left: 4, right: 4 }) + Button($r('app.string.confirm'), { buttonStyle: ButtonStyleMode.EMPHASIZED, role: ButtonRole.NORMAL }) + .flexGrow(1) + .backgroundColor(Color.Transparent) + .fontColor('#0A59F7') + .fontSize('16vp') + .fontWeight(500) + .onClick(() => { + this.confirm(); + }) + } + .height('40vp') + .margin({ top: '8vp' }) + } + .width('328vp') + .padding('24vp') + .borderRadius('32vp') + .backgroundColor(Color.White) + } +} + +@Entry +@Component +struct PictureEdit { + uiContext: UIContext = ContextStorageUtil.getInstance().getUtil(); + uri: string = ''; + imageSource?: image.ImageSource; + @StorageProp('imageInfoArr') imageInfoArr: initDataArrType[] = []; + @State currentIndex: number = 0; + @State currentCropIndex: number = 0; + @State currentTranslateData: number[] = [0, 0]; + @State currentTranslateIndex: number = 0; + @State currentZoom: number = 100; + @State cropList: translateListType[] = [ + { + icon: 'aspect_ratio', + title: $r('app.string.none'), + color: '#fff', + selectColor: '#ff000000' + }, + { + icon: 'rectangle_ratio_1_to_1', + title: '1:1', + color: '#fff', + selectColor: '#ff000000' + }, + { + icon: 'rectangle_ratio_4_to_3', + title: '4:3', + color: '#fff', + selectColor: '#ff000000' + }, + { + icon: 'rectangle_ratio_16_to_9', + title: '16:9', + color: '#fff', + selectColor: '#ff000000' + } + ]; + @State translateList: translateListType[] = [ + { + icon: 'camera_panopor_horizontal', + title: $r('app.string.horizontal'), + color: '#fff', + selectColor: '#ff000000' + }, + { + icon: 'camera_panover_vertical', + title: $r('app.string.vertical'), + color: '#fff', + selectColor: '#ff000000' + }, + ]; + @State isShow: boolean = false; + @State waterShow: boolean = false; + @State dividerList: number[] = [1, 3]; + @Provide('pixelMap') pixelMap?: image.PixelMap = undefined; + @Provide filterPixelMap?: image.PixelMap = undefined; + @Provide cropPixelMap?: image.PixelMap = undefined; + @Provide filterCurrIndex: number = 0; + @Provide('imageInfo') imageInfo: image.ImageInfo = { + size: { height: 0, width: 0 }, + density: 0, + stride: 0, + alphaType: 0, + pixelFormat: 0, + mimeType: '', + isHdr: false + }; + @Provide('currentAdjustData') currentAdjustData: Array = [100, 100, 100]; + @Provide('isPixelMapChange') @Watch('flushPixelMap') isPixelMapChange: boolean = false; + menuIconChangeList = [ + 'crop_rotate', + 'slider_horizontal_2', + 'camera_filters', + 'square_portrait_svg', + 'checkered_magnifyingglass', + 'ic_shutter_photo_svg' + ]; + cropIconChange: Array = cropIconChangeList; + @State saveButtonOptions: SaveButtonOptions = { + icon: SaveIconStyle.FULL_FILLED + }; + + private canvasRenderingContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(); + @State containerWidth: number = 0; + @State containerHeight: number = 0; + @State imageWidth: number = 0; + @State imageHeight: number = 0; + @State canvasReady: boolean = false; + @State imageLoaded: boolean = false; + @State canvasOffsetX: number = 0; + @State canvasOffsetY: number = 0; + dialogController: CustomDialogController = new CustomDialogController({ + builder: MyCustomDialog({ + cancel: () => { + this.onCancel() + }, + confirm: () => { + this.onConfirm() + } + }), + autoCancel: false, + alignment: DialogAlignment.Center, + customStyle: true + }); + + aboutToAppear() { + try { + const params = this.uiContext.getRouter().getParams() as paramsType; + if (params && params.selectUris) { + this.uri = params.selectUris; + console.log('Got URI from router params:', this.uri); + ContextStorageUtil.getInstance().initUri(this.uri); + } + } catch (error) { + console.error('Failed to get router params:', error); + } + + this.pixelInit(); + this.initData(); + } + + async initData() { + try { + this.imageSource = image.createImageSource(await getResourceFd(this.uri)); + + this.pixelMap = await getPixelMap(this.imageSource); + + this.imageSource.getImageInfo((err, imageInfo) => { + if (err) { + console.error("Failed to get image info:", err); + return; + } + this.imageInfo = imageInfo; + this.imageWidth = imageInfo.size.width; + this.imageHeight = imageInfo.size.height; + this.imageLoaded = true; + console.log(`Image loaded successfully: ${this.imageWidth} x ${this.imageHeight}`); + + this.triggerCanvasRedraw(); + + Object.keys(imageInfo).forEach((key) => { + if (key === 'size') { + Object.keys(imageInfo[key]).forEach((chlKey) => { + this.imageInfoArr.push({ + label: chlKey as '', + value: imageInfo[key][chlKey] + }) + }) + } else { + this.imageInfoArr.push({ + label: key as '', + value: imageInfo[key] + }) + } + }); + }) + + // Read EXIF information + await this.imageSource.getImageProperties(CommonConstants.IMG_PROPERTIES_KEYS).then((result) => { + console.info('eventLog uiContext EXIF data:', JSON.stringify(result)); + Object.keys(result).forEach((key) => { + this.imageInfoArr.push({ + label: key as '', + value: result[key] + }) + }); + }).catch((error: BusinessError) => { + console.error('Failed to get the value of the specified attribute key of the image.', error); + }); + AppStorage.setOrCreate('imageInfoArr', this.imageInfoArr) + } catch (error) { + console.error('Error in initData:', error); + } + } + + calculateImageDisplaySize(): ImageDisplaySize { + if (this.containerWidth === 0 || this.containerHeight === 0 || this.imageWidth === 0 || this.imageHeight === 0) { + return { x: 0, y: 0, width: 0, height: 0 }; + } + + const containerRatio = this.containerWidth / this.containerHeight; + const imageRatio = this.imageWidth / this.imageHeight; + + const zoomFactor = this.currentZoom / 100; + + let baseDisplayWidth: number; + let baseDisplayHeight: number; + + if (imageRatio > containerRatio) { + baseDisplayWidth = this.containerWidth; + baseDisplayHeight = this.containerWidth / imageRatio; + } else { + baseDisplayHeight = this.containerHeight; + baseDisplayWidth = this.containerHeight * imageRatio; + } + + const displayWidth = baseDisplayWidth * zoomFactor; + const displayHeight = baseDisplayHeight * zoomFactor; + + const x = (this.containerWidth - displayWidth) / 2; + const y = (this.containerHeight - displayHeight) / 2; + + return { x, y, width: displayWidth, height: displayHeight }; + } + + onCancel() { + this.pixelMap = this.cropPixelMap; + this.dialogController.close(); + this.currentCropIndex = 0; + } + + onConfirm() { + this.dialogController.close(); + this.currentCropIndex = 0; + } + + async pixelInit() { + try { + let imageSource = image.createImageSource(await getResourceFd(this.uri)); + this.pixelMap = await getPixelMap(imageSource); + + this.isPixelMapChange = !this.isPixelMapChange; + + this.filterCurrIndex = 0; + this.currentCropIndex = 0; + this.currentAdjustData = CommonConstants.ADJUST_SLIDER_VALUE.map((item: number) => item); + + this.currentTranslateData = [0, 0]; + this.currentZoom = 100; + await this.initEffectKit(); + + this.triggerCanvasRedraw(); + } catch (error) { + console.error('Error in pixelInit:', error); + } + } + + async initEffectKit() { + try { + if (this.pixelMap) { + this.cropPixelMap = await effectKit.createEffect(this.pixelMap).getEffectPixelMap(); + this.currentTranslateData = [0, 0]; + this.currentZoom = 100; + this.canvasOffsetX = 0; + this.canvasOffsetY = 0; + console.log('Effect kit initialized successfully'); + } + } catch (error) { + console.error('Error initializing effect kit:', error); + } + } + + async cropImage(proportion: CropType) { + if (!this.pixelMap) { + return; + } + + try { + const imageInfo = await this.pixelMap.getImageInfo(); + const size = imageInfo.size; + const imageWidth = size?.width; + const imageHeight = size?.height; + + if (!imageWidth || !imageHeight) { + console.error('Invalid image dimensions'); + return; + } + + switch (proportion) { + case CropType.ORIGINAL_IMAGE: + if (this.cropPixelMap) { + this.pixelMap = await effectKit.createEffect(this.cropPixelMap).getEffectPixelMap(); + this.flushPixelMapChange(); + } + break; + case CropType.SQUARE: + await square(this.pixelMap, imageWidth, imageHeight); + this.flushPixelMapChange(); + break; + case CropType.BANNER: + await banner(this.pixelMap, imageWidth, imageHeight); + this.flushPixelMapChange(); + break; + case CropType.RECTANGLE: + await rectangle(this.pixelMap, imageWidth, imageHeight); + this.flushPixelMapChange(); + break; + default: + break; + } + } catch (error) { + console.error('Error in crop operation:', error); + } + } + + rotateImage() { + if (!this.pixelMap) { + return; + } + try { + this.pixelMap.rotate(CommonConstants.ANTI_CLOCK) + .then(() => { + this.flushPixelMapChange(); + }) + } catch (error) { + Logger.error(TAG, `there is a error in rotate process with ${error?.code}`); + } + } + + mirrorImage(mirrorType: MirrorType) { + if (mirrorType === MirrorType.CLOCKWISE) { + if (!this.pixelMap) { + return; + } + try { + this.pixelMap.flipSync(true, false); + this.flushPixelMapChange(); + } catch (error) { + Logger.error(TAG, `there is a error in rotate process with ${error?.code}`); + } + } + } + + translateImage(index: number, moveValue: number) { + if (index === 0) { + this.canvasOffsetX += moveValue; + } else { + this.canvasOffsetY += moveValue; + } + this.triggerCanvasRedraw(); + console.log(`Canvas offset updated: X=${this.canvasOffsetX}, Y=${this.canvasOffsetY}`); + } + + flushPixelMapChange() { + this.isPixelMapChange = !this.isPixelMapChange; + this.triggerCanvasRedraw(); + } + + flushPixelMap() { + const temp = this.pixelMap; + this.pixelMap = undefined; + this.pixelMap = temp; + this.triggerCanvasRedraw(); + } + + sliderChange(value: number, mode: SliderChangeMode) { + if ((mode === SliderChangeMode.End) && (value !== this.currentTranslateData[this.currentTranslateIndex])) { + let moveValue = Math.round(value) - this.currentTranslateData[this.currentTranslateIndex]; + this.currentTranslateData[this.currentTranslateIndex] = Math.round(value); + this.translateImage(this.currentTranslateIndex, moveValue) + } + } + + async sliderZoomChange(value: number, mode: SliderChangeMode) { + if (mode !== SliderChangeMode.End || value === this.currentZoom || !this.pixelMap || !this.cropPixelMap) { + return; + } + + try { + const scaleRatio = value / 100; + + this.pixelMap = await effectKit.createEffect(this.cropPixelMap).getEffectPixelMap(); + + if (scaleRatio !== 1.0) { + await this.pixelMap.scale(scaleRatio, scaleRatio); + } + + this.currentZoom = value; + this.flushPixelMapChange(); + + console.log(`Zoom applied: ${value}%`); + } catch (error) { + console.error('Zoom operation failed:', error); + } + } + + drawImageOnCanvas() { + if (!this.canvasReady || !this.imageLoaded || !this.pixelMap) { + return; + } + + try { + this.canvasRenderingContext.clearRect(0, 0, this.containerWidth, this.containerHeight); + + this.pixelMap.getImageInfo().then((info) => { + this.imageWidth = info.size.width; + this.imageHeight = info.size.height; + + const displaySize = this.calculateImageDisplaySize(); + + if (displaySize.width > 0 && displaySize.height > 0) { + const imageToRender = (this.filterCurrIndex > 0 && this.filterPixelMap) ? this.filterPixelMap : this.pixelMap; + + const finalX = displaySize.x + this.canvasOffsetX; + const finalY = displaySize.y + this.canvasOffsetY; + + this.canvasRenderingContext.drawImage( + imageToRender, + finalX, + finalY, + displaySize.width, + displaySize.height + ); + } + }); + } catch (error) { + console.error('Canvas drawing failed:', error); + } + } + + triggerCanvasRedraw() { + if (this.canvasReady && this.imageLoaded && this.pixelMap) { + this.drawImageOnCanvas(); + } + } + + async createTranslatedPixelMap(): Promise { + if (!this.pixelMap || (this.canvasOffsetX === 0 && this.canvasOffsetY === 0)) { + const imageToSave = (this.filterCurrIndex > 0 && this.filterPixelMap) ? this.filterPixelMap : this.pixelMap; + return imageToSave; + } + + try { + const imageToProcess = (this.filterCurrIndex > 0 && this.filterPixelMap) ? this.filterPixelMap : this.pixelMap; + const imageInfo = await imageToProcess.getImageInfo(); + const imageWidth = imageInfo.size.width; + const imageHeight = imageInfo.size.height; + + const srcBuffer = new ArrayBuffer(imageWidth * imageHeight * 4); + await imageToProcess.readPixelsToBuffer(srcBuffer); + const srcData = new Uint8Array(srcBuffer); + + const destBuffer = new ArrayBuffer(imageWidth * imageHeight * 4); + const destData = new Uint8Array(destBuffer); + + for (let y = 0; y < imageHeight; y++) { + for (let x = 0; x < imageWidth; x++) { + const srcX = x - this.canvasOffsetX; + const srcY = y - this.canvasOffsetY; + + const destIndex = (y * imageWidth + x) * 4; + + if (srcX >= 0 && srcX < imageWidth && srcY >= 0 && srcY < imageHeight) { + const srcIndex = (srcY * imageWidth + srcX) * 4; + destData[destIndex] = srcData[srcIndex]; + destData[destIndex + 1] = srcData[srcIndex + 1]; + destData[destIndex + 2] = srcData[srcIndex + 2]; + destData[destIndex + 3] = srcData[srcIndex + 3]; + } else { + destData[destIndex] = 0; + destData[destIndex + 1] = 0; + destData[destIndex + 2] = 0; + destData[destIndex + 3] = 0; + } + } + } + + const translatedPixelMap = await image.createPixelMap(destBuffer, { + size: { width: imageWidth, height: imageHeight }, + pixelFormat: image.PixelMapFormat.RGBA_8888 + }); + + return translatedPixelMap; + } catch (error) { + console.error('Failed to create translated pixelMap:', error); + const imageToSave = (this.filterCurrIndex > 0 && this.filterPixelMap) ? this.filterPixelMap : this.pixelMap; + return imageToSave; + } + } + + @Builder + waterMark() { + Column() { + Row() { + Text($r('app.string.watermark')) + .fontSize(20) + .fontWeight(700) + } + .margin({ bottom: 24 }) + .width('100%') + .justifyContent(FlexAlign.Start) + + Scroll() { + WatermarkView() + } + .padding({ + top: 4, + bottom: 4, + left: 12, + right: 12 + }) + .backgroundColor('#fff') + .borderRadius(16) + .scrollable(ScrollDirection.Vertical) + .scrollBar(BarState.Auto) + .scrollBarColor('#6dececececec') + .scrollBarWidth(5) + .friction(0.6) + .edgeEffect(EdgeEffect.None) + } + .padding({ + top: 23, + bottom: 40, + right: 16, + left: 16 + }) + .width('100%') + } + + @Builder + infoBuilder() { + Column() { + Row() { + Text($r('app.string.picture_information')) + .fontSize(20) + .fontWeight(700) + } + .margin({ bottom: 24 }) + .width('100%') + .justifyContent(FlexAlign.Start) + + Scroll() { + Column() { + ForEach(this.imageInfoArr, (item: initDataArrType, index) => { + Row() { + Text(item.label) + .fontSize(16) + .fontWeight(500) + Text(item.value + '') + .fontSize(14) + .fontColor('#99000000') + } + .padding({ top: 13, bottom: 13 }) + .width('100%') + .justifyContent(FlexAlign.SpaceBetween) + .alignItems(VerticalAlign.Center) + + if (this.dividerList.includes(index)) { + Divider().strokeWidth(0.5).color('#33000000') + } + }) + } + .width('100%') + .justifyContent(FlexAlign.Start) + } + .padding({ + top: 4, + bottom: 4, + left: 12, + right: 12 + }) + .backgroundColor('#fff') + .borderRadius(16) + .scrollable(ScrollDirection.Vertical) + .scrollBar(BarState.Auto) + .scrollBarColor('#6dececececec') + .scrollBarWidth(5) + .friction(0.6) + .edgeEffect(EdgeEffect.None) + } + .padding({ + top: 23, + bottom: 40, + right: 16, + left: 16 + }) + .width('100%') + } + + @Builder + TabBuilderMenu(index: number, name: string | Resource) { + Column() { + if (this.menuIconChangeList[index].includes('svg')) { + Image($r(`app.media.${this.menuIconChangeList[index]}`)) + .fillColor(this.currentIndex === index ? '#5291FF' : '#fff') + .width(CommonConstants.TAB_MENU_WIDTH) + .height(CommonConstants.TAB_MENU_WIDTH) + } else { + Text() { + SymbolSpan($r(`sys.symbol.${this.menuIconChangeList[index]}`)) + .fontColor(this.currentIndex === index ? ['#5291FF', '#5291FF', '#5291FF'] : + [Color.White, Color.White, Color.White]) + .fontWeight(FontWeight.Normal) + .fontSize(20) + } + .width(CommonConstants.TAB_MENU_WIDTH) + .height(CommonConstants.TAB_MENU_WIDTH) + } + + Text(name) + .fontColor(this.currentIndex === index ? '#5291FF' : Color.White) + .fontSize(10) + .margin({ top: 2 }) + } + .width(60) + .padding({ left: 4, right: 4 }) + } + + build() { + RelativeContainer() { + Column() { + Flex({ justifyContent: FlexAlign.SpaceBetween, alignItems: ItemAlign.Center }) { + Row() { + Text($r('app.string.image_edit')) + .fontColor('#e6ffffff') + .fontSize(26) + .fontWeight(700) + } + .padding({ left: 16, right: 16 }) + .alignItems(VerticalAlign.Center) + .width(150) + + Row() { + Button() { + Image($r('app.media.ic_reset')) + .width(22) + .height(22) + } + .padding(9) + .borderRadius('50%') + .type(ButtonType.Normal) + .aspectRatio(1) + .backgroundColor('#26ffffff') + .onClick(() => { + this.pixelInit(); + }) + + Text() { + SymbolSpan($r('sys.symbol.info_circle')) + .fontColor([Color.White, Color.White, Color.White]) + .fontWeight(FontWeight.Normal) + .fontSize(22) + } + .margin({ left: 8 }) + .padding(9) + .borderRadius('50%') + .backgroundColor('#26ffffff') + .onClick(() => { + this.isShow = true; + }) + + Text() { + SymbolSpan($r('sys.symbol.save')) + .fontColor([Color.White, Color.White, Color.White]) + .fontWeight(FontWeight.Normal) + .fontSize(22) + } + .margin({ left: 8 }) + .padding(9) + .borderRadius('50%') + .backgroundColor('#26ffffff') + .onClick(async () => { + this.uiContext.showAlertDialog({ + title: $r('app.string.save_image'), + message: $r('app.string.confirm_save'), + alignment: DialogAlignment.Center, + primaryButton: { + value: $r('app.string.save'), + action: async () => { + const imageToSave = await this.createTranslatedPixelMap(); + if (imageToSave) { + encode(imageToSave); + } + } + }, + secondaryButton: { + value: $r('app.string.cancel'), + action: () => { + Logger.info(TAG, `cancel`); + } + } + }) + }) + } + .padding({ right: 16 }) + } + .padding({ top: 44 }) + .width('100%') + .height(92) + + Column() { + Column() { + Canvas(this.canvasRenderingContext) + .width('100%') + .height('100%') + .backgroundColor(Color.Transparent) + .onReady(() => { + console.log('Canvas ready'); + this.canvasReady = true; + this.triggerCanvasRedraw(); + }) + .onAreaChange((oldValue: Area, newValue: Area) => { + this.containerWidth = Number(newValue.width); + this.containerHeight = Number(newValue.height); + console.log(`Canvas size changed: ${this.containerWidth} x ${this.containerHeight}`); + this.triggerCanvasRedraw(); + }) + } + .width(CommonConstants.LAYOUT_FULL_SCREEN) + .height('66%') + + Column() { + Scroll() { + Tabs({ barPosition: BarPosition.End }) { + TabContent() { + Column() { + Flex({ justifyContent: FlexAlign.SpaceBetween }) { + Text() { + SymbolSpan($r(`sys.symbol.rotate_left`)) + .fontColor([Color.White, Color.White, Color.White]) + .fontWeight(FontWeight.Normal) + .fontSize(24) + } + .width(CommonConstants.TAB_MENU_WIDTH) + .height(CommonConstants.TAB_MENU_WIDTH) + .onClick(() => { + this.rotateImage(); + }) + + Text() { + SymbolSpan($r(`sys.symbol.line_arrowtriangle_2_inward`)) + .fontColor([Color.White, Color.White, Color.White]) + .fontWeight(FontWeight.Normal) + .fontSize(24) + } + .width(CommonConstants.TAB_MENU_WIDTH) + .height(CommonConstants.TAB_MENU_WIDTH) + .onClick(() => { + this.mirrorImage(MirrorType.CLOCKWISE); + }) + } + .margin({ top: 26, bottom: 33 }) + .padding({ left: 50, right: 50 }) + + Row() { + ForEach(this.cropList, (item: translateListType, index: number) => { + Column() { + Text() { + SymbolSpan($r(`sys.symbol.${item.icon}`)) + .fontColor(this.currentCropIndex === index ? + [item.selectColor, item.selectColor, item.selectColor] : + [item.color, item.color, item.color]) + .fontWeight(FontWeight.Normal) + .fontSize(24) + } + .padding(9) + .borderRadius('50%') + .backgroundColor(this.currentCropIndex === index ? '#e6ffffff' : '#26ffffff') + + Text(item.title) + .margin({ top: 4 }) + .fontColor('#fff') + .fontSize(10) + } + .onClick(() => { + this.currentCropIndex = index + this.cropImage(index) + }) + }, (item: IconStatus) => JSON.stringify(item)) + } + .padding({ left: 15, right: 15 }) + .justifyContent(FlexAlign.SpaceAround) + .width(CommonConstants.LAYOUT_FULL_SCREEN) + } + .width(CommonConstants.LAYOUT_FULL_SCREEN) + .height(CommonConstants.LAYOUT_FULL_SCREEN) + } + .padding({ bottom: 19 }) + .tabBar(this.TabBuilderMenu(MainTabId.CROP, $r('app.string.crop'))) + + TabContent() { + AdjustContentView() + } + .padding({ bottom: 19 }) + .tabBar(this.TabBuilderMenu(MainTabId.ADJUST, $r('app.string.adjust'))) + .clip(false) + + TabContent() { + ApplyFilterView() + .margin({ top: 60 }) + } + .padding({ bottom: 19 }) + .tabBar(this.TabBuilderMenu(MainTabId.FILTER, $r('app.string.filter'))) + + TabContent() { + Column() { + Row() { + Slider({ + value: this.currentTranslateData[this.currentTranslateIndex], + step: 10, + min: 0, + max: 1000 + }) + .trackColor('#1affffff') + .selectedColor(Color.White) + .width(CommonConstants.SLIDER_WIDTH) + .showSteps(true) + .showTips(true) + .clip(false) + .onChange((value: number, mode: SliderChangeMode) => { + this.sliderChange(value > 1000 ? 1000 : value, mode); + }) + } + .padding({ top: 50, bottom: 20 }) + .width('100%') + .justifyContent(FlexAlign.Center) + .clip(false) + + Row() { + ForEach(this.translateList, (item: translateListType, index) => { + Column() { + Text() { + SymbolSpan($r(`sys.symbol.${item.icon}`)) + .fontColor(this.currentTranslateIndex === index ? + [item.selectColor, item.selectColor, item.selectColor] : + [item.color, item.color, item.color]) + .fontWeight(FontWeight.Normal) + .fontSize(24) + } + .padding(9) + .borderRadius('50%') + .backgroundColor(this.currentTranslateIndex === index ? '#e6ffffff' : '#26ffffff') + + Text(item.title) + .margin({ top: 4 }) + .fontColor('#fff') + .fontSize(10) + } + .margin({ left: 37, right: 37 }) + .onClick(() => { + this.currentTranslateIndex = index + }) + }) + } + .width('100%') + .justifyContent(FlexAlign.Center) + } + .justifyContent(FlexAlign.Start) + .width(CommonConstants.LAYOUT_FULL_SCREEN) + .height('100%') + } + .padding({ bottom: 19 }) + .tabBar(this.TabBuilderMenu(MainTabId.TRANSLATE, $r('app.string.translate'))) + .clip(false) + + TabContent() { + Column() { + Row() { + Slider({ + value: this.currentZoom, + step: 5, + min: 10, + max: 200 + }) + .trackColor('#1affffff') + .selectedColor(Color.White) + .width(CommonConstants.SLIDER_WIDTH) + .showSteps(true) + .showTips(true, (this.currentZoom.toFixed() + '%')) + .clip(false) + .onChange((value: number, mode: SliderChangeMode) => { + const clampedValue = Math.max(10, Math.min(200, value)); + this.sliderZoomChange(clampedValue, mode); + }) + } + .padding({ top: 50, bottom: 20 }) + .width('100%') + .justifyContent(FlexAlign.Center) + + Text(`当前缩放: ${this.currentZoom}%`) + .fontColor('#fff') + .fontSize(14) + .margin({ top: 10 }) + } + .justifyContent(FlexAlign.Start) + .width(CommonConstants.LAYOUT_FULL_SCREEN) + .height('100%') + } + .padding({ bottom: 19 }) + .clip(false) + .tabBar(this.TabBuilderMenu(MainTabId.ZOOM, $r('app.string.zoom'))) + + TabContent() { + } + .padding({ bottom: 19 }) + .clip(false) + .tabBar(this.TabBuilderMenu(5, $r('app.string.watermark'))) + } + .scrollable(true) + .barMode(BarMode.Scrollable) + .clip(false) + .onTabBarClick((index: number) => { + if (this.waterShow === false && index === 5) { + this.waterShow = true; + } + }) + .onChange((index: number) => { + if (this.currentIndex === 0 && this.currentCropIndex !== 0) { + this.dialogController.open(); + } else if (index === 0) { + this.initEffectKit() + } + this.currentIndex = index; + }) + .bindSheet($$this.waterShow, this.waterMark(), { + height: SheetSize.FIT_CONTENT, + onDisappear: () => { + this.waterShow = false; + } + }) + } + .scrollable(ScrollDirection.Horizontal) + .scrollBar(BarState.Off) + .edgeEffect(EdgeEffect.Spring) + .friction(0.6) + } + .padding({ bottom: 30 }) + .width(CommonConstants.LAYOUT_FULL_SCREEN) + .height('34%') + .backgroundColor(Color.Black) + } + .width(CommonConstants.LAYOUT_FULL_SCREEN) + .height('calc(100% - 92vp)') + } + .width(CommonConstants.LAYOUT_FULL_SCREEN) + .height(CommonConstants.LAYOUT_FULL_SCREEN) + .bindSheet($$this.isShow, this.infoBuilder(), { + height: SheetSize.FIT_CONTENT, + onWillAppear: () => { + console.log("Info BindSheet onWillAppear."); + }, + onAppear: () => { + console.log("Info BindSheet onAppear."); + }, + onWillDisappear: () => { + console.log("Info BindSheet onWillDisappear."); + }, + onDisappear: () => { + console.log("Info BindSheet onDisappear."); + this.isShow = false; + } + }) + } + .backgroundColor(Color.Black) + .width(CommonConstants.LAYOUT_FULL_SCREEN) + .height(CommonConstants.LAYOUT_FULL_SCREEN) + } +} diff --git a/entry/src/main/ets/utils/AdjustUtil.ts b/entry/src/main/ets/utils/AdjustUtil.ts new file mode 100644 index 0000000000000000000000000000000000000000..ef4ead83461cc361edaae756fe01448bcc59db3a --- /dev/null +++ b/entry/src/main/ets/utils/AdjustUtil.ts @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License,Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CommonConstants } from '../common/constant/CommonConstants'; +import { RGBIndex, HSVIndex, AngelRange } from '../viewModel/OptionViewModel'; + +/** + * Saturation adjust. + * + * @param pixelMap. + * @param value saturation's value. + * @return arrayBuffer. + */ +export function adjustSaturation(bufferArray: ArrayBuffer, last: number, cur: number) { + return execColorInfo(bufferArray, last, cur, HSVIndex.SATURATION); +} + +/** + * Image brightness adjust. + * + * @param pixelMap. + * @param value image's brigtness. + * @return arrayBuffer. + */ +export function adjustImageValue(bufferArray: ArrayBuffer, last: number, cur: number) { + return execColorInfo(bufferArray, last, cur, HSVIndex.VALUE); +} + +/** + * Exec color transform. + * + * @param bufferArray. + * @param last. + * @param cur. + * @param hsvIndex. + * @return arrayBuffer. + */ +export function execColorInfo(bufferArray: ArrayBuffer, last: number, cur: number, hsvIndex: number) { + if (!bufferArray) { + return; + } + const newBufferArr = bufferArray; + let colorInfo = new Uint8Array(newBufferArr); + for (let i = 0; i < colorInfo?.length; i += CommonConstants.PIXEL_STEP) { + const hsv = rgb2hsv(colorInfo[i + RGBIndex.RED], colorInfo[i + RGBIndex.GREEN], colorInfo[i + RGBIndex.BLUE]); + let rate = cur / last; + hsv[hsvIndex] *= rate; + const rgb = hsv2rgb(hsv[HSVIndex.HUE], hsv[HSVIndex.SATURATION], hsv[HSVIndex.VALUE]); + colorInfo[i + RGBIndex.RED] = rgb[RGBIndex.RED]; + colorInfo[i + RGBIndex.GREEN] = rgb[RGBIndex.GREEN]; + colorInfo[i + RGBIndex.BLUE] = rgb[RGBIndex.BLUE]; + } + return newBufferArr; +} + +/** + * Color transform. + * + * @param rgbValue 0 - 255. + * @return 0 - 1. + */ +function colorTransform(rgbValue: number) { + return Number((rgbValue / CommonConstants.COLOR_LEVEL_MAX).toFixed(CommonConstants.DECIMAL_TWO)); +} + +/** + * RGB transform to HSV. + * + * @param red 0- 255. + * @param green 0- 255. + * @param blue 0- 255. + * @return h (0 - 360) s(0 - 100) v (0 - 100). + */ +function rgb2hsv(red: number, green: number, blue: number) { + let hsvH: number = 0, hsvS: number = 0, hsvV: number = 0; + const rgbR: number = colorTransform(red); + const rgbG: number = colorTransform(green); + const rgbB: number = colorTransform(blue); + const maxValue = Math.max(rgbR, Math.max(rgbG, rgbB)); + const minValue = Math.min(rgbR, Math.min(rgbG, rgbB)); + hsvV = maxValue * CommonConstants.CONVERT_INT; + if (maxValue === 0) { + hsvS = 0; + } else { + hsvS = Number((1 - minValue / maxValue).toFixed(CommonConstants.DECIMAL_TWO)) * CommonConstants.CONVERT_INT; + } + if (maxValue === minValue) { + hsvH = 0; + } + if (maxValue === rgbR && rgbG >= rgbB) { + hsvH = Math.floor(CommonConstants.ANGLE_60 * ((rgbG - rgbB) / (maxValue - minValue))); + } + if (maxValue === rgbR && rgbG < rgbB) { + hsvH = Math.floor(CommonConstants.ANGLE_60 * ((rgbG - rgbB) / (maxValue - minValue)) + CommonConstants.ANGLE_360); + } + if (maxValue === rgbG) { + hsvH = Math.floor(CommonConstants.ANGLE_60 * ((rgbB - rgbR) / (maxValue - minValue)) + CommonConstants.ANGLE_120); + } + if (maxValue === rgbB) { + hsvH = Math.floor(CommonConstants.ANGLE_60 * ((rgbR - rgbG) / (maxValue - minValue)) + CommonConstants.ANGLE_240); + } + return [hsvH, hsvS, hsvV]; +} + +/** + * HSV to RGB conversion formula: + * When 0 <= H <= 360, 0 <= S <= 1 and 0 <= V <= 1: + * C = V * S + * X = C * (1 - Math.abs((H / 60) mod 2 - 1)) + * m = V - C + * | (C, X ,0), 0 <= H < 60 + * | (X, C, 0), 60 <= H < 120 + * | (0, C, X), 120 <= H < 180 + * (R', G', B') = | (0, X, C), 180 <= H < 240 + * | (X, 0, C), 240 <= H < 300 + * | (C, 0, X), 300 <= H < 360 + * + * (R, G, B) = ((R' + m) * 255, (G' + m) * 255, (B' + m) * 255) + * + * @param h hue 0 ~ 360. + * @param s saturation 0 ~ 100. + * @param v value 0 ~ 100. + * @return rgb value. + */ +function hsv2rgb(hue: number, saturation: number, value: number) { + let rgbR: number = 0, rgbG: number = 0, rgbB: number = 0; + if (saturation === 0) { + rgbR = rgbG = rgbB = Math.round((value * CommonConstants.COLOR_LEVEL_MAX) / CommonConstants.CONVERT_INT); + return { rgbR, rgbG, rgbB }; + } + const cxmC = (value * saturation) / (CommonConstants.CONVERT_INT * CommonConstants.CONVERT_INT); + const cxmX = cxmC * (1 - Math.abs((hue / CommonConstants.ANGLE_60) % CommonConstants.MOD_2 - 1)); + const cxmM = (value - cxmC * CommonConstants.CONVERT_INT) / CommonConstants.CONVERT_INT; + const hsvHRange = Math.floor(hue / CommonConstants.ANGLE_60); + switch (hsvHRange) { + case AngelRange.ANGEL_0_60: + rgbR = (cxmC + cxmM) * CommonConstants.COLOR_LEVEL_MAX; + rgbG = (cxmX + cxmM) * CommonConstants.COLOR_LEVEL_MAX; + rgbB = (0 + cxmM) * CommonConstants.COLOR_LEVEL_MAX; + break; + case AngelRange.ANGEL_60_120: + rgbR = (cxmX + cxmM) * CommonConstants.COLOR_LEVEL_MAX; + rgbG = (cxmC + cxmM) * CommonConstants.COLOR_LEVEL_MAX; + rgbB = (0 + cxmM) * CommonConstants.COLOR_LEVEL_MAX; + break; + case AngelRange.ANGEL_120_180: + rgbR = (0 + cxmM) * CommonConstants.COLOR_LEVEL_MAX; + rgbG = (cxmC + cxmM) * CommonConstants.COLOR_LEVEL_MAX; + rgbB = (cxmX + cxmM) * CommonConstants.COLOR_LEVEL_MAX; + break; + case AngelRange.ANGEL_180_240: + rgbR = (0 + cxmM) * CommonConstants.COLOR_LEVEL_MAX; + rgbG = (cxmX + cxmM) * CommonConstants.COLOR_LEVEL_MAX; + rgbB = (cxmC + cxmM) * CommonConstants.COLOR_LEVEL_MAX; + break; + case AngelRange.ANGEL_240_300: + rgbR = (cxmX + cxmM) * CommonConstants.COLOR_LEVEL_MAX; + rgbG = (0 + cxmM) * CommonConstants.COLOR_LEVEL_MAX; + rgbB = (cxmC + cxmM) * CommonConstants.COLOR_LEVEL_MAX; + break; + case AngelRange.ANGEL_300_360: + rgbR = (cxmC + cxmM) * CommonConstants.COLOR_LEVEL_MAX; + rgbG = (0 + cxmM) * CommonConstants.COLOR_LEVEL_MAX; + rgbB = (cxmX + cxmM) * CommonConstants.COLOR_LEVEL_MAX; + break; + default: + break; + } + return [ + Math.round(rgbR), + Math.round(rgbG), + Math.round(rgbB) + ]; +} \ No newline at end of file diff --git a/entry/src/main/ets/utils/ContextStorageUtil.ets b/entry/src/main/ets/utils/ContextStorageUtil.ets new file mode 100644 index 0000000000000000000000000000000000000000..ed6780adfff0ff2d9fe1ed49c1d17283bd4918c9 --- /dev/null +++ b/entry/src/main/ets/utils/ContextStorageUtil.ets @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License,Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +let uiContext: UIContext; +let uri: string; + +export class ContextStorageUtil { + private static instance: ContextStorageUtil; + + constructor() { + } + + public static getInstance() { + if (!ContextStorageUtil.instance) { + ContextStorageUtil.instance = new ContextStorageUtil(); + } + return ContextStorageUtil.instance; + } + + public init(UIContext: UIContext) { + uiContext = UIContext + } + + public getUtil() { + return uiContext; + } + + public initUri(Uri: string) { + uri = Uri + } + + public getUriUtil() { + return uri; + } +} \ No newline at end of file diff --git a/entry/src/main/ets/utils/CropUtil.ets b/entry/src/main/ets/utils/CropUtil.ets new file mode 100644 index 0000000000000000000000000000000000000000..06fa9ff84920a56bb6ab4df06f0fefa50d6e94a3 --- /dev/null +++ b/entry/src/main/ets/utils/CropUtil.ets @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License,Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RegionItem } from '../viewModel/RegionItem'; +import { CommonConstants } from '../common/constant/CommonConstants'; + +/** + * Crop 1:1. + * + * @param pixelMap. + * @param width. + * @param height. + */ +export async function square(pixelMap: PixelMap, width: number, height: number) { + if (width < height) { + await pixelMap.crop({ + size: { + width: width, + height: width + }, + x: 0, + y: Math.round((height - width) / CommonConstants.AVERAGE_WEIGHT_WIDTH) + }); + } else { + await pixelMap.crop({ + size: { + width: height, + height: height + }, + x: Math.round((width - height) / CommonConstants.AVERAGE_WEIGHT_WIDTH), + y: 0 + }); + } +} + +/** + * Common crop function. + * + * @param pixelMap. + * @param cropWidth. + * @param cropHeight. + * @param cropPosition. + */ +export async function cropCommon(pixelMap: PixelMap, cropWidth: number, cropHeight: number, cropPosition: RegionItem) { + await pixelMap.crop({ + size: { + width: cropWidth, + height: cropHeight + }, + x: cropPosition.x, + y: cropPosition.y + }); +} + +/** + * Crop 4:3. + * + * @param pixelMap. + * @param width. + * @param height. + */ +export async function banner(pixelMap: PixelMap, width: number, height: number) { + if (width <= height) { + const cropWidth = width; + const cropHeight = Math.floor(width * CommonConstants.CROP_RATE_4_3); + const cropPosition = new RegionItem(0, Math.floor((height - cropHeight) / CommonConstants.AVERAGE_WEIGHT_WIDTH)); + await cropCommon(pixelMap, cropWidth, cropHeight, cropPosition); + return; + } + if (width * CommonConstants.CROP_RATE_4_3 >= height) { + const cropWidth = Math.floor(height / CommonConstants.CROP_RATE_4_3); + const cropHeight = height; + const cropPosition = new RegionItem(Math.floor((width - cropWidth) / CommonConstants.AVERAGE_WEIGHT_WIDTH), 0); + await cropCommon(pixelMap, cropWidth, cropHeight, cropPosition); + return; + } + + const cropWidth = width; + const cropHeight = Math.floor(width * CommonConstants.CROP_RATE_4_3); + const cropPosition = new RegionItem(0, Math.floor((height - cropHeight) / CommonConstants.AVERAGE_WEIGHT_WIDTH)); + await cropCommon(pixelMap, cropWidth, cropHeight, cropPosition); +} + +/** + * Crop 16:9. + * + * @param pixelMap. + * @param width. + * @param height. + */ +export async function rectangle(pixelMap: PixelMap, width: number, height: number) { + if (width <= height) { + const cropWidth = width; + const cropHeight = Math.floor(width * (CommonConstants.CROP_RATE_9_16)); + const cropPosition = new RegionItem(0, Math.floor((height - cropHeight) / CommonConstants.AVERAGE_WEIGHT_WIDTH)); + await cropCommon(pixelMap, cropWidth, cropHeight, cropPosition); + return; + } + if (width * (CommonConstants.CROP_RATE_9_16) >= height) { + const cropWidth = Math.floor(height / (CommonConstants.CROP_RATE_9_16)); + const cropHeight = height; + const cropPosition = new RegionItem(Math.floor((width - cropWidth) / CommonConstants.AVERAGE_WEIGHT_WIDTH), 0); + await cropCommon(pixelMap, cropWidth, cropHeight, cropPosition); + return; + } + + const cropWidth = width; + const cropHeight = Math.floor(width * (CommonConstants.CROP_RATE_9_16)); + const cropPosition = new RegionItem(0, Math.floor((height - cropHeight) / CommonConstants.AVERAGE_WEIGHT_WIDTH)); + await cropCommon(pixelMap, cropWidth, cropHeight, cropPosition); +} \ No newline at end of file diff --git a/entry/src/main/ets/utils/DecodeUtil.ets b/entry/src/main/ets/utils/DecodeUtil.ets new file mode 100644 index 0000000000000000000000000000000000000000..369e0f57fab09dd3d6d426c1f4bd20254b44161f --- /dev/null +++ b/entry/src/main/ets/utils/DecodeUtil.ets @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License,Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { fileIo as fs } from '@kit.CoreFileKit'; +import { image } from '@kit.ImageKit'; +import Logger from './LoggerUtil'; +import { CommonConstants } from '../common/constant/CommonConstants'; +import { ContextStorageUtil } from './ContextStorageUtil'; +import { common } from '@kit.AbilityKit'; + +const TAG: string = 'imageEdit_Decode'; + +/** + * Async get resource fd. + * + * @return file fd. + */ +export async function getResourceFd(uri: string = '') { + const uiContext: UIContext = ContextStorageUtil.getInstance().getUtil(); + const context = uiContext.getHostContext() as common.UIAbilityContext; + const resourceMgr = context.resourceManager; + + let filePath: string; + let file: fs.File; + + if (uri === '') { + let imageBuffer = await resourceMgr.getMediaContent($r("app.media.ic_low")); + + filePath = context.cacheDir + '/' + CommonConstants.RAW_FILE_NAME; + file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); + fs.writeSync(file.fd, imageBuffer.buffer); + } else { + file = await fs.openSync(uri, fs.OpenMode.READ_ONLY); + } + + return file.fd; +} + +/** + * Async create pixel map. + * + * @return pixelMa. + */ +export async function getPixelMap(imageSource: image.ImageSource) { + if (!imageSource) { + Logger.error(TAG, 'imageSourceAPI created failed!'); + return; + } + let decodingOptions: image.DecodingOptions = { + editable: true, + desiredPixelFormat: 3, + //Setting to AUTO will decode based on the image resource format. If the image resource is an HDR resource, it will be decoded as an HDR pixel map + desiredDynamicRange: image.DecodingDynamicRange.AUTO + }; + const pixelMap = await imageSource.createPixelMap(decodingOptions); + + return pixelMap; +} \ No newline at end of file diff --git a/entry/src/main/ets/utils/EncodeUtil.ets b/entry/src/main/ets/utils/EncodeUtil.ets new file mode 100644 index 0000000000000000000000000000000000000000..f9c1a087d23e6a1f3a6bb32e823482386017a890 --- /dev/null +++ b/entry/src/main/ets/utils/EncodeUtil.ets @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License,Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { photoAccessHelper } from '@kit.MediaLibraryKit'; +import { fileIo } from '@kit.CoreFileKit'; +import { image } from '@kit.ImageKit'; +import Logger from './LoggerUtil'; +import { CommonConstants } from '../common/constant/CommonConstants'; +import { ContextStorageUtil } from './ContextStorageUtil' +import { common } from '@kit.AbilityKit'; + +const TAG: string = 'imageEdit_Encode'; + +/** + * Pack the image. + * + * @param pixelMap. + */ +export async function encode(pixelMap: PixelMap) { + const uiContext: UIContext = ContextStorageUtil.getInstance().getUtil(); + + const context = uiContext.getHostContext() as common.UIAbilityContext; + + let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context); + + const newPixelMap = pixelMap; + // Packing image. + const imagePackerApi = image.createImagePacker(); + const packOptions: image.PackingOption = { + format: CommonConstants.ENCODE_FORMAT, + quality: CommonConstants.ENCODE_QUALITY + } + const imageData = await imagePackerApi.packToData(newPixelMap, packOptions); + Logger.info(TAG, `imageData's length is ${imageData.byteLength}`); + // Create image asset. + let photoType: photoAccessHelper.PhotoType = photoAccessHelper.PhotoType.IMAGE; + let extension: string = 'jpg'; + phAccessHelper.createAsset(photoType, extension, (err, uri) => { + if (uri != undefined) { + let file = fileIo.openSync(uri, fileIo.OpenMode.READ_WRITE); + fileIo.writeSync(file.fd, imageData); + fileIo.close(file.fd); + } + }); +} \ No newline at end of file diff --git a/entry/src/main/ets/utils/LoggerUtil.ets b/entry/src/main/ets/utils/LoggerUtil.ets new file mode 100644 index 0000000000000000000000000000000000000000..fba77aa02f5c8a36f3eb15d7577cd39c94a0033e --- /dev/null +++ b/entry/src/main/ets/utils/LoggerUtil.ets @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License,Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import { hilog } from '@kit.PerformanceAnalysisKit'; + +/** + * Log printing tool class. + */ +class Logger { + private domain: number; + private prefix: string; + private format: string = '%{public}s, %{public}s'; + + /** + * Constructor. + * + * @param Prefix Identifies the log tag. + * @param domain Domain Indicates the service domain, which is a hexadecimal integer ranging from 0x0 to 0xFFFFF. + */ + constructor(prefix: string = 'MyApp', domain: number = 0xFF00) { + this.prefix = prefix; + this.domain = domain; + } + + debug(...args: string[]) { + hilog.debug(this.domain, this.prefix, this.format, args); + } + + info(...args: string[]) { + hilog.info(this.domain, this.prefix, this.format, args); + } + + warn(...args: string[]) { + hilog.warn(this.domain, this.prefix, this.format, args); + } + + error(...args: string[]) { + hilog.error(this.domain, this.prefix, this.format, args); + } +} + +export default new Logger('[ImageEdit]') \ No newline at end of file diff --git a/entry/src/main/ets/utils/OpacityUtil.ets b/entry/src/main/ets/utils/OpacityUtil.ets new file mode 100644 index 0000000000000000000000000000000000000000..3a0cd9aeb1ae976ec76546ccc1168d5ae57f40d1 --- /dev/null +++ b/entry/src/main/ets/utils/OpacityUtil.ets @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License,Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { image } from '@kit.ImageKit'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { CommonConstants } from '../common/constant/CommonConstants'; +import Logger from './LoggerUtil'; + +const TAG = 'Opacity'; + +/** + * Opacity adjust. + * + * @param pixelMap. + * @param value. + * @return pixelMap. + */ +export async function adjustOpacity(pixelMap: image.PixelMap, value: number) { + if (!pixelMap) { + return; + } + const newPixelMap = pixelMap; + await newPixelMap.opacity(value / CommonConstants.SLIDER_MAX).then(() => { + Logger.info(TAG, 'Success in setting opacity.'); + }).catch((err: BusinessError) => { + Logger.error(TAG, `Failed to set opacity: ${JSON.stringify(err.message)}`); + }) + + return newPixelMap; +} \ No newline at end of file diff --git a/entry/src/main/ets/utils/WatermarkUtil.ets b/entry/src/main/ets/utils/WatermarkUtil.ets new file mode 100644 index 0000000000000000000000000000000000000000..00b94d54078d400f40e2613ca2a646719c0e0e8e --- /dev/null +++ b/entry/src/main/ets/utils/WatermarkUtil.ets @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License,Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { image } from '@kit.ImageKit'; +import Logger from './LoggerUtil'; + +const TAG: string = 'WatermarkUtil'; + +export enum WatermarkPosition { + TOP_LEFT = 0, + TOP_RIGHT = 1, + BOTTOM_LEFT = 2, + BOTTOM_RIGHT = 3, + CENTER = 4 +} + +export interface WatermarkConfig { + text: string; + position: WatermarkPosition; + fontSize: number; + color: string; + opacity: number; + rotation?: number; +} + +interface PositionResult { + x: number; + y: number; +} + +interface SpacingConfig { + x: number; + y: number; +} + +export class WatermarkUtil { + /** + * 在PixelMap上添加文字水印 + * @param pixelMap 原始图片 + * @param config 水印配置 + * @returns 添加水印后的PixelMap + */ + static async addTextWatermark(pixelMap: image.PixelMap, config: WatermarkConfig): Promise { + try { + const imageInfo = await pixelMap.getImageInfo(); + const imageWidth = imageInfo.size.width; + const imageHeight = imageInfo.size.height; + + const offscreenCanvas = new OffscreenCanvas(imageWidth, imageHeight); + const context = offscreenCanvas.getContext('2d') as OffscreenCanvasRenderingContext2D; + + context.drawImage(pixelMap, 0, 0, imageWidth, imageHeight); + + context.font = `${config.fontSize}px sans-serif`; + context.fillStyle = config.color; + context.globalAlpha = config.opacity; + + const textMetrics = context.measureText(config.text); + const textWidth = textMetrics.width; + const textHeight = config.fontSize; + + const position = WatermarkUtil.calculateWatermarkPosition( + imageWidth, + imageHeight, + textWidth, + textHeight, + config.position + ); + + if (config.rotation && config.rotation !== 0) { + context.save(); + context.translate(position.x + textWidth / 2, position.y + textHeight / 2); + context.rotate((config.rotation * Math.PI) / 180); + context.fillText(config.text, -textWidth / 2, textHeight / 4); + context.restore(); + } else { + context.fillText(config.text, position.x, position.y + textHeight); + } + + const watermarkedPixelMap = await context.getPixelMap(0, 0, imageWidth, imageHeight); + + Logger.info(TAG, 'Watermark added successfully'); + return watermarkedPixelMap; + } catch (error) { + Logger.error(TAG, `Error adding watermark: ${JSON.stringify(error)}`); + throw new Error(`Failed to add watermark: ${JSON.stringify(error)}`); + } + } + + /** + * 计算水印位置 + */ + private static calculateWatermarkPosition( + imageWidth: number, + imageHeight: number, + textWidth: number, + textHeight: number, + position: WatermarkPosition + ): PositionResult { + const padding = 20; + + switch (position) { + case WatermarkPosition.TOP_LEFT: + return { x: padding, y: padding } as PositionResult; + case WatermarkPosition.TOP_RIGHT: + return { x: imageWidth - textWidth - padding, y: padding } as PositionResult; + case WatermarkPosition.BOTTOM_LEFT: + return { x: padding, y: imageHeight - textHeight - padding } as PositionResult; + case WatermarkPosition.BOTTOM_RIGHT: + return { x: imageWidth - textWidth - padding, y: imageHeight - textHeight - padding } as PositionResult; + case WatermarkPosition.CENTER: + return { + x: (imageWidth - textWidth) / 2, + y: (imageHeight - textHeight) / 2 + } as PositionResult; + default: + return { x: padding, y: padding } as PositionResult; + } + } + + /** + * 创建重复水印效果 + * @param pixelMap 原始图片 + * @param config 水印配置 + * @param spacing 水印间距 + * @returns 添加重复水印后的PixelMap + */ + static async addRepeatedWatermark( + pixelMap: image.PixelMap, + config: WatermarkConfig, + spacing?: SpacingConfig + ): Promise { + try { + const defaultSpacing: SpacingConfig = { x: 200, y: 150 }; + const actualSpacing = spacing || defaultSpacing; + + const imageInfo = await pixelMap.getImageInfo(); + const imageWidth = imageInfo.size.width; + const imageHeight = imageInfo.size.height; + + const offscreenCanvas = new OffscreenCanvas(imageWidth, imageHeight); + const context = offscreenCanvas.getContext('2d') as OffscreenCanvasRenderingContext2D; + + context.drawImage(pixelMap, 0, 0, imageWidth, imageHeight); + + context.font = `${config.fontSize}px sans-serif`; + context.fillStyle = config.color; + context.globalAlpha = config.opacity; + + const textMetrics = context.measureText(config.text); + const textWidth = textMetrics.width; + const textHeight = config.fontSize; + + for (let x = 0; x < imageWidth; x += actualSpacing.x) { + for (let y = 0; y < imageHeight; y += actualSpacing.y) { + if (config.rotation && config.rotation !== 0) { + context.save(); + context.translate(x + textWidth / 2, y + textHeight / 2); + context.rotate((config.rotation * Math.PI) / 180); + context.fillText(config.text, -textWidth / 2, textHeight / 4); + context.restore(); + } else { + context.fillText(config.text, x, y + textHeight); + } + } + } + + const watermarkedPixelMap = await context.getPixelMap(0, 0, imageWidth, imageHeight); + Logger.info(TAG, 'Repeated watermark added successfully'); + return watermarkedPixelMap; + } catch (error) { + Logger.error(TAG, `Error adding repeated watermark: ${JSON.stringify(error)}`); + throw new Error(`Failed to add repeated watermark: ${JSON.stringify(error)}`); + } + } +} \ No newline at end of file diff --git a/entry/src/main/ets/view/AdjustContentView.ets b/entry/src/main/ets/view/AdjustContentView.ets new file mode 100644 index 0000000000000000000000000000000000000000..399e24e76283c79374f0777ead18a671537b3eb7 --- /dev/null +++ b/entry/src/main/ets/view/AdjustContentView.ets @@ -0,0 +1,241 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License,Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { image } from '@kit.ImageKit'; +import { worker, MessageEvents, taskpool } from '@kit.ArkTS'; +import { adjustIconList, IconStatus } from '../viewModel/IconListViewModel'; +import { CommonConstants } from '../common/constant/CommonConstants'; +import { AdjustId } from '../viewModel/OptionViewModel'; +import { MessageItem } from '../viewModel/MessageItem'; + +interface translateListType { + icon: string, + title: string | Resource, + color: string, + selectColor: string +} + +@Component +struct SliderCustom { + @Consume('currentAdjustData') currentAdjustData: number[]; + @Prop min: number; + @Prop max: number; + @Consume('pixelMap') pixelMap?: image.PixelMap; + @Consume('currentAdjustIndex') currentAdjustIndex: number; + @Consume('isPixelMapChange') isPixelMapChange: boolean; + private postState: boolean = true; + saturationLastSlider: number = CommonConstants.SLIDER_MAX; + brightnessLastSlider: number = CommonConstants.SLIDER_MAX; + deviceListDialogController: CustomDialogController = new CustomDialogController({ + builder: Dialog(), + alignment: DialogAlignment.Center, + autoCancel: false, + customStyle: true + }); + + build() { + Column() { + Row() { + Slider({ + value: this.currentAdjustData[this.currentAdjustIndex], + step: CommonConstants.SLIDER_STEP, + min: this.min, + max: this.max + }) + .trackColor('#1affffff') + .selectedColor(Color.White) + .width(CommonConstants.SLIDER_WIDTH) + .showSteps(true) + .showTips(true) + .clip(false) + .onChange((value: number, mode: SliderChangeMode) => { + this.sliderChange(value > this.max ? this.max : value, mode); + }) + } + .padding({ top: 50, bottom: 20 }) + .width(CommonConstants.LAYOUT_FULL_SCREEN) + .justifyContent(FlexAlign.Center) + .clip(false) + } + } + + updatePixelMap(event: MessageEvents) { + const newPixel = this.pixelMap as image.PixelMap; + newPixel.writeBufferToPixels(event.data); + this.pixelMap = newPixel; + this.isPixelMapChange = !this.isPixelMapChange; + this.deviceListDialogController.close(); + this.postState = true; + } + + async sliderChange(value: number, mode: SliderChangeMode) { + if ((mode === SliderChangeMode.End) && (value !== this.currentAdjustData[this.currentAdjustIndex])) { + this.currentAdjustData[this.currentAdjustIndex] = Math.round(value); + switch (this.currentAdjustIndex) { + case AdjustId.BRIGHTNESS: + this.postToWorker(AdjustId.BRIGHTNESS, value, CommonConstants.BRIGHTNESS_WORKER_FILE); + break; + case AdjustId.TRANSPARENCY: + if (this.pixelMap) { + this.deviceListDialogController.open(); + this.pixelMap?.opacitySync(value / CommonConstants.SLIDER_MAX) + this.isPixelMapChange = !this.isPixelMapChange; + this.deviceListDialogController.close(); + } + break; + case AdjustId.SATURATION: + this.postToWorker(AdjustId.SATURATION, value, CommonConstants.SATURATION_WORKER_FILE); + break; + default: + break; + } + } + } + + postToWorker(type: AdjustId, value: number, workerName: string) { + if (!this.pixelMap) { + return; + } + let sliderValue = type === AdjustId.BRIGHTNESS ? this.brightnessLastSlider : this.saturationLastSlider; + let workerInstance = new worker.ThreadWorker(workerName); + const bufferArray = new ArrayBuffer(this.pixelMap.getPixelBytesNumber()); + this.pixelMap.readPixelsToBuffer(bufferArray).then(() => { + let message = new MessageItem(bufferArray, sliderValue, value); + workerInstance.postMessage(message); + if (this.postState) { + this.deviceListDialogController.open(); + } + this.postState = false; + workerInstance.onmessage = (event: MessageEvents) => { + this.updatePixelMap(event); + workerInstance.terminate(); + } + if (type === AdjustId.BRIGHTNESS) { + this.brightnessLastSlider = Math.round(value); + } else { + this.saturationLastSlider = Math.round(value); + } + workerInstance.onexit = () => { + if (workerInstance !== undefined) { + workerInstance.terminate(); + } + } + }); + } +} + +@CustomDialog +export struct Dialog { + controller?: CustomDialogController; + + build() { + Column() { + LoadingProgress() + .color(Color.White) + .width(CommonConstants.LOADING_WH) + .height(CommonConstants.LOADING_WH) + } + } +} + +@Component +export default struct AdjustContentView { + @Builder + TabBuilder(index: number, name: string | Resource) { + Column() { + Image(this.currentAdjustIndex === index ? this.AdjustIconList[index]?.chosen : this.AdjustIconList[index]?.normal) + .width($r('app.float.adjust_icon_width')) + .height($r('app.float.adjust_icon_height')) + + Text(name) + .fontColor(this.currentAdjustIndex === index ? Color.Blue : Color.White) + .fontSize($r('app.float.adjust_font_size')) + .margin({ top: $r('app.float.adjust_margin_top') }) + } + .width(CommonConstants.LAYOUT_FULL_SCREEN) + } + + @Provide currentAdjustIndex: number = 0; + @State adjustList: translateListType[] = [ + { + icon: 'brightness', + title: $r('app.string.light'), + color: '#fff', + selectColor: '#ff000000' + }, + { + icon: 'transparency_lock', + title: $r('app.string.transparency'), + color: '#fff', + selectColor: '#ff000000' + }, + { + icon: 'drop_bottomrighthalf_inset_filled', + title: $r('app.string.saturation'), + color: '#fff', + selectColor: '#ff000000' + } + ]; + @Consume('currentAdjustData') currentAdjustData: Array; + AdjustIconList: Array = adjustIconList; + + build() { + Column() { + Row() { + SliderCustom({ + min: CommonConstants.SLIDER_MIN.valueOf(), + max: CommonConstants.SLIDER_MAX.valueOf() + }) + } + .width('100%') + .justifyContent(FlexAlign.Center) + + Row() { + ForEach(this.adjustList, (item: translateListType, index) => { + Column() { + Text() { + SymbolSpan($r(`sys.symbol.${item.icon}`)) + .fontColor(this.currentAdjustIndex === index ? + [item.selectColor, item.selectColor, item.selectColor] : + [item.color, item.color, item.color]) + .fontWeight(FontWeight.Normal) + .fontSize(22) + } + .padding(9) + .borderRadius('50%') + .backgroundColor(this.currentAdjustIndex === index ? '#e6ffffff' : '#26ffffff') + + Text(item.title) + .margin({ top: 4 }) + .fontColor('#fff') + .fontSize(10) + } + .margin({ left: 37, right: 37 }) + .onClick(() => { + this.currentAdjustIndex = index; + }) + }) + } + .width('100%') + .justifyContent(FlexAlign.Center) + } + .justifyContent(FlexAlign.Start) + .width(CommonConstants.LAYOUT_FULL_SCREEN) + .height('100%') + .clip(false) + } +} + + diff --git a/entry/src/main/ets/view/ApplyFilterView.ets b/entry/src/main/ets/view/ApplyFilterView.ets new file mode 100644 index 0000000000000000000000000000000000000000..7202022289a5fbe5ff0abc997f9a38d0a9773fb9 --- /dev/null +++ b/entry/src/main/ets/view/ApplyFilterView.ets @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License,Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { image } from '@kit.ImageKit'; +import { effectKit } from "@kit.ArkGraphics2D"; + +interface filterDataType { + title: string | Resource + key: string + color: string +} + +@Component +export default struct ApplyFilterView { + scroller: Scroller = new Scroller(); + @State filterData: Array = [ + { + title: $r('app.string.none'), + key: 'null', + color: '#fff1efef' + }, + { + title: $r('app.string.grayscale'), + key: 'grayscale', + color: '#ffeaeaea' + }, + { + title: $r('app.string.brightness'), + key: 'brightness', + color: '#ffc1bdbd' + }, + { + title: $r('app.string.invert'), + key: 'invert', + color: '#ff9f9c9c' + }, + { + title: $r('app.string.blur'), + key: 'blur', + color: '#7a000000' + }, + { + title: $r('app.string.customize'), + key: 'customize', + color: '#7a000000' + } + ]; + @Consume filterCurrIndex: number; + @Consume filterPixelMap?: image.PixelMap; + @Consume('pixelMap') pixelMap?: image.PixelMap; + @Consume('isPixelMapChange') isPixelMapChange: boolean; + + @State private isProcessing: boolean = false; + @State private lastProcessedPixelMap?: image.PixelMap = undefined; + + aboutToAppear() { + if (this.filterCurrIndex > 0 && this.pixelMap) { + this.handleFilter(this.filterCurrIndex); + } + } + + async handleFilter(index: number) { + if (this.isProcessing || !this.pixelMap) { + return; + } + + if (this.lastProcessedPixelMap === this.pixelMap && this.filterCurrIndex === index) { + return; + } + + this.isProcessing = true; + this.filterCurrIndex = index; + + try { + let filter = effectKit.createEffect(this.pixelMap); + switch (this.filterData[index].key) { + case 'grayscale': + this.filterPixelMap = await filter.grayscale().getEffectPixelMap(); + break; + case 'brightness': + this.filterPixelMap = await filter.brightness(0.7).getEffectPixelMap(); + break; + case 'invert': + this.filterPixelMap = await filter.invert().getEffectPixelMap(); + break; + case 'blur': + this.filterPixelMap = await filter.blur(5).getEffectPixelMap(); + break; + case 'null': + this.filterPixelMap = undefined; + break; + default: + this.filterPixelMap = await filter.grayscale().getEffectPixelMap(); + break; + } + + this.lastProcessedPixelMap = this.pixelMap; + + this.isPixelMapChange = !this.isPixelMapChange; + } catch (error) { + console.error('Filter processing failed:', error); + } finally { + this.isProcessing = false; + } + } + + build() { + Column() { + Scroll(this.scroller) { + Row() { + ForEach(this.filterData, (item: filterDataType, index) => { + Column() { + Row() { + Text(item.title) + .margin({ + left: 8, + bottom: 8 + }) + .fontSize(12) + .fontColor('#fff') + } + .width(68) + .height(68) + .alignItems(VerticalAlign.Bottom) + .backgroundImage($rawfile('low.jpg')) + .borderRadius(8) + .borderWidth(index === this.filterCurrIndex ? 2 : 0) + .borderColor('#ffffffff') + } + .margin(2) + .onClick(() => { + if (!this.isProcessing) { + this.handleFilter(index); + } + }) + }) + } + } + .scrollable(ScrollDirection.Horizontal) + .scrollBar(BarState.Off) + .scrollBarColor('#ffeceaea') + .scrollBarWidth(0) + .friction(0) + .edgeEffect(EdgeEffect.None) + } + .padding({ left: 16, right: 16 }) + } +} diff --git a/entry/src/main/ets/view/WatermarkView.ets b/entry/src/main/ets/view/WatermarkView.ets new file mode 100644 index 0000000000000000000000000000000000000000..b1a3b8a4585fdb42e3f293b0df5a8dc256cfc4ba --- /dev/null +++ b/entry/src/main/ets/view/WatermarkView.ets @@ -0,0 +1,344 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License,Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { WatermarkUtil, WatermarkPosition, WatermarkConfig } from '../utils/WatermarkUtil'; + +@Component +export struct WatermarkView { + @Consume pixelMap: PixelMap | undefined; + @Consume isPixelMapChange: boolean; + @State watermarkText: string = ''; + @State currentPosition: WatermarkPosition = WatermarkPosition.BOTTOM_RIGHT; + @State fontSize: number = 24; + @State watermarkOpacity: number = 0.8; + @State watermarkColor: string = '#FFFFFF'; + @State rotation: number = 0; + @State isRepeated: boolean = false; + @State hasWatermark: boolean = false; + @State originalPixelMap: PixelMap | undefined = undefined; + + aboutToAppear() { + if (this.pixelMap && !this.originalPixelMap) { + this.originalPixelMap = this.pixelMap; + } + } + + build() { + Column() { + Scroll() { + Column() { + Row() { + Text($r('app.string.watermark_text')) + .fontSize(16) + .fontWeight(500) + TextInput({ placeholder: $r('app.string.watermark_text') }) + .fontSize(14) + .fontColor('#99000000') + .backgroundColor('#f5f5f5') + .borderRadius(8) + .padding({ left: 12, right: 12 }) + .width(120) + .height(32) + .onChange((value: string) => { + this.watermarkText = value; + }) + } + .padding({ top: 13, bottom: 13 }) + .width('100%') + .justifyContent(FlexAlign.SpaceBetween) + .alignItems(VerticalAlign.Center) + + Divider().strokeWidth(0.5).color('#33000000') + + Row() { + Text($r('app.string.watermark_position')) + .fontSize(16) + .fontWeight(500) + Column() { + Grid() { + GridItem() { + Text($r('app.string.position_top_left')) + .fontSize(14) + .fontColor(this.currentPosition === WatermarkPosition.TOP_LEFT ? '#5291FF' : '#99000000') + .padding(12) + .borderRadius(6) + .backgroundColor(this.currentPosition === WatermarkPosition.TOP_LEFT ? '#e6f3ff' : '#f5f5f5') + .onClick(() => { + this.currentPosition = WatermarkPosition.TOP_LEFT; + }) + } + GridItem() { + Text($r('app.string.position_top_right')) + .fontSize(14) + .fontColor(this.currentPosition === WatermarkPosition.TOP_RIGHT ? '#5291FF' : '#99000000') + .padding(12) + .borderRadius(6) + .backgroundColor(this.currentPosition === WatermarkPosition.TOP_RIGHT ? '#e6f3ff' : '#f5f5f5') + .onClick(() => { + this.currentPosition = WatermarkPosition.TOP_RIGHT; + }) + } + GridItem() { + Text($r('app.string.position_bottom_left')) + .fontSize(14) + .fontColor(this.currentPosition === WatermarkPosition.BOTTOM_LEFT ? '#5291FF' : '#99000000') + .padding(12) + .borderRadius(6) + .backgroundColor(this.currentPosition === WatermarkPosition.BOTTOM_LEFT ? '#e6f3ff' : '#f5f5f5') + .onClick(() => { + this.currentPosition = WatermarkPosition.BOTTOM_LEFT; + }) + } + GridItem() { + Text($r('app.string.position_bottom_right')) + .fontSize(14) + .fontColor(this.currentPosition === WatermarkPosition.BOTTOM_RIGHT ? '#5291FF' : '#99000000') + .padding(12) + .borderRadius(6) + .backgroundColor(this.currentPosition === WatermarkPosition.BOTTOM_RIGHT ? '#e6f3ff' : '#f5f5f5') + .onClick(() => { + this.currentPosition = WatermarkPosition.BOTTOM_RIGHT; + }) + } + GridItem() { + Text($r('app.string.position_center')) + .fontSize(14) + .fontColor(this.currentPosition === WatermarkPosition.CENTER ? '#5291FF' : '#99000000') + .padding(12) + .borderRadius(6) + .backgroundColor(this.currentPosition === WatermarkPosition.CENTER ? '#e6f3ff' : '#f5f5f5') + .onClick(() => { + this.currentPosition = WatermarkPosition.CENTER; + }) + } + } + .columnsTemplate('1fr 1fr') + .rowsTemplate('1fr 1fr 1fr') + .columnsGap(10) + .rowsGap(10) + .width(150) + .height(120) + } + } + .padding({ top: 13, bottom: 13 }) + .width('100%') + .justifyContent(FlexAlign.SpaceBetween) + .alignItems(VerticalAlign.Center) + + Divider().strokeWidth(0.5).color('#33000000') + + Row() { + Text($r('app.string.watermark_size')) + .fontSize(16) + .fontWeight(500) + Text(`${this.fontSize}px`) + .fontSize(14) + .fontColor('#99000000') + } + .padding({ top: 13, bottom: 13 }) + .width('100%') + .justifyContent(FlexAlign.SpaceBetween) + .alignItems(VerticalAlign.Center) + + Row() { + Slider({ + value: this.fontSize, + min: 12, + max: 72, + step: 1 + }) + .width('100%') + .trackColor('#f5f5f5') + .selectedColor('#5291FF') + .blockColor('#5291FF') + .onChange((value: number) => { + this.fontSize = Math.round(value); + }) + } + .padding({ bottom: 13 }) + .width('100%') + + Divider().strokeWidth(0.5).color('#33000000') + + Row() { + Text($r('app.string.watermark_opacity')) + .fontSize(16) + .fontWeight(500) + Text(`${Math.round(this.watermarkOpacity * 100)}%`) + .fontSize(14) + .fontColor('#99000000') + } + .padding({ top: 13, bottom: 13 }) + .width('100%') + .justifyContent(FlexAlign.SpaceBetween) + .alignItems(VerticalAlign.Center) + + Row() { + Slider({ + value: this.watermarkOpacity, + min: 0.1, + max: 1.0, + step: 0.1 + }) + .width('100%') + .trackColor('#f5f5f5') + .selectedColor('#5291FF') + .blockColor('#5291FF') + .onChange((value: number) => { + this.watermarkOpacity = Math.round(value * 10) / 10; + }) + } + .padding({ bottom: 13 }) + .width('100%') + + Divider().strokeWidth(0.5).color('#33000000') + + Row() { + Text($r('app.string.watermark_rotation')) + .fontSize(16) + .fontWeight(500) + Text(`${this.rotation}°`) + .fontSize(14) + .fontColor('#99000000') + } + .padding({ top: 13, bottom: 13 }) + .width('100%') + .justifyContent(FlexAlign.SpaceBetween) + .alignItems(VerticalAlign.Center) + + Row() { + Slider({ + value: this.rotation, + min: -45, + max: 45, + step: 1 + }) + .width('100%') + .trackColor('#f5f5f5') + .selectedColor('#5291FF') + .blockColor('#5291FF') + .onChange((value: number) => { + this.rotation = Math.round(value); + }) + } + .padding({ bottom: 13 }) + .width('100%') + + Divider().strokeWidth(0.5).color('#33000000') + + Row() { + Text($r('app.string.watermark_repeat')) + .fontSize(16) + .fontWeight(500) + Toggle({ type: ToggleType.Switch, isOn: this.isRepeated }) + .selectedColor('#5291FF') + .switchPointColor('#FFFFFF') + .onChange((isOn: boolean) => { + this.isRepeated = isOn; + }) + } + .padding({ top: 13, bottom: 13 }) + .width('100%') + .justifyContent(FlexAlign.SpaceBetween) + .alignItems(VerticalAlign.Center) + + Divider().strokeWidth(0.5).color('#33000000') + + Row() { + Button($r('app.string.add_watermark')) + .fontSize(14) + .fontColor('#5291FF') + .backgroundColor('#e6f3ff') + .borderRadius(8) + .width(100) + .height(36) + .enabled(this.watermarkText.length > 0) + .onClick(() => { + this.addWatermark(); + }) + + Button($r('app.string.remove_watermark')) + .fontSize(14) + .fontColor('#ff4444') + .backgroundColor('#ffe6e6') + .borderRadius(8) + .width(100) + .height(36) + .enabled(this.hasWatermark) + .onClick(() => { + this.removeWatermark(); + }) + } + .padding({ top: 20, bottom: 16, left: 16, right: 16 }) + .width('100%') + .justifyContent(FlexAlign.SpaceBetween) + } + .width('100%') + .justifyContent(FlexAlign.Start) + } + .padding({ + top: 4, + bottom: 4, + left: 12, + right: 12 + }) + .backgroundColor('#fff') + .borderRadius(16) + .scrollable(ScrollDirection.Vertical) + .scrollBar(BarState.Off) + .friction(0.6) + .edgeEffect(EdgeEffect.None) + } + .width('100%') + } + + private async addWatermark() { + if (!this.pixelMap || this.watermarkText.trim() === '') { + return; + } + + try { + const config: WatermarkConfig = { + text: this.watermarkText, + position: this.currentPosition, + fontSize: this.fontSize, + color: this.watermarkColor, + opacity: this.watermarkOpacity, + rotation: this.rotation + }; + + let newPixelMap: PixelMap; + if (this.isRepeated) { + newPixelMap = await WatermarkUtil.addRepeatedWatermark(this.pixelMap, config); + } else { + newPixelMap = await WatermarkUtil.addTextWatermark(this.pixelMap, config); + } + + this.pixelMap = newPixelMap; + this.isPixelMapChange = !this.isPixelMapChange; + this.hasWatermark = true; + } catch (error) { + console.error('添加水印失败:', JSON.stringify(error)); + } + } + + private removeWatermark() { + if (this.originalPixelMap) { + this.pixelMap = this.originalPixelMap; + this.isPixelMapChange = !this.isPixelMapChange; + this.hasWatermark = false; + } + } +} \ No newline at end of file diff --git a/entry/src/main/ets/viewModel/IconListViewModel.ets b/entry/src/main/ets/viewModel/IconListViewModel.ets new file mode 100644 index 0000000000000000000000000000000000000000..1e00bdfc3fed6138220b9df80679c6dbf4912c3e --- /dev/null +++ b/entry/src/main/ets/viewModel/IconListViewModel.ets @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License,Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Icon status. + */ +export class IconStatus { + normal: Resource; + chosen: Resource; + + constructor(normal: Resource, chosen: Resource) { + this.normal = normal; + this.chosen = chosen; + } +} + +/** + * Bottom menu icon. + */ +export const menuIconList: Array = [ + new IconStatus($r('app.media.ic_crop'), $r('app.media.ic_crop_filled')), + new IconStatus($r('app.media.ic_mirror'), $r('app.media.ic_mirror_filled')), + new IconStatus($r('app.media.ic_rotate'), $r('app.media.ic_rotate_filled')), + new IconStatus($r('app.media.ic_adjust'), $r('app.media.ic_adjust_filled')), + new IconStatus($r('app.media.ic_reverse_order'), $r('app.media.ic_reverse_order_filled')) +] + +/** + * Crop icon. + */ +export const cropIconChangeList: Array = [ + new IconStatus($r('app.media.ic_original'), $r('app.media.ic_original_filled')), + new IconStatus($r('app.media.ic_one2one'), $r('app.media.ic_one2one_filled')), + new IconStatus($r('app.media.ic_four2three'), $r('app.media.ic_four2three_filled')), + new IconStatus($r('app.media.ic_sixteen2nine'), $r('app.media.ic_sixteen2nine_filled')) +] + +/** + * Adjust icon. + */ +export const adjustIconList: Array = [ + new IconStatus($r('app.media.ic_brightness'), $r('app.media.ic_brightness_filled')), + new IconStatus($r('app.media.ic_transparency'), $r('app.media.ic_transparency_filled')), + new IconStatus($r('app.media.ic_saturation'), $r('app.media.ic_saturation_filled')) +] + diff --git a/entry/src/main/ets/viewModel/MessageItem.ets b/entry/src/main/ets/viewModel/MessageItem.ets new file mode 100644 index 0000000000000000000000000000000000000000..6a3cecf6ae7137fb2e4dfb65daf14e591688779c --- /dev/null +++ b/entry/src/main/ets/viewModel/MessageItem.ets @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License,Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Multithreading transmission message. + */ +export class MessageItem { + constructor(buf: ArrayBuffer, last: number, cur: number) { + this.buf = buf; + this.last = last; + this.cur = cur; + } + + /** + * Send buffers. + */ + buf: ArrayBuffer; + /** + * Last slider value. + */ + last: number; + /** + * Current slider value. + */ + cur: number; +} \ No newline at end of file diff --git a/entry/src/main/ets/viewModel/OptionViewModel.ts b/entry/src/main/ets/viewModel/OptionViewModel.ts new file mode 100644 index 0000000000000000000000000000000000000000..0886e782cee3735754ecb20b01be38f0e044d7a5 --- /dev/null +++ b/entry/src/main/ets/viewModel/OptionViewModel.ts @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License,Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Crop type. + */ +export enum CropType { + ORIGINAL_IMAGE, + SQUARE, + BANNER, + RECTANGLE +} + +/** + * Rotate type + */ +export enum RotateType { + CLOCKWISE, + ANTI_CLOCK +} + +/** + * Mirror type + */ +export enum MirrorType { + CLOCKWISE, + ANTI_CLOCK +} + +/** + * Adjust type. + */ +export enum AdjustId { + BRIGHTNESS, + TRANSPARENCY, + SATURATION +} + +/** + * Main page tab type. + */ +export enum MainTabId { + CROP, + ADJUST, + FILTER, + TRANSLATE, + ZOOM, + MIRROR, + ROTATE +} + +/** + * RGB color, red,green and blue. + */ +export enum RGBIndex { + RED, + GREEN, + BLUE +} + +/** + * HSV type. + */ +export enum HSVIndex { + HUE, + SATURATION, + VALUE +} + +/** + * Angel range. + */ +export enum AngelRange { + ANGEL_0_60, + ANGEL_60_120, + ANGEL_120_180, + ANGEL_180_240, + ANGEL_240_300, + ANGEL_300_360 +} \ No newline at end of file diff --git a/entry/src/main/ets/viewModel/RegionItem.ets b/entry/src/main/ets/viewModel/RegionItem.ets new file mode 100644 index 0000000000000000000000000000000000000000..c58aeccc87d032373bb1603e71b3b296d77fd2e7 --- /dev/null +++ b/entry/src/main/ets/viewModel/RegionItem.ets @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License,Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class RegionItem { + /** + * width coordinate. + */ + x: number; + /** + * height coordinate. + */ + y: number; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } +} \ No newline at end of file diff --git a/entry/src/main/ets/workers/AdjustBrightnessWork.ts b/entry/src/main/ets/workers/AdjustBrightnessWork.ts new file mode 100644 index 0000000000000000000000000000000000000000..bf7265c5926600152c7019c25091e5c3cc0bbf16 --- /dev/null +++ b/entry/src/main/ets/workers/AdjustBrightnessWork.ts @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License,Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { worker, ThreadWorkerGlobalScope, MessageEvents, ErrorEvent } from '@kit.ArkTS'; +import { adjustImageValue } from '../utils/AdjustUtil'; + +let workerPort: ThreadWorkerGlobalScope = worker.workerPort; + +/** + * Defines the event handler to be called when the worker thread receives a message sent by the host thread. + * The event handler is executed in the worker thread. + * + * @param e message data + */ +workerPort.onmessage = function (event: MessageEvents) { + let bufferArray = event.data.buf; + let last = event.data.last; + let cur = event.data.cur; + let buffer = adjustImageValue(bufferArray, last, cur); + workerPort.postMessage(buffer); +} + +/** + * Defines the event handler to be called when the worker receives a message that cannot be deserialized. + * The event handler is executed in the worker thread. + * + * @param e message data + */ +workerPort.onmessageerror = function (event: MessageEvents) { + hilog.error(0x0000, 'AdjustBrightnessWork', 'Failed to load the content. Cause: %{public}s', + `on message error ${JSON.stringify(event)}`); +} + +/** + * Defines the event handler to be called when an exception occurs during worker execution. + * The event handler is executed in the worker thread. + * + * @param e error message + */ +workerPort.onerror = function (error: ErrorEvent) { + hilog.error(0x0000, 'AdjustBrightnessWork', 'Failed to load the content. Cause: %{public}s', + `on worker error ${JSON.stringify(error)}`); +} \ No newline at end of file diff --git a/entry/src/main/ets/workers/AdjustSaturationWork.ts b/entry/src/main/ets/workers/AdjustSaturationWork.ts new file mode 100644 index 0000000000000000000000000000000000000000..497ee1835bd57a6e837db43a81f087139f6d69bd --- /dev/null +++ b/entry/src/main/ets/workers/AdjustSaturationWork.ts @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License,Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { worker, ThreadWorkerGlobalScope, MessageEvents, ErrorEvent } from '@kit.ArkTS'; +import { adjustSaturation } from '../utils/AdjustUtil'; + +let workerPort: ThreadWorkerGlobalScope = worker.workerPort; + +/** + * Defines the event handler to be called when the worker thread receives a message sent by the host thread. + * The event handler is executed in the worker thread. + * + * @param e message data + */ +workerPort.onmessage = function (event: MessageEvents) { + let bufferArray = event.data.buf; + let last = event.data.last; + let cur = event.data.cur; + let buffer = adjustSaturation(bufferArray, last, cur); + workerPort.postMessage(buffer); +} + +/** + * Defines the event handler to be called when the worker receives a message that cannot be deserialized. + * The event handler is executed in the worker thread. + * + * @param e message data + */ +workerPort.onmessageerror = function (event: MessageEvents) { + hilog.error(0x0000, 'AdjustSaturationWork', 'Failed to load the content. Cause: %{public}s', + `on message error ${JSON.stringify(event)}`); +} + +/** + * Defines the event handler to be called when an exception occurs during worker execution. + * The event handler is executed in the worker thread. + * + * @param e error message + */ +workerPort.onerror = function (error: ErrorEvent) { + hilog.error(0x0000, 'AdjustSaturationWork', 'Failed to load the content. Cause: %{public}s', + `on worker error ${JSON.stringify(error)}`); +} \ No newline at end of file diff --git a/entry/src/main/module.json5 b/entry/src/main/module.json5 new file mode 100644 index 0000000000000000000000000000000000000000..560e0b1c1b375b994ff61fd4168af7797bfc87b8 --- /dev/null +++ b/entry/src/main/module.json5 @@ -0,0 +1,75 @@ +{ + "module": { + "name": "entry", + "type": "entry", + "description": "$string:module_desc", + "mainElement": "EntryAbility", + "deviceTypes": [ + "phone", + "tablet", + "2in1", + "wearable" + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:main_pages", + "abilities": [ + { + "name": "EntryAbility", + "srcEntry": "./ets/entryability/EntryAbility.ets", + "description": "$string:EntryAbility_desc", + "icon": "$media:layered_image", + "label": "$string:EntryAbility_label", + "startWindowIcon": "$media:startIcon", + "startWindowBackground": "$color:start_window_background", + "exported": true, + "skills": [ + { + "entities": [ + "entity.system.home" + ], + "actions": [ + "action.system.home" + ] + } + ] + } + ], + "extensionAbilities": [ + { + "name": "EntryBackupAbility", + "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets", + "type": "backup", + "exported": false, + "metadata": [ + { + "name": "ohos.extension.backup", + "resource": "$profile:backup_config" + } + ], + } + ], + "requestPermissions": [ + { + "name": "ohos.permission.MEDIA_LOCATION", + "reason": "$string:reason", + "usedScene": { + "abilities": [ + "EntryAbility" + ], + "when": "inuse" + } + }, + { + "name": "ohos.permission.WRITE_IMAGEVIDEO", + "reason": "$string:reason", + "usedScene": { + "abilities": [ + "EntryAbility" + ], + "when": "inuse" + } + } + ] + } +} \ No newline at end of file diff --git a/entry/src/main/resources/base/element/color.json b/entry/src/main/resources/base/element/color.json new file mode 100644 index 0000000000000000000000000000000000000000..3c712962da3c2751c2b9ddb53559afcbd2b54a02 --- /dev/null +++ b/entry/src/main/resources/base/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + } + ] +} \ No newline at end of file diff --git a/entry/src/main/resources/base/element/float.json b/entry/src/main/resources/base/element/float.json new file mode 100644 index 0000000000000000000000000000000000000000..4a8a9740066889e7056295def412eaf37c355f51 --- /dev/null +++ b/entry/src/main/resources/base/element/float.json @@ -0,0 +1,76 @@ +{ + "float": [ + { + "name": "title_font_size", + "value": "20fp" + }, + { + "name": "category_font_size", + "value": "16fp" + }, + { + "name": "category_margin_top", + "value": "8vp" + }, + { + "name": "title_image_width", + "value": "24vp" + }, + { + "name": "title_image_height", + "value": "24vp" + }, + { + "name": "crop_image_width", + "value": "48vp" + }, + { + "name": "crop_image_height", + "value": "48vp" + }, + { + "name": "rotate_image_width", + "value": "36vp" + }, + { + "name": "rotate_image_height", + "value": "36vp" + }, + { + "name": "adjust_icon_width", + "value": "20vp" + }, + { + "name": "adjust_icon_height", + "value": "20vp" + }, + { + "name": "adjust_font_size", + "value": "12fp" + }, + { + "name": "adjust_margin_top", + "value": "8vp" + }, + { + "name": "adjust_margin_bottom", + "value": "10vp" + }, + { + "name": "slider_font_size", + "value": "16fp" + }, + { + "name": "slider_margin_top", + "value": "12vp" + }, + { + "name": "title_margin_top", + "value": "15vp" + }, + { + "name": "title_margin_left", + "value": "20vp" + } + ] +} \ No newline at end of file diff --git a/entry/src/main/resources/base/element/string.json b/entry/src/main/resources/base/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..700f4ea8462fd042a6f92c299874f0503a30313a --- /dev/null +++ b/entry/src/main/resources/base/element/string.json @@ -0,0 +1,196 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "ImageEdit" + }, + { + "name": "index_title", + "value": "Image PixelMap Editing" + }, + { + "name": "select_img", + "value": "Select image" + }, + { + "name": "picture_preview", + "value": "Picture preview" + }, + { + "name": "preview", + "value": "Preview" + }, + { + "name": "picture_information", + "value": "Picture information" + }, + { + "name": "reason", + "value": "For saving pictures" + }, + { + "name": "image_edit", + "value": "Image edit" + }, + { + "name": "none", + "value": "none" + }, + { + "name": "grayscale", + "value": "grayscale" + }, + { + "name": "brightness", + "value": "brightness" + }, + { + "name": "invert", + "value": "invert" + }, + { + "name": "blur", + "value": "blur" + }, + { + "name": "customize", + "value": "customize" + }, + { + "name": "horizontal", + "value": "horizontal" + }, + { + "name": "vertical", + "value": "vertical" + }, + { + "name": "crop", + "value": "crop" + }, + { + "name": "mirror", + "value": "mirror" + }, + { + "name": "filter", + "value": "filter" + }, + { + "name": "translate", + "value": "translate" + }, + { + "name": "zoom", + "value": "zoom" + }, + { + "name": "rotate", + "value": "rotate" + }, + { + "name": "adjust", + "value": "adjust" + }, + { + "name": "light", + "value": "light" + }, + { + "name": "transparency", + "value": "transparency" + }, + { + "name": "saturation", + "value": "saturation" + }, + { + "name": "save_image", + "value": "saveImage" + }, + { + "name": "confirm_save", + "value": "are you sure to save image" + }, + { + "name": "save", + "value": "save" + }, + { + "name": "cancel", + "value": "cancel" + }, + { + "name": "confirm", + "value": "confirm" + }, + { + "name": "exec_edit", + "value": "running exec edit." + }, + { + "name": "save_crop", + "value": "Do you want to save the crop?" + }, + { + "name": "watermark", + "value": "watermark" + }, + { + "name": "watermark_text", + "value": "watermark text" + }, + { + "name": "watermark_position", + "value": "position" + }, + { + "name": "watermark_opacity", + "value": "opacity" + }, + { + "name": "watermark_size", + "value": "size" + }, + { + "name": "watermark_color", + "value": "color" + }, + { + "name": "position_top_left", + "value": "top left" + }, + { + "name": "position_top_right", + "value": "top right" + }, + { + "name": "position_bottom_left", + "value": "bottom left" + }, + { + "name": "position_bottom_right", + "value": "bottom right" + }, + { + "name": "position_center", + "value": "center" + }, + { + "name": "add_watermark", + "value": "add watermark" + }, + { + "name": "remove_watermark", + "value": "remove watermark" + } + ] +} \ No newline at end of file diff --git a/entry/src/main/resources/base/media/background.png b/entry/src/main/resources/base/media/background.png new file mode 100644 index 0000000000000000000000000000000000000000..923f2b3f27e915d6871871deea0420eb45ce102f Binary files /dev/null and b/entry/src/main/resources/base/media/background.png differ diff --git a/entry/src/main/resources/base/media/foreground.png b/entry/src/main/resources/base/media/foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..97014d3e10e5ff511409c378cd4255713aecd85f Binary files /dev/null and b/entry/src/main/resources/base/media/foreground.png differ diff --git a/entry/src/main/resources/base/media/ic_adjust.png b/entry/src/main/resources/base/media/ic_adjust.png new file mode 100644 index 0000000000000000000000000000000000000000..0c925713c760486019aebb0700f6c1df5ddb2e36 Binary files /dev/null and b/entry/src/main/resources/base/media/ic_adjust.png differ diff --git a/entry/src/main/resources/base/media/ic_adjust_filled.png b/entry/src/main/resources/base/media/ic_adjust_filled.png new file mode 100644 index 0000000000000000000000000000000000000000..34f56f2a40980db139d5669fcec4fabbf552bce9 Binary files /dev/null and b/entry/src/main/resources/base/media/ic_adjust_filled.png differ diff --git a/entry/src/main/resources/base/media/ic_anti_clockwise.png b/entry/src/main/resources/base/media/ic_anti_clockwise.png new file mode 100644 index 0000000000000000000000000000000000000000..262594ad4b902e5810a1bda18f04ae1c63170b8b Binary files /dev/null and b/entry/src/main/resources/base/media/ic_anti_clockwise.png differ diff --git a/entry/src/main/resources/base/media/ic_brightness.png b/entry/src/main/resources/base/media/ic_brightness.png new file mode 100644 index 0000000000000000000000000000000000000000..79c5ff0f9f4ccc73f7f629f31e3fe1aea7469723 Binary files /dev/null and b/entry/src/main/resources/base/media/ic_brightness.png differ diff --git a/entry/src/main/resources/base/media/ic_brightness_filled.png b/entry/src/main/resources/base/media/ic_brightness_filled.png new file mode 100644 index 0000000000000000000000000000000000000000..41777d105cd8d57c87c2f787bd4da5bb167efcb5 Binary files /dev/null and b/entry/src/main/resources/base/media/ic_brightness_filled.png differ diff --git a/entry/src/main/resources/base/media/ic_clockwise.png b/entry/src/main/resources/base/media/ic_clockwise.png new file mode 100644 index 0000000000000000000000000000000000000000..0b739c4ccea379a18704e849519b74bc685aec6a Binary files /dev/null and b/entry/src/main/resources/base/media/ic_clockwise.png differ diff --git a/entry/src/main/resources/base/media/ic_crop.png b/entry/src/main/resources/base/media/ic_crop.png new file mode 100644 index 0000000000000000000000000000000000000000..cfde5b1a8c02cfd747affa40642cc5d82a1bfc80 Binary files /dev/null and b/entry/src/main/resources/base/media/ic_crop.png differ diff --git a/entry/src/main/resources/base/media/ic_crop_filled.png b/entry/src/main/resources/base/media/ic_crop_filled.png new file mode 100644 index 0000000000000000000000000000000000000000..011533d400576c0ba1f9be222f5a89df71d1d9cc Binary files /dev/null and b/entry/src/main/resources/base/media/ic_crop_filled.png differ diff --git a/entry/src/main/resources/base/media/ic_four2three.png b/entry/src/main/resources/base/media/ic_four2three.png new file mode 100644 index 0000000000000000000000000000000000000000..8a5c7f27f01100535e13585ffd58c6add77bb94d Binary files /dev/null and b/entry/src/main/resources/base/media/ic_four2three.png differ diff --git a/entry/src/main/resources/base/media/ic_four2three_filled.png b/entry/src/main/resources/base/media/ic_four2three_filled.png new file mode 100644 index 0000000000000000000000000000000000000000..f52ebeefdaa8ac7289c787cbcff6e1256981d1bd Binary files /dev/null and b/entry/src/main/resources/base/media/ic_four2three_filled.png differ diff --git a/entry/src/main/resources/base/media/ic_low.jpg b/entry/src/main/resources/base/media/ic_low.jpg new file mode 100644 index 0000000000000000000000000000000000000000..65d34e3b43b750029cb6d66c7cf392aa500a2b90 Binary files /dev/null and b/entry/src/main/resources/base/media/ic_low.jpg differ diff --git a/entry/src/main/resources/base/media/ic_mirror.png b/entry/src/main/resources/base/media/ic_mirror.png new file mode 100644 index 0000000000000000000000000000000000000000..3501fe10910a9c3617be3222009592a850b7710f Binary files /dev/null and b/entry/src/main/resources/base/media/ic_mirror.png differ diff --git a/entry/src/main/resources/base/media/ic_mirror_filled.png b/entry/src/main/resources/base/media/ic_mirror_filled.png new file mode 100644 index 0000000000000000000000000000000000000000..d20d8bf8170478a38ddf3a1eb99a2551845ab18a Binary files /dev/null and b/entry/src/main/resources/base/media/ic_mirror_filled.png differ diff --git a/entry/src/main/resources/base/media/ic_one2one.png b/entry/src/main/resources/base/media/ic_one2one.png new file mode 100644 index 0000000000000000000000000000000000000000..ed831b5419c1cab025b66acad8275e9cb0df4f40 Binary files /dev/null and b/entry/src/main/resources/base/media/ic_one2one.png differ diff --git a/entry/src/main/resources/base/media/ic_one2one_filled.png b/entry/src/main/resources/base/media/ic_one2one_filled.png new file mode 100644 index 0000000000000000000000000000000000000000..1af69bf4201819505459932f8f88312abe9c305b Binary files /dev/null and b/entry/src/main/resources/base/media/ic_one2one_filled.png differ diff --git a/entry/src/main/resources/base/media/ic_original.png b/entry/src/main/resources/base/media/ic_original.png new file mode 100644 index 0000000000000000000000000000000000000000..5a742e402154f9931370e343d29979ef3846b999 Binary files /dev/null and b/entry/src/main/resources/base/media/ic_original.png differ diff --git a/entry/src/main/resources/base/media/ic_original_filled.png b/entry/src/main/resources/base/media/ic_original_filled.png new file mode 100644 index 0000000000000000000000000000000000000000..96a4f39bc742f4ee9a99ef8938bfb6e78395b476 Binary files /dev/null and b/entry/src/main/resources/base/media/ic_original_filled.png differ diff --git a/entry/src/main/resources/base/media/ic_reset.png b/entry/src/main/resources/base/media/ic_reset.png new file mode 100644 index 0000000000000000000000000000000000000000..3ac9af2768caf6d9b79da3bf798d01e153e9820f Binary files /dev/null and b/entry/src/main/resources/base/media/ic_reset.png differ diff --git a/entry/src/main/resources/base/media/ic_reverse_order.png b/entry/src/main/resources/base/media/ic_reverse_order.png new file mode 100644 index 0000000000000000000000000000000000000000..3c79c76991630dd96bf83715ba030e0cc581656c Binary files /dev/null and b/entry/src/main/resources/base/media/ic_reverse_order.png differ diff --git a/entry/src/main/resources/base/media/ic_reverse_order_filled.png b/entry/src/main/resources/base/media/ic_reverse_order_filled.png new file mode 100644 index 0000000000000000000000000000000000000000..fff1c40df7497c1337a94bee66646c061ef14a69 Binary files /dev/null and b/entry/src/main/resources/base/media/ic_reverse_order_filled.png differ diff --git a/entry/src/main/resources/base/media/ic_rotate.png b/entry/src/main/resources/base/media/ic_rotate.png new file mode 100644 index 0000000000000000000000000000000000000000..9620f9967a3b95d0884499e6c71dd49bcbb8684a Binary files /dev/null and b/entry/src/main/resources/base/media/ic_rotate.png differ diff --git a/entry/src/main/resources/base/media/ic_rotate_filled.png b/entry/src/main/resources/base/media/ic_rotate_filled.png new file mode 100644 index 0000000000000000000000000000000000000000..acea9c87a0960d5d2d00ffb8c84e86da4add2d2e Binary files /dev/null and b/entry/src/main/resources/base/media/ic_rotate_filled.png differ diff --git a/entry/src/main/resources/base/media/ic_saturation.png b/entry/src/main/resources/base/media/ic_saturation.png new file mode 100644 index 0000000000000000000000000000000000000000..157bf72c4f3931170252f31c667580a91bb197ee Binary files /dev/null and b/entry/src/main/resources/base/media/ic_saturation.png differ diff --git a/entry/src/main/resources/base/media/ic_saturation_filled.png b/entry/src/main/resources/base/media/ic_saturation_filled.png new file mode 100644 index 0000000000000000000000000000000000000000..ea1aa3f4e29af6a8097bf9b2f9e6d72aed3ce4ff Binary files /dev/null and b/entry/src/main/resources/base/media/ic_saturation_filled.png differ diff --git a/entry/src/main/resources/base/media/ic_save.png b/entry/src/main/resources/base/media/ic_save.png new file mode 100644 index 0000000000000000000000000000000000000000..0f6599c303b99a0e3cf0078d802068b7d5f33255 Binary files /dev/null and b/entry/src/main/resources/base/media/ic_save.png differ diff --git a/entry/src/main/resources/base/media/ic_shutter_photo_svg.svg b/entry/src/main/resources/base/media/ic_shutter_photo_svg.svg new file mode 100644 index 0000000000000000000000000000000000000000..668a0ca88ab1d909fd47592f68c256d5df1b6bde --- /dev/null +++ b/entry/src/main/resources/base/media/ic_shutter_photo_svg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/entry/src/main/resources/base/media/ic_sixteen2nine.png b/entry/src/main/resources/base/media/ic_sixteen2nine.png new file mode 100644 index 0000000000000000000000000000000000000000..c74c7ed34801825647ebaefabb46b75d7dfc9d62 Binary files /dev/null and b/entry/src/main/resources/base/media/ic_sixteen2nine.png differ diff --git a/entry/src/main/resources/base/media/ic_sixteen2nine_filled.png b/entry/src/main/resources/base/media/ic_sixteen2nine_filled.png new file mode 100644 index 0000000000000000000000000000000000000000..6b686b9203ce2290d7210101aa4ef816544c188f Binary files /dev/null and b/entry/src/main/resources/base/media/ic_sixteen2nine_filled.png differ diff --git a/entry/src/main/resources/base/media/ic_transparency.png b/entry/src/main/resources/base/media/ic_transparency.png new file mode 100644 index 0000000000000000000000000000000000000000..c3c52ab3823bf80d8312f416b4f5f5b9c39f5129 Binary files /dev/null and b/entry/src/main/resources/base/media/ic_transparency.png differ diff --git a/entry/src/main/resources/base/media/ic_transparency_filled.png b/entry/src/main/resources/base/media/ic_transparency_filled.png new file mode 100644 index 0000000000000000000000000000000000000000..88b01b99f0af1156d24280be7aa9b7f246c17e3c Binary files /dev/null and b/entry/src/main/resources/base/media/ic_transparency_filled.png differ diff --git a/entry/src/main/resources/base/media/ic_xmark_circle_fill.png b/entry/src/main/resources/base/media/ic_xmark_circle_fill.png new file mode 100644 index 0000000000000000000000000000000000000000..5c9852a339f4ba7d1cb8d9149eaa022dabeb4368 Binary files /dev/null and b/entry/src/main/resources/base/media/ic_xmark_circle_fill.png differ diff --git a/entry/src/main/resources/base/media/layered_image.json b/entry/src/main/resources/base/media/layered_image.json new file mode 100644 index 0000000000000000000000000000000000000000..fb49920440fb4d246c82f9ada275e26123a2136a --- /dev/null +++ b/entry/src/main/resources/base/media/layered_image.json @@ -0,0 +1,7 @@ +{ + "layered-image": + { + "background" : "$media:background", + "foreground" : "$media:foreground" + } +} \ No newline at end of file diff --git a/entry/src/main/resources/base/media/square_portrait_svg.svg b/entry/src/main/resources/base/media/square_portrait_svg.svg new file mode 100644 index 0000000000000000000000000000000000000000..20d71c4b0e149e194a625e1a93fa9dbf5a3b1920 --- /dev/null +++ b/entry/src/main/resources/base/media/square_portrait_svg.svg @@ -0,0 +1,17 @@ + + + Created with Pixso. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/entry/src/main/resources/base/media/startIcon.png b/entry/src/main/resources/base/media/startIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..205ad8b5a8a42e8762fbe4899b8e5e31ce822b8b Binary files /dev/null and b/entry/src/main/resources/base/media/startIcon.png differ diff --git a/entry/src/main/resources/base/profile/backup_config.json b/entry/src/main/resources/base/profile/backup_config.json new file mode 100644 index 0000000000000000000000000000000000000000..78f40ae7c494d71e2482278f359ec790ca73471a --- /dev/null +++ b/entry/src/main/resources/base/profile/backup_config.json @@ -0,0 +1,3 @@ +{ + "allowToBackupRestore": true +} \ No newline at end of file diff --git a/entry/src/main/resources/base/profile/main_pages.json b/entry/src/main/resources/base/profile/main_pages.json new file mode 100644 index 0000000000000000000000000000000000000000..16592128836d16727a55dd4bc90c9ec9b4536885 --- /dev/null +++ b/entry/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "pages/PictureEdit" + ] +} \ No newline at end of file diff --git a/entry/src/main/resources/dark/element/color.json b/entry/src/main/resources/dark/element/color.json new file mode 100644 index 0000000000000000000000000000000000000000..79b11c2747aec33e710fd3a7b2b3c94dd9965499 --- /dev/null +++ b/entry/src/main/resources/dark/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#000000" + } + ] +} \ No newline at end of file diff --git a/entry/src/main/resources/en_US/element/string.json b/entry/src/main/resources/en_US/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..c211272d9e8c70793b828807ca82040fa541db0f --- /dev/null +++ b/entry/src/main/resources/en_US/element/string.json @@ -0,0 +1,204 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "ImageEdit" + }, + { + "name": "index_title", + "value": "Image PixelMap Editing" + }, + { + "name": "select_img", + "value": "Select image" + }, + { + "name": "picture_preview", + "value": "Picture preview" + }, + { + "name": "preview", + "value": "Preview" + }, + { + "name": "picture_information", + "value": "Picture information" + }, + { + "name": "reason", + "value": "For saving pictures" + }, + { + "name": "image_edit", + "value": "Image edit" + }, + { + "name": "none", + "value": "none" + }, + { + "name": "grayscale", + "value": "grayscale" + }, + { + "name": "brightness", + "value": "brightness" + }, + { + "name": "invert", + "value": "invert" + }, + { + "name": "blur", + "value": "blur" + }, + { + "name": "customize", + "value": "customize" + }, + { + "name": "horizontal", + "value": "horizontal" + }, + { + "name": "vertical", + "value": "vertical" + }, + { + "name": "crop", + "value": "crop" + }, + { + "name": "mirror", + "value": "mirror" + }, + { + "name": "filter", + "value": "filter" + }, + { + "name": "translate", + "value": "translate" + }, + { + "name": "zoom", + "value": "zoom" + }, + { + "name": "rotate", + "value": "rotate" + }, + { + "name": "adjust", + "value": "adjust" + }, + { + "name": "light", + "value": "light" + }, + { + "name": "transparency", + "value": "transparency" + }, + { + "name": "saturation", + "value": "saturation" + }, + { + "name": "save_image", + "value": "saveImage" + }, + { + "name": "confirm_save", + "value": "are you sure to save image" + }, + { + "name": "save", + "value": "save" + }, + { + "name": "cancel", + "value": "cancel" + }, + { + "name": "confirm", + "value": "confirm" + }, + { + "name": "exec_edit", + "value": "running exec edit." + }, + { + "name": "save_crop", + "value": "Do you want to save the crop?" + }, + { + "name": "watermark", + "value": "Watermark" + }, + { + "name": "watermark_text", + "value": "Watermark Text" + }, + { + "name": "watermark_position", + "value": "Position" + }, + { + "name": "watermark_opacity", + "value": "Opacity" + }, + { + "name": "watermark_size", + "value": "Size" + }, + { + "name": "watermark_color", + "value": "Color" + }, + { + "name": "position_top_left", + "value": "Top Left" + }, + { + "name": "position_top_right", + "value": "Top Right" + }, + { + "name": "position_bottom_left", + "value": "Bottom Left" + }, + { + "name": "position_bottom_right", + "value": "Bottom Right" + }, + { + "name": "position_center", + "value": "Center" + }, + { + "name": "add_watermark", + "value": "Add Watermark" + }, + { + "name": "remove_watermark", + "value": "Remove Watermark" + }, + { + "name": "watermark_rotation", + "value": "Rotation" + }, + { + "name": "watermark_repeat", + "value": "Repeat Watermark" + } + ] +} \ No newline at end of file diff --git a/entry/src/main/resources/rawfile/low.jpg b/entry/src/main/resources/rawfile/low.jpg new file mode 100644 index 0000000000000000000000000000000000000000..65d34e3b43b750029cb6d66c7cf392aa500a2b90 Binary files /dev/null and b/entry/src/main/resources/rawfile/low.jpg differ diff --git a/entry/src/main/resources/zh_CN/element/string.json b/entry/src/main/resources/zh_CN/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..8d62c76b9bc32f98644c60a986459700edb4253b --- /dev/null +++ b/entry/src/main/resources/zh_CN/element/string.json @@ -0,0 +1,204 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "模块描述" + }, + { + "name": "EntryAbility_desc", + "value": "描述" + }, + { + "name": "EntryAbility_label", + "value": "图片编辑" + }, + { + "name": "index_title", + "value": "图片PixelMap编辑" + }, + { + "name": "select_img", + "value": "选择图片" + }, + { + "name": "picture_preview", + "value": "图片预览" + }, + { + "name": "preview", + "value": "预览" + }, + { + "name": "picture_information", + "value": "图片信息" + }, + { + "name": "reason", + "value": "用于保存图片" + }, + { + "name": "image_edit", + "value": "图片编辑" + }, + { + "name": "none", + "value": "无" + }, + { + "name": "grayscale", + "value": "黑白" + }, + { + "name": "brightness", + "value": "高亮" + }, + { + "name": "invert", + "value": "反转" + }, + { + "name": "blur", + "value": "模糊" + }, + { + "name": "customize", + "value": "自定义" + }, + { + "name": "horizontal", + "value": "横向" + }, + { + "name": "vertical", + "value": "纵向" + }, + { + "name": "crop", + "value": "裁剪" + }, + { + "name": "mirror", + "value": "镜像" + }, + { + "name": "filter", + "value": "滤镜" + }, + { + "name": "translate", + "value": "平移" + }, + { + "name": "zoom", + "value": "缩放" + }, + { + "name": "rotate", + "value": "旋转" + }, + { + "name": "adjust", + "value": "调节" + }, + { + "name": "light", + "value": "亮度" + }, + { + "name": "transparency", + "value": "透明度" + }, + { + "name": "saturation", + "value": "饱和度" + }, + { + "name": "save_image", + "value": "保存图片" + }, + { + "name": "confirm_save", + "value": "确定要保存图片吗" + }, + { + "name": "save", + "value": "保存" + }, + { + "name": "cancel", + "value": "取消" + }, + { + "name": "confirm", + "value": "确认" + }, + { + "name": "exec_edit", + "value": "正在执行编辑" + }, + { + "name": "save_crop", + "value": "是否保存裁剪?" + }, + { + "name": "watermark", + "value": "水印" + }, + { + "name": "watermark_text", + "value": "水印文字" + }, + { + "name": "watermark_position", + "value": "水印位置" + }, + { + "name": "watermark_opacity", + "value": "水印透明度" + }, + { + "name": "watermark_size", + "value": "水印大小" + }, + { + "name": "watermark_color", + "value": "水印颜色" + }, + { + "name": "position_top_left", + "value": "左上" + }, + { + "name": "position_top_right", + "value": "右上" + }, + { + "name": "position_bottom_left", + "value": "左下" + }, + { + "name": "position_bottom_right", + "value": "右下" + }, + { + "name": "position_center", + "value": "居中" + }, + { + "name": "add_watermark", + "value": "添加水印" + }, + { + "name": "remove_watermark", + "value": "移除水印" + }, + { + "name": "watermark_rotation", + "value": "旋转角度" + }, + { + "name": "watermark_repeat", + "value": "重复水印" + } + ] +} \ No newline at end of file diff --git a/entry/src/mock/mock-config.json5 b/entry/src/mock/mock-config.json5 new file mode 100644 index 0000000000000000000000000000000000000000..7a73a41bfdf76d6f793007240d80983a52f15f97 --- /dev/null +++ b/entry/src/mock/mock-config.json5 @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/entry/src/ohosTest/ets/test/Ability.test.ets b/entry/src/ohosTest/ets/test/Ability.test.ets new file mode 100644 index 0000000000000000000000000000000000000000..85c78f67579d6e31b5f5aeea463e216b9b141048 --- /dev/null +++ b/entry/src/ohosTest/ets/test/Ability.test.ets @@ -0,0 +1,35 @@ +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; + +export default function abilityTest() { + describe('ActsAbilityTest', () => { + // Defines a test suite. Two parameters are supported: test suite name and test suite function. + beforeAll(() => { + // Presets an action, which is performed only once before all test cases of the test suite start. + // This API supports only one parameter: preset action function. + }) + beforeEach(() => { + // Presets an action, which is performed before each unit test case starts. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: preset action function. + }) + afterEach(() => { + // Presets a clear action, which is performed after each unit test case ends. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: clear action function. + }) + afterAll(() => { + // Presets a clear action, which is performed after all test cases of the test suite end. + // This API supports only one parameter: clear action function. + }) + it('assertContain', 0, () => { + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. + hilog.info(0x0000, 'testTag', '%{public}s', 'it begin'); + let a = 'abc'; + let b = 'b'; + // Defines a variety of assertion methods, which are used to declare expected boolean conditions. + expect(a).assertContain(b); + expect(a).assertEqual(a); + }) + }) +} \ No newline at end of file diff --git a/entry/src/ohosTest/ets/test/List.test.ets b/entry/src/ohosTest/ets/test/List.test.ets new file mode 100644 index 0000000000000000000000000000000000000000..794c7dc4ed66bd98fa3865e07922906e2fcef545 --- /dev/null +++ b/entry/src/ohosTest/ets/test/List.test.ets @@ -0,0 +1,5 @@ +import abilityTest from './Ability.test'; + +export default function testsuite() { + abilityTest(); +} \ No newline at end of file diff --git a/entry/src/ohosTest/module.json5 b/entry/src/ohosTest/module.json5 new file mode 100644 index 0000000000000000000000000000000000000000..cae0a4a066790141a32a4546bdc6976fa0a4225c --- /dev/null +++ b/entry/src/ohosTest/module.json5 @@ -0,0 +1,14 @@ +{ + "module": { + "name": "entry_test", + "type": "feature", + "deviceTypes": [ + "phone", + "tablet", + "2in1", + "wearable" + ], + "deliveryWithInstall": true, + "installationFree": false + } +} diff --git a/entry/src/test/List.test.ets b/entry/src/test/List.test.ets new file mode 100644 index 0000000000000000000000000000000000000000..bb5b5c3731e283dd507c847560ee59bde477bbc7 --- /dev/null +++ b/entry/src/test/List.test.ets @@ -0,0 +1,5 @@ +import localUnitTest from './LocalUnit.test'; + +export default function testsuite() { + localUnitTest(); +} \ No newline at end of file diff --git a/entry/src/test/LocalUnit.test.ets b/entry/src/test/LocalUnit.test.ets new file mode 100644 index 0000000000000000000000000000000000000000..165fc1615ee8618b4cb6a622f144a9a707eee99f --- /dev/null +++ b/entry/src/test/LocalUnit.test.ets @@ -0,0 +1,33 @@ +import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; + +export default function localUnitTest() { + describe('localUnitTest', () => { + // Defines a test suite. Two parameters are supported: test suite name and test suite function. + beforeAll(() => { + // Presets an action, which is performed only once before all test cases of the test suite start. + // This API supports only one parameter: preset action function. + }); + beforeEach(() => { + // Presets an action, which is performed before each unit test case starts. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: preset action function. + }); + afterEach(() => { + // Presets a clear action, which is performed after each unit test case ends. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: clear action function. + }); + afterAll(() => { + // Presets a clear action, which is performed after all test cases of the test suite end. + // This API supports only one parameter: clear action function. + }); + it('assertContain', 0, () => { + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. + let a = 'abc'; + let b = 'b'; + // Defines a variety of assertion methods, which are used to declare expected boolean conditions. + expect(a).assertContain(b); + expect(a).assertEqual(a); + }); + }); +} \ No newline at end of file diff --git a/hvigor/hvigor-config.json5 b/hvigor/hvigor-config.json5 new file mode 100644 index 0000000000000000000000000000000000000000..85e8d4b2175fc4747650344f025e7d145bc3d361 --- /dev/null +++ b/hvigor/hvigor-config.json5 @@ -0,0 +1,22 @@ +{ + "modelVersion": "5.1.0", + "dependencies": { + }, + "execution": { + // "analyze": "normal", /* Define the build analyze mode. Value: [ "normal" | "advanced" | false ]. Default: "normal" */ + // "daemon": true, /* Enable daemon compilation. Value: [ true | false ]. Default: true */ + // "incremental": true, /* Enable incremental compilation. Value: [ true | false ]. Default: true */ + // "parallel": true, /* Enable parallel compilation. Value: [ true | false ]. Default: true */ + // "typeCheck": false, /* Enable typeCheck. Value: [ true | false ]. Default: false */ + }, + "logging": { + // "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */ + }, + "debugging": { + // "stacktrace": false /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */ + }, + "nodeOptions": { + // "maxOldSpaceSize": 8192 /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process. Default: 8192*/ + // "exposeGC": true /* Enable to trigger garbage collection explicitly. Default: true*/ + } +} diff --git a/hvigorfile.ts b/hvigorfile.ts new file mode 100644 index 0000000000000000000000000000000000000000..f3cb9f1a87a81687554a76283af8df27d8bda775 --- /dev/null +++ b/hvigorfile.ts @@ -0,0 +1,6 @@ +import { appTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins:[] /* Custom plugin to extend the functionality of Hvigor. */ +} diff --git a/oh-package-lock.json5 b/oh-package-lock.json5 new file mode 100644 index 0000000000000000000000000000000000000000..a3925056904d5e0d6791438feddebc0014a84db0 --- /dev/null +++ b/oh-package-lock.json5 @@ -0,0 +1,28 @@ +{ + "meta": { + "stableOrder": true, + "enableUnifiedLockfile": false + }, + "lockfileVersion": 3, + "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", + "specifiers": { + "@ohos/hamock@1.0.0": "@ohos/hamock@1.0.0", + "@ohos/hypium@1.0.21": "@ohos/hypium@1.0.21" + }, + "packages": { + "@ohos/hamock@1.0.0": { + "name": "@ohos/hamock", + "version": "1.0.0", + "integrity": "sha512-K6lDPYc6VkKe6ZBNQa9aoG+ZZMiwqfcR/7yAVFSUGIuOAhPvCJAo9+t1fZnpe0dBRBPxj2bxPPbKh69VuyAtDg==", + "resolved": "https://repo.harmonyos.com/ohpm/@ohos/hamock/-/hamock-1.0.0.har", + "registryType": "ohpm" + }, + "@ohos/hypium@1.0.21": { + "name": "@ohos/hypium", + "version": "1.0.21", + "integrity": "sha512-iyKGMXxE+9PpCkqEwu0VykN/7hNpb+QOeIuHwkmZnxOpI+dFZt6yhPB7k89EgV1MiSK/ieV/hMjr5Z2mWwRfMQ==", + "resolved": "https://repo.harmonyos.com/ohpm/@ohos/hypium/-/hypium-1.0.21.har", + "registryType": "ohpm" + } + } +} \ No newline at end of file diff --git a/oh-package.json5 b/oh-package.json5 new file mode 100644 index 0000000000000000000000000000000000000000..bbe95ba9e19d532fed390c3cc9a0de085c7da773 --- /dev/null +++ b/oh-package.json5 @@ -0,0 +1,10 @@ +{ + "modelVersion": "5.1.0", + "description": "Please describe the basic information.", + "dependencies": { + }, + "devDependencies": { + "@ohos/hypium": "1.0.21", + "@ohos/hamock": "1.0.0" + } +} diff --git a/screenshots/device/dome1.png b/screenshots/device/dome1.png new file mode 100644 index 0000000000000000000000000000000000000000..c213adeda8cd7aa9eb8ad195acbf4d94423b765f Binary files /dev/null and b/screenshots/device/dome1.png differ diff --git a/screenshots/device/dome2.png b/screenshots/device/dome2.png new file mode 100644 index 0000000000000000000000000000000000000000..ebd4e9c0851fe9502fd6100d60807ca11edf038d Binary files /dev/null and b/screenshots/device/dome2.png differ diff --git a/screenshots/device/dome3.png b/screenshots/device/dome3.png new file mode 100644 index 0000000000000000000000000000000000000000..88ddaeee7d73a43c609ff5c86df1cf172e98acfc Binary files /dev/null and b/screenshots/device/dome3.png differ diff --git a/screenshots/device/dome4.png b/screenshots/device/dome4.png new file mode 100644 index 0000000000000000000000000000000000000000..47477c17298a005405ebbdc2c3f94bbe5c3afc7c Binary files /dev/null and b/screenshots/device/dome4.png differ diff --git a/screenshots/device/dome5.png b/screenshots/device/dome5.png new file mode 100644 index 0000000000000000000000000000000000000000..0f77af830146ba82cfa2e590eec49350df3f8340 Binary files /dev/null and b/screenshots/device/dome5.png differ diff --git a/screenshots/device/dome6.png b/screenshots/device/dome6.png new file mode 100644 index 0000000000000000000000000000000000000000..93aeb6d8f6b4dd4ce2d016a1f23eca7f5a519a0a Binary files /dev/null and b/screenshots/device/dome6.png differ