引言:从AI旅行助手到PDF标注的实际需求

在我们之前实现的AI旅行助手项目中,用户常常遇到这样的场景:生成的旅行攻略以PDF格式分享给朋友后,需要在特定景点、餐厅或注意事项上做标记。用户反馈说,现有的PDF阅读器标注功能不够直观,特别是方框标注的颜色设置总是不生效,标注出来的方框一直是黑色,无法区分不同类型的内容。

比如,用户想用红色方框标出必去景点,用绿色方框标出美食推荐,用黄色方框标出交通提示。但实际使用时,无论设置什么颜色,渲染出来的方框都是黑色,这让标注失去了分类和强调的意义。

一、问题重现:fillColor设置无效的困扰

1.1 问题现象描述

在HarmonyOS 6的PDF标注功能中,当我们使用SquareAnnotationInfo添加方形标注时,会遇到一个令人困惑的问题:无论将fillColor属性设置为什么颜色值,最终渲染效果都是黑色。

// 问题代码示例
let squareAnnotationInfo: pdf.Pdf.SquareAnnotationInfo = {
  rect: { left: 100, top: 100, right: 200, bottom: 200 },
  content: "这是一个重要的备注",
  fillColor: 0xFFFF0000, // 期望是红色,但实际渲染为黑色
  borderWidth: 2,
  borderColor: 0xFF0000FF // 边框颜色
};

问题表现

  1. 在代码中明确设置了fillColor: 0xFFFF0000(ARGB格式,红色)

  2. 程序运行没有报错,标注也能正常添加

  3. 但实际显示时,方框的填充颜色始终是黑色

  4. 边框颜色设置正常,可以正确显示

1.2 问题影响范围

这个问题不仅影响方形标注,还影响其他类型的填充标注:

  • 圆形标注(CircleAnnotationInfo)

  • 多边形标注(PolygonAnnotationInfo)

  • 高亮标注(HighlightAnnotationInfo)

实际上,所有涉及fillColor属性的标注类型都可能受到影响。

二、解决方案一:使用PdfBorder设置填充颜色

2.1 核心思路

既然直接设置fillColor无效,我们可以通过设置border属性来间接实现填充效果。具体来说,就是通过设置PdfBorderfillColor属性来实现填充颜色的控制。

2.2 完整实现代码

import { pdf } from '@kit.PdfKit';
import { BusinessError } from '@kit.BasicServicesKit';

/**
 * PDF标注管理器
 */
class PdfAnnotationManager {
  private pdfDocument: pdf.Pdf.PdfDocument | null = null;
  private currentPageIndex: number = 0;

  /**
   * 加载PDF文档
   */
  async loadPdfDocument(filePath: string): Promise<void> {
    try {
      // 创建Pdf实例
      const pdfController: pdf.Pdf.PdfController = new pdf.Pdf.PdfController();
      
      // 加载PDF文档
      this.pdfDocument = await pdfController.loadDocument(filePath);
      
      console.log('PDF文档加载成功');
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      console.error(`加载PDF失败: code: ${err.code}, message: ${err.message}`);
      throw new Error('PDF加载失败');
    }
  }

  /**
   * 方案一:使用PdfBorder设置填充颜色
   */
  async addSquareAnnotationWithBorder(): Promise<void> {
    if (!this.pdfDocument) {
      throw new Error('请先加载PDF文档');
    }

    try {
      // 获取当前页
      const pdfPage: pdf.Pdf.PdfPage = await this.pdfDocument.getPage(this.currentPageIndex);
      
      // 创建PdfBorder实例
      const pdfBorder: pdf.Pdf.PdfBorder = {
        fillColor: 0xFFFF0000, // 红色填充
        borderColor: 0xFF0000FF, // 蓝色边框
        borderWidth: 2, // 边框宽度2px
        borderStyle: pdf.Pdf.LineDashStyle.SOLID // 实线边框
      };

      // 创建方形标注信息
      const squareAnnotationInfo: pdf.Pdf.SquareAnnotationInfo = {
        rect: {
          left: 100,  // 距离左边100px
          top: 100,   // 距离顶部100px
          right: 300, // 宽度200px
          bottom: 200 // 高度100px
        },
        content: "重要景点标注", // 标注内容
        border: pdfBorder, // 使用border设置填充和边框
        opacity: 0.7 // 设置透明度
      };

      // 添加标注
      const annotation: pdf.Pdf.PdfAnnotation = await pdfPage.addAnnotation(squareAnnotationInfo);
      
      console.log('方形标注添加成功,ID:', annotation.getAnnotationId());
      
      // 保存更改
      await this.savePdfDocument();
      
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      console.error(`添加标注失败: code: ${err.code}, message: ${err.message}`);
      throw new Error('标注添加失败');
    }
  }

