|
|
- ;;; 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 <http://www.gnu.org/licenses/>.
-
- ;;; 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
- '("<img +src=\"" "<img +\\(class=\"[^\"]+\"\\)? *src=\"")
- "This regex is used to unalias links that look like images.
- The html to which the links points will be searched for these
- regexes, one by one, until one succeeds. The found image address
- will be used."
- :type '(repeat string))
-
- (defcustom org-download-screenshot-method "gnome-screenshot -a -f %s"
- "The tool to capture screenshots."
- :type '(choice
- (const :tag "gnome-screenshot" "gnome-screenshot -a -f %s")
- (const :tag "scrot" "scrot -s %s")
- (const :tag "gm" "gm import %s")
- (const :tag "imagemagick/import" "import %s")
- (const :tag "imagemagick/import + xclip to save to clipboard"
- "export filename=\"%s\"; import png:\"$filename\" ;xclip -selection clipboard -target image/png -filter < \"$filename\" &>/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
|