diff --git a/toonz/sources/common/tapptools/tenv.cpp b/toonz/sources/common/tapptools/tenv.cpp
index 13de2a2..173c349 100644
--- a/toonz/sources/common/tapptools/tenv.cpp
+++ b/toonz/sources/common/tapptools/tenv.cpp
@@ -172,9 +172,9 @@ public:
     m_applicationFullName = m_version.getAppName() + " " + m_applicationVersion;
     if (m_version.hasAppNote())
       m_applicationFullName += " " + m_version.getAppNote();
-      
-    m_moduleName          = m_version.getAppName();
-    m_rootVarName         = toUpper(m_version.getAppName()) + "ROOT";
+
+    m_moduleName  = m_version.getAppName();
+    m_rootVarName = toUpper(m_version.getAppName()) + "ROOT";
 #ifdef _WIN32
     // from v1.3, registry root is moved to SOFTWARE\\OpenToonz\\OpenToonz
     m_registryRoot =
@@ -694,8 +694,7 @@ std::string toString2(T value) {
 template <>
 std::string toString2(TRect value) {
   std::ostringstream ss;
-  ss << value.x0 << " " << value.y0 << " " << value.x1 << " " << value.y1
-     << '\0';
+  ss << value.x0 << " " << value.y0 << " " << value.x1 << " " << value.y1;
   return ss.str();
 }
 
diff --git a/toonz/sources/toonz/penciltestpopup.cpp b/toonz/sources/toonz/penciltestpopup.cpp
index 61e47eb..94df853 100644
--- a/toonz/sources/toonz/penciltestpopup.cpp
+++ b/toonz/sources/toonz/penciltestpopup.cpp
@@ -90,6 +90,8 @@
 #include <QThreadPool>
 #include <QHostInfo>
 #include <QDesktopServices>
+#include <QDialogButtonBox>
+#include <QMessageBox>
 
 #ifdef _WIN32
 #include <dshow.h>
@@ -119,6 +121,7 @@ TEnv::IntVar CamCapSaveInPopupCreateSceneInFolder(
     "CamCapSaveInPopupCreateSceneInFolder", 0);
 TEnv::IntVar CamCapDoCalibration("CamCapDoCalibration", 0);
 
+TEnv::RectVar CamCapSubCameraRect("CamCapSubCameraRect", TRect());
 TEnv::IntVar CamCapDoAutoDpi("CamCapDoAutoDpi", 1);
 TEnv::DoubleVar CamCapCustomDpi("CamCapDpiForNewLevel", 120.0);
 
@@ -412,6 +415,24 @@ bool getRasterLevelSize(TXshLevel* level, TDimension& dim) {
   return true;
 }
 
+class IconView : public QWidget {
+  QIcon m_icon;
+
+public:
+  IconView(const QString& iconName, const QString& toolTipStr = "",
+           QWidget* parent = 0)
+      : QWidget(parent), m_icon(createQIcon(iconName.toUtf8())) {
+    setMinimumSize(18, 18);
+    setToolTip(toolTipStr);
+  }
+
+protected:
+  void paintEvent(QPaintEvent* e) {
+    QPainter p(this);
+    p.drawPixmap(QRect(0, 2, 18, 18), m_icon.pixmap(18, 18));
+  }
+};
+
 }  // namespace
 
 //=============================================================================
@@ -483,14 +504,17 @@ void MyVideoWidget::computeTransform(QSize imgSize) {
           .scale(scale, scale);
 }
 
