From b0661e83ac7bae4d311c492d5a9c5eb8dff9a31d Mon Sep 17 00:00:00 2001 From: zgf Date: Mon, 4 Nov 2024 15:11:16 +0800 Subject: [PATCH] =?UTF-8?q?3.x=E5=88=86=E6=94=AF=E6=96=B0=E5=A2=9E?= =?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: zgf --- CHANGELOG.md | 1 + README_zh.md | 120 ++++++---- .../main/ets/common/CustomEngineKeyImpl.ets | 6 +- entry/src/main/ets/pages/DownSamplePage.ets | 214 ++++++++++++++++++ entry/src/main/ets/pages/Index.ets | 5 + .../main/resources/base/element/string.json | 16 ++ .../resources/base/profile/main_pages.json | 3 +- .../main/resources/zh_CN/element/string.json | 16 ++ entry/src/ohosTest/ets/test/List.test.ets | 2 + .../ohosTest/ets/test/SamplingTest.test.ets | 86 +++++++ library/index.ets | 2 + library/oh-package-lock.json5 | 17 +- library/src/main/ets/ImageKnifeDispatcher.ets | 4 + library/src/main/ets/ImageKnifeLoader.ets | 52 ++++- .../ets/downsampling/BaseDownsampling.ets | 21 ++ .../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 | 4 + .../src/main/ets/model/ImageKnifeOption.ets | 2 + sharedlibrary/Index.ets | 2 + 22 files changed, 800 insertions(+), 56 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 24ccca0..5b5fbeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ - Added callback information for image loading - Added the interface for obtaining the upper limit and size of the current cache and the number of images corresponding to the current cache - HTTPS custom certificate verification +- Add downsampling function to reduces memory cache consumption ## 3.2.0-rc.1 - Change the queue from Stack to Queue diff --git a/README_zh.md b/README_zh.md index 540aeb8..8d444b6 100644 --- a/README_zh.md +++ b/README_zh.md @@ -293,7 +293,17 @@ ImageKnifeComponent({ ImageKnifeOption: = new ImageKnifeOption({ }) }).width(100).height(100) ``` - +#### 12.图片降采样 示例 +``` +ImageKnifeComponent({ + imageKnifeOption:{ + loadSrc:$r("app.media.pngSample"), + placeholderSrc:$r('app.media.loading'), + errorholderSrc:$r('app.media.failed'), + downsampleOf: DownsampleStrategy.NONE + } + }).width(300).height(300) +``` #### 复用场景 在aboutToRecycle生命周期清空组件内容;通过watch监听触发图片的加载。 @@ -318,59 +328,71 @@ ImageKnifeComponent({ ImageKnifeOption: = new ImageKnifeOption({ ### ImageKnifeOption参数列表 -| 参数名称 | 入参内容 | 功能简介 | -|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------|---------------| -| loadSrc | string、PixelMap、Resource | 主图展示 | -| placeholderSrc | PixelMap、Resource | 占位图图展示(可选) | -| errorholderSrc | PixelMap、Resource | 错误图展示(可选) | -| objectFit | ImageFit | 主图填充效果(可选) | -| placeholderObjectFit | ImageFit | 占位图填充效果(可选) | -| errorholderObjectFit | ImageFit | 错误图填充效果(可选) | -| writeCacheStrategy | CacheStrategyType | 写入缓存策略(可选) | -| onlyRetrieveFromCache | boolean | 是否跳过网络和本地请求(可选) | -| customGetImage | customGetImage?:(context: Context, src: string、PixelMap、Resource ,headers?: Record) => Promise | 自定义下载图片(可选) | | Resource | 错误占位图数据源 | -| border | BorderOptions | 边框圆角(可选) | -| priority | taskpool.Priority | 加载优先级(可选) | -| context | common.UIAbilityContext | 上下文(可选) | -| progressListener | (progress: number)=>void | 进度(可选) | -| signature | String | 自定义缓存关键字(可选) | -| headerOption | Array | 设置请求头(可选) | -| transformation | PixelMapTransformation | 图片变换(可选) | -| drawingColorFilter | ColorFilter | drawing.ColorFilter | 图片变换(可选) | -| onComplete | (event:EventImage | undefined) => voi | 颜色滤镜效果(可选) | -| onLoadListener | onLoadStart?: (req?: ImageKnifeRequest) => void,onLoadSuccess?: (data: string \| PixelMap \| undefined, imageData: ImageKnifeData, req?: ImageKnifeRequest) => void,onLoadFailed?: (err: string, req?: ImageKnifeRequest) => void,onLoadCancel?: (res: string, req?: ImageKnifeRequest) => void | 监听图片加载成功与失败 | +| 参数名称 | 入参内容 | 功能简介 | +|-----------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------| +| loadSrc | string、PixelMap、Resource | 主图展示 | +| placeholderSrc | PixelMap、Resource | 占位图图展示(可选) | +| errorholderSrc | PixelMap、Resource | 错误图展示(可选) | +| objectFit | ImageFit | 主图填充效果(可选) | +| placeholderObjectFit | ImageFit | 占位图填充效果(可选) | +| errorholderObjectFit | ImageFit | 错误图填充效果(可选) | +| writeCacheStrategy | CacheStrategyType | 写入缓存策略(可选) | +| onlyRetrieveFromCache | boolean | 是否跳过网络和本地请求(可选) | +| customGetImage | customGetImage?:(context: Context, src: string、PixelMap、Resource ,headers?: Record) => Promise | 自定义下载图片(可选) | | Resource | 错误占位图数据源 | +| border | BorderOptions | 边框圆角(可选) | +| priority | taskpool.Priority | 加载优先级(可选) | +| context | common.UIAbilityContext | 上下文(可选) | +| progressListener | (progress: number)=>void | 进度(可选) | +| signature | String | 自定义缓存关键字(可选) | +| headerOption | Array | 设置请求头(可选) | +| transformation | PixelMapTransformation | 图片变换(可选) | +| drawingColorFilter | ColorFilter、drawing.ColorFilter | 图片变换(可选) | +| onComplete | (event:EventImage、undefined) => void | 颜色滤镜效果(可选) | +| onLoadListener | onLoadStart?: (req?: ImageKnifeRequest) => void,onLoadSuccess?: (data: string \| PixelMap \| undefined, imageData: ImageKnifeData, req?: ImageKnifeRequest) => void,onLoadFailed?: (err: string, req?: ImageKnifeRequest) => void,onLoadCancel?: (res: string, req?: ImageKnifeRequest) => void | 监听图片加载成功与失败 | +| downsampleOf | DownsampleStrategy | 降采样(可选) | + +### 降采样类型 +| 类型 | 相关描述 | +|---------------------|-------------------| +| NONE | 不进行降采样 | +| AT_MOST | 请求尺寸大于实际尺寸不进行放大 | +| FIT_CENTER_MEMORY | 两边自适应内存优先 | +| FIT_CENTER_QUALITY | 两边自适应质量优先 | +| CENTER_OUTSIDE_MEMORY | 宽高缩放比最大的比例,进行缩放适配内存优先 | +| CENTER_OUTSIDE_QUALITY | 宽高缩放比最大的比例,进行缩放适配质量优先 | +| AT_LEAST | 根据宽高的最小的比例,进行适配 | ### ImageKnife接口 -| 参数名称 | 入参内容 | 功能简介 | -|-------------------|-------------------------------------------------------------------------------------------------------|------------------| -| initMemoryCache | newMemoryCache: IMemoryCache | 自定义内存缓存策略 | -| initFileCache | context: Context, size: number, memory: number | 初始化文件缓存数量和大小 | -| reload | request: ImageKnifeRequest | 图片重新加载 | -| preLoad | loadSrc: string I ImageKnifeOption | 预加载返回图片请求request | -| cancel | request: ImageKnifeRequest | 取消图片请求 | -| preLoadCache | loadSrc: string I ImageKnifeOption | 预加载并返回文件缓存路径 | -| getCacheImage | loadSrc: string, cacheType: CacheStrategy = CacheStrategy.Default, signature?: string) | 从内存或文件缓存中获取资源 | -| addHeader | key: string, value: Object | 全局添加http请求头 | -| setHeaderOptions | Array | 全局设置http请求头 | -| deleteHeader | key: string | 全局删除http请求头 | -| setCustomGetImage | customGetImage?: (context: Context, src: string、PixelMap、Resource ,headers?: Record) => Promise | PixelMap | Resource) => Promise | 全局设置自定义下载 | -| setEngineKeyImpl | IEngineKey | 全局配置缓存key生成策略 | -| putCacheImage | url: string, pixelMap: PixelMap, cacheType: CacheStrategy = CacheStrategy.Default, signature?: string | 写入内存磁盘缓存 | -| removeMemoryCache | url: string | ImageKnifeOption | 清理指定内存缓存 | -| removeFileCache | url: string | ImageKnifeOption | 清理指定磁盘缓存 | -| getCacheLimitSize | cacheType?: CacheStrategy | 获取指定缓存的上限大小 | -| getCurrentCacheNum | cacheType?: CacheStrategy | 获取指定缓存的当前缓存图片个数 | -| getCurrentCacheSize | cacheType?: CacheStrategy | 获取指定缓存的当前大小 | -| getCurrentCacheSize | cacheType?: CacheStrategy | 获取指定缓存的当前大小 | +| 参数名称 | 入参内容 | 功能简介 | +|-------------------|---------------------------------------------------------------------------------------------------------------------------------------|------------------| +| initMemoryCache | newMemoryCache: IMemoryCache | 自定义内存缓存策略 | +| initFileCache | context: Context, size: number, memory: number | 初始化文件缓存数量和大小 | +| reload | request: ImageKnifeRequest | 图片重新加载 | +| preLoad | loadSrc: string I ImageKnifeOption | 预加载返回图片请求request | +| cancel | request: ImageKnifeRequest | 取消图片请求 | +| preLoadCache | loadSrc: string I ImageKnifeOption | 预加载并返回文件缓存路径 | +| getCacheImage | loadSrc: string, cacheType: CacheStrategy = CacheStrategy.Default, signature?: string) | 从内存或文件缓存中获取资源 | +| addHeader | key: string, value: Object | 全局添加http请求头 | +| setHeaderOptions | Array | 全局设置http请求头 | +| deleteHeader | key: string | 全局删除http请求头 | +| setCustomGetImage | customGetImage?: (context: Context, src: string、PixelMap、Resource ,headers?: Record) => Promise| 全局设置自定义下载 | +| setEngineKeyImpl | IEngineKey | 全局配置缓存key生成策略 | +| putCacheImage | url: string, pixelMap: PixelMap, cacheType: CacheStrategy = CacheStrategy.Default, signature?: string | 写入内存磁盘缓存 | +| removeMemoryCache | url: string、ImageKnifeOption | 清理指定内存缓存 | +| removeFileCache | url: string、ImageKnifeOption | 清理指定磁盘缓存 | +| getCacheLimitSize | cacheType?: CacheStrategy | 获取指定缓存的上限大小 | +| getCurrentCacheNum | cacheType?: CacheStrategy | 获取指定缓存的当前缓存图片个数 | +| getCurrentCacheSize | cacheType?: CacheStrategy | 获取指定缓存的当前大小 | +| getCurrentCacheSize | cacheType?: CacheStrategy | 获取指定缓存的当前大小 | ### 回调接口说明 -| 回调接口 | 回调字段 | 回调描述 | -|----------------|-------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| onLoadStart | req: ImageKnifeRequest | req返回字段中包含了图片请求的信息,如图片的url及其组件的宽高,同时ImageKnifeRequest包含了ImageKnifeData,其中包含此次请求的开始及其检查内存缓存的时间点 | -| onLoadSuccess | data: string \| PixelMap \| undefined, imageData: ImageKnifeData, req?: ImageKnifeRequest | data:加载成功的结果数据;imageData:图片的存入缓存中的信息 ,req:图片请求的信息,同时其中的ImageKnifeData,包含此次请求中图片的原始大小、图片的解码大小、格式、图片帧、请求结束时间、磁盘检查时间、网络请求开始结束、图片解码开始结束等时间点 | -| onLoadFailed | err: string, req?: ImageKnifeRequest | err:错误信息描述;req:图片请求的信息,同时其中的ImageKnifeData,包含此次请求错误信息(ErrorInfo,TimeInfo),ErrorInfo其中包含了,错误阶段、错误码及其网络请求的错误码;TimeInfo中包含请求结束时间、磁盘检查时间、网络请求开始结束、图片解码开始结束等时间点 | -| onLoadCancel | reason: string, req?: ImageKnifeRequest | reason:取消回调原因;req:图片请求的信息,同时其中的ImageKnifeData,包含此次请求错误信息(ErrorInfo,TimeInfo),ErrorInfo其中包含了,错误阶段、错误码及其网络请求的错误码;TimeInfo中包含请求结束时间、磁盘检查时间、网络请求开始结束、图片解码开始结束及其请求取消等时间点 | +| 回调接口 | 回调字段 | 回调描述 | +|----------------|-------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| onLoadStart | req: ImageKnifeRequest | req返回字段中包含了图片请求的信息,如图片的url及其组件的宽高,同时ImageKnifeRequest包含了ImageKnifeData,其中包含此次请求的开始及其检查内存缓存的时间点 | +| onLoadSuccess | data: string、PixelMap、undefined, imageData: ImageKnifeData, req?: ImageKnifeRequest | data:加载成功的结果数据;imageData:图片的存入缓存中的信息 ,req:图片请求的信息,同时其中的ImageKnifeData,包含此次请求中图片的原始大小、图片的解码大小、格式、图片帧、请求结束时间、磁盘检查时间、网络请求开始结束、图片解码开始结束等时间点 | +| onLoadFailed | err: string, req?: ImageKnifeRequest | err:错误信息描述;req:图片请求的信息,同时其中的ImageKnifeData,包含此次请求错误信息(ErrorInfo,TimeInfo),ErrorInfo其中包含了,错误阶段、错误码及其网络请求的错误码;TimeInfo中包含请求结束时间、磁盘检查时间、网络请求开始结束、图片解码开始结束等时间点 | +| onLoadCancel | reason: string, req?: ImageKnifeRequest | reason:取消回调原因;req:图片请求的信息,同时其中的ImageKnifeData,包含此次请求错误信息(ErrorInfo,TimeInfo),ErrorInfo其中包含了,错误阶段、错误码及其网络请求的错误码;TimeInfo中包含请求结束时间、磁盘检查时间、网络请求开始结束、图片解码开始结束及其请求取消等时间点 | ### 图形变换类型(需要为GPUImage添加依赖项) diff --git a/entry/src/main/ets/common/CustomEngineKeyImpl.ets b/entry/src/main/ets/common/CustomEngineKeyImpl.ets index 06f2901..acd2f4a 100644 --- a/entry/src/main/ets/common/CustomEngineKeyImpl.ets +++ b/entry/src/main/ets/common/CustomEngineKeyImpl.ets @@ -12,7 +12,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { IEngineKey, ImageKnifeOption, PixelMapTransformation,SparkMD5 ,ImageKnifeRequestSource} from '@ohos/libraryimageknife'; +import { IEngineKey, ImageKnifeOption, PixelMapTransformation,SparkMD5 ,ImageKnifeRequestSource, + DownsampleStrategy} from '@ohos/libraryimageknife'; //全局自定义key demo @Sendable @@ -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..6613599 --- /dev/null +++ b/entry/src/main/ets/pages/DownSamplePage.ets @@ -0,0 +1,214 @@ +/* + * 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 +@Component +struct DownSamplePage { + @State imageKnifeOption: ImageKnifeOption = { + loadSrc: $r('app.media.startIcon'), + placeholderSrc: $r("app.media.loading"), + errorholderSrc: $r("app.media.app_icon"), + objectFit: ImageFit.Contain + } + isBrightness: boolean = false + @State beforeSampling: number = 0 + @State afterSampling: number = 0 + @State 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"), + + ] + @State checked: boolean = false + + updateImageKnifeOption(value: string) { + if (value === 'NONE') { + this.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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 = { + 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 && this.imageKnifeOption.downsampleOf != undefined ) { + 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") + }); + } + getResourceString(res:Resource){ + return getContext().resourceManager.getStringSync(res.id) + } + 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(this.getResourceString($r('app.string.Sampling_pecification'))+ item.value).fontSize(20) + } + }, (item: SamplingType) => JSON.stringify(item)) + Column() { + Text(`${this.getResourceString($r('app.string.Unreal_samples'))}:${this.beforeSampling}`).fontSize(20) + Text(`${ this.getResourceString($r('app.string.After_the_sampling'))}:${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 2b8096a..2dc78be 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -95,6 +95,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 5bb2d46..055082a 100644 --- a/entry/src/main/resources/base/element/string.json +++ b/entry/src/main/resources/base/element/string.json @@ -631,6 +631,22 @@ { "name": "render_time", "value": "render successful time:%s " + }, + { + "name": "Image_Downsampling_Functionality", + "value": "Downsampling function" + }, + { + "name": "Sampling_pecification", + "value": "Downsampling specification" + }, + { + "name": "Unreal_samples", + "value": "Unsampled size" + }, + { + "name": "After_the_sampling", + "value": "Size after downsampling" } ] } \ 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 f181a32..72b840d 100644 --- a/entry/src/main/resources/base/profile/main_pages.json +++ b/entry/src/main/resources/base/profile/main_pages.json @@ -38,6 +38,7 @@ "pages/MaxRequest2", "pages/MaxRequest3", "pages/TestImageKnifeCallbackPage", - "pages/TestListImageKnifeCallbackPage" + "pages/TestListImageKnifeCallbackPage", + "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 8d8e25f..c6ea1fc 100644 --- a/entry/src/main/resources/zh_CN/element/string.json +++ b/entry/src/main/resources/zh_CN/element/string.json @@ -627,6 +627,22 @@ { "name": "render_time", "value": "渲染成功的时间:%s " + }, + { + "name": "Image_Downsampling_Functionality", + "value": "降采样功能" + }, + { + "name": "Sampling_pecification", + "value": "降采样规格" + }, + { + "name": "Unreal_samples", + "value": "未降采样大小" + }, + { + "name": "After_the_sampling", + "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 6c389ee..bd3e845 100644 --- a/entry/src/ohosTest/ets/test/List.test.ets +++ b/entry/src/ohosTest/ets/test/List.test.ets @@ -20,6 +20,7 @@ import ImageKnifeTest from './ImageKnife.test'; import Transform from './transform.test'; import imageFormatAndSize from './imageFormatAndSize.test' import loadCallBackData from './loadCallBackData.test' +import SamplingTest from './SamplingTest.test'; export default function testsuite() { MemoryLruCacheTest(); @@ -28,6 +29,7 @@ export default function testsuite() { ImageKnifeOptionTest(); ImageKnifeTest(); Transform(); + SamplingTest() imageFormatAndSize(); loadCallBackData(); } \ 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..c11562d --- /dev/null +++ b/entry/src/ohosTest/ets/test/SamplingTest.test.ets @@ -0,0 +1,86 @@ +/* + * 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 { 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('AT_MOST', 0, () => { + let reqSize: Size = + new Downsampler().calculateScaling('jpg', 1024, 1024, 200, + 200, DownsampleStrategy.AT_MOST) + let req = (reqSize.width < 1024 && reqSize.height < 1024) + expect(req).assertEqual(true); + }) + it('FIT_CENTER_MEMORY', 1, () => { + let reqSize: Size = + new Downsampler().calculateScaling('jpg', 1024, 1024, 200, + 200, DownsampleStrategy.FIT_CENTER_MEMORY) + let req = (reqSize.width < 1024 && reqSize.height < 1024) + expect(req).assertEqual(true); + }) + it('FIT_CENTER_QUALITY', 2, () => { + let reqSize: Size = + new Downsampler().calculateScaling('jpg', 1024, 1024, 200, + 200, DownsampleStrategy.FIT_CENTER_QUALITY) + let req = (reqSize.width < 1024 && reqSize.height < 1024) + expect(req).assertEqual(true); + }) + it('CENTER_OUTSIDE_MEMORY', 3, () => { + let reqSize: Size = + new Downsampler().calculateScaling('jpg', 1024, 1024, 200, + 200, DownsampleStrategy.CENTER_OUTSIDE_MEMORY) + let req = (reqSize.width < 1024 && reqSize.height < 1024) + expect(req).assertEqual(true); + }) + it('CENTER_OUTSIDE_QUALITY', 4, () => { + let reqSize: Size = + new Downsampler().calculateScaling('jpg', 1024, 1024, 200, + 200, DownsampleStrategy.CENTER_OUTSIDE_QUALITY) + let req = (reqSize.width < 1024 && reqSize.height < 1024) + expect(req).assertEqual(true); + }) + it('AT_LEAST', 5, () => { + let reqSize: Size = + new Downsampler().calculateScaling('jpg', 1024, 1024, 200, + 200, DownsampleStrategy.AT_LEAST) + let req = (reqSize.width < 1024 && reqSize.height < 1024) + expect(req).assertEqual(true); + }) + + }) +} \ No newline at end of file diff --git a/library/index.ets b/library/index.ets index 1f67f70..22893ae 100644 --- a/library/index.ets +++ b/library/index.ets @@ -30,6 +30,8 @@ export { IEngineKey } from './src/main/ets/key/IEngineKey' export { ImageKnifeData , CacheStrategy , ImageKnifeRequestSource} from "./src/main/ets/model/ImageKnifeData" +export { DownsampleStrategy } from './src/main/ets/downsampling/DownsampleStartegy' + export { PixelMapTransformation } from './src/main/ets/transform/PixelMapTransformation' export { MultiTransTransformation } from './src/main/ets/transform/MultiTransTransformation' diff --git a/library/oh-package-lock.json5 b/library/oh-package-lock.json5 index 3245bae..e2f23b9 100644 --- a/library/oh-package-lock.json5 +++ b/library/oh-package-lock.json5 @@ -1,6 +1,19 @@ { + "meta": { + "stableOrder": false + }, "lockfileVersion": 3, "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", - "specifiers": {}, - "packages": {} + "specifiers": { + "@ohos/gpu_transform@^1.0.2": "@ohos/gpu_transform@1.0.4" + }, + "packages": { + "@ohos/gpu_transform@1.0.4": { + "name": "@ohos/gpu_transform", + "version": "1.0.4", + "integrity": "sha512-PrKlOK66kzObw/ANIzt55YMrOLLmtrhmAZIE2c/60GBoTl7+NLxONeHA2NsDZuiW+A0KnD4QylDYWH4/yo8T0w==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/gpu_transform/-/gpu_transform-1.0.4.har", + "registryType": "ohpm" + } + } } \ No newline at end of file diff --git a/library/src/main/ets/ImageKnifeDispatcher.ets b/library/src/main/ets/ImageKnifeDispatcher.ets index 4b27b9b..099d416 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 { @@ -256,6 +257,9 @@ export class ImageKnifeDispatcher { moduleName: moduleName == "" ? undefined : moduleName, resName: resName == "" ? undefined : resName, caPath: currentRequest.imageKnifeOption.caPath, + 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 3ef64c4..88468c0 100644 --- a/library/src/main/ets/ImageKnifeLoader.ets +++ b/library/src/main/ets/ImageKnifeLoader.ets @@ -33,6 +33,8 @@ import image from '@ohos.multimedia.image'; import { RequestJobResult } from './model/ImageKnifeData' import util from '@ohos.util'; import { FileTypeUtil } from './utils/FileTypeUtil'; +import { DownsampleStrategy } from './downsampling/DownsampleStartegy'; +import { Downsampler } from './downsampling/Downsampler'; class RequestData { receiveSize: number = 2000 @@ -124,6 +126,16 @@ export class ImageKnifeLoader { let size = (await imageSource.getImageInfo()).size callBackData.imageWidth = size.width; callBackData.imageHeight = size.height; + try { + if ((request.downsampType !== DownsampleStrategy.NONE) && + request.requestSource == ImageKnifeRequestSource.SRC) { + decodingOptions = + ImageKnifeLoader.getDownsamplerDecodingOptions(typeValue, request, size, ImageKnifeRequestSource.SRC) + } + } catch (err) { + ImageKnifeLoader.makeEmptyResult(request, err) + return + } timeInfo.decodeStartTime = Date.now(); await imageSource.createPixelMap(decodingOptions) @@ -186,9 +198,6 @@ export class ImageKnifeLoader { let scale = size.height / size.width let hValue = Math.round(request.componentHeight); let wValue = Math.round(request.componentWidth); - callBackData.imageWidth = size.width; - callBackData.imageHeight = size.height; - timeInfo.decodeStartTime = Date.now(); let defaultSize: image.Size = { height: vp2px(wValue) * scale, width: vp2px(wValue) @@ -197,6 +206,18 @@ export class ImageKnifeLoader { editable: true, desiredSize: defaultSize }; + callBackData.imageWidth = size.width; + callBackData.imageHeight = size.height; + try { + if ((request.downsampType !== DownsampleStrategy.NONE) && + request.requestSource == ImageKnifeRequestSource.SRC) { + opts = ImageKnifeLoader.getDownsamplerDecodingOptions(typeValue, request, size) + } + } catch (err) { + ImageKnifeLoader.makeEmptyResult(request,err) + return + } + timeInfo.decodeStartTime = Date.now(); await imageSource.createPixelMap(opts) .then((pixelmap: PixelMap) => { timeInfo.decodeEndTime = Date.now(); @@ -541,4 +562,29 @@ export class ImageKnifeLoader { } ImageKnifeLoader.parseImage(resBuf,fileKey,request, callBackData) } + + 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 + } + } + } + } } \ No newline at end of file diff --git a/library/src/main/ets/downsampling/BaseDownsampling.ets b/library/src/main/ets/downsampling/BaseDownsampling.ets new file mode 100644 index 0000000..ac355bb --- /dev/null +++ b/library/src/main/ets/downsampling/BaseDownsampling.ets @@ -0,0 +1,21 @@ +/* + * 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 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..07fe4b7 --- /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 AtLeast implements BaseDownsampling { + getName() { + return "AtLeast" + } + + 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..385a581 --- /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, + AtLeast, + 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 AtLeast(); + default: + throw new Error('Unsupported downsampling strategy'); + } + } +} \ No newline at end of file diff --git a/library/src/main/ets/key/DefaultEngineKey.ets b/library/src/main/ets/key/DefaultEngineKey.ets index f28024b..34c778f 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 ba94e76..6d65078 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, // url @@ -147,5 +148,8 @@ export interface RequestJobRequest { moduleName?:string, resName?: string caPath?: 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 c854f55..ac0fffe 100644 --- a/library/src/main/ets/model/ImageKnifeOption.ets +++ b/library/src/main/ets/model/ImageKnifeOption.ets @@ -18,6 +18,7 @@ import { CacheStrategy, ImageKnifeData,EventImage } from './ImageKnifeData'; import { PixelMapTransformation } from '../transform/PixelMapTransformation'; import { drawing } from '@kit.ArkGraphics2D'; import { ImageKnifeRequest } from './ImageKnifeRequest'; +import { DownsampleStrategy } from '../downsampling/DownsampleStartegy'; export interface HeaderOptions { key: string; @@ -78,6 +79,7 @@ export class ImageKnifeOption { onLoadListener?: OnLoadCallBack | undefined; onComplete?:(event:EventImage | undefined) => void drawingColorFilter?: ColorFilter | drawing.ColorFilter + downsampleOf?: DownsampleStrategy // 降采样 // 自定义证书路径 caPath?: string constructor() { diff --git a/sharedlibrary/Index.ets b/sharedlibrary/Index.ets index 5d74d4b..99a925e 100644 --- a/sharedlibrary/Index.ets +++ b/sharedlibrary/Index.ets @@ -24,6 +24,8 @@ export { ImageKnife } from '@ohos/imageknife' export { ImageKnifeOption,AnimatorOption } from '@ohos/imageknife' +export { DownsampleStrategy } from "@ohos/imageknife" + export { ImageKnifeRequest } from '@ohos/imageknife' export { FileUtils } from '@ohos/imageknife'