Files
Voiced/docs/templates/04-linea.md
T
xavierk 04d92aa149 Add the Neutral template family, template picker and text-only logo allowances
Linea (the vendor's original format.pdf design), Monogram and Serenity are
rebuilt from the Canva PDFs as one parametric family on the engine, with
the user's logo in the measured slot, a home for every GST field and
A4/Letter support. Adds static Montserrat, Open Sans and Poppins fonts and
a generated Ms Madi outline for Serenity's vertical word. Text-only logo
placeholders (Monolith, Tangerine) use a doc-derived allowance box. A
Carbon TemplatePicker with committed thumbnails opens from the page
setup controls.
2026-10-04 07:27:12 +05:30

159 lines
19 KiB
Markdown

# Template 4: Linea (could also be labelled 'Original': this is the vendor's own format.pdf design) (`linea`)
Source: `templates/Modern Neutral Invoice Template/4.pdf`. All coordinates are in points from the top-left of the Canva page (595.5 x 842.25). Measured from the PDF content streams during design analysis.
## Style
Airy, all-charcoal minimalism. A very large letter-spaced Montserrat 'INVOICE' is followed by a hairline rule. Everything else is 10pt Montserrat. The item table has no borders between two hairlines, and a script signature sits bottom-right. There is no logo, no colour and no boxes.
## Fonts
- Montserrat Regular (400): the title 'INVOICE' is 38.01pt, letterSpacing 7.6pt (0.20em), uppercase. All other regular text is 10pt, letterSpacing 0.5pt (0.05em), with a 13.5pt line pitch (lineHeight 1.35). That covers client lines, bank lines, DATE:/DUE DATE: labels and values, the table header (uppercase, NOT bold), item rows, the SUBTOTAL label and value, and 'Tax' with its rate.
- Montserrat Bold (700), 10pt, letterSpacing 0.5pt: 'ISSUED TO:', 'PAY TO:', 'INVOICE NO:' and its value, and the grand-total label 'TOTAL' and its value.
- BuongiornoRastellinoCyr-Script, 60.02pt, no tracking: the signature 'DanielGallego'. It is live text, not an image. The font is Canva-licensed and cannot be redistributed, so replace it with the uploaded signature image.
- Metrics from the embedded descriptors: Montserrat ascent 968, descent -251, capHeight 700 per 1000 em. Cap height is 7.0pt at 10pt and 26.6pt at 38pt.
- Montserrat is OFL. The local copies ~/.local/share/fonts/Montserrat-Regular.ttf and Montserrat-Bold.ttf are Version 9.000 and include U+20B9 (₹), which I checked by parsing the cmap. Bundle these two TTFs plus OFL.txt in public/fonts.
## Colours
Single colour only. All text and every rule is #3D3B3A (PDF rg 0.2392 0.2314 0.2275 = rgb 61,59,58, a warm charcoal). There are no other greys and no accent. The page background is #FFFFFF, set by an explicit full-page white fill. Canva also writes three invisible (alpha 0) tagged rectangles that duplicate the rules for accessibility. Ignore them.
## Layout
Page is 595.5 x 842.25pt (Canva's A4). In react-pdf use A4 (595.28 x 841.89); the difference is under 0.05%. y is measured from the top edge.
**Grid:** hairline rules span x 69.64-525.86 (456.2pt wide, centred on the page). Body text starts at x 77.4, 7.8pt inside the rule ends. The right text edge is 519.2 for headers and meta values, and 514.4 for money values. Implied margins: left/right 69.6 for rules; 77.4 (left) and 76.3 (right) for text; top 135 to the title cap-top; bottom about 70 below the signature ink.
1. **Empty top band**, y 0-135.
2. **Title row**, y 135-162. 'INVOICE' spans x 59.55-270.2, baseline 161.65, cap-top 135.0. It starts 18pt LEFT of the text column and 10pt left of the rule start. A 0.75pt hairline runs from x 290.05 to 535.57 at y about 148.7, the vertical middle of the capitals, with a 20pt gap after the title. The rule overshoots the table's right edge by about 10pt, mirroring the title's overhang, so the row (59.5-535.6) is centred on the page.
3. **Empty space**, y 162-262 (about 100pt).
4. **Info row**: two columns sharing the first baseline 269.22.
- Left, at x 77.4: 'ISSUED TO:' (Bold) at baseline 269.22, then 3 lines at 285.19 / 298.69 / 312.20. That is 15.97 from label to first line, then a 13.5 pitch.
- Right, meta block with 2 sub-columns. Labels are right-aligned at x about 447: 'INVOICE NO:' (Bold) x 376.2-447.7, 'DATE:' 414.6-446.6, 'DUE DATE:' 387.3-446.8. Values are right-aligned at x about 518.1-519.2: '01234' (Bold) 487.8-519.2, dates 465.3-518.1. Baselines are 269.22 / 284.69 / 299.40. The label-to-value gap is about 18.7pt. The meta block spans x 376-519 (143pt).
5. **PAY TO**, left column at x 77.6: 'PAY TO:' (Bold) at baseline 337.73, 25.5pt below the last client line. Then 3 lines at 353.98 / 367.49 / 380.99.
6. **Empty space**, y 381-441.
7. **Items table**, x 69.64-525.86. Header baseline 447.93; rule at y 461.15; rows start at baseline 484.25 with a 21.46 pitch (7 rows, last at 613.33); closing rule at y 631.44.
8. **Totals**, no box. SUBTOTAL at baseline 649.82, with the label at x 77.45 and the value right-aligned at 514.4. 'Tax' (baseline 665.04) and 'TOTAL' (baseline 680.09) form a right-anchored label/value pair: labels right-aligned at 460.3, values right-aligned at 514.4, 15pt pitch.
9. **Signature**, bottom-right: script text x 352.7-526.2, baseline 757.37, ink about y 712-772.
10. About 70pt empty at the bottom. No header, footer or page number.
## Items table
- **Columns (x):**
- DESCRIPTION: left-aligned; header at 77.45, cells at 81.6. The 4pt indent is a Canva artefact and optional.
- UNIT PRICE: centred on x 325.2 (header 293.6-356.8; cell '100' 316.3-334.2).
- QTY: centred on x 416.4 (header 405.7-427.1).
- TOTAL: right-aligned; header right edge 519.2, money cells right edge 514.4.
- Unit price shows a bare number; the currency symbol appears only in TOTAL.
- **Header:** Montserrat Regular 10pt, UPPERCASE, letterSpacing 0.5pt, #3D3B3A. No fill, no top border. ONE 0.75pt #3D3B3A rule 13.2pt below the header baseline (y 461.15), spanning x 69.64-525.86, which is 7.8pt beyond the text on both sides.
- **Body:** Montserrat Regular 10pt, letterSpacing 0.5pt. First baseline is 23.1pt below the rule. Pitch is 21.46pt (13.5 line plus about 4pt padding top and bottom). No row dividers, no zebra, no column borders. A closing 0.75pt rule sits 18.1pt below the last baseline (y 631.44).
- **Numeric alignment:** unit price and qty centred, amount right-aligned. The sample has no decimals.
- **Suggested Voiced widths** (440.6-441.8pt between text edges): description flex:1 (about 210pt, wraps); rate 96pt centred; qty 48pt centred; amount 86pt right. Column centres stay within about 12pt of the design while fitting '₹1,500.00/hr' and '₹1,00,000.00'.
## Totals and payment
- **SUBTOTAL:** a full-width row. The label 'SUBTOTAL' (Regular, caps) sits at the left text edge, x 77.45. The value (Regular) is right-aligned at 514.4. Baseline 649.82, 18.4pt below the closing rule.
- **Tax row:** 'Tax' label right-aligned at 460.3, value '10%' right-aligned at 514.4, baseline 665.04. The value is the RATE only; the sample never prints a tax amount.
- **Grand total:** 'TOTAL' (Bold) right-aligned at 460.3 and '$770' (Bold) right-aligned at 514.4, baseline 680.09. There are no rules, boxes or size change; only the weight sets the total apart.
- **Payment info:** NOT at the bottom. The 'PAY TO:' bank block sits in the left column directly under 'ISSUED TO:', with bank name, 'Account Name: …' and 'Account No.: …'.
- **Signature:** a script-font name bottom-right (60pt, right edge 526.2).
- No notes, terms, thank-you line or footer.
## Decoration and images
- **Title hairline:** 0.75pt #3D3B3A, x 290.05-535.57, y about 148.7, the vertical centre of the 38pt capitals.
- **Table hairlines:** two 0.75pt #3D3B3A rules, x 69.64-525.86, at y 461.15 and 631.44.
- **No raster images at all.** The 42 KB file is just three font subsets. The signature is live text in BuongiornoRastellinoCyr-Script.
- Canva wraps everything in one Form XObject scaled by 0.24 (2482x3510 design units). This doesn't matter for the re-implementation.
- **Fully reproducible in react-pdf** with Views: height 0.75 plus backgroundColor, or borderBottomWidth 0.75.
## Mapping to Voiced fields
**Design slots mapped to Voiced fields:**
- **Title 'INVOICE':** 'INVOICE', or 'TAX INVOICE' when taxType !== 'none'.
- **ISSUED TO block:** clientName; clientAddress (wrap, keep line breaks); 'GSTIN: {clientGstin}' only if set; 'Place of Supply: {state name} ({placeOfSupplyStateCode})'.
- **Meta block (right):** 'INVOICE NO:' with the number (Bold); 'DATE:' invoiceDate; 'DUE DATE:' dueDate, omitting the row if empty; an optional 'PO NO:' poNumber row.
- **PAY TO block:** bank.bankName; 'Account Name: …'; 'Account No.: …'; then 'IFSC: …', 'Branch: …' and the planned 'UPI: …'. The block grows from 3 to 6 lines.
- **Table:**
- Description.
- UNIT PRICE: in rate mode, money(rate) plus a short unit ('/hr', '/min', '/sec', '/session', '/unit'); otherwise '—'.
- QTY: quantity in rate mode, else 1.
- TOTAL: computeLineAmount.
- **Tax row:** replace '10%' with real rows that carry amounts: 'CGST @ 9%' and 'SGST @ 9%', or 'IGST @ 18%'. Put a 'DISCOUNT' row (when greater than 0) before the tax rows, styled like them.
- **Signature slot:** the signature image, in a box of about x 352-526 / y 712-772, max 170x56pt, right-aligned, with 'Authorised Signatory' at 8pt. With no image, print 'Computer-generated invoice; no signature is required.' at 8pt, right-aligned, in the same spot. Never render the vendor name in a script font as a stand-in signature.
**Voiced fields with NO slot, and where to put them:**
- **Vendor name, address, phone, email, GSTIN, PAN.** The design has no vendor identity at all. Add a 'FROM' block right-aligned under the title rule, in the empty band x 290-535 / y about 170-255. Name in Bold 10pt; lines in Regular 9.5-10pt at a 13.5 pitch; 'GSTIN … · PAN …' on one line; maxWidth 245pt. It is in its own row below the title, so a long address can only push content down. It can never overlap the title, which is the reported header bug.
- **Vendor logo:** optional, in the empty top band above the title (x 59.5, y about 56-120, max 150x48pt).
- **Amount in words:** on the left side of the totals block (x 77.4, width at most 300pt) beside the Tax/TOTAL rows, or full width under TOTAL. 9pt, with the label 'AMOUNT IN WORDS' in Bold 8pt caps.
- **Notes:** full width below the amount in words, above the signature row.
- **HSN/SAC:** not in the data model. If added, use an 8pt second line under each description ('SAC 998314') or a 44pt column between DESCRIPTION and UNIT PRICE.
- **Footer:** 'Generated with Voiced — a Bongbetic product' plus 'Page n / N', fixed, 7pt, #3D3B3A, at y about 818.
## Overflow risks
- **Title:** 'TAX INVOICE' at 38pt with 7.6pt tracking is about 326pt wide, ending near x 386. That is 96pt past the fixed rule start (290). The rule must be a flex filler (minimum about 40pt), or the tracking must drop to about 0.12em for 'TAX INVOICE'.
- **Meta block:** only 143pt wide (x 376-519). A number like 'INV/2026-27/0001' in Bold 10pt with 0.5pt tracking is about 105pt, plus the 72pt label and 18pt gap, about 195pt in total. The block must grow leftwards: give the client column flex:1 with at least a 24pt gap, and the meta column flexShrink:0.
- **Client and bank column:** about 260pt from x 77.4 to the meta block, so long names and addresses wrap at roughly 45 characters per line. PAY TO grows to 6 lines (+40pt) and pushes the table down.
- **UNIT PRICE column:** about 70pt in the design, too narrow for '₹1,500.00 per hour' (about 100pt). Use short units or a second 8pt line.
- **Totals values:** they sit in only a 54pt gap after the labels (460.3 to 514.4). '₹12,34,567.89' (about 85pt) would run into 'CGST @ 9%'. Use a fixed value column of about 90pt; labels then end around x 420.
- **Descriptions:** they wrap at about 210pt (about 36 characters). The fixed 21.46 pitch must become padding (4pt top and bottom) so multi-line rows grow.
- **Rows per page:** page 1 holds about 7-8 rows before the totals and signature (about 150pt) spill to page 2. That needs a repeating header and totals/signature blocks that never split.
- **Amount in words:** up to about 100 characters, so 2 lines at a 300pt width.
- **Hyphenation:** react-pdf hyphenates by default. Register a no-op hyphenation callback so GSTINs, account numbers and URLs never get a stray hyphen.
## US Letter adaptation
- **US Letter (612x792):** 16.5pt wider and 50.25pt shorter than A4.
- **Horizontal:** keep the 69.6pt rule margins with the 7.8pt text inset, so content width grows from 456.2 to 472.8. Anchor meta, totals and signature to the right margin; anchor ISSUED TO, PAY TO and the table's left edge to the left margin. The title overhang (-18pt) and rule overshoot (+10pt) stay relative to the margins.
- **Vertical:** take the 50pt from the empty space. Cut the top band from 135 to about 100 before the title cap-top, and the title-to-ISSUED TO gap from about 100 to about 80. Nothing else changes, and the 7 sample rows still fit on Letter page 1.
- **Configurable margins:**
- Express every x as marginLeft + offset, or pageWidth - marginRight - offset. Keep the 7.8pt text inset inside the rule margin.
- Clamp the title (-18pt) and rule (+10pt) overhangs to stay at least 12pt from the paper edge.
- Express vertical anchors as marginTop + offset. Content flows, so smaller margins simply give more rows per page.
## react-pdf notes
- **Fonts:** Font.register({family:'Montserrat', fonts:[{src:'/fonts/Montserrat-Regular.ttf', fontWeight:400},{src:'/fonts/Montserrat-Bold.ttf', fontWeight:700}]}). Page style: {fontFamily:['Montserrat','IBM Plex Sans'], fontSize:10, letterSpacing:0.5, lineHeight:1.35, color:'#3D3B3A'}. Array fallback in fontFamily is supported in react-pdf v4; I checked the docs.
- **Page:** size A4 or LETTER, paddingHorizontal 77.4 (the text column). Rules use marginHorizontal -7.8 to reach 69.6 and 525.9.
- **Header:** the optional logo band, then the title row: View {flexDirection:'row', alignItems:'center', marginLeft:-18, marginRight:-16} holding Text {fontSize:38, letterSpacing:7.6} and a rule View {flex:1, height:0.75, backgroundColor:'#3D3B3A', marginLeft:12.4}.
- marginLeft is 12.4 because react-pdf, like CSS, adds the 7.6pt tracking after the last glyph; 12.4 + 7.6 gives the design's 20pt visual gap.
- For Montserrat the middle of the text box is within 0.3pt of the cap middle, so alignItems 'center' places the rule correctly.
- **Vendor FROM block:** a separate View {alignSelf:'flex-end', maxWidth:245, marginTop:≈10} with textAlign right.
- **Info row:**
- Row View {flexDirection:'row', marginTop:≈100 for the design's empty space}.
- Left View {flex:1, paddingRight:24}; right meta View {flexShrink:0}.
- Each meta row is a row View: label Text (textAlign right, width 90) and value Text (textAlign right, minWidth 54, marginLeft 18).
- PAY TO is an in-flow View with marginTop 17 under ISSUED TO.
- **Table:**
- Header row: View {flexDirection:'row', paddingBottom:8.5, borderBottomWidth:0.75, borderBottomColor:'#3D3B3A', marginHorizontal:-7.8, paddingHorizontal:7.8}. Give it the `fixed` prop so it repeats on continuation pages. Verify this; if it doesn't repeat, draw it on later pages through the page-level render prop.
- Rows: View {flexDirection:'row', paddingVertical:4} with wrap={false}.
- The table container's borderBottomWidth 0.75 plus paddingBottom 9 gives the closing rule.
- **Totals:** View wrap={false}, with minPresenceAhead about 60 on the preceding element so the totals never end up alone on a page.
- SUBTOTAL row: justifyContent 'space-between'.
- Then View {alignSelf:'flex-end', width:≈230} with rows of label (flex:1, right-aligned) and value (width 90, right-aligned). TOTAL uses fontWeight 700.
- **Signature:** View wrap={false}, alignSelf 'flex-end', marginTop about 30. The Image needs an explicit width/height box plus objectFit 'contain' and objectPosition 'right'.
- **Footer:** View fixed, absolute, bottom 20, left/right 77.4, fontSize 7, using render={({pageNumber,totalPages}) => …}.
- **Hyphenation:** Font.registerHyphenationCallback(w => [w]).
## Similarity to other templates
- **Byte-identical page content to /media/Toto/Documents/bongbetic/invoice/format.pdf.** It is the same Canva page; only the Info/XMP title, export timestamps, Canva design ID, xref offsets and trailer /ID differ. So this IS the vendor's original format, still carrying Canva's sample data (Daniel Gallego / Richard Sanchez / Borcele Bank).
- **Not a duplicate of the in-app template.** The current src/pdf/InvoiceDocument.tsx only loosely adapts it (see group_notes).
- **Sibling of 5.pdf and 6.pdf.** They share the grid: rules 69.64-525.86, column centres 325.2 / 416.4, money right edge 514.4, meta label/value anchors about 447 / 519, totals label anchor 460.3. 5 changes fonts and decoration; 6 shifts the grid right. Build 4, 5 and 6 as one configurable 'Neutral' component family.
## Implementation notes
Built as `src/pdf/templates/neutral/` (one family component, `Layout.tsx`, with tokens in `linea.ts`; planning in `plan.ts`). Measured against `4.pdf` with `npm run templates:compare` (`/tmp/invoice-c3/compare-linea.png`, left Canva, right ours).
**Matches the design (measured, A4):** title baseline 161.6 pt and x 59.5 as in Canva; hairline starting 20 pt after the title and ending on the margin; 'ISSUED TO' at x 77.4; the Montserrat glyph widths are metrically identical to the PDF (label and value widths agree to 0.1 pt); header baseline to first row 36.3 pt; row pitch 21.4 pt (Canva 21.46); table rules at x 69.6-525.6 (Canva 69.64-525.86); one colour, #3D3B3A.
**Intentional deviations, and why:**
- **Margins.** The template margin is 59.55 pt left and right, 56 top and 60 bottom, plus a 16 pt footer reserve. The doc's 77.4 pt text column with negative margins would put the title (x 59.5) and the rules (x 69.6) outside the margin box, which the audit forbids for flowing content. Here the title starts on the margin, the rules start 10.09 pt inside it and the text 17.9 pt inside it, so every x is relative to the margin and Narrow/Normal work. The content area ends at y 766 (page height less 60 + 16); Canva's signature ink ends at 772.
- **Supplier block (FROM)** is right-aligned under the title rule (docs: band x 290-535, y 170-255), max 245 pt wide. Its height pushes the info row down by the lines it needs beyond the design's empty band; a long address can never touch the title.
- **PAY TO** stays under ISSUED TO as in the design, with Bank, Account Name, Account No., Branch and IFSC.
- **Table.** An HSN/SAC column appears only when an item carries a code. UNIT PRICE shows the amount with a short unit ('₹1,200.00/hr'); when that does not fit the column it goes on two lines (amount, then 'per hour'). Fixed columns widen to their widest token (an amount cannot wrap) and the description takes the rest, so the table never stacks. The 4 pt indent of the cells (Canva artefact) is dropped: text starts at x 77.4 in the header and the rows.
- **Totals.** SUBTOTAL runs full width; DISCOUNT, TAXABLE VALUE, `CGST @ 9%` / `SGST @ 9%` (or UTGST, IGST) with their amounts and TOTAL form the right-anchored column (236 pt, amounts 92 pt). Amount in words and the reverse-charge line sit left of that column; below 150 pt of room they move under it.
- **Closing.** Only the signature is bottom-right; the user's signature image or, without one, the signatory name and designation plus the computer-generated line. Canva's script signature is not reproduced. The block is pushed to the foot of the last page by the bottom spacer.
- **Logo.** None by default (so Linea matches `format.pdf`); `prefs.logo.show` (the Logo toggle next to the template button) puts the logo in the optional band above the title, at most 150 x 48 pt, bottom-aligned 4.85 pt above the title box.
- **Elastic gaps** (Letter, and A4 when a one-page invoice overflows by less than the slack): the band above the title (68.85 to 33.85 pt), the supplier-to-info gap (down to 14 pt) and the info-to-table gap (49.5 to 24 pt). Type is never scaled. 'TAX INVOICE' drops its tracking to 0.12 em only if the hairline would be under 40 pt.
- Footer ('Generated with Voiced', page n of N) at 7 pt Montserrat in the same ink; the design has none.