Releases: iliaal/fastchart
fastchart 1.1.0
Added
-
Funnel::setStyle(STYLE_CONE)— pyramid layout with front-facing ellipse-arc edges at each band's top and bottom, suggesting a 3D cone seen from the side. Layout is identical toSTYLE_PYRAMID(apex at top, base at bottom, band heights proportional to value); only the silhouette changes. Seedocs/examples/52_funnel_cone.php. -
AreaChart::setBandMode(bool)— with exactly two series the chart fills the envelope between them instead of filling each series down to the baseline.series[0]is the upper bound,series[1]is the lower. Useful for confidence intervals, forecast ranges, min/max envelopes. Silently falls back to the per-series fill whenn_series != 2orsetStacked(true)is also active. Seedocs/examples/53_area_band.php. -
PolarChart::setInterpolation(int $mode)—INTERP_LINEAR(default) connects points with straight segments;INTERP_SMOOTHruns Catmull-Rom subdivision through each segment for a curved fit. Markers anchor to the original data points. Ignored inSTYLE_ROSE. -
PolarChart::addVectors(array $vectors)— overlay arrow vectors anchored in the same(angle, radius)data space as the series. Each entry is['angle' => float, 'radius' => float, 'angle_to' => float, 'radius_to' => float, 'color' => int?]. Useful for wind / flow / phase plots. Atomic commit: a malformed entry mid-list aborts the entire call without partial state. Seedocs/examples/54_polar_smooth_vectors.php. -
BubbleChart::setYAxisScale(SCALE_LOG)— log-10 Y axis for exponential-shaped data. Matches the existing Line / Bar / Area / Scatter / Stock log-axis API. Y values must be strictly positive. Seedocs/examples/55_bubble_log_axis.php. -
Chart::setImageMap(array $entries)+Chart::getImageMap(string $name = 'fastchart')— per-data-point HTML image-map hot-spots on the Chart base class. Entries are index-aligned withsetSeries() / setSlices() / setPoints(). The renderer captures hot-spot geometry during draw;getImageMap()emits the matching<map>markup. Currently wired up for BarChart (rect per category) and PieChart (poly per slice); ScatterChart's existing per-pointsetPoints'href' / 'tooltip'keys are preserved. AreaChart, BubbleChart, LineChart, and StockChart use the same infrastructure but their renderers don't push hot-spots yet (planned for v1.2). URL scheme is allowlisted (http / https / mailto / relative/or#only). Tooltip text is HTML-escaped. Embedded NUL bytes in href / tooltip are rejected. Seedocs/examples/56_image_map_bar_pie.php.
Fixed
-
stb_image PNG palette OOB-read that leaked uninitialized stack bytes into the rendered raster on paletted PNGs whose pixel data references indices
>= pal_len. Vendoredvendor/plutovg/source/plutovg-stb-image.hpatched to zero-init the 1024-byte stackpalette[]at function entry. Local patch marked with afastchart-localcomment so the next vendor refresh re-applies. -
PNG IDAT chunk-length amplifier that allowed a 30-byte malicious PNG to drive stb_image into a ~1 GB realloc before reading any payload, OOM-killing the worker. Added
fc_validate_png_chunksinfastchart_target.cthat walks the chunk list before handoff and rejects declared lengths that would overrun the buffer. -
Chart::setImageMap()use-after-free when the caller didsetImageMap → render → setImageMap → getImageMapwithout an intervening render. The cachedimage_map_areasarray held borrowedhref / tooltippointers intoimage_map_entries[]; the secondsetImageMapfreed those entries while areas still pointed at them. setImageMap now resets areas before freeing entries. -
PolarChart::addVectors()partial commit on mid-loop throw. A malformed entry in the middle of the input array would advancen_vectorsfor the prefix that already wrote, leaving the object in a half-populated state that a retry would duplicate on top of. The setter now stages entries into a temporary buffer and only commits toself->vectorsafter every entry validates. -
Userland-subclass instantiation no longer corrupts heap. Both
FastChart\ChartandFastChart\Symbol(andFastChart\Barcode) abstract bases now install a sentinelcreate_objecthandler.class MyChart extends FastChart\Chart {}(with or without a userland__construct) was previously triggering either a debug assertion or a Z_FASTCHART_OBJ_P heap scribble; it now throws a cleanErrorwith a clear message at instantiation time.
Changed
-
Vendored stb_image attack surface narrowed.
STBI_ONLY_PNG+STBI_ONLY_JPEGdefined beforeSTB_IMAGE_IMPLEMENTATIONinvendor/plutovg/source/plutovg-surface.c, eliminating the BMP / GIF / PSD / TGA / HDR / PIC / PNM decoders from the linked binary. fastchart's MIME sniff already gated these formats at the caller; this closes the defense-in-depth gap. -
FASTCHART_IMAGE_MAP_MAX_COORDSraised from 16 to 32. PieChart poly hot-spots use 14 ints (center + 6 arc samples); the cap bump leaves headroom for denser sampling without bumping the per-area fixed-size coord buffer at the call sites. -
Test suite: 118 → 127 phpts. Six v1.1 feature tests (153 – 158), three review-fix regression tests (159 – 161).
fastchart 1.0.2
A build/packaging patch: the 1.0.1 Linux prebuilt binaries linked against the build host's libjpeg.so.8 (Ubuntu's libjpeg-turbo soname) and failed to load on Debian-based PHP, including every official php:X.Y-cli Docker image where Debian's libjpeg-turbo ships as libjpeg.so.62 instead. Debian provides no package that satisfies libjpeg.so.8, so pie install iliaal/fastchart on a Debian PHP fell back to source build.
Fixed
- Linux prebuilt binaries now portable across distros. Each
.soproduced by.github/workflows/release-linux.ymlstatically links freetype + libpng + libjpeg-turbo + libwebp from./configure-time source builds (pinned versions in workflow env).--with-fastchart-static-codecs=<prefix>is the newconfig.m4flag that drives it: prepends the prefix toPKG_CONFIG_PATH, switches every pkg-config call to--static, and appends-Wl,--exclude-libs=ALLso the codec-archive symbols stay local infastchart.so. Without--exclude-libs, loadingext/gd(dynamically linked against the system libjpeg) in the same process collides onjpeg_CreateCompresswith two differentJPEG_LIB_VERSIONvalues and produces "encoder produced no output" on the firstrenderJpeg(). readelf -d fastchart.soNEEDED entries trimmed fromlibjpeg.so.8,libfreetype.so.6,libpng16.so.16,libwebp.so.7,libz.so.1,libm,libc→ justlibz,libm,libc,ld-linux. Works on Ubuntu, Debian, RHEL, anywhere glibc + zlib are present.- macOS prebuilts unaffected. Mach-O resolves by install-name (absolute path baked at link time), not soname major, so the Ubuntu↔Debian issue doesn't manifest. The macOS path keeps dynamic linking against Homebrew kegs.
Changed
release-linux.ymlgains aworkflow_dispatchtrigger so maintainers can validate the recipe end-to-end without cutting a release tag. The release-asset upload step is gated ongithub.event_name == 'release'; manual runs build + verify but do not upload.release-linux.ymladds aVerify codec libs are staticstep that fails the job iflibjpeg,libpng,libwebp, orlibfreetypeappears in the produced.so's NEEDED entries — catches link-flag regressions immediately.- Static-built codec libs cached via
actions/cache@v4keyed on the pinned lib versions (env:FT_VERSION,PNG_VERSION,JPEG_VERSION,WEBP_VERSION). First build ~4min; subsequent cache-hit builds skip the source build.
Trade-off: .so size grows from ~4MB → ~6.8MB on Linux. Acceptable in exchange for cross-distro portability.
1.0.1
This is the first release with prebuilt binaries on GitHub Releases. No API changes — every change in this release is build / packaging / ZTS-correctness.
Added
-
Prebuilt binaries on GitHub Releases.
composer.jsonnow declaresdownload-url-method: ["pre-packaged-binary", "composer-default"], so PIE prefers a prebuilt asset and falls back to source build when no asset matches the install target. Two new workflows attach binaries on release publish:.github/workflows/windows.yml— Windows DLLs for PHP-8.3/8.4/8.5 (NTS + TS, x64 + x86) viaphp/php-windows-builder. System deps (freetype, libpng, libjpeg-turbo, libwebp) come through the action'slibs:input and resolve via the PHP-on-Windows SDK deps server..github/workflows/release-linux.yml— Linux x86_64 (ubuntu-24.04), Linux arm64 (ubuntu-24.04-arm), and macOS arm64 (macos-14).sobinaries for PHP-8.4 and 8.5 (NTS) viaphp/pie-ext-binary-builder. apt-get / brew install the four deps before the build.
PHP-8.3 on Linux/macOS, macOS Intel, and Alpine musl users continue to source-build via PIE's composer-default fallback.
-
config.w32— Windows build manifest mirroringconfig.m4. All 29 wrapper sources + 12 vendor sources (qrcodegen + plutovg + plutosvg), FreeType mandatory viaCHECK_LIB("freetype_a.lib;...")+CHECK_HEADER_ADD_INCLUDE("ft2build.h", ..., ..\deps\include\ freetype2), libpng / libjpeg-turbo / libwebp optional with the sameHAVE_LIBxxxdefines the Unix side uses.
Fixed
- ZTS module globals not zero-initialised. Under NTS the globals struct lives in BSS so the linker zero-fills it; under ZTS it is heap-allocated per thread by TSRM and the contents are whatever the heap had. On Linux ZTS that often happened to be zero (fresh page from anonymous mmap) and the bug stayed latent; on Windows x64 ZTS the random content looked like a live
FT_Libraryhandle, soFT_Done_Face/FT_New_Facedereferenced garbage and everyrenderXxx()call segfaulted at first use. Explicitmemset(fastchart_globals, 0, sizeof(*fastchart_globals))in the newPHP_GINIT_FUNCTIONcloses both. Valgrind on Linux ZTS now reports zero errors from zero contexts (was 14 from 7). - TSRMLS cache uninitialised in dynamically-loaded ZTS modules. Added
ZEND_TSRMLS_CACHE_DEFINE()at theCOMPILE_DL_FASTCHARTlevel andZEND_TSRMLS_CACHE_UPDATE()inPHP_GINIT_FUNCTION. Without these, everyFASTCHART_G(...)dereference from a loaded DSO under ZTS reads an undefined__declspec(thread)slot — silent on Linux ZTS (GCC__threadhas weaker linkage), segfault on Windows ZTS at first access. Mirrors what ext/intl, ext/mbstring, ext/curl have always done. - Default font path no longer a shared interned
zend_string. The interned-permanent shared string was a v1.0 round-2 fix for a refcount race under ZTS. After surfacing the deeper ZTS bug above it adds nothing, so it is gone. Each chart nowzend_string_inits its own font_path from aconst char *pointing into a static string-literal table. ~32 extra bytes per chart, no cross-thread refcount math. - Windows font candidates for the default-font probe. The probe previously listed only
/usr/share,/Library, and/System/ Librarypaths; on Windows the probe returned NULL, every chart started withfont_path == NULL, and every text-rendering call silently no-op'd. AddedC:\Windows\Fonts\arial.ttfandC:\Windows\Fonts\segoeui.ttf. - MSVC C2057 in
fastchart_stock.c.const int baseT = 10followed byint dq[baseT + 1]is portable C99 (a VLA on GCC / Clang but with a compile-time-constant bound) — MSVC's C front-end rejects it as "expected constant expression". ConvertedbaseTto a#define(scoped#undefat function end). All 17 other uses in arithmetic / bounds checks work unchanged with macro substitution.
Changed
tests/089_font_cache_open_basedir.phpt— moved the/usr/share-font probe into--SKIPIF--. The test was an earlyecho "skip: ..."+exit;inside--FILE--, which run-tests.php treated as a failed assertion (no--SKIPIF--block means "treat output literally"). Windows runners now skip cleanly instead of producing a confusing FAIL.- 30 SVG-validating phpts declare
simplexmlin--EXTENSIONS--. The tests parse SVG output withsimplexml_load_string()to verify well-formedness and element counts. SimpleXML is normally compiled-in but on ZTS PHP builds where libxml2 / simplexml were dropped (custom--disable-allsetups, ASAN-PHP builds) the tests fatal at thesimplexml_load_stringcall. Adding it to--EXTENSIONS--makes run-tests.php skip with a clear "Required extension missing: simplexml" reason.
1.0.0
This release rebuilds the rendering pipeline around an SVG-canonical architecture. SVG is now the source of truth; every raster output (PNG / JPG / WebP) is produced by flattening text to glyph paths, rasterizing through plutovg, and encoding with libpng / libjpeg-turbo / libwebp. libgd is no longer required at runtime.
Breaking
draw(\GdImage $canvas)removed from every chart class. v1.0 owns its pixel buffer end-to-end. Callers that previously composited multiple charts onto a shared canvas should now stitch viadrawSvgFragment()into one SVG document, or callrenderPng()and composite the decoded bytes in userland.renderGif()andrenderAvif()removed fromChartandSymbol. Both raise\Errorif called. UserenderPng(),renderJpeg(),renderWebp(), orrenderSvg()instead. TherenderToFile('out.gif' | 'out.avif')paths reject with the same error.ext/gdis no longer a runtime requirement. Loading fastchart no longer triggers\GdImageclass lookup at MINIT. ext/gd can be absent and fastchart still functions — but if you still callimagecreatefromstring()to consumerenderPng()output, you obviously still want it loaded.- Default JPEG quality is now 88 (was 90). Matches the evaluation-validated sweet spot for the new plutovg + libjpeg-turbo encoder. Tunable via
setJpegQuality()or per-callrenderJpeg(int). - Pixel output is no longer byte-identical to 0.x. plutovg's rasterizer produces smoother anti-aliasing than libgd; byte-compare baselines against 0.x output will fail. Visual quality is improved, especially on diagonal lines and glyph edges.
Added
- Seven new chart families lifting the family count from 19 to 26:
BulletChart— Stephen Few bullet: performance bar against qualitative bands with a target tick.ParetoChart— descending bars + cumulative-percentage line overlay (the 80/20 visualization).CalendarHeatmap— GitHub-style day-grid keyed byYYYY-MM-DDwith a low/high color ramp.SunburstChart— radial hierarchical donut; recursivechildrenarrays with optionalvalueper node (interior nodes auto-sum).SankeyChart— bipartite / multi-layer flow with bezier ribbons;setNodes()+setLinks()withfrom/toindices.MarimekkoChart— variable-width stacked columns where column width is proportional to category total.VectorChart— arrow-on-grid vector field with magnitude scaling and optional ramp coloring.
Funnel::STYLE_PYRAMID: opt into a triangle-with-bands layout instead of the default descending-trapezoid look. Value still drives shape — band heights are value-proportional, widths follow the triangle's natural taper.Chart::svgToPng() / svgToJpeg() / svgToWebp(): three static methods that hand caller-supplied SVG bytes to the same plutovg + encoder pipeline used byrenderPng()/renderJpeg()/renderWebp(). Lets callers stitch chart fragments (viadrawSvgFragment()) into one SVG document and round-trip the whole thing back to raster without ImageMagick /rsvg-convert/ pure-PHP rasterizers.svgToJpeg($svg, int $q = 88, int $bgRgb = 0xFFFFFF)composites transparent regions under$bgRgbbefore JPEG encode (JPEG has no alpha);svgToWebp($svg, int $q = 90, int $mode = WEBP_DRAWING)honours the same mode as the instance-side encoder. Hard caps: 16 MB input cap; output dims capped at 4096 px / 16 Mpx; embeddeddata:image/URIs and<use>elements rejected (plutosvg's data-URI loader bypasses the output-dim cap, and its<use>renderer's cycle detector doesn't count fan-out, so a nested<g><use/>×10tree can hit billion-laughs expansion). Text in caller-supplied SVG renders blank — plutovg has no text renderer; flatten<text>to<path>upstream.Chart::setWebpMode(int)with class constantsWEBP_DRAWING(default; encoder preset tuned for vector-like content),WEBP_PHOTO(preset for photo input),WEBP_LOSSLESS(lossless encode), andWEBP_FAST(lower-effort encode for batch jobs). Same setter onSymbol. DefaultWEBP_DRAWINGmatches what chart content actually is and produces visibly tighter file sizes than the libwebp simple-API default this release also drops.- plutovg + plutosvg rasterizer, vendored under
vendor/plutovg/andvendor/plutosvg/. Builds withPLUTOVG_BUILD_STATIC+PLUTOSVG_BUILD_STATIC+-fvisibility=hiddenso the library symbols stay internal to fastchart.so. The rasterizer has no text support of its own; fastchart flattens text at SVG-build time. Chart::setSvgTextMode(int)with class constantsSVG_TEXT_PATHS(default; every<text>becomes a<g><path d="…"/></g>via FreeType outline decomposition — self-contained SVG, renders in any rasterizer) andSVG_TEXT_NATIVE(raw<text>elements, smaller files, requires consumer text support). Symbol gains the same setter.Chart::setJpegQuality(int)(1..100, default 88). AffectsrenderJpeg()andrenderToFile('*.jpg'). Symbol mirrors.- Raster encoders wired directly: PNG via libpng with pHYs DPI metadata; JPEG via libjpeg-turbo with
optimize_coding=TRUE, 4:2:0 subsampling, and density_unit metadata; WebP via libwebp'sWebPEncodeRGBA. No libgd encoder path remains on the raster output path. - SVG output.
Chart::renderSvg()returns the full document;Chart::drawSvgFragment()returns a<g class="fastchart">…</g>group with no outer envelope for stitching multiple charts into one caller-managed SVG.renderToFile()routes.svgthrough the vector path. Same surface on Symbol. - Internal render-target abstraction (
fastchart_target_t) with two backends: GD-wrapping for the unchanged raster path and SVG-emitting viasmart_str. The axis, text, and palette helpers operate on the abstraction; chart families thread the target down to the primitive layer. SVG primitives use native<text>(no path-embedded glyphs) with the font family resolved via FreeType. SVG output is DPI-invariant — vector strokes scale infinitely, sosetDpi()no longer inflates the SVG viewport while the raster path retains DPI scaling.
Fixed
- Layout reservation for 45° rotated X-axis labels accounted for only the right-end anchor's projection (
width * 0.707), not the up-left extent of the rest of the rotated text. Long labels like "Jan 2025" ran off the bottom of the canvas in raster output too. Reservation now uses the fullwidth * 1.414projection. - Layout reservation for the rightmost X-axis tick label measured a fixed
"999999"numeric probe, even when the caller setsetCategoryLabels()with longer strings. The X-label measurement path now walks the supplied category labels and uses the widest, mirroring the existing Y-axis logic. - Right margin without a secondary Y axis was bare
MARGIN_RIGHT_PADnext to a full Y-axis label reservation on the left, which read as visibly off-center. Right margin now anchors atleft / 2when both Y and X axes are present. StockChart: first and last candles straddled the Y-axis line and right edge because the time domain mappedt_minto exactlyplot.x0. The time domain is now padded by half a bar-step on each end, matching the half-cell offset categorical X axes already use.BubbleChart: range computation didn't reserve headroom for the bubble radius, so the largest bubble could clip pastplot.y0/plot.x1on data sets where the data extremum sat near a niced tick boundary.xmin/xmax/ymin/ymaxnow pad by 10% of the data span before nice-tick rounding.
Changed
- Build dependencies. Drop
libgd-dev. Addlibpng-dev,libjpeg-turbo-dev(orlibjpeg-dev),libwebp-dev, andlibfreetype-dev. config.m4 probes all four via pkg-config.
Followups landed (post-Phase 6)
- libgd link removed. Dead
gdImage*branches stripped from every chart family body;fastchart_text.cswapped libgd'sgdImageStringFTEx(NULL, …)measurement path for directFT_Set_Char_Size+FT_Load_Glyphadvance summation;config.m4drops the libgd probe entirely.ldd modules/fastchart.soshows only libfreetype, libpng, libjpeg, libwebp. - Effects on the SVG path.
fastchart_effects.cgradient_filled_*now emits<linearGradient>defs with stops at the chart'sgradient_from/_to;shadow_filled_*emits an offset-duplicate shape with the shadow color before the main draw. (Hard-edged shadow rather than Gaussian-blurred — plutosvg doesn't parse<filter>.) Wired into Bar and Pie families. - Background image / icon SVG emission.
fastchart_target_image()loads the source file, base64-encodes the bytes, and emits<image href="data:image/png;base64,…" preserveAspectRatio="none">. Source-image byte and dimension caps (8 MiB / 4096 px / 16 Mpx) enforced at render time; open_basedir re-checked. PNG and JPEG source files only — WebP/GIF/AVIF source files are silently skipped because plutosvg's data-URI loader handles only those two. - Pixel-tolerance test sweep. 7 tests that scanned for exact RGB matches now use
fc_color_near()which accepts any AA-blended version of the target color against white (alpha 0.3..1.0, ±4 RGB per channel). Plutovg's 1px stroke centerline lands at ~50% coverage; libgd produced exact-color centerlines. - Test EXTENSIONS. 33 phpt files that round-trip raster output via
imagecreatefromstring()+imagecolorat()for pixel inspection now declaregdin their--EXTENSIONS--block, so they SKIP cleanly when ext/gd isn't loaded rather than fatal.
Followups landed (security, leak-clean, optional codecs)
- Security: open_basedir TOCTOU eliminated in source-image loading.
setBackgroundImage/addIconAtpreviously sniffed dimensions via directfopen(), then stat()-ed, then opened the path through the PHP stream layer withSTREAM_DISABLE_OPEN_BASEDIR. A basedir-inside symlink whose target was swapped to outside basedir would resolve at the bypass-flagged final open. Collapsed to one `php_stream_ope...
0.2.0
Added
- Symbol family. Two-class symbology hierarchy parallel to
Chart:FastChart\Code128(1D barcode, ISO/IEC 15417) andFastChart\QrCode(2D matrix code, ISO/IEC 18004). Render-only API (renderPng(),renderJpeg(),renderWebp(),renderGif(),renderAvif(),renderToFile()) — Symbol classes do not accept a caller-supplied\GdImage.Code128auto-switches between subsets A/B/C with an odd-tail-to-C optimisation; mod-103 checksum is appended automatically;setShowText(true)renders the human-readable payload below the bars.QrCodeships with all four ECC levels (ECC_L/M/Q/H) and versions 1..40 via the vendored nayuki QR encoder undervendor/qrcodegen/. Both classes honoursetSize(),setQuietZone(),setForeground(),setBackground(),setTransparentBackground(), andsetDpi()(with PNG/JPEG metadata written viagdImageSetResolution).
Fixed
StockChart::setOhlcv()now clears overlay (Bollinger / SAR) and indicator-pane buffers when the new candle count is shorter than the previous one. Previously the overlay'snfield kept the old length and the renderer walked off the end of the new candle array — a use-after-realloc OOB read on the candle pointer.- Setters that accept paths (
setFont,setBackgroundImage,addIconAt) now throw an explicitErrorbeforeRETURN_THROWSwhenphp_check_open_basedirblocks the path.php_check_open_basedironly emitsE_WARNINGand does not setEG(exception), soRETURN_THROWSasserted under a debug PHP build. QrCode::setQuietZone()rejects quiet zones above 256 modules with aValueErrorinstead of silently clamping.
Changed
composer.jsonlicense expression is now(BSD-3-Clause AND MIT). fastchart's own code is unchanged BSD-3-Clause; the composite expression declares the MIT-licensed nayuki QR encoder vendored undervendor/qrcodegen/.
0.1.1
Fixed
- Module load order: declare
ZEND_MOD_REQUIRED("gd")on the module entry so the engine reorders MINIT regardless ofphp.ini/ conf.d /-d extension=order. Previously, configurations that loadedfastchartbeforegd(notablydocker-php-ext-enable's alphabeticalconf.d/*.ini) failed startup with "GdImage class not found."PHP_ADD_EXTENSION_DEPinconfig.m4is build-system-only and does not affect runtime ordering.
Added
scripts/pie-smoke.sh: PIE install + functional smoke test that runs againstphp:8.x-cliwithlibgd-devandext/gdprovisioned in the container. Wired into.release-configassmoke_test.