Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR for inserting arbitrary PNG #62

Closed
MP70 opened this issue Apr 28, 2023 · 14 comments
Closed

PR for inserting arbitrary PNG #62

MP70 opened this issue Apr 28, 2023 · 14 comments

Comments

@MP70
Copy link
Collaborator

MP70 commented Apr 28, 2023

Sorry, I know I've just added a PR, not expecting a response soon just thought I'd add it while its on my mind..

Other libs such as node-pptx allow the user to add an arbitrary image in whereas this seems to require the image is already on a template slide. I would like the ability to extend templating (in spirit) to images, like how we have replaceText which takes a text element ID maybe we have a 'replaceImage' which will take a imageElement ID and copy the new image into ppt/images, remove the old image and update the reference in the element. This will be trivial to implement BUT the issue is that it is going to be painful for the user to find the Image element ID in order to pass it in replaceImage.

so could/should we:

  1. take the image file name and find the element based on that
  2. take a imageElement ID
  3. take a text element in the slide that matches a certain regex e.g {%image Image1} then modify it so it becomes a image element, user can pass in W/H (and position?) at replace time?
  4. suggest the user does it as Use pptx-automizer along with from-scratch-libraries (e.g. PptxGenJS) #60
  5. something else/out of scope of this lib.
@singerla
Copy link
Owner

This would be helpful for my projects, too. I could get rid of maintaining image collections in pptx, but use images from a directory.

1.) will be easy to implement though;
2.) I don't know what is an imageElement ID?;
3.) seems to be both, hacky and interesting: Match many items by condition & Turn one thing into another;

I think 4.) is the way with least effort/highest gain;

@MP70
Copy link
Collaborator Author

MP70 commented Apr 28, 2023

This would be helpful for my projects, too. I could get rid of maintaining image collections in pptx, but use images from a directory.

1.) will be easy to implement though;
2.) I don't know what is an imageElement ID?;

My bad phrasing; the selector (id/name) of the image element.

3.) seems to be both, hacky and interesting: Match many items by condition & Turn one thing into another;

It's how its done in docx templater, when I say modify that's just logically, implementation may be to remove the text element and add an image element in its place.

I think 4.) is the way with least effort/highest gain;

yes agreed, though it adds a dependency and requires the user to know about X/Y positioning. We could always come back later and add a method of getting x/y position of {replaceElement} I suppose to enable vague compatibility with replace style templating.

@andrew-t-adaas
Copy link

I also faced a case when I need to replace an image on the template, but I follow an example in the documentation. My current solution is to have a separate slide with assets/images inside to use them when needed by ElementSelector. But there are a few problems:

  1. if you need to have a customizable set of images -> you need to generate slides with ElementSelectors somehow or do it manually and then use Selectors
  2. If you have any content that displays on top of the image -> image overlaps content and you need to remove all elements and add them a second time in a proper sequence from Template Slide.

So from my point of view, it would be great to have the possibility:

  1. replaceImage for templates
    OR/AND
  2. add an image from the file/buffer/stream
  3. control image/shape layer/depths

It's not something urgent, but I believe that it could be a good functionality for the library. Many Thanks!

@singerla
Copy link
Owner

Yes, I totally agree! I can confirm this can be painful to sail around. I need to do more investigation on this!

@labe-me
Copy link

labe-me commented Oct 9, 2023

Hello,

I am trying to replace a few images with images from my disk.

Using pptxgenjs I created a .pptx containing the images (as a library), something like :

import PptxGenJS from "pptxgenjs";
import sharp from "sharp";
...
const base = new PptxGenJS();
let slide = base.addSlide();
const size = await sharp("./test-img.jpg").metadata();
await slide.addImage({
    objectName: "My Image",
    path: "./test-img.jpg",
    w: (size.width || 0) * 0.010416, // inches 96dpi
    h: (size.height || 0) * 0.010416, // inches 96dpi
});
await base.writeFile({ fileName: "templates/images.pptx" });

And then I can use the image using the automizer :

let presentation =  automizer
    .loadRoot("empty.pptx")
    .load("full.pptx")
    .load("images.pptx")
    .addSlide("full.pptx", 1, async function (slide) {
      // insert image 
      slide.addElement("images.pptx", 1, "My Image", [
        modify.updatePosition({ x: 360000 * 10, y: 360000 * 10 })
      ]);
      return slide;
    });

But what I ultimately want is to replace an existing image with this new one so it will inherit ppt animations, size, position, etc.

How should I do that ?

Thanks!

@MP70
Copy link
Collaborator Author

MP70 commented Oct 9, 2023

Not my library, but AKAIK there is no 'native' way of doing this here. What you describe is basically Implementation suggestion 1 or 2 above. It should be a pretty tiny PR, more or less you really just need to add the new image to the folder, and then update the image element to refer to the new image, then finally delete the old image media.

@singerla
Copy link
Owner

singerla commented Oct 9, 2023

This is an interesting approach, but i'm afraid, what you need it is not implemented yet.
While slide.addElement can't inherit properties of an existing element, slide.modifyElement can't be used to modify the target image to refer to new media.

I could imagine something like this:

