OpenHarmony实现拍照、从相册选择、图片上传、照片添加水印等操作

400 阅读5分钟
/**
 * 选择图片接口。
 *
 * 该函数提供了一个接口,用于从相册中选择图片或使用相机拍摄图片。支持设置来源类型、选择数量、是否保存到相册、水印等参数。
 *
 * @param params 参数对象,包含以下属性:
 *   - sourceType 图片来源类型,可以是相册(album)或相机(camera)。
 *   - count 可选择的最大图片数量。
 *   - isSaveToAlbum 是否将图片保存到相册。
 *   - watermark 图片上的水印。
 * @returns 返回一个Promise,解析为一个包含图片信息的对象数组。
 */
export async function chooseImage(params: ESObject): Promise<ESObject> {
  const sourceType: string[] = params?.sourceType || ['album', 'camera'];
  let count: number = params?.count || 0;
  let isSaveToAlbum: number = params?.isSaveToAlbum || 1;
  let watermark: string[] = params?.watermark || [];
  let context = getContext();
  // 参数校验
  if (!Array.isArray(sourceType) || typeof count !== 'number' || !Array.isArray(watermark)) {
    return Promise.reject(new Error('Invalid parameters'));
  }
  try {
    const win = await window.getLastWindow(context);
    let promptAction: PromptAction = win.getUIContext().getPromptAction();

    if (sourceType.includes('album')) {
      const data = await promptAction.showActionMenu({
        title: '',
        buttons: [
          {
            text: $r('app.string.ZKGJ001534'),
            color: '#1a1a1a'
          },
          {
            text: $r('app.string.ZKGJ000402'),
            color: '#1a1a1a'
          }
        ]
      });

      if (data.index === 0) {
        return captureImage(isSaveToAlbum, watermark);
      } else {
        return pickImage(count, watermark);
      }
    } else {
      return captureImage(isSaveToAlbum, watermark);
    }
  } catch (error) {
    return Promise.reject(error);
  }
}

/**
 * 异步捕获图像。
 *
 * 通过调用相机能力捕获图像,并根据参数决定是否将图像保存到相册。
 * 如果应用没有所需的权限,此函数将抛出错误。
 *
 * @param isSaveToAlbum 布尔值,指示是否将捕获的图像保存到相册。
 * @returns 返回一个Promise,解析为本地图像ID。
 * @throws 如果捕获图像或保存图像到相册失败,将抛出错误。
 */
async function captureImage(isSaveToAlbum: number, watermark: string[]): Promise<string[]> {
  const context = getContext() as common.UIAbilityContext;
  const localId = `${dayjs().format('YYYYMMDDHHmmssSSS')}_openHarmony.jpg`;
  const saveDir = `${CAMERA_DIRECTORY}/`;
  const filePath = `${saveDir}${localId}`;

  try {
    const result = await checkPermissions(['ohos.permission.WRITE_IMAGEVIDEO'], context);
    if (result !== 'success') {
      throw new Error('No permission');
    }

    if (!isFileExist(saveDir)) {
      fs.mkdirSync(saveDir, true);
    }

    fs.createRandomAccessFileSync(filePath, fs.OpenMode.CREATE);
    const src = fileUri.getUriFromPath(filePath);
    const pickerProfile: picker.PickerProfile = {
      cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK,
      saveUri: src
    };
    const pickerResult = await picker.pick(context, [picker.PickerMediaType.PHOTO], pickerProfile);

    if (!pickerResult.resultUri) {
      throw new Error('Failed to capture image');
    }

    if (isSaveToAlbum) {
      await saveToAlbum(src);
    }

    await addWaterMark(localId, watermark);

    return [localId];
  } catch (error) {
    throw new Error(`Failed to capture image: ${error.message}`);
  }
}

/**
 * 将图片保存到相册。
 *
 * 本函数通过获取相册访问权限,创建新的图片资产,并将指定图片URI的图片复制到这个新资产中,
 * 最终完成将图片保存到相册的操作。
 *
 * @param imageUri 图片的URI,指定要保存到相册的图片的位置。
 * @returns 无返回值。
 * @throws 如果保存图片到相册失败,将抛出错误。
 */
