From c5b8ee6a916cba4da15dbcfe1ec8d500154b24c5 Mon Sep 17 00:00:00 2001
From: vyv03354 <VYV03354@nifty.ne.jp>
Date: Fri, 8 Feb 2013 21:29:22 +0900
Subject: [PATCH] Implements vertical writing

---
 src/bidi.js             |  10 +++----
 src/canvas.js           |  56 +++++++++++++++++++++++++++++++++-------
 src/evaluator.js        |  39 ++++++++++++++++++++++++----
 src/fonts.js            |  16 +++++++-----
 test/pdfs/.gitignore    |   1 +
 test/pdfs/vertical.pdf  | Bin 0 -> 6905 bytes
 test/test_manifest.json |   6 +++++
 web/viewer.css          |   2 +-
 web/viewer.js           |  11 +++++---
 9 files changed, 112 insertions(+), 29 deletions(-)
 create mode 100644 test/pdfs/vertical.pdf

diff --git a/src/bidi.js b/src/bidi.js
index 568d226f2..2d7fd9407 100644
--- a/src/bidi.js
+++ b/src/bidi.js
@@ -139,16 +139,16 @@ var bidi = PDFJS.bidi = (function bidiClosure() {
     }
   }
 
-  function BidiResult(str, isLTR) {
+  function BidiResult(str, isLTR, vertical) {
     this.str = str;
-    this.ltr = isLTR;
+    this.dir = vertical ? 'ttb' : isLTR ? 'ltr' : 'rtl';
   }
 
-  function bidi(str, startLevel) {
+  function bidi(str, startLevel, vertical) {
     var isLTR = true;
     var strLength = str.length;
-    if (strLength === 0)
-      return new BidiResult(str, isLTR);
+    if (strLength === 0 || vertical)
+      return new BidiResult(str, isLTR, vertical);
 
     // get types, fill arrays
 
diff --git a/src/canvas.js b/src/canvas.js
index 6168372d2..0765049a9 100644
--- a/src/canvas.js
+++ b/src/canvas.js
@@ -896,6 +896,8 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
       var textSelection = textLayer && !skipTextSelection ? true : false;
       var textRenderingMode = current.textRenderingMode;
       var canvasWidth = 0.0;
+      var vertical = font.vertical;
+      var defaultVMetrics = font.defaultVMetrics;
 
       // Type3 fonts - each glyph is a "mini-PDF"
       if (font.coded) {
@@ -969,25 +971,37 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
           }
 
           var character = glyph.fontChar;
-          var charWidth = glyph.width * fontSize * current.fontMatrix[0] +
+          var vmetric = glyph.vmetric || defaultVMetrics;
+          if (vertical) {
+            var vx = vmetric[1] * fontSize * current.fontMatrix[0];
+            var vy = vmetric[2] * fontSize * current.fontMatrix[0];
+          }
+          var width = vmetric ? -vmetric[0] : glyph.width;
+          var charWidth = width * fontSize * current.fontMatrix[0] +
                           charSpacing * current.fontDirection;
 
           if (!glyph.disabled) {
-            var scaledX = x / fontSizeScale;
+            if (vertical) {
+              var scaledX = vx / fontSizeScale;
+              var scaledY = (x + vy) / fontSizeScale;
+            } else {
+              var scaledX = x / fontSizeScale;
+              var scaledY = 0;
+            }
             switch (textRenderingMode) {
               default: // other unsupported rendering modes
               case TextRenderingMode.FILL:
               case TextRenderingMode.FILL_ADD_TO_PATH:
-                ctx.fillText(character, scaledX, 0);
+                ctx.fillText(character, scaledX, scaledY);
                 break;
               case TextRenderingMode.STROKE:
               case TextRenderingMode.STROKE_ADD_TO_PATH:
-                ctx.strokeText(character, scaledX, 0);
+                ctx.strokeText(character, scaledX, scaledY);
                 break;
               case TextRenderingMode.FILL_STROKE:
               case TextRenderingMode.FILL_STROKE_ADD_TO_PATH:
-                ctx.fillText(character, scaledX, 0);
-                ctx.strokeText(character, scaledX, 0);
+                ctx.fillText(character, scaledX, scaledY);
+                ctx.strokeText(character, scaledX, scaledY);
                 break;
               case TextRenderingMode.INVISIBLE:
               case TextRenderingMode.ADD_TO_PATH:
@@ -995,7 +1009,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
             }
             if (textRenderingMode & TextRenderingMode.ADD_TO_PATH_FLAG) {
               var clipCtx = this.getCurrentTextClipping();
-              clipCtx.fillText(character, scaledX, 0);
+              clipCtx.fillText(character, scaledX, scaledY);
             }
           }
 
@@ -1003,12 +1017,23 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
 
           canvasWidth += charWidth;
         }
-        current.x += x * textHScale;
+        if (vertical) {
+          current.y -= x * textHScale;
+        } else {
+          current.x += x * textHScale;
+        }
         ctx.restore();
       }
 
       if (textSelection) {
         geom.canvasWidth = canvasWidth;
+        if (vertical) {
+          var vmetric = font.defaultVMetrics;
+          geom.x -= vmetric[1] * fontSize * current.fontMatrix[0] /
+                    fontSizeScale * geom.hScale;
+          geom.y += vmetric[2] * fontSize * current.fontMatrix[0] /
+                    fontSizeScale * geom.vScale;
+        }
         this.textLayer.appendText(geom);
       }
 
@@ -1027,6 +1052,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
       var geom;
       var canvasWidth = 0.0;
       var textSelection = textLayer ? true : false;
+      var vertical = font.vertical;
 
       if (textSelection) {
         ctx.save();
@@ -1039,7 +1065,11 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
         var e = arr[i];
         if (isNum(e)) {
           var spacingLength = -e * fontSize * textHScale;
-          current.x += spacingLength;
+          if (vertical) {
+            current.y += spacingLength;
+          } else {
+            current.x += spacingLength;
+          }
 
           if (textSelection)
             canvasWidth += spacingLength;
@@ -1055,6 +1085,14 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
 
       if (textSelection) {
         geom.canvasWidth = canvasWidth;
+        if (vertical) {
+          var fontSizeScale = current.fontSizeScale;
+          var vmetric = font.defaultVMetrics;
+          geom.x -= vmetric[1] * fontSize * current.fontMatrix[0] /
+                    fontSizeScale * geom.hScale;
+          geom.y += vmetric[2] * fontSize * current.fontMatrix[0] /
+                    fontSizeScale * geom.vScale;
+        }
         this.textLayer.appendText(geom);
       }
     },
diff --git a/src/evaluator.js b/src/evaluator.js
index 2b98de81a..28f5d8430 100644
--- a/src/evaluator.js
+++ b/src/evaluator.js
@@ -796,7 +796,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
           } // switch
 
           if (chunk !== '') {
-            bidiTexts.push(PDFJS.bidi(chunk, -1));
+            var bidiText = PDFJS.bidi(chunk, -1, font.vertical);
+            bidiTexts.push(bidiText);
 
             chunk = '';
           }
@@ -831,10 +832,6 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
           };
         }
 
