| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247 |
- unit unitCoverWorker;
- {$mode objfpc}{$H+}
- interface
- uses
- Classes, SysUtils, Process, LCLIntf, Graphics, Math, LazJpeg,
- IntfGraphics, FPImage, LazCanvas,
- Book, BookCollection, FileUtil;
- { Call this once after loading your data: it scans the list and enqueues
- only the PDFs that still use the generic cover (i.e. ImagePath=''). }
- procedure CoverWorkerEnqueueMissingFromBookList(AList: TBookCollection);
- { Call this whenever you add/edit a single book and want it queued if needed. }
- procedure CoverWorkerEnqueueBookIfMissing(B: TBook);
- { Starts the background worker (idempotent). It will exit by itself when the
- queue is empty. Call again later to restart if you enqueue more books. }
- procedure CoverWorkerStart;
- implementation
- type
- { Simple worker that drains a TThreadList queue of TBook references }
- TCoverWorker = class(TThread)
- private
- FApplyBook: TBook;
- FApplyImg : String;
- procedure DoApplyCover; // runs in main thread
- protected
- procedure Execute; override;
- end;
- var
- GPdfQueue: TThreadList; // holds TBook references
- GWorker : TCoverWorker;
- {--- helpers ------------------------------------------------------------------}
- function IsPdf(const Path: String): Boolean;
- begin
- Result := LowerCase(ExtractFileExt(Path)) = '.pdf';
- end;
- function HasGenericCover(B: TBook): Boolean;
- begin
- // Our Book.SetImage leaves ImagePath='' when using the generic resource
- Result := (Trim(B.ImagePath) = '');
- end;
- function GeneratePdfCover(const PdfPath: String; W, H: Integer): String;
- var
- OutBase, Converter: String;
- Proc: TProcess;
- Pic: TPicture;
- Img: TLazIntfImage;
- Canvas: TLazCanvas;
- Png: TPortableNetworkGraphic;
- scale: Double;
- dstW, dstH, offX, offY: Integer;
- begin
- Result := '';
- // If a sibling PNG or JPG already exists, just return it
- if FileExists(ChangeFileExt(PdfPath, '.png')) then
- Exit(ChangeFileExt(PdfPath, '.png'));
- if FileExists(ChangeFileExt(PdfPath, '.jpg')) then
- Exit(ChangeFileExt(PdfPath, '.jpg'));
- // look for pdftoppm in PATH (Poppler utilities)
- Converter := FindDefaultExecutablePath('pdftoppm');
- if Converter = '' then Exit; // poppler not installed
- OutBase := ChangeFileExt(PdfPath, ''); // /path/book.pdf -> /path/book
- Proc := TProcess.Create(nil);
- try
- Proc.Executable := Converter;
- // pdftoppm -png -singlefile -f 1 -l 1 <pdf> <out_base>
- Proc.Parameters.Add('-png');
- Proc.Parameters.Add('-singlefile');
- Proc.Parameters.Add('-f'); Proc.Parameters.Add('1');
- Proc.Parameters.Add('-l'); Proc.Parameters.Add('1');
- Proc.Parameters.Add(PdfPath);
- Proc.Parameters.Add(OutBase);
- Proc.Options := [poWaitOnExit];
- Proc.ShowWindow := swoHIDE;
- Proc.Execute;
- finally
- Proc.Free;
- end;
- if FileExists(OutBase + '.png') then
- begin
- Result := OutBase + '.png';
- // Scale down to requested cover size
- if (W > 0) and (H > 0) then
- begin
- Pic := TPicture.Create;
- Img := TLazIntfImage.Create(W, H);
- Canvas := TLazCanvas.Create(Img);
- Png := TPortableNetworkGraphic.Create;
- try
- Pic.LoadFromFile(Result);
- Img.FillPixels(colTransparent);
- if (Pic.Width > 0) and (Pic.Height > 0) then
- begin
- scale := Min(W / Pic.Width, H / Pic.Height);
- if scale > 1 then scale := 1;
- dstW := Round(Pic.Width * scale);
- dstH := Round(Pic.Height * scale);
- offX := (W - dstW) div 2;
- offY := (H - dstH) div 2;
- Canvas.StretchDraw(Rect(offX, offY, offX + dstW, offY + dstH), Pic.Graphic);
- end;
- Png.Assign(Img);
- Png.SaveToFile(Result);
- finally
- Png.Free;
- Canvas.Free;
- Img.Free;
- Pic.Free;
- end;
- end;
- end;
- end;
- procedure EnsureQueue;
- begin
- if GPdfQueue = nil then
- GPdfQueue := TThreadList.Create;
- end;
- {--- public API ----------------------------------------------------------------}
- procedure CoverWorkerEnqueueMissingFromBookList(AList: TBookCollection);
- var
- i: Integer;
- l: TList;
- begin
- if AList = nil then Exit;
- EnsureQueue;
- l := GPdfQueue.LockList;
- try
- for i := 0 to AList.Count - 1 do
- if IsPdf(AList.Books[i].FilePath) and HasGenericCover(AList.Books[i]) then
- l.Add(AList.Books[i]);
- finally
- GPdfQueue.UnlockList;
- end;
- end;
- procedure CoverWorkerEnqueueBookIfMissing(B: TBook);
- var
- l: TList;
- begin
- if (B = nil) then Exit;
- if not (IsPdf(B.FilePath) and HasGenericCover(B)) then Exit;
- EnsureQueue;
- l := GPdfQueue.LockList;
- try
- if l.IndexOf(B) < 0 then
- l.Add(B);
- finally
- GPdfQueue.UnlockList;
- end;
- end;
- procedure CoverWorkerStart;
- begin
- EnsureQueue;
- if (GWorker = nil) or (GWorker.Finished) then
- begin
- GWorker := TCoverWorker.Create(True);
- GWorker.FreeOnTerminate := True;
- GWorker.Start;
- end;
- end;
- {--- worker --------------------------------------------------------------------}
- procedure TCoverWorker.DoApplyCover;
- begin
- // This runs in the main/UI thread
- try
- if Assigned(FApplyBook) and (FApplyImg <> '') and FileExists(FApplyImg) then
- begin
- FApplyBook.ImagePath := FApplyImg; // triggers SetImage + pre-scale
- FApplyBook.EnsureScaledToCoverSize; // in case layout changed
- end;
- except
- // ignore UI exceptions, keep worker going
- end;
- end;
- procedure TCoverWorker.Execute;
- var
- l: TList;
- B: TBook;
- Img: String;
- begin
- // drain the queue
- while not Terminated do
- begin
- // Pop one item
- B := nil;
- l := GPdfQueue.LockList;
- try
- if (l <> nil) and (l.Count > 0) then
- begin
- B := TBook(l[0]);
- l.Delete(0);
- end;
- finally
- GPdfQueue.UnlockList;
- end;
- if B = nil then
- Break; // queue empty → exit thread
- // Skip if it no longer needs a cover
- if not (IsPdf(B.FilePath) and HasGenericCover(B)) then
- begin
- Sleep(5);
- Continue;
- end;
- // Generate cover (background thread)
- Img := GeneratePdfCover(B.FilePath, B.Cover.Width, B.Cover.Height);
- if (Img <> '') and FileExists(Img) then
- begin
- // Pass data to main thread via fields + Synchronize
- FApplyBook := B;
- FApplyImg := Img;
- Synchronize(@DoApplyCover);
- FApplyBook := nil;
- FApplyImg := '';
- end;
- Sleep(5); // be nice to the UI event loop
- end;
- end;
- end.
|