cuzi / Chefkoch PDF export

// ==UserScript==
// @name        Chefkoch PDF export
// @description Erzeugt aus einem Rezept ein PDF Dokument zum Herunterladen oder Drucken
// @namespace   cuzi
// @oujs:author cuzi
// @version     2
// @include     http://www.chefkoch.de/rezepte/*
// @grant       GM_xmlhttpRequest
// @require     https://raw.githubusercontent.com/MrRio/jsPDF/master/dist/jspdf.min.js
// ==/UserScript==

function convertImgToDataURL(url, callback){
  GM_xmlhttpRequest({
    method: "GET",
    responseType : "blob",
    url: url,
    onload: function(response) {
      var reader  = new FileReader();
      reader.onloadend = function () {
        var img = new Image();
        img.onload = function(){
          callback(reader.result, parseInt(this.width,10), parseInt(this.height,10));
        };
        img.src = reader.result;
      };
      reader.readAsDataURL(response.response);
    },
  });
}

function trimArray(arr) {
  return arr.map((e) => e.trim());
}
function trimMultiline(s) {
  return trimArray(s.split("\n")).join("\n").trim();
}

function splitText(doc, s, size) {
  size = size?size:500;
  var p = s.split("\n");
  var r = [];
  for(var i = 0; i < p.length; i++) {
    var t = p[i].trim();
    if(t) {
      r.push(t);
    }
  }
  s = r.join("\n").trim();
  return doc.splitTextToSize(s, size);
}

function makeColums(doc, x, y, fontSize, columnSep, rowSep, data) {
  /* Write text to pdf in columns
  data = [
             [text1, text2, text2],
             [text3, text4, text5],
             ....
         ]
  */
  doc.setFontSize(fontSize);
  
  var columnWidth = [];
  for(var i = 0; i < data.length; i++) {
    for(var j = 0; j < data[i].length; j++) {
      var textWidth = doc.getStringUnitWidth(data[i][j]);
      if(columnWidth[j]) {
        columnWidth[j] = Math.max(columnWidth[j], fontSize * textWidth);
      } else {
        columnWidth.push(fontSize * textWidth);
      }
    }
  }
  
  var start_x = x;
  
  for(var i = 0; i < data.length; i++) {
    for(var j = 0; j < data[i].length; j++) {
      doc.text(x, y, data[i][j]);
      x += columnWidth[j] + columnSep;
    }
    x = start_x;
    y += doc.getLineHeight() + rowSep;
  }
  
  // Return total width and height
  var total_width = columnWidth.reduce((a, b) => a+b) + columnWidth.length * columnSep;
  var total_height = data.length * doc.getLineHeight() + data.length * rowSep;
  return [total_width, total_height];
}

function Layout(doc, x, y) {
  var lineSep = 0;
  var width = 595; // A4 = 495pt x 842pt
  var height = 842;
  var start_x = x;
  var start_y = y;
  
  this.move = function move(toX,toY) {
    x = toX;
    y = toY;
  };
  
  this.pos = function pos() {
    return {"x" : x, "y" : y};
  };
  
  this.pageWidth = function pageWidth() {
    return width;
  };
  
  this.setLineSep = function setLineSep(newlineSep) {
    lineSep = newlineSep
    return this;
  };
  
  this.text = function text(fontSize, str) {
    doc.setFontSize(fontSize);
  
    doc.text(x, y, doc.splitTextToSize(str, width-x));
    
    x += doc.getStringUnitWidth(str) * fontSize;
    return this;
  };
  
  this.line = function line(fontSize, str) {
    doc.setFontSize(fontSize);
        
    x = start_x;
    var textHeight = doc.splitTextToSize(str, width-x).length * doc.getLineHeight();
    this.text(fontSize, str);
    y += textHeight + lineSep;
    return this;
  };
  
  this.r = function r() {
    x = start_x;
  };
  
  this.br = function br(numberOfNewLines) {
    if(numberOfNewLines === -1) {
      x = start_x;
      y -= doc.getLineHeight() - lineSep;
    } else if(numberOfNewLines > 1) {
      for(var i = 0; i < numberOfNewLines; i ++) {
        br(1);
      }
    } else if(numberOfNewLines < 0) {
      for(var i = 0; i < -numberOfNewLines; i ++) {
        br(-1);
      }
    } else {
      x = start_x;
      y += doc.getLineHeight() + lineSep;
    }
    return this;
  };
  
  this.columns = function columns(fontSize, columnSep, rowSep, data) {
    var res = makeColums(doc, x, y, fontSize, columnSep, rowSep, data);
    x += res[0];
    y += res[1] + lineSep;
    return this;
  };
  
  
}


