database.ts 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714
  1. import { Nullable } from "../types";
  2. import { Tools } from "../Misc/tools";
  3. import { Logger } from "../Misc/logger";
  4. import { TGATools } from '../Misc/tga';
  5. import { Engine } from "../Engines/engine";
  6. import { IOfflineProvider } from "./IOfflineProvider";
  7. import { WebRequest } from '../Misc/webRequest';
  8. // Sets the default offline provider to Babylon.js
  9. Engine.OfflineProviderFactory = (urlToScene: string, callbackManifestChecked: (checked: boolean) => any, disableManifestCheck = false) => { return new Database(urlToScene, callbackManifestChecked, disableManifestCheck); };
  10. /**
  11. * Class used to enable access to IndexedDB
  12. * @see http://doc.babylonjs.com/how_to/caching_resources_in_indexeddb
  13. */
  14. export class Database implements IOfflineProvider {
  15. private _callbackManifestChecked: (check: boolean) => any;
  16. private _currentSceneUrl: string;
  17. private _db: Nullable<IDBDatabase>;
  18. private _enableSceneOffline: boolean;
  19. private _enableTexturesOffline: boolean;
  20. private _manifestVersionFound: number;
  21. private _mustUpdateRessources: boolean;
  22. private _hasReachedQuota: boolean;
  23. private _isSupported: boolean;
  24. // Handling various flavors of prefixed version of IndexedDB
  25. private _idbFactory = <IDBFactory>(typeof window !== "undefined" ? window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB : indexedDB);
  26. /** Gets a boolean indicating if the user agent supports blob storage (this value will be updated after creating the first Database object) */
  27. private static IsUASupportingBlobStorage = true;
  28. /**
  29. * Gets a boolean indicating if Database storate is enabled (off by default)
  30. */
  31. static IDBStorageEnabled = false;
  32. /**
  33. * Gets a boolean indicating if scene must be saved in the database
  34. */
  35. public get enableSceneOffline(): boolean {
  36. return this._enableSceneOffline;
  37. }
  38. /**
  39. * Gets a boolean indicating if textures must be saved in the database
  40. */
  41. public get enableTexturesOffline(): boolean {
  42. return this._enableTexturesOffline;
  43. }
  44. /**
  45. * Creates a new Database
  46. * @param urlToScene defines the url to load the scene
  47. * @param callbackManifestChecked defines the callback to use when manifest is checked
  48. * @param disableManifestCheck defines a boolean indicating that we want to skip the manifest validation (it will be considered validated and up to date)
  49. */
  50. constructor(urlToScene: string, callbackManifestChecked: (checked: boolean) => any, disableManifestCheck = false) {
  51. this._callbackManifestChecked = callbackManifestChecked;
  52. this._currentSceneUrl = Database._ReturnFullUrlLocation(urlToScene);
  53. this._db = null;
  54. this._enableSceneOffline = false;
  55. this._enableTexturesOffline = false;
  56. this._manifestVersionFound = 0;
  57. this._mustUpdateRessources = false;
  58. this._hasReachedQuota = false;
  59. if (!Database.IDBStorageEnabled) {
  60. this._callbackManifestChecked(true);
  61. } else {
  62. if (disableManifestCheck) {
  63. this._enableSceneOffline = true;
  64. this._enableTexturesOffline = true;
  65. this._manifestVersionFound = 1;
  66. Tools.SetImmediate(() => {
  67. this._callbackManifestChecked(true);
  68. });
  69. }
  70. else {
  71. this._checkManifestFile();
  72. }
  73. }
  74. }
  75. private static _ParseURL = (url: string) => {
  76. var a = document.createElement('a');
  77. a.href = url;
  78. var urlWithoutHash = url.substring(0, url.lastIndexOf("#"));
  79. var fileName = url.substring(urlWithoutHash.lastIndexOf("/") + 1, url.length);
  80. var absLocation = url.substring(0, url.indexOf(fileName, 0));
  81. return absLocation;
  82. }
  83. private static _ReturnFullUrlLocation = (url: string): string => {
  84. if (url.indexOf("http:/") === -1 && url.indexOf("https:/") === -1) {
  85. return (Database._ParseURL(window.location.href) + url);
  86. }
  87. else {
  88. return url;
  89. }
  90. }
  91. private _checkManifestFile() {
  92. var noManifestFile = () => {
  93. this._enableSceneOffline = false;
  94. this._enableTexturesOffline = false;
  95. this._callbackManifestChecked(false);
  96. };
  97. var timeStampUsed = false;
  98. var manifestURL = this._currentSceneUrl + ".manifest";
  99. var xhr = new WebRequest();
  100. if (navigator.onLine) {
  101. // Adding a timestamp to by-pass browsers' cache
  102. timeStampUsed = true;
  103. manifestURL = manifestURL + (manifestURL.match(/\?/) == null ? "?" : "&") + Date.now();
  104. }
  105. xhr.open("GET", manifestURL);
  106. xhr.addEventListener("load", () => {
  107. if (xhr.status === 200 || Database._ValidateXHRData(xhr, 1)) {
  108. try {
  109. var manifestFile = JSON.parse(xhr.response);
  110. this._enableSceneOffline = manifestFile.enableSceneOffline;
  111. this._enableTexturesOffline = manifestFile.enableTexturesOffline && Database.IsUASupportingBlobStorage;
  112. if (manifestFile.version && !isNaN(parseInt(manifestFile.version))) {
  113. this._manifestVersionFound = manifestFile.version;
  114. }
  115. if (this._callbackManifestChecked) {
  116. this._callbackManifestChecked(true);
  117. }
  118. }
  119. catch (ex) {
  120. noManifestFile();
  121. }
  122. }
  123. else {
  124. noManifestFile();
  125. }
  126. }, false);
  127. xhr.addEventListener("error", () => {
  128. if (timeStampUsed) {
  129. timeStampUsed = false;
  130. // Let's retry without the timeStamp
  131. // It could fail when coupled with HTML5 Offline API
  132. var retryManifestURL = this._currentSceneUrl + ".manifest";
  133. xhr.open("GET", retryManifestURL);
  134. xhr.send();
  135. }
  136. else {
  137. noManifestFile();
  138. }
  139. }, false);
  140. try {
  141. xhr.send();
  142. }
  143. catch (ex) {
  144. Logger.Error("Error on XHR send request.");
  145. this._callbackManifestChecked(false);
  146. }
  147. }
  148. /**
  149. * Open the database and make it available
  150. * @param successCallback defines the callback to call on success
  151. * @param errorCallback defines the callback to call on error
  152. */
  153. public open(successCallback: () => void, errorCallback: () => void): void {
  154. let handleError = () => {
  155. this._isSupported = false;
  156. if (errorCallback) { errorCallback(); }
  157. };
  158. if (!this._idbFactory || !(this._enableSceneOffline || this._enableTexturesOffline)) {
  159. // Your browser doesn't support IndexedDB
  160. this._isSupported = false;
  161. if (errorCallback) { errorCallback(); }
  162. }
  163. else {
  164. // If the DB hasn't been opened or created yet
  165. if (!this._db) {
  166. this._hasReachedQuota = false;
  167. this._isSupported = true;
  168. var request: IDBOpenDBRequest = this._idbFactory.open("babylonjs", 1);
  169. // Could occur if user is blocking the quota for the DB and/or doesn't grant access to IndexedDB
  170. request.onerror = () => {
  171. handleError();
  172. };
  173. // executes when a version change transaction cannot complete due to other active transactions
  174. request.onblocked = () => {
  175. Logger.Error("IDB request blocked. Please reload the page.");
  176. handleError();
  177. };
  178. // DB has been opened successfully
  179. request.onsuccess = () => {
  180. this._db = request.result;
  181. successCallback();
  182. };
  183. // Initialization of the DB. Creating Scenes & Textures stores
  184. request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
  185. this._db = (<any>(event.target)).result;
  186. if (this._db) {
  187. try {
  188. this._db.createObjectStore("scenes", { keyPath: "sceneUrl" });
  189. this._db.createObjectStore("versions", { keyPath: "sceneUrl" });
  190. this._db.createObjectStore("textures", { keyPath: "textureUrl" });
  191. }
  192. catch (ex) {
  193. Logger.Error("Error while creating object stores. Exception: " + ex.message);
  194. handleError();
  195. }
  196. }
  197. };
  198. }
  199. // DB has already been created and opened
  200. else {
  201. if (successCallback) { successCallback(); }
  202. }
  203. }
  204. }
  205. /**
  206. * Loads an image from the database
  207. * @param url defines the url to load from
  208. * @param image defines the target DOM image
  209. */
  210. public loadImage(url: string, image: HTMLImageElement) {
  211. var completeURL = Database._ReturnFullUrlLocation(url);
  212. var saveAndLoadImage = () => {
  213. if (!this._hasReachedQuota && this._db !== null) {
  214. // the texture is not yet in the DB, let's try to save it
  215. this._saveImageIntoDBAsync(completeURL, image);
  216. }
  217. // If the texture is not in the DB and we've reached the DB quota limit
  218. // let's load it directly from the web
  219. else {
  220. image.src = url;
  221. }
  222. };
  223. if (!this._mustUpdateRessources) {
  224. this._loadImageFromDBAsync(completeURL, image, saveAndLoadImage);
  225. }
  226. // First time we're download the images or update requested in the manifest file by a version change
  227. else {
  228. saveAndLoadImage();
  229. }
  230. }
  231. private _loadImageFromDBAsync(url: string, image: HTMLImageElement, notInDBCallback: () => any) {
  232. if (this._isSupported && this._db !== null) {
  233. var texture: any;
  234. var transaction: IDBTransaction = this._db.transaction(["textures"]);
  235. transaction.onabort = () => {
  236. image.src = url;
  237. };
  238. transaction.oncomplete = () => {
  239. var blobTextureURL: string;
  240. if (texture) {
  241. var URL = window.URL || window.webkitURL;
  242. blobTextureURL = URL.createObjectURL(texture.data);
  243. image.onerror = () => {
  244. Logger.Error("Error loading image from blob URL: " + blobTextureURL + " switching back to web url: " + url);
  245. image.src = url;
  246. };
  247. image.src = blobTextureURL;
  248. }
  249. else {
  250. notInDBCallback();
  251. }
  252. };
  253. var getRequest: IDBRequest = transaction.objectStore("textures").get(url);
  254. getRequest.onsuccess = (event) => {
  255. texture = (<any>(event.target)).result;
  256. };
  257. getRequest.onerror = () => {
  258. Logger.Error("Error loading texture " + url + " from DB.");
  259. image.src = url;
  260. };
  261. }
  262. else {
  263. Logger.Error("Error: IndexedDB not supported by your browser or BabylonJS Database is not open.");
  264. image.src = url;
  265. }
  266. }
  267. private _saveImageIntoDBAsync(url: string, image: HTMLImageElement) {
  268. if (this._isSupported) {
  269. // In case of error (type not supported or quota exceeded), we're at least sending back XHR data to allow texture loading later on
  270. var generateBlobUrl = () => {
  271. var blobTextureURL;
  272. if (blob) {
  273. var URL = window.URL || window.webkitURL;
  274. try {
  275. blobTextureURL = URL.createObjectURL(blob);
  276. }
  277. // Chrome is raising a type error if we're setting the oneTimeOnly parameter
  278. catch (ex) {
  279. blobTextureURL = URL.createObjectURL(blob);
  280. }
  281. }
  282. if (blobTextureURL) {
  283. image.src = blobTextureURL;
  284. }
  285. };
  286. if (Database.IsUASupportingBlobStorage) { // Create XHR
  287. var xhr = new WebRequest(),
  288. blob: Blob;
  289. xhr.open("GET", url);
  290. xhr.responseType = "blob";
  291. xhr.addEventListener("load", () => {
  292. if (xhr.status === 200 && this._db) {
  293. // Blob as response (XHR2)
  294. blob = xhr.response;
  295. var transaction = this._db.transaction(["textures"], "readwrite");
  296. // the transaction could abort because of a QuotaExceededError error
  297. transaction.onabort = (event) => {
  298. try {
  299. //backwards compatibility with ts 1.0, srcElement doesn't have an "error" according to ts 1.3
  300. let srcElement = <any>(event.srcElement || event.target);
  301. var error = srcElement.error;
  302. if (error && error.name === "QuotaExceededError") {
  303. this._hasReachedQuota = true;
  304. }
  305. }
  306. catch (ex) { }
  307. generateBlobUrl();
  308. };
  309. transaction.oncomplete = () => {
  310. generateBlobUrl();
  311. };
  312. var newTexture = { textureUrl: url, data: blob };
  313. try {
  314. // Put the blob into the dabase
  315. var addRequest = transaction.objectStore("textures").put(newTexture);
  316. addRequest.onsuccess = () => {
  317. };
  318. addRequest.onerror = () => {
  319. generateBlobUrl();
  320. };
  321. }
  322. catch (ex) {
  323. // "DataCloneError" generated by Chrome when you try to inject blob into IndexedDB
  324. if (ex.code === 25) {
  325. Database.IsUASupportingBlobStorage = false;
  326. this._enableTexturesOffline = false;
  327. }
  328. image.src = url;
  329. }
  330. }
  331. else {
  332. image.src = url;
  333. }
  334. }, false);
  335. xhr.addEventListener("error", () => {
  336. Logger.Error("Error in XHR request in BABYLON.Database.");
  337. image.src = url;
  338. }, false);
  339. xhr.send();
  340. }
  341. else {
  342. image.src = url;
  343. }
  344. }
  345. else {
  346. Logger.Error("Error: IndexedDB not supported by your browser or BabylonJS Database is not open.");
  347. image.src = url;
  348. }
  349. }
  350. private _checkVersionFromDB(url: string, versionLoaded: (version: number) => void) {
  351. var updateVersion = () => {
  352. // the version is not yet in the DB or we need to update it
  353. this._saveVersionIntoDBAsync(url, versionLoaded);
  354. };
  355. this._loadVersionFromDBAsync(url, versionLoaded, updateVersion);
  356. }
  357. private _loadVersionFromDBAsync(url: string, callback: (version: number) => void, updateInDBCallback: () => void) {
  358. if (this._isSupported && this._db) {
  359. var version: any;
  360. try {
  361. var transaction = this._db.transaction(["versions"]);
  362. transaction.oncomplete = () => {
  363. if (version) {
  364. // If the version in the JSON file is different from the version in DB
  365. if (this._manifestVersionFound !== version.data) {
  366. this._mustUpdateRessources = true;
  367. updateInDBCallback();
  368. }
  369. else {
  370. callback(version.data);
  371. }
  372. }
  373. // version was not found in DB
  374. else {
  375. this._mustUpdateRessources = true;
  376. updateInDBCallback();
  377. }
  378. };
  379. transaction.onabort = () => {
  380. callback(-1);
  381. };
  382. var getRequest = transaction.objectStore("versions").get(url);
  383. getRequest.onsuccess = (event) => {
  384. version = (<any>(event.target)).result;
  385. };
  386. getRequest.onerror = () => {
  387. Logger.Error("Error loading version for scene " + url + " from DB.");
  388. callback(-1);
  389. };
  390. }
  391. catch (ex) {
  392. Logger.Error("Error while accessing 'versions' object store (READ OP). Exception: " + ex.message);
  393. callback(-1);
  394. }
  395. }
  396. else {
  397. Logger.Error("Error: IndexedDB not supported by your browser or BabylonJS Database is not open.");
  398. callback(-1);
  399. }
  400. }
  401. private _saveVersionIntoDBAsync(url: string, callback: (version: number) => void) {
  402. if (this._isSupported && !this._hasReachedQuota && this._db) {
  403. try {
  404. // Open a transaction to the database
  405. var transaction = this._db.transaction(["versions"], "readwrite");
  406. // the transaction could abort because of a QuotaExceededError error
  407. transaction.onabort = (event) => {
  408. try {//backwards compatibility with ts 1.0, srcElement doesn't have an "error" according to ts 1.3
  409. var error = (<any>event.srcElement)['error'];
  410. if (error && error.name === "QuotaExceededError") {
  411. this._hasReachedQuota = true;
  412. }
  413. }
  414. catch (ex) { }
  415. callback(-1);
  416. };
  417. transaction.oncomplete = () => {
  418. callback(this._manifestVersionFound);
  419. };
  420. var newVersion = { sceneUrl: url, data: this._manifestVersionFound };
  421. // Put the scene into the database
  422. var addRequest = transaction.objectStore("versions").put(newVersion);
  423. addRequest.onsuccess = () => {
  424. };
  425. addRequest.onerror = () => {
  426. Logger.Error("Error in DB add version request in BABYLON.Database.");
  427. };
  428. }
  429. catch (ex) {
  430. Logger.Error("Error while accessing 'versions' object store (WRITE OP). Exception: " + ex.message);
  431. callback(-1);
  432. }
  433. }
  434. else {
  435. callback(-1);
  436. }
  437. }
  438. /**
  439. * Loads a file from database
  440. * @param url defines the URL to load from
  441. * @param sceneLoaded defines a callback to call on success
  442. * @param progressCallBack defines a callback to call when progress changed
  443. * @param errorCallback defines a callback to call on error
  444. * @param useArrayBuffer defines a boolean to use array buffer instead of text string
  445. */
  446. public loadFile(url: string, sceneLoaded: (data: any) => void, progressCallBack?: (data: any) => void, errorCallback?: () => void, useArrayBuffer?: boolean): void {
  447. var completeUrl = Database._ReturnFullUrlLocation(url);
  448. var saveAndLoadFile = () => {
  449. // the scene is not yet in the DB, let's try to save it
  450. this._saveFileAsync(completeUrl, sceneLoaded, progressCallBack, useArrayBuffer, errorCallback);
  451. };
  452. this._checkVersionFromDB(completeUrl, (version) => {
  453. if (version !== -1) {
  454. if (!this._mustUpdateRessources) {
  455. this._loadFileAsync(completeUrl, sceneLoaded, saveAndLoadFile);
  456. }
  457. else {
  458. this._saveFileAsync(completeUrl, sceneLoaded, progressCallBack, useArrayBuffer, errorCallback);
  459. }
  460. }
  461. else {
  462. if (errorCallback) {
  463. errorCallback();
  464. }
  465. }
  466. });
  467. }
  468. private _loadFileAsync(url: string, callback: (data?: any) => void, notInDBCallback: () => void) {
  469. if (this._isSupported && this._db) {
  470. var targetStore: string;
  471. if (url.indexOf(".babylon") !== -1) {
  472. targetStore = "scenes";
  473. }
  474. else {
  475. targetStore = "textures";
  476. }
  477. var file: any;
  478. var transaction = this._db.transaction([targetStore]);
  479. transaction.oncomplete = () => {
  480. if (file) {
  481. callback(file.data);
  482. }
  483. // file was not found in DB
  484. else {
  485. notInDBCallback();
  486. }
  487. };
  488. transaction.onabort = () => {
  489. notInDBCallback();
  490. };
  491. var getRequest = transaction.objectStore(targetStore).get(url);
  492. getRequest.onsuccess = (event) => {
  493. file = (<any>(event.target)).result;
  494. };
  495. getRequest.onerror = () => {
  496. Logger.Error("Error loading file " + url + " from DB.");
  497. notInDBCallback();
  498. };
  499. }
  500. else {
  501. Logger.Error("Error: IndexedDB not supported by your browser or BabylonJS Database is not open.");
  502. callback();
  503. }
  504. }
  505. private _saveFileAsync(url: string, callback: (data?: any) => void, progressCallback?: (this: XMLHttpRequestEventTarget, ev: ProgressEvent) => any, useArrayBuffer?: boolean, errorCallback?: (data?: any) => void) {
  506. if (this._isSupported) {
  507. var targetStore: string;
  508. if (url.indexOf(".babylon") !== -1) {
  509. targetStore = "scenes";
  510. }
  511. else {
  512. targetStore = "textures";
  513. }
  514. // Create XHR
  515. var xhr = new WebRequest();
  516. var fileData: any;
  517. xhr.open("GET", url + "?" + Date.now());
  518. if (useArrayBuffer) {
  519. xhr.responseType = "arraybuffer";
  520. }
  521. if (progressCallback) {
  522. xhr.onprogress = progressCallback;
  523. }
  524. xhr.addEventListener("load", () => {
  525. if (xhr.status === 200 || (xhr.status < 400 && Database._ValidateXHRData(xhr, !useArrayBuffer ? 1 : 6))) {
  526. // Blob as response (XHR2)
  527. fileData = !useArrayBuffer ? xhr.responseText : xhr.response;
  528. if (!this._hasReachedQuota && this._db) {
  529. // Open a transaction to the database
  530. var transaction = this._db.transaction([targetStore], "readwrite");
  531. // the transaction could abort because of a QuotaExceededError error
  532. transaction.onabort = (event) => {
  533. try {
  534. //backwards compatibility with ts 1.0, srcElement doesn't have an "error" according to ts 1.3
  535. var error = (<any>event.srcElement)['error'];
  536. if (error && error.name === "QuotaExceededError") {
  537. this._hasReachedQuota = true;
  538. }
  539. }
  540. catch (ex) { }
  541. callback(fileData);
  542. };
  543. transaction.oncomplete = () => {
  544. callback(fileData);
  545. };
  546. var newFile;
  547. if (targetStore === "scenes") {
  548. newFile = { sceneUrl: url, data: fileData, version: this._manifestVersionFound };
  549. }
  550. else {
  551. newFile = { textureUrl: url, data: fileData };
  552. }
  553. try {
  554. // Put the scene into the database
  555. var addRequest = transaction.objectStore(targetStore).put(newFile);
  556. addRequest.onsuccess = () => {
  557. };
  558. addRequest.onerror = () => {
  559. Logger.Error("Error in DB add file request in BABYLON.Database.");
  560. };
  561. }
  562. catch (ex) {
  563. callback(fileData);
  564. }
  565. }
  566. else {
  567. callback(fileData);
  568. }
  569. }
  570. else {
  571. if (xhr.status >= 400 && errorCallback) {
  572. errorCallback(xhr);
  573. } else {
  574. callback();
  575. }
  576. }
  577. }, false);
  578. xhr.addEventListener("error", () => {
  579. Logger.Error("error on XHR request.");
  580. callback();
  581. }, false);
  582. xhr.send();
  583. }
  584. else {
  585. Logger.Error("Error: IndexedDB not supported by your browser or Babylon.js Database is not open.");
  586. callback();
  587. }
  588. }
  589. /**
  590. * Validates if xhr data is correct
  591. * @param xhr defines the request to validate
  592. * @param dataType defines the expected data type
  593. * @returns true if data is correct
  594. */
  595. private static _ValidateXHRData(xhr: WebRequest, dataType = 7): boolean {
  596. // 1 for text (.babylon, manifest and shaders), 2 for TGA, 4 for DDS, 7 for all
  597. try {
  598. if (dataType & 1) {
  599. if (xhr.responseText && xhr.responseText.length > 0) {
  600. return true;
  601. } else if (dataType === 1) {
  602. return false;
  603. }
  604. }
  605. if (dataType & 2) {
  606. // Check header width and height since there is no "TGA" magic number
  607. var tgaHeader = TGATools.GetTGAHeader(xhr.response);
  608. if (tgaHeader.width && tgaHeader.height && tgaHeader.width > 0 && tgaHeader.height > 0) {
  609. return true;
  610. } else if (dataType === 2) {
  611. return false;
  612. }
  613. }
  614. if (dataType & 4) {
  615. // Check for the "DDS" magic number
  616. var ddsHeader = new Uint8Array(xhr.response, 0, 3);
  617. if (ddsHeader[0] === 68 && ddsHeader[1] === 68 && ddsHeader[2] === 83) {
  618. return true;
  619. } else {
  620. return false;
  621. }
  622. }
  623. } catch (e) {
  624. // Global protection
  625. }
  626. return false;
  627. }
  628. }