问题背景

在HarmonyOS应用开发中,PDF文档处理是一个常见的业务需求。开发者经常需要将PDF文档转换为图片格式,以便在应用中展示、编辑或分享。华为HarmonyOS提供了convertToImageAPI来实现PDF到图片的转换功能,但在实际使用过程中,开发者遇到了一个普遍问题:转换生成的图片文件名是按数字顺序自动命名的,无法满足业务场景中的自定义命名需求

典型业务场景

  • 文档阅读应用中,用户需要将PDF页面保存为图片并自定义文件名

  • 办公软件中,批量导出PDF页面图片需要按内容命名

  • 教育应用中,课件PDF转换为图片后需要按章节重命名

  • 企业应用中,合同PDF转换为图片后需要按合同编号命名

技术挑战

  • convertToImageAPI生成的图片默认命名为1.png2.png等数字序列

  • 多PDF文件转换时,同名文件会相互覆盖

  • 业务逻辑需要根据PDF内容或元数据自定义文件名

  • 需要保持文件系统的完整性和一致性

效果预览

在深入技术实现之前,先来看看最终效果:

转换流程

  1. 选择PDF文档 → 2. 转换为数字命名的图片 → 3. 批量重命名为业务需要的格式

转换前文件名

  • 1.png

  • 2.png

  • 3.png

转换后文件名

  • 合同_001.png

  • 合同_002.png

  • 合同_003.png

背景知识

PDFKit核心API

HarmonyOS的PDFKit模块提供了丰富的PDF处理能力,其中convertToImage是关键API:

// PDF文档转换图片核心API
document.convertToImage(
    outputDir: string,           // 输出目录
    format: ImageFormat,         // 图片格式
    options?: ConvertOptions     // 转换选项(可选)
): void;

参数详解

  • outputDir: 图片输出目录路径

  • format: 图片格式,支持PNG、JPEG等

  • options: 转换选项,可设置分辨率、质量等

支持的图片格式

  • ImageFormat.PNG: PNG格式,支持透明背景

  • ImageFormat.JPEG: JPEG格式,支持压缩

  • ImageFormat.WEBP: WebP格式,现代图片格式

文件管理核心能力

HarmonyOS的文件管理模块提供了完整的文件操作API:

// 文件重命名核心API
fs.renameSync(oldPath: string, newPath: string): void;

// 文件列表获取
fs.listFileSync(dir: string, options?: ListFileOptions): Array<string>;

关键文件操作

  • 文件创建、读取、写入、删除

  • 目录创建、遍历、删除

  • 文件重命名、移动、复制

  • 文件属性获取(大小、修改时间等)

资源管理机制

HarmonyOS应用需要正确处理资源文件的访问权限:

// 获取rawfile资源内容
const content: Uint8Array = context.resourceManager.getRawFileContentSync('rawfile/test.pdf');

// 写入应用沙箱目录
fs.writeSync(fd, content.buffer);

资源访问路径

  • rawfile/: 应用内置资源目录

  • filesDir/: 应用沙箱文件目录

  • cacheDir/: 应用缓存目录

  • tempDir/: 应用临时目录

完整解决方案

方案设计思路

要实现PDF转图片后的自定义重命名,我们需要解决几个关键技术问题:

  1. PDF文档加载与解析:正确加载PDF文档并获取文档信息

  2. 批量图片转换:将PDF每页高效转换为图片

  3. 文件命名策略:设计合理的文件命名规则

  4. 文件系统操作:安全、高效地操作文件系统

  5. 错误处理与资源管理:确保资源正确释放,避免内存泄漏

架构设计

┌─────────────────────────────────────────────┐
│               应用层                         │
│  ┌─────────────────────────────────────┐  │
│  │         用户界面与交互逻辑           │  │
│  └─────────────────────────────────────┘  │
├─────────────────────────────────────────────┤
│               业务逻辑层                     │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐  │
│  │PDF加载  │  │图片转换 │  │文件重命名│  │
│  └─────────┘  └─────────┘  └─────────┘  │
├─────────────────────────────────────────────┤
│               服务层                         │
│  ┌─────────┐  ┌─────────┐  ┌─────────┐  │
│  │PDFKit   │  │文件管理 │  │资源管理 │  │
│  └─────────┘  └─────────┘  └─────────┘  │
├─────────────────────────────────────────────┤
│               系统层                         │
│  ┌─────────────────────────────────────┐  │
│  │          HarmonyOS 系统API           │  │
│  └─────────────────────────────────────┘  │
└─────────────────────────────────────────────┘

命名策略设计

根据不同的业务场景,我们可以设计多种命名策略:

策略类型

命名格式

适用场景

优点

基础策略

{pdf文件名}_{页码}.png

单文档转换

简单直观,易于管理

时间策略

{时间戳}_{页码}.png

批量处理

避免文件名冲突

内容策略

{文档标题}_{页码}.png

文档管理

便于内容检索

业务策略

{合同编号}_{页码}.png

企业应用

符合业务规范

智能策略

{关键词}_{页码}_{日期}.png

智能分类

自动化程度高

具体实现步骤

步骤1:创建PDF处理工具类

首先,我们创建一个完整的PDF处理工具类,封装所有转换和重命名逻辑:

import { common } from '@kit.AbilityKit';
import { fileIo as fs, ListFileOptions } from '@kit.CoreFileKit';
import { pdfService, pdfViewManager } from '@kit.PDFKit';
import { BusinessError } from '@kit.BasicServicesKit';

/**
 * PDF转图片重命名工具类
 * 提供完整的PDF转图片并重命名功能
 */
export class PdfToImageConverter {
  private context: common.UIAbilityContext;
  private pdfDocument: pdfService.PdfDocument | null = null;
  
  // 转换配置
  private config: ConvertConfig = {
    imageFormat: pdfService.ImageFormat.PNG,
    outputQuality: 90,
    resolution: 300,
    namingStrategy: NamingStrategy.BASIC
  };
  
  // 转换状态
  private conversionStatus: ConversionStatus = {
    totalPages: 0,
    convertedPages: 0,
    isConverting: false,
    error: null
  };
  
  constructor(context: common.UIAbilityContext) {
    this.context = context;
  }
  
  /**
   * 设置转换配置
   */
  setConfig(config: Partial<ConvertConfig>): void {
    this.config = { ...this.config, ...config };
  }
  
  /**
   * 加载PDF文档
   */
  async loadPdfDocument(filePath: string): Promise<boolean> {
    try {
      // 创建PDF文档实例
      this.pdfDocument = new pdfService.PdfDocument();
      
      // 加载PDF文档
      const loadResult = this.pdfDocument.loadDocument(filePath, '');
      
      if (loadResult === pdfService.ParseResult.PARSE_SUCCESS) {
        // 获取文档总页数
        const pageCount = this.pdfDocument.getPageCount();
        this.conversionStatus.totalPages = pageCount;
        
        console.info(`PDF文档加载成功,总页数:${pageCount}`);
        return true;
      } else {
        console.error(`PDF文档加载失败,错误码:${loadResult}`);
        return false;
      }
    } catch (error) {
      console.error(`加载PDF文档异常:${JSON.stringify(error)}`);
      this.conversionStatus.error = error as BusinessError;
      return false;
    }
  }
  
