blog tags:

About:

I'm Dmitry Popov,
lead developer and director of Infognition.

Known in the interwebs as Dee Mon since 1997. You could see me as thedeemon on reddit or LiveJournal.

RSS
Articles Technology Blog News Forum Company
Blog
D as a scripting language
September 12, 2014

Many of us encounter small tasks involving some file operations, text processing and running other processes. These are usually solved using either shell or glue languages (often called scripting languages because of this) like Python, Perl or Ruby. Compiled statically typed languages are rarely used in such cases because popular ones usually require too much effort to write and run a simple script. However some static languages can actually be used here easily instead of scripting ones, providing their usual benefits such as checking for types and typos. Here's an example of using D in such scenario.

Earlier this year I discovered a nice tool called TimeSnapper that sits in background and saves everything that happens on your screen by doing screenshots on regular time intervals. It can also collect some stats about apps you run and sites you browse. During first month I used this feature in the demo version and proved my suspicion that I waste way too much time on reddit and LiveJournal. ;) Later I switched to free version of TimeSnapper that doesn't collect and report stats but just makes the screenshots and allows playing them like a video with timeline. This serves me as a precise visual memory so that later I can always see what happened at certain moments. Found a weird unwanted shortcut on my desktop, what app placed it here? Quick look at its creation time, replay what I was doing then, ah, here's the thing I was installing at that moment. Here's a library I built on day X, but in which project was I using it? Replay that hour, see the opened project, ah, now I remember.

The problem is however that TimeSnapper saves screenshots as PNG files. I configured it to save one every minute, so the shots fill up 5 GBs in a few days, after which TS deletes the old ones. This is wasteful. Since most consecutive shots are similar (most of the screen doesn't change much if at all) a video format that stores just the difference will take much less space, even if the compression is still lossless. Moreover, I even know a video codec that is just perfect for such screen captures: ScreenPressor! So I want to convert sets of PNG images to video files compressed with that codec. What tools do I have for this task? VirtualDub can do, when used manually via its UI, not sure about its command-line interface though. And it won't open a sequence of those PNGs so easily because their file names (given by TimeSnapper) look like timestamps, not consequent numbers. Then I remember about AviSynth I have installed. It can handle a sequence of PNGs (again, if files are named properly) and present it as uncompressed video. And then I need a tool that will compress this video to an AVI file. It's found quickly: AVS2AVI. Having ScreenPressor, AviSynth and AVS2AVI on board all I need is a simple script that will orchestrate them, preparing the files and AVS script and running the tool.

Below is full source code of my script, in D. It should be run from the directory where TimeSnapper stores its images. For each day TS will create a folder named after this date. Inside will be PNG files with timestamps in their filenames. My script scans current directory for subfolders and for each subfolder (except for today's) it creates a video file. To make a video file it renames the PNG files to 0001.png, 0002.png, etc. so that AviSynth could open the sequence, remembering for each renamed file its original name. Then a text file is created: AVS script that tells AviSynth what to do. It opens the image sequence, and applies some resizing. Then avs2avi.exe is executed with the AVS script file, output file and codec id as arguments. After its execution ends, the images are renamed back so that TimeSnapper could find them. When my program is run with argument "delete" it will also delete the images in those subfolders for which corresponding video file already exists.

import std.stdio, std.file, std.array, std.algorithm, std.string, std.range, 
       std.process, std.datetime, std.typecons;

void compressImages(string dirname, bool deleting)
{
  if (dirname.startsWith(".\\")) dirname = dirname[2..$];
  auto now = Clock.currTime;
  string today = format("%04d-%02d-%02d", now.year, now.month, now.day);
  if (dirname == today) return writeln("skipping ", dirname, " - today.");
  auto files = dirEntries(dirname, "*.png", SpanMode.shallow, false).array;
  if (files.empty) return;  
  auto outfile = dirname ~ ".avi";
  if (outfile.exists) {
    if (deleting) {
      foreach(fname; files) 
        remove(fname);
      return writeln(dirname, ": ", files.length, " files deleted.");
    } else return writeln(outfile," already exists.");
  }
  sort(files);
  Tuple!(string,string)[] renamings;
  foreach(i, fname; files) {
    string newname = format("%s\\%04d.png", dirname, i);
    if (fname != newname) {
      renamings ~= tuple(cast(string)fname, newname);
      rename(fname, newname);
    }
  }
  auto f = File("conv.avs","wt");
  f.writefln("clip = ImageSource(\"%s\\%%04d.png\", 0, %d, 4)", dirname, files.length - 1);
  f.writeln("clip.LanczosResize(clip.width / 2, clip.height / 2)");
  f.close;
  auto args = cast(char[][])["avs2avi.exe", "conv.avs", outfile, "-c", "scpr"];
  writeln(args);
  execute(args);
  foreach(t; renamings)
    rename(t[1], t[0]);
}

void main(string[] argv)
{
  bool deleting = argv.length > 1 && argv[1]=="delete";
  foreach(d; dirEntries(".", "*", SpanMode.shallow, false).filter!(nm => nm.isDir)) 
    compressImages(d, deleting);  
}

As you can see the source code is quite short, clear and straightforward. You don't see many type annotations and declarations, there is no boilerplate usually associated with statically typed languages. The same script if written in Python or Ruby would be about the same size. How do we run it? Just like in Ruby you would just type "ruby script.rb" here you just type "rdmd script.d". rdmd (which comes with main D compiler - DMD) will compile and run the program, caching the binary so that subsequent calls will skip compilation and run the ready binary straight away. That means the first run is fast, the following runs are super fast.

If you want to use this script too but don't have DMD installed, I compiled it to an exe file and packed together with ScreenPressor and AVS2AVI in one archive here. All you need to have installed (besides TimeSnapper) is AviSynth. This is, by the way, another advantage of using D instead of usual scripting languages: it's easy to make a binary that can be run without installing any interpreters, frameworks or runtimes, and it's just a few hundred KBs, not several MBs you would get using Python-to-exe or Ruby-to-exe packers.

Here's an example result of using TimeSnapper and stuff from this post: video of my participation in this year's ICFP contest:

Here a compiler was written in D and the newly created language was used to program a bot to play Pacman. Unfortunately I got a working solution only one hour after the contest ended, so the version submitted was a very dirty work-in-progress that crashed often due to wrong arguments order in one function. That brain-dead version happened to gain a positive number of points but of course landed near the end of scoreboard. A working version with source code can be found here.