diff --git a/README.md b/README.md index 1e232d7..bf787e0 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,58 @@ # ImageKnife -**专门为OpenHarmony打造的一款图像加载缓存库,致力于更高效、更轻便、更简单。** +ImageKnife is a specially crafted image loading and caching library for OpenHarmony, optimized for efficiency, lightness, and simplicity. -## 简介 +## Introduction -本项目参考开源库 [Glide](https://github.com/bumptech/glide) 进行OpenHarmony的自研版本: +This project is a self-developed version for OpenHarmony, inspired by the open-source [Glide](https://github.com/bumptech/glide) library. It sports the following features: -- 支持自定义内存缓存策略,支持设置内存缓存的大小(默认LRU策略)。 -- 支持磁盘二级缓存,对于下载图片会保存一份至磁盘当中。 -- 支持自定义实现图片获取/网络下载 -- 支持监听网络下载回调进度 -- 继承Image的能力,支持option传入border,设置边框,圆角 -- 继承Image的能力,支持option传入objectFit设置图片缩放,包括objectFit为auto时根据图片自适应高度 -- 支持通过设置transform缩放图片 -- 并发请求数量,支持请求排队队列的优先级 -- 支持生命周期已销毁的图片,不再发起请求 -- 自定义缓存key -- 自定义http网络请求头 -- 支持writeCacheStrategy控制缓存的存入策略(只存入内存或文件缓存) -- 支持preLoadCache预加载图片 -- 支持onlyRetrieveFromCache仅用缓存加载 -- 支持使用一个或多个图片变换,如模糊,高亮等 +- Customizable memory cache strategy with adjustable cache size (default LRU) +- Disk L2 cache for downloaded images +- Custom implementation for image acquisition and network downloading +- Listening for progress of network downloads through callbacks +- Image options for borders and rounded corners +- Image scaling with **objectFit**, including auto-adapting height +- Image scaling through transformation +- Concurrent request management with priority queuing +- No requests made for images whose lifecycle has been destroyed +- Custom cache keys +- Custom HTTP request headers +- **writeCacheStrategy** for controlling cache storage (memory or file) +- Preloading images with **preLoadCache** +- Loading images exclusively from cache with **onlyRetrieveFromCache** +- Support for image transformations such as blurring and highlighting -待实现特性 +Planned features: -- 内存降采样优化,节约内存的占用 -- 支持自定义图片解码 +- Memory downsampling optimization to save memory usage +- Support for custom image decoding -注意:3.x版本相对2.x版本做了重大的重构,主要体现在: +Note: The 3.x version has been significantly restructured from the 2.x version, mainly in the following aspects: -- 使用Image组件代替Canvas组件渲染 -- 重构Dispatch分发逻辑,支持控制并发请求数,支持请求排队队列的优先级 -- 支持通过initMemoryCache自定义策略内存缓存策略和大小 -- 支持option自定义实现图片获取/网络下载 +- Use of the **Image** component instead of the **Canvas** component for rendering +- Refactored dispatch logic to control the number of concurrent requests and support priority in request queuing +- Support for custom memory cache strategies and sizes through **initMemoryCache** +- Support for custom implementation of image acquisition/network downloading through options -因此API及能力上,目前有部分差异,主要体现在: +Therefore, there are some differences in APIs and capabilities, which mainly include the following: -- 不支持drawLifeCycle接口,通过canvas自会图片 -- mainScaleType,border等参数,新版本与系统Image保持一致 -- gif/webp动图播放与控制(ImageAnimator实现) -- 抗锯齿相关参数 +- The **drawLifeCycle** API is not supported; images are drawn manually through the canvas. +- In the new version, parameters such as **mainScaleType** and **border** are consistent with the system **Image** component. +- GIF/WebP animation playback and control (implemented by **ImageAnimator**). +- Anti-aliasing related parameters. -## 下载安装 +## How to Install ``` ohpm install @ohos/imageknife -// 如果需要用文件缓存,需要提前初始化文件缓存 +// If file caching is required, initialize the file cache in advance. await ImageKnife.getInstance().initFileCache(context, 256, 256 * 1024 * 1024) ``` -## 使用说明 +## How to Use -#### 1.显示本地资源图片 +#### 1. Displaying a Local Resource Image ``` ImageKnifeComponent({ @@ -65,7 +65,7 @@ ImageKnifeComponent({ }).width(100).height(100) ``` -#### 2.显示本地context files下文件 +#### 2. Displaying a File from Local Context Files ``` ImageKnifeComponent({ @@ -78,7 +78,7 @@ ImageKnifeComponent({ }).width(100).height(100) ``` -#### 3.显示网络图片 +#### 3. Displaying a Network Image ``` ImageKnifeComponent({ @@ -91,7 +91,7 @@ ImageKnifeComponent({ }).width(100).height(100) ``` -#### 4.自定义下载图片 +#### 4. Downloading an Image with Custom Options ``` ImageKnifeComponent({ @@ -104,16 +104,16 @@ ImageKnifeComponent({ }) }).width(100).height(100) -// 自定义实现图片获取方法,如自定义网络下载 +// Custom implementation of the image acquisition method, such as custom network download。 @Concurrent async function custom(context: Context, src: string | PixelMap | Resource): Promise { - console.info("ImageKnife:: custom download:" + src) - // 举例写死从本地文件读取,也可以自己请求网络图片 + console.info("ImageKnife:: custom download: " + src) + // Example of hardcoding to read from a local file. You can also request a network image. return context.resourceManager.getMediaContentSync($r("app.media.bb").id).buffer as ArrayBuffer } ``` -#### 5.监听网络下载进度 +#### 5. Listening for Network Download Progress ``` ImageKnifeComponent({ @@ -124,7 +124,7 @@ ImageKnifeComponent({ }).width(100).height(100) ``` -#### 6.支持option传入border,设置边框,圆角 +#### 6. Setting Border Options ``` ImageKnifeComponent({ ImageKnifeOption: new ImageKnifeOption( @@ -135,7 +135,7 @@ ImageKnifeComponent({ ImageKnifeOption: new ImageKnifeOption( }).width(100).height(100) ``` -#### 7.支持option图片变换 +#### 7. Setting Image Transformation Options ``` ImageKnifeComponent({ ImageKnifeOption: new ImageKnifeOption( @@ -146,7 +146,7 @@ ImageKnifeComponent({ ImageKnifeOption: new ImageKnifeOption( }) }).width(100).height(100) ``` -多种组合变换用法 +Multiple combined transformation usages: ``` let transformations: collections.Array = new collections.Array(); @@ -158,17 +158,17 @@ ImageKnifeComponent({ placeholderSrc: $r("app.media.loading"), errorholderSrc: $r("app.media.app_icon"), objectFit: ImageFit.Contain, - border: { radius: { topLeft: 50, bottomRight: 50 } }, // 圆角设置 - transformation: transformations.length > 0 ? new MultiTransTransformation(transformations) : undefined // 图形变换组 + border: { radius: { topLeft: 50, bottomRight: 50 } }, // Rounded corner settings + transformation: transformations.length > 0 ? new MultiTransTransformation(transformations) : undefined // Graphic transformation group }) }).width(300) .height(300) - .rotate({ angle: 90 }) // 旋转90度 - .contrast(12) // 对比度滤波器 + .rotate ({angle: 90}) // Rotate by 90 degrees. + .contrast(12) // Contrast filter ``` -其他变换相关属性,可叠加实现组合变换效果 +Other transformation-related properties can be stacked to achieve combined transformation effects. -圆形裁剪变换示例 +Example of circular cropping transformation: ``` ImageKnifeComponent({ ImageKnifeOption:new ImageKnifeOption( @@ -181,7 +181,7 @@ ImageKnifeComponent({ ImageKnifeOption:new ImageKnifeOption( .height(300) ``` -圆形裁剪带边框变换示例 +Example of Circular cropping with border transformation: ``` ImageKnifeComponent({ ImageKnifeOption:new ImageKnifeOption( @@ -194,7 +194,7 @@ ImageKnifeComponent({ ImageKnifeOption:new ImageKnifeOption( .height(300) ``` -对比度滤波变换示例 +Example of contrast filtering transformation: ``` ImageKnifeComponent({ @@ -206,7 +206,7 @@ ImageKnifeComponent({ .contrast(12) ``` -旋转变换示例 +Example of rotation transformation: ``` ImageKnifeComponent({ @@ -219,7 +219,7 @@ ImageKnifeComponent({ .backgroundColor(Color.Pink) ``` -#### 8.监听图片加载成功与失败 +#### 8. Listening for Image Loading Success and Failure ``` ImageKnifeComponent({ ImageKnifeOption: new ImageKnifeOption( @@ -244,8 +244,8 @@ ImageKnifeComponent({ ImageKnifeOption: new ImageKnifeOption( }) }).width(100).height(100) ``` -#### 9.ImageKnifeComponent - syncLoad -设置是否同步加载图片,默认是异步加载。建议加载尺寸较小的本地图片时将syncLoad设为true,因为耗时较短,在主线程上执行即可 +#### 9. Use of syncLoad +**syncLoad** sets whether to load the image synchronously. By default, the image is loaded asynchronously. When loading a small image, you are advised to set **syncLoad** to **true** so that the image loading can be quickly completed on the main thread. ``` ImageKnifeComponent({ imageKnifeOption:new ImageKnifeOption({ @@ -254,7 +254,7 @@ ImageKnifeComponent({ }),syncLoad:true }) ``` -#### 10.ImageKnifeAnimatorComponent 示例 +#### 10. Use of ImageKnifeAnimatorComponent ``` ImageKnifeAnimatorComponent({ imageKnifeOption:new ImageKnifeOption({ @@ -264,112 +264,112 @@ ImageKnifeAnimatorComponent({ }),animatorOption:this.animatorOption }).width(300).height(300).backgroundColor(Color.Orange).margin({top:30}) ``` -#### 复用场景 -在aboutToRecycle生命周期清空组件内容;通过watch监听触发图片的加载。 -## 接口说明 -### ImageKnife组件 -| 组件名称 | 入参内容 | 功能简介 | -|-----------------------------|---------------------------------|--------| -| ImageKnifeComponent | ImageKnifeOption | 图片显示组件 | -| ImageKnifeAnimatorComponent | ImageKnifeOption、AnimatorOption | 动图控制组件 | +#### Reuse Scenario +Clear the component content in the **aboutToRecycle** lifecycle and trigger image loading through watch observeration. +## Available APIs +### ImageKnife +| Component | Parameter | Description | +| --------------------------- | -------------------------------- | ------------ | +| ImageKnifeComponent | ImageKnifeOption | Image display component.| +| ImageKnifeAnimatorComponent | ImageKnifeOption, AnimatorOption| Animated image control component.| -### AnimatorOption参数列表 -| 参数名称 | 入参内容 | 功能简介 | -|------------|-----------------|----------| -| state | AnimationStatus | 播放状态(可选) | -| iterations | number | 播放次数(可选) | -| reverse | boolean | 播放顺序(可选) | -| onStart | ()=>void | 动画开始播放时触发(可选) | -| onFinish | ()=>void | 动画播放完成时或者停止播放时触发(可选) | -| onPause | ()=>void | 动画暂停播放时触发(可选) | -| onCancel | ()=>void | 动画返回最初状态时触发(可选) | -| onRepeat | ()=>void | 动画重复播放时触发(可选) | +### AnimatorOption +| Parameter | Type | Description | +| ---------- | --------------- | ---------------------------------------- | +| state | AnimationStatus | Playback status. Optional. | +| iterations | number | Number of playback times. Optional. | +| reverse | boolean | Playback order. Optional. | +| onStart | ()=>void | Triggered when the animation starts. Optional. | +| onFinish | ()=>void | Triggered when the animation finishes or stops. Optional.| +| onPause | ()=>void | Triggered when the animation pauses. Optional. | +| onCancel | ()=>void | Triggered when the animation is canceled, that is, when it is reset to its initial state. Optional. | +| onRepeat | ()=>void | Triggered when the animation repeats. Optional. | -### ImageKnifeOption参数列表 +### ImageKnifeOption -| 参数名称 | 入参内容 | 功能简介 | -|-----------------------|-------------------------------------------------------|-----------------| -| loadSrc | string、PixelMap、Resource | 主图展示 | -| placeholderSrc | PixelMap、Resource | 占位图图展示(可选) | -| errorholderSrc | PixelMap、Resource | 错误图展示(可选) | -| objectFit | ImageFit | 主图填充效果(可选) | -| placeholderObjectFit | ImageFit | 占位图填充效果(可选) | -| errorholderObjectFit | ImageFit | 错误图填充效果(可选) | -| writeCacheStrategy | CacheStrategyType | 写入缓存策略(可选) | -| onlyRetrieveFromCache | boolean | 是否跳过网络和本地请求(可选) | -| customGetImage | (context: Context, src: string | 自定义下载图片(可选) | | Resource | 错误占位图数据源 | -| border | BorderOptions | 边框圆角(可选) | -| priority | taskpool.Priority | 加载优先级(可选) | -| context | common.UIAbilityContext | 上下文(可选) | -| progressListener | (progress: number)=>void | 进度(可选) | -| signature | String | 自定义缓存关键字(可选) | -| headerOption | Array | 设置请求头(可选) | -| transformation | PixelMapTransformation | 图片变换(可选) | -| drawingColorFilter | ColorFilter | drawing.ColorFilter | 图片变换(可选) | -| onComplete | (event:EventImage | undefined) => voi | 颜色滤镜效果(可选) | -| onLoadListener | onLoadStart: () => void、onLoadSuccess: (data: string | PixelMap | undefined) => void、onLoadFailed: (err: string) => void| 监听图片加载成功与失败 | +| Parameter | Type | Description | +| --------------------- | ----------------------------------------------------- | ------------------------------ | +| loadSrc | string, PixelMap, Resource | Main image. | +| placeholderSrc | PixelMap, Resource | Placeholder image. Optional. | +| errorholderSrc | PixelMap, Resource | Error image. Optional. | +| objectFit | ImageFit | How the main image is resized to fit its container. Optional. | +| placeholderObjectFit | ImageFit | How the placeholder image is resized to fit its container. Optional. | +| errorholderObjectFit | ImageFit | How the error image is resized to fit its container. Optional. | +| writeCacheStrategy | CacheStrategyType | Cache writing strategy. Optional. | +| onlyRetrieveFromCache | boolean | Whether to skip network and local requests. Optional.| +| customGetImage | (context: Context, src: string | Custom image download. Optional. | +| border | BorderOptions | Border corner. Optional. | +| priority | taskpool.Priority | Load priority. Optional. | +| context | common.UIAbilityContext | Context. Optional. | +| progressListener | (progress: number)=>void | Progress. Optional. | +| signature | String | Custom cache signature. Optional. | +| headerOption | Array\ | Request headers. Optional. | +| transformation | PixelMapTransformation | Image transformation. Optional. | +| drawingColorFilter | ColorFilter | Drawing color filter. Optional. | +| onComplete | (event:EventImage \| undefined)=>void | Callback for image loading completion. Optional. | +| onLoadListener | onLoadStart:()=>void,onLoadSuccess:(data:string\|Pixelmap)=>void | Callback for image loading events. Optional. | -### ImageKnife接口 +### ImageKnife -| 参数名称 | 入参内容 | 功能简介 | -|------------------|-------------------------------------------------------------------------------------------------------|---------------| -| initMemoryCache | newMemoryCache: IMemoryCache | 自定义内存缓存策略 | -| initFileCache | context: Context, size: number, memory: number | 初始化文件缓存数量和大小 | -| preLoadCache | loadSrc: string I ImageKnifeOption | 预加载并返回文件缓存路径 | -| getCacheImage | loadSrc: string, cacheType: CacheStrategy = CacheStrategy.Default, signature?: string) | 从内存或文件缓存中获取资源 | -| addHeader | key: string, value: Object | 全局添加http请求头 | -| setHeaderOptions | Array | 全局设置http请求头 | -| deleteHeader | key: string | 全局删除http请求头 | -| setCustomGetImage | customGetImage?: (context: Context, src: string | PixelMap | Resource) => Promise | 全局设置自定义下载 | -| setEngineKeyImpl | IEngineKey | 全局配置缓存key生成策略 | -| putCacheImage | url: string, pixelMap: PixelMap, cacheType: CacheStrategy = CacheStrategy.Default, signature?: string | 写入内存磁盘缓存 | -| removeMemoryCache| url: string | ImageKnifeOption | 清理指定内存缓存 | -| removeFileCache | url: string | ImageKnifeOption | 清理指定磁盘缓存 | -### 图形变换类型(需要为GPUImage添加依赖项) +| Parameter | Type | Description | +| ----------------- | ------------------------------------------------------------ | -------------------------- | +| initMemoryCache | newMemoryCache: IMemoryCache | Initializes a custom memory cache strategy. | +| initFileCache | context: Context, size: number, memory: number | Initializes the file cache size and quantity | +| preLoadCache | loadSrc: string I ImageKnifeOption | Preloads and returns the file cache path. | +| getCacheImage | loadSrc: string, cacheType: CacheStrategy = CacheStrategy.Default, signature?: string) | Obtains resources from memory or file cache.| +| addHeader | key: string, value: Object | Adds a global HTTP request header. | +| setHeaderOptions | Array | Sets global HTTP request headers. | +| deleteHeader | key: string | Deletes a global HTTP request header. | +| setCustomGetImage | customGetImage?: (context: Context, src: string | PixelMap | +| setEngineKeyImpl | IEngineKey | Sets a global cache key generation strategy. | +| putCacheImage | url: string, pixelMap: PixelMap, cacheType: CacheStrategy = CacheStrategy.Default, signature?: string | Writes to the memory disk cache. | +| removeMemoryCache | url: string | Removes an entry from the memory cache. | +| removeFileCache | url: string | Removes an entry from the file cache. | +### Graphics tRansformation Types (GPUImage Dependency Required) -| 类型 | 相关描述 | -| ---------------------------------- | ----------------------------- | -| BlurTransformation | 模糊处理 | -| BrightnessTransformation | 亮度滤波器 | -| CropSquareTransformation | 正方形剪裁 | -| CropTransformation | 自定义矩形剪裁 | -| GrayScaleTransformation | 灰度级滤波器 | -| InvertTransformation | 反转滤波器 | -| KuwaharaTransformation | 桑原滤波器(使用GPUIImage) | -| MaskTransformation | 遮罩 | -| PixelationTransformation | 像素化滤波器(使用GPUIImage) | -| SepiaTransformation | 乌墨色滤波器(使用GPUIImage) | -| SketchTransformation | 素描滤波器(使用GPUIImage) | -| SwirlTransformation | 扭曲滤波器(使用GPUIImage) | -| ToonTransformation | 动画滤波器(使用GPUIImage) | -| VignetterTransformation | 装饰滤波器(使用GPUIImage) | +| Type | Description | +| ------------------------ | ----------------------------- | +| BlurTransformation | Blurs the image. | +| BrightnessTransformation | Applies a brightness filter. | +| CropSquareTransformation | Crops the image to a square. | +| CropTransformation | Crops the image to a custom rectangle. | +| GrayScaleTransformation | Applies a grayscale filter. | +| InvertTransformation | Applies an inversion filter. | +| KuwaharaTransformation | Applies a Kuwahara filter (requires **GPUImage**). | +| MaskTransformation | Applies a mask. | +| PixelationTransformation | Applies a pixelation filter (requires **GPUImage**).| +| SepiaTransformation | Applies a sepia filter (requires **GPUImage**).| +| SketchTransformation | Applies a sketch filter (requires **GPUIImage**). | +| SwirlTransformation | Applies a swirl filter (requires **GPUImage**). | +| ToonTransformation | Applies a cartoon filter (requires **GPUImage**). | +| VignetterTransformation | Applies a vignette filter (requires **GPUImage**). | -## 下载安装GPUImage依赖 -方法一:在Terminal窗口中,执行如下命令安装三方包,DevEco Studio会自动在工程的oh-package.json5中自动添加三方包依赖。 +## Downloading and Installing the GPUImage Dependency +Method 1: In the **Terminal** window, run the following command to install the third-party HAR. DevEco Studio will automatically add the HAR as a dependency to the **oh-package.json5** file of the project. ``` ohpm install @ohos/gpu_transform ``` -方法二: 在工程的oh-package.json5中设置三方包依赖,配置示例如下: +Method 2: Set the third-party HAR as a dependency in the **oh-package.json5** file of the project. The following is a configuration example: ``` "dependencies": { "@ohos/gpu_transform": "^1.0.2" } ``` -## 约束与限制 +## Constraints -在下述版本验证通过: -DevEco Studio 5.0 Canary3(5.0.3.502)--SDK:API12 (5.0.0.31) +This project has been verified in the following version: -## 贡献代码 +DevEco Studio: 5.0 Canary3 (5.0.3.502), SDK: API 12 (5.0.0.31) -使用过程中发现任何问题都可以提 [issue](https://gitee.com/openharmony-tpc/ImageKnife/issues) -给我们,当然,我们也非常欢迎你给我们发 [PR](https://gitee.com/openharmony-tpc/ImageKnife/issues) 。 +## How to Contribute -## 开源协议 +If you find any problem during the use, submit an [Issue](https://gitee.com/openharmony-tpc/ImageKnife/issues) or a [PR](https://gitee.com/openharmony-tpc/ImageKnife/issues) to us. -本项目基于 [Apache License 2.0](https://gitee.com/openharmony-tpc/ImageKnife/blob/master/LICENSE) ,请自由的享受和参与开源。 +## License -## 遗留问题 +This project is licensed under [Apache License 2.0](https://gitee.com/openharmony-tpc/ImageKnife/blob/master/LICENSE). -- ImageKnifeAnimator组件无法设置ImageFit属性 -- ImageKnifeAnimator组件设置border属性无法将图片变为圆角 \ No newline at end of file +## Known Issues + +- The **ImageFit** attribute cannot be set for the **ImageKnifeAnimator** component. +- The **border** attribute of the **ImageKnifeAnimator** component cannot make the image rounded corners. diff --git a/README_zh.md b/README_zh.md new file mode 100644 index 0000000..1e232d7 --- /dev/null +++ b/README_zh.md @@ -0,0 +1,375 @@ +# ImageKnife + +**专门为OpenHarmony打造的一款图像加载缓存库,致力于更高效、更轻便、更简单。** + +## 简介 + +本项目参考开源库 [Glide](https://github.com/bumptech/glide) 进行OpenHarmony的自研版本: + +- 支持自定义内存缓存策略,支持设置内存缓存的大小(默认LRU策略)。 +- 支持磁盘二级缓存,对于下载图片会保存一份至磁盘当中。 +- 支持自定义实现图片获取/网络下载 +- 支持监听网络下载回调进度 +- 继承Image的能力,支持option传入border,设置边框,圆角 +- 继承Image的能力,支持option传入objectFit设置图片缩放,包括objectFit为auto时根据图片自适应高度 +- 支持通过设置transform缩放图片 +- 并发请求数量,支持请求排队队列的优先级 +- 支持生命周期已销毁的图片,不再发起请求 +- 自定义缓存key +- 自定义http网络请求头 +- 支持writeCacheStrategy控制缓存的存入策略(只存入内存或文件缓存) +- 支持preLoadCache预加载图片 +- 支持onlyRetrieveFromCache仅用缓存加载 +- 支持使用一个或多个图片变换,如模糊,高亮等 + +待实现特性 + +- 内存降采样优化,节约内存的占用 +- 支持自定义图片解码 + +注意:3.x版本相对2.x版本做了重大的重构,主要体现在: + +- 使用Image组件代替Canvas组件渲染 +- 重构Dispatch分发逻辑,支持控制并发请求数,支持请求排队队列的优先级 +- 支持通过initMemoryCache自定义策略内存缓存策略和大小 +- 支持option自定义实现图片获取/网络下载 + +因此API及能力上,目前有部分差异,主要体现在: + +- 不支持drawLifeCycle接口,通过canvas自会图片 +- mainScaleType,border等参数,新版本与系统Image保持一致 +- gif/webp动图播放与控制(ImageAnimator实现) +- 抗锯齿相关参数 + +## 下载安装 + +``` +ohpm install @ohos/imageknife + +// 如果需要用文件缓存,需要提前初始化文件缓存 +await ImageKnife.getInstance().initFileCache(context, 256, 256 * 1024 * 1024) +``` + +## 使用说明 + +#### 1.显示本地资源图片 + +``` +ImageKnifeComponent({ + ImageKnifeOption: new ImageKnifeOption({ + loadSrc: $r("app.media.app_icon"), + placeholderSrc: $r("app.media.loading"), + errorholderSrc: $r("app.media.app_icon"), + objectFit: ImageFit.Auto + }) +}).width(100).height(100) +``` + +#### 2.显示本地context files下文件 + +``` +ImageKnifeComponent({ + ImageKnifeOption: new ImageKnifeOption({ + loadSrc: this.localFile, + placeholderSrc: $r("app.media.loading"), + errorholderSrc: $r("app.media.app_icon"), + objectFit: ImageFit.Auto + }) +}).width(100).height(100) +``` + +#### 3.显示网络图片 + +``` +ImageKnifeComponent({ + ImageKnifeOption: new ImageKnifeOption({ + loadSrc:"https://www.openharmony.cn/_nuxt/img/logo.dcf95b3.png", + placeholderSrc: $r("app.media.loading"), + errorholderSrc: $r("app.media.app_icon"), + objectFit: ImageFit.Auto + }) +}).width(100).height(100) +``` + +#### 4.自定义下载图片 + +``` +ImageKnifeComponent({ + ImageKnifeOption: new ImageKnifeOption({ + loadSrc: "https://file.atomgit.com/uploads/user/1704857786989_8994.jpeg", + placeholderSrc: $r("app.media.loading"), + errorholderSrc: $r("app.media.app_icon"), + objectFit: ImageFit.Auto, + customGetImage: custom + }) +}).width(100).height(100) + +// 自定义实现图片获取方法,如自定义网络下载 +@Concurrent +async function custom(context: Context, src: string | PixelMap | Resource): Promise { + console.info("ImageKnife:: custom download:" + src) + // 举例写死从本地文件读取,也可以自己请求网络图片 + return context.resourceManager.getMediaContentSync($r("app.media.bb").id).buffer as ArrayBuffer +} +``` + +#### 5.监听网络下载进度 + +``` +ImageKnifeComponent({ + ImageKnifeOption: new ImageKnifeOption({ + loadSrc:"https://www.openharmony.cn/_nuxt/img/logo.dcf95b3.png", + progressListener:(progress:number)=>{console.info("ImageKinfe:: call back progress = " + progress)} + }) +}).width(100).height(100) +``` + +#### 6.支持option传入border,设置边框,圆角 + +``` +ImageKnifeComponent({ ImageKnifeOption: new ImageKnifeOption( +{ + loadSrc: $r("app.media.rabbit"), + border: {radius:50} + }) +}).width(100).height(100) +``` + +#### 7.支持option图片变换 + +``` +ImageKnifeComponent({ ImageKnifeOption: new ImageKnifeOption( +{ + loadSrc: $r("app.media.rabbit"), + border: {radius:50}, + transformation: new BlurTransformation(3) + }) +}).width(100).height(100) +``` +多种组合变换用法 + +``` +let transformations: collections.Array = new collections.Array(); +transformations.push(new BlurTransformation(5)); +transformations.push(new BrightnessTransformation(0.2)); +ImageKnifeComponent({ + imageKnifeOption: new ImageKnifeOption({ + loadSrc: $r('app.media.pngSample'), + placeholderSrc: $r("app.media.loading"), + errorholderSrc: $r("app.media.app_icon"), + objectFit: ImageFit.Contain, + border: { radius: { topLeft: 50, bottomRight: 50 } }, // 圆角设置 + transformation: transformations.length > 0 ? new MultiTransTransformation(transformations) : undefined // 图形变换组 +}) +}).width(300) + .height(300) + .rotate({ angle: 90 }) // 旋转90度 + .contrast(12) // 对比度滤波器 +``` +其他变换相关属性,可叠加实现组合变换效果 + +圆形裁剪变换示例 + +``` +ImageKnifeComponent({ ImageKnifeOption:new ImageKnifeOption( + { + loadSrc: $r('app.media.pngSample'), + objectFit: ImageFit.Cover, + border: { radius: 150 } +}) +}).width(300) + .height(300) +``` + +圆形裁剪带边框变换示例 + +``` +ImageKnifeComponent({ ImageKnifeOption:new ImageKnifeOption( + { + loadSrc: $r('app.media.pngSample'), + objectFit: ImageFit.Cover, + border: { radius: 150, color: Color.Red, width: 5 } +}) +}).width(300) + .height(300) +``` + +对比度滤波变换示例 + +``` +ImageKnifeComponent({ + imageKnifeOption: new ImageKnifeOption({ + loadSrc: $r('app.media.pngSample') + }) +}).width(300) + .height(300) + .contrast(12) +``` + +旋转变换示例 + +``` +ImageKnifeComponent({ + imageKnifeOption: new ImageKnifeOption({ + loadSrc: $r('app.media.pngSample') + }) +}).width(300) + .height(300) + .rotate({angle:90}) + .backgroundColor(Color.Pink) +``` + +#### 8.监听图片加载成功与失败 + +``` +ImageKnifeComponent({ ImageKnifeOption: new ImageKnifeOption( +{ + loadSrc: $r("app.media.rabbit"), + onLoadListener:{ + onLoadStart:()=>{ + this.starTime = new Date().getTime() + console.info("Load start: "); + }, + onLoadFailed: (err) => { + console.error("Load Failed Reason: " + err + " cost " + (new Date().getTime() - this.starTime) + " milliseconds"); + }, + onLoadSuccess: (data, imageData) => { + console.info("Load Successful: cost " + (new Date().getTime() - this.starTime) + " milliseconds"); + return data; + }, + onLoadCancel(err){ + console.info(err) + } + } + }) +}).width(100).height(100) +``` +#### 9.ImageKnifeComponent - syncLoad +设置是否同步加载图片,默认是异步加载。建议加载尺寸较小的本地图片时将syncLoad设为true,因为耗时较短,在主线程上执行即可 +``` +ImageKnifeComponent({ + imageKnifeOption:new ImageKnifeOption({ + loadSrc:$r("app.media.pngSample"), + placeholderSrc:$r("app.media.loading") + }),syncLoad:true + }) +``` +#### 10.ImageKnifeAnimatorComponent 示例 +``` +ImageKnifeAnimatorComponent({ + imageKnifeOption:new ImageKnifeOption({ + loadSrc:"https://gd-hbimg.huaban.com/e0a25a7cab0d7c2431978726971d61720732728a315ae-57EskW_fw658", + placeholderSrc:$r('app.media.loading'), + errorholderSrc:$r('app.media.failed') + }),animatorOption:this.animatorOption + }).width(300).height(300).backgroundColor(Color.Orange).margin({top:30}) +``` +#### 复用场景 +在aboutToRecycle生命周期清空组件内容;通过watch监听触发图片的加载。 +## 接口说明 +### ImageKnife组件 +| 组件名称 | 入参内容 | 功能简介 | +|-----------------------------|---------------------------------|--------| +| ImageKnifeComponent | ImageKnifeOption | 图片显示组件 | +| ImageKnifeAnimatorComponent | ImageKnifeOption、AnimatorOption | 动图控制组件 | + +### AnimatorOption参数列表 +| 参数名称 | 入参内容 | 功能简介 | +|------------|-----------------|----------| +| state | AnimationStatus | 播放状态(可选) | +| iterations | number | 播放次数(可选) | +| reverse | boolean | 播放顺序(可选) | +| onStart | ()=>void | 动画开始播放时触发(可选) | +| onFinish | ()=>void | 动画播放完成时或者停止播放时触发(可选) | +| onPause | ()=>void | 动画暂停播放时触发(可选) | +| onCancel | ()=>void | 动画返回最初状态时触发(可选) | +| onRepeat | ()=>void | 动画重复播放时触发(可选) | + +### ImageKnifeOption参数列表 + +| 参数名称 | 入参内容 | 功能简介 | +|-----------------------|-------------------------------------------------------|-----------------| +| loadSrc | string、PixelMap、Resource | 主图展示 | +| placeholderSrc | PixelMap、Resource | 占位图图展示(可选) | +| errorholderSrc | PixelMap、Resource | 错误图展示(可选) | +| objectFit | ImageFit | 主图填充效果(可选) | +| placeholderObjectFit | ImageFit | 占位图填充效果(可选) | +| errorholderObjectFit | ImageFit | 错误图填充效果(可选) | +| writeCacheStrategy | CacheStrategyType | 写入缓存策略(可选) | +| onlyRetrieveFromCache | boolean | 是否跳过网络和本地请求(可选) | +| customGetImage | (context: Context, src: string | 自定义下载图片(可选) | | Resource | 错误占位图数据源 | +| border | BorderOptions | 边框圆角(可选) | +| priority | taskpool.Priority | 加载优先级(可选) | +| context | common.UIAbilityContext | 上下文(可选) | +| progressListener | (progress: number)=>void | 进度(可选) | +| signature | String | 自定义缓存关键字(可选) | +| headerOption | Array | 设置请求头(可选) | +| transformation | PixelMapTransformation | 图片变换(可选) | +| drawingColorFilter | ColorFilter | drawing.ColorFilter | 图片变换(可选) | +| onComplete | (event:EventImage | undefined) => voi | 颜色滤镜效果(可选) | +| onLoadListener | onLoadStart: () => void、onLoadSuccess: (data: string | PixelMap | undefined) => void、onLoadFailed: (err: string) => void| 监听图片加载成功与失败 | + +### ImageKnife接口 + +| 参数名称 | 入参内容 | 功能简介 | +|------------------|-------------------------------------------------------------------------------------------------------|---------------| +| initMemoryCache | newMemoryCache: IMemoryCache | 自定义内存缓存策略 | +| initFileCache | context: Context, size: number, memory: number | 初始化文件缓存数量和大小 | +| preLoadCache | loadSrc: string I ImageKnifeOption | 预加载并返回文件缓存路径 | +| getCacheImage | loadSrc: string, cacheType: CacheStrategy = CacheStrategy.Default, signature?: string) | 从内存或文件缓存中获取资源 | +| addHeader | key: string, value: Object | 全局添加http请求头 | +| setHeaderOptions | Array | 全局设置http请求头 | +| deleteHeader | key: string | 全局删除http请求头 | +| setCustomGetImage | customGetImage?: (context: Context, src: string | PixelMap | Resource) => Promise | 全局设置自定义下载 | +| setEngineKeyImpl | IEngineKey | 全局配置缓存key生成策略 | +| putCacheImage | url: string, pixelMap: PixelMap, cacheType: CacheStrategy = CacheStrategy.Default, signature?: string | 写入内存磁盘缓存 | +| removeMemoryCache| url: string | ImageKnifeOption | 清理指定内存缓存 | +| removeFileCache | url: string | ImageKnifeOption | 清理指定磁盘缓存 | +### 图形变换类型(需要为GPUImage添加依赖项) + +| 类型 | 相关描述 | +| ---------------------------------- | ----------------------------- | +| BlurTransformation | 模糊处理 | +| BrightnessTransformation | 亮度滤波器 | +| CropSquareTransformation | 正方形剪裁 | +| CropTransformation | 自定义矩形剪裁 | +| GrayScaleTransformation | 灰度级滤波器 | +| InvertTransformation | 反转滤波器 | +| KuwaharaTransformation | 桑原滤波器(使用GPUIImage) | +| MaskTransformation | 遮罩 | +| PixelationTransformation | 像素化滤波器(使用GPUIImage) | +| SepiaTransformation | 乌墨色滤波器(使用GPUIImage) | +| SketchTransformation | 素描滤波器(使用GPUIImage) | +| SwirlTransformation | 扭曲滤波器(使用GPUIImage) | +| ToonTransformation | 动画滤波器(使用GPUIImage) | +| VignetterTransformation | 装饰滤波器(使用GPUIImage) | + +## 下载安装GPUImage依赖 +方法一:在Terminal窗口中,执行如下命令安装三方包,DevEco Studio会自动在工程的oh-package.json5中自动添加三方包依赖。 +``` + ohpm install @ohos/gpu_transform +``` +方法二: 在工程的oh-package.json5中设置三方包依赖,配置示例如下: +``` + "dependencies": { + "@ohos/gpu_transform": "^1.0.2" + } +``` +## 约束与限制 + +在下述版本验证通过: +DevEco Studio 5.0 Canary3(5.0.3.502)--SDK:API12 (5.0.0.31) + +## 贡献代码 + +使用过程中发现任何问题都可以提 [issue](https://gitee.com/openharmony-tpc/ImageKnife/issues) +给我们,当然,我们也非常欢迎你给我们发 [PR](https://gitee.com/openharmony-tpc/ImageKnife/issues) 。 + +## 开源协议 + +本项目基于 [Apache License 2.0](https://gitee.com/openharmony-tpc/ImageKnife/blob/master/LICENSE) ,请自由的享受和参与开源。 + +## 遗留问题 + +- ImageKnifeAnimator组件无法设置ImageFit属性 +- ImageKnifeAnimator组件设置border属性无法将图片变为圆角 \ No newline at end of file diff --git a/entry/src/main/ets/pages/ImageAnimatorPage.ets b/entry/src/main/ets/pages/ImageAnimatorPage.ets index b63c56e..5837f7f 100644 --- a/entry/src/main/ets/pages/ImageAnimatorPage.ets +++ b/entry/src/main/ets/pages/ImageAnimatorPage.ets @@ -46,22 +46,22 @@ struct ImageAnimatorPage { build() { Column(){ Flex(){ - Button("播放").onClick(()=>{ + Button($r('app.string.Play')).onClick(()=>{ this.animatorOption.state = AnimationStatus.Running }) - Button("暂停").onClick(()=>{ + Button($r('app.string.Pause')).onClick(()=>{ this.animatorOption.state = AnimationStatus.Paused }) - Button("停止").onClick(()=>{ + Button($r('app.string.Stop')).onClick(()=>{ this.animatorOption.state = AnimationStatus.Stopped }) - Button("无限循环").onClick(()=>{ + Button($r('app.string.Infinite_loop')).onClick(()=>{ this.animatorOption.iterations = -1 }) - Button("播放一次").onClick(()=>{ + Button($r('app.string.Play_once')).onClick(()=>{ this.animatorOption.iterations = 1 }) - Button("播放两次").onClick(()=>{ + Button($r('app.string.Play_twice')).onClick(()=>{ this.animatorOption.iterations = 2 }) } @@ -72,7 +72,7 @@ struct ImageAnimatorPage { errorholderSrc:$r('app.media.failed') }),animatorOption:this.animatorOption }).width(200).height(200).backgroundColor(Color.Orange).margin({top:30}) - Text("动画显示第一帧").fontSize(20) + Text($r('app.string.Display_the_first_frame')).fontSize(20) ImageKnifeAnimatorComponent({ imageKnifeOption:new ImageKnifeOption({ loadSrc:"https://gd-hbimg.huaban.com/e0a25a7cab0d7c2431978726971d61720732728a315ae-57EskW_fw658", @@ -80,7 +80,7 @@ struct ImageAnimatorPage { errorholderSrc:$r('app.media.failed') }),animatorOption:this.animatorOption1 }).width(200).height(200).backgroundColor(Color.Orange).margin({top:30}) - Text("动画显示最后一帧").fontSize(20) + Text($r('app.string.Display_the_last_frame')).fontSize(20) ImageKnifeAnimatorComponent({ imageKnifeOption:new ImageKnifeOption({ loadSrc:"https://gd-hbimg.huaban.com/e0a25a7cab0d7c2431978726971d61720732728a315ae-57EskW_fw658", diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets index f6d2a5e..a5bf801 100644 --- a/entry/src/main/ets/pages/Index.ets +++ b/entry/src/main/ets/pages/Index.ets @@ -18,6 +18,10 @@ import router from '@system.router'; @ComponentV2 struct Index { + getResourceString(res:Resource){ + return getContext().resourceManager.getStringSync(res.id) + } + aboutToAppear(): void { @@ -26,100 +30,100 @@ struct Index { build() { Scroll(){ Column() { - Button("测试ImageAnimator组件").onClick(()=>{ + Button($r('app.string.Test_ImageAnimator')).onClick(()=>{ router.push({ uri: 'pages/ImageAnimatorPage', }); }) - Button("测试加载多张相同图片").margin({top:10}).onClick(()=>{ + Button($r('app.string.Test_multiple_images')).margin({top:10}).onClick(()=>{ router.push({ uri: 'pages/TestCommonImage', }); }) - Button("测试占位图Task报错").margin({top:10}).onClick(()=>{ + Button($r('app.string.Test_Task_error')).margin({top:10}).onClick(()=>{ router.push({ uri: 'pages/TestTaskResourcePage', }); }) - Button("测试HSP场景预加载").margin({top:10}).onClick(()=>{ + Button($r('app.string.Test_HSP')).margin({top:10}).onClick(()=>{ router.push({ uri: 'pages/TestHspPreLoadImage', }); }) - Button("单个图片使用").margin({top:10}).onClick(()=>{ + Button($r('app.string.Test_SingleImage')).margin({top:10}).onClick(()=>{ router.push({ uri: 'pages/SingleImage', }); }) - Button("全局自定义下载").margin({top:10}).onClick(()=>{ + Button($r('app.string.Test_custom_download')).margin({top:10}).onClick(()=>{ router.push({ uri: 'pages/TestSetCustomImagePage', }); }) - Button("多图 + LazyForEach").margin({top:10}).onClick(()=>{ + Button(this.getResourceString($r('app.string.Multiple_images')) + " + LazyForEach").margin({top:10}).onClick(()=>{ router.push({ uri: 'pages/ManyPhotoShowPage', }); }) - Button("多图 + reuse + LazyForeach").margin({top:10}).onClick(()=>{ + Button(this.getResourceString($r('app.string.Multiple_images')) + " + reuse + LazyForeach").margin({top:10}).onClick(()=>{ router.push({ uri: 'pages/UserPage', }); }) - Button("长图显示").margin({top:10}).onClick(()=>{ + Button($r('app.string.Display_long_image')).margin({top:10}).onClick(()=>{ router.push({ uri: 'pages/LongImagePage', }); }) - Button("缩放图片").margin({top:10}).onClick(()=>{ + Button($r('app.string.Image_scaling')).margin({top:10}).onClick(()=>{ router.push({ uri: 'pages/TransformPage', }); }) - Button("消息+List").margin({top:10}).onClick(()=>{ + Button(this.getResourceString($r('app.string.Message_list')) + " + List").margin({top:10}).onClick(()=>{ router.push({ uri: 'pages/TestImageFlash', }); }) - Button("自定义缓存key").margin({top:10}).onClick(()=>{ + Button($r('app.string.Custom_cache_key')).margin({top:10}).onClick(()=>{ router.push({ uri: 'pages/SignatureTestPage', }); }) - Button("预加载图片到文件缓存").margin({top:10}).onClick(()=>{ + Button($r('app.string.Preloading_images_to_cache')).margin({top:10}).onClick(()=>{ router.push({ uri: 'pages/TestPrefetchToFileCache', }); }) - Button("从缓存获取图片显示").margin({top:10}).onClick(()=>{ + Button($r('app.string.Retrieve_image_display_from_cache')).margin({top:10}).onClick(()=>{ router.push({ uri: 'pages/TestIsUrlExist', }); }) - Button("测试单个请求头").margin({top:10}).onClick(()=>{ + Button($r('app.string.Test_single_request_header')).margin({top:10}).onClick(()=>{ router.push({ uri: 'pages/TestHeader', }); }) - Button("测试写入缓存策略").margin({top:10}).onClick(()=>{ + Button($r('app.string.Test_write_cache_strategy')).margin({top:10}).onClick(()=>{ router.push({ uri: 'pages/TestWriteCacheStage', }); }) - Button("图片变换").margin({top:10}).onClick(()=>{ + Button($r('app.string.Image_Transformation')).margin({top:10}).onClick(()=>{ router.push({ uri: 'pages/ImageTransformation', @@ -127,7 +131,7 @@ struct Index { }) - Button("不同的ObjectFit").margin({top:10}).onClick(()=>{ + Button($r('app.string.Different_ObjectFit')).margin({top:10}).onClick(()=>{ router.push({ uri: 'pages/ObjectFitPage', @@ -135,24 +139,24 @@ struct Index { }) - Button('测试图片加载成功/失败事件').margin({top:10}).onClick(()=>{ + Button($r('app.string.Test_image_loading_success_or_failure_events')).margin({top:10}).onClick(()=>{ router.push({ uri: 'pages/LoadStatePage', }) }) - Button('测试移除图片缓存接口').margin({top:10}).onClick(()=>{ + Button($r('app.string.Test_removing_image_cache_interface')).margin({top:10}).onClick(()=>{ router.push({ uri: 'pages/TestRemoveCache', }); }) - Button("测试错误图显示").margin({top:10}).onClick(()=>{ + Button($r('app.string.Test_error_image_display')).margin({top:10}).onClick(()=>{ router.push({ uri: 'pages/TestErrorHolderPage', }); }) - Button("测试媒体url").margin({top:10}).onClick(()=>{ + Button($r('app.string.Test_media_URL')).margin({top:10}).onClick(()=>{ router.push({ uri: 'pages/dataShareUriLoadPage', diff --git a/entry/src/main/ets/pages/LoadStatePage.ets b/entry/src/main/ets/pages/LoadStatePage.ets index ca87944..6731271 100644 --- a/entry/src/main/ets/pages/LoadStatePage.ets +++ b/entry/src/main/ets/pages/LoadStatePage.ets @@ -48,7 +48,7 @@ struct LoadStatePage { Text('测试失败场景请先关闭网络,并保证本地没有此网络图片的缓存') .margin({ top: 20 }) Row() { - Button('测试失败/成功场景') + Button($r('app.string.Test_failure_success')) .onClick(() => { this.ImageKnifeOption = new ImageKnifeOption({ loadSrc: "https://www.openharmony.cn/_nuxt/img/logo.dcf95b3.png", @@ -82,7 +82,7 @@ struct LoadStatePage { Text(this.typeValue) ImageKnifeComponent({ imageKnifeOption: this.ImageKnifeOption }).height(this.currentHeight).width(this.currentWidth) .margin({ top: 20 }) - Button("自定义下载失败").onClick(()=>{ + Button($r('app.string.Custom_download_failed')).onClick(()=>{ this.imageKnifeOption1 = new ImageKnifeOption({ loadSrc: "abc", placeholderSrc:$r('app.media.loading'), diff --git a/entry/src/main/ets/pages/ObjectFitPage.ets b/entry/src/main/ets/pages/ObjectFitPage.ets index 7a1f2ab..58ca101 100644 --- a/entry/src/main/ets/pages/ObjectFitPage.ets +++ b/entry/src/main/ets/pages/ObjectFitPage.ets @@ -27,7 +27,7 @@ struct ObjectFitPage { build() { Column() { - Button("主图Fill拉伸填充").onClick(()=>{ + Button($r('app.string.Main_image_Fill')).onClick(()=>{ this.imageKnifeOption = new ImageKnifeOption({ loadSrc: $r("app.media.app_icon"), placeholderSrc: $r("app.media.loading"), @@ -36,7 +36,7 @@ struct ObjectFitPage { }) }) - Button("占位图Contain保持比例填充").margin({top:10}).onClick(async () => { + Button($r('app.string.Maintain_proportion_filling')).margin({top:10}).onClick(async () => { ImageKnife.getInstance().removeAllMemoryCache() await ImageKnife.getInstance().removeAllFileCache() @@ -50,7 +50,7 @@ struct ObjectFitPage { }) - Button("错误图None不变化").margin({top:10}).onClick(() => { + Button($r('app.string.Error_graph_None')).margin({top:10}).onClick(() => { this.imageKnifeOption = new ImageKnifeOption({ loadSrc: "http://xxxxx", placeholderSrc: $r("app.media.loading"), diff --git a/entry/src/main/ets/pages/SignatureTestPage.ets b/entry/src/main/ets/pages/SignatureTestPage.ets index d01ec45..8fc1763 100644 --- a/entry/src/main/ets/pages/SignatureTestPage.ets +++ b/entry/src/main/ets/pages/SignatureTestPage.ets @@ -33,9 +33,9 @@ struct SignatureTestPage { Scroll() { Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { - Text("key固定为 1").fontSize(15) + Text($r('app.string.The_key_fixed_1')).fontSize(15) Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { - Button("加载") + Button($r('app.string.Load')) .onClick(() => { this.imageKnifeOption1 = new ImageKnifeOption({ loadSrc: 'https://img-blog.csdn.net/20140514114029140', @@ -46,9 +46,9 @@ struct SignatureTestPage { ImageKnifeComponent({ imageKnifeOption: this.imageKnifeOption1 }).width(300).height(300) }.width('100%').backgroundColor(Color.Pink) - Text("key每次变化:时间戳").fontSize(15) + Text($r('app.string.The_key_changes_timestamp')).fontSize(15) Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { - Button("加载") + Button($r('app.string.Load')) .onClick(() => { this.imageKnifeOption2 = new ImageKnifeOption({ loadSrc: 'https://img-blog.csdn.net/20140514114029140', diff --git a/entry/src/main/ets/pages/SingleImage.ets b/entry/src/main/ets/pages/SingleImage.ets index b92d6eb..3671c6c 100644 --- a/entry/src/main/ets/pages/SingleImage.ets +++ b/entry/src/main/ets/pages/SingleImage.ets @@ -42,7 +42,7 @@ struct SingleImage { build() { Scroll(this.scroller) { Column() { - Text("本地资源svg图片") + Text($r('app.string.Local_SVG')) .fontSize(30) .fontWeight(FontWeight.Bold) ImageKnifeComponent({ @@ -56,7 +56,7 @@ struct SingleImage { .onClick(()=>{ this.DrawingColorFilter = drawing.ColorFilter.createBlendModeColorFilter(this.color, drawing.BlendMode.SRC_IN); }) - Text("本地context files下文件") + Text($r('app.string.Under_context_file')) .fontSize(30) .fontWeight(FontWeight.Bold) ImageKnifeComponent({ @@ -67,7 +67,7 @@ struct SingleImage { objectFit: ImageFit.Contain }) }).width(100).height(100) - Text("网络图片") + Text($r('app.string.Network_images')) .fontSize(30) .fontWeight(FontWeight.Bold) ImageKnifeComponent({ @@ -79,7 +79,7 @@ struct SingleImage { progressListener:(progress:number)=>{console.info("ImageKnife:: call back progress = " + progress)} }) }).width(100).height(100) - Text("自定义下载") + Text($r('app.string.Custom_network_download')) .fontSize(30) .fontWeight(FontWeight.Bold) ImageKnifeComponent({ @@ -92,7 +92,7 @@ struct SingleImage { transformation: new BlurTransformation(10) }) }).width(100).height(100) - Text("pixelMap加载图片") + Text($r('app.string.PixelMap_loads_images')) .fontSize(30) .fontWeight(FontWeight.Bold) ImageKnifeComponent({ diff --git a/entry/src/main/ets/pages/TestImageFlash.ets b/entry/src/main/ets/pages/TestImageFlash.ets index e5ed578..df2d363 100644 --- a/entry/src/main/ets/pages/TestImageFlash.ets +++ b/entry/src/main/ets/pages/TestImageFlash.ets @@ -111,12 +111,12 @@ struct ImageTestPage { }) } Row(){ - Text("点击尺寸加50") + Text($r('app.string.Click_on_add')) .onClick(()=> { this.imageSize = this.imageSize + 50 }) .width('50%').backgroundColor(0x88ff0000).textAlign(TextAlign.Center).height(50) - Text("点击尺寸减50") + Text($r('app.string.Click_on_reduce')) .onClick(()=> { this.imageSize = Math.max(this.imageSize - 50, 0) }) diff --git a/entry/src/main/ets/pages/TestIsUrlExist.ets b/entry/src/main/ets/pages/TestIsUrlExist.ets index 04e3714..da91f54 100644 --- a/entry/src/main/ets/pages/TestIsUrlExist.ets +++ b/entry/src/main/ets/pages/TestIsUrlExist.ets @@ -28,11 +28,11 @@ struct TestIsUrlExist { build() { Column() { Flex() { - Button("预加载gif图").onClick(() => { + Button($r('app.string.Preloading_GIF')).onClick(() => { this.imageKnifeOption.loadSrc = "https://gd-hbimg.huaban.com/e0a25a7cab0d7c2431978726971d61720732728a315ae-57EskW_fw658" }) - Button("内存缓存获取gif").onClick(() => { + Button($r('app.string.Retrieve_GIF_from_memory')).onClick(() => { ImageKnife.getInstance() .getCacheImage("https://gd-hbimg.huaban.com/e0a25a7cab0d7c2431978726971d61720732728a315ae-57EskW_fw658", CacheStrategy.Memory) @@ -40,7 +40,7 @@ struct TestIsUrlExist { this.source = data !== undefined ? data.source : $r("app.media.startIcon") }) }) - Button("文件缓存获取gif").onClick(() => { + Button($r('app.string.Retrieve_GIF_from_disk')).onClick(() => { ImageKnife.getInstance() .getCacheImage("https://gd-hbimg.huaban.com/e0a25a7cab0d7c2431978726971d61720732728a315ae-57EskW_fw658", CacheStrategy.File) @@ -51,11 +51,11 @@ struct TestIsUrlExist { } Flex() { - Button("预加载静态图").onClick(() => { + Button($r('app.string.Preloading_static_images')).onClick(() => { this.imageKnifeOption.loadSrc = 'https://hbimg.huabanimg.com/95a6d37a39aa0b70d48fa18dc7df8309e2e0e8e85571e-x4hhks_fw658/format/webp' }) - Button("内存缓存获取").onClick(() => { + Button($r('app.string.Retrieve_images_from_memory')).onClick(() => { ImageKnife.getInstance() .getCacheImage('https://hbimg.huabanimg.com/95a6d37a39aa0b70d48fa18dc7df8309e2e0e8e85571e-x4hhks_fw658/format/webp', CacheStrategy.Memory) @@ -63,7 +63,7 @@ struct TestIsUrlExist { this.source = data!.source }) }) - Button("文件缓存获取").onClick(() => { + Button($r('app.string.Retrieve_images_from_disk')).onClick(() => { ImageKnife.getInstance() .getCacheImage('https://hbimg.huabanimg.com/95a6d37a39aa0b70d48fa18dc7df8309e2e0e8e85571e-x4hhks_fw658/format/webp', CacheStrategy.File) diff --git a/entry/src/main/ets/pages/TestPrefetchToFileCache.ets b/entry/src/main/ets/pages/TestPrefetchToFileCache.ets index b59d1d5..c541bd1 100644 --- a/entry/src/main/ets/pages/TestPrefetchToFileCache.ets +++ b/entry/src/main/ets/pages/TestPrefetchToFileCache.ets @@ -32,13 +32,13 @@ struct TestPrefetchToFileCachePage { } build() { Column() { - Button("url预加载图片到文件缓存").margin({top:10}).onClick(async ()=>{ + Button($r('app.string.Preloading_images_to_file_cache_using_URL')).margin({top:10}).onClick(async ()=>{ await this.preload("https://gd-hbimg.huaban.com/e0a25a7cab0d7c2431978726971d61720732728a315ae-57EskW_fw658") }) - Button("option预加载图片到文件缓存").margin({top:10}).onClick(async ()=>{ + Button($r('app.string.Preloading_images_to_file_cache_using_option')).margin({top:10}).onClick(async ()=>{ await this.preload1("https://gd-hbimg.huaban.com/e0a25a7cab0d7c2431978726971d61720732728a315ae-57EskW_fw658") }) - Button("加载图片(预加载后可断网加载)").margin({top:10}).onClick(()=>{ + Button($r('app.string.Load_image_offline_after_preloading')).margin({top:10}).onClick(()=>{ this.imageKnifeOption.loadSrc = "https://gd-hbimg.huaban.com/e0a25a7cab0d7c2431978726971d61720732728a315ae-57EskW_fw658" }) ImageKnifeComponent({ diff --git a/entry/src/main/ets/pages/TestSetCustomImagePage.ets b/entry/src/main/ets/pages/TestSetCustomImagePage.ets index 2369de8..58a5c7b 100644 --- a/entry/src/main/ets/pages/TestSetCustomImagePage.ets +++ b/entry/src/main/ets/pages/TestSetCustomImagePage.ets @@ -17,6 +17,11 @@ import { ImageKnifeComponent, ImageKnife, ImageKnifeOption } from '@ohos/library @Entry @ComponentV2 struct TestSetCustomImagePage { + + getResourceString(res:Resource){ + return getContext().resourceManager.getStringSync(res.id) + } + @Local imageKnifeOption: ImageKnifeOption = new ImageKnifeOption({ loadSrc: $r('app.media.startIcon'), placeholderSrc: $r('app.media.loading') @@ -29,19 +34,19 @@ struct TestSetCustomImagePage { } build() { Column() { - Button("自定义下载a").onClick(()=>{ + Button(this.getResourceString($r('app.string.Custom_network_download')) + " a").onClick(()=>{ this.imageKnifeOption = new ImageKnifeOption({ loadSrc: "aaa", placeholderSrc: $r('app.media.loading') }) }) - Button("自定义下载b").onClick(()=>{ + Button(this.getResourceString($r('app.string.Custom_network_download')) + " b").onClick(()=>{ this.imageKnifeOption = new ImageKnifeOption({ loadSrc: "bbb", placeholderSrc: $r('app.media.loading') }) }) - Button("自定义下载c").onClick(()=>{ + Button(this.getResourceString($r('app.string.Custom_network_download')) + " c").onClick(()=>{ this.imageKnifeOption = new ImageKnifeOption({ loadSrc: "ccc", placeholderSrc: $r('app.media.loading') diff --git a/entry/src/main/ets/pages/TestWriteCacheStage.ets b/entry/src/main/ets/pages/TestWriteCacheStage.ets index e58d9da..a5f0431 100644 --- a/entry/src/main/ets/pages/TestWriteCacheStage.ets +++ b/entry/src/main/ets/pages/TestWriteCacheStage.ets @@ -35,7 +35,7 @@ struct TestWriteCacheStage { build() { Column() { - Button("写入内存文件缓存").margin({top:10}).onClick(async ()=>{ + Button($r('app.string.Write_memory_and_file')).margin({top:10}).onClick(async ()=>{ this.imageKnifeOption1 = new ImageKnifeOption({ loadSrc:'https://hbimg.huabanimg.com/95a6d37a39aa0b70d48fa18dc7df8309e2e0e8e85571e-x4hhks_fw658/format/webp', placeholderSrc:$r('app.media.loading'), @@ -46,7 +46,7 @@ struct TestWriteCacheStage { ImageKnifeComponent({ imageKnifeOption: this.imageKnifeOption1 }).width(200).height(200).margin({top:10}) - Button("写入内存缓存").margin({top:10}).onClick(async ()=>{ + Button($r('app.string.Write_memory')).margin({top:10}).onClick(async ()=>{ this.imageKnifeOption2 = new ImageKnifeOption({ loadSrc:"https://hbimg.huabanimg.com/cc6af25f8d782d3cf3122bef4e61571378271145735e9-vEVggB", placeholderSrc:$r('app.media.loading'), @@ -57,7 +57,7 @@ struct TestWriteCacheStage { ImageKnifeComponent({ imageKnifeOption: this.imageKnifeOption2 }).width(200).height(200).margin({top:10}) - Button("写入文件缓存").margin({top:10}).onClick(async ()=>{ + Button($r('app.string.Write_file')).margin({top:10}).onClick(async ()=>{ this.imageKnifeOption3 = new ImageKnifeOption({ loadSrc:'https://img-blog.csdn.net/20140514114029140', placeholderSrc:$r('app.media.loading'), diff --git a/entry/src/main/ets/pages/TransformPage.ets b/entry/src/main/ets/pages/TransformPage.ets index 55332d9..0288c1d 100644 --- a/entry/src/main/ets/pages/TransformPage.ets +++ b/entry/src/main/ets/pages/TransformPage.ets @@ -34,12 +34,12 @@ struct TransformPage { ImageKnifeComponent({ imageKnifeOption: this.ImageKnifeOption }).height(200).width(200) .transform(this.matrix1) // Image($r('app.media.rabbit')).objectFit(ImageFit.Contain).height(200).width(200).transform(this.matrix1) - Button("放大").onClick(()=>{ + Button($r('app.string.Enlarge')).onClick(()=>{ this.custom_scale = this.custom_scale * 2 this.matrix1 = matrix4.identity().scale({ x: this.custom_scale, y: this.custom_scale }) }) - Button("缩小").onClick(()=>{ + Button($r('app.string.Reduce')).onClick(()=>{ this.custom_scale = this.custom_scale / 2 this.matrix1 = matrix4.identity().scale({ x: this.custom_scale, y: this.custom_scale }) }) diff --git a/entry/src/main/ets/pages/dataShareUriLoadPage.ets b/entry/src/main/ets/pages/dataShareUriLoadPage.ets index 9e4aa06..7392044 100644 --- a/entry/src/main/ets/pages/dataShareUriLoadPage.ets +++ b/entry/src/main/ets/pages/dataShareUriLoadPage.ets @@ -32,9 +32,9 @@ struct DataShareUriLoadPage { build() { Scroll() { Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { - Text("获取媒体图库的uri用ImageKnife展示").fontSize(15) + Text($r('app.string.Retrieve_media_gallery')).fontSize(15) Flex({ direction: FlexDirection.Row, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { - Button("点击加载Uri并展示") + Button($r('app.string.Click_load_Uri')) .onClick(async () => { let photoSelectOptions = new photoAccessHelper.PhotoSelectOptions(); photoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE; diff --git a/entry/src/main/resources/base/element/string.json b/entry/src/main/resources/base/element/string.json index 13c1fec..a3e08d1 100644 --- a/entry/src/main/resources/base/element/string.json +++ b/entry/src/main/resources/base/element/string.json @@ -19,6 +19,246 @@ { "name": "app_permission_READ_IMAGEVIDEO", "value": "获取读媒体资源权限" + }, + { + "name": "Test_ImageAnimator", + "value": "Test ImageAnimator component" + }, + { + "name": "Test_multiple_images", + "value": "Test loading multiple identical images" + }, + { + "name": "Test_Task_error", + "value": "Test placeholder map Task error" + }, + { + "name": "Test_HSP", + "value": "Test HSP scene preloading" + }, + { + "name": "Test_SingleImage", + "value": "SingleImage" + }, + { + "name": "Test_custom_download", + "value": "Global custom download" + }, + { + "name": "Multiple_images", + "value": "Multiple images" + }, + { + "name": "Display_long_image", + "value": "Display long image" + }, + { + "name": "Image_scaling", + "value": "Image scaling" + }, + { + "name": "Message_list", + "value": "Message list" + }, + { + "name": "Custom_cache_key", + "value": "Custom cache key" + }, + { + "name": "Preloading_images_to_cache", + "value": "Preloading images to file cache" + }, + { + "name": "Retrieve_image_display_from_cache", + "value": "Retrieve image display from cache" + }, + { + "name": "Test_single_request_header", + "value": "Test a single request header" + }, + { + "name": "Test_write_cache_strategy", + "value": "Test write cache strategy" + }, + { + "name": "Image_Transformation", + "value": "Image Transformation" + }, + { + "name": "Different_ObjectFit", + "value": "Different ObjectFit" + }, + { + "name": "Test_image_loading_success_or_failure_events", + "value": "Test image loading success/failure events" + }, + { + "name": "Test_removing_image_cache_interface", + "value": "Test removing image cache interface" + }, + { + "name": "Test_error_image_display", + "value": "Test error image display" + }, + { + "name": "Test_media_URL", + "value": "Test media URL" + }, + { + "name": "Display_the_first_frame", + "value": "Display the first frame of the animation" + }, + { + "name": "Display_the_last_frame", + "value": "Display the last frame of the animation" + }, + { + "name": "Play", + "value": "Play" + }, + { + "name": "Pause", + "value": "Pause" + }, + { + "name": "Stop", + "value": "Stop" + }, + { + "name": "Infinite_loop", + "value": "Infinite loop" + }, + { + "name": "Play_once", + "value": "Play once" + }, + { + "name": "Play_twice", + "value": "Play twice" + }, + { + "name": "Local_SVG", + "value": "Local SVG image" + }, + { + "name": "Under_context_file", + "value": "Files under context file" + }, + { + "name": "Network_images", + "value": "Network images" + }, + { + "name": "Custom_network_download", + "value": "Custom network download" + }, + { + "name": "PixelMap_loads_images", + "value": "PixelMap loads images" + }, + { + "name": "Enlarge", + "value": "Enlarge" + }, + { + "name": "Reduce", + "value": "Reduce" + }, + { + "name": "Click_on_add", + "value": "Click on the size to add 50" + }, + { + "name": "Click_on_reduce", + "value": "Click to reduce size by 50" + }, + { + "name": "The_key_fixed_1", + "value": "The key is fixed at 1" + }, + { + "name": "The_key_changes_timestamp", + "value": "Key changes every time: timestamp" + }, + { + "name": "Load", + "value": "Load" + }, + { + "name": "Preloading_images_to_file_cache_using_URL", + "value": "Preloading images to file cache using URL" + }, + { + "name": "Preloading_images_to_file_cache_using_option", + "value": "Preloading images to file cache using option" + }, + { + "name": "Load_image_offline_after_preloading", + "value": "Load image (can be loaded offline after preloading)" + }, + { + "name": "Preloading_GIF", + "value": "Preloading GIF" + }, + { + "name": "Retrieve_GIF_from_memory", + "value": "Retrieve GIF from memory cache" + }, + { + "name": "Retrieve_GIF_from_disk", + "value": "Retrieve GIF from disk cache" + }, + { + "name": "Preloading_static_images", + "value": "Preloading static images" + }, + { + "name": "Retrieve_images_from_memory", + "value": "Retrieve images from memory cache" + }, + { + "name": "Retrieve_images_from_disk", + "value": "Retrieve images from memory disk" + }, + { + "name": "Write_memory_and_file", + "value": "Write to memory and file cache" + }, + { + "name": "Write_memory", + "value": "Write to memory cache" + }, + { + "name": "Write_file", + "value": "Write to file cache" + }, + { + "name": "Main_image_Fill", + "value": "Main image Fill Stretch Fill" + }, + { + "name": "Maintain_proportion_filling", + "value": "Maintain proportion filling in the placeholder map 'Include'" + }, + { + "name": "Error_graph_None", + "value": "Error graph None remains unchanged" + }, + { + "name": "Test_failure_success", + "value": "Test failure/success" + }, + { + "name": "Custom_download_failed", + "value": "Custom download failed" + }, + { + "name": "Retrieve_media_gallery", + "value": "Retrieve the URI of the media gallery and display it using ImageKnife" + }, + { + "name": "Click_load_Uri", + "value": "Click to load Uri and display" } ] } \ No newline at end of file diff --git a/entry/src/main/resources/zh_CN/element/string.json b/entry/src/main/resources/zh_CN/element/string.json index 6250115..0768f77 100644 --- a/entry/src/main/resources/zh_CN/element/string.json +++ b/entry/src/main/resources/zh_CN/element/string.json @@ -19,6 +19,242 @@ { "name": "app_permission_READ_IMAGEVIDEO", "value": "获取读媒体资源权限" + }, + { + "name": "Test_ImageAnimator", + "value": "测试ImageAnimator组件" + }, + { + "name": "Test_multiple_images", + "value": "测试加载多张相同图片" + }, + { + "name": "Test_Task_error", + "value": "测试占位图Task报错" + }, + { + "name": "Test_HSP", + "value": "测试HSP场景预加载" + }, + { + "name": "Test_SingleImage", + "value": "单个图片使用" + }, + { + "name": "Test_custom_download", + "value": "全局自定义下载" + }, + { + "name": "Multiple_images", + "value": "多图" + }, + { + "name": "Display_long_image", + "value": "长图显示" + }, + { + "name": "Image_scaling", + "value": "缩放图片" + }, + { + "name": "Message_list", + "value": "消息列表" + }, + { + "name": "Custom_cache_key", + "value": "自定义缓存key" + }, + { + "name": "Preloading_images_to_cache", + "value": "预加载图片到文件缓存" + }, + { + "name": "Retrieve_image_display_from_cache", + "value": "从缓存获取图片显示" + }, + { + "name": "Test_single_request_header", + "value": "测试单个请求头" + }, + { + "name": "Test_write_cache_strategy", + "value": "测试写入缓存策略" + }, + { + "name": "Image_Transformation", + "value": "图片变换" + }, + { + "name": "Different_ObjectFit", + "value": "不同的ObjectFit" + }, + { + "name": "Test_image_loading_success_or_failure_events", + "value": "测试图片加载成功/失败事件" + }, + { + "name": "Test_removing_image_cache_interface", + "value": "测试移除图片缓存接口" + }, + { + "name": "Test_error_image_display", + "value": "测试错误图显示" + }, + { + "name": "Display_the_first_frame", + "value": "动画显示第一帧" + }, + { + "name": "Display_the_last_frame", + "value": "动画显示最后一帧" + }, + { + "name": "Play", + "value": "播放" + }, + { + "name": "Pause", + "value": "暂停" + }, + { + "name": "Stop", + "value": "停止" + }, + { + "name": "Infinite_loop", + "value": "无限循环" + }, + { + "name": "Play_once", + "value": "播放一次" + }, + { + "name": "Play_twice", + "value": "播放两次" + }, + { + "name": "Local_SVG", + "value": "本地资源SVG图片" + }, + { + "name": "Under_context_file", + "value": "本地context files下文件" + }, + { + "name": "Network_images", + "value": "网络图片" + }, + { + "name": "Custom_network_download", + "value": "自定义下载" + }, + { + "name": "PixelMap_loads_images", + "value": "PixelMap加载图片" + }, + { + "name": "Enlarge", + "value": "放大" + }, + { + "name": "Reduce", + "value": "缩小" + }, + { + "name": "Click_on_add", + "value": "点击尺寸加50" + }, + { + "name": "Click_on_reduce", + "value": "点击尺寸减50" + }, + { + "name": "The_key_fixed_1", + "value": "key固定为 1" + }, + { + "name": "The_key_changes_timestamp", + "value": "key每次变化:时间戳" + }, + { + "name": "Load", + "value": "加载" + }, + { + "name": "Preloading_images_to_file_cache_using_URL", + "value": "url预加载图片到文件缓存" + }, + { + "name": "Preloading_images_to_file_cache_using_option", + "value": "option预加载图片到文件缓存" + }, + { + "name": "Load_image_offline_after_preloading", + "value": "加载图片(预加载后可断网加载)" + }, + { + "name": "Preloading_GIF", + "value": "预加载gif图" + }, + { + "name": "Retrieve_GIF_from_memory", + "value": "内存缓存获取gif" + }, + { + "name": "Retrieve_GIF_from_disk", + "value": "文件缓存获取gif" + }, + { + "name": "Preloading_static_images", + "value": "预加载静态图" + }, + { + "name": "Retrieve_images_from_memory", + "value": "内存缓存获取" + }, + { + "name": "Retrieve_images_from_disk", + "value": "文件缓存获取" + }, + { + "name": "Write_memory_and_file", + "value": "写入内存文件缓存" + }, + { + "name": "Write_memory", + "value": "写入内存缓存" + }, + { + "name": "Write_file", + "value": "写入文件缓存" + }, + { + "name": "Main_image_Fill", + "value": "主图Fill拉伸填充" + }, + { + "name": "Maintain_proportion_filling", + "value": "占位图Contain保持比例填充" + }, + { + "name": "Error_graph_None", + "value": "错误图None不变化" + }, + { + "name": "Test_failure_success", + "value": "测试失败/成功场景" + }, + { + "name": "Custom_download_failed", + "value": "自定义下载失败" + }, + { + "name": "Retrieve_media_gallery", + "value": "获取媒体图库的uri用ImageKnife展示" + }, + { + "name": "Click_load_Uri", + "value": "点击加载Uri并展示" } ] } \ No newline at end of file diff --git a/sharedlibrary/src/main/ets/pages/Index.ets b/sharedlibrary/src/main/ets/pages/Index.ets index 53fb147..92d223f 100644 --- a/sharedlibrary/src/main/ets/pages/Index.ets +++ b/sharedlibrary/src/main/ets/pages/Index.ets @@ -21,7 +21,7 @@ export struct IndexComponent { }) build() { Column() { - Button("预加载").onClick((event: ClickEvent) => { + Button($r('app.string.Preload')).onClick((event: ClickEvent) => { ImageKnife.getInstance() .preLoadCache('https://hbimg.huabanimg.com/95a6d37a39aa0b70d48fa18dc7df8309e2e0e8e85571e-x4hhks_fw658/format/webp') .then((data) => { diff --git a/sharedlibrary/src/main/resources/base/element/string.json b/sharedlibrary/src/main/resources/base/element/string.json index 98e1d8a..dc2fa1f 100644 --- a/sharedlibrary/src/main/resources/base/element/string.json +++ b/sharedlibrary/src/main/resources/base/element/string.json @@ -3,6 +3,10 @@ { "name": "shared_desc", "value": "description" + }, + { + "name": "Preload", + "value": "Preload" } ] } \ No newline at end of file diff --git a/sharedlibrary/src/main/resources/zh_CN/element/string.json b/sharedlibrary/src/main/resources/zh_CN/element/string.json new file mode 100644 index 0000000..718792d --- /dev/null +++ b/sharedlibrary/src/main/resources/zh_CN/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "Preload", + "value": "预加载" + } + ] +} \ No newline at end of file