/*
* 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 { ImageKnifeRequest, ImageKnifeRequestState } from './model/ImageKnifeRequest';
import { CacheStrategy, ImageKnifeData, ImageKnifeRequestSource } from './model/ImageKnifeData';
import { MemoryLruCache } from './cache/MemoryLruCache';
import { IMemoryCache } from './cache/IMemoryCache'
import { FileCache } from './cache/FileCache';
import { ImageKnifeDispatcher } from './ImageKnifeDispatcher';
import { IEngineKey } from './key/IEngineKey';
import { HeaderOptions, ImageKnifeOption, ImageKnifeOptionV2 } from './model/ImageKnifeOption';
import { FileTypeUtil } from './utils/FileTypeUtil';
import { util } from '@kit.ArkTS';
import { image } from '@kit.ImageKit';
import { common } from '@kit.AbilityKit';
import { LogUtil } from './utils/LogUtil';
import { BusinessError, emitter } from '@kit.BasicServicesKit';
export class ImageKnife {
private static instance: ImageKnife;
// 内存缓存
private memoryCache: IMemoryCache = new MemoryLruCache(256, 128 * 1024 * 1024);
// 文件缓存
private fileCache?: FileCache
private dispatcher: ImageKnifeDispatcher = new ImageKnifeDispatcher()
// 配置全局是否在子线程加载图片请求
private _isRequestInSubThread: boolean = true;
//定义全局网络请求header map
headerMap: Map<string, Object> = new Map<string, Object>();
customGetImage: ((context: Context, src: string | PixelMap | Resource,headers?: Record<string,Object>) => Promise<ArrayBuffer | undefined>) | undefined = undefined
readTimeout?: number
connectTimeout?: number
// JPEG优化解码开关,启用后使用YUV格式解码以减少内存占用
private enableJpegOptimizeDecoding: boolean = false;
public static getInstance(): ImageKnife {
if (!ImageKnife.instance) {
ImageKnife.instance = new ImageKnife();
}
return ImageKnife.instance;
}
private constructor() {
}
getConnectTimeout() {
return this.connectTimeout
}
getReadTimeout() {
return this.readTimeout
}
// 设置连接超时时长
setConnectTimeout(timeout: number) {
this.connectTimeout = timeout
}
// 设置读取超时时长
setReadTimeout(timeout: number) {
this.readTimeout = timeout
}
public set isRequestInSubThread(value: boolean) {
this._isRequestInSubThread = value;
}
public get isRequestInSubThread(): boolean {
return this._isRequestInSubThread;
}
/**
* 初始化文件缓存个数,大小,以及路径
* @param context 上下文
* @param size 缓存数量
* @param memory 内存大小
* @param path 文件目录
*/
async initFileCache(context: Context, size: number = 256, memory: number = 256 * 1024 * 1024,path?: string) {
this.fileCache = new FileCache(context, size, memory)
if ( path != undefined ) {
await this.fileCache.initFileCache(path)
} else {
await this.fileCache.initFileCache()
}
}
/**
* 判断文件缓存是否已完成初始化
* @returns 是否初始化
*/
public isFileCacheInit(): boolean {
return this.fileCache === undefined ? false : this.fileCache.isFileCacheInit()
}
/**
* 重新加载
*/
reload(request: ImageKnifeRequest) {
if (request.requestState == ImageKnifeRequestState.ERROR) {
request.requestState = ImageKnifeRequestState.PROGRESS
ImageKnife.getInstance().execute(request)
}
}
/**
* 全局添加单个请求头header
* @param key 请求头属性
* @param value 请求头值
*/
addHeader(key: string, value: Object) {
this.headerMap.set(key, value)
}
/**
* 全局设置请求头header
* @param options 请求头数组
*/
serHeaderOptions(options: Array<HeaderOptions>) {
options.forEach((value) => {
this.headerMap.set(value.key, value.value)
})
}
/**
* 删除单个请求头header
* @param key 请求头属性
*/
deleteHeader(key: string) {
this.headerMap.delete(key)
}
/**
* 设置自定义的内存缓存
* @param newMemoryCache 自定义内存缓存
*/
initMemoryCache(newMemoryCache: IMemoryCache): void {
this.memoryCache = newMemoryCache
}
/**
* 清除所有内存缓存
*/
removeAllMemoryCache(): void {
this.memoryCache.removeAll()
}
/**
* 清除指定内存缓存
* @param url 待清除的url路径或ImageKnifeOption
*/
removeMemoryCache(url: string | ImageKnifeOption | ImageKnifeOptionV2) {
let imageKnifeOption:ImageKnifeOption | ImageKnifeOptionV2 = new ImageKnifeOption();
if (typeof url == 'string') {
imageKnifeOption.loadSrc = url;
} else {
imageKnifeOption = url;
}
let key = this.getEngineKeyImpl().generateMemoryKey(imageKnifeOption.loadSrc, ImageKnifeRequestSource.SRC, imageKnifeOption);
this.memoryCache.remove(key);
}
/**
* 预加载
* @param loadSrc 图片地址url
* @returns 图片请求request
*/
preload(loadSrc:string | ImageKnifeOption | ImageKnifeOptionV2):ImageKnifeRequest{
let imageKnifeOption:ImageKnifeOption | ImageKnifeOptionV2 = new ImageKnifeOption()
if (typeof loadSrc == 'string') {
imageKnifeOption.loadSrc = loadSrc
} else {
imageKnifeOption = loadSrc;
}
let request = new ImageKnifeRequest(
imageKnifeOption,
imageKnifeOption.context !== undefined ? imageKnifeOption.context : this.fileCache?.context as common.UIAbilityContext,
0,
0,
0,
{
showPixelMap(version: number, pixelMap: PixelMap | string | Resource) {
}
}
)
this.execute(request)
return request
}
/**
* 取消图片请求
* @param request 图片请求request
*/
cancel(request:ImageKnifeRequest) {
if (typeof request?.imageKnifeOption.loadSrc === 'string' && !request?.drawMainSuccess) {
emitter.emit(request.imageKnifeOption.loadSrc + request.componentId)
}
request.requestState = ImageKnifeRequestState.DESTROY
}
/**
* 预加载图片到文件缓存
* @param loadSrc 图片地址url
* @returns 返回文件缓存路径
*/
preLoadCache(loadSrc: string | ImageKnifeOption | ImageKnifeOptionV2): Promise<string> {
return new Promise((resolve, reject) => {
let imageKnifeOption: ImageKnifeOption | ImageKnifeOptionV2 = new ImageKnifeOption()
if (typeof loadSrc == 'string') {
imageKnifeOption.loadSrc = loadSrc
} else {
imageKnifeOption = loadSrc;
}
LogUtil.log('ImageKnife_DataTime_preLoadCache-imageKnifeOption:'+loadSrc)
let fileKey = this.getEngineKeyImpl().generateFileKey(imageKnifeOption.loadSrc, imageKnifeOption.signature)
let cachePath = ImageKnife.getInstance().getFileCache().getFileToPath(fileKey)
if (cachePath == null || cachePath == '' || cachePath == undefined) {
let request = new ImageKnifeRequest(
imageKnifeOption,
imageKnifeOption.context !== undefined ? imageKnifeOption.context : this.fileCache?.context as common.UIAbilityContext,
0,
0,
0,
{
showPixelMap(version: number, pixelMap: PixelMap | string | Resource , requestSource: ImageKnifeRequestSource,size?:Size) {
if (requestSource === ImageKnifeRequestSource.SRC) {
resolve(ImageKnife.getInstance().getFileCache().getFileToPath(fileKey))
}
},
mainLoadError:(err: string) =>{
reject(err)
}
}
)
this.execute(request)
} else {
resolve(cachePath)
}
})
}
private getOption(loadSrc: string | ImageKnifeOption | ImageKnifeOptionV2,signature?: string): ImageKnifeOption | ImageKnifeOptionV2 {
let option: ImageKnifeOption | ImageKnifeOptionV2 = {
loadSrc: '',
signature:signature
}
if (typeof loadSrc === 'string') {
option.loadSrc = loadSrc
} else {
option = loadSrc
}
return option
}
/**
* 从内存或文件缓存中获取图片数据
* @param url 图片地址url
* @param cacheType 缓存策略
* @returns 图片数据
* @param signature key自定义信息
*/
getCacheImage(loadSrc: string | ImageKnifeOption | ImageKnifeOptionV2,
cacheType: CacheStrategy = CacheStrategy.Default, signature?: string): Promise<ImageKnifeData | undefined> {
let option = this.getOption(loadSrc,signature)
return new Promise((resolve, reject) => {
if (cacheType === CacheStrategy.Memory) {
resolve(this.readMemoryCache(option))
} else if (cacheType === CacheStrategy.File) {
this.readFileCache(option.loadSrc, resolve)
} else {
let data = this.readMemoryCache(option)
data == undefined ? this.readFileCache(option.loadSrc, resolve) : resolve(data)
}
})
}
/**
* 从内存或文件缓存中获取图片数据同步接口
* @param url 图片地址url
* @param cacheType 缓存策略
* @returns 图片数据
* @param signature key自定义信息
*/
getCacheImageSync(loadSrc: string | ImageKnifeOption | ImageKnifeOptionV2,
cacheType: CacheStrategy = CacheStrategy.Default, signature?: string): ImageKnifeData | undefined {
let option = this.getOption(loadSrc,signature)
if (cacheType === CacheStrategy.Memory) {
return this.readMemoryCache(option)
} else if (cacheType === CacheStrategy.File) {
return this.readFileCacheSync(option)
} else {
let data = this.readMemoryCache(option)
return data == undefined ? this.readFileCacheSync(option) : data
}
}
/**
* 预加载缓存(用于外部已获取pixelmap,需要加入imageknife缓存的场景)
* @param url 图片地址url
* @param pixelMap 图片
* @param cacheType 缓存策略
* @param signature key自定义信息
*/
putCacheImage(url: string, pixelMap: PixelMap, cacheType: CacheStrategy = CacheStrategy.Default,packOption?: image.PackingOption, signature?: string) {
let memoryKey = this.getEngineKeyImpl()
.generateMemoryKey(url, ImageKnifeRequestSource.SRC, { loadSrc: url, signature: signature });
let fileKey = this.getEngineKeyImpl().generateFileKey(url, signature);
let imageKnifeData: ImageKnifeData = { source: pixelMap, imageWidth: 0, imageHeight: 0 };
switch (cacheType) {
case CacheStrategy.Default:
this.saveMemoryCache(memoryKey, imageKnifeData);
if (packOption) {
this.pixelMapToOriginal(pixelMap,packOption,(buffer)=>{
this.saveFileCache(fileKey, buffer);
})
} else {
this.saveFileCache(fileKey, this.pixelMapToArrayBuffer(pixelMap));
}
break;
case CacheStrategy.File:
if (packOption) {
this.pixelMapToOriginal(pixelMap,packOption,(buffer)=>{
this.saveFileCache(fileKey, buffer);
})
} else {
this.saveFileCache(fileKey, this.pixelMapToArrayBuffer(pixelMap));
}
break
case CacheStrategy.Memory:
this.saveMemoryCache(memoryKey, imageKnifeData);
break
}
}
/**
* 清除所有文件缓存
* @returns
*/
async removeAllFileCache(): Promise<void> {
if (this.fileCache !== undefined) {
await this.fileCache.removeAll()
}
}
/*
* 清除指定文件缓存
* */
removeFileCache(url: string | ImageKnifeOption | ImageKnifeOptionV2) {
let imageKnifeOption:ImageKnifeOption | ImageKnifeOptionV2 = new ImageKnifeOption();
if (typeof url == 'string') {
imageKnifeOption.loadSrc = url;
} else {
imageKnifeOption = url;
}
let key = this.getEngineKeyImpl().generateFileKey(imageKnifeOption.loadSrc, imageKnifeOption.signature);
if (this.fileCache !== undefined) {
this.fileCache.remove(key);
}
}
/**
* 设置taskpool默认并发数量
* @param concurrency 默认并发数量,默认为8
*/
setMaxRequests(concurrency: number): void {
this.dispatcher.setMaxRequests(concurrency)
}
getFileCacheByFile(context: Context, key: string): ArrayBuffer | undefined {
if (this.fileCache !== undefined) {
return FileCache.getFileCacheByFile(context, key)
}
return undefined
}
loadFromMemoryCache(key: string): ImageKnifeData | undefined {
if (key !== '') {
return this.memoryCache.get(key)
}
return undefined
}
saveMemoryCache(key: string, data: ImageKnifeData): void {
if (key !== '') {
this.memoryCache.put(key, data)
}
}
loadFromFileCache(key: string): ArrayBuffer | undefined {
return this.fileCache?.get(key)
}
saveFileCache(key: string, data: ArrayBuffer): void {
this.fileCache?.put(key, data)
}
getFileCache(): FileCache {
return this.fileCache as FileCache
}
/**
* get cache upper limit
* @param cacheType
* @returns
*/
getCacheLimitSize(cacheType?: CacheStrategy): number | undefined {
if (cacheType == undefined || cacheType == CacheStrategy.Default) {
cacheType = CacheStrategy.Memory;
}
if (cacheType == CacheStrategy.Memory) {
return (this.memoryCache as MemoryLruCache).maxMemory;
} else {
if (this.isFileCacheInit()) {
return this.fileCache?.maxMemory;
} else {
throw new Error('the disk cache not init');
}
}
}
/**
* gets the number of images currently cached
* @param cacheType
* @returns
*/
getCurrentCacheNum(cacheType: CacheStrategy): number | undefined {
if (cacheType == undefined || cacheType == CacheStrategy.Default) {
cacheType = CacheStrategy.Memory;
}
if (cacheType == CacheStrategy.Memory) {
return (this.memoryCache as MemoryLruCache).size();
} else {
if (this.isFileCacheInit()) {
return this.fileCache?.size();
} else {
throw new Error('the disk cache not init');
}
}
}
/**
* gets the current cache size
* @param cacheType
* @returns
*/
getCurrentCacheSize(cacheType: CacheStrategy): number | undefined {
if (cacheType == undefined || cacheType == CacheStrategy.Default) {
cacheType = CacheStrategy.Memory;
}
if (cacheType == CacheStrategy.Memory) {
return (this.memoryCache as MemoryLruCache).currentMemory;
} else {
if (this.isFileCacheInit()) {
return this.fileCache?.currentMemory;
} else {
throw new Error('the disk cache not init');
}
}
}
private pixelMapToArrayBuffer(pixelMap: PixelMap): ArrayBuffer {
let imageInfo = pixelMap.getImageInfoSync();
let readBuffer: ArrayBuffer = new ArrayBuffer(imageInfo.size.height * imageInfo.size.width * 4);
pixelMap.readPixelsToBufferSync(readBuffer);
return readBuffer
}
private pixelMapToOriginal(pixelMap: PixelMap, packOption: image.PackingOption,
onComplete: (buffer: ArrayBuffer) => void) {
const imagePackObj = image.createImagePacker()
imagePackObj.packToData(pixelMap,packOption)
.then((data: ArrayBuffer)=>{
onComplete(data)
}).catch((err: BusinessError)=>{
LogUtil.error(`Fail to pack the image.code ${err.code} ,message is ${err.message}`)
})
}
private readMemoryCache( option: ImageKnifeOption | ImageKnifeOptionV2): ImageKnifeData | undefined {
let memoryKey = this.getEngineKeyImpl().generateMemoryKey(option.loadSrc, ImageKnifeRequestSource.SRC, option)
return ImageKnife.getInstance()
.loadFromMemoryCache(memoryKey)
}
private readFileCache(loadSrc: string | image.PixelMap | Resource, onComplete: (data: ImageKnifeData | undefined) => void) {
let keys = this.getEngineKeyImpl().generateFileKey(loadSrc)
let buffer = ImageKnife.getInstance().loadFromFileCache(keys)
if (buffer != undefined) {
let fileTypeUtil = new FileTypeUtil();
let typeValue = fileTypeUtil.getFileType(buffer);
if (typeValue === 'gif' || typeValue === 'webp') {
let base64Help = new util.Base64Helper()
let base64str = 'data:image/' + typeValue + ';base64,' + base64Help.encodeToStringSync(new Uint8Array(buffer))
onComplete({
source: base64str,
imageWidth: 0,
imageHeight: 0
})
}
let imageSource: image.ImageSource = image.createImageSource(buffer);
let decodingOptions: image.DecodingOptions = {
editable: true,
}
imageSource.createPixelMap(decodingOptions)
.then((pixelmap: PixelMap) => {
onComplete({
source: pixelmap,
imageWidth: 0,
imageHeight: 0
})
imageSource.release()
})
} else {
onComplete(undefined)
}
}
private readFileCacheSync(option: ImageKnifeOption | ImageKnifeOptionV2): ImageKnifeData | undefined {
let keys = this.getEngineKeyImpl().generateFileKey(option.loadSrc)
let buffer = ImageKnife.getInstance().loadFromFileCache(keys)
if (buffer != undefined) {
let fileTypeUtil = new FileTypeUtil();
let typeValue = fileTypeUtil.getFileType(buffer);
if (typeValue === 'gif' || typeValue === 'webp') {
let base64Help = new util.Base64Helper()
let base64str = 'data:image/' + typeValue + ';base64,' + base64Help.encodeToStringSync(new Uint8Array(buffer))
return {
source: base64str,
imageWidth: 0,
imageHeight: 0
}
}
let imageSource: image.ImageSource = image.createImageSource(buffer);
try {
let decodingOptions: image.DecodingOptions = {
editable: false,
}
let size = imageSource.getImageInfoSync().size
let pixel = imageSource.createPixelMapSync(decodingOptions)
return {
source: pixel,
imageWidth: size.width,
imageHeight: size.height
}
} catch (e) {
return
} finally {
imageSource.release()
}
} else {
return
}
}
saveWithoutWriteFile(key: string, bufferSize: number): void {
this.fileCache?.putWithoutWriteFile(key, bufferSize)
}
saveFileCacheOnlyFile(context: Context, key: string, value: ArrayBuffer): boolean {
if (this.fileCache !== undefined) {
return FileCache.saveFileCacheOnlyFile(context, key, value)
}
return false
}
async execute(request: ImageKnifeRequest): Promise<void> {
LogUtil.log('ImageKnife_DataTime_execute.start:'+request.imageKnifeOption.loadSrc)
if (this.headerMap.size > 0) {
request.addHeaderMap(this.headerMap)
}
this.dispatcher.enqueue(request)
LogUtil.log('ImageKnife_DataTime_execute.end:'+request.imageKnifeOption.loadSrc)
}
setEngineKeyImpl(impl: IEngineKey): void {
this.dispatcher.setEngineKeyImpl(impl);
}
getEngineKeyImpl(): IEngineKey {
return this.dispatcher.getEngineKeyImpl();
}
/**
* 全局设置自定义下载
* @param customGetImage 自定义请求函数
*/
setCustomGetImage(customGetImage?: (context: Context, src: string | PixelMap | Resource,headers?: Record<string,Object>) => Promise<ArrayBuffer | undefined>) {
this.customGetImage = customGetImage
}
getCustomGetImage(): undefined | ((context: Context, src: string | PixelMap | Resource,headers?: Record<string,Object>) => Promise<ArrayBuffer | undefined>){
return this.customGetImage
}
/**
* 设置默认解码是否使用优化jpeg图片解码
* 没有调用时,默认不启用
* 设置为true后,会优化Jpeg解码,以减小解码后的图片内存占用
* 如果Jpeg图片设置了图形变换,则不会应用该解码优化,仍然使用默认的解码方式
* 启用后,getCacheImage接口可能获取到非RGBA像素格式的pixelmap
* @param enable 是否开启jpeg解码优化
*/
setJpegOptimizeDecoding(enable: boolean): void {
this.enableJpegOptimizeDecoding = enable;
}
/**
* 获取默认解码是否开启了jpeg解码优化
* @return 返回true表示开启了优化解码,返回false表示没有开启
*/
getJpegOptimizeDecoding(): boolean {
return this.enableJpegOptimizeDecoding;
}
}