Repository navigation
The Card Stamp
English · Español
Every card that leaves the app on paper or as an image file carries a small line, «Tapete · Not for sale», in the card's own language, and every PDF sheet carries «Made with Tapete · Not for sale» in its margin, in the app's language. The stamp cannot be turned off. All of it lives in proxy-core::sello (src-tauri/crates/proxy-core/src/sello.rs) and proxy-core::pdf::sello (src-tauri/crates/proxy-core/src/pdf/sello.rs); the app only decides where to ask for it.
- No proxy can pass for an official card without anyone noticing. A proxy printed with the app is meant for casual play and testing; a visible «not for sale» on the face of the card stops it from being sold or traded as a real one by accident or in bad faith further down the line.
- It is the mark the community recommends for proxies. Marking proxies as not genuine on the front is the usual community advice; a line under the copyright, where real cards keep small print, does that without hiding any part of the card.
- It protects the app. A tool that makes undetectable copies is exposed; one whose every output says where it came from and that it is not for sale is not.
A switch would defeat the point: the cards that most need the mark are exactly the ones whose maker would switch it off. There is no setting, no field in PrintSettings and no parameter in export_pdf or card_export_image that skips it; in commands/export.rs every slot that has an image gets a SelloCasilla (comment: «El sello no se puede apagar»), and the tests check that each card and each sheet has its text.
Whoever edits the PDF or the image with another program can remove it: in the PDF it is ordinary text over the card, in an image it is a few rows of pixels. The stamp guarantees that a card made with the app cannot lose it by accident (forgetting a setting, sharing a file without thinking), not that a determined person cannot strip it.
src-tauri/crates/proxy-core/assets/sello/textos.json holds two tables, each with the 15 interface languages (es, en, fr, de, it, pt, ro, pl, nl, ru, uk, ja, ko, zhs, zht):
| Table | Where it goes | Language | Example (en) |
|---|---|---|---|
carta |
On every card | The card's (sello::idioma_de_carta) |
Tapete · Not for sale |
hoja |
In the margin of every PDF page | The app's (export_pdf receives lang) |
Made with Tapete · Not for sale |
texto_carta(lang) and texto_hoja(lang) read them; a missing language falls back to English, and if the JSON could not be read at all, to built-in English strings. «Tapete» is never translated. The texts are not in the src/i18n/ dictionaries because Rust paints them, and the card text follows the card, not the interface.
| Output | Stamp | How | Code |
|---|---|---|---|
| PDF export | Yes, on every slot with an image (fronts, backs, large cards) plus the sheet line | Vector text over the card image |
pdf::sello::dibujar_sello, dibujar_linea_hoja, called from pdf::compose
|
| Sheet thumbnails in the preview | Yes | Painted into the thumbnail pixels |
slot-view.tsx sends PreviewParams.sello = slot.sello; render.rs spec_de_miniatura sets RenderSpec.sello
|
| One card saved as an image (right-click → «Export image») | Yes | Painted |
commands/imagen.rs generar calls sello::pintar
|
| PNG export of designed cards | Yes, in each exported language | Painted |
diseno/png.rs exportar calls sello::pintar(&mut imagen, t.lang)
|
| Calibration sheet | No (it has no cards) |
PdfMeta.hoja = None in pdf/calibration.rs
|
|
| Game table, card designer, lists, variant grids, other thumbnails | Never |
mesa/preparar.rs and every other RenderSpec set sello: None; only SlotView puts sello in PreviewParams
|
Why the table and the editor stay clean: they are working views, not something that is printed or shared, and the stamp would cover the bottom of the card where the artist and set lines are being read or edited.
In the PDF the image of each card is not touched: spec_de_casilla sets sello: None, so a JPEG that already fits keeps going in byte for byte (passthrough, see Imaging and PDF), without re-compression, and identical copies are still embedded once. The stamp is drawn on top with krilla as glyphs of an embedded font subset, so it is sharp at any DPI, adds no image data and is searchable and copyable (the glyphs carry their text ranges, so the PDF has a Unicode mapping). Painting it into the pixels would have forced every card through decode and re-encode.
┌─────────────────────────┐
│ title cost│
│ artwork │
│ type line │
│ rules text │
│ artist · set · copyright│ <- printed line of small print
│ Tapete · Not for sale │ <- the stamp, centred, below it
└─────────────────────────┘
Centred in the bottom strip, below the artist and copyright line: printed cards leave a free border of about 2.5 mm there. Rejected alternatives:
- Bottom-left corner: on borderless and full-art cards that is where the collector number, set code and artist go; the stamp would cover them.
- Top: the title bar and the crowns of legendary or special frames reach the top edge.
TAM = 0.0205 is the em size as a fraction of the card height: 1.80 mm (about 5.1 pt) on an 88 mm card, the size of the copyright text on printed cards. SITIOS lists four candidate places, in order of preference, each a Sitio { escala, base } (scale over TAM, baseline as a fraction of the height from the bottom edge):
| # | escala |
base |
Note |
|---|---|---|---|
| 0 | 1.00 | 0.0135 | The usual place |
| 1 | 1.00 | 0.0118 | A bit lower |
| 2 | 0.88 | 0.0108 | Lower and smaller |
| 3 | 0.80 | 0.0100 | About 4.1 pt, descenders about 0.5 mm from the cut |
The lower places exist because the custom frames of the designer and of frame packs often put their footer lower than printed cards do; the stamp moves down rather than up so it never covers the artist line.
The places above are for an own image (or a Scryfall one), whose footer is unknown: it keeps exactly that behaviour (Origen::Propia, what colocar does). A designed card (from the editor: its sheet slot, the PDF, the PNG export and «Save as image» of a designed face) is placed with colocar_como(img, linea, Origen::Diseno), because its footer is known: the first line carries only the number and rarity on the left, the second the artist and the rights. It tries, in order, for each of these places first centred and then pressed to the right margin (Sitio.derecha, right edge at 87.5 % of the width):
-
PIE_ALTO(scale 0.90, baseline 0.0505): on the first footer line, level with «0016 R», above the artist and copyright line and below the frame line. - The four
SITIOSat the bottom.
The factory footer puts the artist at the centre-left of both lines and has the power/toughness box reach the first line on the right, so there the stamp ends up at the bottom right, clear of everything. A designed card also has a stricter plainness test: besides the standard deviation, no more than TRAZOS_MAXIMOS (0.04 %) of the pixels in the ink box may be far from the mean (TRAZO, 28 levels), which keeps the stamp away from the thin tails of letters that the deviation alone does not notice. The pill fallback is unchanged and stays at the bottom, centred.
colocar(img, linea) looks at the card already cut to size (no bleed) and, for each Sitio in order, measures the luminance of the rectangle the ink will cover (Caja.tinta). Transparent pixels (rounded corners) are treated as white paper.
- The first place whose background is plain (luminance standard deviation ≤
LISO, 14 on 0–255) wins, and the stamp goes there alone (Fondo::Banda), in white or black, whichever has more contrast with that background. Against a plain background the better of the two always gives at least 4.58:1, above the 4.5:1 that WCAG AA asks of small text. - If no place is plain (borderless or full-art cards, where the artwork reaches the edge), the place with the least variation is used with a semi-transparent dark pill behind white text (
Fondo::Pastilla,OPACIDAD_PASTILLA = 0.62, chosen so white text keeps at least 4.5:1 over any background).
The result is a Colocacion { sitio, fondo }, and Colocacion::color() gives the text colour.
As with PagePlan (Layout Engine), there is a single geometry:
-
linea(texto, lang)places the glyphs: for each character, its glyph id, its x in ems and its advance, read from the font's character map and metrics at the stamp weight. No kerning and no shaping, on purpose: the image painter (tiny-skia) and the PDF writer (krilla) draw from the sameLinea, so they cannot disagree because two text layout engines did. -
caja(linea, ancho, alto, sitio)returns, in whatever unit the caller uses (pixels for an image, points for the PDF), the em size, the text origin and baseline, the pill rectangle and the ink rectangle. Everything is a fraction of the card size, so a 244 px thumbnail and a 900 dpi print put the stamp in the same relative place. -
pintar(img, lang)(images) andpdf::sello::dibujar_sello(PDF) draw the sameLineain the sameCaja; the pill is the samepastilla()path (Trazolist) in both.
For the PDF, the placement is decided while preparing each image: sello::colocaciones(bytes, ancho_mm, sangrado_mm, langs) decodes the prepared image, crops its bleed and runs colocar once per language. The same image can appear in slots of different languages, and the text width changes where it fits, so Encargo.idiomas in commands/export.rs collects the languages per image key. If no placement was computed for a slot, the export falls back to SITIOS[0] with a pill, the safe choice on any background.
colocar always looks at the card at the same size: before measuring, it scales the image to ANCHO_MIRADA (400 px wide, Triangle filter), whatever resolution it receives. A thumbnail (146–745 px) and a print image (300–900 dpi) therefore measure the same pixels and choose the same place and background, even when a background is right at the plain threshold. Why: it is the same rule as the one geometry for screen and paper; the preview must show the stamp exactly where it will print, and measuring at each resolution would let small differences in detail push the two to different choices.
pdf::sello::base_hoja(page) centres «Made with Tapete · Not for sale» (6 pt, grey 120) in the bottom margin, below every card's bleed and every cut guide, if that margin is at least 3.2 mm; otherwise in the top margin; if neither fits, the page has no line (the cards still carry their own stamp). It is written in the app's language because it speaks to whoever holds the sheet, while the card stamp follows the card.
sello::idioma_de_carta(card, lang_deck):
- A designed card (
card.designis set): the deck's design language (PrintSettings.designLang), the language the card is painted in. - Otherwise the language of the image:
CardMeta.imageLang(custom images say it separately), thenCardMeta.lang(the printing). - If neither is known, the deck's design language.
sello::idioma(code) maps a Scryfall or interface code to a DesignLang; anything outside the 15 (Latin, Hebrew, Phyrexian…) is English. layout::plan stores the result in Slot.sello (both faces and the back of a card get the card's language), so neither the screen nor the PDF decides: SlotView passes slot.sello and the export uses slot.sello. The PNG export of designs uses each task's own language (t.lang).
The sheet line uses the interface language: exportarA (src/features/export/acciones.ts) sends lang: leerIdioma(), and export_pdf turns it into a DesignLang with sello::idioma (English if missing or unknown).
| Languages | Font | Why |
|---|---|---|
| Latin and Cyrillic (11 languages) | Montserrat, the variable font already bundled in assets/fuentes/Montserrat-Variable.ttf, at weight PESO = 600
|
The default instance of the variable font is weight 100, far too thin for 5 pt on paper. ubicacion sets wght to 600 for metrics and outlines, and the PDF embeds it with Font::new_variable(.., [("wght", 600)]). |
| Japanese, Korean, Simplified and Traditional Chinese |
assets/sello/sello-{ja,ko,zhs,zht}.ttf: subsets of Noto Sans JP/KR/SC/TC, already at weight 600, about 7 KB each |
Montserrat has no CJK glyphs, and the full Noto CJK fonts (10–18 MB each) are downloaded separately and only on demand (letras_cjk.rs). The stamp must work always, so tiny subsets with only the characters of the stamp texts are compiled into the app. |
scripts/sello-letras.py makes the subsets with fontTools: it downloads the same pinned Noto files as src-tauri/assets/letras-cjk.json (fixed commit of google/fonts, SHA-256 checked), instances them at weight 600, keeps only the characters of the carta and hoja texts of that language and writes sello-<lang>.ttf plus Noto-CJK-LICENSE.txt (OFL). The output is reproducible. Usage is in its header:
python3 -m venv /tmp/ft && /tmp/ft/bin/pip install fonttools==4.55.3
/tmp/ft/bin/python -I scripts/sello-letras.py <download folder>Run it again whenever a ja, ko, zhs or zht text in textos.json changes: a new character would have no glyph in the subset. The test los_quince_idiomas_tienen_texto_y_todos_sus_glifos fails if that happens.
-
RenderSpec.selloisOption<DesignLang>withskip_serializing_if = "Option::is_none".RenderSpec::key()hashes the spec as JSON, so a spec without a stamp serialises exactly as before and keeps its key: existing thumbnails and renders stay valid, andPIPELINE_VERSIONdid not change. A stamped sheet thumbnail has a different key from the unstamped thumbnail of the same image used elsewhere (lists, variants), so both live side by side inapp_cache_dir/renders/. - With
selloset,passthrough()is false (the pixels change), andrender_rgba/render_desde_imagenpaint the stamp on the card at its trim size before the bleed is added, so it never falls in what is cut off. - Designs do not change their fingerprint (
huella): the stamp is painted after the designer engine, onto the finished image, and is not part of the effective design, so design previews and caches are untouched. -
PreviewParams.sellois optional (#[ts(optional)],serde(default)), so older/r/URLs still parse.
flowchart TD
D["Deck"] --> P["layout::plan"]
P -->|"sello::idioma_de_carta"| S["Slot.sello (DesignLang)"]
S --> SV["SlotView (slot-view.tsx)"]
SV -->|"PreviewParams.sello"| R["pbimg:// /r/ → AppState::miniatura"]
R -->|"spec_de_miniatura: RenderSpec.sello = Some"| RR["render_rgba: frame, retouch, edges, sello::pintar, bleed"]
RR --> TH["sheet thumbnail with the stamp painted in"]
S --> EX["export_pdf(deck, path, zip?, lang)"]
EX --> EN["encargo: spec_de_casilla (sello: None) + Encargo.idiomas"]
EN --> PR["render_para_pdf / render_diseno_para_pdf (JPEG may pass through)"]
PR -->|"sello::colocaciones (crop bleed, colocar per language)"| CO["Colocacion per (image key, language)"]
CO --> CP["pdf::compose: Casilla { imagen, sello }"]
CP --> DS["dibujar_casilla, then dibujar_sello (vector text)"]
CP -->|"PdfMeta.hoja = app language"| DL["dibujar_linea_hoja"]
DS --> PDF["PDF"]
DL --> PDF
| Test | File | Guards |
|---|---|---|
los_quince_idiomas_tienen_texto_y_todos_sus_glifos |
proxy-core/src/sello.rs |
Every language has both texts, they contain «Tapete», and every character has a glyph with an advance in its font (catches a stale CJK subset). |
cabe_en_una_carta_sin_estrecharse_y_lejos_del_corte |
sello.rs |
In every language and every Sitio: inside the card with side margins, descenders at least 0.5 mm from the cut, top of the ink below the artist line, em never under ~4 pt. |
el_idioma_de_una_carta |
sello.rs |
Codes map to DesignLang; unknown or missing codes are English. |
sobre_un_borde_liso_va_solo_y_con_contraste |
sello.rs |
Plain dark border → white text, plain light → black, awkward greys still go without a pill. |
sobre_la_ilustracion_va_en_pastilla |
sello.rs |
A busy background gets the pill. |
si_el_pie_llega_abajo_busca_un_sitio_mas_bajo |
sello.rs |
A line of text where the usual place is pushes the stamp to a lower place that does not touch it. |
pintar_solo_cambia_la_franja_del_sello |
sello.rs |
Painting changes only pixels inside the stamp box (Spanish, Japanese, Russian). |
la_colocacion_de_una_imagen_con_sangrado_es_la_de_la_carta_sin_el |
sello.rs |
colocaciones crops the bleed before looking. |
dos_veces_lo_mismo_da_los_mismos_pixeles |
sello.rs |
Deterministic painting. |
la_miniatura_y_el_pdf_eligen_lo_mismo |
sello.rs |
Thumbnails at 146, 244, 488 and 745 px of a 1488 px card choose the same placement as the full card. |
con_sello_ya_no_va_tal_cual_y_solo_cambia_la_franja_de_abajo |
proxy-core/src/imaging/pipeline.rs |
A stamped spec has another key, is no longer a passthrough, and only the bottom 4 % changes. |
cada_carta_lleva_su_sello_y_cada_hoja_su_linea_sin_tocar_la_imagen |
proxy-core/src/pdf/compose.rs |
One text object per slot plus one per page, the JPEG embedded unchanged and once, «Tapete» extractable, one extra fill per card with the pill, deterministic bytes. |
la_linea_de_la_hoja_va_en_el_margen_fuera_de_las_cartas_y_las_guias |
pdf/compose.rs |
The sheet line sits in the margin below cards and guides. |
el_sello_va_en_la_miniatura_y_nunca_pintado_en_la_imagen_del_pdf |
src-tauri/src/render.rs |
spec_de_miniatura carries the stamp only when asked; spec_de_casilla never paints it. |
exportar_veinte_cartas_en_duplex |
src-tauri/src/commands/export.rs |
The real export: every page has one text per slot (fronts and backs) plus its sheet line, and «Tapete» is in the text. |
| «manda la instantánea del store, no lo guardado» | src/features/export/__tests__/acciones.test.ts |
The screen sends lang to export_pdf. |
- Changing a text: edit
textos.json; if it is aja,ko,zhsorzhttext, re-runscripts/sello-letras.pyand commit the new subsets. - Changing the size or places:
TAMandSITIOSinsello.rs; keepcabe_en_una_carta_sin_estrecharse_y_lejos_del_cortepassing. Because placement is computed from fractions of the card, screen and PDF follow automatically. - A new output that writes card images for people to keep or print must stamp them (
sello::pintaron the trimmed card, orRenderSpec.sello); a new working view must not.
Use and build · Uso y desarrollo
- User-Guide · Guía de usuario
- Development-Setup · Preparación del entorno de desarrollo
- Testing-and-CI · Tests y CI
- Releases-and-Auto-Updates · Versiones y actualizaciones automáticas
- Conventions · Convenciones
- Rename-to-Tapete · Cambio de nombre a Tapete
Architecture · Arquitectura
- Architecture · Arquitectura
- IPC-and-Error-Handling · IPC y manejo de errores
- Rust-App-Layer · Capa de aplicación Rust
Data · Datos
- Deck-Model · Modelo del mazo
- Storage-and-Blobs · Almacenamiento y blobs
- Deck-Package-Format · Formato del paquete de mazo
- Deck-Library · Biblioteca de mazos
Getting cards · Conseguir cartas
- Decklist-Parsing · Parseo de la lista del mazo
- Scryfall-Integration · Integración con Scryfall
- External-Sources · Fuentes externas
- Card-Identification-and-OCR · Identificación de cartas y OCR
Layout, images, PDF · Maquetación, imagen y PDF
- Layout-Engine · Motor de maquetación
- Imaging-and-PDF · Imagen y PDF
- Export · Exportación
- The-Card-Stamp · El sello de las cartas
- Live-Preview · Vista previa en vivo
- Statistics · Estadísticas
Card designer · Diseñador de cartas
- Designer-Overview · Visión general del diseñador
- Designer-Engine · Motor del diseñador
- Designer-Composition · Composición del diseñador
- Frame-Packs · Packs de marcos
- Designer-App-Layer · Capa de aplicación del diseñador
- Designer-UI · Interfaz del diseñador
- Card-Editor-Dialog · Diálogo Editar carta
Game table · Mesa de juego
- Game-Engine · Motor de la mesa
- Game-Table · Mesa de juego
- Online-Play · Juego en línea
- Friends-and-Messages · Amigos y mensajes
Mobile · Móvil
Frontend · Frontend
- Frontend-Architecture · Arquitectura del frontend
- State-Stores · Stores de estado
- Internationalization · Internacionalización
- Themes-and-Skins · Temas y skins
- Icons · Iconos
- Settings-and-Shell · Ajustes y shell
- Tutorials · Tutoriales
- Decklist-Panel · Panel de la lista del mazo
- Add-Card-and-Variants · Añadir carta y variantes
- Card-Search · Búsqueda de cartas
- Set-Browser · Explorador de sets
Reference · Referencia