//========================================================================
//
// PDFCore.cc
//
// Copyright 2004-2013 Glyph & Cog, LLC
//
//========================================================================

#include <aconf.h>

#ifdef USE_GCC_PRAGMAS
#pragma implementation
#endif

#include <math.h>
#include "GString.h"
#include "GList.h"
#include "GlobalParams.h"
#include "Splash.h"
#include "SplashBitmap.h"
#include "SplashPattern.h"
#include "SplashPath.h"
#include "Error.h"
#include "ErrorCodes.h"
#include "PDFDoc.h"
#include "Link.h"
#include "TextOutputDev.h"
#include "CoreOutputDev.h"
#include "PDFCore.h"

//------------------------------------------------------------------------
// PDFCorePage
//------------------------------------------------------------------------

PDFCorePage::PDFCorePage(int pageA, int wA, int hA, int tileWA, int tileHA) {
  page = pageA;
  tiles = new GList();
  w = wA;
  h = hA;
  tileW = tileWA;
  tileH = tileHA;
  links = NULL;
  text = NULL;
}

PDFCorePage::~PDFCorePage() {
  deleteGList(tiles, PDFCoreTile);
  if (links) {
    delete links;
  }
  if (text) {
    delete text;
  }
}

//------------------------------------------------------------------------
// PDFCoreTile
//------------------------------------------------------------------------

PDFCoreTile::PDFCoreTile(int xDestA, int yDestA) {
  xMin = 0;
  yMin = 0;
  xMax = 0;
  yMax = 0;
  xDest = xDestA;
  yDest = yDestA;
  bitmap = NULL;
}

PDFCoreTile::~PDFCoreTile() {
  if (bitmap) {
    delete bitmap;
  }
}


//------------------------------------------------------------------------
// PDFCore
//------------------------------------------------------------------------

PDFCore::PDFCore(SplashColorMode colorModeA, int bitmapRowPadA,
		 GBool reverseVideoA, SplashColorPtr paperColorA,
		 GBool incrementalUpdate) {
  int i;

  doc = NULL;
  continuousMode = globalParams->getContinuousView();
  drawAreaWidth = drawAreaHeight = 0;
  maxPageW = totalDocH = 0;
  pageY = NULL;
  topPage = 0;
  midPage = 0;
  scrollX = scrollY = 0;
  zoom = defZoom;
  dpi = 0;
  rotate = 0;

  selectPage = 0;
  selectULX = selectLRX = 0;
  selectULY = selectLRY = 0;
  dragging = gFalse;
  lastDragLeft = lastDragTop = gTrue;
  selectXorColor[0] = selectXorColor[1] = selectXorColor[2] =
      reverseVideoA ? 0xff : 0x00;
  splashColorXor(selectXorColor, paperColorA);

  historyCur = pdfHistorySize - 1;
  historyBLen = historyFLen = 0;
  for (i = 0; i < pdfHistorySize; ++i) {
    history[i].fileName = NULL;
  }


  pages = new GList();
  curTile = NULL;

  splashColorCopy(paperColor, paperColorA);
  out = new CoreOutputDev(colorModeA, bitmapRowPadA,
			  reverseVideoA, paperColorA, incrementalUpdate,
			  &redrawCbk, this);
  out->startDoc(NULL);
}

PDFCore::~PDFCore() {
  int i;

  if (doc) {
    delete doc;
  }
  for (i = 0; i < pdfHistorySize; ++i) {
    if (history[i].fileName) {
#ifdef _WIN32
      delete[] history[i].fileName;
#else
      delete history[i].fileName;
#endif
    }
  }
  gfree(pageY);
  deleteGList(pages, PDFCorePage);
  delete out;
}

int PDFCore::loadFile(GString *fileName, GString *ownerPassword,
		      GString *userPassword) {
  int err;

  setBusyCursor(gTrue);
  err = loadFile2(new PDFDoc(fileName->copy(), ownerPassword, userPassword,
			     this));
  setBusyCursor(gFalse);
  return err;
}

#ifdef _WIN32
int PDFCore::loadFile(wchar_t *fileName, int fileNameLen,
		      GString *ownerPassword, GString *userPassword) {
  int err;

  setBusyCursor(gTrue);
  err = loadFile2(new PDFDoc(fileName, fileNameLen,
			     ownerPassword, userPassword, this));
  setBusyCursor(gFalse);
  return err;
}
#endif

int PDFCore::loadFile(BaseStream *stream, GString *ownerPassword,
		      GString *userPassword) {
  int err;

  setBusyCursor(gTrue);
  err = loadFile2(new PDFDoc(stream, ownerPassword, userPassword, this));
  setBusyCursor(gFalse);
  return err;
}

void PDFCore::loadDoc(PDFDoc *docA) {
  setBusyCursor(gTrue);
  loadFile2(docA);
  setBusyCursor(gFalse);
}

int PDFCore::loadFile2(PDFDoc *newDoc) {
  int err;
  double w, h, t;
  int i;

  // open the PDF file
  if (!newDoc->isOk()) {
    err = newDoc->getErrorCode();
    delete newDoc;
    return err;
  }

  // replace old document
  if (doc) {
    delete doc;
  }
  doc = newDoc;
  if (out) {
    out->startDoc(doc->getXRef());
  }

  // nothing displayed yet
  topPage = -99;
  midPage = -99;
  while (pages->getLength() > 0) {
    delete (PDFCorePage *)pages->del(0);
  }

  // compute the max unscaled page size
  maxUnscaledPageW = maxUnscaledPageH = 0;
  for (i = 1; i <= doc->getNumPages(); ++i) {
    w = doc->getPageCropWidth(i);
    h = doc->getPageCropHeight(i);
    if (doc->getPageRotate(i) == 90 || doc->getPageRotate(i) == 270) {
      t = w; w = h; h = t;
    }
    if (w > maxUnscaledPageW) {
      maxUnscaledPageW = w;
    }
    if (h > maxUnscaledPageH) {
      maxUnscaledPageH = h;
    }
  }

  return errNone;
}

void PDFCore::clear() {
  if (!doc) {
    return;
  }

  // no document
  delete doc;
  doc = NULL;
  out->clear();

  // no page displayed
  topPage = -99;
  midPage = -99;
  while (pages->getLength() > 0) {
    delete (PDFCorePage *)pages->del(0);
  }

  // redraw
  scrollX = scrollY = 0;
  redrawWindow(0, 0, drawAreaWidth, drawAreaHeight, gTrue);
  updateScrollbars();
}

PDFDoc *PDFCore::takeDoc(GBool redraw) {
  PDFDoc *docA;

  if (!doc) {
    return NULL;
  }

  // no document
  docA = doc;
  doc = NULL;
  out->clear();

  // no page displayed
  topPage = -99;
  midPage = -99;
  while (pages->getLength() > 0) {
    delete (PDFCorePage *)pages->del(0);
  }

  // redraw
  scrollX = scrollY = 0;
  if (redraw) {
    redrawWindow(0, 0, drawAreaWidth, drawAreaHeight, gTrue);
    updateScrollbars();
  }

  return docA;
}

void PDFCore::displayPage(int topPageA, double zoomA, int rotateA,
			  GBool scrollToTop, GBool addToHist) {
  int scrollXA, scrollYA;

  scrollXA = scrollX;
  if (continuousMode) {
    scrollYA = -1;
  } else if (scrollToTop) {
    scrollYA = 0;
  } else {
    scrollYA = scrollY;
  }
  if (zoomA != zoom) {
    scrollXA = 0;
    scrollYA = continuousMode ? -1 : 0;
  }

  dragging = gFalse;
  lastDragLeft = lastDragTop = gTrue;

  update(topPageA, scrollXA, scrollYA, zoomA, rotateA, gTrue, addToHist,
	 gTrue);
}

