Jump to content

[LISP] BPDF - Batch export Model Space title block frames to individual PDFs


Recommended Posts

Posted

Hi CADTutor,

I'm an architect in Taiwan and wrote a free AutoLISP 
tool to solve a problem we face daily - batch exporting 
all title block frames in Model Space to individual PDFs.

For those of us who work with all drawings arranged 
in one Model Space (common workflow in Taiwan/Asia), 
AutoCAD's built-in Batch Plot doesn't help much since 
it works at the Layout level.

Features:
- Click on a title block frame to detect the Block name 
  automatically
- Option to plot just that 1 frame, or all frames with 
  the same name
- Sorts output top-to-bottom, left-to-right
- Dynamically loads plot styles and paper sizes from 
  your machine
- Remembers last settings
- Compatible with AutoCAD 2014 and above

GitHub: https://github.com/beastt1992/autocad-batch-plot

Already tested by users on Reddit r/AutoCAD with good 
feedback. A couple of bugs were found and fixed based 
on community input (AC_WINDOW constant varies by version, 
RefreshPlotDeviceInfo order).

Would love feedback from the AutoLISP community here - 
especially if anyone tests it on different AutoCAD 
versions or workflows!
 

Posted (edited)

So post the LISP so we can give feedback - it is easier to reference if the LISP is in the same thread as the comments and questions

 

Though I might be tempted to say change the system and get the draughters to use paperspace for what it is meant for.

