news 2026/10/2 1:29:21

Java中使用iText生成图表PDF:矢量绘图实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java中使用iText生成图表PDF:矢量绘图实战指南

做Java服务端的,多多少少都碰过“导PDF”这种需求。最近公司报表模块升级,客户提了一个很具体的要求:导出的PDF里必须有图表,柱状图、折线图、饼图都要有,而且打印出来要清晰,不能是截图那种模糊糊的效果。我第一反应就是用iText来实现,毕竟Java生态里生成PDF绕不开它。真正做起来才发现,iText本身并不带图表组件,所有图形都得靠自己一笔一笔画出来。这篇文章就把我用iText生成图表类型PDF的完整思路、核心代码和踩坑记录梳理一遍,希望能帮到正在被同类需求折磨的人。

1. 方案选型:iText画图表,两条路线怎么选

1.1 路线一:JFreeChart等库生成图片再嵌入PDF

最简单的实现方式,是用JFreeChart这类Java绘图库先把图表渲染成一张图片(PNG或JPEG),再通过iText的Image对象把图片塞进PDF页面里。这个方案我最早上手时也试过,确实省事——JFreeChart自带柱状图、折线图、饼图、堆积图甚至3D效果,封装得很完整,几行代码就能画出一张像模像样的图表出来。

优点很明显。开发效率高,不用自己跟坐标和像素打交道;图表的视觉样式丰富,图例、网格线、渐变、阴影这些效果开箱即用;对复杂图表,比如多系列柱状图、百分比堆积图,用代码画矢量图的成本会非常高,这时候JFreeChart的优势就体现出来了。

但这个方案有个绕不开的硬伤——生成的是位图。图片放进PDF之后,放大打印会产生锯齿和模糊感,尤其是客户要求“打印出来必须清晰”的场景,一张分辨率不够的图很容易被嫌弃。其次,图片本身的体积也不小,一张干净的柱状图PNG动辄几百KB,如果一份PDF里塞了十几张图表,整个文件奔着几十MB去,传到客户那边打开都要卡半天。所以这个方案更适合图表数量少、对文件体积和清晰度要求不高的内部系统。

1.2 路线二:用iText的PdfCanvas直接画矢量图形

另一条路,是直接用iText提供的PdfCanvas在PDF页面上绘制线条、矩形、弧线等基本图形,把图表以矢量形式保存下来。所谓矢量图形,就是所有内容都靠数学路径描述,无论放大多少倍,边缘都是平滑的,打印效果和屏幕显示效果一致,不会出现任何马赛克和锯齿。

我这次最终选的就是这条路线。虽然开发时要手算坐标、手动管理绘图状态,代码量明显比JFreeChart方案大,但换来的是可控性和文件质量。PDF里所有的图表元素都是原生的矢量路径,文字、线条、填充色可以自由调整;文件体积也小,画了二十多张图表的PDF,总大小才两三MB,传给客户非常顺畅。更重要的是,矢量绘制完全由代码控制,后续想调整图表风格、加标注、换颜色,都能精确到某个像素级别的元素,不用重新生成图片再回去替换。

1.3 我的选择与理由

当时我接到需求后,专门列了个对比表来做决策:

对比维度JFreeChart图片方案PdfCanvas矢量绘制方案
开发效率高,封装完整低,需手写绘图逻辑
清晰度受图片分辨率限制矢量高清,任意放大
文件体积大,多图时膨胀明显小,图形路径压缩率高
样式定制受库能力约束完全可控,自由度高
复杂图表支持强,堆积图热力图等内置弱,需大量手写路径

考虑到客户强调“打印清晰”和“文件别太大”,我倾向于方案二。基础图表用PdfCanvas绘制虽然代码多点,但画过一次之后,柱状图、折线图、饼图就是几套模板函数,以后复用起来反而顺手。至于那些特别复杂的图表,至少目前报表模块里还没有出现,先用矢量方案把基础图表做扎实再说。

选择方案二还有一层考虑——团队后续打算做PDF模板化,图表要动态换数据、换样式,如果依赖JFreeChart,每次样式调整都要重新跑一遍图片生成流程;而矢量绘制的图形可以跟业务逻辑无缝衔接,数据源头一变,图表自动跟着变,不用管图片缓存那一套。

