news 2026/10/24 3:16:44

PDFBox实战:Java字节流处理PDF添加空白页与标注

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PDFBox实战:Java字节流处理PDF添加空白页与标注

做了这么多年Java开发,PDF处理的需求几乎绕不开。之前接了个活儿,对方不给我PDF文件路径,直接传了个字节数组过来,让我往里加空白页,还要顺带在页面上做点简单的文字标注。一开始我想着这不就是PDDocument.load一下的事嘛,等真上手才发现,字节流和文件路径的处理方式差异不小,坑也不少。这篇就把我用PDFBox操作PDF字节流、添加空白页、做简易编辑的完整过程和踩过的坑整理出来,给同样被字节流折磨过的朋友参考。

1. 项目背景与需求拆解

1.1 为什么是字节流而不是文件路径

平时最常见的PDF操作方式无非就是传个文件路径,new File("xxx.pdf")一捏,PDDocument.load(file)一读,完事。但真实项目里,尤其是对接第三方系统、微服务调用、消息队列消费这类场景,PDF文件往往是以byte[]或者InputStream的形式存在的。

比如我这次接的需求,上游是一个文档生成服务,它把生成好的PDF直接通过HTTP接口以二进制流形式返回给我。我要做两件事:

  1. 在PDF的指定位置插入若干空白页;
  2. 给每一页加上页脚标注,比如“第X页 / 共Y页”。

这个过程如果先把字节流转成临时文件再处理,也不是不行,但有几个明显问题:

  • 需要额外处理临时文件的创建和删除,搞不好还会留下垃圾文件;
  • 在高并发场景下,频繁的磁盘IO会拖慢整体性能;
  • 在云函数、容器化环境里,文件系统可能是只读的或者临时目录空间有限。

所以直接基于字节流操作,才是干净利落的做法。再说了,PDFBox本身就是支持从InputStream加载和保存到OutputStream的,没必要绕弯路。

1.2 核心需求里藏着哪些技术点

拆解一下这个需求,其实涉及几个关键环节:

  • 字节流加载PDF:PDDocument.load(InputStream),注意这个InputStream最好是支持随机访问的,否则PDFBox会采用不同策略;
  • 添加空白页:PDDocument的addPage方法配合PDPage对象,真正细节的地方在页面尺寸的设定;
  • 内容流编辑:要在页面上写字、画线,就得开PDPageContentStream,设置字体、字号、颜色等属性;
  • 字体问题:中文字体渲染是重灾区,PDFBox默认字体不支持中文,必须手动加载系统TTF字体;
  • 保存与回写:处理完要把PDDocument写回ByteArrayOutputStream,再转回byte[]给下一个环节。

这里面的每一项单独拎出来都不算难,但组合在一起,细节和坑就多了。尤其是字体和页面尺寸这两个,我调试的时候确实费了不少功夫。

2. 环境准备与核心API速览

2.1 Maven依赖引入

我用的是PDFBox 2.x版本,这是目前最稳定、资料最多的版本线。别一上来就用最新版,2.0.x的稳定性和生态经过长期验证,绝对够用。Maven坐标如下:

<dependency> <groupId>org.apache.pdfbox</groupId> <artifactId>pdfbox</artifactId> <version>2.0.27</version> </dependency>

如果要做更复杂的编辑,可能还需要fontbox,不过这个依赖会随着pdfbox一起传递引入,不需要额外声明。用3.x版本的朋友注意一下,3.0之后API有Breaking Change,比如PDDocument.load的异常类型可能不同,但我这里以2.0.27为准,稳定优先。

2.2 核心类和使用逻辑

PDFBox里最核心的几个类先过一遍:

  • PDDocument:整个PDF文档的抽象,加载、保存、关闭都靠它;
  • PDPage:表示PDF里的一个页面,由它来承载内容流;
  • PDPageContentStream:内容流对象,往页面上画东西全靠它,文本、线条、形状都通过它写入;
  • PDRectangle:页面尺寸的载体,A4、A3或者自定义尺寸都是由这个类定义的;
  • PDType0Font:加载外部TTF字体文件,用于中文等非内置字符集渲染。

