降采样

Signed-off-by: tsm <tianshuangming@h-partners.com>
This commit is contained in:
tsm 2024-09-02 11:40:36 +08:00
parent 55fcfe6021
commit f415bb7aa1
20 changed files with 792 additions and 27 deletions

View File

@ -3,6 +3,7 @@
- 修复加载错误图后未去请求排队队列中的请求 - 修复加载错误图后未去请求排队队列中的请求
- 子线程本地Resource参数类型转换成number - 子线程本地Resource参数类型转换成number
- 修改使用hilog记录日志默认打开debug级别的日志 - 修改使用hilog记录日志默认打开debug级别的日志
- 增加降采样
## 3.1.0-rc.2 ## 3.1.0-rc.2
- 修复宽高不等svg图片显示有毛边 - 修复宽高不等svg图片显示有毛边

View File

@ -264,6 +264,17 @@ ImageKnifeAnimatorComponent({
}),animatorOption:this.animatorOption }),animatorOption:this.animatorOption
}).width(300).height(300).backgroundColor(Color.Orange).margin({top:30}) }).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组件 ### ImageKnife组件
| 组件名称 | 入参内容 | 功能简介 | | 组件名称 | 入参内容 | 功能简介 |
@ -306,7 +317,17 @@ ImageKnifeAnimatorComponent({
| drawingColorFilter | ColorFilter | drawing.ColorFilter | 图片变换(可选) | | drawingColorFilter | ColorFilter | drawing.ColorFilter | 图片变换(可选) |
| onComplete | (event:EventImage | undefined) => voi | 颜色滤镜效果(可选) | | onComplete | (event:EventImage | undefined) => voi | 颜色滤镜效果(可选) |
| onLoadListener | onLoadStart: () => void、onLoadSuccess: (data: string | PixelMap | undefined) => void、onLoadFailed: (err: string) => void| 监听图片加载成功与失败 | | onLoadListener | onLoadStart: () => void、onLoadSuccess: (data: string | PixelMap | undefined) => void、onLoadFailed: (err: string) => void| 监听图片加载成功与失败 |
| downsampleOf | DownsampleStrategy | 降采样(可选) |
### 降采样类型
| 类型 | 相关描述 |
|---------------------|-------------------|
| NONE | 不进行降采样 |
| AT_MOST | 请求尺寸大于实际尺寸不进行放大 |
| FIT_CENTER_MEMORY | 两边自适应内存优先 |
| FIT_CENTER_QUALITY | 两边自适应质量优先 |
| CENTER_INSIDE_MEMORY | 宽高缩放比最大的比例,进行缩放适配内存优先 |
| CENTER_INSIDE_QUALITY | 宽高缩放比最大的比例,进行缩放适配质量优先 |
| CENTER_OUTSIDE | 根据宽高的最小的比例,进行适配 |
### ImageKnife接口 ### ImageKnife接口
| 参数名称 | 入参内容 | 功能简介 | | 参数名称 | 入参内容 | 功能简介 |

View File

@ -0,0 +1,208 @@
/*
* 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_MEMORY"),
new SamplingType(4, "FIT_CENTER_QUALITY"),
new SamplingType(5, "CENTER_INSIDE_MEMORY"),
new SamplingType(6, "CENTER_INSIDE_QUALITY"),
new SamplingType(7, "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_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_INSIDE_MEMORY') {
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_MEMORY
})
this.originalPixMap($r('app.media.jpgSample1'))
this.afterSamplingFunc($r('app.media.jpgSample1'))
} else if (value === 'CENTER_INSIDE_QUALITY') {
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_QUALITY
})
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)
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
}
}

View File

@ -47,6 +47,11 @@ struct Index {
}); });
}) })
Button("降采样").margin({top:10}).onClick(() => {
router.push({
uri: 'pages/DownSamplePage',
});
})
Button("单个图片使用").margin({top:10}).onClick(()=>{ Button("单个图片使用").margin({top:10}).onClick(()=>{
router.push({ router.push({
uri: 'pages/SingleImage', uri: 'pages/SingleImage',

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

View File

@ -23,6 +23,7 @@
"pages/ImageAnimatorPage", "pages/ImageAnimatorPage",
"pages/TestSetCustomImagePage", "pages/TestSetCustomImagePage",
"pages/TestErrorHolderPage", "pages/TestErrorHolderPage",
"pages/TestTaskResourcePage" "pages/TestTaskResourcePage",
"pages/DownSamplePage"
] ]
} }

View File

@ -18,6 +18,7 @@ import ImageKnifeOptionTest from './ImageKnifeOption.test';
import MemoryLruCacheTest from './MemoryLruCache.test'; import MemoryLruCacheTest from './MemoryLruCache.test';
import ImageKnifeTest from './ImageKnife.test'; import ImageKnifeTest from './ImageKnife.test';
import Transform from './transform.test'; import Transform from './transform.test';
import SamplingTest from './SamplingTest.test';
export default function testsuite() { export default function testsuite() {
MemoryLruCacheTest(); MemoryLruCacheTest();
@ -26,4 +27,5 @@ export default function testsuite() {
ImageKnifeOptionTest(); ImageKnifeOptionTest();
ImageKnifeTest(); ImageKnifeTest();
Transform(); Transform();
SamplingTest()
} }

View File

@ -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);
})
})
}

View File

@ -66,4 +66,6 @@ export { CropTransformation } from './src/main/ets/transform/CropTransformation'
export { MaskTransformation } from './src/main/ets/transform/MaskTransformation' export { MaskTransformation } from './src/main/ets/transform/MaskTransformation'
export { SepiaTransformation } from './src/main/ets/transform/SepiaTransformation' export { SepiaTransformation } from './src/main/ets/transform/SepiaTransformation'
export { DownsampleStrategy } from './src/main/ets/downsampling/DownsampleStartegy'

View File

@ -40,6 +40,8 @@ import {
} from './model/ImageKnifeData' } from './model/ImageKnifeData'
import { combineArrayBuffers } from './model/utils'; import { combineArrayBuffers } from './model/utils';
import { BusinessError } from '@kit.BasicServicesKit'; import { BusinessError } from '@kit.BasicServicesKit';
import { Downsampler } from './downsampling/Downsampler';
import { DownsampleStrategy } from './downsampling/DownsampleStartegy';
export class ImageKnifeDispatcher { export class ImageKnifeDispatcher {
// 最大并发 // 最大并发
@ -189,7 +191,10 @@ export class ImageKnifeDispatcher {
fileCacheFolder: ImageKnife.getInstance().getFileCache()?.getCacheFolder(), fileCacheFolder: ImageKnife.getInstance().getFileCache()?.getCacheFolder(),
isAnimator:isAnimator, isAnimator:isAnimator,
moduleName: moduleName == "" ? undefined : moduleName, moduleName: moduleName == "" ? undefined : moduleName,
resName: resName == "" ? undefined : resName resName: resName == "" ? undefined : resName,
targetWidth: currentRequest.componentWidth,
targetHeight: currentRequest.componentHeight,
downsampType: currentRequest.imageKnifeOption.downsampleOf,
} }
if(request.customGetImage == undefined) { if(request.customGetImage == undefined) {
@ -204,7 +209,7 @@ export class ImageKnifeDispatcher {
emitter.on(Constants.PROGRESS_EMITTER + memoryKey, (data) => { emitter.on(Constants.PROGRESS_EMITTER + memoryKey, (data) => {
this.progressCallBack(requestList! , data?.data?.value as number) this.progressCallBack(requestList! , data?.data?.value as number)
}); });
} }
LogUtil.log("ImageKnife_DataTime_getAndShowImage_execute.start:" + currentRequest.imageKnifeOption.loadSrc) LogUtil.log("ImageKnife_DataTime_getAndShowImage_execute.start:" + currentRequest.imageKnifeOption.loadSrc)
taskpool.execute(task).then((res: Object) => { taskpool.execute(task).then((res: Object) => {
@ -340,8 +345,8 @@ export class ImageKnifeDispatcher {
} }
} else { } else {
if (requestWithSource.source == ImageKnifeRequestSource.SRC && requestWithSource.request.imageKnifeOption.onLoadListener?.onLoadCancel) { if (requestWithSource.source == ImageKnifeRequestSource.SRC && requestWithSource.request.imageKnifeOption.onLoadListener?.onLoadCancel) {
// 回调请求成功 // 回调请求成功
requestWithSource.request.imageKnifeOption.onLoadListener.onLoadCancel("component has destroyed") requestWithSource.request.imageKnifeOption.onLoadListener.onLoadCancel("component has destroyed")
} }
} }
}); });
@ -484,7 +489,7 @@ async function requestJob(request: RequestJobRequest, requestList?: List<ImageKn
if (data == 200 || data == 206 || data == 204) { if (data == 200 || data == 206 || data == 204) {
resBuf = combineArrayBuffers(arrayBuffers) resBuf = combineArrayBuffers(arrayBuffers)
} else { } else {
loadError = "HttpDownloadClient has error, http code =" + JSON.stringify(data) loadError = "HttpDownloadClient has error, http code =" + JSON.stringify(data)
} }
}).catch((err: Error) => { }).catch((err: Error) => {
loadError = err.message; loadError = err.message;
@ -505,21 +510,21 @@ async function requestJob(request: RequestJobRequest, requestList?: List<ImageKn
loadError = "success get image from filecache for key = " + fileKey; loadError = "success get image from filecache for key = " + fileKey;
} }
} else if (request.src.startsWith('datashare://') || request.src.startsWith('file://')) { } else if (request.src.startsWith('datashare://') || request.src.startsWith('file://')) {
await fs.open(request.src, fs.OpenMode.READ_ONLY).then(async (file) => { await fs.open(request.src, fs.OpenMode.READ_ONLY).then(async (file) => {
await fs.stat(file.fd).then(async (stat) =>{ await fs.stat(file.fd).then(async (stat) =>{
let buf = new ArrayBuffer(stat.size); let buf = new ArrayBuffer(stat.size);
await fs.read(file.fd, buf).then((readLen) => { await fs.read(file.fd, buf).then((readLen) => {
resBuf = buf; resBuf = buf;
fs.close(file.fd); fs.close(file.fd);
}).catch((err:BusinessError) => {
loadError = 'LoadDataShareFileClient fs.read err happened uri=' + request.src + " err.msg=" + err?.message + " err.code=" + err?.code;
})
}).catch((err:BusinessError) => { }).catch((err:BusinessError) => {
loadError = 'LoadDataShareFileClient fs.stat err happened uri=' + request.src + " err.msg=" + err?.message + " err.code=" + err?.code; loadError = 'LoadDataShareFileClient fs.read err happened uri=' + request.src + " err.msg=" + err?.message + " err.code=" + err?.code;
}) })
}).catch((err:BusinessError) => { }).catch((err:BusinessError) => {
loadError ='LoadDataShareFileClient fs.open err happened uri=' + request.src + " err.msg=" + err?.message + " err.code=" + err?.code; loadError = 'LoadDataShareFileClient fs.stat err happened uri=' + request.src + " err.msg=" + err?.message + " err.code=" + err?.code;
}) })
}).catch((err:BusinessError) => {
loadError ='LoadDataShareFileClient fs.open err happened uri=' + request.src + " err.msg=" + err?.message + " err.code=" + err?.code;
})
} else { //从本地文件获取 } else { //从本地文件获取
try { try {
let stat = fs.statSync(request.src); let stat = fs.statSync(request.src);
@ -651,6 +656,20 @@ async function requestJob(request: RequestJobRequest, requestList?: List<ImageKn
editable: true, editable: true,
desiredSize: defaultSize desiredSize: defaultSize
}; };
let imageInfo = await imageSource.getImageInfo()
if ((request.downsampType !== DownsampleStrategy.NONE && request.downsampType !== undefined) &&
request.requestSource == ImageKnifeRequestSource.SRC) {
let reqSize =
new Downsampler().calculateScaling(typeValue, imageInfo.size.width, imageInfo.size.height, request.targetWidth,
request.targetHeight, request.downsampType)
opts = {
editable: true,
desiredSize: {
height: vp2px(reqSize.targetHeight),
width: vp2px(reqSize.targetWidth)
}
}
}
await imageSource.createPixelMap(opts) await imageSource.createPixelMap(opts)
.then((pixelmap: PixelMap) => { .then((pixelmap: PixelMap) => {
resPixelmap = pixelmap resPixelmap = pixelmap
@ -663,6 +682,22 @@ async function requestJob(request: RequestJobRequest, requestList?: List<ImageKn
type:typeValue type:typeValue
}; };
} }
let imageInfo = await imageSource.getImageInfo()
if ((request.downsampType !== undefined && request.downsampType !== DownsampleStrategy.NONE) &&
request.requestSource == ImageKnifeRequestSource.SRC) {
let reqSize =
new Downsampler().calculateScaling(typeValue, imageInfo.size.width, imageInfo.size.height, request.targetWidth,
request.targetHeight, request.downsampType)
decodingOptions = {
editable: true,
desiredSize: {
width: reqSize.targetWidth,
height: reqSize.targetHeight
}
}
}
let size = (await imageSource.getImageInfo()).size let size = (await imageSource.getImageInfo()).size
await imageSource.createPixelMap(decodingOptions) await imageSource.createPixelMap(decodingOptions)
.then((pixelmap: PixelMap) => { .then((pixelmap: PixelMap) => {

View File

@ -16,6 +16,7 @@ import taskpool from '@ohos.taskpool';
import common from '@ohos.app.ability.common' import common from '@ohos.app.ability.common'
import { CacheStrategy, ImageKnifeData,EventImage } from './model/ImageKnifeData'; import { CacheStrategy, ImageKnifeData,EventImage } from './model/ImageKnifeData';
import { PixelMapTransformation } from './transform/PixelMapTransformation'; import { PixelMapTransformation } from './transform/PixelMapTransformation';
import { DownsampleStrategy } from './downsampling/DownsampleStartegy';
import { drawing } from '@kit.ArkGraphics2D'; import { drawing } from '@kit.ArkGraphics2D';
export interface HeaderOptions { export interface HeaderOptions {
@ -90,6 +91,8 @@ interface ImageOption {
onLoadListener?: OnLoadCallBack | undefined; onLoadListener?: OnLoadCallBack | undefined;
onComplete?:(event:EventImage | undefined) => void onComplete?:(event:EventImage | undefined) => void
drawingColorFilter?: ColorFilter | drawing.ColorFilter drawingColorFilter?: ColorFilter | drawing.ColorFilter
downsampleOf?: DownsampleStrategy
} }
@ObservedV2 @ObservedV2
export class ImageKnifeOption { export class ImageKnifeOption {
@ -121,6 +124,9 @@ export class ImageKnifeOption {
onLoadListener?: OnLoadCallBack | undefined; onLoadListener?: OnLoadCallBack | undefined;
onComplete?:(event:EventImage | undefined) => void onComplete?:(event:EventImage | undefined) => void
drawingColorFilter?: ColorFilter | drawing.ColorFilter drawingColorFilter?: ColorFilter | drawing.ColorFilter
// 下采样
downsampleOf?: DownsampleStrategy = DownsampleStrategy.NONE
constructor(option?:ImageOption) { constructor(option?:ImageOption) {
this.loadSrc = option?.loadSrc == undefined ? "" : option?.loadSrc this.loadSrc = option?.loadSrc == undefined ? "" : option?.loadSrc
this.placeholderSrc = option?.placeholderSrc this.placeholderSrc = option?.placeholderSrc
@ -141,6 +147,7 @@ export class ImageKnifeOption {
this.onLoadListener = option?.onLoadListener this.onLoadListener = option?.onLoadListener
this.onComplete = option?.onComplete this.onComplete = option?.onComplete
this.drawingColorFilter = option?.drawingColorFilter this.drawingColorFilter = option?.drawingColorFilter
this.downsampleOf = option?.downsampleOf
} }
} }

View File

@ -15,7 +15,7 @@
import { ImageKnifeOption } from './ImageKnifeOption'; import { ImageKnifeOption } from './ImageKnifeOption';
import common from '@ohos.app.ability.common'; import common from '@ohos.app.ability.common';
import { ImageKnifeRequestSource } from './model/ImageKnifeData'; import { ImageKnifeRequestSource } from './model/ImageKnifeData';
import { DownsampleStrategy } from './downsampling/DownsampleStartegy';
export class ImageKnifeRequest { export class ImageKnifeRequest {
requestState: ImageKnifeRequestState = ImageKnifeRequestState.PROGRESS requestState: ImageKnifeRequestState = ImageKnifeRequestState.PROGRESS
@ -27,18 +27,22 @@ export class ImageKnifeRequest {
ImageKnifeRequestCallback: ImageKnifeRequestCallback ImageKnifeRequestCallback: ImageKnifeRequestCallback
componentVersion: number = 0 componentVersion: number = 0
headers: Map<string,Object> = new Map<string,Object>() headers: Map<string,Object> = new Map<string,Object>()
downsampType?: DownsampleStrategy
constructor(option: ImageKnifeOption, constructor(option: ImageKnifeOption,
uIAbilityContext: common.UIAbilityContext, uIAbilityContext: common.UIAbilityContext,
width: number, width: number,
height: number, height: number,
version: number, version: number,
ImageKnifeRequestCallback: ImageKnifeRequestCallback) { ImageKnifeRequestCallback: ImageKnifeRequestCallback,
downsampType?: DownsampleStrategy,
) {
this.imageKnifeOption = option this.imageKnifeOption = option
this.context = uIAbilityContext this.context = uIAbilityContext
this.componentWidth = width this.componentWidth = width
this.componentHeight = height this.componentHeight = height
this.componentVersion = version this.componentVersion = version
this.ImageKnifeRequestCallback = ImageKnifeRequestCallback this.ImageKnifeRequestCallback = ImageKnifeRequestCallback
this.downsampType = downsampType
} }
// RequestOption调用header对于的方法 // RequestOption调用header对于的方法
addHeader(key: string, value: Object) { addHeader(key: string, value: Object) {

View File

@ -0,0 +1,26 @@
/*
* 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
getSampleSizeRounding(sourceWidth: number, sourceHeight: number, requestWidth: number,
requestHeight: number,downsampType?:DownsampleStrategy): SampleSizeRounding
}

View File

@ -0,0 +1,184 @@
/*
* 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(sourceWidth: number, sourceHeight: number, requestedWidth: number, requestedHeight: number,downsampType:DownsampleStrategy
): number {
if (downsampType===DownsampleStrategy.FIT_CENTER_QUALITY) {
const widthPercentage = requestedWidth / sourceWidth
const heightPercentage = requestedHeight / sourceHeight
return Math.min(widthPercentage, heightPercentage)
} else {
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,downsampType:DownsampleStrategy
): number {
if (downsampType===DownsampleStrategy.FIT_CENTER_QUALITY) {
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,downsampType:DownsampleStrategy
): number {
//获取 FIT_CENTER 的缩放因子
const fitCenterScaleFactor: ESObject =
this.getScale(sourceWidth, sourceHeight, requestedWidth, requestedHeight, downsampType);
//返回不超过 1 的缩放因子,即尽量缩小图像以适应目标尺寸,但不会放大
return Math.min(1, fitCenterScaleFactor);
}
getScale(sourceWidth: number, sourceHeight: number, requestedWidth: number, requestedHeight: number,downsampType:DownsampleStrategy
): number {
if (downsampType===DownsampleStrategy.FIT_CENTER_QUALITY) {
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,downsampType:DownsampleStrategy
): SampleSizeRounding {
//如果缩放因子为 1表示没有缩放优先选择质量
if (this.getScaleFactor(sourceWidth, sourceHeight, requestedWidth, requestedHeight, downsampType) === 1) {
return SampleSizeRounding.QUALITY
}
//否则,使用 FIL_CENTER 的 SampleSizeRounding 值
return this.getSampleSize(sourceWidth, sourceHeight, requestedWidth, requestedHeight,downsampType);
}
getSampleSize(sourceWidth: number, sourceHeight: number, requestedWidth: number, requestedHeight: number,downsampType:DownsampleStrategy
): SampleSizeRounding {
if (downsampType===DownsampleStrategy.CENTER_INSIDE_QUALITY) {
return SampleSizeRounding.QUALITY;
} else {
return SampleSizeRounding.MEMORY;
}
}
}
export enum DownsampleStrategy {
//请求尺寸大于实际尺寸不进行放大
AT_MOST,
//两边自适应内存优先
FIT_CENTER_MEMORY ,
//两边自适应质量优先
FIT_CENTER_QUALITY,
//按照宽高比的最大比进行适配内存优先
CENTER_INSIDE_MEMORY ,
//按照宽高比的最大比进行适配质量优先
CENTER_INSIDE_QUALITY,
//宽高进行等比缩放宽高里面最小的比例先放进去,然后再根据原图的缩放比去适配
CENTER_OUTSIDE,
//不进行降采样
NONE,
}

View File

@ -0,0 +1,36 @@
/*
* 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 {
/**
* 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
}
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);
}

View File

@ -0,0 +1,147 @@
/*
* 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,
): 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 | undefined
let rounding: SampleSizeRounding | undefined
switch (downsampType) {
case DownsampleStrategy.FIT_CENTER_MEMORY || DownsampleStrategy.FIT_CENTER_QUALITY:
exactScaleFactor = new FitCenter()
.getScaleFactor(orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight,downsampType)
rounding = new FitCenter()
.getSampleSizeRounding(orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight,downsampType)
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_MEMORY || DownsampleStrategy.CENTER_INSIDE_QUALITY:
exactScaleFactor = new CenterInside()
.getScaleFactor(orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight,downsampType)
rounding = new CenterInside()
.getSampleSizeRounding(orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight,downsampType)
break
case DownsampleStrategy.CENTER_OUTSIDE:
exactScaleFactor = new CenterOutside()
.getScaleFactor(orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight)
rounding = new CenterOutside()
.getSampleSizeRounding(orientedSourceWidth, orientedSourceHeight, targetWidth, targetHeight)
break
}
if (exactScaleFactor==undefined|| exactScaleFactor <= 0 ) {
throw new Error("Cannot round with exactScaleFactor");
}
if (rounding == undefined) {
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 ==SampleSizeRounding.QUALITY ? 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;
}
}

View File

@ -17,6 +17,7 @@ import { ImageKnifeOption } from '../ImageKnifeOption';
import { IEngineKey } from './IEngineKey'; import { IEngineKey } from './IEngineKey';
import { PixelMapTransformation } from '../transform/PixelMapTransformation'; import { PixelMapTransformation } from '../transform/PixelMapTransformation';
import { ImageKnifeRequestSource } from '../model/ImageKnifeData'; import { ImageKnifeRequestSource } from '../model/ImageKnifeData';
import { DownsampleStrategy } from '../downsampling/DownsampleStartegy';
@Sendable @Sendable
export class DefaultEngineKey implements IEngineKey { export class DefaultEngineKey implements IEngineKey {
@ -31,6 +32,9 @@ export class DefaultEngineKey implements IEngineKey {
if (imageKnifeOption.transformation) { if (imageKnifeOption.transformation) {
key += "transformation=" + this.getTransformation(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 return key
} }

View File

@ -18,7 +18,7 @@ import { IEngineKey } from '../key/IEngineKey'
import { PixelMapTransformation } from '../transform/PixelMapTransformation' import { PixelMapTransformation } from '../transform/PixelMapTransformation'
import common from '@ohos.app.ability.common'; import common from '@ohos.app.ability.common';
import { Size } from '@kit.ArkUI' import { Size } from '@kit.ArkUI'
import { DownsampleStrategy } from '../downsampling/DownsampleStartegy'
export interface ImageKnifeData { export interface ImageKnifeData {
source: PixelMap | string, source: PixelMap | string,
imageWidth: number, imageWidth: number,
@ -103,6 +103,9 @@ export interface RequestJobRequest {
fileCacheFolder: string, fileCacheFolder: string,
isAnimator?: boolean, isAnimator?: boolean,
moduleName?:string, moduleName?:string,
resName?: string resName?: string,
targetWidth: number
targetHeight: number
downsampType?: DownsampleStrategy
} }