You are not logged in.
Pages: 1
var ClipRect: TPdfBox; //... r := pr^; // inc(r.Bottom); // inc(r.Right); ClipRect.Left := r.Left * Canvas.fDevScaleX; ClipRect.Top := Canvas.fPage.GetPageHeight - (r.Top * Canvas.fDevScaleX); ClipRect.Width := (r.Right - r.Left) * Canvas.fDevScaleX; ClipRect.Height := -(r.Bottom - r.Top) * Canvas.fDevScaleY; // Origin is bottom left so reversed // with Canvas.BoxI(r, false) do with ClipRect do Canvas.Rectangle(Left, Top, Width, Height); inc(pr);
I can confirm that this patch fixes the clipping problem. @ab could it be added to the official repository ? Would you accept a PR ? If so @rvk would you like to do it as I don't want to "steal" your code.
Thank you.
Great! Meanwhile, I've greatly updated the EMF sample generator to produce 84 variations of valid metafiles for testing purposes. It can produce variations with different values for:
- SetWindowOrgEx (on or off)
- SetWorldTransform (including none, normal, scale, translate, rotate, shear, reflect)
- Clipping regions (on or off)
Metafiles are visible in IrfanView:
Source code: https://gist.github.com/jonjbar/5e32409 … 349c66ff07
Hopefully, this could help test and enhance the SynPDF producer in the future.
AFAIR IrfanView is using the Windows GDI rendering directly.
So if it can't display anything, there is something wrong with the content.A simple VCL app should be able to display this EMF.
You're right. I could see the result in Illustrator so I though it was good enough for a quick and dirty first test. I'll see if that can be improved.
BTW. That test project doesn't create a EMF which contains an offset WinOrg or non-standard scale, like your original code from this topic does.
So it wouldn't detect the regression for which you opened this topic lol
That was just a quick and dirty first try to produce sample EMF files for testing purposes as you said previously: "For that we need more diverse EMF files."
I believe that it would be perfectly possible to generate the WinOrg using something like this at the beginning of the process:
// Set the window origin
SetWindowOrgEx(MetaFileDC, XOrigin, YOrigin, nil);
Illustrator seems to work. Perhaps InkScape too?
This program can be used to create EMF files with various clipping regions operations (RGN_AND, RGN_COPY, RGN_DIFF...). It could be a useful base for testing purposes:
program Project1;
{$APPTYPE CONSOLE}
{$R *.res}
uses
Windows, SysUtils, Classes, Vcl.Graphics;
procedure CreateEMFWithClipRegion(const FileName: string; Mode: Integer);
var
DC: HDC;
MetaFileDC: HDC;
MetaFile: HENHMETAFILE;
aRect: TRect;
RectRgn, TriangleRgn, CombinedRgn: HRGN;
Brush: HBRUSH;
Pen: HPEN;
Points: array[0..2] of TPoint;
begin
// Create a device context for the screen
DC := GetDC(0);
try
// Define the bounding rectangle for the metafile
aRect := Rect(0, 0, 300, 300);
// Create an enhanced metafile device context
MetaFileDC := CreateEnhMetaFile(DC, PChar(FileName), @aRect, nil);
try
// Draw a colored background
Brush := CreateSolidBrush(RGB(240, 240, 240)); // Light gray background
Pen := CreatePen(PS_SOLID, 1, RGB(240, 240, 240)); // Light gray pen
SelectObject(MetaFileDC, Brush);
SelectObject(MetaFileDC, Pen);
Rectangle(MetaFileDC, aRect.Left, aRect.Top, aRect.Right, aRect.Bottom);
// Define a rectangle clipping region in the middle
RectRgn := CreateRectRgn(75, 75, 225, 225);
// Define a triangle region
Points[0] := Point(50, 250);
Points[1] := Point(150, 50);
Points[2] := Point(250, 250);
TriangleRgn := CreatePolygonRgn(Points, Length(Points), WINDING);
// Combine the rectangle region and the triangle region
CombinedRgn := CreateRectRgn(0, 0, 0, 0); // Create an empty region
CombineRgn(CombinedRgn, RectRgn, TriangleRgn, Mode);
// Select the combined region with the specified mode
ExtSelectClipRgn(MetaFileDC, CombinedRgn, RGN_COPY);
// Draw a green rectangle
Brush := CreateSolidBrush(RGB(0, 255, 0)); // Green brush
Pen := CreatePen(PS_SOLID, 1, RGB(0, 255, 0)); // Green pen
SelectObject(MetaFileDC, Brush);
SelectObject(MetaFileDC, Pen);
Rectangle(MetaFileDC, 30, 30, 200, 200);
// Draw a red circle
Brush := CreateSolidBrush(RGB(255, 0, 0)); // Red brush
Pen := CreatePen(PS_SOLID, 1, RGB(255, 0, 0)); // Red pen
SelectObject(MetaFileDC, Brush);
SelectObject(MetaFileDC, Pen);
Ellipse(MetaFileDC, 100, 100, 270, 270);
// Deselect the clipping region
SelectClipRgn(MetaFileDC, 0);
// Clean up the brush, pen, and regions
DeleteObject(Brush);
DeleteObject(Pen);
DeleteObject(RectRgn);
DeleteObject(TriangleRgn);
DeleteObject(CombinedRgn);
finally
// Close the metafile and get the handle
MetaFile := CloseEnhMetaFile(MetaFileDC);
end;
// Save the metafile to a file
if MetaFile <> 0 then
begin
DeleteEnhMetaFile(MetaFile);
end;
finally
// Release the screen device context
ReleaseDC(0, DC);
end;
end;
procedure TestCreateEMF;
begin
CreateEMFWithClipRegion('C:\Tmp\_meta\Metafile_RGN_AND.emf', RGN_AND);
CreateEMFWithClipRegion('C:\Tmp\_meta\Metafile_RGN_COPY.emf', RGN_COPY);
CreateEMFWithClipRegion('C:\Tmp\_meta\Metafile_RGN_DIFF.emf', RGN_DIFF);
CreateEMFWithClipRegion('C:\Tmp\_meta\Metafile_RGN_OR.emf', RGN_OR);
CreateEMFWithClipRegion('C:\Tmp\_meta\Metafile_RGN_XOR.emf', RGN_XOR);
end;
begin
TestCreateEMF;
end.
As a starting point, here is a Python script that I've created with the help of ChatGPT and here is what it does:
- It extracts all pages from a reference PDF and a generated PDF as PNG images
- It compares the number of pages and fail if different
- It compares each pages and for each of them, it outputs the difference as both a difference image, and a percentage
- It outputs the final result as a consistent and clear textual content for easy integration with automated tests
So I support we could create multiple small command line programs to produce PDFs using SynPDF and test most parts of the library, including MetaFiles conversion. Those programs generate the PDF in a path specified by arguments, so that they can be used to generate the reference PDFs at first (and update them if needed), and re-generate them in the correct folder during automated tests.
Then the Python script is called for each files in the reference folder and fails based on specific conditions.
Requirements: pip install PyMuPDF Pillow Wand numpy termcolor
Script:
import fitz # PyMuPDF
from PIL import Image, ImageChops
import numpy as np
import os
import shutil
from termcolor import colored
# Function to clear the content of a folder or create it if it does not exist
def clear_folder(folder):
if os.path.exists(folder):
shutil.rmtree(folder)
os.makedirs(folder)
# Function to convert PDF pages to PNG images and save them in the output folder
def convert_pdf_to_png(pdf_path, output_folder):
clear_folder(output_folder)
pdf_document = fitz.open(pdf_path)
for page_num in range(len(pdf_document)):
page = pdf_document.load_page(page_num)
pix = page.get_pixmap()
output_path = f"{output_folder}/page_{page_num + 1}.png"
pix.save(output_path)
pdf_document.close()
# Function to compare two images and save the difference image if specified
def compare_images(img1_path, img2_path, diff_img_path=None):
img1 = Image.open(img1_path).convert('RGB')
img2 = Image.open(img2_path).convert('RGB')
# Check if page sizes match
if img1.size != img2.size:
return False, 100.0, "Error: Page sizes do not match"
diff = ImageChops.difference(img1, img2)
# Save the difference image if a path is provided
if diff_img_path:
diff.save(diff_img_path)
np_diff = np.array(diff)
diff_count = np.count_nonzero(np_diff)
total_pixels = np_diff.size / 3 # Divide by 3 for RGB channels
diff_percentage = (diff_count / total_pixels) * 100
return diff_count == 0, diff_percentage, None
# Function to display the final result summary
def display_final_result_summary(all_match, total_diff_percentage, num_pages, page_results, error_message=None):
if error_message:
final_status = "NOT OK"
color = 'red'
avg_diff_percentage = 100.0
else:
avg_diff_percentage = total_diff_percentage / num_pages
final_status = "OK" if all_match else "Partial"
if any(status == "Error" for _, _, status in page_results):
final_status = "NOT OK"
color = 'red'
else:
color = 'green' if final_status == "OK" else 'yellow'
# Output final result summary
print("\nFinal result summary:")
print(colored(f"Average difference percentage: {avg_diff_percentage:.2f}%", color))
print(colored(f"Result: {final_status}", color))
if error_message:
print(colored(error_message, 'red'))
# Main function to handle the PDF comparison process
def main(reference_pdf, generated_pdf, output_folder):
# Check if the reference PDF exists
if not os.path.exists(reference_pdf):
error_message = f"Error: Reference PDF '{reference_pdf}' not found."
print(colored(error_message, 'red'))
display_final_result_summary(False, 0, 0, [], error_message)
return
# Check if the generated PDF exists
if not os.path.exists(generated_pdf):
error_message = f"Error: Generated PDF '{generated_pdf}' not found."
print(colored(error_message, 'red'))
display_final_result_summary(False, 0, 0, [], error_message)
return
# Define folders for reference, generated, and difference images
reference_folder = f"{output_folder}/reference"
generated_folder = f"{output_folder}/generated"
diff_folder = f"{output_folder}/differences"
# Clear or create the folders
clear_folder(reference_folder)
clear_folder(generated_folder)
clear_folder(diff_folder)
# Convert PDFs to PNG images
convert_pdf_to_png(reference_pdf, reference_folder)
convert_pdf_to_png(generated_pdf, generated_folder)
# Get the list of image files
reference_files = sorted([f"{reference_folder}/{file}" for file in os.listdir(reference_folder)])
generated_files = sorted([f"{generated_folder}/{file}" for file in os.listdir(generated_folder)])
# Check if the number of pages (images) match
if len(reference_files) != len(generated_files):
error_message = "Error: PDFs have a different number of pages."
print(colored(error_message, 'red'))
print(f"Reference PDF has {len(reference_files)} pages.")
print(f"Generated PDF has {len(generated_files)} pages.")
display_final_result_summary(False, 0, 0, [], error_message)
return
all_match = True
total_diff_percentage = 0
page_results = []
# Compare each page and collect results
for i, (ref_img, gen_img) in enumerate(zip(reference_files, generated_files)):
diff_img_path = f"{diff_folder}/diff_{os.path.basename(ref_img)}"
match, diff_percentage, error = compare_images(ref_img, gen_img, diff_img_path)
total_diff_percentage += diff_percentage
if error:
print(colored(f"Page {i + 1}: {error}", 'red'))
all_match = False
page_results.append((i + 1, diff_percentage, "Error"))
else:
page_status = "OK" if match else "Partial"
page_results.append((i + 1, diff_percentage, page_status))
if not match:
all_match = False
# Output page-by-page results
print("Page-by-page differences:")
for page_num, diff_percentage, status in page_results:
if status == "OK":
color = 'green'
elif status == "Partial":
color = 'yellow'
else:
color = 'red'
print(colored(f"Page {page_num}: {diff_percentage:.2f}% difference - {status}", color))
# Display final result summary
display_final_result_summary(all_match, total_diff_percentage, len(reference_files), page_results)
if __name__ == "__main__":
import sys
# Ensure the correct number of arguments are provided
if len(sys.argv) != 4:
print("Usage: python script.py <reference_pdf> <generated_pdf> <output_folder>")
sys.exit(1)
# Get the input arguments
reference_pdf = sys.argv[1]
generated_pdf = sys.argv[2]
output_folder = sys.argv[3]
# Create the output folder if it does not exist
if not os.path.exists(output_folder):
os.makedirs(output_folder)
# Run the main function
main(reference_pdf, generated_pdf, output_folder)
Excellent work ? Both of your updates fixes the specific problem for this EMF file.
I'm even more confident that this is the correct fix because LibreOffice also fixed EMR_EXTSELECTCLIPRGN this way: https://github.com/LibreOffice/core/com … 60a101f661
See these lines: https://github.com/LibreOffice/core/blo … r.cxx#L372
@rvk Do you believe that this is good enough for a pull request ?
@ab Would you accept those changes in the main repository ?
So I've added debug code to visualize the clipping rectangle and it looks like it is simply offset by the PDF page's margins! Could we simply subtract the margins from there ? How to get them ?
Screenshot: https://snipboard.io/MxHLvX.jpg
Test code:
procedure TPdfEnum.ExtSelectClipRgn(data: PEMRExtSelectClipRgn);
var
RGNs: PRgnData;
i: Integer;
RCT: TRect;
ClipRect: TPdfBox;
begin
// see http://www.codeproject.com/Articles/1944/Guide-to-WIN-Regions
if data^.iMode <> RGN_COPY then exit; // we are handling RGN_COPY (5) only..
if not DC[nDC].ClipRgnNull then // if current clip then finish
begin
Canvas.GRestore;
Canvas.NewPath;
Canvas.fNewPath := False;
DC[nDC].ClipRgnNull := True;
fFillColor := -1;
end;
if Data^.cbRgnData > 0 then
begin
Canvas.GSave;
Canvas.NewPath;
DC[nDC].ClipRgnNull := False;
RGNs := @Data^.RgnData;
for i := 0 to RGNs^.rdh.nCount - 1 do
begin
Move(RGNs^.Buffer[i * SizeOf(TRect)], RCT, SizeOf(RCT));
Inc(RCT.Bottom);
Inc(RCT.Right);
ClipRect := Canvas.BoxI(RCT, false);
// Draw the clipping rectangle for debugging
Canvas.SetRGBStrokeColor($55FF00FF); // Set a distinct color for the clipping rectangle
Canvas.Rectangle(ClipRect.Left,ClipRect.Top,ClipRect.Width,ClipRect.Height);
Canvas.Stroke; // Draw the outline of the rectangle
// Apply the clipping path
Canvas.NewPath;
Canvas.Rectangle(ClipRect.Left, ClipRect.Top, ClipRect.Width, ClipRect.Height);
end;
Canvas.Closepath;
Canvas.Clip;
Canvas.NewPath;
Canvas.FNewPath := False;
end;
end;
I believe that you briefly mentioned that automated PDF testing would be a great addition in order to avoid regressions with new code. Do you have any specific idea or requirements in mind (such as being compatible with both FPC and Delphi, being cross-platform, not using external tools...) ?
I thought about this a little and here is what I believe could work:
- Create a repository of trusted source to produce PDF documents for various features of SynPDF
- Use a tool such as ImageMagick to extract each of their pages as PNG images
- Automated tests compare the newly generated PNGs with source PNGs and fail if the difference is too big
Any thoughts ? Would you accept such a contribution ?
Unfortunately, this commit introduces a new problem as described in that thread: https://synopse.info/forum/viewtopic.php?pid=41447
It includes a screenshot of the problem as well as a sample application to reproduce this.
Thank you very much for your help.
I can confirm that this commit introduces the problem: https://github.com/synopse/mORMot2/comm … 928c8b882b
I see that is has been discussed in that thread so I'll post there: https://synopse.info/forum/viewtopic.php?pid=40358
Hi,
The latest version of SynPDF2 seem to have a regression which wasn't present in SynPDF1: a part of the content is not drawn.
Screenshot: https://snipboard.io/a7T6tn.jpg
Sample project with source WMF, generate PDF and screenshot of the problem: https://we.tl/t-bLUC4A84kA
I'm using Delphi 12 in 32/64-bit mode with latest SynPDF2 from Git.
I thought I'd post it here before I open a bug on GitHub but I can do so if you wish.
Thank you for any help.
Yes, absolutely.
Hi,
Named destination are useful in PDF documents as they act as bookmarks to access a specific part of the document (e.g. https://site.com/document.pdf#destination)
See: https://evermap.com/Tutorial_ABM_Destinations.asp
I see that SynPDF supports the TPdfDestination object which works fine from hyperlinks, but it looks like this doesn't create any entries in the Acrobat's "Destinations" pane. I suspect that it isn't supported ? Or perhaps I'm missing some kind of options to export them as named destinations ?
Thanks for your help.
My concern is that almost every time I merge some pull request in SynPdf which pleases some users, there is a regression for others...
So I usually un-merge it a few days later...
Perhaps it would make sense to investigate some kind of PDF testing tool? I suspect, as an example, that it would be possible to use Chromium to produce images out of reference PDF documents, and some kind of image comparison algorithm to compare documents generated by nightly builds with those references.
Or perhaps a similar open source tool already exists in the JavaScript / Go / .net... ecosystem?
What do you think @ab ?
Hello Arnaud,
After creating an encrypted (and only in that case) TPdfDocument, if the document's PDFA1 property is changed, an exception is raised when the PDF document is saved.
This doesn't happen if the document is not encrypted or if the PDFA1 property is not modified after creation.
I can provide a test project, and open a ticket if needed.
Thanks.
You could create a new CreateExternalLink method just like TPdfDocument.CreateLink, by following the PDF reference about such external links.
Hello Arnaud,
I've written the following procedure to create external hyperlinks.
Even though I'm not a PDF expert, it looks like it is working even for complex links. Any feedback is welcome.
Hopefully this can be included as-is in SynPDF source code:
function TPDFDocument.CreateExternalLink(const ARect: TPdfRect; const anUrl: RawUTF8;
BorderStyle: TPdfAnnotationBorder; BorderWidth: integer): TPdfDictionary;
var
anAction: TPdfDictionary;
begin
result := CreateAnnotation(asLink, ARect, BorderStyle, BorderWidth);
anAction := TPdfDictionary.Create(FXRef);
anAction.AddItem('Type', 'Action');
anAction.AddItem('S', 'URI');
anAction.AddItemTextUTF8('URI', anUrl);
Result.AddItem('A', anAction);
end;
Great!
Perhaps you should create tickets: http://synopse.info/fossil/reportlist
Those would be great additions indeed.
From a quick overview of your screenshot, it looks like the Y is simply inverted for the link.
Perhaps the CreateLink's reference point is the bottom left of the document while the rectangle's reference is the top left ? Not sure why.
I received the following PM from "douglasmmm" regarding this thread. Unfortunately, I do not recall how this was fixed and do not have access to that test anymore. So I'm posting the message here in case somebody can help him:
about you post "@mORMot.ServiceContext.Factory is nil when method is called from Serve" in Synopse forum.
you could implement the solution?
I ask because I could not understand when AB says 'is to inject the TSQLRestServer instance in the interface constructor".grateful
IMHO any kind of clients (REST included) must be put on a leash as there are plenty of bad things they could do which they shouldn't be allowed to. From the top of my head, I can think about:
- Access resources (rows/fields) without proper permission
- Delete/Update resources without proper permission
- Send invalid data which could result in either data corruption or server crash
- Send malformed (string instead of int, data is too long...) or missing data (required field) which could lead to future problems (data analysis, report generation...)
Where it should be done is another question which should be carefully thought out:
- Invalid access permissions should be checked (at the very least) before returning data to the client. If not before accessing data in the persistence storage
- Invalid modification access should be checked before modifying the persistence storage
- Invalid formated data or missing data should be checked before modifying the persistence storage
- Invalid data resulting in server crash should be handled as soon as it is received (e.g data flood, invalid characters, invalid requests...)
- Invalid data resulting in data corruption should be handled before writing to persistence storage
Regarding table access security, this is great but not enough for most systems: row-level and even field-level security and validation should be handled from the server.
I admit I do not know mORMot enough to say that it is not possible to do so or that it should handle everything.
But as far as I can tell mORMot includes both the data persistence access and REST server with direct interactions between them so it should provide a way to interfere and add safe guards between both.
I hope you'll see those remarks as constructive, not critics
So validation should be done on client side, before sending the data to the server.
Clients must never be trusted!
Auto-synch between servers and clients would be amazing!
This would be a potential alternative to the very popular and great Firebase recently purchased by Google: https://www.firebase.com/
Happy new year Arnaud and best wishes for you and your rodents
Thank you Orel!
OK Thanks. Perhaps in a future update.
I'm really confused about authorization in mORMot. I'm using a TSQLHttpServer.
I understand I should use TSQLAuthUser and TSQLAuthGroup to provide per-table restrictions.
However, how to handle more complex scenarios such as for example:
* A user who created a specific TBlogArticleRecord can have CRUD right access for it while other users can only have read access on that record ?
* A user can only receive a list of the TBlogArticleRecord he has the rights to read when calling for example TBlogArticleRecord.CreateAndFillPrepare as server will filter any un-authorized records ?
Thanks for any help.
That's exactly what I wanted. Thank you very much mpv!
Should I take care of freeing the TSMObjects (e.g. blogObj.free) when the engine is freed or are they owned by the engine ?
Is it possible and how to register an object method ? I tried the following:
Engine.RegisterMethod(Engine.GlobalObj,'blog',dummy_method,0);
Engine.RegisterMethod(Engine.GlobalObj,'blog.article',dummy_method,0);
Engine.RegisterMethod(Engine.GlobalObj,'blog.article.create',blog_article_create,1);
But then I get the following error:
> blog.article.create('test'); // <-- Error: blog.article is undefined
I believe both "blog" and "blog.article" should be registered as objects but I do not know how this can be done.
Any help would be greatly appreciated.
OK then, I will do that
Thanks!
aContext.Request is nil too in that case
Hi,
I'm trying to access an interfaced based service method. It is working fine when called from the client, but when calling from the server, ServiceContext.Factory is always nil:
aContext := @mORMot.ServiceContext;
if Assigned(aContext.Factory) then // <---- Factory is nil when called from the server
...
I'm using this as I need to access the Rest server from that method, to do a DB.BackupBackground.
How can this be handled ? Should I not rely on ServiceContext and use a global instance of my server instead ? This won't be great as this method is shared by both client and server.
Any help would be greatly appreciated.
BTW, did you try to define the method as such:
procedure JSContext.SetOptions(Value: TJSOptions);
(i.e. without the "const")
Unfortunately this still produces an exception in XE3.
I've tried multiple compilation options without any success. I've also tried disabling IDEFixPack without success.
The new code doesn't change anything either. And indeed, it looks like a wrong value is passer to the method.
So this is what I tried and it fixed the problem is:
- Make JSContext.SetOptions public
- In TSMEngine.Create call it instead of assigning to the property
Not sure how this can be explained.
Somebody REALLY need SM for XE3?
No but it's not reassuring that a specific Delphi version crashes
Anything we can do to try to debug this issue ?
Also, perhaps this should be prominently written somewhere to avoid people thinking either they got the wrong DLLs or the code is not working at all.
I can confirm the exception for JS_SetOptions on Delphi XE 3
Thank you very much for taking the time to answer. I think accepting sponsored work through your company is a great idea and provides additional points for the mORMot project when used for commercial usages.
Hi,
I'm currently evaluating various options for a future rewrite of a big project and I'm really impressed with the quantity of work and dedication Arnaud Bouchez put in the mORMot framework, documentation, web-site and associated projects. Congratulation!
That being said, choosing a framework for a project not only implies making sure it is technically good, but also that is will be supported and enhanced in the long run. Being open source is obviously a great start but this is not a guarantee that this will be updated in the future (See some great TurboPower projects which are hardly maintained anymore). So I wonder what is the business model around the mORMot framework: Is there some paid support or product covering the development costs, or a company sponsoring the work on the framework or... ? OR is it a pure gift to the Delphi community out of Arnaud Bouchez' own time ?
Also, would you consider sponsored work should we need a custom feature/fix/advanced support in the future ?
Thank you for your time and great framework. I will be back on those forums as I'll spend time evaluating the framework
Best regards.
Pages: 1