async function saveToAlbum(imageUri: string): Promise<void> {
  const context = getContext() as common.UIAbilityContext;

  try {
    // 获取相册访问的 helper 实例
    const helper = photoAccessHelper.getPhotoAccessHelper(context);

    // 创建一个新的图片资产,并获取目标 URI
    const dest = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg');

    // 打开源文件和目标文件
    const srcFile = fs.openSync(imageUri, fs.OpenMode.READ_ONLY);
    const destFile = fs.openSync(dest, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);

    // 将源文件内容复制到目标文件
    fs.copyFileSync(srcFile.fd, destFile.fd);

    // 关闭文件句柄
    fs.closeSync(srcFile);
    fs.closeSync(destFile);

    console.log('Image saved to album successfully:', dest);

  } catch (error) {
    console.error('Failed to save image to album:', error);
    throw new Error(`Failed to save image to album: ${error.message}`);
  }
}


/**
 * 异步选择图片并添加水印。
 *
 * 该函数允许用户选择指定数量的图片,并对这些图片添加水印后保存到本地。
 * 它使用了photoAccessHelper模块来处理图片选择和文件系统的操作来保存图片。
 *
 * @param count 图片选择的数量限制。
 * @param watermark 水印文字数组,用于在图片上添加文字水印。
 * @returns 返回一个包含已处理图片本地ID的数组。
 * @throws 如果选择图片或添加水印过程中发生错误,则抛出异常。
 */
