snibgo's ImageMagick pages

fmttxt:

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, fmttxt.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.

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 FMTTXT.

  1. > and fmttxt.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:

%IM7DEV%magick -list format |grep -i fmttxt 
 FMTTXT* FMTTXT    rw-   Formatted text image

The installation was successful.

Format defines and escapes

The coder is controlled by four defines:

Define Description Default
fmttxt:format The format string for writing and reading. \x,\y:\c
fmttxt:chsep A single text character that separates channel values for reading and writing. Comma, ","
fmttxt:nummeta The number of meta channels, for reading only. Zero, 0
fmttxt: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:

%IM7DEV%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:

%IM7DEV%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.
%IM7DEV%magick ^
  fmtt_src.miff txt: 
# ImageMagick pixel enumeration: 2,4,4294967295,srgb
0,0: (-0.00999762,-0.00999976,-0.00999928)  #000000000000000000000000  srgb(-2.32775e-10%,-2.32825e-10%,-2.32814e-10%)
1,0: (2.29065e+09,2.86331e+08,5.72662e+08)  #888887A3111110F4222221E9  srgb(53.3333%,6.66667%,13.3333%)
0,1: (1.33621e+09,2.86331e+08,4.77219e+08)  #4FA4F9CA111110F41C71C6ED  srgb(31.1111%,6.66667%,11.1111%)
1,1: (2.95875e+09,1.62254e+09,1.14532e+09)  #B05B048860B60ABE444443D1  srgb(68.8889%,37.7778%,26.6667%)
0,2: (2.67242e+09,5.72662e+08,9.54437e+08)  #9F49F393222221E938E38DD9  srgb(62.2222%,13.3333%,22.2222%)
1,2: (3.62686e+09,2.95875e+09,1.71799e+09)  #D82D816DB05B0488666665BA  srgb(84.4444%,68.8889%,40%)
0,3: (4.00864e+09,8.58993e+08,1.43166e+09)  #EEEEED5D333332DD555554C6  srgb(93.3333%,20%,33.3333%)
1,3: (4.29497e+09,4.29497e+09,2.29065e+09)  #FFFFFE51FFFFFE51888887A3  srgb(100%,100%,53.3333%)

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".

%IM7DEV%magick ^
  fmtt_src.miff sparse-color: 
0,0,srgb(-2.32775e-10%,-2.32825e-10%,-2.32814e-10%) 1,0,srgb(53.3333%,6.66667%,13.3333%) 0,1,srgb(31.1111%,6.66667%,11.1111%) 1,1,srgb(68.8889%,37.7778%,26.6667%) 0,2,srgb(62.2222%,13.3333%,22.2222%) 1,2,srgb(84.4444%,68.8889%,40%) 0,3,srgb(93.3333%,20%,33.3333%) 1,3,srgb(100%,100%,53.3333%) 

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

%IM7DEV%magick ^
  fmtt_src.miff debug: 
# ImageMagick pixel debugging: 2,4,4294967295,srgb
0,0: -0.0099976158144479583545,-0.0099997615814447961963,-0.0099992847443343881725 
1,0: 2290648994.9250764847,286331124.35688441992,572662248.72376906872 
0,1: 1336211913.7021298409,286331124.35688471794,477218540.60147488117 
1,1: 2958754951.7811412811,1622543038.0690131187,1145324497.4575388432 
0,2: 2672423827.4142570496,572662248.72376918793,954437081.21294903755 
1,2: 3626860908.6372060776,2958754951.781141758,1717986746.19130826 
0,3: 4008635741.1263842583,858993373.09065365791,1431655621.8244230747 
1,3: 4294966865.4932703972,4294966865.4932703972,2290648994.9250779152 

Writing formatted text

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

magick in.png out.fmttxt
magick in.png FMTTXT:out.lis
magick in.png FMTTXT:-

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

%IM7DEV%magick ^
  fmtt_src.miff ^
  fmtt_fmt1.fmttxt
0,0:-0.00999762,-0.00999976,-0.00999928
1,0:2.29065e+09,2.86331e+08,5.72662e+08
0,1:1.33621e+09,2.86331e+08,4.77219e+08
1,1:2.95875e+09,1.62254e+09,1.14532e+09
0,2:2.67242e+09,5.72662e+08,9.54437e+08
1,2:3.62686e+09,2.95875e+09,1.71799e+09
0,3:4.00864e+09,8.58993e+08,1.43166e+09
1,3:4.29497e+09,4.29497e+09,2.29065e+09

