diff --git a/config/schema/image_effects.schema.yml b/config/schema/image_effects.schema.yml
index 3fcf1c7..e0bc397 100644
--- a/config/schema/image_effects.schema.yml
+++ b/config/schema/image_effects.schema.yml
@@ -115,6 +115,50 @@ image.effect.image_effects_color_shift:
       type: string
       label: 'RGB color shift'
 
+image.effect.image_effects_set_canvas:
+  type: mapping
+  label: 'Set canvas image effect'
+  mapping:
+    canvas_mode:
+      type: string
+      label: 'Canvas sizing mode, exact or relative'
+    canvas_color:
+      type: string
+      label: 'RGBA color of the canvas'
+    exact:
+      type: mapping
+      mapping:
+        width:
+          type: string
+          label: 'Exact width in px'
+        height:
+          type: string
+          label: 'Exact height in px'
+        placement:
+          type: string
+          label: 'Position of the source image on the canvas'
+        x_offset:
+          type: integer
+          label: 'x offset vs placement'
+        y_offset:
+          type: integer
+          label: 'y offset vs placement'
+    relative:
+      type: mapping
+      mapping:
+        left:
+          type: integer
+          label: 'Left margin in px'
+        right:
+          type: integer
+          label: 'Right margin in px'
+        top:
+          type: integer
+          label: 'Top margin in px'
+        bottom:
+          type: integer
+          label: 'Bottom margin in px'
+
 # The strip metadata effect has no settings.
 image.effect.image_effects_strip_metadata:
   type: sequence