  /**
   * 转换PDF为图片并重命名
   */
  async convertAndRename(
    pdfFilePath: string,
    outputDir?: string,
    namingTemplate?: string
  ): Promise<ConversionResult> {
    // 检查转换状态
    if (this.conversionStatus.isConverting) {
      throw new BusinessError({
        code: 1001,
        message: '当前有转换任务正在进行中'
      });
    }
    
    try {
      this.conversionStatus.isConverting = true;
      this.conversionStatus.convertedPages = 0;
      this.conversionStatus.error = null;
      
      // 1. 加载PDF文档
      const isLoaded = await this.loadPdfDocument(pdfFilePath);
      if (!isLoaded) {
        throw new BusinessError({
          code: 1002,
          message: 'PDF文档加载失败'
        });
      }
      
      // 2. 准备输出目录
      const finalOutputDir = outputDir || await this.prepareOutputDirectory(pdfFilePath);
      
      // 3. 执行转换
      await this.performConversion(finalOutputDir);
      
      // 4. 重命名图片文件
      const renamedFiles = await this.renameImageFiles(
        finalOutputDir,
        pdfFilePath,
        namingTemplate
      );
      
      // 5. 清理临时文件(可选)
      await this.cleanupTemporaryFiles(finalOutputDir);
      
      return {
        success: true,
        totalPages: this.conversionStatus.totalPages,
        convertedPages: this.conversionStatus.convertedPages,
        outputDirectory: finalOutputDir,
        renamedFiles: renamedFiles,
        message: 'PDF转图片并重命名完成'
      };
      
    } catch (error) {
      console.error(`转换过程异常:${JSON.stringify(error)}`);
      this.conversionStatus.error = error as BusinessError;
      
      return {
        success: false,
        totalPages: this.conversionStatus.totalPages,
        convertedPages: this.conversionStatus.convertedPages,
        outputDirectory: '',
        renamedFiles: [],
        message: `转换失败:${error.message}`,
        error: error
      };
    } finally {
      this.conversionStatus.isConverting = false;
      // 释放PDF文档资源
      this.releasePdfDocument();
    }
  }
  
  /**
   * 准备输出目录
   */
  private async prepareOutputDirectory(pdfFilePath: string): Promise<string> {
    try {
      // 获取PDF文件名(不带后缀)
      const pdfFile = fs.openSync(pdfFilePath, fs.OpenMode.READ_ONLY);
      const pdfFileName = pdfFile.name;
      const pdfFileNameWithoutExt = pdfFileName.slice(0, pdfFileName.length - 4);
      fs.closeSync(pdfFile.fd);
      
      // 创建以PDF文件名为名的目录
      const outputDir = `${this.context.filesDir}/${pdfFileNameWithoutExt}_images`;
      
      if (!fs.accessSync(outputDir)) {
        fs.mkdirSync(outputDir);
        console.info(`创建输出目录:${outputDir}`);
      }
      
      return outputDir;
    } catch (error) {
      console.error(`准备输出目录失败:${JSON.stringify(error)}`);
      throw error;
    }
  }
  
  /**
   * 执行PDF到图片的转换
   */
  private async performConversion(outputDir: string): Promise<void> {
    if (!this.pdfDocument) {
      throw new BusinessError({
        code: 1003,
        message: 'PDF文档未加载'
      });
    }
    
    try {
      console.info(`开始转换PDF到图片,输出目录:${outputDir}`);
      
      // 执行转换(同步方法)
      this.pdfDocument.convertToImage(outputDir, this.config.imageFormat);
      
      // 更新转换状态
      this.conversionStatus.convertedPages = this.conversionStatus.totalPages;
      
      console.info(`PDF转换完成,共${this.conversionStatus.totalPages}页`);
      
    } catch (error) {
      console.error(`PDF转换失败:${JSON.stringify(error)}`);
      throw error;
    }
  }
  
  /**
   * 重命名图片文件
   */
  private async renameImageFiles(
    outputDir: string,
    pdfFilePath: string,
    namingTemplate?: string
  ): Promise<string[]> {
    try {
      // 获取PDF文件信息
      const pdfFile = fs.openSync(pdfFilePath, fs.OpenMode.READ_ONLY);
      const pdfFileName = pdfFile.name;
      const pdfFileNameWithoutExt = pdfFileName.slice(0, pdfFileName.length - 4);
      fs.closeSync(pdfFile.fd);
      
      // 获取输出目录中的所有图片文件
      const listFileOption: ListFileOptions = {
        recursion: false,
        listNum: 0,
        filter: {
          suffix: ['.png', '.jpg', '.jpeg', '.webp']
        }
      };
      
      const imageFiles = fs.listFileSync(outputDir, listFileOption);
      console.info(`找到${imageFiles.length}个图片文件需要重命名`);
      
      const renamedFiles: string[] = [];
      
      // 遍历并重命名每个图片文件
      for (let i = 0; i < imageFiles.length; i++) {
        const oldFileName = imageFiles[i];
        const oldFilePath = `${outputDir}/${oldFileName}`;
        
        // 生成新文件名
        const newFileName = this.generateNewFileName(
          oldFileName,
          pdfFileNameWithoutExt,
          i + 1,
          namingTemplate
        );
        
        const newFilePath = `${outputDir}/${newFileName}`;
        
        // 执行重命名
        fs.renameSync(oldFilePath, newFilePath);
        renamedFiles.push(newFileName);
        
        console.info(`重命名:${oldFileName} -> ${newFileName}`);
      }
      
      return renamedFiles;
      
    } catch (error) {
      console.error(`重命名图片文件失败:${JSON.stringify(error)}`);
      throw error;
    }
  }
  
  /**
   * 生成新文件名
   */
  private generateNewFileName(
    oldFileName: string,
    pdfBaseName: string,
    pageNumber: number,
    template?: string
  ): string {
    // 获取文件扩展名
    const extension = this.getFileExtension(oldFileName);
    
    // 使用指定的命名模板或默认模板
    if (template) {
      return this.applyNamingTemplate(template, pdfBaseName, pageNumber, extension);
    }
    
    // 默认命名策略:{PDF文件名}_{页码}.{扩展名}
    return `${pdfBaseName}_${this.padNumber(pageNumber, 3)}.${extension}`;
  }
  
  /**
   * 应用命名模板
   */
  private applyNamingTemplate(
    template: string,
    pdfBaseName: string,
    pageNumber: number,
    extension: string
  ): string {
    // 替换模板中的变量
    let fileName = template
      .replace(/{pdfName}/g, pdfBaseName)
      .replace(/{page}/g, this.padNumber(pageNumber, 3))
      .replace(/{timestamp}/g, Date.now().toString())
      .replace(/{date}/g, this.getFormattedDate());
    
    // 确保文件名以扩展名结尾
    if (!fileName.endsWith(`.${extension}`)) {
      fileName += `.${extension}`;
    }
    
    return fileName;
  }
  
