Add new commands 'image-crop' and 'image-elide'

* doc/lispref/display.texi (Showing Images): Document it.

* lisp/image.el (image-map): Bind commands.

* lisp/image/image-crop.el: New file (bug#51331).
This commit is contained in:
Lars Ingebrigtsen 2022-09-13 15:52:56 +02:00
parent 36993bb9c2
commit 07c0e090bd
4 changed files with 381 additions and 0 deletions

View file

@ -6863,6 +6863,12 @@ A prefix means to rotate by 90 degrees counter-clockwise instead.
@item o
Save the image to a file (@code{image-save}).
@item c
Crop the image interactively (@code{image-crop}).
@item e
Elide a rectangle from the image interactively (@code{image-elide}).
@end table
@node Multi-Frame Images

View file

@ -2429,6 +2429,13 @@ The old name is still available as an obsolete function alias.
* New Modes and Packages in Emacs 29.1
+++
** New commands 'image-crop' and 'image-elide'.
These commands allow interactively cropping/eliding the image under
point. These commands are bound to 'c' and 'e' (respectively) in the
local keymap over images. They rely on external programs to do the
actual cropping/eliding of the image file.
+++
** New package 'oclosure'.
Allows the creation of "functions with slots" or "function objects"

View file

@ -178,6 +178,8 @@ or \"ffmpeg\") is installed."
"+" #'image-increase-size
"r" #'image-rotate
"o" #'image-save
"c" #'image-crop
"e" #'image-elide
"h" #'image-flip-horizontally
"v" #'image-flip-vertically
"C-<wheel-down>" #'image-mouse-decrease-size

366
lisp/image/image-crop.el Normal file
View file

@ -0,0 +1,366 @@
;;; image-crop.el --- Image Cropping -*- lexical-binding: t -*-
;; Copyright (C) 2022 Free Software Foundation, Inc.
;; Keywords: multimedia
;; This file is 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 <https://www.gnu.org/licenses/>.
;;; Commentary:
;; This package provides an interface for cropping images
;; interactively, but relies on external programs to do the actual
;; modifications to files.
;;; Code:
(require 'svg)
(defvar image-crop-exif-rotate nil
"If non-nil, rotate images by updating exif data.
If nil, rotate the images \"physically\".")
(defvar image-crop-resize-command '("convert" "-resize" "%wx" "-" "%f:-")
"Command to resize an image.
The following `format-spec' elements are allowed:
%w: Width.
%f: Result file type.")
(defvar image-crop-elide-command '("convert" "-draw" "rectangle %l,%t %r,%b"
"-" "%f:-")
"Command to make a rectangle inside an image.
The following `format-spec' elements are allowed:
%l: Left.
%t: Top.
%r: Right.
%b: Bottom.
%f: Result file type.")
(defvar image-crop-crop-command '("convert" "+repage" "-crop" "%wx%h+%l+%t"
"-" "%f:-")
"Command to crop an image.
The following `format-spec' elements are allowed:
%l: Left.
%t: Top.
%w: Width.
%h: Height.
%f: Result file type.")
(defvar image-crop-rotate-command '("convert" "-rotate" "%r" "-" "%f:-")
"Command to rotate an image.
The following `format-spec' elements are allowed:
%r: Rotation (in degrees).
%f: Result file type.")
;;;###autoload
(defun image-elide (&optional square)
"Elide a square from the image under point.
If SQUARE (interactively, the prefix), elide a square instead of a
rectangle from the image."
(interactive "P")
(image-crop square t))
;;;###autoload
(defun image-crop (&optional square elide)
"Crop the image under point.
If SQUARE (interactively, the prefix), crop a square instead of a
rectangle from the image.
If ELIDE, remove a rectangle from the image instead of cropping
the image.
After cropping an image, it can be saved by `M-x image-save' or
\\<image-map>\\[image-save] when point is over the image."
(interactive "P")
(unless (image-type-available-p 'svg)
(error "SVG support is needed to crop images"))
(unless (executable-find (car image-crop-crop-command))
(error "Couldn't find %s command to crop the image"
(car image-crop-crop-command)))
(let ((image (get-text-property (point) 'display)))
(unless (imagep image)
(user-error "No image under point"))
;; We replace the image under point with an SVG image that looks
;; just like that image. That allows us to draw lines over it.
;; At the end, we replace that SVG with a cropped version of the
;; original image.
(let* ((data (cl-getf (cdr image) :data))
(undo-handle (prepare-change-group))
(type (cond
((cl-getf (cdr image) :format)
(format "%s" (cl-getf (cdr image) :format)))
(data
(image-crop--content-type data))))
(image-scaling-factor 1)
(size (image-size image t))
(svg (svg-create (car size) (cdr size)
:xmlns:xlink "http://www.w3.org/1999/xlink"
:stroke-width 5))
(text (buffer-substring (pos-bol) (pos-eol)))
(inhibit-read-only t)
orig-data)
(with-temp-buffer
(set-buffer-multibyte nil)
(if (null data)
(insert-file-contents-literally (cl-getf (cdr image) :file))
(insert data))
(let ((image-crop-exif-rotate nil))
(image-crop--possibly-rotate-buffer image))
(setq orig-data (buffer-string))
(setq type (image-crop--content-type orig-data))
(image-crop--process image-crop-resize-command
`((?w . 600)
(?f . ,(cadr (split-string type "/")))))
(setq data (buffer-string)))
(svg-embed svg data type t
:width (car size)
:height (cdr size))
(delete-region (pos-bol) (pos-eol))
(svg-insert-image svg)
(let ((area (condition-case _
(save-excursion
(forward-line 1)
(image-crop--crop-image-1
svg square (car size) (cdr size)))
(quit nil))))
(delete-region (pos-bol) (pos-eol))
(if area
(image-crop--crop-image-update area orig-data size type elide)
;; If the user didn't complete the crop, re-insert the
;; original image (and text).
(insert text))
(undo-amalgamate-change-group undo-handle)))))
(defun image-crop--crop-image-update (area data size type elide)
(let* ((image-scaling-factor 1)
(osize (image-size (create-image data nil t) t))
(factor (/ (float (car osize)) (car size)))
;; width x height + left + top
(width (abs (truncate (* factor (- (cl-getf area :right)
(cl-getf area :left))))))
(height (abs (truncate (* factor (- (cl-getf area :bottom)
(cl-getf area :top))))))
(left (truncate (* factor (min (cl-getf area :left)
(cl-getf area :right)))))
(top (truncate (* factor (min (cl-getf area :top)
(cl-getf area :bottom))))))
(image-crop--insert-image-data
(with-temp-buffer
(set-buffer-multibyte nil)
(insert data)
(if elide
(image-crop--process image-crop-elide-command
`((?l . ,left)
(?t . ,top)
(?r . ,(+ left width))
(?b . ,(+ top height))
(?f . ,(cadr (split-string type "/")))))
(image-crop--process image-crop-crop-command
`((?l . ,left)
(?t . ,top)
(?w . ,width)
(?h . ,height)
(?f . ,(cadr (split-string type "/"))))))
(buffer-string)))))
(defun image-crop--crop-image-1 (svg &optional square image-width image-height)
(track-mouse
(cl-loop
with prompt = (if square "Move square" "Set start point")
and state = (if square 'move-unclick 'begin)
and area = (if square
(list :left (- (/ image-width 2)
(/ image-height 2))
:top 0
:right (+ (/ image-width 2)
(/ image-height 2))
:bottom image-height)
(list :left 0
:top 0
:right 0
:bottom 0))
and corner = nil
for event = (read-event prompt)
do (if (or (not (consp event))
(not (consp (cadr event)))
(not (nth 7 (cadr event)))
;; Only do things if point is over the SVG being
;; tracked.
(not (eq (cl-getf (cdr (nth 7 (cadr event))) :type)
'svg)))
()
(let ((pos (nth 8 (cadr event))))
(cl-case state
('begin
(cond
((eq (car event) 'down-mouse-1)
(setq state 'stretch
prompt "Stretch to end point")
(setf (cl-getf area :left) (car pos)
(cl-getf area :top) (cdr pos)
(cl-getf area :right) (car pos)
(cl-getf area :bottom) (cdr pos)))))
('stretch
(cond
((eq (car event) 'mouse-movement)
(setf (cl-getf area :right) (car pos)
(cl-getf area :bottom) (cdr pos)))
((memq (car event) '(mouse-1 drag-mouse-1))
(setq state 'corner
prompt "Choose corner to adjust (RET to crop)"))))
('corner
(cond
((eq (car event) 'down-mouse-1)
;; Find out what corner we're close to.
(setq corner (image-crop--find-corner
area pos
'((:left :top)
(:left :bottom)
(:right :top)
(:right :bottom))))
(when corner
(setq state 'adjust
prompt "Adjust crop")))))
('adjust
(cond
((memq (car event) '(mouse drag-mouse-1))
(setq state 'corner
prompt "Choose corner to adjust"))
((eq (car event) 'mouse-movement)
(setf (cl-getf area (car corner)) (car pos)
(cl-getf area (cadr corner)) (cdr pos)))))
('move-unclick
(cond
((eq (car event) 'down-mouse-1)
(setq state 'move-click
prompt "Move"))))
('move-click
(cond
((eq (car event) 'mouse-movement)
(setf (cl-getf area :left) (car pos)
(cl-getf area :right) (+ (car pos) image-height)))
((memq (car event) '(mouse-1 drag-mouse-1))
(setq state 'move-unclick
prompt "Click to move")))))))
do (svg-line svg (cl-getf area :left) (cl-getf area :top)
(cl-getf area :right) (cl-getf area :top)
:id "top-line" :stroke-color "white")
(svg-line svg (cl-getf area :left) (cl-getf area :bottom)
(cl-getf area :right) (cl-getf area :bottom)
:id "bottom-line" :stroke-color "white")
(svg-line svg (cl-getf area :left) (cl-getf area :top)
(cl-getf area :left) (cl-getf area :bottom)
:id "left-line" :stroke-color "white")
(svg-line svg (cl-getf area :right) (cl-getf area :top)
(cl-getf area :right) (cl-getf area :bottom)
:id "right-line" :stroke-color "white")
while (not (member event '(return ?q)))
finally (return (and (eq event 'return)
area)))))
(defun image-crop--find-corner (area pos corners)
(cl-loop for corner in corners
;; We accept 10 pixels off.
when (and (< (- (car pos) 10)
(cl-getf area (car corner))
(+ (car pos) 10))
(< (- (cdr pos) 10)
(cl-getf area (cadr corner))
(+ (cdr pos) 10)))
return corner))
(defun image-crop--content-type (image)
;; Get the MIME type by running "file" over it.
(with-temp-buffer
(set-buffer-multibyte nil)
(insert image)
(call-process-region (point-min) (point-max)
"file" t (current-buffer) nil
"--mime-type" "-")
(cadr (split-string (buffer-string)))))
(defun image-crop--possibly-rotate-buffer (image)
(when (imagep image)
(let ((content-type (image-crop--content-type (buffer-string))))
(when (image-property image :rotation)
(cond
;; We can rotate jpegs losslessly by setting the correct
;; orientation.
((and image-crop-exif-rotate
(equal content-type "image/jpeg")
(executable-find "exiftool"))
(call-process-region
(point-min) (point-max) "exiftool" t (list (current-buffer) nil) nil
(format "-Orientation#=%d"
(cl-case (truncate (image-property image :rotation))
(0 0)
(90 6)
(180 3)
(270 8)
(otherwise 0)))
"-o" "-" "-"))
;; Most other image formats have to be reencoded to do
;; rotation.
(t
(image-crop--process
image-crop-rotate-command
`((?r . ,(image-property image :rotation))
(?f . ,(cadr (split-string content-type "/")))))
(when (and (equal content-type "image/jpeg")
(executable-find "exiftool"))
(call-process-region
(point-min) (point-max) "exiftool"
t (list (current-buffer) nil) nil
"-Orientation#=0"
"-o" "-" "-")))))
(when (image-property image :width)
(image-crop--process
image-crop-resize-command
`((?w . ,(image-property image :width))
(?f . ,(cadr (split-string content-type "/")))))))))
(defun image-crop--insert-image-data (image)
(insert-image
(create-image image nil t
:max-width (- (frame-pixel-width) 50)
:max-height (- (frame-pixel-height) 150))
(format "<img src=\"data:%s;base64,%s\">"
(image-crop--content-type image)
;; Get a base64 version of the image.
(with-temp-buffer
(set-buffer-multibyte nil)
(insert image)
(base64-encode-region (point-min) (point-max) t)
(buffer-string)))
nil nil t))
(defun image-crop--process (command expansions)
"Use `call-process-region' with COMMAND expanded with EXPANSIONS."
(apply
#'call-process-region (point-min) (point-max)
(format-spec (car command) expansions)
t (list (current-buffer) nil) nil
(mapcar (lambda (elem)
(format-spec elem expansions))
(cdr command))))
(provide 'image-crop)
;;; image-crop.el ends here