1/* 2 * Copyright (c) 2024 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import imageEffect from 'libentry.so' 17import image from '@ohos.multimedia.image'; 18import { ImageUtils } from '../utils/ImageUtils'; 19import { fileUri } from '@kit.CoreFileKit'; 20import fs from '@ohos.file.fs'; 21 22@Entry 23@Component 24struct ImageEditPage { 25 private tag: string = '[Sample_ImageEdit]'; 26 settingBtn: Resource = $r('app.media.ic_public_settings'); 27 @Provide pixelMap: image.PixelMap | undefined = undefined; 28 @Provide displayPixelMap: image.PixelMap | undefined = undefined; 29 @State brightnessSetValue: number = 100; 30 @State brightnessSelect: boolean = true; 31 @State contrastSetValue: number = 0; 32 @State contrastSelect: boolean = false; 33 @State cropSetValue: number = 100; 34 @State cropSelect: boolean = false; 35 @State customSetValue: number = 0; 36 @State customSelect: boolean = false; 37 @State filterOptions: Array<Array<string | number>> = []; 38 @State filterInfo: string = ""; 39 40 aboutToAppear(): void { 41 console.info(`${this.tag} aboutToAppear called`); 42 ImageUtils.getInstance().getPixelMap($r('app.media.ic_1080x1920')).then(pixelMap => { 43 this.pixelMap = pixelMap; 44 }) 45 ImageUtils.getInstance().getPixelMap($r('app.media.ic_1080x1920')).then(pixelMap => { 46 this.displayPixelMap = pixelMap; 47 }) 48 console.info(`${this.tag} aboutToAppear done`); 49 } 50 51 aboutToDisappear(): void { 52 console.info(`${this.tag} aboutToDisappear called`); 53 console.info(`${this.tag} aboutToDisappear done`); 54 } 55 56 build() { 57 Column() { 58 Row() { 59 Row() { 60 Text($r('app.string.image_edit')) 61 .fontColor(Color.White) 62 .fontSize('app.float.title_font_size') 63 .margin({ left: $r('app.float.home_page_title_margin') }) 64 } 65 66 Blank() 67 68 Row({ space: 0 }) { 69 Button() { 70 Image(this.settingBtn) 71 .width($r('app.float.title_image_width')) 72 .height($r('app.float.title_image_height')) 73 .id('btn_setting') 74 } 75 .height('100%') 76 .type(ButtonType.Normal) 77 .aspectRatio(1) 78 .backgroundColor(Color.Transparent) 79 .onClick(() => { 80 if (this.dialogController != undefined) { 81 console.info(`${this.tag} to open setting dialog`); 82 this.dialogController.open(); 83 } 84 }) 85 } 86 } 87 .width('100%') 88 .height($r('app.float.home_page_title_height')) 89 .margin({ top: $r('app.float.home_page_title_margin') }) 90 91 Column() { 92 Image(this.displayPixelMap) 93 .objectFit(ImageFit.None) 94 } 95 .clip(true) 96 .width('100%') 97 .height('85%') 98 99 Column() { 100 Row() { 101 Button("Reset").id("btn_reset").onClick(() => { 102 this.pixelInit(); 103 }).width(100).margin({ left: 3, right: 3, top: 3, bottom: 6 }) 104 105 Button("Apply").id("btn_apply").onClick(() => { 106 this.doApply(); 107 }).width(100).margin({ left: 3, right: 3, top: 3, bottom: 6 }) 108 } 109 .justifyContent(FlexAlign.Center) 110 } 111 .align(Alignment.End) 112 .width('100%') 113 .height('6%') 114 .margin({ top: $r('app.float.home_page_title_margin') }) 115 .backgroundColor(Color.Black) 116 } 117 .width('100%') 118 .height('100%') 119 .backgroundColor(Color.Black) 120 } 121 122 private async doSavePixel(): Promise<void> { 123 let pixelMap = await ImageUtils.getInstance().getPixelMap($r('app.media.ic_1080x1920')); 124 const imagePackerApi = image.createImagePacker(); 125 const packOption: image.PackingOption = { 126 format: 'image/jpeg', 127 quality: 100 128 }; 129 let filePath = getContext().filesDir + "/test.jpg"; 130 let uri = fileUri.getUriFromPath(filePath); 131 let imageData = await imagePackerApi.packing(pixelMap, packOption); 132 let file = fs.openSync(uri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); 133 let writeLen = fs.writeSync(file.fd, imageData); 134 fs.closeSync(file); 135 console.info(`write data to file succeed and size is ${writeLen}`); 136 } 137 138 private confirmInfo() { 139 this.filterOptions = []; 140 if (this.brightnessSelect) { 141 let brightnessArray: (string | number)[] = []; 142 brightnessArray.push("Brightness", this.brightnessSetValue); 143 this.filterOptions.push(brightnessArray); 144 } 145 if (this.contrastSelect) { 146 let contrastArray: (string | number)[] = []; 147 contrastArray.push("Contrast", this.contrastSetValue); 148 this.filterOptions.push(contrastArray); 149 } 150 if (this.cropSelect) { 151 let cropArray: (string | number)[] = []; 152 cropArray.push("Crop", this.cropSetValue); 153 this.filterOptions.push(cropArray); 154 } 155 if (this.customSelect) { 156 let customArray: (string | number)[] = []; 157 customArray.push("CustomBrightness", this.customSetValue); 158 this.filterOptions.push(customArray); 159 } 160 } 161 162 private async doApply(): Promise<void> { 163 this.confirmInfo(); 164 if (this.brightnessSelect || this.contrastSelect || this.cropSelect || this.customSelect) { 165 await this.doSavePixel(); 166 let filePath = getContext().filesDir + "/test.jpg"; 167 imageEffect.apply(filePath, [...this.filterOptions]); 168 this.displayPixelMap = await ImageUtils.getInstance().getPixelMapByFilePath(filePath); 169 } 170 } 171 172 private async pixelInit(): Promise<void> { 173 this.displayPixelMap?.release(); 174 this.displayPixelMap = await ImageUtils.getInstance().getPixelMap($r('app.media.ic_1080x1920')) 175 } 176 177 dialogController: CustomDialogController = new CustomDialogController({ 178 builder: CustomDialogExample({ 179 cancel: this.onCancel, 180 confirm: this.onAccept, 181 filterOptions: $filterOptions, 182 brightnessSetValue: $brightnessSetValue, 183 brightnessSelect: $brightnessSelect, 184 contrastSetValue: $contrastSetValue, 185 contrastSelect: $contrastSelect, 186 cropSetValue: $cropSetValue, 187 cropSelect: $cropSelect, 188 customSetValue: $customSetValue, 189 customSelect: $customSelect, 190 filterInfo: $filterInfo, 191 }), 192 cancel: this.existApp, 193 autoCancel: true, 194 }) 195 196 onCancel() { 197 console.info(`Callback when the cancel button is clicked`); 198 } 199 200 onAccept() { 201 console.info(`Callback when the confirm button is clicked`); 202 } 203 204 existApp() { 205 console.info(`click the callback in the blank area`); 206 } 207} 208 209@CustomDialog 210struct CustomDialogExample { 211 @Link brightnessSetValue: number 212 @Link brightnessSelect: boolean 213 @Link contrastSetValue: number 214 @Link contrastSelect: boolean 215 @Link cropSetValue: number 216 @Link cropSelect: boolean 217 @Link customSetValue: number 218 @Link customSelect: boolean 219 @Link filterOptions: Array<Array<string | number>> 220 @Link filterInfo: string; 221 controller: CustomDialogController; 222 cancel: () => void = () => { 223 } 224 confirm: () => void = () => { 225 } 226 @State formatList: Array<String> = ["Format:default", "Format:rgba_8888", "Format:nv21", "Format:nv12"] 227 @State handlePopup: boolean = false 228 229 @Builder 230 FilterInfoMenu() { 231 Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Start, justifyContent: FlexAlign.Center }) { 232 Flex({ direction: FlexDirection.Row, justifyContent: FlexAlign.Start }) { 233 Text(this.filterInfo).fontSize(16) 234 } 235 } 236 .width('60%').height('15%') 237 .opacity(0.8) 238 .id("filter_info_menu") 239 } 240 241 private async doLookFilterInfo(name: String): Promise<void> { 242 this.filterInfo = imageEffect.lookupFilterInfo(name); 243 } 244 245 build() { 246 Column() { 247 Column() { 248 Divider().height(2).color(0xcccccc) 249 Column() { 250 Text('Filter') 251 .width('100%') 252 .fontSize(18) 253 .margin({ bottom: 10 }) 254 255 Row() { 256 Column() { 257 Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) { 258 Checkbox({ name: 'brightnessCheckbox', group: 'filterCheckboxGroup' }) 259 .selectedColor(0x39a2db) 260 .select(this.brightnessSelect) 261 .onChange((value: boolean) => { 262 this.brightnessSelect = value; 263 }) 264 .width(10) 265 .height(14) 266 .id("checkbox_brightness") 267 Text('Brightness').fontSize(10).width('18%') 268 Slider({ 269 value: this.brightnessSetValue, 270 min: -100, 271 max: 100, 272 style: SliderStyle.OutSet 273 }) 274 .showTips(true, this.brightnessSetValue.toFixed()) 275 .onChange((value: number, mode: SliderChangeMode) => { 276 this.brightnessSetValue = value 277 console.info('value:' + value + 'mode:' + mode.toString()) 278 }) 279 .width('60%') 280 .id("slider_brightness") 281 // toFixed(0)将滑动条返回值处理为整数精度 282 Column() { 283 Text(this.brightnessSetValue.toFixed(0)).fontSize(12) 284 }.width('8%') 285 286 Column() { 287 Image($r('app.media.ic_public_search')) 288 .width('5%') 289 .height('3.7%') 290 }.bindMenu(this.FilterInfoMenu, { 291 onAppear: () => { 292 this.doLookFilterInfo("Brightness"); 293 }, 294 onDisappear: () => { 295 this.filterInfo = ""; 296 } 297 }) 298 .margin({ left: 2 }) 299 .id("btn_search_brightness") 300 } 301 302 Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) { 303 Checkbox({ name: 'contrastCheckbox', group: 'filterCheckboxGroup' }) 304 .selectedColor(0x39a2db) 305 .select(this.contrastSelect) 306 .onChange((value: boolean) => { 307 this.contrastSelect = value; 308 }) 309 .width(10) 310 .height(14) 311 .id("checkbox_contrast") 312 Text('Contrast').fontSize(10).width('18%') 313 Slider({ 314 value: this.contrastSetValue, 315 min: -100, 316 max: 100, 317 style: SliderStyle.OutSet 318 }) 319 .showTips(true, this.contrastSetValue.toFixed()) 320 .onChange((value: number, mode: SliderChangeMode) => { 321 this.contrastSetValue = value 322 console.info('value:' + value + 'mode:' + mode.toString()) 323 }) 324 .width('60%') 325 .id("slider_contrast") 326 // toFixed(0)将滑动条返回值处理为整数精度 327 Column() { 328 Text(this.contrastSetValue.toFixed(0)).fontSize(12) 329 }.width('8%') 330 331 Column() { 332 Image($r('app.media.ic_public_search')) 333 .width('5%') 334 .height('3.7%') 335 }.bindMenu(this.FilterInfoMenu, { 336 onAppear: () => { 337 this.doLookFilterInfo("Contrast"); 338 }, 339 onDisappear: () => { 340 this.filterInfo = ""; 341 } 342 }) 343 .margin({ left: 2 }) 344 .id("btn_search_contrast") 345 } 346 347 Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) { 348 Checkbox({ name: 'cropCheckbox', group: 'filterCheckboxGroup' }) 349 .selectedColor(0x39a2db) 350 .select(this.cropSelect) 351 .onChange((value: boolean) => { 352 this.cropSelect = value; 353 }) 354 .width(10) 355 .height(14) 356 .id("checkbox_crop") 357 Text('Crop').fontSize(10).width('18%') 358 Slider({ 359 value: this.cropSetValue, 360 min: 1, 361 max: 100, 362 style: SliderStyle.OutSet 363 }) 364 .showTips(true, this.cropSetValue.toFixed()) 365 .onChange((value: number, mode: SliderChangeMode) => { 366 this.cropSetValue = value 367 console.info('value:' + value + 'mode:' + mode.toString()) 368 }) 369 .width('60%') 370 .id("slider_crop") 371 // toFixed(0)将滑动条返回值处理为整数精度 372 Column() { 373 Text(this.cropSetValue.toFixed(0)).fontSize(12) 374 }.width('8%') 375 376 Column() { 377 Image($r('app.media.ic_public_search')) 378 .width('5%') 379 .height('3.7%') 380 }.bindMenu(this.FilterInfoMenu, { 381 onAppear: () => { 382 this.doLookFilterInfo("Crop"); 383 }, 384 onDisappear: () => { 385 this.filterInfo = ""; 386 } 387 }) 388 .margin({ left: 2 }) 389 .id("btn_search_crop") 390 } 391 392 Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) { 393 Checkbox({ name: 'customCheckbox', group: 'filterCheckboxGroup' }) 394 .selectedColor(0x39a2db) 395 .select(this.customSelect) 396 .onChange((value: boolean) => { 397 this.customSelect = value; 398 }) 399 .width(10) 400 .height(14) 401 .id("checkbox_custom") 402 Text('Custom').fontSize(10).width('18%') 403 Slider({ 404 value: this.customSetValue, 405 min: -100, 406 max: 100, 407 style: SliderStyle.OutSet 408 }) 409 .showTips(true, this.customSetValue.toFixed()) 410 .onChange((value: number, mode: SliderChangeMode) => { 411 this.customSetValue = value 412 console.info('value:' + value + 'mode:' + mode.toString()) 413 }) 414 .width('60%') 415 .id("slider_custom") 416 // toFixed(0)将滑动条返回值处理为整数精度 417 Column() { 418 Text(this.customSetValue.toFixed(0)).fontSize(12) 419 }.width('8%') 420 421 Column() { 422 Image($r('app.media.ic_public_search')) 423 .width('5%') 424 .height('3.7%') 425 }.bindMenu(this.FilterInfoMenu, { 426 onAppear: () => { 427 this.doLookFilterInfo("CustomBrightness"); 428 }, 429 onDisappear: () => { 430 this.filterInfo = ""; 431 } 432 }) 433 .margin({ left: 2 }) 434 .id("btn_search_custom") 435 } 436 } 437 .width('100%') 438 } 439 } 440 }.margin({ bottom: 10 }) 441 442 Column() { 443 Divider().height(2).color(0xCCCCCC); 444 Column() { 445 Text($r('app.string.look_up')) 446 .width('100%') 447 .fontSize(18) 448 .margin({ bottom: 10 }) 449 Row() { 450 Column() { 451 Text($r('app.string.btn_search')) 452 .width('15%') 453 .fontSize(20) 454 .margin({ left: '35%' }) 455 } 456 457 Column() { 458 Image($r('app.media.ic_public_arrow_right')) 459 .fillColor(Color.Black) 460 .width('10%') 461 .height('4%') 462 } 463 } 464 .id("btn_search") 465 .width('100%') 466 .justifyContent(FlexAlign.Start) 467 .bindMenu(this.LookupCategoryMenuBuilder) 468 } 469 }.margin({ bottom: 10 }) 470 471 Divider().height(2).color(0xCCCCCC) 472 473 Flex({ justifyContent: FlexAlign.SpaceAround }) { 474 Button($r('app.string.btn_cancel')) 475 .onClick(() => { 476 this.controller.close() 477 this.cancel(); 478 }).backgroundColor(0xffffff) 479 .fontColor(Color.Black) 480 .id("btn_dialog_cancel") 481 Button($r('app.string.btn_confirm')) 482 .onClick(() => { 483 484 this.controller.close() 485 this.confirm() 486 }).backgroundColor(0xffffff) 487 .fontColor(Color.Red) 488 .id("btn_dialog_confirm") 489 } 490 }.margin(24) 491 } 492 493 @Builder 494 LookupCategoryMenuBuilder() { 495 Menu() { 496 MenuItem({ 497 content: "Format", 498 builder: (): void => this.FormatMenuBuilder() 499 }) 500 }.id("menu_category") 501 } 502 503 @Builder 504 FormatMenuBuilder() { 505 Menu() { 506 ForEach(this.formatList, (item: string, index) => { 507 LookupFilterMenuItem({ item: item }) 508 .id("menu_format") 509 }) 510 } 511 } 512} 513 514@Component 515struct LookupFilterMenuItem { 516 @State item: string = ""; 517 518 dialogController: CustomDialogController = new CustomDialogController({ 519 builder: CustomDialogDetails({ 520 item: $item 521 }), 522 offset: { dx: 0, dy: 220 }, 523 autoCancel: true, 524 }) 525 526 build() { 527 MenuItem({ content: this.item }) 528 .onClick(() => { 529 if (this.dialogController != null) { 530 this.dialogController.open() 531 } 532 }) 533 } 534} 535 536@CustomDialog 537struct CustomDialogDetails { 538 @Link item: string 539 controller?: CustomDialogController 540 541 build() { 542 Column() { 543 Text('Filters:\n' + imageEffect.lookupFilters(this.item)) 544 .fontSize(16) 545 } 546 } 547} 548