  /**
   * 获取文件扩展名
   */
  private getFileExtension(fileName: string): string {
    const lastDotIndex = fileName.lastIndexOf('.');
    if (lastDotIndex === -1) {
      return 'png'; // 默认扩展名
    }
    return fileName.substring(lastDotIndex + 1).toLowerCase();
  }
  
  /**
   * 数字补零
   */
  private padNumber(num: number, length: number): string {
    return num.toString().padStart(length, '0');
  }
  
  /**
   * 获取格式化日期
   */
  private getFormattedDate(): string {
    const now = new Date();
    const year = now.getFullYear();
    const month = this.padNumber(now.getMonth() + 1, 2);
    const day = this.padNumber(now.getDate(), 2);
    const hour = this.padNumber(now.getHours(), 2);
    const minute = this.padNumber(now.getMinutes(), 2);
    const second = this.padNumber(now.getSeconds(), 2);
    
    return `${year}${month}${day}_${hour}${minute}${second}`;
  }
  
  /**
   * 清理临时文件
   */
  private async cleanupTemporaryFiles(outputDir: string): Promise<void> {
    // 根据业务需求决定是否清理
    // 可以保留转换后的文件,也可以删除原始数字命名的文件
    // 这里示例保留所有文件
    console.info(`转换完成,文件保存在:${outputDir}`);
  }
  
  /**
   * 释放PDF文档资源
   */
  private releasePdfDocument(): void {
    if (this.pdfDocument) {
      // PDFKit目前没有显式的释放方法
      // 将引用置空以便垃圾回收
      this.pdfDocument = null;
    }
  }
  
  /**
   * 获取转换状态
   */
  getConversionStatus(): ConversionStatus {
    return { ...this.conversionStatus };
  }
  
  /**
   * 取消转换
   */
  cancelConversion(): void {
    // 由于convertToImage是同步方法,无法直接取消
    // 可以在UI层提示用户等待完成
    console.warn('转换进行中,无法取消,请等待完成');
  }
}

// 配置接口
interface ConvertConfig {
  imageFormat: pdfService.ImageFormat;
  outputQuality: number;
  resolution: number;
  namingStrategy: NamingStrategy;
}

// 转换状态接口
interface ConversionStatus {
  totalPages: number;
  convertedPages: number;
  isConverting: boolean;
  error: BusinessError | null;
}

// 转换结果接口
interface ConversionResult {
  success: boolean;
  totalPages: number;
  convertedPages: number;
  outputDirectory: string;
  renamedFiles: string[];
  message: string;
  error?: BusinessError;
}

// 命名策略枚举
enum NamingStrategy {
  BASIC = 'basic',      // 基础策略:{pdfName}_{page}.{ext}
  TIMESTAMP = 'timestamp', // 时间戳策略:{timestamp}_{page}.{ext}
  CUSTOM = 'custom'     // 自定义策略
}

步骤2:创建主页面组件

接下来,我们创建一个使用该工具类的主页面组件:

@Entry
@Component
struct PdfConversionPage {
  private converter: PdfToImageConverter;
  private context = this.getUIContext().getHostContext() as common.UIAbilityContext;
  
  // 状态管理
  @State pdfFilePath: string = '';
  @State conversionResult: ConversionResult | null = null;
  @State isConverting: boolean = false;
  @State progress: number = 0;
  @State selectedNamingTemplate: string = '{pdfName}_{page}.png';
  
  // 可用的命名模板
  private namingTemplates: Array<{ label: string, value: string }> = [
    { label: '基础模板:{pdfName}_{page}.png', value: '{pdfName}_{page}.png' },
    { label: '时间戳模板:{timestamp}_{page}.png', value: '{timestamp}_{page}.png' },
    { label: '日期模板:{date}_{page}.png', value: '{date}_{page}.png' },
    { label: '自定义模板', value: '' }
  ];
  
  aboutToAppear(): void {
    // 初始化转换器
    this.converter = new PdfToImageConverter(this.context);
    
    // 设置默认PDF文件路径
    this.initDefaultPdfFile();
  }
  
  /**
   * 初始化默认PDF文件
   */
  private initDefaultPdfFile(): void {
    const dir: string = this.context.filesDir;
    this.pdfFilePath = dir + '/sample.pdf';
    
    // 检查文件是否存在,如果不存在则从rawfile复制
    if (!fs.accessSync(this.pdfFilePath)) {
      this.copyPdfFromResources();
    }
  }
  
  /**
   * 从资源文件复制PDF
   */
  private copyPdfFromResources(): void {
    try {
      // 从rawfile读取PDF文件内容
      const content: Uint8Array = this.context.resourceManager
        .getRawFileContentSync('rawfile/sample.pdf');
      
      // 写入到应用文件目录
      let file: fs.File | null = null;
      try {
        file = fs.openSync(
          this.pdfFilePath,
          fs.OpenMode.WRITE_ONLY | fs.OpenMode.CREATE | fs.OpenMode.TRUNC
        );
        fs.writeSync(file.fd, content.buffer);
        console.info('PDF文件复制成功');
      } catch (error) {
        console.error('写入PDF文件失败:', JSON.stringify(error));
      } finally {
        if (file !== null) {
          fs.closeSync(file.fd);
        }
      }
    } catch (error) {
      console.error('读取资源文件失败:', JSON.stringify(error));
    }
  }
  
  /**
   * 执行PDF转换
   */
  private async convertPdfToImages(): Promise<void> {
    if (this.isConverting) {
      return;
    }
    
    this.isConverting = true;
    this.progress = 0;
    this.conversionResult = null;
    
    try {
      // 更新进度
      this.progress = 10;
      
      // 执行转换
      const result = await this.converter.convertAndRename(
        this.pdfFilePath,
        undefined, // 使用默认输出目录
        this.selectedNamingTemplate || undefined // 使用选中的命名模板
      );
      
      // 更新结果
      this.conversionResult = result;
      this.progress = 100;
      
      // 显示结果
      if (result.success) {
        this.showSuccessMessage(result);
      } else {
        this.showErrorMessage(result);
      }
      
    } catch (error) {
      console.error('转换过程异常:', JSON.stringify(error));
      this.conversionResult = {
        success: false,
        totalPages: 0,
        convertedPages: 0,
        outputDirectory: '',
        renamedFiles: [],
        message: `转换失败:${error.message}`,
        error: error as BusinessError
      };
      this.showErrorMessage(this.conversionResult);
    } finally {
      this.isConverting = false;
    }
  }
  
