PDF export?


#1

Juce doesn’t seem to have a printing system.

Cross platform printing is AFAIK tricky - but a pdf export would fill most practical needs and wouldn’t need to involve platform specific stuff.

I figure the LowLevelGraphicsPostScriptRenderer could be used as a building block but I’m not sure.

I’m willing to spend some time on this if you can use my help and provide some hints.

In case you wonder why I want this… well, nothing serious, it’s just that I’d like to ditch Micrsoft Publisher for creating concert programs, and perhaps do some note typesetting. But I figure PDF export could be of general use.


#2

It’d certainly be nice to have, but probably involves an awful lot of coding. The way to do it would be to do write a renderer, just like the postscript one (although I don’t think there’d be anything in there that’s reusable)


#3

I googled http://libharu.org/ and changed the Postscript context to use it.

I can now create PDF documents with pahts and text rendered with solid brushes, I haven’t found out how to deal with images and it seems gradients aren’t well supported, the Postscript context doesn’t implement them either.

The header file:

[code]#ifndef JUCE_PDFWRITER_JUCEHEADER
#define JUCE_PDFWRITER_JUCEHEADER

#include “hpdf.h”

BEGIN_JUCE_NAMESPACE

#include “src/gui/graphics/contexts/juce_LowLevelGraphicsContext.h”

class JUCE_API PrintingContext : public LowLevelGraphicsContext
{
public:
enum Orientation
{
Portrait,
Landscape
};

enum PageSize
{
	PageSize_Letter,	//   8.5 x 11 "         612 x 792
	PageSize_Legal,     //   8.5 x 14 "         612 x 1008
	PageSize_A3,        //   297 x 420 mm    841.89 x 1199.551
	PageSize_A4,        //   210 x 297 mm   595.276 x 841.89
	PageSize_A5,        //   148 x 210 mm   419.528 x 595.276
	PageSize_B4,        //   250 x 353 mm   708.661 x 1000.63
	PageSize_B5,        //   176 x 250 mm   498.898 x 708.661
	PageSize_Executive, //  7.25 x 10.5 "       522 x 756
	PageSize_US4x6,     //     4 x 6 "          288 x 432
	PageSize_US4x8,     //     4 x 8 "          288 x 576
	PageSize_US5x7,     //     5 x 7 "          360 x 504
	PageSize_COMM10     // 4.125 x 9.5 "        297 x 684
};

virtual void beginPrinting() = 0;
virtual void endPrinting() = 0;
virtual Graphics *beginPage(PageSize pageSize_, Orientation orientation_) = 0;
virtual void endPage() = 0;

juce_UseDebuggingNewOperator

};

