sharethought 是做什麼的?
sharethought 檔案是一種將文字區段與影像一起攜帶的有效 PNG 格式。它可以在任何地方開啟並像一般圖片一樣渲染。在圖像背後:用於渲染的 SVG 原始碼,以及一份 Markdown 文件。檔案前端有一個清單,為攜帶的資料建立索引,並在檔案本身內包含提取和重新打包的說明。
這些區段可以透過兩種載體之一傳輸:附加在 IEND 區塊之後(僅限簡單的逐位元組精確傳輸),或放在 PNG 輔助區塊內(可在影像管道中存活)。請參閱載體。
使用參考實作
此儲存庫中的 sharethought.py 是經過測試、僅依賴標準庫的實作。建議優先使用它而非手動解析——每次重新實作都是新的錯誤來源。
python3 sharethought.py pack --png base.png --svg s.svg --doc d.md -o out.png --what "one line"
python3 sharethought.py unpack out.png [--out-dir DIR]
python3 sharethought.py repack --text out.png --set doc=new.md -o out2.png
python3 sharethought.py verify out.png
python3 -m unittest -q test_sharethought # 測試套件
pack 會從一次建置中寫入兩種載體(它們不會分歧)。unpack 優先使用區塊,若無則退回尾部,並在兩種載體都存在但內容不一致時大聲報錯——pack 無法產生這種情況,因此不一致表示外部篡改。verify 檢查簽名、IEND、兩種載體、區段名稱語法以及清單與區段的一致性。
格式(精確)
[ PNG 位元組 ... IEND 區塊 ]
SHARETHOUGHT-v1\n
[ JSON 清單 — 一個 UTF-8 物件,參見 schema ]\n
<<<SECTION:name>>>\n
[ 區段內容 — 預設為原始 UTF-8,若 "enc":"base64" 則為 base64 ]\n
<<<END:name>>>\n
... (更多區段,相同框架)
標記 SHARETHOUGHT-v1 緊接在 PNG 的 IEND 區塊之後開始,並以換行符結束。(它前面的位元組是 IEND 區塊的 CRC 尾部,而不是換行符——因此應透過位元組而非行來尋找。)從標記到檔案結尾的所有內容都是尾部。標準區段:svg(影像原始碼,image/svg+xml)和 doc(Markdown,text/markdown)。允許任意附加區段——任何文字類型。
區段名稱必須符合 [A-Za-z0-9_]+。 不符合此語法的名稱將在打包時被拒絕,並在提取時以具名錯誤標記——它們永遠不會無聲無息地消失。
載體
相同的清單和區段可以透過兩種方式攜帶。根據傳輸方式選擇。
A. 尾部——附加在 IEND 區塊之後(預設如上)。
- 手動寫入非常簡單:
cat trailer.txt >> file.png。無需 CRC、無區塊框架、無函式庫。 - 僅在逐位元組精確的傳輸中存活:git、zip、
curl -O、直接檔案複製。 - 無法在重新序列化或重新編碼影像的任何管道中存活。PNG→JPEG 轉換會完全丟棄它。區塊級別的重寫——許多聊天和儲存管道都會這麼做——會保留每個 IDAT 位元組,但丟棄 IEND 之後的所有內容。
B. 輔助區塊——PNG 區塊串流內的 iTXt。
- 存在於區塊列表內部,因此能在區塊級別的重寫和保持 PNG 格式的影像管道中存活。
- 代價是 CRC 和區塊框架,大型區段需要壓縮。
- 無法在 PNG→JPEG 轉換中存活——除了像素之外,沒有任何東西能存活。
建議:同時寫入兩者(pack 就是這樣做的)。相同的負載,一次建置,不可能分歧。讀取器會先尋找區塊,若無則退回尾部,因此檔案在聊天管道和逐位元組精確傳輸中都能正常運作。代價大約是負載多複製一份。
區塊佈局:
[ IHDR ] [ IDAT ... ] [ iTXt sharethought:manifest ] [ iTXt sharethought:<name> ] ... [ IEND ]
每個區段一個 iTXt:清單的關鍵字為 sharethought:manifest,每個區段主體的關鍵字為 sharethought:<name>(原始主體,無分隔符)。標準 iTXt 框架——關鍵字 \0、壓縮標誌(1 位元組,1 = zlib)、壓縮方法(0)、語言標籤 \0、翻譯關鍵字 \0,然後是文字。
清單 schema
{
"v": 1,
"what": "描述此工件的一行文字",
"how": {
"unpack": "如果存在,讀取名為 sharethought:* 的 iTXt 區塊(清單是 sharethought:manifest);否則從 SHARETHOUGHT-v1 標記處讀取位元組到 EOF。解析 JSON 清單,然後解析每個區段在其 <<<SECTION:name>>> 和 <<<END:name>>> 分隔符之間的部分。解碼 base64 區段。",
"repack_text_only": "保留原始的 PNG 位元組直到標記處,然後用更新後的清單和區段重寫尾部。無需重新渲染。或者執行:python3 sharethought.py repack --text FILE --set doc=new.md",
"repack_visual": "要更改影像:編輯 svg 區段,將其渲染為 PNG(rsvg-convert -w 1200 in.svg > out.png,或 cairosvg,或 resvg,或透過 Playwright 的無頭瀏覽器例如 Chromium),然後重新打包。"
},
"sections": [
{ "name": "svg", "type": "image/svg+xml", "enc": "utf8", "summary": "影像的一行描述" },
{ "name": "doc", "type": "text/markdown", "enc": "utf8", "summary": "文件的一行描述" }
]
}
how 欄位是必需的,且必須包含可行的操作說明——檔案會教導任何初次遇到的讀取器如何開啟它。
提取
最佳方式:python3 sharethought.py unpack file.png。手動方式,尾部載體:
raw = open("file.png", "rb").read()
# 走訪 PNG 區塊直到真正的 IEND — 絕不要使用 raw.index() 尋找標記,
# 因為壓縮的 IDAT 位元組可能巧合地包含它。
off = 8
while True:
import struct
ln, typ = struct.unpack(">I", raw[off:off+4])[0], raw[off+4:off+8]
if typ == b"IEND": break
off += 12 + ln
trailer = raw[off + 12:].decode("utf-8")
import json
manifest, _ = json.JSONDecoder().raw_decode(trailer[trailer.index("\n")+1:])
import re
for m in re.finditer(r"<<<SECTION:(\w+)>>>\n(.*?)\n<<<END:\1>>>", trailer, re.DOTALL):
name, body = m.group(1), m.group(2)
# 如果 enc 是 base64:import base64; body = base64.b64decode(body)
手動方式,區塊載體(優先檢查):
import struct, zlib, json
raw = open("file.png", "rb").read()
off, chunks = 8, {}
while off + 8 <= len(raw):
ln = struct.unpack(">I", raw[off:off+4])[0]
typ = raw[off+4:off+8]
data = raw[off+8:off+8+ln]
if typ == b"IEND": break
if typ == b"iTXt":
key, _, rest = data.partition(b"\x00")
flag = rest[0]
rest = rest[2:]
_, _, rest = rest.partition(b"\x00") # 語言標籤
_, _, rest = rest.partition(b"\x00") # 翻譯關鍵字
if flag == 1:
rest = zlib.decompress(rest)
chunks[key.decode()] = rest.decode("utf-8")
off += 12 + ln
manifest = json.loads(chunks["sharethought:manifest"])
sections = {k[len("sharethought:"):]: v
for k, v in chunks.items()
if k.startswith("sharethought:") and k != "sharethought:manifest"}
僅使用 shell:tail -c +$(( $(grep -abo 'SHARETHOUGHT-v1' file.png | head -1 | cut -d: -f1) + 1 )) file.png 以文字形式列印尾部(僅在尾部於傳輸中存活時有效)。
建立
最佳方式:渲染 SVG,然後 python3 sharethought.py pack --png out.png --svg diagram.svg --doc notes.md -o artifact.png。手動方式:
- 撰寫 SVG(人類可見的圖片或圖表)。
- 渲染為 PNG:
rsvg-convert -w 1200 diagram.svg > out.png(或cairosvg diagram.svg -o out.png,或resvg diagram.svg out.png,或無頭瀏覽器——透過 Playwright 的 Chromium——這通常是唯一安裝的 SVG 渲染器)。 - 撰寫 Markdown 文件。
- 附加尾部(
cat trailer.txt >> file.png的形式),更好的做法是同時寫入 iTXt 區塊——pack會從一次建置中完成兩者。
輸出會像一般 PNG 一樣渲染。驗證:在任何圖片檢視器中開啟(顯示圖片),然後 strings artifact.png | grep SHARETHOUGHT(顯示尾部存在),或 python3 sharethought.py verify artifact.png(檢查所有內容,包括兩種載體一致)。
重新打包
最佳方式:python3 sharethought.py repack --text artifact.png --set doc=new.md -o out.png——重寫兩種載體,無需渲染器。
僅文字編輯(文件變更,圖片不變):重複使用原始 PNG 位元組(SHARETHOUGHT-v1 之前的所有內容),用編輯過的區段和更新後的清單摘要寫入新的尾部。如果檔案也攜帶區塊,則一併重寫那些區塊——參考實作從同一個來源重建兩者,這是保持它們一致的唯一安全方式。
視覺編輯(svg 變更):將修改後的 SVG 渲染為全新的 PNG,然後用新的 svg 區段 + 文件 + 清單再次打包。任何 SVG 渲染器都可以使用,包括無頭瀏覽器。
往返規則:重新打包的檔案仍然是 sharethought 檔案。相同的格式,相同的標記,更新後的清單。
分隔符安全性
區段分隔符是各自獨立一行的 <<<SECTION:name>>> 和 <<<END:name>>>。如果內容可能包含這樣的行,請在該區段的清單中設定 "enc":"base64" 並將主體進行 base64 編碼——pack 在主體包含自己的結束分隔符或標記字串時會自動執行此操作。
實際會發生的碰撞:主體包含其他區段的分隔符是無害的。主體包含自己的 <<<END:name>>> 行會被非貪婪正則表達式截斷——該行之後的所有內容都會被孤立。參考實作會檢測此情況(孤立內容和不平衡分隔符錯誤),而不是無聲截斷。若有疑慮,請使用 base64。
保持負載存活
負載不屬於 PNG 的像素資料,因此任何重新編碼影像的工具都可能無聲丟棄它,留下一個看起來完全正常但沒有深度資料的 PNG。這包括 Pillow 中的 Image.open(f).save(g)、影像編輯器和 CDN 最佳化工具。請從原始位元組重新打包,而不是重新儲存渲染後的檔案。
IEND 之後的尾部僅在逐位元組精確的傳輸中存活。如果檔案必須經過影像管道——聊天附件、縮圖產生器、轉碼器——請使用區塊載體(pack 會這麼做),並以上傳檔案而非行內圖片的方式上傳,以避免 PNG 被轉碼為 JPEG。
與人類協作
人類看到 PNG——簡化後的視覺層。所有深度資料(svg 原始碼、文件、清單)對一般瀏覽不可見,但對你完全可見。當人類詢問圖片時,提取並閱讀文件以獲取上下文。當人類想要更改圖片時,編輯 svg 並進行視覺重新打包。當人類想要添加深度資料時,編輯文件並進行僅文字重新打包。這個檔案是完整的交流——一個工件,兩種模式,無需側通道。