HarmonyOS鸿蒙最新OpenHarmony开发实战:图片编辑(ArkTS)(2),2024年最新网易 面经

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!


img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上鸿蒙开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化的资料的朋友,可以戳这里获取

在进行图片编辑前需要先加载图片,当前文档是在生命周期aboutToAppear开始加载。具体实现步骤。

  1. 读取资源文件。
  2. 将获取的fd创建成图片实例,通过实例获取其pixelMap。
  3. 将解析好的pixelMap通过Image组件加载显示。
// HomePage.ets
aboutToAppear() {
  this.pixelInit();
  ...
}

build() {
  Column() {
    ...
    Column() {
      if (this.isCrop && this.showCanvas && this.statusBar > 0) {
        if (this.isSaveFresh) {
          ImageSelect({
            statusBar: this.statusBar
          })
        }
        ...
      } else {
        if (this.isPixelMapChange) {
          Image(this.pixelMap)
            .scale({ x: this.imageScale, y: this.imageScale, z: 1 })
            .objectFit(ImageFit.None)
        }
        ...
      }
    }
    ...
  }
  ...
}

async getResourceFd(filename: string) {
  const resourceMgr = getContext(this).resourceManager;
  const context = getContext(this);
  if (filename === CommonConstants.RAW_FILE_NAME) {
    let imageBuffer = await resourceMgr.getMediaContent($r("app.media.ic_low"))
    let filePath = context.cacheDir + '/' + filename;
    let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
    let writeLen = fs.writeSync(file.fd, imageBuffer.buffer);
    fs.copyFileSync(filePath, context.cacheDir + '/' + CommonConstants.RAW_FILE_NAME_TEST);
    return file.fd;
  } else {
    let filePath = context.cacheDir + '/' + filename;
    let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
    return file.fd;
  }
}

async getPixelMap(fileName: string) {
  const fd = await this.getResourceFd(fileName);
  const imageSourceApi = image.createImageSource(fd);
  if (!imageSourceApi) {
    Logger.error(TAG, 'imageSourceAPI created failed!');
    return;
  }
  const pixelMap = await imageSourceApi.createPixelMap({
    editable: true
  });
  return pixelMap;
}

图片处理

当前章节需要完成图片的裁剪、旋转、色域调节(本章只介绍亮度、透明度、饱和度)等功能。

  • 裁剪:选取图片中的部分进行裁剪生成新的图片。
  • 旋转:将图片按照不同的角度进行旋转,生成新的图片。
  • 色域调节:当前Codelab色域调节的亮度、透明度和饱和度,使用色域模型RGB-HSV来实现的。
    • RGB:是我们接触最多的颜色空间,分别为红色®,绿色(G)和蓝色(B)。
    • HSV:是用色相H,饱和度S,明亮度V来描述颜色的变化。
      • H:色相H取值范围为0°~360°,从红色开始按逆时针方向计算,红色为0°,绿色为120°,蓝色为240°。
      • S:饱和度S越高,颜色则深而艳。光谱色的白光成分为0,饱和度达到最高。通常取值范围为0%~100%,值越大,颜色越饱和。
      • V:明度V表示颜色明亮的程度,对于光源色,明度值与发光体的光亮度有关;对于物体色,此值和物体的透射比或反射比有关。通常取值范围为0%(黑)到100%(白)。
