;;; org-download.el --- Image drag-and-drop for Emacs org-mode. -*- lexical-binding: t -*-
;; Copyright (C) 2014-2019 Free Software Foundation, Inc.
;; Author: Oleh Krehel
;; URL: https://github.com/abo-abo/org-download
;; Package-Version: 20191016.1227
;; Version: 0.1.0
;; Package-Requires: ((async "1.2"))
;; Keywords: images, screenshots, download
;; This file is not part of GNU Emacs.
;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see .
;;; Commentary:
;;
;; This extension facilitates moving images from point A to point B.
;;
;; Point A (the source) can be:
;; 1. An image inside your browser that you can drag to Emacs.
;; 2. An image on your file system that you can drag to Emacs.
;; 3. A local or remote image address in kill-ring.
;; Use the `org-download-yank' command for this.
;; Remember that you can use "0 w" in `dired' to get an address.
;; 4. An screenshot taken using `gnome-screenshot' or `scrot' or `gm'.
;; Use the `org-download-screenshot' command for this.
;; Customize the backend with `org-download-screenshot-method'.
;;
;; Point B (the target) is an Emacs `org-mode' buffer where the inline
;; link will be inserted. Several customization options will determine
;; where exactly on the file system the file will be stored.
;;
;; They are:
;; `org-download-method':
;; a. 'attach => use `org-mode' attachment machinery
;; b. 'directory => construct the directory in two stages:
;; 1. first part of the folder name is:
;; * either "." (current folder)
;; * or `org-download-image-dir' (if it's not nil).
;; `org-download-image-dir' becomes buffer-local when set,
;; so each file can customize this value, e.g with:
;; # -*- mode: Org; org-download-image-dir: "~/Pictures/foo"; -*-
;; 2. second part is:
;; * `org-download-heading-lvl' is nil => ""
;; * `org-download-heading-lvl' is n => the name of current
;; heading with level n. Level count starts with 0,
;; i.e. * is 0, ** is 1, *** is 2 etc.
;; `org-download-heading-lvl' becomes buffer-local when set,
;; so each file can customize this value, e.g with:
;; # -*- mode: Org; org-download-heading-lvl: nil; -*-
;;
;; `org-download-timestamp':
;; optionally add a timestamp to the file name.
;;
;; Customize `org-download-backend' to choose between `url-retrieve'
;; (the default) or `wget' or `curl'.
;;
;;; Code:
(eval-when-compile
(require 'cl))
(require 'async)
(require 'url-parse)
(require 'url-http)
(require 'org)
(defgroup org-download nil
"Image drag-and-drop for org-mode."
:group 'org
:prefix "org-download-")
(defcustom org-download-method 'directory
"The way images should be stored."
:type '(choice
(const :tag "Directory" directory)
(const :tag "Attachment" attach)
(function :tag "Custom function")))
(defcustom org-download-image-dir nil
"If set, images will be stored in this directory instead of \".\".
See `org-download--dir-1' for more info."
:type '(choice
(const :tag "Default" nil)
(string :tag "Directory")))
(make-variable-buffer-local 'org-download-image-dir)
(defcustom org-download-heading-lvl 0
"Heading level to be used in `org-download--dir-2'."
:type 'integer)
(make-variable-buffer-local 'org-download-heading-lvl)
(defvar org-download-path-last-file nil
"Variable to hold the full path of the last downloaded file.
See `org-download-rename-last-file'.")
(defcustom org-download-backend t
"Method to use for downloading."
:type '(choice
(const :tag "wget" "wget \"%s\" -O \"%s\"")
(const :tag "curl" "curl \"%s\" -o \"%s\"")
(const :tag "url-retrieve" t)))
(defcustom org-download-timestamp "%Y-%m-%d_%H-%M-%S_"
"This `format-time-string'-style string will be appended to the file name.
Set this to \"\" if you don't want time stamps."
:type 'string)
(defcustom org-download-img-regex-list
'("/dev/null")
(const :tag "xfce4-screenshooter" "xfce4-screenshooter -r -o cat > %s")
;; screenshot method in ms-windows, /capture=4 stands for interactive.
(const :tag "IrfanView" "i_view64 /capture=4 /convert=\"%s\"")
;; screenshot script in osx, -i stands for interactive,
;; press space key to toggle between selection and
;; window/application mode.
(const :tag "screencapture" "screencapture -i %s")
;; take an image that is already on the clipboard, for Linux
(const :tag "xclip"
"xclip -selection clipboard -t image/png -o > %s")
;; take an image that is already on the clipboard, for Windows
(const :tag "imagemagick/convert" "convert clipboard: %s")
(function :tag "Custom function")))
(defcustom org-download-screenshot-file (expand-file-name "screenshot.png" temporary-file-directory)
"The file to capture screenshots."
:type 'string)
(defcustom org-download-image-html-width 0
"When non-zero add #+attr_html: :width tag to the image."
:type 'integer)
(defcustom org-download-image-latex-width 0
"When non-zero add #+attr_latex: :width tag to the image."
:type 'integer)
(defcustom org-download-image-org-width 0
"When non-zero add #+attr_org: :width tag to the image."
:type 'integer)
(defcustom org-download-image-attr-list nil
"Add attr info to the image.
For example:
(\"#+attr_html: :width 80% :align center\"
\"#+attr_org: :width 100px\")"
:type '(repeat string))
(defcustom org-download-delete-image-after-download nil
"When non-nil delete local image after download."
:type 'boolean)
(defcustom org-download-display-inline-images t
"When non-nil display inline images in org buffer after download."
:type
'(choice
(const :tag "On" t)
(const :tag "Off" nil)
(const :tag "Posframe" posframe)))
(defvar org-download-posframe-show-params
'(;; Please do not remove :timeout or set it to large.
:timeout 1
:internal-border-width 1
:internal-border-color "red"
:min-width 40
:min-height 10
:poshandler posframe-poshandler-window-center)
"List of parameters passed to `posframe-show'.")
(declare-function posframe-workable-p "ext:posframe")
(declare-function posframe-show "ext:posframe")
(defun org-download--display-inline-images ()
(cond
((eq org-download-display-inline-images t)
(org-display-inline-images))
((eq org-download-display-inline-images 'posframe)
(require 'posframe)
(when (posframe-workable-p)
(let ((buffer (get-buffer-create " *org-download-image")))
(with-current-buffer buffer
(erase-buffer)
(insert-image-file org-download-path-last-file))
(apply #'posframe-show
buffer
org-download-posframe-show-params))))))
(defun org-download-get-heading (lvl)
"Return the heading of the current entry's LVL level parent."
(save-excursion
(let ((cur-lvl (org-current-level)))
(if cur-lvl
(progn
(unless (= cur-lvl 1)
(org-up-heading-all (- (1- (org-current-level)) lvl)))
(replace-regexp-in-string
" " "_"
(nth 4 (org-heading-components))))
""))))
(defun org-download--dir-1 ()
"Return the first part of the directory path for `org-download--dir'.
It's `org-download-image-dir', unless it's nil. Then it's \".\"."
(or org-download-image-dir "."))
(defun org-download--dir-2 ()
"Return the second part of the directory path for `org-download--dir'.
Unless `org-download-heading-lvl' is nil, it's the name of the current
`org-download-heading-lvl'-leveled heading. Otherwise it's \"\"."
(when org-download-heading-lvl
(org-download-get-heading
org-download-heading-lvl)))
(defun org-download--dir ()
"Return the directory path for image storage.
The path is composed from `org-download--dir-1' and `org-download--dir-2'.
The directory is created if it didn't exist before."
(if (eq major-mode 'org-mode)
(let* ((part1 (org-download--dir-1))
(part2 (org-download--dir-2))
(dir (if part2
(format "%s/%s" part1 part2)
part1)))
(unless (file-exists-p dir)
(make-directory dir t))
dir)
default-directory))
(defvar org-download-file-format-function #'org-download-file-format-default)
(defun org-download--fullname (link &optional ext)
"Return the file name where LINK will be saved to.
It's affected by `org-download--dir'.
EXT can hold the file extension, in case LINK doesn't provide it."
(let ((filename
(file-name-nondirectory
(car (url-path-and-query
(url-generic-parse-url link)))))
(dir (org-download--dir)))
(when (string-match ".*?\\.\\(?:png\\|jpg\\)\\(.*\\)$" filename)
(setq filename (replace-match "" nil nil filename 1)))
(when ext
(setq filename (concat filename "." ext)))
(abbreviate-file-name
(expand-file-name
(funcall org-download-file-format-function filename)
dir))))
(defun org-download-file-format-default (filename)
"It's affected by `org-download-timestamp'."
(concat
(format-time-string org-download-timestamp)
filename))
(defun org-download--image (link filename)
"Save LINK to FILENAME asynchronously and show inline images in current buffer."
(when (string= "file" (url-type (url-generic-parse-url link)))
(setq link (url-unhex-string (url-filename (url-generic-parse-url link)))))
(cond ((and (not (file-remote-p link))
(file-exists-p link))
(copy-file link (expand-file-name filename)))
((eq org-download-backend t)
(org-download--image/url-retrieve link filename))
(t
(org-download--image/command org-download-backend link filename))))
(defun org-download--image/command (command link filename)
"Using COMMAND, save LINK to FILENAME.
COMMAND is a format-style string with two slots for LINK and FILENAME."
(async-start
`(lambda () (shell-command
,(format command link
(expand-file-name filename))))
(lexical-let ((cur-buf (current-buffer)))
(lambda (_x)
(with-current-buffer cur-buf
(org-download--display-inline-images))))))
(defun org-download--write-image (status filename)
;; Write current buffer to FILENAME
(let ((err (plist-get status :error)))
(when err
(error
"HTTP error %s"
(downcase (nth 2 (assq (nth 2 err) url-http-codes))))))
(delete-region
(point-min)
(progn
(re-search-forward "\n\n" nil 'move)
(point)))
(let ((coding-system-for-write 'no-conversion))
(write-region nil nil filename nil nil nil 'confirm)))
(defun org-download--image/url-retrieve (link filename)
"Save LINK to FILENAME using `url-retrieve'."
(let ((mode major-mode))
(url-retrieve
link
(lambda (status filename buffer)
(org-download--write-image status filename)
(cond ((eq mode 'org-mode)
(with-current-buffer buffer
(org-download--display-inline-images)))
((eq mode 'dired-mode)
(let ((inhibit-message t))
(with-current-buffer (dired (file-name-directory filename))
(revert-buffer nil t))))))
(list
(expand-file-name filename)
(current-buffer))
nil t)))
(defun org-download-yank ()
"Call `org-download-image' with current kill."
(interactive)
(org-download-image
(replace-regexp-in-string "\n+$" "" (current-kill 0))))
(defun org-download-screenshot ()
"Capture screenshot and insert the resulting file.
The screenshot tool is determined by `org-download-screenshot-method'."
(interactive)
(let ((default-directory "~"))
(make-directory (file-name-directory org-download-screenshot-file) t)
(if (functionp org-download-screenshot-method)
(funcall org-download-screenshot-method
org-download-screenshot-file)
(shell-command-to-string
(format org-download-screenshot-method
org-download-screenshot-file))))
(org-download-image org-download-screenshot-file))
(declare-function org-attach-dir "org-attach")
(declare-function org-attach-attach "org-attach")
(declare-function org-attach-sync "org-attach")
(defun org-download-annotate-default (link)
"Annotate LINK with the time of download."
(format "#+DOWNLOADED: %s @ %s\n"
(if (equal link org-download-screenshot-file)
"screenshot"
link)
(format-time-string "%Y-%m-%d %H:%M:%S")))
(defvar org-download-annotate-function
#'org-download-annotate-default
"Function that takes LINK and returns a string.
It's inserted before the image link and is used to annotate it.")
(defvar org-download-link-format
"[[file:%s]]\n"
"Format of the file link to insert.")
(defun org-download-image (link)
"Save image at address LINK to `org-download--dir'."
(interactive "sUrl: ")
(let (ext)
(unless (image-type-from-file-name link)
(with-current-buffer (url-retrieve-synchronously link t)
(cond ((let ((regexes org-download-img-regex-list)
lnk)
(while (and (not lnk) regexes)
(goto-char (point-min))
(when (re-search-forward (pop regexes) nil t)
(backward-char)
(setq lnk (read (current-buffer)))))
(when lnk
(setq link lnk))))
((progn
(goto-char (point-min))
(when (re-search-forward "^Content-Type: image/\\(.*\\)$")
(setq ext (match-string 1)))))
(t
(error "link %s does not point to an image; unaliasing failed" link)))))
(let ((filename
(cond ((eq org-download-method 'attach)
(let ((org-download-image-dir (progn (require 'org-attach)
(org-attach-dir t)))
org-download-heading-lvl)
(org-download--fullname link ext)))
((fboundp org-download-method)
(funcall org-download-method link))
(t
(org-download--fullname link ext)))))
(setq org-download-path-last-file filename)
(when (image-type-from-file-name filename)
(org-download--image link filename)
(when (eq major-mode 'org-mode)
(when (eq org-download-method 'attach)
(org-attach-attach filename nil 'none))
(org-download-insert-link link filename))
(when (and (eq org-download-delete-image-after-download t)
(not (url-handler-file-remote-p (current-kill 0))))
(delete-file link delete-by-moving-to-trash))))))
(defun org-download-rename-at-point ()
"Rename image at point."
(interactive)
(let* ((dir-path (org-download--dir))
(current-name (file-name-nondirectory
(org-element-property :path (org-element-context))))
(current-path (concat dir-path "/" current-name))
(ext (file-name-extension current-name))
(new-name (read-string "Rename file at point to: " (file-name-sans-extension current-name)))
(new-path (concat dir-path "/" new-name "." ext)))
(rename-file current-path new-path)
(message "File successfully renamed...")
(org-download-replace-all current-name (concat new-name "." ext))))
(defun org-download-rename-last-file ()
"Rename the last downloaded file saved in your computer."
(interactive)
(let* ((dir-path (org-download--dir))
(newname (read-string "Rename last file to: " (file-name-base org-download-path-last-file)))
(ext (file-name-extension org-download-path-last-file))
(newpath (concat dir-path "/" newname "." ext)))
(when org-download-path-last-file
(rename-file org-download-path-last-file newpath 1)
(org-download-replace-all
(file-name-nondirectory org-download-path-last-file)
(concat newname "." ext))
(setq org-download-path-last-file newpath)
(org-download--display-inline-images))))
(defun org-download-replace-all (oldpath newpath)
"Function to search for the OLDPATH inside the buffer and replace it by the NEWPATH."
(save-excursion
(goto-char (point-min))
(while (re-search-forward oldpath nil t)
(replace-match newpath))))
(defcustom org-download-abbreviate-filename-function #'file-relative-name
"Function that takes FILENAME and returns an abbreviated file name."
:type '(choice
(const :tag "relative" file-relative-name)
(const :tag "absolute" expand-file-name)))
(defun org-download-insert-link (link filename)
(let* ((beg (point))
(line-beg (line-beginning-position))
(indent (- beg line-beg))
(in-item-p (org-in-item-p))
str)
(if (looking-back "^[ \t]+" line-beg)
(delete-region (match-beginning 0) (match-end 0))
(newline))
(insert (funcall org-download-annotate-function link))
(dolist (attr org-download-image-attr-list)
(insert attr "\n"))
(insert (if (= org-download-image-html-width 0)
""
(format "#+attr_html: :width %dpx\n" org-download-image-html-width)))
(insert (if (= org-download-image-latex-width 0)
""
(format "#+attr_latex: :width %dcm\n" org-download-image-latex-width)))
(insert (if (= org-download-image-org-width 0)
""
(format "#+attr_org: :width %dpx\n" org-download-image-org-width)))
(insert
(format org-download-link-format
(org-link-escape
(funcall org-download-abbreviate-filename-function filename))))
(org-download--display-inline-images)
(setq str (buffer-substring-no-properties line-beg (point)))
(when in-item-p
(indent-region line-beg (point) indent))
str))
(defun org-download--at-comment-p ()
"Check if current line begins with #+DOWLOADED:."
(save-excursion
(move-beginning-of-line nil)
(looking-at "#\\+DOWNLOADED:")))
(defun org-download-delete ()
"Delete inline image link on current line, and the file that it points to."
(interactive)
(cond ((org-download--at-comment-p)
(delete-region (line-beginning-position)
(line-end-position))
(org-download--delete (line-beginning-position)
nil
1))
((region-active-p)
(org-download--delete (region-beginning)
(region-end))
(delete-region (region-beginning)
(region-end)))
((looking-at org-any-link-re)
(let ((fname (org-link-unescape
(match-string-no-properties 2))))
(when (file-exists-p fname)
(delete-file fname)
(delete-region (match-beginning 0)
(match-end 0))
(when (eolp)
(delete-char 1)))))
(t (org-download--delete (line-beginning-position)
(line-end-position))))
(when (eq org-download-method 'attach)
(org-attach-sync)))
(defcustom org-download-edit-cmd "gimp %s"
"Command for editing an image link."
:type 'string)
(defun org-download-edit ()
"Open the image at point for editing."
(interactive)
(let ((context (org-element-context)))
(if (not (eq (car-safe context) 'link))
(user-error "not on a link")
(start-process-shell-command
"org-download-edit"
"org-download-edit"
(format org-download-edit-cmd
(shell-quote-wildcard-pattern
(url-unhex-string (plist-get (cadr context) :path))))))))
(defun org-download--delete (beg end &optional times)
"Delete inline image links and the files they point to between BEG and END.
When TIMES isn't nil, delete only TIMES links."
(unless times
(setq times most-positive-fixnum))
(save-excursion
(goto-char beg)
(while (and (>= (decf times) 0)
(re-search-forward "\\[\\[file:\\([^]]*\\)\\]\\]" end t))
(let ((str (match-string-no-properties 1)))
(delete-region beg
(match-end 0))
(when (file-exists-p str)
(delete-file str))))))
(defun org-download-dnd-fallback (uri action)
(let ((dnd-protocol-alist
(rassq-delete-all
'org-download-dnd
(copy-alist dnd-protocol-alist))))
(dnd-handle-one-url nil action uri)))
(defun org-download-dnd (uri action)
"When in `org-mode' and URI points to image, download it.
Otherwise, pass URI and ACTION back to dnd dispatch."
(cond ((eq major-mode 'org-mode)
(condition-case nil
(org-download-image uri)
(error
(org-download-dnd-fallback uri action))))
((eq major-mode 'dired-mode)
(org-download-dired uri))
;; redirect to someone else
(t
(org-download-dnd-fallback uri action))))
(defun org-download-dired (uri)
"Download URI to current directory."
(raise-frame)
(org-download-image uri))
(defun org-download-dnd-base64 (uri _action)
(when (eq major-mode 'org-mode)
(when (string-match "^data:image/png;base64," uri)
(let* ((me (match-end 0))
(filename (org-download--fullname
(substring-no-properties uri me (+ me 10))
"png")))
(with-temp-buffer
(insert (base64-decode-string (substring uri me)))
(write-file filename))
(org-download-insert-link filename filename)))))
;;;###autoload
(defun org-download-enable ()
"Enable org-download."
(unless (eq (cdr (assoc "^\\(https?\\|ftp\\|file\\|nfs\\):" dnd-protocol-alist))
'org-download-dnd)
(setq dnd-protocol-alist
`(("^\\(https?\\|ftp\\|file\\|nfs\\):" . org-download-dnd)
("^data:" . org-download-dnd-base64)
,@dnd-protocol-alist))))
(defun org-download-disable ()
"Disable org-download."
(rassq-delete-all 'org-download-dnd dnd-protocol-alist))
(org-download-enable)
(provide 'org-download)
;;; org-download.el ends here