1# 拉起图片编辑类应用(startAbilityByType)
2## 使用场景
3当应用自身不具备图片编辑能力、但存在图片编辑的场景时,可以通过startAbilityByType拉起图片编辑类应用扩展面板,由对应的应用完成图片编辑操作。图片编辑类应用可以通过PhotoEditorExtensionAbility实现图片编辑页面,并将该页面注册到图片编辑面板,从而将图片编辑能力开放给其他应用。
4
5流程示意图如下:
6
7![](figures/photoEditorExtensionAbility.png)
8
9例如:用户在图库App中选择编辑图片时,图库App可以通过startAbilityByType拉起图片编辑类应用扩展面板。用户可以从已实现PhotoEditorExtensionAbility应用中选择一款,并进行图片编辑。
10
11## 接口说明
12
13接口详情参见[PhotoEditorExtensionAbility](../reference/apis-ability-kit/js-apis-app-ability-photoEditorExtensionAbility.md)和[PhotoEditorExtensionContext](../reference/apis-ability-kit/js-apis-app-ability-photoEditorExtensionContext.md)。
14
15| **接口名**  | **描述** |
16| -------- | -------- |
17| onStartContentEditing(uri: string, want:Want, session: UIExtensionContentSession):void       | 可以执行读取原始图片、加载页面等操作。|
18| saveEditedContentWithImage(pixelMap: image.PixelMap, option: image.PackingOption): Promise\<AbilityResult\>  | 传入编辑过的图片的PixelMap对象并保存。   |
19
20## 图片编辑类应用实现图片编辑页面
21
221. 在DevEco Studio工程中手动新建一个PhotoEditorExtensionAbility。
23    1. 在工程Module对应的ets目录下,右键选择“New > Directory”,新建一个目录,如PhotoEditorExtensionAbility。
24    2. 在PhotoEditorExtensionAbility目录中,右键选择“New > File”,新建一个.ets文件,如ExamplePhotoEditorAbility.ets252. 在ExamplePhotoEditorAbility.ets中重写onCreate、onForeground、onBackground、onDestroy和onStartContentEditing的生命周期回调。
26
27    其中,需要在onStartContentEditing中加载入口页面文件pages/Index.ets,并将session、uri、实例对象等保存在LocalStorage中传递给页面。
28
29    ```ts
30    import { PhotoEditorExtensionAbility,UIExtensionContentSession,Want } from '@kit.AbilityKit';
31    import { hilog } from '@kit.PerformanceAnalysisKit';
32
33    const TAG = '[ExamplePhotoEditorAbility]';
34    export default class ExamplePhotoEditorAbility extends PhotoEditorExtensionAbility {
35      onCreate() {
36        hilog.info(0x0000, TAG, 'onCreate');
37      }
38
39      // 获取图片,加载页面并将需要的参数传递给页面
40      onStartContentEditing(uri: string, want: Want, session: UIExtensionContentSession): void {
41        hilog.info(0x0000, TAG, `onStartContentEditing want: ${JSON.stringify(want)}, uri: ${uri}`);
42
43        const storage: LocalStorage = new LocalStorage({
44          "session": session,
45          "uri": uri
46        } as Record<string, Object>);
47
48        session.loadContent('pages/Index', storage);
49      }
50
51      onForeground() {
52        hilog.info(0x0000, TAG, 'onForeground');
53      }
54
55      onBackground() {
56        hilog.info(0x0000, TAG, 'onBackground');
57      }
58
59      onDestroy() {
60        hilog.info(0x0000, TAG, 'onDestroy');
61      }
62    }
63
64    ```
653. 在page中实现图片编辑功能。
66
67    图片编辑完成后调用saveEditedContentWithImage保存图片,并将回调结果通过terminateSelfWithResult返回给调用方。
68
69    ```ts
70    import { common } from '@kit.AbilityKit';
71    import { UIExtensionContentSession, Want } from '@kit.AbilityKit';
72    import { hilog } from '@kit.PerformanceAnalysisKit';
73    import { fileIo } from '@kit.CoreFileKit';
74    import { image } from '@kit.ImageKit';
75
76    const storage = LocalStorage.getShared()
77    const TAG = '[ExamplePhotoEditorAbility]';
78
79    @Entry
80    @Component
81    struct Index {
82      @State message: string = 'editImg';
83      @State originalImage: PixelMap | null = null;
84      @State editedImage: PixelMap | null = null;
85      private newWant ?: Want;
86
87      aboutToAppear(): void {
88        let originalImageUri = storage?.get<string>("uri") ?? "";
89        hilog.info(0x0000, TAG, `OriginalImageUri: ${originalImageUri}.`);
90
91        this.readImageByUri(originalImageUri).then(imagePixMap => {
92          this.originalImage = imagePixMap;
93        })
94      }
95
96      // 根据uri读取图片内容
97      async readImageByUri(uri: string): Promise < PixelMap | null > {
98        hilog.info(0x0000, TAG, "uri: " + uri);
99        let file: fileIo.File | undefined;
100        try {
101          file = await fileIo.open(uri, fileIo.OpenMode.READ_ONLY);
102          hilog.info(0x0000, TAG, "Original image file id: " + file.fd);
103
104          let imageSourceApi: image.ImageSource = image.createImageSource(file.fd);
105          if(!imageSourceApi) {
106            hilog.info(0x0000, TAG, "ImageSourceApi failed");
107            return null;
108          }
109          let pixmap: image.PixelMap = await imageSourceApi.createPixelMap();
110          if(!pixmap) {
111            hilog.info(0x0000, TAG, "createPixelMap failed");
112            return null;
113          }
114          this.originalImage = pixmap;
115          return pixmap;
116        } catch(e) {
117          hilog.info(0x0000, TAG, `ReadImage failed:${e}`);
118        } finally {
119          fileIo.close(file);
120        }
121        return null;
122      }
123
124      build() {
125        Row() {
126          Column() {
127            Text(this.message)
128              .fontSize(50)
129              .fontWeight(FontWeight.Bold)
130
131            Button("RotateAndSaveImg").onClick(event => {
132              hilog.info(0x0000, TAG, `Start to edit image and save.`);
133              // 编辑图片功能实现
134              this.originalImage?.rotate(90).then(() => {
135                let packOpts: image.PackingOption = { format: "image/jpeg", quality: 98 };
136                try {
137                  // 调用saveEditedContentWithImage保存图片
138                  (getContext(this) as common.PhotoEditorExtensionContext).saveEditedContentWithImage(this.originalImage as image.PixelMap,
139                    packOpts).then(data => {
140                    if (data.resultCode == 0) {
141                      hilog.info(0x0000, TAG, `Save succeed.`);
142                    }
143                    hilog.info(0x0000, TAG,
144                        `saveContentEditingWithImage result: ${JSON.stringify(data)}`);
145                    this.newWant = data.want;
146                    // data.want.uri存有编辑过图片的uri
147                    this.readImageByUri(this.newWant?.uri ?? "").then(imagePixMap => {
148                      this.editedImage = imagePixMap;
149                    })
150                  })
151                } catch (e) {
152                  hilog.error(0x0000, TAG, `saveContentEditingWithImage failed:${e}`);
153                  return;
154                }
155              })
156            }).margin({ top: 10 })
157
158            Button("terminateSelfWithResult").onClick((event => {
159              hilog.info(0x0000, TAG, `Finish the current editing.`);
160
161              let session = storage.get('session') as UIExtensionContentSession;
162              // 关闭并回传修改结果给调用方
163              session.terminateSelfWithResult({ resultCode: 0, want: this.newWant });
164
165            })).margin({ top: 10 })
166
167            Image(this.originalImage).width("100%").height(200).margin({ top: 10 }).objectFit(ImageFit.Contain)
168
169            Image(this.editedImage).width("100%").height(200).margin({ top: 10 }).objectFit(ImageFit.Contain)
170          }
171          .width('100%')
172        }
173        .height('100%')
174        .backgroundColor(Color.Pink)
175        .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
176      }
177    }
178
179    ```
1804. 在工程Module对应的module.json5配置文件中注册PhotoEditorExtensionAbility。
181
182    type标签需要配置为"photoEditor",srcEntry需要配置为PhotoEditorExtensionAbility组件所对应的代码路径。
183
184    ```json
185    {
186      "module": {
187        "extensionAbilities": [
188          {
189            "name": "ExamplePhotoEditorAbility",
190            "icon": "$media:icon",
191            "description": "ExamplePhotoEditorAbility",
192            "type": "photoEditor",
193            "exported": true,
194            "srcEntry": "./ets/PhotoEditorExtensionAbility/ExamplePhotoEditorAbility.ets",
195            "label": "$string:EntryAbility_label",
196            "extensionProcessMode": "bundle"
197          },
198        ]
199      }
200    }
201    ```
202## 调用方拉起图片编辑类应用编辑图片
203开发者可以在UIAbility或者UIExtensionAbility的页面中通过接口startAbilityByType拉起图片编辑类应用扩展面板,系统将自动查找并在面板上展示基于[PhotoEditorExtensionAbility](../reference/apis-ability-kit/js-apis-app-ability-photoEditorExtensionAbility.md)实现的图片编辑应用,由用户选择某个应用来完成图片编辑的功能,最终将编辑的结果返回给到调用方,具体步骤如下:
2041. 导入模块。
205    ```ts
206    import { common, wantConstant } from '@kit.AbilityKit';
207    import { fileUri, picker } from '@kit.CoreFileKit';
208    ```
2092. (可选)实现从图库中选取图片。
210    ```ts
211    async photoPickerGetUri(): Promise < string > {
212      try {
213        let PhotoSelectOptions = new picker.PhotoSelectOptions();
214        PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
215        PhotoSelectOptions.maxSelectNumber = 1;
216        let photoPicker = new picker.PhotoViewPicker();
217        let photoSelectResult: picker.PhotoSelectResult = await photoPicker.select(PhotoSelectOptions);
218        return photoSelectResult.photoUris[0];
219      } catch(error) {
220        let err: BusinessError = error as BusinessError;
221        hilog.info(0x0000, TAG, 'PhotoViewPicker failed with err: ' + JSON.stringify(err));
222      }
223      return "";
224    }
225    ```
2263. 将图片拷贝到本地沙箱路径。
227   ```ts
228    let context = getContext(this) as common.UIAbilityContext;
229    let file: fileIo.File | undefined;
230    try {
231      file = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY);
232      hilog.info(0x0000, TAG, "file: " + file.fd);
233
234      let timeStamp = Date.now();
235      // 将用户图片拷贝到应用沙箱路径
236      fileIo.copyFileSync(file.fd, context.filesDir + `/original-${timeStamp}.jpg`);
237
238      this.filePath = context.filesDir + `/original-${timeStamp}.jpg`;
239      this.originalImage = fileUri.getUriFromPath(this.filePath);
240    } catch (e) {
241      hilog.info(0x0000, TAG, `readImage failed:${e}`);
242    } finally {
243      fileIo.close(file);
244    }
245   ```
2464. 在startAbilityByType回调函数中,通过want.uri获取编辑后的图片uri,并做对应的处理。
247    ```ts
248      let context = getContext(this) as common.UIAbilityContext;
249      let abilityStartCallback: common.AbilityStartCallback = {
250        onError: (code, name, message) => {
251          const tip: string = `code:` + code + ` name:` + name + ` message:` + message;
252          hilog.error(0x0000, TAG, "startAbilityByType:", tip);
253        },
254        onResult: (result) => {
255          // 获取到回调结果中编辑后的图片uri并做对应的处理
256          let uri = result.want?.uri ?? "";
257          hilog.info(0x0000, TAG, "PhotoEditorCaller result: " + JSON.stringify(result));
258          this.readImage(uri).then(imagePixMap => {
259            this.editedImage = imagePixMap;
260          });
261        }
262      }
263    ```
2645. 将图片转换为图片uri,并调用startAbilityByType拉起图片编辑应用面板。
265   ```ts
266    let uri = fileUri.getUriFromPath(this.filePath);
267    context.startAbilityByType("photoEditor", {
268      "ability.params.stream": [uri], // 原始图片的uri,只支持传入一个uri
269      "ability.want.params.uriPermissionFlag": wantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION // 至少需要分享读权限给到图片编辑面板
270    } as Record<string, Object>, abilityStartCallback, (err) => {
271      let tip: string;
272      if (err) {
273        tip = `Start error: ${JSON.stringify(err)}`;
274        hilog.error(0x0000, TAG, `startAbilityByType: fail, err: ${JSON.stringify(err)}`);
275      } else {
276        tip = `Start success`;
277        hilog.info(0x0000, TAG, "startAbilityByType: ", `success`);
278      }
279    });
280   ```
281
282示例:
283```ts
284import { common, wantConstant } from '@kit.AbilityKit';
285import { fileUri, picker } from '@kit.CoreFileKit';
286import { hilog } from '@kit.PerformanceAnalysisKit';
287import { fileIo } from '@kit.CoreFileKit';
288import { image } from '@kit.ImageKit';
289import { BusinessError } from '@kit.BasicServicesKit';
290import { JSON } from '@kit.ArkTS';
291
292const TAG = 'PhotoEditorCaller';
293
294@Entry
295@Component
296struct Index {
297  @State message: string = 'selectImg';
298  @State originalImage: ResourceStr = "";
299  @State editedImage: PixelMap | null = null;
300  private filePath: string = "";
301
302  // 根据uri读取图片内容
303  async readImage(uri: string): Promise < PixelMap | null > {
304    hilog.info(0x0000, TAG, "image uri: " + uri);
305    let file: fileIo.File | undefined;
306    try {
307      file = await fileIo.open(uri, fileIo.OpenMode.READ_ONLY);
308      hilog.info(0x0000, TAG, "file: " + file.fd);
309
310      let imageSourceApi: image.ImageSource = image.createImageSource(file.fd);
311      if(!imageSourceApi) {
312        hilog.info(0x0000, TAG, "imageSourceApi failed");
313        return null;
314      }
315      let pixmap: image.PixelMap = await imageSourceApi.createPixelMap();
316      if(!pixmap) {
317        hilog.info(0x0000, TAG, "createPixelMap failed");
318        return null;
319      }
320      this.editedImage = pixmap;
321      return pixmap;
322    } catch(e) {
323      hilog.info(0x0000, TAG, `readImage failed:${e}`);
324    } finally {
325      fileIo.close(file);
326    }
327    return null;
328  }
329
330  // 图库中选取图片
331  async photoPickerGetUri(): Promise < string > {
332    try {
333      let PhotoSelectOptions = new picker.PhotoSelectOptions();
334      PhotoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
335      PhotoSelectOptions.maxSelectNumber = 1;
336      let photoPicker = new picker.PhotoViewPicker();
337      let photoSelectResult: picker.PhotoSelectResult = await photoPicker.select(PhotoSelectOptions);
338      hilog.info(0x0000, TAG,
339        'PhotoViewPicker.select successfully, PhotoSelectResult uri: ' + JSON.stringify(photoSelectResult));
340      return photoSelectResult.photoUris[0];
341    } catch(error) {
342      let err: BusinessError = error as BusinessError;
343      hilog.info(0x0000, TAG, 'PhotoViewPicker failed with err: ' + JSON.stringify(err));
344    }
345    return "";
346  }
347
348  build() {
349    Row() {
350      Column() {
351        Text(this.message)
352          .fontSize(50)
353          .fontWeight(FontWeight.Bold)
354
355        Button("selectImg").onClick(event => {
356          // 图库中选取图片
357          this.photoPickerGetUri().then(uri => {
358            hilog.info(0x0000, TAG, "uri: " + uri);
359
360            let context = getContext(this) as common.UIAbilityContext;
361            let file: fileIo.File | undefined;
362            try {
363              file = fileIo.openSync(uri, fileIo.OpenMode.READ_ONLY);
364              hilog.info(0x0000, TAG, "file: " + file.fd);
365
366              let timeStamp = Date.now();
367              // 将用户图片拷贝到应用沙箱路径
368              fileIo.copyFileSync(file.fd, context.filesDir + `/original-${timeStamp}.jpg`);
369
370              this.filePath = context.filesDir + `/original-${timeStamp}.jpg`;
371              this.originalImage = fileUri.getUriFromPath(this.filePath);
372            } catch (e) {
373              hilog.info(0x0000, TAG, `readImage failed:${e}`);
374            } finally {
375              fileIo.close(file);
376            }
377          })
378
379        }).width('200').margin({ top: 20 })
380
381        Button("editImg").onClick(event => {
382          let context = getContext(this) as common.UIAbilityContext;
383          let abilityStartCallback: common.AbilityStartCallback = {
384            onError: (code, name, message) => {
385              const tip: string = `code:` + code + ` name:` + name + ` message:` + message;
386              hilog.error(0x0000, TAG, "startAbilityByType:", tip);
387            },
388            onResult: (result) => {
389              // 获取到回调结果中编辑后的图片uri并做对应的处理
390              let uri = result.want?.uri ?? "";
391              hilog.info(0x0000, TAG, "PhotoEditorCaller result: " + JSON.stringify(result));
392              this.readImage(uri).then(imagePixMap => {
393                this.editedImage = imagePixMap;
394              });
395            }
396          }
397          // 将图片转换为图片uri,并调用startAbilityByType拉起图片编辑应用面板
398          let uri = fileUri.getUriFromPath(this.filePath);
399          context.startAbilityByType("photoEditor", {
400            "ability.params.stream": [uri], // 原始图片的uri,只支持传入一个uri
401            "ability.want.params.uriPermissionFlag": wantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION // 至少需要分享读权限给到图片编辑面板
402          } as Record<string, Object>, abilityStartCallback, (err) => {
403            let tip: string;
404            if (err) {
405              tip = `Start error: ${JSON.stringify(err)}`;
406              hilog.error(0x0000, TAG, `startAbilityByType: fail, err: ${JSON.stringify(err)}`);
407            } else {
408              tip = `Start success`;
409              hilog.info(0x0000, TAG, "startAbilityByType: ", `success`);
410            }
411          });
412
413        }).width('200').margin({ top: 20 })
414
415        Image(this.originalImage).width("100%").height(200).margin({ top: 20 }).objectFit(ImageFit.Contain)
416
417        Image(this.editedImage).width("100%").height(200).margin({ top: 20 }).objectFit(ImageFit.Contain)
418      }
419      .width('100%')
420    }
421    .height('100%')
422    .backgroundColor(Color.Orange)
423    .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
424  }
425}
426
427```