  /**
   * 保存PDF文档
   */
  private async savePdfDocument(): Promise<void> {
    if (!this.pdfDocument) {
      return;
    }

    try {
      const filePath = '/data/storage/el2/base/haps/entry/temp/annotated.pdf';
      await this.pdfDocument.writeFile(filePath);
      console.log('PDF保存成功:', filePath);
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      console.error(`保存PDF失败: code: ${err.code}, message: ${err.message}`);
    }
  }
}

2.3 在AI旅行助手中的应用

在AI旅行助手中,我们可以根据不同类型的内容使用不同颜色的标注:

// 旅行攻略标注管理器
class TravelGuideAnnotationManager extends PdfAnnotationManager {
  // 标注颜色定义
  private annotationColors = {
    SCENIC_SPOT: 0xFFFF0000,     // 红色:重要景点
    RESTAURANT: 0xFF00FF00,      // 绿色:餐厅推荐
    TRANSPORTATION: 0xFFFFFF00,  // 黄色:交通提示
    HOTEL: 0xFFFFA500,          // 橙色:酒店信息
    TIP: 0xFF00BFFF             // 蓝色:旅行小贴士
  };

  /**
   * 添加旅行攻略标注
   */
  async addTravelGuideAnnotation(
    type: string,
    rect: pdf.Pdf.Rect,
    content: string
  ): Promise<void> {
    const color = this.annotationColors[type] || 0xFF000000;
    
    const pdfBorder: pdf.Pdf.PdfBorder = {
      fillColor: color,
      borderColor: 0xFF000000, // 黑色边框
      borderWidth: 1,
      borderStyle: pdf.Pdf.LineDashStyle.SOLID
    };

    const squareAnnotationInfo: pdf.Pdf.SquareAnnotationInfo = {
      rect: rect,
      content: content,
      border: pdfBorder,
      opacity: 0.3 // 半透明填充
    };

    // 调用父类方法添加标注
    await this.addAnnotation(squareAnnotationInfo);
  }

  /**
   * 批量添加攻略标注
   */
  async addBatchAnnotations(annotations: Array<{
    type: string;
    rect: pdf.Pdf.Rect;
    content: string;
  }>): Promise<void> {
    for (const annotation of annotations) {
      await this.addTravelGuideAnnotation(
        annotation.type,
        annotation.rect,
        annotation.content
      );
    }
  }
}

三、解决方案二:使用LineAnnotationInfo手动绘制边框

3.1 核心思路

如果第一种方案在某些情况下仍然不生效,我们可以采用更底层的方案:使用LineAnnotationInfo手动绘制方框的四条边。虽然这种方法稍微复杂,但提供了完全的控制权。

3.2 完整实现代码

/**
 * 手动绘制方框标注
 */
class ManualSquareAnnotationManager {
  private pdfDocument: pdf.Pdf.PdfDocument | null = null;
  private currentPageIndex: number = 0;

  /**
   * 手动绘制方框标注
   */
  async drawSquareAnnotationManually(
    rect: pdf.Pdf.Rect,
    content: string,
    fillColor: number,
    borderColor: number
  ): Promise<void> {
    if (!this.pdfDocument) {
      throw new Error('请先加载PDF文档');
    }

    try {
      const pdfPage: pdf.Pdf.PdfPage = await this.pdfDocument.getPage(this.currentPageIndex);
      
      // 1. 绘制填充矩形(通过设置较粗的边框实现)
      await this.drawFilledRectangle(pdfPage, rect, fillColor);
      
      // 2. 绘制四条边形成边框
      await this.drawSquareBorder(pdfPage, rect, borderColor);
      
      // 3. 添加文本标注
      await this.addTextAnnotation(pdfPage, rect, content);
      
      console.log('手动绘制方框标注完成');
      
    } catch (error) {
      const err: BusinessError = error as BusinessError;
      console.error(`绘制标注失败: code: ${err.code}, message: ${err.message}`);
      throw new Error('标注绘制失败');
    }
  }

