ArcGIS 切片快取緊湊檔案格式分析與使用
一、分析
在ArcGIS 10中出現了一種新的切片快取檔案格式:緊湊型儲存(Compact)。與之前的鬆散型儲存(Exploded)相比,它有遷移方便、建立更快、減少儲存空間等諸多優點,已經成為了建立切片快取的預設格式。對於本身ArcGIS的產品而言,訪問緊湊型儲存與訪問鬆散型儲存沒有任何區別,但是,如果第三方應用想訪問新的切片格式,目前官方給出了“不可以”的答覆:
The internal architecture of the bundle is not publicly documented by ESRI. If you've coded your own logic to pull tiles out of a virtual directory, you should continue to use the "exploded" format which stores each tile as a single file and was the only option at ArcGIS Server versions 9.3.1 and previous.
我Google了一下,也沒有任何相關的資料,因此索性自力更生,自己分析一下緊湊型儲存的格式,相信這是目前可以找到的關於緊湊型儲存內部格式的唯一資料。
緊湊型儲存的原理
緊湊型儲存最主要的兩種檔案是bundle和bundlx檔案,其中bundle檔案用以儲存切片資料,bundlx是bundle檔案中切片資料的索引檔案。
一個bundle檔案中最多可以儲存128×128(16384)個切片,但是建立切片快取並不是一張張切片單獨生成,而是以4096畫素(無抗鋸齒)或2048畫素(有抗鋸齒)為邊長渲染的,如果我們選擇的切片邊長為256畫素並開啟了抗鋸齒,那麼每次ArcSOC程序建立的是一張以8×8(64)個切片拼接成的大圖,然後切割後存入bundle檔案中。
下圖中,藍色邊框代表的是bundle檔案,黑色格子是生成切片時拼接的大圖,具體的每個切片在黑色格子中,圖中並沒有顯示出來。
儲存格式的分析
在分析緊湊型儲存格式之前,我首先問自己,如果你要在一個bundle檔案中儲存內容,同時通過一個bundlx檔案中存放索引應該怎麼做?中規中矩的做法就是參考資料庫的點陣圖索引方式,在bundlx檔案中用固定的幾個位元組標識一個切片在bundle檔案中的狀態(儲存的偏移量和長度)。
觀察ArcGIS生成的bundlx檔案,每個檔案都是一樣的大小:81952位元組。上面已經提到,每個bundle檔案中最多儲存16384個切片,雖然bundle檔案中可能並沒有這麼多切片,但是,我猜測bundlx檔案中必然是保留了所有者16384個切片的索引位置。粗略估計每個切片會佔據大約5個位元組,16384×5=81920位元組,還多出32位元組,猜測儲存bundlx檔案的標識資訊。
通過對一個很儲存切片很稀疏的bundlx檔案的規律進行觀察和猜測,確定了bundlx中檔案起始16位元組和檔案結束16位元組與索引無關,剩餘的81920位元組資料以5個位元組的頻率重複,構成了一個對bundle檔案的索引。
本來以為這5個位元組會儲存bundle檔案中切片資料的偏移和長度,但是發現5個位元組表達的資訊量可能不夠,因此,我同時對bundle中的切片資料進行了一個分析。
我猜想檔案並沒有進行壓縮處理,因此在檔案中搜索PNG檔案的檔案頭0x89504E47(我在建立快取時選擇了PNG24格式),發現果然如此。同時,每2個切片資料之間相隔了4個位元組(切片資料我是用Exploded的圖片直接進行比較的),通過猜想、嘗試,發現這4個位元組正好是以低位到高位的方式標示了後續這個切片資料的長度。
既然切片資料長度是在bundle檔案中記錄的,那麼在bundlx檔案中索引的必然只包括切片資料的偏移量,經過實驗發現,bundlx中的5個位元組也是以低位到高位的方式標示了資料的偏移量。
切片資料長度和資料偏移猜想應該是無符號的整數,後面的實踐證明了這一點。
還有一個問題,bundlx中的每5個位元組標示的到底是哪個切片的資料偏移?我的實驗的結果是:按列排序:
1 |
129 |
… |
… |
2 |
130 |
||
3 |
131 |
||
… |
… |
||
… |
… |
||
128 |
256 |
16384 |
從上面的分析,我們如果知道了一個切片的級別、行號、列號,就可以通過bundlx首先找到bundle中切片內容的偏移,然後從bundle檔案中取出4個位元組的長度資料,再隨後根據這個長度讀取真實的切片資料。關於如何計算切片的行號、列號,以及bundle檔案的命名方式,相對比較簡單,這裡就不詳細敘述了。
二、讀取與儲存
c# 實現
Bundle檔案與Bundlx檔案內容分析
Bundle檔案:
4位元組儲存長度 + 圖片Byte[]迴圈儲存
最大儲存16384(128*128)這樣的迴圈
可以看出參考文章中Bundle檔案只存圖片資料
*更正*
參考文章沒寫完全,10.2的Bundle前面有60個位元組是描述性文字
已嘗試將其全部置0,ArcServer依然可以正確顯示圖片
正確格式是:
60位元組描述頭 + (4位元組儲存長度 + 圖片Byte[] 迴圈儲存)
Bundlx檔案:
內容結構 16byte +81920byte + 16byte
其中2個16位元組 檔案頭資訊與尾資訊 屬於描述性註記 內容無用
經實踐證明故意修改ArcServer自帶建立的Bundlx檔案將前後16位描述性註記寫為空的時候ArcServer也不會去理會,依然可以正確讀取資料
中間的81920位元組檔案主體資訊 內容包含:
81920 = 5*128*128 5位元組儲存偏移量 有128*128個5位元組
所以每個Bundle檔案最多儲存128*128 = 16384個子檔案
另外要注意這個5位元組對應的JPG檔案是按列排序的不是按行
注意:由於Bundle檔案在檔案頭有60位元組的描述,所以Bundlx檔案預設第一個的偏移量一定是60
Bundlx檔案偏移量解釋
偏移量是描述一個圖片在Bundle檔案中的起點,偏移量由5位Buffer轉Int數值,讀取偏移量參考程式碼:
long offset =(long)(buffer[0]& 0xff) + (long)(buffer[1] & 0xff)
*256 + (long)(buffer[2]& 0xff) * 65536
+ (long)(buffer[3] &0xff) * 16777216
+ (long)(buffer[4]& 0xff) * 4294967296L;
這個表示方法表示偏移量是由低位往高位進行表示的
比如byte[5]{0,1,0,0,0}= 偏移 1 * 256 length
又如byte[5]{0,1,5,0,0}= 偏移 1 * 256 + 5 * 256 * 256 length
每一位表示 位值 * 256的位數開方將所有位相加得到位值
同理Bundle檔案的4位元組儲存長度也是這個表達方式
實質上就是一個256進位制與10進位制的轉換過程(因為一個Byte只能存256個不同的值,最大化利用)
Bundle/Bundlx檔案命名規則解釋
R+4位16進位制數字+C+4位16進位制數字組成
比如R0080C0180表示這個緊湊格式表達了
第129-256行384-512列
行列順序從指定的切片原點算(這個與普通切片相同)
現在對同一範圍同比例尺進行ArcServer緊湊切片與切片工具切片
發現Jpg起點與Bundle起點相差了一些
R2380C1280 轉十進位制表示 切片範圍左上角起點:
9088行 4736列
R23cdC12b6 轉十進位制表示 切片範圍左上角起點:
9165行 4790 列
出現差異是因為Jpg是由原點以Step為1遞增到實際位置,實際範圍的起點位置更為精確
而Bundle是由原點以Step為128遞增到實際位置(行列數總是128的倍數)
所以要解決這個問題,必須重算更新的Jpg檔案在所歸屬的Bundle檔案範圍,並將這些檔案註冊到這些範圍中,類似註冊空間索引,然後根據這些註冊資訊,將資料寫入Bundle
如何實現切片成果JPG轉Bundle功能
第一步 獲取更新圖片JPG檔案計算每個檔案的十進位制行列
第二步 獲取切片原點 計算包含此更新區域的Bundle檔案命名 以及十進位制的行列範圍
第三步 寫Bundle檔案
3.1匹配每個JPG檔案到Bundle範圍中
3.2寫入Jpg的大小並轉高低位儲存在頭4個Byte中(注意,如果圖片內容為空,依然要填寫4個0000補齊位數)
3.3寫入Byte[] 內容到Bundle檔案中
3.4記錄每個檔案的行列號和並記錄每個檔案的序號與長度
第四步 寫Bundlx檔案
根據寫Bundle產生的資訊,依次按列填寫每個檔案的偏移量
這裡個功能不僅可以從JPG轉換到Bundle 而且還可以對已有Bundle進行JPG更新
★轉換全程式碼★
使用 只需要指定 JPG的Path 與目標Bundle檔案的Path 即可 呼叫樣例程式碼
static void Main(string[] args) { PackageBundle BundleMaker = new PackageBundle(); BundleMaker._strJPGPath = @"C:\JPGList"; BundleMaker._strBundlePath = @"C:\_alllayers0"; BundleMaker.StartPackage(); }
class PackageBundle { public string _strJPGPath; public string _strBundlePath; private CommonTools _tool = new CommonTools(); public void StartPackage() { try { if (!new DirectoryInfo(_strBundlePath).Exists) new DirectoryInfo(_strBundlePath).Create(); //讀取一個Jpg資料夾,並將其組合為Bundle/Bundlx檔案 //第一步 依次遍歷每個L00 L01 級別 DirectoryInfo RootDir = new DirectoryInfo(_strJPGPath);//C:\Users\chop\Desktop\_alllayers DirectoryInfo[] LevelDirList = RootDir.GetDirectories(); foreach (DirectoryInfo LevelDir in LevelDirList) { int MinRow,MinColumn,MaxRow,MaxColumn; //獲取JPG內容的範圍 GetJpgNamingEnv(LevelDir.FullName,out MinRow,out MinColumn,out MaxRow,out MaxColumn);//C:\Users\chop\Desktop\_alllayers\L00 //獲取Bundle檔案 List<Bundle> BundleList = CreateBundleList(_strBundlePath + "\\" + LevelDir.Name, MinRow, MinColumn, MaxRow, MaxColumn); //匹配JPG檔案到Bundle檔案 AttachJPGToBundle(LevelDir.FullName,BundleList); //開始構造Bundle Bundlx檔案 CreateBundleFiles(BundleList); } } catch (Exception ex) { Console.WriteLine(ex.Message); } finally { Console.WriteLine("打包完畢"); Console.ReadKey(); } } private void GetJpgNamingEnv(string strJPGLevelPath,out int MinRow,out int MinColumn,out int MaxRow,out int MaxColumn) { MinRow = 0; MinColumn = 0; MaxRow = 0; MaxColumn = 0; //獲取所有資料夾 他們代表著行 DirectoryInfo[] RowDirList = _tool.GetAllDirectory(strJPGLevelPath); //獲取所有檔案 他們代表著列 FileInfo[] ColumnFileList = _tool.GetAllFiles(strJPGLevelPath); //翻譯每一個資料夾的名稱(行)到 十進位制 並且排序 List<int> RowList = new List<int>(); foreach (DirectoryInfo RowDir in RowDirList) { string Num16 = RowDir.Name.TrimStart('R'); string Num10 = _tool.To10(Num16); RowList.Add(int.Parse(Num10)); } RowList.Sort(); MinRow = RowList[0]; MaxRow = RowList[RowList.Count - 1]; List<int> ColumnList = new List<int>(); foreach (FileInfo Fileinfo in ColumnFileList) { string Num16 = Fileinfo.Name.TrimStart('C').TrimEnd(Fileinfo.Extension.ToCharArray()); string Num10 = _tool.To10(Num16); ColumnList.Add(int.Parse(Num10)); } ColumnList.Sort(); MinColumn = ColumnList[0]; MaxColumn = ColumnList[ColumnList.Count - 1]; } class Bundle { public string strFileName; public int MinRow; public int MinColumn; public int MaxRow; public int MaxColumn; public Bundle() { JPGList = new List<FileInfo>(); JPGIDList = new List<string>(); } public List<FileInfo> JPGList; public List<string> JPGIDList; } //根據JPG檔案最大最小的行列 建立Bundle集合 private List<Bundle> CreateBundleList(string strBundleLevelDir,int MinRow, int MinColumn, int MaxRow, int MaxColumn) { //Bundle檔案是行列為128個小JPG組成的 每行每列以128為Step從原點遞增 //這裡獲取一個範圍的Bundle 需要從最小的行列 開始計算起始Bundle //然後由起始Bundle遞增128 來增加Bundle數量 只要遞增的Bundle的起始行列 始終在MaxRow MaxColumn範圍內 那麼這個Bundle就是有效Bundle int Size = 128; int BundleRow = (MinRow / Size) * Size; int BundleColumn = (MinColumn / Size) * Size; //這裡建立起來的Bundle有可能是無效Bundle(因為更新區域不可能是規則的矩形),即不包含任何Jpg的Bundle,先不管,到後面判定時再刪除這些Bundle List<Bundle> BundleList = new List<Bundle>(); int BundleColumnMin = BundleColumn; if (!new DirectoryInfo(strBundleLevelDir).Exists) new DirectoryInfo(strBundleLevelDir).Create(); while(BundleRow < MaxRow) { while(BundleColumn < MaxColumn) { Bundle bundle = new Bundle(); bundle.MinRow = BundleRow; bundle.MaxRow = BundleRow + Size; bundle.MinColumn = BundleColumn; bundle.MaxColumn = BundleColumn + Size; bundle.strFileName = strBundleLevelDir + "\\" + "R" + _tool.To16(bundle.MinRow.ToString()) + "C" + _tool.To16(bundle.MinColumn.ToString()) + ".bundle"; BundleList.Add(bundle); BundleColumn = BundleColumn + Size; } BundleColumn = BundleColumnMin; BundleRow = BundleRow + Size; } return BundleList; } //將每個JPG歸屬到Bundle中 private void AttachJPGToBundle(string strJPGLevelPath, List<Bundle> BundleList) { //獲取所有檔案 FileInfo[] ColumnFileList = _tool.GetAllFiles(strJPGLevelPath); //獲取每個檔案所歸屬的行列 用十進位制表示 foreach (FileInfo colFile in ColumnFileList) { int dColumnNum = int.Parse(_tool.To10(colFile.Name.Substring(5, 4))); int dRowNum = int.Parse(_tool.To10(colFile.DirectoryName.Substring(colFile.DirectoryName.Length - 4, 4))); foreach (Bundle bundleFile in BundleList) { if ((bundleFile.MinRow <= dRowNum && dRowNum < bundleFile.MaxRow) && (bundleFile.MinColumn <= dColumnNum && dColumnNum < bundleFile.MaxColumn)) { bundleFile.JPGList.Add(colFile); bundleFile.JPGIDList.Add(dRowNum.ToString() + "-" + dColumnNum.ToString()); } } } //除去冗餘Bundle for (int i = 0; i < BundleList.Count; i++) { if (BundleList[i].JPGList.Count == 0) { BundleList.RemoveAt(i); i--; } } } private void CreateBundleFiles(List<Bundle> BundleList) { //從第一列128個開始往下寫,寫到N列 每次寫到算好對應的16進位制檔名並組成路徑與List已有對比 foreach (Bundle bundleFile in BundleList) { //第一種情況Bundle不存在,這時需要從Bundle檔案直接寫入 if (!new FileInfo(bundleFile.strFileName).Exists) { WriteBundleFile(bundleFile); } //第二種情況Bundle已經存在 這時需要從Bundlx檔案開始分析寫入 else { RewriteBundleFile(bundleFile); } } } private void WriteBundleFile(Bundle bundleFile) { FileStream FsBundle = new FileStream(bundleFile.strFileName, FileMode.CreateNew, FileAccess.Write); FileStream FsBundlx = new FileStream(bundleFile.strFileName.TrimEnd('e') + "x", FileMode.CreateNew, FileAccess.Write); FsBundle.Seek(0, SeekOrigin.Begin);//前60個描述內容統一置0 FsBundlx.Seek(0, SeekOrigin.Begin);//前16個描述內容統一置0 byte[] emptyBytes = new byte[60]; for (int ie = 0; ie < 60; ie++) emptyBytes[ie] = 88; FsBundle.Write(emptyBytes, 0, 60); FsBundlx.Write(emptyBytes, 0, 16);//Bundlx的起點是60 因為Bundle前60個是描述Byte int dOffset = 60; for (int i = 0; i < 128; i++) { for (int j = 0; j < 128; j++) { string strCurrentID = (bundleFile.MinRow + j).ToString() + "-" + (bundleFile.MinColumn + i).ToString(); int dJPGIndex = bundleFile.JPGIDList.IndexOf(strCurrentID); //寫空白圖片 if (dJPGIndex == -1) { FsBundle.Write(OffsetToByte4(0), 0, 4); FsBundlx.Write(OffsetToByte5(dOffset), 0, 5); dOffset = dOffset + 4; } //寫實際圖片 else { byte[] JPGBytes = ReadJPGByte(bundleFile.JPGList[dJPGIndex].FullName); FsBundle.Write(OffsetToByte4(JPGBytes.Length), 0, 4); FsBundle.Write(JPGBytes, 0, JPGBytes.Length); FsBundlx.Write(OffsetToByte5(dOffset), 0, 5); dOffset = dOffset + 4 + JPGBytes.Length; } } } FsBundlx.Write(emptyBytes, 0, 16);//別忘了新增後16個描述內容 後2個描述內容統一置0 FsBundle.Close(); FsBundlx.Close(); } private void RewriteBundleFile(Bundle bundleFile) { FileStream FsBundle = new FileStream(bundleFile.strFileName + ".Buffer", FileMode.CreateNew, FileAccess.Write); FileStream FsBundlx = new FileStream(bundleFile.strFileName.TrimEnd('e') + "x" + ".Buffer", FileMode.CreateNew, FileAccess.Write); FileStream FsBundleSource = new FileStream(bundleFile.strFileName, FileMode.Open, FileAccess.Read); FileStream FsBundlxSource = new FileStream(bundleFile.strFileName.TrimEnd('e') + "x", FileMode.Open, FileAccess.Read); FsBundle.Seek(0, SeekOrigin.Begin);//前60個描述內容統一置0 FsBundlx.Seek(0, SeekOrigin.Begin);//前16個描述內容統一置0 FsBundleSource.Seek(0, SeekOrigin.Begin); FsBundlxSource.Seek(0, SeekOrigin.Begin); byte[] emptyBytes = new byte[60]; for (int ie = 0; ie < 60; ie++) emptyBytes[ie] = 88; FsBundle.Write(emptyBytes, 0, 60); FsBundlx.Write(emptyBytes, 0, 16);//Bundlx的起點是60 因為Bundle前60個是描述Byte int dOffset = 60; for (int i = 0; i < 128; i++)//column { for (int j = 0; j < 128; j++)//row { string strCurrentID = (bundleFile.MinRow + j).ToString() + "-" + (bundleFile.MinColumn + i).ToString(); int dJPGIndex = bundleFile.JPGIDList.IndexOf(strCurrentID); //寫已有Bundle內容圖片 if (dJPGIndex == -1) { byte[] JPGBytes = GetBundleData(FsBundleSource, FsBundlxSource, j, i); if (JPGBytes.Length == 0) { FsBundle.Write(OffsetToByte4(0), 0, 4); FsBundlx.Write(OffsetToByte5(dOffset), 0, 5); dOffset = dOffset + 4; } else { FsBundle.Write(OffsetToByte4(JPGBytes.Length), 0, 4); FsBundle.Write(JPGBytes, 0, JPGBytes.Length); FsBundlx.Write(OffsetToByte5(dOffset), 0, 5); dOffset = dOffset + 4 + JPGBytes.Length; } } //寫實際圖片 else { byte[] JPGBytes = ReadJPGByte(bundleFile.JPGList[dJPGIndex].FullName); FsBundle.Write(OffsetToByte4(JPGBytes.Length), 0, 4); FsBundle.Write(JPGBytes, 0, JPGBytes.Length); FsBundlx.Write(OffsetToByte5(dOffset), 0, 5); dOffset = dOffset + 4 + JPGBytes.Length; } } } FsBundlx.Write(emptyBytes, 0, 16);//別忘了新增後16個描述內容 後2個描述內容統一置0 FsBundle.Close(); FsBundlx.Close(); FsBundleSource.Close(); FsBundlxSource.Close(); //全部寫入完畢後 還需要將Buffer檔案替換到實際命名的檔案; new FileInfo(bundleFile.strFileName).Delete(); new FileInfo(bundleFile.strFileName.TrimEnd('e') + "x").Delete(); new FileInfo(bundleFile.strFileName + ".Buffer").MoveTo(bundleFile.strFileName); new FileInfo(bundleFile.strFileName.TrimEnd('e') + "x" + ".Buffer").MoveTo(bundleFile.strFileName.TrimEnd('e') + "x"); ; } private byte[] GetBundleData(FileStream FsBundleSource, FileStream FsBundlxSource,int dRow,int dColomn) { int dIndex = dColomn * 128 + dRow; FsBundlxSource.Seek(16 + 5 * dIndex, SeekOrigin.Begin); byte[] buffer = new byte[5]; FsBundlxSource.Read(buffer, 0, 5); long offset = (long)(buffer[0] & 0xff) + (long)(buffer[1] & 0xff) * 256 + (long)(buffer[2] & 0xff) * 65536 + (long)(buffer[3] & 0xff) * 16777216 + (long)(buffer[4] & 0xff) * 4294967296L; FsBundleSource.Seek(offset, SeekOrigin.Begin); byte[] lengthBytes = new byte[4]; FsBundleSource.Read(lengthBytes, 0, 4); int length = (int)(lengthBytes[0] & 0xff) + (int)(lengthBytes[1] & 0xff) * 256 + (int)(lengthBytes[2] & 0xff) * 65536 + (int)(lengthBytes[3] & 0xff) * 16777216; byte[] result = new byte[length]; FsBundleSource.Read(result, 0, length); return result; } //10進位制轉256進位制 4位表示法 private byte[] OffsetToByte4(int dOffset) { return System.BitConverter.GetBytes(dOffset); } //10進位制轉256進位制 5位表示法 private byte[] OffsetToByte5(long dOffset) { byte[] byte8 = System.BitConverter.GetBytes(dOffset); byte[] byte5 = new byte[] { byte8[0], byte8[1], byte8[2], byte8[3], byte8[4] }; return byte5; } //讀取檔案位元組流到Byte[] private byte[] ReadJPGByte(string strJPGPath) { using (FileStream fsRead = new FileStream(strJPGPath, FileMode.Open, FileAccess.Read)) { byte[] Buffer = new byte[fsRead.Length]; fsRead.Read(Buffer, 0, Buffer.Length); return Buffer; } } }