PGraphics not exporting to PDF

I have a sketch that I am trying to export to PDF so it can be plotted. I can export to SVG, but it doesn’t preserve any of the underlying vectors graphics, only exports embedded images so it isn’t much use for this application.

Here is the code snippets in question. Full code in a response post below.

I define the size with P2D
size(1300, 850, P2D);

This utility to export to PDF

if(makePDF){
  docName = timeStamp(docBase);
  beginRecord(PDF, "pdf/" + docName + ".pdf");
}

This to define the PGraphics
PGraphics img = createGraphics(imgSize, imgSize);

With these defined I get the expected visual results:

but the following error when I attempt PDF export:

ClassCastException: class sun.java2d.SunGraphics2D cannot be cast to class java.awt.Image (sun.java2d.SunGraphics2D and java.awt.Image are in module java.desktop of loader 'bootstrap')
ClassCastException: class sun.java2d.SunGraphics2D cannot be cast to class java.awt.Image (sun.java2d.SunGraphics2D and java.awt.Image are in module java.desktop of loader 'bootstrap')

If I change the PDF function to the following, the visual is still correct, and the PDF executes without error but the resulting file is blank.

if(makePDF){
  docName = timeStamp(docBase);
  beginRaw(PDF, "pdf/" + docName + ".pdf");
} 

When I change the PGraphics definition to:
PGraphics img = createGraphics(imgSize, imgSize, P2D);

I get this visual result which exports to PDF but obviously useless.

So I am at a loss for how to proceed and could use any guidance the community has to offer. Unfortunately, I’ve had to use PGraphics because of the mask feature (arcs clipped by underlying voronoi cell shape), but if there is another way to accomplish the same effect and will export properly, I’m open to that. But for now, I’m hoping to solve for the PDF export.

import toxi.math.conversion.*;
import toxi.geom.*;
import toxi.math.*;
import toxi.geom.mesh2d.*;
import toxi.util.datatypes.*;
import toxi.util.events.*;
import toxi.geom.mesh.subdiv.*;
import toxi.geom.mesh.*;
import toxi.math.waves.*;
import toxi.util.*;
import toxi.math.noise.*;
import toxi.processing.*;

ToxiclibsSupport gfx;
Voronoi voronoi;

import processing.svg.*;
boolean makeSVG = false;
import processing.pdf.*;
boolean makePDF = false;
String docBase = "voronoi_arcs";
String docName = "";

color bg = color(243, 242, 234);
color[] colorArray = {
  color(39, 225, 193),
  color(14, 162, 147),
  color(47, 15, 93),
  color(65, 42, 98),
  color(107, 36, 95)
};

ArrayList<PVector> pointsList;
ArrayList<Cell> voronCells;
float cellSpace = 100;


void setup() {
  size(1300, 850, P2D);
  background(bg);
  
  gfx = new ToxiclibsSupport(this);
  
  // make a list of the points, evenly distributed around the canvas
  pointsList = new ArrayList<PVector>();
  pointsList = poissonDiskSampling(cellSpace, 30);
  voronCells = new ArrayList<Cell>();
  voronoi = new Voronoi();
  makeVoronoi();
   
  smooth(8);
  //noLoop();
}

void draw() {
  background(bg);
  
  if(makeSVG){
    docName = timeStamp(docBase);
    beginRecord(SVG, "svg/" + docName + ".svg");
  }  else{ docName = ""; }
  if(makePDF){
    docName = timeStamp(docBase);
    beginRecord(PDF, "pdf/" + docName + ".pdf");
  }  else{ docName = ""; }
  
  for(Cell c : voronCells){
    c.render();
  }
  
if(makeSVG){
    endRecord();
    makeSVG = false;
    saveImage();
  }
  if(makePDF){
    endRecord();
    makePDF = false;
  }
}

