snibgo's ImageMagick pages

ftxt: formatted text

A coder for reading and writing pixels as formatted text.

IM reads images from files, and writes images into files, using coders. Most image files are binary, but some are text. This page describes a text coder, ftxt.c, that reads and writes pixels as text. Reading and writing text is far slower than binary and files are larger, but text is sometimes useful.

All versions of ImageMagick used on this page have HDRI. %IM7DEV%magick is Q32, so QuantumRange is 232-1 = 4294967295.

The coder has been written for ImageMagick v7. I have not back-ported it to v6.

This page was first published as a proposal for a new IM coder to be named fmttxt. Since then, it has been implemented as a coder named ftxt.

References

Installing custom coders

I use Windows 8.1 with the Gnu toolset fom the Cygwin distribution.

The official documentation (see References above) links to a Magick Coder Kit. The kit contains C code for a custom coder mgk.c and a README.txt. If you follow the instructions in that text file, this should install the coder.

Sadly, it doesn't work for me. At the make stage, it fails with:

mgk.c:42:10: fatal error: MagickCore/studio.h: No such file or directory

I can't figure out how to fix this. I suppose the make process, when it works, pushes the built results from the subdirectory to its parent directory.

The following process works for me. Suppose we want a new coder called FTXT.

  1. In the coders directory, create ftxt.c and ftxt.h. (See the source code below.) The .c file does not #include the .h file.
  2. Edit coders/coders.h.
  3. Edit coders/coders-list.h.
  4. Edit coders/makefile.am.
  5. In the parent directory of coders, rebuild IM: automake, autoconf, ./configure, make, make install.

If we edit a coder, we need just make, make install. But if we add a new coder, we need all five stages of rebuild, which takes about two hours on my computer.

One disadvantage of my process for building is that my custom coders are in the same directory as coders that come with ImageMagick. Hence when a new source version of IM is downloaded and installed, there is no trivial process for re-installing my custom coders.

Check the installation:

set IM7=%FXNEW%

%IM7%magick -list format |grep -i ftxt 
 FTXT* FTXT      rw-   Formatted text image

The installation was successful.

Format defines and escapes

The coder is controlled by four defines:

Define Description Default
ftxt:format The format string for writing and reading. \x,\y:\c
ftxt:chsep A single text character that separates channel values for reading and writing. Comma, ","
ftxt:nummeta The number of meta channels, for reading only. Zero, 0
ftxt:hasalpha Whether the text has an alpha channel, for reading only. false

When writing formatted text, for each pixel the format string is expanded by replacing escapes with appropriate values.

Reading is similar: for each pixel the coder loops through the format string, trying to match escapes with numbers and non-escapes with literal text.

The escape character is backslash, "\". The escape sequences are:

Format Description
\\ backslash character, "\"
\x x-coordinate of pixel
\y y-coordinate of pixel
\c pixel channel values. When writing, format as \v. When reading, accept values formatted as \v or \p or \h or \f.
\v pixel channel values as IM represents them internally (nominal 0 to QuantumRange)
\p pixel channel values normalized to 100 (nominal 0 to 100, with a percent sign)
\o pixel channel values normalized to 1 (nominal 0 to 1). When reading, this format can be read by \o but not \c
\h pixel channel values as separated integer hex, each with leading #
\f pixel channel values as separated floating-point hex, each with leading 0x
\H pixel values as concatenated integer hex, with single leading #
\s pixel values in sparse-colour format
\j junk. Ignored when writing. When reading, ignores all chars until we encounter the character after \j.
\n new line. When reading, match either Unix "\n" or Windows "\r\n".

Don't use \j at the end of the format. There must at least one character after it. If there isn't, the coder raises a fatal error.

Some settings are relevant. They should be placed in the command line before reading or writing.

Setting Description
-precision Relevant only for writing.
-channel Relevant only for writing.
-size Relevant only for reading.
-colorspace Relevant only for reading.

Sample inputs

For demonstration, we make a small image with just eight pixels:

%IM7%magick ^
  -size 2x4 xc: ^
  -sparse-color Bilinear ^
0,0,#000,^
%%[fx:w-1],0,#812,^
0,%%[fx:h-1],#e35,^
%%[fx:w-1],%%[fx:h-1],#ff8 ^
  -evaluate Multiply 0.9999999 ^
  -evaluate Subtract 0.01 ^
  -define quantum:format=floating-point -depth 64 ^
  fmtt_src.miff
fmtt_src.miffpng

Scale it up just so we can see it:

%IM7%magick ^
  fmtt_src.miff ^
  -scale 100x400 ^
  fmtt_src_scl.png
fmtt_src_scl.png

From the small 2x4 image, we make some text files using conventional IM coders. Below, in Reading formatted text, we will read these text files.

txt: format

There are three sets of numbers, separated by two spaces:

  1. In parentheses, in a nominal range of zero to QuantumRange.
  2. "#" then three or four or five hex numbers with no separators. The size of each hex number depends on the current "-depth".
  3. As for the sparse-color: format.
%IM7%magick ^
  fmtt_src.miff txt: 
# ImageMagick pixel enumeration: 2,4,4294967295,srgb
0,0: (-0.0099976156,-0.0099997614,-0.0099992845)  #000000000000000000000000  srgb(-2.3277513e-10%,-2.3282509e-10%,-2.3281399e-10%)
1,0: (2.2906491e+09,2.8633114e+08,5.7266227e+08)  #888888001111110022222200  srgb(53.33333%,6.6666663%,13.333333%)
0,1: (1.336212e+09,2.8633114e+08,4.7721853e+08)  #4FA4FA00111111001C71C6E0  srgb(31.111109%,6.6666663%,11.11111%)
1,1: (2.9587551e+09,1.622543e+09,1.1453245e+09)  #B05B050060B60A8044444400  srgb(68.888885%,37.777773%,26.666665%)
0,2: (2.6724239e+09,5.7266227e+08,9.5443706e+08)  #9F49F4002222220038E38DC0  srgb(62.222219%,13.333333%,22.222219%)
1,2: (3.6268608e+09,2.9587551e+09,1.7179867e+09)  #D82D8100B05B050066666580  srgb(84.444433%,68.888885%,39.999995%)
0,3: (4.0086356e+09,8.5899334e+08,1.4316557e+09)  #EEEEED00333332C055555500  srgb(93.333322%,19.999997%,33.333331%)
1,3: (4.2949668e+09,4.2949668e+09,2.2906491e+09)  #FFFFFE00FFFFFE0088888800  srgb(99.999988%,99.999988%,53.33333%)

sparse-color: format

Colour numbers are either in the nominal range 0 to 255, or percentages of QuantumRange suffixed by a % character. Alpha is in the nominal range 0 to 1. Each pixel is prefixed with a colorspace, optionally suffixed by "a" for alpha, such as "srgb" or "srgba" or "cmyk".

%IM7%magick ^
  fmtt_src.miff sparse-color: 
0,0,srgb(-2.3277513e-10%,-2.3282509e-10%,-2.3281399e-10%) 1,0,srgb(53.33333%,6.6666663%,13.333333%) 0,1,srgb(31.111109%,6.6666663%,11.11111%) 1,1,srgb(68.888885%,37.777773%,26.666665%) 0,2,srgb(62.222219%,13.333333%,22.222219%) 1,2,srgb(84.444433%,68.888885%,39.999995%) 0,3,srgb(93.333322%,19.999997%,33.333331%) 1,3,srgb(99.999988%,99.999988%,53.33333%) 

CAUTION: when an image has an alpha channel, writing in sparse-color format suffixes "a" to the colorspace, such as "srgba", and writes the alpha value. However, when reading this, IM seems to ignore the alpha channel.

debug: format

%IM7%magick ^
  fmtt_src.miff debug: 