//==============================================================================
/**
An implementation of LowLevelGraphicsContext that turns the drawing operations
into a PDF document.

*/
class JUCE_API PDFWriter : public PrintingContext
{
public:
//==============================================================================
PDFWriter (OutputStream &out_, int dpi_, const bool colour_ = true);

~PDFWriter();

//==============================================================================
virtual void beginPrinting();
virtual void endPrinting();
virtual Graphics *beginPage(PageSize pageSize_, Orientation orientation_);
virtual void endPage();

bool isVectorDevice() const;
void setOrigin (int x, int y);

bool reduceClipRegion (int x, int y, int w, int h);
bool reduceClipRegion (const RectangleList& clipRegion);
void excludeClipRegion (int x, int y, int w, int h);

void saveState();
void restoreState();

bool clipRegionIntersects (int x, int y, int w, int h);
const Rectangle getClipBounds() const;
bool isClipEmpty() const;

//==============================================================================
void fillRectWithColour (int x, int y, int w, int h, const Colour& colour, const bool replaceExistingContents);
void fillRectWithGradient (int x, int y, int w, int h, const ColourGradient& gradient);

void fillPathWithColour (const Path& path, const AffineTransform& transform, const Colour& colour, EdgeTable::OversamplingLevel quality);
void fillPathWithGradient (const Path& path, const AffineTransform& transform, const ColourGradient& gradient, EdgeTable::OversamplingLevel quality);
void fillPathWithImage (const Path& path, const AffineTransform& transform,
                        const Image& image, int imageX, int imageY, float alpha, EdgeTable::OversamplingLevel quality);

void fillAlphaChannelWithColour (const Image& alphaImage, int imageX, int imageY, const Colour& colour);
void fillAlphaChannelWithGradient (const Image& alphaImage, int imageX, int imageY, const ColourGradient& gradient);
void fillAlphaChannelWithImage (const Image& alphaImage, int alphaImageX, int alphaImageY,
                                const Image& fillerImage, int fillerImageX, int fillerImageY, float alpha);

//==============================================================================
void blendImage (const Image& sourceImage, int destX, int destY, int destW, int destH,
                 int sourceX, int sourceY, float alpha);

void blendImageRescaling (const Image& sourceImage, int destX, int destY, int destW, int destH,
                          int sourceX, int sourceY, int sourceW, int sourceH,
                          float alpha, const Graphics::ResamplingQuality quality);

void blendImageWarping (const Image& sourceImage, int srcClipX, int srcClipY, int srcClipW, int srcClipH,
                        const AffineTransform& transform,
                        float alpha, const Graphics::ResamplingQuality quality);

//==============================================================================
void drawLine (double x1, double y1, double x2, double y2, const Colour& colour);

void drawVerticalLine (const int x, double top, double bottom, const Colour& col);
void drawHorizontalLine (const int x, double top, double bottom, const Colour& col);

//==============================================================================
juce_UseDebuggingNewOperator

protected:
//==============================================================================

RectangleList* clip;
const int dpi;
const bool colour;
OutputStream &out;
Graphics *currentGraphics;
bool needToClip;
Colour lastColour;

struct SavedState
{
    SavedState (RectangleList* const clip, const int xOffset, const int yOffset);
    ~SavedState();

    RectangleList* clip;
    const int xOffset, yOffset;

private:
    SavedState (const SavedState&);
    const SavedState& operator= (const SavedState&);
};

OwnedArray <SavedState> stateStack;

void pageSetRGBFill(const Colour &colour);
void pageRectangle(float x, float y, float w, float h);
void pageMoveTo(float x1, float y1);
void pageCubicTo(float x1, float y1, float x2, float y2, float x3, float y3);
void pageQuadraticTo(float x1, float y1, float x2, float y2);
void pageLineTo(float x1, float y1);
void pagePath(const Path &path);
void pageFill();
void pageClip();
void pageDrawImage (const Image& im, const int sx, const int sy, const int maxW, const int maxH) const;

PDFWriter (const PDFWriter& other);
const PDFWriter& operator= (const PDFWriter&);

HPDF_Doc pdf;
HPDF_Page page;

};

END_JUCE_NAMESPACE

#endif // JUCE_PDFWRITER_JUCEHEADER
[/code]
and the implementation file:

#include "src/core/juce_StandardHeader.h"

#include "juce_PDFWriter.h"

BEGIN_JUCE_NAMESPACE

#include "src/gui/graphics/contexts/juce_EdgeTable.h"
#include "src/gui/graphics/imaging/juce_Image.h"
#include "src/gui/graphics/colour/juce_PixelFormats.h"
#include "src/gui/graphics/geometry/juce_PathStrokeType.h"
#include "src/gui/graphics/geometry/juce_Rectangle.h"
#include "src/containers/juce_SparseSet.h"

#if JUCE_MSVC
  #pragma warning (disable: 4996) // deprecated sprintf warning
#endif


// this will throw an assertion if you try to draw something that's not
// possible in PDF
#define WARN_ABOUT_NON_PDF_OPERATIONS 0


//==============================================================================
#if defined (JUCE_DEBUG) && WARN_ABOUT_NON_PDF_OPERATIONS
 #define notPossibleInPDFAssert jassertfalse
#else
 #define notPossibleInPDFAssert
#endif

//==============================================================================
PDFWriter::PDFWriter (OutputStream &out_, const int dpi_, const bool colour_)
    : dpi (dpi_),
	  colour (colour_),
      needToClip (true),
	  clip(NULL),
	  currentGraphics(NULL),
	  page(NULL),
	  out(out_),
	  pdf(NULL)
{
}