void makeVoronoi() {
  // for each of those points now make a voronoi shape then make a corresponding voronoi cell
  for(PVector vec : pointsList){
    voronoi.addPoint(new Vec2D(vec.x, vec.y));
  } 
  
  for(Polygon2D polygon : voronoi.getRegions()) {
    Vec2D polyV = polygon.getCentroid();
    PVector vec = new PVector(0,0);
    if(polyV.x <= 0 || polyV.x >= width || polyV.y <= 0 || polyV.y >= height){
      for(Vec2D v : voronoi.getSites()){
        if(polygon.containsPoint(v)){
          vec.set(v.x, v.y);
        }
      }
    } else{
      vec.set(polyV.x, polyV.y);
    }
    color clr = colorArray[floor(random(colorArray.length))];
    voronCells.add(new Cell(vec, polygon, cellSpace, clr));
  }
}

void keyPressed() {
  if(key == ' ') reset();
  if(key == 'a') saveImage();
  if(key == 's') svgExport();
  if(key == 'd') pdfExport();
}

void reset() {
  background(bg);
  pointsList.clear();
  voronCells.clear();
  
  pointsList = new ArrayList<PVector>();
  pointsList = poissonDiskSampling(cellSpace, 30);
  voronCells = new ArrayList<Cell>();
  voronoi = new Voronoi();
  makeVoronoi();
  //redraw();  
}

void saveImage() {
  if(docName == ""){
    docName = timeStamp(docBase);
  } 
  saveFrame("png/" + docName + ".png");
}

void svgExport() {
  makeSVG = true;
  //redraw();
}

void pdfExport() {
  makePDF = true;
  //redraw();
}

String timeStamp(String name) {
  return name + "-" + year() + nf(month(), 2) + nf(day(), 2) +
    "-" + nf(hour(), 2) + nf(minute(), 2) + nf(second(), 2);
}


class Cell {
  PVector midPt;
  float radius;
  color clrSelect;
  float angle;
  PVector outerPt;
  PVector drawPt;
  PVector boxPt;
  int imgSize;
  float boxCornerRad;
  PGraphics curves;
  PGraphics masker;
  Polygon2D voron;
  
  Cell(PVector thePos, Polygon2D theVoron, float theRad, color theClr) {
    midPt = thePos;
    voron = theVoron;
    radius = theRad;
    clrSelect = theClr;
    angle = random(TWO_PI);
    outerPt = PVector.fromAngle(angle).setMag(radius).add(midPt);
    drawPt = PVector.fromAngle(angle).setMag((radius/2)-5).add(outerPt);
    imgSize = int(radius * 2)+5;
    boxCornerRad = (imgSize * sqrt(2)) / 2;
    boxPt = PVector.fromAngle(PI+QUARTER_PI).setMag(boxCornerRad).add(midPt);
    curves = makeCurves();
    masker = makeMask();
  }

  void render() {
    curves.mask(masker);
    pushMatrix();
    translate(drawPt.x, drawPt.y);
    rotate(angle + HALF_PI + QUARTER_PI);
    image(curves, 0,0);
    popMatrix();
  }
  
  PGraphics makeCurves() {
    PGraphics img = createGraphics(imgSize, imgSize);
    img.beginDraw();
    img.clear();
    img.background(bg);
    img.noFill();
    img.stroke(clrSelect);
    img.strokeWeight(2);
    for(int i=0; i<10; i++){
      float currRad = 40 + (i * 15);
      pushMatrix();
      translate(outerPt.x, outerPt.y);
      img.arc((radius/4), (radius/4), currRad*2, currRad*2, -QUARTER_PI, HALF_PI+QUARTER_PI);
      popMatrix();
    }
    img.endDraw();
    
    return img;
  }
  