-        var cidEncoding = baseDict.get('Encoding');
-        if (isName(cidEncoding))
-          properties.cidEncoding = cidEncoding.name;
-
         var cidToGidMap = dict.get('CIDToGIDMap');
         if (isStream(cidToGidMap))
           properties.cidToGidMap = this.readCidToGidMap(cidToGidMap);
@@ -1031,6 +1028,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
                                                    properties) {
       var glyphsWidths = [];
       var defaultWidth = 0;
+      var glyphsVMetrics = [];
+      var defaultVMetrics;
       if (properties.composite) {
         defaultWidth = dict.get('DW') || 1000;
 
@@ -1049,6 +1048,26 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
             }
           }
         }
+
+        if (properties.vertical) {
+          var vmetrics = dict.get('DW2') || [880, -1000];
+          defaultVMetrics = [vmetrics[1], vmetrics[1] / 2, vmetrics[0]];
+          vmetrics = dict.get('W2');
+          if (vmetrics) {
+            for (var i = 0, ii = vmetrics.length; i < ii; i++) {
+              var start = vmetrics[i++];
+              var code = xref.fetchIfRef(vmetrics[i]);
+              if (isArray(code)) {
+                for (var j = 0, jj = code.length; j < jj; j++)
+                  glyphsVMetrics[start++] = [code[j++], code[j++], code[j]];
+              } else {
+                var vmetric = [vmetrics[++i], vmetrics[++i], vmetrics[++i]];
+                for (var j = start; j <= code; j++)
+                  glyphsVMetrics[j] = vmetric;
+              }
+            }
+          }
+        }
       } else {
         var firstChar = properties.firstChar;
         var widths = dict.get('Widths');
@@ -1089,6 +1108,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
 
       properties.defaultWidth = defaultWidth;
       properties.widths = glyphsWidths;
+      properties.defaultVMetrics = defaultVMetrics;
+      properties.vmetrics = glyphsVMetrics;
     },
 
     isSerifFont: function PartialEvaluator_isSerifFont(baseFontName) {
@@ -1260,6 +1281,14 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
         italicAngle: descriptor.get('ItalicAngle'),
         coded: false
       };
+
+      if (composite) {
+        var cidEncoding = baseDict.get('Encoding');
+        if (isName(cidEncoding)) {
+          properties.cidEncoding = cidEncoding.name;
+          properties.vertical = /-V$/.test(cidEncoding.name);
+        }
+      }
       this.extractWidths(dict, xref, descriptor, properties);
       this.extractDataStructures(dict, baseDict, xref, properties);
 