  /**
   * 绘制填充矩形
   */
  private async drawFilledRectangle(
    pdfPage: pdf.Pdf.PdfPage,
    rect: pdf.Pdf.Rect,
    fillColor: number
  ): Promise<void> {
    // 注意:lineColor使用的是BGR格式,而不是ARGB
    // 0xFF0000 是蓝色,不是红色
    
    const bgrColor = this.argbToBgr(fillColor);
    
    // 上边
    const topLine: pdf.Pdf.LineAnnotationInfo = {
      rect: {
        left: rect.left,
        top: rect.top,
        right: rect.right,
        bottom: rect.top + 20 // 填充高度
      },
      lineColor: bgrColor,
      lineWidth: 20,
      opacity: 0.3
    };
    
    await pdfPage.addAnnotation(topLine);
    
    // 中间填充部分(重复绘制多条线来实现填充效果)
    const fillHeight = rect.bottom - rect.top - 40;
    const lineCount = Math.ceil(fillHeight / 20);
    
    for (let i = 1; i < lineCount; i++) {
      const lineRect = {
        left: rect.left,
        top: rect.top + 20 * i,
        right: rect.right,
        bottom: rect.top + 20 * (i + 1)
      };
      
      const fillLine: pdf.Pdf.LineAnnotationInfo = {
        rect: lineRect,
        lineColor: bgrColor,
        lineWidth: 20,
        opacity: 0.3
      };
      
      await pdfPage.addAnnotation(fillLine);
    }
    
    // 下边
    const bottomLine: pdf.Pdf.LineAnnotationInfo = {
      rect: {
        left: rect.left,
        top: rect.bottom - 20,
        right: rect.right,
        bottom: rect.bottom
      },
      lineColor: bgrColor,
      lineWidth: 20,
      opacity: 0.3
    };
    
    await pdfPage.addAnnotation(bottomLine);
  }

  /**
   * 绘制方框边框
   */
  private async drawSquareBorder(
    pdfPage: pdf.Pdf.PdfPage,
    rect: pdf.Pdf.Rect,
    borderColor: number
  ): Promise<void> {
    const bgrColor = this.argbToBgr(borderColor);
    
    // 绘制四条边
    const borders = [
      // 上边框
      {
        rect: {
          left: rect.left,
          top: rect.top,
          right: rect.right,
          bottom: rect.top + 2
        },
        lineColor: bgrColor,
        lineWidth: 2
      },
      // 下边框
      {
        rect: {
          left: rect.left,
          top: rect.bottom - 2,
          right: rect.right,
          bottom: rect.bottom
        },
        lineColor: bgrColor,
        lineWidth: 2
      },
      // 左边框
      {
        rect: {
          left: rect.left,
          top: rect.top,
          right: rect.left + 2,
          bottom: rect.bottom
        },
        lineColor: bgrColor,
        lineWidth: 2
      },
      // 右边框
      {
        rect: {
          left: rect.right - 2,
          top: rect.top,
          right: rect.right,
          bottom: rect.bottom
        },
        lineColor: bgrColor,
        lineWidth: 2
      }
    ];
    
    for (const border of borders) {
      const lineAnnotation: pdf.Pdf.LineAnnotationInfo = {
        rect: border.rect,
        lineColor: border.lineColor,
        lineWidth: border.lineWidth
      };
      
      await pdfPage.addAnnotation(lineAnnotation);
    }
  }

