pyscripter / SynEdit

SynEdit is a syntax highlighting edit control, not based on the Windows common controls.
26 stars 11 forks source link

Improve printing and print preview #51

Closed pyscripter closed 1 year ago

pyscripter commented 1 year ago

Currently SynEdit outputs text as graphics for printing. See this issue for details. This is mainly because currently we are mixing GDI and D2D for printing. D2D and DWrite are used for rendering, but then and because the printer canvas is not fully supported by DWrite, the rasterized image is sent to the printer using GDI.

A consequence is that if you print to PDF, the resulting file is huge and text cannot be selected.

The solution is to use the Direct2D way of printing and get rid of GDI.

pyscripter commented 1 year ago

The print demo has also been updated.

@MShark67 @vincentparrett @JaFi-cz Could you please test and report any issues?

vincentparrett commented 1 year ago

I haven't tested the printing side of things as I don't use it, but everything else seems ok so far.

pyscripter commented 1 year ago

Teaser: Produced by printing to PDF. Test.pdf

vincentparrett commented 1 year ago

Looks great!

pyscripter commented 1 year ago

@MShark67 Since you are the "scrolling expert", would it be possible to add horizontal mouse wheel scrolling to TSynEditPrintPreview?

MShark67 commented 1 year ago

I've been away and just getting a chance to look at this. Currently I integrate SynEdit printing with my own print preview system (which I wrote years ago.) I was using PrintToCanvas, so now I'm attempting to convert it to the new PaintPreview with ID2D1DCRenderTarget. Once I get that working, I'll try to take a look at the scrolling stuff.

pyscripter commented 1 year ago

@MShark67 Great to hear from you. The demo contains a preview form. You can also have a look at the PyScripter's one with nice svg images Best Kiriakos

MShark67 commented 1 year ago

After several failures I think my issue is that my printing/print preview system is based on TMetaFileCanvas and I don't think it's compatible with the new printing changes. I print more than just SynEdits so it's a bit complicated. I suspect this isn't going to be a quick change.

pyscripter commented 1 year ago

Sorry to hear that. How does your printing/print preview relate to the SynEdit one? Were you using the PrintToCanvas method without using the PrintPreview component?

It would be quite straight-forward to provide a PrintToCanvas method.

Something like:

procedure TSynEditPrint.PrintToCanvas(ACanvas: TCanvas;  RenderRect: TRect; PageNo: Integer);
var
  RT:  ID2D1RenderTarget;
  ScaleX, ScaleY: Single;
  ClipR: TRect;
begin
  with FSynEditPrint.PrinterInfo do
  begin
    // The RenderTarget expects a PPI of 96
    ScaleX := RenderRect.Width / (PhysicalWidth * 96 / XPixPrInch) ;
    ScaleY := RenderRect.Height / (PhysicalHeight * 96 / YPixPrInch);
  end;

  ClipR := Canvas.ClipRect;
  // Transform ClipR to the Coordinate system of RenderTarget
  ClipR.Offset(-RenderRect.Left, -RenderRect.Top);
  ClipR := TRect.Create(
    ScalePoint(ClipR.TopLeft, 1 / ScaleX, 1 / ScaleY),
    ScalePoint(ClipR.BottomRight, 1 / ScaleX, 1 / ScaleY));

  TSynDWrite.ResetRenderTarget;
  try
     RT := TSynDWrite.RenderTarget.BindDC(ACanvas.Hanle, RenderRect);

  // Reset so that rendering for printing is not mixed up with Synedit rendering
  TSynDWrite.ResetRenderTarget;
  RT := TSynDWrite.RenderTarget;
  try
    RT.BindDC(Canvas.Handle, FPaperRect);
    RT.BeginDraw;
    try
      RT.SetTransform(
        TD2DMatrix3X2F.Scale(ScaleX, ScaleY, Point(0, 0)));

      FSynEditPrint.PaintPreview(RT, PageNo, ClipR);
    finally
      RT.EndDraw;
    end;

  finally
    // Reset so that it does not mess up the SynEdit drawing
    TSynDWrite.ResetRenderTarget;
  end;
end;

I have not tested (!!!) but this is what TSynEditPrintPreview.Paint does. I could lift the code to TSynEditPrint if that helps reusability.

However, I am not sure it would work with a Metafile Canvas. In the previous version I was rendering to a bitmap and then BitBlt the bitmap to the canvas, which was affecting quaility and speed.

pyscripter commented 1 year ago

Committed the change. TSynEditPrint.PrintToCanvas is now available. Not sure it would work with a Metafile canvas though. You can also test with the PrintDemo.

pyscripter commented 1 year ago

Wow! It works with a Metafile canvas.

