Skip to content

Commit e6d2af0

Browse files
matmenclaude
andcommitted
PNG: default compression level 4
encode(), encodePNG(), encodeAPNG() and apngEncoder() default to zlib level 4 instead of 1. Measured (our encoder, CPU time): graphic 891 -> 795 kB, readme screenshot 191 -> 149 kB, 1080p photo 2183 -> 1973 kB, external 560 -> 523 kB, for about +30% time (photo 149 -> 196 ms). Level 3 compresses worse than 4 at no less time (deflate_fast vs lazy matching), 6 nearly triples the photo's time. sharp's default (level 6, no filter) stays ~10% smaller on the bench graphic (it writes RGBA, which happens to compress better there), but we're 29% smaller on photos at about its speed. Writing opaque images as RGBA when smaller was measured and left out: it mostly helps small synthetic images, costs ~8% always and makes decoding slower. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BsU2GAqbnFSNaYDBGD1Km2
1 parent 8d5436e commit e6d2af0

4 files changed

Lines changed: 20 additions & 14 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
# Changelog
22

3-
## 3.12.3
3+
## 3.13.0
4+
5+
### Changes
6+
7+
- PNG and APNG encoding (`encode()`, `encodePNG()`, `encodeAPNG()`, `apngEncoder()`) default to compression level 4
8+
instead of 1: 7-22% smaller files (graphics 11%, photos 10%, screenshots 22%) for about 30% more time. zlib's lazy
9+
matching starts at level 4, so levels 2-3 save less. Pass `compression: 1` for the previous speed.
410

511
### Performance
612

‎src/animation-encoder.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export interface GIFAnimationEncoderOptions extends AnimationEncoderOptions {
4444

4545
/** Options of {@link Animation.apngEncoder} */
4646
export interface APNGAnimationEncoderOptions extends AnimationEncoderOptions {
47-
/** The zlib compression level (0-9), higher is smaller but slower. Default: `1` */
47+
/** The zlib compression level (0-9), higher is smaller but slower. Default: `4` */
4848
compression?: number;
4949
}
5050

@@ -177,7 +177,7 @@ export class AnimationEncoder {
177177
}
178178

179179
/** @internal */
180-
static __apng__(width: number, height: number, { loopCount = -1, metadata = {}, optimize = true, compression = 1 }: APNGAnimationEncoderOptions): AnimationEncoder {
180+
static __apng__(width: number, height: number, { loopCount = -1, metadata = {}, optimize = true, compression = 4 }: APNGAnimationEncoderOptions): AnimationEncoder {
181181
canvas(width, height, loopCount);
182182
const text = pngText(resolveMetadata(metadata));
183183
// the frames are kept compressed (quickly): the pixel format, and whether opaque animations are stored without

‎src/animation.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export interface GIFEncodeOptions extends AnimationEncodeOptions {
8686

8787
/** Options of {@link Animation.encodeAPNG} */
8888
export interface APNGEncodeOptions extends AnimationEncodeOptions {
89-
/** The zlib compression level (0-9), higher is smaller but slower. Default: `1` */
89+
/** The zlib compression level (0-9), higher is smaller but slower. Default: `4` */
9090
compression?: number;
9191
}
9292

@@ -279,11 +279,11 @@ export class Animation extends Array<Frame> {
279279
* the first frame. Each frame is stored as the area that changed since the frame before, identical frames are
280280
* merged (unless the `optimize` option is false): the file shows exactly the same animation, but doesn't
281281
* necessarily decode to the same frames
282-
* @param compression The zlib compression level (0-9), higher is smaller but slower
282+
* @param compression The zlib compression level (0-9), higher is smaller but slower. Default: `4`
283283
*/
284284
encodeAPNG(compression?: number): Promise<Uint8Array>;
285-
async encodeAPNG(compressionOrOptions: number | APNGEncodeOptions = 1): Promise<Uint8Array> {
286-
const { compression = 1, metadata = {}, optimize = true } = isOptions(compressionOrOptions) ? compressionOrOptions : { compression: compressionOrOptions };
285+
async encodeAPNG(compressionOrOptions: number | APNGEncodeOptions = 4): Promise<Uint8Array> {
286+
const { compression = 4, metadata = {}, optimize = true } = isOptions(compressionOrOptions) ? compressionOrOptions : { compression: compressionOrOptions };
287287
this.__validate__();
288288
if (!this.length) throw new Error('An animation needs at least one frame');
289289

‎src/image.ts‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export interface SharpenOptions {
9797

9898
/** Options of {@link Image.encode} */
9999
export interface PNGEncodeOptions {
100-
/** The compression level (0-9). Default: `1` */
100+
/** The compression level (0-9), higher is smaller but slower. Default: `4` */
101101
compression?: number;
102102
/** Metadata to include, see {@link Metadata} */
103103
metadata?: Metadata;
@@ -2115,23 +2115,23 @@ export class Image {
21152115
encode(options?: PNGEncodeOptions): Promise<Uint8Array>;
21162116
/**
21172117
* Encodes the image into a PNG
2118-
* @param compression The compression level (0-9). Default: `1`
2118+
* @param compression The compression level (0-9). Default: `4`
21192119
* @param metadata Metadata to include, see {@link Metadata}
21202120
* @return The encoded data
21212121
*/
21222122
encode(compression?: number, metadata?: Metadata): Promise<Uint8Array>;
21232123
/**
2124-
* Encodes the image into a PNG with compression level 1
2124+
* Encodes the image into a PNG with compression level 4
21252125
* @param metadata Metadata to include, see {@link Metadata}
21262126
* @return The encoded data
21272127
*/
21282128
encode(metadata?: Metadata): Promise<Uint8Array>;
2129-
async encode(compressionOrOptions: number | PNGEncodeOptions | Metadata = 1, metadataArgument: Metadata = {}): Promise<Uint8Array> {
2130-
let compression = 1, metadata = metadataArgument;
2129+
async encode(compressionOrOptions: number | PNGEncodeOptions | Metadata = 4, metadataArgument: Metadata = {}): Promise<Uint8Array> {
2130+
let compression = 4, metadata = metadataArgument;
21312131
if (!isOptions(compressionOrOptions)) compression = compressionOrOptions;
21322132
// the options and the metadata have no key in common: an object with neither is empty, the same either way
21332133
else if ('compression' in compressionOrOptions || 'metadata' in compressionOrOptions)
2134-
({ compression = 1, metadata = {} } = compressionOrOptions);
2134+
({ compression = 4, metadata = {} } = compressionOrOptions);
21352135
else metadata = compressionOrOptions as Metadata;
21362136

21372137
const level = Math.min(9, Math.max(0, ~~finite(compression, 'compression level')));
@@ -2144,7 +2144,7 @@ export class Image {
21442144
encodePNG(options?: PNGEncodeOptions): Promise<Uint8Array>;
21452145
/**
21462146
* Encodes the image into a PNG, an alias of {@link Image.encode}
2147-
* @param compression The compression level (0-9). Default: `1`
2147+
* @param compression The compression level (0-9). Default: `4`
21482148
* @param metadata Metadata to include, see {@link Metadata}
21492149
*/
21502150
encodePNG(compression?: number, metadata?: Metadata): Promise<Uint8Array>;

0 commit comments

Comments
 (0)