# ImageMagick pixel debugging: 2,4,4294967295,srgb
0,0: -0.0099976155906915664673,-0.0099997613579034805298,-0.0099992845207452774048 
1,0: 2290649088,286331136,572662272 
0,1: 1336211968,286331136,477218528 
1,1: 2958755072,1622542976,1145324544 
0,2: 2672423936,572662272,954437056 
1,2: 3626860800,2958755072,1717986688 
0,3: 4008635648,858993344,1431655680 
1,3: 4294966784,4294966784,2290649088 

Writing formatted text

As with other coders, we can either use an extension for the file, or a prefix:

magick in.png out.ftxt
magick in.png FTXT:out.lis
magick in.png FTXT:-

When we don't specify a format, the coder uses a default:

%IM7%magick ^
  fmtt_src.miff ^
  fmtt_fmt1.ftxt
0,0:-0.0099976156,-0.0099997614,-0.0099992845
1,0:2.2906491e+09,2.8633114e+08,5.7266227e+08
0,1:1.336212e+09,2.8633114e+08,4.7721853e+08
1,1:2.9587551e+09,1.622543e+09,1.1453245e+09
0,2:2.6724239e+09,5.7266227e+08,9.5443706e+08
1,2:3.6268608e+09,2.9587551e+09,1.7179867e+09
0,3:4.0086356e+09,8.5899334e+08,1.4316557e+09
1,3:4.2949668e+09,4.2949668e+09,2.2906491e+09

Read that formatted text, and compare to the original:

%IM7%magick ^
  fmtt_src.miff ^
  -size 2x4 fmtt_fmt1.ftxt ^
  -metric RMSE -compare -format "%%[distortion]\n" ^
  info:
4.7820112e-09

Within 6-digit precision, this is zero, so there is no significant difference.

When the format contains ordinary text, the output is that text repeated for every pixel:

%IM7%magick ^
  fmtt_src.miff ^
  -define ftxt:format="Hello World." ^
  fmtt_fmt2.ftxt
Hello World.Hello World.Hello World.Hello World.Hello World.Hello World.Hello World.Hello World.

More usefully, we use \v to get the pixel values. We also use \n for a new line, so each pixel is on its own text line.

%IM7%magick ^
  fmtt_src.miff ^
  -define ftxt:format="Hello World: \v\n" ^
  fmtt_fmt3.ftxt
Hello World: -0.0099976156,-0.0099997614,-0.0099992845
Hello World: 2.2906491e+09,2.8633114e+08,5.7266227e+08
Hello World: 1.336212e+09,2.8633114e+08,4.7721853e+08
Hello World: 2.9587551e+09,1.622543e+09,1.1453245e+09
Hello World: 2.6724239e+09,5.7266227e+08,9.5443706e+08
Hello World: 3.6268608e+09,2.9587551e+09,1.7179867e+09
Hello World: 4.0086356e+09,8.5899334e+08,1.4316557e+09
Hello World: 4.2949668e+09,4.2949668e+09,2.2906491e+09

Normally, the only text we want is some punctuation to assist parsing by computers or humans. In this example, we show the coordinates, and the values as percentages:

%IM7%magick ^
  fmtt_src.miff ^
  -define ftxt:format="\x,\y:\p\n" ^
  fmtt_pc.ftxt
0,0:-2.3277513e-10%,-2.3282509e-10%,-2.3281399e-10%
1,0:53.33333%,6.6666663%,13.333333%
0,1:31.111109%,6.6666663%,11.11111%
1,1:68.888885%,37.777773%,26.666665%
0,2:62.222219%,13.333333%,22.222219%
1,2:84.444433%,68.888885%,39.999995%
0,3:93.333322%,19.999997%,33.333331%
1,3:99.999988%,99.999988%,53.33333%

Read that formatted text, and compare to the original:

%IM7%magick ^
  fmtt_src.miff ^
  -define ftxt:format="\x,\y:\p\n" ^
  -size 2x4 fmtt_pc.ftxt ^
  -metric RMSE -compare -format "%%[distortion]\n" ^
  info:
2.8423345e-09

Show values on a nominal scale of 0 to 1:

%IM7%magick ^
  fmtt_src.miff ^
  -define ftxt:format="\x,\y:\o\n" ^
  fmtt_o.ftxt
0,0:-2.3277513e-12,-2.3282509e-12,-2.3281399e-12
1,0:0.5333333,0.066666663,0.13333333
0,1:0.31111109,0.066666663,0.1111111
1,1:0.68888885,0.37777773,0.26666665
0,2:0.62222219,0.13333333,0.22222219
1,2:0.84444433,0.68888885,0.39999995
0,3:0.93333322,0.19999997,0.33333331
1,3:0.99999988,0.99999988,0.5333333

Read that formatted text, and compare to the original:

%IM7%magick ^
  fmtt_src.miff ^
  -define ftxt:format="\x,\y:\o\n" ^
  -size 2x4 fmtt_o.ftxt ^
  -metric RMSE -compare -format "%%[distortion]\n" ^
  info:
2.8423345e-09

Three hex formats are available:

  1. \h pixel values as hex integers with leading #, each channel separately. For example:
    %IM7%magick ^
      xc:sRGB(10%%,20%%,30%%) ^
      -define "ftxt:format=\x,\y:\h\n" ^
      fmtt_hex1.ftxt
    0,0:#1999999a,#33333333,#4ccccccd

    Read that formatted text, and compare to the original:

    %IM7%magick ^
      xc:sRGB(10%%,20%%,30%%) ^
      -define ftxt:format="\x,\y:\h\n" ^
      -size 1x1 fmtt_hex1.ftxt ^
      -metric RMSE -compare -format "%%[distortion]\n" ^
      info:
    9.5052684e-11
  2. \f pixel values as separated floating-point hex, with leading 0x. For example:
    %IM7%magick ^
      xc:sRGB(10%%,20%%,30%%) ^
      -define "ftxt:format=\x,\y:\f\n" ^
      fmtt_hex2.ftxt
    0,0:0x1.9999999800001p+28,0x1.9999999800001p+29,0x1.3333333200001p+30
    The "p" can be read as "multiplied by 2 to the power of...". This is the C printf "%a" conversion. See C output conversions.

    Read that formatted text, and compare to the original:

    %IM7%magick ^
      xc:sRGB(10%%,20%%,30%%) ^
      -define ftxt:format="\x,\y:\f\n" ^
      -size 1x1 fmtt_hex2.ftxt ^
      -metric RMSE -compare -format "%%[distortion]\n" ^
      info:
    0
  3. \H pixel values as concatenated integer hex, with leading #. For example:
    %IM7%magick ^
      xc:sRGB(10%%,20%%,30%%) ^
      -define "ftxt:format=\x,\y:\H\n" ^
      fmtt_hex3.ftxt
    0,0:#1999999A333333334CCCCCCD

    Read that formatted text, and compare to the original:

    %IM7%magick ^
      xc:sRGB(10%%,20%%,30%%) ^
      -define ftxt:format="\x,\y:\H\n" ^
      -size 1x1 fmtt_hex3.ftxt ^
      -metric RMSE -compare -format "%%[distortion]\n" ^
      info:
    9.5052684e-11

We can repeat values from each pixel multiple times, with different formats, like a "txt:" format:

%IM7%magick ^
  fmtt_src.miff ^
  -define "ftxt:format=\x,\y:(\v)  \H  \s\n" ^
  fmtt_fmt4.ftxt
