/* render.c : « austen render » : dessine les cartes décrites dans le fichier de * description (un bloc par image, terminé par « end »), écrit par la partie PHP. * * Directives d'un bloc (le texte libre va jusqu'à la fin de la ligne) : * out quality <0..100> width * box background <#rrggbb> * geo <échelle des traits> (coast.bin, border.bin, dept.bin) * field gust|n100 [] * isobars * center (« D » rouge et pression) * labels (lon lat texte : point noir et valeur, rafales par station) * dots (lon lat valeur : pastilles colorées, observations) * title sub right sign * image (fond : image existante, en projection lon/lat simple, dont la taille fixe celle * de la carte ; sert aux images satellite) * geocolor <#rrggbb> (couleur des côtes, frontières et départements) * idw (champ coloré interpolé entre stations : lon lat valeur ; pondération * par l'inverse du carré de la distance, rien au-delà du rayon) * * Projection : longitude-latitude, échelle horizontale corrigée par le cosinus de la latitude * moyenne (suffisant à l'échelle de la France). Traits anticrénelés par distance au segment, * isobares par « marching squares », textes par FreeType, image finale en WebP (libwebp). */ #include #include FT_FREETYPE_H #include #include #include #include #include #include #include "field.h" #include "util.h" #define PT (100.0 / 72.0) /* un point typographique à 100 ppp, comme les cartes matplotlib d'origine */ #define FONT_BOLD "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf" #define FONT_REG "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" /* échelle Meteociel des rafales : 30 cases, borne basse de chaque case */ static const double GB[30] = {0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 100, 110, 120, 130, 140, 150, 160, 180, 200, 220, 240}; static const uint32_t GC[30] = {0xffffff, 0x97e6ff, 0x33ccff, 0x0099ff, 0x00ff99, 0x33cc66, 0x66cc33, 0x66ff00, 0xa4f22f, 0xb7cf0e, 0xd6f017, 0xcc9900, 0xff9900, 0xff9966, 0xcc9999, 0xcc6633, 0xcc3333, 0xff0d0d, 0xc60000, 0x800000, 0x800050, 0xa00077, 0xcc00cc, 0xff00ff, 0xff40ff, 0xff80ff, 0xffa0ff, 0xffffff, 0xe3e3e3, 0xc6c6c6}; static const uint32_t N100C[7] = {0xffffff, 0xfff3bf, 0xffd43b, 0xfd7e14, 0xe03131, 0x862e9c, 0x3b0a45}; #define BAD 0xe9ecef typedef struct { int w, h; uint8_t *rgb; float *cov; /* couverture des traits en cours (0..1), réutilisée par couche */ double lon0, lon1, lat0, lat1; } Canvas; static FT_Library ft; static FT_Face face_bold, face_reg; /* ------------------------------------------------------------ pixels */ static void blend(Canvas *c, int x, int y, uint32_t col, double a) { if (x < 0 || y < 0 || x >= c->w || y >= c->h || a <= 0) return; if (a > 1) a = 1; uint8_t *p = c->rgb + 3 * ((size_t)y * (size_t)c->w + (size_t)x); double r = (col >> 16) & 255, g = (col >> 8) & 255, b = col & 255; p[0] = (uint8_t)lround(p[0] + (r - p[0]) * a); p[1] = (uint8_t)lround(p[1] + (g - p[1]) * a); p[2] = (uint8_t)lround(p[2] + (b - p[2]) * a); } static void proj(const Canvas *c, double lon, double lat, double *x, double *y) { *x = (lon - c->lon0) / (c->lon1 - c->lon0) * c->w; *y = (c->lat1 - lat) / (c->lat1 - c->lat0) * c->h; } static uint32_t parse_color(const char *s) { unsigned v; if (s[0] != '#' || strlen(s) != 7 || sscanf(s + 1, "%6x", &v) != 1) die("couleur invalide : %s", s); return v; } /* ------------------------------------------------------------ traits */ /* Couverture d'un segment épais (demi-largeur hw) : max par pixel, pour qu'une polyligne faite de * nombreux petits segments ne s'assombrisse pas à chaque jointure. */ static void seg_cov(Canvas *c, double x0, double y0, double x1, double y1, double hw) { double pad = hw + 1.0; int xa = (int)floor(fmin(x0, x1) - pad), xb = (int)ceil(fmax(x0, x1) + pad); int ya = (int)floor(fmin(y0, y1) - pad), yb = (int)ceil(fmax(y0, y1) + pad); if (xb < 0 || yb < 0 || xa >= c->w || ya >= c->h) return; xa = xa < 0 ? 0 : xa; ya = ya < 0 ? 0 : ya; xb = xb >= c->w ? c->w - 1 : xb; yb = yb >= c->h ? c->h - 1 : yb; double dx = x1 - x0, dy = y1 - y0, l2 = dx * dx + dy * dy; for (int y = ya; y <= yb; y++) for (int x = xa; x <= xb; x++) { double px = x + 0.5 - x0, py = y + 0.5 - y0; double t = l2 > 0 ? (px * dx + py * dy) / l2 : 0; t = t < 0 ? 0 : t > 1 ? 1 : t; double ex = px - t * dx, ey = py - t * dy; double cv = hw + 0.5 - sqrt(ex * ex + ey * ey); if (cv <= 0) continue; float *q = &c->cov[(size_t)y * (size_t)c->w + (size_t)x]; if (cv > *q) *q = (float)(cv > 1 ? 1 : cv); } } static void cov_clear(Canvas *c) { memset(c->cov, 0, (size_t)c->w * (size_t)c->h * sizeof(float)); } static void cov_paint(Canvas *c, uint32_t col, double alpha) { for (int y = 0; y < c->h; y++) for (int x = 0; x < c->w; x++) { float v = c->cov[(size_t)y * (size_t)c->w + (size_t)x]; if (v > 0) blend(c, x, y, col, v * alpha); } } /* fichiers de polylignes : u32 nb ; par ligne : u32 nb de points, f32 lon, lat */ static void draw_polylines(Canvas *c, const char *path, double lw_px, uint32_t col, double alpha) { size_t len, p = 0; uint8_t *b = read_file(path, 256 << 20, &len); uint32_t n; if (len < 4) die("%s : tronqué", path); memcpy(&n, b, 4); p = 4; cov_clear(c); for (uint32_t k = 0; k < n; k++) { uint32_t np; if (p + 4 > len) die("%s : tronqué", path); memcpy(&np, b + p, 4); p += 4; if ((size_t)np * 8 > len - p) die("%s : tronqué", path); float prev[2] = {0, 0}; for (uint32_t q = 0; q < np; q++) { float pt[2]; memcpy(pt, b + p + (size_t)q * 8, 8); if (q) { double x0, y0, x1, y1; proj(c, prev[0], prev[1], &x0, &y0); proj(c, pt[0], pt[1], &x1, &y1); seg_cov(c, x0, y0, x1, y1, lw_px / 2); } prev[0] = pt[0]; prev[1] = pt[1]; } p += (size_t)np * 8; } cov_paint(c, col, alpha); free(b); } /* ------------------------------------------------------------ texte */ static size_t utf8_next(const char *s, size_t i, uint32_t *cp) { const unsigned char *u = (const unsigned char *)s; if (u[i] < 0x80) { *cp = u[i]; return i + 1; } int n = (u[i] & 0xe0) == 0xc0 ? 1 : (u[i] & 0xf0) == 0xe0 ? 2 : (u[i] & 0xf8) == 0xf0 ? 3 : 0; uint32_t v = u[i] & (0x3f >> n); size_t j = i + 1; for (int k = 0; k < n; k++, j++) { if ((u[j] & 0xc0) != 0x80) { *cp = 0xfffd; return j; } v = v << 6 | (u[j] & 0x3f); } *cp = n ? v : 0xfffd; return j; } typedef struct { int w, h, base; /* largeur, hauteur, ligne de base depuis le haut */ float *a; /* couverture 0..1 */ } TextMask; static TextMask text_mask(const char *s, double size_px, int bold) { FT_Face f = bold ? face_bold : face_reg; if (FT_Set_Pixel_Sizes(f, 0, (FT_UInt)lround(size_px))) die("taille de police refusée"); int asc = (int)(f->size->metrics.ascender >> 6), desc = (int)(-f->size->metrics.descender >> 6); int width = 0; for (size_t i = 0; s[i];) { uint32_t cp; i = utf8_next(s, i, &cp); if (!FT_Load_Char(f, cp, FT_LOAD_DEFAULT)) width += (int)(f->glyph->advance.x >> 6); } TextMask m = {width + 2, asc + desc + 2, asc + 1, NULL}; m.a = xcalloc((size_t)m.w * (size_t)m.h, sizeof(float)); int pen = 1; for (size_t i = 0; s[i];) { uint32_t cp; i = utf8_next(s, i, &cp); if (FT_Load_Char(f, cp, FT_LOAD_RENDER)) continue; FT_GlyphSlot g = f->glyph; for (unsigned r = 0; r < g->bitmap.rows; r++) for (unsigned q = 0; q < g->bitmap.width; q++) { int x = pen + g->bitmap_left + (int)q, y = m.base - g->bitmap_top + (int)r; if (x < 0 || y < 0 || x >= m.w || y >= m.h) continue; float v = g->bitmap.buffer[r * (unsigned)g->bitmap.pitch + q] / 255.0f; float *d = &m.a[(size_t)y * (size_t)m.w + (size_t)x]; if (v > *d) *d = v; } pen += (int)(g->advance.x >> 6); } return m; } /* ancre : ax 0 gauche, 1 centre, 2 droite ; ay 0 haut, 1 milieu, 2 bas */ static void draw_text(Canvas *c, double x, double y, int ax, int ay, const char *s, double size_px, int bold, uint32_t col, uint32_t halo, double halo_px, uint32_t *box_bg, double box_pad) { TextMask m = text_mask(s, size_px, bold); int ox = (int)lround(x - (ax == 0 ? 0 : ax == 1 ? m.w / 2.0 : m.w)); int oy = (int)lround(y - (ay == 0 ? 0 : ay == 1 ? m.h / 2.0 : m.h)); if (box_bg) { int pad = (int)lround(box_pad); for (int yy = oy - pad; yy < oy + m.h + pad; yy++) for (int xx = ox - pad; xx < ox + m.w + pad; xx++) blend(c, xx, yy, *box_bg, 1.0); } if (halo_px > 0) { /* halo : dilatation de la couverture sur un disque, comme un contour de texte */ int r = (int)ceil(halo_px); for (int yy = -r; yy < m.h + r; yy++) for (int xx = -r; xx < m.w + r; xx++) { float best = 0; for (int dy = -r; dy <= r; dy++) for (int dx = -r; dx <= r; dx++) { double d = sqrt((double)(dx * dx + dy * dy)); if (d > halo_px + 0.5) continue; int sx = xx + dx, sy = yy + dy; if (sx < 0 || sy < 0 || sx >= m.w || sy >= m.h) continue; float v = m.a[(size_t)sy * (size_t)m.w + (size_t)sx] * (float)fmin(1.0, halo_px + 0.5 - d); if (v > best) best = v; } blend(c, ox + xx, oy + yy, halo, best); } } for (int yy = 0; yy < m.h; yy++) for (int xx = 0; xx < m.w; xx++) blend(c, ox + xx, oy + yy, col, m.a[(size_t)yy * (size_t)m.w + (size_t)xx]); free(m.a); } static void disc(Canvas *c, double x, double y, double r, uint32_t fill, double edge_px, uint32_t edge) { double R = r + edge_px; for (int yy = (int)floor(y - R - 1); yy <= (int)ceil(y + R + 1); yy++) for (int xx = (int)floor(x - R - 1); xx <= (int)ceil(x + R + 1); xx++) { double d = hypot(xx + 0.5 - x, yy + 0.5 - y); if (edge_px > 0) blend(c, xx, yy, edge, R + 0.5 - d); blend(c, xx, yy, fill, r + 0.5 - d); } } /* ------------------------------------------------------------ champ coloré */ static uint32_t gust_color(double v) { int i = 0; for (int k = 0; k < 30; k++) if (v >= GB[k]) i = k; return GC[i]; } static void draw_field(Canvas *c, const Grd *d, int layer, int n100, int nmodels) { if (layer < 0 || layer >= d->nt_gust) die("couche %d absente", layer); size_t np = grid_points(&d->g); const float *f = d->gust + (size_t)layer * np; for (int y = 0; y < c->h; y++) { double lat = c->lat1 - (y + 0.5) / c->h * (c->lat1 - c->lat0); for (int x = 0; x < c->w; x++) { double lon = c->lon0 + (x + 0.5) / c->w * (c->lon1 - c->lon0); double v = bilinear(&d->g, f, lat, lon); uint32_t col; if (isnan(v)) col = BAD; else if (n100) { /* nombre de modèles : valeur entière la plus proche du point de grille */ double fy = (lat - d->g.lat0) / d->g.dlat, fx = (lon - d->g.lon0) / d->g.dlon; int k = (int)lround(f[(size_t)lround(fy) * (size_t)d->g.nx + (size_t)lround(fx)]); k = k < 0 ? 0 : k > nmodels ? nmodels : k > 6 ? 6 : k; col = N100C[k]; } else col = gust_color(v); uint8_t *p = c->rgb + 3 * ((size_t)y * (size_t)c->w + (size_t)x); p[0] = (uint8_t)(col >> 16); p[1] = (uint8_t)(col >> 8); p[2] = (uint8_t)col; } } } /* ------------------------------------------------------------ isobares */ typedef struct { double x, y; int level; } LabelPos; static void draw_isobars(Canvas *c, const Grd *d, int idx, int smooth) { if (idx < 0 || idx >= d->nt_msl) die("échéance de pression %d absente", idx); const Grid *g = &d->g; size_t np = grid_points(g); float *P = xmalloc(np, sizeof(float)); if (smooth) /* AROME : moyenne normalisée, les trous ne comptent pas (voir field.c) */ gauss_masked(d->msl + (size_t)idx * np, P, g->nx, g->ny, 2.0); else memcpy(P, d->msl + (size_t)idx * np, np * sizeof(float)); /* cellules utiles : le cadre plus un degré de marge */ int x0 = (int)floor((c->lon0 - 1 - g->lon0) / g->dlon), x1 = (int)ceil((c->lon1 + 1 - g->lon0) / g->dlon); int y0 = (int)floor((c->lat0 - 1 - g->lat0) / g->dlat), y1 = (int)ceil((c->lat1 + 1 - g->lat0) / g->dlat); x0 = x0 < 0 ? 0 : x0; y0 = y0 < 0 ? 0 : y0; x1 = x1 > g->nx - 1 ? g->nx - 1 : x1; y1 = y1 > g->ny - 1 ? g->ny - 1 : y1; LabelPos lab[256]; int nlab = 0, per_level[64] = {0}; cov_clear(c); for (int j = y0; j < y1; j++) for (int i = x0; i < x1; i++) { size_t k = (size_t)j * (size_t)g->nx + (size_t)i; double v[4] = {P[k], P[k + 1], P[k + 1 + (size_t)g->nx], P[k + (size_t)g->nx]}; if (isnan(v[0]) || isnan(v[1]) || isnan(v[2]) || isnan(v[3])) continue; double lo = fmin(fmin(v[0], v[1]), fmin(v[2], v[3])), hi = fmax(fmax(v[0], v[1]), fmax(v[2], v[3])); /* coins (i,j) (i+1,j) (i+1,j+1) (i,j+1) en coordonnées géographiques */ double cx[4] = {i, i + 1.0, i + 1.0, i}, cy[4] = {j, j, j + 1.0, j + 1.0}; for (int L = 940; L <= 1048; L += 2) { if (L < lo || L > hi) continue; int cs = (v[0] > L) | (v[1] > L) << 1 | (v[2] > L) << 2 | (v[3] > L) << 3; if (cs == 0 || cs == 15) continue; /* points de passage sur les arêtes 0:(0-1) 1:(1-2) 2:(2-3) 3:(3-0) */ double ex[4], ey[4]; for (int e = 0; e < 4; e++) { int a = e, b = (e + 1) & 3; double t = (L - v[a]) / (v[b] - v[a]); t = !isfinite(t) ? 0.5 : t < 0 ? 0 : t > 1 ? 1 : t; double gx = cx[a] + t * (cx[b] - cx[a]), gy = cy[a] + t * (cy[b] - cy[a]); proj(c, g->lon0 + gx * g->dlon, g->lat0 + gy * g->dlat, &ex[e], &ey[e]); } int segs[2][2], ns = 0; switch (cs) { case 1: case 14: segs[0][0] = 3; segs[0][1] = 0; ns = 1; break; case 2: case 13: segs[0][0] = 0; segs[0][1] = 1; ns = 1; break; case 3: case 12: segs[0][0] = 3; segs[0][1] = 1; ns = 1; break; case 4: case 11: segs[0][0] = 1; segs[0][1] = 2; ns = 1; break; case 6: case 9: segs[0][0] = 0; segs[0][1] = 2; ns = 1; break; case 7: case 8: segs[0][0] = 2; segs[0][1] = 3; ns = 1; break; case 5: case 10: { /* col : on tranche avec la moyenne des quatre coins */ int center_hi = (v[0] + v[1] + v[2] + v[3]) / 4 > L; if ((cs == 5) == center_hi) { segs[0][0] = 3; segs[0][1] = 2; segs[1][0] = 0; segs[1][1] = 1; } else { segs[0][0] = 3; segs[0][1] = 0; segs[1][0] = 1; segs[1][1] = 2; } ns = 2; break; } } for (int s = 0; s < ns; s++) { double ax = ex[segs[s][0]], ay = ey[segs[s][0]], bx = ex[segs[s][1]], by = ey[segs[s][1]]; seg_cov(c, ax, ay, bx, by, 0.7 * PT / 2); /* étiquettes : niveaux multiples de 4, espacées d'au moins 180 px, loin des bords */ int li = (L - 940) / 2; double mx = (ax + bx) / 2, my = (ay + by) / 2; if (L % 4 || nlab >= 256 || per_level[li] >= 4 || mx < 40 || my < 60 || mx > c->w - 40 || my > c->h - 30) continue; int okp = 1; for (int q = 0; q < nlab && okp; q++) okp = hypot(lab[q].x - mx, lab[q].y - my) > 180; if (okp) { lab[nlab++] = (LabelPos){mx, my, L}; per_level[li]++; } } } } cov_paint(c, 0x1a1a1a, 1.0); for (int q = 0; q < nlab; q++) { char t[8]; snprintf(t, sizeof t, "%d", lab[q].level); draw_text(c, lab[q].x, lab[q].y, 1, 1, t, 8.5 * PT, 0, 0x111111, 0xffffff, 2.0 * PT / 2, NULL, 0); } free(P); } /* ------------------------------------------------------------ fichiers tsv */ typedef struct { double lon, lat; char text[64]; } Row; static Row *read_tsv(const char *path, int *n) { size_t len; char *b = (char *)read_file(path, 16 << 20, &len); Row *r = NULL; int cnt = 0; char *cur_ = b; for (char *line = next_line(&cur_); line; line = next_line(&cur_)) { Row x; char *t1 = strchr(line, '\t'), *t2 = t1 ? strchr(t1 + 1, '\t') : NULL; if (!t1 || !t2 || strlen(t2 + 1) >= sizeof x.text) die("%s : ligne invalide", path); char *end; x.lon = strtod(line, &end); if (end != t1) die("%s : longitude invalide", path); x.lat = strtod(t1 + 1, &end); if (end != t2) die("%s : latitude invalide", path); strcpy(x.text, t2 + 1); r = xrealloc(r, (size_t)cnt + 1, sizeof *r); r[cnt++] = x; } free(b); *n = cnt; return r; } /* ------------------------------------------------------------ champ interpolé entre stations */ static void draw_idw(Canvas *c, const char *path, double radius_km) { int n; Row *r = read_tsv(path, &n); if (n == 0) { free(r); return; } /* grille de recherche : cellules de 0,25° pour ne regarder que les stations voisines */ const double cell = 0.25; int gx = (int)ceil((c->lon1 - c->lon0) / cell) + 1, gy = (int)ceil((c->lat1 - c->lat0) / cell) + 1; int *head = xmalloc((size_t)gx * (size_t)gy, sizeof(int)), *next = xmalloc((size_t)n, sizeof(int)); double *val = xmalloc((size_t)n, sizeof(double)); for (int k = 0; k < gx * gy; k++) head[k] = -1; for (int q = 0; q < n; q++) { val[q] = strtod(r[q].text, NULL); int cx = (int)floor((r[q].lon - c->lon0) / cell), cy = (int)floor((r[q].lat - c->lat0) / cell); if (cx < 0 || cy < 0 || cx >= gx || cy >= gy || !isfinite(val[q])) { next[q] = -2; continue; } next[q] = head[cy * gx + cx]; head[cy * gx + cx] = q; } double coslat = cos((c->lat0 + c->lat1) / 2 * DEG); int reach = (int)ceil(radius_km / (KM_PER_DEG * coslat) / cell) + 1; for (int y = 0; y < c->h; y++) { double lat = c->lat1 - (y + 0.5) / c->h * (c->lat1 - c->lat0); for (int x = 0; x < c->w; x++) { double lon = c->lon0 + (x + 0.5) / c->w * (c->lon1 - c->lon0); int cx = (int)floor((lon - c->lon0) / cell), cy = (int)floor((lat - c->lat0) / cell); double sw = 0, sv = 0, dmin = INFINITY; for (int yy = cy - reach; yy <= cy + reach; yy++) for (int xx = cx - reach; xx <= cx + reach; xx++) { if (xx < 0 || yy < 0 || xx >= gx || yy >= gy) continue; for (int q = head[yy * gx + xx]; q >= 0; q = next[q]) { double d = dist_km(lat, lon, r[q].lat, r[q].lon); if (d > radius_km) continue; double w = 1.0 / (d * d + 25.0); /* 5 km de lissage au plus près d'une station */ sw += w; sv += w * val[q]; if (d < dmin) dmin = d; } } if (sw <= 0) continue; /* fondu sur le dernier tiers du rayon, pour ne pas finir en bord franc */ double a = (radius_km - dmin) / (radius_km * 0.35); blend(c, x, y, gust_color(sv / sw), a > 1 ? 1 : a); } } free(head); free(next); free(val); free(r); } /* ------------------------------------------------------------ bloc */ typedef struct { char out[1024], geo[1024], field[1024], iso[1024], labels[1024], dots[1024], image[1024], idw[1024]; char title[256], sub[256], right[256], sign[128]; int quality, width, layer, n100, nmodels, geo_depts, iso_idx, iso_smooth, has_center; double box[4], geo_lw, clon, clat, cp, idw_km; uint32_t bg, geocolor; int has_geocolor; } Spec; static Grd *cache_grd(const char *path) { static char cpath[2][1024]; static Grd *cg[2]; static int next; for (int k = 0; k < 2; k++) if (cg[k] && !strcmp(cpath[k], path)) return cg[k]; int k = next; next = 1 - next; grd_free(cg[k]); cg[k] = grd_read(path); snprintf(cpath[k], sizeof cpath[k], "%s", path); return cg[k]; } static void render_one(const Spec *s) { if (s->width < 100 || s->width > 4000 || !(s->box[1] > s->box[0]) || !(s->box[3] > s->box[2])) die("bloc invalide pour %s", s->out); Canvas c; c.lon0 = s->box[0]; c.lon1 = s->box[1]; c.lat0 = s->box[2]; c.lat1 = s->box[3]; if (s->image[0]) { /* fond imposé (satellite) : sa taille fixe celle de la carte, projection lon/lat simple */ size_t len; uint8_t *raw = read_file(s->image, 64 << 20, &len); int iw = 0, ih = 0; if (!WebPGetInfo(raw, len, &iw, &ih) || iw < 100 || ih < 50 || iw > 4000 || ih > 4000) die("image de fond illisible : %s", s->image); uint8_t *px = WebPDecodeRGB(raw, len, &iw, &ih); if (!px) die("image de fond illisible : %s", s->image); c.w = iw; c.h = ih; c.rgb = xmalloc((size_t)c.w * (size_t)c.h, 3); memcpy(c.rgb, px, (size_t)c.w * (size_t)c.h * 3); WebPFree(px); free(raw); } else { double k = cos((c.lat0 + c.lat1) / 2 * DEG); c.w = s->width; c.h = (int)lround(c.w * (c.lat1 - c.lat0) / ((c.lon1 - c.lon0) * k)); if (c.h < 50 || c.h > 4000) die("hauteur d'image invalide"); c.rgb = xmalloc((size_t)c.w * (size_t)c.h, 3); for (size_t q = 0; q < (size_t)c.w * (size_t)c.h; q++) { c.rgb[3 * q] = (uint8_t)(s->bg >> 16); c.rgb[3 * q + 1] = (uint8_t)(s->bg >> 8); c.rgb[3 * q + 2] = (uint8_t)s->bg; } } c.cov = xmalloc((size_t)c.w * (size_t)c.h, sizeof(float)); if (s->field[0]) draw_field(&c, cache_grd(s->field), s->layer, s->n100, s->nmodels); if (s->idw[0]) draw_idw(&c, s->idw, s->idw_km); if (s->geo[0]) { char p[1100]; uint32_t line = s->has_geocolor ? s->geocolor : 0x111111, dept = s->has_geocolor ? s->geocolor : 0x3a3a3a; if (s->geo_depts) { snprintf(p, sizeof p, "%s/dept.bin", s->geo); draw_polylines(&c, p, 0.35 * PT * s->geo_lw, dept, s->has_geocolor ? 0.55 : 0.75); } snprintf(p, sizeof p, "%s/border.bin", s->geo); draw_polylines(&c, p, 0.9 * PT * s->geo_lw, line, 1.0); snprintf(p, sizeof p, "%s/coast.bin", s->geo); draw_polylines(&c, p, 0.8 * PT * s->geo_lw, line, 1.0); } if (s->iso[0]) draw_isobars(&c, cache_grd(s->iso), s->iso_idx, s->iso_smooth); if (s->has_center) { double x, y; proj(&c, s->clon, s->clat, &x, &y); draw_text(&c, x, y, 1, 1, "D", 19 * PT, 1, 0xcc0000, 0xffffff, 3 * PT / 2, NULL, 0); double x2, y2; proj(&c, s->clon, s->clat - 0.42, &x2, &y2); char t[16]; snprintf(t, sizeof t, "%d", (int)lround(s->cp)); draw_text(&c, x2, y2, 1, 0, t, 11 * PT, 1, 0xcc0000, 0xffffff, 3 * PT / 2, NULL, 0); } if (s->dots[0]) { int n; Row *r = read_tsv(s->dots, &n); for (int q = 0; q < n; q++) { /* fichier déjà trié du plus faible au plus fort */ double x, y; proj(&c, r[q].lon, r[q].lat, &x, &y); disc(&c, x, y, sqrt(58.0) * PT / 2 - 0.6, gust_color(strtod(r[q].text, NULL)), 0.6 * PT, 0x111111); } free(r); } if (s->labels[0]) { int n; Row *r = read_tsv(s->labels, &n); for (int q = 0; q < n; q++) { double x, y; proj(&c, r[q].lon, r[q].lat, &x, &y); if (strncmp(r[q].text, "*", 1) != 0) /* « *texte » : étiquette sans point */ disc(&c, x, y, 3.2 * PT / 2, 0x111111, 0.8 * PT / 2, 0xffffff); double tx, ty; proj(&c, r[q].lon + 0.07, r[q].lat + 0.05, &tx, &ty); draw_text(&c, tx, ty, 0, 2, r[q].text[0] == '*' ? r[q].text + 1 : r[q].text, 10.5 * PT, 1, 0x111111, 0xffffff, 2.6 * PT / 2, NULL, 0); } free(r); } if (s->title[0]) draw_text(&c, 0.012 * c.w, 0.012 * c.h, 0, 0, s->title, 13 * PT, 1, 0x0b2a4a, 0xffffff, 3 * PT / 2, NULL, 0); if (s->sub[0]) draw_text(&c, 0.012 * c.w, 0.048 * c.h, 0, 0, s->sub, 10.5 * PT, 0, 0x0b2a4a, 0xffffff, 3 * PT / 2, NULL, 0); if (s->right[0]) { uint32_t bgc = 0x111111; draw_text(&c, 0.988 * c.w - 5, 0.012 * c.h + 5, 2, 0, s->right, 10.5 * PT, 0, 0xffffff, 0, 0, &bgc, 5); } if (s->sign[0]) draw_text(&c, 0.988 * c.w, 0.988 * c.h, 2, 2, s->sign, 9 * PT, 0, 0x111111, 0xffffff, 3 * PT / 2, NULL, 0); uint8_t *webp = NULL; size_t sz = WebPEncodeRGB(c.rgb, c.w, c.h, c.w * 3, (float)s->quality, &webp); if (!sz) die("encodage WebP en échec : %s", s->out); write_file_atomic(s->out, webp, sz); WebPFree(webp); free(c.rgb); free(c.cov); } static void copy_rest(char *dst, size_t n, const char *line, const char *key) { const char *v = line + strlen(key); while (*v == ' ') v++; if (strlen(v) >= n) die("valeur trop longue pour %s", key); strcpy(dst, v); } int cmd_render(int argc, char **argv) { if (argc != 1) die("usage : austen render "); if (FT_Init_FreeType(&ft) || FT_New_Face(ft, FONT_BOLD, 0, &face_bold) || FT_New_Face(ft, FONT_REG, 0, &face_reg)) die("polices DejaVu introuvables"); size_t len; char *buf = (char *)read_file(argv[0], 16 << 20, &len); Spec s; memset(&s, 0, sizeof s); s.quality = 85; s.width = 960; s.bg = 0xffffff; s.geo_lw = 1.0; int count = 0, lineno = 0; char *cur_ = buf; for (char *line = next_line(&cur_); line; line = next_line(&cur_)) { lineno++; char key[16] = ""; sscanf(line, "%15s", key); int ok = 1; if (!strcmp(key, "out")) copy_rest(s.out, sizeof s.out, line, key); else if (!strcmp(key, "quality")) ok = sscanf(line, "%*s %d", &s.quality) == 1; else if (!strcmp(key, "width")) ok = sscanf(line, "%*s %d", &s.width) == 1; else if (!strcmp(key, "box")) ok = sscanf(line, "%*s %lf %lf %lf %lf", &s.box[0], &s.box[1], &s.box[2], &s.box[3]) == 4; else if (!strcmp(key, "background")) { char col[16]; ok = sscanf(line, "%*s %15s", col) == 1; if (ok) s.bg = parse_color(col); } else if (!strcmp(key, "geo")) ok = sscanf(line, "%*s %1023s %d %lf", s.geo, &s.geo_depts, &s.geo_lw) == 3; else if (!strcmp(key, "field")) { char pal[16]; s.nmodels = 0; ok = sscanf(line, "%*s %1023s %d %15s %d", s.field, &s.layer, pal, &s.nmodels) >= 3; s.n100 = !strcmp(pal, "n100"); } else if (!strcmp(key, "isobars")) ok = sscanf(line, "%*s %1023s %d %d", s.iso, &s.iso_idx, &s.iso_smooth) == 3; else if (!strcmp(key, "center")) ok = (s.has_center = sscanf(line, "%*s %lf %lf %lf", &s.clon, &s.clat, &s.cp) == 3); else if (!strcmp(key, "labels")) copy_rest(s.labels, sizeof s.labels, line, key); else if (!strcmp(key, "image")) copy_rest(s.image, sizeof s.image, line, key); else if (!strcmp(key, "idw")) ok = sscanf(line, "%*s %1023s %lf", s.idw, &s.idw_km) == 2 && s.idw_km > 0 && s.idw_km < 500; else if (!strcmp(key, "geocolor")) { char col[16]; ok = sscanf(line, "%*s %15s", col) == 1; if (ok) { s.geocolor = parse_color(col); s.has_geocolor = 1; } } else if (!strcmp(key, "dots")) copy_rest(s.dots, sizeof s.dots, line, key); else if (!strcmp(key, "title")) copy_rest(s.title, sizeof s.title, line, key); else if (!strcmp(key, "sub")) copy_rest(s.sub, sizeof s.sub, line, key); else if (!strcmp(key, "right")) copy_rest(s.right, sizeof s.right, line, key); else if (!strcmp(key, "sign")) copy_rest(s.sign, sizeof s.sign, line, key); else if (!strcmp(key, "end")) { if (!s.out[0]) die("spec ligne %d : bloc sans sortie", lineno); render_one(&s); count++; int q = s.quality, w = s.width; uint32_t bg = s.bg; double lw = s.geo_lw; char geo[1024]; int gd = s.geo_depts; memcpy(geo, s.geo, sizeof geo); memset(&s, 0, sizeof s); s.quality = q; /* réglages d'aspect conservés d'un bloc au suivant */ s.width = w; s.bg = bg; s.geo_lw = lw; s.geo_depts = gd; memcpy(s.geo, geo, sizeof geo); } else ok = 0; if (!ok) die("spec ligne %d invalide : %.60s", lineno, line); } free(buf); printf("{\"images\":%d}\n", count); return 0; }