http://mathling.com/image/png library module
http://mathling.com/image/png
PNG read/write;
Relies on Saxon Java API extension.
Embedded metadata doesn't work so I just write it to the side. SIGH.
Maybe someday those turkeys will implement their API functions.
Copyright© Mary Holstege 2022-2023
CC-BY (https://creativecommons.org/licenses/by/4.0/)
Status: Incomplete
Dependencies: Saxon Java API
Imports
http://mathling.com/colour/spaceimport module namespace cs="http://mathling.com/colour/space" at "../colourspace/colour-space.xqy"http://mathling.com/image/matrix
import module namespace cmatrix="http://mathling.com/image/matrix" at "matrix.xqy"http://mathling.com/core/utilities
import module namespace util="http://mathling.com/core/utilities" at "../core/utilities.xqy"http://mathling.com/geometric/matrix
import module namespace matrix="http://mathling.com/geometric/matrix" at "../geo/point-matrix.xqy"http://mathling.com/colour/rgb
import module namespace rgb="http://mathling.com/colour/rgb" at "../colourspace/rgb.xqy"http://mathling.com/core/errors
import module namespace errors="http://mathling.com/core/errors" at "../core/errors.xqy"http://mathling.com/geometric/point
import module namespace point="http://mathling.com/geometric/point" at "../geo/point.xqy"http://mathling.com/type/space
import module namespace space="http://mathling.com/type/space" at "../types/space.xqy"
Variables
Variable: $CRLF as xs:string
Functions
Function: png-extent
declare function png-extent($file as xs:string) as xs:integer*
declare function png-extent($file as xs:string) as xs:integer*
png-extent()
Read the width and height of the image out of the PNG format.
Params
- file as xs:string: location of PNG file
Returns
- xs:integer*: image width and height in that order
declare function this:png-extent( $file as xs:string ) as xs:integer* { let $bi := ImageIO:read(File:new($file)) let $width as xs:integer := $bi=>BufferedImage:getWidth() let $height as xs:integer := $bi=>BufferedImage:getHeight() return ($width, $height) }
Function: png-map
declare function png-map($file as xs:string,
$options as map(xs:string,item()*),
$pixel-function as function(xs:integer, xs:integer, map(xs:string,item()*)) as map(xs:string,item()*)) as map(*)
declare function png-map($file as xs:string, $options as map(xs:string,item()*), $pixel-function as function(xs:integer, xs:integer, map(xs:string,item()*)) as map(xs:string,item()*)) as map(*)
png-map():
Read a PNG file and return a colour map for the colour values.
If no explicit pixel function is given then we use a pixel function that
creates colour points in the selected colour space (option "colourspace") by
converting from the raw RGB.
The default is RGB points.
Params
- file as xs:string: location of PNG file
- options as map(xs:string,item()*): options controlling how to process "colourspace" one of "rgb", "xyz", "hsluv", "lab", "cmyk"
- pixel-function as function(xs:integer,xs:integer,map(xs:string,item()*))asmap(xs:string,item()*): function that defines what value we put into each slot Takes the following parameters: x, y coordinates RGB colour values It returns a colour point
Returns
- map(*)
declare function this:png-map( $file as xs:string, $options as map(xs:string,item()*), $pixel-function as function(xs:integer, xs:integer, map(xs:string,item()*)) as map(xs:string,item()*) ) as map(*) { let $bi := ImageIO:read(File:new($file)) let $width as xs:integer := $bi=>BufferedImage:getWidth() let $height as xs:integer := $bi=>BufferedImage:getHeight() return ( if ($bi=>BufferedImage:getType() = 6 (: 4BYTE_ABGR :) ) then ( util:log("Warning: the Java APIs don't do the right thing with type 6 images!") ) else (), fold-left( 0 to $height - 1, matrix:matrix(space:space($width,$height)), function($matrix as map(*), $y as xs:integer) as map(*) { fold-left( 0 to $width - 1, $matrix, function($matrix as map(*), $x as xs:integer) as map(*) { let $rgb := $bi=>BufferedImage:getRGB($x, $y)=>this:to-rgb() return ( $matrix=>matrix:put($x, $y, $pixel-function($x, $y, $rgb) ) ) } ) } ) ) }
Function: png-map
declare function png-map($file as xs:string,
$options as map(xs:string,item()*)) as map(*)
declare function png-map($file as xs:string, $options as map(xs:string,item()*)) as map(*)
Params
- file as xs:string
- options as map(xs:string,item()*)
Returns
- map(*)
declare function this:png-map( $file as xs:string, $options as map(xs:string,item()*) ) as map(*) { let $cs := ($options("colourspace"),"rgb")[1] let $make-colour := switch($cs) case "rgb" return function( $x as xs:integer, $y as xs:integer, $rgb as map(xs:string,item()*) ) as map(xs:string,item()*) { $rgb } case "xyz" return function( $x as xs:integer, $y as xs:integer, $rgb as map(xs:string,item()*) ) as map(xs:string,item()*) { cs:rgb-to-xyz($rgb) } case "hsluv" return function( $x as xs:integer, $y as xs:integer, $rgb as map(xs:string,item()*) ) as map(xs:string,item()*) { cs:rgb-to-hsluv($rgb) } case "lab" return function( $x as xs:integer, $y as xs:integer, $rgb as map(xs:string,item()*) ) as map(xs:string,item()*) { cs:rgb-to-lab($rgb) } case "cmyk" return function( $x as xs:integer, $y as xs:integer, $rgb as map(xs:string,item()*) ) as map(xs:string,item()*) { cs:rgb-to-cmyk($rgb) } default return function( $x as xs:integer, $y as xs:integer, $rgb as map(xs:string,item()*) ) as map(xs:string,item()*) { $rgb } return ( this:png-map($file, $options, $make-colour) ) }
Function: png-map
declare function png-map($file as xs:string) as map(*)
declare function png-map($file as xs:string) as map(*)
Params
- file as xs:string
Returns
- map(*)
declare function this:png-map( $file as xs:string ) as map(*) { this:png-map($file, map {}) }
Function: png-array
declare function png-array($file as xs:string,
$options as map(xs:string,item()*),
$pixel-function as function(xs:integer, xs:integer, map(xs:string,item()*)) as map(xs:string,item()*)) as map(*)
declare function png-array($file as xs:string, $options as map(xs:string,item()*), $pixel-function as function(xs:integer, xs:integer, map(xs:string,item()*)) as map(xs:string,item()*)) as map(*)
png-array():
Read a PNG file and return the colour values as an array.
If no explicit pixel function is given then we use a pixel function that
creates colour points in the selected colour space (option "colourspace") by
converting from the raw RGB.
The default is RGB points.
Params
- file as xs:string: location of PPM file
- options as map(xs:string,item()*): options controlling how to process "colourspace" one of "rgb", "xyz", "hsluv", "lab", "cmyk"
- pixel-function as function(xs:integer,xs:integer,map(xs:string,item()*))asmap(xs:string,item()*): function that defines what value we put into each slot Takes the following parameters: x, y coordinates RGB colour value It returns a colour point
Returns
- map(*)
declare function this:png-array( $file as xs:string, $options as map(xs:string,item()*), $pixel-function as function(xs:integer, xs:integer, map(xs:string,item()*)) as map(xs:string,item()*) ) as map(*) { let $bi := ImageIO:read(File:new($file)) let $width as xs:integer := $bi=>BufferedImage:getWidth() let $height as xs:integer := $bi=>BufferedImage:getHeight() let $data := for $y in 0 to $height - 1 for $x in 0 to $width - 1 let $rgb := $bi=>BufferedImage:getRGB($x, $y)=>this:to-rgb() return $pixel-function($x, $y, $rgb) return ( if ($bi=>BufferedImage:getType() = 6 (: 4BYTE_ABGR :) ) then ( util:log("Warning: the Java APIs don't do the right thing with type 6 images!") ) else (), cmatrix:array($height, $width, $data) ) (: BufferedImage img = ImageIO.read(file); int[] pixels = ((DataBufferInt)img.getRaster().getDataBuffer()).getData(); :) }
Function: png-array
declare function png-array($file as xs:string,
$options as map(xs:string,item()*)) as map(*)
declare function png-array($file as xs:string, $options as map(xs:string,item()*)) as map(*)
Params
- file as xs:string
- options as map(xs:string,item()*)
Returns
- map(*)
declare function this:png-array( $file as xs:string, $options as map(xs:string,item()*) ) as map(*) { let $cs := ($options("colourspace"),"rgb")[1] let $make-colour := switch($cs) case "rgb" return function( $x as xs:integer, $y as xs:integer, $rgb as map(xs:string,item()*) ) as map(xs:string,item()*) { $rgb } case "xyz" return function( $x as xs:integer, $y as xs:integer, $rgb as map(xs:string,item()*) ) as map(xs:string,item()*) { cs:rgb-to-xyz($rgb) } case "hsluv" return function( $x as xs:integer, $y as xs:integer, $rgb as map(xs:string,item()*) ) as map(xs:string,item()*) { cs:rgb-to-hsluv($rgb) } case "lab" return function( $x as xs:integer, $y as xs:integer, $rgb as map(xs:string,item()*) ) as map(xs:string,item()*) { cs:rgb-to-lab($rgb) } case "cmyk" return function( $x as xs:integer, $y as xs:integer, $rgb as map(xs:string,item()*) ) as map(xs:string,item()*) { cs:rgb-to-cmyk($rgb) } default return function( $x as xs:integer, $y as xs:integer, $rgb as map(xs:string,item()*) ) as map(xs:string,item()*) { $rgb } return ( this:png-array($file, $options, $make-colour) ) }
Function: png-array
declare function png-array($file as xs:string) as map(*)
declare function png-array($file as xs:string) as map(*)
Params
- file as xs:string
Returns
- map(*)
declare function this:png-array( $file as xs:string ) as map(*) { this:png-array($file, map {}) }
Function: png
declare function png($file as xs:string,
$metadata as xs:string?,
$matrix as map(*), (: point matrix: point to colour :)
$options as map(xs:string,item()*)) as empty-sequence()
declare function png($file as xs:string, $metadata as xs:string?, $matrix as map(*), (: point matrix: point to colour :) $options as map(xs:string,item()*)) as empty-sequence()
png()
Write out a document containing a simple PNG.
Params
- file as xs:string: where the output is going
- metadata as xs:string?: metadata to embed
- matrix as map(*): the colour map (point-matrix)
- options as map(xs:string,item()*): processing options "colourspace" one of "rgb", "xyz", "hsluv", "lab", "cmyk" what colour space the point map is to be taken as "flipped": whether to flip the Y coordinate (some algorithms put Y facing the opposite way)
Returns
declare function this:png( $file as xs:string, $metadata as xs:string?, $matrix as map(*), (: point matrix: point to colour :) $options as map(xs:string,item()*) ) as empty-sequence() { if ($matrix=>matrix:kind()=("imagearray","array")) then ( this:png( $file, $metadata, $matrix=>cmatrix:rows(), $matrix=>cmatrix:columns(), $matrix=>cmatrix:data(), $options ) ) else ( let $flipped := ($options("flipped"), false())[1] let $colourspace := ($options("colourspace"), "rgb")[1] let $getrgb := switch($colourspace) case "rgb" return function($val as map(xs:string,item()*)) as map(xs:string,item()*) {$val} case "xyz" return function($val as map(xs:string,item()*)) as map(xs:string,item()*) {cs:xyz-to-rgb($val)} case "hsluv" return function($val as map(xs:string,item()*)) as map(xs:string,item()*) {cs:hsluv-to-rgb($val)} case "lab" return function($val as map(xs:string,item()*)) as map(xs:string,item()*) {cs:lab-to-rgb($val)} case "cmyk" return function($val as map(xs:string,item()*)) as map(xs:string,item()*) {cs:cmyk-to-rgb($val)} default return function($val as map(xs:string,item()*)) as map(xs:string,item()*) {$val} let $min-x := $matrix=>matrix:min-x() let $min-y := $matrix=>matrix:min-y() let $max-x := $matrix=>matrix:max-x() let $max-y := $matrix=>matrix:max-y() let $y-range := if ($flipped) then reverse($min-y to $max-y) else ($min-y to $max-y) let $pixels := for $y in $y-range for $x in $min-x to $max-x return $getrgb($matrix=>matrix:get($x, $y))=>this:to-rgbint() let $width := $max-x - $min-x + 1 let $height := $max-y - $min-y + 1 let $bi := BufferedImage:new($width, $height, BufferedImage:TYPE_INT_ARGB())=> BufferedImage:setRGB(0, 0, $width - 1, $height - 1, $pixels, 0, $width) return ( if (empty($metadata)) then ( if (ImageIO:write($bi, "png", File:new($file))) then () else errors:error("ML-BAD", "Write failed") ) else ( if (ImageIO:write($bi, "png", File:new($file))) then ( file:write-text($file||".meta", document {$metadata})=>trace("writing "||$file||".meta") ) else ( errors:error("ML-BAD", "Write failed") ) ) ) ) }
Function: png
declare function png($file as xs:string,
$metadata as xs:string?,
$matrix as map(*)) as empty-sequence()
declare function png($file as xs:string, $metadata as xs:string?, $matrix as map(*)) as empty-sequence()
Params
- file as xs:string
- metadata as xs:string?
- matrix as map(*)
Returns
declare function this:png( $file as xs:string, $metadata as xs:string?, $matrix as map(*) (: point matrix: point to colour :) ) as empty-sequence() { this:png($file, $metadata, $matrix, map {}) }
Function: png
declare function png($file as xs:string,
$metadata as xs:string?,
$rows as xs:integer,
$columns as xs:integer,
$colours as map(xs:string,item()*)*, (: Sequence of colour values :)
$options as map(xs:string,item()*))
declare function png($file as xs:string, $metadata as xs:string?, $rows as xs:integer, $columns as xs:integer, $colours as map(xs:string,item()*)*, (: Sequence of colour values :) $options as map(xs:string,item()*))
Params
- file as xs:string
- metadata as xs:string?
- rows as xs:integer
- columns as xs:integer
- colours as map(xs:string,item()*)*
- options as map(xs:string,item()*)
declare function this:png( $file as xs:string, $metadata as xs:string?, $rows as xs:integer, $columns as xs:integer, $colours as map(xs:string,item()*)*, (: Sequence of colour values :) $options as map(xs:string,item()*) ) { let $flipped := ($options("flipped"), false())[1] let $colourspace := ($options("colourspace"), "rgb")[1] let $getrgb := switch($colourspace) case "rgb" return function($val as map(xs:string,item()*)) as map(xs:string,item()*) {$val} case "xyz" return function($val as map(xs:string,item()*)) as map(xs:string,item()*) {cs:xyz-to-rgb($val)} case "hsluv" return function($val as map(xs:string,item()*)) as map(xs:string,item()*) {cs:hsluv-to-rgb($val)} case "lab" return function($val as map(xs:string,item()*)) as map(xs:string,item()*) {cs:lab-to-rgb($val)} case "cmyk" return function($val as map(xs:string,item()*)) as map(xs:string,item()*) {cs:cmyk-to-rgb($val)} default return function($val as map(xs:string,item()*)) as map(xs:string,item()*) {$val} (: Some algorithms flip the y coordinates: pure Euclidean vs screen coords :) let $colours := if ($flipped) then ( for $y in reverse(1 to $rows) for $x in 1 to $columns return $colours[ ($y - 1)*$columns + $x ] ) else ( $colours ) let $n-colours := count($colours) let $n-expected := $rows*$columns let $colours := if ($n-colours gt $n-expected) then $colours[position()<=$n-expected] else if ($n-colours lt $n-expected) then for $i in 1 to $n-expected - $n-colours return $getrgb(rgb:rgb("black")) else $colours let $pixels := for $col in $colours return $col=>this:to-rgbint() let $width := $columns let $height := $rows let $bi := BufferedImage:new($width, $height, BufferedImage:TYPE_INT_ARGB())=> BufferedImage:setRGB(0, 0, $width - 1, $height - 1, $pixels, 0, $width) return ( if (empty($metadata)) then ( if (ImageIO:write($bi, "png", File:new($file))) then () else errors:error("ML-BAD", "Write failed") ) else ( if (ImageIO:write($bi, "png", File:new($file))) then ( file:write-text($file||".meta", document {$metadata})=>trace("writing "||$file||".meta") ) else ( errors:error("ML-BAD", "Write failed") ) ) ) }
Function: png
declare function png($file as xs:string,
$metadata as xs:string?,
$rows as xs:integer,
$columns as xs:integer,
$colours as map(xs:string,item()*)*)
declare function png($file as xs:string, $metadata as xs:string?, $rows as xs:integer, $columns as xs:integer, $colours as map(xs:string,item()*)*)
Params
- file as xs:string
- metadata as xs:string?
- rows as xs:integer
- columns as xs:integer
- colours as map(xs:string,item()*)*
declare function this:png( $file as xs:string, $metadata as xs:string?, $rows as xs:integer, $columns as xs:integer, $colours as map(xs:string,item()*)* ) { this:png($file, $metadata, $rows, $columns, map {}) }
Original Source Code
xquery version "3.1"; (:~ : PNG read/write; : Relies on Saxon Java API extension. : Embedded metadata doesn't work so I just write it to the side. SIGH. : Maybe someday those turkeys will implement their API functions. : : Copyright© Mary Holstege 2022-2023 : CC-BY (https://creativecommons.org/licenses/by/4.0/) : @since April 2022 : @custom:Status Incomplete : @custom:Dependencies Saxon Java API :) module namespace this="http://mathling.com/image/png"; import module namespace errors="http://mathling.com/core/errors" at "../core/errors.xqy"; import module namespace util="http://mathling.com/core/utilities" at "../core/utilities.xqy"; import module namespace point="http://mathling.com/geometric/point" at "../geo/point.xqy"; import module namespace matrix="http://mathling.com/geometric/matrix" at "../geo/point-matrix.xqy"; import module namespace cmatrix="http://mathling.com/image/matrix" at "matrix.xqy"; import module namespace space="http://mathling.com/type/space" at "../types/space.xqy"; import module namespace cs="http://mathling.com/colour/space" at "../colourspace/colour-space.xqy"; import module namespace rgb="http://mathling.com/colour/rgb" at "../colourspace/rgb.xqy"; declare namespace map="http://www.w3.org/2005/xpath-functions/map"; declare namespace saxon="http://saxon.sf.net/"; declare namespace file="http://expath.org/ns/file"; declare namespace jt="http://saxon.sf.net/java-type"; declare namespace File="java:java.io.File"; declare namespace ImageIO="java:javax.imageio.ImageIO"; declare namespace ImageWriter="java:javax.imageio.ImageWriter?void=this"; declare namespace BufferedImage="java:java.awt.image.BufferedImage?void=this"; declare namespace Color="java:java.awt.Color"; (: XYZZY: from the failed attempt to get metadata working, for another day :) (: declare namespace FileOutputStream="java:java.io.FileOutputStream"; :) (: declare namespace Iterator="java:java.util.Iterator"; :) (: declare namespace IIOMetadata="java:javax.imageio.metadata.IIOMetadata?void=this"; :) (: declare namespace IIOImage="java:javax.imageio.IIOImage"; :) (: declare namespace ImageTypeSpecifier="java:javax.imageio.ImageTypeSpecifier"; :) (: declare namespace DOMImplementationRegistry="java:org.w3c.dom.bootstrap.DOMImplementationRegistry"; :) (: declare namespace DOMImplementation="java:org.w3c.dom.DOMImplementation"; :) (: declare namespace Document="java:org.w3c.dom.Document"; :) (: declare namespace Element="java:org.w3c.dom.Element"; :) declare variable $this:CRLF as xs:string := " "; declare %private function this:to-rgb($rawrgb as xs:integer) as map(xs:string,item()*) { (: : Note: this fails with type 6 PNGs (4BYTE_ABGR) which InkScape sometimes : produces in converting images. Don't know why. It should work. :) let $val := if ($rawrgb < 0) then 2147483647 + $rawrgb + 1 else $rawrgb let $c := Color:new($val, true()) return ( rgb:rgba( $c=>Color:getRed() div 255, $c=>Color:getGreen() div 255, $c=>Color:getBlue() div 255, $c=>Color:getAlpha() div 255 ) ) }; declare %private function this:to-rgbint($rgb as map(xs:string,item()*)) as xs:integer { let $coords := rgb:coordinates($rgb, 4) return Color:new($coords[1], $coords[2], $coords[3], $coords[4])=>Color:getRGB() }; (:~ : png-extent() : Read the width and height of the image out of the PNG format. : @param $file: location of PNG file : @return image width and height in that order :) declare function this:png-extent( $file as xs:string ) as xs:integer* { let $bi := ImageIO:read(File:new($file)) let $width as xs:integer := $bi=>BufferedImage:getWidth() let $height as xs:integer := $bi=>BufferedImage:getHeight() return ($width, $height) }; (:~ : png-map(): : Read a PNG file and return a colour map for the colour values. : If no explicit pixel function is given then we use a pixel function that : creates colour points in the selected colour space (option "colourspace") by : converting from the raw RGB. : The default is RGB points. : : @param $file: location of PNG file : @param $options: options controlling how to process : "colourspace" one of "rgb", "xyz", "hsluv", "lab", "cmyk" : @param $pixel-function: function that defines what value we put into each slot : Takes the following parameters: : x, y coordinates : RGB colour values : It returns a colour point : :) declare function this:png-map( $file as xs:string, $options as map(xs:string,item()*), $pixel-function as function(xs:integer, xs:integer, map(xs:string,item()*)) as map(xs:string,item()*) ) as map(*) { let $bi := ImageIO:read(File:new($file)) let $width as xs:integer := $bi=>BufferedImage:getWidth() let $height as xs:integer := $bi=>BufferedImage:getHeight() return ( if ($bi=>BufferedImage:getType() = 6 (: 4BYTE_ABGR :) ) then ( util:log("Warning: the Java APIs don't do the right thing with type 6 images!") ) else (), fold-left( 0 to $height - 1, matrix:matrix(space:space($width,$height)), function($matrix as map(*), $y as xs:integer) as map(*) { fold-left( 0 to $width - 1, $matrix, function($matrix as map(*), $x as xs:integer) as map(*) { let $rgb := $bi=>BufferedImage:getRGB($x, $y)=>this:to-rgb() return ( $matrix=>matrix:put($x, $y, $pixel-function($x, $y, $rgb) ) ) } ) } ) ) }; declare function this:png-map( $file as xs:string, $options as map(xs:string,item()*) ) as map(*) { let $cs := ($options("colourspace"),"rgb")[1] let $make-colour := switch($cs) case "rgb" return function( $x as xs:integer, $y as xs:integer, $rgb as map(xs:string,item()*) ) as map(xs:string,item()*) { $rgb } case "xyz" return function( $x as xs:integer, $y as xs:integer, $rgb as map(xs:string,item()*) ) as map(xs:string,item()*) { cs:rgb-to-xyz($rgb) } case "hsluv" return function( $x as xs:integer, $y as xs:integer, $rgb as map(xs:string,item()*) ) as map(xs:string,item()*) { cs:rgb-to-hsluv($rgb) } case "lab" return function( $x as xs:integer, $y as xs:integer, $rgb as map(xs:string,item()*) ) as map(xs:string,item()*) { cs:rgb-to-lab($rgb) } case "cmyk" return function( $x as xs:integer, $y as xs:integer, $rgb as map(xs:string,item()*) ) as map(xs:string,item()*) { cs:rgb-to-cmyk($rgb) } default return function( $x as xs:integer, $y as xs:integer, $rgb as map(xs:string,item()*) ) as map(xs:string,item()*) { $rgb } return ( this:png-map($file, $options, $make-colour) ) }; declare function this:png-map( $file as xs:string ) as map(*) { this:png-map($file, map {}) }; (:~ : png-array(): : Read a PNG file and return the colour values as an array. : If no explicit pixel function is given then we use a pixel function that : creates colour points in the selected colour space (option "colourspace") by : converting from the raw RGB. : The default is RGB points. : : @param $file: location of PPM file : @param $options: options controlling how to process : "colourspace" one of "rgb", "xyz", "hsluv", "lab", "cmyk" : @param $pixel-function: function that defines what value we put into each slot : Takes the following parameters: : x, y coordinates : RGB colour value : It returns a colour point :) declare function this:png-array( $file as xs:string, $options as map(xs:string,item()*), $pixel-function as function(xs:integer, xs:integer, map(xs:string,item()*)) as map(xs:string,item()*) ) as map(*) { let $bi := ImageIO:read(File:new($file)) let $width as xs:integer := $bi=>BufferedImage:getWidth() let $height as xs:integer := $bi=>BufferedImage:getHeight() let $data := for $y in 0 to $height - 1 for $x in 0 to $width - 1 let $rgb := $bi=>BufferedImage:getRGB($x, $y)=>this:to-rgb() return $pixel-function($x, $y, $rgb) return ( if ($bi=>BufferedImage:getType() = 6 (: 4BYTE_ABGR :) ) then ( util:log("Warning: the Java APIs don't do the right thing with type 6 images!") ) else (), cmatrix:array($height, $width, $data) ) (: BufferedImage img = ImageIO.read(file); int[] pixels = ((DataBufferInt)img.getRaster().getDataBuffer()).getData(); :) }; declare function this:png-array( $file as xs:string, $options as map(xs:string,item()*) ) as map(*) { let $cs := ($options("colourspace"),"rgb")[1] let $make-colour := switch($cs) case "rgb" return function( $x as xs:integer, $y as xs:integer, $rgb as map(xs:string,item()*) ) as map(xs:string,item()*) { $rgb } case "xyz" return function( $x as xs:integer, $y as xs:integer, $rgb as map(xs:string,item()*) ) as map(xs:string,item()*) { cs:rgb-to-xyz($rgb) } case "hsluv" return function( $x as xs:integer, $y as xs:integer, $rgb as map(xs:string,item()*) ) as map(xs:string,item()*) { cs:rgb-to-hsluv($rgb) } case "lab" return function( $x as xs:integer, $y as xs:integer, $rgb as map(xs:string,item()*) ) as map(xs:string,item()*) { cs:rgb-to-lab($rgb) } case "cmyk" return function( $x as xs:integer, $y as xs:integer, $rgb as map(xs:string,item()*) ) as map(xs:string,item()*) { cs:rgb-to-cmyk($rgb) } default return function( $x as xs:integer, $y as xs:integer, $rgb as map(xs:string,item()*) ) as map(xs:string,item()*) { $rgb } return ( this:png-array($file, $options, $make-colour) ) }; declare function this:png-array( $file as xs:string ) as map(*) { this:png-array($file, map {}) }; (:~ : png() : Write out a document containing a simple PNG. : : @param $file: where the output is going : @param $metadata: metadata to embed : @param $matrix: the colour map (point-matrix) : @param $options: processing options : "colourspace" one of "rgb", "xyz", "hsluv", "lab", "cmyk" : what colour space the point map is to be taken as : "flipped": whether to flip the Y coordinate (some algorithms put Y : facing the opposite way) :) declare function this:png( $file as xs:string, $metadata as xs:string?, $matrix as map(*), (: point matrix: point to colour :) $options as map(xs:string,item()*) ) as empty-sequence() { if ($matrix=>matrix:kind()=("imagearray","array")) then ( this:png( $file, $metadata, $matrix=>cmatrix:rows(), $matrix=>cmatrix:columns(), $matrix=>cmatrix:data(), $options ) ) else ( let $flipped := ($options("flipped"), false())[1] let $colourspace := ($options("colourspace"), "rgb")[1] let $getrgb := switch($colourspace) case "rgb" return function($val as map(xs:string,item()*)) as map(xs:string,item()*) {$val} case "xyz" return function($val as map(xs:string,item()*)) as map(xs:string,item()*) {cs:xyz-to-rgb($val)} case "hsluv" return function($val as map(xs:string,item()*)) as map(xs:string,item()*) {cs:hsluv-to-rgb($val)} case "lab" return function($val as map(xs:string,item()*)) as map(xs:string,item()*) {cs:lab-to-rgb($val)} case "cmyk" return function($val as map(xs:string,item()*)) as map(xs:string,item()*) {cs:cmyk-to-rgb($val)} default return function($val as map(xs:string,item()*)) as map(xs:string,item()*) {$val} let $min-x := $matrix=>matrix:min-x() let $min-y := $matrix=>matrix:min-y() let $max-x := $matrix=>matrix:max-x() let $max-y := $matrix=>matrix:max-y() let $y-range := if ($flipped) then reverse($min-y to $max-y) else ($min-y to $max-y) let $pixels := for $y in $y-range for $x in $min-x to $max-x return $getrgb($matrix=>matrix:get($x, $y))=>this:to-rgbint() let $width := $max-x - $min-x + 1 let $height := $max-y - $min-y + 1 let $bi := BufferedImage:new($width, $height, BufferedImage:TYPE_INT_ARGB())=> BufferedImage:setRGB(0, 0, $width - 1, $height - 1, $pixels, 0, $width) return ( if (empty($metadata)) then ( if (ImageIO:write($bi, "png", File:new($file))) then () else errors:error("ML-BAD", "Write failed") ) else ( if (ImageIO:write($bi, "png", File:new($file))) then ( file:write-text($file||".meta", document {$metadata})=>trace("writing "||$file||".meta") ) else ( errors:error("ML-BAD", "Write failed") ) ) ) ) }; declare function this:png( $file as xs:string, $metadata as xs:string?, $matrix as map(*) (: point matrix: point to colour :) ) as empty-sequence() { this:png($file, $metadata, $matrix, map {}) }; declare function this:png( $file as xs:string, $metadata as xs:string?, $rows as xs:integer, $columns as xs:integer, $colours as map(xs:string,item()*)*, (: Sequence of colour values :) $options as map(xs:string,item()*) ) { let $flipped := ($options("flipped"), false())[1] let $colourspace := ($options("colourspace"), "rgb")[1] let $getrgb := switch($colourspace) case "rgb" return function($val as map(xs:string,item()*)) as map(xs:string,item()*) {$val} case "xyz" return function($val as map(xs:string,item()*)) as map(xs:string,item()*) {cs:xyz-to-rgb($val)} case "hsluv" return function($val as map(xs:string,item()*)) as map(xs:string,item()*) {cs:hsluv-to-rgb($val)} case "lab" return function($val as map(xs:string,item()*)) as map(xs:string,item()*) {cs:lab-to-rgb($val)} case "cmyk" return function($val as map(xs:string,item()*)) as map(xs:string,item()*) {cs:cmyk-to-rgb($val)} default return function($val as map(xs:string,item()*)) as map(xs:string,item()*) {$val} (: Some algorithms flip the y coordinates: pure Euclidean vs screen coords :) let $colours := if ($flipped) then ( for $y in reverse(1 to $rows) for $x in 1 to $columns return $colours[ ($y - 1)*$columns + $x ] ) else ( $colours ) let $n-colours := count($colours) let $n-expected := $rows*$columns let $colours := if ($n-colours gt $n-expected) then $colours[position()<=$n-expected] else if ($n-colours lt $n-expected) then for $i in 1 to $n-expected - $n-colours return $getrgb(rgb:rgb("black")) else $colours let $pixels := for $col in $colours return $col=>this:to-rgbint() let $width := $columns let $height := $rows let $bi := BufferedImage:new($width, $height, BufferedImage:TYPE_INT_ARGB())=> BufferedImage:setRGB(0, 0, $width - 1, $height - 1, $pixels, 0, $width) return ( if (empty($metadata)) then ( if (ImageIO:write($bi, "png", File:new($file))) then () else errors:error("ML-BAD", "Write failed") ) else ( if (ImageIO:write($bi, "png", File:new($file))) then ( file:write-text($file||".meta", document {$metadata})=>trace("writing "||$file||".meta") ) else ( errors:error("ML-BAD", "Write failed") ) ) ) }; declare function this:png( $file as xs:string, $metadata as xs:string?, $rows as xs:integer, $columns as xs:integer, $colours as map(xs:string,item()*)* ) { this:png($file, $metadata, $rows, $columns, map {}) };