d6cfb1f1创建于 4月27日历史提交
/*
 * Copyright (c) 2026 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import libentry from 'libentry.so';
import { DecodeResult } from '../types/ApngTypes';

const apngDrawableModule = libentry.apng_drawable;

/**
 * A class which holds APNG image information and bridge to the native layer.
 * When finished to use this class, you must explicitly call recycle().
 */
export class Apng {
  private id: number;
  public readonly width: number;
  public readonly height: number;
  public readonly frameCount: number;
  public readonly frameDurations: number[];
  public readonly loopCount: number;
  public readonly allFrameByteCount: number;
  private recycled: boolean = false;

  private constructor(
    id: number,
    width: number,
    height: number,
    frameCount: number,
    frameDurations: number[],
    loopCount: number,
    allFrameByteCount: number
  ) {
    this.id = id;
    this.width = width;
    this.height = height;
    this.frameCount = frameCount;
    this.frameDurations = frameDurations;
    this.loopCount = loopCount;
    this.allFrameByteCount = allFrameByteCount;
  }

  /**
   * The duration to animate one loop of APNG animation.
   */
  public get duration(): number {
    return this.frameDurations.reduce((sum, duration) => sum + duration, 0);
  }

  public get isRecycled(): boolean {
    return this.recycled;
  }

  /**
   * Releases resources managed by the native layer.
   */
  public recycle(): void {
    if (!this.recycled) {
      apngDrawableModule.recycle(this.id);
      this.recycled = true;
    }
  }

  /**
   * Draws specified frame to the output buffer.
   * @param frameIndex Frame index to draw (0-based)
   * @param outputBuffer Output ArrayBuffer (must be at least width * height * 4 bytes)
   */
  public drawWithIndex(frameIndex: number, outputBuffer: ArrayBuffer): boolean {
    if (this.recycled) {
      return false;
    }
    if (frameIndex < 0 || frameIndex >= this.frameCount) {
      return false;
    }
    if (outputBuffer.byteLength < this.width * this.height * 4) {
      return false;
    }
    return apngDrawableModule.draw(this.id, frameIndex, outputBuffer);
  }

  /**
   * Creates Apng from ArrayBuffer.
   */
  public static decode(buffer: ArrayBuffer): Apng {
    try {
      const result: DecodeResult = apngDrawableModule.decode(buffer);
      if (result.id < 0) {
        throw new Error(`Decode failed with error code: ${result.id}`);
      }
      return new Apng(
        result.id,
        result.width,
        result.height,
        result.frameCount,
        result.frameDurations,
        result.loopCount,
        result.allFrameByteCount
      );
    } catch (e) {
      throw new Error(`Failed to decode APNG: ${e}`);
    }
  }

  /**
   * Determines whether or not the given data is APNG format.
   */
  public static isApng(buffer: ArrayBuffer): boolean {
    try {
      return apngDrawableModule.isApng(buffer);
    } catch (e) {
      return false;
    }
  }

  /**
   * Creates a copy of this Apng instance.
   */
  public copy(): Apng {
    if (this.recycled) {
      throw new Error('Cannot copy recycled Apng');
    }
    try {
      const result: DecodeResult = apngDrawableModule.copy(this.id);
      if (result.id < 0) {
        throw new Error(`Copy failed with error code: ${result.id}`);
      }
      return new Apng(
        result.id,
        result.width,
        result.height,
        result.frameCount,
        result.frameDurations,
        result.loopCount,
        result.allFrameByteCount
      );
    } catch (e) {
      throw new Error(`Failed to copy APNG: ${e}`);
    }
  }
}