Skip to content

Commit 3113c14

Browse files
committed
Add multi-encode PNG selecting smallest
Also adds a quick scan of the alpha channel to store RGBA as RGB when the alpha channel is unused, reducing file size.
1 parent 01d8c78 commit 3113c14

File tree

1 file changed

+186
-86
lines changed

1 file changed

+186
-86
lines changed

Sources/GateEngine/Resources/Import & Export/Coding/PNGCoder.swift

Lines changed: 186 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
public final class PNGDecoder {
9-
public func decode(_ data: Data) throws -> Image {
9+
public func decode(_ data: Data) throws(GateEngineError) -> Image {
1010
#if canImport(LibSPNG)
1111
try LibSPNG.decode(data: data)
1212
#else
@@ -33,20 +33,28 @@ public final class PNGDecoder {
3333
}
3434
}
3535

36+
/**
37+
Encodes raw pixel data as PNG formatted data.
38+
*/
3639
public final class PNGEncoder {
37-
/// - note: Assumes RGBA8 data
38-
public func encode(_ data: Data, width: Int, height: Int) throws -> Data {
40+
/**
41+
- parameter data: RGBA8 formatted image data
42+
- parameter width: The count of pixel columns for `data`
43+
- parameter height: The count of pixel rows for `data`
44+
- parameter sacrificePerformanceToShrinkData: `true` if extra processingshould be done to produce smaller PNG data.
45+
*/
46+
public func encode(_ data: Data, width: Int, height: Int, sacrificePerformanceToShrinkData: Bool = false) throws(GateEngineError) -> Data {
3947
#if canImport(LibSPNG)
40-
try LibSPNG.encode(data: data, width: width, height: height)
48+
if sacrificePerformanceToShrinkData {
49+
try LibSPNG.encodeSmallest(data: data, width: width, height: height)
50+
}else{
51+
try LibSPNG.encodeRGBA(data: data, width: width, height: height, optimizeAlpha: false)
52+
}
4153
#else
4254
fatalError("PNGEncoder is not supported on this platform.")
4355
#endif
4456
}
4557

46-
public enum EncodingError: Error {
47-
case failedToEncode(_ string: String)
48-
}
49-
5058
public init() {
5159

5260
}
@@ -57,91 +65,183 @@ public final class PNGEncoder {
5765
import LibSPNG
5866

5967
enum LibSPNG {
68+
/// Tries encoding as RGB/RGBA and Indexed and returns the smaller data.
69+
/// A quick and dirty method of creating a smaller PNG by creating multiple PNGs and picking the smallest.
70+
/// This is intended for compiling assets. The minor file size reduction is not worth the energy used, making this inappropriate for runtime.
71+
// TODO: Give users more customizability to allow for more predictability for runtime use.
6072
@inlinable
61-
static func encode(data: Data, width: Int, height: Int) throws -> Data {
62-
return try data.withUnsafeBytes({ (bytes: UnsafeRawBufferPointer) throws -> Data in
63-
/* Create a context */
64-
let ctx: OpaquePointer? = spng_ctx_new(Int32(SPNG_CTX_ENCODER.rawValue))
65-
defer {
66-
/* Free context memory */
67-
spng_ctx_free(ctx)
68-
}
69-
70-
spng_set_option(ctx, SPNG_ENCODE_TO_BUFFER, 1)
71-
72-
var ihdr = spng_ihdr(
73-
width: UInt32(width),
74-
height: UInt32(height),
75-
bit_depth: 8,
76-
color_type: UInt8(SPNG_COLOR_TYPE_TRUECOLOR_ALPHA.rawValue),
77-
compression_method: 0,
78-
filter_method: UInt8(SPNG_FILTER_NONE.rawValue),
79-
interlace_method: UInt8(SPNG_INTERLACE_NONE.rawValue)
80-
)
81-
spng_set_ihdr(ctx, &ihdr)
82-
83-
spng_encode_image(ctx, bytes.baseAddress, data.count, Int32(SPNG_FMT_PNG.rawValue), Int32(SPNG_ENCODE_FINALIZE.rawValue))
84-
85-
var length: Int = 0
86-
var error: Int32 = 0
87-
if let buffer = spng_get_png_buffer(ctx, &length, &error), error == SPNG_OK.rawValue {
88-
let data = Data(bytes: buffer, count: length)
89-
free(buffer)
90-
return data
91-
}
92-
93-
throw PNGEncoder.EncodingError.failedToEncode(String(cString: spng_strerror(error)))
94-
})
73+
static func encodeSmallest(data: Data, width: Int, height: Int) throws(GateEngineError) -> Data {
74+
let indexed = try encodeIndexed(data: data, width: width, height: height)
75+
let rgba: Data = try encodeRGBA(data: data, width: width, height: height, optimizeAlpha: true)
76+
77+
if rgba.count <= indexed.count {
78+
return rgba
79+
}
80+
return indexed
9581
}
82+
83+
/// Makes a PNG with full color data. This creates efficient PNG data representing photos or images with many unique colors.
9684
@inlinable
97-
static func decode(data: Data) throws -> PNGDecoder.Image {
98-
return try data.withUnsafeBytes { data in
99-
/* Create a context */
100-
let ctx: OpaquePointer? = spng_ctx_new(0)
101-
defer {
102-
/* Free context memory */
103-
spng_ctx_free(ctx)
104-
}
105-
106-
/* Set an input buffer */
107-
let set_buffer_err: Int32 = spng_set_png_buffer(ctx, data.baseAddress, data.count)
108-
if set_buffer_err != 0 {
109-
throw GateEngineError.failedToDecode(String(cString: spng_strerror(set_buffer_err)))
110-
}
111-
112-
/* Determine output image size */
113-
var out_size: Int = -1
114-
let out_size_err: Int32 = spng_decoded_image_size(
115-
ctx,
116-
Int32(SPNG_FMT_RGBA8.rawValue),
117-
&out_size
118-
)
119-
if out_size_err != 0 {
120-
throw GateEngineError.failedToDecode(String(cString: spng_strerror(out_size_err)))
121-
}
122-
123-
/* Decode to 8-bit RGBA */
124-
var out: Data = Data(repeatElement(0, count: out_size))
125-
let decode_err: Int32 = out.withUnsafeMutableBytes({ data in
126-
return spng_decode_image(
85+
static func encodeRGBA(data: Data, width: Int, height: Int, optimizeAlpha: Bool) throws(GateEngineError) -> Data {
86+
do {
87+
return try data.withUnsafeBytes({ (bytes: UnsafeRawBufferPointer) throws -> Data in
88+
/* Create a context */
89+
let ctx: OpaquePointer? = spng_ctx_new(Int32(SPNG_CTX_ENCODER.rawValue))
90+
defer {
91+
/* Free context memory */
92+
spng_ctx_free(ctx)
93+
}
94+
95+
spng_set_option(ctx, SPNG_ENCODE_TO_BUFFER, 1)
96+
97+
let colorType: UInt8
98+
if optimizeAlpha {
99+
var hasAlpha: Bool = false
100+
for index in stride(from: 3, to: data.count, by: 4) {
101+
if data[index] < .max {
102+
// If any alpha value is less then 100% we need to store the alpha
103+
hasAlpha = true
104+
break
105+
}
106+
}
107+
if hasAlpha {
108+
colorType = UInt8(SPNG_COLOR_TYPE_TRUECOLOR_ALPHA.rawValue)
109+
}else{
110+
colorType = UInt8(SPNG_COLOR_TYPE_TRUECOLOR.rawValue)
111+
}
112+
}else{
113+
colorType = UInt8(SPNG_COLOR_TYPE_TRUECOLOR_ALPHA.rawValue)
114+
}
115+
116+
var ihdr = spng_ihdr(
117+
width: UInt32(width),
118+
height: UInt32(height),
119+
bit_depth: 8,
120+
color_type: colorType,
121+
compression_method: 0,
122+
filter_method: UInt8(SPNG_FILTER_NONE.rawValue),
123+
interlace_method: UInt8(SPNG_INTERLACE_NONE.rawValue)
124+
)
125+
spng_set_ihdr(ctx, &ihdr)
126+
127+
spng_encode_image(ctx, bytes.baseAddress, data.count, Int32(SPNG_FMT_PNG.rawValue), Int32(SPNG_ENCODE_FINALIZE.rawValue))
128+
129+
var length: Int = 0
130+
var error: Int32 = 0
131+
if let buffer = spng_get_png_buffer(ctx, &length, &error), error == SPNG_OK.rawValue {
132+
let data = Data(bytes: buffer, count: length)
133+
free(buffer)
134+
return data
135+
}
136+
137+
throw GateEngineError.failedToEncode(String(cString: spng_strerror(error)))
138+
})
139+
}catch let error as GateEngineError {
140+
throw error // Typed throws not supported by closures as of Swift 6.2
141+
}catch{
142+
fatalError() // Impossible, see above
143+
}
144+
}
145+
146+
/// Makes a PNG with an color table backend. This creates efficient PNG data representing pixel art or other images with few unique colors.
147+
@inlinable
148+
static func encodeIndexed(data: Data, width: Int, height: Int) throws(GateEngineError) -> Data {
149+
do {
150+
return try data.withUnsafeBytes({ (bytes: UnsafeRawBufferPointer) throws -> Data in
151+
/* Create a context */
152+
let ctx: OpaquePointer? = spng_ctx_new(Int32(SPNG_CTX_ENCODER.rawValue))
153+
defer {
154+
/* Free context memory */
155+
spng_ctx_free(ctx)
156+
}
157+
158+
spng_set_option(ctx, SPNG_ENCODE_TO_BUFFER, 1)
159+
160+
var ihdr = spng_ihdr(
161+
width: UInt32(width),
162+
height: UInt32(height),
163+
bit_depth: 8,
164+
color_type: UInt8(SPNG_COLOR_TYPE_INDEXED.rawValue),
165+
compression_method: 0,
166+
filter_method: UInt8(SPNG_FILTER_NONE.rawValue),
167+
interlace_method: UInt8(SPNG_INTERLACE_NONE.rawValue)
168+
)
169+
spng_set_ihdr(ctx, &ihdr)
170+
171+
spng_encode_image(ctx, bytes.baseAddress, data.count, Int32(SPNG_FMT_PNG.rawValue), Int32(SPNG_ENCODE_FINALIZE.rawValue))
172+
173+
var length: Int = 0
174+
var error: Int32 = 0
175+
if let buffer = spng_get_png_buffer(ctx, &length, &error), error == SPNG_OK.rawValue {
176+
let data = Data(bytes: buffer, count: length)
177+
free(buffer)
178+
return data
179+
}
180+
181+
throw GateEngineError.failedToEncode(String(cString: spng_strerror(error)))
182+
})
183+
}catch let error as GateEngineError {
184+
throw error // Typed throws not supported by closures as of Swift 6.2
185+
}catch{
186+
fatalError() // Impossible, see above
187+
}
188+
}
189+
190+
@inlinable
191+
static func decode(data: Data) throws(GateEngineError) -> PNGDecoder.Image {
192+
do {
193+
return try data.withUnsafeBytes { data in
194+
/* Create a context */
195+
let ctx: OpaquePointer? = spng_ctx_new(0)
196+
defer {
197+
/* Free context memory */
198+
spng_ctx_free(ctx)
199+
}
200+
201+
/* Set an input buffer */
202+
let set_buffer_err: Int32 = spng_set_png_buffer(ctx, data.baseAddress, data.count)
203+
if set_buffer_err != 0 {
204+
throw GateEngineError.failedToDecode(String(cString: spng_strerror(set_buffer_err)))
205+
}
206+
207+
/* Determine output image size */
208+
var out_size: Int = -1
209+
let out_size_err: Int32 = spng_decoded_image_size(
127210
ctx,
128-
data.baseAddress,
129-
out_size,
130211
Int32(SPNG_FMT_RGBA8.rawValue),
131-
0
212+
&out_size
132213
)
133-
})
134-
if decode_err != 0 {
135-
throw GateEngineError.failedToDecode(String(cString: spng_strerror(decode_err)))
136-
}
137-
138-
var header: spng_ihdr = spng_ihdr()
139-
let header_err: Int32 = spng_get_ihdr(ctx, &header)
140-
if header_err != 0 {
141-
throw GateEngineError.failedToDecode(String(cString: spng_strerror(header_err)))
214+
if out_size_err != 0 {
215+
throw GateEngineError.failedToDecode(String(cString: spng_strerror(out_size_err)))
216+
}
217+
218+
/* Decode to 8-bit RGBA */
219+
var out: Data = Data(repeatElement(0, count: out_size))
220+
let decode_err: Int32 = out.withUnsafeMutableBytes({ data in
221+
return spng_decode_image(
222+
ctx,
223+
data.baseAddress,
224+
out_size,
225+
Int32(SPNG_FMT_RGBA8.rawValue),
226+
0
227+
)
228+
})
229+
if decode_err != 0 {
230+
throw GateEngineError.failedToDecode(String(cString: spng_strerror(decode_err)))
231+
}
232+
233+
var header: spng_ihdr = spng_ihdr()
234+
let header_err: Int32 = spng_get_ihdr(ctx, &header)
235+
if header_err != 0 {
236+
throw GateEngineError.failedToDecode(String(cString: spng_strerror(header_err)))
237+
}
238+
239+
return PNGDecoder.Image(width: Int(header.width), height: Int(header.height), data: out)
142240
}
143-
144-
return PNGDecoder.Image(width: Int(header.width), height: Int(header.height), data: out)
241+
}catch let error as GateEngineError {
242+
throw error // Typed throws not supported by closures as of Swift 6.2
243+
}catch{
244+
fatalError() // Impossible, see above
145245
}
146246
}
147247
}

0 commit comments

Comments
 (0)