1 ;;; mh-xface.el --- MH-E X-Face and Face header field display
3 ;; Copyright (C) 2002, 2003, 2005, 2006, 2007, 2008 Free Software Foundation, Inc.
5 ;; Author: Bill Wohler <wohler@newt.com>
6 ;; Maintainer: Bill Wohler <wohler@newt.com>
10 ;; This file is part of GNU Emacs.
12 ;; GNU Emacs is free software: you can redistribute it and/or modify
13 ;; it under the terms of the GNU General Public License as published by
14 ;; the Free Software Foundation, either version 3 of the License, or
15 ;; (at your option) any later version.
17 ;; GNU Emacs is distributed in the hope that it will be useful,
18 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ;; GNU General Public License for more details.
22 ;; You should have received a copy of the GNU General Public License
23 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
34 (autoload 'message-fetch-field
"message")
36 (defvar mh-show-xface-function
37 (cond ((and (featurep 'xemacs
) (locate-library "x-face") (not (featurep 'xface
)))
39 #'mh-face-display-function
)
40 ((>= emacs-major-version
21)
41 #'mh-face-display-function
)
43 "Determine at run time what function should be called to display X-Face.")
45 (defvar mh-uncompface-executable
46 (and (fboundp 'executable-find
) (executable-find "uncompface")))
53 (defun mh-show-xface ()
55 (when (and window-system mh-show-use-xface-flag
56 (or mh-decode-mime-flag mh-mhl-format-file
57 mh-clean-message-header-flag
))
58 (funcall mh-show-xface-function
)))
60 (defun mh-face-display-function ()
61 "Display a Face, X-Face, or X-Image-URL header field.
62 If more than one of these are present, then the first one found
63 in this order is used."
65 (goto-char (point-min))
66 (re-search-forward "\n\n" (point-max) t
)
67 (narrow-to-region (point-min) (point))
68 (let* ((case-fold-search t
)
69 (face (message-fetch-field "face" t
))
70 (x-face (message-fetch-field "x-face" t
))
71 (url (message-fetch-field "x-image-url" t
))
73 (cond (face (setq raw
(mh-face-to-png face
)
75 (x-face (setq raw
(mh-uncompface x-face
)
77 (url (setq type
'url
))
78 (t (multiple-value-setq (type raw
) (mh-picon-get-image))))
80 (goto-char (point-min))
81 (when (re-search-forward "^from:" (point-max) t
)
85 (mh-x-image-url-display url
)
87 insert-image
(create-image
90 (mh-face-foreground 'mh-show-xface nil t
)
92 (mh-face-background 'mh-show-xface nil t
))
98 (mh-x-image-url-display url
))
100 (when (featurep 'png
)
101 (set-extent-begin-glyph
102 (make-extent (point) (point))
103 (make-glyph (vector 'png
':data
(mh-face-to-png face
))))))
104 ;; Try internal xface support if available...
105 ((and (eq type
'pbm
) (featurep 'xface
))
107 (set-extent-begin-glyph
108 (make-extent (point) (point))
109 (make-glyph (vector 'xface
':data
(concat "X-Face: " x-face
))))
111 ;; Otherwise try external support with x-face...
113 (fboundp 'x-face-xmas-wl-display-x-face
)
114 (fboundp 'executable-find
) (executable-find "uncompface"))
115 (mh-funcall-if-exists x-face-xmas-wl-display-x-face
))
117 ((and raw
(member type
'(xpm xbm gif
)))
118 (when (featurep type
)
119 (set-extent-begin-glyph
120 (make-extent (point) (point))
121 (make-glyph (vector type
':data raw
))))))
122 (when raw
(insert " "))))))))
124 (defun mh-face-to-png (data)
125 "Convert base64 encoded DATA to png image."
127 (set-buffer-multibyte nil
)
129 (ignore-errors (base64-decode-region (point-min) (point-max)))
132 (defun mh-uncompface (data)
133 "Run DATA through `uncompface' to generate bitmap."
135 (set-buffer-multibyte nil
)
137 (when (and mh-uncompface-executable
138 (equal (call-process-region (point-min) (point-max)
139 mh-uncompface-executable t
'(t nil
))
144 (defun mh-icontopbm ()
145 "Elisp substitute for `icontopbm'."
146 (goto-char (point-min))
147 (let ((end (point-max)))
148 (while (re-search-forward "0x\\(..\\)\\(..\\)," nil t
)
150 (goto-char (point-max))
151 (insert (string-to-number (match-string 1) 16))
152 (insert (string-to-number (match-string 2) 16))))
153 (delete-region (point-min) end
)
154 (goto-char (point-min))
155 (insert "P4\n48 48\n")))
161 ;; XXX: This should be customizable. As a side-effect of setting this
162 ;; variable, arrange to reset mh-picon-existing-directory-list to 'unset.
163 (defvar mh-picon-directory-list
164 '("~/.picons" "~/.picons/users" "~/.picons/usenix" "~/.picons/news"
165 "~/.picons/domains" "~/.picons/misc"
166 "/usr/share/picons/" "/usr/share/picons/users" "/usr/share/picons/usenix"
167 "/usr/share/picons/news" "/usr/share/picons/domains"
168 "/usr/share/picons/misc")
169 "List of directories where picons reside.
170 The directories are searched for in the order they appear in the list.")
172 (defvar mh-picon-existing-directory-list
'unset
173 "List of directories to search in.")
175 (defvar mh-picon-cache
(make-hash-table :test
#'equal
))
177 (defvar mh-picon-image-types
178 (loop for type in
'(xpm xbm gif
)
179 when
(or (mh-do-in-gnu-emacs
181 (mh-funcall-if-exists image-type-available-p type
)))
182 (mh-do-in-xemacs (featurep type
)))
185 (autoload 'message-tokenize-header
"sendmail")
187 (defun* mh-picon-get-image
()
188 "Find the best possible match and return contents."
189 (mh-picon-set-directory-list)
191 (let* ((from-field (ignore-errors (car (message-tokenize-header
192 (mh-get-header-field "from:")))))
193 (from (car (ignore-errors
194 (mh-funcall-if-exists ietf-drums-parse-address
197 (string-match "\\([^+]*\\)\\(+.*\\)?@\\(.*\\)" from
)
198 (downcase (match-string 3 from
))))
199 (user (and host
(downcase (match-string 1 from
))))
200 (canonical-address (format "%s@%s" user host
))
201 (cached-value (gethash canonical-address mh-picon-cache
))
202 (host-list (and host
(delete "" (split-string host
"\\."))))
204 (cond (cached-value (return-from mh-picon-get-image cached-value
))
205 ((not host-list
) (return-from mh-picon-get-image nil
)))
209 (loop for dir in mh-picon-existing-directory-list
210 do
(loop for type in mh-picon-image-types
212 for file1
= (format "%s/%s.%s"
213 dir canonical-address type
)
214 when
(file-exists-p file1
)
215 do
(return-from 'loop file1
)
217 for file2
= (format "%s/%s.%s" dir user type
)
218 when
(file-exists-p file2
)
219 do
(return-from 'loop file2
)
221 for file3
= (format "%s/%s.%s" dir host type
)
222 when
(file-exists-p file3
)
223 do
(return-from 'loop file3
)))
225 ;; Search order for user@foo.net:
226 ;; [path]net/foo/user
227 ;; [path]net/foo/user/face
229 ;; [path]net/user/face
230 ;; [path]net/foo/unknown
231 ;; [path]net/foo/unknown/face
233 ;; [path]net/unknown/face
234 (loop for u in
(list user
"unknown")
235 do
(loop for dir in mh-picon-existing-directory-list
236 do
(loop for x on host-list by
#'cdr
237 for y
= (mh-picon-generate-path x u dir
)
238 do
(loop for type in mh-picon-image-types
239 for z1
= (format "%s.%s" y type
)
240 when
(file-exists-p z1
)
241 do
(return-from 'loop z1
)
242 for z2
= (format "%s/face.%s"
244 when
(file-exists-p z2
)
245 do
(return-from 'loop z2
)))))))
246 (setf (gethash canonical-address mh-picon-cache
)
247 (mh-picon-file-contents match
)))))
249 (defun mh-picon-set-directory-list ()
250 "Update `mh-picon-existing-directory-list' if needed."
251 (when (eq mh-picon-existing-directory-list
'unset
)
252 (setq mh-picon-existing-directory-list
253 (loop for x in mh-picon-directory-list
254 when
(file-directory-p x
) collect x
))))
256 (defun mh-picon-generate-path (host-list user directory
)
257 "Generate the image file path.
258 HOST-LIST is the parsed host address of the email address, USER
259 the username and DIRECTORY is the directory relative to which the
262 for elem in host-list
263 do
(setq acc
(format "%s/%s" elem acc
))
264 finally return
(format "%s/%s%s" directory acc user
)))
266 (defun mh-picon-file-contents (file)
267 "Return details about FILE.
268 A list of consisting of a symbol for the type of the file and the
269 file contents as a string is returned. If FILE is nil, then both
270 elements of the list are nil."
273 (set-buffer-multibyte nil
)
274 (let ((type (and (string-match ".*\\.\\(...\\)$" file
)
275 (intern (match-string 1 file
)))))
276 (insert-file-contents-literally file
)
277 (values type
(buffer-string))))
282 ;;; X-Image-URL Display
284 (defvar mh-x-image-scaling-function
285 (cond ((executable-find "convert")
286 'mh-x-image-scale-with-convert
)
287 ((and (executable-find "anytopnm") (executable-find "pnmscale")
288 (executable-find "pnmtopng"))
289 'mh-x-image-scale-with-pnm
)
291 "Function to use to scale image to proper size.")
293 (defun mh-x-image-scale-with-pnm (input output
)
294 "Scale image in INPUT file and write to OUTPUT file using pnm tools."
295 (let ((res (shell-command-to-string
296 (format "anytopnm < %s | pnmscale -xysize 96 48 | pnmtopng > %s"
298 (unless (equal res
"")
299 (delete-file output
))))
301 (defun mh-x-image-scale-with-convert (input output
)
302 "Scale image in INPUT file and write to OUTPUT file using ImageMagick."
303 (call-process "convert" nil nil nil
"-geometry" "96x48" input output
))
305 (defvar mh-wget-executable nil
)
306 (defvar mh-wget-choice
307 (or (and (setq mh-wget-executable
(executable-find "wget")) 'wget
)
308 (and (setq mh-wget-executable
(executable-find "fetch")) 'fetch
)
309 (and (setq mh-wget-executable
(executable-find "curl")) 'curl
)))
310 (defvar mh-wget-option
311 (cdr (assoc mh-wget-choice
'((curl .
"-o") (fetch .
"-o") (wget .
"-O")))))
312 (defvar mh-x-image-temp-file nil
)
313 (defvar mh-x-image-url nil
)
314 (defvar mh-x-image-marker nil
)
315 (defvar mh-x-image-url-cache-file nil
)
317 (defun mh-x-image-url-display (url)
318 "Display image from location URL.
319 If the URL isn't present in the cache then it is fetched with wget."
320 (let* ((cache-filename (mh-x-image-url-cache-canonicalize url
))
321 (state (mh-x-image-get-download-state cache-filename
))
322 (marker (set-marker (make-marker) (point))))
323 (set (make-local-variable 'mh-x-image-marker
) marker
)
324 (cond ((not (mh-x-image-url-sane-p url
)))
326 (mh-x-image-display cache-filename marker
))
327 ((or (not mh-wget-executable
)
328 (eq mh-x-image-scaling-function
'ignore
)))
330 ((not mh-fetch-x-image-url
)
331 (set-marker marker nil
))
332 ((eq state
'try-again
)
333 (mh-x-image-set-download-state cache-filename nil
)
334 (mh-x-image-url-fetch-image url cache-filename marker
335 'mh-x-image-scale-and-display
))
336 ((and (eq mh-fetch-x-image-url
'ask
)
337 (not (y-or-n-p (format "Fetch %s? " url
))))
338 (mh-x-image-set-download-state cache-filename
'never
))
340 (mh-x-image-url-fetch-image url cache-filename marker
341 'mh-x-image-scale-and-display
)))))
343 (defvar mh-x-image-cache-directory nil
344 "Directory where X-Image-URL images are cached.")
347 (defun mh-set-x-image-cache-directory (directory)
348 "Set the DIRECTORY where X-Image-URL images are cached.
349 This is only done if `mh-x-image-cache-directory' is nil."
350 ;; XXX This is the code that used to be in find-user-path. Is there
351 ;; a good reason why the variable is set conditionally? Do we expect
352 ;; the user to have set this variable directly?
353 (unless mh-x-image-cache-directory
354 (setq mh-x-image-cache-directory directory
)))
356 (defun mh-x-image-url-cache-canonicalize (url)
358 Replace the ?/ character with a ?! character and append .png.
359 Also replaces special characters with `mh-url-hexify-string'
360 since not all characters, such as :, are valid within Windows
361 filenames. In addition, replaces * with %2a. See URL
362 `http://msdn.microsoft.com/library/default.asp?url=/library/en-us/shellcc/platform/shell/reference/ifaces/iitemnamelimits/GetValidCharacters.asp'."
363 (format "%s/%s.png" mh-x-image-cache-directory
364 (mh-replace-regexp-in-string
366 (mh-url-hexify-string
369 (mh-replace-string "/" "!")
372 (defun mh-x-image-get-download-state (file)
373 "Check the state of FILE by following any symbolic links."
374 (unless (file-exists-p mh-x-image-cache-directory
)
375 (call-process "mkdir" nil nil nil mh-x-image-cache-directory
))
376 (cond ((file-symlink-p file
)
377 (intern (file-name-nondirectory (file-chase-links file
))))
378 ((not (file-exists-p file
)) nil
)
381 (defun mh-x-image-set-download-state (file data
)
382 "Setup a symbolic link from FILE to DATA."
384 (make-symbolic-link (symbol-name data
) file t
)
387 (defun mh-x-image-url-sane-p (url)
388 "Check if URL is something sensible."
389 (let ((len (length url
)))
390 (cond ((< len
5) nil
)
391 ((not (equal (substring url
0 5) "http:")) nil
)
395 (defun mh-x-image-display (image marker
)
396 "Display IMAGE at MARKER."
397 (with-current-buffer (marker-buffer marker
)
398 (let ((inhibit-read-only t
)
399 (buffer-modified-flag (buffer-modified-p)))
401 (when (and (file-readable-p image
) (not (file-symlink-p image
))
402 (eq marker mh-x-image-marker
))
405 (mh-funcall-if-exists insert-image
(create-image image
'png
)))
407 (when (featurep 'png
)
408 (set-extent-begin-glyph
409 (make-extent (point) (point))
411 (vector 'png
':data
(with-temp-buffer
412 (insert-file-contents-literally image
)
413 (buffer-string))))))))
414 (set-buffer-modified-p buffer-modified-flag
)))))
416 (defun mh-x-image-url-fetch-image (url cache-file marker sentinel
)
417 "Fetch and display the image specified by URL.
418 After the image is fetched, it is stored in CACHE-FILE. It will
419 be displayed in a buffer and position specified by MARKER. The
420 actual display is carried out by the SENTINEL function."
421 (if mh-wget-executable
422 (let ((buffer (get-buffer-create (generate-new-buffer-name
423 mh-temp-fetch-buffer
)))
424 (filename (or (mh-funcall-if-exists make-temp-file
"mhe-fetch")
425 (expand-file-name (make-temp-name "~/mhe-fetch")))))
426 (with-current-buffer buffer
427 (set (make-local-variable 'mh-x-image-url-cache-file
) cache-file
)
428 (set (make-local-variable 'mh-x-image-marker
) marker
)
429 (set (make-local-variable 'mh-x-image-temp-file
) filename
))
430 (set-process-sentinel
431 (start-process "*mh-x-image-url-fetch*" buffer
432 mh-wget-executable mh-wget-option filename url
)
435 (mh-x-image-set-download-state cache-file
'try-again
)))
437 (defun mh-x-image-scale-and-display (process change
)
438 "When the wget PROCESS terminates scale and display image.
439 The argument CHANGE is ignored."
440 (when (eq (process-status process
) 'exit
)
441 (let (marker temp-file cache-filename wget-buffer
)
442 (with-current-buffer (setq wget-buffer
(process-buffer process
))
443 (setq marker mh-x-image-marker
444 cache-filename mh-x-image-url-cache-file
445 temp-file mh-x-image-temp-file
))
447 ;; Check if we have `convert'
448 ((eq mh-x-image-scaling-function
'ignore
)
449 (message "The \"convert\" program is needed to display X-Image-URL")
450 (mh-x-image-set-download-state cache-filename
'try-again
))
451 ;; Scale fetched image
452 ((and (funcall mh-x-image-scaling-function temp-file cache-filename
)
454 ;; Attempt to display image if we have it
455 ((file-exists-p cache-filename
)
456 (mh-x-image-display cache-filename marker
))
457 ;; We didn't find the image. Should we try to display it the next time?
458 (t (mh-x-image-set-download-state cache-filename
'try-again
)))
460 (set-marker marker nil
)
461 (delete-process process
)
462 (kill-buffer wget-buffer
)
463 (delete-file temp-file
)))))
468 ;; indent-tabs-mode: nil
469 ;; sentence-end-double-space: nil
472 ;; arch-tag: a79dd33f-d0e5-4b19-a53a-be690f90229a
473 ;;; mh-xface.el ends here