(haven't had the paperspace / modespace discussion on here for a while now....)

Edited by Steven P
  • Like 1
Posted (edited)
;;; BATCH-PDF
;;; Function: Batch export all title block frames to PDF
;;; Command: BPDF
;;; Compatible: AutoCAD 2014+
;;; v2.0 - Single-frame mode + cross-version compatibility
;;;        AC_WINDOW auto-detected (3 or 4)
;;;        RefreshPlotDeviceInfo order fixed for fresh sessions

(setq AC_0DEG 0)
(setq AC_FIT  0)

(defun BPDF-cfgpath ()
  (strcat (getenv "APPDATA") "\\bpdf_settings.cfg")
)

(defun BPDF-load-cfg ( / f line kv cfg)
  (setq cfg (list
    (cons "blockname" "")
    (cons "prefix" "frame")
    (cons "outpath" (strcat (getenv "USERPROFILE") "\\Desktop"))
    (cons "styleNum" "1")
    (cons "paperNum" "1")
    (cons "scaleStr" "")
  ))
  (setq f (open (BPDF-cfgpath) "r"))
  (if f
    (progn
      (while (setq line (read-line f))
        (setq kv (vl-string-search "=" line))
        (if kv
          (setq cfg (subst
            (cons (substr line 1 kv) (substr line (+ kv 2)))
            (assoc (substr line 1 kv) cfg)
            cfg
          ))
        )
      )
      (close f)
    )
  )
  cfg
)

(defun BPDF-save-cfg (blockname prefix outpath styleNum paperNum scaleStr / f)
  (setq f (open (BPDF-cfgpath) "w"))
  (if f
    (progn
      (write-line (strcat "blockname=" blockname) f)
      (write-line (strcat "prefix=" prefix) f)
      (write-line (strcat "outpath=" outpath) f)
      (write-line (strcat "styleNum=" (itoa styleNum)) f)
      (write-line (strcat "paperNum=" (itoa paperNum)) f)
      (write-line (strcat "scaleStr=" scaleStr) f)
      (close f)
    )
  )
)

(defun c:BPDF ( / blockname outpath ss i ent obj sel entdata plotMode
                  minpoint maxpoint pt1 pt2
                  counter fname adoc alayout aplot
                  scaleStr scaleVal useFit styleSheet
                  prefix frameList frame fx fy fx2 fy2 rowHeight
                  plotterName styleList styleNum
                  allMedia mediaShort mediaIdx m j item
                  paperNum paperSize cfg lastVal inp confirm
                  AC_WINDOW err)
  (vl-load-com)

  (setq cfg (BPDF-load-cfg))

  ;; 1. Click to select title block, or type name
  (princ "\nClick on a title block frame (or press Enter to type block name): ")
  (setq sel (entsel ""))

  (if sel
    (progn
      (setq ent (car sel))
      (setq entdata (entget ent))
      (if (= (cdr (assoc 0 entdata)) "INSERT")
        (progn
          (setq blockname (cdr (assoc 2 entdata)))
          (princ (strcat "\nBlock detected: " blockname "\n"))

          (initget "1 A")
          (setq plotMode
            (getkword "\nPlot [1=This frame only / A=All frames with this name] <A>: ")
          )
          (if (null plotMode) (setq plotMode "A"))
        )
        (progn
          (alert "Please click on a Block (INSERT) entity.")
          (exit)
        )
      )
    )
    (progn
      (setq lastVal (cdr (assoc "blockname" cfg)))
      (if (/= lastVal "")
        (setq inp (getstring (strcat "\nBlock Name <" lastVal ">: ")))
        (setq inp (getstring "\nBlock Name: "))
      )
      (setq blockname (if (= inp "") lastVal inp))
      (if (= blockname "") (progn (princ "\nCancelled.") (exit)))
      (setq plotMode "A")
    )
  )

  ;; 2. PDF Prefix
  (setq lastVal (cdr (assoc "prefix" cfg)))
  (setq inp (getstring (strcat "\nPDF Prefix <" lastVal ">: ")))
  (setq prefix (if (= inp "") lastVal inp))

  ;; 3. Output Folder
  (setq lastVal (cdr (assoc "outpath" cfg)))
  (setq inp (getstring (strcat "\nOutput Folder <" lastVal ">: ")))
  (setq outpath (if (= inp "") lastVal inp))
  (if (/= (substr outpath (strlen outpath)) "\\")
    (setq outpath (strcat outpath "\\"))
  )
  (vl-mkdir outpath)
  (princ (strcat "Output: " outpath "\n"))

  ;; 4. Build frame list
  (if (= plotMode "1")
    (progn
      ;; Single mode: get bbox of the clicked entity directly
      (setq obj (vlax-ename->vla-object ent))
      (vla-getboundingbox obj 'minpoint 'maxpoint)
      (setq frameList (list (list
        (vlax-safearray-get-element minpoint 0)
        (vlax-safearray-get-element minpoint 1)
        (vlax-safearray-get-element maxpoint 0)
        (vlax-safearray-get-element maxpoint 1)
      )))
      (setq counter 1)
      (princ "Mode: Single frame\n")
    )
    (progn
      ;; All mode: find all matching blocks
      (setq ss (ssget "X"
        (list (cons 0 "INSERT") (cons 2 blockname) (cons 410 "Model"))
      ))
      (if (null ss)
        (progn (alert (strcat "Block not found: " blockname)) (exit))
      )
      (setq counter (sslength ss))
      (setq frameList nil i 0)
      (repeat counter
        (setq ent (ssname ss i))
        (setq obj (vlax-ename->vla-object ent))
        (vla-getboundingbox obj 'minpoint 'maxpoint)
        (setq fx (vlax-safearray-get-element minpoint 0))
        (setq fy (vlax-safearray-get-element minpoint 1))
        (setq fx2 (vlax-safearray-get-element maxpoint 0))
        (setq fy2 (vlax-safearray-get-element maxpoint 1))
        (setq frameList (append frameList (list (list fx fy fx2 fy2))))
        (setq i (1+ i))
      )
      ;; Sort: top-to-bottom rows, left-to-right within row
      (setq rowHeight (* (- (cadddr (car frameList)) (cadr (car frameList))) 0.5))
      (setq frameList
        (vl-sort frameList
          (function (lambda (a b)
            (if (> (abs (- (cadr a) (cadr b))) rowHeight)
              (> (cadr a) (cadr b))
              (< (car a) (car b))
            )
          ))
        )
      )
      (princ (strcat "Mode: All frames (" (itoa counter) " found)\n"))
    )
  )

  ;; 5. VLA setup
  ;;    Fix from forum: RefreshPlotDeviceInfo BEFORE ConfigName,
  ;;    then ConfigName, then RefreshPlotDeviceInfo again.
  ;;    This prevents "Invalid input" on fresh CAD sessions.
  (setq adoc (vla-get-activedocument (vlax-get-acad-object)))
  (setq alayout (vla-get-activelayout adoc))
  (setq aplot (vla-get-plot adoc))
  (setq plotterName "DWG To PDF.pc3")
  (vla-RefreshPlotDeviceInfo alayout)
  (vla-put-configname alayout plotterName)
  (vla-RefreshPlotDeviceInfo alayout)

  ;; Detect AC_WINDOW value: try 4 first (works on more setups),
  ;; fall back to 3 only if 4 is rejected.
  ;; Note: plottype=3 can silently "accept" but not actually work.
  (setq AC_WINDOW 4)
  (setq err (vl-catch-all-apply
    (function (lambda () (vla-put-plottype alayout 4)))
  ))
  (if (vl-catch-all-error-p err)
    (progn
      (vla-put-plottype alayout 3)
      (setq AC_WINDOW 3)
    )
  )

  ;; 6. Plot Style
  (setq styleList
    (vlax-safearray->list
      (vlax-variant-value (vla-GetPlotStyleTableNames alayout))
    )
  )
  (setq styleList (append (list "None (Color)") styleList))
  (setq lastVal (atoi (cdr (assoc "styleNum" cfg))))
  (if (or (< lastVal 1) (> lastVal (length styleList))) (setq lastVal 1))
  (princ "\nPlot Style:\n")
  (setq i 1)
  (foreach s styleList
    (princ (strcat "  " (itoa i) ". " s (if (= i lastVal) "  <- last" "") "\n"))
    (setq i (1+ i))
  )
  (setq inp (getint (strcat "Select <" (itoa lastVal) ">: ")))
  (setq styleNum (if (or (null inp) (< inp 1) (> inp (length styleList))) lastVal inp))
  (setq styleSheet (if (= styleNum 1) "" (nth (1- styleNum) styleList)))
  (princ (strcat "Style: " (if (= styleSheet "") "None" styleSheet) "\n"))

  ;; 7. Paper Size
  (setq allMedia
    (vlax-safearray->list
      (vlax-variant-value (vla-GetCanonicalMediaNames alayout))
    )
  )
  (setq mediaShort nil mediaIdx 0)
  (foreach m allMedia
    (if (or (vl-string-search "A0" m) (vl-string-search "A1" m)
            (vl-string-search "A2" m) (vl-string-search "A3" m)
            (vl-string-search "A4" m))
      (setq mediaShort (append mediaShort (list (list mediaIdx m))))
    )
    (setq mediaIdx (1+ mediaIdx))
  )
  (setq lastVal (atoi (cdr (assoc "paperNum" cfg))))
  (if (or (< lastVal 1) (> lastVal (length mediaShort))) (setq lastVal 1))
  (princ "\nPaper Size:\n")
  (setq j 1)
  (foreach item mediaShort
    (princ (strcat "  " (itoa j) ". " (cadr item) (if (= j lastVal) "  <- last" "") "\n"))
    (setq j (1+ j))
  )
  (setq inp (getint (strcat "Select <" (itoa lastVal) ">: ")))
  (setq paperNum (if (or (null inp) (< inp 1) (> inp (length mediaShort))) lastVal inp))
  (setq paperSize (cadr (nth (1- paperNum) mediaShort)))
  (princ (strcat "Paper: " paperSize "\n"))

  ;; 8. Scale
  (setq lastVal (cdr (assoc "scaleStr" cfg)))
  (setq inp (getstring
    (strcat "\nScale denominator (100=1:100, 0 or F=Fit, Enter=last setting)"
      (if (/= lastVal "") (strcat " <" lastVal ">") " <Fit>") ": ")
  ))
  (setq scaleStr (if (= inp "") lastVal inp))
  ;; 0 or F or empty = Fit to paper
  (if (or (= scaleStr "")
          (= scaleStr "0")
          (= (strcase scaleStr) "F")
          (= (strcase scaleStr) "FIT"))
    (progn (setq useFit T scaleVal 0) (setq scaleStr ""))
    (setq useFit nil scaleVal (atof scaleStr))
  )

  (BPDF-save-cfg blockname prefix outpath styleNum paperNum scaleStr)

  ;; 9. Apply plot settings ONCE before loop (not inside loop!)
  (setvar "BACKGROUNDPLOT" 0)
  (vla-put-canonicalmedianame alayout paperSize)
  (vla-put-plotrotation alayout AC_0DEG)
  (vla-put-centerplot alayout :vlax-true)
  (vla-put-plotwithlineweights alayout :vlax-true)
  (if (/= styleSheet "")
    (progn
      (vla-put-plotwithplotstyles alayout :vlax-true)
      (vla-put-stylesheet alayout styleSheet)
    )
    (vla-put-plotwithplotstyles alayout :vlax-false)
  )
  (if useFit
    (progn
      (vla-put-useStandardScale alayout :vlax-true)
      (vla-put-standardscale alayout AC_FIT)
    )
    (progn
      (vla-put-useStandardScale alayout :vlax-false)
      (vla-SetCustomScale alayout 1.0 scaleVal)
    )
  )

  ;; 10. Confirm
  (alert (strcat
    "===== Confirm Plot =====\n\n"
    "Frames: " (itoa counter) "\n"
    "Prefix: " prefix "\n"
    "Output: " outpath "\n"
    "Paper:  " paperSize "\n"
    "Style:  " (if (= styleSheet "") "None" styleSheet) "\n"
    "Scale:  " (if useFit "Fit" (strcat "1:" (rtos scaleVal 2 0))) "\n\n"
    "Click OK to continue"
  ))
  (initget "Y N")
  (setq confirm (getkword "\nConfirm [Y=Yes / N=Cancel] <Y>: "))
  (if (= confirm "N")
    (progn (princ "\nCancelled.") (exit))
  )

  ;; 11. Plot loop
  ;;     SetWindowToPlot THEN put-plottype (per Autodesk docs)
  ;;     No RefreshPlotDeviceInfo inside the loop (that was breaking it)
  (princ "Plotting...\n")
  (setq i 0)
  (foreach frame frameList
    (setq pt1 (vlax-make-safearray vlax-vbdouble '(0 . 1)))
    (vlax-safearray-put-element pt1 0 (car frame))
    (vlax-safearray-put-element pt1 1 (cadr frame))
    (setq pt2 (vlax-make-safearray vlax-vbdouble '(0 . 1)))
    (vlax-safearray-put-element pt2 0 (caddr frame))
    (vlax-safearray-put-element pt2 1 (cadddr frame))
    (vla-SetWindowToPlot alayout pt1 pt2)
    (vla-put-plottype alayout AC_WINDOW)
    (setq fname (strcat outpath prefix "_" (itoa (1+ i)) ".pdf"))
    (vla-PlotToFile aplot fname)
    (princ (strcat "  [" (itoa (1+ i)) "/" (itoa counter) "] Done\n"))
    (setq i (1+ i))
  )

  (setvar "BACKGROUNDPLOT" 2)
  (alert (strcat "Done! " (itoa counter) " PDFs exported\nLocation: " outpath))
  (princ)
)

GitHub link with README and installation instructions:
https://github.com/beastt1992/autocad-batch-plot

Happy to answer any questions!

Edited by Beastt1992
Posted
16 minutes ago, Steven P said:

So post the LISP so we can give feedback - it is easier to reference if the LISP is in the same thread as the comments and questions

 

Though I might be tempted to say change the system and get the draughters to use paperspace for what it is meant for.

(haven't had the paperspace / modespace discussion on here for a while now....)

Thanks Steven! Code is posted above. 
Regarding Paper Space - completely agree it's 
the better long-term approach, but changing an 
entire office workflow is harder than writing 
a tool to work around it!

Posted

My 0.02¢

 

Instead of outputting the status to command line 
(princ (strcat "  [" (itoa (1+ i)) "/" (itoa counter) "] Done\n"))

Output to the Status Bar
(setvar "MODEMACRO" (strcat "Processing: [" (itoa (1+ i)) "/" (itoa counter) "] Layouts"))

 

Then you don't have clutter/spam in the command prompt but still have the current stats. Just have to clear it after complete.

 

image.png.50cd200a98de58726b466cf6e501f3af.png

Posted
1 hour ago, mhupp said:

My 0.02¢

 

Instead of outputting the status to command line 
(princ (strcat "  [" (itoa (1+ i)) "/" (itoa counter) "] Done\n"))

Output to the Status Bar
(setvar "MODEMACRO" (strcat "Processing: [" (itoa (1+ i)) "/" (itoa counter) "] Layouts"))

 

Then you don't have clutter/spam in the command prompt but still have the current stats. Just have to clear it after complete.

 

image.png.50cd200a98de58726b466cf6e501f3af.png

That's a great tip, thank you! Much cleaner than spamming the command line. I'll add this to the next version and clear the status bar when done.
 

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Unfortunately, your content contains terms that we do not allow. Please edit your content to remove the highlighted words below.
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...