/*
* 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}`);
}
}
}