pptx-automizer icon indicating copy to clipboard operation
pptx-automizer copied to clipboard

Added slide elements are not translated to match root's template

Open jhaase1 opened this issue 1 year ago • 7 comments

I have functionality that is largely copied from the examples on the website. However, I'm having an issue that even though the imported slides are "assigned" the root layout. The content is not shifted right in the first image like it is in the second even though both have the side banner content only layout

unapplied

applied

import Automizer from 'pptx-automizer';
const fs = require('fs');
const path = require('path');

async function getSlideNumbers(pres, source) {
  const slideNumbers = await pres
      .getTemplate(source)
      .getAllSlideNumbers();
  return slideNumbers;
}

async function processFile(input_file, pres) {
  try {
      console.log("I think I'm loading:", input_file);
      await pres.load(input_file);

      const slideNumbers = await getSlideNumbers(pres, input_file);

      console.log("I think it had slides:", slideNumbers);

      for (const slideNumber of slideNumbers) {
          console.log("Adding slide:", slideNumber);
          pres.addSlide(input_file, slideNumber);
      }
  } catch (error) {
      console.error(`Error processing ${input_file}:`, error);
  }
}

export async function addSlidesToPresentation(
  file_name_list,
  templatePath = "C:/Users/haas1/programming/presentation_creator/template.pptx",
  outputPath = "C:/Users/haas1/programming/presentation_creator/output.pptx"
) {
  // First, let's set some preferences!
  const automizer = new Automizer({
    // turn this to true if you want to generally use
    // Powerpoint's creationIds instead of slide numbers
    // or shape names:
    useCreationIds: false,

    // Always use the original slideMaster and slideLayout of any
    // imported slide:
    autoImportSlideMasters: false,

    // truncate root presentation and start with zero slides
    removeExistingSlides: true,

    // activate `cleanup` to eventually remove unused files:
    cleanup: false,

    // Set a value from 0-9 to specify the zip-compression level.
    // The lower the number, the faster your output file will be ready.
    // Higher compression levels produce smaller files.
    compression: 0,

    // You can enable 'archiveType' and set mode: 'fs'.
    // This will extract all templates and output to disk.
    // It will not improve performance, but it can help debugging:
    // You don't have to manually extract pptx contents, which can
    // be annoying if you need to look inside your files.
    // archiveType: {
    //   mode: 'fs',
    //   baseDir: `${__dirname}/../__tests__/pptx-cache`,
    //   workDir: 'tmpWorkDir',
    //   cleanupWorkDir: true,
    // },

    // use a callback function to track pptx generation process.
    // statusTracker: myStatusTracker,
  });

  // Now we can start and load a pptx template.
  // With removeExistingSlides set to 'false', each addSlide will append to
  // any existing slide in RootTemplate.pptx. Otherwise, we are going to start
  // with a truncated root template.
  let pres = automizer.loadRoot(templatePath)

  for (const input_file of file_name_list) {
    await processFile(input_file, pres);
  };

  // Get useful information about loaded templates:
  /*
  const presInfo = await pres.getInfo();
  const mySlides = presInfo.slidesByTemplate('shapes');
  const mySlide = presInfo.slideByNumber('shapes', 2);
  const myShape = presInfo.elementByName('shapes', 2, 'Cloud');
  */

  // addSlide takes two arguments: The first will specify the source
  // presentation's label to get the template from, the second will set the
  // slide number to require.

  // Finally, we want to write the output file.
  pres.write(outputPath).then((summary) => {
    console.log(summary);
  });

  // It is also possible to get a ReadableStream.
  // stream() accepts JSZip.JSZipGeneratorOptions for 'nodebuffer' type.
  // const stream = await pres.stream({
  //   compressionOptions: {
  //     level: 9,
  //   },
  // });
  // You can e.g. output the pptx archive to stdout instead of writing a file:
  // stream.pipe(process.stdout);

  // If you need any other output format, you can eventually access
  // the underlying JSZip instance:
  // const finalJSZip = await pres.getJSZip();
  // Convert the output to whatever needed:
  // const base64 = await finalJSZip.generateAsync({ type: 'base64' });

}

jhaase1 avatar Aug 12 '24 01:08 jhaase1