  PGraphics makeMask() {
    ArrayList<PVector> voronPositions = new ArrayList<PVector>();
    PVector originOffsetPt = PVector.sub(midPt, boxPt);
    for(int i=0; i<voron.vertices.size(); i++){
      PVector currPt = new PVector(voron.vertices.get(i).x, voron.vertices.get(i).y);
      float distFromCenter = PVector.dist(midPt, currPt) - 5;
      PVector fromCenterPt = PVector.sub(midPt, currPt);
      pushMatrix();
      translate(midPt.x, midPt.y);
      float cornerAng = fromCenterPt.heading();
      popMatrix();
      float xPos = cos(cornerAng + PI - angle - HALF_PI - QUARTER_PI) * distFromCenter;
      float yPos = sin(cornerAng + PI - angle - HALF_PI - QUARTER_PI) * distFromCenter;
      voronPositions.add(new PVector(xPos, yPos));
    }
    
    PGraphics msk = createGraphics(imgSize, imgSize);
    msk.beginDraw();
    msk.clear();
    msk.translate(originOffsetPt.x, originOffsetPt.y);
    msk.beginShape();
    for(int j=0; j<voronPositions.size(); j++){
      msk.vertex(voronPositions.get(j).x, voronPositions.get(j).y);
    }
    msk.endShape(CLOSE);
    msk.endDraw();
    
    return msk;
  }
}

Your posted demo code will not run on my system; pointsList = poissonDiskSampling(cellSpace, 30); is not defined.

// utilities for the point placement
ArrayList<PVector> poissonDiskSampling(float radius, int k) {
  int N = 2;
  /* The final set of points to return */
  ArrayList<PVector> points = new ArrayList<PVector>();
  /* The currently "active" set of points */
  ArrayList<PVector> active = new ArrayList<PVector>();
  /* Initial point p0 */
  PVector p0 = new PVector(random(width), random(height));
  PVector[][] grid;
  float cellsize = floor(radius/sqrt(N));

  /* Figure out no. of cells in the grid for our canvas */
  int ncells_width = ceil(width/cellsize) + 1;
  int ncells_height = ceil(width/cellsize) + 1;

  /* Allocate the grid an initialize all elements to null */
  grid = new PVector[ncells_width][ncells_height];
  for (int i = 0; i < ncells_width; i++)
    for (int j = 0; j < ncells_height; j++)
      grid[i][j] = null;

  insertPoint(grid, cellsize, p0);
  points.add(p0);
  active.add(p0);

  while (active.size() > 0) {
    int random_index = int(random(active.size()));
    PVector p = active.get(random_index);
    
    boolean found = false;
    for (int tries = 0; tries < k; tries++) {
      float theta = random(TWO_PI);
      float new_radius = random(radius, 2*radius);
      float pnewx = p.x + new_radius * cos(theta);
      float pnewy = p.y + new_radius * sin(theta);
      PVector pnew = new PVector(pnewx, pnewy);
      
      if (!isValidPoint(grid, cellsize,
                        ncells_width, ncells_height,
                        pnew, radius))
        continue;
      
      points.add(pnew);
      insertPoint(grid, cellsize, pnew);
      active.add(pnew);
      found = true;
      break;
    }
    
    /* If no point was found after k tries, remove p */
    if (!found)
      active.remove(random_index);
  }

  return points;
}

boolean isValidPoint(PVector[][] grid, float cellsize,
                     int gwidth, int gheight,
                     PVector p, float radius) {
  /* Make sure the point is on the screen */
  if (p.x < 0 || p.x >= width || p.y < 0 || p.y >= height)
    return false;

  /* Check neighboring eight cells */
  int xindex = floor(p.x / cellsize);
  int yindex = floor(p.y / cellsize);
  int i0 = max(xindex - 1, 0);
  int i1 = min(xindex + 1, gwidth - 1);
  int j0 = max(yindex - 1, 0);
  int j1 = min(yindex + 1, gheight - 1);

  for (int i = i0; i <= i1; i++)
    for (int j = j0; j <= j1; j++)
      if (grid[i][j] != null)
        if (dist(grid[i][j].x, grid[i][j].y, p.x, p.y) < radius)
          return false;

  /* If we get here, return true */
  return true;
}