  /**
   * 添加文本标注
   */
  private async addTextAnnotation(
    pdfPage: pdf.Pdf.PdfPage,
    rect: pdf.Pdf.Rect,
    content: string
  ): Promise<void> {
    const textAnnotationInfo: pdf.Pdf.TextAnnotationInfo = {
      rect: {
        left: rect.left + 5,
        top: rect.top + 5,
        right: rect.right - 5,
        bottom: rect.top + 25
      },
      content: content,
      textColor: 0xFF000000, // 黑色文字
      fontSize: 12
    };
    
    await pdfPage.addAnnotation(textAnnotationInfo);
  }

  /**
   * ARGB转BGR
   * 注意:LineAnnotationInfo的lineColor使用的是BGR格式
   */
  private argbToBgr(argb: number): number {
    // ARGB: AAAAAAAARRRRRRRRGGGGGGGGBBBBBBBB
    // BGR:  BBBBBBBBGGGGGGGGRRRRRRRR
    
    const a = (argb >> 24) & 0xFF;
    const r = (argb >> 16) & 0xFF;
    const g = (argb >> 8) & 0xFF;
    const b = argb & 0xFF;
    
    // 转换为BGR格式,保留Alpha通道
    return (a << 24) | (r << 16) | (g << 8) | b;
  }
}

四、在AI旅行助手中的完整应用

4.1 PDF攻略标注功能实现

@Entry
@Component
struct TravelGuidePdfViewer {
  private pdfManager: TravelGuideAnnotationManager = new TravelGuideAnnotationManager();
  @State isPdfLoaded: boolean = false;
  @State annotations: Array<any> = [];
  @State showColorPicker: boolean = false;
  @State selectedColor: number = 0xFFFF0000; // 默认红色

  /**
   * 加载PDF攻略
   */
  async loadTravelGuidePdf(): Promise<void> {
    try {
      // 从资源文件加载PDF
      const context = getContext(this) as common.UIAbilityContext;
      const filePath = await this.copyPdfFromResource();
      
      await this.pdfManager.loadPdfDocument(filePath);
      this.isPdfLoaded = true;
      
      prompt.showToast({
        message: '旅行攻略PDF加载成功',
        duration: 2000
      });
      
    } catch (error) {
      console.error('加载PDF失败:', error);
      prompt.showToast({
        message: '加载失败,请重试',
        duration: 2000
      });
    }
  }

  /**
   * 从资源文件复制PDF
   */
  private async copyPdfFromResource(): Promise<string> {
    const context = getContext(this) as common.UIAbilityContext;
    const filesDir = context.filesDir;
    const destPath = `${filesDir}/travel_guide.pdf`;
    
    // 检查文件是否已存在
    try {
      await fs.access(destPath);
      return destPath;
    } catch {
      // 文件不存在,从资源复制
    }
    
    // 从rawfile复制
    const resourceManager = context.resourceManager;
    const rawFileDescriptor = await resourceManager.getRawFd('travel_guide.pdf');
    
    const srcPath = `resources/rawfile/travel_guide.pdf`;
    await fs.copyFile(srcPath, destPath);
    
    return destPath;
  }

  /**
   * 添加景点标注
   */
  async addScenicSpotAnnotation(): Promise<void> {
    if (!this.isPdfLoaded) {
      return;
    }
    
    const rect: pdf.Pdf.Rect = {
      left: 100,
      top: 150,
      right: 250,
      bottom: 200
    };
    
    try {
      await this.pdfManager.addTravelGuideAnnotation(
        'SCENIC_SPOT',
        rect,
        '故宫博物院 - 建议游览时间:3-4小时'
      );
      
      this.annotations.push({
        type: 'SCENIC_SPOT',
        rect: rect,
        content: '故宫博物院标注'
      });
      
      prompt.showToast({
        message: '景点标注添加成功',
        duration: 1000
      });
      
    } catch (error) {
      console.error('添加标注失败:', error);
    }
  }