常规的操作链路是:加载文档 → 操作页面 → 用内容流绘制 → 保存 → 关闭文档。这个链路我在字节流场景下走了一遍,发现有几个地方跟文件操作的直觉不一样,下面细说。

3. 字节流加载与保存的正确姿势

3.1 从byte[]到PDDocument

首先是加载,字节流直接塞给PDDocument.load即可:

public PDDocument loadFromBytes(byte[] pdfBytes) throws IOException { return PDDocument.load(new ByteArrayInputStream(pdfBytes)); }

这一步本身没什么难度,但是你要注意一个核心问题:ByteArrayInputStream不支持随机访问。PDFBox内部碰到不支持RandomAccess的输入流时,会把整个流的内容拷贝到内存中的一个RandomAccessBuffer里,然后再解析。这个行为对调用方是透明的,但你心里要有数——如果PDF很大,比如几十MB甚至上百MB,这一步就会吃掉相当可观的内存。

提示:如果你的PDF是从文件读出来的byte[],并且后续要多次读取或频繁操作,可以考虑先存入临时文件再用RandomAccessFile方式加载,能有效降低内存压力。纯字节流场景下,最好评估一下文件大小再做取舍。

另外有一点,PDDocument.load(InputStream)执行完之后,传入的InputStream是可以关闭的,PDFBox内部已经把需要的数据读取完了。但PDDocument用完之后必须调用close(),这个一定要记得,不然文件句柄和内存资源都会泄露。

3.2 从PDDocument回写到byte[]

操作完文档,要把它转回字节流。这里我踩过坑:千万别直接尝试从PDDocument拿什么“原始字节”再改,正确的做法是通过save方法输出到ByteArrayOutputStream:

public byte[] saveToBytes(PDDocument document) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); document.save(baos); return baos.toByteArray(); }

这个方法的背后逻辑是PDFBox会把整个文档结构重新序列化一遍,包括修改过的页面、对象。所以哪怕你只是加了一页,整个文件也可能会被重写,对于大文件来说耗时不可忽视。

还有一点值得注意:document.save(baos)执行完之后,baos还在手里,但如果你紧接着调用document.close(),再尝试读取baos.toByteArray()是没问题的,数据已经拷贝出来了。这里顺序无所谓,重点是别在document.close()之后再去操作baos对应的资源就行。

3.3 一个完整的工具方法骨架

把加载和保存封装起来,写成一个工具类,后面所有场景复用:

public class PdfBytesEditor { public byte[] addBlankPages(byte[] srcBytes, int pageCount) throws IOException { PDDocument document = null; try { document = PDDocument.load(new ByteArrayInputStream(srcBytes)); for (int i = 0; i < pageCount; i++) { PDPage blankPage = new PDPage(PDRectangle.A4); document.addPage(blankPage); } ByteArrayOutputStream baos = new ByteArrayOutputStream(); document.save(baos); return baos.toByteArray(); } finally { if (document != null) { document.close(); } } } }

这个try-finally结构不是装样子,PDDocument如果不close,在服务器端跑久了内存占用会飙得很难看。我后来还专门用try-with-resources重构过,简洁一些。

4. 添加空白页的核心细节与参数选择

4.1 页面尺寸的选择逻辑

PDPage的构造可以直接传一个PDRectangle对象,常见的尺寸如下:

页面类型PDRectangle常量尺寸(磅)
A4PDRectangle.A4595 x 842
A3PDRectangle.A3842 x 1191
LetterPDRectangle.LETTER612 x 792
自定义new PDRectangle(width, height)任意

这里先说明一下PDF里的“磅”:1磅 = 1/72英寸,A4的宽高是210mm×297mm,换算过来正好是595×842磅。如果你是从其他系统拿到的PDF,想跟原页面保持一致的尺寸,千万不要拍脑袋用A4,最稳妥的做法是读取现有页面的尺寸然后套用:

PDRectangle existingSize = document.getPage(0).getMediaBox(); PDPage newPage = new PDPage(existingSize); document.addPage(newPage);