2. 动手前必懂:PDF坐标系和iText绘图API

2.1 PDF坐标系:原点在左下角,不是左上角

一开始我在这块吃了大亏。做Web开发久了,脑子里默认坐标系是屏幕坐标系——原点在左上角,x轴向右,y轴向下。但PDF的页面坐标系完全相反,原点在页面的左下角,x轴向右,y轴向上,单位是“点(point)”,1英寸等于72点。一张A4纸的尺寸是595点宽、842点高,所以页面左上角的坐标其实是(0, 842),右下角是(595, 0)。

这意味着,画柱状图时从下往上画是正常的,柱子底部贴近x轴,顶部按数据高度向y轴方向延伸。但如果你习惯性地把数据值直接当y轴坐标,会发现图表整体颠倒了。另外,在页面上定位元素时,也要从底部开始算距离,不像Web页面那样从顶部往下排。

这个坐标系差异还影响文字的位置。比如我要在柱状图顶部标注数值,如果用屏幕坐标系思维,很容易把文字画到图表下方,调整半天才发现是y轴理解错了。

2.2 PdfCanvas常用绘图原语和状态控制

iText 7中,PdfCanvas是绘图的核心类,它封装了PDF内容流层面的操作。我常用的方法有这些:

  • moveTo(x, y)、lineTo(x, y)、stroke():画直线或折线路径
  • rectangle(x, y, w, h):画矩形路径,配合fill()或stroke()填充或描边
  • circle(x, y, radius):画圆形路径
  • arc(x1, y1, x2, y2, startAngle, extent):画椭圆弧路径
  • setStrokeColor()、setFillColor()、setLineWidth():设置描边颜色、填充颜色和线宽
  • saveState()、restoreState():保存和恢复绘图状态

PdfCanvas本身不负责文字绘制,文字需要配合Canvas类来操作。简单区别就是,PdfCanvas管图形,Canvas管图文复合布局,如果要在页面上写文本、放图片、画表格,用Canvas;如果只要画线条和色块,直接用PdfCanvas就行。这里的Canvas位于com.itextpdf.layout包下,和PdfCanvas是两个不同的类。

2.3 柱状图的坐标换算公式

画任何一个统计图表之前,都得先把业务数据换算成页面坐标。柱状图的换算相对直观,核心就是找一个“基准点”作为坐标系原点,然后按比例把数据值映射到y轴高度。

假设页面上的图表区域左边距为baseX=80,底部边距为baseY=60,图表区域高度为axisHeight=220,数据最大值为maxValue。第i根柱子的高度就是:

barHeight = (value[i] / maxValue) * axisHeight

柱子的底部y坐标就是baseY,顶部y坐标就是baseY + barHeight。x坐标则需要根据柱子数量和间距来算。比如有n根柱子,柱子宽度barWidth,图表总宽度axisWidth,那么每根柱子之间的空隙gap就是:

gap = (axisWidth - barWidth * n) / (n + 1)

第一根柱子的左边缘x坐标是baseX + gap,后面每根柱子递增 (barWidth + gap)。这套公式写好后,换一组数据只要改数组和最大值,柱状图自动重新布局。我在项目里把这套逻辑封装成了一个方法,传入数据数组和颜色列表,返回一个绘制好的柱状图区域,后续所有报表模块都能直接复用。

3. 核心实现:柱状图、折线图、饼图逐个击破

3.1 柱状图:从一组业务数据画到页面上

先看柱状图的完整实现。假设我要展示某网点前五个月的销售额,数据是:

1月 120万 2月 90万 3月 145万 4月 78万 5月 132万

最大销售额是145万,为了给柱子留出头部标注空间,我把y轴最大值设为150。绘制流程分三步:先画坐标轴,再画柱子,最后在柱子顶部写数值。

