A Script To Insert An Image

111 views
Skip to first unread message

Thomas Passin

unread,
Mar 9, 2024, 2:04:19 PMMar 9
to leo-editor
We can't directly insert an image into a standard Leo node because they are text-only.  I find this very annoying sometimes, especially when I am writing a note and want to include an image.  

But we can do the next best thing - insert an ReStructuredText (RsT) instruction to display an image so that we can view it with the viewrendered3 plugin (VR3). The instruction is short and easy, but it's still annoying to type and I usually forget the exact details. I have a button that toggles VR3 on and off so that it's easy to view an embedded image once the RsT instruction is there. An embedding command would make embedding with Leo as easy as embedding an image in a word processor.   Aha, this is Leo, let's write a script!

Here is a script that pops up a file dialog and inserts a relative path to the chosen file.  There are several small variations which I discuss after the code.

"""Insert RsT code at cursor to display an image.

The path to the image file will come from a file dialog.
This action is undoable.
"""
PATH = g.app.gui.runOpenFileDialog(c,
    title="Import File",
    filetypes=[("All files", "*"),],
    defaultextension=".*",
    multiple=False)

if PATH:
    from os.path import relpath
    PATH = relpath(PATH)
    PATH = PATH.replace('\\', '/').replace('"', '').replace("'", '')
    IMAGE_TEMPLATE = f'''

.. figure:: {PATH}
    :scale: 50%

'''
    w = c.frame.body.wrapper
    p = c.p
    s = p.b
    u = c.undoer

    start, _ = w.getSelectionRange()

    undoType = 'insert-rst-image-code'
    undoData = u.beforeChangeNodeContents(p)

    head, tail = s[:start], s[start:]
    p.b = head + IMAGE_TEMPLATE + tail

    c.setChanged()
    p.setDirty()
    u.afterChangeNodeContents(p, undoType, undoData)
    c.redraw()


Variations:
1.  If you want an absolute path instead of a relative path, delete the lines
    from os.path import relpath
    PATH = relpath(PATH)
with

2. If you  want to get the path from the clipboard instead of a file dialog, replace the lines

PATH = g.app.gui.runOpenFileDialog(c,
    title="Import File",
    filetypes=[("All files", "*"),],
    defaultextension=".*",
    multiple=False)


with the line 

    PATH = g.app.gui.getTextFromClipboard()

3. If you want the embedded image to be full width instead of 50%, delete the line

    :scale: 50%

4. You can make this work with Markdown or Asciidoc by using their embedding instruction in the TEMPLATE instead of the RsT one.

I have added the command to my own local menu.  VR3 can open in a tab in the log pane; the command for toggling in a tab is vr3-toggle-tab.  I usually like opening it in the log pane instead of in its own separate pane.

If you would like to create a local menu of your own and don't know how, it's easy.  Just ask and I'll show what to add to myLeoSettings,leo.

jkn

unread,
Mar 9, 2024, 2:14:41 PMMar 9
to leo-editor
This looks interesting and useful, thanks Thomas. I confess I rarely/never use Leo with images, I really should experiment a little.