这样做的好处是文档所有页面尺寸统一,打印时不会出现页面大小不一致导致的分页错乱。

4.2 指定位置插入空白页

addPage默认是把新页追加到文档末尾,但很多业务场景是要在指定位置插入。比如合同需要在第二页和第三页之间插入一页补充协议,这时候就要用getPages().insert方法:

public void insertBlankPage(PDDocument document, int index, PDRectangle pageSize) { PDPage blankPage = new PDPage(pageSize); document.getPages().insert(blankPage, index); }

注意insert的索引从0开始,也就是说insert(blankPage, 0)是插到第一页的位置,insert(blankPage, document.getNumberOfPages())等价于addPage。

4.3 初始化页面属性

创建空白页的时候还能顺手做一些初始化,比如背景颜色。之前我在一个案例里需要在新增页面上加灰色背景,用来区分不同合同附件。设置背景色其实是通过内容流填充整个页面矩形来实现的:

PDPage blankPage = new PDPage(PDRectangle.A4); document.addPage(blankPage); PDPageContentStream cs = new PDPageContentStream(document, blankPage); cs.setNonStrokingColor(240, 240, 240); // 浅灰色 cs.addRect(0, 0, blankPage.getMediaBox().getWidth(), blankPage.getMediaBox().getHeight()); cs.fill(); cs.close();

这里有个细节:new PDPageContentStream(document, blankPage)默认会清空页面上已有的内容流吗?不会。构造方法里有个重载可以传“append模式”,如果页面原本有内容,想要在现有内容之上叠加绘制,需要使用PDPageContentStream(document, blankPage, AppendMode.APPEND, true, true)。但空白页没有这个顾虑,默认模式就好。

注意:每次用完PDPageContentStream之后一定要close()。如果你不close,内容流可能不会被正确写入PDF,页面就真的是“空白的”,你以为加了背景但实际上没有。

5. 简易编辑实战:文字、线条和基础图形

5.1 在页面上写文字

把文字写到页面上,核心是三步:创建内容流、设置字体、绘制文本。最简单的例子:

PDPageContentStream contentStream = new PDPageContentStream(document, page); contentStream.beginText(); contentStream.setFont(font, 12); contentStream.setNonStrokingColor(0, 0, 0); // 黑色 contentStream.newLineAtOffset(72, 720); // 距离页面左下角向右72磅,向上720磅 contentStream.showText("这是测试文本"); contentStream.endText(); contentStream.close();

这段代码的逻辑很简单:beginText开始文本对象,newLineAtOffset定位光标,showText写入文本,endText结束文本对象。需要注意坐标系的原点在页面左下角,x轴向右,y轴向上。所以坐标(72, 720)的意思是距离左边1英寸、距离底边10英寸的位置。

5.2 中文字体是个深坑

如果你直接运行上面的代码,用PDFBox内置的StandardFont,比如PDType1Font.HELVETICA,写中文的时候你会发现页面上的文字全是乱码或者直接就空掉了。这是因为标准14种Type1字体根本不含中文字符的映射,PDFBox也无法为它们编码中文字符。

正确做法是加载系统字体TTF文件:

PDType0Font font = PDType0Font.load(document, new File("/System/Library/Fonts/PingFang.ttc"));

在Windows环境下,中文字体路径一般是:

C:\Windows\Fonts\msyh.ttc // 微软雅黑 C:\Windows\Fonts\simsun.ttc // 宋体

在Linux服务器上,可以先确认是否有中文字体,没有的话需要安装,或者把字体文件放到项目资源目录下,通过类路径加载:

PDType0Font font = PDType0Font.load(document, PdfBytesEditor.class.getClassLoader().getResourceAsStream("fonts/msyh.ttc"));

提示:.ttc是多字体集合,PDFBox加载的时候默认取第一个字体。如果第一个字体不合适,有些版本支持通过PDType0Font.load的重载方法指定字体索引,但不是所有版本都支持,保守的做法是用单独的中文字体文件比如simhei.ttf、msyh.ttf(纯ttf格式)更省心。

