/*****
 * picture.cc
 * Andy Hammerlindl 2002/06/06
 *
 * Stores a picture as a list of drawElements and handles its output to
 * PostScript.
 *****/

#include "errormsg.h"
#include "picture.h"
#include "util.h"
#include "settings.h"
#include "interact.h"
#include "drawverbatim.h"
#include "drawlabel.h"
#include "drawlayer.h"
#include "drawsurface.h"
#include "drawpath3.h"
#include "seconds.h"
#ifdef HAVE_RENDERER
#include "glfw.h"
#include "rendererloader.h"
#endif

#if defined(_WIN32)
#include <Windows.h>
#include <shellapi.h>
#include <cstdio>
#endif

#include <thread>
#include <chrono>

using std::ifstream;
using std::ofstream;
using vm::array;

using namespace settings;

extern pthread_mutex_t main_wait_mutex;
extern pthread_cond_t main_wait_cond;

texstream::~texstream() {
  string texengine=getSetting<string>("tex");
  bool context=settings::context(texengine);
  string name;
  if(!context)
    name=stripFile(outname());
  name += "texput.";
  unlink((name+"aux").c_str());
  unlink((name+"log").c_str());
  unlink((name+"out").c_str());
  if(settings::pdf(texengine)) {
    unlink((name+"pdf").c_str());
    unlink((name+"m9").c_str());
  } else
    unlink((name+"pbsdat").c_str());
  if(context) {
    unlink("cont-new.log");
    unlink((name+"tex").c_str());
    unlink((name+"top").c_str());
    unlink((name+"tua").c_str());
    unlink((name+"tui").c_str());
  }
}