-void MyVideoWidget::setSubCameraSize(QSize size) {
+void MyVideoWidget::setSubCameraRect(QRect rect) {
   QSize frameSize = m_image.size();
-  assert(frameSize == size.expandedTo(frameSize));
+  assert(frameSize == rect.size().expandedTo(frameSize));
 
-  m_subCameraRect.setSize(size);
+  m_subCameraRect = rect;
   // make sure the sub camera is inside of the frame
-  if (!QRect(QPoint(0, 0), frameSize).contains(m_subCameraRect))
+  if (rect.isValid() &&
+      !QRect(QPoint(0, 0), frameSize).contains(m_subCameraRect)) {
     m_subCameraRect.moveCenter(QRect(QPoint(0, 0), frameSize).center());
+    emit subCameraChanged(false);
+  }
 
   update();
 }
@@ -622,9 +646,9 @@ void MyVideoWidget::mouseMoveEvent(QMouseEvent* event) {
 
     if (m_activeSubHandle == HandleFrame) {
       clampPoint(offset, -m_preSubCameraRect.left(),
-                 camSize.width() - m_preSubCameraRect.right(),
+                 camSize.width() - m_preSubCameraRect.right() - 1,
                  -m_preSubCameraRect.top(),
-                 camSize.height() - m_preSubCameraRect.bottom());
+                 camSize.height() - m_preSubCameraRect.bottom() - 1);
       m_subCameraRect = m_preSubCameraRect.translated(offset);
     } else {
       if (m_activeSubHandle == HandleTopLeft ||
@@ -637,7 +661,7 @@ void MyVideoWidget::mouseMoveEvent(QMouseEvent* event) {
                  m_activeSubHandle == HandleBottomRight ||
                  m_activeSubHandle == HandleRight) {
         clampVal(offset.rx(), -m_preSubCameraRect.width() + minimumSize,
-                 camSize.width() - m_preSubCameraRect.right());
+                 camSize.width() - m_preSubCameraRect.right() - 1);
         m_subCameraRect.setRight(m_preSubCameraRect.right() + offset.x());
       }
 
@@ -651,13 +675,13 @@ void MyVideoWidget::mouseMoveEvent(QMouseEvent* event) {
                  m_activeSubHandle == HandleBottomLeft ||
                  m_activeSubHandle == HandleBottom) {
         clampVal(offset.ry(), -m_preSubCameraRect.height() + minimumSize,
-                 camSize.height() - m_preSubCameraRect.bottom());
+                 camSize.height() - m_preSubCameraRect.bottom() - 1);
         m_subCameraRect.setBottom(m_preSubCameraRect.bottom() + offset.y());
       }
-      // if the sub camera size is changed, notify the parent for updating the
-      // fields
-      emit subCameraResized(true);
     }
+    // if the sub camera size is changed, notify the parent for updating the
+    // fields
+    emit subCameraChanged(true);
     update();
   }
 }