Read that formatted text, and compare to the original:

%IM7DEV%magick ^
  fmtt_src.miff ^
  -size 2x4 fmtt_fmt1.fmttxt ^
  -metric RMSE -compare -format "%%[distortion]\n" ^
  info:
6.1548e-07

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:

%IM7DEV%magick ^
  fmtt_src.miff ^
  -define fmttxt:format="Hello World." ^
  fmtt_fmt2.fmttxt
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.

%IM7DEV%magick ^
  fmtt_src.miff ^
  -define fmttxt:format="Hello World: \v\n" ^
  fmtt_fmt3.fmttxt
Hello World: -0.00999762,-0.00999976,-0.00999928
Hello World: 2.29065e+09,2.86331e+08,5.72662e+08
Hello World: 1.33621e+09,2.86331e+08,4.77219e+08
Hello World: 2.95875e+09,1.62254e+09,1.14532e+09
Hello World: 2.67242e+09,5.72662e+08,9.54437e+08
Hello World: 3.62686e+09,2.95875e+09,1.71799e+09
Hello World: 4.00864e+09,8.58993e+08,1.43166e+09
Hello World: 4.29497e+09,4.29497e+09,2.29065e+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:

%IM7DEV%magick ^
  fmtt_src.miff ^
  -define fmttxt:format="\x,\y:\p\n" ^
  fmtt_pc.fmttxt
0,0:-2.32775e-10%,-2.32825e-10%,-2.32814e-10%
1,0:53.3333%,6.66667%,13.3333%
0,1:31.1111%,6.66667%,11.1111%
1,1:68.8889%,37.7778%,26.6667%
0,2:62.2222%,13.3333%,22.2222%
1,2:84.4444%,68.8889%,40%
0,3:93.3333%,20%,33.3333%
1,3:100%,100%,53.3333%

Read that formatted text, and compare to the original:

%IM7DEV%magick ^
  fmtt_src.miff ^
  -define fmttxt:format="\x,\y:\p\n" ^
  -size 2x4 fmtt_pc.fmttxt ^
  -metric RMSE -compare -format "%%[distortion]\n" ^
  info:
2.04938e-07

Show values on a nominal scale of 0 to 1:

%IM7DEV%magick ^
  fmtt_src.miff ^
  -define fmttxt:format="\x,\y:\o\n" ^
  fmtt_o.fmttxt
0,0:-2.32775e-12,-2.32825e-12,-2.32814e-12
1,0:0.533333,0.0666667,0.133333
0,1:0.311111,0.0666667,0.111111
1,1:0.688889,0.377778,0.266667
0,2:0.622222,0.133333,0.222222
1,2:0.844444,0.688889,0.4
0,3:0.933333,0.2,0.333333
1,3:1,1,0.533333

Read that formatted text, and compare to the original:

%IM7DEV%magick ^
  fmtt_src.miff ^
  -define fmttxt:format="\x,\y:\o\n" ^
  -size 2x4 fmtt_o.fmttxt ^
  -metric RMSE -compare -format "%%[distortion]\n" ^
  info:
2.04938e-07

Three hex formats are available:

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

    Read that formatted text, and compare to the original:

    %IM7DEV%magick ^
      xc:sRGB(10%%,20%%,30%%) ^
      -define fmttxt:format="\x,\y:\h\n" ^
      -size 1x1 fmtt_hex1.fmttxt ^
      -metric RMSE -compare -format "%%[distortion]\n" ^
      info:
    9.50527e-11
  2. \f pixel values as separated floating-point hex, with leading 0x. For example:
    %IM7DEV%magick ^
      xc:sRGB(10%%,20%%,30%%) ^
      -define "fmttxt:format=\x,\y:\f\n" ^
      fmtt_hex2.fmttxt
    0,0:0x1.99999998p+28,0x1.99999998p+29,0x1.33333332p+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:

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

    Read that formatted text, and compare to the original:

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

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

%IM7DEV%magick ^
  fmtt_src.miff ^
  -define "fmttxt:format=\x,\y:(\v)  \H  \s\n" ^
  fmtt_fmt4.fmttxt
