照片被Mac删除以及提取缩略图
记录一下3月8号遇到的照片问题:
我的所有照片都存放在Mac自带的图库里,而且只有一份,文件导入后会删掉。当天我一次导入了500张照片,看一下哪些时间轴是有错误的,当时导入的时候就觉得有点慢,看完之后对图片进行删除,并去最近删除里清空,然后它就一直没有反应(之前也遇到过这样的情况,所以并没有很在意),于是按下option进行强退,再次打开,此时桌面中间有个Toast的东西显示正在关闭图库,之前从未出现过这种状态,然后就打不开了,我看了下【照片图库.photoslibrary】仍然是之前的18G,就没有担心。重启电脑之后打开图库显示没有照片,于是我去看【照片图库.photoslibrary】变成了1.6G,当时我就知道这些数据是被Mac图库自身给删除了。
随后自行查找恢复方法,并没有开启icloud备份和时光机,本地也没有备份,网盘里有部分照片但在10天前已经删除了,很绝望。
第二天联系苹果客服,搜集了一些日志暂无结果。同时看百度网盘的超级会员,也只是开通后才保留30天,而不是可以找回30天内删除的文件,我试着上传了20天外删除的照片显示秒传,说明那些文件其实还是存在于百度网盘上的,于是联系网盘负责人,但是对方不予恢复。
然后我试着用软件进行数据恢复,之前我在PC上尝试的效果很好,但是Mac上却只能扫除本地存在的照片,不过我发现了HandShaker和百度网盘的缩略图文件,后来才知道SSD硬盘用软件恢复几乎无效。
我去看photoslibrary里面的内容,找到55张之前删除的图片,看看这剩下的1G内容是什么,在segment下面找到了这样的内容,看字面意思就是缩略图文件片段。
segment目录
到此为止我不太明确知道具体丢失了哪些照片,于是随便拿一个文件ThumbJPGSegment_20.data将它改名为zip,rar,7z都无法打开,后来想应该是把图片做成数据文件集合了,而不是图片目录,于是开始试着恢复缩略图。
我用记事本以UTF8打开图片文件,显示乱码,但是可以看懂部分内容,发现8Photoshop可以是作为分隔符的,而且开头是EXIF信息,结尾也似乎有固定的格式,于是尝试把头尾弄出来,保存成jpg,无法显示。
记事本打开
后来用sublime打开数据文件进行复制,这里可以选择使用UTF8打开或者二进制打开,在UTF8模式下发现含有0x00的内容都无法复制,于是使用二进制的格式进行复制,中间出现了一个错误,就是把复制的二进制内容当成了UTF8的内容保存成了文件,这样文件就是一堆数字,相当于是txt,必须先把目标文件存在本地,然后以二进制打开,再进行内容复制,这样得到的就是二进制数据。
sublime打开
先试着保存一张图片,修改成jpg后依然无法显示,开头结尾我认为应该是对的,我想可能是文件头出了问题,文件头不对解析会出错,如果是内容体或尾部不对只会显示出部分,下面是黑的或者花绿的。我把现存的JPG文件用二进制打开看了看,然后查找了一下JPG的二进制格式规则,以ffd8开头,以ffd9结尾,发现苹果的片段在ffd8之前都有一个其他四位数,于是去掉了这4位数,把ffd9后的数据也都删除,此时图片就显示出来了。
当时很开心,说明还是文件头的问题,于是我又手动试了几个,都可以显示,那么我就开始准备自己去解析出所有的缩略图,看看都丢了哪些文件。这就可以变成是一个算法题,读入一个二进制文件,当遇到ffd8时开始解析,当遇到包含ffd9时停止解析,并把解析内容写入文件保存。结尾符不一定完全是ffd9,可以是ad43 ffd9这种,也可以是adff d900,一开始想着保存上次解析的后两位,和下一次的前两位组合进行判断,后来又看了看原文件,发现结尾符不是ffd9就是d900,这样就可以简化算法。
起初我是把二进制文件作为字符串保存,这样理解起来比较直观,然后用char做单位进行比较,一次读5位,因为作为文本是包含空格的,
File file = new File(Environment.getExternalStorageDirectory() + "/", "ThumbJPGSegment_20.data");
FileInputStream fis = new FileInputStream(file);
InputStreamReader isr = new InputStreamReader(fis);
int n = 0;
char[] cbuf = new char[5];
char[] start = new char[] {'f', 'f', 'd', '8'};
char[] end = new char[] {'f', 'f', 'd', '9'};
char[] end2 = new char[] {'d', '9', '0', '0'};
StringBuilder stringBuilder = new StringBuilder();
boolean find = false;
boolean starts = false;
// count是用来控制log打印的,便于调试位数的错误
int count = 0;
do {
n = isr.read(cbuf);
// count++;
Log.e("sss", "" + cbuf[0]);
Log.e("sss", "" + cbuf[1]);
Log.e("sss", "" + cbuf[2]);
Log.e("sss", "" + cbuf[3]);
Log.e("sss", "cbuf[0]=='f' " + (cbuf[0] == 'f'));
if (n != -1) {
if (!starts) {
// Log.e("sss", "not start");
if (cbuf[0] == start[0] && cbuf[1] == start[1] && cbuf[2] == start[2] && cbuf[3] == start[3]) {
Log.e("sss", "start");
stringBuilder.append(cbuf);
starts = true;
// last = cbuf;
}
} else {
if ((cbuf[0] == end[0] && cbuf[1] == end[1] && cbuf[2] == end[2] && cbuf[3] == end[3]) ||
(cbuf[0] == end2[0] && cbuf[1] == end2[1] && cbuf[2] == end2[2]
&& cbuf[3] == end2[3])) {
stringBuilder.append(cbuf);
Log.e("sss", stringBuilder.toString());
find = true;
starts = false;
File file1 = new File(Environment.getExternalStorageDirectory() + "/", "a.jpg");
if (!file1.exists()) {
file1.createNewFile();
}
FileOutputStream fos = new FileOutputStream(file1);
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(fos));
bw.write(stringBuilder.toString());
bw.close();
// byte bt[] = stringBuilder.toString().getBytes();
// try {
// FileOutputStream in = new FileOutputStream(file1);
// in.write(bt, 0, bt.length);
// in.close();
// Log.e("sss", "写入文件成功");
// } catch (Exception e) {
// e.printStackTrace();
// }
} else {
stringBuilder.append(cbuf);
}
}
}
} while (n != -1 && !find); // &&count<5
这样的问题是需要先把缩略图数据文件转成txt,然后用char数组读取,char比较需要一位一位比,判断上是方便了,但是最终生成的文件其实是个字符串文件,内容是ffd8 3321 6555这种,转成二进制格式就不是图片了,并且考虑到前面还要做处理就想着换成读byte的方式读取原始文件。
那么依然以二进制的方式来读取,用DataInputStream,一次读2位,先把前5个数打印出来是类似0 0 32 40 6这样的展示,那么就来判断开头是否等于ff和d8,此时遇到了一个问题,16进制的ff转成10进制是255,如果直接判断byte数字等于255会显示越界,byte的范围是-128~127,所以ff先变成二进制数11111111,第一个1是符号表示负数,后面7个1按位取反再加1,也就是求补码的规则,ff是-1,同理d8是-40,d9是-39,那么接下来就可以写代码了。
FileInputStream fis = new FileInputStream(file);
DataInputStream dis = new DataInputStream(fis);
int n = 0;
byte[] cbuf = new byte[2];
int count = 1;
do {
if (n != -1) {
File destDir = new File(Environment.getExternalStorageDirectory() + "/输出/" + num + "/");
if (!destDir.exists()) {
destDir.mkdirs();
}
File destFile =
new File(destDir.getAbsolutePath(), count + ".jpg");
if (!destFile.exists()) {
destFile.createNewFile();
}
FileOutputStream in = new FileOutputStream(destFile);
boolean find = false;
boolean starts = false;
do {
n = dis.read(cbuf);
// Log.e("sss",""+cbuf[0]);
// Log.e("sss",""+cbuf[1]);
if (!starts) {
// 补码 byte
if (cbuf[0] == -1 && cbuf[1] == -40) {
Log.e("sss", "start");
// stringBuilder.append(cbuf);
// end[count] = cbuf[0];
// end[count + 1] = cbuf[1];
in.write(cbuf, 0, cbuf.length);
starts = true;
// last = cbuf;
}
} else {
// 二进制转成10进制,不是16
if ((cbuf[0] == -1 && cbuf[1] == -39) ||
(cbuf[0] == -39 && cbuf[1] == 0)) {
in.write(cbuf, 0, cbuf.length);
// end[count] = cbuf[0];
// end[count + 1] = cbuf[1];
// stringBuilder.append(cbuf);
// Log.e("sss", stringBuilder.toString());
find = true;
starts = false;
in.close();
Log.e("sss", "写入文件成功");
} else {
// stringBuilder.append(cbuf);
// end[count] = cbuf[0];
// end[count + 1] = cbuf[1];
in.write(cbuf, 0, cbuf.length);
}
}
} while (!find);
}
count++;
} while (true);
一共2个循环,外层控制整个文件的读取,内层控制单独jpg文件的生成,对于多个源文件外层可以再加个目录的遍历for循环。这里也不考虑效率,一次写2个byte,边读边写,可以优化成读到1024个再写一次,也就是注释中end数组,每次给赋值两位,然后+2。这其实是个java脚本,我在安卓上开了个子线程运行,缩略图都显示出来了。
一个20个目录,每个2000张,共计4万张缩略图,每张缩略图148*120的分辨率大约在6k,质量很差。通过缩略图查看自己究竟丢失了哪些照片,我也发现只要导入图库里的文件,都会生成缩略图,几年前那些删除的老照片也能看到。
缩略图
后续事情:
1.跟进苹果客服维修的事情。
2.维权投诉,数据属于属于Mac自己删除,非用户删除,crash日志存在,缩略图文件存在。
crash部分日志3.计算丢失的照片:
SSD和机械硬盘不同,恢复不是纯软件可以做到的,硬件手段都很难恢复,先罗列下。
4.多渠道恢复数据:
(1)继续联系百度网盘人员
(2)从用过的手机上进行数据恢复,都是未root的手机,成功率不高
(3)其他渠道找回照片:微信,抖音,微博,zao,笔记,简书,
(4)视频截图
5.尝试继续恢复缩略图:
Segment下除了ThumbJPGSegment_20.data外,还有Thumb64Segment_20.data,我发现后者和前者几乎同一天生成,命名数字匹配,且大小是前者2倍,因此怀疑后者可能是大一倍的缩略图也就是250*250的规格,但它不符合JPG规则,目前也没有发现任何规律,全是火星文,可以排除base64加密。
6.求助于数据恢复,求助。