void PDFCore::displayDest(LinkDest *dest, double zoomA, int rotateA,
			  GBool addToHist) {
  Ref pageRef;
  int topPageA;
  int dx, dy, scrollXA, scrollYA;

  if (dest->isPageRef()) {
    pageRef = dest->getPageRef();
    topPageA = doc->findPage(pageRef.num, pageRef.gen);
  } else {
    topPageA = dest->getPageNum();
  }
  if (topPageA <= 0 || topPageA > doc->getNumPages()) {
    topPageA = 1;
  }
  scrollXA = scrollX;
  scrollYA = continuousMode ? -1 : scrollY;
  switch (dest->getKind()) {
  case destXYZ:
    cvtUserToDev(topPageA, dest->getLeft(), dest->getTop(), &dx, &dy);
    scrollXA = dest->getChangeLeft() ? dx : scrollX;
    if (continuousMode) {
      if (topPage <= 0) {
	scrollYA = -1;
      } else if (dest->getChangeTop()) {
	scrollYA = pageY[topPageA - 1] + dy;
      } else {
	scrollYA = pageY[topPageA - 1] + (scrollY - pageY[topPage - 1]);
      }
    } else {
      if (dest->getChangeTop()) {
	scrollYA = dy;
      } else if (topPage > 0) {
	scrollYA = scrollY;
      } else {
	scrollYA = 0;
      }
    }
    //~ this doesn't currently handle the zoom parameter
    update(topPageA, scrollXA, scrollYA, zoom, rotate, gFalse,
	   addToHist && topPageA != topPage, gTrue);
    break;
  case destFit:
  case destFitB:
    scrollXA = 0;
    scrollYA = continuousMode ? -1 : 0;
    update(topPageA, scrollXA, scrollYA, zoomPage, rotate, gFalse,
	   addToHist && topPageA != topPage, gTrue);
    break;
  case destFitH:
  case destFitBH:
    //~ do fit: need a function similar to zoomToRect which will
    //~ accept an absolute top coordinate (rather than centering)
    scrollXA = 0;
    cvtUserToDev(topPageA, 0, dest->getTop(), &dx, &dy);
    if (continuousMode) {
      if (topPage <= 0) {
	scrollYA = -1;
      } else if (dest->getChangeTop()) {
	scrollYA = pageY[topPageA - 1] + dy;
      } else {
	scrollYA = pageY[topPageA - 1] + (scrollY - pageY[topPage - 1]);
      }
    } else {
      if (dest->getChangeTop()) {
	scrollYA = dy;
      } else if (topPage > 0) {
	scrollYA = scrollY;
      } else {
	scrollYA = 0;
      }
    }
    update(topPageA, scrollXA, scrollYA, zoom, rotate, gFalse,
	   addToHist && topPageA != topPage, gTrue);
    break;
  case destFitV:
  case destFitBV:
    //~ do fit: need a function similar to zoomToRect which will
    //~ accept an absolute left coordinate (rather than centering)
    if (dest->getChangeLeft()) {
      cvtUserToDev(topPageA, dest->getLeft(), 0, &dx, &dy);
      scrollXA = dx;
    } else {
      scrollXA = scrollX;
    }
    scrollYA = continuousMode ? -1 : 0;
    update(topPageA, scrollXA, scrollYA, zoom, rotate, gFalse,
	   addToHist && topPageA != topPage, gTrue);
    break;
  case destFitR:
    zoomToRect(topPageA, dest->getLeft(), dest->getTop(),
	       dest->getRight(), dest->getBottom());
    break;
  }
}

void PDFCore::update(int topPageA, int scrollXA, int scrollYA,
		     double zoomA, int rotateA, GBool force,
		     GBool addToHist, GBool adjustScrollX) {
  double hDPI, vDPI, dpiA, uw, uh, ut;
  int w, h, t, x0, x1, y0, y1, x, y;
  int rot;
  int pg0, pg1;
  PDFCoreTile *tile;
  PDFCorePage *page;
  PDFHistory *hist;
  GBool needUpdate;
  int i, j;

  // check for document and valid page number
  if (!doc) {
    // save the new settings
    zoom = zoomA;
    rotate = rotateA;
    return;
  }
  if (topPageA <= 0 || topPageA > doc->getNumPages()) {
    return;
  }

  needUpdate = gFalse;

  // check for changes to the PDF file
  if ((force || (!continuousMode && topPage != topPageA)) &&
      doc->getFileName() &&
      checkForNewFile()) {
    if (loadFile(doc->getFileName()) == errNone) {
      if (topPageA > doc->getNumPages()) {
	topPageA = doc->getNumPages();
      }
      needUpdate = gTrue;
    }
  }

  // compute the DPI
  if (continuousMode) {
    uw = maxUnscaledPageW;
    uh = maxUnscaledPageH;
    rot = rotateA;
  } else {
    uw = doc->getPageCropWidth(topPageA);
    uh = doc->getPageCropHeight(topPageA);
    rot = rotateA + doc->getPageRotate(topPageA);
    if (rot >= 360) {
      rot -= 360;
    } else if (rot < 0) {
      rot += 360;
    }
  }
  if (rot == 90 || rot == 270) {
    ut = uw; uw = uh; uh = ut;
  }
  if (zoomA == zoomPage) {
    hDPI = (drawAreaWidth / uw) * 72;
    if (continuousMode) {
      vDPI = ((drawAreaHeight - continuousModePageSpacing) / uh) * 72;
    } else {
      vDPI = (drawAreaHeight / uh) * 72;
    }
    dpiA = (hDPI < vDPI) ? hDPI : vDPI;
  } else if (zoomA == zoomWidth) {
    dpiA = (drawAreaWidth / uw) * 72;
  } else {
    dpiA = 0.01 * zoomA * 72;
  }
  // this can happen if the window hasn't been sized yet
  if (dpiA <= 0) {
    dpiA = 1;
  }

  // if the display properties have changed, create a new PDFCorePage
  // object
  if (force || pages->getLength() == 0 ||
      (!continuousMode && topPageA != topPage) ||
      fabs(zoomA - zoom) > 1e-8 || fabs(dpiA - dpi) > 1e-8 ||
      rotateA != rotate) {
    needUpdate = gTrue;
    setSelection(0, 0, 0, 0, 0);
    while (pages->getLength() > 0) {
      delete (PDFCorePage *)pages->del(0);
    }
    zoom = zoomA;
    rotate = rotateA;
    dpi = dpiA;
    if (continuousMode) {
      maxPageW = totalDocH = 0;
      pageY = (int *)greallocn(pageY, doc->getNumPages(), sizeof(int));
      for (i = 1; i <= doc->getNumPages(); ++i) {
	pageY[i-1] = totalDocH;
	w = (int)((doc->getPageCropWidth(i) * dpi) / 72 + 0.5);
	h = (int)((doc->getPageCropHeight(i) * dpi) / 72 + 0.5);
	rot = rotate + doc->getPageRotate(i);
	if (rot >= 360) {
	  rot -= 360;
	} else if (rot < 0) {
	  rot += 360;
	}
	if (rot == 90 || rot == 270) {
	  t = w; w = h; h = t;
	}
	if (w > maxPageW) {
	  maxPageW = w;
	}
	totalDocH += h;
	if (i < doc->getNumPages()) {
	  totalDocH += continuousModePageSpacing;
	}
      }
    } else {
      rot = rotate + doc->getPageRotate(topPageA);
      if (rot >= 360) {
	rot -= 360;
      } else if (rot < 0) {
	rot += 360;
      }
      addPage(topPageA, rot);
    }
  } else {
    // erase the selection
    if (selectULX != selectLRX && selectULY != selectLRY) {
      xorRectangle(selectPage, selectULX, selectULY, selectLRX, selectLRY,
		   new SplashSolidColor(selectXorColor));
    }
  }
  if (continuousMode) {
    page = NULL; // make gcc happy
  } else {
    page = (PDFCorePage *)pages->get(0);
  }
  topPage = topPageA;
  midPage = topPage;

  // adjust the scroll position
  scrollX = scrollXA;
  if (continuousMode && scrollYA < 0) {
    scrollY = pageY[topPage - 1];
  } else {
    scrollY = scrollYA;
  }
  if (continuousMode && adjustScrollX) {
    rot = rotate + doc->getPageRotate(topPage);
    if (rot >= 360) {
      rot -= 360;
    } else if (rot < 0) {
      rot += 360;
    }
    if (rot == 90 || rot == 270) {
      w = (int)((doc->getPageCropHeight(topPage) * dpi) / 72 + 0.5);
    } else {
      w = (int)((doc->getPageCropWidth(topPage) * dpi) / 72 + 0.5);
    }
    if (scrollX < (maxPageW - w) / 2) {
      scrollX = (maxPageW - w) / 2;
    }
  }
  w = continuousMode ? maxPageW : page->w;
  if (scrollX > w - drawAreaWidth) {
    scrollX = w - drawAreaWidth;
  }
  if (scrollX < 0) {
    scrollX = 0;
  }
  h = continuousMode ? totalDocH : page->h;
  if (scrollY > h - drawAreaHeight) {
    scrollY = h - drawAreaHeight;
  }
  if (scrollY < 0) {
    scrollY = 0;
  }

  // find topPage, and the first and last pages to be rasterized
  if (continuousMode) {
    //~ should use a binary search
    for (i = 2; i <= doc->getNumPages(); ++i) {
      if (pageY[i-1] > scrollY - drawAreaHeight / 2) {
	break;
      }
    }
    pg0 = i - 1;
    for (i = pg0 + 1; i <= doc->getNumPages(); ++i) {
      if (pageY[i-1] > scrollY) {
	break;
      }
    }
    topPage = i - 1;
    for (i = topPage + 1; i <= doc->getNumPages(); ++i) {
      if (pageY[i-1] > scrollY + drawAreaHeight / 2) {
	break;
      }
    }
    midPage = i - 1;
    for (i = midPage + 1; i <= doc->getNumPages(); ++i) {
      if (pageY[i-1] > scrollY + drawAreaHeight + drawAreaHeight / 2) {
	break;
      }
    }
    pg1 = i - 1;

    // delete pages that are no longer needed and insert new pages
    // objects that are needed
    while (pages->getLength() > 0 &&
	   ((PDFCorePage *)pages->get(0))->page < pg0) {
      delete (PDFCorePage *)pages->del(0);
    }
    i = pages->getLength() - 1;
    while (i > 0 && ((PDFCorePage *)pages->get(i))->page > pg1) {
      delete (PDFCorePage *)pages->del(i--);
    }
    j = pages->getLength() > 0 ? ((PDFCorePage *)pages->get(0))->page - 1
                               : pg1;
    for (i = pg0; i <= j; ++i) {
      rot = rotate + doc->getPageRotate(i);
      if (rot >= 360) {
	rot -= 360;
      } else if (rot < 0) {
	rot += 360;
      }
      addPage(i, rot);
    }      
    j = ((PDFCorePage *)pages->get(pages->getLength() - 1))->page;
    for (i = j + 1; i <= pg1; ++i) {
      rot = rotate + doc->getPageRotate(i);
      if (rot >= 360) {
	rot -= 360;
      } else if (rot < 0) {
	rot += 360;
      }
      addPage(i, rot);
    }

  } else {
    pg0 = pg1 = topPage;
  }

  // delete tiles that are no longer needed
  for (i = 0; i < pages->getLength(); ++i) {
    page = (PDFCorePage *)pages->get(i);
    j = 0;
    while (j < page->tiles->getLength()) {
      tile = (PDFCoreTile *)page->tiles->get(j);
      if (continuousMode) {
	y0 = pageY[page->page - 1] + tile->yMin;
	y1 = pageY[page->page - 1] + tile->yMax;
      } else {
	y0 = tile->yMin;
	y1 = tile->yMax;
      }
      if (tile->xMax < scrollX - drawAreaWidth / 2 ||
	  tile->xMin > scrollX + drawAreaWidth + drawAreaWidth / 2 ||
	  y1 < scrollY - drawAreaHeight / 2 ||
	  y0 > scrollY + drawAreaHeight + drawAreaHeight / 2) {
	delete (PDFCoreTile *)page->tiles->del(j);
      } else {
	++j;
      }
    }
  }

  // update page positions
  for (i = 0; i < pages->getLength(); ++i) {
    page = (PDFCorePage *)pages->get(i);
    page->xDest = -scrollX;
    if (continuousMode) {
      page->yDest = pageY[page->page - 1] - scrollY;
    } else {
      page->yDest = -scrollY;
    }
    if (continuousMode) {
      if (page->w < maxPageW) {
	page->xDest += (maxPageW - page->w) / 2;
      }
      if (maxPageW < drawAreaWidth) {
	page->xDest += (drawAreaWidth - maxPageW) / 2;
      }
    } else if (page->w < drawAreaWidth) {
      page->xDest += (drawAreaWidth - page->w) / 2;
    }
    if (continuousMode && totalDocH < drawAreaHeight) {
      page->yDest += (drawAreaHeight - totalDocH) / 2;
    } else if (!continuousMode && page->h < drawAreaHeight) {
      page->yDest += (drawAreaHeight - page->h) / 2;
    }
  }

  // rasterize any new tiles
  for (i = 0; i < pages->getLength(); ++i) {
    page = (PDFCorePage *)pages->get(i);
    x0 = page->xDest;
    x1 = x0 + page->w - 1;
    if (x0 < -drawAreaWidth / 2) {
      x0 = -drawAreaWidth / 2;
    }
    if (x1 > drawAreaWidth + drawAreaWidth / 2) {
      x1 = drawAreaWidth + drawAreaWidth / 2;
    }
    x0 = ((x0 - page->xDest) / page->tileW) * page->tileW;
    x1 = ((x1 - page->xDest) / page->tileW) * page->tileW;
    y0 = page->yDest;
    y1 = y0 + page->h - 1;
    if (y0 < -drawAreaHeight / 2) {
      y0 = -drawAreaHeight / 2;
    }
    if (y1 > drawAreaHeight + drawAreaHeight / 2) {
      y1 = drawAreaHeight + drawAreaHeight / 2;
    }
    y0 = ((y0 - page->yDest) / page->tileH) * page->tileH;
    y1 = ((y1 - page->yDest) / page->tileH) * page->tileH;
    for (y = y0; y <= y1; y += page->tileH) {
      for (x = x0; x <= x1; x += page->tileW) {
	needTile(page, x, y);
      }
    }
  }

  // update tile positions
  for (i = 0; i < pages->getLength(); ++i) {
    page = (PDFCorePage *)pages->get(i);
    for (j = 0; j < page->tiles->getLength(); ++j) {
      tile = (PDFCoreTile *)page->tiles->get(j);
      tile->xDest = tile->xMin - scrollX;
      if (continuousMode) {
	tile->yDest = tile->yMin + pageY[page->page - 1] - scrollY;
      } else {
	tile->yDest = tile->yMin - scrollY;
      }
      if (continuousMode) {
	if (page->w < maxPageW) {
	  tile->xDest += (maxPageW - page->w) / 2;
	}
	if (maxPageW < drawAreaWidth) {
	  tile->xDest += (drawAreaWidth - maxPageW) / 2;
	}
      } else if (page->w < drawAreaWidth) {
	tile->xDest += (drawAreaWidth - page->w) / 2;
      }
      if (continuousMode && totalDocH < drawAreaHeight) {
	tile->yDest += (drawAreaHeight - totalDocH) / 2;
      } else if (!continuousMode && page->h < drawAreaHeight) {
	tile->yDest += (drawAreaHeight - page->h) / 2;
      }
    }
  }

  // redraw the selection
  if (selectULX != selectLRX && selectULY != selectLRY) {
    xorRectangle(selectPage, selectULX, selectULY, selectLRX, selectLRY,
		 new SplashSolidColor(selectXorColor));
  }

  // redraw the window
  redrawWindow(0, 0, drawAreaWidth, drawAreaHeight, needUpdate);
  updateScrollbars();

  // add to history
  if (addToHist) {
    if (++historyCur == pdfHistorySize) {
      historyCur = 0;
    }
    hist = &history[historyCur];
    if (hist->fileName) {
#ifdef _WIN32
      delete[] hist->fileName;
#else
      delete hist->fileName;
#endif
    }
#ifdef _WIN32
    if (doc->getFileNameU()) {
      hist->fileName = (wchar_t *)gmallocn(MAX_PATH + 1, sizeof(wchar_t));
      if (GetFullPathNameW(doc->getFileNameU(), MAX_PATH + 1,
			   hist->fileName, NULL) == 0) {
	delete[] hist->fileName;
	hist->fileName = NULL;
      }
    } else {
      hist->fileName = NULL;
    }
#else
    if (doc->getFileName()) {
      hist->fileName = doc->getFileName()->copy();
    } else {
      hist->fileName = NULL;
    }
#endif
    hist->page = topPage;
    if (historyBLen < pdfHistorySize) {
      ++historyBLen;
    }
    historyFLen = 0;
  }
}