PDFWriter::~PDFWriter()
{
    delete clip;
	delete currentGraphics;
	if(pdf) HPDF_Free(pdf);
}

void PDFWriter::beginPrinting()
{
	if (pdf)
	{
		jassertfalse // already printing
	}

	pdf = HPDF_New(NULL, NULL); //TODO: supply error handler
	if (!pdf)
	{
		jassertfalse // could not create PDF handle
	}
	
	/* set compression mode */
	HPDF_SetCompressionMode (pdf, HPDF_COMP_ALL);
	 
	/* set page mode to use outlines. */
	HPDF_SetPageMode (pdf, HPDF_PAGE_MODE_USE_OUTLINE);
}

void PDFWriter::endPrinting()
{
	if (!pdf)
	{
		jassertfalse // not printing
	}

	if (page)
	{
		jassertfalse // page not finished
	}

	//TODO: use the Juce OutputStream directly if possible

	const int bufsize = 0x100;
	HPDF_SaveToStream (pdf);
	HPDF_ResetStream (pdf); /* rewind the stream. */
	for (;;) /* copy the stream to out */
	{
		HPDF_BYTE buf[bufsize];
		HPDF_UINT32 siz = bufsize;
		HPDF_STATUS ret = HPDF_ReadFromStream (pdf, buf, &siz);
		if (ret != HPDF_OK && ret != HPDF_STREAM_EOF || siz == 0)
			break;
		out.write(buf, siz);
	}
	HPDF_Free(pdf);
	pdf = NULL;
}

Graphics *PDFWriter::beginPage(PageSize pageSize, Orientation orientation)
{
	if (page)
	{
		jassertfalse // missing endPage()
	}

	page = HPDF_AddPage(pdf);
	HPDF_Page_SetSize(page, (HPDF_PageSizes)pageSize, (HPDF_PageDirection)orientation);
	HPDF_REAL width = HPDF_Page_GetWidth(page);
	HPDF_REAL height = HPDF_Page_GetHeight(page);
    clip = new RectangleList (Rectangle (0, 0, (int)width, (int)height));
	HPDF_Page_Concat(page, 1, 0, 0, -1, 0, height);
	return currentGraphics = new Graphics(this);
}

void PDFWriter::endPage()
{
	if (!page)
	{
		jassertfalse // missing endPage()
	}

	page = NULL;
	delete currentGraphics;
	currentGraphics = NULL;
	delete clip;
	clip = NULL;
}

//==============================================================================
bool PDFWriter::isVectorDevice() const
{
    return true;
}

void PDFWriter::setOrigin (int x, int y)
{
    if (x != 0 || y != 0)
    {
		HPDF_Page_Concat(page, 1, 0, (HPDF_REAL)x, 0, 1, (HPDF_REAL)y);
        needToClip = true;
    }
}

bool PDFWriter::reduceClipRegion (int x, int y, int w, int h)
{
    needToClip = true;
    return clip->clipTo (Rectangle (x, y, w, h));
}

bool PDFWriter::reduceClipRegion (const RectangleList& clipRegion)
{
    needToClip = true;
    return clip->clipTo (clipRegion);
}

void PDFWriter::excludeClipRegion (int x, int y, int w, int h)
{
    needToClip = true;
    clip->subtract (Rectangle (x, y, w, h));
}

bool PDFWriter::clipRegionIntersects (int x, int y, int w, int h)
{
    return clip->intersectsRectangle (Rectangle (x, y, w, h));
}

const Rectangle PDFWriter::getClipBounds() const
{
    return clip->getBounds();
}

bool PDFWriter::isClipEmpty() const
{
    return clip->isEmpty();
}

//==============================================================================
PDFWriter::SavedState::SavedState (RectangleList* const clip_,
                                                            const int xOffset_, const int yOffset_)
    : clip (clip_),
      xOffset (xOffset_),
      yOffset (yOffset_)
{
}

PDFWriter::SavedState::~SavedState()
{
    delete clip;
}

void PDFWriter::saveState()
{
    stateStack.add (new SavedState (new RectangleList (*clip), 0, 0));
	HPDF_Page_GSave(page);
}

