Xah Lee, 2007-10-24
This page shows a example of writing a emacs lisp function that creates a customized HTML inline image link. If you don't know elisp, first take a gander at Emacs Lisp Basics.
I want to write a command, so that, when invoked, emacs will turn a image file's path string under the cursor into a HTML inline image link, with width and height attributes and a alt text based on the file name.
This lesson will also show you how to call shell commands and process their results.
I work a lot with HTML in emacs. Often, i need to create a inline image. Emac's html-mode provides a shortcut “Ctrl+c Ctrl+c i” (invokes the command html-image), which will prompt you to type in a file path, then will insert the tag like this:
<img src="‹fpath›" alt="">
and place your cursor in between the quotes in alt. However, this is relatively inconvenient and inadequate for few reasons. It is inconvenient because when it prompts for a path, it doesn't lets the user navigate a directory to find the file. To work around this, the user should use dired and copy the file path into the clipboard (kill-ring) first, so that when prompted, she can paste it.
This method is inadequate because it doesn't generate the width and height attributes. Width and height attributes are important because they allow browser to quickly settle down on a layout before actual loading the image. Without the width and height attribute, a loading webpage's layout will jump around as it discovers the actual dimensions of inline images. (unless the inline images are set inside tables ...)
Ideally, i'd like to type in a path name on the current buffer, such as “../img/emacs_logo.png”, then press a button, and emacs will automatically turn that line into a inline image, with width and height attributes, and with alt partially filled by using the file name and removing suffix and replacing underscore by space. We proceed to write this function.
First we break down the problem into a few necessary functional units. We'll need:
We proceed to write these. First, we write the string processing function. Here's the code.
(defun xx () "temp func to experiment and learn" (interactive) (let (x) (setq x "my_love_some.png") (setq x (replace-regexp-in-string "\\.[A-Za-z]\\{3,4\\}$" "" x t t)) (setq x (replace-regexp-in-string "_" " " x t t)) (insert x) ) )
The key in writing this function is to find or realize that string replacement for a given string, is “replace-regexp-in-string”. This is found under the elisp manual under section “34 Searching and Matching”. There's no magic in locating this function, just good old experience and doc reading.
Reference: Elisp Manual: Search-and-Replace.
The function takes 3 required parameters: a regex string, a replacement string, a string to replace with. The function takes 3 optional parameters. The first 2 optional parameters are “fixedcase” and “literal”. If “fixedcase” is true, than emacs will not try to change the replacement string's case (i.e. uppercase/lowercase) based on the case of the matched string. If “literal” is true, the replace string is taken literally. i.e. “/1” won't mean the first matched string.
One thing interesting to note here is that in the regex, the backslashes are doubled, because a backslash needs a backslash to represent itself, then this is passed into emacs regex engine, which requires a backslash for some constructs. (See also: Emacs Regex).
With this function, you can just run it “Alt+x xx” and see the result inserted into the buffer. Undo to undo the effect. Using a temp function like this is a easy way develop in elisp. After you know how required functions work, you can gather them and put them together into your final code.
Now, here's the code for getting the image width and height.
(defun xGetDimention () "temp func to experiment and learn" (interactive) (let (img-file-path cmd-name sh-output width height) (setq img-file-path "~/i/xyz.png") (setq cmd-name "identify") (setq sh-output (shell-command-to-string (concat cmd-name " " img-file-path))) ;; sample output from “identify”: ;; xyz.png PNG 520x429+0+0 DirectClass 8-bit 9.1k 0.0u 0:01 (string-match "^[^ ]+ [^ ]+ \\([0-9]+\\)x\\([0-9]+\\)" sh-output) (setq width (match-string 1 sh-output)) (setq height (match-string 2 sh-output)) (insert (concat width " " height)) ))
Here, we call the command line tool “identify”, which is from the ImageMagick suite. (See: ImageMagick Tutorial) The command “identify” will return a string like this: “menu.png PNG 520x429+0+0 DirectClass 8-bit 9.1k 0.0u 0:01”, where the first 2 numbers after the PNG is the width and height of the image. (see bottom of this page for a pure elisp version)
The basic plan for xGetDimention is to call the shell command “identify”, parse its result, and return the height and width.
The key function here is “shell-command-to-string”. Its output is parsed by a regex match using “string-match”.
Reference: Elisp Manual: Synchronous-Processes.
Now, we clean this function up into a final form. We choose a proper name for the function, make it take a path as argument and return a list, and add a properly doc string.
(defun get-image-dimensions (img-file-path) "Returns a image file's width and height as a list. This function requires ImageMagick's “identity” shell command." (let (cmd-name sh-output width height) (setq cmd-name "identify") (setq sh-output (shell-command-to-string (concat cmd-name " " img-file-path))) ; sample output from “identify”: ; xyz.png PNG 520x429+0+0 DirectClass 8-bit 9.1k 0.0u 0:01 (string-match "^[^ ]+ [^ ]+ \\([0-9]+\\)x\\([0-9]+\\)" sh-output) (setq width (match-string 1 sh-output)) (setq height (match-string 2 sh-output)) (list (string-to-number width) (string-to-number height))))
Note here that now get-image-dimensions takes a img-file-path argument, and returns a list of 2 elements, both are numbers. I also removed the “(interactive)” line.
Now, we are ready to write a wrapper function to put the whole thing together.
(defun image-linkify () "Replace a path to image file with a HTML img tag. Example, if cursor is on the word “emacs_logo.png”, then it will became “<img src=\"emacs_logo.png\" alt=\"emacs logo\" width=\"123\" height=\"456\">”. This function requires the “identify” command from ImageMagick.com." (interactive) (let (img-file-path bounds img-dim width height altText myResult) (setq img-file-path (thing-at-point 'filename)) (setq bounds (bounds-of-thing-at-point 'filename)) (setq altText img-file-path) (setq altText (replace-regexp-in-string "\\.[A-Za-z]\\{3,4\\}$" "" altText t t)) (setq altText (replace-regexp-in-string "_" " " altText t t)) (setq img-dim (get-image-dimensions img-file-path)) (setq width (number-to-string (car img-dim))) (setq height (number-to-string (car (last img-dim)))) (setq myResult (concat "<img src=\"" img-file-path "\"" " " "alt=\"" altText "\"" " " "width=\"" width "\" " "height=\"" height "\">")) (save-excursion (delete-region (car bounds) (cdr bounds)) (insert myResult)) ))
The key in this wrapper, are the functions thing-at-point, bounds-of-thing-at-point, save-excursion, delete-region, insert. All these are very frequently used functions. “thing-at-point” will return a string by grabbing the text around the current cursor position. The “thing” can be a word, line, sentence, paragraph, or file path, url, sexp, etc. It saves you the time to actually write code to move around and grab the string you want. The bounds-of-thing-at-point returns a pair of the positions of the thing, so that you can delete it easily in the buffer.
Now, we are done. Now i can assign this function a shortcut “(global-set-key (kbd "<f3>") 'image-linkify)”. So if i want to insert a inline image, i paste into my buffer:
emacs_logo.png
press the f3, then i got:
<img src="emacs_logo.png" alt="emacs logo" width="65" height="82">
this is of tremendous help if you work with manually-crafted sites with thousands of inline images. For example, my website Visual Dictionary of Special Plane Curves, Gallery of Famous Surfaces, Visual Arts Gallery.
Here is a practical story of how i used this function in my work. One time i need to create a web gallery of images generated by a batgirl drawing craze that happened on the blog site livejournal.com. The general process is to collect the images from various blog sites (onto my hard-drive), create html pages, link these images as inline images, gather permission and write comment on them (such as the artist and source). Literally there were thousands of images i need to exam and ends up having about 80 images in the final gallery. Such work is naturally not within any automation system's reach, and will take me actually far more time if done with a blog site or wiki system. (See here for the page: Batgirl Craze)
So, i put some images that i decided should all be on one page of the gallery, into a directory, then type “Ctrl+u Alt+! ls dir_name/*png”, so that emacs lists them in my buffer like this:
_p/bg/0004kt1h.png _p/bg/batgirl-1.png _p/bg/batgirl.png _p/bg/batgirl6mc.png _p/bg/batgirl_abs.png _p/bg/batgirl_squat.png _p/bg/bg2.png _p/bg/dadsgonnakillme.png _p/bg/truebatgirl.png ...
Then, i just move the cursor to a line, press a button, then press arrow down, and hit the button again, and so on, and each time a line is turned into a link. (occationally i record it as a macro then do apply-macro-to-region-lines, but usually not worth the trouble)
Emacs is beautiful!
In the above, we relied on ImageMagick's command line tool “identify”. However, it is not necessary. The following pure elisp code will get the image's width and height.
(defun get-image-dimensions (img-file-relative-path) "Returns a image file's width and height as a list." (let (tmp dimen) (clear-image-cache) (setq tmp (create-image (concat default-directory img-file-relative-path))) (setq dimen (image-size tmp t)) (list (car dimen) (cdr dimen)) ) )
Related essays:
Page created: 2007-10. © 2007 by Xah Lee.