diff --git a/README.md b/README.md
index 1d1866856..73a212370 100644
--- a/README.md
+++ b/README.md
@@ -72,8 +72,6 @@ irm https://raw.githubusercontent.com/OpenDataEnsemble/ode/main/scripts/install-
curl -fsSL https://raw.githubusercontent.com/OpenDataEnsemble/ode/main/scripts/install-synkronus-cli.sh | bash
```
-**License note:** this component is currently **GPL-2.0-or-later** (see [synkronus-cli/LICENSE](synkronus-cli/LICENSE)) while the QR PNG stack depends on GPL-classified libraries. We plan to swap in a stdlib-only renderer and return the CLI to **MIT**. See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
-
### **synkronus-portal**
A web-based admin interface for Synkronus. It covers the same tasks as **synkronus-cli** in the browser.
@@ -150,7 +148,7 @@ CI will **fail** if:
## License
-The repository root is [MIT](./LICENSE). **synkronus-cli** is **GPL-2.0-or-later** until the QR dependency cleanup above is done. Third-party and mixed-component details: [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md).
+The repository is licensed under the [MIT License](./LICENSE). Third-party and component details: [THIRD_PARTY_NOTICES.md](./THIRD_PARTY_NOTICES.md).
## Get Involved
diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md
index b517b1178..f5d26e122 100644
--- a/THIRD_PARTY_NOTICES.md
+++ b/THIRD_PARTY_NOTICES.md
@@ -1,6 +1,6 @@
# Third-party notices
-Open Data Ensemble (ODE) primarily uses the [MIT License](./LICENSE) at the repository root. **`synkronus-cli`** is an exception: it is **GPL-2.0-or-later** until the QR stack is refactored (see below). This file summarizes **other** separately licensed open-source components and points to **SBOMs** for a full machine-readable list.
+Open Data Ensemble (ODE) ships with **your own code** under the [MIT License](./LICENSE). This file summarizes **separately licensed open-source components** that may be included when you build and distribute ODE software, and points to where to find a **machine-readable** full list.
> **This is not legal advice.** For distribution (especially mobile apps and CLI binaries), have your process reviewed against your policies.
@@ -33,24 +33,13 @@ Production JavaScript dependency licenses are **overwhelmingly permissive** (e.g
`synkronus` and `synkronus-cli` SBOMs (`synkronus.cdx.json`, `synkronus-cli.cdx.json`) list modules and detected licenses.
-### `synkronus-cli` license (GPL-2.0-or-later)
+### `synkronus-cli` licensing
-The **synkronus-cli** component is explicitly licensed under **GNU GPL v2 or
-later**; see [`synkronus-cli/LICENSE`](./synkronus-cli/LICENSE). That aligns with
-the current QR PNG stack:
+The **synkronus-cli** component is licensed under the **MIT License**; see [`synkronus-cli/LICENSE`](./synkronus-cli/LICENSE).
-**`yeqown/go-qrcode/writer/standard`** → **`github.com/fogleman/gg`** →
-**`github.com/golang/freetype`** (detectors report **GPL-2.0-or-later**).
+The QR PNG generation path uses a custom standard library-based `qrcode.Writer` (`image`, `image/draw`, `image/png`), avoiding copyleft/GPL dependency chains (`fogleman/gg` / `golang/freetype`).
-The rest of the monorepo may remain under other licenses (e.g. MIT at the
-repo root).
-
-**Follow-up:** replace `writer/standard` with a **stdlib-only** `qrcode.Writer`,
-drop `gg` / freetype from the module graph, then **re-license synkronus-cli
-to MIT** if desired. Ticket-ready notes:
-[`synkronus-cli/FOLLOWUP-custom-qrcode-writer.md`](./synkronus-cli/FOLLOWUP-custom-qrcode-writer.md).
-
-`synkronus` server SBOMs checked in this audit did **not** show GPL/AGPL/LGPL identifiers in CycloneDX output; still rely on the SBOM for each release.
+`synkronus` and `synkronus-cli` SBOMs checked in this audit did **not** show GPL/AGPL/LGPL identifiers in CycloneDX output; still rely on the SBOM for each release.
## Vendored native sources (Formulus / Android)
diff --git a/docs/docs/reference/synkronus-cli.md b/docs/docs/reference/synkronus-cli.md
index 8a0c279ef..30cccec32 100644
--- a/docs/docs/reference/synkronus-cli.md
+++ b/docs/docs/reference/synkronus-cli.md
@@ -659,7 +659,7 @@ synk: command not found
## License
-The Synkronus CLI is currently **GPL-2.0-or-later** while it depends on GPL-classified QR code libraries. The plan is to swap in stdlib-only rendering and return to MIT license. See `synkronus-cli/FOLLOWUP-custom-qrcode-writer.md` and `THIRD_PARTY_NOTICES.md` for details.
+The Synkronus CLI is licensed under the **MIT License**. See `synkronus-cli/LICENSE` and `THIRD_PARTY_NOTICES.md` for details.
## Related Content
diff --git a/synkronus-cli/AGENTS.md b/synkronus-cli/AGENTS.md
index a449ed846..1a12d107e 100644
--- a/synkronus-cli/AGENTS.md
+++ b/synkronus-cli/AGENTS.md
@@ -23,7 +23,7 @@
## License note
-The CLI may be **GPL-2.0-or-later** until the QR dependency cleanup described in the root [README.md](../README.md) and [FOLLOWUP-custom-qrcode-writer.md](FOLLOWUP-custom-qrcode-writer.md). **Do not** change license without maintainer intent.
+The CLI is licensed under **MIT** (see [`LICENSE`](LICENSE)).
---
diff --git a/synkronus-cli/LICENSE b/synkronus-cli/LICENSE
index 9341e9649..95674de4a 100644
--- a/synkronus-cli/LICENSE
+++ b/synkronus-cli/LICENSE
@@ -1,354 +1,21 @@
-Synkronus CLI — project licensing
-Copyright (C) 2026 Open Data Ensemble
-
-The **synkronus-cli** program in this repository is licensed under the
-GNU General Public License **version 2 or later** (GPL-2.0-or-later), in
-part because current releases link material that is offered under
-GPL-2.0-or-later (see `THIRD_PARTY_NOTICES.md` at the repository root and
-release SBOMs).
-
-Other directories in the same monorepo may remain under different
-licenses (e.g. MIT). This file applies to **synkronus-cli** as a component.
-
-SPDX-License-Identifier: GPL-2.0-or-later
-
-------------------------------------------------------------------------
-
- GNU GENERAL PUBLIC LICENSE
- Version 2, June 1991
-
- Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
-
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The licenses for most software are designed to take away your
-freedom to share and change it. By contrast, the GNU General Public
-License is intended to guarantee your freedom to share and change free
-software--to make sure the software is free for all its users. This
-General Public License applies to most of the Free Software
-Foundation's software and to any other program whose authors commit to
-using it. (Some other Free Software Foundation software is covered by
-the GNU Lesser General Public License instead.) You can apply it to
-your programs, too.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-this service if you wish), that you receive source code or can get it
-if you want it, that you can change the software or use pieces of it
-in new free programs; and that you know you can do these things.
-
- To protect your rights, we need to make restrictions that forbid
-anyone to deny you these rights or to ask you to surrender the rights.
-These restrictions translate to certain responsibilities for you if you
-distribute copies of the software, or if you modify it.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must give the recipients all the rights that
-you have. You must make sure that they, too, receive or can get the
-source code. And you must show them these terms so they know their
-rights.
-
- We protect your rights with two steps: (1) copyright the software, and
-(2) offer you this license which gives you legal permission to copy,
-distribute and/or modify the software.
-
- Also, for each author's protection and ours, we want to make certain
-that everyone understands that there is no warranty for this free
-software. If the software is modified by someone else and passed on, we
-want its recipients to know that what they have is not the original, so
-that any problems introduced by others will not reflect on the original
-authors' reputations.
-
- Finally, any free program is threatened constantly by software
-patents. We wish to avoid the danger that redistributors of a free
-program will individually obtain patent licenses, in effect making the
-program proprietary. To prevent this, we have made it clear that any
-patent must be licensed for everyone's free use or not licensed at all.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- GNU GENERAL PUBLIC LICENSE
- TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
-
- 0. This License applies to any program or other work which contains
-a notice placed by the copyright holder saying it may be distributed
-under the terms of this General Public License. The "Program", below,
-refers to any such program or work, and a "work based on the Program"
-means either the Program or any derivative work under copyright law:
-that is to say, a work containing the Program or a portion of it,
-either verbatim or with modifications and/or translated into another
-language. (Hereinafter, translation is included without limitation in
-the term "modification".) Each licensee is addressed as "you".
-
-Activities other than copying, distribution and modification are not
-covered by this License; they are outside its scope. The act of
-running the Program is not restricted, and the output from the Program
-is covered only if its contents constitute a work based on the
-Program (independent of having been made by running the Program).
-Whether that is true depends on what the Program does.
-
- 1. You may copy and distribute verbatim copies of the Program's
-source code as you receive it, in any medium, provided that you
-conspicuously and appropriately publish on each copy an appropriate
-copyright notice and disclaimer of warranty; keep intact all the
-notices that refer to this License and to the absence of any warranty;
-and give any other recipients of the Program a copy of this License
-along with the Program.
-
-You may charge a fee for the physical act of transferring a copy, and
-you may at your option offer warranty protection in exchange for a fee.
-
- 2. You may modify your copy or copies of the Program or any portion
-of it, thus forming a work based on the Program, and copy and
-distribute such modifications or work under the terms of Section 1
-above, provided that you also meet all of these conditions:
-
- a) You must cause the modified files to carry prominent notices
- stating that you changed the files and the date of any change.
-
- b) You must cause any work that you distribute or publish, that in
- whole or in part contains or is derived from the Program or any
- part thereof, to be licensed as a whole at no charge to all third
- parties under the terms of this License.
-
- c) If the modified program normally reads commands interactively
- when run, you must cause it, when started running for such
- interactive use in the most ordinary way, to print or display an
- announcement including an appropriate copyright notice and a
- notice that there is no warranty (or else, saying that you provide
- a warranty) and that users may redistribute the program under
- these conditions, and telling the user how to view a copy of this
- License. (Exception: if the Program itself is interactive but
- does not normally print such an announcement, your work based on
- the Program is not required to print an announcement.)
-
-These requirements apply to the modified work as a whole. If
-identifiable sections of that work are not derived from the Program,
-and can be reasonably considered independent and separate works in
-themselves, then this License, and its terms, do not apply to those
-sections when you distribute them as separate works. But when you
-distribute the same sections as part of a whole which is a work based
-on the Program, the distribution of the whole must be on the terms of
-this License, whose permissions for other licensees extend to the
-entire whole, and thus to each and every part regardless of who wrote it.
-
-Thus, it is not the intent of this section to claim rights or contest
-your rights to work written entirely by you; rather, the intent is to
-exercise the right to control the distribution of derivative or
-collective works based on the Program.
-
-In addition, mere aggregation of another work not based on the Program
-with the Program (or with a work based on the Program) on a volume of
-a storage or distribution medium does not bring the other work under
-the scope of this License.
-
- 3. You may copy and distribute the Program (or a work based on it,
-under Section 2) in object code or executable form under the terms of
-Sections 1 and 2 above provided that you also do one of the following:
-
- a) Accompany it with the complete corresponding machine-readable
- source code, which must be distributed under the terms of Sections
- 1 and 2 above on a medium customarily used for software interchange; or,
-
- b) Accompany it with a written offer, valid for at least three
- years, to give any third party, for a charge no more than your
- cost of physically performing source distribution, a complete
- machine-readable copy of the corresponding source code, to be
- distributed under the terms of Sections 1 and 2 above on a medium
- customarily used for software interchange; or,
-
- c) Accompany it with the information you received as to the offer
- to distribute corresponding source code. (This alternative is
- allowed only for noncommercial distribution and only if you
- received the program in object code or executable form with such
- an offer, in accord with Subsection b above.)
-
-The source code for a work means the preferred form of the work for
-making modifications to it. For an executable work, complete source
-code means all the source code for all modules it contains, plus any
-associated interface definition files, plus the scripts used to
-control compilation and installation of the executable. However, as a
-special exception, the source code distributed need not include
-anything that is normally distributed (in either source or binary
-form) with the major components (compiler, kernel, and so on) of the
-operating system on which the executable runs, unless that component
-itself accompanies the executable.
-
-If distribution of executable or object code is made by offering
-access to copy from a designated place, then offering equivalent
-access to copy the source code from the same place counts as
-distribution of the source code, even though third parties are not
-compelled to copy the source along with the object code.
-
- 4. You may not copy, modify, sublicense, or distribute the Program
-except as expressly provided under this License. Any attempt
-otherwise to copy, modify, sublicense or distribute the Program is
-void, and will automatically terminate your rights under this License.
-However, parties who have received copies, or rights, from you under
-this License will not have their licenses terminated so long as such
-parties remain in full compliance.
-
- 5. You are not required to accept this License, since you have not
-signed it. However, nothing else grants you permission to modify or
-distribute the Program or its derivative works. These actions are
-prohibited by law if you do not accept this License. Therefore, by
-modifying or distributing the Program (or any work based on the
-Program), you indicate your acceptance of this License to do so, and
-all its terms and conditions for copying, distributing or modifying
-the Program or works based on it.
-
- 6. Each time you redistribute the Program (or any work based on the
-Program), the recipient automatically receives a license from the
-original licensor to copy, distribute or modify the Program subject to
-these terms and conditions. You may not impose any further
-restrictions on the recipients' exercise of the rights granted herein.
-You are not responsible for enforcing compliance by third parties to
-this License.
-
- 7. If, as a consequence of a court judgment or allegation of patent
-infringement or for any other reason (not limited to patent issues),
-conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot
-distribute so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you
-may not distribute the Program at all. For example, if a patent
-license would not permit royalty-free redistribution of the Program by
-all those who receive copies directly or indirectly through you, then
-the only way you could satisfy both it and this License would be to
-refrain entirely from distribution of the Program.
-
-If any portion of this section is held invalid or unenforceable under
-any particular circumstance, the balance of the section is intended to
-apply and the section as a whole is intended to apply in other
-circumstances.
-
-It is not the purpose of this section to induce you to infringe any
-patents or other property right claims or to contest validity of any
-such claims; this section has the sole purpose of protecting the
-integrity of the free software distribution system, which is
-implemented by public license practices. Many people have made
-generous contributions to the wide range of software distributed
-through that system in reliance on consistent application of that
-system; it is up to the author/donor to decide if he or she is willing
-to distribute software through any other system and a licensee cannot
-impose that choice.
-
-This section is intended to make thoroughly clear what is believed to
-be a consequence of the rest of this License.
-
- 8. If the distribution and/or use of the Program is restricted in
-certain countries either by patents or by copyrighted interfaces, the
-original copyright holder who places the Program under this License
-may add an explicit geographical distribution limitation excluding
-those countries, so that distribution is permitted only in or among
-countries not thus excluded. In such case, this License incorporates
-the limitation as if written in the body of this License.
-
- 9. The Free Software Foundation may publish revised and/or new versions
-of the General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
-Each version is given a distinguishing version number. If the Program
-specifies a version number of this License which applies to it and "any
-later version", you have the option of following the terms and conditions
-either of that version or of any later version published by the Free
-Software Foundation. If the Program does not specify a version number of
-this License, you may choose any version ever published by the Free Software
-Foundation.
-
- 10. If you wish to incorporate parts of the Program into other free
-programs whose distribution conditions are different, write to the author
-to ask for permission. For software which is copyrighted by the Free
-Software Foundation, write to the Free Software Foundation; we sometimes
-make exceptions for this. Our decision will be guided by the two goals
-of preserving the free status of all derivatives of our free software and
-of promoting the sharing and reuse of software generally.
-
- NO WARRANTY
-
- 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
-FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
-OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
-PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
-OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
-MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
-TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
-PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
-REPAIR OR CORRECTION.
-
- 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
-REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
-INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
-OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
-TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
-YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
-PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGES.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-convey the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-
- Copyright (C)
-
- This program is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License along
- with this program; if not, see .
-
-Also add information on how to contact you by electronic and paper mail.
-
-If the program is interactive, make it output a short notice like this
-when it starts in an interactive mode:
-
- Gnomovision version 69, Copyright (C) year name of author
- Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, the commands you use may
-be called something other than `show w' and `show c'; they could even be
-mouse-clicks or menu items--whatever suits your program.
-
-You should also get your employer (if you work as a programmer) or your
-school, if any, to sign a "copyright disclaimer" for the program, if
-necessary. Here is a sample; alter the names:
-
- Yoyodyne, Inc., hereby disclaims all copyright interest in the program
- `Gnomovision' (which makes passes at compilers) written by James Hacker.
-
- , 1 April 1989
- Moe Ghoul, President of Vice
-
-This General Public License does not permit incorporating your program into
-proprietary programs. If your program is a subroutine library, you may
-consider it more useful to permit linking proprietary applications with the
-library. If this is what you want to do, use the GNU Lesser General
-Public License instead of this License.
+MIT License
+
+Copyright (c) 2026 Open Data Ensemble
+
+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:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+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.
diff --git a/synkronus-cli/README.md b/synkronus-cli/README.md
index 3b33a62c4..f64977b9a 100644
--- a/synkronus-cli/README.md
+++ b/synkronus-cli/README.md
@@ -193,16 +193,7 @@ synk data export ./backups/observations_parquet.zip
## License
-**GPL-2.0-or-later** — see [`LICENSE`](./LICENSE) in this directory.
-
-The rest of the ODE monorepo may use other licenses (for example MIT at the
-repository root). This applies to **synkronus-cli** only.
-
-**Planned follow-up:** replace the `yeqown/go-qrcode/writer/standard` (gg /
-freetype) stack with a small stdlib `qrcode.Writer` implementation so this
-component can return to a permissive license. See
-[`FOLLOWUP-custom-qrcode-writer.md`](./FOLLOWUP-custom-qrcode-writer.md) for a
-ticket-ready summary.
+MIT — see [`LICENSE`](./LICENSE) in this directory.
## Dev. notes
diff --git a/synkronus-cli/go.mod b/synkronus-cli/go.mod
index 8ccd81309..780a1409a 100644
--- a/synkronus-cli/go.mod
+++ b/synkronus-cli/go.mod
@@ -13,7 +13,6 @@ require (
github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.18.2
github.com/yeqown/go-qrcode/v2 v2.2.5
- github.com/yeqown/go-qrcode/writer/standard v1.3.0
golang.org/x/term v0.32.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -21,12 +20,10 @@ require (
require (
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect
- github.com/fogleman/gg v1.3.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/getkin/kin-openapi v0.133.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
- github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
@@ -40,7 +37,6 @@ require (
github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
- github.com/pkg/errors v0.9.1 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
@@ -56,7 +52,6 @@ require (
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
- golang.org/x/image v0.10.0 // indirect
golang.org/x/mod v0.23.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.33.0 // indirect
diff --git a/synkronus-cli/go.sum b/synkronus-cli/go.sum
index ff0acc27d..34c748a3f 100644
--- a/synkronus-cli/go.sum
+++ b/synkronus-cli/go.sum
@@ -15,8 +15,6 @@ github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 h1:PRxIJD8XjimM5aT
github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936/go.mod h1:ttYvX5qlB+mlV1okblJqcSMtR4c52UKxDiX9GRBS8+Q=
github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
-github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
-github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -34,8 +32,6 @@ github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
-github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
-github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
@@ -110,8 +106,6 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -164,12 +158,9 @@ github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIj
github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
github.com/yeqown/go-qrcode/v2 v2.2.5 h1:HCOe2bSjkhZyYoyyNaXNzh4DJZll6inVJQQw+8228Zk=
github.com/yeqown/go-qrcode/v2 v2.2.5/go.mod h1:uHpt9CM0V1HeXLz+Wg5MN50/sI/fQhfkZlOM+cOTHxw=
-github.com/yeqown/go-qrcode/writer/standard v1.3.0 h1:chdyhEfRtUPgQtuPeaWVGQ/TQx4rE1PqeoW3U+53t34=
-github.com/yeqown/go-qrcode/writer/standard v1.3.0/go.mod h1:O4MbzsotGCvy8upYPCR91j81dr5XLT7heuljcNXW+oQ=
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
@@ -177,14 +168,9 @@ go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTV
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
-golang.org/x/image v0.10.0 h1:gXjUUtwtx5yOE0VKWq1CH4IJAClq4UGgUA3i+rpON9M=
-golang.org/x/image v0.10.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-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/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -192,18 +178,13 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-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/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -220,31 +201,23 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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-20211216021012-1d35b9e2eb4e/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.0.0-20220811171246-fbc7d0a398ab/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.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
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.6/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.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
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.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-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/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/synkronus-cli/internal/cmd/qr.go b/synkronus-cli/internal/cmd/qr.go
index 9437ac8f9..848208bd0 100644
--- a/synkronus-cli/internal/cmd/qr.go
+++ b/synkronus-cli/internal/cmd/qr.go
@@ -13,7 +13,6 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
qrcode "github.com/yeqown/go-qrcode/v2"
- "github.com/yeqown/go-qrcode/writer/standard"
)
// encodeFRMLS replicates the FRMLS encoding used by the Formulus generateQR.ts script.
@@ -152,14 +151,19 @@ for the Formulus mobile app. The QR encodes server URL, username, and password.`
outputFile = deriveOutputFilename(serverURL)
}
- // Generate QR code with logo using yeqown/go-qrcode
+ // Generate QR code with logo using yeqown/go-qrcode and custom stdlib PNG writer
logoPath := "qr_logo.png"
qrc, err := qrcode.New(encoded)
if err != nil {
return fmt.Errorf("failed to create QR code: %w", err)
}
- w, err := standard.New(outputFile, standard.WithLogoImageFilePNG(logoPath))
+ logo, err := loadQRLogo(logoPath)
+ if err != nil {
+ return fmt.Errorf("failed to load QR logo: %w", err)
+ }
+
+ w, err := newPNGWriter(outputFile, logo)
if err != nil {
return fmt.Errorf("failed to create QR writer: %w", err)
}
diff --git a/synkronus-cli/internal/cmd/qr_logo.png b/synkronus-cli/internal/cmd/qr_logo.png
new file mode 100644
index 000000000..a5994bc28
Binary files /dev/null and b/synkronus-cli/internal/cmd/qr_logo.png differ
diff --git a/synkronus-cli/internal/cmd/qr_test.go b/synkronus-cli/internal/cmd/qr_test.go
new file mode 100644
index 000000000..a0c5f2f1e
--- /dev/null
+++ b/synkronus-cli/internal/cmd/qr_test.go
@@ -0,0 +1,109 @@
+package cmd
+
+import (
+ "bytes"
+ "image/png"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ qrcode "github.com/yeqown/go-qrcode/v2"
+)
+
+func TestEncodeFRMLS(t *testing.T) {
+ encoded := encodeFRMLS(1, "https://demo.synkronus.org", "testuser", "secret")
+ if !strings.HasPrefix(encoded, "FRMLS:") {
+ t.Fatalf("expected FRMLS: prefix, got %q", encoded)
+ }
+ if !strings.HasSuffix(encoded, ";;") {
+ t.Fatalf("expected ;; suffix, got %q", encoded)
+ }
+ if !strings.Contains(encoded, "v:") || !strings.Contains(encoded, "s:") ||
+ !strings.Contains(encoded, "u:") || !strings.Contains(encoded, "p:") {
+ t.Fatalf("missing required FRMLS fields in %q", encoded)
+ }
+}
+
+func TestDeriveOutputFilename(t *testing.T) {
+ tests := []struct {
+ url string
+ expected string
+ }{
+ {"", "formulus-qr.png"},
+ {"https://example.com", "example.com.png"},
+ {"https://example.com/api/v1", "example.com_api_v1.png"},
+ {"http://localhost:8080/sync", "localhost_sync.png"},
+ }
+
+ for _, tt := range tests {
+ got := deriveOutputFilename(tt.url)
+ if got != tt.expected {
+ t.Errorf("deriveOutputFilename(%q) = %q, want %q", tt.url, got, tt.expected)
+ }
+ }
+}
+
+func TestLoadQRLogo(t *testing.T) {
+ // Test loading embedded logo fallback
+ logo, err := loadQRLogo("nonexistent_logo_file.png")
+ if err != nil {
+ t.Fatalf("loadQRLogo failed: %v", err)
+ }
+ if logo == nil {
+ t.Fatal("expected embedded logo to be loaded, got nil")
+ }
+ if logo.Bounds().Dx() <= 0 || logo.Bounds().Dy() <= 0 {
+ t.Fatalf("invalid logo dimensions: %v", logo.Bounds())
+ }
+}
+
+func TestPNGWriterGeneratesValidPNG(t *testing.T) {
+ tmpDir := t.TempDir()
+ outPath := filepath.Join(tmpDir, "test_qr.png")
+
+ encoded := encodeFRMLS(1, "https://example.com", "testuser", "testpass")
+ qrc, err := qrcode.New(encoded)
+ if err != nil {
+ t.Fatalf("failed to create QR code: %v", err)
+ }
+
+ logo, err := loadQRLogo("")
+ if err != nil {
+ t.Fatalf("failed to load logo: %v", err)
+ }
+
+ writer, err := newPNGWriter(outPath, logo)
+ if err != nil {
+ t.Fatalf("failed to create PNG writer: %v", err)
+ }
+
+ if err := qrc.Save(writer); err != nil {
+ t.Fatalf("failed to save QR code: %v", err)
+ }
+
+ data, err := os.ReadFile(outPath)
+ if err != nil {
+ t.Fatalf("failed to read output file: %v", err)
+ }
+ if len(data) == 0 {
+ t.Fatal("output file is empty")
+ }
+
+ // Verify it's a valid PNG
+ img, err := png.Decode(bytes.NewReader(data))
+ if err != nil {
+ t.Fatalf("output is not a valid PNG: %v", err)
+ }
+
+ bounds := img.Bounds()
+ if bounds.Dx() <= 0 || bounds.Dy() <= 0 {
+ t.Fatalf("invalid image bounds: %v", bounds)
+ }
+
+ // Dimension should be mat.Width()*20 + 80
+ expectedDim := qrc.Dimension()*20 + 80
+ if bounds.Dx() != expectedDim || bounds.Dy() != expectedDim {
+ t.Errorf("expected image dimension %dx%d, got %dx%d", expectedDim, expectedDim, bounds.Dx(), bounds.Dy())
+ }
+}
diff --git a/synkronus-cli/internal/cmd/qr_writer.go b/synkronus-cli/internal/cmd/qr_writer.go
new file mode 100644
index 000000000..2b951bc7f
--- /dev/null
+++ b/synkronus-cli/internal/cmd/qr_writer.go
@@ -0,0 +1,132 @@
+package cmd
+
+import (
+ "bytes"
+ _ "embed"
+ "fmt"
+ "image"
+ "image/color"
+ "image/draw"
+ "image/png"
+ "os"
+
+ qrcode "github.com/yeqown/go-qrcode/v2"
+)
+
+//go:embed qr_logo.png
+var defaultQRLogoBytes []byte
+
+// pngWriter implements qrcode.Writer using only Go standard library image packages.
+type pngWriter struct {
+ file *os.File
+ logo image.Image
+ blockWidth int
+ padding int
+}
+
+// newPNGWriter creates a new pngWriter that outputs to the given file path.
+func newPNGWriter(filename string, logo image.Image) (*pngWriter, error) {
+ f, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create output file: %w", err)
+ }
+ return &pngWriter{
+ file: f,
+ logo: logo,
+ blockWidth: 20,
+ padding: 40,
+ }, nil
+}
+
+// Write generates the PNG image from the matrix and encodes it into the file.
+func (w *pngWriter) Write(mat qrcode.Matrix) error {
+ if w.file == nil {
+ return fmt.Errorf("output file is not open")
+ }
+ img := renderQRImage(mat, w.blockWidth, w.padding, w.logo)
+ return png.Encode(w.file, img)
+}
+
+// Close closes the underlying file handle.
+func (w *pngWriter) Close() error {
+ if w.file != nil {
+ err := w.file.Close()
+ w.file = nil
+ return err
+ }
+ return nil
+}
+
+// renderQRImage renders the QR matrix as an RGBA image with an optional centered logo.
+func renderQRImage(mat qrcode.Matrix, blockWidth, padding int, logo image.Image) image.Image {
+ if blockWidth <= 0 {
+ blockWidth = 20
+ }
+ if padding < 0 {
+ padding = 40
+ }
+
+ matW := mat.Width()
+ matH := mat.Height()
+ imgW := matW*blockWidth + 2*padding
+ imgH := matH*blockWidth + 2*padding
+
+ img := image.NewRGBA(image.Rect(0, 0, imgW, imgH))
+
+ // Fill white background
+ white := color.RGBA{R: 255, G: 255, B: 255, A: 255}
+ draw.Draw(img, img.Bounds(), &image.Uniform{C: white}, image.Point{}, draw.Src)
+
+ // Draw QR modules
+ black := color.RGBA{R: 0, G: 0, B: 0, A: 255}
+ blackUniform := &image.Uniform{C: black}
+ bitmap := mat.Bitmap()
+
+ for y := 0; y < matH; y++ {
+ for x := 0; x < matW; x++ {
+ if bitmap[y][x] {
+ x0 := padding + x*blockWidth
+ y0 := padding + y*blockWidth
+ rect := image.Rect(x0, y0, x0+blockWidth, y0+blockWidth)
+ draw.Draw(img, rect, blackUniform, image.Point{}, draw.Src)
+ }
+ }
+ }
+
+ // Composite centered logo if provided
+ if logo != nil {
+ bounds := logo.Bounds()
+ logoW := bounds.Dx()
+ logoH := bounds.Dy()
+ if logoW > 0 && logoH > 0 && logoW <= imgW && logoH <= imgH {
+ x0 := (imgW - logoW) / 2
+ y0 := (imgH - logoH) / 2
+ rect := image.Rect(x0, y0, x0+logoW, y0+logoH)
+ draw.Draw(img, rect, logo, bounds.Min, draw.Over)
+ }
+ }
+
+ return img
+}
+
+// loadQRLogo attempts to load the logo from the specified path, falling back to embedded default.
+func loadQRLogo(logoPath string) (image.Image, error) {
+ if logoPath != "" {
+ if f, err := os.Open(logoPath); err == nil {
+ defer f.Close()
+ if img, err := png.Decode(f); err == nil {
+ return img, nil
+ }
+ }
+ }
+
+ if len(defaultQRLogoBytes) > 0 {
+ img, err := png.Decode(bytes.NewReader(defaultQRLogoBytes))
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode embedded logo: %w", err)
+ }
+ return img, nil
+ }
+
+ return nil, nil
+}