0,0:(-0.0099976156,-0.0099997614,-0.0099992845)  #000000000000000000000000  srgb(-2.3277513e-10%,-2.3282509e-10%,-2.3281399e-10%)
1,0:(2.2906491e+09,2.8633114e+08,5.7266227e+08)  #888888001111110022222200  srgb(53.33333%,6.6666663%,13.333333%)
0,1:(1.336212e+09,2.8633114e+08,4.7721853e+08)  #4FA4FA00111111001C71C6E0  srgb(31.111109%,6.6666663%,11.11111%)
1,1:(2.9587551e+09,1.622543e+09,1.1453245e+09)  #B05B050060B60A8044444400  srgb(68.888885%,37.777773%,26.666665%)
0,2:(2.6724239e+09,5.7266227e+08,9.5443706e+08)  #9F49F4002222220038E38DC0  srgb(62.222219%,13.333333%,22.222219%)
1,2:(3.6268608e+09,2.9587551e+09,1.7179867e+09)  #D82D8100B05B050066666580  srgb(84.444433%,68.888885%,39.999995%)
0,3:(4.0086356e+09,8.5899334e+08,1.4316557e+09)  #EEEEED00333332C055555500  srgb(93.333322%,19.999997%,33.333331%)
1,3:(4.2949668e+09,4.2949668e+09,2.2906491e+09)  #FFFFFE00FFFFFE0088888800  srgb(99.999988%,99.999988%,53.33333%)

Like a "sparse-color:" format:

%IM7%magick ^
  fmtt_src.miff ^
  -define "ftxt:format=\x,\y,\s " ^
  fmtt_fmt5.ftxt
0,0,srgb(-2.3277513e-10%,-2.3282509e-10%,-2.3281399e-10%) 1,0,srgb(53.33333%,6.6666663%,13.333333%) 0,1,srgb(31.111109%,6.6666663%,11.11111%) 1,1,srgb(68.888885%,37.777773%,26.666665%) 0,2,srgb(62.222219%,13.333333%,22.222219%) 1,2,srgb(84.444433%,68.888885%,39.999995%) 0,3,srgb(93.333322%,19.999997%,33.333331%) 1,3,srgb(99.999988%,99.999988%,53.33333%) 

Like a "debug:" format:

%IM7%magick ^
  fmtt_src.miff ^
  -define "ftxt:format=\x,\y,\v " ^
  fmtt_fmt6.ftxt
0,0,-0.0099976156,-0.0099997614,-0.0099992845 1,0,2.2906491e+09,2.8633114e+08,5.7266227e+08 0,1,1.336212e+09,2.8633114e+08,4.7721853e+08 1,1,2.9587551e+09,1.622543e+09,1.1453245e+09 0,2,2.6724239e+09,5.7266227e+08,9.5443706e+08 1,2,3.6268608e+09,2.9587551e+09,1.7179867e+09 0,3,4.0086356e+09,8.5899334e+08,1.4316557e+09 1,3,4.2949668e+09,4.2949668e+09,2.2906491e+09 

A file with this format stores only one image, so multiple inputs create multiple outputs:

%IM7%magick ^
  fmtt_src.miff ^
  ( +clone -evaluate Multiply 2 ) ^
  -define "ftxt:format=\x,\y,\s " ^
  fmtt_fmt7.ftxt

This has created two output files:

dir /b fmtt_fmt7*.ftxt >fmtt_fmt7.lis
fmtt_fmt7-0.ftxt
fmtt_fmt7-1.ftxt

These are the two created files:

0,0,srgb(-2.3277513e-10%,-2.3282509e-10%,-2.3281399e-10%) 1,0,srgb(53.33333%,6.6666663%,13.333333%) 0,1,srgb(31.111109%,6.6666663%,11.11111%) 1,1,srgb(68.888885%,37.777773%,26.666665%) 0,2,srgb(62.222219%,13.333333%,22.222219%) 1,2,srgb(84.444433%,68.888885%,39.999995%) 0,3,srgb(93.333322%,19.999997%,33.333331%) 1,3,srgb(99.999988%,99.999988%,53.33333%) 
0,0,srgb(-4.6555025e-10%,-4.6565017e-10%,-4.6562797e-10%) 1,0,srgb(106.66666%,13.333333%,26.666665%) 0,1,srgb(62.222219%,13.333333%,22.222219%) 1,1,srgb(137.77777%,75.555545%,53.33333%) 0,2,srgb(124.44444%,26.666665%,44.444439%) 1,2,srgb(168.88887%,137.77777%,79.999989%) 0,3,srgb(186.66664%,39.999995%,66.666663%) 1,3,srgb(199.99998%,199.99998%,106.66666%) 

The format can store many channels:

%IM7%magick ^
  xc:sRGB(10%%,20%%,30%%) ^
  -channel-fx "1=>9" -channel-fx "0<=>9" -channel-fx 4=50%% ^
  -channel 3 -fx u.r+u.g+u.b +channel ^
  -channel 0,1,2,3,4,5,6,7,8,9 ^
  -evaluate Multiply 1.5 ^
  -define "ftxt:format=\x,\y:(\p\v) \n" ^
  fmtt_mult.ftxt
0,0:(3%,3%,4.5%,0%1.2884902e+08%,1.2884902e+08%,1.9327353e+08%,0%) 
 0,0:(i=0 ch=0 tr=6
3.33333i=1 ch=1 tr=6
 -5.78333e+138i=2 ch=2 tr=6
 -8.675e+138i=3 ch=3 tr=6
 -nani=4 ch=4 tr=2
 8.33333i=5 ch=5 tr=6
 -nani=6 ch=6 tr=6
 -nani=7 ch=7 tr=6
 -nani=8 ch=8 tr=6
 4.64233e-162i=9 ch=9 tr=1
i=10 ch=4 tr=2
 8.33333i=11 ch=9 tr=1
)

The separator beween channel values can be changed. The separator should not be a character that can appear in decimal or hex floating-point numbers, so not one of "0123456789abcdefABCDEFnxpNXP%.+-". To ensure the output can be easily parsed, the channel separator should be different to whatever character comes after channel values, which might be "\n".

In this example, we don't output the x- and y-coordinates. When the formatted text is read, the pixels will be set starting at top-left, row by row.

%IM7%magick ^
  fmtt_src.miff ^
  -precision 15 ^
  -define ftxt:chsep=";" ^
  -define ftxt:format="\v\n" ^
  fmtt_fmt8.ftxt
-0.00999761559069157;-0.00999976135790348;-0.00999928452074528
2290649088;286331136;572662272
1336211968;286331136;477218528
2958755072;1622542976;1145324544
2672423936;572662272;954437056
3626860800;2958755072;1717986688
4008635648;858993344;1431655680
4294966784;4294966784;2290649088

Read that formatted text, and compare to the original:

%IM7%magick ^
  fmtt_src.miff ^
  -define ftxt:chsep=";" ^
  -define ftxt:format="\v\n" ^
  -size 2x4 fmtt_fmt8.ftxt ^
  -metric RMSE -compare -format "%%[distortion]\n" ^
  info:
1.8435282e-28

When the input image has alpha values, we can choose whether or not to write them, with -channel RGBA (the default) or -channel RGB.

Create a formatted text image with alpha:

%IM7%magick ^
  xc:srgba(10%%,20%%,30%%,0.4) ^
  -define ftxt:format="\p\n" ^
  fmtt_alp1.ftxt
10%,20%,30%,40.000001%

Read the formatted text file:

%IM7%magick ^
  -size 1x1 ^
  -define ftxt:format="\p\n" ^
  fmtt_alp1.ftxt ^
  txt: >fmtt_alp1.lis 2^>^&1