void PDFCore::addPage(int pg, int rot) {
  PDFCorePage *page;
  int w, h, t, tileW, tileH, i;

  w = (int)((doc->getPageCropWidth(pg) * dpi) / 72 + 0.5);
  h = (int)((doc->getPageCropHeight(pg) * dpi) / 72 + 0.5);
  if (rot == 90 || rot == 270) {
    t = w; w = h; h = t;
  }
  tileW = 2 * drawAreaWidth;
  if (tileW < 1500) {
    tileW = 1500;
  }
  if (tileW > w) {
    // tileW can't be zero -- we end up with div-by-zero problems
    tileW = w ? w : 1;
  }
  tileH = 2 * drawAreaHeight;
  if (tileH < 1500) {
    tileH = 1500;
  }
  if (tileH > h) {
    // tileH can't be zero -- we end up with div-by-zero problems
    tileH = h ? h : 1;
  }
  page = new PDFCorePage(pg, w, h, tileW, tileH);
  for (i = 0;
       i < pages->getLength() && pg > ((PDFCorePage *)pages->get(i))->page;
       ++i) ;
  pages->insert(i, page);
}

void PDFCore::needTile(PDFCorePage *page, int x, int y) {
  PDFCoreTile *tile;
  TextOutputControl textOutCtrl;
  TextOutputDev *textOut;
  int xDest, yDest, sliceW, sliceH;
  int i;

  for (i = 0; i < page->tiles->getLength(); ++i) {
    tile = (PDFCoreTile *)page->tiles->get(i);
    if (x == tile->xMin && y == tile->yMin) {
      return;
    }
  }

  setBusyCursor(gTrue);

  sliceW = page->tileW;
  if (x + sliceW > page->w) {
    sliceW = page->w - x;
  }
  sliceH = page->tileH;
  if (y + sliceH > page->h) {
    sliceH = page->h - y;
  }

  xDest = x - scrollX;
  if (continuousMode) {
    yDest = y + pageY[page->page - 1] - scrollY;
  } else {
    yDest = y - scrollY;
  }
  if (continuousMode) {
    if (page->w < maxPageW) {
      xDest += (maxPageW - page->w) / 2;
    }
    if (maxPageW < drawAreaWidth) {
      xDest += (drawAreaWidth - maxPageW) / 2;
    }
  } else if (page->w < drawAreaWidth) {
    xDest += (drawAreaWidth - page->w) / 2;
  }
  if (continuousMode && totalDocH < drawAreaHeight) {
    yDest += (drawAreaHeight - totalDocH) / 2;
  } else if (!continuousMode && page->h < drawAreaHeight) {
    yDest += (drawAreaHeight - page->h) / 2;
  }
  curTile = tile = newTile(xDest, yDest);
  curPage = page;
  tile->xMin = x;
  tile->yMin = y;
  tile->xMax = x + sliceW;
  tile->yMax = y + sliceH;
  tile->edges = 0;
  if (tile->xMin == 0) {
    tile->edges |= pdfCoreTileLeftEdge;
  }
  if (tile->xMax == page->w) {
    tile->edges |= pdfCoreTileRightEdge;
  }
  if (continuousMode) {
    if (tile->yMin == 0) {
      tile->edges |= pdfCoreTileTopSpace;
      if (page->page == 1) {
	tile->edges |= pdfCoreTileTopEdge;
      }
    }
    if (tile->yMax == page->h) {
      tile->edges |= pdfCoreTileBottomSpace;
      if (page->page == doc->getNumPages()) {
	tile->edges |= pdfCoreTileBottomEdge;
      }
    }
  } else {
    if (tile->yMin == 0) {
      tile->edges |= pdfCoreTileTopEdge;
    }
    if (tile->yMax == page->h) {
      tile->edges |= pdfCoreTileBottomEdge;
    }
  }
  doc->displayPageSlice(out, page->page, dpi, dpi, rotate,
			gFalse, gTrue, gFalse, x, y, sliceW, sliceH);
  tile->bitmap = out->takeBitmap();
  memcpy(tile->ctm, out->getDefCTM(), 6 * sizeof(double));
  memcpy(tile->ictm, out->getDefICTM(), 6 * sizeof(double));
  if (!page->links) {
    page->links = doc->getLinks(page->page);
  }
  if (!page->text) {
    textOutCtrl.mode = textOutPhysLayout;
    if ((textOut = new TextOutputDev(NULL, &textOutCtrl, gFalse))) {
      doc->displayPage(textOut, page->page, dpi, dpi, rotate,
		       gFalse, gTrue, gFalse);
      page->text = textOut->takeText();
      delete textOut;
    }
  }
  page->tiles->append(tile);
  curTile = NULL;
  curPage = NULL;

  setBusyCursor(gFalse);
}

