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:

%IMG7%magick -list format |grep -i 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\n
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:

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

%IMG7%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.
%IMG7%magick ^
  fmtt_src.miff txt: 
# ImageMagick pixel enumeration: 2,4,0,65535,srgb
0,0: (0,0,0)  #000000000000  srgb(-1.5259e-05%,-1.5259e-05%,-1.5259e-05%)
1,0: (34952,4369,8738)  #888811112222  srgb(53.3333%,6.66665%,13.3333%)
0,1: (20389,4369,7282)  #4FA511111C72  srgb(31.1111%,6.66665%,11.1111%)
1,1: (45146,24758,17476)  #B05A60B64444  srgb(68.8889%,37.7778%,26.6666%)
0,2: (40777,8738,14563)  #9F49222238E3  srgb(62.2222%,13.3333%,22.2222%)
1,2: (55341,45146,26214)  #D82DB05A6666  srgb(84.4444%,68.8889%,40%)
0,3: (61166,13107,21845)  #EEEE33335555  srgb(93.3333%,20%,33.3333%)
1,3: (65535,65535,34952)  #FFFFFFFF8888  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".

%IMG7%magick ^
  fmtt_src.miff sparse-color: 
0,0,srgb(-1.5259e-05%,-1.5259e-05%,-1.5259e-05%) 1,0,srgb(53.3333%,6.66665%,13.3333%) 0,1,srgb(31.1111%,6.66665%,11.1111%) 1,1,srgb(68.8889%,37.7778%,26.6666%) 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

%IMG7%magick ^
  fmtt_src.miff debug: 
# ImageMagick pixel debugging: 2,4,65535,srgb
0,0: -0.0099999997764825820923,-0.0099999997764825820923,-0.0099999997764825820923 
1,0: 34951.984375,4368.98974609375,8737.9892578125 
0,1: 20388.654296875,4368.98974609375,7281.65625 
1,1: 45146.31640625,24757.654296875,17475.98828125 
0,2: 40777.31640625,8737.9892578125,14563.322265625 
1,2: 55340.65234375,45146.31640625,26213.98828125 
0,3: 61165.98046875,13106.9892578125,21844.98828125 
1,3: 65534.98046875,65534.98046875,34951.984375 

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:

%IMG7%magick ^
  fmtt_src.miff ^
  fmtt_fmt1.ftxt
0,0:-0.01,-0.01,-0.01
1,0:34952,4368.99,8737.99
0,1:20388.7,4368.99,7281.66
1,1:45146.3,24757.7,17476
0,2:40777.3,8737.99,14563.3
1,2:55340.7,45146.3,26214
0,3:61166,13107,21845
1,3:65535,65535,34952

Read that formatted text, and compare to the original:

%IMG7%magick ^
  fmtt_src.miff ^
  -size 2x4 fmtt_fmt1.ftxt ^
  -metric RMSE -compare -format "%%[distortion]\n" ^
  info:
3.0594e-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:

%IMG7%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.

%IMG7%magick ^
  fmtt_src.miff ^
  -define ftxt:format="Hello World: \v\n" ^
  fmtt_fmt3.ftxt
Hello World: -0.01,-0.01,-0.01
Hello World: 34952,4368.99,8737.99
Hello World: 20388.7,4368.99,7281.66
Hello World: 45146.3,24757.7,17476
Hello World: 40777.3,8737.99,14563.3
Hello World: 55340.7,45146.3,26214
Hello World: 61166,13107,21845
Hello World: 65535,65535,34952

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:

%IMG7%magick ^
  fmtt_src.miff ^
  -define ftxt:format="\x,\y:\p\n" ^
  fmtt_pc.ftxt
0,0:-1.5259e-05%,-1.5259e-05%,-1.5259e-05%
1,0:53.3333%,6.66665%,13.3333%
0,1:31.1111%,6.66665%,11.1111%
1,1:68.8889%,37.7778%,26.6666%
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:

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

Show values on a nominal scale of 0 to 1:

%IMG7%magick ^
  fmtt_src.miff ^
  -define ftxt:format="\x,\y:\o\n" ^
  fmtt_o.ftxt
0,0:-1.5259e-07,-1.5259e-07,-1.5259e-07
1,0:0.533333,0.0666665,0.133333
0,1:0.311111,0.0666665,0.111111
1,1:0.688889,0.377778,0.266666
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:

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

Three hex formats are available:

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

    Read that formatted text, and compare to the original:

    %IMG7%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:
    6.22947e-06
  2. \f pixel values as separated floating-point hex, with leading 0x. For example:
    %IMG7%magick ^
      xc:sRGB(10%%,20%%,30%%) ^
      -define "ftxt:format=\x,\y:\f\n" ^
      fmtt_hex2.ftxt
    0,0:0x1.9998000000000p+12,0x1.9998000000000p+13,0x1.3332000000000p+14
    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:

    %IMG7%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:
    %IMG7%magick ^
      xc:sRGB(10%%,20%%,30%%) ^
      -define "ftxt:format=\x,\y:\H\n" ^
      fmtt_hex3.ftxt
    0,0:#199A33334CCD

    Read that formatted text, and compare to the original:

    %IMG7%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:
    6.22947e-06

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

%IMG7%magick ^
  fmtt_src.miff ^
  -define "ftxt:format=\x,\y:(\v)  \H  \s\n" ^
  fmtt_fmt4.ftxt
0,0:(-0.01,-0.01,-0.01)  #000000000000000000000000  srgb(-1.5259e-05%,-1.5259e-05%,-1.5259e-05%)
1,0:(34952,4368.99,8737.99)  #8888848811110E7122221F62  srgb(53.3333%,6.66665%,13.3333%)
0,1:(20388.7,4368.99,7281.66)  #4FA4F72511110E711C71C472  srgb(31.1111%,6.66665%,11.1111%)
1,1:(45146.3,24757.7,17476)  #B05B015A60B6083644444144  srgb(68.8889%,37.7778%,26.6666%)
0,2:(40777.3,8737.99,14563.3)  #9F49F04922221F6238E38B63  srgb(62.2222%,13.3333%,22.2222%)
1,2:(55340.7,45146.3,26214)  #D82D7F2DB05B015A66666366  srgb(84.4444%,68.8889%,40%)
0,3:(61166,13107,21845)  #EEEEE9EE3333307355555255  srgb(93.3333%,20%,33.3333%)
1,3:(65535,65535,34952)  #FFFFFAFFFFFFFAFF88888488  srgb(100%,100%,53.3333%)

Like a "sparse-color:" format:

%IMG7%magick ^
  fmtt_src.miff ^
  -define "ftxt:format=\x,\y,\s " ^
  fmtt_fmt5.ftxt
0,0,srgb(-1.5259e-05%,-1.5259e-05%,-1.5259e-05%) 1,0,srgb(53.3333%,6.66665%,13.3333%) 0,1,srgb(31.1111%,6.66665%,11.1111%) 1,1,srgb(68.8889%,37.7778%,26.6666%) 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:

%IMG7%magick ^
  fmtt_src.miff ^
  -define "ftxt:format=\x,\y,\v " ^
  fmtt_fmt6.ftxt
0,0,-0.01,-0.01,-0.01 1,0,34952,4368.99,8737.99 0,1,20388.7,4368.99,7281.66 1,1,45146.3,24757.7,17476 0,2,40777.3,8737.99,14563.3 1,2,55340.7,45146.3,26214 0,3,61166,13107,21845 1,3,65535,65535,34952 

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