  /**
   * 显示成功消息
   */
  private showSuccessMessage(result: ConversionResult): void {
    // 在实际应用中,这里可以显示Toast或Dialog
    console.info('转换成功:', result.message);
    console.info(`输出目录:${result.outputDirectory}`);
    console.info(`重命名文件:${result.renamedFiles.join(', ')}`);
  }
  
  /**
   * 显示错误消息
   */
  private showErrorMessage(result: ConversionResult): void {
    // 在实际应用中,这里可以显示错误提示
    console.error('转换失败:', result.message);
    if (result.error) {
      console.error('错误详情:', JSON.stringify(result.error));
    }
  }
  
  /**
   * 选择PDF文件
   */
  private async selectPdfFile(): Promise<void> {
    // 这里可以实现文件选择器逻辑
    // 由于HarmonyOS文件选择API较复杂,这里简化为示例
    console.info('打开文件选择器...');
    
    // 模拟文件选择
    const selectedFile = await this.simulateFilePicker();
    if (selectedFile) {
      this.pdfFilePath = selectedFile;
    }
  }
  
  /**
   * 模拟文件选择器
   */
  private async simulateFilePicker(): Promise<string> {
    return new Promise((resolve) => {
      // 在实际应用中,这里应该使用官方的文件选择器API
      // 这里简化为返回一个示例路径
      setTimeout(() => {
        resolve(this.context.filesDir + '/selected.pdf');
      }, 500);
    });
  }
  
  /**
   * 查看输出目录
   */
  private viewOutputDirectory(): void {
    if (this.conversionResult?.outputDirectory) {
      // 在实际应用中,这里可以打开文件管理器
      console.info('打开输出目录:', this.conversionResult.outputDirectory);
      
      // 可以使用系统能力打开文件管理器
      // 这里简化为日志输出
    } else {
      console.warn('没有可用的输出目录');
    }
  }
  
  build() {
    Column({ space: 20 }) {
      // 标题
      Text('PDF转图片重命名工具')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 40, bottom: 20 });
      
      // PDF文件选择区域
      Column({ space: 10 }) {
        Text('PDF文件路径:')
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .textAlign(TextAlign.Start)
          .width('100%');
        
        Text(this.pdfFilePath)
          .fontSize(14)
          .fontColor(Color.Gray)
          .maxLines(2)
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .width('100%')
          .padding(10)
          .backgroundColor('#F5F5F5')
          .borderRadius(8);
        
        Button('选择PDF文件')
          .width('60%')
          .height(40)
          .onClick(() => {
            this.selectPdfFile();
          });
      }
      .width('90%')
      .padding(15)
      .backgroundColor(Color.White)
      .borderRadius(12)
      .shadow({ radius: 8, color: '#00000010', offsetX: 0, offsetY: 2 });
      
      // 命名模板选择区域
      Column({ space: 10 }) {
        Text('命名模板:')
          .fontSize(16)
          .fontWeight(FontWeight.Medium)
          .textAlign(TextAlign.Start)
          .width('100%');
        
        ForEach(this.namingTemplates, (template) => {
          Row({ space: 10 }) {
            Radio({ value: template.value, group: 'namingTemplate' })
              .checked(this.selectedNamingTemplate === template.value)
              .onChange((isChecked: boolean) => {
                if (isChecked) {
                  this.selectedNamingTemplate = template.value;
                }
              });
            
            Text(template.label)
              .fontSize(14)
              .fontColor(Color.Black)
              .layoutWeight(1);
          }
          .width('100%')
          .padding({ top: 5, bottom: 5 });
        });
        
        // 自定义模板输入
        if (this.selectedNamingTemplate === '') {
          TextInput({ placeholder: '请输入自定义模板,如:文档_{page}_{timestamp}.png' })
            .width('100%')
            .height(40)
            .onChange((value: string) => {
              this.selectedNamingTemplate = value;
            });
        }
      }
      .width('90%')
      .padding(15)
      .backgroundColor(Color.White)
      .borderRadius(12)
      .shadow({ radius: 8, color: '#00000010', offsetX: 0, offsetY: 2 });
      
      // 转换按钮
      Button(this.isConverting ? '转换中...' : '开始转换')
        .width(200)
        .height(48)
        .backgroundColor(this.isConverting ? '#CCCCCC' : '#007DFF')
        .enabled(!this.isConverting)
        .onClick(() => {
          this.convertPdfToImages();
        })
        .margin({ top: 20 });
      
      // 进度显示
      if (this.isConverting) {
        Column({ space: 10 }) {
          Text(`转换进度:${this.progress}%`)
            .fontSize(14)
            .fontColor(Color.Black);
          
          Progress({ value: this.progress, total: 100 })
            .width('80%')
            .height(8)
            .color('#007DFF');
        }
        .width('100%')
        .margin({ top: 20 });
      }
      
      // 结果显示
      if (this.conversionResult) {
        Column({ space: 10 }) {
          Text(this.conversionResult.success ? '✅ 转换成功' : '❌ 转换失败')
            .fontSize(18)
            .fontWeight(FontWeight.Bold)
            .fontColor(this.conversionResult.success ? Color.Green : Color.Red);
          
          Text(this.conversionResult.message)
            .fontSize(14)
            .fontColor(Color.Gray)
            .maxLines(3)
            .textOverflow({ overflow: TextOverflow.Ellipsis });
          
          if (this.conversionResult.success) {
            Column({ space: 5 }) {
              Text(`总页数:${this.conversionResult.totalPages}`)
                .fontSize(14);
              
              Text(`输出目录:${this.conversionResult.outputDirectory}`)
                .fontSize(14)
                .fontColor(Color.Blue)
                .onClick(() => {
                  this.viewOutputDirectory();
                });
              
              Button('查看文件')
                .width(120)
                .height(36)
                .margin({ top: 10 })
                .onClick(() => {
                  this.viewOutputDirectory();
                });
            }
            .width('100%')
            .padding(10)
            .backgroundColor('#F0F8FF')
            .borderRadius(8);
          }
        }
        .width('90%')
        .padding(15)
        .backgroundColor(Color.White)
        .borderRadius(12)
        .shadow({ radius: 8, color: '#00000010', offsetX: 0, offsetY: 2 })
        .margin({ top: 20 });
      }
      
      // 使用说明
      Column({ space: 10 }) {
        Text('使用说明:')
          .fontSize(16)
          .fontWeight(FontWeight.Medium);
        
        Text('1. 选择或准备PDF文件')
          .fontSize(14);
        Text('2. 选择命名模板或自定义模板')
          .fontSize(14);
        Text('3. 点击"开始转换"按钮')
          .fontSize(14);
        Text('4. 转换完成后可查看输出文件')
          .fontSize(14);
      }
      .width('90%')
      .padding(15)
      .backgroundColor('#FFF8E1')
      .borderRadius(12)
      .margin({ top: 30, bottom: 40 });
    }
    .width('100%')
    .height('100%')
    .alignItems(HorizontalAlign.Center)
    .backgroundColor('#F8F9FA');
  }
}

