☰
你发的照片可能带着家庭住址——我用码道 Agent 写了个 EXIF 隐私清除器,19 项断言验证清除彻底
2026/10/7 15:33:36 网站建设 项目流程

你发的照片可能带着家庭住址——我用码道 Agent 写了个 EXIF 隐私清除器,19 项断言验证清除彻底

一键开通华为云码道 CodeArts 代码智能体:https://developer.huaweicloud.com/codeartsco.html?source=dmzntgwatomgit1&sourcead=dmzntgwatomgithd

一、这玩意儿是干嘛的

你在朋友圈、社交平台发一张手机拍的照片,以为只是发了张图。但那张图的字节里,可能悄悄塞着你家的 GPS 经纬度、拍摄时间、手机型号、甚至相机序列号。别人把图存下来,用个免费的 EXIF 查看器,就能知道你什么时候、在哪个小区按的快门。

我做了个工具:拖入一张照片,它解析出里面所有隐私字段(GPS、机型、时间…)给你看,然后一键把这些字段从字节层面抹掉,输出一张"干净"的图。关键是——清除结果可验证:清除后 GPS 读不出来了、但图片本身没坏(尺寸、画面完好)。

代码是喂华为云码道 CodeArts Agent一轮轮建的,仓库atomgit.com/wangleizi/exif-scrubber,零第三方依赖,纯字节解析,验证脚本 19/19 断言全过。

二、为什么做这个

前阵子看新闻,有人晒火车票照片,二维码和座位信息没打码,被人反查出行程。我心想照片这玩意儿更隐蔽——火车票你至少知道要打码,但 EXIF 里的 GPS 是默认写进去、肉眼完全看不见的。

我自己试了下:手机随手拍一张,用系统"属性"一看,经度纬度、几点几分、哪款手机,全在里面。这太吓人了,而且大家根本没意识。

更麻烦的是,很多人以为"我裁了图、加了滤镜再发就没事了"——不一定。很多修图 App 会保留原图的 EXIF,你裁完发出去 GPS 还在。真正稳妥的办法,是在发送前把元数据整段剥掉。可市面上大部分"清除 EXIF"的在线工具,要你先把隐私照片上传到别人服务器——这本身就是二次泄露。所以我想做一个本地跑、不联网、字节级操作的:照片不出你电脑,清除过程全透明,还能验证。

这事儿适合做成"可验证"的工具,因为它有确定的对错标准:一个 JPEG 的 EXIF 存在 APP1 段里,格式是公开的规范(TIFF/IFD 结构)。我能精确解析出每个字段、精确地把那段字节删掉、再精确地验证"删干净了且没删坏图片"。每一步都能用断言卡住,不是"感觉清掉了"。

选题三条铁律:一秒看懂、有客观真值、别撞车。隐私清除这个赛道,获奖名单里没有,跟"文件管家"也不同——那是整理文件,这是面向隐私的字节级解析器,且清除结果可被第三方工具复核。

三、先跑起来看看

零依赖纯 Node 20+ ES Module:

gitclone https://atomgit.com/wangleizi/exif-scrubber.gitcdexif-scrubbernode--test# 单元 + 集成测试nodeevidence/verify.mjs# 19 项断言,验证清除彻底nodebin/scrub.mjs in.jpg out.jpg# 命令行清除

命令行node bin/scrub.mjs一条命令把一张带 GPS 的图洗成干净的。前端页面(白底)拖入图片能可视化看到"清除前有哪些隐私字段、清除后还剩什么"。

这里先给个直观感受:一张 354 字节的最小测试 JPEG(含 GPS、机型、时间),清除后只剩 33 字节——那 300 多字节全是你的隐私元数据,图片画面本身一个像素没动。真实手机照片里,这个 APP1 段可能有几十 KB,塞着经纬度、精确到秒的拍摄时间、你手机的具体型号、甚至序列号。

四、怎么"钉"码道 Agent

老规矩,提示词钉到函数级,结尾挂铁律:

直接建文件并git add -A && git commit && git push,不要只描述、不要贴代码正文,只回复 git log + 文件树 + 测试 pass/fail。

第一轮提示词(原样):