Recently I have been using Obsidian as a note-taking app (Joplin is similar). Neither are as capable as Leo, in many ways, but they have their niceties.
One that is handy when note-taking is the ability to paste *from the clipboard*. You can setup an area (directory0 in an Obsidian 'vault' - then 'paste from clipboard' will
(a) create a unique filename within the image directory, and put the clipboard contents in there as eg. a .png file
(b) add a (markdown) reference to the new image in the 'note' that you are in.

It'd be nice to have something similar in Leo... ;-)

Regards, Jon N

Thomas Passin

unread,
Mar 9, 2024, 2:35:15 PMMar 9
to leo-editor
Shouldn't be hard.  What would be on the clipboard?  Image bytes?  Or an image filename?  I often select an image in a file manager window, copy it to an "images" subdirectory of the current outline, then write the embedding code into and "images" child node.  That would be easy to write a script for.

jkn

unread,
Mar 9, 2024, 3:09:24 PMMar 9
to leo-editor
The way I have been using the Obsidian feature is to paste image bytes (eg. from a screen region capture). So Obsidian saves a file:

/path/to/attachments/

Pasted image 20240228230106.png


Where /path/to/attachments is an Obsidian setting, and the name of the file is clearly a timestamp.


Obsidian only uses markdown, so it inserts


![[Pasted image 20240228230106.png]]


into the 'node being edited'


I think you can also drag and drop files into a 'node'.


There would clearly have to be some work to make this generally useful in a Leo context...


Thomas Passin

unread,
Mar 9, 2024, 3:22:44 PMMar 9
to leo-editor
I seem to remember that somewhere in the code base, Leo code to capture the screen has already been worked out.  If that's the case we're in clover.

jkn

unread,
Mar 9, 2024, 3:27:30 PMMar 9
to leo-editor
I would expect that to be somewhat os-dependant (I mostly use Linux), but perhaps it is there. I will try to take a look for clipboard-paste functions.

Thomas Passin

unread,
Mar 9, 2024, 3:34:45 PMMar 9
to leo-editor
Yup, Terry Brown has already done most of it for us in the screen_capture plugin.  Here's the docstring:

"""
screen_capture.py
=================

Capture screen shots - single frames are useful.  The
`Recorder` class can also capture frames continuously but that's not
really useful, it doesn't handle audio or video encoding, and can't maintain
a 30 fps frame rate.  The code for doing so is not hooked up in this plugin.

Screen captures are saved in ``~/.leo/screen_captures`` with
timestamps in the filenames.

Commands
--------

``screen-capture-5sec``
  Wait five seconds, then take a screen shot.
``screen-capture-now``
  Take a screen shot.

Settings
--------

``@string screen-capture-save-path``
  Save screen shots here instead of ~/.leo/screen_captures

Terry Brown, Terry_...@yahoo.com, Fri Apr 19 16:33:45 2013
"""

The code may need to be updated for Qt6, since it was originally written for Qt4 in 2013. Or just the key capture part of the code could be used, since the entire plugin is more complicated than necessary.  Also it doesn't capture a part of the screen as best as I can see, only the whole.

jkn

unread,
Mar 9, 2024, 5:07:19 PMMar 9
to leo-editor
I am really thinking of the situation where I have already captured an image to the clipboard (using some other app...), and then want to paste into Leo. But there will undoubtably be something useful in what Terry has done

Thomas Passin

unread,
Mar 11, 2024, 12:33:23 PMMar 11
to leo-editor
It turns out that to get the relative path conversion w have to remove any quotation marks around the path before running os.relpath(). So the script becomes:

"""Insert RsT code at cursor to display an image.

The path to the image file will come from a file dialog.
This action is undoable.
"""
PATH = g.app.gui.runOpenFileDialog(c,
    title="Import File",
    filetypes=[("All files", "*"),],
    defaultextension=".*",
    multiple=False)

if PATH:
    from os.path import relpath
    PATH = PATH.replace('"', '').replace("'", '')
    PATH = relpath(PATH)
    PATH = PATH.replace('\\', '/')

    IMAGE_TEMPLATE = f'''

.. figure:: {PATH}
    :scale: 50%

'''

    w = c.frame.body.wrapper
    p = c.p
    s = p.b
    u = c.undoer

    start, _ = w.getSelectionRange()

    undoType = 'insert-rst-image-code'
    undoData = u.beforeChangeNodeContents(p)

    head, tail = s[:start], s[start:]
    p.b = head + IMAGE_TEMPLATE + tail

    c.setChanged()
    p.setDirty()
    u.afterChangeNodeContents(p, undoType, undoData)
    c.redraw()

jkn

unread,
Mar 11, 2024, 4:10:03 PMMar 11
to leo-editor
Has anyone played much with class QClipboard? I did a couple of trivial experiments but I must be misunderstanding something.

for instance, QClipboard.image() returns a non-null value even if the clipboard does not contain an image.

    J^n

jkn

unread,
Mar 11, 2024, 4:23:42 PMMar 11
to leo-editor
Doh! should have read the documentation better.

you have to test for a 'null image' using eg. img.isnull()

on with my trivial experiments...

jkn

unread,
Mar 23, 2024, 4:59:25 PMMar 23
to leo-editor
OK, here is a simple demonstration of part of the feature I am interested in.

When run with an image in the (global) clipboard, it saves this to a local timestamped file (under ~/tmp), and indicates how a markdown reference to that file could be inserted.

There's plenty more needed but this is the QClipboard part, I think.

import os
import time
   
def unique_png_fname():
    """ return a unique (timestamped) filename to use
    """
    FORMAT="%Y-%m-%d-%H-%M-%S.png"
    return time.strftime(FORMAT)

def link_string_md(fname):
    """ get markdown format to insert a filename
    """
    return "![[%s]]" % fname

def main():
    # get clipboard contents - default mode is global clipboard
    cb = g.app.gui.qtApp.clipboard()
    # is it in image format?
    img = cb.image()
    if not img.isNull():
        basefiledir = os.path.expanduser("~/tmp")
        fqfilepath = os.path.join(basefiledir, unique_png_fname())
        img.save(fqfilepath, "PNG")
        g.es("wrote clipboard to:", fqfilepath)
        g.es("could insert:", link_string_md(fqfilepath))
    else:
        g.es("clipboard not in image format")

main()

Thomas Passin

unread,
Mar 23, 2024, 6:09:59 PMMar 23
to leo-editor
Looks pretty straightforward.  Perhaps you'll want to make it undoable (so far as Leo is concerned).  As a matter of Python programming, defining a main() function here isn't needed.  If you are only going to use it as a script, then you don't need a function at all. Just unindent those lines of code (and delete the "main()" line) and they will run when the script runs.  No need to call main() at all.   If you want that clipboard part of the block to run and if it succeeds then insert the markdown text into a node, then the function shouldn't be called "main" but something more descriptive.

jkn

unread,
Mar 23, 2024, 6:30:21 PMMar 23
to leo-editor
Hi Thomas
    not sure what an undoer would do here? maybe delete the file? There is no leo-relevant action here (yet)

I'm well aware I don't *need* the main(), but I prefer to write this way. Same as with bash scripts etc. It helps for future modularity, I find. Of course, 'YAGNI', but still. f course you are right that main() could be more descriptive, this is only a POC.

The tricky bit is deciding where to store the clipboard files. Obsidian has a special action when you click on inserted 'snippet' images; it just stores the filename in the 'body', and uses its knowledge of where the snippets are located when you do the equivalent of CTRL-click on the URL

Linking the location of the 'snippets' to the leo file and/or node is an interesting challenge...

Thomas Passin

unread,
Mar 23, 2024, 6:42:32 PMMar 23
to leo-editor
I've been using an "images" subdirectory under the location of the outline, or under the @path location if you use one.  It would make backup or sharing easier than some random location in the file system.  The script could check for its existence and create it if needed.  

Here's how I get the path of the node or it's @path into the clipboard:

"""Copy the effective path of the current node to the clipboard.

This command honors @path directives.
"""
from pathlib import Path

pth = Path(c.fullPath(p))
if pth.exists():
    if pth.is_file():
        direc = pth.parent
    else:
        direc = pth
else:
    direc = Path(g.scanAllAtPathDirectives(c, p))

if direc:
    normdir = str(direc.resolve())
    g.app.gui.replaceClipboardWith(normdir)
else:
    g.es(f"Path {direc} doesn't exist")


BTW, I have this script linked to a button, which is very convenient sometimes.

jkn

unread,
Mar 23, 2024, 6:55:16 PMMar 23
to leo-editor
I think I have an earlier version of this snippet from you somewhere - I was going to take a look at it for this purpose. So thanks!

    J^n

jkn

unread,
Mar 23, 2024, 7:14:14 PMMar 23
to leo-editor
BTW [Edward], isn't this documentation, from https://leo-editor.github.io/leo-editor/customizing.html#simple-settings-nodes, incorrect - or am I misunderstanding something?

"""
Simple settings nodes have headlines of the form @<type> name = val. These settings set the value of name to val, with the indicated type:
"""
ISTM that the headlines are of the form

    @type val

or can you actually write

@path = <some path>

etc? I can find no examples of this...

Thomas Passin

unread,
Mar 24, 2024, 12:10:38 AMMar 24
to leo-editor
@path headlines must be like this: @path c:/test/python. Use forward slashes even on Windows. The pathlib methods will use forward slashes and output whatever is right for the OS.

jkn

unread,
Mar 24, 2024, 5:20:38 AMMar 24
to leo-editor
That's not what I am commenting on - I'm well aware of all of that.

I am commenting on the fact that the documentation says that an @path directive (and all the others in the table below) takes the form

@path **=** my/path

whereas in fact no equals sign is necessary (any might well cause an error?)

@path  my/path


Edward K. Ream

unread,
Mar 24, 2024, 6:27:45 AMMar 24
to leo-e...@googlegroups.com
On Sun, Mar 24, 2024 at 4:20 AM jkn <jkn...@nicorp.f9.co.uk> wrote:

I am commenting on the fact that the documentation says that an @path directive (and all the others in the table below) takes the form

@path **=** my/path

whereas in fact no equals sign is necessary (any might well cause an error?)

@path  my/path

I see no equal sign in Leo's directive reference page. What documentation are you talking about?

Edward

jkn

unread,
Mar 24, 2024, 6:39:03 AMMar 24
to leo-editor
Hi Edward
I put the link (to a different part of the documentation) in an earlier post: https://leo-editor.github.io/leo-editor/customizing.html#simple-settings-nodes

The section you reference is clear, and correct. The link above perhaps references an older syntax?

Regards, J^n

Thomas Passin

unread,
Mar 24, 2024, 8:01:40 AMMar 24
to leo-editor
Settings and headlines are not the same.

Edward K. Ream

unread,
Mar 24, 2024, 8:22:16 AMMar 24
to leo-e...@googlegroups.com
On Sun, Mar 24, 2024 at 5:39 AM jkn <jkn...@nicorp.f9.co.uk> wrote:

I put the link (to a different part of the documentation) in an earlier post: https://leo-editor.github.io/leo-editor/customizing.html#simple-settings-nodes

Thanks. I'll take a look.

Edward

jkn

unread,
Mar 24, 2024, 8:23:26 AMMar 24
to leo-editor
I see what you mean... yes, I conflated the two (a site search for '@path' got me to the link I mentioned, and not to the canonical documentation Edward pointed out)

In that case, can someone give me an example of the use for

@settings
    @path mypath = path/to/...

?
Thanks, J^n

Edward K. Ream

unread,
Mar 24, 2024, 8:24:47 AMMar 24
to leo-e...@googlegroups.com
On Sun, Mar 24, 2024 at 7:01 AM Thomas Passin <tbp1...@gmail.com> wrote:
Settings and headlines are not the same.

At last I understand :-)  As Thomas says, the syntax for settings nodes is different from directives.

In other words, Leo's documentation appears to be correct.

Edward

Thomas Passin

unread,
Mar 24, 2024, 8:48:13 AMMar 24
to leo-editor
It's still a good question, though.  Is there an actual use for a setting of type "@path"?    Having it included in that documentation section seems to imply that there is, though its inclusion may have been meant only as a syntax example. Can anyone resolve this?

jkn

unread,
Mar 24, 2024, 9:49:40 AMMar 24
to leo-editor
I think the fact that (recently discovered) ... there seems to be no method getPath() in LeoConfig.py  tells us something.

Not trying to make a big deal of this, mind you...
Reply all
Reply to author
Forward
0 new messages