  /**
   * 添加餐厅标注
   */
  async addRestaurantAnnotation(): Promise<void> {
    if (!this.isPdfLoaded) {
      return;
    }
    
    const rect: pdf.Pdf.Rect = {
      left: 100,
      top: 220,
      right: 250,
      bottom: 270
    };
    
    try {
      await this.pdfManager.addTravelGuideAnnotation(
        'RESTAURANT',
        rect,
        '全聚德烤鸭店 - 推荐:烤鸭、鸭汤'
      );
      
      this.annotations.push({
        type: 'RESTAURANT',
        rect: rect,
        content: '餐厅标注'
      });
      
      prompt.showToast({
        message: '餐厅标注添加成功',
        duration: 1000
      });
      
    } catch (error) {
      console.error('添加标注失败:', error);
    }
  }

  build() {
    Column({ space: 20 }) {
      // 标题
      Text('旅行攻略标注工具')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 30, bottom: 20 })
      
      // PDF显示区域
      if (this.isPdfLoaded) {
        Column({ space: 10 }) {
          Text('PDF加载成功')
            .fontSize(16)
            .fontColor('#52c41a')
          
          // 这里应该是PDF预览组件
          // 由于PDF预览需要特定组件,这里用占位图代替
          Image($r('app.media.pdf_preview'))
            .width('90%')
            .height(400)
            .objectFit(ImageFit.Contain)
            .backgroundColor('#f0f0f0')
            .borderRadius(8)
        }
        .width('100%')
        .alignItems(HorizontalAlign.Center)
      } else {
        Column({ space: 10 }) {
          Image($r('app.media.pdf_icon'))
            .width(100)
            .height(100)
            .margin({ bottom: 20 })
          
          Text('点击加载旅行攻略PDF')
            .fontSize(16)
            .fontColor('#666666')
        }
        .width('100%')
        .height(400)
        .justifyContent(FlexAlign.Center)
        .alignItems(HorizontalAlign.Center)
        .onClick(() => this.loadTravelGuidePdf())
      }
      
      // 标注工具区域
      Column({ space: 15 }) {
        Text('标注工具')
          .fontSize(18)
          .fontWeight(FontWeight.Medium)
          .width('100%')
          .textAlign(TextAlign.Start)
          .margin({ left: 20 })
        
        Row({ space: 15 }) {
          // 景点标注按钮
          Button('添加景点标注')
            .onClick(() => this.addScenicSpotAnnotation())
            .backgroundColor('#ff4d4f')
            .fontColor(Color.White)
            .width(120)
            .height(40)
          
          // 餐厅标注按钮
          Button('添加餐厅标注')
            .onClick(() => this.addRestaurantAnnotation())
            .backgroundColor('#52c41a')
            .fontColor(Color.White)
            .width(120)
            .height(40)
          
          // 颜色选择器
          Button('选择颜色')
            .onClick(() => {
              this.showColorPicker = !this.showColorPicker;
            })
            .backgroundColor('#1890ff')
            .fontColor(Color.White)
            .width(100)
            .height(40)
        }
        .width('100%')
        .justifyContent(FlexAlign.Center)
        .padding(10)
        
        // 颜色选择器
        if (this.showColorPicker) {
          Column({ space: 10 }) {
            Text('选择标注颜色')
              .fontSize(14)
              .fontColor('#666666')
            
            Row({ space: 10 }) {
              // 红色
              Circle()
                .width(30)
                .height(30)
                .fill('#ff4d4f')
                .onClick(() => {
                  this.selectedColor = 0xFFFF0000;
                  this.showColorPicker = false;
                })
              
              // 绿色
              Circle()
                .width(30)
                .height(30)
                .fill('#52c41a')
                .onClick(() => {
                  this.selectedColor = 0xFF00FF00;
                  this.showColorPicker = false;
                })
              
              // 黄色
              Circle()
                .width(30)
                .height(30)
                .fill('#fadb14')
                .onClick(() => {
                  this.selectedColor = 0xFFFFFF00;
                  this.showColorPicker = false;
                })
              
              // 蓝色
              Circle()
                .width(30)
                .height(30)
                .fill('#1890ff')
                .onClick(() => {
                  this.selectedColor = 0xFF0000FF;
                  this.showColorPicker = false;
                })
            }
          }
          .width('100%')
          .padding(15)
          .backgroundColor(Color.White)
          .border({ width: 1, color: '#e8e8e8' })
          .borderRadius(8)
          .margin({ top: 10 })
        }
      }
      .width('100%')
      .padding(20)
      .backgroundColor('#fafafa')
      .borderRadius(12)
      .margin({ left: 20, right: 20 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#ffffff')
  }
}

4.2 标注管理功能

/**
 * PDF标注管理器增强版
 */
class EnhancedPdfAnnotationManager extends PdfAnnotationManager {
  private annotations: Map<string, pdf.Pdf.PdfAnnotation> = new Map();
  private annotationStyles: Map<string, AnnotationStyle> = new Map();

