2023年2月19日日曜日

Googleフォトに保存されているデータ数を把握する方法

概要

Googleフォトにバックアップされている画像枚数を調べる方法を紹介する。

なお、「Googleフォトに保存されているデータ容量を調べる」で紹介している方法ではデータをすべてダウンロードする際などに必要になる容量を確認できる。

容量ではなく、画像の枚数に関してはいくつかの方法で確認することができるが、それぞれ表示される値が異なります。

1.Googleダッシュボードでの確認

2.AndroidスマホのGmailアプリなどで確認

3.Googleアカウント画面での確認

4.GoogleTakeoutでダウンロードして確認(調査中)

以降ではそれぞれの確認方法を説明します。

1.Googleダッシュボードでの確認

確認手順

  1. Safari、Chrome、EdgeなどのWEBブラウザで https://myaccount.google.com/dashboard/ にアクセスする
  2. 「フォト」欄の「xxx,xxx以上 枚の写真」を確認する。

結果

300,000以上 枚の写真

考察

枚数が少ないうちは正確な数値を確認できるかもしれませんがここでの枚数確認は使い物になりません。

2.AndroidスマホのGmailアプリなどで確認

確認手順

  1. AndroidのGmailアプリを起動
  2. 新規メール作成
  3. 画面上のクリップアイコンを選択
  4. 「ファイルを添付」を選択
  5. 「他のアプリでファイルを探す」の中に表示されている「フォト」を選択
  6. 「写真を選択」画面の「写真」項目の「xxx,xxx個の項目」を確認する。

結果

324,012個の項目

考察

ダッシュボードよりは具体的な数値が表示される。 なお、Googleから教えたもらった内容によると、「Gmail では添付できないタイプのデータである場合は、 Gmail アプリでは検出できずに数値に含まれない可能性がございます。」とのことですが、、、Googleドライブからも同じ手順で確認できて、同じ数値が表示されます。なので添付できない画像があるというリクツは成り立たない感じです。

3.Googleアカウント画面での確認

確認手順

※ アカウントを削除する前の確認画面なので最新の注意を払って操作してください。

  1. Safari、Chrome、EdgeなどのWEBブラウザで https://myaccount.google.com/deleteaccount にアクセスする
  2. 「このコンテンツがすべて削除されます」の中を確認
  3. 「フォト」の項目「xxx,xxx 枚の写真が削除されます」や「他 xxx,xxx件」を確認する。

結果

334,044枚の写真 

他 334,041件

考察

この方法で確認した数値が一番大きいが、「他 XXX,XXX件」と表示されている枚数は何なのか謎が多い。もしかしたらここで表示されているのはGoogleフォトだけではなく、Bloggerなどで使用している画像データの枚数も合算されている可能性がある。

4.GoogleTakeoutでダウンロードして確認(調査中)

確認手順[T.B.D.]

  1. GoogleTakeout画面に遷移
  2. Googleフォト以外のみにチェックを入れる
  3. 自動的に作成された年毎のアルバムにチェックを入れる
  4. アーカイブにもチェックを入れる
  5. 必要であればゴミ箱にもチェックを入れる
  6. Exportする

結果

T.B.D.

考察

T.B.D.

結論

GoogleTakeoutでの確認が完了していないが、枚数が少ないときはGoogleダッシュボードで確認できる、ある程度以上枚数が多くなったときは、AndroidのGmailアプリで確認するか、アカウント削除確認画面で確認するしかない感じです。

2022年12月26日月曜日

Google Oneを安く契約する方法

GoogleOneを 100GB月200円で契約する方法

GoogleOneは契約方法によって値段が違います、まったく同じサービスが提供されるのであれば少しでも安い方が良いです。どの方法で契約しても手間は変わらないのでぜひ安い方法で契約してください。

結論:iPhoneのGmailアプリで契約すると¥200/月

  1. iPhoneのGmailアプリの右上のアカウントアイコンをタップ
  2. 「15GBのストレージのXX%を使用してます」をタップ
  3. 料金が表示されますが、なぜか¥200/月という最安値

Googleフォトアプリ経由だと¥240/月

GoogleOneアプリ経由だと通常価格の¥250/月

2022年11月29日火曜日

Google Formsでドロップダウン形式の日付を毎日変更する

背景

グーグルフォームのGASで自動で毎日ドロップダウン形式の日付が変わる仕組みの質問質問があり、リファレンスマニュアルをみてなんとなくできそうなことはわかっていたが、実際に作ってみた。

ソースコート

