Skip to main content
Skip table of contents

Custom transforms

Introduction

Mako’s ITransforms class offers a powerful yet convenient method of applying common operations to DOM objects such as brushes, colors, colorspaces, glyphs, or paths. They can operate on individual nodes or entire trees of objects, such as a complete page.

There are many built-in transforms in Mako for:

  • Image downsampling (IImageDownsamplerTransform)

  • Color conversion (IColorConverterTransform)

  • Color simplification (IComplexColorSimplifierTransform)

  • Image merging (IImageMergerTransform)

  • Remove non-visible Optional Content (IOptionalContentFixerTransformPtr)

Transforms simplify complex operations to be applied to a number of DOM objects in one go, and despite their name, transforms don't have to make a change; they can be used to simply interrogate document content.

A custom transform enables a Mako SDK developer to take advantage of the ITransforms framework for their own purposes.

Custom transform header

A framework to enable development of custom transforms is provided as a header, customtransform.h

CPP
#include "jawsmako/customtransform.h"

A custom transform implements callbacks for the DOM objects listed below. All you need to do is override the cases you are interested in, providing one or more methods for actually doing the work.

For example, let's say you wanted to find out information for every image in a document. You could write a custom transform to override transformImageBrush(). Your code will be called every time an imagebrush (IDOMImageBrush) is encountered, and from there obtain the image (IDOMImage) and its frame (IDOMFrame) to obtain all the information you need (size, resolution, colorspace, and so forth). You would run the transform on a page and repeat for all pages in a document.

A more likely scenario is one where you want to change the object in some way. Your implementation can do this, returning the updated object or a NULL if you want it removed from the DOM tree. The step-by-step example below demonstrates this.

IDOM object

Description

IDOM object

Description

IAnnotationAppearance

Annotation appearance

IDOMForm

The children of this node type comprise the contents of a PDF/PS style form. (XForm)

IDOMColor

Color

IDOMFormInstance

A node whose content must be an IDOMForm

IDOMColorSpace

Colorspace

IDOMBrush

The parent class for the many brush types. Brushes are used to fill paths (that is, a defined geometric region on the page)

IDOMImage

Base class describing an image

IDOMSolidColorBrush

A solid color brush is used to fill a path with a solid color

IDOMNode

Base class for the many DOM node types

IDOMGradientBrush

The parent class for linear and radial gradient brushes

IDOMFixedPage

The contents of a page

IDOMLinearGradientBrush

A linear gradient brush is used to specify a gradient along a vector

IDOMGroup

A DOM node representing Group Elements. A group of IDOMNodes that share common properties such as a clipping path or render transform

IDOMRadialGradientBrush

A radial gradient brush defines an ellipse to be filled with a gradient

IDOMCharPathGroup

A DOM node representing Group Elements that consist of stroked text

IDOMVisualBrush

A visual brush is used to fill a path with a vector drawing

IDOMTransparencyGroup

A DOM node representing Group Elements that share common transparency settings. Analogous to PDF Transparency Groups.

IDOMImageBrush

An image brush is used to fill a path with an image

IDOMCanvas

A canvas is a special form of an isolated, non-knockout, normal blended transparency group. It groups elements of a page together.

IDOMTilingPatternBrush

A tiling pattern brush is used to fill a path with a PS-style tiling pattern.

IDOMGlyphs

Glyphs nodes are used to represent a run of uniformly formatted text from a single font

IDOMShadingPatternBrush

A shading pattern brush is used to fill a defined geometric region with a PS-style shading pattern.

IDOMFont

Font base class

IDOMSoftMaskBrush

A soft mask brush provides a way of representing a PDF-style soft mask in its entirety. It will contain a suitable IDOMTransparency group as well as the necessary soft mask details

IDOMPathNode

A path node specifies geometry that can be filled with a brush

IDOMMaskedBrush

A masked brush describes a generalization of a masked image

IDOMVisualRoot

Special node type used by XPS for the root node inside a visual brush (normally tiling patterns)

IDOMNullBrush

A null brush

CTransformState

Class for tracking the graphics state leading to the point where a transform is applied.

Consider for example the IImageDownsamplerTransform described in the API documentation (paste the full name into the Search box). In order to determine how to downsample an image, the transform needs to know how large the image will eventually be. The CTransformState provides this information by providing the combined transform that applies to the image based on the RenderTransforms of all the nodes entered leading to the point where the image is actually encountered.