magick: NumChannelsError `fmtt_alp1.ftxt' @ error/ftxt.c/ReadFTXTImage/552.

Oops: the coder has failed with an error. It counted channels starting from zero and reached number three, so it found four channels, but expected only three.

To tell the coder that the formatted text file contains an alpha, we use a define:

%IM7%magick ^
  -size 1x1 ^
  -define ftxt:format="\p\n" ^
  -define ftxt:hasalpha=true ^
  fmtt_alp1.ftxt ^
  txt: >fmtt_alp2.lis 2^>^&1
# ImageMagick pixel enumeration: 1,1,4294967295,undefineda
0,0: (4.2949674e+08,8.5899347e+08,1.2884902e+09,1.7179869e+09)  #1999999A333333334CCCCCCD66666691  undefineda(25.5,51.000001,76.500003,0.40000001)

That's better. The coder has found the expected number of channels, and the command completes without an error.

Reading formatted text

Like other coders, reading a file of this format creates a new image. It requires a -size before reading the file. If no size is given, the coder raises an error.

If "-colorspace" is specified before reading the file, that will be used for the expected number of channels, for example -colorspace CMYK if we know the file has those channels. If "-colorspace" is not specified, the colorspace will be "undefined", with three expected channels.

The -channel option cannot be used before reading the file.

The coder is very fussy when reading formatted text. For example, if the format ends with "\n" then every pixel entry must end with a newline (either "\n" or "\r\n"). The final pixel entry and thus the file must end with a single newline. Exactly one newline. Not zero, and not two or more.

When the coder reads the file, the string "\n" in the format will swallow the single character '\n' or the two-character sequence "\r\n" from the input file, to cope with Windows line-endings.

On error, or when "-verbose" is in effect, the coder reports the number of pixels, and the maximum X,Y found.

If the file contains more pixels than is correct for the -size setting, or any X- or Y-coordinates exceed the image boundary, the coder raises a warning but still creates an image of the required -size.

The formatted text file may contain X- and Y-coordinates for each pixel, or just the X-coordinate, or just the Y-coordinate, or neither.

When the coder reads the file, it applies the following rules:

When X and Y are provided, they do not need to be in sequential order, and not all pixels need to be represented. Any pixels that are not represented will be set to all-zero.

Strings like "0.5" have nothing to say whether they are on a scale 0 to 1 (format "\o"), or 0 to QuantumRange (format "\v"). The \c format assumes "0.5" is a "\v" number.

Some formatted text can be written that cannot be unambiguously parsed. For example:

%IM7%magick ^
  fmtt_src.miff ^
  -precision 15 -depth 8 ^
  -define ftxt:chsep="" ^
  -define ftxt:format=\v ^
  ftxt:
00022906492242863311535726623061330597711286331153471604252296436958416169288641145324612267803843157266230696005151336212469352964369584171798691840086361428589934591431655765429496729542949672952290649224

The output is a long unbroken string of digits, which cannot be parsed. The coder does not check for unparseable formats.

There is a fundamental difference between the \H format "#def" and the \h format "#d,#e,#f":

Reading txt: format

We use the common utility "tail" to ignore the first line, which is the header:

tail --lines=+2 fmtt_txt.lis >fmtt_txt_noh.lis

If we read all three txt: formats on each line of the input, the value from second will overwrite the first, and the third will overwrite the second. The result will depend on only the third set of values.

In these examples, we compare the values read from the formatted text file with the original file.

%IM7%magick ^
  fmtt_src.miff ^
  -size 2x4 ^
  -define ftxt:format="\x,\y: (\c)  \H  \s\n" ^
  ftxt:fmtt_txt_noh.lis ^
  -metric RMSE -compare -format "%%[distortion]\n" ^
  info:
1.400171e-08

So this wastes effort. Insead, we can read just the first format, then use \j to skip over the other input characters.

%IM7%magick ^
  fmtt_src.miff ^
  -size 2x4 ^
  -define ftxt:format="\x,\y: (\c)\j\n" ^
  ftxt:fmtt_txt_noh.lis ^
  -metric RMSE -compare -format "%%[distortion]\n" ^
  info:
4.7820112e-09

We could read just the hex values, ignoring the first and third formats:

%IM7%magick ^
  fmtt_src.miff ^
  -size 2x4 ^
  -define ftxt:format="\x,\y: (\j)  \H  \j\n" ^
  ftxt:fmtt_txt_noh.lis ^
  -metric RMSE -compare -format "%%[distortion]\n" ^
  info:
8.2308903e-13

The hex value in the \H format does not record fractional values, nor values outside the range zero to QuantumRange.

Reading sparse-color: format

This format has no header, so we can read it directly. After each pixel there is a space character, rather than a \n character.

%IM7%magick ^
  fmtt_src.miff ^
  -size 2x4 ^
  -define ftxt:format="\x,\y,\s " ^
  ftxt:fmtt_sps.lis ^
  -metric RMSE -compare -format "%%[distortion]\n" ^
  info:
1.400171e-08

When IM writes to a sparse-color: format, it writes only those pixels that are fully opaque. We won't generally know how many pixels are opaque, so we can't set an appropriate -size. The solution is to read the output from sparse-color: twice. The first time use -size 1x1. If there was more than one pixel, the coder will send a warning to stderr. It is just a warning, and a 1x1 image will be created from the first pixel. If there were no pixels (because all the pixels had transparency), the coder would raise an error. We want to ignore the x- and y-coordinates, so we use "\j,\j" instead of "\x,\y".

%IM7%magick ^
  -size 1x1 ^
  -define ftxt:format="\j,\j,\s " ^
  ftxt:fmtt_sps.lis ^
  fmtt_sps_out1.png 
magick: TooManyPixels `fmtt_sps.lis' @ warning/ftxt.c/ReadFTXTImage/556.

So a Windows BAT script that does this processing is:

set nPix=0

for /F "usebackq tokens=1,2 delims== " %%A in (`%IM7%magick ^
  -size 1x1 ^
  -define "ftxt:format=\j,\j,\s " ^
  ftxt:fmtt_sps.lis ^
  NULL: 2^>^&1`) do (
    echo %%A,%%B
    if "%%A"=="nPix" set nPix=%%B
  )

if %nPix%==0 (
  echo No pixels were opaque
) else (
  echo %nPix% pixels were opaque. Creating %nPix%x1 image.

  %IM7%magick ^
    -size %nPix%x1 ^
    -define "ftxt:format=\j,\j,\s " ^
    ftxt:fmtt_sps.lis ^
    fmtt_sps_out2.png
)

This is the created file:

%IM7%magick ^
  fmtt_sps_out2.png ^
  txt: 
# ImageMagick pixel enumeration: 8,1,65535,srgb
0,0: (0,0,0)  #000000000000  black
1,0: (2.2906493e+09,2.8633117e+08,5.7266234e+08)  #888811112222  srgb(136.00001,17.000001,34.000002)
2,0: (1.3362339e+09,2.8633117e+08,4.7724045e+08)  #4FA511111C72  srgb(31.111619%,6.666667%,11.11162%)
3,0: (2.9587333e+09,1.622565e+09,1.1453247e+09)  #B05A60B64444  srgb(68.888378%,37.778285%,26.666668%)
4,0: (2.6724022e+09,5.7266234e+08,9.5441536e+08)  #9F49222238E3  srgb(62.221712%,13.333334%,22.221714%)
5,0: (3.6268831e+09,2.9587333e+09,1.7179869e+09)  #D82DB05A6666  srgb(84.444952%,68.888378%,40.000001%)
6,0: (4.0086362e+09,8.5899347e+08,1.4316558e+09)  #EEEE33335555  srgb(238,51.000001,85.000003)
7,0: (4.2949673e+09,4.2949673e+09,2.2906493e+09)  #FFFFFFFF8888  srgb(255,255,136.00001)

As expected, it has 8x1 pixels.

Reading debug: format

This format has a header similar to the txt: format.

tail --lines=+2 fmtt_dbg.lis >fmtt_dbg_noh.lis

