Exporting images in TW using wikitext

Hey all!

Working on exporting captures of graphs in TW5-Graph, when I’ve hit a snag that I feel like I’m not seeing the simple solution.

How do you export image tiddlers like PNG using actions? Right now, I can get my graph engine to create the png and put it into a temporary tiddler, but getting that tiddler to the saverHandler without it getting mangled is proving tricky. It’s because PNG tiddlers store their data base64encoded, and it needs to decode before downloading.

I tried creating an export template that did something like <$text text={{{ [<imageTiddler>get[text]decodebase64:binary[]] }}} />, but it turns out $text actually mangles its contained text by removing “\r” characters, and even if it didn’t, I really don’t think I can just drop binary data into a dom element’s body and expect it to work. My hex editor is showing other differences…

Surely somewhere along the line, someone has had to export binary tiddlers. I know this could be done easily with raw javascript, but TW5-Graph is about wikitext solutions.

Anyone know?

-Flibbles

Maybe export them in zip files?

I’m not sure that’s the outcome I need. The idea is someone clicks the export button on their graph, and their browser downloads an image of the graph. Making a zip file of it first just adds extra steps for the user.

I think I’m going to have to bite the bullet here and make a startup module that introduces a new tm-message of some sort.

At the time, it seemed that this was the only solution that didn’t require me to use javascript. Or rather, using JS that someone who knew what they were doing had created.

The main thing is that every platform now has some unzip tool available.

It will be interesting to see what you come up with. It does seem to be an omission from the kit of tools that are available.

I’m not sure how GitHub - tiddly-gittly/Tid2PNG: 条目截图,捕获当前条目为PNG文件并保存到本地 works, but you can let your copilot have a look.

I’m using this plugin.

@Flibbles I believe your problem may be solved as I did for binary files. One needs to have the appropriate mime type to display binaries which should include PNG and allow download to go from base64encoded to the required format.

Note how I revisited this recently

@linonetwo : That’s not quite what I need. The graph engines, like vis-network already do the “capture to png”. I just need to download it.

@TW_Tones : Wow, looks like nobody really knows how to do this. I’m not really sure I follow the bit at the end of that thread where all you have to do is register a file type. PNG is already registered.

Currently, I pushed TW5-Graph with a startup module hack to accomplish downloading binary images, but there is absolutely no reason why a hack should be necessary. I’ve proposed the following PR:

It’s a half-line change which makes TW capable of downloading binary files. This might not be the way we want to do it. One shouldn’t have to indicate base64 in the mime-type. Perhaps we should just allow the mime type to be “image/png” or “application/7z”, and it’ll download correctly, but that might have more backward compatibility considerations than what I’m proposing here.

I belive it should be possible doing the foillowing, it seems to work but the result is an empty file;

\procedure download-binary-actions() 
<$action-sendmessage $message="tm-download-file" $param=<<currentTiddler>> type={{!!type}} filename=<<currentTiddler>>/>
\end download-binary-actions

<$let currentTiddler="child-on-swing-favicon-orig.png">
<$button actions=<<download-binary-actions>> >
Download <<currentTiddler>> as {{!!type}}
</$button>

Poorly colored png example.


end of image

@Scott_Sauyet seems to be doing what you need via a data statement. See

see also $:/core/modules/parsers/imageparser.js

@TW_Tones What @Scott_Sauyet has doesn’t work for me, because that creates a download link, whereas I need to initiate the download immediately, as a button has already been pressed to generate the png in the first place.

What you put above doesn’t work because the tm-download-file is trying to render the image using the imageParser", as the image tiddler’s type warrants. This creates a link, (e.g. <img source="data:image/png;base64,AG34..." ></img>). But then the download action only knows how to pull content from that either as text/plain or as text/html, and since it’s anything other than an html type (it’s an image type). It pulls the resulting parse-tree-node’s “textContent”, which is nothing, because it was an open-and-shut <img> element.

Here’s something that does work, but only if we merge my PR.

\procedure download-binary-actions() 
<$action-sendmessage $message="tm-download-file" $param="$:/core/templates/plain-text-tiddler" type={{{ [{!!type}addsuffix[;base64]]}}} currentTiddler=<<currentTiddler>> filename=<<currentTiddler>>/>
\end download-binary-actions

<$let currentTiddler="$:/favicon.ico">
<$button actions=<<download-binary-actions>> >
Download <<currentTiddler>> as {{!!type}}
</$button>

This renders the content first using $:/core/templates/plain-text-tiddler's type, which is text/html. That template spits out its current tiddler as plain-text, so you get a wall of base64 text. Then same as before, the download action tries to pull content from that as either text/plain or as text/html based on the requested type, and since image/png isn’t html, it gets plaintext. In this case, the blob of base64.

But my code also plugs the ;base64 onto the end of the type, so even though tiddlywiki is only offering raw base64, it says it’s a ;base64 type, and our browsers can figure it out and decrypt while downloading. So long as it’s presented as a download link and not a blob, which is what my PR does.

This is not great, but it works for me. What you wrote should be able to work exactly as is, but I didn’t push a PR to do that. Instead, it’s very minimal, because I get the sense we’re trying to stabilize TW right now as we’re moving toward V5.4.0. (I’m figuring this from the way none of my PRs in the last month have been merged.).