void PDFWriter::restoreState()
{
    SavedState* const top = stateStack.getLast();

    if (top != 0)
    {
        clip->swapWith (*top->clip);
        stateStack.removeLast();
        needToClip = true;
		HPDF_Page_GRestore(page);
    }
    else
    {
        jassertfalse // trying to pop with an empty stack!
    }
}

//==============================================================================
void PDFWriter::pageClip()
{
	// TODO: this should probably create a path of clipping rectangles
	// and use the HPDF_Page_clip () function

	if (needToClip)
    {
        needToClip = false;

        for (RectangleList::Iterator i (*clip); i.next();)
        {
            const Rectangle& r = *i.getRectangle();

//            out << r.getX() << ' ' << -r.getY() << ' '
//                << r.getWidth() << ' ' << -r.getHeight() << " pr ";
        }

//        out << "endclip\n";
    }
}

void PDFWriter::pageSetRGBFill (const Colour& colour)
{
    Colour c (Colours::white.overlaidWith (colour));
	HPDF_Page_SetRGBFill (page, c.getFloatRed(), c.getFloatGreen(), c.getFloatBlue());
}

void PDFWriter::pagePath (const Path& path)
{
    Path::Iterator i (path);

    while (i.next())
    {
        switch (i.elementType)
        {
        case Path::Iterator::startNewSubPath:
            HPDF_Page_MoveTo(page, i.x1, i.y1);
            break;

        case Path::Iterator::lineTo:
            HPDF_Page_LineTo(page, i.x1, i.y1);
            break;

        case Path::Iterator::quadraticTo:
			HPDF_Page_CurveTo2(page, i.x1, i.y1, i.x2, i.y2);
            break;

        case Path::Iterator::cubicTo:
			HPDF_Page_CurveTo(page, i.x1, i.y1, i.x2, i.y2, i.x3, i.y3);
            break;

        case Path::Iterator::closePath:
			HPDF_Page_ClosePath(page);
            break;

        default:
            jassertfalse
            break;
        }
    }
}

void PDFWriter::fillRectWithColour (int x, int y, int w, int h, const Colour& colour, const bool /*replaceExistingContents*/)
{
    pageClip ();
	pageSetRGBFill (colour);
	pageRectangle ((float)(x), (float)(y), (float)w, (float)h);
	pageFill ();
}

void PDFWriter::fillRectWithGradient (int x, int y, int w, int h, const ColourGradient& gradient)
{
    Path p;
    p.addRectangle ((float) x, (float) y, (float) w, (float) h);

    fillPathWithGradient (p, AffineTransform::identity, gradient, EdgeTable::Oversampling_256times);
}

//==============================================================================
void PDFWriter::fillPathWithColour (const Path& path, const AffineTransform& t,
                                                             const Colour& colour, EdgeTable::OversamplingLevel /*quality*/)
{
    pageClip();
    Path p (path);
	p.applyTransform(t);
	pageSetRGBFill (colour);
    pagePath (p);
	pageFill();
}

void PDFWriter::fillPathWithGradient (const Path& path, const AffineTransform& t, const ColourGradient& gradient, EdgeTable::OversamplingLevel /*quality*/)
{
	pageClip ();

	// this doesn't work correctly yet - it could be improved to handle solid gradients, but
    // PDF can't do semi-transparent ones.
    notPossibleInPDFAssert   // you can disable this warning by setting the WARN_ABOUT_NON_PDF_OPERATIONS flag at the top of this file

	int numColours = 256;
    PixelARGB* const colours = gradient.createLookupTable (numColours);

    for (int i = numColours; --i >= 0;)
        colours[i].unpremultiply();

    const Rectangle bounds (clip->getBounds());
    pageSetRGBFill (Colour (colours [numColours / 2].getARGB()));
    juce_free (colours);

	pagePath (path);
	pageFill ();
}