Hi! Are the master slides of your loaded files in file_name_list based on template.pptx? Do they have the same page size? You screenshots shown above remind me of a problem I once had with using different page sizes. But it could also be related with placeholders having different positions on master slides. You can avoid conflicts by using "clones" of template.pptx.

Please let me know if this could already help, otherwise I would need to take a look at template.pptx and at least one of the loaded pptx. Cheers!

singerla avatar Aug 12 '24 08:08 singerla

Hi, There's a 1-1 matching layouts between the template image

and the files image

They're actually from a common ancestor rather a descendent but are identical except for the image on the sidebar. I'll recreate a file based off of template and report back also

template.pptx All That Is Hidden.pptx

jhaase1 avatar Aug 17 '24 20:08 jhaase1

Making a slide that's a descendent of template worked; however, it kept the "No side banner" layout rather than switching to the "Side banner" layout. I want to be able to add them with either layout. How do I do that?

jhaase1 avatar Aug 17 '24 20:08 jhaase1

Hi! I'm sorry for the late response... Could you figure out a solution for this?

singerla avatar Sep 19 '24 07:09 singerla

Nope, I've been smashed at work and haven't even looked at it 😕

jhaase1 avatar Sep 20 '24 11:09 jhaase1

If I'm understanding the code correctly it's possible to add a callback to the addSlide method

https://github.com/singerla/pptx-automizer/blob/0887d73325c7dcd567211e996d4375c574148d19/src/automizer.ts#L325

Could this callback be the useSlideLayout argument?

https://github.com/singerla/pptx-automizer/blob/0887d73325c7dcd567211e996d4375c574148d19/src/interfaces/islide.ts#L28

jhaase1 avatar Oct 05 '24 23:10 jhaase1

Hi! Please take a look at https://github.com/singerla/pptx-automizer/blob/main/tests/add-slide-master.test.ts

singerla avatar Oct 07 '24 08:10 singerla

Ok, I tried that and it pulls the layout from the source presentation (so I end up with duplicate layouts). Also adding useSlideLayout causes the imported slides masters to have the same skew as in imported slides are having. Can I apply the layout from the root presentation? Also how can I switch between the "Side Banner" master and the "No Side Banner" master?

(slide) => {
  slide.useSlideLayout("Blank");
}

image

jhaase1 avatar Oct 13 '24 22:10 jhaase1

I took a look at the two .pptx files you have uploaded recently. On first sight: While template.pptx Master is in 16:9 format, All That Is Hidden.pptx is defined as 4:3 ratio. Please note that (afaik) it is not possible to use different slide sizes in one presentation in PowerPoint.

Please let me know if this helps!

singerla avatar Oct 17 '24 12:10 singerla

I want to change to 16:9 at import

jhaase1 avatar Oct 17 '24 15:10 jhaase1

You could give it a try and insert the image on a blank page like here https://github.com/singerla/pptx-automizer/blob/main/tests/add-single-images.test.ts

You could use slide.getAllElements() for a list containing the image per slide. Adjust position afterwards with ModifyShapeHelper.setPosition.

By that, you can avoid importing the slide master.

singerla avatar Oct 17 '24 20:10 singerla

Auto scale (on slide.add) might be a valid feature to add, but you can do this outside the library too, just to add to Thomas' comment, you will likely need the slide.getDimensions() that I added mostly for this purpose a while back.

It might be something like this, but could be much more advanced.

