Batch Convert image files

Guile lets us use ImageMagick and potrace with the procedures system and system*. I use (system "ls") to check the files in the REPL's current working directory. Browsing the manual(fn:1) showed (getcwd) as a way to avoid (system "pwd"), and (chdir "directory-to-change-to") as a way to stay in scheme.

Image Magick's convert command is simple in this process, but the potrace command is long.

(define bmp2svg
  (lambda (bmp)
    "convert a .jpg of .png file to .svg file using system calls to ImageMagick's `convert' and then to `potrace'."
    (let ((pnm (replace-file-ext bmp "pnm"))
	  (svg (replace-file-ext bmp "svg")))
      (system* "convert" bmp pnm)
      (system* "potrace" "-b" "svg" "--tight" "-M" ".02" "--opaque" pnm "-o" svg))))
;; potrace gives error about no file if don't split all the arguments into separate quoted strings ... 

With this procedure to convert one image file, for-each make it easy to convert a list of files. With no need to return a list of files 'for-each' is appropriate while map is not. With bmps (bitmaps liks .jpg or .png files) as a list of filenames: (for-each bmp2svg bmps) will work. An executable script is convenient so the line goes into a main procedure as described in the manual.

The code (scandir dir (cut string-match ext <>)) relies on (use-modules (ice-9 ftw)) for scandir and (use-modules (srfi srfi-26))) for cut . Working with cut helps me think more on lambda in the comparison:

(scandir dir (lambda (s) (string-match ext s)))   
(scandir dir (cut string-match ext <>))

The .pnm files are needed to get from a bitmap file to an .svg vector file. But .pnm files are very big so, after the conversion, they get deleted: (for-each delete-file (scandir dir (cut string-match "pnm$" <>))).

(define main 
  (let ((bmps (scandir dir (cut string-match ext <>))))
    (for-each bmp2svg bmps)
    (write "svgs ok")(newline)
    (for-each delete-file (scandir dir (cut string-match "pnm$" <>)))))

To keep the conversion, system* in bmp2svg , and repetition, for-each in main code compact and manageable short filename-manipulation procedures are helpful. With emacs lisp f.el is convenient for compact code. These filename-manipulation or -adjustment procedures were influenced by experience with f.el.

(define get-file-ext
  (lambda (fname)
    "Get the extension from a filename with string-split and reverse"
    (car (reverse (string-split fname #\.)))))
;; <!--more--> 

(define get-ext-len
  (lambda (fname)
     "Get length of file extention using get-file-ext"
    (string-length (get-file-ext fname))))

(define get-file-base
  (lambda (fname)
    "Get basename of file using string-length, get-ext-len, and substring."
    (let ((flen (string-length fname))
	  (elen (get-ext-len fname)))
      (substring fname 0 (- flen (1+ elen))))))
;;    (string-take fname (- flen (1+ elen))))))

(define add-file-ext
  (lambda (bname fext)
    "Add to a basename a period '.' and a file extension."
    (string-append bname "." fext)))

(define replace-file-ext
  (lambda (fname next)
    "Remove from a filename its extension and replace the extension, using `get-file-base and `add-file-ext'."
    (let ((base (get-file-base fname)))
      (add-file-ext base next))))

Still without competence in Guile's command-line argument handling this script must be run while the current working directory is the directory with all the image files.

(define dir "./")
(define ext "jpg")

The code snippets on this page probably all have to be in the file in reverse order for the script to work. The top of the script needs a “POSIX” “she-bang”(?) line.

#!/usr/local/bin/guile -s
!#

And then the modules that define scandir, string-match, and cut need to come into the script:

(use-modules (ice-9 ftw)    ;; scandir
	     (ice-9 regex)  ;; string-match
	     (srfi srfi-26)) ;; cut

With a few small changes the script can take optional arguments to handle convert images other than .jpg files, such as .png files.

At the top of the script:

#!/usr/local/bin/guile \
-e main -s
!#

And then at the bottom

(define (main args)
  (let ((ext (if (cadr args)
		 (cadr args)
		 "jpg"))
	(dir "./"))
    (let ((bmps (scandir dir (cut string-match ext <>)))) 
      (for-each bmp2svg bmps)
      (write "svgs ok")(newline)
      (for-each delete-file (scandir dir (cut string-match ".pnm$" <>))))))

Later I'll have to think through how to make the dir directory optional also. But in the meantime this script is useful for me saved as a dot file in my home directory. At a shell prompt:

$ chmod +x "~/.jpgs-to-svgs"

Now I can move into a directory of image files and convert them to .svg files:

$ export GUILE_AUTO_COMPILE=0
$ ~/.jpgs-to-svgs

or

$ guile -s "~/.jpgs-to-svgs" --no-auto-compile

My scripts don't feel refined enough to make guile, or the system, compile them.

I wish there was a book version of the manual for Guile 2.2... I learned a lot (specific commands and general approaches), by browsing the manual at times when I don't have enough time or attention to work on the scripts.

#Guile #Scheme #Programming #Scripting #BatchConvert