├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ └── release.yaml ├── Dockerfile ├── docs └── testpdf.png ├── .gitignore ├── main.go ├── go.mod ├── pkg └── helpers │ └── strings.go ├── cmd ├── root.go ├── recover.go └── generate.go ├── LICENSE ├── README.md ├── go.sum └── .goreleaser.yaml /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @techwolf12 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | COPY qrkey /usr/bin/qrkey 3 | ENTRYPOINT ["/usr/bin/qrkey"] -------------------------------------------------------------------------------- /docs/testpdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Techwolf12/qrkey/HEAD/docs/testpdf.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pdf 2 | *.txt 3 | qrkey 4 | .idea/ 5 | # Added by goreleaser init: 6 | dist/ 7 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright © 2025 Christiaan de Die le Clercq 3 | 4 | */ 5 | package main 6 | 7 | import "github.com/techwolf12/qrkey/cmd" 8 | 9 | func main() { 10 | cmd.Execute() 11 | } 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/techwolf12/qrkey 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/jung-kurt/gofpdf v1.16.2 7 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e 8 | github.com/spf13/cobra v1.10.1 9 | ) 10 | 11 | require ( 12 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 13 | github.com/spf13/pflag v1.0.9 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /pkg/helpers/strings.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | func SplitString(s string, chunkSize int) []string { 9 | var chunks []string 10 | for i := 0; i < len(s); i += chunkSize { 11 | end := i + chunkSize 12 | if end > len(s) { 13 | end = len(s) 14 | } 15 | chunks = append(chunks, s[i:end]) 16 | } 17 | return chunks 18 | } 19 | 20 | func FlagLookup(cmd *cobra.Command, flagName string) (string, error) { 21 | flag := cmd.Flags().Lookup(flagName) 22 | 23 | if flag != nil && flag.Value != nil { 24 | return flag.Value.String(), nil 25 | } 26 | 27 | return "", fmt.Errorf("flag '%s' not found", flagName) 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: read 10 | packages: write 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v5 17 | with: 18 | fetch-depth: 0 19 | - uses: actions/setup-go@v6 20 | with: 21 | go-version: 1.24 22 | cache: true 23 | - run: go mod tidy 24 | - uses: docker/login-action@v3 25 | with: 26 | registry: ghcr.io 27 | username: ${{ github.repository_owner }} 28 | password: ${{ secrets.GH_PAT }} 29 | - uses: goreleaser/goreleaser-action@v6 30 | if: success() && startsWith(github.ref, 'refs/tags/') 31 | with: 32 | version: latest 33 | args: release 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GH_PAT }} 36 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | type Metadata struct { 10 | Filename string `json:"filename"` 11 | SHA256 string `json:"sha256"` 12 | QRCount int `json:"qr_count"` 13 | } 14 | 15 | // rootCmd represents the base command when called without any subcommands 16 | var rootCmd = &cobra.Command{ 17 | Use: "qrkey", 18 | Short: "qrkey is a command-line tool for generating and recovering QR codes from files for offline private key backup.", 19 | Long: `qrkey is a command-line tool for generating and recovering QR codes from files for offline private key backup. 20 | It allows you to convert files into QR codes that can be printed or stored, and later recovered from those QR codes. 21 | It supports large files by splitting them into multiple QR codes, and includes metadata for easy recovery.`, 22 | } 23 | 24 | func Execute() { 25 | err := rootCmd.Execute() 26 | if err != nil { 27 | os.Exit(1) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2025 Christiaan de Die le Clercq 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | QRKey 2 | ==== 3 | 4 | ![QR code example](https://github.com/techwolf12/qrkey/raw/main/docs/testpdf.png "QR example") 5 | 6 | `qrkey` is a command-line tool for generating and recovering QR codes from files for offline private key backup. 7 | It allows you to convert files into QR codes that can be printed or stored, and later recovered from those QR codes. 8 | It supports large files by splitting them into multiple QR codes, and includes metadata for easy recovery and validation. 9 | 10 | * Convert a file into a PDF with QR codes 11 | * Recover from a PDF with QR codes with a barcode scanner 12 | * Recover from a PDF with QR codes from a file with lines 13 | 14 | ## Installation 15 | 16 | macOS users can install `qrkey` using Homebrew Tap: 17 | 18 | ```bash 19 | brew tap techwolf12/tap 20 | brew install techwolf12/tap/qrkey 21 | ``` 22 | 23 | For Docker users, you can use the Docker image: 24 | 25 | ```bash 26 | docker run -v "$(pwd)":/mnt ghcr.io/techwolf12/qrkey:latest generate --in /mnt/testfile.txt --out /mnt/test.pdf 27 | ``` 28 | 29 | For other systems, see the [releases page](https://github.com/Techwolf12/qrkey/releases/). 30 | 31 | ## Usage 32 | To generate a QR code from a file, use the following command: 33 | 34 | ```bash 35 | qrkey generate --in --out file.pdf 36 | ``` 37 | 38 | To recover a file from QR codes, use the following command: 39 | 40 | ```bash 41 | qrkey recover --in 42 | ``` 43 | 44 | Or to recover interactively: 45 | 46 | ```bash 47 | qrkey recover 48 | ``` 49 | 50 | ## License 51 | 52 | See [`LICENSE`](./LICENSE). -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 5 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 6 | github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= 7 | github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc= 8 | github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0= 9 | github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= 10 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 11 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 12 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 13 | github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= 14 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= 15 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= 16 | github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= 17 | github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= 18 | github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= 19 | github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 20 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 21 | golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 22 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 23 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 24 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 25 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 2 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 3 | 4 | version: 2 5 | 6 | before: 7 | hooks: 8 | - go mod tidy 9 | 10 | builds: 11 | - env: 12 | - CGO_ENABLED=0 13 | goos: 14 | - linux 15 | - windows 16 | - darwin 17 | 18 | archives: 19 | - formats: [tar.gz] 20 | name_template: >- 21 | {{ .ProjectName }}_ 22 | {{- title .Os }}_ 23 | {{- if eq .Arch "amd64" }}x86_64 24 | {{- else if eq .Arch "386" }}i386 25 | {{- else }}{{ .Arch }}{{ end }} 26 | {{- if .Arm }}v{{ .Arm }}{{ end }} 27 | format_overrides: 28 | - goos: windows 29 | formats: [zip] 30 | 31 | changelog: 32 | sort: asc 33 | filters: 34 | exclude: 35 | - "^docs:" 36 | - "^test:" 37 | 38 | release: 39 | footer: >- 40 | 41 | --- 42 | 43 | Released by [GoReleaser](https://github.com/goreleaser/goreleaser). 44 | 45 | # Creates Linux packages. 46 | nfpms: 47 | - file_name_template: "{{ .ConventionalFileName }}" 48 | maintainer: Christiaan de Die le Clercq 49 | formats: 50 | - deb 51 | - apk 52 | - rpm 53 | 54 | # Creates Darwin universal binaries. 55 | universal_binaries: 56 | - replace: true 57 | 58 | dockers: 59 | - image_templates: ["ghcr.io/techwolf12/qrkey:{{ .Version }}"] 60 | dockerfile: Dockerfile 61 | build_flag_templates: 62 | - --label=org.opencontainers.image.title=qrkey 63 | - --label=org.opencontainers.image.description={{ .ProjectName }} 64 | - --label=org.opencontainers.image.url=https://github.com/techwolf12/qrkey 65 | - --label=org.opencontainers.image.source=https://github.com/techwolf12/qrkey 66 | - --label=org.opencontainers.image.version={{ .Version }} 67 | - --label=org.opencontainers.image.revision={{ .FullCommit }} 68 | - --label=org.opencontainers.image.licenses=MIT 69 | 70 | # Sets up homebrew-taps. 71 | brews: 72 | - repository: 73 | owner: techwolf12 74 | name: homebrew-tap 75 | token: "{{ .Env.GITHUB_TOKEN }}" 76 | directory: Formula 77 | homepage: https://github.com/techwolf12/qrkey 78 | description: qrkey is a command-line tool for generating and recovering QR codes from files for offline private key backup. 79 | license: MIT -------------------------------------------------------------------------------- /cmd/recover.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/techwolf12/qrkey/pkg/helpers" 6 | 7 | "github.com/spf13/cobra" 8 | 9 | "bufio" 10 | "crypto/sha256" 11 | "encoding/base64" 12 | "encoding/json" 13 | "io" 14 | "os" 15 | "strings" 16 | ) 17 | 18 | func recoverFromQR(cmd *cobra.Command, args []string) { 19 | inputFile, err := helpers.FlagLookup(cmd, "in") 20 | if err != nil { 21 | panic(err) 22 | } 23 | 24 | var lines []string 25 | 26 | if inputFile != "" { 27 | lines, err = readLinesFromFile(inputFile) 28 | if err != nil { 29 | panic(err) 30 | } 31 | } else { 32 | lines, err = readLinesFromStdin() 33 | if err != nil { 34 | panic(err) 35 | } 36 | } 37 | 38 | if len(lines) == 0 { 39 | fmt.Println("No input lines found.") 40 | os.Exit(1) 41 | } 42 | 43 | var meta Metadata 44 | err = json.Unmarshal([]byte(lines[0]), &meta) 45 | if err != nil { 46 | fmt.Println("First line is not valid metadata JSON.") 47 | os.Exit(1) 48 | } 49 | 50 | fmt.Printf("Filename: %s\nSHA256: %s\nQR codes: %d\n", meta.Filename, meta.SHA256, meta.QRCount) 51 | 52 | for len(lines) < meta.QRCount { 53 | fmt.Printf("Enter QR code %d/%d: ", len(lines)+1, meta.QRCount) 54 | reader := bufio.NewReader(os.Stdin) 55 | l, _ := reader.ReadString('\n') 56 | lines = append(lines, strings.TrimSpace(l)) 57 | } 58 | 59 | b64 := strings.Join(lines[1:meta.QRCount], "") 60 | 61 | data, err := base64.StdEncoding.DecodeString(b64) 62 | if err != nil { 63 | fmt.Println("Base64 decode error:", err) 64 | os.Exit(1) 65 | } 66 | 67 | hash := sha256.Sum256(data) 68 | hashStr := fmt.Sprintf("%x", hash[:]) 69 | if hashStr != meta.SHA256 { 70 | fmt.Println("SHA256 mismatch! File may be corrupted.") 71 | } 72 | 73 | err = os.WriteFile(meta.Filename, data, 0644) 74 | if err != nil { 75 | fmt.Println("Error writing file:", err) 76 | os.Exit(1) 77 | } 78 | fmt.Printf("File written: %s\n", meta.Filename) 79 | } 80 | 81 | func readLinesFromFile(filename string) ([]string, error) { 82 | f, err := os.Open(filename) 83 | if err != nil { 84 | return nil, err 85 | } 86 | defer f.Close() 87 | return readLines(f) 88 | } 89 | 90 | func readLinesFromStdin() ([]string, error) { 91 | fmt.Println("Paste each QR code content (one per line, metadata first) and press ctrl-D when finished:") 92 | return readLines(os.Stdin) 93 | } 94 | 95 | func readLines(r io.Reader) ([]string, error) { 96 | var lines []string 97 | scanner := bufio.NewScanner(r) 98 | for scanner.Scan() { 99 | line := strings.TrimSpace(scanner.Text()) 100 | if line != "" { 101 | lines = append(lines, line) 102 | } 103 | } 104 | return lines, scanner.Err() 105 | } 106 | 107 | var recoverCmd = &cobra.Command{ 108 | Use: "recover", 109 | Short: "Recover data from QR codes", 110 | Long: `Recover data from QR codes generated by the qrkey tool. 111 | This command reads QR codes from a file or stdin, reconstructs the base64 data. 112 | It verifies the SHA256 checksum and writes the original file to disk.`, 113 | Example: `qrkey recover --in qr_codes.txt`, 114 | Run: recoverFromQR, 115 | } 116 | 117 | func init() { 118 | rootCmd.AddCommand(recoverCmd) 119 | 120 | recoverCmd.Flags().StringP("in", "i", "", "Input file with one line per QR (optional)") 121 | err := recoverCmd.MarkFlagFilename("in", "*") 122 | if err != nil { 123 | panic(err) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /cmd/generate.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "github.com/techwolf12/qrkey/pkg/helpers" 6 | 7 | "crypto/sha256" 8 | "encoding/base64" 9 | "encoding/json" 10 | "github.com/jung-kurt/gofpdf" 11 | "github.com/skip2/go-qrcode" 12 | "github.com/spf13/cobra" 13 | "math" 14 | "os" 15 | "path/filepath" 16 | ) 17 | 18 | const ( 19 | qrChunkSize = 800 // bytes per QR code (safe for QR version 10, error correction M) 20 | qrSize = 100 // px, size of each QR code in the PDF 21 | gridCols = 4 // QR codes per row 22 | gridRows = 5 // QR codes per column 23 | ) 24 | 25 | func generateQR(cmd *cobra.Command, args []string) { 26 | inputFile, err := helpers.FlagLookup(cmd, "in") 27 | if err != nil { 28 | panic(err) 29 | } 30 | outputPDF, err := helpers.FlagLookup(cmd, "out") 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | data, err := os.ReadFile(inputFile) 36 | if err != nil { 37 | panic(err) 38 | } 39 | 40 | b64 := base64.StdEncoding.EncodeToString(data) 41 | 42 | hash := sha256.Sum256(data) 43 | hashStr := fmt.Sprintf("%x", hash[:]) 44 | 45 | chunks := helpers.SplitString(b64, qrChunkSize) 46 | qrCount := len(chunks) + 1 // +1 for metadata QR 47 | 48 | meta := Metadata{ 49 | Filename: filepath.Base(inputFile), 50 | SHA256: hashStr, 51 | QRCount: qrCount, 52 | } 53 | metaBytes, _ := json.Marshal(meta) 54 | metaQR, err := qrcode.New(string(metaBytes), qrcode.Medium) 55 | if err != nil { 56 | panic(err) 57 | } 58 | 59 | qrCodes := make([][]byte, 0, qrCount) 60 | metaPNG, _ := metaQR.PNG(qrSize) 61 | qrCodes = append(qrCodes, metaPNG) 62 | for _, chunk := range chunks { 63 | qr, err := qrcode.New(chunk, qrcode.Medium) 64 | if err != nil { 65 | panic(err) 66 | } 67 | pngData, _ := qr.PNG(qrSize) 68 | qrCodes = append(qrCodes, pngData) 69 | } 70 | 71 | // PDF 72 | pdf := gofpdf.New("P", "mm", "A4", "") 73 | pageW, pageH := pdf.GetPageSize() 74 | margin := 10.0 75 | usableW := pageW - 2*margin 76 | usableH := pageH - 2*margin - 20 // 20mm for title 77 | 78 | cellW := usableW / float64(gridCols) 79 | cellH := usableH / float64(gridRows) 80 | 81 | perPage := gridCols * gridRows 82 | totalPages := int(math.Ceil(float64(qrCount) / float64(perPage))) 83 | 84 | for page := 0; page < totalPages; page++ { 85 | pdf.AddPage() 86 | // Title 87 | pdf.SetFont("Arial", "B", 16) 88 | pdf.CellFormat(0, 10, fmt.Sprintf("File: %s", filepath.Base(inputFile)), "", 1, "C", false, 0, "") 89 | pdf.SetFont("Arial", "", 12) 90 | pdf.CellFormat(0, 10, fmt.Sprintf("Page %d / %d, made with https://github.com/techwolf12/qrkey", page+1, totalPages), "", 1, "C", false, 0, "") 91 | 92 | // QR grid 93 | for i := 0; i < perPage; i++ { 94 | idx := page*perPage + i 95 | if idx >= len(qrCodes) { 96 | break 97 | } 98 | x := margin + float64(i%gridCols)*cellW 99 | y := margin + 20 + float64(i/gridCols)*cellH 100 | 101 | // Write QR image 102 | imgOpt := gofpdf.ImageOptions{ImageType: "PNG", ReadDpi: false} 103 | // Save temp file for gofpdf 104 | tmpFile := fmt.Sprintf("tmp_qr_%d.png", idx) 105 | err := os.WriteFile(tmpFile, qrCodes[idx], 0644) 106 | if err != nil { 107 | panic(err) 108 | } 109 | pdf.ImageOptions(tmpFile, x, y, cellW, cellH, false, imgOpt, 0, "") 110 | err = os.Remove(tmpFile) 111 | if err != nil { 112 | panic(err) 113 | } 114 | } 115 | } 116 | 117 | err = pdf.OutputFileAndClose(outputPDF) 118 | if err != nil { 119 | panic(err) 120 | } 121 | fmt.Printf("PDF created: %s\n", outputPDF) 122 | } 123 | 124 | var generateCmd = &cobra.Command{ 125 | Use: "generate", 126 | Short: "Generate a PDF with QR codes from a file", 127 | Long: `Generate a PDF containing QR codes representing the contents of a file. 128 | Each QR code will contain a chunk of the file's base64-encoded content, along with metadata about the file. 129 | The first QR code will contain metadata including the filename, SHA256 hash, and total number of QR codes.`, 130 | Example: `qrkey generate --in myfile.txt --out myfile.pdf`, 131 | Run: generateQR, 132 | } 133 | 134 | func init() { 135 | rootCmd.AddCommand(generateCmd) 136 | 137 | generateCmd.Flags().StringP("in", "i", "", "Input file (required)") 138 | err := generateCmd.MarkFlagRequired("in") 139 | if err != nil { 140 | panic(err) 141 | } 142 | err = generateCmd.MarkFlagFilename("in", "*") 143 | if err != nil { 144 | panic(err) 145 | } 146 | 147 | generateCmd.Flags().StringP("out", "o", "", "Output PDF file (required)") 148 | err = generateCmd.MarkFlagRequired("out") 149 | if err != nil { 150 | panic(err) 151 | } 152 | } 153 | --------------------------------------------------------------------------------