@@ -685,7 +709,8 @@ void MyVideoWidget::mouseReleaseEvent(QMouseEvent* event) {
     return;
 
   m_preSubCameraRect = QRect();
-  if (m_activeSubHandle != HandleFrame) emit subCameraResized(false);
+
+  emit subCameraChanged(false);
 
   // restart the camera
   emit startCamera();
@@ -1268,6 +1293,212 @@ void PencilTestSaveInFolderPopup::updateParentFolder() {
 }
 
 //=============================================================================
+namespace {
+
+bool strToSubCamera(const QString& str, QRect& subCamera, double& dpi) {
+#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
+  QStringList values = str.split(',', Qt::SkipEmptyParts);
+#else
+  QStringList values         = str.split(',', QString::SkipEmptyParts);
+#endif
+  if (values.count() != 4 && values.count() != 5) return false;
+  subCamera = QRect(values[0].toInt(), values[1].toInt(), values[2].toInt(),
+                    values[3].toInt());
+  dpi       = (values.count() == 5) ? values[4].toDouble() : -1.;
+  return true;
+}
+
+const QString subCameraToStr(const QRect& subCamera, const double dpi = -1.) {
+  QString ret = QString("%1,%2,%3,%4")
+                    .arg(subCamera.left())
+                    .arg(subCamera.top())
+                    .arg(subCamera.width())
+                    .arg(subCamera.height());
+  if (dpi > 0.) ret += QString(",%1").arg(dpi);
+  return ret;
+}
+}  // namespace
+
+SubCameraButton::SubCameraButton(const QString& text, QWidget* parent)
+    : QPushButton(text, parent), m_currentDpi(-1.) {
+  setObjectName("SubcameraButton");
+  setIconSize(QSize(16, 16));
+  setIcon(createQIcon("subcamera"));
+  setCheckable(true);
+
+  // load preference file
+  TFilePath layoutDir = ToonzFolder::getMyModuleDir();
+  TFilePath prefPath  = layoutDir + TFilePath("camera_capture_subcamera.ini");
+  // In case the personal settings is not exist (for new users)
+  if (!TFileStatus(prefPath).doesExist()) {
+    TFilePath templatePath = ToonzFolder::getTemplateModuleDir() +
+                             TFilePath("camera_capture_subcamera.ini");
+    // If there is the template, copy it to the personal one
+    if (TFileStatus(templatePath).doesExist())
+      TSystem::copyFile(prefPath, templatePath);
+  }
+  m_settings.reset(new QSettings(
+      QString::fromStdWString(prefPath.getWideString()), QSettings::IniFormat));
+}
+
+void SubCameraButton::contextMenuEvent(QContextMenuEvent* event) {
+  // return if the current camera is not valid
+  if (!m_curResolution.isValid()) return;
+  // load presets for the current resolution
+  QString groupName = QString("%1x%2")
+                          .arg(m_curResolution.width())
+                          .arg(m_curResolution.height());
+  m_settings->beginGroup(groupName);
+
+  bool hasPresets = !m_settings->childKeys().isEmpty();
+
+  if (!m_curSubCamera.isValid() && !hasPresets) {
+    m_settings->endGroup();
+    return;
+  }
+
+  QMenu menu(this);
+  menu.setToolTipsVisible(true);
+
+  for (auto key : m_settings->childKeys()) {
+    QAction* scAct = menu.addAction(key);
+    scAct->setData(m_settings->value(key));
+    QRect rect;
+    double dpi;
+    if (!strToSubCamera(m_settings->value(key).toString(), rect, dpi)) continue;
+    if (m_curSubCamera == rect && m_currentDpi == dpi) {
+      scAct->setCheckable(true);
+      scAct->setChecked(true);
+      scAct->setEnabled(false);
+    } else {
+      QString toolTip = QString("%1 x %2, X%3 Y%4")
+                            .arg(rect.width())
+                            .arg(rect.height())
+                            .arg(rect.x())
+                            .arg(rect.y());
+      if (dpi > 0.) toolTip += QString(", %1DPI").arg(dpi);
+      scAct->setToolTip(toolTip);
+      connect(scAct, SIGNAL(triggered()), this, SLOT(onSubCameraAct()));
+    }
+  }
+
+  if (hasPresets) menu.addSeparator();
+
+  // save preset (visible if the subcamera is active)
+  if (m_curSubCamera.isValid()) {
+    QAction* saveAct = menu.addAction(tr("Save Current Subcamera"));
+    connect(saveAct, SIGNAL(triggered()), this, SLOT(onSaveSubCamera()));
+  }
+
+  // delete preset (visible if there is any)
+  if (hasPresets) {
+    QMenu* delMenu = menu.addMenu(tr("Delete Preset"));
+    for (auto key : m_settings->childKeys()) {
+      QAction* delAct = delMenu->addAction(tr("Delete %1").arg(key));
+      delAct->setData(key);
+      connect(delAct, SIGNAL(triggered()), this, SLOT(onDeletePreset()));
+    }
+  }
+
+  m_settings->endGroup();
+
+  menu.exec(event->globalPos());
+}
+
+void SubCameraButton::onSubCameraAct() {
+  QRect subCameraRect;
+  double dpi;
+  if (strToSubCamera(qobject_cast<QAction*>(sender())->data().toString(),
+                     subCameraRect, dpi))
+    emit subCameraPresetSelected(subCameraRect, dpi);
+}
+
+void SubCameraButton::onSaveSubCamera() {
+  auto initDialog = [](QLineEdit** lineEdit) {
+    QDialog* ret = new QDialog();
+    *lineEdit    = new QLineEdit(ret);
+    QDialogButtonBox* buttonBox =
+        new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
+    QVBoxLayout* lay = new QVBoxLayout();
+    lay->setMargin(5);
+    lay->setSpacing(10);
+    lay->addWidget(*lineEdit);
+    lay->addWidget(buttonBox);
+    ret->setLayout(lay);
+    connect(buttonBox, &QDialogButtonBox::accepted, ret, &QDialog::accept);
+    connect(buttonBox, &QDialogButtonBox::rejected, ret, &QDialog::reject);
+    return ret;
+  };
+
+  static QDialog* nameDialog = nullptr;
+  static QLineEdit* lineEdit = nullptr;
+  if (!nameDialog) nameDialog = initDialog(&lineEdit);
+
+  // ask name
+  QString oldName;
+  QString groupName = QString("%1x%2")
+                          .arg(m_curResolution.width())
+                          .arg(m_curResolution.height());
+  m_settings->beginGroup(groupName);
+  for (auto key : m_settings->childKeys()) {
+    QRect rect;
+    double dpi;
+    if (!strToSubCamera(m_settings->value(key).toString(), rect, dpi)) continue;
+    if (m_curSubCamera == rect && m_currentDpi == dpi) {
+      oldName = key;
+    }
+  }
+  lineEdit->setText(oldName);
+  lineEdit->selectAll();
+
+  if (nameDialog->exec() == QDialog::Rejected) {
+    m_settings->endGroup();
+    return;
+  }
+
+  QString newName = lineEdit->text();
+
+  if (newName.isEmpty()) {
+    m_settings->endGroup();
+    return;
+  }
+
+  // ask if there are the same name / data in the existing entries
+  if (m_settings->contains(newName) || !oldName.isEmpty()) {
+    QString txt =
+        tr("Overwriting the existing subcamera preset. Are you sure?");
+    if (QMessageBox::Yes != QMessageBox::question(this, tr("Question"), txt))
+      return;
+    if (!oldName.isEmpty()) m_settings->remove(oldName);
+  }
+
+  // register
+  m_settings->setValue(newName, subCameraToStr(m_curSubCamera, m_currentDpi));
+  m_settings->endGroup();
+}
+
+void SubCameraButton::onDeletePreset() {
+  QString key       = qobject_cast<QAction*>(sender())->data().toString();
+  QString groupName = QString("%1x%2")
+                          .arg(m_curResolution.width())
+                          .arg(m_curResolution.height());
+  m_settings->beginGroup(groupName);
+  if (!m_settings->contains(key)) {
+    m_settings->endGroup();
+    return;
+  }
+
+  QString txt = tr("Deleting the subcamera preset %1. Are you sure?").arg(key);
+  if (QMessageBox::Yes != QMessageBox::question(this, tr("Question"), txt)) {
+    m_settings->endGroup();
+    return;
+  }
+
+  m_settings->remove(key);
+  m_settings->endGroup();
+}
+
+//=============================================================================
 
 PencilTestPopup::PencilTestPopup()
     // set the parent 0 in order to enable the popup behind the main window
@@ -1347,9 +1578,11 @@ PencilTestPopup::PencilTestPopup()
   QPushButton* subfolderButton = new QPushButton(tr("Subfolder"), this);
 
   // subcamera
-  m_subcameraButton     = new QPushButton(tr("Subcamera"), this);
+  m_subcameraButton     = new SubCameraButton(tr("Subcamera"), this);
   m_subWidthFld         = new IntLineEdit(this);
   m_subHeightFld        = new IntLineEdit(this);
+  m_subXPosFld          = new IntLineEdit(this);
+  m_subYPosFld          = new IntLineEdit(this);
   QWidget* subCamWidget = new QWidget(this);
 
   // Calibration
@@ -1430,11 +1663,8 @@ PencilTestPopup::PencilTestPopup()
 
   m_saveInFolderPopup->hide();
 
-  m_subcameraButton->setObjectName("SubcameraButton");
-  m_subcameraButton->setIconSize(QSize(16, 16));
-  m_subcameraButton->setIcon(createQIcon("subcamera"));
-  m_subcameraButton->setCheckable(true);
   m_subcameraButton->setChecked(false);
+  m_subcameraButton->setEnabled(false);
   subCamWidget->setHidden(true);
 
   // Calibration
@@ -1449,6 +1679,12 @@ PencilTestPopup::PencilTestPopup()
   m_calibration.label->hide();
   m_calibration.exportBtn->setEnabled(false);
 
+  int subCameraFieldWidth = fontMetrics().width("00000") + 5;
+  m_subWidthFld->setFixedWidth(subCameraFieldWidth);
+  m_subHeightFld->setFixedWidth(subCameraFieldWidth);
+  m_subXPosFld->setFixedWidth(subCameraFieldWidth);
+  m_subYPosFld->setFixedWidth(subCameraFieldWidth);
+
   m_dpiBtn->setObjectName("SubcameraButton");
 
   //---- layout ----
@@ -1477,9 +1713,19 @@ PencilTestPopup::PencilTestPopup()
       subCamLay->setMargin(0);
       subCamLay->setSpacing(3);
       {
+        subCamLay->addWidget(new IconView("edit_scale", tr("Size"), this), 0);
         subCamLay->addWidget(m_subWidthFld, 0);
         subCamLay->addWidget(new QLabel("x", this), 0);
         subCamLay->addWidget(m_subHeightFld, 0);
+
+        subCamLay->addSpacing(3);
+        subCamLay->addWidget(
+            new IconView("edit_position", tr("Position"), this), 0);
+        subCamLay->addWidget(new QLabel("X:", this), 0);
+        subCamLay->addWidget(m_subXPosFld, 0);
+        subCamLay->addWidget(new QLabel("Y:", this), 0);
+        subCamLay->addWidget(m_subYPosFld, 0);
+
         subCamLay->addStretch(0);
       }
       subCamWidget->setLayout(subCamLay);
@@ -1648,15 +1894,15 @@ PencilTestPopup::PencilTestPopup()
   //---- signal-slot connections ----
   bool ret = true;
   ret      = ret && connect(refreshCamListButton, SIGNAL(pressed()), this,
-                       SLOT(refreshCameraList()));
+                            SLOT(refreshCameraList()));
   ret      = ret && connect(m_cameraListCombo, SIGNAL(activated(int)), this,
-                       SLOT(onCameraListComboActivated(int)));
+                            SLOT(onCameraListComboActivated(int)));
   ret      = ret && connect(m_resolutionCombo, SIGNAL(activated(int)), this,
-                       SLOT(onResolutionComboActivated()));
+                            SLOT(onResolutionComboActivated()));
   ret      = ret && connect(m_fileFormatOptionButton, SIGNAL(pressed()), this,
-                       SLOT(onFileFormatOptionButtonPressed()));
+                            SLOT(onFileFormatOptionButtonPressed()));
   ret      = ret && connect(m_levelNameEdit, SIGNAL(levelNameEdited()), this,
-                       SLOT(onLevelNameEdited()));
+                            SLOT(onLevelNameEdited()));
   ret      = ret &&
         connect(nextLevelButton, SIGNAL(pressed()), this, SLOT(onNextName()));
   ret = ret && connect(m_previousLevelButton, SIGNAL(pressed()), this,
@@ -1697,12 +1943,21 @@ PencilTestPopup::PencilTestPopup()
                        SLOT(onSubCameraToggled(bool)));
   ret = ret && connect(m_subcameraButton, SIGNAL(toggled(bool)), subCamWidget,
                        SLOT(setVisible(bool)));
