unit uPdfRotateService;

interface

uses
  System.SysUtils, System.Classes, Winapi.Windows, Winapi.ShellAPI,
  System.RegularExpressions, System.IOUtils;

type
  TPdfRotateService = class
  private
    class function TryRotateWithPython(const AInputPdf, AOutputPdf: string; ADegrees: Integer = 180): Boolean;
    class function TryRotateNative(const AInputPdf, AOutputPdf: string; ADegrees: Integer = 180): Boolean;
    class function ExecuteProcessWait(const ACommandLine: string; ATimeoutMs: Cardinal = 5000): Boolean;
  public
    /// <summary>
    /// Rota un archivo PDF los grados indicados (por defecto 180°) y lo guarda en AOutputPdf.
    /// </summary>
    class function RotarPdfArchivo(const AInputPdf, AOutputPdf: string; ADegrees: Integer = 180): Boolean;

    /// <summary>
    /// Rota el propio archivo PDF 180 grados in-place.
    /// </summary>
    class function RotarPdfArchivo180(const APdfPath: string): Boolean;

    /// <summary>
    /// Rota un buffer en memoria TBytes de un PDF 180 grados.
    /// </summary>
    class function RotarPdfBytes180(const ABytes: TBytes): TBytes;
  end;

implementation

{ TPdfRotateService }

class function TPdfRotateService.ExecuteProcessWait(const ACommandLine: string; ATimeoutMs: Cardinal): Boolean;
var
  StartupInfo: TStartupInfo;
  ProcessInfo: TProcessInformation;
  Cmd: string;
begin
  Result := False;
  FillChar(StartupInfo, SizeOf(StartupInfo), 0);
  StartupInfo.cb := SizeOf(StartupInfo);
  StartupInfo.dwFlags := STARTF_USESHOWWINDOW;
  StartupInfo.wShowWindow := SW_HIDE;

  Cmd := ACommandLine;
  UniqueString(Cmd);

  if CreateProcess(nil, PChar(Cmd), nil, nil, False, CREATE_NO_WINDOW, nil, nil, StartupInfo, ProcessInfo) then
  begin
    try
      if WaitForSingleObject(ProcessInfo.hProcess, ATimeoutMs) = WAIT_OBJECT_0 then
      begin
        var ExitCode: DWORD := 0;
        if GetExitCodeProcess(ProcessInfo.hProcess, ExitCode) and (ExitCode = 0) then
          Result := True;
      end
      else
      begin
        TerminateProcess(ProcessInfo.hProcess, 1);
      end;
    finally
      CloseHandle(ProcessInfo.hProcess);
      CloseHandle(ProcessInfo.hThread);
    end;
  end;
end;

class function TPdfRotateService.TryRotateWithPython(const AInputPdf, AOutputPdf: string; ADegrees: Integer): Boolean;
var
  LScriptPath, LAppDir, LCmd: string;
begin
  Result := False;
  LAppDir := ExtractFilePath(ParamStr(0));

  // Buscar script rotate_pdf.py en varias ubicaciones habituales
  LScriptPath := TPath.Combine(LAppDir, 'scripts\rotate_pdf.py');
  if not FileExists(LScriptPath) then
    LScriptPath := TPath.Combine(ExtractFilePath(ExcludeTrailingPathDelimiter(LAppDir)), 'scripts\rotate_pdf.py');
  if not FileExists(LScriptPath) then
    LScriptPath := 'c:\fuentes\proyectos delphi\RAUL_ASENCIO\GESTION\scripts\rotate_pdf.py';
  if not FileExists(LScriptPath) then
    LScriptPath := 'C:\GESTIGEST\scripts\rotate_pdf.py';

  if not FileExists(LScriptPath) then
    Exit;

  // Intentar con python o py
  LCmd := Format('python "%s" "%s" "%s" %d', [LScriptPath, AInputPdf, AOutputPdf, ADegrees]);
  Result := ExecuteProcessWait(LCmd, 6000);

  if not Result or not FileExists(AOutputPdf) or (TFile.GetSize(AOutputPdf) < 100) then
  begin
    LCmd := Format('py "%s" "%s" "%s" %d', [LScriptPath, AInputPdf, AOutputPdf, ADegrees]);
    Result := ExecuteProcessWait(LCmd, 6000);
  end;

  if Result and FileExists(AOutputPdf) and (TFile.GetSize(AOutputPdf) > 100) then
    Result := True
  else
    Result := False;
end;

class function TPdfRotateService.TryRotateNative(const AInputPdf, AOutputPdf: string; ADegrees: Integer): Boolean;
var
  LBytes: TBytes;
  LRotatedBytes: TBytes;
begin
  Result := False;
  try
    LBytes := TFile.ReadAllBytes(AInputPdf);
    if Length(LBytes) < 50 then Exit;

    LRotatedBytes := RotarPdfBytes180(LBytes);
    if Length(LRotatedBytes) > 50 then
    begin
      TFile.WriteAllBytes(AOutputPdf, LRotatedBytes);
      Result := True;
    end;
  except
    Result := False;
  end;
end;

class function TPdfRotateService.RotarPdfBytes180(const ABytes: TBytes): TBytes;
var
  LRawStr: AnsiString;
  LModified: AnsiString;
  LLastXrefPos, LTrailerPos, LXrefOffset: Integer;
  LTrailerDict: AnsiString;
  LObjMatch: TMatch;
  LObjRegex: TRegEx;
  LOffsets: array of Integer;
  LMaxId, LObjId, LObjOffset: Integer;
  LNewXref: AnsiString;
  i: Integer;
  LPageRegex: TRegEx;