void PDFWriter::fillPathWithImage (const Path& path, const AffineTransform& transform,
                                                            const Image& sourceImage,
                                                            int imageX, int imageY,
                                                            float opacity, EdgeTable::OversamplingLevel /*quality*/)
{
    pageClip ();
    Path p (path);
	p.applyTransform(transform);
    pagePath (p);
    blendImage (sourceImage, imageX, imageY, sourceImage.getWidth(), sourceImage.getHeight(), 0, 0, opacity);
}


//==============================================================================
void PDFWriter::fillAlphaChannelWithColour (const Image& /*clipImage*/, int x, int y, const Colour& colour)
{
    pageClip();
    notPossibleInPDFAssert   // you can disable this warning by setting the WARN_ABOUT_NON_PDF_OPERATIONS flag at the top of this file
}

void PDFWriter::fillAlphaChannelWithGradient (const Image& /*alphaChannelImage*/, int imageX, int imageY, const ColourGradient& /*gradient*/)
{
    pageClip();
    notPossibleInPDFAssert   // you can disable this warning by setting the WARN_ABOUT_NON_PDF_OPERATIONS flag at the top of this file
}

void PDFWriter::fillAlphaChannelWithImage (const Image& /*alphaImage*/, int alphaImageX, int alphaImageY,
                                                                    const Image& /*fillerImage*/, int fillerImageX, int fillerImageY, float /*opacity*/)
{
    pageClip();
    notPossibleInPDFAssert   // you can disable this warning by setting the WARN_ABOUT_NON_PDF_OPERATIONS flag at the top of this file
}


//==============================================================================
void PDFWriter::blendImageRescaling (const Image& sourceImage,
                                                              int dx, int dy, int dw, int dh,
                                                              int sx, int sy, int sw, int sh,
                                                              float alpha,
                                                              const Graphics::ResamplingQuality quality)
{
    if (sw > 0 && sh > 0)
    {
        jassert (sx >= 0 && sx + sw <= sourceImage.getWidth());
        jassert (sy >= 0 && sy + sh <= sourceImage.getHeight());

        if (sw == dw && sh == dh)
        {
            blendImage (sourceImage,
                        dx, dy, dw, dh,
                        sx, sy, alpha);
        }
        else
        {
            blendImageWarping (sourceImage,
                               sx, sy, sw, sh,
                               AffineTransform::scale (dw / (float) sw,
                                                       dh / (float) sh)
                                   .translated ((float) (dx - sx),
                                                (float) (dy - sy)),
                               alpha,
                               quality);
        }
    }
}

//==============================================================================
void PDFWriter::blendImage (const Image& sourceImage, int dx, int dy, int dw, int dh, int sx, int sy, float opacity)
{
    blendImageWarping (sourceImage,
                       sx, sy, dw, dh,
                       AffineTransform::translation ((float) dx, (float) dy),
                       opacity, Graphics::highResamplingQuality);
}

//==============================================================================
void PDFWriter::pageDrawImage (const Image& im, const int sx, const int sy, const int maxW, const int maxH) const
{
//    out << "{<\n";

    const int w = jmin (maxW, im.getWidth());
    const int h = jmin (maxH, im.getHeight());

    int charsOnLine = 0;
    int lineStride, pixelStride;
    const uint8* data = im.lockPixelDataReadOnly (0, 0, w, h, lineStride, pixelStride);

    Colour pixel;

    for (int y = h; --y >= 0;)
    {
        for (int x = 0; x < w; ++x)
        {
            const uint8* pixelData = data + lineStride * y + pixelStride * x;

            if (x >= sx && y >= sy)
            {
                if (im.isARGB())
                {
                    PixelARGB p (*(const PixelARGB*) pixelData);
                    p.unpremultiply();
                    pixel = Colours::white.overlaidWith (Colour (p.getARGB()));
                }
                else if (im.isRGB())
                {
                    pixel = Colour (((const PixelRGB*) pixelData)->getARGB());
                }
                else
                {
                    pixel = Colour ((uint8) 0, (uint8) 0, (uint8) 0, *pixelData);
                }
            }
            else
            {
                pixel = Colours::transparentWhite;
            }

            char colourString [16];
            sprintf (colourString, "%x%x%x", pixel.getRed(), pixel.getGreen(), pixel.getBlue());

//            out << (const char*) colourString;
            charsOnLine += 3;

            if (charsOnLine > 100)
            {
//                out << '\n';
                charsOnLine = 0;
            }
        }
    }

    im.releasePixelDataReadOnly (data);

//    out << "\n>}\n";
}