function updateForm() {
  // 毎日実行するトリガを設定
  setTrigger( arguments.callee.name );

  var forms = FormApp.getActiveForm();
  var items = forms.getItems();

  var table = [
    {'title':'発症日','start':-14 ,'end':0 }, // 二週間前から今日まで
    {'title':'診察希望日1','start':7 ,'end':1 }, // 7日後から明日まで(逆順)
    {'title':'診察希望日2','start':1 ,'end':7 } // 明日から7日後まで
  ];
  
  // 途中に日付が変わると面倒なのでここで取得
  var today = new Date();
  items.forEach( (item) =>{
    table.forEach( (range) =>{
      // リスト形式で質問がテーブルのtitleと一致したときに処理
      if( item.getType() == FormApp.ItemType.LIST && item.getTitle() == range.title )
      {
        item = item.asListItem();
        var choices = [];

        // テーブルの内容に従い選択肢を追加
        var dir = (range.start<range.end)?1:-1;
        for( var i = range.start; (range.start<=i&&i<=range.end)||(range.end<=i&&i<=range.start); i += dir )
        {
         var day = new Date( today );
          day.setDate(today.getDate() + i);
          choices.push( item.createChoice(formatDate( day, "yyyy/MM/dd" )));
        }
        item.setChoices(choices);
      }
    });

  });
}

function formatDate (date, format) {
  format = format.replace(/yyyy/g, date.getFullYear());
  format = format.replace(/MM/g, ('0' + (date.getMonth() + 1)).slice(-2));
  format = format.replace(/dd/g, ('0' + date.getDate()).slice(-2));
  format = format.replace(/HH/g, ('0' + date.getHours()).slice(-2));
  format = format.replace(/mm/g, ('0' + date.getMinutes()).slice(-2));
  format = format.replace(/ss/g, ('0' + date.getSeconds()).slice(-2));
  format = format.replace(/SSS/g, ('00' + date.getMilliseconds()).slice(-3));
  return format;
}

function setTrigger(name) {
  // 念のため既存のトリガはすべて削除してから次のトリガーを設定する
  var triggers = ScriptApp.getProjectTriggers();
  triggers.forEach( (trigger) =>{
    if( trigger.getHandlerFunction() == name )
    {
      ScriptApp.deleteTrigger(trigger);
    }
  });
  var setTime = new Date();
  setTime.setDate(setTime.getDate() + 1)
  setTime.setHours(12);
  setTime.setMinutes(00); 
  ScriptApp.newTrigger(name).timeBased().at(setTime).create();  
}

フォームに追加したプルダウン



2022年11月20日日曜日

GoogleTakeoutでダウンロードしたGoogleフォト画像のファイルタイムスタンプを修正するプログラム