  /**
   * 标注样式定义
   */
  private defineAnnotationStyles(): void {
    this.annotationStyles.set('HIGHLIGHT', {
      fillColor: 0x30FFD700, // 黄色高亮,30%透明度
      borderColor: 0xFFFFD700,
      borderWidth: 1,
      borderStyle: pdf.Pdf.LineDashStyle.SOLID
    });
    
    this.annotationStyles.set('COMMENT', {
      fillColor: 0x30009688, // 青色注释
      borderColor: 0xFF009688,
      borderWidth: 1,
      borderStyle: pdf.Pdf.LineDashStyle.DASHED
    });
    
    this.annotationStyles.set('IMPORTANT', {
      fillColor: 0x30FF5252, // 红色重要
      borderColor: 0xFFFF5252,
      borderWidth: 2,
      borderStyle: pdf.Pdf.LineDashStyle.SOLID
    });
  }

  /**
   * 添加智能标注
   */
  async addSmartAnnotation(
    type: string,
    rect: pdf.Pdf.Rect,
    content: string
  ): Promise<string> {
    const style = this.annotationStyles.get(type) || this.annotationStyles.get('COMMENT')!;
    
    const pdfBorder: pdf.Pdf.PdfBorder = {
      fillColor: style.fillColor,
      borderColor: style.borderColor,
      borderWidth: style.borderWidth,
      borderStyle: style.borderStyle
    };

    const squareAnnotationInfo: pdf.Pdf.SquareAnnotationInfo = {
      rect: rect,
      content: content,
      border: pdfBorder,
      opacity: 0.3
    };

    const pdfPage = await this.getCurrentPage();
    const annotation = await pdfPage.addAnnotation(squareAnnotationInfo);
    const annotationId = annotation.getAnnotationId();
    
    this.annotations.set(annotationId, annotation);
    
    return annotationId;
  }

  /**
   * 删除标注
   */
  async removeAnnotation(annotationId: string): Promise<boolean> {
    const annotation = this.annotations.get(annotationId);
    if (!annotation) {
      return false;
    }
    
    try {
      await annotation.delete();
      this.annotations.delete(annotationId);
      return true;
    } catch (error) {
      console.error('删除标注失败:', error);
      return false;
    }
  }

  /**
   * 修改标注样式
   */
  async updateAnnotationStyle(
    annotationId: string,
    newStyle: AnnotationStyle
  ): Promise<boolean> {
    const annotation = this.annotations.get(annotationId);
    if (!annotation) {
      return false;
    }
    
    try {
      // 先删除旧标注
      await annotation.delete();
      
      // 重新添加新样式的标注
      const rect = await annotation.getRect();
      const content = await annotation.getContent();
      
      const pdfBorder: pdf.Pdf.PdfBorder = {
        fillColor: newStyle.fillColor,
        borderColor: newStyle.borderColor,
        borderWidth: newStyle.borderWidth,
        borderStyle: newStyle.borderStyle
      };
      
      const squareAnnotationInfo: pdf.Pdf.SquareAnnotationInfo = {
        rect: rect,
        content: content,
        border: pdfBorder,
        opacity: 0.3
      };
      
      const pdfPage = await this.getCurrentPage();
      const newAnnotation = await pdfPage.addAnnotation(squareAnnotationInfo);
      
      this.annotations.set(annotationId, newAnnotation);
      return true;
      
    } catch (error) {
      console.error('更新标注样式失败:', error);
      return false;
    }
  }

