Compare commits
5 Commits
5733342619
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a0bd62432 | |||
| d409928506 | |||
| f1e2231675 | |||
| e87f19e7ce | |||
| 3d53660319 |
65
README.md
65
README.md
@@ -1,36 +1,47 @@
|
||||
<h1 align="center">
|
||||
<br>
|
||||
<img src="https://i.imgur.com/an3wOdO.png" alt="QRCode Image" width="200">
|
||||
<br>
|
||||
simple-qr.com
|
||||
<br>
|
||||
</h1>
|
||||
<div align="center">
|
||||
<h1><a target="_blank" href="https://simple-qr.com">simple-qr.com</a></h1>
|
||||
<img src="./docs/demo.gif" alt="demo" />
|
||||
</div>
|
||||
|
||||
<h4 align="center">Simple, bullshit-free QR code generator with straightforward API.</h4>
|
||||
Simple, bullshit-free QR code generator with straightforward API.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://nuxt.com">
|
||||
<img src="https://img.shields.io/badge/nuxt-4ade80?style=for-the-badge&logo=vite&logoColor=white">
|
||||
</a>
|
||||
<a href="https://typescriptlang.org">
|
||||
<img src="https://img.shields.io/badge/TypeScript-007acc?style=for-the-badge&logo=typescript&logoColor=white">
|
||||
</a>
|
||||
</p>
|
||||
## What it does
|
||||
|
||||
<p align="center" id="links">
|
||||
<a href="#description">Description</a> •
|
||||
<a href="https://simple-qr.com">Visit it</a> •
|
||||
<a href="#license">License</a>
|
||||
</p>
|
||||
- Generate QR codes via a simple web UI or a straightforward REST API
|
||||
- Embed logos (30+ brands: Gitea, Signal, Monero, Session, etc.) in the center of QR codes
|
||||
- Export in PNG, JPEG, or WebP formats
|
||||
- No authentication, no rate limiting, no ads
|
||||
|
||||
<br>
|
||||
## Stack
|
||||
|
||||
## Description
|
||||
- [Nuxt 4](https://nuxt.com) + [Nuxt UI](https://ui.nuxt.com)
|
||||
|
||||
I created this side project to learn more about Nuxt and Vue, also because I needed a simple way to generate QR codes, but all websites I found were full of ads and were over complicated.
|
||||
## API
|
||||
|
||||
<br>
|
||||
### `GET /api` - Generate a QR code
|
||||
|
||||
## License
|
||||
Returns the QR code as an image file.
|
||||
|
||||
This project is <a href="https://opensource.org/licenses/MIT">MIT</a> licensed.
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|---------------------------|----------|-----------------------------------------------------|
|
||||
| `content` | string | yes | Data to encode |
|
||||
| `format` | `png` \| `jpeg` \| `webp` | no | Output format (default: `png`) |
|
||||
| `logo` | string | no | Logo name to embed in the center (case-insensitive) |
|
||||
|
||||
**Example:**
|
||||
```
|
||||
GET https://simple-qr.com/api?content=https://git.pihkaal.me&format=webp&logo=gitea
|
||||
```
|
||||
|
||||
### `GET /api/logos` - List available logos
|
||||
|
||||
Returns a JSON array of available logo names.
|
||||
|
||||
**Example:**
|
||||
```
|
||||
GET https://simple-qr.com/api/logos
|
||||
```
|
||||
|
||||
```json
|
||||
["signal", "monero", "session", ...]
|
||||
```
|
||||
|
||||
@@ -11,6 +11,7 @@ services:
|
||||
- "traefik.http.services.simple-qr.loadbalancer.server.port=3000"
|
||||
- "traefik.http.routers.simple-qr.tls=true"
|
||||
- "traefik.http.routers.simple-qr.tls.certResolver=myresolver"
|
||||
- "traefik.http.routers.simple-qr.middlewares=umami-middleware@file"
|
||||
restart: always
|
||||
|
||||
networks:
|
||||
|
||||
BIN
docs/demo.gif
Normal file
BIN
docs/demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 512 KiB |
@@ -1,4 +1,8 @@
|
||||
import { resolve } from "path";
|
||||
import type { Canvas } from "skia-canvas";
|
||||
import { getLogoNames } from "../utils/logos";
|
||||
|
||||
const normalize = (s: string) => s.toLowerCase().replace(/[\s_-]+/g, "");
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { format, logo, content } = await getValidatedQuery(
|
||||
@@ -6,8 +10,26 @@ export default defineEventHandler(async (event) => {
|
||||
settingsSchema.parse,
|
||||
);
|
||||
|
||||
const logoUrl = logo ? resolve("public", `logos/${logo}.png`) : undefined;
|
||||
const canvas = await renderQRCodeToCanvas(content, logoUrl);
|
||||
let canvas: Canvas;
|
||||
|
||||
if (logo) {
|
||||
const names = await getLogoNames();
|
||||
if (!names)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: "Could not retrieve logos",
|
||||
});
|
||||
|
||||
const match = names.find((n) => normalize(n) === normalize(logo));
|
||||
const resolvedLogo = match ?? logo;
|
||||
|
||||
canvas = await renderQRCodeToCanvas(
|
||||
content,
|
||||
resolve("public", `logos/${resolvedLogo}.png`),
|
||||
);
|
||||
} else {
|
||||
canvas = await renderQRCodeToCanvas(content, undefined);
|
||||
}
|
||||
|
||||
const image = canvas.toBuffer(format);
|
||||
event.node.res.setHeader("Content-Type", `image/${format}`);
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
import { readdir } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { createHash } from "crypto";
|
||||
import { getLogoNames } from "../utils/logos";
|
||||
|
||||
const logosDir = import.meta.dev
|
||||
? join(process.cwd(), "public/logos")
|
||||
: join(process.cwd(), ".output/public/logos");
|
||||
export default defineEventHandler(async () => {
|
||||
const names = await getLogoNames();
|
||||
if (!names)
|
||||
throw createError({ statusCode: 500, message: "Could not retrieve logos" });
|
||||
|
||||
export default defineCachedEventHandler(async () => {
|
||||
const files = await readdir(logosDir);
|
||||
return files
|
||||
.filter((f) => f.endsWith(".png"))
|
||||
.map((f) => f.replace(/\.png$/, ""))
|
||||
.sort();
|
||||
}, {
|
||||
maxAge: 60 * 60 * 24,
|
||||
getKey: async () => {
|
||||
const files = await readdir(logosDir);
|
||||
const key = files.filter((f) => f.endsWith(".png")).sort().join(",");
|
||||
return createHash("sha256").update(key).digest("hex");
|
||||
},
|
||||
return names;
|
||||
});
|
||||
|
||||
29
server/utils/logos.ts
Normal file
29
server/utils/logos.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { readdir } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { createHash } from "crypto";
|
||||
|
||||
const logosDir = import.meta.dev
|
||||
? join(process.cwd(), "public/logos")
|
||||
: join(process.cwd(), ".output/public/logos");
|
||||
|
||||
export const getLogoNames = cachedFunction(
|
||||
async () => {
|
||||
const files = await readdir(logosDir);
|
||||
return files
|
||||
.filter((f) => f.endsWith(".png"))
|
||||
.map((f) => f.replace(/\.png$/, ""))
|
||||
.sort();
|
||||
},
|
||||
{
|
||||
maxAge: 60 * 60 * 24,
|
||||
name: "logoNames",
|
||||
getKey: async () => {
|
||||
const files = await readdir(logosDir);
|
||||
const key = files
|
||||
.filter((f) => f.endsWith(".png"))
|
||||
.sort()
|
||||
.join(",");
|
||||
return createHash("sha256").update(key).digest("hex");
|
||||
},
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user