Skip to content

The Card Stamp

Pablo Martínez López edited this page Nov 10, 2026 · 2 revisions

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.

Why it exists

  • 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.

Why it cannot be turned off

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.

What it does not protect against

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.

What it says

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.

Where it goes, and where it does not

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.

Why vector text in the PDF

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.

Where on the card

 ┌─────────────────────────┐
 │ 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.

Size and candidate places (SITIOS)

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.

Own images and designed cards (Origen)

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):

  1. 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.
  2. The four SITIOS at 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.

Choosing a place and a background (colocar)

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.

One calculation for screen and paper

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 same Linea, 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) and pdf::sello::dibujar_sello (PDF) draw the same Linea in the same Caja; the pill is the same pastilla() path (Trazo list) 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.

The sheet line

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.

Which language a card gets

sello::idioma_de_carta(card, lang_deck):

  1. A designed card (card.design is set): the deck's design language (PrintSettings.designLang), the language the card is painted in.
  2. Otherwise the language of the image: CardMeta.imageLang (custom images say it separately), then CardMeta.lang (the printing).
  3. 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).

Fonts

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.

Caches and fingerprints

  • RenderSpec.sello is Option<DesignLang> with skip_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, and PIPELINE_VERSION did 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 in app_cache_dir/renders/.
  • With sello set, passthrough() is false (the pixels change), and render_rgba / render_desde_imagen paint 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.sello is optional (#[ts(optional)], serde(default)), so older /r/ URLs still parse.

Flow

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
Loading

Tests

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.

Where to start when extending

  • Changing a text: edit textos.json; if it is a ja, ko, zhs or zht text, re-run scripts/sello-letras.py and commit the new subsets.
  • Changing the size or places: TAM and SITIOS in sello.rs; keep cabe_en_una_carta_sin_estrecharse_y_lejos_del_corte passing. 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::pintar on the trimmed card, or RenderSpec.sello); a new working view must not.

See also

Documentation · Documentación

Language: English · Español

Use and build · Uso y desarrollo

Architecture · Arquitectura

Data · Datos

Getting cards · Conseguir cartas

Layout, images, PDF · Maquetación, imagen y PDF

Card designer · Diseñador de cartas

Game table · Mesa de juego

Mobile · Móvil

Frontend · Frontend

Reference · Referencia

Clone this wiki locally