Bash Script to scale and/or resize PDFs from the command line.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1191 lines
38 KiB

  1. #!/usr/bin/env bash
  2. # pdfScale.sh
  3. #
  4. # Scale PDF to specified percentage of original size.
  5. #
  6. # Gustavo Arnosti Neves - 2016 / 07 / 10
  7. # Latest Version - 2017 / 05 / 14
  8. #
  9. # This script: https://github.com/tavinus/pdfScale
  10. # Based on: http://ma.juii.net/blog/scale-page-content-of-pdf-files
  11. # And: https://gist.github.com/MichaelJCole/86e4968dbfc13256228a
  12. ###################################################
  13. # PAGESIZE LOGIC
  14. # 1- Try to get Mediabox with CAT/GREP
  15. # 2- MacOS => try to use mdls
  16. # 3- Try to use pdfinfo
  17. # 4- Try to use identify (imagemagick)
  18. # 5- Fail
  19. ###################################################
  20. VERSION="2.0.0"
  21. SCALE="0.95" # scaling factor (0.95 = 95%, e.g.)
  22. VERBOSE=0 # verbosity Level
  23. PDFSCALE_NAME="$(basename $0)" # simplified name of this script
  24. # SET AND VALIDATED LATER
  25. GSBIN="" # GhostScript Binary
  26. BCBIN="" # BC Math Binary
  27. IDBIN="" # Identify Binary
  28. PDFINFOBIN="" # PDF Info Binary
  29. MDLSBIN="" # MacOS mdls Binary
  30. OSNAME="$(uname 2>/dev/null)" # Check where we are running
  31. LC_MEASUREMENT="C" # To make sure our numbers have .decimals
  32. LC_ALL="C" # Some languages use , as decimal token
  33. LC_CTYPE="C"
  34. LC_NUMERIC="C"
  35. TRUE=0 # Silly stuff
  36. FALSE=1
  37. ADAPTIVEMODE=$TRUE # Automatically try to guess best mode
  38. AUTOMATIC_SCALING=$TRUE # Default scaling in $SCALE, override by resize mode
  39. MODE=""
  40. RESIZE_PAPER_TYPE=""
  41. CUSTOM_RESIZE_PAPER=$FALSE
  42. FLIP_DETECTION=$TRUE
  43. FLIP_FORCE=$FALSE
  44. AUTO_ROTATION='/PageByPage'
  45. PGWIDTH=""
  46. PGHEIGHT=""
  47. RESIZE_WIDTH=""
  48. RESIZE_HEIGHT=""
  49. ### EXIT FLAGS
  50. EXIT_SUCCESS=0
  51. EXIT_ERROR=1
  52. EXIT_INVALID_PAGE_SIZE_DETECTED=10
  53. EXIT_FILE_NOT_FOUND=20
  54. EXIT_INPUT_NOT_PDF=21
  55. EXIT_INVALID_OPTION=22
  56. EXIT_NO_INPUT_FILE=23
  57. EXIT_INVALID_SCALE=24
  58. EXIT_MISSING_DEPENDENCY=25
  59. EXIT_IMAGEMAGIK_NOT_FOUND=26
  60. EXIT_MAC_MDLS_NOT_FOUND=27
  61. EXIT_PDFINFO_NOT_FOUND=28
  62. EXIT_INVALID_PAPER_SIZE=50
  63. ############################# MAIN #############################
  64. # Main function called at the end
  65. main() {
  66. checkDeps
  67. vprint " Input File: $INFILEPDF"
  68. vprint " Output File: $OUTFILEPDF"
  69. getPageSize
  70. vPrintPageSizes ' Source'
  71. local finalRet=$EXIT_ERROR
  72. if isMixedMode; then
  73. outputFile="$OUTFILEPDF" # backup outFile name
  74. tempFile="${OUTFILEPDF%.pdf}.__TEMP__.pdf" # set a temp file name
  75. OUTFILEPDF="$tempFile" # set output to tmp file
  76. pageResize # resize to tmp file
  77. finalRet=$?
  78. INFILEPDF="$tempFile" # get tmp file as input
  79. OUTFILEPDF="$outputFile" # reset final target
  80. PGWIDTH=$RESIZE_WIDTH # we already know the new page size
  81. PGHEIGHT=$RESIZE_HEIGHT # from the last command (Resize)
  82. vPrintPageSizes ' New'
  83. vPrintScaleFactor
  84. pageScale # scale the resized pdf
  85. finalRet=$(($finalRet+$?))
  86. # remove tmp file
  87. rm "$tempFile" >/dev/null 2>&1 || printError "Error when removing temporary file: $tempFile"
  88. elif isResizeMode; then
  89. vPrintScaleFactor "Disabled (resize only)"
  90. pageResize
  91. finalRet=$?
  92. else
  93. local scaleMode=""
  94. isManualScaledMode && scaleMode='(manual)' || scaleMode='(auto)'
  95. vPrintScaleFactor "$SCALE $scaleMode"
  96. pageScale
  97. finalRet=$?
  98. fi
  99. if [[ finalRet -eq $EXIT_SUCCESS ]]; then
  100. vprint " Final Status: File created successfully"
  101. else
  102. vprint " Final Status: Errors were detected. Exit status: $finalRet"
  103. fi
  104. return $finalRet
  105. }
  106. ###################### GHOSTSCRIPT CALLS #######################
  107. # Runs the ghostscript scaling script
  108. pageScale() {
  109. # Compute translation factors (to center page).
  110. XTRANS=$(echo "scale=6; 0.5*(1.0-$SCALE)/$SCALE*$PGWIDTH" | "$BCBIN")
  111. YTRANS=$(echo "scale=6; 0.5*(1.0-$SCALE)/$SCALE*$PGHEIGHT" | "$BCBIN")
  112. vprint " Translation X: $XTRANS"
  113. vprint " Translation Y: $YTRANS"
  114. local increase=$(echo "scale=0; (($SCALE - 1) * 100)/1" | "$BCBIN")
  115. vprint " Run Scaling: $increase %"
  116. # Scale page
  117. "$GSBIN" \
  118. -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -dSAFER \
  119. -dCompatibilityLevel="1.5" -dPDFSETTINGS="/printer" \
  120. -dColorConversionStrategy=/LeaveColorUnchanged \
  121. -dSubsetFonts=true -dEmbedAllFonts=true \
  122. -dDEVICEWIDTHPOINTS=$PGWIDTH -dDEVICEHEIGHTPOINTS=$PGHEIGHT \
  123. -sOutputFile="$OUTFILEPDF" \
  124. -c "<</BeginPage{$SCALE $SCALE scale $XTRANS $YTRANS translate}>> setpagedevice" \
  125. -f "$INFILEPDF"
  126. return $?
  127. }
  128. # Runs the ghostscript paper resize script
  129. pageResize() {
  130. # Get new paper sizes if not custom paper
  131. isNotCustomPaper && getGSPaperSize "$RESIZE_PAPER_TYPE"
  132. vprint " Auto Rotate: $(basename $AUTO_ROTATION)"
  133. # Flip detect
  134. local tmpInverter=""
  135. if [[ $FLIP_DETECTION -eq $TRUE || $FLIP_FORCE -eq $TRUE ]]; then
  136. if [[ $PGWIDTH -gt $PGHEIGHT && $RESIZE_WIDTH -lt $RESIZE_HEIGHT ]] || [[ $FLIP_FORCE -eq $TRUE ]]; then
  137. [[ $FLIP_FORCE -eq $TRUE ]] && vprint " Flip Detect: Forced Mode!" || vprint " Flip Detect: Wrong orientation detected!"
  138. vprint " Inverting Width <-> Height"
  139. tmpInverter=$RESIZE_HEIGHT
  140. RESIZE_HEIGHT=$RESIZE_WIDTH
  141. RESIZE_WIDTH=$tmpInverter
  142. else
  143. vprint " Flip Detect: No change needed"
  144. fi
  145. else
  146. vprint " Flip Detect: Disabled"
  147. fi
  148. vprint " Run Resizing: $(uppercase "$RESIZE_PAPER_TYPE") ( "$RESIZE_WIDTH" x "$RESIZE_HEIGHT" ) pts"
  149. # Change page size
  150. "$GSBIN" \
  151. -q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -dSAFER \
  152. -dCompatibilityLevel="1.5" -dPDFSETTINGS="/printer" \
  153. -dColorConversionStrategy=/LeaveColorUnchanged \
  154. -dSubsetFonts=true -dEmbedAllFonts=true \
  155. -dDEVICEWIDTHPOINTS=$RESIZE_WIDTH -dDEVICEHEIGHTPOINTS=$RESIZE_HEIGHT \
  156. -dAutoRotatePages=$AUTO_ROTATION \
  157. -dFIXEDMEDIA -dPDFFitPage \
  158. -sOutputFile="$OUTFILEPDF" \
  159. -f "$INFILEPDF"
  160. return $?
  161. }
  162. ########################## INITIALIZERS #########################
  163. # Loads external dependencies and checks for errors
  164. initDeps() {
  165. GSBIN="$(which gs 2>/dev/null)"
  166. BCBIN="$(which bc 2>/dev/null)"
  167. IDBIN=$(which identify 2>/dev/null)
  168. MDLSBIN="$(which mdls 2>/dev/null)"
  169. PDFINFOBIN="$(which pdfinfo 2>/dev/null)"
  170. vprint "Checking for ghostscript and bcmath"
  171. if notIsAvailable "$GSBIN"; then printDependency 'ghostscript'; fi
  172. if notIsAvailable "$BCBIN"; then printDependency 'bc'; fi
  173. }
  174. # Checks for dependencies errors, run after getting options
  175. checkDeps() {
  176. if [[ $MODE = "IDENTIFY" ]]; then
  177. vprint "Checking for imagemagick's identify"
  178. if notIsAvailable "$IDBIN"; then printDependency 'imagemagick'; fi
  179. fi
  180. if [[ $MODE = "PDFINFO" ]]; then
  181. vprint "Checking for pdfinfo"
  182. if notIsAvailable "$PDFINFOBIN"; then printDependency 'pdfinfo'; fi
  183. fi
  184. if [[ $MODE = "MDLS" ]]; then
  185. vprint "Checking for MacOS mdls"
  186. if notIsAvailable "$MDLSBIN"; then
  187. initError 'mdls executable was not found! Is this even MacOS?' $EXIT_MAC_MDLS_NOT_FOUND 'nobanner'
  188. fi
  189. fi
  190. }
  191. ######################### CLI OPTIONS ##########################
  192. # Parse options
  193. getOptions() {
  194. while getopts ":vhVs:m:r:pf:a:" o; do
  195. case "${o}" in
  196. v)
  197. ((VERBOSE++))
  198. ;;
  199. h)
  200. printHelp
  201. exit $EXIT_SUCCESS
  202. ;;
  203. V)
  204. printVersion
  205. exit $EXIT_SUCCESS
  206. ;;
  207. s)
  208. parseScale ${OPTARG}
  209. ;;
  210. m)
  211. parseMode ${OPTARG}
  212. ;;
  213. r)
  214. parsePaperResize ${OPTARG}
  215. ;;
  216. p)
  217. printPaperInfo
  218. exit $EXIT_SUCCESS
  219. ;;
  220. f)
  221. parseFlipDetectionMode ${OPTARG}
  222. ;;
  223. a)
  224. parseAutoRotationMode ${OPTARG}
  225. ;;
  226. *)
  227. initError "Invalid Option: -$OPTARG" $EXIT_INVALID_OPTION
  228. ;;
  229. esac
  230. done
  231. shift $((OPTIND-1))
  232. # Validate input PDF file
  233. INFILEPDF="$1"
  234. isEmpty "$INFILEPDF" && initError "Input file is empty!" $EXIT_NO_INPUT_FILE
  235. isPDF "$INFILEPDF" || initError "Input file is not a PDF file: $INFILEPDF" $EXIT_INPUT_NOT_PDF
  236. isFile "$INFILEPDF" || initError "Input file not found: $INFILEPDF" $EXIT_FILE_NOT_FOUND
  237. printVersion 1 'verbose'
  238. if isMixedMode; then
  239. vprint " Mixed Tasks: Resize & Scale"
  240. isEmpty "$2" && OUTFILEPDF="${INFILEPDF%.pdf}.$(uppercase $RESIZE_PAPER_TYPE).SCALED.pdf"
  241. elif isResizeMode; then
  242. vprint " Single Task: Resize PDF Paper"
  243. isEmpty "$2" && OUTFILEPDF="${INFILEPDF%.pdf}.$(uppercase $RESIZE_PAPER_TYPE).pdf"
  244. else
  245. vprint " Single Task: Scale PDF Contents"
  246. isEmpty "$2" && OUTFILEPDF="${INFILEPDF%.pdf}.SCALED.pdf"
  247. fi
  248. isNotEmpty "$2" && OUTFILEPDF="${2%.pdf}.pdf"
  249. }
  250. # Parses and validates the scaling factor
  251. parseScale() {
  252. AUTOMATIC_SCALING=$FALSE
  253. if ! isFloatBiggerThanZero "$1"; then
  254. printError "Invalid factor: $1"
  255. printError "The factor must be a floating point number greater than 0"
  256. printError "Example: for 80% use 0.8"
  257. exit $EXIT_INVALID_SCALE
  258. fi
  259. SCALE="$1"
  260. }
  261. # Parse a forced mode of operation
  262. parseMode() {
  263. local param="$(lowercase $1)"
  264. case "${param}" in
  265. c|catgrep|'cat+grep')
  266. ADAPTIVEMODE=$FALSE
  267. MODE="CATGREP"
  268. return $TRUE
  269. ;;
  270. i|imagemagick|identify)
  271. ADAPTIVEMODE=$FALSE
  272. MODE="IDENTIFY"
  273. return $TRUE
  274. ;;
  275. m|mdls|quartz|mac)
  276. ADAPTIVEMODE=$FALSE
  277. MODE="MDLS"
  278. return $TRUE
  279. ;;
  280. p|pdfinfo)
  281. ADAPTIVEMODE=$FALSE
  282. MODE="PDFINFO"
  283. return $TRUE
  284. ;;
  285. *)
  286. ADAPTIVEMODE=$TRUE
  287. MODE=""
  288. if [[ "$param" != 'a' && "$param" != 'auto' && "$param" != 'automatic' && "$param" != 'adaptive' ]]; then
  289. printError "Error! Invalid PDF Size Detection Mode: \"$1\", using adaptive mode!"
  290. return $FALSE
  291. fi
  292. return $TRUE
  293. ;;
  294. esac
  295. return $FALSE
  296. }
  297. # Parses and validates the scaling factor
  298. parseFlipDetectionMode() {
  299. local param="$(lowercase $1)"
  300. case "${param}" in
  301. d|disable)
  302. FLIP_DETECTION=$FALSE
  303. FLIP_FORCE=$FALSE
  304. ;;
  305. f|force)
  306. FLIP_DETECTION=$FALSE
  307. FLIP_FORCE=$TRUE
  308. ;;
  309. *)
  310. [[ "$param" != 'a' && "$param" != 'auto' ]] && printError "Error! Invalid Flip Detection Mode: \"$1\", using automatic mode!"
  311. FLIP_DETECTION=$TRUE
  312. FLIP_FORCE=$FALSE
  313. ;;
  314. esac
  315. }
  316. # Parses and validates the scaling factor
  317. parseAutoRotationMode() {
  318. local param="$(lowercase $1)"
  319. case "${param}" in
  320. n|none)
  321. AUTO_ROTATION='/None'
  322. ;;
  323. a|all)
  324. AUTO_ROTATION='/All'
  325. ;;
  326. p|pagebypage|auto)
  327. AUTO_ROTATION='/PageByPage'
  328. ;;
  329. *)
  330. printError "Error! Invalid Auto Rotation Mode: $param, using default: $(basename $AUTO_ROTATION)"
  331. ;;
  332. esac
  333. }
  334. ################### PDF PAGE SIZE DETECTION ####################
  335. # Gets page size using imagemagick's identify
  336. getPageSizeImagemagick() {
  337. # Sanity and Adaptive together
  338. if notIsFile "$IDBIN" && isNotAdaptiveMode; then
  339. notAdaptiveFailed "Make sure you installed ImageMagick and have identify on your \$PATH" "ImageMagick's Identify"
  340. elif notIsFile "$IDBIN" && isAdaptiveMode; then
  341. return $FALSE
  342. fi
  343. # get data from image magick
  344. local identify="$("$IDBIN" -format '%[fx:w] %[fx:h]BREAKME' "$INFILEPDF" 2>/dev/null)"
  345. if isEmpty "$identify" && isNotAdaptiveMode; then
  346. notAdaptiveFailed "ImageMagicks's Identify returned an empty string!"
  347. elif isEmpty "$identify" && isAdaptiveMode; then
  348. return $FALSE
  349. fi
  350. identify="${identify%%BREAKME*}" # get page size only for 1st page
  351. identify=($identify) # make it an array
  352. PGWIDTH=$(printf '%.0f' "${identify[0]}") # assign
  353. PGHEIGHT=$(printf '%.0f' "${identify[1]}") # assign
  354. return $TRUE
  355. }
  356. # Gets page size using Mac Quarts mdls
  357. getPageSizeMdls() {
  358. # Sanity and Adaptive together
  359. if notIsFile "$MDLSBIN" && isNotAdaptiveMode; then
  360. notAdaptiveFailed "Are you even trying this on a Mac?" "Mac Quartz mdls"
  361. elif notIsFile "$MDLSBIN" && isAdaptiveMode; then
  362. return $FALSE
  363. fi
  364. local identify="$("$MDLSBIN" -mdls -name kMDItemPageHeight -name kMDItemPageWidth "$INFILEPDF" 2>/dev/null)"
  365. if isEmpty "$identify" && isNotAdaptiveMode; then
  366. notAdaptiveFailed "Mac Quartz mdls returned an empty string!"
  367. elif isEmpty "$identify" && isAdaptiveMode; then
  368. return $FALSE
  369. fi
  370. identify=${identify//$'\t'/ } # change tab to space
  371. identify=($identify) # make it an array
  372. if [[ "${identify[5]}" = "(null)" || "${identify[2]}" = "(null)" ]] && isNotAdaptiveMode; then
  373. notAdaptiveFailed "There was no metadata to read from the file! Is Spotlight OFF?"
  374. elif [[ "${identify[5]}" = "(null)" || "${identify[2]}" = "(null)" ]] && isAdaptiveMode; then
  375. return $FALSE
  376. fi
  377. PGWIDTH=$(printf '%.0f' "${identify[5]}") # assign
  378. PGHEIGHT=$(printf '%.0f' "${identify[2]}") # assign
  379. return $TRUE
  380. }
  381. # Gets page size using Linux PdfInfo
  382. getPageSizePdfInfo() {
  383. # Sanity and Adaptive together
  384. if notIsFile "$PDFINFOBIN" && isNotAdaptiveMode; then
  385. notAdaptiveFailed "Do you have pdfinfo installed and available on your \$PATH?" "Linux pdfinfo"
  386. elif notIsFile "$PDFINFOBIN" && isAdaptiveMode; then
  387. return $FALSE
  388. fi
  389. # get data from image magick
  390. local identify="$("$PDFINFOBIN" "$INFILEPDF" 2>/dev/null | grep -i 'Page size:' )"
  391. if isEmpty "$identify" && isNotAdaptiveMode; then
  392. notAdaptiveFailed "Linux PdfInfo returned an empty string!"
  393. elif isEmpty "$identify" && isAdaptiveMode; then
  394. return $FALSE
  395. fi
  396. identify="${identify##*Page size:}" # remove stuff
  397. identify=($identify) # make it an array
  398. PGWIDTH=$(printf '%.0f' "${identify[0]}") # assign
  399. PGHEIGHT=$(printf '%.0f' "${identify[2]}") # assign
  400. return $TRUE
  401. }
  402. # Gets page size using cat and grep
  403. getPageSizeCatGrep() {
  404. # get MediaBox info from PDF file using cat and grep, these are all possible
  405. # /MediaBox [0 0 595 841]
  406. # /MediaBox [ 0 0 595.28 841.89]
  407. # /MediaBox[ 0 0 595.28 841.89 ]
  408. # Get MediaBox data if possible
  409. local mediaBox="$(cat "$INFILEPDF" | grep -a '/MediaBox' | head -n1)"
  410. mediaBox="${mediaBox##*/MediaBox}"
  411. # No page size data available
  412. if isEmpty "$mediaBox" && isNotAdaptiveMode; then
  413. notAdaptiveFailed "There is no MediaBox in the pdf document!"
  414. elif isEmpty "$mediaBox" && isAdaptiveMode; then
  415. return $FALSE
  416. fi
  417. # remove chars [ and ]
  418. mediaBox="${mediaBox//[}"
  419. mediaBox="${mediaBox//]}"
  420. mediaBox=($mediaBox) # make it an array
  421. mbCount=${#mediaBox[@]} # array size
  422. # sanity
  423. if [[ $mbCount -lt 4 ]]; then
  424. printError "Error when reading the page size!"
  425. printError "The page size information is invalid!"
  426. exit $EXIT_INVALID_PAGE_SIZE_DETECTED
  427. fi
  428. # we are done
  429. PGWIDTH=$(printf '%.0f' "${mediaBox[2]}") # Get Round Width
  430. PGHEIGHT=$(printf '%.0f' "${mediaBox[3]}") # Get Round Height
  431. return $TRUE
  432. }
  433. # Detects operation mode and also runs the adaptive mode
  434. getPageSize() {
  435. if isNotAdaptiveMode; then
  436. vprint " Get Page Size: Adaptive Disabled"
  437. if [[ $MODE = "CATGREP" ]]; then
  438. vprint " Method: Cat + Grep"
  439. getPageSizeCatGrep
  440. elif [[ $MODE = "MDLS" ]]; then
  441. vprint " Method: Mac Quartz mdls"
  442. getPageSizeMdls
  443. elif [[ $MODE = "PDFINFO" ]]; then
  444. vprint " Method: PDFInfo"
  445. getPageSizePdfInfo
  446. elif [[ $MODE = "IDENTIFY" ]]; then
  447. vprint " Method: ImageMagick's Identify"
  448. getPageSizeImagemagick
  449. else
  450. printError "Error! Invalid Mode: $MODE"
  451. printError "Aborting execution..."
  452. exit $EXIT_INVALID_OPTION
  453. fi
  454. return $TRUE
  455. fi
  456. vprint " Get Page Size: Adaptive Enabled"
  457. vprint " Method: Cat + Grep"
  458. getPageSizeCatGrep
  459. if pageSizeIsInvalid && [[ $OSNAME = "Darwin" ]]; then
  460. vprint " Failed"
  461. vprint " Method: Mac Quartz mdls"
  462. getPageSizeMdls
  463. fi
  464. if pageSizeIsInvalid; then
  465. vprint " Failed"
  466. vprint " Method: PDFInfo"
  467. getPageSizePdfInfo
  468. fi
  469. if pageSizeIsInvalid; then
  470. vprint " Failed"
  471. vprint " Method: ImageMagick's Identify"
  472. getPageSizeImagemagick
  473. fi
  474. if pageSizeIsInvalid; then
  475. vprint " Failed"
  476. printError "Error when detecting PDF paper size!"
  477. printError "All methods of detection failed"
  478. printError "You may want to install pdfinfo or imagemagick"
  479. exit $EXIT_INVALID_PAGE_SIZE_DETECTED
  480. fi
  481. return $TRUE
  482. }
  483. # Prints error message and exits execution
  484. notAdaptiveFailed() {
  485. local errProgram="$2"
  486. local errStr="$1"
  487. if isEmpty "$2"; then
  488. printError "Error when reading input file!"
  489. printError "Could not determine the page size!"
  490. else
  491. printError "Error! $2 was not found!"
  492. fi
  493. isNotEmpty "$errStr" && printError "$errStr"
  494. printError "Aborting! You may want to try the adaptive mode."
  495. exit $EXIT_INVALID_PAGE_SIZE_DETECTED
  496. }
  497. # Verbose print of the Width and Height (Source or New) to screen
  498. vPrintPageSizes() {
  499. vprint " $1 Width: $PGWIDTH postscript-points"
  500. vprint "$1 Height: $PGHEIGHT postscript-points"
  501. }
  502. #################### GHOSTSCRIPT PAPER INFO ####################
  503. # Loads GS paper info to memory
  504. getPaperInfo() {
  505. # name inchesW inchesH mmW mmH pointsW pointsH
  506. sizesUS="\
  507. 11x17 11.0 17.0 279 432 792 1224
  508. ledger 17.0 11.0 432 279 1224 792
  509. legal 8.5 14.0 216 356 612 1008
  510. letter 8.5 11.0 216 279 612 792
  511. lettersmall 8.5 11.0 216 279 612 792
  512. archE 36.0 48.0 914 1219 2592 3456
  513. archD 24.0 36.0 610 914 1728 2592
  514. archC 18.0 24.0 457 610 1296 1728
  515. archB 12.0 18.0 305 457 864 1296
  516. archA 9.0 12.0 229 305 648 864"
  517. sizesISO="\
  518. a0 33.1 46.8 841 1189 2384 3370
  519. a1 23.4 33.1 594 841 1684 2384
  520. a2 16.5 23.4 420 594 1191 1684
  521. a3 11.7 16.5 297 420 842 1191
  522. a4 8.3 11.7 210 297 595 842
  523. a4small 8.3 11.7 210 297 595 842
  524. a5 5.8 8.3 148 210 420 595
  525. a6 4.1 5.8 105 148 297 420
  526. a7 2.9 4.1 74 105 210 297
  527. a8 2.1 2.9 52 74 148 210
  528. a9 1.5 2.1 37 52 105 148
  529. a10 1.0 1.5 26 37 73 105
  530. isob0 39.4 55.7 1000 1414 2835 4008
  531. isob1 27.8 39.4 707 1000 2004 2835
  532. isob2 19.7 27.8 500 707 1417 2004
  533. isob3 13.9 19.7 353 500 1001 1417
  534. isob4 9.8 13.9 250 353 709 1001
  535. isob5 6.9 9.8 176 250 499 709
  536. isob6 4.9 6.9 125 176 354 499
  537. c0 36.1 51.1 917 1297 2599 3677
  538. c1 25.5 36.1 648 917 1837 2599
  539. c2 18.0 25.5 458 648 1298 1837
  540. c3 12.8 18.0 324 458 918 1298
  541. c4 9.0 12.8 229 324 649 918
  542. c5 6.4 9.0 162 229 459 649
  543. c6 4.5 6.4 114 162 323 459"
  544. sizesJIS="\
  545. jisb0 NA NA 1030 1456 2920 4127
  546. jisb1 NA NA 728 1030 2064 2920
  547. jisb2 NA NA 515 728 1460 2064
  548. jisb3 NA NA 364 515 1032 1460
  549. jisb4 NA NA 257 364 729 1032
  550. jisb5 NA NA 182 257 516 729
  551. jisb6 NA NA 128 182 363 516"
  552. sizesOther="\
  553. flsa 8.5 13.0 216 330 612 936
  554. flse 8.5 13.0 216 330 612 936
  555. halfletter 5.5 8.5 140 216 396 612
  556. hagaki 3.9 5.8 100 148 283 420"
  557. sizesAll="\
  558. $sizesUS
  559. $sizesISO
  560. $sizesJIS
  561. $sizesOther"
  562. }
  563. # Gets a paper size in points and sets it to RESIZE_WIDTH and RESIZE_HEIGHT
  564. getGSPaperSize() {
  565. isEmpty "$sizesall" && getPaperInfo
  566. while read l; do
  567. local cols=($l)
  568. if [[ "$1" == ${cols[0]} ]]; then
  569. RESIZE_WIDTH=${cols[5]}
  570. RESIZE_HEIGHT=${cols[6]}
  571. return $TRUE
  572. fi
  573. done <<< "$sizesAll"
  574. }
  575. # Loads an array with paper names to memory
  576. getPaperNames() {
  577. paperNames=(a0 a1 a2 a3 a4 a4small a5 a6 a7 a8 a9 a10 isob0 isob1 isob2 isob3 isob4 isob5 isob6 c0 c1 c2 c3 c4 c5 c6 \
  578. 11x17 ledger legal letter lettersmall archE archD archC archB archA \
  579. jisb0 jisb1 jisb2 jisb3 jisb4 jisb5 jisb6 \
  580. flsa flse halfletter hagaki)
  581. }
  582. # Prints uppercase paper names to screen (used in help)
  583. printPaperNames() {
  584. isEmpty "$paperNames" && getPaperNames
  585. for i in "${!paperNames[@]}"; do
  586. [[ $i -eq 0 ]] && echo -n -e ' '
  587. [[ $i -ne 0 && $((i % 5)) -eq 0 ]] && echo -n -e $'\n '
  588. ppN="$(uppercase ${paperNames[i]})"
  589. printf "%-14s" "$ppN"
  590. done
  591. echo ""
  592. }
  593. # Returns $TRUE if $! is a valid paper name, $FALSE otherwise
  594. isPaperName() {
  595. isEmpty "$1" && return $FALSE
  596. isEmpty "$paperNames" && getPaperNames
  597. for i in "${paperNames[@]}"; do
  598. [[ "$i" = "$1" ]] && return $TRUE
  599. done
  600. return $FALSE
  601. }
  602. # Prints all tables with ghostscript paper information
  603. printPaperInfo() {
  604. printVersion
  605. echo $'\n'"Valid Ghostscript Paper Sizes accepted"$'\n'
  606. getPaperInfo
  607. printPaperTable "ISO STANDARD" "$sizesISO"; echo
  608. printPaperTable "US STANDARD" "$sizesUS"; echo
  609. printPaperTable "JIS STANDARD *Aproximated Points" "$sizesJIS"; echo
  610. printPaperTable "OTHERS" "$sizesOther"; echo
  611. }
  612. # GS paper table helper, prints a full line
  613. printTableLine() {
  614. echo '+-----------------------------------------------------------------+'
  615. }
  616. # GS paper table helper, prints a line with dividers
  617. printTableDivider() {
  618. echo '+-----------------+-------+-------+-------+-------+-------+-------+'
  619. }
  620. # GS paper table helper, prints a table header
  621. printTableHeader() {
  622. echo '| Name | inchW | inchH | mm W | mm H | pts W | pts H |'
  623. }
  624. # GS paper table helper, prints a table title
  625. printTableTitle() {
  626. printf "| %-64s%s\n" "$1" '|'
  627. }
  628. # GS paper table printer, prints a table for a paper variable
  629. printPaperTable() {
  630. printTableLine
  631. printTableTitle "$1"
  632. printTableLine
  633. printTableHeader
  634. printTableDivider
  635. while read l; do
  636. local cols=($l)
  637. printf "| %-15s | %+5s | %+5s | %+5s | %+5s | %+5s | %+5s |\n" ${cols[*]};
  638. done <<< "$2"
  639. printTableDivider
  640. }
  641. # Validades the a paper resize CLI option and sets the paper to $RESIZE_PAPER_TYPE
  642. parsePaperResize() {
  643. isEmpty "$1" && initError 'Invalid Paper Type: (empty)' $EXIT_INVALID_PAPER_SIZE
  644. local lowercasePaper="$(lowercase $1)"
  645. if [[ "$1" = 'custom' ]]; then
  646. if isNotValidMeasure "$2" || ! isFloatBiggerThanZero "$3" || ! isFloatBiggerThanZero "$4"; then
  647. initError "Invalid Custom Paper Definition!"$'\n'"Use: -r 'custom <measurement> <width> <height>'"$'\n'"Measurements: mm, in, pts" $EXIT_INVALID_OPTION
  648. fi
  649. RESIZE_PAPER_TYPE="custom"
  650. CUSTOM_RESIZE_PAPER=$TRUE
  651. if isMilimeter "$2"; then
  652. RESIZE_WIDTH="$(milimetersToPoints "$3")"
  653. RESIZE_HEIGHT="$(milimetersToPoints "$4")"
  654. elif isInch "$2"; then
  655. RESIZE_WIDTH="$(inchesToPoints "$3")"
  656. RESIZE_HEIGHT="$(inchesToPoints "$4")"
  657. elif isPoint "$2"; then
  658. RESIZE_WIDTH="$3"
  659. RESIZE_HEIGHT="$4"
  660. else
  661. initError "Invalid Custom Paper Definition!"$'\n'"Use: -r 'custom <measurement> <width> <height>'"$'\n'"Measurements: mm, in, pts" $EXIT_INVALID_OPTION
  662. fi
  663. else
  664. isPaperName "$lowercasePaper" || initError "Invalid Paper Type: $1" $EXIT_INVALID_PAPER_SIZE
  665. RESIZE_PAPER_TYPE="$lowercasePaper"
  666. fi
  667. }
  668. # Returns $TRUE if $1 is a valid measurement for a custom paper, $FALSE otherwise
  669. isNotValidMeasure() {
  670. isMilimeter "$1" || isInch "$1" || isPoint "$1" && return $FALSE
  671. return $TRUE
  672. }
  673. # Returns $TRUE if $1 is a valid milimeter string, $FALSE otherwise
  674. isMilimeter() {
  675. [[ "$1" = 'mm' || "$1" = 'milimeters' || "$1" = 'milimeter' ]] && return $TRUE
  676. return $FALSE
  677. }
  678. # Returns $TRUE if $1 is a valid inch string, $FALSE otherwise
  679. isInch() {
  680. [[ "$1" = 'in' || "$1" = 'inch' || "$1" = 'inches' ]] && return $TRUE
  681. return $FALSE
  682. }
  683. # Returns $TRUE if $1 is a valid point string, $FALSE otherwise
  684. isPoint() {
  685. [[ "$1" = 'pt' || "$1" = 'pts' || "$1" = 'point' || "$1" = 'points' ]] && return $TRUE
  686. return $FALSE
  687. }
  688. # Returns $TRUE if a custom paper is being used, $FALSE otherwise
  689. isCustomPaper() {
  690. return $CUSTOM_RESIZE_PAPER
  691. }
  692. isNotCustomPaper() {
  693. isCustomPaper && return $FALSE
  694. return $TRUE
  695. }
  696. # Returns $TRUE if the scale was set manually, $FALSE if we are using automatic scaling
  697. isManualScaledMode() {
  698. [[ $AUTOMATIC_SCALING -eq $TRUE ]] && return $FALSE
  699. return $TRUE
  700. }
  701. # Returns true if we are resizing a paper (ignores scaling), false otherwise
  702. isResizeMode() {
  703. isEmpty $RESIZE_PAPER_TYPE && return $FALSE
  704. return $TRUE
  705. }
  706. # Returns true if we are resizing a paper and the scale was manually set
  707. isMixedMode() {
  708. isResizeMode && isManualScaledMode && return $TRUE
  709. return $FALSE
  710. }
  711. # Prints the lowercase char value for $1
  712. lowercaseChar() {
  713. case "$1" in
  714. [A-Z])
  715. n=$(printf "%d" "'$1")
  716. n=$((n+32))
  717. printf \\$(printf "%o" "$n")
  718. ;;
  719. *)
  720. printf "%s" "$1"
  721. ;;
  722. esac
  723. }
  724. # Prints the lowercase version of a string
  725. lowercase() {
  726. word="$@"
  727. for((i=0;i<${#word};i++))
  728. do
  729. ch="${word:$i:1}"
  730. lowercaseChar "$ch"
  731. done
  732. }
  733. # Prints the uppercase char value for $1
  734. uppercaseChar(){
  735. case "$1" in
  736. [a-z])
  737. n=$(printf "%d" "'$1")
  738. n=$((n-32))
  739. printf \\$(printf "%o" "$n")
  740. ;;
  741. *)
  742. printf "%s" "$1"
  743. ;;
  744. esac
  745. }
  746. # Prints the uppercase version of a string
  747. uppercase() {
  748. word="$@"
  749. for((i=0;i<${#word};i++))
  750. do
  751. ch="${word:$i:1}"
  752. uppercaseChar "$ch"
  753. done
  754. }
  755. # Prints the postscript points rounded equivalent from $1 mm
  756. milimetersToPoints() {
  757. local pts=$(echo "scale=6; $1 * 72 / 25.4" | "$BCBIN")
  758. printf '%.0f' "$pts" # Print rounded conversion
  759. }
  760. # Prints the postscript points rounded equivalent from $1 mm
  761. inchesToPoints() {
  762. local pts=$(echo "scale=6; $1 * 72" | "$BCBIN")
  763. printf '%.0f' "$pts" # Print rounded conversion
  764. }
  765. ########################## VALIDATORS ##########################
  766. # Returns $TRUE if $PGWIDTH OR $PGWIDTH are empty or NOT an Integer, $FALSE otherwise
  767. pageSizeIsInvalid() {
  768. if isNotAnInteger "$PGWIDTH" || isNotAnInteger "$PGHEIGHT"; then
  769. return $TRUE
  770. fi
  771. return $FALSE
  772. }
  773. # Return $TRUE if adaptive mode is enabled, $FALSE otherwise
  774. isAdaptiveMode() {
  775. return $ADAPTIVEMODE
  776. }
  777. # Return $TRUE if adaptive mode is disabled, $FALSE otherwise
  778. isNotAdaptiveMode() {
  779. isAdaptiveMode && return $FALSE
  780. return $TRUE
  781. }
  782. # Return $TRUE if $1 is empty, $FALSE otherwise
  783. isEmpty() {
  784. [[ -z "$1" ]] && return $TRUE
  785. return $FALSE
  786. }
  787. # Return $TRUE if $1 is NOT empty, $FALSE otherwise
  788. isNotEmpty() {
  789. [[ -z "$1" ]] && return $FALSE
  790. return $TRUE
  791. }
  792. # Returns $TRUE if $1 is an integer, $FALSE otherwise
  793. isAnInteger() {
  794. case $1 in
  795. ''|*[!0-9]*) return $FALSE ;;
  796. *) return $TRUE ;;
  797. esac
  798. }
  799. # Returns $TRUE if $1 is NOT an integer, $FALSE otherwise
  800. isNotAnInteger() {
  801. case $1 in
  802. ''|*[!0-9]*) return $TRUE ;;
  803. *) return $FALSE ;;
  804. esac
  805. }
  806. # Returns $TRUE if $1 is a floating point number (or an integer), $FALSE otherwise
  807. isFloat() {
  808. [[ -n "$1" && "$1" =~ ^-?[0-9]*([.][0-9]+)?$ ]] && return $TRUE
  809. return $FALSE
  810. }
  811. # Returns $TRUE if $1 is a floating point number bigger than zero, $FALSE otherwise
  812. isFloatBiggerThanZero() {
  813. isFloat "$1" && [[ (( $1 > 0 )) ]] && return $TRUE
  814. return $FALSE
  815. }
  816. # Returns $TRUE if $1 has a .pdf extension, false otherwsie
  817. isPDF() {
  818. [[ "$1" =~ ^..*\.pdf$ ]] && return $TRUE
  819. return $FALSE
  820. }
  821. # Returns $TRUE if $1 is a file, false otherwsie
  822. isFile() {
  823. [[ -f "$1" ]] && return $TRUE
  824. return $FALSE
  825. }
  826. # Returns $TRUE if $1 is NOT a file, false otherwsie
  827. notIsFile() {
  828. [[ -f "$1" ]] && return $FALSE
  829. return $TRUE
  830. }
  831. # Returns $TRUE if $1 is executable, false otherwsie
  832. isExecutable() {
  833. [[ -x "$1" ]] && return $TRUE
  834. return $FALSE
  835. }
  836. # Returns $TRUE if $1 is NOT executable, false otherwsie
  837. notIsExecutable() {
  838. [[ -x "$1" ]] && return $FALSE
  839. return $TRUE
  840. }
  841. # Returns $TRUE if $1 is a file and executable, false otherwsie
  842. isAvailable() {
  843. if isFile "$1" && isExecutable "$1"; then
  844. return $TRUE
  845. fi
  846. return $FALSE
  847. }
  848. # Returns $TRUE if $1 is NOT a file or NOT executable, false otherwsie
  849. notIsAvailable() {
  850. if notIsFile "$1" || notIsExecutable "$1"; then
  851. return $TRUE
  852. fi
  853. return $FALSE
  854. }
  855. ###################### PRINTING TO SCREEN ######################
  856. # Prints version
  857. printVersion() {
  858. local vStr=""
  859. [[ "$2" = 'verbose' ]] && vStr=" - Verbose Execution"
  860. local strBanner="$PDFSCALE_NAME v$VERSION$vStr"
  861. if [[ $1 -eq 2 ]]; then
  862. printError "$strBanner"
  863. elif [[ $1 -eq 3 ]]; then
  864. echo "$strBanner"
  865. else
  866. vprint "$strBanner"
  867. fi
  868. }
  869. # Prints the scale factor to screen, or custom message
  870. vPrintScaleFactor() {
  871. local scaleMsg="$SCALE"
  872. isNotEmpty "$1" && scaleMsg="$1"
  873. vprint " Scale Factor: $scaleMsg"
  874. }
  875. # Prints help info
  876. printHelp() {
  877. printVersion 3
  878. local paperList="$(printPaperNames)"
  879. echo "
  880. Usage: $PDFSCALE_NAME <inFile.pdf>
  881. $PDFSCALE_NAME [-v] [-s <factor>] [-m <page-detection>] <inFile.pdf> [outfile.pdf]
  882. $PDFSCALE_NAME [-v] [-r <paper>] [-f <flip-detection>] [-a <auto-rotation>] <inFile.pdf> [outfile.pdf]
  883. $PDFSCALE_NAME -p
  884. $PDFSCALE_NAME -h
  885. $PDFSCALE_NAME -V
  886. Parameters:
  887. -v Verbose mode, prints extra information
  888. Use twice for timestamp
  889. -h Print this help to screen and exits
  890. -V Prints version to screen and exits
  891. -m <mode> Page size Detection mode
  892. May disable the Adaptive Mode
  893. -s <factor> Changes the scaling factor or forces scaling
  894. Defaults: $SCALE / no scaling (resize mode)
  895. MUST be a number bigger than zero
  896. Eg. -s 0.8 for 80% of the original size
  897. -r <paper> Triggers the Resize Paper Mode
  898. Resize PDF paper proportionally
  899. Uses a valid paper name or a custom defined paper
  900. -f <mode> Flip Detection Mode, defaults to 'auto'.
  901. Inverts Width <-> Height of a Resized PDF.
  902. Modes: a, auto - automatic detection, default
  903. f, force - forces flip W <-> H
  904. d, disable - disables flipping
  905. -a <mode> GS Auto-Rotation Setting, defaults to 'PageByPage'.
  906. Setting for GS -dAutoRotatePages.
  907. Modes: p, pagebypage - auto-rotates pages individually
  908. a, all - rotates all pages (or none) depending
  909. on a kind of \"majority decision\"
  910. n, none - retains orientation of each page
  911. -p Prints Ghostscript paper info tables to screen
  912. Scaling Mode:
  913. The default mode of operation is scaling mode with fixed paper
  914. size and scaling pre-set to $SCALE. By not using the resize mode
  915. you are using scaling mode. Flip-Detection and auto-rotation are
  916. disabled in Scaling mode.
  917. Resize Paper Mode:
  918. Disables the default scaling factor! ($SCALE)
  919. Changes the PDF Paper Size in points. Will fit-to-page.
  920. Mixed Mode:
  921. In mixed mode both the -s option and -r option must be specified.
  922. The PDF will be first resized then scaled.
  923. Output filename:
  924. The output filename is optional. If no file name is passed
  925. the output file will have the same name/destination of the
  926. input file with added suffixes:
  927. .SCALED.pdf is added to scaled files
  928. .<PAPERSIZE>.pdf is added to resized files
  929. .<PAPERSIZE>.SCALED.pdf is added in mixed mode
  930. Page Size Detection Modes:
  931. a, adaptive Default mode, tries all the methods below
  932. c, cat+grep Forces the use of the cat + grep method
  933. m, mdls Forces the use of MacOS Quartz mdls
  934. p, pdfinfo Forces the use of PDFInfo
  935. i, identify Forces the use of ImageMagick's Identify
  936. Valid Paper Names: (case-insensitive)
  937. $paperList
  938. Custom Paper Size:
  939. Paper size can be set manually in Milimeters, Inches or Points.
  940. Use: $PDFSCALE_NAME -r 'custom <measurement> <width> <height>'
  941. Ex: $PDFSCALE_NAME -r 'custom mm 300 300'
  942. Measurements can be: mm, inch, pts.
  943. Custom paper definition MUST be quoted into a single parameter.
  944. Actual size is applied in points (mms and inches are transformed).
  945. Additional Notes:
  946. - Adaptive Page size detection will try different modes until
  947. it gets a page size. You can force a mode with -m 'mode'.
  948. - Options must be passed before the file names to be parsed.
  949. - Having the extension .pdf on the output file name is optional,
  950. it will be added if not present.
  951. - File and folder names with spaces should be quoted or escaped.
  952. - The scaling is centered and using a scale bigger than 1 may
  953. result on cropping parts of the pdf.
  954. Examples:
  955. $PDFSCALE_NAME myPdfFile.pdf
  956. $PDFSCALE_NAME myPdfFile.pdf \"My Scaled Pdf\"
  957. $PDFSCALE_NAME -v -v myPdfFile.pdf
  958. $PDFSCALE_NAME -s 0.85 myPdfFile.pdf My\\ Scaled\\ Pdf.pdf
  959. $PDFSCALE_NAME -m pdfinfo -s 0.80 -v myPdfFile.pdf
  960. $PDFSCALE_NAME -v -v -m i -s 0.7 myPdfFile.pdf
  961. $PDFSCALE_NAME -r A4 myPdfFile.pdf
  962. $PDFSCALE_NAME -v -v -r \"custom mm 252 356\" -s 0.9 -f \"../input file.pdf\" \"../my new pdf\"
  963. $PDFSCALE_NAME -h
  964. "
  965. }
  966. # Prints usage info
  967. usage() {
  968. [[ "$2" != 'nobanner' ]] && printVersion 2
  969. isNotEmpty "$1" && printError "$1"
  970. printError "Usage: $PDFSCALE_NAME [-v] [-s <factor>] [-m <mode>] <inFile.pdf> [outfile.pdf]"
  971. printError "Try: $PDFSCALE_NAME -h # for help"
  972. }
  973. # Prints Verbose information
  974. vprint() {
  975. [[ $VERBOSE -eq 0 ]] && return $TRUE
  976. timestamp=""
  977. [[ $VERBOSE -gt 1 ]] && timestamp="$(date +%Y-%m-%d:%H:%M:%S) | "
  978. echo "$timestamp$1"
  979. }
  980. # Prints dependency information and aborts execution
  981. printDependency() {
  982. #printVersion 2
  983. local brewName="$1"
  984. [[ "$1" = 'pdfinfo' && "$OSNAME" = "Darwin" ]] && brewName="xpdf"
  985. printError $'\n'"ERROR! You need to install the package '$1'"$'\n'
  986. printError "Linux apt-get.: sudo apt-get install $1"
  987. printError "Linux yum.....: sudo yum install $1"
  988. printError "MacOS homebrew: brew install $brewName"
  989. printError $'\n'"Aborting..."
  990. exit $EXIT_MISSING_DEPENDENCY
  991. }
  992. # Prints initialization errors and aborts execution
  993. initError() {
  994. local errStr="$1"
  995. local exitStat=$2
  996. isEmpty "$exitStat" && exitStat=$EXIT_ERROR
  997. usage "ERROR! $errStr" "$3"
  998. exit $exitStat
  999. }
  1000. # Prints to stderr
  1001. printError() {
  1002. echo >&2 "$@"
  1003. }
  1004. ########################## EXECUTION ###########################
  1005. initDeps
  1006. getOptions "${@}"
  1007. main
  1008. exit $?