步骤3:实现批量处理功能

对于需要处理多个PDF文件的场景,我们需要扩展批量处理功能:

/**
 * PDF批量转换管理器
 * 支持多个PDF文件的批量转换和重命名
 */
export class BatchPdfConverter {
  private converter: PdfToImageConverter;
  private context: common.UIAbilityContext;
  
  // 批量任务队列
  private taskQueue: Array<ConversionTask> = [];
  private isProcessing: boolean = false;
  private currentTaskIndex: number = 0;
  
  // 进度回调
  private progressCallback: ((progress: BatchProgress) => void) | null = null;
  private completionCallback: ((results: BatchResult[]) => void) | null = null;
  
  constructor(context: common.UIAbilityContext) {
    this.context = context;
    this.converter = new PdfToImageConverter(context);
  }
  
  /**
   * 添加批量转换任务
   */
  addBatchTasks(tasks: Array<{ pdfPath: string; outputDir?: string; namingTemplate?: string }>): void {
    tasks.forEach((task, index) => {
      this.taskQueue.push({
        id: `task_${Date.now()}_${index}`,
        pdfFilePath: task.pdfPath,
        outputDir: task.outputDir,
        namingTemplate: task.namingTemplate,
        status: TaskStatus.PENDING,
        progress: 0,
        result: null
      });
    });
  }
  
  /**
   * 开始批量转换
   */
  async startBatchConversion(): Promise<BatchResult[]> {
    if (this.isProcessing) {
      throw new BusinessError({
        code: 2001,
        message: '批量转换正在进行中'
      });
    }
    
    this.isProcessing = true;
    this.currentTaskIndex = 0;
    const results: BatchResult[] = [];
    
    try {
      for (let i = 0; i < this.taskQueue.length; i++) {
        const task = this.taskQueue[i];
        this.currentTaskIndex = i;
        
        // 更新任务状态
        task.status = TaskStatus.PROCESSING;
        task.progress = 0;
        this.updateProgress();
        
        try {
          // 执行单个PDF转换
          const result = await this.converter.convertAndRename(
            task.pdfFilePath,
            task.outputDir,
            task.namingTemplate
          );
          
          // 更新任务结果
          task.status = TaskStatus.COMPLETED;
          task.progress = 100;
          task.result = result;
          
          results.push({
            taskId: task.id,
            fileName: this.getFileNameFromPath(task.pdfFilePath),
            success: result.success,
            message: result.message,
            outputDirectory: result.outputDirectory,
            convertedPages: result.convertedPages,
            renamedFiles: result.renamedFiles
          });
          
        } catch (error) {
          // 处理单个任务失败
          task.status = TaskStatus.FAILED;
          task.progress = 0;
          task.result = {
            success: false,
            totalPages: 0,
            convertedPages: 0,
            outputDirectory: '',
            renamedFiles: [],
            message: `任务失败:${error.message}`,
            error: error as BusinessError
          };
          
          results.push({
            taskId: task.id,
            fileName: this.getFileNameFromPath(task.pdfFilePath),
            success: false,
            message: error.message,
            outputDirectory: '',
            convertedPages: 0,
            renamedFiles: []
          });
        }
        
        // 更新总体进度
        this.updateProgress();
      }
      
      return results;
      
    } finally {
      this.isProcessing = false;
      this.taskQueue = [];
      this.currentTaskIndex = 0;
    }
  }
  
  /**
   * 设置进度回调
   */
  setProgressCallback(callback: (progress: BatchProgress) => void): void {
    this.progressCallback = callback;
  }
  
  /**
   * 设置完成回调
   */
  setCompletionCallback(callback: (results: BatchResult[]) => void): void {
    this.completionCallback = callback;
  }
  
  /**
   * 更新进度信息
   */
  private updateProgress(): void {
    if (!this.progressCallback) {
      return;
    }
    
    const totalTasks = this.taskQueue.length;
    const completedTasks = this.taskQueue.filter(t => 
      t.status === TaskStatus.COMPLETED || t.status === TaskStatus.FAILED
    ).length;
    
    const progress: BatchProgress = {
      totalTasks: totalTasks,
      completedTasks: completedTasks,
      currentTask: this.taskQueue[this.currentTaskIndex],
      overallProgress: totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0
    };
    
    this.progressCallback(progress);
  }
  
  /**
   * 从路径中提取文件名
   */
  private getFileNameFromPath(path: string): string {
    const parts = path.split('/');
    return parts[parts.length - 1];
  }
  
  /**
   * 暂停批量转换
   */
  pauseBatchConversion(): void {
    // 由于convertToImage是同步方法,无法直接暂停
    // 可以在任务间添加检查点
    console.warn('当前版本不支持暂停,请等待当前任务完成');
  }
  
  /**
   * 恢复批量转换
   */
  resumeBatchConversion(): void {
    console.info('恢复批量转换');
  }
  
  /**
   * 取消批量转换
   */
  cancelBatchConversion(): void {
    this.taskQueue = [];
    this.isProcessing = false;
    this.currentTaskIndex = 0;
    console.info('批量转换已取消');
  }
  
  /**
   * 获取任务队列状态
   */
  getTaskQueueStatus(): Array<TaskStatusInfo> {
    return this.taskQueue.map(task => ({
      id: task.id,
      fileName: this.getFileNameFromPath(task.pdfFilePath),
      status: task.status,
      progress: task.progress
    }));
  }
}

// 任务状态枚举
enum TaskStatus {
  PENDING = 'pending',
  PROCESSING = 'processing',
  COMPLETED = 'completed',
  FAILED = 'failed'
}

// 转换任务接口
interface ConversionTask {
  id: string;
  pdfFilePath: string;
  outputDir?: string;
  namingTemplate?: string;
  status: TaskStatus;
  progress: number;
  result: ConversionResult | null;
}

// 批量进度接口
interface BatchProgress {
  totalTasks: number;
  completedTasks: number;
  currentTask: ConversionTask | undefined;
  overallProgress: number;
}

// 批量结果接口
interface BatchResult {
  taskId: string;
  fileName: string;
  success: boolean;
  message: string;
  outputDirectory: string;
  convertedPages: number;
  renamedFiles: string[];
}

// 任务状态信息接口
interface TaskStatusInfo {
  id: string;
  fileName: string;
  status: TaskStatus;
  progress: number;
}

注意事项

1. 线程与性能优化

由于convertToImage是同步方法且转换大型PDF文件可能耗时较长,在开发过程中需要注意以下几点:

避免主线程阻塞

// ❌ 错误做法:在主线程执行耗时操作
async convertPdfInMainThread(): Promise<void> {
  const result = await this.converter.convertAndRename(pdfPath); // 可能阻塞UI
}

// ✅ 正确做法:使用任务池在子线程执行
import { taskpool } from '@kit.TaskPoolKit';