void insertPoint(PVector[][] grid, float cellsize, PVector point) {
  int xindex = floor(point.x / cellsize);
  int yindex = floor(point.y / cellsize);
  grid[xindex][yindex] = point;
}


// function to find the intersection points of circles to define arcs for each cell
// https://gist.github.com/jupdike/bfe5eb23d1c395d8a0a1a4ddd94882ac
PVector[] intersectTwoCircles(float x1, float y1, float r1, float x2, float y2, float r2) {
  PVector[] coords = new PVector[2];
  float centerdx = x1 - x2;
  float centerdy = y1 - y2;
  float R = sqrt(centerdx * centerdx + centerdy * centerdy);
  if(!(abs(r1 - r2) <= R && R <= r1 + r2)){ // no intersection
    return coords; // empty list of results
  }
  // intersection(s) should exist

  float R2 = R*R;
  float R4 = R2*R2;
  float a = (r1*r1 - r2*r2) / (2 * R2);
  float r2r2 = (r1*r1 - r2*r2);
  float c = sqrt(2 * (r1*r1 + r2*r2) / R2 - (r2r2 * r2r2) / R4 - 1);

  float fx = (x1+x2) / 2 + a * (x2 - x1);
  float gx = c * (y2 - y1) / 2;
  float ix1 = fx + gx;
  float ix2 = fx - gx;

  float fy = (y1+y2) / 2 + a * (y2 - y1);
  float gy = c * (x1 - x2) / 2;
  float iy1 = fy + gy;
  float iy2 = fy - gy;
  
  coords[0] = new PVector(ix1, iy1);
  coords[1] = new PVector(ix2, iy2);

  // note if gy == 0 and gx == 0 then the circles are tangent and there is only one solution
  // but that one solution will just be duplicated as the code is currently written
  return coords;
}

Apologies. I forgot to copy over the point sampling utilities.

The following works on my system:

  if (key == 'd') {
    beginRecord(PDF, "/Users/xxxxx/Desktop/frame-####.pdf");
    saveFrame("data/" + "curvy" + ".png");
    PImage img = loadImage("curvy.png");
    image(img, 0, 0);
    endRecord();
  }
1 Like

Hello @travis.stdenis,

This worked with your code and Processing 4.2 on W10:

PGraphics makeCurves() {
    PGraphics img = createGraphics(imgSize, imgSize, P2D);
    
//  Add these two lines:
    img.beginDraw();
    img.endDraw();
    
    img.beginDraw();
    // Your code
    img.endDraw();
    
    return img;
  }

Reference explains above additions to code:

I got the ToxicLibs library directly from link in here:

:)

Please try using createGraphics() in a standalone function to create a pdf file: PGraphics pdf = createGraphics(width, height, PDF, "/Users/xxxx/Desktop/output.pdf"); Reference: [PDF Export / Libraries / Processing.org ] (PDF Export / Libraries / Processing.org ) It does not crash on my system. Try to draw the entire final image inside this call.
Addendum:
The following works on my system:

  if (key == 'd') {
    beginRecord(PDF, "/Users/xxxxx/Desktop/frame-####.pdf");
    saveFrame("data/" + "curvy" + ".png");
    PImage img = loadImage("curvy.png");
    image(img, 0, 0);
    endRecord();
  }

Tried this and while the error disappears, the exported PDF is a blank document. Was this the only change you made or did you also change the P2D parameter on the createGraphics() call or something else?

This worked with your code and Processing 4.2 on W10:

PGraphics makeCurves() {
    PGraphics img = createGraphics(imgSize, imgSize, P2D);
    
//  Add these two lines:
    img.beginDraw();
    img.endDraw();
    
    img.beginDraw();
    // Your code
    img.endDraw();
    
    return img;
  }

This worked perfectly. Just to clarify for anyone else seeing this, it is adding the beginDraw() and endDraw() along with the addition of P2D as a parameter to the createGraphics() call.

