1/* juxtapose - v1.1.2 - 2015-07-16 2 * Copyright (c) 2015 Alex Duner and Northwestern University Knight Lab 3 */ 4 5(function (document, window) { 6 7 var juxtapose = { 8 sliders: [], 9 OPTIMIZATION_ACCEPTED: 1, 10 OPTIMIZATION_WAS_CONSTRAINED: 2 11 }; 12 13 var flickr_key = "d90fc2d1f4acc584e08b8eaea5bf4d6c"; 14 var FLICKR_SIZE_PREFERENCES = ['Large', 'Medium']; 15 16 function Graphic(properties, slider) { 17 var self = this; 18 this.image = new Image(); 19 20 this.loaded = false; 21 this.image.onload = function() { 22 self.loaded = true; 23 slider._onLoaded(); 24 }; 25 26 this.image.src = properties.src; 27 this.image.alt = properties.alt || ''; 28 this.label = properties.label || false; 29 this.credit = properties.credit || false; 30 } 31 32 function FlickrGraphic(properties, slider) { 33 var self = this; 34 this.image = new Image(); 35 36 this.loaded = false; 37 this.image.onload = function() { 38 self.loaded = true; 39 slider._onLoaded(); 40 }; 41 42 this.flickrID = this.getFlickrID(properties.src); 43 this.callFlickrAPI(this.flickrID, self); 44 45 this.label = properties.label || false; 46 this.credit = properties.credit || false; 47 } 48 49 FlickrGraphic.prototype = { 50 getFlickrID: function(url) { 51 if (url.match(/flic.kr\/.+/i)) { 52 var encoded = url.split('/').slice(-1)[0]; 53 return base58Decode(encoded); 54 } 55 var idx = url.indexOf("flickr.com/photos/"); 56 var pos = idx + "flickr.com/photos/".length; 57 var photo_info = url.substr(pos); 58 if (photo_info.indexOf('/') == -1) return null; 59 if (photo_info.indexOf('/') === 0) photo_info = photo_info.substr(1); 60 id = photo_info.split("/")[1]; 61 return id; 62 }, 63 64 callFlickrAPI: function(id, self) { 65 var url = 'https://api.flickr.com/services/rest/?method=flickr.photos.getSizes' + 66 '&api_key=' + flickr_key + 67 '&photo_id=' + id + '&format=json&nojsoncallback=1'; 68 69 var request = new XMLHttpRequest(); 70 request.open('GET', url, true); 71 request.onload = function() { 72 if (request.status >= 200 && request.status < 400){ 73 data = JSON.parse(request.responseText); 74 var flickr_url = self.bestFlickrUrl(data.sizes.size); 75 self.setFlickrImage(flickr_url); 76 } else { 77 console.error("There was an error getting the picture from Flickr"); 78 } 79 }; 80 request.onerror = function() { 81 console.error("There was an error getting the picture from Flickr"); 82 }; 83 request.send(); 84 }, 85 86 setFlickrImage: function(src) { 87 this.image.src = src; 88 }, 89 90 bestFlickrUrl: function(ary) { 91 var dict = {}; 92 for (var i = 0; i < ary.length; i++) { 93 dict[ary[i].label] = ary[i].source; 94 } 95 for (var j = 0; j < FLICKR_SIZE_PREFERENCES.length; j++) { 96 if (FLICKR_SIZE_PREFERENCES[j] in dict) { 97 return dict[FLICKR_SIZE_PREFERENCES[j]]; 98 } 99 } 100 return ary[0].source; 101 } 102 }; 103 104 function getNaturalDimensions(DOMelement) { 105 if (DOMelement.naturalWidth && DOMelement.naturalHeight) { 106 return {width: DOMelement.naturalWidth, height: DOMelement.naturalHeight}; 107 } 108 // http://www.jacklmoore.com/notes/naturalwidth-and-naturalheight-in-ie/ 109 var img = new Image(); 110 img.src = DOMelement.src; 111 return {width: img.width, height: img.height}; 112 } 113 114 function getImageDimensions(img) { 115 var dimensions = { 116 width: getNaturalDimensions(img).width, 117 height: getNaturalDimensions(img).height, 118 aspect: function() { return (this.width / this.height); } 119 }; 120 return dimensions; 121 } 122 123 function addClass(element, c) { 124 if (element.classList) { 125 element.classList.add(c); 126 } else { 127 element.className += " " + c; 128 } 129 } 130 131 function removeClass(element, c) { 132 element.className = element.className.replace(/(\S+)\s*/g, function (w, match) { 133 if (match === c) { 134 return ''; 135 } 136 return w; 137 }).replace(/^\s+/, ''); 138 } 139 140 function setText(element, text) { 141 if (document.body.textContent) { 142 element.textContent = text; 143 } else { 144 element.innerText = text; 145 } 146 } 147 148 function getComputedWidthAndHeight(element) { 149 if (window.getComputedStyle) { 150 return { 151 width: parseInt(getComputedStyle(element).width, 10), 152 height: parseInt(getComputedStyle(element).height, 10) 153 }; 154 } else { 155 w = element.getBoundingClientRect().right - element.getBoundingClientRect().left; 156 h = element.getBoundingClientRect().bottom - element.getBoundingClientRect().top; 157 return { 158 width: parseInt(w, 10) || 0, 159 height: parseInt(h, 10) || 0 160 }; 161 } 162 } 163 164 function viewport() { 165 var e = window, a = 'inner'; 166 if ( !( 'innerWidth' in window ) ) { 167 a = 'client'; 168 e = document.documentElement || document.body; 169 } 170 return { width : e[ a+'Width' ] , height : e[ a+'Height' ] } 171 } 172 173 function getPageX(e) { 174 var pageX; 175 if (e.pageX) { 176 pageX = e.pageX; 177 } else if (e.touches) { 178 pageX = e.touches[0].pageX; 179 } else { 180 pageX = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; 181 } 182 return pageX; 183 } 184 185 function getPageY(e) { 186 var pageY; 187 if (e.pageY) { 188 pageY = e.pageY; 189 } else if (e.touches) { 190 pageY = e.touches[0].pageY; 191 } else { 192 pageY = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; 193 } 194 return pageY; 195 } 196 197 function checkFlickr(url) { 198 if (url.match(/flic.kr\/.+/i)) { 199 return true; 200 } 201 var idx = url.indexOf("flickr.com/photos/"); 202 if (idx == -1) { 203 return false;
204 } else { 205 return true; 206 } 207 } 208 209 function base58Decode(encoded) { 210 var alphabet = '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ', 211 base = alphabet.length; 212 if (typeof encoded !== 'string') { 213 throw '"base58Decode" only accepts strings.'; 214 } 215 var decoded = 0; 216 while (encoded) { 217 var alphabetPosition = alphabet.indexOf(encoded[0]); 218 if (alphabetPosition < 0) { 219 throw '"base58Decode" can\'t find "' + encoded[0] + '" in the alphabet: "' + alphabet + '"'; 220 } 221 var powerOf = encoded.length - 1; 222 decoded += alphabetPosition * (Math.pow(base, powerOf)); 223 encoded = encoded.substring(1); 224 } 225 return decoded.toString(); 226 } 227 228 function getLeftPercent(slider, input) { 229 if (typeof(input) === "string" || typeof(input) === "number") { 230 leftPercent = parseInt(input, 10); 231 } else { 232 var sliderRect = slider.getBoundingClientRect(); 233 var offset = { 234 top: sliderRect.top + document.body.scrollTop + document.documentElement.scrollTop, 235 left: sliderRect.left + document.body.scrollLeft + document.documentElement.scrollLeft 236 }; 237 var width = slider.offsetWidth; 238 var pageX = getPageX(input); 239 var relativeX = pageX - offset.left; 240 leftPercent = (relativeX / width) * 100; 241 } 242 return leftPercent; 243 } 244 245 function getTopPercent(slider, input) { 246 if (typeof(input) === "string" || typeof(input) === "number") { 247 topPercent = parseInt(input, 10); 248 } else { 249 var sliderRect = slider.getBoundingClientRect(); 250 var offset = { 251 top: sliderRect.top + document.body.scrollTop + document.documentElement.scrollTop, 252 left: sliderRect.left + document.body.scrollLeft + document.documentElement.scrollLeft 253 }; 254 var width = slider.offsetHeight; 255 var pageY = getPageY(input); 256 var relativeY = pageY - offset.top; 257 topPercent = (relativeY / width) * 100; 258 } 259 return topPercent; 260 } 261 262 // values of BOOLEAN_OPTIONS are ignored. just used for 'in' test on keys 263 var BOOLEAN_OPTIONS = {'animate': true, 'showLabels': true, 'showCredits': true, 'makeResponsive': true }; 264 function interpret_boolean(x) { 265 if (typeof(x) != 'string') { 266 return Boolean(x); 267 } 268 return !(x === 'false' || x === ''); 269 } 270 271 function JXSlider(selector, images, options) { 272 273 this.selector = selector; 274 275 var i; 276 this.options = { // new options must have default values set here. 277 animate: true, 278 showLabels: true, 279 showCredits: true, 280 makeResponsive: true, 281 startingPosition: "50%", 282 mode: 'horizontal', 283 callback: null // pass a callback function if you like 284 }; 285 286 for (i in this.options) { 287 if(i in options) { 288 if (i in BOOLEAN_OPTIONS) { 289 this.options[i] = interpret_boolean(options[i]); 290 } else { 291 this.options[i] = options[i]; 292 } 293 } 294 } 295 296 if (images.length == 2) { 297 298 if(checkFlickr(images[0].src)) { 299 this.imgBefore = new FlickrGraphic(images[0], this); 300 } else { 301 this.imgBefore = new Graphic(images[0], this); 302 } 303 304 if(checkFlickr(images[1].src)) { 305 this.imgAfter = new FlickrGraphic(images[1], this); 306 } else { 307 this.imgAfter = new Graphic(images[1], this); 308 } 309 310 } else { 311 console.warn("The images parameter takes two Image objects."); 312 } 313 314 if (this.imgBefore.credit || this.imgAfter.credit) { 315 this.options.showCredits = true; 316 } else { 317 this.options.showCredits = false; 318 } 319 } 320 321 JXSlider.prototype = { 322 323 updateSlider: function(input, animate) { 324 var leftPercent, rightPercent; 325 326 if (this.options.mode === "vertical") { 327 leftPercent = getTopPercent(this.slider, input); 328 } else { 329 leftPercent = getLeftPercent(this.slider, input); 330 } 331 332 leftPercent = leftPercent.toFixed(2) + "%"; 333 leftPercentNum = parseFloat(leftPercent); 334 rightPercent = (100 - leftPercentNum) + "%"; 335 336 if (leftPercentNum > 0 && leftPercentNum < 100) { 337 removeClass(this.handle, 'transition'); 338 removeClass(this.rightImage, 'transition'); 339 removeClass(this.leftImage, 'transition'); 340 341 if (this.options.animate && animate) { 342 addClass(this.handle, 'transition'); 343 addClass(this.leftImage, 'transition'); 344 addClass(this.rightImage, 'transition'); 345 } 346 347 if (this.options.mode === "vertical") { 348 this.handle.style.top = leftPercent; 349 this.leftImage.style.height = leftPercent; 350 this.rightImage.style.height = rightPercent; 351 } else { 352 this.handle.style.left = leftPercent; 353 this.leftImage.style.width = leftPercent; 354 this.rightImage.style.width = rightPercent; 355 } 356 this.sliderPosition = leftPercent; 357 } 358 }, 359 360 getPosition: function() { 361 return this.sliderPosition; 362 }, 363 364 displayLabel: function(element, labelText) { 365 label = document.createElement("div"); 366 label.className = 'jx-label'; 367 label.setAttribute('tabindex', 0); //put the controller in the natural tab order of the document 368 369 setText(label, labelText); 370 element.appendChild(label); 371 }, 372 373 displayCredits: function() { 374 credit = document.createElement("div"); 375 credit.className = "jx-credit"; 376 377 text = "<em>Photo Credits:</em>"; 378 if (this.imgBefore.credit) { text += " <em>Before</em> " + this.imgBefore.credit; } 379 if (this.imgAfter.credit) { text += " <em>After</em> " + this.imgAfter.credit; } 380 381 credit.innerHTML = text; 382 383 this.wrapper.appendChild(credit); 384 }, 385 386 setStartingPosition: function(s) { 387 this.options.startingPosition = s; 388 }, 389 390 checkImages: function() { 391 if (getImageDimensions(this.imgBefore.image).aspect() == 392 getImageDimensions(this.imgAfter.image).aspect()) { 393 return true; 394 } else { 395 return false;
396 } 397 }, 398 399 calculateDims: function(width, height){ 400 var ratio = getImageDimensions(this.imgBefore.image).aspect(); 401 if (width) { 402 height = width / ratio; 403 } else if (height) { 404 width = height * ratio; 405 } 406 return { 407 width: width, 408 height: height, 409 ratio: ratio 410 }; 411 }, 412 413 responsivizeIframe: function(dims){ 414 //Check the slider dimensions against the iframe (window) dimensions 415 if (dims.height < window.innerHeight){ 416 //If the aspect ratio is greater than 1, imgs are landscape, so letterbox top and bottom 417 if (dims.ratio >= 1){ 418 this.wrapper.style.paddingTop = parseInt((window.innerHeight - dims.height) / 2) + "px"; 419 } 420 } else if (dims.height > window.innerHeight) { 421 /* If the image is too tall for the window, which happens at 100% width on large screens, 422 * force dimension recalculation based on height instead of width */ 423 dims = this.calculateDims(0, window.innerHeight); 424 this.wrapper.style.paddingLeft = parseInt((window.innerWidth - dims.width) / 2) + "px"; 425 } 426 if (this.options.showCredits) { 427 // accommodate the credits box within the iframe 428 dims.height -= 13; 429 } 430 return dims; 431 }, 432 433 setWrapperDimensions: function() { 434 var wrapperWidth = getComputedWidthAndHeight(this.wrapper).width; 435 var wrapperHeight = getComputedWidthAndHeight(this.wrapper).height; 436 var dims = this.calculateDims(wrapperWidth, wrapperHeight); 437 // if window is in iframe, make sure images don't overflow boundaries 438 if (window.location !== window.parent.location && !this.options.makeResponsive) { 439 dims = this.responsivizeIframe(dims); 440 } 441 442 this.wrapper.style.height = parseInt(dims.height) + "px"; 443 this.wrapper.style.width = parseInt(dims.width) + "px"; 444 }, 445 446 optimizeWrapper: function(maxWidth){ 447 var result = juxtapose.OPTIMIZATION_ACCEPTED; 448 if ((this.imgBefore.image.naturalWidth >= maxWidth) && (this.imgAfter.image.naturalWidth >= maxWidth)) { 449 this.wrapper.style.width = maxWidth + "px"; 450 result = juxtapose.OPTIMIZATION_WAS_CONSTRAINED; 451 } else if (this.imgAfter.image.naturalWidth < maxWidth) { 452 this.wrapper.style.width = this.imgAfter.image.naturalWidth + "px"; 453 } else { 454 this.wrapper.style.width = this.imgBefore.image.naturalWidth + "px"; 455 } 456 this.setWrapperDimensions(); 457 return result; 458 }, 459 460 _onLoaded: function() { 461 462 if (this.imgBefore && this.imgBefore.loaded === true && 463 this.imgAfter && this.imgAfter.loaded === true) { 464 465 this.wrapper = document.querySelector(this.selector); 466 addClass(this.wrapper, 'juxtapose'); 467 468 this.wrapper.style.width = getNaturalDimensions(this.imgBefore.image).width; 469 this.setWrapperDimensions(); 470 471 this.slider = document.createElement("div"); 472 this.slider.className = 'jx-slider'; 473 this.wrapper.appendChild(this.slider); 474 475 if (this.options.mode != "horizontal") { 476 addClass(this.slider, this.options.mode); 477 } 478 479 this.handle = document.createElement("div"); 480 this.handle.className = 'jx-handle'; 481 482 this.rightImage = document.createElement("div"); 483 this.rightImage.className = 'jx-image jx-right'; 484 this.rightImage.appendChild(this.imgAfter.image); 485 486 487 this.leftImage = document.createElement("div"); 488 this.leftImage.className = 'jx-image jx-left'; 489 this.leftImage.appendChild(this.imgBefore.image); 490 491 this.slider.appendChild(this.handle); 492 this.slider.appendChild(this.leftImage); 493 this.slider.appendChild(this.rightImage); 494 495 this.leftArrow = document.createElement("div"); 496 this.rightArrow = document.createElement("div"); 497 this.control = document.createElement("div"); 498 this.controller = document.createElement("div"); 499 500 this.leftArrow.className = 'jx-arrow jx-left'; 501 this.rightArrow.className = 'jx-arrow jx-right'; 502 this.control.className = 'jx-control'; 503 this.controller.className = 'jx-controller'; 504 505 this.controller.setAttribute('tabindex', 0); //put the controller in the natural tab order of the document 506 this.controller.setAttribute('role', 'slider'); 507 this.controller.setAttribute('aria-valuenow', 50); 508 this.controller.setAttribute('aria-valuemin', 0); 509 this.controller.setAttribute('aria-valuemax', 100); 510 511 this.handle.appendChild(this.leftArrow); 512 this.handle.appendChild(this.control); 513 this.handle.appendChild(this.rightArrow); 514 this.control.appendChild(this.controller); 515 516 this._init(); 517 } 518 }, 519 520 _init: function() { 521 522 if (this.checkImages() === false) { 523 console.warn(this, "Check that the two images have the same aspect ratio for the slider to work correctly."); 524 } 525 526 this.updateSlider(this.options.startingPosition, false); 527 528 if (this.options.showLabels === true) { 529 if (this.imgBefore.label) { this.displayLabel(this.leftImage, this.imgBefore.label); } 530 if (this.imgAfter.label) { this.displayLabel(this.rightImage, this.imgAfter.label); } 531 } 532 533 if (this.options.showCredits === true) { 534 this.displayCredits(); 535 } 536 537 var self = this; 538 window.addEventListener("resize", function() { 539 self.setWrapperDimensions(); 540 }); 541 542 543 // Set up Javascript Events 544 // On mousedown, call updateSlider then set animate to false 545 // (if animate is true, adds css transition when updating). 546 547 this.slider.addEventListener("mousedown", function(e) { 548 e = e || window.event; 549 e.preventDefault(); 550 self.updateSlider(e, true); 551 animate = true; 552 553 this.addEventListener("mousemove", function(e) { 554 e = e || window.event; 555 e.preventDefault(); 556 if (animate) { self.updateSlider(e, false); } 557 }); 558 559 this.addEventListener('mouseup', function(e) { 560 e = e || window.event; 561 e.preventDefault(); 562 e.stopPropagation(); 563 this.removeEventListener('mouseup', arguments.callee); 564 animate = false;
565 }); 566 }); 567 568 this.slider.addEventListener("touchstart", function(e) { 569 e = e || window.event; 570 e.preventDefault(); 571 e.stopPropagation(); 572 self.updateSlider(e, true); 573 574 this.addEventListener("touchmove", function(e) { 575 e = e || window.event; 576 e.preventDefault(); 577 e.stopPropagation(); 578 self.updateSlider(event, false); 579 }); 580 581 }); 582 583 /* keyboard accessibility */ 584 585 this.handle.addEventListener("keydown", function (e) { 586 e = e || window.event; 587 var key = e.which || e.keyCode; 588 var ariaValue = parseFloat(this.style.left); 589 590 //move jx-controller left 591 if (key == 37) { 592 ariaValue = ariaValue - 1; 593 var leftStart = parseFloat(this.style.left) - 1; 594 self.updateSlider(leftStart, false); 595 self.controller.setAttribute('aria-valuenow', ariaValue); 596 } 597 598 //move jx-controller right 599 if (key == 39) { 600 ariaValue = ariaValue + 1; 601 var rightStart = parseFloat(this.style.left) + 1; 602 self.updateSlider(rightStart, false); 603 self.controller.setAttribute('aria-valuenow', ariaValue); 604 } 605 }); 606 607 //toggle right-hand image visibility 608 this.leftImage.addEventListener("keydown", function (event) { 609 var key = event.which || event.keyCode; 610 if ((key == 13) || (key ==32)) { 611 self.updateSlider("90%", true); 612 self.controller.setAttribute('aria-valuenow', 90); 613 } 614 }); 615 616 //toggle left-hand image visibility 617 this.rightImage.addEventListener("keydown", function (event) { 618 var key = event.which || event.keyCode; 619 if ((key == 13) || (key ==32)) { 620 self.updateSlider("10%", true); 621 self.controller.setAttribute('aria-valuenow', 10); 622 } 623 }); 624 625 juxtapose.sliders.push(this); 626 627 if (this.options.callback && typeof(this.options.callback) == 'function') { 628 this.options.callback(this); 629 } 630 } 631 632 }; 633 634 /* 635 Given an element that is configured with the proper data elements, make a slider out of it. 636 Normally this will just be used by scanPage. 637 */ 638 juxtapose.makeSlider = function (element, idx) { 639 if (typeof idx == 'undefined') { 640 idx = juxtapose.sliders.length; // not super threadsafe... 641 } 642 643 var w = element; 644 645 var images = w.querySelectorAll('img'); 646 647 var options = {}; 648 // don't set empty string into options, that's a false false. 649 if (w.getAttribute('data-animate')) { 650 options.animate = w.getAttribute('data-animate'); 651 } 652 if (w.getAttribute('data-showlabels')) { 653 options.showLabels = w.getAttribute('data-showlabels'); 654 } 655 if (w.getAttribute('data-showcredits')) { 656 options.showCredits = w.getAttribute('data-showcredits'); 657 } 658 if (w.getAttribute('data-startingposition')) { 659 options.startingPosition = w.getAttribute('data-startingposition'); 660 } 661 if (w.getAttribute('data-mode')) { 662 options.mode = w.getAttribute('data-mode'); 663 } 664 if (w.getAttribute('data-makeresponsive')) { 665 options.mode = w.getAttribute('data-makeresponsive'); 666 } 667 668 specificClass = 'juxtapose-' + idx; 669 addClass(element, specificClass); 670 671 selector = '.' + specificClass; 672 673 if (w.innerHTML) { 674 w.innerHTML = ''; 675 } else { 676 w.innerText = ''; 677 } 678 679 slider = new juxtapose.JXSlider( 680 selector, 681 [ 682 { 683 src: images[0].src, 684 label: images[0].getAttribute('data-label'), 685 credit: images[0].getAttribute('data-credit'), 686 alt: images[0].alt 687 }, 688 { 689 src: images[1].src, 690 label: images[1].getAttribute('data-label'), 691 credit: images[1].getAttribute('data-credit'), 692 alt: images[1].alt 693 } 694 ], 695 options 696 ); 697 }; 698 699 //Enable HTML Implementation 700 juxtapose.scanPage = function() { 701 var elements = document.querySelectorAll('.juxtapose'); 702 for (var i = 0; i < elements.length; i++) { 703 juxtapose.makeSlider(elements[i], i); 704 } 705 }; 706 707 juxtapose.JXSlider = JXSlider; 708 window.juxtapose = juxtapose; 709 710 juxtapose.scanPage(); 711 712}(document, window)); 713 714 715// addEventListener polyfill / jonathantneal 716!window.addEventListener && (function (WindowPrototype, DocumentPrototype, ElementPrototype, addEventListener, removeEventListener, dispatchEvent, registry) { 717 WindowPrototype[addEventListener] = DocumentPrototype[addEventListener] = ElementPrototype[addEventListener] = function (type, listener) { 718 var target = this; 719 720 registry.unshift([target, type, listener, function (event) { 721 event.currentTarget = target; 722 event.preventDefault = function () { event.returnValue = false }; 723 event.stopPropagation = function () { event.cancelBubble = true }; 724 event.target = event.srcElement || target; 725 726 listener.call(target, event); 727 }]); 728 729 this.attachEvent("on" + type, registry[0][3]); 730 }; 731 732 WindowPrototype[removeEventListener] = DocumentPrototype[removeEventListener] = ElementPrototype[removeEventListener] = function (type, listener) { 733 for (var index = 0, register; register = registry[index]; ++index) { 734 if (register[0] == this && register[1] == type && register[2] == listener) { 735 return this.detachEvent("on" + type, registry.splice(index, 1)[0][3]); 736 } 737 } 738 }; 739 740 WindowPrototype[dispatchEvent] = DocumentPrototype[dispatchEvent] = ElementPrototype[dispatchEvent] = function (eventObject) { 741 return this.fireEvent("on" + eventObject.type, eventObject); 742 }; 743})(Window.prototype, HTMLDocument.prototype, Element.prototype, "addEventListener", "removeEventListener", "dispatchEvent", []);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.