async convertPdfInBackground(): Promise<void> {
  // 创建后台任务
  const task: taskpool.Task = new taskpool.Task(
    (pdfPath: string, outputDir: string) => {
      // 在子线程中执行转换
      const converter = new PdfToImageConverter(this.context);
      return converter.convertAndRename(pdfPath, outputDir);
    },
    [pdfPath, outputDir]
  );
  
  // 执行任务
  const result = await taskpool.execute(task);
}

大文件分页处理

对于超大型PDF文件(超过100页),建议分批次处理以避免内存溢出:

class LargePdfProcessor {
  /**
   * 分批处理大型PDF
   */
  async processLargePdfInBatches(
    pdfPath: string,
    batchSize: number = 20
  ): Promise<void> {
    const document = new pdfService.PdfDocument();
    const pageCount = document.getPageCount();
    
    for (let i = 0; i < pageCount; i += batchSize) {
      const endPage = Math.min(i + batchSize, pageCount);
      
      // 分批转换
      await this.convertPageRange(document, i, endPage);
      
      // 每批次完成后清理内存
      await this.cleanupBatchMemory();
      
      // 更新进度
      this.updateProgress(i, pageCount);
    }
  }
}

2. 文件权限管理

在HarmonyOS 6中,文件访问权限管理更加严格:

权限配置

