Merge pull request #433 from guzba/master

update zippy dep, -d:lto + gcc >= 11 fix, decode image dimensions, small things
This commit is contained in:
Andre von Houck 2022-06-09 09:22:05 +03:00 committed by GitHub
commit 4a7b5a1e74
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 277 additions and 48 deletions

View file

@ -93,6 +93,9 @@ exportObject ColorStop:
exportObject TextMetrics:
discard
exportObject ImageDimensions:
discard
exportSeq seq[float32]:
discard
@ -310,7 +313,10 @@ exportRefObject Context:
isPointInStroke(Context, Path, float32, float32)
exportProcs:
decodeImage
decodeImageDimensions
readImage
readImageDimensions
readmask
readTypeface
readFont

View file

@ -8,7 +8,7 @@ srcDir = "src"
requires "nim >= 1.4.8"
requires "vmath >= 1.1.4"
requires "chroma >= 0.2.5"
requires "zippy >= 0.9.9"
requires "zippy >= 0.9.11"
requires "flatty >= 0.3.3"
requires "nimsimd >= 1.0.0"
requires "bumpy >= 1.1.1"

View file

@ -18,6 +18,25 @@ converter autoPremultipliedAlpha*(c: ColorRGBA): ColorRGBX {.inline, raises: [].
## Convert a straight alpha RGBA to a premultiplied alpha RGBA.
c.rgbx()
proc decodeImageDimensions*(
data: string
): ImageDimensions {.raises: [PixieError].} =
## Decodes an image's dimensions from memory.
if data.len > 8 and data.readUint64(0) == cast[uint64](pngSignature):
decodePngDimensions(data)
elif data.len > 2 and data.readUint16(0) == cast[uint16](jpegStartOfImage):
decodeJpegDimensions(data)
elif data.len > 2 and data.readStr(0, 2) == bmpSignature:
decodeBmpDimensions(data)
elif data.len > 6 and data.readStr(0, 6) in gifSignatures:
decodeGifDimensions(data)
elif data.len > (14+8) and data.readStr(0, 4) == qoiSignature:
decodeQoiDimensions(data)
elif data.len > 9 and data.readStr(0, 2) in ppmSignatures:
decodePpmDimensions(data)
else:
raise newException(PixieError, "Unsupported image file format")
proc decodeImage*(data: string): Image {.raises: [PixieError].} =
## Loads an image from memory.
if data.len > 8 and data.readUint64(0) == cast[uint64](pngSignature):
@ -45,6 +64,15 @@ proc decodeMask*(data: string): Mask {.raises: [PixieError].} =
else:
raise newException(PixieError, "Unsupported mask file format")
proc readImageDimensions*(
filePath: string
): ImageDimensions {.inline, raises: [PixieError].} =
## Loads an image from a file.
try:
decodeImageDimensions(readFile(filePath))
except IOError as e:
raise newException(PixieError, e.msg, e)
proc readImage*(filePath: string): Image {.inline, raises: [PixieError].} =
## Loads an image from a file.
try:

View file

@ -28,6 +28,9 @@ type
SubtractMaskBlend ## Inverse mask
ExcludeMaskBlend
ImageDimensions* = object
width*, height*: int
proc mix*(a, b: uint8, t: float32): uint8 {.inline, raises: [].} =
## Linearly interpolate between a and b using t.
let t = round(t * 255).uint32

View file

@ -227,6 +227,21 @@ proc decodeBmp*(data: string): Image {.raises: [PixieError].} =
decodeDib(data[14].unsafeAddr, data.len - 14)
proc decodeBmpDimensions*(
data: string
): ImageDimensions {.raises: [PixieError].} =
## Decodes the BMP dimensions.
if data.len < 26:
failInvalid()
# BMP Header
if data[0 .. 1] != "BM":
failInvalid()
result.width = data.readInt32(18).int
result.height = abs(data.readInt32(22)).int
proc encodeBmp*(image: Image): string {.raises: [].} =
## Encodes an image into the BMP file format.

View file

@ -178,5 +178,18 @@ proc decodeGif*(data: string): Image {.raises: [PixieError].} =
else:
raise newException(PixieError, "Invalid GIF block type")
proc decodeGifDimensions*(
data: string
): ImageDimensions {.raises: [PixieError].} =
## Decodes the GIF dimensions.
if data.len < 10:
failInvalid()
if data[0 .. 5] notin gifSignatures:
raise newException(PixieError, "Invalid GIF file signature")
result.width = data.readInt16(6).int
result.height = data.readInt16(8).int
when defined(release):
{.pop.}

View file

@ -68,6 +68,7 @@ type
len, pos: int
bitsBuffered: int
bitBuffer: uint32
foundSOF: bool
imageHeight, imageWidth: int
progressive: bool
quantizationTables: array[4, array[64, uint8]]
@ -243,6 +244,10 @@ proc decodeDHT(state: var DecoderState) =
proc decodeSOF0(state: var DecoderState) =
## Decode start of Frame
if state.foundSOF:
failInvalid()
state.foundSOF = true
var len = state.readUint16be().int - 2
let precision = state.readUint8()
@ -1056,7 +1061,7 @@ proc decodeJpeg*(data: string): Image {.raises: [PixieError].} =
state.decodeDHT()
of 0xD8:
# SOI - Start of Image
continue
discard
of 0xD9:
# EOI - End of Image
break
@ -1094,5 +1099,63 @@ proc decodeJpeg*(data: string): Image {.raises: [PixieError].} =
state.buildImage()
proc decodeJpegDimensions*(
data: string
): ImageDimensions {.raises: [PixieError].} =
## Decodes the JPEG dimensions.
var state = DecoderState()
state.buffer = cast[ptr UncheckedArray[uint8]](data.cstring)
state.len = data.len
while true:
if state.readUint8() != 0xFF:
failInvalid("invalid chunk marker")
let chunkId = state.readUint8()
case chunkId:
of 0xD8:
# SOI - Start of Image
discard
of 0xC0:
# Start Of Frame (Baseline DCT)
state.decodeSOF0()
break
of 0xC1:
# Start Of Frame (Extended sequential DCT)
state.decodeSOF1()
break
of 0xC2:
# Start Of Frame (Progressive DCT)
state.decodeSOF2()
break
of 0xDB:
# Define Quantization Table(s)
state.skipChunk()
of 0XE0:
# Application-specific
state.skipChunk()
of 0xE1:
# Exif/APP1
state.decodeExif()
of 0xE2..0xEF:
# Application-specific
state.skipChunk()
of 0xFE:
# Comment
state.skipChunk()
else:
failInvalid("invalid chunk " & chunkId.toHex())
case state.orientation:
of 0, 1, 2, 3, 4:
result.width = state.imageWidth
result.height = state.imageHeight
of 5, 6, 7, 8:
result.width = state.imageHeight
result.height = state.imageWidth
else:
failInvalid("invalid orientation")
when defined(release):
{.pop.}

View file

@ -90,7 +90,7 @@ proc decodePalette(data: string): seq[ColorRGB] =
result.setLen(data.len div 3)
for i in 0 ..< data.len div 3:
result[i] = cast[ptr ColorRGB](data[i * 3].unsafeAddr)[]
copyMem(result[i].addr, data[i * 3].unsafeAddr, 3)
proc unfilter(
uncompressed: string, height, rowBytes, bpp: int
@ -262,21 +262,20 @@ proc decodeImageData(
# While we can read an extra byte safely, do so. Much faster.
for i in 0 ..< header.height * header.width - 1:
var rgba = cast[ptr ColorRGBA](unfiltered[i * 3].unsafeAddr)[]
rgba.a = 255
if rgba == special:
rgba.a = 0
result[i] = rgba
copyMem(result[i].addr, unfiltered[i * 3].unsafeAddr, 4)
result[i].a = 255
if result[i] == special:
result[i].a = 0
else:
# While we can read an extra byte safely, do so. Much faster.
# var rgba: ColorRGBA
for i in 0 ..< header.height * header.width - 1:
var rgba = cast[ptr ColorRGBA](unfiltered[i * 3].unsafeAddr)[]
rgba.a = 255
result[i] = rgba
copyMem(result[i].addr, unfiltered[i * 3].unsafeAddr, 4)
result[i].a = 255
let
lastOffset = header.height * header.width - 1
rgb = cast[ptr array[3, uint8]](unfiltered[lastOffset * 3].unsafeAddr)[]
let lastOffset = header.height * header.width - 1
var rgb: array[3, uint8]
copyMem(rgb.addr, unfiltered[lastOffset * 3].unsafeAddr, 3)
var rgba = ColorRGBA(r: rgb[0], g: rgb[1], b: rgb[2], a: 255)
if rgba == special:
rgba.a = 0
@ -342,6 +341,26 @@ proc newImage*(png: Png): Image {.raises: [PixieError].} =
copyMem(result.data[0].addr, png.data[0].addr, png.data.len * 4)
result.data.toPremultipliedAlpha()
proc decodePngDimensions*(
data: string
): ImageDimensions {.raises: [PixieError].} =
## Decodes the PNG dimensions.
if data.len < (8 + (8 + 13 + 4) + 4): # Magic bytes + IHDR + IEND
failInvalid()
# PNG file signature
let signature = cast[array[8, uint8]](data.readUint64(0))
if signature != pngSignature:
failInvalid()
# First chunk must be IHDR
if data.readUint32(8).swap() != 13 or data.readStr(12, 4) != "IHDR":
failInvalid()
let header = decodeHeader(data[16 ..< 16 + 13])
result.width = header.width
result.height = header.height
proc decodePng*(data: string): Png {.raises: [PixieError].} =
## Decodes the PNG data.
if data.len < (8 + (8 + 13 + 4) + 4): # Magic bytes + IHDR + IEND

View file

@ -135,6 +135,14 @@ proc decodePpm*(data: string): Image {.raises: [PixieError].} =
else:
decodeP6Data(data[header.dataOffset .. ^1], header.maxVal)
proc decodePpmDimensions*(
data: string
): ImageDimensions {.raises: [PixieError].} =
## Decodes the PPM dimensions.
let header = decodeHeader(data)
result.width = header.width
result.height = header.height
proc encodePpm*(image: Image): string {.raises: [].} =
## Encodes an image into the PPM file format (version P6).

View file

@ -121,6 +121,16 @@ proc decodeQoi*(data: string): Qoi {.raises: [PixieError].} =
raise newException(PixieError, "Invalid QOI padding")
inc(p)
proc decodeQoiDimensions*(
data: string
): ImageDimensions {.raises: [PixieError].} =
## Decodes the QOI dimensions.
if data.len <= 12 or data[0 .. 3] != qoiSignature:
raise newException(PixieError, "Invalid QOI header")
result.width = data.readUint32(4).swap().int
result.height = data.readUint32(8).swap().int
proc encodeQoi*(qoi: Qoi): string {.raises: [PixieError].} =
## Encodes raw QOI pixels to the QOI file format.

View file

@ -304,14 +304,13 @@ proc minifyBy2*(image: Image, power = 1): Image {.raises: [PixieError].} =
b = src.unsafe[x * 2 + 1, y * 2 + 0]
c = src.unsafe[x * 2 + 1, y * 2 + 1]
d = src.unsafe[x * 2 + 0, y * 2 + 1]
rgba = rgbx(
mixed = rgbx(
((a.r.uint32 + b.r + c.r + d.r) div 4).uint8,
((a.g.uint32 + b.g + c.g + d.g) div 4).uint8,
((a.b.uint32 + b.b + c.b + d.b) div 4).uint8,
((a.a.uint32 + b.a + c.a + d.a) div 4).uint8
)
result.unsafe[x, y] = rgba
result.unsafe[x, y] = mixed
if srcWidthIsOdd:
let rgbx = mix(
@ -382,6 +381,8 @@ proc magnifyBy2*(image: Image, power = 1): Image {.raises: [PixieError].} =
proc applyOpacity*(target: Image | Mask, opacity: float32) {.raises: [].} =
## Multiplies alpha of the image by opacity.
let opacity = round(255 * opacity).uint16
if opacity == 255:
return
if opacity == 0:
when type(target) is Image:
@ -434,12 +435,12 @@ proc applyOpacity*(target: Image | Mask, opacity: float32) {.raises: [].} =
when type(target) is Image:
for j in i div 4 ..< target.data.len:
var rgba = target.data[j]
rgba.r = ((rgba.r * opacity) div 255).uint8
rgba.g = ((rgba.g * opacity) div 255).uint8
rgba.b = ((rgba.b * opacity) div 255).uint8
rgba.a = ((rgba.a * opacity) div 255).uint8
target.data[j] = rgba
var rgbx = target.data[j]
rgbx.r = ((rgbx.r * opacity) div 255).uint8
rgbx.g = ((rgbx.g * opacity) div 255).uint8
rgbx.b = ((rgbx.b * opacity) div 255).uint8
rgbx.a = ((rgbx.a * opacity) div 255).uint8
target.data[j] = rgbx
else:
for j in i ..< target.data.len:
target.data[j] = ((target.data[j] * opacity) div 255).uint8

View file

@ -71,11 +71,11 @@ proc fillUnsafe*(
else:
when sizeof(int) == 8:
# Fill 8 bytes at a time when possible
let
var
u32 = cast[uint32](rgbx)
u64 = cast[uint64]([u32, u32])
for _ in 0 ..< len div 2:
cast[ptr uint64](data[i].addr)[] = u64
copyMem(data[i].addr, u64.addr, 8)
i += 2
# Fill whatever is left the slow way
for j in i ..< start + len:

View file

@ -2,5 +2,5 @@ import benchy, pixie/fileformats/gif
let data = readFile("tests/fileformats/gif/audrey.gif")
timeIt "pixie decode":
keep decodeGif(data)
timeIt "gif decode":
discard decodeGif(data)

View file

@ -1,4 +1,4 @@
import chroma, os, pixie, pixie/fileformats/bmp, strutils
import os, pixie/fileformats/bmp
# block:
# var image = newImage(4, 2)
@ -32,9 +32,9 @@ import chroma, os, pixie, pixie/fileformats/bmp, strutils
block:
for bits in [32, 24]:
let image = decodeBmp(readFile(
"tests/fileformats/bmp/knight." & $bits & ".master.bmp"
))
let
path = "tests/fileformats/bmp/knight." & $bits & ".master.bmp"
image = decodeBmp(readFile(path))
writeFile("tests/fileformats/bmp/knight." & $bits & ".bmp", encodeBmp(image))
block:
@ -46,5 +46,9 @@ block:
block:
for file in walkFiles("tests/fileformats/bmp/bmpsuite/*"):
# echo file
let image = decodeBmp(readFile(file))
let
image = decodeBmp(readFile(file))
dimensions = decodeBmpDimensions(readFile(file))
#image.writeFile(file.replace("bmpsuite", "output") & ".png")
doAssert image.width == dimensions.width
doAssert image.height == dimensions.height

View file

@ -1,13 +1,37 @@
import pixie, pixie/fileformats/gif
var img = decodeGIF(readFile("tests/fileformats/gif/3x5.gif"))
img.writeFile("tests/fileformats/gif/3x5.png")
block:
let
path = "tests/fileformats/gif/3x5.gif"
image = decodeGIF(readFile(path))
dimensions = decodeGifDimensions(readFile(path))
image.writeFile("tests/fileformats/gif/3x5.png")
doAssert image.width == dimensions.width
doAssert image.height == dimensions.height
var img2 = decodeGIF(readFile("tests/fileformats/gif/audrey.gif"))
img2.writeFile("tests/fileformats/gif/audrey.png")
block:
let
path = "tests/fileformats/gif/audrey.gif"
image = decodeGIF(readFile(path))
dimensions = decodeGifDimensions(readFile(path))
image.writeFile("tests/fileformats/gif/audrey.png")
doAssert image.width == dimensions.width
doAssert image.height == dimensions.height
var img3 = decodeGIF(readFile("tests/fileformats/gif/sunflower.gif"))
img3.writeFile("tests/fileformats/gif/sunflower.png")
block:
let
path = "tests/fileformats/gif/sunflower.gif"
image = decodeGIF(readFile(path))
dimensions = decodeGifDimensions(readFile(path))
image.writeFile("tests/fileformats/gif/sunflower.png")
doAssert image.width == dimensions.width
doAssert image.height == dimensions.height
var img4 = readImage("tests/fileformats/gif/sunflower.gif")
doAssert img3.data == img4.data
block:
let
path = "tests/fileformats/gif/sunflower.gif"
image = decodeGIF(readFile(path))
dimensions = decodeGifDimensions(readFile(path))
image.writeFile("tests/fileformats/gif/sunflower.png")
doAssert image.width == dimensions.width
doAssert image.height == dimensions.height

View file

@ -203,6 +203,17 @@ block:
image.fill(rgba(255, 255, 255, 255))
doAssert not image.isTransparent()
block:
let image = newImage(100, 100)
image.fill(rgba(255, 255, 255, 255))
doAssert image.isOpaque()
block:
let image = newImage(100, 100)
image.fill(rgba(255, 255, 255, 255))
image[9, 13] = rgbx(250, 250, 250, 250)
doAssert not image.isOpaque()
block:
let a = newImage(400, 400)
let b = newImage(156, 434)

View file

@ -1,4 +1,8 @@
import jpegsuite, pixie
import jpegsuite, pixie, pixie/fileformats/jpeg
for file in jpegSuiteFiles:
let img = readImage(file)
let
image = readImage(file)
dimensions = decodeJpegDimensions(readFile(file))
doAssert image.width == dimensions.width
doAssert image.height == dimensions.height

View file

@ -33,3 +33,9 @@ block:
block:
discard readImage("tests/fileformats/png/trailing_data.png")
block:
let dimensions =
decodeImageDimensions(readFile("tests/fileformats/png/mandrill.png"))
doAssert dimensions.width == 512
doAssert dimensions.height == 512

View file

@ -2,13 +2,22 @@ import pixie/fileformats/ppm
block:
for format in @["p3", "p6"]:
let image = decodePpm(readFile(
"tests/fileformats/ppm/feep." & $format & ".master.ppm"
))
let
path = "tests/fileformats/ppm/feep." & $format & ".master.ppm"
image = decodePpm(readFile(path))
dimensions = decodePpmDimensions(readFile(path))
writeFile("tests/fileformats/ppm/feep." & $format & ".ppm", encodePpm(image))
doAssert image.width == dimensions.width
doAssert image.height == dimensions.height
let image = decodePpm(readFile("tests/fileformats/ppm/feep.p3.hidepth.master.ppm"))
block:
let
path = "tests/fileformats/ppm/feep.p3.hidepth.master.ppm"
image = decodePpm(readFile(path))
dimensions = decodePpmDimensions(readFile(path))
writeFile("tests/fileformats/ppm/feep.p3.hidepth.ppm", encodePpm(image))
doAssert image.width == dimensions.width
doAssert image.height == dimensions.height
# produced output should be identical to P6 master
let p6Master = readFile("tests/fileformats/ppm/feep.p6.master.ppm")

View file

@ -4,14 +4,19 @@ const tests = ["testcard", "testcard_rgba"]
for name in tests:
let
input = readImage("tests/fileformats/qoi/" & name & ".qoi")
path = "tests/fileformats/qoi/" & name & ".qoi"
input = readImage(path)
control = readImage("tests/fileformats/qoi/" & name & ".png")
dimensions = decodeQoiDimensions(readFile(path))
doAssert input.data == control.data, "input mismatch of " & name
doAssert input.width == dimensions.width
doAssert input.height == dimensions.height
discard encodeQoi(control)
for name in tests:
let
input = decodeQoi(readFile("tests/fileformats/qoi/" & name & ".qoi"))
path = "tests/fileformats/qoi/" & name & ".qoi"
input = decodeQoi(readFile(path))
output = decodeQoi(encodeQoi(input))
doAssert output.data.len == input.data.len
doAssert output.data == input.data