kopia lustrzana https://github.com/backface/turtlestitch
support embedding blocks into PNG metadata
rodzic
789e257c0b
commit
1e18b5b1ad
|
@ -41,6 +41,9 @@
|
|||
* **Translation Updates:**
|
||||
* German
|
||||
|
||||
### 2022-04-22
|
||||
* morphic, objects, gui: support embedding blocks into PNG metadata
|
||||
|
||||
### 2022-04-20
|
||||
* threads: terminate all threads waiting to display a question on ASKing a falsy value
|
||||
* threads: clear "answer" on ASK nothing/falsy
|
||||
|
|
|
@ -13,14 +13,14 @@
|
|||
<meta name="apple-mobile-web-app-title" content="Snap!">
|
||||
<meta name="msapplication-TileImage" content="img/snap-icon-144.png">
|
||||
<meta name="msapplication-TileColor" content="#FFFFFF">
|
||||
<script src="src/morphic.js?version=2022-01-28"></script>
|
||||
<script src="src/morphic.js?version=2022-04-22"></script>
|
||||
<script src="src/symbols.js?version=2021-03-03"></script>
|
||||
<script src="src/widgets.js?version=2021-17-09"></script>
|
||||
<script src="src/blocks.js?version=2022-04-20"></script>
|
||||
<script src="src/threads.js?version=2022-04-20"></script>
|
||||
<script src="src/objects.js?version=2022-04-20"></script>
|
||||
<script src="src/objects.js?version=2022-04-22"></script>
|
||||
<script src="src/scenes.js?version=2022-03-03"></script>
|
||||
<script src="src/gui.js?version=2022-04-06"></script>
|
||||
<script src="src/gui.js?version=2022-04-22"></script>
|
||||
<script src="src/paint.js?version=2021-07-05"></script>
|
||||
<script src="src/lists.js?version=2022-02-07"></script>
|
||||
<script src="src/byob.js?version=2022-04-20"></script>
|
||||
|
|
|
@ -86,7 +86,7 @@ BlockVisibilityDialogMorph, ThreadManager*/
|
|||
|
||||
// Global stuff ////////////////////////////////////////////////////////
|
||||
|
||||
modules.gui = '2022-April-20';
|
||||
modules.gui = '2022-April-22';
|
||||
|
||||
// Declarations
|
||||
|
||||
|
@ -2471,7 +2471,7 @@ IDE_Morph.prototype.endBulkDrop = function () {
|
|||
this.bulkDropInProgress = false;
|
||||
};
|
||||
|
||||
IDE_Morph.prototype.droppedImage = function (aCanvas, name) {
|
||||
IDE_Morph.prototype.droppedImage = function (aCanvas, name, embeddedCode) {
|
||||
var costume = new Costume(
|
||||
aCanvas,
|
||||
this.currentSprite.newCostumeName(
|
||||
|
@ -2491,6 +2491,7 @@ IDE_Morph.prototype.droppedImage = function (aCanvas, name) {
|
|||
return;
|
||||
}
|
||||
|
||||
costume.code = embeddedCode || null;
|
||||
this.currentSprite.addCostume(costume);
|
||||
this.currentSprite.wearCostume(costume);
|
||||
this.spriteBar.tabBar.tabTo('costumes');
|
||||
|
@ -10063,6 +10064,9 @@ CostumeIconMorph.prototype.exportCostume = function () {
|
|||
if (this.object instanceof SVG_Costume) {
|
||||
// don't show SVG costumes in a new tab (shows text)
|
||||
ide.saveFileAs(this.object.contents.src, 'text/svg', this.object.name);
|
||||
} else if (this.object.code) {
|
||||
// embed blocks code inside the PNG image data
|
||||
ide.saveFileAs(this.object.pngData(), 'image/png', this.object.name);
|
||||
} else { // rasterized Costume
|
||||
ide.saveCanvasAs(this.object.contents, this.object.name);
|
||||
}
|
||||
|
|
121
src/morphic.js
121
src/morphic.js
|
@ -642,7 +642,7 @@
|
|||
|
||||
Drops of image elements from outside the world canvas are dispatched as
|
||||
|
||||
droppedImage(aCanvas, name)
|
||||
droppedImage(aCanvas, name, embeddedCode)
|
||||
droppedSVG(anImage, name)
|
||||
|
||||
events to interested Morphs at the mouse pointer. If you want your Morph
|
||||
|
@ -663,8 +663,22 @@
|
|||
droppedImage() event with a canvas containing a rasterized version of the
|
||||
SVG.
|
||||
|
||||
The same applies to drops of audio or text files from outside the world
|
||||
canvas.
|
||||
Note that PNG images provide for embedded text comments, which can be used
|
||||
to include code inside the image. Such a payload has to be identified by
|
||||
an agreed-upon marker. The default tag is stored in MorphicPreferences and
|
||||
can be overriden by apps wishing to make use of this feature. If such an
|
||||
embedded text-payload is found inside a PNG it is passed as the optional
|
||||
third "embeddedCode" parameter to the "droppedImage()" event. embedded text
|
||||
only applies to PNGs. You can embed a string into the PNG metadata of a PNG
|
||||
by calling
|
||||
|
||||
embedMetadataPNG(aCanvas, aString)
|
||||
|
||||
with a raster image represented by a canvas and a string that is to be
|
||||
embedded into the PNG's metadata.
|
||||
|
||||
The same event mechanism applies to drops of audio or text files from
|
||||
outside the world canvas.
|
||||
|
||||
Those are dispatched as
|
||||
|
||||
|
@ -1276,6 +1290,8 @@
|
|||
Jason N (@cyderize) contributed native copy & paste for text editing.
|
||||
Bartosz Leper contributed retina display support.
|
||||
Zhenlei Jia and Dariusz Dorożalski pioneered IME text editing.
|
||||
Dariusz Dorożalski and Jesus Villalobos contributed embedding blocks
|
||||
into image metadata.
|
||||
Bernat Romagosa contributed to text editing and to the core design.
|
||||
Michael Ball found and fixed a longstanding scrolling bug.
|
||||
Brian Harvey contributed to the design and implementation of submenus.
|
||||
|
@ -1289,9 +1305,9 @@
|
|||
|
||||
/*global window, HTMLCanvasElement, FileReader, Audio, FileList, Map*/
|
||||
|
||||
/*jshint esversion: 6*/
|
||||
/*jshint esversion: 11, bitwise: false*/
|
||||
|
||||
var morphicVersion = '2022-January-28';
|
||||
var morphicVersion = '2022-April-22';
|
||||
var modules = {}; // keep track of additional loaded modules
|
||||
var useBlurredShadows = true;
|
||||
|
||||
|
@ -1303,6 +1319,7 @@ const CLEAR = new Color(0, 0, 0, 0);
|
|||
Object.freeze(ZERO);
|
||||
Object.freeze(BLACK);
|
||||
Object.freeze(WHITE);
|
||||
Object.freeze(CLEAR);
|
||||
|
||||
var standardSettings = {
|
||||
minimumFontHeight: getMinimumFontHeight(), // browser settings
|
||||
|
@ -1318,6 +1335,7 @@ var standardSettings = {
|
|||
mouseScrollAmount: 40,
|
||||
useSliderForInput: false,
|
||||
isTouchDevice: false, // turned on by touch events, don't set
|
||||
pngPayloadMarker: 'Data\tPayload\tEmbedded',
|
||||
rasterizeSVGs: false,
|
||||
isFlat: false,
|
||||
grabThreshold: 5,
|
||||
|
@ -1338,6 +1356,7 @@ var touchScreenSettings = {
|
|||
mouseScrollAmount: 40,
|
||||
useSliderForInput: false,
|
||||
isTouchDevice: true,
|
||||
pngPayloadMarker: 'Data\tPayload\tEmbedded',
|
||||
rasterizeSVGs: false,
|
||||
isFlat: false,
|
||||
grabThreshold: 5,
|
||||
|
@ -1569,6 +1588,65 @@ function copy(target) {
|
|||
return c;
|
||||
}
|
||||
|
||||
function escapeString(str) {
|
||||
var len, R = '', k = 0, S, chr, ord,
|
||||
ascii = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' +
|
||||
'0123456789@*_+-./,';
|
||||
str = str.toString();
|
||||
len = str.length;
|
||||
while(k < len) {
|
||||
chr = str[k];
|
||||
if (ascii.indexOf(chr) != -1) {
|
||||
S = chr;
|
||||
} else {
|
||||
ord = str.charCodeAt(k);
|
||||
if (ord < 256) {
|
||||
S = '%' + ("00" + ord.toString(16)).toUpperCase().slice(-2);
|
||||
} else {
|
||||
S = '%u' + ("0000" + ord.toString(16)).toUpperCase().slice(-4);
|
||||
}
|
||||
}
|
||||
R += S;
|
||||
k++;
|
||||
}
|
||||
return R;
|
||||
}
|
||||
|
||||
function embedMetadataPNG(aCanvas, aString) {
|
||||
var embedTag = MorphicPreferences.pngPayloadMarker,
|
||||
crc32 = (str, crc) => {
|
||||
let table = [...Array(256).keys()].map(it =>
|
||||
[...Array(8)].reduce((cc) =>
|
||||
(cc & 1) ? (0xedb88320 ^ (cc >>> 1)) : (cc >>> 1), it)
|
||||
);
|
||||
crc = [...str].reduce(
|
||||
(crc, ch) => (crc >>> 8) ^ table[(crc ^ ch.charCodeAt(0)) & 0xff],
|
||||
(crc ? crc = 0 : crc) ^ (-1) // (crc ||= 0) ^ (-1)
|
||||
);
|
||||
return ( crc ^ (-1) ) >>> 0;
|
||||
},
|
||||
arr2Str = (arr) =>
|
||||
arr.reduce((res, byte) => res + String.fromCharCode(byte), ''),
|
||||
int2BStr = (val) =>
|
||||
arr2Str(Array.from(new Uint8Array(new Uint32Array( [val] ).buffer)).reverse()),
|
||||
buildChunk = (data) => {
|
||||
let res = "iTXt" + data;
|
||||
return int2BStr(data.length) + res + int2BStr(crc32(res));
|
||||
},
|
||||
parts = aCanvas.toDataURL("image/png").split(","),
|
||||
bPart = atob(parts[1]).split(""),
|
||||
newChunk = buildChunk(
|
||||
"Snap!_SRC\0\0\0\0\0" +
|
||||
embedTag +
|
||||
aString +
|
||||
embedTag
|
||||
);
|
||||
bPart.splice(-12, 0, ...newChunk);
|
||||
parts[1] = btoa(bPart.join(""));
|
||||
return parts.join(',');
|
||||
}
|
||||
|
||||
|
||||
// Retina Display Support //////////////////////////////////////////////
|
||||
|
||||
/*
|
||||
|
@ -11643,7 +11721,7 @@ HandMorph.prototype.processDrop = function (event) {
|
|||
onto the world canvas, turn it into an offscreen canvas or audio
|
||||
element and dispatch the
|
||||
|
||||
droppedImage(canvas, name)
|
||||
droppedImage(canvas, name, embeddedCode)
|
||||
droppedSVG(image, name)
|
||||
droppedAudio(audio, name)
|
||||
droppedText(text, name, type)
|
||||
|
@ -11692,16 +11770,41 @@ HandMorph.prototype.processDrop = function (event) {
|
|||
function readImage(aFile) {
|
||||
var pic = new Image(),
|
||||
frd = new FileReader(),
|
||||
trg = target;
|
||||
url = event.dataTransfer?.getData( "text/uri-list"),
|
||||
file = event.dataTransfer?.files?.[0],
|
||||
trg = target,
|
||||
embedTag = MorphicPreferences.pngPayloadMarker;
|
||||
|
||||
while (!trg.droppedImage) {
|
||||
trg = trg.parent;
|
||||
}
|
||||
|
||||
pic.onload = () => {
|
||||
canvas = newCanvas(new Point(pic.width, pic.height), true);
|
||||
canvas.getContext('2d').drawImage(pic, 0, 0);
|
||||
trg.droppedImage(canvas, aFile.name);
|
||||
bulkDrop();
|
||||
|
||||
(async () => {
|
||||
if (!file && url) {
|
||||
// alternative: "https://api.allorigins.win/raw?url=" + url
|
||||
file = await fetch(url);
|
||||
}
|
||||
|
||||
// extract embedded data (e.g. blocks)
|
||||
// from the image's meta data if present.
|
||||
let buff = new Uint8Array(await file?.arrayBuffer()),
|
||||
strBuff = buff.reduce((acc, b) =>
|
||||
acc + String.fromCharCode(b), ""),
|
||||
embedded = strBuff.includes(embedTag) ?
|
||||
decodeURIComponent(
|
||||
escapeString((strBuff)?.split(embedTag)[1])
|
||||
)
|
||||
: null;
|
||||
|
||||
trg.droppedImage(canvas, aFile.name, embedded);
|
||||
bulkDrop();
|
||||
})();
|
||||
};
|
||||
|
||||
frd = new FileReader();
|
||||
frd.onloadend = (e) => pic.src = e.target.result;
|
||||
frd.readAsDataURL(aFile);
|
||||
|
|
|
@ -89,11 +89,12 @@ SpeechBubbleMorph, InputSlotMorph, isNil, FileReader, TableDialogMorph, String,
|
|||
BlockEditorMorph, BlockDialogMorph, PrototypeHatBlockMorph, BooleanSlotMorph,
|
||||
localize, TableMorph, TableFrameMorph, normalizeCanvas, VectorPaintEditorMorph,
|
||||
AlignmentMorph, Process, WorldMap, copyCanvas, useBlurredShadows, BLACK,
|
||||
BlockVisibilityDialogMorph, CostumeIconMorph, SoundIconMorph, MenuItemMorph*/
|
||||
BlockVisibilityDialogMorph, CostumeIconMorph, SoundIconMorph, MenuItemMorph,
|
||||
embedMetadataPNG*/
|
||||
|
||||
/*jshint esversion: 6*/
|
||||
|
||||
modules.objects = '2022-April-20';
|
||||
modules.objects = '2022-April-22';
|
||||
|
||||
var SpriteMorph;
|
||||
var StageMorph;
|
||||
|
@ -10776,6 +10777,12 @@ Costume.prototype.isTainted = function () {
|
|||
return false;
|
||||
};
|
||||
|
||||
// Costume storing blocks code in PNG exports
|
||||
|
||||
Costume.prototype.pngData = function () {
|
||||
return embedMetadataPNG(this.contents, this.code);
|
||||
};
|
||||
|
||||
// SVG_Costume /////////////////////////////////////////////////////////////
|
||||
|
||||
/*
|
||||
|
|
Ładowanie…
Reference in New Issue