66 */
77
88public 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+ */
3639public 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 {
5765import LibSPNG
5866
5967enum 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