Skip to content

Conversation

@canislupaster
Copy link

@canislupaster canislupaster commented Oct 25, 2025

I was wondering why I was getting different results when downscaling my canvas. It took me a while to isolate that the issue required both clipping and image data. Here's a PoC (now a visual test):

tests['clip path after image data'] = function (ctx) {
  ctx.scale(10,10);
  const data = ctx.getImageData(0,0,1,1);
  ctx.putImageData(data,0,0);

  ctx.rect(5,5,10,10);
  ctx.clip();

  ctx.rect(0,0,15,15);
  ctx.fillStyle="black";
  ctx.fill();
}

This was blank with skia-canvas but draws a square with canvas.

It occurs since setting image data restores to save count 1, and then set_matrix during Context2D::pop overwrites the matrix at save count 1. When the clip path is loaded again with set_clip, it has already been transformed and undergoes the same transform again at save count 1.

The Skia docs note that restoreToCount

Does nothing if saveCount is greater than state stack count. Restores state to initial values if saveCount is less than or equal to one.

but the former behavior takes precedence (I think / apparently), so the matrix isn't reset during PageRecorder::restore.

I simplified things so both set_matrix and set_clip call restore (though this could be split up again if there are other concerns like performance). PageRecorder::restore calls save after restoring to save count 1, so it maintains the invariant that save count 1 has no clip path and the identity matrix, which set_matrix previously violated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant