OpenpackagingUtils.java

package pro.verron.officestamper.utils.openpackaging;

import org.docx4j.openpackaging.contenttype.ContentType;
import org.docx4j.openpackaging.contenttype.ContentTypeManager;
import org.docx4j.openpackaging.contenttype.ContentTypes;
import org.docx4j.openpackaging.exceptions.Docx4JException;
import org.docx4j.openpackaging.exceptions.InvalidFormatException;
import org.docx4j.openpackaging.exceptions.PartUnrecognisedException;
import org.docx4j.openpackaging.packages.PresentationMLPackage;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.openpackaging.parts.XmlPart;
import org.docx4j.openpackaging.parts.relationships.Namespaces;
import org.docx4j.wml.R;
import org.w3c.dom.Document;
import pro.verron.officestamper.utils.UtilsException;
import pro.verron.officestamper.utils.image.ImageRunOptions;
import pro.verron.officestamper.utils.svg.SvgUtils;
import pro.verron.officestamper.utils.wml.WmlFactory;

import java.io.InputStream;
import java.io.OutputStream;
import java.util.function.Supplier;

/// Utility class for working with Open Packaging documents. This class provides methods to load and export Word
/// documents using DOCX4J
public class OpenpackagingUtils {
    private OpenpackagingUtils() {
        throw new UtilsException("Utility class shouldn't be instantiated");
    }

    /// Loads a Word document from the provided input stream.
    ///
    /// @param is the input stream containing the Word document data
    /// @return a WordprocessingMLPackage representing the loaded document
    /// @throws UtilsException if there is an error loading the document
    public static WordprocessingMLPackage loadWord(InputStream is) {
        try {
            return WordprocessingMLPackage.load(is);
        } catch (Docx4JException e) {
            throw new UtilsException(e);
        }
    }

    /// Exports a Word document to the provided output stream.
    ///
    /// @param wordprocessingMLPackage the Word document to export
    /// @param os the output stream to write the document to
    /// @throws UtilsException if there is an error exporting the document
    public static void exportWord(WordprocessingMLPackage wordprocessingMLPackage, OutputStream os) {
        try {
            wordprocessingMLPackage.save(os);
        } catch (Docx4JException e) {
            throw new UtilsException(e);
        }
    }

    /// Loads a PowerPoint document from the provided input stream.
    ///
    /// @param is the input stream containing the PowerPoint document data
    /// @return a PresentationMLPackage representing the loaded document
    /// @throws UtilsException if there is an error loading the document
    public static PresentationMLPackage loadPowerPoint(InputStream is) {
        try {
            return PresentationMLPackage.load(is);
        } catch (Docx4JException e) {
            throw new UtilsException(e);
        }
    }

    /// Exports a PowerPoint document to the provided output stream.
    ///
    /// @param presentationMLPackage the PowerPoint document to export
    /// @param os the output stream to write the document to
    /// @throws UtilsException if there is an error exporting the document
    public static void exportPowerPoint(PresentationMLPackage presentationMLPackage, OutputStream os) {
        try {
            presentationMLPackage.save(os);
        } catch (Docx4JException e) {
            throw new UtilsException(e);
        }
    }

    /// Creates a new run element containing an image, which is embedded in a [WordprocessingMLPackage] document.
    ///
    /// @param openPackage       the open package of the Word document, providing access to the document model
    ///                          and its sections; typically used to generate the appropriate relationships
    ///                          and content.
    /// @param bytes             a supplier of the byte array representing the image data to be embedded
    ///                          in the document.
    /// @param imageRunOptions   options for the image run, including alternate text, filename hints,
    ///                          maximum width, and deduplication preferences.
    /// @return a [R] (run) object representing the created image run, which can be added to the
    ///         document content.
    /// @throws UtilsException   if there is an error during the creation of the image part, such as
    ///                          an invalid image format or issues with the document model.
    public static R newImageRun(
            OpenPackage<WordprocessingMLPackage> openPackage,
            Supplier<byte[]> bytes,
            ImageRunOptions imageRunOptions
    ) {
        try {
            var document = openPackage.document();
            var documentModel = document.getDocumentModel();
            var sections = documentModel.getSections();
            var lastSection = sections.getLast();
            var pageDimension = lastSection.getPageDimensions();
            var imgPart = openPackage.findOrCreateImgPart(bytes, imageRunOptions.deduplicate());
            var relationship = imgPart.relationship();
            var imgFormat = imgPart.format();
            var dimension = imgFormat.dimension();
            var format = imgFormat.name();
            var scale = WmlFactory.computeScale(pageDimension,
                    imageRunOptions.maxWidth() == null ? -1 : imageRunOptions.maxWidth(),
                    dimension);
            var inline = format.equals("svg")
                    ? WmlFactory.newSVGInline(relationship,
                    imageRunOptions.filenameHint(),
                    imageRunOptions.altText(),
                    scale)
                    : WmlFactory.newImgInline(relationship,
                            imageRunOptions.filenameHint(),
                            imageRunOptions.altText(),
                            scale);
            var drawing = WmlFactory.newDrawing(inline);
            return WmlFactory.newRun(drawing);
        } catch (Exception e) {
            throw new UtilsException("Failed to create an ImagePart", e);
        }
    }

    /// Extracts the SVG XML content from the provided byte array by parsing it into a document and
    /// creating an SVG part using the specified ContentTypeManager.
    ///
    /// @param bytes the byte array containing the SVG content to be parsed
    /// @param ctm the content type manager used to manage the creation of the SVG part
    /// @return the extracted XML content as a String
    /// @throws RuntimeException if an error occurs during the processing or extraction of the XML content
    public static String extractSvgXml(byte[] bytes, ContentTypeManager ctm) {
        var newDocument = SvgUtils.parseDocument(bytes);
        var svgPart = createSvgPart(ctm, newDocument, "/temporary");
        return extractXml(svgPart);
    }

    /// Creates a new SVG part with the specified document, content type manager, and part name.
    ///
    /// @param contentTypeManager the content type manager used to create the SVG part
    /// @param document the XML document to associate with the new SVG part
    /// @param partName the name to assign to the new SVG part
    /// @return the created SVG part
    /// @throws UtilsException if an error occurs during part creation or initialization
    public static XmlPart createSvgPart(ContentTypeManager contentTypeManager, Document document, String partName) {
        XmlPart part;
        try {
            part = (XmlPart) contentTypeManager.newPartForContentType(ContentTypes.IMAGE_SVG, partName, null);
        } catch (InvalidFormatException | PartUnrecognisedException e) {
            throw new UtilsException(e);
        }
        part.setRelationshipType(Namespaces.IMAGE);
        part.setContentType(new ContentType(ContentTypes.IMAGE_SVG));
        part.setDocument(document);
        return part;
    }

    /// Extracts the XML content from the specified XmlPart.
    ///
    /// @param xmlPart the XmlPart from which to extract the XML content
    /// @return the extracted XML content as a String
    /// @throws RuntimeException if an error occurs while retrieving the XML content
    public static String extractXml(XmlPart xmlPart) {
        String existingXml;
        try {
            existingXml = xmlPart.getXML();
        } catch (Docx4JException e) {
            throw new RuntimeException(e);
        }
        return existingXml;
    }
}