From 5fa004afc042557bcfa44234b25eb387c59f0cc2 Mon Sep 17 00:00:00 2001 From: tsm Date: Fri, 18 Oct 2024 15:31:38 +0800 Subject: [PATCH] =?UTF-8?q?=E9=99=8D=E9=87=87=E6=A0=B7=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: tsm --- CHANGELOG.md | 2 +- README.md | 23 +- .../main/ets/common/CustomEngineKeyImpl.ets | 4 + entry/src/main/ets/pages/DownSamplePage.ets | 212 ++++++++++++++++++ entry/src/main/ets/pages/Index.ets | 5 + .../main/resources/base/element/string.json | 4 + .../resources/base/profile/main_pages.json | 3 +- .../main/resources/zh_CN/element/string.json | 4 + entry/src/ohosTest/ets/test/List.test.ets | 3 +- .../ohosTest/ets/test/SamplingTest.test.ets | 79 +++++++ library/index.ets | 4 +- library/src/main/ets/ImageKnifeDispatcher.ets | 6 +- library/src/main/ets/ImageKnifeLoader.ets | 47 +++- .../ets/components/ImageKnifeComponent.ets | 2 +- .../ets/downsampling/BaseDownsampling.ets | 23 ++ .../ets/downsampling/DownsampleStartegy.ets | 148 ++++++++++++ .../main/ets/downsampling/DownsampleUtils.ets | 58 +++++ .../src/main/ets/downsampling/Downsampler.ets | 73 ++++++ library/src/main/ets/key/DefaultEngineKey.ets | 4 + library/src/main/ets/model/ImageKnifeData.ets | 6 +- .../src/main/ets/model/ImageKnifeOption.ets | 5 + .../src/main/ets/model/ImageKnifeRequest.ets | 7 +- 22 files changed, 712 insertions(+), 10 deletions(-) create mode 100644 entry/src/main/ets/pages/DownSamplePage.ets create mode 100644 entry/src/ohosTest/ets/test/SamplingTest.test.ets create mode 100644 library/src/main/ets/downsampling/BaseDownsampling.ets create mode 100644 library/src/main/ets/downsampling/DownsampleStartegy.ets create mode 100644 library/src/main/ets/downsampling/DownsampleUtils.ets create mode 100644 library/src/main/ets/downsampling/Downsampler.ets diff --git a/CHANGELOG.md b/CHANGELOG.md index 794a2f7..78aa788 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ ## 3.1.1-rc.0 - 重构代码:抽取ImageKnifeDispatcher子线程requestJob相关代码到ImageKnifeLoader中,降低函数复杂度 - +- 降采样功能 ## 3.1.0 - 部分静态webp图片有delay属性导致识别成动图,改用getFrameCount识别 - 修复加载错误图后未去请求排队队列中的请求 diff --git a/README.md b/README.md index bf787e0..23624dd 100644 --- a/README.md +++ b/README.md @@ -264,6 +264,17 @@ ImageKnifeAnimatorComponent({ }),animatorOption:this.animatorOption }).width(300).height(300).backgroundColor(Color.Orange).margin({top:30}) ``` +#### 11.图片降采样 示例 +``` +ImageKnifeComponent({ + imageKnifeOption:new ImageKnifeOption({ + loadSrc:$r("app.media.pngSample"), + placeholderSrc:$r('app.media.loading'), + errorholderSrc:$r('app.media.failed'), + downsampleOf: DownsampleStrategy.NONE + }),animatorOption:this.animatorOption + }).width(300).height(300) +``` #### Reuse Scenario Clear the component content in the **aboutToRecycle** lifecycle and trigger image loading through watch observeration. ## Available APIs @@ -308,7 +319,17 @@ Clear the component content in the **aboutToRecycle** lifecycle and trigger imag | drawingColorFilter | ColorFilter | Drawing color filter. Optional. | | onComplete | (event:EventImage \| undefined)=>void | Callback for image loading completion. Optional. | | onLoadListener | onLoadStart:()=>void,onLoadSuccess:(data:string\|Pixelmap)=>void | Callback for image loading events. Optional. | - +| downsampleOf | DownsampleStrategy | 降采样(可选) | +### 降采样类型 +| 类型 | 相关描述 | +|---------------------|-------------------| +| NONE | 不进行降采样 | +| AT_MOST | 请求尺寸大于实际尺寸不进行放大 | +| FIT_CENTER_MEMORY | 两边自适应内存优先 | +| FIT_CENTER_QUALITY | 两边自适应质量优先 | +| CENTER_OUTSIDE_MEMORY | 宽高缩放比最大的比例,进行缩放适配内存优先 | +| CENTER_OUTSIDE_QUALITY | 宽高缩放比最大的比例,进行缩放适配质量优先 | +| AT_LEAST | 根据宽高的最小的比例,进行适配 | ### ImageKnife | Parameter | Type | Description | diff --git a/entry/src/main/ets/common/CustomEngineKeyImpl.ets b/entry/src/main/ets/common/CustomEngineKeyImpl.ets index 06f2901..63437c3 100644 --- a/entry/src/main/ets/common/CustomEngineKeyImpl.ets +++ b/entry/src/main/ets/common/CustomEngineKeyImpl.ets @@ -12,6 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { DownsampleStrategy } from '@ohos/imageknife'; import { IEngineKey, ImageKnifeOption, PixelMapTransformation,SparkMD5 ,ImageKnifeRequestSource} from '@ohos/libraryimageknife'; //全局自定义key demo @@ -35,6 +36,9 @@ export class CustomEngineKeyImpl implements IEngineKey { if (imageKnifeOption.transformation) { key += "transformation=" + this.getTransformation(imageKnifeOption.transformation) + ";" } + if ((imageKnifeOption.downsampleOf !== DownsampleStrategy.NONE && imageKnifeOption.downsampleOf !== undefined)) { + key += "downsampleOf" + imageKnifeOption.downsampleOf +"width="+width+"height="+ height + } } return key } diff --git a/entry/src/main/ets/pages/DownSamplePage.ets b/entry/src/main/ets/pages/DownSamplePage.ets new file mode 100644 index 0000000..aeaa9af --- /dev/null +++ b/entry/src/main/ets/pages/DownSamplePage.ets @@ -0,0 +1,212 @@ +/* + * Copyright (C) 2024 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 { DownsampleStrategy, ImageKnifeOption, } from '@ohos/imageknife'; +import { ImageKnifeComponent } from '@ohos/libraryimageknife'; +import { image } from '@kit.ImageKit'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { Downsampler } from '@ohos/imageknife/src/main/ets/downsampling/Downsampler'; +import { FileTypeUtil } from '@ohos/imageknife/src/main/ets/utils/FileTypeUtil'; + +@Entry +@ComponentV2 +struct DownSamplePage { + @Local imageKnifeOption: ImageKnifeOption = new ImageKnifeOption({ + loadSrc: $r('app.media.startIcon'), + placeholderSrc: $r("app.media.loading"), + errorholderSrc: $r("app.media.app_icon"), + objectFit: ImageFit.Contain + }) + isBrightness: boolean = false + @Local beforeSampling: number = 0 + @Local afterSampling: number = 0 + @Local SamplingList: SamplingType[] = [ + + new SamplingType(7, "AT_LEAST"), + new SamplingType(1, "AT_MOST"), + + new SamplingType(2, "FIT_CENTER_MEMORY"), + new SamplingType(4, "FIT_CENTER_QUALITY"), + new SamplingType(5, "CENTER_OUTSIDE_MEMORY"), + new SamplingType(6, "CENTER_OUTSIDE_QUALITY"), + new SamplingType(0, "NONE"), + + ] + @Local checked: boolean = false + + updateImageKnifeOption(value: string) { + if (value === 'NONE') { + this.imageKnifeOption = new ImageKnifeOption({ + loadSrc: $r('app.media.pngSample'), + placeholderSrc: $r("app.media.loading"), + errorholderSrc: $r("app.media.app_icon"), + objectFit: ImageFit.Contain, + downsampleOf: DownsampleStrategy.NONE + }) + this.originalPixMap($r('app.media.pngSample')) + this.afterSamplingFunc($r('app.media.pngSample')) + } else if (value === 'AT_MOST') { + this.imageKnifeOption = new ImageKnifeOption({ + loadSrc: $r('app.media.pngSample'), + placeholderSrc: $r("app.media.loading"), + errorholderSrc: $r("app.media.app_icon"), + objectFit: ImageFit.Contain, + downsampleOf: DownsampleStrategy.AT_MOST + }) + this.originalPixMap($r('app.media.pngSample')) + this.afterSamplingFunc($r('app.media.pngSample')) + } else if (value === 'FIT_CENTER_MEMORY') { + this.imageKnifeOption = new ImageKnifeOption({ + loadSrc: $r('app.media.pngSample'), + placeholderSrc: $r("app.media.loading"), + errorholderSrc: $r("app.media.app_icon"), + objectFit: ImageFit.Contain, + downsampleOf: DownsampleStrategy.FIT_CENTER_MEMORY + }) + this.originalPixMap($r('app.media.pngSample')) + this.afterSamplingFunc($r('app.media.pngSample')) + } else if (value === 'FIT_CENTER_QUALITY') { + this.imageKnifeOption = new ImageKnifeOption({ + loadSrc: $r('app.media.pngSample'), + placeholderSrc: $r("app.media.loading"), + errorholderSrc: $r("app.media.app_icon"), + objectFit: ImageFit.Contain, + downsampleOf: DownsampleStrategy.FIT_CENTER_QUALITY + }) + this.originalPixMap($r('app.media.pngSample')) + this.afterSamplingFunc($r('app.media.pngSample')) + } else if (value === 'CENTER_OUTSIDE_MEMORY') { + this.imageKnifeOption = new ImageKnifeOption({ + loadSrc: $r('app.media.pngSample'), + placeholderSrc: $r("app.media.loading"), + errorholderSrc: $r("app.media.app_icon"), + objectFit: ImageFit.Contain, + downsampleOf: DownsampleStrategy.CENTER_OUTSIDE_MEMORY + }) + this.originalPixMap($r('app.media.pngSample')) + this.afterSamplingFunc($r('app.media.pngSample')) + } else if (value === 'CENTER_OUTSIDE_QUALITY') { + this.imageKnifeOption = new ImageKnifeOption({ + loadSrc: $r('app.media.pngSample'), + placeholderSrc: $r("app.media.loading"), + errorholderSrc: $r("app.media.app_icon"), + objectFit: ImageFit.Contain, + downsampleOf: DownsampleStrategy.CENTER_OUTSIDE_QUALITY + }) + this.originalPixMap($r('app.media.pngSample')) + this.afterSamplingFunc($r('app.media.pngSample')) + } else { + this.imageKnifeOption = new ImageKnifeOption({ + loadSrc: $r('app.media.pngSample'), + placeholderSrc: $r("app.media.loading"), + errorholderSrc: $r("app.media.app_icon"), + objectFit: ImageFit.Contain, + downsampleOf: DownsampleStrategy.AT_LEAST + }) + this.originalPixMap($r('app.media.pngSample')) + this.afterSamplingFunc($r('app.media.pngSample')) + } + } + + async afterSamplingFunc(imgs: Resource) { + let img: Uint8Array = await getContext(this).resourceManager.getMediaContent(imgs); + let imageSource: image.ImageSource = image.createImageSource(img.buffer.slice(0)); + let fileTypeUtil = new FileTypeUtil(); + let typeValue = fileTypeUtil.getFileType(img.buffer.slice(0)) as string; + let decodingOptions: image.DecodingOptions = { + editable: true, + desiredPixelFormat: 3, + } + let imageInfo = await imageSource.getImageInfo() + + if (this.imageKnifeOption.downsampleOf !== DownsampleStrategy.NONE){ + let reqSize = + new Downsampler().calculateScaling(typeValue, imageInfo.size.width, imageInfo.size.height, 300, + 300, this.imageKnifeOption.downsampleOf) + decodingOptions = { + editable: true, + desiredSize: { + width: reqSize.width, + height: reqSize.height + } + } + } + + // 创建pixelMap + imageSource.createPixelMap(decodingOptions).then((pixelMap: image.PixelMap) => { + this.afterSampling = pixelMap.getPixelBytesNumber() + }).catch((err: BusinessError) => { + console.error("Failed to create PixelMap") + }); + } + + async originalPixMap(imgs: Resource,) { + let img: Uint8Array = await getContext(this).resourceManager.getMediaContent(imgs); + let imageSource: image.ImageSource = image.createImageSource(img.buffer.slice(0)); + let decodingOptions: image.DecodingOptions = { + editable: true, + desiredPixelFormat: 3, + } + // 创建pixelMap + imageSource.createPixelMap(decodingOptions).then((pixelMap: image.PixelMap) => { + this.beforeSampling = pixelMap.getPixelBytesNumber() + }).catch((err: BusinessError) => { + console.error("Failed to create PixelMap") + }); + } + + build() { + Scroll() { + Column() { + ForEach(this.SamplingList, (item: SamplingType, index) => { + Flex({ justifyContent: FlexAlign.Center, alignItems: ItemAlign.Center }) { + Radio({ value: item.value + 'radio', group: 'radioGroup' }) + .height(50) + .width(50) + .checked(this.checked) + .onClick(() => { + this.updateImageKnifeOption(item.value) + }) + Text("降采样规格:" + item.value).fontSize(20) + } + }, (item: SamplingType) => JSON.stringify(item)) + Column() { + Text(`未降采样大小:${this.beforeSampling}`).fontSize(20) + Text(`降采样后大小:${this.afterSampling}`).fontSize(20) + } + + ImageKnifeComponent({ + imageKnifeOption: this.imageKnifeOption + }) + .height(300) + .width(300) + .borderWidth(1) + .borderColor(Color.Pink) + } + + } + .height('100%') + .width('100%') + } +} + +class SamplingType { + key: number + value: string + + constructor(key: number, value: string) { + this.key = key + this.value = value + } +} \ No newline at end of file diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index 75fa2f2..458ef5a 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -52,6 +52,11 @@ struct Index { }); }) + Button($r('app.string.Image_Downsampling_Functionality')).margin({top:10}).onClick(()=>{ + router.push({ + uri: 'pages/DownSamplePage', + }); + }) Button($r('app.string.Display_long_image')).margin({top:10}).onClick(()=>{ router.push({ uri: 'pages/LongImagePage', diff --git a/entry/src/main/resources/base/element/string.json b/entry/src/main/resources/base/element/string.json index f982e81..aff88e0 100644 --- a/entry/src/main/resources/base/element/string.json +++ b/entry/src/main/resources/base/element/string.json @@ -367,6 +367,10 @@ { "name": "TIPS", "value": "Please shut down the network first and ensure that there is no cache of images from this network in the test failure scenario locally" + }, + { + "name": "Image_Downsampling_Functionality", + "value": "Downscale Image effect" } ] } \ 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 index d42cd2d..edf3108 100644 --- a/entry/src/main/resources/base/profile/main_pages.json +++ b/entry/src/main/resources/base/profile/main_pages.json @@ -22,6 +22,7 @@ "pages/ImageAnimatorPage", "pages/TestSetCustomImagePage", "pages/TestErrorHolderPage", - "pages/TestTaskResourcePage" + "pages/TestTaskResourcePage", + "pages/DownSamplePage" ] } \ No newline at end of file diff --git a/entry/src/main/resources/zh_CN/element/string.json b/entry/src/main/resources/zh_CN/element/string.json index 74fedfe..bd30a41 100644 --- a/entry/src/main/resources/zh_CN/element/string.json +++ b/entry/src/main/resources/zh_CN/element/string.json @@ -363,6 +363,10 @@ { "name": "TIPS", "value": "测试失败场景请先关闭网络,并保证本地没有此网络图片的缓存" + }, + { + "name": "Image_Downsampling_Functionality", + "value": "降采样功能" } ] } \ No newline at end of file diff --git a/entry/src/ohosTest/ets/test/List.test.ets b/entry/src/ohosTest/ets/test/List.test.ets index 8c04309..7bbeda5 100644 --- a/entry/src/ohosTest/ets/test/List.test.ets +++ b/entry/src/ohosTest/ets/test/List.test.ets @@ -18,7 +18,7 @@ import ImageKnifeOptionTest from './ImageKnifeOption.test'; import MemoryLruCacheTest from './MemoryLruCache.test'; import ImageKnifeTest from './ImageKnife.test'; import Transform from './transform.test'; - +import SamplingTest from './SamplingTest.test'; export default function testsuite() { MemoryLruCacheTest(); FileLruCacheTest(); @@ -26,4 +26,5 @@ export default function testsuite() { ImageKnifeOptionTest(); ImageKnifeTest(); Transform(); + SamplingTest() } \ No newline at end of file diff --git a/entry/src/ohosTest/ets/test/SamplingTest.test.ets b/entry/src/ohosTest/ets/test/SamplingTest.test.ets new file mode 100644 index 0000000..074057d --- /dev/null +++ b/entry/src/ohosTest/ets/test/SamplingTest.test.ets @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2024 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 { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium' +import { image } from '@kit.ImageKit' +import { BusinessError } from '@kit.BasicServicesKit' +import { calculateScaleType, Downsampler } from '@ohos/imageknife/src/main/ets/downsampling/Downsampler' +import { DownsampleStrategy } from '@ohos/imageknife' + +export default function SamplingTest() { + describe('SamplingTest', () => { + // Defines a test suite. Two parameters are supported: test suite name and test suite function. + beforeAll(() => { + // Presets an action, which is performed only once before all test cases of the test suite start. + // This API supports only one parameter: preset action function. + }) + beforeEach(() => { + // Presets an action, which is performed before each unit test case starts. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: preset action function. + }) + afterEach(() => { + // Presets a clear action, which is performed after each unit test case ends. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: clear action function. + }) + afterAll(() => { + // Presets a clear action, which is performed after all test cases of the test suite end. + // This API supports only one parameter: clear action function. + }) + it('NONE', 0, () => { + let reqSize: calculateScaleType = + new Downsampler().calculateScaling('jpg', 1024, 1024, 200, + 200, DownsampleStrategy.NONE,) + let req = (reqSize.targetWidth == 1024 && reqSize.targetHeight == 1024) + expect(req).assertEqual(true); + }) + it('AT_MOST', 1, () => { + let reqSize: calculateScaleType = + new Downsampler().calculateScaling('jpg', 1024, 1024, 200, + 200, DownsampleStrategy.AT_MOST) + let req = (reqSize.targetWidth < 1024 && reqSize.targetHeight < 1024) + expect(req).assertEqual(true); + }) + it('FIT_CENTER', 2, () => { + let reqSize: calculateScaleType = + new Downsampler().calculateScaling('jpg', 1024, 1024, 200, + 200, DownsampleStrategy.FIT_CENTER_MEMORY) + let req = (reqSize.targetWidth < 1024 && reqSize.targetHeight < 1024) + expect(req).assertEqual(true); + }) + it('CENTER_OUTSIDE', 3, () => { + let reqSize: calculateScaleType = + new Downsampler().calculateScaling('jpg', 1024, 1024, 200, + 200, DownsampleStrategy.CENTER_OUTSIDE_MEMORY) + let req = (reqSize.targetWidth < 1024 && reqSize.targetHeight < 1024) + expect(req).assertEqual(true); + }) + it('AT_LEAST', 4, () => { + let reqSize: calculateScaleType = + new Downsampler().calculateScaling('jpg', 1024, 1024, 200, + 200, DownsampleStrategy.AT_LEAST) + let req = (reqSize.targetWidth < 1024 && reqSize.targetHeight < 1024) + expect(req).assertEqual(true); + }) + + }) +} diff --git a/library/index.ets b/library/index.ets index 1f67f70..058e860 100644 --- a/library/index.ets +++ b/library/index.ets @@ -66,4 +66,6 @@ export { CropTransformation } from './src/main/ets/transform/CropTransformation' export { MaskTransformation } from './src/main/ets/transform/MaskTransformation' -export { SepiaTransformation } from './src/main/ets/transform/SepiaTransformation' \ No newline at end of file +export { SepiaTransformation } from './src/main/ets/transform/SepiaTransformation' + +export { DownsampleStrategy } from './src/main/ets/downsampling/DownsampleStartegy' \ No newline at end of file diff --git a/library/src/main/ets/ImageKnifeDispatcher.ets b/library/src/main/ets/ImageKnifeDispatcher.ets index fbbb3a8..e2e1466 100644 --- a/library/src/main/ets/ImageKnifeDispatcher.ets +++ b/library/src/main/ets/ImageKnifeDispatcher.ets @@ -35,6 +35,7 @@ import { } from './model/ImageKnifeData' import { BusinessError } from '@kit.BasicServicesKit'; import { ImageKnifeLoader } from './ImageKnifeLoader' +import { DownsampleStrategy } from './downsampling/DownsampleStartegy'; export class ImageKnifeDispatcher { @@ -183,7 +184,10 @@ export class ImageKnifeDispatcher { fileCacheFolder: ImageKnife.getInstance().getFileCache()?.getCacheFolder(), isAnimator:isAnimator, moduleName: moduleName == "" ? undefined : moduleName, - resName: resName == "" ? undefined : resName + resName: resName == "" ? undefined : resName, + targetWidth: currentRequest.componentWidth, + targetHeight: currentRequest.componentHeight, + downsampType: currentRequest.imageKnifeOption.downsampleOf==undefined?DownsampleStrategy.NONE:currentRequest.imageKnifeOption.downsampleOf, } if(request.customGetImage == undefined) { diff --git a/library/src/main/ets/ImageKnifeLoader.ets b/library/src/main/ets/ImageKnifeLoader.ets index 1c14d0c..b00e5fb 100644 --- a/library/src/main/ets/ImageKnifeLoader.ets +++ b/library/src/main/ets/ImageKnifeLoader.ets @@ -28,6 +28,8 @@ import emitter from '@ohos.events.emitter'; import image from '@ohos.multimedia.image'; import { RequestJobResult } from './model/ImageKnifeData' import util from '@ohos.util'; +import { DownsampleStrategy } from './downsampling/DownsampleStartegy'; +import { Downsampler } from './downsampling/Downsampler'; class RequestData { receiveSize: number = 2000 @@ -73,6 +75,15 @@ export class ImageKnifeLoader { } let size = (await imageSource.getImageInfo()).size + try { + if ((request.downsampType !== DownsampleStrategy.NONE) && + request.requestSource == ImageKnifeRequestSource.SRC) { + decodingOptions = ImageKnifeLoader.getDownsamplerDecodingOptions(typeValue, request, size, ImageKnifeRequestSource.SRC) + } + } catch (err) { + return ImageKnifeLoader.makeEmptyResult(err) + } + await imageSource.createPixelMap(decodingOptions) .then((pixelmap: PixelMap) => { resPixelmap = pixelmap @@ -110,6 +121,14 @@ export class ImageKnifeLoader { editable: true, desiredSize: defaultSize }; + try { + if ((request.downsampType !== DownsampleStrategy.NONE) && + request.requestSource == ImageKnifeRequestSource.SRC) { + opts = ImageKnifeLoader.getDownsamplerDecodingOptions(typeValue, request, size) + } + } catch (err) { + return ImageKnifeLoader.makeEmptyResult(err) + } await imageSource.createPixelMap(opts) .then((pixelmap: PixelMap) => { resPixelmap = pixelmap @@ -354,4 +373,30 @@ export class ImageKnifeLoader { } return resBuf } -} \ No newline at end of file + + static getDownsamplerDecodingOptions(typeValue: string, request: RequestJobRequest, size: Size, + SRC?: ImageKnifeRequestSource):image.DecodingOptions { + let reqSize = + new Downsampler().calculateScaling(typeValue, size.width, size.height, request.targetWidth, request.targetHeight, + request.downsampType) + if (typeValue == "svg") { + return { + editable: true, + desiredSize: { + height: vp2px(reqSize.height), + width: vp2px(reqSize.width) + } + + } + } else { + return { + editable: request.requestSource === SRC && request.transformation !== undefined ? true : false, + desiredSize:{ + width: reqSize.width, + height: reqSize.height + } + } + } + } +} + diff --git a/library/src/main/ets/components/ImageKnifeComponent.ets b/library/src/main/ets/components/ImageKnifeComponent.ets index f96cecc..59812e9 100644 --- a/library/src/main/ets/components/ImageKnifeComponent.ets +++ b/library/src/main/ets/components/ImageKnifeComponent.ets @@ -38,7 +38,7 @@ export struct ImageKnifeComponent { @Param imageKnifeOption: ImageKnifeOption = new ImageKnifeOption(); @Monitor('imageKnifeOption', - "imageKnifeOption.loadSrc","imageKnifeOption.signature","imageKnifeOption.transformation","imageKnifeOption.border","imageKnifeOption.objectFit") + "imageKnifeOption.loadSrc","imageKnifeOption.signature","imageKnifeOption.transformation","imageKnifeOption.border","imageKnifeOption.objectFit",'imageKnifeOption.downsampleOf') watchImageKnifeOption() { this.clearLastRequest() this.componentVersion++ diff --git a/library/src/main/ets/downsampling/BaseDownsampling.ets b/library/src/main/ets/downsampling/BaseDownsampling.ets new file mode 100644 index 0000000..439a1cb --- /dev/null +++ b/library/src/main/ets/downsampling/BaseDownsampling.ets @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 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 { DownsampleStrategy } from './DownsampleStartegy'; + +import { SampleSizeRounding } from './DownsampleUtils'; + +export interface BaseDownsampling { + getName(): string + + getScaleFactor(sourceWidth: number, sourceHeight: number, requestWidth: number, requestHeight: number,downsampType?:DownsampleStrategy): number +} \ No newline at end of file diff --git a/library/src/main/ets/downsampling/DownsampleStartegy.ets b/library/src/main/ets/downsampling/DownsampleStartegy.ets new file mode 100644 index 0000000..0446568 --- /dev/null +++ b/library/src/main/ets/downsampling/DownsampleStartegy.ets @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2024 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 { BaseDownsampling } from './BaseDownsampling'; +import { getScale, highestOneBit, round, SampleSizeRounding } from './DownsampleUtils'; + +export class FitCenter implements BaseDownsampling { + getName() { + return "FitCenter" + } + + getScaleFactor(sourceWidth: number, sourceHeight: number, requestedWidth: number, requestedHeight: number, + downsampType: DownsampleStrategy + ): number { + //重新计算宽高比; + let outSize: Size = { + width: round(getScale(sourceWidth, sourceHeight, requestedWidth, requestedHeight, downsampType) * sourceWidth), + height:round(getScale(sourceWidth, sourceHeight, requestedWidth, requestedHeight, downsampType) * sourceHeight) + } + let scaleFactor = downsampType === DownsampleStrategy.FIT_CENTER_QUALITY? + Math.max(1, highestOneBit(Math.max(sourceWidth / outSize.width, sourceHeight / outSize.height))) : + Math.max(1, highestOneBit(Math.min(sourceWidth / outSize.width, sourceHeight / outSize.height)))//将整型的缩放因子转换为2的次幂采样大小 + + if (downsampType === DownsampleStrategy.FIT_CENTER_MEMORY + && (scaleFactor < (1 / getScale(sourceWidth, sourceHeight, requestedWidth, requestedHeight, downsampType)))) { + scaleFactor = scaleFactor << 1; + } + return scaleFactor + } +} + + +/*宽高进行等比缩放宽高里面最小的比例先放进去 +然后再更据原图的缩放比去适配另一边*/ + +export class CenterOutside implements BaseDownsampling { + getName() { + return "CenterOutside" + } + + getScaleFactor(sourceWidth: number, sourceHeight: number, requestedWidth: number, requestedHeight: number): number { + const widthPercentage = requestedWidth / sourceWidth; + const heightPercentage = requestedHeight / sourceHeight; + //返回宽度和高度比例中最大的值 + let outSize: Size = { + width: round(Math.max(widthPercentage, heightPercentage) * sourceWidth), + height:round(Math.max(widthPercentage, heightPercentage) * sourceHeight) + } + let scaleFactor = Math.max(1, highestOneBit(Math.max(sourceWidth / outSize.width, sourceHeight / outSize.height))) + + return scaleFactor + } +} + +/*请求尺寸大于实际尺寸不进行放大,按照原图展示*/ + +export class AtMost implements BaseDownsampling { + getName() { + return "AtMost" + } + + + getScaleFactor(sourceWidth: number, sourceHeight: number, requestedWidth: number, requestedHeight: number): number { + const maxIntegerFactor = Math.ceil(Math.max(sourceHeight / requestedHeight, sourceWidth / requestedWidth)); + let lesserOrEqualSampleSize = Math.max(1, highestOneBit(maxIntegerFactor)); + let greaterOrEqualSampleSize = lesserOrEqualSampleSize + if (lesserOrEqualSampleSize < maxIntegerFactor) { + greaterOrEqualSampleSize = lesserOrEqualSampleSize <<= 1; + } + greaterOrEqualSampleSize = lesserOrEqualSampleSize << (lesserOrEqualSampleSize < maxIntegerFactor ? 1 : 0) + + let outSize: Size = { + width: round((1 / greaterOrEqualSampleSize) * sourceWidth), + height:round((1 / greaterOrEqualSampleSize) * sourceHeight) + } + let scaleFactor = Math.max(1, highestOneBit(Math.min(sourceWidth / outSize.width, sourceHeight / outSize.height))) + if ((scaleFactor < greaterOrEqualSampleSize)) { + scaleFactor = scaleFactor << 1; + } + return scaleFactor + } +} + +/*宽高进行等比缩放宽高里面最大的比例先放进去 +然后再更据原图的缩放比去适配另一边*/ +export class CenterInside implements BaseDownsampling { + getName() { + return "CenterInside" + } + getScaleFactor(sourceWidth: number, sourceHeight: number, requestedWidth: number, requestedHeight: number, + downsampType: DownsampleStrategy + ): number { + let outSize: Size = { + width: round(Math.min(1, getScale(sourceWidth, sourceHeight, requestedWidth, requestedHeight, downsampType)) * sourceWidth), + height:round(Math.min(1, getScale(sourceWidth, sourceHeight, requestedWidth, requestedHeight, downsampType)) * sourceHeight) + } + //将整型的缩放因子转换为2的次幂采样大小 + let scaleFactor = this.getSampleSizeType(sourceWidth, sourceHeight, requestedWidth, requestedHeight, + downsampType) == SampleSizeRounding.QUALITY ? + Math.max(1, highestOneBit(Math.max(sourceWidth / outSize.width, sourceHeight / outSize.height))) : + Math.max(1, highestOneBit(Math.min(sourceWidth / outSize.width, sourceHeight / outSize.height))) + if (this.getSampleSizeType(sourceWidth, sourceHeight, requestedWidth, requestedHeight, downsampType) + == SampleSizeRounding.MEMORY && (scaleFactor < (1 / Math.min(1, getScale(sourceWidth, sourceHeight, requestedWidth, requestedHeight, downsampType))))) { + scaleFactor = scaleFactor << 1; + } + return scaleFactor + + } + + getSampleSizeType(sourceWidth: number, sourceHeight: number, requestedWidth: number, requestedHeight: number, + downsampType: DownsampleStrategy + ): SampleSizeRounding { + //如果缩放因子为 1,表示没有缩放,优先选择质量 + if (Math.min(1, getScale(sourceWidth, sourceHeight, requestedWidth, requestedHeight, downsampType)) === 1) { + return SampleSizeRounding.QUALITY + } + //否则,使用 FIL_CENTER 的 SampleSizeRounding 值 + return downsampType === DownsampleStrategy.CENTER_OUTSIDE_MEMORY?SampleSizeRounding.MEMORY:SampleSizeRounding.QUALITY + } +} + +export enum DownsampleStrategy { + //请求尺寸大于实际尺寸不进行放大 + AT_MOST, + //两边自适应内存优先 + FIT_CENTER_MEMORY, + //两边自适应质量优先 + FIT_CENTER_QUALITY, + //按照宽高比的最大比进行适配内存优先 + CENTER_OUTSIDE_MEMORY, + //按照宽高比的最大比进行适配质量优先 + CENTER_OUTSIDE_QUALITY, + //宽高进行等比缩放宽高里面最小的比例先放进去,然后再根据原图的缩放比去适配 + AT_LEAST, + //不进行降采样 + NONE, +} \ No newline at end of file diff --git a/library/src/main/ets/downsampling/DownsampleUtils.ets b/library/src/main/ets/downsampling/DownsampleUtils.ets new file mode 100644 index 0000000..567026a --- /dev/null +++ b/library/src/main/ets/downsampling/DownsampleUtils.ets @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2024 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 { DownsampleStrategy } from './DownsampleStartegy'; + +export enum SampleSizeRounding { + /** + * Prefer to round the sample size up so that the image is downsampled to smaller than the + * requested size to use less memory. + */ + //(内存优先) + MEMORY, + /** + * Prefer to round the sample size down so that the image is downsampled to larger than the + * requested size to maintain quality at the expense of extra memory usage. + */ + //(质量优先) + QUALITY +} +//找出给定整数 i 中最高位的1(即最左边的1)所代表的值 +export function highestOneBit(i: number): number { + i |= (i >> 1); + i |= (i >> 2); + i |= (i >> 4); + i |= (i >> 8); + i |= (i >> 16); + return i - (i >>> 1); +} + + +export function getScale(sourceWidth: number, sourceHeight: number, requestedWidth: number, requestedHeight: number, + downsampType: DownsampleStrategy +): number { + if (downsampType === DownsampleStrategy.FIT_CENTER_MEMORY) { + const widthPercentage = requestedWidth / sourceWidth + const heightPercentage = requestedHeight / sourceHeight + return Math.min(widthPercentage, heightPercentage) + } else { + const maxIntegerFactor = Math.max(sourceHeight / requestedHeight, sourceWidth / requestedWidth); + return maxIntegerFactor === 0 ? 1 : 1 / highestOneBit(maxIntegerFactor); + + } +} +//四舍五入 +export function round(value: number): number { + return Math.floor(value + 0.5); +} \ No newline at end of file diff --git a/library/src/main/ets/downsampling/Downsampler.ets b/library/src/main/ets/downsampling/Downsampler.ets new file mode 100644 index 0000000..2a0a49f --- /dev/null +++ b/library/src/main/ets/downsampling/Downsampler.ets @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2024 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 { + AtMost, + CenterInside, + CenterOutside, + DownsampleStrategy, + FitCenter, +} from './DownsampleStartegy'; +export class Downsampler { + calculateScaling( + typeValue: string, + sourceWidth: number, //原始宽高 + sourceHeight: number, //原始宽高 + requestWidth: number, //请求宽高 + requestHeight: number, //请求宽高 + downsampType: DownsampleStrategy, + ): Size { + + if (sourceHeight <= 0 || sourceWidth <= 0) { + throw new Error(`Invalid width and height, sourceHeight:${sourceHeight}+ sourceWidth:${sourceWidth}`) + } + let downsampler = this.getDownsampler(downsampType); + let scaleFactor: number = + downsampler.getScaleFactor(sourceWidth, sourceHeight, requestWidth, requestHeight, downsampType);//缩放比 + //基于上一步得出的采样大小,根据不同的图片类型,计算采样后的图片尺寸 + if (typeValue === "png") { + return { + width: Math.floor(sourceWidth / scaleFactor), + height: Math.floor(sourceHeight / scaleFactor) + } + } else if (typeValue === "webp") { + return { + width: Math.round(sourceWidth / scaleFactor), + height: Math.round(sourceHeight / scaleFactor) + } + } else { + return { + width: sourceWidth / scaleFactor, + height: sourceHeight / scaleFactor + + } + } + } + getDownsampler(downsampType: DownsampleStrategy) { + switch (downsampType) { + case DownsampleStrategy.FIT_CENTER_MEMORY: + case DownsampleStrategy.FIT_CENTER_QUALITY: + return new FitCenter(); + case DownsampleStrategy.AT_MOST: + return new AtMost(); + case DownsampleStrategy.CENTER_OUTSIDE_MEMORY: + case DownsampleStrategy.CENTER_OUTSIDE_QUALITY: + return new CenterInside(); + case DownsampleStrategy.AT_LEAST: + return new CenterOutside(); + default: + throw new Error('Unsupported downsampling strategy'); + } + } +} diff --git a/library/src/main/ets/key/DefaultEngineKey.ets b/library/src/main/ets/key/DefaultEngineKey.ets index f28024b..12f010f 100644 --- a/library/src/main/ets/key/DefaultEngineKey.ets +++ b/library/src/main/ets/key/DefaultEngineKey.ets @@ -17,6 +17,7 @@ import { ImageKnifeOption } from '../model/ImageKnifeOption'; import { IEngineKey } from './IEngineKey'; import { PixelMapTransformation } from '../transform/PixelMapTransformation'; import { ImageKnifeRequestSource } from '../model/ImageKnifeData'; +import { DownsampleStrategy } from '../downsampling/DownsampleStartegy'; @Sendable export class DefaultEngineKey implements IEngineKey { @@ -31,6 +32,9 @@ export class DefaultEngineKey implements IEngineKey { if (imageKnifeOption.transformation) { key += "transformation=" + this.getTransformation(imageKnifeOption.transformation) + ";" } + if ((imageKnifeOption.downsampleOf !== DownsampleStrategy.NONE && imageKnifeOption.downsampleOf !== undefined)) { + key += "downsampleOf" + imageKnifeOption.downsampleOf +"width="+width+"height="+ height + } } return key } diff --git a/library/src/main/ets/model/ImageKnifeData.ets b/library/src/main/ets/model/ImageKnifeData.ets index e8a4c2a..8fb3b61 100644 --- a/library/src/main/ets/model/ImageKnifeData.ets +++ b/library/src/main/ets/model/ImageKnifeData.ets @@ -18,6 +18,7 @@ import { IEngineKey } from '../key/IEngineKey' import { PixelMapTransformation } from '../transform/PixelMapTransformation' import common from '@ohos.app.ability.common'; import { Size } from '@kit.ArkUI' +import { DownsampleStrategy } from '../downsampling/DownsampleStartegy' export interface ImageKnifeData { source: PixelMap | string, @@ -103,6 +104,9 @@ export interface RequestJobRequest { fileCacheFolder: string, isAnimator?: boolean, moduleName?:string, - resName?: string + resName?: string, + targetWidth: number + targetHeight: number + downsampType: DownsampleStrategy } diff --git a/library/src/main/ets/model/ImageKnifeOption.ets b/library/src/main/ets/model/ImageKnifeOption.ets index 5c5f292..4e42b00 100644 --- a/library/src/main/ets/model/ImageKnifeOption.ets +++ b/library/src/main/ets/model/ImageKnifeOption.ets @@ -17,6 +17,7 @@ import common from '@ohos.app.ability.common' import { CacheStrategy, ImageKnifeData,EventImage } from './ImageKnifeData'; import { PixelMapTransformation } from '../transform/PixelMapTransformation'; import { drawing } from '@kit.ArkGraphics2D'; +import { DownsampleStrategy } from '../downsampling/DownsampleStartegy'; export interface HeaderOptions { key: string; @@ -90,6 +91,7 @@ interface ImageOption { onLoadListener?: OnLoadCallBack | undefined; onComplete?:(event:EventImage | undefined) => void drawingColorFilter?: ColorFilter | drawing.ColorFilter + downsampleOf?: DownsampleStrategy } @ObservedV2 export class ImageKnifeOption { @@ -121,6 +123,8 @@ export class ImageKnifeOption { onLoadListener?: OnLoadCallBack | undefined; onComplete?:(event:EventImage | undefined) => void drawingColorFilter?: ColorFilter | drawing.ColorFilter + // 下采样 + @Trace downsampleOf: DownsampleStrategy = DownsampleStrategy.NONE constructor(option?:ImageOption) { this.loadSrc = option?.loadSrc == undefined ? "" : option?.loadSrc this.placeholderSrc = option?.placeholderSrc @@ -141,6 +145,7 @@ export class ImageKnifeOption { this.onLoadListener = option?.onLoadListener this.onComplete = option?.onComplete this.drawingColorFilter = option?.drawingColorFilter + this.downsampleOf = option?.downsampleOf==undefined?DownsampleStrategy.NONE:option?.downsampleOf } } diff --git a/library/src/main/ets/model/ImageKnifeRequest.ets b/library/src/main/ets/model/ImageKnifeRequest.ets index c63f8be..530b707 100644 --- a/library/src/main/ets/model/ImageKnifeRequest.ets +++ b/library/src/main/ets/model/ImageKnifeRequest.ets @@ -15,6 +15,7 @@ import { ImageKnifeOption } from './ImageKnifeOption'; import common from '@ohos.app.ability.common'; import { ImageKnifeRequestSource } from './ImageKnifeData'; +import { DownsampleStrategy } from '../downsampling/DownsampleStartegy'; export class ImageKnifeRequest { @@ -27,18 +28,22 @@ export class ImageKnifeRequest { ImageKnifeRequestCallback: ImageKnifeRequestCallback componentVersion: number = 0 headers: Map = new Map() + downsampType?: DownsampleStrategy constructor(option: ImageKnifeOption, uIAbilityContext: common.UIAbilityContext, width: number, height: number, version: number, - ImageKnifeRequestCallback: ImageKnifeRequestCallback) { + ImageKnifeRequestCallback: ImageKnifeRequestCallback, + downsampType?: DownsampleStrategy + ) { this.imageKnifeOption = option this.context = uIAbilityContext this.componentWidth = width this.componentHeight = height this.componentVersion = version this.ImageKnifeRequestCallback = ImageKnifeRequestCallback + this.downsampType = downsampType } // RequestOption调用header对于的方法 addHeader(key: string, value: Object) {