GBool PDFCore::gotoNextPage(int inc, GBool top) {
  int pg, scrollYA;

  if (!doc || doc->getNumPages() == 0 || topPage >= doc->getNumPages()) {
    return gFalse;
  }
  if ((pg = topPage + inc) > doc->getNumPages()) {
    pg = doc->getNumPages();
  }
  if (continuousMode) {
    scrollYA = -1;
  } else if (top) {
    scrollYA = 0;
  } else {
    scrollYA = scrollY;
  }
  update(pg, scrollX, scrollYA, zoom, rotate, gFalse, gTrue, gTrue);
  return gTrue;
}

GBool PDFCore::gotoPrevPage(int dec, GBool top, GBool bottom) {
  int pg, scrollYA;

  if (!doc || doc->getNumPages() == 0 || topPage <= 1) {
    return gFalse;
  }
  if ((pg = topPage - dec) < 1) {
    pg = 1;
  }
  if (continuousMode) {
    scrollYA = -1;
  } else if (top) {
    scrollYA = 0;
  } else if (bottom) {
    scrollYA = ((PDFCorePage *)pages->get(0))->h - drawAreaHeight;
    if (scrollYA < 0) {
      scrollYA = 0;
    }
  } else {
    scrollYA = scrollY;
  }
  update(pg, scrollX, scrollYA, zoom, rotate, gFalse, gTrue, gTrue);
  return gTrue;
}

GBool PDFCore::gotoNamedDestination(GString *dest) {
  LinkDest *d;

  if (!doc) {
    return gFalse;
  }
  if (!(d = doc->findDest(dest))) {
    return gFalse;
  }
  displayDest(d, zoom, rotate, gTrue);
  delete d;
  return gTrue;
}

GBool PDFCore::goForward() {
  int pg;

  if (historyFLen == 0) {
    return gFalse;
  }
  if (++historyCur == pdfHistorySize) {
    historyCur = 0;
  }
  --historyFLen;
  ++historyBLen;
  if (!history[historyCur].fileName) {
    return gFalse;
  }
#ifdef _WIN32
  if (!doc ||
      !doc->getFileNameU() ||
      wcscmp(history[historyCur].fileName, doc->getFileNameU()) != 0) {
    if (loadFile(history[historyCur].fileName,
		 wcslen(history[historyCur].fileName)) != errNone) {
      return gFalse;
    }
  }
#else
  if (!doc ||
      !doc->getFileName() ||
      history[historyCur].fileName->cmp(doc->getFileName()) != 0) {
    if (loadFile(history[historyCur].fileName) != errNone) {
      return gFalse;
    }
  }
#endif
  pg = history[historyCur].page;
  update(pg, scrollX, continuousMode ? -1 : scrollY,
	 zoom, rotate, gFalse, gFalse, gTrue);
  return gTrue;
}

GBool PDFCore::goBackward() {
  int pg;

  if (historyBLen <= 1) {
    return gFalse;
  }
  if (--historyCur < 0) {
    historyCur = pdfHistorySize - 1;
  }
  --historyBLen;
  ++historyFLen;
  if (!history[historyCur].fileName) {
    return gFalse;
  }
#ifdef _WIN32
  if (!doc ||
      !doc->getFileNameU() ||
      wcscmp(history[historyCur].fileName, doc->getFileNameU()) != 0) {
    if (loadFile(history[historyCur].fileName,
		 wcslen(history[historyCur].fileName)) != errNone) {
      return gFalse;
    }
  }
#else
  if (!doc ||
      !doc->getFileName() ||
      history[historyCur].fileName->cmp(doc->getFileName()) != 0) {
    if (loadFile(history[historyCur].fileName) != errNone) {
      return gFalse;
    }
  }
#endif
  pg = history[historyCur].page;
  update(pg, scrollX, continuousMode ? -1 : scrollY,
	 zoom, rotate, gFalse, gFalse, gTrue);
  return gTrue;
}

void PDFCore::scrollLeft(int nCols) {
  scrollTo(scrollX - nCols, scrollY);
}

void PDFCore::scrollRight(int nCols) {
  scrollTo(scrollX + nCols, scrollY);
}

void PDFCore::scrollUp(int nLines) {
  scrollTo(scrollX, scrollY - nLines);
}

void PDFCore::scrollUpPrevPage(int nLines) {
  if (!continuousMode && scrollY == 0) {
    gotoPrevPage(1, gFalse, gTrue);
  } else {
    scrollTo(scrollX, scrollY - nLines);
  }
}

void PDFCore::scrollDown(int nLines) {
  scrollTo(scrollX, scrollY + nLines);
}

void PDFCore::scrollDownNextPage(int nLines) {
  if (!continuousMode &&
      scrollY >= ((PDFCorePage *)pages->get(0))->h - drawAreaHeight) {
    gotoNextPage(1, gTrue);
  } else {
    scrollTo(scrollX, scrollY + nLines);
  }
}

void PDFCore::scrollPageUp() {
  if (!continuousMode && scrollY == 0) {
    gotoPrevPage(1, gFalse, gTrue);
  } else {
    scrollTo(scrollX, scrollY - drawAreaHeight);
  }
}

void PDFCore::scrollPageDown() {
  if (!continuousMode &&
      pages->getLength() > 0 &&
      scrollY >= ((PDFCorePage *)pages->get(0))->h - drawAreaHeight) {
    gotoNextPage(1, gTrue);
  } else {
    scrollTo(scrollX, scrollY + drawAreaHeight);
  }
}

void PDFCore::scrollTo(int x, int y) {
  update(topPage, x, y < 0 ? 0 : y, zoom, rotate, gFalse, gFalse, gFalse);
}

void PDFCore::scrollToLeftEdge() {
  update(topPage, 0, scrollY, zoom, rotate, gFalse, gFalse, gFalse);
}

void PDFCore::scrollToRightEdge() {
  PDFCorePage *page;

  page = (PDFCorePage *)pages->get(0);
  update(topPage, page->w - drawAreaWidth, scrollY,
	 zoom, rotate, gFalse, gFalse, gFalse);
}

void PDFCore::scrollToTopEdge() {
  int y;

  y = continuousMode ? pageY[topPage - 1] : 0;
  update(topPage, scrollX, y, zoom, rotate, gFalse, gFalse, gFalse);
}

void PDFCore::scrollToBottomEdge() {
  PDFCorePage *page;
  int y, i;

  for (i = pages->getLength() - 1; i > 0; --i) {
    page = (PDFCorePage *)pages->get(i);
    if (page->yDest < drawAreaHeight) {
      break;
    }
  }
  page = (PDFCorePage *)pages->get(i);
  if (continuousMode) {
    y = pageY[page->page - 1] + page->h - drawAreaHeight;
  } else {
    y = page->h - drawAreaHeight;
  }
  update(topPage, scrollX, y, zoom, rotate, gFalse, gFalse, gFalse);
}

