Coder Social home page Coder Social logo

pptx-automizer's Introduction

pptx-automizer: A Powerful .pptx Modifier for Node.js

pptx-automizer is a Node.js-based PowerPoint (.pptx) generator that automates the manipulation of existing .pptx files. With pptx-automizer, you can import your library of .pptx templates, merge templates, and customize slide content. pptx-automizer will not write files from scratch, but edit and merge existing pptx files. You can style template slides within PowerPoint, and these templates will be seamlessly integrated into the output presentation. Most of the content can be modified by using callbacks with xmldom.

pptx-automizer is particularly well-suited for users who aim to manage their own library of .pptx template files, making it an ideal choice for those who work with intricate, well-designed customized layouts. With this tool, any existing slide or even a single element can serve as a data-driven template for generating output .pptx files.

This project is accompanied by automizer-data. You can use automizer-data to import, browse and transform .xlsx- or .sav-data into perfectly fitting graph or table data.

Thanks to all contributors! You are always welcome to share code, tipps and ideas. We appreciate all levels of expertise and encourage everyone to get involved. Whether you're a seasoned pro or just starting out, your contributions are invaluable. Get started

If you require commercial support for complex .pptx automation, you can explore ensemblio.com. Ensemblio is a web application that leverages pptx-automizer and automizer-data to provide an accessible and convenient solution for automating .pptx files. Engaging with Ensemblio is likely to enhance and further develop this library.

Table of contents

Requirements and Limitations

This generator can only be used on the server-side and requires a Node.js environment.

Shape Types

At the moment, you might encounter difficulties with special shape types that require additional relations (e.g., hyperlinks, video and audio may not work correctly). However, most shape types, including connection shapes, tables, and charts, are already supported. If you encounter any issues, please feel free to report any issue.

Chart Types

Extended chart types, like waterfall or map charts, are basically supported. You might need additional modifiers to handle extended properties, which are not implemented yet. Please help to improve pptx-automizer and report issues regarding extended charts.

Animations

Animations are currently out of scope of this library. You might get errors on opening an output .pptx when there are added or removed shapes. This is because pptx-automizer doesn't synchronize id-attributes of animations with the existing shapes on a slide.

Slide Masters and -Layouts

pptx-automizer supports importing slide masters and their associated slide layouts into the output presentation. It is important to note that you cannot add, modify, or remove individual slideLayouts directly. However, you have the flexibility to modify the underlying slideMaster, which can serve as a workaround for certain changes.

Please be aware that importing slideLayouts containing complex contents, such as charts and images, is currently not supported. For instance, if a slideLayout includes an icon that is not present on the slideMaster, this icon will break when the slideMaster is auto-imported into an output presentation. To avoid this issue, ensure that all images and charts are placed exclusively on a slideMaster and not on a slideLayout.

Direct Manipulation of Elements

It is also important to know that pptx-automizer is currently limited to adding things to the output presentation. If you require the ability to, for instance, modify a specific element on a slide within an existing presentation and leave the rest untouched, you will need to include all the other slides in the process. Find some workarounds below.

PowerPoint Version

All testing focuses on PowerPoint 2019 .pptx file format.

Installation

There are basically two ways to use pptx-automizer.

As a Cloned Repository

If you want to see how it works and you like to run own tests, you should clone this repository and install the dependencies:

$ git clone [email protected]:singerla/pptx-automizer.git
$ cd pptx-automizer
$ yarn install

You can now run

$ yarn dev

and see the most recent feature from src/dev.ts. Every time you change & save this file, you will see new console output and a pptx file in the destination folder. Take a look into __tests__-directory to see a lot of examples for several use cases!

As a Package

If you are working on an existing project, you can add pptx-automizer to it using npm or yarn. Run

$ yarn add pptx-automizer

or

$ npm install pptx-automizer

in the root folder of your project. This will download and install the most recent version into your existing project.

Usage

Take a look into tests-directory to see a lot of examples for several use cases. You will also find example .pptx-files there. Most of the examples shown below make use of those files.

Basic Example

This is a basic example on how to use pptx-automizer in your code:

import Automizer from 'pptx-automizer';