async function adjustSlideElements(slide, targetWidth = 1920, targetHeight = 1080) {
    const dimensions = slide.getDimensions();
    const { width: currentWidth, height: currentHeight } = dimensions;

    // If dimensions match the target, no scaling needed
    if (currentWidth === targetWidth && currentHeight === targetHeight) {
        console.log("No scaling required, dimensions match.");
        return;
    }

    // Determine scale factors for width and height
    const scaleWidth = targetWidth / currentWidth;
    const scaleHeight = targetHeight / currentHeight;

    // Check if the aspect ratio is the same
    const currentAspectRatio = currentWidth / currentHeight;
    const targetAspectRatio = targetWidth / targetHeight;
    let scaleFactor;

    if (currentAspectRatio === targetAspectRatio) {
        // Same aspect ratio: scale uniformly
        scaleFactor = Math.min(scaleWidth, scaleHeight);
    } else {
        // Different aspect ratio: scale based on width and height independently
        scaleFactor = { width: scaleWidth, height: scaleHeight };
    }

    // Get all elements in the slide
    const elements = slide.getAllElements();

    elements.forEach(element => {
        if (typeof scaleFactor === 'number') {
            // Apply uniform scaling (same aspect ratio)
            element.setPosition({
                x: element.position.x * scaleFactor,
                y: element.position.y * scaleFactor,
            });
            element.setSize({
                width: element.size.width * scaleFactor,
                height: element.size.height * scaleFactor,
            });
        } else {
            // Apply independent scaling (different aspect ratio)
            element.setPosition({
                x: element.position.x * scaleFactor.width,
                y: element.position.y * scaleFactor.height,
            });
            element.setSize({
                width: element.size.width * scaleFactor.width,
                height: element.size.height * scaleFactor.height,
            });
        }
    });
}

MP70 avatar Oct 22 '24 20:10 MP70

Thanks a lot, @MP70 ! It's great to have you back!

singerla avatar Oct 23 '24 06:10 singerla

Hi All, The problem with just scaling is that it doesn't match the layout that's in the template. I want to apply the template version of the layout. Is there a method to do that while doing the import? Thank you so much for all your help!

jhaase1 avatar Nov 06 '24 01:11 jhaase1

Are you able to share/publish current code + files or is it proprietary ?

MP70 avatar Nov 06 '24 12:11 MP70

Not proprietary, just a volunteer project that I'm doing. This is the file that interacts with pptx-automizer.

Here's the template I'm using and a few example files. I can attach more if you want

template.pptx All That Is Hidden.pptx Ave Maria (Kantor).pptx Lord of the Dance.pptx

jhaase1 avatar Nov 09 '24 15:11 jhaase1

Hi @jhaase1, I was playing around a bit with your input. I assume you are trying to convert several slides from several source files to a 16:9 output presentation, and it should look nice in all cases, right? :smiley:

It seems as if PowerPoint is doing good automated adjustmenton pasting slides from a source pptx to template.pptx manually. So, we need to reproduce this behaviour in our code.

Some thoughts:

  • You don't need to import a slide layout from a source pptx, we will only use template.pptx.
  • The source files and the target template are of equal size, while they differ in width.
  • This is why we need to adjust x-position (from the left), and maybe scale all a bit larger.
  • All shapes should eventually be centered and middled.
  • Given all shapes in all of your source pptx files are equally sized, we can reduce complexity a bit.

The snipped proposed by @MP70 is basically doing the right things, but I guess you will prefer a more customizable setting. So, please give this a try:

async function adjustSlideElements(slide: ISlide) {
  // Get all elements in the slide
  // Don't forget to use 'await'
  const elements = await slide.getAllElements();

  // All shapes should be vertically centered to these dimensions:
  const targetDimensions = {
    // imagine a virtual rectangle with a width of 23cm
    w: CmToDxa(23),
    // and a position of 10cm from the left.
    x: CmToDxa(10),
  };

  // We enlarge all shapes by 120%
  const scale = 1.2;

  elements.forEach((element) => {
    slide.modifyElement(element.name, (xml: XmlElement) => {
      // This will update shape position from the left,
      // centered and according to the target vertical coordinates:
      const targetOffset = (targetDimensions.w - element.position.cx) / 2;
      const targetPos = {
        x: targetDimensions.x + targetOffset,
      };
      ModifyShapeHelper.setPosition(targetPos)(xml);

      // This will 'zoom' into the shape respecting its updated position:
      const addWidth = element.position.cx * scale - element.position.cx;
      const addHeight = element.position.cy * scale - element.position.cy;
      const targetSize = {
        w: element.position.cx + addWidth,
        h: element.position.cy + addHeight,
        x: targetPos.x - addWidth / 2,
        y: element.position.y - addHeight / 2,
      };
      ModifyShapeHelper.setPosition(targetSize)(xml);
    });
  });
}

Looking forward to your feedback!

singerla avatar Nov 11 '24 11:11 singerla

That worked perfectly! I needed to dial in the constants a bit but I cannot even see the difference between built-in layout and adjustSlideElements!

jhaase1 avatar Dec 10 '24 00:12 jhaase1