We all know that emojis are usually GIFs. This article will introduce you to Apng, a new upgrade of the ship. Hopefully, this article will give you an idea of Apng and how to manipulate binary data with JS.

The cause of

One day, UI proposed a requirement to adjust the delay of the GIF in the project. The apNG format pictures used in the project have a fixed interval after adjustment, so I thought it would be better to directly modify the APNG data instead of introducing apNG-JS. Moreover, it does not support exporting as pictures, and the existing pictures need to be changed into components. There are some tools available to modify Apng, but they are not very handy (they don’t fit this requirement), or they don’t have the package, so they plan to build one themselves.

Introduction of Apng

PNG, or Portable Network Graphics, is a raster file format that supports lossless data compression and has been developed as an improved GIF (some say PNG is short for “PNG not GIF”).

Our first impression of PNG is generally to support transparency, the impression of GIF is generally gifs (various emojis ~), so why say PNG is an improved GIF?

Here’s a story: GIF stands for Graphics Interchange Format and was originally created to fill a gap in cross-platform image formats, which can be understood as the gap between still images and video. GIF at that time has met the needs, the problem is that GIF compression using LZW algorithm (string table compression algorithm), and the commercial use of this algorithm is required to authorize, in this shadow, the industry soon launched images without LZW algorithm, PNG was born.

In our understanding, the main feature of GIF is to support dynamic images, based on this point, how can PNG replace GIF? This should be considered based on the actual situation at that time, the application of dynamic GIF was relatively few at that time, and based on other reasons, PNG was determined to be a single image format specification, and the support for Multiple images was realized by the subsequent extension, called MULTIPLE-image Network Graphics in full name of MNG. MNG was launched in 2001, but unfortunately was not supported by the major browsers and died out.

In 2004, Mozilla created an internal GIF format called Apng, which itself is made up of frames of PNGS and, combined with excellent compression algorithms, can achieve a better realistic image at the same size as a GIF, demoted to a static PNG image in browsers that don’t support Apng. Here’s how GIF looks compared to Apng.

Apng has been supported by major browsers in recent years. We can try to use Apng in the scene that needs to display dynamic images and has requirements on picture quality. Due to the characteristics of PNG itself, the display effect will be better than GIF.

Apng format

Apng, as an extension of PNG, has the same overall data results as PNG, except that it defines several more data block types. The structure of Apng is shown in the figure below, which is actually composed of multiple PNG images. The animation effect is controlled by the acTL module, and the transition effect between frames is controlled by the fcTL module. The data block structure of the image remains the same as that of PNG. Except for the first frame, the data frame is named fdAT, and the other data blocks are the same as that of PNG.

The composition of each data block is shown below. Except for the file header, all blocks can be parsed according to this rule

Let’s take a quick look at some of the major data blocks.

PNG signature

This section of file header is fixed, called magic number, which is a PNG file header.

IHDR

Header chunk (IHDR) : This chunk contains basic information about image data stored in a PNG file and must be the first chunk in a PNG data stream. Only one header chunk can be stored in a PNG data stream. It stores the following data

  • The length of the width

  • Height height

  • Bit Depth Image depth

  • Color Type Color type

  • Compression method Compression method

  • Filter Method Filter method

  • Interlace Method Non-interlaced scanning method

PNG width and height are defined here.

Color type represents the color type of the image, which can be distinguished as gray level, index color or true color, etc. When the value is 3, it represents index color, and a PLTE data block needs to be provided. PNG can be subdivided into PnG-8, PnG-24, and PnG-32. Png-8 uses index colors, as shown in the following figure:

acTL

Animation Control chunk, used to control the effects of an animation, provides two pieces of data

  • num_framesHow many frames are there
  • num_playHow many times does the animation play? 0 is a loop

fcTL

