1/* $Id$ $Revision$ */
2/* vim:set shiftwidth=4 ts=8: */
3
4/*************************************************************************
5 * Copyright (c) 2011 AT&T Intellectual Property
6 * All rights reserved. This program and the accompanying materials
7 * are made available under the terms of the Eclipse Public License v1.0
8 * which accompanies this distribution, and is available at
9 * http://www.eclipse.org/legal/epl-v10.html
10 *
11 * Contributors: See CVS logs. Details at http://www.graphviz.org/
12 *************************************************************************/
13
14#include "config.h"
15
16#include <stdlib.h>
17#include <stddef.h>
18#include <stdint.h>
19#include <string.h>
20#include <fcntl.h>
21
22#include "gvplugin_render.h"
23#include "gvplugin_device.h"
24#include "gvcint.h" /* for gvc->g for agget */
25#include "gd.h"
26
27#ifndef INT32_MAX
28#define INT32_MAX (2147483647)
29#endif
30#ifndef UINT32_MAX
31#define UINT32_MAX (4294967295U)
32#endif
33
34
35typedef enum {
36 FORMAT_GIF,
37 FORMAT_JPEG,
38 FORMAT_PNG,
39 FORMAT_WBMP,
40 FORMAT_GD,
41 FORMAT_GD2,
42 FORMAT_XBM,
43} format_type;
44
45extern boolean mapbool(char *);
46extern pointf Bezier(pointf * V, int degree, double t, pointf * Left, pointf * Right);
47
48#define BEZIERSUBDIVISION 10
49
50static void gdgen_resolve_color(GVJ_t * job, gvcolor_t * color)
51{
52 gdImagePtr im = (gdImagePtr) job->context;
53 int alpha;
54
55 if (!im)
56 return;
57
58 /* convert alpha (normally an "opacity" value) to gd's "transparency" */
59 alpha = (255 - color->u.rgba[3]) * gdAlphaMax / 255;
60
61 if(alpha == gdAlphaMax)
62 color->u.index = gdImageGetTransparent(im);
63 else
64 color->u.index = gdImageColorResolveAlpha(im,
65 color->u.rgba[0],
66 color->u.rgba[1],
67 color->u.rgba[2],
68 alpha);
69 color->type = COLOR_INDEX;
70}
71
72static int white, black, transparent, basecolor;
73
74#define GD_XYMAX INT32_MAX
75
76static void gdgen_begin_page(GVJ_t * job)
77{
78 char *bgcolor_str = NULL, *truecolor_str = NULL;
79 boolean truecolor_p = FALSE; /* try to use cheaper paletted mode */
80 boolean bg_transparent_p = FALSE;
81 gdImagePtr im = NULL;
82
83 truecolor_str = agget((graph_t*)(job->gvc->g), "truecolor"); /* allow user to force truecolor */
84 bgcolor_str = agget((graph_t*)(job->gvc->g), "bgcolor");
85
86 if (truecolor_str && truecolor_str[0])
87 truecolor_p = mapbool(truecolor_str);
88
89 if (bgcolor_str && strcmp(bgcolor_str, "transparent") == 0) {
90 bg_transparent_p = TRUE;
91 if (job->render.features->flags & GVDEVICE_DOES_TRUECOLOR)
92 truecolor_p = TRUE; /* force truecolor */
93 }
94
95 if (GD_has_images(job->gvc->g))
96 truecolor_p = TRUE; /* force truecolor */
97
98 if (job->external_context) {
99 if (job->common->verbose)
100 fprintf(stderr, "%s: using existing GD image\n", job->common->cmdname);
101 im = (gdImagePtr) (job->context);
102 } else {
103 if (job->width * job->height >= GD_XYMAX) {
104 double scale = sqrt(GD_XYMAX / (job->width * job->height));
105 job->width *= scale;
106 job->height *= scale;
107 job->zoom *= scale;
108 fprintf(stderr,
109 "%s: graph is too large for gd-renderer bitmaps. Scaling by %g to fit\n",
110 job->common->cmdname, scale);
111 }
112 if (truecolor_p) {
113 if (job->common->verbose)
114 fprintf(stderr,
115 "%s: allocating a %dK TrueColor GD image (%d x %d pixels)\n",
116 job->common->cmdname,
117 ROUND(job->width * job->height * 4 / 1024.),
118 job->width, job->height);
119 im = gdImageCreateTrueColor(job->width, job->height);
120 } else {
121 if (job->common->verbose)
122 fprintf(stderr,
123 "%s: allocating a %dK PaletteColor GD image (%d x %d pixels)\n",
124 job->common->cmdname,
125 ROUND(job->width * job->height / 1024.),
126 job->width, job->height);
127 im = gdImageCreate(job->width, job->height);
128 }
129 job->context = (void *) im;
130 }
131
132 if (!im) {
133 job->common->errorfn("gdImageCreate returned NULL. Malloc problem?\n");
134 return;
135 }
136
137 /* first color is the default background color */
138 /* - used for margins - if any */
139 transparent = gdImageColorResolveAlpha(im,
140 gdRedMax - 1, gdGreenMax,
141 gdBlueMax, gdAlphaTransparent);
142 gdImageColorTransparent(im, transparent);
143
144 white = gdImageColorResolveAlpha(im,
145 gdRedMax, gdGreenMax, gdBlueMax,
146 gdAlphaOpaque);
147
148 black = gdImageColorResolveAlpha(im, 0, 0, 0, gdAlphaOpaque);
149
150 /* Blending must be off to lay a transparent basecolor.
151 Nothing to blend with anyway. */
152 gdImageAlphaBlending(im, FALSE);
153 gdImageFill(im, im->sx / 2, im->sy / 2, transparent);
154 /* Blend everything else together,
155 especially fonts over non-transparent backgrounds */
156 gdImageAlphaBlending(im, TRUE);
157}
158
159extern int gvdevice_gd_putBuf (gdIOCtx *context, const void *buffer, int len);
160extern void gvdevice_gd_putC (gdIOCtx *context, int C);
161
162static void gdgen_end_page(GVJ_t * job)
163{
164 gdImagePtr im = (gdImagePtr) job->context;
165
166 gdIOCtx ctx;
167
168 ctx.putBuf = gvdevice_gd_putBuf;
169 ctx.putC = gvdevice_gd_putC;
170 ctx.tell = (void*)job; /* hide *job here */
171
172 if (!im)
173 return;
174 if (job->external_context) {
175 /* leave image in memory to be handled by Gdtclft output routines */
176#ifdef MYTRACE
177 fprintf(stderr, "gdgen_end_graph (to memory)\n");
178#endif
179 } else {
180 /* Only save the alpha channel in outputs that support it if
181 the base color was transparent. Otherwise everything
182 was blended so there is no useful alpha info */
183 gdImageSaveAlpha(im, (basecolor == transparent));
184 switch (job->render.id) {
185 case FORMAT_GIF:
186#ifdef HAVE_GD_GIF
187 gdImageTrueColorToPalette(im, 0, 256);
188 gdImageGifCtx(im, &ctx);
189#endif
190 break;
191 case FORMAT_JPEG:
192#ifdef HAVE_GD_JPEG
193 /*
194 * Write IM to OUTFILE as a JFIF-formatted JPEG image, using
195 * quality JPEG_QUALITY. If JPEG_QUALITY is in the range
196 * 0-100, increasing values represent higher quality but also
197 * larger image size. If JPEG_QUALITY is negative, the
198 * IJG JPEG library's default quality is used (which should
199 * be near optimal for many applications). See the IJG JPEG
200 * library documentation for more details. */
201#define JPEG_QUALITY -1
202 gdImageJpegCtx(im, &ctx, JPEG_QUALITY);
203#endif
204
205 break;
206 case FORMAT_PNG:
207#ifdef HAVE_GD_PNG
208 gdImagePngCtx(im, &ctx);
209#endif
210 break;
211
212#ifdef HAVE_GD_GIF
213 case FORMAT_WBMP:
214 {
215 /* Use black for the foreground color for the B&W wbmp image. */
216 int black = gdImageColorResolveAlpha(im, 0, 0, 0, gdAlphaOpaque);
217 gdImageWBMPCtx(im, black, &ctx);
218 }
219 break;
220#endif
221
222 case FORMAT_GD:
223 gdImageGd(im, job->output_file);
224 break;
225
226#ifdef HAVE_LIBZ
227 case FORMAT_GD2:
228#define GD2_CHUNKSIZE 128
229#define GD2_RAW 1
230#define GD2_COMPRESSED 2
231 gdImageGd2(im, job->output_file, GD2_CHUNKSIZE, GD2_COMPRESSED);
232 break;
233#endif
234
235 case FORMAT_XBM:
236#if 0
237/* libgd support only reading .xpm files */
238#ifdef HAVE_GD_XPM
239 gdImageXbm(im, job->output_file);
240#endif
241#endif
242 break;
243 }
244 gdImageDestroy(im);
245#ifdef MYTRACE
246 fprintf(stderr, "gdgen_end_graph (to file)\n");
247#endif
248 job->context = NULL;
249 }
250}
251
252static void gdgen_missingfont(char *err, char *fontreq)
253{
254 static char *lastmissing = 0;
255 static int n_errors = 0;
256
257 if (n_errors >= 20)
258 return;
259 if ((lastmissing == 0) || (strcmp(lastmissing, fontreq))) {
260#ifdef HAVE_GD_FONTCONFIG
261#if 0
262/* FIXME - error function */
263 agerr(AGERR, "%s : %s\n", err, fontreq);
264#endif
265#else
266 char *p = getenv("GDFONTPATH");
267 if (!p)
268 p = DEFAULT_FONTPATH;
269#if 0
270/* FIXME - error function */
271 agerr(AGERR, "%s : %s in %s\n", err, fontreq, p);
272#endif
273#endif
274 if (lastmissing)
275 free(lastmissing);
276 lastmissing = strdup(fontreq);
277 n_errors++;
278#if 0
279/* FIXME - error function */
280 if (n_errors >= 20)
281 agerr(AGWARN, "(font errors suppressed)\n");
282#endif
283 }
284}
285
286/* fontsize at which text is omitted entirely */
287#define FONTSIZE_MUCH_TOO_SMALL 0.15
288/* fontsize at which text is rendered by a simple line */
289#define FONTSIZE_TOO_SMALL 1.5
290
291#ifdef _WIN32
292# define GD_IMPORT __declspec(dllimport)
293#else
294# define GD_IMPORT extern
295#endif
296GD_IMPORT gdFontPtr gdFontTiny, gdFontSmall, gdFontMediumBold, gdFontLarge, gdFontGiant;
297
298void gdgen_text(gdImagePtr im, pointf spf, pointf epf, int fontcolor, double fontsize, int fontdpi, double fontangle, char *fontname, char *str)
299{
300 gdFTStringExtra strex;
301 point sp, ep; /* start point, end point, in pixels */
302
303 PF2P(spf, sp);
304 PF2P(epf, ep);
305
306 strex.flags = gdFTEX_RESOLUTION;
307 strex.hdpi = strex.vdpi = fontdpi;
308
309 if (strstr(fontname, "/"))
310 strex.flags |= gdFTEX_FONTPATHNAME;
311 else
312 strex.flags |= gdFTEX_FONTCONFIG;
313
314 if (fontsize <= FONTSIZE_MUCH_TOO_SMALL) {
315 /* ignore entirely */
316 } else if (fontsize <= FONTSIZE_TOO_SMALL) {
317 /* draw line in place of text */
318 gdImageLine(im, sp.x, sp.y, ep.x, ep.y, fontcolor);
319 } else {
320#ifdef HAVE_GD_FREETYPE
321 char *err;
322 int brect[8];
323#ifdef HAVE_GD_FONTCONFIG
324 char* fontlist = fontname;
325#else
326 extern char *gd_alternate_fontlist(char *font);
327 char* fontlist = gd_alternate_fontlist(fontname);
328#endif
329 err = gdImageStringFTEx(im, brect, fontcolor,
330 fontlist, fontsize, fontangle, sp.x, sp.y, str, &strex);
331
332 if (err) {
333 /* revert to builtin fonts */
334 gdgen_missingfont(err, fontname);
335#endif
336 sp.y += 2;
337 if (fontsize <= 8.5) {
338 gdImageString(im, gdFontTiny, sp.x, sp.y - 9, (unsigned char*)str, fontcolor);
339 } else if (fontsize <= 9.5) {
340 gdImageString(im, gdFontSmall, sp.x, sp.y - 12, (unsigned char*)str, fontcolor);
341 } else if (fontsize <= 10.5) {
342 gdImageString(im, gdFontMediumBold, sp.x, sp.y - 13, (unsigned char*)str, fontcolor);
343 } else if (fontsize <= 11.5) {
344 gdImageString(im, gdFontLarge, sp.x, sp.y - 14, (unsigned char*)str, fontcolor);
345 } else {
346 gdImageString(im, gdFontGiant, sp.x, sp.y - 15, (unsigned char*)str, fontcolor);
347 }
348#ifdef HAVE_GD_FREETYPE
349 }
350#endif
351 }
352}
353
354extern char* gd_psfontResolve (PostscriptAlias* pa);
355
356static void gdgen_textspan(GVJ_t * job, pointf p, textspan_t * span)
357{
358 gdImagePtr im = (gdImagePtr) job->context;
359 pointf spf, epf;
360 double spanwidth = span->size.x * job->zoom * job->dpi.x / POINTS_PER_INCH;
361 char* fontname;
362#ifdef HAVE_GD_FONTCONFIG
363 PostscriptAlias *pA;
364#endif
365
366 if (!im)
367 return;
368
369 switch (span->just) {
370 case 'l':
371 spf.x = 0.0;
372 break;
373 case 'r':
374 spf.x = -spanwidth;
375 break;
376 default:
377 case 'n':
378 spf.x = -spanwidth / 2;
379 break;
380 }
381 epf.x = spf.x + spanwidth;
382
383 if (job->rotation) {
384 spf.y = -spf.x + p.y;
385 epf.y = epf.x + p.y;
386 epf.x = spf.x = p.x;
387 }
388 else {
389 spf.x += p.x;
390 epf.x += p.x;
391 epf.y = spf.y = p.y - span->yoffset_centerline * job->zoom * job->dpi.x / POINTS_PER_INCH;
392 }
393
394#ifdef HAVE_GD_FONTCONFIG
395 pA = span->font->postscript_alias;
396 if (pA)
397 fontname = gd_psfontResolve (pA);
398 else
399#endif
400 fontname = span->font->name;
401
402 gdgen_text(im, spf, epf,
403 job->obj->pencolor.u.index,
404 span->font->size * job->zoom,
405 job->dpi.x,
406 job->rotation ? (M_PI / 2) : 0,
407 fontname,
408 span->str);
409}
410
411static int gdgen_set_penstyle(GVJ_t * job, gdImagePtr im, gdImagePtr* brush)
412{
413 obj_state_t *obj = job->obj;
414 int i, pen, width, dashstyle[40];
415
416 if (obj->pen == PEN_DASHED) {
417 for (i = 0; i < 10; i++)
418 dashstyle[i] = obj->pencolor.u.index;
419 for (; i < 20; i++)
420 dashstyle[i] = gdTransparent;
421 gdImageSetStyle(im, dashstyle, 20);
422 pen = gdStyled;
423 } else if (obj->pen == PEN_DOTTED) {
424 for (i = 0; i < 2; i++)
425 dashstyle[i] = obj->pencolor.u.index;
426 for (; i < 14; i++)
427 dashstyle[i] = gdTransparent;
428 gdImageSetStyle(im, dashstyle, 12);
429 pen = gdStyled;
430 } else {
431 pen = obj->pencolor.u.index;
432 }
433
434 width = obj->penwidth * job->zoom;
435 if (width < PENWIDTH_NORMAL)
436 width = PENWIDTH_NORMAL; /* gd can't do thin lines */
437 gdImageSetThickness(im, width);
438 /* use brush instead of Thickness to improve end butts */
439 if (width != PENWIDTH_NORMAL) {
440 if (im->trueColor) {
441 *brush = gdImageCreateTrueColor(width,width);
442 }
443 else {
444 *brush = gdImageCreate(width, width);
445 gdImagePaletteCopy(*brush, im);
446 }
447 gdImageFilledRectangle(*brush, 0, 0, width - 1, width - 1,
448 obj->pencolor.u.index);
449 gdImageSetBrush(im, *brush);
450 if (pen == gdStyled)
451 pen = gdStyledBrushed;
452 else
453 pen = gdBrushed;
454 }
455
456 return pen;
457}
458
459static void
460gdgen_bezier(GVJ_t * job, pointf * A, int n, int arrow_at_start,
461 int arrow_at_end, int filled)
462{
463 obj_state_t *obj = job->obj;
464 gdImagePtr im = (gdImagePtr) job->context;
465 pointf p0, p1, V[4];
466 int i, j, step, pen;
467 boolean pen_ok, fill_ok;
468 gdImagePtr brush = NULL;
469 gdPoint F[4];
470
471 if (!im)
472 return;
473
474 pen = gdgen_set_penstyle(job, im, &brush);
475 pen_ok = (pen != gdImageGetTransparent(im));
476 fill_ok = (filled && obj->fillcolor.u.index != gdImageGetTransparent(im));
477
478 if (pen_ok || fill_ok) {
479 V[3] = A[0];
480 PF2P(A[0], F[0]);
481 PF2P(A[n-1], F[3]);
482 for (i = 0; i + 3 < n; i += 3) {
483 V[0] = V[3];
484 for (j = 1; j <= 3; j++)
485 V[j] = A[i + j];
486 p0 = V[0];
487 for (step = 1; step <= BEZIERSUBDIVISION; step++) {
488 p1 = Bezier(V, 3, (double) step / BEZIERSUBDIVISION, NULL, NULL);
489 PF2P(p0, F[1]);
490 PF2P(p1, F[2]);
491 if (pen_ok)
492 gdImageLine(im, F[1].x, F[1].y, F[2].x, F[2].y, pen);
493 if (fill_ok)
494 gdImageFilledPolygon(im, F, 4, obj->fillcolor.u.index);
495 p0 = p1;
496 }
497 }
498 }
499 if (brush)
500 gdImageDestroy(brush);
501}
502
503static gdPoint *points;
504static int points_allocated;
505
506static void gdgen_polygon(GVJ_t * job, pointf * A, int n, int filled)
507{
508 obj_state_t *obj = job->obj;
509 gdImagePtr im = (gdImagePtr) job->context;
510 gdImagePtr brush = NULL;
511 int i;
512 int pen;
513 boolean pen_ok, fill_ok;
514
515 if (!im)
516 return;
517
518 pen = gdgen_set_penstyle(job, im, &brush);
519 pen_ok = (pen != gdImageGetTransparent(im));
520 fill_ok = (filled && obj->fillcolor.u.index != gdImageGetTransparent(im));
521
522 if (pen_ok || fill_ok) {
523 if (n > points_allocated) {
524 points = realloc(points, n * sizeof(gdPoint));
525 points_allocated = n;
526 }
527 for (i = 0; i < n; i++) {
528 points[i].x = ROUND(A[i].x);
529 points[i].y = ROUND(A[i].y);
530 }
531 if (fill_ok)
532 gdImageFilledPolygon(im, points, n, obj->fillcolor.u.index);
533
534 if (pen_ok)
535 gdImagePolygon(im, points, n, pen);
536 }
537 if (brush)
538 gdImageDestroy(brush);
539}
540
541static void gdgen_ellipse(GVJ_t * job, pointf * A, int filled)
542{
543 obj_state_t *obj = job->obj;
544 gdImagePtr im = (gdImagePtr) job->context;
545 double dx, dy;
546 int pen;
547 boolean pen_ok, fill_ok;
548 gdImagePtr brush = NULL;
549
550 if (!im)
551 return;
552
553 pen = gdgen_set_penstyle(job, im, &brush);
554 pen_ok = (pen != gdImageGetTransparent(im));
555 fill_ok = (filled && obj->fillcolor.u.index != gdImageGetTransparent(im));
556
557 dx = 2 * (A[1].x - A[0].x);
558 dy = 2 * (A[1].y - A[0].y);
559
560 if (fill_ok)
561 gdImageFilledEllipse(im, ROUND(A[0].x), ROUND(A[0].y),
562 ROUND(dx), ROUND(dy),
563 obj->fillcolor.u.index);
564 if (pen_ok)
565 gdImageArc(im, ROUND(A[0].x), ROUND(A[0].y), ROUND(dx), ROUND(dy),
566 0, 360, pen);
567 if (brush)
568 gdImageDestroy(brush);
569}
570
571static void gdgen_polyline(GVJ_t * job, pointf * A, int n)
572{
573 gdImagePtr im = (gdImagePtr) job->context;
574 pointf p, p1;
575 int i;
576 int pen;
577 boolean pen_ok;
578 gdImagePtr brush = NULL;
579
580 if (!im)
581 return;
582
583 pen = gdgen_set_penstyle(job, im, &brush);
584 pen_ok = (pen != gdImageGetTransparent(im));
585
586 if (pen_ok) {
587 p = A[0];
588 for (i = 1; i < n; i++) {
589 p1 = A[i];
590 gdImageLine(im, ROUND(p.x), ROUND(p.y),
591 ROUND(p1.x), ROUND(p1.y), pen);
592 p = p1;
593 }
594 }
595 if (brush)
596 gdImageDestroy(brush);
597}
598
599static gvrender_engine_t gdgen_engine = {
600 0, /* gdgen_begin_job */
601 0, /* gdgen_end_job */
602 0, /* gdgen_begin_graph */
603 0, /* gdgen_end_graph */
604 0, /* gdgen_begin_layer */
605 0, /* gdgen_end_layer */
606 gdgen_begin_page,
607 gdgen_end_page,
608 0, /* gdgen_begin_cluster */
609 0, /* gdgen_end_cluster */
610 0, /* gdgen_begin_nodes */
611 0, /* gdgen_end_nodes */
612 0, /* gdgen_begin_edges */
613 0, /* gdgen_end_edges */
614 0, /* gdgen_begin_node */
615 0, /* gdgen_end_node */
616 0, /* gdgen_begin_edge */
617 0, /* gdgen_end_edge */
618 0, /* gdgen_begin_anchor */
619 0, /* gdgen_end_anchor */
620 0, /* gdgen_begin_label */
621 0, /* gdgen_end_label */
622 gdgen_textspan,
623 gdgen_resolve_color,
624 gdgen_ellipse,
625 gdgen_polygon,
626 gdgen_bezier,
627 gdgen_polyline,
628 0, /* gdgen_comment */
629 0, /* gdgen_library_shape */
630};
631
632static gvrender_features_t render_features_gd = {
633 GVRENDER_Y_GOES_DOWN, /* flags */
634 4., /* default pad - graph units */
635 NULL, /* knowncolors */
636 0, /* sizeof knowncolors */
637 RGBA_BYTE, /* color_type */
638};
639
640static gvdevice_features_t device_features_gd = {
641 GVDEVICE_BINARY_FORMAT, /* flags */
642 {0.,0.}, /* default margin - points */
643 {0.,0.}, /* default page width, height - points */
644 {96.,96.}, /* default dpi */
645};
646
647static gvdevice_features_t device_features_gd_tc = {
648 GVDEVICE_BINARY_FORMAT
649 | GVDEVICE_DOES_TRUECOLOR,/* flags */
650 {0.,0.}, /* default margin - points */
651 {0.,0.}, /* default page width, height - points */
652 {96.,96.}, /* default dpi */
653};
654
655static gvdevice_features_t device_features_gd_tc_no_writer = {
656 GVDEVICE_BINARY_FORMAT
657 | GVDEVICE_DOES_TRUECOLOR
658 | GVDEVICE_NO_WRITER, /* flags */
659 {0.,0.}, /* default margin - points */
660 {0.,0.}, /* default page width, height - points */
661 {96.,96.}, /* default dpi */
662};
663
664gvplugin_installed_t gvrender_gd_types[] = {
665 {FORMAT_GD, "gd", 1, &gdgen_engine, &render_features_gd},
666 {0, NULL, 0, NULL, NULL}
667};
668
669gvplugin_installed_t gvdevice_gd_types2[] = {
670#ifdef HAVE_GD_GIF
671 {FORMAT_GIF, "gif:gd", 1, NULL, &device_features_gd_tc}, /* pretend gif is truecolor because it supports transparency */
672 {FORMAT_WBMP, "wbmp:gd", 1, NULL, &device_features_gd},
673#endif
674
675#ifdef HAVE_GD_JPEG
676 {FORMAT_JPEG, "jpe:gd", 1, NULL, &device_features_gd},
677 {FORMAT_JPEG, "jpeg:gd", 1, NULL, &device_features_gd},
678 {FORMAT_JPEG, "jpg:gd", 1, NULL, &device_features_gd},
679#endif
680
681#ifdef HAVE_GD_PNG
682 {FORMAT_PNG, "png:gd", 1, NULL, &device_features_gd_tc},
683#endif
684
685 {FORMAT_GD, "gd:gd", 1, NULL, &device_features_gd_tc_no_writer},
686
687#ifdef HAVE_LIBZ
688 {FORMAT_GD2, "gd2:gd", 1, NULL, &device_features_gd_tc_no_writer},
689#endif
690
691#if 0
692/* libgd has no support for xbm as output */
693#ifdef HAVE_GD_XPM
694 {FORMAT_XBM, "xbm:gd", 1, NULL, &device_features_gd},
695#endif
696#endif
697 {0, NULL, 0, NULL, NULL}
698};
699