CrossKnowledge/SubConverterBundle

View on GitHub
Providers/Ttaf1Subtitles.php

Summary

Maintainability
C
1 day
Test Coverage
<?php

namespace CrossKnowledge\SubConverterBundle\Providers;

/**
 * TTAF1 subtitles class
 */
class Ttaf1Subtitles extends Subtitles
{
    /**
     * Path to XML template for export
     * @var string
     */
    public $template = null;

    /**
     * Subtitle set title
     * @var string
     */
    public $title = null;

    /**
     * Copyright info
     * @var string
     */
    public $copyright = null;

    /**
     * Return true if the provided file is in the current format
     * @param string $filename
     * @return boolean
     */
    public function checkFormat($filename)
    {
        $xml = self::loadXml($filename);
        if (empty($xml)) {
            return false;
        }

        if ($xml->getElementsByTagName('tt')->length != 1) {
            return false;
        }

        $xPath = new \DOMXPath($xml);
        $xPath->registerNamespace('x', $xml->lookupNamespaceUri($xml->namespaceURI));

        $bodyNodes = $xPath->query('/x:tt/x:body');
        if ($bodyNodes->length != 1) {
            return false;
        }

        return true;
    }

    /**
     * Import the provided file
     *
     * @param string $filename
     * @return Subtitles
     * @throws \Exception
     */
    public function import($filename)
    {
        if (!$this->checkFormat($filename)) {
            throw new \Exception("Invalid TTAF1 file: ".basename($filename));
        }

        $xml = self::loadXml($filename);

        $xPath = new \DOMXPath($xml);
        $xPath->registerNamespace('x', $xml->lookupNamespaceUri($xml->namespaceURI));

        $ttNode = $xml->getElementsByTagName('tt')->item(0);

        $fps = (int)$ttNode->getAttribute('frameRate');
        if (!empty($fps)) {
            $this->framerate = $fps;
        }

        $this->language = $ttNode->getAttribute('xml:lang');

        $metadataNodes = $xPath->query('/x:tt/x:head/x:metadata');
        $metadataNode = null;
        if ($metadataNodes->length == 1) {
            $metadataNode = $metadataNodes->item(0);
        }

        $bodyNodes = $xPath->query('/x:tt/x:body');
        $bodyNode = $bodyNodes->item(0);

        // Get title
        $titleNodes = $metadataNode->getElementsByTagName('title');
        if ($titleNodes->length == 1) {
            $this->title = $titleNodes->item(0)->textContent;
        }

        // Get copyright
        $titleNodes = $metadataNode->getElementsByTagName('copyright');
        if ($titleNodes->length == 1) {
            $this->copyright = $titleNodes->item(0)->textContent;
        }

        if (empty($this->framerate)) {
            // Default Framerate
            $fps = 25;
        } else {
            $fps = $this->framerate;
        }

        // Get subtitles
        $this->subtitles = array();
        $pNodes = $bodyNode->getElementsByTagName('p');
        foreach ($pNodes as $aPNode) {
            if (preg_match('/^([0-9]+)f/i', $aPNode->getAttribute('begin'), $matches)) {
                $from = $matches[1] / $fps;
            } else {
                throw new \Exception("Invalid begin value for slide ".$aPNode->getAttribute('xml:id'));
            }

            if (preg_match('/^([0-9]+)f/i', $aPNode->getAttribute('end'), $matches)) {
                $to = $matches[1] / $fps;
            } else {
                throw new \Exception("Invalid begin value for slide ".$aPNode->getAttribute('xml:id'));
            }

            $text = '';
            foreach ($aPNode->childNodes as $aTextNode) {
                $text .= $xml->saveXml($aTextNode);
            }

            $this->subtitles[] = array(
                'from' => $from,
                'to' => $to,
                'text' => trim(self::htmlToText($text), " \t\r\n"),
            );
        }

        return $this;
    }

