initial commit
38
go.mod
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
module dobble-round
|
||||
|
||||
go 1.22.3
|
||||
|
||||
require (
|
||||
github.com/charmbracelet/huh v0.4.2
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/go-pdf/fpdf v0.9.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/catppuccin/go v0.2.0 // indirect
|
||||
github.com/charmbracelet/bubbles v0.18.0 // indirect
|
||||
github.com/charmbracelet/bubbletea v0.26.3 // indirect
|
||||
github.com/charmbracelet/lipgloss v0.11.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240524151031-ff83003bf67a // indirect
|
||||
github.com/charmbracelet/x/input v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/term v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/windows v0.1.2 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.15.2 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/image v0.12.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
)
|
||||
97
go.sum
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/catppuccin/go v0.2.0 h1:ktBeIrIP42b/8FGiScP9sgrWOss3lw0Z5SktRoithGA=
|
||||
github.com/catppuccin/go v0.2.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||
github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0=
|
||||
github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw=
|
||||
github.com/charmbracelet/bubbletea v0.26.3 h1:iXyGvI+FfOWqkB2V07m1DF3xxQijxjY2j8PqiXYqasg=
|
||||
github.com/charmbracelet/bubbletea v0.26.3/go.mod h1:bpZHfDHTYJC5g+FBK+ptJRCQotRC+Dhh3AoMxa/2+3Q=
|
||||
github.com/charmbracelet/huh v0.4.2 h1:5wLkwrA58XDAfEZsJzNQlfJ+K8N9+wYwvR5FOM7jXFM=
|
||||
github.com/charmbracelet/huh v0.4.2/go.mod h1:g9OXBgtY3zRV4ahnVih9bZE+1yGYN+y2C9Q6L2P+WM0=
|
||||
github.com/charmbracelet/lipgloss v0.11.0 h1:UoAcbQ6Qml8hDwSWs0Y1cB5TEQuZkDPH/ZqwWWYTG4g=
|
||||
github.com/charmbracelet/lipgloss v0.11.0/go.mod h1:1UdRTH9gYgpcdNN5oBtjbu/IzNKtzVtb7sqN1t9LNn8=
|
||||
github.com/charmbracelet/x/ansi v0.1.1 h1:CGAduulr6egay/YVbGc8Hsu8deMg1xZ/bkaXTPi1JDk=
|
||||
github.com/charmbracelet/x/ansi v0.1.1/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240524151031-ff83003bf67a h1:lOpqe2UvPmlln41DGoii7wlSZ/q8qGIon5JJ8Biu46I=
|
||||
github.com/charmbracelet/x/exp/strings v0.0.0-20240524151031-ff83003bf67a/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||
github.com/charmbracelet/x/exp/term v0.0.0-20240524151031-ff83003bf67a h1:k/s6UoOSVynWiw7PlclyGO2VdVs5ZLbMIHiGp4shFZE=
|
||||
github.com/charmbracelet/x/exp/term v0.0.0-20240524151031-ff83003bf67a/go.mod h1:YBotIGhfoWhHDlnUpJMkjebGV2pdGRCn1Y4/Nk/vVcU=
|
||||
github.com/charmbracelet/x/input v0.1.1 h1:YDOJaTUKCqtGnq9PHzx3pkkl4pXDOANUHmhH3DqMtM4=
|
||||
github.com/charmbracelet/x/input v0.1.1/go.mod h1:jvdTVUnNWj/RD6hjC4FsoB0SeZCJ2ZBkiuFP9zXvZI0=
|
||||
github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI=
|
||||
github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw=
|
||||
github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg=
|
||||
github.com/charmbracelet/x/windows v0.1.2/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ=
|
||||
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
|
||||
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/go-pdf/fpdf v0.9.0 h1:PPvSaUuo1iMi9KkaAn90NuKi+P4gwMedWPHhj8YlJQw=
|
||||
github.com/go-pdf/fpdf v0.9.0/go.mod h1:oO8N111TkmKb9D7VvWGLvLJlaZUQVPM+6V42pp3iV4Y=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ=
|
||||
golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
BIN
img/1.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
img/10.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
img/11.png
Normal file
|
After Width: | Height: | Size: 3 KiB |
BIN
img/12.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
img/13.png
Normal file
|
After Width: | Height: | Size: 6 KiB |
BIN
img/14.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
img/2.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
img/3.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
img/4.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
img/5.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
img/6.png
Normal file
|
After Width: | Height: | Size: 4.2 KiB |
BIN
img/7.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
img/8.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
img/9.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
325
main.go
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"log/slog"
|
||||
"math"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/go-pdf/fpdf"
|
||||
)
|
||||
|
||||
const (
|
||||
imgDir = "./img"
|
||||
cardWidth = 55.0
|
||||
cardHeight = 85.0
|
||||
margin = 5.0
|
||||
dpiScale = 3.779528 // 96 DPI
|
||||
outputFileName = "dobble_cards.pdf"
|
||||
minScaleFactor = 0.7
|
||||
maxScaleFactor = 1.0
|
||||
)
|
||||
|
||||
type CardGenerator struct {
|
||||
TotalCards int
|
||||
ImagesPerCard int
|
||||
ImageFiles []string
|
||||
RoundCards bool
|
||||
}
|
||||
|
||||
func main() {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||
slog.SetDefault(logger)
|
||||
|
||||
cg, err := getInputAndInitialize()
|
||||
if err != nil {
|
||||
logger.Error("Initialization failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
cards := cg.generateCards()
|
||||
logger.Info("Cards generated", "count", len(cards))
|
||||
|
||||
if err := generatePDF(cards, cg.RoundCards); err != nil {
|
||||
logger.Error("PDF generation failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logger.Info("PDF successfully generated")
|
||||
}
|
||||
|
||||
func getInputAndInitialize() (*CardGenerator, error) {
|
||||
var totalCardsStr, imagesPerCardStr string
|
||||
var roundCards bool
|
||||
|
||||
form := huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().Title("Enter the total number of cards:").Value(&totalCardsStr),
|
||||
huh.NewInput().Title("Enter the number of images per card:").Value(&imagesPerCardStr),
|
||||
huh.NewConfirm().
|
||||
Title("Do you want round cards?").
|
||||
Value(&roundCards),
|
||||
),
|
||||
)
|
||||
|
||||
if err := form.Run(); err != nil {
|
||||
return nil, fmt.Errorf("form input failed: %w", err)
|
||||
}
|
||||
|
||||
totalCards, err1 := strconv.Atoi(totalCardsStr)
|
||||
imagesPerCard, err2 := strconv.Atoi(imagesPerCardStr)
|
||||
if err := errors.Join(err1, err2); err != nil {
|
||||
return nil, fmt.Errorf("invalid input: %w", err)
|
||||
}
|
||||
|
||||
cg := &CardGenerator{
|
||||
TotalCards: totalCards,
|
||||
ImagesPerCard: imagesPerCard,
|
||||
RoundCards: roundCards,
|
||||
}
|
||||
|
||||
if err := cg.loadImageFiles(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cg, nil
|
||||
}
|
||||
|
||||
func (cg *CardGenerator) generateCards() [][]string {
|
||||
n := cg.ImagesPerCard - 1
|
||||
totalCards := n*n + n + 1
|
||||
|
||||
if len(cg.ImageFiles) < totalCards {
|
||||
slog.Error("Not enough images for the given parameters",
|
||||
"required", totalCards,
|
||||
"available", len(cg.ImageFiles))
|
||||
return nil
|
||||
}
|
||||
|
||||
cards := cg.generateCardIndices(n)
|
||||
imageCards := cg.convertToImageCards(cards)
|
||||
cg.shuffleCards(imageCards)
|
||||
|
||||
return cg.limitCards(imageCards)
|
||||
}
|
||||
|
||||
func (cg *CardGenerator) generateCardIndices(n int) [][]int {
|
||||
cards := make([][]int, 0, n*n+n+1)
|
||||
|
||||
for i := 0; i < n+1; i++ {
|
||||
card := make([]int, cg.ImagesPerCard)
|
||||
card[0] = 1
|
||||
for j := 0; j < n; j++ {
|
||||
card[j+1] = (j + 1) + (i * n) + 1
|
||||
}
|
||||
cards = append(cards, card)
|
||||
}
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
for j := 0; j < n; j++ {
|
||||
card := make([]int, cg.ImagesPerCard)
|
||||
card[0] = i + 2
|
||||
for k := 0; k < n; k++ {
|
||||
card[k+1] = (n + 1 + n*k + (i*k+j)%n) + 1
|
||||
}
|
||||
cards = append(cards, card)
|
||||
}
|
||||
}
|
||||
|
||||
return cards
|
||||
}
|
||||
|
||||
func (cg *CardGenerator) convertToImageCards(cards [][]int) [][]string {
|
||||
imageCards := make([][]string, len(cards))
|
||||
for i, card := range cards {
|
||||
imageCards[i] = make([]string, len(card))
|
||||
for j, symbolIndex := range card {
|
||||
imageCards[i][j] = cg.ImageFiles[symbolIndex-1]
|
||||
}
|
||||
}
|
||||
return imageCards
|
||||
}
|
||||
|
||||
func (cg *CardGenerator) shuffleCards(cards [][]string) {
|
||||
rand.Shuffle(len(cards), func(i, j int) {
|
||||
cards[i], cards[j] = cards[j], cards[i]
|
||||
})
|
||||
|
||||
for i := range cards {
|
||||
rand.Shuffle(len(cards[i]), func(j, k int) {
|
||||
cards[i][j], cards[i][k] = cards[i][k], cards[i][j]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (cg *CardGenerator) limitCards(cards [][]string) [][]string {
|
||||
if cg.TotalCards < len(cards) {
|
||||
return cards[:cg.TotalCards]
|
||||
}
|
||||
return cards
|
||||
}
|
||||
|
||||
func (cg *CardGenerator) loadImageFiles() error {
|
||||
files, err := os.ReadDir(imgDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read image directory: %w", err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if !file.IsDir() && filepath.Ext(file.Name()) == ".png" {
|
||||
cg.ImageFiles = append(cg.ImageFiles, filepath.Join(imgDir, file.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
requiredImages := cg.calculateRequiredImages()
|
||||
|
||||
if len(cg.ImageFiles) < requiredImages {
|
||||
return fmt.Errorf("not enough images in the img folder: required %d, found %d", requiredImages, len(cg.ImageFiles))
|
||||
}
|
||||
|
||||
rand.Shuffle(len(cg.ImageFiles), func(i, j int) {
|
||||
cg.ImageFiles[i], cg.ImageFiles[j] = cg.ImageFiles[j], cg.ImageFiles[i]
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cg *CardGenerator) calculateRequiredImages() int {
|
||||
n := cg.ImagesPerCard - 1
|
||||
return n*n + n + 1
|
||||
}
|
||||
|
||||
func generatePDF(cards [][]string, roundCards bool) error {
|
||||
pdf := fpdf.New("P", "mm", "A4", "")
|
||||
pdf.SetAutoPageBreak(true, 10)
|
||||
|
||||
pageWidth, pageHeight, _ := pdf.PageSize(1)
|
||||
cardSize := math.Min(cardWidth, cardHeight)
|
||||
cardsPerRow := int((pageWidth - 2*margin) / (cardSize + margin))
|
||||
cardsPerCol := int((pageHeight - 2*margin) / (cardSize + margin))
|
||||
cardsPerPage := cardsPerRow * cardsPerCol
|
||||
|
||||
for i, card := range cards {
|
||||
if i%cardsPerPage == 0 {
|
||||
pdf.AddPage()
|
||||
}
|
||||
|
||||
col := i % cardsPerRow
|
||||
row := (i / cardsPerRow) % cardsPerCol
|
||||
|
||||
x := margin + float64(col)*(cardSize+margin)
|
||||
y := margin + float64(row)*(cardSize+margin)
|
||||
|
||||
slog.Info("Processing card", "index", i, "x", x, "y", y)
|
||||
|
||||
if roundCards {
|
||||
if err := processRoundCard(pdf, x, y, card); err != nil {
|
||||
return fmt.Errorf("failed to process round card %d: %w", i, err)
|
||||
}
|
||||
} else {
|
||||
if err := processSquareCard(pdf, x, y, card); err != nil {
|
||||
return fmt.Errorf("failed to process square card %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pdf.OutputFileAndClose(outputFileName)
|
||||
}
|
||||
|
||||
func processRoundCard(pdf *fpdf.Fpdf, x, y float64, card []string) error {
|
||||
diameter := math.Min(cardWidth, cardHeight)
|
||||
radius := diameter / 2
|
||||
|
||||
pdf.SetDrawColor(0, 0, 0)
|
||||
pdf.Circle(x+radius, y+radius, radius, "D")
|
||||
|
||||
availableRadius := radius - 5
|
||||
optimalImageSize := availableRadius * 2 / math.Sqrt(float64(len(card)))
|
||||
|
||||
for i, imgFile := range card {
|
||||
angle := 2 * math.Pi * float64(i) / float64(len(card))
|
||||
distanceFromCenter := availableRadius * 0.6
|
||||
|
||||
imgX := x + radius + distanceFromCenter*math.Cos(angle) - optimalImageSize/2
|
||||
imgY := y + radius + distanceFromCenter*math.Sin(angle) - optimalImageSize/2
|
||||
|
||||
if err := processImage(pdf, imgFile, imgX, imgY, optimalImageSize); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func processSquareCard(pdf *fpdf.Fpdf, x, y float64, card []string) error {
|
||||
pdf.Rect(x, y, cardWidth, cardHeight, "D")
|
||||
|
||||
availableWidth := cardWidth - 10
|
||||
availableHeight := cardHeight - 10
|
||||
optimalImageSize := math.Min(availableWidth/2, availableHeight/float64(len(card)))
|
||||
|
||||
for i, imgFile := range card {
|
||||
imgX := x + 5 + rand.Float64()*(availableWidth-optimalImageSize)
|
||||
imgY := y + 5 + float64(i)*(availableHeight/float64(len(card))) + rand.Float64()*(availableHeight/float64(len(card))-optimalImageSize)
|
||||
|
||||
if err := processImage(pdf, imgFile, imgX, imgY, optimalImageSize); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func processImage(pdf *fpdf.Fpdf, imgFile string, x, y, size float64) error {
|
||||
file, err := os.Open(imgFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open image file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode image: %w", err)
|
||||
}
|
||||
|
||||
scaleFactor := minScaleFactor + rand.Float64()*(maxScaleFactor-minScaleFactor)
|
||||
imgSize := size * scaleFactor
|
||||
targetSize := uint(imgSize * dpiScale)
|
||||
|
||||
img = imaging.Fit(img, int(targetSize), int(targetSize), imaging.Lanczos)
|
||||
|
||||
rotation := rand.Intn(4) * 90
|
||||
rotatedImg := imaging.Rotate(img, float64(rotation), color.Transparent)
|
||||
|
||||
tmpFile, err := os.CreateTemp("", "processed_*.png")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
|
||||
if err := png.Encode(tmpFile, rotatedImg); err != nil {
|
||||
return fmt.Errorf("failed to encode processed image: %w", err)
|
||||
}
|
||||
tmpFile.Close()
|
||||
|
||||
pdf.ImageOptions(
|
||||
tmpFile.Name(),
|
||||
x, y,
|
||||
imgSize, imgSize,
|
||||
false,
|
||||
fpdf.ImageOptions{ImageType: "PNG"},
|
||||
0,
|
||||
"",
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||