Is there a limitation on dimensions of Windows Metafiles? - delphi

I'm creating some .wmf files, but some of them seem corrupted and can't be shown in any metafile viewer. After some trial and error, I found that the problem is caused by their dimensions. If I scale the same drawing by a factor to reduce the dimensions, it will be shown.
Now, I want to know if there's a limitation on the size of drawing or if the problem is something else. I know that these files have a 16-bit data structure, so I guess that the limitation would be 2^16 units in each dimension, (or 2^15 if it's signed). But in my tests it is around 25,000. So I can't rely on this value since the limitation can be on anything (Width*Height maybe, or maybe the resolution of the drawing may affect it). I can't find a reliable resource about .wmf files that describes this.
Here is sample code that shows the problem:
procedure DrawWMF(const Rect: TRect; const Scale: Double; FileName: string);
var
Metafile: TMetafile;
Canvas: TMetafileCanvas;
W, H: Integer;
begin
W := Round(Rect.Width * Scale);
H := Round(Rect.Height * Scale);
Metafile := TMetafile.Create;
Metafile.SetSize(W, H);
Canvas := TMetafileCanvas.Create(Metafile, 0);
Canvas.LineTo(W, H);
Canvas.Free;
Metafile.SaveToFile(FileName);
Metafile.Free;
end;
procedure TForm1.Button1Click(Sender: TObject);
const
Dim = 40000;
begin
DrawWMF(Rect(0, 0, Dim, Dim), 1.0, 'Original.wmf');
DrawWMF(Rect(0, 0, Dim, Dim), 0.5, 'Scaled.wmf');
try
Image1.Picture.LoadFromFile('Original.wmf');
except
Image1.Picture.Assign(nil);
end;
try
Image2.Picture.LoadFromFile('Scaled.wmf');
except
Image2.Picture.Assign(nil);
end;
end;
PS: I know that setting Metafile.Enhanced to True and saving it as an .emf file will solve the problem, but the destination application that I'm generating files for doesn't support Enhanced Metafiles.
Edit:
As mentioned in answers below, there are two different problems here:
The main problem is about the file itself, it has a 2^15 limitation on each dimension. If either width or height of the drawing overpasses this value, delphi will write a corrupted file. You can find more details in Sertac's answer.
The second problem is about loading the file in a TImage. There's another limitation when you want to show the image in a delphi VCL application. This one is system dependent and is related to dpi of DC that the drawing is going to be painted on. Tom's answer describes this in details. Passing 0.7 as Scale to DrawWMF (code sample above) reproduces this situation on my PC. The generated file is OK and can be viewed with other Metafile viewers (I use MS Office Picture Manager) but VCL fails to show it, however, no exception is raised while loading the file.

