Export to PDF: The specified path was not found

Hi!

I've got an exception when trying to export an excel file to pdf on one of my users' PC. It says "The specified path was not found". This exception raises only on one computer and doesn't appears on the others.
At
first I thought it was because of my incorrect using export utilities,
but the same error was occured when running demo project
(\Demo\Delphi\Modules\25.Printing and Exporting\30.ExportPdf).
I
think that the problem is most likely in the incorrect settings of a
particular computer, but I cannot find which settings causes it.

Unfortunately
I cannot run project on that computer in debug mode. So all I have
found out by now is that exception raises when executing function Canvas.MeasureStringEmptyHasHeight in unit _URenderMetrics.RenderMetrics. Here the stack I have by now:


DEMO unit UExportPdf;
    procedure TFExportPdf.ActionExportAsPdfExecute
        ...
        Pdf.ExportAllVisibleSheets(false, TPath.GetFileNameWithoutExtension(exportDialog.FileName));
        ...

        ↓
        
unit _UFlexCelPdfExport.FlexCelPdfExport;
    procedure TFlexCelPdfExport.ExportAllVisibleSheets
        ...
        ExportSheet(StartSheet, TotalPages);
        ...

        ↓

unit _UFlexCelPdfExport.FlexCelPdfExport;
    procedure TFlexCelPdfExport.ExportSheet
        ...
   
     FRenderer.GenericPrint(aCanvas, 
PdfGraphics.ConvertToUnits(PdfCanvas.PageSize), MyPrintRange, 
startPageToDisplay + i, PaintClipRect,
totalPagesToDisplay, ReallyExport, PagePrintRange, PrintArea);
        ...

        ↓

unit _UFlexCelRender.FlexCelRender;
    procedure FlexCelRender.GenericPrint
        ...
        //Draw Normal cells
        DrawPage(PaintClipRectDat, PagePrintRange, true, SpawnedCells, DrawObjects, false, TArtifactType.None);
        ...

        ↓

unit _UFlexCelRender.FlexCelRender;
    procedure FlexCelRender.DrawPage
        ...
        DrawCells(PaintClipRect, PagePrintRange, SpawnedCells, BorderRect, Tag);
        ...

        ↓

unit _UFlexCelRender.FlexCelRender
    procedure FlexCelRender.DrawCells
        ...
        DrawInsideCells(PaintClipRect, PagePrintRange, SpawnedCells, BorderRect, Tag);
        ...

        ↓

unit _UFlexCelRender.FlexCelRender
    procedure FlexCelRender.DrawInsideCells
        ...
        DrawColumns(PaintClipRect, PagePrintRange, SpawnedCells, BorderRect, Row, Ch, Cw, Tag);
        ...

        ↓

unit _UFlexCelRender.FlexCelRender
    procedure FlexCelRender.DrawColumns
        ...
        DrawCell(Col, Row, Col, Row, ARect, BorderRect, SpawnedCells, true, false, TSpanDirection.Both, Tag);
        ...

        ↓

unit _UFlexCelRender.FlexCelRender
    procedure FlexCelRender.DrawCell
        ...
   
     DrawText(FWorkbook, Canvas, Cache.FontCache, Zoom100, 
ReverseRightToLeftStrings, Self, aCol, aRow, CellRect0, PaintClipRect, 
SpawnedCells,
ReallyDraw, OnlySpawned, Clp, MultiLine, HAlignGeneral,
 HJustify, VJustify, Alpha, Vertical, DrawFont, DrawFontColor, SubData, 
HAlign, VAlign, Indent,
OutTextInfo, Merged, IsText, ActualValue, AdaptativeFormats, ShrinkToFit, SheetIsRightToLeft);
        ...

        ↓

unit _UFlexCelRender.FlexCelRender
    class procedure FlexCelRender.DrawText
        ...
   
     TextPainter.CalcTextBox(Canvas, FontCache, Zoom100, CellRect1, Clp,
 MultiLine, Alpha, Vertical, OutText, AFont, AF, TextExtent, TextLines,
MaxDescent);
        ...

        ↓

unit _UTextWriter.TextPainter
    class procedure TextPainter.CalcTextBox
        ...
        TextExtent := RenderMetrics.CalcTextExtent(Canvas, FontCache, Zoom100, AFont, OutText, mdx);
        ...

        ↓

unit _URenderMetrics.RenderMetrics
    class function RenderMetrics.CalcTextExtent
        ...
        exit(Canvas.MeasureStringEmptyHasHeight(Text, AFont, nil));
        ...



Do you have any idea what can cause "The specified path was not found" exception in function MeasureStringEmptyHasHeight and how to avoid it?

Thanks

Hi,

The pdf engine looks for the Windows font folder (I assume the issue is in Windows) to load the font data. An error of "The specified path was not found" while trying to measure a string seems to be related to FlexCel not finding the Windows Fonts path.

So I think the first thing to investigate would be this. Can you create an empty console application, paste the following code and tell me the results when you run it in the machine with the issue?


program Project41;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils, Winapi.ShlObj, IOUtils, WinAPI.Windows;

function GetFontPath: string;
var
  LStr: array[0 .. MAX_PATH] of Char;
begin
  Result := '';
  SetLastError(ERROR_SUCCESS);

  if SHGetFolderPath(0, CSIDL_FONTS, 0, 0, @LStr) = S_OK then
    Result := LStr;
end;

begin
  WriteLn('Path to fonts: "', GetFontPath, '"');
  if (TDirectory.Exists(GetFontPath)) then WriteLn('Path to fonts is ok.') else WriteLn('Can''t find path to fonts');

  WriteLn('Fonts in font folder: ', Length(TDirectory.GetFiles(GetFontPath, '*.?t?')));

end.





Hello, Adrian!

Looks like you are right:

Hi,

This is strange, but I've seen by googling that there are cases where SHGetFolderPath can return an error. What is worse, it seems to be deprecated. ( https://msdn.microsoft.com/en-us/library/windows/desktop/bb762181(v=vs.85).aspx ) 

So just in case, we can try with the alternative to see if it behaves better. Can you try the code below in the machine with the problem and let me know the results?



program Project41;


{$APPTYPE CONSOLE}


{$R *.res}


uses
  System.SysUtils, Winapi.ShlObj, IOUtils, WinAPI.Windows, ActiveX, KnownFolders;


function GetSpecialFolder(const rfid: TGUID): string;
var
  ResultPath: PWideChar;
begin
  Result := '';
  ResultPath := nil;
  if Succeeded(SHGetKnownFolderPath(rfid, KF_FLAG_DEFAULT, 0, ResultPath)) then
  begin
    Result := ResultPath;
    if (ResultPath <> nil) then CoTaskMemFree(ResultPath);
  end;
end;


function GetFontPath: string;
begin
  Result := GetSpecialFolder(FOLDERID_Fonts);
end;


function GetWindowsPath: string;
begin
  Result := GetSpecialFolder(FOLDERID_Windows);
end;


begin
  WriteLn('Path to fonts: "', GetFontPath, '"');
  if (TDirectory.Exists(GetFontPath)) then WriteLn('Path to fonts is ok.') else WriteLn('Can''t find path to fonts');


    WriteLn('Path to Windows: "', GetWindowsPath, '"');
  if (TDirectory.Exists(GetWindowsPath)) then WriteLn('Path to Windows is ok.') else WriteLn('Can''t find path to Windows');
  if (TDirectory.Exists(TPath.Combine(GetWindowsPath, 'Fonts'))) then WriteLn('Windows\Fonts exists') else WriteLn('No Windows\Fonts folder');
  


  Readln;
end.

Hi,

I've modiified your code a little cause it didn't show anything on that PC:


program Project17;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils, Winapi.ShlObj, IOUtils, WinAPI.Windows, ActiveX, KnownFolders;

function GetSpecialFolder(const rfid: TGUID): string;
var
  ResultPath: PWideChar;
begin
  Result := '';
  ResultPath := nil;
  if Succeeded(SHGetKnownFolderPath(rfid, KF_FLAG_DEFAULT, 0, ResultPath)) then
  begin
    Result := ResultPath;
    if (ResultPath <> nil) then CoTaskMemFree(ResultPath);
  end;
end;

function GetFontPath: string;
begin
  try
    Result := GetSpecialFolder(FOLDERID_Fonts);
  except
    on e: Exception do
      Result := e.ToString;
  end;
end;

function GetWindowsPath: string;
begin
  try
    Result := GetSpecialFolder(FOLDERID_Windows);
  except
    on e: Exception do
      Result := e.ToString;
  end;
end;

begin
  WriteLn('Path to fonts: "', GetFontPath, '"');
  if (TDirectory.Exists(GetFontPath)) then WriteLn('Path to fonts is ok.') else WriteLn('Can''t find path to fonts');

    WriteLn('Path to Windows: "', GetWindowsPath, '"');
  if (TDirectory.Exists(GetWindowsPath)) then WriteLn('Path to Windows is ok.') else WriteLn('Can''t find path to Windows');
 
 if (TDirectory.Exists(TPath.Combine(GetWindowsPath, 'Fonts'))) then 
WriteLn('Windows\Fonts exists') else WriteLn('No Windows\Fonts folder');

  Readln;
end.




And now it says:

That is weird, I can't get much information about this error anywhere. What Windows version has the machine with the problem? Is it running as a non admin user? Can you manually access c:\Windows\fonts with the windows explorer?


On our side, I've modified the error message so it tells you which folder is failing, but I am not sure we can do much more about it. FlexCel needs access to the Fonts folder to be able to export to pdf, and if Windows is throwing an exception when you try to get the font folder, there is not much we can do about it.

As a workaround you can use the OnGetFontFolder event to redirect FlexCel to use an specific folder that you know exists in the machine, or you can copy the fonts inside a "Fonts" folder immediately below your exe and FlexCel will read them from there. But you should be able to access the font folder in the machine.

Hi, Adrian!

That machine has Windows Server 2003 x64 installed:


It's running as non admin user. Moreover this machine is using as a terminal server for lots of users and all of them cannot export to pdf.
Yes, I can access manually to folder c:\Windows\fonts and I can see fonts there.
More over, if I copy this folder to direcory C:\Documents and Settings%username%\WINDOWS then export to pdf starts working fine too.
I'm not sure it's a good solution to copy fonts folder or manually set its path on OnGetFontFolder event. Maybe there is another way to solve it? Something with environment variables?

Anyway, thank you for help.



Hi, Adrian!

We've found out another solution of this problem. If you change function GetFontPath in unit _PdfFonts.TPdfEmbeddedFontList to this:

function GetFontPath: string;
var
  LStr: array[0 .. MAX_PATH] of Char;
begin
  Result := '';
  SetLastError(ERROR_SUCCESS);
  if TOSVersion.Major < 6 then
    Result := SysUtils.GetEnvironmentVariable('SystemRoot') + '\Fonts'
  else
    if SHGetFolderPath(0, CSIDL_FONTS, 0, 0, @LStr) = S_OK then
      Result := LStr;
end;


then export to PDF works fine on all machines we've tested.

Please consider this modification of the function GetFontPath in your original source code.

Hi,

We'll consider it, but I am not sure I like the solution. The main issue here is the "Fonts" folder hardcoded in the solution: It is a little bit better than just trying c:\Windows\Fonts directly. Yes, in a standard windows install fonts are in that folder, but you might have changed the defaults.

And for XP, SHGetFontFolder works fine, so it is a better option than GetEnvironmentVariable('SystemRoot') + '\Fonts' 

So there is the chance that by doing this change we break an existing working FlexCel app which was used in XP and in a non-standard Fonts folder not located under the normal "Windows" folder. Whenever we make a change like this, we want to be as sure as possible we aren't breaking someone else's app.

On the other side, both XP and Win 2003 are out of support by Microsoft, so I am not sure on how much this matters.

But the supported solution (which is sure not to break any existing app) is to just use the OnGetFontFolder and write the code you wrote above in that event. That's why the OnGetFontFolder exists at all: to allow you to modify the standard GetFontPath result without needing to change the source code.

But I'll see if something can be added directly in our code. Maybe not checking for the os version (which would make it not work in XP) but trying to do SHGetFolderPath and only trying the Systemroot\Fonts alternative if SHGetFolderPath fails.

Hi, Adrian!


That is what I want to suggest too :)

Hi,
Version 6.17.3 now tries with the 3 methods: First it tries ShGetKnownFolderPath (won't work in XP/2003, but allows paths longer than MAXPATH), if it fails it tries ShGetFolderPath (should work in XP/2003 and it does here, but for some reason fails in your machine), and if that fails we try with SystemRoot variable + Fonts.

Thank you VERY much!