0,0:(-0.00999762,-0.00999976,-0.00999928)  #000000000000000000000000  srgb(-2.32775e-10%,-2.32825e-10%,-2.32814e-10%)
1,0:(2.29065e+09,2.86331e+08,5.72662e+08)  #888887A3111110F4222221E9  srgb(53.3333%,6.66667%,13.3333%)
0,1:(1.33621e+09,2.86331e+08,4.77219e+08)  #4FA4F9CA111110F41C71C6ED  srgb(31.1111%,6.66667%,11.1111%)
1,1:(2.95875e+09,1.62254e+09,1.14532e+09)  #B05B048860B60ABE444443D1  srgb(68.8889%,37.7778%,26.6667%)
0,2:(2.67242e+09,5.72662e+08,9.54437e+08)  #9F49F393222221E938E38DD9  srgb(62.2222%,13.3333%,22.2222%)
1,2:(3.62686e+09,2.95875e+09,1.71799e+09)  #D82D816DB05B0488666665BA  srgb(84.4444%,68.8889%,40%)
0,3:(4.00864e+09,8.58993e+08,1.43166e+09)  #EEEEED5D333332DD555554C6  srgb(93.3333%,20%,33.3333%)
1,3:(4.29497e+09,4.29497e+09,2.29065e+09)  #FFFFFE51FFFFFE51888887A3  srgb(100%,100%,53.3333%)

Like a "sparse-color:" format:

%IM7DEV%magick ^
  fmtt_src.miff ^
  -define "fmttxt:format=\x,\y,\s " ^
  fmtt_fmt5.fmttxt
0,0,srgb(-2.32775e-10%,-2.32825e-10%,-2.32814e-10%) 1,0,srgb(53.3333%,6.66667%,13.3333%) 0,1,srgb(31.1111%,6.66667%,11.1111%) 1,1,srgb(68.8889%,37.7778%,26.6667%) 0,2,srgb(62.2222%,13.3333%,22.2222%) 1,2,srgb(84.4444%,68.8889%,40%) 0,3,srgb(93.3333%,20%,33.3333%) 1,3,srgb(100%,100%,53.3333%) 

Like a "debug:" format:

%IM7DEV%magick ^
  fmtt_src.miff ^
  -define "fmttxt:format=\x,\y,\v " ^
  fmtt_fmt6.fmttxt
0,0,-0.00999762,-0.00999976,-0.00999928 1,0,2.29065e+09,2.86331e+08,5.72662e+08 0,1,1.33621e+09,2.86331e+08,4.77219e+08 1,1,2.95875e+09,1.62254e+09,1.14532e+09 0,2,2.67242e+09,5.72662e+08,9.54437e+08 1,2,3.62686e+09,2.95875e+09,1.71799e+09 0,3,4.00864e+09,8.58993e+08,1.43166e+09 1,3,4.29497e+09,4.29497e+09,2.29065e+09 

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

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

This has created two output files:

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

These are the two created files:

0,0,srgb(-2.32775e-10%,-2.32825e-10%,-2.32814e-10%) 1,0,srgb(53.3333%,6.66667%,13.3333%) 0,1,srgb(31.1111%,6.66667%,11.1111%) 1,1,srgb(68.8889%,37.7778%,26.6667%) 0,2,srgb(62.2222%,13.3333%,22.2222%) 1,2,srgb(84.4444%,68.8889%,40%) 0,3,srgb(93.3333%,20%,33.3333%) 1,3,srgb(100%,100%,53.3333%) 
0,0,srgb(-4.6555e-10%,-4.6565e-10%,-4.65628e-10%) 1,0,srgb(106.667%,13.3333%,26.6667%) 0,1,srgb(62.2222%,13.3333%,22.2222%) 1,1,srgb(137.778%,75.5555%,53.3333%) 0,2,srgb(124.444%,26.6667%,44.4444%) 1,2,srgb(168.889%,137.778%,80%) 0,3,srgb(186.667%,40%,66.6667%) 1,3,srgb(200%,200%,106.667%) 

The format can store many channels:

%IM7DEV%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 "fmttxt:format=\x,\y:(\p\v) \n" ^
  fmtt_mult.fmttxt