let presentation =  automizer
  .loadRoot("empty.pptx")
  .load("full.pptx")
  .load("images.pptx")
  .addSlide("empty.pptx", 1, async function (slide) {
    // add the externally created image to a temporary slide
    // to have it in your output presentation
    slide.addElement("images.pptx", 1, "My Image");
    // The created media target of this image needs to be 
    // made available for other operations.
  });
  .addSlide("full.pptx", 1, async function (slide) {
    // modify animated image 
    slide.modifyElement("My animated image", [
      // Todo is `modify.updateRelationTarget`:
      modify.updateRelationTarget(*/ get created media target from "My Image" /*)
    ]);
    return slide;
  });

It is basically what @MP70 figured out, but you will need some information about the created contents during runtime. Some caveats could be:

  • externally created image and modified animated image might need to have the same filetype (e.g. svg is more complex than others)
  • animation stuff is fully untested and could cause trouble

You can take a look at ppt\slides\_rels\slide1.xml.rels

<Relationship Target="../media/image6.png" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Id="rId3"/>

We need to "simply" update Target for the corresponding rId.

So, a more elegant approach would be an image modifier to do this in one go, just like:

let presentation =  automizer
    .loadRoot("empty.pptx")
    .load("full.pptx")
    .load("images.pptx")
    .addSlide("full.pptx", 1, async function (slide) {
      // mody animated image 
      slide.modifyElement("My animated image", [
        // This could be a shortcut:
        modify.updateTarget("images.pptx", 1, "My Image")
      ]);
      return slide;
    });

I need to investigate this more detailed! 🤔

@singerla
Copy link
Owner

singerla commented Oct 9, 2023

Referring to @andrew-t-adaas suggestion, it would be an improvement to add an image directly, without the need to build a .pptx around first.

let presentation =  automizer
    .loadRoot("empty.pptx")
    .load("full.pptx")
    .addSlide("full.pptx", 1, async function (slide) {
      // mody animated image 
      slide.modifyElement("My animated image", [
        // Read from file:
        modify.updateMedia("/path/to/image.jpg")
      ]);
      return slide;
    });

which will be no big thing to implement. But we need to register each new image extension once in [Content_Types].xml, too.

@labe-me
Copy link

labe-me commented Oct 9, 2023

In case it helps someone, We opted for the following post processing workaround :

First we keep track of the images we would like to replace :

slide.modify((e: any) => {
        // retrieve image to replace by its ID
        const img = e.getElementById("5");
        // find the r:embed value
        const rid =
          img.parentNode.nextSibling.firstChild.getAttribute("r:embed");
        // store the slideId, the rid and the expected image path in some global array
        replacements.push({ ... })
});

When the ppt is done we replace the images in the resulting zip file (work in progress, should be optimized by slide) :

const zip = await presentation.getJSZip();
for (let replacement of replacements){
  await zip.files["ppt/slides/_rels/slide"+replacement.slideId+".xml.rels"]
    .async("string")
    .then(async (data) => {
      const doc = new DOMParser().parseFromString(data);
      const targetPath = doc
        .getElementsByTagName("Relationship")
        .filter((e) => e.getAttribute("Id") === replacement.rid)[0]
        .getAttribute("Target");
      const img = fs.createReadStream(replacement.replaceWithPath);
      await zip.file(targetPath.replace("../media", "ppt/media"), img);
    });
}

  const out = fs.createWriteStream("output/result.pptx");
  await zip
    .generateNodeStream({ type: "nodebuffer", streamFiles: true })
    .pipe(out);

Quite ugly but it works :)

@singerla
Copy link
Owner

singerla commented Oct 12, 2023

Update: It is now possible to load and apply an external media file by modifier.

@labe-me Thanks a lot for your code snippet! Based on this simple and effective inspiration, I have implemented a generic solution to import files to ppt/media and override Target 😃

  const automizer = new Automizer({
    // ...
    // specify a fallback dir to import media files from
    mediaDir: `${__dirname}/../__tests__/media`,
  });

  const pres = automizer
    .loadRoot(`RootTemplate.pptx`)
    // load one or more files from mediaDir
    .loadMedia([`feather.png`, `test.png`], /* or use a custom dir */)
    .load(`SlideWithShapes.pptx`, 'shapes')
    .load(`SlideWithImages.pptx`, 'images');

  pres.addSlide('shapes', 1, (slide) => {
    slide.addElement('images', 2, 'imagePNG', [
      // directly override "Target" attribtue of recently created relation for 'imagePNG'
      ModifyImageHelper.setRelationTarget('test.png'),

      // eventually update size
      ModifyShapeHelper.setPosition({
        w: CmToDxa(5),
        h: CmToDxa(3),
      }),
    ]);
  });

Please also refer to add-external-image.test.ts.

@labe-me
Copy link

labe-me commented Oct 12, 2023

@singerla Nice! Can the setRelationTarget() modifier be used on an existing slide's image too?

slide.modifyElement('MyImageId', [  
  ModifyImageHelper.setRelationTarget("newImage.png") 
]);

@singerla
Copy link
Owner

No, currently it does only work on added images... Needs some tweaks 😃

@singerla
Copy link
Owner

singerla commented Oct 13, 2023

Please check out v0.4.0 to use setRelationTarget with slide.modifyElement on existing or with with slide.addElement on added images.

@andrew-t-adaas: Modified images will not be moved to top layer any more.

@singerla
Copy link
Owner

This could now be done with help of beautiful PptxGenJS

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants