img

一、案例介绍

本案例将展示如何使用XComponent组件实现图像处理功能,通过创建一个简单的图像编辑器,帮助开发者掌握XComponent在图像处理场景中的应用方法。

二、效果预览

在这个案例中,我们将实现一个基础的图像编辑器,支持亮度调节、对比度调整、滤镜效果等基本图像处理功能。

三、代码实现

1. 基本结构

@Entry
@Component
struct ImageProcessingExample {
  private controller: XComponentController = new XComponentController()
  private context: CanvasRenderingContext2D = null
  private originalImageData: ImageData = null
  @State currentFilter: string = 'none'
  @State adjustments = {
    brightness: 0,
    contrast: 100,
    saturation: 100
  }

  build() {
    Column({ space: 16 }) {
      // 工具栏
      Row({ space: 16 }) {
        Button('加载图片')
          .onClick(() => {
            this.loadImage()
          })

        Button('重置')
          .onClick(() => {
            this.resetAdjustments()
          })
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)

      // 调整控制
      Column({ space: 12 }) {
        // 亮度调节
        Row({ space: 8 }) {
          Text('亮度')
            .width(80)
          Slider({
            value: this.adjustments.brightness,
            min: -100,
            max: 100,
            step: 1
          })
            .onChange((value: number) => {
              this.adjustments.brightness = value
              this.applyAdjustments()
            })
        }

        // 对比度调节
        Row({ space: 8 }) {
          Text('对比度')
            .width(80)
          Slider({
            value: this.adjustments.contrast,
            min: 0,
            max: 200,
            step: 1
          })
            .onChange((value: number) => {
              this.adjustments.contrast = value
              this.applyAdjustments()
            })
        }

        // 饱和度调节
        Row({ space: 8 }) {
          Text('饱和度')
            .width(80)
          Slider({
            value: this.adjustments.saturation,
            min: 0,
            max: 200,
            step: 1
          })
            .onChange((value: number) => {
              this.adjustments.saturation = value
              this.applyAdjustments()
            })
        }
      }
      .width('100%')
      .padding(16)
      .backgroundColor(Color.White)
      .borderRadius(8)

      // 滤镜选择
      Row({ space: 16 }) {
        ForEach(['none', 'grayscale', 'sepia', 'invert'], (filter: string) => {
          Button(this.getFilterName(filter))
            .onClick(() => {
              this.currentFilter = filter
              this.applyFilter()
            })
            .backgroundColor(this.currentFilter === filter ? '#007DFF' : '#DDDDDD')
        })
      }
      .width('100%')
      .justifyContent(FlexAlign.Center)

      // 图像显示区域
      XComponent({
        id: 'image_canvas',
        type: 'surface',
        libraryname: '',
        controller: this.controller
      })
        .onLoad(() => {
          this.initCanvas()
        })
        .width(350)
        .height(350)
        .backgroundColor(Color.White)
        .borderRadius(8)
        .border({
          width: 1,
          color: '#DDDDDD'
        })
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .backgroundColor('#F8F8F8')
  }

  initCanvas() {
    this.context = this.controller.getContext('2d')
    if (this.context) {
      // 绘制初始提示
      this.context.fillStyle = '#666666'
      this.context.font = '16px sans-serif'
      this.context.textAlign = 'center'
      this.context.fillText('点击"加载图片"按钮开始', 175, 175)
    }
  }

  async loadImage() {
    try {
      // 模拟图片加载
      const image = new Image()
      image.src = 'data:image/png;base64,...' // 实际应用中替换为真实图片
      
      image.onload = () => {
        // 清空画布
        this.context.clearRect(0, 0, 350, 350)

        // 绘制图片并保持比例
        const scale = Math.min(350 / image.width, 350 / image.height)
        const x = (350 - image.width * scale) / 2
        const y = (350 - image.height * scale) / 2

        this.context.drawImage(
          image,
          x, y,
          image.width * scale,
          image.height * scale
        )

        // 保存原始图像数据
        this.originalImageData = this.context.getImageData(0, 0, 350, 350)
      }
    } catch (error) {
      console.error('Failed to load image:', error)
      prompt.showToast({
        message: '加载图片失败',
        duration: 2000
      })
    }
  }

  resetAdjustments() {
    this.adjustments.brightness = 0
    this.adjustments.contrast = 100
    this.adjustments.saturation = 100
    this.currentFilter = 'none'

    if (this.originalImageData) {
      this.context.putImageData(this.originalImageData, 0, 0)
    }
  }

  applyAdjustments() {
    if (!this.originalImageData) return

    // 创建临时canvas进行处理
    const tempCanvas = new OffscreenCanvas(350, 350)
    const tempContext = tempCanvas.getContext('2d')
    tempContext.putImageData(this.originalImageData, 0, 0)

    // 获取图像数据
    const imageData = tempContext.getImageData(0, 0, 350, 350)
    const data = imageData.data

    // 应用调整
    for (let i = 0; i < data.length; i += 4) {
      // 转换为HSL
      const [h, s, l] = this.rgbToHsl(data[i], data[i + 1], data[i + 2])

      // 应用亮度调整
      const newL = l * (1 + this.adjustments.brightness / 100)

      // 应用对比度调整
      const contrast = this.adjustments.contrast / 100
      const newS = s * contrast

      // 应用饱和度调整
      const saturation = this.adjustments.saturation / 100
      const finalS = newS * saturation

      // 转换回RGB
      const [r, g, b] = this.hslToRgb(h, Math.min(1, Math.max(0, finalS)), Math.min(1, Math.max(0, newL)))

      data[i] = r
      data[i + 1] = g
      data[i + 2] = b
    }

    // 更新画布
    this.context.putImageData(imageData, 0, 0)

    // 应用当前滤镜
    if (this.currentFilter !== 'none') {
      this.applyFilter()
    }
  }

  applyFilter() {
    if (!this.originalImageData) return

    const imageData = this.context.getImageData(0, 0, 350, 350)
    const data = imageData.data

    switch (this.currentFilter) {
      case 'grayscale':
        for (let i = 0; i < data.length; i += 4) {
          const avg = (data[i] + data[i + 1] + data[i + 2]) / 3
          data[i] = avg
          data[i + 1] = avg
          data[i + 2] = avg
        }
        break

      case 'sepia':
        for (let i = 0; i < data.length; i += 4) {
          const r = data[i]
          const g = data[i + 1]
          const b = data[i + 2]
          data[i] = Math.min(255, (r * 0.393) + (g * 0.769) + (b * 0.189))
          data[i + 1] = Math.min(255, (r * 0.349) + (g * 0.686) + (b * 0.168))
          data[i + 2] = Math.min(255, (r * 0.272) + (g * 0.534) + (b * 0.131))
        }
        break

      case 'invert':
        for (let i = 0; i < data.length; i += 4) {
          data[i] = 255 - data[i]
          data[i + 1] = 255 - data[i + 1]
          data[i + 2] = 255 - data[i + 2]
        }
        break
    }

    this.context.putImageData(imageData, 0, 0)
  }

  getFilterName(filter: string): string {
    switch (filter) {
      case 'none': return '原图'
      case 'grayscale': return '灰度'
      case 'sepia': return '复古'
      case 'invert': return '反色'
      default: return filter
    }
  }

  // RGB转HSL
  rgbToHsl(r: number, g: number, b: number): [number, number, number] {
    r /= 255
    g /= 255
    b /= 255

    const max = Math.max(r, g, b)
    const min = Math.min(r, g, b)
    let h = 0
    let s = 0
    const l = (max + min) / 2

    if (max !== min) {
      const d = max - min
      s = l > 0.5 ? d / (2 - max - min) : d / (max + min)

      switch (max) {
        case r:
          h = (g - b) / d + (g < b ? 6 : 0)
          break
        case g:
          h = (b - r) / d + 2
          break
        case b:
          h = (r - g) / d + 4
          break
      }

      h /= 6
    }

    return [h, s, l]
  }

  // HSL转RGB
  hslToRgb(h: number, s: number, l: number): [number, number, number] {
    let r = 0
    let g = 0
    let b = 0

    if (s === 0) {
      r = g = b = l
    } else {
      const hue2rgb = (p: number, q: number, t: number) => {
        if (t < 0) t += 1
        if (t > 1) t -= 1
        if (t < 1/6) return p + (q - p) * 6 * t
        if (t < 1/2) return q
        if (t < 2/3) return p + (q - p) * (2/3 - t) * 6
        return p
      }

      const q = l < 0.5 ? l * (1 + s) : l + s - l * s
      const p = 2 * l - q

      r = hue2rgb(p, q, h + 1/3)
      g = hue2rgb(p, q, h)
      b = hue2rgb(p, q, h - 1/3)
    }

    return [
      Math.round(r * 255),
      Math.round(g * 255),
      Math.round(b * 255)
    ]
  }
}

四、实现要点

1. 图像加载和显示