%IMG7%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(-1.5259e-05%,-1.5259e-05%,-1.5259e-05%) 1,0,srgb(53.3333%,6.66665%,13.3333%) 0,1,srgb(31.1111%,6.66665%,11.1111%) 1,1,srgb(68.8889%,37.7778%,26.6666%) 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(-3.0518e-05%,-3.0518e-05%,-3.0518e-05%) 1,0,srgb(106.667%,13.3333%,26.6666%) 0,1,srgb(62.2222%,13.3333%,22.2222%) 1,1,srgb(137.778%,75.5555%,53.3333%) 0,2,srgb(124.444%,26.6666%,44.4444%) 1,2,srgb(168.889%,137.778%,80%) 0,3,srgb(186.667%,40%,66.6666%) 1,3,srgb(200%,200%,106.667%) 

The format can store many channels:

%IMG7%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%,2.00995e-46%1966.05%,1966.05%,2949.07%,1.31722e-43%) 
 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.

%IMG7%magick ^
  fmtt_src.miff ^
  -precision 15 ^
  -define ftxt:chsep=";" ^
  -define ftxt:format="\v\n" ^
  fmtt_fmt8.ftxt
-0.00999999977648258;-0.00999999977648258;-0.00999999977648258
34951.984375;4368.98974609375;8737.9892578125
20388.654296875;4368.98974609375;7281.65625
45146.31640625;24757.654296875;17475.98828125
40777.31640625;8737.9892578125;14563.322265625
55340.65234375;45146.31640625;26213.98828125
61165.98046875;13106.9892578125;21844.98828125
65534.98046875;65534.98046875;34951.984375

Read that formatted text, and compare to the original:

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

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:

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

Read the formatted text file:

%IMG7%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/731.

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:

%IMG7%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,0,65535,undefineda
0,0: (6554,13107,19661,26214)  #199A33334CCD6666  undefineda(25.5,51,76.5,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:

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

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.

%IMG7%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:
2.12666e-07

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

%IMG7%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:
2.93041e-06

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

%IMG7%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:
2.93041e-06

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.

%IMG7%magick ^
  fmtt_src.miff ^
  -size 2x4 ^
  -define ftxt:format="\x,\y,\s " ^
  ftxt:fmtt_sps.lis ^
  -metric RMSE -compare -format "%%[distortion]\n" ^
  info:
2.12666e-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".

%IMG7%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/733.

So a Windows BAT script that does this processing is:

set nPix=0

