commit 4d134422afc0d2098eecc932e8843bf81f0a44f7 Author: Patryk Hegenberg Date: Wed Jul 3 20:06:54 2024 +0200 initial commit diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..13f6d75 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..53c8793 --- /dev/null +++ b/go.sum @@ -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= diff --git a/img/1.png b/img/1.png new file mode 100644 index 0000000..59012c5 Binary files /dev/null and b/img/1.png differ diff --git a/img/10.png b/img/10.png new file mode 100644 index 0000000..d54a68e Binary files /dev/null and b/img/10.png differ diff --git a/img/11.png b/img/11.png new file mode 100644 index 0000000..019cdb0 Binary files /dev/null and b/img/11.png differ diff --git a/img/12.png b/img/12.png new file mode 100644 index 0000000..f00491a Binary files /dev/null and b/img/12.png differ diff --git a/img/13.png b/img/13.png new file mode 100644 index 0000000..78e7fc9 Binary files /dev/null and b/img/13.png differ diff --git a/img/14.png b/img/14.png new file mode 100644 index 0000000..271352a Binary files /dev/null and b/img/14.png differ diff --git a/img/2.png b/img/2.png new file mode 100644 index 0000000..587d43e Binary files /dev/null and b/img/2.png differ diff --git a/img/3.png b/img/3.png new file mode 100644 index 0000000..d2c817c Binary files /dev/null and b/img/3.png differ diff --git a/img/4.png b/img/4.png new file mode 100644 index 0000000..830b670 Binary files /dev/null and b/img/4.png differ diff --git a/img/5.png b/img/5.png new file mode 100644 index 0000000..c5e879d Binary files /dev/null and b/img/5.png differ diff --git a/img/6.png b/img/6.png new file mode 100644 index 0000000..1d43baf Binary files /dev/null and b/img/6.png differ diff --git a/img/7.png b/img/7.png new file mode 100644 index 0000000..3280752 Binary files /dev/null and b/img/7.png differ diff --git a/img/8.png b/img/8.png new file mode 100644 index 0000000..c7d8a22 Binary files /dev/null and b/img/8.png differ diff --git a/img/9.png b/img/9.png new file mode 100644 index 0000000..0783a41 Binary files /dev/null and b/img/9.png differ diff --git a/main.go b/main.go new file mode 100644 index 0000000..c761c85 --- /dev/null +++ b/main.go @@ -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 +}