I have to defer to your knowledge of this as I don’t have the background knowledge however I did not think you needed to submit a PR to solve this;

  • I thought it was always possible or should have been to export any thing we can import.
  • this was not possible with executables, but was solved at the earlier link I shared.

regardless I support your desire to resolve this but hope it is for all binaries driven by their type.

  • worst case that we add a plugin for additional support.

keep in mind you may be able to add this support using a module that overrides the core behaviour, by later application.

I followed lineonetwo’s lead above to this which may be helpful:

https://github.com/tiddly-gittly/Tid2PNG/blob/master/src/tid2png/index.ts

let img_elist = tiddler_frame.querySelectorAll("img");
    let imgArray = Array.from(img_elist);
    // console.log(imgArray);

    Promise.all(imgArray.map((item, index, arr) => {
      // let img_e = document.querySelector("img");
      // img_e.getAttribute("src");
      // console.log(item.getAttribute("src"));
      return this.getBase64Image(item.getAttribute("src"))
    })).then(result => {

<truncated>

if (tiddler_frame) {
        html2canvas(tiddler_frame, setup).then((canvas) => {
          canvas.toBlob((blob) => {
            if (details) {
              details.hidden = false;
            }
            var url = URL.createObjectURL(blob);
            var a = document.createElement('a');
            a.href = url;
            a.download = `${this.makeTitleSafe(this.getVariable("currentTiddler"))}_${new Date().getTime()}.png`;
            a.click();
            window.URL.revokeObjectURL(url);
            NProgress.done();
          });

It’s a javascript solution though, invoked after a button pressed, that create a image download link and self-click it !

Thanks @jacng, That looks like it would work. It looks a little bit like the hack I put into TW5-Graph for the time-being, but I do believe there should be an easy way to export images. using tm-download-file on a binary image tiddler should give you an image. Not a blank file, or one filled with base64.

Maybe not for this release, since I think we’re trying to stabilize, but soon.

For what it’s worth, I tested the following using a javascript macro in a single file TW to initiate a download of an image tiddler. It did save the image successfully with my filename in my Chrome download directory. The macro is listed below.

A side track: This topic confuses me and I could be missing something. Having read about the hassle of importing images into TW, I have the impression that a node.js or web/webdav server is needed to import and then externalize images. A single file TW can’t do it. But with this, it seems that single file TW can at least save imported images to the browser download directory ?!? OTOH, TW can download itself as a HTML file, so this is probably how it does it internally.


Test tiddler:

title: Test download
\define downloadimg()
<$macrocall $name="download-image.js"  imageTiddler="image_png"  imageFilename="new cat.png" />
\end

This button will download the image stored in the "image_png" tiddler.

<$button actions=<<downloadimg>> >
    Download Image
</$button>

TW JavaScript macro:

title: download-image.js
Type:application/javascript
module-type: macro

/*\
title: download-image.js
type: application/javascript
module-type: macro
\*/

(function(){

"use strict";

exports.name = "download-image.js";
exports.params = [
    {name: "imageTiddler"},
    {name: "imageFilename"}
];
exports.run =  function( imageTiddler, imageFilename ) {

    if (!imageTiddler) {
         console.log("No image tiddler provided.");
         return;
    };

    const imagetid = $tw.wiki.getTiddler( imageTiddler );
    if ( !imagetid || !imagetid.fields.text || !imagetid.fields.type.startsWith("image/") ) {
          alert( "Error: Tiddler [" + imageTiddler + "] is not a valid image tiddler." );
          return;
     };

     // Create a temporary download link and trigger a self-click
     const link = document.createElement('a');

     // The image data (usually a base64 Data URI) is in the 'text' field
     link.href = "data:image/png;base64,"+imagetid.fields.text;
     link.download = imageFilename; // Set desired filename
     document.body.appendChild(link);
     link.click();
     document.body.removeChild(link);

  return;
};

})();

As see this it is simply not true. First you can import images as much as you want on any platform. Droping an image into the text of a tiddler will both import and link to it, or drop it else where to import it then reference as needed.

Then there are a range of ways to subsequently externalise an image, in someways as I understand it it is automatic with node.

I use single file wikis a lot

I like/have a batch exporter tool that turns images into external ones and gives me a zip file of images I can save in the same or selected folder below the single file wiki. I just need to do this occasionaly before the wiki gets too slow to load.

I will share if I can find it. Yep, here it is Externalising Image Files with JSzip using tools you probably already have

Yes, it was an impression I had which is wrong on reflection. A single file TW can save itself as an HTML, and the JSzip plugin can pack and save images as zip. Likewise, a single file TW can save an image in the download directory.

Which means that a single file TW can, in theory, import an image from the web into TW, externalize it by saving the image and deleting the image tiddler, then link to that external image. The image externalizing part didn’t happen somehow, so I may still be missing something here.

The limitation is the image is saved to the browser download folder, which an external script can help to overcome. The image filename can be anything, so it can store a local path (maybe using “%%” instead of “/”) and a timestamp. A script, something like Polly, can then scan the download folder to move and rename these images to their intended folders.

Incidentally, the script will also work on the HTML files saved by single file TW, if we use “$:/config/SaveWikiButton/Filename” to store local path and timestamp into the saved TW filename. Voila, another TW saver :grimacing: !

Note: This is offtrack from the original topic, I wouldn’t mind if it’s moved to another thread.