Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions src/pixie.nim
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,135 @@ proc decodeImage*(data: string): Image {.raises: [PixieError].} =
else:
raise newException(PixieError, "Unsupported image file format")

proc validateScaledImageTarget(width, height: int) {.raises: [PixieError].} =
if width <= 0 or width > int32.high.int:
raise newException(PixieError, "Invalid target width")
if height <= 0 or height > int32.high.int:
raise newException(PixieError, "Invalid target height")

proc validateScaledImageTarget(target: Image) {.raises: [PixieError].} =
if target.isNil:
raise newException(PixieError, "Invalid target Image")
validateScaledImageTarget(target.width, target.height)

proc copyIntoTarget(target, source: Image) {.raises: [PixieError].} =
if target.width != source.width or target.height != source.height:
raise newException(PixieError, "Image dimensions do not match target")
if target.data.len > 0:
copyMem(
target.data[0].addr,
source.data[0].unsafeAddr,
target.data.len * sizeof(ColorRGBX)
)

proc decodeImageScaled*(
data: string, width, height: int, fit = fitStretch
): Image {.raises: [PixieError].}

proc decodeImageScaled*(
data: var string, width, height: int, fit = fitStretch
): Image {.raises: [PixieError].}

proc decodeImageScaledInto*(
data: var string, target: Image, fit = fitStretch
): Image {.raises: [PixieError].}

proc decodeImageScaled*(
data: pointer, len, width, height: int, fit = fitStretch
): Image {.raises: [PixieError].} =
## Loads an image from memory scaled to the requested dimensions.
validateScaledImageTarget(width, height)
if len > 8 and equalMem(data, pngSignature[0].unsafeAddr, 8):
decodePngScaled(data, len, width, height, fit)
elif len > 2 and equalMem(data, jpegStartOfImage[0].unsafeAddr, 2):
decodeJpegScaled(data, len, width, height, fit)
else:
var copy = newString(len)
if len > 0:
copyMem(addr copy[0], data, len)
decodeImageScaled(copy, width, height, fit)

proc decodeImageScaled*(
data: string, width, height: int, fit = fitStretch
): Image {.raises: [PixieError].} =
## Loads an image from memory scaled to the requested dimensions.
validateScaledImageTarget(width, height)
if data.len > 8 and data.readUint64(0) == cast[uint64](pngSignature):
decodePngScaled(data, width, height, fit)
elif data.len > 2 and data.readUint16(0) == cast[uint16](jpegStartOfImage):
decodeJpegScaled(data, width, height, fit)
else:
let image = decodeImage(data)
if image.width == width and image.height == height:
image
else:
image.resize(width, height)

proc decodeImageScaled*(
data: var string, width, height: int, fit = fitStretch
): Image {.raises: [PixieError].} =
## Loads an image from memory scaled to the requested dimensions. JPEG
## releases the source buffer before allocating the destination; PNG releases
## it after parsing because the PNG stream must be inflated first.
validateScaledImageTarget(width, height)
if data.len > 8 and data.readUint64(0) == cast[uint64](pngSignature):
decodePngScaled(data, width, height, fit)
elif data.len > 2 and data.readUint16(0) == cast[uint16](jpegStartOfImage):
decodeJpegScaled(data, width, height, fit)
else:
let image = decodeImage(data)
data = ""
try:
GC_fullCollect()
except Exception:
discard
if image.width == width and image.height == height:
image
else:
image.resize(width, height)

proc decodeImageScaledInto*(
data: pointer, len: int, target: Image, fit = fitStretch
): Image {.raises: [PixieError].} =
## Loads an image from memory scaled into an existing target Image.
validateScaledImageTarget(target)
if len > 8 and equalMem(data, pngSignature[0].unsafeAddr, 8):
decodePngScaledInto(data, len, target, fit)
elif len > 2 and equalMem(data, jpegStartOfImage[0].unsafeAddr, 2):
decodeJpegScaledInto(data, len, target, fit)
else:
var copy = newString(len)
if len > 0:
copyMem(addr copy[0], data, len)
discard decodeImageScaledInto(copy, target, fit)
target