namespace camp {

extern void draw();

bool isIdTransform3(const double* t)
{
  return (t == NULL || (t[0]==1 && t[1]==0 && t[2]==0 && t[3]==0 &&
                        t[4]==0 && t[5]==1 && t[6]==0 && t[7]==0 &&
                        t[8]==0 && t[9]==0 && t[10]==1 && t[11]==0 &&
                        t[12]==0 && t[13]==0 && t[14]==0 && t[15]==1));
}

// copy array to 4x4 transform matrix with range checks
void copyArray4x4C(double*& dest, const vm::array *a)
{
  double tt[16];
  const size_t n=checkArray(a);
  const string fourbyfour="4x4 array of doubles expected";
  if(n != 4) reportError(fourbyfour);

  for(size_t i=0; i < 4; i++) {
    const vm::array *ai=vm::read<vm::array*>(a,i);
    const size_t aisize=checkArray(ai);
    double *tti=tt+4*i;
    if(aisize == 4) {
      for(size_t j=0; j < 4; j++)
        tti[j]=vm::read<double>(ai,j);
    } else reportError(fourbyfour);
  }

  copyTransform3(dest,tt);
}

void copyTransform3(double*& d, const double* s, GCPlacement placement)

{
  if(s != NULL) {
    if(d == NULL)
      d=placement == NoGC ? new double[16] : new(placement) double[16];
    memcpy(d,s,sizeof(double)*16);
  }
}

// t = s*r
void multiplyTransform3(double*& t, const double* s, const double* r)
{
  if(isIdTransform3(s)) {
    copyTransform3(t,r);
  } else if(isIdTransform3(r)) {
    copyTransform3(t,s);
  } else {
    t=new(UseGC) double[16];
    for(size_t i=0; i < 4; i++) {
      size_t i4=4*i;
      const double *si=s+i4;
      const double& s0=si[0];
      const double& s1=si[1];
      const double& s2=si[2];
      const double& s3=si[3];
      double *ti=t+i4;
      ti[0]=s0*r[0]+s1*r[4]+s2*r[8]+s3*r[12];
      ti[1]=s0*r[1]+s1*r[5]+s2*r[9]+s3*r[13];
      ti[2]=s0*r[2]+s1*r[6]+s2*r[10]+s3*r[14];
      ti[3]=s0*r[3]+s1*r[7]+s2*r[11]+s3*r[15];
    }
  }
}

double xratio(const triple& v) {return v.getx()/v.getz();}
double yratio(const triple& v) {return v.gety()/v.getz();}

class matrixstack {
  mem::stack<const double*> mstack;

public:
  // return current transform
  const double* T() const
  {
    if(mstack.empty())
      return NULL;
    else
      return mstack.top();
  }
  // we store the accumulated transform of all pushed transforms
  void push(const double *r)
  {
    double* T3 = NULL;
    multiplyTransform3(T3,T(),r);
    mstack.push(T3);
  }
  void pop()
  {
    if(!mstack.empty())
      mstack.pop();
  }

};

const char *texpathmessage() {
  ostringstream buf;
  buf << "the directory containing your " << getSetting<string>("tex")
      << " engine (" << texcommand() << ")";
  return Strdup(buf.str());
}

picture::~picture()
{
}

void picture::enclose(drawElement *begin, drawElement *end)
{
  assert(begin);
  assert(end);
  nodes.push_front(begin);
  lastnumber=0;
  lastnumber3=0;

  for(nodelist::iterator p=nodes.begin(); p != nodes.end(); ++p) {
    assert(*p);
    if((*p)->islayer()) {
      nodes.insert(p,end);
      ++p;
      while(p != nodes.end() && (*p)->islayer()) ++p;
      if(p == nodes.end()) return;
      nodes.insert(p,begin);
    }
  }
  nodes.push_back(end);
}

// Insert at beginning of picture.
void picture::prepend(drawElement *p)
{
  assert(p);
  nodes.push_front(p);
  lastnumber=0;
  lastnumber3=0;
}

void picture::append(drawElement *p)
{
  assert(p);
  nodes.push_back(p);
}

void picture::add(picture &pic)
{
  if (&pic == this) return;

  // STL's funny way of copying one list into another.
  copy(pic.nodes.begin(), pic.nodes.end(), back_inserter(nodes));
}

// Insert picture pic at beginning of picture.
void picture::prepend(picture &pic)
{
  if (&pic == this) return;

  copy(pic.nodes.begin(), pic.nodes.end(), inserter(nodes, nodes.begin()));
  lastnumber=0;
  lastnumber3=0;
}

bool picture::havelabels()
{
  size_t n=nodes.size();
  if(n > lastnumber && !labels && getSetting<string>("tex") != "none") {
    // Check to see if there are any labels yet
    nodelist::iterator p=nodes.begin();
    for(size_t i=0; i < lastnumber; ++i) ++p;
    for(; p != nodes.end(); ++p) {
      assert(*p);
      if((*p)->islabel()) {
        labels=true;
        break;
      }
    }
  }
  return labels;
}

bool picture::have3D()
{
  for(nodelist::iterator p=nodes.begin(); p != nodes.end(); ++p) {
    assert(*p);
    if((*p)->is3D())
      return true;
  }
  return false;
}

bool picture::havepng()
{
  for(nodelist::iterator p=nodes.begin(); p != nodes.end(); ++p) {
    assert(*p);
    if((*p)->svgpng())
      return true;
  }
  return false;
}

unsigned int picture::pagecount()
{
  unsigned int c=1;
  for(nodelist::iterator p=nodes.begin(); p != nodes.end(); ++p) {
    assert(*p);
    if((*p)->isnewpage())
      ++c;
  }
  return c;
}

bbox picture::bounds()
{
  size_t n=nodes.size();
  if(n == lastnumber) return b_cached;

  if(lastnumber == 0) { // Maybe these should be put into a structure.
    b_cached=bbox();
    labelbounds.clear();
    bboxstack.clear();
  }

  if(havelabels()) texinit();

  nodelist::iterator p=nodes.begin();
  processDataStruct& pd=processData();

  for(size_t i=0; i < lastnumber; ++i) ++p;
  for(; p != nodes.end(); ++p) {
    assert(*p);
    (*p)->bounds(b_cached,pd.tex,labelbounds,bboxstack);

    // Optimization for interpreters with fixed stack limits.
    if((*p)->endclip()) {
      nodelist::iterator q=p;
      if(q != nodes.begin()) {
        --q;
        assert(*q);
        if((*q)->endclip())
          (*q)->save(false);
      }
    }
  }

  lastnumber=n;
  return b_cached;
}

bbox3 picture::bounds3()
{
  size_t n=nodes.size();
  if(n == lastnumber3) return b3;

  if(lastnumber3 == 0)
    b3=bbox3();

  matrixstack ms;
  for(nodelist::const_iterator p=nodes.begin(); p != nodes.end(); ++p) {
    assert(*p);
    if((*p)->begingroup3())
      ms.push((*p)->transf3());
    else if((*p)->endgroup3())
      ms.pop();
    else
      (*p)->bounds(ms.T(),b3);
  }

  lastnumber3=n;
  return b3;
}

pair picture::ratio(double (*m)(double, double))
{
  bool first=true;
  pair b;
  bounds3();
  double fuzz=Fuzz*(b3.Max()-b3.Min()).length();
  matrixstack ms;
  for(nodelist::const_iterator p=nodes.begin(); p != nodes.end(); ++p) {
    assert(*p);
    if((*p)->begingroup3())
      ms.push((*p)->transf3());
    else if((*p)->endgroup3())
      ms.pop();
    else
      (*p)->ratio(ms.T(),b,m,fuzz,first);
  }
  return b;
}

void texinit()
{
  drawElement::lastpen=pen(initialpen);
  processDataStruct &pd=processData();
  // Output any new texpreamble commands
  if(pd.tex.isopen()) {
    if(pd.TeXpipepreamble.empty()) return;
    texpreamble(pd.tex,pd.TeXpipepreamble,true);
    pd.TeXpipepreamble.clear();
    return;
  }

  bool context=settings::context(getSetting<string>("tex"));
  string dir=stripFile(outname());
  string logname;
  if(!context) logname=dir;
  logname += "texput.log";
  const char *cname=logname.c_str();
  ofstream writeable(cname);
  if(!writeable)
    reportError("Cannot write to "+logname);
  else
    writeable.close();
  unlink(cname);

  dir=dir.substr(0,dir.length()-1);
  mem::vector<string> cmd;
  cmd.push_back(texprogram());
  string oldPath;
  if(context) {
    if(!dir.empty()) {
      oldPath=getPath();
      setPath(dir.c_str());
    }
    cmd.push_back("--pipe");
  } else {
    if(!dir.empty())
      cmd.push_back("-output-directory="+dir);
    string jobname="texput";
    if(getSetting<bool>("inlineimage") || getSetting<bool>("inlinetex")) {
      string name=stripDir(stripExt((outname())));
      size_t pos=name.rfind("-");
      if(pos < string::npos) {
        name=stripExt(name).substr(0,pos);
        unlink((name+".aux").c_str());
        jobname=name.substr(0,pos);
        cmd.push_back("-jobname="+jobname);
#ifdef __MSDOS__
        cmd.push_back("NUL"); // For MikTeX
#endif
      }
    }
    cmd.push_back("\\scrollmode");
  }

  pd.tex.open(cmd,"texpath");
  pd.tex.wait("\n*");
  pd.tex << "\n";
  texdocumentclass(pd.tex,true);

  texdefines(pd.tex,pd.TeXpreamble,true);
  pd.TeXpipepreamble.clear();
}

int opentex(const string& texname, const string& prefix, bool dvi)
{
  string aux=auxname(prefix,"aux");
  unlink(aux.c_str());
  bool context=settings::context(getSetting<string>("tex"));
  mem::vector<string> cmd;
  cmd.push_back(texprogram());
  if(dvi)
    cmd.push_back("-output-format=dvi");
  string dir=stripFile(texname);
  dir=dir.substr(0,dir.length()-1);
  string oldPath;
  if(context) {
    if(!dir.empty()) {
      oldPath=getPath();
      setPath(dir.c_str());
    }
    cmd.push_back("--nonstopmode");
    cmd.push_back(texname);
  } else {
    if(!dir.empty())
      cmd.push_back("-output-directory="+dir);
    cmd.push_back("\\nonstopmode\\input");
    cmd.push_back(stripDir(texname));
  }

  bool quiet=verbose <= 1;
  int status=System(cmd,quiet ? 1 : 0,true,"texpath",texpathmessage());
  if(!status && getSetting<bool>("twice"))
    status=System(cmd,quiet ? 1 : 0,true,"texpath",texpathmessage());
  if(status) {
    if(quiet) {
      cmd[1]=context ? "--scrollmode" : "\\scrollmode\\input";
      System(cmd,0);
    }
  }
  if(context && !oldPath.empty())
    setPath(oldPath.c_str());
  return status;
}

string dvisvgmCommand(mem::vector<string>& cmd, const string& outname)
{
  string dir=stripFile(outname);
  string oldPath;
  if(!dir.empty()) {
    oldPath=getPath();
    setPath(dir.c_str());
  }
  cmd.push_back(getSetting<string>("dvisvgm"));
  cmd.push_back("-n");
  cmd.push_back("-v3");
  string libgs=getSetting<string>("libgs");
  if(!libgs.empty())
    cmd.push_back("--libgs="+libgs);
  push_split(cmd,getSetting<string>("dvisvgmOptions"));
  string outfile=stripDir(outname);
  if(!outfile.empty())
    cmd.push_back("-o"+outfile);
  return oldPath;
}

bool picture::texprocess(const string& texname, const string& outname,
                         const string& prefix, const pair& bboxshift,
                         bool svg)
{
  int status=1;
  ifstream outfile;

  outfile.open(texname.c_str());
  bool keep=getSetting<bool>("keep");

  if(outfile) {
    outfile.close();

    status=opentex(texname,prefix);
    string texengine=getSetting<string>("tex");

    if(status == 0) {
      string dviname=auxname(prefix,"dvi");
      mem::vector<string> cmd;

      if(svg) {
        string name=deconstruct ? buildname(prefix+"_%1p","svg") : outname;
        string oldPath=dvisvgmCommand(cmd,name);
        cmd.push_back(stripDir(dviname));
        if(deconstruct)
          cmd.push_back("-p1-");
        status=System(cmd,0,true,"dvisvgm");
        if(!oldPath.empty())
          setPath(oldPath.c_str());
        if(!keep)
          unlink(dviname.c_str());
      } else {
        if(!settings::pdf(texengine)) {
          string psname=auxname(prefix,"ps");
          double height=b.top-b.bottom+1.0;

          // Magic dvips offsets:
          double hoffset=-128.4;
          double vertical=height;
          if(!latex(texengine)) vertical += 2.0;
          double voffset=(vertical < 13.0) ? -137.8+vertical : -124.8;
          double paperHeight=getSetting<double>("paperheight");

          hoffset += b.left+bboxshift.getx();
          voffset += paperHeight-height-b.bottom-bboxshift.gety();

          string dvipsrc=getSetting<string>("dir");
          if(dvipsrc.empty()) dvipsrc=systemDir;
          dvipsrc += dirsep+"nopapersize.ps";
          setenv("DVIPSRC",dvipsrc.c_str(),true);
          string papertype=getSetting<string>("papertype") == "letter" ?
            "letterSize" : "a4size";
          cmd.push_back(getSetting<string>("dvips"));
          cmd.push_back("-R");
          cmd.push_back("-Pdownload35");
          cmd.push_back("-D600");
          cmd.push_back("-O"+String(hoffset)+"bp,"+String(voffset)+"bp");
          bool ps=pagecount() > 1;
          cmd.push_back("-T"+String(getSetting<double>("paperwidth"))+"bp,"+
                        String(paperHeight)+"bp");
          push_split(cmd,getSetting<string>("dvipsOptions"));
          if(ps && getSetting<string>("papertype") != "")
            cmd.push_back("-t"+papertype);
          if(verbose <= 1) cmd.push_back("-q");
          cmd.push_back("-o"+psname);
          cmd.push_back(dviname);
          status=System(cmd,0,true,"dvips");
          if(status == 0) {
            ifstream fin(psname.c_str());
            psfile fout(outname,false);

            string s;
            bool first=true;
            transform t=shift(bboxshift)*T;
            bool shift=!t.isIdentity();

            const string beginspecial="TeXDict begin @defspecial";
            const size_t beginlength=beginspecial.size();
            const string endspecial="@fedspecial end";
            const size_t endlength=endspecial.size();

            bool inpapersize=false;
            while(getline(fin,s)) {

              if (inpapersize) {
                if(s.find("%%EndPaperSize") == 0)
                  inpapersize=false;
                continue;
              } else {
                if (s.find("%%BeginPaperSize:") == 0) {
                  inpapersize=true;
                  continue;
                }
              }

              if (s[0] == '%') {
                if (s.find("%%DocumentPaperSizes:") == 0)
                  continue;

                if(s.find("%!PS-Adobe-") == 0) {
                  fout.header(!ps);
                  continue;
                }

                if (first && s.find("%%BoundingBox:") == 0) {
                  bbox box=b.shift(bboxshift);
                  if(verbose > 2) BoundingBox(cout,box);
                  fout.BoundingBox(box);
                  first=false;
                  continue;
                }
              }

              if (shift) {
                if (s.compare(0, beginlength, beginspecial) == 0) {
                  fout.verbatimline(s);
                  fout.gsave();
                  fout.concat(t);
                  continue;
                }
                if (s.compare(0, endlength, endspecial) == 0) {
                  fout.grestore();
                  fout.verbatimline(s);
                  continue;
                }
              }

              // For the default line, output it unchanged.
              fout.verbatimline(s);
            }
          }
          if(!keep) {
            unlink(dviname.c_str());
            unlink(psname.c_str());
          }
        }
      }
    }

    if(!keep) {
      unlink(texname.c_str());
      if(!getSetting<bool>("keepaux"))
        unlink(auxname(prefix,"aux").c_str());
      unlink(auxname(prefix,"log").c_str());
      unlink(auxname(prefix,"out").c_str());
      string dir=stripFile(prefix);
      unlink((dir+"texput.log").c_str());
      unlink((dir+"texput.aux").c_str());
      if(settings::context(texengine)) {
        unlink(auxname(prefix,"top").c_str());
        unlink(auxname(prefix,"tua").c_str());
        unlink(auxname(prefix,"tuc").c_str());
        unlink(auxname(prefix,"tui").c_str());
        unlink(auxname(prefix,"tuo").c_str());
      }
    }
    if(status == 0) return true;
  }
  return false;
}

int picture::epstopdf(const string& epsname, const string& pdfname)
{
  string outputformat=getSetting<string>("outformat");
  bool pdf=settings::pdf(getSetting<string>("tex"));
  bool pdfformat=(pdf && outputformat == "") || outputformat == "pdf";
  string compress=getSetting<bool>("compress") && pdfformat ?
    "true" : "false";
  mem::vector<string> cmd;
  cmd.push_back(getSetting<string>("gs"));
  cmd.push_back("-q");
  cmd.push_back("-dNOPAUSE");
  cmd.push_back("-dBATCH");
  cmd.push_back("-P");
  if(safe)
    cmd.push_back("-dSAFER");
  cmd.push_back("-dALLOWPSTRANSPARENCY"); // Support transparency extensions.
  cmd.push_back("-sDEVICE=pdfwrite");
  cmd.push_back("-dEPSCrop");
  cmd.push_back("-dSubsetFonts=true");
  cmd.push_back("-dEmbedAllFonts=true");
  cmd.push_back("-dMaxSubsetPct=100");
  cmd.push_back("-dEncodeColorImages="+compress);
  cmd.push_back("-dEncodeGrayImages="+compress);
  cmd.push_back("-dCompatibilityLevel=1.5");
  cmd.push_back("-dTransferFunctionInfo=/Apply");
  if(!getSetting<bool>("autorotate"))
    cmd.push_back("-dAutoRotatePages=/None");
  cmd.push_back("-g"+String(max(ceil(getSetting<double>("paperwidth")),1.0))
                +"x"+String(max(ceil(getSetting<double>("paperheight")),1.0)));
  cmd.push_back("-dDEVICEWIDTHPOINTS="+String(max(b.right-b.left,3.0)));
  cmd.push_back("-dDEVICEHEIGHTPOINTS="+String(max(b.top-b.bottom,3.0)));
  push_split(cmd,getSetting<string>("gsOptions"));
  cmd.push_back("-sOutputFile="+stripDir(pdfname));
  if(safe) {
    cmd.push_back("-c");
    cmd.push_back(".setsafe");
    cmd.push_back("-f");
  }
  cmd.push_back(stripDir(epsname));

  char *oldPath=NULL;
  string dir=stripFile(pdfname);
  if(!dir.empty()) {
    oldPath=getPath();
    setPath(dir.c_str());
  }
  int status=System(cmd,0,true,"gs","Ghostscript");
  if(oldPath != NULL)
    setPath(oldPath);
  return status;
}

int picture::pdftoeps(const string& pdfname, const string& epsname, bool eps)
{
  mem::vector<string> cmd;
  cmd.push_back(getSetting<string>("gs"));
  cmd.push_back("-q");
  cmd.push_back("-dNoOutputFonts");
  cmd.push_back("-dNOPAUSE");
  cmd.push_back("-dBATCH");
  cmd.push_back("-P");
  if(safe)
    cmd.push_back("-dSAFER");
  string texengine=getSetting<string>("tex");
  cmd.push_back("-sDEVICE="+getSetting<string>(eps ? "epsdriver": "psdriver"));
  cmd.push_back("-sOutputFile="+stripDir(epsname));
  cmd.push_back(stripDir(pdfname));

  char *oldPath=NULL;
  string dir=stripFile(epsname);
  if(!dir.empty()) {
    oldPath=getPath();
    setPath(dir.c_str());
  }
  int status=System(cmd,0,true,"gs","Ghostscript");
  if(oldPath != NULL)
    setPath(oldPath);
  return status;
}

bool picture::reloadPDF(const string& Viewer, const string& outname) const
{
  static bool needReload=true;
  static bool haveReload=false;

  string reloadprefix="reload";
  if(needReload) {
    needReload=false;
    string name=getPath()+string("/")+outname;
    // Write javascript code to redraw picture.
    runString("settings.tex='pdflatex'; tex('\\ \\pdfannot width 0pt height 0pt { /AA << /PO << /S /JavaScript /JS (try{reload(\""+name+"\");} catch(e) {} closeDoc(this);) >> >> }'); shipout('"+reloadprefix+"',wait=false,view=false);erase();exit();",false);
    haveReload=true;
  }

  if(haveReload) {
    mem::vector<string> cmd;
    push_command(cmd,Viewer);
    string pdfreloadOptions=getSetting<string>("pdfreloadOptions");
    if(!pdfreloadOptions.empty())
      cmd.push_back(pdfreloadOptions);
    cmd.push_back(reloadprefix+".pdf");
    System(cmd,0,false);
  }
  return true;
}

int picture::epstosvg(const string& epsname, const string& outname,
                      unsigned int pages)
{
  string oldPath;
  int status=0;
  if(deconstruct && getSetting<bool>("dvisvgmMultipleFiles")) {
    mem::vector<string> cmd;
    oldPath=dvisvgmCommand(cmd,stripFile(outname));
    for(unsigned i=1; i <= pages; ++i) {
      ostringstream buf;
      buf << epsname << i << ".ps";
      cmd.push_back(buf.str());
    }
    cmd.push_back("-E");
    status=System(cmd,0,true,"dvisvgm");
    for(unsigned i=1; i <= pages; ++i) {
      ostringstream buf;
      buf << epsname << i << ".ps";
      if(!getSetting<bool>("keep"))
        unlink(buf.str().c_str());
    }
    if(!oldPath.empty())
      setPath(oldPath.c_str());
  } else {
    string outprefix=stripExt(outname);
    for(unsigned i=1; i <= pages; ++i) {
      mem::vector<string> cmd;
      ostringstream out;
      out << outprefix;
      if(deconstruct)
        out << "_" << i;
      out << ".svg";
      oldPath=dvisvgmCommand(cmd,out.str());
      ostringstream buf;
      buf << epsname << i << ".ps";
      cmd.push_back(buf.str());
      cmd.push_back("-E");
      status=System(cmd,0,true,"dvisvgm");
      if(!getSetting<bool>("keep"))
        unlink(buf.str().c_str());
      if(!oldPath.empty())
        setPath(oldPath.c_str());
      if(status != 0) break;
    }
  }
  return status;
}

void htmlView(string name)
{
  string const browser=getSetting<string>("htmlviewer");
  string const htmlFile=locateFile(name, true);

  if (browser.empty())
  {
#if defined(_WIN32)
    // for windows, no browser means to use the windows' default
    auto const result = reinterpret_cast<INT_PTR>(
      ShellExecuteA(
        nullptr,
        "open",
        htmlFile.c_str(),
        nullptr,
        nullptr,
        SW_SHOWNORMAL
      ));
    // see https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shellexecutea
    if (result <= 32)
    {
      // error code should be stored in GetLastError
      w32::reportAndFailWithLastError( "Cannot open browser for viewing");
    }
#else
    reportError("No browser specified; please specify your browser in htmlviewer");
#endif
  }
  else
  {
    string const browserOptions= getSetting<string>("htmlviewerOptions");
    mem::vector<string> cmd;
    push_command(cmd, browser);
    cmd.push_back(htmlFile);
    if (browserOptions.empty())
    {
      push_split(cmd, browserOptions);
    }
    System(cmd, 2, false);
  }
}

bool picture::postprocess(const string& prename, const string& outname,
                          const string& outputformat,
                          bool wait, bool view, bool pdftex,
                          bool epsformat, bool svg)
{
  int status=0;
  bool pdf=settings::pdf(getSetting<string>("tex"));
  bool pdfformat=(pdf && outputformat == "") || outputformat == "pdf";

  mem::vector<string> cmd;
  if(pdftex || !epsformat) {
    if(pdfformat) {
      if(pdftex) {
        status=renameOverwrite(prename.c_str(),outname.c_str());
        if(status != 0)
          reportError("Cannot rename "+prename+" to "+outname);
      } else status=epstopdf(prename,outname);
    } else if(epsformat) {
      if(svg) {
        string psname=stripExt(prename);
        status=pdftoeps(prename,psname+"%d.ps",false);
        if(status != 0) return false;
        status=epstosvg(stripDir(psname),outname,pagecount());
        if(status != 0) return false;
        epsformat=false;
      } else
        status=pdftoeps(prename,outname);
    } else {
      double render=fabs(getSetting<double>("render"));
      if(render == 0) render=1.0;
      double res=render*72.0;
      Int antialias=getSetting<Int>("antialias");
      if(outputformat == "png" && antialias == 2) {
        cmd.push_back(getSetting<string>("gs"));
        cmd.push_back("-q");
        cmd.push_back("-dNOPAUSE");
        cmd.push_back("-dBATCH");
        cmd.push_back("-P");
        cmd.push_back("-sDEVICE="+getSetting<string>("pngdriver"));
        if(safe)
          cmd.push_back("-dSAFER");
        cmd.push_back("-r"+String(res)+"x"+String(res));
        push_split(cmd,getSetting<string>("gsOptions"));
        cmd.push_back("-sOutputFile="+outname);
        cmd.push_back("-dTextAlphaBits=4");
        cmd.push_back("-dGraphicsAlphaBits=4");
        cmd.push_back(prename);
        status=System(cmd,0,true,"gs","Ghostscript");
      } else if(!svg && !xasy) {
        double expand=antialias;
        if(expand < 2.0) expand=1.0;
        res *= expand;
        string s=getSetting<string>("convert");
        cmd.push_back(s);
        cmd.push_back("-density");
        cmd.push_back(String(res)+"x"+String(res));
        cmd.push_back(prename);
        if(expand == 1.0)
          cmd.push_back("+antialias");
        push_split(cmd,getSetting<string>("convertOptions"));
        cmd.push_back("-resize");
        cmd.push_back(String(100.0/expand)+"%x");
        if(outputformat == "jpg" || outputformat == "jpeg")
          cmd.push_back("-flatten");
        cmd.push_back(outputformat+":"+outname);
        status=System(cmd,0,true,"convert");
      }
    }
    if(!getSetting<bool>("keep"))
      unlink(prename.c_str());
  }
  if(status != 0) return false;

  if(verbose > 0 && !deconstruct)
    cout << "Wrote " << outname << endl;

  return display(outname,outputformat,wait,view,epsformat);
}

bool picture::display(const string& outname, const string& outputformat,
                      bool wait, bool view, bool epsformat)
{
  static mem::map<const string,int> pids;
  if (settings::view() && view)
  {
    int status;

    bool const pdf=settings::pdf(getSetting<string>("tex"));
    bool pdfformat=(pdf && outputformat.empty()) || outputformat == "pdf";

    if(epsformat || pdfformat) {
      // Check to see if there is an existing viewer for this outname.
      mem::map<const string,int>::iterator const p=pids.find(outname);
      bool running=(p != pids.end());
      string Viewer=
        pdfformat ?
        getSetting<string>("pdfviewer") : getSetting<string>("psviewer");
      int pid;
      if(running) {
        pid=p->second;
        if(pid)
        {
#if defined(_WIN32)
          running=w32::isProcessRunning(pid);
#else
          running= (waitpid(pid, &status, WNOHANG) != pid);
#endif
        }
      }

      bool pdfreload=pdfformat && getSetting<bool>("pdfreload");
      if(running) {
#if defined(_WIN32)
        if (pdfreload)
        {
          reloadPDF(Viewer,outname);
        }
#else // win32 does not support reload by sighup
        // Tell gv/acroread to reread file.
        if(Viewer == "gv")
        {
          kill(pid,SIGHUP);
        }
        else if(pdfreload)
        {
          reloadPDF(Viewer,outname);
        }
#endif
      } else {
        // start new process
        if (Viewer.empty())
        {
#if defined(_WIN32)
          // no viewer, use default

          string const fullOutFilePath= locateFile(outname, true);
          SHELLEXECUTEINFOA execInfo = {};
          execInfo.cbSize= sizeof(execInfo);
          execInfo.hwnd = nullptr;
          execInfo.lpVerb= "open";
          execInfo.lpFile= fullOutFilePath.c_str();
          execInfo.lpDirectory = nullptr;
          execInfo.nShow= SW_SHOWNORMAL;
          execInfo.fMask= SEE_MASK_NOCLOSEPROCESS;

          if (!ShellExecuteExA(&execInfo))
          {
            return false;
          }

          if (!w32::checkShellExecuteResult(reinterpret_cast<INT_PTR>(execInfo.hInstApp),false))
          {
            // see https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ns-shellapi-shellexecuteinfoa
            return false;
          }

          if (execInfo.hProcess!=nullptr)
          {
            // wait option does not always work, especially if a new process is not created
            // for example, if an existing PDF viewer with multiple tabs open is the viewer,
            // asymptote thinks no process is being created;
            // in this case, treat it as "no wait"
            pid=static_cast<int>(GetProcessId(execInfo.hProcess));
            CloseHandle(execInfo.hProcess);
          }
#else
          cerr << "No viewer specified" << endl;
          return false;
#endif
        }
        else
        {
          string viewerOptions= getSetting<string>(pdfformat ? "pdfviewerOptions" : "psviewerOptions");
          mem::vector<string> cmd;

          push_command(cmd, Viewer);
          if (!viewerOptions.empty()) push_split(cmd, viewerOptions);
          cmd.push_back(outname);

          status=System(cmd,
            0,
            wait,
            pdfformat ? "pdfviewer" : "psviewer",
            pdfformat ? "your PDF viewer" : "your PostScript viewer",
            &pid);

          if (status != 0) return false;
        }

        if(!wait) pids[outname]=pid;

        if(pdfreload) {
          // Work around race conditions in acroread initialization script
          std::this_thread::sleep_for(std::chrono::microseconds(
            getSetting<Int>("pdfreloaddelay")
          ));
          // Only reload if pdf viewer process is already running
#if defined(_WIN32)
          bool processRunning=w32::isProcessRunning(pid);
#else
          bool processRunning= waitpid(pid, &status, WNOHANG) == pid;
#endif
          if (processRunning)
            reloadPDF(Viewer,outname);
        }
      }
    } else {
      if(outputformat == "svg" || outputformat == "html")
        htmlView(outname);
      else {
        string displayProgram=getSetting<string>("display");
        if (displayProgram.empty())
        {
#if defined(_WIN32)

          auto const result = reinterpret_cast<INT_PTR>(ShellExecuteA(
            nullptr,
            "open",
            outname.c_str(), nullptr,
            nullptr, SW_SHOWNORMAL));

          if (result <= 32)
          {
            cerr << "Cannot start display viewer" << endl;
            return false;
          }
#else
          cerr << "No viewer specified; please specify a viewer in 'display' setting" << endl;
          return false;
#endif
        }
        else
        {
          mem::vector<string> cmd;
          push_command(cmd, displayProgram);
          cmd.push_back(outname);
          string const application= "your " + outputformat + " viewer";
          status= System(cmd, 0, wait, "display", application.c_str());
          if (status != 0) return false;
        }
      }
    }
  }
  return true;
}

string Outname(const string& prefix, const string& outputformat,
               bool standardout, string aux="")
{
  return standardout ? "-" : buildname(prefix,outputformat,aux);
}

bool picture::shipout(picture *preamble, const string& Prefix,
                      const string& format, bool wait, bool view)
{
  bool keep=getSetting<bool>("keep");

  string aux="";
  b=bounds();
  bool empty=b.empty;

  string outputformat=format.empty() ? defaultformat() : format;

  bool htmlformat=outputformat == "html";
  if(htmlformat) {
    outputformat="svg";
    aux="_";
    if(view) view=false;
    else htmlformat=false;
  }

  if(outputformat == "v3d")
    camp::reportError("v3d format only supports 3D files");

  bool svgformat=outputformat == "svg";
  bool png=outputformat == "png";

  string texengine=getSetting<string>("tex");

  string texengineSave;

  if(!empty && !deconstruct && (png || (svgformat && havepng())) &&
     texengine == "latex") {
    texengineSave=texengine;
    Setting("tex")=texengine="pdflatex";
  }

  bool usetex=texengine != "none";
  bool TeXmode=getSetting<bool>("inlinetex") && usetex;
  bool pdf=settings::pdf(texengine);

  bool standardout=Prefix == "-";
  string prefix=standardout ? standardprefix : Prefix;

  string preformat=nativeformat();
  bool epsformat=outputformat == "eps";
  bool pdfformat=pdf || png || outputformat == "pdf";
  bool dvi=false;
  bool svg=svgformat && usetex &&
    (!have3D() || getSetting<double>("render") == 0.0);
  if(svg) {
    if(pdf) epsformat=true;
    else dvi=true;
  }

  string outname=Outname(prefix,outputformat,standardout,aux);
  string epsname=epsformat ? (standardout ? "" : outname) :
    auxname(prefix,"eps");

  bool Labels=labels || TeXmode;

  bool Empty=b.right-b.left < 1.0 || b.top-b.bottom < 1.0;
  if(png && Empty)
    empty=true;

  if(Empty && (svgformat || htmlformat)) return true;

  if(empty && !Labels) { // Output a null file
    bbox b;
    b.left=b.bottom=0;
    b.right=b.top=1;
    psfile out(epsname,false);
    out.prologue(b);
    out.epilogue();
    out.close();
    return postprocess(epsname,outname,outputformat,wait,view,false,
                       epsformat,false);
  }

  if(svg || png)
    Labels=true;

  if(Labels)
    prefix=cleanpath(prefix);

  string prename=((epsformat && !pdf) || !Labels) ? epsname :
    auxname(prefix,preformat);

  SetPageDimensions();

  pair aligndir=getSetting<pair>("aligndir");
  string origin=getSetting<string>("align");

  pair bboxshift=(origin == "Z" && epsformat) ?
    pair(0.0,0.0) : pair(-b.left,-b.bottom);

  if(epsformat) {
    bboxshift += getSetting<pair>("offset");
    double yexcess=max(getSetting<double>("paperheight")-
                       (b.top-b.bottom+1.0),0.0);
    double xexcess=max(getSetting<double>("paperwidth")-
                       (b.right-b.left+1.0),0.0);
    if(aligndir == pair(0,0)) {
      if(origin != "Z" && origin != "B") {
        if(origin == "T") bboxshift += pair(0.0,yexcess);
        else bboxshift += pair(0.5*xexcess,0.5*yexcess);
      }
    } else {
      double scale=max(fabs(aligndir.getx()),fabs(aligndir.gety()));
      if(scale != 0) aligndir *= 0.5/scale;
      bboxshift +=
        pair((aligndir.getx()+0.5)*xexcess,(aligndir.gety()+0.5)*yexcess);
    }
  }

  bool status=true;

  string texname;
  texfile *tex=NULL;

  if(Labels) {
    texname=TeXmode ? buildname(prefix,"tex") : auxname(prefix,"tex");
    tex=dvi ?
      new svgtexfile(texname,b,false,deconstruct) :
      new texfile(texname,b);
    tex->prologue(deconstruct);
  }

  nodelist::iterator layerp=nodes.begin();
  nodelist::iterator p=layerp;
  unsigned layer=0;
  mem::list<string> files;

  bbox bshift=b;

  int svgcount=0;

  typedef mem::list<drawElement *> clipstack;
  clipstack begin;

  while(p != nodes.end()) {
    string psname,pdfname;
    if(Labels) {
      ostringstream buf;
      buf << prefix << "_" << layer;
      psname=buildname(buf.str(),"eps");
      if(pdf) pdfname=buildname(buf.str(),"pdf");
    } else {
      psname=epsname;
      bshift=bshift.shift(bboxshift);
    }
    files.push_back(psname);
    if(pdf) files.push_back(pdfname);
    psfile out(psname,pdfformat);
    out.prologue(bshift);

    if(!Labels) {
      out.gsave();
      out.translate(bboxshift);
    }

    if(preamble) {
      // Postscript preamble.
      nodelist Nodes=preamble->nodes;
      nodelist::iterator P=Nodes.begin();
      if(P != Nodes.end()) {
        out.resetpen();
        for(; P != Nodes.end(); ++P) {
          assert(*P);
          (*P)->draw(&out);
        }
      }
    }
    out.resetpen();

    bool postscript=false;
    drawLabel *L=NULL;

    if(dvi)
      for(nodelist::const_iterator r=begin.begin(); r != begin.end(); ++r)
        (*r)->draw(&out);

    processDataStruct &pd=processData();

    for(; p != nodes.end(); ++p) {
      assert(*p);
      if(Labels && (*p)->islayer()) break;

      if(dvi && (*p)->svg()) {
        picture *f=(*p)->svgpng() ? new picture : NULL;
        nodelist::const_iterator q=layerp;
        for(;;) {
          if((*q)->beginclip())
            begin.push_back(*q);
          else if((*q)->endclip()) {
            if(begin.size() < 1)
              reportError("endclip without matching beginclip");
            begin.pop_back();
          }
          if(q == p) break;
          ++q;
        }

        if(f) {
          for(nodelist::const_iterator r=begin.begin(); r != begin.end(); ++r)
            f->append(*r);

          f->append(*(q++));
        }

        while(q != nodes.end() && !(*q)->islayer()) ++q;

        clipstack end;

        for(nodelist::const_iterator r=--q;; --r) {
          if((*r)->beginclip() && end.size() >= 1)
            end.pop_back();
          else if((*r)->endclip())
            end.push_back(*r);
          if(r == p) break;
        }

        for(nodelist::reverse_iterator r=end.rbegin(); r != end.rend();
            ++r) {
          (*r)->draw(&out);
          if(f)
            f->append(*r);
        }

        if(f) {
          ostringstream buf;
          buf << prefix << "_" << svgcount;
          ++svgcount;
          string pngname=buildname(buf.str(),"png");
          f->shipout(preamble,buf.str(),"png",false,false);
          pair m=f->bounds().Min();
          pair M=f->bounds().Max();
          delete f;

          pair size=M-m;
          ostringstream cmd;
          cmd << "\\special{dvisvgm:img " << size.getx()*ps2tex << " "
              << size.gety()*ps2tex << " " << pngname << "}";
          static pen P;
          static pair zero;
          L=new drawLabel(cmd.str(),"",identity,pair(m.getx(),M.gety()),zero,P);
          texinit();
          L->bounds(b_cached,pd.tex,labelbounds,bboxstack);
          postscript=true;
        }
        break;
      } else postscript |= (*p)->draw(&out);
    }

    if(Labels) {
      if(!svg || pdf)
        tex->beginlayer(pdf ? pdfname : psname,postscript);
    } else out.grestore();

    out.epilogue();
    out.close();

    if(Labels) {
      tex->resetpen();
      if(pdf && !b.empty) {
        status=(epstopdf(psname,pdfname) == 0);
        if(!keep) unlink(psname.c_str());
      }

      if(status) {
        for (p=layerp; p != nodes.end(); ++p) {
          assert(*p);
          bool islayer=(*p)->islayer();
          if(dvi && (*p)->svg()) {
            islayer=true;
            if((*p)->svgpng())
              L->write(tex,b);
            else
              (*p)->draw(tex);
          } else
            (*p)->write(tex,b);
          if(islayer) {
            tex->endlayer();
            layerp=++p;
            layer++;
            break;
          }
        }
      }
    }
  }

  bool context=settings::context(texengine);
  if(status) {
    if(TeXmode) {
      if(Labels && verbose > 0) cout << "Wrote " << texname << endl;
      delete tex;
    } else {
      if(Labels) {
        tex->epilogue();
        if(context) prefix=stripDir(prefix);
        delete tex;
        status=texprocess(texname,dvi ? outname : prename,prefix,
                          bboxshift,dvi);
        if(!keep) {
          for(mem::list<string>::iterator p=files.begin(); p != files.end();
              ++p)
            unlink(p->c_str());
        }
      }
      if(status) {
        if(context) prename=stripDir(prename);
        status=postprocess(prename,outname,outputformat,wait,
                           view,pdf && Labels,epsformat,svg);
        if(pdfformat && !keep) {
          unlink(auxname(prefix,"m9").c_str());
          unlink(auxname(prefix,"pbsdat").c_str());
        }
      }
    }
  }

  if(!status) reportError("shipout failed");

  if(!texengineSave.empty()) Setting("tex")=texengineSave;

  if(htmlformat) {
    jsfile out;
    out.svgtohtml(prefix);
    string name=buildname(prefix,"html");
    display(name,"html",wait,true,false);
    if(!keep)
      unlink(outname.c_str());
  }

  return true;
}

// render viewport with width x height pixels.
void picture::render(double size2, const triple& Min, const triple& Max,
                     double perspective, bool remesh) const
{
  for(nodelist::const_iterator p=nodes.begin(); p != nodes.end(); ++p) {
    assert(*p);
    if(remesh) (*p)->meshinit();
    (*p)->render(size2,Min,Max,perspective,remesh);
  }
}

#ifdef HAVE_LIBGLM
AsyRender::RenderFunctionArgs args = {};

extern bool allowRender;

void glrenderWrapper()
{
#ifdef HAVE_RENDERER
  if (gl == nullptr) {
    // Renderer not yet initialised.  In threaded mode, the asymain thread
    // may have woken us to create the renderer (to avoid a race condition
    // caused by dlopen from a non-main thread).
#ifdef HAVE_PTHREAD
    if(AsyRender::threads) {
      createRenderer();
      if(camp::gl != nullptr) {
        // Signal asymain that the renderer is ready.
        pthread_mutex_lock(&main_wait_mutex);
        pthread_cond_signal(&main_wait_cond);
        pthread_mutex_unlock(&main_wait_mutex);
      }
    }
#endif
    return;
  }
#ifdef HAVE_PTHREAD
  gl->threadMgr.wait(gl->threadMgr.initSignal,gl->threadMgr.initLock);
  gl->threadMgr.endwait(gl->threadMgr.initSignal,gl->threadMgr.initLock);
#endif
  if(allowRender)
    gl->render(args);
#endif
}
#endif // HAVE_LIBGLM

bool picture::shipout3(const string& prefix, const string& format,
                       double width, double height, double angle, double zoom,
                       const triple& m, const triple& M, const pair& shift,
                       const pair& margin, double *t, double *tup,
                       double *background,
                       size_t nlights, triple *lights, double *diffuse,
                       bool view)
{
  if(getSetting<bool>("interrupt"))
    return true;

  if(width <= 0 || height <= 0) return false;

#ifdef HAVE_LIBGLM
  initRenderer(format.c_str());
#endif

  bool webgl=format == "html";
  bool v3d=format == "v3d";

#ifndef HAVE_LIBGLM
  if(webgl || v3d)
    camp::reportError("to support html and v3d rendering, please install glm header files, then ./configure; make");
#endif

#ifndef HAVE_LIBOSMESA
#ifndef HAVE_RENDERER
  if(!webgl) {
#ifdef _WIN32
    string extra="vulkan and glslang";
#else
    string extra="either vulkan and glslang or GL";
#endif
    camp::reportError("to support onscreen rendering, please install glfw and "+extra+" development libraries, then ./configure; make");
  }
#endif
#endif


  picture *pic = new picture;

  matrixstack ms;
  for(nodelist::const_iterator p=nodes.begin(); p != nodes.end(); ++p) {
    assert(*p);
    if((*p)->begingroup3())
      ms.push((*p)->transf3());
    else if((*p)->endgroup3())
      ms.pop();
    pic->append((*p)->transformed(ms.T()));
  }

  pic->b3=bbox3();
  for(nodelist::iterator p=pic->nodes.begin(); p != pic->nodes.end(); ++p) {
    assert(*p);
    (*p)->bounds(pic->b3);
  }
  pic->lastnumber3=pic->nodes.size();

  for(nodelist::iterator p=pic->nodes.begin(); p != pic->nodes.end(); ++p) {
    assert(*p);
    (*p)->displacement();
  }

  const string outputformat=format.empty() ?
    getSetting<string>("outformat") : format;

#ifdef HAVE_LIBGLM
  static int oldpid=0;
  bool View=settings::view() && view;
#endif

#ifdef HAVE_RENDERER
  bool offscreen=false;
#ifdef HAVE_LIBOSMESA
  offscreen=!vulkan;
#endif
#ifdef HAVE_PTHREAD
  bool animating=getSetting<bool>("animating");
  bool Wait=!interact::interactive || !View || animating;
#endif
#endif

  bool format3d=webgl || v3d;
#ifdef HAVE_LIBGLM
  args.prefix=prefix;
  args.pic=pic;
  args.format=outputformat;
  args.width=width;
  args.height=height;
  args.angle=angle;
  args.zoom=zoom;
  args.m=m;
  args.M=M;
  args.shift=shift;
  args.margin=margin;
  args.t=t;
  args.tup=tup;
  args.background=background;
  args.nlightsin=nlights;
  args.lights=lights;
  args.diffuse=diffuse;
  args.view=View;
  args.oldpid=oldpid;
#endif
  if(!format3d) {
#ifdef HAVE_RENDERER
#ifdef HAVE_LIBGLM
    if(AsyRender::threads && !offscreen) {
#ifdef HAVE_PTHREAD
      if(!gl->initialized) {
        if(Wait)
          pthread_mutex_lock(&gl->threadMgr.readyLock);
        allowRender=true;
        gl->threadMgr.wait(gl->threadMgr.initSignal,gl->threadMgr.initLock);
        gl->threadMgr.endwait(gl->threadMgr.initSignal,gl->threadMgr.initLock);
        static bool initialize=true;
        if(initialize) {
          gl->threadMgr.wait(gl->threadMgr.initSignal,gl->threadMgr.initLock);
          gl->threadMgr.endwait(gl->threadMgr.initSignal,gl->threadMgr.initLock);
          initialize=false;
        }
        if(Wait) {
          pthread_cond_wait(&gl->threadMgr.readySignal,&gl->threadMgr.readyLock);
          pthread_mutex_unlock(&gl->threadMgr.readyLock);
        }
        return true;
      }
      if(Wait)
        pthread_mutex_lock(&gl->threadMgr.readyLock);
#ifdef HAVE_RENDERER
      // glfwPostEmptyEvent() only works when the render thread is inside
      // a GLFW event loop.  In headless mode (llvmpipe on macOS without
      // Metal), the render thread is blocked on initSignal, so we must
      // use the handshake instead.
      if(camp::headlessRenderer) {
        // Set up args for the render thread, then wake it via handshake.
        allowRender=true;
        gl->threadMgr.wait(gl->threadMgr.initSignal,gl->threadMgr.initLock);
        gl->threadMgr.endwait(gl->threadMgr.initSignal,gl->threadMgr.initLock);
        // The render thread has done the work.  Wait for completion
        // and return -- don't fall through to gl->render() below.
        if(Wait) {
          pthread_cond_wait(&gl->threadMgr.readySignal,&gl->threadMgr.readyLock);
          pthread_mutex_unlock(&gl->threadMgr.readyLock);
        }
        return true;
      } else {
        glfwPostEmptyEvent();
      }
#endif
#endif
    } else {
#endif
#if !defined(_WIN32)
      int pid=fork();
      if(pid == -1)
        camp::reportError("Cannot fork process");
      if(pid != 0)  {
        oldpid=pid;
        waitpid(pid,NULL,interact::interactive && View ? WNOHANG : 0);
        return true;
      }
#endif
#ifdef HAVE_LIBGLM
    }
#endif
#endif
  }

#if HAVE_LIBGLM
  gl->render(args);
  if(format3d) {
    string name=buildname(prefix,format);
    abs3Doutfile *fileObj=nullptr;

    if(webgl)
      fileObj=new jsfile(name);
    else if(v3d)
#ifdef HAVE_LIBTIRPC
      fileObj=new gzv3dfile(name,getSetting<bool>("lossy") ||
                            getSetting<double>("prerender") > 0.0);
#else
    {
    ostringstream buf;
    buf << name << ": XDR write support not enabled";
    reportError(buf);
    }
#endif

    if(fileObj) {
      for (auto& p : pic->nodes) {
        assert(p);
        p->write(fileObj);
      }

      fileObj->close();
      delete fileObj;
    }

    if(webgl && View)
      htmlView(name);

#ifdef HAVE_RENDERER
    if(gl->format3dWait) {
      gl->format3dWait=false;
#ifdef HAVE_PTHREAD
      gl->threadMgr.endwait(gl->threadMgr.initSignal,gl->threadMgr.initLock);
#endif
    }
#endif

    return true;
  }
#endif

#ifdef HAVE_RENDERER
#ifdef HAVE_PTHREAD
#ifdef HAVE_LIBGLM
  if(AsyRender::threads && !offscreen && Wait) {
    pthread_cond_wait(&gl->threadMgr.readySignal,&gl->threadMgr.readyLock);
    pthread_mutex_unlock(&gl->threadMgr.readyLock);
  }
#endif
  return true;
#endif
#endif

  return false;
}

bool picture::shipout3(const string& prefix, const string format)
{
  bounds3();
  bool status;

  string name=buildname(prefix,"prc");
  prcfile prc(name);

  static const double limit=2.5*10.0/INT_MAX;
  double compressionlimit=max(length(b3.Max()),length(b3.Min()))*limit;

  groups.push_back(groupmap());
  for(nodelist::iterator p=nodes.begin(); p != nodes.end(); ++p) {
    assert(*p);
    (*p)->write(&prc,&billboard,compressionlimit,groups);
  }
  groups.pop_back();
  status=prc.finish();

  if(!status) reportError("shipout3 failed");

  if(verbose > 0) cout << "Wrote " << name << endl;

  return true;
}

picture *picture::transformed(const transform& t)
{
  picture *pic = new picture;

  nodelist::iterator p;
  for (p = nodes.begin(); p != nodes.end(); ++p) {
    assert(*p);
    pic->append((*p)->transformed(t));
  }
  pic->T=transform(t*T);

  return pic;
}

picture *picture::transformed(const array& t)
{
  picture *pic = new picture;
  double* T=NULL;
  copyArray4x4C(T,&t);
  size_t level = 0;
  for (nodelist::iterator p = nodes.begin(); p != nodes.end(); ++p) {
    assert(*p);
    if(level==0)
      pic->append((*p)->transformed(T));
    else
      pic->append(*p);
    if((*p)->begingroup3())
      level++;
    if((*p)->endgroup3()) {
      if(level==0)
        reportError("endgroup3 without matching begingroup3");
      else
        level--;
    }
  }

  return pic;
}


} // namespace camp