+  ret =
+      ret &&
+      connect(m_subcameraButton,
+              SIGNAL(subCameraPresetSelected(const QRect&, const double)), this,
+              SLOT(onSubCameraPresetSelected(const QRect&, const double)));
   ret = ret && connect(m_subWidthFld, SIGNAL(editingFinished()), this,
-                       SLOT(onSubCameraSizeEdited()));
+                       SLOT(onSubCameraRectEdited()));
   ret = ret && connect(m_subHeightFld, SIGNAL(editingFinished()), this,
-                       SLOT(onSubCameraSizeEdited()));
-  ret = ret && connect(m_videoWidget, SIGNAL(subCameraResized(bool)), this,
-                       SLOT(onSubCameraResized(bool)));
+                       SLOT(onSubCameraRectEdited()));
+  ret = ret && connect(m_subXPosFld, SIGNAL(editingFinished()), this,
+                       SLOT(onSubCameraRectEdited()));
+  ret = ret && connect(m_subYPosFld, SIGNAL(editingFinished()), this,
+                       SLOT(onSubCameraRectEdited()));
+  ret = ret && connect(m_videoWidget, SIGNAL(subCameraChanged(bool)), this,
+                       SLOT(onSubCameraChanged(bool)));
 
   ret = ret && connect(m_timer, SIGNAL(timeout()), this, SLOT(onTimeout()));
 