// First, let's set some preferences!
const automizer = new Automizer({
  // this is where your template pptx files are coming from:
  templateDir: `my/pptx/templates`,

  // use a fallback directory for e.g. generic templates:
  templateFallbackDir: `my/pptx/fallback-templates`,

  // specify the directory to write your final pptx output files:
  outputDir: `my/pptx/output`,

  // 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: true,

  // 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('RootTemplate.pptx')
  // We want to make some more files available and give them a handy label.
  .load('SlideWithShapes.pptx', 'shapes')
  .load('SlideWithGraph.pptx', 'graph')
  // Skipping the second argument will not set a label.
  .load('SlideWithImages.pptx');

// 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.
pres
  .addSlide('graph', 1)
  .addSlide('shapes', 1)
  .addSlide('SlideWithImages.pptx', 2);

// Finally, we want to write the output file.
pres.write('myPresentation.pptx').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' });

How to Select Slides Shapes

pptx-automizer needs a selector to find the required shape on a template slide. While an imported .pptx file is identified by filename or custom label, there are different ways to address its slides and shapes.

Select slide by number and shape by name

If your .pptx-templates are more or less static and you do not expect them to evolve a lot, it's ok to use the slide number and the shape name to find the proper source of automation.

// This will take slide #2 from 'SlideWithGraph.pptx' and expect it
// to contain a shape called 'ColumnChart':
pres.addSlide('SlideWithGraph.pptx', 2, (slide) => {
  // `slide` is slide #2 of 'SlideWithGraph.pptx'
  slide.modifyElement('ColumnChart', [
    /* ... */
  ]);
});

// This example will take slide #1 from 'RootTemplate.pptx' and place
// 'ColumnChart' from slide #2 of 'SlideWithGraph.pptx' on it.
pres.addSlide('RootTemplate.pptx', 1, (slide) => {
  // `slide` is slide #1 of 'RootTemplate.pptx'
  slide.addElement('SlideWithGraph.pptx', 2, 'ColumnChart', [
    /* ... */
  ]);
});

You can display and manage shape names directly in PowerPoint by opening the "Selection"-pane for your current slide. Hit ALT+F10 and PowerPoint will give you a (nested) list including all (grouped) shapes. You can edit a shape name by double-click or by hitting F2 after selecting a shape from the list. See MS-docs for more info.

But be aware: Whenever your template slides are rearranged or a template shape is renamed, you need to update your code as well.

Please also make sure that each shape to add or modify has a unique name on its slide. Otherwise, only the last matching shape will be taken as target.

Select slides by creationId

Additionally, each slide and shape is stored together with a (more or less) unique creationId. In XML, it looks like this:

<p:cNvPr name="MyPicture" id="64">
    <a:extLst>
        <a:ext uri="{FF2B5EF4-FFF2-40B4-BE49-F238E27FC236}">
            <a16:creationId id="{0980FF19-E7E7-493C-8D3E-15B2100EA940}" xmlns:a16="http://schemas.microsoft.com/office/drawing/2014/main"/>
        </a:ext>
    </a:extLst>
</p:cNvPr>

This is where name and creationId are coupled together for each shape.

While our shape could now be identified by both, MyPicture or by {0980FF19-E7E7-493C-8D3E-15B2100EA940}, creationIds for slides consist of an integer value, e.g. 501735012 below:

<p:extLst>
   <p:ext uri="{BB962C8B-B14F-4D97-AF65-F5344CB8AC3E}">
      <p14:creationId val="501735012" xmlns:p14="http://schemas.microsoft.com/office/powerpoint/2010/main"/>
   </p:ext>
</p:extLst>

You can add a simple code snippet to get a list of the available creationIds of your loaded templates:

const pres = automizer
  .loadRoot(`RootTemplate.pptx`)
  .load(`SlideWithShapes.pptx`, 'shapes');

const creationIds = await pres.setCreationIds();

// This is going to print the slide creationId and a list of all
// shapes from slide #1 in `SlideWithShapes.pptx` (aka `shapes`).
console.log(
  creationIds
    .find((template) => template.name === 'shapes')
    .slides.find((slide) => slide.number === 1),
);
// Find the corresponding slide-creationId and -number on top of this list.

If your templates are not final and if you expect to have new slides and shapes added in the future, it is worth the effort and use creationId in general:

const automizer = new Automizer({
  templateDir: `${__dirname}/pptx-templates`,
  outputDir: `${__dirname}/pptx-output`,
  // turn this to true and use creationIds for both, slides and shapes
  useCreationIds: true,
});

Regarding shapes, it is also possible to use a creationId and the shape name as a fallback. These are the different types of a FindElementSelector:

import { FindElementSelector } from './types/types';

// This is default when set up with `useCreationIds: true`:
const myShapeSelectorCreationId: FindElementSelector =
  '{E43D12C3-AD5A-4317-BC00-FDED287C0BE8}';

// pptx-generator will try to find the shape even if one of the given keys
// won't match any shape on the target slide:
const myShapeSelectorFallback: FindElementSelector = {
  creationId: '{E43D12C3-AD5A-4317-BC00-FDED287C0BE8}',
  name: 'Drum',
};

// Use this only if `useCreationIds: false`:
const myShapeSelectorName: FindElementSelector = 'Drum';

// Whenever `useCreationIds` was set to true, you need to replace slide numbers
// by `creationId`, too:
await pres.addSlide('shapes', 4167997312, (slide) => {
  // slide is now #1 of `SlideWithShapes.pptx`
  slide.addElement('shapes', 273148976, {
    creationId: '{E43D12C3-AD5A-4317-BC00-FDED287C0BE8}',
    name: 'Drum',
  });
  // 'Drum' is from #2 of `SlideWithShapes.pptx`, see __tests__ dir for an
  // example.
});

If you decide to use the creationId method, you are safe to add, remove and rearrange slides in your templates. It is also no problem to update shape names, and you also don't need to pay attention to unique shape names per slide.

Please note: PowerPoint is going to update a shape's creationId only in case the shape was copied & pasted on a slide with an already existing identical shape creationId. If you were copying a slide, each shape creationId will be copied, too. As a result, you have unique shape ids, but different slide creationIds. If you are now going to paste a shape an such a slide, a new creationId will be given to the pasted shape. As a result, slide ids are unique throughout a presentation, but shape ids are unique only on one slide.

Find and Modify Shapes

There are basically to ways to access a target shape on a slide:

  • slide.modifyElement(...) requires an existing shape on the current slide,
  • slide.addElement(...) adds a shape from another slide to the current slide.

Modifications can be applied to both in the same way:

import { modify, CmToDxa } from 'pptx-automizer';

pres.addSlide('shapes', 2, (slide) => {
  // This will only work if there is a shape called 'Drum'
  // on slide #2 of the template labelled 'shapes'.
  slide.modifyElement('Drum', [
    // You can use some of the builtin modifiers to edit a shape's xml:
    modify.setPosition({
      // set position from the left to 5 cm
      x: CmToDxa(5),
      // or use a number in DXA unit
      h: 5000000,
      w: 5000000,
    }),
    // Log your target xml into the console:
    modify.dump,
  ]);
});

pres.addSlide('shapes', 1, (slide) => {
  // This will import the 'Drum' shape from
  // slide #2 of the template labelled 'shapes'.
  slide.addElement('shapes', 2, 'Drum', [
    // add modifiers as seen in the example above
  ]);
});

Modify Text

You can select and import generic shapes from any loaded template. It is possible to update the containing text in several ways:

import { ModifyTextHelper } from 'pptx-automizer';

pres.addSlide('SlideWithImages.pptx', 1, (slide) => {
  // You can directly modify the child nodes of <p:sp>
  slide.addElement('shapes', 2, 'Arrow', (element) => {
    element.getElementsByTagName('a:t').item(0).firstChild.data =
      'Custom content';
  });

  // You might prefer a built-in function to set text:
  slide.addElement('shapes', 2, 'Arrow', [
    ModifyTextHelper.setText('This is my text'),
  ]);
});

pptx-automizer also provides a powerful helper to replace tagged text. You can use e.g. {{myTag}} on your slide and apply a modifier to insert dynamic text. Font style can be inherited from template or updated by the modifier.

import { modify } from 'pptx-automizer';

pres.addSlide('TextReplace.pptx', 1, (slide) => {
  slide.modifyElement(
    // This is the name of the target element on slide #1 of
    // 'TextReplace.pptx
    'replaceText',
    // This will look for a string `{{replace}}` inside the text
    // contents of 'replaceText' shape
    modify.replaceText([
      {
        replace: 'replace',
        by: {
          text: 'Apples',
        },
      },
    ]),
  );
});

Find out more about text replacement:

Modify Images

pptx-automizer can extract images from loaded .pptx template files and add to your output presentation. You can use shape modifiers (e.g. for size and position) on images, too. Additionally, it is possible to load external media files directly and update relation Target of an existing image. This works on both, existing or added images.

const automizer = new Automizer({
  // ...
  // Specify a directory to import external media files from:
  mediaDir: `path/to/media`,
});

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

pres.addSlide('images', 2, (slide) => {
  slide.modifyElement('imagePNG', [
    // Override the original media source of element 'imagePNG'
    // by an imported file:
    ModifyImageHelper.setRelationTarget('feather.png'),

    // You might need to update size
    ModifyShapeHelper.setPosition({
      w: CmToDxa(5),
      h: CmToDxa(3),
    }),
  ]);
});

Find more examples on image manipulation:

Modify Tables

You can use a PowerPoint table and add/modify data and style. It is also possible to add rows and columns and to style cells.

const pres = automizer
  .loadRoot(`RootTemplate.pptx`)
  .load(`SlideWithTables.pptx`, 'tables');

const result = await pres.addSlide('tables', 3, (slide) => {
  slide.modifyElement('TableWithEmptyCells', [
    modify.setTable({
      // Use an array of rows to insert data.
      // use `label` key for your information only
      body: [
        { label: 'item test r1', values: ['test1', 10, 16, 12, 11] },
        { label: 'item test r2', values: ['test2', 12, 18, 15, 12] },
        { label: 'item test r3', values: ['test3', 14, 12, 11, 14] },
      ],
    }),
  ]);
});

Find out more about formatting cells:

Modify Charts

All data and styles of a chart can be modified. Please note that if your template contains more data than your data object, Automizer will remove these extra nodes. Conversely, if you provide more data, new nodes will be cloned from the first existing one in the template.

// Modify an existing chart on an added slide.
pres.addSlide('charts', 2, (slide) => {
  slide.modifyElement('ColumnChart', [
    // Use an object like this to inject the new chart data.
    // Additional series and categories will be copied from
    // previous sibling.
    modify.setChartData({
      series: [
        { label: 'series 1' },
        { label: 'series 2' },
        { label: 'series 3' },
      ],
      categories: [
        { label: 'cat 2-1', values: [50, 50, 20] },
        { label: 'cat 2-2', values: [14, 50, 20] },
        { label: 'cat 2-3', values: [15, 50, 20] },
        { label: 'cat 2-4', values: [26, 50, 20] },
      ],
    }),
  ]);
});

Find out more about modifying charts:

Modify Extended Charts

If you need to modify extended chart types, such like waterfall or map charts, you need to use modify.setExtendedChartData.

// Add and modify a waterfall chart on slide.
pres.addSlide('charts', 2, (slide) => {
  slide.addElement('ChartWaterfall.pptx', 1, 'Waterfall 1', [
    modify.setExtendedChartData(<ChartData>{
      series: [{ label: 'series 1' }],
      categories: [
        { label: 'cat 2-1', values: [100] },
        { label: 'cat 2-3', values: [50] },
        { label: 'cat 2-4', values: [-40] },
        // ...
      ],
    }),
  ]);
});

Remove elements from a slide

You can as well remove elements from slides.

// Remove existing charts, images or shapes from added slide.
pres
  .addSlide('charts', 2, (slide) => {
    slide.removeElement('ColumnChart');
  })
  .addSlide('images', 2, (slide) => {
    slide.removeElement('imageJPG');
    slide.removeElement('Textfeld 5');
    slide.addElement('images', 2, 'imageJPG');
  });

Tipps and Tricks

Loop through the slides of a presentation

If you would like to modify elements in a single .pptx file, it is important to know that pptx-automizer is not able to directly "jump" to a shape to modify it.

This is how it works internally:

  • Load a root template to append slides to it
  • (Probably) load root template again to modify slides
  • Load other templates
  • Append a loaded slide to (probably truncated) root template
  • Modify the recently added slide
  • Write root template and appended slides as output presentation.

In case you need to apply modifications to the root template, you need to load it as a normal template:

import Automizer, {
  CmToDxa,
  ISlide,
  ModifyColorHelper,
  ModifyShapeHelper,
  ModifyTextHelper,
} from 'pptx-automizer';

const run = async () => {
  const automizer = new Automizer({
    templateDir: `path/to/pptx-templates`,
    outputDir: `path/to/pptx-output`,
    // this is required to start with no slides:
    removeExistingSlides: true,
  });

  let pres = automizer
    .loadRoot(`SlideWithShapes.pptx`)
    // We load it twice to make it available for modifying slides.
    // Defining a "name" as second params makes it a little easier
    .load(`SlideWithShapes.pptx`, 'myTemplate');

  // Get useful information about loaded templates:
  const myTemplates = await pres.getInfo();
  const mySlides = myTemplates.slidesByTemplate(`myTemplate`);

  // Feel free to create some functions to pre-define all modifications
  // you need to apply to your slides.
  type CallbackBySlideNumber = {
    slideNumber: number;
    callback: (slide: ISlide) => void;
  };
  const callbacks: CallbackBySlideNumber[] = [
    {
      slideNumber: 2,
      callback: (slide: ISlide) => {
        slide.modifyElement('Cloud', [
          ModifyTextHelper.setText('My content'),
          ModifyShapeHelper.setPosition({
            h: CmToDxa(5),
          }),
          ModifyColorHelper.solidFill({
            type: 'srgbClr',
            value: 'cccccc',
          }),
        ]);
      },
    },
  ];
  const getCallbacks = (slideNumber: number) => {
    return callbacks.find((callback) => callback.slideNumber === slideNumber)
      ?.callback;
  };

  // We can loop all slides an apply the callbacks if defined
  mySlides.forEach((mySlide) => {
    pres.addSlide('myTemplate', mySlide.number, getCallbacks(mySlide.number));
  });

  // This will result to an output presentation containing all slides of "SlideWithShapes.pptx"
  pres.write(`myOutputPresentation.pptx`).then((summary) => {
    console.log(summary);
  });
};

run().catch((error) => {
  console.error(error);
});

Quickly get all slide numbers of a template

When calling pres.getInfo(), it will gather information about all elements on all slides of all templates. In case you just want to loop through all slides of a certain template, you can use this shortcut:

const slideNumbers = await pres
  .getTemplate('myTemplate.pptx')
  .getAllSlideNumbers();

for (const slideNumber of slideNumbers) {
  // do the thing
}

Find all text elements on a slide

When processing an added slide, you might want to apply a modifier to any existing text element. Call slide.getAllTextElementIds() for this:

import Automizer, { modify } from 'pptx-automizer';

pres.addSlide('myTemplate.pptx', 1, async (slide) => {
  const elements = await slide.getAllTextElementIds();
  elements.forEach((element) => {
    // element has a text body:
    slide.modifyElement(element, [modify.setText('my text')]);
    // ... or use the tag replace function:
    slide.modifyElement(element, [
      modify.replaceText([
        {
          replace: 'TAG',
          by: {
            text: 'my tag text',
          },
        },
      ]),
    ]);
  });
});

Sort output slides

There are three ways to arrange slides in an output presentation.

  1. By default, all slides will be appended to the existing slides in your root template. The order of addSlide-calls will define slide sortation in output presentation.

  2. You can alternatively remove all existing slides by setting the removeExistingSlides flag to true. The first slide added with addSlide will be first slide in the output presentation. If you want to insert slides from root template, you need to load it a second time.

import Automizer from 'pptx-automizer';

const automizer = new Automizer({
  templateDir: `my/pptx/templates`,
  outputDir: `my/pptx/output`,

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

let pres = automizer
  .loadRoot(`RootTemplate.pptx`)
  // We load this twice to make it available for sorting slide
  .load(`RootTemplate.pptx`, 'root')
  .load(`SlideWithShapes.pptx`, 'shapes')
  .load(`SlideWithGraph.pptx`, 'graph');

pres
  .addSlide('root', 1) // First slide will be taken from root
  .addSlide('graph', 1)
  .addSlide('shapes', 1)
  .addSlide('root', 3) // Third slide from root will be appended
  .addSlide('root', 2); // Second and third slide will switch position

pres.write(`mySortedPresentation.pptx`).then((summary) => {
  console.log(summary);
});
  1. Use sortSlides-callback You can pass an array of numbers and create a callback and apply it to presentation.xml. This will also work without adding slides.

Slides will be appended to the existing slides by slide number (starting from 1). You may find irritating results in case you skip a slide number.

import ModifyPresentationHelper from './helper/modify-presentation-helper';

//
// You may truncate root template or you may not
// ...

// It is possible to skip adding slides, try sorting an unmodified presentation
pres
  .addSlide('charts', 1)
  .addSlide('charts', 2)
  .addSlide('images', 1)
  .addSlide('images', 2);

const order = [3, 2, 4, 1];
pres.modify(ModifyPresentationHelper.sortSlides(order));

Import and modify slide Masters

You can import, modify and use one or more slideMasters and the related slideLayouts. It is only supported to add and modify shapes on the underlying slideMaster, you cannot modify something on a slideLayout. This means, each modification on a slideMaster will appear on all related slideLayouts.

To specify the target index of the required slide master to import, you need to count slideMasters in your template presentation. To specify another slideLayout for an added output slide, you need to count slideLayouts in your output presentation

To add and modify shapes on a slide master, please take a look at Add and modify shapes.

If you require to modify slide master backgrounds, please refer to

// Import another slide master and all its slide layouts.
// Index 1 means, you want to import the first of all masters:
pres.addMaster('SlidesWithAdditionalMaster.pptx', 1, (master) => {
  // Modify a certain shape on the slide master:
  master.modifyElement(
    `MasterRectangle`,
    ModifyTextHelper.setText('my text on master'),
  );
  // Add a shape from an imported templated to the current slideMaster.
  master.addElement('SlideWithShapes.pptx', 1, 'Cloud 1');
});

Any imported slideMaster will be appended to the existing ones in the root template. If you have already e.g. one master with five layouts, and you import a new master coming with seven slide layouts, the first new layout will be #6.

// Import a slideMaster and its slideLayouts:
pres.addMaster('SlidesWithAdditionalMaster.pptx', 1);

// Add a slide and switch to another layout:
pres.addSlide('SlidesWithAdditionalMaster.pptx', 3, (slide) => {
  // use another master, e.g. the imported one from 'SlidesWithAdditionalMaster.pptx'
  // You need to pass the index of the desired layout after all
  // related layouts of all imported masters have been added to rootTemplate.
  slide.useSlideLayout(12);
});

// It is also possible to use the original slideLayout of any added slide:
pres.addSlide('SlidesWithAdditionalMaster.pptx', 3, (slide) => {
  // To use the original master from 'SlidesWithAdditionalMaster.pptx',
  // we can skip the argument:
  slide.useSlideLayout();
  // This will also auto-import the original slideMaster, if not done already,
  // and look for the created index of the source slideLayout.
});

Please notice: If your root template and your imported slides have an equal structure of slideMasters and slideLayouts, it won't be necessary to add slideMasters manually.

If you have trouble with messed up slideMasters, and if you don't worry about the impact on performance, you can try and set autoImportSlideMasters: true to always import all required files:

import Automizer from 'pptx-automizer';

const automizer = new Automizer({
  // ...

  // Always use the original slideMaster and slideLayout of any
  // imported slide:
  autoImportSlideMasters: true,
  // ...
});

Track status of automation process

When creating large presentations, you might want to have some information about the current status. Use a custom status tracker:

import Automizer, { StatusTracker } from 'pptx-automizer';

// If you want to track the steps of creation process,
// you can use a custom callback:
const myStatusTracker = (status: StatusTracker) => {
  console.log(status.info + ' (' + status.share + '%)');
};

const automizer = new Automizer({
  // ...
  statusTracker: myStatusTracker,
});

Create a new modifier

If the built-in modifiers of pptx-automizer are not sufficient to your task, you can access the target xml elements with xmldom. A modifier is a wrapper for such an operation.

Let's first take a look at a (simplified) existing modifier: ModifyTextHelper.content('This is my text').

// "setTextContent" is a function that returns a function.
// A "label" argument needs to be passed to "setTextContent".
const setTextContent = function (label: number | string) {
  // On setup, we can handle the argument.
  const newTextContent = String(label);

  // A new function is returned to apply the label at runtime.
  return function (shape: XmlElement) {
    // "shape" contains a modifiable xmldom object.
    // You can use a selector to find the required 'a:t' element:
    const textElement = shape.getElementsByTagName('a:t').item(0);

    // You can now apply the "newTextContent".
    if (textElement?.firstChild) {
      // Refer to xmldom for available functions.
      textElement.firstChild.textContent = newTextContent;
    }
    // It is possible to output the xml to console at any time.
    // XmlHelper.dump(element);
  };
};

This function will construct an anonymous callback function on setup, while the callback function itself will be executed on runtime, when it's up to the target element on a slide.

You can use the modifier e.g. on adding an element:

pres.addSlide('SlideWithShapes.pptx', 2, (slide) => {
  // This will import the 'Drum' shape
  slide.modifyElement('Cloud', [
    // 1. Dump the original xml:
    // Notice: don't call XmlHelper.dump, just pass it
    XmlHelper.dump,
    // 2. Apply modifier from the example above:
    setTextContent('New text'),
    XmlHelper.dump,
  ]);
});

We can wrap any xml modification by such a modifier. If you have a working example and you think it will be useful to others, you are very welcome to fork this repo and send a pull request or simply post it.

More examples

Take a look into tests-directory to see a lot of examples for several use cases, e.g.:

Troubleshooting

If you encounter problems when opening a .pptx-file modified by this library, you might worry about PowerPoint not giving any details about the error. It can be hard to find the cause, but there are some things you can check:

  • Broken relation: There are still unsupported shape types and pptx-automizer wil not copy required relations of those. You can inflate .pptx-output and check ppt/slides/_rels/slide[#].xml.rels-files to find possible missing files.
  • Unsupported media: You can also take a look at the ppt/media-directory of an inflated .pptx-file. If you discover any unusual file formats, remove or replace the files by one of the known types.
  • Broken animation: Pay attention to modified/removed shapes which are part of an animation. In case of doubt, (temporarily) remove all animations from your template. (see #78)
  • Proprietary/Binary contents (e.g. ThinkCell): Walk through all slides, slideMasters and slideLayouts and seek for hidden Objects. Hit ALT+F10 to toggle the sidebar.
  • Chart styles not working: If you try to change e.g. color or size of a chart data label, and it doesn't work as expected, try to remove all data labels and activate them again. If this does not help, try to give the first data label of a series a slightly different style (this creates a single data point).
  • Replace Text not working: Cut out your e.g. {CustomerName} tag from textbox to clipboard, paste it into a plaintext editor to remove all (visible and invisible) formatting. Copy & paste {CustomerName} back to the textbox. (see #82 and #73)
  • No related chart worksheet: It might happen to PowerPoint to lose the worksheet relation for a chart. If a chart gets corrupted by this, you will see a normal chart on your slide, but get an error message if you try to open the datasheet. Please replace the corrupted chart by a working one. (see #104)

If none of these could help, please don't hesitate to talk about it.

Testing

You can run all unit tests using these commands:

yarn test
yarn test-coverage

Special Thanks

This project was inspired by:

pptx-automizer's People

Contributors

dependabot[bot] avatar gpoussel avatar jarekw69 avatar jrking4 avatar mp70 avatar singerla avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

pptx-automizer's Issues

idea: positioning

Hi,

We know we can set positioning using this lib e.g

modify.setPosition({
      // set position from the left to 5 cm
      x: CmToDxa(5),
      // or use a number in DXA unit
      h: 5000000,
      w: 5000000,
    }),

But is there a way (using this lib) of getting existing position of elements, so we could move them e.g 1cm to the right (or whatever) programatically. I'm thinking, could this be added to getInfo, or we just add a helper;

getSizesAndPositions([elementIds]) which returns an object with the id,name, current position and dimensions or similar?

or slide.getAllElementPositions(), bonus points for us using internal helpers similar to the backend workings of my old slide.getAllTextElementIds() (or whatever it was) to tell the user if it is a textbox, image or chart (further support for element types added as we go, this could be a type ElementTypeIfKnown: "text" | "chart" ...

This would hook in with your recent support for replacing images with external files and so on, now we can move them in a relative rather than absolute way.

Thoughts?

Dynamically add rows and columns to a table / remove odd ones

It should be possible to add and remove table rows and/or columns. At the moment, you can only insert values directly like this.

It would also be useful to resize table rows and columns to the original dimensions. This will make it possible to use a table as seperated labels list besides a chart axis.

Match existing cells by address in a worksheet

It may happen to have cells e.g. in Column B without an existing cell in Column A. pptx-automizer stumbles across this, because it cannot match existing and new cells by cell address.
E.g. "B1" will be inserted as second cell, even if there is no value for A1, but an existing value for B1. B1 will exist twice, and excel complaining. Datasheets of charts will not open.

Set format of a chart series

It should be possible to set background color / border color etc. of an existing or added chart series.

It could look like this:

// Modify an existing chart on an added slide.
pres.addSlide('charts', 2, (slide) => {
  slide.modifyElement('ColumnChart', [
    // Use an object like this to inject the new chart data.
    // Assumed, "series 3" needs to be reformatted (e.g. because it was added dynamically and
    // looks exactly like "series 2"), we want to apply some modifications.
    modify.setChartData({
      series: [
        { label: 'series 1' },
        { label: 'series 2' },
        { label: 'series 3', modify: [
          ModifyChartHelper.setSeriesFormat({
            solidFill: {
              schemeClr: 'accent2'
            }
          })
        ]},
      ],
      categories: [
        { label: 'cat 2-1', values: [ 50, 50, 20 ] },
        { label: 'cat 2-2', values: [ 14, 50, 20 ] }
      ]
    })
  ])
})

This requires to apply a modifier to a certain series, which needs to be implemented. I consider this as a neccessary feature for convenient chart templating.

Questions about template application

I have a file as a master RootTemplate, using an already edited file as Test, but I find that the template setting doesn't take effect, either using addMaster or addSlide just adds a new page with the specified PPTX!

Output files are bigger than input

Hello! Really appreciate your work on this. One small thing: I'm finding that the output files are often quite large, larger than the initial file even when only retaining one slide. For example using inputdemo.pptx as an example input to the following code I'm getting a 1.5Mb output file which is ~50% bigger than the input file despite having removed 3 slides.

import Automizer from './index';

const automizer = new Automizer({
  templateDir: `./`,
  outputDir: `./`,
  removeExistingSlides: true,
});

const run = async () => {
  const pres = automizer
    .loadRoot(`inputdemo.pptx`)
    .load(`inputdemo.pptx`, 'ContentSlides')

  const result = await pres
    .addSlide('ContentSlides', 2, (slide) => {
      // Could modify here
    })
    .write(`outputdemo.pptx`);
};

run().catch((error) => {
  console.error(error);
});

Manually unzipping the output file it looks to have retained all three of the images from the four slides of the template file, then re-added the image which was used in the slide that was added back as content to the newly cleaned template.

Does this make sense? Is there anything I can do to work around this? Thanks again!

Types for setAxisRange are incorrect

Hi, thank you for a really great project.

I noticed that the modify.setAxisRange has a type error:
Type '(chart: XmlDocument) => void' is not assignable to type 'ShapeModificationCallback'

Changing the setAxisRange method from:

static setAxisRange =
  (range: ChartAxisRange) =>
    (chart: XmlDocument): void => {

to:

static setAxisRange =
  (range: ChartAxisRange) =>
    (element: XmlDocument | XmlElement, chart?: XmlDocument): void => {

seems to fix the error but I'm unsure if that is the correct approach.

TypeError: slide.useSlideLayout is not a function

Greetings, thank you for the great tool first, I know that you just uploaded new changes, but I already have a question about them.

pptx-automizer version: 0.3.0
env: Node v16.13.0 + TS

I want to combine several slides together into one slide deck, they have different layouts and I've seen that you've updated a version with the changes I'm looking for (hopefully).
I've tried to use the same way as in the example:

// Import a slideMaster and its slideLayouts:
pres.addMaster('SlidesWithAdditionalMaster.pptx', 1);

// It is also possible to use the original slideLayout of any added slide:
pres.addSlide('SlidesWithAdditionalMaster.pptx', 3, (slide) => {
  // To use the original master from 'SlidesWithAdditionalMaster.pptx',
  // we can skip the argument:
  slide.useSlideLayout();
  // This will also auto-import the original slideMaster, if not done already, 
  // and look for the created index of the source slideLayout.
});

But it seems like slide.useSlideLayout is missing in export, or I did something wrong.

Also, the same situation with :

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

Thanks in advance, looking forward to your reply!

PR for inserting arbitrary PNG

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 #60
  5. something else/out of scope of this lib.

Can't find element on slide 1 in test.pptx:

Good time of day.

I'm having trouble replacing text from a presentation template. The examples given in here work out correctly (RootTemplate.pptx and TextReplace.pptx). But as soon as I try to replace the text from my presentation (the {{test}} tag is specified on the first slide), nothing works:

slide.modifyElement(
 'replaceText',
  modify.replaceText(
  [{ replace: 'test', by: { text: 'work' } }],
  { openingTag: '{{', closingTag: '}}' }
 ))

As I understood from the error, the 'replaceText' selector does not find it.
Can you tell me how to set these selectors to text objects in your presentations?

Get number of slides in a presentation

Hello, I'm searching for how to get number of slides in a presentation.
I would like to add all slides from a loaded presentation in my root presentation.
Is there any way to do it with pptx-automizer ?
I searched in different attributes of my 'pres' object (like your example) but i didn't find anything :(

Could you help me ? Thanks a lot

Generate Node stream

Hi. Is there a way to generate a Node stream from an Automizer instance? For my project we want to direct output to stdout instead of creating a file

Charts an images are not copied to output presentation if placed on a slideLayout

It will result to corrupted pptx files if a slideMaster and related slideLayouts are (auto-) imported into an output presentation when there are images or charts on a slideLayout, but not on the corresponding slideMaster.

Currently, it is not supported to use images or charts on a slideLayout, but only on the underlying slideMaster.

Modify root presentation master

As already mentioned in #6, it should also be possible to edit the presentation master(s). As you can have different masters, it needs to have a selector as well. Syntax would be similar:

let pres = automizer.loadRoot(`RootTemplate.pptx`);

// We would also like to modify an existing slide master:
pres.modifySlideMaster(1, (slideMaster) => {
  // "Placeholder 1" has to be the name of an existing element on slideMaster #1 in RootTemplate.pptx
  slideMaster.modifyElement('Placeholder 1', [
    modify.setContent("My custom placeholder")
  ]);
});

This has a slightly higher effort, because pptx-automizer cannot handle slide masters at all so far.

Functionality Question

Thanks for putting this library together. I'm trying to do something fairly simple. I have an PPT template that I want to do some search/replace with tokenized strings. IE - {{NAME}} replace with a name. I see that I can do that when I add a slide, but I can't see how I do that with existing slides. When I load a PPT into automizer, it doesn't actually show the slides. But when I save it, the slides are there. I must be missing something. Ideally, I'd love to loop through each slide and perform modifications, but I just can't find a way to do this.

Modfying Text within a table

Are there any examples of modifying text within a table. Specifically, I'm looking to

  1. Change color to white
  2. Bold
  3. Italicize
  4. Change background color (I believe there ARE samples for this one)

I can see how to do that outside of a table, but couldn't find any examples inside.

Swap element in slide master?

I am trying to swap an element on roots; however, I am not understanding the syntax. From the example the addMaster function is used, but trying to run a similar code (because I don't have the presentations) I am getting the error. Can someone help with the syntax? Once I understand it I can add some new examples

Can't find element on slide 1 in root:
{
presName: 'root',
slideNumber: 1,
selector: 'MasterRectangle',
mode: 'modify',
callback: [Function (anonymous)]
}

Use xml-relationship-helper instead of static xml-helper methods

The new features for slideMaster handling have arrived with xml-relationship-helper.ts. It would be very welcome to replace the corresponding static method calls from xml-helper.ts by xml-relationship-helper.ts.

E.g. is parseRelationTarget(), part of both, xml-helper.ts and xml-relationship-helper.ts, but should be removed from the first step by step.

Add/Delete Series and Categories to Chart

Hello Thomas,

Thank you for the development, it's works fine !

Could you please add possibility to add and/or delete series and categories to an existing charts (and automatically recalculate data zone area) or have you already plan to do this evolution ?

Thank You
Luc

row.styles in tables

When adding new rows to a table using the ModifyTable class, it currently applies the styles of row 0 to the new rows (well actually an empty style object and this seems to be powerpoints default behaviour for that). For instance, if row 0 is a header with larger and bold text, and rows 1-3 have normal smaller text, calling modify with 10 rows results in rows 1-3 keeping their original text style, while rows 4-9 have the header text style.

Although it's possible to override this using the style object for each row, it requires the application to be aware of the table styling, instead of allowing the user to freely style it in the template file and have the styles respected.

It doesn't actually matter in my use of this library whatsoever but I did stumble upon it, and it seemed perhaps suboptimal.

To address this, we could introduce a "has header" toggle or a "repeatLastRowStyles" option that repeats last row style rather than empty row style. This would allow the user to choose how new rows should inherit styles from existing rows, providing a more flexible solution.

Something like this ? What do you think :)

setRows() {
  const lastRowIndex = this.data.body.length - 1;
  const lastRowStyles = this.data.body[lastRowIndex]?.styles || {};

  this.data.body.forEach((row: TableRow, r: number) => {
    row.values.forEach((cell: number | string, c: number) => {
      let rowStyles = {};

      if (row.styles && row.styles[c]) {
        rowStyles = row.styles[c];
      } else if (this.data.repeatLastRowStyles) {
        rowStyles = lastRowStyles[c] ? lastRowStyles[c] : {};
      } else {
        rowStyles = {};
      }

      this.table.modify(
        this.row(r, this.column(c, this.cell(cell, rowStyles))),
      );
      this.table.modify({
        'a16:rowId': {
          index: r,
          modify: ModifyXmlHelper.attribute('val', r),
        },
      });
    });
  });
}

Waterfall chart throws Zipped file not found: ppt/charts/chartNaN.xml

Hi, very cool library! Unfortunately, I encountered an issue that prevents me from using waterfall charts.

Stack trace

(node:4420) UnhandledPromiseRejectionWarning: Error: Zipped file not found: ppt/charts/chartNaN.xml
    at Function.<anonymous> (/node_modules/pptx-automizer/src/helper/file-helper.ts:56:13)
    at Generator.next (<anonymous>)
    at /node_modules/pptx-automizer/dist/helper/file-helper.js:8:71
    at new Promise (<anonymous>)
    at __awaiter (/node_modules/pptx-automizer/dist/helper/file-helper.js:4:12)
    at Function.zipCopy (/node_modules/pptx-automizer/dist/helper/file-helper.js:52:16)
    at Chart.<anonymous> (/node_modules/pptx-automizer/dist/shapes/chart.js:149:44)
    at Generator.next (<anonymous>)
    at /node_modules/pptx-automizer/dist/shapes/chart.js:8:71
    at new Promise (<anonymous>)

Steps to reproduce:

  1. create a new presentation called "root.xlsx" with one slide
  2. create a new presentation called "waterfall.xlsx", add a waterfall chart to it and call it "WATERFALL"
  3. try adding the slide from "waterfall" to "root"
  4. Error: Zipped file not found: ppt/charts/chartNaN.xml

Minimal code

const ppt = Automizer.loadRoot('root.pptx').load('waterfall.pptx', 'waterfall');

ppt.addSlide('waterfall', 1, (slide) => {
    slide.modifyElement('WATERFALL', [
        modify.setChartData({
            series: [{ label: 'Value' }],
            categories: [
                { label: 'Revenue', values: [500] },
                { label: 'Cost', values: [-100] },
                { label: 'Profit', values: [400] },
            ],
        }),
    ]);
}

const summary = await ppt.write('test.pptx');
console.log(summary);

System

macOS Monterey v12.6
PowerPoint for Mac v16.66 (22100900)

Can't use root template without any slide

As it seems, a root template must contain at least one slide to work properly.

Otherwise, an error occurs:

TypeError: Cannot read property 'appendChild' of undefined

  66 |
  67 |     const parent = element.parent(xml);
> 68 |     parent.appendChild(newElement);
     |            ^
  69 |
  70 |     XmlHelper.writeXmlToArchive(element.archive, element.file, xml);
  71 |

  at Function.<anonymous> (src/helper/xml-helper.ts:68:12)
  at fulfilled (src/helper/xml-helper.ts:5:58)

Unable to modify empty table

This is probably a fairly small issue, but with the exact sample code in the README, I receive an error when attempting to modify a completely empty table with two cells and no text. The text on the link says "Insert data into table with empty cells," so I assumed no data was needed. Once I add data, it works properly. Note: you have to add data to EVERY cell. If I have a 1 x 2 table and I want to add data to both columns, the template must have text in both in order to not receive an error. https://github.com/singerla/pptx-automizer#modify-tables

Here's the full stack if you want it:

TypeError: Cannot read properties of undefined (reading 'parentNode')
at XmlHelper.insertAfter (C:\Code\fortis-financial\node_modules\pptx-automizer\dist\helper\xml-helper.js:329:30)
at XmlElements.text (C:\Code\fortis-financial\node_modules\pptx-automizer\dist\helper\xml-elements.js:18:32)
at ModifyXmlHelper.createElement (C:\Code\fortis-financial\node_modules\pptx-automizer\dist\helper\modify-xml-helper.js:66:52)
at ModifyXmlHelper.assertElement (C:\Code\fortis-financial\node_modules\pptx-automizer\dist\helper\modify-xml-helper.js:46:22)
at ModifyXmlHelper.modify (C:\Code\fortis-financial\node_modules\pptx-automizer\dist\helper\modify-xml-helper.js:27:34)
at ModifyXmlHelper.modify (C:\Code\fortis-financial\node_modules\pptx-automizer\dist\helper\modify-xml-helper.js:39:22)
at ModifyXmlHelper.modify (C:\Code\fortis-financial\node_modules\pptx-automizer\dist\helper\modify-xml-helper.js:39:22)
at C:\Code\fortis-financial\node_modules\pptx-automizer\dist\modify\modify-table.js:56:28
at Array.forEach ()
at C:\Code\fortis-financial\node_modules\pptx-automizer\dist\modify\modify-table.js:54:24
at Array.forEach ()
at ModifyTable.setRows (C:\Code\fortis-financial\node_modules\pptx-automizer\dist\modify\modify-table.js:53:24)
at ModifyTable.modify (C:\Code\fortis-financial\node_modules\pptx-automizer\dist\modify\modify-table.js:46:14)
at C:\Code\fortis-financial\node_modules\pptx-automizer\dist\helper\modify-table-helper.js:9:14
at C:\Code\fortis-financial\node_modules\pptx-automizer\dist\classes\shape.js:119:21
at Array.forEach ()

Can't find *element* on slide

Hello Thomas, I'm searching for how to replace text values in a slide.
I think i understand how it works with pptx-automizer, i have a tag in PowerPoint for my text label (named prog).
I can find it in my elements Array but i have the same error again and again :
image
Do you have any solution ? Thanks a lot ๐Ÿ™

You can find my code here :

let pres = automizer
  .loadRoot(`deb.pptx`)
  .load(`mid.pptx`, "mid")
  .load(`end.pptx`, "end");

async function merge() {
  const ids = await pres.setCreationIds();

  const midSlides = ids.find((tpl) => tpl.name === "mid").slides;
  midSlides.sort((a, b) => a.number - b.number);

  const myTplt = midSlides[0];
  console.log(myTplt.elements);
  pres.addSlide("mid", myTplt.id, (slide) => {
    slide.modifyElement("prog", (e) => {
      //do something
    });
  });

  const endSlides = ids.find((tpl) => tpl.name === "end").slides;
  endSlides.sort((a, b) => a.number - b.number);

  endSlides.forEach((tpl) => {
    pres.addSlide("end", tpl.id);
  });

  pres.write(`myPresentation.pptx`).then((summary) => {});
}

merge();

Would you accept PRs for :

Awesome libary! Thank you.

I started off using setCreationIds to figure out how many slides we have in a template so I can loop through and add all slides, but, I have errors on PPTXs that aren't generated in powerpoint (keynote in this case), because they don't seem to add creationIds. I know this library technically only supports Powerpoint 2009, but I was wondering if you'd accept a PR that

1) lets setCreationIds fail more gracefully in this case, I was thinking throw a helpful error message?

e.g

const creationIdNode = slideXml.getElementsByTagName('p14:creationId').item(0);

   if (!creationIdNode) {
     // Handle the case where the creationIdNode is not found (e.g., log an error, skip the slide, or use a default value - do you have a preference? )
     continue;
   }

Currently it throws like this

TypeError: Cannot read properties of undefined (reading 'getAttribute')

2) Implements a caveman method of counting slides in a template, this is pseudo code but just as an idea..

async getSlideCountPerTemplate(): Promise<Map<string, number>> {
  const slideTemplates: Map<string, number> = new Map();
  const creationIds = await this.getCreationIds();

  for (const slideInfo of creationIds) {
    const layoutName = slideInfo.info.layoutName;
    slideTemplates.set(layoutName, (slideTemplates.get(layoutName) || 0) + 1);
  }

  return slideTemplates;
}

3) gives all text elements in a given slide for use with modify element replace text (if use doesn't know the element ID they can loop all text IDs)

Again pseudo code but ..

async listElementIdsForSlide(slideNumber: number): Promise<string[]> {
  const slideRelFile = `slides/slide${slideNumber}.xml`;
  const slideXml = await XmlHelper.getXmlFromArchive(this.archive, 'ppt/' + slideRelFile);

  if (!slideXml) {
    throw new Error(`Slide with number ${slideNumber} not found.`);
  }

  const shapeTree = slideXml.getElementsByTagName('p:spTree').item(0);
  const shapeNodes = shapeTree.getElementsByTagName('p:sp');
  const elementIds: string[] = [];

  for (let i = 0; i < shapeNodes.length; i++) {
    const shapeNode = shapeNodes.item(i);
    const txBody = shapeNode.getElementsByTagName('p:txBody').item(0);

    if (txBody) {
      const nonVisualProperties = shapeNode.getElementsByTagName('p:nvSpPr').item(0);
      if (nonVisualProperties) {
        const cNvPr = nonVisualProperties.getElementsByTagName('p:cNvPr').item(0);
        if (cNvPr) {
          const id = cNvPr.getAttribute('id');
          elementIds.push(id);
        }
      }
    }
  }

  return elementIds;
}

Tip: How To Loop Through Slides and/or Programatically Replace Text

It took me hours to figure this out so thought I'd share a quick workaround on how to:
i) loop through all the slides in a presentation
ii) replace all instances of the text you want to replace, without needed to know/hardcode the selector names

This won't work for text in tables though, if anyone has a tip on how to do that, I'd be happy to try.

   let variableData = [{variable: "First Name", value: "John"}, {variable: "Last Name", value: "Doe"}]
   let textReplacements = [];
   let templateName = 'test';

    variableData.forEach(async variable => {
        textReplacements.push({
            replace: variable.variable,
            by: {
                text: variable.value,
            },
        });
    });
    
    // Load Pptx Automizer
    const automizer = new Automizer({
        templateDir: templateDir,
        outputDir: outputDir,
        removeExistingSlides: true,
    });

    let pres = automizer.loadRoot(rootTemplate).load(extraTemplate, templateName);

    // Count the total number of slides
    const creationIds = await pres.setCreationIds();
    let totalSlides = 0;

    creationIds.forEach((template) => {
        totalSlides += template.slides.length;
    });

    console.log(`Processing ${totalSlides} slides!`);
        
    // Loop through all the slides
    for (let i = 1; i <= totalSlides; i++) {
        pres.addSlide(templateName, i, async slide => {
            let placeholders = await slide.getAllTextElementIds();    

            // Replace text for each placeholder programatically
            placeholders.forEach(async placeholder => {
                slide.modifyElement(
                    placeholder,
                    modify.replaceText(textReplacements),
                );
            });
        });
    }

    // Write the output file
    pres.write('Output.pptx').then((summary) => {
        console.log(summary);
    });

[Feat?] make slide masters/layouts editable.

I think this would be 'not too difficult' to add; we just need to work out how to present it to the user.. maybe slide.modifyMaster()?

I appreciate the same can be achieved by re adding the master pres.addMaster() that's already there but this seems really inefficient and requires user to have already looked up the slide master relationship.

This would enable us to change text or logos that are stored in the masters. For example if a user puts the customer name and logo in the master we can still automate that without requiring them to pull them out of the master.

What do you think?

Alongside this I'd also like to work on getting slide backgrounds something we can edit.

Modify root Presentation & accessing other properties

Hi Thomas,

Thank you for the previous evolutions. It works well !

Could you please add (and illustrate) the following features:

  • Modify elements in root presentation / root presentation master
  • Modify additional graph properties like activating (or not) Legend, title, X axis, ...
  • Modify cell format (e.g. specify format such as @,%, 990,00, ....) or to access to other properties (e.g. set the background color of serie X) ?

I think it would be great to be able to change elements in the root Presentation, for example, for specifying dynamically a Title, Date of report, ... (*)

Thank you Thomas for the work
Have a nice day
Luc

(*) Actually, the only possibility to do that consists in unzipping the pptx, replacing some elements and then zipping again...

Modify additional graph properties

It should be possible to modify graph properties, such as

  • activating (or not) Legend
  • graph title
  • axis range
  • plot area dimensions
  • ...

This could be a good entrypoint for contributions, as it will only require a few experience in JS. Each modification mentioned above will result in a tiny and reusable modifier function.

I am going to publish some examples and a contribution guide about "create your own modifier".

Use pptx-automizer along with from-scratch-libraries (e.g. PptxGenJS)

The template based approach of pptx-automizer requires all contents of a presentation to be present in one or more template pptx files. While most other pptx automation libraries will write files from scratch, it is required to define contents programmatically there.

I was wondering if it is probably a good idea to use pptx-automizer along with e.g. PptxGenJS and combine the best of two worlds.

It could look like:

import pptxgen from "pptxgenjs";
import Automizer from 'pptx-automizer';

const automizer = new Automizer({
  // ... your config
});

// Tell automizer to use another library.
// Typing could be done similar to vuejs InjectionKey
automizer.use('pptxgen', new pptxgen())

let textboxText = "Hello World from PptxGenJS!";
let textboxOpts = { x: 1, y: 1, color: "363636" };

let pres = automizer
  .loadRoot(`RootTemplate.pptx`)
  .load(`MyTemplate.pptx`);

await pres
  .addSlide('MyTemplate.pptx', 1, (slide) => {
      // add a new textbox with the power of pptxgen
      slide.use('pptxgen').addText(textboxText, textboxOpts);
      
      // or modify an existing shape
      slide.modifyElement('Drum', [
        modify.setPosition({ x: 1000000, h: 5000000, w: 5000000 }),
      ]);
  })

Any ideas on this?

Modify elements in root presentation

It should be possible to select and modify elements in the root presentation. This could also allow workflows without loading additional templates.

It could look like this:

let pres = automizer.loadRoot(`RootTemplate.pptx`);

// we can already addSlide(), but we would also like to modify an existing slide:
pres.modifySlide(1, (slide) => {
  // "Drum" has to be the name of an existing element on slide #1 in RootTemplate.pptx
  slide.modifyElement('Drum', [
    modify.setPosition({x: 1000000})
  ]);
  // We can access all the other functions as well
  slide.addElement('shapes', 2, 'Arrow');
});

modify.replaceText not working properly

Aside from that, I'm having a weird issue where modify isn't working. I think this should work, but the modify() doesn't actually replace the text.

let pres = automizer.loadRoot("review-template.pptx");
pres.load("review-template.pptx", "base");
const templates = await pres.getInfo();
const slides = templates.slidesByTemplate("base");

slides.forEach((slide) => {
    pres.addSlide("base", slide.number, async(slide) => {
        const elements = await slide.getAllTextElementIds();

        elements.forEach(element => {

            slide.modifyElement(element, () => {
                modify.replaceText([
                    {
                        replace: 'NAME',
                        by: {
                            text: client.displayName
                        }
                    }]
                )
            })
        })
    });
});

await pres.write("myPresentation.pptx").then((summary) => {
    console.log(summary);
});

Originally posted by @chriscap-fd in #71 (comment)

Import Masters and Layouts from pptx templates

It should be possible to import slideMasters and slideLayouts from pptx templates and use them across added slides.
It could be done like this:

const result2 = await automizer
  // Import another slide master and all its slide layouts:
  .addMaster('SlidesWithAdditionalMaster.pptx', 1, 'myMaster#1')
  // Import a slide master and use the original naming from ppt:
  .addMaster('SlidesWithAdditionalMaster.pptx', 2)

  // Add a slide (which might require an imported master):
  .addSlide('SlidesWithAdditionalMaster.pptx', 3, (slide) => {
    // use another master, e.g. the imported one from 'SlidesWithAdditionalMaster.pptx'
    slide.useMaster('myMaster#2');
  })

  // Add a slide and use a master by original name from ppt:
  .addSlide('SlidesWithAdditionalMaster.pptx', 2, (slide) => {
    // find an imported master/layout by name
    slide.useMaster('Orange Design', 'Leer');
  })
  .write(outputName2);

All animations are removed whenever an element is removed

Hello,

I've noticed that whenever I want to remove an element that has an animation on it, Powerpoint makes me repair the pptx and remove all of the animations in the slide.
Do I do something wrong here? Is there already a workaround for this issue?
Otherwise, would it be possible to reorganize the animations while removing the element?

I have this issue with this code for example:

const title = (pres: Automizer, title?: string, subtitle?: string) => {
  pres.addSlide('title.pptx', 1, (slide) => {
    if (title)
      slide.modifyElement('title', [
        modify.replaceText([{ replace: 'title', by: { title } }]),
      ])
    if (subtitle)
      slide.modifyElement('subtitle', [
        modify.replaceText([{ replace: 'subtitle', by: { subtitle } }]),
      ])

    if (!title) slide.removeElement('title')
    if (!subtitle) slide.removeElement('subtitle')
  })
}

My temporary solution would be to have a slightly different template for each time I have to remove an element, but it's not handy nor maintainable.

setCreationIds (and therefore things like .getInfo) Errors uncleanly on files with no creationIDs set

Even with useCreationIds: false,

Can we make it cleaner by either

  • A) not running if useCreationIds is false
  • B) catch the error nicely
  • C) Is it at all possible to just names in case of useCreationIds: false? (I'm looking at my getTextIds helper and that is what I did there.
    -- added debug here
  async getCreationIds(): Promise<SlideInfo[]> {
    const archive = this.archive;
    const relationships = await XmlHelper.getTargetsByRelationshipType(
      archive,
      this.path,
      this.relType,
    );
  
    const creationIds: SlideInfo[] = [];
    for (const slideRel of relationships) {
      try {
        const slideXml = await XmlHelper.getXmlFromArchive(
          archive,
          'ppt/' + slideRel.file,
        );
        
        if (!slideXml) {
          console.warn(`slideXml is undefined for file ${slideRel.file}`);
          continue;
        }
  
        const creationIdItem = slideXml.getElementsByTagName('p14:creationId').item(0);
  
        if (!creationIdItem) {
          console.warn(`No 'p14:creationId' tag found in ${slideRel.file}`);
          continue;
        }
  
        const creationIdSlide = creationIdItem.getAttribute('val');
  
        if (!creationIdSlide) {
          console.warn(`Attribute 'val' is undefined in ${slideRel.file}`);
          continue;
        }
  
        const elementIds = this.elementCreationIds(slideXml, archive);
        const slideInfo = await this.getSlideInfo(
          slideXml,
          archive,
          slideRel.file,
        );
  
        creationIds.push({
          id: Number(creationIdSlide),
          number: this.parseSlideRelFile(slideRel.file),
          elements: elementIds,
          info: slideInfo,
        });
      } catch (err) {
        console.error(`An error occurred while processing ${slideRel.file}:`, err);
      }
    }
  
    return creationIds.sort((slideA, slideB) => 
      slideA.number < slideB.number ? -1 : 1
    );
  }

Old error

 TypeError: Cannot read properties of undefined (reading 'getAttribute')

New error

 console.warn
      No 'p14:creationId' tag found in slides/slide1.xml

      47 |   
      48 |         if (!creationIdItem) {
    > 49 |           console.warn(`No 'p14:creationId' tag found in ${slideRel.file}`);
         |                   ^
      50 |           continue;
      51 |         }
      52 |   

Repro steps. -- chuck in a getInfo or setCreationIds into the get-all-text-elements test file

Adding slides at specific position

Adding slides at specific position would be a great addition to the library. Without it someone still has to go through the presentation and reorder slides. Of course, this needs to be done only if the order is important. In my use case it is.

Ideal final result of the merge

staticSlide1 ("root.pptx")
staticSlide2 ("root.pptx")
staticSlide3 ("root.pptx")
dynamicSlide1 (pptx-automizer)
dynamicSlide2 (pptx-automizer)
dynamicSlide3 (pptx-automizer)
staticSlide4 ("root.pptx")
staticSlide5 ("root.pptx")
staticSlide6 ("root.pptx")

[Question] Getting element's text.

Hi.

First of all, thanks a lot for the library, I think it's really good.

I wonder if you guys know a way of getting the current text of an element?

For example:

This is a callback fnt for editing existing slides:

const updateSlide = async (slide) => { const elements = await slide.getAllTextElementIds(); elements.forEach((element, index) => { console.log(index); if (index === 4) { slide.modifyElement(element, [ModifyTextHelper.setText("My content")]); } }); };

I need to get the element text and compare if it's equal to something, I will need to update that text, at the moment, I am only able to edit existing elements but I can't find a way to validate which of the elements in the slide is the one that I need to update.

Unfortunately, I cannot use replace because my original files do not have the {{}}

Thanks a lot.

"replaceText" not working

Environment - node.js version v21.0.0
OS - Mac

The package is very useful. Thanks for open-sourcing it.

I am trying to use the software to generate PPTX file from a template file. The "ReplaceText" function is not working as expected.

I tried to debug the code. But could not find any method that gives me text in an element.

Please find attached sample code I am using for my testing.
agreement-ppt.zip

Modify chart not working when there are non-visible empty rows in datasheet xml

In case pptx-automizer is throwing this error:

Error: Index 0 not found at "c"
   at ModifyXmlHelper.assertNode...

This is because Excel will keep "invisible" empty rows in datasheet xml after you may have deleted rows.

    <row r="2" spans="1:10" x14ac:dyDescent="0.25">
      <c r="A2" t="s">
        <v>
          2
        </v>
      </c>
      <c r="B2" s="1">
        <v>
          0
        </v>
      </c>
    </row>
    <row r="3" spans="1:10" customFormat="1" x14ac:dyDescent="0.25"/>

As automizer will only replicate what is already there, those rows are missing slots for columns at <row r="3" />, which is causing the error.

This issue is related to #4 and will be fixed by implementing a fallback function to ensure existence of a certain node before it is cloned.

'Could not find file ppt/slides/slide{x}.xml'

Hi,

I'm also noticing an issue with I think maybe cleanup, on making two different sets of files, if the first file has 5 slides from input files A,B,C , then I run the code again on input files D,E,C and this has 4 slides I get
STARTING PRESWRTE Appending slide 2 (33%) Appending slide 3 (67%) Appending slide 4 (100%) Error detail: Could not find file Stack trace: Error: Could not find file at ArchiveJszip.<anonymous> (/node_modules/pptx-automizer/dist/helper/archive/archive-jszip.js) at Generator.next (<anonymous>) at /node_modules/pptx-automizer/dist/helper/archive/archive-jszip.js at new Promise (<anonymous>) at __awaiter (/node_modules/pptx-automizer/dist/helper/archive/archive-jszip.js) at ArchiveJszip.read (/node_modules/pptx-automizer/dist/helper/archive/archive-jszip.js) at ArchiveJszip.<anonymous> (/node_modules/pptx-automizer/dist/helper/archive/archive-jszip.js) at Generator.next (<anonymous>) at /node_modules/pptx-automizer/dist/helper/archive/archive-jszip.js at new Promise (<anonymous>)
I am using all cleanup functions available, and at the end removing both input and output pptxs from /tmp where we save them for processing, and setting the autonomiser and pres instances to null; any ideas would be massively appreciated.

0.39 > 0.41

Hi,

I was just upgrading from .39>.41 and noticed this syntax is now invalid;
slide.modifyElement( elementId, modify.replaceText(replacements, { openingTag: "{", closingTag: "}", }) ); } });

and we now must
slide.modifyElement( elementId, () => { return modify.replaceText(replacements, { openingTag: "{", closingTag: "}", }); } ); . Is this right and should we update docs to reflect it?

Pres/Slide Dimensions

Hi,

Hope, you're well. I was thinking of adding a method to get dimensions of a slide, right now I've popped it on the slide.getDimensions() but maybe this can go inside getInfo().slidenumber > .info eventually?

Would you like it in your lib?

If yes:
Does this feel like the right order of precedence for what dimensions override each-other? My memory says yes but I'd need to read the spec to confirm.
What do you want to see in a PR? Is this fine (with tidy up obviously, and initing the source archive in the function) more or less as is or?

MP70@d0e028c

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.