void PDFWriter::blendImageWarping (const Image& sourceImage,
                                                            int srcClipX, int srcClipY,
                                                            int srcClipW, int srcClipH,
                                                            const AffineTransform& t,
                                                            float /*opacity*/,
                                                            const Graphics::ResamplingQuality /*quality*/)
{
	const int w = jmin (sourceImage.getWidth(), srcClipX + srcClipW);
	const int h = jmin (sourceImage.getHeight(), srcClipY + srcClipH);
	pageClip();
	RectangleList imageClip;
	sourceImage.createSolidAreaMask (imageClip, 0.5f);
	imageClip.clipTo (Rectangle (srcClipX, srcClipY, srcClipW, srcClipH));

    for (RectangleList::Iterator i (imageClip); i.next();)
    {
        const Rectangle& r = *i.getRectangle();

//        out << r.getX() << ' ' << r.getY() << ' ' << r.getWidth() << ' ' << r.getHeight() << " pr ";
    }

//    out << " clip newpath\n";

//    out << w << ' ' << h << " scale\n";
//    out << w << ' ' << h << " 8 [" << w << " 0 0 -" << h << ' ' << (int) 0 << ' ' << h << " ]\n";
	
	pageDrawImage (sourceImage, srcClipX, srcClipY, srcClipW, srcClipH);

//    out << "false 3 colorimage grestore\n";
    needToClip = true;
}


//==============================================================================
void PDFWriter::drawLine (double x1, double y1, double x2, double y2, const Colour& colour)
{
    Path p;
    p.addLineSegment ((float) x1, (float) y1, (float) x2, (float) y2, 1.0f);

    fillPathWithColour (p, AffineTransform::identity, colour, EdgeTable::Oversampling_256times);
}

void PDFWriter::drawVerticalLine (const int x, double top, double bottom, const Colour& col)
{
    drawLine (x, top, x, bottom, col);
}

void PDFWriter::drawHorizontalLine (const int y, double left, double right, const Colour& col)
{
    drawLine (left, y, right, y, col);
}

void PDFWriter::pageRectangle(float x, float y, float w, float h)
{
    Path p;
    p.startNewSubPath (x, y);
	p.lineTo(x + w, y);
	p.lineTo(x + w, y + h);
	p.lineTo(x, y + h);
	p.closeSubPath();
	pagePath(p);
}

void PDFWriter::pageFill()
{
	HPDF_Page_Fill(page);
}

END_JUCE_NAMESPACE

I don’t have a C++ sample of how to use it, I tested it from PILS.


#4

the only problem with the actual LowLevelGraphicsContext interface is that you will not be able to add text to your pdf, since the LowLevelGraphicsContext doesn’t know anything about font and text output ( i think this is a major drawback of the juce graphics system).

i had written some time ago a OpenGL LowLevel context, but sinc i wasn’t able to actually draw text in the juce style with it i gave up, as my juce Components were displayed without text at all, so they were useless !

shouldn’t be the time to actually make the Context interface so powerful so we can actually fully implement it and have our applications to actually be fully drawn (text, images, primitives) anywhere (opengl, software, gdi, pdf, …) ?


#5

Kraken,

I guess you’ve seen that post.
By that time, I had made a patch, but I don’t know if it still apply now to the code base.
IIRC, the patch added a drawText method to the LLRenderer, and that indeed, called the native font routine to print. In that I’ve intercepted all text drawing operations and hooked them in the LLRenderer.

There are still issues, as all other rendering primitives are still made to a bitmap and then later sent to the printer.
So the sequence:
Draw background / DrawText / DrawTransparentStuffOverTheText
is seen on the printer as:
Draw background / DrawTransparentStuffOverTheText / DrawText.

For a printer, it doesn’t really matter (there is no transparency), but for a OGL context it might.

If you want the code, please send a PM with your mail address, I’ll send it to you (it too big to post here).