字体选择上也有一点经验:别用宋体画页面标注,宋体的笔画在某些PDF阅读器里细得看不清;微软雅黑和黑体是页脚标注、水印这类内容的好选择,清晰度高、视觉效果好。

5.3 画直线和矩形框

有时候打字还不够,需要在页面上画表格线、分割线或者高亮框。绘制线条:

contentStream.setStrokingColor(0, 0, 0); contentStream.setLineWidth(1f); contentStream.moveTo(50, 500); contentStream.lineTo(545, 500); contentStream.stroke();

moveTo划线起点,lineTo划线终点,stroke是真正把线画出来的动作,少一步线都出不来。画矩形框:

contentStream.setStrokingColor(0, 0, 255); contentStream.addRect(50, 500, 200, 100); contentStream.stroke();

addRect(x, y, width, height)按照左下角起始坐标、宽高来定位。如果只想要边框,用stroke;如果还要填充底色,用fill或者先fill再stroke。

5.4 给每一页加页脚

这次需求里的“第X页/共Y页”标注,实现起来就靠遍历页面加内容。一个关键细节是:总页数要在遍历之前先取好,不然后面边添加边遍历会出问题:

public byte[] addPageFooters(byte[] srcBytes) throws IOException { try (PDDocument document = PDDocument.load(new ByteArrayInputStream(srcBytes))) { int totalPages = document.getNumberOfPages(); // 加载字体 PDFont font = PDType0Font.load(document, PdfBytesEditor.class.getClassLoader().getResourceAsStream("fonts/msyh.ttf")); for (int i = 0; i < totalPages; i++) { PDPage page = document.getPage(i); try (PDPageContentStream cs = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true, true)) { float pageWidth = page.getMediaBox().getWidth(); cs.beginText(); cs.setFont(font, 9); cs.setNonStrokingColor(128, 128, 128); cs.newLineAtOffset(pageWidth / 2 - 30, 20); cs.showText("第 " + (i + 1) + " 页 / 共 " + totalPages + " 页"); cs.endText(); } } ByteArrayOutputStream baos = new ByteArrayOutputStream(); document.save(baos); return baos.toByteArray(); } }

看到这里的关键点没?AppendMode.APPEND配合后面两个布尔值true, true,意义分别是“是否压缩内容流”“是否重置上下文”。如果你要在原页面上追加内容,这个重载必须用对。写成new PDPageContentStream(document, page)会直接覆盖原有内容,合同文件被这么一搞就废了。

注意:totalPages在遍历前取好非常关键,如果循环里用document.getNumberOfPages()做判断,而循环体里又加了新页面,会死循环或者越界。这是新手最容易犯的错。

6. 进阶:内容流绘制中的坐标与状态管理

6.1 理解PDF的坐标系和变换

之前提到坐标系原点是左下角,但实际项目中经常遇到要旋转文本、或者把元素放到页面上特定位置的情况。简单说明一下坐标系变换规律:

  • 平移:contentStream.transform(Matrix.getTranslateInstance(tx, ty))
  • 旋转:contentStream.transform(Matrix.getRotateInstance(angle, x, y))
  • 缩放:contentStream.transform(Matrix.getScaleInstance(sx, sy))

在页脚场景,你会想把文字放在页面底部居中,用pageWidth / 2 - 估算文本宽度 / 2来定位。PDFBox没有直接获取文本宽度的通用API,但可以算,前提是字体已经加载:

float textWidth = font.getStringWidth("第 1 页 / 共 5 页") / 1000f * fontSize;

注意getStringWidth返回的是千分之一字号的单位,要除以1000再乘以字号才是实际磅数。这个小计算我调了半天才搞明白。

6.2 内容流的资源管理

PDPageContentStream是持有底层资源句柄的,用完必须关闭。但你在操作过程中如果中间抛异常了,用try-with-resources是最稳的写法:

try (PDPageContentStream cs = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true, true)) { // 绘制操作 }

这个写法简洁明了,也避免了手动关闭时的异常遮蔽问题。如果项目用了更老的Java版本,也需要保证finally里有个cs.close()兜底。

6.3 多次写入内容流的注意事项

实操过程中我经常需要分多次往同一页面写内容。比如先写标题,再画分割线,最后加页脚。这里有一个潜在坑:如果每次都用new PDPageContentStream创建内容流,默认模式都会尝试替代原页面内容流。解决方式就是每次都带上AppendMode.APPEND,或者在一个内容流里完成所有绘制。

我是倾向于后者:把一次完整页面需要的内容一次性在一个内容流里画完,既清晰又不容易出错。只有在内容和逻辑实在分不开的时候,才用追加模式。

7. 实操中遇到的几个典型问题与排查记录

7.1 中文字体加载后仍显示乱码

这个情况我排查过很多次,最后定位到原因基本是两个:

一个是用错了字体文件格式。有些.ttc字体文件里的字体名称不是常规的,PDFBox虽然能加载,但内部无法正确映射字符。我踩过内置字体加载失败的坑之后,就换了项目中自带的一个msyh.ttf,问题迎刃而解。

另一个是字体文件本身损坏或路径不对。PDType0Font.load解析的是字节流内容,如果路径指向的资源不是有效字体,直接抛IOException。用getResourceAsStream之后记得先检查返回是否为null。

7.2 页面内容被清空

这个现象通常就是没用追加模式创建PDPageContentStream。我之前一版代码:

new PDPageContentStream(document, page)

结果原页面内容全没了。后来改成追加模式就好了。有一个特别注意的点:追加模式的构造参数里,两个布尔值不能随便写。compress参数表示是否压缩内容流,resetContext表示是否重置图形状态。实测下来两个都传true是标准做法。

7.3 生成的文件体积异常变大

对同一个PDF反复添加页面和内容,文件体积可能会不断膨胀。这是PDF对象累积的结果。页面被多次追加内容流,旧的未引用对象没有垃圾回收。PDFBox提供了document.save时自动清理机制,但不彻底。

如果要压缩体积,可以考虑用PDFMergerUtility合并时优化,或者在保存前手动压缩:

PDDocument addDocument = PDDocument.load(...); // ... 操作 ... addDocument.save(baos); addDocument.close();

当然,最简单也最笨的办法是每次从原始字节重新处理,避免对同一文档反复操作。

7.4 高并发场景下的线程安全问题

PDDocument不是线程安全的,多个线程共用同一个PDDocument实例必然出事。我在做批量处理服务时,每个请求都创建一个独立实例,处理完释放内存。要提升吞吐,只能靠多实例并行+资源可控。

实测来看,用线程池并行处理不同PDF文件的效果还是很好的。但注意字体加载这一块是重量级操作,最好做成单例缓存,不然每个线程都加载一遍字体,CPU和内存开销都上去了。我用一个简单的ConcurrentHashMap缓存了字体对象,按字体路径做key,性能提升明显。

8. 需求落地后的效果和性能优化心得

8.1 从字节进去字节出来

最终实现是这个链路:

byte[] → ByteArrayInputStream → PDDocument → 操作页面/内容流 → ByteArrayOutputStream → byte[]

后端接口拿到新的字节流之后,直接转成HTTP响应流返回给前端下载,无临时文件、无磁盘残留。这个链路在服务端处理文档的场景里非常通用,无论是上传PDF后自动盖章、加水印,还是模板填充后转出,都适用。

8.2 性能方面的实测感受

我用一份200页的PDF做了测试,源文件约15MB。纯加载 + 每页追加一行页脚文字 + 保存回写,整个流程耗时大概在2秒到3秒之间,内存峰值大约200MB。这个表现在可以接受,但如果你要做上千页的批量处理,就得考虑分批或者限制并发数了。

内存这块还有个小优化思路:PDDocument.load(new ByteArrayInputStream(bytes))会在内存里复制一份数据做随机访问缓冲。如果原始字节数组你确实用不到了,可以传RandomAccessBuffer优化。但大部分场景下这个优化意义不大,保持代码清晰才是第一位的。

8.3 事务性考虑

在服务端处理PDF时要注意,如果处理过程中出了问题,原始字节流要保留住,否则用户重试的时候你手里没原始文件可处理。我的做法是先存一份源文件字节或者对象存储路径,处理成功后再更新状态为“已处理”。这样至少可以保证重试时能拿到原始数据,不至于搞出只处理了一半的文件。

9. 几个容易被忽略的边界情况

9.1 页面数量为零的PDF

测试的时候一定要考虑到空文档的情况。某些系统会生成一个完全没有页面的PDF,这时你对document.getPage(0)调用直接抛IndexOutOfBoundsException。处理方式是提前判断:

if (document.getNumberOfPages() == 0) { // 直接加一页空白页或返回原始字节 }

9.2 加密的PDF

只调用PDDocument.load对加密PDF会抛异常,需要先用OpenPassword提供密码。如果你的业务有可能遇到加密PDF,建议在出错时捕获异常并给出明确提示,别让用户看到一串堆栈。

9.3 页面旋转

有些从扫描仪或者手机App生成的PDF,页面内部可能设置了旋转属性(Rotate的值为90、180、270)。你在追加内容时如果没考虑这个旋转,文字坐标会让人觉得“歪了”。处理办法是在内容流中读取页面的旋转值并做坐标换算,或者直接用PageMode相关API辅助定位。

9.4 字符编码与全角半角

写中文文本时还有个小坑:字符串里有全角空格或者特殊符号时,某些字体渲染出来可能跟预期不一样。我在页脚里用了中文标点“/”,在部分字体下显示变窄。后来统一换成英文斜杠感觉反而更协调。这种事情不试根本不知道,做批量处理前建议先拿单页测一下输出效果。

10. 实用总结与扩展建议

用PDFBox对PDF字节流添加空白页、做简易编辑,核心流程比我预想的简单,但细节确实不少。归纳一下,最关键的几点经验是:

  • 加载方式:PDDocument.load(new ByteArrayInputStream(bytes)),用完一定close;
  • 保存方式:document.save(ByteArrayOutputStream),然后toByteArray();
  • 追加内容:创建PDPageContentStream务必用AppendMode.APPEND模式,否则原页面内容会被覆盖;
  • 中文字体:必须加载外部TTF字体,使用内置标准字体写中文会乱码;
  • 页面尺寸:为了让新增页面与原文档保持一致,最好用getMediaBox()取得原页面尺寸再创建新页面。

这个能力不光能用于“添加空白页”和“写页脚”,稍微扩展一下就能做很多事:批量给合同添加水印、在指定位置插入公司LOGO图片、为PDF添加批注信息、把多页PDF拆分后重新组装等等。原理都一样,只是绘制内容的差异罢了。

我后来还把这个工具类封装成了独立的微服务接口,输入输出都是字节流,上游系统完全不用关心PDF内部结构。整套方案跑了大半年,稳定性一直不错。根据我的实测,字节流模式下的大文件处理必须谨慎评估内存,但如果控制住并发量和文件规模,这个方案完全够用,而且它天然适合微服务和云原生的部署环境。

如果你也是在被“字节流 + PDF操作”的组合拳折磨,希望这篇里的经验和代码能帮你少踩几个坑。头几次做的时候别追求一次到位,先写一版能把文字放上去的demo,再逐步加功能,你会发现PDFBox的接口设计其实非常贴心,大部分你想做的事情它都有对应的原生方法。

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

一键清理系统垃圾工具推荐:小白也能轻松上手的C盘瘦身指南

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

作者头像 李华
网站建设 2026/10/24 3:14:47

OpenCV图像阈值处理详解:固定阈值、Otsu与自适应阈值实战

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

作者头像 李华
网站建设 2026/10/24 3:13:48

PCA9422与STM32F302VC协同实现低功耗嵌入式电源管理全流程解析

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

作者头像 李华
网站建设 2026/10/24 3:13:43

原子物理学习路线:从能级量子数到光谱跃迁的因果链解析

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

作者头像 李华
网站建设 2026/10/24 3:13:24

低功耗电源管理设计:PCA9422 PMIC与PIC18LF46K40 MCU实战方案

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

作者头像 李华
网站建设 2026/10/24 3:09:12

PCA9422与STM32F410RB组合实现低功耗电源管理方案

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

作者头像 李华