module.json5中配置必要权限:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.READ_MEDIA",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "always"
        }
      },
      {
        "name": "ohos.permission.WRITE_MEDIA", 
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "always"
        }
      },
      {
        "name": "ohos.permission.MEDIA_LOCATION",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

运行时权限检查

import { abilityAccessCtrl, Permissions } from '@kit.AccessControlKit';

class PermissionManager {
  /**
   * 检查并申请文件权限
   */
  static async checkAndRequestFilePermissions(): Promise<boolean> {
    const atManager = abilityAccessCtrl.createAtManager();
    const permissions: Array<Permissions> = [
      'ohos.permission.READ_MEDIA',
      'ohos.permission.WRITE_MEDIA'
    ];
    
    try {
      // 检查权限
      for (const permission of permissions) {
        const grantStatus = await atManager.verifyAccessToken(permission);
        if (grantStatus !== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
          // 请求权限
          const requestResult = await atManager.requestPermissionsFromUser(
            this.context,
            [permission]
          );
          
          if (requestResult.authResults[0] !== 0) {
            console.error(`权限被拒绝: ${permission}`);
            return false;
          }
        }
      }
      return true;
    } catch (error) {
      console.error(`权限检查失败: ${error.message}`);
      return false;
    }
  }
}

3. 错误处理与恢复

完善错误处理机制

class RobustPdfConverter extends PdfToImageConverter {
  private errorRecoveryStrategies = new Map<number, RecoveryStrategy>();
  
  constructor(context: common.UIAbilityContext) {
    super(context);
    this.initErrorRecoveryStrategies();
  }
  
  private initErrorRecoveryStrategies(): void {
    // 内存不足错误
    this.errorRecoveryStrategies.set(13900001, {
      canRetry: true,
      maxRetries: 2,
      recoveryAction: async (error: BusinessError) => {
        console.warn('内存不足,尝试清理缓存');
        await this.cleanupMemoryCache();
        return true;
      }
    });
    
    // 文件系统错误
    this.errorRecoveryStrategies.set(13900002, {
      canRetry: false,
      maxRetries: 0,
      recoveryAction: async (error: BusinessError) => {
        console.error('文件系统错误,需要用户干预');
        await this.showFileSystemErrorDialog(error);
        return false;
      }
    });
    
    // 网络错误(如果从网络加载PDF)
    this.errorRecoveryStrategies.set(13900003, {
      canRetry: true,
      maxRetries: 3,
      recoveryAction: async (error: BusinessError) => {
        console.warn('网络错误,等待重试');
        await this.delay(2000); // 等待2秒
        return true;
      }
    });
  }
  
  /**
   * 带错误恢复的转换
   */
  async convertWithRecovery(
    pdfPath: string,
    maxRetries: number = 3
  ): Promise<ConversionResult> {
    let retryCount = 0;
    
    while (retryCount <= maxRetries) {
      try {
        return await super.convertAndRename(pdfPath);
      } catch (error) {
        retryCount++;
        
        const errorCode = error.code || 0;
        const strategy = this.errorRecoveryStrategies.get(errorCode) || 
                        this.getDefaultRecoveryStrategy();
        
        if (!strategy.canRetry || retryCount > strategy.maxRetries) {
          throw error;
        }
        
        console.warn(`转换失败,尝试恢复 (第${retryCount}次重试)`);
        
        const shouldRetry = await strategy.recoveryAction(error);
        if (!shouldRetry) {
          throw error;
        }
      }
    }
    
    throw new BusinessError({
      code: 5001,
      message: `转换失败,已达到最大重试次数: ${maxRetries}`
    });
  }
}

兼容性处理

1. 版本兼容性

API版本检查

class CompatibilityManager {
  /**
   * 检查PDFKit API可用性
   */
  static isPdfKitAvailable(): boolean {
    try {
      // 尝试创建PDF文档对象
      const document = new pdfService.PdfDocument();
      return document !== undefined;
    } catch (error) {
      console.warn('PDFKit API不可用:', error.message);
      return false;
    }
  }
  
  /**
   * 获取API版本信息
   */
  static getApiVersionInfo(): ApiVersionInfo {
    const systemInfo = system.getSystemInfoSync();
    
    return {
      harmonyVersion: systemInfo.harmonyVersion,
      apiVersion: systemInfo.apiVersion,
      pdfKitSupported: this.isPdfKitAvailable(),
      fileSystemSupported: this.isFileSystemSupported()
    };
  }
  
  /**
   * 根据版本选择实现方案
   */
  static createPdfConverter(context: common.UIAbilityContext): IPdfConverter {
    const versionInfo = this.getApiVersionInfo();
    
    if (versionInfo.apiVersion >= 12) {
      // HarmonyOS 6.0+ 使用完整功能
      return new PdfToImageConverter(context);
    } else if (versionInfo.apiVersion >= 10) {
      // HarmonyOS 5.0+ 使用兼容模式
      return new LegacyPdfConverter(context);
    } else {
      // 更早版本使用备用方案
      return new FallbackPdfConverter(context);
    }
  }
}

2. 设备兼容性

设备能力检测

class DeviceCapabilityChecker {
  /**
   * 检测设备是否支持PDF转换
   */
  static async checkPdfConversionCapability(): Promise<DeviceCapability> {
    const capabilities: DeviceCapability = {
      supportsPdfConversion: false,
      maxRecommendedPages: 50,
      supportedFormats: [],
      memoryAvailable: 0,
      storageAvailable: 0
    };
    
    try {
      // 检查内存
      const memoryInfo = system.getMemoryInfoSync();
      capabilities.memoryAvailable = memoryInfo.avail;
      
      // 检查存储
      const storageInfo = file.getFreeSizeSync(this.context.filesDir);
      capabilities.storageAvailable = storageInfo;
      
      // 检查PDFKit支持
      capabilities.supportsPdfConversion = 
        await this.testPdfConversion();
      
      // 根据设备能力调整推荐值
      if (memoryInfo.avail < 100 * 1024 * 1024) { // 小于100MB
        capabilities.maxRecommendedPages = 20;
      } else if (memoryInfo.avail < 500 * 1024 * 1024) { // 小于500MB
        capabilities.maxRecommendedPages = 100;
      } else {
        capabilities.maxRecommendedPages = 500; // 500MB以上
      }
      
      // 获取支持的格式
      capabilities.supportedFormats = await this.getSupportedFormats();
      
    } catch (error) {
      console.error('设备能力检测失败:', error);
    }
    
    return capabilities;
  }
  
  /**
   * 测试PDF转换功能
   */
  private static async testPdfConversion(): Promise<boolean> {
    try {
      // 创建一个简单的测试PDF
      const testPdf = await this.createTestPdf();
      
      // 尝试转换
      const document = new pdfService.PdfDocument();
      document.loadDocument(testPdf, '');
      document.convertToImage(this.context.cacheDir, pdfService.ImageFormat.PNG);
      
      return true;
    } catch (error) {
      return false;
    }
  }
}

高级功能扩展

1. 智能命名策略

基于PDF元数据的命名

class IntelligentNamingStrategy {
  /**
   * 从PDF提取元数据并生成智能文件名
   */
  static async generateIntelligentName(
    pdfPath: string,
    pageNumber: number
  ): Promise<string> {
    const document = new pdfService.PdfDocument();
    document.loadDocument(pdfPath, '');
    
    // 提取文档元数据
    const metadata = {
      title: document.getTitle() || this.extractFileName(pdfPath),
      author: document.getAuthor() || '未知作者',
      subject: document.getSubject() || '',
      keywords: document.getKeywords() || '',
      creationDate: document.getCreationDate() || new Date()
    };
    
    // 提取页面内容特征(简化版)
    const pageFeatures = await this.extractPageFeatures(document, pageNumber);
    
    // 生成智能文件名
    return this.buildIntelligentFileName(metadata, pageFeatures, pageNumber);
  }
  
  /**
   * 提取页面特征
   */
  private static async extractPageFeatures(
    document: pdfService.PdfDocument,
    pageNumber: number
  ): Promise<PageFeatures> {
    const features: PageFeatures = {
      hasImages: false,
      hasText: false,
      isLandscape: false,
      dominantColor: '#FFFFFF',
      textKeywords: []
    };
    
    try {
      // 获取页面信息
      const page = document.getPage(pageNumber);
      
      // 检查页面方向
      const rect = page.getMediaBox();
      features.isLandscape = rect.width > rect.height;
      
      // 提取文本关键词(简化实现)
      const textContent = await this.extractTextContent(page);
      features.hasText = textContent.length > 0;
      
      if (features.hasText) {
        features.textKeywords = this.extractKeywords(textContent);
      }
      
    } catch (error) {
      console.warn('页面特征提取失败:', error);
    }
    
    return features;
  }
  
  /**
   * 构建智能文件名
   */
  private static buildIntelligentFileName(
    metadata: PdfMetadata,
    features: PageFeatures,
    pageNumber: number
  ): string {
    const parts: string[] = [];
    
    // 添加标题
    if (metadata.title) {
      const cleanTitle = this.cleanFileName(metadata.title);
      parts.push(cleanTitle.substring(0, 50)); // 限制长度
    }
    
    // 添加页码
    parts.push(`P${pageNumber.toString().padStart(3, '0')}`);
    
    // 添加内容特征
    if (features.hasImages && features.hasText) {
      parts.push('图文');
    } else if (features.hasImages) {
      parts.push('图片');
    } else if (features.hasText) {
      parts.push('文本');
    }
    
    // 添加日期
    const dateStr = this.formatDate(metadata.creationDate);
    parts.push(dateStr);
    
    return parts.join('_') + '.png';
  }
}

2. 批量处理优化

并行处理与队列管理

class ParallelPdfProcessor {
  private queue: ConversionTask[] = [];
  private workers: Worker[] = [];
  private maxConcurrent: number = 3;
  private isProcessing: boolean = false;
  
  /**
   * 添加批量任务
   */
  addBatchTasks(tasks: ConversionTask[]): void {
    this.queue.push(...tasks);
    this.processQueue();
  }
  
  /**
   * 处理队列
   */
  private async processQueue(): Promise<void> {
    if (this.isProcessing || this.queue.length === 0) {
      return;
    }
    
    this.isProcessing = true;
    
    while (this.queue.length > 0 && this.workers.length < this.maxConcurrent) {
      const task = this.queue.shift();
      if (!task) continue;
      
      const worker = this.createWorker(task);
      this.workers.push(worker);
      
      worker.onFinish(() => {
        // 移除完成的worker
        const index = this.workers.indexOf(worker);
        if (index > -1) {
          this.workers.splice(index, 1);
        }
        
        // 继续处理队列
        this.processQueue();
      });
      
      worker.start();
    }
    
    this.isProcessing = false;
  }
  
  /**
   * 创建工作线程
   */
  private createWorker(task: ConversionTask): Worker {
    return new PdfConversionWorker(task, this.context);
  }
  
  /**
   * 暂停处理
   */
  pause(): void {
    this.isProcessing = false;
    this.workers.forEach(worker => worker.pause());
  }
  
  /**
   * 恢复处理
   */
  resume(): void {
    this.processQueue();
    this.workers.forEach(worker => worker.resume());
  }
  
  /**
   * 取消所有任务
   */
  cancelAll(): void {
    this.queue = [];
    this.workers.forEach(worker => worker.cancel());
    this.workers = [];
  }
}

测试与调试

1. 单元测试

// PDF转换器单元测试
describe('PdfToImageConverter', () => {
  let converter: PdfToImageConverter;
  let mockContext: any;
  
  beforeEach(() => {
    mockContext = {
      filesDir: '/data/test',
      resourceManager: {
        getRawFileContentSync: jest.fn()
      }
    };
    
    converter = new PdfToImageConverter(mockContext as common.UIAbilityContext);
  });
  
  it('应该正确加载PDF文档', async () => {
    const mockPdfDocument = {
      loadDocument: jest.fn().mockReturnValue(pdfService.ParseResult.PARSE_SUCCESS),
      getPageCount: jest.fn().mockReturnValue(10)
    };
    
    // 模拟PDFKit
    jest.spyOn(pdfService, 'PdfDocument').mockImplementation(() => mockPdfDocument as any);
    
    const result = await converter.loadPdfDocument('/test.pdf');
    
    expect(result).toBe(true);
    expect(mockPdfDocument.loadDocument).toHaveBeenCalledWith('/test.pdf', '');
  });
  
  it('应该处理PDF加载失败', async () => {
    const mockPdfDocument = {
      loadDocument: jest.fn().mockReturnValue(pdfService.ParseResult.PARSE_FAILED)
    };
    
    jest.spyOn(pdfService, 'PdfDocument').mockImplementation(() => mockPdfDocument as any);
    
    const result = await converter.loadPdfDocument('/test.pdf');
    
    expect(result).toBe(false);
  });
  
  it('应该正确重命名图片文件', () => {
    const oldFileName = '1.png';
    const pdfBaseName = 'document';
    const pageNumber = 1;
    
    const newFileName = (converter as any).generateNewFileName(
      oldFileName,
      pdfBaseName,
      pageNumber
    );
    
    expect(newFileName).toBe('document_001.png');
  });
});

2. 性能测试

class PerformanceTester {
  /**
   * 运行性能测试
   */
  static async runPerformanceTests(): Promise<PerformanceReport> {
    const report: PerformanceReport = {
      testCases: [],
      summary: {
        totalTests: 0,
        passedTests: 0,
        failedTests: 0,
        averageTime: 0
      }
    };
    
    // 测试用例
    const testCases: PerformanceTestCase[] = [
      {
        name: '小文件转换 (1-10页)',
        fileSize: '100KB',
        pageCount: 5,
        expectedMaxTime: 5000 // 5秒
      },
      {
        name: '中文件转换 (11-50页)',
        fileSize: '5MB',
        pageCount: 30,
        expectedMaxTime: 30000 // 30秒
      },
      {
        name: '大文件转换 (51-200页)',
        fileSize: '20MB',
        pageCount: 100,
        expectedMaxTime: 120000 // 120秒
      }
    ];
    
    for (const testCase of testCases) {
      const result = await this.runSingleTest(testCase);
      report.testCases.push(result);
      
      if (result.passed) {
        report.summary.passedTests++;
      } else {
        report.summary.failedTests++;
      }
    }
    
    report.summary.totalTests = report.testCases.length;
    report.summary.averageTime = this.calculateAverageTime(report.testCases);
    
    return report;
  }
  
  /**
   * 运行单个性能测试
   */
  private static async runSingleTest(
    testCase: PerformanceTestCase
  ): Promise<PerformanceTestResult> {
    const startTime = Date.now();
    
    try {
      // 创建测试PDF
      const testPdfPath = await this.createTestPdf(testCase.pageCount);
      
      // 执行转换
      const converter = new PdfToImageConverter(this.context);
      const result = await converter.convertAndRename(testPdfPath);
      
      const endTime = Date.now();
      const duration = endTime - startTime;
      
      return {
        name: testCase.name,
        passed: result.success && duration <= testCase.expectedMaxTime,
        duration: duration,
        expectedMaxTime: testCase.expectedMaxTime,
        memoryUsage: process.memoryUsage().heapUsed,
        fileCount: result.renamedFiles.length
      };
      
    } catch (error) {
      return {
        name: testCase.name,
        passed: false,
        duration: 0,
        expectedMaxTime: testCase.expectedMaxTime,
        error: error.message
      };
    }
  }
}

部署与发布

1. 应用配置

HarmonyOS应用配置文件​ (module.json5):

{
  "module": {
    "name": "pdf_converter",
    "type": "entry",
    "description": "$string:module_desc",
    "mainElement": "EntryAbility",
    "deviceTypes": [
      "phone",
      "tablet",
      "2in1"
    ],
    "deliveryWithInstall": true,
    "installationFree": false,
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:icon",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:icon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "entities": [
              "entity.system.home"
            ],
            "actions": [
              "action.system.home"
            ]
          }
        ]
      }
    ],
    "requestPermissions": [
      {
        "name": "ohos.permission.READ_MEDIA",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "always"
        }
      },
      {
        "name": "ohos.permission.WRITE_MEDIA",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "always"
        }
      },
      {
        "name": "ohos.permission.MEDIA_LOCATION",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ],
    "metadata": [
      {
        "name": "pdf_converter_config",
        "value": "",
        "resource": "$profile:pdf_converter_config"
      }
    ]
  }
}

