///|
pub fn default_ico_sizes() -> Array[Int] {
[16, 24, 32, 48, 64, 128, 256]
}
///|
pub fn default_icns_sizes() -> Array[Int] {
[16, 32, 64, 128, 256, 512, 1024]
}
///|
/// Encode an ICO whose directory entries point to PNG payloads.
pub fn encode_ico(
image : Image,
sizes? : Array[Int] = default_ico_sizes(),
compression_level? : Int = 6,
) -> Bytes raise ImageError {
let variants = encode_png_sizes(image, sizes, compression_level~)
if variants.length() == 0 || variants.length() > 65535 {
raise ImageError(InvalidInput, "ICO requires between one and 65535 images")
}
let out = Buffer()
write_u16_le(out, 0)
write_u16_le(out, 1)
write_u16_le(out, variants.length())
let mut offset = 6 + variants.length() * 16
for variant in variants {
if variant.size > 256 {
raise ImageError(InvalidDimensions, "ICO image size exceeds 256")
}
out.write_byte(
(if variant.size == 256 { 0 } else { variant.size }).to_byte(),
)
out.write_byte(
(if variant.size == 256 { 0 } else { variant.size }).to_byte(),
)
out.write_byte(b'\x00')
out.write_byte(b'\x00')
write_u16_le(out, 1)
write_u16_le(out, 32)
write_u32_le(out, variant.png.length())
write_u32_le(out, offset)
offset = offset + variant.png.length()
}
for variant in variants {
out.write_bytes(variant.png[:])
}
out.to_bytes()
}
///|
fn icns_types(size : Int) -> Array[Bytes] raise ImageError {
match size {
16 => [b"icp4"]
32 => [b"icp5", b"ic11"]
64 => [b"icp6", b"ic12"]
128 => [b"ic07"]
256 => [b"ic08", b"ic13"]
512 => [b"ic09", b"ic14"]
1024 => [b"ic10"]
_ => raise ImageError(InvalidDimensions, "unsupported ICNS PNG size")
}
}
///|
/// Encode a deterministic ICNS containing PNG chunks for standard macOS icon
/// sizes and their Retina chunk counterparts.
pub fn encode_icns(
image : Image,
sizes? : Array[Int] = default_icns_sizes(),
compression_level? : Int = 6,
) -> Bytes raise ImageError {
let variants = encode_png_sizes(image, sizes, compression_level~)
if variants.length() == 0 {
raise ImageError(InvalidInput, "ICNS requires at least one image")
}
let chunks = Buffer()
for variant in variants {
for kind in icns_types(variant.size) {
chunks.write_bytes(kind[:])
write_u32_be(chunks, variant.png.length() + 8)
chunks.write_bytes(variant.png[:])
}
}
let out = Buffer()
out.write_bytes(b"icns")
write_u32_be(out, chunks.length() + 8)
out.write_bytes(chunks.to_bytes()[:])
out.to_bytes()
}
///|
fn base64_digit(index : Int) -> Byte {
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[index]
}
///|
/// Wrap a PNG byte sequence in a portable data-URI SVG image element.
pub fn png_data_uri_svg(
png : Bytes,
width : Int,
height : Int,
) -> String raise ImageError {
if width <= 0 || height <= 0 {
raise ImageError(InvalidDimensions, "SVG dimensions must be positive")
}
let encoded = Buffer()
let mut index = 0
while index < png.length() {
let remaining = png.length() - index
let a = png[index].to_int()
let b = if remaining > 1 { png[index + 1].to_int() } else { 0 }
let c = if remaining > 2 { png[index + 2].to_int() } else { 0 }
encoded.write_byte(base64_digit(a >> 2))
encoded.write_byte(base64_digit(((a & 3) << 4) | (b >> 4)))
encoded.write_byte(
if remaining > 1 {
base64_digit(((b & 15) << 2) | (c >> 6))
} else {
b'='
},
)
encoded.write_byte(if remaining > 2 { base64_digit(c & 63) } else { b'=' })
index = index + 3
}
""
}