proc decodeImageScaledInto*(
data: string, target: Image, fit = fitStretch
): Image {.raises: [PixieError].} =
## Loads an image from memory scaled into an existing target Image.
validateScaledImageTarget(target)
if data.len > 8 and data.readUint64(0) == cast[uint64](pngSignature):
decodePngScaledInto(data, target, fit)
elif data.len > 2 and data.readUint16(0) == cast[uint16](jpegStartOfImage):
decodeJpegScaledInto(data, target, fit)
else:
target.copyIntoTarget(decodeImageScaled(data, target.width, target.height))
target

proc decodeImageScaledInto*(
data: var string, target: Image, fit = fitStretch
): Image {.raises: [PixieError].} =
## Loads an image from memory scaled into an existing target Image.
validateScaledImageTarget(target)
if data.len > 8 and data.readUint64(0) == cast[uint64](pngSignature):
decodePngScaledInto(data, target, fit)
elif data.len > 2 and data.readUint16(0) == cast[uint16](jpegStartOfImage):
decodeJpegScaledInto(data, target, fit)
else:
target.copyIntoTarget(decodeImageScaled(data, target.width, target.height))
target

proc readImageDimensions*(
filePath: string
): ImageDimensions {.inline, raises: [PixieError].} =
Expand All @@ -94,6 +223,16 @@ proc readImage*(filePath: string): Image {.inline, raises: [PixieError].} =
except IOError as e:
raise newException(PixieError, e.msg, e)

proc readImageScaled*(
filePath: string, width, height: int, fit = fitStretch
): Image {.inline, raises: [PixieError].} =
## Loads an image from a file scaled to the requested dimensions.
try:
var data = readFile(filePath)
decodeImageScaled(data, width, height, fit)
except IOError as e:
raise newException(PixieError, e.msg, e)

proc encodeImage*(
image: Image, fileFormat: FileFormat
): string {.raises: [PixieError].} =
Expand Down
6 changes: 6 additions & 0 deletions src/pixie/common.nim
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ type
ImageDimensions* = object
width*, height*: int

ScaledDecodeFit* = enum
## How a scaled decode maps the source onto the target image.
fitStretch ## fill the whole target, ignoring aspect ratio
fitCover ## fill the whole target, cropping the source centered
fitContain ## fit the whole source centered, leaving target borders untouched

Image* = ref object
## Image object that holds bitmap data in premultiplied alpha RGBA format.
width*, height*: int
Expand Down
38 changes: 38 additions & 0 deletions src/pixie/decodebudget.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
## Runtime memory budget for image decoding.
##
## Decoders consult `decodeBudgetBytes` before allocating image-sized
## buffers (coefficient blocks, channel masks, inflate output, pixel seqs)
## and raise a catchable PixieError when a decode would exceed it. The
## budget covers decode *intermediates plus output* for a single decode
## call, not process-wide usage.
##
## 0 means unlimited (upstream pixie behaviour). Embedded builds default
## to a conservative budget; hosts default to unlimited until the
## application calls `setDecodeBudgetBytes` with a live value derived from
## available memory.

when defined(frameosEmbedded):
const defaultDecodeBudgetBytes = 10 * 1024 * 1024
else:
const defaultDecodeBudgetBytes = 0

var decodeBudget {.threadvar.}: int
var decodeBudgetInitialized {.threadvar.}: bool

proc decodeBudgetBytes*(): int {.inline, raises: [].} =
## Current per-decode memory budget in bytes; 0 = unlimited.
if not decodeBudgetInitialized:
decodeBudget = defaultDecodeBudgetBytes
decodeBudgetInitialized = true
decodeBudget

proc setDecodeBudgetBytes*(bytes: int) {.raises: [].} =
## Sets the per-decode memory budget; 0 = unlimited. Refresh this from
## live available memory before heavy decodes for best results.
decodeBudget = max(0, bytes)
decodeBudgetInitialized = true

proc overDecodeBudget*(bytes: int64): bool {.inline, raises: [].} =
## True when an allocation plan of `bytes` exceeds the current budget.
let budget = decodeBudgetBytes()
budget > 0 and bytes > budget.int64
Loading