Frame control chunk is used to control the mixing effect between frames, direct coverage or partial coverage, offset and delay, etc. The data provided includes:

  • Sequence_number frame number

  • Width the width of the

  • Height height

  • X_offset Specifies the X-axis offset of the frame data

  • Y_offset Indicates the Y-axis offset of this frame

  • Delay_num Spacer

  • Delay_den Interval denominator

  • Dispose_op what to do with the preceding buffered output area before displaying the frame.

  • Blend_op Frame render type

    Here we can find a way to optimize the volume of Apng. The data of each frame may not provide a complete picture, but it needs to be combined with the previous frame to render. If the whole picture is only partially changed, in fact, the current frame only provides the image data of the changed part.

Dispose_op specifies the processing to be done before the next frame is displayed.

  • 0: Leave it as it is without any processing
  • 1: Makes all data in the current buffer transparent
  • 2: Restores the buffer to its previous state before rendering the next frame

Blend_op specifies how the contents of the current frame should be processed.

  • 0: Replaces the contents of the current frame with the current buffer
  • 1: Blends the contents of the current frame into the current buffer

fdAT

Frame data chunk. The basic structure of frame data chunk is the same as THAT of IDAT, except that 4 bytes are added before the data part to indicate the frame number.

Binary processing in JS

After the above introduction, we understand the structure of several data blocks added by Apng on PNG. Based on this, we can adjust the performance of Apng by parsing the content of Apng and modifying the data in corresponding positions, and preview the effect after modification.

We use javascript to parse APNG files, first need to understand the js processing binary data several methods:

ArrayBuffer

Represents a binary data, representing a generic, fixed-length buffer of raw binary data that is not editable. You can think of this object as storing a bunch of 01 data.

DataView

Data view is a low-level interface that can read and write multiple numeric types from binary ArrayBuffer objects, regardless of the byte order of different platforms. Popularly understood, the editing of the ArrayBuffer is delegated to the DataView.

Uint8Array

Represents an 8-bit array of unsigned integers initialized to 0 when created. Once created, you can refer to the elements of the array as objects or by using an array subscript index.

Implement editor

We will probably use the above objects for processing this time. First, we will determine whether the file is PNG according to the Magic number, and then we can start traversing the data object. The code is as follows:

const travelsChunk = (
	buffer: ArrayBuffer,
	cb: (chunkBuffer: Uint8Array, chunkOffset: number, fileBuffer: ArrayBuffer) = >void
) = > {
	let byteOffset = 8; // PNG header
	const dataView = new DataView(buffer);
	do {
		const chunkLength = dataView.getUint32(byteOffset);
		const chunkData = new Uint8Array(buffer.slice(byteOffset, byteOffset + 8 + chunkLength + 4));

		cb(chunkData, byteOffset, buffer);
		byteOffset += 4 + 4 + chunkLength + 4;
	} while (byteOffset < buffer.byteLength);
};
Copy the code

The processing done here is mainly based on the structure of the block, the whole binary object is divided into different data blocks for processing, traversal until the end.

By parsing bytes 4 through 8 of each block, we can get the block name. Then, depending on the name of each block, we can process the blocks that need to support editing, and keep the rest. For example, the base class of a block is defined as:

class Chunk<T = object> {
	// Binary data
	public data: Uint8Array;

	// The name of the block
	public get name() {
		return readBinary(this.data.subarray(4.8));
	}

	constructor(data: Uint8Array) {
		this.data = data;
	}

	// Modify the configuration and create a new instance
	public newByConfig(newConfig: Partial<T>) {
		const newInstance = this.createNewInstance();
		Object.keys(newConfig).forEach((name) = > (newInstance[name] = newConfig[name]));
		newInstance.syncData();
		newInstance.data = createBinary(newInstance.data.subarray(4.this.data.byteLength - 4));
		return newInstance as unknown as this;
	}

	// Different operations to update data
	public syncData() {}

	public createNewInstance() {
		return new Chunk(this.data); }}Copy the code

We will modify the content through the unified exposure method, the rest is the user interface work and preview.