begin
  Result := ABytes;
  if Length(ABytes) < 50 then Exit;

  SetString(LRawStr, PAnsiChar(@ABytes[0]), Length(ABytes));

  var LStrUnicode: string := string(LRawStr);
  if Pos('/Rotate', LStrUnicode) > 0 then
    LStrUnicode := TRegEx.Replace(LStrUnicode, '/Rotate\s+\d+', '/Rotate 180')
  else
  begin
    LStrUnicode := TRegEx.Replace(LStrUnicode, '(/Type\s*/Pages\b)', '$1' + #13#10 + '/Rotate 180');
    LStrUnicode := TRegEx.Replace(LStrUnicode, '(/Type\s*/Page\b)', '$1' + #13#10 + '/Rotate 180');
  end;

  LModified := AnsiString(LStrUnicode);

  // 2. Localizar el último xref y trailer original
  // Buscar 'xref' hacia atrás desde el final
  var LIdx: Integer := Length(LModified) - 4;
  LLastXrefPos := 0;
  while LIdx > 10 do
  begin
    if (LModified[LIdx] = 'x') and (LModified[LIdx+1] = 'r') and (LModified[LIdx+2] = 'e') and (LModified[LIdx+3] = 'f') then
    begin
      LLastXrefPos := LIdx;
      Break;
    end;
    Dec(LIdx);
  end;

  if LLastXrefPos <= 0 then
  begin
    // Si no se localizó la tabla xref clásica, devolver la modificación directa
    SetLength(Result, Length(LModified));
    Move(LModified[1], Result[0], Length(LModified));
    Exit;
  end;

  var LBody: AnsiString := Copy(LModified, 1, LLastXrefPos - 1);

  // 3. Extraer el diccionario de trailer
  LTrailerPos := Pos('trailer', string(LModified));
  if LTrailerPos > 0 then
  begin
    var LOpenDict := Pos('<<', string(LModified), LTrailerPos);
    var LCloseDict := Pos('>>', string(LModified), LOpenDict);
    if (LOpenDict > 0) and (LCloseDict > LOpenDict) then
      LTrailerDict := Copy(LModified, LOpenDict, LCloseDict - LOpenDict + 2)
    else
      LTrailerDict := '<< /Size 99 /Root 1 0 R >>';
  end
  else
    LTrailerDict := '<< /Size 99 /Root 1 0 R >>';

  // 4. Escanear todos los offsets de objetos en el nuevo cuerpo
  LObjRegex := TRegEx.Create('(\d+)\s+(\d+)\s+obj');
  LObjMatch := LObjRegex.Match(string(LBody));
  LMaxId := 0;
  SetLength(LOffsets, 2000);
  for i := 0 to High(LOffsets) do LOffsets[i] := 0;

  while LObjMatch.Success do
  begin
    LObjId := StrToIntDef(LObjMatch.Groups[1].Value, 0);
    LObjOffset := LObjMatch.Index - 1; // 0-based offset en el stream
    if (LObjId > 0) and (LObjId < Length(LOffsets)) then
    begin
      LOffsets[LObjId] := LObjOffset;
      if LObjId > LMaxId then LMaxId := LObjId;
    end;
    LObjMatch := LObjMatch.NextMatch;
  end;

  // 5. Reconstruir tabla xref
  LNewXref := 'xref' + #13#10 + '0 ' + AnsiString(IntToStr(LMaxId + 1)) + #13#10;
  LNewXref := LNewXref + '0000000000 65535 f ' + #13#10;
  for i := 1 to LMaxId do
  begin
    if LOffsets[i] > 0 then
      LNewXref := LNewXref + AnsiString(Format('%.10d 00000 n ', [LOffsets[i]])) + #13#10
    else
      LNewXref := LNewXref + '0000000000 00000 f ' + #13#10;
  end;

  // Actualizar /Size en trailer si existe
  var LTrailerStr: string := string(LTrailerDict);
  if Pos('/Size', LTrailerStr) > 0 then
    LTrailerStr := TRegEx.Replace(LTrailerStr, '/Size\s+\d+', '/Size ' + IntToStr(LMaxId + 1));
  LTrailerDict := AnsiString(LTrailerStr);

  LXrefOffset := Length(LBody);

  var LFinalPdf: AnsiString :=
    LBody +
    LNewXref +
    'trailer' + #13#10 +
    LTrailerDict + #13#10 +
    'startxref' + #13#10 +
    AnsiString(IntToStr(LXrefOffset)) + #13#10 +
    '%%EOF' + #13#10;

  SetLength(Result, Length(LFinalPdf));
  Move(LFinalPdf[1], Result[0], Length(LFinalPdf));
end;

class function TPdfRotateService.RotarPdfArchivo(const AInputPdf, AOutputPdf: string; ADegrees: Integer): Boolean;
begin
  Result := False;
  if not FileExists(AInputPdf) then Exit;

  // 1. Prioridad: Utilidad Python rápida y exacta con pypdf
  if TryRotateWithPython(AInputPdf, AOutputPdf, ADegrees) then
  begin
    Result := True;
    Exit;
  end;

  // 2. Fallback nativo Pascal
  Result := TryRotateNative(AInputPdf, AOutputPdf, ADegrees);
end;

class function TPdfRotateService.RotarPdfArchivo180(const APdfPath: string): Boolean;
var
  LTempRotated: string;
begin
  Result := False;
  if not FileExists(APdfPath) then Exit;

  LTempRotated := APdfPath + '.rotated.tmp';
  try
    if RotarPdfArchivo(APdfPath, LTempRotated, 180) then
    begin
      if FileExists(LTempRotated) and (TFile.GetSize(LTempRotated) > 100) then
      begin
        TFile.Delete(APdfPath);
        TFile.Move(LTempRotated, APdfPath);
        Result := True;
      end;
    end;
  finally
    if FileExists(LTempRotated) then
      TFile.Delete(LTempRotated);
  end;
end;

end.
