diff --git a/imagelib/src/main/kotlin/pikt/error/ImageValueType.kt b/imagelib/src/main/kotlin/pikt/error/ImageValueType.kt index 6b5c3be..7cb86da 100644 --- a/imagelib/src/main/kotlin/pikt/error/ImageValueType.kt +++ b/imagelib/src/main/kotlin/pikt/error/ImageValueType.kt @@ -7,4 +7,5 @@ object ImageValueType { const val IMAGE = "image" const val WRITABLE_IMAGE = "writable image" + const val COLOR = "color" } \ No newline at end of file diff --git a/imagelib/src/main/kotlin/pikt/imagelib/AwtImage.kt b/imagelib/src/main/kotlin/pikt/imagelib/AwtImage.kt index 217a07d..4f20ef5 100644 --- a/imagelib/src/main/kotlin/pikt/imagelib/AwtImage.kt +++ b/imagelib/src/main/kotlin/pikt/imagelib/AwtImage.kt @@ -1,11 +1,14 @@ package pikt.imagelib import pikt.error.PiktIOException +import pikt.error.PiktIndexOutOfBoundsException import java.awt.image.BufferedImage import java.io.File import java.io.IOException import javax.imageio.ImageIO +private typealias AwtColor = java.awt.Color + /** * [Image] implementation based on AWT [BufferedImage] for the JVM. * @@ -19,6 +22,37 @@ class AwtImage(private val image: BufferedImage) : WritableImage { override val height: Int get() = image.height + private fun checkCoordinates(x: Int, y: Int, reference: Any) { + if (x < 0 || x >= image.width) { + throw PiktIndexOutOfBoundsException( + index = x, + size = image.width, + reference + ) + } + + if (y < 0 || y >= image.height) { + throw PiktIndexOutOfBoundsException( + index = y, + size = image.height, + reference + ) + } + } + + override fun getColor(x: Int, y: Int): Color { + this.checkCoordinates(x, y, reference = object {}) + + val rgb = image.getRGB(x, y) + return AwtColor(rgb).toPiktColor() + } + + override fun setColor(x: Int, y: Int, color: Color) { + this.checkCoordinates(x, y, reference = object {}) + + image.setRGB(x, y, color.toAwtColor().rgb) + } + override fun save(file: File) { try { ImageIO.write(image, file.extension, file) @@ -29,6 +63,9 @@ class AwtImage(private val image: BufferedImage) : WritableImage { override fun toString() = "AwtImage (width=$width, height=$height)" + private fun AwtColor.toPiktColor() = Color(red, green, blue) + private fun Color.toAwtColor() = AwtColor(red, green, blue) + companion object : ImageFactory { override fun blank(width: Int, height: Int, transparent: Boolean): AwtImage { diff --git a/imagelib/src/main/kotlin/pikt/imagelib/Color.kt b/imagelib/src/main/kotlin/pikt/imagelib/Color.kt new file mode 100644 index 0000000..15196b6 --- /dev/null +++ b/imagelib/src/main/kotlin/pikt/imagelib/Color.kt @@ -0,0 +1,60 @@ +package pikt.imagelib + +import pikt.error.PiktException +import pikt.error.PiktWrongArgumentTypeException +import pikt.error.ValueType.NUMBER +import pikt.stdlib.Struct + +// TODO find a better way via color schemes instead of hardcoding colors +private const val RED = "`E70000`" +private const val GREEN = "`00E700`" +private const val BLUE = "`0000E7`" + +private const val UPPER_LIMIT = 255 + +/** + * Representation of an RGB color. + * + * @param red the red component in 0-255 value + * @param green the green component in 0-255 value + * @param blue the blue component in 0-255 value + */ +class Color(red: Int = 0, green: Int = 0, blue: Int = 0) : Struct(RED to red, GREEN to green, BLUE to blue) { + + /** + * The red component in 0-255 value + */ + val red: Int + get() = this[RED] as Int + + /** + * The green component in 0-255 value + */ + val green: Int + get() = this[GREEN] as Int + + /** + * The blue component in 0-255 value + */ + val blue: Int + get() = this[BLUE] as Int + + override operator fun set(property: Any, value: Any) { + if (value !is Int) { + throw PiktWrongArgumentTypeException( + parameterName = "R/G/B", + argumentValue = value, + expectedType = NUMBER, + reference = object {} + ) + } + + if (value < 0 || value > UPPER_LIMIT) { + throw PiktException("0-255 value expected for R/G/B.", reference = object {}) + } + + super.set(property, value) + } + + override fun toString() = "Color (red=$red, green=$green, blue=$blue)" +} \ No newline at end of file diff --git a/imagelib/src/main/kotlin/pikt/imagelib/Image.kt b/imagelib/src/main/kotlin/pikt/imagelib/Image.kt index 06ca6ea..66b822d 100644 --- a/imagelib/src/main/kotlin/pikt/imagelib/Image.kt +++ b/imagelib/src/main/kotlin/pikt/imagelib/Image.kt @@ -1,5 +1,7 @@ package pikt.imagelib +import pikt.error.PiktIndexOutOfBoundsException + /** * Abstraction of images and their manipulations. */ @@ -14,4 +16,12 @@ interface Image { * Height of the image, in pixels. */ val height: Int + + /** + * @param x X coordinate + * @param y Y coordinate + * @return the color of the image at the given coordinate + * @throws PiktIndexOutOfBoundsException if one of the coordinates is negative or greater than the image size + */ + fun getColor(x: Int, y: Int): Color } \ No newline at end of file diff --git a/imagelib/src/main/kotlin/pikt/imagelib/ImageFunctions.kt b/imagelib/src/main/kotlin/pikt/imagelib/ImageFunctions.kt index e802ea4..5503746 100644 --- a/imagelib/src/main/kotlin/pikt/imagelib/ImageFunctions.kt +++ b/imagelib/src/main/kotlin/pikt/imagelib/ImageFunctions.kt @@ -1,8 +1,10 @@ package pikt.imagelib +import pikt.error.ImageValueType.COLOR import pikt.error.ImageValueType.IMAGE import pikt.error.ImageValueType.WRITABLE_IMAGE import pikt.error.PiktIOException +import pikt.error.PiktIndexOutOfBoundsException import pikt.error.PiktWrongArgumentTypeException import pikt.error.ValueType.NUMBER import pikt.stdlib.newFile @@ -11,6 +13,42 @@ import java.io.File // Top-level functions to access image methods from Pikt. // The only supported implementation is currently the AWT one for the JVM. +/** + * @throws PiktWrongArgumentTypeException if [image] is not an [Image], or is not a [WritableImage] and [requireWritable] is `true`. + */ +private fun checkImageType(image: Any, requireWritable: Boolean = false, reference: Any) { + if (image !is Image || (requireWritable && image !is WritableImage)) { + throw PiktWrongArgumentTypeException( + parameterName = "image", + argumentValue = image, + expectedType = if (requireWritable) WRITABLE_IMAGE else IMAGE, + reference + ) + } +} + +/** + * @throws PiktWrongArgumentTypeException if [x] and/or [y] are not numbers. + */ +private fun checkCoordinatesType(x: Any, y: Any, reference: Any) { + if (x !is Int) { + throw PiktWrongArgumentTypeException( + parameterName = "x", + argumentValue = x, + expectedType = NUMBER, + reference + ) + } + if (y !is Int) { + throw PiktWrongArgumentTypeException( + parameterName = "y", + argumentValue = y, + expectedType = NUMBER, + reference + ) + } +} + /** * Instantiates a new writable [Image]. * @param width image width as [Int] @@ -52,46 +90,58 @@ fun newImage(pathOrFile: Any): WritableImage = AwtImage.fromFile(newFile(pathOrF * @throws PiktIOException if the image could not be saved */ fun saveImage(image: Any, pathOrFile: Any) { - if (image !is WritableImage) { - throw PiktWrongArgumentTypeException( - parameterName = "image", - argumentValue = image, - expectedType = WRITABLE_IMAGE, - reference = object {} - ) - } - - image.save(newFile(pathOrFile)) + checkImageType(image, requireWritable = true, reference = object {}) + (image as WritableImage).save(newFile(pathOrFile)) } /** * @return width of [image] */ fun imageWidth(image: Any): Int { - if (image !is Image) { - throw PiktWrongArgumentTypeException( - parameterName = "image", - argumentValue = image, - expectedType = IMAGE, - reference = object {} - ) - } - - return image.width + checkImageType(image, reference = object {}) + return (image as Image).width } /** * @return height of [image] */ fun imageHeight(image: Any): Int { - if (image !is Image) { + checkImageType(image, reference = object {}) + return (image as Image).height +} + +/** + * @param x X coordinate + * @param y Y coordinate + * @return the color of the [image] at the given coordinate + * @throws PiktIndexOutOfBoundsException if one of the coordinates is negative or greater than the image size + */ +fun getImageColor(image: Any, x: Any, y: Any): Color { + checkImageType(image, reference = object {}) + checkCoordinatesType(x, y, object {}) + + return (image as Image).getColor(x as Int, y as Int) +} + +/** + * Changes the color of a pixel of the [image] at the given coordinates. + * @param x X coordinate + * @param y Y coordinate + * @param color color to set + * @throws PiktIndexOutOfBoundsException if one of the coordinates is negative or greater than the image size + */ +fun setImageColor(image: Any, x: Any, y: Any, color: Any) { + checkImageType(image, requireWritable = true, reference = object {}) + checkCoordinatesType(x, y, reference = object {}) + + if (color !is Color) { throw PiktWrongArgumentTypeException( - parameterName = "image", - argumentValue = image, - expectedType = IMAGE, + parameterName = "color", + argumentValue = color, + expectedType = COLOR, reference = object {} ) } - return image.height -} + return (image as WritableImage).setColor(x as Int, y as Int, color) +} \ No newline at end of file diff --git a/imagelib/src/main/kotlin/pikt/imagelib/WritableImage.kt b/imagelib/src/main/kotlin/pikt/imagelib/WritableImage.kt index 7759480..36642b7 100644 --- a/imagelib/src/main/kotlin/pikt/imagelib/WritableImage.kt +++ b/imagelib/src/main/kotlin/pikt/imagelib/WritableImage.kt @@ -1,6 +1,7 @@ package pikt.imagelib import pikt.error.PiktIOException +import pikt.error.PiktIndexOutOfBoundsException import java.io.File /** @@ -8,6 +9,15 @@ import java.io.File */ interface WritableImage : Image { + /** + * Changes the color of a pixel. + * @param x X coordinate + * @param y Y coordinate + * @param color color to set at the given coordinates + * @throws PiktIndexOutOfBoundsException if one of the coordinates is negative or greater than the image size + */ + fun setColor(x: Int, y: Int, color: Color) + /** * Saves this image to file. * @param file file to save the image to diff --git a/imagelib/src/main/resources/colors.properties b/imagelib/src/main/resources/colors.properties index 4a7c7ad..6acfed6 100644 --- a/imagelib/src/main/resources/colors.properties +++ b/imagelib/src/main/resources/colors.properties @@ -1,4 +1,6 @@ newImage=B41EFF saveImage=681EFF imageWidth= -imageHeight= \ No newline at end of file +imageHeight= +getImageColor= +setImageColor= \ No newline at end of file diff --git a/stdlib/src/main/kotlin/pikt/stdlib/Struct.kt b/stdlib/src/main/kotlin/pikt/stdlib/Struct.kt index bdc383c..bf4d469 100644 --- a/stdlib/src/main/kotlin/pikt/stdlib/Struct.kt +++ b/stdlib/src/main/kotlin/pikt/stdlib/Struct.kt @@ -22,7 +22,7 @@ open class Struct(vararg members: Pair) { * @param property member of this struct associated with the desired value * @throws PiktNoSuchElementException if [property]'s `toString()` is not a member of this struct */ - operator fun get(property: Any) = properties.mapGet(property.toString()) + open operator fun get(property: Any) = properties.mapGet(property.toString()) ?: throw PiktNoSuchElementException(property, reference = object {}) /** @@ -31,7 +31,7 @@ open class Struct(vararg members: Pair) { * @param value new value to overwrite * @throws PiktNoSuchElementException if [property]'s `toString()` is not a member of this struct */ - operator fun set(property: Any, value: Any) { + open operator fun set(property: Any, value: Any) { val key = property.toString() if(properties.containsKey(key)) { properties.mapSet(key, value)