diff --git a/image_effects.module b/image_effects.module
index 10ffdd9..bf908fc 100644
--- a/image_effects.module
+++ b/image_effects.module
@@ -29,6 +29,10 @@ function image_effects_theme() {
     'image_effects_brightness_summary' => [
       'variables' => ['data' => NULL, 'effect' => []],
     ],
+    // Set canvas image effect - summary
+    'image_effects_set_canvas_summary' => [
+      'variables' => ['data' => NULL, 'effect' => []],
+    ],
     // Watemark image effect - summary
     'image_effects_watermark_summary' => [
       'variables' => ['data' => NULL, 'effect' => []],
diff --git a/src/Component/ImageUtility.php b/src/Component/ImageUtility.php
new file mode 100644
index 0000000..332e202
--- /dev/null
+++ b/src/Component/ImageUtility.php
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image_effects\Component\ImageUtility.
+ */
+
+namespace Drupal\image_effects\Component;
+
+/**
+ * image_effects - Image handling methods.
+ */
+abstract class ImageUtility {
+
+  /**
+   * Computes a length based on a length specification and an actual length.
+   *
+   * Examples:
+   *  (50, 400) returns 50; (50%, 400) returns 200;
+   *  (50, null) returns 50; (50%, null) returns null;
+   *  (null, null) returns null; (null, 100) returns null.
+   *
+   * @param string|null $length_specification
+   *   The length specification. An integer constant or a % specification.
+   * @param int|null $current_length
+   *   The current length. May be null.
+   *
+   * @return int|null
+   */
+  public static function percentFilter($length_specification, $current_length) {
+    if (strpos($length_specification, '%') !== FALSE) {
+      $length_specification =  $current_length !== NULL ? str_replace('%', '', $length_specification) * 0.01 * $current_length : NULL;
+    }
+    return $length_specification;
+  }
+
+}
diff --git a/src/Component/PositionedRectangle.php b/src/Component/PositionedRectangle.php
new file mode 100644
index 0000000..9457110
--- /dev/null
+++ b/src/Component/PositionedRectangle.php
@@ -0,0 +1,310 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image_effects\Component\PositionedRectangle.
+ */
+
+namespace Drupal\image_effects\Component;
+
+/**
+ * Rectangle algebra class.
+ */
+class PositionedRectangle {
+
+  /**
+   * An array of point coordinates, keyed by an id.
+   *
+   * Canonical points are:
+   * 'c_a' - bottom left corner of the rectangle
+   * 'c_b' - bottom right corner of the rectangle
+   * 'c_c' - top right corner of the rectangle
+   * 'c_d' - top left corner of the rectangle
+   * 'o_a' - bottom left corner of the bounding rectangle, once the rectangle
+   *         is rotated
+   * 'o_c' - top right corner of the bounding rectangle, once the rectangle
+   *         is rotated
+   * Additional points can be added through the setPoint() method. These will
+   * be subject to translation/rotation with the rest of the points when
+   * getTranslatedRectangle() method is executed.
+   *
+   * @var array
+   */
+  protected $points = [];
+
+  /**
+   * The width of the rectangle.
+   *
+   * The width is not influenced by rotation/translation.
+   *
+   * @var int
+   */
+  protected $width = 0;
+
+  /**
+   * The height of the rectangle.
+   *
+   * The height is not influenced by rotation/translation.
+   *
+   * @var int
+   */
+  protected $height = 0;
+
+  /**
+   * The angle at which the rectangle has been rotated.
+   *
+   * @var float
+   */
+  protected $angle = 0;
+
+  /**
+   * The offset needed to reposition the rectangle fully into first quadrant.
+   *
+   * Rotating a rectangle which is sticking to axes in the first quadrant
+   * results in some of its corners to shift to other quadrants. The x/y
+   * offset required to reposition it fully in the first quadrant is stored
+   * here.
+   *
+   * @var array
+   */
+  protected $rotationOffset = [0, 0];
+
+  /**
+   * Constructs a new PositionedRectangle object.
+   *
+   * @param int $width
+   *   (Optional) The width of the rectangle.
+   * @param int $height
+   *   (Optional) The height of the rectangle.
+   */
+  public function __construct($width = 0, $height = 0) {
+    if ($width !== 0 && $height !== 0) {
+      $this->setFromDimensions($width, $height);
+    }
+  }
+
+  /**
+   * Sets a rectangle from its width and height.
+   *
+   * @param int $width
+   *   The width of the rectangle.
+   * @param int $height
+   *   The height of the rectangle.
+   *
+   * @return $this
+   */
+  public function setFromDimensions($width, $height) {
+    $this->setFromCorners([
+      'c_a' => [0, 0],
+      'c_b' => [$width - 1, 0],
+      'c_c' => [$width - 1, $height - 1],
+      'c_d' => [0, $height - 1],
+    ]);
+    return $this;
+  }
+
+  /**
+   * Sets a rectangle from the coordinates of its corners.
+   *
+   * @param array $corners
+   *   An associative array of point coordinates. The keys 'c_a', 'c_b',
+   *   'c_c' and 'c_d' represent each of the four a, b, c, d corners of the
+   *   rectangle in the format
+   *   D +-----------------+ C
+   *     |                 |
+   *     |                 |
+   *   A +-----------------+ B
+   *
+   * @return $this
+   */
+  public function setFromCorners(array $corners) {
+    $this
+      ->setPoint('c_a', $corners['c_a'])
+      ->setPoint('c_b', $corners['c_b'])
+      ->setPoint('c_c', $corners['c_c'])
+      ->setPoint('c_d', $corners['c_d'])
+      ->determineBoundingCorners();
+    $this->width = $this->getBoundingWidth();
+    $this->height = $this->getBoundingHeight();
+    return $this;
+  }
+
+  /**
+   * Sets a point and its coordinates.
+   *
+   * @param string $id
+   *   The point ID.
+   * @param array $coords
+   *   An array of x, y coordinates.
+   *
+   * @return $this
+   */
+  public function setPoint($id, array $coords = [0, 0]) {
+    $this->points[$id] = $coords;
+    return $this;
+  }
+
+  /**
+   * Gets the coordinates of a point.
+   *
+   * @param string $id
+   *   The point ID.
+   *
+   * @return array
+   *   An array of x, y coordinates.
+   */
+  public function getPoint($id) {
+    return $this->points[$id];
+  }
+
+  /**
+   * Gets the width of the rectangle.
+   *
+   * @return int
+   *   The width of the rectangle.
+   */
+  public function getWidth() {
+    return $this->width;
+  }
+
+  /**
+   * Gets the height of the rectangle.
+   *
+   * @return int
+   *   The height of the rectangle.
+   */
+  public function getHeight() {
+    return $this->height;
+  }
+
+  /**
+   * Gets the rotation offset of the rectangle.
+   *
+   * @return array
+   *   The x/y offset required to reposition the rectangle fully in the first
+   *   quadrant after it has been rotated.
+   */
+  public function getRotationOffset() {
+    return $this->rotationOffset;
+  }
+
+  /**
+   * Gets the bounding width of the rectangle.
+   *
+   * @return int
+   *   The bounding width of the rotated rectangle.
+   */
+  public function getBoundingWidth() {
+    return $this->points['o_c'][0] - $this->points['o_a'][0] + 1;
+  }
+
+  /**
+   * Gets the bounding height of the rectangle.
+   *
+   * @return int
+   *   The bounding height of the rotated rectangle.
+   */
+  public function getBoundingHeight() {
+    return $this->points['o_c'][1] - $this->points['o_a'][1] + 1;
+  }
+
+  /**
+   * Translates a point by an offset.
+   *
+   * @param array $point
+   *   An array of x, y coordinates.
+   * @param array $offset
+   *   Offset array (x, y).
+   *
+   * @return $this
+   */
+  protected function translatePoint(array &$point, array $offset) {
+    $point[0] += $offset[0];
+    $point[1] += $offset[1];
+    return $this;
+  }
+
+  /**
+   * Rotates a point, by an offset and a rotation angle.
+   *
+   * @param array $point
+   *   An array of x, y coordinates.
+   * @param float $angle
+   *   Rotation angle.
+   * @param array $offset
+   *   Offset array (x, y).
+   *
+   * @return $this
+   */
+  protected function rotatePoint(&$point, $angle) {
+    $rad = deg2rad($angle);
+    $sin = sin($rad);
+    $cos = cos($rad);
+    list($x, $y) = $point;
+    $tx = round(($x * $cos + $y * -$sin), 3);
+    $ty = round(($y * $cos - $x * -$sin), 3);
+    $point[0] = ($tx >= 0) ? ceil($tx) : -ceil(-$tx);
+    $point[1] = ($ty >= 0) ? ceil($ty) : -ceil(-$ty);
+    return $this;
+  }
+
+  /**
+   * Rotates the rectangle and any additional point.
+   *
+   * @param float $angle
+   *   Rotation angle.
+   */
+  public function rotate($angle) {
+    if ($angle) {
+      $this->angle = $angle;
+      foreach ($this->points as &$point) {
+        $this->rotatePoint($point, $angle);
+      }
+      $this->determineBoundingCorners();
+      $this->rotationOffset = [-$this->points['o_a'][0], -$this->points['o_a'][1]];
+    }
+    return $this;
+  }
+
+  /**
+   * Translates the rectangle and any additional point.
+   *
+   * @param array $offset
+   *   Offset array (x, y).
+   *
+   * @return $this
+   */
+  public function translate($offset) {
+    if ($offset[0] || $offset[1]) {
+      foreach ($this->points as &$point) {
+        $this->translatePoint($point, $offset);
+      }
+    }
+    return $this;
+  }
+
+  /**
+   * Calculates the corners of the bounding rectangle.
+   *
+   * The bottom left ('o_a') and top right ('o_c') corners of the bounding
+   * rectangle of a rotated rectangle are needed to determine the bounding
+   * width and height, and to calculate rotation-induced offest.
+   *
+   * @return $this
+   */
+  protected function determineBoundingCorners() {
+    $this
+      ->setPoint('o_a', [
+          min($this->points['c_a'][0], $this->points['c_b'][0], $this->points['c_c'][0], $this->points['c_d'][0]),
+          min($this->points['c_a'][1], $this->points['c_b'][1], $this->points['c_c'][1], $this->points['c_d'][1])
+        ]
+      )
+      ->setPoint('o_c', [
+          max($this->points['c_a'][0], $this->points['c_b'][0], $this->points['c_c'][0], $this->points['c_d'][0]),
+          max($this->points['c_a'][1], $this->points['c_b'][1], $this->points['c_c'][1], $this->points['c_d'][1])
+        ]
+      );
+    return $this;
+  }
+
+}
diff --git a/src/Plugin/ImageEffect/SetCanvasImageEffect.php b/src/Plugin/ImageEffect/SetCanvasImageEffect.php
new file mode 100644
index 0000000..dc1e42b
--- /dev/null
+++ b/src/Plugin/ImageEffect/SetCanvasImageEffect.php
@@ -0,0 +1,291 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image_effects\Plugin\ImageEffect\SetCanvasImageEffect.
+ */
+
+namespace Drupal\image_effects\Plugin\ImageEffect;
+
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Image\ImageInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\image\ConfigurableImageEffectBase;
+use Drupal\image_effects\Component\ImageUtility;
+
+/**
+ * Class SetCanvasImageEffect
+ *
+ * @ImageEffect(
+ *   id = "image_effects_set_canvas",
+ *   label = @Translation("Set canvas"),
+ *   description = @Translation("Define the size of the working canvas and background color, this controls the dimensions of the output image.")
+ * )
+ */
+class SetCanvasImageEffect extends ConfigurableImageEffectBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return NestedArray::mergeDeep([
+      'canvas_mode' => 'exact',
+      'canvas_color' => NULL,
+      'exact' => [
+        'width' => '',
+        'height' => '',
+        'placement' => 'center-center',
+        'x_offset' => 0,
+        'y_offset' => 0,
+      ],
+      'relative' => [
+        'left' => 0,
+        'right' => 0,
+        'top' => 0,
+        'bottom' => 0,
+      ],
+    ], parent::defaultConfiguration());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSummary() {
+    $data = $this->configuration;
+
+    $data['color_info'] = [
+      '#theme' => 'image_effects_color_detail',
+      '#color' => $this->configuration['canvas_color'],
+      '#border' => TRUE,
+      '#border_color' => 'matchLuma',
+    ];
+
+    return [
+      '#theme' => 'image_effects_set_canvas_summary',
+      '#data' => $data,
+    ] + parent::getSummary();
+  }
+  
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $form['canvas_mode'] = [
+      '#type' => 'radios',
+      '#title' => $this->t('Canvas mode'),
+      '#default_value' => $this->configuration['canvas_mode'],
+      '#options' => [
+        'exact' => $this->t('Exact size'),
+        'relative' => $this->t('Relative size'),
+      ],
+      '#required' => TRUE,
+    ];
+    
+    // Exact size canvas.
+    $form['exact'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Exact size'),
+      '#description'  => $this->t('Set the canvas to a precise size, possibly cropping the image. Use to start with a known size.'),
+      '#open' => TRUE,
+      '#collapsible' => FALSE,
+      '#states' => [
+        'visible' => [
+          ':input[name="data[canvas_mode]"]' => ['value' => 'exact'],
+        ],
+      ],
+    ];
+    $form['exact']['width'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Width'),
+      '#default_value' => $this->configuration['exact']['width'],
+      '#description' => $this->t('Enter a value in pixels or percent'),
+      '#size' => 5,
+    ];
+    $form['exact']['height'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Height'),
+      '#default_value' => $this->configuration['exact']['height'],
+      '#description' => $this->t('Enter a value in pixels or percent'),
+      '#size' => 5,
+    ];
+    $form['exact']['placement'] = [
+      '#type' => 'radios',
+      '#title' => $this->t('Placement'),
+      '#options' => [
+        'left-top' => $this->t('Top left'),
+        'center-top' => $this->t('Top center'),
+        'right-top' => $this->t('Top right'),
+        'left-center' => $this->t('Center left'),
+        'center-center' => $this->t('Center'),
+        'right-center' => $this->t('Center right'),
+        'left-bottom' => $this->t('Bottom left'),
+        'center-bottom' => $this->t('Bottom center'),
+        'right-bottom' => $this->t('Bottom right'),
+      ],
+      '#theme' => 'image_anchor',
+      '#default_value' => $this->configuration['exact']['placement'],
+      '#description' => $this->t('Position of the image on the canvas.'),
+    ];
+    $form['exact']['x_offset'] = [
+      '#type'  => 'number',
+      '#title' => $this->t('Horizontal offset'),
+      '#field_suffix'  => 'px',
+      '#description'   => $this->t('Additional horizontal offset from placement.'),
+      '#default_value' => $this->configuration['exact']['x_offset'],
+      '#maxlength' => 4,
+      '#size' => 4,
+    ];
+    $form['exact']['y_offset'] = [
+      '#type'  => 'number',
+      '#title' => $this->t('Vertical offset'),
+      '#field_suffix'  => 'px',
+      '#description'   => $this->t('Additional vertical offset from placement.'),
+      '#default_value' => $this->configuration['exact']['y_offset'],
+      '#maxlength' => 4,
+      '#size' => 4,
+    ];
+
+    // Relative size canvas.
+    $form['relative'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Relative size'),
+      '#description'  => $this->t('Set the canvas to a relative size, based on the current image dimensions. Use to add simple borders or expand by a fixed amount. Negative values may crop the image.'),
+      '#open' => TRUE,
+      '#collapsible' => FALSE,
+      '#states' => [
+        'visible' => [
+          ':input[name="data[canvas_mode]"]' => ['value' => 'relative'],
+        ],
+      ],
+    ];
+    $form['relative']['left'] = [
+      '#type'  => 'number',
+      '#title' => $this->t('Left margin'),
+      '#default_value' => $this->configuration['relative']['left'],
+      '#maxlength' => 4,
+      '#size' => 4,
+      '#description' => $this->t('Enter an offset in pixels.'),
+    ];
+    $form['relative']['right'] = [
+      '#type'  => 'number',
+      '#title' => $this->t('Right margin'),
+      '#default_value' => $this->configuration['relative']['right'],
+      '#maxlength' => 4,
+      '#size' => 4,
+      '#description' => $this->t('Enter an offset in pixels.'),
+    ];
+    $form['relative']['top'] = [
+      '#type'  => 'number',
+      '#title' => $this->t('Top margin'),
+      '#default_value' => $this->configuration['relative']['top'],
+      '#maxlength' => 4,
+      '#size' => 4,
+      '#description' => $this->t('Enter an offset in pixels.'),
+    ];
+    $form['relative']['bottom'] = [
+      '#type'  => 'number',
+      '#title' => $this->t('Bottom margin'),
+      '#default_value' => $this->configuration['relative']['bottom'],
+      '#maxlength' => 4,
+      '#size' => 4,
+      '#description' => $this->t('Enter an offset in pixels.'),
+    ];
+
+    // Canvas color.
+    $form['canvas_color'] = [
+      '#type' => 'image_effects_color',
+      '#title' => $this->t('Canvas color'),
+      '#allow_null' => TRUE,
+      '#allow_opacity' => TRUE,
+      '#description'  => $this->t("This will have the effect of adding colored (or transparent) margins around the image."),
+      '#default_value' => $this->configuration['canvas_color'],
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+    parent::validateConfigurationForm($form, $form_state);
+    $this->configuration = $form_state->getValues();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    parent::submitConfigurationForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applyEffect(ImageInterface $image) {
+    $data = [];
+    $data['canvas_color'] = $this->configuration['canvas_color'];
+
+    // Get resulting dimensions.
+    $dimensions = $this->getDimensions($image->getWidth(), $image->getHeight());
+    $data['width'] = $dimensions['width'];
+    $data['height'] = $dimensions['height'];
+
+    // Get offset of original image.
+    if ($this->configuration['canvas_mode'] === 'exact') {
+      list($x_pos, $y_pos) = explode('-', $this->configuration['exact']['placement']);
+      $data['x_pos'] = image_filter_keyword($x_pos, $data['width'], $image->getWidth()) + $this->configuration['exact']['x_offset'];
+      $data['y_pos'] = image_filter_keyword($y_pos, $data['height'], $image->getHeight()) + $this->configuration['exact']['y_offset'];
+    }
+    else {
+      $data['x_pos'] = $this->configuration['relative']['left'];
+      $data['y_pos'] = $this->configuration['relative']['top'];
+    }
+
+    // All the math is done, now defer to the toolkit in use.
+    return $image->apply('set_canvas', $data);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function transformDimensions(array &$dimensions, $uri) {
+    if ($dimensions['width'] && $dimensions['height']) {
+      $d = $this->getDimensions($image->getWidth(), $image->getHeight());
+      $dimensions['width'] = $d['width'];
+      $dimensions['height'] = $d['height'];
+    }
+    return;
+  }
+
+  /**
+   * Calculate resulting image dimensions.
+   *
+   * @param int $source_width
+   *   Source image width.
+   * @param int $source_height
+   *   Source image height.
+   *
+   * @return array
+   *   Associative array.
+   *   - width: Integer with the derivative image width.
+   *   - height: Integer with the derivative image height.
+   */
+  protected function getDimensions($source_width, $source_height) {
+    $dimensions = [];
+    if ($this->configuration['canvas_mode'] === 'exact') {
+      // Exact size.
+      $tmp_width = $this->configuration['exact']['width'] ?: $source_width;
+      $tmp_height = $this->configuration['exact']['height'] ?: $source_height;
+      $dimensions['width'] = ImageUtility::percentFilter($tmp_width, $source_width);
+      $dimensions['height'] = ImageUtility::percentFilter($tmp_height, $source_height);
+    }
+    else {
+      // Relative size.
+      $dimensions['width'] = $source_width + $this->configuration['relative']['left'] + $this->configuration['relative']['right'];
+      $dimensions['height'] = $source_height + $this->configuration['relative']['top'] + $this->configuration['relative']['bottom'];
+    }
+    return $dimensions;
+  }
+
+}
diff --git a/src/Plugin/ImageToolkit/Operation/DrawRectangleTrait.php b/src/Plugin/ImageToolkit/Operation/DrawRectangleTrait.php
new file mode 100644
index 0000000..12b2aa4
--- /dev/null
+++ b/src/Plugin/ImageToolkit/Operation/DrawRectangleTrait.php
@@ -0,0 +1,67 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image_effects\Plugin\ImageToolkit\Operation\DrawRectangleTrait.
+ */
+
+namespace Drupal\image_effects\Plugin\ImageToolkit\Operation;
+
+use Drupal\image_effects\Component\ColorUtility;
+use Drupal\image_effects\Component\PositionedRectangle;
+
+/**
+ * Base trait for draw rectangle operations.
+ */
+trait DrawRectangleTrait {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function arguments() {
+    return array(
+      'rectangle' => array(
+        'description' => 'A PositionedRectangle object.',
+      ),
+      'fill_color' => array(
+        'description' => 'The RGBA color of the polygon fill.',
+        'required' => FALSE,
+        'default' => NULL,
+      ),
+      'fill_color_luma' => array(
+        'description' => 'If TRUE, convert RGBA of the polygon fill to best match using luma.',
+        'required' => FALSE,
+        'default' => FALSE,
+      ),
+      'border_color' => array(
+        'description' => 'The RGBA color of the polygon line.',
+        'required' => FALSE,
+        'default' => NULL,
+      ),
+      'border_color_luma' => array(
+        'description' => 'If TRUE, convert RGBA of the polygon line to best match using luma.',
+        'required' => FALSE,
+        'default' => FALSE,
+      ),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function validateArguments(array $arguments) {
+    // Ensure 'rectangle' is an expected PositionedRectangle object.
+    if (!$arguments['rectangle'] instanceof PositionedRectangle) {
+      throw new \InvalidArgumentException("PositionedRectangle passed to the 'draw_rectangle' operation is invalid");
+    }
+    // Match color luma.
+    if ($arguments['fill_color'] && $arguments['fill_color_luma']) {
+      $arguments['fill_color'] = ColorUtility::matchLuma($arguments['fill_color']);
+    }
+    if ($arguments['border_color'] && $arguments['border_color_luma']) {
+      $arguments['border_color'] = ColorUtility::matchLuma($arguments['border_color']);
+    }
+    return $arguments;
+  }
+
+}
diff --git a/src/Plugin/ImageToolkit/Operation/SetCanvasTrait.php b/src/Plugin/ImageToolkit/Operation/SetCanvasTrait.php
new file mode 100644
index 0000000..4eed863
--- /dev/null
+++ b/src/Plugin/ImageToolkit/Operation/SetCanvasTrait.php
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image_effects\Plugin\ImageToolkit\Operation\SetCanvasTrait.
+ */
+
+namespace Drupal\image_effects\Plugin\ImageToolkit\Operation;
+
+/**
+ * Base trait for set canvas operations.
+ */
+trait SetCanvasTrait {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function arguments() {
+    return array(
+      'canvas_color' => array(
+        'description' => 'Color',
+        'required' => FALSE,
+        'default' => NULL,
+      ),
+      'width' => array(
+        'description' => 'The width of the canvas image, in pixels',
+      ),
+      'height' => array(
+        'description' => 'The height of the canvas image, in pixels',
+      ),
+      'x_pos' => array(
+        'description' => 'The left offset of the original image on the canvas, in pixels',
+      ),
+      'y_pos' => array(
+        'description' => 'The top offset of the original image on the canvas, in pixels',
+      ),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function validateArguments(array $arguments) {
+    return $arguments;
+  }
+
+}
diff --git a/src/Plugin/ImageToolkit/Operation/gd/DrawRectangle.php b/src/Plugin/ImageToolkit/Operation/gd/DrawRectangle.php
new file mode 100644
index 0000000..e65b38a
--- /dev/null
+++ b/src/Plugin/ImageToolkit/Operation/gd/DrawRectangle.php
@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image_effects\Plugin\ImageToolkit\Operation\gd\DrawRectangle.
+ */
+
+namespace Drupal\image_effects\Plugin\ImageToolkit\Operation\gd;
+
+use Drupal\image_effects\Plugin\ImageToolkit\Operation\DrawRectangleTrait;
+use Drupal\system\Plugin\ImageToolkit\Operation\gd\GDImageToolkitOperationBase;
+
+/**
+ * Defines GD2 draw rectangle operation.
+ *
+ * @ImageToolkitOperation(
+ *   id = "image_effects_gd_draw_rectangle",
+ *   toolkit = "gd",
+ *   operation = "draw_rectangle",
+ *   label = @Translation("Draw rectangle"),
+ *   description = @Translation("Draws  a rectangle on the image, optionally filling it in with a specified color.")
+ * )
+ */
+class DrawRectangle extends GDImageToolkitOperationBase {
+
+  use DrawRectangleTrait;
+  use GDOperationTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function execute(array $arguments) {
+    $success = TRUE;
+    if ($arguments['fill_color']) {
+      $color = $this->allocateColorFromRgba($arguments['fill_color']);
+      $success = imagefilledpolygon($this->getToolkit()->getResource(), $this->getRectangleCorners($arguments['rectangle']), 4, $color);
+    }
+    if ($success && $arguments['border_color']) {
+      $color = $this->allocateColorFromRgba($arguments['border_color']);
+      $success = imagepolygon($this->getToolkit()->getResource(), $this->getRectangleCorners($arguments['rectangle']), 4, $color);
+    }
+    return $success;
+  }
+
+}
diff --git a/src/Plugin/ImageToolkit/Operation/gd/GDOperationTrait.php b/src/Plugin/ImageToolkit/Operation/gd/GDOperationTrait.php
new file mode 100644
index 0000000..94070e0
--- /dev/null
+++ b/src/Plugin/ImageToolkit/Operation/gd/GDOperationTrait.php
@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image_effects\Plugin\ImageToolkit\Operation\gd\GDOperationTrait.
+ */
+
+namespace Drupal\image_effects\Plugin\ImageToolkit\Operation\gd;
+
+use Drupal\Component\Utility\Color;
+use Drupal\Component\Utility\Unicode;
+use Drupal\image_effects\Component\ColorUtility;
+use Drupal\image_effects\Component\PositionedRectangle;
+
+/**
+ * Trait for GD image toolkit operations.
+ */
+trait GDOperationTrait {
+
+  /**
+   * Allocates a GD color from an RGBA hexadecimal.
+   *
+   * @param string $rgba_hex
+   *   A string specifing an RGBA color in the format '#RRGGBBAA'.
+   *
+   * @return int
+   *   A GD color index.
+   */
+  protected function allocateColorFromRgba($rgba_hex) {
+    list($r, $g, $b, $alpha) = array_values($this->hexToRgba($rgba_hex));
+    return imagecolorallocatealpha($this->getToolkit()->getResource(), $r, $g, $b, $alpha);
+  }
+
+  /**
+   * Convert a RGBA hex to its RGBA integer GD components.
+   *
+   * GD expects a value between 0 and 127 for alpha, where 0 indicates
+   * completely opaque while 127 indicates completely transparent.
+   * RGBA hexadecimal notation has #00 for transparent and #FF for
+   * fully opaque.
+   *
+   * @param string $rgba_hex
+   *   A string specifing an RGBA color in the format '#RRGGBBAA'.
+   *
+   * @return array
+   *   An array with four elements for red, green, blue, and alpha.
+   */
+  protected function hexToRgba($rgba_hex) {
+    $rgbHex = Unicode::substr($rgba_hex, 0, 7);
+    try {
+      $rgb = Color::hexToRgb($rgbHex);
+      $opacity = ColorUtility::rgbaToOpacity($rgba_hex);
+      $alpha = 127 - floor(($opacity / 100) * 127);
+      $rgb['alpha'] = $alpha;
+      return $rgb;
+    }
+    catch (\InvalidArgumentException $e) {
+      return FALSE;
+    }
+  }
+
+  /**
+   * Convert a rectangle to a sequence of point coordinates.
+   *
+   * GD requires a simple array of point coordinates in its
+   * imagepolygon() function.
+   *
+   * @param \Drupal\image_effects\Component\PositionedRectangle $rect
+   *   A PositionedRectangle object.
+   *
+   * @return array
+   *   A simple array of 8 point coordinates.
+   */
+  protected function getRectangleCorners(PositionedRectangle $rect) {
+    $points = [];
+    foreach (array('c_d', 'c_c', 'c_b', 'c_a') as $c) {
+      $point = $rect->getPoint($c);
+      $points[] = $point[0];
+      $points[] = $point[1];
+    }
+    return $points;
+  }
+
+}
diff --git a/src/Plugin/ImageToolkit/Operation/gd/SetCanvas.php b/src/Plugin/ImageToolkit/Operation/gd/SetCanvas.php
new file mode 100644
index 0000000..2c441f2
--- /dev/null
+++ b/src/Plugin/ImageToolkit/Operation/gd/SetCanvas.php
@@ -0,0 +1,75 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image_effects\Plugin\ImageToolkit\Operation\gd\SetCanvas.
+ */
+
+namespace Drupal\image_effects\Plugin\ImageToolkit\Operation\gd;
+
+use Drupal\image_effects\Component\PositionedRectangle;
+use Drupal\image_effects\Plugin\ImageToolkit\Operation\SetCanvasTrait;
+use Drupal\system\Plugin\ImageToolkit\Operation\gd\GDImageToolkitOperationBase;
+
+/**
+ * Defines GD2 set canvas operation.
+ *
+ * @ImageToolkitOperation(
+ *   id = "image_effects_gd_set_canvas",
+ *   toolkit = "gd",
+ *   operation = "set_canvas",
+ *   label = @Translation("Set canvas"),
+ *   description = @Translation("Lay the image over a colored canvas.")
+ * )
+ */
+class SetCanvas extends GDImageToolkitOperationBase {
+
+  use SetCanvasTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function execute(array $arguments) {
+    // Store the original resource.
+    $original_res = $this->getToolkit()->getResource();
+
+    // Prepare the canvas.
+    $data = array(
+      'width' => $arguments['width'],
+      'height' => $arguments['height'],
+      'extension' => image_type_to_extension($this->getToolkit()->getType(), FALSE),
+      'transparent_color' => $this->getToolkit()->getTransparentColor(),
+      'is_temp' => TRUE,
+    );
+    if (!$this->getToolkit()->apply('create_new', $data)) {
+      return FALSE;
+    }
+
+    // Fill the canvas with required color.
+    $data = array(
+      'rectangle' => new PositionedRectangle($arguments['width'], $arguments['height']),
+      'fill_color' => $arguments['canvas_color'],
+    );
+    if (!$this->getToolkit()->apply('draw_rectangle', $data)) {
+      return FALSE;
+    }
+
+    // Overlay the current image on the canvas.
+    imagealphablending($original_res, TRUE);
+    imagesavealpha($original_res, TRUE);
+    imagealphablending($this->getToolkit()->getResource(), TRUE);
+    imagesavealpha($this->getToolkit()->getResource(), TRUE);
+    if (imagecopy($this->getToolkit()->getResource(), $original_res, $arguments['x_pos'], $arguments['y_pos'], 0, 0, imagesx($original_res), imagesy($original_res))) {
+      imagedestroy($original_res);
+      return TRUE;
+    }
+    else {
+      // In case of failure, destroy the temporary resource and restore
+      // the original one.
+      imagedestroy($this->getToolkit()->getResource());
+      $this->getToolkit()->setResource($original_res);
+    }
+    return FALSE;
+  }
+
+}
diff --git a/src/Plugin/ImageToolkit/Operation/imagemagick/DrawRectangle.php b/src/Plugin/ImageToolkit/Operation/imagemagick/DrawRectangle.php
new file mode 100644
index 0000000..d4ab61f
--- /dev/null
+++ b/src/Plugin/ImageToolkit/Operation/imagemagick/DrawRectangle.php
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image_effects\Plugin\ImageToolkit\Operation\imagemagick\DrawRectangle.
+ */
+
+namespace Drupal\image_effects\Plugin\ImageToolkit\Operation\imagemagick;
+
+use Drupal\image_effects\Plugin\ImageToolkit\Operation\DrawRectangleTrait;
+use Drupal\imagemagick\Plugin\ImageToolkit\Operation\imagemagick\ImagemagickImageToolkitOperationBase;
+
+/**
+ * Defines ImageMagick draw rectangle operation.
+ *
+ * @ImageToolkitOperation(
+ *   id = "image_effects_imagemagick_draw_rectangle",
+ *   toolkit = "imagemagick",
+ *   operation = "draw_rectangle",
+ *   label = @Translation("Draw rectangle"),
+ *   description = @Translation("Draws  a rectangle on the image, optionally filling it in with a specified color.")
+ * )
+ */
+class DrawRectangle extends ImagemagickImageToolkitOperationBase {
+
+  use DrawRectangleTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function execute(array $arguments) {
+    $arg = '';
+    if ($arguments['fill_color']) {
+      $arg .= '-fill ' . $this->getToolkit()->escapeShellArg($arguments['fill_color']);
+    }
+    else {
+      $arg .= '-fill none';
+    }
+    if ($arguments['border_color']) {
+      $arg .= ' -stroke ' . $this->getToolkit()->escapeShellArg($arguments['border_color']) . ' -strokewidth 1';
+    }
+    $a = $arguments['rectangle']->getPoint('c_a');
+    $b = $arguments['rectangle']->getPoint('c_b');
+    $c = $arguments['rectangle']->getPoint('c_c');
+    $d = $arguments['rectangle']->getPoint('c_d');
+    $this->getToolkit()->addArgument($arg . ' -draw ' . $this->getToolkit()->escapeShellArg("polygon {$d[0]},{$d[1]} {$c[0]},{$c[1]} {$b[0]},{$b[1]} {$a[0]},{$a[1]}"));
+    return TRUE;
+  }
+
+}
diff --git a/src/Plugin/ImageToolkit/Operation/imagemagick/ImagemagickOperationTrait.php b/src/Plugin/ImageToolkit/Operation/imagemagick/ImagemagickOperationTrait.php
new file mode 100644
index 0000000..db974fd
--- /dev/null
+++ b/src/Plugin/ImageToolkit/Operation/imagemagick/ImagemagickOperationTrait.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image_effects\Plugin\ImageToolkit\Operation\imagemagick\ImagemagickOperationTrait.
+ */
+
+namespace Drupal\image_effects\Plugin\ImageToolkit\Operation\imagemagick;
+
+/**
+ * Trait for ImageMagick image toolkit operations.
+ */
+trait ImagemagickOperationTrait {
+
+  /**
+   * The format mapper service.
+   *
+   * @var \Drupal\imagemagick\ImagemagickFormatMapperInterface
+   */
+  protected $formatMapper;
+
+  /**
+   * Returns the format mapper service.
+   *
+   * @return \Drupal\imagemagick\ImagemagickFormatMapperInterface
+   *   The format mapper service.
+   */
+  protected function getFormatMapper() {
+    if (!$this->formatMapper) {
+      $this->formatMapper = \Drupal::service('imagemagick.format_mapper');
+    }
+    return $this->formatMapper;
+  }
+
+}
diff --git a/src/Plugin/ImageToolkit/Operation/imagemagick/SetCanvas.php b/src/Plugin/ImageToolkit/Operation/imagemagick/SetCanvas.php
new file mode 100644
index 0000000..5be9a14
--- /dev/null
+++ b/src/Plugin/ImageToolkit/Operation/imagemagick/SetCanvas.php
@@ -0,0 +1,67 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\image_effects\Plugin\ImageToolkit\Operation\imagemagick\SetCanvas.
+ */
+
+namespace Drupal\image_effects\Plugin\ImageToolkit\Operation\imagemagick;
+
+use Drupal\image_effects\Component\PositionedRectangle;
+use Drupal\image_effects\Plugin\ImageToolkit\Operation\SetCanvasTrait;
+use Drupal\imagemagick\Plugin\ImageToolkit\Operation\imagemagick\ImagemagickImageToolkitOperationBase;
+
+/**
+ * Defines ImageMagick set canvas operation.
+ *
+ * @ImageToolkitOperation(
+ *   id = "image_effects_imagemagick_set_canvas",
+ *   toolkit = "imagemagick",
+ *   operation = "set_canvas",
+ *   label = @Translation("Set canvas"),
+ *   description = @Translation("Lay the image over a colored canvas.")
+ * )
+ */
+class SetCanvas extends ImagemagickImageToolkitOperationBase {
+
+  use ImagemagickOperationTrait;
+  use SetCanvasTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function execute(array $arguments) {
+    // Calculate geometry.
+    $geometry = sprintf('%dx%d', $arguments['width'], $arguments['height']);
+    if ($arguments['x_pos'] || $arguments['y_pos']) {
+      $geometry .= sprintf('%+d%+d', -$arguments['x_pos'], -$arguments['y_pos']);
+    }
+
+    // Determine background.
+    if ($arguments['canvas_color']) {
+      $bg = '-background ' . $this->getToolkit()->escapeShellArg($arguments['canvas_color']);
+    }
+    else {
+      $format = $this->getToolkit()->getDestinationFormat() ?: $this->getToolkit()->getSourceFormat();
+      $mime_type = $this->getFormatMapper()->getMimeTypeFromFormat($format);
+      if ($mime_type === 'image/jpeg') {
+        // JPEG does not allow transparency. Set to white. @todo allow to be configurable.
+        $bg = '-background ' . $this->getToolkit()->escapeShellArg('#FFFFFF');
+      }
+      else {
+        $bg = '-background transparent';
+      }
+    }
+
+    // Add argument.
+    $this->getToolkit()->addArgument("-gravity none {$bg} -compose src-over -extent {$geometry}");
+
+    // Set dimensions.
+    $this->getToolkit()
+      ->setWidth($arguments['width'])
+      ->setHeight($arguments['height']);
+
+    return TRUE;
+  }
+
+}
diff --git a/templates/image-effects-set-canvas-summary.html.twig b/templates/image-effects-set-canvas-summary.html.twig
new file mode 100644
index 0000000..8beecd9
--- /dev/null
+++ b/templates/image-effects-set-canvas-summary.html.twig
@@ -0,0 +1,46 @@
+{#
+/**
+ * @file
+ * Default theme implementation for a summary of set canvas image effect.
+ *
+ * Available variables:
+ * - data: The current configuration for this set canvas effect, including:
+ *   - canvas_color: The canvas color in RGBA format.
+ *   - canvas_mode: 'exact' or 'relative'.
+ *   - exact: The configuration for exact size, including:
+ *     - width: the width in px or as % of source image.
+ *     - height: the height in px or as % of source image.
+ *     - placement: The position of the source on the underlying canvas.
+ *     - x_offset: The x_offset of the source image vs placement.
+ *     - y_offset: The y_offset of the source image vs placement.
+ *   - relative: The configuration for relative size, including:
+ *     - left: the size in px of the margin on the left side.
+ *     - right: the size in px of the margin on the right side.
+ *     - top: the size in px of the margin on the top side.
+ *     - bottom: the size in px of the margin on the bottom side.
+ *   - color_info: A render element for canvas color information including a
+ *     color preview.
+ * - effect: The effect information, including:
+ *   - id: The effect identifier.
+ *   - label: The effect name.
+ *   - description: The effect description.
+ *
+ * @ingroup themeable
+ */
+#}
+{% spaceless %}
+{% if data.canvas_mode == 'exact' %}
+  - {{ 'Exact size'|t }}:
+  {% if data.exact.width %}{{ data.exact.width|e }}{% else %}{{ '100%'|t }}{% endif %}x{% if data.exact.height %}{{ data.exact.height|e }}{% else %}{{ '100%'|t }}{% endif %}
+  {{ 'Placement'|t }}: {{ data.exact.placement|e }}
+  {% if data.exact.x_offset %} {{ 'X offset'|t }}: {{ data.exact.x_offset|e }}px{% endif %}
+  {% if data.exact.y_offset %} {{ 'Y offset'|t }}: {{ data.exact.y_offset|e }}px{% endif %}
+{% elseif data.canvas_mode == 'relative' %}
+  - {{ 'Relative size'|t }}:
+  {% if data.relative.left %} {{ 'left'|t }}: {{ data.relative.left|e }}px{% endif %}
+  {% if data.relative.right %} {{ 'right'|t }}: {{ data.relative.right|e }}px{% endif %}
+  {% if data.relative.top %} {{ 'top'|t }}: {{ data.relative.top|e }}px{% endif %}
+  {% if data.relative.bottom %} {{ 'bottom'|t }}: {{ data.relative.bottom|e }}px{% endif %}
+{% endif %}
+ - {{ 'Color'|t }}: {{ data.color_info }}
+{% endspaceless %}
