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..1aedd80cea411b51f39ee65f6ce3fe81833119b6 --- /dev/null +++ b/AppScope/app.json5 @@ -0,0 +1,10 @@ +{ + "app": { + "bundleName": "com.example.imageCanvasEdit", + "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..4746e7b205d9be109078d7b28dabe920f9a00eba --- /dev/null +++ b/AppScope/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "ImageCanvasEdit" + } + ] +} 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/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..338e5b0bc22082e0ffcc7121c2ed3897a3ddccb0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,78 @@ + Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved. + + 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. + +Apache License, Version 2.0 +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: +1.You must give any other recipients of the Work or Derivative Works a copy of this License; and +2.You must cause any modified files to carry prominent notices stating that You changed the files; and +3.You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and +4.If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/README.en.md b/README.en.md index f3bb16ff982943076dd468e2380da1413e044d7c..3cf064b94ee08963820c4c20b29d2abe635db397 100644 --- a/README.en.md +++ b/README.en.md @@ -1,36 +1,88 @@ -# 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/device/dome1.png) | ![dome2.png](screenshots/device/dome2.png) | ![dome3.png](screenshots/device/dome3.png) | +| ---------------------------------------------- | ---------------------------------------------- | ---------------------------------------------- | -#### Installation +### Concepts -1. xxxx -2. xxxx -3. xxxx +- 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: Image encoding refers to encoding PixelMap into archived images in different formats for subsequent processing, such as saving and transferring. +- Canvas: a canvas component for custom drawing of graphics, images, and other content. +- OffscreenCanvas: an off-screen canvas for background image processing and rendering. -#### Instructions +### Features -1. xxxx -2. xxxx -3. xxxx +- **Original image information**: View decoded original image data +- **Basic Editing**: Crop, rotate, mirror +- **Image Adjustment**: Brightness, transparency, saturation adjustment +- **Image Transformation**: Translation, scaling +- **Filter Effects**: Various filters such as black and white, blur, and highlight +- **Watermark**: Support for adding text +- **Real-time Preview**: All editing operations support real-time preview +- **Save Function**: Support for saving edited images -#### Contribution +### 工程目录 +``` +├──ets +│ ├──common +│ │ └──CommonConstants.ets // constant +│ ├──component +│ │ ├──TabComponent.ets // Bottom tab bar component +│ │ └──TitleComponent.ets // Top title bar components +│ ├──dialog +│ │ ├──MyCustomDialog.ets // Customize the pop-up box +│ │ └──SavePicker.ets // Save pop-up +│ ├──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 +``` -1. Fork the repository -2. Create Feat_xxx branch -3. Commit your code -4. Create Pull Request +### 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 + - **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 +3. Preview editing effects in real-time. +4. Tap the save button to save the edited image. -#### Gitee Feature +### Permissions -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/) +- Allows applications to access geographic location information in user media files:ohos.permission.MEDIA_LOCATION +- Allows applications to read and write media file information in external storage:ohos.permission.WRITE_MEDIA + +### Constraints + +1. The sample is only supported on Huawei phones with standard systems. +2. HarmonyOS: HarmonyOS 5.1.0 Release or later. +3. DevEco Studio: DevEco Studio 5.1.0 Release or later. +4. HarmonyOS SDK: HarmonyOS 5.1.0 Release SDK or later. \ No newline at end of file diff --git a/README.md b/README.md index 759781263216184471e034203841fe9cce606bdb..cf9773b044fcda0b935765748675c72fedc59cac 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,90 @@ -# ImageCanvasEdit +# 基于图片编解码实现图片编辑功能 -#### 介绍 -构建Surface输入输出的图片编辑 +基于图片编解码,实现图片编辑,包含裁剪、旋转、色域调节(本章介绍亮度、镜像、透明度、饱和度)等功能。 -#### 软件架构 -软件架构说明 +| ![dome1.png](screenshots/device/dome1.png) | ![dome2.png](screenshots/device/dome2.png) | ![dome3.png](screenshots/device/dome3.png) | +|--------------------------------------------| ---------------------------------------------- | ---------------------------------------------- | +### 相关概念 -#### 安装教程 +- 图片解码:图片解码指将所支持格式的存档图片解码成统一的PixelMap,以便在应用或系统中进行图片显示或图片处理。 +- PixelMap:图像像素类,用于读取或写入图像数据以及获取图像信息。 +- 图片编码:图片编码指将PixelMap编码成不同格式的存档图片,用于后续处理,如保存、传输等。 +- Canvas:画布组件,用于自定义绘制图形、图片等内容。 +- OffscreenCanvas:离屏画布,用于在后台进行图像处理和渲染。 -1. xxxx -2. xxxx -3. xxxx +### 功能特性 -#### 使用说明 +- **原图信息**:查看解码原图数据 +- **基础编辑**:裁剪、旋转、镜像 +- **图像调节**:亮度、透明度、饱和度调节 +- **图像变换**:平移、缩放 +- **滤镜效果**:黑白、模糊、高亮等多种滤镜 +- **水印功能**:支持添加文字 +- **实时预览**:所有编辑操作支持实时预览 +- **保存功能**:支持保存编辑后的图片 -1. xxxx -2. xxxx -3. xxxx +### 工程目录 +``` +├──ets +│ ├──common +│ │ └──CommonConstants.ets // 常量 +│ ├──component +│ │ ├──TabComponent.ets // 底部Tab栏组件 +│ │ └──TitleComponent.ets // 顶部标题栏组件 +│ ├──dialog +│ │ ├──MyCustomDialog.ets // 自定义弹出框 +│ │ └──SavePicker.ets // 保存弹出框 +│ ├──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 // 操作选项枚举 +│ │ ├──PictureEditModel.ets // 首页列模型 +│ │ ├──PictureEditModelData.ets // 首页数据类 +│ │ └──RegionItem.ets // 区域项模型 +│ └──workers +│ ├──AdjustBrightnessWork.ets // 亮度调节工作线程 +│ └──AdjustSaturationWork.ets // 饱和度调节工作线程 +└──resources +``` -#### 参与贡献 +### 使用说明 -1. Fork 本仓库 -2. 新建 Feat_xxx 分支 -3. 提交代码 -4. 新建 Pull Request +1. 启动应用后选择要编辑的图片。 +2. 使用底部标签页切换不同的编辑功能: + - **裁剪**:支持多种比例裁剪 + - **调节**:调整图片亮度、透明度、饱和度 + - **滤镜**:应用各种滤镜效果 + - **平移**:水平和垂直移动图片 + - **缩放**:放大或缩小图片 + - **水印**:添加文字 +3. 实时预览编辑效果。 +4. 点击保存按钮保存编辑后的图片。 +### 相关权限 -#### 特技 +- 允许应用访问用户媒体文件中的地理位置信息:ohos.permission.MEDIA_LOCATION +- 允许应用读写用户外部存储中的媒体文件信息: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. HarmonyOS系统:HarmonyOS 5.1.0 Release及以上。 +3. DevEco Studio版本:DevEco Studio 5.1.0 Release及以上。 +4. HarmonyOS SDK版本:HarmonyOS 5.1.0 Release SDK及以上。 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/CommonConstants.ts b/entry/src/main/ets/common/CommonConstants.ts new file mode 100644 index 0000000000000000000000000000000000000000..2aeb741fc879b7ebc9a0920422cdd5f78813e9e1 --- /dev/null +++ b/entry/src/main/ets/common/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/component/TabComponent.ets b/entry/src/main/ets/component/TabComponent.ets new file mode 100644 index 0000000000000000000000000000000000000000..87656d267c6460d44315b0cb8da46c868daa879a --- /dev/null +++ b/entry/src/main/ets/component/TabComponent.ets @@ -0,0 +1,578 @@ +/* + * 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 { hilog } from "@kit.PerformanceAnalysisKit"; +import { WatermarkView } from "../view/WatermarkView"; +import { MyCustomDialog } from "../dialog/MyCustomDialog"; +import { CommonConstants } from "../common/CommonConstants"; +import { IconStatus } from "../viewModel/IconListViewModel"; +import { banner, rectangle, square } from "../utils/CropUtil"; +import { ImageDisplaySize, TranslateListType } from "../viewModel/PictureEditModel"; +import { CropType, MainTabId, MirrorType } from "../viewModel/OptionViewModel"; +import ApplyFilterView from "../view/ApplyFilterView"; +import AdjustContentView from "../view/AdjustContentView"; + +const DOMAIN: number = 0XFF; +const TAG: string = 'ContentComponent'; + +@CustomDialog +struct AdjustCustomDialog { + controller?: CustomDialogController; + cancel: () => void = () => { + }; + confirm: () => void = () => { + }; + + build() { + Column() { + Text($r('app.string.save_adjust')) + .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(24) + .borderRadius(32) + .backgroundColor(Color.White) + } +} + +@Component +export struct TabComponent { + @Prop displaySize: ImageDisplaySize; + @Link containerWidth: number; + @Link containerHeight: number; + @Link cropList: TranslateListType[]; + @Link currentCropIndex: number; + @Link currentIndex: number; + @Link currentTranslateData: number[]; + @Link currentTranslateIndex: number; + @Link translateList: TranslateListType[]; + @Link currentZoom: number; + @Link waterShow: boolean; + @Link canvasOffsetX: number; + @Link canvasOffsetY: number; + @Consume pixelMap: image.PixelMap; + @Consume('pixelMapInit') pixelMapInit?: image.PixelMap; + @Consume cropPixelMap: image.PixelMap; + @Consume adjustPixelMap: image.PixelMap; + @Consume isInitTranslateValue: boolean; + triggerCanvasRedraw: () => void = () => { + }; + flushPixelMapChange: () => void = () => { + }; + initEffectKit: () => void = () => { + }; + drawTranslate: (x: number, y: number) => void = (x, y) => { + } + private controller: TabsController = new TabsController(); + private menuIconChangeList = [ + 'crop_rotate', + 'slider_horizontal_2', + 'camera_filters', + 'square_portrait_svg', + 'checkered_magnifyingglass', + 'shutter_photo' + ]; + private offsetX: number = 0; + private offsetY: number = 0; + dialogController: CustomDialogController = new CustomDialogController({ + builder: MyCustomDialog({ + cancel: () => { + this.onCancel() + }, + confirm: () => { + this.onConfirm() + } + }), + autoCancel: false, + alignment: DialogAlignment.Center, + customStyle: true + }); + adjustDialogController: CustomDialogController = new CustomDialogController({ + builder: AdjustCustomDialog({ + cancel: () => { + this.onCancelAdjust() + }, + confirm: () => { + this.adjustDialogController.close() + } + }), + autoCancel: false, + alignment: DialogAlignment.Center, + customStyle: true + }); + + onCancel() { + this.cropImage(0); + this.pixelMap = this.cropPixelMap; + this.dialogController.close(); + this.currentCropIndex = 0; + } + + onCancelAdjust() { + this.pixelMap = this.adjustPixelMap; + this.cropPixelMap = this.adjustPixelMap; + this.adjustDialogController.close(); + this.flushPixelMapChange(); + } + + onConfirm() { + this.dialogController.close(); + this.currentCropIndex = 0; + } + + rotateImage() { + if (!this.pixelMap) { + return; + } + try { + this.pixelMap.rotate(CommonConstants.ANTI_CLOCK) + .then(() => { + this.flushPixelMapChange(); + }) + } catch (error) { + hilog.error(DOMAIN, TAG, '%{public}s, %{public}s', `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) { + hilog.error(DOMAIN, TAG, '%{public}s, %{public}s', `there is a error in rotate process with ${error?.code}`); + } + } + } + + // Crop image + async cropImage(proportion: CropType) { + if (!this.pixelMap) { + return; + } + if (!this.cropPixelMap) { + return; + } + this.pixelMap = await effectKit.createEffect(this.cropPixelMap).getEffectPixelMap(); + + try { + const imageInfo = await this.pixelMap.getImageInfo(); + const size = imageInfo.size; + const imageWidth = size?.width; + const imageHeight = size?.height; + + if (!imageWidth || !imageHeight) { + hilog.error(DOMAIN, TAG, '%{public}s', 'Invalid image dimensions'); + return; + } + + switch (proportion) { + case CropType.ORIGINAL_IMAGE: + if (this.cropPixelMap) { + this.pixelMap = await effectKit.createEffect(this.cropPixelMap).getEffectPixelMap(); + } + break; + case CropType.SQUARE: + await square(this.pixelMap, imageWidth, imageHeight); + break; + case CropType.BANNER: + await banner(this.pixelMap, imageWidth, imageHeight); + break; + case CropType.RECTANGLE: + await rectangle(this.pixelMap, imageWidth, imageHeight); + break; + default: + break; + } + + this.flushPixelMapChange(); + } catch (error) { + hilog.error(DOMAIN, TAG, '%{public}s, %{public}s', 'Error in crop operation:', JSON.stringify(error)); + } + } + + sliderChange(value: number, mode: SliderChangeMode) { + if ((mode === SliderChangeMode.End) && (value !== this.currentTranslateData[this.currentTranslateIndex])) { + if (this.isInitTranslateValue) { + this.offsetX = 0; + this.offsetY = 0; + } + this.currentTranslateData[this.currentTranslateIndex] = Math.round(value); + if (this.currentTranslateIndex === 0) { + this.offsetX = (this.displaySize.width + this.displaySize.x) * (value / 1000); + } else if (this.currentTranslateIndex === 1) { + this.offsetY = this.displaySize.height * (value / 1000); + } + this.drawTranslate(this.offsetX, this.offsetY); + this.isInitTranslateValue = false; + } + } + + 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, image.AntiAliasingLevel.LOW); + } + + this.currentZoom = value; + this.flushPixelMapChange(); + + hilog.info(DOMAIN, TAG, '%{public}s', `Zoom applied: ${value}%`); + } catch (error) { + hilog.error(DOMAIN, TAG, '%{public}s, %{public}s', 'Zoom operation failed:', JSON.stringify(error)); + } + } + + @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) + } + .textAlign(TextAlign.Center) + .width(CommonConstants.TAB_MENU_WIDTH) + .height(CommonConstants.TAB_MENU_WIDTH) + } + + Text(name) + .fontColor(this.currentIndex === index ? '#5291FF' : Color.White) + .fontSize(10) + .margin({ top: 2 }) + } + .alignItems(HorizontalAlign.Center) + .justifyContent(FlexAlign.Center) + .width(70) + .padding({ left: 4, right: 4 }) + } + + @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%') + } + + build() { + Column() { + Tabs({ barPosition: BarPosition.End, controller: this.controller }) { + 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) + .justifyContent(FlexAlign.SpaceBetween) + } + .padding({ bottom: 16 }) + .tabBar(this.TabBuilderMenu(MainTabId.CROP, $r('app.string.crop'))) + + TabContent() { + AdjustContentView().width('100%').height('100%') + } + .padding({ bottom: 16 }) + .tabBar(this.TabBuilderMenu(MainTabId.ADJUST, $r('app.string.adjust'))) + .clip(false) + + TabContent() { + ApplyFilterView() + .margin({ top: 60 }).width('100%').height('100%') + } + .padding({ bottom: 16 }) + .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: 20, 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.SpaceBetween) + .width(CommonConstants.LAYOUT_FULL_SCREEN) + .height('100%') + } + .width('100%') + .height('100%') + .padding({ bottom: 16 }) + .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(`current zoom: ${this.currentZoom}%`) + .fontColor('#FFF') + .fontSize(14) + .margin({ top: 10 }) + } + .justifyContent(FlexAlign.Start) + .width(CommonConstants.LAYOUT_FULL_SCREEN) + .height('100%') + } + .width('100%') + .height('100%') + .padding({ bottom: 16 }) + .clip(false) + .tabBar(this.TabBuilderMenu(MainTabId.ZOOM, $r('app.string.zoom'))) + + TabContent() { + } + .padding({ bottom: 16 }) + .clip(false) + .tabBar(this.TabBuilderMenu(5, $r('app.string.watermark'))) + } + .vertical(false) + .barMode(BarMode.Scrollable) + .barHeight('auto') + .width('100%') + .animationDuration(400) + .fadingEdge(false) + .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; + } + }) + } + .width('100%') + .height('100%') + } +} \ No newline at end of file diff --git a/entry/src/main/ets/component/TitleComponent.ets b/entry/src/main/ets/component/TitleComponent.ets new file mode 100644 index 0000000000000000000000000000000000000000..83e341311f9a119cbba35d155928e2e8ece7f49d --- /dev/null +++ b/entry/src/main/ets/component/TitleComponent.ets @@ -0,0 +1,178 @@ +/* + * 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 { hilog } from '@kit.PerformanceAnalysisKit'; +import { encode } from '../utils/EncodeUtil'; +import { SavePicker } from '../dialog/SavePicker'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { photoAccessHelper } from '@kit.MediaLibraryKit'; +import { fileIo } from '@kit.CoreFileKit'; +import Logger from '../utils/LoggerUtil'; + +const DOMAIN: number = 0XFF; +const TAG: string = 'TitleComponent'; + +@Component +export struct TitleComponent { + @Link isShow: boolean; + @Link canvasOffsetX: number; + @Link canvasOffsetY: number; + @Consume pixelMap: image.PixelMap + @Consume filterCurrIndex: number; + @Consume filterPixelMap: image.PixelMap; + pixelInit: () => void = () => { + } + dialogSaveController: CustomDialogController | null = new CustomDialogController({ + builder: SavePicker({ + cancel: () => { + this.CancelSave() + }, + confirm: () => { + this.dialogSave() + this.dialogSaveController?.close(); + } + }) + }) + private context: Context = this.getUIContext().getHostContext()!; + + CancelSave() { + this.dialogSaveController?.close(); + } + + async dialogSave() { + const imageToSave = await this.createTranslatedPixelMap(); + if (imageToSave) { + encode(imageToSave); + } + } + + 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) { + hilog.error(DOMAIN, TAG, '%{public}s, %{public}s', 'Failed to create translated pixelMap:', + JSON.stringify(error)); + const imageToSave = (this.filterCurrIndex > 0 && this.filterPixelMap) ? this.filterPixelMap : this.pixelMap; + return imageToSave; + } + } + + build() { + 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) + .height(56) + + 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(() => { + this.dialogSaveController?.open(); + }) + } + .padding({ right: 16 }) + .height(56) + } + .width('100%') + } +} \ No newline at end of file diff --git a/entry/src/main/ets/dialog/MyCustomDialog.ets b/entry/src/main/ets/dialog/MyCustomDialog.ets new file mode 100644 index 0000000000000000000000000000000000000000..fcaf91f60c23c63f7bdfec2d39005617404de956 --- /dev/null +++ b/entry/src/main/ets/dialog/MyCustomDialog.ets @@ -0,0 +1,67 @@ +/* + * 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. + */ + +@CustomDialog +@Component +export 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) + } +} \ No newline at end of file diff --git a/entry/src/main/ets/dialog/SavePicker.ets b/entry/src/main/ets/dialog/SavePicker.ets new file mode 100644 index 0000000000000000000000000000000000000000..1b31abe5dd571be43b3406ecfd2765368ea4d9c3 --- /dev/null +++ b/entry/src/main/ets/dialog/SavePicker.ets @@ -0,0 +1,74 @@ +/* + * 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. + */ + +@CustomDialog +@Component +export struct SavePicker { + controller?: CustomDialogController; + cancel: () => void = () => { + }; + confirm: () => void = () => { + }; + + build() { + Column() { + Text($r('app.string.save_image')) + .textAlign(TextAlign.Center) + .fontSize(18) + .fontColor('#000000') + .fontSize(20) + .height(56) + .fontWeight(FontWeight.Bold) + + Text($r('app.string.confirm_save')) + .textAlign(TextAlign.Center) + .fontSize(16) + .fontColor('rgba(0, 0, 0, 0.9)') + .lineHeight('21vp') + .fontWeight(500) + + Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { + Button($r('app.string.cancel'), + { buttonStyle: ButtonStyleMode.TEXTUAL, role: ButtonRole.NORMAL, stateEffect: false }) + .fontColor('#0A59F7') + .fontSize(16) + .fontWeight(500) + .onClick(() => { + this.cancel() + }) + Divider() + .vertical(true) + .height(22) + .color('#33000000') + .opacity(0.6) + .margin({ left: 50, right: 50 }) + Button($r('app.string.save'), + { buttonStyle: ButtonStyleMode.TEXTUAL, role: ButtonRole.NORMAL, stateEffect: false }) + .fontSize(16) + .fontColor('#0A59F7') + .fontWeight(500) + .onClick(() => { + this.confirm(); + }) + + } + .height('40vp') + .margin({ top: '8vp', bottom: 16 }) + } + .backgroundColor('#0C182431') + .borderRadius('32vp') + .padding({ left: 24, right: 24 }) + } +} \ 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..97f56b5ffdfab43313dc5618d904e9e24dd38521 --- /dev/null +++ b/entry/src/main/ets/entryability/EntryAbility.ets @@ -0,0 +1,115 @@ +/* + * 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 { ContextStorageManager } from '../utils/ContextStorageUtil' + +const DOMAIN = 0xff00 +const TAG: string = 'EntryAbility' + +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, TAG, '%{public}s', 'Ability onCreate') + } + + onDestroy(): void { + hilog.info(DOMAIN, TAG, '%{public}s', 'Ability onDestroy') + } + + onWindowStageCreate(windowStage: window.WindowStage): void { + // Main window is created, set main page for this ability + hilog.info(DOMAIN, TAG, '%{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( + DOMAIN, + TAG, + 'Failed to requestPermission. Cause: %{public}s', + JSON.stringify(err) ?? '' + ) + } else { + hilog.info( + DOMAIN, + TAG, + 'Succeeded in requestPermission. Data: %{public}s', + JSON.stringify(data) ?? '' + ) + } + } + ) + + windowStage.getMainWindow((_err, windowClass) => { + windowClass.setWindowLayoutFullScreen(false) + let SystemBarProperties: window.SystemBarProperties = { + statusBarColor: '#000000', + statusBarContentColor: '#FFFFFF', + } + windowClass.setWindowSystemBarProperties(SystemBarProperties) + }) + + windowStage.loadContent('pages/PictureEdit', (err) => { + if (err.code) { + hilog.error( + DOMAIN, + TAG, + 'Failed to load the content. Cause: %{public}s', + JSON.stringify(err) + ) + return + } + let uiContext = windowStage.getMainWindowSync().getUIContext() + ContextStorageManager.getInstance().init(uiContext) + + hilog.info(DOMAIN, TAG, 'Succeeded in loading the content.') + }) + } + + onWindowStageDestroy(): void { + // Main window is destroyed, release UI related resources + hilog.info(DOMAIN, TAG, '%{public}s', 'Ability onWindowStageDestroy') + } + + onForeground(): void { + // Ability has brought to foreground + hilog.info(DOMAIN, TAG, '%{public}s', 'Ability onForeground') + } + + onBackground(): void { + // Ability has back to background + hilog.info(DOMAIN, TAG, '%{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..e8652709046cc2b0b87941c1c7373db0934521fd --- /dev/null +++ b/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets @@ -0,0 +1,32 @@ +/* + * 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 = 0xFF00; +const TAG: string = 'EntryBackupAbility'; + +export default class EntryBackupAbility extends BackupExtensionAbility { + async onBackup() { + hilog.info(DOMAIN, TAG, 'onBackup ok'); + await Promise.resolve(); + } + + async onRestore(bundleVersion: BundleVersion) { + hilog.info(DOMAIN, TAG, '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..3f1774b9f17cf32a80b76cd5facb7809f3ebd53f --- /dev/null +++ b/entry/src/main/ets/pages/PictureEdit.ets @@ -0,0 +1,566 @@ +/* + * 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 { hilog } from '@kit.PerformanceAnalysisKit'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { IconStatus } from '../viewModel/IconListViewModel'; +import { CommonConstants } from '../common/CommonConstants'; +import { TitleComponent } from '../component/TitleComponent'; +import { TabComponent } from '../component/TabComponent'; +import { getPixelMap, getResourceFd } from '../utils/DecodeUtil'; +import { ContextStorageManager } from '../utils/ContextStorageUtil'; +import { cropIconChangeList } from '../viewModel/IconListViewModel'; +import { cropListData, translateListData } from '../viewModel/PictureEditModelData'; +import { ImageDisplaySize, InitDataArrType, ParamsType, TranslateListType } from '../viewModel/PictureEditModel'; +import { display } from '@kit.ArkUI'; + +const DOMAIN = 0xFF00; +const TAG: string = 'PictureEdit'; + +@Entry +@Component +struct PictureEdit { + uiContext: UIContext = ContextStorageManager.getInstance().getContext(); + uri: string = ''; + imageSource?: image.ImageSource; + @StorageProp('imageInfoArr') imageInfoArr: InitDataArrType[] = []; + @Provide currentIndex: number = 0; + @Provide currentCropIndex: number = 0; + @State currentTranslateData: number[] = [0, 0]; + @State currentTranslateIndex: number = 0; + @State currentZoom: number = 100; + @State cropList: TranslateListType[] = cropListData; + @State translateList: TranslateListType[] = translateListData; + @State isShow: boolean = false; + @State waterShow: boolean = false; + @State dividerList: number[] = [0, 1, 2, 3, 4, 5]; + @Provide('pixelMap') pixelMap?: image.PixelMap = undefined; + @Provide('pixelMapInit') pixelMapInit?: image.PixelMap = undefined; + @Provide filterPixelMap?: image.PixelMap = undefined; + @Provide cropPixelMap?: image.PixelMap = undefined; + @Provide adjustPixelMap?: image.PixelMap = undefined; + @Provide filterCurrIndex: number = 0; + @Provide isInitTranslateValue: boolean = false; + @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; + 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; + @State displaySize: ImageDisplaySize | null = null; + @State imageToRender: image.PixelMap | undefined = undefined; + private redrawRequested: boolean = false + @State refreshRate: number = 0; + + aboutToAppear() { + this.getRefreshRate(); + try { + const params = this.uiContext.getRouter().getParams() as ParamsType; + if (params && params.selectUris) { + this.uri = params.selectUris; + hilog.info(DOMAIN, TAG, '%{public}s, %{public}s', 'Got URI from router params:', this.uri); + ContextStorageManager.getInstance().initUri(this.uri); + } + } catch (error) { + hilog.error(DOMAIN, TAG, '%{public}s, %{public}s', 'Failed to get router params:', JSON.stringify(error)); + } + + this.pixelInit(); + this.initData(); + } + + getRefreshRate() { + const refreshRateNow = display.getDefaultDisplaySync().refreshRate; + this.refreshRate = Math.floor(1000 / refreshRateNow); + } + + async initData() { + try { + this.imageSource = image.createImageSource(await getResourceFd(this.uri)); + + + this.imageSource.getImageInfo((err, imageInfo) => { + if (err) { + hilog.error(DOMAIN, TAG, '%{public}s, %{public}s', 'Failed to get image info:', JSON.stringify(err)); + return; + } + this.imageInfo = imageInfo; + this.imageWidth = imageInfo.size.width; + this.imageHeight = imageInfo.size.height; + this.imageLoaded = true; + hilog.info(DOMAIN, TAG, '%{public}s', `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) => { + hilog.info(DOMAIN, TAG, '%{public}s, %{public}s', 'eventLog uiContext EXIF data:', JSON.stringify(result)); + Object.keys(result).forEach((key) => { + this.imageInfoArr.push({ + label: key as '', + value: result[key] + }) + }); + }).catch((error: BusinessError) => { + hilog.error(DOMAIN, TAG, '%{public}s, %{public}s', + 'Failed to get the value of the specified attribute key of the image.', JSON.stringify(error)); + }); + AppStorage.setOrCreate('imageInfoArr', this.imageInfoArr) + } catch (error) { + hilog.error(DOMAIN, TAG, '%{public}s, %{public}s', 'Error in initData:', JSON.stringify(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 + }; + } + + // Top reset + async pixelInit() { + try { + let imageSource = image.createImageSource(await getResourceFd(this.uri)); + this.pixelMap = this.pixelMapInit = await getPixelMap(imageSource); + + + 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(); + this.isInitTranslateValue = true; + } catch (error) { + hilog.error(DOMAIN, TAG, '%{public}s, %{public}s', 'Error in pixelInit:', JSON.stringify(error)); + } + } + + async initEffectKit() { + try { + if (this.pixelMap) { + this.cropPixelMap = await effectKit.createEffect(this.pixelMap).getEffectPixelMap(); + + this.currentZoom = 100; + this.canvasOffsetX = 0; + this.canvasOffsetY = 0; + hilog.info(DOMAIN, TAG, '%{public}s', 'Effect kit initialized successfully'); + } + } catch (error) { + hilog.error(DOMAIN, TAG, '%{public}s, %{public}s', 'Error initializing effect kit:', JSON.stringify(error)); + } + } + + flushPixelMapChange() { + this.isPixelMapChange = !this.isPixelMapChange; + setTimeout(() => { + this.triggerCanvasRedraw(); + }, this.refreshRate) + } + + flushPixelMap() { + const temp = this.pixelMap; + this.pixelMap = undefined; + this.pixelMap = temp; + this.triggerCanvasRedraw(); + } + + // Canvas drawing + 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; + + this.displaySize = this.calculateImageDisplaySize(); + + if (this.displaySize.width > 0 && this.displaySize.height > 0) { + this.imageToRender = (this.filterCurrIndex > 0 && this.filterPixelMap) ? this.filterPixelMap : this.pixelMap; + + const finalX = this.displaySize.x + this.canvasOffsetX; + const finalY = this.displaySize.y + this.canvasOffsetY; + + this.canvasRenderingContext.drawImage( + this.imageToRender, + finalX, + finalY, + this.displaySize.width, + this.displaySize.height + ); + } + }); + } catch (error) { + hilog.error(DOMAIN, TAG, '%{public}s, %{public}s', 'Canvas drawing failed:', JSON.stringify(error)); + } + } + + drawTranslate(x: number, y: number) { + this.canvasRenderingContext.clearRect(0, 0, this.containerWidth, this.containerHeight); + this.displaySize = this.calculateImageDisplaySize(); + const finalX = this.displaySize!.x + this.canvasOffsetX + x; + const finalY = this.displaySize!.y + this.canvasOffsetY + y; + this.canvasRenderingContext.drawImage( + this.pixelMapInit, + finalX, + finalY, + this.displaySize!.width, + this.displaySize!.height + ); + this.pixelMap = this.canvasRenderingContext.getPixelMap(0, 0, this.containerWidth, this.containerHeight); + + if (this.filterCurrIndex > 0) { + this.filterPixelMap = this.pixelMap; + } + } + + triggerCanvasRedraw() { + if (this.canvasReady && this.imageLoaded && this.pixelMap) { + if (!this.redrawRequested) { + this.redrawRequested = true + setTimeout(() => { + this.drawImageOnCanvas() + this.redrawRequested = false + }, this.refreshRate) + } + } + } + + @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%') + } + + build() { + RelativeContainer() { + Column() { + TitleComponent({ + isShow: this.isShow, + canvasOffsetX: this.canvasOffsetX, + canvasOffsetY: this.canvasOffsetY, + pixelInit: () => { + this.pixelInit() + } + }) + .width('100%') + + Column() { + Column() { + if (this.currentIndex > 2) { + if (this.filterCurrIndex) { + Canvas(this.canvasRenderingContext) + .width('100%') + .height('100%') + .backgroundColor(Color.Transparent) + .onReady(() => { + hilog.info(DOMAIN, TAG, '%{public}s', 'Canvas ready'); + this.canvasReady = true; + this.triggerCanvasRedraw(); + + }) + .onAreaChange((oldValue: Area, newValue: Area) => { + this.containerWidth = Number(newValue.width); + this.containerHeight = Number(newValue.height); + hilog.info(DOMAIN, TAG, '%{public}s', + `Canvas size changed: ${this.containerWidth} x ${this.containerHeight}`); + this.triggerCanvasRedraw(); + }) + .opacity(this.filterCurrIndex ? 1 : 0) + .transition(TransitionEffect.OPACITY.animation({ duration: 400 })) + } else { + Canvas(this.canvasRenderingContext) + .width('100%') + .height('100%') + .backgroundColor(Color.Transparent) + .onReady(() => { + hilog.info(DOMAIN, TAG, '%{public}s', 'Canvas ready'); + this.canvasReady = true; + this.triggerCanvasRedraw(); + + }) + .onAreaChange((oldValue: Area, newValue: Area) => { + this.containerWidth = Number(newValue.width); + this.containerHeight = Number(newValue.height); + hilog.info(DOMAIN, TAG, '%{public}s', + `Canvas size changed: ${this.containerWidth} x ${this.containerHeight}`); + this.triggerCanvasRedraw(); + }) + .opacity(this.filterCurrIndex ? 0 : 1) + .transition(TransitionEffect.OPACITY.animation({ duration: 400 })) + } + } else { + if (!this.filterCurrIndex) { + Canvas(this.canvasRenderingContext) + .width('100%') + .height('100%') + .backgroundColor(Color.Transparent) + .onReady(() => { + hilog.info(DOMAIN, TAG, '%{public}s', 'Canvas ready'); + this.canvasReady = true; + this.triggerCanvasRedraw(); + + }) + .onAreaChange((oldValue: Area, newValue: Area) => { + this.containerWidth = Number(newValue.width); + this.containerHeight = Number(newValue.height); + hilog.info(DOMAIN, TAG, '%{public}s', + `Canvas size changed: ${this.containerWidth} x ${this.containerHeight}`); + this.triggerCanvasRedraw(); + }) + .opacity(this.filterCurrIndex ? 0 : 1) + .transition(TransitionEffect.OPACITY.animation({ duration: 400 })) + } else { + if (this.isPixelMapChange) { + Canvas(this.canvasRenderingContext) + .width('100%') + .height('100%') + .backgroundColor(Color.Transparent) + .onReady(() => { + hilog.info(DOMAIN, TAG, '%{public}s', 'Canvas ready'); + this.canvasReady = true; + this.triggerCanvasRedraw(); + + }) + .onAreaChange((oldValue: Area, newValue: Area) => { + this.containerWidth = Number(newValue.width); + this.containerHeight = Number(newValue.height); + hilog.info(DOMAIN, TAG, '%{public}s', + `Canvas size changed: ${this.containerWidth} x ${this.containerHeight}`); + this.triggerCanvasRedraw(); + }) + .opacity(this.filterCurrIndex ? 1 : 0) + .transition(TransitionEffect.OPACITY.animation({ duration: 400 })) + } else { + Canvas(this.canvasRenderingContext) + .width('100%') + .height('100%') + .backgroundColor(Color.Transparent) + .onReady(() => { + hilog.info(DOMAIN, TAG, '%{public}s', 'Canvas ready'); + this.canvasReady = true; + this.triggerCanvasRedraw(); + + }) + .onAreaChange((oldValue: Area, newValue: Area) => { + this.containerWidth = Number(newValue.width); + this.containerHeight = Number(newValue.height); + hilog.info(DOMAIN, TAG, '%{public}s', + `Canvas size changed: ${this.containerWidth} x ${this.containerHeight}`); + this.triggerCanvasRedraw(); + }) + .opacity(this.filterCurrIndex ? 1 : 0) + .transition(TransitionEffect.OPACITY.animation({ duration: 400 })) + } + } + } + + } + .margin({ top: 5 }) + .width(CommonConstants.LAYOUT_FULL_SCREEN) + .height('66%') + + TabComponent({ + displaySize: this.displaySize as ImageDisplaySize, + containerWidth: this.containerWidth, + containerHeight: this.containerHeight, + cropList: this.cropList, + currentCropIndex: this.currentCropIndex, + currentIndex: this.currentIndex, + currentTranslateData: this.currentTranslateData, + currentTranslateIndex: this.currentTranslateIndex, + translateList: this.translateList, + currentZoom: this.currentZoom, + waterShow: this.waterShow, + canvasOffsetX: this.canvasOffsetX, + canvasOffsetY: this.canvasOffsetY, + triggerCanvasRedraw: () => { + this.triggerCanvasRedraw() + }, + flushPixelMapChange: () => { + this.flushPixelMapChange() + }, + initEffectKit: () => { + this.initEffectKit() + }, + drawTranslate: (x, y) => { + this.drawTranslate(x, y) + } + }) + .padding({ bottom: 30 }) + .width(CommonConstants.LAYOUT_FULL_SCREEN) + .height('34%') + .backgroundColor(Color.Black) + } + .width(CommonConstants.LAYOUT_FULL_SCREEN) + .height('calc(100% - 28vp)') + } + .width(CommonConstants.LAYOUT_FULL_SCREEN) + .height(CommonConstants.LAYOUT_FULL_SCREEN) + .bindSheet($$this.isShow, this.infoBuilder(), { + height: SheetSize.FIT_CONTENT, + onWillAppear: () => { + hilog.info(DOMAIN, TAG, '%{public}s', "Info BindSheet onWillAppear."); + }, + onAppear: () => { + hilog.info(DOMAIN, TAG, '%{public}s', "Info BindSheet onAppear."); + }, + onWillDisappear: () => { + hilog.info(DOMAIN, TAG, '%{public}s', "Info BindSheet onWillDisappear."); + }, + onDisappear: () => { + hilog.info(DOMAIN, TAG, '%{public}s', "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..00d904879e521d87f71457fc7a8742da16c06ad8 --- /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/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..3308a4e4775c51f310383d8e4f2581acf8665a56 --- /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 ContextStorageManager { + private static instance: ContextStorageManager; + + private constructor() { + } + + public static getInstance() { + if (!ContextStorageManager.instance) { + ContextStorageManager.instance = new ContextStorageManager(); + } + return ContextStorageManager.instance; + } + + public init(UIContext: UIContext) { + uiContext = UIContext + } + + public getContext() { + 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..e6062d19da37d180e0cbc36deb7ae4a23c375b63 --- /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/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..2cac3b8a0464c37817b7b70cb8e41cc77fa2ecc1 --- /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/CommonConstants'; +import { ContextStorageManager } 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 = ContextStorageManager.getInstance().getContext(); + 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 = 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..daab60634ea9e76685a7603b9fb215f643c7a2a6 --- /dev/null +++ b/entry/src/main/ets/utils/EncodeUtil.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. + */ + +import { photoAccessHelper } from '@kit.MediaLibraryKit'; +import { fileIo } from '@kit.CoreFileKit'; +import { image } from '@kit.ImageKit'; +import Logger from './LoggerUtil'; +import { CommonConstants } from '../common/CommonConstants'; +import { ContextStorageManager } 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 = ContextStorageManager.getInstance().getContext(); + + 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); + uiContext.getPromptAction().showToast({ message: $r('app.string.saved_to_album') }); + } + }); +} \ 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..f94a2db7a61007c0bb99f806ae20c12c0441305f --- /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/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..5f949bbecc4c6a6ce8fe01b0baf6482ef57dc478 --- /dev/null +++ b/entry/src/main/ets/utils/WatermarkUtil.ets @@ -0,0 +1,192 @@ +/* + * 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 { ContextStorageManager } from './ContextStorageUtil'; +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 { + /** + * Add text watermarks on PixelMap + * @param pixelMap original image + * @param config Watermark configuration + * @returns PixelMap after watermarking + */ + static async addTextWatermark(pixelMap: image.PixelMap, config: WatermarkConfig): Promise { + try { + const uiContext: UIContext = ContextStorageManager.getInstance().getContext(); + + const imageInfo = await pixelMap.getImageInfo(); + const imageWidth = uiContext.px2vp(imageInfo.size.width); + const imageHeight = uiContext.px2vp(imageInfo.size.height); + const offscreenCanvas = new OffscreenCanvas(imageWidth, imageHeight); + const offscreenCanvasContext = offscreenCanvas.getContext('2d') as OffscreenCanvasRenderingContext2D; + + offscreenCanvasContext.drawImage(pixelMap, 0, 0, imageWidth, imageHeight); + + offscreenCanvasContext.font = `${uiContext.fp2px(config.fontSize)}px sans-serif`; + offscreenCanvasContext.fillStyle = config.color; + offscreenCanvasContext.globalAlpha = config.opacity; + + const textMetrics = offscreenCanvasContext.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) { + offscreenCanvasContext.save(); + offscreenCanvasContext.translate(position.x + textWidth / 2, position.y + textHeight / 2); + offscreenCanvasContext.rotate((config.rotation * Math.PI) / 180); + offscreenCanvasContext.fillText(config.text, -textWidth / 2, textHeight / 4); + offscreenCanvasContext.restore(); + } else { + offscreenCanvasContext.fillText(config.text, position.x, position.y + textHeight); + } + + + const watermarkedPixelMap = offscreenCanvasContext.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)}`); + } + } + + /** + * Calculate watermark position + */ + 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; + } + } + + /** + * To create a repeated watermark effect + * @param pixelMap original image + * @param config Watermark configuration + * @param Spacing Watermark Spacing + * @returns PixelMap after adding repeated watermarks + */ + static async addRepeatedWatermark( + pixelMap: image.PixelMap, + config: WatermarkConfig, + spacing?: SpacingConfig + ): Promise { + try { + const uiContext: UIContext = ContextStorageManager.getInstance().getContext(); + + const defaultSpacing: SpacingConfig = { x: 200, y: 150 }; + const actualSpacing = spacing || defaultSpacing; + + const imageInfo = await pixelMap.getImageInfo(); + const imageWidth = uiContext.px2vp(imageInfo.size.width); + const imageHeight = uiContext.px2vp(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 = `${uiContext.fp2px(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 = 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..c64088c4e510b104906f9f1abe97f7b27767e5a4 --- /dev/null +++ b/entry/src/main/ets/view/AdjustContentView.ets @@ -0,0 +1,294 @@ +/* + * 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 } from '@kit.ArkTS'; +import { adjustIconList, IconStatus } from '../viewModel/IconListViewModel'; +import { CommonConstants } from '../common/CommonConstants'; +import { AdjustId } from '../viewModel/OptionViewModel'; +import { MessageItem } from '../viewModel/MessageItem'; +import { PixelDataManager } from './PixelDataManager'; +import { effectKit } from '@kit.ArkGraphics2D'; + +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('pixelMapInit') pixelMapInit?: image.PixelMap; + @Consume('currentAdjustIndex') @Watch('onAdjustIndexChange') currentAdjustIndex: number; + @Consume('isPixelMapChange') isPixelMapChange: boolean; + @Consume('currentIndex') currentIndex: number; + @Consume('currentCropIndex') @Watch('onCurrentCropIndex') currentCropIndex: number; + @Consume('adjustPixelMap') adjustPixelMap?: image.PixelMap; + private paramValues: number[] = [ + CommonConstants.SLIDER_MAX, + CommonConstants.SLIDER_MAX, + CommonConstants.SLIDER_MAX + ]; + private pixelDataManager = PixelDataManager.getInstance(); + private initialPixelBuffer?: ArrayBuffer; + deviceListDialogController: CustomDialogController = new CustomDialogController({ + builder: Dialog(), + alignment: DialogAlignment.Center, + autoCancel: false, + customStyle: true + }); + + aboutToAppear() { + this.initEffectKit(); + if (this.pixelMap && !this.pixelDataManager.hasInitialPixelBuffer()) { + this.initialPixelMap(); + } + this.paramValues = [...this.currentAdjustData]; + } + + async initEffectKit() { + this.adjustPixelMap = await effectKit.createEffect(this.pixelMap).getEffectPixelMap(); + } + + onCurrentCropIndex() { + if (this.currentCropIndex === 0 && this.currentIndex === 1) { + if (this.pixelMap) { + this.currentAdjustData = [ + CommonConstants.SLIDER_MAX, + CommonConstants.SLIDER_MAX, + CommonConstants.SLIDER_MAX + ]; + this.isPixelMapChange = !this.isPixelMapChange; + this.initialPixelMap(); + } + this.paramValues = [...this.currentAdjustData]; + } + } + + onAdjustIndexChange() { + this.paramValues[this.currentAdjustIndex] = this.currentAdjustData[this.currentAdjustIndex]; + } + + initialPixelMap() { + this.initialPixelBuffer = new ArrayBuffer(this.pixelMap!.getPixelBytesNumber()); + this.pixelMap!.readPixelsToBuffer(this.initialPixelBuffer); + this.pixelDataManager.saveInitialPixelBuffer(this.pixelMap!); + } + + private checkAndRestoreOriginal() { + const allDefault = this.paramValues.every(val => val === CommonConstants.SLIDER_MAX); + if (allDefault && this.pixelMap && this.initialPixelBuffer) { + this.pixelMap.writeBufferToPixelsSync(this.initialPixelBuffer); + this.deviceListDialogController.open(); + this.flushPixelMapChange(); + return true; + } + return false; + } + + 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: 20, bottom: 20 }) + .width(CommonConstants.LAYOUT_FULL_SCREEN) + .justifyContent(FlexAlign.Center) + } + } + + flushPixelMapChange() { + this.pixelMap?.translate(0, 0).then(() => { + setTimeout(() => { + this.isPixelMapChange = !this.isPixelMapChange; + this.deviceListDialogController.close(); + }, 1000) + }) + } + + async sliderChange(value: number, mode: SliderChangeMode) { + if ((mode === SliderChangeMode.End) && (value !== this.currentAdjustData[this.currentAdjustIndex])) { + const roundedValue = Math.round(value); + this.currentAdjustData[this.currentAdjustIndex] = roundedValue; + this.paramValues[this.currentAdjustIndex] = roundedValue; + + if (this.checkAndRestoreOriginal()) { + return; + } + this.deviceListDialogController.open(); + const bufferArray = this.pixelDataManager.getInitialPixelBufferCopy(); + if (!bufferArray || !this.pixelMap) { + this.deviceListDialogController.close(); + return; + } + let processedBuffer = bufferArray; + if (this.paramValues[AdjustId.BRIGHTNESS] !== CommonConstants.SLIDER_MAX) { + processedBuffer = await this.processWithWorker( + this.paramValues[AdjustId.BRIGHTNESS], + CommonConstants.BRIGHTNESS_WORKER_FILE, + processedBuffer + ); + } + if (this.paramValues[AdjustId.SATURATION] !== CommonConstants.SLIDER_MAX) { + processedBuffer = await this.processWithWorker( + this.paramValues[AdjustId.SATURATION], + CommonConstants.SATURATION_WORKER_FILE, + processedBuffer + ); + } + this.pixelMap.writeBufferToPixelsSync(processedBuffer); + const opacity = this.paramValues[AdjustId.TRANSPARENCY] / CommonConstants.SLIDER_MAX; + this.pixelMap.opacitySync(opacity); + this.flushPixelMapChange(); + } + } + + processWithWorker(value: number, workerName: string, buffer: ArrayBuffer): Promise { + return new Promise((resolve) => { + const workerInstance = new worker.ThreadWorker(workerName); + const message = new MessageItem(buffer, CommonConstants.SLIDER_MAX, value); + workerInstance.postMessage(message); + workerInstance.onmessage = (event: MessageEvents) => { + resolve(event.data); + workerInstance.terminate(); + }; + workerInstance.onexit = () => { + 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.SpaceBetween) + .width(CommonConstants.LAYOUT_FULL_SCREEN) + .height('100%') + } +} + + diff --git a/entry/src/main/ets/view/ApplyFilterView.ets b/entry/src/main/ets/view/ApplyFilterView.ets new file mode 100644 index 0000000000000000000000000000000000000000..f643ec5d5ec3ac051917b340068e0539430b53ea --- /dev/null +++ b/entry/src/main/ets/view/ApplyFilterView.ets @@ -0,0 +1,168 @@ +/* + * 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 { hilog } from '@kit.PerformanceAnalysisKit'; + +const DOMAIN = 0xFF00; +const TAG = 'ApplyFilterView'; + +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('pixelMapInit') pixelMapInit?: image.PixelMap; + @Consume('isPixelMapChange') @Watch('handlePixelMap') isPixelMapChange: boolean; + @State private isProcessing: boolean = false; + @State private lastProcessedPixelMap?: image.PixelMap = undefined; + + handlePixelMap() { + if (this.filterPixelMap !== undefined && this.filterCurrIndex) { + this.handleFilter(this.filterCurrIndex); + } + } + + async handleFilter(index: number) { + if (this.isProcessing || !this.pixelMap || !this.pixelMapInit) { + return; + } + + if (this.lastProcessedPixelMap === this.pixelMap && this.filterCurrIndex === index) { + return; + } + + this.isProcessing = true; + this.filterCurrIndex = index; + let curFilterPixelMap: PixelMap; + let curFilterPixelMapInit: PixelMap; + try { + let filter = effectKit.createEffect(this.pixelMap); + let filterInit = effectKit.createEffect(this.pixelMapInit); + switch (this.filterData[index].key) { + case 'grayscale': + curFilterPixelMap = await filter.grayscale().getEffectPixelMap(); + curFilterPixelMapInit = await filterInit.grayscale().getEffectPixelMap(); + break; + case 'brightness': + curFilterPixelMap = await filter.brightness(0.7).getEffectPixelMap(); + curFilterPixelMapInit = await filterInit.brightness(0.7).getEffectPixelMap(); + break; + case 'invert': + curFilterPixelMap = await filter.invert().getEffectPixelMap(); + curFilterPixelMapInit = await filterInit.invert().getEffectPixelMap(); + break; + case 'blur': + curFilterPixelMap = await filter.blur(5).getEffectPixelMap(); + curFilterPixelMapInit = await filterInit.blur(5).getEffectPixelMap(); + break; + + default: + curFilterPixelMap = await filter.grayscale().getEffectPixelMap(); + curFilterPixelMapInit = await filterInit.grayscale().getEffectPixelMap(); + break; + } + this.filterPixelMap = curFilterPixelMap; + this.pixelMapInit = curFilterPixelMapInit; + + this.isPixelMapChange = !this.isPixelMapChange; + } catch (error) { + hilog.error(DOMAIN, TAG, '%{public}s, %{public}s', 'Filter processing failed:', JSON.stringify(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/PixelDataManager.ets b/entry/src/main/ets/view/PixelDataManager.ets new file mode 100644 index 0000000000000000000000000000000000000000..35681bf52767865ebcbd5bf065afaabc4c15d8f7 --- /dev/null +++ b/entry/src/main/ets/view/PixelDataManager.ets @@ -0,0 +1,49 @@ +/* + * 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 PixelDataManager { + private static instance: PixelDataManager; + private initialPixelBuffer?: ArrayBuffer; + + private constructor() { + } + + public static getInstance(): PixelDataManager { + if (!PixelDataManager.instance) { + PixelDataManager.instance = new PixelDataManager(); + } + return PixelDataManager.instance; + } + + public saveInitialPixelBuffer(pixelMap: image.PixelMap): void { + this.initialPixelBuffer = new ArrayBuffer(pixelMap.getPixelBytesNumber()); + pixelMap.readPixelsToBuffer(this.initialPixelBuffer); + } + + public getInitialPixelBufferCopy(): ArrayBuffer | undefined { + return this.initialPixelBuffer?.slice(0); + } + + public hasInitialPixelBuffer(): boolean { + return !!this.initialPixelBuffer; + } + + public clearInitialPixelBuffer(): void { + this.initialPixelBuffer = undefined; + } +} + diff --git a/entry/src/main/ets/view/WatermarkView.ets b/entry/src/main/ets/view/WatermarkView.ets new file mode 100644 index 0000000000000000000000000000000000000000..aa1d25226c099ac56ceaba4fff4f73df870e5c2b --- /dev/null +++ b/entry/src/main/ets/view/WatermarkView.ets @@ -0,0 +1,347 @@ +/* + * 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'; +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { image } from '@kit.ImageKit'; + +const DOMAIN = 0xFF00; +const TAG = 'WatermarkView'; + +@Component +export struct WatermarkView { + @Consume pixelMap: PixelMap | undefined; + @Consume('pixelMapInit') pixelMapInit: image.PixelMap; + @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; + } + } + + @Builder + positionBtn(name: Resource, styleName: number) { + Row() { + Button(name, { + controlSize: ControlSize.SMALL, + buttonStyle: this.currentPosition === styleName ? ButtonStyleMode.EMPHASIZED : + ButtonStyleMode.NORMAL + }) + .fontSize(14) + .fontColor(this.currentPosition === styleName ? '#FFFFFF' : '#99000000') + .fontWeight(FontWeight.Medium) + .padding({ + left: 16, + right: 16 + }) + .onClick(() => { + this.currentPosition = styleName; + }) + + .height(28) + } + .alignItems(VerticalAlign.Center) + .justifyContent(FlexAlign.Center) + .width(70) + .height(28) + } + + build() { + Column() { + Scroll() { + Column() { + Row() { + Text($r('app.string.watermark_text')) + .fontSize(16) + .fontWeight(500) + TextInput({ placeholder: $r('app.string.watermark_text') }) + .fontColor('#E5') + .backgroundColor('#F5F5F5') + .placeholderColor('#99000000') + .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') + + Column() { + Row() { + Text($r('app.string.watermark_position')) + .fontSize(16) + .fontWeight(500) + Row() { + }.width('10%') + } + .width('100%') + .padding({ bottom: 13 }) + .justifyContent(FlexAlign.SpaceBetween) + + Tabs({ barPosition: BarPosition.End }) { + TabContent().tabBar(this.positionBtn($r('app.string.position_top_left'), WatermarkPosition.TOP_LEFT)) + + TabContent() + .tabBar(this.positionBtn($r('app.string.position_top_right'), WatermarkPosition.TOP_RIGHT)) + + + TabContent() + .tabBar(this.positionBtn($r('app.string.position_bottom_left'), WatermarkPosition.BOTTOM_LEFT)) + + TabContent() + .tabBar(this.positionBtn($r('app.string.position_bottom_right'), WatermarkPosition.BOTTOM_RIGHT)) + + + TabContent() + .tabBar(this.positionBtn($r('app.string.position_center'), WatermarkPosition.CENTER)) + } + .vertical(false) + .scrollable(true) + .barMode(BarMode.Scrollable) + .barHeight(80) + .animationDuration(400) + .onChange((index: number) => { + hilog.info(DOMAIN, TAG, '%{public}s', index.toString()); + }) + .fadingEdge(false) + .barHeight(28) + .height(30) + .width('100%') + } + .padding({ top: 13, bottom: 13 }) + .width('100%') + .justifyContent(FlexAlign.SpaceBetween) + + 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') + .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') + .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') + .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 }) + .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({ space: 12 }) { + Button($r('app.string.add_watermark'), + { + buttonStyle: ButtonStyleMode.NORMAL, + role: ButtonRole.NORMAL, + }) + .opacity(this.watermarkText.length > 0 ? 1 : 0.4) + .fontSize(14) + .borderRadius(8) + .width('calc(50% - 6vp)') + .height(36) + .enabled(this.watermarkText.length > 0) + .onClick(() => { + this.addWatermark(); + }) + + Button($r('app.string.remove_watermark'), { buttonStyle: ButtonStyleMode.NORMAL, role: ButtonRole.ERROR }) + .opacity(this.watermarkText.length > 0 ? 1 : 0.4) + .fontSize(14) + .borderRadius(8) + .width('calc(50% - 6vp)') + .height(36) + .enabled(this.hasWatermark) + .onClick(() => { + this.removeWatermark(); + }) + } + .padding({ + top: 20, + bottom: 16, + }) + .width('100%') + .justifyContent(FlexAlign.SpaceBetween) + } + .width('100%') + .justifyContent(FlexAlign.Start) + } + .padding({ + top: 4, + bottom: 4, + }) + .backgroundColor('#FFF') + .borderRadius(16) + .scrollable(ScrollDirection.Vertical) + .scrollBar(BarState.Off) + .friction(0.6) + .edgeEffect(EdgeEffect.None) + } + .width('100%') + } + + // Add watermark + 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; + let newPixelMapInit: PixelMap; + if (this.isRepeated) { + newPixelMap = await WatermarkUtil.addRepeatedWatermark(this.pixelMap, config); + newPixelMapInit = await WatermarkUtil.addRepeatedWatermark(this.pixelMapInit!, config); + } else { + newPixelMap = await WatermarkUtil.addTextWatermark(this.pixelMap, config); + newPixelMapInit = await WatermarkUtil.addTextWatermark(this.pixelMapInit!, config); + } + + this.pixelMap = newPixelMap; + this.pixelMapInit = newPixelMapInit; + this.isPixelMapChange = !this.isPixelMapChange; + this.hasWatermark = true; + } catch (error) { + hilog.error(DOMAIN, TAG, '%{public}s, %{public}s', 'Adding watermark failed:', 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/PictureEditModel.ets b/entry/src/main/ets/viewModel/PictureEditModel.ets new file mode 100644 index 0000000000000000000000000000000000000000..e40ed6f640a7b5b3992a8e37d30a74fc417e32de --- /dev/null +++ b/entry/src/main/ets/viewModel/PictureEditModel.ets @@ -0,0 +1,37 @@ +/* + * 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 interface TranslateListType { + icon: string, + title: Resource | string, + color: string, + selectColor: string +} + +export interface InitDataArrType { + label: '', + value: '' +} + +export interface ParamsType { + selectUris: string +} + +export interface ImageDisplaySize { + x: number; + y: number; + width: number; + height: number; +} \ No newline at end of file diff --git a/entry/src/main/ets/viewModel/PictureEditModelData.ets b/entry/src/main/ets/viewModel/PictureEditModelData.ets new file mode 100644 index 0000000000000000000000000000000000000000..82b9ce9a67b1224de26684ca16a1d14c47b9b05a --- /dev/null +++ b/entry/src/main/ets/viewModel/PictureEditModelData.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. + */ + +import { TranslateListType } from "./PictureEditModel"; + +export const cropListData: 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' + } +] + +export const translateListData: 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' + }, +] \ 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..0089e1f5323e84772c99ee9eae311df7b71753e5 --- /dev/null +++ b/entry/src/main/ets/workers/AdjustBrightnessWork.ts @@ -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. + */ + +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { worker, ThreadWorkerGlobalScope, MessageEvents, ErrorEvent } from '@kit.ArkTS'; +import { adjustImageValue } from '../utils/AdjustUtil'; + +const DOMAIN = 0xFF00; +const TAG: string = 'AdjustBrightnessWork'; +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(DOMAIN, TAG, '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(DOMAIN, TAG, '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..9ca713e54178859bd269109601b39ed94c5f3f72 --- /dev/null +++ b/entry/src/main/ets/workers/AdjustSaturationWork.ts @@ -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. + */ + +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { worker, ThreadWorkerGlobalScope, MessageEvents, ErrorEvent } from '@kit.ArkTS'; +import { adjustSaturation } from '../utils/AdjustUtil'; + +const DOMAIN = 0xFF00; +const TAG: string = 'AdjustSaturationWork'; +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(DOMAIN, TAG, '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(DOMAIN, TAG, '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..da39a1f0fb18f20f2fb366c2bc6997443fc8eb34 --- /dev/null +++ b/entry/src/main/module.json5 @@ -0,0 +1,72 @@ +{ + "module": { + "name": "entry", + "type": "entry", + "description": "$string:module_desc", + "mainElement": "EntryAbility", + "deviceTypes": [ + "phone", + ], + "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..eff52cf452adb1b6b31502638b2f7c559d4582be --- /dev/null +++ b/entry/src/main/resources/base/element/string.json @@ -0,0 +1,212 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "ImageCanvasEdit" + }, + { + "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 Edit" + }, + { + "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" + }, + { + "name": "saved_to_album", + "value": "Saved to album" + }, + { + "name": "save_adjust", + "value": "Do you want to save the adjust?" + } + ] +} \ 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_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..ceb62850d4cf0d2c8d68462602951c78b226bdd1 --- /dev/null +++ b/entry/src/main/resources/en_US/element/string.json @@ -0,0 +1,212 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "ImageCanvasEdit" + }, + { + "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 Edit" + }, + { + "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" + }, + { + "name": "saved_to_album", + "value": "Saved to album" + }, + { + "name": "save_adjust", + "value": "Do you want to save the adjust?" + } + ] +} \ 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..1406467201e53d8c4982bd614518e46b89f712bd --- /dev/null +++ b/entry/src/main/resources/zh_CN/element/string.json @@ -0,0 +1,212 @@ +{ + "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": "重复水印" + }, + { + "name": "saved_to_album", + "value": "已保存至相册" + }, + { + "name": "save_adjust", + "value": "是否保存调试?" + } + ] +} \ 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..867669ae10f46088d6b3f4fa828ab85edfc86686 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..1117bbacb582fc6ff79cfb585c6dd4987a3d221d 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..34dc1667f956ef2f54ecd7009754f9e59263dd4c Binary files /dev/null and b/screenshots/device/dome3.png differ