diff --git a/CHANGELOG.md b/CHANGELOG.md index 07dce31..42194fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,12 @@ +## 3.1.0-rc.1 +- 添加图片降采样功能 + ## 3.1.0-rc.0 - ComponentV2装饰器适配 - imageKnifeOption={...}用法改为new ImageKnifeOption({...}) - animatorOption={...}用法改为new AnimatorOption({...}) + ## 3.0.1 - 修复animatorOption属性设置初始化值失效 - 网络请求code为206、204时返回arraybuffer diff --git a/README.md b/README.md index b616b70..1959075 100644 --- a/README.md +++ b/README.md @@ -265,6 +265,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) +``` ## 接口说明 ### ImageKnife组件 | 组件名称 | 入参内容 | 功能简介 | @@ -302,6 +313,7 @@ ImageKnifeAnimatorComponent({ | drawingColorFilter | ColorFilter | drawing.ColorFilter | 图片变换(可选) | | onComplete | (event:EventImage | undefined) => voi | 颜色滤镜效果(可选) | | onLoadListener | onLoadStart: () => void、onLoadSuccess: (data: string | PixelMap | undefined) => void、onLoadFailed: (err: string) => void| 监听图片加载成功与失败 | +| downsampleOf | DownsampleStrategy | 降采样(可选) | ### ImageKnife接口 @@ -338,6 +350,16 @@ ImageKnifeAnimatorComponent({ | ToonTransformation | 动画滤波器(使用GPUIImage) | | VignetterTransformation | 装饰滤波器(使用GPUIImage) | +### 降采样类型 +| 类型 | 相关描述 | +|---------------------|-------------------| +| NONE | 不进行降采样 | +| AT_MOST | 请求尺寸大于实际尺寸不进行放大 | +| FIT_CENTER | 两边自适应 | +| CENTER_INSIDE | 宽高缩放比最大的比例,进行缩放适配 | +| CENTER_OUTSIDE | 根据宽高的最小的比例,进行适配 | + + ## 下载安装GPUImage依赖 方法一:在Terminal窗口中,执行如下命令安装三方包,DevEco Studio会自动在工程的oh-package.json5中自动添加三方包依赖。 ``` diff --git a/entry/src/main/ets/pages/DownSamplePage.ets b/entry/src/main/ets/pages/DownSamplePage.ets new file mode 100644 index 0000000..75c351e --- /dev/null +++ b/entry/src/main/ets/pages/DownSamplePage.ets @@ -0,0 +1,183 @@ +/* + * 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, BlurTransformation, } 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(0, "NONE"), + new SamplingType(1, "AT_MOST"), + new SamplingType(2, "FIT_CENTER"), + new SamplingType(3, "CENTER_INSIDE"), + new SamplingType(4, "CENTER_OUTSIDE"), + ] + @Local checked: boolean = false + + updateImageKnifeOption(value: string) { + if (value === 'NONE') { + this.imageKnifeOption =new ImageKnifeOption( { + loadSrc: $r('app.media.jpgSample'), + placeholderSrc: $r("app.media.loading"), + errorholderSrc: $r("app.media.app_icon"), + objectFit: ImageFit.Contain, + downsampleOf: DownsampleStrategy.NONE + }) + this.originalPixMap($r('app.media.jpgSample')) + this.afterSamplingFunc($r('app.media.jpgSample')) + } else if (value === 'AT_MOST') { + this.imageKnifeOption =new ImageKnifeOption( { + loadSrc: $r('app.media.svgSample'), + placeholderSrc: $r("app.media.loading"), + errorholderSrc: $r("app.media.app_icon"), + objectFit: ImageFit.Contain, + downsampleOf: DownsampleStrategy.AT_MOST + }) + this.originalPixMap($r('app.media.svgSample')) + this.afterSamplingFunc($r('app.media.svgSample')) + } else if (value === 'FIT_CENTER') { + 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 + }) + this.originalPixMap($r('app.media.pngSample')) + this.afterSamplingFunc($r('app.media.pngSample')) + } else if (value === 'CENTER_INSIDE') { + this.imageKnifeOption = new ImageKnifeOption({ + loadSrc: $r('app.media.jpgSample1'), + placeholderSrc: $r("app.media.loading"), + errorholderSrc: $r("app.media.app_icon"), + objectFit: ImageFit.Contain, + downsampleOf: DownsampleStrategy.CENTER_INSIDE + }) + this.originalPixMap($r('app.media.jpgSample1')) + this.afterSamplingFunc($r('app.media.jpgSample1')) + } else { + this.imageKnifeOption = new ImageKnifeOption({ + loadSrc: $r('app.media.jpgSample2'), + placeholderSrc: $r("app.media.loading"), + errorholderSrc: $r("app.media.app_icon"), + objectFit: ImageFit.Contain, + downsampleOf: DownsampleStrategy.CENTER_OUTSIDE + }) + this.originalPixMap($r('app.media.jpgSample2')) + this.afterSamplingFunc($r('app.media.jpgSample2')) + } + } + + 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)); + let decodingOptions: image.DecodingOptions = { + editable: true, + desiredPixelFormat: 3, + } + let imageInfo = await imageSource.getImageInfo() + let reqSize = + new Downsampler().calculateScaling(typeValue, imageInfo.size.width, imageInfo.size.height, 300, + 300, this.imageKnifeOption.downsampleOf, false) + decodingOptions = { + editable: true, + desiredSize: { + width: reqSize.targetWidth, + height: reqSize.targetHeight + } + } + // 创建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 e3e5074..72c436c 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -42,6 +42,11 @@ struct Index { }); }) + Button("降采样").onClick(() => { + router.push({ + uri: 'pages/DownSamplePage', + }); + }) Button("单个图片使用").margin({top:10}).onClick(()=>{ router.push({ uri: 'pages/SingleImage', diff --git a/entry/src/main/resources/base/media/jpgSample.jpg b/entry/src/main/resources/base/media/jpgSample.jpg new file mode 100644 index 0000000..c259cdc Binary files /dev/null and b/entry/src/main/resources/base/media/jpgSample.jpg differ diff --git a/entry/src/main/resources/base/media/jpgSample1.jpg b/entry/src/main/resources/base/media/jpgSample1.jpg new file mode 100644 index 0000000..a4aa87b Binary files /dev/null and b/entry/src/main/resources/base/media/jpgSample1.jpg differ diff --git a/entry/src/main/resources/base/media/jpgSample2.jpg b/entry/src/main/resources/base/media/jpgSample2.jpg new file mode 100644 index 0000000..33c27f7 Binary files /dev/null and b/entry/src/main/resources/base/media/jpgSample2.jpg differ diff --git a/entry/src/main/resources/base/profile/main_pages.json b/entry/src/main/resources/base/profile/main_pages.json index 42b571c..03af5b2 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/TestCommonImage", "pages/ImageAnimatorPage", "pages/TestSetCustomImagePage", - "pages/TestErrorHolderPage" + "pages/TestErrorHolderPage", + "pages/DownSamplePage" ] } \ 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..51a0c15 100644 --- a/entry/src/ohosTest/ets/test/List.test.ets +++ b/entry/src/ohosTest/ets/test/List.test.ets @@ -18,6 +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(); @@ -26,4 +27,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..8fe3093 --- /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, false) + 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, false) + 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, false) + let req = (reqSize.targetWidth < 1024 && reqSize.targetHeight < 1024) + expect(req).assertEqual(true); + }) + it('CENTER_INSIDE', 3, () => { + let reqSize: calculateScaleType = + new Downsampler().calculateScaling('jpg', 1024, 1024, 200, + 200, DownsampleStrategy.CENTER_INSIDE, false) + let req = (reqSize.targetWidth < 1024 && reqSize.targetHeight < 1024) + expect(req).assertEqual(true); + }) + it('CENTER_OUTSIDE', 4, () => { + let reqSize: calculateScaleType = + new Downsampler().calculateScaling('jpg', 1024, 1024, 200, + 200, DownsampleStrategy.CENTER_OUTSIDE, false) + let req = (reqSize.targetWidth < 1024 && reqSize.targetHeight < 1024) + expect(req).assertEqual(true); + }) + + }) +} diff --git a/entry/src/ohosTest/ets/test/imageknifeOption.test.ets b/entry/src/ohosTest/ets/test/imageknifeOption.test.ets deleted file mode 100644 index 6a3acac..0000000 --- a/entry/src/ohosTest/ets/test/imageknifeOption.test.ets +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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 { ImageKnifeOption,ImageKnifeData } from "@ohos/imageknife" - -export default function ImageKnifeOptionTest() { - describe('ImageKnifeOptionTest',() => { - // 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('onLoadListener', 0, () => { - let a = 'abc'; - let b: string = ''; - let imageData:ImageKnifeData = { - source: "", - imageWidth: 0, - imageHeight: 0, - } - let imageKnifeOption: ImageKnifeOption = new ImageKnifeOption({ - loadSrc: $r("app.media.rabbit"), - onLoadListener: { - onLoadFailed: (err) => { - console.error("Load Failed Reason: " + err); - }, - onLoadSuccess: (data,imageknifeData) => { - if(typeof data == 'string') { - return b = data; - } - imageData = imageknifeData - return data; - }, - }, - }) - if (imageKnifeOption.onLoadListener && imageKnifeOption.onLoadListener.onLoadSuccess && imageKnifeOption.onLoadListener.onLoadFailed) { - imageKnifeOption.onLoadListener.onLoadSuccess(a,imageData); - imageKnifeOption.onLoadListener.onLoadFailed(a); - } - expect(a).assertEqual(b); - }); - }); -} \ No newline at end of file diff --git a/library/index.ets b/library/index.ets index 4576c08..acdeb60 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/oh-package.json5 b/library/oh-package.json5 index 67af1d6..5922ecf 100644 --- a/library/oh-package.json5 +++ b/library/oh-package.json5 @@ -14,7 +14,7 @@ "main": "index.ets", "repository": "https://gitee.com/openharmony-tpc/ImageKnife", "type": "module", - "version": "3.0.1-rc.2", + "version": "3.1.0-rc.1", "dependencies": { "@ohos/gpu_transform": "^1.0.2" }, diff --git a/library/src/main/ets/ImageKnifeDispatcher.ets b/library/src/main/ets/ImageKnifeDispatcher.ets index 14ee1ad..6da65ee 100644 --- a/library/src/main/ets/ImageKnifeDispatcher.ets +++ b/library/src/main/ets/ImageKnifeDispatcher.ets @@ -40,6 +40,8 @@ import { } from './model/ImageKnifeData' import { combineArrayBuffers } from './model/utils'; import { BusinessError } from '@kit.BasicServicesKit'; +import { Downsampler } from './downsampling/Downsampler' +import { DownsampleStrategy } from './downsampling/DownsampleStartegy'; export class ImageKnifeDispatcher { // 最大并发 @@ -176,7 +178,11 @@ export class ImageKnifeDispatcher { isWatchProgress: isWatchProgress, memoryKey: memoryKey, fileCacheFolder: ImageKnife.getInstance().getFileCache()?.getCacheFolder(), - isAnimator:isAnimator + isAnimator:isAnimator, + targetWidth: currentRequest.componentWidth, + targetHeight: currentRequest.componentHeight, + downsampType: currentRequest.imageKnifeOption.downsampleOf, + autoResize: currentRequest.imageKnifeOption.autoResize, } if(request.customGetImage == undefined) { @@ -636,6 +642,21 @@ async function requestJob(request: RequestJobRequest, requestList?: List { resPixelmap = pixelmap @@ -648,6 +669,23 @@ async function requestJob(request: RequestJobRequest, requestList?: List { diff --git a/library/src/main/ets/ImageKnifeOption.ets b/library/src/main/ets/ImageKnifeOption.ets index 68b23da..fa7925a 100644 --- a/library/src/main/ets/ImageKnifeOption.ets +++ b/library/src/main/ets/ImageKnifeOption.ets @@ -16,6 +16,7 @@ import taskpool from '@ohos.taskpool'; import common from '@ohos.app.ability.common' import { CacheStrategy, ImageKnifeData,EventImage } from './model/ImageKnifeData'; import { PixelMapTransformation } from './transform/PixelMapTransformation'; +import { DownsampleStrategy } from './downsampling/DownsampleStartegy'; import { drawing } from '@kit.ArkGraphics2D'; export interface HeaderOptions { @@ -71,6 +72,8 @@ interface ImageOption { onLoadListener?: OnLoadCallBack | undefined; onComplete?:(event:EventImage | undefined) => void drawingColorFilter?: ColorFilter | drawing.ColorFilter + downsampleOf?: DownsampleStrategy + autoResize?: boolean } @ObservedV2 export class ImageKnifeOption { @@ -102,6 +105,9 @@ export class ImageKnifeOption { onLoadListener?: OnLoadCallBack | undefined; onComplete?:(event:EventImage | undefined) => void drawingColorFilter?: ColorFilter | drawing.ColorFilter + // 下采样 + downsampleOf?: DownsampleStrategy = DownsampleStrategy.NONE + autoResize?: boolean constructor(option?:ImageOption) { this.loadSrc = option?.loadSrc == undefined ? "" : option?.loadSrc this.placeholderSrc = option?.placeholderSrc @@ -122,6 +128,8 @@ export class ImageKnifeOption { this.onLoadListener = option?.onLoadListener this.onComplete = option?.onComplete this.drawingColorFilter = option?.drawingColorFilter + this.downsampleOf = option?.downsampleOf + this.autoResize = option?.autoResize } } diff --git a/library/src/main/ets/ImageKnifeRequest.ets b/library/src/main/ets/ImageKnifeRequest.ets index cb2115b..523ad1f 100644 --- a/library/src/main/ets/ImageKnifeRequest.ets +++ b/library/src/main/ets/ImageKnifeRequest.ets @@ -15,7 +15,7 @@ import { ImageKnifeOption } from './ImageKnifeOption'; import common from '@ohos.app.ability.common'; import { ImageKnifeRequestSource } from './model/ImageKnifeData'; - +import { DownsampleStrategy } from './downsampling/DownsampleStartegy'; export class ImageKnifeRequest { requestState: ImageKnifeRequestState = ImageKnifeRequestState.PROGRESS @@ -27,18 +27,25 @@ export class ImageKnifeRequest { ImageKnifeRequestCallback: ImageKnifeRequestCallback componentVersion: number = 0 headers: Map = new Map() + downsampType?: DownsampleStrategy + autoResizes?: boolean constructor(option: ImageKnifeOption, uIAbilityContext: common.UIAbilityContext, width: number, height: number, version: number, - ImageKnifeRequestCallback: ImageKnifeRequestCallback) { + ImageKnifeRequestCallback: ImageKnifeRequestCallback, + downsampType?: DownsampleStrategy, + autoResizes?: boolean + ) { this.imageKnifeOption = option this.context = uIAbilityContext this.componentWidth = width this.componentHeight = height this.componentVersion = version this.ImageKnifeRequestCallback = ImageKnifeRequestCallback + this.downsampType = downsampType + this.autoResizes = autoResizes } // RequestOption调用header对于的方法 addHeader(key: string, value: Object) { diff --git a/library/src/main/ets/downsampling/BaseDownsampling.ets b/library/src/main/ets/downsampling/BaseDownsampling.ets new file mode 100644 index 0000000..5660ec8 --- /dev/null +++ b/library/src/main/ets/downsampling/BaseDownsampling.ets @@ -0,0 +1,25 @@ +/* + * 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 { SampleSizeRounding } from './downsampleUtils'; + +export interface BaseDownsampling { + getName(): string + + getScaleFactor(sourceWidth: number, sourceHeight: number, requestWidth: number, requestHeight: number): number + + getSampleSizeRounding(sourceWidth: number, sourceHeight: number, requestWidth: number, + requestHeight: number): SampleSizeRounding +} \ 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..06f5022 --- /dev/null +++ b/library/src/main/ets/downsampling/DownsampleStartegy.ets @@ -0,0 +1,189 @@ +/* + * 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 { highestOneBit, SampleSizeRounding } from './downsampleUtils'; + +export class FitCenter implements BaseDownsampling { + getName() { + return "FitCenter" + } + + //实现 getScaleFactor 方法 + getScaleFactor(sourceWidth: number, sourceHeight: number, requestedWidth: number, requestedHeight: number, + autoResize?: boolean): number { + const IS_BITMAP_FACTORY_SCALING_SUPPORTED = autoResize; //这里需要根据实际情况设置这个值 + if (IS_BITMAP_FACTORY_SCALING_SUPPORTED) { + const widthPercentage = requestedWidth / sourceWidth + const heightPercentage = requestedHeight / sourceHeight + return Math.min(widthPercentage, heightPercentage) + } else { + //类似 AT_LEAST,但只要求一个维度或另一个维度大于等于请求的尺寸 + const maxIntegerFactor = Math.max(sourceHeight / requestedHeight, sourceWidth / requestedWidth); + const a = maxIntegerFactor === 0 ? 1 : 1 / highestOneBit(maxIntegerFactor) + return a; + } + } + + //实现 getSampleSizeRounding 方法 + getSampleSizeRounding(sourceWidth: number, sourceHeight: number, requestedWidth: number, requestedHeight: number, + autoResize?: boolean): number { + const IS_BITMAP_FACTORY_SCALING_SUPPORTED = autoResize; //这里需要根据实际情况设置这个值 + + if (!IS_BITMAP_FACTORY_SCALING_SUPPORTED) { + return SampleSizeRounding.QUALITY; + } else { + return SampleSizeRounding.MEMORY; + } + + } +} + +export class None implements BaseDownsampling { + getName(): string { + return "DownsampleNone" + } + + public getScaleFactor(sourceWidth: number, sourceHeight: number, requestedWidth: number, + requestedHeight: number): number { + //不进行任何下采样,缩放因子为 1 + return 1; + } + + //实现 getSampleSizeRounding 方法 + public getSampleSizeRounding(sourceWidth: number, sourceHeight: number, requestedWidth: number, + requestedHeight: number): SampleSizeRounding { + return SampleSizeRounding.QUALITY + + } +} + + +/*宽高进行等比缩放宽高里面最小的比例先放进去 +然后再更据原图的缩放比去适配另一边*/ + +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; + //返回宽度和高度比例中最大的值 + return Math.max(widthPercentage, heightPercentage); + } + + getSampleSizeRounding(sourceWidth: number, sourceHeight: number, requestedWidth: number, + requestedHeight: number): SampleSizeRounding { + //根据 CenterOutside 的逻辑,总是返回 QUALITY + return SampleSizeRounding.QUALITY; + } +} + +/*请求尺寸大于实际尺寸不进行放大,按照原图展示*/ + +export class AtMost implements BaseDownsampling { + getName() { + return "AtMost" + } + + //实现 getScaleFactor 方法 + 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) + //返回缩放因子 + return 1 / greaterOrEqualSampleSize + } + + + getSampleSizeRounding(sourceWidth: number, sourceHeight: number, requestedWidth: number, + requestHeight: number): SampleSizeRounding { + //根据 AtMost 的逻辑,总是返回 MEMORY + return SampleSizeRounding.MEMORY + } +} + +/*宽高进行等比缩放宽高里面最大的比例先放进去 +然后再更据原图的缩放比去适配另一边*/ +export class CenterInside implements BaseDownsampling { + getName() { + return "CenterInside" + } + + getScaleFactor(sourceWidth: number, sourceHeight: number, requestedWidth: number, requestedHeight: number, + autoResize?: boolean): number { + //获取 FIT_CENTER 的缩放因子 + const fitCenterScaleFactor: ESObject = + this.getScale(sourceWidth, sourceHeight, requestedWidth, requestedHeight, autoResize); + //返回不超过 1 的缩放因子,即尽量缩小图像以适应目标尺寸,但不会放大 + return Math.min(1, fitCenterScaleFactor); + + } + + getScale(sourceWidth: number, sourceHeight: number, requestedWidth: number, requestedHeight: number, + autoResize?: boolean): number { + const IS_BITMAP_FACTORY_SCALING_SUPPORTED = autoResize; + if (IS_BITMAP_FACTORY_SCALING_SUPPORTED) { + const widthPercentage = requestedWidth / sourceWidth + const heightPercentage = requestedHeight / sourceHeight + return Math.min(widthPercentage, heightPercentage) + } else { + //类似 AT_LEAST,但只要求一个维度或另一个维度大于等于请求的尺寸 + const maxIntegerFactor = Math.max(sourceHeight / requestedHeight, sourceWidth / requestedWidth); + + return maxIntegerFactor === 0 ? 1 : 1 / highestOneBit(maxIntegerFactor); + + } + } + + getSampleSizeRounding(sourceWidth: number, sourceHeight: number, requestedWidth: number, requestedHeight: number, + autoResize?: boolean): SampleSizeRounding { + //如果缩放因子为 1,表示没有缩放,优先选择质量 + if (this.getScaleFactor(sourceWidth, sourceHeight, requestedWidth, requestedHeight, autoResize) === 1) { + return SampleSizeRounding.QUALITY + } + //否则,使用 FIL_CENTER 的 SampleSizeRounding 值 + return this.getSampleSize(sourceWidth, sourceHeight, requestedWidth, requestedHeight); + } + + getSampleSize(sourceWidth: number, sourceHeight: number, requestedWidth: number, requestedHeight: number, + autoResize?: boolean): SampleSizeRounding { + const IS_BITMAP_FACTORY_SCALING_SUPPORTED = autoResize; + + if (IS_BITMAP_FACTORY_SCALING_SUPPORTED) { + return SampleSizeRounding.QUALITY; + } else { + return SampleSizeRounding.MEMORY; + } + } +} + +export enum DownsampleStrategy { + //请求尺寸大于实际尺寸不进行放大 + AT_MOST, + //两边自适应 + FIT_CENTER, + //宽高进行等比缩放宽高里面最大的比例先放进去,然后再根据原图的缩放比去适配 + CENTER_INSIDE, + //宽高进行等比缩放宽高里面最小的比例先放进去,然后再根据原图的缩放比去适配 + CENTER_OUTSIDE, + //不进行降采样 + NONE, +} \ 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..b3f4160 --- /dev/null +++ b/library/src/main/ets/downsampling/Downsampler.ets @@ -0,0 +1,150 @@ +/* + * 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, + None, +} from './DownsampleStartegy'; +import { highestOneBit, SampleSizeRounding } from './downsampleUtils'; + +export interface calculateScaleType { + targetWidth: number, + targetHeight: number +} + +export class Downsampler { + calculateScaling( + typeValue: string | null, + sourceHeight: number, //原始宽高 + sourceWidth: number | undefined, //原始宽高 + requestHeight: number, //请求宽高 + requestWidth: number, //请求宽高 + downsampType: DownsampleStrategy | undefined, + autoResize: boolean | undefined + + ): calculateScaleType { + let degreesToRotate: ESObject = 90; + const fileType = typeValue //获取图片类型 + let powerOfTwoWidth: number | null = null; + let powerOfTwoHeight: number | null = null; + let targetWidth: number = 0 + let targetHeight: number = 0 + + if (sourceHeight <= 0 || sourceWidth == undefined || sourceWidth == null || sourceWidth <= 0) { + throw new Error("Cannot found width or height"); + } + let orientedSourceWidth = sourceWidth; + let orientedSourceHeight = sourceHeight; + if (this.isRotationRequired(degreesToRotate)) { + orientedSourceWidth = sourceHeight; + orientedSourceHeight = sourceWidth; + } + if (requestWidth && !requestHeight) { + targetWidth = this.round((requestHeight) * orientedSourceWidth / orientedSourceHeight) + } else if (requestHeight && !requestWidth) { + targetHeight = this.round((requestWidth) * orientedSourceHeight / orientedSourceWidth) + } else if (requestHeight && requestWidth) { + targetWidth = + requestHeight == sourceWidth ? (this.isRotationRequired(degreesToRotate) ? sourceHeight : sourceWidth) : + requestWidth; + targetHeight = + requestHeight == sourceHeight ? (this.isRotationRequired(degreesToRotate) ? sourceWidth : sourceHeight) : + requestWidth; + } else { + throw new Error("Cannot found width or height"); + } + let exactScaleFactor: number = new FitCenter() + .getScaleFactor(orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight, autoResize) + let rounding: SampleSizeRounding = new FitCenter() + .getSampleSizeRounding(orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight, autoResize) + switch (downsampType) { + case DownsampleStrategy.FIT_CENTER: + exactScaleFactor = new FitCenter() + .getScaleFactor(orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight) + rounding = new FitCenter() + .getSampleSizeRounding(orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight) + break; + case DownsampleStrategy.NONE: + exactScaleFactor = new None() + .getScaleFactor(orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight) + rounding = new None() + .getSampleSizeRounding(orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight) + break + case DownsampleStrategy.AT_MOST: + exactScaleFactor = new AtMost() + .getScaleFactor(orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight) + rounding = new AtMost() + .getSampleSizeRounding(orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight) + break + case DownsampleStrategy.CENTER_INSIDE: + exactScaleFactor = new CenterInside() + .getScaleFactor(orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight, autoResize) + rounding = new CenterInside() + .getSampleSizeRounding(orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight, autoResize) + break + + case DownsampleStrategy.CENTER_OUTSIDE: + exactScaleFactor = new CenterOutside() + .getScaleFactor(orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight) + rounding = new CenterOutside() + .getSampleSizeRounding(orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight) + break + } + if (exactScaleFactor <= 0) { + throw new Error("Cannot round with exactScaleFactor"); + } + if (rounding == null) { + throw new Error("Cannot round with null rounding"); + } + let outWidth: number = this.round(exactScaleFactor * orientedSourceWidth); + let outHeight: number = this.round(exactScaleFactor * orientedSourceHeight); + let widthScaleFactor = orientedSourceWidth / outWidth; + let heightScaleFactor = orientedSourceHeight / outHeight; + let scaleFactor = rounding == 1 ? Math.max(widthScaleFactor, heightScaleFactor) : + Math.min(widthScaleFactor, heightScaleFactor) //将整型的缩放因子转换为2的次幂采样大小 + let powerOfTwoSampleSize: number = scaleFactor; + powerOfTwoSampleSize = Math.max(1, highestOneBit(scaleFactor)) + if (rounding == 1 && (powerOfTwoSampleSize < (1 / exactScaleFactor))) { + powerOfTwoSampleSize = powerOfTwoSampleSize << 1; + } + //基于上一步得出的采样大小,根据不同的图片类型,计算采样后的图片尺寸 + if (fileType === "png") { + powerOfTwoWidth = Math.floor(orientedSourceWidth / powerOfTwoSampleSize); + powerOfTwoHeight = Math.floor(orientedSourceHeight / powerOfTwoSampleSize); + } else if (fileType === "webp") { + powerOfTwoWidth = Math.round(orientedSourceWidth / powerOfTwoSampleSize); + powerOfTwoHeight = Math.round(orientedSourceHeight / powerOfTwoSampleSize); + } else { + powerOfTwoWidth = orientedSourceWidth / powerOfTwoSampleSize; + powerOfTwoHeight = orientedSourceHeight / powerOfTwoSampleSize; + } + let a: calculateScaleType = { + "targetWidth": powerOfTwoWidth, + "targetHeight": powerOfTwoHeight + } + return a + } + + round(value: number): number { + return Math.floor(value + 0.5); + } + + isRotationRequired(degreesToRotate: number): boolean { + return degreesToRotate == 90 || degreesToRotate == 270; + } +} diff --git a/library/src/main/ets/downsampling/downsampleUtils.ets b/library/src/main/ets/downsampling/downsampleUtils.ets new file mode 100644 index 0000000..c7590a4 --- /dev/null +++ b/library/src/main/ets/downsampling/downsampleUtils.ets @@ -0,0 +1,29 @@ +/* + * 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. + */ +export enum SampleSizeRounding { + // 采样 + QUALITY, + //不采样 + MEMORY +} + +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); +} \ 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 6d43be1..e2f48a7 100644 --- a/library/src/main/ets/key/DefaultEngineKey.ets +++ b/library/src/main/ets/key/DefaultEngineKey.ets @@ -17,6 +17,7 @@ import { ImageKnifeOption } from '../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 79fe5d0..1cc353a 100644 --- a/library/src/main/ets/model/ImageKnifeData.ets +++ b/library/src/main/ets/model/ImageKnifeData.ets @@ -18,7 +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, imageWidth: number, @@ -102,5 +102,9 @@ export interface RequestJobRequest { memoryKey: string fileCacheFolder: string, isAnimator?: boolean + targetWidth: number + targetHeight: number + autoResize?: boolean + downsampType?: DownsampleStrategy }