以前の記事( https://blog.hikozaru.com/2022/11/googletakeoutgoogle.html)で紹介した作業をもう少し簡単に実行できるようにプログラムを作成したのでそのソースコードを公開する。いろいろ手を抜くために邪道なこと(変数名に日本語を使う)を行っているが、気になる人は好きに修正するとよい。プロジェクト全体が欲しい方はここから、実行できるものが欲しい方はここからダウンロードしてください。

takeout機能で複数のZIPファイルに分割された場合にはすべての同一のフォルダに統合してください。****.jpegと****.jpeg.jsonが同一フォルダに存在する場合に処理可能なのですが、****.jpegと****.jpeg.jsonが同一のZIPファイルに格納されていると限りません。

該当のファイルを実行アイコンにドラッグ&ドロップするか、実行して表示されたウインドウにドラッグ&ドロップしてください。


プログラムの大まかな処理の流れは以下です。

  1. 指定されたファイルもしくは指定されたフォルダ配下のファイルすべてを処理
    ただし、JSONファイルは処理をスキップ
  2. ファイル名の後ろに".json"を付加したファイルが存在していたらデシリアライズして撮影日時を取得
  3. 撮影日時はunixtimeなのでutcを経由してローカル時間に変換
  4. ファイルの作成日時と更新日時を変更、その処理が成功したらJSONファイルも同様に作成日時と更新日時を変更
  5. 結果をDataGridに出力
JSONファイルから撮影日時を取り出してファイルのタイムスタンプを修正する処理に関連するコードはほんの一部(コードの背景色を変えた行)です。その他は操作性やエラー発生時の処理などを考慮したコードです。

まだ、大量データでのテストは実施していませんが、DataGridへの出力がボトルネックとなる可能性があります。もしかしたら処理成功したファイルの表示は辞めるとか、そもそもDataGridへの出力は辞めて結果をログファイルに出力の方が良いかもしれません。


まずはCSファイル

using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Threading;
using System.Windows;
namespace PhotosTimestamp
{
    [DataContract]
    public class ImageInfo
    {
        [DataMember(Name = "photoTakenTime")]
        public PhotoTakenTime PhotoTakenTime { get; set; }
    }
    [DataContract]
    public class PhotoTakenTime
    {
        [DataMember(Name = "timestamp")]
        public string timstamp { get; set; }
    }

    public partial class MainWindow : Window
    {
        enum E_RESULT
        {
            更新,
            失敗,
            SKIP
        };

        class Result
        {
            public int 番号 { get; set; }
            public E_RESULT 結果 { get; set; }
            public string ファイル名 { get; set; }
            public string エラーメッセージ { get; set; }
            public string 例外メッセージ { get; set; }
        }

        const string HowToUse
            = "Google TakeoutでダウンロードしたZIPファイルを展開し、展開されたファイル/フォルダをドラッグ&ドロップして下さい。\n"
            + "コマンドライン引数やプログラムアイコンへのドラッグ&ドロップでもファイル/フォルダ指定可能です。\n"
            + "画像ファイルにJSONファイル(例:IMG_1234.PNGに対しIMG_1234.PNG.json)がある場合に処理を行います。\n"
            + "TakeoutでZIPファイルが分割された場合には一つのフォルダに統合してください、画像ファイルとJSONファイルが同一のZIPに格納されるとは限りません。\n"
            + "JSONファイルに記録されているphotoTakenTime.timestampをLocal時間に変換し、画像ファイルとJSONファイルの作成日時と更新日時に設定します。";

        public MainWindow()
        {
            InitializeComponent();
            var resultList = new ObservableCollection();
            resultDataGrid.ItemsSource = resultList;
        }

        // 引数で指定されたときは即実行
        private void Window_ContentRendered(object sender, EventArgs e)
        {
            string[] args = Environment.GetCommandLineArgs();
            if (args.Length > 1)
            {
                Thread thread = new Thread(new ParameterizedThreadStart(WorkerThread));
                thread.Start(args.Skip(1).ToArray());
            }
            else
            {
                ddPanel.AllowDrop = true;
                status.Content = HowToUse;
            }
        }

        // Drag&Drop関連処理
        private void ddPanel_PreviewDragOver(object sender, DragEventArgs e)
        {
            e.Effects = DragDropEffects.None;
            if (e.Data.GetDataPresent(System.Windows.DataFormats.FileDrop, true))
            {
                e.Effects = DragDropEffects.Copy;
                e.Handled = e.Data.GetDataPresent(DataFormats.FileDrop);
            }
        }
        private void ddPanel_Drop(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                Thread thread = new Thread(new ParameterizedThreadStart(WorkerThread));
                thread.Start(e.Data.GetData(DataFormats.FileDrop));
            }
        }


        // UIが固まらないようにThreadで処理
        void WorkerThread(object arg)
        {
            Dispatcher.Invoke((Action)(() =>
            {
                ddPanel.AllowDrop = false;
                status.Content = "処理開始";
                var dataList = resultDataGrid.ItemsSource as ObservableCollection;
                dataList.Clear();
            }));
            DoEntries((string[])arg);
            Dispatcher.Invoke((Action)(() =>
            {
                ddPanel.AllowDrop = true;
                status.Content = "処理が終了しました。\n" + HowToUse;
            }));
        }
        void DoEntries(string[] args)
        {
            foreach (string arg in args)
            {
                if (File.Exists(arg) == true)
                {
                    DoFile(arg);
                }
                else
                {
                    try
                    {
                        foreach (var path in Directory.GetFiles(arg, "*", System.IO.SearchOption.AllDirectories))
                        {
                            DoFile(path);
                        }
                    }
                    catch (Exception e)
                    {
                        Dispatcher.Invoke((Action)(() =>
                        {
                            var dataList = resultDataGrid.ItemsSource as ObservableCollection;
                            dataList.Add(new Result() { 番号=dataList.Count+1, 結果=E_RESULT.失敗, ファイル名 = arg, エラーメッセージ="ファイルまたはフォルダが見つかりませんでした。",例外メッセージ = e.ToString() });
                        }));
                    }
                }
            }
        }

        void DoFile(string targetFilename)
        {
            var jsonFilename = targetFilename + ".json";
            if (string.Compare(".json", System.IO.Path.GetExtension(targetFilename), true) == 0)
            {
                Dispatcher.Invoke((Action)(() =>
                {
                    status.Content = targetFilename;
                }));
            }
            else if (System.IO.File.Exists(jsonFilename))
            {
                DateTime photoTakenTime;
                try
                {
                    using (var stream = new FileStream(jsonFilename, FileMode.Open, FileAccess.Read))
                    {
                        var serializer = new System.Runtime.Serialization.Json.DataContractJsonSerializer(typeof(ImageInfo));
                        var imageInfo = (ImageInfo)serializer.ReadObject(stream);
                        var unixtime = imageInfo.PhotoTakenTime.timstamp;
                        photoTakenTime = DateTimeOffset.FromUnixTimeSeconds(long.Parse(unixtime)).LocalDateTime;
                    }
                    var imageFile = new System.IO.FileInfo(targetFilename);
                    var jsonFile = new System.IO.FileInfo(jsonFilename);

                    var results = new Result[] {
                        new Result(){ 結果=E_RESULT.SKIP,ファイル名=targetFilename,エラーメッセージ="",例外メッセージ="" },
                        new Result(){ 結果=E_RESULT.SKIP,ファイル名=jsonFilename,エラーメッセージ="画像ファイルの更新に失敗したときはJSONファイルも更新しません。",例外メッセージ="" }
                    };

                    foreach (var result in results)
                    {
                        try
                        {
                            var file = new System.IO.FileInfo(result.ファイル名);
                            file.CreationTime = photoTakenTime;
                            file.LastWriteTime = photoTakenTime;
                            result.結果 = E_RESULT.更新;
                            result.エラーメッセージ = "";
                            result.例外メッセージ = "";
                        }
                        catch (Exception e)
                        {
                            result.結果 = E_RESULT.失敗;
                            result.エラーメッセージ = "タイムスタンプの更新に失敗しました。";
                            result.例外メッセージ = e.ToString();
                            break;
                        }
                    }
                    Dispatcher.Invoke((Action)(() =>
                    {
                        status.Content = targetFilename;
                        var dataList = resultDataGrid.ItemsSource as ObservableCollection;
                        foreach (var result in results)
                        {
                            result.番号 = dataList.Count + 1;
                            dataList.Add(result);
                        }
                    }));
                }
                catch(Exception e)
                {
                    Dispatcher.Invoke((Action)(() =>
                    {
                        status.Content = targetFilename;
                        var dataList = resultDataGrid.ItemsSource as ObservableCollection;
                        dataList.Add(new Result() { 番号 = dataList.Count + 1, 結果 = E_RESULT.失敗, ファイル名 = jsonFilename, エラーメッセージ = "タイムスタンプの取得に失敗しました。", 例外メッセージ = e.ToString() });
                    }));
                }
            }
            else
            {
                Dispatcher.Invoke((Action)(() =>
                {
                    status.Content = targetFilename;
                    var dataList = resultDataGrid.ItemsSource as ObservableCollection;
                    dataList.Add(new Result() { 番号 = dataList.Count + 1, 結果 = E_RESULT.SKIP, ファイル名 = targetFilename, エラーメッセージ = "*.JSONファイルがありません。", 例外メッセージ = "" });
                }));
            }
        }
    }
}
次はXAMLファイル
<window contentrendered="Window_ContentRendered" height="450" mc:ignorable="d" title="Google Takeoutでダウンロードした画像のタイムスタンプをJSONファイルを参考に修正します。" width="1200" x:class="PhotosTimestamp.MainWindow" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:PhotosTimestamp" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
    <dockpanel allowdrop="false" drop="ddPanel_Drop" name="ddPanel" previewdragover="ddPanel_PreviewDragOver">
        <label dockpanel.dock="Bottom" name="status">
        <datagrid dockpanel.dock="Top" isreadonly="True" name="resultDataGrid">
    </datagrid></label></dockpanel>