void PDFCore::scrollToTopLeft() {
  int y;

  y = continuousMode ? pageY[topPage - 1] : 0;
  update(topPage, 0, y, zoom, rotate, gFalse, gFalse, gFalse);
}

void PDFCore::scrollToBottomRight() {
  PDFCorePage *page;
  int x, y, i;

  for (i = pages->getLength() - 1; i > 0; --i) {
    page = (PDFCorePage *)pages->get(i);
    if (page->yDest < drawAreaHeight) {
      break;
    }
  }
  page = (PDFCorePage *)pages->get(i);
  x = page->w - drawAreaWidth;
  if (continuousMode) {
    y = pageY[page->page - 1] + page->h - drawAreaHeight;
  } else {
    y = page->h - drawAreaHeight;
  }
  update(topPage, x, y, zoom, rotate, gFalse, gFalse, gFalse);
}

void PDFCore::zoomToRect(int pg, double ulx, double uly,
			 double lrx, double lry) {
  int x0, y0, x1, y1, u, sx, sy;
  double rx, ry, newZoom, t;
  PDFCorePage *p;

  cvtUserToDev(pg, ulx, uly, &x0, &y0);
  cvtUserToDev(pg, lrx, lry, &x1, &y1);
  if (x0 > x1) {
    u = x0; x0 = x1; x1 = u;
  }
  if (y0 > y1) {
    u = y0; y0 = y1; y1 = u;
  }
  rx = (double)drawAreaWidth / (double)(x1 - x0);
  ry = (double)drawAreaHeight / (double)(y1 - y0);
  if (rx < ry) {
    newZoom = rx * (dpi / (0.01 * 72));
    sx = (int)(rx * x0);
    t = (drawAreaHeight * (x1 - x0)) / drawAreaWidth;
    sy = (int)(rx * (y0 + y1 - t) / 2);
    if (continuousMode) {
      if ((p = findPage(pg)) && p->w < maxPageW) {
	sx += (int)(0.5 * rx * (maxPageW - p->w));
      }
      u = (pg - 1) * continuousModePageSpacing;
      sy += (int)(rx * (pageY[pg - 1] - u)) + u;
    }
  } else {
    newZoom = ry * (dpi / (0.01 * 72));
    t = (drawAreaWidth * (y1 - y0)) / drawAreaHeight;
    sx = (int)(ry * (x0 + x1 - t) / 2);
    sy = (int)(ry * y0);
    if (continuousMode) {
      if ((p = findPage(pg)) && p->w < maxPageW) {
	sx += (int)(0.5 * rx * (maxPageW - p->w));
      }
      u = (pg - 1) * continuousModePageSpacing;
      sy += (int)(ry * (pageY[pg - 1] - u)) + u;
    }
  }
  update(pg, sx, sy, newZoom, rotate, gFalse, gFalse, gFalse);
}

void PDFCore::zoomCentered(double zoomA) {
  int sx, sy, rot, hAdjust, vAdjust, i;
  double dpi1, dpi2, pageW, pageH;
  PDFCorePage *page;

  if (zoomA == zoomPage) {
    if (continuousMode) {
      pageW = (rotate == 90 || rotate == 270) ? maxUnscaledPageH
	                                      : maxUnscaledPageW;
      pageH = (rotate == 90 || rotate == 270) ? maxUnscaledPageW
	                                      : maxUnscaledPageH;
      dpi1 = 72.0 * (double)drawAreaWidth / pageW;
      dpi2 = 72.0 * (double)(drawAreaHeight - continuousModePageSpacing) /
	     pageH;
      if (dpi2 < dpi1) {
	dpi1 = dpi2;
      }
    } else {
      // in single-page mode, sx=sy=0 -- so dpi1 is irrelevant
      dpi1 = dpi;
    }
    sx = 0;

  } else if (zoomA == zoomWidth) {
    if (continuousMode) {
      pageW = (rotate == 90 || rotate == 270) ? maxUnscaledPageH
	                                      : maxUnscaledPageW;
    } else {
      rot = rotate + doc->getPageRotate(topPage);
      if (rot >= 360) {
	rot -= 360;
      } else if (rot < 0) {
	rot += 360;
      }
      pageW = (rot == 90 || rot == 270) ? doc->getPageCropHeight(topPage)
	                                : doc->getPageCropWidth(topPage);
    }
    dpi1 = 72.0 * (double)drawAreaWidth / pageW;
    sx = 0;

  } else if (zoomA <= 0) {
    return;

  } else {
    dpi1 = 72.0 * zoomA / 100.0;
    if ((page = (PDFCorePage *)pages->get(0)) && page->xDest > 0) {
      hAdjust = page->xDest;
    } else {
      hAdjust = 0;
    }
    sx = (int)((scrollX - hAdjust + drawAreaWidth / 2) * (dpi1 / dpi)) -
         drawAreaWidth / 2;
    if (sx < 0) {
      sx = 0;
    }
  }

  if (continuousMode) {
    // we can't just multiply scrollY by dpi1/dpi -- the rounding
    // errors add up (because the pageY values are integers) -- so
    // we compute the pageY values at the new zoom level instead
    sy = 0;
    for (i = 1; i < topPage; ++i) {
      rot = rotate + doc->getPageRotate(i);
      if (rot >= 360) {
	rot -= 360;
      } else if (rot < 0) {
	rot += 360;
      }
      if (rot == 90 || rot == 270) {
	sy += (int)((doc->getPageCropWidth(i) * dpi1) / 72 + 0.5);
      } else {
	sy += (int)((doc->getPageCropHeight(i) * dpi1) / 72 + 0.5);
      }
    }
    vAdjust = (topPage - 1) * continuousModePageSpacing;
    sy = sy + (int)((scrollY - pageY[topPage - 1] + drawAreaHeight / 2)
		    * (dpi1 / dpi))
         + vAdjust - drawAreaHeight / 2;
  } else {
    sy = (int)((scrollY + drawAreaHeight / 2) * (dpi1 / dpi))
         - drawAreaHeight / 2;
  }

  update(topPage, sx, sy, zoomA, rotate, gFalse, gFalse, gFalse);
}

// Zoom so that the current page(s) fill the window width.  Maintain
// the vertical center.
void PDFCore::zoomToCurrentWidth() {
  double w, maxW, dpi1;
  int sx, sy, vAdjust, rot, i;

  // compute the maximum page width of visible pages
  rot = rotate + doc->getPageRotate(topPage);
  if (rot >= 360) {
    rot -= 360;
  } else if (rot < 0) {
    rot += 360;
  }
  if (rot == 90 || rot == 270) {
    maxW = doc->getPageCropHeight(topPage);
  } else {
    maxW = doc->getPageCropWidth(topPage);
  }
  if (continuousMode) {
    for (i = topPage + 1;
	 i < doc->getNumPages() && pageY[i-1] < scrollY + drawAreaHeight;
	 ++i) {
      rot = rotate + doc->getPageRotate(i);
      if (rot >= 360) {
	rot -= 360;
      } else if (rot < 0) {
	rot += 360;
      }
      if (rot == 90 || rot == 270) {
	w = doc->getPageCropHeight(i);
      } else {
	w = doc->getPageCropWidth(i);
      }
      if (w > maxW) {
	maxW = w;
      }
    }
  }

  // compute the resolution
  dpi1 = (drawAreaWidth / maxW) * 72;

  // compute the horizontal scroll position
  if (continuousMode) {
    sx = ((int)(maxPageW * dpi1 / dpi) - drawAreaWidth) / 2;
  } else {
    sx = 0;
  }

  // compute the vertical scroll position
  if (continuousMode) {
    // we can't just multiply scrollY by dpi1/dpi -- the rounding
    // errors add up (because the pageY values are integers) -- so
    // we compute the pageY values at the new zoom level instead
    sy = 0;
    for (i = 1; i < topPage; ++i) {
      rot = rotate + doc->getPageRotate(i);
      if (rot >= 360) {
	rot -= 360;
      } else if (rot < 0) {
	rot += 360;
      }
      if (rot == 90 || rot == 270) {
	sy += (int)((doc->getPageCropWidth(i) * dpi1) / 72 + 0.5);
      } else {
	sy += (int)((doc->getPageCropHeight(i) * dpi1) / 72 + 0.5);
      }
    }
    vAdjust = (topPage - 1) * continuousModePageSpacing;
    sy = sy + (int)((scrollY - pageY[topPage - 1] + drawAreaHeight / 2)
		    * (dpi1 / dpi))
         + vAdjust - drawAreaHeight / 2;
  } else {
    sy = (int)((scrollY + drawAreaHeight / 2) * (dpi1 / dpi))
         - drawAreaHeight / 2;
  }

  update(topPage, sx, sy, (dpi1 * 100) / 72, rotate, gFalse, gFalse, gFalse);
}

void PDFCore::setContinuousMode(GBool cm) {
  if (continuousMode != cm) {
    continuousMode = cm;
    update(topPage, scrollX, -1, zoom, rotate, gTrue, gFalse, gTrue);
  }
}

void PDFCore::setSelectionColor(SplashColor color) {
  splashColorCopy(selectXorColor, color);
  splashColorXor(selectXorColor, paperColor);
}