for /F "usebackq tokens=1,2 delims== " %%A in (`%IMG7%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.

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

This is the created file:

%IMG7%magick ^
  fmtt_sps_out2.png ^
  txt: 
# ImageMagick pixel enumeration: 8,1,0,65535,srgb
0,0: (0,0,0)  #000000000000  black
1,0: (34952,4369,8738)  #888811112222  srgb(136,17,34)
2,0: (20389,4369,7282)  #4FA511111C72  srgb(31.1116%,6.66667%,11.1116%)
3,0: (45146,24758,17476)  #B05A60B64444  srgb(68.8884%,37.7783%,26.6667%)
4,0: (40777,8738,14563)  #9F49222238E3  srgb(62.2217%,13.3333%,22.2217%)
5,0: (55341,45146,26214)  #D82DB05A6666  srgb(84.445%,68.8884%,40%)
6,0: (61166,13107,21845)  #EEEE33335555  srgb(238,51,85)
7,0: (65535,65535,34952)  #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

%IMG7%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

%IMG7%magick ^
  -size 2x1 ^
  -colorspace Gray ^
  -define ftxt:format="\c\n" ^
  ftxt:fmtt_gry.lis ^
  txt:
# ImageMagick pixel enumeration: 2,1,0,65535,gray
0,0: (305419904)  #FFFFFFFFFFFF  gray(466041%)
1,0: (305419904)  #FFFFFFFFFFFF  gray(466041%)

An example with an alpha channel:

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

%IMG7%magick ^
  -size 2x1 ^
  -colorspace Gray ^
  -define ftxt:hasalpha=true ^
  -define ftxt:format="\c\n" ^
  ftxt:fmtt_gry2.lis ^
  txt:
# ImageMagick pixel enumeration: 2,1,0,65535,graya
0,0: (305419904,26214)  #FFFFFFFFFFFF6666  graya(466041%,0.4)
1,0: (0,0)  #0000000000000000  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

%IMG7%magick ^
  -size 2x1 ^
  -define ftxt:format="\c\n" ^
  ftxt:fmtt_mix.lis ^
  txt:
# ImageMagick pixel enumeration: 2,1,0,65535,undefined
0,0: (305419904,13107,234567)  #FFFF3333FFFF  undefined(1.1884e+06,51,912.712)
1,0: (305419904,19661,40)  #FFFF4CCD0028  undefined(1.1884e+06,76.5,0.155642)

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.

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

%IMG7%magick ^
  -size 1x1 -define ftxt:format="\c\n" -define ftxt:nummeta=8 ftxt:fmtt_mch.lis ^
  txt: 
# ImageMagick pixel enumeration: 1,1,8,65535,undefined
0,0: (0,0,13107,19661,26214,32768,39321,45875,52428,58982,66190)  #000000003333  undefined(0,-25.5,51)

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:

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

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

%IMG7%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

/*
  Copyright @ 1999 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 "coders/coders-private.h"

#define MagickFTXTHeaders \
  MagickCoderHeader("FTXT", 0, "id=ftxt")

#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 @ 2021 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/annotate.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/color.h"
#include "MagickCore/color-private.h"
#include "MagickCore/colorspace.h"
#include "MagickCore/constitute.h"
#include "MagickCore/draw.h"
#include "MagickCore/exception.h"
#include "MagickCore/exception-private.h"
#include "MagickCore/geometry.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/option.h"
#include "MagickCore/pixel-accessor.h"
#include "MagickCore/quantum-private.h"
#include "MagickCore/static.h"
#include "MagickCore/statistic.h"
#include "MagickCore/string_.h"
#include "MagickCore/token.h"
#include "coders/ftxt.h"

/*
  Define declaractions.
*/
#define chEsc '\\'
#define dfltChSep ","
#define dfltFmt "\\x,\\y:\\c\\n"

/*
  Enumerated declaractions.
*/
typedef enum
  {
    vtAny,
    vtQuant,
    vtPercent,
    vtProp,
    vtIntHex,
    vtFltHex
  } ValueTypeT;

/*
  Forward declaration.
*/
static MagickBooleanType
  WriteFTXTImage(const ImageInfo *,Image *,ExceptionInfo *);

/*
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%                                                                             %
%                                                                             %
%                                                                             %
%   I s F T X T                                                               %
%                                                                             %
%                                                                             %
%                                                                             %
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
%  IsFTXT() returns MagickTrue if the image format type, identified by the
%  magick string, is formatted text.
%
%  The format of the IsFTXT method is:
%
%      MagickBooleanType IsFTXT(const unsigned char *magick,const size_t length)
%
%  A description of each parameter follows:
%
%    o magick: compare image format pattern against these bytes.
%
%    o length: Specifies the length of the magick string.
%
*/
static MagickBooleanType IsFTXT(const unsigned char *magick,const size_t length)
{
  if (length < 7)
    return(MagickFalse);
  if (LocaleNCompare((char *) magick,"id=ftxt",7) == 0)
    return(MagickTrue);
  return(MagickFalse);
}

/*
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%                                                                             %
%                                                                             %
%                                                                             %
%   R e a d F T X T I m a g e                                                 %
%                                                                             %
%                                                                             %
%                                                                             %
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
%  ReadFTXTImage() reads an formatted text image file and returns it.  It
%  allocates the memory necessary for the new Image structure and returns a
%  pointer to the new image.
%
%  The format of the ReadFTXTImage method is:
%
%      Image *ReadFTXTImage(image_info,ExceptionInfo *exception)
%
%  A description of each parameter follows:
%
%    o image_info: the image info.
%
%    o exception: return any errors or warnings in this structure.
%
*/

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,
    *tail;

  int
    chIn,
    val;

  p=buffer;
  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;
  errno=0;
  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)
{
  long double
    val;

  *err=MagickFalse;
  val=0;
  if (*buffer == '#')
    {
      char
        *p;

      /* read hex integer */
      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;

  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 bufSize)
{
  int
    chIn,
    i;

  i=0;
  for (;;)
    {
      chIn=ReadChar(image,chPushed);
      if (chIn == EOF)
        {
          if (i==0)
            *eofInp=MagickTrue;
          break;
        }
      if (chIn == UntilChar)
        break;
      if (i >= bufSize)
        {
          *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],
    chSep,
    *ppf,
    procFmt[MaxTextExtent];

  const char
    *pf,
    *sChSep,
    *sFmt,
    *sNumMeta;

  Image
    *image;

  int
    chIn,
    chPushed,
    i,
    nExpCh,
    numMeta;

  long double
    chVals[MaxPixelChannels];

  MagickBooleanType
    eofInp,
    firstX,
    firstY,
    hasAlpha,
    intErr,
    nChErr,
    status,
    typeErr;

  PixelInfo
    mppBlack;

  Quantum
    *q;

  ssize_t
    maxX,
    maxY,
    nPix,
    x,
    y;

  assert(image_info != (const ImageInfo *) NULL);
  assert(image_info->signature == MagickCoreSignature);
  assert(exception != (ExceptionInfo *) NULL);
  assert(exception->signature == MagickCoreSignature);
  if (IsEventLogging() != MagickFalse)
    (void) LogMagickEvent(TraceEvent,GetMagickModule(),"%s",
      image_info->filename);
  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);
  sFmt=GetImageArtifact(image,"ftxt:format");
  if (sFmt == (const char *) NULL)
    sFmt=dfltFmt;
  sChSep=GetImageArtifact(image,"ftxt:chsep");
  if (sChSep == (const char *) NULL)
    sChSep=dfltChSep;
  if ((sChSep[0] == chEsc) && ((sChSep[1] == 'n' || sChSep[1] == 'N')))
    chSep='\n';
  else
    chSep=sChSep[0];
  hasAlpha=IsStringTrue(GetImageArtifact(image,"ftxt:hasalpha"));
  numMeta=0;
  sNumMeta=GetImageArtifact(image,"ftxt:nummeta");
  if (sNumMeta != (const char *) NULL)
    numMeta=atoi(sNumMeta);
  if (hasAlpha)
    {
      if (SetImageAlphaChannel(image,OpaqueAlphaChannel,exception) == MagickFalse)
        ThrowReaderException(OptionError,"SetImageAlphaChannelFailure");
    }
  if (numMeta)
    {
      if (SetPixelMetaChannels (image, numMeta, exception) == MagickFalse)
        ThrowReaderException(OptionError,"SetPixelMetaChannelsFailure");
    }
  /* make image zero (if RGB channels, transparent black). */
  GetPixelInfo(image,&mppBlack);
  if (hasAlpha)
    mppBlack.alpha=TransparentAlpha;
  SetImageColor(image,&mppBlack,exception);
  pf=sFmt;
  ppf=procFmt;
  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? */
  nExpCh=0;
  for (i=0; i < (ssize_t) GetPixelChannels (image); i++)
  {
    PixelChannel
      channel;

    PixelTrait
      traits;

    channel=GetPixelChannelChannel(image,i);
    traits=GetPixelChannelTraits(image,channel);
    if ((traits & UpdatePixelTrait) != UpdatePixelTrait)
      continue;
    nExpCh++;
  }
  for (i=0; i < MaxPixelChannels; i++)
    chVals[i] = 0;
  eofInp=MagickFalse;
  chPushed=0;
  x=0;
  y=0,
  maxX=-1;
  maxY=-1;
  nPix=0;
  firstX=MagickTrue,
  firstY=MagickTrue,
  intErr=MagickFalse,
  typeErr=MagickFalse,
  nChErr=MagickFalse;
  while (!eofInp)
  {
    ValueTypeT
      expectType;

    expectType=vtAny;
    ppf=procFmt;
    while (*ppf && eofInp == MagickFalse)
    {
      if (*ppf == chEsc)
        {
          ppf++;
          switch (*ppf)
          {
            case 'x':
            {
              x=ReadInt(image,&eofInp,&chPushed,&intErr);
              if ((intErr != MagickFalse) || (eofInp != MagickFalse))
                continue;
              if (firstX != MagickFalse)
                {
                  firstX=MagickFalse;
                  maxX=x;
                }
              else if (maxX < x)
                maxX=x;
              break;
            }
            case 'y':
            {
              y=ReadInt(image,&eofInp,&chPushed,&intErr);
              if ((intErr != MagickFalse) || (eofInp != MagickFalse))
                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':
            {
              char
                *pt,
                *tail;

              int
                untilChar;

              long double
                val;

              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.
              */
              untilChar=*(ppf+1);
              ReadUntil(image,untilChar,&eofInp,&chPushed,buffer,
                MaxTextExtent-1);
              if (eofInp != MagickFalse)
                break;
              pt=buffer;
              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 == (Quantum *) NULL)
                    break;
                  for (i=0; i< nExpCh; i++)
                    q[i]=chVals[i];
                  if (SyncAuthenticPixels(image,exception) == MagickFalse)
                    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;

              PixelInfo
                pixelinf;

              untilChar=*(ppf+1);
              ReadUntil(image,untilChar,&eofInp,&chPushed,buffer,
                MaxTextExtent-1);
              if (eofInp != MagickFalse)
                break;
              if (*buffer == 0)
                ThrowReaderException(CorruptImageError,
                  "No input for escape 'H' or 's'.");
              if (QueryColorCompliance(buffer,AllCompliance,&pixelinf,
                    exception) == MagickFalse)
                break;
              if (x < (ssize_t) image->columns && y < (ssize_t) image->rows)
                {
                  q=QueueAuthenticPixels(image,x,y,1,1,exception);
                  if (q == (Quantum *) NULL)
                    break;
                  SetPixelViaPixelInfo(image,&pixelinf,q);
                  if (SyncAuthenticPixels(image,exception) == MagickFalse)
                    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 == MagickFalse)
      {
        nPix++;
        if (maxX < x)
          maxX=x;
        if (maxY < y)
          maxY=y;
        if ((firstX != MagickFalse) && (firstY != MagickFalse))
          {
            x++;
            if (x >= (ssize_t) image->columns)
              {
                x=0;
                y++;
              }
          }
      }
  }
  if (intErr != MagickFalse)
    ThrowReaderException(CorruptImageError,"ParseIntegerError");
  if (typeErr != MagickFalse)
    ThrowReaderException(CorruptImageError,"TypeError");
  if (chPushed != 0)
    ThrowReaderException(CorruptImageError,"UnusedPushedChar");
  if ((maxX < 0) && (maxY < 0))
    ThrowReaderException(CorruptImageError,"UnexpectedEof");
  if (nChErr != MagickFalse)
    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);
  (void) CloseBlob(image);
  return(GetFirstImageInList(image));
}

/*
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%                                                                             %
%                                                                             %
%                                                                             %
%   R e g i s t e r F T X T I m a g e                                         %
%                                                                             %
%                                                                             %
%                                                                             %
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
%  RegisterFTXTImage() adds properties for the FTXT image format to
%  the list of supported formats. The properties include the image format
%  tag, a method to read and/or write the format, whether the format
%  supports the saving of more than one frame to the same file or blob,
%  whether the format supports native in-memory I/O, and a brief
%  description of the format.
%
%  The format of the RegisterFTXTImage method is:
%
%      size_t RegisterFTXTImage(void)
%
*/
ModuleExport size_t 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);
}

/*
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%                                                                             %
%                                                                             %
%                                                                             %
%   U n r e g i s t e r F T X T I m a g e                                     %
%                                                                             %
%                                                                             %
%                                                                             %
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
%  UnregisterFTXTImage() removes format registrations made by the
%  FTXT module from the list of supported formats.
%
%  The format of the UnregisterFTXTImage method is:
%
%      UnregisterFTXTImage(void)
%
*/
ModuleExport void UnregisterFTXTImage(void)
{
  (void) UnregisterMagickInfo("FTXT");
}

/*
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%                                                                             %
%                                                                             %
%                                                                             %
%   W r i t e F T X T I m a g e                                               %
%                                                                             %
%                                                                             %
%                                                                             %
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
%  WriteFTXTImage() writes an image in the formatted text image format.
%
%  The format of the WriteFTXTImage method is:
%
%      MagickBooleanType WriteFTXTImage(const ImageInfo *image_info,
%        Image *image,ExceptionInfo *exception)
%
%  A description of each parameter follows.
%
%    o image_info: the image info.
%
%    o image:  The image.
%
%    o exception: return any errors or warnings in this structure.
%
*/
static MagickBooleanType WriteFTXTImage(const ImageInfo *image_info,Image *image,
  ExceptionInfo *exception)
{
  char
    buffer[MaxTextExtent],
    chSep,
    sSuff[2];

  const char
    *sChSep,
    *sFmt;

  const Quantum
    *p;

  int
    precision;

  MagickBooleanType
    status;

  MagickOffsetType
    scene;

  PixelInfo
    pixel;

  assert(image_info != (const ImageInfo *) NULL);
  assert(image_info->signature == MagickCoreSignature);
  assert(image != (Image *) NULL);
  assert(image->signature == MagickCoreSignature);
  if (IsEventLogging() != MagickFalse)
    (void) LogMagickEvent(TraceEvent,GetMagickModule(),"%s",image->filename);
  status=OpenBlob(image_info,image,WriteBinaryBlobMode,exception);
  if (status == MagickFalse)
    return(status);
  scene=0;
  precision=GetMagickPrecision();
  sFmt=GetImageArtifact(image,"ftxt:format");
  if (sFmt == (const char *) NULL)
    sFmt=dfltFmt;
  sChSep=GetImageArtifact(image,"ftxt:chsep");
  if (sChSep == (const char *) NULL)
    sChSep=dfltChSep;
  if ((sChSep[0]==chEsc) && ((sChSep[1] == 'n') || (sChSep[1] == 'N')))
    chSep='\n';
  else
    chSep=sChSep[0];
  sSuff[0]='\0';
  sSuff[1]='\0';

  do
  {
    long
      x,
      y;

    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++)
      {
        const char
          *pFmt;

        long double
          valMult;

        MagickBooleanType
          fltHexFmt,
          hexFmt;

        valMult=1.0;
        hexFmt=MagickFalse;
        fltHexFmt=MagickFalse;
        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':
              {
                char
                  sSep[2];

                ssize_t
                  i;

                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. */
                sSep[0]=sSep[1]='\0';
                for (i=0; i < (ssize_t) GetPixelChannels (image); i++)
                {
                  PixelChannel
                    channel;

                  PixelTrait
                    traits;

                  channel=GetPixelChannelChannel(image,i);
                  traits=GetPixelChannelTraits(image,channel);
                  if ((traits & UpdatePixelTrait) != UpdatePixelTrait)
                    continue;
                  if (hexFmt)
                    FormatLocaleString(buffer,MaxTextExtent,"%s#%llx",sSep,
                      (signed long long)(((long double) p[i])+0.5));
                  else if (fltHexFmt)
                    FormatLocaleString(buffer,MaxTextExtent,"%s%a",sSep,
                      (double) 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) &&
          (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:

%IMG7%magick identify -version
Version: ImageMagick 7.1.0-49 Q16-HDRI x64 7a3f3f1:20220924 https://imagemagick.org
Copyright: (C) 1999 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI OpenCL 
Delegates (built-in): bzlib cairo freetype gslib heic jng jp2 jpeg jxl lcms lqr lzma openexr pangocairo png ps raqm raw rsvg tiff webp xml zip zlib
Compiler: Visual Studio 2022 (193331630)

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 16-Mar-2023 05:37:35.

Copyright © 2023 Alan Gibson.