Other transforms need access to other information, such as the approximate clip area, the current group colorspace, the renderingIntent (if present), the current antialiasing mode (edge mode) and/or how a brush is used.

CTransformState::transformPriv

This member is used to track extra information needed by your transform process. Use it as you wish.

Implementing a custom transform

This GitHub example of a custom transform implementation converts glyphs to paths.

Note that:

  • You need only to implement transforms for those objects you are interested in examining or changing

  • Returning the default genericImplementation allows the transform to continue, possibly recursing further into the object

  • The object you return will replace the object being transformed (so if it's the same object, it will have no effect)

  • Returning a null or empty object will result in that object being removed from the tree

In the example, a path is returned which replaces the glyph object in the tree (that is, the glyph object is removed, and the path added).

Creating and calling the transform

It is instantiated by the calling program:

Creating the transform instance
CPP
		// Create a transform to convert glyphs to paths
		GlyphTransformImplementation implementation(jawsMako, strokeWidth, solidBrush);
		ITransformPtr outliner = ICustomTransform::create(jawsMako, &implementation, abort, true, true, true, true, true, true);

Once the instance exists, there is no need to create it again. Just call it as required. In the example, it is called for each page:

Calling the transform
CPP
        // Apply the transform to every page
        for (uint32 pageNum = 0; pageNum < pageCount; pageNum++)
        {
			IPagePtr page = document->getPage(pageNum);
        	IDOMFixedPagePtr fixedPage = page->edit();

			// Convert any glyphs
			bool result = false;
			outliner->transform(fixedPage, result);
        }

One advantage of custom transforms is that they can hide away complexity. Remember that you can implement callbacks for as many objects types as you wish, in a single transform.

Object sharing

In general, if the custom transform callback has a “changed” parameter, then the object being passed in is not shared and can be edited at will, providing you set the “changed” result appropriately.

If the custom transform callback does not, then the object is potentially shared, in which case you must make a clone (with the clone() method) before editing it. The internal transformation machinery detects changes if a different object is returned.

What are the Booleans in the call to the transform?

They control caching behavior. Depending on the nature of the transform, it may not be necessary to process the same transform repeatedly. Setting one or more of these values to false may speed up a custom transform by allowing Mako to return a reference to a cached copy. Each relates to the properties of various object types, seen here in this excerpt from customtransform.h:

Custom transform call parameters
CPP
static JAWSMAKO_API ICustomTransformPtr create(const IJawsMakoPtr &jawsMako, 
                                                IImplementation *implementation,
                                                const IAbortPtr &abort = IAbortPtr (),
                                                bool dependsOnClipBounds = true,
                                                bool dependsOnGroupSpace = true,
                                                bool dependsOnRenderingIntent = true,
                                                bool dependsOnTransform = true,
                                                bool dependsOnBrushUsage = true,
                                                bool dependsOnEdgeMode = true,
                                                bool dependsOnUncoloredTilingBrush = true);

We recommend you experiment with these to determine their applicability to your implementation.

Step-by-step

To help you get started writing your own custom transform, the following is a worked example that shows you how to write a custom transform that changes the color of every character in a PDF, in this case to change 100% black to 50%.

The implementation

A custom transform requires an implementation class. The minimum is a constructor and a method to override the default method, for a given object type.

  • The constructor is a useful place to pass in variables that will be needed, such as the Mako instance pointer

  • To know what the signature for the transform method you want to write looks like, simply look in customtransform.h. Search for the object type (in this case glyphs) then copy that method’s code to get you started.

Here is the complete implementation, and the corresponding header

Click here to expand...
CPP
/* ---------------------------------------------------------------------- -
 *  <copyright file="TextTransformImplementation.cpp" company="Global Graphics Software Ltd">
 *      Copyright (c) 2024 Global Graphics Software Ltd. All rights reserved.
 *  </copyright>
 *  <summary>
 *  This example is provided on an "as is" basis and without warranty of any kind.
 *  Global Graphics Software Ltd. does not warrant or make any representations
 *  regarding the use or results of use of this example.
 *  </summary>
 * -----------------------------------------------------------------------
 */

#include <iostream>
#include "TextTransformImplementation.h"

TextTransformImplementation::TextTransformImplementation(const IJawsMakoPtr& jawsMako, const float& textInkValue) :
    m_mako(jawsMako), m_textInkValue(textInkValue)
{
}

IDOMNodePtr TextTransformImplementation::transformGlyphs(IImplementation* genericImplementation,
                                                         const IDOMGlyphsPtr& glyphs, bool& changed,
                                                         const CTransformState& state)
{
    try
    {
        const auto fill = glyphs->getFill();
        if (fill->getBrushType() != IDOMBrush::eSolidColor)
            return genericImplementation->transformGlyphs(nullptr, glyphs, changed, state);

        if (edlobj2IDOMSolidColorBrush(fill)->getColor()->getColorSpace()->equals(IDOMColorSpaceDeviceCMYK::create(m_mako)) &&
            edlobj2IDOMSolidColorBrush(fill)->getColor()->getComponentValue(3) == 1.0f)
        {
            glyphs->setFill(IDOMSolidColorBrush::createSolidCmyk(m_mako, 0.0f, 0.0f, 0.0f, m_textInkValue));
            changed = true;
            return glyphs;
        }
    }
    catch (IError& e)
    {
        const String errorFormatString = getEDLErrorString(e.getErrorCode());
        std::wcerr << L"Exception thrown: " << e.getErrorDescription(errorFormatString) << std::endl;
    }
    catch (std::exception& e)
    {
        std::wcerr << L"std::exception thrown: " << e.what() << std::endl;
    }
    return genericImplementation->transformGlyphs(nullptr, glyphs, changed, state);
}
CPP
/* ---------------------------------------------------------------------- -
 *  <copyright file="TextTransformImplementation.h" company="Global Graphics Software Ltd">
 *      Copyright (c) 2024 Global Graphics Software Ltd. All rights reserved.
 *  </copyright>
 *  <summary>
 *  This example is provided on an "as is" basis and without warranty of any kind.
 *  Global Graphics Software Ltd. does not warrant or make any representations
 *  regarding the use or results of use of this example.
 *  </summary>
 * -----------------------------------------------------------------------
 */
#pragma once

#include "jawsmako/customtransform.h"

using namespace JawsMako;
using namespace EDL;

class TextTransformImplementation : public ICustomTransform::IImplementation
{
public:
    TextTransformImplementation(const IJawsMakoPtr& jawsMako, const float& textInkValue);
    IDOMNodePtr transformGlyphs(IImplementation* genericImplementation, const IDOMGlyphsPtr& glyphs, bool& changed, const CTransformState& state) override;

private:
    IJawsMakoPtr m_mako;
    float m_textInkValue;
};

This code snippet instantiates the class, then calls the transform for each page of a PDF.

CPP
// Initialize Mako
const IJawsMakoPtr mako = initializeMako();

// Load the source file.
const IDocumentAssemblyPtr assembly = loadDocument(mako, inputFile.c_str());

TextTransformImplementation implementation(mako, inkValue);
const ITransformPtr textModifier = ICustomTransform::create(mako, &implementation, IAbortPtr(), true, true, true, true, true, true);

const auto documentPtr = assembly->getDocument();

const auto pageCount = documentPtr->getNumPages();

std::wcout << L"Processing " << pageCount << " pages..." << std::endl;

for (uint32 pageIndex = 0; pageIndex < pageCount; pageIndex++)
{
    // Modify the page
    const auto page = documentPtr->getPage(pageIndex);
    textModifier->transformPage(page);
    page->release();
}

std::wcout << std::endl << L"Writing output file ..." << std::endl;

const IPDFOutputPtr pdfOutput = IPDFOutput::create(mako);
pdfOutput->writeAssembly(assembly, outputFile.c_str());

Transform chains

You may need to call several transforms in sequence to achieve a particular result. Mako provides a mechanism for this, ITransformChain. To use it, create an instance, then add the required transforms with pushTransform() or pushTransformFront(). The eventual order will be the order in which the transforms are called.

CPP
// The sequence of transforms to process a page
ITransformChainPtr transformChain = ITransformChain::create(jawsMako);

// Form unpacker
transformChain->pushTransform(IFormUnpackerTransform::create(jawsMako));

// Color converter
IColorConverterTransformPtr colorConverter = IColorConverterTransform::create(jawsMako);
colorConverter->setTargetSpace(IDOMColorSpaceDeviceCMYK::create(jawsMako));
colorConverter->setIgnoreRenderedImages(true);
transformChain->pushTransform(colorConverter);

Subsequently, use the transform chain on the required objects as you would a single transform.

CPP
transformChain->transformPage(page);

JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.