I tested by changing TSynEditPrintPreview.Paint to

procedure TSynEditPrintPreview.Paint;
begin
  PaintPaper;
  if (csDesigning in ComponentState) or (not Assigned(FSynEditPrint)) then
    Exit;

  var MyMetafile := TMetafile.Create;
  var ACanvas := TMetafileCanvas.Create(MyMetafile, Canvas.Handle);
  var R := FPaperRect;
  R.Offset(-FPaperRect.Left, -FPaperRect.Top);
  FSynEditPrint.PrintToCanvas(ACanvas, R, FPageNumber);
  ACanvas.Free;

  Canvas.Draw(FPaperRect.Left, FPaperRect.Top, MyMetafile)

  //FSynEditPrint.PrintToCanvas(Canvas, FPaperRect, FPageNumber);
end;
MShark67 commented 1 year ago

Interesting! The way my printing system works, the metafile canvas is created based on the current printer: MetafileCanvas := TMetafileCanvas.Create(BufMetaFile, Printer.Handle); Then I would call the old PrintToCanvas(MetaFileCanvas, PageNumber) (which I believe always printed the entire page) After that I would draw the metafile either to my preview control canvas or to the printer canvas. If it needed to go to the preview control I would use a mapping mode to scale it with the following code:

  // Now set the mapping mode.
  SetMapMode( ViewPanel.Canvas.Handle, MM_ANISOTROPIC );
  SetWindowExtEx( ViewPanel.Canvas.Handle, PrinterInfo.PageWidthPixels, PrinterInfo.PageHeightPixels, nil );
  SetViewportOrgEx( ViewPanel.Canvas.Handle, 0, 0, nil );
  SetViewportExtEx( ViewPanel.Canvas.Handle, ViewPanel.ClientWidth, ViewPanel.ClientHeight, nil );
  ViewPanel.Canvas.Font.PixelsPerInch := PrinterInfo.PPIY;

  // Now draw the metafile.
  ViewPanel.Canvas.Draw( 0, 0, BufMetafile );

My ViewPanel sits inside a TScrollBox and so it's always drawing the whole page (which is most likely less efficient but has always worked fairly well. The code to actually print is the same just without the mapping mode part.

Now I can get this to partially work if I change my code to use my ViewPanel's canvas to create the metafile canvas, but the output is clipped in that I just see the first couple of lines of my text. I think my issue is that the new PrintToCanvas mode is either doing its own scaling/clipping or that I'm not setting the ClipRect parameter correctly. I'm still trying to figure this all out, but maybe some of the above will make sense. Happy holidays btw! I appreciate your changes above and don't want you to waste your time on something that I probably really need to learn how to do lol!

pyscripter commented 1 year ago

Mery Christmas!

If you look at the current print preview all the map mode stuff is gone. PrintToPage always assumes: width: PhysicalWidth 96 / XPixPrInch height: PhysicalHeight 96 / YPixPrInch

i.e the paper size at 96 dpi.

PrintToCanvas scales the output from RenderRect to what PrintPage assumes. PrintRange (the printing function) uses D2D printing which handles the complexities of converting the rendering commands to proper high quality printer output.

If you want to print say to the printer canvas instead of a metafile canvas, then just provide the pixel dimensions of the printed page to PrintToCanvas (PhysicalWidth, PhysicalHeight)

By the way why don't you use the print preview component? I think now is near perfect! See the pdf file above generated with Print to PDF. You can try it with the PrintDemo.

MShark67 commented 1 year ago