</window>

2022年11月14日月曜日

GoogleTakeoutでダウンロードしたGoogleフォト画像のファイルタイムスタンプを修正する

22022/11/20追記:PowerShellは難しいという方は次の記事で専用のプログラムをダウンロードできるようにしています。

概要

Googleフォトのデータをtakeout(https://takeout.google.com/?hl=ja)機能でダウンロードするとファイルのタイムスタンプがダウンロード実施した日になってしまう。Takeoutで出力したデータに含まれているJSONデータに撮影日時が記録されているので、その情報をもとに修正する方法を説明します。Windowsパソコンが必要です。
なお、takeout機能で複数のZIPファイルに分割された場合にはすべての同一のフォルダに統合してください。****.jpegと****.jpeg.jsonが同一フォルダに存在する場合に処理可能なのですが、****.jpegと****.jpeg.jsonが同一のZIPファイルに格納されていると限りません。

①PowerShellのスクリプトを実行できるようにする

PowerShell 管理者権限として実行します。ウインドウズメニューを開いた状態で「PowerSell」と入力すると見つかるので、「管理者として実行する」を選択してください。

以下のコマンドを実行し現在の権限を確認しメモしておいてください。

Get-ExecutionPolicy

以下のコマンドを実行して権限を変更してください。

Set-ExecutionPolicy RemoteSigned

変更してよいか確認されるので Y キーを押してください。

なお、権限(RestrictedやRemoteSignedなどの)の詳細は以下を参照してください。

https://learn.microsoft.com/ja-jp/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-7.3#powershell-execution-policies


②スクリプトを作成

拡張子を表示した状態で「CorrectTimestamp.ps1」というファイルを作成しメモ帳で以下の様に編集してください。

$files = Get-ChildItem $PSScriptRoot -File -Recurse -Exclude *.json,*.html,*.ps1,*.zip
foreach($i in $files){
  Write-Host $i.FullName
  $jsondata = Get-Content -Path $i".json" | ConvertFrom-JSON
  $j = $i.FullName+".json"
  
  $UnixTime = $jsondata.photoTakenTime.timestamp.Trim('"')
  $UtcTime = ([DateTime]::Parse("1970/01/01 00:00:00")).addSeconds($UnixTime)
  $LocalTime= [TimeZoneInfo]::ConvertTimeFromUtc($UtcTime,[TimezoneInfo]::local)

  Set-ItemProperty $i -name CreationTime -value $LocalTime
  Set-ItemProperty $j -name CreationTime -value $LocalTime
  Set-ItemProperty $i -name LastWriteTime -value $LocalTime
  Set-ItemProperty $j -name LastWriteTime -value $LocalTime
}
Write-Host "Processing has finished. Please press any key. . ." -NoNewLine
[Console]::ReadKey($true) > $null

JSON内にはいくつかの日時が記録されております、上記のスクリプトではphotoLastModifiedTimeを更新日時に設定していますが、photoTakenTimeを採用するなど好みに合わせて変更してください。更新日時もphotoTakenTimeにしています。


③Zipファイルの展開とスクリプトの配置

Takeout機能でダウンロードしたZIPファイルを全て展開し、展開されたフォルダに「CorrectTimestamp.ps1」をコピーしてください。


④スクリプトの実行

「CorrectTimestamp.ps1」ファイルを右クリックして「PowerShellで実行」を行うとタイムスタンプが修正されます。


その他

JSONファイルに記録されているデータを使ってファイル名を変えたりすることもできるが、PowerShellでなくWSH(Windows Script Host)などでも処理できると思います。




2022年10月9日日曜日

バッチファイル(*.bat)でファイルをゴミ箱に入れる

バッチファイルでファイルを直接削除するのは怖いのでゴミ箱を経由する

バッチファイルで直接ゴミ箱に入れることはできないので、WSH経由で削除します。

PowerShellを使う方法もありますが、セキュリティ的に使えない事が多いのでWSHを選択しました。
また、そもそも全てをWSHで実現すれば良いと思いますが、BATファイルの方が楽なケースも多いので結構役に立ちます。

sample.bat (バッチファイルサンプル)
@echo on
CScript.exe trash.js c:\folder\test1.txt c:\folder\test2.txt
trash.js
var args = WScript.Arguments;
var sa = WScript.CreateObject("Shell.Application");
var fso = WScript.CreateObject("Scripting.FileSystemObject");

for (var i = 0; i < args.length; i++ ) {
  arg = args(i);
  WScript.Echo(arg);
  sa.NameSpace(10).MoveHere( arg );
  while( fso.FileExists( arg ) ){
    WScript.Sleep(100);
  }
}

// ゴミ箱の取得(NameSpace関数の10の意味)
// https://learn.microsoft.com/ja-jp/windows/win32/api/shldisp/ne-shldisp-shellspecialfolderconstants
// WScript.Echo( sa.NameSpace(10).Title );

今後改善したい項目

  • エラー処理
  • 相対パス指定対応

2022年8月28日日曜日

WSH(JScript)で*.xlsmを*.xlsxに変換する

マクロを有効にした XML ブック(*.xlsm)を既定のXML ブック(*.xlsx)に変換する。

xlsm2xlsx.jsのアイコンに変換したい *.xlsm ファイルを複数Drag&Dropすると *.xlsx ファイルを生成する。

xlsm2xlsx.js

var xlWorkbookDefault = 51;

var ExcelApp = new ActiveXObject( "Excel.Application" );

// *.jsファイルにDrag&Dropされたファイルを順番に処理
var args = WScript.Arguments;
for (var i = 0; i < args.length; i++ ) {
  var orgFilename = args(i);

  // 特定の拡張子のみ処理を実施
  var orgExtension = orgFilename.split('.').pop();
  if( orgExtension == 'xlsm' ){
    var baseName = orgFilename.substring(0, orgFilename.indexOf('.'+orgExtension))

    // ファイルを開いて形式を変更して保存
    var book = ExcelApp.Workbooks.Open( orgFilename );
    ExcelApp.DisplayAlerts= false;
    book.SaveAs( baseName + '.xlsx' ,  xlWorkbookDefault );
    ExcelApp.DisplayAlerts= true;
    ExcelApp.Quit();
  }
}
WScript.Echo( "finish" );
ExcelApp = null;

参考

https://docs.microsoft.com/ja-jp/office/vba/api/excel.xlfileformat

https://docs.microsoft.com/ja-jp/office/vba/api/excel.workbook.saveas

2022年7月3日日曜日

HTML+CSSでトグルボタン

すぐに忘れるので画像も文字列も変化するトグルボタン。デザインは適当。

まずはコードから

<!DOCTYPE html>
<html>
<head>
  </head>
  <body>
    <style>
      .btn,.btn:checked+label,.btn+label+label
      {
        display:none;
      }
      .btn+label,.btn:checked+label+label
      {
        display:inline-block;
        user-select: none;
        background-repeat: no-repeat;
      }
      .btn+label
      {
          background-color:aqua;
      }
      .btn:checked+label+label
      {
        background-color:pink;
      }
      .btn:active+label,
      .btn:active+label+label
      {
        background-color: lightgray;
      }
  
      .vertical+label,.vertical+label+label
      {
        padding-top: 2em;
        background-position:top;
        background-size: auto 2em;
      }
      .horizontal+label,.horizontal+label+label
      {
        padding-left: 2em;
        background-position:left;
        background-size: 2em auto;
      }
      .image+label,.image+label+label
      {
        height: 2em;
        width: 2em;
        background-position:center;
        background-size: contain;
        text-indent:100%;
        white-space:nowrap;
        overflow:hidden;
      }
      .text+label,.text+label+label
      {
        background-position:center;
        background-size: 0 0;
      }
    </style>
      <table>
      <TR><TH>ボタン</TH><TH>トグル</TH></TR>
      <TR><TD>
        <input class="btn vertical" type="button" id="1"/>
        <label for="1" style="background-image:url(send.png)">メール送信</label>
        <hr>
        <input class="btn horizontal" type="button" id="2"/>
        <label for="2" style="background-image:url(send.png)">メール送信</label>
        <hr>
        <input class="btn image" type="button" id="5"/>
        <label for="5" style="background-image:url(send.png)">メール送信</label>
        <hr>
        <input class="btn text" type="button" id="6"/>
        <label for="6" style="background-image:url(send.png)">メール送信</label>
        <hr>
      </TD>
      <TD>
        <input class="btn vertical" type="checkbox" id="3"/>
        <label for="3" style="background-image:url(opened.png)">既読メール</label>
        <label for="3" style="background-image:url(unopened.png)">未読メール</label>
        <hr>
        <input class="btn horizontal" type="checkbox" id="4" checked/>
        <label for="4" style="background-image:url(opened.png)">既読メール</label>
        <label for="4" style="background-image:url(unopened.png)">未読メール</label>
        <hr>
        <input class="btn image" type="checkbox" id="7"/>
        <label for="7" style="background-image:url(opened.png)">既読メール</label>
        <label for="7" style="background-image:url(unopened.png)">未読メール</label>
        <hr>
        <input class="btn text" type="checkbox" id="8"/>
        <label for="8" style="background-image:url(opened.png)">既読メール</label>
        <label for="8" style="background-image:url(unopened.png)">未読メール</label>
        <hr>
    </TD></TR>
    </table>
  </body>
<html>

動作は以下のような感じ

普通のボタントグルするボタン








2022年5月28日土曜日

GASでドライブの画像を表示する

知恵袋への回答は質問に直接答えたが、画面ロード時に時間がかかりそうなので処理を全体的に変えてみた。


コード.gs
function doGet(e) {
  return HtmlService.createTemplateFromFile("index.html").evaluate().setTitle("タイトル");
}

function GetBase64Image(fileId,param)
{
  var file = DriveApp.getFileById(fileId);
  var blob = file.getBlob();
  var contentType = blob.getContentType();
  var base64 = Utilities.base64Encode(blob.getBytes());
  var imageSrc = "data:" + contentType + ";base64, " + base64;
  return {"name":file.getName(),"id":fileId,"base64":imageSrc,"param":param};
}
index.html
<html>
<head>
  <script>
    <?
      var folder = DriveApp.getFolderById("1MdeTdaYDv9BK9QTaZes5_bTDRpKiLRnA");
      var files = folder.getFiles();
      var list = [];

      while (files.hasNext()) {
        var file = files.next();
        list.push({"id":file.getId(),"name":file.getName(),"url":file.getUrl()});
      }
    ?>
    var list = <?!= JSON.stringify(list) ?>;
  
    function onSelected(event)
    {
      var img = document.getElementById("image")
      img.alt = "Loading....";
      img.src = null;
      var fileId = event.currentTarget.value;
      google.script.run
        .withSuccessHandler( SuccessGetBase64Image )
        .withFailureHandler( Failure )
        .GetBase64Image( fileId, img.id );
    }
    function Failure(){
      alert("失敗");
    }
    function SuccessGetBase64Image(entry)
    {
      document.getElementById(entry.param).alt = entry.name;
      document.getElementById(entry.param).src = entry.base64;
    }

    function onLoad()
    {
      var select = document.getElementById("select");
      select.addEventListener('change', onSelected);
      list.forEach( file => {
        var option = document.createElement("option");
        option.value = file.id;
        option.innerText = file.name;
        select.appendChild(option);
      });
    }
  </script>
</head>
<body onload="onLoad();">

<label for="select">Choose a menu:</label>
<select name="select" id="select">
<option value="">--Please choose an option--</option>
</select>

<p><img src="" height="562" width="750" alt="選択されてません" align="top" id="image">ここに画像表示</p>
</script>

</body>
</html>

2022年5月14日土曜日

JavaScriptで○×ゲームを作ってみた

JavaScriptでcanvasを使って、三目並べの○×ゲームを作ってみた。
canvasで作るよりもTableやButtonで作った方が大分楽。
<!DOCTYPE html>
<html lang="jp">
  <head>
    <meta charset="utf-8">
    <script>
      let canvas;
      let context;
      let Magnification = 100;
      let Finished = false;
      let FirstStrike=true;
      let MarubatsuTable={};
      function drawFrame()
      {
        // #を描画
        context.beginPath();
        context.moveTo(Magnification*0, Magnification*1);
        context.lineTo(Magnification*3, Magnification*1);
        context.moveTo(Magnification*0, Magnification*2);
        context.lineTo(Magnification*3, Magnification*2);
        context.moveTo(Magnification*1, Magnification*0);
        context.lineTo(Magnification*1, Magnification*3);
        context.moveTo(Magnification*2, Magnification*0);
        context.lineTo(Magnification*2, Magnification*3);
        context.stroke();
      }
      function onClick(e)
      {
        if( Finished )
        {
          // ゲームが終わっている場合はリセット
          context.clearRect(0, 0, canvas.width, canvas.height);
          drawFrame();
          MarubatsuTable = {};
          Finished = false;
          FirstStrike = true;
          return;
        }
        // クリックされたセルを検出
        let rect = e.target.getBoundingClientRect();
        let x = Math.floor( (e.clientX - rect.left) / Magnification );
        let y = Math.floor( (e.clientY - rect.top) / Magnification );
        let id = "X"+ x + "Y" + y;
        if( !MarubatsuTable[id]  )
        {
          if( FirstStrike )
          {
            MarubatsuTable[id] = "○";
            context.beginPath();
            context.arc( (x+0.5)*Magnification, (y+0.5)*Magnification, Magnification/3 , 0, 2 * Math.PI, false ) ;
            context.stroke();
          }
          else
          {
            MarubatsuTable[id] = "×";
            context.beginPath();
            context.moveTo((x+0.2)*Magnification, (y+0.2)*Magnification);
            context.lineTo((x+0.8)*Magnification, (y+0.8)*Magnification);
            context.moveTo((x+0.8)*Magnification, (y+0.2)*Magnification);
            context.lineTo((x+0.2)*Magnification, (y+0.8)*Magnification);
            context.stroke();
          }
          // 結果判定
          const decisionTable =
            [ {dicision:["X0Y0","X1Y0","X2Y0"],line:{x0:0,y0:0.5,x1:3,y1:0.5}},
              {dicision:["X0Y1","X1Y1","X2Y1"],line:{x0:0,y0:1.5,x1:3,y1:1.5}},
              {dicision:["X0Y2","X1Y2","X2Y2"],line:{x0:0,y0:2.5,x1:3,y1:2.5}},
              {dicision:["X0Y0","X0Y1","X0Y2"],line:{x0:0.5,y0:0,x1:0.5,y1:3}},
              {dicision:["X1Y0","X1Y1","X1Y2"],line:{x0:1.5,y0:0,x1:1.5,y1:3}},
              {dicision:["X2Y0","X2Y1","X2Y2"],line:{x0:2.5,y0:0,x1:2.5,y1:3}},
              {dicision:["X0Y0","X1Y1","X2Y2"],line:{x0:0,y0:0,x1:3,y1:3}},
              {dicision:["X2Y0","X1Y1","X0Y2"],line:{x0:0,y0:3,x1:3,y1:0}} ];
          decisionTable.forEach((a)=>
          {
            var temp = MarubatsuTable[a.dicision[0]] + MarubatsuTable[a.dicision[1]] + MarubatsuTable[a.dicision[2]];
            if( temp == "○○○" || temp == "×××" )
            {
              context.beginPath();
              context.moveTo( a.line.x0*Magnification, a.line.y0*Magnification);
              context.lineTo( a.line.x1*Magnification, a.line.y1*Magnification);
              context.stroke();
              Finished = true;
            }
            if( Object.keys(MarubatsuTable).length == 9 )
            {
              Finished = true;
            }
          });
          // 先攻後攻変更
          FirstStrike = !FirstStrike;
        }
      }
      function onLoad(){
        canvas = document.getElementById('sampleCanvas');
        context = canvas.getContext('2d');
        if ( ! canvas || ! context ) {
          return false;
        }
        canvas.style.width = Magnification * 3+"px";
        canvas.style.height = Magnification * 3+"px";
        canvas.width = Magnification * 3;
        canvas.height = Magnification * 3;

        drawFrame();
        canvas.addEventListener('click', onClick, false);
      }
    </script>
  </head>
  <body onload="onLoad();">
    <canvas id="sampleCanvas"></canvas>
  </body>
</html>

おまけ:Canvas縛りがない場合はもっと簡単

<!DOCTYPE html>
<html lang="jp">
  <head>
    <meta charset="utf-8">
    <script>
      let FirstStrike=true;
      let Finished = false;
      let Count = 0;
      function onClick(event){
        // 勝敗決定後のクリックは結果クリア
        if( Finished )
        {
          for (const td of event.currentTarget.querySelectorAll("td")) {
            td.innerText = " ";
            td.style.backgroundColor = "";
          }
          FirstStrike=true;
          Finished = false;
          Count = 0;
          return;
        }

        // クリックされたセルに"○"/"×"を記録
        if(event.target.tagName=="TD")
        {
          if( event.target.innerText==" " ){
            event.target.innerText = FirstStrike?"○":"×";
            FirstStrike =! FirstStrike;
            Count++;

            // 結果判定
            const decisionTable =
              [ ["00","01","02"],
                ["01","11","12"],
                ["02","21","22"],
                ["00","10","20"],
                ["01","11","21"],
                ["02","12","22"],
                ["00","11","22"],
                ["20","11","02"] ];
            decisionTable.forEach((a)=>
            {
              var temp = document.getElementById(a[0]).innerText+document.getElementById(a[1]).innerText+document.getElementById(a[2]).innerText;
              if( temp == "○○○" || temp == "×××" )
              {
                Finished = true;
                document.getElementById(a[0]).style.backgroundColor = "lightblue"; 
                document.getElementById(a[1]).style.backgroundColor = "lightblue"; 
                document.getElementById(a[2]).style.backgroundColor = "lightblue"; 
              } 
            });
            if( Count >=9 )
            {
              Finished = true;
            }
          }
        }
      }
    </script>
  </head>
  <body>
    <table onclick="onClick(event);" border="1">
      <tr><td id="00"> </td><td id="01"> </td><td id="02"> </td></tr>
      <tr><td id="10"> </td><td id="11"> </td><td id="12"> </td></tr>
      <tr><td id="20"> </td><td id="21"> </td><td id="22"> </td></tr>
    </table>
  </body>
</html>