2. 构建配置

HAP构建配置文件​ (build-profile.json5):

{
  "app": {
    "signingConfigs": [],
    "products": [
      {
        "name": "default",
        "signingConfig": "default",
        "compileSdkVersion": 12,
        "compatibleSdkVersion": 9,
        "runtimeOS": "HarmonyOS"
      }
    ]
  },
  "modules": [
    {
      "name": "entry",
      "srcPath": "./entry",
      "targets": [
        {
          "name": "default",
          "applyToProducts": [
            "default"
          ],
          "buildMode": "release",
          "jsHeapSize": "large",
          "disableMultidex": false,
          "dependencies": [
            {
              "packageName": "@kit.PDFKit",
              "bundleName": "com.huawei.pdfkit"
            },
            {
              "packageName": "@ohos.file.fs",
              "bundleName": "com.huawei.fileio"
            }
          ],
          "externalNativeOptions": {
            "path": "./src/main/cpp/CMakeLists.txt",
            "arguments": "",
            "cppFlags": "",
            "abiFilters": [
              "arm64-v8a"
            ]
          },
          "arkOptions": {
            "apiType": "stageMode",
            "compileMode": "esmodule",
            "runtime": "ark"
          }
        }
      ]
    }
  ]
}

总结

关键技术点回顾

  1. PDF转换核心:使用pdfService.PdfDocument.convertToImage()将PDF页面转换为图片

  2. 文件重命名:通过fs.renameSync()实现文件重命名

  3. 批量处理:支持多PDF文件的批量转换和重命名

  4. 错误处理:完善的错误恢复和重试机制

  5. 性能优化:内存管理、线程优化、分批处理

  6. 兼容性:多版本HarmonyOS兼容处理

  7. 用户体验:进度显示、智能命名、权限管理

最佳实践建议

  1. 大文件处理:对于超过100页的PDF,建议分批次转换

  2. 内存管理:及时释放不再使用的资源,避免内存泄漏

  3. 用户体验:提供清晰的进度反馈和错误提示

  4. 权限管理:遵循最小权限原则,及时申请必要权限

  5. 测试覆盖:编写全面的单元测试和性能测试

  6. 错误处理:设计完善的错误恢复策略

  7. 日志记录:详细记录操作日志,便于问题排查

扩展方向

  1. 云服务集成:支持从云端加载PDF文件

  2. OCR集成:集成OCR功能提取PDF中的文字信息

  3. 图片优化:转换后自动优化图片质量和大小

  4. 格式转换:支持更多图片格式输出(WebP、AVIF等)

  5. 批量模板:支持自定义批量转换模板

  6. AI增强:使用AI技术自动生成更有意义的文件名

通过本文详细的实现方案,开发者可以在HarmonyOS 6应用中实现高效、稳定的PDF转图片重命名功能,满足各种业务场景的需求。该方案不仅提供了基础功能,还包含了性能优化、错误处理、兼容性适配等企业级应用所需的完整解决方案。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