0,0:(-nan%,-nan%,-nan%,-nan%,-nan%,-nan%,-nan%,-nan%,-nan%,-nan%-nan%,-nan%,-nan%,-nan%,-nan%,-nan%,-nan%,-nan%,-nan%,-nan%) 
 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.

%IM7DEV%magick ^
  fmtt_src.miff ^
  -precision 15 ^
  -define fmttxt:chsep=";" ^
  -define fmttxt:format="\v\n" ^
  fmtt_fmt8.fmttxt
-0.00999761581444796;-0.0099997615814448;-0.00999928474433439
2290648994.92508;286331124.356884;572662248.723769
1336211913.70213;286331124.356885;477218540.601475
2958754951.78114;1622543038.06901;1145324497.45754
2672423827.41426;572662248.723769;954437081.212949
3626860908.63721;2958754951.78114;1717986746.19131
4008635741.12638;858993373.090654;1431655621.82442
4294966865.49327;4294966865.49327;2290648994.92508

Read that formatted text, and compare to the original:

%IM7DEV%magick ^
  fmtt_src.miff ^
  -define fmttxt:chsep=";" ^
  -define fmttxt:format="\v\n" ^
  -size 2x4 fmtt_fmt8.fmttxt ^
  -metric RMSE -compare -format "%%[distortion]\n" ^
  info:
4.40806e-16

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:

%IM7DEV%magick ^
  xc:srgba(10%%,20%%,30%%,0.4) ^
  -define fmttxt:format="\p\n" ^
  fmtt_alp1.fmttxt
10%,20%,30%,40%

Read the formatted text file:

%IM7DEV%magick ^
  -size 1x1 ^
  -define fmttxt:format="\p\n" ^
  fmtt_alp1.fmttxt ^
  txt: >fmtt_alp1.lis 2^>^&1
NumChannelsError (0,0): i=3 but nExpCh=3
magick: NumChannelsError `fmtt_alp1.fmttxt' @ error/fmttxt.c/ReadFMTTXTImage/567.

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:

%IM7DEV%magick ^
  -size 1x1 ^
  -define fmttxt:format="\p\n" ^
  -define fmttxt:hasalpha=true ^
  fmtt_alp1.fmttxt ^
  txt: >fmtt_alp2.lis 2^>^&1
# ImageMagick pixel enumeration: 1,1,4294967295,undefineda
0,0: (4.29497e+08,8.58993e+08,1.28849e+09,1.71799e+09)  #1999999A333333334CCCCCCD66666666  undefineda(10%,20%,30%,0.4)

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:

%IM7DEV%magick ^
  fmtt_src.miff ^
  -precision 15 -depth 8 ^
  -define fmttxt:chsep="" ^
  -define fmttxt:format=\v ^
  fmttxt:
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.

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

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

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

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

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

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.

%IM7DEV%magick ^
  fmtt_src.miff ^
  -size 2x4 ^
  -define fmttxt:format="\x,\y,\s " ^
  fmttxt:fmtt_sps.lis ^
  -metric RMSE -compare -format "%%[distortion]\n" ^
  info:
2.04938e-07

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".

%IM7DEV%magick ^
  -size 1x1 ^
  -define fmttxt:format="\j,\j,\s " ^
  fmttxt:fmtt_sps.lis ^
  fmtt_sps_out1.png 
Too many pixels were read
nPix=8 MaxX=0 MaxY=7
magick: TooManyPixels `fmtt_sps.lis' @ warning/fmttxt.c/ReadFMTTXTImage/575.

So a Windows BAT script that does this processing is:

set nPix=0