That’s all that I did; no other changes. Works well on my Mac. It works by saving the window frame to a .png image saved into a sketch folder called ‘data’ and then immediately loading the .png into a PImage and recording it into a .pdf. My output is below:

When I tried the Windows fix on my Mac, it changed the background color to pure white instead of the off colored yellow of the original code.

Just curious; what happens if you try to run this code:

import processing.pdf.*;

void setup() {
  size(500, 500);
  surface.setTitle("Press 'p' for sketch folder pdf");
}

void draw() {
  fill(0);
  circle(width/2, height/2, 300);
}

void keyPressed() {
  if (key == 'p') {
    beginRecord(PDF, "myPDF-####.pdf");
    saveFrame("myPNG.png");
    PImage img = loadImage("myPNG.png");
    image(img, 0, 0);
    endRecord();
    println("pdf file in sketch folder");
  }
}

1 Like

Hello @travis.stdenis ,

Both of the createGraphics() calls. :)

@svan,

The background should be applied between record calls to apply to final PDF (also to SVG):

void draw() 
  {
  //background(bg);
  beginRecord(PDF,"test.pdf");
  
  background(bg);              //Background should be here to apply to PDF
  for(Cell c : voronCells)
    {
    c.render();
    }
  endRecord();
  }

Check this out:

There may be something in there that will allow you to generate your shapes and create a PDF or SVG with vector graphics instead of raster graphics.

I was able to export PDFs (with vector graphics) for one of the examples:

:)

There should be no reason why you can’t save the png, pdf, and svg files with one keystroke and remove a lot of your code (presuming this technique works on a Windows system if that’s what you have; works well on a Mac).

import processing.pdf.*;
import processing.svg.*;

void setup() {
  size(500, 500);
  surface.setTitle("Press 'p' for sketch folder png, pdf, and svg");
}

void draw() {
  fill(0);
  circle(width/2, height/2, 300);
}

void keyPressed() {
  if (key == 'p') {
    saveFrame("myPNG.png");
    PImage img = loadImage("myPNG.png");
    beginRecord(PDF, "myPDF-####.pdf");
    image(img, 0, 0);
    endRecord();
    beginRecord(SVG, "mySVG-####.svg");
    image(img, 0, 0);
    endRecord();
    println("png, pdf, and svg files saved in sketch folder");
  }
}

Sketch folder:
tres

1 Like

Check this out:

There may be something in there that will allow you to generate your shapes and create a PDF or SVG with vector graphics instead of raster graphics.

This looks really cool. Thanks for the share. I would have preferred to execute using vector graphics instead of rasters but I needed to clip the curved lines to the voronoi cell shape and PGraphics seemed the only way to accomplish that. I can do this fairly simply with standard geometric shapes given line intersection math, but I didn’t know how I could do it using the non-normal polygon shape. if anyone has ideas, i’m very open to it. Maybe this library will solve that.

1 Like

The problem with this method is that it’s not saving a vector drawing. It is instead saving a fixed resolution pixel image inside a vector image format. It loses the whole point of using a vector format in the first place which is to be resolution independent.

SVG and PDF store path and fill drawing commands independent of output resolution which means that they can later be blown up to whatever resolution you need such as for printing a large poster or issuing smooth drawing commands to a plotter at the native resolution of your output device.

1 Like

Good point, but as far as this thread goes I can’t see that it would make a difference since he is using rasters to begin with.

Hello @travis.stdenis ,

I was able to do this with the PGS library:

image

  s2 = PGS_ShapeBoolean.intersect(s1, star);
  PGS_Conversion.setAllStrokeColor(s2, color(0, 255, 0), 2);

s1 is a PShape with lines.
star is a PShape with the star.
s2 is the intersect PShape and displayed on the right on top of the star PShape with a grey fill.

:)

That’s perfect, exactly what i’m looking for. I’ll take a closer look at the library tonight.