Apple PencilKit - PKDrawing のバイナリ
メモ帳とかフリーボードとかにApplePencilで描くあれ。 SVGに変換したくてClaudeに投げたら解析してくれた。
ヘッダー8Bの後はProtoBufferの模様。
ヘッダー8Bはマジックナンバー4Bとバージョン4B。
マジックナンバーは wrd\xf0==77 72 64 f0
protobufで解凍できるところをひたすら解凍する感じ。
一番上のfield4がツールについて、field5がストローク
ツールは色と名前が入っている。 ストロークはツールのインデックス番号とかの情報に並んで点群が入っている。 だいたいFloat32なのでこれを読んでいく。
とにかく下のコードを読んでなんとか理解してほしい。 丸投げ。
このコードで私のアイコンの元絵は読めた。 が、データによってまちまち。 そもそもメモ側で形状認識を使うと Apple Paper 形式 みたいなフォルダでsqliteが吐かれたり、ラスタ消しゴム使うとpngが吐かれたりと、純粋なPKDrawingバイナリはなかなか手に入らない。 面白いテーマではあるけども、使い所がいまいち……
#!/usr/bin/env -S bun --install=force
import{depb}from'@mcbeeringi/petit/protobuf';
const
td=new TextDecoder(),
uuid=x=>x.toHex(),//x.reduce((a,x)=>a+x.toString(16).padStart(2,0),''),
va=x=>depb(x).reduce((a,x)=>(x.type==0&&(a[x.i-1]=+x.value),a),[]),
fa=x=>depb(x).reduce((a,x)=>(x.type==5&&(a[x.i-1]=new Float32Array(x.value.buffer)[0]),a),[]),
parse=w=>w.slice(0,4).toHex()=='777264f0'&&depb(w.slice(8)).reduce((a,x)=>(
x.i={
2:'uuid',
3:'order',
4:'tool',
5:'stroke',
6:'view',
7:'meta'
}[x.i]??x.i,
a[x.i]??=[],
a[x.i].push(({
uuid,
order:va,
tool:x=>depb(x).reduce((a,x)=>(
x.i={
1:'col',2:'name',3:'ver'
}[x.i]??x.i,
a[x.i]=({
col:x=>fa(x).reduce((a,x,i)=>a+(i==3&&+x==1?'':Math.round(x*255).toString(16).padStart(2,0)),'#'),
name:x=>td.decode(x).split('.').reverse(),
}[x.i]??(_=>_))(x.value),
a
),{}),
stroke:x=>depb(x).reduce((a,x)=>(
x.i={
2:'ctx',
3:'layer_info',
4:'tool',
5:'path',
6:'bound'
}[x.i]??x.i,
a[x.i]=({
field2:va,
field3:va,
path:x=>depb(x).reduce((a,x)=>(
x.i={
1:'uuid',
2:'time',
3:'num',
6:'scale',
7:'point'
}[x.i]??x.i,
a[x.i]=({
uuid,
// time:x=>new BigUint64Array(x.buffer)[0]
// scale:x=>({
// scale:new Float32Array(x.buffer,0,1)[0],
// grid:new Uint32Array(x.buffer,4,1)[0],
// flag:x.subarray(8)
// }),
point:(w,n=w.length/a.num)=>[...Array(+a.num)].map((x,i)=>(
x=w.slice(i++*n,i*n),
new Float32Array(x.buffer,0,3).reduce(
(a,x,i)=>(a[['x','y','pressure'][i]??i]=x,a),
{unknown:x.subarray(12)}
)
))
}[x.i]??(_=>_))(x.value),
a
),{}),
bound:fa
}[x.i]??(_=>_))(x.value),
a
),{}),
view:fa,
meta:va
}[x.i]??(_=>_))(x.value)),
a
),{version:w.slice(4,8)}),
p2a=x=>x.map(x=>[x.x,x.y]),
psub=(a,b)=>a.map((x,i)=>b[i]-x),
pfmt=x=>x.map(x=>x.toFixed(2)),
mp=(a,b)=>a.map((x,i)=>(x+b[i])/2),
pk2svg=w=>(w=parse(w))&&`<svg xmlns="http://www.w3.org/2000/svg" viewBox="${
console.log(w.view),
(x=>[x.bx,x.by,x.tx-x.bx,x.ty-x.by])(w.stroke.reduce((a,{bound:x})=>({
bx:Math.min(a.bx?? 1/0,x[0]),
by:Math.min(a.by?? 1/0,x[1]),
tx:Math.max(a.tx??-1/0,x[0]+x[2]),
ty:Math.max(a.ty??-1/0,x[1]+x[3])
}),{}))
}" fill="none" stroke-linejoin="round" stroke-linecap="round">\n${
w.stroke.map(x=>`<path d="${
// x.path.point.map((x,i)=>(x=[x.x,x.y].map(x=>x.toFixed(2)),i?`L${x}`:`M${x}`)).join('')
[
(x=>`M${pfmt(x[0])}l${pfmt(psub(x[0],mp(x[0],x[1])))}`)(p2a(x.path.point.slice(0,2))),
...[...Array(x.path.point.length-2)].map((y,i,a)=>(y=p2a(x.path.point.slice(i,i+3)),y[0]=mp(y[0],y[1]),`${i?',':'q'}${pfmt(psub(y[0],y[1]))},${pfmt(psub(y[0],mp(y[1],y[2])))}`)),
(x=>`l${pfmt(psub(mp(x[0],x[1]),x[1]))}`)(p2a(x.path.point.slice(-2)))
].join('').replace(/,([-0])0?/g,'$1')
}" stroke="${w.tool[x.tool].col}"${w.tool[x.tool].name[0]=='marker'?' opacity=".5" stroke-width="5"':''}/>\n`).join('')
}</svg>`;
await Bun.write(
`${Bun.argv[2].split('/').pop()}.svg`,
pk2svg(await Bun.file(Bun.argv[2]).bytes())
);