for /F "usebackq tokens=1,2 delims== " %%A in (`%IM7DEV%magick ^
  -size 1x1 ^
  -define "fmttxt:format=\j,\j,\s " ^
  fmttxt: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.

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

This is the created file:

%IM7DEV%magick ^
  fmtt_sps_out2.png ^
  txt: 
# ImageMagick pixel enumeration: 8,1,4294967295,srgb
0,0: (0,0,0)  #000000000000  black
1,0: (2.29065e+09,2.86331e+08,5.72662e+08)  #888811112222  srgb(136,17,34)
2,0: (1.33623e+09,2.86331e+08,4.7724e+08)  #4FA511111C72  srgb(31.1116%,6.66667%,11.1116%)
3,0: (2.95873e+09,1.62257e+09,1.14532e+09)  #B05A60B64444  srgb(68.8884%,37.7783%,26.6667%)
4,0: (2.6724e+09,5.72662e+08,9.54415e+08)  #9F49222238E3  srgb(62.2217%,13.3333%,22.2217%)
5,0: (3.62688e+09,2.95873e+09,1.71799e+09)  #D82DB05A6666  srgb(84.445%,68.8884%,40%)
6,0: (4.00864e+09,8.58993e+08,1.43166e+09)  #EEEE33335555  srgb(238,51,85)
7,0: (4.29497e+09,4.29497e+09,2.29065e+09)  #FFFFFFFF8888  srgb(255,255,136)

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

%IM7DEV%magick ^
  fmtt_src.miff ^
  -size 2x4 ^
  -define fmttxt:format="\x,\y: \v\n" ^
  fmttxt: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

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

An example with an alpha channel:

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

%IM7DEV%magick ^
  -size 2x1 ^
  -colorspace Gray ^
  -define fmttxt:hasalpha=true ^
  -define fmttxt:format="\c\n" ^
  fmttxt:fmtt_gry2.lis ^
  txt:
# ImageMagick pixel enumeration: 2,1,4294967295,graya
0,0: (3.0542e+08,1.71799e+09)  #12345678123456781234567866666666  graya(7.11111%,0.4)
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

%IM7DEV%magick ^
  -size 2x1 ^
  -define fmttxt:format="\c\n" ^
  fmttxt:fmtt_mix.lis ^
  txt:
# ImageMagick pixel enumeration: 2,1,4294967295,undefined
0,0: (3.0542e+08,8.58993e+08,234567)  #123456783333333300039447  undefined(7.11111%,20%,0.00546144%)
1,0: (3.0542e+08,1.28849e+09,40)  #123456784CCCCCCD00000028  undefined(7.11111%,30%,9.31323e-07%)

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 fmttxt: format. We need to declare the number of meta channels. We leave the colorspace as undefined.

%IM7DEV%magick ^
  -size 1x1 -define fmttxt:format="\c\n" -define fmttxt:nummeta=8 fmttxt:fmtt_mch.lis ^
  -define fmttxt:format="\p\n" ^
  fmttxt: 
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:

%IM7DEV%magick ^
  -size 1x1 -define fmttxt:format="\c\n" -define fmttxt:nummeta=8 fmttxt:fmtt_mch.lis ^
  txt: 
# ImageMagick pixel enumeration: 1,1,4294967295,undefined
0,0: (0,-4.29497e+08,8.58993e+08,1.71799e+09)  #00000000000000003333333366666666  undefineda(0%,-10%,20%,0.4)

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:

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

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

The MPC format can record the meta channels:

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

%IM7DEV%magick ^
  fmtt_mch_4.mpc ^
  -define fmttxt:format="\p\n" ^
  fmttxt: 
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 "FMTTXT" 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.

fmttxt.h

#include "coders/coders-private.h"

#define MagickFMTTXTHeaders

#define MagickFMTTXTAliases

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

MagickCoderExports(FMTTXT)

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

fmttxt.c

/*
   By: Alan Gibson, 18-October-2021
*/

#include <MagickCore/studio.h>
#include <MagickCore/artifact.h>
#include <MagickCore/blob.h>
#include <MagickCore/cache.h>
#include <MagickCore/channel.h>
#include <MagickCore/colorspace.h>
#include <MagickCore/exception.h>
#include <MagickCore/image.h>
#include <MagickCore/list.h>
#include <MagickCore/magick.h>
#include <MagickCore/memory_.h>
#include <MagickCore/monitor.h>
#include <MagickCore/pixel-accessor.h>
#include <MagickCore/string_.h>
#include <MagickCore/module.h>
#include <MagickCore/attribute.h>

#include "MagickCore/blob-private.h"
#include "MagickCore/exception-private.h"
#include "MagickCore/image-private.h"
#include "MagickCore/monitor-private.h"
#include "MagickCore/quantum-private.h"

/*
  Forward declaration.
*/
static MagickBooleanType
  WriteFMTTXTImage(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 IsFMTTXT(const unsigned char *magick,const size_t length)
{
  if (length < 10)
    return(MagickFalse);
  if (LocaleNCompare((char *) magick,"id=fmttxt",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) {
      fprintf (stderr, "ReadInt too long\n");
      *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) {
    if (errno) fprintf (stderr, "ReadInt errno=%i: %s\n", errno, strerror (errno));
    if (*tail) fprintf (stderr, "ReadInt: unused input [%s]\n", tail);
    *eofInp = MagickTrue;
    *err = MagickTrue;
  }

  if (val < 0) {
    fprintf (stderr, "Negative integer [%i] not permitted\n", val);
    *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
    errno = 0;
    val = strtold (buffer, tail);
    if (errno) fprintf (stderr, "ReadFlt HexFlt errno=%i: %s\n", errno, strerror (errno));
    if (expectType != vtAny && expectType != vtFltHex) *err = MagickTrue;
  } else {
    // Read decimal floating-point (possibly a percent).
    errno = 0;
    val = strtold (buffer, tail);
    if (errno) {
      fprintf (stderr, "ReadFlt flt errno=%i: %s\n", errno, strerror (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) {
      fprintf (stderr, "ReadUntil: BufSiz busted\n");
      *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 *ReadFMTTXTImage(const ImageInfo *image_info,ExceptionInfo *exception)
{
  char
    buffer[MaxTextExtent];

  Image
    *image;

  MagickBooleanType
    status;

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

  MagickBooleanType verbose = image_info->verbose;

  if (verbose) {
    fprintf (stderr, "Image colorspace: %i\n", image->colorspace);
    fprintf (stderr, "Image_info colorspace: %i\n", image_info->colorspace);
  }

  SetImageColorspace (image, RGBColorspace, exception);
  SetImageColorspace (image, image_info->colorspace, exception);

  if (verbose) {
    fprintf (stderr, "Image colorspace: %i\n", image->colorspace);
    fprintf (stderr, "num channels: %lu\n", GetPixelChannels (image));
    fprintf (stderr, "Image depth: %lu\n", image->depth);
    fprintf (stderr, "Image_info depth: %lu\n", image_info->depth);
  }

  const char * sFmt = GetImageArtifact (image, "fmttxt:format");
  if (sFmt == NULL) sFmt = dfltFmt;

  const char * sChSep = GetImageArtifact (image, "fmttxt: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, "fmttxt:hasalpha"));

  int numMeta = 0;
  const char * sNumMeta = GetImageArtifact (image, "fmttxt: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");
  }

  if (verbose) {
    fprintf (stderr, "numMeta %i\n", numMeta);
    fprintf (stderr, "num channels: %lu\n", GetPixelChannels (image));
  }

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

  if (verbose) fprintf (stderr, "size %lix%li\n", image->columns, image->rows);

  // 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 (verbose) fprintf (stderr, "i=%i ch=%i traits=%i\n", i, channel, traits);
    if ((traits & UpdatePixelTrait) != UpdatePixelTrait) continue;
    nExpCh++;
  }
  if (verbose)
    fprintf (stderr, "nExpCh=%i: Expect %i channel values%s.\n",
             nExpCh, nExpCh,
             (hasAlpha) ? " (including alpha)" : ""
            );

  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;
    char * 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) {
                fprintf (stderr, "Type error: (%li,%li) expected %i at %s i=%i val=%Lg tail=[%s]\n",
                         x, y, (int)expectType, pt, i, val, tail);
                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;
              fprintf (stderr, "NumChannelsError (%li,%li): i=%i but nExpCh=%i\n", x, y, i, nExpCh);
            }

            if (x < image->columns && y < 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) {
              fprintf (stderr, "buffer empty (%li,%li)\n", x, y);
              ThrowReaderException(CorruptImageError,"No input for escape 'H' or 's'.");
            }
            PixelInfo pixelinf;
            if (!QueryColorCompliance(buffer, AllCompliance, &pixelinf, exception)) {
              fprintf (stderr, "QueryColorCompliance failed (%li,%li) [%s]\n", x, y, buffer);
              break;
            }

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

              SetPixelViaPixelInfo(image, &pixelinf, q);

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

            break;
          }
          default:
            fprintf (stderr, "Unknown escape '%c'\n", *ppf);
            break;
        }
      } else {
        // Not escape
        chIn = ReadChar (image, &chPushed);
        if (chIn == EOF) {
          if (ppf != procFmt) {
            fprintf (stderr, "(%li,%li) Expect [%c] %i but found EOF.\n", x, y, *ppf, (int)*ppf);
            ThrowReaderException(CorruptImageError,"EOFduringFormat");
          }
          eofInp = MagickTrue;
        } else {
          if (chIn == '\r' && *ppf == '\n') {
            chIn = ReadChar (image, &chPushed);
            if (chIn != '\n') {
              fprintf (stderr, "(%li,%li) \\r not followed by \\n.\n", x, y);
              ThrowReaderException(CorruptImageError,"BackslashRbad");
            }
          }

          if (chIn != *ppf) {
            fprintf (stderr, "(%li,%li) Error at character '%c'. Expected '%c'.\n", x, y, 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 >= image->columns) {
          x = 0;
          y++;
        }
      }
    }
  }
  if (IntErr)
    ThrowReaderException(CorruptImageError,"ParseIntegerError");

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

  if (chPushed) {
    fprintf (stderr, "Unused pushed char [%c] %i\n", chPushed, (int)chPushed);
    ThrowReaderException(CorruptImageError,"UnusedPushedChar");
  }

  if (MaxX < 0 && MaxY < 0) {
    fprintf (stderr, "Unexpected EOF: no pixels were read\n");
    ThrowReaderException(CorruptImageError,"UnexpectedEof");
  }

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

  if (verbose) fprintf (stderr, "nPix=%li MaxX=%li MaxY=%li\n", nPix, MaxX, MaxY);

  if (nPix > image->columns * image->rows) {
    fprintf (stderr, "Too many pixels were read\n");
    fprintf (stderr, "nPix=%li MaxX=%li MaxY=%li\n", nPix, MaxX, MaxY);
    ThrowMagickException(exception, GetMagickModule(), CorruptImageWarning,"TooManyPixels", "`%s'",image_info->filename);
  } else if (MaxX >= image->columns || MaxY >= image->rows) {
    fprintf (stderr, "Image bounds exceeded\n");
    fprintf (stderr, "nPix=%li MaxX=%li MaxY=%li\n", nPix, MaxX, MaxY);
    ThrowMagickException(exception, GetMagickModule(), CorruptImageWarning,"ImageBoundsExceeded", "`%s'",image_info->filename);
  }

  return(GetFirstImageInList(image));

}



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

  entry=AcquireMagickInfo("FMTTXT","FMTTXT","Formatted text image");
  entry->decoder=(DecodeImageHandler *) ReadFMTTXTImage;
  entry->encoder=(EncodeImageHandler *) WriteFMTTXTImage;
  entry->magick=(IsImageFormatHandler *) IsFMTTXT;
  entry->flags^=CoderAdjoinFlag;
  (void) RegisterMagickInfo(entry);
  return(MagickImageCoderSignature);
}