%IM7%magick ^
  fmtt_src.miff ^
  -size 2x4 ^
  -define ftxt:format="\x,\y: \v\n" ^
  ftxt:fmtt_dbg_noh.lis ^
  -metric RMSE -compare -format "%%[distortion]\n" ^
  info:
0

The result is exactly correct.

Reading grayscale files

An example with no alpha channel:

echo #12345678>fmtt_gry.lis
echo 0x12345678>>fmtt_gry.lis

%IM7%magick ^
  -size 2x1 ^
  -colorspace Gray ^
  -define ftxt:format="\c\n" ^
  ftxt:fmtt_gry.lis ^
  txt:
# ImageMagick pixel enumeration: 2,1,4294967295,gray
0,0: (3.054199e+08)  #123456781234567812345678  gray(7.1111113%)
1,0: (3.054199e+08)  #123456781234567812345678  gray(7.1111113%)

An example with an alpha channel:

echo #12345678,0>fmtt_gry2.lis
echo 0x12345678,40%%>>fmtt_gry2.lis

%IM7%magick ^
  -size 2x1 ^
  -colorspace Gray ^
  -define ftxt:hasalpha=true ^
  -define ftxt:format="\c\n" ^
  ftxt:fmtt_gry2.lis ^
  txt:
# ImageMagick pixel enumeration: 2,1,4294967295,graya
0,0: (3.054199e+08,1.7179869e+09)  #12345678123456781234567866666666  graya(7.1111113%,0.40000001)
1,0: (0,0)  #00000000000000000000000000000000  graya(0,0)

Reading mixed-format pixel values

When the format contains \v, \p, \h or \f, all values that are read for that escape must be of the correct type. If a value is not the correct type, an error is raised. However, the format \c can be used to automatically determine the correct type from a prefix or suffix to the number:

For example:

echo #12345678,20%%,234567>fmtt_mix.lis
echo 0x12345678,30%%,40>>fmtt_mix.lis

%IM7%magick ^
  -size 2x1 ^
  -define ftxt:format="\c\n" ^
  ftxt:fmtt_mix.lis ^
  txt:
# ImageMagick pixel enumeration: 2,1,4294967295,undefined
0,0: (3.054199e+08,8.5899347e+08,234567)  #123456783333333300039447  undefined(18.133334,51.000001,0.013926668)
1,0: (3.054199e+08,1.2884902e+09,40)  #123456784CCCCCCD00000028  undefined(18.133334,76.500003,2.3748726e-06)

Reading meta channels

CAUTION: as ImageMagick handling of meta channels evolves, this coder may change.

Create a formatted text file with eleven channels:

echo 0%%,-10%%,20%%,30%%,40%%,50%%,60%%,70%%,80%%,90%%,101%%>fmtt_mch.lis

Read that formatted text file, and write it in ftxt: format. We need to declare the number of meta channels. We leave the colorspace as undefined.

%IM7%magick ^
  -size 1x1 -define ftxt:format="\c\n" -define ftxt:nummeta=8 ftxt:fmtt_mch.lis ^
  -define ftxt:format="\p\n" ^
  ftxt: 
0%,-10%,20%,30%,40%,50%,60%,70%,80%,90%,101%

All channels have been written to the output.

Note that when writing formatted text, we don't need to declare the number of meta channels. If the channel is selected (by the "trait") then it will be written.

Read the file again, and write it in txt: format:

%IM7%magick ^
  -size 1x1 -define ftxt:format="\c\n" -define ftxt:nummeta=8 ftxt:fmtt_mch.lis ^
  txt: 
# ImageMagick pixel enumeration: 1,1,4294967295,undefined
0,0: (0,-4.2949674e+08,8.5899347e+08)  #000000000000000033333333  undefined(0,-25.5,51.000001)

In the "txt:" format, only the first three channels, and the fifth channel, have been written to the output. Why not the fourth channel? Because that would be the "K" channel of "CMYK", and this image isn't encoded as CMYK. "-verbose info:" does list all the channels.

We can output just some channels:

%IM7%magick ^
  -size 1x1 -define ftxt:format="\c\n" -define ftxt:nummeta=8 ftxt:fmtt_mch.lis ^
  -channel 4,6,8,1,2 ^
  -define ftxt:format="\p\n" ^
  ftxt: 
-10%,20%

The order given in -channel is ignored. The channels are written in numerical order.

The MPC format can record the meta channels:

%IM7%magick ^
  -size 1x1 -define ftxt:format="\c\n" -define ftxt:nummeta=8 ftxt:fmtt_mch.lis ^
  fmtt_mch_4.mpc

%IM7%magick ^
  fmtt_mch_4.mpc ^
  -define ftxt:format="\p\n" ^
  ftxt: 
0%,-10%,20%,30%,40%,50%,60%,70%,80%,90%,101%

Performance

This coder reads and writes pixels values as text. Inevitably this is far slower than reading and writing binary numbers. In addition, when reading the text, the pixels can occur in any order, with any gaps, so the code cannot process entire rows together.

The x- and y-coordinates are often superfluous. For best performance, if they are not needed, then don't write or read them.

Possible future

The formatted text is currently written without headers. A consequence is that a reader of that file needs to know the image dimensions, the colorspace, the number of channels, the format string that wrote the data, and the separator character. A possible future enhancement would be to write that metadata in a header. If we want to retain flexibility, this would add complexity when reading: perhaps file metadata would be overridden by options such as "-size", and perhaps each file metadata element would be individually optional.

Another consequence is that IM will not automatically recognise that a file is in the "FTXT" format. We need to explicitly tell IM that the file is in this format, either by file extension or by prefix.

When a formatted text file that contains meta channels is read, we need to declare the number of meta channels. In principle, the coder might count the actual number of values in the text and adjust the number of meta channels as required. But this would mean making the adjustment after values have already been read, which seems unwise.

The coder has no way of naming channels, such as "green" or "meta 2". This might change.

When reading formatted text, instead of the "hasalpha" define, we might assume that if we find exactly one channel more than expected from the colourspace, that the extra channel is alpha. But it might be a meta channel, so being explicit seems wiser. A better solution would be if IM accepted an "a" suffix to colorspaces, such as "-colorspace sRGBa".

Source code

For convenience, .bat scripts are also available in a single zip file. See Zipped BAT files.

ftxt.h

#include "coders/coders-private.h"

#define MagickFTXTHeaders

#define MagickFTXTAliases

#if defined(__cplusplus) || defined(c_plusplus)
extern "C" {
#endif

MagickCoderExports(FTXT)

#if defined(__cplusplus) || defined(c_plusplus)
}
#endif

ftxt.c

/*
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%                                                                             %
%                                                                             %
%                                                                             %
%                        FFFFF  TTTTT  X   X  TTTTT                           %
%                        F        T     X X     T                             %
%                        FFF      T      X      T                             %
%                        F        T     X X     T                             %
%                        F        T    X   X    T                             %
%                                                                             %
%                 Read and Write Pixels as Formatted Text                     %
%                                                                             %
%                               Software Design                               %
%                             snibgo (Alan Gibson)                            %
%                                 October 2021                                %
%                                                                             %
%                                                                             %
%                                                                             %
%  Copyright 1999-2022 ImageMagick Studio LLC, a non-profit organization      %
%  dedicated to making software imaging solutions freely available.           %
%                                                                             %
%  You may not use this file except in compliance with the License.  You may  %
%  obtain a copy of the License at                                            %
%                                                                             %
%    https://imagemagick.org/script/license.php                               %
%                                                                             %
%  Unless required by applicable law or agreed to in writing, software        %
%  distributed under the License is distributed on an "AS IS" BASIS,          %
%  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.   %
%  See the License for the specific language governing permissions and        %
%  limitations under the License.                                             %
%                                                                             %
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
%
%
*/

