最近的一个项目中,我打算使用 OrgMode 作为文档的书写工具,主要原因是比较简单,而且可以使用文学编程,简化 API 文档的写作流程。但首先遇到了个难题:如何方便的插入图片。于是我花了些时间,边学边用 elisp 做了一个函数,解决了我项目中的问题:从剪贴板保存图片到项目目录,并以相对目录的形式插入到 org 文件中。

从剪贴板到本地图片

Linux 和 MacOS 的用户直接可以用 org-download 之类的插件,或者如这个网址所介绍的方法,我使用的 windows 在方法上是有点不同,在原链里也有提到:使用 powershell 脚本

powershell -command "Add-Type -AssemblyName System.Windows.Forms;if ($([System.Windows.Forms.Clipboard]::ContainsImage())) {$image = [System.Windows.Forms.Clipboard]::GetImage();[System.Drawing.Bitmap]$image.Save('filepath.jpg',[System.Drawing.Imaging.ImageFormat]::Jpeg); Write-Output 'clipboard content saved as file'} else {Write-Output 'clipboard does not contain image data'}"

基本原理就是用 .net framework 直接读 clipboard, 再保存到指定位置。可见 powershell 在 .net 加持下的强大。我们使用这段代码可以写出 elisp 函数的基本结构

(defun org-docs-insert-image-from-clipboard ()
  "Take a screenshot into a time stamped unique-named file in the
   same directory as the org-buffer and insert a link to this file."  
   (interactive)
   (setq (jpg-file-name (format-time-string "%Y%m%d_%H%M%S.jpg")))
   (shell-command (concat "powershell -command \"Add-Type -AssemblyName System.Windows.Forms;if ($([System.Windows.Forms.Clipboard]::ContainsImage())) {$image = [System.Windows.Forms.Clipboard]::GetImage();[System.Drawing.Bitmap]$image.Save('" jpg-file-name "',[System.Drawing.Imaging.ImageFormat]::Png); Write-Output 'clipboard content saved as file'} else {Write-Output 'clipboard does not contain image data'}\""))
   (insert (concat "[[file:" jpg-file-name "]]"))

将文件写入项目目录

由于我的文档是在项目目录中的,所以我利用 projectile 定位将图片作为附件的目录,以让代码有更好的普适性

(defun org-docs-insert-image-from-clipboard ()
    "Take a screenshot into a time stamped unique-named file in the
   same directory as the org-buffer and insert a link to this file."
    (interactive)
    (let* ((the-dir (concat (projectile-project-root) "docs/"))
           (attachments-dir (concat the-dir "assets/attachments/"))
           (jpg-file-name (format-time-string "%Y%m%d_%H%M%S.jpg"))
           (jpg-path (concat attachments-dir jpg-file-name))
    (shell-command (concat "powershell -command \"Add-Type -AssemblyName System.Windows.Forms;if ($([System.Windows.Forms.Clipboard]::ContainsImage())) {$image = [System.Windows.Forms.Clipboard]::GetImage();[System.Drawing.Bitmap]$image.Save('" jpg-path "',[System.Drawing.Imaging.ImageFormat]::Jpeg); Write-Output 'clipboard content saved as file'} else {Write-Output 'clipboard does not contain image data'}\""))
    (insert (concat "[[file:" jpg-path "]]"))
    ))

我的文档在项目目录的 ./docs/ 下,并在 ./docs/assets/attachments/ 下存放图片文件,方便统一的编译发布。

我的项目目录

插入相对路径

由于 docs 文件夹下可能会有多个目录,以方便对文档的组织,所以不同路径下的 org 文件,他的引用图片的相对路径也是不同的。为了得到相对路径,我就需要让当前文件和图片目录比较,以得到正确的相对路径。简单搜索了一下,发现 f.el 的辅助函数:f-relative(比较两个文件的相对位置) 和 f-dirname(获得文件目录),可以大大简化人代码

(f-relative jpg-path  (f-dirname buffer-file-name))

比如在文件 ./docs/rest/basic.org 中,插入图片,地址就为 ../assets/attachments/xxx.jpg

完整版本

;; orgmode import image from clipboard

(defun org-docs-insert-image-from-clipboard ()
    "Take a screenshot into a time stamped unique-named file in the
    same directory as the org-buffer and insert a link to this file."
    (interactive)
    (let* ((the-dir (concat (projectile-project-root) "docs/"))
           (attachments-dir (concat the-dir "assets/attachments/"))
           (jpg-file-name (format-time-string "%Y%m%d_%H%M%S.jpg"))
           (jpg-path (concat attachments-dir jpg-file-name))
           (jpg-relative-path (f-relative jpg-path  (f-dirname buffer-file-name))))
          (shell-command (concat "powershell -command \"Add-Type -AssemblyName System.Windows.Forms;if ($([System.Windows.Forms.Clipboard]::ContainsImage())) {$image = [System.Windows.Forms.Clipboard]::GetImage();[System.Drawing.Bitmap]$image.Save('" jpg-path "',[System.Drawing.Imaging.ImageFormat]::Jpeg); Write-Output 'clipboard content saved as file'} else {Write-Output 'clipboard does not contain image data'}\""))
          (insert (concat "[[file:" jpg-relative-path "]]"))
          ))
;; set keybinding

(global-set-key (kbd "M-o M-v") 'org-docs-insert-image-from-clipboard)

实现的效果如图

实现截图效果