@@ -1762,6 +2017,20 @@ PencilTestPopup::PencilTestPopup()
     if (startupResolutionIndex >= 0) {
       m_resolutionCombo->setCurrentIndex(startupResolutionIndex);
       onResolutionComboActivated();
+
+      // if the saved resolution was reproduced, then reproduce the subcamera
+      TRect subCamRect = CamCapSubCameraRect;
+      if (!subCamRect.isEmpty()) {
+        m_subWidthFld->setValue(subCamRect.getLx());
+        m_subHeightFld->setValue(subCamRect.getLy());
+        m_subXPosFld->setValue(subCamRect.x0);
+        m_subYPosFld->setValue(subCamRect.y0);
+        // need to store the dummy image before starting the camera
+        QImage dummyImg(m_resolution, QImage::Format_RGB32);
+        dummyImg.fill(Qt::transparent);
+        m_videoWidget->setImage(dummyImg);
+        m_subcameraButton->setChecked(true);
+      }
     }
   }
 
@@ -1851,6 +2120,7 @@ void PencilTestPopup::onCameraListComboActivated(int comboIndex) {
     m_videoWidget->setImage(QImage());
     // update env
     CamCapCameraName = "";
+    m_subcameraButton->setEnabled(false);
     return;
   }
 
@@ -1882,6 +2152,7 @@ void PencilTestPopup::onCameraListComboActivated(int comboIndex) {
   m_timer->start(40);
   // update env
   CamCapCameraName = m_cameraListCombo->itemText(comboIndex).toStdString();
+  m_subcameraButton->setEnabled(true);
 }
 
 //-----------------------------------------------------------------------------
