http://mathling.com/image/ppm library module
http://mathling.com/image/ppm
PPM image formats
Copyright© Mary Holstege 2021-2023
CC-BY (https://creativecommons.org/licenses/by/4.0/)
Status: Stable
Dependencies: EXPath binary, EXPath file
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 "../image/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: p3-extent
declare function p3-extent($file as xs:string) as xs:integer*
declare function p3-extent($file as xs:string) as xs:integer*
p3-extent()
Read the width and height of the image out of the P3 format.
Params
- file as xs:string: location of PPM file
Returns
- xs:integer*: image width and height in that order
declare function this:p3-extent( $file as xs:string ) as xs:integer* { let $lines := unparsed-text-lines($file)[not(.="") and not(starts-with(.,"#"))] let $width := xs:integer(substring-before($lines[2], " ")) let $height := xs:integer(substring-after($lines[2], " ")) return ($width, $height) }
Function: p3-map
declare function p3-map($file as xs:string,
$options as map(xs:string,item()*),
$pixel-function as function(xs:integer, xs:integer, xs:integer, xs:integer, xs:integer, xs:integer) as map(xs:string,item()*)) as map(*)
declare function p3-map($file as xs:string, $options as map(xs:string,item()*), $pixel-function as function(xs:integer, xs:integer, xs:integer, xs:integer, xs:integer, xs:integer) as map(xs:string,item()*)) as map(*)
p3-map():
Read a raw PPM file that uses the ASCII format for image values and
return a colour map. 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,xs:integer,xs:integer,xs:integer,xs:integer)asmap(xs:string,item()*): function that defines what value we put into each slot Takes the following parameters: x, y coordinates maximum colour value (255, generally) r, g, b raw RGB colour values It returns a colour point Format is: P3 width height # comments, if any 255 (=colour max) r g b r g b ...
Returns
- map(*)
declare function this:p3-map( $file as xs:string, $options as map(xs:string,item()*), $pixel-function as function(xs:integer, xs:integer, xs:integer, xs:integer, xs:integer, xs:integer) as map(xs:string,item()*) ) as map(*) { let $lines := unparsed-text-lines($file)[not(.="") and not(starts-with(.,"#"))] let $width := xs:integer(substring-before($lines[2], " ")) let $height := xs:integer(substring-after($lines[2], " ")) let $colour-max := xs:integer($lines[3]) let $pixels := ( (: Check format :) if ($lines[1] eq "P3") then () else errors:error("IMAGE-BADFORMAT", $lines[1]), for $line in $lines[position()>3] return (tokenize($line, " ")[not(.="")])!xs:integer(.) ) return ( fold-left( 1 to $height, matrix:matrix(space:space($width,$height)), function($matrix as map(*), $y as xs:integer) as map(*) { fold-left( 1 to $width, $matrix, function($matrix as map(*), $x as xs:integer) as map(*) { let $index := ((($y - 1) * $width + $x) - 1) * 3 let $r := $pixels[$index + 1] let $g := $pixels[$index + 2] let $b := $pixels[$index + 3] return ( if ($x <= $width and $y <= $height) then ( $matrix=>matrix:put($x - 1, $y - 1, $pixel-function($x, $y, $colour-max, $r, $g, $b) ) ) else ( $matrix ) ) } ) } ) ) }
Function: p3-map
declare function p3-map($file as xs:string,
$options as map(xs:string,item()*)) as map(*)
declare function p3-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:p3-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, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value) } case "xyz" return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { cs:rgb-to-xyz(rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value)) } case "hsluv" return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { cs:rgb-to-hsluv(rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value)) } case "lab" return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { cs:rgb-to-lab(rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value)) } case "cmyk" return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { cs:rgb-to-cmyk(rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value)) } default return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value) } return ( this:p3-map($file, $options, $make-colour) ) }
Function: p3-map
declare function p3-map($file as xs:string) as map(*)
declare function p3-map($file as xs:string) as map(*)
Params
- file as xs:string
Returns
- map(*)
declare function this:p3-map( $file as xs:string ) as map(*) { this:p3-map($file, map {}) }
Function: p3-array
declare function p3-array($file as xs:string,
$options as map(xs:string,item()*),
$pixel-function as function(xs:integer, xs:integer, xs:integer, xs:integer, xs:integer, xs:integer) as map(xs:string,item()*)) as map(*)
declare function p3-array($file as xs:string, $options as map(xs:string,item()*), $pixel-function as function(xs:integer, xs:integer, xs:integer, xs:integer, xs:integer, xs:integer) as map(xs:string,item()*)) as map(*)
p3-array():
Read a raw PPM file that uses the ASCII format for image values and
return a colour 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,xs:integer,xs:integer,xs:integer,xs:integer)asmap(xs:string,item()*): function that defines what value we put into each slot Takes the following parameters: x, y coordinates maximum colour value (255, generally) r, g, b raw RGB colour values It returns a colour point Format is: P3 width height # comments, if any 255 (=colour max) r g b r g b ...
Returns
- map(*)
declare function this:p3-array( $file as xs:string, $options as map(xs:string,item()*), $pixel-function as function(xs:integer, xs:integer, xs:integer, xs:integer, xs:integer, xs:integer) as map(xs:string,item()*) ) as map(*) { let $lines := unparsed-text-lines($file)[not(.="") and not(starts-with(.,"#"))] let $width := xs:integer(substring-before($lines[2], " ")) let $height := xs:integer(substring-after($lines[2], " ")) let $colour-max := xs:integer($lines[3]) let $pixels := ( (: Check format :) if ($lines[1] eq "P3") then () else errors:error("IMAGE-BADFORMAT", $lines[1]), for $line in $lines[position()>3] return (tokenize($line, " ")[not(.="")])!xs:integer(.) ) let $data := for $y in 1 to $height for $x in 1 to $width let $index := ((($y - 1) * $width + $x) - 1) * 3 let $r := $pixels[$index + 1] let $g := $pixels[$index + 2] let $b := $pixels[$index + 3] return $pixel-function($x, $y, $colour-max, $r, $g, $b) return cmatrix:array($height, $width, $data) }
Function: p3-array
declare function p3-array($file as xs:string,
$options as map(xs:string,item()*)) as map(*)
declare function p3-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:p3-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, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value) } case "xyz" return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { cs:rgb-to-xyz(rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value)) } case "hsluv" return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { cs:rgb-to-hsluv(rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value)) } case "lab" return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { cs:rgb-to-lab(rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value)) } case "cmyk" return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { cs:rgb-to-cmyk(rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value)) } default return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value) } return ( this:p3-array($file, $options, $make-colour) ) }
Function: p3-array
declare function p3-array($file as xs:string) as map(*)
declare function p3-array($file as xs:string) as map(*)
Params
- file as xs:string
Returns
- map(*)
declare function this:p3-array( $file as xs:string ) as map(*) { this:p3-array($file, map {}) }
Function: p3
declare function p3($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 p3($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()
p3()
Write out a document containing a raw PPM format image using the ASCII
format for image values. Can be converted, processed with GIMP, or viewed
in Emacs but not browser.
Params
- file as xs:string: where the output is going
- metadata as xs:string?: metadata to embed
- matrix as map(*): the colour map
- 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) Format is: P3 width height # comments, if any 255 (=colour max) r g b r g b ...
Returns
declare function this:p3( $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:p3( $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 $header := "P3"||$this:CRLF|| ($max-x - $min-x + 1)||" "||($max-y - $min-y + 1)||$this:CRLF|| string-join( for $line in tokenize($metadata, "\n")[. ne ""] return "#"||$line, $this:CRLF )||$this:CRLF|| 255||$this:CRLF let $y-range := if ($flipped) then reverse($min-y to $max-y) else ($min-y to $max-y) let $bytes := string-join( for $y in $y-range return ( string-join( for $x in $min-x to $max-x let $rgb := $getrgb($matrix=>matrix:get($x, $y)) return rgb:coordinates($rgb, 3)!string(.), " " ) ), $this:CRLF ) return ( file:write-text($file, document {$header, $bytes}) ) ) }
Function: p3
declare function p3($file as xs:string,
$metadata as xs:string?,
$matrix as map(*)) as empty-sequence()
declare function p3($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:p3( $file as xs:string, $metadata as xs:string?, $matrix as map(*) (: point matrix: point to colour :) ) as empty-sequence() { this:p3($file, $metadata, $matrix, map {}) }
Function: p3
declare function p3($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 p3($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()*))
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:p3( $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()*) ) { 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 $header := "P3"||$this:CRLF|| $columns||" "||$rows||$this:CRLF|| string-join( for $line in tokenize($metadata, "\n")[. ne ""] return "#"||$line, $this:CRLF )||$this:CRLF|| 255||$this:CRLF (: 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 $bytes := string-join( for $col in $colours return ( rgb:coordinates($getrgb($col), 3)!string(.) ), " " ) return ( file:write-text($file, document {$header, $bytes}) ) }
Function: p6-array
declare function p6-array($file as xs:string,
$options as map(xs:string,item()*),
$pixel-function as function(xs:integer, xs:integer, xs:integer, xs:integer, xs:integer, xs:integer) as map(xs:string,item()*)) as map(*)
declare function p6-array($file as xs:string, $options as map(xs:string,item()*), $pixel-function as function(xs:integer, xs:integer, xs:integer, xs:integer, xs:integer, xs:integer) as map(xs:string,item()*)) as map(*)
p6-array():
Read a raw PPM file that uses the binary format for image values and
return a colour 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,xs:integer,xs:integer,xs:integer,xs:integer)asmap(xs:string,item()*): function that defines what value we put into each slot Takes the following parameters: x, y coordinates maximum colour value (255, generally) r, g, b raw RGB colour values It returns a colour point Format is: P6 width height # comments, if any 255 (=colour max) r g b r g b ... (as binary)
Returns
- map(*)
declare function this:p6-array( $file as xs:string, $options as map(xs:string,item()*), $pixel-function as function(xs:integer, xs:integer, xs:integer, xs:integer, xs:integer, xs:integer) as map(xs:string,item()*) ) as map(*) { let $data := file:read-binary($file)=>binary:to-octets() let $linebreaks := ( (0, for $i in 1 to count($data) where $data[$i]=10 (:CR:) return $i) ) let $lines := ( for $i in 1 to count($linebreaks) - 1 return ( if ($linebreaks[$i] + 1 = $linebreaks[$i + 1]) then () else if ($data[$linebreaks[$i]+1] = 35 (:#:)) then () else ( array { $data[position() > $linebreaks[$i] and position() < $linebreaks[$i + 1]] } ) ) ) let $width := xs:integer(substring-before(codepoints-to-string($lines[2]?*), " ")) let $height := xs:integer(substring-after(codepoints-to-string($lines[2]?*), " ")) let $colour-max := xs:integer(codepoints-to-string($lines[3]?*)) let $start-offset := ( for $i in 1 to count($linebreaks) - 1 return ( if ($linebreaks[$i] + 1 = $linebreaks[$i + 1]) then () else if ($data[$linebreaks[$i]+1] = 35 (:#:)) then () else $linebreaks[$i] ) )[4] (: 0 + ^P6 + ^width height + ^255 :) let $pixels := ( (: Check format :) if (codepoints-to-string($lines[1]?*) eq "P6") then () else errors:error("IMAGE-BADFORMAT", $lines[1]), $data[position() > $start-offset] (: octets :) ) let $data := for $y in 1 to $height for $x in 1 to $width let $index := ((($y - 1) * $width + $x) - 1) * 3 let $r := $pixels[$index + 1] let $g := $pixels[$index + 2] let $b := $pixels[$index + 3] return $pixel-function($x, $y, $colour-max, $r, $g, $b) return cmatrix:array($height, $width, $data) }
Function: p6-array
declare function p6-array($file as xs:string,
$options as map(xs:string,item()*)) as map(*)
declare function p6-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:p6-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, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value) } case "xyz" return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { cs:rgb-to-xyz(rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value)) } case "hsluv" return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { cs:rgb-to-hsluv(rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value)) } case "lab" return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { cs:rgb-to-lab(rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value)) } case "cmyk" return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { cs:rgb-to-cmyk(rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value)) } default return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value) } return ( this:p6-array($file, $options, $make-colour) ) }
Function: p6-array
declare function p6-array($file as xs:string) as map(*)
declare function p6-array($file as xs:string) as map(*)
Params
- file as xs:string
Returns
- map(*)
declare function this:p6-array( $file as xs:string ) as map(*) { this:p6-array($file, map {}) }
Function: p6
declare function p6($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 p6($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()
p6()
Write out a file containing the raw PPM format image using the
binary format for image values.
Can be converted, processed with GIMP, or viewed in Emacs but not browser.
Params
- file as xs:string: where the output is going
- metadata as xs:string?: metadata to embed
- matrix as map(*): the colour map
- 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) Format is: P6 width height # comments, if any 255 (=colour max) r g b r g b ... (as binary)
Returns
declare function this:p6( $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:p6( $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 $header := "P6"||$this:CRLF|| ($max-x - $min-x + 1)||" "||($max-y - $min-y + 1)||$this:CRLF|| string-join( for $line in tokenize($metadata, "\n")[. ne ""] return "#"||$line, $this:CRLF )||$this:CRLF|| 255||$this:CRLF (: Some algorithms flip the y coordinates: pure Euclidean vs screen coords :) let $y-range := if ($flipped) then reverse($min-y to $max-y) else ($min-y to $max-y) let $bytes := binary:from-octets( for $y in $y-range for $x in $min-x to $max-x let $rgb := $getrgb($matrix=>matrix:get($x, $y)) for $val in rgb:coordinates($rgb, 3) return ( $val ) ) return ( file:write-text($file, $header), file:append-binary($file, $bytes) ) ) }
Function: p6
declare function p6($file as xs:string,
$metadata as xs:string?,
$matrix as map(*)) as empty-sequence()
declare function p6($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:p6( $file as xs:string, $metadata as xs:string?, $matrix as map(*) (: point matrix: point to colour :) ) as empty-sequence() { this:p6($file, $metadata, $matrix, map {}) }
Function: p6
declare function p6($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()*)) as empty-sequence()
declare function p6($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()*)) as empty-sequence()
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()*)
Returns
declare function this:p6( $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()*) ) as empty-sequence() { 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 $header := "P6"||$this:CRLF|| $columns||" "||$rows||$this:CRLF|| string-join( for $line in tokenize($metadata, "\n")[. ne ""] return "#"||$line, $this:CRLF )||$this:CRLF|| 255||$this:CRLF (: 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 $bytes := binary:from-octets( for $col in $colours let $rgb := $getrgb($col) for $val in rgb:coordinates($rgb, 3) return ( $val ) ) return ( file:write-text($file, $header), file:append-binary($file, $bytes) ) }
Function: pam
declare function pam($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 pam($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()
pam()
Write out a file containing raw PAM format image using the binary
format for image values. This format can include alpha values for
each pixel, not just RGB.
Params
- file as xs:string: where the output is going
- metadata as xs:string?: metadata to embed
- matrix as map(*): the colour map
- 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) Format is: P7 WIDTH width HEIGHT height DEPTH 4 MAXVAL 255 TUPLTYPE RGB_ALPHA # comments, if any ENDHDR r g b a r g b a ... (as binary) ImageMagick can convert this but Emacs integration, GIMP, and browser all fail on it.
Returns
declare function this:pam( $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:pam( $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 $getrgba := 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 $header := "P7"||$this:CRLF|| "WIDTH "||($max-x - $min-x + 1)||$this:CRLF|| "HEIGHT "||($max-y - $min-y + 1)||$this:CRLF|| "DEPTH 4"||$this:CRLF|| "MAXVAL 255"||$this:CRLF|| "TUPLTYPE RGB_ALPHA"||$this:CRLF|| string-join( for $line in tokenize($metadata, "\n")[. ne ""] return "#"||$line, $this:CRLF )|| $this:CRLF||"ENDHDR"||$this:CRLF (: Some algorithms flip the y coordinates: pure Euclidean vs screen coords :) let $y-range := if ($flipped) then reverse($min-y to $max-y) else ($min-y to $max-y) let $bytes := binary:from-octets( for $y in $y-range for $x in $min-x to $max-x let $rgb := $getrgba($matrix=>matrix:get($x, $y)) for $val in rgb:coordinates($rgb, 4) return ( $val ) ) return ( file:write-text($file, $header), file:append-binary($file, $bytes) ) ) }
Function: pam
declare function pam($file as xs:string,
$metadata as xs:string?,
$matrix as map(*)) as empty-sequence()
declare function pam($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:pam( $file as xs:string, $metadata as xs:string?, $matrix as map(*) (: point matrix: point to colour :) ) as empty-sequence() { this:pam($file, $metadata, $matrix, map {}) }
Function: pam
declare function pam($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 pam($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()*))
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:pam( $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()*) ) { 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 $header := "P7"||$this:CRLF|| "WIDTH "||$columns||$this:CRLF|| "HEIGHT "||$rows||$this:CRLF|| "DEPTH 4"||$this:CRLF|| "MAXVAL 255"||$this:CRLF|| "TUPLTYPE RGB_ALPHA"||$this:CRLF|| string-join( for $line in tokenize($metadata, "\n")[. ne ""] return "#"||$line, $this:CRLF )|| $this:CRLF||"ENDHDR"||$this:CRLF (: 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 $bytes := string-join( for $col in $colours return ( rgb:coordinates($getrgb($col), 3)!string(.) ), " " ) return ( file:write-text($file, document {$header, $bytes}) ) }
Original Source Code
xquery version "3.1"; (:~ : PPM image formats : : Copyright© Mary Holstege 2021-2023 : CC-BY (https://creativecommons.org/licenses/by/4.0/) : @since April 2021 : @custom:Status Stable : @custom:Dependencies EXPath binary, EXPath file :) module namespace this="http://mathling.com/image/ppm"; 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 "../image/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 binary="http://expath.org/ns/binary"; declare namespace file="http://expath.org/ns/file"; declare variable $this:CRLF as xs:string := " "; (:~ : p3-extent() : Read the width and height of the image out of the P3 format. : @param $file: location of PPM file : @return image width and height in that order :) declare function this:p3-extent( $file as xs:string ) as xs:integer* { let $lines := unparsed-text-lines($file)[not(.="") and not(starts-with(.,"#"))] let $width := xs:integer(substring-before($lines[2], " ")) let $height := xs:integer(substring-after($lines[2], " ")) return ($width, $height) }; (:~ : p3-map(): : Read a raw PPM file that uses the ASCII format for image values and : return a colour map. 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 : maximum colour value (255, generally) : r, g, b raw RGB colour values : It returns a colour point : : Format is: : P3 : width height : # comments, if any : 255 (=colour max) : r g b r g b ... :) declare function this:p3-map( $file as xs:string, $options as map(xs:string,item()*), $pixel-function as function(xs:integer, xs:integer, xs:integer, xs:integer, xs:integer, xs:integer) as map(xs:string,item()*) ) as map(*) { let $lines := unparsed-text-lines($file)[not(.="") and not(starts-with(.,"#"))] let $width := xs:integer(substring-before($lines[2], " ")) let $height := xs:integer(substring-after($lines[2], " ")) let $colour-max := xs:integer($lines[3]) let $pixels := ( (: Check format :) if ($lines[1] eq "P3") then () else errors:error("IMAGE-BADFORMAT", $lines[1]), for $line in $lines[position()>3] return (tokenize($line, " ")[not(.="")])!xs:integer(.) ) return ( fold-left( 1 to $height, matrix:matrix(space:space($width,$height)), function($matrix as map(*), $y as xs:integer) as map(*) { fold-left( 1 to $width, $matrix, function($matrix as map(*), $x as xs:integer) as map(*) { let $index := ((($y - 1) * $width + $x) - 1) * 3 let $r := $pixels[$index + 1] let $g := $pixels[$index + 2] let $b := $pixels[$index + 3] return ( if ($x <= $width and $y <= $height) then ( $matrix=>matrix:put($x - 1, $y - 1, $pixel-function($x, $y, $colour-max, $r, $g, $b) ) ) else ( $matrix ) ) } ) } ) ) }; declare function this:p3-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, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value) } case "xyz" return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { cs:rgb-to-xyz(rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value)) } case "hsluv" return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { cs:rgb-to-hsluv(rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value)) } case "lab" return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { cs:rgb-to-lab(rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value)) } case "cmyk" return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { cs:rgb-to-cmyk(rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value)) } default return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value) } return ( this:p3-map($file, $options, $make-colour) ) }; declare function this:p3-map( $file as xs:string ) as map(*) { this:p3-map($file, map {}) }; (:~ : p3-array(): : Read a raw PPM file that uses the ASCII format for image values and : return a colour 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 : maximum colour value (255, generally) : r, g, b raw RGB colour values : It returns a colour point : : Format is: : P3 : width height : # comments, if any : 255 (=colour max) : r g b r g b ... :) declare function this:p3-array( $file as xs:string, $options as map(xs:string,item()*), $pixel-function as function(xs:integer, xs:integer, xs:integer, xs:integer, xs:integer, xs:integer) as map(xs:string,item()*) ) as map(*) { let $lines := unparsed-text-lines($file)[not(.="") and not(starts-with(.,"#"))] let $width := xs:integer(substring-before($lines[2], " ")) let $height := xs:integer(substring-after($lines[2], " ")) let $colour-max := xs:integer($lines[3]) let $pixels := ( (: Check format :) if ($lines[1] eq "P3") then () else errors:error("IMAGE-BADFORMAT", $lines[1]), for $line in $lines[position()>3] return (tokenize($line, " ")[not(.="")])!xs:integer(.) ) let $data := for $y in 1 to $height for $x in 1 to $width let $index := ((($y - 1) * $width + $x) - 1) * 3 let $r := $pixels[$index + 1] let $g := $pixels[$index + 2] let $b := $pixels[$index + 3] return $pixel-function($x, $y, $colour-max, $r, $g, $b) return cmatrix:array($height, $width, $data) }; declare function this:p3-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, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value) } case "xyz" return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { cs:rgb-to-xyz(rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value)) } case "hsluv" return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { cs:rgb-to-hsluv(rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value)) } case "lab" return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { cs:rgb-to-lab(rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value)) } case "cmyk" return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { cs:rgb-to-cmyk(rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value)) } default return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value) } return ( this:p3-array($file, $options, $make-colour) ) }; declare function this:p3-array( $file as xs:string ) as map(*) { this:p3-array($file, map {}) }; (:~ : p3() : Write out a document containing a raw PPM format image using the ASCII : format for image values. Can be converted, processed with GIMP, or viewed : in Emacs but not browser. : : @param $file: where the output is going : @param $metadata: metadata to embed : @param $matrix: the colour map : @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) : : Format is: : P3 : width height : # comments, if any : 255 (=colour max) : r g b r g b ... :) declare function this:p3( $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:p3( $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 $header := "P3"||$this:CRLF|| ($max-x - $min-x + 1)||" "||($max-y - $min-y + 1)||$this:CRLF|| string-join( for $line in tokenize($metadata, "\n")[. ne ""] return "#"||$line, $this:CRLF )||$this:CRLF|| 255||$this:CRLF let $y-range := if ($flipped) then reverse($min-y to $max-y) else ($min-y to $max-y) let $bytes := string-join( for $y in $y-range return ( string-join( for $x in $min-x to $max-x let $rgb := $getrgb($matrix=>matrix:get($x, $y)) return rgb:coordinates($rgb, 3)!string(.), " " ) ), $this:CRLF ) return ( file:write-text($file, document {$header, $bytes}) ) ) }; declare function this:p3( $file as xs:string, $metadata as xs:string?, $matrix as map(*) (: point matrix: point to colour :) ) as empty-sequence() { this:p3($file, $metadata, $matrix, map {}) }; declare function this:p3( $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()*) ) { 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 $header := "P3"||$this:CRLF|| $columns||" "||$rows||$this:CRLF|| string-join( for $line in tokenize($metadata, "\n")[. ne ""] return "#"||$line, $this:CRLF )||$this:CRLF|| 255||$this:CRLF (: 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 $bytes := string-join( for $col in $colours return ( rgb:coordinates($getrgb($col), 3)!string(.) ), " " ) return ( file:write-text($file, document {$header, $bytes}) ) }; (:~ : p6-array(): : Read a raw PPM file that uses the binary format for image values and : return a colour 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 : maximum colour value (255, generally) : r, g, b raw RGB colour values : It returns a colour point : : Format is: : P6 : width height : # comments, if any : 255 (=colour max) : r g b r g b ... (as binary) :) declare function this:p6-array( $file as xs:string, $options as map(xs:string,item()*), $pixel-function as function(xs:integer, xs:integer, xs:integer, xs:integer, xs:integer, xs:integer) as map(xs:string,item()*) ) as map(*) { let $data := file:read-binary($file)=>binary:to-octets() let $linebreaks := ( (0, for $i in 1 to count($data) where $data[$i]=10 (:CR:) return $i) ) let $lines := ( for $i in 1 to count($linebreaks) - 1 return ( if ($linebreaks[$i] + 1 = $linebreaks[$i + 1]) then () else if ($data[$linebreaks[$i]+1] = 35 (:#:)) then () else ( array { $data[position() > $linebreaks[$i] and position() < $linebreaks[$i + 1]] } ) ) ) let $width := xs:integer(substring-before(codepoints-to-string($lines[2]?*), " ")) let $height := xs:integer(substring-after(codepoints-to-string($lines[2]?*), " ")) let $colour-max := xs:integer(codepoints-to-string($lines[3]?*)) let $start-offset := ( for $i in 1 to count($linebreaks) - 1 return ( if ($linebreaks[$i] + 1 = $linebreaks[$i + 1]) then () else if ($data[$linebreaks[$i]+1] = 35 (:#:)) then () else $linebreaks[$i] ) )[4] (: 0 + ^P6 + ^width height + ^255 :) let $pixels := ( (: Check format :) if (codepoints-to-string($lines[1]?*) eq "P6") then () else errors:error("IMAGE-BADFORMAT", $lines[1]), $data[position() > $start-offset] (: octets :) ) let $data := for $y in 1 to $height for $x in 1 to $width let $index := ((($y - 1) * $width + $x) - 1) * 3 let $r := $pixels[$index + 1] let $g := $pixels[$index + 2] let $b := $pixels[$index + 3] return $pixel-function($x, $y, $colour-max, $r, $g, $b) return cmatrix:array($height, $width, $data) }; declare function this:p6-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, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value) } case "xyz" return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { cs:rgb-to-xyz(rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value)) } case "hsluv" return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { cs:rgb-to-hsluv(rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value)) } case "lab" return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { cs:rgb-to-lab(rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value)) } case "cmyk" return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { cs:rgb-to-cmyk(rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value)) } default return function( $x as xs:integer, $y as xs:integer, $max-value as xs:integer, $r as xs:integer, $g as xs:integer, $b as xs:integer ) as map(xs:string,item()*) { rgb:rgb($r div $max-value, $g div $max-value, $b div $max-value) } return ( this:p6-array($file, $options, $make-colour) ) }; declare function this:p6-array( $file as xs:string ) as map(*) { this:p6-array($file, map {}) }; (:~ : p6() : Write out a file containing the raw PPM format image using the : binary format for image values. : Can be converted, processed with GIMP, or viewed in Emacs but not browser. : : @param $file: where the output is going : @param $metadata: metadata to embed : @param $matrix: the colour map : @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) : : Format is: : P6 : width height : # comments, if any : 255 (=colour max) : r g b r g b ... (as binary) :) declare function this:p6( $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:p6( $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 $header := "P6"||$this:CRLF|| ($max-x - $min-x + 1)||" "||($max-y - $min-y + 1)||$this:CRLF|| string-join( for $line in tokenize($metadata, "\n")[. ne ""] return "#"||$line, $this:CRLF )||$this:CRLF|| 255||$this:CRLF (: Some algorithms flip the y coordinates: pure Euclidean vs screen coords :) let $y-range := if ($flipped) then reverse($min-y to $max-y) else ($min-y to $max-y) let $bytes := binary:from-octets( for $y in $y-range for $x in $min-x to $max-x let $rgb := $getrgb($matrix=>matrix:get($x, $y)) for $val in rgb:coordinates($rgb, 3) return ( $val ) ) return ( file:write-text($file, $header), file:append-binary($file, $bytes) ) ) }; declare function this:p6( $file as xs:string, $metadata as xs:string?, $matrix as map(*) (: point matrix: point to colour :) ) as empty-sequence() { this:p6($file, $metadata, $matrix, map {}) }; declare function this:p6( $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()*) ) as empty-sequence() { 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 $header := "P6"||$this:CRLF|| $columns||" "||$rows||$this:CRLF|| string-join( for $line in tokenize($metadata, "\n")[. ne ""] return "#"||$line, $this:CRLF )||$this:CRLF|| 255||$this:CRLF (: 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 $bytes := binary:from-octets( for $col in $colours let $rgb := $getrgb($col) for $val in rgb:coordinates($rgb, 3) return ( $val ) ) return ( file:write-text($file, $header), file:append-binary($file, $bytes) ) }; (:~ : pam() : Write out a file containing raw PAM format image using the binary : format for image values. This format can include alpha values for : each pixel, not just RGB. : : @param $file: where the output is going : @param $metadata: metadata to embed : @param $matrix: the colour map : @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) : : Format is: : P7 : WIDTH width : HEIGHT height : DEPTH 4 : MAXVAL 255 : TUPLTYPE RGB_ALPHA : # comments, if any : ENDHDR : r g b a r g b a ... (as binary) : : ImageMagick can convert this but Emacs integration, GIMP, and browser all : fail on it. :) declare function this:pam( $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:pam( $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 $getrgba := 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 $header := "P7"||$this:CRLF|| "WIDTH "||($max-x - $min-x + 1)||$this:CRLF|| "HEIGHT "||($max-y - $min-y + 1)||$this:CRLF|| "DEPTH 4"||$this:CRLF|| "MAXVAL 255"||$this:CRLF|| "TUPLTYPE RGB_ALPHA"||$this:CRLF|| string-join( for $line in tokenize($metadata, "\n")[. ne ""] return "#"||$line, $this:CRLF )|| $this:CRLF||"ENDHDR"||$this:CRLF (: Some algorithms flip the y coordinates: pure Euclidean vs screen coords :) let $y-range := if ($flipped) then reverse($min-y to $max-y) else ($min-y to $max-y) let $bytes := binary:from-octets( for $y in $y-range for $x in $min-x to $max-x let $rgb := $getrgba($matrix=>matrix:get($x, $y)) for $val in rgb:coordinates($rgb, 4) return ( $val ) ) return ( file:write-text($file, $header), file:append-binary($file, $bytes) ) ) }; declare function this:pam( $file as xs:string, $metadata as xs:string?, $matrix as map(*) (: point matrix: point to colour :) ) as empty-sequence() { this:pam($file, $metadata, $matrix, map {}) }; declare function this:pam( $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()*) ) { 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 $header := "P7"||$this:CRLF|| "WIDTH "||$columns||$this:CRLF|| "HEIGHT "||$rows||$this:CRLF|| "DEPTH 4"||$this:CRLF|| "MAXVAL 255"||$this:CRLF|| "TUPLTYPE RGB_ALPHA"||$this:CRLF|| string-join( for $line in tokenize($metadata, "\n")[. ne ""] return "#"||$line, $this:CRLF )|| $this:CRLF||"ENDHDR"||$this:CRLF (: 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 $bytes := string-join( for $col in $colours return ( rgb:coordinates($getrgb($col), 3)!string(.) ), " " ) return ( file:write-text($file, document {$header, $bytes}) ) };