Over a year ago, I hit a dead end on Windows 10 with David Himmelstrup’s reanimate
package. I returned to the package with a simple animation in mind. In the interim, things had moved on. I had no difficulty creating the animation and rendering it to a GIF image:
Dependencies
On Windows 10, the Haskell Tool Stack provides MSYS2 on the path in the stack exec
environment. The MSYS2 package manager (pacman
) can be used to install FFmpeg (executable ffmpeg
), rsvg-convert
(provided with package librsvg
), and ImageMagick (executable magick
):
1 2 3 |
stack exec -- pacman --sync mingw64/mingw-w64-x86_64-ffmpeg stack exec -- pacman --sync mingw64/mingw-w64-x86_64-librsvg stack exec -- pacman --sync mingw64/mingw-w64-x86_64-imagemagick |
The versions of FFmpeg available on Windows 10 are, however, not compiled with option --enable-librsvg
. This means that the Has ffmpeg(rsvg)
dependency is no
.
POV-Ray for Windows, Blender and Inkscape can each be downloaded and its executable’s location added to the PATH
.
LaTeX can be downloaded via the MikTex project, which also includes the dvisvgm
tool. The ctex
package requires the font SimHei
, which is provided by installing the Windows Language Pack for Chinese (Simplified, China).
With the dependencies in place, almost all of the reanimate
checks passed:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
reanimate checks: Has ffmpeg: 4.4 Has ffmpeg(rsvg): no Has dvisvgm: C:\Users\mike\AppData\Local\Programs\MiKTeX\miktex\bin\x64\dvisvgm.exe Has povray: C:\Program Files\POV-Ray\v3.7\bin\pvengine64.exe Has blender: 2.93.5 Has rsvg-convert: 2.50.3 Has inkscape: 1.1.1 Has imagemagick: 7.0.10-11 Has LaTeX: C:\Users\mike\AppData\Local\Programs\MiKTeX\miktex\bin\x64\latex.exe Has LaTeX package 'babel': OK Has LaTeX package 'preview': OK Has LaTeX package 'amsmath': OK Has XeLaTeX: C:\Users\mike\AppData\Local\Programs\MiKTeX\miktex\bin\x64\xelatex.exe Has XeLaTeX package 'ctex': OK |
POV-Ray for Windows
POV-Ray is described in the reanimate
documentation as one of the pillars on which the package is built. However, I discovered that using POV-Ray for Windows with the package was not straightforward.
On Unix-like operating systems, POV-Ray is a command-line application and the executable is named povray
. On Windows, POV-Ray uses a graphical user interface (GUI) and the 64-bit version of the executable is named pvengine64.exe
.
pvengine64
can be used from the command prompt. However, it needs a special command-line option /EXIT
. As the POV-Ray documentation explains: the /EXIT
command tells POV-Ray to perform the render given by the other command-line options, combined with previously-set options such as internal command-line settings, INI file, source file, and so forth, and then to exit. By default, if this switch is not present, POV-Ray for Windows will remain running after the render is complete.
The reanimate
package was making use of povray
via System.Process.readProcessWithExitCode
, but I could not get pvengine64
to do the same. It seemed to be interpreting the first argument as the name of a POV-Ray INI
file.
On Windows 10, readProcessWithExitCode
leads to System.Process.Windows.createProcess_Internal_wrapper
and its use of commandToProcess
and translateInternal
. translateInternal
wraps arguments in quotation marks:
1 2 3 4 5 6 7 |
translateInternal :: String -> String translateInternal xs = '"' : snd (foldr escape (True,"\"") xs) where escape '"' (_, str) = (True, '\\' : '"' : str) escape '\\' (True, str) = (True, '\\' : '\\' : str) escape '\\' (False, str) = (False, '\\' : str) escape c (_, str) = (False, c : str) |
The foldr
works from right to left along the string, escaping backslash characters (if the ‘quotation’ mode is on) and double quotation mark characters. ‘Quotation` mode is on to begin with, is turned on by a quotation character, and is turned off by a character that is not a backslash or quotation character. So, +A
translates as "+A"
, and C:\Program Files\POV-Ray\v3.7\bin\pvengine64.exe
translates as "C:\Program Files\POV-Ray\v3.7\bin\pvengine.exe"
.
This escaping appears to be consistent with the parsing carried out by Microsoft’s CommandLineToArgW
function. However, POV-Ray for Windows does not appear to use that function to parse a command line but a C function parse_commandline
in source code file pvengine.cpp
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
int parse_commandline (char *s) { char *prevWord = NULL; char inQuote = '\0'; static char str [_MAX_PATH * 3]; static char filename [_MAX_PATH]; argc = 0; GetModuleFileName (hInstance, filename, sizeof (filename) - 1); argv [argc++] = filename; s = strncpy (str, s, sizeof (str) - 1); while (*s) { switch (*s) { case '"': case '\'': if (inQuote) { if (*s == inQuote) inQuote = 0; } else { inQuote = *s; if (prevWord == NULL) prevWord = s; } break ; case ' ': case '\t': if (!inQuote) { if (prevWord != NULL) { *s = '\0'; argv [argc++] = prevWord; prevWord = NULL; } } break; default : if (prevWord == NULL) prevWord = s; break; } if (argc >= MAX_ARGV - 1) break; s++; } if (prevWord != NULL && argc < MAX_ARGV - 1) argv [argc++] = prevWord; argv [argc] = NULL; return (argc); } |
The C algorithm is convoluted, but if s
points to a null-terminated "+A"
, then prevWord
will point to the first quotation character and argv[1]
will point to null-terminated "+A"
. That is, the quotation marks around +A
are preserved by the parser. This can also be seen in the output of the POV-Ray for Windows program in the Messages window.
This appears to be problematic, because POV-Ray expects command-line switches to begin with a +
or a -
, and everything else is assumed to be a path to a POV-Ray INI
file. The main
function in source file povmain.cpp
includes the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
try { ProcessRenderOptions renderoptions; POVMSObject obj; int l = 0; err = POVMSObject_New(&obj, kPOVObjectClass_IniOptions); if(err != kNoErr) throw POV_EXCEPTION_CODE(err); for(i = 1 ;i < argc; i++) { if(pov_stricmp(argv[i], "-povms") != 0) { err = renderoptions.ParseString(argv[i], &obj, true); if(err != kNoErr) throw POV_EXCEPTION_CODE(err); } } |
ParseString
is applied to each of the argv
in turn, with the single switch flag set to true
.
ProcessRenderOptions
is a subclass of ProcessOptions
, which class provides the function ParseString
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
int ProcessOptions::ParseString(const char *commandline, POVMSObjectPtr obj, bool singleswitch) { int err = kNoErr; // read the whole command-line to the end while ((*commandline != 0) && (err == kNoErr)) { if (singleswitch == false) ... switch (*commandline) { // end of command-line case 0: break; // quoted string case '\"': case '\'': err = kFalseErr; break; // switch case '+': case '-': #if (POV_SLASH_IS_SWITCH_CHARACTER) // POV-Ray-style INI file with system specific command-line switch on some systems (e.g. Windos) case '/': #endif commandline++; err = Parse_CL_Switch(commandline, *(commandline - 1), obj, singleswitch); break; // INI file style option default: if (isalnum(*commandline) || (*commandline == '_')) { const char *cltemp = commandline; err = Parse_CL_Option(commandline, obj, singleswitch); if (err == kParseErr) { commandline = cltemp; err = kFalseErr; } } else err = kFalseErr; break; } // if nothing else was appropriate, assume it is some other kind of string requiring // special attention if (err == kFalseErr) { int chr = *commandline; char *plainstring = nullptr; if ((chr == '\"') || (chr == '\'')) { commandline++; plainstring = Parse_CL_String(commandline, chr); } // if there were no quotes, just read up to the next space or newline else if (singleswitch == false) ... else plainstring = Parse_CL_String(commandline, 0); if (err == kFalseErr) err = ProcessUnknownString(plainstring, obj); if (plainstring != nullptr) delete[] plainstring; } } // all errors here are non-fatal, the calling code has to decide if an error is fatal if (err != kNoErr) { if (*commandline != 0) { ParseError("Cannot process command-line at '%s' due to a parse error.\n" "This is not a valid command-line. Check the command-line for syntax errors, correct them, and try again.\n" "Valid command-line switches are explained in detail in the reference part of the documentation.", commandline); } else { ParseError("Cannot process command-line due to a parse error.\n" "This is not a valid command-line. Check the command-line for syntax errors, correct them, and try again.\n" "Valid command-line switches are explained in detail in the reference part of the documentation."); } } return err; } |
The switch based on the first character pointed to by commandline
shows that only +
, -
, and /
(on Windows) get to Parse_CL_Switch
, while "
and '
(via err = kFalseErr
) get to Parse_CL_String
.
The work around (or, at least, the immediate one) for reanimate
was to write the pvengine64
command to a temporary batch file and then use readProcessWithExitCode
to run that batch file as a command. I also considered using readCreateProcessWithExitCode $ shell command
(which uses CMD.EXE \C <command>
) but, surprisingly, that seemed to about 5% slower.