@@ -1913,6 +2184,7 @@ void PencilTestPopup::onResolutionComboActivated() {
 
   // reset subcamera info
   m_subcameraButton->setChecked(false);  // this will hide the size fields
+  m_subcameraButton->setCurResolution(m_resolution);
   m_subWidthFld->setRange(10, newResolution.width());
   m_subHeightFld->setRange(10, newResolution.height());
   // if there is no existing level or its size is larger than the current camera
@@ -2419,6 +2691,14 @@ void PencilTestPopup::hideEvent(QHideEvent* event) {
              SLOT(refreshFrameInfo()));
   disconnect(sceneHandle, SIGNAL(preferenceChanged(const QString&)), this,
              SLOT(onPreferenceChanged(const QString&)));
+
+  // save the current subcamera to env
+  if (m_subcameraButton->isChecked()) {
+    CamCapSubCameraRect = TRect(
+        TPoint(m_subXPosFld->getValue(), m_subYPosFld->getValue()),
+        TDimension(m_subWidthFld->getValue(), m_subHeightFld->getValue()));
+  } else
+    CamCapSubCameraRect = TRect();
 }
 
 //-----------------------------------------------------------------------------
@@ -3187,32 +3467,70 @@ void PencilTestPopup::onSceneSwitched() {
 //-----------------------------------------------------------------------------
 
 void PencilTestPopup::onSubCameraToggled(bool on) {
-  m_videoWidget->setSubCameraSize(
-      on ? QSize(m_subWidthFld->getValue(), m_subHeightFld->getValue())
-         : QSize());
+  QRect subCamera =
+      on ? QRect(m_subXPosFld->getValue(), m_subYPosFld->getValue(),
+                 m_subWidthFld->getValue(), m_subHeightFld->getValue())
+         : QRect();
+  m_videoWidget->setSubCameraRect(subCamera);
+  m_subcameraButton->setCurSubCamera(subCamera);
   refreshFrameInfo();
 }
 
 //-----------------------------------------------------------------------------
 
-void PencilTestPopup::onSubCameraResized(bool isDragging) {
-  QSize subSize = m_videoWidget->subCameraRect().size();
-  assert(subSize.isValid());
-  m_subWidthFld->setValue(subSize.width());
-  m_subHeightFld->setValue(subSize.height());
-  if (!isDragging) refreshFrameInfo();
+void PencilTestPopup::onSubCameraChanged(bool isDragging) {
+  QRect subCameraRect = m_videoWidget->subCameraRect();
+  assert(subCameraRect.isValid());
+  m_subWidthFld->setValue(subCameraRect.width());
+  m_subHeightFld->setValue(subCameraRect.height());
+  m_subXPosFld->setValue(subCameraRect.x());
+  m_subYPosFld->setValue(subCameraRect.y());
+
+  if (!isDragging) {
+    refreshFrameInfo();
+    m_subcameraButton->setCurSubCamera(subCameraRect);
+  }
 }
 
 //-----------------------------------------------------------------------------
 
-void PencilTestPopup::onSubCameraSizeEdited() {
-  m_videoWidget->setSubCameraSize(
-      QSize(m_subWidthFld->getValue(), m_subHeightFld->getValue()));
+void PencilTestPopup::onSubCameraRectEdited() {
+  m_videoWidget->setSubCameraRect(
+      QRect(m_subXPosFld->getValue(), m_subYPosFld->getValue(),
+            m_subWidthFld->getValue(), m_subHeightFld->getValue()));
   refreshFrameInfo();
 }
 
 //-----------------------------------------------------------------------------
 
+void PencilTestPopup::onSubCameraPresetSelected(const QRect& subCameraRect,
+                                                const double dpi) {
+  assert(subCameraRect.isValid());
+  m_subWidthFld->setValue(subCameraRect.width());
+  m_subHeightFld->setValue(subCameraRect.height());
+  m_subXPosFld->setValue(subCameraRect.x());
+  m_subYPosFld->setValue(subCameraRect.y());
+
+  if (!m_subcameraButton->isChecked())
+    // calling onSubCameraToggled() accordingly
+    m_subcameraButton->setChecked(true);
+  else
+    onSubCameraToggled(true);
+
+  if (m_dpiMenuWidget) {
+    m_autoDpiRadioBtn->setChecked(true);
+    if (dpi > 0.) {
+      m_customDpi     = dpi;
+      CamCapCustomDpi = dpi;
+      m_customDpiField->setText(QString::number(dpi));
+      m_dpiBtn->setText(tr("DPI:%1").arg(QString::number(dpi)));
+      m_customDpiRadioBtn->setChecked(true);
+    }
+  }
+}
+
+//-----------------------------------------------------------------------------
+
 void PencilTestPopup::onCalibCapBtnClicked() {
   m_calibration.captureCue = true;
 }
@@ -3393,9 +3711,15 @@ QWidget* PencilTestPopup::createDpiMenuWidget() {
          "      the image will fit to the camera frame.\n"
          "Custom : Always use the custom dpi specified here."));
 
-  QRadioButton* autoDpiRadioBtn   = new QRadioButton(tr("Auto"), this);
-  QRadioButton* customDpiRadioBtn = new QRadioButton(tr("Custom"), this);
-  m_customDpiField                = new QLineEdit(this);
+  m_autoDpiRadioBtn   = new QRadioButton(tr("Auto"), this);
+  m_customDpiRadioBtn = new QRadioButton(tr("Custom"), this);
+  // check the opposite option so that the slot will be called when setting the
+  // initial state
+  if (m_doAutoDpi)
+    m_customDpiRadioBtn->setChecked(true);
+  else
+    m_autoDpiRadioBtn->setChecked(true);
+  m_customDpiField = new QLineEdit(this);
 
   m_customDpiField->setValidator(new QDoubleValidator(1.0, 2000.0, 6));
 
@@ -3404,9 +3728,9 @@ QWidget* PencilTestPopup::createDpiMenuWidget() {
   layout->setHorizontalSpacing(5);
   layout->setVerticalSpacing(10);
   {
-    layout->addWidget(autoDpiRadioBtn, 0, 0, 1, 2,
+    layout->addWidget(m_autoDpiRadioBtn, 0, 0, 1, 2,
                       Qt::AlignLeft | Qt::AlignVCenter);
-    layout->addWidget(customDpiRadioBtn, 1, 0,
+    layout->addWidget(m_customDpiRadioBtn, 1, 0,
                       Qt::AlignLeft | Qt::AlignVCenter);
     layout->addWidget(m_customDpiField, 1, 1);
   }
@@ -3414,28 +3738,34 @@ QWidget* PencilTestPopup::createDpiMenuWidget() {
 
   bool ret = true;
   ret      = ret &&
-        connect(autoDpiRadioBtn, &QRadioButton::toggled, [&](bool checked) {
+        connect(m_autoDpiRadioBtn, &QRadioButton::toggled, [&](bool checked) {
           m_doAutoDpi     = checked;
           CamCapDoAutoDpi = checked;
           m_customDpiField->setDisabled(checked);
-          if (checked)
+          if (checked) {
             m_dpiBtn->setText(tr("DPI:Auto"));
-          else
+            m_subcameraButton->setCurDpi(-1.);
+          } else {
             m_dpiBtn->setText(tr("DPI:%1").arg(QString::number(m_customDpi)));
+            m_subcameraButton->setCurDpi(m_customDpi);
+          }
         });
 
   ret = ret && connect(m_customDpiField, &QLineEdit::editingFinished, [&]() {
           m_customDpi     = m_customDpiField->text().toDouble();
           CamCapCustomDpi = m_customDpi;
+          m_dpiBtn->setText(tr("DPI:%1").arg(QString::number(m_customDpi)));
+          m_subcameraButton->setCurDpi(m_customDpi);
+          m_dpiMenuWidget->hide();
         });
   assert(ret);
 
   m_customDpiField->setText(QString::number(m_customDpi));
   if (m_doAutoDpi)
-    autoDpiRadioBtn->setChecked(true);
+    m_autoDpiRadioBtn->setChecked(true);
   else
-    customDpiRadioBtn->setChecked(true);
-  m_customDpiField->setDisabled(m_doAutoDpi);
+    m_customDpiRadioBtn->setChecked(true);
+  // m_customDpiField->setDisabled(m_doAutoDpi);
 
   return widget;
 }
diff --git a/toonz/sources/toonz/penciltestpopup.h b/toonz/sources/toonz/penciltestpopup.h
index d05ba72..60fe140 100644
--- a/toonz/sources/toonz/penciltestpopup.h
+++ b/toonz/sources/toonz/penciltestpopup.h
@@ -14,6 +14,7 @@
 #include <QAbstractVideoSurface>
 #include <QRunnable>
 #include <QLineEdit>
+#include <QPushButton>
 
 // forward decl.
 class QCamera;
@@ -22,14 +23,13 @@ class QCameraImageCapture;
 class QComboBox;
 class QSlider;
 class QCheckBox;
-class QPushButton;
 class QVideoFrame;
 class QTimer;
 class QIntValidator;
 class QRegExpValidator;
-class QPushButton;
 class QLabel;
 class QGroupBox;
+class QRadioButton;
 #ifdef MACOSX
 class QCameraViewfinder;
 #endif
@@ -93,7 +93,7 @@ public:
     repaint();
   }
 
-  void setSubCameraSize(QSize size);
+  void setSubCameraRect(QRect rect);
   QRect subCameraRect() { return m_subCameraRect; }
 
   void computeTransform(QSize imgSize);
@@ -112,7 +112,7 @@ protected slots:
 signals:
   void startCamera();
   void stopCamera();
-  void subCameraResized(bool isDragging);
+  void subCameraChanged(bool isDragging);
 };
 
 //=============================================================================
@@ -220,6 +220,35 @@ protected slots:
 };
 
 //=============================================================================
+// SubCameraButton
+// button with context menu to open preset
+//-----------------------------------------------------------------------------
+
+class SubCameraButton : public QPushButton {
+  Q_OBJECT
+  std::unique_ptr<QSettings> m_settings;
+
+  QSize m_curResolution;
+  QRect m_curSubCamera;
+  double m_currentDpi;
+
+public:
+  SubCameraButton(const QString& text, QWidget* parent = 0);
+  void setCurResolution(const QSize& size) { m_curResolution = size; }
+  void setCurSubCamera(const QRect& rect) { m_curSubCamera = rect; }
+  void setCurDpi(const double& dpi) { m_currentDpi = dpi; }
+
+protected:
+  void contextMenuEvent(QContextMenuEvent* event) override;
+protected slots:
+  void onSubCameraAct();
+  void onSaveSubCamera();
+  void onDeletePreset();
+signals:
+  void subCameraPresetSelected(const QRect&, const double dpi);
+};
+
+//=============================================================================
 // PencilTestPopup
 //-----------------------------------------------------------------------------
 
@@ -262,8 +291,9 @@ class PencilTestPopup : public DVGui::Dialog {
 
   QToolButton* m_previousLevelButton;
 
-  QPushButton* m_subcameraButton;
+  SubCameraButton* m_subcameraButton;
   DVGui::IntLineEdit *m_subWidthFld, *m_subHeightFld;
+  DVGui::IntLineEdit *m_subXPosFld, *m_subYPosFld;
   QSize m_allowedCameraSize;
 
   bool m_captureWhiteBGCue;
@@ -295,6 +325,7 @@ class PencilTestPopup : public DVGui::Dialog {
   double m_customDpi;
   QWidget* m_dpiMenuWidget;
   QPushButton* m_dpiBtn;
+  QRadioButton *m_autoDpiRadioBtn, *m_customDpiRadioBtn;
   QLineEdit* m_customDpiField;
 
   void captureCalibrationRefImage(cv::Mat& procImage);
@@ -353,8 +384,9 @@ protected slots:
   void onSceneSwitched();
 
   void onSubCameraToggled(bool);
-  void onSubCameraResized(bool isDragging);
-  void onSubCameraSizeEdited();
+  void onSubCameraChanged(bool isDragging);
+  void onSubCameraRectEdited();
+  void onSubCameraPresetSelected(const QRect&, const double);
 
   void onTimeout();