import com.itextpdf.kernel.colors.DeviceRgb; import com.itextpdf.kernel.font.PdfFont; import com.itextpdf.kernel.font.PdfFontFactory; import com.itextpdf.kernel.font.PdfEncodings; import com.itextpdf.kernel.geom.PageSize; import com.itextpdf.kernel.geom.Rectangle; import com.itextpdf.kernel.pdf.PdfDocument; import com.itextpdf.kernel.pdf.PdfWriter; import com.itextpdf.kernel.pdf.canvas.PdfCanvas; import com.itextpdf.layout.Canvas; import com.itextpdf.layout.properties.TextAlignment; public class BarChartPdf { public static void main(String[] args) throws Exception { String dest = "bar_chart.pdf"; PdfWriter writer = new PdfWriter(dest); PdfDocument pdfDoc = new PdfDocument(writer); PdfCanvas canvas = new PdfCanvas(pdfDoc.addNewPage(PageSize.A4)); PdfFont font = PdfFontFactory.createFont( "C:/Windows/Fonts/simsun.ttc", PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED); String[] labels = {"1月", "2月", "3月", "4月", "5月"}; double[] values = {120, 90, 145, 78, 132}; double maxValue = 150; float baseX = 80; float baseY = 60; float axisHeight = 220; float axisWidth = 300; float barWidth = 30; float gap = (axisWidth - barWidth * values.length) / (values.length + 1); float startX = baseX + gap; // 绘制坐标轴 canvas.setStrokeColor(new DeviceRgb(80, 80, 80)); canvas.setLineWidth(1.2f); canvas.moveTo(baseX, baseY); canvas.lineTo(baseX, baseY + axisHeight); canvas.stroke(); canvas.moveTo(baseX, baseY); canvas.lineTo(baseX + axisWidth, baseY); canvas.stroke(); // 绘制柱子 for (int i = 0; i < values.length; i++) { float barHeight = (float) (values[i] / maxValue * axisHeight); float x = startX + i * (barWidth + gap); canvas.setFillColor(new DeviceRgb(52, 119, 235)); canvas.rectangle(x, baseY, barWidth, barHeight); canvas.fill(); // 用Canvas在柱子顶部写数据值 Canvas textCanvas = new Canvas(canvas, pdfDoc, new Rectangle(0, 0, PageSize.A4.getWidth(), PageSize.A4.getHeight())); textCanvas.setFont(font).setFontSize(9); textCanvas.showTextAligned( String.valueOf(values[i]), x + barWidth / 2, baseY + barHeight + 8, TextAlignment.CENTER); textCanvas.close(); // 在x轴下方写分类名称 textCanvas = new Canvas(canvas, pdfDoc, new Rectangle(0, 0, PageSize.A4.getWidth(), PageSize.A4.getHeight())); textCanvas.setFont(font).setFontSize(10); textCanvas.showTextAligned( labels[i], x + barWidth / 2, baseY - 16, TextAlignment.CENTER); textCanvas.close(); } pdfDoc.close(); } }

这里有两个细节值得说。第一,每次写文字时都new一个Canvas对象,用完必须close,否则内容流的状态会乱掉,我最初没close导致后续图形渲染出问题,排查了半天。第二,文字对齐用的是TextAlignment.CENTER,这样数值能正好居中在柱子顶部,视觉上比较舒服。

实际项目中,颜色最好准备一套预设色板,比如蓝色、橙色、绿色、紫色交替使用,避免所有柱子一个颜色显得呆板。另外,如果柱子数量很多,比如二十几个分类,柱子宽度要动态缩小,我一般是把barWidth设为 (axisWidth / n) * 0.6,这样间距和柱子宽度的比例自动协调。

3.2 折线图:趋势线、数据点和网格线

折线图跟柱状图的坐标换算逻辑类似,区别在于要把数据点映射成(x, y)坐标,然后用连线把它们串起来。一般业务场景里,x轴是时间或分类,y轴是数值。比如我画一组某产品半年内的月度活跃用户趋势:

数据点是 (1月, 12500),(2月, 15200),(3月, 18700),(4月, 17900),(5月, 20100),(6月, 25400)。

float chartWidth = 320; float chartHeight = 200; float originX = 80; float originY = 80; double[] yData = {12500, 15200, 18700, 17900, 20100, 25400}; double maxY = 26000; int n = yData.length; float scaleX = chartWidth / (n - 1); float scaleY = chartHeight / (float) maxY; // 绘制Y轴和X轴 canvas.setStrokeColor(new DeviceRgb(100, 100, 100)); canvas.setLineWidth(1f); canvas.moveTo(originX, originY); canvas.lineTo(originX, originY + chartHeight); canvas.stroke(); canvas.moveTo(originX, originY); canvas.lineTo(originX + chartWidth, originY); canvas.stroke(); // 绘制水平网格线 canvas.setStrokeColor(new DeviceRgb(220, 220, 220)); canvas.setLineWidth(0.5f); for (int i = 0; i <= 4; i++) { float y = originY + (chartHeight / 4) * i; canvas.moveTo(originX, y); canvas.lineTo(originX + chartWidth, y); canvas.stroke(); } // 折线 canvas.setStrokeColor(new DeviceRgb(230, 126, 34)); canvas.setLineWidth(2.5f); for (int i = 0; i < n - 1; i++) { float x1 = originX + i * scaleX; float y1 = originY + (float) (yData[i] * scaleY); float x2 = originX + (i + 1) * scaleX; float y2 = originY + (float) (yData[i + 1] * scaleY); canvas.moveTo(x1, y1); canvas.lineTo(x2, y2); canvas.stroke(); } // 数据点小圆圈 canvas.setFillColor(new DeviceRgb(231, 76, 60)); for (int i = 0; i < n; i++) { float x = originX + i * scaleX; float y = originY + (float) (yData[i] * scaleY); canvas.circle(x, y, 3f); canvas.fill(); }

网格线我特意用了浅灰色,线宽也调成0.5,这样既不影响数据读取,又不会喧宾夺主。折线的颜色选的橙色,数据点用的红色,一暖一冷对比明显,客户反馈看着舒服。如果数据量特别大,比如连续三十天的曲线,数据点可以只画首尾和几个关键点,否则满屏小圆圈反而干扰视觉。

有一点需要提醒:scaleX = chartWidth / (n - 1) 这里除以n-1而不是n,因为n个数据点之间只有n-1段连线,x坐标要从originX开始,最后一个点的x正好落在originX + chartWidth上,不会超出图表区域。

3.3 饼图:扇形的绘制与颜色分配

饼图的绘制思路是所有扇形围绕圆心旋转,每个扇区占的角度比例等于它的数据占比。比如四个渠道的流量占比分别是40%、25%、15%、20%,那它们对应角度就是144度、90度、54度、72度。绘制时从0度开始,累计旋转角度,用moveTo先移到圆心,再用arc画弧线,最后lineTo回到圆心形成一个封闭路径,填充颜色。

float centerX = 250; float centerY = 300; float radius = 90; double[] ratios = {40, 25, 15, 20}; DeviceRgb[] colors = { new DeviceRgb(52, 119, 235), new DeviceRgb(230, 126, 34), new DeviceRgb(46, 204, 113), new DeviceRgb(155, 89, 182) }; float startAngle = 0; for (int i = 0; i < ratios.length; i++) { float sweep = (float) (ratios[i] / 100.0 * 360); canvas.setFillColor(colors[i]); canvas.moveTo(centerX, centerY); canvas.arc(centerX - radius, centerY - radius, centerX + radius, centerY + radius, startAngle, sweep); canvas.lineTo(centerX, centerY); canvas.fill(); startAngle += sweep; }

arc方法的前四个参数是椭圆外接矩形的左下角坐标和右上角坐标,因为我画的是正圆,所以外接矩形是个正方形,圆心正好在中心。startAngle是起始角度,0度对应三点钟方向,逆时针旋转是正方向,sweep是跨越的角度。

这里有个容易踩的坑:如果某个扇区角度特别大,比如超过90度,一次性画大弧时arc方法在某些iText版本里生成的贝塞尔曲线路径可能会有一点偏差,导致扇形边缘不够圆润。稳妥的做法是把sweep拆成多段小弧,每段不超过90度,这样路径更平滑。不过这个偏差只是在极端情况下肉眼可见,常规数据基本不影响。

颜色分配上,饼图配色很重要,相邻扇区色差要尽量大。我用的这组蓝、橙、绿、紫组合,颜色对比明显,而且在黑白打印时也有可辨识度。如果实际项目中配色不够,可以从Material Design色板里挑,但要注意深色和浅色交替,避免两块深蓝色挨在一起。

4. 排版与布局:图表和文字、表格怎么搭

4.1 用Canvas把文字和图表组合在一起

只画图表还不够,一份像样的PDF报表,图表旁边得有标题、说明、指标汇总,甚至还要配一个数据明细表格。这时就需要把图表和文字组合到同一个页面上。PdfCanvas绘制的图形是直接写到内容流里的,和后面用Canvas添加的文字、表格相互独立,所以可以先用PdfCanvas画完图表,再用Document或Canvas添加文字元素。

我实际的做法是先画图表,再把标题和描述文字放到图表上方或下方。比如画一个柱状图,在页面顶部用Canvas写一行大标题“2025年各月销售额统计”,下面写一行小字汇总“年度累计销售额565万,同比增长12.3%”,然后再接柱状图。这样整页内容主次分明,客户一眼就能看到关键结论。

Canvas textCanvas = new Canvas(pdfDoc, new Rectangle(40, 760, PageSize.A4.getWidth() - 80, 60)); textCanvas.setFont(font) .setFontSize(18) .setBold() .showTextAligned("2025年各月销售额统计", 40, 780, TextAlignment.LEFT); textCanvas.close();

这里textCanvas挂到了pdfDoc上,而不是某个PdfCanvas上,因为此时不需要叠加到已有图形内容流里,直接在当前页面新增内容。如果图表和文字在同一个内容流里叠加,也可以用Rectangle(0, 0, pageWidth, pageHeight) 创建一个覆盖全页的画布,再showTextAligned指定任意坐标。

4.2 多图表排版的间距和分页策略

一份报表往往不止一张图表。我做的月度经营分析报告里,就同时包含柱状图、折线图和饼图,依次排列在一页或跨两页。这时布局就显得特别关键。

如果图表都在一页,我会严格控制每个图表区域的占位。比如页面可用高度是842点,减去上下边距各60点,可绘制高度约720点。柱状图放在页面中上部,高度约280点;折线图放在中部偏下,高度约220点;饼图放在右侧或者下方,直径控制在150点以内。所有图表之间留出至少30点的空白,避免图形和文字挨得太近。我习惯先用矩形预览框在PDF上调试位置,确认无误后再填充具体图表:

canvas.setStrokeColor(new DeviceRgb(200, 200, 200)); canvas.rectangle(40, 560, 300, 220); canvas.stroke();

如果图表数量多到一页放不下,就得考虑分页。这里我的策略是:把一组相关的图表放在同一页,比如柱状图和折线图放在第一页,表格和饼图放在第二页。不要贪多,一页纸塞五六张图表会让版面非常拥挤,可读性大打折扣。分页时,每页都要重新new一个PdfCanvas(通过pdfDoc.addNewPage()获得),在新页面上从零开始布局。

另外,图表的标题“字体大小”也要统一。我一般图表大标题用18点加粗,图表内部文字和轴标签用9到10点,正文描述用12点。这样整个PDF的视觉层级清晰,客户打印出来阅读体验也好。

5. 避坑指南:我在实际项目里踩过的几个坑

5.1 中文乱码:不设置字体就没得玩

这是iText生成PDF最经典的问题,没有之一。iText默认字体不支持中文,直接用默认字体写中文,生成出来的PDF里全是方块“□□□□”。解决办法是显式加载一个中文字体文件,并用支持中文的编码。

PdfFont font = PdfFontFactory.createFont( "C:/Windows/Fonts/simsun.ttc", PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED);

注意simsun.ttc是TrueType Collection文件,里面包含多个字体,iText 7里可以用PdfFontFactory.createTtcFont(path, 0, PdfEncodings.IDENTITY_H, true) 取下标0的那个字体,或者直接用createFont配合.ttc路径也兼容。我在Windows开发环境没问题,但部署到Linux服务器上就踩坑了——服务器没有Windows的宋体字体,跑起来直接报FontProviderException。后来我把字体文件放到了项目resources目录,用绝对路径去加载,问题才解决。如果你的环境不允许携带外部字体文件,可以考虑用开源的思源黑体或文泉驿字体,嵌入PDF后文件体积会大一些,但显示效果稳定。

5.2 坐标轴方向“上下颠倒”

前面提到PDF客户端的坐标系是左下角原点,我一同事在这个问题上栽了跟头。他想画一个从右往左的横向柱状图,结果习惯性地把x轴当成了屏幕方向,导致柱子从右往左延伸反而跑到了页面外面。排查了半天才发现,PDF的x轴虽然也是向右,但页面宽度是固定595点,如果从右侧起始画矩形,坐标就要从600开始往左延伸,而不是从0开始向右。

这个问题最好在编码前画一个坐标草稿图,把所有关键点标出来:页面左下角(0,0),右下角(595,0),左上角(0,842),右上角(595,842)。图表原点放在(80,60)后,所有图形的坐标都要基于这个基准来推算,不要凭空想像。

5.3 图片式图表导致PDF体积过大

有段时间我图省事,柱状图用了JFreeChart生成图片然后嵌入PDF,效果其实也还行。但后来图表数量多了,一份PDF塞了15张图,文件直接飙到80MB,客户反馈打开卡到怀疑人生。用PdfCanvas改成矢量绘制后,同一份PDF压缩到了3MB,差别非常明显。

如果你确实需要保留图片方案,至少要做两件事:一是控制图片输出尺寸,JFreeChart设定输出宽度为页面宽度的2倍左右就行,不要四倍五倍地出图;二是图片格式尽量用PNG,别用BMP这种无压缩格式,必要时可以转成JPEG并适当降低质量。

5.4 iText 5和7的API差异让人头疼

网上搜iText画图表的教程,大量都是iText 5时代的写法,用的是com.lowagie.text.pdf.PdfContentByte这个类。iText 7彻底重构了API,PdfContentByte变成了PdfCanvas,方法名和参数也有变化。我最初照着旧教程写arc方法,发现参数根本对不上,找了一下午才发现是版本问题。

如果你用的是iText 7,画弧线时记得用PdfCanvas.arc(float x1, float y1, float x2, float y2, float startAngle, float extent),这个方法在iText 5里是没有的,旧代码可能用Arc这个工具类,千万别直接照搬。

5.5 图形过多导致PDF打开卡顿的优化思路

有些报表图表数量确实多,每页五六张图,总共十几页,虽然矢量图形文件体积不大,但PDF渲染器打开时还是要逐条执行路径绘制指令。如果你的PDF里图形密集,打开速度变慢,可以从几个方面优化:尽量减少无意义的路径指令,比如网格线数量控制在四五条就好,不要铺满整个背景;柱子或者折线的数据点数量如果上千个,考虑降采样,比如每五个点取一个代表点;填充颜色避免使用带透明度的颜色,透明混合会让PDF渲染器做额外计算,影响性能。

我在月度报表里就是这么优化的,原来打开要两三秒的PDF,优化后加载速度提升了一倍左右,客户反馈也正面了许多。


以我这两年的实操经验来说,用iText画基础图表并不难,难的是把这些基础图形组合成一份可用的业务报表。坐标系换算搞明白了,字体问题解决了,剩下的就只是按数据堆代码。最后分享一个小技巧:如果你只是偶尔做一两张图,用JFreeChart生成图片再嵌进PDF完全没有问题,效率最高;但如果图表是长期动态生成的,而且对清晰度和文件体积有要求,那还是踏踏实实用PdfCanvas做矢量绘制。两种方案没有绝对的好坏,按需求选就行。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 1:29:02

STM32CubeMX安装配置与工程实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 1:28:54

JupyterHub多用户部署实战:Linux服务器从零配置到反向代理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 1:28:54

LIN总线主从节点通信失败原因分析与实战排查指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 1:28:04

Canvas 是前端可视化的底层基建:原理、性能与生产实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 1:26:48

单细胞转录组人工注释全流程:从marker筛选到功能验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华