ModuleExport void UnregisterFMTTXTImage(void)
{
  (void) UnregisterMagickInfo("FMTTXT");
}



static MagickBooleanType WriteFMTTXTImage(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;

  MagickBooleanType verbose = image_info->verbose;

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

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

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

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

  int depth = image->depth;

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

  if (verbose) {
    fprintf (stderr, "Image colorspace: %i\n", image->colorspace);
    fprintf (stderr, "GetPixelChannels(): %lu\n", GetPixelChannels (image));
    fprintf (stderr, "Image depth: %i\n", depth);
    fprintf (stderr, "Image_info depth: %lu\n", image_info->depth);
    fprintf (stderr, "Image Q depth: %lu\n", GetImageQuantumDepth(image, MagickFalse));
  }

  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 (verbose) fprintf (stderr, "i=%li ch=%i tr=%i\n", i, channel, traits);
                  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:

%IM7DEV%magick identify -version
Version: ImageMagick 7.0.8-64 Q32 x86_64 2021-10-18 https://imagemagick.org
Copyright: © 1999-2019 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI Modules OpenMP(4.5) 
Delegates (built-in): bzlib cairo fftw fontconfig fpx freetype jbig jng jpeg lcms ltdl lzma pangocairo png raqm rsvg tiff webp wmf x xml zlib

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 28-Nov-2021 21:54:48.

Copyright © 2021 Alan Gibson.