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/)

April 2022
Status: Incomplete
Dependencies: Saxon Java API

Imports

http://mathling.com/colour/space
import 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*


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(*)


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(*)

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(*)

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(*)


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(*)

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(*)

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()


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()

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()*))

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()*)*)

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 {})
};