function RecipePage() {
  if(!document.querySelector("#recipe-incredients")) {
    throw Error("RecipePage() needs a recipe page");
  }

  this.title = function getTitle() {
    return document.querySelector("h1").textContent.trim();
  };

  this.summary = function getSummary() {
    return document.querySelector(".summary")?document.querySelector(".summary").textContent.trim():"";
  };

  this.ingredients = function getIngredients() {
    var ingredients = [];
    var tr = document.querySelectorAll("#recipe-incredients tr");
    for(var i = 0; i < tr.length; i++) {
      var td = tr[i].getElementsByTagName("td");

      var c = [];
      for(var j = 0; j < td.length; j++) {
        c.push(td[j].textContent.trim());
      }
      if(c) {
        ingredients.push(c);
      }
    }
    return ingredients;
  };

  this.servings = function getServings() {
    return (document.querySelector("#divisor").value + ' ' + document.querySelector("#divisor").nextElementSibling.firstChild.data).trim();
  };

  this.details = function getDetails() {
    return trimMultiline(document.querySelector("#rezept-zubereitung").previousElementSibling.textContent.trim().replace(/(\s)\s*/g,"$1").replace(/\n/g," "));
  };

  this.instructions = function getInstructions() {
    return trimMultiline(document.querySelector("#rezept-zubereitung").textContent);
  };

  this.imageURL = function getImageURL() {
    if(document.querySelectorAll("#slideshow a")[0].href) {
      var imgs = Array.from(document.querySelectorAll("#slideshow a")).filter((e) => e.style.display == 'block');
      if(imgs.length && imgs[0] && imgs[0].href) {
        return imgs[0].href.toString();
      }
    }
    return false;
  };

}

function makePdf(cb, recipe, imageData, imgWidth, imgHeight) {

  // Generate PDF
  var doc = new jsPDF("portrait", 'pt', 'a4');
  var layout = new Layout(doc, 20, 20);

  layout.setLineSep(5);

  layout.line(14,recipe.title());
  layout.line(11, recipe.summary());
  var image_y_start = layout.pos().y;
  layout.br();
  layout.line(13, "Zutaten (für "+recipe.servings()+")").r();
  layout.columns(12, 20, 5, recipe.ingredients());
  var image_x_start = layout.pos().x;
  var image_y_end = layout.pos().y;
  layout.line(13, "Zubereitung");
  layout.line(12, recipe.details());
  layout.line(12, recipe.instructions());
  
  if(imageData) {
    // Insert Image
    var paddingLeft = 10;
    var paddingRight = 10;
    var newImageWidth = layout.pageWidth() - image_x_start - paddingLeft - paddingRight;
    var newImageHeight = image_y_end - image_y_start;
    if(Math.min(newImageWidth, newImageHeight) > 20) { // Do not include images, if there's less than 20pt available
      var scale = Math.min(newImageWidth/imgWidth, newImageHeight/imgHeight);
      newImageWidth  = Math.floor(scale * imgWidth);
      newImageHeight = Math.floor(scale * imgHeight);
      doc.addImage(imageData, 'JPEG', image_x_start+paddingLeft, image_y_start, newImageWidth, newImageHeight);
    }
  }
  
  // Show PDF
  var datauristring = doc.output('datauristring');
  
  var div = document.createElement("div");
  div.style = "background:#90b262; position:absolute; top:15px; left:2px; padding:3px 5px;";
  document.body.appendChild(div);
  var head = document.createElement("div");
  head.style = "color:White; height:30px;";
  head.appendChild(document.createTextNode("PDF Dokument: "+(parseInt((datauristring.length-28)*0.75/102.4)/10)+"kB"));
  var a = document.createElement("a");
  a.style = "margin-left:10px; color:white; text-decoration:underline";
  a.href = datauristring;
  a.target = '_blank';
  a.appendChild(document.createTextNode("Download"));
  head.appendChild(a);
  var close = document.createElement("a");
  head.appendChild(close);
  close.innerHTML = '<button id="cboxClose" style="top: -15px;" type="button"><span>x</span></button>';
  close.style = "cursor:pointer;";
  close.addEventListener("click",function() {document.body.removeChild(div);});
  div.appendChild(head);
  var iframe = document.createElement("iframe");
  iframe.style = "width:400px; height:600px; ";
  div.appendChild(iframe);
  iframe.src = datauristring;
  cb(doc);
}

function downloadImageAndMakePdf(cb) {
  // Download the currently selected image and then create the PDF document
  var recipe = new RecipePage();
  
  if(recipe.imageURL()) {
    convertImgToDataURL(recipe.imageURL(), function(dataURI, width, height) {
      makePdf(cb, recipe, dataURI, width, height);
    });
  } else {
    makePdf(cb, recipe, false);
  }
};

(function () {
  // Show Button
  var a = document.querySelector("#recipe-buttons a").cloneNode();
  a.innerHTML = "PDF";
  a.href = "javascript:void(0)";
  a.title = "PDF Dokument erzeugen";
  a.className = "button-green  button-file-export";
  var click = function() {
    a.innerHTML = "Warten auf PDF..."; 
    window.setTimeout(function() {
      downloadImageAndMakePdf(function(doc) {
        window.setTimeout(function() {
        a.innerHTML = "PDF erstellt."; 
        a.title = "Hier klicken um PDF zu öffnen. Rechtsklick zum Speichern.";
        a.removeEventListener("click", click); 
        a.href = doc.output('datauristring');
        }, 5000);
      });
    },1); 
  };
  a.addEventListener("click", click);
  document.querySelector("#recipe-buttons").insertBefore(a, document.querySelector("#recipe-buttons a"));
})();