Scan to PDF tool for Linux (only Linux)

Applications, Games, Tools, User libs and useful stuff coded in PureBasic
User avatar
Kukulkan
Addict
Addict
Posts: 1352
Joined: Mon Jun 06, 2005 2:35 pm
Location: germany
Contact:

Scan to PDF tool for Linux (only Linux)

Post by Kukulkan »

Hi,

a repeating task for me is to scan documents into PDF's. On Windows, there where several tools available. After switching my desktop to Linux (Kubuntu 14.04), I found that there are also some programs available for this. After trying several sort of software for that task (Scan to PDF) I found the existing either to complex, to hard to handle, to simple or crashing. So I decided to do a quick tool for that by myself. Here it is. It is far away from being complete and definitely not perfect. But it does exactly what I need. So if you like it you may extend it? I would be happy if every enhancement is posted here so maybe I can also get benefit of it :-)

BTW, after scanning I use the sigmoidal-contrast of ImageMagic toolset instead of the standard contrast method (http://www.imagemagick.org/Usage/color_mods/#sigmoidal). IMHO this algorithm works much better on scanned text than the standard contrast function. You may replace this if you plan to scan fotos. Or make this an option by enhancing the code...

I fixed JPG quality to 75%. You also may add a setting for this...

In order to work, the following tools must be installed:
sane and sane-utils (for the 'scanimage' tool)
imagemagick (for the 'convert' tool)

To install these prerequisites, run this in your console window (line by line, each followed by the enter key):

Code: Select all

sudo apt-get update
sudo apt-get install sane
sudo apt-get install sane-utils
sudo apt-get install imagemagick
sudo apt-get install qpdf
You also need at least one SANE compatible scanner connected to your PC (http://www.sane-project.org/sane-mfgs.html).

If you get error "Error during save. Try again...", please edit /etc/ImageMagick-6/policy.xml and ensure this line (may already be there):

Code: Select all

<policy domain="coder" rights="read|write" pattern="PDF" />
Updates:
[2015-09-01: Fixed wrong order of pages during display and/or PDF creation.]
[2015-09-08: Some optical enhancements (paper-shadow, page number).]
[2015-10-29: Fixed issue saving PDF to a path containing space, enhanced scanner compatibility, remember last used save-folder.]
[2015-11-03: Added field for optional scanner options and button to show such scanner options.]
[2015-11-06: Fixed wrong date format in default filename. Prevent error if 'save' is clicked while no image is available.]
[2015-11-17: Added 'Print' button to also make it a quick copy-tool. Added some tool-tips to most buttons. Fixed bug in scanner-list handling.]
[2016-01-11: Fixed wrong page order and font size for page numbering if there are more than 9 pages.]
[2016-02-09: Added displaying some setup information on first start to help people to get it running. Some minor fixes.]
[2016-08-01: Now showing scan progress (learning scan time to be more accurate)]
[2016-09-28: Main window is now remembering its position and size for the next start]
[2017-09-18: scanimage options are now stored for every scanner in the list. Added generic scanner.]
[2018-11-06: Hint: You may have to edit /etc/ImageMagick-6/policy.xml to continue this tool.]
[2022-12-14: Added option to save password protected PDF (needs qpdf on your system)]

The source (compile using PB 5.24 or higher):

Code: Select all

; ScanToPDF for Linux
; The source is free to use in any non-commercial or commercial project.
; (w) 2015-2022 by V. Schmid
; PureBasic 5.45 LTS, 5.72 x64 (Linux only!)
;
; IMPORTANT: Use gtk2 as subsystem!!!!!
; It does not work with qt because of missing canvas in scrollarea (VS, PB 5.72, 2022)

UseJPEGImageDecoder()

EnableExplicit

CompilerIf #PB_Compiler_Unicode
  #XmlEncoding = #PB_UTF8
CompilerElse 
  #XmlEncoding = #PB_Ascii
CompilerEndIf

#preview_spacing = 10 ; spacing around the preview image

Structure strPrevImage
  Name.s
  PosX.i
  PosY.i
  Width.i
  Height.i
  Aspect.f
EndStructure

Structure strScanPrefs
  Contrast.i
  Gamma.i
  DPI.i
  Device.s
  Options.s
EndStructure

Structure strScanner
  Name.s
  Device.s
  Options.s
EndStructure

Structure strGui
  dialog.i
  window.i
  scan.i
  print.i
  save.i
  saveCrypt.i
  clear.i
  close.i
  dpi.i
  gamma.i
  lastPath.s
  txtGamma.i
  contrast.i
  txtContrast.i
  scanner.i
  scanRefresh.i
  scrollArea.i
  canvas.i
  btnOptions.i
  strOptions.i
  lblPageCount.i
  lblScanTime.i
  chkAutoClick.i
EndStructure

Global NewList ScannerList.strScanner()

Global gui.strGui

Global progStart.i
Global progLastTime.i = 0
Global progPos.i

Procedure.i openProgress(parentWin.i)
  Protected XML.s
  XML.s = "<window id='#PB_Any' name='Please wait' text='Scanning in progress' minwidth='300' minheight='auto' flags='#PB_Window_WindowCentered|#PB_Window_Tool'>" +
          "  <vbox expand=''>" +
          "    <text name='lblMessage' text='Please wait...' />" +
          "    <progressbar name='progress' min='0' max='100' height='30'/>" +
          "  </vbox>" +
          "</window>"
  Protected xm.i = CatchXML(#PB_Any, @XML, StringByteLength(XML), 0, #XmlEncoding)
  If XMLStatus(xm.i) = #PB_XML_Success
    Protected dialog.i = CreateDialog(#PB_Any)
    If OpenXMLDialog(dialog.i, xm.i, "Please wait", #PB_Ignore, #PB_Ignore, #PB_Ignore, #PB_Ignore, WindowID(parentWin))
      SetGadgetState(DialogGadget(dialog.i, "progress"), 0)
      StickyWindow(DialogWindow(dialog.i), #True)
    Else
      Debug "Window not opened :-("
      End
    EndIf
  Else
    Debug "XML Error: " + XMLError(xm.i)
    End
  EndIf
  
  progPos.i = 0
  progStart.i = ElapsedMilliseconds()
  
  ProcedureReturn dialog.i

EndProcedure

; just update
Procedure doProgress(dialog.i)
  Protected actTime.i = ElapsedMilliseconds() - progStart.i
  Protected pos.f = (100 / progLastTime.i) * actTime.i
  If pos > 100
    If pos > 110
      ; more than 10% more time needed. Inform about learning
      SetGadgetText(DialogGadget(dialog.i, "lblMessage"), "Learning scan-time...")
    EndIf
    pos = 100
  EndIf
  If pos < 0: pos = 0: EndIf
  SetGadgetState(DialogGadget(dialog.i, "progress"), pos)
EndProcedure

; returns the time it needed last time
Procedure closeProgress(dialog.i)
  If IsDialog(dialog.i)
    FreeDialog(dialog.i)
  EndIf
  Protected actTime.i = ElapsedMilliseconds() - progStart.i
  progLastTime.i = (actTime.i + progLastTime.i) / 2 ; count new time for 50%
  dialog.i = 0
  ProcedureReturn actTime.i
EndProcedure

Procedure FillScannerlist(Refresh.b = #False)
  Protected lst.s, x.i, entry.s
  
  If Refresh.b = #True Or ListSize(ScannerList()) = 0
    ; make a copy to preserve scanner options (if scanner stays available)
    Protected NewList oldList.strScanner()
    CopyList(ScannerList(), oldList())
    
    ClearList(ScannerList())
    AddElement(ScannerList())
    ScannerList()\Name = "-- Generic --"
    ScannerList()\Device = ""
    Protected proc.i = RunProgram("scanimage", "-f "+Chr(34)+"%m|%dµ"+Chr(34), "", #PB_Program_Open | #PB_Program_Read)
    If proc.i
      While ProgramRunning(proc.i)
        If AvailableProgramOutput(proc.i)
          lst.s + ReadProgramString(proc.i) + Chr(13)
        EndIf
      Wend
      CloseProgram(proc.i)
    EndIf
    For x.i = 1 To CountString(lst.s, "µ")
      entry.s = StringField(lst.s, x.i, "µ")
      AddElement(ScannerList())
      ScannerList()\Name = StringField(entry.s, 1, "|")
      ScannerList()\Device = StringField(entry.s, 2, "|")
      ForEach oldList()
        If oldList()\Device = ScannerList()\Device
          ; found previous options and keep them
          ScannerList()\Options = oldList()\Options
          Break
        EndIf
      Next
    Next
  EndIf
  
  ClearGadgetItems(gui\scanner)
  ForEach ScannerList()
    AddGadgetItem(gui\scanner, -1, ScannerList()\Name)  
  Next
  
EndProcedure

Procedure ShowScannerOptions(*Settings.strScanPrefs)
  Protected ScanimageParams.s = "-A"
  If *Settings\Device <> ""
    ScanimageParams.s = "--device-name=" + *Settings\Device + " " + ScanimageParams.s
  EndIf
  Protected proc.i = RunProgram("scanimage", ScanimageParams.s, "", #PB_Program_Open | #PB_Program_Read)
  Protected lst.s = ""
  If proc.i
    While ProgramRunning(proc.i)
      If AvailableProgramOutput(proc.i)
        lst.s + ReadProgramString(proc.i) + Chr(10)
      EndIf
    Wend
    CloseProgram(proc.i)
  EndIf
  Protected Lines.i = CountString(lst.s, Chr(10))
  Protected x.i
  If Lines.i > 45
    Protected Output.s = ""
    For x.i = 1 To Lines.i
      Output.s + StringField(lst.s, x.i, Chr(10)) + Chr(10)
      If CountString(Output.s, Chr(10)) >= 30 And
         Left(Trim(StringField(lst.s, x.i+1, Chr(10))), 2) = "--"
        MessageRequester("Scanner options", Output.s)
        Output.s = ""
      EndIf
    Next
    MessageRequester("Scanner options", Output.s)
  Else
    MessageRequester("Scanner options", lst.s)
  EndIf
EndProcedure

Procedure SaveSettings()
  Protected Path.s = GetHomeDirectory() + ".ScanToPDF"
  If FileSize(Path.s) <> -2
    If CreateDirectory(Path.s) = 0
      Debug "Failed to create settings dir!!!"
      End
    EndIf
  EndIf
  Path.s + "/settings.ini"
  OpenPreferences(Path.s, #PB_Preference_GroupSeparator)
  PreferenceGroup("Main")
  WritePreferenceInteger("dpiSel", GetGadgetState(gui\dpi))
  WritePreferenceInteger("gamma", GetGadgetState(gui\gamma))
  WritePreferenceInteger("contrast", GetGadgetState(gui\contrast))
  WritePreferenceString("lastPath", gui\lastPath)
  WritePreferenceString("options", GetGadgetText(gui\strOptions))
  If progLastTime > 0
    WritePreferenceInteger("lastScanDuration", progLastTime)
  EndIf
  WritePreferenceInteger("mainLeft", WindowX(gui\window))
  WritePreferenceInteger("mainTop", WindowY(gui\window))
  WritePreferenceInteger("mainWidth", WindowWidth(gui\window))
  WritePreferenceInteger("mainHeight", WindowHeight(gui\window))
  
  PreferenceGroup("Scanners")
  WritePreferenceInteger("count", ListSize(ScannerList()))
  WritePreferenceInteger("selected", GetGadgetState(gui\scanner))
  Protected c.i = 0
  ForEach ScannerList()
    c.i + 1
    WritePreferenceString("name"+Str(c.i), ScannerList()\Name)
    WritePreferenceString("device"+Str(c.i), ScannerList()\Device)
    WritePreferenceString("options"+Str(c.i), ScannerList()\Options)
  Next
  ClosePreferences()
EndProcedure

Procedure LoadSettings()
  Protected Path.s = GetHomeDirectory() + ".ScanToPDF"
  If FileSize(Path.s) <> -2
    Protected ret.i = MessageRequester("ScanToPDF / first start", 
                                       "Hello. To use this tool you need the following tools installed:" + #LF$ +
                                       "sane and sane-utils (for the 'scanimage' tool)" + #LF$ + 
                                       "imagemagick (for the 'convert' tool)" + #LF$ + #LF$ +
                                       "Did you already install these components?",
                                       #PB_MessageRequester_YesNo)
    If ret.i <> #PB_MessageRequester_Yes
      SetClipboardText("sudo apt-get update" + #LF$ +
                       "sudo apt-get install sane" + #LF$ +
                       "sudo apt-get install sane-utils" + #LF$ +
                       "sudo apt-get install imagemagick")
      MessageRequester("ScanToPDF / install information", 
                       "I just copied the needed apt-get commandlines to your clipboard." + #LF$ + #LF$ +
                       "Paste and run it in a console window or check it before in some text editor.")
      End
    EndIf
    
    If CreateDirectory(Path.s) = 0
      MessageRequester("ScanToPDF error", "Failed to create settings dir [" + Path.s + "]. Check permissions.")
      End
    EndIf
  EndIf
  Path.s + "/settings.ini"
  OpenPreferences(Path.s, #PB_Preference_GroupSeparator)
  PreferenceGroup("Main")
  SetGadgetState(gui\dpi, ReadPreferenceInteger("dpiSel", 2))
  SetGadgetState(gui\gamma, ReadPreferenceInteger("gamma", 0))
  SetGadgetState(gui\contrast, ReadPreferenceInteger("contrast", 15))
  SetGadgetText(gui\strOptions, ReadPreferenceString("options", "--mode color"))
  gui\lastPath = ReadPreferenceString("lastPath", "")
  progLastTime = ReadPreferenceInteger("lastScanDuration", 10000)
  
  ResizeWindow(gui\window,  ReadPreferenceInteger("mainLeft", #PB_Ignore),
                            ReadPreferenceInteger("mainTop", #PB_Ignore),
                            ReadPreferenceInteger("mainWidth", #PB_Ignore),
                            ReadPreferenceInteger("mainHeight", #PB_Ignore))
  
  PreferenceGroup("Scanners")
  Protected c.i = ReadPreferenceInteger("count", 0)
  Protected x.i
  For x.i = 1 To c.i
    AddElement(ScannerList())
    ScannerList()\Name = ReadPreferenceString("name"+Str(x.i), "?")
    ScannerList()\Device = ReadPreferenceString("device"+Str(x.i), "?")
    ScannerList()\Options = ReadPreferenceString("options"+Str(x.i), "")
  Next
  
  FillScannerlist()
  
  SetGadgetState(gui\scanner, ReadPreferenceInteger("selected", 0))
  
  ClosePreferences()
  
  ; update some other fields
  SetGadgetText(gui\txtContrast, Str(GetGadgetState(gui\contrast)))
  SetGadgetText(gui\txtGamma, Str(GetGadgetState(gui\gamma)))
EndProcedure

Procedure.i CountPNMFiles(Foldername.s)
  Protected Count.i = 0
  Protected dir.i = ExamineDirectory(#PB_Any, Foldername.s, "*.pnm")
  If dir.i
    While NextDirectoryEntry(dir.i)
      If DirectoryEntryType(dir.i) = #PB_DirectoryEntry_File
        Count.i + 1
      EndIf
    Wend
    FinishDirectory(dir.i)
  EndIf
  ProcedureReturn Count.i
EndProcedure

Procedure doScanning(Foldername.s, *Settings.strScanPrefs, Documents.i = 1)
  Protected StartID.i = CountPNMFiles(Foldername.s) + 1
  Protected Res.i, x.i, pid.i, startTime.i, timeoutTime.i
  
  Protected myStart.i = ElapsedMilliseconds()
  
  For x.i = 1 To Documents.i
    If x.i > 1
      If GetGadgetState(gui\chkAutoClick) = 0
        Res.i = MessageRequester("ScanToPDF", "Please prepare page " + Str(x.i) + " of " + Str(Documents.i) + "." + #LF$ + #LF$ +
                                              "Press 'Yes' to scan. Press 'No' to cancel.", 
                                 #PB_MessageRequester_YesNo)
        If Res.i <> #PB_MessageRequester_Yes
          Break
        EndIf
      EndIf
    EndIf
    Protected ScanimageParams.s = "--batch " +
                                  "--batch-start=" + Str(StartID.i) + " " +
                                  "--batch-count=1 " +
                                  "--batch=out%03d.pnm " +
                                  "--format=pnm " + 
                                  "--resolution " + Str(*Settings\DPI)
                                
    If *Settings\Options <> ""
      ScanimageParams.s = ScanimageParams.s + " " + *Settings\Options
    EndIf
                                
    If *Settings\Device <> ""
      ScanimageParams.s = "--device-name=" + *Settings\Device + " " + ScanimageParams.s
    EndIf
    
    Debug "scanimage " + ScanimageParams.s
    
    Protected d.i = openProgress(DialogWindow(gui\dialog))
    startTime.i = ElapsedMilliseconds()
    timeoutTime.i = startTime.i + 30000 ; wait for max. 30 seconds
    pid.i = RunProgram("scanimage", ScanimageParams.s, Foldername.s, #PB_Program_Open)
    While ProgramRunning(pid.i) <> 0 And ElapsedMilliseconds() < timeoutTime.i
      WaitWindowEvent(100)
      doProgress(d.i)
    Wend
    CloseProgram(pid.i)
    
    closeProgress(d.i)
    
    StartID.i + 1
    
    Protected neededTime.i = ElapsedMilliseconds() - myStart.i
    Protected timePerPage.i = neededTime.i / x.i
    SetGadgetText(gui\lblScanTime, "Scanning speed: " + Str(timePerPage.i / 1000) + " sec/page")
  Next 
EndProcedure

Procedure doConvert(Foldername.s, *Settings.strScanPrefs)
  Protected options.s = "-quality 75%"
  NewList files.s()
  If *Settings\Contrast > 0
    options.s + " -sigmoidal-contrast "+Str(*Settings\Contrast)+",70%"
  EndIf
  If *Settings\Gamma <> 0
    options.s + " -level 0%,100%,"+StrF(1 + (*Settings\Gamma / 10), 1)+"%"
  EndIf
  
  Protected dir.i = ExamineDirectory(#PB_Any, Foldername.s, "*.pnm")  
  If dir.i
    While NextDirectoryEntry(dir.i)
      If DirectoryEntryType(dir.i) = #PB_DirectoryEntry_File
        AddElement(files())
        files() = DirectoryEntryName(dir.i)
      EndIf
    Wend
    FinishDirectory(dir.i)
  EndIf
  SortList(files(), #PB_Sort_Ascending)
  ForEach files()
    Protected source.s = files()
    Protected destination.s = GetFilePart(source.s, #PB_FileSystem_NoExtension) + ".jpg"
    Protected Convert.s = Chr(34) + source.s + Chr(34) + " " + options.s + " " + Chr(34) + destination.s + Chr(34)
    RunProgram("convert", Convert.s, Foldername.s, #PB_Program_Wait)
  Next
EndProcedure

Procedure.i doPDF(Foldername.s, Destination.s, password.s)
  Protected File.s
  NewList files.s()
  If FileSize(Destination.s) <> -1
    DeleteFile(Destination.s) ; cleanup
  EndIf
  Protected dir.i = ExamineDirectory(#PB_Any, Foldername.s, "*.jpg")  
  If dir.i
    While NextDirectoryEntry(dir.i)
      If DirectoryEntryType(dir.i) = #PB_DirectoryEntry_File
        AddElement(files())
        files() = DirectoryEntryName(dir.i)
      EndIf
    Wend
    FinishDirectory(dir.i)
  EndIf
  SortList(files(), #PB_Sort_Ascending)
  ForEach files()
    File.s + " " + Chr(34) + files() + Chr(34)
  Next 
  If File.s = ""
    ProcedureReturn #False
  EndIf
  
  File.s + " " + Chr(34) + Destination.s + Chr(34)
  
  RunProgram("convert", File.s, Foldername.s, #PB_Program_Wait)
  
  If FileSize(Destination.s) < 0
    ProcedureReturn #False
  EndIf
  
  If password.s <> ""
    File.s = Chr(34) + Destination.s + Chr(34)
    RunProgram("qpdf", "--replace-input --encrypt "+Chr(34)+password.s+Chr(34)+" "+Chr(34)+password.s+Chr(34)+" 256 -- " + File.s, Foldername.s, #PB_Program_Wait)
  EndIf
  
  ProcedureReturn #True
EndProcedure

Procedure.i doPrint(Foldername.s)
  Protected File.s
  Protected PDFFile.s = Foldername.s + "printDummy.pdf"
  
  If doPDF(Foldername.s, PDFFile.s, "")
    File.s = Chr(34) + PDFFile.s + Chr(34)
    RunProgram("lpr", File.s, "", #PB_Program_Wait)
  Else
    MessageRequester("ScanToLinux", "Failed to convert PDF")
  EndIf
  
  DeleteFile(PDFFile.s)
  
  ProcedureReturn #True
EndProcedure

Procedure.s GetSelectedScannerDevice()
  Protected name.s = GetGadgetText(gui\scanner)
  ForEach ScannerList()
    If ScannerList()\Name = name.s
      ProcedureReturn ScannerList()\Device
    EndIf
  Next
  ProcedureReturn ""
EndProcedure

Procedure.s GetSelectedScannerOptions()
  Protected name.s = GetGadgetText(gui\scanner)
  ForEach ScannerList()
    If ScannerList()\Name = name.s
      ProcedureReturn ScannerList()\Options
    EndIf
  Next
  ProcedureReturn ""
EndProcedure

Procedure SetSelectedScannerOptions(Options.s)
  Protected name.s = GetGadgetText(gui\scanner)
  ForEach ScannerList()
    If ScannerList()\Name = name.s
      ScannerList()\Options = Options.s
    EndIf
  Next
EndProcedure

Procedure FillPreview(Foldername.s)
  Protected pw.i = GadgetWidth(gui\scrollArea) * 0.83  ; preview width
  Protected px.i = GadgetWidth(gui\scrollArea) * 0.05 ; preview left
  Protected posY.i = #preview_spacing
  Protected firstImage.strPrevImage, imgId.i, page.i
  NewList prevImg.strPrevImage()
  NewList files.s()
  
  ; calculate height and define preview positions
  Protected dir.i = ExamineDirectory(#PB_Any, Foldername.s, "*.jpg")  
  If dir.i
    While NextDirectoryEntry(dir.i)
      If DirectoryEntryType(dir.i) = #PB_DirectoryEntry_File
        AddElement(files())
        files() = DirectoryEntryName(dir.i)
      EndIf
    Wend
    FinishDirectory(dir.i)
    SortList(files(), #PB_Sort_Ascending)
    ForEach files()
      ; make preview
      If firstImage\Name = ""
        ; get data from first image
        imgId.i = LoadImage(#PB_Any, Foldername.s + files())
        If imgId.i <> 0
          firstImage\Name = Foldername.s + files()
          firstImage\Height = ImageHeight(imgId.i)
          firstImage\Width = ImageWidth(imgId.i)
          firstImage\Aspect = firstImage\Height / firstImage\Width
        EndIf
        FreeImage(imgId.i)
      EndIf
      
      AddElement(prevImg())
      With prevImg()
        \Name = Foldername.s + files()
        \PosX = px.i
        \PosY = posY.i
        \Width = pw.i
        \Height = pw.i * firstImage\Aspect
        posY.i = posY.i + \Height + #preview_spacing
      EndWith
    Next
    
  EndIf
   
  If ListSize(prevImg()) < 1
    posY.i = GadgetHeight(gui\scrollArea)
  EndIf
  
  ; resize canvas to fit all preview images
  ResizeGadget(gui\canvas, #PB_Ignore, #PB_Ignore, GadgetWidth(gui\scrollArea), posY.i)
  
  ; draw preview
  Protected font11.i = LoadFont(#PB_Any, "Arial", 11)
  Protected font8.i = LoadFont(#PB_Any, "Arial", 8)
  StartDrawing(CanvasOutput(gui\canvas))
  Box(0, 0, GadgetWidth(gui\canvas), GadgetHeight(gui\canvas), RGB(100,100,100))
  FrontColor(RGB(255,255,255))
  BackColor(RGB(60,60,60))
  ForEach prevImg()
    With prevImg()
      imgId.i = LoadImage(#PB_Any, \Name)
      If imgId.i <> 0
        page.i = page.i + 1
        ; draw some shadow
        Box(\PosX + \Width, \PosY + 4, 4, \Height, RGB(60,60,60)) ; right
        Box(\PosX + 4, \PosY + \Height, \Width, 4, RGB(60,60,60)) ; bottom
        ; draw preview image
        DrawImage(ImageID(imgId.i), \PosX, \PosY, \Width, \Height)
        ; page number
        Circle(\PosX + \Width + 5, \PosY + 8, 12, RGB(225,225,75))
        Circle(\PosX + \Width + 5, \PosY + 8, 10, RGB(60,60,60))
        If page.i < 10
          DrawingFont(FontID(font11.i))
          DrawText(\PosX + \Width + 1, \PosY, Str(page.i))
        Else
          DrawingFont(FontID(font8.i))
          DrawText(\PosX + \Width, \PosY + 2, Str(page.i))
        EndIf
      EndIf
      FreeImage(imgId.i)
    EndWith
  Next
  StopDrawing()
  FreeFont(font11.i)
  FreeFont(font8.i)
  
  ; set scrollarea
  SetGadgetAttribute(gui\scrollArea, #PB_ScrollArea_InnerHeight, posY.i)
  
  SetGadgetText(gui\lblPageCount, "Pages: " + Str(page.i))
EndProcedure

Procedure main()
  Protected XML.s, Event.i, evType.i, evGadget.i
  Protected Foldername.s = GetTemporaryDirectory() + "ScanToLinux/"
  Protected LastSize.i = 0, LastModify.i = 0
  Protected Settings.strScanPrefs
  Protected opt.s = ""
  
  XML  = "<window id='#PB_Any' name='ScanToPDF' text='ScanToPDF for Linux' minwidth='500' minheight='600' flags='#PB_Window_ScreenCentered | #PB_Window_SystemMenu | #PB_Window_SizeGadget'>" +
         "  <vbox expand='item:7' spacing='10'>" +
         "    <hbox height='50'>" +
         "      <button name='scan' text='Scan Page(s)' />" +
         "        <hbox>" +
         "          <button name='save' text='Save PDF' />" +
         "          <button name='saveCrypt' text='Save PDF+pwd' />" +
         "        </hbox>" +
         "      <button name='print' text='Print' />" +
         "      <button name='clear' text='Clear Document' />" +
         "      <button name='close' text='Close' />" +
         "    </hbox>" +
         "    <text text='Scanner settings:' />" +
         "    <hbox>" +
         "      <hbox expand='item:2'>" +
         "        <text text='Scanner:' />" +
         "        <combobox name='scanner' />" +
         "        <button name='scanRefresh' text='refresh list' />" +
         "      </hbox>" +
         "      <hbox expand='item:2'>" +
         "        <text text='Scan resolution:' />" +
         "        <combobox name='dpi' />" +
         "      </hbox>" +
         "    </hbox>" +
         "    <hbox expand='item:2'>" +
         "      <text text='Additional scanimage options:' />" +
         "      <string name='strOptions' text='' />" +
         "      <button name='btnOptions' text='?' />" +
         "    </hbox>" +
         "    <hbox expand='item:1'>" +
         "      <checkbox name='chkAutoClick' text='Automatically click &lt;Yes&gt; for next page (eg ADF scanners)' />" +
         "    </hbox>" +
         "    <hbox>" +
         "      <hbox expand='item:2'>" +
         "        <text text='Gamma:' />" +
         "        <scrollbar name='gamma' min='-10' max='10' value='0' page='1'/>" +
         "        <text name='txtGamma' text='0    ' />" +
         "      </hbox>" +
         "      <hbox expand='item:2'>" +
         "        <text text='Contrast:' />" +
         "        <scrollbar name='contrast' min='0' max='50' value='20' page='1'/>" +
         "        <text name='txtContrast' text='20 ' />" +
         "      </hbox>" +
         "    </hbox>" +
         "    <scrollarea name='scrollarea' scrolling='vertical' innerheight='200' flags='#PB_ScrollArea_Flat'>" +
         "      <!-- <canvas name='canvas'/> -->" +
         "    </scrollarea>" +
         "    <hbox>" +
         "      <text name='lblPageCount' text='Pages: 0' flags='#PB_Text_Border' height='20'/>" +
         "      <text name='lblScanTime' text='Scanning speed: ?'  flags='#PB_Text_Border'/>" +
         "    </hbox> " +
         "  </vbox>" +
         "</window>"
  
  Protected xm.i = CatchXML(#PB_Any, @XML, StringByteLength(XML), 0, #XmlEncoding)
  If XMLStatus(xm.i) = #PB_XML_Success
    Protected dialog.i = CreateDialog(#PB_Any)
    If OpenXMLDialog(dialog.i, xm.i, "ScanToPDF")
      
      With gui
        \dialog = dialog.i
        \window = DialogWindow(dialog.i)
        \scan = DialogGadget(dialog.i, "scan")
        \save = DialogGadget(dialog.i, "save")
        \saveCrypt = DialogGadget(dialog.i, "saveCrypt")
        \print = DialogGadget(dialog.i, "print")
        \clear = DialogGadget(dialog.i, "clear")
        \close = DialogGadget(dialog.i, "close")
        \dpi = DialogGadget(dialog.i, "dpi")
        \gamma = DialogGadget(dialog.i, "gamma")
        \contrast = DialogGadget(dialog.i, "contrast")
        \scanner = DialogGadget(dialog.i, "scanner")
        \scanRefresh = DialogGadget(dialog.i, "scanRefresh")
        \scrollArea = DialogGadget(dialog.i, "scrollarea")
        \txtContrast = DialogGadget(dialog.i, "txtContrast")
        \txtGamma = DialogGadget(dialog.i, "txtGamma")
        \btnOptions = DialogGadget(dialog.i, "btnOptions")
        \strOptions = DialogGadget(dialog.i, "strOptions")
        \lblPageCount = DialogGadget(dialog.i, "lblPageCount")
        \lblScanTime = DialogGadget(dialog.i, "lblScanTime")
        \chkAutoClick = DialogGadget(dialog.i, "chkAutoClick")
        
        GadgetToolTip(\scan, "Scan and add one or more pages")
        GadgetToolTip(\save, "Save all pages as one PDF document")
        GadgetToolTip(\saveCrypt, "Save all pages as encrypted PDF document with password")
        GadgetToolTip(\print, "Output all pages to the standard printer")
        GadgetToolTip(\clear, "Deletes all previous scanned pages")
        GadgetToolTip(\close, "Close program")
        GadgetToolTip(\scanRefresh, "Refresh the list of available scanners")
        GadgetToolTip(\scanner, "The scanner to use for scanning")
        GadgetToolTip(\strOptions, "This will get added to the 'scanimage' call for individual scanner options")
        GadgetToolTip(\btnOptions, "Show individual options of the chosen scanner")
        GadgetToolTip(\dpi, "Chose the DPI for the next scans")
        
      EndWith
      
      ; create canvas
      OpenGadgetList(gui\scrollArea)
      gui\canvas = CanvasGadget(#PB_Any, 0, 0, 1000, 1000)
      CloseGadgetList()
      
      ; fill dpi list
      AddGadgetItem(gui\dpi, -1, "50 DPI")
      AddGadgetItem(gui\dpi, -1, "75 DPI")
      AddGadgetItem(gui\dpi, -1, "100 DPI")
      AddGadgetItem(gui\dpi, -1, "150 DPI")
      AddGadgetItem(gui\dpi, -1, "200 DPI")
      AddGadgetItem(gui\dpi, -1, "300 DPI")
      AddGadgetItem(gui\dpi, -1, "600 DPI")
      AddGadgetItem(gui\dpi, -1, "1200 DPI")
      
      ; preview
      FillPreview(Foldername.s)
      
      RefreshDialog(dialog.i)
      
      LoadSettings()
      
      Repeat
        Event = WaitWindowEvent(250)
        If Event = #PB_Event_Gadget
          evType.i = EventType()
          evGadget.i = EventGadget()
          If evGadget = gui\close And evType.i = #PB_EventType_LeftClick
            Event = #PB_Event_CloseWindow 
          EndIf
          
          If evGadget = gui\scan And evType.i = #PB_EventType_LeftClick
            ; prepare
            If FileSize(Foldername.s) = -1
              CreateDirectory(Foldername.s) ; create folder
            EndIf
            ; ask number of pages
            Protected Pages.i = Val(InputRequester("ScanToPDF", "How many pages do you like to add?", "1"))
            If Pages.i > 0
              ; scan
              Settings\DPI = Val(GetGadgetText(gui\dpi))
              Settings\Device = GetSelectedScannerDevice()
              Settings\Contrast = GetGadgetState(gui\contrast)
              Settings\Options = GetGadgetText(gui\strOptions)
              doScanning(Foldername.s, Settings, Pages.i)
              ; convert
              doConvert(Foldername.s, Settings)
              ; preview
              FillPreview(Foldername.s)
            EndIf
          EndIf
          
          If evGadget = gui\clear And evType.i = #PB_EventType_LeftClick
            If FileSize(Foldername.s) = -2
              DeleteDirectory(Foldername.s, "*.*") ; cleanup
            Else
              CreateDirectory(Foldername.s) ; create folder
            EndIf
            ; update preview
            FillPreview(Foldername.s)
          EndIf
          
          If evGadget = gui\save Or evGadget = gui\saveCrypt
            If CountPNMFiles(Foldername.s) < 1
              Continue
            EndIf
            Protected d.s = gui\lastPath + FormatDate("%yyyy-%mm-%dd", Date()) + "_"
savePDFagain:
            Protected Destination.s = SaveFileRequester("ScanToPDF - Save file to...", d.s, "PDF|*.pdf|*|*", 0)
            If Destination.s <> ""
              If LCase(Right(Destination.s, 4)) <> ".pdf"
                Destination.s + ".pdf"
              EndIf
              Protected pwd.s = ""
              If evGadget = gui\saveCrypt
                pwd.s = Trim(InputRequester("ScanToPDF", "Please enter password for protection:", "", #PB_InputRequester_Password))
              EndIf
              If doPDF(Foldername.s, Destination.s, pwd.s) = #True
                MessageRequester("ScanToPDF", "Document is saved.")
                gui\lastPath = GetPathPart(Destination.s)
              Else
                MessageRequester("ScanToPDF", "Error during save. Try again...")
                d.s = Destination.s
                Goto savePDFagain
              EndIf
            EndIf
          EndIf
          
          If evGadget = gui\scanner
            opt.s = GetSelectedScannerOptions()
            SetGadgetText(gui\strOptions, opt.s)
          EndIf
          
          If evGadget = gui\strOptions
            opt.s = GetGadgetText(gui\strOptions)
            SetSelectedScannerOptions(opt.s)
          EndIf
          
          If evGadget = gui\contrast
            LastModify.i = ElapsedMilliseconds()
            SetGadgetText(gui\txtContrast, Str(GetGadgetState(gui\contrast)))
          EndIf
          
          If evGadget = gui\gamma
            LastModify.i = ElapsedMilliseconds()
            SetGadgetText(gui\txtGamma, Str(GetGadgetState(gui\gamma)))
          EndIf
          
          If evGadget = gui\scanRefresh
            FillScannerlist(#True)
          EndIf
          
          If evGadget = gui\btnOptions
            Settings\Device = GetSelectedScannerDevice()
            ShowScannerOptions(Settings)
          EndIf
          
          If evGadget = gui\print
            If CountPNMFiles(Foldername.s) < 1
              Continue
            EndIf
            doPrint(Foldername.s)
          EndIf
          
        EndIf
        If Event = #PB_Event_SizeWindow
          LastSize.i = ElapsedMilliseconds()
        EndIf
        
        If LastSize.i > 0 And ElapsedMilliseconds() > LastSize.i + 500
          ; set scrollarea
          SetGadgetAttribute(gui\scrollArea, #PB_ScrollArea_InnerHeight, GadgetHeight(gui\canvas))
          ; preview
          FillPreview(Foldername.s)
          LastSize.i = 0
        EndIf
        
        If LastModify.i > 0 And ElapsedMilliseconds() > LastModify.i + 1000
          Settings\Contrast = GetGadgetState(gui\contrast)
          Settings\Gamma = GetGadgetState(gui\gamma)
          doConvert(Foldername.s, Settings)
          ; preview
          FillPreview(Foldername.s)
          LastModify.i = 0
        EndIf
      Until Event = #PB_Event_CloseWindow 
      
    Else  
      Debug "Dialog error: " + DialogError(dialog.i)
    EndIf
  Else
    Debug "XML error: " + XMLError(xm) + " (Line: " + XMLErrorLine(xm) + ")"
  EndIf
  
  SaveSettings()
  
  FreeDialog(gui\dialog)
  
EndProcedure

main()
Best,

Kukulkan
Last edited by Kukulkan on Wed Dec 14, 2022 10:05 am, edited 15 times in total.
User avatar
Kukulkan
Addict
Addict
Posts: 1352
Joined: Mon Jun 06, 2005 2:35 pm
Location: germany
Contact:

Re: Scan to PDF tool for Linux (only Linux)

Post by Kukulkan »

I just updated the source in the first post. It now also makes sure that the display order and the order in generated PDF's is always the same like the scanning order.
User avatar
Kukulkan
Addict
Addict
Posts: 1352
Joined: Mon Jun 06, 2005 2:35 pm
Location: germany
Contact:

Re: Scan to PDF tool for Linux (only Linux)

Post by Kukulkan »

I just updated the code in the first post. Simply some optical enhancements (paper-shadow, page number).

Sadly, no Linux user seems to use or test it. So, I'm the only lonely Linux user who needs to scan to PDF :-)
User avatar
Kukulkan
Addict
Addict
Posts: 1352
Joined: Mon Jun 06, 2005 2:35 pm
Location: germany
Contact:

Re: Scan to PDF tool for Linux (only Linux)

Post by Kukulkan »

I just updated the source in the first post:

- Fixed issue saving PDF to a path containing space
- Enhanced scanner compatibility
- Remember last used save-folder
User avatar
Kukulkan
Addict
Addict
Posts: 1352
Joined: Mon Jun 06, 2005 2:35 pm
Location: germany
Contact:

Re: Scan to PDF tool for Linux (only Linux)

Post by Kukulkan »

Just updated the first post again:

[2015-11-03: Added field for optional scanner options and button to show such scanner options]
User avatar
Kukulkan
Addict
Addict
Posts: 1352
Joined: Mon Jun 06, 2005 2:35 pm
Location: germany
Contact:

Re: Scan to PDF tool for Linux (only Linux)

Post by Kukulkan »

Another update:

[2015-11-06: Fixed wrong date format in default filename. Prevent error if 'save' is clicked while no image is available]
User avatar
ts-soft
Always Here
Always Here
Posts: 5756
Joined: Thu Jun 24, 2004 2:44 pm
Location: Berlin - Germany

Re: Scan to PDF tool for Linux (only Linux)

Post by ts-soft »

Kukulkan wrote:Sadly, no Linux user seems to use or test it. So, I'm the only lonely Linux user who needs to scan to PDF :-)
I need it, but i can't use it :cry:
I don't use sane or can use sane, i use scangearmp (canon) and this don't work with your code.

Sorry, but a nice app!
PureBasic 5.73 | SpiderBasic 2.30 | Windows 10 Pro (x64) | Linux Mint 20.1 (x64)
Old bugs good, new bugs bad! Updates are evil: might fix old bugs and introduce no new ones.
Image
User avatar
Kukulkan
Addict
Addict
Posts: 1352
Joined: Mon Jun 06, 2005 2:35 pm
Location: germany
Contact:

Re: Scan to PDF tool for Linux (only Linux)

Post by Kukulkan »

Hello ts-soft,

Sadly, I found no information about scanimagemp comandline options. What options does it offer? Is it only supporting Canon scanners?

Kukulkan
User avatar
Kukulkan
Addict
Addict
Posts: 1352
Joined: Mon Jun 06, 2005 2:35 pm
Location: germany
Contact:

Re: Scan to PDF tool for Linux (only Linux)

Post by Kukulkan »

Another update:

2015-11-17: Added 'Print' button to also make it a quick copy-tool. Added some tool-tips to most buttons. Fixed bug in scanner-list handling.
User avatar
Kukulkan
Addict
Addict
Posts: 1352
Joined: Mon Jun 06, 2005 2:35 pm
Location: germany
Contact:

Re: Scan to PDF tool for Linux (only Linux)

Post by Kukulkan »

Another update:

2016-01-11: Fixed wrong page order and font size for page numbering if there are more than 9 pages.
User avatar
Kukulkan
Addict
Addict
Posts: 1352
Joined: Mon Jun 06, 2005 2:35 pm
Location: germany
Contact:

Re: Scan to PDF tool for Linux (only Linux)

Post by Kukulkan »

Another update:

2016-02-09: Added displaying some setup information on first start to help people to get it running. Some minor fixes.
User avatar
Kukulkan
Addict
Addict
Posts: 1352
Joined: Mon Jun 06, 2005 2:35 pm
Location: germany
Contact:

Re: Scan to PDF tool for Linux (only Linux)

Post by Kukulkan »

Update:

2016-08-01: Now showing scan progress (learning scan time to be more accurate)
User avatar
Kukulkan
Addict
Addict
Posts: 1352
Joined: Mon Jun 06, 2005 2:35 pm
Location: germany
Contact:

Re: Scan to PDF tool for Linux (only Linux)

Post by Kukulkan »

Update:

2016-09-28: Main window is now remembering its position and size for the next start
User avatar
Kukulkan
Addict
Addict
Posts: 1352
Joined: Mon Jun 06, 2005 2:35 pm
Location: germany
Contact:

Re: Scan to PDF tool for Linux (only Linux)

Post by Kukulkan »

Update:

2017-09-18: The scanimage options are now individually stored for every scanner in the list. So it is more easy to switch between multiple scanners and their settings. Added generic scanner entry.

Important: Settings of a scanner are lost if the scanner is removed from the system (even temporarily) and the "refresh list" button is pressed! So make sure that all your scanners are attached if you click this button.
harkon
Enthusiast
Enthusiast
Posts: 217
Joined: Wed Nov 23, 2005 5:48 pm

Re: Scan to PDF tool for Linux (only Linux)

Post by harkon »

Thanks for doing this. I've been looking for examples to get this done in Linux. Unfortunately it doesn't work for me. HP ScanJet 3500. I think this is common to sane and not anything with your code, but it's like after I am finished with a scan it seems like the scanner is left in an unusable state. I wish there were some facility to force scanner initialization. The other issue I had is when scanning a single page, the scan happens, but there is no image in the preview window and save to PDF fails. If I try to scan another page, the first scanned page poops up and I can save, but the next scanned page never shows up.

I wrote a Windows version of this some time back using EZ-Twain.DLL and was hoping to learn how this was done in Linux. When I have time I will try to figure out the ins and outs of this.
Missed it by that much!!
HK
Post Reply