async function pickImage(count: number, watermark: string[]): Promise<string[]> {
  const PhotoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
  PhotoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
  PhotoSelectOptions.maxSelectNumber = count;
  try {
    const photoPicker = new photoAccessHelper.PhotoViewPicker();
    const PhotoSelectResult = await photoPicker.select(PhotoSelectOptions);
    const localIds: string[] = [];
    for (const photoUri of PhotoSelectResult.photoUris) {
      const fileName = getFileName(photoUri);
      const fileType = fileName.lastIndexOf('.') > -1 ? fileName.substring(fileName.lastIndexOf('.')) : '';
      const localId = `${dayjs().format('YYYYMMDDHHmmssSSS')}_openHarmony${fileType}`;
      const saveDir = `${CAMERA_DIRECTORY}/`;
      const dest = `${saveDir}${localId}`;
      if (!isFileExist(saveDir)) {
        fs.mkdirSync(saveDir, true);
      }
      const srcFile = fs.openSync(photoUri, fs.OpenMode.READ_ONLY);
      const destFile = fs.openSync(dest, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
      fs.copyFileSync(srcFile.fd, destFile.fd);
      fs.closeSync(srcFile);
      fs.closeSync(destFile);
      await addWaterMark(localId, watermark);
      localIds.push(localId);
    }

    return localIds;
  } catch (error) {
    throw new Error(`Failed to pick images: ${error.message}`);
  }
}

/**
 * 异步函数:为指定的本地文件添加水印。
 * @param localId 本地文件的唯一标识符。
 * @param watermark 要添加的水印数组。
 * @returns 无返回值。
 *
 * 该函数首先通过文件标识符构建文件的URI,然后读取文件内容。
 * 使用读取到的内容创建ImageSource对象,通过此对象获取图片信息。
 * 根据图片信息创建PixelMap,并在PixelMap上添加水印。
 * 最后,将添加了水印的PixelMap保存回原文件。
 * 如果在任何步骤中发生错误,函数将抛出异常。
 */
async function addWaterMark(localId: string, watermark: string[]): Promise<void> {
  const uri = `${CAMERA_DIRECTORY}/${localId}`;
  const buffer = readFile(uri);
  const imageSource: image.ImageSource = image.createImageSource(buffer);
  try {
    const value = await imageSource.getImageInfo();
    const pixelMap = await createPixelMap(imageSource, value.size);
    if (!pixelMap) {
      throw new Error("Failed to create PixelMap");
    }
    const modifiedPixelMap = await addWatermarkToPixelMap(pixelMap, value.size, watermark);
    if (!modifiedPixelMap) {
      throw new Error("Failed to create modified PixelMap");
    }
    await saveToFile(modifiedPixelMap, uri);
    console.log('Watermark added and image saved successfully:', uri);
  } catch (error) {
    console.error('Failed to add watermark:', error);
    throw new Error(`Failed to add watermark: ${error.message}`);
  }
}

/**
 * 上传图片函数
 * @param {ESObject} params - 上传参数对象
 * @param {string} params.localId - 图片的本地ID
 * @param {string} params.fileName - 图片的文件名
 * @param {number} params.isShowProgressTips - 是否显示上传进度提示,默认为1,显示
 * @param {number} params.timeout - 上传超时时间,默认为0,表示不超时
 * @param {string} params.uploadDir - 上传目录,可选,用于指定上传文件的具体目录
 * @returns {Promise<ESObject>} 返回一个Promise对象,包含上传成功后的服务器ID、文件名、基础URL等信息
 */
export function uploadImage(params: ESObject): Promise<ESObject> {
  const localId: string = params?.localId || '';
  const fileName: string = params?.fileName || '';
  const filePath: string = `${CAMERA_DIRECTORY}${viewId}files/${localId}`;
  let uploadDir: string = params?.uploadDir || '';
  const imageStore:ImageRelationalStore = ImageRelationalStore.getInstance()

  if (isFileExist(filePath) && isFile(filePath)) {
    let buffer: ArrayBuffer = readFile(filePath);
    let formData = new FormData();
    formData.append('uploadfile', buffer, localId);
    formData.append('uploadDir', uploadDir);
    formData.append('fileName', fileName || localId);
    formData.append('fileSize', String(getFileSize(filePath)));
    return new Promise(async (resolve, reject) => {
      let hashCode = await generateHash(localId, getFileSize(filePath));
      formData.append('hashCode', hashCode);
      let uploadUrl = Global.getInstance().getImageUploadUrl();
      let fileData: FileData = {
        localId:localId,
        fileName:fileName || localId,
        uploadDir:uploadDir,
        filePath:filePath,
      }
      imageStore?.insert(fileData)?.then(() => {
        imageStore.upload()
        resolve({
          serverId: localId,
          fileName: fileName,
          baseUrl: Global.getInstance().getImageUrl(),
          path: `${uploadDir}${localId}`,
          localPath: 'file://' + filePath
        })
      })?.catch((err: ESObject) => {
        console.info('imageStore err' + JSON.stringify(err))
      })
    });
  } else {
    return Promise.reject(new Error('localFile is not exist'))
  }
}

/**
 * 创建一个像素映射。
 *
 * 此异步函数通过给定的图像源和图像大小,创建一个像素映射对象。像素映射是对图像的像素级操作的接口,
 * 允许进行诸如编辑、分析等操作。
 *
 * @param imageSource 图像源对象,提供创建像素映射所需的图像数据。
 * @param imageSize 图像的原始大小,用于计算像素映射的大小。
 * @returns 返回一个Promise,解析为image.PixelMap类型,代表创建的像素映射。
 */
async function createPixelMap(imageSource: image.ImageSource, imageSize: image.Size): Promise<image.PixelMap> {
  const defaultSize: image.Size = {
    height: Math.round(imageSize.height * 1),
    width: Math.round(imageSize.width * 1)
  };

  const opts: image.DecodingOptions = {
    editable: true,
    desiredSize: defaultSize
  };

  return new Promise((resolve, reject) => {
    imageSource.createPixelMap(opts).then(pixelMap => {
      resolve(pixelMap);
    }).catch((err: BusinessError) => {
      reject(err);
    })
  });
}

/**
 * 向图像添加水印。
 *
 * 此函数在给定的PixelMap上绘制文本水印。它根据图像宽度与默认显示宽度的比例计算缩放因子,
 * 并从图像底部开始绘制水印。水印使用白色笔和刷子绘制,以确保在任何背景上的可见性。
 *
 * @param pixelMap 要添加水印的图像对象。
 * @param imageSize 图像的尺寸。
 * @param watermarks 水印文本数组。
 * @returns 返回带有添加了水印的PixelMap对象。
 */
function addWatermarkToPixelMap(pixelMap: image.PixelMap, imageSize: image.Size, watermarks: string[]): image.PixelMap {
  const imageScale = imageSize.width / display.getDefaultDisplaySync().width;
  const canvas = new drawing.Canvas(pixelMap);

  const pen = new drawing.Pen();
  pen.setColor({ alpha: 255, red: 255, green: 255, blue: 255 }); // White color for text

  const font = new drawing.Font();
  font.setSize(32 * imageScale);

  const brush = new drawing.Brush();
  brush.setColor({ alpha: 255, red: 255, green: 255, blue: 255 }); // White color for brush

  const lineHeight = font.getSize() * 1.5; // Line height for spacing between lines
  let yPos = 0;
  if (imageSize.width > imageSize.height) {
    // Landscape orientation
    yPos = imageSize.height - 50 * imageScale;
  } else {
    // Portrait orientation
    yPos = imageSize.width - 50 * imageScale;
  }

  // Loop through each watermark string in reverse order
  for (let i = watermarks.length - 1; i >= 0; i--) {
    const watermark = watermarks[i];
    let lines = breakTextIntoLines(watermark, font, imageSize.width - 40 * imageScale); // Allow some padding on both sides

    lines.forEach(line => {
      const textBlob = drawing.TextBlob.makeFromString(line, font, drawing.TextEncoding.TEXT_ENCODING_UTF8);
      canvas.attachBrush(brush);
      canvas.attachPen(pen);
      canvas.drawTextBlob(textBlob, 20 * imageScale, yPos);
      canvas.detachBrush();
      canvas.detachPen();
      yPos -= lineHeight; // Move to the next line
    });
  }

  return pixelMap;
}

/**
 * 将文本根据指定的字体和最大宽度拆分成多行。
 *
 * 此函数的目的是为了处理文本布局问题,确保每一行的宽度都不超过给定的最大宽度。
 * 它通过将文本拆分成单词,然后逐个尝试将单词添加到当前行来实现这个目标。
 * 如果添加单词后当前行的宽度超过了最大宽度,则将当前行作为一个独立的行添加到结果中,
 * 并开始新的当前行。
 *
 * @param text 待处理的文本字符串。
 * @param font 用于测量文本的字体对象。
 * @param maxWidth 每行允许的最大宽度,以像素为单位。
 * @returns 一个字符串数组,表示拆分后的文本行。
 */
function breakTextIntoLines(text: string, font: drawing.Font, maxWidth: number): string[] {
  const lines: string[] = [];
  let currentLine = '';
  const words = text.split(' ');

  words.forEach(word => {
    const testLine = currentLine.length === 0 ? word : `${currentLine} ${word}`;
    const testWidth = font.measureText(testLine, drawing.TextEncoding.TEXT_ENCODING_UTF8);

    if (testWidth <= maxWidth) {
      if (currentLine.length === 0) {
        currentLine = word;
      } else {
        currentLine += ` ${word}`;
      }
    } else {
      lines.push(currentLine);
      currentLine = word;
    }
  });

  if (currentLine.length > 0) {
    lines.push(currentLine);
  }

  return lines;
}

/**
 * 将像素图保存到指定路径的文件中。
 *
 * 此函数使用图像打包器将像素图打包为JPEG格式,并写入到指定路径的文件中。
 * 如果文件已存在,将覆盖原有文件内容。
 *
 * @param pixelMap image.PixelMap - 要保存的像素图对象。
 * @param imagePath string - 图像文件的保存路径。
 * @throws 如果保存过程中发生错误,则抛出异常。
 */
export async function saveToFile(pixelMap: image.PixelMap, imagePath: string): Promise<void> {
  let fd: number | null = null;
  try {
    const filePath = imagePath;
    const imagePacker = image.createImagePacker();
    const imageBuffer = await imagePacker.packing(pixelMap, {
      format: 'image/jpeg',
      quality: 100
    });
    const mode = fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE;
    fd = (await fs.open(filePath, mode)).fd;
    await fs.truncate(fd);
    await fs.write(fd, imageBuffer);
  } catch (err) {
    console.error('saveToFile error: ' + err);
    throw new Error(`Failed to save image to file: ${err}`);
  } finally {
    if (fd) {
      fs.close(fd);
    }
  }
}