univ-pau-ics/build/service/CalendarExtractor.php

498 lines
13 KiB
PHP

<?php
namespace service;
use \database\core\DatabaseDriver;
use \service\Tesseract;
class CalendarExtractor{
/* [1] Attributes
=========================================================*/
private $start_d = null; // 1st day date
private $img_url = null; // image url
private $d_uid = null; // diplome uid
private $img_res = null; // image resource
private $event = []; // events
/* [2] Constants
=========================================================*/
public static $start_y = 79; // start y
public static $stop_y = 699; // stop y
public static $start_h = 6; // start hour (GMT)
public static $stop_h = 18; // stop hour (GMT)
/* (1) Builds a calendar extractor
*
* @d_uid<String> UID of the diplome
* @img_url<String> URL of the string to extract from
* @start_d<String> Date of the first day
*
* @return instance<CalendarExtractor> Instance
*
---------------------------------------------------------*/
public function __construct($d_uid=null, $img_url=null, $start_d=null){
/* [1] Check arguments
=========================================================*/ {
/* (1) Check type */
if( !is_string($img_url) || !is_string($start_d) )
throw new \Exception("CalendarExtractor.__construct(<String>, <String>, <String>) expected but CalendarExtractor.__construct(<".gettype($d_uid).">, <".gettype($img_url).">, <".gettype($start_d).">) received");
/* (2) Check @img_url link availability */
if( !($img_head=@get_headers($img_url)) )
throw new \Exception("CalendarExtractor.__construct(<String>, <URL>, <String>) received but cannot reach <URL>");
if( !preg_match('@HTTP.+200@', $img_head[0]) )
throw new \Exception("CalendarExtractor.__construct(<String>, <URL>, <String>) received but cannot reach <URL>");
/* (3) Check @start_d format */
if( !preg_match("@^\d{3,}-\d{1,2}-\d{1,2}$@", $start_d) )
throw new \Exception("CalendarExtractor.__construct(<String>, <String>, <DATE>) received <DATE> has not the correct format");
/* (4) Check @d_uid format */
if( !preg_match("@^T\d+$@", $d_uid) )
throw new \Exception("CalendarExtractor.__construct(<UID>, <String>, <String>) received <UID> has not the correct format");
}
/* [2] Fetch file
=========================================================*/ {
/* (1) Try to open file with GD */
$this->img_res = @imagecreatefrompng($img_url);
/* (2) Manage error */
if( !$this->img_res )
throw new \Exception("URL is not a JPEG image, or is unreachable");
/* (3) Register data */
$this->img_url = $img_url;
$this->d_uid = $d_uid;
$this->start_d = $start_d;
}
}
/* (2) Extracts calendar data from image
*
* @return error<bool> FALSE on error
*
---------------------------------------------------------*/
public function process(){
/* [1] Global variables
=========================================================*/ {
/* (1) Image size */ {
// {1} Request for size //
$img_siz = @getimagesize($this->img_url);
// {2} If error //
if( !$img_siz )
throw new \Exception("Cannot get image size");
$img_siz = [
'w' => $img_siz[0],
'h' => $img_siz[1]
];
}
}
/* [2] Extract day limits
=========================================================*/ {
/* (1) Will contain the column+1 w index */
$col_ind = [];
/* (2) Extract column indexes */
for( $x = 0 ; $x < $img_siz['w'] ; $x++ ){
if( $this->getColor($x, 62) <= 10 )
$col_ind[] = $x+1;
}
}
/* [3] For each day -> get events
=========================================================*/ {
$uid = 0;
/* (1) For each day */
for( $day_n = 0 ; $day_n < count($col_ind)-1 ; $day_n++ ){
$col_x = $col_ind[$day_n];
/* (2) For each y pixel -> exctract event */
for( $y = self::$start_y ; $y < self::$stop_y ; $y++ ){
/* (3) Get current color + next */
$p = $this->getColor($col_x, $y);
$p1 = $this->getColor($col_x, $y+1);
$color = '#'.str_pad(dechex($p1), 6, '0', STR_PAD_LEFT);
/* (4) If on black pixel and next not white */
if( $p == 0 && $p1 != 0xffffff ){
// {1} calculate time //
$start_y = $y;
$time_start = $this->yToTime($day_n, $start_y);
$date_start = date('Ymd\THis\Z', $time_start);
// {2} Incr uid //
$uid++;
// {3} Store event start //
$this->event[$uid][$date_start] = [];
// {4} Seek end of event //
$y++;
while( $y < self::$stop_y && $this->getColor($col_x, $y) != 0 )
$y++;
// {5} If end reached //
$time_stop = $this->yToTime($day_n, $y);
$this->event[$uid][$date_start] = [ date('Ymd\THis\Z', $time_stop) ];
// {6} Exctract event's image //
$ev = $this->extractEvent("$date_start-$uid", [$col_x, $start_y+1], [$col_ind[$day_n+1]-1, $y]);
/* {7} Check @event if already exists */ {
$read_name = is_null($ev['name']) ? '?' : $ev['name'];
$sqlr = DatabaseDriver::getPDO()->prepare("SELECT id_event FROM event WHERE basename = :bn AND color = :c AND id_diplome = :idd");
$sqlr->execute([ ':bn' => $read_name, ':c' => $color, ':idd' => $this->d_uid ]);
$fetched = $sqlr->fetch();
// {7.1} If not found in db -> insert //
if( !$fetched ){
// {7.2} Insert new event //
$sqlr = DatabaseDriver::getPDO()->prepare("INSERT INTO event(id_event,basename,color,id_diplome,name) VALUES(DEFAULT,:bn,:c,:idd,:n)");
$sqlr->execute([ ':c' => $color, ':idd' => $this->d_uid, ':bn' => $read_name, ':n' => $read_name ]);
// {7.3} Store id in current event //
$event_id = DatabaseDriver::getPDO()->lastInsertId();
}else
$event_id = $fetched['id_event'];
}
/* (8) Check @location if already exists */ {
$read_location = is_null($ev['location']) ? '?' : $ev['location'];
$sqlr = DatabaseDriver::getPDO()->prepare("SELECT id_location FROM location WHERE basename = :bn AND id_event = :ide");
$sqlr->execute([ ':bn' => $read_location, ':ide' => $event_id ]);
$fetched = $sqlr->fetch();
// {8.1} If not found in db -> insert //
if( !$fetched ){
// {8.2} Insert new location //
$sqlr = DatabaseDriver::getPDO()->prepare("INSERT INTO location(id_location,id_event,basename,name) VALUES(DEFAULT,:ide,:bn,:n)");
$sqlr->execute([ ':ide' => $event_id, ':bn' => $read_location, ':n' => $read_location ]);
// {8.3} Store id in current location //
$location_id = DatabaseDriver::getPDO()->lastInsertId();
}else
$location_id = $fetched['id_location'];
}
/* (9) Check @course if already exists */ {
$date = [
'start' => date('Y-m-d H:i:s', $time_start),
'stop' => date('Y-m-d H:i:s', $time_stop)
];
$sqlr = DatabaseDriver::getPDO()->prepare("SELECT c.id_course as idc FROM course as c, event as e, location as l WHERE c.id_event = e.id_event AND c.id_location = l.id_location AND e.name = :ename AND l.name = :lname AND e.id_event = :ide AND l.id_location = :idl AND c.start_date = :startd AND c.stop_date = :stopd");
$sqlr->execute([ ':ename' => $read_name, ':lname' => $read_location, ':ide' => $event_id, ':idl' => $location_id, ':startd' => $date['start'], ':stopd' => $date['stop'] ]);
$fetched = $sqlr->fetch();
// {8.1} If not found in db -> insert //
if( !$fetched ){
// {8.2} Insert new location //
$sqlr = DatabaseDriver::getPDO()->prepare("INSERT INTO course(id_course,id_event,id_location,start_date,stop_date) VALUES(DEFAULT,:ide,:idl,:startd,:stopd)");
$sqlr->execute([ ':ide' => $event_id, ':idl' => $location_id, ':startd' => $date['start'], ':stopd' => $date['stop'] ]);
// {8.3} Store id in current location //
$course_id = DatabaseDriver::getPDO()->lastInsertId();
}else
$course_id = $fetched['idc'];
}
/* (9) Store local data for ics */
$this->event[$uid][$date_start][1] = $location_id;
}
}
}
}
}
/* (3) Exctracts an event using OCR
*
* @uid<String> Image uid
* @start<Array> Start rect (x, y)
* @stop<Array> Stop rect (x, y)
*
---------------------------------------------------------*/
public function extractEvent($uid, $start=null, $stop=null){
$link = __ROOT__."/tmp/$uid.jpeg";
$width = $stop[0]-$start[0];
$height = $stop[1]-$start[1];
$resize_factor = 2;
/* [1] Get the right clip
=========================================================*/ {
/* (1) Create clipped copy */
$clip = \imagecreatetruecolor($width*$resize_factor, $height*$resize_factor);
$copied = \imagecopyresized(
$clip, // destin img
$this->img_res, // source img
0, // dest x
0, // dest y
$start[0], // src x
$start[1], // src y
$width*$resize_factor, // dest w
$height*$resize_factor, // dest h
$width, // src w
$height // src h
);
/* (2) Manage copy error */
if( !$copied )
return [ 'name' => null, 'location' => null ];
/* (3) Save to jpeg */
\imagesavealpha($clip, true);
// ob_start();
\imagejpeg($clip, $link);
// $image_data = \base64_encode(ob_get_contents());
// ob_end_clean();
}
/* [2] Apply Tesseract
=========================================================*/ {
/* (1) Load image with tesseract */
try{
$tesseract = new Tesseract($link);
$read = $tesseract->read();
/* (2) Manage error */
}catch(\Exception $e){
// Remove file //
unlink($link);
return [ 'name' => null, 'location' => null ];
}
}
/* [3] End procedure
=========================================================*/
/* (1) Remove file */
unlink($link);
/* (2) Return read value */
return $read;
}
/* (3) Get a pixel's color
*
* @x<int> X coordinate
* @y<int> Y coordinate
*
* @return color<u_int32> Raw color
*
---------------------------------------------------------*/
private function getColor($x, $y){
return imagecolorat($this->img_res, $x, $y);
}
/* (4) Get time from a y-coordinate
*
* @day_n<int> Day relative index
* @y<int> y Coordinate
*
* @return time<int> GMT timestamp
*
---------------------------------------------------------*/
private function yToTime($day_n, $y){
/* [1] Get the date
=========================================================*/ {
/* (1) Get the day's date */
$day_ts = strtotime($this->start_d." + $day_n days");
/* (2) Format it */
$day = date('Ymd', $day_ts);
}
/* [2] Get the time
=========================================================*/{
/* (1) Get the time */
$time = self::$start_h + (self::$stop_h-self::$start_h) * ($y-self::$start_y) / (self::$stop_y-self::$start_y);
/* (2) Calculate hour form time */
$hour = floor($time);
/* (3) Calculate min from time */
$min = round( 60 * ($time-$hour) );
/* (4) Round minutes to 10min */
$min = round($min/10)*10;
/* (5) Format to 2-digit */
$hour = ( $hour < 10 ) ? "0$hour" : $hour;
$min = ( $min < 10 ) ? "0$min" : $min ;
}
/* [3] Convert to GMT (UTC+0)
=========================================================*/
/* (1) Set fixed timezone offset */
// $tz_offset = +2;
/* (2) Get GMT (UTC+0) timestamp */
$ts = strtotime("${day} $hour:$min:00");
/* (3) Return GMT date */
return $ts;
}
/* (5) Generate ICS output
*
* @return ics<String> ICS Representation of the events
*
---------------------------------------------------------*/
public function toIcs(){
$RAW = "";
/* [1] For each event
=========================================================*/
foreach($this->event as $event_col=>$events){
/* (2) For each event of each type
---------------------------------------------------------*/
foreach($events as $start_t=>$data){
/* (1) If a name -> Search if there is a correction in the database */
$sqlr = DatabaseDriver::getPDO()->prepare("SELECT e.id_event as ide, l.id_location as idl, l.id_location as idl, e.name as ename, l.name as lname FROM event as e, location as l WHERE e.id_event = l.id_event AND l.id_location= :idl");
$sqlr->execute([ ':idl' => $data[1] ]);
/* (2) Default values */
$ide = -1;
$idl = -1;
$name = '?';
$location = '?';
/* (2) If a match found -> set it */
if( ($fetched=$sqlr->fetch()) ){
$ide = $fetched['ide'];
$idl = $fetched['idl'];
$name = $fetched['ename'];
$location = $fetched['lname'];
}
/* (3) Build ICS event */
$RAW .= "BEGIN:VEVENT\n";
$RAW .= "DTSTAMP:".gmdate("Ymd\THis\Z", time())."\n"; // required
$RAW .= "DTSTART:${start_t}\n";
$RAW .= "DTEND:${data[0]}\n";
$RAW .= "UID:$start_t\n"; // required
$RAW .= "DESCRIPTION:event@$ide\n"; // event@id_event
$RAW .= "SUMMARY:$name\n";
$RAW .= "DESCRIPTION:location@$idl\n"; // location@id_location
$RAW .= "LOCATION:$location\n";
$RAW .= "CATEGORIES: UPPA Calendar\n";
$RAW .= "END:VEVENT\n";
}
}
return $RAW;
}
}