;;; 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