Thanks. My current print system handles more types of printing than just text, so I can't easily just replace it with the SynEdit one. I could have two separate print previews, but I'd rather avoid that (but maybe that's how it will end up anyways.) I have tried setting the RenderRect to the physical printer size in pixels. You mention paper size at 96 dpi, of course my printer dpi is 600 which I use to set the canvas's font's pixelsperinch setting, perhaps that's an issue?

      // Need to set PixelsPerInch before setting any font properties.
      MetafileCanvas.Font.PixelsPerInch := PrnInfo.PPIY;

Basically, my system up until now has used PrintToCanvas for all synedit printing (either to the preview window or to the actual printer.)

pyscripter commented 1 year ago

My current print system handles more types of printing than just text,

I see.

The current design is based on the assumption that printing/previewing is done at 96 DPI. In PrintToCanvas we have:

  with FSynEditPrint.PrinterInfo do
  begin
    // The RenderTarget expects a PPI of 96
    ScaleX := RenderRect.Width / (PhysicalWidth * 96 / XPixPrInch) ;
    ScaleY := RenderRect.Height / (PhysicalHeight * 96 / YPixPrInch);
  end;
  ...

      RT.SetTransform(
        TD2DMatrix3X2F.Scale(ScaleX, ScaleY, Point(0, 0)));

PhysicalWidth * 96 / XPixPrInch = 96 * (PhysicalWidth / XPixPrInch) = 96 * PageWidthInInches

PrintToPage always assumes it is printing to a rectangular area equal to the paper size at 96 DPI. In PrintToCanvas, RenderRect is transformed to that size. Fonts and other scaling are based on this assumption. So you should not scale fonts. The font height should correspond to the 96 DPI height.

RenderRect should be correspond to the rectangular area of the screen or printer in which you want the page to be drawn. If you have made DC transformations then RenderRect should be specified in the transformed space.

In the code I showed earlier:

  var MyMetafile := TMetafile.Create;
  var ACanvas := TMetafileCanvas.Create(MyMetafile, Canvas.Handle);
  var R := FPaperRect;
  R.Offset(-FPaperRect.Left, -FPaperRect.Top);
  FSynEditPrint.PrintToCanvas(ACanvas, R, FPageNumber);
  ACanvas.Free;

  Canvas.Draw(FPaperRect.Left, FPaperRect.Top, MyMetafile)

FPaperRect is the rectangular of the screen, containing the page in whatever system the Canvas is rendered.

Try the demo and modify TSynEditPrintPreview.Paint as suggested and see how it works.

MShark67 commented 1 year ago

It looks like the main difference (as you pointed out) is that the new PrintToCanvas (and the entire TSynEditPrint control) is now based on screen dpi. The old PrintToCanvas could be passed a canvas with fonts set to printer dpi and it would render the entire page to the canvas, which could then be used in a more generic way (not necessarily a better way, my on-screen previews were obviously scaled but were good enough to see how printing would go.) I assume there's no way to get the new one to work the way the old one did (or at least not easily.) I think I may just be out of luck in trying to integrate the new printing system into my printing system and should now just figure out a way to use this one in parallel to my own.

pyscripter commented 1 year ago

It looks like the main difference (as you pointed out) is that the new PrintToCanvas (and the entire TSynEditPrint control) is now based on screen dpi.

Not quite

I don't see why that would not work with your printing framework.

Based on your code, something like this

  // Now set the mapping mode.
  SetMapMode( ViewPanel.Canvas.Handle, MM_ANISOTROPIC );
  SetWindowExtEx( ViewPanel.Canvas.Handle, PrinterInfo.PageWidthPixels, PrinterInfo.PageHeightPixels, nil );
  SetViewportOrgEx( ViewPanel.Canvas.Handle, 0, 0, nil );
  SetViewportExtEx( ViewPanel.Canvas.Handle, ViewPanel.ClientWidth, ViewPanel.ClientHeight, nil );

  var RenderRect := Rect(0, 0, PrinterInfo.PageWidthPixels, PrinterInfo.PageHeightPixels)
  SynEditPrint.PrintToCanvas( ViewPanel.Canvas, RenderRect, PageNo);

should work.

Or similarly print to a metafile and then render the metafile to the canvas.

pyscripter commented 1 year ago

Playing with the PrintDemo:

The following works but the output quality is very bad:

procedure TSynEditPrintPreview.Paint;
var
  ptOrgScreen: TPoint;
begin
  //PaintPaper;
  if (csDesigning in ComponentState) or (not Assigned(FSynEditPrint)) then
    Exit;
  with FSynEditPrint.PrinterInfo do
  begin
    // Now set the mapping mode.
    SetMapMode(Canvas.Handle, MM_ANISOTROPIC );
    SetWindowExtEx(Canvas.Handle, PhysicalWidth, MulDiv(ClientHeight, PhysicalWidth, ClientWidth), nil );
    SetViewportOrgEx(Canvas.Handle, 0, 0, nil );
    SetViewportExtEx(Canvas.Handle, ClientWidth, ClientHeight, nil );
    var R := Rect(0, 0, PhysicalWidth, PhysicalHeight);

    var MyMetafile := TMetafile.Create;
    var ACanvas := TMetafileCanvas.Create(MyMetafile, Canvas.Handle);
    FSynEditPrint.PrintToCanvas(ACanvas, R, FPageNumber);

    ACanvas.Free;
    Canvas.Draw(0, 0, MyMetafile);
    MyMetaFile.Free;
  end;
end;

The following produces good quality:

procedure TSynEditPrintPreview.Paint;
var
  ptOrgScreen: TPoint;
begin
  //PaintPaper;
  if (csDesigning in ComponentState) or (not Assigned(FSynEditPrint)) then
    Exit;

  Canvas.Brush.Color := clWhite;
  Canvas.FillRect(Rect(0, 0,  ClientWidth, ClientHeight));

  with FSynEditPrint.PrinterInfo do
    begin
    var R := Rect(0, 0, ClientWidth,  GetPageHeightFromWidth(ClientWidth));

    var MyMetafile := TMetafile.Create;
    var ACanvas := TMetafileCanvas.Create(MyMetafile, Canvas.Handle);
    FSynEditPrint.PrintToCanvas(ACanvas, R, FPageNumber);

    ACanvas.Free;
    Canvas.Draw(0, 0, MyMetafile);
    MyMetaFile.Free;
  end;
end;

I guess mixing the map modes with D2D output does not work well.

By the way, did you try the Printing demo? How is the performance and print/preview quality at your end?

MShark67 commented 1 year ago

The printing demo seems to work flawlessly. Performance does not appear to be any kind of problem even on fairly large files.

I've got my own preview system partially working and I think it's pointing out the main change to PrintToCanvas I mentioned above. To get things to work on my end I have to change the code of PrintToCanvas so that it does not use the canvas's ClipRect and instead just use the RenderRect (which in my case is always the printer dimensions in pixels.)

  ClipR := RenderRect;                  // My change to get things working in my specific case
  // ClipR := ACanvas.ClipRect;     // Original line

I also have to change the device context that I use for my MetafileCanvas. I would normally use this:

    // Create the MetafileCanvas based on the printer canvas.
    MetafileCanvas := TMetafileCanvas.Create(BufMetaFile, Printer.Handle);

But to get it to work I have to do this:

    // Create the MetafileCanvas based on the view control's canvas.
    MetafileCanvas := TMetafileCanvas.Create(BufMetaFile, ViewPanel.Canvas.Handle);

Making those two changes allows my preview system to work the way it did before the new PrintToCanvas changes. It however means that I can't use my system to actually print since it doesn't work at all unless the canvas is based on the screen and not the printer. Any thoughts appreciated.

pyscripter commented 1 year ago

ClipR is used just to optimize what is printed (e.g. faster scrolling). It would make sense to make it the fourth parameter to PrintToCanvas, which would solve your first problem. If it is indeed the case, please let me know and I will commit the change.

If you are not previewing and you want to print, why not just call SynEditPrint.PrintRange?

MShark67 commented 1 year ago

If you are not previewing and you want to print, why not just call SynEditPrint.PrintRange?

I add stuff to the MetafileCanvas before it gets either previewed or printed. The only difference in my printing system is that the page either gets scaled (via viewports) to display, or gets drawn directly to the printer (in which case no scaling is needed.)

I'm not worried about the ClipR thing, that's fairly easy to work around. The real question in my mind is why PrintToCanvas won't work with a printer based metafilecanvas. Any idea on that?

pyscripter commented 1 year ago

The real question in my mind is why PrintToCanvas won't work with a printer based metafilecanvas. Any idea on that?

I have committed the ClipRect change anyway,

D2D drawing requires an ID2D1RenderTarget. PyScripter uses an ID2D1DCRenderTarget which has a BindDC(DC, Rect) method. That does not work well with printer DCs (such as "Print To PDF"), and this is why in the previous version, I was drawing to a WIC bitmap and the contents of that bitmap were drawn to the printer or display DC using GDI. This was not fully satisfactory and one of the implications was this reported issue.

The current version uses the D2D print mechanism, which isolates SynEdit of the particulars of the printer and converts whatever you are drawing on a ID2D1RenderTarget to an optimal printer output. So, for example, printing to a PDF file or to a physical printer work in the best possible way (for instance includes the fonts in the PDF).

Now regarding your question. I don't know much about Metafiles. I thought metafiles are not linked to a specific Canvas. You create them and then you draw them wherever you want. PrintToCanvas seems to work well with metafiles. From what you say, creating a Metafile with a Printer DC does not work. Can't you create a metafile using a display DC and then draw it to a printer DC?

I presume that you are printing text together with other things on the same page. Is this why you cannot use SynEditPrint.PrintRange?

MShark67 commented 1 year ago

Correct. In my current system I modify the canvas before displaying it or printing it. That all worked great until the latest change to PrintToCanvas. It may be that what I'd like to do is no longer possible, or perhaps I can figure out a different way.

pyscripter commented 1 year ago

As I mentioned above, previously it worked by drawing to a bitmap and then rendering the bitmap using GDI (look at the PrintPage code, before the change). You still have this option. But there must be a better way.

MShark67 commented 1 year ago

Thanks, that's the option I'm going with for now. It seems at least as good as before, so it's good enough lol. I'll probably revisit it when I have more time at some point. Appreciate the help!

pyscripter commented 1 year ago

@MShark67 Would it be possible to post here some samples of your printed pages say as a PDF file? Just to get an idea in case I am able to make suggestions.