希卡文字是遊戲《塞爾達傳說曠野之息》中一種虛構的文字,在塞爾達遊戲中全部的希卡族的建築上都能找到上面的符號的影子,一直覺得這些只是裝飾性的符號,直到看到塞學家的分析才恍然大悟,原來這些都是文字呀!不愧是老任,塞爾達天下第一!javascript
希卡文能夠與英文字符作相互映射,轉換器實現的就是兩種文字的相互轉換,支持將英文字符轉換成希卡文和希卡文圖片內容解析成英文:css
工具的演示地址在這:kinglisky.github.io/zelda-wordshtml
github.io 打不開的同窗戳這裏:nlush.com/zelda-words…前端
倉庫地址:github.com/kinglisky/z…vue
虛構世界的文字每每是基於現實文字創造的,希卡文字與英文字母是一一對應的,映射以下:java
知道了映射關係咱們只須要將一個個字母轉換成對應的希卡文就行了,先來準備下希卡文的文字素材,這裏推薦一篇文章:從虛構世界的文字提及。node
做者十分貼心的實現了一套希卡文字體,咱們可從網站中扒拉下字體文件:git
3type.cn/css/fonts/s…github
拿到字體文件其實咱們配置下 @font-face
就能夠直接使用了web
@font-face {
font-family: "SheikahGlyphs";
src: url("https://3type.cn/css/fonts/sheikahglyphs-regular-webfont.woff2") format("woff2");
}
.sheikah-word {
font-family: SheikahGlyphs;
}
複製代碼
<span class="sheikah-word">abc</span>
複製代碼
不過考慮到後面咱們須要固定文字的格子與間距的大小,咱們換一種用法將字體轉成 svg 圖標來使用。
使用使用上述工具咱們獲得字體文件的單個字符的 svg 文件了,而後導入到 iconfont 中生成字體圖標:
<script src="//at.alicdn.com/t/font_2375469_s4wmtifuqro.js"></script>
複製代碼
而後咱們封裝一個簡單的文件圖標組件:
<template>
<svg class="word-icon" aria-hidden="true" :style="iconStyle" >
<use v-if="iconName" :xlink:href="iconName" />
</svg>
</template>
<script> import { computed } from 'vue'; export default { name: 'WordIcon', props: { // 圖標名稱 name: { type: String, required: true, }, width: { type: Number, default: '', }, height: { type: Number, default: '', }, color: { type: String, default: '', }, opacity: { type: String, default: '', }, }, setup: (props) => { const iconName = computed(() => props.name ? `#icon-${props.name}` : ''); const iconStyle = computed(() => ({ color: props.color, opacity: props.opacity, width: `${props.width}px`, height: `${props.height}px`, })); return { iconName, iconStyle, }; }, }; </script>
<style> .word-icon { overflow: hidden; width: 1em; height: 1em; padding: 0; margin: 0; fill: currentColor; } </style>
複製代碼
英文字符的翻譯的面板能夠簡單的實現,使用換行符 \n
拆分文字分組,替換不支持的字符爲空字符:
<template>
<section class="words-panel" ref="container" >
<div class="words-panel__groups" v-for="(words, index) in wordGroups" :key="index" >
<WordIcon class="words-panel__icon" v-for="(word, idx) in words" :key="idx" :name="word" :width="size" :height="size" >
{{ word }}
</WordIcon>
</div>
</section>
</template>
<script> import { computed, ref } from 'vue'; import WordIcon from './icon.vue'; const WORDS = [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '.', '!', '?', '-', ]; export default { name: 'WordsPanel', components: { WordIcon, }, data() { return { words: 'hello world', size: 60, }; }, setup: (props) => { const container = ref(null); const wordGroups = computed(() => { return props.words .toLowerCase() .split('\n') .map(words => words.split('').map(v => WORDS.includes(v) ? v : '')); }); return { container, wordGroups, }; }, }; </script>
複製代碼
最後一步就是將希卡文字導出了,由於咱們並無涉及複雜的 DOM 結構與樣式,這裏咱們直接偷懶使用前端 DOM 出圖的方式將目標的文字面板直接導出一張圖片,這塊現成的庫不少,如 html2canvas 和 dom-to-image ,這裏咱們使用 dom-to-image 來出圖,這個庫十分的小巧使用也十分簡單。
這裏簡單講下 DOM 出圖的原理,DOM 出圖主要是利用了 SVG 元素的 foreignObject 標籤,咱們能夠在 foreignObject 標籤下塞入自定義 html 片斷而後將整個 svg 做爲一張圖片 drawImage 到 canvas 上實現出圖:
(async function () {
const svg =
`<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"> <foreignObject x="0" y="0" width="200" height="200"> <div xmlns="http://www.w3.org/1999/xhtml">OUTPUT</div> </foreignObject> </svg>`;
const dataUrl = 'data:image/svg+xml;charset=utf-8,' + svg;
const loadImage = (url) => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = (e) => reject(e);
img.src = url;
});
};
const image = await loadImage(dataUrl);
const canvas = document.createElement('canvas');
canvas.width = 120;
canvas.height = 60;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
console.log(canvas.toDataURL());
})();
複製代碼
dom-to-image 內部處理與上面的流程相似,處理咱們的字體圖標時會生成相似於以下的 SVG 結構:
<svg xmlns="http://www.w3.org/2000/svg">
<foreignObject width="120" height="60">
<svg>
<use xlink:href="#icon-a" />
</svg>
</foreignObject>
</svg>
複製代碼
圖標中使用的特殊的 use 標籤,use 所引用的內容存在於全局,因此在出圖時咱們須要處理下這部分的 symbol 引用。
處理成以下的結構:
<svg xmlns="http://www.w3.org/2000/svg">
<foreignObject width="120" height="60">
<svg>
<symbol id="icon-a" viewBox="0 0 1024 1024">
<path d="xxx"></path>
</symbol>
<use xlink:href="#icon-a" />
</svg>
</foreignObject>
</svg>
複製代碼
最終的圖片導出咱們能夠這樣處理:
import domtoimage from 'dom-to-image';
// fix 節點中 svg 圖標依賴
function fixSvgIconNode(node) {
if (node instanceof SVGElement) {
const useNodes = Array.from(node.querySelectorAll('use') || []);
useNodes.forEach((use) => {
const id = use.getAttribute('xlink:href');
// 將 svg 圖片中依賴的 <symbol> 節點塞到當前 svg 節點下
if (id && !node.querySelector(id)) {
const symbolNode = document.querySelector(id);
if (symbolNode) {
node.insertBefore(
symbolNode.cloneNode(true),
node.children[0]
);
}
}
});
}
return true;
}
export default function exportImage (node) {
return domtoimage.toPng(node, { filter: fixSvgIconNode })
.then(dataUrl => {
console.log(dataUrl);
});
}
複製代碼
自此英文到希卡文的轉換就完成了,重點的咱們看下如何實現希卡文卡片內容的翻譯。
咱們最終產出的內容是一張圖片,咱們須要考慮如何圖片的內容「翻譯」出來,這裏的翻譯我打了個引號,可能咱們並不須要真正的解析翻譯出圖片的內容,或者咱們能夠考慮一種投巧的方式將本來的文字信息隱藏在最終圖片中?
咱們先來試試投巧的方式,將本來的文字信息隱藏圖片中。
將目標信息隱藏在圖片中而不影響圖片視覺展現的技術能夠稱爲圖片隱寫術,若是隱藏的目標對象是一張圖片的話則能夠稱之爲盲水印,盲水印經常使用於圖片版權保護,圖片的泄密追蹤等。
咱們先來試試一種最簡單的圖片隱寫手段:LSB(Least Significant Bit)最低有效位。
咱們都知道一張圖片的每一個像素都是由 RGB 通道的顏色混合而成,而 RGB 某個通道的上色值 +1 或 -1 咱們在肉眼上是沒法區分,就拿 rgb(0, 0, 0)
和 rgb(1, 0, 0)
你在肉眼上能區分嗎?
顯然不行,因此咱們能夠在 RGB 某個通道上對色值進行增減 1 使其變爲奇數(對應 1) 或者 偶數(對應 0),咱們只需將隱藏的信息轉成二進制就能夠映射到某個顏色通道的奇偶數值就能夠實現信息的隱藏了。解析的過程也很簡單,讀取目標通道上的色值,奇數爲 1 偶數爲 0 反轉出 01 的二進制數據再還原成原始數據就行了。
上面希卡文對應的信息是 hello world
,咱們如今試着將它隱藏在上面的圖片的中。
首先將 hello world
轉換成二進制,咱們固然能夠將每一個字母轉成對應的 ASCII 碼而後轉成對應的 8 位二進制數,不過既然是隱藏信息對文本進行編碼咱們何不用一些現成的工具(其實就是偷懶啦),二維碼就是一個很不錯的載體。
咱們能夠生成一張 hello world
對應的黑白二維碼:
下面就是將二維碼的信息隱藏到希卡文結果的圖片中,由於是黑白的二維碼圖片,咱們能夠很簡單將黑色像素的值歸爲 0(偶數位)白色像素的值歸爲 1(奇數位),但由於二維碼圖片和希卡文圖片的尺寸並不一致,咱們不方便將兩張圖片的像素位一一對應,我能夠先將二維碼的尺寸調整成和希卡文圖片一致。
圖片準備好了,咱們先來實現隱藏和解析水印的方法:
// 寫入二維碼水印
function writeMetaInfo(baseImageData, qrcodeImageData) {
const { width, height, data } = qrcodeImageData;
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
// 選用 r 通道來隱藏信息
const r = (x + y * width) * 4;
const v = data[r];
// 二維碼白色部分(背景)標識爲 1,黑色部分(內容)標識爲 0
const bit = v === 255 ? 1 : 0;
// 若是當前 R 通道色值奇偶性和二維碼對應像素不一致則進行加減一使其奇偶性一致
if (baseImageData.data[r] % 2 !== bit) {
baseImageData.data[r] += bit ? 1 : -1;
}
}
}
return baseImageData;
}
// 讀取二維碼水印
function readMetaInfo(imageData) {
const { width, height, data } = imageData;
const qrcodeImageData = new ImageData(width, height);
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
// 讀取 r 通道息
const r = (x + y * width) * 4;
// 奇數顏色爲白色 255,偶數顏色爲黑色 0
const v = data[r] % 2 === 0 ? 0 : 255;
qrcodeImageData.data[r] = v;
qrcodeImageData.data[r + 1] = v;
qrcodeImageData.data[r + 2] = v;
qrcodeImageData.data[r + 3] = 255;
}
}
return qrcodeImageData;
}
複製代碼
完整的示例在這裏 ,下面是隱藏二維碼後的希卡文圖片,是否是肉眼看不到什麼變化?
對應的希卡片解析出的二維碼以下,雖然帶有一些噪點信息,但不影響二維碼的識別。
最先的一版希卡圖片識別就是用圖片的最低有效位來隱藏信息的,完成的時候興高采烈準備分享到微信讓小可愛看下,等等!隱約記得微信會壓縮圖片,要不發微信再下載下來試試?
蒼天吶!果真經過微信分享後圖片會通過一些壓縮處理(微信會把 PNG 圖片都處理成 JPG 圖片),致使咱們隱藏在圖片中奇偶位信息丟失,試着解析了下微信的分享壓縮事後的圖片最終得出圖片以下:
最低有效位的實現簡單,但隱藏信息抗干擾能力卻不好,圖片的壓縮很容形成奇偶位信息的丟失。咱們須要考慮如何提升隱藏信息抗干擾能力,最低有效是將信息隱藏在某個像素通道上的,若是咱們能夠把隱藏信息的範圍擴大呢?好比說二維碼是用一個個黑白的色塊標識數據比特位。咱們是否能經過一個個色塊來隱藏信息呢?看個例子:
上面的色塊影藏的什麼信息呢?
[100, 200]
[01100100, 11001000]
複製代碼
其實就是用了 16 個黑白色塊表示表示了數字 100 和 200,黑色表示 0 白色表示 1,解析也十分簡單,上面的圖片拆分紅 16 個色塊,檢查每一個色塊,黑色的讀取爲 0 白色的讀取爲 1。上面使用黑白色來映射 01 咱們換個規則,好比用 rgb(0, 0 , 0)
表示 0 rgb(2, 2, 2)
表示 1。生成的圖片長什麼樣呢?
肉眼是否是很難看出色差,但咱們確實是把信息影藏進圖片裏了,解析規則也很簡單,拆分色塊讀取顏色,rgb(0, 0 , 0)
爲 0,大於 rgb(0, 0, 0)
的爲 1,這樣必定程度上只要圖片不是壓縮的過度,咱們仍是能解析出原始信息的,固然提升兩個顏色間的差值對比也不失爲一種方法(肉眼不可見的範圍內儘可能拉高)。
簡單的實現以下:
// 統一成 8 位
function paddingLfet(bits) {
return ('00000000' + bits).slice(-8);
}
function write(data) {
const bits = data.reduce((s, it) => s + paddingLfet(it.toString(2)), '');
const size = 100;
const width = size * bits.length;
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = size;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#0000000';
ctx.fillRect(0, 0, width, size);
for (let i = 0; i < bits.length; i++) {
if (Number(bits[i])) {
ctx.fillStyle = '#020202';
ctx.fillRect(i * size, 0, size, size);
}
}
return canvas.toDataURL();
}
async function read(url) {
const image = await loadImage(url);
const canvas = document.createElement('canvas');
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
const size = 100;
const bits = [];
for (let i = 0; i < 16; i++) {
const imageData = ctx.getImageData(i * size, 0, size, size);
const r = imageData.data[0];
const g = imageData.data[1];
const b = imageData.data[2];
bits.push(r + g + b === 0 ? 0 : 1);
}
return bits;
}
複製代碼
這種方法穩是挺穩的,但能隱藏的信息太少了,咱們用來隱藏大量的文字信息並不實用,卻是能夠隱藏一些關鍵的信息,後面咱們會用這種方式去記錄希卡文卡片的格子大小,用於圖片的解析。
圖片的隱藏水印是一門高深的學問,以上只是些樸素實現,實際生產中一版是利用傅里葉變換生成圖片的頻域圖,而後將水印信息隱藏在頻域圖中再作傅里葉逆變換還原成正常的圖片,這樣生成的圖片有很好的抗干擾能力,不過這塊超綱了(啃不動),摸清楚了再來試試。
接上面的問題,既然隱藏文字信息的「投巧」方案走不通,那咱們就試着真正去解析圖片的內容吧~
識別圖片的文字能想到技術就是 OCR,也扒拉到了現成的工具 tesseractjs,不過想要實現一套希卡文字的識別則須要訓練生成希卡文字的 raineddata
才行,這裏咱們試着用一種樸素的方式來實現(主要是太菜玩不轉😂)。
對於生成的希卡圖片,咱們已經知道符號和英文字母的映射關係,並且它們都是由同一套字體生成,文字按照一樣的格子大小排布在圖片中,空格子表示空字符串,若是咱們能把文字內容拆分一個個格子,再與已知字符圖片進行匹配,挑出最接近圖片字符圖片不就現實了文字內容的識別碼?這裏最核心的內容其實就是如何實現兩張類似圖片的識別。
咱們先來確認兩個關鍵信息:
對於一張希卡圖片咱們總得知道它的格子大小才能作拆分吧?其實在生成的圖片中咱們已經偷偷把這些信息藏進去了,我貼張圖片你們就懂了:
還記得上面使用色塊隱藏信息的方法嗎?生成的希卡圖片時咱們偷偷在第一行藏了些隱藏信息,不過使用的顏色與背景色十分接近肉眼很難區分罷了。
圖片的首行咱們塞了三個關鍵信息:文字排列方式(0 or 1 標識)、文字格子的大小,圖片的寬度(幾個格子大小),每一個信息二進制爲 8 bit 長度,總長度 24 位。解析時咱們拿到圖片咱們只須要截取圖片第一行(高度隨意 2 ~ 4 像素足以)拆分均等的 24 份,第一份的顏色用於標識 0(由於前八位表示文字排列只有 01 兩種狀況,01 換成 8 位二進制前 7 位都是 0),剩下的 23 個色塊顏色與第一塊相同的標識爲 0 不用則標識爲 1,簡單暴力。
經過上面的方法咱們能夠拿到最關鍵的信息圖片格子大小,咱們能夠按照格子大小將圖片拆分紅一個個均等的格子:
最終咱們能夠獲得這樣的一個格子,如今咱們須要作的就是從字典圖片中匹配出這個符號,字典圖片是咱們提早準備好的,就是下面這張:
圖片的格子大小爲 100,從左到右分別是:abcdefghijklmnopqrstuvwxyz0123456789.-!?
,咱們同樣能夠按順序拆分每一個字母對應的符號圖片。
剩下的就是比較兩張圖片類似性,從字典圖片中找出最類似的圖片,類似圖片的識別其實原理很簡單,以前有很詳細的整理過一篇文章這裏就很少贅述了。
下面全部涉及的類似圖片檢查的內容都在這了 類似圖片識別的樸素實現,有興趣的同窗能夠看看。
姑且概述類似圖片比較的原理,咱們無法很直接去比較兩張圖片是否類似,若是圖片兩張都是以二進制方式表示呢?若是兩張圖片都能變成下面同等長度的二進制字符串,咱們比較兩張圖片類似性,只須要判斷這兩字符的同個位置上有差別個數(漢明距離),差別越小,圖片越類似。
00101101
00111001
複製代碼
處理步驟是:
工具在這,上面是統一將圖片縮小成 64x64 大小的樣子,8x8 大小的圖片指紋以下:
咱們須要作的是生成 40 個英文字符對應的圖片指紋(8x8 大小),而後將解析的圖片拆分的格子也按相同的流程生成對應的圖片指紋(8x8 大小),接下來只要從字典中匹配出漢明距離最小的指紋,也就匹配出了原始的英文字符了,而後按照文字排列的方式將文本按順序輸出就行了。
具體代碼實現的代碼比較多就不往這裏貼了,有興趣的同窗能夠點這裏看代碼實現。
這個倉庫其實建了好久了,那時塞爾達玩的正入迷,想搞了希卡文字卡片生成器玩玩的,而後咕咕咕的放了兩年,新年開工摸魚整理倉庫時發現的,恰好想試試 vite 和 vue3,因而摸魚寫了個小工具,翻了翻倉庫發現有太多被本身擱置的東西了,但願你對這個世界還感好奇吧~
最後給特別的你~