    /**
     * Export the subtitles in the current format
     *
     * @param boolean $bom Add UTF-8 BOM
     * @return string
     * @throws \Exception
     */
    public function export($bom = false)
    {
        $templateDir = __DIR__ .'/../Resources/ttaf1_templates/';

        // Use template file
        if (!empty($this->template)) {
            // Predefined template ?
            $predefinedTemplateFile = $templateDir.strtolower($this->template).'.xml';
            if (file_exists($predefinedTemplateFile) && is_file($predefinedTemplateFile)) {
                $templateFile = $predefinedTemplateFile;
            } else {
                $templateFile = $this->template;
            }

            // Check if file exists
            if (!file_exists($templateFile) || !is_file($templateFile)) {
                throw new \Exception("The template file \"".basename($templateFile)."\" could not be found.");
            }

            // Check format
            if (!$this->checkFormat($templateFile)) {
                throw new \Exception("The template file \"".basename($templateFile)."\" is not a valid TTAF1 file.");
            }
        } else {
            $templateFile = $templateDir.'default.xml';
        }

        // Load template
        $xml = self::loadXml($templateFile);

        $xPath = new \DOMXPath($xml);
        $xPath->registerNamespace('x', $xml->lookupNamespaceUri($xml->namespaceURI));

        // Get framerate
        $fps = 25; // Assuming 25 FPS by default
        $ttNode = $xml->getElementsByTagName('tt')->item(0);

        $templateFps = (int)$ttNode->getAttribute('ttp:frameRate');
        if (!empty($templateFps)) {
            $fps = $templateFps;
        } else {
            if (!empty($this->framerate)) {
                $fps = $this->framerate;
            } else {
                file_put_contents("php://stderr", "Warning: No framerate specified for export, assuming 25 FPS.\n");
            }
        }

        // Set head
        $headNodes = $ttNode->getElementsByTagName('head');
        if ($headNodes->length > 0) {
            $headNode = $headNodes->item(0);
        } else {
            $headNode = $ttNode->appendChild(new \DOMElement('head'));
        }

        // Set metadata
        $metadataNodes = $headNode->getElementsByTagName('metadata');
        if ($metadataNodes->length > 0) {
            $metadataNode = $metadataNodes->item(0);
        } else {
            $metadataNode = $headNode->appendChild(new \DOMElement('metadata'));
            $metadataNode->setAttributeNS(
                'http://www.w3.org/2000/xmlns/',
                'xmlns:ttm',
                'http://www.w3.org/2006/10/ttaf1#metadata'
            );
        }

        // Set framerate
        $ttNode->setAttributeNS(
            'http://www.w3.org/2000/xmlns/',
            'xmlns:ttp',
            'http://www.w3.org/2006/10/ttaf1parameter'
        );
        $ttNode->setAttribute('ttp:frameRate', $fps);

        // Set language
        if (!empty($this->language)) {
            $ttNode->setAttribute('xml:lang', htmlspecialchars($this->language));
        }

        // Set title
        if (!empty($this->title)) {
            self::replaceNodeValue(
                $metadataNode,
                'ttm:title',
                htmlspecialchars($this->title),
                'http://www.w3.org/2006/10/ttaf1#metadata'
            );
        }

        // Set copyright
        if (!empty($this->copyright)) {
            self::replaceNodeValue(
                $metadataNode,
                'ttm:copyright',
                htmlspecialchars($this->copyright),
                'http://www.w3.org/2006/10/ttaf1#metadata'
            );
        }

        // Clear body
        $bodyNode = $xPath->query('/x:tt/x:body')->item(0);
        for ($i = $bodyNode->childNodes->length - 1; $i >= 0; $i--) {
            $bodyNode->removeChild($bodyNode->childNodes->item($i));
        }

        // Create subtitles container
        $containerNode = new \DOMElement('div');
        $bodyNode->appendChild($containerNode);

        // Add subtitles
        $i = 1;
        foreach ($this->subtitles as $row) {
            $subtitleXml = $xml->createDocumentFragment();
            $subtitleXml->appendXML(
                '<p xml:id="subtitle'.$i.'" begin="'.round($row['from'] * $fps).'f" end="'.round(
                    $row['to'] * $fps
                ).'f">'.htmlspecialchars($row['text']).'</p>'
            );
            $containerNode->appendChild($subtitleXml->firstChild);
            $i++;
        }

        // Return final XML
        if ($bom) {
            return self::addUtf8Bom($xml->saveXml());
        } else {
            return $xml->saveXml();
        }
    }

    /**
     * Return file extension for the current format
     * @return string
     */
    public function getFileExt()
    {
        return 'xml';
    }

    /**
     * Replace the value of the given node
     * @param \DOMNode $node
     * @param string $tagname
     * @param string $value
     * @param string $namespaceUri
     */
    protected static function replaceNodeValue($node, $tagname, $value, $namespaceUri = null)
    {
        if ($namespaceUri) {
            $newNode = $node->ownerDocument->createElementNS($namespaceUri, $tagname, $value);
            $nodes = $node->getElementsByTagNameNS($namespaceUri, preg_replace('/^.+\\:/', '', $tagname));
        } else {
            $newNode = $node->ownerDocument->createElement($tagname, $value);
            $nodes = $node->getElementsByTagName($tagname);
        }

        if ($nodes->length > 0) {
            $node->replaceChild($newNode, $nodes->item(0));
        } else {
            $node->appendChild($newNode);
        }
    }

    /**
     * Safely load an UTF-8 XML file, handling missing header.
     * @param string $filename
     * @return \DOMDocument
     */
    protected static function loadXml($filename)
    {
        $strXml = trim(self::forceUtf8(file_get_contents($filename)));
        if (!preg_match('/^[^<]*<\?xml /i', $strXml)) // Take care of this damn UTF-8 BOM
        {
            $strXml = '<?xml version="1.0" encoding="utf-8"?>'.$strXml;
        }

        $xml = new \DOMDocument('1.0', 'UTF-8');
        @$xml->loadXML($strXml);

        return $xml;
    }
}

?>