AngusJohnson / Image32

An extensive 2D graphics library written in Delphi Pascal
Boost Software License 1.0
137 stars 31 forks source link

Newest EraseOutsidePaths() needs further investigation #94

Closed tomwiel closed 1 month ago

tomwiel commented 1 month ago

Sometimes a wrong effect results with new EraseOutsidePaths(). I was not able to find the reason and it's difficult to reproduce.

Here is one example: Svg101.dpr.
1) Modify TSvgReader.DrawImage() just for testing:

procedure TSvgReader.DrawImage(img: TImage32; scaleToImage: Boolean);
....
  MatrixScale(di.matrix, 2, 2); // add this line
  MatrixTranslate(di.matrix, -120, -100); // add this line
  fRootElement.Draw(img, di); ....
end;

2) In EraseOutsidePaths(img, ....), in 2nd line, also add a constraint to prevent later AV: Types.IntersectRect( vOutsideBounds, outsideBounds, img.bounds); ..... After this line, use vOutsideBounds instead of outsideBounds. Note: This constraint is probably not the reason of the reported artifact. The same constraint in old EraseOutsidePaths() would not produce artifacts.

3) After building, start the app and

eraseOutsidePaths

tomwiel commented 1 month ago

I might have found something. The challenge is, fOutsideBounds around the mask-shape must be erased, similar to EraseOutsideRect(), but also for polygons. TMaskRenderer.RenderProc() is called only for each y in mask-shapes.
The remaining y (lines) in fOutsideBounds need to be erased too. If polygon-rendering runs from top to bottom, something like:

In TMaskRenderer.RenderProc():

for i := fLastY+1 to y-1 do begin
    dst := GetDstPixel(fOutsideBounds.Left, i);
    FillChar(dst^, OutsideBoundsW * SizeOf(TColor32), 0);
end;
fLastY := y;

in EraseOutsidePaths():

Rasterize(img, paths, vOutsideBounds, fillRule, renderer);
if renderer.fLastY < vOutsideBounds.bottom-1 then begin
   img.FillRect( Rect( vOutsideBounds.Left, renderer.fLastY+1, vOutsideBounds.Right, vOutsideBounds.Bottom), 0);
end; 

But still not complete for a solution, because shapes can have multiple x-ranges per y.

AngusJohnson commented 1 month ago

Hi Tom. I'll have a look when I can, but without a simpler example I suspect this won't be a quick fix 😉.

tomwiel commented 1 month ago

Yes, the example is inefficient for bug finding, but I've found the reason: The requirement is that all non-shape pixels inside of TMaskRenderer.fOutsideBounds need to be erased (0). TMaskRenderer.RenderProc() does not fulfill this, because it's called only for visible fill-ranges (interior) of shape. TMaskRenderer.RenderProc() already implements erasing of non-shape pixels, but not completely for all cases.

Example: fOutsideBounds correctly includes the whole clip-path. Due to the zoomed view in the example, only subregions of the clip-path are inside of TImage32. This causes fOutsideBounds to contain more empty lines (around subregions). fOutsideBounds (still large from whole clip-path) is correct, but erase is not complete.

For example, non-processed (bug) bottom half under green subregion: clippath

The other potential issue is (not in the example): Complex clip-shapes can contain inner gaps which need erasing (0).

My quick fix is this (without TMaskRenderer):

procedure EraseOutsidePaths(img: TImage32; const paths: TPathsD;
  fillRule: TFillRule; const outsideBounds: TRect;
  rendererCache: TCustomRendererCache);
var
  w, h: integer;
  vOutsideBounds, r: TRect;
  mask: TImage32;
  pp: TPathsD;
begin
  if not assigned(paths) then Exit;
  Types.IntersectRect( vOutsideBounds, outsideBounds, img.Bounds);
  RectWidthHeight(vOutsideBounds, w, h);
  if (w <= 0) or (h <= 0)  then Exit;

  if (fillRule in [frEvenOdd, frNonZero]) and IsSimpleRectanglePath(paths, r) then
  begin
    EraseOutsideRect(img, r, vOutsideBounds);
    Exit;
  end;

  mask := TImage32.Create(w, h);
  try
    pp := TranslatePath(paths, -vOutsideBounds.Left, -vOutsideBounds.top);
    DrawPolygon(mask, pp, fillRule, clBlack32, rendererCache);
    img.CopyBlend(mask, mask.Bounds, vOutsideBounds, BlendMaskLine);
  finally
    mask.Free;
  end;
end; 
ahausladen commented 1 month ago

Should be fixed with my new PR.

ahausladen commented 1 month ago

I actually used the car2.svg for checking if the TMaskRenderer worked. Unfortunately I had my eyes only on the mask for the wheel not the other parts of the car. And my other example SVG file had a mask, that has the same dimensions as outsideBounds, so it had no untouched clipping region.

As you already found out, the problem with the TMaskRenderer was that it only worked for scanlines for which RenderProc was called. All other scanlines in the clipping region were not filled with zeros.

The fix introduces the new method TMaskRenderer.RenderSkipProc which is called for all scanlines that Rasterize skips, including the scanlines from cliprec.top to cliprec2.top and the scanlines from cliprec2.bottom to cliprec.bottom. The last block is combined with the last skipped scanlines, to make only one call to RenderSkipProc instead of 2.

I made a PNG comparison with the old EraseOutsidePaths code (DrawPolygon+CopyBlend(BlendMask)) and the new code to also get the clipping boundaries correct. CopyBlend excludes the Right/Bottom pixels whereas TMaskRenderer included them. But not anymore.

tomwiel commented 1 month ago

Very useful. Thank you!