Your limit is 32767.
Tracing VCL code, the output file gets corrupt in TMetafile.WriteWMFStream. VCL writes a WmfPlaceableFileHeader (TMetafileHeader in VCL) record and then calls GetWinMetaFileBits to have 'emf' records converted to 'wmf' records. This function fails if any of the dimensions of the bounding rectangle (used when calling CreateEnhMetaFile) is greater than 32767. Not checking the return value, VCL does not raise any exception and closes the file with only 22 bytes - having only the "placeable header".
Even for dimensions less than 32767, the "placeable header" may have possible wrong values (read details about the reason and implications from Tom's answer and comments to the answer), but more on this later...
I used the below code to find the limit. Note that GetWinMetaFileBits does not get called with an enhanced metafile in VCL code.
function IsDimOverLimit(W, H: Integer): Boolean;
var
Metafile: TMetafile;
RefDC: HDC;
begin
Metafile := TMetafile.Create;
Metafile.SetSize(W, H);
RefDC := GetDC(0);
TMetafileCanvas.Create(Metafile, RefDC).Free;
Result := GetWinMetaFileBits(MetaFile.Handle, 0, nil, MM_ANISOTROPIC, RefDC) > 0;
ReleaseDC(0, RefDC);
Metafile.Free;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
i: Integer;
begin
for i := 20000 to 40000 do
if not IsDimOverLimit(100, i) then begin
ShowMessage(SysErrorMessage(GetLastError)); // ReleaseDc and freeing meta file does not set any last error
Break;
end;
end;
The error is a 534 ("Arithmetic result exceeded 32 bits"). Obviously there's some signed integer overflow. Some 'mf3216.dll' ("32-bit to 16-bit Metafile Conversion DLL") sets the error during a call by GetWinMetaFileBits to its exported ConvertEmfToWmf function, but that doesn't lead to any documentation regarding the overflow. The only official documentation regarding wmf limitations I can find is this (its main point is "use wmf only in 16 bit executables" :)).
As mentioned earlier, the bogus "placeable header" structure may have "bogus" values and this may prevent the VCL from correctly playing the metafile. Specifically, dimensions of the metafile, as the VCL know them, may overflow. You may perform a simple sanity check after you have loaded the images for them to be displayed properly:
var
Header: TEnhMetaHeader;
begin
DrawWMF(Rect(0, 0, Dim, Dim), 1.0, 'Original.wmf');
DrawWMF(Rect(0, 0, Dim, Dim), 0.5, 'Scaled.wmf');
try
Image1.Picture.LoadFromFile('Original.wmf');
if (TMetafile(Image1.Picture.Graphic).Width < 0) or
(TMetafile(Image1.Picture.Graphic).Height < 0) then begin
GetEnhMetaFileHeader(TMetafile(Image1.Picture.Graphic).Handle,
SizeOf(Header), #Header);
TMetafile(Image1.Picture.Graphic).Width := MulDiv(Header.rclFrame.Right,
Header.szlDevice.cx, Header.szlMillimeters.cx * 100);
TMetafile(Image1.Picture.Graphic).Height := MulDiv(Header.rclFrame.Bottom,
Header.szlDevice.cy, Header.szlMillimeters.cy * 100);
end;
...

When docs don't help, look at the source :). The file creation fails if the either width or height is too big, and the file becomes invalid. In the following I look at the horizontal dimension only, but the vertical dimension is treated the same.
In Vcl.Graphics:
constructor TMetafileCanvas.CreateWithComment(AMetafile : TMetafile;
ReferenceDevice: HDC; const CreatedBy, Description: String);
FMetafile.MMWidth := MulDiv(FMetafile.Width,
GetDeviceCaps(RefDC, HORZSIZE) * 100, GetDeviceCaps(RefDC, HORZRES));
If ReferenceDevice is not defined, then the screen (GetDC(0)) is used. On my machine horizontal size is reported as 677 and horizontal resolution as 1920. Thus FMetafile.MMWidth := 40000 * 67700 div 1920 ( = 1410416). Since FMetaFile.MMWidth is an integer, no problems at this point.
Next, let's look at the file writing, which is done with WriteWMFStream because we write to a .wmf file:
procedure TMetafile.WriteWMFStream(Stream: TStream);
var
WMF: TMetafileHeader;
...
begin
...
Inch := 96 { WMF defaults to 96 units per inch }
...
Right := MulDiv(FWidth, WMF.Inch, HundredthMMPerInch);
...
The WMF header structure indicates where things are going south
TMetafileHeader = record
Key: Longint;
Handle: SmallInt;
Box: TSmallRect; // smallint members
Inch: Word;
Reserved: Longint;
CheckSum: Word;
end;
The Box: TSmallRect field can not hold bigger coordinates than smallint-sized values.
Right is calculated as Right := 1410417 * 96 div 2540 ( = 53307 as smallint= -12229). The dimensions of the image overflows and the wmf data can not be 'played' to the file.
The question rizes: What dimensions can I use on my machine?
Both FMetaFile.MMWidth and FMetaFile.MMHeight needs to be less or equal to
MaxSmallInt * HundredthMMPerInch div UnitsPerInch or
32767 * 2540 div 96 = 866960
On my testmachine horizontal display size and resolution are 677 and 1920. Vertical display size and resolution are 381 and 1080. Thus maximum dimensions of a metafile becomes:
Horizontal: 866960 * 1920 div 67700 = 24587
Vertical: 866960 * 1080 div 38100 = 24575
Verified by testing.
Update after further investigation inspired by comments:
With horizontal and vertical dimension up to 32767, the metafile is readable with some applications, f.ex. GIMP, it shows the image. Possibly this is due to those programs considering the extents of the drawing as word instead of SmallInt. GIMP reported pixels per inch to be 90 and when changed to 96 (which is the value used by Delphi, GIMP chrashed with a 'GIMP Message: Plug-in crashed: "file-wmf.exe".
The procedure in the OP does not show an error message with dimensions of 32767 or less. However, if either dimension is higher than previously presented calculated max value, the drawing is not shown. When reading the metafile, the same TMetafileHeader structure type is used as when saving and the FWidth and FHeight get negative values:
procedure TMetafile.ReadWMFStream(Stream: TStream; Length: Longint);
...
FWidth := MulDiv(WMF.Box.Right - WMF.Box.Left, HundredthMMPerInch, WMF.Inch);
FHeight := MulDiv(WMF.Box.Bottom - WMF.Box.Top, HundredthMMPerInch, WMF.Inch);
procedure TImage.PictureChanged(Sender: TObject);
if AutoSize and (Picture.Width > 0) and (Picture.Height > 0) then
SetBounds(Left, Top, Picture.Width, Picture.Height);
The negative values ripple through to the Paint procedure in the DestRect function and the image is therefore not seen.
procedure TImage.Paint;
...
with inherited Canvas do
StretchDraw(DestRect, Picture.Graphic);
DestRect has negative values for Right and Bottom
I maintain that the only way to find actual limit is to call GetDeviceCaps() for both horizontal and vertical size and resolution, and perform the calculations above. Note however, the file may still not be displayable with a Delphi program on another machine. Keeping the drawing size within 20000 x 20000 is probably a safe limit.

Related

Increasing the size or depth of a TBitmap

When I increase the size of a bitmap by (for example) ...
bitmap.Width := bitmap.Width + 30;
... do I have any control of what the right-hand 30 pixels get filled with, or do I just assume they're uninitialized
Similarly if I change PixelFormat from pf24bit to pf32bit, can I control what the alpha bytes are initialized with?
Embarrasingly newbie question, but my google-fu has failed me. :)
Since it isn't defined in the documentation what happens in this instance, you cannot rely on anything. Most likely the new pixels will be 0 (black), but if it is important to you what they are, you should explicitly fill them:
FUNCTION ExpandBitMap(BMP : TBitMap ; AddW,AddH : Cardinal ; FillColor : TColor = clBlack) : TBitMap;
VAR
OrgW,OrgH : Cardinal;
BEGIN
OrgW:=BMP.Width; OrgH:=BMP.Height;
BMP.Width:=OrgW+AddW; BMP.Height:=OrgH+AddH;
BMP.Canvas.Brush.Color:=FillColor;
BMP.Canvas.Brush.Style:=bsSolid;
BMP.Canvas.FillRect(Rect(OrgW,0,BMP.Width,BMP.Height));
BMP.Canvas.FillRect(Rect(0,OrgH,OrgW,BMP.Height));
Result:=BMP
END;
Likewise with the Alpha channel - I'll leave it as an exercise to the user to make a similar function :-).

Font change with different computers? [duplicate]

When choosing large font sizes in the Windows control panel (like 125%, or 150%) then there are problems in a VCL application, every time something has been set pixelwise.
Take the TStatusBar.Panel. I have set its width so that it contains exactly one label, now with big fonts the label "overflows". Same problem with other components.
Some new laptops from Dell ship already with 125% as default setting, so while in the past this problem was quite rare now it is really important.
What can be done to overcome this problem?
Your settings in the .dfm file will be scaled up correctly, so long as Scaled is True.
If you are setting dimensions in code then you need to scale them by Screen.PixelsPerInch divided by Form.PixelsPerInch. Use MulDiv to do this.
function TMyForm.ScaleDimension(const X: Integer): Integer;
begin
Result := MulDiv(X, Screen.PixelsPerInch, PixelsPerInch);
end;
This is what the form persistence framework does when Scaled is True.
In fact, you can make a cogent argument for replacing this function with a version that hard codes a value of 96 for the denominator. This allows you to use absolute dimension values and not worry about the meaning changing if you happen to change font scaling on your development machine and re-save the .dfm file. The reason that matters is that the PixelsPerInch property stored in the .dfm file is the value of the machine on which the .dfm file was last saved.
const
SmallFontsPixelsPerInch = 96;
function ScaleFromSmallFontsDimension(const X: Integer): Integer;
begin
Result := MulDiv(X, Screen.PixelsPerInch, SmallFontsPixelsPerInch);
end;
So, continuing the theme, another thing to be wary of is that if your project is developed on multiple machines with different DPI values, you will find that the scaling that Delphi uses when saving .dfm files results in controls wandering over a series of edits. At my place of work, to avoid this, we have a strict policy that forms are only ever edited at 96dpi (100% scaling).
In fact my version of ScaleFromSmallFontsDimension also makes allowance for the possibility of the form font differing at runtime from that set at designtime. On XP machines my application's forms use 8pt Tahoma. On Vista and up 9pt Segoe UI is used. This provides yet another degree of freedom. The scaling must account for this because the absolute dimension values used in the source code are assumed to be relative to the baseline of 8pt Tahoma at 96dpi.
If you use any images or glyphs in your UI then these need to scale too. A common example would be the glyphs that are used on toolbars and menus. You'll want to provide these glyphs as icon resources linked to your executable. Each icon should contain a range of sizes and then at runtime you choose the most appropriate size and load it into an image list. Some details on that topic can be found here: How do I load icons from a resource without suffering from aliasing?
Another useful trick is to define dimensions in relative units, relative to TextWidth or TextHeight. So, if you want something to be around 10 vertical lines in size you can use 10*Canvas.TextHeight('Ag'). This is a very rough and ready metric because it doesn't allow for line spacing and so on. However, often all you need to do is be able to arrange that the GUI scales correctly with PixelsPerInch.
You should also mark your application as being high DPI aware. The best way to do this is through the application manifest. Since Delphi's build tools don't allow you to customise the manifest you use this forces you to link your own manifest resource.
<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<asmv3:application xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<asmv3:windowsSettings
xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
<dpiAware>true</dpiAware>
</asmv3:windowsSettings>
</asmv3:application>
</assembly>
The resource script looks like this:
1 24 "Manifest.txt"
where Manifest.txt contains the actual manifest. You would also need to include the comctl32 v6 section and set requestedExecutionLevel to asInvoker. You then link this compiled resource to your app and make sure that Delphi doesn't try to do the same with its manifest. In modern Delphi you achieve that by setting the Runtime Themes project option to None.
The manifest is the right way to declare your app to be high DPI aware. If you just want to try it out quickly without messing with your manifest, call SetProcessDPIAware. Do so as the very first thing you do when your app runs. Preferably in one of the early unit initialization sections, or as the first thing in your .dpr file.
If you don't declare your app to be high DPI aware then Vista and up will render it in a legacy mode for any font scaling above 125%. This looks quite dreadful. Try to avoid falling into that trap.
Windows 8.1 per monitor DPI update
As of Windows 8.1, there is now OS support for per-monitor DPI settings (http://msdn.microsoft.com/en-ca/magazine/dn574798.aspx). This is a big issue for modern devices which might have different displays attached with very different capabilities. You might have a very high DPI laptop screen, and a low DPI external projector. Supporting such a scenario takes even more work than described above.
Note: Please see the other answers as they contain very valuable techniques. My answer here only provides caveats and cautions against assuming DPI-awareness is easy.
I generally avoid DPI-aware scaling with TForm.Scaled = True. DPI awareness is only important to me when it becomes important to customers who call me and are willing to pay for it. The technical reason behind that point of view is that DPI-awareness or not, you are opening a window into a world of hurt. Many standard and third party VCL controls do not work well in High DPI. The notable exception that the VCL parts that wrap Windows Common Controls work remarkably well at high DPI. A huge number of third party and built-in Delphi VCL custom controls do not work well, or at all, at high DPI. If you plan to turn on TForm.Scaled be sure to test at 96, 125, and 150 DPI for every single form in your project, and every single third party and built in control that you use.
Delphi itself is written in Delphi. It has the High DPI awareness flag turned on, for most forms, although even as recently as in Delphi XE2, the IDE authors themselves decided NOT to turn that High DPI Awareness manifest flag on. Note that in Delphi XE4 and later, the HIGH DPI awareness flag is turned on, and the IDE looks good.
I suggest that you do not use TForm.Scaled=true (which is a default in Delphi so unless you've modified it, most of your forms have Scaled=true) with the High DPI Aware flags (as shown in David's answers) with VCL applications that are built using the built-in delphi form designer.
I have tried in the past to make a minimal sample of the kind of breakage you can expect to see when TForm.Scaled is true, and when Delphi form scaling has a glitch. These glitches are not always and only triggered by a DPI value other than 96. I have been unable to determine a complete list of other things, that includes Windows XP font size changes. But since most of these glitches appear only in my own applications, in fairly complex situations, I have decided to show you some evidence you can verify yourselves.
Delphi XE looks like this when you set the DPI Scaling to "Fonts # 200%" in Windows 7, and Delphi XE2 is similarly broken on Windows 7 and 8, but these glitches appear to be fixed as of Delphi XE4:
These are mostly Standard VCL controls that are misbehaving at high DPI. Note that most things have not been scaled at all, so the Delphi IDE developers have decided to ignore the DPI awareness, as well as turning off the DPI virtualization. Such an interesting choice.
Turn off DPI virtualization only if want this new additional source of pain, and difficult choices. I suggest you leave it alone. Note that Windows common controls mostly seem to work fine. Note that the Delphi data-explorer control is a C# WinForms wrapper around a standard Windows Tree common control. That's a pure microsoft glitch, and fixing it might either require Embarcadero to rewrite a pure native .Net tree control for their data explorer, or to write some DPI-check-and-modify-properties code to change item heights in the control. Not even microsoft WinForms can handle high DPI cleanly, automatically and without custom kludge code.
Update: Interesting factoid: While the delphi IDE appears not to be "virtualized", it is not using the manifest content shown by David to achieve "non-DPI-virtualization". Perhaps it is using some API function at runtime.
Update 2: In response to how I would support 100%/125% DPI, I would come up with a two-phase plan. Phase 1 is to inventory my code for custom controls that need to be fixed for high DPI, and then make a plan to fix them or phase them out. Phase 2 would be to take some areas of my code which are designed as forms without layout management and change them over to forms that use some kind of layout management so that DPI or font height changes can work without clipping. I suspect that this "inter-control" layout work would be far more complex in most applications than the "intra-control" work.
Update: In 2016, the latest Delphi 10.1 Berlin is working well on my 150 dpi workstation.
It's also important to note that honoring the user's DPI is only a subset of your real job:
honoring the user's font size
The user's DPI is a side-effect of their font choice.
if you honor the user's font size, you will by definition be honoring their DPI (good)
if you only honor the user's DPI, you will not be honoring their font choice (bad)
Windows developers need to stop thinking that honoring DPI is something they want to do. You don't want to honor their DPI. DPI is not a setting you want to be honoring. If you honor DPI you are doing it wrong.
You want to honor their font. (which affects DPI)
For decades, Windows has solved this issue with the notion performing layout using Dialog Units, rather than pixels. A "dialog unit" is defined so that font's average character is
4 dialog units (dlus) wide, and
8 dialog units (dlus) high
Delphi does ship with a (buggy) notion of TCustomForm.Scaled, where a form tries to automatically adjust based on the
Windows DPI settings of the user, verses
the DPI setting on the machine of the developer who last saved the form
That doesn't solve the problem when the user uses a font different from what you designed the form with, e.g.:
developer designed the form with MS Sans Serif 8pt (where the average character is 6.21px x 13.00px at 96dpi)
user running with Tahoma 8pt (where the average character is 5.94px x 13.00px at 96dpi)
As was the case with anyone developing an application for Windows 2000 or Windows XP.
or
developer designed the form with Tahoma 8pt (where the average character is 5.94px x 13.00px at 96dpi)
a user running with Segoe UI 9pt (where the average character is 6.67px x 15px, at 96dpi)
Using TCustomForm.Scaled is bad. It's a bad idea. It's a bad choice. It's a bad design. You need to turn .Scaled of all your forms to False at design time.
As a good developer your goal is to honor your user's font preferences. This means that you also need to scale all controls on your form to match the new font size:
expand everything horizontally by 12.29% (6.67/5.94)
stretch everything vertically by 15.38% (15/13)
Scaled won't handle this for you.
It gets worse when:
designed your form at Segoe UI 9pt (the Windows Vista, Windows 7, Windows 8 default)
user is running Segoe UI 14pt, (e.g. my preference) which is 10.52px x 25px
Now you have to scale everything
horizontally by 57.72%
vertically by 66.66%
Scaled won't handle this for you.
If you're smart you can see how honoring DPI is irrelavent:
form designed with Segoe UI 9pt # 96dpi (6.67px x 15px)
user running with Segoe UI 9pt # 150dpi (10.52px x 25px)
You should not be looking at the user's DPI setting, you should be looking at their font size. Two users running:
Segoe UI 14pt # 96dpi (10.52px x 25px)
Segoe UI 9pt # 150dpi (10.52px x 25px)
are running the same font. DPI is just one thing that affects font size; the user's preferences are the other.
StandardizeFormFont
Clovis noticed that i reference a function StandardizeFormFont that fixes the font on a form, and scales it to the new font size. It's not a standard function, but an entire set of functions that accomplish the simple task that Borland never handled.
function StandardizeFormFont(AForm: TForm): Real;
var
preferredFontName: string;
preferredFontHeight: Integer;
begin
GetUserFontPreference({out}preferredFontName, {out}preferredFontHeight);
//e.g. "Segoe UI",
Result := Toolkit.StandardizeFormFont(AForm, PreferredFontName, PreferredFontHeight);
end;
There is no single "font setting" in Windows. Windows has 6 different fonts:
Font
How to retrieve
Icon Title
SystemParametersInfo(SPI_GETICONTITLELOGFONT)
Caption
SystemParametersInfo(SPI_GETNONCLIENTMETRICS).lfCaptionFont
Small Caption
SystemParametersInfo(SPI_GETNONCLIENTMETRICS).lfSmCaptionFont
Menu
SystemParametersInfo(SPI_GETNONCLIENTMETRICS).lfMenuFont
Status
SystemParametersInfo(SPI_GETNONCLIENTMETRICS).lfStatusFont
Message
SystemParametersInfo(SPI_GETNONCLIENTMETRICS).lfMessageFont
But we know from experience that our forms should follow the Icon Title Font setting
procedure GetUserFontPreference(out FaceName: string; out PixelHeight: Integer);
var
font: TFont;
begin
font := Toolkit.GetIconTitleFont;
try
FaceName := font.Name; //e.g. "Segoe UI"
//Dogfood testing: use a larger font than we're used to; to force us to actually test it
if IsDebuggerPresent then
font.Size := font.Size+1;
PixelHeight := font.Height; //e.g. -16
finally
font.Free;
end;
end;
Once we know the font size we will scale the form to, we get the form's current font height (in pixels), and scale up by that factor.
For example, if i am setting the form to -16, and the form is currently at -11, then we need to scale the entire form by:
-16 / -11 = 1.45454%
The standardization happens in two phases. First scale the form by the ratio of the new:old font sizes. Then actually change the controls (recursively) to use the new font.
function StandardizeFormFont(AForm: TForm; FontName: string; FontHeight: Integer): Real;
var
oldHeight: Integer;
begin
Assert(Assigned(AForm));
if (AForm.Scaled) then
begin
OutputDebugString(PChar('WARNING: StandardizeFormFont: Form "'+GetControlName(AForm)+'" is set to Scaled. Proper form scaling requires VCL scaling to be disabled, unless you implement scaling by overriding the protected ChangeScale() method of the form.'));
end;
if (AForm.AutoScroll) then
begin
if AForm.WindowState = wsNormal then
begin
OutputDebugString(PChar('WARNING: StandardizeFormFont: Form "'+GetControlName(AForm)+'" is set to AutoScroll. Form designed size will be suseptable to changes in Windows form caption height (e.g. 2000 vs XP).'));
if IsDebuggerPresent then
Windows.DebugBreak; //Some forms would like it (to fix maximizing problem)
end;
end;
if (not AForm.ShowHint) then
begin
AForm.ShowHint := True;
OutputDebugString(PChar('INFORMATION: StandardizeFormFont: Turning on form "'+GetControlName(AForm)+'" hints. (ShowHint := True)'));
if IsDebuggerPresent then
Windows.DebugBreak; //Some forms would like it (to fix maximizing problem)
end;
oldHeight := AForm.Font.Height;
//Scale the form to the new font size
// if (FontHeight <> oldHeight) then For compatibility, it's safer to trigger a call to ChangeScale, since a lot of people will be assuming it always is called
begin
ScaleForm(AForm, FontHeight, oldHeight);
end;
//Now change all controls to actually use the new font
Toolkit.StandardizeFont_ControlCore(AForm, g_ForceClearType, FontName, FontHeight,
AForm.Font.Name, AForm.Font.Size);
//Return the scaling ratio, so any hard-coded values can be multiplied
Result := FontHeight / oldHeight;
end;
Here's the job of actually scaling a form. It works around bugs in Borland's own Form.ScaleBy method. First it has to disable all anchors on the form, then perform the scaling, then re-enable the anchors:
TAnchorsArray = array of TAnchors;
procedure ScaleForm(const AForm: TForm; const M, D: Integer);
var
aAnchorStorage: TAnchorsArray;
RectBefore, RectAfter: TRect;
x, y: Integer;
monitorInfo: TMonitorInfo;
workArea: TRect;
begin
if (M = 0) and (D = 0) then
Exit;
RectBefore := AForm.BoundsRect;
SetLength(aAnchorStorage, 0);
aAnchorStorage := DisableAnchors(AForm);
try
AForm.ScaleBy(M, D);
finally
EnableAnchors(AForm, aAnchorStorage);
end;
RectAfter := AForm.BoundsRect;
case AForm.Position of
poScreenCenter, poDesktopCenter, poMainFormCenter, poOwnerFormCenter,
poDesigned: //i think i really want everything else to also follow the nudging rules...why did i exclude poDesigned
begin
//This was only nudging by one quarter the difference, rather than one half the difference
// x := RectAfter.Left - ((RectAfter.Right-RectBefore.Right) div 2);
// y := RectAfter.Top - ((RectAfter.Bottom-RectBefore.Bottom) div 2);
x := RectAfter.Left - ((RectAfter.Right-RectAfter.Left) - (RectBefore.Right-RectBefore.Left)) div 2;
y := RectAfter.Top - ((RectAfter.Bottom-RectAfter.Top)-(RectBefore.Bottom-RectBefore.Top)) div 2;
end;
else
//poDesigned, poDefault, poDefaultPosOnly, poDefaultSizeOnly:
x := RectAfter.Left;
y := RectAfter.Top;
end;
if AForm.Monitor <> nil then
begin
monitorInfo.cbSize := SizeOf(monitorInfo);
if GetMonitorInfo(AForm.Monitor.Handle, #monitorInfo) then
workArea := monitorInfo.rcWork
else
begin
OutputDebugString(PChar(SysErrorMessage(GetLastError)));
workArea := Rect(AForm.Monitor.Left, AForm.Monitor.Top, AForm.Monitor.Left+AForm.Monitor.Width, AForm.Monitor.Top+AForm.Monitor.Height);
end;
// If the form is off the right or bottom of the screen then we need to pull it back
if RectAfter.Right > workArea.Right then
x := workArea.Right - (RectAfter.Right-RectAfter.Left); //rightEdge - widthOfForm
if RectAfter.Bottom > workArea.Bottom then
y := workArea.Bottom - (RectAfter.Bottom-RectAfter.Top); //bottomEdge - heightOfForm
x := Max(x, workArea.Left); //don't go beyond left edge
y := Max(y, workArea.Top); //don't go above top edge
end
else
begin
x := Max(x, 0); //don't go beyond left edge
y := Max(y, 0); //don't go above top edge
end;
AForm.SetBounds(x, y,
RectAfter.Right-RectAfter.Left, //Width
RectAfter.Bottom-RectAfter.Top); //Height
end;
and then we have to recursively actually use the new font:
procedure StandardizeFont_ControlCore(AControl: TControl; ForceClearType: Boolean;
FontName: string; FontSize: Integer;
ForceFontIfName: string; ForceFontIfSize: Integer);
const
CLEARTYPE_QUALITY = 5;
var
i: Integer;
RunComponent: TComponent;
AControlFont: TFont;
begin
if not Assigned(AControl) then
Exit;
if (AControl is TStatusBar) then
begin
TStatusBar(AControl).UseSystemFont := False; //force...
TStatusBar(AControl).UseSystemFont := True; //...it
end
else
begin
AControlFont := Toolkit.GetControlFont(AControl);
if not Assigned(AControlFont) then
Exit;
StandardizeFont_ControlFontCore(AControlFont, ForceClearType,
FontName, FontSize,
ForceFontIfName, ForceFontIfSize);
end;
{ If a panel has a toolbar on it, the toolbar won't paint properly. So this idea won't work.
if (not Toolkit.IsRemoteSession) and (AControl is TWinControl) and (not (AControl is TToolBar)) then
TWinControl(AControl).DoubleBuffered := True;
}
//Iterate children
for i := 0 to AControl.ComponentCount-1 do
begin
RunComponent := AControl.Components[i];
if RunComponent is TControl then
StandardizeFont_ControlCore(
TControl(RunComponent), ForceClearType,
FontName, FontSize,
ForceFontIfName, ForceFontIfSize);
end;
end;
With the anchors being recursively disabled:
function DisableAnchors(ParentControl: TWinControl): TAnchorsArray;
var
StartingIndex: Integer;
begin
StartingIndex := 0;
DisableAnchors_Core(ParentControl, Result, StartingIndex);
end;
procedure DisableAnchors_Core(ParentControl: TWinControl; var aAnchorStorage: TAnchorsArray; var StartingIndex: Integer);
var
iCounter: integer;
ChildControl: TControl;
begin
if (StartingIndex+ParentControl.ControlCount+1) > (Length(aAnchorStorage)) then
SetLength(aAnchorStorage, StartingIndex+ParentControl.ControlCount+1);
for iCounter := 0 to ParentControl.ControlCount - 1 do
begin
ChildControl := ParentControl.Controls[iCounter];
aAnchorStorage[StartingIndex] := ChildControl.Anchors;
//doesn't work for set of stacked top-aligned panels
// if ([akRight, akBottom ] * ChildControl.Anchors) <> [] then
// ChildControl.Anchors := [akLeft, akTop];
if (ChildControl.Anchors) <> [akTop, akLeft] then
ChildControl.Anchors := [akLeft, akTop];
// if ([akTop, akBottom] * ChildControl.Anchors) = [akTop, akBottom] then
// ChildControl.Anchors := ChildControl.Anchors - [akBottom];
Inc(StartingIndex);
end;
//Add children
for iCounter := 0 to ParentControl.ControlCount - 1 do
begin
ChildControl := ParentControl.Controls[iCounter];
if ChildControl is TWinControl then
DisableAnchors_Core(TWinControl(ChildControl), aAnchorStorage, StartingIndex);
end;
end;
And anchors being recursively re-enabled:
procedure EnableAnchors(ParentControl: TWinControl; aAnchorStorage: TAnchorsArray);
var
StartingIndex: Integer;
begin
StartingIndex := 0;
EnableAnchors_Core(ParentControl, aAnchorStorage, StartingIndex);
end;
procedure EnableAnchors_Core(ParentControl: TWinControl; aAnchorStorage: TAnchorsArray; var StartingIndex: Integer);
var
iCounter: integer;
ChildControl: TControl;
begin
for iCounter := 0 to ParentControl.ControlCount - 1 do
begin
ChildControl := ParentControl.Controls[iCounter];
ChildControl.Anchors := aAnchorStorage[StartingIndex];
Inc(StartingIndex);
end;
//Restore children
for iCounter := 0 to ParentControl.ControlCount - 1 do
begin
ChildControl := ParentControl.Controls[iCounter];
if ChildControl is TWinControl then
EnableAnchors_Core(TWinControl(ChildControl), aAnchorStorage, StartingIndex);
end;
end;
With the work of actually changing a controls font left to:
procedure StandardizeFont_ControlFontCore(AControlFont: TFont; ForceClearType: Boolean;
FontName: string; FontSize: Integer;
ForceFontIfName: string; ForceFontIfSize: Integer);
const
CLEARTYPE_QUALITY = 5;
var
CanChangeName: Boolean;
CanChangeSize: Boolean;
lf: TLogFont;
begin
if not Assigned(AControlFont) then
Exit;
{$IFDEF ForceClearType}
ForceClearType := True;
{$ELSE}
if g_ForceClearType then
ForceClearType := True;
{$ENDIF}
//Standardize the font if it's currently
// "MS Shell Dlg 2" (meaning whoever it was opted into the 'change me' system
// "MS Sans Serif" (the Delphi default)
// "Tahoma" (when they wanted to match the OS, but "MS Shell Dlg 2" should have been used)
// "MS Shell Dlg" (the 9x name)
CanChangeName :=
(FontName <> '')
and
(AControlFont.Name <> FontName)
and
(
(
(ForceFontIfName <> '')
and
(AControlFont.Name = ForceFontIfName)
)
or
(
(ForceFontIfName = '')
and
(
(AControlFont.Name = 'MS Sans Serif') or
(AControlFont.Name = 'Tahoma') or
(AControlFont.Name = 'MS Shell Dlg 2') or
(AControlFont.Name = 'MS Shell Dlg')
)
)
);
CanChangeSize :=
(
//there is a font size
(FontSize <> 0)
and
(
//the font is at it's default size, or we're specifying what it's default size is
(AControlFont.Size = 8)
or
((ForceFontIfSize <> 0) and (AControlFont.Size = ForceFontIfSize))
)
and
//the font size (or height) is not equal
(
//negative for height (px)
((FontSize < 0) and (AControlFont.Height <> FontSize))
or
//positive for size (pt)
((FontSize > 0) and (AControlFont.Size <> FontSize))
)
and
//no point in using default font's size if they're not using the face
(
(AControlFont.Name = FontName)
or
CanChangeName
)
);
if CanChangeName or CanChangeSize or ForceClearType then
begin
if GetObject(AControlFont.Handle, SizeOf(TLogFont), #lf) <> 0 then
begin
//Change the font attributes and put it back
if CanChangeName then
StrPLCopy(Addr(lf.lfFaceName[0]), FontName, LF_FACESIZE);
if CanChangeSize then
lf.lfHeight := FontSize;
if ForceClearType then
lf.lfQuality := CLEARTYPE_QUALITY;
AControlFont.Handle := CreateFontIndirect(lf);
end
else
begin
if CanChangeName then
AControlFont.Name := FontName;
if CanChangeSize then
begin
if FontSize > 0 then
AControlFont.Size := FontSize
else if FontSize < 0 then
AControlFont.Height := FontSize;
end;
end;
end;
end;
That's a whole lot more code than you thought it was going to be; i know. The sad thing is that there is no Delphi developer on earth, except for me, who actually makes their applications correct.
Dear Delphi Developer: Set your Windows font to Segoe UI 14pt, and fix your buggy application
Note: Any code is released into the public domain. No attribution required.
Here is my gift. A function that can help you with horizontal positioning of elements in your GUI layouts. Free for all.
function CenterInParent(Place,NumberOfPlaces,ObjectWidth,ParentWidth,CropPercent: Integer): Integer;
{returns formated centered position of an object relative to parent.
Place - P order number of an object beeing centered
NumberOfPlaces - NOP total number of places available for object beeing centered
ObjectWidth - OW width of an object beeing centered
ParentWidth - PW width of an parent
CropPercent - CP percentage of safe margin on both sides which we want to omit from calculation
+-----------------------------------------------------+
| |
| +--------+ +---+ +--------+ |
| | | | | | | |
| +--------+ +---+ +--------+ |
| | | | | |
+-----------------------------------------------------+
| |<---------------------A----------------->| |
|<-C->|<------B----->|<-----B----->|<-----B---->|<-C->|
| |<-D>|
|<----------E------------>|
A = PW-C B = A/NOP C=(CP*PW)/100 D = (B-OW)/2
E = C+(P-1)*B+D }
var
A, B, C, D: Integer;
begin
C := Trunc((CropPercent*ParentWidth)/100);
A := ParentWidth - C;
B := Trunc(A/NumberOfPlaces);
D := Trunc((B-ObjectWidth)/2);
Result := C+(Place-1)*B+D;
end;

Delphi TGIFImage animation issue with some GIF viewers

I have discovered that animated GIFs created using Delphi 2009's TGIFImage sometimes doesn't play correctly in some GIF viewers. The problem is that the animation is restarted prematurely.
Consider the following example:
program GIFAnomaly;
{$APPTYPE CONSOLE}
uses
Windows, Types, Classes, SysUtils, Graphics, GIFImg;
var
g: TGIFImage;
bm: TBitmap;
procedure MakeFrame(n: integer);
var
x: Integer;
y: Integer;
begin
for x := 0 to 256 - 1 do
for y := 0 to 256 - 1 do
bm.Canvas.Pixels[x, y] := RGB((x + n) mod 255,
(x + y - 2*n) mod 255, (x*y*n div 500) mod 255);
end;
var
i: integer;
begin
bm := TBitmap.Create;
bm.SetSize(256, 256);
g := TGIFImage.Create;
g.Animate := true;
for i := 0 to 499 do
begin
MakeFrame(i);
TGIFGraphicControlExtension.Create(g.Add(bm)).Delay := 3;
Writeln('Creating frame ', i+1, ' of 500.');
end;
TGIFAppExtNSLoop.Create(g.Images.Frames[0]).Loops := 0;
g.SaveToFile('C:\Users\Andreas Rejbrand\Desktop\test.gif');
end.
(This is the simplest example I could find that exhibits the problem.)
The output is a rather large animated GIF. In Internet Explorer 11, the entire 15-second 'movie' is played properly, but in Google Chrome the 'movie' is prematurely restarted after only about four seconds.
Why is this?
Is there something wrong with the output GIF file?
If so, is there something wrong with my code above, or is there a problem with GIFImg?
If not, what is the nature of the problem in the viewer? What fraction of the available viewers have this problem? Is there a way to 'avoid' this problem during GIF creation?
For the benefit of the SO user, the above code is a minimal working example. Of course, I wasn't creating these psychedelic patterns when I discovered the issue. Instead, I was working on a Lorenz system simulator, and produced this GIF animation which does play in IE but not in Chrome:
In Internet Explorer 11, the model is rotated 360 degrees before the animation is restarted. In Google Chrome, the animation is restarted prematurely after only some 20 degrees.
The Lorenz image works in Internet Explorer 11.0.9600.17239, The GIMP 2.8.0, Opera 12.16
The Lorenz image does not work in Google Chrome 36.0.1985.143 m, Firefox 26.0, 27.0.1, 31.0.
If I open a 'problematic' GIF in The GIMP and let GIMP (re)save it as an animated GIF, the result works in every viewer. The following is the GIMPed version of the Lorenz animation:
Comparing the two files using a hex editor, and using the Wikipedia article as a reference, it seems, for instance, like the 'NETSCAPE' string is at the wrong place in the original (unGIMPed) version. It is somewhat strange, that even if I set the width and height of the GIF image, the corresponding values in the Logical Screen Descriptor are not there.
It's a bug in TGIFImage's LZW encoder.
In some very rare circumstances the LZW encoder will output an extra zero byte at the end of the LZW steam. Since the LZW end block marker is also a zero byte, a strict GIF reader might choke on this or interpret it as the end of the GIF (although the end of file marker is $3B).
The reason some GIF readers can handle this is probably that GIFs with this problem was common many years ago. Apparently TGIFImage wasn't the only library to make that particular mistake.
To fix the problem make the following modification to gifimg.pas (change marked with *):
procedure TGIFWriter.FlushBuffer;
begin
if (FNeedsFlush) then
begin
FBuffer[0] := Byte(FBufferCount-1); // Block size excluding the count
Stream.WriteBuffer(FBuffer, FBufferCount);
FBufferCount := 1; // Reserve first byte of buffer for length
FNeedsFlush := False; // *** Add this ***
end;
end;
Edit: This turned out not to be the answer but I'm keeping it as the rule about the loop extension still applies.
The NETSCAPE loop extension must be the first extension:
var
Frame: TGIFFrame;
...
for i := 0 to 499 do
begin
MakeFrame(i);
Frame := g.Add(bm);
if (i = 0) then
TGIFAppExtNSLoop.Create(Frame).Loops := 0;
TGIFGraphicControlExtension.Create(Frame).Delay := 3;
Writeln('Creating frame ', i+1, ' of 500.');
end;
See: The TGIFImage FAQ.
Apart from that I see nothing wrong with your GIF, but you could reduce the size a bit with a global color table.

Accurately compute text widths in metric units

I want to compute the exact text width in metric units of a given string. My pseudocode looks like this:
Bitmap.Canvas.Assign(Font);
PixelWidth := Bitmap.Canvas.TextWidth(Font)
MetricWidth := PtToMM * (PixelWidth * 72.0 / GetScreenDPI);
PtToMM is a constant that is defined as 0.352777778. This is pretty accurate for some fonts and font sizes but for others it is either too small or too large. I experimented a lot witht other possiblities such as GetCharWidth32 and GetCharABCWidths, also with mapping mode MM_LOMETRIC but I just can't get it to work. This problem is haunting me, so please can anyone help and show me where I'm wrong. Thank you very much!
EDIT I checked for one string: metric width is computed as 4.17 cm, the width on the actual printout (measured on paper) is 4.4 cm (font Times New Roman, size 12).
I didn't test it extensively, but it seems to be giving the proper results. The result is the width of the text in thousandth of milimeter. This function doesn't support wordwrapping and other special considerations.
Also, note that when working with printer, improper Print Registration can "stretch" the text.
Also, note that for printer, changing, for example, the print resolution from 300 ppp to 1200 ppp will also change the result.
uses
ConvUtils, stdConvs ;
function CalcRequiredTextWidth(aDC : HDC; aFont : TFont; const asText: string): Double;
var vCanvas : TCanvas;
iPixelsWidth : Integer;
dInchWidth : Double;
iFontSize : Integer;
begin
vCanvas := TCanvas.Create;
try
vCanvas.Handle := aDC;
vCanvas.Font.Assign(aFont);
iFontSize := vCanvas.Font.Size;
vCanvas.Font.PixelsPerInch := GetDeviceCaps(aDC, LOGPIXELSY);
vCanvas.Font.Size := iFontSize;
iPixelsWidth := vCanvas.TextExtent(asText).cx;
dInchWidth := iPixelsWidth / GetDeviceCaps(vCanvas.Handle, LOGPIXELSX);
Result := Convert(dInchWidth, duInches, duMicrons);
finally
vCanvas.Free;
end;
end;

Delphi Pascal - Using SetFilePointerEx and GetFileSizeEx, Getting Physical Media exact size when reading as a file

I do not know how to use any API that is not in the RTL. I have been using SetFilePointer and GetFileSize to read a Physical Disk into a buffer and dump it to a file, something like this in a loop does the job for flash memory cards under 2GB:
SetFilePointer(PD,0,nil,FILE_BEGIN);
SetLength(Buffer,512);
ReadFile(PD,Buffer[0],512,BytesReturned,nil);
However GetFileSize has a limit at 2GB and so does SetFilePointer. I have absolutley no idea how to delcare an external API, I have looked at the RTL and googled for many examples and have found no correct answer.
I tried this
function GetFileSizeEx(hFile: THandle; lpFileSizeHigh: Pointer): DWORD;
external 'kernel32';
and as suggested this
function GetFileSizeEx(hFile: THandle; var FileSize: Int64): DWORD;
stdcall; external 'kernel32';
But the function returns a 0 even though I am using a valid disk handle which I have confirmed and dumped data from using the older API's.
I am using SetFilePointer to jump every 512 bytes and ReadFile to write into a buffer, in reverse I can use it to set when I am using WriteFile to write Initial Program Loader Code or something else to the disk. I need to be able to set the file pointer beyond 2gb well beyond.
Can someone help me make the external declarations and a call to both GetFileSizeEx and SetFilePointerEx that work so I can modify my older code to work with say 4 to 32gb flash cards.
I suggest that you take a look at this Primoz Gabrijelcic blog article and his GpHugeFile unit which should give you enough pointers to get the file size.
Edit 1 This looks rather a daft answer now in light of the edit to the question.
Edit 2 Now that this answer has been accepted, following a long threads of comments to jachguate's answer, I feel it incumbent to summarise what has been learnt.
GetFileSize and
SetFilePointer have no 2GB
limitation, they can be used on files
of essentially arbitrary size.
GetFileSizeEx and
SetFilePointerEx are much
easier to use because they work
directly with 64 bit quantities and
have far simpler error condition
signals.
The OP did not in fact need to
calculate the size of his disk. Since
the OP was reading the entire
contents of the disk the size was not
needed. All that was required was to
read the contents sequentially until
there was nothing left.
In fact
GetFileSize/GetFileSizeEx
do not support handles to devices
(e.g. a physical disk or volume) as
was requested by the OP. What's more,
SetFilePointer/SetFilePointerEx
cannot seek to the end of such device
handles.
In order to obtain the size of a
disk, volume or partition, one should
pass the the
IOCTL_DISK_GET_LENGTH_INFO
control code to
DeviceIoControl.
Finally, should you need to use GetFileSizeEx and SetFilePointerEx then they can be declared as follows:
function GetFileSizeEx(hFile: THandle; var lpFileSize: Int64): BOOL;
stdcall; external 'kernel32.dll';
function SetFilePointerEx(hFile: THandle; liDistanceToMove: Int64;
lpNewFilePointer: PInt64; dwMoveMethod: DWORD): BOOL;
stdcall; external 'kernel32.dll';
One easy way to obtain these API imports them is through the excellent JEDI API Library.
The GetFileSizeEx routine expects a pointer to a LARGE_INTEGER data type, and documentation says:
If your compiler has built-in support for 64-bit integers, use the QuadPart member to store the 64-bit integer
Lucky you, Delphi has built-in support for 64 bit integers, so use it:
var
DriveSize: LongWord;
begin
GetFilePointerSizeEx(PD, #DriveSize);
end;
SetFilePointerEx, on the other hand, expects parameters for liDistanceToMove, lpNewFilePointer, both 64 bit integers. My understanding is it wants signed integers, but you have the UInt64 data type for Unsingned 64 bit integers if I'm missunderstanding the documentation.
Alternative coding
Suicide, first of all your approach is wrong, and because of your wrong approach you ran into some hairy problems with the way Windows handles Disk drives opened as files. In pseudo code your approach seems to be:
Size = GetFileSize;
for i=0 to (Size / 512) do
begin
Seek(i * 512);
ReadBlock;
WriteBlockToFile;
end;
That's functionally correct, but there's a simpler way to do the same without actually getting the SizeOfDisk and without seeking. When reading something from a file (or a stream), the "pointer" is automatically moved with the ammount of data you just read, so you can skip the "seek". All the functions used to read data from a file return the amount of data that was actually read: you can use that to know when you reached the end of the file without knowing the size of the file to start with!
Here's an idea of how you can read an physical disk to a file, without knowing much about the disk device, using Delphi's TFileStream:
var DiskStream, DestinationStream:TFileStream;
Buff:array[0..512-1] of Byte;
BuffRead:Integer;
begin
// Open the disk for reading
DiskStream := TFileStream.Create('\\.\PhysicalDrive0', fmOpenRead);
try
// Create the file
DestinationStream := TFileStream.Create('D:\Something.IMG', fmCreate);
try
// Read & write in a loop; This is where all the work's done:
BuffRead := DiskStream.Read(Buff, SizeOf(Buff));
while BuffRead > 0 do
begin
DestinationStream.Write(Buff, BuffRead);
BuffRead := DiskStream.Read(Buff, SizeOf(Buff));
end;
finally DestinationStream.Free;
end;
finally DiskStream.Free;
end;
end;
You can obviously do something similar the other way around, reading from a file and writing to disk. Before writing that code I actually attempted doing it your way (getting the file size, etc), and immediately ran into problems! Apparently Windows doesn't know the exact size of the "file", not unless you read from it.
Problems with disks opened as files
For all my testing I used this simple code as the base:
var F: TFileStream;
begin
F := TFileStream.Create('\\.\PhysicalDrive0', fmOpenRead);
try
// Test code goes here...
finally F.Free;
end;
end;
The first (obvious) thing to try was:
ShowMessage(IntToStr(DiskStream.Size));
That fails. In the TFileStream implementation that depends on calling FileSeek, and FileSeek can't handle files larger then 2Gb. So I gave GetFileSize a try, using this code:
var RetSize, UpperWord:DWORD;
RetSize := GetFileSize(F.Handle, #UpperWord);
ShowMessage(IntToStr(UpperWord) + ' / ' + IntToStr(RetSize));
That also fails, even those it should be perfectly capable of returning file size as an 64 bit number! Next I tried using the SetFilePointer API, because that's also supposed to handle 64bit numbers. I thought I'd simply seek to the end of the file and look at the result, using this code:
var RetPos, UpperWord:DWORD;
UpperWord := 0;
RetPos := SetFilePos(F.Handle, 0, #UpperWord, FILE_END);
ShowMessage(IntToStr(UpperWord) + ' / ' + IntToStr(RetPos));
This code also fails! And now I'm thinking, why did the first code work? Apparently reading block-by-block works just fine and Windows knows exactly when to stop reading!! So I thought maybe there's a problem with the implementation of the 64 bit file handling routines, let's try seeking to end of the file in small increments; When we get an error seeking we know we reached the end we'll stop:
var PrevUpWord, PrevPos: DWORD;
UpWord, Pos: DWORD;
UpWord := 0;
Pos := SetFilePointer(F.Handle, 1024, #UpWord, FILE_CURRENT); // Advance the pointer 512 bytes from it's current position
while (UpWord <> PrevUpWord) or (Pos <> PrevPos) do
begin
PrevUpWord := UpWord;
PrevPos := Pos;
UpWord := 0;
Pos := SetFilePointer(F.Handle, 1024, #UpWord, FILE_CURRENT);
end;
When trying this code I had a surprise: It doesn't stop at the of the file, it just goes on and on, for ever. It never fails. To be perfectly honest I'm not sure it's supposed to ever fail... It's probably not supposed to fail. Anyway, doing a READ in that loop fails when we're past the end of file so we can use a VERY hacky mixed approach to handle this situation.
Ready-made routines that work around the problem
Here's the ready-made routine that gets the size of the physical disk opened as a file, even when GetFileSize fails, and SetFilePointer with FILE_END fails. Pass it an opened TFileStream and it will return the size as an Int64:
function Hacky_GetStreamSize(F: TFileStream): Int64;
var Step:DWORD;
StartPos: Int64;
StartPos_DWORD: packed array [0..1] of DWORD absolute StartPos;
KnownGoodPosition: Int64;
KGP_DWORD: packed array [0..1] of DWORD absolute KnownGoodPosition;
Dummy:DWORD;
Block:array[0..512-1] of Byte;
begin
// Get starting pointer position
StartPos := 0;
StartPos_DWORD[0] := SetFilePointer(F.Handle, 0, #StartPos_DWORD[1], FILE_CURRENT);
try
// Move file pointer to the first byte
SetFilePointer(F.Handle, 0, nil, FILE_BEGIN);
// Init
KnownGoodPosition := 0;
Step := 1024 * 1024 * 1024; // Initial step will be 1Gb
while Step > 512 do
begin
// Try to move
Dummy := 0;
SetFilePointer(F.Handle, Step, #Dummy, FILE_CURRENT);
// Test: Try to read!
if F.Read(Block, 512) = 512 then
begin
// Ok! Save the last known good position
KGP_DWORD[1] := 0;
KGP_DWORD[0] := SetFilePointer(F.Handle, 0, #KGP_DWORD[1], FILE_CURRENT);
end
else
begin
// Read failed! Move back to the last known good position and make Step smaller
SetFilePointer(F.Handle, KGP_DWORD[0], #KGP_DWORD[1], FILE_BEGIN);
Step := Step div 4; // it's optimal to devide by 4
end;
end;
// From here on we'll use 512 byte steps until we can't read any more
SetFilePointer(F.Handle, KGP_DWORD[0], #KGP_DWORD[1], FILE_BEGIN);
while F.Read(Block, 512) = 512 do
KnownGoodPosition := KnownGoodPosition + 512;
// Done!
Result := KnownGoodPosition;
finally
// Move file pointer back to starting position
SetFilePointer(F.Handle, StartPos_DWORD[0], #StartPos_DWORD[1], FILE_BEGIN);
end;
end;
To be complete, here are two routines that may be used to set and get the file pointer using Int64 for positioning:
function Hacky_SetStreamPos(F: TFileStream; Pos: Int64):Int64;
var aPos:Int64;
DWA:packed array[0..1] of DWORD absolute aPos;
const INVALID_SET_FILE_POINTER = $FFFFFFFF;
begin
aPos := Pos;
DWA[0] := SetFilePointer(F.Handle, DWA[0], #DWA[1], FILE_BEGIN);
if (DWA[0] = INVALID_SET_FILE_POINTER) and (GetLastError <> NO_ERROR) then
RaiseLastOSError;
Result := aPos;
end;
function Hacky_GetStreamPos(F: TFileStream): Int64;
var Pos:Int64;
DWA:packed array[0..1] of DWORD absolute Pos;
begin
Pos := 0;
DWA[0] := SetFilePointer(F.Handle, 0, #DWA[1], FILE_CURRENT);
Result := Pos;
end;
Last notes
The 3 routines I'm providing take as a parameter an TFileStream, because that's what I use for file reading and writing. They obviously only use TFileStream.Handle, so the parameter can simply be replaced with an file handle: the functionality would stay the same.
I know this thread is old, but...
One small suggestion - if you use the Windows DeviceIoControl(...) function you can get Drive Geometry and/or Partition Information, and use them to get the total size/length of the opened drive or partition. No more messing around with incrementally seeking to the end of the device.
Those IOCTLs can also be used to give you the correct volume sector size, and you could use that instead of defaulting to 512 everywhere.
Very very useful. But I got a problem for disks greater then 4 GB.
I solved replacing:
// Ok! Save the last known good position
KGP_DWORD[1] := 0;
KGP_DWORD[0] := SetFilePointer(F.Handle, 0, #KGP_DWORD[1], FILE_CURRENT);
with the following:
// Ok! Save the last known good position
KnownGoodPosition := KnownGoodPosition + Step;
Many thanks again...
And many thanks also to James R. Twine. I followed the advice of using IOCTL_DISK_GET_DRIVE_GEOMETRY_EX and got disk dimension with no problem and no strange workaround.
Here is the code:
TDISK_GEOMETRY = record
Cylinders : Int64; //LargeInteger
MediaType : DWORD; //MEDIA_TYPE
TracksPerCylinder: DWORD ;
SectorsPerTrack: DWORD ;
BytesPerSector : DWORD ;
end;
TDISK_GEOMETRY_EX = record
Geometry: TDISK_GEOMETRY ;
DiskSize: Int64; //LARGE_INTEGER ;
Data : array[1..1000] of byte; // unknown length
end;
function get_disk_size(handle: thandle): int64;
var
BytesReturned: DWORD;
DISK_GEOMETRY_EX : TDISK_GEOMETRY_EX;
begin
result := 0;
if DeviceIOControl(handle,IOCTL_DISK_GET_DRIVE_GEOMETRY_EX,
nil,0,#DISK_GEOMETRY_EX, sizeof(TDISK_GEOMETRY_EX),BytesReturned,nil)
then result := DISK_GEOMETRY_EX.DiskSize;
end;

Resources