Yu-Gi-Oh! Deck Building and Card Inventory Management web interface written in Common Lisp, utilizing HTMX.
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

278 Zeilen
12KB

  1. #|
  2. draw.lisp
  3. The main drawing stuff from ImageMagick is handled here. We use
  4. UIOP:RUN-PROGRAM. It's pretty clumsy, but it works for the types
  5. of files we're working with.
  6. It could probably use some polishing up. Particularly the command
  7. processing. Using INFERIOR-SHELL just calls UIOP:RUN-PROGRAM under the
  8. hood anyway. Is it worth it for the extra param processing? It might
  9. be slightly safer, certainly LISP-ier...
  10. |#
  11. (in-package :cl-user)
  12. (defpackage #:cl-deck-builder2.draw
  13. (:use #:cl)
  14. (:local-nicknames (#:v #:org.shirakumo.verbose))
  15. (:import-from #:cl-deck-builder2.toolkit
  16. #:make-static-filename-kind
  17. #:relative-file-list
  18. #:ygo-probe-file-list
  19. #:static-probe-file-list)
  20. (:import-from #:cl-deck-builder2.models
  21. #:ydk-main-deck-of
  22. #:ydk-extra-deck-of
  23. #:ydk-side-deck-of)
  24. (:export :draw-deck-image)
  25. (:documentation "The Drawing package.
  26. We currently wrap ImageMagick on the command line using UIOP:RUN-PROGRAM.
  27. Cairo2 does not support JPEG. We generate a command pipeline with ~montage~ and ~convert~ binaries."))
  28. (in-package #:cl-deck-builder2.draw)
  29. ;; Rough class abstraction:
  30. (defclass draw-pipeline ()
  31. ())
  32. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  33. (defparameter *command* nil
  34. "The current command line used in the drawing process. Useful for inspection/debug purposes.")
  35. (defparameter +convert-binary+ "convert"
  36. "The path to ImageMagick ~convert~ binary.")
  37. (defparameter +montage-binary+ "montage"
  38. "The path to ImageMagick ~montage~ binary.")
  39. ;; This is for full deck list. I can have a few params for different
  40. ;; decks e.g. main, side, extra.
  41. ;;
  42. ;; TODO Having all these params is ugly. Maybe a hash table?
  43. (defparameter +command-line-params-main-deck+
  44. ;;"-resize '421x614>' -geometry +5+5 -tile 10x")
  45. ; "-geometry +5+5 -tile 10x -texture '/home/user/public/noise.png'" ;;'#ff8b53'"
  46. "-geometry +5+5 -tile 10x -background '#FF8B53'"
  47. "Parameters for making the MAIN deck. It's 10x cards wide, There's 5 px between cards. The color is from <https://yugioh.fandom.com/wiki/Card_colors>.
  48. This is for full main deck list. I can have a few params for different decks e.g. main, side, extra. This one is for the main deck. Parameters for making the MAIN deck: It's 10x cards wide,There's 5 px between cards.")
  49. (defparameter +command-line-params-extra-deck+
  50. ;; "-resize '421x614>' -geometry +5+5 -tile 15x")
  51. "-geometry +5+5 -tile 15x -background '#BC5A84'"
  52. "Parameters for making the EXTRA deck. It's 15x cards wide,There's 5 px between cards.")
  53. (defparameter +command-line-params-side-deck+
  54. ;; "-resize '421x614>' -geometry +5+5 -tile 15x")
  55. "-geometry +5+5 -tile 15x -background '#1D9E74'"
  56. "Parameters for making the SIDE deck. It's 15x cards wide,There's 5 px between cards.")
  57. (defparameter +command-line-params-main-deck-output+
  58. ;; "-resize '421x614>' -geometry +5+5 -tile 15x")
  59. ;; "-geometry '4310x2496>'"
  60. ;; "-geometry '2560x>' -background '#FF8B53' -gravity South -splice 0x10"
  61. "-background '#FF8B53' -gravity South -splice 0x10"
  62. "Parameters for final output of the MAIN deck. It's the width of the MAIN deck output, which will be set to 2560.
  63. TODO: Use identify to query this information.")
  64. (defparameter +command-line-params-extra-deck-output+
  65. ;; "-resize '421x614>' -geometry +5+5 -tile 15x")
  66. ;; "-geometry '4310x2496>'"
  67. "-geometry '4310>'"
  68. "Parameters for final output of the EXTRA and SIDE decks. It's exactly the width of the MAIN deck output, which happens to be 4310x2496.
  69. TODO: Use identify to query this information.
  70. Parameters for final output of the EXTRA and SIDE decks. It's exactly the width of the MAIN deck output, which happens to be 4310x2496. Card images are almost always 421x614 px in size, and we add 5 px of padding around all sides. So that's where this number comes from (((421+10)*10) = 4310).")
  71. (defparameter +command-line-params-final-output+
  72. ;; "-resize '421x614>' -geometry +0+0 -tile 1x")
  73. "-geometry +0+0 -tile 1x -background none"
  74. "The options passed to convert for the final image result. Smoosh everything together into one 1x wide image!")
  75. (defparameter +command-line-params-watermark-lower-right+
  76. "-gravity southeast -geometry +10+10 -draw \"image Over 32,32 256,256 'runew.png'\""
  77. "Draw a watermark in the bottom right with this command.")
  78. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  79. (defun command-setup (file-list pathname &key (deck-type :main) (dry-run nil))
  80. "Set up the *COMMAND* parameter for use in COMMAND-EXEC.
  81. You can kind of see the structure of the command:
  82. ~A ~A ~{\"~A\" ~} miff:- | ~A miff:- ~A \"~A\"
  83. That's \"montage [input-options] [list-of-input-filenames] miff:- | convert miff:- [output-options] [output-filename]\".
  84. We use the MIFF format to speed up the whole montage process.
  85. ARGUMENTS:
  86. FILE-LIST The list of files to be used in the generation of the deck image. The files must exist.
  87. PATHNAME The pathname of the output file.
  88. DECK-TYPE The kind of deck we'll be generating. Allowed values are :MAIN :SIDE :EXTRA and :FINAL. The default is :MAIN.
  89. DRY-RUN If DRY-RUN is T, instead of filling in a whole file list, which can be clunky to view, we replace the list of files with ~'(FILE LIST)~. Very kludgy.
  90. SEE *COMMAND*
  91. SEE COMMAND-EXEC"
  92. (let* ((extra-params (case deck-type
  93. (:main +command-line-params-main-deck+)
  94. (:extra +command-line-params-extra-deck+)
  95. (:side +command-line-params-side-deck+)
  96. (:final +command-line-params-final-output+)
  97. (otherwise "")))
  98. (output-params (case deck-type
  99. (:main +command-line-params-main-deck-output+)
  100. ((:side :extra) +command-line-params-extra-deck-output+)
  101. (otherwise "")))
  102. (command (format nil "~A ~A ~{\"~A\" ~} miff:- | ~A miff:- ~A \"~A\""
  103. +montage-binary+
  104. extra-params
  105. (if dry-run
  106. '(file list)
  107. file-list)
  108. +convert-binary+
  109. output-params
  110. pathname)))
  111. (setf *command* command)))
  112. (defun command-exec (&key (command *command*) (dry-run nil))
  113. "Execute the command with UIOP:RUN-PROGRAM, or do a dry run.
  114. ARGUMENTS:
  115. COMMAND The command line to execute. It doesn't have to be anything to do with ImageMagick. This is a generic wrapper around UIOP:RUN-PROGRAM. The default command line to pass is *COMMAND*.
  116. DRY-RUN If DRY-RUN is T, use FORMAT to output a string instead."
  117. (if dry-run
  118. (v:info :draw "~a" command)
  119. (uiop:run-program command
  120. :output t
  121. :error-output t)))
  122. (defun identify-image (pathname &optional (dry-run nil))
  123. "Wrapper around ImageMagick IDENTIFY.
  124. PATHNAME The path to the file to identify.
  125. DRY-RUN Inform COMMAND-EXEC that this is a DRY-RUN. "
  126. (command-exec :command (format nil "identify \"~A\"" pathname)
  127. :dry-run dry-run))
  128. ;; TODO The way we pass args around like this is ugly af. Maybe a STRUCT?
  129. ;;
  130. ;; This absolutely needs a rewrite. I should be using classes for this.
  131. (defun make-passcode-files-list (types pathname-name ydk)
  132. "Build TYPES -> MERGE-PATHNAMES ASSOCIATION-LIST. TYPES gets filled in
  133. with PATHNAME-NAME and PASSCODE info:
  134. (:MAIN OG_bechi_21-Chaos Turbo-main.jpg ( main deck ... ))
  135. (:SIDE OG_bechi_21-Chaos Turbo-side.jpg ( side deck ... ))
  136. (:EXTRA OG_bechi_21-Chaos Turbo-extra.jpg ( extra deck ... ))
  137. Then the :FINAL image instead of a list of passcodes, is a list of
  138. files passed to STATIC-RELATIVE-FILE-LIST:
  139. (:FINAL OG_bechi_21-Chaos Turbo-final.jpg (OG_bechi_21-Chaos Turbo-main.jpg
  140. OG_bechi_21-Chaos Turbo-extra.jpg
  141. OG_bechi_21-Chaos Turbo-side.jpg))
  142. ARGUMENTS:
  143. types A LIST of the types of images we want to generate. May be one of :MAIN, :SIDE, :EXTRA, or :FINAL.
  144. pathname-name The PATHNAME-NAME of the file you want to create. E.g. if you had a YDK file named \"Chaos Turbo 2023.ydk\" you should pass the output of (PATHNAME-NAME #P\"Chaos Turbo 2023.ydk\"
  145. ydk The YDK information we're generating the deck for."
  146. (loop for type in types
  147. collect
  148. (list type
  149. (make-static-filename-kind pathname-name type)
  150. (case type
  151. (:main (ydk-main-deck-of ydk))
  152. (:extra (ydk-extra-deck-of ydk))
  153. (:side (ydk-side-deck-of ydk))))))
  154. (defun draw-deck-image (ydk pathname &key (dry-run nil))
  155. "Parse a YDK, setup a bunch of command lines, and generate a deck image. Very clunky and clumsy, most likely candidate for a rewrite right here.
  156. ARGUMENTS
  157. DATA Raw YDK data. May be anything that INITIALIZE-WITH-CONTENT can process.
  158. PATHNAME The path to where the output images will be stored. The PATHNAME-NAME will be munged to have ~-main~,~-side~,~-extra-~, or ~-final~ appended to it.
  159. DRY-RUN Do a dry run.
  160. SEE INITIALIZE-WITH-CONTENT"
  161. (v:info :draw "DRAW-DECK-IMAGE: ~a" pathname)
  162. (let* ((pathname-name (typecase pathname
  163. (pathname (pathname-name pathname))
  164. (string pathname)
  165. (integer (princ-to-string pathname))))
  166. (datum (make-passcode-files-list '(:main :extra :side)
  167. pathname-name ydk)))
  168. ;; Build the three images
  169. (loop for data in datum do
  170. (destructuring-bind (kind path lst)
  171. data
  172. (if (not lst)
  173. (progn
  174. ;; Whatever item this is, there are no cards in
  175. ;; it. Remove it from the query.
  176. ;; (format t "~A~%" datum)
  177. (setf datum (delete kind datum :key #'car)))
  178. (progn
  179. ;; There are cards in it.
  180. (v:info :draw "Generating ~A deck: ~A (~A cards)" kind path (length lst))
  181. (if (probe-file path)
  182. (v:info :draw "File exists: ~A" path)
  183. (progn
  184. (command-setup (ygo-probe-file-list lst) path :deck-type kind :dry-run dry-run)
  185. (command-exec :dry-run dry-run)))))))
  186. (let* (;; KEY-PAIRS is :MAIN + (:MAIN :EXTRA:) + (:MAIN :SIDE) if they exist.
  187. ;; If we filtered out something above, it won't show up in this list...
  188. (key-pairs (alexandria:map-product 'list
  189. '(:main)
  190. (remove :main (mapcar #'first datum))))
  191. ;; COMBINATIONS-LIST is every file name combined,
  192. ;; e.g. Deck-Name-MAIN, Deck-Name-MAIN-EXTRA,
  193. ;; Deck-Name-FINAL; the ones from KEY-PAIRS noted above
  194. ;; combined with PATHNAME-NAME. It's in a similar format as
  195. ;; the output from MAKE-PASSCODE-FILES-LIST.
  196. (combinations-list
  197. (loop for key-pair in key-pairs
  198. collect
  199. (list
  200. ;; double loop ... ? Who cares, it works...
  201. (loop for key-pair in key-pair
  202. collect (make-static-filename-kind pathname-name key-pair))
  203. (make-static-filename-kind pathname-name key-pair))))
  204. (final-filenames
  205. (append
  206. (mapcar #'second datum)
  207. (mapcar #'second combinations-list)
  208. (list (make-static-filename-kind pathname-name :final)))))
  209. (merge-images (car (last final-filenames)) (mapcar #'second datum))
  210. (loop for combination in combinations-list do
  211. (merge-images (second combination) (first combination) dry-run))
  212. (relative-file-list final-filenames))))
  213. (defun merge-images (final-filename image-lst &optional dry-run)
  214. "Merge images in IMAGE-LST into FINAL-FILENAME.
  215. IMAGE-LST The list of images to be merged. Will be merged in order specified.
  216. FINAL-FILENAME The output filename.
  217. DRY-RUN Do a dry run."
  218. (v:info :draw "Generating final image: ~A ~a" final-filename image-lst)
  219. ;; MAPCAR #'SECOND??? Oh Well, it works.
  220. (if (probe-file final-filename)
  221. (v:info :draw "File exists: ~A" final-filename)
  222. (progn
  223. (command-setup (static-probe-file-list image-lst)
  224. final-filename :deck-type :final :dry-run dry-run)
  225. (command-exec :dry-run dry-run))))