PageSourceSearch

https://planitron.ai/assets/TakeoffResults-Bxs8PH2V.js

js planitron.ai collected 2026-10-05 12:25:42 UTC 18,847 bytes, 17 lines download raw bytes

1import{bc as xe,D as ue,z as he,r as u,w as N,x as v,j as t,ax as U,bd as ge,B as b,ac as pe,a6 as fe,s as be,ao as B,aK as V,C as H,f as je,a1 as ye,a2 as we,a4 as Ne,I as K,P as ve,X as Ce,c5 as Se,ay as Fe,aA as Te,aB as We,aC as ke,aD as De,aE as Ee,aF as Pe,aG as Ie,aH as Ae,H as Le}from"./index-Br6Hs4h4.js";import{S as _e}from"./slider-QBzzu-_Z.js";import qe from"./jspdf.es.min-BplPJhKu.js";import{S as Re,a as $e,b as ze,c as Ge,d as Qe}from"./select-BvcvX9lb.js";import{L as Me}from"./layout-grid-vGPNg5en.js";import{P as Oe}from"./pen-tool-DgTTMC_d.js";import{F as Ue}from"./file-spreadsheet-DgEkGzXm.js";import{P as Be}from"./printer-BHFjg1w_.js";import{L as q}from"./layers-DbWAa4S-.js";import{M as Ve}from"./map-pin-3rH5AGwl.js";import{M as He}from"./minus-CFJMH7Di.js";import{S as Ke}from"./square-C9x5Z_sc.js";const Xe={en:{title:"Takeoff Results",backToDashboard:"Back to Takeoff",exportCSV:"Export CSV",print:"Print Report",category:"Category",label:"Label",quantity:"Quantity",unit:"Unit",confidence:"Confidence",actions:"Actions",noItems:"No items in this takeoff",room:"Room",interior_wall:"Interior Wall",exterior_wall:"Exterior Wall",door:"Door",window:"Window",sf:"SF",lf:"LF",count:"Count",summary:"Project Summary",totalRooms:"Total Rooms",totalArea:"Total Area",totalWalls:"Total Walls",totalWallLength:"Total Wall Length",totalDoors:"Total Doors",totalWindows:"Total Windows",deleteConfirm:"Are you sure you want to delete this item?",cancel:"Cancel",delete:"Delete",aiGenerated:"AI Generated",manual:"Manual",edit:"Edit",save:"Save",wastePercentage:"Waste %",withWaste:"With Waste",baseQuantity:"Base Quantity",adjustWaste:"Adjust Waste Factor",groupBreakdown:"Group Breakdown",items:"items",expandAll:"Expand All",collapseAll:"Collapse All",totalWithWaste:"Total with Waste",measurements:"Measurements",materialEstimate:"Material Estimate",page:"Page"},es:{title:"Resultados del Despegue",backToDashboard:"Volver a Despegue",exportCSV:"Exportar CSV",print:"Imprimir Reporte",category:"Categoría",label:"Etiqueta",quantity:"Cantidad",unit:"Unidad",confidence:"Confianza",actions:"Acciones",noItems:"Sin elementos en este despegue",room:"Habitación",interior_wall:"Muro Interior",exterior_wall:"Muro Exterior",door:"Puerta",window:"Ventana",sf:"PC",lf:"PL",count:"Conteo",summary:"Resumen del Proyecto",totalRooms:"Total Habitaciones",totalArea:"Área Total",totalWalls:"Total Muros",totalWallLength:"Longitud Total Muros",totalDoors:"Total Puertas",totalWindows:"Total Ventanas",deleteConfirm:"¿Estás seguro de que deseas eliminar este elemento?",cancel:"Cancelar",delete:"Eliminar",aiGenerated:"Generado por IA",manual:"Manual",edit:"Editar",save:"Guardar",wastePercentage:"% Desperdicio",withWaste:"Con Desperdicio",baseQuantity:"Cantidad Base",adjustWaste:"Ajustar Factor de Desperdicio",groupBreakdown:"Desglose por Grupo",items:"elementos",expandAll:"Expandir Todo",collapseAll:"Colapsar Todo",totalWithWaste:"Total con Desperdicio",measurements:"Mediciones",materialEstimate:"Estimación de Material",page:"Página"}},Ye=["sf","lf","count"],X=[{bg:"from-blue-500/20 to-blue-600/10",border:"border-blue-500/30",text:"text-blue-400",icon:"text-blue-400"},{bg:"from-emerald-500/20 to-emerald-600/10",border:"border-emerald-500/30",text:"text-emerald-400",icon:"text-emerald-400"},{bg:"from-amber-500/20 to-amber-600/10",border:"border-amber-500/30",text:"text-amber-400",icon:"text-amber-400"},{bg:"from-purple-500/20 to-purple-600/10",border:"border-purple-500/30",text:"text-purple-400",icon:"text-purple-400"},{bg:"from-rose-500/20 to-rose-600/10",border:"border-rose-500/30",text:"text-rose-400",icon:"text-rose-400"},{bg:"from-cyan-500/20 to-cyan-600/10",border:"border-cyan-500/30",text:"text-cyan-400",icon:"text-cyan-400"},{bg:"from-orange-500/20 to-orange-600/10",border:"border-orange-500/30",text:"text-orange-400",icon:"text-orange-400"},{bg:"from-indigo-500/20 to-indigo-600/10",border:"border-indigo-500/30",text:"text-indigo-400",icon:"text-indigo-400"}],Je=I=>{switch(I){case"area":return Ke;case"linear":return He;case"count":return Ve;default:return q}},ut=()=>{const{projectId:I,runId:T}=xe(),R=ue(),{language:p,setLanguage:Ze}=he(),l=Xe[p],[g,Y]=u.useState(null),[W,A]=u.useState([]),[et,J]=u.useState([]),[Z,$]=u.useState(!0),[k,z]=u.useState(null),[h,C]=u.useState({}),[D,L]=u.useState(null),[y,G]=u.useState({}),[ee,E]=u.useState(new Set),[Q,_]=u.useState(!1);u.useEffect(()=>{T&&te()},[T]);const te=async()=>{$(!0);try{const{data:{user:a}}=await N.auth.getUser();if(!a){R("/auth");return}const{data:e,error:i}=await N.from("takeoff_runs").select("*").eq("id",T).single();if(i)throw i;Y(e);const{data:c,error:o}=await N.from("takeoff_items").select(`
2          *,
3          takeoff_groups (
4            id,
5            name,
6            color,
7            measurement_type,
8            config
9          )
10        `).eq("run_id",T).order("created_at",{ascending:!0});if(o)throw o;A(c||[]);const{data:n}=await N.from("takeoff_groups").select("*").eq("user_id",a.id).order("display_order",{ascending:!0});n&&J(n);const s={};
10(c||[]).forEach(d=>{const m=d.group_id||d.category;s[m]||(s[m]=10)}),G(s);const r=new Set((c||[]).map(d=>d.group_id||d.category));E(r),_(!0)}catch(a){console.error("Error fetching results:",a),v.error(p==="es"?"No se pudieron cargar los resultados":"Failed to load results")}finally{$(!1)}},f=u.useMemo(()=>{const a=new Map;return W.forEach(e=>{const i=e.group_id||e.category,c=a.get(i);c?(c.items.push(e),c.totalQuantity+=e.quantity):a.set(i,{items:[e],groupInfo:e.takeoff_groups||{name:l[e.category]||e.category,color:"#888888",measurement_type:e.unit==="sf"?"area":e.unit==="lf"?"linear":"count"},totalQuantity:e.quantity,unit:e.unit,measurementType:e.takeoff_groups?.measurement_type||(e.unit==="sf"?"area":e.unit==="lf"?"linear":"count")})}),Array.from(a.entries()).map(([e,i],c)=>({id:e,colorIndex:c%X.length,...i}))},[W,l]);u.useMemo(()=>{let a=0,e=0,i=0,c=0,o=0;return f.forEach(n=>{const s=y[n.id]??10,r=n.totalQuantity*(1+s/100);a+=n.totalQuantity,e+=r,n.measurementType==="area"?i+=r:n.measurementType==="linear"?c+=r:o+=n.totalQuantity}),{totalBase:a,totalWithWaste:e,areaTotal:i,linearTotal:c,countTotal:o}},[f,y]);const ae=a=>{E(e=>{const i=new Set(e);return i.has(a)?i.delete(a):i.add(a),i})},se=()=>{if(Q)E(new Set),_(!1);else{const a=new Set(f.map(e=>e.id));E(a),_(!0)}},oe=(a,e)=>{G(i=>({...i,[a]:e}))},re=a=>{z(a.id),C({category:a.category,label:a.label,quantity:a.quantity,unit:a.unit})},M=()=>{z(null),C({})},ne=async()=>{if(!(!k||!h.label))try{const{error:a}=await N.from("takeoff_items").update({category:h.category,label:h.label,quantity:h.quantity,unit:h.unit}).eq("id",k);if(a)throw a;A(W.map(e=>e.id===k?{...e,...h}:e)),v.success(p==="es"?"Elemento actualizado":"Item updated"),M()}catch(a){console.error("Update error:",a),v.error(p==="es"?"No se pudo actualizar el elemento":"Failed to update item")}},le=async()=>{if(D)try{const{error:a}=await N.from("takeoff_items").delete().eq("id",D);if(a)throw a;A(W.filter(e=>e.id!==D)),v.success(p==="es"?"Elemento eliminado":"Item deleted")}catch(a){console.error("Delete error:",a),v.error(p==="es"?"No se pudo eliminar el elemento":"Failed to delete item")}finally{L(null)}},ie=()=>{const a=["Group","Label","Base Quantity","Waste %","With Waste","Unit","Page"],e=[];f.forEach(s=>{const r=y[s.id]??10;s.items.forEach(d=>{const m=d.quantity*(1+r/100);e.push([s.groupInfo.name,d.label,d.quantity.toFixed(2),r.toString()+"%",m.toFixed(2),l[d.unit]||d.unit,d.page_number?.toString()||"1"])})}),e.push([]),e.push(["--- TOTALS ---","","","","","",""]),f.forEach(s=>{const r=y[s.id]??10,d=s.totalQuantity*(1+r/100);e.push([s.groupInfo.name,"TOTAL",s.totalQuantity.toFixed(2),r.toString()+"%",d.toFixed(2),l[s.unit]||s.unit,""])});const i=[a.join(","),...e.map(s=>s.join(","))].join(`
11`),c=new Blob([i],{type:"text/csv;charset=utf-8;"}),o=URL.createObjectURL(c),n=document.createElement("a");n.href=o,n.download=`takeoff-${g?.name||"results"}-with-waste.csv`,n.click(),URL.revokeObjectURL(o),v.success(p==="es"?"CSV exportado con cálculos de desperdicio":"CSV exported with waste calculations")},ce=()=>{const a=new Image;a.crossOrigin="anonymous",a.onload=()=>O(a),a.onerror=()=>O(null),a.src=Le},O=a=>{const e=new qe({orientation:"portrait",unit:"pt",format:"letter"}),i=e.internal.pageSize.getWidth(),c=e.internal.pageSize.getHeight(),o=40,n=i-o*2;let s=o;const r={r:34,g:139,b:230},d=x=>{s+x>c-50&&(m(),e.addPage(),s=o+10)},m=()=>{const x=c-24;e.setDrawColor(r.r,r.g,r.b),e.setLineWidth(.5),e.line(o,x-8,o+n,x-8),e.setFontSize(7),e.setFont("helvetica","normal"),e.setTextColor(140),e.text("Generated by Planitron",o,x),e.text(`${new Date().toLocaleDateString()}`,o+n,x,{align:"right"}),e.text(`Page ${e.getNumberOfPages()}`,i/2,x,{align:"center"}),e.setTextColor(0)};if(e.setFillColor(r.r,r.g,r.b),e.rect(0,0,i,4,"F"),a){const S=a.naturalWidth/a.naturalHeight*28,w=document.createElement("canvas");w.width=a.naturalWidth,w.height=a.naturalHeight;const P=w.getContext("2d");P&&(P.drawImage(a,0,0),e.addImage(w.toDataURL("image/png"),"PNG",o,s,S,28)),s+=36}else e.setFontSize(18),e.setFont("helvetica","bold"),e.setTextColor(r.r,r.g,r.b),e.text("PLANITRON",o,s+14),e.setTextColor(0),s+=24;e.setFontSize(14),e.setFont("helvetica","bold"),e.setTextColor(40),e.text(l.title,o,s+4),g&&(e.setFontSize(9),e.setFont("helvetica","normal"),e.setTextColor(100),e.text(g.name,o,s+16)),e.setFontSize(8),e.setFont("helvetica","normal"),e.setTextColor(130),e.text(new Date().toLocaleDateString(p==="es"?"es-ES":"en-US",{year:"numeric",month:"long",day:"numeric"}),o+n,s+4,{align:"right"}),e.setTextColor(0),s+=g?24:12,e.setDrawColor(r.r,r.g,r.b),e.setLineWidth(1.5),e.line(o,s,o+n,s),e.setLineWidth(.5),s+=18,f.forEach(x=>{d(60);const S=y[x.id]??10,w=x.groupInfo.color||"#888888";e.setFillColor(w),e.rect(o,s-9,3,12,"F"),e.setFontSize(10),e.setFont("helvetica","bold"),e.setTextColor(40),e.text(x.groupInfo.name,o+10,s),e.setTextColor(0),s+=14,e.setFillColor(245,245,248),e.rect(o+6,s-9,n-6,13,"F"),e.setFontSize(7),e.setFont("helvetica","bold"),e.setTextColor(100),e.text(l.label,o+10,s),e.text(l.baseQuantity,o+n*.4,s),e.text(l.unit,o+n*.58,s),e.text(`${l.wastePercentage}`,o+n*.68,s),e.text(l.withWaste,o+n*.8,s),e.text(l.page,o+n*.93,s),e.setTextColor(0),s+=12,x.items.forEac
11h((F,de)=>{d(14),de%2===0&&(e.setFillColor(250,250,252),e.rect(o+6,s-9,n-6,12,"F"));const me=F.quantity*(1+S/100);e.setFontSize(8),e.setFont("helvetica","normal"),e.text(F.label||"-",o+10,s),e.text(F.quantity.toFixed(1),o+n*.4,s),e.text(j(F.unit),o+n*.58,s),e.text(`${S}%`,o+n*.68,s),e.text(me.toFixed(1),o+n*.8,s),e.text(`${F.page_number||1}`,o+n*.93,s),s+=12}),d(20),e.setDrawColor(200),e.line(o+6,s-4,o+n,s-4),e.setFont("helvetica","bold"),e.setFontSize(9),e.setTextColor(r.r,r.g,r.b);const P=x.totalQuantity*(1+S/100);e.text(`Total: ${x.totalQuantity.toFixed(1)} ${j(x.unit)}`,o+n*.4,s+4),e.text(`${l.withWaste}: ${P.toFixed(1)} ${j(x.unit)}`,o+n*.68,s+4),e.setTextColor(0),s+=24}),m(),e.save(`planitron-takeoff-${g?.name||"results"}-${new Date().toISOString().split("T")[0]}.pdf`)},j=a=>l[a]||a.toUpperCase();return Z?t.jsx(U,{children:t.jsx("div",{className:"flex items-center justify-center min-h-[60vh]",children:t.jsxs("div",{className:"text-center space-y-4",children:[t.jsxs("div",{className:"relative",children:[t.jsx("div",{className:"w-16 h-16 rounded-full border-4 border-primary/30 border-t-primary animate-spin"}),t.jsx(ge,{className:"absolute inset-0 m-auto h-6 w-6 text-primary"})]}),t.jsx("p",{className:"text-muted-foreground",children:"Loading takeoff results..."})]})})}):t.jsxs(U,{children:[t.jsxs("main",{className:"container mx-auto px-4 py-6 max-w-7xl",children:[t.jsxs(b,{variant:"ghost",size:"sm",className:"mb-4 -ml-2 print:hidden",onClick:()=>R(`/project/${I}/takeoff`),children:[t.jsx(pe,{className:"h-4 w-4 mr-2"}),l.backToDashboard]}),t.jsxs("div",{className:"flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4 mb-8",children:[t.jsx("div",{className:"space-y-2",children:t.jsxs("div",{className:"flex items-center gap-3",children:[t.jsx("div",{className:"p-2.5 rounded-xl bg-gradient-to-br from-primary/20 to-primary/5 border border-primary/20",children:t.jsx(Me,{className:"h-6 w-6 text-primary"})}),t.jsxs("div",{children:[t.jsx("h1",{className:"text-2xl lg:text-3xl font-bold text-foreground",children:l.title}),g&&t.jsxs("div",{className:"flex items-center gap-2 mt-1",children:[t.jsx("span",{className:"text-muted-foreground",children:g.name}),t.jsx(fe,{variant:g.ai_generated?"default":"secondary",className:"text-xs",children:g.ai_generated?t.jsxs(t.Fragment,{children:[t.jsx(be,{className:"h-3 w-3 mr-1"})," ",l.aiGenerated]}):t.jsxs(t.Fragment,{children:[t.jsx(Oe,{className:"h-3 w-3 mr-1"})," ",l.manual]})})]})]})]})}),t.jsxs("div",{className:"flex items-center gap-2 print:hidden",children:[t.jsx(b,{variant:"outline",onClick:se,size:"sm",children:Q?t.jsxs(t.Fragment,{children:[t.jsx(B,{className:"h-4 w-4 mr-1"})," ",l.collapseAll]}):t.jsxs(t.Fragment,{children:[t.jsx(V,{className:"h-4 w-4 mr-1"})," ",l.expandAll]})}),t.jsxs(b,{variant:"outline",onClick:ie,children:[t.jsx(Ue,{className:"h-4 w-4 mr-2"}),l.exportCSV]}),t.jsxs(b,{onClick:ce,className:"bg-primary hover:bg-primary/90",children:[t.jsx(Be,{className:"h-4 w-4 mr-2"}),l.print]})]})]}),f.length===0?t.jsx(H,{children:t.jsxs(je,{className:"py-16 text-center",children:[t.jsx(q,{className:"h-12 w-12 mx-auto text-muted-foreground/50 mb-4"}),t.jsx("p",{className:"text-lg text-muted-foreground",children:l.noItems})]})}):t.jsxs("div",{className:"space-y-4",children:[t.jsxs("h2",{className:"text-lg font-semibold flex items-center gap-2",children:[t.jsx(q,{className:"h-5 w-5 text-primary"}),l.groupBreakdown]}),f.map(a=>{const e=X[a.colorIndex],i=Je(a.measurementType),c=ee.has(a.id),o=y[a.id]??10,n=a.totalQuantity*(1+o/100),s=n-a.totalQuantity;return t.jsx(ye,{open:c,onOpenChange:()=>
11ae(a.id),children:t.jsxs(H,{className:`overflow-hidden border ${e.border} transition-all duration-200 hover:shadow-lg`,children:[t.jsx(we,{className:"w-full",children:t.jsx("div",{className:`bg-gradient-to-r ${e.bg} p-4`,children:t.jsxs("div",{className:"flex items-center justify-between",children:[t.jsxs("div",{className:"flex items-center gap-3",children:[c?t.jsx(B,{className:"h-5 w-5 text-muted-foreground"}):t.jsx(V,{className:"h-5 w-5 text-muted-foreground"}),t.jsx("div",{className:"w-4 h-4 rounded-sm",style:{backgroundColor:a.groupInfo.color}}),t.jsx("div",{className:"p-1.5 rounded-lg bg-background/50",children:t.jsx(i,{className:`h-4 w-4 ${e.icon}`})}),t.jsxs("div",{className:"text-left",children:[t.jsx("h3",{className:"font-semibold text-foreground",children:a.groupInfo.name}),t.jsxs("p",{className:"text-xs text-muted-foreground",children:[a.items.length," ",l.items," · ",o,"% waste"]})]})]}),t.jsxs("div",{className:"text-right",children:[t.jsx("p",{className:`text-2xl font-bold ${e.text}`,children:n.toLocaleString(void 0,{maximumFractionDigits:1})}),t.jsx("p",{className:"text-xs text-muted-foreground",children:j(a.unit)})]})]})})}),t.jsx(Ne,{children:t.jsxs("div",{className:"border-t border-border/50",children:[t.jsx("div",{className:"px-4 py-2 bg-muted/10 border-b border-border/20 print:hidden",children:t.jsxs("div",{className:"flex items-center gap-2",children:[t.jsx("span",{className:"text-[11px] text-muted-foreground shrink-0",children:"Waste"}),t.jsx(_e,{value:[o],onValueChange:([r])=>oe(a.id,r),min:0,max:30,step:1,className:"flex-1 max-w-[200px]"}),t.jsxs("span",{className:`text-xs font-mono font-semibold min-w-[28px] text-right ${o===0?"text-muted-foreground":e.text}`,children:[o,"%"]})]})}),t.jsx("div",{className:"divide-y divide-border/20",children:a.items.map(r=>{const d=r.quantity*(1+o/100);return t.jsx("div",{className:"px-4 py-2.5 hover:bg-muted/20 transition-colors group/item",children:k===r.id?t.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[t.jsx(K,{value:h.label||"",onChange:m=>C({...h,label:m.target.value}),className:"flex-1 min-w-[150px]",placeholder:"Label"}),t.jsx(K,{type:"number",value:h.quantity||0,onChange:m=>C({...h,quantity:parseFloat(m.target.value)}),className:"w-24"}),t.jsxs(Re,{value:h.unit,onValueChange:m=>C({...h,unit:m}),children:[t.jsx($e,{className:"w-20",children:t.jsx(ze,{})}),t.jsx(Ge,{className:"bg-background",children:Ye.map(m=>t.jsx(Qe,{value:m,children:j(m)},m))})]}),t.jsx(b,{size:"sm",onClick:ne,"aria-label":"Save changes",children:t.jsx(ve,{className:"h-4 w-4"})}),t.jsx(b,{size:"sm",variant:"ghost",onClick:M,"aria-label":"Cancel edit",children:t.jsx(Ce,{className:"h-4 w-4"})})]}):t.jsxs("div",{className:"flex items-center justify-between",children:[t.jsx("div",{className:"flex-1 min-w-0",children:t.jsx("p",{className:"text-sm font-medium truncate",children:r.label})}),t.jsxs("div",{className:"flex items-center gap-3",children:[t.jsxs("div",{className:"text-right font-mono text-sm tabular-nums",children:[o>0?t.jsxs("span",{children:[r.quantity.toFixed(1)," → ",t.jsx("span",{className:`font-semibold ${e.text}`,children:d.toFixed(1)})]}):t.jsx("span",{className:"font-semibold",children:r.quantity.toFixed(1)}),t.jsx("span",{className:"text-xs text-muted-foreground ml-1",children:j(r.unit)})]}),t.jsxs("div",{className:"flex items-center gap-0.5 opacity-0 group-hover/item:opacity-100 transition-opacity print:hidden",children:[t.jsx(b,{variant:"ghost",size:"icon",className:"h-7 w-7",onClick:()=>re(r),children:t.jsx(Se,{className:"h-3 w-3"})}),t.jsx(b,{variant:"ghost",size:"icon",className:"h-7 w-7 text-destructive hover:text-destructive",onClick:()=>L(r.id),children:t.jsx(Fe,{className:"h-3 w-3"})})]})]})]})},r.id)})}),t.jsx("div",{className:`px-4 py-2.5 bg-gradient-to-r ${e.bg} border-t border-border/30`,children:t.jsxs("div",{className:"flex items-center justify-between text-sm",children:[t.jsx("span",{className:"font-medium",children:l.totalWithWaste}),t.jsxs("span",{className:`font-bold ${e.text}`,children:[a.totalQuantity.toFixed(1),o>0&&t.jsxs("span",{className:"text-muted-foreground font-normal",children:[" + ",s.toFixed(1)," waste = "]}),o>0&&t.jsx("span",{children:n.toFixed(1)})," ",j(a.unit)]})]})})]})})]})},a.id)})]})]}),t.jsx(Te,{open:!!D,onOpenChange:()=>L(null),children:t.jsxs(We,{className:"bg-background",children:[t.jsxs(ke,{children:[t.jsx(De,{children:l.delete}),t.jsx(Ee,{children:l.deleteConfirm})]}),t.jsxs(Pe,{children:[t.jsx(Ie,{children:l.cancel}),t.jsx(Ae,{onClick:le,className:"bg-destructive text-destructive-foreground hover:bg-destructive/90",children:l.delete})]})]})}),t.jsx("style",{children:`
12        @media print {
13          body { background: white !important; }
14          .print\\:hidden { display: none !important; }
15          .print\\:bg-white { background: white !important; }
16        }
17      `})]})};export{ut as default};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.