diff --git a/src/fonts.js b/src/fonts.js
index 9f04c9930..09a04cb56 100644
--- a/src/fonts.js
+++ b/src/fonts.js
@@ -2363,6 +2363,11 @@ var Font = (function FontClosure() {
     // Trying to fix encoding using glyph CIDSystemInfo.
     this.loadCidToUnicode(properties);
     this.cidEncoding = properties.cidEncoding;
+    this.vertical = properties.vertical;
+    if (this.vertical) {
+      this.vmetrics = properties.vmetrics;
+      this.defaultVMetrics = properties.defaultVMetrics;
+    }
 
     if (properties.toUnicode)
       this.toUnicode = properties.toUnicode;
@@ -4449,17 +4454,15 @@ var Font = (function FontClosure() {
       var fontCharCode, width, operatorList, disabled;
 
       var width = this.widths[charcode];
+      var vmetric = this.vmetrics && this.vmetrics[charcode];
 
       switch (this.type) {
         case 'CIDFontType0':
-          if (this.noUnicodeAdaptation) {
-            width = this.widths[this.unicodeToCID[charcode] || charcode];
-          }
-          fontCharCode = this.toFontChar[charcode] || charcode;
-          break;
         case 'CIDFontType2':
+          var cid = this.unicodeToCID[charcode] || charcode;
           if (this.noUnicodeAdaptation) {
-            width = this.widths[this.unicodeToCID[charcode] || charcode];
+            width = this.widths[cid];
+            vmetric = this.vmetrics && this.vmetrics[cid];
           }
           fontCharCode = this.toFontChar[charcode] || charcode;
           break;
@@ -4523,6 +4526,7 @@ var Font = (function FontClosure() {
         fontChar: String.fromCharCode(fontCharCode),
         unicode: unicodeChars,
         width: width,
+        vmetric: vmetric,
         disabled: disabled,
         operatorList: operatorList
       };
diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore
index 9e0c0ffd5..29a8e3880 100644
--- a/test/pdfs/.gitignore
+++ b/test/pdfs/.gitignore
@@ -44,4 +44,5 @@
 !noembed-jis7.pdf
 !noembed-eucjp.pdf
 !noembed-sjis.pdf
+!vertical.pdf
 !issue2099-1.pdf
diff --git a/test/pdfs/vertical.pdf b/test/pdfs/vertical.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..22245220a2a9027523c17932ba93f9b064ee7db9
GIT binary patch
literal 6905
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
MSi{7`#oJZmUlQ<Pvj6}9

literal 0
HcmV?d00001

diff --git a/test/test_manifest.json b/test/test_manifest.json
index d481fc57b..d2cb421bd 100644
--- a/test/test_manifest.json
+++ b/test/test_manifest.json
@@ -906,6 +906,12 @@
       "rounds": 1,
       "type": "eq"
     },
+    {  "id": "vertical",
+      "file": "pdfs/vertical.pdf",
+      "md5": "8a74d33504701edcefeef2afd022765e",
+      "rounds": 1,
+      "type": "eq"
+    },
     {  "id": "issue2099-1",
       "file": "pdfs/issue2099-1.pdf",
       "md5": "c7eca682d70a976dfc4b7e64d3e9f1ce",
diff --git a/web/viewer.css b/web/viewer.css
index d0483a57a..82e6372fd 100644
--- a/web/viewer.css
+++ b/web/viewer.css
@@ -1171,7 +1171,7 @@ canvas {
 .textLayer > div {
   color: transparent;
   position: absolute;
-  line-height:1.3;
+  line-height:1;
   white-space:pre;
 }
 
diff --git a/web/viewer.js b/web/viewer.js
index f10ef1e96..258311535 100644
--- a/web/viewer.js
+++ b/web/viewer.js
@@ -2515,6 +2515,7 @@ var TextLayerBuilder = function textLayerBuilder(textLayerDiv, pageIdx) {
   this.renderLayer = function textLayerBuilderRenderLayer() {
     var self = this;
     var textDivs = this.textDivs;
+    var bidiTexts = this.textContent.bidiTexts;
     var textLayerDiv = this.textLayerDiv;
     var canvas = document.createElement('canvas');
     var ctx = canvas.getContext('2d');
@@ -2535,8 +2536,11 @@ var TextLayerBuilder = function textLayerBuilder(textLayerDiv, pageIdx) {
       if (width > 0) {
         var textScale = textDiv.dataset.canvasWidth / width;
 
-        CustomStyle.setProp('transform' , textDiv,
-          'scale(' + textScale + ', 1)');
+        var transform = 'scale(' + textScale + ', 1)';
+        if (bidiTexts[i].dir === 'ttb') {
+          transform = 'rotate(90deg) ' + transform;
+        }
+        CustomStyle.setProp('transform' , textDiv, transform);
         CustomStyle.setProp('transformOrigin' , textDiv, '0% 0%');
 
         textLayerDiv.appendChild(textDiv);
@@ -2601,7 +2605,8 @@ var TextLayerBuilder = function textLayerBuilder(textLayerDiv, pageIdx) {
       var textDiv = textDivs[i];
 
       textDiv.textContent = bidiText.str;
-      textDiv.dir = bidiText.ltr ? 'ltr' : 'rtl';
+      // bidiText.dir may be 'ttb' for vertical texts.
+      textDiv.dir = bidiText.dir === 'rtl' ? 'rtl' : 'ltr';
     }
 
     this.setupRenderLayoutTimer();