【R1 · EXIF 隐私清除器 exif-scrubber 核心字节解析层】 建 public 仓库 exif-scrubber。Node 20+ ES Module,零依赖(自己解析字节,不引第三方 exif 库)。 - src/jpeg.mjs parseSegments:校验 SOI(FFD8),遍历 marker(FFEx APPn/FFC0 SOF/FFD9 EOI) - src/tiff.mjs parseIfd:解析 TIFF header(II/MM 字节序 + 魔数 0x2A + IFD 偏移),读 IFD0 - src/scrub.mjs 移除 APP1/EXIF 段,保留 SOI/SOF/SOS/图像数据/EOI - test 用手工构造的最小 JPEG Buffer 断言能识别 APP1、scrub 后 hasExif=false 且 SOI/EOI 仍在 直接建文件并 git commit && git push,只回复 git log + 文件树 + 测试 pass/fail

切三轮喂:R1 字节解析核心、R2 白底前端+命令行+对拍、R3 README+集成测试。每轮本地复核。

五、架构:把 JPEG 当成一串带标记的块

JPEG 不是一坨像素,它是一串带 marker 的段:SOI 开头、中间若干 APPn 段(EXIF 就藏在 APP1)、SOF 存尺寸、SOS 之后才是压缩的图像数据、EOI 结尾。清除 EXIF 的本质,就是找到那个 APP1 段、把它的字节整段抠掉,别碰其它段。

想通这一点,整个项目就简单了:它不是"图像处理",是"二进制文件结构解析"。我不需要懂 JPEG 怎么压缩像素,我只需要认得每个 marker 的魔数、按声明的长度跳过对应的段。APP1 段自己会告诉你它有多长,我从头顺着 marker 走一遍,遇到 EXIF 的那个就记下起止位置,最后把这些字节挖掉、其余原样拼回去。图像数据段(SOS 之后那一长串)我根本不碰,所以画面不可能被弄坏——这也是为什么"清除后尺寸仍是 800×600"这条断言几乎必然成立。

而 APP1 里面又是另一套结构:它开头是Exif\0\0六个字节,后面跟一个完整的 TIFF 文件。TIFF 又分 IFD0(相机厂商、型号这些)、Exif 子 IFD(拍摄时间这些)、GPS 子 IFD(经纬度这些),彼此用偏移量指针串起来。所以解析 EXIF 其实是"在 JPEG 里解析一个 TIFF",两层结构套娃。这也是这个项目最有意思、也最容易翻车的地方。

bytes(大小端读) → jpeg(marker 段遍历) → tiff(IFD 目录解析) → fields(敏感字段表) → scrub(移除 APP1) ↓ verify(19 断言:清除彻底 + 图片完好)

六、核心代码,掰开揉碎

scrub 的逻辑特别干净:遍历所有段,凡是 EXIF 的 APP1 段,记下它的字节区间,然后把这段从 buffer 里剔除、其余原样拼接:

// src/scrub.mjsexportfunctionscrub(buf){constsegments=parseSegments(buf);constremoveRanges=[];for(constsegofsegments){if(isExifApp1(buf,seg))removeRanges.push([seg.offset,seg.offset+seg.length]);}if(removeRanges.length===0)returnBuffer.from(buf);removeRanges.sort((a,b)=>a[0]-b[0]);constparts=[];letcur=0;for(const[start,end]ofremoveRanges){if(start>cur)parts.push(buf.subarray(cur,start));cur=end;}if(cur<buf.length)parts.push(buf.subarray(cur));returnBuffer.concat(parts);}

哪些字段算"敏感",我让码道列了一张明确的表——GPS 经纬度/海拔/时间戳、拍摄时间、相机厂商型号、序列号、机主姓名,共 18 个:

// src/fields.mjs(节选)exportconstSENSITIVE_TAGS=[{tag:0x8825,name:'GPSInfoIFDPointer',why:'GPS 信息块指针,含经纬度/海拔/时间戳'},{tag:0x0002,name:'GPSLatitude',why:'GPS 纬度坐标'},{tag:0x0004,name:'GPSLongitude',why:'GPS 经度坐标'},{tag:0x9003,name:'DateTimeOriginal',why:'原始拍摄时间'},{tag:0x010F,name:'Make',why:'相机制造商'},{tag:0xA435,name:'SerialNumber',why:'相机序列号'},{tag:0xA420,name:'OwnerName',why:'相机所有者姓名'},// ... 共 18 项];

这张表我特意让码道给每个字段都配了一句"为什么敏感"的人话说明,而不是光列个 tag 号。因为对普通用户来说,"0x8825"没有意义,"你家的经纬度"才有意义。工具的输出要让人看懂自己在丢什么,这比功能本身更重要。比如MakerNote(厂商私有注记)这个字段,很多人不知道它里面常藏着相机序列号、对焦距离甚至拍摄者信息,我也是在做这个项目的过程中才搞清楚的——这大概就是这类"隐私科普型工具"的额外价值:它顺手把你没注意到的东西摊开给你看。

