unitCoverWorker.pas 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. unit unitCoverWorker;
  2. {$mode objfpc}{$H+}
  3. interface
  4. uses
  5. Classes, SysUtils, Process, LCLIntf, Graphics, Math, LazJpeg,
  6. IntfGraphics, FPImage, LazCanvas,
  7. Book, BookCollection, FileUtil;
  8. { Call this once after loading your data: it scans the list and enqueues
  9. only the PDFs that still use the generic cover (i.e. ImagePath=''). }
  10. procedure CoverWorkerEnqueueMissingFromBookList(AList: TBookCollection);
  11. { Call this whenever you add/edit a single book and want it queued if needed. }
  12. procedure CoverWorkerEnqueueBookIfMissing(B: TBook);
  13. { Starts the background worker (idempotent). It will exit by itself when the
  14. queue is empty. Call again later to restart if you enqueue more books. }
  15. procedure CoverWorkerStart;
  16. implementation
  17. type
  18. { Simple worker that drains a TThreadList queue of TBook references }
  19. TCoverWorker = class(TThread)
  20. private
  21. FApplyBook: TBook;
  22. FApplyImg : String;
  23. procedure DoApplyCover; // runs in main thread
  24. protected
  25. procedure Execute; override;
  26. end;
  27. var
  28. GPdfQueue: TThreadList; // holds TBook references
  29. GWorker : TCoverWorker;
  30. {--- helpers ------------------------------------------------------------------}
  31. function IsPdf(const Path: String): Boolean;
  32. begin
  33. Result := LowerCase(ExtractFileExt(Path)) = '.pdf';
  34. end;
  35. function HasGenericCover(B: TBook): Boolean;
  36. begin
  37. // Our Book.SetImage leaves ImagePath='' when using the generic resource
  38. Result := (Trim(B.ImagePath) = '');
  39. end;
  40. function GeneratePdfCover(const PdfPath: String; W, H: Integer): String;
  41. var
  42. OutBase, Converter: String;
  43. Proc: TProcess;
  44. Pic: TPicture;
  45. Img: TLazIntfImage;
  46. Canvas: TLazCanvas;
  47. Png: TPortableNetworkGraphic;
  48. scale: Double;
  49. dstW, dstH, offX, offY: Integer;
  50. begin
  51. Result := '';
  52. // If a sibling PNG or JPG already exists, just return it
  53. if FileExists(ChangeFileExt(PdfPath, '.png')) then
  54. Exit(ChangeFileExt(PdfPath, '.png'));
  55. if FileExists(ChangeFileExt(PdfPath, '.jpg')) then
  56. Exit(ChangeFileExt(PdfPath, '.jpg'));
  57. // look for pdftoppm in PATH (Poppler utilities)
  58. Converter := FindDefaultExecutablePath('pdftoppm');
  59. if Converter = '' then Exit; // poppler not installed
  60. OutBase := ChangeFileExt(PdfPath, ''); // /path/book.pdf -> /path/book
  61. Proc := TProcess.Create(nil);
  62. try
  63. Proc.Executable := Converter;
  64. // pdftoppm -png -singlefile -f 1 -l 1 <pdf> <out_base>
  65. Proc.Parameters.Add('-png');
  66. Proc.Parameters.Add('-singlefile');
  67. Proc.Parameters.Add('-f'); Proc.Parameters.Add('1');
  68. Proc.Parameters.Add('-l'); Proc.Parameters.Add('1');
  69. Proc.Parameters.Add(PdfPath);
  70. Proc.Parameters.Add(OutBase);
  71. Proc.Options := [poWaitOnExit];
  72. Proc.ShowWindow := swoHIDE;
  73. Proc.Execute;
  74. finally
  75. Proc.Free;
  76. end;
  77. if FileExists(OutBase + '.png') then
  78. begin
  79. Result := OutBase + '.png';
  80. // Scale down to requested cover size
  81. if (W > 0) and (H > 0) then
  82. begin
  83. Pic := TPicture.Create;
  84. Img := TLazIntfImage.Create(W, H);
  85. Canvas := TLazCanvas.Create(Img);
  86. Png := TPortableNetworkGraphic.Create;
  87. try
  88. Pic.LoadFromFile(Result);
  89. Img.FillPixels(colTransparent);
  90. if (Pic.Width > 0) and (Pic.Height > 0) then
  91. begin
  92. scale := Min(W / Pic.Width, H / Pic.Height);
  93. if scale > 1 then scale := 1;
  94. dstW := Round(Pic.Width * scale);
  95. dstH := Round(Pic.Height * scale);
  96. offX := (W - dstW) div 2;
  97. offY := (H - dstH) div 2;
  98. Canvas.StretchDraw(Rect(offX, offY, offX + dstW, offY + dstH), Pic.Graphic);
  99. end;
  100. Png.Assign(Img);
  101. Png.SaveToFile(Result);
  102. finally
  103. Png.Free;
  104. Canvas.Free;
  105. Img.Free;
  106. Pic.Free;
  107. end;
  108. end;
  109. end;
  110. end;
  111. procedure EnsureQueue;
  112. begin
  113. if GPdfQueue = nil then
  114. GPdfQueue := TThreadList.Create;
  115. end;
  116. {--- public API ----------------------------------------------------------------}
  117. procedure CoverWorkerEnqueueMissingFromBookList(AList: TBookCollection);
  118. var
  119. i: Integer;
  120. l: TList;
  121. begin
  122. if AList = nil then Exit;
  123. EnsureQueue;
  124. l := GPdfQueue.LockList;
  125. try
  126. for i := 0 to AList.Count - 1 do
  127. if IsPdf(AList.Books[i].FilePath) and HasGenericCover(AList.Books[i]) then
  128. l.Add(AList.Books[i]);
  129. finally
  130. GPdfQueue.UnlockList;
  131. end;
  132. end;
  133. procedure CoverWorkerEnqueueBookIfMissing(B: TBook);
  134. var
  135. l: TList;
  136. begin
  137. if (B = nil) then Exit;
  138. if not (IsPdf(B.FilePath) and HasGenericCover(B)) then Exit;
  139. EnsureQueue;
  140. l := GPdfQueue.LockList;
  141. try
  142. if l.IndexOf(B) < 0 then
  143. l.Add(B);
  144. finally
  145. GPdfQueue.UnlockList;
  146. end;
  147. end;
  148. procedure CoverWorkerStart;
  149. begin
  150. EnsureQueue;
  151. if (GWorker = nil) or (GWorker.Finished) then
  152. begin
  153. GWorker := TCoverWorker.Create(True);
  154. GWorker.FreeOnTerminate := True;
  155. GWorker.Start;
  156. end;
  157. end;
  158. {--- worker --------------------------------------------------------------------}
  159. procedure TCoverWorker.DoApplyCover;
  160. begin
  161. // This runs in the main/UI thread
  162. try
  163. if Assigned(FApplyBook) and (FApplyImg <> '') and FileExists(FApplyImg) then
  164. begin
  165. FApplyBook.ImagePath := FApplyImg; // triggers SetImage + pre-scale
  166. FApplyBook.EnsureScaledToCoverSize; // in case layout changed
  167. end;
  168. except
  169. // ignore UI exceptions, keep worker going
  170. end;
  171. end;
  172. procedure TCoverWorker.Execute;
  173. var
  174. l: TList;
  175. B: TBook;
  176. Img: String;
  177. begin
  178. // drain the queue
  179. while not Terminated do
  180. begin
  181. // Pop one item
  182. B := nil;
  183. l := GPdfQueue.LockList;
  184. try
  185. if (l <> nil) and (l.Count > 0) then
  186. begin
  187. B := TBook(l[0]);
  188. l.Delete(0);
  189. end;
  190. finally
  191. GPdfQueue.UnlockList;
  192. end;
  193. if B = nil then
  194. Break; // queue empty → exit thread
  195. // Skip if it no longer needs a cover
  196. if not (IsPdf(B.FilePath) and HasGenericCover(B)) then
  197. begin
  198. Sleep(5);
  199. Continue;
  200. end;
  201. // Generate cover (background thread)
  202. Img := GeneratePdfCover(B.FilePath, B.Cover.Width, B.Cover.Height);
  203. if (Img <> '') and FileExists(Img) then
  204. begin
  205. // Pass data to main thread via fields + Synchronize
  206. FApplyBook := B;
  207. FApplyImg := Img;
  208. Synchronize(@DoApplyCover);
  209. FApplyBook := nil;
  210. FApplyImg := '';
  211. end;
  212. Sleep(5); // be nice to the UI event loop
  213. end;
  214. end;
  215. end.