void PDFCore::setSelection(int newSelectPage,
			   int newSelectULX, int newSelectULY,
			   int newSelectLRX, int newSelectLRY) {
  int x0, y0, x1, y1, py;
  GBool haveSel, newHaveSel;
  GBool needRedraw, needScroll;
  GBool moveLeft, moveRight, moveTop, moveBottom;
  PDFCorePage *page;


  haveSel = selectULX != selectLRX && selectULY != selectLRY;
  newHaveSel = newSelectULX != newSelectLRX && newSelectULY != newSelectLRY;

  // erase old selection on off-screen bitmap
  needRedraw = gFalse;
  if (haveSel) {
    xorRectangle(selectPage, selectULX, selectULY, selectLRX, selectLRY,
		 new SplashSolidColor(selectXorColor));
    needRedraw = gTrue;
  }

  // draw new selection on off-screen bitmap
  if (newHaveSel) {
    xorRectangle(newSelectPage, newSelectULX, newSelectULY,
		 newSelectLRX, newSelectLRY,
		 new SplashSolidColor(selectXorColor));
    needRedraw = gTrue;
  }

  // check which edges moved
  if (!haveSel || newSelectPage != selectPage) {
    moveLeft = moveTop = moveRight = moveBottom = gTrue;
  } else {
    moveLeft = newSelectULX != selectULX;
    moveTop = newSelectULY != selectULY;
    moveRight = newSelectLRX != selectLRX;
    moveBottom = newSelectLRY != selectLRY;
  }

  // redraw currently visible part of bitmap
  if (needRedraw) {
    if (!haveSel) {
      page = findPage(newSelectPage);
      x0 = newSelectULX;
      y0 = newSelectULY;
      x1 = newSelectLRX;
      y1 = newSelectLRY;
      redrawWindow(page->xDest + x0, page->yDest + y0,
		   x1 - x0 + 1, y1 - y0 + 1, gFalse);
    } else if (!newHaveSel) {
      if ((page = findPage(selectPage))) {
	x0 = selectULX;
	y0 = selectULY;
	x1 = selectLRX;
	y1 = selectLRY;
	redrawWindow(page->xDest + x0, page->yDest + y0,
		     x1 - x0 + 1, y1 - y0 + 1, gFalse);
      }
    } else {
      page = findPage(newSelectPage);
      if (moveLeft) {
	x0 = newSelectULX < selectULX ? newSelectULX : selectULX;
	y0 = newSelectULY < selectULY ? newSelectULY : selectULY;
	x1 = newSelectULX > selectULX ? newSelectULX : selectULX;
	y1 = newSelectLRY > selectLRY ? newSelectLRY : selectLRY;
	redrawWindow(page->xDest + x0, page->yDest + y0,
		     x1 - x0 + 1, y1 - y0 + 1, gFalse);
      }
      if (moveRight) {
	x0 = newSelectLRX < selectLRX ? newSelectLRX : selectLRX;
	y0 = newSelectULY < selectULY ? newSelectULY : selectULY;
	x1 = newSelectLRX > selectLRX ? newSelectLRX : selectLRX;
	y1 = newSelectLRY > selectLRY ? newSelectLRY : selectLRY;
	redrawWindow(page->xDest + x0, page->yDest + y0,
		     x1 - x0 + 1, y1 - y0 + 1, gFalse);
      }
      if (moveTop) {
	x0 = newSelectULX < selectULX ? newSelectULX : selectULX;
	y0 = newSelectULY < selectULY ? newSelectULY : selectULY;
	x1 = newSelectLRX > selectLRX ? newSelectLRX : selectLRX;
	y1 = newSelectULY > selectULY ? newSelectULY : selectULY;
	redrawWindow(page->xDest + x0, page->yDest + y0,
		     x1 - x0 + 1, y1 - y0 + 1, gFalse);
      }
      if (moveBottom) {
	x0 = newSelectULX < selectULX ? newSelectULX : selectULX;
	y0 = newSelectLRY < selectLRY ? newSelectLRY : selectLRY;
	x1 = newSelectLRX > selectLRX ? newSelectLRX : selectLRX;
	y1 = newSelectLRY > selectLRY ? newSelectLRY : selectLRY;
	redrawWindow(page->xDest + x0, page->yDest + y0,
		     x1 - x0 + 1, y1 - y0 + 1, gFalse);
      }
    }
  }

  // switch to new selection coords
  selectPage = newSelectPage;
  selectULX = newSelectULX;
  selectULY = newSelectULY;
  selectLRX = newSelectLRX;
  selectLRY = newSelectLRY;

  // scroll if necessary
  if (newHaveSel) {
    page = findPage(selectPage);
    needScroll = gFalse;
    x0 = scrollX;
    y0 = scrollY;
    if (moveLeft && page->xDest + selectULX < 0) {
      x0 += page->xDest + selectULX;
      needScroll = gTrue;
    } else if (moveRight && page->xDest + selectLRX >= drawAreaWidth) {
      x0 += page->xDest + selectLRX - drawAreaWidth;
      needScroll = gTrue;
    } else if (moveLeft && page->xDest + selectULX >= drawAreaWidth) {
      x0 += page->xDest + selectULX - drawAreaWidth;
      needScroll = gTrue;
    } else if (moveRight && page->xDest + selectLRX < 0) {
      x0 += page->xDest + selectLRX;
      needScroll = gTrue;
    }
    py = continuousMode ? pageY[selectPage - 1] : 0;
    if (moveTop && py + selectULY < y0) {
      y0 = py + selectULY;
      needScroll = gTrue;
    } else if (moveBottom && py + selectLRY >= y0 + drawAreaHeight) {
      y0 = py + selectLRY - drawAreaHeight;
      needScroll = gTrue;
    } else if (moveTop && py + selectULY >= y0 + drawAreaHeight) {
      y0 = py + selectULY - drawAreaHeight;
      needScroll = gTrue;
    } else if (moveBottom && py + selectLRY < y0) {
      y0 = py + selectLRY;
      needScroll = gTrue;
    }
    if (needScroll) {
      scrollTo(x0, y0);
    }
  }
}

void PDFCore::moveSelection(int pg, int x, int y) {
  int newSelectULX, newSelectULY, newSelectLRX, newSelectLRY;

  // don't allow selections to span multiple pages
  if (pg != selectPage) {
    return;
  }

  // move appropriate edges of selection
  if (lastDragLeft) {
    if (x < selectLRX) {
      newSelectULX = x;
      newSelectLRX = selectLRX;
    } else {
      newSelectULX = selectLRX;
      newSelectLRX = x;
      lastDragLeft = gFalse;
    }
  } else {
    if (x > selectULX) {
      newSelectULX = selectULX;
      newSelectLRX = x;
    } else {
      newSelectULX = x;
      newSelectLRX = selectULX;
      lastDragLeft = gTrue;
    }
  }
  if (lastDragTop) {
    if (y < selectLRY) {
      newSelectULY = y;
      newSelectLRY = selectLRY;
    } else {
      newSelectULY = selectLRY;
      newSelectLRY = y;
      lastDragTop = gFalse;
    }
  } else {
    if (y > selectULY) {
      newSelectULY = selectULY;
      newSelectLRY = y;
    } else {
      newSelectULY = y;
      newSelectLRY = selectULY;
      lastDragTop = gTrue;
    }
  }

  // redraw the selection
  setSelection(selectPage, newSelectULX, newSelectULY,
	       newSelectLRX, newSelectLRY);
}

void PDFCore::xorRectangle(int pg, int x0, int y0, int x1, int y1,
			   SplashPattern *pattern, PDFCoreTile *oneTile) {
  Splash *splash;
  SplashPath *path;
  PDFCorePage *page;
  PDFCoreTile *tile;
  SplashCoord xx0, yy0, xx1, yy1;
  int xi, yi, wi, hi;
  int i;

  if ((page = findPage(pg))) {
    for (i = 0; i < page->tiles->getLength(); ++i) {
      tile = (PDFCoreTile *)page->tiles->get(i);
      if (!oneTile || tile == oneTile) {
	splash = new Splash(tile->bitmap, gFalse);
	splash->setFillPattern(pattern->copy());
	xx0 = (SplashCoord)(x0 - tile->xMin);
	yy0 = (SplashCoord)(y0 - tile->yMin);
	xx1 = (SplashCoord)(x1 - tile->xMin);
	yy1 = (SplashCoord)(y1 - tile->yMin);
	path = new SplashPath();
	path->moveTo(xx0, yy0);
	path->lineTo(xx1, yy0);
	path->lineTo(xx1, yy1);
	path->lineTo(xx0, yy1);
	path->close();
	splash->xorFill(path, gTrue);
	delete path;
	delete splash;
	xi = x0 - tile->xMin;
	wi = x1 - x0;
	if (xi < 0) {
	  wi += xi;
	  xi = 0;
	}
	if (xi + wi > tile->bitmap->getWidth()) {
	  wi = tile->bitmap->getWidth() - xi;
	}
	yi = y0 - tile->yMin;
	hi = y1 - y0;
	if (yi < 0) {
	  hi += yi;
	  yi = 0;
	}
	if (yi + hi > tile->bitmap->getHeight()) {
	  hi = tile->bitmap->getHeight() - yi;
	}
	updateTileData(tile, xi, yi, wi, hi, gTrue);
      }
    }
  }
  delete pattern;
}