TIFF/IFD 解析是最硬的一块——要处理大小端字节序、目录项偏移、有理数(GPS 坐标是"度/分/秒"三个有理数拼的)。码道这块一次写对了:

底层得先有个能按大小端读字节的工具。TIFF 的字节序由 header 头两个字节决定(II小端 /MM大端),读错端序,所有偏移和数值全乱:

// src/bytes.mjs —— 支持大小端的字节读取器exportfunctionReader(buf,littleEndian){constu16=(o)=>littleEndian?buf.readUInt16LE(o):buf.readUInt16BE(o);constu32=(o)=>littleEndian?buf.readUInt32LE(o):buf.readUInt32BE(o);constrational=(o)=>({num:u32(o),den:u32(o+4)});// GPS 度分秒用return{u16,u32,rational};}

verify.mjs 把"两类断言"落地成可跑代码——清除前必须读得到、清除后必须读不到、且图片结构必须还在:

// evidence/verify.mjs(节选)constbefore=parseIfd(findApp1(sample));assert.equal(before.Make,'TestCam');// 清除前:隐私在assert.ok(before.GPSLatitude);// 清除前:GPS 在constcleaned=scrub(sample);assert.equal(hasExif(cleaned),false);// 清除后:EXIF 没了assert.equal(cleaned[0],0xFF);// 清除后:SOI 还在assert.deepEqual(sizeOf(cleaned),{w:800,h:600});// 清除后:尺寸没坏

七、护城河:不光要"删了",还要证明"删干净了且没删坏"

一个隐私清除器最容易自欺的地方是:它"声称"清除了,但你怎么知道它真清干净了、又怎么知道它没把图片搞坏?我让码道写了个evidence/verify.mjs,用一张内置的、故意塞满 GPS/机型/时间的最小 JPEG做样本,跑 19 条断言:

清除前:解析出 GPS 30°15′N / 120°E、Make=TestCam、Model=X-100、DateTime … ✅ 清除后:hasExif=false ✅ · SOI/EOI 完整 ✅ · 尺寸仍 800×600 ✅ · APP1 段消失 ✅ · 体积 354 → 33 字节 ✅

这 19 条分两类,缺一不可:一类证明隐私真的没了(GPS/机型/时间读不出来了),一类证明图片没被删坏(SOI/EOI 还在、尺寸解析得出来、画面数据完整)。只做前一类,可能把整张图删废了也算"清除成功";只做后一类,可能图片好好的但 EXIF 没删掉。两类一起,才叫"清除彻底且无损"。


八、测试

// test/jpeg.test.mjstest('scrub 移除 APP1 且保留 SOI/EOI',()=>{constout=scrub(sampleJpeg);assert.equal(hasExif(out),false);assert.equal(out[0],0xFF);assert.equal(out[1],0xD8);// SOI 还在assert.equal(out.at(-2),0xFF);assert.equal(out.at(-1),0xD9);// EOI 还在});// test/tiff.test.mjs —— 小端 TIFF 能读出 Make='TESTCAM'

九、真实的坑

坑 1:又是私有仓。码道默认把 exif-scrubber 建成了 private,我本地 clone 弹登录框。发 nudge 让它改 public 才匿名可访问。这毛病我三篇里遇到两回了,提示词写"public"它也不当回事。

坑 2:push 认证崩。R2/R3 码道本地提交推不上去(ag 的 remote token 没了),我发"重新绑定仓库再 push"的 nudge 才救回来。做到这个系列,码道 push 崩已经是保留节目。

坑 3:本地跑不起来。最坑的一次——我本机 git clone exif-scrubber 一直弹凭据框、被自动取消(另两个仓库的凭据还缓存着能 clone,这个没有),raw/API/zip 下载全返回 SPA 外壳。最后我改用"登录浏览器导航到 blob 页、从渲染的 DOM 里把代码抠出来"才拿到源码和验证报告。这条路的折腾,本身就说明:验证一个工具,最好别只信它自己的报告——我这篇里的数字,是我从远端仓库的真实报告里抓出来的,不是我本地跑出来的,二者能对上才算数。

坑 4:字节序和偏移是雷区。TIFF 里 IFD 的偏移是"相对于 TIFF header 起点"的,不是相对于 JPEG 的 APP1 起点。码道第一版把基准搞混过一次,读出来的 Make 全是乱码。修法是解析时统一记住 TIFF 段的起始偏移,所有 IFD/子目录偏移都基于它换算。这种"差一个基准全错"的坑,AI 不太会主动防,得靠测试卡。