#include <MagickCore/studio.h>
#include <MagickCore/artifact.h>
#include <MagickCore/attribute.h>
#include <MagickCore/blob.h>
#include "MagickCore/blob-private.h"
#include <MagickCore/cache.h>
#include <MagickCore/channel.h>
#include <MagickCore/colorspace.h>
#include <MagickCore/exception.h>
#include "MagickCore/exception-private.h"
#include <MagickCore/image.h>
#include "MagickCore/image-private.h"
#include <MagickCore/list.h>
#include <MagickCore/magick.h>
#include <MagickCore/memory_.h>
#include <MagickCore/module.h>
#include <MagickCore/monitor.h>
#include "MagickCore/monitor-private.h"
#include <MagickCore/pixel-accessor.h>
#include "MagickCore/quantum-private.h"
#include <MagickCore/string_.h>
/*
  Forward declaration.
*/
static MagickBooleanType
  WriteFTXTImage(const ImageInfo *,Image *,ExceptionInfo *);


typedef enum {
  vtAny,
  vtQuant,
  vtPercent,
  vtProp,
  vtIntHex,
  vtFltHex
} ValueTypeT;

#define chEsc '\\'

#define dfltChSep ","

#define dfltFmt "\\x,\\y:\\c\\n"


static MagickBooleanType IsFTXT(const unsigned char *magick,const size_t length)
{
  if (length < 10)
    return(MagickFalse);
  if (LocaleNCompare((char *) magick,"id=ftxt",10) == 0)
    return(MagickTrue);
  return(MagickFalse);
}



static int ReadChar (Image * image, int * chPushed)
{
  int ch;
  if (*chPushed) {
    ch = *chPushed;
    *chPushed = 0;
  } else {
    ch = ReadBlobByte (image);
  }
  return ch;
}

static int ReadInt (Image * image, MagickBooleanType *eofInp, int * chPushed, MagickBooleanType *err)
{
  char
    buffer[MaxTextExtent];

  char * p = buffer;

  int chIn = ReadChar (image, chPushed);
  if (chIn == EOF) *eofInp = MagickTrue;

  while (isdigit (chIn)) {
    *p = chIn;
    p++;
    if (p-buffer >= MaxTextExtent) {
      *eofInp = MagickTrue;
      continue;
    }
    chIn = ReadChar (image, chPushed);
  }
  if (p==buffer) {
    *eofInp = MagickTrue;
    return 0;
  }
  if (*eofInp) {
    *chPushed = '\0';
    return 0;
  }
  *p = '\0';
  *chPushed = chIn;

  char * tail;
  errno = 0;
  int val = strtol (buffer, &tail, 10);
  if (errno || *tail) {
    *eofInp = MagickTrue;
    *err = MagickTrue;
  }

  if (val < 0) {
    *err = MagickTrue;
  }

  return (val);
}

static long double BufToFlt (char * buffer, char ** tail, ValueTypeT expectType, MagickBooleanType *err)
{
  *err = MagickFalse;
  long double val = 0;

  if (*buffer == '#') {
    // read hex integer
    char * p = buffer + 1;
    while (*p) {
      short v;
      if (*p >= '0' && *p <= '9') v = *p - '0';
      else if (*p >= 'a' && *p <= 'f') v = *p - 'a' + 10;
      else if (*p >= 'A' && *p <= 'F') v = *p - 'A' + 10;
      else break;
      val = val * 16 + v;
      p++;
    }
    *tail = p;
    if (expectType != vtAny && expectType != vtIntHex) *err = MagickTrue;
  } else if (*buffer == '0' && *(buffer+1)=='x') {
    // read hex floating-point
    val = strtold (buffer, tail);
    if (expectType != vtAny && expectType != vtFltHex) *err = MagickTrue;
  } else {
    // Read decimal floating-point (possibly a percent).
    errno = 0;
    val = strtold (buffer, tail);
    if (errno) {
      *err = MagickTrue;
    }
    if (**tail=='%') {
      (*tail)++;
      val *= QuantumRange / 100.0;
      if (expectType != vtAny && expectType != vtPercent) *err = MagickTrue;
    } else {
      if (expectType == vtPercent) *err = MagickTrue;
    }
  }

  return (val);
}

static void SkipUntil (Image * image, int UntilChar, MagickBooleanType *eofInp, int * chPushed)
{
  int chIn = ReadChar (image, chPushed);
  if (chIn == EOF) {
    *eofInp = MagickTrue;
    *chPushed = '\0';
    return;
  }

  while (chIn != UntilChar && chIn != EOF) {
    chIn = ReadChar (image, chPushed);
  }
  if (chIn == EOF) {
    *eofInp = MagickTrue;
    *chPushed = '\0';
    return;
  }
  *chPushed = chIn;
}

static void ReadUntil (Image * image, int UntilChar, MagickBooleanType *eofInp, int * chPushed,
  char * buf, int BufSiz)
{
  int chIn;
  int i=0;

  for (;;) {
    chIn = ReadChar (image, chPushed);
    if (chIn == EOF) {
      if (i==0) *eofInp = MagickTrue;
      break;
    }
    if (chIn == UntilChar) break;
    if (i >= BufSiz) {
      *eofInp = MagickTrue;
      break;
    }
    buf[i++] = chIn;
  }

  if (*eofInp) {
    *chPushed = '\0';
  }
  else *chPushed = chIn;
  buf[i] = '\0';
  if (UntilChar=='\n' && i>0 && buf[i-1]=='\r') buf[i-1]='\0';
}