Realize the preview

Once the Apng file is parsed, we can render each frame according to the fcTL configuration, depending on the acTL configuration how many times to play. The main processing is the processing defined in dispose_op and BLend_op in fcTL. If we use canvas to render, this part is relatively easy to deal with. We only need to retain or clear the image of the corresponding area according to the corresponding data.

// The previous frame defined the data area to be empty
if (prevFrame && prevFrame.fcTLChunk.disposeOp == 1) {
	this.ctx.clearRect(
		prevFrame.fcTLChunk.xOffset,
		prevFrame.fcTLChunk.yOffset,
		prevFrame.fcTLChunk.width,
		prevFrame.fcTLChunk.height
	);
	// The previous frame defined the data area to be restored to its previous state
} else if (prevFrame && this.prevFrameData && prevFrame.fcTLChunk.disposeOp == 2) {
	this.ctx.putImageData(this.prevFrameData, prevFrame.fcTLChunk.xOffset, prevFrame.fcTLChunk.yOffset);
}

// Save the state before rendering the current frame
if (frame.fcTLChunk.disposeOp == 2) {
	this.prevFrameData = this.ctx.getImageData(fctl.xOffset, fctl.yOffset, fctl.width, fctl.height);
}

// Clear the data area before rendering the current frame
if (frame.fcTLChunk.blendOp == 0) {
	this.ctx.clearRect(fctl.xOffset, fctl.yOffset, fctl.width, fctl.height);
}

this.ctx.drawImage(this.$images[this.index], fctl.xOffset, fctl.yOffset);
Copy the code

Then we just need to play according to the number of times defined in the acTL to preview.

Now we have the ability to edit and preview. For example, in the following example, we can change an APNG image that is played only once to play in a loop:

You can also set the interval between frames smaller and make the baby elephant run faster (note that most browsers set the minimum interval to11ms, if less than this value, the interval will automatically increase) :

All of the above functions can be experienced in the online demo here.

Packaging application

After implementing these functions, as an editor tool, it can be used completely offline, we can package it as an App. As a gadget that doesn’t want to be too big, we have plenty of other options besides electron: Tauri, NeutralinoJS, Chromely, Electrino, Go-Astilectron, Wails, and so on, all with their own merits. From the star count, I chose to use Tauri to package the application.

The tauri front end uses the system’s WebView directly, and the system interaction part is written in Rust, which is certainly not as compatible as Electron, but is more than enough to make a gadget. It is also easy to use. You only need to install Rust and configure the environment. It is also easy to use the API at the system level.

But tauri’s direct use of the system’s Webview has led to a lack of compatibility, and on the MAC it is inevitable that it will face the big brother of Safari. For example, in my gadget, tauri’s support for files is poor. On MAC, input[type=file] does not raise the file selection box. On Linux, input is ok, but file drop does not trigger issues. In MAC, canvas will crash from time to time when using, and feel that this road to achieve cross-terminal application is rather bumpy…… 😂

conclusion

As a substitute for GIF, PNG is not popular now, but with its excellent performance, I believe it will become a mainstream choice in the future. Through this article we can also see the vigorous development of the Web ecology now, now you can relatively convenient to operate files, call system API, develop an application of their own, can do more and more things, I hope that the follow-up can also continue to maintain the momentum of development under 🤯!

The resources

  • apng.onevcat.com/demo/

  • codereview.chromium.org/2814453004

  • Bugzilla.mozilla.org/show_bug.cg…

  • Wiki.mozilla.org/APNG_Specif…

  • Littlesvr. Ca/apng/gif_ap…

  • En.wikipedia.org/wiki/Portab…

  • en.wikipedia.org/wiki/APNG

  • People.apache.org/~jim/NewArc…

  • www.cnblogs.com/ECJTUACM-87…

  • Sking7. Making. IO/articles / 66…

  • User.observersnews.com/main/conten…

  • www.alloyteam.com/2017/03/the…