  /**
   * 导出带标注的PDF
   */
  async exportAnnotatedPdf(): Promise<string> {
    if (!this.pdfDocument) {
      throw new Error('PDF文档未加载');
    }
    
    const timestamp = new Date().getTime();
    const exportPath = `/data/storage/el2/base/haps/entry/files/annotated_${timestamp}.pdf`;
    
    try {
      await this.pdfDocument.writeFile(exportPath);
      return exportPath;
    } catch (error) {
      console.error('导出PDF失败:', error);
      throw new Error('导出失败');
    }
  }
}

五、解决方案对比与选择建议

5.1 方案对比

特性

方案一:PdfBorder方案

方案二:手动绘制方案

实现复杂度

简单,直接使用API

复杂,需要计算坐标和手动绘制

性能

优秀,原生API支持

一般,需要多次绘制操作

兼容性

高,官方推荐方案

高,完全可控

颜色准确性

高,直接设置颜色值

注意BGR格式转换

维护成本

扩展性

一般

高,可自定义各种形状

5.2 选择建议

推荐使用方案一(PdfBorder方案)的情况

  1. 对性能要求较高

  2. 需要快速实现基本功能

  3. 项目时间紧张

  4. 标注样式相对固定

推荐使用方案二(手动绘制方案)的情况

  1. 需要特殊形状的标注

  2. 对标注样式有特殊要求

  3. 方案一在某些设备上不兼容

  4. 需要高度自定义的标注效果

5.3 最佳实践建议

  1. 先尝试方案一:在大多数情况下,方案一都能正常工作且效果更好

  2. 做好兼容性测试:在不同设备和系统版本上测试标注效果

  3. 提供用户反馈:当标注不生效时,给用户明确的提示

  4. 实现降级方案:当方案一失败时,自动降级到方案二

  5. 颜色值验证:确保颜色值的格式正确(ARGB或BGR)

六、常见问题与解决方案

6.1 问题:颜色显示不正确

解决方案

// 颜色格式转换工具函数
class ColorUtils {
  /**
   * 验证并修复颜色值
   */
  static normalizeColor(color: number, format: 'ARGB' | 'BGR' = 'ARGB'): number {
    // 确保Alpha通道不为0
    let alpha = (color >> 24) & 0xFF;
    if (alpha === 0) {
      alpha = 0xFF; // 默认不透明
    }
    
    const red = (color >> 16) & 0xFF;
    const green = (color >> 8) & 0xFF;
    const blue = color & 0xFF;
    
    if (format === 'BGR') {
      // 转换为BGR格式
      return (alpha << 24) | (blue << 16) | (green << 8) | red;
    } else {
      // 保持ARGB格式
      return (alpha << 24) | (red << 16) | (green << 8) | blue;
    }
  }
  
  /**
   * 创建带透明度的颜色
   */
  static withAlpha(color: number, alpha: number): number {
    // alpha: 0-255
    const red = (color >> 16) & 0xFF;
    const green = (color >> 8) & 0xFF;
    const blue = color & 0xFF;
    
    return (alpha << 24) | (red << 16) | (green << 8) | blue;
  }
}

6.2 问题:标注位置计算

解决方案

/**
 * 坐标计算工具
 */
class CoordinateUtils {
  /**
   * 根据百分比计算绝对坐标
   */
  static percentToAbsolute(
    percentRect: { left: number; top: number; right: number; bottom: number },
    pageWidth: number,
    pageHeight: number
  ): pdf.Pdf.Rect {
    return {
      left: (percentRect.left / 100) * pageWidth,
      top: (percentRect.top / 100) * pageHeight,
      right: (percentRect.right / 100) * pageWidth,
      bottom: (percentRect.bottom / 100) * pageHeight
    };
  }
  
  /**
   * 确保坐标在页面范围内
   */
  static clampRect(rect: pdf.Pdf.Rect, pageWidth: number, pageHeight: number): pdf.Pdf.Rect {
    return {
      left: Math.max(0, Math.min(rect.left, pageWidth)),
      top: Math.max(0, Math.min(rect.top, pageHeight)),
      right: Math.max(rect.left, Math.min(rect.right, pageWidth)),
      bottom: Math.max(rect.top, Math.min(rect.bottom, pageHeight))
    };
  }
}

七、在AI旅行助手中的完整工作流

7.1 标注工作流程

// 完整的旅行攻略标注工作流
class TravelGuideAnnotationWorkflow {
  private pdfManager: EnhancedPdfAnnotationManager;
  private currentGuide: TravelGuide | null = null;
  
