org-download.el 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. ;;; org-download.el --- Image drag-and-drop for Emacs org-mode
  2. ;; Copyright (C) 2014-2016 Free Software Foundation, Inc.
  3. ;; Author: Oleh Krehel
  4. ;; Keywords: images, screenshots, download
  5. ;; Homepage: http://orgmode.org
  6. ;; This file is not part of GNU Emacs.
  7. ;; GNU Emacs is free software: you can redistribute it and/or modify
  8. ;; it under the terms of the GNU General Public License as published by
  9. ;; the Free Software Foundation, either version 3 of the License, or
  10. ;; (at your option) any later version.
  11. ;; GNU Emacs is distributed in the hope that it will be useful,
  12. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. ;; GNU General Public License for more details.
  15. ;; You should have received a copy of the GNU General Public License
  16. ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
  17. ;;; Commentary:
  18. ;;
  19. ;; This extension facilitates moving images from point A to point B.
  20. ;;
  21. ;; Point A (the source) can be:
  22. ;; 1. An image inside your browser that you can drag to Emacs.
  23. ;; 2. An image on your file system that you can drag to Emacs.
  24. ;; 3. A local or remote image address in kill-ring.
  25. ;; Use the `org-download-yank' command for this.
  26. ;; Remember that you can use "0 w" in `dired' to get an address.
  27. ;; 4. An screenshot taken using `gnome-screenshot' or `scrot' or `gm'.
  28. ;; Use the `org-download-screenshot' command for this.
  29. ;; Customize the backend with `org-download-screenshot-method'.
  30. ;;
  31. ;; Point B (the target) is an Emacs `org-mode' buffer where the inline
  32. ;; link will be inserted. Several customization options will determine
  33. ;; where exactly on the file system the file will be stored.
  34. ;;
  35. ;; They are:
  36. ;; `org-download-method':
  37. ;; a. 'attach => use `org-mode' attachment machinery
  38. ;; b. 'directory => construct the directory in two stages:
  39. ;; 1. first part of the folder name is:
  40. ;; * either "." (current folder)
  41. ;; * or `org-download-image-dir' (if it's not nil).
  42. ;; `org-download-image-dir' becomes buffer-local when set,
  43. ;; so each file can customize this value, e.g with:
  44. ;; # -*- mode: Org; org-download-image-dir: "~/Pictures/foo"; -*-
  45. ;; 2. second part is:
  46. ;; * `org-download-heading-lvl' is nil => ""
  47. ;; * `org-download-heading-lvl' is n => the name of current
  48. ;; heading with level n. Level count starts with 0,
  49. ;; i.e. * is 0, ** is 1, *** is 2 etc.
  50. ;; `org-download-heading-lvl' becomes buffer-local when set,
  51. ;; so each file can customize this value, e.g with:
  52. ;; # -*- mode: Org; org-download-heading-lvl: nil; -*-
  53. ;;
  54. ;; `org-download-timestamp':
  55. ;; optionally add a timestamp to the file name.
  56. ;;
  57. ;; Customize `org-download-backend' to choose between `url-retrieve'
  58. ;; (the default) or `wget' or `curl'.
  59. ;;
  60. ;;; Code:
  61. (eval-when-compile
  62. (require 'cl))
  63. (require 'url-parse)
  64. (require 'url-http)
  65. (defgroup org-download nil
  66. "Image drag-and-drop for org-mode."
  67. :group 'org
  68. :prefix "org-download-")
  69. (defcustom org-download-method 'directory
  70. "The way images should be stored."
  71. :type '(choice
  72. (const :tag "Directory" directory)
  73. (const :tag "Attachment" attach))
  74. :group 'org-download)
  75. (defcustom org-download-image-dir nil
  76. "If set, images will be stored in this directory instead of \".\".
  77. See `org-download--dir-1' for more info."
  78. :type '(choice
  79. (const :tag "Default" nil)
  80. (string :tag "Directory"))
  81. :group 'org-download)
  82. (make-variable-buffer-local 'org-download-image-dir)
  83. (defcustom org-download-heading-lvl 0
  84. "Heading level to be used in `org-download--dir-2'."
  85. :group 'org-download)
  86. (make-variable-buffer-local 'org-download-heading-lvl)
  87. (defcustom org-download-backend t
  88. "Method to use for downloading."
  89. :type '(choice
  90. (const :tag "wget" "wget \"%s\" -O \"%s\"")
  91. (const :tag "curl" "curl \"%s\" -o \"%s\"")
  92. (const :tag "url-retrieve" t))
  93. :group 'org-download)
  94. (defcustom org-download-timestamp "_%Y-%m-%d_%H:%M:%S"
  95. "This `format-time-string'-style string will be appended to the file name.
  96. Set this to \"\" if you don't want time stamps."
  97. :type 'string
  98. :group 'org-download)
  99. (defcustom org-download-img-regex-list
  100. '("<img +src=\"" "<img +\\(class=\"[^\"]+\"\\)? *src=\"")
  101. "This regex is used to unalias links that look like images.
  102. The html to which the links points will be searched for these
  103. regexes, one by one, until one succeeds. The found image address
  104. will be used."
  105. :group 'org-download)
  106. (defcustom org-download-screenshot-method "gnome-screenshot -a -f %s"
  107. "The tool to capture screenshots."
  108. :type '(choice
  109. (const :tag "gnome-screenshot" "gnome-screenshot -a -f %s")
  110. (const :tag "scrot" "scrot -s %s")
  111. (const :tag "gm" "gm import %s"))
  112. :group 'org-download)
  113. (defcustom org-download-image-width 0
  114. "When non-zero add #+attr_html: :width tag to the image."
  115. :type 'integer
  116. :group 'org-download)
  117. (defun org-download-get-heading (lvl)
  118. "Return the heading of the current entry's LVL level parent."
  119. (save-excursion
  120. (let ((cur-lvl (org-current-level)))
  121. (if cur-lvl
  122. (progn
  123. (unless (= cur-lvl 1)
  124. (org-up-heading-all (- (1- (org-current-level)) lvl)))
  125. (substring-no-properties
  126. (org-get-heading)))
  127. ""))))
  128. (defun org-download--dir-1 ()
  129. "Return the first part of the directory path for `org-download--dir'.
  130. It's `org-download-image-dir', unless it's nil. Then it's \".\"."
  131. (or org-download-image-dir "."))
  132. (defun org-download--dir-2 ()
  133. "Return the second part of the directory path for `org-download--dir'.
  134. Unless `org-download-heading-lvl' is nil, it's the name of the current
  135. `org-download-heading-lvl'-leveled heading. Otherwise it's \"\"."
  136. (and org-download-heading-lvl
  137. (org-download-get-heading
  138. org-download-heading-lvl)))
  139. (defun org-download--dir ()
  140. "Return the directory path for image storage.
  141. The path is composed from `org-download--dir-1' and `org-download--dir-2'.
  142. The directory is created if it didn't exist before."
  143. (let* ((part1 (org-download--dir-1))
  144. (part2 (org-download--dir-2))
  145. (dir (if part2
  146. (format "%s/%s" part1 part2)
  147. part1)))
  148. (unless (file-exists-p dir)
  149. (make-directory dir t))
  150. dir))
  151. (defun org-download--fullname (link)
  152. "Return the file name where LINK will be saved to.
  153. It's affected by `org-download-timestamp' and `org-download--dir'."
  154. (let ((filename
  155. (file-name-nondirectory
  156. (car (url-path-and-query
  157. (url-generic-parse-url link)))))
  158. (dir (org-download--dir)))
  159. (when (string-match ".*?\\.\\(?:png\\|jpg\\)\\(.*\\)$" filename)
  160. (setq filename (replace-match "" nil nil filename 1)))
  161. (abbreviate-file-name
  162. (expand-file-name
  163. (format "%s%s.%s"
  164. (file-name-sans-extension filename)
  165. (format-time-string org-download-timestamp)
  166. (file-name-extension filename))
  167. dir))))
  168. (defun org-download--image (link filename)
  169. "Save LINK to FILENAME asynchronously and show inline images in current buffer."
  170. (when (string-match "^file://\\(.*\\)" link)
  171. (setq link (url-unhex-string (match-string 1 link))))
  172. (cond ((and (not (file-remote-p link))
  173. (file-exists-p link))
  174. (org-download--image/command "cp \"%s\" \"%s\"" link filename))
  175. ((eq org-download-backend t)
  176. (org-download--image/url-retrieve link filename))
  177. (t
  178. (org-download--image/command org-download-backend link filename))))
  179. (defun org-download--image/command (command link filename)
  180. "Using COMMAND, save LINK to FILENAME.
  181. COMMAND is a format-style string with two slots for LINK and FILENAME."
  182. (require 'async)
  183. (async-start
  184. `(lambda() (shell-command
  185. ,(format command link
  186. (expand-file-name filename))))
  187. (lexical-let ((cur-buf (current-buffer)))
  188. (lambda(x)
  189. (with-current-buffer cur-buf
  190. (org-display-inline-images))))))
  191. (defun org-download--image/url-retrieve (link filename)
  192. "Save LINK to FILENAME using `url-retrieve'."
  193. (url-retrieve
  194. link
  195. (lambda (status filename buffer)
  196. ;; Write current buffer to FILENAME
  197. ;; and update inline images in BUFFER
  198. (let ((err (plist-get status :error)))
  199. (if err (error
  200. "\"%s\" %s" link
  201. (downcase (nth 2 (assq (nth 2 err) url-http-codes))))))
  202. (delete-region
  203. (point-min)
  204. (progn
  205. (re-search-forward "\n\n" nil 'move)
  206. (point)))
  207. (let ((coding-system-for-write 'no-conversion))
  208. (write-region nil nil filename nil nil nil 'confirm))
  209. (with-current-buffer buffer
  210. (org-display-inline-images)))
  211. (list
  212. (expand-file-name filename)
  213. (current-buffer))
  214. nil t))
  215. (defun org-download-yank ()
  216. "Call `org-download-image' with current kill."
  217. (interactive)
  218. (org-download-image (current-kill 0)))
  219. (defun org-download-screenshot ()
  220. "Capture screenshot and insert the resulting file.
  221. The screenshot tool is determined by `org-download-screenshot-method'."
  222. (interactive)
  223. (let ((link "/tmp/screenshot.png"))
  224. (shell-command (format org-download-screenshot-method link))
  225. (org-download-image link)))
  226. (defun org-download-image (link)
  227. "Save image at address LINK to `org-download--dir'."
  228. (interactive "sUrl: ")
  229. (unless (image-type-from-file-name link)
  230. (with-current-buffer
  231. (url-retrieve-synchronously link t)
  232. (let ((regexes org-download-img-regex-list)
  233. lnk)
  234. (while (and (not lnk) regexes)
  235. (goto-char (point-min))
  236. (when (re-search-forward (pop regexes) nil t)
  237. (backward-char)
  238. (setq lnk (read (current-buffer)))))
  239. (if lnk
  240. (setq link lnk)
  241. (error "link %s does not point to an image; unaliasing failed" link)))))
  242. (let ((filename
  243. (if (eq org-download-method 'attach)
  244. (let ((org-download-image-dir (progn (require 'org-attach)
  245. (org-attach-dir t)))
  246. org-download-heading-lvl)
  247. (org-download--fullname link))
  248. (org-download--fullname link))))
  249. (when (image-type-from-file-name filename)
  250. (org-download--image link filename)
  251. (when (eq org-download-method 'attach)
  252. (org-attach-attach filename nil 'none))
  253. (if (looking-back "^[ \t]+")
  254. (delete-region (match-beginning 0) (match-end 0))
  255. (newline))
  256. (insert
  257. (format "#+DOWNLOADED: %s @ %s\n%s [[%s]]"
  258. link
  259. (format-time-string "%Y-%m-%d %H:%M:%S")
  260. (if (= org-download-image-width 0)
  261. ""
  262. (format
  263. "#+attr_html: :width %dpx\n" org-download-image-width))
  264. filename))
  265. (org-display-inline-images))))
  266. (defun org-download--at-comment-p ()
  267. "Check if current line begins with #+DOWLOADED:."
  268. (save-excursion
  269. (move-beginning-of-line nil)
  270. (looking-at "#\\+DOWNLOADED:")))
  271. (defun org-download-delete ()
  272. "Delete inline image link on current line, and the file that it points to."
  273. (interactive)
  274. (cond ((org-download--at-comment-p)
  275. (delete-region (line-beginning-position)
  276. (line-end-position))
  277. (org-download--delete (line-beginning-position)
  278. nil
  279. 1))
  280. ((region-active-p)
  281. (org-download--delete (region-beginning)
  282. (region-end))
  283. (delete-region (region-beginning)
  284. (region-end)))
  285. (t (org-download--delete (line-beginning-position)
  286. (line-end-position)))))
  287. (defun org-download--delete (beg end &optional times)
  288. "Delete inline image links and the files they point to between BEG and END.
  289. When TIMES isn't nil, delete only TIMES links."
  290. (unless times
  291. (setq times most-positive-fixnum))
  292. (save-excursion
  293. (goto-char beg)
  294. (while (and (>= (decf times) 0)
  295. (re-search-forward "\\[\\[\\([^]]*\\)\\]\\]" end t))
  296. (let ((str (match-string-no-properties 1)))
  297. (delete-region beg
  298. (match-end 0))
  299. (when (file-exists-p str)
  300. (delete-file str))))))
  301. (defun org-download-dnd (uri action)
  302. "When in `org-mode' and URI points to image, download it.
  303. Otherwise, pass URI and ACTION back to dnd dispatch."
  304. (cond ((eq major-mode 'org-mode)
  305. ;; probably shouldn't redirect
  306. (unless (org-download-image uri)
  307. (message "not an image URL")))
  308. ((eq major-mode 'dired-mode)
  309. (org-download-dired uri))
  310. ;; redirect to someone else
  311. (t
  312. (let ((dnd-protocol-alist
  313. (rassq-delete-all
  314. 'org-download-dnd
  315. (copy-alist dnd-protocol-alist))))
  316. (dnd-handle-one-url nil action uri)))))
  317. (defun org-download-dired (uri)
  318. "Download URI to current directory."
  319. (raise-frame)
  320. (let ((filename (file-name-nondirectory
  321. (car (url-path-and-query
  322. (url-generic-parse-url uri))))))
  323. (message "Downloading %s to %s ..."
  324. filename
  325. (expand-file-name filename))
  326. (url-retrieve
  327. uri
  328. (lambda (status filename)
  329. (let ((err (plist-get status :error)))
  330. (if err (error
  331. "\"%s\" %s" uri
  332. (downcase (nth 2 (assq (nth 2 err) url-http-codes))))))
  333. (let ((coding-system-for-write 'no-conversion))
  334. (write-region nil nil filename nil nil nil 'confirm)))
  335. (list
  336. (expand-file-name filename))
  337. t t)))
  338. (defun org-download-enable ()
  339. "Enable org-download."
  340. (unless (eq (cdr (assoc "^\\(https?\\|ftp\\|file\\|nfs\\)://" dnd-protocol-alist))
  341. 'org-download-dnd)
  342. (setq dnd-protocol-alist
  343. `(("^\\(https?\\|ftp\\|file\\|nfs\\)://" . org-download-dnd) ,@dnd-protocol-alist))))
  344. (defun org-download-disable ()
  345. "Disable org-download."
  346. (rassq-delete-all 'org-download-dnd dnd-protocol-alist))
  347. (org-download-enable)
  348. (provide 'org-download)
  349. ;;; org-download.el ends here