Repository navigation
Expand file tree
/
Copy pathphoto.js
More file actions
150 lines (143 loc) · 8.86 KB
/
Copy pathphoto.js
File metadata and controls
150 lines (143 loc) · 8.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
// photo.js — decider si une photo TIENT dans un block, avant de la proposer a quiconque.
// ================================================================================================
// ⛔ CE MODULE EST PUR ET TESTE PARCE QU IL VIT DANS LE CHEMIN QUI SIGNE. Trois regressions sont
// deja parties en production cette semaine sur cette page. Aucun correctif n y entre plus sans
// un test qui l a d abord montre ROUGE.
//
// ⛔ LE MUR EST MESURE, PAS SUPPOSE. `cout-photo.mjs` sur Base mainnet, le 2026-09-06 :
// 0 Ko de photo -> 350 515 gas ✅
// 5 Ko -> 6 847 635 ✅
// 12 Ko -> 15 921 140 ✅
// 13 Ko -> REFUSE : « out of gas: gas exhausted during precompiled contract execution »
// Et ce n est PAS un plafond d estimation du noeud : la meme sonde a 13 Ko en offrant
// 100 000 000 de gaz echoue a l identique. C est une limite INTRINSEQUE du precompile B20 ;
// elle ne se contourne pas, et aucun reglage cote app ne la deplacera.
//
// ⚠️ ET LE COUT N EST PAS CELUI QU ON CROIT : 712 gas par octet ajoute, alors que 16 est le
// MAXIMUM theorique pour de la calldata. Le poste dominant est donc le travail du precompile
// (il STOCKE l URI), pas le transport. Une intuition « c est juste des octets » sous-estimerait
// la facture d un facteur 44.
//
// ⛔ POURQUOI UNE MARGE. 12 Ko a PASSE et 13 a ECHOUE : la frontiere est quelque part entre les
// deux, et je ne l ai pas raffinee a l octet. Se poser sur 12 288 exactement, ce serait parier
// que le dernier point mesure est aussi le dernier point valide. La taille d une meme photo
// varie aussi avec l encodeur du navigateur. On garde donc une marge, et on la NOMME.
/** Le plus grand FICHIER reellement accepte lors de la mesure. Au-dela : refus du precompile. */
export const PHOTO_MESUREE_OK = 12 * 1024;
/** Le plus petit FICHIER reellement refuse. Entre les deux : non mesure, donc interdit. */
export const PHOTO_MESUREE_REFUS = 13 * 1024;
/**
* Taille du data: URI JSON final, pour `n` octets de fichier image.
* ⛔ CE N EST PAS `n`. Le fichier est encode en base64 (4 octets pour 3), glisse dans un JSON,
* et le JSON entier est LUI AUSSI encode en base64. Deux inflations successives, pas une.
* Ignorer la seconde sous-estimerait d un tiers — et l app annoncerait un cout qu elle ne
* tiendrait pas.
* @param {number} n octets du fichier image
* @param {number} enrobage octets du JSON hors image (nom, symbole, description…)
*/
export function octetsUri(n, enrobage = 0) {
if (!Number.isInteger(n) || n < 0) return null;
const b64Image = 4 * Math.ceil(n / 3);
const json = b64Image + enrobage;
return 4 * Math.ceil(json / 3);
}
/* ⛔⛔ DEUX GRANDEURS QUE J AI CONFONDUES, ET LA CONFUSION COUTAIT LA MOITIE DE LA QUALITE D IMAGE.
* Le mur a ete mesure en octets du FICHIER (12 Ko passent, 13 sont refuses). Mais ce que l app
* doit borner, c est l URI COMPLET — et l URI vaut environ 1,79 fois le fichier apres la double
* inflation base64. Comparer un budget « fichier » a des octets « URI » divisait donc la photo
* autorisee par presque deux. Aucune garde ne pouvait le voir : les deux sont des nombres
* d octets, seul le SENS differe. On derive donc le budget URI du mur mesure, une seule fois,
* et on ne compare plus jamais que des URI a des URI. */
export const URI_MESURE_OK = octetsUri(PHOTO_MESUREE_OK, 120);
/** Ce qu on autorise : sous le dernier succes, avec 12,5 % de marge. */
export const PHOTO_MAX = Math.floor(URI_MESURE_OK * 0.875);
/**
* Taille du data: URI JSON final quand la photo est posee SUR UNE FACE du block.
* ⛔⛔ TROIS INFLATIONS, PAS DEUX, ET C EST LA DIFFERENCE QUI COMPTE. Quand la photo REMPLACE le
* logo, le fichier est encode une fois pour son data: URI puis une fois avec le JSON : deux.
* Quand elle est posee sur une face, son data: URI vit DANS LE TEXTE DU SVG, et ce SVG est a
* son tour encode en base64 avant d entrer dans le JSON, qui est encode a son tour : TROIS.
* Chaque inflation multiplie par 4/3 ; trois font 2,37 au lieu de 1,78. Reutiliser le budget
* de `PHOTO_MAX` ici ferait accepter une photo qui ferait ECHOUER la creation APRES la
* signature — le seul moment ou une erreur coute de l argent reel.
* ⚠️ `corpsSvg` est le SVG SANS la photo. Il est inflate lui aussi, deux fois : le compter pour sa
* taille brute sous-estimerait de pres d un tiers.
* @param {number} n octets du fichier image
* @param {number} corpsSvg octets du SVG hors image (le block dessine)
* @param {number} enrobage octets du JSON hors image (nom, symbole, description…)
*/
export function octetsUriSurFace(n, corpsSvg = 0, enrobage = 0) {
if (!Number.isInteger(n) || n < 0) return null;
if (!Number.isInteger(corpsSvg) || corpsSvg < 0) return null;
/* 1. le fichier devient un data: URI — le prefixe `data:image/jpeg;base64,` compte */
const uriPhoto = 4 * Math.ceil(n / 3) + 23;
/* 2. cet URI est ECRIT dans le SVG, qui est encode en entier */
const uriSvg = 4 * Math.ceil((corpsSvg + uriPhoto) / 3) + 26;
/* 3. et le JSON qui le porte est encode a son tour */
return 4 * Math.ceil((uriSvg + enrobage) / 3);
}
/**
* Le plus gros FICHIER acceptable quand la photo est sur une face, pour un SVG donne.
* ⛔ ON CHERCHE, ON NE DIVISE PAS. J allais ecrire `PHOTO_MAX / 1.33` : faux, parce que les
* arrondis au multiple de 3, les trois prefixes et le corps du SVG ne se factorisent pas. On
* fait donc croitre jusqu au refus et on rend le dernier qui passe — la meme discipline que
* `chercherQualite`, qui mesure au lieu de modeliser.
* ⚠️ Rend 0 si meme une photo vide ne tient pas : un SVG deja trop gros pour le precompile ne
* laisse aucune place, et il faut le DIRE plutot que de rendre un budget negatif.
*/
export function budgetSurFace(corpsSvg, enrobage = 120, plafond = PHOTO_MAX) {
if (!Number.isInteger(corpsSvg) || corpsSvg < 0) return null;
if (octetsUriSurFace(0, corpsSvg, enrobage) > plafond) return 0;
let bas = 0, haut = plafond;
while (bas < haut) {
const m = Math.ceil((bas + haut + 1) / 2);
if (octetsUriSurFace(m, corpsSvg, enrobage) <= plafond) bas = m; else haut = m - 1;
}
return bas;
}
/**
* Le verdict sur UNE photo. Quatre etats — jamais un booleen.
* ⛔ UN BOOLEEN MENTIRAIT ICI. « false » confondrait « trop grosse » (l utilisateur peut agir :
* recadrer, recompresser) avec « ce n est pas une image » (il doit changer de fichier) et avec
* « je n ai pas pu mesurer » (l app ne doit RIEN affirmer). Trois causes, trois gestes.
* ⚠️ FAIL-CLOSED : toute entree qu on ne sait pas lire rend NON_MESURE, jamais ACCEPTEE.
*/
export function verdictPhoto({ octets, type, plafond = PHOTO_MAX }) {
if (typeof octets !== 'number' || !Number.isFinite(octets) || octets < 0) {
return { etat: 'NON_MESURE', pourquoi: 'the file size could not be read' };
}
if (octets === 0) return { etat: 'NON_MESURE', pourquoi: 'the file is empty' };
if (typeof type !== 'string' || !type.startsWith('image/')) {
return { etat: 'PAS_UNE_IMAGE', pourquoi: 'this file is not an image (' + (type || 'unknown type') + ')' };
}
if (octets > plafond) {
return { etat: 'TROP_GROSSE', pourquoi: 'the B-20 factory refuses a block this large',
octets, plafond, trop: octets - plafond };
}
return { etat: 'ACCEPTEE', octets, plafond };
}
/**
* La qualite JPEG a viser pour tenir sous le plafond, par dichotomie cote navigateur.
* ⛔ ON NE DEVINE PAS UNE QUALITE : on RE-ENCODE et on RE-MESURE a chaque essai. Le rapport entre
* qualite et octets depend de l image (une photo de ciel compresse dix fois mieux qu une
* capture de texte) et de l encodeur du navigateur. Une table de correspondance serait juste
* pour une image et fausse pour la suivante.
* @param {(q:number)=>Promise<number>} mesurer rend les octets obtenus pour la qualite q
*/
export async function chercherQualite(mesurer, plafond = PHOTO_MAX, essais = 7) {
let bas = 0.3, haut = 0.95, meilleure = null;
/* ⛔ ON ESSAIE LE HAUT D ABORD : si la meilleure qualite tient deja, degrader l image serait une
* perte gratuite. Le cas frequent ne doit pas payer le prix du cas rare. */
const auHaut = await mesurer(haut);
if (typeof auHaut === 'number' && auHaut <= plafond) return { qualite: haut, octets: auHaut };
for (let i = 0; i < essais && haut - bas > 0.02; i++) {
const q = (bas + haut) / 2;
const n = await mesurer(q);
/* ⚠️ UNE MESURE ILLISIBLE NE VAUT PAS « TROP GROS » : on retrecit sans jamais la retenir. */
if (typeof n !== 'number' || !Number.isFinite(n)) { haut = q; continue; }
if (n <= plafond) { meilleure = { qualite: q, octets: n }; bas = q; } else { haut = q; }
}
/* ⛔ RENDRE `null` PLUTOT QUE LA MOINS MAUVAISE. Aucune qualite ne tient => l app doit dire a
* l utilisateur de recadrer, pas graver a jamais une image illisible dans un block. */
return meilleure;
}