  /**
   * 初始化工作流
   */
  constructor() {
    this.pdfManager = new EnhancedPdfAnnotationManager();
    this.pdfManager.defineAnnotationStyles();
  }
  
  /**
   * 加载旅行攻略并自动添加标注
   */
  async loadAndAnnotateGuide(guideId: string): Promise<void> {
    try {
      // 1. 加载旅行攻略
      this.currentGuide = await this.loadTravelGuide(guideId);
      
      // 2. 生成PDF
      const pdfPath = await this.generatePdfFromGuide(this.currentGuide);
      
      // 3. 加载PDF
      await this.pdfManager.loadPdfDocument(pdfPath);
      
      // 4. 自动添加智能标注
      await this.addAutoAnnotations(this.currentGuide);
      
      // 5. 显示标注工具
      this.showAnnotationTools();
      
    } catch (error) {
      console.error('攻略标注工作流失败:', error);
      throw error;
    }
  }
  
  /**
   * 自动添加智能标注
   */
  private async addAutoAnnotations(guide: TravelGuide): Promise<void> {
    const annotations = this.extractAnnotationsFromGuide(guide);
    
    for (const annotation of annotations) {
      await this.pdfManager.addSmartAnnotation(
        annotation.type,
        annotation.rect,
        annotation.content
      );
    }
  }
  
  /**
   * 从攻略中提取标注信息
   */
  private extractAnnotationsFromGuide(guide: TravelGuide): Array<{
    type: string;
    rect: pdf.Pdf.Rect;
    content: string;
  }> {
    const annotations = [];
    
    // 提取景点信息
    for (const scenicSpot of guide.scenicSpots) {
      annotations.push({
        type: 'SCENIC_SPOT',
        rect: this.calculateRectForContent(scenicSpot.description),
        content: `景点:${scenicSpot.name}\n建议:${scenicSpot.tips}`
      });
    }
    
    // 提取餐厅信息
    for (const restaurant of guide.restaurants) {
      annotations.push({
        type: 'RESTAURANT',
        rect: this.calculateRectForContent(restaurant.description),
        content: `餐厅:${restaurant.name}\n推荐:${restaurant.recommendations.join('、')}`
      });
    }
    
    // 提取重要提示
    for (const tip of guide.importantTips) {
      annotations.push({
        type: 'IMPORTANT',
        rect: this.calculateRectForContent(tip),
        content: `重要提示:${tip}`
      });
    }
    
    return annotations;
  }
}

八、总结

通过本文的详细解析,我们解决了HarmonyOS 6中PDF标注功能的关键问题,并提供了两种可行的解决方案。在AI旅行助手项目中,这些技术可以帮助用户:

  1. 高效标注旅行攻略:快速标记重要景点、餐厅和注意事项

  2. 颜色分类管理:通过不同颜色区分不同类型的标注

  3. 智能标注建议:基于AI分析自动推荐标注位置

  4. 便捷分享:导出带标注的PDF与朋友分享

关键收获

  • PDF标注的fillColor问题可以通过PdfBorder属性解决

  • 对于特殊需求,可以使用LineAnnotationInfo手动绘制

  • 颜色格式转换是关键,特别是BGR与ARGB的区别

  • 在实际应用中要考虑用户体验和性能平衡

最佳实践建议

  1. 始终优先使用方案一,在必要时降级到方案二

  2. 提供颜色选择器,让用户自定义标注颜色

  3. 实现标注的保存、加载和分享功能

  4. 考虑多端适配,确保在手机、平板、PC上都有良好体验

通过这些技术的应用,AI旅行助手的PDF标注功能不仅解决了颜色显示问题,还为用户提供了更加丰富和实用的标注体验,让旅行攻略的阅读和分享变得更加高效和有趣。

Logo

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

更多推荐