// AdjustUtil.ets
// rgb转hsv
function rgb2hsv(red: number, green: number, blue: number) {
  let hsvH: number = 0, hsvS: number = 0, hsvV: number = 0;
  const rgbR: number = colorTransform(red);
  const rgbG: number = colorTransform(green);
  const rgbB: number = colorTransform(blue);
  const maxValue = Math.max(rgbR, Math.max(rgbG, rgbB));
  const minValue = Math.min(rgbR, Math.min(rgbG, rgbB));
  hsvV = maxValue * CommonConstants.CONVERT_INT;
  if (maxValue === 0) {
    hsvS = 0;
  } else {
    hsvS = Number((1 - minValue / maxValue).toFixed(CommonConstants.DECIMAL_TWO)) * CommonConstants.CONVERT_INT;
  }
  if (maxValue === minValue) {
    hsvH = 0;
  }
  if (maxValue === rgbR && rgbG >= rgbB) {
    hsvH = Math.floor(CommonConstants.ANGLE_60 * ((rgbG - rgbB) / (maxValue - minValue)));
  }
  if (maxValue === rgbR && rgbG < rgbB) {
    hsvH = Math.floor(CommonConstants.ANGLE_60 * ((rgbG - rgbB) / (maxValue - minValue)) + CommonConstants.ANGLE_360);
  }
  if (maxValue === rgbG) {
    hsvH = Math.floor(CommonConstants.ANGLE_60 * ((rgbB - rgbR) / (maxValue - minValue)) + CommonConstants.ANGLE_120);
  }
  if (maxValue === rgbB) {
    hsvH = Math.floor(CommonConstants.ANGLE_60 * ((rgbR - rgbG) / (maxValue - minValue)) + CommonConstants.ANGLE_240);
  }
  return [hsvH, hsvS, hsvV];
}
// hsv转rgb
function hsv2rgb(hue: number, saturation: number, value: number) {
  let rgbR: number = 0, rgbG: number = 0, rgbB: number = 0;
  if (saturation === 0) {
    rgbR = rgbG = rgbB = Math.round((value * CommonConstants.COLOR_LEVEL_MAX) / CommonConstants.CONVERT_INT);
    return { rgbR, rgbG, rgbB };
  }
  const cxmC = (value * saturation) / (CommonConstants.CONVERT_INT * CommonConstants.CONVERT_INT);
  const cxmX = cxmC * (1 - Math.abs((hue / CommonConstants.ANGLE_60) % CommonConstants.MOD_2 - 1));
  const cxmM = (value - cxmC * CommonConstants.CONVERT_INT) / CommonConstants.CONVERT_INT;
  const hsvHRange = Math.floor(hue / CommonConstants.ANGLE_60);
  switch (hsvHRange) {
    case AngelRange.ANGEL_0_60:
      rgbR = (cxmC + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      rgbG = (cxmX + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      rgbB = (0 + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      break;
    case AngelRange.ANGEL_60_120:
      rgbR = (cxmX + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      rgbG = (cxmC + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      rgbB = (0 + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      break;
    case AngelRange.ANGEL_120_180:
      rgbR = (0 + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      rgbG = (cxmC + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      rgbB = (cxmX + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      break;
    case AngelRange.ANGEL_180_240:
      rgbR = (0 + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      rgbG = (cxmX + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      rgbB = (cxmC + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      break;
    case AngelRange.ANGEL_240_300:
      rgbR = (cxmX + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      rgbG = (0 + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      rgbB = (cxmC + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      break;
    case AngelRange.ANGEL_300_360:
      rgbR = (cxmC + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      rgbG = (0 + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      rgbB = (cxmX + cxmM) * CommonConstants.COLOR_LEVEL_MAX;
      break;
    default:
      break;
  }
  return [
    Math.round(rgbR),
    Math.round(rgbG),
    Math.round(rgbB)
  ];
}
图片裁剪
  1. 通过pixelMap获取图片尺寸,为后续裁剪做准备。
  2. 确定裁剪的方式,当前裁剪默认有自由选取、1:1选取、4:3选取、16:9选取。
  3. 通过pixelMap调用接口crop()进行裁剪操作。

说明: 当前裁剪功能采用pixelMap裁剪能力直接做切割,会有叠加效果,后续会通过增加选取框对当前功能进行优化。

// HomePage.ets
cropImage(index: CropType) {
  this.currentCropIndex = index;
  switch (this.currentCropIndex) {
    case CropType.ORIGINAL_IMAGE:
      this.cropRatio = CropRatioType.RATIO_TYPE_FREE;
      break;
    case CropType.SQUARE:
      this.cropRatio = CropRatioType.RATIO_TYPE_1_1;
      break;
    case CropType.BANNER:
      this.cropRatio = CropRatioType.RATIO_TYPE_4_3;
      break;
    case CropType.RECTANGLE:
      this.cropRatio = CropRatioType.RATIO_TYPE_16_9;
      break;
    default:
      this.cropRatio = CropRatioType.RATIO_TYPE_FREE;
      break;
  }
}

// ImageFilterCrop.ets
cropImage(pixelMap: PixelMapWrapper, realCropRect: RectF, callback: () => void) {
  let offWidth = realCropRect.getWidth();
  let offHeight = realCropRect.getHeight();
  if (pixelMap.pixelMap!== undefined) {
    pixelMap.pixelMap.crop({
      size:{ height: vp2px(offHeight), width: vp2px(offWidth) },
      x: vp2px(realCropRect.left),
      y: vp2px(realCropRect.top)
    }, callback);
  }
}
图片旋转
  1. 确定旋转方向,当前支持顺时针和逆时针旋转。
  2. 通过pixelMap调用接口rotate()进行旋转操作。

// HomePage.ets
rotateImage(rotateType: RotateType) {
  if (rotateType === RotateType.CLOCKWISE) {
    try {
      if (this.pixelMap !== undefined) {
        this.pixelMap.rotate(CommonConstants.CLOCK_WISE)
          .then(() => {
            this.flushPixelMapNew();
          })
      }
    } catch (error) {
      Logger.error(TAG, `there is a error in rotate process with ${error?.code}`);
    }
  }
  if (rotateType === RotateType.ANTI_CLOCK) {
    try {
      if (this.pixelMap !== undefined) {
        this.pixelMap.rotate(CommonConstants.ANTI_CLOCK)
          .then(() => {
            this.flushPixelMapNew();
          })
      }
    } catch (error) {
      Logger.error(TAG, `there is a error in rotate process with ${error?.code}`);
    }
  }
}
亮度调节
  1. 将pixelMap转换成ArrayBuffer。
  2. 将生成好的ArrayBuffer发送到worker线程。
  3. 对每一个像素点的亮度值按倍率计算。
  4. 将计算好的ArrayBuffer发送回主线程。
  5. 将ArrayBuffer写入pixelMap,刷新UI。

说明: 当前亮度调节是在UI层面实现的,未实现细节优化算法,只做简单示例。调节后的图片会有色彩上的失真。

// AdjustContentView.ets
// 转化成pixelMap及发送buffer到worker,返回数据刷新ui
postToWorker(type: AdjustId, value: number, workerName: string) {
  let sliderValue = type === AdjustId.BRIGHTNESS ? this.brightnessLastSlider : this.saturationLastSlider;
  try {
    let workerInstance = new worker.ThreadWorker(workerName);
    const bufferArray = new ArrayBuffer(this.pixelMap.getPixelBytesNumber());
    this.pixelMap.readPixelsToBuffer(bufferArray).then(() => {
      let message = new MessageItem(bufferArray, sliderValue, value);
      workerInstance.postMessage(message);
      if (this.postState) {
        this.deviceListDialogController.open();
      }
      this.postState = false;
      workerInstance.onmessage = (event: MessageEvents) => {
        this.updatePixelMap(event)
      };
      if (type === AdjustId.BRIGHTNESS) {
        this.brightnessLastSlider = Math.round(value);
      } else {
        this.saturationLastSlider = Math.round(value);
      }
      workerInstance.onexit = () => {
        if (workerInstance !== undefined) {
          workerInstance.terminate();
        }
      }
    });
  } catch (error) {
    Logger.error(`Create work instance fail, error message: ${JSON.stringify(error)}`)
  }
}

// AdjustBrightnessWork.ts
// worker线程处理部分
workerPort.onmessage = function(event : MessageEvents) {
  let bufferArray = event.data.buf;
  let last = event.data.last;
  let cur = event.data.cur;
  let buffer = adjustImageValue(bufferArray, last, cur);
  workerPort.postMessage(buffer);
  workerPort.close();
}

// AdjustUtil.ets
// 倍率计算部分
export function adjustImageValue(bufferArray: ArrayBuffer, last: number, cur: number) {
  return execColorInfo(bufferArray, last, cur, HSVIndex.VALUE);
}
透明度调节
  1. 获取pixelMap。
  2. 调用接口opacity()进行透明度调节。

// OpacityUtil.ets
export async function adjustOpacity(pixelMap: PixelMap, value: number) {
  if (!pixelMap) {
    return;
  }
  const newPixelMap = pixelMap;
  await newPixelMap.opacity(value / CommonConstants.SLIDER_MAX);
  return newPixelMap;
}
饱和度调节
  1. 将pixelMap转换成ArrayBuffer。
  2. 将生成好的ArrayBuffer发送到worker线程。
  3. 对每一个像素点的饱和度按倍率计算。
  4. 将计算好的ArrayBuffer发送回主线程。
  5. 将ArrayBuffer写入pixelMap,刷新UI。

说明: 当前饱和度调节是在UI层面实现的,未实现细节优化算法,只做简单示例。调节后的图片会有色彩上的失真。

// AdjustContentView.ets
// 转化成pixelMap及发送buffer到worker,返回数据刷新ui
postToWorker(type: AdjustId, value: number, workerName: string) {
  let sliderValue = type === AdjustId.BRIGHTNESS ? this.brightnessLastSlider : this.saturationLastSlider;
  try {
    let workerInstance = new worker.ThreadWorker(workerName);
    const bufferArray = new ArrayBuffer(this.pixelMap.getPixelBytesNumber());
    this.pixelMap.readPixelsToBuffer(bufferArray).then(() => {
      let message = new MessageItem(bufferArray, sliderValue, value);
      workerInstance.postMessage(message);
      if (this.postState) {
        this.deviceListDialogController.open();
      }
      this.postState = false;
      workerInstance.onmessage = (event: MessageEvents) => {
        this.updatePixelMap(event)
      };
      if (type === AdjustId.BRIGHTNESS) {
        this.brightnessLastSlider = Math.round(value);
      } else {
        this.saturationLastSlider = Math.round(value);
      }
      workerInstance.onexit = () => {
        if (workerInstance !== undefined) {
          workerInstance.terminate();
        }
      }
    });
  } catch (error) {
    Logger.error(`Create work instance fail, error message: ${JSON.stringify(error)}`);
  }
}

// AdjustSaturationWork.ts
// worker线程处理部分
workerPort.onmessage = function(event : MessageEvents) {
  let bufferArray = event.data.buf;
  let last = event.data.last;
  let cur = event.data.cur;
  let buffer = adjustSaturation(bufferArray, last, cur)
  workerPort.postMessage(buffer);
  workerPort.close();
}

// AdjustUtil.ets
// 倍率计算部分
export function adjustSaturation(bufferArray: ArrayBuffer, last: number, cur: number) {
  return execColorInfo(bufferArray, last, cur, HSVIndex.SATURATION);
}

图片编码

图片位图经过处理之后,还是属于解码的状态,还需要进行打包编码成对应的格式,本章讲解编码的具体过程。

  1. 通过image组件创建打包工具packer。
  2. 使用PackingOption进行打包参数设定,比如格式、压缩质量等。
  3. 打包成图片信息数据imageData。
  4. 创建媒体库media,获取公共路径。
  5. 创建媒体文件asset,获取其fd。
  6. 使用fs将打包好的图片数据写入到媒体文件asset中。

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

创建打包工具packer。
2. 使用PackingOption进行打包参数设定,比如格式、压缩质量等。
3. 打包成图片信息数据imageData。
4. 创建媒体库media,获取公共路径。
5. 创建媒体文件asset,获取其fd。
6. 使用fs将打包好的图片数据写入到媒体文件asset中。

[外链图片转存中…(img-SqRfWwtP-1715882559351)]
[外链图片转存中…(img-IUdqCNDt-1715882559352)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值