static Image *ReadFTXTImage(const ImageInfo *image_info,ExceptionInfo *exception)
{
  char
    buffer[MaxTextExtent];

  Image
    *image;

  MagickBooleanType
    status;

  Quantum
    *q;

  assert(image_info != (const ImageInfo *) NULL);
  assert(image_info->signature == MagickCoreSignature);
  if (image_info->debug != MagickFalse)
    (void) LogMagickEvent(TraceEvent,GetMagickModule(),"%s",
      image_info->filename);
  assert(exception != (ExceptionInfo *) NULL);
  assert(exception->signature == MagickCoreSignature);
  image=AcquireImage(image_info,exception);
  status=OpenBlob(image_info,image,ReadBinaryBlobMode,exception);
  if (status == MagickFalse)
    {
      image=DestroyImageList(image);
      return((Image *) NULL);
    }
  SetImageColorspace (image, RGBColorspace, exception);
  SetImageColorspace (image, image_info->colorspace, exception);
  const char * sFmt = GetImageArtifact (image, "ftxt:format");
  if (sFmt == NULL) sFmt = dfltFmt;

  const char * sChSep = GetImageArtifact (image, "ftxt:chsep");
  if (sChSep == NULL) sChSep = dfltChSep;

  char chSep;
  if (sChSep[0]==chEsc && (sChSep[1] == 'n' || sChSep[1] == 'N')) chSep = '\n';
  else chSep = sChSep[0];

  MagickBooleanType hasAlpha = IsStringTrue (GetImageArtifact (image, "ftxt:hasalpha"));

  int numMeta = 0;
  const char * sNumMeta = GetImageArtifact (image, "ftxt:nummeta");
  if (sNumMeta != NULL) numMeta = atoi(sNumMeta);

  if (hasAlpha) {
    if (!SetImageAlphaChannel (image, OpaqueAlphaChannel, exception))
      ThrowReaderException(OptionError,"SetImageAlphaChannelFailure");
  }

  if (numMeta) {
    if (!SetPixelMetaChannels (image, numMeta, exception))
      ThrowReaderException(OptionError,"SetPixelMetaChannelsFailure");
  }

  // make image zero (if RGB channels, transparent black).
  PixelInfo mppBlack;
  GetPixelInfo (image, &mppBlack);
  if (hasAlpha) mppBlack.alpha = TransparentAlpha;
  SetImageColor (image, &mppBlack, exception);

  char
    procFmt[MaxTextExtent];

  const char * pf = sFmt;
  char * ppf = procFmt;

  int i = 0;
  while (*pf) {
    if (*pf == chEsc) {
      pf++;
      switch (*pf) {
        case chEsc:
          if (++i >= MaxTextExtent) ThrowReaderException (DelegateFatalError, "ppf bust");
          *ppf = chEsc;
          ppf++;
          break;
        case 'n':
          if (++i >= MaxTextExtent) ThrowReaderException (DelegateFatalError, "ppf bust");
          *ppf = '\n';
          ppf++;
          break;
        case 'j':
          if (*(pf+1)=='\0') ThrowReaderException (DelegateFatalError, "EscapeJproblem");
          // Drop through...
        default:
          if ((i+=2) >= MaxTextExtent) ThrowReaderException (DelegateFatalError, "ppf bust");
          *ppf = chEsc;
          ppf++;
          *ppf = *pf;
          ppf++;
          break;
      }
    } else {
      // Not escape
      if (++i >= MaxTextExtent) ThrowReaderException (DelegateFatalError, "ppf bust");
      *ppf = *pf;
      ppf++;
    }
    pf++;
  }
  *ppf = '\0';

  if ((image->columns == 0) || (image->rows == 0))
    ThrowReaderException(OptionError,"MustSpecifyImageSize");

  // How many channel values can we expect?
  int nExpCh = 0;
  for (i=0; i < (ssize_t) GetPixelChannels (image); i++) {
    PixelChannel channel = GetPixelChannelChannel (image, i);
    PixelTrait traits = GetPixelChannelTraits (image, channel);
    if ((traits & UpdatePixelTrait) != UpdatePixelTrait) continue;
    nExpCh++;
  }

  long double chVals[MaxPixelChannels];
  for (i=0; i < MaxPixelChannels; i++) chVals[i] = 0;

  MagickBooleanType eofInp = MagickFalse;

  int chIn;
  int chPushed = 0;

  ssize_t x=0, y=0, MaxX=-1, MaxY=-1;
  ssize_t nPix = 0;

  MagickBooleanType
    firstX = MagickTrue,
    firstY = MagickTrue,
    IntErr = MagickFalse,
    TypeErr = MagickFalse,
    nChErr = MagickFalse;

  while (!eofInp) {
    ValueTypeT expectType = vtAny;
    ppf = procFmt;
    while (*ppf && !eofInp) {
      if (*ppf == chEsc) {
        ppf++;
        switch (*ppf) {
          case 'x': {
            x = ReadInt (image, &eofInp, &chPushed, &IntErr);
            if (IntErr || eofInp) continue;
            if (firstX) {
              firstX = MagickFalse;
              MaxX = x;
            } else if (MaxX < x) MaxX = x;
            break;
          }
          case 'y': {
            y = ReadInt (image, &eofInp, &chPushed, &IntErr);
            if (IntErr || eofInp) continue;
            if (firstY) {
              firstY = MagickFalse;
              MaxY = y;
            } else if (MaxY < y) MaxY = y;
            break;
          }

          case 'c':
          case 'v':
          case 'p':
          case 'o':
          case 'h':
          case 'f':
          {
            if (*ppf=='c') {
              expectType = vtAny;
            } else if (*ppf=='v') {
              expectType = vtQuant;
            } else if (*ppf=='p') {
              expectType = vtPercent;
            } else if (*ppf=='o') {
              expectType = vtProp;
            } else if (*ppf=='h') {
              expectType = vtIntHex;
            } else if (*ppf=='f') {
              expectType = vtFltHex;
            }
            /* Read chars until next char in format,
               then parse that string into chVals[],
               then write that into image.
            */
            int UntilChar = *(ppf+1);
            ReadUntil (image, UntilChar, &eofInp, &chPushed, buffer, MaxTextExtent-1);
            if (eofInp) break;

            char * tail;
            char * pt = buffer;
            long double val;
            i = 0;
            for (;;) { // Loop through input channels.
              val = BufToFlt (pt, &tail, expectType, &TypeErr);
              if (expectType == vtProp) val *= QuantumRange;
              if (TypeErr) {
                break;
              }

              if (i < MaxPixelChannels) chVals[i] = val;

              if (*tail=='\r' && chSep=='\n' && *(tail+1)=='\n') tail++;

              if (*tail == chSep) {
                pt = tail+1;
              } else {
                break;
              }
              i++;
            }

            if (i+1 != nExpCh) {
              nChErr = MagickTrue;
            }

            if (x < (ssize_t) image->columns && y < (ssize_t) image->rows) {
              q = QueueAuthenticPixels (image, x,y, 1,1, exception);
              if (!q) break;
              for (i=0; i< nExpCh; i++) {
                q[i] = chVals[i];
              }
              if (!SyncAuthenticPixels (image,exception)) break;
            }

            break;
          }
          case 'j':
            // Skip chars until we find char after *ppf.
            SkipUntil (image, *(ppf+1), &eofInp, &chPushed);
            break;
          case 'H':
          case 's': {
            int UntilChar = *(ppf+1);
            ReadUntil (image, UntilChar, &eofInp, &chPushed, buffer, MaxTextExtent-1);
            if (eofInp) break;
            if (!*buffer) {
              ThrowReaderException(CorruptImageError,"No input for escape 'H' or 's'.");
            }
            PixelInfo pixelinf;
            if (!QueryColorCompliance(buffer, AllCompliance, &pixelinf, exception)) {
              break;
            }

            if (x < (ssize_t) image->columns && y < (ssize_t) image->rows) {
              q = QueueAuthenticPixels (image, x,y, 1,1, exception);
              if (!q) break;

              SetPixelViaPixelInfo(image, &pixelinf, q);

              if (!SyncAuthenticPixels (image,exception)) break;
            }

            break;
          }
          default:
            break;
        }
      } else {
        // Not escape
        chIn = ReadChar (image, &chPushed);
        if (chIn == EOF) {
          if (ppf != procFmt) {
            ThrowReaderException(CorruptImageError,"EOFduringFormat");
          }
          eofInp = MagickTrue;
        } else {
          if (chIn == '\r' && *ppf == '\n') {
            chIn = ReadChar (image, &chPushed);
            if (chIn != '\n') {
              ThrowReaderException(CorruptImageError,"BackslashRbad");
            }
          }

          if (chIn != *ppf) {
            ThrowReaderException(CorruptImageError,"UnexpectedInputChar");
          }
        }
      }
      ppf++;
    }
    if (!eofInp) {
      nPix++;

      if (MaxX < x) MaxX = x;
      if (MaxY < y) MaxY = y;

      if (firstX && firstY) {
        x++;
        if (x >= (ssize_t) image->columns) {
          x = 0;
          y++;
        }
      }
    }
  }
  if (IntErr)
    ThrowReaderException(CorruptImageError,"ParseIntegerError");

  if (TypeErr)
    ThrowReaderException(CorruptImageError,"TypeError");

  if (chPushed) {
    ThrowReaderException(CorruptImageError,"UnusedPushedChar");
  }

  if (MaxX < 0 && MaxY < 0) {
    ThrowReaderException(CorruptImageError,"UnexpectedEof");
  }

  if (nChErr) {
    ThrowReaderException(CorruptImageError,"NumChannelsError");
  }

  if (nPix > (ssize_t) (image->columns * image->rows)) {
    ThrowMagickException(exception, GetMagickModule(), CorruptImageWarning,"TooManyPixels", "`%s'",image_info->filename);
  } else if (MaxX >= (ssize_t) image->columns || MaxY >= (ssize_t) image->rows) {
    ThrowMagickException(exception, GetMagickModule(), CorruptImageWarning,"ImageBoundsExceeded", "`%s'",image_info->filename);
  }

  return(GetFirstImageInList(image));

}