集成测试我把"端到端"钉死成一条链——构造带隐私的图、解析读到、清除、再解析读不到、且图片仍完好:

// test/integration.test.mjstest('端到端:带 GPS 的图 → 清除 → 隐私消失且图片完好',()=>{constdirty=buildSampleJpeg({gps:[30,120],make:'TestCam'});assert.ok(hasExif(dirty));assert.ok(parseIfd(findApp1(dirty)).GPSLatitude);constclean=scrub(dirty);assert.ok(!hasExif(clean));assert.equal(parseSof(clean).width,800);// 画面尺寸没被删坏});

说到底,做隐私工具最讽刺的失败是"你以为删干净了,其实没删"。所以我没让它只输出"清除成功"这句话,而是逼它把清除后的字节再解析一遍、把 GPS 字段再读一次、把图片尺寸再量一次——三个动作分别对应"删没删、删干净没、删坏没"。少任何一个,这个工具都不值得信。

十、提效数据

环节码道 Agent我
建仓库 + 字节解析核心✅出题
JPEG/TIFF/IFD 解析✅(大小端一次对)验收
18 个敏感字段表✅补充
19 条可验证断言✅ 实现定"删干净+没删坏"两类
改 public / 修 push❌✅ nudge
抓远端报告核对数字❌✅ 浏览器 blob

这张表看下来你会发现一个规律:凡是"有明确规范可对照"的活(JPEG marker、TIFF IFD、字段表、断言),码道一次就做得又快又对;凡是"需要判断这算不算真验证、边界在哪、报告可不可信"的活,它一概不管,甚至会把私有仓、失效 push 这些坑留给你收尾。所以用它的正确姿势是:把标准明确的部分甩给它,把需要较真的部分自己攥着。

十一、五维自检

  • 眼前一亮:你发的照片可能带着家庭住址,一秒被戳中。

  • 可验证护城河:19/19 断言,既证隐私清除、又证图片无损,node evidence/verify.mjs可复现。

  • 原创度:不是文件管理器,是面向隐私的字节级解析器。

  • 工程质量:零依赖、纯 Buffer 操作、大小端正确处理。

  • 诚实度:只处理 JPEG APP1、未覆盖 PNG/WebP、缩略图内嵌 EXIF 未处理,全写明。

这里特别说一下"未覆盖"的部分,因为做隐私工具,说清楚"哪些没保护到"比吹"保护得多好"重要得多。第一,我只处理 JPEG 的 APP1 段;PNG 的元数据在 tEXT/iTXt 块里、WebP 又在另一套结构里,这个工具都不碰——你拿 PNG 来洗,它会老实告诉你"没检测到 EXIF",而不是假装成功。第二,JPEG 的缩略图(EXIF 里会内嵌一张小预览图)本身也可能带元数据,我整段移除 APP1 时其实把缩略图一起删了,所以这个反而顺带解决了,但我在 README 里没把它当卖点,因为它是个副作用不是设计。第三,有些手机会把位置写进 XMP(一种基于 XML 的元数据),那不在标准 EXIF 的 IFD 里,我也没处理。这些边界我全列在 README 的"已知不足"里——一个隐私工具最怕的就是给你一种"洗干净了"的虚假安全感。

十二、写在最后

这个小工具让我自己养成了一个习惯:发照片前先洗一遍 EXIF。而它教会我最深的一课,恰恰是那个"本地跑不起来、只能从远端仓库抓报告"的坑——当你的验证环境本身可能骗你时,去信一个你没法篡改的第三方视角。我没法在本地跑 exif-scrubber,所以我没有编数字,而是把它推到 public 仓库、再从仓库的渲染页面把真实报告抠回来核对。这跟这个系列前几篇反复讲的"别把自证当验证",其实是同一句话。

顺带说一句做这类"字节解析"工具的体会:它跟做 UI、做算法都不一样,几乎没有模糊空间——一个 marker 的魔数对不对、一个偏移的基准算没算错,要么成要么崩,测试骗不了人。也正因如此,它特别适合拿来练"可验证"这件事:你没法靠"看起来能跑"糊弄过去,只能老老实实把每条断言写清楚、跑通。码道在这种"标准明确"的活儿上表现最好,因为它不用猜你要什么,JPEG/TIFF 规范就摆在那,它照着实现、我照着验。

欢迎把你手机里随手拍的照片丢进去看看,那些你从没注意到的经纬度,会把你吓一跳:https://atomgit.com/wangleizi/exif-scrubber。洗完再发,图还是那张图,只是不再顺带交出你家地址了。


需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询