  • 加载和绘制图像
  • 保持图像比例
  • 处理图像数据

2. 图像调整

  • 亮度调节实现
  • 对比度调整
  • 饱和度控制

3. 滤镜效果

  • 灰度转换
  • 复古效果
  • 反色处理

4. 颜色空间转换

  • RGB与HSL转换
  • 颜色调整算法
  • 图像数据处理

五、扩展优化

  1. 图像裁剪
class CropTool {
  private startX: number = 0
  private startY: number = 0
  private width: number = 0
  private height: number = 0
  private isDragging: boolean = false

  startCrop(x: number, y: number) {
    this.startX = x
    this.startY = y
    this.isDragging = true
  }

  updateCrop(x: number, y: number) {
    if (!this.isDragging) return

    this.width = x - this.startX
    this.height = y - this.startY
  }

  finishCrop(context: CanvasRenderingContext2D) {
    if (!this.isDragging) return

    const imageData = context.getImageData(
      this.startX,
      this.startY,
      this.width,
      this.height
    )

    // 清空画布
    context.clearRect(0, 0, context.canvas.width, context.canvas.height)

    // 绘制裁剪后的图像
    context.putImageData(imageData, 0, 0)

    this.isDragging = false
  }
}
  1. 图像旋转
rotateImage(degrees: number) {
  if (!this.originalImageData) return

  const tempCanvas = new OffscreenCanvas(350, 350)
  const tempContext = tempCanvas.getContext('2d')

  // 保存当前状态
  tempContext.save()

  // 移动到中心点
  tempContext.translate(175, 175)

  // 旋转
  tempContext.rotate(degrees * Math.PI / 180)

  // 绘制图像
  tempContext.drawImage(
    this.context.canvas,
    -175, -175
  )

  // 恢复状态
  tempContext.restore()

  // 更新画布
  this.context.clearRect(0, 0, 350, 350)
  this.context.drawImage(tempCanvas, 0, 0)
}

六、总结

通过本案例,掌握XComponent开发图像处理应用的方法,提升开发能力。

Logo

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

更多推荐