ModuleExport unsigned long RegisterFTXTImage(void)
{
  MagickInfo
    *entry;

  entry=AcquireMagickInfo("FTXT","FTXT","Formatted text image");
  entry->decoder=(DecodeImageHandler *) ReadFTXTImage;
  entry->encoder=(EncodeImageHandler *) WriteFTXTImage;
  entry->magick=(IsImageFormatHandler *) IsFTXT;
  entry->flags^=CoderAdjoinFlag;
  (void) RegisterMagickInfo(entry);
  return(MagickImageCoderSignature);
}



ModuleExport void UnregisterFTXTImage(void)
{
  (void) UnregisterMagickInfo("FTXT");
}



static MagickBooleanType WriteFTXTImage(const ImageInfo *image_info,Image *image,
  ExceptionInfo *exception)
{
  char
    buffer[MaxTextExtent];

  long
    x,
    y;

  MagickBooleanType
    status;

  MagickOffsetType
    scene;

  const Quantum
    *p;

  PixelInfo
    pixel;

  assert(image_info != (const ImageInfo *) NULL);
  assert(image_info->signature == MagickCoreSignature);
  assert(image != (Image *) NULL);
  assert(image->signature == MagickCoreSignature);
  if (image->debug != MagickFalse)
    (void) LogMagickEvent(TraceEvent,GetMagickModule(),"%s",image->filename);
  status=OpenBlob(image_info,image,WriteBinaryBlobMode,exception);
  if (status == MagickFalse)
    return(status);
  scene=0;

  int precision = GetMagickPrecision();
  const char * sFmt;

  sFmt = GetImageArtifact (image, "ftxt:format");
  if (sFmt == NULL) sFmt = dfltFmt;

  const char * sChSep = GetImageArtifact (image, "ftxt:chsep");
  if (sChSep == NULL) sChSep = dfltChSep;

  char chSep;
  if (sChSep[0]==chEsc && (sChSep[1] == 'n' || sChSep[1] == 'N')) chSep = '\n';
  else chSep = sChSep[0];

  char sSuff[2];
  sSuff[0] = '\0';
  sSuff[1] = '\0';

  do
  {
    GetPixelInfo(image,&pixel);

    for (y=0; y < (long) image->rows; y++)
    {
      p=GetVirtualPixels(image,0,y,image->columns,1,exception);
      if (!p) break;
      for (x=0; x < (long) image->columns; x++)
      {
        long double ValMult=1.0;
        MagickBooleanType HexFmt = MagickFalse;
        MagickBooleanType FltHexFmt = MagickFalse;
        const char * pFmt = sFmt;
        while (*pFmt) {
          if (*pFmt == chEsc) {
            pFmt++;
            switch (*pFmt) {
              case 'x':
                FormatLocaleString (buffer, MaxTextExtent, "%li", x);
                WriteBlobString (image, buffer);
                break;
              case 'y':
                FormatLocaleString (buffer, MaxTextExtent, "%li", y);
                WriteBlobString (image, buffer);
                break;
              case 'n':
                WriteBlobString (image, "\n");
                break;
              case chEsc:
                FormatLocaleString (buffer, MaxTextExtent, "%c%c", chEsc, chEsc);
                WriteBlobString (image, buffer);
                break;
              case 'c':
              case 'v':
              case 'p':
              case 'o':
              case 'h':
              case 'f':
              {
                HexFmt = MagickFalse;
                if (*pFmt=='c') {
                  ValMult = 1.0;
                } else if (*pFmt=='v') {
                  ValMult = 1.0;
                } else if (*pFmt=='p') {
                  ValMult = 100 * QuantumScale;
                  sSuff[0] = '%';
                } else if (*pFmt=='o') {
                  ValMult = QuantumScale;
                } else if (*pFmt=='h') {
                  ValMult = 1.0;
                  HexFmt = MagickTrue;
                } else if (*pFmt=='f') {
                  ValMult = 1.0;
                  FltHexFmt = MagickTrue;
                }
                // Output all "-channel" channels.
                ssize_t i;
                char sSep[2];
                sSep[0] = sSep[1] = '\0';
                for (i=0; i < (ssize_t) GetPixelChannels (image); i++) {
                  PixelChannel channel = GetPixelChannelChannel (image, i);
                  PixelTrait traits = GetPixelChannelTraits (image, channel);
                  if ((traits & UpdatePixelTrait) != UpdatePixelTrait) continue;
                  if (HexFmt) {
                    FormatLocaleString (buffer,MaxTextExtent,"%s#%Lx", sSep, (signed long long)(((long double)p[i])+0.5));
                  } else if (FltHexFmt) {
                    FormatLocaleString (buffer,MaxTextExtent,"%s%a", sSep, p[i]);
                  } else {
                    FormatLocaleString (buffer,MaxTextExtent,"%s%.*Lg%s", sSep, precision, p[i]*ValMult, sSuff);
                  }
                  WriteBlobString (image, buffer);
                  sSep[0] = chSep;
                }
                break;
              }
              case 'j':
                // Output nothing.
                break;
              case 's':
                GetPixelInfoPixel (image, p, &pixel);
                GetColorTuple (&pixel, MagickFalse, buffer);
                WriteBlobString (image, buffer);
                break;
              case 'H':
                GetPixelInfoPixel (image, p, &pixel);
                // For reading, QueryColorCompliance misreads 64 bit/channel hex colours,
                // so when writing we ensure it is at most 32 bits.
                if (pixel.depth > 32) pixel.depth = 32;
                GetColorTuple (&pixel, MagickTrue, buffer);
                WriteBlobString (image, buffer);
                break;
              default:
                break;
            }
          } else {
            // Not an escape char.
            buffer[0] = *pFmt;
            buffer[1] = '\0';
            (void) WriteBlobString(image,buffer);
          }
          pFmt++;
        }
        p += GetPixelChannels (image);
      }
      if (image->previous == (Image *) NULL)
        if ((image->progress_monitor != (MagickProgressMonitor) NULL) &&
            (QuantumTick(y,image->rows) != MagickFalse))
          {
            status=image->progress_monitor(SaveImageTag,y,image->rows,
              image->client_data);
            if (status == MagickFalse)
              break;
          }
    }
    if (GetNextImageInList(image) == (Image *) NULL)
      break;
    image=SyncNextImageInList(image);
    status=SetImageProgress(image,SaveImagesTag,scene,
      GetImageListLength(image));
    if (status == MagickFalse)
      break;
    scene++;
  } while (image_info->adjoin != MagickFalse);
  (void) CloseBlob(image);
  return(MagickTrue);
}

All images on this page were created by the commands shown, using:

%IM7%magick identify -version
Version: ImageMagick 7.1.0-20 Q32-HDRI x86_64 2022-01-16 https://imagemagick.org
Copyright: (C) 1999-2021 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI Modules OpenMP(4.5) 
Delegates (built-in): bzlib cairo fontconfig fpx freetype jbig jng jpeg lcms ltdl lzma pangocairo png raqm rsvg tiff webp wmf x xml zip zlib
Compiler: gcc (11.2)

To improve internet download speeds, some images may have been automatically converted (by ImageMagick, of course) from PNG or TIFF or MIFF to JPG.

Source file for this web page is fmttxt.h1. To re-create this web page, execute "procH1 fmttxt".


This page, including the images, is my copyright. Anyone is permitted to use or adapt any of the code, scripts or images for any purpose, including commercial use.

Anyone is permitted to re-publish this page, but only for non-commercial use.

Anyone is permitted to link to this page, including for commercial use.


Page version v1.0 24-November-2021.

Page created 12-Mar-2022 19:33:08.

Copyright © 2022 Alan Gibson.