GBool PDFCore::getSelection(int *pg, double *ulx, double *uly,
			    double *lrx, double *lry) {
  if (selectULX == selectLRX || selectULY == selectLRY) {
    return gFalse;
  }
  *pg = selectPage;
  cvtDevToUser(selectPage, selectULX, selectULY, ulx, uly);
  cvtDevToUser(selectPage, selectLRX, selectLRY, lrx, lry);
  return gTrue;
}

GString *PDFCore::extractText(int pg, double xMin, double yMin,
			      double xMax, double yMax) {
  PDFCorePage *page;
  TextOutputControl textOutCtrl;
  TextOutputDev *textOut;
  int x0, y0, x1, y1, t;
  GString *s;

  if (!doc->okToCopy()) {
    return NULL;
  }
  if ((page = findPage(pg))) {
    cvtUserToDev(pg, xMin, yMin, &x0, &y0);
    cvtUserToDev(pg, xMax, yMax, &x1, &y1);
    if (x0 > x1) {
      t = x0; x0 = x1; x1 = t;
    }
    if (y0 > y1) {
      t = y0; y0 = y1; y1 = t;
    }
    s = page->text->getText(x0, y0, x1, y1);
  } else {
    textOutCtrl.mode = textOutPhysLayout;
    textOut = new TextOutputDev(NULL, &textOutCtrl, gFalse);
    if (textOut->isOk()) {
      doc->displayPage(textOut, pg, dpi, dpi, rotate, gFalse, gTrue, gFalse);
      textOut->cvtUserToDev(xMin, yMin, &x0, &y0);
      textOut->cvtUserToDev(xMax, yMax, &x1, &y1);
      if (x0 > x1) {
	t = x0; x0 = x1; x1 = t;
      }
      if (y0 > y1) {
	t = y0; y0 = y1; y1 = t;
      }
      s = textOut->getText(x0, y0, x1, y1);
    } else {
      s = new GString();
    }
    delete textOut;
  }
  return s;
}

GBool PDFCore::find(char *s, GBool caseSensitive, GBool next, GBool backward,
		    GBool wholeWord, GBool onePageOnly) {
  Unicode *u;
  int len, i;
  GBool ret;

  // convert to Unicode
  len = (int)strlen(s);
  u = (Unicode *)gmallocn(len, sizeof(Unicode));
  for (i = 0; i < len; ++i) {
    u[i] = (Unicode)(s[i] & 0xff);
  }

  ret = findU(u, len, caseSensitive, next, backward, wholeWord, onePageOnly);

  gfree(u);
  return ret;
}

GBool PDFCore::findU(Unicode *u, int len, GBool caseSensitive,
		     GBool next, GBool backward, GBool wholeWord,
		     GBool onePageOnly) {
  TextOutputControl textOutCtrl;
  TextOutputDev *textOut;
  double xMin, yMin, xMax, yMax;
  PDFCorePage *page;
  int pg;
  GBool startAtTop, startAtLast, stopAtLast;

  // check for zero-length string
  if (len == 0) {
    return gFalse;
  }

  setBusyCursor(gTrue);

  // search current page starting at previous result, current
  // selection, or top/bottom of page
  startAtTop = startAtLast = gFalse;
  xMin = yMin = xMax = yMax = 0;
  pg = topPage;
  if (next) {
    startAtLast = gTrue;
  } else if (selectULX != selectLRX && selectULY != selectLRY) {
    pg = selectPage;
    if (backward) {
      xMin = selectULX - 1;
      yMin = selectULY - 1;
    } else {
      xMin = selectULX + 1;
      yMin = selectULY + 1;
    }
  } else {
    startAtTop = gTrue;
  }
  if (!(page = findPage(pg))) {
    displayPage(pg, zoom, rotate, gTrue, gFalse);
    page = findPage(pg);
  }
  if (page->text->findText(u, len, startAtTop, gTrue, startAtLast, gFalse,
			   caseSensitive, backward, wholeWord,
			   &xMin, &yMin, &xMax, &yMax)) {
    goto found;
  }

  if (!onePageOnly) {

    // search following/previous pages
    textOutCtrl.mode = textOutPhysLayout;
    textOut = new TextOutputDev(NULL, &textOutCtrl, gFalse);
    if (!textOut->isOk()) {
      delete textOut;
      goto notFound;
    }
    for (pg = backward ? pg - 1 : pg + 1;
	 backward ? pg >= 1 : pg <= doc->getNumPages();
	 pg += backward ? -1 : 1) {
      doc->displayPage(textOut, pg, 72, 72, 0, gFalse, gTrue, gFalse);
      if (textOut->findText(u, len, gTrue, gTrue, gFalse, gFalse,
			    caseSensitive, backward, wholeWord,
			    &xMin, &yMin, &xMax, &yMax)) {
	delete textOut;
	goto foundPage;
      }
    }

    // search previous/following pages
    for (pg = backward ? doc->getNumPages() : 1;
	 backward ? pg > topPage : pg < topPage;
	 pg += backward ? -1 : 1) {
      doc->displayPage(textOut, pg, 72, 72, 0, gFalse, gTrue, gFalse);
      if (textOut->findText(u, len, gTrue, gTrue, gFalse, gFalse,
			    caseSensitive, backward, wholeWord,
			    &xMin, &yMin, &xMax, &yMax)) {
	delete textOut;
	goto foundPage;
      }
    }
    delete textOut;

  }

  // search current page ending at previous result, current selection,
  // or bottom/top of page
  if (!startAtTop) {
    xMin = yMin = xMax = yMax = 0;
    if (next) {
      stopAtLast = gTrue;
    } else {
      stopAtLast = gFalse;
      xMax = selectLRX;
      yMax = selectLRY;
    }
    if (page->text->findText(u, len, gTrue, gFalse, gFalse, stopAtLast,
			     caseSensitive, backward, wholeWord,
			     &xMin, &yMin, &xMax, &yMax)) {
      goto found;
    }
  }

  // not found
 notFound:
  setBusyCursor(gFalse);
  return gFalse;

  // found on a different page
 foundPage:
  update(pg, scrollX, continuousMode ? -1 : 0, zoom, rotate, gFalse, gTrue,
	 gTrue);
  page = findPage(pg);
  if (!page->text->findText(u, len, gTrue, gTrue, gFalse, gFalse,
			    caseSensitive, backward, wholeWord,
			    &xMin, &yMin, &xMax, &yMax)) {
    // this can happen if coalescing is bad
    goto notFound;
  }

  // found: change the selection
 found:
  setSelection(pg, (int)floor(xMin), (int)floor(yMin),
	       (int)ceil(xMax), (int)ceil(yMax));

  setBusyCursor(gFalse);
  return gTrue;
}


GBool PDFCore::cvtWindowToUser(int xw, int yw,
			       int *pg, double *xu, double *yu) {
  PDFCorePage *page;
  PDFCoreTile *tile;
  int i;

  for (i = 0; i < pages->getLength(); ++i) {
    page = (PDFCorePage *)pages->get(i);
    if (xw >= page->xDest && xw < page->xDest + page->w &&
	yw >= page->yDest && yw < page->yDest + page->h) {
      if (page->tiles->getLength() == 0) {
	break;
      }
      tile = (PDFCoreTile *)page->tiles->get(0);
      *pg = page->page;
      xw -= tile->xDest;
      yw -= tile->yDest;
      *xu = tile->ictm[0] * xw + tile->ictm[2] * yw + tile->ictm[4];
      *yu = tile->ictm[1] * xw + tile->ictm[3] * yw + tile->ictm[5];
      return gTrue;
    }
  }
  *pg = 0;
  *xu = *yu = 0;
  return gFalse;
}

GBool PDFCore::cvtWindowToDev(int xw, int yw, int *pg, int *xd, int *yd) {
  PDFCorePage *page;
  int i;

  for (i = 0; i < pages->getLength(); ++i) {
    page = (PDFCorePage *)pages->get(i);
    if (xw >= page->xDest && xw < page->xDest + page->w &&
	yw >= page->yDest && yw < page->yDest + page->h) {
      *pg = page->page;
      *xd = xw - page->xDest;
      *yd = yw - page->yDest;
      return gTrue;
    }
  }
  *pg = 0;
  *xd = *yd = 0;
  return gFalse;
}

void PDFCore::cvtUserToWindow(int pg, double xu, double yu, int *xw, int *yw) {
  PDFCorePage *page;
  PDFCoreTile *tile;

  if ((page = findPage(pg)) &&
      page->tiles->getLength() > 0) {
    tile = (PDFCoreTile *)page->tiles->get(0);
  } else if (curTile && curPage->page == pg) {
    tile = curTile;
  } else {
    tile = NULL;
  }
  if (tile) {
    *xw = tile->xDest + (int)(tile->ctm[0] * xu + tile->ctm[2] * yu +
			      tile->ctm[4] + 0.5);
    *yw = tile->yDest + (int)(tile->ctm[1] * xu + tile->ctm[3] * yu +
			      tile->ctm[5] + 0.5);
  } else {
    // this should never happen
    *xw = *yw = 0;
  }
}

