mirror of
https://github.com/doublecmd/doublecmd.git
synced 2026-06-21 09:58:13 +00:00
398 lines
13 KiB
ObjectPascal
398 lines
13 KiB
ObjectPascal
unit uThumbnails;
|
|
|
|
{$mode objfpc}{$H+}
|
|
|
|
interface
|
|
|
|
uses
|
|
Classes, SysUtils, Graphics, Controls, Types, fgl, DCClassesUtf8, uFile;
|
|
|
|
type
|
|
|
|
{ TCreatePreviewHandler }
|
|
|
|
TCreatePreviewHandler = function(const aFileName: String; aSize: TSize): TBitmap;
|
|
|
|
{ TBitmapList }
|
|
|
|
TBitmapList = specialize TFPGObjectList<TBitmap>;
|
|
|
|
{ TThumbnailManager }
|
|
|
|
TThumbnailManager = class
|
|
private
|
|
FOwnerControl: TControl;
|
|
FBitmap: TBitmap;
|
|
FThumbBitmapSize: TSize;
|
|
FBackColor: TColor;
|
|
FFileName: String;
|
|
FThumbPath: String;
|
|
FProviderList: array of TCreatePreviewHandler; static;
|
|
private
|
|
procedure DoCreatePreviewText;
|
|
function GetPreviewFileExt(const sFileExt: String): String;
|
|
function GetPreviewFileName(const sFileName: String): String;
|
|
function CreatePreviewImage(const Graphic: TGraphic): TBitmap;
|
|
function ReadMetaData(const aFile: TFile; FileStream: TFileStreamEx): Boolean;
|
|
function WriteMetaData(const aFile: TFile; FileStream: TFileStreamEx): Boolean;
|
|
class function ReadFileName(const aThumb: String; out aFileName: String): Boolean;
|
|
public
|
|
constructor Create(const OwnerControl: TControl; const BackColor: TColor);
|
|
function CreatePreview(const aFile: TFile): TBitmap;
|
|
function CreatePreview(const FullPathToFile: String): TBitmap;
|
|
function RemovePreview(const FullPathToFile: String): Boolean;
|
|
public
|
|
class procedure CompactCache;
|
|
class function RegisterProvider(Provider: TCreatePreviewHandler): Integer;
|
|
class function GetPreviewScaleSize(const bitmapSize: TSize; const aWidth, aHeight: Integer): TSize;
|
|
class function GetPreviewFromProvider(const aFileName: String; aSize: TSize; aSkip: Integer): TBitmap;
|
|
end;
|
|
|
|
implementation
|
|
|
|
uses
|
|
StreamEx, URIParser, MD5, FileUtil, LazFileUtils, Forms, uDCUtils, DCOSUtils, DCStrUtils,
|
|
uDebug, uReSample, uGlobsPaths, uGlobs, uPixmapManager, uFileSystemFileSource,
|
|
uGraphics, uFileProcs;
|
|
|
|
const
|
|
ThumbSign: QWord = $0000235448554D42; // '#0 #0 # T H U M B'
|
|
|
|
function TThumbnailManager.GetPreviewFileExt(const sFileExt: String): String;
|
|
begin
|
|
if (sFileExt = 'jpg') or (sFileExt = 'jpeg') or (sFileExt = 'bmp') then
|
|
Result:= 'jpg'
|
|
else
|
|
Result:= 'png';
|
|
end;
|
|
|
|
function TThumbnailManager.GetPreviewFileName(const sFileName: String): String;
|
|
begin
|
|
Result:= MD5Print(MD5String(sFileName));
|
|
end;
|
|
|
|
function TThumbnailManager.CreatePreviewImage(const Graphic: TGraphic): TBitmap;
|
|
var
|
|
aSize: TSize;
|
|
bmpTemp: TBitmap = nil;
|
|
begin
|
|
try
|
|
// Calculate aspect width and height of thumb
|
|
aSize:= GetPreviewScaleSize(FThumbBitmapSize, Graphic.Width, Graphic.Height);
|
|
bmpTemp:= TBitMap.Create;
|
|
bmpTemp.Assign(Graphic);
|
|
Result:= TBitMap.Create;
|
|
Result.SetSize(aSize.cx, aSize.cy);
|
|
Stretch(bmpTemp, Result, ResampleFilters[2].Filter, ResampleFilters[2].Width);
|
|
finally
|
|
FreeAndNil(bmpTemp);
|
|
end;
|
|
end;
|
|
|
|
procedure TThumbnailManager.DoCreatePreviewText;
|
|
var
|
|
S: String;
|
|
Y: Integer;
|
|
Stream: TFileStreamEx;
|
|
Reader: TStreamReader;
|
|
begin
|
|
FBitmap:= TBitmap.Create;
|
|
with FBitmap do
|
|
begin
|
|
SetSize(FThumbBitmapSize.cx, FThumbBitmapSize.cy);
|
|
Canvas.Brush.Color:= clWindow;
|
|
Canvas.FillRect(Canvas.ClipRect);
|
|
Canvas.Font.Color:= clWindowText;
|
|
Canvas.Font.Size := FThumbBitmapSize.cy div 16;
|
|
try
|
|
Stream:= TFileStreamEx.Create(FFileName, fmOpenRead or fmShareDenyNone);
|
|
try
|
|
Y:= 0;
|
|
Reader:= TStreamReader.Create(Stream, BUFFER_SIZE, True);
|
|
repeat
|
|
S:= Reader.ReadLine;
|
|
Canvas.TextOut(0, Y, S);
|
|
Y += Canvas.TextHeight(S) + 2;
|
|
until (Y >= FThumbBitmapSize.cy) or Reader.Eof;
|
|
finally
|
|
Reader.Free;
|
|
end;
|
|
except
|
|
// Ignore
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function TThumbnailManager.ReadMetaData(const aFile: TFile; FileStream: TFileStreamEx): Boolean;
|
|
var
|
|
sFileName: AnsiString;
|
|
begin
|
|
Result:= True;
|
|
try
|
|
// Read metadata position from last 4 byte of file
|
|
FileStream.Seek(-4, soEnd);
|
|
FileStream.Seek(FileStream.ReadDWord, soBeginning);
|
|
// Check signature
|
|
if (FileStream.ReadQWord <> NtoBE(ThumbSign)) then
|
|
Exit(False);
|
|
// Read thumbnail metadata
|
|
Result:= (URIToFilename(FileStream.ReadAnsiString, sFileName) and SameText(sFileName, aFile.FullPath));
|
|
if not Result then Exit;
|
|
Result:= (aFile.Size = FileStream.ReadQWord) and (QWord(aFile.ModificationTime) = FileStream.ReadQWord);
|
|
if not Result then Exit;
|
|
Result:= (FThumbBitmapSize.cx = FileStream.ReadWord) and (FThumbBitmapSize.cy = FileStream.ReadWord);
|
|
except
|
|
Result:= False;
|
|
end;
|
|
end;
|
|
|
|
function TThumbnailManager.WriteMetaData(const aFile: TFile; FileStream: TFileStreamEx): Boolean;
|
|
var
|
|
iEnd: Int64;
|
|
begin
|
|
Result:= True;
|
|
try
|
|
// Get original file size
|
|
iEnd:= FileStream.Seek(0, soEnd);
|
|
// Write signature
|
|
FileStream.WriteQWord(NtoBE(ThumbSign));
|
|
// Write thumbnail meta data
|
|
FileStream.WriteAnsiString(FilenameToURI(aFile.FullPath));
|
|
FileStream.WriteQWord(aFile.Size);
|
|
FileStream.WriteQWord(QWord(aFile.ModificationTime));
|
|
FileStream.WriteWord(FThumbBitmapSize.cx);
|
|
FileStream.WriteWord(FThumbBitmapSize.cy);
|
|
// Write original file size
|
|
FileStream.WriteDWord(iEnd);
|
|
except
|
|
Result:= False;
|
|
end;
|
|
end;
|
|
|
|
class function TThumbnailManager.ReadFileName(const aThumb: String;
|
|
out aFileName: String): Boolean;
|
|
var
|
|
fsFileStream: TFileStreamEx;
|
|
begin
|
|
try
|
|
fsFileStream:= TFileStreamEx.Create(aThumb, fmOpenRead or fmShareDenyNone);
|
|
try
|
|
// Read metadata position from last 4 byte of file
|
|
fsFileStream.Seek(-4, soEnd);
|
|
fsFileStream.Seek(fsFileStream.ReadDWord, soBeginning);
|
|
// Check signature
|
|
if (fsFileStream.ReadQWord <> NtoBE(ThumbSign)) then
|
|
Exit(False);
|
|
// Read source file name
|
|
Result:= URIToFilename(fsFileStream.ReadAnsiString, aFileName);
|
|
finally
|
|
fsFileStream.Free;
|
|
end;
|
|
except
|
|
Result:= False;
|
|
end;
|
|
end;
|
|
|
|
constructor TThumbnailManager.Create(const OwnerControl: TControl; const BackColor: TColor);
|
|
begin
|
|
FOwnerControl:= OwnerControl;
|
|
FBackColor:= BackColor;
|
|
FThumbPath:= gpThumbCacheDir;
|
|
// If directory not exists then create it
|
|
if not mbDirectoryExists(FThumbPath) then mbForceDirectory(FThumbPath);
|
|
end;
|
|
|
|
function TThumbnailManager.RemovePreview(const FullPathToFile: String): Boolean;
|
|
var
|
|
sExt, sName: String;
|
|
begin
|
|
sExt:= GetPreviewFileExt(ExtractOnlyFileExt(FullPathToFile));
|
|
sName:= GetPreviewFileName(FullPathToFile);
|
|
// Delete thumb from cache
|
|
Result:= mbDeleteFile(FThumbPath + PathDelim + sName + '.' + sExt);
|
|
end;
|
|
|
|
function TThumbnailManager.CreatePreview(const aFile: TFile): TBitmap;
|
|
var
|
|
I: Integer;
|
|
sFullPathToFile, sThumbFileName,
|
|
sExt: String;
|
|
fsFileStream: TFileStreamEx = nil;
|
|
Picture: TPicture = nil;
|
|
ABitmap: TBitmap;
|
|
factor: Double;
|
|
begin
|
|
Result:= nil;
|
|
try
|
|
factor:= findScaleFactorByControl(FOwnerControl);
|
|
FThumbBitmapSize.Width:= Round(gThumbSize.Width * factor);
|
|
FThumbBitmapSize.Height:= Round(gThumbSize.Height * factor);
|
|
Picture:= TPicture.Create;
|
|
try
|
|
sFullPathToFile:= aFile.FullPath;
|
|
sExt:= GetPreviewFileExt(ExtractOnlyFileExt(sFullPathToFile));
|
|
sThumbFileName:= FThumbPath + PathDelim + GetPreviewFileName(sFullPathToFile) + '.' + sExt;
|
|
// If thumbnail already exists in cache for this file then load it
|
|
if mbFileExists(sThumbFileName) then
|
|
begin
|
|
fsFileStream:= TFileStreamEx.Create(sThumbFileName, fmOpenRead or fmShareDenyNone or fmOpenNoATime);
|
|
try
|
|
if ReadMetaData(aFile, fsFileStream) then
|
|
begin
|
|
fsFileStream.Position:= 0;
|
|
Picture.LoadFromStreamWithFileExt(fsFileStream, sExt);
|
|
Result:= TBitmap.Create;
|
|
Result.Assign(Picture.Graphic);
|
|
Exit;
|
|
end;
|
|
finally
|
|
FreeAndNil(fsFileStream);
|
|
end;
|
|
end;
|
|
// Try to create thumnail using providers
|
|
for I:= Low(FProviderList) to High(FProviderList) do
|
|
begin
|
|
Result:= FProviderList[I](sFullPathToFile, FThumbBitmapSize);
|
|
if Assigned(Result) then Break;
|
|
end;
|
|
if Assigned(Result) then
|
|
begin
|
|
if (Result.Width > FThumbBitmapSize.cx) or (Result.Height > FThumbBitmapSize.cy) then
|
|
begin
|
|
ABitmap:= CreatePreviewImage(Result);
|
|
BitmapAssign(Result, ABitmap);
|
|
ABitmap.Free;
|
|
end;
|
|
end;
|
|
if not Assigned(Result) then
|
|
begin
|
|
sExt:= ExtractOnlyFileExt(sFullPathToFile);
|
|
// Create thumb for image files
|
|
if GetGraphicClassForFileExtension(sExt) <> nil then
|
|
begin
|
|
fsFileStream:= TFileStreamEx.Create(sFullPathToFile, fmOpenRead or fmShareDenyNone or fmOpenNoATime);
|
|
with Picture do
|
|
try
|
|
LoadFromStreamWithFileExt(fsFileStream, sExt);
|
|
if (Graphic.Width > FThumbBitmapSize.cx) or (Graphic.Height > FThumbBitmapSize.cy) then
|
|
Result:= CreatePreviewImage(Graphic)
|
|
else
|
|
begin
|
|
Result:= TBitmap.Create;
|
|
Result.Assign(Graphic);
|
|
Exit; // No need to save in cache
|
|
end;
|
|
finally
|
|
FreeAndNil(fsFileStream);
|
|
end
|
|
end
|
|
// Create thumb for text files
|
|
else if (mbFileExists(sFullPathToFile)) and (FileIsText(sFullPathToFile)) then
|
|
begin
|
|
FFileName:= sFullPathToFile;
|
|
// Some widgetsets can not draw from background
|
|
// thread so call draw text function from main thread
|
|
TThread.Synchronize(nil, @DoCreatePreviewText);
|
|
Exit(FBitmap); // No need to save in cache
|
|
end;
|
|
end;
|
|
// Save created thumb to cache
|
|
if gThumbSave and Assigned(Result) and not IsInPath(FThumbPath, sFullPathToFile, False, False) then
|
|
begin
|
|
Picture.Bitmap.Assign(Result);
|
|
sExt:= GetPreviewFileExt(sExt);
|
|
try
|
|
fsFileStream:= TFileStreamEx.Create(sThumbFileName, fmCreate);
|
|
try
|
|
Picture.SaveToStreamWithFileExt(fsFileStream, sExt);
|
|
WriteMetaData(aFile, fsFileStream);
|
|
finally
|
|
FreeAndNil(fsFileStream);
|
|
end;
|
|
except
|
|
on e: EStreamError do
|
|
DCDebug(['Cannot save thumbnail to file "', sThumbFileName, '": ', e.Message]);
|
|
end;
|
|
end;
|
|
finally
|
|
FreeAndNil(Picture);
|
|
end;
|
|
except
|
|
Result:= nil;
|
|
end;
|
|
if not Assigned(Result) then
|
|
Result:= PixMapManager.LoadBitmapEnhanced(sFullPathToFile, gIconsSize, True, FBackColor);
|
|
end;
|
|
|
|
function TThumbnailManager.CreatePreview(const FullPathToFile: String): TBitmap;
|
|
var
|
|
aFile: TFile;
|
|
begin
|
|
aFile := TFileSystemFileSource.CreateFileFromFile(FullPathToFile);
|
|
try
|
|
Result:= CreatePreview(aFile);
|
|
finally
|
|
FreeAndNil(AFile);
|
|
end;
|
|
end;
|
|
|
|
class procedure TThumbnailManager.CompactCache;
|
|
var
|
|
I: Integer;
|
|
aFileName: String;
|
|
aFileList: TStringList;
|
|
begin
|
|
aFileList:= FindAllFiles(gpThumbCacheDir);
|
|
for I:= 0 to Pred(aFileList.Count) do
|
|
begin
|
|
if not (ReadFileName(aFileList[I], aFileName) and mbFileExists(aFileName)) then
|
|
begin
|
|
mbDeleteFile(aFileList[I]);
|
|
end;
|
|
end;
|
|
aFileList.Free;
|
|
end;
|
|
|
|
class function TThumbnailManager.RegisterProvider(Provider: TCreatePreviewHandler): Integer;
|
|
begin
|
|
SetLength(FProviderList, Length(FProviderList) + 1);
|
|
FProviderList[High(FProviderList)]:= Provider;
|
|
Result:= High(FProviderList);
|
|
end;
|
|
|
|
class function TThumbnailManager.GetPreviewScaleSize(const bitmapSize: TSize; const aWidth, aHeight: Integer): TSize;
|
|
begin
|
|
if aWidth > aHeight then
|
|
begin
|
|
Result.cx:= bitmapSize.cx;
|
|
Result.cy:= Result.cx * aHeight div aWidth;
|
|
if Result.cy > bitmapSize.cy then
|
|
begin
|
|
Result.cy:= bitmapSize.cy;
|
|
Result.cx:= Result.cy * aWidth div aHeight;
|
|
end;
|
|
end
|
|
else
|
|
begin
|
|
Result.cy:= bitmapSize.cy;
|
|
Result.cx:= Result.cy * aWidth div aHeight;
|
|
end;
|
|
end;
|
|
|
|
class function TThumbnailManager.GetPreviewFromProvider(const aFileName: String; aSize: TSize; aSkip: Integer): TBitmap;
|
|
var
|
|
Index: Integer;
|
|
begin
|
|
for Index:= Low(FProviderList) to High(FProviderList) do
|
|
begin
|
|
if (Index <> aSkip) then
|
|
begin
|
|
Result:= FProviderList[Index](aFileName, aSize);
|
|
if Assigned(Result) then Exit;
|
|
end;
|
|
end;
|
|
Result:= nil;
|
|
end;
|
|
|
|
end.
|
|
|