Repository navigation
Frame Packs
English · Español
A frame pack tells the designer how a card looks: where each piece goes, which colours it uses and which layers are drawn over the artwork. The builtin pack ships inside the app; "Original" (image frames) and "Balatro" (pixel art) are downloaded on demand from pinned, hash-checked sources; users can import their own as a folder or .zip. This page documents the pack format (version 2) as implemented in pack.rs, the shipped packs, the download machinery, the free-font catalogue and the security model.
The user-facing description of the format is docs/packs-de-marcos.md (Spanish); this page re-states it against the code. See Designer-Engine for how packs are consumed by the painter and Designer-Composition for the pack layouts used by multi-piece cards.
flowchart LR
subgraph Sources
B["builtin: embedded in proxy-core (include_str!)"]
O["Original: Card Conjurer PNGs, pinned commit"]
D["Downloadable: proxy-builder-packs repo, pinned commit"]
U["User import: folder or .zip"]
end
B --> P["Pack::de_fabrica"]
O --> L["app_data_dir/marcos/original/"]
D --> L2["app_data_dir/marcos/id/"]
U --> L3["app_data_dir/marcos/id/"]
L --> R["recursos.rs: leer_packs / recargar_pack"]
L2 --> R
L3 --> R
P --> R
R --> V["Pack::cargar_con_imagenes: validate + huella"]
V --> E["render::pintar_cara"]
Code map:
| What | Where |
|---|---|
| Format, validation, builtin and Original definitions |
src-tauri/crates/proxy-core/src/diseno/pack.rs (Pack, PackDef, EstiloDef, Capa, Condicion, Hueco, PackError, VERSION_PACK, BUILTIN, ORIGINAL, ORIGINAL_IMAGENES) |
| Builtin pack files |
src-tauri/crates/proxy-core/assets/marcos/builtin/ (pack.json plus SVGs per style and layouts/) |
| Original definition and PNG list |
src-tauri/crates/proxy-core/assets/marcos/original/ (pack.json, 4 SVGs, imagenes.json) |
Loading/import/removal, PackInfo
|
src-tauri/src/diseno/recursos.rs |
| Original downloader | src-tauri/src/diseno/original.rs |
| Downloadable packs |
src-tauri/src/diseno/descargables.rs, manifest src-tauri/assets/packs-descargables.json
|
| Free fonts |
src-tauri/src/diseno/letras_libres.rs, catalogue src-tauri/assets/letras-libres.json
|
| Commands |
src-tauri/src/commands/diseno.rs: packs_list, pack_import, pack_remove, fonts_list, font_import, fonts_free_list, font_free_download, original_info, original_download, packs_downloadable_list, pack_downloadable_download, pack_extra_use
|
| UI stores |
src/features/disenar/marcos-originales.ts, src/features/disenar/packs-descargables.ts, recursos-dialog.tsx
|
| Pack authoring repo |
proxy-builder-packs, a separate repository (see below) |
my-pack/
pack.json required
<style>/frame.svg SVG layers (use {{variables}})
m15/frame{color}.png PNG layers (one per palette)
symbols/*.png own mana symbols (optional)
labels/*.png label pill backgrounds (optional)
Only .json, .svg and .png files are read (recursos.rs → tipo_de); other files are ignored. Fonts are not part of a pack: the pack names font families and they must be loaded separately (the Balatro download imports its font as part of the pack download).
Pack::cargar_con_imagenes(ficheros, imagenes) returns a Pack { def, ficheros, imagenes, huella, simbolos } or a PackError:
PackError |
Cause |
|---|---|
Json |
pack.json not parseable; or a style base that is not one of the four built-in styles; or a layer with both/neither of svg and png; or a cajaReglas layer that is not alone with a zone; or desde/mascara on a layer that is not PNG or that has tramos; or an own symbol that fails to parse |
Version |
version is 0 or greater than VERSION_PACK (3). An older app therefore rejects a newer pack instead of painting it wrongly. The builtin pack declares version 1, Balatro 2, Original 3 (it uses desde/mascara) |
FaltaFichero |
a layer's SVG, a layer's mascara PNG, a PNG for any palette (checked for every palette and, for {color1}/{color2}, for every single-colour combination), a label background PNG, the loyalty PNG, an own symbol file, or pack.json itself is missing |
FaltaHueco |
a style lacks one of titulo, tipo, reglas, pie (HUECOS_OBLIGATORIOS) |
FaltaEstilo |
requested style absent (Pack::estilo) |
Color |
a malformed colour (#rgb, #rrggbb, #rrggbbaa accepted), or a missing palette |
Expresion |
an invalid hueco expression |
All nine palettes (W U B R G gold artifact land colorless) are mandatory. Hueco colours may be a {{variable}}. Root-level layouts get their layers and colours checked but not their required huecos. App-side, recursos.rs::de_pack maps each error to AppError InvalidInput with motivo: "pack_invalido" and params (falta, estilo, hueco, color, version, detalle, expresion), which the UI translates (see IPC-and-Error-Handling).
Pack.huella is a SHA-256 over every text file then every PNG (sorted by path, each followed by NUL separators). It feeds the render hash, so changing any pack file invalidates cached renders, and keys the PNG caches in render::imagenes_png.
- Directory:
app_data_dir/marcos/<id>/.leer_packsloadsbuiltin(embedded) plus every directory there at startup; a directory that fails validation is silently skipped. -
Recursos::importar_pack(ruta): reads a folder or.zip(leer_zip: per-file caps of 8 MiB for json/svg and 32 MiB for png, exceeding one fails withInvalidInput/motivo: "pack_grande"; entries with..are skipped; if everything sits in one top-level folder, that prefix is stripped; the caps are not applied to folder imports), validates, sanitises the id to[A-Za-z0-9_-], rejects an empty id orbuiltin(motivo: "pack_id"), replaces any existing directory of that id, writes files atomically (escribir_atomico) and registers the pack. Importing the same id again replaces it. -
quitar_pack(id): refusesbuiltinand ids containing/,\or.. -
PackInfo { id, nombre, importado, estilos, estilos_propios, origen }goes to the UI;importadois false only for builtin.estilos_propioslists own styles with their names per language code (EstiloPropio.nombres),baseand variants (VariantePack.nombres), passed as the pack wrote them; the UI picks the interface language, else English, else the id (nombreEnIdiomainsrc/features/disenar/estilos.ts).
All geometry is in millimetres on a 63 x 88 card; a style or layout may declare another size with tamano: [w, h]. The engine scales to the output pixels, so the preview, 300/600/900 DPI PDFs and PNG export come from the same render.
Expressions (pack::evaluar, Valor): any hueco or layer-zone coordinate is a number or a string that is a sum/subtraction of numbers and variable names ("yTipo+1.6", "80-yTipo"). No parentheses, no multiplication. Available variables, only when the style has a dynamic rules box (caja): yReglas (top of the rules text), altoReglas (height of the rules text), yTipo (top of the type bar). If a variable is missing (fixed box) the coordinate evaluates to 0: a hueco with zero width/height is not drawn, and a PNG layer whose zone cannot be evaluated is skipped.
SVG variables (rellenar, unknown names become empty): replaced in layer SVGs before rendering.
| Variable | Meaning |
|---|---|
{{defs}} |
gradient definitions: url(#marco), #brillo, #panel, #panelfr, #crema, #lateral, #vetas, #bisel, #barra (put inside <defs>; see render::defs) |
{{claro}} {{medio}} {{oscuro}} {{tinte}} {{caja}} |
palette colours (or the mix of two palettes) |
{{fondoCaja}} {{opacidadCaja}} {{tintaCaja}} |
rules-box fill, opacity and the text ink that reads on it |
{{refuerzoCaja}} |
0..1, how much more opaque the box was asked to be (for image frames with the box baked in: draw a black rect under it with that opacity) |
{{tintaCrema}} {{tintaFr}} |
ink on cream and on the P/T box |
{{yReglas}} {{altoReglas}} {{yTipo}} {{yTipo04}} |
dynamic-box geometry. In SVGs yReglas/altoReglas are the whole box (top and full height); in hueco expressions they are the text area. yTipo04 is yTipo + 0.4
|
{{extensionArte}} |
mm the artwork continues below its window (the fine-tuning fade extension, or the style's extensionDifuminado; 0 by default). The black below the art has to start that much lower. |
{{subeCaja}} |
mm the rules box was asked to be taller in the fine tuning (negative = shorter; 0 by default). |
{{= 56.25 - subeCaja}} |
An arithmetic expression (sums and subtractions) over the numeric variables, so a fixed frame can move its type bar and the top edge of its box. |
Hueco expressions can use yReglas, altoReglas, yTipo and subeCaja in sums and subtractions: "y": "yTipo+0.4", "h": "52.5 - subeCaja". A pack that does not use subeCaja does not change with that setting; in the dynamic box the requested height is simply added to it. The fine-tuning rulesBoxDynamic (dynamic / fixed / as the frame) applies to every frame: see rules_box_dynamic in Designer-Engine.
| Field | Type | Meaning |
|---|---|---|
id, nombre, version
|
string, string, int | identity; version 1 or 2 |
paletas |
map | the nine mandatory palettes (below) |
letras |
map role to font | roles titulo tipo reglas fr pie cinta (below) |
estilos |
map id to style | built-in ids borderless extended classic fullArt plus own ids |
layouts |
map name to full style | complete layouts valid for any style (below) |
colores |
map palette to letter | what replaces {color} in PNG paths; defaults W U B R G, gold to M, artifact to A, land to L, colorless to C (Card Conjurer letters). Balatro maps them to words (white, blue, …) |
origen |
string | where the images come from, shown in the UI |
escalado |
suave / pixel
|
default PNG scaling (below) |
simbolos |
map code to file | own mana symbols (SVG or PNG), shadowing Scryfall's; absent codes fall through |
etiquetas |
map value to label | label pill per value (common uncommon rare mythic special bonus legendary plains island swamp mountain forest wastes) |
insignias |
{fondo, borde, texto, pngLealtad} |
colours of section badges and an optional loyalty-badge PNG |
coloresTexto |
{resalte, habilidad} |
colour of [[highlight]] spans and of ability words in rules. Without it the markers are stripped and everything is normal colour |
sustituir |
map char to char | characters the pack font lacks and their replacements, applied only with this pack (set into Maquetador.sustituir) |
plantillaPie |
map language code to template | the pack's default footer per design language (falls back to en). Used only when the collection's template is the factory default (pie_del_pack). See Texts per language
|
separacionCoste |
float | gap between cost symbols as a fraction of symbol side (default 0.05) |
Each of W U B R G gold artifact land colorless: claro, medio, oscuro (frame highlight, body, shadow; the vertical gradient), tinte (translucent rules box), caja, tinta (light rules box and its ink). Colour choice: one colour gives its palette; two give a gradient of the two palettes; three or more give gold; none give artifact, land or colorless. The user can force another (FrameColor).
familia, peso (default 400), mayuscula: cap height divided by the em body. Text is sized by cap height, which is what the eye sees: font size = hueco.mayuscula(mm) / letra.mayuscula. The family must be loaded in the engine.
A pack font does not need to cover every language. Characters it lacks from Latin («ș», «ł»…) or Cyrillic are taken from Spectral or Montserrat, registered as script fallbacks (Letras::respaldos), instead of painting boxes. Japanese, Korean and Chinese come from Noto Sans JP, KR, SC or TC, which the app downloads the first time one of those design languages is chosen (Letras::cargar_cjk). See Designer-Engine.
nombre and variantes of the styles, the texto of the etiquetas and the plantillaPie are maps language code → text (BTreeMap<String, String> in pack.rs). The codes are the app's (Scryfall's): es, en, fr, de, it, pt (Brazilian Portuguese), ro, pl, nl, ru, uk, ja, ko, zhs (Simplified Chinese), zht (Traditional Chinese). A pack can bring any subset:
-
nombreandvariantesare shown in the interface language; without it, English; without English, the id. -
etiquetasandplantillaPieare painted in the card's design language; without it, English. - A key that is not an app language is never looked up, so it is effectively ignored.
-
{lang}in the footer template is the code printed on real cards in that language (textos_motor::codigo_pie:FR,DE,JP,KR,CS,CT…; Spanish staysES).
The authoring guide docs/packs-de-marcos.md (section «Textos por idioma») documents the same rules for pack authors.
| Field | Meaning |
|---|---|
tamano |
[w, h] mm canvas, default 63 x 88 |
arte |
artwork window (a hueco; accepts box variables) |
caja |
dynamic rules box (CajaDinamica, below) |
cajaPorDefecto |
tinte (translucent, card-coloured, 0.80 opacity) or clara (light, opaque) |
capas |
layers painted over the artwork, in order |
fondo |
layers painted under the artwork (paper of a card whose art does not fill the window) |
huecos |
text slots (below) |
layouts |
style-level layout overrides (ficha, alto, or the ids of variants) |
radio |
screen corner radius in mm (default 3); print is square unless the fine-tuning radius is set |
coronaLegendaria |
legendary cards get the crown automatically; default only for borderless
|
ventanaArte |
art window mask: {x, y, w, h, radio, soloAbajo, repintar, repintarTras}
|
difuminadoArte |
mm of the bottom edge of the artwork faded into black so it does not end abruptly (the built-in extended frame: 12). Fine tuning can override it |
extensionDifuminado |
mm the artwork continues below its window, fading out; goes with difuminadoArte
|
nombre |
display name per language (own styles) |
base |
the built-in style this own style inherits behaviour from (and falls back to if the pack is missing); default classic; must be one of the four |
variantes |
user-selectable variants: id to names per language |
layoutSi |
[{ "si": {...}, "layout": "id" }]: layouts chosen by card properties; first match wins |
CajaDinamica: fondo (fixed bottom edge, mm), altoMin, altoMax, barraTipo (type bar height above the box), padArriba, padAbajo, altoComodo (height up to which the box grows before the font starts shrinking, down to 75 %), fijaDeSerie (boolean, default false: the box is always altoMax unless the user turns on "Height follows the text"; the three Original styles set it). When the text does not fit even at altoMax the font goes down in 0.1 pt steps to 6 pt, then a TextoNoCabe warning is raised.
ventanaArte: black outside the window with corners rounded to radio (or FineTuning.art_radius); soloAbajo rounds only the bottom corners; repintar is a list of [x, y, w, h] zones where an image layer has the curve baked in, and repintarTras the number of layers after which those zones are repainted with artwork or black when the user sets a radius. Zones are clipped so they never start above the bottom of the type bar (yReglas when the box is dynamic): the bar moves down with a short box, and repainting its ends would cut them. The window is painted above the artwork and below the layers.
| Field | Meaning |
|---|---|
svg |
path of an SVG with {{variables}}, drawn over the whole canvas |
png |
path of a PNG; {color} becomes the palette letter, {color1} / {color2} the first and second card colour in WUBRG order (colourless letter if none; both equal with one colour), used for the colour squares near the title |
en |
zone {x, y, w, h} (each may be an expression) the PNG is stretched to; absent means the whole canvas |
tramos |
[top, bottom] mm: those slices (at the scale of the zone width) are drawn without distortion and only the middle stretches (growing boxes with corners) |
desde |
(v3) zone {x, y, w, h} in mm (expressions allowed) measured as if the PNG covered the whole card: only that piece is drawn. Without en it lands on the same spot of the card; with en, stretched to that zone. Used to borrow a piece of another frame (the Original borderless takes the classic's parchment rules box) |
mascara |
(v3) path of a card-sized PNG whose alpha multiplies the layer's alpha (Card Conjurer masks such as m15MaskType.png: only the type bar). With desde, the same piece of the mask is used. No {color}: one mask for every colour. Neither field combines with tramos or SVG layers |
cajaReglas |
true with en and no png/svg: paints the requested rules-box tint there when the design asks for a dark, light or custom box (image frames have the box baked in). The rules text then takes the ink that reads on that tint |
colores |
per-layer palette-to-letter overrides |
si |
condition (below) |
escalado |
suave or pixel for this layer |
mezcla |
normal, oscurecer (Darken, e.g. scanlines) or multiplicar
|
opacidad |
0..1 |
A layer is either SVG or PNG (exactly one) or a cajaReglas marker. A layer file missing in a pack falls back to the builtin pack's file of the same path at paint time.
All present keys must hold (absent keys are ignored). Facts are computed by render::hechos_de and pintar_cara.
| Key | True when |
|---|---|
corona |
the crown is on (legendary rule or forced) |
fr |
the face has power and toughness |
cinta |
the original name goes in the ribbon |
emblema |
an emblem is set |
tipo |
any of these (English, lower case) appears among the words of the English type line: land, creature, planeswalker, artifact, enchantment, instant, sorcery, battle, token… |
basica |
basic land (type line contains basic) or not |
legendaria |
legendary (type line or legendary frame effect) |
colores |
[min, max] number of colours of the face (a colourless land uses its identity) |
simbolos |
[min, max] number of symbols in the mana cost ([3, 3] means exactly three) |
variante |
the user picked that variant |
caja |
(v3) the rules box chosen in the design is one of these: auto, dark, light, tint (Hechos.caja, from FrameOptions.rules_box) |
Required in every style: titulo, tipo, reglas, pie. Used when present: coste, fr, cinta, emblema, etiqueta (label pill), simbolo (full-art colour symbol), mano and vida (Vanguard).
| Field | Meaning |
|---|---|
x y w h |
rectangle in mm (expressions allowed) |
rol |
font role |
mayuscula |
capital height in mm (default 1.78) |
color |
#rrggbb[aa] or a variable such as {{tintaCaja}}
|
alinear |
inicio, centro, fin; on coste, centro centres the symbols |
sombra |
soft shadow behind the text |
versalitas |
small caps (token titles) |
mayusculas |
upper-case the text (symbols untouched) |
alinearV |
vertical placement in a fixed box: inicio, centro (default), fin
|
tabs, interlinea
|
footer only: tab stops (mm) and line pitch (default 1.6) |
contenido |
on etiqueta: rareza (default), supertipo ("Legendary"), subtipoTierra (basic land type) |
The title and type line squeeze horizontally before shrinking; see Designer-Engine. Footers use \n for lines, \t for columns (with tabs each column is squeezed to its tab, otherwise columns follow each other and the whole line is squeezed down to half) and everything after {>} is pinned to the right. If the template has none of {credit}, {universe}, {year} they are appended after {>} on the last line (e.g. "Universe · (c) 2026 Credit").
Two kinds:
-
Style layouts (
EstiloDef.layouts[name], aLayoutDef):arte,caja,capas,fondo,huecos. Fields present replace the style's wholesale (acapaslist replaces the whole list; a hueco replaces that hueco, others stay).-
ficha: tokens, emblems, double-faced tokens (centred small-caps title…). -
alto: taller rules box for section bodies (planeswalker, class, case, leveler, Station). Optional. - the id of a user variant, applied automatically when it is chosen.
-
-
Pack layouts (
PackDef.layouts[name], completeEstiloDefs valid for any style):saga,mazmorra,secuelaArriba(63 x 51),secuelaAbajo(33 x 63),fusionar(88 x 6),flip,batalla(88 x 63),plano(127 x 88.9),planandvanguard(88.9 x 127),pie(footer only). Split, Fuse and Room halves need no layout of their own. Two of them deserve a note, and the builtin, Original and Balatro packs all define them this way:-
mazmorra: like the printed dungeons, only the title (centred) and the map.arteis 0 x 0, there is notipooremblemahueco, and thereglashueco covers the whole area from under the title to above the footer; the engine draws the floor plan (rooms, walls, doors) inside it, on top of whatever the layout's layers paint there (see Designer-Composition). Leaving out the type line works because a pack layout replaces the style completely: there is no fallback to the style's huecos. -
secuelaAbajo: title, an art window, type line and rules, top to bottom; the piece is pasted rotated to the right, so its title ends up along the card's right edge, as on the printed cards. The engine paints the second design face's artwork in that window, like the printed aftermath cards. A pack whosesecuelaAbajokeeps a 0 x 0artesimply leaves that half without artwork.
-
Resolution order (render::estilo_para): own style from the pack (else warning EstiloNoEnElPack and the built-in style), else the pack's built-in-style entry, else the builtin pack's, else the pack's borderless. Layout request priority: the card's own request (ficha, saga…), then the chosen variant if the style has a layout with that id, then the first matching layoutSi. A missing token layout falls back to the builtin pack's style silently; any other missing layout falls back to the builtin pack's complete layout and, if none exists, PackSinLayout is reported (not for alto).
suave (default): Lanczos3, for painted or photographed frames. pixel: when enlarging, nearest-neighbour so every pixel is repeated; when reducing, box-filter average (render::reescalar_pixel, via fast_image_resize). Set at pack level or per layer; own symbols follow the pack flag. This is what keeps pixel art crisp at 300 to 900 DPI.
Location: src-tauri/crates/proxy-core/assets/marcos/builtin/, embedded with include_str! through the fichero! macro (BUILTIN list in pack.rs), so it is part of the binary. Pack::de_fabrica() builds it; Pack::de_fabrica_compartido() caches one copy for fallbacks.
- Id
builtin, version 1, nine palettes, fonts Spectral (titles, type, rules, P/T, ribbon) and Montserrat (footer). - Four styles:
borderless(art 63 x 83 with a growing rules boxfondo 83, altoMin 12, altoMax 27.4, barraTipo 6, art window with a 6 mm radius rounded at the bottom, crown, ribbon, title bar and P/T SVGs; layoutsficha,alto),extendedandclassic(frame SVGsmarco.svg/marco-alto.svg; layoutsficha,alto),fullArt(landscape basics: strip plus colour symbol, no layouts). - Eleven pack layouts:
saga,mazmorra,secuelaArriba,secuelaAbajo,fusionar,pie,flip,batalla,plano,plan,vanguard. - Because every other pack falls back to it, it must keep working as the reference implementation of the format.
- Tests that guard it:
dorados.rs(golden images),layouts.rs, and the unit tests inpack.rsandrender.rs(un_pack_de_antes_sin_el_layout_usa_el_de_la_casa,un_estilo_que_falta_sale_del_pack_de_la_casa, etc.).
Decision: the M15 image frames from the Card Conjurer project are not shipped in the repository or installers. They are downloaded on first use and verified.
- Definition:
assets/marcos/original/pack.json(idoriginal, version 3,origen= Card Conjurer), four SVGs (cinta.svg,cubierta-corona.svg,refuerzo-caja.svg,sombra-pie.svg) andimagenes.json:{ commit, base, imagenes: { path: sha256 } }. Currently 53 PNGs (about 37 MB) underm15/regular(8 frames, 8 P/T, masksm15MaskTitle.pngandm15MaskType.png),m15/new/extended(8),m15/borderless(9),m15/crowns(18), pinned to a 40-hex commit of the public repository, baseimg/frames/. - Three styles that share the classic's marble bars and parchment, so a deck looks consistent whichever style each card uses. Decision: Card Conjurer's own extended and borderless frames are smoother (dark translucent type bar, translucent plates with no grain), so the pieces that carry the texture are borrowed from the regular frame with
mascaraanddesde:-
classic: regular M15 frame (m15/regular/m15Frame{color}.png),cajaPorDefecto: clara. -
extended:m15/new/extended/{color}.png(lettersw u b r g m a l; colourless usesalike the classic usesA), art from y 7.4 mm to under the type bar, full width; the regular frame masked withm15MaskType.pngon top for a marble type bar; the floating crown (there are no side borders under the title for the regular crown to rest on); the classic's P/T box and text colours. With the legendary crown (si.corona) the top piece of that PNG (frame rim) would peek out around the crown, so the top 12 mm are drawn in pieces: the side glows (x0–2.3 and 60.7–63), a one-column stretch of each glow over the gap next to the crown (desde0.1 mm wide,en1.1 mm wide, so the glow continues without the rim), and the middle (3.4–59.6); the same layer without corona (si.corona: false) is drawn whole. Without this, thin vertical slivers and a faint column showed beside the crown on legendary cards (most visible on planeswalkers). -
borderless: Card Conjurer's generic showcase frame (ventanaArtewithrepintarzones andrepintarTras: 4), with the regular frame masked bym15MaskTitle.pngandm15MaskType.pngfor the bars, the regular frame's rules-box interior laid on the showcase box withdesdealone (the opening between the box borders is at the same place in both images, x 4.72–58.38 mm and y 55.2–81.4 mm, so the piece goes on its own spot without stretching; anentaken from thecajaReglaszone was 0.2 mm off and left a gap on one side), the classic's P/T box, dark text andcajaPorDefecto: clara. The parchment layer has"si": {"caja": ["auto", "light"]}: with a dark or tinted box it is skipped, so the frame's own translucent box stays and thecajaReglastint goes over it, keeping the artwork visible through the box (and the "more opaque rules box" fine-tuning meaningful). The box border always comes from the frame. - All three styles define a
cajawithfijaDeSerie, so the rules box is fixed ataltoMaxby default and follows the text height like the house borderless only when the user turns on "Height follows the text". The box is baked into the PNG at a fixed height, so the pack rebuilds it in three slices withdesde+enand expressions overyTipo: top (with the type bar) and bottom copied as they are, and the middle stretched between them (borderless: bar48.5–55.4, middle55.4–80placed atyTipo+6.9with height73.1 - yTipo, bottom80–88, plus corner strips so the sides are not stretched).cajaReglasisen {4.91, "yTipo+6.55", 53.43, "74.34 - yTipo"}andrefuerzo-caja.svguses{{yReglas}}/{{altoReglas}};ventanaArte.repintarTrasis 9 in borderless. Basic lands use the pack's ownfullArtstyle: the borderless frame's marble title bar (the showcase title slicey 3.0–10.3plus the regular frame throughm15MaskTitle.png, moved toy 72.7) — the title bar and not the type bar, because the type bar's slice is open at the bottom where the rules box joins it — over a full-bleed art window down to 81 mm with 3 mm rounded bottom corners (ventanaArte, norepintarand no corner strips, which belong to the rules box), with the name centred in the bar and the mana symbol inside it at the left; without its ownfullArta pack falls back to the house one (dark strip). The pack defines all its own pack-levellayouts(saga, clase, mazmorra, flip, secuelaArriba/secuelaAbajo, fusionar, pie, batalla, plano, plan, vanguard), built from pieces of its PNGs (three-slice bars from the title bar slice, nine-slice rules boxes from the regular frame, same marble and colour borders), plus analtolayout in each style (rules box up to 34.6 mm) so planeswalkers, cases and levelers fit (none of the special layouts falls back to the house look); class cards use aclaselayout (art left, levels right) and fall back toaltoin packs without one. Balatro does the same from its Joker pieces (balatro/source/layouts.py, called bybuild.py). TheprepareScryfall layout (a prepared creature with its spell, e.g. Emeritus of Conflict) isMaquetacion::Preparar: the adventure body with the spell column on the right (Contenido::Aventura.libro_derecha).
-
- Download (
original.rs): URL =https://raw.githubusercontent.com/Investigamer/cardconjurer/<commit>/<base><path>.descargaralways rewrites the definition files with the version embedded in the app, lists the images whose on-disk SHA-256 does not match (pendientes/bueno), fetches those one by one, rejects any file whose SHA-256 differs (ErrorCode::Networkwithfichero), writes atomically intoapp_data_dir/marcos/original/, reports progress, and callsrecargar_pack("original"). Progress event:original://progresswith{ done, total }.info()returnsOriginalInfo { listo, faltan, total }. -
leer_packscallsoriginal::definicion_al_diaon every start so the pack definition on disk always matches the app version even if the images were downloaded long ago. - A newer app may ask for images an older one did not download (the extended style's).
leer_packsandrecargar_packload this pack withoriginal::cargar: if the full pack fails, it keeps only the styles that load on their own, so the missing ones fall back to the builtin pack instead of the whole Original pack disappearing. At startuplib.rsspawnsoriginal::completar, which downloads whatever is pending when the folder already has apack.json(never on a fresh install: that stays opt-in). Without network it only logs a warning and retries on the next start or when the user picks the pack. - Tests (
original.rs): the list is well formed (40-char commit, trailing-slash base, more than 40 images,.pngpaths without.., 64-char hashes); a file with another hash is rejected; an old definition is rewritten; nothing downloaded means all pending; without the newer images the pack loads without the extended style (sin_las_imagenes_nuevas_carga_sin_ese_estilo).en_vivo(ignored) downloads for real. Inproxy-core,el_pack_original_es_valido_con_sus_imageneschecks the image list is exactly what the layers ask for, masks included.
descargables.rs + assets/packs-descargables.json deliver packs hosted in the separate packs repository. Unlike Original, everything (including pack.json) comes from that repo.
Manifest shape (Manifiesto { repo, packs: Vec<Descargable> }):
| Field | Meaning |
|---|---|
repo |
https://raw.githubusercontent.com/PoolFlamingo/proxy-builder-packs |
packs[].id, nombre
|
pack id (also the install directory) and display name |
commit |
40-hex commit of the packs repo; every URL is <repo>/<commit>/<carpeta><path>
|
carpeta |
pack folder inside the repo (balatro/) |
ficheros |
map relative path to SHA-256: pack.json, layers/, symbols/, labels/ (128 files for Balatro) |
letras |
fonts: ruta, sha256, licencia, licencia_sha256
|
extras |
extra images: ruta (prefixed extras/), sha256, tipo (reverso, ficha, tierra, carta), nombre (137 for Balatro: 82 ready cards, 50 lands, 3 tokens, 2 backs) |
Behaviour:
-
lista(state)returnsDescargableInfo { id, nombre, listo, faltan, total, bytes, extras }for each pack that isdisponible: its commit must be a real 40-hex hash (publicado), or, in debug builds only (cfg!(debug_assertions)),PB_PACKS_LOCALmust be set.listomeans everything on disk matches its hash and the pack is loaded;extrasis only listed once ready. -
descargar(state, id, progreso): computes pending tasks (files plus extras whose on-disk hash differs) and missing fonts (font id equals file hash, compared with the imported fonts), downloads each throughstate.externo.bajar(url, MAX_IMAGEN)(15 MiB cap), verifies the hash (comprobar; a mismatch isErrorCode::Networkwithfichero), writes atomically, imports fonts withimportar_letra_datosand stores each licence underfuentes/licencias/<stem>-LICENCIA.txt, thenquitar_lo_que_sobradeletes anything in the pack and extras folders that the new manifest no longer lists (old paths, leftover.partfiles), and finallyrecargar_pack. A second run with nothing changed downloads nothing (total == 0), so upgrading to a new pack version fetches only changed files. - Locations: pack in
app_data_dir/marcos/<id>/, extras inapp_data_dir/packs-extras/<id>/(kept apart so they do not load with the pack). - Extras:
extra_por_indice(bytes to display, hash re-checked),usar_extra/ruta_extra(to the blob store so the extra can be a card back or a custom card; thepack_extra_usecommand). - Dev override: with
PB_PACKS_LOCAL=<folder of the packs repo>(debug builds only), files are read from that folder instead of the network; hashes are still checked. - Events:
packs://progresswith{ id, done, total }. UI:src/features/disenar/packs-descargables.ts(usePacksDescargables). - Tests:
el_manifiesto_se_entiende(repo URL shape,pack.jsonpresent, 64-char hashes, and each file URL passes the allowlisturl_permitida),una_huella_que_no_cuadra_no_vale,lo_de_una_version_anterior_se_quita; ignoredbalatro_en_vivodownloads and checks 7 own styles.
The only downloadable pack so far (id: balatro, version 2, escalado: pixel). Pixel-art frames themed after the game, built from seven PSD templates, each exposed as an own style: joker, planeswalker, espectral, tarot, planeta, vale, tierra (the ids stay in Spanish because saved designs refer to them; display names come from the pack's per-language nombre maps). Six inherit from classic, tierra from fullArt. It uses:
-
layouts: every style has afichalayout;tierraalsobasica, selected bylayoutSiwhen the card is a basic land. - 31 own symbols (
W U B R G C T,0to20,X Y Z) drawn as poker chips, withseparacionCoste0.122. - 13 label pills (rarities,
legendary, basic land types) with the text written by the engine in the design's language. -
coloresTexto(highlight and ability-word colours),insignias(includingpngLealtad),sustituir(8 typographic characters mapped to ASCII for the pixel font),plantillaPie(single-line footer) and a dynamic rules box withaltoComodoin six of the seven styles (planeswalkeruses a fixed box). - The font
m6x11plus.ttf(free to use with attribution; licence file shipped and stored on the user's machine). - Variants are supported by the format, but the current
pack.jsondeclares none. - Extras: two card backs, tokens, basic lands and ready-made cards of the sample deck.
When the manifest moves to a new commit of the packs repository, an installed pack shows the changed files as pending (faltan) until the user downloads it again from "Fonts, frames and symbols" ("Letras, marcos y símbolos"); a change that only touches the layouts is a single file, pack.json. Inferred: until then the installed copy keeps loading with its previous layouts, because packs are read from disk whatever the manifest says.
Credits are in the pack's CREDITS.md (templates by a community artist redistributed with permission; Balatro belongs to its publisher; the cards are proxies for playing, not selling).
Pack content is built and versioned in a separate repository, cloned next to this one (proxy-builder-packs, published as PoolFlamingo/proxy-builder-packs). It has its own working sessions and history, and the app only ever sees it through the manifest. Layout of the repo:
proxy-builder-packs/
README.md
balatro/
pack.json the pack definition
layers/<template>/ PNG layers cropped to the card area (common, joker, land, planeswalker, planet, spectral, tarot, voucher)
symbols/ mana chips
labels/ rarity, legendary and land-type pills
fonts/ m6x11plus.ttf and its licence
extras/{cards,tokens,backs,lands}/ ready-made images
extras.json inventory of extras (type and display name)
CREDITS.md
source/ convert.py, build.py, manifest.py, inventory.json/.md, references.json, views/
Folders and file names are in English; the style ids and JSON keys stay in Spanish because the app and saved decks read them.
Workflow (from the repo README, run inside balatro/source/ in a virtualenv with psd-tools, pillow and numpy; always python -I):
-
convert.py <folder with the PSDs> ..: exports every image layer (except the sample art, text and swatches) cropped to the card area of a 1365 x 1855 canvas, with each layer's zone in mm, and writesinventory.json/inventory.md. Text layers are recorded with box, size, colour and alignment. The visible layers are recomposed and compared with the PSD's own composite as a check. -
build.py .. <folder with the original extras> <m6x11plus.ttf>: generates symbols, labels, thelayers/commoncolour boxes, copies fonts and extras (cropped without the 3 mm bleed) and writespack.json. It deletes the layers it does not use, so runconvert.pyagain before re-running it. -
manifest.py .. <commit>: prints the app's manifest entry for the pack (every file's SHA-256, the font list with licences, extras withextras/prefix and hashes). It skipssource/,extras/,fonts/,golden/,extras.json,README.md,CREDITS.mdfromficheros.
Publishing a new pack version:
- Commit and push the pack changes to the packs repo (the commit must exist on the remote because the app downloads by commit).
- Run
manifest.pywith that full commit hash and replace the pack's entry insrc-tauri/assets/packs-descargables.json. - Run the app's tests (
el_manifiesto_se_entiende) and, optionally, the ignoredbalatro_en_vivo. - Ship an app release; players get the new files the next time they open the pack download (only changed files are fetched).
During development use PB_PACKS_LOCAL with a placeholder (non-40-hex) commit in the manifest to test before pushing. The engine-side tests that need the pack contents (balatro_con_sus_imagenes, balatro_matriz, balatro_contra_el_autor) take the pack folder through PB_BALATRO (see Designer-Engine).
src-tauri/assets/letras-libres.json lists 22 OFL-licensed families from the Google Fonts repository, pinned to a commit, with the SHA-256 and size of each file and of each licence. They are downloaded by a button in the resources dialog, are not in the repo or the installers, and are imported like any user font.
Entry (Familia): id, familia (the name used in collections and packs), uso (UsoLetra: titulo, reglas, ambos), cursiva (has an italic, used for reminder and flavour text), ficheros: [{ ruta, sha256, bytes }], licencia: { ruta, sha256 }. Root: commit and base (ofl/).
Selection rule: legible at card sizes, no hairline strokes. Families: Cinzel, Almendra, Uncial Antiqua, Grenze Gotisch, Eczar, Amarante, Metamorphous, Inknut Antiqua, Bitter, Domine (titles); Grenze, Caudex, Cardo, Vollkorn, Libre Baskerville, Literata, Merriweather, Lora, Pixelify Sans (both; Pixelify Sans is the free alternative to the Balatro font); Crimson Pro, Source Serif 4, PT Serif (rules). Preview images are in src/assets/letras/<id>.png.
Flow (letras_libres.rs):
-
lista(state):LetraLibre { id, familia, uso, cursiva, descargada, bytes };descargadawhen every file's SHA-256 is already among the imported fonts (font id equals file hash). -
descargar(state, id): for each file not yet present, fetchhttps://raw.githubusercontent.com/google/fonts/<commit>/ofl/<ruta>, require the pinned SHA-256, import withimportar_letra_datos(datos, "ttf")(stored asfuentes/<sha256>.ttf), then fetch and verify the licence and write it tofuentes/licencias/<id>-OFL.txt. Any mismatch aborts withErrorCode::Network. - Tests:
el_catalogo_esta_bien_escrito(40-char commit, at least 10 families, unique ids, each path starts with its id and ends.ttfwith no.., 64-char hashes, licence endsOFL.txt),sin_descargar_ninguna_esta; ignoredlos_ficheros_son_los_del_catalogo(needsPB_GF, a local copy of the Google Fonts repo; checks hashes and that each file registers the declared family) anden_vivo. - Adding a family: copy its files and licence from the Google Fonts repo at the (pinned or a new) commit, compute SHA-256 and size, add an entry, add a preview PNG, run the ignored hash test with
PB_GF. If the commit changes, every entry must still match that commit.
-
Pinned content. All remote pack content is addressed by a full commit hash and verified by a SHA-256 stored in the app binary (
imagenes.json,packs-descargables.json,letras-libres.json). A force-pushed branch, a changed file or a tampered response is rejected (ErrorCode::Network), so a compromised host cannot inject images or fonts. -
Atomic writes. Files are written through
escribir_atomico(temp then rename), so a crash leaves no half file that could pass as complete; the next run re-checks hashes. -
Allowlist for the packs repo.
descargables.rsgoes throughstate.externo(fuentes::Externo), which only accepts the closed listPERMITIDOSinsrc-tauri/src/fuentes/mod.rs(the packs repo entry israw.githubusercontent.comwith path prefix/PoolFlamingo/proxy-builder-packs/), re-checks every redirect and caps each download atMAX_IMAGEN(15 MiB). A unit test asserts every manifest URL passes the list. -
Original and fonts.
original.rs,letras_libres.rsandletras_cjk.rs(the CJK fonts, cataloguesrc-tauri/assets/letras-cjk.json, see Designer-App-Layer) use their ownreqwestclient with compile-time fixed repository URLs (Card Conjurer, Google Fonts) and the pinned hashes, not theExternoallowlist. Their URL prefixes are not user-controllable. -
Web view. Pack content is fetched by Rust; the web view never goes to the network (its CSP in
tauri.conf.jsononly allowsconnect-srcto the IPC endpoints), it only displays rendered images viapbimg://. See Architecture and External-Sources. -
User packs. Only
.json,.svg,.pngare read; size caps apply to zips; the id is sanitised andbuiltinis reserved; paths containing..are skipped. A user pack is trusted to the extent that its SVGs are rasterised byresvg; Inferred: a hostile SVG can at worst produce wrong pixels or waste CPU, as no script engine is involved. -
Licensing. Downloaded fonts keep their licence text next to them in
fuentes/licencias/.
docs/packs-de-marcos.md is the maintained authoring guide in Spanish. Statements checked against the code on this page: pack version rules (1 or 2), required palettes and huecos, si keys, layout names and sizes, PB_PACKS_LOCAL. Small drift noticed: the guide says the Original pack ships "three own SVGs" while ORIGINAL in pack.rs lists four; the module comment of descargables.rs mentions fuente/manifiesto.py while the script in the packs repo is source/manifest.py.
| Goal | Start at |
|---|---|
A new field in pack.json
|
pack.rs (add with #[serde(default, skip_serializing_if…)], bump VERSION_PACK only if an old app would paint it wrongly), consume in render.rs, document in docs/packs-de-marcos.md and here |
A new si condition |
Condicion, Hechos and Condicion::cumple in pack.rs, fill Hechos in render::hechos_de
|
| A new downloadable pack | build it in the packs repo, add its manifest entry, ensure its URL prefix is allowed in PERMITIDOS
|
| A new pack layout | add it to builtin pack.json and its SVG, add a case in tests/layouts.rs (Designer-Composition) |
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
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
- 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
Frontend · Frontend
- Frontend-Architecture · Arquitectura del frontend
- State-Stores · Stores de estado
- Internationalization · Internacionalización
- Themes-and-Skins · Temas y skins
- 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
- Set-Browser · Explorador de sets
Reference · Referencia