void PDFCore::cvtUserToDev(int pg, double xu, double yu, int *xd, int *yd) {
  PDFCorePage *page;
  PDFCoreTile *tile;
  double ctm[6];

  if ((page = findPage(pg)) &&
      page->tiles->getLength() > 0) {
    tile = (PDFCoreTile *)page->tiles->get(0);
  } else if (curTile && curPage->page == pg) {
    tile = curTile;
  } else {
    tile = NULL;
  }
  if (tile) {
    *xd = (int)(tile->xMin + tile->ctm[0] * xu +
		tile->ctm[2] * yu + tile->ctm[4] + 0.5);
    *yd = (int)(tile->yMin + tile->ctm[1] * xu +
		tile->ctm[3] * yu + tile->ctm[5] + 0.5);
  } else {
    doc->getCatalog()->getPage(pg)->getDefaultCTM(ctm, dpi, dpi, rotate,
						  gFalse, out->upsideDown());
    *xd = (int)(ctm[0] * xu + ctm[2] * yu + ctm[4] + 0.5);
    *yd = (int)(ctm[1] * xu + ctm[3] * yu + ctm[5] + 0.5);
  }
}

void PDFCore::cvtDevToWindow(int pg, int xd, int yd, int *xw, int *yw) {
  PDFCorePage *page;

  if ((page = findPage(pg))) {
    *xw = page->xDest + xd;
    *yw = page->yDest + yd;
  } else {
    // this should never happen
    *xw = *yw = 0;
  }
}

void PDFCore::cvtDevToUser(int pg, int xd, int yd, double *xu, double *yu) {
  PDFCorePage *page;
  PDFCoreTile *tile;

  if ((page = findPage(pg)) &&
      page->tiles->getLength() > 0) {
    tile = (PDFCoreTile *)page->tiles->get(0);
  } else if (curTile && curPage->page == pg) {
    tile = curTile;
  } else {
    tile = NULL;
  }
  if (tile) {
    xd -= tile->xMin;
    yd -= tile->yMin;
    *xu = tile->ictm[0] * xd + tile->ictm[2] * yd + tile->ictm[4];
    *yu = tile->ictm[1] * xd + tile->ictm[3] * yd + tile->ictm[5];
  } else {
    // this should never happen
    *xu = *yu = 0;
  }
}

void PDFCore::setReverseVideo(GBool reverseVideoA) {
  out->setReverseVideo(reverseVideoA);
  update(topPage, scrollX, scrollY, zoom, rotate, gTrue, gFalse, gFalse);
}

LinkAction *PDFCore::findLink(int pg, double x, double y) {
  PDFCorePage *page;

  if ((page = findPage(pg))) {
    return page->links ? page->links->find(x, y) : (LinkAction *)NULL;
  }
  return NULL;
}

PDFCorePage *PDFCore::findPage(int pg) {
  PDFCorePage *page;
  int i;

  for (i = 0; i < pages->getLength(); ++i) {
    page = (PDFCorePage *)pages->get(i);
    if (page->page == pg) {
      return page;
    }
  }
  return NULL;
}

void PDFCore::redrawCbk(void *data, int x0, int y0, int x1, int y1,
			GBool composited) {
  PDFCore *core = (PDFCore *)data;

  core->curTile->bitmap = core->out->getBitmap();

  // the default CTM is set by the Gfx constructor; tile->ctm is
  // needed by the coordinate conversion functions (which may be
  // called during redraw)
  memcpy(core->curTile->ctm, core->out->getDefCTM(), 6 * sizeof(double));
  memcpy(core->curTile->ictm, core->out->getDefICTM(), 6 * sizeof(double));

  // the bitmap created by Gfx and SplashOutputDev can be a slightly
  // different size due to rounding errors
  if (x1 >= core->curTile->xMax - core->curTile->xMin) {
    x1 = core->curTile->xMax - core->curTile->xMin - 1;
  }
  if (y1 >= core->curTile->yMax - core->curTile->yMin) {
    y1 = core->curTile->yMax - core->curTile->yMin - 1;
  }

  core->clippedRedrawRect(core->curTile, x0, y0,
			  core->curTile->xDest + x0, core->curTile->yDest + y0,
			  x1 - x0 + 1, y1 - y0 + 1,
			  0, 0, core->drawAreaWidth, core->drawAreaHeight,
			  gTrue, composited);
}

void PDFCore::redrawWindow(int x, int y, int width, int height,
			   GBool needUpdate) {
  PDFCorePage *page;
  PDFCoreTile *tile;
  int xDest, yDest, w, i, j;

  if (pages->getLength() == 0) {
    redrawRect(NULL, 0, 0, x, y, width, height, gTrue);
    return;
  }

  for (i = 0; i < pages->getLength(); ++i) {
    page = (PDFCorePage *)pages->get(i);
    for (j = 0; j < page->tiles->getLength(); ++j) {
      tile = (PDFCoreTile *)page->tiles->get(j);
      if (tile->edges & pdfCoreTileTopEdge) {
	if (tile->edges & pdfCoreTileLeftEdge) {
	  xDest = 0;
	} else {
	  xDest = tile->xDest;
	}
	if (tile->edges & pdfCoreTileRightEdge) {
	  w = drawAreaWidth - xDest;
	} else {
	  w = tile->xDest + (tile->xMax - tile->xMin) - xDest;
	}
	clippedRedrawRect(NULL, 0, 0,
			  xDest, 0, w, tile->yDest,
			  x, y, width, height, gFalse);
      }
      if (tile->edges & pdfCoreTileBottomEdge) {
	if (tile->edges & pdfCoreTileLeftEdge) {
	  xDest = 0;
	} else {
	  xDest = tile->xDest;
	}
	if (tile->edges & pdfCoreTileRightEdge) {
	  w = drawAreaWidth - xDest;
	} else {
	  w = tile->xDest + (tile->xMax - tile->xMin) - xDest;
	}
	yDest = tile->yDest + (tile->yMax - tile->yMin);
	clippedRedrawRect(NULL, 0, 0,
			  xDest, yDest, w, drawAreaHeight - yDest,
			  x, y, width, height, gFalse);
      } else if ((tile->edges & pdfCoreTileBottomSpace) &&
		 i+1 < pages->getLength()) {
	if (tile->edges & pdfCoreTileLeftEdge) {
	  xDest = 0;
	} else {
	  xDest = tile->xDest;
	}
	if (tile->edges & pdfCoreTileRightEdge) {
	  w = drawAreaWidth - xDest;
	} else {
	  w = tile->xDest + (tile->xMax - tile->xMin) - xDest;
	}
	yDest = tile->yDest + (tile->yMax - tile->yMin);
	clippedRedrawRect(NULL, 0, 0,
			  xDest, yDest,
			  w, ((PDFCorePage *)pages->get(i+1))->yDest - yDest,
			  x, y, width, height, gFalse);
      }
      if (tile->edges & pdfCoreTileLeftEdge) {
	clippedRedrawRect(NULL, 0, 0,
			  0, tile->yDest,
			  tile->xDest, tile->yMax - tile->yMin,
			  x, y, width, height, gFalse);
      }
      if (tile->edges & pdfCoreTileRightEdge) {
	xDest = tile->xDest + (tile->xMax - tile->xMin);
	clippedRedrawRect(NULL, 0, 0,
			  xDest, tile->yDest,
			  drawAreaWidth - xDest, tile->yMax - tile->yMin,
			  x, y, width, height, gFalse);
      }
      clippedRedrawRect(tile, 0, 0, tile->xDest, tile->yDest,
			tile->bitmap->getWidth(), tile->bitmap->getHeight(),
			x, y, width, height, needUpdate);
    }
  }
}

PDFCoreTile *PDFCore::newTile(int xDestA, int yDestA) {
  return new PDFCoreTile(xDestA, yDestA);
}

void PDFCore::updateTileData(PDFCoreTile *tileA, int xSrc, int ySrc,
			     int width, int height, GBool composited) {
}

void PDFCore::clippedRedrawRect(PDFCoreTile *tile, int xSrc, int ySrc,
				int xDest, int yDest, int width, int height,
				int xClip, int yClip, int wClip, int hClip,
				GBool needUpdate, GBool composited) {
  if (tile && needUpdate) {
    updateTileData(tile, xSrc, ySrc, width, height, composited);
  }
  if (xDest < xClip) {
    xSrc += xClip - xDest;
    width -= xClip - xDest;
    xDest = xClip;
  }
  if (xDest + width > xClip + wClip) {
    width = xClip + wClip - xDest;
  }
  if (yDest < yClip) {
    ySrc += yClip - yDest;
    height -= yClip - yDest;
    yDest = yClip;
  }
  if (yDest + height > yClip + hClip) {
    height = yClip + hClip - yDest;
  }
  if (width > 0 && height > 0) {
    redrawRect(tile, xSrc, ySrc, xDest, yDest, width, height, composited);
  }
}
